freesolo-flash-dev 0.2.38__tar.gz → 0.2.39__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.
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/PKG-INFO +1 -1
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/docker/kernel_fingerprint.py +5 -3
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/cli/render.py +3 -1
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/cost/analytical.py +21 -6
- freesolo_flash_dev-0.2.39/flash/cost/facts.py +161 -0
- freesolo_flash_dev-0.2.39/flash/providers/__init__.py +63 -0
- freesolo_flash_dev-0.2.39/flash/providers/_hf_artifacts.py +177 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/_instance.py +76 -2
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/_instance_bootstrap.py +3 -0
- freesolo_flash_dev-0.2.39/flash/providers/_instance_poll.py +438 -0
- freesolo_flash_dev-0.2.39/flash/providers/_instance_provider.py +171 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/_poll.py +12 -0
- freesolo_flash_dev-0.2.38/flash/providers/runpod/train/deps.py → freesolo_flash_dev-0.2.39/flash/providers/_worker.py +83 -2
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/allocator.py +28 -39
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/base.py +128 -3
- freesolo_flash_dev-0.2.39/flash/providers/lambdalabs/__init__.py +154 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/lambdalabs/gpus.py +2 -9
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/lambdalabs/jobs/__init__.py +80 -230
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/lambdalabs/jobs/builders.py +31 -31
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/realized.py +7 -2
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/runpod/__init__.py +22 -1
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/runpod/gpus.py +2 -2
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/runpod/jobs.py +34 -85
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/runpod/pricing.py +2 -2
- freesolo_flash_dev-0.2.39/flash/providers/runpod/train/__init__.py +29 -0
- freesolo_flash_dev-0.2.39/flash/providers/runpod/train/deps.py +19 -0
- freesolo_flash_dev-0.2.39/flash/providers/vast/__init__.py +194 -0
- freesolo_flash_dev-0.2.39/flash/providers/vast/api.py +318 -0
- freesolo_flash_dev-0.2.39/flash/providers/vast/auth.py +16 -0
- freesolo_flash_dev-0.2.39/flash/providers/vast/jobs/__init__.py +656 -0
- freesolo_flash_dev-0.2.39/flash/providers/vast/jobs/builders.py +197 -0
- freesolo_flash_dev-0.2.39/flash/providers/vast/preflight.py +17 -0
- freesolo_flash_dev-0.2.39/flash/providers/vast/pricing.py +147 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/runner/__init__.py +16 -2
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/runner/deploy.py +28 -1
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/runner/lifecycle.py +68 -22
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/server/_runtime.py +153 -8
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/server/app.py +8 -7
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/pyproject.toml +2 -2
- freesolo_flash_dev-0.2.39/tests/_helpers/vast.py +38 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/conftest.py +6 -5
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/live/conftest.py +6 -5
- freesolo_flash_dev-0.2.39/tests/live/test_vast_live.py +56 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_allocator.py +117 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_cancel_remote.py +1 -2
- freesolo_flash_dev-0.2.39/tests/test_cost_estimate.py +231 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_gpus.py +31 -9
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_grpo_params.py +17 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_jobs.py +153 -17
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_lambda_runner.py +16 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_oom_escalate_gpu.py +7 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_orchestrator_flash.py +4 -4
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_providers_symmetry.py +110 -14
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_resume_on_retry.py +120 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_server_api.py +437 -1
- freesolo_flash_dev-0.2.39/tests/test_vast_api.py +462 -0
- freesolo_flash_dev-0.2.39/tests/test_vast_offers.py +250 -0
- freesolo_flash_dev-0.2.39/tests/test_vast_runner.py +1471 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_weight_cache.py +1 -1
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/uv.lock +1 -1
- freesolo_flash_dev-0.2.38/flash/cost/facts.py +0 -110
- freesolo_flash_dev-0.2.38/flash/providers/__init__.py +0 -37
- freesolo_flash_dev-0.2.38/flash/providers/lambdalabs/__init__.py +0 -122
- freesolo_flash_dev-0.2.38/flash/providers/lambdalabs/train.py +0 -20
- freesolo_flash_dev-0.2.38/flash/providers/runpod/train/__init__.py +0 -115
- freesolo_flash_dev-0.2.38/tests/test_cost_estimate.py +0 -84
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/.dockerignore +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/.env.example +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/.github/workflows/auto-rebake.yml +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/.github/workflows/bake-kernel-cache.yml +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/.github/workflows/ci.yml +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/.github/workflows/main-source-guard.yml +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/.github/workflows/notify-tests-repo.yml +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/.github/workflows/publish-dev.yml +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/.github/workflows/publish-image.yml +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/.github/workflows/publish.yml +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/.github/workflows/version-parity.yml +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/.github/workflows/worker-image.yml +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/.gitignore +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/Dockerfile +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/Dockerfile.worker +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/LICENSE +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/README.md +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/build/kernel_cache/.gitignore +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/build/kernel_cache/.keep +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/docker/Dockerfile.kernelcache +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/docker/Dockerfile.kernelcache.relayer +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/docker/bake_kernel_cache.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/docker/bake_pod_entry.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/docker/make_rp_handler.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/docs/cli-style/README.md +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/docs/cli-style/generate.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/docs/cli-style/index.html +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/docs/cli-style/preview.png +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/docs/cli-style/themed-errors.png +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/docs/kernel-cache.md +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/__init__.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/_channel.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/_fileio.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/_logging.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/_update_check.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/adapter_artifacts.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/catalog.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/cli/__init__.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/cli/__main__.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/cli/_tty.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/cli/commands.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/cli/envpush.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/cli/training_doc.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/client/__init__.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/client/config.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/client/http.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/client/runtime_secrets.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/client/specs.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/cost/__init__.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/cost/spec.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/cost/types.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/__init__.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/accounting.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/chalk_kernels.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/multiturn_rollout.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/recipe.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/vram.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/__init__.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/__main__.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/_pkg.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/adapter.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/decoding.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/finalize.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/gpu_setup.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/grpo.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/heartbeat.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/hf.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/kernel_warmup.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/lora.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/packing.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/perf/__init__.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/perf/attn.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/perf/diagnostics.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/perf/lifecycle.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/perf/liger.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/perf/loraplus.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/perf/memory.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/rl.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/sft.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/engine/worker/wandb_log.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/envs/__init__.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/envs/adapter.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/envs/archive_policy.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/envs/base.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/envs/loader.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/envs/pull.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/envs/registry.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/lora_rank.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/_auth.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/_hf_retry.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/_http.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/lambdalabs/api.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/lambdalabs/auth.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/lambdalabs/preflight.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/lambdalabs/pricing.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/preflight.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/runpod/api.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/runpod/auth.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/runpod/cost.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/runpod/keys.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/runpod/preflight.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/runpod/preload.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/runpod/slots.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/runpod/train/endpoints.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/py.typed +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/runner/checkpoints.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/schema/__init__.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/schema/fields.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/serve/__init__.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/serve/deploy.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/serve/export.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/serve/pricing.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/server/__init__.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/server/__main__.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/server/_deps.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/server/_internal_client.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/server/_locks.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/server/auth.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/server/billing.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/server/billing_retry.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/server/checkpoints.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/server/db.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/server/environment_registry.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/server/envs.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/server/reconcile.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/server/routes/__init__.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/server/routes/envs.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/server/routes/meta.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/server/routes/runs.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/server/routes/serving.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/server/run_registry.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/spec.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/infisical-entrypoint.sh +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/scripts/build_dev_dist.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/__init__.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/_helpers/__init__.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/_helpers/runner.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/_helpers/specs.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/fixtures/math_eval.jsonl +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/fixtures/math_train.jsonl +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/live/__init__.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/live/test_lambda_live.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/live/test_runpod_live.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_agent_flash_cli_contract.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_algorithms.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_b200_rollout_opt.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_backend_jobspec_contract.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_billing_retry.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_catalog_consistency.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_chalk_kernels.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_charge_pricing.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_checkpoints.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_cli_commands.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_cli_errors.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_cli_estimate.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_cli_help.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_cli_managed.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_cli_render_theme.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_client.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_client_server_integration.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_config_overrides.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_cost_analytical.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_cost_equation.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_cost_hardware.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_cost_models.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_cost_rewards.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_dev_channel.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_disk_gb.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_endpoint_name.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_env_cache_evict.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_env_delete.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_env_download.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_env_download_api.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_env_no_install.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_env_publish.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_env_pull.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_env_pull_managed_control_plane.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_env_push.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_env_rate_limit_resolve.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_envs_coverage.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_export.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_flash_mvp.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_flash_worker.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_flashinfer_cache_dirs.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_github_urlopen_retry.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_grpo_mask_aware.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_grpo_sleep_gate.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_health_liveness.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_idle_endpoint_reaper.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_internal_client.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_kernel_cache.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_kernel_fingerprint.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_kv_util.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_logging.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_login_perms.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_lora_rank_preflight.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_managed_hf_repo.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_metrics_schema_agent_contract.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_mig_guard.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_multiturn_rollout.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_open_model_policy.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_packing.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_poll_helpers.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_preflight.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_provider_routing.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_provider_teardown_robustness.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_realized_cost.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_resolve_params_b.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_runmgmt.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_runpod_api_delete.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_runpod_key_fingerprint.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_runpod_key_waterfall.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_runpod_slots.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_serve.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_server_billing.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_server_db.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_serving_contract.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_sft_gc_off.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_spec_and_validation.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_thinking_config.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_update_check.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_verifiers.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_version.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_vl_warmstart_adapter_keys.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_vl_warmstart_recombine.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_vl_weight_sync.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_wandb_naming.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_warmstart_cross_repo.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_worker_dryrun.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_worker_hardexit.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_worker_image.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_worker_init_heartbeat.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_worker_stack.py +0 -0
- {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/tests/test_worker_thinking.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: freesolo-flash-dev
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.39
|
|
4
4
|
Summary: Flash — managed LoRA post-training (SFT/GRPO) for Freesolo environments, driven by the `flash` CLI
|
|
5
5
|
Project-URL: Homepage, https://github.com/freesolo-co/flash
|
|
6
6
|
Project-URL: Repository, https://github.com/freesolo-co/flash
|
|
@@ -143,13 +143,13 @@ def collect_inputs(
|
|
|
143
143
|
|
|
144
144
|
Each value comes from where the IMAGE actually gets it:
|
|
145
145
|
* pins / FROM / causal-conv1d from Dockerfile.worker (what the image is built from),
|
|
146
|
-
* default chalk spec textually from
|
|
146
|
+
* default chalk spec textually from _worker.py (what the bake warms),
|
|
147
147
|
* FA2/FA3 from worker-image.yml's build-args (overridable via fa2_spec/fa3_spec for the
|
|
148
148
|
resolved-build-arg case), and file hashes for the warmup/handler sources.
|
|
149
149
|
base_inputs_partial does NOT yet include fp_cache; compute_fingerprints folds it in.
|
|
150
150
|
"""
|
|
151
151
|
dockerfile = (root / "Dockerfile.worker").read_text()
|
|
152
|
-
|
|
152
|
+
worker_pkg = (root / "flash" / "providers" / "_worker.py").read_text()
|
|
153
153
|
worker_image_yml = (root / ".github" / "workflows" / "worker-image.yml").read_text()
|
|
154
154
|
|
|
155
155
|
specs = _pip_stack_specs(dockerfile)
|
|
@@ -168,7 +168,9 @@ def collect_inputs(
|
|
|
168
168
|
raise ValueError(
|
|
169
169
|
"kernel_fingerprint: fla spec missing or not pinned to a 40-char commit sha"
|
|
170
170
|
)
|
|
171
|
-
chalk = _python_string_constant(
|
|
171
|
+
chalk = _python_string_constant(
|
|
172
|
+
worker_pkg, "DEFAULT_CHALK_SPEC", "_worker.py DEFAULT_CHALK_SPEC"
|
|
173
|
+
)
|
|
172
174
|
|
|
173
175
|
cache_inputs = {
|
|
174
176
|
"from_image": from_image,
|
|
@@ -359,6 +359,8 @@ def _color_json(obj, depth: int) -> str:
|
|
|
359
359
|
|
|
360
360
|
def _hide_provider_metadata(obj):
|
|
361
361
|
"""Styled CLI details are for humans; keep backend provider names out of that view."""
|
|
362
|
+
from flash.providers import PROVIDER_NAMES
|
|
363
|
+
|
|
362
364
|
if isinstance(obj, dict):
|
|
363
365
|
return {
|
|
364
366
|
k: _hide_provider_metadata(v)
|
|
@@ -367,7 +369,7 @@ def _hide_provider_metadata(obj):
|
|
|
367
369
|
}
|
|
368
370
|
if isinstance(obj, list):
|
|
369
371
|
return [_hide_provider_metadata(v) for v in obj]
|
|
370
|
-
if isinstance(obj, str) and obj.lower() in
|
|
372
|
+
if isinstance(obj, str) and obj.lower() in PROVIDER_NAMES:
|
|
371
373
|
return "managed"
|
|
372
374
|
return obj
|
|
373
375
|
|
|
@@ -98,7 +98,6 @@ def seconds_per_step(config: RunConfig, gpu: str) -> float:
|
|
|
98
98
|
reward_s = math.ceil(completions / REWARD_CONCURRENCY) * latency # ceil: a partial wave still costs one latency
|
|
99
99
|
return gen_s + reward_s + update_s
|
|
100
100
|
|
|
101
|
-
|
|
102
101
|
def sft_seconds_for_tokens(config: RunConfig, gpu: str, train_tokens: float) -> float:
|
|
103
102
|
"""SFT steady-state wall time for an actual token count on ``gpu``."""
|
|
104
103
|
n = config.normalized()
|
|
@@ -108,7 +107,7 @@ def sft_seconds_for_tokens(config: RunConfig, gpu: str, train_tokens: float) ->
|
|
|
108
107
|
return flops / (peak * MFU_SFT_TRAIN)
|
|
109
108
|
|
|
110
109
|
|
|
111
|
-
def select_gpu(config: RunConfig) -> tuple[str, int]:
|
|
110
|
+
def select_gpu(config: RunConfig, *, max_wall_seconds: float = 0.0) -> tuple[str, int]:
|
|
112
111
|
"""(chosen GPU class, required VRAM GB): the cheapest fitting class for the cost.
|
|
113
112
|
|
|
114
113
|
Uses ``pick_gpu``, which (unlike the submit-time allocator) intentionally stays gate-free —
|
|
@@ -122,7 +121,7 @@ def select_gpu(config: RunConfig) -> tuple[str, int]:
|
|
|
122
121
|
train=config.train_knobs(),
|
|
123
122
|
thinking=config.thinking,
|
|
124
123
|
)
|
|
125
|
-
gpu = pick_gpu(need, provider=config.provider)
|
|
124
|
+
gpu = pick_gpu(need, provider=config.provider, max_wall_seconds=max_wall_seconds)
|
|
126
125
|
return gpu, need
|
|
127
126
|
|
|
128
127
|
|
|
@@ -153,10 +152,26 @@ def _notes(config: RunConfig, raw_train_s: float, wall_capped: bool, cap_s: floa
|
|
|
153
152
|
|
|
154
153
|
def estimate_cost(config: RunConfig, *, wall_cap_s: float = DEFAULT_WALL_CAP_S) -> CostEstimate:
|
|
155
154
|
"""Deterministic pre-flight cost calculation."""
|
|
156
|
-
|
|
157
|
-
hourly = gpu_hourly_usd(gpu, provider=config.provider)
|
|
158
|
-
# Mirror the runner's max(60, max_wall_seconds) floor so elapsed wall time is not undercounted.
|
|
155
|
+
# Billing cap: mirror the runner's max(60, max_wall_seconds) floor so a sub-60s cap isn't underpriced.
|
|
159
156
|
cap_s = max(60.0, float(config.max_wall_seconds)) if config.max_wall_seconds is not None else wall_cap_s
|
|
157
|
+
# Vast market duration filter: price against offers that outlast the run, using the SAME semantics
|
|
158
|
+
# ``usable_offers`` applies at LAUNCH (not the 60s-floored billing cap_s) — a non-positive wall means
|
|
159
|
+
# NO filter, a positive one is floored at 60s by usable_offers itself:
|
|
160
|
+
# None -> the 24h spec default the run runs under (== DEFAULT_WALL_CAP_S);
|
|
161
|
+
# > 0 -> that wall; <= 0 -> 0.0 (no filter, exactly like launch).
|
|
162
|
+
if config.max_wall_seconds is None:
|
|
163
|
+
market_wall_s = wall_cap_s
|
|
164
|
+
elif config.max_wall_seconds > 0:
|
|
165
|
+
market_wall_s = float(config.max_wall_seconds)
|
|
166
|
+
else:
|
|
167
|
+
market_wall_s = 0.0
|
|
168
|
+
gpu, need = select_gpu(config, max_wall_seconds=market_wall_s)
|
|
169
|
+
# Quote the SAME VRAM-floored Vast market pick_gpu selected under (min_vram_gb=need): without the
|
|
170
|
+
# floor the rate lookup searches from the smallest managed class, letting cheap small-card offers
|
|
171
|
+
# crowd a high-VRAM selection off the limited page -> it silently falls back to the static rate.
|
|
172
|
+
hourly = gpu_hourly_usd(
|
|
173
|
+
gpu, provider=config.provider, max_wall_seconds=market_wall_s, min_vram_gb=need
|
|
174
|
+
)
|
|
160
175
|
|
|
161
176
|
setup = setup_seconds(config)
|
|
162
177
|
sps = seconds_per_step(config, gpu)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Static lookup facts for the cost model: GPU price/VRAM/compute + cheapest-fit
|
|
2
|
+
selection, model size/quant, and reward-grader latency. Pure tables + accessors."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from flash.catalog import MODELS
|
|
7
|
+
from flash.providers.base import GPU_INFO, GpuClass, providers_for
|
|
8
|
+
|
|
9
|
+
GPU_COMPUTE_TFLOPS: dict[str, float] = {
|
|
10
|
+
# A10: 125 TFLOPS dense bf16 tensor (NVIDIA spec); Lambda-only 24 GB class, else defaults to 100.
|
|
11
|
+
"A10": 125.0,
|
|
12
|
+
"RTX 4090": 165.0,
|
|
13
|
+
"RTX 5090": 210.0,
|
|
14
|
+
"A100 PCIe": 312.0,
|
|
15
|
+
"A100 SXM": 312.0,
|
|
16
|
+
# A100 SXM 40GB: same SMs/tensor cores as the 80GB A100 SXM, less HBM only.
|
|
17
|
+
# Without this, 33-40 GB Lambda/Vast quotes fall back to _DEFAULT_TFLOPS.
|
|
18
|
+
"A100 SXM 40GB": 312.0,
|
|
19
|
+
"H100": 990.0,
|
|
20
|
+
# H200: same SMs/tensor cores as H100, more HBM only.
|
|
21
|
+
"H200": 990.0,
|
|
22
|
+
"RTX Pro 6000": 250.0,
|
|
23
|
+
# B200: 2.25 PFLOPS bf16 dense (NVIDIA spec); prevents ~10x cost over-estimate vs _DEFAULT_TFLOPS.
|
|
24
|
+
"B200": 2250.0,
|
|
25
|
+
}
|
|
26
|
+
_DEFAULT_TFLOPS = 100.0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def gpu_tflops(name: str) -> float:
|
|
30
|
+
"""Peak bf16 tensor TFLOPS for a managed GPU class."""
|
|
31
|
+
return GPU_COMPUTE_TFLOPS.get(name, _DEFAULT_TFLOPS)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def gpu_hourly_usd(
|
|
35
|
+
name: str, provider: str | None = None, max_wall_seconds: float = 0.0, min_vram_gb: int = 0
|
|
36
|
+
) -> float:
|
|
37
|
+
"""Representative $/hr for a class, on ``provider`` when given.
|
|
38
|
+
|
|
39
|
+
When ``provider`` is ``lambda`` or ``vast`` and the class is offered there, price it through that
|
|
40
|
+
provider's pricing module (live with a static fallback); otherwise use the RunPod static rate.
|
|
41
|
+
|
|
42
|
+
``max_wall_seconds`` (>0) is threaded into the Vast live market so a duration-bound quote prices
|
|
43
|
+
against offers that outlast the run, not a short-lived one filtered out at launch.
|
|
44
|
+
|
|
45
|
+
``min_vram_gb`` (>0) floors the Vast market search at the job's required VRAM — the SAME floor
|
|
46
|
+
``pick_gpu`` selected under — so a high-VRAM class isn't crowded off the price-sorted page and
|
|
47
|
+
misquoted on the static fallback (selection/quote parity).
|
|
48
|
+
"""
|
|
49
|
+
info = GPU_INFO.get(name)
|
|
50
|
+
if info is None:
|
|
51
|
+
raise KeyError(f"unknown GPU class {name!r}")
|
|
52
|
+
p = (provider or "").strip().lower()
|
|
53
|
+
if p == "lambda" and info.lambda_name:
|
|
54
|
+
from flash.providers import get_provider
|
|
55
|
+
|
|
56
|
+
return get_provider("lambda").hourly_rate(name)
|
|
57
|
+
if p == "vast" and info.vast_name:
|
|
58
|
+
# Vast is a live market whose rates differ materially from RunPod's static ones, so price a
|
|
59
|
+
# provider="vast" quote through the Vast pricing module (live + static fallback).
|
|
60
|
+
from flash.providers.vast.pricing import hourly_rate
|
|
61
|
+
|
|
62
|
+
return hourly_rate(name, max_wall_seconds=max_wall_seconds, min_vram_gb=min_vram_gb)
|
|
63
|
+
return info.hourly_usd
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def gpu_vram_gb(name: str) -> int:
|
|
67
|
+
info = GPU_INFO.get(name)
|
|
68
|
+
if info is None:
|
|
69
|
+
raise KeyError(f"unknown GPU class {name!r}")
|
|
70
|
+
return info.vram_gb
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def pick_gpu(
|
|
74
|
+
required_vram_gb: int, *, provider: str | None = None, max_wall_seconds: float = 0.0
|
|
75
|
+
) -> str:
|
|
76
|
+
"""Cheapest GPU class that fits ``required_vram_gb``, ranked by static $/hr.
|
|
77
|
+
|
|
78
|
+
No pin; every fitting class is eligible, validated or not. NOTE this is intentionally
|
|
79
|
+
gate-free: the submit-time allocator restricts to the validated pool, so the
|
|
80
|
+
actually-provisioned class can be pricier than the one priced here. ``provider`` restricts
|
|
81
|
+
candidates to what it can provision. ``max_wall_seconds`` (>0) prices the Vast market against
|
|
82
|
+
offers that outlast the run, so a long-run quote doesn't SELECT a class on the strength of a
|
|
83
|
+
short-lived offer that won't survive to launch.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def _selectable(g: GpuClass) -> bool:
|
|
87
|
+
return provider in (None, "auto") or provider in providers_for(g.name)
|
|
88
|
+
|
|
89
|
+
candidates = [g for g in GPU_INFO.values() if g.vram_gb >= required_vram_gb and _selectable(g)]
|
|
90
|
+
if not candidates:
|
|
91
|
+
raise ValueError(f"no GPU class fits >= {required_vram_gb} GB")
|
|
92
|
+
# Rank by the rate on the REQUESTED provider, not the RunPod nominal. For Vast, fetch the live offer
|
|
93
|
+
# map ONCE (a duration-bound query bypasses the per-call cache, so pricing per candidate would fire N
|
|
94
|
+
# identical market fetches) and, when reachable, restrict to classes that actually have a rentable
|
|
95
|
+
# offer under the wall cap and rank by LIVE price — else a cheaper class with no surviving offer gets
|
|
96
|
+
# selected/quoted on a static rate the launch path would never rent. Static fallback when offline.
|
|
97
|
+
if (provider or "").strip().lower() == "vast":
|
|
98
|
+
from flash.providers.vast.pricing import live_offer_rates
|
|
99
|
+
|
|
100
|
+
live = live_offer_rates(max_wall_seconds=max_wall_seconds, min_vram_gb=required_vram_gb)
|
|
101
|
+
rentable = [g for g in candidates if g.name in live] if live else []
|
|
102
|
+
if rentable:
|
|
103
|
+
candidates = rentable
|
|
104
|
+
|
|
105
|
+
def _rate(g: GpuClass) -> float:
|
|
106
|
+
return live[g.name]
|
|
107
|
+
else:
|
|
108
|
+
|
|
109
|
+
def _rate(g: GpuClass) -> float:
|
|
110
|
+
return g.hourly_usd
|
|
111
|
+
else:
|
|
112
|
+
|
|
113
|
+
def _rate(g: GpuClass) -> float:
|
|
114
|
+
return gpu_hourly_usd(g.name, provider=provider, max_wall_seconds=max_wall_seconds)
|
|
115
|
+
|
|
116
|
+
best = min(candidates, key=lambda g: (_rate(g), g.vram_gb, g.name))
|
|
117
|
+
return best.name
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def total_params_b(model_id: str) -> float:
|
|
121
|
+
"""Total parameter count (billions) for a catalog model."""
|
|
122
|
+
info = MODELS.get(model_id)
|
|
123
|
+
if info is None:
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"unknown model {model_id!r}; cost estimation supports catalog models only "
|
|
126
|
+
f"({', '.join(MODELS)})"
|
|
127
|
+
)
|
|
128
|
+
return info.params_b
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def active_params_b(model_id: str) -> float:
|
|
132
|
+
"""Active params per token (billions); falls back to total for dense models. Use for FLOPs, not VRAM."""
|
|
133
|
+
info = MODELS.get(model_id)
|
|
134
|
+
if info is None:
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f"unknown model {model_id!r}; cost estimation supports catalog models only "
|
|
137
|
+
f"({', '.join(MODELS)})"
|
|
138
|
+
)
|
|
139
|
+
return info.active_params_b or info.params_b
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def model_quant(model_id: str) -> str:
|
|
143
|
+
"""Quantization of the catalog entry; defaults to 'bf16'."""
|
|
144
|
+
info = MODELS.get(model_id)
|
|
145
|
+
return (info.quant or "bf16") if info is not None else "bf16"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def download_weight_gb(model_id: str) -> float:
|
|
149
|
+
"""Full bf16 checkpoint size in GB (2 bytes/param)."""
|
|
150
|
+
return total_params_b(model_id) * 2.0
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ~1s mid-range default across grader types (regex ~0.01s to LLM judge ~3s).
|
|
154
|
+
AVG_REWARD_SECONDS_PER_COMPLETION = 1.0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def reward_seconds_per_completion(override: float | None = None) -> float:
|
|
158
|
+
"""Per-completion reward latency (s): the explicit override, else the single average."""
|
|
159
|
+
if override is not None:
|
|
160
|
+
return max(0.0, override)
|
|
161
|
+
return AVG_REWARD_SECONDS_PER_COMPLETION
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Pluggable GPU substrates.
|
|
2
|
+
|
|
3
|
+
The training worker (``flash.engine.worker``) reads a JobSpec from the environment, pulls code
|
|
4
|
+
from the HF dataset repo, and streams artifacts/heartbeats/metrics back to it. The provider
|
|
5
|
+
owns pricing, provisioning, polling, cancellation, and teardown.
|
|
6
|
+
|
|
7
|
+
runpod serverless Flash endpoints (always on)
|
|
8
|
+
lambda Lambda Cloud GPU instances (instance-based complement; iff LAMBDA_API_KEY set)
|
|
9
|
+
vast Vast.ai verified-datacenter containers (live-market complement; iff VAST_API_KEY set)
|
|
10
|
+
|
|
11
|
+
This module is the registry: ``get_provider(name)`` / ``PROVIDER_NAMES``.
|
|
12
|
+
``allocator.allocate`` iterates the active provider list below; ``available_providers`` narrows
|
|
13
|
+
it to the ones configured on THIS control plane (Lambda/Vast are opt-in via their operator
|
|
14
|
+
keys, so a box without them silently behaves exactly as the RunPod-only setup).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from functools import cache
|
|
20
|
+
|
|
21
|
+
from flash.providers.base import Provider
|
|
22
|
+
|
|
23
|
+
# Registry / iteration order only — NOT a selection preference. Allocation ranks candidates purely
|
|
24
|
+
# by price (see allocator.allocate), so runpod/lambda/vast get no tie-break edge from this order.
|
|
25
|
+
PROVIDER_NAMES: tuple[str, ...] = ("runpod", "lambda", "vast")
|
|
26
|
+
|
|
27
|
+
# Instance-billed providers: they rent a VM/container that BILLS UNTIL TERMINATED, so they need the
|
|
28
|
+
# generic instance-cleanup paths (retry teardown, gc-by-label) that endpoint-based RunPod doesn't.
|
|
29
|
+
# Single source of truth — keep cleanup/realization sites pointed here so a new instance provider is
|
|
30
|
+
# wired into every reaper by adding ONE name (not by hunting hardcoded ("lambda", "vast") tuples).
|
|
31
|
+
INSTANCE_PROVIDERS: tuple[str, ...] = ("lambda", "vast")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_provider(name: str) -> Provider:
|
|
35
|
+
"""Return the ``Provider`` singleton for a registered name (raises on unknown)."""
|
|
36
|
+
return _get_provider((name or "").strip().lower())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@cache
|
|
40
|
+
def _get_provider(key: str) -> Provider:
|
|
41
|
+
if key == "runpod":
|
|
42
|
+
from flash.providers.runpod import PROVIDER
|
|
43
|
+
|
|
44
|
+
return PROVIDER
|
|
45
|
+
if key == "lambda":
|
|
46
|
+
from flash.providers.lambdalabs import PROVIDER
|
|
47
|
+
|
|
48
|
+
return PROVIDER
|
|
49
|
+
if key == "vast":
|
|
50
|
+
from flash.providers.vast import PROVIDER
|
|
51
|
+
|
|
52
|
+
return PROVIDER
|
|
53
|
+
raise KeyError(f"unknown provider {key!r} (known: {', '.join(PROVIDER_NAMES)})")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def available_providers() -> tuple[str, ...]:
|
|
57
|
+
"""Provider names whose credentials are present on this control plane."""
|
|
58
|
+
return tuple(n for n in PROVIDER_NAMES if get_provider(n).is_configured())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def configured_providers() -> list[Provider]:
|
|
62
|
+
"""The ``Provider`` objects available right now (see ``available_providers``)."""
|
|
63
|
+
return [get_provider(n) for n in available_providers()]
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Provider-neutral HF-artifact reading + heartbeat provenance.
|
|
2
|
+
|
|
3
|
+
The control plane observes a remote worker purely through the artifacts it uploads to a private HF
|
|
4
|
+
dataset repo: rate-limited text/JSON readers for ``heartbeat.json`` / ``error_*.txt`` / console logs,
|
|
5
|
+
plus the provenance predicates that decide whether a heartbeat belongs to THIS attempt or a leftover
|
|
6
|
+
prior one. None of this is provider-specific — RunPod, Lambda, and Vast all poll the same artifact
|
|
7
|
+
shape — so it lives here in the shared kernel and no provider package imports another for it.
|
|
8
|
+
|
|
9
|
+
The worker-side bootstrap (``_instance_bootstrap``) cannot import flash, so it re-implements the
|
|
10
|
+
upload half; this module is the read half every poller shares.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
from flash.providers._poll import _attempt_int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def make_hf_text_reader(hf_repo: str, path_in_repo: str, min_interval_s: float = 45.0):
|
|
23
|
+
"""Rate-limited reader for an HF artifact; returns None until it exists or on any error."""
|
|
24
|
+
state = {"last": 0.0}
|
|
25
|
+
|
|
26
|
+
def read(force: bool = False) -> str | None:
|
|
27
|
+
if not hf_repo:
|
|
28
|
+
return None
|
|
29
|
+
if not force and time.time() - state["last"] < min_interval_s:
|
|
30
|
+
return None
|
|
31
|
+
state["last"] = time.time()
|
|
32
|
+
try:
|
|
33
|
+
from huggingface_hub import hf_hub_download
|
|
34
|
+
|
|
35
|
+
p = hf_hub_download(
|
|
36
|
+
hf_repo,
|
|
37
|
+
path_in_repo,
|
|
38
|
+
repo_type="dataset",
|
|
39
|
+
token=os.environ.get("HF_TOKEN"),
|
|
40
|
+
force_download=True,
|
|
41
|
+
)
|
|
42
|
+
with open(p) as f:
|
|
43
|
+
return f.read()
|
|
44
|
+
except Exception:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
return read
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def make_hf_heartbeat_reader(hf_repo: str, prefix: str, min_interval_s: float = 30.0):
|
|
51
|
+
"""Rate-limited JSON reader for ``{prefix}/heartbeat.json`` on HF."""
|
|
52
|
+
text_reader = make_hf_text_reader(hf_repo, f"{prefix}/heartbeat.json", min_interval_s)
|
|
53
|
+
|
|
54
|
+
def read(force: bool = False) -> dict | None:
|
|
55
|
+
raw = text_reader(force=force)
|
|
56
|
+
if raw is None:
|
|
57
|
+
return None
|
|
58
|
+
try:
|
|
59
|
+
return json.loads(raw)
|
|
60
|
+
except (ValueError, TypeError):
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
return read
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def heartbeat_reader_for(spec):
|
|
67
|
+
"""The HF heartbeat reader for a run's spec (None when the run has no hf_repo)."""
|
|
68
|
+
hf_repo = spec.train.hf_repo
|
|
69
|
+
return make_hf_heartbeat_reader(hf_repo, f"{spec.phase}/{spec.run_id}") if hf_repo else None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def error_artifact_name(phase: str, attempt) -> str:
|
|
73
|
+
"""Worker error-artifact filename for a phase+attempt (mirrors the worker's error_artifact_name)."""
|
|
74
|
+
return f"error_{phase}_attempt{int(attempt or 0)}.txt"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def make_hf_failure_detail_reader(
|
|
78
|
+
hf_repo: str,
|
|
79
|
+
prefix: str,
|
|
80
|
+
phase: str,
|
|
81
|
+
min_interval_s: float = 45.0,
|
|
82
|
+
attempt: int = 0,
|
|
83
|
+
):
|
|
84
|
+
"""Reader for worker-uploaded failure artifacts on HF (error/console txt); force-read after terminal failure."""
|
|
85
|
+
# Attempt-scoped to match the worker's error_artifact_name(mode, attempt).
|
|
86
|
+
err_name = error_artifact_name(phase, attempt)
|
|
87
|
+
error_reader = make_hf_text_reader(hf_repo, f"{prefix}/{err_name}", min_interval_s)
|
|
88
|
+
console_reader = make_hf_text_reader(
|
|
89
|
+
hf_repo, f"{prefix}/console_{phase}.txt", min_interval_s
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def read(force: bool = False) -> str | None:
|
|
93
|
+
parts: list[str] = []
|
|
94
|
+
error_text = error_reader(force=force)
|
|
95
|
+
if error_text:
|
|
96
|
+
parts.append(f"--- {err_name} ---\n{error_text[-4000:]}")
|
|
97
|
+
console_text = console_reader(force=force)
|
|
98
|
+
if console_text:
|
|
99
|
+
parts.append(f"--- console_{phase}.txt ---\n{console_text[-4000:]}")
|
|
100
|
+
return "\n".join(parts) if parts else None
|
|
101
|
+
|
|
102
|
+
return read
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _heartbeat_is_prior_attempt(hb: dict, launch_ts: float | None, current_attempt) -> bool:
|
|
106
|
+
"""Positively attribute a heartbeat to a PRIOR (earlier) attempt: an explicit ``attempt`` differing
|
|
107
|
+
from ``current_attempt`` (definitive provenance — a MATCH proves THIS launch, so a lagging worker-host
|
|
108
|
+
clock cannot demote it), else a parseable ``ts`` predating this attempt's launch. Un-dateable (no
|
|
109
|
+
attempt AND no usable ts) -> False: mere absence of proof is never treated as a leftover. Truthy
|
|
110
|
+
``launch_ts`` only (0.0 = unknown launch, uncomparable). Shared decision core of
|
|
111
|
+
``worker_flagged_retriable`` (honor the retriable flag iff NOT prior) and
|
|
112
|
+
``heartbeat_is_stale_prior_attempt`` (stale iff prior) — one edit site instead of two lockstep copies."""
|
|
113
|
+
hb_attempt = _attempt_int(hb.get("attempt"))
|
|
114
|
+
cur_attempt = _attempt_int(current_attempt)
|
|
115
|
+
if hb_attempt is not None and cur_attempt is not None:
|
|
116
|
+
return hb_attempt != cur_attempt
|
|
117
|
+
if launch_ts:
|
|
118
|
+
try:
|
|
119
|
+
ts = float(hb.get("ts"))
|
|
120
|
+
except (TypeError, ValueError):
|
|
121
|
+
ts = None
|
|
122
|
+
if ts is not None and ts < float(launch_ts):
|
|
123
|
+
return True
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def worker_flagged_retriable(
|
|
128
|
+
heartbeat_reader, *, launch_ts: float | None = None, current_attempt: int | None = None
|
|
129
|
+
) -> bool:
|
|
130
|
+
"""True if the worker stamped ``retriable`` (a RetriableInfraError) in its last heartbeat — the
|
|
131
|
+
structured worker<->poller contract that replaces failure-detail parsing: ``retriable`` means
|
|
132
|
+
retry on a fresh worker. Forces a fresh read past the rate limit.
|
|
133
|
+
|
|
134
|
+
``launch_ts`` / ``current_attempt``, when supplied, gate the flag to THIS attempt. The seed
|
|
135
|
+
heartbeat path is shared across retries, so a leftover ``retriable=True`` from attempt N-1 must
|
|
136
|
+
NOT override attempt N's own (non-retriable) failure marker — otherwise a deterministic
|
|
137
|
+
bootstrap/config error that fails BEFORE this attempt's worker emits any heartbeat would be
|
|
138
|
+
reported job_preempted and burn GPUs on an endless retry instead of failing fast. Only positive
|
|
139
|
+
prior-attempt evidence gates the flag: a ts that predates launch OR an explicit attempt mismatch.
|
|
140
|
+
With NEITHER arg the flag is honored ungated (back-compat for callers that don't date heartbeats)."""
|
|
141
|
+
if heartbeat_reader is None:
|
|
142
|
+
return False
|
|
143
|
+
hb = heartbeat_reader(force=True)
|
|
144
|
+
if not isinstance(hb, dict):
|
|
145
|
+
return False
|
|
146
|
+
if not bool(hb.get("retriable")):
|
|
147
|
+
return False
|
|
148
|
+
if launch_ts is None and current_attempt is None:
|
|
149
|
+
return True # ungated: caller can't date the heartbeat -> preserve prior behavior
|
|
150
|
+
# Honor the retriable flag unless the heartbeat provably belongs to a PRIOR attempt.
|
|
151
|
+
return not _heartbeat_is_prior_attempt(hb, launch_ts, current_attempt)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def heartbeat_is_stale_prior_attempt(
|
|
155
|
+
heartbeat_reader, *, launch_ts: float | None = None, current_attempt: int | None = None
|
|
156
|
+
) -> bool:
|
|
157
|
+
"""True ONLY when a heartbeat can be POSITIVELY attributed to a PRIOR (earlier) attempt — either it
|
|
158
|
+
carries an explicit ``attempt`` that differs from ``current_attempt``, OR a parseable ``ts`` that
|
|
159
|
+
predates THIS attempt's launch. Everything else returns False: no heartbeat, an empty/uninformative
|
|
160
|
+
heartbeat (no ts AND no attempt — e.g. ``{}``), a heartbeat matching this attempt, or one that
|
|
161
|
+
cannot be dated. The asymmetry is deliberate — we suppress a crash classification only on PROOF of
|
|
162
|
+
a leftover, never on mere absence of proof (an un-dateable heartbeat is NOT evidence of a prior run
|
|
163
|
+
and must not mask THIS attempt's deterministic crash).
|
|
164
|
+
|
|
165
|
+
The seed heartbeat path AND the seed-scoped ``error_<phase>.txt`` crash artifact are BOTH shared
|
|
166
|
+
across this seed's retries, so a prior attempt can leave either behind. When the latest heartbeat
|
|
167
|
+
provably belongs to an earlier attempt, the co-located error file is presumed leftover too — so a
|
|
168
|
+
dead-host poll on attempt N must NOT read that stale crash file as THIS attempt's DETERMINISTIC
|
|
169
|
+
failure (which would fail-fast a genuine host LOSS instead of retrying it on a fresh host). Gating
|
|
170
|
+
requires BOTH ``launch_ts`` and ``current_attempt``; without them a heartbeat cannot be dated, so
|
|
171
|
+
it is never called stale (conservative — keep the caller's existing classification)."""
|
|
172
|
+
if heartbeat_reader is None:
|
|
173
|
+
return False
|
|
174
|
+
hb = heartbeat_reader(force=True)
|
|
175
|
+
if not isinstance(hb, dict) or launch_ts is None or current_attempt is None:
|
|
176
|
+
return False
|
|
177
|
+
return _heartbeat_is_prior_attempt(hb, launch_ts, current_attempt)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Shared building blocks for the instance-based providers (
|
|
1
|
+
"""Shared building blocks for the instance-based providers (Lambda, Vast)."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -6,7 +6,9 @@ import base64
|
|
|
6
6
|
import hashlib
|
|
7
7
|
import io
|
|
8
8
|
import json
|
|
9
|
+
from dataclasses import dataclass
|
|
9
10
|
from pathlib import Path
|
|
11
|
+
from typing import Any, ClassVar
|
|
10
12
|
|
|
11
13
|
# Bounded so the name is never truncated at launch — truncation desyncs the sweep-matched prefix.
|
|
12
14
|
_MAX_NAME = 60
|
|
@@ -26,6 +28,14 @@ def run_label_prefix(run_id: str) -> str:
|
|
|
26
28
|
return f"{base[: _PREFIX_BUDGET - 9]}-{h}"
|
|
27
29
|
|
|
28
30
|
|
|
31
|
+
def label_matches_run(label: str, prefix: str) -> bool:
|
|
32
|
+
"""True iff ``label`` belongs to the run whose prefix is ``prefix`` — an EXACT match, or the prefix
|
|
33
|
+
followed by the ``-s`` seed boundary. Boundary-anchored so ``flash-100`` never matches
|
|
34
|
+
``flash-1000-...`` (or vice versa). The single label-ownership test every instance provider's sweep
|
|
35
|
+
and run-scoped teardown shares."""
|
|
36
|
+
return label == prefix or label.startswith(prefix + "-s")
|
|
37
|
+
|
|
38
|
+
|
|
29
39
|
def instance_label(run_id: str, seed: int, attempt: int) -> str:
|
|
30
40
|
"""Instance name: run-derived so ``sweep_orphans`` can tell ours from anything else on the
|
|
31
41
|
account, and bounded (via ``run_label_prefix``) so the provider never truncates it."""
|
|
@@ -45,6 +55,70 @@ def instance_label(run_id: str, seed: int, attempt: int) -> str:
|
|
|
45
55
|
return f"{run_label_prefix(run_id)}-s{seed_s}-a{attempt_s}"
|
|
46
56
|
|
|
47
57
|
|
|
58
|
+
@dataclass
|
|
59
|
+
class InstanceJobHandle:
|
|
60
|
+
"""Fields + (de)serialization common to every rent-a-box provider handle (Lambda, Vast).
|
|
61
|
+
|
|
62
|
+
Persisted in ``RunStatus.remote`` so any process can reattach/cancel (cf. ``base.JobHandle``). Each
|
|
63
|
+
provider subclass tags its ``provider`` and adds its own locator fields (via ``_extra_to_dict`` /
|
|
64
|
+
``_extra_from_dict``); the shared ``to_dict``/``from_dict`` carry the common set so the serialized
|
|
65
|
+
shape stays byte-identical across providers. ``instance_id`` is the poll/destroy target, so unlike
|
|
66
|
+
every other field it has NO safe default — a missing/uncoercible one is a corrupt handle that must
|
|
67
|
+
fail with a clear, actionable error (not a bare KeyError/ValueError that crashes reattach/recovery).
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
instance_id: int | str
|
|
71
|
+
gpu: str
|
|
72
|
+
hourly_usd: float
|
|
73
|
+
attempt: int
|
|
74
|
+
started_ts: float
|
|
75
|
+
|
|
76
|
+
provider: ClassVar[str] = "instance"
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def _coerce_instance_id(raw: Any) -> Any:
|
|
80
|
+
"""Provider-specific instance_id coercion (Vast=int, Lambda=str). Overridden per subclass."""
|
|
81
|
+
return raw
|
|
82
|
+
|
|
83
|
+
def _extra_to_dict(self) -> dict:
|
|
84
|
+
"""Provider-specific fields, serialized between ``instance_id`` and the shared tail."""
|
|
85
|
+
return {}
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def _extra_from_dict(d: dict) -> dict:
|
|
89
|
+
"""Provider-specific fields, parsed from a persisted handle dict (kwargs for the constructor)."""
|
|
90
|
+
return {}
|
|
91
|
+
|
|
92
|
+
def to_dict(self) -> dict:
|
|
93
|
+
return {
|
|
94
|
+
"provider": self.provider,
|
|
95
|
+
"instance_id": self.instance_id,
|
|
96
|
+
**self._extra_to_dict(),
|
|
97
|
+
"gpu": self.gpu,
|
|
98
|
+
"hourly_usd": self.hourly_usd,
|
|
99
|
+
"attempt": self.attempt,
|
|
100
|
+
"started_ts": self.started_ts,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def from_dict(cls, d: dict) -> InstanceJobHandle:
|
|
105
|
+
try:
|
|
106
|
+
instance_id = cls._coerce_instance_id(d["instance_id"])
|
|
107
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"corrupt {cls.provider} handle: missing/non-numeric instance_id "
|
|
110
|
+
f"({d.get('instance_id')!r}) in persisted handle {d!r}"
|
|
111
|
+
) from exc
|
|
112
|
+
return cls(
|
|
113
|
+
instance_id=instance_id,
|
|
114
|
+
gpu=str(d.get("gpu") or ""),
|
|
115
|
+
hourly_usd=float(d.get("hourly_usd") or 0),
|
|
116
|
+
attempt=int(d.get("attempt") or 0),
|
|
117
|
+
started_ts=float(d.get("started_ts") or 0),
|
|
118
|
+
**cls._extra_from_dict(d),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
48
122
|
# Fixed container path the per-region cache is bind-mounted at (host mount differs per provider).
|
|
49
123
|
CACHE_CONTAINER_MOUNT = "/weight-cache"
|
|
50
124
|
CACHE_HF_HOME = f"{CACHE_CONTAINER_MOUNT}/hf-cache"
|
|
@@ -131,7 +205,7 @@ def build_payload(
|
|
|
131
205
|
bits the instance can't infer (HF prefix for markers, wall cap, attempt, and the substrate
|
|
132
206
|
``arm`` that the bootstrap stamps as FLASH_ARM + the marker name)."""
|
|
133
207
|
from flash.envs.registry import worker_pip_for_env
|
|
134
|
-
from flash.providers.
|
|
208
|
+
from flash.providers._worker import (
|
|
135
209
|
build_worker_env,
|
|
136
210
|
chalk_extra_pip,
|
|
137
211
|
strip_runpod_volume_env,
|
{freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.39}/flash/providers/_instance_bootstrap.py
RENAMED
|
@@ -57,6 +57,9 @@ def _code_prefix(payload: dict) -> str:
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def _code_dir(payload: dict) -> str:
|
|
60
|
+
raw = payload.get("code_prefix")
|
|
61
|
+
if not isinstance(raw, str) or not raw.strip():
|
|
62
|
+
return os.path.join(CODE_ROOT, "code")
|
|
60
63
|
return os.path.join(CODE_ROOT, os.path.dirname(_code_prefix(payload)) or ".")
|
|
61
64
|
|
|
62
65
|
|