@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 989c176c0a
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 4, 03:45 UTC

[Perf][3/n] Eliminate GPU<->CPU syncs in attention impls (#41434)

Signed-off-by: Nick Hill <nickhill123@gmail.com>

Nick Hill committed May 7, 2026 at 19:44 UTC 989c176c0a14e1adc5a9ba33cb5c3a39fceec3d3
10 files changed +169 -68
vllm/utils/torch_utils.py
+9 -3
@@ -3,6 +3,7 @@
3 import contextlib
4 import importlib.metadata
5 import os
6 +import platform
7 import random
8 import threading
9 from collections.abc import Callable, Collection
@@ -67,6 +68,11 @@ MODELOPT_TO_VLLM_KV_CACHE_DTYPE_MAP = {
68 T = TypeVar("T")
69
70
71 +# Pin memory in non-WSL case.
72 +# Logic duplicated here for now to avoid circular import.
73 +PIN_MEMORY = "microsoft" not in " ".join(platform.uname()).lower()
74 +
75 +
76 def is_quantized_kv_cache(kv_cache_dtype: str) -> bool:
77 return (
78 kv_cache_dtype.startswith("fp8")
@@ -602,12 +608,12 @@ def create_kv_caches_with_random(
608 def async_tensor_h2d(
609 data: list,
610 dtype: torch.dtype,
605 - target_device: str | torch.device,
606 - pin_memory: bool,
611 + device: str | torch.device,
612 + pin_memory: bool = PIN_MEMORY,
613 ) -> torch.Tensor:
614 """Asynchronously create a tensor and copy it from host to device."""
615 t = torch.tensor(data, dtype=dtype, pin_memory=pin_memory, device="cpu")
610 - return t.to(device=target_device, non_blocking=True)
616 + return t.to(device=device, non_blocking=True)
617
618
619 def make_ndarray_with_pad(
vllm/v1/attention/backends/flashinfer.py
+2 -1
@@ -1105,7 +1105,8 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1105 paged_kv_indptr_prefill_gpu = self.paged_kv_indptr.gpu[
1106 prefill_start : num_reqs + 1
1107 ]
1108 - paged_kv_indptr_prefill_gpu[0] = 0
1108 + # Assign to slice to avoid cpu sync.
1109 + paged_kv_indptr_prefill_gpu[:1] = 0
1110 torch.cumsum(
1111 num_blocks_per_req,
1112 dim=0,
vllm/v1/attention/backends/flex_attention.py
+30 -18
@@ -48,12 +48,16 @@ create_block_mask_compiled = torch.compile(
48 flex_attention_compiled = torch.compile(flex_attention, fullgraph=True)
49
50
51 -def _offsets_to_doc_ids_tensor(offsets: torch.Tensor) -> torch.Tensor:
52 - device = offsets.device
53 - counts = offsets[1:] - offsets[:-1]
54 - return torch.repeat_interleave(
55 - torch.arange(len(counts), device=device, dtype=torch.int32), counts
51 +def _offsets_to_doc_ids_tensor(
52 + offsets_cpu: torch.Tensor, device: torch.device
53 +) -> torch.Tensor:
54 + # Build on CPU (so `repeat_interleave` doesn't force a GPU->CPU sync to
55 + # learn the data-dependent output length) and upload non-blocking.
56 + counts = offsets_cpu[1:] - offsets_cpu[:-1]
57 + doc_ids = torch.repeat_interleave(
58 + torch.arange(len(counts), dtype=torch.int32), counts
59 )
60 + return doc_ids.to(device, non_blocking=True)
61
62
63 def pad_to_multiple(x: torch.Tensor, multiple: int, dim: int):
@@ -290,11 +294,13 @@ def unique_static_unsorted(
294 keep = (x_flat != ignored_val) & (idx == first_idx.gather(1, x_flat)) # [B, N]
295
296 # ── left-pack uniques into a fresh tensor ───────────────────────────
297 + # Route non-kept entries to a garbage slot at column N so we can do a
298 + # single scatter rather than using torch.nonzero (which would force a
299 + # GPU->CPU sync to enumerate kept positions).
300 dest_pos = torch.cumsum(keep.to(torch.long), dim=1) - 1 # where to go
294 - packed_flat = torch.full_like(x_flat, pad_val)
295 -
296 - rows, src_cols = torch.nonzero(keep, as_tuple=True)
297 - packed_flat[rows, dest_pos[rows, src_cols]] = x_flat[rows, src_cols]
301 + dest_pos = torch.where(keep, dest_pos, N)
302 + packed_extended = torch.full((B, N + 1), pad_val, device=device, dtype=x_flat.dtype)
303 + packed_flat = packed_extended.scatter_(1, dest_pos, x_flat)[:, :N]
304
305 # ── restore original layout ─────────────────────────────────────────
306 packed = packed_flat.reshape(x_perm.shape).movedim(-1, dim)
@@ -346,6 +352,9 @@ class FlexAttentionMetadata:
352 num_actual_tokens: int # Number of tokens excluding padding.
353 max_query_len: int
354 query_start_loc: torch.Tensor
355 + # CPU-resident copy of query_start_loc used to derive doc_ids without a
356 + # GPU->CPU sync from repeat_interleave's data-dependent output size.
357 + query_start_loc_cpu: torch.Tensor
358 max_seq_len: int
359 seq_lens: torch.Tensor
360 block_table: torch.Tensor
@@ -452,12 +461,7 @@ class FlexAttentionMetadata:
461 (is_valid, logical_q_idx, logical_kv_idx) = (
462 self._convert_physical_to_logical(self.doc_ids, q_idx, physical_kv_idx)
463 )
455 - # Apply mask modification only for valid indices
456 - return torch.where(
457 - is_valid,
458 - self.logical_mask_mod(b, h, logical_q_idx, logical_kv_idx),
459 - False,
460 - )
464 + return is_valid & self.logical_mask_mod(b, h, logical_q_idx, logical_kv_idx)
465
466 return final_mask_mod
467
@@ -469,7 +473,9 @@ class FlexAttentionMetadata:
473 packed query sequences.
474 """
475 # Create a lookup mapping from query indices -> request number
472 - request_lookup = _offsets_to_doc_ids_tensor(self.query_start_loc)
476 + request_lookup = _offsets_to_doc_ids_tensor(
477 + self.query_start_loc_cpu, self.query_start_loc.device
478 + )
479
480 def final_mask_mod(
481 b: torch.Tensor,
@@ -581,7 +587,9 @@ class FlexAttentionMetadata:
587 return None
588
589 # Create a lookup mapping from query indices -> request number
584 - request_lookup = _offsets_to_doc_ids_tensor(self.query_start_loc)
590 + request_lookup = _offsets_to_doc_ids_tensor(
591 + self.query_start_loc_cpu, self.query_start_loc.device
592 + )
593 user_score_mod = self.score_mod
594
595 def transformed_score_mod(
@@ -726,7 +734,9 @@ class FlexAttentionMetadata:
734 assert self.prefix_kv_lens is None, "Not implemented yet."
735 assert self.suffix_kv_lens is None, "Not implemented yet."
736 # Create a lookup mapping from query indices -> request number
729 - self.doc_ids = _offsets_to_doc_ids_tensor(self.query_start_loc)
737 + self.doc_ids = _offsets_to_doc_ids_tensor(
738 + self.query_start_loc_cpu, self.query_start_loc.device
739 + )
740 self.doc_ids = copy_to_persistent(self.persistent_doc_ids, self.doc_ids)
741 self.num_blocks = self.total_cache_tokens // self.block_size
742
@@ -807,6 +817,7 @@ class FlexAttentionMetadataBuilder(AttentionMetadataBuilder[FlexAttentionMetadat
817
818 max_seq_len = common_attn_metadata.max_seq_len
819 query_start_loc = common_attn_metadata.query_start_loc
820 + query_start_loc_cpu = common_attn_metadata.query_start_loc_cpu
821 seq_lens = common_attn_metadata.seq_lens
822 block_table_tensor = common_attn_metadata.block_table_tensor
823 slot_mapping = common_attn_metadata.slot_mapping
@@ -871,6 +882,7 @@ class FlexAttentionMetadataBuilder(AttentionMetadataBuilder[FlexAttentionMetadat
882 num_actual_tokens=num_actual_tokens,
883 max_query_len=max_query_len,
884 query_start_loc=query_start_loc,
885 + query_start_loc_cpu=query_start_loc_cpu,
886 max_seq_len=max_seq_len,
887 seq_lens=seq_lens,
888 block_table=block_table_tensor,
vllm/v1/attention/backends/mamba_attn.py
+18 -19
@@ -9,6 +9,7 @@ import torch
9
10 from vllm.config import VllmConfig
11 from vllm.utils.math_utils import cdiv
12 +from vllm.utils.torch_utils import async_tensor_h2d
13 from vllm.v1.attention.backend import (
14 AttentionCGSupport,
15 AttentionMetadataBuilder,
@@ -270,16 +271,20 @@ class BaseMambaAttentionMetadataBuilder(AttentionMetadataBuilder[M], abc.ABC):
271 num_prefills = common.num_prefills
272 num_decode_tokens = common.num_decode_tokens
273
273 - num_computed_tokens_cpu = (
274 - common_attn_metadata.compute_num_computed_tokens().cpu()
275 - )
276 - num_computed_tokens_p_cpu = num_computed_tokens_cpu[
277 - num_reqs - num_prefills : num_reqs
278 - ]
274 + # Derive prefill context lengths from CPU data only.
275 + # `seq_lens_cpu_upper_bound` is precise for prefill rows in all modes
276 + # (including async spec decode), so this avoids the D2H sync that
277 + # `compute_num_computed_tokens().cpu()` would force.
278 + seq_lens_cpu = common_attn_metadata.seq_lens_cpu_upper_bound
279 + assert seq_lens_cpu is not None
280 query_start_loc_p_cpu = (
281 common_attn_metadata.query_start_loc_cpu[-num_prefills - 1 :]
282 - num_decode_tokens
283 )
284 + prefill_query_lens_cpu = query_start_loc_p_cpu[1:] - query_start_loc_p_cpu[:-1]
285 + num_computed_tokens_p_cpu = (
286 + seq_lens_cpu[num_reqs - num_prefills : num_reqs] - prefill_query_lens_cpu
287 + )
288
289 cu_chunk_seqlen, seq_idx, last_chunk_indices = self._compute_chunk_metadata(
290 chunk_size,
@@ -289,20 +294,14 @@ class BaseMambaAttentionMetadataBuilder(AttentionMetadataBuilder[M], abc.ABC):
294 )
295
296 device = common_attn_metadata.query_start_loc.device
292 - cu_chunk_seqlen_p = torch.as_tensor(
293 - cu_chunk_seqlen,
294 - device=device,
295 - dtype=torch.int32,
296 - )
297 - seq_idx_p = torch.as_tensor(
298 - seq_idx,
299 - device=device,
300 - dtype=torch.int32,
297 + # Build on pinned CPU and upload non-blocking to avoid the synchronous
298 + # H2D copy that `torch.as_tensor(list, device=cuda)` would force.
299 + cu_chunk_seqlen_p = async_tensor_h2d(
300 + cu_chunk_seqlen, dtype=torch.int32, device=device
301 )
302 - last_chunk_indices_p = torch.as_tensor(
303 - last_chunk_indices,
304 - device=device,
305 - dtype=torch.int32,
302 + seq_idx_p = async_tensor_h2d(seq_idx, dtype=torch.int32, device=device)
303 + last_chunk_indices_p = async_tensor_h2d(
304 + last_chunk_indices, dtype=torch.int32, device=device
305 )
306 return cu_chunk_seqlen_p, seq_idx_p, last_chunk_indices_p
307
vllm/v1/attention/backends/tree_attn.py
+52 -6
@@ -90,6 +90,14 @@ class TreeAttentionMetadata:
90 num_prefills: int = 0
91 num_decodes: int = 0
92
93 + # Precomputed (on CPU in the builder) max_query_len and max_seq_len for
94 + # the prefill-only and decode-only sub-batches. Used by the properties
95 + # below to avoid a GPU->CPU sync via `.max().item()` on every forward.
96 + max_query_len_prefill: int = 0
97 + max_seq_len_prefill: int = 0
98 + max_query_len_decode: int = 0
99 + max_seq_len_decode: int = 0
100 +
101 tree_attn_bias: torch.Tensor | None = None
102
103 # Cached Prefill/decode metadata.
@@ -107,14 +115,13 @@ class TreeAttentionMetadata:
115 return self._cached_prefill_metadata
116
117 q_start_loc = self.query_start_loc[self.num_decodes :]
110 - q_seqlens = torch.diff(q_start_loc)
118 kv_seqlens = self.seq_lens[self.num_decodes :]
119 # Construct & cache prefill-phase attention metadata structure
120 self._cached_prefill_metadata = TreeAttentionMetadata(
121 num_actual_tokens=self.num_prefill_tokens,
115 - max_query_len=int(q_seqlens.max().item()),
122 + max_query_len=self.max_query_len_prefill,
123 query_start_loc=q_start_loc - q_start_loc[0],
117 - max_seq_len=int(kv_seqlens.max().item()),
124 + max_seq_len=self.max_seq_len_prefill,
125 seq_lens=kv_seqlens,
126 block_table=self.block_table[self.num_decodes :],
127 slot_mapping=self.slot_mapping[self.num_decode_tokens :],
@@ -132,14 +139,13 @@ class TreeAttentionMetadata:
139 return self._cached_decode_metadata
140
141 q_start_loc = self.query_start_loc[: self.num_decodes + 1]
135 - q_seqlens = torch.diff(q_start_loc)
142 kv_seqlens = self.seq_lens[: self.num_decodes]
143 # Construct & cache decode-phase attention metadata structure
144 self._cached_decode_metadata = TreeAttentionMetadata(
145 num_actual_tokens=self.num_decode_tokens,
140 - max_query_len=int(q_seqlens.max().item()),
146 + max_query_len=self.max_query_len_decode,
147 query_start_loc=q_start_loc,
142 - max_seq_len=int(kv_seqlens.max().item()),
148 + max_seq_len=self.max_seq_len_decode,
149 seq_lens=kv_seqlens,
150 block_table=self.block_table[: self.num_decodes],
151 slot_mapping=self.slot_mapping[: self.num_decode_tokens],
@@ -199,6 +205,42 @@ class TreeAttentionMetadataBuilder(AttentionMetadataBuilder[TreeAttentionMetadat
205 block_table = common_attn_metadata.block_table_tensor
206 slot_mapping = common_attn_metadata.slot_mapping
207
208 + # Precompute prefill/decode sub-batch max_query_len / max_seq_len on
209 + # CPU so the prefill_metadata / decode_metadata properties don't need
210 + # a GPU->CPU sync via `.max().item()` on every forward.
211 + # Prefer `seq_lens_cpu_upper_bound` over the (deprecated)
212 + # `seq_lens_cpu` property: the upper bound is precise for prefill
213 + # rows and optimistic-but-safe for decode rows (workspace sizing
214 + # from `max()` is fine with an over-estimate), and avoids the
215 + # `seq_lens.to("cpu")` sync the property would fall through to in
216 + # async-spec-decode mode. The draft-attention path (eagle
217 + # speculator) doesn't populate it; fall back to the batch-wide
218 + # `max_seq_len` as a safe upper bound for both sub-batches.
219 + q_start_loc_cpu = common_attn_metadata.query_start_loc_cpu
220 + seq_lens_cpu = common_attn_metadata.seq_lens_cpu_upper_bound
221 + if num_prefills > 0:
222 + q_seqlens_p = torch.diff(q_start_loc_cpu[num_decodes:])
223 + max_query_len_prefill = int(q_seqlens_p.max())
224 + max_seq_len_prefill = (
225 + int(seq_lens_cpu[num_decodes:].max())
226 + if seq_lens_cpu is not None
227 + else max_seq_len
228 + )
229 + else:
230 + max_query_len_prefill = 0
231 + max_seq_len_prefill = 0
232 + if num_decodes > 0:
233 + q_seqlens_d = torch.diff(q_start_loc_cpu[: num_decodes + 1])
234 + max_query_len_decode = int(q_seqlens_d.max())
235 + max_seq_len_decode = (
236 + int(seq_lens_cpu[:num_decodes].max())
237 + if seq_lens_cpu is not None
238 + else max_seq_len
239 + )
240 + else:
241 + max_query_len_decode = 0
242 + max_seq_len_decode = 0
243 +
244 return TreeAttentionMetadata(
245 num_actual_tokens=num_actual_tokens,
246 num_prefill_tokens=num_prefill_tokens,
@@ -211,6 +253,10 @@ class TreeAttentionMetadataBuilder(AttentionMetadataBuilder[TreeAttentionMetadat
253 seq_lens=kv_seqlens,
254 block_table=block_table,
255 slot_mapping=slot_mapping,
256 + max_query_len_prefill=max_query_len_prefill,
257 + max_seq_len_prefill=max_seq_len_prefill,
258 + max_query_len_decode=max_query_len_decode,
259 + max_seq_len_decode=max_seq_len_decode,
260 tree_attn_bias=self.tree_attn_bias,
261 )
262
vllm/v1/attention/backends/triton_attn.py
+4 -5
@@ -18,7 +18,7 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
18 from vllm.platforms import current_platform
19 from vllm.platforms.interface import DeviceCapability
20 from vllm.utils.math_utils import next_power_of_2
21 -from vllm.utils.torch_utils import is_quantized_kv_cache
21 +from vllm.utils.torch_utils import async_tensor_h2d, is_quantized_kv_cache
22 from vllm.v1.attention.backend import (
23 AttentionBackend,
24 AttentionCGSupport,
@@ -117,10 +117,9 @@ class TritonAttentionMetadata:
117 for r in range_lists:
118 padded_r = list(r) + [(0, 0)] * (max_ranges - len(r))
119 padded.append(padded_r)
120 - # Create tensor with efficient H2D transfer
121 - return torch.tensor(padded, dtype=torch.int32, device=device).view(
122 - num_seqs, max_ranges, 2
123 - )
120 + # Build on pinned CPU memory so the H2D transfer is non-blocking.
121 + padded = async_tensor_h2d(padded, dtype=torch.int32, device=device)
122 + return padded.view(num_seqs, max_ranges, 2)
123
124
125 class TritonAttentionMetadataBuilder(AttentionMetadataBuilder[TritonAttentionMetadata]):
vllm/v1/attention/backends/turboquant_attn.py
+37 -9
@@ -187,6 +187,10 @@ class TurboQuantMetadata(AttentionMetadata):
187 is_prefill: bool = False
188 num_decodes: int = 0 # number of decode requests (first in batch)
189 num_decode_tokens: int = 0 # tokens from decode requests
190 + # CPU-resident copies used by the prefill path for per-request iteration
191 + # without per-step D2H syncs.
192 + query_start_loc_cpu: torch.Tensor | None = None
193 + seq_lens_cpu: torch.Tensor | None = None
194
195
196 class TurboQuantMetadataBuilder(AttentionMetadataBuilder[TurboQuantMetadata]):
@@ -230,6 +234,8 @@ class TurboQuantMetadataBuilder(AttentionMetadataBuilder[TurboQuantMetadata]):
234 is_prefill=(cam.max_query_len > 1),
235 num_decodes=num_decodes,
236 num_decode_tokens=num_decode_tokens,
237 + query_start_loc_cpu=cam.query_start_loc_cpu,
238 + seq_lens_cpu=cam.seq_lens_cpu_upper_bound,
239 )
240
241
@@ -474,11 +480,21 @@ class TurboQuantAttentionImpl(AttentionImpl["TurboQuantMetadata"]):
480 # first-chunk prefills. Using full-batch max_seq_len breaks
481 # this because decode requests inflate max_seq_len.
482 prefill_seq_lens = attn_metadata.seq_lens[num_decodes:]
477 - # Use CPU-side max to avoid GPU→CPU sync from .item()
478 - prefill_max_seq = max(attn_metadata.seq_lens[num_decodes:].tolist())
483 + # Use the CPU-resident `seq_lens` upper-bound from the metadata
484 + # (populated in the builder) to compute the prefill sub-batch
485 + # max without a GPU→CPU sync.
486 + if attn_metadata.seq_lens_cpu is not None:
487 + prefill_max_seq = int(attn_metadata.seq_lens_cpu[num_decodes:].max())
488 + else:
489 + prefill_max_seq = attn_metadata.max_seq_len
490 prefill_qsl = (
491 attn_metadata.query_start_loc[num_decodes:] - num_decode_tokens
492 )
493 + prefill_qsl_cpu = None
494 + if attn_metadata.query_start_loc_cpu is not None:
495 + prefill_qsl_cpu = (
496 + attn_metadata.query_start_loc_cpu[num_decodes:] - num_decode_tokens
497 + )
498 prefill_meta = TurboQuantMetadata(
499 seq_lens=prefill_seq_lens,
500 slot_mapping=attn_metadata.slot_mapping[num_decode_tokens:N],
@@ -488,6 +504,10 @@ class TurboQuantAttentionImpl(AttentionImpl["TurboQuantMetadata"]):
504 max_query_len=attn_metadata.max_query_len,
505 max_seq_len=prefill_max_seq,
506 is_prefill=True,
507 + query_start_loc_cpu=prefill_qsl_cpu,
508 + seq_lens_cpu=attn_metadata.seq_lens_cpu[num_decodes:]
509 + if attn_metadata.seq_lens_cpu is not None
510 + else None,
511 )
512 k = key[:N].view(N, self.num_kv_heads, self.head_size)
513 v = value[:N].view(N, self.num_kv_heads, self.head_size)
@@ -578,10 +598,16 @@ class TurboQuantAttentionImpl(AttentionImpl["TurboQuantMetadata"]):
598
599 output = torch.zeros(N, Hq, D, device=query.device, dtype=query.dtype)
600
581 - # Convert to Python lists once (single CPU-GPU sync) instead of
582 - # per-request .item() calls that each force a sync.
583 - qsl = query_start_loc.tolist()
584 - seq_lens_list = attn_metadata.seq_lens.tolist()
601 + # Prefer the CPU-resident copies from the metadata if populated —
602 + # otherwise `.tolist()` on GPU tensors forces a synchronizing copy.
603 + if attn_metadata.query_start_loc_cpu is not None:
604 + qsl = attn_metadata.query_start_loc_cpu.tolist()
605 + else:
606 + qsl = query_start_loc.tolist()
607 + if attn_metadata.seq_lens_cpu is not None:
608 + seq_lens_list = attn_metadata.seq_lens_cpu.tolist()
609 + else:
610 + seq_lens_list = attn_metadata.seq_lens.tolist()
611
612 # Pre-allocate cu_seqlens for single-request flash_attn calls
613 # to avoid per-request host→device tensor creation.
@@ -612,7 +638,8 @@ class TurboQuantAttentionImpl(AttentionImpl["TurboQuantMetadata"]):
638 if q_len == seq_len:
639 # First-chunk prefill: all K/V are in the current batch.
640 if _HAS_FLASH_ATTN:
615 - self._cu_2[1] = q_len
641 + # Assign to slice to avoid gpu/cpu sync.
642 + self._cu_2[1:2] = q_len
643 cu = self._cu_2
644 out = self._flash_attn_varlen(
645 q=q_seq,
@@ -791,8 +818,9 @@ class TurboQuantAttentionImpl(AttentionImpl["TurboQuantMetadata"]):
818 if not hasattr(self, "_cu_2_q"):
819 self._cu_2_q = torch.zeros(2, device=device, dtype=torch.int32)
820 self._cu_2_k = torch.zeros(2, device=device, dtype=torch.int32)
794 - self._cu_2_q[1] = q_len
795 - self._cu_2_k[1] = seq_len
821 + # Assigning to slice uses fill_ which avoids cpu/gpu sync.
822 + self._cu_2_q[1:2] = q_len
823 + self._cu_2_k[1:2] = seq_len
824 cu_seqlens_q = self._cu_2_q
825 cu_seqlens_k = self._cu_2_k
826 return self._flash_attn_varlen(
vllm/v1/attention/backends/utils.py
+15 -4
@@ -332,8 +332,10 @@ def make_local_attention_virtual_batches(
332 # regression when using numpy arrays (batch and block indices) to index into
333 # torch tensor (block_table). As a workaround, convert numpy arrays to torch
334 # tensor first, which recovers perf.
335 - batch_indices_torch = torch.from_numpy(batch_indices)
336 - block_indices_torch = torch.from_numpy(block_indices)
335 + # Upload the index tensors to the block_table's device up-front so that the
336 + # fancy indexing below doesn't implicitly force a synchronous H2D copy.
337 + batch_indices_torch = torch.from_numpy(batch_indices).to(device, non_blocking=True)
338 + block_indices_torch = torch.from_numpy(block_indices).to(device, non_blocking=True)
339
340 # Save as a lambda so we can return this for update_block_table
341 make_block_table = lambda block_table: block_table[
@@ -391,7 +393,16 @@ def make_kv_sharing_fast_prefill_common_attn_metadata(
393
394 # Figure out how many tokens are in each request
395 # num_decode_tokens: [1, 2, 1]
394 - num_decode_tokens = torch.bincount(request_ids, minlength=num_reqs)
396 + # Avoid `torch.bincount` here — on CUDA it forces a sync to determine
397 + # the output size (even with `minlength`, the kernel must confirm no
398 + # value exceeds the bound). `scatter_add_` into a preallocated buffer
399 + # is equivalent and stays async.
400 + num_decode_tokens = torch.zeros(
401 + num_reqs, dtype=request_ids.dtype, device=request_ids.device
402 + )
403 + num_decode_tokens.scatter_add_(
404 + 0, request_ids.to(num_decode_tokens.dtype), torch.ones_like(request_ids)
405 + )
406
407 # Calculate new query_start_loc with tokens in generation_indices
408 # decode_query_start_loc: [0, 1, 3, 4]
@@ -399,7 +410,7 @@ def make_kv_sharing_fast_prefill_common_attn_metadata(
410 num_reqs + 1, device=query_start_loc.device, dtype=query_start_loc.dtype
411 )
412
402 - decode_query_start_loc[0] = 0
413 + decode_query_start_loc[:1].fill_(0) # Avoid sync from scalar assignment.
414 decode_query_start_loc[1:] = torch.cumsum(num_decode_tokens, dim=0)
415 decode_max_query_len = int(num_decode_tokens.max().item())
416 total_num_decode_tokens = int(num_decode_tokens.sum().item())
vllm/v1/worker/gpu/buffer_utils.py
+1 -1
@@ -167,7 +167,7 @@ class StagedWriteTensor:
167
168 # Special handling for write_contents
169 write_contents = async_tensor_h2d(
170 - self._staged_write_contents, self.dtype, self.device, pin_memory=True
170 + self._staged_write_contents, self.dtype, self.device
171 )
172
173 # Write diffs to the GPU buffer
vllm/v1/worker/gpu/sample/penalties.py
+1 -2
@@ -58,8 +58,7 @@ class PenaltiesState:
58 idx_mapping = async_tensor_h2d(
59 self._new_penalties_reqs,
60 dtype=torch.int32,
61 - target_device=self.device,
62 - pin_memory=True,
61 + device=self.device,
62 )
63
64 prefill_lens = self.req_states.prefill_len.np[self._new_penalties_reqs]