freesolo-flash-dev 0.2.44__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 (308) hide show
  1. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/PKG-INFO +6 -4
  2. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/README.md +5 -3
  3. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/catalog.py +27 -9
  4. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/cli/commands.py +56 -6
  5. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/cli/render.py +53 -0
  6. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/cli/training_doc.py +49 -3
  7. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/client/runtime_secrets.py +7 -0
  8. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/cost/analytical.py +69 -4
  9. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/cost/facts.py +43 -0
  10. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/cost/spec.py +15 -3
  11. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/cost/types.py +38 -15
  12. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/accounting.py +5 -1
  13. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/multiturn_rollout.py +174 -42
  14. freesolo_flash_dev-0.2.46/flash/engine/recipe.py +88 -0
  15. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/vram.py +83 -14
  16. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/__init__.py +53 -17
  17. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/adapter.py +7 -10
  18. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/finalize.py +18 -1
  19. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/gpu_setup.py +46 -0
  20. freesolo_flash_dev-0.2.46/flash/engine/worker/heartbeat.py +377 -0
  21. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/hf.py +59 -27
  22. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/lora.py +114 -313
  23. freesolo_flash_dev-0.2.46/flash/engine/worker/opd.py +1190 -0
  24. freesolo_flash_dev-0.2.46/flash/engine/worker/opd_gkd.py +255 -0
  25. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/rl.py +118 -101
  26. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/sft.py +72 -43
  27. freesolo_flash_dev-0.2.46/flash/engine/worker/teacher.py +318 -0
  28. freesolo_flash_dev-0.2.46/flash/engine/worker/tokenizer_align.py +119 -0
  29. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/lora_rank.py +45 -21
  30. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/_poll.py +32 -6
  31. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/_worker.py +30 -10
  32. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/runner/__init__.py +4 -3
  33. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/schema/__init__.py +69 -14
  34. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/schema/fields.py +10 -0
  35. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/spec.py +3 -0
  36. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/pyproject.toml +2 -2
  37. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_algorithms.py +47 -1
  38. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_allocator.py +22 -0
  39. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_charge_pricing.py +12 -0
  40. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_cli_commands.py +89 -0
  41. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_cli_render_theme.py +49 -0
  42. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_cost_analytical.py +32 -0
  43. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_flash_worker.py +58 -1
  44. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_lora_rank_preflight.py +35 -1
  45. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_metrics_schema_agent_contract.py +2 -0
  46. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_multiturn_rollout.py +101 -0
  47. freesolo_flash_dev-0.2.46/tests/test_opd.py +2709 -0
  48. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_providers_symmetry.py +5 -1
  49. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_resume_on_retry.py +4 -0
  50. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_runmgmt.py +36 -0
  51. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_vl_warmstart_recombine.py +5 -3
  52. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_worker_hardexit.py +21 -0
  53. freesolo_flash_dev-0.2.46/tests/test_worker_init_heartbeat.py +882 -0
  54. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_worker_stack.py +12 -19
  55. freesolo_flash_dev-0.2.44/docs/cli-style/README.md +0 -33
  56. freesolo_flash_dev-0.2.44/docs/cli-style/env-setup-survey-dark.png +0 -0
  57. freesolo_flash_dev-0.2.44/docs/cli-style/env-setup-survey-light.png +0 -0
  58. freesolo_flash_dev-0.2.44/docs/cli-style/generate.py +0 -492
  59. freesolo_flash_dev-0.2.44/docs/cli-style/index.html +0 -343
  60. freesolo_flash_dev-0.2.44/docs/cli-style/preview.png +0 -0
  61. freesolo_flash_dev-0.2.44/docs/cli-style/themed-errors.png +0 -0
  62. freesolo_flash_dev-0.2.44/docs/cli-style/train-config-error-after.png +0 -0
  63. freesolo_flash_dev-0.2.44/docs/cli-style/train-config-error-before.png +0 -0
  64. freesolo_flash_dev-0.2.44/docs/kernel-cache.md +0 -100
  65. freesolo_flash_dev-0.2.44/flash/engine/recipe.py +0 -53
  66. freesolo_flash_dev-0.2.44/flash/engine/worker/heartbeat.py +0 -251
  67. freesolo_flash_dev-0.2.44/tests/test_vl_weight_sync.py +0 -205
  68. freesolo_flash_dev-0.2.44/tests/test_worker_init_heartbeat.py +0 -413
  69. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/.dockerignore +0 -0
  70. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/.env.example +0 -0
  71. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/.github/workflows/auto-rebake.yml +0 -0
  72. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/.github/workflows/bake-kernel-cache.yml +0 -0
  73. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/.github/workflows/ci.yml +0 -0
  74. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/.github/workflows/main-source-guard.yml +0 -0
  75. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/.github/workflows/notify-tests-repo.yml +0 -0
  76. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/.github/workflows/publish-dev.yml +0 -0
  77. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/.github/workflows/publish-image.yml +0 -0
  78. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/.github/workflows/publish.yml +0 -0
  79. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/.github/workflows/version-parity.yml +0 -0
  80. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/.github/workflows/worker-image.yml +0 -0
  81. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/.gitignore +0 -0
  82. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/Dockerfile +0 -0
  83. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/Dockerfile.worker +0 -0
  84. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/LICENSE +0 -0
  85. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/build/kernel_cache/.gitignore +0 -0
  86. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/build/kernel_cache/.keep +0 -0
  87. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/docker/Dockerfile.kernelcache +0 -0
  88. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/docker/Dockerfile.kernelcache.relayer +0 -0
  89. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/docker/bake_kernel_cache.py +0 -0
  90. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/docker/bake_pod_entry.py +0 -0
  91. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/docker/kernel_fingerprint.py +0 -0
  92. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/docker/make_rp_handler.py +0 -0
  93. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/__init__.py +0 -0
  94. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/_channel.py +0 -0
  95. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/_fileio.py +0 -0
  96. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/_logging.py +0 -0
  97. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/_update_check.py +0 -0
  98. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/adapter_artifacts.py +0 -0
  99. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/cli/__init__.py +0 -0
  100. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/cli/__main__.py +0 -0
  101. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/cli/_tty.py +0 -0
  102. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/cli/envpush.py +0 -0
  103. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/client/__init__.py +0 -0
  104. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/client/config.py +0 -0
  105. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/client/http.py +0 -0
  106. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/client/specs.py +0 -0
  107. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/cost/__init__.py +0 -0
  108. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/__init__.py +0 -0
  109. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/chalk_kernels.py +0 -0
  110. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/__main__.py +0 -0
  111. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/_pkg.py +0 -0
  112. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/decoding.py +0 -0
  113. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/grpo.py +0 -0
  114. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/kernel_warmup.py +0 -0
  115. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/packing.py +0 -0
  116. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/perf/__init__.py +0 -0
  117. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/perf/attn.py +0 -0
  118. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/perf/diagnostics.py +0 -0
  119. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/perf/lifecycle.py +0 -0
  120. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/perf/liger.py +0 -0
  121. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/perf/loraplus.py +0 -0
  122. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/perf/memory.py +0 -0
  123. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/engine/worker/wandb_log.py +0 -0
  124. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/envs/__init__.py +0 -0
  125. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/envs/adapter.py +0 -0
  126. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/envs/archive_policy.py +0 -0
  127. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/envs/base.py +0 -0
  128. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/envs/loader.py +0 -0
  129. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/envs/pull.py +0 -0
  130. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/envs/registry.py +0 -0
  131. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/__init__.py +0 -0
  132. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/_auth.py +0 -0
  133. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/_hf_artifacts.py +0 -0
  134. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/_hf_retry.py +0 -0
  135. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/_http.py +0 -0
  136. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/_instance.py +0 -0
  137. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/_instance_bootstrap.py +0 -0
  138. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/_instance_poll.py +0 -0
  139. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/_instance_provider.py +0 -0
  140. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/allocator.py +0 -0
  141. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/base.py +0 -0
  142. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/lambdalabs/__init__.py +0 -0
  143. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/lambdalabs/api.py +0 -0
  144. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/lambdalabs/auth.py +0 -0
  145. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/lambdalabs/gpus.py +0 -0
  146. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/lambdalabs/jobs/__init__.py +0 -0
  147. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/lambdalabs/jobs/builders.py +0 -0
  148. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/lambdalabs/preflight.py +0 -0
  149. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/lambdalabs/pricing.py +0 -0
  150. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/preflight.py +0 -0
  151. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/realized.py +0 -0
  152. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/__init__.py +0 -0
  153. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/api.py +0 -0
  154. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/auth.py +0 -0
  155. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/cost.py +0 -0
  156. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/gpus.py +0 -0
  157. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/jobs.py +0 -0
  158. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/keys.py +0 -0
  159. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/preflight.py +0 -0
  160. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/preload.py +0 -0
  161. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/pricing.py +0 -0
  162. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/slots.py +0 -0
  163. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/train/__init__.py +0 -0
  164. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/train/deps.py +0 -0
  165. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/runpod/train/endpoints.py +0 -0
  166. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/vast/__init__.py +0 -0
  167. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/vast/api.py +0 -0
  168. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/vast/auth.py +0 -0
  169. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/vast/jobs/__init__.py +0 -0
  170. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/vast/jobs/builders.py +0 -0
  171. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/vast/preflight.py +0 -0
  172. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/providers/vast/pricing.py +0 -0
  173. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/py.typed +0 -0
  174. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/runner/checkpoints.py +0 -0
  175. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/runner/deploy.py +0 -0
  176. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/runner/lifecycle.py +0 -0
  177. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/serve/__init__.py +0 -0
  178. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/serve/deploy.py +0 -0
  179. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/serve/export.py +0 -0
  180. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/serve/pricing.py +0 -0
  181. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/server/__init__.py +0 -0
  182. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/server/__main__.py +0 -0
  183. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/server/_deps.py +0 -0
  184. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/server/_internal_client.py +0 -0
  185. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/server/_locks.py +0 -0
  186. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/server/_runtime.py +0 -0
  187. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/server/app.py +0 -0
  188. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/server/auth.py +0 -0
  189. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/server/billing.py +0 -0
  190. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/server/billing_retry.py +0 -0
  191. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/server/checkpoints.py +0 -0
  192. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/server/db.py +0 -0
  193. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/server/environment_registry.py +0 -0
  194. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/server/envs.py +0 -0
  195. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/server/reconcile.py +0 -0
  196. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/server/routes/__init__.py +0 -0
  197. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/server/routes/envs.py +0 -0
  198. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/server/routes/meta.py +0 -0
  199. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/server/routes/runs.py +0 -0
  200. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/server/routes/serving.py +0 -0
  201. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/flash/server/run_registry.py +0 -0
  202. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/infisical-entrypoint.sh +0 -0
  203. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/scripts/build_dev_dist.py +0 -0
  204. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/__init__.py +0 -0
  205. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/_helpers/__init__.py +0 -0
  206. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/_helpers/runner.py +0 -0
  207. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/_helpers/specs.py +0 -0
  208. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/_helpers/vast.py +0 -0
  209. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/conftest.py +0 -0
  210. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/fixtures/math_eval.jsonl +0 -0
  211. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/fixtures/math_train.jsonl +0 -0
  212. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/live/__init__.py +0 -0
  213. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/live/conftest.py +0 -0
  214. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/live/test_lambda_live.py +0 -0
  215. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/live/test_runpod_live.py +0 -0
  216. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/live/test_vast_live.py +0 -0
  217. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_agent_flash_cli_contract.py +0 -0
  218. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_b200_rollout_opt.py +0 -0
  219. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_backend_jobspec_contract.py +0 -0
  220. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_billing_retry.py +0 -0
  221. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_cancel_remote.py +0 -0
  222. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_catalog_consistency.py +0 -0
  223. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_chalk_kernels.py +0 -0
  224. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_checkpoints.py +0 -0
  225. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_cli_errors.py +0 -0
  226. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_cli_estimate.py +0 -0
  227. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_cli_help.py +0 -0
  228. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_cli_managed.py +0 -0
  229. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_client.py +0 -0
  230. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_client_server_integration.py +0 -0
  231. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_config_overrides.py +0 -0
  232. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_cost_equation.py +0 -0
  233. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_cost_estimate.py +0 -0
  234. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_cost_hardware.py +0 -0
  235. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_cost_models.py +0 -0
  236. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_cost_rewards.py +0 -0
  237. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_dev_channel.py +0 -0
  238. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_disk_gb.py +0 -0
  239. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_endpoint_name.py +0 -0
  240. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_env_cache_evict.py +0 -0
  241. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_env_delete.py +0 -0
  242. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_env_download.py +0 -0
  243. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_env_download_api.py +0 -0
  244. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_env_no_install.py +0 -0
  245. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_env_publish.py +0 -0
  246. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_env_pull.py +0 -0
  247. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_env_pull_managed_control_plane.py +0 -0
  248. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_env_push.py +0 -0
  249. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_env_rate_limit_resolve.py +0 -0
  250. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_envs_coverage.py +0 -0
  251. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_export.py +0 -0
  252. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_flash_mvp.py +0 -0
  253. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_flashinfer_cache_dirs.py +0 -0
  254. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_github_urlopen_retry.py +0 -0
  255. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_gpus.py +0 -0
  256. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_grpo_mask_aware.py +0 -0
  257. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_grpo_params.py +0 -0
  258. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_grpo_sleep_gate.py +0 -0
  259. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_health_liveness.py +0 -0
  260. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_idle_endpoint_reaper.py +0 -0
  261. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_internal_client.py +0 -0
  262. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_jobs.py +0 -0
  263. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_kernel_cache.py +0 -0
  264. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_kernel_fingerprint.py +0 -0
  265. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_kv_util.py +0 -0
  266. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_lambda_runner.py +0 -0
  267. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_logging.py +0 -0
  268. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_login_perms.py +0 -0
  269. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_managed_hf_repo.py +0 -0
  270. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_mig_guard.py +0 -0
  271. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_oom_escalate_gpu.py +0 -0
  272. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_open_model_policy.py +0 -0
  273. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_orchestrator_flash.py +0 -0
  274. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_packing.py +0 -0
  275. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_poll_helpers.py +0 -0
  276. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_preflight.py +0 -0
  277. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_provider_routing.py +0 -0
  278. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_provider_teardown_robustness.py +0 -0
  279. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_realized_cost.py +0 -0
  280. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_resolve_params_b.py +0 -0
  281. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_runpod_api_delete.py +0 -0
  282. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_runpod_key_fingerprint.py +0 -0
  283. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_runpod_key_waterfall.py +0 -0
  284. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_runpod_slots.py +0 -0
  285. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_serve.py +0 -0
  286. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_server_api.py +0 -0
  287. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_server_billing.py +0 -0
  288. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_server_db.py +0 -0
  289. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_serving_contract.py +0 -0
  290. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_sft_example_selection.py +0 -0
  291. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_sft_gc_off.py +0 -0
  292. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_spec_and_validation.py +0 -0
  293. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_thinking_config.py +0 -0
  294. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_train_context_preflight.py +0 -0
  295. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_update_check.py +0 -0
  296. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_vast_api.py +0 -0
  297. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_vast_offers.py +0 -0
  298. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_vast_runner.py +0 -0
  299. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_verifiers.py +0 -0
  300. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_version.py +0 -0
  301. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_vl_warmstart_adapter_keys.py +0 -0
  302. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_wandb_naming.py +0 -0
  303. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_warmstart_cross_repo.py +0 -0
  304. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_weight_cache.py +0 -0
  305. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_worker_dryrun.py +0 -0
  306. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_worker_image.py +0 -0
  307. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.46}/tests/test_worker_thinking.py +0 -0
  308. {freesolo_flash_dev-0.2.44 → 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.44
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
@@ -51,7 +51,8 @@ Description-Content-Type: text/markdown
51
51
 
52
52
  # Flash
53
53
 
54
- Managed LoRA post-training service: SFT and GRPO on managed Flash GPUs.
54
+ Managed LoRA post-training service: SFT, GRPO, and on-policy distillation (from a
55
+ Fireworks-hosted GLM teacher) on managed Flash GPUs.
55
56
  The allocator picks the cheapest validated managed GPU class that fits the run.
56
57
 
57
58
  ## Scope
@@ -79,8 +80,9 @@ The allocator picks the cheapest validated managed GPU class that fits the run.
79
80
  - `flash/providers/` — managed GPU substrate code (pricing, GPU classes, durable
80
81
  submit/poll, preflight) behind the `base.Provider` protocol, with an
81
82
  `allocator.py` that picks the cheapest fitting managed GPU class
82
- - `flash/engine/` — the on-GPU worker (TRL + colocated vLLM rollouts) and the
83
- shared recipe; SFT targets and RL rewards route through the active environment
83
+ - `flash/engine/` — the on-GPU worker (TRL + colocated vLLM rollouts; distillation
84
+ scores on-policy student samples against a remote GLM teacher) and the shared
85
+ recipe; SFT targets and RL rewards route through the active environment
84
86
  (task-specific grading lives with its example, not in the engine)
85
87
  - `flash/envs/` — environment machinery: registry and the adapter that loads
86
88
  Freesolo SDK environments onto the worker's interface
@@ -1,6 +1,7 @@
1
1
  # Flash
2
2
 
3
- Managed LoRA post-training service: SFT and GRPO on managed Flash GPUs.
3
+ Managed LoRA post-training service: SFT, GRPO, and on-policy distillation (from a
4
+ Fireworks-hosted GLM teacher) on managed Flash GPUs.
4
5
  The allocator picks the cheapest validated managed GPU class that fits the run.
5
6
 
6
7
  ## Scope
@@ -28,8 +29,9 @@ The allocator picks the cheapest validated managed GPU class that fits the run.
28
29
  - `flash/providers/` — managed GPU substrate code (pricing, GPU classes, durable
29
30
  submit/poll, preflight) behind the `base.Provider` protocol, with an
30
31
  `allocator.py` that picks the cheapest fitting managed GPU class
31
- - `flash/engine/` — the on-GPU worker (TRL + colocated vLLM rollouts) and the
32
- shared recipe; SFT targets and RL rewards route through the active environment
32
+ - `flash/engine/` — the on-GPU worker (TRL + colocated vLLM rollouts; distillation
33
+ scores on-policy student samples against a remote GLM teacher) and the shared
34
+ recipe; SFT targets and RL rewards route through the active environment
33
35
  (task-specific grading lives with its example, not in the engine)
34
36
  - `flash/envs/` — environment machinery: registry and the adapter that loads
35
37
  Freesolo SDK environments onto the worker's interface
@@ -6,7 +6,24 @@ import math
6
6
  from dataclasses import asdict, dataclass
7
7
  from typing import Any
8
8
 
9
- ALGORITHMS = ("sft", "grpo")
9
+ ALGORITHMS = ("sft", "grpo", "opd")
10
+
11
+ # Algorithms whose training step SAMPLES on-policy student completions (a rollout): GRPO and OPD,
12
+ # versus SFT which trains on a fixed dataset. Single source of truth for the "has rollouts" concept
13
+ # the cost model, VRAM sizing and worker allocator branch on — import ``samples_on_policy`` instead
14
+ # of hand-rolling the ("grpo", "opd") tuple at each site.
15
+ _ON_POLICY_ALGORITHMS = frozenset({"grpo", "opd"})
16
+
17
+
18
+ def samples_on_policy(algorithm: str) -> bool:
19
+ """True when a training step samples on-policy student completions (GRPO or OPD).
20
+
21
+ Tolerant of the ``rl`` phase-name alias for grpo (``JobSpec.phase``) so callers that pass a
22
+ phase rather than an algorithm resolve identically."""
23
+ algo = (algorithm or "").strip().lower()
24
+ if algo == "rl": # phase-name alias for grpo
25
+ algo = "grpo"
26
+ return algo in _ON_POLICY_ALGORITHMS
10
27
 
11
28
 
12
29
  def normalize_algorithm(value: str) -> str:
@@ -145,7 +162,7 @@ MODELS: dict[str, ModelInfo] = {
145
162
  params="1.2B dense (Llama arch)",
146
163
  params_b=1.2,
147
164
  vocab_size=130_560,
148
- algos=("sft", "grpo"),
165
+ algos=("sft", "grpo", "opd"),
149
166
  min_vram_gb=12,
150
167
  recommended_gpu="RTX 4090",
151
168
  serving=ServingCapacity(
@@ -164,7 +181,7 @@ MODELS: dict[str, ModelInfo] = {
164
181
  params="0.9B (text-only fine-tune)",
165
182
  params_b=0.9,
166
183
  vocab_size=248_320,
167
- algos=("sft", "grpo"),
184
+ algos=("sft", "grpo", "opd"),
168
185
  min_vram_gb=12,
169
186
  recommended_gpu="RTX 4090",
170
187
  serving=ServingCapacity(
@@ -183,7 +200,7 @@ MODELS: dict[str, ModelInfo] = {
183
200
  params="2.3B (text-only fine-tune)",
184
201
  params_b=2.3,
185
202
  vocab_size=248_320,
186
- algos=("sft", "grpo"),
203
+ algos=("sft", "grpo", "opd"),
187
204
  min_vram_gb=16,
188
205
  recommended_gpu="RTX 4090",
189
206
  serving=ServingCapacity(
@@ -201,7 +218,7 @@ MODELS: dict[str, ModelInfo] = {
201
218
  params="4.7B (text-only fine-tune)",
202
219
  params_b=4.7,
203
220
  vocab_size=248_320,
204
- algos=("sft", "grpo"),
221
+ algos=("sft", "grpo", "opd"),
205
222
  min_vram_gb=32,
206
223
  recommended_gpu="RTX 5090",
207
224
  serving=ServingCapacity(
@@ -223,7 +240,7 @@ MODELS: dict[str, ModelInfo] = {
223
240
  params="9.7B (text-only fine-tune)",
224
241
  params_b=9.7,
225
242
  vocab_size=248_320,
226
- algos=("sft", "grpo"),
243
+ algos=("sft", "grpo", "opd"),
227
244
  min_vram_gb=48,
228
245
  # NOT QLoRA: peft bnb merge during GRPO rollout diverges trainer precision -> TRL ratio collapses to 0.
229
246
  grpo_min_vram_gb=80,
@@ -255,7 +272,7 @@ MODELS: dict[str, ModelInfo] = {
255
272
  num_layers=40,
256
273
  hidden_size=2048,
257
274
  vocab_size=248_320,
258
- algos=("sft", "grpo"),
275
+ algos=("sft", "grpo", "opd"),
259
276
  min_vram_gb=141,
260
277
  # Floor to 100 GB so SFT lands on H200, not the thin-margin consumer Blackwell or 80 GB H100.
261
278
  sft_min_vram_gb=100,
@@ -403,8 +420,9 @@ def _resolve_open_model(model_id: str, algo: str, gpu: str | None) -> ModelInfo:
403
420
  def validate_model_for_algorithm(model_id: str, algorithm: str) -> ModelInfo:
404
421
  info = get_model(model_id)
405
422
  algo = normalize_algorithm(algorithm)
406
- required = "grpo" if algo == "grpo" else "sft"
407
- if required not in info.algos:
423
+ # Each algorithm gates on its own capability entry (sft/grpo/opd), so a model must
424
+ # explicitly opt into opd via its algos tuple — same contract as sft/grpo.
425
+ if algo not in info.algos:
408
426
  allowed = ", ".join(info.algos)
409
427
  raise ValueError(f"{model_id} supports {allowed}, not {algo}")
410
428
  return info
@@ -210,11 +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
- Both SFT and GRPO train off this file:
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) 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).
218
221
  """
219
222
 
220
223
  from __future__ import annotations
@@ -487,6 +490,33 @@ def cmd_env_setup(args) -> int:
487
490
  "# GPU and HF artifacts are managed automatically by the platform: the GPU is\n"
488
491
  "# the cheapest fitting managed class, and artifacts live in a private environment-scoped repo.\n"
489
492
  )
493
+ opd = Path("configs/opd.toml")
494
+ if not opd.exists():
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.
498
+ opd_multiturn_note = (
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"
501
+ if multi_turn
502
+ else ""
503
+ )
504
+ opd.write_text(
505
+ f"{opd_multiturn_note}"
506
+ 'model = "Qwen/Qwen3.5-4B"\n'
507
+ 'algorithm = "opd" # on-policy distillation from a Fireworks GLM teacher\n\n'
508
+ "# Environment: upload this project folder with\n"
509
+ "# `flash env push --name my-env .`, then paste the returned id below.\n"
510
+ "# The GLM teacher key is platform-managed — nothing to set up or export.\n"
511
+ "[environment]\n"
512
+ 'id = ""\n\n'
513
+ "[train]\n"
514
+ "steps = 100 # opd is step-driven (like GRPO)\n"
515
+ "lora_rank = 32\n"
516
+ '# teacher_model = "accounts/fireworks/models/glm-5p2" # Fireworks GLM teacher (default)\n'
517
+ "# GPU and HF artifacts are managed automatically by the platform: the GPU is\n"
518
+ "# the cheapest fitting managed class, and artifacts live in a private environment-scoped repo.\n"
519
+ )
490
520
  training = Path("TRAINING.md")
491
521
  if not training.exists():
492
522
  # Explicit UTF-8: TRAINING_MD has non-ASCII chars that raise UnicodeEncodeError under a non-UTF-8 locale.
@@ -496,6 +526,7 @@ def cmd_env_setup(args) -> int:
496
526
  "dataset/train.jsonl",
497
527
  "configs/sft.toml",
498
528
  "configs/rl.toml",
529
+ "configs/opd.toml",
499
530
  "TRAINING.md",
500
531
  ]
501
532
  if render.styled():
@@ -655,6 +686,13 @@ def _log_follow_progress(status: dict | None, fallback_state: str) -> tuple[str,
655
686
  step = heartbeat.get("step")
656
687
  if step is not None:
657
688
  parts.append(f"step={step}")
689
+ # live heartbeat age so a long quiet phase reads as "alive, throttled" not "frozen".
690
+ # minute granularity: the non-TTY follow path prints a line whenever this string changes,
691
+ # so a seconds-precision age would emit one line per poll.
692
+ ts = heartbeat.get("ts")
693
+ if isinstance(ts, (int, float)) and ts > 0:
694
+ mins = int(max(0.0, time.time() - ts) // 60)
695
+ parts.append(f"hb={mins}m" if mins else "hb=<1m")
658
696
  realized = status.get("realized_cost_usd")
659
697
  if realized is not None:
660
698
  if isinstance(realized, (int, float)):
@@ -800,13 +838,25 @@ def cmd_cancel(args) -> int:
800
838
  else:
801
839
  print(json.dumps(payload, indent=2))
802
840
  if checkpoints:
803
- 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
804
850
  # stderr in the plain path so the machine-readable stdout JSON stays untouched.
805
851
  out = sys.stdout if render.styled() else sys.stderr
806
- msg = (
852
+ base = (
807
853
  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}`."
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}."
810
860
  )
811
861
  print(render.note(msg) if render.styled() else msg, file=out)
812
862
  return 0
@@ -817,7 +867,7 @@ def cmd_checkpoints(args) -> int:
817
867
  if not checkpoints:
818
868
  message = (
819
869
  f"no deployable checkpoints for {args.run_id} yet "
820
- "(RL streams one per save interval; SFT-only runs have none)."
870
+ "(RL/opd stream one per save interval; SFT-only runs have none)."
821
871
  )
822
872
  if render.styled():
823
873
  print(render.empty("checkpoints", "0 deployable", message))
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  import sys
7
+ import time
7
8
 
8
9
  from flash._channel import CLI_NAME
9
10
 
@@ -520,6 +521,47 @@ def _humanize_ts(value) -> str | None:
520
521
  return datetime.datetime.fromtimestamp(value, datetime.UTC).strftime("%Y-%m-%d %H:%M UTC")
521
522
 
522
523
 
524
+ def _humanize_age(value) -> str | None:
525
+ """Format an epoch seconds value as a compact age ("3m ago"), None for non-numbers."""
526
+ if not isinstance(value, (int, float)) or value <= 0:
527
+ return None
528
+ secs = max(0.0, time.time() - value)
529
+ if secs < 90:
530
+ return f"{int(secs)}s ago"
531
+ if secs < 5400:
532
+ return f"{int(secs // 60)}m ago"
533
+ return f"{secs / 3600:.1f}h ago"
534
+
535
+
536
+ # heartbeat age past which the panel reminds that quiet is normal: worker uploads are throttled
537
+ # (240s quiet phases, up to 900s mid-training), so a frozen ts is usually NOT a dead worker.
538
+ _HB_QUIET_HINT_AFTER_S = 300.0
539
+
540
+
541
+ def _heartbeat_pairs(obj: dict) -> list[tuple[str, str]]:
542
+ """Worker heartbeat rows for the status panel: stage, step, age, and a quiet-is-normal hint."""
543
+ hb = obj.get("last_heartbeat")
544
+ if not isinstance(hb, dict) or not hb.get("stage"):
545
+ return []
546
+ worker = str(hb["stage"])
547
+ step = hb.get("step")
548
+ if step is not None:
549
+ worker += f" · step {step}"
550
+ if hb.get("liveness"):
551
+ worker += " · alive ping"
552
+ pairs = [("worker", worker)]
553
+ ts = hb.get("ts")
554
+ age = _humanize_age(ts)
555
+ if age:
556
+ running = str(obj.get("state") or "") == "running"
557
+ if running and isinstance(ts, (int, float)) and (time.time() - ts) > _HB_QUIET_HINT_AFTER_S:
558
+ age += _dim(
559
+ " (heartbeat uploads are throttled; quiet is not dead - check flash log -f <run-id>)"
560
+ )
561
+ pairs.append(("heartbeat", age))
562
+ return pairs
563
+
564
+
523
565
  def run_status(obj: dict) -> str:
524
566
  """A curated status panel for `flash status`, with the full JSON below for completeness."""
525
567
  spec = obj.get("spec") or {}
@@ -534,6 +576,7 @@ def run_status(obj: dict) -> str:
534
576
  realized = obj.get("realized_cost_usd")
535
577
  if realized is not None:
536
578
  pairs.append(("realized", money(realized)))
579
+ pairs += _heartbeat_pairs(obj)
537
580
  pairs += [
538
581
  ("created", _humanize_ts(obj.get("created_at"))),
539
582
  ("updated", _humanize_ts(obj.get("updated_at"))),
@@ -661,6 +704,16 @@ def cost_panel(est) -> str:
661
704
  ("wall clock", f"{est.wall_clock_hours:.2f} h"),
662
705
  ("billable", f"{est.billable_hours:.2f} h {_dim('(training only)')}"),
663
706
  ]
707
+ # opd teacher spend is itemized but NOT part of total_usd (billed by Fireworks on the user's key).
708
+ # Mirror CostEstimate.breakdown() so the styled panel doesn't silently drop it (cursor[bot]).
709
+ if getattr(est, "teacher_api_usd", 0) > 0:
710
+ pairs.append(
711
+ (
712
+ "teacher api",
713
+ f"{money(est.teacher_api_usd, 2)} "
714
+ f"{_dim('(Fireworks GLM token spend on your key — billed by Fireworks, NOT in TOTAL)')}",
715
+ )
716
+ )
664
717
  panel = _kv(pairs)
665
718
  total = f" {_paint('TOTAL'.ljust(10), _GRAY, '1')} {_paint(_glyph('·', '-'), _FAINT)} {_paint(f'${est.total_usd:.2f}', _TEAL, '1')}"
666
719
  out = f"{header('train', 'pre-flight cost estimate')}\n{panel}\n{_rule()}\n{total}"
@@ -19,8 +19,8 @@ actually produced, and decide — honestly — whether the model got better.
19
19
  ## Using Flash
20
20
 
21
21
  Flash is a **managed** training service with a thin CLI/client. You author an
22
- environment (the task + its reward), publish it, and submit SFT or GRPO runs from a
23
- TOML config. Flash allocates the cheapest fitting managed GPU class, runs the job,
22
+ environment (the task + its reward), publish it, and submit SFT, GRPO, or on-policy
23
+ **distillation** runs from a TOML config. Flash allocates the cheapest fitting managed GPU class, runs the job,
24
24
  streams logs back, and serves the result. You never handle infrastructure credentials —
25
25
  you authenticate once with a freesolo API key, and everything below is a `flash` CLI
26
26
  command.
@@ -42,6 +42,7 @@ environment.py # the task: how to prompt the model and how to score it
42
42
  dataset/train.jsonl # training rows, one JSON object per line: {"input": ..., "output": ...}
43
43
  configs/sft.toml # an SFT run config
44
44
  configs/rl.toml # a GRPO (RL) run config
45
+ configs/opd.toml # an on-policy distillation run config
45
46
  TRAINING.md # this file
46
47
  ```
47
48
 
@@ -92,7 +93,7 @@ edit to `environment.py` or `dataset/` so the managed run uses your change.
92
93
 
93
94
  ```toml
94
95
  model = "Qwen/Qwen3.5-4B" # see `flash models`
95
- algorithm = "sft" # "sft" (supervised) or "grpo" (RL)
96
+ algorithm = "sft" # "sft" (supervised), "grpo" (RL), or "opd" (on-policy distillation)
96
97
  # thinking = true # opt-in reasoning mode, for models that support it
97
98
 
98
99
  [environment]
@@ -373,6 +374,51 @@ SFT is **epoch-driven** (`epochs`); GRPO is **step-driven** (`steps`).
373
374
 
374
375
  ---
375
376
 
377
+ ## On-policy distillation (`algorithm = "opd"`)
378
+
379
+ Pick distillation when a much stronger **teacher** model can grade your student's work
380
+ token-by-token. The student samples on-policy (like GRPO), a Fireworks-hosted GLM teacher scores
381
+ each of *its own* completions, and a dense per-token loss teaches the student to match the teacher —
382
+ far more sample-efficient than reward-based RL and with no reward to design. It is **step-driven**
383
+ (`steps`) and produces a LoRA served exactly like SFT.
384
+
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.
389
+ - **The student (Qwen / MiniCPM / Kimi) and the teacher (GLM) have different tokenizers.** Flash
390
+ bridges the vocabulary mismatch with **groupwise reverse-KL** (the collinear-ai *spider* / Tinker
391
+ method): it aligns the two tokenizations by shared decoded-text spans and applies per-span reverse
392
+ KL using only realized-token logprobs — no vocabulary projection, so it covers every token exactly
393
+ and works for any student tokenizer. When the tokenizers happen to agree it reduces to plain
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).
401
+ - **Judge it like SFT.** Distillation logs a falling per-token loss; a low loss alone is not proof.
402
+ Keep a held-out split, `flash deploy` the adapter, and score it — confirm the student actually
403
+ moved toward the teacher's behavior, not just its surface tokens.
404
+
405
+ ```toml
406
+ # configs/opd.toml
407
+ model = "Qwen/Qwen3.5-4B"
408
+ algorithm = "opd"
409
+
410
+ [environment]
411
+ id = "your-org/my-env"
412
+
413
+ [train]
414
+ steps = 100
415
+ lora_rank = 32
416
+ # teacher_model = "accounts/fireworks/models/glm-5p2" # Fireworks GLM teacher (default)
417
+ # kl_penalty_coef = 1.0 # reverse-KL scale
418
+ ```
419
+
420
+ ---
421
+
376
422
  ## GRPO knobs that matter
377
423
 
378
424
  Set these in `[train]`. Each is `None` by default — the worker's tuned recipe fills
@@ -5,6 +5,13 @@ from __future__ import annotations
5
5
  import os
6
6
  from pathlib import Path
7
7
 
8
+ # The global runtime-secret allow-list: keys collected/gated/injected for EVERY run regardless of
9
+ # algorithm. Only WANDB_API_KEY belongs here (optional experiment logging, useful to all arms). The
10
+ # opd teacher key (FIREWORKS_API_KEY) is NOT global: opd auto-declares it as a required environment
11
+ # secret in spec_from_dict, and each gate unions the spec's declared secrets on top of this set
12
+ # (client collection below; server create gate server/_deps.py; worker-env injection
13
+ # providers/runpod/train/deps.py). Keeping it out of the default means SFT/GRPO workers never
14
+ # receive the teacher key they don't need.
8
15
  DEFAULT_RUNTIME_SECRET_KEYS = frozenset({"WANDB_API_KEY"})
9
16
 
10
17
 
@@ -20,6 +20,8 @@ from .facts import (
20
20
  model_quant,
21
21
  pick_gpu,
22
22
  reward_seconds_per_completion,
23
+ teacher_seconds_per_completion,
24
+ teacher_token_cost_usd,
23
25
  total_params_b,
24
26
  )
25
27
  from .types import CostEstimate, RunConfig
@@ -28,6 +30,9 @@ from .types import CostEstimate, RunConfig
28
30
  SFT_FLOPS_PER_TOKEN_PER_PARAM = 6.0 # forward (2) + backward (4)
29
31
  GRPO_GEN_FLOPS_PER_TOKEN_PER_PARAM = 2.0 # autoregressive rollout forward
30
32
  GRPO_UPDATE_FLOPS_PER_TOKEN_PER_PARAM = 8.0 # policy fwd+bwd (6) + frozen-ref fwd (2)
33
+ # OPD's update is policy fwd+bwd ONLY (6): the teacher's per-token logprobs come from the
34
+ # Fireworks API, so there is NO local frozen-reference forward (GRPO's extra 2).
35
+ OPD_UPDATE_FLOPS_PER_TOKEN_PER_PARAM = 6.0
31
36
 
32
37
  # Model-FLOPs utilization (fraction of peak sustained), calibrated against real RunPod
33
38
  # wall clock. LoRA + small batches sit well below dense-pretraining MFU.
@@ -39,6 +44,11 @@ MFU_DECODE = 0.12 # batched vLLM rollout (decode is memory-bandwidth-bound)
39
44
  # wall is ceil(completions / slots) waves x latency, not completions x latency.
40
45
  REWARD_CONCURRENCY = 16.0
41
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
+
42
52
  # Cold-start overhead (seconds): container boot + deps + model load (+ vLLM init for GRPO).
43
53
  #
44
54
  # Calibrated against a real fresh-worker run (0.8B SFT, RTX 3090 @ $0.239/hr) whose elapsed wall
@@ -77,6 +87,14 @@ def setup_seconds(config: RunConfig) -> float:
77
87
  return s
78
88
 
79
89
 
90
+ def _opd_step_shape(n: RunConfig) -> tuple[int, int]:
91
+ """(completions per step, prompt+completion tokens per step) for one OPD step, from a NORMALIZED
92
+ config. completions = batch x group; each is billed over the FULL n.seq_len (prompt+completion,
93
+ not completion-only) since the loss forward runs model(prompt_ids + student_ids)."""
94
+ completions = n.batch_size * n.group_size
95
+ return completions, completions * n.seq_len
96
+
97
+
80
98
  def seconds_per_step(config: RunConfig, gpu: str) -> float:
81
99
  """Steady-state wall time for one optimizer step on ``gpu``."""
82
100
  n = config.normalized()
@@ -85,6 +103,22 @@ def seconds_per_step(config: RunConfig, gpu: str) -> float:
85
103
  params = active_params_b(n.model_id) * 1e9
86
104
  peak = gpu_tflops(gpu) * 1e12 # FLOP/s
87
105
 
106
+ if n.is_opd:
107
+ # OPD step = on-policy student rollout (like GRPO) + remote teacher scoring (CONCURRENT
108
+ # Fireworks round-trips, replaces reward grading) + policy update (fwd+bwd only, NO local
109
+ # reference forward — the teacher is the API). Bill local compute on the FULL prompt+completion
110
+ # sequence (see _opd_step_shape), not completion-only, or long-prompt opd is underquoted.
111
+ completions, seq_tokens = _opd_step_shape(n)
112
+ gen_s = (GRPO_GEN_FLOPS_PER_TOKEN_PER_PARAM * params * seq_tokens) / (peak * MFU_DECODE)
113
+ update_s = (OPD_UPDATE_FLOPS_PER_TOKEN_PER_PARAM * params * seq_tokens) / (peak * MFU_TRAIN)
114
+ teacher_lat = teacher_seconds_per_completion()
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
120
+ return gen_s + teacher_s + update_s
121
+
88
122
  if not n.is_grpo:
89
123
  flops = SFT_FLOPS_PER_TOKEN_PER_PARAM * params * (n.batch_size * n.seq_len)
90
124
  return flops / (peak * MFU_SFT_TRAIN)
@@ -95,7 +129,9 @@ def seconds_per_step(config: RunConfig, gpu: str) -> float:
95
129
  gen_s = (GRPO_GEN_FLOPS_PER_TOKEN_PER_PARAM * params * gen_tokens) / (peak * MFU_DECODE)
96
130
  update_s = (GRPO_UPDATE_FLOPS_PER_TOKEN_PER_PARAM * params * gen_tokens) / (peak * MFU_TRAIN)
97
131
  latency = reward_seconds_per_completion(n.reward_seconds_per_completion)
98
- reward_s = math.ceil(completions / REWARD_CONCURRENCY) * latency # ceil: a partial wave still costs one latency
132
+ reward_s = (
133
+ math.ceil(completions / REWARD_CONCURRENCY) * latency
134
+ ) # ceil: a partial wave still costs one latency
99
135
  return gen_s + reward_s + update_s
100
136
 
101
137
  def sft_seconds_for_tokens(config: RunConfig, gpu: str, train_tokens: float) -> float:
@@ -114,7 +150,9 @@ def select_gpu(config: RunConfig, *, max_wall_seconds: float = 0.0) -> tuple[str
114
150
  it considers every fitting class, validated or not — so the estimate reflects the cheapest
115
151
  card that *could* run the job. The live allocator restricts to the validated pool, so the
116
152
  actually-provisioned class can be pricier than this. Catalog sizing is offline/deterministic."""
117
- total_params_b(config.model_id) # catalog-only: reject a non-catalog model before any (HF) sizing
153
+ total_params_b(
154
+ config.model_id
155
+ ) # catalog-only: reject a non-catalog model before any (HF) sizing
118
156
  need = required_vram_gb(
119
157
  config.model_id,
120
158
  config.method,
@@ -125,12 +163,22 @@ def select_gpu(config: RunConfig, *, max_wall_seconds: float = 0.0) -> tuple[str
125
163
  return gpu, need
126
164
 
127
165
 
128
- def _notes(config: RunConfig, raw_train_s: float, wall_capped: bool, cap_s: float) -> tuple[str, ...]:
166
+ def _notes(
167
+ config: RunConfig, raw_train_s: float, wall_capped: bool, cap_s: float
168
+ ) -> tuple[str, ...]:
129
169
  n = config.normalized()
130
170
  notes: list[str] = []
131
171
  if (quant := model_quant(n.model_id)) != "bf16":
132
172
  notes.append(f"{quant}: smaller VRAM footprint -> cheaper GPU class fits")
133
- if n.is_grpo:
173
+ if n.is_opd:
174
+ comps, _ = _opd_step_shape(n)
175
+ tsec = teacher_seconds_per_completion()
176
+ notes.append(
177
+ f"opd step = student rollout of {n.batch_size}x{n.group_size}={comps} completions "
178
+ f"@ {n.completion_len} tok + GLM teacher scoring ({tsec:.2f}s/completion) + policy "
179
+ "update (no local reference forward)"
180
+ )
181
+ elif n.is_grpo:
134
182
  comps = n.batch_size * n.group_size
135
183
  rsec = reward_seconds_per_completion(n.reward_seconds_per_completion)
136
184
  notes.append(
@@ -187,6 +235,18 @@ def estimate_cost(config: RunConfig, *, wall_cap_s: float = DEFAULT_WALL_CAP_S)
187
235
  train = max(0.0, cap_s - setup) if wall_capped else raw_train
188
236
  wall = setup + train
189
237
 
238
+ # OPD: add the external Fireworks teacher token spend. The teacher echo-scores every
239
+ # sampled completion (input ~ prompt+completion per completion), so bill INPUT tokens over the
240
+ # EFFECTIVE (wall-capped) step count — not the uncapped `steps` — so a wall-capped run's teacher
241
+ # bill tracks the GPU time it is actually billed for.
242
+ teacher_api_usd = 0.0
243
+ if config.is_opd:
244
+ n = config.normalized()
245
+ effective_steps = (train / sps) if sps > 0 else config.steps
246
+ _, tokens_per_step = _opd_step_shape(n)
247
+ teacher_input_tokens = effective_steps * tokens_per_step
248
+ teacher_api_usd = teacher_token_cost_usd(teacher_input_tokens, 0.0, config.teacher_model)
249
+
190
250
  return CostEstimate(
191
251
  model_id=config.model_id,
192
252
  method=config.method,
@@ -201,6 +261,11 @@ def estimate_cost(config: RunConfig, *, wall_cap_s: float = DEFAULT_WALL_CAP_S)
201
261
  train_seconds=train,
202
262
  wall_clock_seconds=wall,
203
263
  wall_capped=wall_capped,
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]).
204
268
  total_usd=train / 3600.0 * hourly,
269
+ teacher_api_usd=teacher_api_usd,
205
270
  notes=_notes(config, raw_train, wall_capped, cap_s),
206
271
  )
@@ -159,3 +159,46 @@ def reward_seconds_per_completion(override: float | None = None) -> float:
159
159
  if override is not None:
160
160
  return max(0.0, override)
161
161
  return AVG_REWARD_SECONDS_PER_COMPLETION
162
+
163
+
164
+ # Fireworks serverless pricing for the on-policy-distillation GLM teacher, $/1M tokens as
165
+ # (input, output). Static like gpu_hourly_usd so cost estimation is deterministic/offline and needs
166
+ # NO FIREWORKS_API_KEY. Source: https://fireworks.ai/models/fireworks/glm-5p2 lists
167
+ # $1.40 / $0.14 / $4.40 (input / cached input / output) per 1M. opd echo-scores completions
168
+ # (max_tokens=0), so only INPUT tokens are billed (the teacher never generates) — but the table keeps
169
+ # both so a mispriced entry is obvious. glm-5p1 shares the GLM-5 serverless rate.
170
+ TEACHER_USD_PER_1M: dict[str, tuple[float, float]] = {
171
+ "accounts/fireworks/models/glm-5p2": (1.40, 4.40),
172
+ "accounts/fireworks/models/glm-5p1": (1.40, 4.40),
173
+ }
174
+ # Fireworks echo-scoring round-trip per completion (wall time, concurrency-bound like reward grading).
175
+ AVG_TEACHER_SECONDS_PER_COMPLETION = 2.0
176
+
177
+
178
+ def teacher_price_per_1m(teacher_model: str) -> tuple[float, float]:
179
+ """(input, output) $/1M tokens for a teacher model.
180
+
181
+ The empty-string sentinel (an opd run that OMITS [train] teacher_model) resolves to the recipe's
182
+ default teacher (``RECIPE.opd.teacher_model``) — the SINGLE source of "who is the default
183
+ teacher" — so the quote uses that model's real rate with no forked default-price constant. An
184
+ unknown non-default teacher falls back to the default teacher's rate (spec-parse already rejects
185
+ an unpriced teacher_model, so this is only a defensive backstop)."""
186
+ from flash.engine.recipe import RECIPE
187
+
188
+ default = TEACHER_USD_PER_1M[RECIPE.opd.teacher_model]
189
+ return TEACHER_USD_PER_1M.get(teacher_model or RECIPE.opd.teacher_model, default)
190
+
191
+
192
+ def teacher_token_cost_usd(
193
+ input_tokens: float, output_tokens: float = 0.0, teacher_model: str = ""
194
+ ) -> float:
195
+ """External teacher-API dollar cost for a token count. Deterministic; no network."""
196
+ inp, outp = teacher_price_per_1m(teacher_model)
197
+ return (max(0.0, input_tokens) * inp + max(0.0, output_tokens) * outp) / 1_000_000.0
198
+
199
+
200
+ def teacher_seconds_per_completion(override: float | None = None) -> float:
201
+ """Per-completion teacher-scoring latency (s): the explicit override, else the average."""
202
+ if override is not None:
203
+ return max(0.0, override)
204
+ return AVG_TEACHER_SECONDS_PER_COMPLETION