Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 0 additions & 22 deletions csrc/cumem_allocator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -226,28 +226,6 @@ void create_and_map(unsigned long long device, ssize_t size, CUdeviceptr d_mem,
return;
}

#ifdef USE_ROCM
// ROCm-only: zero the freshly (re)mapped region.
//
// On some amdgpu driver versions the physical VRAM backing a buffer is not
// scrubbed on release, so the pages handed back here (e.g. on wake_up after a
// sleep that discarded a tag) can still contain stale data from a previously
// freed allocation -- possibly from another process. Callers of the device
// allocator expect newly mapped memory to be well defined; in particular
// hybrid Mamba / gated-delta-net (GDN) models keep a persisted conv/recurrent
// state cache, and a new sequence that reads an unzeroed slot will propagate
// garbage/NaN, collapsing generation to token id 0 (decoded as "!").
//
// This is scoped to the ROCm path on purpose: the CUDA path is unchanged.
// Use CUDA_CHECK so a failure sets the global error_code; the Python caller
// (python_create_and_map) only inspects error_code, so returning without
// setting it would make a failed memset look successful and still hand back
// unzeroed memory.
CUDA_CHECK((CUresult)hipMemset(reinterpret_cast<void*>(d_mem), 0, size));
if (error_code != 0) {
return;
}
#endif
// std::cout << "create_and_map: device=" << device << ", size=" << size << ",
// d_mem=" << d_mem << ", p_memHandle=" << p_memHandle << std::endl;
}
Expand Down
2 changes: 1 addition & 1 deletion docs/features/sleep_mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ By default, CUDA graph memory stays on the GPU while asleep. With
`sleep_mode_offload_cudagraph=True` (off by default), CUDA graphs are captured
into a cuMem pool that sleep backs up to CPU memory at both levels and any wake
restores in place, so graphs are replayed, not recaptured. It needs the default
`cumem` backend, CUDA and CUDA graphs; otherwise it has no effect.
`cumem` backend, CUDA or ROCm, and CUDA graphs; otherwise it has no effect.

