freesolo-flash-dev 0.2.44__tar.gz → 0.2.45__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.45}/PKG-INFO +6 -4
  2. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/README.md +5 -3
  3. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/catalog.py +27 -9
  4. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/cli/commands.py +44 -2
  5. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/cli/render.py +53 -0
  6. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/cli/training_doc.py +43 -3
  7. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/client/runtime_secrets.py +7 -0
  8. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/cost/analytical.py +63 -4
  9. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/cost/facts.py +43 -0
  10. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/cost/spec.py +15 -3
  11. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/cost/types.py +37 -15
  12. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/accounting.py +5 -1
  13. freesolo_flash_dev-0.2.45/flash/engine/recipe.py +88 -0
  14. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/vram.py +83 -14
  15. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/__init__.py +47 -17
  16. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/adapter.py +7 -10
  17. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/finalize.py +18 -1
  18. freesolo_flash_dev-0.2.45/flash/engine/worker/heartbeat.py +357 -0
  19. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/hf.py +11 -7
  20. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/lora.py +114 -313
  21. freesolo_flash_dev-0.2.45/flash/engine/worker/opd.py +1083 -0
  22. freesolo_flash_dev-0.2.45/flash/engine/worker/opd_gkd.py +255 -0
  23. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/rl.py +109 -101
  24. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/sft.py +67 -43
  25. freesolo_flash_dev-0.2.45/flash/engine/worker/teacher.py +318 -0
  26. freesolo_flash_dev-0.2.45/flash/engine/worker/tokenizer_align.py +119 -0
  27. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/lora_rank.py +45 -21
  28. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/_poll.py +32 -6
  29. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/_worker.py +24 -10
  30. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/runner/__init__.py +4 -3
  31. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/schema/__init__.py +86 -14
  32. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/schema/fields.py +10 -0
  33. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/spec.py +3 -0
  34. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/pyproject.toml +2 -2
  35. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_algorithms.py +46 -1
  36. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_allocator.py +22 -0
  37. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_charge_pricing.py +12 -0
  38. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_cli_commands.py +61 -0
  39. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_cli_render_theme.py +49 -0
  40. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_flash_worker.py +25 -1
  41. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_lora_rank_preflight.py +35 -1
  42. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_metrics_schema_agent_contract.py +2 -0
  43. freesolo_flash_dev-0.2.45/tests/test_opd.py +2462 -0
  44. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_providers_symmetry.py +5 -1
  45. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_resume_on_retry.py +4 -0
  46. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_runmgmt.py +36 -0
  47. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_vl_warmstart_recombine.py +5 -3
  48. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_worker_hardexit.py +21 -0
  49. freesolo_flash_dev-0.2.45/tests/test_worker_init_heartbeat.py +842 -0
  50. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_worker_stack.py +12 -19
  51. freesolo_flash_dev-0.2.44/docs/cli-style/README.md +0 -33
  52. freesolo_flash_dev-0.2.44/docs/cli-style/env-setup-survey-dark.png +0 -0
  53. freesolo_flash_dev-0.2.44/docs/cli-style/env-setup-survey-light.png +0 -0
  54. freesolo_flash_dev-0.2.44/docs/cli-style/generate.py +0 -492
  55. freesolo_flash_dev-0.2.44/docs/cli-style/index.html +0 -343
  56. freesolo_flash_dev-0.2.44/docs/cli-style/preview.png +0 -0
  57. freesolo_flash_dev-0.2.44/docs/cli-style/themed-errors.png +0 -0
  58. freesolo_flash_dev-0.2.44/docs/cli-style/train-config-error-after.png +0 -0
  59. freesolo_flash_dev-0.2.44/docs/cli-style/train-config-error-before.png +0 -0
  60. freesolo_flash_dev-0.2.44/docs/kernel-cache.md +0 -100
  61. freesolo_flash_dev-0.2.44/flash/engine/recipe.py +0 -53
  62. freesolo_flash_dev-0.2.44/flash/engine/worker/heartbeat.py +0 -251
  63. freesolo_flash_dev-0.2.44/tests/test_vl_weight_sync.py +0 -205
  64. freesolo_flash_dev-0.2.44/tests/test_worker_init_heartbeat.py +0 -413
  65. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/.dockerignore +0 -0
  66. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/.env.example +0 -0
  67. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/.github/workflows/auto-rebake.yml +0 -0
  68. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/.github/workflows/bake-kernel-cache.yml +0 -0
  69. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/.github/workflows/ci.yml +0 -0
  70. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/.github/workflows/main-source-guard.yml +0 -0
  71. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/.github/workflows/notify-tests-repo.yml +0 -0
  72. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/.github/workflows/publish-dev.yml +0 -0
  73. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/.github/workflows/publish-image.yml +0 -0
  74. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/.github/workflows/publish.yml +0 -0
  75. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/.github/workflows/version-parity.yml +0 -0
  76. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/.github/workflows/worker-image.yml +0 -0
  77. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/.gitignore +0 -0
  78. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/Dockerfile +0 -0
  79. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/Dockerfile.worker +0 -0
  80. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/LICENSE +0 -0
  81. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/build/kernel_cache/.gitignore +0 -0
  82. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/build/kernel_cache/.keep +0 -0
  83. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/docker/Dockerfile.kernelcache +0 -0
  84. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/docker/Dockerfile.kernelcache.relayer +0 -0
  85. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/docker/bake_kernel_cache.py +0 -0
  86. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/docker/bake_pod_entry.py +0 -0
  87. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/docker/kernel_fingerprint.py +0 -0
  88. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/docker/make_rp_handler.py +0 -0
  89. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/__init__.py +0 -0
  90. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/_channel.py +0 -0
  91. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/_fileio.py +0 -0
  92. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/_logging.py +0 -0
  93. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/_update_check.py +0 -0
  94. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/adapter_artifacts.py +0 -0
  95. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/cli/__init__.py +0 -0
  96. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/cli/__main__.py +0 -0
  97. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/cli/_tty.py +0 -0
  98. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/cli/envpush.py +0 -0
  99. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/client/__init__.py +0 -0
  100. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/client/config.py +0 -0
  101. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/client/http.py +0 -0
  102. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/client/specs.py +0 -0
  103. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/cost/__init__.py +0 -0
  104. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/__init__.py +0 -0
  105. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/chalk_kernels.py +0 -0
  106. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/multiturn_rollout.py +0 -0
  107. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/__main__.py +0 -0
  108. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/_pkg.py +0 -0
  109. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/decoding.py +0 -0
  110. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/gpu_setup.py +0 -0
  111. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/grpo.py +0 -0
  112. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/kernel_warmup.py +0 -0
  113. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/packing.py +0 -0
  114. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/perf/__init__.py +0 -0
  115. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/perf/attn.py +0 -0
  116. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/perf/diagnostics.py +0 -0
  117. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/perf/lifecycle.py +0 -0
  118. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/perf/liger.py +0 -0
  119. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/perf/loraplus.py +0 -0
  120. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/perf/memory.py +0 -0
  121. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/engine/worker/wandb_log.py +0 -0
  122. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/envs/__init__.py +0 -0
  123. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/envs/adapter.py +0 -0
  124. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/envs/archive_policy.py +0 -0
  125. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/envs/base.py +0 -0
  126. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/envs/loader.py +0 -0
  127. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/envs/pull.py +0 -0
  128. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/envs/registry.py +0 -0
  129. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/__init__.py +0 -0
  130. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/_auth.py +0 -0
  131. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/_hf_artifacts.py +0 -0
  132. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/_hf_retry.py +0 -0
  133. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/_http.py +0 -0
  134. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/_instance.py +0 -0
  135. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/_instance_bootstrap.py +0 -0
  136. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/_instance_poll.py +0 -0
  137. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/_instance_provider.py +0 -0
  138. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/allocator.py +0 -0
  139. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/base.py +0 -0
  140. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/lambdalabs/__init__.py +0 -0
  141. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/lambdalabs/api.py +0 -0
  142. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/lambdalabs/auth.py +0 -0
  143. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/lambdalabs/gpus.py +0 -0
  144. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/lambdalabs/jobs/__init__.py +0 -0
  145. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/lambdalabs/jobs/builders.py +0 -0
  146. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/lambdalabs/preflight.py +0 -0
  147. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/lambdalabs/pricing.py +0 -0
  148. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/preflight.py +0 -0
  149. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/realized.py +0 -0
  150. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/runpod/__init__.py +0 -0
  151. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/runpod/api.py +0 -0
  152. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/runpod/auth.py +0 -0
  153. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/runpod/cost.py +0 -0
  154. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/runpod/gpus.py +0 -0
  155. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/runpod/jobs.py +0 -0
  156. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/runpod/keys.py +0 -0
  157. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/runpod/preflight.py +0 -0
  158. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/runpod/preload.py +0 -0
  159. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/runpod/pricing.py +0 -0
  160. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/runpod/slots.py +0 -0
  161. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/runpod/train/__init__.py +0 -0
  162. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/runpod/train/deps.py +0 -0
  163. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/runpod/train/endpoints.py +0 -0
  164. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/vast/__init__.py +0 -0
  165. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/vast/api.py +0 -0
  166. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/vast/auth.py +0 -0
  167. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/vast/jobs/__init__.py +0 -0
  168. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/vast/jobs/builders.py +0 -0
  169. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/vast/preflight.py +0 -0
  170. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/providers/vast/pricing.py +0 -0
  171. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/py.typed +0 -0
  172. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/runner/checkpoints.py +0 -0
  173. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/runner/deploy.py +0 -0
  174. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/runner/lifecycle.py +0 -0
  175. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/serve/__init__.py +0 -0
  176. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/serve/deploy.py +0 -0
  177. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/serve/export.py +0 -0
  178. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/serve/pricing.py +0 -0
  179. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/server/__init__.py +0 -0
  180. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/server/__main__.py +0 -0
  181. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/server/_deps.py +0 -0
  182. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/server/_internal_client.py +0 -0
  183. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/server/_locks.py +0 -0
  184. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/server/_runtime.py +0 -0
  185. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/server/app.py +0 -0
  186. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/server/auth.py +0 -0
  187. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/server/billing.py +0 -0
  188. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/server/billing_retry.py +0 -0
  189. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/server/checkpoints.py +0 -0
  190. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/server/db.py +0 -0
  191. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/server/environment_registry.py +0 -0
  192. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/server/envs.py +0 -0
  193. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/server/reconcile.py +0 -0
  194. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/server/routes/__init__.py +0 -0
  195. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/server/routes/envs.py +0 -0
  196. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/server/routes/meta.py +0 -0
  197. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/server/routes/runs.py +0 -0
  198. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/server/routes/serving.py +0 -0
  199. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/flash/server/run_registry.py +0 -0
  200. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/infisical-entrypoint.sh +0 -0
  201. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/scripts/build_dev_dist.py +0 -0
  202. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/__init__.py +0 -0
  203. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/_helpers/__init__.py +0 -0
  204. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/_helpers/runner.py +0 -0
  205. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/_helpers/specs.py +0 -0
  206. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/_helpers/vast.py +0 -0
  207. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/conftest.py +0 -0
  208. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/fixtures/math_eval.jsonl +0 -0
  209. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/fixtures/math_train.jsonl +0 -0
  210. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/live/__init__.py +0 -0
  211. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/live/conftest.py +0 -0
  212. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/live/test_lambda_live.py +0 -0
  213. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/live/test_runpod_live.py +0 -0
  214. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/live/test_vast_live.py +0 -0
  215. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_agent_flash_cli_contract.py +0 -0
  216. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_b200_rollout_opt.py +0 -0
  217. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_backend_jobspec_contract.py +0 -0
  218. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_billing_retry.py +0 -0
  219. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_cancel_remote.py +0 -0
  220. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_catalog_consistency.py +0 -0
  221. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_chalk_kernels.py +0 -0
  222. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_checkpoints.py +0 -0
  223. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_cli_errors.py +0 -0
  224. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_cli_estimate.py +0 -0
  225. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_cli_help.py +0 -0
  226. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_cli_managed.py +0 -0
  227. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_client.py +0 -0
  228. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_client_server_integration.py +0 -0
  229. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_config_overrides.py +0 -0
  230. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_cost_analytical.py +0 -0
  231. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_cost_equation.py +0 -0
  232. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_cost_estimate.py +0 -0
  233. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_cost_hardware.py +0 -0
  234. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_cost_models.py +0 -0
  235. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_cost_rewards.py +0 -0
  236. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_dev_channel.py +0 -0
  237. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_disk_gb.py +0 -0
  238. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_endpoint_name.py +0 -0
  239. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_env_cache_evict.py +0 -0
  240. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_env_delete.py +0 -0
  241. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_env_download.py +0 -0
  242. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_env_download_api.py +0 -0
  243. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_env_no_install.py +0 -0
  244. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_env_publish.py +0 -0
  245. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_env_pull.py +0 -0
  246. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_env_pull_managed_control_plane.py +0 -0
  247. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_env_push.py +0 -0
  248. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_env_rate_limit_resolve.py +0 -0
  249. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_envs_coverage.py +0 -0
  250. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_export.py +0 -0
  251. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_flash_mvp.py +0 -0
  252. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_flashinfer_cache_dirs.py +0 -0
  253. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_github_urlopen_retry.py +0 -0
  254. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_gpus.py +0 -0
  255. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_grpo_mask_aware.py +0 -0
  256. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_grpo_params.py +0 -0
  257. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_grpo_sleep_gate.py +0 -0
  258. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_health_liveness.py +0 -0
  259. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_idle_endpoint_reaper.py +0 -0
  260. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_internal_client.py +0 -0
  261. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_jobs.py +0 -0
  262. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_kernel_cache.py +0 -0
  263. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_kernel_fingerprint.py +0 -0
  264. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_kv_util.py +0 -0
  265. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_lambda_runner.py +0 -0
  266. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_logging.py +0 -0
  267. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_login_perms.py +0 -0
  268. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_managed_hf_repo.py +0 -0
  269. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_mig_guard.py +0 -0
  270. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_multiturn_rollout.py +0 -0
  271. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_oom_escalate_gpu.py +0 -0
  272. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_open_model_policy.py +0 -0
  273. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_orchestrator_flash.py +0 -0
  274. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_packing.py +0 -0
  275. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_poll_helpers.py +0 -0
  276. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_preflight.py +0 -0
  277. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_provider_routing.py +0 -0
  278. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_provider_teardown_robustness.py +0 -0
  279. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_realized_cost.py +0 -0
  280. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_resolve_params_b.py +0 -0
  281. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_runpod_api_delete.py +0 -0
  282. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_runpod_key_fingerprint.py +0 -0
  283. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_runpod_key_waterfall.py +0 -0
  284. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_runpod_slots.py +0 -0
  285. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_serve.py +0 -0
  286. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_server_api.py +0 -0
  287. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_server_billing.py +0 -0
  288. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_server_db.py +0 -0
  289. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_serving_contract.py +0 -0
  290. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_sft_example_selection.py +0 -0
  291. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_sft_gc_off.py +0 -0
  292. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_spec_and_validation.py +0 -0
  293. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_thinking_config.py +0 -0
  294. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_train_context_preflight.py +0 -0
  295. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_update_check.py +0 -0
  296. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_vast_api.py +0 -0
  297. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_vast_offers.py +0 -0
  298. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_vast_runner.py +0 -0
  299. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_verifiers.py +0 -0
  300. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_version.py +0 -0
  301. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_vl_warmstart_adapter_keys.py +0 -0
  302. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_wandb_naming.py +0 -0
  303. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_warmstart_cross_repo.py +0 -0
  304. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_weight_cache.py +0 -0
  305. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_worker_dryrun.py +0 -0
  306. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_worker_image.py +0 -0
  307. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/tests/test_worker_thinking.py +0 -0
  308. {freesolo_flash_dev-0.2.44 → freesolo_flash_dev-0.2.45}/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.45
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
+ SFT and GRPO train off this file; OPD (configs/opd.toml) does NOT support multi-turn:
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
221
  """
219
222
 
220
223
  from __future__ import annotations
@@ -487,6 +490,37 @@ 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) 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.
499
+ 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"
504
+ if multi_turn
505
+ else ""
506
+ )
507
+ opd.write_text(
508
+ f"{opd_multiturn_note}"
509
+ 'model = "Qwen/Qwen3.5-4B"\n'
510
+ 'algorithm = "opd" # on-policy distillation from a Fireworks GLM teacher (single-turn only)\n\n'
511
+ "# Environment: upload this project folder with\n"
512
+ "# `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"
514
+ "[environment]\n"
515
+ 'id = ""\n'
516
+ 'secrets = ["FIREWORKS_API_KEY"]\n\n'
517
+ "[train]\n"
518
+ "steps = 100 # opd is step-driven (like GRPO)\n"
519
+ "lora_rank = 32\n"
520
+ '# teacher_model = "accounts/fireworks/models/glm-5p2" # Fireworks GLM teacher (default)\n'
521
+ "# GPU and HF artifacts are managed automatically by the platform: the GPU is\n"
522
+ "# the cheapest fitting managed class, and artifacts live in a private environment-scoped repo.\n"
523
+ )
490
524
  training = Path("TRAINING.md")
491
525
  if not training.exists():
492
526
  # Explicit UTF-8: TRAINING_MD has non-ASCII chars that raise UnicodeEncodeError under a non-UTF-8 locale.
@@ -496,6 +530,7 @@ def cmd_env_setup(args) -> int:
496
530
  "dataset/train.jsonl",
497
531
  "configs/sft.toml",
498
532
  "configs/rl.toml",
533
+ "configs/opd.toml",
499
534
  "TRAINING.md",
500
535
  ]
501
536
  if render.styled():
@@ -655,6 +690,13 @@ def _log_follow_progress(status: dict | None, fallback_state: str) -> tuple[str,
655
690
  step = heartbeat.get("step")
656
691
  if step is not None:
657
692
  parts.append(f"step={step}")
693
+ # live heartbeat age so a long quiet phase reads as "alive, throttled" not "frozen".
694
+ # minute granularity: the non-TTY follow path prints a line whenever this string changes,
695
+ # so a seconds-precision age would emit one line per poll.
696
+ ts = heartbeat.get("ts")
697
+ if isinstance(ts, (int, float)) and ts > 0:
698
+ mins = int(max(0.0, time.time() - ts) // 60)
699
+ parts.append(f"hb={mins}m" if mins else "hb=<1m")
658
700
  realized = status.get("realized_cost_usd")
659
701
  if realized is not None:
660
702
  if isinstance(realized, (int, float)):
@@ -817,7 +859,7 @@ def cmd_checkpoints(args) -> int:
817
859
  if not checkpoints:
818
860
  message = (
819
861
  f"no deployable checkpoints for {args.run_id} yet "
820
- "(RL streams one per save interval; SFT-only runs have none)."
862
+ "(RL/opd stream one per save interval; SFT-only runs have none)."
821
863
  )
822
864
  if render.styled():
823
865
  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,45 @@ 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
+ - **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.
388
+ - **The student (Qwen / MiniCPM / Kimi) and the teacher (GLM) have different tokenizers.** Flash
389
+ bridges the vocabulary mismatch with **groupwise reverse-KL** (the collinear-ai *spider* / Tinker
390
+ method): it aligns the two tokenizations by shared decoded-text spans and applies per-span reverse
391
+ KL using only realized-token logprobs — no vocabulary projection, so it covers every token exactly
392
+ and works for any student tokenizer. When the tokenizers happen to agree it reduces to plain
393
+ per-token reverse KL (Thinking Machines, *On-Policy Distillation*). Nothing to configure.
394
+ - **Judge it like SFT.** Distillation logs a falling per-token loss; a low loss alone is not proof.
395
+ Keep a held-out split, `flash deploy` the adapter, and score it — confirm the student actually
396
+ moved toward the teacher's behavior, not just its surface tokens.
397
+
398
+ ```toml
399
+ # configs/opd.toml
400
+ model = "Qwen/Qwen3.5-4B"
401
+ algorithm = "opd"
402
+
403
+ [environment]
404
+ id = "your-org/my-env"
405
+ secrets = ["FIREWORKS_API_KEY"]
406
+
407
+ [train]
408
+ steps = 100
409
+ lora_rank = 32
410
+ # teacher_model = "accounts/fireworks/models/glm-5p2" # Fireworks GLM teacher (default)
411
+ # kl_penalty_coef = 1.0 # reverse-KL scale
412
+ ```
413
+
414
+ ---
415
+
376
416
  ## GRPO knobs that matter
377
417
 
378
418
  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.
@@ -77,6 +82,14 @@ def setup_seconds(config: RunConfig) -> float:
77
82
  return s
78
83
 
79
84
 
85
+ def _opd_step_shape(n: RunConfig) -> tuple[int, int]:
86
+ """(completions per step, prompt+completion tokens per step) for one OPD step, from a NORMALIZED
87
+ config. completions = batch x group; each is billed over the FULL n.seq_len (prompt+completion,
88
+ not completion-only) since the loss forward runs model(prompt_ids + student_ids)."""
89
+ completions = n.batch_size * n.group_size
90
+ return completions, completions * n.seq_len
91
+
92
+
80
93
  def seconds_per_step(config: RunConfig, gpu: str) -> float:
81
94
  """Steady-state wall time for one optimizer step on ``gpu``."""
82
95
  n = config.normalized()
@@ -85,6 +98,21 @@ def seconds_per_step(config: RunConfig, gpu: str) -> float:
85
98
  params = active_params_b(n.model_id) * 1e9
86
99
  peak = gpu_tflops(gpu) * 1e12 # FLOP/s
87
100
 
101
+ if n.is_opd:
102
+ # OPD step = on-policy student rollout (like GRPO) + remote teacher scoring (serial
103
+ # Fireworks round-trips, replaces reward grading) + policy update (fwd+bwd only, NO local
104
+ # reference forward — the teacher is the API). Bill local compute on the FULL prompt+completion
105
+ # sequence (see _opd_step_shape), not completion-only, or long-prompt opd is underquoted.
106
+ completions, seq_tokens = _opd_step_shape(n)
107
+ gen_s = (GRPO_GEN_FLOPS_PER_TOKEN_PER_PARAM * params * seq_tokens) / (peak * MFU_DECODE)
108
+ update_s = (OPD_UPDATE_FLOPS_PER_TOKEN_PER_PARAM * params * seq_tokens) / (peak * MFU_TRAIN)
109
+ 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
114
+ return gen_s + teacher_s + update_s
115
+
88
116
  if not n.is_grpo:
89
117
  flops = SFT_FLOPS_PER_TOKEN_PER_PARAM * params * (n.batch_size * n.seq_len)
90
118
  return flops / (peak * MFU_SFT_TRAIN)
@@ -95,7 +123,9 @@ def seconds_per_step(config: RunConfig, gpu: str) -> float:
95
123
  gen_s = (GRPO_GEN_FLOPS_PER_TOKEN_PER_PARAM * params * gen_tokens) / (peak * MFU_DECODE)
96
124
  update_s = (GRPO_UPDATE_FLOPS_PER_TOKEN_PER_PARAM * params * gen_tokens) / (peak * MFU_TRAIN)
97
125
  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
126
+ reward_s = (
127
+ math.ceil(completions / REWARD_CONCURRENCY) * latency
128
+ ) # ceil: a partial wave still costs one latency
99
129
  return gen_s + reward_s + update_s
100
130
 
101
131
  def sft_seconds_for_tokens(config: RunConfig, gpu: str, train_tokens: float) -> float:
@@ -114,7 +144,9 @@ def select_gpu(config: RunConfig, *, max_wall_seconds: float = 0.0) -> tuple[str
114
144
  it considers every fitting class, validated or not — so the estimate reflects the cheapest
115
145
  card that *could* run the job. The live allocator restricts to the validated pool, so the
116
146
  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
147
+ total_params_b(
148
+ config.model_id
149
+ ) # catalog-only: reject a non-catalog model before any (HF) sizing
118
150
  need = required_vram_gb(
119
151
  config.model_id,
120
152
  config.method,
@@ -125,12 +157,22 @@ def select_gpu(config: RunConfig, *, max_wall_seconds: float = 0.0) -> tuple[str
125
157
  return gpu, need
126
158
 
127
159
 
128
- def _notes(config: RunConfig, raw_train_s: float, wall_capped: bool, cap_s: float) -> tuple[str, ...]:
160
+ def _notes(
161
+ config: RunConfig, raw_train_s: float, wall_capped: bool, cap_s: float
162
+ ) -> tuple[str, ...]:
129
163
  n = config.normalized()
130
164
  notes: list[str] = []
131
165
  if (quant := model_quant(n.model_id)) != "bf16":
132
166
  notes.append(f"{quant}: smaller VRAM footprint -> cheaper GPU class fits")
133
- if n.is_grpo:
167
+ if n.is_opd:
168
+ comps, _ = _opd_step_shape(n)
169
+ tsec = teacher_seconds_per_completion()
170
+ notes.append(
171
+ f"opd step = student rollout of {n.batch_size}x{n.group_size}={comps} completions "
172
+ f"@ {n.completion_len} tok + GLM teacher scoring ({tsec:.2f}s/completion) + policy "
173
+ "update (no local reference forward)"
174
+ )
175
+ elif n.is_grpo:
134
176
  comps = n.batch_size * n.group_size
135
177
  rsec = reward_seconds_per_completion(n.reward_seconds_per_completion)
136
178
  notes.append(
@@ -187,6 +229,18 @@ def estimate_cost(config: RunConfig, *, wall_cap_s: float = DEFAULT_WALL_CAP_S)
187
229
  train = max(0.0, cap_s - setup) if wall_capped else raw_train
188
230
  wall = setup + train
189
231
 
232
+ # OPD: add the external Fireworks teacher token spend. The teacher echo-scores every
233
+ # sampled completion (input ~ prompt+completion per completion), so bill INPUT tokens over the
234
+ # EFFECTIVE (wall-capped) step count — not the uncapped `steps` — so a wall-capped run's teacher
235
+ # bill tracks the GPU time it is actually billed for.
236
+ teacher_api_usd = 0.0
237
+ if config.is_opd:
238
+ n = config.normalized()
239
+ effective_steps = (train / sps) if sps > 0 else config.steps
240
+ _, tokens_per_step = _opd_step_shape(n)
241
+ teacher_input_tokens = effective_steps * tokens_per_step
242
+ teacher_api_usd = teacher_token_cost_usd(teacher_input_tokens, 0.0, config.teacher_model)
243
+
190
244
  return CostEstimate(
191
245
  model_id=config.model_id,
192
246
  method=config.method,
@@ -201,6 +255,11 @@ def estimate_cost(config: RunConfig, *, wall_cap_s: float = DEFAULT_WALL_CAP_S)
201
255
  train_seconds=train,
202
256
  wall_clock_seconds=wall,
203
257
  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]).
204
262
  total_usd=train / 3600.0 * hourly,
263
+ teacher_api_usd=teacher_api_usd,
205
264
  notes=_notes(config, raw_train, wall_capped, cap_s),
206
265
  )
@@ -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
@@ -7,6 +7,7 @@ from __future__ import annotations
7
7
 
8
8
  import math
9
9
 
10
+ from flash.catalog import samples_on_policy
10
11
  from flash.cost.analytical import estimate_cost
11
12
  from flash.cost.types import CostEstimate, RunConfig
12
13
 
@@ -77,6 +78,12 @@ def spec_steps(spec) -> int:
77
78
  if t.steps is not None:
78
79
  return max(1, int(t.steps))
79
80
  return RECIPE.rl.num_steps
81
+ if spec.algorithm == "opd":
82
+ # Step-driven like GRPO (on-policy sampling), NOT example-driven — so a opd quote never
83
+ # demands [train].max_examples (the SFT fallback below would raise).
84
+ if t.steps is not None:
85
+ return max(1, int(t.steps))
86
+ return RECIPE.opd.num_steps
80
87
  # max_examples is a CAP; 0 (like None) means "no cap" (worker trains the full dataset), so
81
88
  # don't let max_examples=0 price a single step.
82
89
  return _sft_steps_from_examples(spec, _sft_example_count(spec), apply_cap=True)
@@ -87,20 +94,25 @@ def runconfig_from_spec(spec) -> RunConfig:
87
94
  estimate covers a single job. The estimate doesn't pin a GPU -- it does its own cheapest-fit
88
95
  (provider="auto")."""
