Skip to content

perf(startup): overlap engine boot with train init + mmap loading - #215

Draft
Rockdu wants to merge 7 commits into
mainfrom
kangrui/overlap-engine-init
Draft

perf(startup): overlap engine boot with train init + mmap loading#215
Rockdu wants to merge 7 commits into
mainfrom
kangrui/overlap-engine-init

Conversation

@Rockdu

@Rockdu Rockdu commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

What

  • Overlap engine boot with train-actor init: RolloutManager.__init__ no longer blocks on ray.get(engine.init...) (the long-standing TODO at init_rollout_engines); the driver's first offload becomes fire-and-forget, and every later engine touchpoint (offload/onload/generate/eval/get_rollout_engines_and_lock) syncs through _ensure_engines_ready(). --debug-rollout-only keeps 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 on boot_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).
  • Default sglang-d engines to mmap weight loading (SGLANG_USE_RUNAI_MODEL_STREAMER=false in the engine env): safetensors safe_open replaces 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 timing probes: every process on the startup path (driver, RolloutManager, each engine actor, each train rank) emits parseable STARTUP_TIMING lines; scripts/parse_startup_timing.py rebuilds a full cross-process timeline from a single ray job log.

Why

Startup (launch → first update_weights done) 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 one ray.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 mainstartup_done (first weight sync complete).

variant total startup vs base
C0 base 242.9s
C1 mmap only 216.0s −26.9s (−11%)
C2 overlap only 188.2s −54.7s (−23%)
C3 both 153.0s −89.9s (−37%)

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_manager block 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:

  • Current safetensors get_tensor copies 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; probes
  • miles/ray/placement_group.py — fire-and-forget first offload; probes
  • miles/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 probes
  • miles/utils/startup_timing.py — probe module (import-light, print-based, cross-process)
  • scripts/parse_startup_timing.py — log → timeline/rollup report

Remaining work (why draft)

  • Re-run the ablation with the VRAM gate to confirm the retained saving
  • Confirm on hardware that a colocate startup never has engine weights and the FSDP broadcast peak resident together (the new test pins the call ordering; peak memory itself still needs a GPU run)
  • Run a full multi-rollout training to exercise generate/fault-tolerance/recover_rollout_engines under deferred init.
  • Decide whether the mmap default should auto-detect local vs remote checkpoint paths instead of an env override.

Checklist

  • pre-commit run --all-files passes
  • Added/updated tests for new behaviour — tests/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 H200
  • pytest -x is 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.
  • If launch flags changed, python3 train.py --help still parses — no CLI flags changed; arg parsing exercised by all 8 validation runs
  • If a public flag was added, it appears in the CLI reference docs — n/a, no new flags
  • If an example was added, it has a real walkthrough — n/a

Rockdu and others added 7 commits August 20, 2026 17:06
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant