freesolo-flash-dev 0.2.45__tar.gz → 0.2.46__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 (294) hide show
  1. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/PKG-INFO +1 -1
  2. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/cli/commands.py +28 -20
  3. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/cli/training_doc.py +10 -4
  4. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/cost/analytical.py +15 -9
  5. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/cost/types.py +5 -4
  6. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/multiturn_rollout.py +174 -42
  7. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/__init__.py +6 -0
  8. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/gpu_setup.py +46 -0
  9. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/heartbeat.py +26 -6
  10. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/hf.py +48 -20
  11. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/opd.py +138 -31
  12. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/rl.py +11 -2
  13. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/sft.py +6 -1
  14. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/_worker.py +6 -0
  15. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/schema/__init__.py +3 -20
  16. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/pyproject.toml +2 -2
  17. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_algorithms.py +3 -2
  18. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_cli_commands.py +40 -12
  19. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_cost_analytical.py +32 -0
  20. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_flash_worker.py +33 -0
  21. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_multiturn_rollout.py +101 -0
  22. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_opd.py +260 -13
  23. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_worker_init_heartbeat.py +41 -1
  24. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/.dockerignore +0 -0
  25. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/.env.example +0 -0
  26. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/.github/workflows/auto-rebake.yml +0 -0
  27. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/.github/workflows/bake-kernel-cache.yml +0 -0
  28. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/.github/workflows/ci.yml +0 -0
  29. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/.github/workflows/main-source-guard.yml +0 -0
  30. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/.github/workflows/notify-tests-repo.yml +0 -0
  31. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/.github/workflows/publish-dev.yml +0 -0
  32. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/.github/workflows/publish-image.yml +0 -0
  33. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/.github/workflows/publish.yml +0 -0
  34. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/.github/workflows/version-parity.yml +0 -0
  35. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/.github/workflows/worker-image.yml +0 -0
  36. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/.gitignore +0 -0
  37. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/Dockerfile +0 -0
  38. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/Dockerfile.worker +0 -0
  39. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/LICENSE +0 -0
  40. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/README.md +0 -0
  41. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/build/kernel_cache/.gitignore +0 -0
  42. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/build/kernel_cache/.keep +0 -0
  43. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/docker/Dockerfile.kernelcache +0 -0
  44. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/docker/Dockerfile.kernelcache.relayer +0 -0
  45. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/docker/bake_kernel_cache.py +0 -0
  46. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/docker/bake_pod_entry.py +0 -0
  47. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/docker/kernel_fingerprint.py +0 -0
  48. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/docker/make_rp_handler.py +0 -0
  49. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/__init__.py +0 -0
  50. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/_channel.py +0 -0
  51. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/_fileio.py +0 -0
  52. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/_logging.py +0 -0
  53. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/_update_check.py +0 -0
  54. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/adapter_artifacts.py +0 -0
  55. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/catalog.py +0 -0
  56. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/cli/__init__.py +0 -0
  57. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/cli/__main__.py +0 -0
  58. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/cli/_tty.py +0 -0
  59. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/cli/envpush.py +0 -0
  60. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/cli/render.py +0 -0
  61. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/client/__init__.py +0 -0
  62. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/client/config.py +0 -0
  63. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/client/http.py +0 -0
  64. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/client/runtime_secrets.py +0 -0
  65. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/client/specs.py +0 -0
  66. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/cost/__init__.py +0 -0
  67. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/cost/facts.py +0 -0
  68. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/cost/spec.py +0 -0
  69. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/__init__.py +0 -0
  70. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/accounting.py +0 -0
  71. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/chalk_kernels.py +0 -0
  72. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/recipe.py +0 -0
  73. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/vram.py +0 -0
  74. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/__main__.py +0 -0
  75. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/_pkg.py +0 -0
  76. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/adapter.py +0 -0
  77. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/decoding.py +0 -0
  78. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/finalize.py +0 -0
  79. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/grpo.py +0 -0
  80. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/kernel_warmup.py +0 -0
  81. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/lora.py +0 -0
  82. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/opd_gkd.py +0 -0
  83. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/packing.py +0 -0
  84. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/perf/__init__.py +0 -0
  85. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/perf/attn.py +0 -0
  86. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/perf/diagnostics.py +0 -0
  87. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/perf/lifecycle.py +0 -0
  88. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/perf/liger.py +0 -0
  89. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/perf/loraplus.py +0 -0
  90. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/perf/memory.py +0 -0
  91. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/teacher.py +0 -0
  92. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/tokenizer_align.py +0 -0
  93. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/engine/worker/wandb_log.py +0 -0
  94. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/envs/__init__.py +0 -0
  95. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/envs/adapter.py +0 -0
  96. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/envs/archive_policy.py +0 -0
  97. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/envs/base.py +0 -0
  98. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/envs/loader.py +0 -0
  99. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/envs/pull.py +0 -0
  100. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/envs/registry.py +0 -0
  101. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/lora_rank.py +0 -0
  102. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/__init__.py +0 -0
  103. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/_auth.py +0 -0
  104. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/_hf_artifacts.py +0 -0
  105. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/_hf_retry.py +0 -0
  106. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/_http.py +0 -0
  107. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/_instance.py +0 -0
  108. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/_instance_bootstrap.py +0 -0
  109. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/_instance_poll.py +0 -0
  110. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/_instance_provider.py +0 -0
  111. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/_poll.py +0 -0
  112. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/allocator.py +0 -0
  113. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/base.py +0 -0
  114. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/lambdalabs/__init__.py +0 -0
  115. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/lambdalabs/api.py +0 -0
  116. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/lambdalabs/auth.py +0 -0
  117. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/lambdalabs/gpus.py +0 -0
  118. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/lambdalabs/jobs/__init__.py +0 -0
  119. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/lambdalabs/jobs/builders.py +0 -0
  120. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/lambdalabs/preflight.py +0 -0
  121. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/lambdalabs/pricing.py +0 -0
  122. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/preflight.py +0 -0
  123. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/realized.py +0 -0
  124. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/__init__.py +0 -0
  125. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/api.py +0 -0
  126. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/auth.py +0 -0
  127. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/cost.py +0 -0
  128. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/gpus.py +0 -0
  129. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/jobs.py +0 -0
  130. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/keys.py +0 -0
  131. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/preflight.py +0 -0
  132. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/preload.py +0 -0
  133. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/pricing.py +0 -0
  134. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/slots.py +0 -0
  135. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/train/__init__.py +0 -0
  136. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/train/deps.py +0 -0
  137. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/train/endpoints.py +0 -0
  138. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/vast/__init__.py +0 -0
  139. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/vast/api.py +0 -0
  140. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/vast/auth.py +0 -0
  141. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/vast/jobs/__init__.py +0 -0
  142. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/vast/jobs/builders.py +0 -0
  143. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/vast/preflight.py +0 -0
  144. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/providers/vast/pricing.py +0 -0
  145. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/py.typed +0 -0
  146. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/runner/__init__.py +0 -0
  147. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/runner/checkpoints.py +0 -0
  148. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/runner/deploy.py +0 -0
  149. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/runner/lifecycle.py +0 -0
  150. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/schema/fields.py +0 -0
  151. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/serve/__init__.py +0 -0
  152. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/serve/deploy.py +0 -0
  153. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/serve/export.py +0 -0
  154. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/serve/pricing.py +0 -0
  155. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/server/__init__.py +0 -0
  156. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/server/__main__.py +0 -0
  157. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/server/_deps.py +0 -0
  158. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/server/_internal_client.py +0 -0
  159. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/server/_locks.py +0 -0
  160. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/server/_runtime.py +0 -0
  161. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/server/app.py +0 -0
  162. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/server/auth.py +0 -0
  163. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/server/billing.py +0 -0
  164. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/server/billing_retry.py +0 -0
  165. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/server/checkpoints.py +0 -0
  166. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/server/db.py +0 -0
  167. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/server/environment_registry.py +0 -0
  168. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/server/envs.py +0 -0
  169. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/server/reconcile.py +0 -0
  170. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/server/routes/__init__.py +0 -0
  171. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/server/routes/envs.py +0 -0
  172. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/server/routes/meta.py +0 -0
  173. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/server/routes/runs.py +0 -0
  174. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/server/routes/serving.py +0 -0
  175. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/server/run_registry.py +0 -0
  176. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/flash/spec.py +0 -0
  177. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/infisical-entrypoint.sh +0 -0
  178. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/scripts/build_dev_dist.py +0 -0
  179. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/__init__.py +0 -0
  180. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/_helpers/__init__.py +0 -0
  181. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/_helpers/runner.py +0 -0
  182. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/_helpers/specs.py +0 -0
  183. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/_helpers/vast.py +0 -0
  184. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/conftest.py +0 -0
  185. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/fixtures/math_eval.jsonl +0 -0
  186. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/fixtures/math_train.jsonl +0 -0
  187. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/live/__init__.py +0 -0
  188. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/live/conftest.py +0 -0
  189. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/live/test_lambda_live.py +0 -0
  190. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/live/test_runpod_live.py +0 -0
  191. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/live/test_vast_live.py +0 -0
  192. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_agent_flash_cli_contract.py +0 -0
  193. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_allocator.py +0 -0
  194. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_b200_rollout_opt.py +0 -0
  195. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_backend_jobspec_contract.py +0 -0
  196. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_billing_retry.py +0 -0
  197. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_cancel_remote.py +0 -0
  198. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_catalog_consistency.py +0 -0
  199. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_chalk_kernels.py +0 -0
  200. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_charge_pricing.py +0 -0
  201. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_checkpoints.py +0 -0
  202. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_cli_errors.py +0 -0
  203. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_cli_estimate.py +0 -0
  204. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_cli_help.py +0 -0
  205. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_cli_managed.py +0 -0
  206. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_cli_render_theme.py +0 -0
  207. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_client.py +0 -0
  208. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_client_server_integration.py +0 -0
  209. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_config_overrides.py +0 -0
  210. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_cost_equation.py +0 -0
  211. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_cost_estimate.py +0 -0
  212. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_cost_hardware.py +0 -0
  213. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_cost_models.py +0 -0
  214. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_cost_rewards.py +0 -0
  215. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_dev_channel.py +0 -0
  216. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_disk_gb.py +0 -0
  217. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_endpoint_name.py +0 -0
  218. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_env_cache_evict.py +0 -0
  219. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_env_delete.py +0 -0
  220. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_env_download.py +0 -0
  221. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_env_download_api.py +0 -0
  222. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_env_no_install.py +0 -0
  223. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_env_publish.py +0 -0
  224. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_env_pull.py +0 -0
  225. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_env_pull_managed_control_plane.py +0 -0
  226. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_env_push.py +0 -0
  227. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_env_rate_limit_resolve.py +0 -0
  228. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_envs_coverage.py +0 -0
  229. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_export.py +0 -0
  230. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_flash_mvp.py +0 -0
  231. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_flashinfer_cache_dirs.py +0 -0
  232. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_github_urlopen_retry.py +0 -0
  233. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_gpus.py +0 -0
  234. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_grpo_mask_aware.py +0 -0
  235. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_grpo_params.py +0 -0
  236. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_grpo_sleep_gate.py +0 -0
  237. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_health_liveness.py +0 -0
  238. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_idle_endpoint_reaper.py +0 -0
  239. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_internal_client.py +0 -0
  240. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_jobs.py +0 -0
  241. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_kernel_cache.py +0 -0
  242. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_kernel_fingerprint.py +0 -0
  243. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_kv_util.py +0 -0
  244. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_lambda_runner.py +0 -0
  245. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_logging.py +0 -0
  246. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_login_perms.py +0 -0
  247. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_lora_rank_preflight.py +0 -0
  248. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_managed_hf_repo.py +0 -0
  249. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_metrics_schema_agent_contract.py +0 -0
  250. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_mig_guard.py +0 -0
  251. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_oom_escalate_gpu.py +0 -0
  252. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_open_model_policy.py +0 -0
  253. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_orchestrator_flash.py +0 -0
  254. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_packing.py +0 -0
  255. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_poll_helpers.py +0 -0
  256. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_preflight.py +0 -0
  257. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_provider_routing.py +0 -0
  258. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_provider_teardown_robustness.py +0 -0
  259. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_providers_symmetry.py +0 -0
  260. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_realized_cost.py +0 -0
  261. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_resolve_params_b.py +0 -0
  262. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_resume_on_retry.py +0 -0
  263. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_runmgmt.py +0 -0
  264. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_runpod_api_delete.py +0 -0
  265. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_runpod_key_fingerprint.py +0 -0
  266. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_runpod_key_waterfall.py +0 -0
  267. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_runpod_slots.py +0 -0
  268. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_serve.py +0 -0
  269. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_server_api.py +0 -0
  270. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_server_billing.py +0 -0
  271. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_server_db.py +0 -0
  272. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_serving_contract.py +0 -0
  273. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_sft_example_selection.py +0 -0
  274. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_sft_gc_off.py +0 -0
  275. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_spec_and_validation.py +0 -0
  276. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_thinking_config.py +0 -0
  277. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_train_context_preflight.py +0 -0
  278. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_update_check.py +0 -0
  279. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_vast_api.py +0 -0
  280. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_vast_offers.py +0 -0
  281. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_vast_runner.py +0 -0
  282. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_verifiers.py +0 -0
  283. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_version.py +0 -0
  284. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_vl_warmstart_adapter_keys.py +0 -0
  285. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_vl_warmstart_recombine.py +0 -0
  286. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_wandb_naming.py +0 -0
  287. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_warmstart_cross_repo.py +0 -0
  288. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_weight_cache.py +0 -0
  289. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_worker_dryrun.py +0 -0
  290. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_worker_hardexit.py +0 -0
  291. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_worker_image.py +0 -0
  292. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_worker_stack.py +0 -0
  293. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/tests/test_worker_thinking.py +0 -0
  294. {freesolo_flash_dev-0.2.45 → freesolo_flash_dev-0.2.46}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: freesolo-flash-dev