```python
llm = LLM("Qwen/Qwen3-8B", enable_sleep_mode=True, sleep_mode_offload_cudagraph=True)
Expand Down
11 changes: 7 additions & 4 deletions tests/basic_correctness/memory/cumem/test_cumem.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ def test_runtime_state_survives_sleep(level):
@create_new_process_for_each_test("fork" if current_platform.is_cuda() else "spawn")
@pytest.mark.skipif(current_platform.is_xpu(), reason="Uses the CuMem allocator")
def test_level2_discards_ordinary_tensor_with_weights_tag():
"""Reproduce the level-2 variant for an ordinary tensor in weights."""
"""Discarded weights-tag memory is remapped; ROCm zeroes it over stale pages."""
allocator = get_mem_allocator_instance()

with allocator.use_memory_pool("weights"):
Expand All @@ -374,8 +374,9 @@ def test_level2_discards_ordinary_tensor_with_weights_tag():
torch.accelerator.synchronize()

assert (fake_weight.data_ptr(), ordinary_tensor.data_ptr()) == pointers
assert torch.all(fake_weight == 0xA5)
assert torch.all(ordinary_tensor == 0xA5)
expected = 0 if current_platform.is_rocm() else 0xA5
assert torch.all(fake_weight == expected)
assert torch.all(ordinary_tensor == expected)


@create_new_process_for_each_test("fork" if current_platform.is_cuda() else "spawn")
Expand Down Expand Up @@ -427,7 +428,9 @@ def model(x):

@pytest.mark.parametrize("level", [1, 2], ids=["sleep-1", "sleep-2"])
@create_new_process_for_each_test("fork" if current_platform.is_cuda() else "spawn")
@pytest.mark.skipif(not current_platform.is_cuda(), reason="cuMem CUDA graph pool")
@pytest.mark.skipif(
not current_platform.is_cuda_alike(), reason="cuMem CUDA graph pool"
)
def test_cudagraph_pool_sleep(level):
"""Routing, and the graph pool backed up at both sleep levels and restored
in place by any wake."""
Expand Down
62 changes: 57 additions & 5 deletions tests/basic_correctness/memory/sleep_mode/test_sleep_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,20 @@ def _cudagraph_bytes(worker) -> int:
def _custom_ar_active(worker) -> bool:
from vllm.distributed.parallel_state import get_tp_group

ca_comm = get_tp_group().device_communicator.ca_comm
return ca_comm is not None and not ca_comm.disabled
comm = get_tp_group().device_communicator
return any(
c is not None and not c.disabled for c in (comm.ca_comm, comm.aiter_ar_comm)
)


def _capture_registers_buffers(worker) -> bool:
from vllm.distributed.parallel_state import get_tp_group

comm = get_tp_group().device_communicator
ca, aiter = comm.ca_comm, comm.aiter_ar_comm
return (ca is not None and ca._capture_registered) or (
aiter is not None and aiter.aiter_ca.enable_register_for_capturing
)


@pytest.mark.parametrize(
Expand All @@ -227,7 +239,9 @@ def _custom_ar_active(worker) -> bool:
ids=["full", "piecewise", "breakable", "custom-ar-tp2", "off-by-default"],
Comment thread
mawong-amd marked this conversation as resolved.
)
@create_new_process_for_each_test()
@pytest.mark.skipif(not current_platform.is_cuda(), reason="cuMem CUDA graph pool")
@pytest.mark.skipif(
not current_platform.is_cuda_alike(), reason="cuMem CUDA graph pool"
)
def test_sleep_cudagraph_pool(monkeypatch, mode, breakable, tp, offload):
"""Each capture site uses the pool, exact across sleeps; TP=2 needs
unregistered custom AR; off changes nothing."""
Expand Down Expand Up @@ -255,8 +269,10 @@ def test_sleep_cudagraph_pool(monkeypatch, mode, breakable, tp, offload):
assert all(graph_bytes) if offload else not any(graph_bytes)
if not offload:
return
if tp > 1 and not all(llm.collective_rpc(_custom_ar_active)):
pytest.skip("Custom allreduce is unavailable on these GPUs")
if tp > 1:
if not all(llm.collective_rpc(_custom_ar_active)):
pytest.skip("Custom allreduce is unavailable on these GPUs")
assert not any(llm.collective_rpc(_capture_registers_buffers))
prompt, params = "How are you?", SamplingParams(temperature=0, max_tokens=10)
expected = llm.generate(prompt, params)[0].outputs[0].text
for level in (1, 2):
Expand All @@ -268,6 +284,42 @@ def test_sleep_cudagraph_pool(monkeypatch, mode, breakable, tp, offload):
assert llm.generate(prompt, params)[0].outputs[0].text == expected


@multi_gpu_test(num_gpus=2)
@create_new_process_for_each_test()
@pytest.mark.skipif(not current_platform.is_rocm(), reason="AITER fused allreduce")
def test_sleep_cudagraph_pool_aiter_fused_ar_group_quant(monkeypatch):
"""The fused AITER AR + RMSNorm + per-group FP8 quant captures into the
cuMem pool without IPC-registering its input, across sleeps."""
for name, value in [
("VLLM_USE_V2_MODEL_RUNNER", "1"),
("VLLM_ROCM_USE_AITER", "1"),
("VLLM_ALLOW_INSECURE_SERIALIZATION", "1"),
]:
monkeypatch.setenv(name, value)
monkeypatch.delenv("NCCL_GRAPH_REGISTER", raising=False)
llm = LLM(
"Qwen/Qwen3-0.6B-FP8",
enable_sleep_mode=True,
sleep_mode_offload_cudagraph=True,
tensor_parallel_size=2,
max_model_len=1024,
compilation_config={"pass_config": {"fuse_allreduce_rms": True}},
)
if not all(llm.collective_rpc(_custom_ar_active)):
pytest.skip("AITER custom allreduce is unavailable on these GPUs")
assert not any(llm.collective_rpc(_capture_registers_buffers))
assert all(llm.collective_rpc(_cudagraph_bytes))
prompt, params = "How are you?", SamplingParams(temperature=0, max_tokens=10)
assert llm.generate(prompt, params)[0].outputs[0].text
for level in (1, 2):
llm.sleep(level=level)
llm.wake_up(tags=["weights"])
if level == 2:
llm.collective_rpc("reload_weights")
llm.wake_up(tags=["kv_cache"])
assert llm.generate(prompt, params)[0].outputs[0].text


@create_new_process_for_each_test()
def test_deep_sleep_async(gpu_memory_cleared):
async def test():
Expand Down
6 changes: 4 additions & 2 deletions vllm/_aiter_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -1167,7 +1167,8 @@ def _rocm_aiter_fused_allreduce_rmsnorm_quant_per_group_impl(
w=weight,
eps=epsilon,
group_size=group_size,
registered=torch.cuda.is_current_stream_capturing(),
registered=torch.cuda.is_current_stream_capturing()
and ca.enable_register_for_capturing,
use_1stage=use_1stage,
)
assert result is not None
Expand Down Expand Up @@ -1219,7 +1220,8 @@ def _rocm_aiter_fused_allreduce_rmsnorm_quant_per_group_with_bf16_norm_impl(
w=weight,
eps=epsilon,
group_size=group_size,
registered=torch.cuda.is_current_stream_capturing(),
registered=torch.cuda.is_current_stream_capturing()
and ca.enable_register_for_capturing,
use_1stage=use_1stage,
emit_bf16=True,
)
Expand Down
2 changes: 1 addition & 1 deletion vllm/config/vllm.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,7 @@ def use_cumem_cudagraph_pool(self) -> bool:
and model_config.enable_sleep_mode
and model_config.sleep_mode_backend == "cumem"
and self.compilation_config.cudagraph_mode != CUDAGraphMode.NONE
and current_platform.is_cuda()
and current_platform.is_cuda_alike()
)

@property
Expand Down
8 changes: 6 additions & 2 deletions vllm/device_allocator/cumem.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class CuMemAllocator:
offloaded to CPU memory, and the rest of the tensors will be discarded.
When we call `wake_up`, all tensors that are previously offloaded
will be loaded back to GPU memory, and the rest of the tensors will
have empty memory.
have empty memory (zeroed on ROCm).

Why it needs to be a singleton?
When allocated tensors are garbage collected, PyTorch will call
Expand Down Expand Up @@ -331,7 +331,7 @@ def discard(self, tags: tuple[str, ...] | str) -> None:
def wake_up(self, tags: list[str] | None = None) -> None:
"""Wake up the allocator from sleep mode.
All data that is previously offloaded will be loaded back to GPU
memory, and the rest of the data will have empty memory.
memory, and the rest will have empty memory (zeroed on ROCm).

Args:
tags: The deferrable tags (weights, kv_cache) to load back to GPU
Expand All @@ -358,6 +358,10 @@ def wake_up(self, tags: list[str] | None = None) -> None:
cpu_ptr = cpu_backup_tensor.data_ptr()
libcudart.cudaMemcpy(ptr, cpu_ptr, size_in_bytes)
data.cpu_backup_tensor = None
elif current_platform.is_rocm():
# amdgpu <6.14 may return stale VRAM (#44972); drop with its support
libcudart.cudaMemset(ptr, 0, handle[1])
torch.accelerator.synchronize()

@contextmanager
def use_memory_pool(self, tag: str | None = None):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ def __init__(
group: ProcessGroup,
device: int | str | torch.device,
max_size: int | None = None,
*,
register_graph_buffers: bool = True,
):
from aiter.dist.device_communicators.custom_all_reduce import (
CustomAllreduce as _AiterCustomAllreduce,
Expand All @@ -38,7 +40,10 @@ def __init__(
if max_size is None:
max_size = self.MAX_SIZE

self._impl = _AiterCustomAllreduce(group, device, max_size=max_size)
kwargs = {}
if not register_graph_buffers:
kwargs["enable_register_for_capturing"] = False
Comment thread
mawong-amd marked this conversation as resolved.
self._impl = _AiterCustomAllreduce(group, device, max_size=max_size, **kwargs)

@property
def aiter_ca(self):
Expand Down
2 changes: 2 additions & 0 deletions vllm/distributed/device_communicators/cuda_communicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ def __init__(
self.aiter_ar_comm = AiterCustomAllreduce(
group=self.cpu_group,
device=self.device,
register_graph_buffers=register,
)

if use_custom_allreduce and self.aiter_ar_comm is None and self.world_size > 1:
Expand All @@ -174,6 +175,7 @@ def __init__(
self.aiter_ar_comm = AiterCustomAllreduce(
group=self.cpu_group,
device=self.device,
register_graph_buffers=register,
)
if self.aiter_ar_comm.disabled:
self.aiter_ar_comm = None
Expand Down
Loading