freesolo-flash-dev 0.2.43__tar.gz → 0.2.44__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 (300) hide show
  1. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/PKG-INFO +1 -1
  2. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/cli/commands.py +21 -1
  3. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/hf.py +41 -112
  4. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/_worker.py +6 -5
  5. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/pyproject.toml +2 -2
  6. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_checkpoints.py +59 -170
  7. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_cli_commands.py +34 -0
  8. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_flash_worker.py +1 -1
  9. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/.dockerignore +0 -0
  10. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/.env.example +0 -0
  11. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/.github/workflows/auto-rebake.yml +0 -0
  12. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/.github/workflows/bake-kernel-cache.yml +0 -0
  13. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/.github/workflows/ci.yml +0 -0
  14. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/.github/workflows/main-source-guard.yml +0 -0
  15. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/.github/workflows/notify-tests-repo.yml +0 -0
  16. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/.github/workflows/publish-dev.yml +0 -0
  17. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/.github/workflows/publish-image.yml +0 -0
  18. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/.github/workflows/publish.yml +0 -0
  19. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/.github/workflows/version-parity.yml +0 -0
  20. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/.github/workflows/worker-image.yml +0 -0
  21. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/.gitignore +0 -0
  22. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/Dockerfile +0 -0
  23. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/Dockerfile.worker +0 -0
  24. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/LICENSE +0 -0
  25. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/README.md +0 -0
  26. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/build/kernel_cache/.gitignore +0 -0
  27. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/build/kernel_cache/.keep +0 -0
  28. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/docker/Dockerfile.kernelcache +0 -0
  29. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/docker/Dockerfile.kernelcache.relayer +0 -0
  30. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/docker/bake_kernel_cache.py +0 -0
  31. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/docker/bake_pod_entry.py +0 -0
  32. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/docker/kernel_fingerprint.py +0 -0
  33. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/docker/make_rp_handler.py +0 -0
  34. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/docs/cli-style/README.md +0 -0
  35. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/docs/cli-style/env-setup-survey-dark.png +0 -0
  36. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/docs/cli-style/env-setup-survey-light.png +0 -0
  37. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/docs/cli-style/generate.py +0 -0
  38. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/docs/cli-style/index.html +0 -0
  39. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/docs/cli-style/preview.png +0 -0
  40. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/docs/cli-style/themed-errors.png +0 -0
  41. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/docs/cli-style/train-config-error-after.png +0 -0
  42. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/docs/cli-style/train-config-error-before.png +0 -0
  43. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/docs/kernel-cache.md +0 -0
  44. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/__init__.py +0 -0
  45. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/_channel.py +0 -0
  46. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/_fileio.py +0 -0
  47. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/_logging.py +0 -0
  48. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/_update_check.py +0 -0
  49. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/adapter_artifacts.py +0 -0
  50. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/catalog.py +0 -0
  51. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/cli/__init__.py +0 -0
  52. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/cli/__main__.py +0 -0
  53. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/cli/_tty.py +0 -0
  54. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/cli/envpush.py +0 -0
  55. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/cli/render.py +0 -0
  56. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/cli/training_doc.py +0 -0
  57. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/client/__init__.py +0 -0
  58. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/client/config.py +0 -0
  59. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/client/http.py +0 -0
  60. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/client/runtime_secrets.py +0 -0
  61. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/client/specs.py +0 -0
  62. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/cost/__init__.py +0 -0
  63. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/cost/analytical.py +0 -0
  64. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/cost/facts.py +0 -0
  65. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/cost/spec.py +0 -0
  66. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/cost/types.py +0 -0
  67. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/__init__.py +0 -0
  68. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/accounting.py +0 -0
  69. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/chalk_kernels.py +0 -0
  70. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/multiturn_rollout.py +0 -0
  71. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/recipe.py +0 -0
  72. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/vram.py +0 -0
  73. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/__init__.py +0 -0
  74. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/__main__.py +0 -0
  75. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/_pkg.py +0 -0
  76. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/adapter.py +0 -0
  77. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/decoding.py +0 -0
  78. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/finalize.py +0 -0
  79. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/gpu_setup.py +0 -0
  80. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/grpo.py +0 -0
  81. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/heartbeat.py +0 -0
  82. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/kernel_warmup.py +0 -0
  83. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/lora.py +0 -0
  84. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/packing.py +0 -0
  85. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/perf/__init__.py +0 -0
  86. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/perf/attn.py +0 -0
  87. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/perf/diagnostics.py +0 -0
  88. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/perf/lifecycle.py +0 -0
  89. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/perf/liger.py +0 -0
  90. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/perf/loraplus.py +0 -0
  91. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/perf/memory.py +0 -0
  92. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/rl.py +0 -0
  93. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/sft.py +0 -0
  94. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/engine/worker/wandb_log.py +0 -0
  95. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/envs/__init__.py +0 -0
  96. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/envs/adapter.py +0 -0
  97. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/envs/archive_policy.py +0 -0
  98. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/envs/base.py +0 -0
  99. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/envs/loader.py +0 -0
  100. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/envs/pull.py +0 -0
  101. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/envs/registry.py +0 -0
  102. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/lora_rank.py +0 -0
  103. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/__init__.py +0 -0
  104. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/_auth.py +0 -0
  105. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/_hf_artifacts.py +0 -0
  106. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/_hf_retry.py +0 -0
  107. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/_http.py +0 -0
  108. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/_instance.py +0 -0
  109. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/_instance_bootstrap.py +0 -0
  110. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/_instance_poll.py +0 -0
  111. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/_instance_provider.py +0 -0
  112. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/_poll.py +0 -0
  113. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/allocator.py +0 -0
  114. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/base.py +0 -0
  115. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/lambdalabs/__init__.py +0 -0
  116. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/lambdalabs/api.py +0 -0
  117. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/lambdalabs/auth.py +0 -0
  118. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/lambdalabs/gpus.py +0 -0
  119. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/lambdalabs/jobs/__init__.py +0 -0
  120. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/lambdalabs/jobs/builders.py +0 -0
  121. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/lambdalabs/preflight.py +0 -0
  122. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/lambdalabs/pricing.py +0 -0
  123. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/preflight.py +0 -0
  124. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/realized.py +0 -0
  125. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/runpod/__init__.py +0 -0
  126. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/runpod/api.py +0 -0
  127. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/runpod/auth.py +0 -0
  128. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/runpod/cost.py +0 -0
  129. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/runpod/gpus.py +0 -0
  130. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/runpod/jobs.py +0 -0
  131. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/runpod/keys.py +0 -0
  132. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/runpod/preflight.py +0 -0
  133. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/runpod/preload.py +0 -0
  134. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/runpod/pricing.py +0 -0
  135. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/runpod/slots.py +0 -0
  136. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/runpod/train/__init__.py +0 -0
  137. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/runpod/train/deps.py +0 -0
  138. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/runpod/train/endpoints.py +0 -0
  139. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/vast/__init__.py +0 -0
  140. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/vast/api.py +0 -0
  141. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/vast/auth.py +0 -0
  142. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/vast/jobs/__init__.py +0 -0
  143. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/vast/jobs/builders.py +0 -0
  144. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/vast/preflight.py +0 -0
  145. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/providers/vast/pricing.py +0 -0
  146. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/py.typed +0 -0
  147. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/runner/__init__.py +0 -0
  148. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/runner/checkpoints.py +0 -0
  149. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/runner/deploy.py +0 -0
  150. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/runner/lifecycle.py +0 -0
  151. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/schema/__init__.py +0 -0
  152. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/schema/fields.py +0 -0
  153. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/serve/__init__.py +0 -0
  154. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/serve/deploy.py +0 -0
  155. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/serve/export.py +0 -0
  156. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/serve/pricing.py +0 -0
  157. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/server/__init__.py +0 -0
  158. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/server/__main__.py +0 -0
  159. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/server/_deps.py +0 -0
  160. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/server/_internal_client.py +0 -0
  161. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/server/_locks.py +0 -0
  162. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/server/_runtime.py +0 -0
  163. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/server/app.py +0 -0
  164. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/server/auth.py +0 -0
  165. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/server/billing.py +0 -0
  166. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/server/billing_retry.py +0 -0
  167. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/server/checkpoints.py +0 -0
  168. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/server/db.py +0 -0
  169. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/server/environment_registry.py +0 -0
  170. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/server/envs.py +0 -0
  171. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/server/reconcile.py +0 -0
  172. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/server/routes/__init__.py +0 -0
  173. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/server/routes/envs.py +0 -0
  174. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/server/routes/meta.py +0 -0
  175. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/server/routes/runs.py +0 -0
  176. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/server/routes/serving.py +0 -0
  177. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/server/run_registry.py +0 -0
  178. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/flash/spec.py +0 -0
  179. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/infisical-entrypoint.sh +0 -0
  180. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/scripts/build_dev_dist.py +0 -0
  181. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/__init__.py +0 -0
  182. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/_helpers/__init__.py +0 -0
  183. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/_helpers/runner.py +0 -0
  184. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/_helpers/specs.py +0 -0
  185. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/_helpers/vast.py +0 -0
  186. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/conftest.py +0 -0
  187. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/fixtures/math_eval.jsonl +0 -0
  188. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/fixtures/math_train.jsonl +0 -0
  189. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/live/__init__.py +0 -0
  190. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/live/conftest.py +0 -0
  191. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/live/test_lambda_live.py +0 -0
  192. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/live/test_runpod_live.py +0 -0
  193. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/live/test_vast_live.py +0 -0
  194. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_agent_flash_cli_contract.py +0 -0
  195. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_algorithms.py +0 -0
  196. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_allocator.py +0 -0
  197. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_b200_rollout_opt.py +0 -0
  198. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_backend_jobspec_contract.py +0 -0
  199. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_billing_retry.py +0 -0
  200. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_cancel_remote.py +0 -0
  201. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_catalog_consistency.py +0 -0
  202. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_chalk_kernels.py +0 -0
  203. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_charge_pricing.py +0 -0
  204. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_cli_errors.py +0 -0
  205. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_cli_estimate.py +0 -0
  206. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_cli_help.py +0 -0
  207. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_cli_managed.py +0 -0
  208. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_cli_render_theme.py +0 -0
  209. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_client.py +0 -0
  210. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_client_server_integration.py +0 -0
  211. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_config_overrides.py +0 -0
  212. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_cost_analytical.py +0 -0
  213. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_cost_equation.py +0 -0
  214. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_cost_estimate.py +0 -0
  215. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_cost_hardware.py +0 -0
  216. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_cost_models.py +0 -0
  217. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_cost_rewards.py +0 -0
  218. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_dev_channel.py +0 -0
  219. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_disk_gb.py +0 -0
  220. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_endpoint_name.py +0 -0
  221. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_env_cache_evict.py +0 -0
  222. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_env_delete.py +0 -0
  223. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_env_download.py +0 -0
  224. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_env_download_api.py +0 -0
  225. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_env_no_install.py +0 -0
  226. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_env_publish.py +0 -0
  227. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_env_pull.py +0 -0
  228. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_env_pull_managed_control_plane.py +0 -0
  229. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_env_push.py +0 -0
  230. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_env_rate_limit_resolve.py +0 -0
  231. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_envs_coverage.py +0 -0
  232. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_export.py +0 -0
  233. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_flash_mvp.py +0 -0
  234. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_flashinfer_cache_dirs.py +0 -0
  235. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_github_urlopen_retry.py +0 -0
  236. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_gpus.py +0 -0
  237. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_grpo_mask_aware.py +0 -0
  238. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_grpo_params.py +0 -0
  239. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_grpo_sleep_gate.py +0 -0
  240. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_health_liveness.py +0 -0
  241. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_idle_endpoint_reaper.py +0 -0
  242. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_internal_client.py +0 -0
  243. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_jobs.py +0 -0
  244. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_kernel_cache.py +0 -0
  245. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_kernel_fingerprint.py +0 -0
  246. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_kv_util.py +0 -0
  247. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_lambda_runner.py +0 -0
  248. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_logging.py +0 -0
  249. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_login_perms.py +0 -0
  250. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_lora_rank_preflight.py +0 -0
  251. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_managed_hf_repo.py +0 -0
  252. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_metrics_schema_agent_contract.py +0 -0
  253. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_mig_guard.py +0 -0
  254. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_multiturn_rollout.py +0 -0
  255. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_oom_escalate_gpu.py +0 -0
  256. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_open_model_policy.py +0 -0
  257. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_orchestrator_flash.py +0 -0
  258. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_packing.py +0 -0
  259. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_poll_helpers.py +0 -0
  260. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_preflight.py +0 -0
  261. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_provider_routing.py +0 -0
  262. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_provider_teardown_robustness.py +0 -0
  263. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_providers_symmetry.py +0 -0
  264. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_realized_cost.py +0 -0
  265. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_resolve_params_b.py +0 -0
  266. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_resume_on_retry.py +0 -0
  267. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_runmgmt.py +0 -0
  268. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_runpod_api_delete.py +0 -0
  269. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_runpod_key_fingerprint.py +0 -0
  270. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_runpod_key_waterfall.py +0 -0
  271. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_runpod_slots.py +0 -0
  272. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_serve.py +0 -0
  273. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_server_api.py +0 -0
  274. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_server_billing.py +0 -0
  275. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_server_db.py +0 -0
  276. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_serving_contract.py +0 -0
  277. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_sft_example_selection.py +0 -0
  278. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_sft_gc_off.py +0 -0
  279. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_spec_and_validation.py +0 -0
  280. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_thinking_config.py +0 -0
  281. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_train_context_preflight.py +0 -0
  282. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_update_check.py +0 -0
  283. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_vast_api.py +0 -0
  284. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_vast_offers.py +0 -0
  285. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_vast_runner.py +0 -0
  286. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_verifiers.py +0 -0
  287. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_version.py +0 -0
  288. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_vl_warmstart_adapter_keys.py +0 -0
  289. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_vl_warmstart_recombine.py +0 -0
  290. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_vl_weight_sync.py +0 -0
  291. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_wandb_naming.py +0 -0
  292. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_warmstart_cross_repo.py +0 -0
  293. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_weight_cache.py +0 -0
  294. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_worker_dryrun.py +0 -0
  295. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_worker_hardexit.py +0 -0
  296. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_worker_image.py +0 -0
  297. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_worker_init_heartbeat.py +0 -0
  298. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_worker_stack.py +0 -0
  299. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/tests/test_worker_thinking.py +0 -0
  300. {freesolo_flash_dev-0.2.43 → freesolo_flash_dev-0.2.44}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: freesolo-flash-dev