3
- Version: 0.2.45
3
+ Version: 0.2.46
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
@@ -210,14 +210,14 @@ A managed run should use the returned [environment] id from
210
210
  This starter implements a tiny "guess the secret number" game so you can see the
211
211
  episode hooks wired end-to-end. Replace it with your real task before a real run.
212
212
 
213
- SFT and GRPO train off this file; OPD (configs/opd.toml) does NOT support multi-turn:
213
+ All three algorithms train off this file:
214
214
  - GRPO (configs/rl.toml) rolls out full episodes and optimizes `score_episode`.
215
215
  - SFT (configs/sft.toml) learns the gold trajectory. Provide it per row as
216
216
  `output = {"messages": [...]}` (a full assistant/tool trajectory) or a scalar
217
217
  `output` for a single gold assistant turn.
218
- - OPD (configs/opd.toml) distils one sampled completion per prompt and cannot drive the
219
- turn loop, so it is single-turn only `flash train configs/opd.toml` fails fast here.
220
- Use OPD with a single-turn env (`flash env setup --single-turn`).
218
+ - OPD (configs/opd.toml) rolls out each episode and distils EVERY assistant turn against
219
+ the Fireworks GLM teacher, conditioned on the transcript so far the multi-turn
220
+ on-policy-distillation objective. The teacher key is platform-managed (nothing to set).
221
221
  """
222
222
 
223
223
  from __future__ import annotations
@@ -492,28 +492,24 @@ def cmd_env_setup(args) -> int:
492
492
  )
493
493
  opd = Path("configs/opd.toml")
494
494
  if not opd.exists():
495
- # opd (on-policy distillation) is SINGLE-TURN only: it samples one completion per prompt and
496
- # cannot drive a multi-turn/tool episode loop, so `flash train configs/opd.toml` FAILS FAST on a
497
- # multi-turn env (run_opd rejects it at setup). Scaffold it in both modes for reference, but say
498
- # so loudly in the multi-turn one so the config isn't a footgun.
495
+ # opd (on-policy distillation) works in BOTH modes: single-turn distils one sampled completion
496
+ # per prompt; multi-turn rolls out each episode and distils every assistant turn against the
497
+ # transcript so far. The scaffold differs only by a one-line note pointing at the mode.
499
498
  opd_multiturn_note = (
500
- "# NOTE: opd is SINGLE-TURN only it distils one sampled completion per prompt and cannot\n"
501
- "# drive this multi-turn environment's episode loop, so `flash train configs/opd.toml` will\n"
502
- "# fail fast here. Use configs/rl.toml (grpo) or configs/sft.toml for multi-turn; keep opd for\n"
503
- "# a single-turn env (`flash env setup --single-turn`).\n\n"
499
+ "# NOTE: opd rolls out each episode and distils EVERY assistant turn (conditioned on the\n"
500
+ "# transcript so far) against the Fireworks GLM teacher the multi-turn distillation path.\n\n"
504
501
  if multi_turn
505
502
  else ""
506
503
  )
507
504
  opd.write_text(
508
505
  f"{opd_multiturn_note}"
509
506
  'model = "Qwen/Qwen3.5-4B"\n'
510
- 'algorithm = "opd" # on-policy distillation from a Fireworks GLM teacher (single-turn only)\n\n'
507
+ 'algorithm = "opd" # on-policy distillation from a Fireworks GLM teacher\n\n'
511
508
  "# Environment: upload this project folder with\n"
512
509
  "# `flash env push --name my-env .`, then paste the returned id below.\n"
513
- "# FIREWORKS_API_KEY (the GLM teacher key) is read from your shell/.env at submit time.\n"
510
+ "# The GLM teacher key is platform-managed nothing to set up or export.\n"
514
511
  "[environment]\n"
515
- 'id = ""\n'
516
- 'secrets = ["FIREWORKS_API_KEY"]\n\n'
512
+ 'id = ""\n\n'
517
513
  "[train]\n"
518
514
  "steps = 100 # opd is step-driven (like GRPO)\n"
519
515
  "lora_rank = 32\n"
@@ -842,13 +838,25 @@ def cmd_cancel(args) -> int:
842
838
  else:
843
839
  print(json.dumps(payload, indent=2))
844
840
  if checkpoints:
845
- top = max(c["step"] for c in checkpoints)
841
+ # Best-effort hint (the cancel already succeeded), so never crash on a malformed checkpoint
842
+ # shape: coerce steps defensively — a dict missing 'step' or carrying a non-int must not raise
843
+ # a traceback here. Only surface the `step-N` deploy example when we recovered a real step.
844
+ steps = []
845
+ for c in checkpoints:
846
+ try:
847
+ steps.append(int(c["step"]))
848
+ except (KeyError, TypeError, ValueError):
849
+ continue
846
850
  # stderr in the plain path so the machine-readable stdout JSON stays untouched.
847
851
  out = sys.stdout if render.styled() else sys.stderr
848
- msg = (
852
+ base = (
849
853
  f"{len(checkpoints)} deployable checkpoint(s) survive this cancel — list with "
850
- f"`flash checkpoints {args.run_id}`, deploy one with "
851
- f"`flash deploy {args.run_id}/step-{top}`."
854
+ f"`flash checkpoints {args.run_id}`"
855
+ )
856
+ msg = (
857
+ f"{base}, deploy one with `flash deploy {args.run_id}/step-{max(steps)}`."
858
+ if steps
859
+ else f"{base}."
852
860
  )
853
861
  print(render.note(msg) if render.styled() else msg, file=out)
854
862
  return 0
@@ -382,15 +382,22 @@ each of *its own* completions, and a dense per-token loss teaches the student to
382
382
  far more sample-efficient than reward-based RL and with no reward to design. It is **step-driven**
383
383
  (`steps`) and produces a LoRA served exactly like SFT.
384
384
 
385
- - **Provide the teacher key.** Distillation needs `FIREWORKS_API_KEY` at submit time. List it under
386
- `[environment] secrets = ["FIREWORKS_API_KEY"]` and export it in your shell / local `.env`; the
387
- value travels out-of-band and is never stored in the spec or needed at serving time.
385
+ - **No teacher key to set up.** The Fireworks key for the GLM teacher is platform-managed: the
386
+ service supplies its own key to every opd run, so there is nothing to export or declare — an opd
387
+ run submits like any other. (Bring-your-own teacher keys are not supported; a `FIREWORKS_API_KEY`
388
+ in your shell is simply ignored.) The key is never stored in the spec or needed at serving time.
388
389
  - **The student (Qwen / MiniCPM / Kimi) and the teacher (GLM) have different tokenizers.** Flash
389
390
  bridges the vocabulary mismatch with **groupwise reverse-KL** (the collinear-ai *spider* / Tinker
390
391
  method): it aligns the two tokenizations by shared decoded-text spans and applies per-span reverse
391
392
  KL using only realized-token logprobs — no vocabulary projection, so it covers every token exactly
392
393
  and works for any student tokenizer. When the tokenizers happen to agree it reduces to plain
393
394
  per-token reverse KL (Thinking Machines, *On-Policy Distillation*). Nothing to configure.
395
+ - **Works for multi-turn envs too.** Against an `EnvironmentMultiTurn`, opd rolls out each episode
396
+ (driving `step_episode` / observations just like GRPO) and distils EVERY assistant turn against the
397
+ teacher, each conditioned on the transcript up to that turn — the episode's total reverse-KL over
398
+ the student's generated tokens is the sum of its per-turn reverse-KLs. Env/observation tokens are
399
+ never distilled (they're context, not the student's output). Set `[train] max_length` to bound the
400
+ transcript; the teacher must cover it (GLM-5.2's context far exceeds the default budget).
394
401
  - **Judge it like SFT.** Distillation logs a falling per-token loss; a low loss alone is not proof.
395
402
  Keep a held-out split, `flash deploy` the adapter, and score it — confirm the student actually
396
403
  moved toward the teacher's behavior, not just its surface tokens.
@@ -402,7 +409,6 @@ algorithm = "opd"
402
409
 
403
410
  [environment]
404
411
  id = "your-org/my-env"
405
- secrets = ["FIREWORKS_API_KEY"]
406
412
 
407
413
  [train]
408
414
  steps = 100
@@ -44,6 +44,11 @@ MFU_DECODE = 0.12 # batched vLLM rollout (decode is memory-bandwidth-bound)
44
44
  # wall is ceil(completions / slots) waves x latency, not completions x latency.
45
45
  REWARD_CONCURRENCY = 16.0
46
46
 
47
+ # Teacher scoring is CONCURRENT too: run_opd's primary path fans a step's completions across up to
48
+ # TEACHER_CONCURRENCY Fireworks calls (mirror of opd.py _TEACHER_SCORE_MAX_WORKERS), so the teacher
49
+ # wall is ceil(completions / slots) waves x latency, not completions x latency.
50
+ TEACHER_CONCURRENCY = 8.0
51
+
47
52
  # Cold-start overhead (seconds): container boot + deps + model load (+ vLLM init for GRPO).
48
53
  #
49
54
  # Calibrated against a real fresh-worker run (0.8B SFT, RTX 3090 @ $0.239/hr) whose elapsed wall
@@ -99,7 +104,7 @@ def seconds_per_step(config: RunConfig, gpu: str) -> float:
99
104
  peak = gpu_tflops(gpu) * 1e12 # FLOP/s
100
105
 
101
106
  if n.is_opd:
102
- # OPD step = on-policy student rollout (like GRPO) + remote teacher scoring (serial
107
+ # OPD step = on-policy student rollout (like GRPO) + remote teacher scoring (CONCURRENT
103
108
  # Fireworks round-trips, replaces reward grading) + policy update (fwd+bwd only, NO local
104
109
  # reference forward — the teacher is the API). Bill local compute on the FULL prompt+completion
105
110
  # sequence (see _opd_step_shape), not completion-only, or long-prompt opd is underquoted.
@@ -107,10 +112,11 @@ def seconds_per_step(config: RunConfig, gpu: str) -> float:
107
112
  gen_s = (GRPO_GEN_FLOPS_PER_TOKEN_PER_PARAM * params * seq_tokens) / (peak * MFU_DECODE)
108
113
  update_s = (OPD_UPDATE_FLOPS_PER_TOKEN_PER_PARAM * params * seq_tokens) / (peak * MFU_TRAIN)
109
114
  teacher_lat = teacher_seconds_per_completion()
110
- # run_opd scores each completion serially (nested prompt/group loops each await
111
- # teacher.score before the next), so the wall cost is the full serial sum — do NOT divide
112
- # by a concurrency factor or the quote materially understates teacher-bound steps.
113
- teacher_s = completions * teacher_lat
115
+ # run_opd's primary path scores a step's completions CONCURRENTLY over Fireworks, bounded by
116
+ # TEACHER_CONCURRENCY workers (opd.py _TEACHER_SCORE_MAX_WORKERS), so the teacher wall is
117
+ # ceil(completions / slots) waves x latency — NOT the full serial sum (that describes only the
118
+ # CPU-test fallback that can't batch-generate). ceil keeps a partial last wave at one latency.
119
+ teacher_s = math.ceil(completions / TEACHER_CONCURRENCY) * teacher_lat
114
120
  return gen_s + teacher_s + update_s
115
121
 
116
122
  if not n.is_grpo:
@@ -255,10 +261,10 @@ def estimate_cost(config: RunConfig, *, wall_cap_s: float = DEFAULT_WALL_CAP_S)
255
261
  train_seconds=train,
256
262
  wall_clock_seconds=wall,
257
263
  wall_capped=wall_capped,
258
- # GPU (platform-billed) time only. OPD's teacher tokens are billed by Fireworks DIRECTLY to
259
- # the user's FIREWORKS_API_KEY (a user-supplied [environment].secret), so folding
260
- # teacher_api_usd into the charged total would bill it a second time keep it itemized as a
261
- # diagnostic instead (codex[bot]).
264
+ # GPU (platform-billed) time only. OPD's teacher tokens are billed by Fireworks to the
265
+ # platform-managed teacher key never through this GPU charge — so folding teacher_api_usd
266
+ # into the charged total would bill it a second time. Keep it itemized as a diagnostic
267
+ # instead (codex[bot]).
262
268
  total_usd=train / 3600.0 * hourly,
263
269
  teacher_api_usd=teacher_api_usd,
264
270
  notes=_notes(config, raw_train, wall_capped, cap_s),
@@ -150,8 +150,9 @@ class CostEstimate:
150
150
  wall_capped: bool
151
151
  total_usd: float
152
152
  # opd only: external Fireworks GLM teacher token spend (0.0 for sft/grpo). Billed by Fireworks
153
- # DIRECTLY to the user's FIREWORKS_API_KEY, so it is NOT part of total_usd shown as its own
154
- # itemized diagnostic line only.
153
+ # to the platform-managed teacher key (users don't supply one), tracked separately from the
154
+ # platform-billed GPU charge — so it is NOT part of total_usd; shown as its own itemized
155
+ # diagnostic line only.
155
156
  teacher_api_usd: float = 0.0
156
157
  notes: tuple[str, ...] = ()
157
158
 
@@ -181,8 +182,8 @@ class CostEstimate:
181
182
  ]
182
183
  if self.teacher_api_usd > 0:
183
184
  lines.append(
184
- f"Teacher API: ${self.teacher_api_usd:.2f} (Fireworks GLM token spend on your "
185
- "FIREWORKS_API_KEYbilled by Fireworks, NOT included in TOTAL)"
185
+ f"Teacher API: ${self.teacher_api_usd:.2f} (Fireworks GLM token spend on the "
186
+ "platform-managed teacher key tracked separately, NOT included in TOTAL)"
186
187
  )
187
188
  lines.append(f"TOTAL : ${self.total_usd:.2f}")
188
189
  if self.notes:
@@ -27,6 +27,26 @@ class RolloutResult(TypedDict):
27
27
  reward: float
28
28
 
29
29
 
30
+ class TurnRecord(TypedDict):
31
+ """One assistant turn of a multi-turn episode, as :func:`rollout_one_records` emits it.
32
+
33
+ ``prefix_ids`` are the student token ids of the whole transcript BEFORE this turn (initial prompt
34
+ + every prior assistant turn + every inter-turn env "glue"), i.e. the exact on-policy context the
35
+ student sampled this turn's completion after. ``context_messages`` is the parallel message list at
36
+ the same point (prompt + prior turns + prior env replies), for callers that render a separate
37
+ teacher/scoring prompt from it. ``gen`` is whatever the injected ``generate`` callable returned for
38
+ this turn (opaque to the driver beyond the four attributes it reads: ``completion_ids``,
39
+ ``completion_text``, ``truncated``, ``skip``) — e.g. OPD hands back its ``_GenResult`` so the
40
+ per-turn record feeds straight into the existing single-turn scoring/loss path. Distilling each
41
+ turn against ``prefix_ids`` is the multi-turn on-policy-distillation objective: the episode's total
42
+ reverse-KL over student-generated tokens is the sum of per-turn reverse-KLs, each conditioned on the
43
+ transcript so far."""
44
+
45
+ prefix_ids: list[int]
46
+ gen: object
47
+ context_messages: list[dict]
48
+
49
+
30
50
  _ROLLOUT_FIELDS: tuple[str, ...] = (
31
51
  "prompt_ids",
32
52
  "completion_ids",
@@ -99,6 +119,20 @@ def index_collisions(rows: list[dict], prompt_of: Callable[[dict], object]) -> i
99
119
  return len(rows) - len({_prompt_key(prompt_of(r)) for r in rows})
100
120
 
101
121
 
122
+ def _dedup_seam_terminator(prev_completion_ids: list[int], glue: list[int]) -> list[int]:
123
+ """Collapse a duplicate turn terminator at the turn/env seam.
124
+
125
+ ``env_glue`` leads with the assistant turn's terminator (e.g. ``<|im_end|>``), which a
126
+ naturally-stopped assistant turn also keeps as its final token — so the raw stream would carry two.
127
+ When the previous turn's last token equals ``glue[0]`` drop the glue copy, keeping the assistant's
128
+ own token (real logprob / model-generated) so the stream has exactly one terminator per turn,
129
+ matching the chat template + SFT transcripts. Shared by :func:`rollout_one`,
130
+ :func:`_advance_after_turn`, and :func:`rollout_one_records` so the three paths can't drift."""
131
+ if glue and prev_completion_ids and prev_completion_ids[-1] == glue[0]:
132
+ return glue[1:]
133
+ return glue
134
+
135
+
102
136
  def rollout_one(
103
137
  *,
104
138
  example: dict,
@@ -152,13 +186,7 @@ def rollout_one(
152
186
  if active_env.rollout_done(state, max_turns):
153
187
  break
154
188
 
155
- glue = env_glue(env_msgs)
156
- # Collapse a duplicate turn terminator at the seam: env_glue leads with the assistant turn's
157
- # terminator (e.g. <|im_end|>), which vLLM also keeps as the last token of a naturally-stopped
158
- # turn — keep the assistant's own (env_mask=1, real logprob) over the env copy. See
159
- # _advance_after_turn (the rollout_async twin) for the same fix.
160
- if glue and completion_ids and completion_ids[-1] == glue[0]:
161
- glue = glue[1:]
189
+ glue = _dedup_seam_terminator(completion_ids, env_glue(env_msgs))
162
190
  if token_budget is not None and len(completion_ids) + len(glue) > token_budget:
163
191
  break
164
192
  completion_ids.extend(glue)
@@ -176,6 +204,102 @@ def rollout_one(
176
204
  }
177
205
 
178
206
 
207
+ def rollout_one_records(
208
+ *,
209
+ example: dict,
210
+ active_env,
211
+ render: Callable[[list, bool], list[int]],
212
+ generate: Callable[[list, int], object],
213
+ env_glue: Callable[[list], list[int]],
214
+ max_turns: int,
215
+ per_turn_max_tokens: int,
216
+ engine_max_len: int | None = None,
217
+ on_turn_generated: Callable[[], None] | None = None,
218
+ ) -> list[TurnRecord]:
219
+ """Drive one multi-turn episode and return a per-turn :class:`TurnRecord` list (NOT a flat masked
220
+ sequence like :func:`rollout_one`). Used by OPD, which distils each assistant turn as an independent
221
+ single-turn sample conditioned on the transcript so far.
222
+
223
+ The env turn loop is identical to :func:`rollout_one` (``new_rollout_state`` → generate →
224
+ ``record_model_turn`` → ``rollout_done`` → ``env_reply`` → glue), and the tokenizer-sensitive seam
225
+ dedup / engine-budget accounting reuse the same helpers, so the two paths can't drift. The
226
+ differences are all OPD-shaped:
227
+
228
+ - ``generate(prefix_ids, max_new)`` returns an OBJECT (opaque here) exposing ``completion_ids``
229
+ (``list[int] | None``), ``completion_text`` (``str``), ``truncated`` (``bool``) and ``skip``
230
+ (``bool``) — OPD passes its ``_GenResult`` from ``_generate_one`` (termination/trim/U+FFFD gates
231
+ already applied), so the record drops straight into the existing teacher-scoring + gkd-loss path.
232
+ - Every turn (including a truncated/empty one) is recorded so the caller counts it; a
233
+ truncated/skip turn ENDS the episode (a student that didn't terminate its turn, or emitted an
234
+ empty/invalid completion, can't meaningfully continue — and its bad turn is skipped, not
235
+ distilled, by the caller's ``_resolve_sample``).
236
+ - ``on_turn_generated`` (optional) fires AFTER each turn's generation to refresh the worker stall
237
+ clock and advance the sample counter — a many-turn episode is a long serial stretch of GPU
238
+ generates + teacher-less env steps that would otherwise emit no progress ping.
239
+
240
+ ``prefix_ids`` and ``context_messages`` are snapshotted BEFORE generation, so each record carries
241
+ the exact student-token and message context the teacher must condition on for that turn.
242
+ """
243
+ state = active_env.new_rollout_state(example)
244
+ initial_messages = state.get("prompt") or state.get("messages")
245
+ if not isinstance(initial_messages, list):
246
+ raise KeyError("multi-turn rollout state must include prompt or messages")
247
+ messages = [dict(m) for m in initial_messages]
248
+ prompt_ids = render(messages, True)
249
+ cur_ids = list(prompt_ids) # invariant: cur_ids == prompt_ids + completion tokens so far
250
+ token_budget = (engine_max_len - len(prompt_ids) - 8) if engine_max_len else None
251
+ records: list[TurnRecord] = []
252
+
253
+ turns = 0
254
+ while True:
255
+ completion_so_far = len(cur_ids) - len(prompt_ids)
256
+ max_new = per_turn_max_tokens
257
+ if token_budget is not None:
258
+ remaining = token_budget - completion_so_far
259
+ if remaining <= 0:
260
+ break
261
+ max_new = min(max_new, remaining)
262
+ # Snapshot the student-token prefix and message context BEFORE generation: this is what the
263
+ # teacher must condition on to score THIS turn (the transcript up to, but excluding, the turn).
264
+ prefix_ids = list(cur_ids)
265
+ context_messages = [dict(m) for m in messages]
266
+ gen = generate(prefix_ids, max(1, max_new))
267
+ if on_turn_generated is not None:
268
+ on_turn_generated()
269
+ records.append(
270
+ {"prefix_ids": prefix_ids, "gen": gen, "context_messages": context_messages}
271
+ )
272
+ text = getattr(gen, "completion_text", "") or ""
273
+ active_env.record_model_turn(state, text)
274
+ messages.append({"role": "assistant", "content": text})
275
+ turns += 1
276
+ # A turn that didn't terminate naturally (truncated) or produced no usable text (skip) ends the
277
+ # episode: it's recorded (counted) but not distilled, and continuing from a broken turn is
278
+ # pointless (the student can't end its turn / said nothing).
279
+ if getattr(gen, "truncated", False) or getattr(gen, "skip", False):
280
+ break
281
+ asst_ids = getattr(gen, "completion_ids", None) or []
282
+ cur_ids.extend(asst_ids)
283
+ completion_so_far = len(cur_ids) - len(prompt_ids)
284
+ if token_budget is not None and completion_so_far >= token_budget:
285
+ break
286
+ if turns >= max_turns or active_env.rollout_done(state, max_turns):
287
+ break
288
+ env_msgs = active_env.env_reply(messages, state)
289
+ if not env_msgs:
290
+ break
291
+ messages.extend(env_msgs)
292
+ # Don't append glue if the env step finished — no next model turn.
293
+ if active_env.rollout_done(state, max_turns):
294
+ break
295
+ glue = _dedup_seam_terminator(asst_ids, env_glue(env_msgs))
296
+ if token_budget is not None and completion_so_far + len(glue) > token_budget:
297
+ break
298
+ cur_ids.extend(glue)
299
+
300
+ return records
301
+
302
+
179
303
  class _RolloutState:
180
304
  """Mutable per-rollout accumulator for :func:`rollout_async` (mirrors :func:`rollout_one` locals)."""
181
305
 
@@ -248,13 +372,7 @@ def _advance_after_turn(
248
372
  if active_env.rollout_done(r.state, max_turns):
249
373
  r.done = True
250
374
  return
251
- glue = env_glue(env_msgs)
252
- # env_glue's text starts at the assistant turn's terminator (e.g. <|im_end|>); vLLM also keeps that
253
- # terminator as the final token of a naturally-stopped turn (it's in asst_ids). Collapse the duplicate
254
- # at the seam so the stream has exactly one terminator per turn (matches the chat template + SFT
255
- # transcripts), keeping the assistant's own token (env_mask=1, real logprob) over the env copy.
256
- if glue and r.completion_ids and r.completion_ids[-1] == glue[0]:
257
- glue = glue[1:]
375
+ glue = _dedup_seam_terminator(r.completion_ids, env_glue(env_msgs))
258
376
  if r.budget is not None and len(r.completion_ids) + len(glue) > r.budget:
259
377
  r.done = True
260
378
  return
@@ -429,6 +547,47 @@ def render_message_ids(tok, messages, add_generation_prompt: bool, *, thinking:
429
547
  return [int(t) for t in tok(text, add_special_tokens=False).input_ids]
430
548
 
431
549
 
550
+ def make_env_glue(tok, *, thinking: bool, cache_size: int = 8192) -> Callable[[list], list[int]]:
551
+ """Build the inter-turn "glue" tokenizer used by both the GRPO rollout (:func:`build_rollout_func`)
552
+ and OPD's per-turn rollout (:func:`rollout_one_records`).
553
+
554
+ Given the env's reply messages for a turn, returns the token ids that sit BETWEEN the assistant
555
+ turn and the next assistant generation prompt (the turn terminator + the env/observation messages +
556
+ the next assistant header). Computed with the probe trick — render ``[{assistant: PROBE}, *env]``
557
+ with ``add_generation_prompt=True`` and take everything after the probe — so the history is never
558
+ re-rendered (Qwen3's template doesn't round-trip a re-rendered transcript). Results are LRU-cached
559
+ by the env messages. Raises ``ValueError`` if the model's chat template doesn't insert assistant
560
+ content verbatim (token-aligned multi-turn is then unsupported for that model)."""
561
+ cache = _LRUCache(cache_size)
562
+ probe = "flash-env-glue-probe"
563
+
564
+ def env_glue(env_messages: list) -> list[int]:
565
+ cache_key = json.dumps(env_messages, sort_keys=True, default=str)
566
+ cached = cache.get(cache_key)
567
+ if cached is not None:
568
+ return cached
569
+ text = tok.apply_chat_template(
570
+ [{"role": "assistant", "content": probe}, *env_messages],
571
+ add_generation_prompt=True,
572
+ tokenize=False,
573
+ enable_thinking=thinking,
574
+ )
575
+ first = text.find(probe)
576
+ if first == -1 or text.find(probe, first + len(probe)) != -1:
577
+ raise ValueError(
578
+ "multi-turn env_glue could not uniquely locate its probe in the rendered chat "
579
+ "template; this model's template does not insert assistant content verbatim, so "
580
+ "token-aligned multi-turn rollout is unsupported for it (use a single-turn/tool "
581
+ "env or a different model)."
582
+ )
583
+ glue_text = text[first + len(probe) :]
584
+ glue = [int(t) for t in tok(glue_text, add_special_tokens=False).input_ids]
585
+ cache.put(cache_key, glue)
586
+ return glue
587
+
588
+ return env_glue
589
+
590
+
432
591
  def _engine_vocab_size(engine) -> int | None:
433
592
  """Best-effort vocab size from the colocate vLLM engine, or None. Never raises."""
434
593
  try:
@@ -482,34 +641,7 @@ def build_rollout_func(
482
641
  _render_cache.put(cache_key, ids)
483
642
  return ids
484
643
 
485
- _glue_cache = _LRUCache(8192)
486
-
487
- def env_glue(env_messages: list) -> list[int]:
488
- cache_key = json.dumps(env_messages, sort_keys=True, default=str)
489
- cached = _glue_cache.get(cache_key)
490
- if cached is not None:
491
- return cached
492
- # Render a probe assistant turn + env messages, then take everything after the probe
493
- # to get the inter-turn glue without re-rendering history (Qwen3 doesn't round-trip).
494
- probe = "flash-env-glue-probe"
495
- text = tok.apply_chat_template(
496
- [{"role": "assistant", "content": probe}, *env_messages],
497
- add_generation_prompt=True,
498
- tokenize=False,
499
- enable_thinking=thinking,
500
- )
501
- first = text.find(probe)
502
- if first == -1 or text.find(probe, first + len(probe)) != -1:
503
- raise ValueError(
504
- "multi-turn env_glue could not uniquely locate its probe in the rendered chat "
505
- "template; this model's template does not insert assistant content verbatim, so "
506
- "token-aligned multi-turn rollout is unsupported for it (use a single-turn/tool "
507
- "env or a different model)."
508
- )
509
- glue_text = text[first + len(probe) :]
510
- glue = [int(t) for t in tok(glue_text, add_special_tokens=False).input_ids]
511
- _glue_cache.put(cache_key, glue)
512
- return glue
644
+ env_glue = make_env_glue(tok, thinking=thinking)
513
645
 
514
646
  def rollout_func(prompts, trainer):
515
647
  engine = trainer.vllm_generation.llm
@@ -35,6 +35,7 @@ from flash.engine.worker.decoding import (
35
35
  from flash.engine.worker.finalize import write_train_meta
36
36
  from flash.engine.worker.gpu_setup import (
37
37
  finalize_alloc_conf_for_sleep,
38
+ force_vit_sdpa_on_blackwell,
38
39
  force_vllm_backend_for_sm120,
39
40
  patch_trl_colocate_llm_kwargs,
40
41
  )
@@ -54,6 +55,8 @@ from flash.engine.worker.heartbeat import (
54
55
  _HB_TERMINAL_ONLY_INTERVAL_S,
55
56
  _HB_TERMINAL_STAGES,
56
57
  _HB_THROTTLED_STAGES,
58
+ _HB_TIGHT_LIVENESS_STAGES,
59
+ _HB_UPLOAD_LIVENESS_STAGES,
57
60
  _HB_UPLOAD_LOCK,
58
61
  _SFT_HEARTBEAT_INTERVAL_S,
59
62
  _STEP_GPU_DIAG_INTERVAL_S,
@@ -371,6 +374,8 @@ __all__ = [
371
374
  "_HB_TERMINAL_ONLY_INTERVAL_S",
372
375
  "_HB_TERMINAL_STAGES",
373
376
  "_HB_THROTTLED_STAGES",
377
+ "_HB_TIGHT_LIVENESS_STAGES",
378
+ "_HB_UPLOAD_LIVENESS_STAGES",
374
379
  "_HB_UPLOAD_LOCK",
375
380
  "_SFT_HEARTBEAT_INTERVAL_S",
376
381
  "_STEP_GPU_DIAG_INTERVAL_S",
@@ -412,6 +417,7 @@ __all__ = [
412
417
  "disable_liger_grpo_torch_compile",
413
418
  "error_artifact_name",
414
419
  "finalize_alloc_conf_for_sleep",
420
+ "force_vit_sdpa_on_blackwell",
415
421
  "force_vllm_backend_for_sm120",
416
422
  "free_gpu",
417
423
  "fused_optim_name",
@@ -49,6 +49,45 @@ def force_vllm_backend_for_sm120() -> str | None:
49
49
  return backend
50
50
 
51
51
 
52
+ def force_vit_sdpa_on_blackwell() -> bool:
53
+ """Force the VISION-encoder (ViT) attention backend to TORCH_SDPA on Blackwell (sm100/sm120).
54
+
55
+ Qwen3.6-35B-A3B is a VL model, so vLLM builds its vision tower even for a text-only GRPO rollout
56
+ and runs a ViT attention during engine init/profiling. On Blackwell vLLM 0.19.1 routes the ViT to
57
+ its CUTE flash-attn (``vit_flash_attn_wrapper`` -> ``flash_attn_varlen_func`` ->
58
+ ``vllm.vllm_flash_attn.cute``), which is UNIMPORTABLE against every published ``nvidia-cutlass-dsl``:
59
+ the vendored cute references ``cutlass.cute.core.ThrMma`` (present only <=4.5.x) while
60
+ ``vit_attn_wrappers`` also imports ``cutlass._mlir_helpers`` (present only >=4.6.0) — the two symbols
61
+ never coexist, so the first ViT attention call aborts with
62
+ ``AttributeError: module 'cutlass.cute.core' has no attribute 'ThrMma'`` (4.6.0) or
63
+ ``ModuleNotFoundError: cutlass._mlir_helpers`` (<=4.5.2), crashing EVERY B200 GRPO rollout (a
64
+ version pin cannot fix it — measured 2026-07-07). ``get_vit_attn_backend`` honors
65
+ ``MultiModalConfig.mm_encoder_attn_backend`` UNCONDITIONALLY, and TORCH_SDPA is a supported ViT
66
+ backend on cc>=8.0, so pinning it sidesteps the CUTE import entirely (the LM/decoder attention is
67
+ unaffected — that path is FLASHINFER/flash-attn, chosen separately).
68
+
69
+ * Injected as a colocate ``LLM(...)`` kwarg (``EngineArgs.mm_encoder_attn_backend``, str-typed),
70
+ composing with the other overrides; must run BEFORE ``GRPOTrainer.__init__`` builds the engine.
71
+ * No-op off Blackwell and on non-multimodal models (a text-only model simply never builds a ViT).
72
+ Returns True iff the override was injected."""
73
+ try:
74
+ import torch
75
+
76
+ if not torch.cuda.is_available() or torch.cuda.get_device_capability(0)[0] not in (10, 12):
77
+ return False
78
+ except Exception as e:
79
+ print("[rl] ViT-SDPA Blackwell probe skipped:", e)
80
+ return False
81
+ ok = patch_trl_colocate_llm_kwargs(mm_encoder_attn_backend="TORCH_SDPA")
82
+ if ok:
83
+ print(
84
+ "[rl] Blackwell (sm100/sm120): mm_encoder_attn_backend=TORCH_SDPA on the colocate rollout "
85
+ "engine (vLLM 0.19.1 ViT CUTE flash-attn is unimportable vs every nvidia-cutlass-dsl: "
86
+ "cute.core.ThrMma <=4.5.x XOR cutlass._mlir_helpers >=4.6.0 -> crashes the B200 rollout)"
87
+ )
88
+ return ok
89
+
90
+
52
91
  def finalize_alloc_conf_for_sleep() -> None:
53
92
  """Sync PYTORCH_ALLOC_CONF with the resolved GRPO sleep mode (RL only).
54
93
 
@@ -76,6 +115,7 @@ def patch_trl_colocate_llm_kwargs(
76
115
  max_num_batched_tokens: int | None = None,
77
116
  enforce_eager: bool | None = None,
78
117
  attention_backend: str | None = None,
118
+ mm_encoder_attn_backend: str | None = None,
79
119
  ) -> bool:
80
120
  """Inject vLLM ``LLM(...)`` kwargs into TRL's colocated rollout engine that can't be expressed via
81
121
  ``GRPOConfig`` or the (pinned) vLLM env registry — must run BEFORE ``GRPOTrainer.__init__`` builds
@@ -105,6 +145,10 @@ def patch_trl_colocate_llm_kwargs(
105
145
  * ``attention_backend="FLASHINFER"|"TRITON_ATTN"``: pin a PTX-independent decoder attention backend
106
146
  (consumer-Blackwell sm120) — the SUPPORTED replacement for the removed ``VLLM_ATTENTION_BACKEND``
107
147
  env. ``EngineArgs`` coerces the bare member name through ``AttentionConfig.validate_backend_before``.
148
+ * ``mm_encoder_attn_backend="TORCH_SDPA"``: force the VISION-encoder (ViT) attention backend for a
149
+ multimodal model (Qwen3.6-35B-A3B is VL) off vLLM's CUTE flash-attn path — see
150
+ ``force_vit_sdpa_on_blackwell``. Maps to ``MultiModalConfig.mm_encoder_attn_backend``, which
151
+ ``get_vit_attn_backend`` honors unconditionally.
108
152
 
109
153
  Repeated calls COMPOSE: run_rl injects the attention backend, the KV/prefill knobs, and eager mode
110
154
  at three SEPARATE points, so each call merges its kwargs into one accumulated module-level override
@@ -120,6 +164,8 @@ def patch_trl_colocate_llm_kwargs(
120
164
  new_overrides["enforce_eager"] = bool(enforce_eager)
121
165
  if attention_backend is not None:
122
166
  new_overrides["attention_backend"] = attention_backend
167
+ if mm_encoder_attn_backend is not None:
168
+ new_overrides["mm_encoder_attn_backend"] = mm_encoder_attn_backend
123
169
  if not new_overrides:
124
170
  return False
125
171
  try:
@@ -38,12 +38,21 @@ _HB_SETUP_LIVENESS_STAGES = frozenset(
38
38
  "rl_finalizing",
39
39
  }
40
40
  )
41
+ # Mid-training the per-step checkpoint upload runs SYNCHRONOUSLY on the trainer thread (dev #445),
42
+ # which freezes the trainer's global_step — so the ``rl_step``/``sft_step`` liveness daemon can only
43
+ # emit bare liveness pings for the whole upload, and those DON'T advance the provider's stall clock.
44
+ # A ``checkpoint_uploading`` keepalive daemon (liveness_heartbeat(keepalive=True)) wraps the upload to
45
+ # keep that clock fed; it rides the SAME tight, throttled cadence as a setup stage. Kept OUT of
46
+ # _HB_SETUP_LIVENESS_STAGES so that set's "setup-phase" meaning (and its tests) stay honest.
47
+ _HB_UPLOAD_LIVENESS_STAGES = frozenset({"checkpoint_uploading"})
48
+ # Liveness stages that ride the tighter setup-liveness upload interval (setup + mid-train upload).
49
+ _HB_TIGHT_LIVENESS_STAGES = _HB_SETUP_LIVENESS_STAGES | _HB_UPLOAD_LIVENESS_STAGES
41
50
  # Throttled to avoid blowing the 128/hr HF commit cap; terminal transitions are never throttled. Every
42
- # setup-liveness stage is throttled (⊂) PLUS the per-step training stages: opd_filtering_prompts alone
51
+ # tight-liveness stage is throttled (⊂) PLUS the per-step training stages: opd_filtering_prompts alone
43
52
  # emits a REAL (non-liveness) heartbeat every scan tick — ~120/hr on a large split before model load —
44
53
  # so unthrottled the setup stages blow the cap; throttle them exactly like their sft_pretokenizing
45
- # analogue (codex[bot]).
46
- _HB_THROTTLED_STAGES = _HB_SETUP_LIVENESS_STAGES | frozenset({"rl_step", "sft_step", "opd_step"})
54
+ # analogue (codex[bot]). checkpoint_uploading keepalive re-emits every 30s too, so it MUST be throttled.
55
+ _HB_THROTTLED_STAGES = _HB_TIGHT_LIVENESS_STAGES | frozenset({"rl_step", "sft_step", "opd_step"})
47
56
  _HB_TERMINAL_STAGES = frozenset({"done", "already_done"})
48
57
  # 600s -> ~6 commits/hr; keeps stall detector alive without hitting the HF commit cap.
49
58
  _HB_TERMINAL_ONLY_INTERVAL_S = 600.0
@@ -137,7 +146,7 @@ def heartbeat(stage: str, *, liveness: bool = False, force: bool = False, **kw):
137
146
  else:
138
147
  throttled = stage in _HB_THROTTLED_STAGES
139
148
  interval_s = _w._HB_MIN_INTERVAL_S
140
- if stage in _HB_SETUP_LIVENESS_STAGES:
149
+ if stage in _HB_TIGHT_LIVENESS_STAGES:
141
150
  interval_s = min(interval_s, _w._HB_SETUP_LIVENESS_INTERVAL_S)
142
151
  upload_due = not throttled or (now - _w._HB_LAST_UPLOAD) >= interval_s
143
152
  # ``force`` bypasses the per-stage throttle (but not TERMINAL_ONLY mode, handled above) when
@@ -291,9 +300,18 @@ _STALL_DUMP_S = 1200.0
291
300
 
292
301
 
293
302
  @contextlib.contextmanager
294
- def liveness_heartbeat(stage, progress=None, fields=None, progress_step=False):
303
+ def liveness_heartbeat(stage, progress=None, fields=None, progress_step=False, keepalive=False):
295
304
  """Emit liveness pings for ``stage`` while the wrapped block runs on the main thread.
296
305
 
306
+ ``keepalive``: the wrapped block is legitimate BLOCKING I/O with no per-step progress signal the
307
+ daemon can observe — a synchronous checkpoint/adapter upload (dev #445) or the finalize upload —
308
+ so EVERY tick emits a REAL (non-liveness) heartbeat instead of a bare ping. Bare liveness pings do
309
+ NOT advance the provider's stall clock (`_poll.surface_heartbeat` returns stage=None for them), so
310
+ without this a healthy multi-minute upload that outlasts STALL_AFTER_S (1500s) is wrongly killed
311
+ mid-save. Safe because a genuinely wedged upload surfaces as an EXCEPTION through its own retry
312
+ budget (ending this context), not an infinite silent hang — so it does not mask a real stall. Pair
313
+ with a throttled stage (see _HB_UPLOAD_LIVENESS_STAGES) so the 30s re-emit can't blow the HF cap.
314
+
297
315
  ``progress``: optional ``() -> float | None`` monotonic counter; advances emit a REAL heartbeat.
298
316
  ``fields``: optional ``() -> dict`` of EXTRA payload fields merged into every emission (liveness
299
317
  and progress alike). Use it to carry the billing/stall ``step`` on a stage the poller step-gates:
@@ -342,7 +360,9 @@ def liveness_heartbeat(stage, progress=None, fields=None, progress_step=False):
342
360
  extra = fields() or {}
343
361
  if progress_step and last_val is not None:
344
362
  extra["step"] = int(last_val)
345
- _w.heartbeat(stage, liveness=not made_progress, gpu=gpu, **extra)
363
+ # keepalive: a legitimate blocking upload IS progress the daemon can't sample per-step, so
364
+ # force a REAL heartbeat every tick to keep the provider's stall clock fed (see docstring).
365
+ _w.heartbeat(stage, liveness=(not made_progress) and not keepalive, gpu=gpu, **extra)
346
366
  last_progress = float(getattr(_w, "_HB_LAST_PROGRESS_TS", 0.0) or 0.0)
347
367
  if not dumped and last_progress and (time.time() - last_progress) > _STALL_DUMP_S:
348
368
  _dump_thread_stacks(f"{stage}: no progress for >{_STALL_DUMP_S:.0f}s")