[Perf] Avoid some more unnecessary GPU<->CPU syncs (#51458)
Signed-off-by: Nick Hill <nickhill123@gmail.com>
Nick Hill committed
Aug 8, 2026 at 17:13 UTC
9b0afeb4f6c435a06d394f8a25209d4682a215af
13 files changed
+99
-49
tests/basic_correctness/test_basic_correctness.py
+2
-1
@@ -307,7 +307,8 @@ def test_raise_on_logit_nans(
307
308
def compute_nan_logits(self, *args, **kwargs):
309
logits = original_compute_logits(self, *args, **kwargs)
310
- logits[0, 0] = float("nan")
310
+ # `logits[0, 0] = ...` copies the scalar H2D and blocks.
311
+ logits[0, 0].fill_(float("nan"))
312
return logits
313
314
monkeypatch.setattr(model_cls, "compute_logits", compute_nan_logits)
tests/v1/e2e/general/test_mamba_prefix_cache.py
+7
-7
@@ -19,6 +19,7 @@ from vllm.distributed import cleanup_dist_env_and_memory
19
from vllm.model_executor.layers.mamba.mamba_utils import MambaStateCopyFunc
20
from vllm.platforms import current_platform
21
from vllm.sequence import IntermediateTensors
22
+from vllm.utils.torch_utils import async_tensor_h2d
23
from vllm.v1.attention.backends.utils import CommonAttentionMetadata
24
from vllm.v1.core.kv_cache_manager import KVCacheBlocks, KVCacheManager
25
from vllm.v1.core.sched.output import SchedulerOutput
@@ -77,7 +78,7 @@ def get_fake_sample_fn() -> SamplerOutput:
78
first_token_id_index = num_computed_tokens + 1
79
if spec_decode_metadata is None:
80
return SamplerOutput(
80
- sampled_token_ids=torch.tensor(
81
+ sampled_token_ids=async_tensor_h2d(
82
[[prompt_token_ids[first_token_id_index]]],
83
device=DEVICE_TYPE,
84
dtype=torch.int32,
@@ -90,7 +91,7 @@ def get_fake_sample_fn() -> SamplerOutput:
91
]
92
sampled_token_ids = accepted_tokens
93
return SamplerOutput(
93
- sampled_token_ids=torch.tensor(
94
+ sampled_token_ids=async_tensor_h2d(
95
[sampled_token_ids],
96
device=DEVICE_TYPE,
97
dtype=torch.int32,
@@ -132,7 +133,7 @@ def get_fake_propose_draft_token_ids_fn():
133
]
134
]
135
135
- next_token_ids = torch.tensor(
136
+ next_token_ids = async_tensor_h2d(
137
prompt_token_ids[
138
first_token_id_index - 1 : first_token_id_index
139
- 1
@@ -142,7 +143,7 @@ def get_fake_propose_draft_token_ids_fn():
143
dtype=torch.int32,
144
)
145
145
- valid_sampled_tokens_count = torch.tensor(
146
+ valid_sampled_tokens_count = async_tensor_h2d(
147
[num_accepted_tokens],
148
device=DEVICE_TYPE,
149
dtype=torch.int32,
@@ -150,7 +151,7 @@ def get_fake_propose_draft_token_ids_fn():
151
152
self._copy_valid_sampled_token_count(next_token_ids, valid_sampled_tokens_count)
153
153
- return torch.tensor(
154
+ return async_tensor_h2d(
155
proposed_draft_token_ids,
156
device=DEVICE_TYPE,
157
dtype=torch.int32,
@@ -1088,11 +1089,10 @@ def _run_mamba_prefix_cache_mrv2(
1089
device=hidden_states.device,
1090
dtype=torch.int64,
1091
)
1091
- num_logits = torch.tensor(
1092
+ num_logits = async_tensor_h2d(
1093
input_batch.cu_num_logits_np[1 : num_reqs + 1]
1094
- input_batch.cu_num_logits_np[:num_reqs],
1095
device=hidden_states.device,
1095
- dtype=torch.int32,
1096
)
1097
accepted = torch.full_like(num_logits, num_accepted_tokens)
1098
num_sampled = torch.minimum(accepted, num_logits)
tests/v1/logits_processors/utils.py
+14
-6
@@ -14,6 +14,7 @@ from vllm.config import VllmConfig
14
from vllm.exceptions import VLLMValidationError
15
from vllm.logger import init_logger
16
from vllm.sampling_params import SamplingParams
17
+from vllm.utils.torch_utils import async_tensor_h2d
18
from vllm.v1.sample.logits_processor import (
19
LOGITSPROCS_GROUP,
20
AdapterLogitsProcessor,
@@ -90,17 +91,24 @@ class DummyLogitsProcessor(LogitsProcessor):
91
if not self.req_info:
92
return logits
93
93
- # Save target values before modification
94
- cols = torch.tensor(
94
+ # Save target values before modification.
95
+ cols = async_tensor_h2d(
96
list(self.req_info.values()), dtype=torch.long, device=logits.device
97
)
97
- rows = torch.tensor(
98
+ rows = async_tensor_h2d(
99
list(self.req_info.keys()), dtype=torch.long, device=logits.device
100
)
101
values_to_keep = logits[rows, cols].clone()
102
102
- # Mask all but target tokens
103
- logits[rows] = float("-inf")
103
+ # Mask all but target tokens. Use an on-device fill tensor so the
104
+ # scatter doesn't force a synchronizing scalar H2D.
105
+ fill = torch.full(
106
+ (rows.numel(), logits.size(-1)),
107
+ float("-inf"),
108
+ dtype=logits.dtype,
109
+ device=logits.device,
110
+ )
111
+ logits[rows] = fill
112
logits[rows, cols] = values_to_keep
113
114
return logits
@@ -141,7 +149,7 @@ class DummyPerReqLogitsProcessor:
149
output_ids: list[int],
150
logits: torch.Tensor,
151
) -> torch.Tensor:
144
- val_to_keep = logits[self.target_token].item()
152
+ val_to_keep = logits[self.target_token].clone()
153
logits[:] = float("-inf")
154
logits[self.target_token] = val_to_keep
155
return logits
vllm/distributed/kv_transfer/kv_connector/utils.py
+8
-3
@@ -20,6 +20,7 @@ from vllm.distributed.kv_transfer.kv_connector.factory import KVConnectorFactory
20
from vllm.logger import init_logger
21
from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
22
from vllm.platforms import current_platform
23
+from vllm.utils.torch_utils import async_tensor_h2d
24
from vllm.v1.attention.backend import AttentionBackend
25
from vllm.v1.outputs import KVConnectorOutput, ModelRunnerOutput
26
@@ -179,9 +180,13 @@ def _make_src_and_dst_indices(
180
src_device: torch.device | str,
181
dst_device: torch.device | str,
182
) -> tuple[torch.Tensor, torch.Tensor]:
182
- src_indices = torch.tensor(src_block_ids, device=src_device, dtype=torch.int64)
183
- dst_indices = torch.tensor(dst_block_ids, device=dst_device, dtype=torch.int64)
184
- return src_indices, dst_indices
183
+ def _to(block_ids: list[int], device: torch.device | str) -> torch.Tensor:
184
+ device = torch.device(device) if isinstance(device, str) else device
185
+ if device.type == "cpu":
186
+ return torch.tensor(block_ids, dtype=torch.int64, device=device)
187
+ return async_tensor_h2d(block_ids, dtype=torch.int64, device=device)
188
+
189
+ return _to(src_block_ids, src_device), _to(dst_block_ids, dst_device)
190
191
192
def copy_kv_blocks(
vllm/distributed/kv_transfer/kv_connector/v1/example_connector.py
+4
-3
@@ -135,6 +135,7 @@ class ExampleConnector(KVConnectorBase_V1):
135
slot_mapping (torch.Tensor): the slot mapping. In shape
136
[num_tokens].
137
"""
138
+ slot_mapping = slot_mapping.to(dst_kv_cache_layer.device, non_blocking=True)
139
if isinstance(attn_metadata, MLACommonMetadata):
140
dst_kv_cache_layer_shape = dst_kv_cache_layer.shape
141
num_pages = dst_kv_cache_layer_shape[0]
@@ -178,9 +179,8 @@ class ExampleConnector(KVConnectorBase_V1):
179
filename = self._generate_filename_debug(
180
layer_name, request.token_ids, request.mm_hashes
181
)
181
- kv_cache = safetensors.torch.load_file(
182
- filename, device=str(kv_cache_layer.device)
183
- )["kv_cache"]
182
+ kv_cache_cpu = safetensors.torch.load_file(filename)["kv_cache"]
183
+ kv_cache = kv_cache_cpu.to("cuda", non_blocking=True)
184
if isinstance(attn_metadata, dict):
185
inject_kv_into_layer(
186
kv_cache_layer,
@@ -227,6 +227,7 @@ class ExampleConnector(KVConnectorBase_V1):
227
Assume the shape of the layer is (num_pages, page_size, xxx)
228
for MLA, and (num_pages, 2, page_size, xxx) otherwise.
229
"""
230
+ slot_mapping = slot_mapping.to(layer.device, non_blocking=True)
231
if isinstance(attn_metadata, MLACommonMetadata):
232
num_pages, page_size = layer.shape[0], layer.shape[1]
233
return layer.reshape(num_pages * page_size, -1)[slot_mapping, ...]
vllm/lora/ops/triton_ops/fused_moe_lora_op.py
+2
-2
@@ -11,7 +11,7 @@ from vllm.platforms import current_platform
11
from vllm.triton_utils import tl, triton
12
from vllm.triton_utils.allocation import set_triton_allocator
13
from vllm.utils.mem_utils import get_max_shared_memory_bytes
14
-from vllm.utils.torch_utils import direct_register_custom_op
14
+from vllm.utils.torch_utils import async_tensor_h2d, direct_register_custom_op
15
16
from .utils import supports_pdl, supports_tma
17
@@ -866,7 +866,7 @@ def _get_ptr(lora_weights: list[torch.Tensor], device: torch.device):
866
tensor_ptrs = []
867
for lora_weight in lora_weights:
868
tensor_ptrs.append(lora_weight.data_ptr())
869
- ptr_tensor = torch.tensor(tensor_ptrs, device=device, dtype=torch.uint64)
869
+ ptr_tensor = async_tensor_h2d(tensor_ptrs, dtype=torch.uint64, device=device)
870
871
_LORA_PTR_DICT[key] = ptr_tensor
872
return _LORA_PTR_DICT.get(key)
vllm/model_executor/models/chameleon.py
+32
-4
@@ -55,6 +55,7 @@ from vllm.multimodal.processing import (
55
)
56
from vllm.sequence import IntermediateTensors
57
from vllm.utils.tensor_schema import TensorSchema, TensorShape
58
+from vllm.utils.torch_utils import async_tensor_h2d
59
60
from .interfaces import (
61
MultiModalEmbeddings,
@@ -819,8 +820,20 @@ class ChameleonImageVocabularyMapping:
820
821
def convert_img2bpe(self, img_batch: torch.Tensor) -> torch.Tensor:
822
device = img_batch.device
822
- img_tokens = self.img2bpe_mapping_tensor[img_batch.to("cpu")]
823
- return img_tokens.to(device)
823
+ # Cache a per-device copy of the (small, static) mapping tensor so we
824
+ # can index entirely on `device` instead of forcing a D2H on
825
+ # `img_batch` and an H2D on the result.
826
+ cache = getattr(self, "_img2bpe_mapping_cache", None)
827
+ if cache is None:
828
+ cache = {}
829
+ self._img2bpe_mapping_cache = cache
830
+ mapping_on_device = cache.get(device)
831
+ if mapping_on_device is None:
832
+ mapping_on_device = async_tensor_h2d(
833
+ self.img2bpe_mapping_tensor, device=device
834
+ )
835
+ cache[device] = mapping_on_device
836
+ return mapping_on_device[img_batch]
837
838
839
class ChameleonModel(nn.Module):
@@ -1025,8 +1038,23 @@ class ChameleonForConditionalGeneration(
1038
# Disallow image tokens which does not include special
1039
# begin-image and end-image tokens
1040
if logits is not None:
1028
- image_tokens = self.model.vocabulary_mapping.image_tokens
1029
- logits[:, image_tokens] = torch.finfo(logits.dtype).min
1041
+ # Cache a per-device index tensor for the (static) image-token
1042
+ # set, and use `index_fill_` instead of advanced-index assign
1043
+ # so the scatter runs entirely on device (no host roundtrip
1044
+ # for the scalar fill value or for the Python-list indices).
1045
+ cache = getattr(self, "_image_tokens_index_cache", None)
1046
+ if cache is None:
1047
+ cache = {}
1048
+ self._image_tokens_index_cache = cache
1049
+ image_tokens_idx = cache.get(logits.device)
1050
+ if image_tokens_idx is None:
1051
+ image_tokens_idx = async_tensor_h2d(
1052
+ self.model.vocabulary_mapping.image_tokens,
1053
+ dtype=torch.long,
1054
+ device=logits.device,
1055
+ )
1056
+ cache[logits.device] = image_tokens_idx
1057
+ logits.index_fill_(1, image_tokens_idx, torch.finfo(logits.dtype).min)
1058
1059
return logits
1060
vllm/model_executor/models/gemma3n_mm.py
+13
-4
@@ -54,6 +54,7 @@ from vllm.multimodal.processing.processor import (
54
)
55
from vllm.sequence import IntermediateTensors
56
from vllm.utils.tensor_schema import TensorSchema, TensorShape
57
+from vllm.utils.torch_utils import async_tensor_h2d
58
59
from .interfaces import MultiModalEmbeddings, SupportsMultiModal, SupportsTranscription
60
from .utils import (
@@ -683,10 +684,18 @@ class Gemma3nForConditionalGeneration(
684
# We handle both cases:
685
# - If fewer tokens: pad with the embedding of the last vocab token
686
# - If more tokens: truncate to the expected count
686
- # TODO precompute and cache padding
687
- audio_padding_toks = torch.tensor(
688
- [[self.vocab_size - 1]], dtype=torch.long, device=audio_features.device
689
- )
687
+ # Cache the single-scalar padding-token tensor per-device to avoid a
688
+ # synchronous H2D tensor construction on every forward.
689
+ cache = getattr(self, "_audio_padding_toks_cache", None)
690
+ if cache is None:
691
+ cache = {}
692
+ self._audio_padding_toks_cache = cache
693
+ audio_padding_toks = cache.get(audio_features.device)
694
+ if audio_padding_toks is None:
695
+ audio_padding_toks = async_tensor_h2d(
696
+ [[self.vocab_size - 1]], dtype=torch.long, device=audio_features.device
697
+ )
698
+ cache[audio_features.device] = audio_padding_toks
699
audio_padding_embs = self.embed_audio(input_ids=audio_padding_toks)
700
audio_features = torch.where(
701
audio_mask.unsqueeze(-1), audio_padding_embs, audio_features
vllm/model_executor/models/glm4_1v.py
+11
-4
@@ -99,6 +99,7 @@ from vllm.sequence import IntermediateTensors
99
from vllm.transformers_utils.processor import get_processor_cls_name_from_config
100
from vllm.transformers_utils.utils import convert_model_repo_to_path
101
from vllm.utils.tensor_schema import TensorSchema, TensorShape
102
+from vllm.utils.torch_utils import async_tensor_h2d
103
from vllm.v1.attention.backends.registry import AttentionBackendEnum
104
from vllm.v1.worker.encoder_cudagraph_defs import EncoderCudaGraphReplayBuffers
105
@@ -535,7 +536,8 @@ class Glm4vVisionEmbeddings(nn.Module):
536
total_seq = h_coords.shape[0]
537
device = pos_embed_weight.device
538
538
- # Move coordinates to correct device
539
+ # Move coordinates to correct device. The only caller already hands
540
+ # these over pinned + non-blocking, so this is normally a no-op.
541
h_coords, w_coords = h_coords.to(device), w_coords.to(device)
542
543
# Handle empty sequence case
@@ -816,10 +818,15 @@ class Glm4vVisionTransformer(nn.Module):
818
)
819
820
lengths = [h * w] * t
819
- image_shapes = torch.tensor([[t, h, w]], device=device)
821
+ image_shapes = async_tensor_h2d(
822
+ [[t, h, w]], dtype=torch.long, device=device
823
+ )
824
821
- h_coords_repeated = h_coords.repeat(t)
822
- w_coords_repeated = w_coords.repeat(t)
825
+ # Build the coordinates on the host (cheap integer math) but move
826
+ # them across pinned + non-blocking, so the consumer's
827
+ # `.to(device)` below is a no-op rather than a blocking H2D.
828
+ h_coords_repeated = async_tensor_h2d(h_coords.repeat(t), device=device)
829
+ w_coords_repeated = async_tensor_h2d(w_coords.repeat(t), device=device)
830
831
embeds = self.embeddings(
832
embeddings=torch.zeros(
vllm/model_executor/models/qwen3_omni_moe_thinker.py
+1
-1
@@ -437,7 +437,7 @@ class Qwen3OmniMoeAudioEncoder(nn.Module):
437
# Compute chunk information
438
chunk_num = torch.ceil(feature_lens / (self.n_window * 2)).long()
439
440
- chunk_lengths = torch.tensor(
440
+ chunk_lengths = async_tensor_h2d(
441
[self.n_window * 2] * chunk_num.sum(),
442
dtype=torch.long,
443
device=feature_lens.device,
vllm/model_executor/models/voxtral.py
+3
-7
@@ -773,15 +773,11 @@ class VoxtralEncoderModel(nn.Module):
773
if global_log_mel_max := self.config.global_log_mel_max:
774
if not isinstance(global_log_mel_max, float):
775
raise TypeError(f"{global_log_mel_max=} needs to be of type float.")
776
- log_spec_max = torch.tensor(
777
- global_log_mel_max,
778
- device=log_spec.device,
779
- dtype=log_spec.dtype,
780
- )
776
+ # Use `clamp` to avoid gpu<->cpu sync.
777
+ log_spec = torch.clamp(log_spec, min=global_log_mel_max - 8.0)
778
else:
779
log_spec_max = log_spec.max()
783
-
784
- log_spec = torch.maximum(log_spec, log_spec_max - 8.0)
780
+ log_spec = torch.maximum(log_spec, log_spec_max - 8.0)
781
log_spec = (log_spec + 4.0) / 4.0
782
return log_spec.to(input_dtype)
783
vllm/v1/spec_decode/extract_hidden_states.py
+1
-6
@@ -13,7 +13,6 @@ from vllm.distributed.eplb.eplb_state import EplbState
13
from vllm.forward_context import set_forward_context
14
from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
15
from vllm.model_executor.model_loader import get_model
16
-from vllm.utils.torch_utils import PIN_MEMORY
16
from vllm.v1.attention.backend import AttentionMetadataBuilder, CommonAttentionMetadata
17
from vllm.v1.cudagraph_dispatcher import CudagraphDispatcher
18
from vllm.v1.utils import CpuGpuBuffer
@@ -59,11 +58,7 @@ class ExtractHiddenStatesProposer:
58
)
59
60
self.backup_next_token_ids = CpuGpuBuffer(
62
- max_batch_size,
63
- dtype=torch.int32,
64
- pin_memory=PIN_MEMORY,
65
- device=device,
66
- with_numpy=True,
61
+ max_batch_size, dtype=torch.int32, device=device
62
)
63
64
self.hf_config = vllm_config.speculative_config.draft_model_config.hf_config
vllm/v1/worker/gpu_model_runner.py
+1
-1
@@ -3069,7 +3069,7 @@ class GPUModelRunner(
3069
3070
self._cache_encoder_output(
3071
mm_hashes[i],
3072
- pe_tensor.to(self.device),
3072
+ async_tensor_h2d(pe_tensor, device=self.device),
3073
scheduler_output.ec_manager_metadata,
3074
scheduler_output.free_encoder_mm_hashes,
3075
)