3
- Version: 0.2.43
3
+ Version: 0.2.44
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
@@ -783,12 +783,32 @@ def cmd_runs(args) -> int:
783
783
 
784
784
 
785
785
  def cmd_cancel(args) -> int:
786
- status = client_from_config().cancel_run(args.run_id)
786
+ client = client_from_config()
787
+ status = client.cancel_run(args.run_id)
787
788
  payload = {"run_id": args.run_id, "state": status["state"]}
789
+ # A cancelled run is not necessarily worthless: every completed save interval already streamed
790
+ # a deployable checkpoint, even though the run shows adapter_ref=null / cost=0. Surface the
791
+ # surviving steps here so the run isn't discarded unseen. Best-effort: cancel never fails on it.
792
+ checkpoints: list[dict] = []
793
+ if payload["state"] == "cancelled":
794
+ try:
795
+ checkpoints = client.checkpoints(args.run_id)
796
+ except Exception:
797
+ checkpoints = []
788
798
  if render.styled():
789
799
  print(render.cancelled(payload))
790
800
  else:
791
801
  print(json.dumps(payload, indent=2))
802
+ if checkpoints:
803
+ top = max(c["step"] for c in checkpoints)
804
+ # stderr in the plain path so the machine-readable stdout JSON stays untouched.
805
+ out = sys.stdout if render.styled() else sys.stderr
806
+ msg = (
807
+ f"{len(checkpoints)} deployable checkpoint(s) survive this cancel — list with "
808
+ f"`flash checkpoints {args.run_id}`, deploy one with "
809
+ f"`flash deploy {args.run_id}/step-{top}`."
810
+ )
811
+ print(render.note(msg) if render.styled() else msg, file=out)
792
812
  return 0