89
96
  t, g = spec.train, spec.gpu
90
- is_grpo = spec.algorithm == "grpo"
97
+ # Both grpo and opd sample on-policy student completions, so both carry the rollout
98
+ # dimensions (completion length + group size) into the cost model.
99
+ has_rollout = samples_on_policy(spec.algorithm)
91
100
  return RunConfig(
92
101
  model_id=spec.model,
93
102
  method=spec.algorithm,
94
103
  steps=spec_steps(spec),
95
104
  seq_len=t.max_length,
96
- completion_len=t.max_tokens if is_grpo else None,
105
+ completion_len=t.max_tokens if has_rollout else None,
97
106
  batch_size=t.batch_size,
98
- group_size=t.group_size if is_grpo else None,
107
+ group_size=t.group_size if has_rollout else None,
99
108
  lora_rank=t.lora_rank,
100
109
  thinking=spec.thinking,
101
110
  provider="auto",
102
111
  max_wall_seconds=g.max_wall_seconds,
103
112
  environment=spec.environment.id or None,
113
+ # "" is the unset sentinel (RunConfig.teacher_model); facts.teacher_price_per_1m resolves it
114
+ # to the recipe default. Non-opd carries no teacher.
115
+ teacher_model=t.teacher_model if spec.algorithm == "opd" else "",
104
116
  )
105
117
 
106
118