mantatech-sdk 0.5b0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- examples/__init__.py +0 -0
- examples/mnist_cnn/README.md +107 -0
- examples/mnist_cnn/__init__.py +0 -0
- examples/mnist_cnn/_bootstrap.py +282 -0
- examples/mnist_cnn/driver.py +98 -0
- examples/mnist_cnn/notebook.py +231 -0
- examples/mnist_cnn/swarm.py +28 -0
- examples/mnist_cnn/worker.py +103 -0
- manta/__init__.light.py +22 -0
- manta/__init__.py +83 -0
- manta/__main__.py +21 -0
- manta/apis/__init__.py +7 -0
- manta/apis/async_user_api.py +6576 -0
- manta/apis/graph.py +498 -0
- manta/apis/module.py +362 -0
- manta/apis/results.py +251 -0
- manta/apis/swarm.py +207 -0
- manta/apis/user_api.py +1034 -0
- manta/cli/__init__.py +1 -0
- manta/cli/commands/__init__.py +1 -0
- manta/cli/commands/base_handler.py +229 -0
- manta/cli/commands/doc.py +192 -0
- manta/cli/commands/install.py +346 -0
- manta/cli/commands/sdk.py +9 -0
- manta/cli/commands/sdk_cluster.py +211 -0
- manta/cli/commands/sdk_config.py +347 -0
- manta/cli/commands/sdk_globals.py +280 -0
- manta/cli/commands/sdk_logs.py +174 -0
- manta/cli/commands/sdk_main.py +167 -0
- manta/cli/commands/sdk_module.py +516 -0
- manta/cli/commands/sdk_nodes.py +168 -0
- manta/cli/commands/sdk_original.py +3873 -0
- manta/cli/commands/sdk_results.py +265 -0
- manta/cli/commands/sdk_swarm.py +454 -0
- manta/cli/commands/sdk_user.py +234 -0
- manta/cli/commands/status.py +292 -0
- manta/cli/component_detector.py +112 -0
- manta/cli/config_manager.py +445 -0
- manta/cli/main.py +265 -0
- manta/cli/utils/__init__.py +27 -0
- manta/cli/utils/converters.py +140 -0
- manta/clients/_tls.py +41 -0
- manta/clients/cluster_management_client.py +491 -0
- manta/clients/local_client.py +149 -0
- manta/clients/module_management_client.py +222 -0
- manta/clients/swarm_management_client.py +590 -0
- manta/clients/user_management_client.py +400 -0
- manta/clients/world_client.py +195 -0
- manta/light/__init__.py +31 -0
- manta/light/globals.py +245 -0
- manta/light/local.py +407 -0
- manta/light/logging_config.py +39 -0
- manta/light/path.py +116 -0
- manta/light/results.py +236 -0
- manta/light/task.py +100 -0
- manta/light/utils.py +217 -0
- manta/light/world.py +177 -0
- mantatech_sdk-0.5b0.dist-info/METADATA +1041 -0
- mantatech_sdk-0.5b0.dist-info/RECORD +63 -0
- mantatech_sdk-0.5b0.dist-info/WHEEL +5 -0
- mantatech_sdk-0.5b0.dist-info/entry_points.txt +2 -0
- mantatech_sdk-0.5b0.dist-info/licenses/LICENSE +683 -0
- mantatech_sdk-0.5b0.dist-info/top_level.txt +2 -0
examples/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# MNIST CNN Example
|
|
2
|
+
|
|
3
|
+
Trains a CNN to classify handwritten digits from the MNIST dataset on a Manta node cluster.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- A Manta account
|
|
8
|
+
- Python 3.10+ and the manta SDK + examples bundle installed: `pip install --pre mantatech-sdk[examples]` (~700 MB CPU / ~2 GB CUDA — most of the weight is `torch` + `torchvision`)
|
|
9
|
+
- **Docker** running locally — the example uses the `ghcr.io/mantatech/manta-light:pytorch` image to execute the worker task
|
|
10
|
+
- Outbound network: gRPC to the manager (`:443`), MQTT-TLS to the broker (`:8883`), HTTPS to `ghcr.io` (image pull, first run only) and the MNIST mirror (dataset download, first run only)
|
|
11
|
+
|
|
12
|
+
> **`--pre` scope:** `pip install --pre` allows pre-release versions across the **whole** dependency tree, not just `mantatech-sdk`. To constrain pre-release resolution to this package only (recommended for reproducibility), use uv: `uv pip install --prerelease=only mantatech-sdk[examples]`.
|
|
13
|
+
|
|
14
|
+
By default the example **bootstraps a cluster + an in-process node for you** — no separate setup needed. If you already have a running cluster with attached nodes (e.g. via [manta-front](https://front.manta-tech.io)'s node wizard or `manta-deploy staging connect`), the example will use that instead.
|
|
15
|
+
|
|
16
|
+
## Files
|
|
17
|
+
|
|
18
|
+
| File | Where it runs | Purpose |
|
|
19
|
+
|------|--------------|---------|
|
|
20
|
+
| `worker.py` | Inside the Docker container on your node | Defines and trains the CNN |
|
|
21
|
+
| `swarm.py` | Driver (your machine) | Declares the task graph and hyperparameters |
|
|
22
|
+
| `driver.py` | Driver (your machine) | Orchestrates the full run via the Manta API |
|
|
23
|
+
| `notebook.py` | Driver (your machine) | Interactive Marimo notebook — same flow with a live UI |
|
|
24
|
+
| `_bootstrap.py` | Driver (your machine) | Helper: pick an existing RUNNING cluster, or bootstrap a fresh one with an embedded local node (example-internal, not a public SDK API) |
|
|
25
|
+
| `__init__.py` | — | Marks `examples.mnist_cnn` as a Python package so it ships with the wheel |
|
|
26
|
+
|
|
27
|
+
## Configuration
|
|
28
|
+
|
|
29
|
+
Set these environment variables before running:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
export MANTA_HOST=api.manta-tech.io # or your self-hosted manager host
|
|
33
|
+
export MANTA_PORT=443 # 443 for cloud, 50051 for local
|
|
34
|
+
|
|
35
|
+
# Either a JWT token (preferred — skips sign-in):
|
|
36
|
+
export MANTA_TOKEN=<your-jwt-token>
|
|
37
|
+
|
|
38
|
+
# Or email + password sign-in:
|
|
39
|
+
export MANTA_EMAIL=you@example.com
|
|
40
|
+
export MANTA_PASSWORD=yourpassword
|
|
41
|
+
|
|
42
|
+
# Optional — cluster selection:
|
|
43
|
+
export MANTA_CLUSTER_ID=<cluster-id> # pin to a specific RUNNING cluster
|
|
44
|
+
export MANTA_BOOTSTRAP=0 # disable the auto-bootstrap fallback
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The notebook UI surfaces credentials as fields. `MANTA_TOKEN` takes precedence; if it's set, email/password are ignored.
|
|
48
|
+
|
|
49
|
+
**Cluster resolution order**: a specific `MANTA_CLUSTER_ID` (if set) → the first RUNNING cluster you own with at least one node attached → a freshly bootstrapped cluster with an embedded local node (unless `MANTA_BOOTSTRAP=0`).
|
|
50
|
+
|
|
51
|
+
## Running
|
|
52
|
+
|
|
53
|
+
The example is bundled inside the SDK install (`mantatech-sdk[examples]`), so it works from any directory once installed.
|
|
54
|
+
|
|
55
|
+
**Script (terminal):**
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
python -m examples.mnist_cnn.driver
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The driver signs in, acquires a cluster (existing or freshly bootstrapped), deploys the swarm, polls until training finishes, and prints per-node accuracy and loss. A cluster the example bootstrapped is cleaned up on exit; one you already owned is left alone.
|
|
62
|
+
|
|
63
|
+
Expected output (3 epochs, 5 000 samples, seed 42):
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
Connecting to api.manta-tech.io:443 ...
|
|
67
|
+
Using cluster <id>
|
|
68
|
+
Swarm deployed: <id>
|
|
69
|
+
[ 10s] status: ACTIVE
|
|
70
|
+
...
|
|
71
|
+
[ 30s] status: ACTIVE
|
|
72
|
+
|
|
73
|
+
Results:
|
|
74
|
+
iteration=0 node=<node_id> accuracy=0.9646 loss=0.1107
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Interactive notebook (Marimo):**
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Marimo loads notebooks by path; resolve the installed location:
|
|
81
|
+
NOTEBOOK=$(python -c "import examples.mnist_cnn; print(examples.mnist_cnn.__path__[0])")/notebook.py
|
|
82
|
+
|
|
83
|
+
marimo run "$NOTEBOOK" # browser UI, read-only
|
|
84
|
+
marimo edit "$NOTEBOOK" # browser UI, editable cells
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The notebook provides credential inputs, hyperparameter sliders, a run button, a live status log, and a results table — no terminal required.
|
|
88
|
+
|
|
89
|
+
## Hyperparameters
|
|
90
|
+
|
|
91
|
+
Edit `swarm.py` to change training behaviour:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
self.set_global("hyperparameters", {
|
|
95
|
+
"epochs": 3,
|
|
96
|
+
"batch_size": 64,
|
|
97
|
+
"lr": 0.001,
|
|
98
|
+
"seed": 42, # controls dataset subset + weight init
|
|
99
|
+
"num_samples": 5000,
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The `seed` value makes results reproducible across runs.
|
|
104
|
+
|
|
105
|
+
## Reproducibility
|
|
106
|
+
|
|
107
|
+
Fixed seeds (`seed=42`, fixed `num_samples`, seeded DataLoader generator) make runs reproducible on the same node hardware. The example is not currently wired into CI; future integration tests can build on these seeds to assert a known accuracy value.
|
|
File without changes
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Embed a manta-node in the driver process so the example is self-contained.
|
|
2
|
+
|
|
3
|
+
Not a public SDK API — lives next to the example because a fresh Beta user
|
|
4
|
+
has no cluster + no nodes, so the canonical pattern (find a RUNNING cluster
|
|
5
|
+
via stream_clusters) errors fast with no recovery path. This helper creates
|
|
6
|
+
a cluster, embeds a Node in the current event loop, and yields once the
|
|
7
|
+
node has registered with the manager.
|
|
8
|
+
|
|
9
|
+
Requires Docker on the host machine — the embedded node still spawns a
|
|
10
|
+
container per task (manta-light:pytorch image). See the example README.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import shutil
|
|
19
|
+
import socket
|
|
20
|
+
import ssl
|
|
21
|
+
import tempfile
|
|
22
|
+
import time
|
|
23
|
+
from contextlib import asynccontextmanager
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import AsyncIterator, Tuple
|
|
26
|
+
|
|
27
|
+
from manta.apis import AsyncUserAPI
|
|
28
|
+
from manta_node import Node
|
|
29
|
+
from manta_node.infrastructure.config import NodeConfigManager
|
|
30
|
+
|
|
31
|
+
_logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
_DEFAULT_TIMEOUT_S = 60
|
|
34
|
+
_STOP_TIMEOUT_S = 10
|
|
35
|
+
_TLS_READY_TIMEOUT_S = 120.0
|
|
36
|
+
_TLS_RETRY_S = 5.0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _wait_for_tls_ready_sync(host: str, port: int, timeout: float) -> None:
|
|
40
|
+
"""Block until a TLS handshake to host:port succeeds with the system CA bundle.
|
|
41
|
+
|
|
42
|
+
cert-manager provisions a real TLS cert lazily for fresh per-cluster
|
|
43
|
+
subdomains — there's a window where the ingress serves a self-signed
|
|
44
|
+
fallback. Polling here avoids the Node startup race that would otherwise
|
|
45
|
+
surface as a misleading "JWT_SECRET mismatch" error from
|
|
46
|
+
`node_orchestrator.check_manager_availability`.
|
|
47
|
+
"""
|
|
48
|
+
ctx = ssl.create_default_context()
|
|
49
|
+
deadline = time.monotonic() + timeout
|
|
50
|
+
attempt = 0
|
|
51
|
+
last_exc: BaseException = Exception("no attempt yet")
|
|
52
|
+
while True:
|
|
53
|
+
attempt += 1
|
|
54
|
+
remaining = deadline - time.monotonic()
|
|
55
|
+
if remaining <= 0:
|
|
56
|
+
raise TimeoutError(
|
|
57
|
+
f"TLS readiness probe timed out after {timeout:.0f}s waiting for "
|
|
58
|
+
f"{host}:{port} to serve a trusted certificate. Last error: {last_exc!r}"
|
|
59
|
+
)
|
|
60
|
+
sock_timeout = min(10.0, remaining)
|
|
61
|
+
try:
|
|
62
|
+
with socket.create_connection((host, port), timeout=sock_timeout) as raw:
|
|
63
|
+
with ctx.wrap_socket(raw, server_hostname=host):
|
|
64
|
+
return
|
|
65
|
+
except ssl.SSLCertVerificationError as exc:
|
|
66
|
+
last_exc = exc
|
|
67
|
+
except OSError as exc:
|
|
68
|
+
last_exc = exc
|
|
69
|
+
print(f" waiting for TLS readiness: {host}:{port} (attempt {attempt})...")
|
|
70
|
+
time.sleep(min(_TLS_RETRY_S, max(0.0, deadline - time.monotonic())))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _render_node_toml(
|
|
74
|
+
*, alias: str, manager_host: str, manager_port: int, jwt_token: str
|
|
75
|
+
) -> str:
|
|
76
|
+
return f"""# Auto-generated by mnist_cnn bootstrap — do not edit.
|
|
77
|
+
|
|
78
|
+
[identity]
|
|
79
|
+
alias = {json.dumps(alias)}
|
|
80
|
+
secured_token = {json.dumps(jwt_token)}
|
|
81
|
+
random_id = false
|
|
82
|
+
|
|
83
|
+
[metadata]
|
|
84
|
+
name = {json.dumps(alias)}
|
|
85
|
+
|
|
86
|
+
[network]
|
|
87
|
+
manager_host = {json.dumps(manager_host)}
|
|
88
|
+
manager_port = {manager_port}
|
|
89
|
+
|
|
90
|
+
[logging]
|
|
91
|
+
level = "INFO"
|
|
92
|
+
log_to_file = true
|
|
93
|
+
log_to_console = false
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
_DEFAULT_CLUSTER_NAME = "manta-sdk-mnist-cnn-example"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def _sweep_prior_clusters(api: AsyncUserAPI, name: str) -> int:
|
|
101
|
+
"""Stop + delete any existing clusters with this exact name.
|
|
102
|
+
|
|
103
|
+
Idempotency over orphans: the example's create-or-reset model assumes
|
|
104
|
+
any prior cluster with our reserved name is stale (the embedded node
|
|
105
|
+
that was attached to it died with the previous process). Sweep them
|
|
106
|
+
before creating a fresh one to avoid the manager surfacing them as
|
|
107
|
+
healthy-but-actually-dead.
|
|
108
|
+
"""
|
|
109
|
+
swept = 0
|
|
110
|
+
async for cluster in api.stream_clusters():
|
|
111
|
+
if cluster.name != name:
|
|
112
|
+
continue
|
|
113
|
+
try:
|
|
114
|
+
await api.stop_cluster(cluster.cluster_id)
|
|
115
|
+
except Exception as exc:
|
|
116
|
+
_logger.debug("stop_cluster(%s) failed during sweep: %s", cluster.cluster_id, exc)
|
|
117
|
+
try:
|
|
118
|
+
await api.delete_cluster(cluster.cluster_id)
|
|
119
|
+
swept += 1
|
|
120
|
+
print(f" swept prior example cluster {cluster.cluster_id}")
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
_logger.warning(
|
|
123
|
+
"delete_cluster(%s) failed during sweep: %s — continuing",
|
|
124
|
+
cluster.cluster_id,
|
|
125
|
+
exc,
|
|
126
|
+
)
|
|
127
|
+
return swept
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@asynccontextmanager
|
|
131
|
+
async def bootstrap_local_node(
|
|
132
|
+
api: AsyncUserAPI,
|
|
133
|
+
*,
|
|
134
|
+
cluster_name: str = _DEFAULT_CLUSTER_NAME,
|
|
135
|
+
node_alias: str = "local-node",
|
|
136
|
+
wait_timeout_s: int = _DEFAULT_TIMEOUT_S,
|
|
137
|
+
) -> AsyncIterator[Tuple[str, str]]:
|
|
138
|
+
"""Create a cluster, embed a Node in the current loop, yield (cluster_id, node_id).
|
|
139
|
+
|
|
140
|
+
Idempotent across runs: any prior cluster with this exact name is
|
|
141
|
+
swept before creating a new one (the previous embedded node is dead
|
|
142
|
+
by the time we run again). On exit: stops the node, deletes the
|
|
143
|
+
cluster, removes the temp config.
|
|
144
|
+
"""
|
|
145
|
+
work_dir = Path(tempfile.mkdtemp(prefix="manta-mnist-bootstrap-"))
|
|
146
|
+
config_path = work_dir / "node.toml"
|
|
147
|
+
cluster_id: str | None = None
|
|
148
|
+
node: Node | None = None
|
|
149
|
+
node_task: asyncio.Task | None = None
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
await _sweep_prior_clusters(api, cluster_name)
|
|
153
|
+
cluster_id = await api.create_cluster(name=cluster_name)
|
|
154
|
+
start = await api.start_cluster(cluster_id)
|
|
155
|
+
manager_host = getattr(start, "node_host", None)
|
|
156
|
+
manager_port = getattr(start, "node_port", None)
|
|
157
|
+
jwt_token = getattr(start, "jwt_token", None)
|
|
158
|
+
if not (manager_host and manager_port and jwt_token):
|
|
159
|
+
raise RuntimeError(
|
|
160
|
+
"start_cluster did not return node_host/node_port/jwt_token"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
config_path.write_text(
|
|
164
|
+
_render_node_toml(
|
|
165
|
+
alias=node_alias,
|
|
166
|
+
manager_host=manager_host,
|
|
167
|
+
manager_port=int(manager_port),
|
|
168
|
+
jwt_token=jwt_token,
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
config_path.chmod(0o600)
|
|
172
|
+
|
|
173
|
+
# Wait for cert-manager to provision a trusted TLS cert for the
|
|
174
|
+
# per-cluster watchdog subdomain before starting the node. Without
|
|
175
|
+
# this, Node.check_manager_availability() races the cert issuance
|
|
176
|
+
# and dies with a misleading "JWT_SECRET mismatch" error.
|
|
177
|
+
await asyncio.to_thread(
|
|
178
|
+
_wait_for_tls_ready_sync,
|
|
179
|
+
manager_host,
|
|
180
|
+
int(manager_port),
|
|
181
|
+
_TLS_READY_TIMEOUT_S,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
mgr = NodeConfigManager(config_path)
|
|
185
|
+
node = Node(mgr)
|
|
186
|
+
|
|
187
|
+
# Node.start() runs forever — drive it as a task, use node registration
|
|
188
|
+
# at the manager as the readiness signal.
|
|
189
|
+
node_task = asyncio.create_task(node.start(), name="manta-node-embedded")
|
|
190
|
+
|
|
191
|
+
deadline = time.monotonic() + wait_timeout_s
|
|
192
|
+
node_id: str | None = None
|
|
193
|
+
while True:
|
|
194
|
+
if node_task.done():
|
|
195
|
+
exc = node_task.exception()
|
|
196
|
+
if exc:
|
|
197
|
+
raise exc
|
|
198
|
+
raise RuntimeError("Node.start() returned before registration")
|
|
199
|
+
node_ids = await api.list_node_ids(cluster_id)
|
|
200
|
+
if node_ids:
|
|
201
|
+
node_id = node_ids[0]
|
|
202
|
+
break
|
|
203
|
+
if time.monotonic() > deadline:
|
|
204
|
+
raise TimeoutError(
|
|
205
|
+
f"Embedded node did not register within {wait_timeout_s}s"
|
|
206
|
+
)
|
|
207
|
+
await asyncio.sleep(1)
|
|
208
|
+
|
|
209
|
+
yield cluster_id, node_id
|
|
210
|
+
|
|
211
|
+
finally:
|
|
212
|
+
if node is not None:
|
|
213
|
+
try:
|
|
214
|
+
await asyncio.wait_for(node.stop(), timeout=_STOP_TIMEOUT_S)
|
|
215
|
+
except (asyncio.TimeoutError, Exception) as exc:
|
|
216
|
+
_logger.warning("node.stop() failed: %s", exc)
|
|
217
|
+
if node_task is not None and not node_task.done():
|
|
218
|
+
node_task.cancel()
|
|
219
|
+
try:
|
|
220
|
+
await node_task
|
|
221
|
+
except (asyncio.CancelledError, Exception):
|
|
222
|
+
pass
|
|
223
|
+
if cluster_id:
|
|
224
|
+
try:
|
|
225
|
+
await api.stop_cluster(cluster_id)
|
|
226
|
+
await api.delete_cluster(cluster_id)
|
|
227
|
+
except Exception as exc:
|
|
228
|
+
_logger.warning("cluster cleanup failed: %s", exc)
|
|
229
|
+
shutil.rmtree(work_dir, ignore_errors=True)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@asynccontextmanager
|
|
233
|
+
async def acquire_cluster(
|
|
234
|
+
api: AsyncUserAPI,
|
|
235
|
+
*,
|
|
236
|
+
requested_cluster_id: str | None = None,
|
|
237
|
+
allow_bootstrap: bool = True,
|
|
238
|
+
) -> AsyncIterator[str]:
|
|
239
|
+
"""Pick a usable cluster.
|
|
240
|
+
|
|
241
|
+
Resolution order:
|
|
242
|
+
1. If `requested_cluster_id` is set, use that cluster (must be RUNNING
|
|
243
|
+
with at least one node attached). No bootstrap, no cleanup on exit.
|
|
244
|
+
2. Else, if `allow_bootstrap`, sweep any prior auto-bootstrapped cluster
|
|
245
|
+
and bootstrap a fresh one with an embedded local node. Cleaned up
|
|
246
|
+
on exit.
|
|
247
|
+
|
|
248
|
+
The "auto-pick an existing RUNNING cluster" fallback was deliberately
|
|
249
|
+
removed: it produced confusing behaviour when a previous run's embedded
|
|
250
|
+
node had died but its cluster + stale node entry still existed
|
|
251
|
+
server-side, causing the example to silently deploy onto a dead node.
|
|
252
|
+
Power users with their own running cluster should set MANTA_CLUSTER_ID.
|
|
253
|
+
"""
|
|
254
|
+
if requested_cluster_id:
|
|
255
|
+
async for cluster in api.stream_clusters():
|
|
256
|
+
if cluster.cluster_id != requested_cluster_id:
|
|
257
|
+
continue
|
|
258
|
+
if str(cluster.status) != "RUNNING":
|
|
259
|
+
raise RuntimeError(
|
|
260
|
+
f"Cluster {requested_cluster_id} status={cluster.status!s} — must be RUNNING."
|
|
261
|
+
)
|
|
262
|
+
node_ids = await api.list_node_ids(cluster.cluster_id)
|
|
263
|
+
if not node_ids:
|
|
264
|
+
raise RuntimeError(
|
|
265
|
+
f"Cluster {cluster.cluster_id} has 0 nodes attached. "
|
|
266
|
+
"Attach a node before running the example."
|
|
267
|
+
)
|
|
268
|
+
yield cluster.cluster_id
|
|
269
|
+
return
|
|
270
|
+
raise RuntimeError(
|
|
271
|
+
f"No cluster found matching MANTA_CLUSTER_ID={requested_cluster_id}."
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if not allow_bootstrap:
|
|
275
|
+
raise RuntimeError(
|
|
276
|
+
"MANTA_BOOTSTRAP=0 is set and no MANTA_CLUSTER_ID provided. "
|
|
277
|
+
"Set MANTA_CLUSTER_ID to a RUNNING cluster you've already prepared, "
|
|
278
|
+
"or re-enable bootstrap."
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
async with bootstrap_local_node(api) as (cluster_id, _node_id):
|
|
282
|
+
yield cluster_id
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from manta.apis import AsyncUserAPI
|
|
6
|
+
from manta_common.conversions import ID, bytes_to_dict
|
|
7
|
+
|
|
8
|
+
from examples.mnist_cnn._bootstrap import acquire_cluster
|
|
9
|
+
from examples.mnist_cnn.swarm import MnistCnnSwarm
|
|
10
|
+
|
|
11
|
+
POLL_INTERVAL_S = 10
|
|
12
|
+
TIMEOUT_S = 600
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def main():
|
|
16
|
+
host = os.environ.get("MANTA_HOST", "localhost")
|
|
17
|
+
port = int(os.environ.get("MANTA_PORT", "50051"))
|
|
18
|
+
token = os.environ.get("MANTA_TOKEN")
|
|
19
|
+
email = os.environ.get("MANTA_EMAIL")
|
|
20
|
+
password = os.environ.get("MANTA_PASSWORD")
|
|
21
|
+
|
|
22
|
+
if not token and not (email and password):
|
|
23
|
+
print(
|
|
24
|
+
"Error: set MANTA_TOKEN (preferred) or MANTA_EMAIL + MANTA_PASSWORD."
|
|
25
|
+
)
|
|
26
|
+
sys.exit(1)
|
|
27
|
+
|
|
28
|
+
print(f"Connecting to {host}:{port} ...")
|
|
29
|
+
|
|
30
|
+
requested_cluster_id = os.environ.get("MANTA_CLUSTER_ID")
|
|
31
|
+
allow_bootstrap = os.environ.get("MANTA_BOOTSTRAP", "1") != "0"
|
|
32
|
+
|
|
33
|
+
if token:
|
|
34
|
+
api = AsyncUserAPI(token=token, host=host, port=port)
|
|
35
|
+
else:
|
|
36
|
+
api = await AsyncUserAPI.sign_in(email, password, host=host, port=port)
|
|
37
|
+
|
|
38
|
+
async with api:
|
|
39
|
+
async with acquire_cluster(
|
|
40
|
+
api,
|
|
41
|
+
requested_cluster_id=requested_cluster_id,
|
|
42
|
+
allow_bootstrap=allow_bootstrap,
|
|
43
|
+
) as cluster_id:
|
|
44
|
+
print(f"Using cluster {cluster_id}")
|
|
45
|
+
|
|
46
|
+
swarm_proto = await api.deploy_swarm(cluster_id, MnistCnnSwarm())
|
|
47
|
+
swarm_id = ID(swarm_proto.swarm_id).xid
|
|
48
|
+
print(f"Swarm deployed: {swarm_id}")
|
|
49
|
+
|
|
50
|
+
# Single-shot training: poll for results to appear, with the swarm
|
|
51
|
+
# status as a backstop in case training fails. We don't wait for
|
|
52
|
+
# the swarm itself to transition to STOPPED — SwarmStatusEnum has
|
|
53
|
+
# no terminal "completed" state for one-off tasks.
|
|
54
|
+
elapsed = 0
|
|
55
|
+
result_obj = None
|
|
56
|
+
while elapsed < TIMEOUT_S:
|
|
57
|
+
await asyncio.sleep(POLL_INTERVAL_S)
|
|
58
|
+
elapsed += POLL_INTERVAL_S
|
|
59
|
+
results = await api.select_results(swarm_id, "metrics")
|
|
60
|
+
result_obj = results.get(swarm_id)
|
|
61
|
+
if result_obj and result_obj.values:
|
|
62
|
+
break
|
|
63
|
+
current = await api.get_swarm(swarm_id)
|
|
64
|
+
status = str(current.status)
|
|
65
|
+
print(f" [{elapsed:>4}s] status: {status}")
|
|
66
|
+
if status == "KILLED":
|
|
67
|
+
print(f"Swarm ended in KILLED state — training failed.")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
if not (result_obj and result_obj.values):
|
|
71
|
+
print(f"Timed out after {elapsed}s without receiving any results.")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
print("\nResults:")
|
|
75
|
+
for i, iter_data in enumerate(result_obj.values):
|
|
76
|
+
for node_id, row in zip(result_obj.schema.rows, iter_data):
|
|
77
|
+
column_data = dict(zip(result_obj.schema.columns, row))
|
|
78
|
+
# The "metrics" column holds the serialized worker payload
|
|
79
|
+
# (the dict the worker stored via self.world.results.add).
|
|
80
|
+
payload = column_data.get("metrics")
|
|
81
|
+
if isinstance(payload, (bytes, bytearray)):
|
|
82
|
+
metrics = bytes_to_dict(payload)
|
|
83
|
+
elif isinstance(payload, dict):
|
|
84
|
+
metrics = payload
|
|
85
|
+
else:
|
|
86
|
+
metrics = {}
|
|
87
|
+
acc = metrics.get("accuracy")
|
|
88
|
+
loss = metrics.get("loss")
|
|
89
|
+
acc_s = f"{acc:.4f}" if isinstance(acc, (int, float)) else "?"
|
|
90
|
+
loss_s = f"{loss:.4f}" if isinstance(loss, (int, float)) else "?"
|
|
91
|
+
print(
|
|
92
|
+
f" iteration={i} node={node_id} "
|
|
93
|
+
f"accuracy={acc_s} loss={loss_s}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
asyncio.run(main())
|