Repository navigation
Reference: startup time experiments for radixark/miles_diffusion#310 - #1
OnePunchMonk wants to merge 5 commits into
Conversation
Add STARTUP_TIMING probes across driver, rollout manager, engines, scheduler and train actors, plus scripts/parse_startup_timing.py to rebuild the timeline. Switch the sglang engine weight loader to mmap (SGLANG_USE_RUNAI_MODEL_STREAMER=false). Env-flag gated, off by default: MILES_STARTUP_OVERLAP, MILES_STARTUP_BUDGET_GATE, MILES_STARTUP_GATE_REQUIRE_ENGINES, MILES_STARTUP_SKIP_ROUNDTRIP, MILES_STARTUP_ENGINES_FIRST, MILES_STARTUP_ROUTER_POLL, MILES_STARTUP_BENCH_EXIT.
Engine init is no longer awaited by the rollout manager, the startup offload/onload round trip is skipped, and colocated train ranks gate their first large GPU allocation on engine init plus a memory budget (shard + largest tensor + margin). If the budget does not fit next to the resident engines, the engines are offloaded once and onloaded again before the first weight sync. --serial-startup restores the old order. Drops the engines-first and router polling experiments, which did not measure. Bakes .pyc files into the image.
…tartup gate The rollout manager and engine actor processes no longer import sglang at module load: GPU_MEMORY_TYPE_WEIGHTS is a local constant, kill_process_tree and ServerArgs are imported where used. Importing miles.ray.rollout drops from 29 s to 5.5 s and the engine actor module from 23 s to 1.4 s, both on the startup critical path. --startup-engine-reserve-gib lets colocated train ranks start their GPU load before engine init finishes when free memory minus that reserve covers the init peak. Unset keeps waiting for the engines. Also stubs the startup probes in test_ema_optimizer, which execs actor methods in a hand-built namespace.
…ctor The launch_server child imports sglang anyway; building ServerArgs there keeps sglang out of the engine actor, which saves one full sglang import on the engine startup path. The actor passes a plain-picklable copy of args, the host and the ports.
Rank 0 used to finish booting (about 50 s, mostly imports) before ranks 1..N were created. Now all ranks start together and get rank 0's rendezvous address before init.
|
Two more reference branches: |
|
Measurement history moved here from radixark#310 so that PR stays readable. The comments below are copied verbatim, in order, with their original dates. Index:
|
First GPU result: base variant (RunAI streamer on, no overlap), H3 t2va recipe, 2x H200 on Modal, full MiniMax-H3 weights on local disk. First run in the container, so the page cache was cold. Time to the end of the first weight sync attempt: 461.6 s.
Notes:
|
Variant 2: mmap loader (SGLANG_USE_RUNAI_MODEL_STREAMER=false), same setup, second run in the container so the page cache is warmer. Time to the end of the first weight sync attempt: 452.1 s (base was 461.6 s).
Reading it:
|
Variant 3: overlap (engine init not awaited, boot offload not awaited, budget gate on), on top of mmap. Time to the end of the first weight sync attempt: 261.7 s (mmap 452.1 s, base 461.6 s).
Reading it:
|
Variant 4: overlap_ready (overlap plus the train gate also waits for engine init), on top of overlap. Time to the end of the first weight sync attempt: 333.9 s (overlap 261.7 s).
Reading it:
|
Variant 5: skip_roundtrip (no startup offload, no startup onload; engines stay resident while train inits), on top of overlap_ready. Time to the end of the first weight sync attempt: 257.8 s (overlap_ready 333.9 s, ungated overlap 261.7 s).
Reading it:
|
Variant 6 (engines_first: engine init launched before router/tracking/data source, router port polled instead of sleep 3), and the full ladder. engines_first: 263.6 s to the end of the first weight sync attempt. That is the same as skip_roundtrip (257.8 s) within noise, so this change is not visible at this resolution. Full ladder, 2x H200, full MiniMax-H3, one rep each, each row adds one change to the row above (cumulative, not isolated):
Net: the gated, race free stack (last row) is about 43% faster than base here (462 s to 264 s). What moved the number: overlapping train init with engine boot (about 190 s) and, once the gate waits for the engines, not queuing that wait behind a boot offload. mmap shows up in engine boot (166 s to 110 s) but not in the total at this noise level. The router and ordering change is not measurable here. What the data says about next steps:
Caveats:
|
Breakdown of engine.wait_server_healthy (118 s in the skip_roundtrip run, engine0). From sglang's own timestamped log lines, no extra GPU run:
So it is about half Python process boot and half weight load. The DiT is only 23 s of it. Import profile (CPU container, same image, python -X importtime):
Other notes:
|
Pushed 900c755: the overlapped startup is now the default, not behind env flags.
Corrections to my earlier comments:
CPU tests: 240 passed on this commit. The new default path has not run on GPU yet; a full run is queued on Modal and I will post it here. |
GPU check of the new default (900c755), 2x H200, full MiniMax-H3, one rep. Time to the end of the first weight sync attempt: 331.9 s. This was the first engine load in a fresh container, so compare it with the cold base run (461.6 s): about 28% faster. The ladder's 258 to 264 s numbers were warm runs later in a container.
Engine experiments:
|
Critical path of the current default stack, warm run (257.8 s to the first weight sync), from the probe timestamps:
Five Python processes start one after another on this path (driver, rollout manager, engine actor, launch_server, scheduler), each importing torch and sglang for 25 to 30 s. Next experiments, in one Modal container:
|
Pushed a new commit: lighter actor imports, an opt-in engine memory reserve for the startup gate, and a fix for test_ema_optimizer (it execs actor methods in a hand-built namespace and broke on the startup probes; that break is in the earlier commits of this PR too).
First GPU result of this round, cold container, SGLANG_SKIP_P2P_CHECK=1: the P2P step went from 35 s to 2 s and the engine now starts 7 s after the rollout manager instead of about 30 s. But the run was slower overall (500 s vs 332 s cold before): launch_server import 66 s, scheduler 52 s and weight load 94 s were all much slower than in the earlier cold run. My guess is the earlier engine start now overlaps rank 0's cold read of the full model, so both hit a cold disk at once. Not confirmed. Warm runs (old default vs each change) are running now and will tell whether the changes help. |
Warm runs from this round, same container, one rep each. Measured from driver process start to the end of the first weight sync attempt, because the lazy imports move the driver's sglang import from before main() into parse_args (so the old in-main totals are not comparable).
Threaded text_encoder load and the combined run are still going. |
Last two runs of this round (same container, from driver start to the end of the first weight sync attempt):
Threaded text_encoder load: drop it.
So the keepers from this round are the lighter actor imports (about -39 s) and the engine memory reserve (about -93 s more), both already pushed. The reserve is opt-in ( One rep per row, on a slower container than the earlier ladder. |
GPU, one container (a faster one than the last round), 2x H200, full MiniMax-H3, all rows with
So about -36 s (-17%) for the two changes together.
On forking the scheduler workers: the launch_server process already has CUDA initialized before |
Summary so far (2x H200, full MiniMax-H3, H3 t2va recipe):
|
Trimmed this PR down to the required optimised code, now a single commit (749057e). The experiment code (STARTUP_TIMING probes, scripts/parse_startup_timing.py, the benchmark exit hook, the RunAI streamer toggle, and the step by step commit history) is kept for reference in #1. The code here is the same as the measured version minus that instrumentation. tests/fast: 439 passed. |
New commit 7ddb65f: It needs a small hook in sglang ( Result (2x H200, full MiniMax-H3, same box, two runs each, with
Engine boot (server spawn to healthy) went from about 138 s to 104 s. The engine and the train side now finish at about the same time (143 to 149 s), so the next cuts have to hit both. The previous version of this PR is kept at OnePunchMonk/miles_diffusion branch Tests: tests/fast 441 passed; the sglang hook's unit test passes on the benchmark image. @Rockdu could you also take a look at OnePunchMonk/sglang#8? If the approach looks fine to you, I can open it as an upstream PR in sgl-project/sglang. |
|
Updated reference branch |
Reference copy of the full startup-time work behind radixark#310, kept here so the upstream PR can carry only the final code.
This branch has everything that was used for the measurements:
scripts/parse_startup_timing.py, which rebuilds one cross-process timeline from a ray job log.MILES_STARTUP_BENCH_EXIT=1, which stops the driver right after the first weight sync.MILES_SGLANG_USE_RUNAI_STREAMER=1, to switch the engine back to the RunAI streamer for comparison.Results and analysis are in the comments on radixark#310. Not meant to be merged.