perf(startup): overlap engine boot with train init + mmap loading - #215
Draft
Rockdu wants to merge 7 commits into
Draft
perf(startup): overlap engine boot with train init + mmap loading#215Rockdu wants to merge 7 commits into
Rockdu wants to merge 7 commits into
Conversation
…es, and train actors Emits parseable STARTUP_TIMING lines from every process on the startup path (launch -> first update_weights) so a single ray job log can be rebuilt into a full cross-process startup breakdown by scripts/parse_startup_timing.py. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Defer the engine init ray.get out of RolloutManager.__init__ and make the driver's first offload fire-and-forget; anything that later touches the engines (offload/onload/generate/eval/get_rollout_engines_and_lock) syncs via _ensure_engines_ready. --debug-rollout-only keeps the old sync. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…amer by default On local-disk checkpoints the streamer reads into private buffers and then clones every tensor, capping at ~2.4GiB/s even with a warm page cache; safetensors safe_open cuts a warm H3 engine boot from 88s to 63s (DiT load 37s -> 14s). Cold remote FS still favors the streamer's parallel prefetch, so the launcher env can re-enable it via SGLANG_USE_RUNAI_MODEL_STREAMER. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Under colocate, overlapped engine boot lets boot-time engine weights (~66GB/GPU for H3 tp=2) coexist with the FSDP wrap + rank-0 full-model broadcast peak, which only fits by luck (94% VRAM on H200 for H3 bf16 LoRA; qwen-image fp32 full-finetune would OOM). CPU-side init still overlaps engine boot; the first large GPU allocation now waits for the RolloutManager's first offload via wait_engines_offloaded, which actor task ordering places after deferred engine init. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The train-side VRAM gate waited on an offload the driver had queued separately, but Ray only orders actor tasks per caller: if the gate's task was dequeued first it returned while the engines' boot-time weights were still resident, and a colocated train actor would materialize its model on top of them. boot_offload now performs the release itself, exactly once, so the invariant holds whichever caller arrives first. Add CPU regression tests for the ordering invariant and for the gate being skipped when rollout keeps its weights (disaggregated placement). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
RolloutManager.__init__no longer blocks onray.get(engine.init...)(the long-standing TODO atinit_rollout_engines); the driver's firstoffloadbecomes fire-and-forget, and every later engine touchpoint (offload/onload/generate/eval/get_rollout_engines_and_lock) syncs through_ensure_engines_ready().--debug-rollout-onlykeeps the old synchronous path. VRAM safety under colocate: only the CPU-side part of train init overlaps engine boot — the first large train-side GPU allocation (FSDP wrap + rank-0 broadcast) gates onboot_offload, which releases the engines' boot-time weights itself, exactly once. Ray only orders actor tasks per caller, so the gate must not assume the driver's own submission ran first; making the release idempotent and self-triggering is what keeps rollout and training residency sequential for any model size. The gate is skipped when rollout keeps its weights resident (disaggregated placement).SGLANG_USE_RUNAI_MODEL_STREAMER=falsein the engine env): safetensorssafe_openreplaces the RunAI streamer's read-into-private-buffer + per-tensor clone pipeline. Operators can re-enable the streamer by exporting the var in the launcher env (still the right choice for cold remote FS).STARTUP_TIMINGlines;scripts/parse_startup_timing.pyrebuilds a full cross-process timeline from a single ray job log.Why
Startup (launch → first
update_weightsdone) took 4–8+ min per run. Probing showed the two dominant serial blocks: engine boot (~130s: subprocess re-imports + weight load + post-boot offload) ran strictly before train-actor init (~62s) due to oneray.get, and the streamer capped weight reads at ~2.4 GiB/s even from a warm page cache (it is copy-pipeline-bound, not disk-bound, on local checkpoints).Validation
2×2 ablation on 4× H200 (single node, MiniMax-H3 t2va GRPO recipe: tp=2 → 2 engines, 4 colocated train ranks, LoRA-IPC, weights on local /scratch, warm page cache). Two independent runs per cell, spread ≤ ±2s; metric is driver
main→startup_done(first weight sync complete).Mechanism checks: with mmap, per-engine health-wait drops 88s → 63s (DiT 61.7 GiB load 37s → 14s); independently reproduced on a second machine (2 engines, 96s → 62s). With overlap, the driver's
create_rollout_managerblock drops 145s → 0.1s and train init hides entirely inside engine boot. Effects compose slightly super-additively. The ablation numbers above were measured before the VRAM gate (engine weights coexisted with the broadcast peak at ~94% VRAM on H3 — fits, but only by luck; larger fp32 configs would OOM, hence the gate). With the gate, the GPU-heavy ~45s of train init serializes after engine offload, so the overlap saving shrinks to an estimated ~25s warm (more on cold cache, where the overlapped CPU-side checkpoint read dominates); gated re-measurement pending.Caveats found during validation:
get_tensorcopies on CPU (measured Rss≈Pss across engines), so mmap gives no resident-RAM dedup — the read side shares the page cache and peak RAM drops ~53 GB (no streamer buffers/clones), but per-engine resident copies remain.Files
miles/ray/rollout.py— deferred engine init +_ensure_engines_ready(); mmap env default; probesmiles/ray/placement_group.py— fire-and-forget first offload; probesmiles/ray/train_actor.py,miles/backends/fsdp_utils/actor.py— probes (NCCL init, per-component load/FSDP/broadcast, first weight sync)miles/backends/sglang_diffusion_utils/sglang_diffusion_engine.py— probes (spawn, health wait, router registration)train_diffusion.py— driver phase probesmiles/utils/startup_timing.py— probe module (import-light, print-based, cross-process)scripts/parse_startup_timing.py— log → timeline/rollup reportRemaining work (why draft)
generate/fault-tolerance/recover_rollout_enginesunder deferred init.Checklist
pre-commit run --all-filespassestests/fast/rollout/test_startup_offload_ordering.py(stage-a-cpu) covers the colocate ordering invariant; plus 8 instrumented end-to-end startup runs + 2 corroboration runs on H200pytest -xis green — not run locally: the dev machine has no ray/torch/sglang (all fast tests in this package import them). The new test's CI registration parses, its logic was verified against an equivalent replica, and CI runs the real thing.python3 train.py --helpstill parses — no CLI flags changed; arg parsing exercised by all 8 validation runs