793
813
 
794
814
 
@@ -310,11 +310,13 @@ def publish_deployable_checkpoint(
310
310
  return None
311
311
 
312
312
 
313
- # Daemon upload threads are killed on worker exit; on_train_end blocks up to this long to flush.
314
- _CKPT_FLUSH_TIMEOUT_S = 300.0
315
- # Retry/backoff for the lock-timeout fallback publish (rides out a transient concurrent-commit 409).
313
+ # Retry/backoff for the deployable-adapter publish (rides out a transient concurrent-commit 409).
316
314
  _CKPT_FLUSH_RETRIES = 3
317
315
  _CKPT_FLUSH_BACKOFF_S = 1.0
316
+ # Retry/backoff for each synchronous checkpoint upload. `on_save` BLOCKS the training loop on the
317
+ # upload, so a transient HF error is retried until the step lands rather than costing the step.
318
+ _CKPT_UPLOAD_RETRIES = 3
319
+ _CKPT_UPLOAD_BACKOFF_S = 5.0
318
320
 
319
321
 
320
322
  def _latest_checkpoint_dir(output_dir: str) -> tuple[int, str] | None:
@@ -368,72 +370,14 @@ def _prune_stale_resume_checkpoints(keep_step: int) -> None:
368
370
  api.delete_folder(path_in_repo=folder, repo_id=_w.HF_REPO, repo_type="dataset")
369
371
 
370
372
 
371
- class _UploadPump:
372
- """Coalescing single-slot upload queue: at most one upload in flight, at most one queued.
373
-
374
- ONE lock guards two fields — ``uploading`` and ``queued`` (newest wins). Every producer path
375
- is enqueue-then-pump and the upload thread's finally is done()-then-pump, so a save that
376
- lands while the in-flight upload is draining can never be lost (no check-then-set window
377
- between separate locks).
378
- """
379
-
380
- def __init__(self, start_upload) -> None:
381
- self._cond = threading.Condition()
382
- self._start_upload = start_upload # (step, ckpt_dir) -> spawns thread; must call done()
383
- self.uploading = False
384
- self.queued: tuple[int, str] | None = None
385
-
386
- def enqueue(self, step: int, ckpt_dir: str) -> None:
387
- """Queue `step` (replacing any older queued step) and start it if idle."""
388
- with self._cond:
389
- busy = self.uploading
390
- self.queued = (step, ckpt_dir)
391
- if busy:
392
- print(
393
- f"[ckpt] upload busy; queued step {step} (uploads when the in-flight one finishes)"
394
- )
395
- self.pump()
396
-
397
- def pump(self) -> None:
398
- """Atomically start the queued upload iff nothing is in flight."""
399
- with self._cond:
400
- if self.uploading or self.queued is None:
401
- return
402
- step, ckpt_dir = self.queued
403
- self.queued = None
404
- gone = not os.path.isdir(ckpt_dir)
405
- if not gone:
406
- self.uploading = True
407
- if gone: # pruned by the trainer's save_total_limit meanwhile
408
- print(f"[ckpt] queued step {step} checkpoint dir is gone; dropping")
409
- return
410
- self._start_upload(step, ckpt_dir)
411
-
412
- def done(self) -> None:
413
- """Mark the in-flight upload finished, then pump any queued step."""
414
- with self._cond:
415
- self.uploading = False
416
- self._cond.notify_all()
417
- self.pump()
418
-
419
- def take_queued(self) -> tuple[int, str] | None:
420
- """Remove and return the queued step, if any (for a synchronous train-end flush)."""
421
- with self._cond:
422
- queued, self.queued = self.queued, None
423
- return queued
424
-
425
- def wait_idle(self, timeout: float) -> bool:
426
- """Wait up to `timeout` seconds for the in-flight upload (if any) to finish."""
427
- with self._cond:
428
- return self._cond.wait_for(lambda: not self.uploading, timeout)
429
-
430
-
431
373
  def make_checkpoint_upload_callback():
432
374
  """Return a TrainerCallback that streams each save to HF and publishes deployable per-step adapters.
433
375
 
434
- Uploads run in a background daemon thread; a save that fires while one is in flight is queued
435
- (newest wins) and uploaded next. on_train_end flushes the queue and the final deployable
436
- synchronously before exit.
376
+ Uploads are SYNCHRONOUS: on_save blocks the training loop until the checkpoint is durably on
377
+ HF. A save therefore never returns while its upload is still running, so there is never a
378
+ second upload in flight when the next save fires — the "upload busy" contention (and the
379
+ coalescing/dropping it used to cause) cannot happen by construction. Every `save_every` step is
380
+ guaranteed to upload, retrying transient HF errors instead of skipping.
437
381
  """
438
382
  from transformers import TrainerCallback
439
383
 
@@ -491,72 +435,57 @@ def make_checkpoint_upload_callback():
491
435
  _prune_stale_resume_checkpoints(step)
492
436
  _w.heartbeat("checkpoint_uploaded", step=step)
493
437
 
494
- def _start_upload(step: int, ckpt_dir: str) -> None:
495
- """Upload `step` in a background thread; the pump is marked done (and re-pumped) after."""
438
+ uploaded_steps: set[int] = set()
496
439
 
497
- def _upload():
440
+ def _upload_with_retries(step: int, ckpt_dir: str) -> bool:
441
+ """Run the full upload for `step`, retrying transient failures; True once it lands."""
442
+ for attempt in range(_CKPT_UPLOAD_RETRIES):
498
443
  try:
499
444
  _upload_once(step, ckpt_dir)
445
+ uploaded_steps.add(step)
446
+ return True
500
447
  except Exception as e:
501
- print("ckpt upload warn:", e)
502
- finally:
503
- pump.done()
504
-
505
- threading.Thread(target=_upload, daemon=True).start()
506
-
507
- # `save_every` must not be advisory: a save that fires while a prior upload is still in
508
- # flight is QUEUED (newest wins) and uploaded as soon as the in-flight one finishes, instead
509
- # of being dropped with "upload busy; skipping step N". Coalescing to the newest pending step
510
- # keeps at most one waiter, so a slow uplink can't pile up threads, while the registered step
511
- # list stays as dense as the uplink allows.
512
- pump = _UploadPump(_start_upload)
448
+ if attempt + 1 < _CKPT_UPLOAD_RETRIES:
449
+ print(
450
+ f"[ckpt] step {step} upload retry {attempt + 1}/{_CKPT_UPLOAD_RETRIES}: {e}"
451
+ )
452
+ time.sleep(_CKPT_UPLOAD_BACKOFF_S * (attempt + 1))
453
+ else:
454
+ print(
455
+ f"[ckpt] step {step} upload FAILED after {_CKPT_UPLOAD_RETRIES} attempts: {e}"
456
+ )
457
+ with contextlib.suppress(Exception):
458
+ _w.heartbeat("checkpoint_upload_failed", step=step)
459
+ return False
513
460
 
514
461
  class _CheckpointUpload(TrainerCallback):
515
462
  def on_save(self, args, state, control, **kwargs):
463
+ # SYNCHRONOUS on the training thread: the trainer blocks here until this checkpoint is
464
+ # uploaded. `save_total_limit=1` rotation (which runs earlier, inside this same save's
465
+ # `_save_checkpoint`) deletes the PREVIOUS checkpoint, never the current one — and the
466
+ # next save can't begin until we return — so the dir is stable for the whole upload and
467
+ # no snapshot/queue is needed. Because the upload finishes before the save returns,
468
+ # there is never a concurrent upload for a later save to be "busy" against.
516
469
  if not _w.HF_REPO:
517
470
  return
518
471
  step = int(state.global_step)
519
472
  ckpt_dir = os.path.join(args.output_dir, f"checkpoint-{step}")
520
473
  if not os.path.isdir(ckpt_dir):
521
474
  return
522
- pump.enqueue(step, ckpt_dir)
475
+ _upload_with_retries(step, ckpt_dir)
523
476
 
524
477
  def on_train_end(self, args, state, control, **kwargs):
525
- # Daemon threads are killed on exit; flush synchronously here.
478
+ # Safety net for a final checkpoint the trainer wrote without an on_save (e.g. a
479
+ # load_best_model_at_end / end-of-training save). Synchronous on_save already uploaded
480
+ # every step it saw, so this only publishes an as-yet-unuploaded latest step.
526
481
  if not _w.HF_REPO:
527
482
  return
528
483
  latest = _latest_checkpoint_dir(args.output_dir)
529
484
  if latest is None:
530
485
  return
531
486
  step, ckpt_dir = latest
532
- # A queued save the pump never started must not be silently dropped: take it out
533
- # of the pump (so a racing done()->pump() can't double-start it) and run it here.
534
- queued = pump.take_queued()
535
- idle = pump.wait_idle(_CKPT_FLUSH_TIMEOUT_S)
536
- if queued is not None and os.path.isdir(queued[1]):
537
- q_step, q_dir = queued
538
- if idle:
539
- try:
540
- _upload_once(q_step, q_dir)
541
- except Exception as e:
542
- print("ckpt upload warn:", e)
543
- else:
544
- # Another upload is still pruning latest-only resume checkpoints. Publish only
545
- # the durable per-step adapter here so concurrent prune passes cannot delete a
546
- # newer resume checkpoint while it is being flushed.
547
- try:
548
- _publish_deployable_recombined(q_dir, q_step, with_retry=True)
549
- except Exception as e:
550
- print("ckpt deployable flush warn:", e)
551
- if q_step == step:
552
- return # the queued flush already published the final deployable
553
- if idle:
554
- _publish_deployable_recombined(ckpt_dir, step)
555
- else:
556
- print(
557
- f"[ckpt] upload still in flight after {_CKPT_FLUSH_TIMEOUT_S:.0f}s; publishing "
558
- f"final deployable (step {step}) alongside it"
559
- )
560
- _publish_deployable_recombined(ckpt_dir, step, with_retry=True)
487
+ if step in uploaded_steps:
488
+ return
489
+ _upload_with_retries(step, ckpt_dir)
561
490
 
562
491
  return _CheckpointUpload()
@@ -112,13 +112,14 @@ def _effective_worker_env(spec=None) -> dict[str, str]:
112
112
  # Chalk is published to PUBLIC PyPI on every version bump (chalk .github/workflows/publish.yml).
113
113
  # Pin the exact PUBLIC version so worker + kernel-cache-bake pods install with NO GitHub auth — the
114
114
  # chalk repo is INTERNAL, so a git+https default made installs fail wherever GITHUB_TOKEN is absent
115
- # (bake pod, tokenless workers). freesolo-chalk 0.5.2 = chalk dev->main sync #44 (standalone, Liger
116
- # fully replaced; adds arch-TUNED gdn_gated_rmsnorm + lora on sm89/sm90/sm100, MoE grouped-GEMM,
117
- # serving surface, MiniCPM/Llama kernels). Bump DEFAULT_CHALK_VERSION when the merged kernel surface moves.
118
- DEFAULT_CHALK_VERSION = "0.5.2"
115
+ # (bake pod, tokenless workers). freesolo-chalk 0.5.3 = chalk dev->main sync #67 (adds the decode
116
+ # serving kernels rope + GDN gated-rmsnorm; GPU-A/B'd launch tuning: flce chunk_mult 16, rope
117
+ # num_warps=2, gdn_conv warps formula, lora fwd autotune candidate; OP_MEAS_OVERRIDE; flce
118
+ # chunk_mult tradeoff docs). Bump DEFAULT_CHALK_VERSION when the merged kernel surface moves.
119
+ DEFAULT_CHALK_VERSION = "0.5.3"
119
120
  DEFAULT_CHALK_SPEC = f"freesolo-chalk=={DEFAULT_CHALK_VERSION}"
120
121
  # Provenance only (kernel-cache fingerprint / traceability): the chalk commit this version was cut from.
121
- LATEST_CHALK_MAIN_SHA = "37b2fb9e1a123ceb902ba6ffc4ed182182c6e7d7"
122
+ LATEST_CHALK_MAIN_SHA = "c63f0367e01795006bc0e91e2540ec85e740f19b"
122
123
 
123
124
 
124
125
  def chalk_extra_pip(spec=None) -> list[str]:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "freesolo-flash-dev"
3
- version = "0.2.43"
3
+ version = "0.2.44"
4
4
  description = "Flash — managed LoRA post-training (SFT/GRPO) for Freesolo environments, driven by the `flash` CLI"
5
5
  readme = "README.md"
6
6
  # RunPod Flash supports Python 3.11-3.12 (not 3.13 yet).
@@ -106,7 +106,7 @@ artifacts = ["flash/py.typed"]
106
106
  # auto-published by .github/workflows/publish-dev.yml: a push to `dev` whose version isn't yet on
107
107
  # PyPI publishes it; pushes that don't change it find it already published and no-op. So cutting a
108
108
  # dev-channel release = bump this (and [project].version) and merge to `dev`.
109
- version = "0.2.43"
109
+ version = "0.2.44"
110
110
 
111
111
  [tool.pytest.ini_options]
112
112
  pythonpath = ["."]
@@ -10,7 +10,6 @@ All HF/network boundaries are stubbed; nothing here touches a GPU or the network
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
- import time
14
13
  from types import SimpleNamespace
15
14
 
16
15
  import pytest
@@ -197,19 +196,20 @@ def test_latest_checkpoint_dir_picks_highest_step(tmp_path):
197
196
  def test_on_train_end_flushes_final_deployable_checkpoint(
198
197
  tmp_path, monkeypatch, fake_trainer_callback
199
198
  ):
200
- """A fast RL run can exit before its last save's async (daemon) upload finishes, so
201
- on_train_end must SYNCHRONOUSLY publish the latest on-disk checkpoint as a deployable
202
- snapshot — otherwise `flash checkpoints` is empty even though the run trained fine."""
199
+ """on_train_end is the safety net for a final checkpoint the trainer wrote WITHOUT an on_save
200
+ (e.g. an end-of-training / load_best_model_at_end save). It must publish the latest on-disk
201
+ checkpoint as a deployable — otherwise `flash checkpoints` is empty even though the run trained
202
+ fine. (on_save uploads synchronously, so every step it sees is already up by this point.)"""
203
203
  import flash.engine.worker as worker
204
204
 
205
205
  rec = _RecordingHfApi()
206
206
  _prime_worker(monkeypatch, rec)
207
207
  out = tmp_path / "out"
208
208
  out.mkdir()
209
- _make_ckpt_dir(out, 8) # last save the trainer wrote locally; its async upload was "lost"
209
+ _make_ckpt_dir(out, 8) # a final checkpoint on disk that no on_save uploaded
210
210
  cb = worker.make_checkpoint_upload_callback()
211
211
 
212
- # No on_save uploads recorded (simulating their daemon threads being killed at exit).
212
+ # No on_save fired for this step, so on_train_end must publish it.
213
213
  cb.on_train_end(SimpleNamespace(output_dir=str(out)), None, None)
214
214
 
215
215
  deployable = [
@@ -219,72 +219,38 @@ def test_on_train_end_flushes_final_deployable_checkpoint(
219
219
  assert "optimizer.pt" in deployable[0]["ignore_patterns"]
220
220
 
221
221
 
222
- def test_on_train_end_publishes_final_deployable_when_lock_is_held(
223
- tmp_path, monkeypatch, fake_trainer_callback
224
- ):
225
- """Regression: a slow on_save upload can still hold the upload lock at run end (and the final
226
- step's own on_save may have been skipped on that busy lock). on_train_end must then publish the
227
- final deployable WITHOUT the lock instead of timing out and skipping it — otherwise the worker
228
- exits, kills the daemon mid-upload, and `flash checkpoints` is empty despite a successful run."""
229
- import threading
230
-
231
- from flash.engine.worker import hf as worker_hf
222
+ def test_on_train_end_no_checkpoints_is_noop(tmp_path, monkeypatch, fake_trainer_callback):
223
+ import flash.engine.worker as worker
232
224
 
233
225
  rec = _RecordingHfApi()
234
- worker = _prime_worker(monkeypatch, rec)
235
- monkeypatch.setattr(worker_hf, "_CKPT_FLUSH_TIMEOUT_S", 0.05) # give up on the held lock fast
236
-
226
+ _prime_worker(monkeypatch, rec)
237
227
  out = tmp_path / "out"
238
228
  out.mkdir()
239
- _make_ckpt_dir(out, 4) # earlier step; its on_save upload will block, holding the lock
240
- _make_ckpt_dir(out, 8) # the FINAL step (its on_save was skipped on the busy lock)
241
-
242
- holding = threading.Event()
243
- release = threading.Event()
244
- base_upload = rec.upload_folder
245
-
246
- def upload(**kwargs):
247
- base_upload(**kwargs)
248
- if kwargs["path_in_repo"].endswith("checkpoint/checkpoint-4"):
249
- holding.set() # the resume upload now holds the lock...
250
- release.wait(5) # ...keep holding it across the on_train_end flush
251
-
252
- rec.upload_folder = upload
253
-
254
- started: list = []
255
- real_thread = threading.Thread
256
- monkeypatch.setattr(
257
- worker.threading,
258
- "Thread",
259
- lambda *a, **k: started.append(real_thread(*a, **k)) or started[-1],
229
+ worker.make_checkpoint_upload_callback().on_train_end(
230
+ SimpleNamespace(output_dir=str(out)), None, None
260
231
  )
261
-
262
- cb = worker.make_checkpoint_upload_callback()
263
- try:
264
- cb.on_save(SimpleNamespace(output_dir=str(out)), SimpleNamespace(global_step=4), None)
265
- assert holding.wait(5), "the step-4 upload should be holding the lock"
266
- cb.on_train_end(SimpleNamespace(output_dir=str(out)), None, None)
267
- deployable = [
268
- u for u in rec.uploads if u["path_in_repo"].endswith("checkpoints/step-8/adapter")
269
- ]
270
- assert len(deployable) == 1, "final deployable must publish even when the lock is held"
271
- finally:
272
- release.set()
273
- for t in started:
274
- t.join(5)
232
+ assert rec.uploads == []
275
233
 
276
234
 
277
- def test_on_train_end_no_checkpoints_is_noop(tmp_path, monkeypatch, fake_trainer_callback):
235
+ def test_on_save_uploads_synchronously_before_returning(
236
+ tmp_path, monkeypatch, fake_trainer_callback
237
+ ):
238
+ """Uploads are synchronous: by the time on_save returns, the checkpoint is fully on HF — no
239
+ background thread is left running. This is what makes "upload busy" impossible by construction:
240
+ a later save can never find this one still in flight."""
278
241
  import flash.engine.worker as worker
279
242
 
280
243
  rec = _RecordingHfApi()
281
244
  _prime_worker(monkeypatch, rec)
282
245
  out = tmp_path / "out"
283
246
  out.mkdir()
284
- worker.make_checkpoint_upload_callback().on_train_end(
285
- SimpleNamespace(output_dir=str(out)), None, None
286
- )
287
- assert rec.uploads == []
247
+ _make_ckpt_dir(out, 4)
248
+ cb = worker.make_checkpoint_upload_callback()
249
+ cb.on_save(SimpleNamespace(output_dir=str(out)), SimpleNamespace(global_step=4), None)
250
+ # Fully uploaded the moment on_save returns — no polling, no thread join needed.
251
+ paths = [u["path_in_repo"] for u in rec.uploads]
252
+ assert "rl/flash-ckpt-1/checkpoints/step-4/adapter" in paths
253
+ assert "rl/flash-ckpt-1/checkpoint/checkpoint-4" in paths
288
254
 
289
255
 
290
256
  def test_on_save_publishes_deployable_before_resume(tmp_path, monkeypatch, fake_trainer_callback):
@@ -294,25 +260,11 @@ def test_on_save_publishes_deployable_before_resume(tmp_path, monkeypatch, fake_
294
260
 
295
261
  rec = _RecordingHfApi()
296
262
  _prime_worker(monkeypatch, rec)
297
-
298
- class _SyncThread: # run the upload body inline so the unit test is deterministic
299
- def __init__(self, target=None, daemon=None, **kw):
300
- self._target = target
301
-
302
- def start(self):
303
- if self._target:
304
- self._target()
305
-
306
- monkeypatch.setattr(worker.threading, "Thread", _SyncThread)
307
263
  out = tmp_path / "out"
308
264
  out.mkdir()
309
265
  _make_ckpt_dir(out, 4)
310
266
  cb = worker.make_checkpoint_upload_callback()
311
- cb.on_save(
312
- SimpleNamespace(output_dir=str(out)),
313
- SimpleNamespace(global_step=4),
314
- None,
315
- )
267
+ cb.on_save(SimpleNamespace(output_dir=str(out)), SimpleNamespace(global_step=4), None)
316
268
  paths = [u["path_in_repo"] for u in rec.uploads]
317
269
  assert paths == [
318
270
  "rl/flash-ckpt-1/checkpoints/step-4/adapter", # deployable first
@@ -320,115 +272,61 @@ def test_on_save_publishes_deployable_before_resume(tmp_path, monkeypatch, fake_
320
272
  ]
321
273
 
322
274
 
323
- def test_on_save_queues_busy_step_instead_of_skipping(tmp_path, monkeypatch, fake_trainer_callback):
324
- """`save_every` must not be advisory: a save that fires while a prior upload is still in
325
- flight is queued and uploaded once the in-flight one finishes previously it was dropped
326
- ("upload busy; skipping step N"), leaving a sparse registered step list."""
327
- import threading
275
+ def test_consecutive_saves_each_upload_no_coalescing(tmp_path, monkeypatch, fake_trainer_callback):
276
+ """Every save uploads its own step. Synchronous uploads make "upload busy" coalescing
277
+ impossible: back-to-back saves each publish their deployable, so the registered step list is
278
+ never thinned under contention (the old newest-wins slot dropped intermediate steps)."""
279
+ import flash.engine.worker as worker
328
280
 
329
281
  rec = _RecordingHfApi()
330
- worker = _prime_worker(monkeypatch, rec)
331
-
282
+ _prime_worker(monkeypatch, rec)
332
283
  out = tmp_path / "out"
333
284
  out.mkdir()
334
- _make_ckpt_dir(out, 4)
335
- _make_ckpt_dir(out, 8)
336
-
337
- holding = threading.Event()
338
- release = threading.Event()
339
- base_upload = rec.upload_folder
340
-
341
- def upload(**kwargs):
342
- base_upload(**kwargs)
343
- if kwargs["path_in_repo"].endswith("checkpoint/checkpoint-4"):
344
- holding.set() # the step-4 resume upload now holds the lock...
345
- release.wait(5) # ...until we let it finish
346
-
347
- rec.upload_folder = upload
348
-
349
285
  cb = worker.make_checkpoint_upload_callback()
350
- cb.on_save(SimpleNamespace(output_dir=str(out)), SimpleNamespace(global_step=4), None)
351
- assert holding.wait(5), "the step-4 upload should be holding the lock"
352
- # step-8 save fires while step-4 is uploading: it must be queued, not skipped.
353
- cb.on_save(SimpleNamespace(output_dir=str(out)), SimpleNamespace(global_step=8), None)
354
- release.set()
355
-
356
- deadline = time.time() + 10
357
- while time.time() < deadline:
358
- if any(u["path_in_repo"].endswith("checkpoints/step-8/adapter") for u in rec.uploads):
359
- break
360
- time.sleep(0.02)
361
- deployable = [
362
- u for u in rec.uploads if u["path_in_repo"].endswith("checkpoints/step-8/adapter")
363
- ]
364
- assert len(deployable) == 1, "a busy-lock save must be queued and uploaded, not skipped"
365
-
366
-
367
- def test_upload_pump_no_lost_wakeup(tmp_path):
368
- """Regression: a save enqueued in the window where the in-flight upload has passed its own
369
- drain check but not yet finished must still be started by done() — with the old two-lock
370
- design it could be silently dropped."""
371
- from flash.engine.worker.hf import _UploadPump
286
+ for step in (4, 8, 12):
287
+ _make_ckpt_dir(out, step)
288
+ cb.on_save(SimpleNamespace(output_dir=str(out)), SimpleNamespace(global_step=step), None)
289
+ deployables = sorted(
290
+ p["path_in_repo"] for p in rec.uploads if p["path_in_repo"].endswith("/adapter")
291
+ )
292
+ assert deployables == [
293
+ "rl/flash-ckpt-1/checkpoints/step-12/adapter",
294
+ "rl/flash-ckpt-1/checkpoints/step-4/adapter",
295
+ "rl/flash-ckpt-1/checkpoints/step-8/adapter",
296
+ ], "each save must publish its own deployable — no step coalesced away"
372
297
 
373
- ckpt = tmp_path / "checkpoint-8"
374
- ckpt.mkdir()
375
- started: list[int] = []
376
- pump = _UploadPump(lambda step, d: started.append(step))
377
- # Simulate an in-flight upload that has already drained the queue (the racy window).
378
- pump.uploading = True
379
- pump.enqueue(8, str(ckpt))
380
- assert started == [], "enqueue while busy must queue, not start"
381
- pump.done() # in-flight finishes: the queued step MUST be pumped, never lost
382
- assert started == [8]
383
- pump.done()
384
- assert started == [8], "queue is single-slot; nothing left to start"
385
-
386
-
387
- def test_on_train_end_flushes_queued_deployable_without_resume_race(
388
- tmp_path, monkeypatch, fake_trainer_callback
389
- ):
390
- """A save queued behind a still-in-flight upload at run end must publish its deployable adapter
391
- without starting a second resume-checkpoint upload/prune race."""
392
- import threading
393
298
 
299
+ def test_on_save_retries_transient_upload_error(tmp_path, monkeypatch, fake_trainer_callback):
300
+ """A transient HF error must not cost the step: the synchronous upload retries until it lands,
301
+ so `save_every` is a guarantee, not best-effort."""
394
302
  from flash.engine.worker import hf as worker_hf
395
303
 
396
304
  rec = _RecordingHfApi()
397
305
  worker = _prime_worker(monkeypatch, rec)
398
- monkeypatch.setattr(worker_hf, "_CKPT_FLUSH_TIMEOUT_S", 0.05)
306
+ monkeypatch.setattr(worker_hf, "_CKPT_UPLOAD_BACKOFF_S", 0.0) # no real sleep between retries
399
307
 
400
308
  out = tmp_path / "out"
401
309
  out.mkdir()
402
310
  _make_ckpt_dir(out, 4)
403
- _make_ckpt_dir(out, 8)
404
311
 
405
- holding = threading.Event()
406
- release = threading.Event()
312
+ resume_calls = {"n": 0}
407
313
  base_upload = rec.upload_folder
408
314
 
409
- def upload(**kwargs):
410
- base_upload(**kwargs)
315
+ def flaky(**kwargs):
411
316
  if kwargs["path_in_repo"].endswith("checkpoint/checkpoint-4"):
412
- holding.set() # the step-4 resume upload is now in flight...
413
- release.wait(5) # ...and stays in flight across on_train_end
317
+ resume_calls["n"] += 1
318
+ if resume_calls["n"] == 1:
319
+ raise RuntimeError("transient 500 from HF")
320
+ return base_upload(**kwargs)
414
321
 
415
- rec.upload_folder = upload
322
+ rec.upload_folder = flaky
416
323
 
417
324
  cb = worker.make_checkpoint_upload_callback()
418
- try:
419
- cb.on_save(SimpleNamespace(output_dir=str(out)), SimpleNamespace(global_step=4), None)
420
- assert holding.wait(5), "the step-4 upload should be in flight"
421
- cb.on_save(SimpleNamespace(output_dir=str(out)), SimpleNamespace(global_step=8), None)
422
- cb.on_train_end(SimpleNamespace(output_dir=str(out)), None, None)
423
- paths = [u["path_in_repo"] for u in rec.uploads]
424
- assert "rl/flash-ckpt-1/checkpoints/step-8/adapter" in paths, (
425
- "queued step's deployable must be flushed at train end"
426
- )
427
- assert "rl/flash-ckpt-1/checkpoint/checkpoint-8" not in paths, (
428
- "queued step's resume checkpoint must not race the in-flight resume prune"
429
- )
430
- finally:
431
- release.set()
325
+ cb.on_save(SimpleNamespace(output_dir=str(out)), SimpleNamespace(global_step=4), None)
326
+ assert resume_calls["n"] == 2, "the resume upload must be retried after a transient failure"
327
+ assert any(u["path_in_repo"].endswith("checkpoint/checkpoint-4") for u in rec.uploads), (
328
+ "the checkpoint must land on retry, not be skipped"
329
+ )
432
330
 
433
331
 
434
332
  def test_prune_stale_resume_checkpoints_keeps_only_latest(monkeypatch):
@@ -485,15 +383,6 @@ def test_on_save_skips_deployable_when_vl_recombine_fails(
485
383
 
486
384
  monkeypatch.setattr(worker, "recombined_warmstart_adapter_dir", _raise)
487
385
 
488
- class _SyncThread:
489
- def __init__(self, target=None, daemon=None, **kw):
490
- self._target = target
491
-
492
- def start(self):
493
- if self._target:
494
- self._target()
495
-
496
- monkeypatch.setattr(worker.threading, "Thread", _SyncThread)
497
386
  out = tmp_path / "out"
498
387
  out.mkdir()
499
388
  _make_ckpt_dir(out, 4)
@@ -77,6 +77,10 @@ class _FakeClient:
77
77
  self.calls.append(("cancel", run_id))
78
78
  return {"run_id": run_id, "state": "cancelled"}
79
79
 
80
+ def checkpoints(self, run_id: str) -> list[dict]:
81
+ self.calls.append(("checkpoints", run_id))
82
+ return [{"step": 20}, {"step": 40}]
83
+
80
84
  def deploy(self, run_id: str, **kwargs) -> dict:
81
85
  self.calls.append(("deploy", run_id, kwargs))
82
86
  return {
@@ -372,6 +376,36 @@ def test_follow_logs_uses_status_progress_when_log_tail_lags(monkeypatch, capsys
372
376
  assert "realized_cost=$1.2346" in err
373
377
 
374
378
 
379
+ def test_cancel_surfaces_surviving_checkpoints(fake_client, capsys) -> None:
380
+ """`state=cancelled` + adapter_ref=null + cost=0 reads as discardable, yet the per-step
381
+ deployable checkpoints streamed before the cancel survive it — the cancel output must say
382
+ so (on stderr in the plain path, keeping the stdout JSON machine-readable)."""
383
+ import json as _json
384
+
385
+ assert _run(["cancel", "flash-1"]) == 0
386
+ assert ("checkpoints", "flash-1") in fake_client.calls
387
+ out, err = capsys.readouterr()
388
+ assert _json.loads(out)["state"] == "cancelled" # stdout stays pure JSON in the plain path
389
+ assert "2 deployable checkpoint(s) survive this cancel" in err
390
+ assert "flash checkpoints flash-1" in err
391
+ assert "flash deploy flash-1/step-40" in err # points at the newest surviving step
392
+
393
+
394
+ def test_cancel_hint_is_best_effort_when_checkpoint_listing_fails(
395
+ fake_client, capsys, monkeypatch
396
+ ) -> None:
397
+ """The surviving-checkpoints lookup must never break `flash cancel` itself."""
398
+
399
+ def boom(run_id):
400
+ raise RuntimeError("backend hiccup")
401
+
402
+ monkeypatch.setattr(fake_client, "checkpoints", boom)
403
+ assert _run(["cancel", "flash-1"]) == 0
404
+ out, err = capsys.readouterr()
405
+ assert '"state": "cancelled"' in out
406
+ assert "deployable checkpoint" not in err
407
+
408
+
375
409
  def test_cancel_deploy_undeploy_deployments(fake_client, capsys) -> None:
376
410
  assert _run(["cancel", "flash-1"]) == 0
377
411
  assert ("cancel", "flash-1") in fake_client.calls