[Perf] Ensure async h2d copies are pinned in more places (#55202)

Nick Hill committed Sep 4, 2026 at 17:17 UTC 8277c42e4c74c5dd604f29b6d938a3456658f247
21 files changed +206 -73
tests/kernels/attention/test_flashinfer.py
+2
@@ -190,6 +190,7 @@ def test_fast_plan_decode_warmup_uses_full_plan(dtype: torch.dtype) -> None:
190 indptr_cpu=kv_indptr,
191 indices=kv_indices,
192 last_page_len_cpu=kv_last_page_lens,
193 + seq_lens_cpu=torch.tensor(kv_lens, dtype=torch.int32, device="cpu"),
194 num_qo_heads=num_query_heads,
195 num_kv_heads=num_kv_heads,
196 head_dim=head_size,
@@ -268,6 +269,7 @@ def test_fast_plan_decode_matches_full_plan(
269 indptr_cpu=kv_indptr,
270 indices=kv_indices_buf,
271 last_page_len_cpu=kv_last_page_lens,
272 + seq_lens_cpu=torch.tensor(kv_lens, dtype=torch.int32, device="cpu"),
273 num_qo_heads=num_query_heads,
274 num_kv_heads=num_kv_heads,
275 head_dim=head_size,
vllm/distributed/eplb/eplb_state.py
+10 -1
@@ -49,6 +49,7 @@ from vllm.logger import init_logger
49 from vllm.model_executor.models.interfaces import MixtureOfExperts
50 from vllm.platforms import current_platform
51 from vllm.utils.gpu_sync_debug import gpu_sync_allowed
52 +from vllm.utils.torch_utils import PIN_MEMORY
53
54 from .async_worker import start_async_worker
55 from .eplb_communicator import EplbCommunicator, create_eplb_communicator
@@ -1298,6 +1299,8 @@ def _commit_eplb_maps_for_layer(
1299 f"Current number of physical experts: {dst.shape[0]}. New number of physical "
1300 f"experts {src.shape[0]}."
1301 )
1302 + if PIN_MEMORY and src.is_cpu:
1303 + src = src.new_empty(src.shape, pin_memory=True).copy_(src)
1304 dst.copy_(src, non_blocking=True)
1305
1306 num_logical_experts = model_state.logical_to_physical_map.shape[1]
@@ -1309,6 +1312,8 @@ def _commit_eplb_maps_for_layer(
1312 src = new_replica_count
1313 dst = model_state.logical_replica_count[layer]
1314 assert src.shape == dst.shape
1315 + if PIN_MEMORY:
1316 + src = src.pin_memory()
1317 dst.copy_(src, non_blocking=True)
1318
1319
@@ -1326,12 +1331,14 @@ def _commit_eplb_maps(
1331 src = new_physical_to_logical_map
1332 dst = model_state.physical_to_logical_map
1333
1334 + if PIN_MEMORY and src.is_cpu:
1335 + src = src.new_empty(src.shape, pin_memory=True).copy_(src)
1336 # Rare Case: When the number of physical experts has changed, discard the old
1337 # physical to logical expert map and use the new one. This only happens when the
1338 # number of GPUs available to vLLM changes while vLLM is running. Otherwise copy the
1339 # new map into the old one.
1340 if src.shape[1] != dst.shape[1]:
1334 - model_state.physical_to_logical_map = src.to(dst.device)
1341 + model_state.physical_to_logical_map = src.to(dst.device, non_blocking=True)
1342 else:
1343 dst.copy_(src, non_blocking=True)
1344
@@ -1346,6 +1353,8 @@ def _commit_eplb_maps(
1353 # Commit logical_replica_count
1354 src = new_replica_count
1355 dst = model_state.logical_replica_count
1356 + if PIN_MEMORY:
1357 + src = src.pin_memory()
1358 dst.copy_(src, non_blocking=True)
1359
1360
vllm/distributed/kv_transfer/kv_connector/v1/example_hidden_states_connector.py
+6 -4
@@ -21,6 +21,7 @@ from vllm.distributed.kv_transfer.kv_connector.v1.base import (
21 )
22 from vllm.distributed.parallel_state import get_tensor_model_parallel_rank
23 from vllm.logger import init_logger
24 +from vllm.utils.gpu_sync_debug import gpu_sync_allowed
25 from vllm.v1.attention.backend import AttentionMetadata
26 from vllm.v1.core.sched.output import SchedulerOutput
27
@@ -377,10 +378,11 @@ class ExampleHiddenStatesConnector(KVConnectorBase_V1, SupportsHMA):
378 with torch.cuda.stream(copy_stream):
379 # Move the CPU slot_mapping to GPU on the copy stream so the
380 # implicit H2D inside fancy indexing doesn't sync the default
380 - # stream.
381 - slot_mapping_gpu = slot_mapping.to(
382 - device=self._kv_cache.device, non_blocking=True
383 - )
381 + # stream. Deliberate bulk transfer in this reference connector.
382 + with gpu_sync_allowed():
383 + slot_mapping_gpu = slot_mapping.to(
384 + device=self._kv_cache.device, non_blocking=True
385 + )
386 hidden_states_gpu = extract_from_kv_cache(
387 self._kv_cache, slot_mapping_gpu, num_tokens
388 )
vllm/model_executor/layers/attention/mla_attention.py
+8 -2
@@ -265,6 +265,7 @@ from vllm.platforms import current_platform
265 from vllm.utils.flashinfer import has_flashinfer
266 from vllm.utils.math_utils import cdiv, round_down, round_up
267 from vllm.utils.torch_utils import (
268 + PIN_MEMORY,
269 LayerNameType,
270 _encode_layer_name,
271 _resolve_layer_name,
@@ -2510,8 +2511,13 @@ class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
2511 query_start_loc_cpu[reqs_start + 1 : num_reqs + 1]
2512 - query_start_loc_cpu[reqs_start:num_reqs]
2513 )
2513 - context_lens_cpu = (
2514 - seq_lens_cpu[reqs_start:num_reqs] - prefill_query_lens_cpu
2514 + context_lens_cpu = torch.empty(
2515 + num_prefills, dtype=seq_lens_cpu.dtype, pin_memory=PIN_MEMORY
2516 + )
2517 + torch.subtract(
2518 + seq_lens_cpu[reqs_start:num_reqs],
2519 + prefill_query_lens_cpu,
2520 + out=context_lens_cpu,
2521 )
2522 prefill_query_start_loc = (
2523 query_start_loc[reqs_start:] - query_start_loc[reqs_start]
vllm/model_executor/layers/fused_moe/modular_kernel.py
+5 -1
@@ -34,6 +34,7 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
34 QuantKey,
35 )
36 from vllm.platforms import current_platform
37 +from vllm.utils.torch_utils import PIN_MEMORY
38 from vllm.v1.worker.ubatching import (
39 dbo_enabled,
40 dbo_maybe_run_recv_hook,
@@ -109,7 +110,10 @@ class ExpertTokensMetadata:
110 expert_num_tokens_list: list[int], device: str
111 ) -> "ExpertTokensMetadata":
112 expert_num_tokens_cpu = torch.tensor(
112 - expert_num_tokens_list, device="cpu", dtype=torch.int32
113 + expert_num_tokens_list,
114 + device="cpu",
115 + dtype=torch.int32,
116 + pin_memory=PIN_MEMORY,
117 )
118 return ExpertTokensMetadata(
119 expert_num_tokens=expert_num_tokens_cpu.to(device, non_blocking=True),
vllm/model_executor/models/cosmos3_edge.py
+2 -4
@@ -21,6 +21,7 @@ from vllm.multimodal import MULTIMODAL_REGISTRY
21 from vllm.multimodal.inputs import MultiModalFeatureSpec
22 from vllm.sequence import IntermediateTensors
23 from vllm.transformers_utils.configs.nemotron_h import NemotronHConfig
24 +from vllm.utils.torch_utils import async_tensor_h2d
25
26 from .interfaces import (
27 MultiModalEmbeddings,
@@ -80,10 +81,7 @@ class Cosmos3EdgeVisionEncoder(Siglip2VisionTransformer):
81 dim=0,
82 )
83 lengths_cpu = spatial_shapes.prod(dim=-1).to(torch.int32)
83 - lengths = lengths_cpu.to(
84 - device=pixel_values.device,
85 - non_blocking=True,
86 - )
84 + lengths = async_tensor_h2d(lengths_cpu, pixel_values.device)
85
86 cu_seqlens = torch.zeros(
87 lengths.numel() + 1,
vllm/model_executor/models/ernie45_vl.py
+4 -4
@@ -538,7 +538,7 @@ class Ernie4_5_VisionTransformer(nn.Module):
538 else:
539 max_seqlen = self.compute_attn_mask_seqlen(cu_seqlens)
540
541 - cu_seqlens = cu_seqlens.to(device, non_blocking=True)
541 + cu_seqlens = async_tensor_h2d(cu_seqlens, device)
542
543 return {
544 "rotary_pos_emb": rotary_pos_emb,
@@ -1746,8 +1746,8 @@ class Ernie4_5_VLMoeForConditionalGeneration(
1746 # Eager fallback: run the full pipeline (ViT + resampler). The result
1747 # is scattered directly, so it must be the post-merge embeddings.
1748 pixel_values = mm_kwargs["pixel_values"].type(self.vision_model.dtype)
1749 - grid_thw = mm_kwargs["image_grid_thw"].to(
1750 - self.vision_model.device, non_blocking=True
1749 + grid_thw = async_tensor_h2d(
1750 + mm_kwargs["image_grid_thw"], self.vision_model.device
1751 )
1752 image_features = self.vision_model(pixel_values, grid_thw)
1753 return self.resampler_model(image_features, grid_thw)
@@ -1768,7 +1768,7 @@ class Ernie4_5_VLMoeForConditionalGeneration(
1768 output = outputs["default"]
1769 assert batch_mm_kwargs is not None
1770 grid_thw_cpu = batch_mm_kwargs["image_grid_thw"]
1771 - grid_thw = grid_thw_cpu.to(output.device, non_blocking=True)
1771 + grid_thw = async_tensor_h2d(grid_thw_cpu, output.device)
1772 # The valid token count slices the graph output for the eager
1773 # resampler call, so it has to come back to the host.
1774 num_valid = int(
vllm/model_executor/models/glm_ocr.py
+6 -2
@@ -70,6 +70,7 @@ from vllm.model_executor.models.glm4_1v import (
70 Glm4vVisionTransformer,
71 )
72 from vllm.multimodal import MULTIMODAL_REGISTRY
73 +from vllm.utils.torch_utils import PIN_MEMORY
74
75 from .utils import (
76 maybe_prefix,
@@ -344,8 +345,11 @@ class GlmOcrVisionTransformer(Glm4vVisionTransformer):
345 cu_seqlens = torch.repeat_interleave(
346 grid_thw[:, 1] * grid_thw[:, 2], grid_thw[:, 0]
347 ).cumsum(dim=0, dtype=torch.int32)
347 - cu_seqlens = torch.cat([cu_seqlens.new_zeros(1), cu_seqlens])
348 - cu_seqlens = cu_seqlens.to(self.device, non_blocking=True)
348 + # cat straight into a pinned buffer so the H2D copy stays non-blocking.
349 + cu_seqlens = torch.cat(
350 + [cu_seqlens.new_zeros(1), cu_seqlens],
351 + out=cu_seqlens.new_empty(cu_seqlens.numel() + 1, pin_memory=PIN_MEMORY),
352 + ).to(self.device, non_blocking=True)
353
354 # pre-compute max_seqlen for attn mask to reduce cuMemcpy operations
355 max_seqlen = self.compute_attn_mask_seqlen(cu_seqlens)
vllm/model_executor/models/isaac.py
+9 -3
@@ -69,6 +69,7 @@ from vllm.transformers_utils.processors.isaac import (
69 get_image_size_for_max_num_patches,
70 )
71 from vllm.utils.tensor_schema import TensorSchema, TensorShape
72 +from vllm.utils.torch_utils import PIN_MEMORY, async_tensor_h2d
73
74 from .vision import is_vit_use_data_parallel
75
@@ -970,9 +971,14 @@ class IsaacForConditionalGeneration(
971 device = next(self.language_model.parameters()).device
972 dtype = self.vision_embedding.linear_fc1.weight.dtype
973 pixel_values = pixel_values.to(device=device, dtype=dtype)
973 - spatial_grids = image_grid_thw[:, 1:3].to(
974 - device, dtype=torch.int32, non_blocking=True
975 - )
974 + # The [:, 1:3] column slice isn't densely laid out, so stage it with a
975 + # single copy straight into pinned memory to keep the H2D non-blocking.
976 + spatial_grids_cpu = torch.empty(
977 + (image_grid_thw.shape[0], 2),
978 + dtype=image_grid_thw.dtype,
979 + pin_memory=PIN_MEMORY,
980 + ).copy_(image_grid_thw[:, 1:3])
981 + spatial_grids = async_tensor_h2d(spatial_grids_cpu, device, dtype=torch.int32)
982
983 vision_embeddings = self.vision_embedding((pixel_values, spatial_grids))
984 merge_size = self.config.vision_config.pixel_shuffle_scale_factor
vllm/model_executor/models/keye.py
+41 -16
@@ -59,7 +59,7 @@ from vllm.multimodal.processing import (
59 )
60 from vllm.sequence import IntermediateTensors
61 from vllm.utils.tensor_schema import TensorSchema, TensorShape
62 -from vllm.utils.torch_utils import async_tensor_h2d
62 +from vllm.utils.torch_utils import PIN_MEMORY, async_tensor_h2d
63
64 from .interfaces import (
65 MultiModalEmbeddings,
@@ -1305,17 +1305,29 @@ class BaseKeyeModule(nn.Module, SupportsMultiModal):
1305 )
1306 else:
1307 pixel_values = image_input["pixel_values"].type(self.visual.dtype)
1308 - # These are all built on the host, so stage them over
1309 - # non-blocking rather than forcing a sync per transfer.
1310 - siglip_position_ids = torch.concat(siglip_position_ids, dim=0).to(
1311 - pixel_values.device, non_blocking=True
1312 - )
1308 + # These are all built on the host; concat straight into pinned
1309 + # buffers so the H2D copies stay non-blocking.
1310 + siglip_position_ids = torch.concat(
1311 + siglip_position_ids,
1312 + dim=0,
1313 + out=torch.empty(
1314 + sum(t.numel() for t in siglip_position_ids),
1315 + dtype=torch.int64,
1316 + pin_memory=PIN_MEMORY,
1317 + ),
1318 + ).to(pixel_values.device, non_blocking=True)
1319 cu_seqlens = async_tensor_h2d(
1320 cu_seqlens, dtype=torch.int32, device=pixel_values.device
1321 )
1316 - sample_indices = torch.concat(sample_indices, dim=0).to(
1317 - pixel_values.device, non_blocking=True
1318 - )
1322 + sample_indices = torch.concat(
1323 + sample_indices,
1324 + dim=0,
1325 + out=torch.empty(
1326 + sum(t.numel() for t in sample_indices),
1327 + dtype=torch.int64,
1328 + pin_memory=PIN_MEMORY,
1329 + ),
1330 + ).to(pixel_values.device, non_blocking=True)
1331
1332 image_embeds = self.visual(
1333 pixel_values=pixel_values,
@@ -1359,16 +1371,29 @@ class BaseKeyeModule(nn.Module, SupportsMultiModal):
1371 )
1372 else:
1373 pixel_values_videos = pixel_values_videos.type(self.visual.dtype)
1362 - # These are host-built; stage them over non-blocking.
1363 - siglip_position_ids = torch.concat(siglip_position_ids, dim=0).to(
1364 - pixel_values_videos.device, non_blocking=True
1365 - )
1374 + # These are host-built; concat straight into pinned buffers so
1375 + # the H2D copies stay non-blocking.
1376 + siglip_position_ids = torch.concat(
1377 + siglip_position_ids,
1378 + dim=0,
1379 + out=torch.empty(
1380 + sum(t.numel() for t in siglip_position_ids),
1381 + dtype=torch.int64,
1382 + pin_memory=PIN_MEMORY,
1383 + ),
1384 + ).to(pixel_values_videos.device, non_blocking=True)
1385 cu_seqlens = async_tensor_h2d(
1386 cu_seqlens, dtype=torch.int32, device=pixel_values_videos.device
1387 )
1369 - sample_indices = torch.concat(sample_indices, dim=0).to(
1370 - pixel_values_videos.device, non_blocking=True
1371 - )
1388 + sample_indices = torch.concat(
1389 + sample_indices,
1390 + dim=0,
1391 + out=torch.empty(
1392 + sum(t.numel() for t in sample_indices),
1393 + dtype=torch.int64,
1394 + pin_memory=PIN_MEMORY,
1395 + ),
1396 + ).to(pixel_values_videos.device, non_blocking=True)
1397
1398 video_embeds = self.visual(
1399 pixel_values=pixel_values_videos,
vllm/model_executor/models/paddleocr_vl.py
+12 -5
@@ -68,7 +68,7 @@ from vllm.multimodal.processing import (
68 )
69 from vllm.sequence import IntermediateTensors
70 from vllm.utils.tensor_schema import TensorSchema, TensorShape
71 -from vllm.utils.torch_utils import async_tensor_h2d
71 +from vllm.utils.torch_utils import PIN_MEMORY, async_tensor_h2d
72 from vllm.v1.attention.backends.registry import AttentionBackendEnum
73
74 from .ernie45 import Ernie4_5ForCausalLM
@@ -1142,10 +1142,17 @@ class PaddleOCRVLForConditionalGeneration(nn.Module, SupportsMultiModal, Support
1142 siglip_position_ids.append(image_position_ids)
1143 cu_seqlens.append(cu_seqlens[-1] + numel)
1144
1145 - # Both are built on the host; stage them over non-blocking.
1146 - siglip_position_ids = torch.concat(siglip_position_ids, dim=0).to(
1147 - pixel_values.device, non_blocking=True
1148 - )
1145 + # Both are built on the host; concat straight into a pinned buffer
1146 + # so the H2D copy stays non-blocking.
1147 + siglip_position_ids = torch.concat(
1148 + siglip_position_ids,
1149 + dim=0,
1150 + out=torch.empty(
1151 + sum(t.numel() for t in siglip_position_ids),
1152 + dtype=torch.int64,
1153 + pin_memory=PIN_MEMORY,
1154 + ),
1155 + ).to(pixel_values.device, non_blocking=True)
1156 cu_seqlens = async_tensor_h2d(
1157 cu_seqlens, dtype=torch.int32, device=pixel_values.device
1158 )
vllm/model_executor/models/phi4mm_audio.py
+2 -2
@@ -643,8 +643,8 @@ class TransformerEncoderBase(abc.ABC, nn.Module):
643 seq_len, batch_size, chunk_size_nc, left_chunk_nc
644 )
645 if device.type != "cpu":
646 - enc_streaming_mask_nc = enc_streaming_mask_nc.contiguous().to(
647 - device, non_blocking=True
646 + enc_streaming_mask_nc = async_tensor_h2d(
647 + enc_streaming_mask_nc.contiguous(), device
648 )
649 if masks is not None:
650 hs_mask_nc = masks & enc_streaming_mask_nc
vllm/model_executor/models/qwen2_5_omni_thinker.py
+3 -2
@@ -91,6 +91,7 @@ from vllm.multimodal.processing.processor import (
91 from vllm.sequence import IntermediateTensors
92 from vllm.utils.gpu_sync_debug import gpu_sync_allowed
93 from vllm.utils.tensor_schema import TensorSchema, TensorShape
94 +from vllm.utils.torch_utils import async_tensor_h2d
95
96 from .interfaces import (
97 MultiModalEmbeddings,
@@ -985,8 +986,8 @@ class Qwen2_5OmniConditionalGenerationMixin:
986 input_features = audio_input["input_features"]
987 # audio_feature_lengths is keep_on_cpu; the audio tower derives
988 # device placement from feature_lens, so move it explicitly.
988 - audio_feature_lengths = audio_input["audio_feature_lengths"].to(
989 - input_features.device, non_blocking=True
989 + audio_feature_lengths = async_tensor_h2d(
990 + audio_input["audio_feature_lengths"], input_features.device
991 )
992
993 audio_feat_lengths, audio_output_lengths = (
vllm/model_executor/models/qwen3_omni_moe_thinker.py
+3 -3
@@ -1001,7 +1001,7 @@ class Qwen3Omni_VisionTransformer(nn.Module):
1001
1002 # Move cu_seqlens to GPU; grid_thw may be on CPU during profile_run
1003 # and FA3 vit attention requires cu_seqlens on CUDA.
1004 - cu_seqlens = cu_seqlens.to(self.device, non_blocking=True)
1004 + cu_seqlens = async_tensor_h2d(cu_seqlens, self.device)
1005 hidden_states = hidden_states.unsqueeze(1)
1006 rotary_pos_emb_cos = rotary_pos_emb_cos.to(hidden_states.device)
1007 rotary_pos_emb_sin = rotary_pos_emb_sin.to(hidden_states.device)
@@ -1574,8 +1574,8 @@ class Qwen3OmniMoeConditionalGenerationMixin(Qwen2_5OmniConditionalGenerationMix
1574 input_features = audio_input["input_features"]
1575 # audio_feature_lengths is keep_on_cpu; the audio tower derives
1576 # device placement from feature_lens, so move it explicitly.
1577 - audio_feature_lengths = audio_input["audio_feature_lengths"].to(
1578 - input_features.device, non_blocking=True
1577 + audio_feature_lengths = async_tensor_h2d(
1578 + audio_input["audio_feature_lengths"], input_features.device
1579 )
1580
1581 audio_output_lengths = _get_feat_extract_output_lengths(audio_feature_lengths)
vllm/model_executor/models/qwen3_vl.py
+7 -10
@@ -2367,8 +2367,8 @@ class Qwen3VLForConditionalGeneration(
2367 grid_thw_list = grid_thw.tolist()
2368 image_embeds_out = []
2369 for emb, size in zip(image_embeds_split, grid_thw_list):
2370 - positions = compute_mrope_for_media(size, merge_size).to(
2371 - emb.device, non_blocking=True
2370 + positions = async_tensor_h2d(
2371 + compute_mrope_for_media(size, merge_size), emb.device
2372 )
2373 positions = torch.cat(
2374 [
@@ -2596,14 +2596,11 @@ class Qwen3VLForConditionalGeneration(
2596 identifier="DUMMY",
2597 mm_position=PlaceholderRange(offset=0, length=len(unpruned_token_ids)),
2598 )
2599 - original_mrope = (
2600 - self.get_mrope_input_positions(
2601 - input_tokens=unpruned_token_ids,
2602 - mm_features=[mm_feature],
2603 - )[0]
2604 - .to(device, non_blocking=True)
2605 - .permute(1, 0)
2606 - )
2599 + original_mrope_cpu = self.get_mrope_input_positions(
2600 + input_tokens=unpruned_token_ids,
2601 + mm_features=[mm_feature],
2602 + )[0]
2603 + original_mrope = async_tensor_h2d(original_mrope_cpu, device).permute(1, 0)
2604 full_is_video_embed = unpruned_token_ids_tensor == embed_token_id
2605
2606 with gpu_sync_allowed():
vllm/utils/flashinfer.py
+26
@@ -21,6 +21,7 @@ import vllm.envs as envs
21 from vllm.logger import init_logger
22 from vllm.platforms import current_platform
23 from vllm.utils.math_utils import cdiv
24 +from vllm.utils.torch_utils import PIN_MEMORY
25
26 logger = init_logger(__name__)
27
@@ -181,6 +182,31 @@ def _lazy_import_wrapper(
182 return wrapper
183
184
185 +def pin_host_range_buf(length: int) -> None:
186 + """Pin flashinfer's cached host-side arange buffer covering `length`.
187 +
188 + flashinfer's decode plan() builds its host qo_indptr from the cached
189 + `_get_range_buf` entry and copies it to the GPU with non_blocking=True on
190 + every call; the cache entry is unpinned, so the copy silently blocks the
191 + host. Replace the entry with a pinned copy so the copies stay async.
192 + """
193 + if not PIN_MEMORY:
194 + return
195 + mod = _get_submodule("flashinfer.utils")
196 + if mod is None:
197 + return
198 + get_range_buf = getattr(mod, "_get_range_buf", None)
199 + cache_buf = getattr(mod, "_cache_buf", None)
200 + ceil_pow2 = getattr(mod, "_ceil_pow2", None)
201 + if get_range_buf is None or cache_buf is None or ceil_pow2 is None:
202 + return
203 + get_range_buf(length, "cpu") # Ensure the cache entry exists.
204 + key = (f"range_{ceil_pow2(length)}", "cpu")
205 + buf = cache_buf[key]
206 + if not buf.is_pinned():
207 + cache_buf[key] = buf.pin_memory()
208 +
209 +
210 # Create lazy wrappers for each function
211 flashinfer_trtllm_bf16_moe = _lazy_import_wrapper(
212 "flashinfer.fused_moe", "trtllm_bf16_moe"
vllm/v1/attention/backends/flashinfer.py
+34 -1
@@ -15,6 +15,7 @@ from flashinfer import (
15 BatchPrefillWithPagedKVCacheWrapper,
16 BatchPrefillWithRaggedKVCacheWrapper,
17 MultiLevelCascadeAttentionWrapper,
18 + get_seq_lens,
19 )
20 from flashinfer.decode import fast_decode_plan, trtllm_batch_decode_with_kv_cache
21 from flashinfer.prefill import trtllm_batch_context_with_kv_cache
@@ -43,6 +44,7 @@ from vllm.utils.flashinfer import (
44 can_use_trtllm_attention,
45 flashinfer_xqa_batch_decode_with_kv_cache,
46 force_use_trtllm_attention,
47 + pin_host_range_buf,
48 supports_trtllm_attention,
49 use_trtllm_attention,
50 )
@@ -299,6 +301,7 @@ class BatchDCPPrefillWrapper:
301 paged_kv_indptr_cpu: torch.Tensor,
302 paged_kv_indices: torch.Tensor,
303 paged_kv_last_page_len_cpu: torch.Tensor,
304 + kv_lens_cpu: torch.Tensor,
305 page_size: int,
306 num_qo_heads: int,
307 dcp_world_size: int,
@@ -318,6 +321,7 @@ class BatchDCPPrefillWrapper:
321 paged_kv_indptr=paged_kv_indptr_cpu,
322 paged_kv_indices=paged_kv_indices,
323 paged_kv_last_page_len=paged_kv_last_page_len_cpu,
324 + seq_lens=kv_lens_cpu,
325 num_qo_heads=num_qo_heads * dcp_world_size,
326 num_kv_heads=num_kv_heads,
327 head_dim_qk=head_dim,
@@ -1580,6 +1584,17 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1584 paged_kv_last_page_len_prefill_cpu = (
1585 paged_kv_last_page_len_prefill_cpu.pin_memory()
1586 )
1587 + # plan() also copies kv_lens to the GPU with non_blocking=True,
1588 + # deriving them via get_seq_lens() into unpinned memory when
1589 + # seq_lens is not passed; pass an explicit pinned copy of the
1590 + # same values so that copy stays async too.
1591 + kv_lens_prefill_cpu = get_seq_lens(
1592 + paged_kv_indptr_prefill_cpu,
1593 + paged_kv_last_page_len_prefill_cpu,
1594 + page_size,
1595 + )
1596 + if PIN_MEMORY:
1597 + kv_lens_prefill_cpu = kv_lens_prefill_cpu.pin_memory()
1598 if self.use_dcp:
1599 assert isinstance(prefill_wrapper, BatchDCPPrefillWrapper)
1600 prefill_wrapper.plan(
@@ -1587,6 +1602,7 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1602 paged_kv_indptr_cpu=paged_kv_indptr_prefill_cpu,
1603 paged_kv_indices=paged_kv_indices,
1604 paged_kv_last_page_len_cpu=paged_kv_last_page_len_prefill_cpu,
1605 + kv_lens_cpu=kv_lens_prefill_cpu,
1606 page_size=self.page_size,
1607 num_qo_heads=self.num_qo_heads,
1608 dcp_world_size=self.dcp_world_size,
@@ -1616,6 +1632,7 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1632 paged_kv_indptr=paged_kv_indptr_prefill_cpu,
1633 paged_kv_indices=paged_kv_indices,
1634 paged_kv_last_page_len=paged_kv_last_page_len_prefill_cpu,
1635 + seq_lens=kv_lens_prefill_cpu,
1636 num_qo_heads=self.num_qo_heads,
1637 num_kv_heads=self.num_kv_heads,
1638 head_dim_qk=self.head_dim,
@@ -1716,11 +1733,22 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1733 if PIN_MEMORY:
1734 paged_kv_indptr_cpu = paged_kv_indptr_cpu.pin_memory()
1735 paged_kv_last_page_len_cpu = paged_kv_last_page_len_cpu.pin_memory()
1736 + # plan() also copies kv_lens to the GPU with non_blocking=True
1737 + # (trtllm-gen/cute-dsl backends), deriving them via
1738 + # get_seq_lens() into unpinned memory when seq_lens is not
1739 + # passed; pass an explicit pinned copy of the same values
1740 + # so that copy stays async too.
1741 + kv_lens_decode_cpu = get_seq_lens(
1742 + paged_kv_indptr_cpu, paged_kv_last_page_len_cpu, page_size
1743 + )
1744 + if PIN_MEMORY:
1745 + kv_lens_decode_cpu = kv_lens_decode_cpu.pin_memory()
1746 fast_plan_decode(
1747 decode_wrapper,
1748 indptr_cpu=paged_kv_indptr_cpu,
1749 indices=paged_kv_indices,
1750 last_page_len_cpu=paged_kv_last_page_len_cpu,
1751 + seq_lens_cpu=kv_lens_decode_cpu,
1752 num_qo_heads=self.num_qo_heads * self.dcp_world_size,
1753 num_kv_heads=self.num_kv_heads,
1754 head_dim=self.head_dim,
@@ -2579,6 +2607,7 @@ def fast_plan_decode(
2607 indptr_cpu: torch.Tensor,
2608 indices: torch.Tensor,
2609 last_page_len_cpu: torch.Tensor,
2610 + seq_lens_cpu: torch.Tensor,
2611 num_qo_heads: int,
2612 num_kv_heads: int,
2613 head_dim: int,
@@ -2614,6 +2643,10 @@ def fast_plan_decode(
2643 # original plan if we run for dynamic shape. For fixed shape (cudagraph),
2644 # this warm up is to generate the _cached_module for the decode wrapper.
2645 if not self.is_cuda_graph_enabled or getattr(self, "vllm_first_call", True):
2646 + # plan() copies qo_indptr from flashinfer's internal cached host
2647 + # arange buffer (_get_range_buf) with non_blocking=True; pin the
2648 + # cache entry so the copy stays async.
2649 + pin_host_range_buf(len(last_page_len_cpu) + 1)
2650 self.plan(
2651 indptr=indptr_cpu,
2652 indices=indices,
@@ -2634,7 +2667,7 @@ def fast_plan_decode(
2667 rope_theta=rope_theta,
2668 non_blocking=non_blocking,
2669 block_tables=None,
2637 - seq_lens=None,
2670 + seq_lens=seq_lens_cpu,
2671 fixed_split_size=fixed_split_size,
2672 disable_split_kv=disable_split_kv,
2673 )
vllm/v1/attention/backends/hpc_attn.py
+8 -2
@@ -19,6 +19,7 @@ from vllm.config.cache import CacheDType
19 from vllm.logger import init_logger
20 from vllm.platforms import current_platform
21 from vllm.platforms.interface import DeviceCapability
22 +from vllm.utils.torch_utils import PIN_MEMORY
23 from vllm.v1.attention.backend import (
24 AttentionBackend,
25 AttentionCGSupport,
@@ -216,8 +217,13 @@ class HpcAttnMetadataBuilder(AttentionMetadataBuilder[HpcAttnMetadata]):
217 if num_prefills > 0:
218 qo_indptr_cpu = common_attn_metadata.query_start_loc_cpu
219 prefill_start = num_decodes
219 - qo_indptr_prefill_cpu = (
220 - qo_indptr_cpu[prefill_start:] - qo_indptr_cpu[prefill_start]
220 + qo_indptr_prefill_cpu = torch.empty(
221 + num_prefills + 1, dtype=torch.int32, pin_memory=PIN_MEMORY
222 + )
223 + torch.subtract(
224 + qo_indptr_cpu[prefill_start:],
225 + qo_indptr_cpu[prefill_start],
226 + out=qo_indptr_prefill_cpu,
227 )
228 qo_indptr = qo_indptr_prefill_cpu.to(self.device, non_blocking=True)
229
vllm/v1/attention/backends/rocm_aiter_fa.py
+10 -5
@@ -21,7 +21,11 @@ from vllm.platforms.interface import DeviceCapability
21 from vllm.utils.gpu_sync_debug import gpu_sync_allowed
22 from vllm.utils.math_utils import cdiv
23 from vllm.utils.platform_utils import num_compute_units
24 -from vllm.utils.torch_utils import get_dtype_size, is_quantized_kv_cache
24 +from vllm.utils.torch_utils import (
25 + async_tensor_h2d,
26 + get_dtype_size,
27 + is_quantized_kv_cache,
28 +)
29 from vllm.v1.attention.backend import (
30 AttentionBackend,
31 AttentionCGSupport,
@@ -593,6 +597,7 @@ class AiterFlashAttentionMetadataBuilder(
597 num_extends + 1,
598 dtype=torch.int32,
599 device=seq_lens_for_extend.device,
600 + pin_memory=True,
601 )
602 torch.cumsum(
603 swa_seqlen_for_extend,
@@ -623,8 +628,8 @@ class AiterFlashAttentionMetadataBuilder(
628
629 swa_metadata = AiterChunkSlidingWindowMetadata(
630 swa_cu_seqlens=cu_seq_lens.to(self.device, non_blocking=True),
626 - swa_seq_starts=seq_starts.to(self.device, non_blocking=True),
627 - swa_token_to_batch=token_to_seq.to(self.device, non_blocking=True),
631 + swa_seq_starts=async_tensor_h2d(seq_starts, self.device),
632 + swa_token_to_batch=async_tensor_h2d(token_to_seq, self.device),
633 swa_max_seqlens=max_seqlen_k,
634 swa_total_tokens=total_tokens,
635 swa_workspace=swa_workspace,
@@ -668,8 +673,8 @@ class AiterFlashAttentionMetadataBuilder(
673 chunk_context_metadata = AiterChunkContextMetadata(
674 workspace=self.extend_workspace,
675 cu_seq_lens_chunk=cu_seq_lens_cpu.to(self.device, non_blocking=True),
671 - chunk_starts=chunk_starts.to(self.device, non_blocking=True),
672 - token_to_batch=token_to_batch_tensor.to(self.device, non_blocking=True),
676 + chunk_starts=async_tensor_h2d(chunk_starts, self.device),
677 + token_to_batch=async_tensor_h2d(token_to_batch_tensor, self.device),
678 max_seq_lens=chunk_seq_lens.max(dim=1).values.tolist(),
679 num_chunks=num_chunks,
680 total_token_per_batch=cu_seq_lens_cpu[:, -1].tolist(),
vllm/v1/worker/dp_utils.py
+3 -1
@@ -10,6 +10,7 @@ from vllm.config import ParallelConfig
10 from vllm.distributed.parallel_state import get_dp_group
11 from vllm.logger import init_logger
12 from vllm.utils.gpu_sync_debug import gpu_sync_allowed
13 +from vllm.utils.torch_utils import PIN_MEMORY
14 from vllm.v1.worker.ubatch_utils import (
15 check_ubatch_thresholds,
16 is_last_ubatch_empty,
@@ -47,7 +48,8 @@ def _run_ar(
48 dp_rank = parallel_config.data_parallel_rank
49 device, group = _get_device_and_group(parallel_config)
50 # Populate this rank's contribution on CPU to reduce GPU syncs.
50 - tensor_cpu = torch.zeros(4, dp_size, dtype=torch.int32)
51 + pin_memory = PIN_MEMORY and torch.device(device).type == "cuda"
52 + tensor_cpu = torch.zeros(4, dp_size, dtype=torch.int32, pin_memory=pin_memory)
53 tensor_cpu[0][dp_rank] = orig_num_tokens_per_ubatch
54 tensor_cpu[1][dp_rank] = padded_num_tokens_per_ubatch
55 tensor_cpu[2][dp_rank] = 1 if should_ubatch else 0
vllm/v1/worker/gpu_model_runner.py
+5 -5
@@ -2258,8 +2258,10 @@ class GPUModelRunner(
2258 if self.use_async_spec_decode and (self.uses_mrope or self.uses_xdrope_dim > 0):
2259 drift = self.num_computed_tokens[req_indices_gpu].to(
2260 torch.int64
2261 - ) - self.input_batch.num_computed_tokens_cpu_tensor[req_indices].to(
2262 - device=self.device, dtype=torch.int64, non_blocking=True
2261 + ) - async_tensor_h2d(
2262 + self.input_batch.num_computed_tokens_cpu_tensor[req_indices],
2263 + device=self.device,
2264 + dtype=torch.int64,
2265 )
2266 target = self.mrope_positions if self.uses_mrope else self.xdrope_positions
2267 target.gpu[:, :total_num_scheduled_tokens] += drift
@@ -6324,9 +6326,7 @@ class GPUModelRunner(
6326 self.eplb_step(is_dummy=True, is_profile=is_profile)
6327
6328 logit_indices = np.cumsum(num_scheduled_tokens) - 1
6327 - logit_indices_device = torch.from_numpy(logit_indices).to(
6328 - self.device, non_blocking=True
6329 - )
6329 + logit_indices_device = async_tensor_h2d(logit_indices, device=self.device)
6330 return hidden_states, hidden_states[logit_indices_device]
6331
6332 @torch.inference_mode()