Repository navigation
[ROCm] Enable the cuMem CUDA-graph pool offload on ROCm - #59523
Conversation
5ec10b8 to
1c6815d
Compare
da8b225 to
35f4cf8
Compare
|
This pull request has merge conflicts that must be resolved before it can be |
35f4cf8 to
1ceae6d
Compare
1ceae6d to
890158f
Compare
|
This pull request has merge conflicts that must be resolved before it can be |
890158f to
7d9eb6d
Compare
aec5451 to
71b96a1
Compare
e3c77ac to
89f3000
Compare
|
@aoshen02 on whether we need the zeroing, I dug into this more on MI300X (gfx942, amdgpu 6.16):
So I agree it only matters if ≤6.10 is still supported. If it is, keeping it ROCm-only (or gated on the driver version) would avoid the cost elsewhere. Separately, a correction to my earlier CI comment: the TP=2 sleep tests weren't actually skipped on AMD. |
|
@mawong-amd thanks for digging into this. The VRAM-scrub check and the cost numbers settle it for current drivers. One question before I finalize: does vLLM still need to support amdgpu 6.10 and older, the drivers that don't scrub VRAM on release?
|
|
Yes — vLLM's documented floor is ROCm 6.3, and ROCm 6.x stacks pair with amdgpu 6.8–6.10, the range that doesn't scrub VRAM on release. So those drivers are in the supported window. The case where it helps: on those drivers a wake remap can return stale VRAM, and we showed ROCM_AITER_MLA reads KV slots it hasn't written — NaN bit patterns there change outputs. #44972's GDN case is the same shape (collapse to token 0). Both fail silently, with no error. So I'd keep it: worst case ~35 ms per wake for a 150 GiB KV cache, and wake isn't on a known hot path. Cheap insurance for a supported driver range. Keeping it ROCm-only, as you proposed, also leaves CUDA untouched. Could be worth a one-line comment there noting it's unnecessary on amdgpu ≥6.14, which scrubs on release, so a future reader knows it can go once that floor moves. |
The zero-fill this replaces was ROCm-only (vllm-project#44972); keep CUDA wake-up unchanged. Signed-off-by: aoshen02 <aoshen@inferact.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The MI300 distributed steps already run the TP=2 sleep tests. Signed-off-by: aoshen02 <aoshen@inferact.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Pushed 9ddebaa and 52c78f9 on top of the main merge:
Thanks for checking the driver range. |
Match main, which zeroes with hipMemset and no sync. Signed-off-by: aoshen02 <aoshen@inferact.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hipMemset at offset 0 returns before it finishes, and vLLM's non-blocking stream does not wait for the null stream, so a level-2 buffer restore can be overwritten by a late memset. Reproduced at the allocator level on MI355X; main has the same race. Signed-off-by: aoshen02 <aoshen@inferact.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: aoshen02 <aoshen@inferact.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use hipMemsetAsync on the current stream instead of hipMemset plus a device sync: work after wake is ordered behind the zero-fill by the stream. Signed-off-by: aoshen02 <aoshen@inferact.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep the existing hipMemset binding and sync after it, instead of adding a cudaMemsetAsync binding. Signed-off-by: aoshen02 <aoshen@inferact.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/ci run |
|
✅ Triggered Buildkite CI #93733 for commit |
|
/ci retry |
|
✅ Queued 4 failed job(s) for retry in Buildkite CI #93733. |
Purpose
#59160 captures CUDA graphs (both model runners) into a cuMem pool that sleep mode offloads, opt-in with
sleep_mode_offload_cudagraph(off by default, like SGLang'sSGLANG_MEMORY_SAVER_CUDA_GRAPH). Its policy (VllmConfig.use_cumem_cudagraph_pool) requirescurrent_platform.is_cuda(), so on ROCm it is inert even when opted in: every capture site getsnullcontext(pool), nocudagraphtag appears, and asleep memory equals main with #59156. This PR enables the same path on ROCm.Based on main, which now includes #59156, #59158 and #59160. RFC: #59055. It composes with #59909 (release the graph pool instead of backing it up); see Test Result.
What breaks with the Python gate alone
Relaxing only the policy's platform check makes the first PIECEWISE capture fail on MI355X with
(
hipErrorStreamCaptureImplicit), followed byunknown error at :461and an engine start failure. The cause is ROCm-only code at the PR base:create_and_mapzeroes every freshly mapped region with a blockinghipMemseton the legacy stream (the stale-VRAM guard for GDN state from #43022/#46203). On CUDA there is no memset, so a cuMem allocation during capture is legal; on ROCm the graph pool's first allocation during capture triggers the memset, and the memset breaks the capture.Changes
vllm/config/vllm.py:VllmConfig.use_cumem_cudagraph_poolchecksis_cuda_alike()instead ofis_cuda().csrc/cumem_allocator.cpp: the ROCm-only zero-fill increate_and_mapis removed, so the CUDA and ROCm paths are identical again and nothing touches graph capture. Instead, on ROCm only,CuMemAllocator.wake_upzeroes discarded allocations (those without a CPU backup) after remapping them, and synchronizes after each zero-fill.hipMemsetreturns before it finishes, and vLLM's non-blocking stream does not wait for it. Without the sync, a late memset can overwrite the level-2 buffer restore. Main has the same race. On MI355X it reproduces at the allocator level: 200/200 on main, 0/200 with the sync. That is the memory that can hold stale VRAM on drivers that don't scrub on release ([Test][V1] Add sleep/wake correctness regression test for hybrid GDN/… #44972); startup allocations don't need it (the KV cache istorch.zeros). Suggested by @mawong-amd. It stays because vLLM's ROCm floor (6.3) pairs with amdgpu 6.8–6.10, which don't scrub VRAM on release; amdgpu ≥6.14 does, so it can go once that is the floor. CUDA wake is unchanged. On ROCm it costs about 35 ms per wake for a 150 GiB KV cache.AiterCustomAllreduce: passesenable_register_for_capturing=Falseto AITER'sCustomAllreducewhen the cuMem graph pool is active. This is required: at capture AITER IPC-registers the graph input pointers, andhipIpcGetMemHandlerejects VMM memory just ascudaIpcGetMemHandledoes on CUDA ([Feature] Release the CUDA graph pool on sleep #59160's custom-allreduce fix). Without it, capture with AITER custom allreduce at TP2 fails withhipIpcGetMemHandle ... invalid argument(custom_all_reduce.cuh:4080) and the engine doesn't start (gfx942, reproduced by @mawong-amd). AITER already forces copy-in for expandable-segment pointers for the same reason.vllm/_aiter_ops.py: the fused all-reduce + RMSNorm + per-group FP8 quant ops (plain andemit_bf16) passedregistered=<capturing>to AITER's rawfused_ar_rms_per_group_quant, which bypassesenable_register_for_capturing, so under the cuMem graph pool the capture input was IPC-registered and capture aborted (hipIpcGetMemHandle ... invalid argument, reproduced on MI355X). Both now passregistered=capturing and ca.enable_register_for_capturing; with the fix capture succeeds and replay is bit-exact, also after sleep/wake of thecudagraphtag.test_sleep_cudagraph_pool[custom-ar-tp2]now also runs when AITER custom allreduce is active (it skipped before becauseca_commisNonethen), and a new test covers the fused AITER allreduce + RMSNorm + per-group FP8 quant op under the cuMem graph pool. The new test fails without the_aiter_ops.pychange above.Confirmed and left alone:
CustomAllreduceneeds [Feature] Release the CUDA graph pool on sleep #59160's_capture_registered = Falseon ROCm too: forcingregistered=Truefails capture withcustom_all_reduce_hip.cuh:167 'invalid argument'. The copy-in path works unchanged.NCCL_GRAPH_REGISTERdefault (VllmConfig._verify_cumem_cudagraph_pool:setdefault("0")when the pool is active, an explicit value kept with a warning, as in fix(nccl): disable graph buffer registration for pausable graph pools sgl-project/sglang#40648) keys onuse_cumem_cudagraph_pool, so it applies to RCCL once this PR enables the gate on ROCm (RCCL 2.30.4 honoursNCCL_GRAPH_REGISTER). Not duplicated. AITER'senable_register_for_capturing=False, like vLLM's custom allreduce (Support releasing CUDA graph memory when paused sgl-project/sglang#7873), applies only when the pool is active.VLLM_ROCM_QUICK_REDUCE_QUANTIZATION=FP.Results on MI355X
8x MI355X, vLLM nightly
e975732152with #59156 (PR1), #59160 and this change overlaid;--gpu-memory-utilization 0.85 --max-model-len 4096, level-1 sleep. Values are rocm-smi "VRAM Total Used" in MiB per GPU, polled until stable (rocm-smi lags frees by 7–13 s).cudagraphtagVLLM_USE_V2_MODEL_RUNNER=1)cudagraphtag is present, asleep while sleeping and restored on any wake, including weights-only.weights, thenkv_cache). TP2 stays bit-exact through a 10-cycle sleep/wake loop plus level 2 (sleep?level=2, wake weights,reload_weights, wake kv_cache), with and without [Feature] Offload KV-init runtime state on sleep #59158, and with QuickReduce. V4-Flash TP4 is bit-exact over 5 cycles. (DSv2-Lite TP1 with graphs is nondeterministic before any sleep, unpatched included, so exact match is not meaningful there; the graph-replay-after-wake property is covered by the unit test.)What this does not release
On ROCm the torch graph private pool is only 72–244 MiB per GPU, while graphs cost 948 / 1044 / 1543 MiB (DSv2-Lite TP1 / TP2 / DP2) and 1743 MiB (V4-Flash TP4) against
--enforce-eager. The remaining 0.85–1.5 GB is HIP runtime graph memory held outside any torch pool, the ROCm counterpart of the graph-exec residue described in #59160. One difference matters: in a bare-process probe on MI355X, destroying graphs does return that memory (200 graphs: +4070 MiB captured, back to baseline afterreset()+empty_cache()), whereascuGraphExecDestroyreturned 0 on GB300. So "destroy graphs on sleep, recapture on wake" would recover roughly the full graphs-vs-eager delta on ROCm, at the cost of recapture time (9–50 s here). That is a possible follow-up, not part of this PR.Test Plan
pre-commit run --files <changed files>.CUDA:
csrc/cumem_allocator.cpponly loses code inside#ifdef USE_ROCM; run the cuMem and sleep-mode tests on CUDA.ROCm unit test (single GPU):
pytest -v tests/basic_correctness/memory/cumem/test_cumem.py -k "cudagraph_pool_survives_sleep or plain_cudagraph_capture"ROCm end to end: DeepSeek-V2-Lite TP1/TP2/DP2 and DeepSeek-V4-Flash TP4 (V2 runner), sleep/wake cycles with greedy-output comparison, as above.
Test Result
g++ -std=c++17against CUDA 12.9 headers builds the TU; the preprocessed output is identical to the base PR head apart from the__LINE__values thatCUDA_CHECKembeds in its messages. Not built with hipcc in this environment.test_cudagraph_pool_survives_sleeppasses at levels 1 and 2 with the C++ change and fails at capture without it. The ROCm runs used a build that created the zero stream percreate_and_mapcall; this PR creates it once per device.pool_test.pypasses at levels 1 and 2; DeepSeek-V4-Flash TP4 (V2 runner, with [Feature] Release WorkspaceManager scratch on sleep #59156 + [Feature] Offload KV-init runtime state on sleep #59158) 30/30 cycles bit-exact, flat at 10779 MiB asleep, wake 0.88 s (1.58 s with the per-call build); DeepSeek-V2-Lite TP2 10/10 cycles including a level-2 reload.csrc/unchanged since the build above): 25 cold startups with this build (DeepSeek-V2-Lite TP2 ×21, V4-Flash TP4 ×4, 5 nodes, every GPU pair) all came up, against 23 with the earlier per-call build. One earlier startup with this build had hung during KV-cache allocation (AMDKFD_IOC_ALLOC_MEMORY_OF_GPU); it did not reproduce and no common factor was found, so a rare driver-side issue cannot be fully excluded. Memory after wake vs before sleep: in-process −12 MiB (V4-Flash TP4) and 0 MiB (DSv2-Lite TP2); 30/30 cycles bit-exact on both, including a level-2 reload on TP2.test_cumem.py -k cudagraph4 passed;test_sleep_cudagraph_custom_allreduce_tp2passed.Re-test after rebase onto main
bdd31c31d(no conflicts)AMD MI355X (gfx950), ROCm nightly
29468dde(ROCm 7.2.3), cumem allocator rebuilt from this branch:test_cumem.py: 21 passed, 3 runs (incl.test_cudagraph_pool_sleepat levels 1 and 2).test_sleep_mode.py -k test_sleep_cudagraph_pool, 2 GPUs: 5 passed, 3 runs (full, piecewise, breakable, custom-ar-tp2, off-by-default).test_sleep_mode_backend.py: 9 passed, 3 runs.--sleep-mode-offload-cudagraph, 5 runs of 3 cycles (level-1 full wake, level-1 selective wake, level 2 +reload_weights): outputs identical to awake in every cycle, with vLLM custom allreduce and with AITER custom allreduce. Asleep 9549 MiB/GPU vs 9696 MiB with the option off (the graph pool is 72 MiB/GPU on this model).Re-test after review (0e54b3f: zero on wake instead of every mapping)
AMD MI355X, ROCm nightly
92044241a(already includes #59156, #59158, #59160), cumem allocator rebuilt from this branch. The.sono longer importshipMemset,hipMemsetAsyncor anyhipStream*function.test_cumem.pytest_level2_discards_ordinary_tensor_with_weights_tagfail.test_sleep_mode.py -k test_sleep_cudagraph_pool, 2 GPUsVLLM_ROCM_USE_AITER=1(AITER custom AR). No custom AR registers graph buffers.test_sleep_mode.py -k "undefined_remap or deep_sleep"test_sleep_mode_backend.py--sleep-mode-offload-cudagraph, 3 cycles (L1 full wake, L1 selective wake, L2 +reload_weights):test_cumem.py21 passed.test_sleep_mode.py -k "cudagraph_pool or undefined_remap"6 passed. Main gives the same results.AITER tests from #60525 (@indianspeedster)
8x MI355X (gfx950), ROCm 7.2.3,
vllm/vllm-openai-rocm:nightly-bb87d227, cumem allocator rebuilt from this branch plus #60367:custom-ar-tp2withVLLM_ROCM_USE_AITER=1, beforecustom-ar-tp2withVLLM_ROCM_USE_AITER=1, after_aiter_ops.pyfixhipIpcGetMemHandle ... invalid argument)The new test needs #60367 to start on ROCm. CI's ROCm sleep-mode step runs on 1 GPU, so both TP=2 cases skip there.
🤖 Generated with Claude Code