freesolo-flash-dev 0.2.56__tar.gz → 0.2.57__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 (309) hide show
  1. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/PKG-INFO +1 -1
  2. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/recipe.py +11 -0
  3. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/opd.py +212 -25
  4. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/sft.py +5 -1
  5. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/schema/__init__.py +7 -0
  6. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/serve/deploy.py +42 -1
  7. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/routes/serving.py +4 -0
  8. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/spec.py +7 -0
  9. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/pyproject.toml +2 -2
  10. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_opd.py +265 -19
  11. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_serve.py +107 -0
  12. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_server_api.py +30 -0
  13. freesolo_flash_dev-0.2.57/tests/test_sft_max_context.py +60 -0
  14. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/uv.lock +7 -7
  15. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/.dockerignore +0 -0
  16. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/.env.example +0 -0
  17. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/.github/workflows/auto-rebake.yml +0 -0
  18. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/.github/workflows/bake-kernel-cache.yml +0 -0
  19. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/.github/workflows/ci.yml +0 -0
  20. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/.github/workflows/main-source-guard.yml +0 -0
  21. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/.github/workflows/notify-tests-repo.yml +0 -0
  22. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/.github/workflows/publish-dev.yml +0 -0
  23. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/.github/workflows/publish-image.yml +0 -0
  24. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/.github/workflows/publish.yml +0 -0
  25. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/.github/workflows/version-parity.yml +0 -0
  26. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/.github/workflows/worker-image.yml +0 -0
  27. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/.gitignore +0 -0
  28. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/Dockerfile +0 -0
  29. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/Dockerfile.worker +0 -0
  30. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/LICENSE +0 -0
  31. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/README.md +0 -0
  32. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/build/kernel_cache/.gitignore +0 -0
  33. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/build/kernel_cache/.keep +0 -0
  34. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/docker/Dockerfile.kernelcache +0 -0
  35. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/docker/Dockerfile.kernelcache.relayer +0 -0
  36. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/docker/bake_kernel_cache.py +0 -0
  37. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/docker/bake_pod_entry.py +0 -0
  38. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/docker/kernel_fingerprint.py +0 -0
  39. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/docker/make_rp_handler.py +0 -0
  40. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/__init__.py +0 -0
  41. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/_channel.py +0 -0
  42. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/_fileio.py +0 -0
  43. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/_logging.py +0 -0
  44. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/_update_check.py +0 -0
  45. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/adapter_artifacts.py +0 -0
  46. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/catalog.py +0 -0
  47. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/cli/__init__.py +0 -0
  48. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/cli/__main__.py +0 -0
  49. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/cli/_tty.py +0 -0
  50. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/cli/commands.py +0 -0
  51. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/cli/env_setup.py +0 -0
  52. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/cli/envpush.py +0 -0
  53. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/cli/render.py +0 -0
  54. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/cli/training_doc.py +0 -0
  55. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/client/__init__.py +0 -0
  56. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/client/config.py +0 -0
  57. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/client/http.py +0 -0
  58. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/client/runtime_secrets.py +0 -0
  59. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/client/specs.py +0 -0
  60. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/cost/__init__.py +0 -0
  61. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/cost/analytical.py +0 -0
  62. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/cost/facts.py +0 -0
  63. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/cost/spec.py +0 -0
  64. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/cost/types.py +0 -0
  65. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/__init__.py +0 -0
  66. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/accounting.py +0 -0
  67. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/chalk_kernels.py +0 -0
  68. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/multiturn_rollout.py +0 -0
  69. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/steps.py +0 -0
  70. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/structured_outputs.py +0 -0
  71. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/vram.py +0 -0
  72. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/__init__.py +0 -0
  73. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/__main__.py +0 -0
  74. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/_pkg.py +0 -0
  75. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/adapter.py +0 -0
  76. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/decoding.py +0 -0
  77. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/finalize.py +0 -0
  78. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/gpu_setup.py +0 -0
  79. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/grpo.py +0 -0
  80. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/heartbeat.py +0 -0
  81. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/hf.py +0 -0
  82. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/kernel_warmup.py +0 -0
  83. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/lora.py +0 -0
  84. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/opd_gkd.py +0 -0
  85. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/opd_vllm.py +0 -0
  86. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/packing.py +0 -0
  87. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/perf/__init__.py +0 -0
  88. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/perf/attn.py +0 -0
  89. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/perf/diagnostics.py +0 -0
  90. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/perf/lifecycle.py +0 -0
  91. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/perf/liger.py +0 -0
  92. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/perf/loraplus.py +0 -0
  93. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/perf/memory.py +0 -0
  94. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/rl.py +0 -0
  95. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/teacher.py +0 -0
  96. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/tokenizer_align.py +0 -0
  97. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/engine/worker/wandb_log.py +0 -0
  98. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/envs/__init__.py +0 -0
  99. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/envs/adapter.py +0 -0
  100. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/envs/archive_policy.py +0 -0
  101. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/envs/base.py +0 -0
  102. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/envs/loader.py +0 -0
  103. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/envs/pull.py +0 -0
  104. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/envs/registry.py +0 -0
  105. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/lora_rank.py +0 -0
  106. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/__init__.py +0 -0
  107. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/_auth.py +0 -0
  108. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/_hf_artifacts.py +0 -0
  109. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/_hf_retry.py +0 -0
  110. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/_http.py +0 -0
  111. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/_instance.py +0 -0
  112. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/_instance_bootstrap.py +0 -0
  113. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/_instance_poll.py +0 -0
  114. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/_instance_provider.py +0 -0
  115. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/_poll.py +0 -0
  116. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/_worker.py +0 -0
  117. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/allocator.py +0 -0
  118. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/base.py +0 -0
  119. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/lambdalabs/__init__.py +0 -0
  120. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/lambdalabs/api.py +0 -0
  121. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/lambdalabs/auth.py +0 -0
  122. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/lambdalabs/gpus.py +0 -0
  123. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/lambdalabs/jobs/__init__.py +0 -0
  124. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/lambdalabs/jobs/builders.py +0 -0
  125. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/lambdalabs/preflight.py +0 -0
  126. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/lambdalabs/pricing.py +0 -0
  127. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/preflight.py +0 -0
  128. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/realized.py +0 -0
  129. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/runpod/__init__.py +0 -0
  130. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/runpod/api.py +0 -0
  131. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/runpod/auth.py +0 -0
  132. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/runpod/cost.py +0 -0
  133. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/runpod/gpus.py +0 -0
  134. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/runpod/jobs.py +0 -0
  135. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/runpod/keys.py +0 -0
  136. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/runpod/preflight.py +0 -0
  137. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/runpod/preload.py +0 -0
  138. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/runpod/pricing.py +0 -0
  139. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/runpod/slots.py +0 -0
  140. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/runpod/train/__init__.py +0 -0
  141. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/runpod/train/deps.py +0 -0
  142. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/runpod/train/endpoints.py +0 -0
  143. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/vast/__init__.py +0 -0
  144. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/vast/api.py +0 -0
  145. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/vast/auth.py +0 -0
  146. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/vast/jobs/__init__.py +0 -0
  147. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/vast/jobs/builders.py +0 -0
  148. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/vast/preflight.py +0 -0
  149. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/providers/vast/pricing.py +0 -0
  150. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/py.typed +0 -0
  151. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/runner/__init__.py +0 -0
  152. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/runner/checkpoints.py +0 -0
  153. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/runner/deploy.py +0 -0
  154. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/runner/lifecycle.py +0 -0
  155. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/schema/fields.py +0 -0
  156. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/serve/__init__.py +0 -0
  157. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/serve/export.py +0 -0
  158. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/serve/pricing.py +0 -0
  159. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/__init__.py +0 -0
  160. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/__main__.py +0 -0
  161. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/_deps.py +0 -0
  162. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/_internal_client.py +0 -0
  163. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/_locks.py +0 -0
  164. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/_runtime.py +0 -0
  165. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/app.py +0 -0
  166. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/auth.py +0 -0
  167. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/billing.py +0 -0
  168. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/billing_retry.py +0 -0
  169. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/checkpoints.py +0 -0
  170. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/db.py +0 -0
  171. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/environment_registry.py +0 -0
  172. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/envs.py +0 -0
  173. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/reconcile.py +0 -0
  174. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/repo_cleanup.py +0 -0
  175. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/routes/__init__.py +0 -0
  176. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/routes/envs.py +0 -0
  177. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/routes/meta.py +0 -0
  178. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/routes/runs.py +0 -0
  179. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/flash/server/run_registry.py +0 -0
  180. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/infisical-entrypoint.sh +0 -0
  181. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/scripts/build_dev_dist.py +0 -0
  182. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/__init__.py +0 -0
  183. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/_helpers/__init__.py +0 -0
  184. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/_helpers/runner.py +0 -0
  185. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/_helpers/specs.py +0 -0
  186. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/_helpers/vast.py +0 -0
  187. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/conftest.py +0 -0
  188. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/fixtures/math_eval.jsonl +0 -0
  189. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/fixtures/math_train.jsonl +0 -0
  190. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/live/__init__.py +0 -0
  191. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/live/conftest.py +0 -0
  192. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/live/test_lambda_live.py +0 -0
  193. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/live/test_runpod_live.py +0 -0
  194. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/live/test_vast_live.py +0 -0
  195. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_agent_flash_cli_contract.py +0 -0
  196. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_algorithms.py +0 -0
  197. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_allocator.py +0 -0
  198. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_b200_rollout_opt.py +0 -0
  199. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_backend_jobspec_contract.py +0 -0
  200. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_billing_retry.py +0 -0
  201. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_cancel_remote.py +0 -0
  202. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_catalog_consistency.py +0 -0
  203. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_chalk_kernels.py +0 -0
  204. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_charge_pricing.py +0 -0
  205. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_checkpoints.py +0 -0
  206. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_cli_commands.py +0 -0
  207. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_cli_errors.py +0 -0
  208. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_cli_estimate.py +0 -0
  209. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_cli_help.py +0 -0
  210. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_cli_managed.py +0 -0
  211. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_cli_render_coverage.py +0 -0
  212. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_cli_render_theme.py +0 -0
  213. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_client.py +0 -0
  214. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_client_server_integration.py +0 -0
  215. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_config_overrides.py +0 -0
  216. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_cost_analytical.py +0 -0
  217. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_cost_equation.py +0 -0
  218. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_cost_estimate.py +0 -0
  219. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_cost_hardware.py +0 -0
  220. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_cost_models.py +0 -0
  221. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_cost_rewards.py +0 -0
  222. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_dev_channel.py +0 -0
  223. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_disk_gb.py +0 -0
  224. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_endpoint_name.py +0 -0
  225. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_env_cache_evict.py +0 -0
  226. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_env_delete.py +0 -0
  227. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_env_download.py +0 -0
  228. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_env_download_api.py +0 -0
  229. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_env_no_install.py +0 -0
  230. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_env_publish.py +0 -0
  231. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_env_pull.py +0 -0
  232. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_env_pull_loader_coverage.py +0 -0
  233. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_env_pull_managed_control_plane.py +0 -0
  234. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_env_push.py +0 -0
  235. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_env_rate_limit_resolve.py +0 -0
  236. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_envs_coverage.py +0 -0
  237. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_export.py +0 -0
  238. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_flash_mvp.py +0 -0
  239. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_flash_worker.py +0 -0
  240. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_flashinfer_cache_dirs.py +0 -0
  241. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_github_urlopen_retry.py +0 -0
  242. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_gpus.py +0 -0
  243. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_grpo_mask_aware.py +0 -0
  244. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_grpo_params.py +0 -0
  245. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_grpo_sleep_gate.py +0 -0
  246. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_health_liveness.py +0 -0
  247. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_idle_endpoint_reaper.py +0 -0
  248. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_instance_bootstrap_coverage.py +0 -0
  249. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_internal_client.py +0 -0
  250. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_jobs.py +0 -0
  251. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_kernel_cache.py +0 -0
  252. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_kernel_fingerprint.py +0 -0
  253. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_kv_util.py +0 -0
  254. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_lambda_api_coverage.py +0 -0
  255. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_lambda_runner.py +0 -0
  256. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_logging.py +0 -0
  257. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_login_perms.py +0 -0
  258. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_lora_rank_preflight.py +0 -0
  259. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_managed_hf_repo.py +0 -0
  260. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_metrics_schema_agent_contract.py +0 -0
  261. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_mig_guard.py +0 -0
  262. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_multiturn_rollout.py +0 -0
  263. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_multiturn_rollout_coverage.py +0 -0
  264. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_oom_escalate_gpu.py +0 -0
  265. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_opd_vllm.py +0 -0
  266. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_open_model_policy.py +0 -0
  267. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_orchestrator_flash.py +0 -0
  268. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_packing.py +0 -0
  269. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_poll_helpers.py +0 -0
  270. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_preflight.py +0 -0
  271. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_provider_routing.py +0 -0
  272. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_provider_teardown_robustness.py +0 -0
  273. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_providers_symmetry.py +0 -0
  274. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_realized_cost.py +0 -0
  275. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_repo_cleanup.py +0 -0
  276. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_resolve_params_b.py +0 -0
  277. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_resume_on_retry.py +0 -0
  278. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_runmgmt.py +0 -0
  279. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_runpod_api_delete.py +0 -0
  280. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_runpod_key_fingerprint.py +0 -0
  281. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_runpod_key_waterfall.py +0 -0
  282. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_runpod_slots.py +0 -0
  283. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_server_billing.py +0 -0
  284. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_server_db.py +0 -0
  285. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_server_envs_coverage.py +0 -0
  286. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_serving_contract.py +0 -0
  287. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_sft_example_selection.py +0 -0
  288. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_sft_gc_off.py +0 -0
  289. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_spec_and_validation.py +0 -0
  290. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_structured_outputs.py +0 -0
  291. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_thinking_config.py +0 -0
  292. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_train_context_preflight.py +0 -0
  293. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_update_check.py +0 -0
  294. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_vast_api.py +0 -0
  295. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_vast_api_coverage.py +0 -0
  296. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_vast_offers.py +0 -0
  297. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_vast_runner.py +0 -0
  298. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_verifiers.py +0 -0
  299. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_version.py +0 -0
  300. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_vl_warmstart_adapter_keys.py +0 -0
  301. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_wandb_naming.py +0 -0
  302. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_warmstart_cross_repo.py +0 -0
  303. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_weight_cache.py +0 -0
  304. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_worker_dryrun.py +0 -0
  305. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_worker_hardexit.py +0 -0
  306. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_worker_image.py +0 -0
  307. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_worker_init_heartbeat.py +0 -0
  308. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_worker_stack.py +0 -0
  309. {freesolo_flash_dev-0.2.56 → freesolo_flash_dev-0.2.57}/tests/test_worker_thinking.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: freesolo-flash-dev
