Skip to content

Commit 99c0875

Browse files
committed
Account V2 FlashInfer cudagraph workspace
Signed-off-by: lesj0610 <lesj0610@users.noreply.github.com>
1 parent ab613f9 commit 99c0875

2 files changed

Lines changed: 288 additions & 6 deletions

File tree

tests/v1/worker/test_cudagraph_memory_profiling.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,3 +505,180 @@ def null_context(*args, **kwargs):
505505
assert warmup_calls[2][1]["profile_seq_lens"] == 1
506506
assert warmup_calls[3][1]["profile_seq_lens"] is None
507507
assert cleanup_calls == ["cleanup"]
508+
509+
510+
def test_v2_profile_accounts_attention_workspace(monkeypatch):
511+
from vllm.v1.worker.gpu import model_runner as gpu_model_runner_v2
512+
from vllm.v1.worker.gpu.model_runner import GPUModelRunner
513+
514+
@contextlib.contextmanager
515+
def null_context(*args, **kwargs):
516+
yield
517+
518+
runner = GPUModelRunner.__new__(GPUModelRunner)
519+
runner.vllm_config = object()
520+
521+
events = []
522+
523+
monkeypatch.setattr(
524+
gpu_model_runner_v2,
525+
"set_current_vllm_config",
526+
lambda *args, **kwargs: null_context(),
527+
)
528+
529+
def reserve_attention_workspace():
530+
events.append("reserve")
531+
return 4096
532+
533+
runner._init_minimal_kv_cache_for_profiling = lambda: events.append("init")
534+
runner._reserve_attention_workspace_for_cudagraph_capture = (
535+
reserve_attention_workspace
536+
)
537+
runner._cleanup_profiling_kv_cache = lambda: events.append("cleanup")
538+
539+
estimate = runner.profile_cudagraph_memory()
540+
541+
assert estimate == 4096
542+
assert runner.cudagraph_memory_persistent_estimate == 4096
543+
assert runner.cudagraph_memory_graph_pool_estimate == 0
544+
assert events == ["init", "reserve", "cleanup"]
545+
546+
547+
def test_v2_cleanup_profiling_kv_cache_releases_builder_refs(monkeypatch):
548+
from vllm.v1.worker.gpu import model_runner as gpu_model_runner_v2
549+
from vllm.v1.worker.gpu.model_runner import GPUModelRunner
550+
551+
class Builder:
552+
pass
553+
554+
builder = Builder()
555+
builder_ref = weakref.ref(builder)
556+
layer = SimpleNamespace(
557+
kv_cache=torch.empty(1),
558+
impl=SimpleNamespace(_k_scale_cache=object(), _v_scale_cache=object()),
559+
)
560+
561+
runner = GPUModelRunner.__new__(GPUModelRunner)
562+
runner.cache_config = SimpleNamespace(num_gpu_blocks=4)
563+
runner.kv_caches = [torch.empty(1)]
564+
runner.attn_groups = [[SimpleNamespace(metadata_builders=[builder])]]
565+
runner.kv_cache_config = object()
566+
runner.block_tables = object()
567+
runner.kernel_block_sizes = [16]
568+
runner.cudagraph_manager = object()
569+
runner.compilation_config = SimpleNamespace(static_forward_context={"layer": layer})
570+
del builder
571+
572+
monkeypatch.setattr(
573+
gpu_model_runner_v2.torch.accelerator, "synchronize", lambda: None
574+
)
575+
monkeypatch.setattr(
576+
gpu_model_runner_v2.torch.accelerator, "empty_cache", lambda: None
577+
)
578+
579+
runner._cleanup_profiling_kv_cache()
580+
gc.collect()
581+
582+
assert runner.kv_caches == []
583+
assert not hasattr(runner, "attn_groups")
584+
assert not hasattr(runner, "kv_cache_config")
585+
assert not hasattr(runner, "block_tables")
586+
assert not hasattr(runner, "kernel_block_sizes")
587+
assert not hasattr(runner, "cudagraph_manager")
588+
assert runner.cache_config.num_gpu_blocks is None
589+
assert isinstance(layer.kv_cache, torch.Tensor)
590+
assert layer.kv_cache.numel() == 0
591+
assert layer.impl._k_scale_cache is None
592+
assert layer.impl._v_scale_cache is None
593+
assert builder_ref() is None
594+
595+
596+
def test_v2_capture_reserves_workspace_before_measurement_and_locks(monkeypatch):
597+
from vllm.v1.worker.gpu import model_runner as gpu_model_runner_v2
598+
from vllm.v1.worker.gpu.model_runner import GPUModelRunner
599+
600+
@contextlib.contextmanager
601+
def null_context(*args, **kwargs):
602+
yield
603+
604+
class Builder:
605+
reserved = False
606+
607+
def reserve_workspace_for_cudagraph_capture(self):
608+
events.append("builder_reserve")
609+
self.reserved = True
610+
611+
class FakeCudaGraphManager:
612+
def needs_capture(self):
613+
return True
614+
615+
def capture(
616+
self,
617+
model,
618+
model_state,
619+
input_buffers,
620+
intermediate_tensors,
621+
block_tables,
622+
attn_groups,
623+
kv_cache_config,
624+
**kwargs,
625+
):
626+
events.append("capture")
627+
assert attn_groups[0][0].metadata_builders[0].reserved
628+
return {}
629+
630+
events = []
631+
builder = Builder()
632+
runner = GPUModelRunner.__new__(GPUModelRunner)
633+
runner.device = torch.device("cpu")
634+
runner.cudagraph_manager = FakeCudaGraphManager()
635+
runner.lora_config = None
636+
runner.maybe_setup_dummy_loras = lambda lora_config: null_context()
637+
runner.model = object()
638+
runner.model_state = object()
639+
runner.input_buffers = object()
640+
runner.intermediate_tensors = None
641+
runner.block_tables = object()
642+
runner.attn_groups = [[SimpleNamespace(metadata_builders=[builder])]]
643+
runner.kv_cache_config = object()
644+
runner.use_aux_hidden_state_outputs = False
645+
runner.speculator = None
646+
647+
memory_reserved_values = iter([1_000, 1_128])
648+
get_memory_info_values = iter([(10_000, 0), (9_000, 0)])
649+
650+
monkeypatch.setattr(
651+
gpu_model_runner_v2.torch.accelerator, "synchronize", lambda: None
652+
)
653+
monkeypatch.setattr(
654+
gpu_model_runner_v2.torch.accelerator, "empty_cache", lambda: None
655+
)
656+
monkeypatch.setattr(
657+
gpu_model_runner_v2.torch.accelerator,
658+
"memory_reserved",
659+
lambda device: next(memory_reserved_values),
660+
)
661+
662+
def get_memory_info():
663+
events.append("memory_info")
664+
return next(get_memory_info_values)
665+
666+
monkeypatch.setattr(
667+
gpu_model_runner_v2.torch.accelerator,
668+
"get_memory_info",
669+
get_memory_info,
670+
)
671+
monkeypatch.setattr(
672+
gpu_model_runner_v2,
673+
"lock_workspace",
674+
lambda: events.append("lock"),
675+
)
676+
677+
assert runner.capture_model() == 1_000
678+
assert events == [
679+
"builder_reserve",
680+
"memory_info",
681+
"capture",
682+
"memory_info",
683+
"lock",
684+
]

