flashruntime 0.4.2__tar.gz → 0.4.3__tar.gz
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.
- {flashruntime-0.4.2 → flashruntime-0.4.3}/PKG-INFO +1 -1
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashml_workloads/fedavg_driver.py +30 -6
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/protocol/v1alpha1.py +30 -1
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/recipes/command.py +15 -6
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/scheduler/__init__.py +122 -7
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/service/modea.py +40 -18
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime.egg-info/PKG-INFO +1 -1
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime.egg-info/SOURCES.txt +4 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/pyproject.toml +13 -1
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_fedavg_driver.py +47 -0
- flashruntime-0.4.3/tests/test_placement_pool.py +170 -0
- flashruntime-0.4.3/tests/test_placement_trusted_argv.py +142 -0
- flashruntime-0.4.3/tests/test_protocol_pools.py +57 -0
- flashruntime-0.4.3/tests/test_recipe_pool.py +50 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/LICENSE +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/README.md +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashml_workloads/__init__.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashml_workloads/fedavg_weights.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashml_workloads/fedavg_worker.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashml_workloads/kmeans_driver.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashml_workloads/kmeans_shard.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashml_workloads/sgd_trainer.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashml_workloads/sharded_kmeans.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashml_workloads/sklearn_trial.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/__init__.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/artifacts/__init__.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/artifacts/store.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/backends/__init__.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/backends/base.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/backends/kuberay.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/checkpoint/__init__.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/checkpoint/catalog.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/checkpoint/local.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/checkpoint/store.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/integrations/__init__.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/integrations/huggingface.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/integrations/pytorch.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/integrations/sklearn.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/launchers/__init__.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/launchers/local.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/leases/__init__.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/leases/manager.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/leases/sqlite_store.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/leases/store.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/monitor/__init__.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/monitor/sampler.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/planner/__init__.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/planner/candidates.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/planner/catalog.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/planner/comm.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/planner/explain.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/planner/memory.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/planner/resolve.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/planner/selector.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/planner/timecost.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/profiling/__init__.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/protocol/__init__.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/protocol/plan_v1alpha1.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/providers/__init__.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/py.typed +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/recipes/__init__.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/recovery/__init__.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/recovery/policy.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/recovery/signals.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/recovery/taxonomy.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/sdk.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/service/__init__.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/service/app.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/service/auth.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/service/checkpoints.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/service/cli.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/service/dashboard.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/service/ledger.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/strategies/__init__.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/strategies/command.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/torch/__init__.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/__init__.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/_docs/benchmarks.html +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/_docs/concepts/architecture.html +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/_docs/get-started.html +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/_docs/guides/federated-averaging.html +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/_docs/guides/huggingface.html +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/_docs/guides/jobspec-and-isolation.html +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/_docs/guides/pytorch.html +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/_docs/guides/sklearn.html +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/_docs/index.html +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/_docs/reference/cli.html +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/_docs/reference/integrations.html +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/_docs/reference/sdk.html +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/_docs/reference/torch-helper.html +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/_docs/search-index.json +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/_docs/tutorials/convnet.html +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/_docs/tutorials/fault-tolerance.html +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/_docs/tutorials/sklearn-sweeps.html +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/flowmap.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/page.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/server.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/viewer/state.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/workloads/__init__.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime/workloads/command.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime.egg-info/dependency_links.txt +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime.egg-info/entry_points.txt +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime.egg-info/requires.txt +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/flashruntime.egg-info/top_level.txt +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/setup.cfg +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_auto_recovery.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_benchmarks.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_checkpoint.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_checkpoint_local.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_cli_submit.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_documentation.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_examples_e2e.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_fedavg_convergence.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_fedavg_weights.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_fedavg_worker.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_gpu_e2e.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_integrations.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_interfaces.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_kmeans_leases.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_kmeans_workload.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_kuberay_backend.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_launcher_local.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_leases.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_leases_scope.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_leases_sqlite.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_monitor_sampler.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_placement_exclusion.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_placement_gpu.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_placement_local_data.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_planner.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_protocol_evidence.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_protocol_gpu.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_protocol_local_datasets.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_protocol_v1alpha1.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_recipe_gpu.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_recipe_local_inputs.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_recipes_command.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_recovery.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_run_json.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_scheduler_isolation.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_sdk_submit.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_service_auth.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_service_auth_startup.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_service_checkpoints.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_service_command_recipe.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_service_delegation.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_service_fedavg.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_service_modea.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_service_write_scope.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_sgd_trainer.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_strategy_command.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_torch_helper.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_viewer_flowmap.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_viewer_page.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_viewer_server.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_viewer_state.py +0 -0
- {flashruntime-0.4.2 → flashruntime-0.4.3}/tests/test_workloads_command.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flashruntime
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Open fault-tolerant distributed ML runtime: strategy planning, job protocol, leases, checkpointing, and recovery across heterogeneous compute.
|
|
5
5
|
Author: Zolli Labs
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -142,7 +142,8 @@ class Coordinator(Protocol):
|
|
|
142
142
|
|
|
143
143
|
def _round_body(round_idx: int, num_shards: int, worker_params: dict,
|
|
144
144
|
weights_uri: str | None, lease_seconds: float,
|
|
145
|
-
image: str, isolation_tier: str, allow_fallback: bool
|
|
145
|
+
image: str, isolation_tier: str, allow_fallback: bool,
|
|
146
|
+
pool: str | None = None) -> dict:
|
|
146
147
|
params: dict[str, Any] = dict(worker_params)
|
|
147
148
|
params.update({"round": round_idx, "num_shards": num_shards,
|
|
148
149
|
"lease_seconds": lease_seconds})
|
|
@@ -153,7 +154,7 @@ def _round_body(round_idx: int, num_shards: int, worker_params: dict,
|
|
|
153
154
|
raise ValueError(
|
|
154
155
|
f"image must be 'repository:tag' with a pinned tag, got {image!r}"
|
|
155
156
|
)
|
|
156
|
-
|
|
157
|
+
body = {
|
|
157
158
|
"apiVersion": "flashml.dev/v1alpha1", "kind": "Job",
|
|
158
159
|
"metadata": {"name": f"fedavg-r{round_idx:03d}"},
|
|
159
160
|
"spec": {
|
|
@@ -163,6 +164,17 @@ def _round_body(round_idx: int, num_shards: int, worker_params: dict,
|
|
|
163
164
|
"workload": {"type": "federated_averaging", "parameters": params},
|
|
164
165
|
},
|
|
165
166
|
}
|
|
167
|
+
if pool is not None and pool != "any":
|
|
168
|
+
# Same rule as the four expander stamps in recipes/command.py and
|
|
169
|
+
# service/modea.py: absent stays absent, and "any" is never stamped
|
|
170
|
+
# explicitly — the pool placement gate treats a stamped "any" as an
|
|
171
|
+
# ordinary pool id, which would make the round's tasks unplaceable
|
|
172
|
+
# everywhere rather than placeable anywhere. `None` (the caller made
|
|
173
|
+
# no pool statement) and the literal `"any"` (the caller stated the
|
|
174
|
+
# public queue explicitly) both mean "leave `placement` absent" —
|
|
175
|
+
# the default `PlacementSpec.pool` picks up "any" on its own.
|
|
176
|
+
body["spec"]["placement"] = {"pool": pool}
|
|
177
|
+
return body
|
|
166
178
|
|
|
167
179
|
|
|
168
180
|
def _default_task_ids(num_shards: int) -> list[str]:
|
|
@@ -433,6 +445,7 @@ def run_fedavg(
|
|
|
433
445
|
image: str = DEFAULT_IMAGE,
|
|
434
446
|
isolation_tier: str = "standard",
|
|
435
447
|
allow_fallback: bool = False,
|
|
448
|
+
pool: str | None = None,
|
|
436
449
|
poll_attempts: int = 4,
|
|
437
450
|
poll_backoff_s: float = 0.5,
|
|
438
451
|
prior_job_ids: Sequence[tuple[int, str]] | None = None,
|
|
@@ -448,6 +461,17 @@ def run_fedavg(
|
|
|
448
461
|
the same "two places, each correct in isolation" shape as the task-module
|
|
449
462
|
allowlist drift that already caused an outage here.
|
|
450
463
|
|
|
464
|
+
`pool` is caller-settable for the same reason: the cloud control plane
|
|
465
|
+
owns pool ids, not this driver, so it must be free to say which pool (or
|
|
466
|
+
none) each round belongs to. `None` means the public queue, exactly as
|
|
467
|
+
before this parameter existed — every round of every run before pools
|
|
468
|
+
landed built a `placement`-less body, and that must stay byte-identical.
|
|
469
|
+
Round 0 and every later round go through the same `_round_body` call
|
|
470
|
+
inside this loop, so a pool named for round 0 travels to round 1, 2, …
|
|
471
|
+
without the caller repeating itself — the whole point being that pool
|
|
472
|
+
confinement must survive the round boundary, not just the round the job
|
|
473
|
+
was first submitted for.
|
|
474
|
+
|
|
451
475
|
`build_round` replaces how a round becomes a job. The default builds the
|
|
452
476
|
built-in `federated_averaging` body, whose tasks run
|
|
453
477
|
`flashml_workloads.fedavg_worker`. A caller that wants the *user's own*
|
|
@@ -457,9 +481,9 @@ def run_fedavg(
|
|
|
457
481
|
resume) is unchanged, because none of it depends on what ran inside the
|
|
458
482
|
round, only on the task ids it produced and the `metrics.json` /
|
|
459
483
|
`delta.json` pair each one committed. `worker_params`, `image`,
|
|
460
|
-
`isolation_tier`, `allow_fallback` and `lease_seconds` are
|
|
461
|
-
*default* builder and are ignored when `build_round` is
|
|
462
|
-
builder already knows all of it.
|
|
484
|
+
`isolation_tier`, `allow_fallback`, `pool` and `lease_seconds` are
|
|
485
|
+
inputs to the *default* builder and are ignored when `build_round` is
|
|
486
|
+
supplied — the builder already knows all of it.
|
|
463
487
|
|
|
464
488
|
`initial_weights` may be `{}`, and that is not the same as "start from
|
|
465
489
|
zeros": it means the driver holds no weights yet, so round 0's reduced
|
|
@@ -497,7 +521,7 @@ def run_fedavg(
|
|
|
497
521
|
plan: RoundPlan = {
|
|
498
522
|
"body": _round_body(r, num_shards, worker_params, weights_uri,
|
|
499
523
|
lease_seconds, image, isolation_tier,
|
|
500
|
-
allow_fallback),
|
|
524
|
+
allow_fallback, pool),
|
|
501
525
|
"task_ids": _default_task_ids(num_shards),
|
|
502
526
|
}
|
|
503
527
|
else:
|
|
@@ -96,7 +96,13 @@ class ResourcesSpec(BaseModel):
|
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
class PlacementSpec(BaseModel):
|
|
99
|
-
|
|
99
|
+
#: "any" (the default, and every pre-pools job) or a team-pool id minted
|
|
100
|
+
#: by the control plane. Until 0.4.3 this was a closed Literal of
|
|
101
|
+
#: infrastructure pool names that nothing ever read; widened when it
|
|
102
|
+
#: gained its first reader. Non-"any" makes every task of the job carry
|
|
103
|
+
#: payload["pool"] (recipes stamp it) and place only on nodes listing
|
|
104
|
+
#: that id in capabilities.pools.
|
|
105
|
+
pool: str = "any"
|
|
100
106
|
architectures: list[Literal["amd64", "arm64"]] = Field(default_factory=lambda: ["amd64"])
|
|
101
107
|
|
|
102
108
|
|
|
@@ -309,6 +315,15 @@ class NodeCapabilities(BaseModel):
|
|
|
309
315
|
#: this runtime cannot parse — never a guess. Placement reads the LENGTH
|
|
310
316
|
#: of this list and nothing else in v1.
|
|
311
317
|
gpus: list[GpuInfo] = Field(default_factory=list)
|
|
318
|
+
#: Team pools this node serves, as pool ids minted by the cloud control
|
|
319
|
+
#: plane. Stamped SERVER-SIDE by the cloud API's agent proxy from the
|
|
320
|
+
#: machine owner's memberships — an agent's self-reported value is
|
|
321
|
+
#: overwritten there, so the field is only as trustworthy as the
|
|
322
|
+
#: operator fronting registration. Empty means "serves no pool", and the
|
|
323
|
+
#: seventh placement gate refuses pool-scoped work (fail closed). NOT
|
|
324
|
+
#: the same thing as NodeRegistration.pool (singular), a deployment
|
|
325
|
+
#: profile label that predates teams; the two never interact.
|
|
326
|
+
pools: list[str] = Field(default_factory=list)
|
|
312
327
|
os: str = ""
|
|
313
328
|
architecture: str = ""
|
|
314
329
|
|
|
@@ -325,6 +340,14 @@ class NodeRegistration(BaseModel):
|
|
|
325
340
|
#: every already-deployed agent is excluded from argv work until it is
|
|
326
341
|
#: upgraded and explicitly opted in (security fields fail closed).
|
|
327
342
|
argv_capable: bool = False
|
|
343
|
+
#: The operator explicitly opted this host into executing POOL-SCOPED
|
|
344
|
+
#: argv payloads without a container (`flashnode work --runner trusted`).
|
|
345
|
+
#: Defaults False so every existing agent is excluded until its owner
|
|
346
|
+
#: opts in (security fields fail closed). Distinct from argv_capable,
|
|
347
|
+
#: which asserts the CONTAINERISED argv contract — this asserts the
|
|
348
|
+
#: opposite: no sandbox, trusted-pool work only. The scheduler requires
|
|
349
|
+
#: pool + allowFallback + this flag together; any one alone places nothing.
|
|
350
|
+
unsandboxed_argv_capable: bool = False
|
|
328
351
|
#: This node can run "module" (python -m <allowlisted module>) tasks.
|
|
329
352
|
#: Defaults True — unlike argv_capable this is an AVAILABILITY gate, not
|
|
330
353
|
#: a safety one: a module task placed on an incapable node just wastes
|
|
@@ -354,6 +377,12 @@ class NodeHeartbeat(BaseModel):
|
|
|
354
377
|
node_id: str
|
|
355
378
|
timestamp: datetime = Field(default_factory=utcnow)
|
|
356
379
|
status: Literal["online", "draining", "terminating"] = "online"
|
|
380
|
+
#: Optional pool-membership refresh, stamped by the cloud API on the
|
|
381
|
+
#: heartbeat proxy so joining or leaving a pool reaches placement
|
|
382
|
+
#: without an agent restart. None means "no statement" — the
|
|
383
|
+
#: coordinator keeps whatever registration said. A list (even empty)
|
|
384
|
+
#: replaces `capabilities.pools` wholesale. Agents never set this.
|
|
385
|
+
pools: list[str] | None = None
|
|
357
386
|
|
|
358
387
|
|
|
359
388
|
class NodeStatusView(BaseModel):
|
|
@@ -82,13 +82,16 @@ class CommandRecipe(WorkloadRecipe):
|
|
|
82
82
|
|
|
83
83
|
def expand(self, job_id: str, spec: JobSpec) -> list[TaskSpec]:
|
|
84
84
|
isolation_spec = spec.spec.isolation
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
#
|
|
88
|
-
#
|
|
85
|
+
pool = spec.spec.placement.pool
|
|
86
|
+
if isolation_spec.allowFallback and pool == "any":
|
|
87
|
+
# The waiver is only acceptable INSIDE a pool: members chose to
|
|
88
|
+
# trust each other, and the seventh gate confines the task to
|
|
89
|
+
# them. Without a pool it would let a submitter place arbitrary
|
|
90
|
+
# code on any opted-in host — the exact thing the original
|
|
91
|
+
# unconditional refusal existed to prevent.
|
|
89
92
|
raise ValueError(
|
|
90
|
-
"command jobs may not set isolation.allowFallback
|
|
91
|
-
"argv
|
|
93
|
+
"command jobs may not set isolation.allowFallback without "
|
|
94
|
+
"placement.pool — unsandboxed argv is confined to team pools"
|
|
92
95
|
)
|
|
93
96
|
if isolation_spec.tier != "sandboxed":
|
|
94
97
|
# Coordinator-side opt-in only: the operator running the pool
|
|
@@ -180,6 +183,12 @@ class CommandRecipe(WorkloadRecipe):
|
|
|
180
183
|
# exercising the key-missing branch, as unpack_inputs and
|
|
181
184
|
# local_inputs do.
|
|
182
185
|
payload["gpus"] = int(gpus)
|
|
186
|
+
if pool != "any":
|
|
187
|
+
# Same hop, same warning as local_inputs/gpus above: dropping
|
|
188
|
+
# this does NOT fail closed — the seventh gate would see a
|
|
189
|
+
# task requiring nothing and place it anywhere, carrying the
|
|
190
|
+
# waiver with it. Absent stays absent for "any".
|
|
191
|
+
payload["pool"] = pool
|
|
183
192
|
tasks.append(
|
|
184
193
|
TaskSpec(
|
|
185
194
|
task_id=task_id,
|
|
@@ -113,11 +113,36 @@ class IsolationAwarePlacement(PlacementPolicy):
|
|
|
113
113
|
|
|
114
114
|
A second, independent gate applies to tasks carrying an `argv` payload
|
|
115
115
|
(arbitrary user command lines): the claiming node must advertise
|
|
116
|
-
`argv_capable is True
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
`argv_capable is True` — the containerised argv contract, always
|
|
117
|
+
acceptable. This is checked BEFORE the isolation block's `allowFallback`
|
|
118
|
+
waiver below, and the waiver does not apply to it directly — a submitter
|
|
119
|
+
setting `allowFallback: true` waives the sandbox-tier requirement only,
|
|
120
|
+
never the argv-runner requirement on its own, or arbitrary argv could
|
|
121
|
+
land on a node with no argv runner at all.
|
|
122
|
+
|
|
123
|
+
A node lacking `argv_capable` has exactly one alternative: trusted-pool
|
|
124
|
+
placement, for a host whose OPERATOR opted it into running pool argv
|
|
125
|
+
work unsandboxed (`flashnode work --runner trusted`,
|
|
126
|
+
`NodeRegistration.unsandboxed_argv_capable`). Three legs, ALL required,
|
|
127
|
+
each `is`-checked, each fails closed — any one alone places nothing:
|
|
128
|
+
|
|
129
|
+
- The task is pool-scoped: `payload["pool"]` is a non-empty `str`.
|
|
130
|
+
- Its submitter waived the tier: `payload["isolation"]` is a `dict` and
|
|
131
|
+
its `allowFallback` is exactly `True` — the same waiver the isolation
|
|
132
|
+
block reads below, checked again here rather than assumed, because
|
|
133
|
+
this gate must not depend on evaluation order elsewhere.
|
|
134
|
+
- The node opted in: `unsandboxed_argv_capable is True` on the node view
|
|
135
|
+
— a truthy stand-in (`1`, `"true"`) does NOT count, matching every
|
|
136
|
+
other boolean capability in this class.
|
|
137
|
+
|
|
138
|
+
The pool leg here is a guard, not the boundary: the seventh gate below
|
|
139
|
+
independently confines the task to pool members regardless of this one.
|
|
140
|
+
This leg exists so that a waiver which somehow reached placement without
|
|
141
|
+
a pool — `CommandRecipe` now accepts `allowFallback` only when
|
|
142
|
+
`placement.pool` is set, coupling the waiver to a pool at submission
|
|
143
|
+
time, so nothing upstream should ever produce one without a pool — still
|
|
144
|
+
unlocks nothing here either. The gate does not trust another layer to
|
|
145
|
+
have already enforced the rule it is itself stating.
|
|
121
146
|
|
|
122
147
|
A third gate applies to tasks carrying a `module` payload (the
|
|
123
148
|
"python -m <allowlisted module>" tier): the claiming node must not be
|
|
@@ -254,14 +279,80 @@ class IsolationAwarePlacement(PlacementPolicy):
|
|
|
254
279
|
already holds the other half of a verification pair — and the point of
|
|
255
280
|
the pair is that the node cannot tell it is in one.
|
|
256
281
|
|
|
282
|
+
A seventh gate applies to tasks whose payload names a `pool`: the
|
|
283
|
+
claiming node's `capabilities.pools` must list it. It takes the
|
|
284
|
+
argv/local-data/gpu/exclude_nodes polarity — **fail closed** — and for
|
|
285
|
+
the sharpest reason of the seven: pool jobs are precisely the ones that
|
|
286
|
+
CARRY `allowFallback`, so a task that slipped this gate would not just
|
|
287
|
+
misplace, it would run UNSANDBOXED on a machine outside the trust
|
|
288
|
+
boundary the waiver assumed. Checked before the isolation block for the
|
|
289
|
+
same reason the gates above are — but here the ordering is load-bearing
|
|
290
|
+
twice over, since this gate and the waiver interact directly rather than
|
|
291
|
+
merely sharing a payload:
|
|
292
|
+
|
|
293
|
+
- The requirement must be a non-empty `str`. `None` means no pool was
|
|
294
|
+
asked for and the task places like any pre-pools job; anything else
|
|
295
|
+
typed (an `int`, a `list`, `True`, or the empty string) is a
|
|
296
|
+
type-confused requirement and makes the task ineligible everywhere,
|
|
297
|
+
exactly as a non-`int` `gpus` or a non-list `exclude_nodes` does.
|
|
298
|
+
- `capabilities` may be absent or type-confused; read as membership in
|
|
299
|
+
no pool rather than allowed to raise, via `isinstance`, not
|
|
300
|
+
`capabilities.get(...) or {}` — a string `capabilities` value has no
|
|
301
|
+
`.get` and must fail closed rather than crash the predicate, the same
|
|
302
|
+
pattern the gpu gate uses for the same reason.
|
|
303
|
+
- The advertisement must be a genuine *list* of names. Absent, `None`, a
|
|
304
|
+
bare string, a dict, or a bare `int` all count as serving no pool. A
|
|
305
|
+
bare string deserves the same suspicion `local_datasets` earns:
|
|
306
|
+
substring membership would let one node's advertisement quietly match
|
|
307
|
+
a pool it never joined.
|
|
308
|
+
- Every member must be a `str`. A `None`, an `int`, or a nested object
|
|
309
|
+
means the stamp was built wrong, and the pool it meant to serve is
|
|
310
|
+
exactly the one a plain membership test would then get wrong — the
|
|
311
|
+
same shape of failure `exclude_nodes` refuses for the same reason.
|
|
312
|
+
The whole node is refused rather than the one bad member ignored.
|
|
313
|
+
|
|
314
|
+
`allowFallback` does not waive this gate, and this is the one case in
|
|
315
|
+
the class where saying so is not enough — the test that pins it
|
|
316
|
+
(`test_allow_fallback_does_not_waive_the_pool_gate`) is the argument
|
|
317
|
+
itself, not a restatement of it. Every other gate's waiver-immunity is
|
|
318
|
+
"the waiver covers the sandbox tier and nothing else, so it has nothing
|
|
319
|
+
to say here." This gate's is stronger: pool-scoped jobs are the ones
|
|
320
|
+
that set `allowFallback: true` in the first place, trading the
|
|
321
|
+
sandboxed-container guarantee for a trusted-pool machine that runs their
|
|
322
|
+
argv directly. If this gate waived on `allowFallback`, the exact tasks
|
|
323
|
+
carrying that trade would be the ones allowed to escape the pool
|
|
324
|
+
boundary that made the trade acceptable, and they would land unsandboxed
|
|
325
|
+
on a stranger's machine — the design's worst failure mode, reached by
|
|
326
|
+
reading the one waiver already present on every task this gate exists to
|
|
327
|
+
confine.
|
|
328
|
+
|
|
257
329
|
Everything genuinely standard keeps the fail-open placement default."""
|
|
258
330
|
|
|
259
331
|
def eligible(self, task: TaskSpec, node: NodeView) -> bool:
|
|
260
332
|
# Checked before the allowFallback waiver below: the waiver relaxes
|
|
261
333
|
# the sandbox-tier requirement, and must never be readable as
|
|
262
334
|
# permission to run argv on a node with no argv runner.
|
|
263
|
-
if "argv" in task.payload
|
|
264
|
-
|
|
335
|
+
if "argv" in task.payload:
|
|
336
|
+
if node.get("argv_capable") is True:
|
|
337
|
+
pass # the containerised argv contract — always acceptable
|
|
338
|
+
else:
|
|
339
|
+
# Trusted-pool alternative: the host OPERATOR opted into
|
|
340
|
+
# unsandboxed pool argv (`flashnode work --runner trusted`).
|
|
341
|
+
# Three legs, each `is`-checked, each fails closed. The pool
|
|
342
|
+
# leg here is a guard, not the boundary — the seventh gate
|
|
343
|
+
# independently confines the task to pool members; this leg
|
|
344
|
+
# exists so a waiver that somehow escaped compile/recipe
|
|
345
|
+
# coupling still unlocks nothing outside a pool.
|
|
346
|
+
task_pool = task.payload.get("pool")
|
|
347
|
+
isolation_payload = task.payload.get("isolation")
|
|
348
|
+
if not (
|
|
349
|
+
isinstance(task_pool, str)
|
|
350
|
+
and task_pool
|
|
351
|
+
and isinstance(isolation_payload, dict)
|
|
352
|
+
and isolation_payload.get("allowFallback") is True
|
|
353
|
+
and node.get("unsandboxed_argv_capable") is True
|
|
354
|
+
):
|
|
355
|
+
return False
|
|
265
356
|
# Availability gate, mirrored polarity from the argv gate above —
|
|
266
357
|
# see the class docstring. An argv-only volunteer poisons every
|
|
267
358
|
# module job in the pool otherwise: it claims, ArgvDockerRunner
|
|
@@ -322,6 +413,30 @@ class IsolationAwarePlacement(PlacementPolicy):
|
|
|
322
413
|
return False # cannot answer "is this you?" ⇒ do not risk it
|
|
323
414
|
if node_id in excluded:
|
|
324
415
|
return False
|
|
416
|
+
# Fail-closed like every gate above, and checked before the
|
|
417
|
+
# allowFallback waiver below for the sharpest reason yet: pool jobs
|
|
418
|
+
# are exactly the ones that CARRY the waiver, so a pool task that
|
|
419
|
+
# slipped this gate would run unsandboxed on a machine outside the
|
|
420
|
+
# trust boundary that made the waiver acceptable.
|
|
421
|
+
required_pool = task.payload.get("pool")
|
|
422
|
+
if required_pool is not None:
|
|
423
|
+
if not isinstance(required_pool, str) or not required_pool:
|
|
424
|
+
return False # type-confused requirement ⇒ fail closed, no crash
|
|
425
|
+
capabilities = node.get("capabilities")
|
|
426
|
+
# isinstance, not `or {}` — a string capabilities value has no
|
|
427
|
+
# `.get` and must fail closed rather than crash the predicate.
|
|
428
|
+
advertised = (
|
|
429
|
+
capabilities.get("pools") if isinstance(capabilities, dict) else None
|
|
430
|
+
)
|
|
431
|
+
if not isinstance(advertised, list):
|
|
432
|
+
return False # absent/type-confused ⇒ serves no pool
|
|
433
|
+
if not all(isinstance(p, str) for p in advertised):
|
|
434
|
+
# A non-name member means the stamp was built wrong; the pool
|
|
435
|
+
# it meant to serve is precisely what a membership test would
|
|
436
|
+
# now get wrong. Refuse the node, not the one member.
|
|
437
|
+
return False
|
|
438
|
+
if required_pool not in advertised:
|
|
439
|
+
return False
|
|
325
440
|
isolation = task.payload.get("isolation")
|
|
326
441
|
if isolation is None:
|
|
327
442
|
return True # no isolation payload ⇒ standard, runs anywhere
|
|
@@ -171,6 +171,8 @@ def expand_tasks(job_id: str, spec: JobSpec) -> list[TaskSpec]:
|
|
|
171
171
|
}
|
|
172
172
|
if checkpoint is not None:
|
|
173
173
|
payload["checkpoint"] = checkpoint
|
|
174
|
+
if spec.spec.placement.pool != "any":
|
|
175
|
+
payload["pool"] = spec.spec.placement.pool
|
|
174
176
|
tasks.append(
|
|
175
177
|
TaskSpec(
|
|
176
178
|
task_id=task_id,
|
|
@@ -208,6 +210,17 @@ def _expand_kmeans(job_id: str, spec: JobSpec) -> list[TaskSpec]:
|
|
|
208
210
|
tasks = []
|
|
209
211
|
for i, shard_uri in enumerate(shards):
|
|
210
212
|
task_id = f"it{iteration:02d}-shard-{i:03d}"
|
|
213
|
+
payload = {
|
|
214
|
+
"module": "flashml_workloads.kmeans_shard",
|
|
215
|
+
"params": {"centroids": centroids},
|
|
216
|
+
"inputs": {"shard": shard_uri},
|
|
217
|
+
"output_prefix": f"jobs/{job_id}/{task_id}/",
|
|
218
|
+
"task_id": task_id,
|
|
219
|
+
"image": spec.spec.image.reference,
|
|
220
|
+
"isolation": isolation,
|
|
221
|
+
}
|
|
222
|
+
if spec.spec.placement.pool != "any":
|
|
223
|
+
payload["pool"] = spec.spec.placement.pool
|
|
211
224
|
tasks.append(
|
|
212
225
|
TaskSpec(
|
|
213
226
|
task_id=task_id,
|
|
@@ -215,15 +228,7 @@ def _expand_kmeans(job_id: str, spec: JobSpec) -> list[TaskSpec]:
|
|
|
215
228
|
commit_key=f"jobs/{job_id}/{task_id}/metrics.json",
|
|
216
229
|
max_attempts=spec.spec.retryPolicy.maxTaskAttempts,
|
|
217
230
|
lease_seconds=_lease_seconds(p, 60.0),
|
|
218
|
-
payload=
|
|
219
|
-
"module": "flashml_workloads.kmeans_shard",
|
|
220
|
-
"params": {"centroids": centroids},
|
|
221
|
-
"inputs": {"shard": shard_uri},
|
|
222
|
-
"output_prefix": f"jobs/{job_id}/{task_id}/",
|
|
223
|
-
"task_id": task_id,
|
|
224
|
-
"image": spec.spec.image.reference,
|
|
225
|
-
"isolation": isolation,
|
|
226
|
-
},
|
|
231
|
+
payload=payload,
|
|
227
232
|
)
|
|
228
233
|
)
|
|
229
234
|
return tasks
|
|
@@ -273,6 +278,17 @@ def _expand_fedavg(job_id: str, spec: JobSpec) -> list[TaskSpec]:
|
|
|
273
278
|
params = {k: p[k] for k in worker_keys}
|
|
274
279
|
params.update({"round": int(p.get("round", 0)),
|
|
275
280
|
"shard": shard, "num_shards": num_shards})
|
|
281
|
+
payload = {
|
|
282
|
+
"module": "flashml_workloads.fedavg_worker",
|
|
283
|
+
"params": params,
|
|
284
|
+
"inputs": inputs,
|
|
285
|
+
"output_prefix": f"jobs/{job_id}/{task_id}/",
|
|
286
|
+
"task_id": task_id,
|
|
287
|
+
"image": spec.spec.image.reference,
|
|
288
|
+
"isolation": isolation,
|
|
289
|
+
}
|
|
290
|
+
if spec.spec.placement.pool != "any":
|
|
291
|
+
payload["pool"] = spec.spec.placement.pool
|
|
276
292
|
tasks.append(
|
|
277
293
|
TaskSpec(
|
|
278
294
|
task_id=task_id,
|
|
@@ -280,15 +296,7 @@ def _expand_fedavg(job_id: str, spec: JobSpec) -> list[TaskSpec]:
|
|
|
280
296
|
commit_key=f"jobs/{job_id}/{task_id}/metrics.json",
|
|
281
297
|
max_attempts=spec.spec.retryPolicy.maxTaskAttempts,
|
|
282
298
|
lease_seconds=_lease_seconds(p, 120.0),
|
|
283
|
-
payload=
|
|
284
|
-
"module": "flashml_workloads.fedavg_worker",
|
|
285
|
-
"params": params,
|
|
286
|
-
"inputs": inputs,
|
|
287
|
-
"output_prefix": f"jobs/{job_id}/{task_id}/",
|
|
288
|
-
"task_id": task_id,
|
|
289
|
-
"image": spec.spec.image.reference,
|
|
290
|
-
"isolation": isolation,
|
|
291
|
-
},
|
|
299
|
+
payload=payload,
|
|
292
300
|
)
|
|
293
301
|
)
|
|
294
302
|
return tasks
|
|
@@ -633,6 +641,19 @@ def build_router(state: ModeAState) -> APIRouter:
|
|
|
633
641
|
if entry is None:
|
|
634
642
|
raise HTTPException(status_code=404, detail=f"unknown node {node_id} — register first")
|
|
635
643
|
entry.last_heartbeat = hb.timestamp
|
|
644
|
+
if hb.pools is not None:
|
|
645
|
+
# Server-stamped membership refresh (cloud proxy). A list, even
|
|
646
|
+
# empty, replaces the registration's pools wholesale; None means
|
|
647
|
+
# no statement. Replace the capabilities object rather than
|
|
648
|
+
# mutating it in place so the registration model stays the
|
|
649
|
+
# single source the claim node view dumps from.
|
|
650
|
+
entry.registration = entry.registration.model_copy(
|
|
651
|
+
update={
|
|
652
|
+
"capabilities": entry.registration.capabilities.model_copy(
|
|
653
|
+
update={"pools": list(hb.pools)}
|
|
654
|
+
)
|
|
655
|
+
}
|
|
656
|
+
)
|
|
636
657
|
return {"status": "ok"}
|
|
637
658
|
|
|
638
659
|
@router.get("/nodes")
|
|
@@ -657,6 +678,7 @@ def build_router(state: ModeAState) -> APIRouter:
|
|
|
657
678
|
"node_id": req.node_id,
|
|
658
679
|
"sandbox_capable": entry.registration.sandbox_capable,
|
|
659
680
|
"argv_capable": entry.registration.argv_capable,
|
|
681
|
+
"unsandboxed_argv_capable": entry.registration.unsandboxed_argv_capable,
|
|
660
682
|
"module_capable": entry.registration.module_capable,
|
|
661
683
|
# Names only — the host paths behind them never reach us, and the
|
|
662
684
|
# placement gate needs the names to place local-data work at all.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flashruntime
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Open fault-tolerant distributed ML runtime: strategy planning, job protocol, leases, checkpointing, and recovery across heterogeneous compute.
|
|
5
5
|
Author: Zolli Labs
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -121,13 +121,17 @@ tests/test_monitor_sampler.py
|
|
|
121
121
|
tests/test_placement_exclusion.py
|
|
122
122
|
tests/test_placement_gpu.py
|
|
123
123
|
tests/test_placement_local_data.py
|
|
124
|
+
tests/test_placement_pool.py
|
|
125
|
+
tests/test_placement_trusted_argv.py
|
|
124
126
|
tests/test_planner.py
|
|
125
127
|
tests/test_protocol_evidence.py
|
|
126
128
|
tests/test_protocol_gpu.py
|
|
127
129
|
tests/test_protocol_local_datasets.py
|
|
130
|
+
tests/test_protocol_pools.py
|
|
128
131
|
tests/test_protocol_v1alpha1.py
|
|
129
132
|
tests/test_recipe_gpu.py
|
|
130
133
|
tests/test_recipe_local_inputs.py
|
|
134
|
+
tests/test_recipe_pool.py
|
|
131
135
|
tests/test_recipes_command.py
|
|
132
136
|
tests/test_recovery.py
|
|
133
137
|
tests/test_run_json.py
|
|
@@ -5,6 +5,18 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "flashruntime"
|
|
8
|
+
# 0.4.3 adds team pools: NodeCapabilities.pools + NodeHeartbeat.pools
|
|
9
|
+
# (stamped server-side by the cloud proxy, never by agents),
|
|
10
|
+
# NodeRegistration.unsandboxed_argv_capable (operator opt-in, fail closed),
|
|
11
|
+
# PlacementSpec.pool widened from a dead closed Literal to str, the seventh
|
|
12
|
+
# placement gate, and the trusted-pool argv alternative. Fedavg round bodies
|
|
13
|
+
# carry placement.pool so pool confinement survives round boundaries.
|
|
14
|
+
# Additive and optional on the wire, so the compatibility range does not
|
|
15
|
+
# move — the bump is 0.4.1's rule again: bump on any wire-visible change,
|
|
16
|
+
# even additive.
|
|
17
|
+
# Agents need nothing from this release unless they opt into
|
|
18
|
+
# `--runner trusted` (flashnode 0.3.4, whose floor moves to >=0.4.3).
|
|
19
|
+
#
|
|
8
20
|
# 0.4.2 adds ExecutionEvidence and CompleteRequest.evidence — the agent's
|
|
9
21
|
# self-report at commit time. Additive and OPTIONAL on the wire, so the
|
|
10
22
|
# compatibility range does not move and an agent that predates it keeps
|
|
@@ -34,7 +46,7 @@ name = "flashruntime"
|
|
|
34
46
|
# must pin a range that includes it. (0.3.0 was the first published release;
|
|
35
47
|
# 0.1.0 and 0.2.0 were declared here but never reached PyPI.) Released from
|
|
36
48
|
# the Zolli-Labs/flashml monorepo by pushing a `flashruntime-vX.Y.Z` tag.
|
|
37
|
-
version = "0.4.
|
|
49
|
+
version = "0.4.3"
|
|
38
50
|
description = "Open fault-tolerant distributed ML runtime: strategy planning, job protocol, leases, checkpointing, and recovery across heterogeneous compute."
|
|
39
51
|
readme = "README.md"
|
|
40
52
|
# PEP 639 SPDX expression (string form) — replaces the deprecated
|
|
@@ -558,6 +558,53 @@ def test_image_and_isolation_tier_are_settable_by_the_caller():
|
|
|
558
558
|
assert spec["isolation"]["tier"] == "sandboxed"
|
|
559
559
|
|
|
560
560
|
|
|
561
|
+
def test_pool_travels_through_round_bodies_to_task_payloads():
|
|
562
|
+
"""Pool confinement must survive the round boundary.
|
|
563
|
+
|
|
564
|
+
The cloud API calls `run_fedavg` directly to drive every round of a
|
|
565
|
+
federated job — round 1+ bodies are built here, by `_round_body`, never
|
|
566
|
+
by `CommandRecipe.expand`. Without this stamp a pool-scoped job would
|
|
567
|
+
revert to `pool: "any"` on round 1, exactly the "unstamped hop" bug the
|
|
568
|
+
four expander stamps in recipes/command.py and service/modea.py exist to
|
|
569
|
+
prevent (tests/test_recipe_pool.py) — this is that same bug's fifth
|
|
570
|
+
hop, one `run_fedavg` alone can produce."""
|
|
571
|
+
from flashruntime.protocol.v1alpha1 import JobSpec
|
|
572
|
+
from flashruntime.service.modea import expand_tasks
|
|
573
|
+
|
|
574
|
+
fake = FakeCoordinator({0: [(0, 1.0, 10), (1, 1.0, 10)]})
|
|
575
|
+
run_fedavg(fake, rounds=1, num_shards=2, min_participants=2,
|
|
576
|
+
worker_params=_params(), pool="p-1",
|
|
577
|
+
initial_weights={"w": {"shape": [1], "data": [0.0]}})
|
|
578
|
+
body = fake.submitted[0][1]
|
|
579
|
+
assert body["spec"]["placement"]["pool"] == "p-1"
|
|
580
|
+
|
|
581
|
+
# The full round-boundary hop: what the driver submits is what the
|
|
582
|
+
# service actually expands, not a hand-built intermediate.
|
|
583
|
+
spec = JobSpec.model_validate(body)
|
|
584
|
+
tasks = expand_tasks("job-x", spec)
|
|
585
|
+
assert tasks, "expected at least one task"
|
|
586
|
+
assert all(t.payload["pool"] == "p-1" for t in tasks)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def test_no_pool_leaves_placement_absent_byte_compatible_with_today():
|
|
590
|
+
"""`pool=None` (the default) must reproduce exactly today's body — no
|
|
591
|
+
`placement` key at all, and no `pool` in any task payload."""
|
|
592
|
+
from flashruntime.protocol.v1alpha1 import JobSpec
|
|
593
|
+
from flashruntime.service.modea import expand_tasks
|
|
594
|
+
|
|
595
|
+
fake = FakeCoordinator({0: [(0, 1.0, 10), (1, 1.0, 10)]})
|
|
596
|
+
run_fedavg(fake, rounds=1, num_shards=2, min_participants=2,
|
|
597
|
+
worker_params=_params(),
|
|
598
|
+
initial_weights={"w": {"shape": [1], "data": [0.0]}})
|
|
599
|
+
body = fake.submitted[0][1]
|
|
600
|
+
assert "placement" not in body["spec"]
|
|
601
|
+
|
|
602
|
+
spec = JobSpec.model_validate(body)
|
|
603
|
+
tasks = expand_tasks("job-x", spec)
|
|
604
|
+
assert tasks, "expected at least one task"
|
|
605
|
+
assert all("pool" not in t.payload for t in tasks)
|
|
606
|
+
|
|
607
|
+
|
|
561
608
|
def test_an_image_without_a_tag_is_rejected():
|
|
562
609
|
fake = FakeCoordinator({0: [(0, 1.0, 10)]})
|
|
563
610
|
with pytest.raises(ValueError, match="repository:tag"):
|