main
py 1,243 lines 50.9 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """TurboQuant attention backend for vLLM.
4
5 Prefill: Standard scaled dot-product attention on uncompressed K/V,
6 then quantize K and store K+V into combined cache slot.
7 Decode: Compute TQ attention scores from compressed cache,
8 unpack FP16 values, softmax + weighted sum.
9
10 Cache layout (no leading 2 dimension):
11 (num_blocks, block_size, num_kv_heads, slot_size)
12 where slot_size = key_packed_size + value_fp16_size
13
14 Per-head per-position slot layout:
15 [key_packed (kps bytes) | value_fp16 (D*2 bytes)]
16 For turboquant_k3v4_nc head_dim=256: [100 bytes key | 512 bytes value] = 612
17 """
18
19 import contextlib
20 import functools
21 import math
22 from dataclasses import dataclass, replace
23 from typing import Any, ClassVar
24
25 import torch
26 import torch.nn.functional as F
27
28 from vllm.config import get_current_vllm_config
29 from vllm.config.cache import CacheDType
30 from vllm.logger import init_logger
31 from vllm.model_executor.layers.quantization.turboquant.centroids import (
32 get_centroids,
33 )
34 from vllm.triton_utils import triton
35 from vllm.utils.math_utils import round_up
36 from vllm.v1.attention.backend import (
37 AttentionBackend,
38 AttentionCGSupport,
39 AttentionImpl,
40 AttentionLayer,
41 AttentionMetadata,
42 AttentionMetadataBuilder,
43 AttentionType,
44 CommonAttentionMetadata,
45 MultipleOf,
46 )
47 from vllm.v1.attention.backends.fa_utils import (
48 get_flash_attn_version,
49 is_flash_attn_varlen_func_available,
50 )
51 from vllm.v1.attention.backends.utils import split_decodes_and_prefills
52
53 # FlyDSL TurboQuant decode (AMD gfx950). Auto-selected when FlyDSL is available
54 # for eligible layers (SoA store + FlyDSL decode + SoA-aware continuation);
55 # non-gfx950 or ineligible layers use the SoA Triton decode.
56 from vllm.v1.attention.ops.flydsl_turboquant_decode import (
57 flydsl_turboquant_decode_attention,
58 is_flydsl_available,
59 is_flydsl_gqa6_available,
60 )
61 from vllm.v1.attention.ops.triton_turboquant_decode import (
62 _tq_full_dequant_kv,
63 _use_fp8_e4b15,
64 triton_turboquant_decode_attention,
65 )
66 from vllm.v1.attention.ops.triton_turboquant_store import triton_turboquant_store
67 from vllm.v1.kv_cache_interface import AttentionSpec, KVCacheLayout
68 from vllm.v1.worker.workspace import (
69 current_workspace_manager,
70 is_workspace_manager_initialized,
71 )
72
73 logger = init_logger(__name__)
74
75 _HAS_FLASH_ATTN = is_flash_attn_varlen_func_available()
76 if _HAS_FLASH_ATTN:
77 from vllm.v1.attention.backends.fa_utils import flash_attn_varlen_func
78
79 # Continuation prefill: for small continuation chunks (q_len ≤ threshold),
80 # use the TQ decode kernel directly instead of full-dequant + flash_attn.
81 # do_kv_cache_update already stored all tokens to TQ cache, so the decode
82 # kernel can read them efficiently. This avoids O(cached_len) dequant work
83 # per continuation, eliminating the O(N²/chunk_size) collapse at long context.
84 _CONTINUATION_DECODE_THRESHOLD = 128
85
86
87 def _soa_imports():
88 """Lazy import of the HIP-free SoA Triton subset (store / dequant / decode).
89
90 Kept lazy so the default (FlyDSL-off) path never imports these modules.
91 """
92 from vllm.v1.attention.ops.turboquant_soa.triton_turboquant_decode import (
93 _tq_full_dequant_kv as soa_dequant,
94 )
95 from vllm.v1.attention.ops.turboquant_soa.triton_turboquant_store import (
96 triton_turboquant_store as soa_store,
97 )
98 from vllm.v1.attention.ops.turboquant_soa.triton_turboquant_unified_attention import ( # noqa: E501
99 triton_turboquant_decode_attention_soa as soa_decode,
100 )
101
102 return soa_store, soa_dequant, soa_decode
103
104
105 def _build_hadamard(d: int, device_str: str) -> torch.Tensor:
106 """Orthonormal Hadamard matrix (Sylvester construction), cached per (d, device).
107
108 Precomputed D×D matrix enables matmul-based WHT — single cuBLAS GEMM
109 instead of log2(D) butterfly kernel launches. 64KB for D=128.
110 """
111 # Normalize device string so "cuda" and "cuda:0" hit the same cache entry.
112 return _build_hadamard_cached(d, str(torch.device(device_str)))
113
114
115 @functools.cache
116 def _build_hadamard_cached(d: int, device_str: str) -> torch.Tensor:
117 H = torch.tensor([[1.0]])
118 while H.shape[0] < d:
119 H = torch.cat([torch.cat([H, H], 1), torch.cat([H, -H], 1)], 0)
120 return (H / math.sqrt(d)).to(torch.device(device_str))
121
122
123 class TurboQuantAttentionBackend(AttentionBackend):
124 """Attention backend using TurboQuant KV-cache compression."""
125
126 accept_output_buffer: bool = True
127 forward_includes_kv_cache_update: bool = False
128
129 supported_dtypes: ClassVar[list[torch.dtype]] = [
130 torch.float16,
131 torch.bfloat16,
132 ]
133 supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
134 "turboquant_k8v4",
135 "turboquant_4bit_nc",
136 "turboquant_k3v4_nc",
137 "turboquant_3bit_nc",
138 ]
139
140 @classmethod
141 def customize_spec(cls, spec: AttentionSpec) -> AttentionSpec:
142 """TurboQuant packs K+V into one slot per head."""
143 if spec.state_content_bytes is not None or not spec.kv_quant_mode.is_turboquant:
144 return spec
145 from vllm.model_executor.layers.quantization.turboquant.config import (
146 TurboQuantConfig,
147 )
148
149 # KVQuantMode member names mirror the preset strings.
150 tq = TurboQuantConfig.from_cache_dtype(
151 spec.kv_quant_mode.name.lower(), spec.head_size
152 )
153 return replace(spec, state_content_bytes=tq.slot_size_aligned)
154
155 @staticmethod
156 def get_name() -> str:
157 return "TURBOQUANT"
158
159 @classmethod
160 def supported_kv_cache_layouts(cls) -> tuple[KVCacheLayout, ...]:
161 return (KVCacheLayout.LBNHC,)
162
163 @staticmethod
164 def get_supported_kernel_block_sizes(kv_cache_spec=None) -> list[int | MultipleOf]:
165 return [16, 32, 64, 128]
166
167 @classmethod
168 def supports_attn_type(cls, attn_type: str) -> bool:
169 return attn_type == AttentionType.DECODER
170
171 @classmethod
172 def supports_per_head_quant_scales(cls) -> bool:
173 return False
174
175 @staticmethod
176 def get_impl_cls() -> type["TurboQuantAttentionImpl"]:
177 return TurboQuantAttentionImpl
178
179 @staticmethod
180 def get_builder_cls() -> type["TurboQuantMetadataBuilder"]:
181 return TurboQuantMetadataBuilder
182
183 @classmethod
184 def supports_kv_cache_dtype(cls, kv_cache_dtype: CacheDType | None) -> bool:
185 if kv_cache_dtype is None:
186 return False
187 return kv_cache_dtype.startswith("turboquant_")
188
189 @classmethod
190 def supports_head_size(cls, head_size: int) -> bool:
191 # head_size from spec is effective_head_size (padded_slot//2),
192 # not the model's actual head_dim. Accept any positive value.
193 return head_size > 0
194
195
196 @dataclass
197 class TurboQuantMetadata(AttentionMetadata):
198 """Metadata for TurboQuant attention."""
199
200 seq_lens: torch.Tensor # (num_reqs,) — total context length per request
201 slot_mapping: torch.Tensor # (num_tokens,) — cache slot for each token
202 block_table: torch.Tensor # (num_reqs, max_num_blocks)
203 query_start_loc: torch.Tensor # (num_reqs + 1,) — cu_seqlens for queries
204 num_actual_tokens: int = 0 # actual tokens (excluding padding)
205 max_query_len: int = 0 # longest query in batch
206 max_seq_len: int = 0 # longest context in batch
207 is_prefill: bool = False
208 num_decodes: int = 0 # number of decode requests (first in batch)
209 num_decode_tokens: int = 0 # tokens from decode requests
210 # CPU-resident copies used by the prefill path for per-request iteration
211 # without per-step D2H syncs.
212 query_start_loc_cpu: torch.Tensor | None = None
213 seq_lens_cpu: torch.Tensor | None = None
214
215
216 class TurboQuantMetadataBuilder(AttentionMetadataBuilder[TurboQuantMetadata]):
217 """Builds TurboQuantMetadata from scheduler output."""
218
219 kv_cache_spec: AttentionSpec
220 _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.UNIFORM_BATCH
221
222 def __init__(self, kv_cache_spec, layer_names, vllm_config, device):
223 super().__init__(kv_cache_spec, layer_names, vllm_config, device)
224 self._init_reorder_batch_threshold(1, supports_spec_as_decode=False)
225 self._reserve_workspace()
226
227 def _reserve_workspace(self) -> None:
228 if not is_workspace_manager_initialized():
229 return
230
231 scheduler_config = self.vllm_config.scheduler_config
232 model_config = self.vllm_config.model_config
233 parallel_config = self.vllm_config.parallel_config
234
235 max_num_reqs = scheduler_config.max_num_seqs
236 num_heads = model_config.get_num_attention_heads(parallel_config)
237 num_kv_heads = self.kv_cache_spec.num_kv_heads
238 head_size = self.kv_cache_spec.head_size
239 max_num_splits = (
240 self.vllm_config.attention_config.tq_max_kv_splits_for_cuda_graph
241 )
242
243 current_workspace_manager().get_simultaneous(
244 ((max_num_reqs, num_heads, max_num_splits, head_size + 1), torch.float32),
245 ((max_num_reqs, num_heads, head_size), model_config.dtype),
246 ((max_num_reqs, num_heads), torch.float32),
247 )
248
249 reserve_continuation_prefill = (
250 scheduler_config.enable_chunked_prefill
251 and scheduler_config.max_num_batched_tokens > _CONTINUATION_DECODE_THRESHOLD
252 )
253 if not reserve_continuation_prefill:
254 return
255
256 max_cached_len = max(0, model_config.max_model_len - 1)
257 alloc_len = round_up(max_cached_len, self.kv_cache_spec.block_size)
258 cache_buf_shape = (1, num_kv_heads, alloc_len, head_size)
259 current_workspace_manager().get_simultaneous(
260 (cache_buf_shape, torch.float16),
261 (cache_buf_shape, torch.float16),
262 )
263
264 def build_for_cudagraph_capture(
265 self, common_attn_metadata: CommonAttentionMetadata
266 ) -> TurboQuantMetadata:
267 attn_metadata = self.build(0, common_attn_metadata)
268 # Set seq_lens to 1 so CUDA graph capture is fast
269 # (real seq_lens are filled at replay time).
270 attn_metadata.seq_lens.fill_(1)
271 return attn_metadata
272
273 def build(self, common_prefix_len, common_attn_metadata, fast_build=False):
274 """Build TurboQuantMetadata from common attention metadata."""
275 cam = common_attn_metadata
276
277 # With reorder_batch_threshold=1, the model runner guarantees
278 # decodes come first in the batch. split_decodes_and_prefills
279 # finds the boundary (operates on CPU tensors — no GPU sync).
280 assert self.reorder_batch_threshold is not None
281 num_decodes, num_prefills, num_decode_tokens, _ = split_decodes_and_prefills(
282 cam, decode_threshold=self.reorder_batch_threshold
283 )
284
285 return TurboQuantMetadata(
286 seq_lens=cam.seq_lens,
287 slot_mapping=cam.slot_mapping,
288 block_table=cam.block_table_tensor,
289 query_start_loc=cam.query_start_loc,
290 num_actual_tokens=cam.num_actual_tokens,
291 max_query_len=cam.max_query_len,
292 max_seq_len=cam.max_seq_len,
293 is_prefill=(cam.max_query_len > 1),
294 num_decodes=num_decodes,
295 num_decode_tokens=num_decode_tokens,
296 query_start_loc_cpu=cam.query_start_loc_cpu,
297 seq_lens_cpu=cam.seq_lens_cpu_upper_bound,
298 )
299
300
301 class TurboQuantAttentionImpl(AttentionImpl["TurboQuantMetadata"]):
302 """TurboQuant attention implementation.
303
304 Vectorized PyTorch: batch quantize/store, vectorized bit-unpack
305 decode with einsum scores and value gather.
306 """
307
308 supports_quant_query_input: bool = False
309
310 # Lazily populated before cudagraph capture (FlyDSL decode path only).
311 _arange_cache: torch.Tensor
312 _cu_2: torch.Tensor
313
314 def __init__(
315 self,
316 num_heads: int,
317 head_size: int,
318 scale: float,
319 num_kv_heads: int | None = None,
320 alibi_slopes: list[float] | None = None,
321 sliding_window: int | None = None,
322 kv_cache_dtype: str = "auto",
323 logits_soft_cap: float | None = None,
324 attn_type: str = AttentionType.DECODER,
325 kv_sharing_target_layer_name: str | None = None,
326 **kwargs,
327 ):
328 self.num_heads = num_heads
329 self.head_size = head_size
330 self.scale = scale
331 self.num_kv_heads = num_kv_heads if num_kv_heads is not None else num_heads
332 self.num_kv_groups = num_heads // self.num_kv_heads
333 self.kv_cache_dtype = kv_cache_dtype
334
335 from vllm.model_executor.layers.quantization.turboquant.config import (
336 TurboQuantConfig,
337 )
338
339 self.tq_config = TurboQuantConfig.from_cache_dtype(kv_cache_dtype, head_size)
340
341 # Pre-compute kernel constants from config (avoid repeated arithmetic)
342 cfg = self.tq_config
343 self._mse_bytes = (
344 math.ceil(head_size * cfg.key_mse_bits / 8)
345 if not cfg.key_fp8
346 else head_size
347 )
348 self._val_data_bytes = math.ceil(head_size * cfg.effective_value_quant_bits / 8)
349 self._n_centroids = cfg.n_centroids if not cfg.key_fp8 else 1
350
351 # Detect flash-attn version (FA2/3/4) for prefill paths.
352 self.fa_version = get_flash_attn_version(head_size=head_size)
353
354 # Fixed NUM_KV_SPLITS (grid dims must be constant for cudagraph,
355 # and benchmarks show no regression vs dynamic in eager mode).
356 vllm_config = get_current_vllm_config()
357 self.max_num_kv_splits = (
358 vllm_config.attention_config.tq_max_kv_splits_for_cuda_graph
359 )
360
361 # FlyDSL decode state. Auto-enabled on gfx950 when FlyDSL is available.
362 self.sliding_window = sliding_window
363 self.sinks = kwargs.get("sinks")
364 # Cache max_model_len now (config is available at __init__ but NOT
365 # during CUDA-graph capture when _ensure_on_device is re-entered).
366 self._max_model_len = vllm_config.model_config.max_model_len
367 # SoA store is required by the FlyDSL decode/continuation path, so it
368 # tracks FlyDSL availability (single switch for the whole pipeline).
369 self._use_flydsl = is_flydsl_available()
370 self._soa_store = self._use_flydsl
371
372 def _flash_attn_varlen(
373 self,
374 q: torch.Tensor,
375 k: torch.Tensor,
376 v: torch.Tensor,
377 cu_seqlens_q: torch.Tensor,
378 cu_seqlens_k: torch.Tensor,
379 max_seqlen_q: int,
380 max_seqlen_k: int,
381 ) -> torch.Tensor:
382 # fa_utils.get_flash_attn_version() returns None on backends that
383 # should not pass an explicit fa_version kwarg.
384 if self.fa_version is None:
385 return flash_attn_varlen_func(
386 q=q,
387 k=k,
388 v=v,
389 cu_seqlens_q=cu_seqlens_q,
390 cu_seqlens_k=cu_seqlens_k,
391 max_seqlen_q=max_seqlen_q,
392 max_seqlen_k=max_seqlen_k,
393 softmax_scale=self.scale,
394 causal=True,
395 )
396 return flash_attn_varlen_func(
397 q=q,
398 k=k,
399 v=v,
400 cu_seqlens_q=cu_seqlens_q,
401 cu_seqlens_k=cu_seqlens_k,
402 max_seqlen_q=max_seqlen_q,
403 max_seqlen_k=max_seqlen_k,
404 softmax_scale=self.scale,
405 causal=True,
406 fa_version=self.fa_version,
407 )
408
409 def _ensure_on_device(self, layer, device):
410 """One-time derivation of TQ buffers (rotation matrix, midpoints).
411
412 The Hadamard rotation is shared across all layers: random sign
413 flips do not improve Lloyd-Max quantization quality because the
414 quantizer is symmetric around zero (sign-flipping a coordinate
415 maps it to the mirror centroid with identical distortion).
416 """
417 if self._soa_store:
418 # CUDA-graph capture safety for the FlyDSL decode path on ROCm.
419 # (1) Pre-allocate _arange_cache / _cu_2 BEFORE any capture; lazy
420 # allocation during graph replay lands in the HIP graph memory
421 # pool and yields stale addresses (GPU fault / garbage).
422 # (2) Pre-warm the WorkspaceManager to its max size before capture
423 # so mid-capture growth cannot invalidate pointers baked into
424 # already-captured batch sizes.
425 _max_len = self._max_model_len
426 _already_ok = (
427 hasattr(self, "_arange_cache")
428 and self._arange_cache.device.type == str(device).split(":")[0]
429 and self._arange_cache.shape[0] >= _max_len + 2
430 )
431 if not _already_ok:
432 self._arange_cache = torch.arange(
433 0, _max_len + 2, device=device, dtype=torch.int32
434 )
435 if not hasattr(self, "_cu_2") or self._cu_2.device != torch.device(device):
436 self._cu_2 = torch.zeros(2, device=device, dtype=torch.int32)
437 if (
438 is_workspace_manager_initialized()
439 and not current_workspace_manager().is_locked()
440 ):
441 B_max = self._max_capture_batch_size()
442 D = self.head_size
443 Hq = self.num_heads
444 S = self.max_num_kv_splits
445 _pre_warm_bytes = (
446 B_max * Hq * (S * (D + 1) + D) * 4 # fp32 mid_o + fp32 lse
447 + B_max * Hq * D * 2 # query-dtype output (bf16 = 2 B)
448 + 512 # alignment padding
449 )
450 with contextlib.suppress(AssertionError):
451 current_workspace_manager().get_simultaneous(
452 ((_pre_warm_bytes,), torch.uint8)
453 )
454
455 if not hasattr(layer, "_tq_cached"):
456 D = self.head_size
457
458 # Pure Hadamard: orthonormal + symmetric (H = H^T), enabling
459 # in-kernel butterfly fusion and trivial inverse for continuation.
460 H = _build_hadamard(D, str(device))
461 layer._tq_PiT = H
462 layer._tq_Pi = H
463 # fp16 copy for rotation in continuation prefill path
464 layer._tq_Pi_half = H.to(torch.float16)
465
466 # Centroids for Lloyd-Max quantization.
467 layer._tq_centroids = get_centroids(D, self.tq_config.centroid_bits).to(
468 device=device, dtype=torch.float32
469 )
470
471 c_sorted, _ = layer._tq_centroids.sort()
472 layer._tq_midpoints = (c_sorted[:-1] + c_sorted[1:]) / 2
473 layer._tq_cached = True
474
475 def _max_capture_batch_size(self) -> int:
476 """Largest decode batch we might see at runtime (for workspace pre-warm).
477
478 Take max(cudagraph_capture_sizes, scheduler.max_num_seqs): a forward
479 pass exceeding the largest captured graph size falls back to eager,
480 but the workspace is locked and that eager path can still hit batch
481 sizes up to max_num_seqs. Falls back to 1024 if config is unavailable.
482 """
483 try:
484 cfg = get_current_vllm_config()
485 candidates: list[int] = []
486 sizes = cfg.compilation_config.cudagraph_capture_sizes
487 if sizes:
488 candidates.append(int(max(sizes)))
489 sched = getattr(cfg, "scheduler_config", None)
490 if sched is not None and getattr(sched, "max_num_seqs", None):
491 candidates.append(int(sched.max_num_seqs))
492 if candidates:
493 return max(candidates)
494 except Exception: # noqa: BLE001
495 pass
496 return 1024
497
498 def do_kv_cache_update(
499 self,
500 layer: torch.nn.Module,
501 key: torch.Tensor,
502 value: torch.Tensor,
503 kv_cache: torch.Tensor,
504 slot_mapping: torch.Tensor,
505 ) -> None:
506 """Store compressed K/V into the combined TQ cache.
507
508 Called as a separate custom op (unified_kv_cache_update) BEFORE
509 the attention forward, matching FlashAttention's split pattern.
510 slot_mapping is already sliced to num_actual_tokens by the caller.
511 """
512 N = slot_mapping.shape[0]
513 if N <= 0:
514 return
515
516 device = key.device
517 self._ensure_on_device(layer, device)
518
519 k = key[:N].view(N, self.num_kv_heads, self.head_size)
520 v = value[:N].view(N, self.num_kv_heads, self.head_size)
521 # (B, H, N, C) -> (B, N, H, C) for TQ kernels
522 kv_cache = kv_cache.transpose(1, 2)
523 self._store_kv(k, v, kv_cache, slot_mapping, layer)
524
525 def forward(
526 self,
527 layer: AttentionLayer,
528 query: torch.Tensor,
529 key: torch.Tensor,
530 value: torch.Tensor,
531 kv_cache: torch.Tensor,
532 attn_metadata: "TurboQuantMetadata",
533 output: torch.Tensor | None = None,
534 output_scale: torch.Tensor | None = None,
535 output_block_scale: torch.Tensor | None = None,
536 ) -> torch.Tensor:
537 num_tokens = query.shape[0]
538
539 if output is None:
540 output = torch.zeros(
541 num_tokens,
542 self.num_heads * self.head_size,
543 dtype=query.dtype,
544 device=query.device,
545 )
546
547 if attn_metadata is None:
548 return output.fill_(0)
549
550 # (B, H, N, C) -> (B, N, H, C) for TQ kernels
551 kv_cache = kv_cache.transpose(1, 2)
552
553 # Slice to actual tokens
554 N = attn_metadata.num_actual_tokens
555 if N <= 0:
556 return output.fill_(0)
557
558 q = query[:N].view(N, self.num_heads, self.head_size)
559
560 # Get TQ buffers, ensure on device (one-time migration).
561 # Use Any-typed alias for dynamic _tq_* attrs set by _ensure_on_device.
562 tq_layer: Any = layer
563 device = q.device
564 self._ensure_on_device(tq_layer, device)
565 Pi = tq_layer._tq_Pi
566 PiT = tq_layer._tq_PiT
567 centroids = tq_layer._tq_centroids
568
569 # Compute attention (KV cache was already updated by do_kv_cache_update)
570 # With reorder_batch_threshold=1, decodes come first in the batch.
571 # num_decodes/num_decode_tokens from metadata give the split point.
572 num_decodes = attn_metadata.num_decodes
573 num_decode_tokens = attn_metadata.num_decode_tokens
574
575 if not attn_metadata.is_prefill:
576 # Pure decode batch — fast path
577 attn_out = self._decode_attention(
578 q, kv_cache, attn_metadata, Pi, centroids, PiT, layer
579 )
580 elif num_decodes == 0:
581 # Pure prefill batch
582 k = key[:N].view(N, self.num_kv_heads, self.head_size)
583 v = value[:N].view(N, self.num_kv_heads, self.head_size)
584 attn_out = self._prefill_attention(
585 q,
586 k,
587 v,
588 kv_cache,
589 attn_metadata,
590 Pi,
591 centroids,
592 PiT,
593 layer=layer,
594 )
595 else:
596 # Mixed batch: decodes first (guaranteed by reorder_batch).
597 attn_out = torch.empty(
598 N, self.num_heads, self.head_size, device=device, dtype=q.dtype
599 )
600
601 # --- Decode portion (first num_decodes requests) ---
602 # Use full-batch max_seq_len as safe upper bound (no GPU sync).
603 decode_meta = TurboQuantMetadata(
604 seq_lens=attn_metadata.seq_lens[:num_decodes],
605 slot_mapping=attn_metadata.slot_mapping[:num_decode_tokens],
606 block_table=attn_metadata.block_table[:num_decodes],
607 query_start_loc=attn_metadata.query_start_loc[: num_decodes + 1],
608 num_actual_tokens=num_decode_tokens,
609 max_query_len=1,
610 max_seq_len=attn_metadata.max_seq_len,
611 is_prefill=False,
612 )
613 attn_out[:num_decode_tokens] = self._decode_attention(
614 q[:num_decode_tokens], kv_cache, decode_meta, Pi, centroids, PiT, layer
615 )
616
617 # --- Prefill portion (remaining requests) ---
618 # CRITICAL: use prefill-specific max_seq_len so flash_attn's
619 # fast path (max_query_len == max_seq_len) triggers for
620 # first-chunk prefills. Using full-batch max_seq_len breaks
621 # this because decode requests inflate max_seq_len.
622 prefill_seq_lens = attn_metadata.seq_lens[num_decodes:]
623 # Use the CPU-resident `seq_lens` upper-bound from the metadata
624 # (populated in the builder) to compute the prefill sub-batch
625 # max without a GPU→CPU sync.
626 if attn_metadata.seq_lens_cpu is not None:
627 prefill_max_seq = int(attn_metadata.seq_lens_cpu[num_decodes:].max())
628 else:
629 prefill_max_seq = attn_metadata.max_seq_len
630 prefill_qsl = (
631 attn_metadata.query_start_loc[num_decodes:] - num_decode_tokens
632 )
633 prefill_qsl_cpu = None
634 if attn_metadata.query_start_loc_cpu is not None:
635 prefill_qsl_cpu = (
636 attn_metadata.query_start_loc_cpu[num_decodes:] - num_decode_tokens
637 )
638 prefill_meta = TurboQuantMetadata(
639 seq_lens=prefill_seq_lens,
640 slot_mapping=attn_metadata.slot_mapping[num_decode_tokens:N],
641 block_table=attn_metadata.block_table[num_decodes:],
642 query_start_loc=prefill_qsl,
643 num_actual_tokens=N - num_decode_tokens,
644 max_query_len=attn_metadata.max_query_len,
645 max_seq_len=prefill_max_seq,
646 is_prefill=True,
647 query_start_loc_cpu=prefill_qsl_cpu,
648 seq_lens_cpu=attn_metadata.seq_lens_cpu[num_decodes:]
649 if attn_metadata.seq_lens_cpu is not None
650 else None,
651 )
652 k = key[:N].view(N, self.num_kv_heads, self.head_size)
653 v = value[:N].view(N, self.num_kv_heads, self.head_size)
654 attn_out[num_decode_tokens:] = self._prefill_attention(
655 q[num_decode_tokens:],
656 k[num_decode_tokens:],
657 v[num_decode_tokens:],
658 kv_cache,
659 prefill_meta,
660 Pi,
661 centroids,
662 PiT,
663 layer=layer,
664 )
665
666 # Write into output buffer: attn_out is (N, Hq, D)
667 # output may be 2D (N, Hq*D) or 3D (N, Hq, D)
668 if output.ndim == 3:
669 output[:N] = attn_out.to(output.dtype)
670 else:
671 output[:N] = attn_out.reshape(N, -1).to(output.dtype)
672 return output
673
674 # ------------------------------------------------------------------ #
675 # Store K/V into combined cache (vectorized) #
676 # ------------------------------------------------------------------ #
677 def _store_kv(
678 self,
679 key: torch.Tensor, # (N, Hk, D)
680 value: torch.Tensor, # (N, Hk, D)
681 kv_cache: torch.Tensor, # (num_blocks, block_size, Hk, slot_size)
682 slot_mapping: torch.Tensor,
683 layer: Any,
684 ):
685 """Quantize + store via fused Triton kernel."""
686 if self._soa_store:
687 # SoA layout (data region + metadata region separated per block),
688 # required by the FlyDSL decode kernel. Pure-Triton store; the
689 # cache tensor shape is identical to the default AoS store, only
690 # the within-block byte convention differs.
691 soa_store, _, _ = _soa_imports()
692 soa_store(
693 key=key,
694 value=value,
695 kv_cache=kv_cache,
696 slot_mapping=slot_mapping,
697 PiT=layer._tq_PiT,
698 midpoints=layer._tq_midpoints,
699 mse_bits=self.tq_config.key_mse_bits,
700 key_packed_size=self.tq_config.key_packed_size,
701 value_quant_bits=self.tq_config.effective_value_quant_bits,
702 key_fp8=self.tq_config.key_fp8,
703 centroids=layer._tq_centroids,
704 norm_correction=self.tq_config.norm_correction,
705 )
706 return
707 triton_turboquant_store(
708 key,
709 value,
710 kv_cache,
711 slot_mapping,
712 layer._tq_PiT,
713 layer._tq_midpoints,
714 mse_bits=self.tq_config.key_mse_bits,
715 key_packed_size=self.tq_config.key_packed_size,
716 value_quant_bits=self.tq_config.effective_value_quant_bits,
717 key_fp8=self.tq_config.key_fp8,
718 )
719
720 # ------------------------------------------------------------------ #
721 # Prefill: SDPA on raw Q/K/V with causal mask #
722 # ------------------------------------------------------------------ #
723 def _prefill_attention(
724 self,
725 query: torch.Tensor, # (N, Hq, D)
726 key: torch.Tensor, # (N, Hk, D)
727 value: torch.Tensor, # (N, Hk, D)
728 kv_cache: torch.Tensor, # (num_blocks, block_size, Hk, slot_size)
729 attn_metadata: TurboQuantMetadata,
730 Pi: torch.Tensor,
731 centroids: torch.Tensor,
732 PiT: torch.Tensor | None = None,
733 layer: Any = None,
734 ) -> torch.Tensor:
735 N, Hq, D = query.shape
736
737 # Fast path: use flash_attn for first-chunk prefills (all K/V in batch).
738 # max_query_len == max_seq_len means no request has prior cached KV.
739 # Both are Python ints — no GPU sync.
740 if _HAS_FLASH_ATTN and attn_metadata.max_query_len == attn_metadata.max_seq_len:
741 return self._flash_attn_varlen(
742 q=query,
743 k=key,
744 v=value,
745 cu_seqlens_q=attn_metadata.query_start_loc,
746 cu_seqlens_k=attn_metadata.query_start_loc,
747 max_seqlen_q=attn_metadata.max_query_len,
748 max_seqlen_k=attn_metadata.max_query_len,
749 )
750
751 # Continuation or no flash_attn: per-request attention.
752 # For continuation chunks (seq_len > q_len), we must attend to
753 # previously cached K/V from the TQ cache, not just the current
754 # chunk's raw K/V.
755 Hk = key.shape[1]
756 use_gqa = Hk < Hq
757 query_start_loc = attn_metadata.query_start_loc
758 num_reqs = query_start_loc.shape[0] - 1
759
760 output = torch.zeros(N, Hq, D, device=query.device, dtype=query.dtype)
761
762 # Prefer the CPU-resident copies from the metadata if populated —
763 # otherwise `.tolist()` on GPU tensors forces a synchronizing copy.
764 if attn_metadata.query_start_loc_cpu is not None:
765 qsl = attn_metadata.query_start_loc_cpu.tolist()
766 else:
767 qsl = query_start_loc.tolist()
768 if attn_metadata.seq_lens_cpu is not None:
769 seq_lens_list = attn_metadata.seq_lens_cpu.tolist()
770 else:
771 seq_lens_list = attn_metadata.seq_lens.tolist()
772
773 # Pre-allocate cu_seqlens for single-request flash_attn calls
774 # to avoid per-request host→device tensor creation.
775 if not hasattr(self, "_cu_2"):
776 self._cu_2 = torch.zeros(2, device=query.device, dtype=torch.int32)
777 # Cache arange on self (avoid per-call kernel launch).
778 _max_seq = attn_metadata.max_seq_len
779 _ac: torch.Tensor | None = getattr(self, "_arange_cache", None)
780 if _ac is None or _ac.shape[0] <= _max_seq:
781 _ac = torch.arange(
782 0, _max_seq + 1, device=query.device, dtype=attn_metadata.seq_lens.dtype
783 )
784 self._arange_cache = _ac
785 _arange_cache: torch.Tensor = _ac
786
787 for i in range(num_reqs):
788 q_start = qsl[i]
789 q_end = qsl[i + 1]
790 q_len = q_end - q_start
791 if q_len <= 0:
792 continue
793
794 seq_len = seq_lens_list[i]
795 q_seq = query[q_start:q_end] # (q_len, Hq, D)
796 k_seq = key[q_start:q_end] # (q_len, Hk, D)
797 v_seq = value[q_start:q_end] # (q_len, Hk, D)
798
799 if q_len == seq_len:
800 # First-chunk prefill: all K/V are in the current batch.
801 if _HAS_FLASH_ATTN:
802 # Assign to slice to avoid gpu/cpu sync.
803 self._cu_2[1:2] = q_len
804 cu = self._cu_2
805 out = self._flash_attn_varlen(
806 q=q_seq,
807 k=k_seq,
808 v=v_seq,
809 cu_seqlens_q=cu,
810 cu_seqlens_k=cu,
811 max_seqlen_q=q_len,
812 max_seqlen_k=q_len,
813 )
814 else:
815 q_t = q_seq.transpose(0, 1).contiguous()
816 k_t = k_seq.transpose(0, 1).contiguous()
817 v_t = v_seq.transpose(0, 1).contiguous()
818 out = F.scaled_dot_product_attention(
819 q_t,
820 k_t,
821 v_t,
822 is_causal=True,
823 scale=self.scale,
824 enable_gqa=use_gqa,
825 ).transpose(0, 1)
826 output[q_start:q_end] = out.to(query.dtype)
827 else:
828 # Continuation chunk: tokens already stored to TQ cache
829 # by do_kv_cache_update. Use decode kernel directly to
830 # avoid O(cached_len) full-dequant per continuation.
831 # For large continuations, fall back to _continuation_prefill.
832 cached_len = seq_len - q_len
833 if q_len <= _CONTINUATION_DECODE_THRESHOLD:
834 # Fast path: treat each query as a decode request
835 # with incremental seq_lens for causal masking.
836 # Slice from pre-built arange (no kernel launch)
837 synth_seq_lens = _arange_cache[cached_len + 1 : seq_len + 1]
838 synth_bt = attn_metadata.block_table[i : i + 1].expand(q_len, -1)
839 if self._soa_store:
840 # The cache was written in SoA layout (always, for FlyDSL),
841 # so it MUST be read with the SoA-aware decode. The
842 # default AoS decode reads k_norm/v_scale/v_zero from
843 # the wrong offsets in a SoA cache -> garbage
844 # cached-prefix output ->
845 # accuracy collapse on every multi-turn / prefix-cached
846 # (APC) request. Continuation stays on the Triton SoA
847 # path even when FlyDSL is the main decode kernel
848 # (FlyDSL is decode-batch only).
849 out = self._dispatch_decode_soa(
850 query=q_seq,
851 kv_cache=kv_cache,
852 block_table=synth_bt,
853 seq_lens=synth_seq_lens,
854 Pi=Pi,
855 centroids=centroids,
856 scale=self.scale,
857 mse_bits=self.tq_config.key_mse_bits,
858 key_packed_size=self.tq_config.key_packed_size,
859 value_quant_bits=(
860 self.tq_config.effective_value_quant_bits
861 ),
862 value_packed_size=self.tq_config.value_packed_size,
863 max_seq_len=int(seq_len),
864 key_fp8=self.tq_config.key_fp8,
865 norm_correction=self.tq_config.norm_correction,
866 PiT=PiT,
867 sinks=self.sinks,
868 sliding_window=self.sliding_window,
869 )
870 else:
871 out = triton_turboquant_decode_attention(
872 query=q_seq,
873 kv_cache=kv_cache,
874 block_table=synth_bt,
875 seq_lens=synth_seq_lens,
876 Pi=Pi,
877 centroids=centroids,
878 scale=self.scale,
879 mse_bits=self.tq_config.key_mse_bits,
880 key_packed_size=self.tq_config.key_packed_size,
881 value_quant_bits=(
882 self.tq_config.effective_value_quant_bits
883 ),
884 key_fp8=self.tq_config.key_fp8,
885 norm_correction=self.tq_config.norm_correction,
886 PiT=PiT,
887 )
888 else:
889 # Large continuation: dequant cached K/V and use
890 # flash_attn for better throughput.
891 out = self._continuation_prefill(
892 layer,
893 q_seq,
894 k_seq,
895 v_seq,
896 kv_cache,
897 attn_metadata.block_table[i : i + 1],
898 cached_len,
899 seq_len,
900 Pi,
901 centroids,
902 )
903 output[q_start:q_end] = out.to(query.dtype)
904
905 return output
906
907 def _continuation_prefill(
908 self,
909 layer: Any,
910 query: torch.Tensor, # (q_len, Hq, D)
911 key_chunk: torch.Tensor, # (q_len, Hk, D)
912 val_chunk: torch.Tensor, # (q_len, Hk, D)
913 kv_cache: torch.Tensor, # (num_blocks, block_size, Hk, slot_size)
914 block_table: torch.Tensor, # (1, max_num_blocks)
915 cached_len: int,
916 seq_len: int,
917 Pi: torch.Tensor,
918 centroids: torch.Tensor,
919 ) -> torch.Tensor:
920 """Handle continuation chunk by dequanting cached K/V from TQ cache.
921
922 Dequants previously cached K/V, concatenates with the current
923 chunk's raw K/V, then runs flash_attn with causal masking.
924 """
925 q_len, Hq, D = query.shape
926 Hk = key_chunk.shape[1]
927 device = query.device
928 block_size = kv_cache.shape[1]
929 BLOCK_D = triton.next_power_of_2(D)
930
931 mse_bytes = self._mse_bytes
932 val_data_bytes = self._val_data_bytes
933
934 # Dequant cached K/V from TQ cache
935 # Allocate slightly over to align to block_size for the grid.
936 # Reuse cached buffers to avoid per-call allocation (~16MB at 8K).
937 alloc_len = math.ceil(cached_len / block_size) * block_size
938 buf_shape = (1, Hk, alloc_len, D)
939 # Use WorkspaceManager for dequant buffers.
940 # Shared across all layers — saves 60× memory at long context.
941 # Required for CUDA Graph capture (per-layer growth incompatible with CG).
942 k_buf, v_buf = current_workspace_manager().get_simultaneous(
943 (buf_shape, torch.float16),
944 (buf_shape, torch.float16),
945 )
946 # Skip .zero_() — kernel writes all positions up to cached_len,
947 # and we only read [:cached_len] afterwards.
948 k_cached = k_buf[:, :, :alloc_len, :]
949 v_cached = v_buf[:, :, :alloc_len, :]
950
951 grid = (alloc_len, 1 * Hk)
952 if self._soa_store:
953 # SoA-aware dequant: read the data/metadata-separated SoA cache
954 # written by the SoA store. Constants must match the store side.
955 _, soa_dequant, _ = _soa_imports()
956 key_fp8 = self.tq_config.key_fp8
957 key_data_bytes = D if key_fp8 else mse_bytes
958 data_bytes_per_slot = key_data_bytes + val_data_bytes
959 meta_region_offset = block_size * Hk * data_bytes_per_slot
960 num_soa_fields = 2 if key_fp8 else 3
961 soa_k_norm = 0
962 soa_v_scale = 0 if key_fp8 else 1
963 soa_v_zero = 1 if key_fp8 else 2
964 kv_cache_u16 = kv_cache.view(torch.uint16)
965 soa_dequant[grid](
966 kv_cache,
967 kv_cache_u16,
968 block_table,
969 centroids,
970 k_cached,
971 v_cached,
972 k_cached.stride(0),
973 k_cached.stride(1),
974 k_cached.stride(2),
975 v_cached.stride(0),
976 v_cached.stride(1),
977 v_cached.stride(2),
978 kv_cache.stride(0),
979 block_table.stride(0),
980 HEAD_DIM=D,
981 BLOCK_SIZE=block_size,
982 NUM_KV_HEADS=Hk,
983 MSE_BYTES=mse_bytes,
984 VQB=self.tq_config.effective_value_quant_bits,
985 VAL_DATA_BYTES=val_data_bytes,
986 MSE_BITS=self.tq_config.key_mse_bits,
987 KEY_FP8=1 if key_fp8 else 0,
988 KEY_DATA_BYTES=key_data_bytes,
989 META_REGION_OFFSET=meta_region_offset,
990 NUM_SOA_FIELDS=num_soa_fields,
991 SOA_K_NORM=soa_k_norm,
992 SOA_V_SCALE=soa_v_scale,
993 SOA_V_ZERO=soa_v_zero,
994 BLOCK_D=BLOCK_D,
995 NORM_CORRECTION=1 if self.tq_config.norm_correction else 0,
996 FP8_E4B15=_use_fp8_e4b15(device.index or 0),
997 num_warps=4,
998 )
999 else:
1000 _tq_full_dequant_kv[grid](
1001 kv_cache,
1002 block_table,
1003 centroids,
1004 k_cached,
1005 v_cached,
1006 k_cached.stride(0),
1007 k_cached.stride(1),
1008 k_cached.stride(2),
1009 v_cached.stride(0),
1010 v_cached.stride(1),
1011 v_cached.stride(2),
1012 kv_cache.stride(0),
1013 kv_cache.stride(1),
1014 kv_cache.stride(2),
1015 block_table.stride(0),
1016 HEAD_DIM=D,
1017 BLOCK_SIZE=block_size,
1018 NUM_KV_HEADS=Hk,
1019 MSE_BYTES=mse_bytes,
1020 KPS=self.tq_config.key_packed_size,
1021 VQB=self.tq_config.effective_value_quant_bits,
1022 VAL_DATA_BYTES=val_data_bytes,
1023 MSE_BITS=self.tq_config.key_mse_bits,
1024 KEY_FP8=1 if self.tq_config.key_fp8 else 0,
1025 BLOCK_D=BLOCK_D,
1026 NORM_CORRECTION=1 if self.tq_config.norm_correction else 0,
1027 FP8_E4B15=_use_fp8_e4b15(device.index or 0),
1028 num_warps=4,
1029 )
1030
1031 # Inverse-rotate MSE keys back to original space
1032 if not self.tq_config.key_fp8:
1033 # fp16 matmul for rotation (2× less bandwidth, uses fp16 tensor cores)
1034 Pi_half = layer._tq_Pi_half
1035 k_flat = k_cached[0, :, :cached_len, :].reshape(-1, D)
1036 k_flat = k_flat @ Pi_half
1037 k_cached_trim = k_flat.reshape(Hk, cached_len, D).transpose(
1038 0, 1
1039 ) # (cached_len, Hk, D) — already fp16
1040 else:
1041 k_cached_trim = k_cached[0, :, :cached_len, :].transpose(
1042 0, 1
1043 ) # (cached_len, Hk, D)
1044
1045 # Skip .contiguous() — the copy into k_full/v_full handles layout
1046 v_cached_trim = v_cached[0, :, :cached_len, :].transpose(0, 1)
1047
1048 # Concatenate cached + current chunk K/V (match query dtype)
1049 # Pre-allocate full K/V buffer, copy into slices (no cat alloc)
1050 qdtype = query.dtype
1051 k_full = torch.empty(seq_len, Hk, D, dtype=qdtype, device=device)
1052 v_full = torch.empty(seq_len, Hk, D, dtype=qdtype, device=device)
1053 k_full[:cached_len] = k_cached_trim.to(qdtype)
1054 k_full[cached_len:] = key_chunk
1055 v_full[:cached_len] = v_cached_trim.to(qdtype)
1056 v_full[cached_len:] = val_chunk
1057
1058 # Attention: q_len queries attending to seq_len K/V with causal mask
1059 if _HAS_FLASH_ATTN:
1060 # Reuse pre-allocated cu_seqlens (avoid host→device transfer)
1061 if not hasattr(self, "_cu_2_q"):
1062 self._cu_2_q = torch.zeros(2, device=device, dtype=torch.int32)
1063 self._cu_2_k = torch.zeros(2, device=device, dtype=torch.int32)
1064 # Assigning to slice uses fill_ which avoids cpu/gpu sync.
1065 self._cu_2_q[1:2] = q_len
1066 self._cu_2_k[1:2] = seq_len
1067 cu_seqlens_q = self._cu_2_q
1068 cu_seqlens_k = self._cu_2_k
1069 return self._flash_attn_varlen(
1070 q=query,
1071 k=k_full,
1072 v=v_full,
1073 cu_seqlens_q=cu_seqlens_q,
1074 cu_seqlens_k=cu_seqlens_k,
1075 max_seqlen_q=q_len,
1076 max_seqlen_k=seq_len,
1077 )
1078 else:
1079 # SDPA fallback: expand KV for GQA, build causal mask
1080 q_t = query.transpose(0, 1).unsqueeze(0) # (1, Hq, q_len, D)
1081 k_t = k_full.transpose(0, 1).unsqueeze(0) # (1, Hk, seq_len, D)
1082 v_t = v_full.transpose(0, 1).unsqueeze(0) # (1, Hk, seq_len, D)
1083 # Build causal mask: query position p can attend to K position j
1084 # where j <= cached_len + p (p is 0-indexed within chunk)
1085 q_pos = torch.arange(q_len, device=device).unsqueeze(1) + cached_len
1086 k_pos = torch.arange(seq_len, device=device).unsqueeze(0)
1087 mask = k_pos <= q_pos # (q_len, seq_len)
1088 out = F.scaled_dot_product_attention(
1089 q_t,
1090 k_t,
1091 v_t,
1092 attn_mask=mask,
1093 scale=self.scale,
1094 enable_gqa=(Hk < Hq),
1095 ) # (1, Hq, q_len, D)
1096 return out[0].transpose(0, 1) # (q_len, Hq, D)
1097
1098 # ------------------------------------------------------------------ #
1099 # Decode: Triton TQ decode attention #
1100 # ------------------------------------------------------------------ #
1101 def _decode_attention(
1102 self,
1103 query: torch.Tensor, # (B, Hq, D)
1104 kv_cache: torch.Tensor, # (num_blocks, block_size, Hk, slot_size)
1105 attn_metadata: TurboQuantMetadata,
1106 Pi: torch.Tensor,
1107 centroids: torch.Tensor,
1108 PiT: torch.Tensor | None = None,
1109 layer: torch.nn.Module | None = None,
1110 ) -> torch.Tensor:
1111 # Acquire shared decode scratch buffers from WorkspaceManager.
1112 # Layers execute sequentially so one set of buffers is sufficient.
1113 # Falls back to kernel-internal allocation if workspace unavailable.
1114 B = query.shape[0]
1115 D = self.head_size
1116 S = self.max_num_kv_splits
1117 Hq = self.num_heads
1118 mid_o_buf = output_buf = lse_buf = None
1119 if is_workspace_manager_initialized():
1120 # output_buf in query dtype — matches the in-kernel fp16 cast in stage2.
1121 mid_o_buf, output_buf, lse_buf = (
1122 current_workspace_manager().get_simultaneous(
1123 ((B, Hq, S, D + 1), torch.float32),
1124 ((B, Hq, D), query.dtype),
1125 ((B, Hq), torch.float32),
1126 )
1127 )
1128
1129 if self._use_flydsl:
1130 # FlyDSL decode (gfx950, MSE-key, HEAD_SIZE=128, GQA in {6, 8, 16}).
1131 # GQA-6 routes to the MiniMax sibling kernel. Ineligible layers fall
1132 # back to SoA Triton decode.
1133 _gqa = self.num_kv_groups
1134 flydsl_gqa_ok = (_gqa in (8, 16)) or (
1135 _gqa == 6 and is_flydsl_gqa6_available()
1136 )
1137 flydsl_eligible = (
1138 not self.tq_config.key_fp8
1139 and self.tq_config.key_mse_bits == 4
1140 and self.tq_config.effective_value_quant_bits == 4
1141 and self.head_size == 128
1142 and flydsl_gqa_ok
1143 and self.sinks is None
1144 and not (self.sliding_window and self.sliding_window > 0)
1145 )
1146 if flydsl_eligible:
1147 return flydsl_turboquant_decode_attention(
1148 query=query,
1149 kv_cache=kv_cache,
1150 block_table=attn_metadata.block_table,
1151 seq_lens=attn_metadata.seq_lens,
1152 Pi=Pi,
1153 centroids=centroids,
1154 scale=self.scale,
1155 mse_bits=self.tq_config.key_mse_bits,
1156 key_packed_size=self.tq_config.key_packed_size,
1157 value_quant_bits=self.tq_config.effective_value_quant_bits,
1158 value_packed_size=self.tq_config.value_packed_size,
1159 max_seq_len=attn_metadata.max_seq_len,
1160 key_fp8=self.tq_config.key_fp8,
1161 norm_correction=self.tq_config.norm_correction,
1162 PiT=PiT,
1163 mid_o_buf=mid_o_buf,
1164 output_buf=output_buf,
1165 lse_buf=lse_buf,
1166 buf_holder=layer,
1167 max_num_kv_splits=self.max_num_kv_splits,
1168 sinks=self.sinks,
1169 )
1170 logger.warning_once(
1171 "TurboQuant FlyDSL ineligible (key_fp8=%s mse_bits=%s vqb=%s "
1172 "head_size=%s num_kv_groups=%s sinks=%s) -> SoA Triton decode",
1173 self.tq_config.key_fp8,
1174 self.tq_config.key_mse_bits,
1175 self.tq_config.effective_value_quant_bits,
1176 self.head_size,
1177 self.num_kv_groups,
1178 self.sinks is not None,
1179 )
1180 return self._dispatch_decode_soa(
1181 query=query,
1182 kv_cache=kv_cache,
1183 block_table=attn_metadata.block_table,
1184 seq_lens=attn_metadata.seq_lens,
1185 Pi=Pi,
1186 centroids=centroids,
1187 scale=self.scale,
1188 mse_bits=self.tq_config.key_mse_bits,
1189 key_packed_size=self.tq_config.key_packed_size,
1190 value_quant_bits=self.tq_config.effective_value_quant_bits,
1191 value_packed_size=self.tq_config.value_packed_size,
1192 max_seq_len=attn_metadata.max_seq_len,
1193 key_fp8=self.tq_config.key_fp8,
1194 norm_correction=self.tq_config.norm_correction,
1195 PiT=PiT,
1196 mid_o_buf=mid_o_buf,
1197 output_buf=output_buf,
1198 lse_buf=lse_buf,
1199 buf_holder=layer,
1200 max_num_kv_splits=self.max_num_kv_splits,
1201 sinks=self.sinks,
1202 sliding_window=self.sliding_window,
1203 )
1204
1205 result = triton_turboquant_decode_attention(
1206 query=query,
1207 kv_cache=kv_cache,
1208 block_table=attn_metadata.block_table,
1209 seq_lens=attn_metadata.seq_lens,
1210 Pi=Pi,
1211 centroids=centroids,
1212 scale=self.scale,
1213 mse_bits=self.tq_config.key_mse_bits,
1214 key_packed_size=self.tq_config.key_packed_size,
1215 value_quant_bits=self.tq_config.effective_value_quant_bits,
1216 key_fp8=self.tq_config.key_fp8,
1217 norm_correction=self.tq_config.norm_correction,
1218 PiT=PiT,
1219 mid_o_buf=mid_o_buf,
1220 output_buf=output_buf,
1221 lse_buf=lse_buf,
1222 buf_holder=layer,
1223 max_num_kv_splits=self.max_num_kv_splits,
1224 )
1225 return result
1226
1227 def _dispatch_decode_soa(self, **kwargs):
1228 """SoA-aware Triton decode — fallback for FlyDSL-ineligible layers.
1229
1230 The SoA decode launcher accepts a subset of the FlyDSL kwargs; filter to
1231 the params it accepts and raise if a *meaningful* (non-None) kwarg would
1232 be silently dropped (so we never mask a real feature gap).
1233 """
1234 import inspect
1235
1236 _, _, soa_decode = _soa_imports()
1237 accepted = set(inspect.signature(soa_decode).parameters)
1238 dropped = [k for k, v in kwargs.items() if k not in accepted and v is not None]
1239 if dropped:
1240 raise NotImplementedError(
1241 f"SoA decode does not support kwargs {sorted(dropped)}"
1242 )
1243 return soa_decode(**{k: v for k, v in kwargs.items() if k in accepted})