vllm/v1/worker/gpu/model_runner.py

Lines changed: 111 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
import vllm.envs as envs
3131
from vllm.compilation.counter import compilation_counter
32-
from vllm.config import VllmConfig
32+
from vllm.config import VllmConfig, set_current_vllm_config
3333
from vllm.config.compilation import CUDAGraphMode
3434
from vllm.distributed.parallel_state import (
3535
get_dcp_group,
@@ -50,6 +50,7 @@
5050
from vllm.utils.torch_utils import PIN_MEMORY, STR_DTYPE_TO_TORCH_DTYPE
5151
from vllm.v1.core.sched.output import GrammarOutput, SchedulerOutput
5252
from vllm.v1.kv_cache_interface import KVCacheConfig, MambaSpec
53+
from vllm.v1.kv_cache_spec_registry import KVCacheSpecRegistry
5354
from vllm.v1.outputs import DraftTokenIds, ModelRunnerOutput
5455
from vllm.v1.worker.cp_utils import check_attention_cp_compatibility
5556
from vllm.v1.worker.gpu.async_utils import AsyncOutput, AsyncPoolingOutput
@@ -113,6 +114,7 @@
113114
from vllm.v1.worker.gpu.structured_outputs import StructuredOutputsWorker
114115
from vllm.v1.worker.lora_model_runner_mixin import LoRAModelRunnerMixin
115116
from vllm.v1.worker.utils import KVBlockZeroer
117+
from vllm.v1.worker.workspace import lock_workspace
116118

117119
logger = init_logger(__name__)
118120

@@ -402,7 +404,9 @@ def main_stream(self) -> torch.cuda.Stream:
402404
def get_kv_cache_spec(self):
403405
return get_kv_cache_spec(self.vllm_config)
404406

405-
def initialize_kv_cache(self, kv_cache_config: KVCacheConfig) -> None:
407+
def initialize_kv_cache(
408+
self, kv_cache_config: KVCacheConfig, is_profiling: bool = False
409+
) -> None:
406410
kv_cache_config = deepcopy(kv_cache_config)
407411
self.kv_cache_config = kv_cache_config
408412

@@ -475,7 +479,7 @@ def initialize_kv_cache(self, kv_cache_config: KVCacheConfig) -> None:
475479
self.speculator.init_cudagraph_manager(cudagraph_mode)
476480

477481
check_attention_cp_compatibility(self.vllm_config)
478-
if isinstance(self.speculator, DraftModelSpeculator):
482+
if isinstance(self.speculator, DraftModelSpeculator) and not is_profiling:
479483
# HACK(woosuk)
480484
self.speculator.set_attn(
481485
self.model_state, self.kv_cache_config, self.block_tables
@@ -492,7 +496,8 @@ def initialize_kv_cache(self, kv_cache_config: KVCacheConfig) -> None:
492496
self.kernel_block_sizes,
493497
self.vllm_config,
494498
)
495-
self.kv_connector = get_kv_connector(self.vllm_config, kv_caches_dict)
499+
if not is_profiling:
500+
self.kv_connector = get_kv_connector(self.vllm_config, kv_caches_dict)
496501

497502
def _init_kv_zero_meta(self) -> None:
498503
"""Build KV-block zeroing metadata; invoked from gpu_worker."""
@@ -682,9 +687,106 @@ def _get_num_input_tokens(self, num_scheduled_tokens: int) -> int:
682687
# SP is not supported yet.
683688
return num_scheduled_tokens
684689

690+
def _init_minimal_kv_cache_for_profiling(self) -> None:
691+
from vllm.v1.core.kv_cache_utils import (
692+
get_kv_cache_config_from_groups,
693+
get_kv_cache_groups,
694+
)
695+
696+
kv_cache_spec = self.get_kv_cache_spec()
697+
KVCacheSpecRegistry.check_kv_cache_spec_registry(kv_cache_spec)
698+
kv_cache_groups = get_kv_cache_groups(self.vllm_config, kv_cache_spec)
699+
min_blocks = self.compilation_config.max_cudagraph_capture_size or 1
700+
701+
# Temporarily allocate just enough KV cache state to instantiate
702+
# attention metadata builders for workspace sizing.
703+
saved_override = self.cache_config.num_gpu_blocks_override
704+
self.cache_config.num_gpu_blocks_override = min_blocks
705+
try:
706+
minimal_config = get_kv_cache_config_from_groups(
707+
self.vllm_config,
708+
kv_cache_groups,
709+
available_memory=0,
710+
)
711+
finally:
712+
self.cache_config.num_gpu_blocks_override = saved_override
713+
714+
self.initialize_kv_cache(minimal_config, is_profiling=True)
715+
self.cache_config.num_gpu_blocks = minimal_config.num_blocks
716+
717+
logger.debug("Initialized minimal KV cache for CUDA graph profiling")
718+
719+
def _cleanup_profiling_kv_cache(self) -> None:
720+
torch.accelerator.synchronize()
721+
722+
if hasattr(self, "kv_caches") and self.kv_caches:
723+
for i in range(len(self.kv_caches)):
724+
self.kv_caches[i] = None # type: ignore[assignment]
725+
self.kv_caches.clear()
726+
if hasattr(self, "attn_groups"):
727+
self.attn_groups.clear()
728+
for attr in (
729+
"attn_groups",
730+
"kv_cache_config",
731+
"block_tables",
732+
"kernel_block_sizes",
733+
"cudagraph_manager",
734+
):
735+
if hasattr(self, attr):
736+
delattr(self, attr)
737+
self.cache_config.num_gpu_blocks = None
738+
739+
for layer in self.compilation_config.static_forward_context.values():
740+
if hasattr(layer, "kv_cache"):
741+
kv_cache = layer.kv_cache
742+
layer.kv_cache = (
743+
torch.tensor([]) if isinstance(kv_cache, torch.Tensor) else []
744+
)
745+
# Clean up quantized KV cache scale views
746+
# (int8_per_token_head, fp8_per_token_head).
747+
if hasattr(layer, "impl"):
748+
if hasattr(layer.impl, "_k_scale_cache"):
749+
layer.impl._k_scale_cache = None
750+
if hasattr(layer.impl, "_v_scale_cache"):
751+
layer.impl._v_scale_cache = None
752+
753+
gc.collect()
754+
torch.accelerator.empty_cache()
755+
logger.debug("Cleaned up profiling KV cache and CUDA graphs")
756+
757+
def _reserve_attention_workspace_for_cudagraph_capture(self) -> int:
758+
if not getattr(self, "attn_groups", None):
759+
return 0
760+
761+
reserved_before = torch.accelerator.memory_reserved(self.device)
762+
for groups in self.attn_groups:
763+
for attn_group in groups:
764+
for builder in attn_group.metadata_builders:
765+
builder.reserve_workspace_for_cudagraph_capture()
766+
torch.accelerator.synchronize()
767+
torch.accelerator.empty_cache()
768+
reserved_after = torch.accelerator.memory_reserved(self.device)
769+
return max(reserved_after - reserved_before, 0)
770+
685771
def profile_cudagraph_memory(self) -> int:
686-
# NOTE(woosuk): It is TBD whether we keep this API or not.
687-
return 0
772+
self.cudagraph_memory_persistent_estimate = 0
773+
self.cudagraph_memory_graph_pool_estimate = 0
774+
775+
try:
776+
with set_current_vllm_config(self.vllm_config):
777+
self._init_minimal_kv_cache_for_profiling()
778+
persistent_estimate = (
779+
self._reserve_attention_workspace_for_cudagraph_capture()
780+
)
781+
finally:
782+
self._cleanup_profiling_kv_cache()
783+
784+
self.cudagraph_memory_persistent_estimate = int(persistent_estimate)
785+
logger.info(
786+
"Estimated CUDA graph persistent workspace memory: %.2f GiB",
787+
persistent_estimate / (1 << 30),
788+
)
789+
return int(persistent_estimate)
688790

689791
@torch.inference_mode()
690792
def capture_model(self) -> int:
@@ -700,6 +802,8 @@ def capture_model(self) -> int:
700802

701803
start_time = time.perf_counter()
702804
gc.collect()
805+
self._reserve_attention_workspace_for_cudagraph_capture()
806+
torch.accelerator.synchronize()
703807
torch.accelerator.empty_cache()
704808
start_free_gpu_memory = torch.accelerator.get_memory_info()[0]
705809

@@ -729,6 +833,7 @@ def capture_model(self) -> int:
729833
elapsed_time,
730834
cuda_graph_size / (1 << 30),
731835
)
836+
lock_workspace()
732837
return cuda_graph_size
733838

734839
def _remove_request(self, req_id: str) -> bool:

0 commit comments

Comments
 (0)