[Core] Check for GPU<->CPU syncs during CI (#43107)
Signed-off-by: Nick Hill <nickhill123@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Nick Hill committed
Aug 14, 2026 at 17:42 UTC
615d4cfadeb3d5ea1df248eb59aa128af5dbd441
44 files changed
+607
-364
.buildkite/test_areas/plugins.yaml
+6
@@ -75,6 +75,9 @@ steps:
75
- vllm/model_executor/layers/quantization
76
- tests/plugins_tests/bitsandbytes
77
commands:
78
+ # bitsandbytes' int8 quant syncs internally, and it is out-of-tree, so
79
+ # there is nothing to wrap on the vLLM side.
80
+ - unset VLLM_GPU_SYNC_CHECK
81
- pip install "vllm-bnb-plugin >= 0.0.1"
82
- pytest -v -s plugins_tests/bitsandbytes -m 'not distributed'
83
@@ -89,5 +92,8 @@ steps:
92
- vllm/model_executor/layers/quantization
93
- tests/plugins_tests/bitsandbytes
94
commands:
95
+ # bitsandbytes' int8 quant syncs internally, and it is out-of-tree, so
96
+ # there is nothing to wrap on the vLLM side.
97
+ - unset VLLM_GPU_SYNC_CHECK
98
- pip install "vllm-bnb-plugin >= 0.0.1"
99
- pytest -v -s plugins_tests/bitsandbytes -m 'distributed(num_gpus=2)'
docker/Dockerfile
+3
@@ -965,6 +965,9 @@ ENV HF_XET_HIGH_PERFORMANCE 1
965
# increase timeout for hf downloads (for testing)
966
ENV HF_HUB_DOWNLOAD_TIMEOUT 60
967
968
+# Catch GPU<->CPU syncs in execute_model/sample_tokens
969
+ENV VLLM_GPU_SYNC_CHECK=error
970
+
971
# Copy in the v1 package for testing (it isn't distributed yet)
972
COPY vllm/v1 /usr/local/lib/python${PYTHON_VERSION}/dist-packages/vllm/v1
973
docker/Dockerfile.rocm
+3
@@ -820,6 +820,9 @@ COPY --from=export_vllm /vllm_v1 /usr/local/lib/python${PYTHON_VERSION}/dist-pac
820
# Hide source under src/ so it won't shadow the installed package in tests.
821
RUN mkdir src && mv vllm src/vllm
822
823
+# Catch GPU<->CPU syncs in execute_model/sample_tokens during tests.
824
+ENV VLLM_GPU_SYNC_CHECK=error
825
+
826
# -----------------------
827
# Final vLLM image
828
FROM mori_base AS final
tests/models/multimodal/generation/test_mm_prefix_lm.py
+4
-1
@@ -8,6 +8,7 @@ import torch
8
from transformers import AutoModelForImageTextToText
9
10
from vllm.platforms import current_platform
11
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
12
13
from ....conftest import HfRunner, ImageTestAssets, VllmRunner
14
from .vlm_utils import model_utils
@@ -30,7 +31,9 @@ def _install_prefill_hidden_capture(model):
31
def forward(*args, **kwargs):
32
hidden_states = original_forward(*args, **kwargs)
33
if model._prefill_hidden is None and torch.is_tensor(hidden_states):
33
- model._prefill_hidden = hidden_states.detach().float().cpu()
34
+ # Capturing hidden states for comparison is a deliberate D2H.
35
+ with gpu_sync_allowed():
36
+ model._prefill_hidden = hidden_states.detach().float().cpu()
37
return hidden_states
38
39
language_model.forward = forward
tests/v1/e2e/general/test_mamba_prefix_cache.py
+28
-17
@@ -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.gpu_sync_debug import gpu_sync_allowed
23
from vllm.utils.torch_utils import async_tensor_h2d
24
from vllm.v1.attention.backends.utils import CommonAttentionMetadata
25
from vllm.v1.core.kv_cache_manager import KVCacheBlocks, KVCacheManager
@@ -981,7 +982,10 @@ def _run_mamba_prefix_cache_mrv2(
982
yield forward_context[layer_name].kv_cache[-1], block_table
983
984
def temporal_block(temporal_state, block_table, col):
984
- return temporal_state[int(block_table[0, col].item())]
985
+ # Resolving the block id for assertions is a deliberate D2H.
986
+ with gpu_sync_allowed():
987
+ block_id = int(block_table[0, col].item())
988
+ return temporal_state[block_id]
989
990
def wrapped_preprocess_state(
991
self: MambaHybridModelState,
@@ -1005,19 +1009,24 @@ def _run_mamba_prefix_cache_mrv2(
1009
self, input_batch, block_tables, kv_cache_config, num_computed_tokens
1010
)
1011
if cur_step_action is not None:
1008
- req_idx = int(input_batch.idx_mapping[0].item())
1009
- src_col = int(self._mamba_src_col_gpu[req_idx].item())
1010
- off = int(self._mamba_src_off_gpu[req_idx].item())
1011
- dst = int(self._mamba_state_idx_gpu[req_idx].item())
1012
+ # Reading the GPU-side copy state back to assert on it is a
1013
+ # deliberate D2H.
1014
+ with gpu_sync_allowed():
1015
+ req_idx = int(input_batch.idx_mapping[0].item())
1016
+ src_col = int(self._mamba_src_col_gpu[req_idx].item())
1017
+ off = int(self._mamba_src_off_gpu[req_idx].item())
1018
+ dst = int(self._mamba_state_idx_gpu[req_idx].item())
1019
actual = (-1, -1) if src_col < 0 or src_col == dst else (src_col + off, dst)
1020
assert actual == expected, (
1021
f"V2 align preprocess copy: expected={expected}, "
1022
f"actual={actual}, {cur_step_action=}"
1023
)
1017
- for temporal, bt, src_state in snapshots:
1018
- torch.testing.assert_close(
1019
- temporal_block(temporal, bt, expected[1]), src_state
1020
- )
1024
+ # Comparing device tensors for the assertion is a deliberate D2H.
1025
+ with gpu_sync_allowed():
1026
+ for temporal, bt, src_state in snapshots:
1027
+ torch.testing.assert_close(
1028
+ temporal_block(temporal, bt, expected[1]), src_state
1029
+ )
1030
return ret
1031
1032
def wrapped_postprocess_state(
@@ -1048,10 +1057,12 @@ def _run_mamba_prefix_cache_mrv2(
1057
ret = original_postprocess_state(
1058
self, idx_mapping, num_sampled, num_computed_tokens
1059
)
1051
- for temporal, bt, src_state in snapshots:
1052
- torch.testing.assert_close(
1053
- temporal_block(temporal, bt, expected[1]), src_state
1054
- )
1060
+ # Comparing device tensors for the assertion is a deliberate D2H.
1061
+ with gpu_sync_allowed():
1062
+ for temporal, bt, src_state in snapshots:
1063
+ torch.testing.assert_close(
1064
+ temporal_block(temporal, bt, expected[1]), src_state
1065
+ )
1066
return ret
1067
1068
def wrapped_execute_model(
@@ -1072,10 +1083,10 @@ def _run_mamba_prefix_cache_mrv2(
1083
ret = original_execute_model(self, scheduler_output, *args, **kwargs)
1084
if cur_step_action is not None and self.execute_model_state is not None:
1085
input_batch = self.execute_model_state.input_batch
1075
- assert (
1076
- cur_step_action.num_computed_tokens_start
1077
- == input_batch.positions[input_batch.query_start_loc[0]].item()
1078
- )
1086
+ # Reading positions back to assert on them is a deliberate D2H.
1087
+ with gpu_sync_allowed():
1088
+ start_pos = input_batch.positions[input_batch.query_start_loc[0]].item()
1089
+ assert cur_step_action.num_computed_tokens_start == start_pos
1090
return ret
1091
1092
def fake_sample(
vllm/distributed/eplb/eplb_communicator.py
+6
-4
@@ -33,6 +33,7 @@ from vllm.distributed.stateless_coordinator import StatelessGroupCoordinator
33
from vllm.distributed.utils import is_weak_contiguous
34
from vllm.logger import init_logger
35
from vllm.platforms import current_platform
36
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
37
38
logger = init_logger(__name__)
39
@@ -222,10 +223,11 @@ class TorchDistGlooStagedEplbCommunicator(EplbCommunicator):
223
224
# Wait for all D2H copies to finish
225
# before issuing gloo batch_isend_irecv operations.
225
- if self._cuda_stream is not None:
226
- self._cuda_stream.synchronize()
227
- else:
228
- torch.cuda.current_stream().synchronize()
226
+ with gpu_sync_allowed():
227
+ if self._cuda_stream is not None:
228
+ self._cuda_stream.synchronize()
229
+ else:
230
+ torch.cuda.current_stream().synchronize()
231
232
reqs = batch_isend_irecv(p2p_ops)
233
for req in reqs:
vllm/distributed/eplb/eplb_state.py
+21
-18
@@ -47,6 +47,7 @@ from vllm.distributed.utils import StatelessProcessGroup
47
from vllm.logger import init_logger
48
from vllm.model_executor.models.interfaces import MixtureOfExperts
49
from vllm.platforms import current_platform
50
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
51
52
from .async_worker import start_async_worker
53
from .eplb_communicator import EplbCommunicator, create_eplb_communicator
@@ -596,10 +597,11 @@ class EplbState:
597
num_tokens_per_rank
598
)
599
599
- # Just to make type checker happy
600
- tokens_tensors: list[float] = torch.stack(
601
- [avg_tokens_tensor, max_tokens_tensor]
602
- ).tolist()
600
+ # This gpu/cpu sync only happens with expert stat logging enabled.
601
+ with gpu_sync_allowed():
602
+ tokens_tensors: list[float] = torch.stack(
603
+ [avg_tokens_tensor, max_tokens_tensor]
604
+ ).tolist()
605
avg_tokens, max_tokens = tokens_tensors
606
balancedness = avg_tokens / max_tokens if max_tokens > 0 else 0.0
607
@@ -825,15 +827,17 @@ class EplbState:
827
self.model_states.values(), global_expert_load_windows
828
):
829
if not self.is_async or is_profile:
828
- # Get new expert mappings for the model
829
- new_physical_to_logical_map = self.policy.rebalance_experts(
830
- global_expert_load_window.cpu(),
831
- num_replicas,
832
- num_groups,
833
- num_nodes,
834
- num_gpus,
835
- eplb_model_state.physical_to_logical_map.cpu(),
836
- )
830
+ # Get new expert mappings for the model. The policy runs on the
831
+ # host, so the load window and current map have to come back.
832
+ with gpu_sync_allowed():
833
+ new_physical_to_logical_map = self.policy.rebalance_experts(
834
+ global_expert_load_window.cpu(),
835
+ num_replicas,
836
+ num_groups,
837
+ num_nodes,
838
+ num_gpus,
839
+ eplb_model_state.physical_to_logical_map.cpu(),
840
+ )
841
842
skip_rearrange = False
843
if (
@@ -1248,7 +1252,9 @@ def _pad_out_tensor(src: torch.Tensor, dst: torch.Tensor) -> None:
1252
src_padding = dst.shape[-1] - src.shape[-1]
1253
assert src_padding >= 0
1254
new_src = torch.nn.functional.pad(src, (0, src_padding), value=-1)
1251
- dst.copy_(new_src)
1255
+ # The map is committed from the host once per layer per rearrangement.
1256
+ with gpu_sync_allowed():
1257
+ dst.copy_(new_src)
1258
1259
1260
def _commit_eplb_maps_for_layer(
@@ -1276,10 +1282,7 @@ def _commit_eplb_maps_for_layer(
1282
num_logical_experts = model_state.logical_to_physical_map.shape[1]
1283
new_logical, new_replica_count = compute_logical_maps(src, num_logical_experts)
1284
# Commit logical_to_physical_map
1279
- _pad_out_tensor(
1280
- src=new_logical,
1281
- dst=model_state.logical_to_physical_map[layer],
1282
- )
1285
+ _pad_out_tensor(src=new_logical, dst=model_state.logical_to_physical_map[layer])
1286
1287
# Commit logical_replica_count
1288
src = new_replica_count
vllm/distributed/eplb/rebalance_execute.py
+6
-2
@@ -16,6 +16,7 @@ from torch.distributed import ProcessGroup, all_gather
16
from vllm.distributed.eplb.eplb_communicator import EplbCommunicator
17
from vllm.distributed.eplb.eplb_utils import CpuGpuEvent
18
from vllm.logger import init_logger
19
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
20
21
logger = init_logger(__name__)
22
@@ -590,8 +591,11 @@ def rearrange_expert_weights_inplace(
591
592
weights_buffer = list(expert_buffer)
593
593
- old_global_expert_indices_cpu = old_global_expert_indices.cpu().numpy()
594
- new_global_expert_indices_cpu = new_global_expert_indices.cpu().numpy()
594
+ # The per-layer transfer plan is built in Python, so both maps have to
595
+ # come back to the host. Once per rearrangement.
596
+ with gpu_sync_allowed():
597
+ old_global_expert_indices_cpu = old_global_expert_indices.cpu().numpy()
598
+ new_global_expert_indices_cpu = new_global_expert_indices.cpu().numpy()
599
600
for layer_idx in range(num_moe_layers):
601
transfer_metadata = move_to_buffer(
vllm/distributed/kv_transfer/kv_connector/v1/example_connector.py
+3
-1
@@ -15,6 +15,7 @@ from vllm.distributed.kv_transfer.kv_connector.v1.base import (
15
)
16
from vllm.logger import init_logger
17
from vllm.model_executor.layers.attention.mla_attention import MLACommonMetadata
18
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
19
from vllm.utils.hashing import safe_hash
20
from vllm.v1.attention.backend import AttentionMetadata
21
from vllm.v1.core.sched.output import SchedulerOutput
@@ -243,7 +244,8 @@ class ExampleConnector(KVConnectorBase_V1):
244
layer_name, request.token_ids, request.mm_hashes
245
)
246
kv_cache = extract_kv_from_layer(kv_layer, request.slot_mapping)
246
- tensors = {"kv_cache": kv_cache.detach().cpu()}
247
+ with gpu_sync_allowed():
248
+ tensors = {"kv_cache": kv_cache.detach().cpu()}
249
safetensors.torch.save_file(tensors, filename)
250
251
def wait_for_save(self):
vllm/distributed/kv_transfer/kv_connector/v1/nixl/base_worker.py
+32
-27
@@ -67,6 +67,7 @@ from vllm.distributed.parallel_state import (
67
)
68
from vllm.logger import init_logger
69
from vllm.platforms import current_platform
70
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
71
from vllm.utils.network_utils import make_zmq_path
72
from vllm.utils.torch_utils import async_tensor_h2d
73
from vllm.v1.attention.backends.utils import get_kv_cache_layout
@@ -1882,14 +1883,16 @@ class NixlBaseConnectorWorker:
1883
1884
local_block_ids = meta.local_physical_block_ids
1885
# TODO (NickLucche) D2H<>H2D ops could benefit from coalescing io across groups
1885
- for group_block_ids in local_block_ids:
1886
- self.copy_blocks(
1887
- self.host_xfer_buffers,
1888
- self.device_kv_caches,
1889
- group_block_ids,
1890
- group_block_ids,
1891
- "h2d",
1892
- )
1886
+ # The h2d block copies below are intentionally synchronous.
1887
+ with gpu_sync_allowed():
1888
+ for group_block_ids in local_block_ids:
1889
+ self.copy_blocks(
1890
+ self.host_xfer_buffers,
1891
+ self.device_kv_caches,
1892
+ group_block_ids,
1893
+ group_block_ids,
1894
+ "h2d",
1895
+ )
1896
if logger.isEnabledFor(logging.DEBUG):
1897
logger.debug(
1898
"synced recved kv of request[%s] to device kv buffer,"
@@ -1903,26 +1906,28 @@ class NixlBaseConnectorWorker:
1906
assert self.use_host_buffer
1907
assert self.copy_blocks is not None
1908
1906
- for req_id, meta in metadata.reqs_to_save.items():
1907
- meta.local_physical_block_ids = self._logical_to_kernel_block_ids(
1908
- meta.local_block_ids, self._physical_blocks_per_logical_kv_block
1909
- )
1910
- if logger.isEnabledFor(logging.DEBUG):
1911
- logger.debug(
1912
- "save_load_kv for request[%s] to host xfer buffer."
1913
- "local_block_ids: %s. ",
1914
- req_id,
1915
- ",".join(map(str, meta.local_physical_block_ids)),
1916
- )
1917
- # blocking
1918
- for group_block_ids in meta.local_physical_block_ids:
1919
- self.copy_blocks(
1920
- self.device_kv_caches,
1921
- self.host_xfer_buffers,
1922
- group_block_ids,
1923
- group_block_ids,
1924
- "d2h",
1909
+ # The d2h block copies below are intentionally synchronous.
1910
+ with gpu_sync_allowed():
1911
+ for req_id, meta in metadata.reqs_to_save.items():
1912
+ meta.local_physical_block_ids = self._logical_to_kernel_block_ids(
1913
+ meta.local_block_ids, self._physical_blocks_per_logical_kv_block
1914
)
1915
+ if logger.isEnabledFor(logging.DEBUG):
1916
+ logger.debug(
1917
+ "save_load_kv for request[%s] to host xfer buffer."
1918
+ "local_block_ids: %s. ",
1919
+ req_id,
1920
+ ",".join(map(str, meta.local_physical_block_ids)),
1921
+ )
1922
+ # blocking
1923
+ for group_block_ids in meta.local_physical_block_ids:
1924
+ self.copy_blocks(
1925
+ self.device_kv_caches,
1926
+ self.host_xfer_buffers,
1927
+ group_block_ids,
1928
+ group_block_ids,
1929
+ "d2h",
1930
+ )
1931
1932
@cached_property
1933
def _attention_kv_caches(self) -> list[torch.Tensor]:
vllm/lora/punica_wrapper/punica_gpu.py
+6
-3
@@ -14,6 +14,7 @@ import torch
14
from vllm.lora.layers import LoRAMapping
15
from vllm.lora.utils import get_captured_lora_counts
16
from vllm.triton_utils import HAS_TRITON, triton
17
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
18
from vllm.utils.math_utils import round_up
19
20
if HAS_TRITON:
@@ -83,9 +84,11 @@ class PunicaWrapperGPU(PunicaWrapperBase):
84
self.is_prefill = mapping.is_prefill
85
self._update_base_metadata(mapping, lora_index_to_id, max_loras, vocab_size)
86
86
- # Prepare cuda kernel metadata tensors
87
- self.token_mapping_meta.prepare_tensors(self.token_lora_indices)
88
- self.prompt_mapping_meta.prepare_tensors(self.sampler_indices)
87
+ # TODO avoid gpu<->cpu sync here
88
+ with gpu_sync_allowed():
89
+ # Prepare cuda kernel metadata tensors
90
+ self.token_mapping_meta.prepare_tensors(self.token_lora_indices)
91
+ self.prompt_mapping_meta.prepare_tensors(self.sampler_indices)
92
93
def add_shrink(
94
self,
vllm/lora/worker_manager.py
+36
-31
@@ -19,6 +19,7 @@ from vllm.lora.model_manager import (
19
from vllm.lora.peft_helper import PEFTHelper
20
from vllm.lora.request import LoRARequest
21
from vllm.lora.utils import get_adapter_absolute_path
22
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
23
24
logger = init_logger(__name__)
25
@@ -223,9 +224,11 @@ class WorkerLoRAManager:
224
def add_adapter(self, adapter_request: Any) -> bool:
225
if adapter_request.adapter_id in self.list_adapters():
226
return False
226
- loaded_adapter = self._load_adapter(adapter_request)
227
- loaded = self._adapter_manager.add_adapter(loaded_adapter)
228
- self._adapter_manager.activate_adapter(loaded_adapter.id)
227
+ # One-time per adapter
228
+ with gpu_sync_allowed():
229
+ loaded_adapter = self._load_adapter(adapter_request)
230
+ loaded = self._adapter_manager.add_adapter(loaded_adapter)
231
+ self._adapter_manager.activate_adapter(loaded_adapter.id)
232
return loaded
233
234
def remove_adapter(self, adapter_id: int) -> bool:
@@ -288,32 +291,34 @@ class LRUCacheWorkerLoRAManager(WorkerLoRAManager):
291
# This is ok because it's currently only called from
292
# the single-threaded core engine loop.
293
291
- if (
292
- lora_request.lora_int_id not in self.list_adapters()
293
- or lora_request.load_inplace
294
- ):
295
- # Load the new adapter first to ensure it is actually valid, before
296
- # evicting any existing adapters.
297
- # This may cause the # of loaded lora adapters to very temporarily
298
- # exceed `--max-cpu-loras`.
299
- lora = self._load_adapter(lora_request)
300
-
301
- # Remove the existing adapter if it exists
302
- # Use case for LoRA inplace
303
- self._adapter_manager.remove_adapter(lora.id)
304
-
305
- # Loading succeeded, now check if we will exceed cache capacity and
306
- # evict if the oldest adapter if so
307
- if len(self._adapter_manager) + 1 > self._adapter_manager.capacity:
308
- assert isinstance(self._adapter_manager, LRUCacheLoRAModelManager)
309
- self._adapter_manager.remove_oldest_adapter()
310
- # Then add the new adapter to the cache
311
- loaded = self._adapter_manager.add_adapter(lora)
312
- else:
313
- # If the lora is already loaded, just touch it to
314
- # update its position in the caches
315
- loaded = (
316
- self._adapter_manager.get_adapter(lora_request.lora_int_id) is not None
317
- )
318
- self._adapter_manager.activate_adapter(lora_request.lora_int_id)
294
+ with gpu_sync_allowed():
295
+ if (
296
+ lora_request.lora_int_id not in self.list_adapters()
297
+ or lora_request.load_inplace
298
+ ):
299
+ # Load the new adapter first to ensure it is actually valid, before
300
+ # evicting any existing adapters.
301
+ # This may cause the # of loaded lora adapters to very temporarily
302
+ # exceed `--max-cpu-loras`.
303
+ lora = self._load_adapter(lora_request)
304
+
305
+ # Remove the existing adapter if it exists
306
+ # Use case for LoRA inplace
307
+ self._adapter_manager.remove_adapter(lora.id)
308
+
309
+ # Loading succeeded, now check if we will exceed cache capacity and
310
+ # evict if the oldest adapter if so
311
+ if len(self._adapter_manager) + 1 > self._adapter_manager.capacity:
312
+ assert isinstance(self._adapter_manager, LRUCacheLoRAModelManager)
313
+ self._adapter_manager.remove_oldest_adapter()
314
+ # Then add the new adapter to the cache
315
+ loaded = self._adapter_manager.add_adapter(lora)
316
+ else:
317
+ # If the lora is already loaded, just touch it to
318
+ # update its position in the caches
319
+ loaded = (
320
+ self._adapter_manager.get_adapter(lora_request.lora_int_id)
321
+ is not None
322
+ )
323
+ self._adapter_manager.activate_adapter(lora_request.lora_int_id)
324
return loaded
vllm/model_executor/layers/mamba/mamba_mixer2.py
+13
-8
@@ -48,6 +48,7 @@ from vllm.model_executor.model_loader.weight_utils import (
48
from vllm.model_executor.parameter import BasevLLMParameter
49
from vllm.model_executor.utils import set_weight_attrs
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 (
53
LayerNameType,
54
_encode_layer_name,
@@ -904,17 +905,21 @@ class MambaMixer2(MambaBase, PluggableLayer):
905
# then chunk_stride = 2
906
chunk_stride = mamba_block_size // chunk_size
907
908
+ # The per-sequence loop below uses these as Python scalars.
909
+ # TODO avoid sync here?
910
+ with gpu_sync_allowed():
911
+ block_idx_first_cpu = block_idx_first_scheduled_token_p.tolist()
912
+ block_idx_last_cpu = block_idx_last_scheduled_token_p.tolist()
913
+ num_computed_tokens_p_cpu = num_computed_tokens_p.tolist()
914
+ last_chunk_indices_p_cpu = last_chunk_indices_p.tolist()
915
+
916
# Save state for sequences with more than just final state
917
for seq_idx in range(num_prefills):
918
# Block index for the first scheduled token
910
- block_idx_first_scheduled_token = block_idx_first_scheduled_token_p[
911
- seq_idx
912
- ]
919
+ block_idx_first_scheduled_token = block_idx_first_cpu[seq_idx]
920
921
# Block index for the last scheduled token
915
- block_idx_last_scheduled_token = block_idx_last_scheduled_token_p[
916
- seq_idx
917
- ]
922
+ block_idx_last_scheduled_token = block_idx_last_cpu[seq_idx]
923
924
# Number of blocks that need to be written
925
n_blocks_to_fill = (
@@ -935,7 +940,7 @@ class MambaMixer2(MambaBase, PluggableLayer):
940
if seq_idx == 0:
941
first_chunk = 0
942
else:
938
- first_chunk = 1 + last_chunk_indices_p[seq_idx - 1]
943
+ first_chunk = 1 + last_chunk_indices_p_cpu[seq_idx - 1]
944
945
# First chunk that is aligned on the mamba block boundary
946
first_aligned_chunk = first_chunk + chunk_stride - 1
@@ -943,7 +948,7 @@ class MambaMixer2(MambaBase, PluggableLayer):
948
# Calculate the number of computed tokens that were not
949
# already cached
950
num_unaligned_computed_tokens = (
946
- num_computed_tokens_p[seq_idx] % mamba_block_size
951
+ num_computed_tokens_p_cpu[seq_idx] % mamba_block_size
952
)
953
954
if num_unaligned_computed_tokens > 0:
vllm/model_executor/models/audioflamingo3.py
+9
-5
@@ -54,6 +54,7 @@ from vllm.multimodal.processing import (
54
PromptUpdateDetails,
55
)
56
from vllm.sequence import IntermediateTensors
57
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
58
from vllm.utils.tensor_schema import TensorSchema, TensorShape
59
60
from .interfaces import (
@@ -306,7 +307,10 @@ def _flatten_valid_audio_embeddings(
307
< output_lengths[:, None]
308
)
309
309
- return audio_embeddings[valid_mask], output_lengths
310
+ # Boolean-mask indexing has a data-dependent output shape, so the count
311
+ # has to come back to the host.
312
+ with gpu_sync_allowed():
313
+ return audio_embeddings[valid_mask], output_lengths
314
315
316
def _count_audio_tokens_from_mask(
@@ -630,10 +634,10 @@ class AudioFlamingo3ForConditionalGeneration(
634
audio_features,
635
feature_attention_mask,
636
)
633
- chunk_embeddings = torch.split(
634
- masked_audio_features,
635
- audio_output_lengths.tolist(),
636
- )
637
+ # `split` needs Python int sizes.
638
+ with gpu_sync_allowed():
639
+ split_sizes = audio_output_lengths.tolist()
640
+ chunk_embeddings = torch.split(masked_audio_features, split_sizes)
641
642
grouped_embeddings = []
643
current_idx = 0
vllm/model_executor/models/cohere_asr.py
+4
-4
@@ -56,6 +56,7 @@ from vllm.transformers_utils.processors.cohere_asr import (
56
CohereASRProcessor,
57
)
58
from vllm.utils.collection_utils import is_list_of
59
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
60
from vllm.v1.attention.backend import (
61
AttentionType,
62
)
@@ -1770,10 +1771,9 @@ class CohereASRModel(nn.Module):
1771
out = self.encoder_decoder_proj(out)
1772
1773
# Convert padded tensor to packed
1773
- outs = []
1774
- for i, feat in enumerate(out):
1775
- feat_len = encoder_output_length[i]
1776
- outs.append(feat[:feat_len, :])
1774
+ with gpu_sync_allowed():
1775
+ lengths = encoder_output_length.tolist()
1776
+ outs = [feat[:length, :] for feat, length in zip(out, lengths)]
1777
1778
return outs
1779
else:
vllm/model_executor/models/ernie45_vl.py
+13
-3
@@ -71,6 +71,7 @@ from vllm.multimodal.processing import (
71
PromptUpdate,
72
)
73
from vllm.sequence import IntermediateTensors
74
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
75
from vllm.utils.tensor_schema import TensorSchema, TensorShape
76
from vllm.utils.torch_utils import async_tensor_h2d
77
from vllm.v1.attention.backends.registry import AttentionBackendEnum
@@ -788,7 +789,10 @@ class VariableResolutionResamplerModel(nn.Module):
789
return x
790
791
def fwd_placeholder(x, grid_thw, to_tensor=False):
791
- grid_thw_cpu = grid_thw.cpu().numpy()
792
+ # The per-image grids drive the Python-level offset arithmetic
793
+ # below; the same read is wrapped on the cudagraph path.
794
+ with gpu_sync_allowed():
795
+ grid_thw_cpu = grid_thw.cpu().numpy()
796
grid_t, grid_hw = grid_thw_cpu[:, 0], grid_thw_cpu[:, 1:]
797
grid_hw_after_conv = grid_hw.prod(-1) // (self.spatial_conv_size**2)
798
@@ -1697,8 +1701,11 @@ class Ernie4_5_VLMoeForConditionalGeneration(
1701
EncoderCudaGraphReplayBuffers,
1702
)
1703
1704
+ # The per-image grids are needed as Python ints to size the buffers.
1705
+ with gpu_sync_allowed():
1706
+ grid_thw_list = mm_kwargs["image_grid_thw"].tolist()
1707
metadata = self.vision_model.prepare_encoder_metadata(
1701
- mm_kwargs["image_grid_thw"].tolist(), max_batch_size=max_batch_size
1708
+ grid_thw_list, max_batch_size=max_batch_size
1709
)
1710
values = metadata | {
1711
"pixel_values": mm_kwargs["pixel_values"],
@@ -1738,7 +1745,10 @@ class Ernie4_5_VLMoeForConditionalGeneration(
1745
# Ernie only uses the single "default" encoder path.
1746
output = outputs["default"]
1747
grid_thw = batch_mm_kwargs["image_grid_thw"].to(output.device)
1741
- num_valid = int((grid_thw[:, 0] * grid_thw[:, 1] * grid_thw[:, 2]).sum())
1748
+ # The valid token count slices the graph output for the eager
1749
+ # resampler call, so it has to come back to the host.
1750
+ with gpu_sync_allowed():
1751
+ num_valid = int((grid_thw[:, 0] * grid_thw[:, 1] * grid_thw[:, 2]).sum())
1752
image_embeds = self.resampler_model(output[:num_valid], grid_thw)
1753
scatter_output_slices(image_embeds, indices, per_item_out_tokens, dest, clone)
1754
vllm/model_executor/models/gemma4_mm.py
+43
-27
@@ -69,6 +69,7 @@ from vllm.multimodal.processing.processor import (
69
PromptUpdateDetails,
70
)
71
from vllm.sequence import IntermediateTensors
72
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
73
from vllm.utils.tensor_schema import TensorSchema, TensorShape
74
from vllm.utils.torch_utils import async_tensor_h2d
75
@@ -1365,11 +1366,14 @@ class Gemma4ForConditionalGeneration(
1366
pp_tensor,
1367
pad_tensor,
1368
).to(self.model_dtype)
1368
- encoder_outputs = vt.encoder(
1369
- inputs_embeds=inputs_embeds,
1370
- attention_mask=~pad_tensor,
1371
- pixel_position_ids=pp_tensor,
1372
- )
1369
+ # HuggingFace's mask builder probes `padding_mask.all()` to
1370
+ # decide whether the mask can be skipped, which syncs.
1371
+ with gpu_sync_allowed():
1372
+ encoder_outputs = vt.encoder(
1373
+ inputs_embeds=inputs_embeds,
1374
+ attention_mask=~pad_tensor,
1375
+ pixel_position_ids=pp_tensor,
1376
+ )
1377
hidden_states = encoder_outputs.last_hidden_state
1378
1379
for i, (orig_idx, _, _) in enumerate(chunk_items):
@@ -1387,13 +1391,17 @@ class Gemma4ForConditionalGeneration(
1391
single_pos_ids = pool_position_ids[orig_idx].unsqueeze(0)
1392
padding_positions = (single_pos_ids == -1).all(dim=-1)
1393
1390
- pooled_states, valid_mask = vt.pooler(
1391
- hidden_states=single_hidden,
1392
- pixel_position_ids=single_pos_ids,
1393
- padding_positions=padding_positions,
1394
- output_length=output_length,
1395
- )
1396
- valid_states = pooled_states[valid_mask]
1394
+ # The pooler goes through HuggingFace's mask builder, which probes
1395
+ # `padding_mask.all()`, and the mask indexing below needs the
1396
+ # selected count on the host.
1397
+ with gpu_sync_allowed():
1398
+ pooled_states, valid_mask = vt.pooler(
1399
+ hidden_states=single_hidden,
1400
+ pixel_position_ids=single_pos_ids,
1401
+ padding_positions=padding_positions,
1402
+ output_length=output_length,
1403
+ )
1404
+ valid_states = pooled_states[valid_mask]
1405
1406
if getattr(vt.config, "standardize", False):
1407
valid_states = (valid_states - vt.std_bias) * vt.std_scale
@@ -1444,7 +1452,9 @@ class Gemma4ForConditionalGeneration(
1452
pooling_k2 = vision_cfg.pooling_kernel_size**2
1453
1454
if isinstance(frame_counts, torch.Tensor):
1447
- fc_list = frame_counts.tolist()
1455
+ # Per-video frame counts drive the Python-level batching below.
1456
+ with gpu_sync_allowed():
1457
+ fc_list = frame_counts.tolist()
1458
else:
1459
fc_list = list(frame_counts)
1460
@@ -1474,11 +1484,13 @@ class Gemma4ForConditionalGeneration(
1484
pp_chunk,
1485
pad_chunk,
1486
).to(self.model_dtype)
1477
- encoder_outputs = vt.encoder(
1478
- inputs_embeds=inputs_embeds,
1479
- attention_mask=~pad_chunk,
1480
- pixel_position_ids=pp_chunk,
1481
- )
1487
+ # HuggingFace's mask builder probes `padding_mask.all()`.
1488
+ with gpu_sync_allowed():
1489
+ encoder_outputs = vt.encoder(
1490
+ inputs_embeds=inputs_embeds,
1491
+ attention_mask=~pad_chunk,
1492
+ pixel_position_ids=pp_chunk,
1493
+ )
1494
last_hidden_states_list.append(encoder_outputs.last_hidden_state)
1495
1496
last_hidden_states = torch.cat(last_hidden_states_list, dim=0)
@@ -1493,13 +1505,15 @@ class Gemma4ForConditionalGeneration(
1505
single_pos_ids = pixel_position_ids[i].unsqueeze(0)
1506
single_pad_pos = padding_positions[i].unsqueeze(0)
1507
1496
- pooled_states, valid_mask = vt.pooler(
1497
- hidden_states=single_hidden,
1498
- pixel_position_ids=single_pos_ids,
1499
- padding_positions=single_pad_pos,
1500
- output_length=output_length,
1501
- )
1502
- valid_states = pooled_states[valid_mask]
1508
+ # As above, plus mask indexing that needs the count on the host.
1509
+ with gpu_sync_allowed():
1510
+ pooled_states, valid_mask = vt.pooler(
1511
+ hidden_states=single_hidden,
1512
+ pixel_position_ids=single_pos_ids,
1513
+ padding_positions=single_pad_pos,
1514
+ output_length=output_length,
1515
+ )
1516
+ valid_states = pooled_states[valid_mask]
1517
1518
if getattr(vt.config, "standardize", False):
1519
valid_states = (valid_states - vt.std_bias) * vt.std_scale
@@ -1553,9 +1567,11 @@ class Gemma4ForConditionalGeneration(
1567
1568
# Strip padding per-batch element: only keep valid (non-padding)
1569
# tokens.
1570
+ # Boolean-mask indexing needs the selected count on the host.
1571
per_audio = []
1557
- for enc, mask in zip(audio_features, audio_mask, strict=True):
1558
- per_audio.append(enc[mask]) # [num_real, hidden_size]
1572
+ with gpu_sync_allowed():
1573
+ for enc, mask in zip(audio_features, audio_mask, strict=True):
1574
+ per_audio.append(enc[mask]) # [num_real, hidden_size]
1575
1576
return per_audio
1577
vllm/model_executor/models/granite_speech.py
+4
-2
@@ -60,6 +60,7 @@ from vllm.multimodal.processing import (
60
from vllm.sequence import IntermediateTensors
61
from vllm.tokenizers import cached_tokenizer_from_config
62
from vllm.transformers_utils.processor import cached_processor_from_config
63
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
64
from vllm.utils.tensor_schema import TensorSchema, TensorShape
65
from vllm.utils.torch_utils import PIN_MEMORY
66
@@ -788,8 +789,9 @@ class GraniteSpeechForConditionalGeneration(
789
encoder_embeds = self.encoder(audio_input["input_features"])
790
# [bsz, <max feature size>, 4096]
791
projected_embeds = self.projector(encoder_embeds)
791
- # Apply mask on variable length audio features
792
- masked_embeds = projected_embeds[audio_input["input_features_mask"]]
792
+ # Apply mask on variable length audio features.
793
+ with gpu_sync_allowed():
794
+ masked_embeds = projected_embeds[audio_input["input_features_mask"]]
795
# Split variable length features into a tuple
796
return torch.split(masked_embeds, audio_input["audio_embed_sizes"])
797
vllm/model_executor/models/idefics2_vision_model.py
+13
-6
@@ -39,6 +39,7 @@ from vllm.model_executor.layers.linear import (
39
RowParallelLinear,
40
)
41
from vllm.model_executor.layers.quantization import QuantizationConfig
42
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
43
44
from .utils import AutoWeightsLoader, WeightsMapper
45
from .vision import is_vit_use_data_parallel, run_dp_sharded_vision_model
@@ -96,10 +97,14 @@ class Idefics2VisionEmbeddings(nn.Module):
97
size=(batch_size, max_nb_patches_h * max_nb_patches_w), fill_value=0
98
)
99
99
- for batch_idx, p_attn_mask in enumerate(patch_attention_mask):
100
- if tgt_sizes is not None:
101
- nb_patches_h = tgt_sizes[batch_idx][0]
102
- nb_patches_w = tgt_sizes[batch_idx][1]
100
+ with gpu_sync_allowed():
101
+ patch_attention_mask_cpu = patch_attention_mask.cpu()
102
+ tgt_sizes_cpu = tgt_sizes.cpu() if tgt_sizes is not None else None
103
+
104
+ for batch_idx, p_attn_mask in enumerate(patch_attention_mask_cpu):
105
+ if tgt_sizes_cpu is not None:
106
+ nb_patches_h = tgt_sizes_cpu[batch_idx][0]
107
+ nb_patches_w = tgt_sizes_cpu[batch_idx][1]
108
else:
109
nb_patches_h = p_attn_mask[:, 0].sum()
110
nb_patches_w = p_attn_mask[0].sum()
@@ -114,8 +119,10 @@ class Idefics2VisionEmbeddings(nn.Module):
119
pos_ids = (
120
bucket_coords_h[:, None] * self.num_patches_per_side + bucket_coords_w
121
).flatten()
117
- position_ids[batch_idx][p_attn_mask.view(-1).cpu()] = pos_ids
118
- position_ids = position_ids.to(self.position_embedding.weight.device)
122
+ position_ids[batch_idx][p_attn_mask.view(-1)] = pos_ids
123
+ position_ids = position_ids.to(
124
+ self.position_embedding.weight.device, non_blocking=True
125
+ )
126
embeddings += self.position_embedding(position_ids)
127
return embeddings
128
vllm/model_executor/models/idefics3.py
+6
-4
@@ -51,6 +51,7 @@ from vllm.multimodal.processing import (
51
PromptUpdateDetails,
52
)
53
from vllm.sequence import IntermediateTensors
54
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
55
from vllm.utils.tensor_schema import TensorSchema, TensorShape
56
57
from .idefics2_vision_model import (
@@ -443,11 +444,12 @@ class Idefics3Model(nn.Module):
444
real_images_inds = (pixel_values == 0.0).sum(
445
dim=(-1, -2, -3)
446
) != nb_values_per_image
446
- pixel_values = pixel_values[real_images_inds].contiguous()
447
+ with gpu_sync_allowed():
448
+ pixel_values = pixel_values[real_images_inds].contiguous()
449
448
- # Handle the vision attention mask
449
- # Remove padding images from the mask
450
- pixel_attention_mask = pixel_attention_mask[real_images_inds].contiguous()
450
+ # Handle the vision attention mask
451
+ # Remove padding images from the mask
452
+ pixel_attention_mask = pixel_attention_mask[real_images_inds].contiguous()
453
454
patch_size = self.config.vision_config.patch_size
455
patches_subgrid = pixel_attention_mask.unfold(
vllm/model_executor/models/jina.py
+19
-13
@@ -16,6 +16,7 @@ from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead
16
from vllm.sequence import IntermediateTensors
17
from vllm.tasks import PoolingTask
18
from vllm.transformers_utils.repo_utils import get_hf_file_bytes
19
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
20
from vllm.v1.pool.metadata import PoolingMetadata
21
22
from ..layers.pooler import DispatchPooler
@@ -110,19 +111,24 @@ class JinaForRankingPool(StepPool):
111
prompt_token_ids = pooling_metadata.get_prompt_token_ids()
112
113
embeds_list = list[torch.Tensor | None]()
113
- for data, token_ids in zip(pooled_data_lst, prompt_token_ids):
114
- # for unfinished chunked prefill
115
- if data is None:
116
- embeds_list.append(None)
117
- else:
118
- docs_indexes = torch.where(torch.eq(token_ids, self.doc_token_id))[0]
119
- query_indexes = torch.where(torch.eq(token_ids, self.query_token_id))[0]
120
-
121
- # The JinaForRanking model concatenates docs first, then query.
122
- # Let's stay consistent with this novel design.
123
- indexes = torch.cat([docs_indexes, query_indexes])
124
- embeds = self.projector(data[indexes])
125
- embeds_list.append(embeds)
114
+ # `torch.where` resolves the match count on the host.
115
+ with gpu_sync_allowed():
116
+ for data, token_ids in zip(pooled_data_lst, prompt_token_ids):
117
+ # for unfinished chunked prefill
118
+ if data is None:
119
+ embeds_list.append(None)
120
+ else:
121
+ doc_match = torch.eq(token_ids, self.doc_token_id)
122
+ docs_indexes = torch.where(doc_match)[0]
123
+ query_indexes = torch.where(
124
+ torch.eq(token_ids, self.query_token_id)
125
+ )[0]
126
+
127
+ # The JinaForRanking model concatenates docs first, then query.
128
+ # Let's stay consistent with this novel design.
129
+ indexes = torch.cat([docs_indexes, query_indexes])
130
+ embeds = self.projector(data[indexes])
131
+ embeds_list.append(embeds)
132
133
return embeds_list
134
vllm/model_executor/models/minicpmv.py
+10
-4
@@ -98,6 +98,7 @@ from vllm.transformers_utils.processor import (
98
)
99
from vllm.transformers_utils.utils import convert_model_repo_to_path
100
from vllm.utils.collection_utils import flatten_2d_lists
101
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
102
from vllm.utils.tensor_schema import TensorSchema, TensorShape
103
from vllm.utils.torch_utils import set_default_torch_dtype
104
@@ -1548,7 +1549,8 @@ class MiniCPMV2_5(MiniCPMVBaseModel, SupportsLoRA):
1549
all_pixel_values[i, ..., :L_item] = pixel_values_item
1550
1551
num_patches = tgt_sizes.prod(-1)
1551
- max_patches = num_patches.max().item()
1552
+ with gpu_sync_allowed():
1553
+ max_patches = num_patches.max().item()
1554
assert isinstance(max_patches, int)
1555
1556
patch_attn_mask = torch.zeros((B, max_patches), dtype=torch.bool, device=device)
@@ -1640,7 +1642,9 @@ class MiniCPMV2_6(MiniCPMVBaseModel, SupportsLoRA):
1642
all_pixel_values[i, ..., :L_item] = pixel_values_item
1643
1644
num_patches = tgt_sizes.prod(-1)
1643
- max_patches = num_patches.max().item()
1645
+ # Needed as a Python int to size the mask below.
1646
+ with gpu_sync_allowed():
1647
+ max_patches = num_patches.max().item()
1648
assert isinstance(max_patches, int)
1649
1650
patch_attn_mask = torch.zeros((B, max_patches), dtype=torch.bool, device=device)
@@ -1737,7 +1741,8 @@ class MiniCPMV4_0(MiniCPMVBaseModel, SupportsLoRA):
1741
all_pixel_values[i, ..., :L_item] = pixel_values_item
1742
1743
num_patches = tgt_sizes.prod(-1)
1740
- max_patches = num_patches.max().item()
1744
+ with gpu_sync_allowed():
1745
+ max_patches = num_patches.max().item()
1746
assert isinstance(max_patches, int)
1747
1748
patch_attn_mask = torch.zeros((B, max_patches), dtype=torch.bool, device=device)
@@ -1839,7 +1844,8 @@ class MiniCPMV4_5(MiniCPMVBaseModel, SupportsLoRA):
1844
all_pixel_values[i, ..., :L_item] = pixel_values_item
1845
1846
num_patches = tgt_sizes.prod(-1)
1842
- max_patches = num_patches.max().item()
1847
+ with gpu_sync_allowed():
1848
+ max_patches = num_patches.max().item()
1849
assert isinstance(max_patches, int)
1850
1851
patch_attn_mask = torch.zeros((B, max_patches), dtype=torch.bool, device=device)
vllm/model_executor/models/moonvit.py
+11
-5
@@ -66,6 +66,7 @@ from vllm.model_executor.models.utils import maybe_prefix
66
from vllm.model_executor.models.vision import is_vit_use_data_parallel
67
from vllm.platforms import current_platform
68
from vllm.transformers_utils.configs.moonvit import MoonViTConfig
69
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
70
from vllm.utils.torch_utils import async_tensor_h2d
71
72
@@ -811,9 +812,14 @@ class MoonVitPretrainedModel(PreTrainedModel):
812
merge_kernel_size=self.merge_kernel_size,
813
)
814
814
- hidden_states = self.patch_embed(pixel_values, grid_hw)
815
- hidden_states = self.encoder(hidden_states, grid_hw)
816
- hidden_states = patch_merger(
817
- hidden_states, grid_hw, merge_kernel_size=self.merge_kernel_size
818
- )
815
+ # Legacy path: patch_embed, encoder and patch_merger each iterate the
816
+ # per-image grids in Python, so all three read `grid_hw` back to the
817
+ # host. The `encoder_metadata` path above precomputes that outside the
818
+ # graph and does not sync.
819
+ with gpu_sync_allowed():
820
+ hidden_states = self.patch_embed(pixel_values, grid_hw)
821
+ hidden_states = self.encoder(hidden_states, grid_hw)
822
+ hidden_states = patch_merger(
823
+ hidden_states, grid_hw, merge_kernel_size=self.merge_kernel_size
824
+ )
825
return hidden_states
vllm/model_executor/models/moss_audio.py
+14
-9
@@ -55,6 +55,7 @@ from vllm.multimodal.processing import (
55
)
56
from vllm.sequence import IntermediateTensors
57
from vllm.transformers_utils.repo_utils import get_hf_file_to_dict
58
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
59
from vllm.utils.tensor_schema import TensorSchema, TensorShape
60
61
from .interfaces import (
@@ -1667,15 +1668,19 @@ class MossAudioModel(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA):
1668
"""
1669
audio_data = audio_input["audio_data"]
1670
audio_data_seqlens = audio_input["audio_data_seqlens"]
1670
- last_hidden_state, deepstack = self.audio_encoder(
1671
- audio_data.to(self.audio_encoder.dtype),
1672
- feature_lens=audio_data_seqlens,
1673
- output_deepstack_hidden_states=len(self.deepstack_audio_merger_list) > 0,
1674
- )
1675
- audio_embeds = self.audio_adapter(last_hidden_state)
1676
- audio_lengths = MossAudioEncoder._compute_downsampled_length(
1677
- audio_data_seqlens.to(device=audio_embeds.device, dtype=torch.long)
1678
- ).tolist()
1671
+ # The encoder chunks the input by per-audio feature lengths, which
1672
+ # needs Python ints for `split`/`pad_sequence`.
1673
+ want_deepstack = len(self.deepstack_audio_merger_list) > 0
1674
+ with gpu_sync_allowed():
1675
+ last_hidden_state, deepstack = self.audio_encoder(
1676
+ audio_data.to(self.audio_encoder.dtype),
1677
+ feature_lens=audio_data_seqlens,
1678
+ output_deepstack_hidden_states=want_deepstack,
1679
+ )
1680
+ audio_embeds = self.audio_adapter(last_hidden_state)
1681
+ audio_lengths = MossAudioEncoder._compute_downsampled_length(
1682
+ audio_data_seqlens.to(device=audio_embeds.device, dtype=torch.long)
1683
+ ).tolist()
1684
main_embeddings = tuple(audio_embeds.squeeze(0).split(audio_lengths, dim=0))
1685
1686
deepstack_embeddings: list[tuple[torch.Tensor, ...]] = []
vllm/model_executor/models/phi4mm.py
+15
-3
@@ -49,6 +49,7 @@ from vllm.multimodal.processing.processor import (
49
ResolvedPromptUpdate,
50
)
51
from vllm.sequence import IntermediateTensors
52
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
53
from vllm.utils.tensor_schema import TensorSchema, TensorShape
54
55
from .idefics2_vision_model import Idefics2VisionTransformer
@@ -396,12 +397,23 @@ class Phi4MMImageEncoder(nn.Module):
397
w * base_feat_width // base_feat_height_reduction,
398
)
399
)
399
- useful_height = int(reshaped_image_attention_mask[0, :, 0].sum().item())
400
- useful_width = int(reshaped_image_attention_mask[0, 0, :].sum().item())
400
+ # The mask stays on device for the encoder above, so these
401
+ # per-image counts have to come back to the host to drive the
402
+ # slicing and the Python-level length arithmetic.
403
+ with gpu_sync_allowed():
404
+ useful_height = int(
405
+ reshaped_image_attention_mask[0, :, 0].sum().item()
406
+ )
407
+ useful_width = int(
408
+ reshaped_image_attention_mask[0, 0, :].sum().item()
409
+ )
410
+ mask_token_count = int(
411
+ image_attention_mask[_bs, : B_ + 1, 0::2, 0::2].sum().item()
412
+ )
413
sub_img = sub_img[:, :useful_height, :useful_width]
414
temp_sub_GN = self.sub_GN.repeat(1, useful_height, 1, 1)
415
temp_len = (
404
- int(image_attention_mask[_bs, : B_ + 1, 0::2, 0::2].sum().item())
416
+ mask_token_count
417
+ (useful_height + 1)
418
+ base_feat_height // base_feat_height_reduction
419
)
vllm/model_executor/models/qwen2_5_omni_thinker.py
+15
-11
@@ -89,6 +89,7 @@ from vllm.multimodal.processing.processor import (
89
PromptUpdateDetails,
90
)
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
95
from .interfaces import (
@@ -1036,12 +1037,14 @@ class Qwen2_5OmniConditionalGenerationMixin:
1037
self.audio_tower._get_feat_extract_output_lengths(audio_feature_lengths)
1038
)
1039
1039
- audio_outputs = self.audio_tower(
1040
- input_features.to(self.audio_tower.dtype),
1041
- feature_lens=audio_feature_lengths,
1042
- aftercnn_lens=audio_feat_lengths,
1043
- )
1044
- return audio_outputs.last_hidden_state.split(audio_output_lengths.tolist())
1040
+ with gpu_sync_allowed():
1041
+ audio_outputs = self.audio_tower(
1042
+ input_features.to(self.audio_tower.dtype),
1043
+ feature_lens=audio_feature_lengths,
1044
+ aftercnn_lens=audio_feat_lengths,
1045
+ )
1046
+ split_sizes = audio_output_lengths.tolist()
1047
+ return audio_outputs.last_hidden_state.split(split_sizes)
1048
1049
def _process_image_input(
1050
self, image_input: Qwen2_5_VLImageInputs
@@ -1526,12 +1529,13 @@ class Qwen2_5OmniThinkerForConditionalGeneration(
1529
video_token_id = self.config.video_token_index
1530
audio_token_id = self.config.audio_token_index
1531
1529
- input_ids_cpu = input_ids.cpu()
1530
- is_video = is_multimodal & (input_ids_cpu == video_token_id)
1531
- is_audio = is_multimodal & (input_ids_cpu == audio_token_id)
1532
+ with gpu_sync_allowed():
1533
+ input_ids_cpu = input_ids.cpu()
1534
+ is_video = is_multimodal & (input_ids_cpu == video_token_id)
1535
+ is_audio = is_multimodal & (input_ids_cpu == audio_token_id)
1536
1533
- num_video = is_video.sum().item()
1534
- num_audio = is_audio.sum().item()
1537
+ num_video = is_video.sum().item()
1538
+ num_audio = is_audio.sum().item()
1539
1540
if check_interleaved_audio_video(is_video, is_audio, num_video, num_audio):
1541
inputs_embeds = self._embed_text_input_ids(
vllm/model_executor/models/qwen2_5_vl.py
+14
-11
@@ -87,6 +87,7 @@ from vllm.utils.torch_utils import PIN_MEMORY, async_tensor_h2d
87
from vllm.v1.attention.backends.registry import AttentionBackendEnum
88
from vllm.v1.worker.encoder_cudagraph_defs import EncoderCudaGraphReplayBuffers
89
90
+from ...utils.gpu_sync_debug import gpu_sync_allowed
91
from .interfaces import (
92
MultiModalEmbeddings,
93
SupportsEagle,
@@ -1569,8 +1570,9 @@ class Qwen2_5_VLForConditionalGeneration(
1570
video_second_per_grid=video_second_per_grid_t.item(),
1571
).to(emb.device, non_blocking=True)
1572
1572
- emb = emb[retention_mask]
1573
- positions = positions[retention_mask]
1573
+ with gpu_sync_allowed():
1574
+ emb = emb[retention_mask]
1575
+ positions = positions[retention_mask]
1576
emb = torch.cat([emb, positions], dim=1)
1577
video_embeds_out.append(emb)
1578
return tuple(video_embeds_out)
@@ -1624,15 +1626,16 @@ class Qwen2_5_VLForConditionalGeneration(
1626
mm[:, -4:].permute(1, 0).long() for mm in multimodal_embeddings
1627
]
1628
1627
- positions, mrope_positions_delta = recompute_mrope_positions(
1628
- input_ids_t,
1629
- mm_embeddings_pos,
1630
- mrope_positions,
1631
- num_computed_tokens,
1632
- vision_start_token_id,
1633
- image_token_id,
1634
- video_token_id,
1635
- )
1629
+ with gpu_sync_allowed():
1630
+ positions, mrope_positions_delta = recompute_mrope_positions(
1631
+ input_ids_t,
1632
+ mm_embeddings_pos,
1633
+ mrope_positions,
1634
+ num_computed_tokens,
1635
+ vision_start_token_id,
1636
+ image_token_id,
1637
+ video_token_id,
1638
+ )
1639
1640
return mm_embeddings_out, positions, mrope_positions_delta
1641
vllm/model_executor/models/qwen2_audio.py
+9
-5
@@ -60,6 +60,7 @@ from vllm.multimodal.processing import (
60
PromptUpdate,
61
)
62
from vllm.sequence import IntermediateTensors
63
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
64
from vllm.utils.tensor_schema import TensorSchema, TensorShape
65
66
from .interfaces import MultiModalEmbeddings, SupportsMultiModal, SupportsPP
@@ -444,12 +445,15 @@ class Qwen2AudioForConditionalGeneration(nn.Module, SupportsMultiModal, Supports
445
)
446
< audio_output_lengths
447
)
447
- masked_audio_features = audio_features[audio_features_mask].view(-1, embed_dim)
448
+ with gpu_sync_allowed():
449
+ masked_audio_features = audio_features[audio_features_mask].view(
450
+ -1, embed_dim
451
+ )
452
449
- # Split to tuple of embeddings for individual audio input.
450
- return torch.split(
451
- masked_audio_features, audio_output_lengths.flatten().tolist()
452
- )
453
+ # Split to tuple of embeddings for individual audio input.
454
+ return torch.split(
455
+ masked_audio_features, audio_output_lengths.flatten().tolist()
456
+ )
457
458
def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
459
audio_input = self._parse_and_validate_audio_input(**kwargs)
vllm/model_executor/models/qwen3_asr.py
+4
-1
@@ -92,6 +92,7 @@ from vllm.transformers_utils.processor import cached_processor_from_config
92
from vllm.transformers_utils.processors.qwen3_asr import (
93
Qwen3ASRProcessor,
94
)
95
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
96
97
logger = init_logger(__name__)
98
_ASR_TEXT_TAG = "<asr_text>"
@@ -467,7 +468,9 @@ class Qwen3ASRForConditionalGeneration(
468
feature_lens=audio_feature_lengths,
469
aftercnn_lens=audio_output_lengths,
470
)
470
- return audio_features.split(audio_output_lengths.tolist())
471
+ with gpu_sync_allowed():
472
+ split_sizes = audio_output_lengths.tolist()
473
+ return audio_features.split(split_sizes)
474
475
def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings | None:
476
mm_input_by_modality = self._parse_and_validate_multimodal_inputs(**kwargs)
vllm/model_executor/models/qwen3_omni_moe_thinker.py
+17
-9
@@ -77,6 +77,7 @@ from vllm.multimodal.processing.processor import (
77
)
78
from vllm.sequence import IntermediateTensors
79
from vllm.transformers_utils.processor import cached_processor_from_config
80
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
81
from vllm.utils.torch_utils import async_tensor_h2d
82
from vllm.v1.attention.backends.registry import AttentionBackendEnum
83
@@ -434,11 +435,14 @@ class Qwen3OmniMoeAudioEncoder(nn.Module):
435
feature_lens: torch.Tensor,
436
aftercnn_lens: torch.Tensor,
437
):
437
- # Compute chunk information
438
+ # Compute chunk information.
439
chunk_num = torch.ceil(feature_lens / (self.n_window * 2)).long()
440
441
+ with gpu_sync_allowed():
442
+ total_chunks = int(chunk_num.sum())
443
+
444
chunk_lengths = async_tensor_h2d(
441
- [self.n_window * 2] * chunk_num.sum(),
445
+ [self.n_window * 2] * total_chunks,
446
dtype=torch.long,
447
device=feature_lens.device,
448
)
@@ -447,15 +451,18 @@ class Qwen3OmniMoeAudioEncoder(nn.Module):
451
chunk_lengths[chunk_lengths == 0] = self.n_window * 2
452
453
# Split input features into chunks and pad
450
- chunk_list = input_features.T.split(chunk_lengths.tolist(), dim=0)
454
+ with gpu_sync_allowed():
455
+ chunk_lengths_list = chunk_lengths.tolist()
456
+ chunk_list = input_features.T.split(chunk_lengths_list, dim=0)
457
padded_feature = nn.utils.rnn.pad_sequence(
458
chunk_list, batch_first=True
459
).transpose(1, 2)
460
461
# Compute feature lengths after CNN
462
feature_lens_after_cnn = self._get_cnn_output_lengths(chunk_lengths)
457
- # Vectorized mask creation: avoid creating many small tensors
458
- max_len_after_cnn = feature_lens_after_cnn.max().item()
463
+ # Vectorized mask creation: avoid creating many small tensors.
464
+ with gpu_sync_allowed():
465
+ max_len_after_cnn = feature_lens_after_cnn.max().item()
466
indices = torch.arange(max_len_after_cnn, device=padded_feature.device)
467
padded_mask_after_cnn = indices.unsqueeze(0) < feature_lens_after_cnn.unsqueeze(
468
1
@@ -494,16 +501,17 @@ class Qwen3OmniMoeAudioEncoder(nn.Module):
501
)
502
padded_embed = padded_embed + positional_embedding
503
497
- # Extract valid hidden states and compute cu_seqlens
498
- hidden_states = padded_embed[padded_mask_after_cnn]
504
+ with gpu_sync_allowed():
505
+ hidden_states = padded_embed[padded_mask_after_cnn]
506
+ # Use tolist() for efficient batch conversion from tensor to Python.
507
+ aftercnn_lens_list = aftercnn_lens.tolist()
508
509
# Compute cumulative sequence lengths for chunked attention
510
cu_chunk_lens = [0]
511
window_aftercnn = padded_mask_after_cnn.shape[-1] * (
512
self.n_window_infer // (self.n_window * 2)
513
)
505
- # Use tolist() for efficient batch conversion from tensor to Python
506
- for cnn_len in aftercnn_lens.tolist():
514
+ for cnn_len in aftercnn_lens_list:
515
num_full_chunks = cnn_len // window_aftercnn
516
remainder = cnn_len % window_aftercnn
517
cu_chunk_lens.extend([window_aftercnn] * num_full_chunks)
vllm/model_executor/models/qwen3_vl.py
+29
-25
@@ -115,6 +115,7 @@ from vllm.utils.math_utils import round_up
115
from vllm.utils.torch_utils import PIN_MEMORY
116
from vllm.v1.worker.encoder_cudagraph_defs import EncoderCudaGraphReplayBuffers
117
118
+from ...utils.gpu_sync_debug import gpu_sync_allowed
119
from ...utils.torch_utils import async_tensor_h2d
120
from .interfaces import (
121
MultiModalEmbeddings,
@@ -2368,19 +2369,17 @@ class Qwen3VLForConditionalGeneration(
2369
spatial_merge_size=self.visual.spatial_merge_size,
2370
q=self.video_pruning_rate,
2371
)
2371
- # Apply retention mask.
2372
- emb = emb[retention_mask]
2372
2374
- # Calculate the actual number of retained tokens per frame.
2375
- num_frames, rows, cols = (
2376
- t,
2377
- h // merge_size,
2378
- w // merge_size,
2379
- )
2380
- retention_mask_thw = retention_mask.reshape(num_frames, rows, cols)
2381
- num_tokens_per_frame = (
2382
- retention_mask_thw.sum(dim=(1, 2)).long().tolist()
2383
- )
2373
+ with gpu_sync_allowed():
2374
+ # Apply retention mask.
2375
+ emb = emb[retention_mask]
2376
+
2377
+ # Calculate the actual number of retained tokens per frame.
2378
+ num_frames, rows, cols = t, h // merge_size, w // merge_size
2379
+ retention_mask_thw = retention_mask.reshape(num_frames, rows, cols)
2380
+ num_tokens_per_frame = (
2381
+ retention_mask_thw.sum(dim=(1, 2)).long().tolist()
2382
+ )
2383
else:
2384
feature_size = emb.shape[0] // num_frames
2385
num_tokens_per_frame = [feature_size] * num_frames
@@ -2548,10 +2547,14 @@ class Qwen3VLForConditionalGeneration(
2547
.permute(1, 0)
2548
)
2549
full_is_video_embed = unpruned_token_ids_tensor == embed_token_id
2551
- expanded_positions[is_video_embed, :3] = original_mrope[full_is_video_embed][
2552
- retention_mask
2553
- ]
2554
- expanded_positions[~is_video_embed, :3] = original_mrope[~full_is_video_embed]
2550
+
2551
+ with gpu_sync_allowed():
2552
+ expanded_positions[is_video_embed, :3] = original_mrope[
2553
+ full_is_video_embed
2554
+ ][retention_mask]
2555
+ expanded_positions[~is_video_embed, :3] = original_mrope[
2556
+ ~full_is_video_embed
2557
+ ]
2558
expanded_positions[..., 3] = is_vision_start
2559
expanded_positions[..., 4] = is_video_embed
2560
@@ -2814,15 +2817,16 @@ class Qwen3VLForConditionalGeneration(
2817
torch.empty(5, 0, device=device, dtype=torch.long)
2818
)
2819
2817
- positions, mrope_positions_delta = recompute_mrope_positions(
2818
- input_ids_t,
2819
- mm_embeddings_pos,
2820
- mrope_positions,
2821
- num_computed_tokens,
2822
- vision_start_token_id,
2823
- image_token_id,
2824
- video_token_id,
2825
- )
2820
+ with gpu_sync_allowed():
2821
+ positions, mrope_positions_delta = recompute_mrope_positions(
2822
+ input_ids_t,
2823
+ mm_embeddings_pos,
2824
+ mrope_positions,
2825
+ num_computed_tokens,
2826
+ vision_start_token_id,
2827
+ image_token_id,
2828
+ video_token_id,
2829
+ )
2830
2831
return mm_embeddings_out, positions, mrope_positions_delta
2832
vllm/model_executor/models/siglip.py
+20
-15
@@ -54,6 +54,7 @@ from vllm.multimodal.processing import (
54
TimingContext,
55
)
56
from vllm.sequence import IntermediateTensors
57
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
58
from vllm.utils.tensor_schema import TensorSchema, TensorShape
59
60
from .interfaces import MultiModalEmbeddings, SupportsMultiModal, SupportsQuant
@@ -998,27 +999,31 @@ class SiglipEmbeddingModel(nn.Module, SupportsMultiModal, SupportsQuant):
999
position_diffs = position_ids[1:] - position_ids[:-1]
1000
boundary_mask = position_diffs <= 0
1001
1001
- boundary_indices = torch.cat(
1002
- [
1003
- torch.tensor([0], device=features.device),
1004
- torch.where(boundary_mask)[0] + 1,
1005
- torch.tensor([len(features)], device=features.device),
1006
- ]
1007
- )
1002
+ with gpu_sync_allowed():
1003
+ boundary_mid_cpu = torch.where(boundary_mask.cpu())[0] + 1
1004
+
1005
+ zero = torch.zeros(1, dtype=boundary_mid_cpu.dtype)
1006
+ end = torch.full((1,), len(features), dtype=boundary_mid_cpu.dtype)
1007
+ boundary_indices_cpu = torch.cat([zero, boundary_mid_cpu, end])
1008
1009
# For each sequence [start, end), position i flips to: start + end - 1 - i
1010
- lengths = boundary_indices[1:] - boundary_indices[:-1]
1011
- starts = boundary_indices[:-1]
1012
- ends = boundary_indices[1:]
1010
+ lengths_cpu = boundary_indices_cpu[1:] - boundary_indices_cpu[:-1]
1011
+ starts_cpu = boundary_indices_cpu[:-1]
1012
+ ends_cpu = boundary_indices_cpu[1:]
1013
1014
# Assign sequence ID to each element
1015
- sequence_ids = torch.arange(
1016
- len(lengths), device=features.device
1017
- ).repeat_interleave(lengths)
1015
+ sequence_ids_cpu = torch.arange(
1016
+ len(lengths_cpu), dtype=boundary_mid_cpu.dtype
1017
+ ).repeat_interleave(lengths_cpu)
1018
1019
# Calculate flipped indices for all positions at once
1020
- current_positions = torch.arange(len(features), device=features.device)
1021
- flip_indices = starts[sequence_ids] + ends[sequence_ids] - 1 - current_positions
1020
+ current_positions_cpu = torch.arange(
1021
+ len(features), dtype=boundary_mid_cpu.dtype
1022
+ )
1023
+ flip_indices_cpu = (
1024
+ starts_cpu[sequence_ids_cpu] + ends_cpu[sequence_ids_cpu]
1025
+ ) - (1 + current_positions_cpu)
1026
+ flip_indices = flip_indices_cpu.to(features.device, non_blocking=True)
1027
1028
return features[flip_indices]
1029
vllm/model_executor/models/terratorch.py
+4
-1
@@ -61,6 +61,7 @@ from vllm.multimodal.processing import (
61
TimingContext,
62
)
63
from vllm.sequence import IntermediateTensors
64
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
65
66
from .interfaces import IsAttentionFree, MultiModalEmbeddings, SupportsMultiModal
67
from .interfaces_base import attn_type
@@ -280,7 +281,9 @@ class Terratorch(nn.Module, IsAttentionFree, SupportsMultiModal):
281
inputs_embeds: torch.Tensor | None = None,
282
**kwargs: object,
283
):
283
- model_output = self.inference_runner.forward(**kwargs)
284
+ # terratorch's forward has internal GPU syncs.
285
+ with gpu_sync_allowed():
286
+ model_output = self.inference_runner.forward(**kwargs)
287
return model_output.output
288
289
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
vllm/model_executor/models/transformers/multimodal.py
+16
-5
@@ -54,6 +54,7 @@ from vllm.multimodal.processing import (
54
TimingContext,
55
)
56
from vllm.sequence import IntermediateTensors
57
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
58
59
if TYPE_CHECKING:
60
from transformers import BatchFeature, PreTrainedModel
@@ -726,12 +727,17 @@ class MultiModalMixin(SupportsMultiModal, SupportsMRoPE):
727
kwargs.pop("token_type_ids", None)
728
kwargs.pop("mm_token_type_ids", None)
729
729
- audio_output = self.model.get_audio_features(
730
- input_features, return_dict=True, **kwargs
731
- )
730
+ # HuggingFace's `get_audio_features` implementations branch on
731
+ # per-sample feature lengths internally.
732
+ with gpu_sync_allowed():
733
+ audio_output = self.model.get_audio_features(
734
+ input_features, return_dict=True, **kwargs
735
+ )
736
audio_embeddings = audio_output.pooler_output
737
734
- split_sizes = num_audio_tokens.flatten().tolist()
738
+ # Per-audio token counts are needed as Python ints to split.
739
+ with gpu_sync_allowed():
740
+ split_sizes = num_audio_tokens.flatten().tolist()
741
return self._split_embeddings(audio_embeddings, split_sizes)
742
743
def _process_image_input(self, **kwargs) -> list[torch.Tensor] | None:
@@ -749,7 +755,12 @@ class MultiModalMixin(SupportsMultiModal, SupportsMRoPE):
755
756
num_image_patches = kwargs.pop("num_image_patches")
757
752
- vision_embeddings = self.model.get_image_features(pixel_values, **kwargs)
758
+ # The underlying HuggingFace `get_image_features` implementations
759
+ # contain model-internal syncs (e.g. Idefics3 filters all-zero
760
+ # padding images via boolean-mask indexing, LlavaOnevision
761
+ # branches on per-sample batch counts).
762
+ with gpu_sync_allowed():
763
+ vision_embeddings = self.model.get_image_features(pixel_values, **kwargs)
764
765
# Transformers `v5`, `self.get_image_features` returns a tuple
766
# containing the features and optionally attentions/hidden_states
vllm/model_executor/models/ultravox.py
+13
-8
@@ -43,6 +43,7 @@ from vllm.multimodal.processing import (
43
from vllm.renderers import TokenizeParams
44
from vllm.sequence import IntermediateTensors
45
from vllm.transformers_utils.configs.ultravox import UltravoxConfig
46
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
47
from vllm.utils.tensor_schema import TensorSchema, TensorShape
48
49
from .interfaces import (
@@ -841,15 +842,19 @@ class UltravoxModel(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA):
842
embeddings.shape[0], -1
843
)
844
mask = indices < audio_token_len[:, None]
844
- # Apply mask and flatten
845
- flattened_embeddings = embeddings[mask]
845
847
- # Return one tensor per input audio
848
- embed_lens = [
849
- chunk_lens.sum().item()
850
- for chunk_lens in audio_token_len.split(audio_input["num_chunks"].tolist())
851
- ]
852
- return flattened_embeddings.split(embed_lens)
846
+ with gpu_sync_allowed():
847
+ # Apply mask and flatten
848
+ flattened_embeddings = embeddings[mask]
849
+
850
+ # Return one tensor per input audio
851
+ embed_lens = [
852
+ chunk_lens.sum().item()
853
+ for chunk_lens in audio_token_len.split(
854
+ audio_input["num_chunks"].tolist()
855
+ )
856
+ ]
857
+ return flattened_embeddings.split(embed_lens)
858
859
def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
860
audio_input = self._parse_and_validate_audio_input(**kwargs)
vllm/third_party/flash_linear_attention/ops/index.py
+5
-6
@@ -9,6 +9,7 @@
9
# ruff: noqa: E501
10
import torch
11
12
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
13
from vllm.triton_utils import triton
14
15
from .utils import tensor_cache
@@ -21,12 +22,10 @@ def prepare_lens(cu_seqlens: torch.Tensor) -> torch.Tensor:
22
23
@tensor_cache
24
def prepare_chunk_indices(cu_seqlens: torch.Tensor, chunk_size: int) -> torch.Tensor:
24
- indices = torch.cat(
25
- [
26
- torch.arange(n)
27
- for n in triton.cdiv(prepare_lens(cu_seqlens), chunk_size).tolist()
28
- ]
29
- )
25
+ # This will be fixed by https://github.com/vllm-project/vllm/pull/51540.
26
+ with gpu_sync_allowed():
27
+ chunk_counts = triton.cdiv(prepare_lens(cu_seqlens), chunk_size).tolist()
28
+ indices = torch.cat([torch.arange(n) for n in chunk_counts])
29
chunk_indices = torch.stack([indices.eq(0).cumsum(0) - 1, indices], 1)
30
return chunk_indices.to(
31
device=cu_seqlens.device, dtype=cu_seqlens.dtype, non_blocking=True
vllm/v1/attention/backends/flashinfer.py
+10
-7
@@ -46,6 +46,7 @@ from vllm.utils.flashinfer import (
46
supports_trtllm_attention,
47
use_trtllm_attention,
48
)
49
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
50
from vllm.utils.math_utils import cdiv
51
from vllm.utils.torch_utils import (
52
PIN_MEMORY,
@@ -1408,13 +1409,15 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1409
# seq_lens_cpu is not needed since TRTLLM paths use GPU tensors
1410
# (block_tables, seq_lens) directly.
1411
needs_seq_lens_cpu = self.use_dcp or use_cascade or not all_uses_trtllm
1411
- seq_lens_cpu = common_attn_metadata.seq_lens_cpu if needs_seq_lens_cpu else None
1412
- seq_lens_np = seq_lens_cpu.numpy() if seq_lens_cpu is not None else None
1413
- num_blocks_np = (
1414
- (seq_lens_np + (page_size - 1)) // page_size
1415
- if seq_lens_np is not None
1416
- else None
1417
- )
1412
+ if needs_seq_lens_cpu:
1413
+ with gpu_sync_allowed():
1414
+ seq_lens_cpu = common_attn_metadata.seq_lens_cpu
1415
+ seq_lens_np = seq_lens_cpu.numpy()
1416
+ num_blocks_np = (seq_lens_np + (page_size - 1)) // page_size
1417
+ else:
1418
+ seq_lens_cpu = None
1419
+ seq_lens_np = None
1420
+ num_blocks_np = None
1421
1422
# Adjust seq_lens_cpu for DCP
1423
if self.use_dcp:
vllm/v1/attention/ops/vit_attn_wrappers.py
+20
-4
@@ -20,6 +20,7 @@ import torch.nn.functional as F
20
21
from vllm._aiter_ops import rocm_aiter_ops
22
from vllm.platforms import current_platform
23
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
24
from vllm.utils.torch_utils import direct_register_custom_op
25
26
@@ -50,7 +51,12 @@ def flash_attn_maxseqlen_wrapper(
51
cu_seqlens = torch.arange(
52
0, (batch_size + 1) * q_len, step=q_len, dtype=torch.int32, device=q.device
53
)
53
- max_seqlen = q_len if max_seqlen is None else max_seqlen.item()
54
+ if max_seqlen is None:
55
+ max_seqlen = q_len
56
+ else:
57
+ # `flash_attn_varlen_func` needs a Python int for kernel launch bounds.
58
+ with gpu_sync_allowed():
59
+ max_seqlen = max_seqlen.item()
60
61
q, k, v = (einops.rearrange(x, "b s ... -> (b s) ...") for x in [q, k, v])
62
output = flash_attn_varlen_func(
@@ -159,7 +165,12 @@ def triton_attn_wrapper(
165
cu_seqlens = torch.arange(
166
0, (batch_size + 1) * q_len, step=q_len, dtype=torch.int32, device=q.device
167
)
162
- max_seqlen = q_len if max_seqlen is None else max_seqlen.item()
168
+ if max_seqlen is None:
169
+ max_seqlen = q_len
170
+ else:
171
+ # `context_attention_fwd` needs a Python int.
172
+ with gpu_sync_allowed():
173
+ max_seqlen = max_seqlen.item()
174
175
q, k, v = (einops.rearrange(x, "b s ... -> (b s) ...") for x in [q, k, v])
176
output = torch.empty_like(q)
@@ -261,7 +272,9 @@ def torch_sdpa_wrapper(
272
273
outputs = []
274
264
- lens = (cu_seqlens[1:] - cu_seqlens[:-1]).tolist()
275
+ # `torch.split` needs Python int sizes.
276
+ with gpu_sync_allowed():
277
+ lens = (cu_seqlens[1:] - cu_seqlens[:-1]).tolist()
278
q_chunks = torch.split(q, lens, dim=1)
279
k_chunks = torch.split(k, lens, dim=1)
280
v_chunks = torch.split(v, lens, dim=1)
@@ -337,7 +350,10 @@ def flashinfer_wrapper(
350
batch_offsets_qko = cu_seqlens[:cu_seqlength].view(-1, 1, 1, 1)
351
batch_offsets_v = cu_seqlens[cu_seqlength:].view(-1, 1, 1, 1)
352
sequence_lengths = sequence_lengths.view(-1, 1, 1, 1)
340
- max_seqlen = max_seqlen.item()
353
+ # `cudnn_batch_prefill_with_kv_cache` needs Python ints for the
354
+ # max-token-per-seq bounds.
355
+ with gpu_sync_allowed():
356
+ max_seqlen = max_seqlen.item()
357
358
output, _ = cudnn_batch_prefill_with_kv_cache(
359
q,
vllm/v1/sample/ops/topk_topp_triton.py
+8
-6
@@ -12,6 +12,7 @@ using Pivot-based Truncation and Selection" By Park et al.
12
import torch
13
14
from vllm.triton_utils import tl, triton
15
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
16
from vllm.utils.math_utils import next_power_of_2
17
from vllm.utils.platform_utils import num_compute_units
18
@@ -919,12 +920,13 @@ def apply_top_k_top_p_triton(
920
# Cache lookup table entries on each device.
921
tables = _TRITON_TABLE_CACHE.get(logits.device)
922
if tables is None:
922
- normal_cdf_to_sigma_table = logits.new_tensor(_NORMAL_CDF_TO_SIGMA_TABLE)
923
- percentile_to_std_table = logits.new_tensor(_PERCENTILE_TO_STD_TABLE)
924
- _TRITON_TABLE_CACHE[logits.device] = (
925
- normal_cdf_to_sigma_table,
926
- percentile_to_std_table,
927
- )
923
+ with gpu_sync_allowed():
924
+ normal_cdf_to_sigma_table = logits.new_tensor(_NORMAL_CDF_TO_SIGMA_TABLE)
925
+ percentile_to_std_table = logits.new_tensor(_PERCENTILE_TO_STD_TABLE)
926
+ _TRITON_TABLE_CACHE[logits.device] = (
927
+ normal_cdf_to_sigma_table,
928
+ percentile_to_std_table,
929
+ )
930
else:
931
normal_cdf_to_sigma_table, percentile_to_std_table = tables
932
vllm/v1/sample/sampler.py
+5
-3
@@ -6,6 +6,7 @@ import torch
6
import torch.nn as nn
7
8
from vllm.config.model import LogprobsMode
9
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
10
from vllm.utils.torch_utils import PIN_MEMORY
11
from vllm.v1.outputs import LogprobsTensors, SamplerOutput
12
from vllm.v1.sample.metadata import SamplingMetadata
@@ -342,9 +343,10 @@ class Sampler(nn.Module):
343
# of the compiled batched_count_greater_than. mark_unbacked makes
344
# the size fully symbolic so dynamo doesn't specialize when
345
# batch_size transitions from 1 to >=2.
345
- torch._dynamo.decorators.mark_unbacked(logprobs, 0)
346
- torch._dynamo.decorators.mark_unbacked(token_logprobs, 0)
347
- token_ranks = batched_count_greater_than(logprobs, token_logprobs)
346
+ with gpu_sync_allowed(first_only=True):
347
+ torch._dynamo.decorators.mark_unbacked(logprobs, 0)
348
+ torch._dynamo.decorators.mark_unbacked(token_logprobs, 0)
349
+ token_ranks = batched_count_greater_than(logprobs, token_logprobs)
350
351
# Concatenate together with the topk.
352
indices = torch.cat((token_ids, topk_indices), dim=1)
vllm/v1/worker/dp_utils.py
+30
-21
@@ -1,12 +1,15 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
+from contextlib import nullcontext
5
+
6
import torch
7
import torch.distributed as dist
8
9
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.v1.worker.ubatch_utils import (
14
check_ubatch_thresholds,
15
is_last_ubatch_empty,
@@ -136,27 +139,33 @@ def _synchronize_dp_ranks(
139
parallel_config=parallel_config,
140
)
141
139
- # Synchronize cudagraph_mode across ranks first (take min).
140
- # This is needed before DP padding decision since we use the synced
141
- # cudagraph mode to determine whether DP padding is needed.
142
- synced_cudagraph_mode = _post_process_cudagraph_mode(tensor)
143
-
144
- # Check conditions for microbatching
145
- should_ubatch = _post_process_ubatch(tensor, parallel_config.num_ubatches)
146
-
147
- # DP padding is needed when cudagraph is enabled (synced across ranks)
148
- # or when ubatching/DBO is active (ubatching requires uniform batch
149
- # sizes across DP ranks currently).
150
- # Use the synced runtime cudagraph mode rather than the compilation config
151
- # so we can avoid padding when cudagraph is not enabled for this step.
152
- should_dp_pad = synced_cudagraph_mode != 0 or should_ubatch
153
-
154
- # Pad all DP ranks up to the maximum token count across ranks if
155
- # should_dp_pad is True
156
- num_tokens_after_padding = _post_process_dp_padding(
157
- tensor,
158
- should_dp_pad,
159
- )
142
+ # Only the NCCL path leaves `tensor` on device. With Gloo -- the default
143
+ # under async scheduling -- the all-reduce runs on CPU, so the reads below
144
+ # are host-side and the check should stay armed.
145
+ with (
146
+ nullcontext()
147
+ if parallel_config.disable_nccl_for_dp_synchronization
148
+ else gpu_sync_allowed()
149
+ ):
150
+ # Synchronize cudagraph_mode across ranks first (take min).
151
+ # This is needed before DP padding decision since we use the synced
152
+ # cudagraph mode to determine whether DP padding is needed.
153
+ synced_cudagraph_mode = _post_process_cudagraph_mode(tensor)
154
+
155
+ # Check conditions for microbatching
156
+ should_ubatch = _post_process_ubatch(tensor, parallel_config.num_ubatches)
157
+
158
+ # DP padding is needed when cudagraph is enabled (synced across ranks)
159
+ # or when ubatching/DBO is active (ubatching requires uniform batch
160
+ # sizes across DP ranks currently).
161
+ # Use the synced runtime cudagraph mode rather than the compilation
162
+ # config so we can avoid padding when cudagraph is not enabled for
163
+ # this step.
164
+ should_dp_pad = synced_cudagraph_mode != 0 or should_ubatch
165
+
166
+ # Pad all DP ranks up to the maximum token count across ranks if
167
+ # should_dp_pad is True
168
+ num_tokens_after_padding = _post_process_dp_padding(tensor, should_dp_pad)
169
170
return should_ubatch, num_tokens_after_padding, synced_cudagraph_mode
171
vllm/v1/worker/encoder_cudagraph.py
+17
-8
@@ -19,6 +19,7 @@ from vllm.model_executor.models.interfaces import (
19
)
20
from vllm.model_executor.models.utils import scatter_output_slices
21
from vllm.model_executor.models.vision import get_load_balance_assignment
22
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
23
from vllm.v1.worker.encoder_cudagraph_defs import (
24
EncoderCudaGraphConfig,
25
EncoderItemSpec,
@@ -298,7 +299,19 @@ class EncoderCudaGraphManager:
299
300
def _get_item_specs(self, mm_kwargs: dict[str, Any]) -> list[EncoderItemSpec]:
301
"""Get item specs from the model."""
301
- return self.model.get_encoder_cudagraph_item_specs(mm_kwargs)
302
+ # Implementations read per-item grid/patch counts off device tensors
303
+ # to size the cudagraph buffers, so the D2H is inherent here.
304
+ with gpu_sync_allowed():
305
+ return self.model.get_encoder_cudagraph_item_specs(mm_kwargs)
306
+
307
+ def _select_items(
308
+ self, mm_kwargs: dict[str, Any], indices: list[int]
309
+ ) -> dict[str, Any]:
310
+ """Select the mm kwargs for `indices` from the model."""
311
+ # Same as `_get_item_specs`: implementations re-read the per-item
312
+ # grid/patch counts to slice the batch, so the D2H is inherent.
313
+ with gpu_sync_allowed():
314
+ return self.model.select_encoder_cudagraph_items(mm_kwargs, indices)
315
316
def _get_per_item_out_tokens(self, mm_kwargs: dict[str, Any]) -> list[int]:
317
"""Get per-item output token counts as plain ints."""
@@ -415,9 +428,7 @@ class EncoderCudaGraphManager:
428
429
outputs_by_orig_idx: dict[int, torch.Tensor] = {}
430
for batch_indices, path_budgets in batches:
418
- batch_mm_kwargs = self.model.select_encoder_cudagraph_items(
419
- mm_kwargs, batch_indices
420
- )
431
+ batch_mm_kwargs = self._select_items(mm_kwargs, batch_indices)
432
graph_outputs: dict[str, torch.Tensor] = {}
433
all_eager = True
434
@@ -491,11 +502,9 @@ class EncoderCudaGraphManager:
502
]
503
504
if len(local_indices) > 0:
494
- local_mm_kwargs = self.model.select_encoder_cudagraph_items(
495
- mm_kwargs, local_indices
496
- )
505
+ local_mm_kwargs = self._select_items(mm_kwargs, local_indices)
506
else:
498
- local_mm_kwargs = self.model.select_encoder_cudagraph_items(mm_kwargs, [])
507
+ local_mm_kwargs = self._select_items(mm_kwargs, [])
508
509
max_output_tokens_per_rank = (
510
max(
vllm/v1/worker/gpu_model_runner.py
+27
-21
@@ -120,6 +120,7 @@ from vllm.sequence import IntermediateTensors
120
from vllm.tasks import GenerationTask, PoolingTask, SupportedTask
121
from vllm.tracing import instrument
122
from vllm.utils import length_from_prompt_token_ids_or_embeds
123
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
124
from vllm.utils.math_utils import cdiv, round_up
125
from vllm.utils.mem_utils import DeviceMemoryProfiler, format_gib
126
from vllm.utils.nvtx_pytorch_hooks import PytHooks
@@ -3848,26 +3849,28 @@ class GPUModelRunner(
3849
self.routed_experts_slot_mapping_device[:total],
3850
non_blocking=True,
3851
)
3851
-
3852
- # Get the valid generated tokens.
3853
- max_gen_len = sampled_token_ids.shape[-1]
3854
- if max_gen_len == 1:
3855
- # No spec decode tokens.
3856
- valid_sampled_token_ids = self._to_list(sampled_token_ids)
3857
- # Mask out the sampled tokens that should not be sampled.
3858
- for i in discard_sampled_tokens_req_indices:
3859
- valid_sampled_token_ids[int(i)].clear()
3860
-
3861
- if logprobs_tensors is not None:
3862
- logprobs_lists = logprobs_tensors.tolists()
3863
- else:
3864
- # Includes spec decode tokens.
3865
- valid_sampled_token_ids, logprobs_lists = RejectionSampler.parse_output(
3866
- sampled_token_ids,
3867
- self.input_batch.vocab_size,
3868
- discard_sampled_tokens_req_indices,
3869
- logprobs_tensors=logprobs_tensors,
3870
- )
3852
+ with gpu_sync_allowed():
3853
+ # Get the valid generated tokens.
3854
+ max_gen_len = sampled_token_ids.shape[-1]
3855
+ if max_gen_len == 1:
3856
+ # No spec decode tokens.
3857
+ valid_sampled_token_ids = self._to_list(sampled_token_ids)
3858
+ # Mask out the sampled tokens that should not be sampled.
3859
+ for i in discard_sampled_tokens_req_indices:
3860
+ valid_sampled_token_ids[int(i)].clear()
3861
+
3862
+ if logprobs_tensors is not None:
3863
+ logprobs_lists = logprobs_tensors.tolists()
3864
+ else:
3865
+ # Includes spec decode tokens.
3866
+ valid_sampled_token_ids, logprobs_lists = (
3867
+ RejectionSampler.parse_output(
3868
+ sampled_token_ids,
3869
+ self.input_batch.vocab_size,
3870
+ discard_sampled_tokens_req_indices,
3871
+ logprobs_tensors=logprobs_tensors,
3872
+ )
3873
+ )
3874
else:
3875
valid_sampled_token_ids = []
3876
invalid_req_indices = discard_sampled_tokens_req_indices.tolist()
@@ -5843,7 +5846,10 @@ class GPUModelRunner(
5846
on device instead; see`AsyncGPUModelRunnerOutput`.
5847
"""
5848
try:
5846
- counts = [] if logits is None else count_nans_per_row(logits).tolist()
5849
+ # Reporting per-request NaN counts requires them on the host; this
5850
+ # path is opt-in diagnostics, so the D2H is intended.
5851
+ with gpu_sync_allowed():
5852
+ counts = [] if logits is None else count_nans_per_row(logits).tolist()
5853
num_nans_in_logits = nans_to_dict(counts, self.input_batch.req_id_to_index)
5854
if envs.VLLM_RAISE_ON_LOGIT_NANS:
5855
raise_if_nan_logits(num_nans_in_logits)
vllm/v1/worker/mamba_utils.py
+16
@@ -16,6 +16,7 @@ from vllm.model_executor.layers.mamba.mamba_utils import (
16
is_conv_state_dim_first,
17
)
18
from vllm.triton_utils import tl, triton
19
+from vllm.utils.gpu_sync_debug import gpu_sync_allowed
20
from vllm.utils.math_utils import cdiv
21
from vllm.v1.core.sched.output import SchedulerOutput
22
from vllm.v1.kv_cache_interface import KVCacheConfig, MambaSpec
@@ -754,7 +755,22 @@ class MambaSpecDecodeGPUContext:
755
"""
756
if self.is_initialized:
757
return
758
+ # This only runs once per worker.
759
+ with gpu_sync_allowed():
760
+ self._populate_metadata(
761
+ kv_cache_config,
762
+ forward_context,
763
+ mamba_state_copy_funcs,
764
+ block_tables,
765
+ )
766
767
+ def _populate_metadata(
768
+ self,
769
+ kv_cache_config: KVCacheConfig,
770
+ forward_context: dict[str, Any],
771
+ mamba_state_copy_funcs: tuple[MambaStateCopyFunc, ...],
772
+ block_tables: list[torch.Tensor],
773
+ ) -> None:
774
idx = 0
775
for group_local_idx, mamba_group_id in enumerate(self.mamba_group_ids):
776
layer_names = kv_cache_config.kv_cache_groups[mamba_group_id].layer_names