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, 15:00 UTC
[Attention][TurboQuant] Share dequant buffers, eliminate float16_copy (#40941)
Signed-off-by: Bhoomit Vasani <bhoomit.2010@gmail.com> Signed-off-by: Vasani Bhoomit <bhoomit.2010@gmail.com>
Bhoomit committed
Apr 26, 2026 at 22:48 UTC
2cc008e7b491bb77f0caf4d27ad55a83f196114c
4 files changed
+98
-92
vllm/model_executor/layers/attention/attention.py
-48
@@ -386,10 +386,6 @@ class Attention(nn.Module, AttentionLayerBase):
386
# Initialize KV cache quantization attributes
387
_init_kv_cache_quant(self, quant_config, prefix)
388
389
- # Initialize TurboQuant buffers (Pi, S, centroids) if tq cache dtype
390
- if kv_cache_dtype.startswith("turboquant_"):
391
- self._init_turboquant_buffers(kv_cache_dtype, head_size, prefix)
392
-
389
# for attn backends supporting query quantization
390
self.query_quant = None
391
if (
@@ -410,50 +406,6 @@ class Attention(nn.Module, AttentionLayerBase):
406
else GroupShape.PER_TENSOR,
407
)
408
413
- def _init_turboquant_buffers(
414
- self, cache_dtype: str, head_size: int, prefix: str
415
- ) -> None:
416
- """Initialize TurboQuant centroids for Lloyd-Max quantization."""
417
- from vllm.model_executor.layers.quantization.turboquant.centroids import (
418
- get_centroids,
419
- )
420
- from vllm.model_executor.layers.quantization.turboquant.config import (
421
- TurboQuantConfig,
422
- )
423
-
424
- tq_config = TurboQuantConfig.from_cache_dtype(cache_dtype, head_size)
425
-
426
- self.register_buffer(
427
- "_tq_centroids",
428
- get_centroids(head_size, tq_config.centroid_bits),
429
- )
430
- self._tq_config = tq_config
431
-
432
- # Pre-allocate decode intermediate buffers so model.to(device) moves
433
- # them to GPU *before* the memory profiler runs. Without this the
434
- # profiler gives all free memory to KV cache blocks and the first
435
- # decode OOMs when these buffers are lazily allocated.
436
- _vllm_cfg = get_current_vllm_config()
437
- B = _vllm_cfg.scheduler_config.max_num_seqs
438
- Hq = self.num_heads
439
- S = _vllm_cfg.attention_config.tq_max_kv_splits_for_cuda_graph
440
- D = head_size
441
- self.register_buffer(
442
- "_tq_mid_o_buf",
443
- torch.empty(B, Hq, S, D + 1, dtype=torch.float32),
444
- persistent=False,
445
- )
446
- self.register_buffer(
447
- "_tq_output_buf",
448
- torch.empty(B, Hq, D, dtype=torch.float32),
449
- persistent=False,
450
- )
451
- self.register_buffer(
452
- "_tq_lse_buf",
453
- torch.empty(B, Hq, dtype=torch.float32),
454
- persistent=False,
455
- )
456
-
409
def forward(
410
self,
411
query: torch.Tensor,
vllm/v1/attention/backends/turboquant_attn.py
+83
-40
@@ -26,6 +26,9 @@ import torch.nn.functional as F
26
27
from vllm.config import get_current_vllm_config
28
from vllm.config.cache import CacheDType
29
+from vllm.model_executor.layers.quantization.turboquant.centroids import (
30
+ get_centroids,
31
+)
32
from vllm.triton_utils import triton
33
from vllm.v1.attention.backend import (
34
AttentionBackend,
@@ -49,6 +52,10 @@ from vllm.v1.attention.ops.triton_turboquant_decode import (
52
triton_turboquant_decode_attention,
53
)
54
from vllm.v1.attention.ops.triton_turboquant_store import triton_turboquant_store
55
+from vllm.v1.worker.workspace import (
56
+ current_workspace_manager,
57
+ is_workspace_manager_initialized,
58
+)
59
60
_HAS_FLASH_ATTN = is_flash_attn_varlen_func_available()
61
if _HAS_FLASH_ATTN:
@@ -335,9 +342,15 @@ class TurboQuantAttentionImpl(AttentionImpl["TurboQuantMetadata"]):
342
H = _build_hadamard(D, str(device))
343
layer._tq_PiT = H
344
layer._tq_Pi = H
345
+ # fp16 copy for rotation in continuation prefill path
346
+ layer._tq_Pi_half = H.to(torch.float16)
347
339
- c = layer._tq_centroids.to(device=device, dtype=torch.float32)
340
- c_sorted, _ = c.sort()
348
+ # Centroids for Lloyd-Max quantization.
349
+ layer._tq_centroids = get_centroids(D, self.tq_config.centroid_bits).to(
350
+ device=device, dtype=torch.float32
351
+ )
352
+
353
+ c_sorted, _ = layer._tq_centroids.sort()
354
layer._tq_midpoints = (c_sorted[:-1] + c_sorted[1:]) / 2
355
layer._tq_cached = True
356
@@ -572,7 +585,17 @@ class TurboQuantAttentionImpl(AttentionImpl["TurboQuantMetadata"]):
585
586
# Pre-allocate cu_seqlens for single-request flash_attn calls
587
# to avoid per-request host→device tensor creation.
575
- _cu_2 = torch.zeros(2, device=query.device, dtype=torch.int32)
588
+ if not hasattr(self, "_cu_2"):
589
+ self._cu_2 = torch.zeros(2, device=query.device, dtype=torch.int32)
590
+ # Cache arange on self (avoid per-call kernel launch).
591
+ _max_seq = attn_metadata.max_seq_len
592
+ _ac: torch.Tensor | None = getattr(self, "_arange_cache", None)
593
+ if _ac is None or _ac.shape[0] <= _max_seq:
594
+ _ac = torch.arange(
595
+ 0, _max_seq + 1, device=query.device, dtype=attn_metadata.seq_lens.dtype
596
+ )
597
+ self._arange_cache = _ac
598
+ _arange_cache: torch.Tensor = _ac
599
600
for i in range(num_reqs):
601
q_start = qsl[i]
@@ -589,8 +612,8 @@ class TurboQuantAttentionImpl(AttentionImpl["TurboQuantMetadata"]):
612
if q_len == seq_len:
613
# First-chunk prefill: all K/V are in the current batch.
614
if _HAS_FLASH_ATTN:
592
- _cu_2[1] = q_len
593
- cu = _cu_2
615
+ self._cu_2[1] = q_len
616
+ cu = self._cu_2
617
out = self._flash_attn_varlen(
618
q=q_seq,
619
k=k_seq,
@@ -622,12 +645,8 @@ class TurboQuantAttentionImpl(AttentionImpl["TurboQuantMetadata"]):
645
if q_len <= _CONTINUATION_DECODE_THRESHOLD:
646
# Fast path: treat each query as a decode request
647
# with incremental seq_lens for causal masking.
625
- synth_seq_lens = torch.arange(
626
- cached_len + 1,
627
- seq_len + 1,
628
- device=query.device,
629
- dtype=attn_metadata.seq_lens.dtype,
630
- )
648
+ # Slice from pre-built arange (no kernel launch)
649
+ synth_seq_lens = _arange_cache[cached_len + 1 : seq_len + 1]
650
synth_bt = attn_metadata.block_table[i : i + 1].expand(q_len, -1)
651
out = triton_turboquant_decode_attention(
652
query=q_seq,
@@ -695,16 +714,17 @@ class TurboQuantAttentionImpl(AttentionImpl["TurboQuantMetadata"]):
714
# Reuse cached buffers to avoid per-call allocation (~16MB at 8K).
715
alloc_len = math.ceil(cached_len / block_size) * block_size
716
buf_shape = (1, Hk, alloc_len, D)
698
- k_buf = getattr(layer, "_tq_k_dequant_buf", None)
699
- if k_buf is None or k_buf.shape[2] < alloc_len:
700
- k_buf = torch.empty(buf_shape, dtype=torch.float16, device=device)
701
- v_buf = torch.empty(buf_shape, dtype=torch.float16, device=device)
702
- layer._tq_k_dequant_buf = k_buf
703
- layer._tq_v_dequant_buf = v_buf
704
- else:
705
- v_buf = layer._tq_v_dequant_buf
706
- k_cached = k_buf[:, :, :alloc_len, :].zero_()
707
- v_cached = v_buf[:, :, :alloc_len, :].zero_()
717
+ # Use WorkspaceManager for dequant buffers.
718
+ # Shared across all layers — saves 60× memory at long context.
719
+ # Required for CUDA Graph capture (per-layer growth incompatible with CG).
720
+ k_buf, v_buf = current_workspace_manager().get_simultaneous(
721
+ (buf_shape, torch.float16),
722
+ (buf_shape, torch.float16),
723
+ )
724
+ # Skip .zero_() — kernel writes all positions up to cached_len,
725
+ # and we only read [:cached_len] afterwards.
726
+ k_cached = k_buf[:, :, :alloc_len, :]
727
+ v_cached = v_buf[:, :, :alloc_len, :]
728
729
grid = (alloc_len, 1 * Hk)
730
_tq_full_dequant_kv[grid](
@@ -740,29 +760,41 @@ class TurboQuantAttentionImpl(AttentionImpl["TurboQuantMetadata"]):
760
761
# Inverse-rotate MSE keys back to original space
762
if not self.tq_config.key_fp8:
743
- k_flat = k_cached[0, :, :cached_len, :].reshape(-1, D).float()
744
- k_flat = k_flat @ Pi
745
- k_cached_trim = (
746
- k_flat.to(torch.float16).reshape(Hk, cached_len, D).transpose(0, 1)
747
- ) # (cached_len, Hk, D)
763
+ # fp16 matmul for rotation (2× less bandwidth, uses fp16 tensor cores)
764
+ Pi_half = layer._tq_Pi_half
765
+ k_flat = k_cached[0, :, :cached_len, :].reshape(-1, D)
766
+ k_flat = k_flat @ Pi_half
767
+ k_cached_trim = k_flat.reshape(Hk, cached_len, D).transpose(
768
+ 0, 1
769
+ ) # (cached_len, Hk, D) — already fp16
770
else:
749
- k_cached_trim = (
750
- k_cached[0, :, :cached_len, :].transpose(0, 1).contiguous()
771
+ k_cached_trim = k_cached[0, :, :cached_len, :].transpose(
772
+ 0, 1
773
) # (cached_len, Hk, D)
774
753
- v_cached_trim = (
754
- v_cached[0, :, :cached_len, :].transpose(0, 1).contiguous()
755
- ) # (cached_len, Hk, D)
775
+ # Skip .contiguous() — the copy into k_full/v_full handles layout
776
+ v_cached_trim = v_cached[0, :, :cached_len, :].transpose(0, 1)
777
778
# Concatenate cached + current chunk K/V (match query dtype)
779
+ # Pre-allocate full K/V buffer, copy into slices (no cat alloc)
780
qdtype = query.dtype
759
- k_full = torch.cat([k_cached_trim.to(qdtype), key_chunk], dim=0)
760
- v_full = torch.cat([v_cached_trim.to(qdtype), val_chunk], dim=0)
781
+ k_full = torch.empty(seq_len, Hk, D, dtype=qdtype, device=device)
782
+ v_full = torch.empty(seq_len, Hk, D, dtype=qdtype, device=device)
783
+ k_full[:cached_len] = k_cached_trim.to(qdtype)
784
+ k_full[cached_len:] = key_chunk
785
+ v_full[:cached_len] = v_cached_trim.to(qdtype)
786
+ v_full[cached_len:] = val_chunk
787
788
# Attention: q_len queries attending to seq_len K/V with causal mask
789
if _HAS_FLASH_ATTN:
764
- cu_seqlens_q = torch.tensor([0, q_len], device=device, dtype=torch.int32)
765
- cu_seqlens_k = torch.tensor([0, seq_len], device=device, dtype=torch.int32)
790
+ # Reuse pre-allocated cu_seqlens (avoid host→device transfer)
791
+ if not hasattr(self, "_cu_2_q"):
792
+ self._cu_2_q = torch.zeros(2, device=device, dtype=torch.int32)
793
+ 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
796
+ cu_seqlens_q = self._cu_2_q
797
+ cu_seqlens_k = self._cu_2_k
798
return self._flash_attn_varlen(
799
q=query,
800
k=k_full,
@@ -805,12 +837,23 @@ class TurboQuantAttentionImpl(AttentionImpl["TurboQuantMetadata"]):
837
PiT: torch.Tensor | None = None,
838
layer: torch.nn.Module | None = None,
839
) -> torch.Tensor:
808
- # Grab cached decode buffers from the layer (lazily allocated).
840
+ # Acquire shared decode scratch buffers from WorkspaceManager.
841
+ # Layers execute sequentially so one set of buffers is sufficient.
842
+ # Falls back to kernel-internal allocation if workspace unavailable.
843
+ B = query.shape[0]
844
+ D = self.head_size
845
+ S = self.max_num_kv_splits
846
+ Hq = self.num_heads
847
mid_o_buf = output_buf = lse_buf = None
810
- if layer is not None:
811
- mid_o_buf = getattr(layer, "_tq_mid_o_buf", None)
812
- output_buf = getattr(layer, "_tq_output_buf", None)
813
- lse_buf = getattr(layer, "_tq_lse_buf", None)
848
+ if is_workspace_manager_initialized():
849
+ # output_buf in query dtype — matches the in-kernel fp16 cast in stage2.
850
+ mid_o_buf, output_buf, lse_buf = (
851
+ current_workspace_manager().get_simultaneous(
852
+ ((B, Hq, S, D + 1), torch.float32),
853
+ ((B, Hq, D), query.dtype),
854
+ ((B, Hq), torch.float32),
855
+ )
856
+ )
857
858
result = triton_turboquant_decode_attention(
859
query=query,
vllm/v1/attention/ops/triton_decode_attention.py
+5
-1
@@ -551,6 +551,7 @@ def _fwd_kernel_stage2(
551
NUM_KV_SPLITS: tl.constexpr,
552
BLOCK_DV: tl.constexpr,
553
Lv: tl.constexpr,
554
+ OUTPUT_FP16: tl.constexpr = 0,
555
):
556
cur_batch = tl.program_id(0)
557
cur_head = tl.program_id(1)
@@ -587,9 +588,12 @@ def _fwd_kernel_stage2(
588
e_sum = e_sum * old_scale + exp_logic
589
e_max = n_e_max
590
591
+ result = acc / e_sum
592
+ if OUTPUT_FP16:
593
+ result = result.to(tl.float16)
594
tl.store(
595
o + cur_batch * stride_obs + cur_head * stride_oh + offs_d,
592
- acc / e_sum,
596
+ result,
597
mask=mask_d,
598
)
599
lse_val = e_max + tl.log(e_sum)
vllm/v1/attention/ops/triton_turboquant_decode.py
+10
-3
@@ -588,10 +588,16 @@ def triton_turboquant_decode_attention(
588
)
589
590
# Stage 2: Reduce across KV splits
591
- if output_buf is not None and output_buf.shape[0] >= B:
591
+ # Output in query dtype — eliminates float16_copy kernel after stage2
592
+ out_dtype = query.dtype
593
+ if (
594
+ output_buf is not None
595
+ and output_buf.shape[0] >= B
596
+ and output_buf.dtype == out_dtype
597
+ ):
598
output = output_buf[:B, :Hq, :D]
599
else:
594
- output = torch.empty(B, Hq, D, dtype=torch.float32, device=device)
600
+ output = torch.empty(B, Hq, D, dtype=out_dtype, device=device)
601
if buf_holder is not None:
602
buf_holder._tq_output_buf = output
603
if lse_buf is not None and lse_buf.shape[0] >= B:
@@ -616,8 +622,9 @@ def triton_turboquant_decode_attention(
622
NUM_KV_SPLITS=NUM_KV_SPLITS,
623
BLOCK_DV=cfg["BLOCK_D"],
624
Lv=D,
625
+ OUTPUT_FP16=1 if out_dtype == torch.float16 else 0,
626
num_warps=4,
627
num_stages=2,
628
)
629
623
- return output.to(query.dtype)
630
+ return output # already in query dtype