flashruntime 0.5.0__tar.gz → 0.6.0__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.5.0/flashruntime.egg-info → flashruntime-0.6.0}/PKG-INFO +1 -1
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/protocol/v1alpha1.py +11 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/recipes/command.py +37 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/scheduler/__init__.py +92 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0/flashruntime.egg-info}/PKG-INFO +1 -1
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime.egg-info/SOURCES.txt +3 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/pyproject.toml +1 -1
- flashruntime-0.6.0/tests/test_placement_datasets.py +354 -0
- flashruntime-0.6.0/tests/test_protocol_datasets.py +28 -0
- flashruntime-0.6.0/tests/test_recipe_datasets.py +95 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/LICENSE +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/README.md +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashml_workloads/__init__.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashml_workloads/chunks.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashml_workloads/fedavg_driver.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashml_workloads/fedavg_weights.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashml_workloads/fedavg_worker.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashml_workloads/kmeans_driver.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashml_workloads/kmeans_shard.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashml_workloads/sgd_trainer.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashml_workloads/sharded_kmeans.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashml_workloads/sklearn_trial.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/__init__.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/artifacts/__init__.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/artifacts/store.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/backends/__init__.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/backends/base.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/backends/kuberay.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/checkpoint/__init__.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/checkpoint/catalog.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/checkpoint/local.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/checkpoint/store.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/envelope.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/images/TAG +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/images/__init__.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/images/python-slim.txt +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/images/pytorch-cpu.txt +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/images/pytorch-cuda.txt +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/images/sklearn.txt +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/integrations/__init__.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/integrations/huggingface.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/integrations/pytorch.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/integrations/sklearn.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/launchers/__init__.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/launchers/local.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/leases/__init__.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/leases/manager.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/leases/sqlite_store.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/leases/store.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/monitor/__init__.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/monitor/sampler.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/planner/__init__.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/planner/candidates.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/planner/catalog.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/planner/comm.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/planner/explain.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/planner/memory.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/planner/resolve.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/planner/selector.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/planner/timecost.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/profiling/__init__.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/protocol/__init__.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/protocol/plan_v1alpha1.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/providers/__init__.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/py.typed +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/recipes/__init__.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/recipes/partition.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/recovery/__init__.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/recovery/policy.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/recovery/signals.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/recovery/taxonomy.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/reduce.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/retention.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/sdk.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/service/__init__.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/service/app.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/service/auth.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/service/checkpoints.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/service/cli.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/service/dashboard.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/service/ledger.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/service/modea.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/strategies/__init__.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/strategies/command.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/torch/__init__.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/validators.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/__init__.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/_docs/benchmarks.html +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/_docs/concepts/architecture.html +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/_docs/get-started.html +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/_docs/guides/federated-averaging.html +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/_docs/guides/huggingface.html +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/_docs/guides/jobspec-and-isolation.html +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/_docs/guides/pytorch.html +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/_docs/guides/sklearn.html +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/_docs/index.html +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/_docs/reference/cli.html +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/_docs/reference/integrations.html +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/_docs/reference/sdk.html +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/_docs/reference/torch-helper.html +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/_docs/search-index.json +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/_docs/tutorials/convnet.html +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/_docs/tutorials/fault-tolerance.html +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/_docs/tutorials/sklearn-sweeps.html +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/flowmap.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/page.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/server.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/viewer/state.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/workloads/__init__.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime/workloads/command.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime.egg-info/dependency_links.txt +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime.egg-info/entry_points.txt +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime.egg-info/requires.txt +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/flashruntime.egg-info/top_level.txt +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/setup.cfg +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_auto_recovery.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_benchmarks.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_checkpoint.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_checkpoint_local.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_chunks.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_cli_submit.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_cost_benchmark.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_declared_validators.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_documentation.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_envelope.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_examples_e2e.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_fedavg_convergence.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_fedavg_driver.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_fedavg_weights.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_fedavg_worker.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_gpu_e2e.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_headline.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_image_manifest_lookup.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_integrations.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_interfaces.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_job_state_partial.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_kmeans_leases.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_kmeans_workload.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_kuberay_backend.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_launcher_local.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_lease_seconds_bound.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_leases.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_leases_scope.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_leases_sqlite.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_monitor_sampler.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_placement_dependencies.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_placement_exclusion.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_placement_gpu.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_placement_local_data.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_placement_pool.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_placement_reliability.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_placement_trusted_argv.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_planner.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_protocol_evidence.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_protocol_gpu.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_protocol_local_datasets.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_protocol_pools.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_protocol_v1alpha1.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_recipe_gpu.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_recipe_local_inputs.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_recipe_partition.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_recipe_pool.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_recipes_command.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_recovery.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_reduce.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_retention.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_run_json.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_scheduler_isolation.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_sdk_submit.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_service_artifact_delete.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_service_auth.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_service_auth_startup.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_service_checkpoints.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_service_command_recipe.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_service_delegation.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_service_fedavg.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_service_job_result.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_service_modea.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_service_reliability.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_service_validate_output.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_service_write_scope.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_sgd_trainer.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_strategy_command.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_submit_envelope.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_submit_retention.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_torch_helper.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_viewer_flowmap.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_viewer_page.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_viewer_server.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_viewer_state.py +0 -0
- {flashruntime-0.5.0 → flashruntime-0.6.0}/tests/test_workloads_command.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flashruntime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
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
|
|
@@ -332,6 +332,17 @@ class NodeCapabilities(BaseModel):
|
|
|
332
332
|
#: this runtime cannot parse — never a guess. Placement reads the LENGTH
|
|
333
333
|
#: of this list and nothing else in v1.
|
|
334
334
|
gpus: list[GpuInfo] = Field(default_factory=list)
|
|
335
|
+
#: Bytes this host will lend to cached dataset shards. Advertised so the
|
|
336
|
+
#: control plane can refuse a job at SUBMIT time when no host in the pool
|
|
337
|
+
#: can hold a slice — twenty machines each downloading for forty minutes
|
|
338
|
+
#: and then refusing is the failure this exists to replace.
|
|
339
|
+
#:
|
|
340
|
+
#: Defaults to 0, and 0 means "send me no dataset work". Fail closed, the
|
|
341
|
+
#: same polarity as `can_install_dependencies`: an agent whose
|
|
342
|
+
#: registration predates this field has nowhere to put a shard and must
|
|
343
|
+
#: never be handed one. Bytes rather than GB to match `memory_bytes` —
|
|
344
|
+
#: one unit for every capacity on this model.
|
|
345
|
+
dataset_cache_bytes: int = 0
|
|
335
346
|
#: Team pools this node serves, as pool ids minted by the cloud control
|
|
336
347
|
#: plane. Stamped SERVER-SIDE by the cloud API's agent proxy from the
|
|
337
348
|
#: machine owner's memberships — an agent's self-reported value is
|
|
@@ -66,6 +66,18 @@ class CommandRecipe(WorkloadRecipe):
|
|
|
66
66
|
)
|
|
67
67
|
elif not all(isinstance(d, str) for d in extra_deps):
|
|
68
68
|
problems.append("'extra_dependencies' must be a list of strings")
|
|
69
|
+
slices = params.get("dataset_slices")
|
|
70
|
+
if slices is not None:
|
|
71
|
+
if not isinstance(slices, list) or isinstance(slices, (str, bytes)):
|
|
72
|
+
problems.append(
|
|
73
|
+
"'dataset_slices' must be a list with one entry per task "
|
|
74
|
+
"(a list, not a single string)"
|
|
75
|
+
)
|
|
76
|
+
elif not all(isinstance(s, list) for s in slices):
|
|
77
|
+
problems.append(
|
|
78
|
+
"'dataset_slices' must be a list of per-task lists, one "
|
|
79
|
+
"list of dataset slices for each task"
|
|
80
|
+
)
|
|
69
81
|
problems.extend(self._unpack_problems(params))
|
|
70
82
|
problems.extend(partition_problems(params))
|
|
71
83
|
problems.extend(declaration_problems(params.get("validators")))
|
|
@@ -239,6 +251,31 @@ class CommandRecipe(WorkloadRecipe):
|
|
|
239
251
|
# hop have tests that pass while it is broken, because each
|
|
240
252
|
# constructs the payload directly.
|
|
241
253
|
payload["extra_dependencies"] = list(p["extra_dependencies"])
|
|
254
|
+
if p.get("dataset_slices") is not None:
|
|
255
|
+
# The ONLY per-task payload key in this method. Every other
|
|
256
|
+
# forward above copies one value to every task; each task
|
|
257
|
+
# fetches a DIFFERENT slice of the manifest, so this one is
|
|
258
|
+
# indexed by task position exactly as `task_params` is.
|
|
259
|
+
#
|
|
260
|
+
# Computed by the flashml-cloud compiler and opaque here:
|
|
261
|
+
# the byte-weighted mapping lives beside `fleet_shape` in
|
|
262
|
+
# that repo's `elastic.py`, and a second implementation here
|
|
263
|
+
# is the drift that module's docstring warns about.
|
|
264
|
+
#
|
|
265
|
+
# Same failure shape as `local_inputs` and `dependencies`:
|
|
266
|
+
# drop this line and every host fetches nothing, the workload
|
|
267
|
+
# dies on a missing path, and both ends' unit tests still
|
|
268
|
+
# pass because each constructs the payload directly.
|
|
269
|
+
slices = p["dataset_slices"]
|
|
270
|
+
if i >= len(slices):
|
|
271
|
+
raise ValueError(
|
|
272
|
+
f"task {i}: 'dataset_slices' has {len(slices)} "
|
|
273
|
+
f"entr{'y' if len(slices) == 1 else 'ies'} but this "
|
|
274
|
+
f"job expanded to more tasks — every task needs a "
|
|
275
|
+
f"slice, even an empty one"
|
|
276
|
+
)
|
|
277
|
+
# `list(...)` so a payload never aliases the caller's spec.
|
|
278
|
+
payload["datasets"] = list(slices[i])
|
|
242
279
|
gpus = spec.spec.resources.gpuPerTask
|
|
243
280
|
if gpus:
|
|
244
281
|
# The GPU requirement is the ONLY payload key sourced from
|
|
@@ -386,6 +386,56 @@ class IsolationAwarePlacement(PlacementPolicy):
|
|
|
386
386
|
waiver relaxes the sandbox tier and nothing else. No waiver makes a
|
|
387
387
|
read-only container with no network able to run `pip install`.
|
|
388
388
|
|
|
389
|
+
A ninth gate applies to tasks whose payload carries `datasets`: the sum
|
|
390
|
+
of every slice entry's `size` must fit inside the claiming node's
|
|
391
|
+
`capabilities.dataset_cache_bytes`. This is CAPACITY, not capability —
|
|
392
|
+
every tier can fetch, because the AGENT fetches before the sandbox
|
|
393
|
+
closes, so unlike `extra_dependencies` there is no tier that physically
|
|
394
|
+
cannot. The only question is whether the bytes fit.
|
|
395
|
+
|
|
396
|
+
Not to be confused with the fourth gate. `local_inputs`/`local_datasets`
|
|
397
|
+
asks whether a host already HOLDS named data it will never upload; this
|
|
398
|
+
one asks whether a host has ROOM for data it is about to fetch from a
|
|
399
|
+
public origin. Different payload key, different capability, different
|
|
400
|
+
question.
|
|
401
|
+
|
|
402
|
+
It takes the argv/local-data/gpu/exclude_nodes/pool/dependencies
|
|
403
|
+
polarity — **fail closed** — and every leg of it:
|
|
404
|
+
|
|
405
|
+
- A node advertising nothing (an agent whose registration predates the
|
|
406
|
+
field) is refused, and so is one advertising `0`, which is the model
|
|
407
|
+
default and means "send me no dataset work". A shard with nowhere to
|
|
408
|
+
go is a task that dies after a forty-minute download rather than one
|
|
409
|
+
that never started, and twenty machines doing that at once is the
|
|
410
|
+
failure the advertisement exists to prevent.
|
|
411
|
+
- The advertisement must be a genuine `int`. `bool` is excluded
|
|
412
|
+
explicitly for the reason the `gpus` gate excludes it: it is an `int`
|
|
413
|
+
subclass, so a `dataset_cache_bytes: true` typo would read as the
|
|
414
|
+
capacity 1.
|
|
415
|
+
- `capabilities` itself may be absent or type-confused; that is read as
|
|
416
|
+
advertising nothing rather than allowed to raise. `isinstance`, never
|
|
417
|
+
`(node.get("capabilities") or {})` — a string `capabilities` value has
|
|
418
|
+
no `.get` and would crash the predicate inside the claim path, taking
|
|
419
|
+
the whole claim down rather than one task.
|
|
420
|
+
- A non-dict slice, a non-dict entry, or an entry whose `size` is not a
|
|
421
|
+
non-negative `int` makes the task ineligible everywhere. The manifest
|
|
422
|
+
is submitter-controlled, so an unreadable size must NOT contribute 0 to
|
|
423
|
+
the total: that is precisely how a hostile slice would buy itself an
|
|
424
|
+
unbounded fetch on a node that advertised a modest cache.
|
|
425
|
+
- The requirement is the sum over every entry of every slice. Checking
|
|
426
|
+
per-dataset, or against the largest entry, would place a task that then
|
|
427
|
+
fills a volunteer's disk halfway through the fetch.
|
|
428
|
+
- Empty and absent both mean "declares no data": `if
|
|
429
|
+
task.payload.get("datasets")` treats `None` and `[]` alike, so every
|
|
430
|
+
job deployed today is completely unaffected. A slice that is PRESENT
|
|
431
|
+
but happens to carry no entries is still dataset work and still needs
|
|
432
|
+
a readable advertisement — the fail-closed leg is the advertisement,
|
|
433
|
+
not the total.
|
|
434
|
+
|
|
435
|
+
`allowFallback` does not waive this gate either. It waives the sandbox
|
|
436
|
+
tier and nothing else; a submitter's isolation posture has nothing to say
|
|
437
|
+
about how much disk a stranger's machine offered.
|
|
438
|
+
|
|
389
439
|
Everything genuinely standard keeps the fail-open placement default."""
|
|
390
440
|
|
|
391
441
|
def eligible(self, task: TaskSpec, node: NodeView) -> bool:
|
|
@@ -516,6 +566,48 @@ class IsolationAwarePlacement(PlacementPolicy):
|
|
|
516
566
|
if task.payload.get("extra_dependencies"):
|
|
517
567
|
if node.get("can_install_dependencies") is not True:
|
|
518
568
|
return False
|
|
569
|
+
# Capacity, not capability, and the only gate in this class that is.
|
|
570
|
+
# Every tier's AGENT has network and a writable disk before the
|
|
571
|
+
# sandbox closes, so unlike `extra_dependencies` there is no tier
|
|
572
|
+
# that physically cannot fetch — the only question is whether the
|
|
573
|
+
# bytes fit. Distinct from the `local_inputs` gate above: that one
|
|
574
|
+
# asks whether a host already HOLDS named data, this one whether it
|
|
575
|
+
# has ROOM for data it is about to fetch.
|
|
576
|
+
#
|
|
577
|
+
# Fail-closed on an absent or type-confused advertisement, and
|
|
578
|
+
# checked before the allowFallback waiver below for the same reason
|
|
579
|
+
# as every gate above: the waiver relaxes the sandbox tier and has
|
|
580
|
+
# nothing to say about how much disk a stranger's machine offered.
|
|
581
|
+
if task.payload.get("datasets"):
|
|
582
|
+
needed = 0
|
|
583
|
+
for dataset in task.payload["datasets"]:
|
|
584
|
+
if not isinstance(dataset, dict):
|
|
585
|
+
return False # type-confused slice ⇒ fail closed, no crash
|
|
586
|
+
for entry in dataset.get("entries") or []:
|
|
587
|
+
size = entry.get("size") if isinstance(entry, dict) else None
|
|
588
|
+
# A non-int size is a malformed slice, not a free one:
|
|
589
|
+
# letting it contribute 0 is how a hostile manifest buys
|
|
590
|
+
# an unbounded fetch on a modest cache. `bool` is an
|
|
591
|
+
# `int` subclass, so `size: true` is excluded explicitly.
|
|
592
|
+
if (
|
|
593
|
+
not isinstance(size, int)
|
|
594
|
+
or isinstance(size, bool)
|
|
595
|
+
or size < 0
|
|
596
|
+
):
|
|
597
|
+
return False
|
|
598
|
+
needed += size
|
|
599
|
+
capabilities = node.get("capabilities")
|
|
600
|
+
# isinstance, not `or {}` — a string capabilities value has no
|
|
601
|
+
# `.get` and must fail closed rather than crash the predicate.
|
|
602
|
+
advertised = (
|
|
603
|
+
capabilities.get("dataset_cache_bytes")
|
|
604
|
+
if isinstance(capabilities, dict)
|
|
605
|
+
else None
|
|
606
|
+
)
|
|
607
|
+
if not isinstance(advertised, int) or isinstance(advertised, bool):
|
|
608
|
+
return False # absent/type-confused ⇒ advertises nothing
|
|
609
|
+
if needed > advertised:
|
|
610
|
+
return False
|
|
519
611
|
isolation = task.payload.get("isolation")
|
|
520
612
|
if isolation is None:
|
|
521
613
|
return True # no isolation payload ⇒ standard, runs anywhere
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flashruntime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
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
|
|
@@ -138,6 +138,7 @@ tests/test_leases.py
|
|
|
138
138
|
tests/test_leases_scope.py
|
|
139
139
|
tests/test_leases_sqlite.py
|
|
140
140
|
tests/test_monitor_sampler.py
|
|
141
|
+
tests/test_placement_datasets.py
|
|
141
142
|
tests/test_placement_dependencies.py
|
|
142
143
|
tests/test_placement_exclusion.py
|
|
143
144
|
tests/test_placement_gpu.py
|
|
@@ -146,11 +147,13 @@ tests/test_placement_pool.py
|
|
|
146
147
|
tests/test_placement_reliability.py
|
|
147
148
|
tests/test_placement_trusted_argv.py
|
|
148
149
|
tests/test_planner.py
|
|
150
|
+
tests/test_protocol_datasets.py
|
|
149
151
|
tests/test_protocol_evidence.py
|
|
150
152
|
tests/test_protocol_gpu.py
|
|
151
153
|
tests/test_protocol_local_datasets.py
|
|
152
154
|
tests/test_protocol_pools.py
|
|
153
155
|
tests/test_protocol_v1alpha1.py
|
|
156
|
+
tests/test_recipe_datasets.py
|
|
154
157
|
tests/test_recipe_gpu.py
|
|
155
158
|
tests/test_recipe_local_inputs.py
|
|
156
159
|
tests/test_recipe_partition.py
|
|
@@ -46,7 +46,7 @@ name = "flashruntime"
|
|
|
46
46
|
# must pin a range that includes it. (0.3.0 was the first published release;
|
|
47
47
|
# 0.1.0 and 0.2.0 were declared here but never reached PyPI.) Released from
|
|
48
48
|
# the Zolli-Labs/flashml monorepo by pushing a `flashruntime-vX.Y.Z` tag.
|
|
49
|
-
version = "0.
|
|
49
|
+
version = "0.6.0"
|
|
50
50
|
description = "Open fault-tolerant distributed ML runtime: strategy planning, job protocol, leases, checkpointing, and recovery across heterogeneous compute."
|
|
51
51
|
readme = "README.md"
|
|
52
52
|
# PEP 639 SPDX expression (string form) — replaces the deprecated
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""The ninth placement gate: a node must have room for the slice it is
|
|
2
|
+
offered — `capabilities.dataset_cache_bytes` must cover the sum of every
|
|
3
|
+
declared entry's `size` (AGENTS.md rule 3).
|
|
4
|
+
|
|
5
|
+
Capacity, not capability. Unlike installing dependencies — which the
|
|
6
|
+
container tiers physically cannot do — every tier's AGENT fetches before the
|
|
7
|
+
sandbox closes, so there is no tier that *cannot* hold a shard. The only
|
|
8
|
+
question this gate asks is whether the bytes fit.
|
|
9
|
+
|
|
10
|
+
Fail closed, taking the argv/local-data/gpu polarity: a node advertising
|
|
11
|
+
nothing (an agent whose registration predates the field) holds no dataset
|
|
12
|
+
work. A shard with nowhere to go is a task that dies after a forty-minute
|
|
13
|
+
download rather than one that never started.
|
|
14
|
+
|
|
15
|
+
Not to be confused with `local_inputs`/`local_datasets`
|
|
16
|
+
(`test_placement_local_data.py`), which is a different feature with its own
|
|
17
|
+
gate: that one asks whether a host already HOLDS named data it will never
|
|
18
|
+
upload; this one asks whether a host has room for data it is about to fetch.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import pytest
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _entry(size=100, **over):
|
|
27
|
+
entry = {
|
|
28
|
+
"path": "a.parquet",
|
|
29
|
+
"url": "https://origin.invalid/a.parquet",
|
|
30
|
+
"size": size,
|
|
31
|
+
"integrity": {"kind": "sha256", "value": "aa"},
|
|
32
|
+
}
|
|
33
|
+
entry.update(over)
|
|
34
|
+
return entry
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _dataset(*sizes, name="d", **over):
|
|
38
|
+
dataset = {"name": name, "split": "shard",
|
|
39
|
+
"entries": [_entry(size) for size in sizes]}
|
|
40
|
+
dataset.update(over)
|
|
41
|
+
return dataset
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _task(datasets, **payload_extra):
|
|
45
|
+
"""A task carrying `datasets` RAW, so tests can poison it with values the
|
|
46
|
+
compiler would never emit (the trick `_local_task` plays on `local_inputs`
|
|
47
|
+
and `_task_raw` plays on the isolation payload)."""
|
|
48
|
+
from flashruntime.protocol.v1alpha1 import TaskSpec
|
|
49
|
+
|
|
50
|
+
payload = {"module": "flashml_workloads.sklearn_trial"}
|
|
51
|
+
if datasets is not None:
|
|
52
|
+
payload["datasets"] = datasets
|
|
53
|
+
payload.update(payload_extra)
|
|
54
|
+
return TaskSpec(
|
|
55
|
+
task_id="task-000", job_id="job-a", commit_key="job-a/task-000/m.json",
|
|
56
|
+
payload=payload,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _node(cache_bytes=None, **over):
|
|
61
|
+
"""A node view shaped like the claim endpoint's. `cache_bytes` defaults to
|
|
62
|
+
ABSENT (not 0) so a bare `_node()` models an agent whose registration
|
|
63
|
+
predates the field — the fail-closed case."""
|
|
64
|
+
node = {"node_id": "n1", "sandbox_capable": True, "capabilities": {}}
|
|
65
|
+
if cache_bytes is not None:
|
|
66
|
+
node["capabilities"] = {"dataset_cache_bytes": cache_bytes}
|
|
67
|
+
node.update(over)
|
|
68
|
+
return node
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_a_node_with_room_is_eligible():
|
|
72
|
+
from flashruntime.scheduler import IsolationAwarePlacement
|
|
73
|
+
|
|
74
|
+
assert IsolationAwarePlacement().eligible(
|
|
75
|
+
_task([_dataset(100)]), _node(1000)) is True
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_a_node_without_room_is_not():
|
|
79
|
+
from flashruntime.scheduler import IsolationAwarePlacement
|
|
80
|
+
|
|
81
|
+
assert IsolationAwarePlacement().eligible(
|
|
82
|
+
_task([_dataset(2000)]), _node(1000)) is False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_an_exact_fit_is_eligible():
|
|
86
|
+
"""The boundary is `needed > advertised`, not `>=`: a node that advertised
|
|
87
|
+
exactly this many bytes said yes to exactly this many bytes."""
|
|
88
|
+
from flashruntime.scheduler import IsolationAwarePlacement
|
|
89
|
+
|
|
90
|
+
assert IsolationAwarePlacement().eligible(
|
|
91
|
+
_task([_dataset(1000)]), _node(1000)) is True
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_the_requirement_is_the_sum_over_every_entry_of_every_dataset():
|
|
95
|
+
"""One entry fits; the whole slice does not. Summing per-dataset or
|
|
96
|
+
checking only the largest entry would place a task that then fills a
|
|
97
|
+
volunteer's disk halfway through the fetch."""
|
|
98
|
+
from flashruntime.scheduler import IsolationAwarePlacement
|
|
99
|
+
|
|
100
|
+
policy = IsolationAwarePlacement()
|
|
101
|
+
task = _task([_dataset(400, 400, name="d1"), _dataset(400, name="d2")])
|
|
102
|
+
assert policy.eligible(task, _node(1000)) is False
|
|
103
|
+
assert policy.eligible(task, _node(1200)) is True
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_a_node_predating_the_field_holds_no_dataset_work():
|
|
107
|
+
"""Fail closed — an absent capability reads as 'advertises nothing', never
|
|
108
|
+
as 'advertises everything'."""
|
|
109
|
+
from flashruntime.scheduler import IsolationAwarePlacement
|
|
110
|
+
|
|
111
|
+
assert IsolationAwarePlacement().eligible(_task([_dataset(1)]), _node()) is False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_a_zero_advertisement_holds_no_dataset_work():
|
|
115
|
+
"""`0` is the model default and it means 'send me no dataset work' — the
|
|
116
|
+
key being PRESENT must not read as consent."""
|
|
117
|
+
from flashruntime.scheduler import IsolationAwarePlacement
|
|
118
|
+
|
|
119
|
+
assert IsolationAwarePlacement().eligible(_task([_dataset(1)]), _node(0)) is False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@pytest.mark.parametrize(
|
|
123
|
+
"capabilities",
|
|
124
|
+
[
|
|
125
|
+
pytest.param("not-a-dict", id="bare-string"),
|
|
126
|
+
pytest.param(None, id="explicit-none"),
|
|
127
|
+
pytest.param(["dataset_cache_bytes"], id="list"),
|
|
128
|
+
pytest.param(17179869184, id="bare-int"),
|
|
129
|
+
],
|
|
130
|
+
)
|
|
131
|
+
def test_a_type_confused_capabilities_value_does_not_raise(capabilities):
|
|
132
|
+
"""`(node.get("capabilities") or {})` would crash the predicate on a
|
|
133
|
+
string — it has no `.get`, and a crash inside the claim path takes the
|
|
134
|
+
whole claim down rather than one task. The house rule is `isinstance`
|
|
135
|
+
(documented at `scheduler/__init__.py:234-236`), and the reading is
|
|
136
|
+
'advertises nothing'."""
|
|
137
|
+
from flashruntime.scheduler import IsolationAwarePlacement
|
|
138
|
+
|
|
139
|
+
node = _node()
|
|
140
|
+
node["capabilities"] = capabilities
|
|
141
|
+
assert IsolationAwarePlacement().eligible(_task([_dataset(1)]), node) is False
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@pytest.mark.parametrize(
|
|
145
|
+
"advertised",
|
|
146
|
+
[
|
|
147
|
+
pytest.param("17179869184", id="numeric-string"),
|
|
148
|
+
pytest.param(16.0, id="float"),
|
|
149
|
+
pytest.param(None, id="explicit-none"),
|
|
150
|
+
pytest.param([16], id="list"),
|
|
151
|
+
pytest.param(True, id="truthy-boolean"),
|
|
152
|
+
],
|
|
153
|
+
)
|
|
154
|
+
def test_a_non_int_advertisement_holds_no_dataset_work(advertised):
|
|
155
|
+
"""A capacity that is not a genuine `int` is an advertisement nobody can
|
|
156
|
+
act on. `bool` is named explicitly because it is an `int` subclass: a
|
|
157
|
+
`dataset_cache_bytes: true` typo must not read as the capacity 1."""
|
|
158
|
+
from flashruntime.scheduler import IsolationAwarePlacement
|
|
159
|
+
|
|
160
|
+
assert IsolationAwarePlacement().eligible(
|
|
161
|
+
_task([_dataset(1)]), _node(advertised)) is False
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@pytest.mark.parametrize(
|
|
165
|
+
"size",
|
|
166
|
+
[
|
|
167
|
+
pytest.param("100", id="numeric-string"),
|
|
168
|
+
pytest.param(100.5, id="float"),
|
|
169
|
+
pytest.param(None, id="explicit-none"),
|
|
170
|
+
pytest.param(True, id="truthy-boolean"),
|
|
171
|
+
pytest.param(-1, id="negative"),
|
|
172
|
+
],
|
|
173
|
+
)
|
|
174
|
+
def test_a_non_int_entry_size_is_a_malformed_slice_not_a_free_one(size):
|
|
175
|
+
"""The manifest is submitter-controlled. An unreadable size must make the
|
|
176
|
+
task unplaceable, never contribute 0 to the total — the latter is how a
|
|
177
|
+
hostile slice buys itself an unbounded fetch on a node that advertised a
|
|
178
|
+
modest cache."""
|
|
179
|
+
from flashruntime.scheduler import IsolationAwarePlacement
|
|
180
|
+
|
|
181
|
+
task = _task([{"name": "d", "split": "shard", "entries": [_entry(size)]}])
|
|
182
|
+
assert IsolationAwarePlacement().eligible(task, _node(1_000_000)) is False
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_an_entry_missing_its_size_is_refused():
|
|
186
|
+
from flashruntime.scheduler import IsolationAwarePlacement
|
|
187
|
+
|
|
188
|
+
entry = _entry()
|
|
189
|
+
del entry["size"]
|
|
190
|
+
task = _task([{"name": "d", "split": "shard", "entries": [entry]}])
|
|
191
|
+
assert IsolationAwarePlacement().eligible(task, _node(1_000_000)) is False
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@pytest.mark.parametrize(
|
|
195
|
+
"entry",
|
|
196
|
+
[
|
|
197
|
+
pytest.param("a.parquet", id="bare-string"),
|
|
198
|
+
pytest.param(None, id="explicit-none"),
|
|
199
|
+
pytest.param(100, id="bare-int"),
|
|
200
|
+
],
|
|
201
|
+
)
|
|
202
|
+
def test_a_non_dict_entry_is_refused(entry):
|
|
203
|
+
from flashruntime.scheduler import IsolationAwarePlacement
|
|
204
|
+
|
|
205
|
+
task = _task([{"name": "d", "split": "shard", "entries": [entry]}])
|
|
206
|
+
assert IsolationAwarePlacement().eligible(task, _node(1_000_000)) is False
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@pytest.mark.parametrize(
|
|
210
|
+
"dataset",
|
|
211
|
+
[
|
|
212
|
+
pytest.param("imdb", id="bare-string"),
|
|
213
|
+
pytest.param(None, id="explicit-none"),
|
|
214
|
+
pytest.param([{"path": "a.parquet"}], id="nested-list"),
|
|
215
|
+
],
|
|
216
|
+
)
|
|
217
|
+
def test_a_non_dict_dataset_is_refused(dataset):
|
|
218
|
+
"""A slice that is not a mapping was built wrong, and the gate must say so
|
|
219
|
+
by refusing rather than by raising inside the claim path."""
|
|
220
|
+
from flashruntime.scheduler import IsolationAwarePlacement
|
|
221
|
+
|
|
222
|
+
assert IsolationAwarePlacement().eligible(
|
|
223
|
+
_task([dataset]), _node(1_000_000)) is False
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@pytest.mark.parametrize(
|
|
227
|
+
"datasets",
|
|
228
|
+
[
|
|
229
|
+
pytest.param("imdb", id="bare-string"),
|
|
230
|
+
pytest.param({"name": "imdb"}, id="single-dict-not-a-list"),
|
|
231
|
+
],
|
|
232
|
+
)
|
|
233
|
+
def test_a_type_confused_datasets_payload_is_ineligible_everywhere(datasets):
|
|
234
|
+
"""Present but not a list of slices: fail closed on every node, including
|
|
235
|
+
one with room to spare, and never crash the predicate."""
|
|
236
|
+
from flashruntime.scheduler import IsolationAwarePlacement
|
|
237
|
+
|
|
238
|
+
assert IsolationAwarePlacement().eligible(
|
|
239
|
+
_task(datasets), _node(1_000_000)) is False
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_a_task_without_datasets_is_unaffected():
|
|
243
|
+
"""Every job deployed today. The gate must be invisible to them — including
|
|
244
|
+
on a node that advertises no cache at all."""
|
|
245
|
+
from flashruntime.scheduler import IsolationAwarePlacement
|
|
246
|
+
|
|
247
|
+
policy = IsolationAwarePlacement()
|
|
248
|
+
assert policy.eligible(_task(None), _node()) is True
|
|
249
|
+
assert policy.eligible(_task(None), _node(1000)) is True
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_an_empty_datasets_list_requires_nothing():
|
|
253
|
+
"""Absent and empty mean the same thing here, exactly as they do for
|
|
254
|
+
`extra_dependencies`: a job that declares no data runs anywhere."""
|
|
255
|
+
from flashruntime.scheduler import IsolationAwarePlacement
|
|
256
|
+
|
|
257
|
+
assert IsolationAwarePlacement().eligible(_task([]), _node()) is True
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_a_slice_with_no_entries_still_needs_an_advertisement():
|
|
261
|
+
"""`needed == 0` is not a licence: a node that advertised nothing has said
|
|
262
|
+
it wants no dataset work, and a slice that happens to be empty this time is
|
|
263
|
+
still dataset work. Fail closed on the advertisement, not on the total."""
|
|
264
|
+
from flashruntime.scheduler import IsolationAwarePlacement
|
|
265
|
+
|
|
266
|
+
policy = IsolationAwarePlacement()
|
|
267
|
+
empty = _task([{"name": "d", "split": "shard", "entries": []}])
|
|
268
|
+
assert policy.eligible(empty, _node()) is False
|
|
269
|
+
assert policy.eligible(empty, _node(1000)) is True
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_allow_fallback_does_not_waive_this_gate():
|
|
273
|
+
"""The waiver covers the sandbox tier and nothing else. A submitter's
|
|
274
|
+
isolation posture has nothing to say about how much disk a stranger's
|
|
275
|
+
machine offered."""
|
|
276
|
+
from flashruntime.scheduler import IsolationAwarePlacement
|
|
277
|
+
|
|
278
|
+
task = _task(
|
|
279
|
+
[_dataset(2000)],
|
|
280
|
+
pool="p1",
|
|
281
|
+
isolation={"tier": "sandboxed", "allowFallback": True},
|
|
282
|
+
)
|
|
283
|
+
node = _node(1000)
|
|
284
|
+
node["capabilities"]["pools"] = ["p1"]
|
|
285
|
+
assert IsolationAwarePlacement().eligible(task, node) is False
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_claim_over_a_dataset_queue_serves_the_node_with_room():
|
|
289
|
+
"""A slice too large for the asking node must not block the queue: the next
|
|
290
|
+
clean task is still leased, and the dataset task waits for a host that fits
|
|
291
|
+
it."""
|
|
292
|
+
from flashruntime.leases import LeaseManager
|
|
293
|
+
from flashruntime.protocol.v1alpha1 import TaskSpec
|
|
294
|
+
from flashruntime.scheduler import IsolationAwarePlacement
|
|
295
|
+
|
|
296
|
+
mgr = LeaseManager()
|
|
297
|
+
mgr.add_task(_task([_dataset(2000)])) # head of queue, needs room
|
|
298
|
+
mgr.add_task(
|
|
299
|
+
TaskSpec(task_id="task-001", job_id="job-a", commit_key="job-a/task-001/m.json")
|
|
300
|
+
)
|
|
301
|
+
policy = IsolationAwarePlacement()
|
|
302
|
+
|
|
303
|
+
lease = mgr.claim("n1", policy=policy, node=_node(1000))
|
|
304
|
+
assert lease is not None
|
|
305
|
+
assert lease.task_id == "task-001"
|
|
306
|
+
|
|
307
|
+
lease2 = mgr.claim("n2", policy=policy, node=_node(1_000_000, node_id="n2"))
|
|
308
|
+
assert lease2 is not None
|
|
309
|
+
assert lease2.task_id == "task-000"
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def test_claim_endpoint_leases_dataset_work_only_to_a_host_with_room():
|
|
313
|
+
"""End to end over the REAL claim endpoint, not a hand-built node dict.
|
|
314
|
+
|
|
315
|
+
Every other test in this file hands `eligible()` a dictionary shaped like
|
|
316
|
+
the claim endpoint's node view. None of them touches the code that
|
|
317
|
+
actually BUILDS that view (`service/modea.py`), so all of them would pass
|
|
318
|
+
while the endpoint omitted `dataset_cache_bytes` — the gate would read an
|
|
319
|
+
absent capability on EVERY node and refuse every dataset task everywhere.
|
|
320
|
+
Fail closed, and also permanently stuck: inert in production, green in CI.
|
|
321
|
+
"""
|
|
322
|
+
import pathlib
|
|
323
|
+
|
|
324
|
+
import fastapi
|
|
325
|
+
from fastapi.testclient import TestClient
|
|
326
|
+
|
|
327
|
+
from flashruntime.leases import LeaseManager
|
|
328
|
+
from flashruntime.service.modea import ModeAState, build_router
|
|
329
|
+
|
|
330
|
+
state = ModeAState(LeaseManager(), artifacts_dir=pathlib.Path("/tmp"))
|
|
331
|
+
app = fastapi.FastAPI()
|
|
332
|
+
app.include_router(build_router(state))
|
|
333
|
+
client = TestClient(app)
|
|
334
|
+
state.manager.add_task(_task([_dataset(4096)]))
|
|
335
|
+
|
|
336
|
+
def register(node_id: str, cache_bytes: int):
|
|
337
|
+
r = client.post(
|
|
338
|
+
"/v1alpha1/nodes/register",
|
|
339
|
+
json={
|
|
340
|
+
"node_id": node_id, "kubernetes_node": "", "hostname": node_id,
|
|
341
|
+
"capabilities": {"dataset_cache_bytes": cache_bytes},
|
|
342
|
+
},
|
|
343
|
+
)
|
|
344
|
+
assert r.status_code == 200
|
|
345
|
+
|
|
346
|
+
register("no-room-node", 0)
|
|
347
|
+
register("roomy-node", 1024 ** 3)
|
|
348
|
+
|
|
349
|
+
assert client.post(
|
|
350
|
+
"/v1alpha1/leases/claim", json={"node_id": "no-room-node"}
|
|
351
|
+
).status_code == 204
|
|
352
|
+
r = client.post("/v1alpha1/leases/claim", json={"node_id": "roomy-node"})
|
|
353
|
+
assert r.status_code == 200, r.text
|
|
354
|
+
assert r.json()["task_id"] == "task-000"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Wire field for dataset cache capacity (AGENTS.md rule 3: fail closed).
|
|
2
|
+
|
|
3
|
+
Bytes, not GB, to match `memory_bytes` — one unit for every capacity on
|
|
4
|
+
this model. Zero means "send me no dataset work", which is what an agent
|
|
5
|
+
predating the field must be read as saying.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from flashruntime.protocol.v1alpha1 import NodeCapabilities, NodeRegistration
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_dataset_cache_bytes_defaults_to_zero():
|
|
14
|
+
assert NodeCapabilities().dataset_cache_bytes == 0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_dataset_cache_bytes_round_trips():
|
|
18
|
+
caps = NodeCapabilities.model_validate({"dataset_cache_bytes": 17179869184})
|
|
19
|
+
assert caps.model_dump()["dataset_cache_bytes"] == 17179869184
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_old_wire_shapes_still_validate():
|
|
23
|
+
"""An agent on 0.5.0 sends none of this; nothing may break."""
|
|
24
|
+
reg = NodeRegistration.model_validate(
|
|
25
|
+
{"node_id": "n", "kubernetes_node": "", "hostname": "h",
|
|
26
|
+
"capabilities": {"cpu_cores": 4}}
|
|
27
|
+
)
|
|
28
|
+
assert reg.capabilities.dataset_cache_bytes == 0
|