3
- Version: 0.2.56
3
+ Version: 0.2.57
4
4
  Summary: Flash — managed LoRA post-training (SFT/GRPO/OPD) 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
@@ -169,6 +169,17 @@ class OPDConfig:
169
169
  # that pushes up log P(eos) at the position a rollout naturally terminated, restoring the stop
170
170
  # signal the alignment cannot express. 0 disables it; overridable via [train].opd_eos_loss_coef.
171
171
  eos_loss_coef: float = 0.5
172
+ # student-side entropy floor: a hinge on the per-sample MEAN completion entropy (nats), added to
173
+ # the reverse-KL loss. reverse-KL is mode-seeking and progressively over-sharpens small students
174
+ # (<=2b) until greedy (temp=0) serving falls into a repetition loop that never emits EOS — the
175
+ # teacher-gated fix (EOPD, arxiv 2603.07079) is unimplementable here (Fireworks echo-scoring
176
+ # returns no teacher distribution, and teacher/student are cross-tokenizer), so the STUDENT's own
177
+ # distribution — local, same-vocab, already computed — is regularized instead. the hinge is
178
+ # self-limiting: zero loss and zero gradient while mean entropy sits at/above the floor, so runs
179
+ # that don't collapse pay nothing. 0 disables the term (mean entropy is still logged);
180
+ # overridable via [train].opd_entropy_floor_coef / [train].opd_entropy_floor.
181
+ entropy_floor_coef: float = 0.0
182
+ entropy_floor: float = 0.5
172
183
 
