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