[Refactor][GLM-5.3-Flash] Move sparse_attn_indexer_kpool into the model folder and split AMD/NVIDIA (#55358)

Signed-off-by: zjy0516 <riverclouds.zhu@qq.com> Co-authored-by: TJian <tunjian.tan@embeddedllm.com>

Jiangyun Zhu committed Sep 16, 2026 at 13:50 UTC 4fe9e6f6e5647a159c9e65e4f3bf33dec5d12e1c
6 files changed +1008 -398
tests/v1/attention/test_sparse_indexer_decode_seq_lens.py
+10 -6
@@ -20,10 +20,10 @@ kernel is documented to replicate.
20
21 import torch
22
23 -# Bootstrap the glm5next package before entering the indexer module: its
24 -# kpool_compress import runs glm5next/__init__, which pulls model ->
25 -# attention -> back into sparse_attn_indexer_kpool (attention.py imports the
26 -# class at module scope). Production always enters via attention.py first.
23 +# Bootstrap the glm5next package before importing kpool_compress directly: its
24 +# import runs glm5next/__init__, which pulls model -> attention -> the indexer
25 +# dispatcher (sparse_indexer) -> back into the nvidia/amd sparse_indexer
26 +# modules. Production always enters via attention.py first.
27 # isort: off
28 import vllm.models.glm5next # noqa: F401
29 from vllm.models.glm5next.nvidia.ops.kpool_compress import ( # noqa: E402
@@ -32,13 +32,17 @@ from vllm.models.glm5next.nvidia.ops.kpool_compress import ( # noqa: E402
32 )
33 # isort: on
34
35 -import vllm.model_executor.layers.sparse_attn_indexer_kpool as indexer_mod
36 -from vllm.model_executor.layers.sparse_attn_indexer_kpool import (
35 +from vllm.models.glm5next.common.sparse_indexer import (
36 _decode_topk_seq_lens,
37 _fill_short_decode_causal_indices,
38 )
39 from vllm.platforms import current_platform
40
41 +if current_platform.is_rocm():
42 + from vllm.models.glm5next.amd import sparse_indexer as indexer_mod
43 +else:
44 + from vllm.models.glm5next.nvidia import sparse_indexer as indexer_mod
45 +
46 KPOOL = 4
47 TOPK_TOKENS = 16
48 SELECT_K = TOPK_TOKENS // KPOOL
vllm/models/glm5next/amd/sparse_indexer.py new
+729
@@ -0,0 +1,729 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Custom Sparse Attention Indexer layers."""
4 +
5 +import torch
6 +
7 +import vllm.envs as envs
8 +from vllm import _custom_ops # noqa: F401 # registers the torch.ops._C kernels
9 +from vllm._aiter_ops import rocm_aiter_ops
10 +from vllm.compilation.breakable_cudagraph import eager_break_during_capture
11 +from vllm.config import get_current_vllm_config_or_none
12 +from vllm.forward_context import get_forward_context
13 +from vllm.logger import init_logger
14 +from vllm.model_executor.custom_op import CustomOp
15 +from vllm.models.glm5next.amd.ops import kpool_compress as kpool_ops
16 +from vllm.models.glm5next.common.sparse_indexer import (
17 + RADIX_TOPK_WORKSPACE_SIZE,
18 + _build_decode_scatter_indices,
19 + _decode_topk_seq_lens,
20 + _fill_causal_indices,
21 + _fill_short_decode_causal_indices,
22 + _gather_workspace_shapes,
23 + _scatter_decode_tokens_by_request,
24 + kv_cache_as_quant_view,
25 +)
26 +from vllm.platforms import current_platform
27 +from vllm.utils.torch_utils import (
28 + LayerNameType,
29 + _encode_layer_name,
30 + _resolve_layer_name,
31 +)
32 +from vllm.v1.attention.backends.mla.indexer import (
33 + DeepseekV32IndexerMetadata,
34 +)
35 +from vllm.v1.attention.ops.common import pack_seq_triton, unpack_seq_triton
36 +from vllm.v1.worker.workspace import current_workspace_manager
37 +
38 +logger = init_logger(__name__)
39 +
40 +# kpool write helper: form pools from the current token batch and compress them
41 +# into the index K cache via the fused Triton kernel.
42 +
43 +
44 +def _kpool_compress_insert(
45 + k: torch.Tensor,
46 + gate_score: torch.Tensor,
47 + ape: torch.Tensor,
48 + kv_cache: torch.Tensor,
49 + slot_mapping: torch.Tensor,
50 + kpool: int,
51 + head_dim: int,
52 + round_scale: bool,
53 +) -> None:
54 + """Pool ``kpool`` consecutive tokens into one fp8 K and write at pool slots.
55 +
56 + ``slot_mapping`` is pool-granular (compress_ratio == kpool on the spec):
57 + only the *last* token of each complete pool carries a valid (>=0) slot;
58 + intra-pool tokens are -1. Every position is treated as a pool-completion
59 + candidate and non-completions are masked off inside the kernel. Compacting
60 + the valid rows first costs two device syncs on the eager prefill path and
61 + buys nothing numerically. Assumes pool-aligned chunk starts.
62 + """
63 + n = slot_mapping.shape[0]
64 + # No pool can complete in a batch smaller than one pool; also keeps the
65 + # clamped gather indices below in bounds.
66 + if n < kpool:
67 + return
68 + pos = torch.arange(n, device=k.device)
69 + valid = slot_mapping >= 0
70 + # Drop pools whose start falls before the batch (leading padding); their
71 + # gate/k data is undefined anyway.
72 + write_mask = valid & (pos >= kpool - 1)
73 + offs = torch.arange(kpool, device=k.device)
74 + idx = (pos - (kpool - 1)).clamp_min(0)[:, None] + offs[None, :]
75 + kpool_ops.kpool_compress_and_write_cache(
76 + kv_cache,
77 + k[idx], # [n, kpool, head_dim]
78 + gate_score[idx],
79 + ape,
80 + slot_mapping.to(torch.int64),
81 + pool_size=kpool,
82 + head_dim=head_dim,
83 + write_mask=write_mask,
84 + round_scale=round_scale,
85 + write_cache=True,
86 + return_compressed=False,
87 + )
88 +
89 +
90 +@eager_break_during_capture
91 +def sparse_attn_indexer_kpool(
92 + hidden_states: torch.Tensor,
93 + k_cache_prefix: LayerNameType,
94 + kv_cache: torch.Tensor,
95 + q_quant: torch.Tensor,
96 + q_scale: torch.Tensor | None,
97 + k: torch.Tensor,
98 + weights: torch.Tensor,
99 + quant_block_size: int,
100 + scale_fmt: str | None,
101 + topk_tokens: int,
102 + head_dim: int,
103 + max_model_len: int,
104 + total_seq_lens: int,
105 + topk_indices_buffer: torch.Tensor,
106 + skip_k_cache_insert: bool,
107 + use_fp4_cache: bool = False,
108 + # kpool params (Plan-A: gate is consumed at write time and read back at
109 + # topk time to softmax-weight the pool).
110 + gate_score: torch.Tensor | None = None,
111 + compress_ape: torch.Tensor | None = None,
112 + index_kpool: int = 1,
113 + positions: torch.Tensor | None = None,
114 + # Paged tail cache (in-progress pool's raw K + gate score), replacing the
115 + # transient _DECODE_TAIL ring. tail_prefix resolves attn_metadata[tail_prefix]
116 + # for the tail group's token-granular slot_mapping. None on the dummy/profiling
117 + # path and when the tail cache is disabled.
118 + tail_kv_cache: torch.Tensor | None = None,
119 + tail_prefix: str | None = None,
120 +) -> torch.Tensor:
121 + # careful! this will be None in dummy run
122 + attn_metadata = get_forward_context().attn_metadata
123 + fp8_dtype = current_platform.fp8_dtype()
124 + k_cache_prefix = _resolve_layer_name(k_cache_prefix)
125 +
126 + # assert isinstance(attn_metadata, dict)
127 + if not isinstance(attn_metadata, dict):
128 + # Reserve workspace for indexer during profiling run
129 + values_spec, scales_spec = _gather_workspace_shapes(
130 + total_seq_lens, head_dim, fp8_dtype, use_fp4_cache
131 + )
132 + current_workspace_manager().get_simultaneous(
133 + values_spec,
134 + scales_spec,
135 + ((RADIX_TOPK_WORKSPACE_SIZE,), torch.uint8),
136 + )
137 +
138 + # Reserve profiler-visible memory for the worst-case decode logits,
139 + # whose shape is [B * next_n, max_model_len]. This profiling branch
140 + # returns before invoking the logits kernel itself.
141 + cfg = get_current_vllm_config_or_none()
142 + worst_decode_tokens = 0
143 + if cfg is not None:
144 + sched = cfg.scheduler_config
145 + num_spec = (
146 + cfg.speculative_config.num_speculative_tokens
147 + if cfg.speculative_config is not None
148 + else 0
149 + )
150 + worst_decode_tokens = min(
151 + sched.max_num_seqs * (num_spec + 1),
152 + sched.max_num_batched_tokens,
153 + )
154 + # float32 logits -> 4 bytes/element; uint8 sentinel so elems == bytes.
155 + decode_logits_elems = worst_decode_tokens * max_model_len * 4
156 + prefill_cap_elems = envs.VLLM_SPARSE_INDEXER_MAX_LOGITS_MB * 1024 * 1024
157 + max_logits_elems = max(decode_logits_elems, prefill_cap_elems)
158 + _ = torch.empty(
159 + max_logits_elems, dtype=torch.uint8, device=hidden_states.device
160 + )
161 +
162 + return topk_indices_buffer
163 + attn_metadata_narrowed = attn_metadata[k_cache_prefix]
164 + assert isinstance(attn_metadata_narrowed, DeepseekV32IndexerMetadata)
165 + slot_mapping = attn_metadata_narrowed.slot_mapping
166 + has_decode = attn_metadata_narrowed.num_decodes > 0
167 + has_prefill = attn_metadata_narrowed.num_prefills > 0
168 + num_decode_tokens = attn_metadata_narrowed.num_decode_tokens
169 +
170 + # q_scale is required iff the FP4 cache path is enabled; the FP8 path
171 + # folds the Q scale into `weights` inside fused_indexer_q_rope_quant.
172 + if use_fp4_cache:
173 + assert q_scale is not None, "use_fp4_cache=True requires q_scale"
174 + else:
175 + assert q_scale is None, "q_scale must be None when use_fp4_cache=False"
176 +
177 + # During speculative decoding, k may be padded to the CUDA graph batch
178 + # size while slot_mapping only covers actual tokens. Truncate k to avoid
179 + # out-of-bounds reads in the kernel.
180 + num_tokens = slot_mapping.shape[0]
181 + if k is not None:
182 + k = k[:num_tokens]
183 +
184 + if not skip_k_cache_insert:
185 + assert not use_fp4_cache, "Unfused FP4 Insert is not supported yet"
186 + if index_kpool > 1 and gate_score is not None and compress_ape is not None:
187 + # kpool prefill write: pool kpool consecutive prefill tokens via
188 + # softmax(gate+ape)-weighted sum -> Hadamard -> fp8 -> pool slots.
189 + # Decode tokens (the first num_decode_tokens in the batch) cannot be
190 + # pooled here — their pool's earlier tokens are not in this batch —
191 + # so they are deferred to the tail-buffer kernel in has_decode.
192 + # compress_ratio == index_kpool makes slot_mapping pool-granular.
193 + n_prefill = num_tokens - num_decode_tokens
194 + if n_prefill > 0:
195 + # decode tokens are batched first; prefill tokens follow.
196 + prefill_slice = slice(num_decode_tokens, num_tokens)
197 + _kpool_compress_insert(
198 + k[prefill_slice],
199 + gate_score[prefill_slice],
200 + compress_ape,
201 + kv_cache,
202 + slot_mapping[prefill_slice],
203 + index_kpool,
204 + head_dim,
205 + round_scale=(scale_fmt is not None),
206 + )
207 + # Persist each request's incomplete prefill pool so decode can
208 + # finish it, including after PD transfer. Tail slots use
209 + # ``pos % kpool`` within the request's tail block. Processing
210 + # only the batch's trailing tokens would miss all but the last
211 + # request in a multi-request prefill.
212 + if tail_kv_cache is not None and tail_prefix is not None:
213 + tail_meta = attn_metadata.get(_resolve_layer_name(tail_prefix))
214 + if tail_meta is not None:
215 + assert isinstance(tail_meta, DeepseekV32IndexerMetadata)
216 + kpool_ops.kpool_seed_tail_cache(
217 + tail_kv_cache,
218 + k[prefill_slice],
219 + gate_score[prefill_slice],
220 + tail_meta.slot_mapping[prefill_slice],
221 + index_kpool,
222 + head_dim,
223 + )
224 + else:
225 + # standard: per-token fp8 quant + scatter (all tokens).
226 + assert scale_fmt is not None
227 + from vllm.v1.attention.ops.rocm_aiter_mla_sparse import (
228 + indexer_k_quant_and_cache_triton,
229 + )
230 +
231 + indexer_k_quant_and_cache_triton(
232 + k,
233 + kv_cache,
234 + slot_mapping,
235 + quant_block_size,
236 + scale_fmt,
237 + )
238 +
239 + topk_indices_buffer[: hidden_states.shape[0]] = -1
240 + if has_prefill:
241 + prefill_metadata = attn_metadata_narrowed.prefill
242 + assert prefill_metadata is not None
243 +
244 + # Short sequences select every pool, so skip sparse scoring and fill
245 + # the top-k buffer with all causal token indices. The index-K cache was
246 + # already written above.
247 + n_prefill_sf = num_tokens - num_decode_tokens
248 + # Host-side short-prefill predicate: max_prefill_seq_len is computed
249 + # in the metadata builder (exact for prefill rows) and equals
250 + # positions[prefill_slice].max() + 1, so this replaces a
251 + # positions.max().item() device sync per layer. -1 (unknown metadata)
252 + # falls back to the device-side check.
253 + if prefill_metadata.max_prefill_seq_len >= 0:
254 + short_prefill = (
255 + n_prefill_sf > 0
256 + and positions is not None
257 + and prefill_metadata.max_prefill_seq_len <= topk_tokens
258 + )
259 + else:
260 + short_prefill = (
261 + n_prefill_sf > 0
262 + and positions is not None
263 + and int(positions[num_decode_tokens:num_tokens].max().item()) + 1
264 + <= topk_tokens
265 + )
266 + if short_prefill:
267 + # short_prefill is only True when positions is not None (above),
268 + # but narrow explicitly for the indexer below.
269 + assert positions is not None
270 + _pos = positions[num_decode_tokens:num_tokens].to(torch.int32)
271 + _buf = topk_indices_buffer[num_decode_tokens:num_tokens]
272 + _fill_causal_indices(_buf, _pos)
273 +
274 + # Get the full shared workspace buffers once (will allocate on first use).
275 + # Layout switches between FP8 (head_dim bytes + 4-byte fp32 scale) and
276 + # MXFP4 (head_dim/2 bytes packed + head_dim/MXFP4_BLOCK_SIZE ue8m0
277 + # scales) based on use_fp4_cache.
278 + workspace_manager = current_workspace_manager()
279 + values_spec, scales_spec = _gather_workspace_shapes(
280 + total_seq_lens, head_dim, fp8_dtype, use_fp4_cache
281 + )
282 + k_quant_full, k_scale_full = workspace_manager.get_simultaneous(
283 + values_spec,
284 + scales_spec,
285 + )
286 + for chunk in prefill_metadata.chunks if not short_prefill else ():
287 + k_quant = k_quant_full[: chunk.total_seq_lens]
288 + k_scale = k_scale_full[: chunk.total_seq_lens]
289 +
290 + if not chunk.skip_kv_gather:
291 + from vllm.v1.attention.ops.rocm_aiter_mla_sparse import (
292 + cp_gather_indexer_k_quant_cache_triton,
293 + )
294 +
295 + cp_gather_indexer_k_quant_cache_triton(
296 + kv_cache,
297 + k_quant,
298 + k_scale,
299 + chunk.block_table,
300 + chunk.cu_seq_lens,
301 + token_to_seq=chunk.token_to_seq,
302 + )
303 +
304 + q_slice = q_quant[chunk.token_start : chunk.token_end]
305 + q_scale_slice = (
306 + q_scale[chunk.token_start : chunk.token_end]
307 + if q_scale is not None
308 + else None
309 + )
310 + # DeepGEMM scalar-type tags (zero-copy): MXFP4 values → int8
311 + # (kPackedFP4), scales → int32 squeezed to 1-D kv_sf / 2-D q_sf.
312 + if use_fp4_cache:
313 + q_slice_cast = q_slice.view(torch.int8)
314 + k_quant_cast = k_quant.view(torch.int8)
315 + k_scale_cast = k_scale.view(torch.int32).squeeze(-1)
316 + else:
317 + q_slice_cast = q_slice
318 + k_quant_cast = k_quant
319 + k_scale_cast = k_scale.view(torch.float32).squeeze(-1)
320 + from vllm.v1.attention.ops.rocm_aiter_mla_sparse import (
321 + rocm_fp8_mqa_logits,
322 + )
323 +
324 + assert q_scale_slice is None
325 + logits = rocm_fp8_mqa_logits(
326 + q_slice_cast,
327 + (k_quant_cast, k_scale_cast),
328 + weights[chunk.token_start : chunk.token_end],
329 + chunk.cu_seqlen_ks,
330 + chunk.cu_seqlen_ke,
331 + )
332 + num_rows = logits.shape[0]
333 +
334 + # kpool: logits are pool-granular (compress_ratio == index_kpool),
335 + # so topk selects pools. We pick topk_tokens // kpool pools then
336 + # expand each pool back to its kpool constituent tokens.
337 + select_k = topk_tokens // index_kpool if index_kpool > 1 else topk_tokens
338 + if index_kpool > 1:
339 + pool_topk = torch.empty(
340 + (num_rows, select_k), dtype=torch.int32, device=logits.device
341 + )
342 + topk_dst = pool_topk
343 + else:
344 + topk_dst = topk_indices_buffer[
345 + chunk.token_start : chunk.token_end, :topk_tokens
346 + ]
347 +
348 + torch.ops._C.top_k_per_row_prefill(
349 + logits,
350 + chunk.cu_seqlen_ks,
351 + chunk.cu_seqlen_ke,
352 + topk_dst,
353 + num_rows,
354 + logits.stride(0),
355 + logits.stride(1),
356 + select_k,
357 + )
358 +
359 + if index_kpool > 1:
360 + pool_ids = pool_topk.to(torch.int64)
361 + if positions is not None:
362 + # Fused expand-pools + append-tail into one Triton kernel
363 + # (replaces ~25 elementwise ops). seq_len is token-granular
364 + # (pos+1); the kernel derives pool_len internally.
365 + q_seq = (
366 + positions[chunk.token_start : chunk.token_end].to(torch.int32)
367 + + 1
368 + )
369 + expanded = kpool_ops.expand_pools_and_append_tail(
370 + pool_ids, q_seq, index_kpool
371 + )
372 + else:
373 + valid = pool_ids >= 0
374 + expanded = kpool_ops.expand_pools_to_tokens(
375 + pool_ids, valid, topk_tokens, index_kpool
376 + )
377 + topk_indices_buffer[
378 + chunk.token_start : chunk.token_end, : expanded.shape[-1]
379 + ] = expanded
380 +
381 + if has_decode:
382 + decode_metadata = attn_metadata_narrowed.decode
383 + assert decode_metadata is not None
384 + kv_cache_raw = kv_cache # raw [num_blocks, block_size, head_dim+4] for writes
385 + kv_cache = kv_cache_as_quant_view(kv_cache, head_dim, use_fp4_cache)
386 +
387 + # Update the tail before reading logits; completed pools are compressed
388 + # into the slot supplied by slot_mapping.
389 + # Spec verification groups tokens by request and preserves position
390 + # order so each token is stashed before the next completes its pool.
391 + # Positions must remain token-granular because the kernel derives the
392 + # pool phase and tail index from ``pos % kpool``.
393 + if (
394 + index_kpool > 1
395 + and gate_score is not None
396 + and compress_ape is not None
397 + and positions is not None
398 + and not skip_k_cache_insert
399 + ):
400 + num_requests = attn_metadata_narrowed.num_decodes
401 + # Kpool writes must recover the original request grouping after the
402 + # indexer's flattened decode path. Host metadata avoids a CUDA graph
403 + # sync when choosing the uniform or padded layout.
404 + per_req_lens = decode_metadata.per_req_decode_lens
405 + if per_req_lens is not None:
406 + use_uniform = (
407 + decode_metadata.decode_is_uniform
408 + and num_decode_tokens
409 + == num_requests * decode_metadata.write_max_decode_len
410 + )
411 + group_lens = per_req_lens
412 + lmax = decode_metadata.write_max_decode_len
413 + else:
414 + # Legacy metadata without per-request lens: fall back to the
415 + # host-side requires_padding flag. Unreached now (per-request
416 + # lens is always populated for decode), kept defensive.
417 + use_uniform = not decode_metadata.requires_padding
418 + group_lens = decode_metadata.decode_lens
419 + lmax = int(decode_metadata.decode_lens.max().item())
420 + if not use_uniform:
421 + # Non-uniform decode_lens (mixed plain-decode + spec-verify, or
422 + # a variable MTP-verify batch): scatter actual tokens into a
423 + # padded [B, lmax] layout. int32 tensors can't go through
424 + # pack_seq_triton (float/uint8 only). The scatter indices are
425 + # shared by all five scatters below (and the tail slot one).
426 + scatter_idx = _build_decode_scatter_indices(
427 + group_lens, num_requests, num_decode_tokens
428 + )
429 + dec_k = _scatter_decode_tokens_by_request(
430 + k[:num_decode_tokens], 0, num_requests, lmax, scatter_idx
431 + )
432 + dec_gate = _scatter_decode_tokens_by_request(
433 + gate_score[:num_decode_tokens],
434 + 0,
435 + num_requests,
436 + lmax,
437 + scatter_idx,
438 + )
439 + dec_slot = _scatter_decode_tokens_by_request(
440 + slot_mapping[:num_decode_tokens],
441 + -1,
442 + num_requests,
443 + lmax,
444 + scatter_idx,
445 + )
446 + dec_pos = _scatter_decode_tokens_by_request(
447 + positions[:num_decode_tokens].to(torch.int32),
448 + -1,
449 + num_requests,
450 + lmax,
451 + scatter_idx,
452 + )
453 + else:
454 + next_n = num_decode_tokens // num_requests
455 + shape2 = (num_requests, next_n)
456 + dec_k = k[:num_decode_tokens].view(*shape2, head_dim)
457 + dec_gate = gate_score[:num_decode_tokens].view(*shape2, head_dim)
458 + dec_slot = slot_mapping[:num_decode_tokens].view(shape2)
459 + dec_pos = positions[:num_decode_tokens].to(torch.int32).view(shape2)
460 + tail_meta = (
461 + attn_metadata.get(_resolve_layer_name(tail_prefix))
462 + if tail_prefix is not None
463 + else None
464 + )
465 + # Paged tail cache replaces the transient _DECODE_TAIL ring. Group
466 + # the tail group's token-granular slot_mapping per-request, mirroring
467 + # dec_slot / dec_pos, so the kernel gets each request's current-token
468 + # tail slot (block * kpool + pos % kpool).
469 + if tail_meta is not None:
470 + assert isinstance(tail_meta, DeepseekV32IndexerMetadata)
471 + if tail_meta is None or tail_kv_cache is None:
472 + dec_tail_slot = None
473 + elif not use_uniform:
474 + dec_tail_slot = _scatter_decode_tokens_by_request(
475 + tail_meta.slot_mapping[:num_decode_tokens],
476 + -1,
477 + num_requests,
478 + lmax,
479 + scatter_idx,
480 + )
481 + else:
482 + dec_tail_slot = tail_meta.slot_mapping[:num_decode_tokens].view(shape2)
483 + # The compress kernel writes the raw fp8 cache (not the quant view);
484 + # pass the underlying kv_cache, not kv_cache_quant_view.
485 + if dec_tail_slot is not None:
486 + # Single batched launch over [num_requests, next_n] replaces the
487 + # per-token sequential loop. The kernel iterates each request's
488 + # tokens in position order internally, preserving the
489 + # pool-completion read-after-stash dependency that the loop
490 + # provided. Inputs are already grouped per request (uniform:
491 + # view; non-uniform: _scatter_decode_tokens_by_request padded to
492 + # [B, lmax]) — no per-token .contiguous() copies needed.
493 + kpool_ops.kpool_decode_update_and_maybe_write_cache_batched(
494 + kv_cache_raw,
495 + tail_kv_cache,
496 + dec_tail_slot,
497 + dec_k,
498 + dec_gate,
499 + compress_ape,
500 + dec_slot,
501 + dec_pos,
502 + index_kpool,
503 + head_dim,
504 + round_scale=(scale_fmt is not None),
505 + )
506 + if current_platform.is_cuda_alike() and _fill_short_decode_causal_indices(
507 + topk_indices_buffer,
508 + positions,
509 + num_decode_tokens,
510 + attn_metadata_narrowed.max_seq_len,
511 + topk_tokens,
512 + ):
513 + return topk_indices_buffer
514 + decode_lens = decode_metadata.decode_lens
515 + if decode_metadata.requires_padding:
516 + # Padding also covers short chunked prefills classified as decode.
517 + # MXFP4 uses zero-byte padding so padded slots dequantize to zero.
518 + if q_scale is not None:
519 + padded_q_quant_decode_tokens = pack_seq_triton(
520 + q_quant[:num_decode_tokens], decode_lens, pad_value=0
521 + )
522 + padded_q_scale = pack_seq_triton(
523 + q_scale[:num_decode_tokens], decode_lens, pad_value=0
524 + )
525 + else:
526 + padded_q_quant_decode_tokens = pack_seq_triton(
527 + q_quant[:num_decode_tokens], decode_lens
528 + )
529 + padded_q_scale = None
530 + padded_weights = pack_seq_triton(
531 + weights[:num_decode_tokens], decode_lens, pad_value=0
532 + ).reshape(-1, *weights.shape[1:])
533 + else:
534 + padded_q_quant_decode_tokens = q_quant[:num_decode_tokens].reshape(
535 + decode_lens.shape[0], -1, *q_quant.shape[1:]
536 + )
537 + if q_scale is not None:
538 + padded_q_scale = q_scale[:num_decode_tokens].reshape(
539 + decode_lens.shape[0], -1, *q_scale.shape[1:]
540 + )
541 + else:
542 + padded_q_scale = None
543 + padded_weights = weights[:num_decode_tokens]
544 + # TODO: move and optimize below logic with triton kernels
545 + batch_size = padded_q_quant_decode_tokens.shape[0]
546 + next_n = padded_q_quant_decode_tokens.shape[1]
547 + num_padded_tokens = batch_size * next_n
548 + seq_lens = decode_metadata.seq_lens[:batch_size]
549 + # seq_lens is always 2D: (B, next_n) for native spec decode, (B, 1)
550 + # otherwise. deep_gemm fp8_fp4_paged_mqa_logits requires 2D context_lens;
551 + # the downstream topk kernels accept both 1D and 2D.
552 + padded_q_quant_cast = (
553 + padded_q_quant_decode_tokens.view(torch.int8)
554 + if use_fp4_cache
555 + else padded_q_quant_decode_tokens
556 + )
557 + from vllm.v1.attention.ops.rocm_aiter_mla_sparse import (
558 + rocm_fp8_paged_mqa_logits,
559 + )
560 +
561 + assert padded_q_scale is None
562 + logits = rocm_fp8_paged_mqa_logits(
563 + padded_q_quant_cast,
564 + kv_cache,
565 + padded_weights[:num_padded_tokens],
566 + seq_lens,
567 + decode_metadata.block_table,
568 + decode_metadata.schedule_metadata,
569 + max_model_len=max_model_len,
570 + )
571 + num_rows = logits.shape[0]
572 + # kpool: logits are pool-granular -> select topk_tokens//kpool pools,
573 + # then expand each pool back to its kpool tokens.
574 + select_k = topk_tokens // index_kpool if index_kpool > 1 else topk_tokens
575 + if index_kpool > 1:
576 + pool_topk = torch.empty(
577 + (num_rows, select_k), dtype=torch.int32, device=logits.device
578 + )
579 + topk_dst = pool_topk
580 + else:
581 + topk_dst = topk_indices_buffer[:num_padded_tokens, :topk_tokens]
582 +
583 + torch.ops._C.top_k_per_row_decode(
584 + logits,
585 + next_n,
586 + seq_lens,
587 + topk_dst,
588 + num_rows,
589 + logits.stride(0),
590 + logits.stride(1),
591 + select_k,
592 + )
593 +
594 + # Resolve to token-level indices in the output buffer.
595 + if index_kpool > 1:
596 + pool_ids = pool_topk.to(torch.int64)
597 + n = pool_topk.shape[0]
598 + # Decode seq_lens are pool-granular; recover token lengths from
599 + # positions using the padded [B, next_n] row layout when needed.
600 + if positions is not None:
601 + dec_seq = _decode_topk_seq_lens(
602 + positions,
603 + decode_lens,
604 + num_decode_tokens,
605 + batch_size,
606 + next_n,
607 + decode_metadata.requires_padding,
608 + )
609 + else:
610 + dec_seq = decode_metadata.seq_lens[:n]
611 + if dec_seq.ndim == 2:
612 + dec_seq = dec_seq[:, -1]
613 + dec_seq = dec_seq.to(torch.int32)
614 + out = kpool_ops.expand_pools_and_append_tail(pool_ids, dec_seq, index_kpool)
615 + else:
616 + out = topk_dst
617 +
618 + if decode_metadata.requires_padding:
619 + # Drop padded query rows introduced by the next_n padding above.
620 + out = unpack_seq_triton(
621 + out.reshape(batch_size, -1, out.shape[-1]), decode_lens
622 + )
623 + topk_indices_buffer[: out.shape[0], : out.shape[-1]] = out
624 +
625 + return topk_indices_buffer
626 +
627 +
628 +@CustomOp.register("sparse_attn_indexer_kpool")
629 +class SparseAttnIndexerKpool(CustomOp):
630 + """Sparse Attention Indexer Custom Op Layer. This layer is extracted as a
631 + separate custom op since it involves heavy custom kernels like `mqa_logits`,
632 + `paged_mqa_logits` and `top_k_per_row`, etc. Those kernels maybe requires
633 + specific memory layout or implementation for different hardware backends to
634 + achieve optimal performance.
635 +
636 + For now, the default native path will use CUDA backend path. Other platform
637 + may requires add the corresponding Custom Op name `sparse_attn_indexer` to
638 + `custom_ops` in `CompilationConfig` to enable the platform specific path.
639 + """
640 +
641 + def __init__(
642 + self,
643 + k_cache,
644 + quant_block_size: int,
645 + scale_fmt: str,
646 + topk_tokens: int,
647 + head_dim: int,
648 + max_model_len: int,
649 + max_total_seq_len: int,
650 + topk_indices_buffer: torch.Tensor,
651 + skip_k_cache_insert: bool = False,
652 + use_fp4_cache: bool = False,
653 + tail_cache=None,
654 + ):
655 + super().__init__()
656 + self.k_cache = k_cache
657 + self.tail_cache = tail_cache
658 + self.quant_block_size = quant_block_size
659 + self.scale_fmt = scale_fmt
660 + self.topk_tokens = topk_tokens
661 + self.head_dim = head_dim
662 + self.max_model_len = max_model_len
663 + self.max_total_seq_len = max_total_seq_len
664 + self.topk_indices_buffer = topk_indices_buffer
665 + self.skip_k_cache_insert = skip_k_cache_insert
666 + self.use_fp4_cache = use_fp4_cache
667 +
668 + def forward_native(
669 + self,
670 + hidden_states: torch.Tensor,
671 + q_quant: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
672 + k: torch.Tensor,
673 + weights: torch.Tensor,
674 + *,
675 + gate_score: torch.Tensor | None = None,
676 + compress_ape: torch.Tensor | None = None,
677 + index_kpool: int = 1,
678 + positions: torch.Tensor | None = None,
679 + ):
680 + assert not self.use_fp4_cache, "AMD platform doesn't support fp4 cache yet"
681 + assert isinstance(q_quant, torch.Tensor), (
682 + "AMD sparse_attn_indexer expects a single FP8 q_quant tensor"
683 + )
684 + if not rocm_aiter_ops.is_enabled():
685 + raise RuntimeError(
686 + "Sparse attention indexer ROCm path is only supported on AITER. "
687 + "Please enable aiter with VLLM_ROCM_USE_AITER=1"
688 + )
689 + if index_kpool <= 1:
690 + return torch.ops.vllm.rocm_aiter_sparse_attn_indexer(
691 + hidden_states,
692 + _encode_layer_name(self.k_cache.prefix),
693 + self.k_cache.kv_cache,
694 + q_quant,
695 + k,
696 + weights,
697 + self.quant_block_size,
698 + self.scale_fmt,
699 + self.topk_tokens,
700 + self.head_dim,
701 + self.max_model_len,
702 + self.max_total_seq_len,
703 + self.topk_indices_buffer,
704 + skip_k_cache_insert=self.skip_k_cache_insert,
705 + )
706 + return sparse_attn_indexer_kpool(
707 + hidden_states,
708 + self.k_cache.prefix,
709 + self.k_cache.kv_cache,
710 + q_quant,
711 + None,
712 + k,
713 + weights,
714 + self.quant_block_size,
715 + self.scale_fmt,
716 + self.topk_tokens,
717 + self.head_dim,
718 + self.max_model_len,
719 + self.max_total_seq_len,
720 + self.topk_indices_buffer,
721 + self.skip_k_cache_insert,
722 + self.use_fp4_cache,
723 + gate_score,
724 + compress_ape,
725 + index_kpool,
726 + positions,
727 + self.tail_cache.kv_cache if self.tail_cache is not None else None,
728 + self.tail_cache.prefix if self.tail_cache is not None else None,
729 + )
vllm/models/glm5next/common/attention.py
+1 -1
@@ -23,7 +23,6 @@ from vllm.model_executor.layers.linear import (
23 from vllm.model_executor.layers.mla import MLAModules, MultiHeadLatentAttentionWrapper
24 from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
25 from vllm.model_executor.layers.rotary_embedding import RotaryEmbedding, get_rope
26 -from vllm.model_executor.layers.sparse_attn_indexer_kpool import SparseAttnIndexerKpool
26 from vllm.model_executor.models.deepseek_v2 import (
27 DeepSeekV2FusedQkvAProjLinear,
28 DeepseekV32IndexerCache,
@@ -31,6 +30,7 @@ from vllm.model_executor.models.deepseek_v2 import (
30 )
31 from vllm.model_executor.utils import maybe_disable_graph_partition
32 from vllm.models.glm5next.nvidia.ops.kpool_compress import fwht128_quant_fp8
33 +from vllm.models.glm5next.sparse_indexer import SparseAttnIndexerKpool
34 from vllm.platforms import current_platform
35 from vllm.transformers_utils.configs.glm5_next import Glm5NextConfig
36 from vllm.utils.deep_gemm import PAGED_MQA_PAGE_SIZES
vllm/models/glm5next/common/sparse_indexer.py new
+170
@@ -0,0 +1,170 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Shared helpers for the glm5next sparse attention indexer (kpool) layers."""
4 +
5 +import torch
6 +
7 +RADIX_TOPK_WORKSPACE_SIZE = 1024 * 1024
8 +
9 +# MXFP4 layout: 2 values packed per byte, ue8m0 (1-byte) scale per block of 32.
10 +MXFP4_BLOCK_SIZE = 32
11 +
12 +
13 +def _build_decode_scatter_indices(
14 + decode_lens: torch.Tensor,
15 + num_requests: int,
16 + n: int,
17 +) -> tuple[torch.Tensor, torch.Tensor]:
18 + """Per-token (request id, intra-request index) for a non-uniform decode
19 + batch, with ``n == decode_lens.sum()`` as a host int (avoids a
20 + device sync and keeps both repeat_interleaves sync-free).
21 +
22 + Shared by every ``_scatter_decode_tokens_by_request`` call in a step:
23 + building it per call would repeat the same repeat_interleave/cumsum chain
24 + up to 5x per layer on the eager decode break.
25 + """
26 + device = decode_lens.device
27 + dl = decode_lens.to(torch.int64)
28 + req_id = torch.repeat_interleave(
29 + torch.arange(num_requests, device=device, dtype=torch.int64),
30 + dl,
31 + output_size=n,
32 + )
33 + req_starts = torch.cumsum(
34 + torch.cat([torch.zeros(1, device=device, dtype=torch.int64), dl[:-1]]),
35 + dim=0,
36 + )
37 + # Broadcast the per-request start offsets to per-token (length n ==
38 + # dl.sum()) so each token's intra-request index subtracts its own
39 + # request's start.
40 + starts = torch.repeat_interleave(req_starts, dl, output_size=n)
41 + intra = torch.arange(n, device=device, dtype=torch.int64) - starts
42 + return req_id, intra
43 +
44 +
45 +def _scatter_decode_tokens_by_request(
46 + tokens: torch.Tensor,
47 + pad_value,
48 + num_requests: int,
49 + lmax: int,
50 + scatter_indices: tuple[torch.Tensor, torch.Tensor],
51 +) -> torch.Tensor:
52 + """Group ``[N, ...]`` decode tokens into a padded ``[num_requests, lmax, ...]``
53 + layout: request ``r``'s tokens at row ``r`` in order; short requests padded.
54 +
55 + Unlike ``pack_seq_triton`` this is dtype-agnostic (needed for the int32
56 + slot/pos tensors) — it scatters with the shared per-step indices from
57 + ``_build_decode_scatter_indices``. Used only for the non-uniform
58 + (``requires_padding``) decode batch; uniform batches use a zero-copy
59 + reshape.
60 + """
61 + req_id, intra = scatter_indices
62 + out = torch.full(
63 + (num_requests, lmax, *tokens.shape[1:]),
64 + pad_value,
65 + dtype=tokens.dtype,
66 + device=tokens.device,
67 + )
68 + out[req_id, intra] = tokens
69 + return out
70 +
71 +
72 +def _decode_topk_seq_lens(
73 + positions: torch.Tensor,
74 + decode_lens: torch.Tensor,
75 + num_decode_tokens: int,
76 + batch_size: int,
77 + next_n: int,
78 + requires_padding: bool,
79 +) -> torch.Tensor:
80 + """Token-granular seq_len (pos + 1) per pool-topk row, layout-aware.
81 +
82 + ``pool_topk`` (and the logits it comes from) follow the padded
83 + ``[batch_size, next_n]`` grid whenever ``requires_padding`` is set, so row
84 + ``(b, t)`` corresponds to flat decode token ``offset_b + t`` -- NOT
85 + ``b * next_n + t``. Slicing flat ``positions[: batch_size * next_n]``
86 + (the uniform-layout shortcut) misaligns every row after the first
87 + non-uniform request and, past the decode region, reads prefill tokens'
88 + positions; ``expand_pools_and_append_tail`` then anchors the tail at
89 + another request's length, dropping the row's real tail tokens or emitting
90 + indices past its sequence (out-of-bounds block-table reads). Padded rows
91 + get 0 (empty tail); they are dropped by ``unpack_seq_triton`` anyway.
92 + """
93 + n = batch_size * next_n
94 + if not requires_padding:
95 + return positions[:n].to(torch.int32) + 1
96 + scatter_idx = _build_decode_scatter_indices(
97 + decode_lens, batch_size, num_decode_tokens
98 + )
99 + padded = _scatter_decode_tokens_by_request(
100 + positions[:num_decode_tokens].to(torch.int32),
101 + -1,
102 + batch_size,
103 + next_n,
104 + scatter_idx,
105 + )
106 + return padded.reshape(n) + 1 # pad rows: -1 + 1 = 0 -> empty tail
107 +
108 +
109 +def _fill_causal_indices(rows: torch.Tensor, positions: torch.Tensor) -> None:
110 + causal_range = torch.arange(rows.shape[1], device=rows.device, dtype=torch.int32)
111 + positions = positions.to(torch.int32)
112 + rows[:] = causal_range[None, :]
113 + rows[causal_range[None, :] > positions[:, None]] = -1
114 +
115 +
116 +def _fill_short_decode_causal_indices(
117 + topk_indices_buffer: torch.Tensor,
118 + positions: torch.Tensor | None,
119 + num_decode_tokens: int,
120 + max_seq_len: int,
121 + topk_tokens: int,
122 +) -> bool:
123 + """Fill exact causal rows when sparse decode would select every token."""
124 + if positions is None or positions.numel() == 0 or max_seq_len > topk_tokens:
125 + return False
126 + _fill_causal_indices(
127 + topk_indices_buffer[:num_decode_tokens], positions[:num_decode_tokens]
128 + )
129 + return True
130 +
131 +
132 +def _gather_workspace_shapes(
133 + total_seq_lens: int,
134 + head_dim: int,
135 + fp8_dtype: torch.dtype,
136 + use_fp4_cache: bool,
137 +) -> tuple[tuple[tuple[int, int], torch.dtype], tuple[tuple[int, int], torch.dtype]]:
138 + """Return ((values_shape, values_dtype), (scales_shape, scales_dtype)) for
139 + the K-gather workspace. FP8 path: (T, head_dim) fp8 + (T, 4) uint8 fp32
140 + scales. MXFP4 path: (T, head_dim // 2) uint8 packed mxfp4 +
141 + (T, head_dim // MXFP4_BLOCK_SIZE) uint8 ue8m0 scales."""
142 + if use_fp4_cache:
143 + return (
144 + ((total_seq_lens, head_dim // 2), torch.uint8),
145 + ((total_seq_lens, head_dim // MXFP4_BLOCK_SIZE), torch.uint8),
146 + )
147 + return (
148 + ((total_seq_lens, head_dim), fp8_dtype),
149 + ((total_seq_lens, 4), torch.uint8),
150 + )
151 +
152 +
153 +def kv_cache_as_quant_view(
154 + kv_cache: torch.Tensor,
155 + head_dim: int,
156 + use_fp4_cache: bool,
157 +) -> torch.Tensor:
158 + """4D ``[num_blocks, block_size, 1, head_width]`` view expected by
159 + DeepGEMM, from the 3D indexer kv-cache allocation."""
160 + if use_fp4_cache:
161 + assert kv_cache.ndim == 3 and kv_cache.dtype == torch.uint8
162 + num_blocks, block_size, _ = kv_cache.shape
163 + page_bytes = int(kv_cache.stride(0))
164 + fp4_bytes = head_dim // 2 + head_dim // MXFP4_BLOCK_SIZE
165 + return torch.as_strided(
166 + kv_cache,
167 + size=(num_blocks, block_size, 1, fp4_bytes),
168 + stride=(page_bytes, fp4_bytes, fp4_bytes, 1),
169 + )
170 + return kv_cache.unsqueeze(-2)
vllm/models/glm5next/nvidia/sparse_indexer.py renamed
+76 -391
@@ -2,30 +2,29 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Custom Sparse Attention Indexer layers."""
4
5 -from typing import TYPE_CHECKING
6 -
5 import torch
6
7 import vllm.envs as envs
10 -from vllm._aiter_ops import rocm_aiter_ops
8 from vllm.compilation.breakable_cudagraph import eager_break_during_capture
9 from vllm.config import get_current_vllm_config_or_none
10 from vllm.forward_context import get_forward_context
11 from vllm.logger import init_logger
12 from vllm.model_executor.custom_op import CustomOp
13 +from vllm.models.glm5next.common.sparse_indexer import (
14 + RADIX_TOPK_WORKSPACE_SIZE,
15 + _build_decode_scatter_indices,
16 + _decode_topk_seq_lens,
17 + _fill_causal_indices,
18 + _fill_short_decode_causal_indices,
19 + _gather_workspace_shapes,
20 + _scatter_decode_tokens_by_request,
21 + kv_cache_as_quant_view,
22 +)
23 +from vllm.models.glm5next.nvidia.ops import kpool_compress as kpool_ops
24 from vllm.platforms import current_platform
17 -
18 -if TYPE_CHECKING:
19 - from vllm.models.glm5next.nvidia.ops import kpool_compress as kpool_ops
20 -elif current_platform.is_rocm():
21 - from vllm.models.glm5next.amd.ops import kpool_compress as kpool_ops
22 -else:
23 - from vllm.models.glm5next.nvidia.ops import kpool_compress as kpool_ops
24 -
25 from vllm.utils.deep_gemm import has_deep_gemm
26 from vllm.utils.torch_utils import (
27 LayerNameType,
28 - _encode_layer_name,
28 _resolve_layer_name,
29 )
30 from vllm.v1.attention.backends.mla.indexer import (
@@ -36,16 +35,9 @@ from vllm.v1.worker.workspace import current_workspace_manager
35
36 if current_platform.is_cuda_alike():
37 from vllm import _custom_ops as ops
39 -elif current_platform.is_xpu():
40 - from vllm._xpu_ops import xpu_ops
38
39 logger = init_logger(__name__)
40
44 -RADIX_TOPK_WORKSPACE_SIZE = 1024 * 1024
45 -
46 -# MXFP4 layout: 2 values packed per byte, ue8m0 (1-byte) scale per block of 32.
47 -MXFP4_BLOCK_SIZE = 32
48 -
41 # kpool write helper: form pools from the current token batch and compress them
42 # into the index K cache via the fused Triton kernel.
43
@@ -96,166 +88,6 @@ def _kpool_compress_insert(
88 )
89
90
99 -def _build_decode_scatter_indices(
100 - decode_lens: torch.Tensor,
101 - num_requests: int,
102 - n: int,
103 -) -> tuple[torch.Tensor, torch.Tensor]:
104 - """Per-token (request id, intra-request index) for a non-uniform decode
105 - batch, with ``n == decode_lens.sum()`` as a host int (avoids a
106 - device sync and keeps both repeat_interleaves sync-free).
107 -
108 - Shared by every ``_scatter_decode_tokens_by_request`` call in a step:
109 - building it per call would repeat the same repeat_interleave/cumsum chain
110 - up to 5x per layer on the eager decode break.
111 - """
112 - device = decode_lens.device
113 - dl = decode_lens.to(torch.int64)
114 - req_id = torch.repeat_interleave(
115 - torch.arange(num_requests, device=device, dtype=torch.int64),
116 - dl,
117 - output_size=n,
118 - )
119 - req_starts = torch.cumsum(
120 - torch.cat([torch.zeros(1, device=device, dtype=torch.int64), dl[:-1]]),
121 - dim=0,
122 - )
123 - # Broadcast the per-request start offsets to per-token (length n ==
124 - # dl.sum()) so each token's intra-request index subtracts its own
125 - # request's start.
126 - starts = torch.repeat_interleave(req_starts, dl, output_size=n)
127 - intra = torch.arange(n, device=device, dtype=torch.int64) - starts
128 - return req_id, intra
129 -
130 -
131 -def _scatter_decode_tokens_by_request(
132 - tokens: torch.Tensor,
133 - pad_value,
134 - num_requests: int,
135 - lmax: int,
136 - scatter_indices: tuple[torch.Tensor, torch.Tensor],
137 -) -> torch.Tensor:
138 - """Group ``[N, ...]`` decode tokens into a padded ``[num_requests, lmax, ...]``
139 - layout: request ``r``'s tokens at row ``r`` in order; short requests padded.
140 -
141 - Unlike ``pack_seq_triton`` this is dtype-agnostic (needed for the int32
142 - slot/pos tensors) — it scatters with the shared per-step indices from
143 - ``_build_decode_scatter_indices``. Used only for the non-uniform
144 - (``requires_padding``) decode batch; uniform batches use a zero-copy
145 - reshape.
146 - """
147 - req_id, intra = scatter_indices
148 - out = torch.full(
149 - (num_requests, lmax, *tokens.shape[1:]),
150 - pad_value,
151 - dtype=tokens.dtype,
152 - device=tokens.device,
153 - )
154 - out[req_id, intra] = tokens
155 - return out
156 -
157 -
158 -def _decode_topk_seq_lens(
159 - positions: torch.Tensor,
160 - decode_lens: torch.Tensor,
161 - num_decode_tokens: int,
162 - batch_size: int,
163 - next_n: int,
164 - requires_padding: bool,
165 -) -> torch.Tensor:
166 - """Token-granular seq_len (pos + 1) per pool-topk row, layout-aware.
167 -
168 - ``pool_topk`` (and the logits it comes from) follow the padded
169 - ``[batch_size, next_n]`` grid whenever ``requires_padding`` is set, so row
170 - ``(b, t)`` corresponds to flat decode token ``offset_b + t`` -- NOT
171 - ``b * next_n + t``. Slicing flat ``positions[: batch_size * next_n]``
172 - (the uniform-layout shortcut) misaligns every row after the first
173 - non-uniform request and, past the decode region, reads prefill tokens'
174 - positions; ``expand_pools_and_append_tail`` then anchors the tail at
175 - another request's length, dropping the row's real tail tokens or emitting
176 - indices past its sequence (out-of-bounds block-table reads). Padded rows
177 - get 0 (empty tail); they are dropped by ``unpack_seq_triton`` anyway.
178 - """
179 - n = batch_size * next_n
180 - if not requires_padding:
181 - return positions[:n].to(torch.int32) + 1
182 - scatter_idx = _build_decode_scatter_indices(
183 - decode_lens, batch_size, num_decode_tokens
184 - )
185 - padded = _scatter_decode_tokens_by_request(
186 - positions[:num_decode_tokens].to(torch.int32),
187 - -1,
188 - batch_size,
189 - next_n,
190 - scatter_idx,
191 - )
192 - return padded.reshape(n) + 1 # pad rows: -1 + 1 = 0 -> empty tail
193 -
194 -
195 -def _fill_causal_indices(rows: torch.Tensor, positions: torch.Tensor) -> None:
196 - causal_range = torch.arange(rows.shape[1], device=rows.device, dtype=torch.int32)
197 - positions = positions.to(torch.int32)
198 - rows[:] = causal_range[None, :]
199 - rows[causal_range[None, :] > positions[:, None]] = -1
200 -
201 -
202 -def _fill_short_decode_causal_indices(
203 - topk_indices_buffer: torch.Tensor,
204 - positions: torch.Tensor | None,
205 - num_decode_tokens: int,
206 - max_seq_len: int,
207 - topk_tokens: int,
208 -) -> bool:
209 - """Fill exact causal rows when sparse decode would select every token."""
210 - if positions is None or positions.numel() == 0 or max_seq_len > topk_tokens:
211 - return False
212 - _fill_causal_indices(
213 - topk_indices_buffer[:num_decode_tokens], positions[:num_decode_tokens]
214 - )
215 - return True
216 -
217 -
218 -def _gather_workspace_shapes(
219 - total_seq_lens: int,
220 - head_dim: int,
221 - fp8_dtype: torch.dtype,
222 - use_fp4_cache: bool,
223 -) -> tuple[tuple[tuple[int, int], torch.dtype], tuple[tuple[int, int], torch.dtype]]:
224 - """Return ((values_shape, values_dtype), (scales_shape, scales_dtype)) for
225 - the K-gather workspace. FP8 path: (T, head_dim) fp8 + (T, 4) uint8 fp32
226 - scales. MXFP4 path: (T, head_dim // 2) uint8 packed mxfp4 +
227 - (T, head_dim // MXFP4_BLOCK_SIZE) uint8 ue8m0 scales."""
228 - if use_fp4_cache:
229 - return (
230 - ((total_seq_lens, head_dim // 2), torch.uint8),
231 - ((total_seq_lens, head_dim // MXFP4_BLOCK_SIZE), torch.uint8),
232 - )
233 - return (
234 - ((total_seq_lens, head_dim), fp8_dtype),
235 - ((total_seq_lens, 4), torch.uint8),
236 - )
237 -
238 -
239 -def kv_cache_as_quant_view(
240 - kv_cache: torch.Tensor,
241 - head_dim: int,
242 - use_fp4_cache: bool,
243 -) -> torch.Tensor:
244 - """4D ``[num_blocks, block_size, 1, head_width]`` view expected by
245 - DeepGEMM, from the 3D indexer kv-cache allocation."""
246 - if use_fp4_cache:
247 - assert kv_cache.ndim == 3 and kv_cache.dtype == torch.uint8
248 - num_blocks, block_size, _ = kv_cache.shape
249 - page_bytes = int(kv_cache.stride(0))
250 - fp4_bytes = head_dim // 2 + head_dim // MXFP4_BLOCK_SIZE
251 - return torch.as_strided(
252 - kv_cache,
253 - size=(num_blocks, block_size, 1, fp4_bytes),
254 - stride=(page_bytes, fp4_bytes, fp4_bytes, 1),
255 - )
256 - return kv_cache.unsqueeze(-2)
257 -
258 -
91 @eager_break_during_capture
92 def sparse_attn_indexer_kpool(
93 hidden_states: torch.Tensor,
@@ -393,26 +225,13 @@ def sparse_attn_indexer_kpool(
225 else:
226 # standard: per-token fp8 quant + scatter (all tokens).
227 assert scale_fmt is not None
396 - if current_platform.is_rocm():
397 - from vllm.v1.attention.ops.rocm_aiter_mla_sparse import (
398 - indexer_k_quant_and_cache_triton,
399 - )
400 -
401 - indexer_k_quant_and_cache_triton(
402 - k,
403 - kv_cache,
404 - slot_mapping,
405 - quant_block_size,
406 - scale_fmt,
407 - )
408 - else:
409 - ops.indexer_k_quant_and_cache(
410 - k,
411 - kv_cache,
412 - slot_mapping,
413 - quant_block_size,
414 - scale_fmt,
415 - )
228 + ops.indexer_k_quant_and_cache(
229 + k,
230 + kv_cache,
231 + slot_mapping,
232 + quant_block_size,
233 + scale_fmt,
234 + )
235
236 topk_indices_buffer[: hidden_states.shape[0]] = -1
237 if has_prefill:
@@ -466,27 +285,13 @@ def sparse_attn_indexer_kpool(
285 k_scale = k_scale_full[: chunk.total_seq_lens]
286
287 if not chunk.skip_kv_gather:
469 - if current_platform.is_rocm():
470 - from vllm.v1.attention.ops.rocm_aiter_mla_sparse import (
471 - cp_gather_indexer_k_quant_cache_triton,
472 - )
473 -
474 - cp_gather_indexer_k_quant_cache_triton(
475 - kv_cache,
476 - k_quant,
477 - k_scale,
478 - chunk.block_table,
479 - chunk.cu_seq_lens,
480 - token_to_seq=chunk.token_to_seq,
481 - )
482 - else:
483 - ops.cp_gather_indexer_k_quant_cache(
484 - kv_cache,
485 - k_quant,
486 - k_scale,
487 - chunk.block_table,
488 - chunk.cu_seq_lens,
489 - )
288 + ops.cp_gather_indexer_k_quant_cache(
289 + kv_cache,
290 + k_quant,
291 + k_scale,
292 + chunk.block_table,
293 + chunk.cu_seq_lens,
294 + )
295
296 q_slice = q_quant[chunk.token_start : chunk.token_end]
297 q_scale_slice = (
@@ -504,30 +309,16 @@ def sparse_attn_indexer_kpool(
309 q_slice_cast = q_slice
310 k_quant_cast = k_quant
311 k_scale_cast = k_scale.view(torch.float32).squeeze(-1)
507 - if current_platform.is_rocm():
508 - from vllm.v1.attention.ops.rocm_aiter_mla_sparse import (
509 - rocm_fp8_mqa_logits,
510 - )
511 -
512 - assert q_scale_slice is None
513 - logits = rocm_fp8_mqa_logits(
514 - q_slice_cast,
515 - (k_quant_cast, k_scale_cast),
516 - weights[chunk.token_start : chunk.token_end],
517 - chunk.cu_seqlen_ks,
518 - chunk.cu_seqlen_ke,
519 - )
520 - else:
521 - from vllm.utils.deep_gemm import fp8_fp4_mqa_logits
522 -
523 - logits = fp8_fp4_mqa_logits(
524 - (q_slice_cast, q_scale_slice),
525 - (k_quant_cast, k_scale_cast),
526 - weights[chunk.token_start : chunk.token_end],
527 - chunk.cu_seqlen_ks,
528 - chunk.cu_seqlen_ke,
529 - clean_logits=False,
530 - )
312 + from vllm.utils.deep_gemm import fp8_fp4_mqa_logits
313 +
314 + logits = fp8_fp4_mqa_logits(
315 + (q_slice_cast, q_scale_slice),
316 + (k_quant_cast, k_scale_cast),
317 + weights[chunk.token_start : chunk.token_end],
318 + chunk.cu_seqlen_ks,
319 + chunk.cu_seqlen_ke,
320 + clean_logits=False,
321 + )
322 num_rows = logits.shape[0]
323
324 # kpool: logits are pool-granular (compress_ratio == index_kpool),
@@ -544,28 +335,16 @@ def sparse_attn_indexer_kpool(
335 chunk.token_start : chunk.token_end, :topk_tokens
336 ]
337
547 - if current_platform.is_xpu():
548 - xpu_ops.top_k_per_row_prefill( # type: ignore[attr-defined]
549 - logits,
550 - chunk.cu_seqlen_ks,
551 - chunk.cu_seqlen_ke,
552 - topk_dst,
553 - num_rows,
554 - logits.stride(0),
555 - logits.stride(1),
556 - select_k,
557 - )
558 - else:
559 - torch.ops._C.top_k_per_row_prefill(
560 - logits,
561 - chunk.cu_seqlen_ks,
562 - chunk.cu_seqlen_ke,
563 - topk_dst,
564 - num_rows,
565 - logits.stride(0),
566 - logits.stride(1),
567 - select_k,
568 - )
338 + torch.ops._C.top_k_per_row_prefill(
339 + logits,
340 + chunk.cu_seqlen_ks,
341 + chunk.cu_seqlen_ke,
342 + topk_dst,
343 + num_rows,
344 + logits.stride(0),
345 + logits.stride(1),
346 + select_k,
347 + )
348
349 if index_kpool > 1:
350 pool_ids = pool_topk.to(torch.int64)
@@ -765,34 +544,18 @@ def sparse_attn_indexer_kpool(
544 if use_fp4_cache
545 else padded_q_quant_decode_tokens
546 )
768 - if current_platform.is_rocm():
769 - from vllm.v1.attention.ops.rocm_aiter_mla_sparse import (
770 - rocm_fp8_paged_mqa_logits,
771 - )
772 -
773 - assert padded_q_scale is None
774 - logits = rocm_fp8_paged_mqa_logits(
775 - padded_q_quant_cast,
776 - kv_cache,
777 - padded_weights[:num_padded_tokens],
778 - seq_lens,
779 - decode_metadata.block_table,
780 - decode_metadata.schedule_metadata,
781 - max_model_len=max_model_len,
782 - )
783 - else:
784 - from vllm.utils.deep_gemm import fp8_fp4_paged_mqa_logits
547 + from vllm.utils.deep_gemm import fp8_fp4_paged_mqa_logits
548
786 - logits = fp8_fp4_paged_mqa_logits(
787 - (padded_q_quant_cast, padded_q_scale),
788 - kv_cache,
789 - padded_weights[:num_padded_tokens],
790 - seq_lens,
791 - decode_metadata.block_table,
792 - decode_metadata.schedule_metadata,
793 - max_model_len=max_model_len,
794 - clean_logits=False,
795 - )
549 + logits = fp8_fp4_paged_mqa_logits(
550 + (padded_q_quant_cast, padded_q_scale),
551 + kv_cache,
552 + padded_weights[:num_padded_tokens],
553 + seq_lens,
554 + decode_metadata.block_table,
555 + decode_metadata.schedule_metadata,
556 + max_model_len=max_model_len,
557 + clean_logits=False,
558 + )
559 num_rows = logits.shape[0]
560 # kpool: logits are pool-granular -> select topk_tokens//kpool pools,
561 # then expand each pool back to its kpool tokens.
@@ -819,28 +582,16 @@ def sparse_attn_indexer_kpool(
582 attn_metadata_narrowed.max_seq_len,
583 )
584 else:
822 - if current_platform.is_xpu():
823 - xpu_ops.top_k_per_row_decode( # type: ignore[attr-defined]
824 - logits,
825 - next_n,
826 - seq_lens,
827 - topk_dst,
828 - num_rows,
829 - logits.stride(0),
830 - logits.stride(1),
831 - select_k,
832 - )
833 - else:
834 - torch.ops._C.top_k_per_row_decode(
835 - logits,
836 - next_n,
837 - seq_lens,
838 - topk_dst,
839 - num_rows,
840 - logits.stride(0),
841 - logits.stride(1),
842 - select_k,
843 - )
585 + torch.ops._C.top_k_per_row_decode(
586 + logits,
587 + next_n,
588 + seq_lens,
589 + topk_dst,
590 + num_rows,
591 + logits.stride(0),
592 + logits.stride(1),
593 + select_k,
594 + )
595
596 # Resolve to token-level indices in the output buffer.
597 if index_kpool > 1:
@@ -942,33 +693,16 @@ class SparseAttnIndexerKpool(CustomOp):
693 index_kpool: int = 1,
694 positions: torch.Tensor | None = None,
695 ):
945 - if current_platform.is_cuda() or current_platform.is_xpu():
946 - return self.forward_cuda(
947 - hidden_states,
948 - q_quant,
949 - k,
950 - weights,
951 - gate_score=gate_score,
952 - compress_ape=compress_ape,
953 - index_kpool=index_kpool,
954 - positions=positions,
955 - )
956 - elif current_platform.is_rocm():
957 - return self.forward_hip(
958 - hidden_states,
959 - q_quant,
960 - k,
961 - weights,
962 - gate_score=gate_score,
963 - compress_ape=compress_ape,
964 - index_kpool=index_kpool,
965 - positions=positions,
966 - )
967 - else:
968 - raise NotImplementedError(
969 - "SparseAttnIndexer native forward is only implemented for "
970 - "CUDA, ROCm and XPU platforms."
971 - )
696 + return self.forward_cuda(
697 + hidden_states,
698 + q_quant,
699 + k,
700 + weights,
701 + gate_score=gate_score,
702 + compress_ape=compress_ape,
703 + index_kpool=index_kpool,
704 + positions=positions,
705 + )
706
707 def forward_cuda(
708 self,
@@ -1012,52 +746,3 @@ class SparseAttnIndexerKpool(CustomOp):
746 self.tail_cache.kv_cache if self.tail_cache is not None else None,
747 self.tail_cache.prefix if self.tail_cache is not None else None,
748 )
1015 -
1016 - def forward_hip(
1017 - self,
1018 - hidden_states: torch.Tensor,
1019 - q_quant: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
1020 - k: torch.Tensor,
1021 - weights: torch.Tensor,
1022 - *,
1023 - gate_score: torch.Tensor | None = None,
1024 - compress_ape: torch.Tensor | None = None,
1025 - index_kpool: int = 1,
1026 - positions: torch.Tensor | None = None,
1027 - ):
1028 - assert not self.use_fp4_cache, "AMD platform doesn't support fp4 cache yet"
1029 - assert isinstance(q_quant, torch.Tensor), (
1030 - "AMD sparse_attn_indexer expects a single FP8 q_quant tensor"
1031 - )
1032 - if rocm_aiter_ops.is_enabled():
1033 - if index_kpool <= 1:
1034 - return torch.ops.vllm.rocm_aiter_sparse_attn_indexer(
1035 - hidden_states,
1036 - _encode_layer_name(self.k_cache.prefix),
1037 - self.k_cache.kv_cache,
1038 - q_quant,
1039 - k,
1040 - weights,
1041 - self.quant_block_size,
1042 - self.scale_fmt,
1043 - self.topk_tokens,
1044 - self.head_dim,
1045 - self.max_model_len,
1046 - self.max_total_seq_len,
1047 - self.topk_indices_buffer,
1048 - skip_k_cache_insert=self.skip_k_cache_insert,
1049 - )
1050 - return self.forward_cuda(
1051 - hidden_states,
1052 - q_quant,
1053 - k,
1054 - weights,
1055 - gate_score=gate_score,
1056 - compress_ape=compress_ape,
1057 - index_kpool=index_kpool,
1058 - positions=positions,
1059 - )
1060 - raise RuntimeError(
1061 - "Sparse attention indexer ROCm path is only supported on AITER. "
1062 - "Please enable aiter with VLLM_ROCM_USE_AITER=1"
1063 - )
vllm/models/glm5next/sparse_indexer.py new
+22
@@ -0,0 +1,22 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Sparse attention indexer (kpool) — hardware-isolated entry point.
4 +
5 +The implementation lives under ``nvidia/`` and ``amd/`` (with shared helpers
6 +in ``common/``); this module picks the right one for the current platform.
7 +"""
8 +
9 +from vllm.platforms import current_platform
10 +
11 +if current_platform.is_rocm():
12 + from .amd.sparse_indexer import (
13 + SparseAttnIndexerKpool,
14 + )
15 +else:
16 + from .nvidia.sparse_indexer import (
17 + SparseAttnIndexerKpool,
18 + )
19 +
20 +__all__ = [
21 + "SparseAttnIndexerKpool",
22 +]