173
184
 
174
185
  @dataclass(frozen=True)
@@ -158,6 +158,12 @@ class OpdKnobs:
158
158
  # over shared decoded-text spans cannot supervise the zero-width stop token, so this restores the
159
159
  # stop signal; 0 disables it. [train].opd_eos_loss_coef, else RECIPE.opd.eos_loss_coef.
160
160
  eos_loss_coef: float = RECIPE.opd.eos_loss_coef
161
+ # student-side entropy floor (see _entropy_floor_term): hinge weight and target (nats) on the
162
+ # per-sample mean completion entropy, countering reverse-KL over-sharpening on small students.
163
+ # coef 0 disables the loss term; the mean entropy is still computed (detached) for logging.
164
+ # [train].opd_entropy_floor_coef / opd_entropy_floor, else RECIPE.opd defaults.
165
+ entropy_floor_coef: float = RECIPE.opd.entropy_floor_coef
166
+ entropy_floor: float = RECIPE.opd.entropy_floor
161
167
  save_every: int = 0
162
168
  max_length: int = 0
163
169
  # Student on-policy sampling stops at these delimiters (parity with GRPO), so the teacher never
@@ -224,6 +230,19 @@ def _resolve_opd_knobs() -> OpdKnobs:
224
230
  _eos if (_eos := opt("opd_eos_loss_coef", None)) is not None else d.eos_loss_coef
225
231
  ),
226
232
  ),
233
+ # like eos_loss_coef: explicit 0 is valid (disable), only None falls back to the recipe.
234
+ entropy_floor_coef=max(
235
+ 0.0,
236
+ float(
237
+ _ec
238
+ if (_ec := opt("opd_entropy_floor_coef", None)) is not None
239
+ else d.entropy_floor_coef
240
+ ),
241
+ ),
242
+ entropy_floor=max(
243
+ 0.0,
244
+ float(_ef if (_ef := opt("opd_entropy_floor", None)) is not None else d.entropy_floor),
245
+ ),
227
246
  save_every=int(opt("save_every", 0) or 20),
228
247
  max_length=int(opt("max_context_tokens", 0) or 0),
229
248
  stop_sequences=tuple(getattr(t, "stop_sequences", ()) or ()),
@@ -315,7 +334,9 @@ def _student_model(model_id, model_init_kwargs, device):
315
334
 
316
335
  # init_model is the base id (fresh run). VL runs load the full multimodal model so all-linear LoRA
317
336
  # sees every target; non-VL runs keep the lighter causal-LM loader.
318
- base = model_cls.from_pretrained(init_model, trust_remote_code=True, **model_init_kwargs).to(device)
337
+ base = model_cls.from_pretrained(init_model, trust_remote_code=True, **model_init_kwargs).to(
338
+ device
339
+ )
319
340
  return get_peft_model(base, init_peft), str(init_model)
320
341
 
321
342
 
@@ -369,7 +390,8 @@ def run_opd():
369
390
  f"[opd] gkd (groupwise reverse-KL) teacher={knobs.teacher_model} "
370
391
  f"epochs={knobs.epochs} warm_start={warm_start or 'none'} "
371
392
  f"mode={'multi-turn' if multi_turn else 'single-turn'} "
372
- f"eos_loss_coef={knobs.eos_loss_coef}"
393
+ f"eos_loss_coef={knobs.eos_loss_coef} "
394
+ f"entropy_floor_coef={knobs.entropy_floor_coef} entropy_floor={knobs.entropy_floor}"
373
395
  )
374
396
 
375
397
  # The GLM teacher key is a platform-owned credential the control plane injects into the worker
@@ -621,12 +643,24 @@ def run_opd():
621
643
  # EMA of the per-rollout truncation rate — the runaway signal that scales terminal-EOS reinforcement
622
644
  # (see _runaway_eos_scale). Starts at 0 (assume the student stops fine); rises if rollouts run away.
623
645
  trunc_ema = 0.0
646
+ # EMA of the per-rollout EMPTY-completion rate — the OVER-correction signal (see _overcorrection_damp).
647
+ # Terminal-EOS reinforcement raises the SHARED eos logit, so pushing it too hard makes the student
648
+ # emit eos FIRST (empty completion -> empty serve). trunc_ema can't see this: an empty rollout is
649
+ # truncated=False, so it reads as "terminated fine" and keeps the coef pinned high; empty_ema damps
650
+ # the coef back down as empties appear, closing the feedback loop.
651
+ empty_ema = 0.0
624
652
  granularity_sum = 0.0
625
653
  granularity_n = 0
626
654
  # Running mean of the reinforced terminal log P(eos): should RISE over a run as the student learns
627
655
  # to stop, the counterpart to truncated_rollouts FALLING. Surfaced in train_meta.
628
656
  eos_logprob_sum = 0.0
629
657
  eos_logprob_n = 0
658
+ # Running mean of the per-sample student completion entropy (nats) and how many distilled samples
659
+ # tripped the entropy-floor hinge — the in-band view of reverse-KL entropy collapse (see
660
+ # _entropy_floor_term). Surfaced in train_meta; the per-step mean goes to wandb/heartbeats.
661
+ entropy_sum = 0.0
662
+ entropy_n = 0
663
+ entropy_floor_active = 0
630
664
  opt_steps = (
631
665
  0 # optimizer steps actually applied (< steps if any iteration had no teacher signal)
632
666
  )
