freesolo-flash-dev 0.2.38__tar.gz → 0.2.40__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.
Files changed (302) hide show
  1. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/PKG-INFO +1 -1
  2. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/docker/kernel_fingerprint.py +5 -3
  3. freesolo_flash_dev-0.2.40/docs/cli-style/train-config-error-after.png +0 -0
  4. freesolo_flash_dev-0.2.40/docs/cli-style/train-config-error-before.png +0 -0
  5. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/cli/render.py +3 -1
  6. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/cost/analytical.py +21 -6
  7. freesolo_flash_dev-0.2.40/flash/cost/facts.py +161 -0
  8. freesolo_flash_dev-0.2.40/flash/providers/__init__.py +63 -0
  9. freesolo_flash_dev-0.2.40/flash/providers/_hf_artifacts.py +177 -0
  10. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/_instance.py +76 -2
  11. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/_instance_bootstrap.py +3 -0
  12. freesolo_flash_dev-0.2.40/flash/providers/_instance_poll.py +438 -0
  13. freesolo_flash_dev-0.2.40/flash/providers/_instance_provider.py +171 -0
  14. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/_poll.py +12 -0
  15. freesolo_flash_dev-0.2.38/flash/providers/runpod/train/deps.py → freesolo_flash_dev-0.2.40/flash/providers/_worker.py +83 -2
  16. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/allocator.py +28 -39
  17. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/base.py +128 -3
  18. freesolo_flash_dev-0.2.40/flash/providers/lambdalabs/__init__.py +154 -0
  19. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/lambdalabs/gpus.py +2 -9
  20. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/lambdalabs/jobs/__init__.py +80 -230
  21. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/lambdalabs/jobs/builders.py +31 -31
  22. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/realized.py +7 -2
  23. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/runpod/__init__.py +22 -1
  24. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/runpod/gpus.py +2 -2
  25. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/runpod/jobs.py +34 -85
  26. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/runpod/pricing.py +2 -2
  27. freesolo_flash_dev-0.2.40/flash/providers/runpod/train/__init__.py +29 -0
  28. freesolo_flash_dev-0.2.40/flash/providers/runpod/train/deps.py +19 -0
  29. freesolo_flash_dev-0.2.40/flash/providers/vast/__init__.py +194 -0
  30. freesolo_flash_dev-0.2.40/flash/providers/vast/api.py +318 -0
  31. freesolo_flash_dev-0.2.40/flash/providers/vast/auth.py +16 -0
  32. freesolo_flash_dev-0.2.40/flash/providers/vast/jobs/__init__.py +656 -0
  33. freesolo_flash_dev-0.2.40/flash/providers/vast/jobs/builders.py +197 -0
  34. freesolo_flash_dev-0.2.40/flash/providers/vast/preflight.py +17 -0
  35. freesolo_flash_dev-0.2.40/flash/providers/vast/pricing.py +147 -0
  36. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/runner/__init__.py +16 -2
  37. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/runner/deploy.py +28 -1
  38. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/runner/lifecycle.py +68 -22
  39. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/schema/__init__.py +18 -2
  40. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/server/_runtime.py +153 -8
  41. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/server/app.py +8 -7
  42. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/pyproject.toml +2 -2
  43. freesolo_flash_dev-0.2.40/tests/_helpers/vast.py +38 -0
  44. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/conftest.py +6 -5
  45. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/live/conftest.py +6 -5
  46. freesolo_flash_dev-0.2.40/tests/live/test_vast_live.py +56 -0
  47. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_allocator.py +117 -0
  48. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_cancel_remote.py +1 -2
  49. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_cli_errors.py +25 -5
  50. freesolo_flash_dev-0.2.40/tests/test_cost_estimate.py +231 -0
  51. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_gpus.py +31 -9
  52. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_grpo_params.py +17 -0
  53. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_jobs.py +153 -17
  54. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_lambda_runner.py +16 -0
  55. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_oom_escalate_gpu.py +7 -0
  56. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_orchestrator_flash.py +4 -4
  57. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_providers_symmetry.py +110 -14
  58. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_resume_on_retry.py +120 -0
  59. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_server_api.py +437 -1
  60. freesolo_flash_dev-0.2.40/tests/test_vast_api.py +462 -0
  61. freesolo_flash_dev-0.2.40/tests/test_vast_offers.py +250 -0
  62. freesolo_flash_dev-0.2.40/tests/test_vast_runner.py +1471 -0
  63. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_weight_cache.py +1 -1
  64. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/uv.lock +1 -1
  65. freesolo_flash_dev-0.2.38/flash/cost/facts.py +0 -110
  66. freesolo_flash_dev-0.2.38/flash/providers/__init__.py +0 -37
  67. freesolo_flash_dev-0.2.38/flash/providers/lambdalabs/__init__.py +0 -122
  68. freesolo_flash_dev-0.2.38/flash/providers/lambdalabs/train.py +0 -20
  69. freesolo_flash_dev-0.2.38/flash/providers/runpod/train/__init__.py +0 -115
  70. freesolo_flash_dev-0.2.38/tests/test_cost_estimate.py +0 -84
  71. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/.dockerignore +0 -0
  72. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/.env.example +0 -0
  73. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/.github/workflows/auto-rebake.yml +0 -0
  74. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/.github/workflows/bake-kernel-cache.yml +0 -0
  75. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/.github/workflows/ci.yml +0 -0
  76. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/.github/workflows/main-source-guard.yml +0 -0
  77. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/.github/workflows/notify-tests-repo.yml +0 -0
  78. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/.github/workflows/publish-dev.yml +0 -0
  79. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/.github/workflows/publish-image.yml +0 -0
  80. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/.github/workflows/publish.yml +0 -0
  81. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/.github/workflows/version-parity.yml +0 -0
  82. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/.github/workflows/worker-image.yml +0 -0
  83. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/.gitignore +0 -0
  84. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/Dockerfile +0 -0
  85. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/Dockerfile.worker +0 -0
  86. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/LICENSE +0 -0
  87. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/README.md +0 -0
  88. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/build/kernel_cache/.gitignore +0 -0
  89. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/build/kernel_cache/.keep +0 -0
  90. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/docker/Dockerfile.kernelcache +0 -0
  91. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/docker/Dockerfile.kernelcache.relayer +0 -0
  92. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/docker/bake_kernel_cache.py +0 -0
  93. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/docker/bake_pod_entry.py +0 -0
  94. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/docker/make_rp_handler.py +0 -0
  95. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/docs/cli-style/README.md +0 -0
  96. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/docs/cli-style/generate.py +0 -0
  97. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/docs/cli-style/index.html +0 -0
  98. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/docs/cli-style/preview.png +0 -0
  99. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/docs/cli-style/themed-errors.png +0 -0
  100. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/docs/kernel-cache.md +0 -0
  101. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/__init__.py +0 -0
  102. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/_channel.py +0 -0
  103. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/_fileio.py +0 -0
  104. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/_logging.py +0 -0
  105. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/_update_check.py +0 -0
  106. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/adapter_artifacts.py +0 -0
  107. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/catalog.py +0 -0
  108. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/cli/__init__.py +0 -0
  109. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/cli/__main__.py +0 -0
  110. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/cli/_tty.py +0 -0
  111. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/cli/commands.py +0 -0
  112. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/cli/envpush.py +0 -0
  113. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/cli/training_doc.py +0 -0
  114. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/client/__init__.py +0 -0
  115. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/client/config.py +0 -0
  116. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/client/http.py +0 -0
  117. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/client/runtime_secrets.py +0 -0
  118. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/client/specs.py +0 -0
  119. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/cost/__init__.py +0 -0
  120. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/cost/spec.py +0 -0
  121. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/cost/types.py +0 -0
  122. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/__init__.py +0 -0
  123. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/accounting.py +0 -0
  124. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/chalk_kernels.py +0 -0
  125. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/multiturn_rollout.py +0 -0
  126. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/recipe.py +0 -0
  127. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/vram.py +0 -0
  128. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/__init__.py +0 -0
  129. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/__main__.py +0 -0
  130. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/_pkg.py +0 -0
  131. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/adapter.py +0 -0
  132. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/decoding.py +0 -0
  133. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/finalize.py +0 -0
  134. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/gpu_setup.py +0 -0
  135. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/grpo.py +0 -0
  136. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/heartbeat.py +0 -0
  137. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/hf.py +0 -0
  138. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/kernel_warmup.py +0 -0
  139. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/lora.py +0 -0
  140. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/packing.py +0 -0
  141. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/perf/__init__.py +0 -0
  142. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/perf/attn.py +0 -0
  143. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/perf/diagnostics.py +0 -0
  144. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/perf/lifecycle.py +0 -0
  145. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/perf/liger.py +0 -0
  146. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/perf/loraplus.py +0 -0
  147. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/perf/memory.py +0 -0
  148. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/rl.py +0 -0
  149. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/sft.py +0 -0
  150. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/engine/worker/wandb_log.py +0 -0
  151. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/envs/__init__.py +0 -0
  152. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/envs/adapter.py +0 -0
  153. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/envs/archive_policy.py +0 -0
  154. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/envs/base.py +0 -0
  155. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/envs/loader.py +0 -0
  156. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/envs/pull.py +0 -0
  157. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/envs/registry.py +0 -0
  158. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/lora_rank.py +0 -0
  159. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/_auth.py +0 -0
  160. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/_hf_retry.py +0 -0
  161. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/_http.py +0 -0
  162. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/lambdalabs/api.py +0 -0
  163. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/lambdalabs/auth.py +0 -0
  164. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/lambdalabs/preflight.py +0 -0
  165. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/lambdalabs/pricing.py +0 -0
  166. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/preflight.py +0 -0
  167. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/runpod/api.py +0 -0
  168. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/runpod/auth.py +0 -0
  169. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/runpod/cost.py +0 -0
  170. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/runpod/keys.py +0 -0
  171. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/runpod/preflight.py +0 -0
  172. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/runpod/preload.py +0 -0
  173. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/runpod/slots.py +0 -0
  174. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/providers/runpod/train/endpoints.py +0 -0
  175. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/py.typed +0 -0
  176. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/runner/checkpoints.py +0 -0
  177. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/schema/fields.py +0 -0
  178. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/serve/__init__.py +0 -0
  179. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/serve/deploy.py +0 -0
  180. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/serve/export.py +0 -0
  181. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/serve/pricing.py +0 -0
  182. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/server/__init__.py +0 -0
  183. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/server/__main__.py +0 -0
  184. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/server/_deps.py +0 -0
  185. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/server/_internal_client.py +0 -0
  186. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/server/_locks.py +0 -0
  187. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/server/auth.py +0 -0
  188. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/server/billing.py +0 -0
  189. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/server/billing_retry.py +0 -0
  190. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/server/checkpoints.py +0 -0
  191. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/server/db.py +0 -0
  192. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/server/environment_registry.py +0 -0
  193. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/server/envs.py +0 -0
  194. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/server/reconcile.py +0 -0
  195. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/server/routes/__init__.py +0 -0
  196. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/server/routes/envs.py +0 -0
  197. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/server/routes/meta.py +0 -0
  198. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/server/routes/runs.py +0 -0
  199. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/server/routes/serving.py +0 -0
  200. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/server/run_registry.py +0 -0
  201. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/flash/spec.py +0 -0
  202. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/infisical-entrypoint.sh +0 -0
  203. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/scripts/build_dev_dist.py +0 -0
  204. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/__init__.py +0 -0
  205. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/_helpers/__init__.py +0 -0
  206. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/_helpers/runner.py +0 -0
  207. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/_helpers/specs.py +0 -0
  208. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/fixtures/math_eval.jsonl +0 -0
  209. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/fixtures/math_train.jsonl +0 -0
  210. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/live/__init__.py +0 -0
  211. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/live/test_lambda_live.py +0 -0
  212. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/live/test_runpod_live.py +0 -0
  213. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_agent_flash_cli_contract.py +0 -0
  214. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_algorithms.py +0 -0
  215. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_b200_rollout_opt.py +0 -0
  216. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_backend_jobspec_contract.py +0 -0
  217. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_billing_retry.py +0 -0
  218. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_catalog_consistency.py +0 -0
  219. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_chalk_kernels.py +0 -0
  220. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_charge_pricing.py +0 -0
  221. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_checkpoints.py +0 -0
  222. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_cli_commands.py +0 -0
  223. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_cli_estimate.py +0 -0
  224. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_cli_help.py +0 -0
  225. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_cli_managed.py +0 -0
  226. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_cli_render_theme.py +0 -0
  227. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_client.py +0 -0
  228. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_client_server_integration.py +0 -0
  229. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_config_overrides.py +0 -0
  230. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_cost_analytical.py +0 -0
  231. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_cost_equation.py +0 -0
  232. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_cost_hardware.py +0 -0
  233. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_cost_models.py +0 -0
  234. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_cost_rewards.py +0 -0
  235. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_dev_channel.py +0 -0
  236. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_disk_gb.py +0 -0
  237. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_endpoint_name.py +0 -0
  238. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_env_cache_evict.py +0 -0
  239. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_env_delete.py +0 -0
  240. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_env_download.py +0 -0
  241. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_env_download_api.py +0 -0
  242. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_env_no_install.py +0 -0
  243. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_env_publish.py +0 -0
  244. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_env_pull.py +0 -0
  245. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_env_pull_managed_control_plane.py +0 -0
  246. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_env_push.py +0 -0
  247. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_env_rate_limit_resolve.py +0 -0
  248. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_envs_coverage.py +0 -0
  249. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_export.py +0 -0
  250. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_flash_mvp.py +0 -0
  251. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_flash_worker.py +0 -0
  252. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_flashinfer_cache_dirs.py +0 -0
  253. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_github_urlopen_retry.py +0 -0
  254. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_grpo_mask_aware.py +0 -0
  255. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_grpo_sleep_gate.py +0 -0
  256. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_health_liveness.py +0 -0
  257. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_idle_endpoint_reaper.py +0 -0
  258. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_internal_client.py +0 -0
  259. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_kernel_cache.py +0 -0
  260. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_kernel_fingerprint.py +0 -0
  261. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_kv_util.py +0 -0
  262. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_logging.py +0 -0
  263. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_login_perms.py +0 -0
  264. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_lora_rank_preflight.py +0 -0
  265. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_managed_hf_repo.py +0 -0
  266. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_metrics_schema_agent_contract.py +0 -0
  267. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_mig_guard.py +0 -0
  268. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_multiturn_rollout.py +0 -0
  269. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_open_model_policy.py +0 -0
  270. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_packing.py +0 -0
  271. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_poll_helpers.py +0 -0
  272. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_preflight.py +0 -0
  273. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_provider_routing.py +0 -0
  274. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_provider_teardown_robustness.py +0 -0
  275. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_realized_cost.py +0 -0
  276. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_resolve_params_b.py +0 -0
  277. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_runmgmt.py +0 -0
  278. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_runpod_api_delete.py +0 -0
  279. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_runpod_key_fingerprint.py +0 -0
  280. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_runpod_key_waterfall.py +0 -0
  281. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_runpod_slots.py +0 -0
  282. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_serve.py +0 -0
  283. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_server_billing.py +0 -0
  284. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_server_db.py +0 -0
  285. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_serving_contract.py +0 -0
  286. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_sft_gc_off.py +0 -0
  287. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_spec_and_validation.py +0 -0
  288. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_thinking_config.py +0 -0
  289. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_update_check.py +0 -0
  290. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_verifiers.py +0 -0
  291. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_version.py +0 -0
  292. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_vl_warmstart_adapter_keys.py +0 -0
  293. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_vl_warmstart_recombine.py +0 -0
  294. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_vl_weight_sync.py +0 -0
  295. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_wandb_naming.py +0 -0
  296. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_warmstart_cross_repo.py +0 -0
  297. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_worker_dryrun.py +0 -0
  298. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_worker_hardexit.py +0 -0
  299. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_worker_image.py +0 -0
  300. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_worker_init_heartbeat.py +0 -0
  301. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/tests/test_worker_stack.py +0 -0
  302. {freesolo_flash_dev-0.2.38 → freesolo_flash_dev-0.2.40}/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.38
3
+ Version: 0.2.40
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 deps.py (what the bake warms),
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
- deps = (root / "flash" / "providers" / "runpod" / "train" / "deps.py").read_text()
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(deps, "DEFAULT_CHALK_SPEC", "deps.py DEFAULT_CHALK_SPEC")
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 {"runpod", "lambda"}:
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
- gpu, need = select_gpu(config)
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 (e.g. Lambda)."""
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.runpod.train import (
208
+ from flash.providers._worker import (
135
209
  build_worker_env,
136
210
  chalk_extra_pip,
137
211
  strip_runpod_volume_env,
@@ -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