@@ -688,9 +722,16 @@ def run_opd():
688
722
  sample (scaled 1/accum_target for gradient accumulation), advance the step aggregates, and
689
723
  refresh the stall clock. Called after vLLM generation and teacher scoring; ``samples_seen`` is
690
724
  advanced by the caller once per generated rollout so its timing is unchanged."""
691
- nonlocal teacher_ok, teacher_transient, teacher_error, truncated_rollouts, step_loss, step_cov
725
+ nonlocal \
726
+ teacher_ok, \
727
+ teacher_transient, \
728
+ teacher_error, \
729
+ truncated_rollouts, \
730
+ step_loss, \
731
+ step_cov
692
732
  nonlocal granularity_sum, granularity_n, generated_tokens, teacher_input_tokens, nseq
693
- nonlocal eos_logprob_sum, eos_logprob_n, trunc_ema
733
+ nonlocal eos_logprob_sum, eos_logprob_n, trunc_ema, empty_ema
734
+ nonlocal entropy_sum, entropy_n, entropy_floor_active, step_ent_sum, step_ent_n
694
735
  if r.teacher_status == "ok":
695
736
  teacher_ok += 1
696
737
  elif r.teacher_status == "transient":
@@ -702,6 +743,10 @@ def run_opd():
702
743
  # Update the runaway EMA once per accounted rollout (truncated or not) so terminal-EOS
703
744
  # reinforcement tracks the student's live termination health (see _runaway_eos_scale).
704
745
  trunc_ema += (1.0 - _EOS_TRUNC_EMA_DECAY) * ((1.0 if r.truncated else 0.0) - trunc_ema)
746
+ # An empty completion terminates (truncated=False), so trunc_ema reads it as healthy; track it
747
+ # separately so the controller can back OFF when its own EOS push starts collapsing rollouts.
748
+ _is_empty = r.loss is None and _sample_skip_reason(r) == "empty_completion"
749
+ empty_ema += (1.0 - _EOS_TRUNC_EMA_DECAY) * ((1.0 if _is_empty else 0.0) - empty_ema)
705
750
  if r.loss is None:
706
751
  reason = _sample_skip_reason(r)
707
752
  skip_counts[reason] += 1
@@ -721,6 +766,13 @@ def run_opd():
721
766
  if r.eos_logprob is not None:
722
767
  eos_logprob_sum += r.eos_logprob
723
768
  eos_logprob_n += 1
769
+ if r.entropy is not None:
770
+ entropy_sum += r.entropy
771
+ entropy_n += 1
772
+ step_ent_sum += r.entropy
773
+ step_ent_n += 1
774
+ if knobs.entropy_floor_coef > 0 and r.entropy < knobs.entropy_floor:
775
+ entropy_floor_active += 1
724
776
  nseq += 1
725
777
  # Non-liveness progress ping WITHIN the step (rationale in _opd_progress).
726
778
  _opd_progress(opt_steps, nseq)
@@ -747,6 +799,7 @@ def run_opd():
747
799
  max_teacher_workers = _opd_teacher_workers(
748
800
  knobs.prompts_per_step * knobs.group_size, teacher_batch_size
749
801
  )
802
+
750
803
  # fields= carries opt_steps on the liveness thread's opd_step pings: opd_step is upload-throttled,
751
804
  # so a stepless liveness ping could win the slot and overwrite the main thread's stepped heartbeat,
752
805
  # leaving actual_steps_run to floor a cancelled run to 1 step (codex[bot]).
@@ -770,10 +823,15 @@ def run_opd():
770
823
  for no_signal_attempt in range(1, max_no_signal_attempts + 1):
771
824
  it = step # data-slice + display index for THIS rollout attempt
772
825
  step += 1 # advance up front so the nseq==0 retry path can't spin forever
773
- batch = [examples[(it * prompts_per_step + i) % len(examples)] for i in range(prompts_per_step)]
826
+ batch = [
827
+ examples[(it * prompts_per_step + i) % len(examples)]
828
+ for i in range(prompts_per_step)
829
+ ]
774
830
  accum_target = max(1, prompts_per_step * group)
775
831
  step_loss = 0.0
776
832
  step_cov = 0.0
833
+ step_ent_sum = 0.0
834
+ step_ent_n = 0
777
835
  nseq = 0
778
836
  step_skip_counts: Counter[str] = Counter()
779
837
  # Step pipeline. vLLM produces on-policy rollouts, scorable completions are immediately
@@ -848,6 +906,7 @@ def run_opd():
848
906
  loss_microbatch_size,
849
907
  backward_scale=1.0 / _accum_target,
850
908
  runaway_rate=trunc_ema,
909
+ overcorr_rate=empty_ema,
851
910
  )
852
911
  opd_phase_seconds["loss_backward"] += time.perf_counter() - loss_started
853
912
  opd_phase_counts["loss_batches"] += 1
@@ -991,6 +1050,8 @@ def run_opd():
991
1050
  opd_phase_counts["vllm_syncs"] += 1
992
1051
  avg_loss = step_loss / nseq
993
1052
  avg_cov = step_cov / nseq
1053
+ # None when no distilled sample this step carried an entropy (bare test doubles).
1054
+ avg_ent = (step_ent_sum / step_ent_n) if step_ent_n else None
994
1055
  loss_curve.append(avg_loss)
995
1056
  coverage_curve.append(avg_cov)
996
1057
  _w.heartbeat(
@@ -1004,6 +1065,7 @@ def run_opd():
1004
1065
  step=opt_steps,
1005
1066
  loss=avg_loss,
1006
1067
  coverage=avg_cov,
1068
+ entropy=avg_ent,
1007
1069
  gpu=gpu_diagnostics(include_torch=False),
1008
1070
  force=True,
1009
1071
  )
@@ -1012,11 +1074,17 @@ def run_opd():
1012
1074
  with contextlib.suppress(Exception):
1013
1075
  import wandb
1014
1076
 
1015
- wandb.log({"opd/loss": avg_loss, "opd/coverage": avg_cov}, step=opt_steps)
1077
+ wandb_metrics = {"opd/loss": avg_loss, "opd/coverage": avg_cov}
1078
+ if avg_ent is not None:
1079
+ # falls over a collapsing run; should flatten at/above opd_entropy_floor
1080
+ # once the floor is enabled (see _entropy_floor_term).
1081
+ wandb_metrics["opd/entropy"] = avg_ent
1082
+ wandb.log(wandb_metrics, step=opt_steps)
1016
1083
  if it % 10 == 0:
1017
1084
  print(
1018
1085
  f"[opd] step {it + 1}/{steps} loss={avg_loss:.4f} "
1019
1086
  f"coverage={avg_cov:.0%} seqs={nseq}"
1087
+ + (f" entropy={avg_ent:.3f}" if avg_ent is not None else "")
1020
1088
  )
1021
1089
  # Checkpoint on OPTIMIZER-step count, not the loop index: a `step-N` artifact must
1022
1090
  # contain N real updates (skipped no-signal iterations don't advance opt_steps), else a
@@ -1122,6 +1190,17 @@ def run_opd():
1122
1190
  "eos_loss_coef": knobs.eos_loss_coef,
1123
1191
  "eos_reinforced_samples": eos_logprob_n,
1124
1192
  "mean_eos_logprob": (eos_logprob_sum / eos_logprob_n) if eos_logprob_n else None,
1193
+ # Final EMA of the empty-completion rate (_overcorrection_damp input). A non-trivial value
1194
+ # means the controller detected its own EOS push collapsing rollouts and damped the coef.
1195
+ "final_empty_rate_ema": round(empty_ema, 5),
1196
+ # Student entropy floor (see _entropy_floor_term): the knobs, the run-mean per-sample
1197
+ # completion entropy (nats), and how many distilled samples tripped the hinge. A
1198
+ # collapsing run shows mean_student_entropy FALLING step over step (opd/entropy in
1199
+ # wandb); with the floor enabled it should flatten at/above opd_entropy_floor.
1200
+ "entropy_floor_coef": knobs.entropy_floor_coef,
1201
+ "entropy_floor": knobs.entropy_floor,
1202
+ "mean_student_entropy": (entropy_sum / entropy_n) if entropy_n else None,
1203
+ "entropy_floor_active_samples": entropy_floor_active,
1125
1204
  "teacher_transient_failures": teacher_transient,
1126
1205
  "teacher_errors": teacher_error,
1127
1206
  "no_signal_resamples": no_signal_resamples,
@@ -1169,7 +1248,9 @@ def run_opd():
1169
1248
  else None
1170
1249
  ),
1171
1250
  "opd_rollout_pipeline_max_chunks": (
1172
- _opd_rollout_pipeline_max_chunks(prompts_per_step * group) if not multi_turn else None
1251
+ _opd_rollout_pipeline_max_chunks(prompts_per_step * group)
1252
+ if not multi_turn
1253
+ else None
1173
1254
  ),
1174
1255
  "opd_teacher_workers": max_teacher_workers,
1175
1256
  "opd_teacher_batch_size": teacher_batch_size,
@@ -1218,6 +1299,10 @@ class SampleResult:
1218
1299
  # Detached log P(eos) at the reinforced terminal position, when the EOS behaviour-cloning term
1219
1300
  # was applied (else None). Surfaced as mean_eos_logprob so a run's rising termination is visible.
1220
1301
  eos_logprob: float | None = None
1302
+ # Detached mean completion entropy (nats) of this sample's student predictive distributions
1303
+ # (distilled samples only, else None). Surfaced as opd/entropy + mean_student_entropy so the
1304
+ # reverse-KL entropy collapse behind the small-model greedy repetition loop is visible in-band.
1305
+ entropy: float | None = None
1221
1306
 
1222
1307
 
1223
1308
  @dataclass(frozen=True)
@@ -1422,7 +1507,9 @@ def _score_many(
1422
1507
  for _ in pendings
1423
1508
  ]
1424
1509
  return [_ScoreResult(teacher_toks=toks, status="ok") for toks in scored]
1425
- return [_ScoreResult(status="error", error="teacher batch attempts exhausted") for _ in pendings]
1510
+ return [
1511
+ _ScoreResult(status="error", error="teacher batch attempts exhausted") for _ in pendings
1512
+ ]
1426
1513
 
1427
1514
 
1428
1515
  @dataclass(frozen=True)
@@ -1508,12 +1595,10 @@ def _gkd_loss_from_logps(sp_t, groups, kl_coef=1.0):
1508
1595
  )
1509
1596
  student_group_logsum = sp_det.new_zeros(len(prepared.group_lengths))
1510
1597
  student_group_logsum.index_add_(0, group_ids_t, sp_det.index_select(0, flat_idx_t))
1511
- teacher_logsum_t = torch.tensor(
1512
- prepared.teacher_logsums, dtype=sp_t.dtype, device=sp_t.device
1598
+ teacher_logsum_t = torch.tensor(prepared.teacher_logsums, dtype=sp_t.dtype, device=sp_t.device)
1599
+ coeffs = (
1600
+ kl_coef * (student_group_logsum - teacher_logsum_t) / group_lengths_t.to(dtype=sp_t.dtype)
1513
1601
  )
1514
- coeffs = kl_coef * (
1515
- student_group_logsum - teacher_logsum_t
1516
- ) / group_lengths_t.to(dtype=sp_t.dtype)
1517
1602
  coeff_vec = coeffs.index_select(0, group_ids_t)
1518
1603
  sp_sel = sp_t.index_select(0, flat_idx_t)
1519
1604
  return (coeff_vec * sp_sel).mean()
@@ -1609,6 +1694,73 @@ def _eos_reinforce_term(
1609
1694
  return (-float(eos_coef)) * logp, float(logp.detach())
1610
1695
 
1611
1696
 
1697
+ # Row-chunk size for _entropy_floor_term's full-vocab passes: 128 rows x ~150k vocab x 3 fp32
1698
+ # temporaries ~= 250 MB transient, comfortably inside the existing dense-logits headroom.
1699
+ _ENTROPY_CHUNK_ROWS = 128
1700
+
1701
+
1702
+ def _entropy_floor_term(rows, coef: float, floor: float):
1703
+ """Student-side entropy floor: hinge the per-sample MEAN completion entropy (nats) at ``floor``.
1704
+
1705
+ Reverse-KL distillation is mode-seeking: on small students (<=2b) it progressively over-sharpens
1706
+ the next-token distribution over training steps until greedy (temperature=0) serving — the
1707
+ deploy default — falls into a repetition attractor and never emits EOS (observed on the 2b:
1708
+ step-20 checkpoint clean, step-63 loops on 80% of a 200-row eval; severity scales with
1709
+ rank x steps / model size). The principled fix gates the KL direction on the TEACHER's per-token
1710
+ entropy (EOPD, arxiv 2603.07079), but flash's teacher path cannot express it: Fireworks
1711
+ echo-scoring returns only realized-token scalars (no top-k, no entropy) and teacher/student are
1712
+ cross-tokenizer. So regularize the STUDENT's own distribution — local, same-vocab, already
1713
+ computed for the reverse-KL loss — to keep its entropy from collapsing.
1714
+
1715
+ The hinge is on the per-sample MEAN, not per token: individual tokens are often legitimately
1716
+ near-deterministic (syntax, format, ``</think>``), and a per-token floor would push every
1717
+ confident token up, fighting correct behaviour everywhere. The observed pathology is
1718
+ distribution-wide drift (mean entropy falls monotonically over OPD steps), which the mean hinge
1719
+ targets exactly. Like the terminal-EOS controller above, it is SELF-LIMITING: zero loss and zero
1720
+ gradient while the mean sits at/above the floor, so students that don't collapse (4b/9b, or the
1721
+ 2b's early steps) pay nothing.
1722
+
1723
+ ``rows`` is the same ``[completion_len, vocab]`` logits slice the reverse-KL loss consumes.
1724
+ Returns ``(term_or_none, mean_entropy)``: the differentiable addend
1725
+ ``coef * (floor - mean_entropy)`` when the hinge is active (else ``None``), plus the detached
1726
+ mean entropy in nats for logging (always computed, even with coef 0, so a run's entropy
1727
+ trajectory is visible in-band — serving exposes no logprobs to measure it after the fact).
1728
+
1729
+ MEMORY: entropy needs the full-vocab distribution, and an unchunked fp32 log_softmax + exp over
1730
+ ``[completion_len, vocab]`` would materialize multiple ~GB temporaries per sample — an
1731
+ unbudgeted addition to the dense-logits peak _opd_loss_microbatch_size was sized for, EVEN WITH
1732
+ THE FEATURE DISABLED (codex[bot]). Both passes therefore run in _ENTROPY_CHUNK_ROWS-row chunks,
1733
+ bounding the detached (logging) pass to a ~chunk x vocab transient. The AUTOGRAD pass still
1734
+ retains ~2 fp32 ``[completion_len, vocab]`` buffers for backward — but only for samples the
1735
+ hinge is actively penalizing (coef > 0 AND mean entropy below the floor), i.e. the opt-in
1736
+ experimental case, never the default path."""
1737
+ import torch
1738
+ import torch.nn.functional as F
1739
+
1740
+ if rows is None or rows.shape[0] == 0:
1741
+ return None, None
1742
+ n = rows.shape[0]
1743
+
1744
+ def _chunked_entropy_sum(grad: bool):
1745
+ total = None
1746
+ for i in range(0, n, _ENTROPY_CHUNK_ROWS):
1747
+ lp = F.log_softmax(rows[i : i + _ENTROPY_CHUNK_ROWS].float(), dim=-1)
1748
+ part = -(lp.exp() * lp).sum()
1749
+ if not grad:
1750
+ part = float(part)
1751
+ total = part if total is None else total + part
1752
+ return total
1753
+
1754
+ # detached pass first: cheap logging value, and the gate that decides whether the (autograd)
1755
+ # penalty pass is needed at all — at/above the floor the hinge is identically zero.
1756
+ with torch.no_grad():
1757
+ mean_entropy = _chunked_entropy_sum(grad=False) / n
1758
+ if coef <= 0 or mean_entropy >= floor:
1759
+ return None, mean_entropy
1760
+ ent = _chunked_entropy_sum(grad=True) / n
1761
+ return float(coef) * (float(floor) - ent), mean_entropy
1762
+
1763
+
1612
1764
  # Terminal-EOS reinforcement is a PROPORTIONAL CONTROLLER, not a constant push. #482 added the EOS
1613
1765
  # behaviour-cloning term on every distilled (cleanly-terminated) rollout unconditionally; because it
1614
1766
  # raises the SHARED eos output logit — which governs P(eos) at EVERY position, not just the terminal
@@ -1619,7 +1771,9 @@ def _eos_reinforce_term(
1619
1771
  # rollouts, then an all-empty collapse by step ~55/63. Fix: scale the coef by how much the student is
1620
1772
  # CURRENTLY failing to terminate (an EMA of the per-rollout truncation rate), so a run that stops
1621
1773
  # reliably gets ~zero EOS push and cannot ratchet, while genuine runaway still drives full reinforcement.
1622
- _EOS_RUNAWAY_LO = 0.02 # truncation rate <= this: student stops fine -> NO reinforcement (no ratchet)
1774
+ _EOS_RUNAWAY_LO = (
1775
+ 0.02 # truncation rate <= this: student stops fine -> NO reinforcement (no ratchet)
1776
+ )
1623
1777
  _EOS_RUNAWAY_HI = 0.15 # truncation rate >= this: full opd_eos_loss_coef (clear runaway to correct)
1624
1778
  _EOS_TRUNC_EMA_DECAY = 0.97 # per-rollout EMA (~30-rollout window): responsive yet smoothed
1625
1779
 
@@ -1636,6 +1790,30 @@ def _runaway_eos_scale(runaway_rate: float) -> float:
1636
1790
  return (runaway_rate - _EOS_RUNAWAY_LO) / (_EOS_RUNAWAY_HI - _EOS_RUNAWAY_LO)
1637
1791
 
1638
1792
 
1793
+ # _runaway_eos_scale ramps the coef UP on runaway, but on its own it CANNOT stop an empty-collapse when
1794
+ # runaway is extreme (base-model / cold start): truncation stays high the whole run so the coef is pinned
1795
+ # at full, and an empty completion is truncated=False so it reads as "terminated" and never lowers the
1796
+ # ramp. Result (measured on a base-model 35B-thinking run, coef 0.5): 0 runaway but 58% of SAMPLED serves
1797
+ # come back empty. So damp the coef by an EMA of the empty-completion rate — the direct symptom of the
1798
+ # over-correction — giving two-sided negative feedback: reinforce EOS when running away, but SHUT THE
1799
+ # PUSH OFF the moment the student starts emitting eos first. Empties are always a failure (never
1800
+ # task-length like a truncation), so damp aggressively.
1801
+ _EOS_OVERCORR_LO = 0.01 # empty-rate EMA <= this: no damping (full reinforcement per _runaway_eos_scale)
1802
+ _EOS_OVERCORR_HI = 0.05 # empty-rate EMA >= this: reinforcement fully suppressed (student over-stops)
1803
+
1804
+
1805
+ def _overcorrection_damp(empty_rate: float) -> float:
1806
+ """Fraction of the runaway-scaled EOS coef to KEEP given the CURRENT empty-completion rate (EMA).
1807
+ 1.0 while the student isn't collapsing to empty (rate <= _EOS_OVERCORR_LO); ramps linearly to 0.0 by
1808
+ _EOS_OVERCORR_HI so the terminal-EOS push shuts off before it ratchets P(eos | position 0) past the
1809
+ sampling threshold. Composes multiplicatively with _runaway_eos_scale."""
1810
+ if empty_rate <= _EOS_OVERCORR_LO:
1811
+ return 1.0
1812
+ if empty_rate >= _EOS_OVERCORR_HI:
1813
+ return 0.0
1814
+ return 1.0 - (empty_rate - _EOS_OVERCORR_LO) / (_EOS_OVERCORR_HI - _EOS_OVERCORR_LO)
1815
+
1816
+
1639
1817
  def _resolve_samples_batched(
1640
1818
  model,
1641
1819
  tok,
@@ -1646,6 +1824,7 @@ def _resolve_samples_batched(
1646
1824
  *,
1647
1825
  backward_scale: float | None = None,
1648
1826
  runaway_rate: float = 1.0,
1827
+ overcorr_rate: float = 0.0,
1649
1828
  ) -> list[SampleResult]:
1650
1829
  import torch
1651
1830
 
@@ -1657,11 +1836,16 @@ def _resolve_samples_batched(
1657
1836
  # don't change across the chunk. getattr defaults keep bare test stand-ins (SimpleNamespace knobs)
1658
1837
  # and eos-less fake tokenizers working with reinforcement OFF.
1659
1838
  eos_coef = float(getattr(knobs, "eos_loss_coef", 0.0) or 0.0)
1660
- # Controller: reinforce the terminal EOS only in proportion to the student's CURRENT failure to
1661
- # terminate (see _runaway_eos_scale). A cleanly-terminating run -> ~0 push -> the shared eos logit
1662
- # can't ratchet into an empty-collapse; genuine runaway still drives full reinforcement. Default
1663
- # runaway_rate=1.0 keeps bare callers (tests) at full strength = pre-controller behaviour.
1664
- eos_coef *= _runaway_eos_scale(runaway_rate)
1839
+ # Two-sided controller: reinforce the terminal EOS in proportion to the student's CURRENT failure to
1840
+ # terminate (_runaway_eos_scale), then DAMP by its current empty-completion rate (_overcorrection_damp)
1841
+ # so the push shuts off if it starts collapsing rollouts to empty. A cleanly-terminating run -> ~0
1842
+ # push; genuine runaway -> full push UNLESS that push is already producing empties. Defaults
1843
+ # runaway_rate=1.0 / overcorr_rate=0.0 keep bare callers (tests) at full strength = pre-controller.
1844
+ eos_coef *= _runaway_eos_scale(runaway_rate) * _overcorrection_damp(overcorr_rate)
1845
+ # student entropy floor (see _entropy_floor_term). getattr defaults keep bare test stand-ins
1846
+ # working with the penalty OFF; the detached mean entropy is computed either way for logging.
1847
+ ent_coef = float(getattr(knobs, "entropy_floor_coef", 0.0) or 0.0)
1848
+ ent_floor = float(getattr(knobs, "entropy_floor", 0.0) or 0.0)
1665
1849
  eos_stop_sequences = tuple(getattr(knobs, "stop_sequences", ()) or ())
1666
1850
  eos_ids = _generation_eos_ids(model, tok) if eos_coef > 0 else frozenset()
1667
1851
  eos_primary = _primary_eos_id(tok, eos_ids) if eos_coef > 0 else None
@@ -1726,13 +1910,9 @@ def _resolve_samples_batched(
1726
1910
  seqs = [list(p.prompt_ids) + list(p.student_ids) for p in chunk]
1727
1911
  max_len = max(len(seq) for seq in seqs)
1728
1912
  input_ids = torch.full((len(seqs), max_len), pad_id, dtype=torch.long, device=device)
1729
- attention_mask = torch.zeros(
1730
- (len(seqs), max_len), dtype=torch.long, device=device
1731
- )
1913
+ attention_mask = torch.zeros((len(seqs), max_len), dtype=torch.long, device=device)
1732
1914
  for row, seq in enumerate(seqs):
1733
- input_ids[row, : len(seq)] = torch.tensor(
1734
- seq, dtype=torch.long, device=device
1735
- )
1915
+ input_ids[row, : len(seq)] = torch.tensor(seq, dtype=torch.long, device=device)
1736
1916
  attention_mask[row, : len(seq)] = 1
1737
1917
  logits = _forward_logits(model, input_ids, attention_mask)
1738
1918
  losses = []
@@ -1771,6 +1951,12 @@ def _resolve_samples_batched(
1771
1951
  if eos_out is not None:
1772
1952
  eos_term, eos_logprob = eos_out
1773
1953
  loss = loss + eos_term
1954
+ # entropy floor on the SAME rows the reverse-KL consumed; added to distilled
1955
+ # samples only (loss is not None), so skip accounting is unchanged. the detached
1956
+ # mean entropy is logged even when the penalty is off/inactive.
1957
+ ent_term, entropy = _entropy_floor_term(rows, ent_coef, ent_floor)
1958
+ if ent_term is not None:
1959
+ loss = loss + ent_term
1774
1960
  loss_for_result = loss
1775
1961
  if backward_scale is not None:
1776
1962
  losses.append(loss)
@@ -1783,6 +1969,7 @@ def _resolve_samples_batched(
1783
1969
  teacher_tokens=p.teacher_tokens,
1784
1970
  group_granularity=p.group_granularity,
1785
1971
  eos_logprob=eos_logprob,
1972
+ entropy=entropy,
1786
1973
  )
1787
1974
  if losses:
1788
1975
  (sum(losses) * float(backward_scale)).backward()
@@ -196,8 +196,12 @@ def run_sft():
196
196
  from flash.engine.vram import sft_grad_accum
197
197
 
198
198
  sft_lr = _train_opt("learning_rate", RECIPE.sft.learning_rate)
199
+ # Total SFT sequence budget (prompt + completion), honoring [train] max_context_tokens like the
200
+ # cost/preflight path (flash.cost.spec._sft_seq_len); unset -> the tuned recipe cap. Read the
201
+ # RENAMED knob: reading a stale "max_length" here made getattr silently return the 1024 default.
199
202
  sft_max_len = _train_opt(
200
- "max_length", RECIPE.sft.max_seq_len_thinking if _w.THINKING else RECIPE.sft.max_seq_len
203
+ "max_context_tokens",
204
+ RECIPE.sft.max_seq_len_thinking if _w.THINKING else RECIPE.sft.max_seq_len,
201
205
  )
202
206
  with liveness_heartbeat("sft_pretokenizing"):
203
207
  texts, _pretok, _dropped = _pretokenize_completion_only(texts, tok, sft_max_len)
@@ -208,6 +208,8 @@ _TRAIN_KEYS = frozenset(
208
208
  "advantage_clip",
209
209
  "thinking_length_penalty_coef",
210
210
  "opd_eos_loss_coef",
211
+ "opd_entropy_floor_coef",
212
+ "opd_entropy_floor",
211
213
  "teacher_model",
212
214
  "stop_sequences",
213
215
  "structured_outputs",
@@ -216,6 +218,7 @@ _TRAIN_KEYS = frozenset(
216
218
  }
217
219
  )
218
220
 
221
+
219
222
  def spec_from_dict(raw: dict[str, Any], run_id: str | None = None) -> JobSpec:
220
223
  # Only reject table-valued unknowns — callers pass harmless scalar flags like dry_run alongside spec.
221
224
  unknown = sorted(k for k in set(raw) - _TOP_LEVEL_KEYS if isinstance(raw[k], dict))
@@ -356,6 +359,10 @@ def spec_from_dict(raw: dict[str, Any], run_id: str | None = None) -> JobSpec:
356
359
  ),
357
360
  # OPD-only terminal-EOS reinforcement weight; 0 disables. None -> recipe default (0.5).
358
361
  opd_eos_loss_coef=_train_float(train_raw, "opd_eos_loss_coef", minimum=0.0),
362
+ # OPD-only student entropy floor (hinge weight + target nats); coef 0 disables the term.
363
+ # None -> recipe defaults (0.0 / 0.5, i.e. off until validated).
364
+ opd_entropy_floor_coef=_train_float(train_raw, "opd_entropy_floor_coef", minimum=0.0),
365
+ opd_entropy_floor=_train_float(train_raw, "opd_entropy_floor", minimum=0.0),
359
366
  # OPD-only managed teacher alias, validated against the allow-list; "" -> default GLM 5.2.
360
367
  teacher_model=_train_teacher(train_raw),
361
368
  stop_sequences=_train_stops(train_raw),
@@ -11,6 +11,7 @@ from dataclasses import asdict, dataclass
11
11
  import httpx
12
12
 
13
13
  from flash._logging import get_logger
14
+ from flash.engine.structured_outputs import parse_structured_outputs
14
15
  from flash.lora_rank import rank_from_adapter_config
15
16
 
16
17
  logger = get_logger(__name__)
@@ -263,6 +264,34 @@ def adapter_artifact_lora_rank(hf_repo: str, subfolder: str) -> int:
263
264
  return rank_from_adapter_config(config, source=f"{hf_repo}:{filename}")
264
265
 
265
266
 
267
+ def _structured_outputs_body(
268
+ run_id: str, structured_outputs: str, *, thinking: bool
269
+ ) -> dict | None:
270
+ """The per-adapter structured-outputs serving default for the registration body, or None.
271
+
272
+ Returns the parsed canonical StructuredOutputsParams-kwargs dict when the run configured a
273
+ constraint AND is non-thinking; None otherwise (no default is registered). Non-thinking only:
274
+ serving sets no reasoning-parser, so a per-adapter grammar would bind from the very first
275
+ generated token and force a thinking adapter's ``<think>`` phase to open the schema (e.g. ``{``)
276
+ — strictly worse than serving it unconstrained. Training stays compatible with thinking via a
277
+ DEFERRED grammar (``reasoning_parser="deepseek_r1"`` holds the constraint until ``</think>``),
278
+ but serving has no such deferral, so we skip the serve-time default for thinking runs rather
279
+ than break reasoning. Raises ValueError on a corrupt spec — a wiring bug, since
280
+ ``[train].structured_outputs`` is already canonicalized at run creation (schema/fields.py), so
281
+ this cannot fire for a real run; it fails loudly rather than ever silently serving unconstrained."""
282
+ if not structured_outputs:
283
+ return None
284
+ if thinking:
285
+ logger.info(
286
+ "adapter %s trained with structured_outputs but thinking=True; not registering a "
287
+ "serving guided-decoding default — serving has no reasoning-parser deferral, so it "
288
+ "would bind the grammar from token 0 and suppress the <think> phase",
289
+ run_id,
290
+ )
291
+ return None
292
+ return parse_structured_outputs(structured_outputs)
293
+
294
+
266
295
  def deploy_adapter(
267
296
  run_id: str,
268
297
  model: str,
@@ -272,9 +301,18 @@ def deploy_adapter(
272
301
  dry_run: bool = False,
273
302
  lora_rank: int = 32,
274
303
  thinking: bool = False,
304
+ structured_outputs: str = "",
275
305
  org_id: str | None = None,
276
306
  ) -> Deployment:
277
- """Register the trained adapter with the freesolo serving app."""
307
+ """Register the trained adapter with the freesolo serving app.
308
+
309
+ ``structured_outputs`` is the run's ``[train].structured_outputs`` spec (canonical
310
+ StructuredOutputsParams-kwargs JSON, or "" for none). When set, it is registered as the
311
+ adapter's per-adapter guided-decoding DEFAULT so every serve request is constrained by the
312
+ SAME grammar the adapter trained under — otherwise a structured-outputs run drifts at
313
+ unconstrained serving (train/serve exposure bias: it over-generates arrays that truncate to
314
+ invalid JSON and emits keys the training grammar had masked). See ``_structured_outputs_body``.
315
+ """
278
316
  validate_serving_lora_rank(model, lora_rank, rank_source="configured train.lora_rank")
279
317
  subfolder = f"{adapter_prefix}/adapter"
280
318
  if not dry_run:
@@ -298,6 +336,9 @@ def deploy_adapter(
298
336
  # Preserves thinking parity: without this, Qwen3.5 defaults to thinking ON regardless of training.
299
337
  "thinking": bool(thinking),
300
338
  }
339
+ so_default = _structured_outputs_body(run_id, structured_outputs, thinking=thinking)
340
+ if so_default is not None:
341
+ body["structured_outputs"] = so_default
301
342
  normalized_org_id = (org_id or "").strip()
302
343
  if normalized_org_id:
303
344
  body["org_id"] = normalized_org_id
@@ -386,6 +386,10 @@ def deploy(run_id: str, key: Annotated[dict, Depends(require_key)], payload: dic
386
386
  "lora_rank": spec.train.lora_rank,
387
387
  # a run trained with thinking serves with thinking (per-run parity)
388
388
  "thinking": spec.thinking,
389
+ # a run trained with structured_outputs serves under the SAME grammar (guided-decoding
390
+ # parity), so it doesn't drift at unconstrained serving; deploy_adapter registers it as
391
+ # the per-adapter default (non-thinking runs only — see _structured_outputs_body).
392
+ "structured_outputs": spec.train.structured_outputs,
389
393
  "org_id": deploy_org_id,
390
394
  }
391
395
  if dry_run:
@@ -125,6 +125,11 @@ class TrainSpec:
125
125
  # OPD only: weight of the terminal-EOS behaviour-cloning term (recipe default when None). 0
126
126
  # disables it. Raise it for a student that keeps running past the length cap without emitting EOS.
127
127
  opd_eos_loss_coef: float | None = None
128
+ # OPD only: student-side entropy floor — weight and target (nats) of the hinge on the per-sample
129
+ # mean completion entropy (recipe defaults when None; coef 0 disables the term). counteracts
130
+ # reverse-KL over-sharpening on small students (the greedy repetition loop at temp=0).
131
+ opd_entropy_floor_coef: float | None = None
132
+ opd_entropy_floor: float | None = None
128
133
  # OPD only: the managed teacher to distil from, stored as its resolved Fireworks model id (parse
129
134
  # canonicalizes any alias / spaced / raw-id form via recipe.resolve_teacher, e.g. "glm-5.2" =>
130
135
  # "accounts/fireworks/models/glm-5p2"). "" => the recipe default (GLM 5.2).
@@ -218,6 +223,8 @@ class JobSpec:
218
223
  advantage_clip=_opt_float(train.get("advantage_clip")),
219
224
  thinking_length_penalty_coef=_opt_float(train.get("thinking_length_penalty_coef")),
220
225
  opd_eos_loss_coef=_opt_float(train.get("opd_eos_loss_coef")),
226
+ opd_entropy_floor_coef=_opt_float(train.get("opd_entropy_floor_coef")),
227
+ opd_entropy_floor=_opt_float(train.get("opd_entropy_floor")),
221
228
  teacher_model=str(train.get("teacher_model") or ""),
222
229
  stop_sequences=_str_tuple(train.get("stop_sequences")),
223
230
  structured_outputs=str(train.get("structured_outputs") or ""),
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "freesolo-flash-dev"
3
- version = "0.2.56"
3
+ version = "0.2.57"
4
4
  description = "Flash — managed LoRA post-training (SFT/GRPO/OPD) for Freesolo environments, driven by the `flash` CLI"
5
5
  readme = "README.md"
6
6
  # RunPod Flash supports Python 3.11-3.12 (not 3.13 yet).
@@ -106,7 +106,7 @@ artifacts = ["flash/py.typed"]
106
106
  # auto-published by .github/workflows/publish-dev.yml: a push to `dev` whose version isn't yet on
107
107
  # PyPI publishes it; pushes that don't change it find it already published and no-op. So cutting a
108
108
  # dev-channel release = bump this (and [project].version) and merge to `dev`.
109
- version = "0.2.56"
109
+ version = "0.2.57"
110
110
 
111
111
  [tool.pytest.ini_options]
112
112
  pythonpath = ["."]