com-junkawasaki commited on
Commit
9e9e40a
·
verified ·
1 Parent(s): 7b4ba28

Add pinned GLM-5.3 vLLM runtime patches

Browse files
serving/Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Matched-pair runtime for GLM-5.3-Flash-NVFP4-FP8ATTN-512K.
2
+ # Base: the public per-model vLLM image for glm5_next on SM120 (not redistributed here).
3
+ # The four COPY lines are the ENTIRE delta — see README.md in this directory.
4
+ #
5
+ # docker build -t local/vllm-glm53:fp8attn-512k .
6
+ FROM cstechdev/vllm:glm53-flash-nope-sm120-cu130-20260826-r1
7
+
8
+ # (1) KDA layers: stop stripping quant_config at construction (kda.py:168-175 upstream)
9
+ COPY kda.py /usr/local/lib/python3.12/dist-packages/vllm/models/glm5next/nvidia/kda.py
10
+ # (2) MLA layers (+ MTP draft): quant_config=None -> real config (model.py:329 upstream)
11
+ COPY model.py /usr/local/lib/python3.12/dist-packages/vllm/models/glm5next/nvidia/model.py
12
+ # (3) ModelOptMixedPrecisionConfig: FP8_BLOCK128/64/32 dispatch, fused-name resolver,
13
+ # MTP draft-prefix aliases, block-FP8 ParallelLMHead loader (vocab-sharded scales)
14
+ COPY modelopt.py /usr/local/lib/python3.12/dist-packages/vllm/model_executor/layers/quantization/modelopt.py
15
+ # (4) SM120-tuned w8a8 block-FP8 triton config for the KDA fused in_proj shape
16
+ # (N=12576, K=4096, block [32,32]) — worth +60% decode on its own
17
+ COPY configs/*.json /usr/local/lib/python3.12/dist-packages/vllm/model_executor/layers/quantization/utils/configs/
serving/README.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # serving/ — the matched-pair runtime
2
+
3
+ This checkpoint stores attention, shared/dense-MLP, and `lm_head` weights as block-FP8
4
+ (`F8_E4M3` + FP32 `weight_scale_inv`). The public `glm5_next` vLLM images cannot load that:
5
+ they build every attention layer **unquantized** (the official checkpoints keep attention BF16,
6
+ so the model code strips `quant_config` at layer construction), and they have no FP8
7
+ `ParallelLMHead` loader. The result on an unpatched image is a `KeyError` on the first attention
8
+ scale tensor, or a vocab-embedding shape assert.
9
+
10
+ The fix is four files on top of `cstechdev/vllm:glm53-flash-nope-sm120-cu130-20260826-r1`
11
+ (vLLM `0.1.dev20051+g487ecf187`), applied by the Dockerfile here:
12
+
13
+ | file | target path in image | semantic change |
14
+ |---|---|---|
15
+ | `kda.py` | `vllm/models/glm5next/nvidia/kda.py` | remove the save/`None`/restore strip of `vllm_config.quant_config` around KDA base-class construction (lines 168–175 upstream) — the layer now sees the real quant config; the checkpoint's `ignore` list still keeps anything BF16 that should stay BF16 |
16
+ | `model.py` | `vllm/models/glm5next/nvidia/model.py` | `Glm5NextDecoderLayer.__init__`: MLA attention gets `quant_config=quant_config` instead of `quant_config=None` (line 329 upstream); one site covers the 11 MLA layers and the MTP draft layer. The vision-tower `quant_config=None` site is deliberately untouched (upstream warns quantizing the tower NaNs image features; this checkpoint ships the tower BF16) |
17
+ | `modelopt.py` | `vllm/model_executor/layers/quantization/modelopt.py` | `ModelOptMixedPrecisionConfig`: `FP8_BLOCK128` / `FP8_BLOCK64` / `FP8_BLOCK32` per-layer dispatch to dynamic-activation block `Fp8LinearMethod`, fused-module name resolution, MTP draft-prefix aliases, and a `ParallelLMHead` method whose `weight_scale_inv` loader shards scale rows by vocab shard / 128 (exact: 154880 and the 77440-row TP2 shard divide by 128) |
18
+ | `configs/N=12576,K=4096,…block_shape=[32,32].json` | `vllm/model_executor/layers/quantization/utils/configs/` | autotuned triton tile configs for the KDA fused in_proj GEMM on RTX PRO 6000 Blackwell (SM120) — vLLM ships zero SM120 block-FP8 configs; the default config costs 313 µs/call vs 22.3 µs tuned (43.8% of decode GPU time → ~3%) |
19
+
20
+ Safety property (measured): this patched image is **bitwise identical** to the vendor image for
21
+ BF16-attention checkpoints — the parent NVFP4 checkpoint produces 100.00% teacher-forced
22
+ agreement with |Δlogprob| = 0.00000 under it, because its `quantization_config.ignore` already
23
+ excludes every attention module. The patch only *allows* quantized attention when a checkpoint's
24
+ manifest asks for it.
25
+
26
+ All three `.py` files are Apache-2.0 vLLM-tree files (SPDX headers retained) carrying local
27
+ modifications; they are **not** MIT-licensed model weights. Credit: the vLLM project; the
28
+ official `glm5_next` per-model image as packaged by
29
+ [chriswritescode-dev/glm-5.3-flash-sm120](https://github.com/chriswritescode-dev/glm-5.3-flash-sm120)
30
+ (our base-image lineage). The base image is pulled from Docker Hub at build time and is not
31
+ redistributed in this repo.
32
+
33
+ Parallel-work credit: [local-inference-lab](https://github.com/local-inference-lab/vllm)
34
+ independently landed the equivalent MLA quant-config passthrough in their public vLLM fork
35
+ (`dev/jovian-judgement@8590bf9c`, 2026-08-29), paired with MXFP8 attention via the b12x SM120
36
+ kernels. This patch set does the same for the official per-model-image code path and adds the
37
+ block-[32,32] FP8 dispatch for the fused KDA in_proj plus the block-FP8 lm_head loader.
38
+
39
+ Numerics note for the [32,32] block: the tuned configs keep `BLOCK_SIZE_K=32`, the same k-split
40
+ as the default config, so tuned vs untuned kernels are bit-identical in reduction order.
serving/configs/N=12576,K=4096,device_name=NVIDIA_RTX_PRO_6000_Blackwell_Workstation_Edition,dtype=fp8_w8a8,block_shape=[32,32].json ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "1": {
3
+ "BLOCK_SIZE_M": 16,
4
+ "BLOCK_SIZE_N": 64,
5
+ "BLOCK_SIZE_K": 32,
6
+ "GROUP_SIZE_M": 1,
7
+ "num_warps": 4,
8
+ "num_stages": 6
9
+ },
10
+ "2": {
11
+ "BLOCK_SIZE_M": 16,
12
+ "BLOCK_SIZE_N": 128,
13
+ "BLOCK_SIZE_K": 32,
14
+ "GROUP_SIZE_M": 32,
15
+ "num_warps": 4,
16
+ "num_stages": 4
17
+ },
18
+ "4": {
19
+ "BLOCK_SIZE_M": 16,
20
+ "BLOCK_SIZE_N": 128,
21
+ "BLOCK_SIZE_K": 32,
22
+ "GROUP_SIZE_M": 32,
23
+ "num_warps": 4,
24
+ "num_stages": 4
25
+ },
26
+ "8": {
27
+ "BLOCK_SIZE_M": 16,
28
+ "BLOCK_SIZE_N": 128,
29
+ "BLOCK_SIZE_K": 32,
30
+ "GROUP_SIZE_M": 1,
31
+ "num_warps": 4,
32
+ "num_stages": 4
33
+ },
34
+ "16": {
35
+ "BLOCK_SIZE_M": 16,
36
+ "BLOCK_SIZE_N": 128,
37
+ "BLOCK_SIZE_K": 32,
38
+ "GROUP_SIZE_M": 1,
39
+ "num_warps": 4,
40
+ "num_stages": 6
41
+ },
42
+ "64": {
43
+ "BLOCK_SIZE_M": 64,
44
+ "BLOCK_SIZE_N": 64,
45
+ "BLOCK_SIZE_K": 32,
46
+ "GROUP_SIZE_M": 8,
47
+ "num_warps": 4,
48
+ "num_stages": 3
49
+ },
50
+ "256": {
51
+ "BLOCK_SIZE_M": 128,
52
+ "BLOCK_SIZE_N": 64,
53
+ "BLOCK_SIZE_K": 32,
54
+ "GROUP_SIZE_M": 32,
55
+ "num_warps": 4,
56
+ "num_stages": 2
57
+ },
58
+ "1024": {
59
+ "BLOCK_SIZE_M": 64,
60
+ "BLOCK_SIZE_N": 128,
61
+ "BLOCK_SIZE_K": 32,
62
+ "GROUP_SIZE_M": 8,
63
+ "num_warps": 4,
64
+ "num_stages": 2
65
+ },
66
+ "2048": {
67
+ "BLOCK_SIZE_M": 128,
68
+ "BLOCK_SIZE_N": 128,
69
+ "BLOCK_SIZE_K": 32,
70
+ "GROUP_SIZE_M": 8,
71
+ "num_warps": 4,
72
+ "num_stages": 2
73
+ },
74
+ "4096": {
75
+ "BLOCK_SIZE_M": 128,
76
+ "BLOCK_SIZE_N": 128,
77
+ "BLOCK_SIZE_K": 32,
78
+ "GROUP_SIZE_M": 8,
79
+ "num_warps": 4,
80
+ "num_stages": 2
81
+ },
82
+ "8192": {
83
+ "BLOCK_SIZE_M": 128,
84
+ "BLOCK_SIZE_N": 128,
85
+ "BLOCK_SIZE_K": 32,
86
+ "GROUP_SIZE_M": 1,
87
+ "num_warps": 4,
88
+ "num_stages": 2
89
+ }
90
+ }
serving/everything-lane/Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # "Everything" lane: MTP + vision + 524,288 ctx, zero offload.
2
+ # Build the base 512k image from ../ first, then layer this patch set:
3
+ # - indexer.py: bounded-memory sparse-MLA indexer scoring (identical top-k
4
+ # indices; caps the O(context) prefill transient)
5
+ # - kvq.py / kvq_kernels.py + the mla/sparse_mla/kv_cache/flashinfer files:
6
+ # compact KV rows for the sparse-MLA layers, gated by VLLM_KVQ_TILES.
7
+ # "8888" repacks the fp8_ds_mla row 656->528 B (bit-identical: the 128 B
8
+ # RoPE slot is provably inert -- qk_rope_head_dim=0); "8666" (1 fp8 +
9
+ # 3 fp6 tiles, 444 B/row) is the shipped 512k+MTP rung (TF 95.57%);
10
+ # "6666" (all-fp6, 416 B/row) trades ~1.5 TF points for a thinner pin.
11
+ # Decode/prefill re-stage top-k rows into canonical 656 B staging consumed
12
+ # by the unmodified FlashInfer SM120 cubin. Unset = stock path, no change.
13
+ # - model.py / modelopt.py: the 512k lane's files plus quant_config
14
+ # passthrough for the embedding/vision construction sites (manifest-gated:
15
+ # the shipped checkpoint's block-FP8 vision tower loads through it;
16
+ # BF16-tower checkpoints construct bit-identically to the base image)
17
+ ARG BASE=local/vllm-glm53:fp8attn-512k
18
+ FROM ${BASE}
19
+ ARG V=/usr/local/lib/python3.12/dist-packages/vllm
20
+ COPY flashinfer_mla_sparse.py flashinfer_mla_sparse_sm120.py $V/v1/attention/backends/mla/
21
+ COPY indexer.py $V/v1/attention/backends/mla/indexer.py
22
+ COPY kv_cache_interface.py $V/v1/
23
+ COPY kvq.py kvq_kernels.py $V/
24
+ COPY sparse_mla_attention.py mla_attention.py $V/model_executor/layers/attention/
25
+ COPY modelopt.py $V/model_executor/layers/quantization/modelopt.py
26
+ COPY model.py /usr/local/lib/python3.12/dist-packages/vllm/models/glm5next/nvidia/model.py
serving/everything-lane/flashinfer_mla_sparse.py ADDED
@@ -0,0 +1,553 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+ """FlashInfer sparse MLA attention backend."""
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, ClassVar
7
+
8
+ import torch
9
+
10
+ from vllm import envs
11
+ from vllm.config import VllmConfig
12
+ from vllm.config.cache import CacheDType
13
+ from vllm.logger import init_logger
14
+ from vllm.model_executor.layers.attention.mla_attention import MLACommonPrefillMetadata
15
+ from vllm.model_executor.layers.attention.sparse_mla_attention import (
16
+ SparseMLACommonImpl,
17
+ SparseMLACommonMetadataBuilder,
18
+ )
19
+ from vllm.platforms.interface import DeviceCapability
20
+ from vllm.utils.torch_utils import is_quantized_kv_cache
21
+ from vllm.v1.attention.backend import (
22
+ AttentionBackend,
23
+ AttentionCGSupport,
24
+ AttentionLayer,
25
+ AttentionMetadata,
26
+ AttentionType,
27
+ MLAAttentionImpl,
28
+ MultipleOf,
29
+ )
30
+ from vllm.v1.attention.backends.mla.sparse_utils import (
31
+ triton_convert_req_index_to_global_index,
32
+ triton_filter_and_convert_dcp_index,
33
+ )
34
+ from vllm.v1.attention.backends.utils import KVCacheLayoutType
35
+ from vllm.v1.kv_cache_interface import AttentionSpec
36
+
37
+ if TYPE_CHECKING:
38
+ from vllm.model_executor.models.deepseek_v2 import Indexer
39
+
40
+ logger = init_logger(__name__)
41
+
42
+
43
+ class _FlashInferMLASparseBackendBase(AttentionBackend):
44
+ """Common metadata for concrete FlashInfer sparse MLA backends."""
45
+
46
+ @staticmethod
47
+ def get_name() -> str:
48
+ return "FLASHINFER_MLA_SPARSE"
49
+
50
+ @staticmethod
51
+ def get_builder_cls() -> type["FlashInferMLASparseMetadataBuilder"]:
52
+ return FlashInferMLASparseMetadataBuilder
53
+
54
+ @classmethod
55
+ def get_supported_head_sizes(cls) -> list[int]:
56
+ # 576 = 512 NoPE + 64 RoPE (with-rope layout); 512 = 512 NoPE only
57
+ # (no-rope layout, qk_rope_head_dim == 0). Both share D_V = 512 and are
58
+ # served by the TRTLLM-GEN sparse MLA kernel.
59
+ return [512, 576]
60
+
61
+ @classmethod
62
+ def is_mla(cls) -> bool:
63
+ return True
64
+
65
+ @classmethod
66
+ def is_sparse(cls) -> bool:
67
+ return True
68
+
69
+
70
+ class FlashInferMLASparseTRTLLMBackend(_FlashInferMLASparseBackendBase):
71
+ """FlashInfer sparse MLA backend using the TRTLLM-gen launcher."""
72
+
73
+ supported_dtypes: ClassVar[list[torch.dtype]] = [torch.float16, torch.bfloat16]
74
+ supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
75
+ "auto",
76
+ "float16",
77
+ "bfloat16",
78
+ "fp8",
79
+ "fp8_e4m3",
80
+ ]
81
+
82
+ @staticmethod
83
+ def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
84
+ return [32, 64]
85
+
86
+ @staticmethod
87
+ def get_impl_cls() -> type[MLAAttentionImpl]:
88
+ return FlashInferMLASparseImpl
89
+
90
+ @classmethod
91
+ def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
92
+ return capability.major == 10
93
+
94
+ @classmethod
95
+ def supports_combination(
96
+ cls,
97
+ head_size: int,
98
+ dtype: torch.dtype,
99
+ kv_cache_dtype: CacheDType | None,
100
+ block_size: int | None,
101
+ use_mla: bool,
102
+ has_sink: bool,
103
+ use_sparse: bool,
104
+ use_mm_prefix: bool,
105
+ device_capability: DeviceCapability,
106
+ ) -> str | None:
107
+ from vllm.config import get_current_vllm_config
108
+
109
+ vllm_config = get_current_vllm_config()
110
+ if kv_cache_dtype == "fp8_ds_mla":
111
+ return (
112
+ "FLASHINFER_MLA_SPARSE SM10 does not support fp8_ds_mla kv-cache dtype"
113
+ )
114
+
115
+ # FlashInfer MLA sparse SM10 kernel requires qk_nope_head_dim in [128, 192].
116
+ if vllm_config.model_config is not None:
117
+ hf_text_config = vllm_config.model_config.hf_text_config
118
+ qk_nope_head_dim = getattr(hf_text_config, "qk_nope_head_dim", 1)
119
+ qk_rope_head_dim = getattr(hf_text_config, "qk_rope_head_dim", 64)
120
+ kv_lora_rank = getattr(hf_text_config, "kv_lora_rank", 512)
121
+ if qk_rope_head_dim == 0:
122
+ # Native no-rope MLA: FlashInfer only ships the one shape
123
+ # (nope_mla_dimensions in flashinfer.mla._core).
124
+ if qk_nope_head_dim != 256 or kv_lora_rank != 512:
125
+ return (
126
+ "FlashInfer native no-rope MLA requires "
127
+ "qk_nope_head_dim=256 and kv_lora_rank=512, but got "
128
+ f"qk_nope_head_dim={qk_nope_head_dim}, "
129
+ f"kv_lora_rank={kv_lora_rank}"
130
+ )
131
+ elif qk_nope_head_dim not in [128, 192]:
132
+ return (
133
+ "FlashInfer MLA Sparse kernel requires qk_nope_head_dim "
134
+ f"in [128, 192], but got {qk_nope_head_dim}"
135
+ )
136
+ # Check for index_topk which indicates sparse model
137
+ if not hasattr(hf_text_config, "index_topk"):
138
+ return "FlashInfer MLA Sparse requires model with index_topk config"
139
+ return None
140
+
141
+ @staticmethod
142
+ def get_kv_cache_shape(
143
+ num_blocks: int,
144
+ block_size: int,
145
+ num_kv_heads: int, # assumed to be 1 for MLA
146
+ head_size: int,
147
+ cache_dtype_str: str = "auto",
148
+ ) -> tuple[int, ...]:
149
+ return (num_blocks, block_size, head_size)
150
+
151
+ @classmethod
152
+ def get_required_kv_cache_layout(cls) -> "KVCacheLayoutType | None":
153
+ return "HND"
154
+
155
+
156
+ class FlashInferMLASparseSM120Backend(_FlashInferMLASparseBackendBase):
157
+ """FlashInfer sparse MLA backend for SM120."""
158
+
159
+ supported_dtypes: ClassVar[list[torch.dtype]] = [torch.bfloat16]
160
+ supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
161
+ "auto",
162
+ "fp8",
163
+ "fp8_e4m3",
164
+ "fp8_ds_mla",
165
+ ]
166
+
167
+ @staticmethod
168
+ def get_name() -> str:
169
+ return "FLASHINFER_MLA_SPARSE_SM120"
170
+
171
+ @staticmethod
172
+ def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
173
+ return [64]
174
+
175
+ @staticmethod
176
+ def get_impl_cls() -> type[MLAAttentionImpl]:
177
+ from vllm.v1.attention.backends.mla.flashinfer_mla_sparse_sm120 import (
178
+ FlashInferMLASparseSM120Impl,
179
+ )
180
+
181
+ return FlashInferMLASparseSM120Impl
182
+
183
+ @classmethod
184
+ def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
185
+ return capability.major == 12
186
+
187
+ @classmethod
188
+ def supports_combination(
189
+ cls,
190
+ head_size: int,
191
+ dtype: torch.dtype,
192
+ kv_cache_dtype: CacheDType | None,
193
+ block_size: int | None,
194
+ use_mla: bool,
195
+ has_sink: bool,
196
+ use_sparse: bool,
197
+ use_mm_prefix: bool,
198
+ device_capability: DeviceCapability,
199
+ ) -> str | None:
200
+ from vllm.config import get_current_vllm_config
201
+ from vllm.utils.flashinfer import has_flashinfer_sparse_mla_sm120
202
+
203
+ if not has_flashinfer_sparse_mla_sm120():
204
+ return (
205
+ "FLASHINFER_MLA_SPARSE_SM120 requires FlashInfer's "
206
+ "sparse MLA decode API"
207
+ )
208
+ if dtype != torch.bfloat16:
209
+ return "dtype not supported"
210
+ if kv_cache_dtype not in (
211
+ None,
212
+ "auto",
213
+ "fp8",
214
+ "fp8_e4m3",
215
+ "fp8_ds_mla",
216
+ ):
217
+ return "kv_cache_dtype not supported"
218
+ vllm_config = get_current_vllm_config()
219
+ if vllm_config.model_config is not None:
220
+ hf_text_config = vllm_config.model_config.hf_text_config
221
+ index_topk = getattr(hf_text_config, "index_topk", None)
222
+ if index_topk is None:
223
+ return (
224
+ "FLASHINFER_MLA_SPARSE_SM120 requires a model with "
225
+ "index_topk config"
226
+ )
227
+ if int(index_topk) != 2048:
228
+ return (
229
+ "FLASHINFER_MLA_SPARSE_SM120 requires index_topk=2048; "
230
+ f"got {index_topk}"
231
+ )
232
+ return None
233
+
234
+ @staticmethod
235
+ def get_kv_cache_shape(
236
+ num_blocks: int,
237
+ block_size: int,
238
+ num_kv_heads: int, # assumed to be 1 for MLA
239
+ head_size: int,
240
+ cache_dtype_str: str = "auto",
241
+ ) -> tuple[int, ...]:
242
+ if cache_dtype_str in ("auto", "fp8", "fp8_e4m3", "fp8_ds_mla"):
243
+ # fp8_ds_mla packed layout: 512 NoPE + 16 scales + 128 RoPE.
244
+ # KVQ (glm-max): compact row layout when VLLM_KVQ_TILES is set
245
+ # (kvq.row_bytes() returns 656 when unset).
246
+ from vllm import kvq
247
+
248
+ return (num_blocks, block_size, kvq.row_bytes())
249
+ return (num_blocks, block_size, head_size)
250
+
251
+ @classmethod
252
+ def get_required_kv_cache_layout(cls) -> "KVCacheLayoutType | None":
253
+ return None
254
+
255
+
256
+ @dataclass
257
+ class FlashInferMLASparseMetadata(AttentionMetadata):
258
+ """Attention metadata for FlashInfer MLA Sparse backend."""
259
+
260
+ num_reqs: int
261
+ max_query_len: int
262
+ max_seq_len: int
263
+ num_actual_tokens: int
264
+
265
+ # Query start locations
266
+ query_start_loc: torch.Tensor
267
+ slot_mapping: torch.Tensor
268
+ block_table: torch.Tensor
269
+ req_id_per_token: torch.Tensor
270
+
271
+ # Sequence lengths for all requests (context + query)
272
+ seq_lens: torch.Tensor
273
+ num_decodes: int
274
+ num_prefills: int
275
+ num_decode_tokens: int
276
+ prefill_max_seq_len: int = 0
277
+ prefill: MLACommonPrefillMetadata | None = None
278
+
279
+ # Sparse-specific
280
+ block_size: int = 64
281
+ topk_tokens: int = 2048
282
+ cp_kv_cache_interleave_size: int = 1
283
+
284
+
285
+ class FlashInferMLASparseMetadataBuilder(
286
+ SparseMLACommonMetadataBuilder[FlashInferMLASparseMetadata]
287
+ ):
288
+ """Builder for FlashInfer MLA Sparse attention metadata."""
289
+
290
+ metadata_cls = FlashInferMLASparseMetadata
291
+ _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.UNIFORM_BATCH
292
+
293
+ def __init__(
294
+ self,
295
+ kv_cache_spec: AttentionSpec,
296
+ layer_names: list[str],
297
+ vllm_config: VllmConfig,
298
+ device: torch.device,
299
+ ) -> None:
300
+ super().__init__(kv_cache_spec, layer_names, vllm_config, device)
301
+
302
+ num_q_heads = vllm_config.model_config.get_num_attention_heads(
303
+ vllm_config.parallel_config
304
+ )
305
+ threshold = {8: 128, 16: 128, 32: 128, 64: 256, 128: 1024}.get(
306
+ num_q_heads, 1024
307
+ )
308
+ # KVQ (glm-max): the compact-row decode path re-stages the top-k rows
309
+ # per decode token into a bounded staging cache, so cap the number of
310
+ # query tokens the decode (forward_mqa) path can see per request;
311
+ # longer continuation chunks route through the prefill path instead.
312
+ from vllm import kvq
313
+
314
+ if kvq.enabled():
315
+ threshold = min(threshold, kvq.decode_qlen())
316
+ self._init_reorder_batch_threshold(
317
+ threshold,
318
+ supports_spec_as_decode=True,
319
+ supports_dcp_with_varlen=True,
320
+ )
321
+
322
+
323
+ # Global workspace buffer (lazily initialized)
324
+ _fi_sparse_workspace: torch.Tensor | None = None
325
+
326
+
327
+ def _get_workspace_buffer(device: torch.device) -> torch.Tensor:
328
+ global _fi_sparse_workspace
329
+ if _fi_sparse_workspace is None:
330
+ # FlashInfer's CuteDSL MLA-decode tactic requires an int8 workspace;
331
+ # the trtllm-gen path views it as uint8, so int8 is safe for all backends.
332
+ _fi_sparse_workspace = torch.zeros(
333
+ envs.VLLM_FLASHINFER_WORKSPACE_BUFFER_SIZE,
334
+ dtype=torch.int8,
335
+ device=device,
336
+ )
337
+ return _fi_sparse_workspace
338
+
339
+
340
+ class FlashInferMLASparseImpl(SparseMLACommonImpl[FlashInferMLASparseMetadata]):
341
+ """FlashInfer MLA Sparse implementation.
342
+
343
+ Uses the TRT-LLM MLA kernel with sparse_mla_top_k parameter for
344
+ sparse attention computation.
345
+ """
346
+
347
+ can_return_lse_for_decode: bool = True
348
+ lse_base_on_e: bool = False
349
+
350
+ def __init__(
351
+ self,
352
+ num_heads: int,
353
+ head_size: int,
354
+ scale: float,
355
+ num_kv_heads: int,
356
+ alibi_slopes: list[float] | None,
357
+ sliding_window: int | None,
358
+ kv_cache_dtype: str,
359
+ logits_soft_cap: float | None,
360
+ attn_type: str,
361
+ kv_sharing_target_layer_name: str | None,
362
+ # MLA Specific Arguments
363
+ topk_indices_buffer: torch.Tensor | None = None,
364
+ indexer: "Indexer | None" = None,
365
+ **mla_args,
366
+ ) -> None:
367
+ unsupported_features = [alibi_slopes, sliding_window, logits_soft_cap]
368
+ if any(unsupported_features):
369
+ raise NotImplementedError(
370
+ "FlashInferMLASparseImpl does not support one of the following: "
371
+ "alibi_slopes, sliding_window, logits_soft_cap"
372
+ )
373
+
374
+ if attn_type != AttentionType.DECODER:
375
+ raise NotImplementedError(
376
+ "Encoder self-attention and "
377
+ "encoder/decoder cross-attention "
378
+ "are not implemented for "
379
+ "FlashInferMLASparseImpl"
380
+ )
381
+
382
+ super().__init__(
383
+ num_heads,
384
+ head_size,
385
+ scale,
386
+ num_kv_heads,
387
+ alibi_slopes,
388
+ sliding_window,
389
+ kv_cache_dtype,
390
+ logits_soft_cap,
391
+ attn_type,
392
+ kv_sharing_target_layer_name,
393
+ indexer=indexer,
394
+ topk_indices_buffer=topk_indices_buffer,
395
+ **mla_args,
396
+ )
397
+
398
+ self._workspace_buffer: torch.Tensor | None = None
399
+ self.bmm1_scale: float | None = None
400
+ self.bmm2_scale: float | None = None
401
+
402
+ # Native no-rope MLA additionally requires a per-query-token active
403
+ # top-k length tensor.
404
+ self.is_nope_mla = self.qk_rope_head_dim == 0
405
+
406
+ # fp8 query quantization is required when using fp8 kv_cache,
407
+ # as the TRTLLM-GEN sparse MLA kernel requires matching dtypes
408
+ # for query and kv_cache (mixed bf16+fp8 is not supported).
409
+ self.supports_quant_query_input = True
410
+
411
+ def forward_mqa(
412
+ self,
413
+ q: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
414
+ kv_c_and_k_pe_cache: torch.Tensor,
415
+ attn_metadata: FlashInferMLASparseMetadata,
416
+ layer: AttentionLayer,
417
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
418
+ if isinstance(q, tuple):
419
+ q = torch.cat(q, dim=-1)
420
+
421
+ num_actual_toks = q.shape[0]
422
+
423
+ assert self.topk_indices_buffer is not None
424
+ topk_indices = self.topk_indices_buffer[:num_actual_toks]
425
+
426
+ if self.dcp_world_size > 1:
427
+ topk_indices_physical, seq_lens = triton_filter_and_convert_dcp_index(
428
+ attn_metadata.req_id_per_token[:num_actual_toks],
429
+ attn_metadata.block_table,
430
+ topk_indices,
431
+ dcp_size=self.dcp_world_size,
432
+ dcp_rank=self.dcp_rank,
433
+ cp_kv_cache_interleave_size=(attn_metadata.cp_kv_cache_interleave_size),
434
+ BLOCK_SIZE=attn_metadata.block_size,
435
+ NUM_TOPK_TOKENS=topk_indices.shape[1],
436
+ return_valid_counts=True,
437
+ )
438
+ else:
439
+ topk_indices_physical, seq_lens = triton_convert_req_index_to_global_index(
440
+ attn_metadata.req_id_per_token[:num_actual_toks],
441
+ attn_metadata.block_table,
442
+ topk_indices,
443
+ BLOCK_SIZE=attn_metadata.block_size,
444
+ NUM_TOPK_TOKENS=topk_indices.shape[1],
445
+ return_valid_counts=True,
446
+ )
447
+
448
+ if self._workspace_buffer is None:
449
+ self._workspace_buffer = _get_workspace_buffer(q.device)
450
+
451
+ if self.bmm1_scale is None:
452
+ self.bmm1_scale = self.scale
453
+ if is_quantized_kv_cache(self.kv_cache_dtype):
454
+ self.bmm1_scale *= layer._q_scale_float * layer._k_scale_float
455
+ if self.bmm2_scale is None:
456
+ self.bmm2_scale = 1.0
457
+ if is_quantized_kv_cache(self.kv_cache_dtype):
458
+ self.bmm2_scale *= layer._k_scale_float
459
+
460
+ from flashinfer.decode import trtllm_batch_decode_with_kv_cache_mla
461
+
462
+ # Single-token sparse decode. trtllm-gen requires the q_len_per_request
463
+ # dim, but the sparse attention mask is fully per-token (each query token
464
+ # carries its own top-k index row), so unsqueeze is sufficient and
465
+ # correct. The MTP/multi-token q_len grouping is a perf-only layout and is
466
+ # deferred until MTP is validated end-to-end for this backend.
467
+ query = q.unsqueeze(1)
468
+ block_tables = topk_indices_physical.unsqueeze(1)
469
+ seq_lens_arg = seq_lens
470
+
471
+ # page_table width = topk buffer width, which kpool widens past
472
+ # index_topk (topk_tokens) and rounds up to a multiple of 128. The
473
+ # kernel treats sparse_mla_top_k as the page-table *capacity* and bounds
474
+ # the active per-query length by ``seq_lens`` (the compacted valid
475
+ # count), so the -1 padding slots past seq_lens are never attended to.
476
+ # Use the actual buffer width instead of the fixed topk_tokens, which
477
+ # mismatches the page_table when index_kpool > 1.
478
+ sparse_topk_capacity = topk_indices_physical.shape[1]
479
+
480
+ extra_kwargs: dict[str, torch.Tensor] = {}
481
+ empty_rows: torch.Tensor | None = None
482
+ if self.is_nope_mla:
483
+ # The native no-rope kernel takes the active top-k length per query
484
+ # token (``seq_lens`` here is already the compacted per-token valid
485
+ # count, int32) and rejects zero-length rows. Point empty rows at a
486
+ # single valid dummy slot with length 1 and zero their output after
487
+ # the launch. ``triton_convert_req_index_to_global_index`` packs the
488
+ # valid indices into a contiguous prefix, which is what the kernel
489
+ # requires of the page table.
490
+ empty_rows = seq_lens == 0
491
+ topk_indices_physical[:, 0] = topk_indices_physical[:, 0].masked_fill(
492
+ empty_rows, 0
493
+ )
494
+ extra_kwargs["sparse_mla_top_k_lens"] = seq_lens.clamp(min=1)
495
+
496
+ kernel_out = trtllm_batch_decode_with_kv_cache_mla(
497
+ query=query,
498
+ kv_cache=kv_c_and_k_pe_cache.unsqueeze(1),
499
+ workspace_buffer=self._workspace_buffer,
500
+ qk_nope_head_dim=self.qk_nope_head_dim,
501
+ kv_lora_rank=self.kv_lora_rank,
502
+ qk_rope_head_dim=self.qk_rope_head_dim,
503
+ block_tables=block_tables,
504
+ seq_lens=seq_lens_arg,
505
+ max_seq_len=sparse_topk_capacity,
506
+ bmm1_scale=self.bmm1_scale,
507
+ bmm2_scale=self.bmm2_scale,
508
+ sparse_mla_top_k=sparse_topk_capacity,
509
+ return_lse=self.need_to_return_lse_for_decode,
510
+ **extra_kwargs,
511
+ )
512
+ if self.need_to_return_lse_for_decode:
513
+ assert isinstance(kernel_out, tuple)
514
+ o, lse = kernel_out
515
+ else:
516
+ assert isinstance(kernel_out, torch.Tensor)
517
+ o = kernel_out
518
+ lse = None
519
+
520
+ out = o.view(-1, o.shape[-2], o.shape[-1])
521
+ if lse is not None:
522
+ lse = self._normalize_lse(lse, out.shape[0], out.shape[1])
523
+ if empty_rows is None and lse is not None:
524
+ empty_rows = (topk_indices_physical == -1).all(dim=-1)
525
+ if empty_rows is not None:
526
+ out.masked_fill_(empty_rows.view(-1, 1, 1), 0.0)
527
+ if lse is not None:
528
+ lse.masked_fill_(empty_rows.view(-1, 1), float("-inf"))
529
+ return out, lse
530
+
531
+ @staticmethod
532
+ def _normalize_lse(
533
+ lse: torch.Tensor,
534
+ num_tokens: int,
535
+ num_heads: int,
536
+ ) -> torch.Tensor:
537
+ # FlashInfer returns the decode LSE either as 2D (num_tokens, num_heads)
538
+ # or 3D ((num_tokens, num_heads, 1) / (num_tokens, 1, num_heads)).
539
+ # Collapse all of these to the (num_tokens, num_heads) the shared DCP
540
+ # reducer expects.
541
+ if lse.dim() == 3:
542
+ if lse.shape[-1] == 1:
543
+ lse = lse.squeeze(-1)
544
+ elif lse.shape[1] == 1:
545
+ lse = lse.squeeze(1)
546
+ elif lse.shape[0] * lse.shape[1] == num_tokens:
547
+ lse = lse.reshape(num_tokens, lse.shape[-1])
548
+ if lse.shape != (num_tokens, num_heads):
549
+ raise RuntimeError(
550
+ "Unexpected FlashInfer sparse MLA LSE shape: "
551
+ f"{tuple(lse.shape)}, expected ({num_tokens}, {num_heads})."
552
+ )
553
+ return lse
serving/everything-lane/flashinfer_mla_sparse_sm120.py ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+ """SM120 implementation variant for ``FLASHINFER_MLA_SPARSE``.
4
+
5
+ KVQ (glm-max) patch on top of fp8attn-r5: when VLLM_KVQ_TILES is set, the MLA
6
+ KV cache holds compact rows (see vllm/kvq.py) instead of 656-byte fp8_ds_mla
7
+ rows. The write path quantises via a Triton kernel, and the decode path
8
+ re-stages each token's top-k rows into a small canonical 656-byte staging
9
+ cache that the (unmodified) FlashInfer SM120 sparse-MLA kernel consumes with
10
+ an identity page table. With VLLM_KVQ_TILES unset this file behaves exactly
11
+ like the r5 original.
12
+ """
13
+
14
+ from typing import TYPE_CHECKING, cast
15
+
16
+ import torch
17
+
18
+ from vllm.v1.attention.backend import (
19
+ AttentionLayer,
20
+ AttentionType,
21
+ MLAAttentionImpl,
22
+ )
23
+ from vllm.v1.attention.backends.mla.flashinfer_mla_sparse import (
24
+ FlashInferMLASparseMetadata,
25
+ _get_workspace_buffer,
26
+ )
27
+ from vllm.v1.attention.backends.mla.sparse_utils import (
28
+ triton_convert_req_index_to_global_index,
29
+ )
30
+
31
+ if TYPE_CHECKING:
32
+ from vllm.model_executor.models.deepseek_v2 import Indexer
33
+
34
+
35
+ def _kv_scale_format_for_model(model_type: str | None) -> str:
36
+ if model_type is not None and model_type.startswith("glm"):
37
+ return "arbitrary_fp32"
38
+ return "pow2_fp32"
39
+
40
+
41
+ # --- KVQ (glm-max): shared compact-row staging ------------------------------
42
+ # On this backend prefill_backend is None (supports_dense_mha_prefill=False),
43
+ # so forward_mqa sees EVERY scheduled token — prefill chunks up to
44
+ # max_num_batched_tokens included, not just decode tokens. Rather than size
45
+ # the staging cache for the whole batch (num_tokens * topk * 656B is GBs), we
46
+ # process tokens in chunks of 64 — the SM120 decode cubin's num_tokens cap —
47
+ # so every call takes the proven decode dispatch and staging stays bounded at
48
+ # 64 * topk * 656B (86 MiB for topk=2048). Per-token top-k attention has no
49
+ # cross-token coupling, so chunking is exact. Layers execute sequentially
50
+ # and every chunk fully re-stages the rows it reads, so one buffer is shared
51
+ # across all impl instances (~12 layers would otherwise cost ~1 GiB).
52
+ _KVQ_CHUNK = 64
53
+
54
+ _kvq_staging: torch.Tensor | None = None
55
+ _kvq_arange: torch.Tensor | None = None
56
+
57
+
58
+ def _kvq_shared_staging(
59
+ capacity: int, device: torch.device
60
+ ) -> tuple[torch.Tensor, torch.Tensor]:
61
+ global _kvq_staging, _kvq_arange
62
+ rows = _KVQ_CHUNK * capacity
63
+ if (
64
+ _kvq_staging is None
65
+ or _kvq_staging.shape[0] != rows
66
+ or _kvq_staging.device != device
67
+ ):
68
+ # Canonical 656-byte staging rows; the rope region [528:656) is never
69
+ # written and stays zero (q's rope half is exact zeros, so any finite
70
+ # bytes there are inert; zeros also guard against NaN).
71
+ _kvq_staging = torch.zeros((rows, 656), dtype=torch.uint8, device=device)
72
+ _kvq_arange = (
73
+ torch.arange(rows, dtype=torch.int32, device=device)
74
+ .view(_KVQ_CHUNK, capacity)
75
+ .contiguous()
76
+ )
77
+ assert _kvq_arange is not None
78
+ return _kvq_staging, _kvq_arange
79
+
80
+
81
+ class FlashInferMLASparseSM120Impl(MLAAttentionImpl[FlashInferMLASparseMetadata]):
82
+ """SM120 FlashInfer sparse-MLA implementation."""
83
+
84
+ is_sparse = True
85
+ supports_dense_mha_prefill = False
86
+
87
+ def __init__(
88
+ self,
89
+ num_heads: int,
90
+ head_size: int,
91
+ scale: float,
92
+ num_kv_heads: int,
93
+ alibi_slopes: list[float] | None,
94
+ sliding_window: int | None,
95
+ kv_cache_dtype: str,
96
+ logits_soft_cap: float | None,
97
+ attn_type: str,
98
+ kv_sharing_target_layer_name: str | None,
99
+ indexer: "Indexer | None" = None,
100
+ **mla_args,
101
+ ) -> None:
102
+ if any([alibi_slopes, sliding_window, logits_soft_cap]):
103
+ raise NotImplementedError(
104
+ "FLASHINFER_MLA_SPARSE_SM120 does not support alibi_slopes / "
105
+ "sliding_window / logits_soft_cap"
106
+ )
107
+ if attn_type != AttentionType.DECODER:
108
+ raise NotImplementedError(
109
+ "FLASHINFER_MLA_SPARSE_SM120 only supports decoder self-attention"
110
+ )
111
+
112
+ self.num_heads = num_heads
113
+ self.head_size = head_size
114
+ self.scale = float(scale)
115
+ self.num_kv_heads = num_kv_heads
116
+ self.kv_cache_dtype = kv_cache_dtype
117
+ if self.kv_cache_dtype != "fp8_ds_mla":
118
+ raise NotImplementedError(
119
+ "FLASHINFER_MLA_SPARSE_SM120 requires the packed fp8_ds_mla "
120
+ f"KV cache layout; got kv_cache_dtype={kv_cache_dtype!r}."
121
+ )
122
+
123
+ self.kv_lora_rank: int = mla_args["kv_lora_rank"]
124
+ self.qk_nope_head_dim: int = mla_args["qk_nope_head_dim"]
125
+ self.qk_rope_head_dim: int = mla_args["qk_rope_head_dim"]
126
+ self.rope_pad = 0
127
+ if self.qk_rope_head_dim == 0:
128
+ if self.kv_lora_rank != 512:
129
+ raise NotImplementedError(
130
+ "FLASHINFER_MLA_SPARSE_SM120 pads NoPE MLA into the "
131
+ "576-wide GLM_NSA geometry, which requires "
132
+ f"kv_lora_rank=512; got {self.kv_lora_rank}."
133
+ )
134
+ self.rope_pad = 64
135
+ self.kernel_qk_rope_head_dim = self.qk_rope_head_dim + self.rope_pad
136
+ from vllm.config import get_current_vllm_config
137
+
138
+ vllm_config = get_current_vllm_config()
139
+ model_type = None
140
+ if vllm_config.model_config is not None:
141
+ model_type = getattr(
142
+ vllm_config.model_config.hf_text_config, "model_type", None
143
+ )
144
+ self.kv_scale_format = _kv_scale_format_for_model(model_type)
145
+
146
+ # Skip-topk layers are built with indexer=None and get the shared
147
+ # buffer via mla_args instead (cf. FLASHMLA_SPARSE).
148
+ self.topk_indices_buffer: torch.Tensor | None = (
149
+ indexer.topk_indices_buffer
150
+ if indexer is not None
151
+ else mla_args.get("topk_indices_buffer")
152
+ )
153
+ from vllm.utils.flashinfer import has_flashinfer_sparse_mla_sm120
154
+
155
+ if not has_flashinfer_sparse_mla_sm120():
156
+ raise RuntimeError(
157
+ "FLASHINFER_MLA_SPARSE_SM120 requires FlashInfer's "
158
+ "sparse MLA decode API."
159
+ )
160
+ assert self.topk_indices_buffer is not None
161
+
162
+ self.supports_quant_query_input = False
163
+ self._workspace_buffer: torch.Tensor | None = None
164
+
165
+ # --- KVQ (glm-max): compact-row cache support -----------------------
166
+ from vllm import kvq
167
+
168
+ self._kvq_enabled = kvq.enabled()
169
+ if self._kvq_enabled:
170
+ if self.rope_pad == 0:
171
+ raise NotImplementedError(
172
+ "KVQ compact rows are only supported for the NoPE MLA "
173
+ "geometry (qk_rope_head_dim == 0): the dropped rope slot "
174
+ "must be provably inert."
175
+ )
176
+ # Allocate the shared staging buffer eagerly so its memory is
177
+ # claimed before KV-cache profiling.
178
+ _kvq_shared_staging(
179
+ self.topk_indices_buffer.shape[1],
180
+ self.topk_indices_buffer.device,
181
+ )
182
+
183
+ def forward_mqa(
184
+ self,
185
+ q: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
186
+ kv_c_and_k_pe_cache: torch.Tensor,
187
+ attn_metadata: FlashInferMLASparseMetadata,
188
+ layer: AttentionLayer,
189
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
190
+ if isinstance(q, tuple):
191
+ q = torch.cat(q, dim=-1)
192
+ if self.rope_pad:
193
+ q = torch.nn.functional.pad(q, (0, self.rope_pad))
194
+
195
+ num_actual_toks = q.shape[0]
196
+
197
+ assert self.topk_indices_buffer is not None
198
+ topk_indices = self.topk_indices_buffer[:num_actual_toks]
199
+
200
+ topk_indices_physical, topk_lengths = cast(
201
+ tuple[torch.Tensor, torch.Tensor],
202
+ triton_convert_req_index_to_global_index(
203
+ attn_metadata.req_id_per_token[:num_actual_toks],
204
+ attn_metadata.block_table,
205
+ topk_indices,
206
+ BLOCK_SIZE=attn_metadata.block_size,
207
+ NUM_TOPK_TOKENS=topk_indices.shape[1],
208
+ return_valid_counts=True,
209
+ ),
210
+ )
211
+ sparse_topk_capacity = topk_indices_physical.shape[1]
212
+ empty_rows = topk_lengths == 0
213
+
214
+ output = q.new_empty(
215
+ (num_actual_toks, self.num_heads, self.kv_lora_rank),
216
+ dtype=q.dtype,
217
+ )
218
+
219
+ if self._workspace_buffer is None:
220
+ self._workspace_buffer = _get_workspace_buffer(q.device)
221
+
222
+ from vllm.utils.flashinfer import (
223
+ flashinfer_trtllm_batch_decode_with_kv_cache_mla,
224
+ )
225
+
226
+ if self._kvq_enabled:
227
+ # Re-stage each token's top-k compact rows into the canonical
228
+ # 656-byte staging cache and consume it with an identity page
229
+ # table, in chunks of <= 64 tokens so every call takes the SM120
230
+ # decode cubin (its num_tokens cap) against the bounded shared
231
+ # staging buffer. Per-token top-k attention has no cross-token
232
+ # coupling, so chunking is exact.
233
+ #
234
+ # Layout contract (flashinfer/mla/_sparse_mla_sm120.py):
235
+ # _packed_kv_page_block_size reads page_block_size from shape[1]
236
+ # of a 3-D cache, and _decode_dsv3_2_dispatchable requires
237
+ # page_block_size == 64 — anything else falls through to the
238
+ # prefill orchestrator, which asserts num_tokens > 64. Indices
239
+ # are global token slot ids (page = slot // 64, offset =
240
+ # slot % 64), so the flat identity arange remains valid under the
241
+ # (num_pages, 64, 656) view: slot i addresses byte offset i*656.
242
+ from vllm import kvq_kernels
243
+
244
+ staging, arange = _kvq_shared_staging(
245
+ sparse_topk_capacity, topk_indices_physical.device
246
+ )
247
+ kernel_kv_cache = staging.view(-1, 64, 656)
248
+ raw_lengths = topk_lengths
249
+ topk_lengths = topk_lengths.clamp(min=1)
250
+ for i0 in range(0, num_actual_toks, _KVQ_CHUNK):
251
+ i1 = min(i0 + _KVQ_CHUNK, num_actual_toks)
252
+ kvq_kernels.stage_fp8(
253
+ kv_c_and_k_pe_cache,
254
+ topk_indices_physical[i0:i1],
255
+ raw_lengths[i0:i1],
256
+ staging,
257
+ )
258
+ flashinfer_trtllm_batch_decode_with_kv_cache_mla(
259
+ query=q[i0:i1].unsqueeze(1),
260
+ kv_cache=kernel_kv_cache,
261
+ workspace_buffer=self._workspace_buffer,
262
+ qk_nope_head_dim=self.qk_nope_head_dim,
263
+ kv_lora_rank=self.kv_lora_rank,
264
+ qk_rope_head_dim=self.kernel_qk_rope_head_dim,
265
+ block_tables=arange[: i1 - i0].unsqueeze(1),
266
+ seq_lens=topk_lengths[i0:i1],
267
+ max_seq_len=sparse_topk_capacity,
268
+ out=output[i0:i1].unsqueeze(1),
269
+ bmm1_scale=self.scale,
270
+ bmm2_scale=1.0,
271
+ sparse_mla_top_k=sparse_topk_capacity,
272
+ kv_scale_format=self.kv_scale_format,
273
+ )
274
+ output.masked_fill_(empty_rows.view(-1, 1, 1), 0.0)
275
+ return output, None
276
+
277
+ kernel_kv_cache = kv_c_and_k_pe_cache.view(torch.uint8).unsqueeze(1)
278
+ topk_indices_physical[:, 0] = topk_indices_physical[:, 0].masked_fill(
279
+ empty_rows, 0
280
+ )
281
+ kernel_block_tables = topk_indices_physical.unsqueeze(1)
282
+
283
+ topk_lengths = topk_lengths.clamp(min=1)
284
+
285
+ out = flashinfer_trtllm_batch_decode_with_kv_cache_mla(
286
+ query=q.unsqueeze(1),
287
+ kv_cache=kernel_kv_cache,
288
+ workspace_buffer=self._workspace_buffer,
289
+ qk_nope_head_dim=self.qk_nope_head_dim,
290
+ kv_lora_rank=self.kv_lora_rank,
291
+ qk_rope_head_dim=self.kernel_qk_rope_head_dim,
292
+ block_tables=kernel_block_tables,
293
+ seq_lens=topk_lengths,
294
+ max_seq_len=sparse_topk_capacity,
295
+ out=output.unsqueeze(1),
296
+ bmm1_scale=self.scale,
297
+ bmm2_scale=1.0,
298
+ sparse_mla_top_k=sparse_topk_capacity,
299
+ kv_scale_format=self.kv_scale_format,
300
+ )
301
+ out = out.squeeze(1)
302
+ out.masked_fill_(empty_rows.view(-1, 1, 1), 0.0)
303
+ return out, None
304
+
305
+ def do_kv_cache_update(
306
+ self,
307
+ kv_c_normed: torch.Tensor,
308
+ k_pe: torch.Tensor,
309
+ kv_cache: torch.Tensor,
310
+ slot_mapping: torch.Tensor,
311
+ kv_cache_dtype: str,
312
+ k_scale: torch.Tensor,
313
+ ) -> None:
314
+ if self._kvq_enabled:
315
+ if kv_cache.numel() == 0:
316
+ return
317
+ from vllm import kvq_kernels
318
+
319
+ kvq_kernels.quant_write(
320
+ kv_c_normed, kv_cache.view(torch.uint8), slot_mapping.flatten()
321
+ )
322
+ return
323
+ if self.rope_pad:
324
+ k_pe = k_pe.new_zeros((k_pe.shape[0], 1, self.rope_pad))
325
+ super().do_kv_cache_update(
326
+ kv_c_normed, k_pe, kv_cache, slot_mapping, kv_cache_dtype, k_scale
327
+ )
serving/everything-lane/indexer.py ADDED
@@ -0,0 +1,1471 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+ import os
4
+ from dataclasses import dataclass
5
+
6
+ import torch
7
+
8
+ import vllm.envs as envs
9
+ from vllm.config import VllmConfig
10
+ from vllm.distributed import get_dcp_group, get_pcp_group
11
+ from vllm.logger import init_logger
12
+ from vllm.model_executor.warmup.jit_warmup import (
13
+ VllmJitKernel,
14
+ WarmupIntRange,
15
+ )
16
+ from vllm.model_executor.warmup.jit_warmup_triton_helper import (
17
+ TritonPointerInputVariant,
18
+ TritonWarmupTensor,
19
+ )
20
+ from vllm.platforms import current_platform
21
+ from vllm.triton_utils import tl, triton
22
+ from vllm.utils.deep_gemm import (
23
+ get_paged_mqa_logits_metadata,
24
+ has_deep_gemm,
25
+ )
26
+ from vllm.utils.platform_utils import num_compute_units
27
+ from vllm.v1.attention.backend import (
28
+ AttentionBackend,
29
+ AttentionCGSupport,
30
+ AttentionMetadataBuilder,
31
+ CommonAttentionMetadata,
32
+ MultipleOf,
33
+ )
34
+ from vllm.v1.attention.backends.mla.compressor_utils import get_compressed_slot_mapping
35
+ from vllm.v1.attention.backends.utils import (
36
+ get_dcp_local_seq_lens,
37
+ split_decodes_and_prefills,
38
+ )
39
+ from vllm.v1.kv_cache_interface import (
40
+ AttentionSpec,
41
+ KVCacheSpec,
42
+ MLAAttentionSpec,
43
+ )
44
+
45
+ logger = init_logger(__name__)
46
+
47
+
48
+ @triton.jit
49
+ def _prepare_uniform_decode_kernel(
50
+ seq_lens_ptr,
51
+ decode_seq_lens_ptr,
52
+ block_table_ptr,
53
+ block_table_stride,
54
+ expanded_block_table_ptr,
55
+ expanded_bt_stride,
56
+ decode_lens_ptr,
57
+ max_decode_len,
58
+ BLOCK_SIZE: tl.constexpr,
59
+ ):
60
+ idx = tl.program_id(0)
61
+ req_id = idx // max_decode_len
62
+ local_idx = idx % max_decode_len
63
+
64
+ # Compute number of KVs attended to by this token.
65
+ seq_len = tl.load(seq_lens_ptr + req_id)
66
+ per_token_seq_len = seq_len - max_decode_len + local_idx + 1
67
+ tl.store(decode_seq_lens_ptr + idx, per_token_seq_len)
68
+
69
+ # Copy block table row.
70
+ src = block_table_ptr + req_id * block_table_stride
71
+ dst = expanded_block_table_ptr + idx * expanded_bt_stride
72
+ for i in tl.range(0, expanded_bt_stride, BLOCK_SIZE):
73
+ off = i + tl.arange(0, BLOCK_SIZE)
74
+ mask = off < expanded_bt_stride
75
+ src_block = tl.load(src + off, mask=mask)
76
+ tl.store(dst + off, src_block, mask=mask)
77
+
78
+ # All reqs now have decode_len = 1.
79
+ tl.store(decode_lens_ptr + idx, 1)
80
+
81
+
82
+ def split_indexer_prefill_chunks(
83
+ seq_lens_cpu: torch.Tensor,
84
+ query_lens_cpu: torch.Tensor,
85
+ workspace_size: int,
86
+ max_logits_bytes: int,
87
+ request_offset: int = 0,
88
+ ) -> list[tuple[slice, slice]]:
89
+ """
90
+ Split prefill requests into chunks for the sparse indexer, respecting:
91
+ - N constraint: total_seq_lens <= workspace_size (existing O(N) workspace)
92
+ - Logits constraint: M * N * 4 <= max_logits_bytes
93
+
94
+ When a single request-level chunk still exceeds the logits budget,
95
+ sub-chunks on the query dimension (M) to bound peak memory.
96
+
97
+ Returns list of (req_slice, query_slice) tuples.
98
+ """
99
+ chunks: list[tuple[slice, slice]] = []
100
+ n = len(seq_lens_cpu)
101
+ max_logits_elems = max_logits_bytes // 4
102
+ end = 0
103
+
104
+ while end < n:
105
+ start, chunk_m, chunk_n = end, 0, 0
106
+
107
+ while end < n:
108
+ q, s = query_lens_cpu[end].item(), seq_lens_cpu[end].item()
109
+ new_m, new_n = chunk_m + q, chunk_n + s
110
+ if new_n <= workspace_size and new_m * new_n <= max_logits_elems:
111
+ chunk_m, chunk_n = new_m, new_n
112
+ end += 1
113
+ else:
114
+ break
115
+
116
+ # A single request can exceed the budget, requiring sub-chunking
117
+ # on the query dimension.
118
+ if end == start:
119
+ chunk_m, chunk_n = query_lens_cpu[end].item(), seq_lens_cpu[end].item()
120
+ end += 1
121
+
122
+ req_slice = slice(start + request_offset, end + request_offset)
123
+ max_q = max(1, max_logits_elems // chunk_n) if chunk_n > 0 else max(1, chunk_m)
124
+ for q_off in range(0, chunk_m, max_q):
125
+ sub_m = min(max_q, chunk_m - q_off)
126
+ chunks.append((req_slice, slice(q_off, q_off + sub_m)))
127
+
128
+ return chunks
129
+
130
+
131
+ class DeepseekV32IndexerBackend(AttentionBackend):
132
+ @classmethod
133
+ def supports_pcp(cls) -> bool:
134
+ return True
135
+
136
+ @classmethod
137
+ def supports_device_cpu_query_lens_mismatch(cls) -> bool:
138
+ # Only the varlen paged MQA logits kernel takes per-request query
139
+ # lengths from device tensors; otherwise the indexer needs uniform ones.
140
+ return _supports_varlen_paged_mqa_logits()
141
+
142
+ @staticmethod
143
+ def get_name() -> str:
144
+ return "DEEPSEEK_V32_INDEXER"
145
+
146
+ @staticmethod
147
+ def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
148
+ return [1, 64] if current_platform.is_rocm() else [64]
149
+
150
+ @classmethod
151
+ def get_supported_head_sizes(cls) -> list[int]:
152
+ return [32, 64, 128]
153
+
154
+ @staticmethod
155
+ def get_builder_cls() -> type["DeepseekV32IndexerMetadataBuilder"]:
156
+ return DeepseekV32IndexerMetadataBuilder
157
+
158
+ @staticmethod
159
+ def get_kv_cache_shape(
160
+ num_blocks: int,
161
+ block_size: int,
162
+ num_kv_heads: int,
163
+ head_size: int,
164
+ cache_dtype_str: str = "auto",
165
+ ) -> tuple[int, ...]:
166
+ assert num_kv_heads == 1
167
+ return (num_blocks, block_size, head_size)
168
+
169
+ @staticmethod
170
+ def get_kv_cache_stride_order(
171
+ include_num_layers_dimension: bool = False,
172
+ ) -> tuple[int, ...]:
173
+ if include_num_layers_dimension:
174
+ # DeepseekV32Indexer kernels do not support cross-layer
175
+ # KV cache layout. Identity permutation keeps num_layers
176
+ # first, signaling incompatibility.
177
+ return (0, 1, 2, 3)
178
+ return (0, 1, 2)
179
+
180
+
181
+ class KpoolTailBackend(DeepseekV32IndexerBackend):
182
+ """Storage-only backend for the GLM5Next kpool tail cache.
183
+
184
+ The tail cache holds the in-progress pool's raw K + gate score packed into
185
+ one ``[num_blocks, 2, block_size, head_dim]`` bf16 tensor (K at index 0,
186
+ gate score at index 1) and never runs attention. It reuses the indexer
187
+ metadata builder (token-granular when ``compress_ratio == 1``) but exposes a
188
+ 4D K||score shape and unrestricted head/block sizes: the indexer backend
189
+ hard-requires ``head_size in {32,64,128}`` and ``kernel_block_size 64``,
190
+ which the tail's ``block_size == kpool`` (e.g. 4) violates. The 4D
191
+ ``[2, block_size, head_dim]`` layout keeps K and score as separate block
192
+ halves so the connectors' non-MLA K/V half-split transfers them correctly.
193
+ """
194
+
195
+ @staticmethod
196
+ def get_name() -> str:
197
+ return "KPOOL_TAIL"
198
+
199
+ @classmethod
200
+ def get_supported_head_sizes(cls) -> list[int]:
201
+ return [] # supports any (K+score packed into head_size)
202
+
203
+ @staticmethod
204
+ def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
205
+ # block_size == kpool; no sub-block splitting (kernel_block_size ==
206
+ # block_size), so the tensor stays [num_blocks, 2, kpool, head_dim].
207
+ return [MultipleOf(1)]
208
+
209
+ @staticmethod
210
+ def get_kv_cache_shape(
211
+ num_blocks: int,
212
+ block_size: int,
213
+ num_kv_heads: int,
214
+ head_size: int,
215
+ cache_dtype_str: str = "auto",
216
+ ) -> tuple[int, ...]:
217
+ # head_size carries K+score packed (== 2 * head_dim); split into
218
+ # [2, block_size, head_dim] so K (idx 0) and score (idx 1) are separate
219
+ # block halves.
220
+ assert num_kv_heads == 1
221
+ assert head_size % 2 == 0, "KpoolTailSpec head_size must be 2*head_dim"
222
+ return (num_blocks, 2, block_size, head_size // 2)
223
+
224
+ @staticmethod
225
+ def get_kv_cache_stride_order(
226
+ include_num_layers_dimension: bool = False,
227
+ ) -> tuple[int, ...]:
228
+ return (0, 1, 2, 3)
229
+
230
+ @staticmethod
231
+ def get_builder_cls() -> type["KpoolTailMetadataBuilder"]: # type: ignore[override]
232
+ return KpoolTailMetadataBuilder
233
+
234
+
235
+ class DeepseekV4IndexerBackend(DeepseekV32IndexerBackend):
236
+ @staticmethod
237
+ def get_name() -> str:
238
+ return "DEEPSEEK_V4_INDEXER"
239
+
240
+ @staticmethod
241
+ def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
242
+ return [256]
243
+
244
+
245
+ @dataclass
246
+ class DeepseekV32IndexerPrefillChunkMetadata:
247
+ block_table: torch.Tensor
248
+ # Under DCP (dcp_world_size > 1) these hold this rank's local row bounds;
249
+ # otherwise they hold the global bounds.
250
+ cu_seqlen_ks: torch.Tensor
251
+ cu_seqlen_ke: torch.Tensor
252
+ cu_seq_lens: torch.Tensor
253
+ token_to_seq: torch.Tensor
254
+ total_seq_lens: int
255
+ token_start: int
256
+ token_end: int
257
+ num_reqs: int
258
+ skip_kv_gather: bool = False
259
+ local_cu_seq_lens: torch.Tensor | None = None
260
+ local_total_seq_lens: int = 0
261
+ max_local_total_seq_lens: int = 0
262
+
263
+
264
+ _BUILD_PREFILL_CHUNK_METADATA_INPUT_VARIANTS = (
265
+ TritonPointerInputVariant.from_alignment(uncompressed_seq_lens=True),
266
+ TritonPointerInputVariant.from_alignment(uncompressed_seq_lens=False),
267
+ )
268
+
269
+
270
+ class BuildPrefillChunkMetadataKernel(
271
+ VllmJitKernel["BuildPrefillChunkMetadataKernel.CompileKey"]
272
+ ):
273
+ BLOCK_SIZE = 1024
274
+
275
+ @dataclass(frozen=True)
276
+ class CompileKey:
277
+ query_slice_start: int
278
+ query_slice_stop: int
279
+ DCP_RANK: int
280
+ DCP_WORLD: int
281
+ DCP_INTERLEAVE: int
282
+ BLOCK_SIZE: int
283
+ COMPRESS_RATIO: int
284
+ input_variant: TritonPointerInputVariant
285
+
286
+ @staticmethod
287
+ @triton.jit
288
+ def kernel(
289
+ # Inputs
290
+ query_start_loc_ptr,
291
+ uncompressed_seq_lens_ptr,
292
+ cu_compressed_seq_lens_ptr,
293
+ # Row-start base for cu_seq_len_ks/ke: local cumulative lens under DCP,
294
+ # aliases cu_compressed_seq_lens_ptr otherwise.
295
+ row_start_cu_compressed_seq_lens_ptr,
296
+ # Outputs
297
+ token_to_seq_ptr,
298
+ cu_compressed_seq_len_ks_ptr,
299
+ cu_compressed_seq_len_ke_ptr,
300
+ query_slice_start,
301
+ query_slice_stop,
302
+ DCP_RANK,
303
+ DCP_WORLD,
304
+ DCP_INTERLEAVE,
305
+ BLOCK_SIZE: tl.constexpr,
306
+ COMPRESS_RATIO: tl.constexpr,
307
+ ):
308
+ batch_idx = tl.program_id(0)
309
+
310
+ query_start = tl.load(query_start_loc_ptr + batch_idx)
311
+ query_end = tl.load(query_start_loc_ptr + batch_idx + 1)
312
+ query_len = query_end - query_start
313
+
314
+ seq_start = tl.load(cu_compressed_seq_lens_ptr + batch_idx)
315
+ seq_end = tl.load(cu_compressed_seq_lens_ptr + batch_idx + 1)
316
+ compressed_seq_len = seq_end - seq_start
317
+
318
+ # Row start for the (possibly localized) cu_seq_len_ks/ke. Equals seq_start
319
+ # when DCP is disabled (the pointer aliases cu_compressed_seq_lens_ptr).
320
+ row_start = tl.load(row_start_cu_compressed_seq_lens_ptr + batch_idx)
321
+
322
+ uncompressed_seq_len = tl.load(uncompressed_seq_lens_ptr + batch_idx)
323
+ start_pos = uncompressed_seq_len - query_len
324
+
325
+ for i in range(0, query_len, BLOCK_SIZE):
326
+ offset = i + tl.arange(0, BLOCK_SIZE)
327
+ abs_pos = query_start + offset
328
+ mask = (
329
+ (offset < query_len)
330
+ & (abs_pos >= query_slice_start)
331
+ & (abs_pos < query_slice_stop)
332
+ )
333
+ out_pos = abs_pos - query_slice_start
334
+
335
+ # cu_seq_len_ks: row start in the gathered K buffer.
336
+ tl.store(cu_compressed_seq_len_ks_ptr + out_pos, row_start, mask=mask)
337
+
338
+ # cu_seq_len_ke: row start + per-token context length. Under DCP the
339
+ # global per-token length is sharded across ranks.
340
+ global_ctx = start_pos + 1 + offset
341
+ len_per_token = global_ctx // COMPRESS_RATIO
342
+ if DCP_WORLD > 1:
343
+ # Per-rank local context length under interleave-aware DCP, matching
344
+ # get_dcp_local_seq_lens. K == 1 reduces to (len + world-1-rank)//world.
345
+ base = (len_per_token // DCP_INTERLEAVE // DCP_WORLD) * DCP_INTERLEAVE
346
+ remainder = len_per_token - base * DCP_WORLD
347
+ remainder = tl.minimum(
348
+ tl.maximum(remainder - DCP_RANK * DCP_INTERLEAVE, 0), DCP_INTERLEAVE
349
+ )
350
+ len_per_token = base + remainder
351
+ tl.store(
352
+ cu_compressed_seq_len_ke_ptr + out_pos,
353
+ row_start + len_per_token,
354
+ mask=mask,
355
+ )
356
+
357
+ # Compute token_to_seq
358
+ for i in range(0, compressed_seq_len, BLOCK_SIZE):
359
+ offset = i + tl.arange(0, BLOCK_SIZE)
360
+ mask = offset < compressed_seq_len
361
+ tl.store(token_to_seq_ptr + seq_start + offset, batch_idx, mask=mask)
362
+
363
+ def dispatch( # type: ignore[override]
364
+ self,
365
+ *,
366
+ query_slice_start: int,
367
+ query_slice_stop: int,
368
+ DCP_RANK: int,
369
+ DCP_WORLD: int,
370
+ DCP_INTERLEAVE: int,
371
+ BLOCK_SIZE: int,
372
+ COMPRESS_RATIO: int,
373
+ input_variant: TritonPointerInputVariant,
374
+ ) -> CompileKey:
375
+ return self.CompileKey(
376
+ query_slice_start=query_slice_start,
377
+ query_slice_stop=query_slice_stop,
378
+ DCP_RANK=DCP_RANK,
379
+ DCP_WORLD=DCP_WORLD,
380
+ DCP_INTERLEAVE=DCP_INTERLEAVE,
381
+ BLOCK_SIZE=BLOCK_SIZE,
382
+ COMPRESS_RATIO=COMPRESS_RATIO,
383
+ input_variant=input_variant,
384
+ )
385
+
386
+ def get_warmup_keys(self, vllm_config: VllmConfig) -> list[CompileKey]:
387
+ max_tokens = max(1, min(vllm_config.scheduler_config.max_num_batched_tokens, 8))
388
+ hf_config = vllm_config.model_config.hf_config
389
+ parallel_config = vllm_config.parallel_config
390
+ dcp_world = parallel_config.decode_context_parallel_size
391
+ dcp_interleave = parallel_config.cp_kv_cache_interleave_size
392
+ dcp_rank = get_dcp_group().rank_in_group if dcp_world > 1 else 0
393
+ compress_ratios = tuple(
394
+ max(1, int(ratio))
395
+ for ratio in (getattr(hf_config, "compress_ratios", None) or (1,))
396
+ )
397
+ # GLM5Next's kpool indexer sets COMPRESS_RATIO = index_kpool on the
398
+ # indexer spec, which is not in compress_ratios. Include it so the
399
+ # warmup covers the runtime key instead of JIT-ing on first prefill.
400
+ index_kpool = getattr(hf_config, "index_kpool", None)
401
+ if index_kpool and index_kpool > 1 and index_kpool not in compress_ratios:
402
+ compress_ratios = compress_ratios + (index_kpool,)
403
+ return self._trace_dispatch(self.dispatch)(
404
+ query_slice_start=WarmupIntRange(0, 2),
405
+ query_slice_stop=(1, 2 * max_tokens - 1, 2 * max_tokens),
406
+ DCP_RANK=dcp_rank,
407
+ DCP_WORLD=dcp_world,
408
+ DCP_INTERLEAVE=dcp_interleave,
409
+ BLOCK_SIZE=self.BLOCK_SIZE,
410
+ COMPRESS_RATIO=list(compress_ratios),
411
+ input_variant=_BUILD_PREFILL_CHUNK_METADATA_INPUT_VARIANTS,
412
+ )
413
+
414
+ def compile(self, compile_key: CompileKey) -> None:
415
+ warmup = getattr(self.kernel, "warmup", None)
416
+ assert warmup is not None
417
+ int32_ptr = TritonWarmupTensor(torch.int32)
418
+ warmup(
419
+ int32_ptr,
420
+ compile_key.input_variant.pointer("uncompressed_seq_lens", torch.int32),
421
+ int32_ptr,
422
+ int32_ptr,
423
+ int32_ptr,
424
+ int32_ptr,
425
+ int32_ptr,
426
+ compile_key.query_slice_start,
427
+ compile_key.query_slice_stop,
428
+ compile_key.DCP_RANK,
429
+ compile_key.DCP_WORLD,
430
+ compile_key.DCP_INTERLEAVE,
431
+ BLOCK_SIZE=compile_key.BLOCK_SIZE,
432
+ COMPRESS_RATIO=compile_key.COMPRESS_RATIO,
433
+ grid=(1,),
434
+ )
435
+
436
+ def __call__(
437
+ self,
438
+ query_start_loc: torch.Tensor,
439
+ uncompressed_seq_lens: torch.Tensor,
440
+ cu_compressed_seq_lens: torch.Tensor,
441
+ row_start_cu_compressed_seq_lens: torch.Tensor,
442
+ token_to_seq: torch.Tensor,
443
+ cu_compressed_seq_len_ks: torch.Tensor,
444
+ cu_compressed_seq_len_ke: torch.Tensor,
445
+ query_slice_start: int,
446
+ query_slice_stop: int,
447
+ DCP_RANK: int,
448
+ DCP_WORLD: int,
449
+ DCP_INTERLEAVE: int,
450
+ *,
451
+ num_reqs: int,
452
+ COMPRESS_RATIO: int,
453
+ ) -> None:
454
+ self.kernel[(num_reqs,)](
455
+ query_start_loc,
456
+ uncompressed_seq_lens,
457
+ cu_compressed_seq_lens,
458
+ row_start_cu_compressed_seq_lens,
459
+ token_to_seq,
460
+ cu_compressed_seq_len_ks,
461
+ cu_compressed_seq_len_ke,
462
+ query_slice_start,
463
+ query_slice_stop,
464
+ DCP_RANK,
465
+ DCP_WORLD,
466
+ DCP_INTERLEAVE,
467
+ BLOCK_SIZE=self.BLOCK_SIZE,
468
+ COMPRESS_RATIO=COMPRESS_RATIO,
469
+ )
470
+
471
+
472
+ _BUILD_PREFILL_CHUNK_METADATA_KERNEL = BuildPrefillChunkMetadataKernel()
473
+
474
+
475
+ @dataclass
476
+ class DeepseekV32IndexerPrefillMetadata:
477
+ chunks: list[DeepseekV32IndexerPrefillChunkMetadata]
478
+ # Host-side max prefill seq len (== max token position + 1 across the
479
+ # step's prefill tokens; exact for prefill rows per the assert in build()).
480
+ # Lets the indexer layer's short-sequence check branch without the
481
+ # device sync a positions.max().item() would cost on every layer.
482
+ # -1 = unknown (other construction sites): the layer falls back to the
483
+ # device-side check.
484
+ max_prefill_seq_len: int = -1
485
+
486
+
487
+ @dataclass
488
+ class DeepSeekV32IndexerDecodeMetadata:
489
+ block_table: torch.Tensor
490
+ # seq_lens: per-token effective context lengths.
491
+ # - flatten path / plain decode: 1D (batch_size,)
492
+ # - native MTP path: 2D (B, next_n) where [b,j] = L_b - next_n + j + 1
493
+ # Both fp8_fp4_paged_mqa_logits and the topk kernels accept both shapes.
494
+ seq_lens: torch.Tensor
495
+ decode_lens: torch.Tensor
496
+ requires_padding: bool
497
+ schedule_metadata: torch.Tensor
498
+ global_seq_lens: torch.Tensor | None = None
499
+ # Original per-request decode_lens (length num_decodes) captured before
500
+ # _prepare_decode_tensors rewrites decode_lens. The kpool decode-write path
501
+ # uses these to group tokens by request on a variable MTP-verify batch,
502
+ # which the flatten path represents as all-1s with requires_padding=False.
503
+ per_req_decode_lens: torch.Tensor | None = None
504
+ # Host-side (build-time) values so the kpool decode-write path can branch
505
+ # without a runtime .item() (which would break cudagraph capture).
506
+ decode_is_uniform: bool = True
507
+ write_max_decode_len: int = 0
508
+ indices: torch.Tensor | None = None
509
+
510
+
511
+ @dataclass
512
+ class DeepseekV32IndexerMetadata:
513
+ # FIXME (zyongye)
514
+ # hacky way to access the data now, need to be in chunked meta
515
+ seq_lens: torch.Tensor
516
+ max_seq_len: int
517
+ slot_mapping: torch.Tensor
518
+
519
+ # New for MLA (compared to FlashAttention)
520
+ # For handling prefill decode split
521
+ num_decodes: int
522
+ num_decode_tokens: int
523
+ num_prefills: int
524
+ num_prefill_tokens: int
525
+
526
+ decode: DeepSeekV32IndexerDecodeMetadata | None = None
527
+ prefill: DeepseekV32IndexerPrefillMetadata | None = None
528
+
529
+
530
+ def compute_kpool_tail_slot_mapping(
531
+ slot_mapping: torch.Tensor,
532
+ block_table: torch.Tensor,
533
+ query_start_loc: torch.Tensor,
534
+ positions: torch.Tensor,
535
+ num_actual_tokens: int,
536
+ num_reqs: int,
537
+ kpool: int,
538
+ ) -> torch.Tensor:
539
+ """Circular tail slots: every token of request r lands in r's own block.
540
+
541
+ The generic per-group slot kernel maps ``pos -> bt[req][pos // bs] * bs +
542
+ pos % bs`` (``_compute_slot_mappings_kernel``). The tail group's block
543
+ table only ever has its FIRST column written -- ``KpoolTailManager``
544
+ allocates exactly one block per request and never grows -- so for any
545
+ token at ``pos >= kpool`` that kernel reads a zero column and the slot
546
+ collapses onto physical tail block 0. All concurrently running requests
547
+ then share one ``kpool``-slot ring and corrupt each other's pool
548
+ compression (seed / stash / completion all target block 0). This maps
549
+ ``slot = own_block * kpool + pos % kpool`` instead, which is the layout
550
+ the tail kernels and ``KpoolTailManager`` are designed around.
551
+
552
+ Pure torch (no Triton, no device sync): the indexer op consumes the tail
553
+ slot mapping on its eager break, so the returned tensor need not be the
554
+ persistent ``BlockTables`` buffer.
555
+ """
556
+ out = slot_mapping.clone()
557
+ if num_actual_tokens == 0:
558
+ return out
559
+ device = slot_mapping.device
560
+ tokens = torch.arange(num_actual_tokens, device=device)
561
+ # searchsorted(right=True): token i in [qsl[r], qsl[r+1]) -> request r.
562
+ req = torch.searchsorted(query_start_loc, tokens, right=True) - 1
563
+ req = req.clamp_(min=0, max=num_reqs - 1)
564
+ own_block = block_table[:num_reqs, 0].index_select(0, req).to(torch.int64)
565
+ pos = positions[:num_actual_tokens].to(torch.int64)
566
+ out[:num_actual_tokens] = own_block * kpool + torch.remainder(pos, kpool)
567
+ return out
568
+
569
+
570
+ class KpoolTailMetadataBuilder(AttentionMetadataBuilder):
571
+ """Lean metadata builder for the kpool tail cache.
572
+
573
+ The tail is storage-only (no attention / MQA-logits), so it skips the
574
+ DeepseekV32 indexer builder's DeepGEMM paged-MQA path -- which requires
575
+ ``block_kv in {32,64}`` and asserts on the tail's ``block_size == kpool``.
576
+ It exports only the group's token-granular ``slot_mapping`` plus the
577
+ prefill/decode counts; the indexer op reads
578
+ ``attn_metadata[tail_prefix].slot_mapping``. The slot mapping is
579
+ recomputed with the circular per-request layout (see
580
+ :func:`compute_kpool_tail_slot_mapping`) instead of reusing the generic
581
+ per-group kernel output, which cannot express a 1-block-per-request
582
+ circular buffer.
583
+ """
584
+
585
+ _cudagraph_support = AttentionCGSupport.ALWAYS
586
+ supports_update_block_table = False
587
+ reorder_batch_threshold = None
588
+
589
+ def __init__(
590
+ self,
591
+ kv_cache_spec: AttentionSpec,
592
+ layer_names: list[str],
593
+ vllm_config: VllmConfig,
594
+ device: torch.device,
595
+ ):
596
+ # No indexer-builder buffers (expanded_block_table / scheduler_metadata /
597
+ # compressed_slot_mapping) -- the tail is storage-only and exports only
598
+ # slot_mapping, which is rebuilt per step from the group's block table.
599
+ super().__init__(kv_cache_spec, layer_names, vllm_config, device)
600
+
601
+ def build(
602
+ self,
603
+ common_prefix_len: int,
604
+ common_attn_metadata: CommonAttentionMetadata,
605
+ fast_build: bool = False,
606
+ ) -> DeepseekV32IndexerMetadata:
607
+ num_decodes, num_prefills, num_decode_tokens, num_prefill_tokens = (
608
+ split_decodes_and_prefills(common_attn_metadata)
609
+ )
610
+ slot_mapping = common_attn_metadata.slot_mapping
611
+ positions = common_attn_metadata.positions
612
+ if positions is not None:
613
+ # Circular per-request layout; the generic kernel output collapses
614
+ # onto tail block 0 for pos >= kpool (see compute_... docstring).
615
+ slot_mapping = compute_kpool_tail_slot_mapping(
616
+ slot_mapping,
617
+ common_attn_metadata.block_table_tensor,
618
+ common_attn_metadata.query_start_loc,
619
+ positions,
620
+ common_attn_metadata.num_actual_tokens,
621
+ common_attn_metadata.num_reqs,
622
+ self.kv_cache_spec.block_size,
623
+ )
624
+ return DeepseekV32IndexerMetadata(
625
+ seq_lens=common_attn_metadata.seq_lens,
626
+ max_seq_len=common_attn_metadata.max_seq_len,
627
+ slot_mapping=slot_mapping,
628
+ num_decodes=num_decodes,
629
+ num_decode_tokens=num_decode_tokens,
630
+ num_prefills=num_prefills,
631
+ num_prefill_tokens=num_prefill_tokens,
632
+ prefill=None,
633
+ decode=None,
634
+ )
635
+
636
+
637
+ def get_max_prefill_buffer_size(vllm_config: VllmConfig):
638
+ max_model_len = vllm_config.model_config.max_model_len
639
+ # NOTE(Chen): 40 is a magic number for controlling the prefill buffer size.
640
+ # Each entry is 128 fp8 bytes and 4 scale bytes for a total of 132 bytes.
641
+ # The flashmla_sparse backend uses a workspace size of 5 * max_model_len.
642
+ # The memory usage of the workspace there is 576 * 2 bytes; so we size this as
643
+ # (576 * 2 // 132) * 5 = 40 to maximize this workspace size while still fitting
644
+ # within the flashmla_sparse workspace.
645
+ # For DeepSeek-V3.2, the max_model_len is 163840.
646
+ # 40 * 163840 * 132 = 865075200 bytes = 825 MB
647
+ legacy = max_model_len * 40
648
+ # TRANSIENT patch (2026-08-30, worker TRANSIENT, image
649
+ # local/vllm-glm53:transient-test): the rows of this buffer hold
650
+ # COMPRESSED (pool-granular) K entries when the model declares an indexer
651
+ # compress ratio (Glm5NextTextConfig.index_kpool; the indexer kv-cache
652
+ # spec sets compress_ratio = index_kpool and split_indexer_prefill_chunks
653
+ # budgets in compressed units). One scheduler batch can gather at most
654
+ # max_num_seqs requests x ceil(max_model_len / kpool) compressed rows, so
655
+ # the legacy 40*max_model_len sizing over-allocates the shared workspace
656
+ # by orders of magnitude at large max_model_len (GLM-5.3 at 524288 /
657
+ # kpool 4: 2.58 GiB reserved vs 16.5 MiB ever addressable). Size it to
658
+ # the reachable bound, with a single-request floor: the chunker admits an
659
+ # oversized single request as its own chunk, so the buffer must always
660
+ # hold one full request. Numerics are untouched -- the chunker's request
661
+ # packing is identical whenever every feasible batch fits (guaranteed by
662
+ # the seqs * per_req bound), and per-row scoring/top-k never depends on
663
+ # chunk grouping. Models without index_kpool (DeepSeek V3.2/V4) keep the
664
+ # legacy size bit-for-bit.
665
+ # Override for experiments: VLLM_SPARSE_INDEXER_PREFILL_BUFFER_ROWS
666
+ # (compressed rows; clamped to the single-request floor).
667
+ text_cfg = (
668
+ getattr(vllm_config.model_config, "hf_text_config", None)
669
+ or vllm_config.model_config.hf_config
670
+ )
671
+ kpool = getattr(text_cfg, "index_kpool", None) or 1
672
+ per_req = -(-max_model_len // kpool) # ceil div
673
+ env_rows = os.environ.get("VLLM_SPARSE_INDEXER_PREFILL_BUFFER_ROWS")
674
+ if env_rows:
675
+ return max(int(env_rows), per_req)
676
+ if kpool > 1:
677
+ max_num_seqs = vllm_config.scheduler_config.max_num_seqs
678
+ return max(per_req, min(legacy, max_num_seqs * per_req))
679
+ return legacy
680
+
681
+
682
+ def _supports_varlen_paged_mqa_logits() -> bool:
683
+ return (
684
+ current_platform.is_cuda()
685
+ and current_platform.is_device_capability_family(100)
686
+ and has_deep_gemm()
687
+ )
688
+
689
+
690
+ class DeepseekV32IndexerMetadataBuilder(AttentionMetadataBuilder):
691
+ # The indexer opts out of the shared reorder-threshold vote (see __init__),
692
+ # so this is None; its own split uses self.decode_threshold.
693
+ reorder_batch_threshold: int | None = None
694
+ requires_block_table_width = True
695
+
696
+ @classmethod
697
+ def get_cudagraph_support(
698
+ cls,
699
+ vllm_config: VllmConfig,
700
+ kv_cache_spec: KVCacheSpec,
701
+ ) -> AttentionCGSupport:
702
+ # FULL-mode cudagraph is supported: every tensor this builder exports
703
+ # in the decode path is either a view of a runner-refreshed input
704
+ # (block_table / seq_lens / query_start_loc / slot_mapping) or a
705
+ # persistent builder buffer overwritten each step (the __init__
706
+ # buffers, incl. indexer_decode_block_table_buffer). Captured pointers
707
+ # therefore stay valid on replay.
708
+ if _supports_varlen_paged_mqa_logits():
709
+ return AttentionCGSupport.ALWAYS
710
+ return AttentionCGSupport.UNIFORM_BATCH
711
+
712
+ def __init__(self, *args, block_table_width: int, **kwargs) -> None:
713
+ super().__init__(*args, **kwargs)
714
+ scheduler_config = self.vllm_config.scheduler_config
715
+ parallel_config = self.vllm_config.parallel_config
716
+ self.dcp_world_size = parallel_config.decode_context_parallel_size
717
+ self.dcp_rank = get_dcp_group().rank_in_group if self.dcp_world_size > 1 else 0
718
+ self.pcp_world_size = parallel_config.prefill_context_parallel_size
719
+ self.use_pcp = self.pcp_world_size > 1
720
+ self.cp_kv_cache_interleave_size = parallel_config.cp_kv_cache_interleave_size
721
+ # The DCP sparse-indexer code is parameterized by interleave size, but
722
+ # interleave > 1 is not yet validated end-to-end (gsm8k parity fails),
723
+ # so fail closed here rather than silently produce wrong output.
724
+ if self.dcp_world_size > 1 and self.cp_kv_cache_interleave_size > 1:
725
+ raise NotImplementedError(
726
+ "DCP sparse indexer currently supports only "
727
+ f"cp_kv_cache_interleave_size=1 (got "
728
+ f"{self.cp_kv_cache_interleave_size})."
729
+ )
730
+ # NOTE(Chen):an estimated max size of flattened_kv. Need to double check.
731
+ self.max_prefill_buffer_size = get_max_prefill_buffer_size(self.vllm_config)
732
+ self.num_speculative_tokens = (
733
+ self.vllm_config.speculative_config.num_speculative_tokens
734
+ if self.vllm_config.speculative_config
735
+ else 0
736
+ )
737
+ self.use_fp4_indexer_cache = (
738
+ self.vllm_config.attention_config.use_fp4_indexer_cache
739
+ )
740
+
741
+ assert (
742
+ current_platform.is_device_capability_family(100)
743
+ or not self.use_fp4_indexer_cache
744
+ ), (
745
+ "use_fp4_indexer_cache requires Blackwell datacenter GPUs "
746
+ "(sm_10x, e.g. B200/GB200); sm_120 (consumer Blackwell) and "
747
+ "earlier architectures are not supported."
748
+ )
749
+
750
+ next_n = self.num_speculative_tokens + 1
751
+ self.decode_threshold = next_n
752
+ self.reorder_batch_threshold = None
753
+ # NOTE: SM100 datacenter GPUs support any next_n natively via the
754
+ # multi-atom paged MQA logits kernels (FP8 and FP4 indexer
755
+ # caches). Outside the SM100 family the FP8
756
+ # paged MQA logits kernel only supports next_n in (1, 2)
757
+ # (deepgemm smxx_fp8_fp4_paged_mqa_logits.hpp:233), so flatten there.
758
+ self.use_flattening = not current_platform.is_device_capability_family(
759
+ 100
760
+ ) and next_n not in (1, 2)
761
+ self.supports_varlen = _supports_varlen_paged_mqa_logits()
762
+ logger.info_once(
763
+ "DSA indexer decode path: use_flattening=%s supports_varlen=%s "
764
+ "(next_n=%d, use_fp4_indexer_cache=%s)",
765
+ self.use_flattening,
766
+ self.supports_varlen,
767
+ next_n,
768
+ self.use_fp4_indexer_cache,
769
+ )
770
+
771
+ sm_count = num_compute_units(self.device.index)
772
+ self.num_sms = sm_count
773
+
774
+ self.offsets_buffer = torch.arange(
775
+ next_n, device=self.device, dtype=torch.int32
776
+ )
777
+ self.decode_lens_buffer = torch.zeros(
778
+ (scheduler_config.max_num_batched_tokens,),
779
+ dtype=torch.int32,
780
+ device=self.device,
781
+ )
782
+ # Persistent mirror of the original per-request decode_lens (length
783
+ # num_decodes), captured in build() before _prepare_decode_tensors
784
+ # flattens decode_lens_buffer to all-1s. Persistent so FULL-cudagraph
785
+ # replay reads a stable pointer (a per-step clone would move address).
786
+ self.per_req_decode_lens_buffer = torch.zeros(
787
+ (scheduler_config.max_num_batched_tokens,),
788
+ dtype=torch.int32,
789
+ device=self.device,
790
+ )
791
+ # Shared workspace for decode seq_lens. Native MTP views this as
792
+ # (B, max_decode_len) at runtime, keeping context_lens contiguous even
793
+ # when max_decode_len is smaller than next_n.
794
+ self.decode_seq_lens_buffer = torch.zeros(
795
+ (scheduler_config.max_num_batched_tokens,),
796
+ dtype=torch.int32,
797
+ device=self.device,
798
+ )
799
+ self.global_decode_seq_lens_buffer = torch.zeros(
800
+ (scheduler_config.max_num_batched_tokens,),
801
+ dtype=torch.int32,
802
+ device=self.device,
803
+ )
804
+ self.decode_indices_buffer = torch.zeros(
805
+ (scheduler_config.max_num_batched_tokens,),
806
+ dtype=torch.int32,
807
+ device=self.device,
808
+ )
809
+ self.arange_buffer = torch.arange(
810
+ max(
811
+ scheduler_config.max_num_seqs * next_n,
812
+ scheduler_config.max_num_batched_tokens,
813
+ ),
814
+ dtype=torch.int32,
815
+ device=self.device,
816
+ )
817
+ # block_table_width is the per-request block-table width the runner
818
+ # actually allocates. get_block_table_width already folds in any
819
+ # compress_ratio expansion (via kernel_block_size = block_size //
820
+ # compress_ratio), so it is the authoritative runtime width -- the
821
+ # block_table handed to _prepare_decode_tensors has exactly this many
822
+ # columns. Sizing the buffer to it keeps the row-copy / repeat_interleave
823
+ # assignments width-consistent (a separate max with a compress-ratio
824
+ # estimate over-counts and diverges from the allocated table).
825
+ expanded_block_table_width = block_table_width
826
+ self.expanded_block_table_buffer = torch.zeros(
827
+ (scheduler_config.max_num_batched_tokens, expanded_block_table_width),
828
+ dtype=torch.int32,
829
+ device=self.device,
830
+ )
831
+
832
+ # See: DeepGMM/csrc/apis/attention.hpp
833
+ self.scheduler_metadata_buffer = torch.empty(
834
+ (self.num_sms + 1, 2), dtype=torch.int32, device=self.device
835
+ )
836
+
837
+ # KV compression. Default to 1 for no compression.
838
+ self.compress_ratio = 1
839
+ # Get compress_ratio for DeepseekV4 support
840
+ if isinstance(self.kv_cache_spec, MLAAttentionSpec):
841
+ self.compress_ratio = self.kv_cache_spec.compress_ratio
842
+ if self.dcp_world_size > 1 and self.compress_ratio > 1:
843
+ raise NotImplementedError(
844
+ "DCP is not supported with sparse indexer KV compression "
845
+ f"(compress_ratio={self.compress_ratio})."
846
+ )
847
+
848
+ # Pre-allocate buffers for CUDA graph compatibility when
849
+ if self.compress_ratio > 1:
850
+ # compress_ratio > 1 (DeepseekV4)
851
+ # Compressed slot mapping output buffer
852
+ self.compressed_slot_mapping_buffer = torch.zeros(
853
+ (scheduler_config.max_num_batched_tokens,),
854
+ dtype=torch.int64,
855
+ device=self.device,
856
+ )
857
+ # Buffer for compressed seq_lens in decode path
858
+ self.expanded_seq_lens_buffer = torch.zeros(
859
+ (scheduler_config.max_num_batched_tokens,),
860
+ dtype=torch.int32,
861
+ device=self.device,
862
+ )
863
+ # Persistent buffer for the decode block_table translated to
864
+ # indexer-page granularity. Required for FULL-mode cudagraph: the
865
+ # captured graph dereferences the address recorded at capture time, so
866
+ # the translated table must live at a stable address that build()
867
+ # overwrites each step (a fresh .contiguous() would be frozen at
868
+ # capture-time content). Lazily allocated on first decode at the exact
869
+ # compressed width -- constant across batches, so the address is fixed
870
+ # before any graph captures it and the buffer is ~compress_ratio x
871
+ # smaller than the raw block table.
872
+ self.indexer_decode_block_table_buffer: torch.Tensor | None = None
873
+ self._max_num_batched_tokens = scheduler_config.max_num_batched_tokens
874
+
875
+ def _dcp_localize_decode_seq_lens(
876
+ self,
877
+ seq_lens: torch.Tensor,
878
+ num_decodes: int,
879
+ seq_lens_is_buffer_view: bool,
880
+ ) -> torch.Tensor:
881
+ local_seq_lens = get_dcp_local_seq_lens(
882
+ seq_lens,
883
+ self.dcp_world_size,
884
+ self.dcp_rank,
885
+ self.cp_kv_cache_interleave_size,
886
+ )
887
+ if seq_lens_is_buffer_view:
888
+ seq_lens.copy_(local_seq_lens)
889
+ return seq_lens
890
+
891
+ out = self.decode_seq_lens_buffer[:num_decodes]
892
+ out.copy_(local_seq_lens)
893
+ return out
894
+
895
+ def _prepare_decode_tensors(
896
+ self,
897
+ seq_lens: torch.Tensor,
898
+ block_table: torch.Tensor,
899
+ decode_lens: torch.Tensor,
900
+ decode_lens_cpu: torch.Tensor,
901
+ query_start_loc: torch.Tensor,
902
+ num_decodes: int,
903
+ num_decode_tokens: int,
904
+ use_native: bool,
905
+ next_n: int,
906
+ max_decode_len: int,
907
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, int, bool]:
908
+ """Prepare native or per-token flattened decode tensors."""
909
+ min_decode_len = int(decode_lens_cpu.min().item())
910
+ if not use_native:
911
+ assert self.decode_seq_lens_buffer.dim() == 1
912
+ if (
913
+ not self.supports_varlen
914
+ and min_decode_len == max_decode_len
915
+ and num_decodes * max_decode_len == num_decode_tokens
916
+ ):
917
+ # Uniform decode lengths with no cudagraph token padding.
918
+ _prepare_uniform_decode_kernel[(num_decode_tokens,)](
919
+ seq_lens,
920
+ self.decode_seq_lens_buffer,
921
+ block_table,
922
+ block_table.stride(0),
923
+ self.expanded_block_table_buffer,
924
+ self.expanded_block_table_buffer.stride(0),
925
+ self.decode_lens_buffer,
926
+ max_decode_len,
927
+ BLOCK_SIZE=1024,
928
+ )
929
+ self.decode_seq_lens_buffer[num_decode_tokens:] = 0
930
+ seq_lens = self.decode_seq_lens_buffer[:num_decode_tokens]
931
+ block_table = self.expanded_block_table_buffer[:num_decode_tokens]
932
+ decode_lens = self.decode_lens_buffer[:num_decode_tokens]
933
+ return seq_lens, block_table, decode_lens, num_decode_tokens, False
934
+ else:
935
+ # Variable decode lengths.
936
+ # Assume 4 requests with seq_lens [10, 7, 12, 0] (the final req is
937
+ # padding) and decode_lens [3, 1, 4, 0] in the below example comments.
938
+ # The context lengths are therefore
939
+ # [10-3, 7-1, 12-4, 0-0] = [7, 6, 8, 0].
940
+
941
+ # 3 + 1 + 4 + 0 = 8
942
+ actual_expanded = int(decode_lens_cpu.sum().item())
943
+
944
+ # Fuse expanded_base and expanded_starts into a single
945
+ # repeat_interleave:
946
+ # seq_len_i = (context_start[b] - query_start_loc[b]) + arange[i] + 1
947
+ # where context_start[b] = seq_lens[b] - decode_lens[b].
948
+ # Example: offsets = [7-0, 6-3, 8-4, 0-8] = [7, 3, 4, -8]
949
+ # expanded_offsets = [7, 7, 7, 3, 4, 4, 4, 4]
950
+ # result = [8, 9, 10, 7, 9, 10, 11, 12]
951
+ expanded_offsets = torch.repeat_interleave(
952
+ seq_lens - decode_lens - query_start_loc,
953
+ decode_lens,
954
+ output_size=actual_expanded,
955
+ )
956
+
957
+ # [8, 9, 10, 7, 9, 10, 11, 12, ...] where ... is unused buffer space
958
+ self.decode_seq_lens_buffer[:actual_expanded] = (
959
+ expanded_offsets + self.arange_buffer[:actual_expanded] + 1
960
+ )
961
+ self.decode_seq_lens_buffer[actual_expanded:] = 0
962
+ seq_lens = self.decode_seq_lens_buffer[:num_decode_tokens]
963
+
964
+ # Give each of the flattened entries the same block table row as the
965
+ # original request.
966
+ self.expanded_block_table_buffer[:actual_expanded] = (
967
+ torch.repeat_interleave(
968
+ block_table, decode_lens, dim=0, output_size=actual_expanded
969
+ )
970
+ )
971
+ if actual_expanded < num_decode_tokens:
972
+ self.expanded_block_table_buffer[
973
+ actual_expanded:num_decode_tokens, 0
974
+ ] = 0
975
+ block_table = self.expanded_block_table_buffer[:num_decode_tokens]
976
+
977
+ # All reqs now have decode_len=1
978
+ self.decode_lens_buffer[:num_decode_tokens] = 1
979
+ decode_lens = self.decode_lens_buffer[:num_decode_tokens]
980
+ return seq_lens, block_table, decode_lens, num_decode_tokens, False
981
+ else:
982
+ # Native path: plain decode (next_n==1) or spec decode
983
+ # with 2D per-token context lengths (next_n > 1).
984
+ #
985
+ # When decode_lens are not truly uniform (e.g. some requests have
986
+ # decode_len < next_n due to padding or short prefills), the simple
987
+ # reshape in sparse_attn_indexer won't work. Use pack_seq_triton
988
+ # (requires_padding) instead.
989
+ requires_padding = min_decode_len != max_decode_len
990
+ if use_native and next_n > 1:
991
+ assert self.decode_seq_lens_buffer.dim() == 1
992
+ # (B, max_decode_len): token j attends to
993
+ # L - max_decode_len + j + 1 KV tokens.
994
+ seq_lens_buffer = self.decode_seq_lens_buffer[
995
+ : num_decodes * max_decode_len
996
+ ].view(num_decodes, max_decode_len)
997
+ seq_lens_buffer[:] = (
998
+ seq_lens.unsqueeze(1)
999
+ - max_decode_len
1000
+ + 1
1001
+ + self.offsets_buffer[:max_decode_len]
1002
+ )
1003
+ seq_lens = seq_lens_buffer
1004
+ return seq_lens, block_table, decode_lens, num_decodes, requires_padding
1005
+
1006
+ def _prepare_global_decode_seq_lens(
1007
+ self,
1008
+ global_seq_lens: torch.Tensor | None,
1009
+ decode_lens: torch.Tensor,
1010
+ decode_lens_cpu: torch.Tensor,
1011
+ query_start_loc: torch.Tensor,
1012
+ num_decode_tokens: int,
1013
+ use_native: bool,
1014
+ max_decode_len: int,
1015
+ ) -> torch.Tensor | None:
1016
+ if global_seq_lens is None:
1017
+ return None
1018
+ if use_native or max_decode_len <= 1:
1019
+ return global_seq_lens
1020
+
1021
+ actual_expanded = int(decode_lens_cpu.sum().item())
1022
+ if actual_expanded > 0:
1023
+ expanded_offsets = torch.repeat_interleave(
1024
+ global_seq_lens - decode_lens - query_start_loc,
1025
+ decode_lens,
1026
+ output_size=actual_expanded,
1027
+ )
1028
+ self.global_decode_seq_lens_buffer[:actual_expanded] = (
1029
+ expanded_offsets + self.arange_buffer[:actual_expanded] + 1
1030
+ )
1031
+ self.global_decode_seq_lens_buffer[actual_expanded:num_decode_tokens] = 0
1032
+ return self.global_decode_seq_lens_buffer[:num_decode_tokens]
1033
+
1034
+ def _build_varlen_decode_indices(
1035
+ self,
1036
+ decode_lens: torch.Tensor,
1037
+ decode_lens_cpu: torch.Tensor,
1038
+ num_decode_tokens: int,
1039
+ ) -> torch.Tensor:
1040
+ """Build request ids for flattened SM100 varlen rows."""
1041
+ indices = self.decode_indices_buffer[:num_decode_tokens]
1042
+ actual_expanded = int(decode_lens_cpu.sum().item())
1043
+ num_decodes = decode_lens.shape[0]
1044
+ indices[:actual_expanded] = torch.repeat_interleave(
1045
+ self.arange_buffer[:num_decodes],
1046
+ decode_lens,
1047
+ output_size=actual_expanded,
1048
+ )
1049
+ if actual_expanded < num_decode_tokens:
1050
+ pad = num_decode_tokens - actual_expanded
1051
+ indices[actual_expanded:num_decode_tokens] = (
1052
+ num_decodes + self.arange_buffer[:pad]
1053
+ )
1054
+ return indices
1055
+
1056
+ def build(
1057
+ self,
1058
+ common_prefix_len: int,
1059
+ common_attn_metadata: CommonAttentionMetadata,
1060
+ fast_build: bool = False,
1061
+ ) -> DeepseekV32IndexerMetadata:
1062
+ num_reqs = common_attn_metadata.num_reqs
1063
+ num_tokens = common_attn_metadata.num_actual_tokens
1064
+ query_start_loc = common_attn_metadata.query_start_loc
1065
+ query_start_loc_cpu = common_attn_metadata.query_start_loc_cpu
1066
+ seq_lens = common_attn_metadata.seq_lens
1067
+ slot_mapping = common_attn_metadata.slot_mapping
1068
+ block_table = common_attn_metadata.block_table_tensor
1069
+ dcp_local_seq_lens = common_attn_metadata.dcp_local_seq_lens
1070
+ num_decodes, num_prefills, num_decode_tokens, num_prefill_tokens = (
1071
+ split_decodes_and_prefills(
1072
+ common_attn_metadata,
1073
+ decode_threshold=self.decode_threshold,
1074
+ require_uniform=not (self.use_flattening or self.supports_varlen),
1075
+ treat_short_extends_as_decodes=not self.use_pcp,
1076
+ )
1077
+ )
1078
+
1079
+ assert num_decodes + num_prefills == num_reqs
1080
+ assert num_decode_tokens + num_prefill_tokens == num_tokens
1081
+
1082
+ compressed_slot_mapping = slot_mapping
1083
+ compressed_seq_lens = seq_lens
1084
+ # Default to the raw shared table; translated below when the indexer is
1085
+ # co-located with a virtually-split MLA (compress_ratio > 1). This
1086
+ # translated table is the ONE block_table the indexer must use for every
1087
+ # cache access (write via slot_mapping AND the top-k reads), so that the
1088
+ # pool keys written at indexer-page granularity are read back from the
1089
+ # same physical pages.
1090
+ indexer_block_table = block_table
1091
+ if self.compress_ratio > 1:
1092
+ # The shared block_table is in kernel_block_size (token) units
1093
+ # (e.g. 64) because the co-located MLA is virtually split. The
1094
+ # indexer is NOT split and is addressed at storage_block_size
1095
+ # pool-page granularity, so translate the table: every
1096
+ # (block_size // kernel_block_size) columns is one indexer page,
1097
+ # and each such entry is (page_phys * that factor), so dividing
1098
+ # recovers the indexer physical page id in [0, num_blocks).
1099
+ kbs = getattr(self, "kernel_block_size", None)
1100
+ if (
1101
+ kbs is not None
1102
+ and self.kv_cache_spec.block_size != kbs
1103
+ and self.kv_cache_spec.block_size % kbs == 0
1104
+ ):
1105
+ factor = self.kv_cache_spec.block_size // kbs
1106
+ indexer_block_table = (block_table[:, ::factor] // factor).contiguous()
1107
+ padded_num_tokens = num_tokens
1108
+ if self.pcp_world_size > 1:
1109
+ padded_num_tokens = slot_mapping.shape[0] // self.pcp_world_size
1110
+ compressed_slot_mapping = get_compressed_slot_mapping(
1111
+ num_tokens,
1112
+ query_start_loc,
1113
+ seq_lens,
1114
+ indexer_block_table,
1115
+ self.kv_cache_spec.storage_block_size,
1116
+ self.compress_ratio,
1117
+ out=self.compressed_slot_mapping_buffer,
1118
+ )
1119
+ if self.pcp_world_size > 1:
1120
+ compressed_slot_mapping = get_pcp_group().all_gather(
1121
+ self.compressed_slot_mapping_buffer[:padded_num_tokens],
1122
+ dim=0,
1123
+ )
1124
+ compressed_seq_lens = seq_lens // self.compress_ratio
1125
+
1126
+ prefill_metadata = None
1127
+ if num_prefills > 0:
1128
+ # This CPU value is an upper bound for async-spec extend rows. It
1129
+ # is safe for chunking/allocation because CUDA metadata below is
1130
+ # built from exact device seq_lens and gather ignores the tail.
1131
+ assert common_attn_metadata.seq_lens_cpu_upper_bound is not None
1132
+ seq_lens_cpu = common_attn_metadata.seq_lens_cpu_upper_bound
1133
+ compressed_seq_lens_cpu = (
1134
+ seq_lens_cpu // self.compress_ratio
1135
+ if self.compress_ratio > 1
1136
+ else seq_lens_cpu
1137
+ )
1138
+ prefill_query_lens_cpu = torch.diff(
1139
+ query_start_loc_cpu[num_decodes : num_decodes + num_prefills + 1]
1140
+ )
1141
+ max_logits_bytes = envs.VLLM_SPARSE_INDEXER_MAX_LOGITS_MB * 1024 * 1024
1142
+ # Upper bound is exact for prefill rows (the `[num_decodes:]`
1143
+ # slice below).
1144
+ assert common_attn_metadata.seq_lens_cpu_upper_bound is not None
1145
+ seq_lens_cpu = common_attn_metadata.seq_lens_cpu_upper_bound
1146
+ chunk_specs = split_indexer_prefill_chunks(
1147
+ compressed_seq_lens_cpu[num_decodes:],
1148
+ prefill_query_lens_cpu,
1149
+ self.max_prefill_buffer_size,
1150
+ max_logits_bytes,
1151
+ request_offset=num_decodes,
1152
+ )
1153
+
1154
+ chunks = []
1155
+ for req_slice, query_slice in chunk_specs:
1156
+ metadata = build_prefill_chunk_metadata(
1157
+ req_slice.start,
1158
+ req_slice.stop,
1159
+ query_start_loc,
1160
+ query_start_loc_cpu,
1161
+ seq_lens,
1162
+ compressed_seq_lens,
1163
+ compressed_seq_lens_cpu,
1164
+ indexer_block_table,
1165
+ self.compress_ratio,
1166
+ query_slice=query_slice,
1167
+ skip_kv_gather=query_slice.start > 0,
1168
+ dcp_rank=self.dcp_rank,
1169
+ dcp_world_size=self.dcp_world_size,
1170
+ cp_kv_cache_interleave_size=self.cp_kv_cache_interleave_size,
1171
+ )
1172
+ # Skip when total_seq_lens is 0 (i.e., no compressed token).
1173
+ if metadata is not None:
1174
+ chunks.append(metadata)
1175
+ prefill_metadata = DeepseekV32IndexerPrefillMetadata(
1176
+ chunks,
1177
+ max_prefill_seq_len=(
1178
+ # Uncompressed (token-granular) to match `positions`.
1179
+ # seq_lens_cpu is exact for the prefill rows here, and the
1180
+ # step's prefill tokens always include each request's last
1181
+ # token (position seq_len-1), so this equals
1182
+ # positions[prefill_slice].max() + 1.
1183
+ int(seq_lens_cpu[num_decodes:].max().item())
1184
+ if num_prefills > 0
1185
+ else 0
1186
+ ),
1187
+ )
1188
+
1189
+ decode_metadata = None
1190
+ if num_decodes > 0:
1191
+ torch.diff(
1192
+ common_attn_metadata.query_start_loc[: num_decodes + 1],
1193
+ out=self.decode_lens_buffer[:num_decodes],
1194
+ )
1195
+ decode_lens = self.decode_lens_buffer[:num_decodes]
1196
+ # Stash the original per-request decode_lens before
1197
+ # _prepare_decode_tensors rewrites decode_lens_buffer (the flatten
1198
+ # path sets every entry to 1 for the logits read). The kpool
1199
+ # decode-write path uses these to scatter tokens by request on a
1200
+ # variable MTP-verify batch.
1201
+ self.per_req_decode_lens_buffer[:num_decodes].copy_(decode_lens)
1202
+ decode_lens_cpu = torch.diff(
1203
+ common_attn_metadata.query_start_loc_cpu[: num_decodes + 1]
1204
+ )
1205
+
1206
+ # Under DCP the per-token decode bounds must be localized AFTER the
1207
+ # per-token expansion below, not before. Expanding from a
1208
+ # request-level localized length subtracts decode offsets in local
1209
+ # space and yields too-short bounds (e.g. world=2, rank=1, global
1210
+ # per-token bounds [8, 9, 10] -> [3, 4, 5] instead of [4, 4, 5]), so
1211
+ # the first decode token would run top-k against too short a local KV
1212
+ # range and miss valid tokens. Keep the global seq_lens here and
1213
+ # localize the expanded bounds further down.
1214
+ global_seq_lens_for_decode: torch.Tensor | None = None
1215
+ if dcp_local_seq_lens is not None:
1216
+ global_seq_lens_for_decode = common_attn_metadata.seq_lens[:num_decodes]
1217
+ seq_lens = common_attn_metadata.seq_lens[:num_decodes]
1218
+ block_table = common_attn_metadata.block_table_tensor[:num_decodes, ...]
1219
+
1220
+ max_decode_len = int(decode_lens_cpu.max().item())
1221
+ min_decode_len = int(decode_lens_cpu.min().item())
1222
+ # Host-side uniformity flag for the kpool decode-write path: the
1223
+ # flatten path below rewrites decode_lens to all-1s and reports
1224
+ # requires_padding=False even for a variable MTP-verify batch, so
1225
+ # the write path can't reuse requires_padding. Compute it here (on
1226
+ # the CPU copy, so no runtime .item() that would break cudagraph
1227
+ # capture) and hand down max_decode_len as the scatter lmax.
1228
+ write_is_uniform = min_decode_len == max_decode_len
1229
+ next_n = 1 + self.num_speculative_tokens
1230
+ use_native = (
1231
+ not (self.use_flattening or self.supports_varlen)
1232
+ and max_decode_len <= next_n
1233
+ )
1234
+
1235
+ global_seq_lens_for_decode = self._prepare_global_decode_seq_lens(
1236
+ global_seq_lens=global_seq_lens_for_decode,
1237
+ decode_lens=decode_lens,
1238
+ decode_lens_cpu=decode_lens_cpu,
1239
+ query_start_loc=common_attn_metadata.query_start_loc[:num_decodes],
1240
+ num_decode_tokens=num_decode_tokens,
1241
+ use_native=use_native,
1242
+ max_decode_len=max_decode_len,
1243
+ )
1244
+
1245
+ decode_indices = None
1246
+ if self.supports_varlen:
1247
+ decode_indices = self._build_varlen_decode_indices(
1248
+ decode_lens=decode_lens,
1249
+ decode_lens_cpu=decode_lens_cpu,
1250
+ num_decode_tokens=num_decode_tokens,
1251
+ )
1252
+
1253
+ seq_lens, block_table, decode_lens, batch_size, requires_padding = (
1254
+ self._prepare_decode_tensors(
1255
+ seq_lens=seq_lens,
1256
+ block_table=block_table,
1257
+ decode_lens=decode_lens,
1258
+ decode_lens_cpu=decode_lens_cpu,
1259
+ query_start_loc=common_attn_metadata.query_start_loc[:num_decodes],
1260
+ num_decodes=num_decodes,
1261
+ num_decode_tokens=num_decode_tokens,
1262
+ use_native=use_native,
1263
+ next_n=next_n,
1264
+ max_decode_len=max_decode_len,
1265
+ )
1266
+ )
1267
+
1268
+ # Translate the decode block_table to indexer-page granularity,
1269
+ # matching the prefill read and the write. Done AFTER
1270
+ # _prepare_decode_tensors because it copies raw MLA-width rows
1271
+ # (its expand kernel reads expanded_bt_stride columns per row).
1272
+ if self.compress_ratio > 1:
1273
+ _kbs = getattr(self, "kernel_block_size", None)
1274
+ if (
1275
+ _kbs is not None
1276
+ and self.kv_cache_spec.block_size != _kbs
1277
+ and self.kv_cache_spec.block_size % _kbs == 0
1278
+ ):
1279
+ _factor = self.kv_cache_spec.block_size // _kbs
1280
+ # Copy into the persistent buffer (not a fresh
1281
+ # .contiguous()) so FULL-mode cudagraph replay reads
1282
+ # content overwritten each step by build(); a one-shot
1283
+ # allocation would be frozen at capture-time values.
1284
+ compressed = block_table[:, ::_factor] // _factor
1285
+ rows, cols = compressed.shape
1286
+ if self.indexer_decode_block_table_buffer is None:
1287
+ self.indexer_decode_block_table_buffer = torch.zeros(
1288
+ (self._max_num_batched_tokens, cols),
1289
+ dtype=torch.int32,
1290
+ device=self.device,
1291
+ )
1292
+ self.indexer_decode_block_table_buffer[:rows, :cols].copy_(
1293
+ compressed
1294
+ )
1295
+ block_table = self.indexer_decode_block_table_buffer[:rows, :cols]
1296
+ seq_lens_is_buffer_view = (use_native and next_n > 1) or (
1297
+ not use_native and max_decode_len > 1
1298
+ )
1299
+
1300
+ # DCP: localize the now-expanded per-token global bounds to this
1301
+ # rank's owned KV. Done here (after expansion) so each token's global
1302
+ # causal length is localized individually; see the comment above.
1303
+ if dcp_local_seq_lens is not None:
1304
+ seq_lens = self._dcp_localize_decode_seq_lens(
1305
+ seq_lens, num_decodes, seq_lens_is_buffer_view
1306
+ )
1307
+
1308
+ # For DeepseekV4 (compress_ratio > 1), the indexer KV cache stores
1309
+ # compressed tokens. Convert uncompressed seq_lens to compressed.
1310
+ if self.compress_ratio > 1:
1311
+ if seq_lens_is_buffer_view:
1312
+ seq_lens //= self.compress_ratio
1313
+ else:
1314
+ # Copy to avoid mutating shared state; keeps CG address stable.
1315
+ self.expanded_seq_lens_buffer[:num_decodes] = (
1316
+ seq_lens // self.compress_ratio
1317
+ )
1318
+ self.expanded_seq_lens_buffer[num_decodes:num_decode_tokens] = 0
1319
+ seq_lens = self.expanded_seq_lens_buffer[:num_decode_tokens]
1320
+
1321
+ # Non-MTP: deep_gemm paged MQA logits requires 2D context_lens
1322
+ # (csrc/apis/attention.hpp). Unsqueeze to (B, 1) so downstream
1323
+ # kernels see the same (B, next_n) layout as the MTP path.
1324
+ if seq_lens.dim() == 1:
1325
+ seq_lens = seq_lens.unsqueeze(-1)
1326
+
1327
+ # DeepGEMM is required for the paged MQA logits on CUDA devices
1328
+ if current_platform.is_cuda() and has_deep_gemm():
1329
+ self.scheduler_metadata_buffer[:] = get_paged_mqa_logits_metadata(
1330
+ seq_lens,
1331
+ self.kv_cache_spec.storage_block_size,
1332
+ self.num_sms,
1333
+ indices=decode_indices,
1334
+ )
1335
+
1336
+ decode_metadata = DeepSeekV32IndexerDecodeMetadata(
1337
+ block_table=block_table,
1338
+ seq_lens=seq_lens,
1339
+ decode_lens=decode_lens,
1340
+ requires_padding=requires_padding,
1341
+ schedule_metadata=self.scheduler_metadata_buffer,
1342
+ indices=decode_indices,
1343
+ global_seq_lens=global_seq_lens_for_decode,
1344
+ per_req_decode_lens=self.per_req_decode_lens_buffer[:num_decodes],
1345
+ decode_is_uniform=write_is_uniform,
1346
+ write_max_decode_len=max_decode_len,
1347
+ )
1348
+
1349
+ attn_metadata = DeepseekV32IndexerMetadata(
1350
+ seq_lens=common_attn_metadata.seq_lens,
1351
+ max_seq_len=common_attn_metadata.max_seq_len,
1352
+ slot_mapping=compressed_slot_mapping,
1353
+ num_decodes=num_decodes,
1354
+ num_decode_tokens=num_decode_tokens,
1355
+ num_prefills=num_prefills,
1356
+ num_prefill_tokens=num_prefill_tokens,
1357
+ prefill=prefill_metadata,
1358
+ decode=decode_metadata,
1359
+ )
1360
+
1361
+ return attn_metadata
1362
+
1363
+
1364
+ def build_prefill_chunk_metadata(
1365
+ start_idx: int,
1366
+ end_idx: int,
1367
+ query_start_loc: torch.Tensor,
1368
+ query_start_loc_cpu: torch.Tensor,
1369
+ uncompressed_seq_lens: torch.Tensor,
1370
+ compressed_seq_lens: torch.Tensor,
1371
+ compressed_seq_lens_cpu: torch.Tensor,
1372
+ block_table: torch.Tensor,
1373
+ compress_ratio: int,
1374
+ query_slice: slice | None = None,
1375
+ skip_kv_gather: bool = False,
1376
+ dcp_rank: int = 0,
1377
+ dcp_world_size: int = 1,
1378
+ cp_kv_cache_interleave_size: int = 1,
1379
+ ) -> DeepseekV32IndexerPrefillChunkMetadata | None:
1380
+ total_seq_lens = compressed_seq_lens_cpu[start_idx:end_idx].sum().item()
1381
+ if total_seq_lens == 0:
1382
+ return None
1383
+
1384
+ num_reqs = end_idx - start_idx
1385
+ device = block_table.device
1386
+ token_to_seq = torch.empty(total_seq_lens, dtype=torch.int32, device=device)
1387
+
1388
+ cu_seq_lens = torch.empty(num_reqs + 1, dtype=torch.int32, device=device)
1389
+ # Assigning to slice avoids cpu sync.
1390
+ cu_seq_lens[:1] = 0
1391
+ torch.cumsum(compressed_seq_lens[start_idx:end_idx], dim=0, out=cu_seq_lens[1:])
1392
+
1393
+ local_cu_seq_lens = cu_seq_lens
1394
+ local_total_seq_lens = total_seq_lens
1395
+ max_local_total_seq_lens = total_seq_lens
1396
+ if dcp_world_size > 1:
1397
+ # Per-rank local KV length under interleave-aware DCP sharding, shape
1398
+ # [num_reqs, dcp_world_size]. Reuse the canonical CP helper so the
1399
+ # sharding matches the rest of the DCP pipeline (decode/prefill).
1400
+ local_seq_lens = get_dcp_local_seq_lens(
1401
+ compressed_seq_lens[start_idx:end_idx],
1402
+ dcp_world_size,
1403
+ None,
1404
+ cp_kv_cache_interleave_size,
1405
+ )
1406
+ this_rank_counts = local_seq_lens[:, dcp_rank].to(torch.int32)
1407
+ local_cu_seq_lens = torch.zeros(num_reqs + 1, dtype=torch.int32, device=device)
1408
+ torch.cumsum(this_rank_counts, dim=0, out=local_cu_seq_lens[1:])
1409
+ local_total_seq_lens = int(local_cu_seq_lens[-1].item())
1410
+ max_local_total_seq_lens = int(local_seq_lens.sum(dim=0).max().item())
1411
+
1412
+ query_start_loc = (
1413
+ query_start_loc[start_idx : end_idx + 1] - query_start_loc[start_idx]
1414
+ )
1415
+
1416
+ total_query_len = int(
1417
+ (query_start_loc_cpu[end_idx] - query_start_loc_cpu[start_idx]).item()
1418
+ )
1419
+ if query_slice is not None:
1420
+ qs_start = query_slice.start
1421
+ qs_stop = query_slice.stop
1422
+ else:
1423
+ qs_start = 0
1424
+ qs_stop = total_query_len
1425
+ output_query_len = qs_stop - qs_start
1426
+
1427
+ cu_seq_len_ks = torch.empty(output_query_len, dtype=torch.int32, device=device)
1428
+ cu_seq_len_ke = torch.empty(output_query_len, dtype=torch.int32, device=device)
1429
+
1430
+ # Under DCP the kernel writes this rank's local row bounds into
1431
+ # cu_seq_len_ks/ke; otherwise local_cu_seq_lens aliases cu_seq_lens.
1432
+ _BUILD_PREFILL_CHUNK_METADATA_KERNEL(
1433
+ query_start_loc,
1434
+ uncompressed_seq_lens[start_idx:end_idx],
1435
+ cu_seq_lens,
1436
+ local_cu_seq_lens,
1437
+ token_to_seq,
1438
+ cu_seq_len_ks,
1439
+ cu_seq_len_ke,
1440
+ qs_start,
1441
+ qs_stop,
1442
+ dcp_rank,
1443
+ dcp_world_size,
1444
+ cp_kv_cache_interleave_size,
1445
+ num_reqs=num_reqs,
1446
+ COMPRESS_RATIO=compress_ratio,
1447
+ )
1448
+
1449
+ token_start = query_start_loc_cpu[start_idx].item()
1450
+ if query_slice is not None:
1451
+ token_end = token_start + qs_stop
1452
+ token_start = token_start + qs_start
1453
+ skip_kv_gather = skip_kv_gather or qs_start > 0
1454
+ else:
1455
+ token_end = query_start_loc_cpu[end_idx].item()
1456
+
1457
+ return DeepseekV32IndexerPrefillChunkMetadata(
1458
+ cu_seqlen_ks=cu_seq_len_ks,
1459
+ cu_seqlen_ke=cu_seq_len_ke,
1460
+ cu_seq_lens=cu_seq_lens,
1461
+ token_to_seq=token_to_seq,
1462
+ total_seq_lens=total_seq_lens,
1463
+ block_table=block_table[start_idx:end_idx],
1464
+ token_start=token_start,
1465
+ token_end=token_end,
1466
+ num_reqs=num_reqs,
1467
+ skip_kv_gather=skip_kv_gather,
1468
+ local_cu_seq_lens=local_cu_seq_lens,
1469
+ local_total_seq_lens=local_total_seq_lens,
1470
+ max_local_total_seq_lens=max_local_total_seq_lens,
1471
+ )
serving/everything-lane/kv_cache_interface.py ADDED
@@ -0,0 +1,1141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+ from __future__ import annotations
5
+
6
+ import copy
7
+ from collections import Counter
8
+ from dataclasses import dataclass, fields, replace
9
+ from enum import Enum, IntEnum
10
+ from math import prod
11
+ from typing import TYPE_CHECKING, TypeVar
12
+
13
+ import torch
14
+ from typing_extensions import Self
15
+
16
+ from vllm.logger import init_logger
17
+ from vllm.utils.math_utils import cdiv, round_up
18
+ from vllm.utils.torch_utils import get_dtype_size, nvfp4_kv_cache_full_dim
19
+ from vllm.v1.attention.backends.registry import MambaAttentionBackendEnum
20
+ from vllm.v1.kv_cache_spec_registry import KVCacheSpecRegistry
21
+
22
+ if TYPE_CHECKING:
23
+ from vllm.config import VllmConfig
24
+
25
+ logger = init_logger(__name__)
26
+
27
+ _SpecT = TypeVar("_SpecT", bound="KVCacheSpec")
28
+
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # KV cache quantization mode
32
+ # ---------------------------------------------------------------------------
33
+
34
+
35
+ class KVQuantMode(IntEnum):
36
+ """KV cache quantization mode.
37
+
38
+ Used by attention backends and kernels to dispatch quantization logic
39
+ without string matching on ``kv_cache_dtype``.
40
+ """
41
+
42
+ NONE = 0
43
+ FP8_PER_TENSOR = 1 # per-tensor scales (current fp8 path)
44
+ INT8_PER_TOKEN_HEAD = 2 # per-token-head dynamic scales for int8
45
+ FP8_PER_TOKEN_HEAD = 3 # per-token-head dynamic scales for fp8
46
+ INT4_PER_TOKEN_HEAD = 4 # packed 2×int4/byte, RHT + asymmetric zp
47
+ NVFP4 = 5 # packed fp4 data + fp8 block scales
48
+ TURBOQUANT = 6 # Hadamard-rotated Lloyd-Max quant, packed K+V per slot
49
+
50
+ @property
51
+ def is_per_token_head(self) -> bool:
52
+ """True for any per-token-head quantization mode."""
53
+ return self in (
54
+ KVQuantMode.INT8_PER_TOKEN_HEAD,
55
+ KVQuantMode.FP8_PER_TOKEN_HEAD,
56
+ KVQuantMode.INT4_PER_TOKEN_HEAD,
57
+ )
58
+
59
+ @property
60
+ def is_nvfp4(self) -> bool:
61
+ """True for NVFP4 packed quantization mode."""
62
+ return self == KVQuantMode.NVFP4
63
+
64
+ @property
65
+ def is_turboquant(self) -> bool:
66
+ """True for turboquant quantization mode."""
67
+ return self == KVQuantMode.TURBOQUANT
68
+
69
+
70
+ def get_kv_quant_mode(kv_cache_dtype: str) -> KVQuantMode:
71
+ """Map a ``kv_cache_dtype`` string to a :class:`KVQuantMode`."""
72
+ if kv_cache_dtype == "int4_per_token_head":
73
+ return KVQuantMode.INT4_PER_TOKEN_HEAD
74
+ if kv_cache_dtype == "int8_per_token_head":
75
+ return KVQuantMode.INT8_PER_TOKEN_HEAD
76
+ if kv_cache_dtype == "fp8_per_token_head":
77
+ return KVQuantMode.FP8_PER_TOKEN_HEAD
78
+ if kv_cache_dtype.startswith("nvfp4"):
79
+ return KVQuantMode.NVFP4
80
+ if isinstance(kv_cache_dtype, str) and kv_cache_dtype.startswith("turboquant_"):
81
+ return KVQuantMode.TURBOQUANT
82
+ if isinstance(kv_cache_dtype, str) and kv_cache_dtype.startswith("fp8"):
83
+ return KVQuantMode.FP8_PER_TENSOR
84
+ return KVQuantMode.NONE
85
+
86
+
87
+ def is_quantized_kv_cache(kv_cache_dtype: str) -> bool:
88
+ return get_kv_quant_mode(kv_cache_dtype) != KVQuantMode.NONE
89
+
90
+
91
+ def replace_as(spec: KVCacheSpec, target_cls: type[_SpecT], **changes) -> _SpecT:
92
+ """``dataclasses.replace``, but rebuilding *spec* as *target_cls*
93
+ e.g. ``SlidingWindowSpec`` -> ``FullAttentionSpec``
94
+
95
+ Every field of *spec* must exist on *target_cls*; fields only *target_cls* has keep
96
+ their default values.
97
+ """
98
+ kwargs = {f.name: getattr(spec, f.name) for f in fields(spec) if f.init}
99
+ kwargs.update(changes)
100
+ return target_cls(**kwargs)
101
+
102
+
103
+ def kv_cache_uses_per_token_head_scales(kv_cache_dtype: str) -> bool:
104
+ """Return True if *kv_cache_dtype* needs per-token-head scales."""
105
+ return get_kv_quant_mode(kv_cache_dtype).is_per_token_head
106
+
107
+
108
+ class KVCacheSpecKind(str, Enum):
109
+ FULL_ATTENTION = "full_attention"
110
+ MLA_ATTENTION = "mla_attention"
111
+ SLIDING_WINDOW = "sliding_window"
112
+ SLIDING_WINDOW_MLA = "sliding_window_mla"
113
+ MAMBA = "mamba"
114
+ CHUNKED_LOCAL_ATTENTION = "chunked_local_attention"
115
+ SINK_FULL_ATTENTION = "sink_full_attention"
116
+ ENCODER_ONLY_ATTENTION = "encoder_only_attention"
117
+ CROSS_ATTENTION = "cross_attention"
118
+ UNKNOWN = "unknown"
119
+
120
+
121
+ @dataclass(frozen=True)
122
+ class KVCacheSpec:
123
+ """
124
+ A base class for specifying the KV cache format of one layer.
125
+ """
126
+
127
+ # number of tokens in a block
128
+ block_size: int
129
+
130
+ @property
131
+ def participates_in_prefix_caching(self) -> bool:
132
+ """Whether this spec's group participates in prefix caching.
133
+
134
+ Co-owned / transient groups that are not real block allocators (e.g.
135
+ GLM5Next's kpool tail, a 1-block/req circular scratch buffer whose
136
+ content is per-request and overwritten in place) opt out by returning
137
+ ``False``. The structural prefix-caching machinery (the global
138
+ ``cache_config.block_size`` min, the hybrid divisibility assert, and the
139
+ ``attention_groups`` hit-lookup) then ignores them, so a non-shareable
140
+ group can neither drag the block size down nor cap the hybrid hit at 0.
141
+ Their manager still allocates/frees blocks normally.
142
+ """
143
+ return True
144
+
145
+ @property
146
+ def page_size_bytes(self) -> int:
147
+ """
148
+ The size of a page with `block_size` tokens in bytes.
149
+
150
+ Returns:
151
+ The page size
152
+ """
153
+ raise NotImplementedError
154
+
155
+ @property
156
+ def storage_block_size(self) -> int:
157
+ return self.block_size
158
+
159
+ def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
160
+ """
161
+ The maximum possible memory usage of this KV cache in bytes.
162
+
163
+ Returns:
164
+ The KV cache size in bytes
165
+ """
166
+ raise NotImplementedError
167
+
168
+ def max_num_blocks_per_req(self, vllm_config: VllmConfig, max_len: int) -> int:
169
+ """
170
+ The number of block table entries needed per request, i.e. the row
171
+ length of the worker-side block table for this cache group.
172
+
173
+ Args:
174
+ vllm_config: The vllm config.
175
+ max_len: The maximum sequence length to size for, including the
176
+ encoder length for encoder-decoder models.
177
+ """
178
+ return cdiv(max_len, self.block_size)
179
+
180
+ def copy_with_new_block_size(self, block_size: int) -> Self:
181
+ """
182
+ Create a new KVCacheSpec from self but replacing the block size.
183
+ """
184
+ return replace(self, block_size=block_size)
185
+
186
+ @classmethod
187
+ def merge(cls, specs: list[Self]) -> Self:
188
+ """
189
+ Merge a list of KVCacheSpec objects into a single KVCacheSpec object.
190
+ """
191
+ assert all(spec == specs[0] for spec in specs[1:]), (
192
+ "All layers in the same KV cache group must be the same."
193
+ )
194
+ return copy.deepcopy(specs[0])
195
+
196
+ def is_uniform_with_collection(
197
+ self, kv_cache_specs: dict[str, KVCacheSpec]
198
+ ) -> bool:
199
+ """
200
+ Whether this KVCacheSpec is uniform with all specs of all layers.
201
+ """
202
+ uniform_type_base_spec = KVCacheSpecRegistry.get_uniform_type_base_spec(self)
203
+ assert uniform_type_base_spec is not None, (
204
+ f"Unsupported KV cache spec type: {type(self)}. "
205
+ "Please register it using @register_kv_cache_spec decorator."
206
+ )
207
+ return all(
208
+ isinstance(spec, uniform_type_base_spec) for spec in kv_cache_specs.values()
209
+ )
210
+
211
+
212
+ @dataclass(frozen=True, kw_only=True)
213
+ class AttentionSpec(KVCacheSpec):
214
+ num_kv_heads: int
215
+ head_size: int
216
+ dtype: torch.dtype
217
+ kv_quant_mode: KVQuantMode = KVQuantMode.NONE
218
+ page_size_padded: int | None = None
219
+ indexes_kv_by_block_stride: bool = False
220
+
221
+ @property
222
+ def unpadded_page_size_bytes(self) -> int:
223
+ unpadded = self.real_page_size_bytes
224
+ # Per-token-head scales are stored in separate tensors managed
225
+ # by the attention backend, but the memory is carved from the
226
+ # raw KV cache allocation so it must be budgeted here.
227
+ if self.kv_quant_mode.is_per_token_head:
228
+ unpadded += (
229
+ 2 * self.block_size * self.num_kv_heads * get_dtype_size(torch.float32)
230
+ )
231
+ return unpadded
232
+
233
+ @property
234
+ def page_size_bytes(self) -> int:
235
+ if self.page_size_padded is not None:
236
+ assert self.page_size_padded >= self.unpadded_page_size_bytes
237
+ return self.page_size_padded
238
+ return self.unpadded_page_size_bytes
239
+
240
+ @property
241
+ def real_page_size_bytes(self) -> int:
242
+ if self.kv_quant_mode.is_nvfp4:
243
+ # Packed layout: fp4 data + fp8 block scales per head.
244
+ head_dim = nvfp4_kv_cache_full_dim(self.head_size)
245
+ elif self.kv_quant_mode == KVQuantMode.INT4_PER_TOKEN_HEAD:
246
+ head_dim = self.head_size // 2
247
+ else:
248
+ head_dim = self.head_size
249
+ return (
250
+ 2
251
+ * self.block_size
252
+ * self.num_kv_heads
253
+ * head_dim
254
+ * get_dtype_size(self.dtype)
255
+ )
256
+
257
+ def max_num_blocks_per_req(self, vllm_config: VllmConfig, max_len: int) -> int:
258
+ parallel_config = vllm_config.parallel_config
259
+ kv_shard_count = parallel_config.decode_context_parallel_size
260
+ return cdiv(max_len, self.block_size * kv_shard_count)
261
+
262
+
263
+ @dataclass(frozen=True, kw_only=True)
264
+ class FullAttentionSpec(AttentionSpec):
265
+ """
266
+ When hybrid allocator is disabled and the model contains both full
267
+ attention layers and sliding window attention layers, sliding
268
+ window attention are regarded as full attention in KV cache manager
269
+ (blocks are allocated for all tokens), while computed as sliding window
270
+ attention in model runner.
271
+ In this case, we use FullAttentionSpec and record the sliding window size.
272
+ """
273
+
274
+ head_size_v: int = None # type: ignore[assignment]
275
+
276
+ sliding_window: int | None = None
277
+ """
278
+ Default to None for not using sliding window attention.
279
+ """
280
+ attention_chunk_size: int | None = None
281
+
282
+ non_causal: bool = False
283
+ """
284
+ Whether the layer attends non-causally (e.g. Prefix LM). Carried on the
285
+ spec so the engine core, which collects specs from all workers before the
286
+ scheduler is built, can adjust scheduling policy (chunked prefill / prefix
287
+ caching) regardless of tensor-parallel layout. It does not affect the KV
288
+ cache layout itself.
289
+ """
290
+
291
+ def __post_init__(self):
292
+ if self.head_size_v is None:
293
+ object.__setattr__(self, "head_size_v", self.head_size)
294
+
295
+ def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
296
+ max_model_len = vllm_config.model_config.max_model_len
297
+ dcp_world_size = vllm_config.parallel_config.decode_context_parallel_size
298
+ if dcp_world_size > 1:
299
+ max_model_len = cdiv(max_model_len, dcp_world_size)
300
+ return cdiv(max_model_len, self.block_size) * self.page_size_bytes
301
+
302
+ @classmethod
303
+ def merge_window_sizes(cls, window_sizes: set[int]) -> int | None:
304
+ if len(window_sizes) == 0:
305
+ return None
306
+ elif len(window_sizes) == 1:
307
+ return window_sizes.pop()
308
+ else:
309
+ raise ValueError(
310
+ "All attention layers in the same KV cache group must have the "
311
+ "same window size."
312
+ )
313
+
314
+ @classmethod
315
+ def merge(cls, specs: list[Self]) -> Self:
316
+ """
317
+ Merge a list of FullAttentionSpec objects into a single
318
+ FullAttentionSpec object.
319
+ """
320
+ assert all(isinstance(spec, FullAttentionSpec) for spec in specs), (
321
+ "All attention layers in the same KV cache group must be FullAttentionSpec."
322
+ )
323
+
324
+ sliding_window = set(
325
+ spec.sliding_window for spec in specs if spec.sliding_window is not None
326
+ )
327
+ attention_chunk_size = set(
328
+ spec.attention_chunk_size
329
+ for spec in specs
330
+ if spec.attention_chunk_size is not None
331
+ )
332
+ assert not any(isinstance(spec, MLAAttentionSpec) for spec in specs), (
333
+ "MLAAttentionSpec should be merged in MLAAttentionSpec.merge"
334
+ )
335
+ merged_spec = cls(
336
+ block_size=specs[0].block_size,
337
+ num_kv_heads=specs[0].num_kv_heads,
338
+ head_size=specs[0].head_size,
339
+ head_size_v=specs[0].head_size_v,
340
+ dtype=specs[0].dtype,
341
+ kv_quant_mode=specs[0].kv_quant_mode,
342
+ page_size_padded=specs[0].page_size_padded,
343
+ indexes_kv_by_block_stride=specs[0].indexes_kv_by_block_stride,
344
+ sliding_window=cls.merge_window_sizes(sliding_window),
345
+ attention_chunk_size=cls.merge_window_sizes(attention_chunk_size),
346
+ # If any layer in the group is non-causal, treat the group as
347
+ # non-causal so the engine core disables incompatible scheduling.
348
+ non_causal=any(spec.non_causal for spec in specs),
349
+ )
350
+ for spec in specs:
351
+ for f in fields(AttentionSpec):
352
+ assert getattr(spec, f.name) == getattr(merged_spec, f.name), (
353
+ "All attention layers in the same KV cache group must have "
354
+ "the same attention spec."
355
+ )
356
+ assert (merged_spec.sliding_window is not None) + (
357
+ merged_spec.attention_chunk_size is not None
358
+ ) <= 1, (
359
+ "Model with both sliding window layers and chunked local attention "
360
+ "layers is not supported."
361
+ )
362
+ return merged_spec
363
+
364
+ @property
365
+ def real_page_size_bytes(self) -> int:
366
+ if self.kv_quant_mode.is_nvfp4:
367
+ # Packed layout per head: fp4 data + fp8 block scales.
368
+ # fp4 data: head_size//2 bytes (2 fp4 values per byte)
369
+ # fp8 block scale: head_size//16 bytes (1 scale per 16 elements)
370
+ last_dim = nvfp4_kv_cache_full_dim(
371
+ self.head_size
372
+ ) + nvfp4_kv_cache_full_dim(self.head_size_v)
373
+ elif self.kv_quant_mode == KVQuantMode.INT4_PER_TOKEN_HEAD:
374
+ last_dim = self.head_size // 2 + self.head_size_v // 2
375
+ else:
376
+ last_dim = self.head_size + self.head_size_v
377
+ return (
378
+ self.block_size * self.num_kv_heads * last_dim * get_dtype_size(self.dtype)
379
+ )
380
+
381
+
382
+ def _apply_alignment_padding(spec: MLAAttentionSpec | SlidingWindowMLASpec):
383
+ if spec.alignment is None:
384
+ return
385
+ actual_page_size = spec.real_page_size_bytes
386
+ padded_page_size = round_up(actual_page_size, spec.alignment)
387
+ if padded_page_size != actual_page_size:
388
+ object.__setattr__(spec, "page_size_padded", padded_page_size)
389
+
390
+
391
+ @dataclass(frozen=True, kw_only=True)
392
+ class TQFullAttentionSpec(FullAttentionSpec):
393
+ """FullAttentionSpec with TQ-aware page size.
394
+
395
+ Python equivalent of the C++ TQ4FullAttentionSpec. Overrides
396
+ real_page_size_bytes to use TQ slot bytes instead of the raw
397
+ head_size * dtype formula.
398
+ """
399
+
400
+ tq_slot_size: int = 0
401
+
402
+ @property
403
+ def real_page_size_bytes(self) -> int:
404
+ if self.tq_slot_size > 0:
405
+ return self.block_size * self.num_kv_heads * self.tq_slot_size
406
+ return super().real_page_size_bytes
407
+
408
+ @classmethod
409
+ def merge(cls, specs: list[Self]) -> Self:
410
+ merged = super().merge(specs)
411
+ assert all(s.tq_slot_size == specs[0].tq_slot_size for s in specs), (
412
+ "All TQ layers in the same KV cache group must use the same tq_slot_size."
413
+ )
414
+ return replace(merged, tq_slot_size=specs[0].tq_slot_size)
415
+
416
+
417
+ @dataclass(frozen=True, kw_only=True)
418
+ class MLAAttentionSpec(FullAttentionSpec):
419
+ # TODO(Lucas/Chen): less hacky way to do this
420
+ cache_dtype_str: str | None = None
421
+ # DeepseekV4 only fields. Non-DeepseekV4 MLA models leave these at defaults.
422
+ alignment: int | None = None # Default to None for no padding.
423
+ compress_ratio: int = 1 # Default to 1 for no compression.
424
+ model_version: str | None = None
425
+ # Marks draft groups that flatten a non-causal query block into decode rows.
426
+ non_causal_multi_token_decode: bool = False
427
+
428
+ def __post_init__(self):
429
+ super().__post_init__()
430
+ _apply_alignment_padding(self)
431
+
432
+ @property
433
+ def storage_block_size(self) -> int:
434
+ return self.block_size // self.compress_ratio
435
+
436
+ @property
437
+ def real_page_size_bytes(self) -> int:
438
+ if self.cache_dtype_str == "fp8_ds_mla":
439
+ if self.model_version == "deepseek_v4":
440
+ # DeepseekV4: 448B NoPE + 128B RoPE + 8B fp8 scale = 584B per token.
441
+ # head_size stays semantic (512); bytes are determined here.
442
+ return self.storage_block_size * 584
443
+ # V3.2 main MLA: 656-byte custom layout (kv_lora_rank=512 +
444
+ # qk_rope_head_dim=64, head_size=576). See flashmla_sparse.py.
445
+ # KVQ (glm-max): compact row layout for NoPE GLM-5.3 when
446
+ # VLLM_KVQ_TILES is set; kvq.row_bytes() returns 656 when unset.
447
+ from vllm import kvq
448
+
449
+ return self.block_size * kvq.row_bytes()
450
+ if self.kv_quant_mode == KVQuantMode.INT4_PER_TOKEN_HEAD:
451
+ head_dim = self.head_size // 2
452
+ else:
453
+ head_dim = self.head_size
454
+ return (
455
+ self.storage_block_size
456
+ * self.num_kv_heads
457
+ * head_dim
458
+ * get_dtype_size(self.dtype)
459
+ )
460
+
461
+ @classmethod
462
+ def merge(cls, specs: list[Self]) -> Self:
463
+ assert all(isinstance(spec, MLAAttentionSpec) for spec in specs), (
464
+ "All attention layers in the same KV cache group must be MLAAttentionSpec."
465
+ )
466
+ cache_dtype_str_set = set(spec.cache_dtype_str for spec in specs)
467
+ compress_ratio_set = set(spec.compress_ratio for spec in specs)
468
+ model_version_set = set(spec.model_version for spec in specs)
469
+ block_stride_set = set(spec.indexes_kv_by_block_stride for spec in specs)
470
+ assert (
471
+ len(cache_dtype_str_set) == 1
472
+ and len(compress_ratio_set) == 1
473
+ and len(model_version_set) == 1
474
+ and len(block_stride_set) == 1
475
+ ), (
476
+ "All attention layers in the same KV cache group must use the same "
477
+ "quantization method, compress ratio, model version, and KV block "
478
+ "stride indexing."
479
+ )
480
+ merged_spec = cls(
481
+ block_size=specs[0].block_size,
482
+ num_kv_heads=specs[0].num_kv_heads,
483
+ head_size=specs[0].head_size,
484
+ dtype=specs[0].dtype,
485
+ kv_quant_mode=specs[0].kv_quant_mode,
486
+ page_size_padded=specs[0].page_size_padded,
487
+ indexes_kv_by_block_stride=block_stride_set.pop(),
488
+ cache_dtype_str=cache_dtype_str_set.pop(),
489
+ compress_ratio=compress_ratio_set.pop(),
490
+ model_version=model_version_set.pop(),
491
+ non_causal_multi_token_decode=any(
492
+ spec.non_causal_multi_token_decode for spec in specs
493
+ ),
494
+ )
495
+ for spec in specs:
496
+ for f in fields(AttentionSpec):
497
+ assert getattr(spec, f.name) == getattr(merged_spec, f.name), (
498
+ "All attention layers in the same KV cache group must have "
499
+ "the same attention spec."
500
+ )
501
+ return merged_spec
502
+
503
+
504
+ @dataclass(frozen=True, kw_only=True)
505
+ class HiddenStateCacheSpec(MLAAttentionSpec):
506
+ """Marker for hidden-state cache layers used by extract_hidden_states."""
507
+
508
+ pass
509
+
510
+
511
+ @dataclass(frozen=True, kw_only=True)
512
+ class RSWASpec(FullAttentionSpec):
513
+ """KV cache spec for Reference Sliding Window Attention (R-SWA).
514
+
515
+ Prefill (image + text prompt) tokens are always globally visible.
516
+ Only the last ``rswa_window`` generated tokens are kept in the KV cache;
517
+ gap blocks (between the prefill tail and the current decode window) are
518
+ evicted during each decode step to bound memory at
519
+ O(prefix_blocks + window_blocks).
520
+ """
521
+
522
+ rswa_window: int
523
+
524
+ @classmethod
525
+ def merge(cls, specs: list[RSWASpec]) -> RSWASpec:
526
+ assert all(isinstance(spec, RSWASpec) for spec in specs), (
527
+ "All attention layers in the same KV cache group must be RSWASpec."
528
+ )
529
+ rswa_windows = {spec.rswa_window for spec in specs}
530
+ assert len(rswa_windows) == 1, (
531
+ f"All R-SWA layers must share the same rswa_window, got {rswa_windows}"
532
+ )
533
+ # Delegate common field merging to the parent, then reattach rswa_window.
534
+ base = FullAttentionSpec.merge(specs) # type: ignore[arg-type]
535
+ return cls(
536
+ block_size=base.block_size,
537
+ num_kv_heads=base.num_kv_heads,
538
+ head_size=base.head_size,
539
+ head_size_v=base.head_size_v,
540
+ dtype=base.dtype,
541
+ kv_quant_mode=base.kv_quant_mode,
542
+ page_size_padded=base.page_size_padded,
543
+ indexes_kv_by_block_stride=base.indexes_kv_by_block_stride,
544
+ sliding_window=base.sliding_window,
545
+ attention_chunk_size=base.attention_chunk_size,
546
+ non_causal=base.non_causal,
547
+ rswa_window=rswa_windows.pop(),
548
+ )
549
+
550
+
551
+ @dataclass(frozen=True, kw_only=True)
552
+ class ChunkedLocalAttentionSpec(AttentionSpec):
553
+ attention_chunk_size: int
554
+
555
+ def max_admission_blocks_per_request(
556
+ self, max_in_flight_tokens: int, max_model_len: int
557
+ ) -> int:
558
+ """Per-request admission cap, in blocks.
559
+
560
+ Single source of truth for both startup pool sizing
561
+ (`max_memory_usage_bytes`) and the runtime admission gate, so requests
562
+ admitted by startup can also be admitted at runtime.
563
+
564
+ `max_in_flight_tokens` is the max tokens scheduled but not yet settled
565
+ (one batch per concurrent step); see `VllmConfig.max_in_flight_tokens`.
566
+ """
567
+ # During chunked prefill, we hold KV for at most one chunk window plus
568
+ # the in-flight tokens, since frees happen on the processed-token basis.
569
+ num_tokens = min(
570
+ self.attention_chunk_size + max_in_flight_tokens, max_model_len
571
+ )
572
+ return cdiv(num_tokens, self.block_size)
573
+
574
+ def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
575
+ max_blocks = self.max_admission_blocks_per_request(
576
+ max_in_flight_tokens=vllm_config.max_in_flight_tokens,
577
+ max_model_len=vllm_config.model_config.max_model_len,
578
+ )
579
+ return max_blocks * self.page_size_bytes
580
+
581
+ def is_uniform_with_collection(
582
+ self, kv_cache_specs: dict[str, KVCacheSpec]
583
+ ) -> bool:
584
+ return all(
585
+ isinstance(spec, ChunkedLocalAttentionSpec)
586
+ and spec.attention_chunk_size == self.attention_chunk_size
587
+ for spec in kv_cache_specs.values()
588
+ )
589
+
590
+
591
+ @dataclass(frozen=True, kw_only=True)
592
+ class SlidingWindowSpec(AttentionSpec):
593
+ sliding_window: int
594
+ head_size_v: int = None # type: ignore[assignment]
595
+
596
+ def __post_init__(self):
597
+ if self.head_size_v is None:
598
+ object.__setattr__(self, "head_size_v", self.head_size)
599
+
600
+ @property
601
+ def real_page_size_bytes(self) -> int:
602
+ # Mirror ``FullAttentionSpec.real_page_size_bytes`` for NVFP4 KV cache.
603
+ if self.kv_quant_mode.is_nvfp4:
604
+ last_dim = nvfp4_kv_cache_full_dim(
605
+ self.head_size
606
+ ) + nvfp4_kv_cache_full_dim(self.head_size_v)
607
+ return (
608
+ self.block_size
609
+ * self.num_kv_heads
610
+ * last_dim
611
+ * get_dtype_size(self.dtype)
612
+ )
613
+ return (
614
+ self.block_size
615
+ * self.num_kv_heads
616
+ * (self.head_size + self.head_size_v)
617
+ * get_dtype_size(self.dtype)
618
+ )
619
+
620
+ def max_admission_blocks_per_request(
621
+ self, max_in_flight_tokens: int, max_model_len: int
622
+ ) -> int:
623
+ """Per-request admission cap, in blocks.
624
+
625
+ Single source of truth for both startup pool sizing
626
+ (`max_memory_usage_bytes`) and the runtime admission gate. Per-request
627
+ real-held blocks plateau at this bound because
628
+ `SlidingWindowManager.remove_skipped_blocks` runs from `allocate_slots`
629
+ before each chunk's `get_num_blocks_to_allocate`.
630
+
631
+ `max_in_flight_tokens` is the max tokens scheduled but not yet settled
632
+ (one batch per concurrent step); see `VllmConfig.max_in_flight_tokens`.
633
+ """
634
+ # During chunked prefill, we hold KV for the last `sliding_window-1`
635
+ # computed tokens plus the in-flight tokens (frees happen on the
636
+ # processed-token basis); never more than `max_model_len`.
637
+ num_tokens = min(self.sliding_window - 1 + max_in_flight_tokens, max_model_len)
638
+ # +1 because the sliding window may not start from the beginning of
639
+ # the block. E.g. block size 4 and num_token 4 needs two blocks
640
+ # [XXCD][EF] to store the 6-token window [CDEF].
641
+ return cdiv(num_tokens, self.block_size) + 1
642
+
643
+ def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
644
+ assert vllm_config.parallel_config.decode_context_parallel_size == 1, (
645
+ "DCP not support sliding window."
646
+ )
647
+ max_blocks = self.max_admission_blocks_per_request(
648
+ max_in_flight_tokens=vllm_config.max_in_flight_tokens,
649
+ max_model_len=vllm_config.model_config.max_model_len,
650
+ )
651
+ return max_blocks * self.page_size_bytes
652
+
653
+ def is_uniform_with_collection(
654
+ self, kv_cache_specs: dict[str, KVCacheSpec]
655
+ ) -> bool:
656
+ return all(
657
+ isinstance(spec, SlidingWindowSpec)
658
+ and spec.sliding_window == self.sliding_window
659
+ for spec in kv_cache_specs.values()
660
+ )
661
+
662
+
663
+ @dataclass(frozen=True, kw_only=True)
664
+ class SlidingWindowMLASpec(SlidingWindowSpec):
665
+ """Sliding window attention with MLA cache format."""
666
+
667
+ cache_dtype_str: str | None = None
668
+ # DeepseekV4-only: see MLAAttentionSpec.model_version.
669
+ alignment: int | None = None # Default to None for no padding.
670
+ compress_ratio: int = 1
671
+ model_version: str | None = None
672
+
673
+ def __post_init__(self):
674
+ _apply_alignment_padding(self)
675
+
676
+ @property
677
+ def storage_block_size(self) -> int:
678
+ return self.block_size // self.compress_ratio
679
+
680
+ @property
681
+ def real_page_size_bytes(self) -> int:
682
+ if self.model_version == "deepseek_v4" and self.cache_dtype_str == "fp8_ds_mla":
683
+ # DeepseekV4 FlashMLA: 448B NoPE + 128B RoPE + 8B fp8 scale = 584B
684
+ # per token. FlashInfer's contiguous bf16/fp8 cache falls through to
685
+ # the element-size formula below.
686
+ return self.storage_block_size * 584
687
+ assert self.model_version in (None, "deepseek_v4"), (
688
+ f"Unsupported model version: {self.model_version}"
689
+ )
690
+ return (
691
+ self.storage_block_size
692
+ * self.num_kv_heads
693
+ * self.head_size
694
+ * get_dtype_size(self.dtype)
695
+ )
696
+
697
+ @classmethod
698
+ def merge(cls, specs: list[Self]) -> Self:
699
+ assert all(isinstance(spec, SlidingWindowMLASpec) for spec in specs), (
700
+ "All attention layers in the same KV cache group must be "
701
+ "SlidingWindowMLASpec."
702
+ )
703
+ cache_dtype_str_set = set(spec.cache_dtype_str for spec in specs)
704
+ compress_ratio_set = set(spec.compress_ratio for spec in specs)
705
+ model_version_set = set(spec.model_version for spec in specs)
706
+ sliding_window_set = set(spec.sliding_window for spec in specs)
707
+ block_stride_set = set(spec.indexes_kv_by_block_stride for spec in specs)
708
+ assert (
709
+ len(cache_dtype_str_set) == 1
710
+ and len(compress_ratio_set) == 1
711
+ and len(model_version_set) == 1
712
+ and len(sliding_window_set) == 1
713
+ and len(block_stride_set) == 1
714
+ ), (
715
+ "All attention layers in the same KV cache group must use the same "
716
+ "quantization method, compress ratio, model version, sliding "
717
+ "window size, and KV block stride indexing."
718
+ )
719
+ return cls(
720
+ block_size=specs[0].block_size,
721
+ num_kv_heads=specs[0].num_kv_heads,
722
+ head_size=specs[0].head_size,
723
+ dtype=specs[0].dtype,
724
+ page_size_padded=specs[0].page_size_padded,
725
+ indexes_kv_by_block_stride=block_stride_set.pop(),
726
+ sliding_window=sliding_window_set.pop(),
727
+ cache_dtype_str=cache_dtype_str_set.pop(),
728
+ compress_ratio=compress_ratio_set.pop(),
729
+ model_version=model_version_set.pop(),
730
+ )
731
+
732
+ def is_uniform_with_collection(
733
+ self, kv_cache_specs: dict[str, KVCacheSpec]
734
+ ) -> bool:
735
+ return all(
736
+ isinstance(spec, SlidingWindowMLASpec)
737
+ and spec.sliding_window == self.sliding_window
738
+ for spec in kv_cache_specs.values()
739
+ )
740
+
741
+
742
+ @dataclass(frozen=True, kw_only=True)
743
+ class KpoolTailSpec(SlidingWindowSpec):
744
+ """GLM5Next kpool indexer tail cache: a paged circular buffer holding the
745
+ in-progress (incomplete) kpool's raw K + gate score.
746
+
747
+ One block of ``block_size`` (== ``index_kpool``) slots per request,
748
+ overwritten circularly by ``pos % kpool`` as decode/spec-decode advances.
749
+ Prefill writes the trailing incomplete pool's raw K+gate here (instead of
750
+ discarding it) so the boundary pool can be compressed correctly on the
751
+ decode side, including across PD disaggregation. Raw K is packed as the "K"
752
+ half of each block and the gate score as the "V" half
753
+ (``head_size_v == head_size``).
754
+
755
+ The SW base is reused only for its ``real_page_size_bytes`` formula and the
756
+ admission-cap wiring; the manager is swapped to ``KpoolTailManager`` via the
757
+ registry, because ``SlidingWindowManager``'s mid-pool eviction would destroy
758
+ the in-progress pool. Admission is fixed at 1 block/req regardless of
759
+ ``sliding_window``/``max_in_flight_tokens``: the in-progress pool is always
760
+ <= kpool tokens (completed pools are flushed), so MTP > kpool still fits in
761
+ one circularly-reused block.
762
+ """
763
+
764
+ def max_admission_blocks_per_request(
765
+ self, max_in_flight_tokens: int, max_model_len: int
766
+ ) -> int:
767
+ # The in-progress pool is <= kpool tokens == 1 block, circularly reused.
768
+ # No growth, no window sliding -> exactly one block per request, even
769
+ # under spec-decode (MTP > kpool): completed pools flush mid-step.
770
+ return 1
771
+
772
+ def max_num_blocks_per_req(self, vllm_config: VllmConfig, max_len: int) -> int:
773
+ # One block per request for the request's whole lifetime; caps the
774
+ # per-request block_table buffer the runner sizes via this method.
775
+ return 1
776
+
777
+ def is_uniform_with_collection(
778
+ self, kv_cache_specs: dict[str, KVCacheSpec]
779
+ ) -> bool:
780
+ return all(isinstance(spec, KpoolTailSpec) for spec in kv_cache_specs.values())
781
+
782
+ @property
783
+ def participates_in_prefix_caching(self) -> bool:
784
+ # The tail is a per-request circular scratch buffer (1 block/req,
785
+ # overwritten by ``pos % kpool``), not a real block allocator: its
786
+ # contents are transient and never shareable across requests. Exclude
787
+ # it from the structural prefix-caching machinery so it neither drags
788
+ # the global block size down to ``kpool`` nor caps the hybrid hit at 0.
789
+ # ``KpoolTailManager`` already no-ops the manager-level cache hooks.
790
+ return False
791
+
792
+
793
+ @dataclass(frozen=True)
794
+ class MambaSpec(KVCacheSpec):
795
+ shapes: tuple[tuple[int, ...], ...]
796
+ dtypes: tuple[torch.dtype]
797
+ page_size_padded: int | None = None
798
+ mamba_type: MambaAttentionBackendEnum = MambaAttentionBackendEnum.MAMBA2
799
+ mamba_cache_mode: str = "none"
800
+ num_speculative_blocks: int = 0
801
+
802
+ @property
803
+ def real_page_size_bytes(self) -> int:
804
+ return sum(
805
+ prod(shape) * get_dtype_size(dtype)
806
+ for (shape, dtype) in zip(self.shapes, self.dtypes)
807
+ )
808
+
809
+ @property
810
+ def page_size_bytes(self) -> int:
811
+ if self.page_size_padded is not None:
812
+ assert self.page_size_padded >= self.real_page_size_bytes
813
+ return self.page_size_padded
814
+ return self.real_page_size_bytes
815
+
816
+ def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
817
+ if vllm_config.cache_config.mamba_cache_mode == "all":
818
+ max_model_len = vllm_config.model_config.max_model_len
819
+ return (
820
+ cdiv(max_model_len, self.block_size) + self.num_speculative_blocks
821
+ ) * self.page_size_bytes
822
+ elif vllm_config.cache_config.mamba_cache_mode == "align":
823
+ return self.page_size_bytes * (2 + self.num_speculative_blocks)
824
+ else:
825
+ return self.page_size_bytes * (1 + self.num_speculative_blocks)
826
+
827
+ def max_num_blocks_per_req(self, vllm_config: VllmConfig, max_len: int) -> int:
828
+ # Mamba state is replicated across DCP/PCP ranks, never sharded, so
829
+ # no CP scaling applies.
830
+ if vllm_config.cache_config.mamba_cache_mode == "align":
831
+ # Block table rows are position-indexed over the full sequence
832
+ # even though only 2 + num_speculative_blocks state blocks are
833
+ # resident at a time (earlier states are nulled out by
834
+ # remove_skipped_blocks), so the row length must cover max_len
835
+ # rather than max_memory_usage_bytes.
836
+ return cdiv(max_len, self.block_size) + self.num_speculative_blocks
837
+ return cdiv(self.max_memory_usage_bytes(vllm_config), self.page_size_bytes)
838
+
839
+ def is_uniform_with_collection(
840
+ self, kv_cache_specs: dict[str, KVCacheSpec]
841
+ ) -> bool:
842
+ return all(
843
+ isinstance(spec, MambaSpec)
844
+ and spec.num_speculative_blocks == self.num_speculative_blocks
845
+ for spec in kv_cache_specs.values()
846
+ )
847
+
848
+
849
+ @dataclass(frozen=True)
850
+ class EncoderOnlyAttentionSpec(AttentionSpec):
851
+ def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
852
+ # Encoder-only layers do not need KV cache
853
+ return 0
854
+
855
+
856
+ @dataclass(frozen=True)
857
+ class CrossAttentionSpec(AttentionSpec):
858
+ """
859
+ KV cache spec for cross-attention layers in encoder-decoder models.
860
+ """
861
+
862
+ def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
863
+ # For cross-attention, we need to cache encoder states
864
+ # Get encoder length (e.g., 1500 for Whisper).
865
+ max_encoder_len = vllm_config.scheduler_config.max_num_encoder_input_tokens
866
+ return cdiv(max_encoder_len, self.block_size) * self.page_size_bytes
867
+
868
+
869
+ @dataclass(frozen=True)
870
+ class SinkFullAttentionSpec(FullAttentionSpec):
871
+ sink_len: int | None = None
872
+
873
+ @classmethod
874
+ def merge(cls, specs: list[Self]) -> Self:
875
+ """
876
+ Merge a list of FullAttentionSpec objects into a single
877
+ FullAttentionSpec object.
878
+ """
879
+ assert all(isinstance(spec, FullAttentionSpec) for spec in specs), (
880
+ "All attention layers in the same KV cache group must be FullAttentionSpec."
881
+ )
882
+
883
+ sliding_window = set(
884
+ spec.sliding_window for spec in specs if spec.sliding_window is not None
885
+ )
886
+ attention_chunk_size = set(
887
+ spec.attention_chunk_size
888
+ for spec in specs
889
+ if spec.attention_chunk_size is not None
890
+ )
891
+ assert not any(isinstance(spec, MLAAttentionSpec) for spec in specs), (
892
+ "MLAAttentionSpec should be merged in MLAAttentionSpec.merge"
893
+ )
894
+ merged_spec = cls(
895
+ block_size=specs[0].block_size,
896
+ num_kv_heads=specs[0].num_kv_heads,
897
+ head_size=specs[0].head_size,
898
+ head_size_v=specs[0].head_size_v,
899
+ sink_len=specs[0].sink_len,
900
+ dtype=specs[0].dtype,
901
+ kv_quant_mode=specs[0].kv_quant_mode,
902
+ page_size_padded=specs[0].page_size_padded,
903
+ indexes_kv_by_block_stride=specs[0].indexes_kv_by_block_stride,
904
+ sliding_window=cls.merge_window_sizes(sliding_window),
905
+ attention_chunk_size=cls.merge_window_sizes(attention_chunk_size),
906
+ non_causal=any(spec.non_causal for spec in specs),
907
+ )
908
+ for spec in specs:
909
+ for f in fields(AttentionSpec):
910
+ assert getattr(spec, f.name) == getattr(merged_spec, f.name), (
911
+ "All attention layers in the same KV cache group must have "
912
+ "the same attention spec."
913
+ )
914
+ assert (merged_spec.sliding_window is not None) + (
915
+ merged_spec.attention_chunk_size is not None
916
+ ) <= 1, (
917
+ "Model with both sliding window layers and chunked local attention "
918
+ "layers is not supported."
919
+ )
920
+ return merged_spec
921
+
922
+
923
+ @dataclass(frozen=True)
924
+ class UniformTypeKVCacheSpecs(KVCacheSpec):
925
+ """
926
+ A KV cache spec for multiple layers with the same type of attention. Here,
927
+ same types means always need the same number of token slots. For example,
928
+ sliding window attentions with different window sizes are not the same type
929
+ and should not be merged into one UniformTypeKVCacheSpecs.
930
+ """
931
+
932
+ kv_cache_specs: dict[str, KVCacheSpec]
933
+
934
+ @property
935
+ def participates_in_prefix_caching(self) -> bool:
936
+ # A uniform group shares one block allocator; if any member is a
937
+ # non-shareable scratch spec (e.g. KpoolTailSpec), the group's blocks
938
+ # can't be reused across requests, so the whole group opts out.
939
+ return all(
940
+ s.participates_in_prefix_caching for s in self.kv_cache_specs.values()
941
+ )
942
+
943
+ @property
944
+ def page_size_bytes(self) -> int:
945
+ return sum(spec.page_size_bytes for spec in self.kv_cache_specs.values())
946
+
947
+ def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
948
+ max_num_pages = max(
949
+ cdiv(spec.max_memory_usage_bytes(vllm_config), spec.page_size_bytes)
950
+ for spec in self.kv_cache_specs.values()
951
+ )
952
+ return max_num_pages * self.page_size_bytes
953
+
954
+ def max_num_blocks_per_req(self, vllm_config: VllmConfig, max_len: int) -> int:
955
+ # Metadata builders are constructed from the per-layer spec, so the base
956
+ # cdiv(max_len, block_size) would drop its DCP sharding and size the
957
+ # block table wider than those builders expect.
958
+ widths = {
959
+ spec.max_num_blocks_per_req(vllm_config, max_len)
960
+ for spec in self.kv_cache_specs.values()
961
+ }
962
+ assert len(widths) == 1, (
963
+ "All layers in the same KV cache group must need the same number "
964
+ f"of block table entries, got {sorted(widths)}."
965
+ )
966
+ return next(iter(widths))
967
+
968
+ @classmethod
969
+ def is_uniform_type(cls, kv_cache_specs: dict[str, KVCacheSpec]) -> bool:
970
+ """
971
+ Whether all layers have the same type of KV cache spec.
972
+
973
+ Uses the registry to determine grouping base classes, so custom specs
974
+ that inherit from FullAttentionSpec are treated as full attention.
975
+ """
976
+ block_sizes = set(spec.block_size for spec in kv_cache_specs.values())
977
+ if len(block_sizes) > 1:
978
+ # Different block sizes, not uniform.
979
+ return False
980
+ first_spec = next(iter(kv_cache_specs.values()))
981
+ return first_spec.is_uniform_with_collection(kv_cache_specs)
982
+
983
+ @classmethod
984
+ def from_specs(cls, kv_cache_specs: dict[str, KVCacheSpec]) -> Self | None:
985
+ """
986
+ Return a SameTypeKVCacheSpecs object if all layers have the same type
987
+ of KV cache spec. Return None if not.
988
+ """
989
+ if cls.is_uniform_type(kv_cache_specs):
990
+ block_size = next(iter(kv_cache_specs.values())).block_size
991
+ return cls(block_size=block_size, kv_cache_specs=kv_cache_specs)
992
+ else:
993
+ return None
994
+
995
+ # NOTE: below util functions are only used by DeepseekV4 for now.
996
+ def get_page_sizes(self) -> list[int]:
997
+ return list(set(spec.page_size_bytes for spec in self.kv_cache_specs.values()))
998
+
999
+ def get_num_layer_tuples(self) -> int:
1000
+ return Counter(
1001
+ spec.page_size_bytes for spec in self.kv_cache_specs.values()
1002
+ ).most_common(1)[0][1]
1003
+
1004
+ def max_memory_usage_pages(self, vllm_config: VllmConfig) -> int:
1005
+ return max(
1006
+ cdiv(spec.max_memory_usage_bytes(vllm_config), spec.page_size_bytes)
1007
+ for spec in self.kv_cache_specs.values()
1008
+ )
1009
+
1010
+
1011
+ def get_kv_cache_spec_kind(kv_cache_spec: KVCacheSpec) -> KVCacheSpecKind:
1012
+ if isinstance(kv_cache_spec, UniformTypeKVCacheSpecs):
1013
+ inner_kinds = {
1014
+ get_kv_cache_spec_kind(spec)
1015
+ for spec in kv_cache_spec.kv_cache_specs.values()
1016
+ }
1017
+ if len(inner_kinds) == 1:
1018
+ return next(iter(inner_kinds))
1019
+ # A group is only formed when all members share one registered
1020
+ # uniform_type_base_spec, so UNKNOWN would discard what the merge
1021
+ # already established.
1022
+ base_specs = {
1023
+ KVCacheSpecRegistry.get_uniform_type_base_spec(spec)
1024
+ for spec in kv_cache_spec.kv_cache_specs.values()
1025
+ }
1026
+ if len(base_specs) == 1 and next(iter(base_specs)) is FullAttentionSpec:
1027
+ return KVCacheSpecKind.FULL_ATTENTION
1028
+ return KVCacheSpecKind.UNKNOWN
1029
+ # Keep subclass checks before base classes so specialized specs keep their
1030
+ # more precise kind.
1031
+ if isinstance(kv_cache_spec, SlidingWindowMLASpec):
1032
+ return KVCacheSpecKind.SLIDING_WINDOW_MLA
1033
+ if isinstance(kv_cache_spec, MLAAttentionSpec):
1034
+ return KVCacheSpecKind.MLA_ATTENTION
1035
+ if isinstance(kv_cache_spec, SinkFullAttentionSpec):
1036
+ return KVCacheSpecKind.SINK_FULL_ATTENTION
1037
+ if isinstance(kv_cache_spec, FullAttentionSpec):
1038
+ return KVCacheSpecKind.FULL_ATTENTION
1039
+ if isinstance(kv_cache_spec, ChunkedLocalAttentionSpec):
1040
+ return KVCacheSpecKind.CHUNKED_LOCAL_ATTENTION
1041
+ if isinstance(kv_cache_spec, SlidingWindowSpec):
1042
+ return KVCacheSpecKind.SLIDING_WINDOW
1043
+ if isinstance(kv_cache_spec, MambaSpec):
1044
+ return KVCacheSpecKind.MAMBA
1045
+ if isinstance(kv_cache_spec, EncoderOnlyAttentionSpec):
1046
+ return KVCacheSpecKind.ENCODER_ONLY_ATTENTION
1047
+ if isinstance(kv_cache_spec, CrossAttentionSpec):
1048
+ return KVCacheSpecKind.CROSS_ATTENTION
1049
+ return KVCacheSpecKind.UNKNOWN
1050
+
1051
+
1052
+ def get_kv_cache_spec_sliding_window(kv_cache_spec: KVCacheSpec) -> int | None:
1053
+ if isinstance(kv_cache_spec, UniformTypeKVCacheSpecs):
1054
+ inner_windows = {
1055
+ get_kv_cache_spec_sliding_window(spec)
1056
+ for spec in kv_cache_spec.kv_cache_specs.values()
1057
+ }
1058
+ return next(iter(inner_windows)) if len(inner_windows) == 1 else None
1059
+ if isinstance(kv_cache_spec, SlidingWindowSpec):
1060
+ return kv_cache_spec.sliding_window
1061
+ return None
1062
+
1063
+
1064
+ @dataclass
1065
+ class KVCacheTensor:
1066
+ """
1067
+ A class for specifying how the workers should initialize the KV cache.
1068
+ """
1069
+
1070
+ size: int # size of the KV cache tensor in bytes
1071
+ shared_by: list[str] # layer names that share the same KV cache tensor
1072
+ offset: int = 0 # byte offset of this layer within a contiguous block
1073
+ block_stride: int = 0 # total bytes per block in a packed layout (0 = not packed)
1074
+
1075
+
1076
+ @dataclass
1077
+ class KVCacheGroupSpec:
1078
+ """
1079
+ Represents a group of model layers that share the same KV cache block table.
1080
+ These layers are regarded as one layer in the KV cache manager.
1081
+ """
1082
+
1083
+ # The names of model layers in this group
1084
+ layer_names: list[str]
1085
+ # The KV cache spec of this manager layer
1086
+ kv_cache_spec: KVCacheSpec
1087
+ # Whether this group contains EAGLE/MTP draft attention layers.
1088
+ is_eagle_group: bool = False
1089
+
1090
+
1091
+ @dataclass
1092
+ class KVCacheConfig:
1093
+ """
1094
+ The KV cache configuration of a model.
1095
+ """
1096
+
1097
+ num_blocks: int
1098
+ """The number of KV cache blocks"""
1099
+ kv_cache_tensors: list[KVCacheTensor]
1100
+ """How should model runner initialize the KV cache tensors for each layer"""
1101
+ kv_cache_groups: list[KVCacheGroupSpec]
1102
+ """
1103
+ The kv cache groups of the model.
1104
+ For models with only one type of attention, there is only one group that
1105
+ contains all layers.
1106
+ For models with multiple types of attention, there will be multiple groups,
1107
+ see `_get_kv_cache_config_uniform_page_size` for more details.
1108
+ """
1109
+
1110
+ @property
1111
+ def has_mamba_layers(self) -> bool:
1112
+ return any(isinstance(g.kv_cache_spec, MambaSpec) for g in self.kv_cache_groups)
1113
+
1114
+ @property
1115
+ def has_mixed_precision_kv_cache(self) -> bool:
1116
+ """Whether attention groups store their KV cache at more than one precision."""
1117
+ kv_cache_precisions: set[tuple[torch.dtype, KVQuantMode]] = set()
1118
+ for group in self.kv_cache_groups:
1119
+ group_spec = group.kv_cache_spec
1120
+ group_specs = (
1121
+ list(group_spec.kv_cache_specs.values())
1122
+ if isinstance(group_spec, UniformTypeKVCacheSpecs)
1123
+ else [group_spec]
1124
+ )
1125
+ kv_cache_precisions.update(
1126
+ (spec.dtype, spec.kv_quant_mode)
1127
+ for spec in group_specs
1128
+ if isinstance(spec, AttentionSpec)
1129
+ )
1130
+ return len(kv_cache_precisions) > 1
1131
+
1132
+ @property
1133
+ def needs_kv_cache_zeroing(self) -> bool:
1134
+ """Whether newly allocated KV cache blocks must be zeroed before use.
1135
+
1136
+ Required for Mamba layers, whose state is read before it is fully written
1137
+ (#35219), and for mixed-precision caches, where a block reused across
1138
+ groups can be reinterpreted under a different precision and decode stale
1139
+ bytes to NaN/Inf. Uniform-precision caches skip zeroing.
1140
+ """
1141
+ return self.has_mamba_layers or self.has_mixed_precision_kv_cache
serving/everything-lane/kvq.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """KVQ (worker KVQ, glm-max): compact MLA latent KV-cache row layout.
3
+
4
+ Replaces the fp8_ds_mla 656-byte per-token row (512B fp8 nope + 16B fp32
5
+ tile scales + 128B bf16 rope) with a compact row for the NoPE GLM-5.3-Flash
6
+ geometry (qk_rope_head_dim=0 -> the rope slot is provably inert: q is
7
+ zero-padded and V has no rope component).
8
+
9
+ Row layout (per token), driven by VLLM_KVQ_TILES (4 chars, one per 128-dim
10
+ tile of the 512-dim latent, each in {8,6,4}):
11
+
12
+ [0:16) 4 x fp32 per-128-tile scales (identical semantics to fp8_ds_mla)
13
+ then per tile, in order:
14
+ '8' (fp8 e4m3): 128 B data (bit-identical to the fp8_ds_mla bytes)
15
+ '6' (fp6 e2m3): 4 B per-32-group exponents (u8, bias 64)
16
+ + 64 B low-nibble plane + 32 B high-2-bit plane
17
+ '4' (fp4 e2m1): 4 B per-32-group exponents + 64 B nibble data
18
+
19
+ Dual-level scaling: value = grid_val * 2^group_exp * tile_scale.
20
+ fp6 e2m3 has the same 3-bit mantissa as fp8 e4m3, so dequantising a stored
21
+ fp6 value to fp8 (for the FlashInfer decode kernel's staged 656-byte rows)
22
+ is exact -- the only quantisation error is the one taken at write time.
23
+
24
+ Env:
25
+ VLLM_KVQ_TILES e.g. "8888" (drop rope slot only, bit-identical),
26
+ "6666" (fp6), "8444" (1 fp8 + 3 fp4). Unset/empty ->
27
+ stock fp8_ds_mla behaviour (656 B, CUDA ops, no
28
+ staging) -- the patch is inert.
29
+ VLLM_KVQ_DECODE_QLEN max per-request query length treated as decode by
30
+ the sparse-MLA metadata builder when KVQ is active
31
+ (bounds the decode staging buffer). Default 4.
32
+ """
33
+
34
+ import os
35
+ from functools import lru_cache
36
+
37
+ ROPE_BYTES = 128
38
+ CANONICAL_ROW = 656 # fp8_ds_mla: 512 + 16 + 128
39
+ SCALES_BYTES = 16 # 4 x fp32
40
+ N_TILES = 4
41
+ TILE_DIMS = 128
42
+ GROUPS_PER_TILE = 4 # 32 dims per group
43
+
44
+ _TILE_BYTES = {"8": 128, "6": GROUPS_PER_TILE + 64 + 32, "4": GROUPS_PER_TILE + 64}
45
+
46
+
47
+ @lru_cache(maxsize=1)
48
+ def tiles() -> str | None:
49
+ s = os.environ.get("VLLM_KVQ_TILES", "").strip()
50
+ if not s:
51
+ return None
52
+ if len(s) != N_TILES or any(c not in _TILE_BYTES for c in s):
53
+ raise ValueError(
54
+ f"VLLM_KVQ_TILES must be {N_TILES} chars from {{8,6,4}}, got {s!r}"
55
+ )
56
+ return s
57
+
58
+
59
+ def enabled() -> bool:
60
+ return tiles() is not None
61
+
62
+
63
+ @lru_cache(maxsize=1)
64
+ def row_bytes() -> int:
65
+ t = tiles()
66
+ if t is None:
67
+ return CANONICAL_ROW
68
+ return SCALES_BYTES + sum(_TILE_BYTES[c] for c in t)
69
+
70
+
71
+ @lru_cache(maxsize=1)
72
+ def tile_offsets() -> tuple[tuple[int, int], ...]:
73
+ """Per tile: (format as int, byte offset of the tile's region)."""
74
+ t = tiles()
75
+ assert t is not None
76
+ offs = []
77
+ off = SCALES_BYTES
78
+ for c in t:
79
+ offs.append((int(c), off))
80
+ off += _TILE_BYTES[c]
81
+ return tuple(offs)
82
+
83
+
84
+ @lru_cache(maxsize=1)
85
+ def decode_qlen() -> int:
86
+ return int(os.environ.get("VLLM_KVQ_DECODE_QLEN", "4"))
serving/everything-lane/kvq_kernels.py ADDED
@@ -0,0 +1,527 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Triton kernels for the KVQ compact MLA KV-cache row layout (see kvq.py).
3
+
4
+ Three entry points:
5
+ quant_write : bf16 latent rows -> compact cache rows (write path,
6
+ replaces ops.concat_and_cache_mla for fp8_ds_mla)
7
+ gather_dequant : compact cache rows -> bf16 workspace rows (prefill
8
+ context path, replaces ops.gather_and_maybe_dequant_cache
9
+ / ops.cp_gather_and_upconvert_fp8_kv_cache)
10
+ stage_fp8 : top-k compact rows -> canonical 656-byte fp8_ds_mla rows
11
+ in a small staging cache consumed verbatim by the
12
+ FlashInfer SM120 sparse-MLA decode kernel.
13
+
14
+ Quantisation (write): per 128-dim tile, fp32 scale = max(amax/448, eps) as in
15
+ the fp8_ds_mla CUDA path; '8' tiles store the e4m3 bytes of x/scale.
16
+ '6'/'4' tiles additionally choose a per-32-group power-of-2 exponent e so
17
+ |x/scale| * 2^-e fits the e2m3 (max 7.5) / e2m1 (max 6) grid, then round
18
+ RNE onto that grid. Dequant multiplies by 2^e (exact in e4m3 for the '6'
19
+ grid; e2m1 values are also exact e4m3 values), so staging back to fp8 adds
20
+ NO extra rounding: the only loss is the write-time quantisation.
21
+ """
22
+
23
+ import torch
24
+ import triton
25
+ import triton.language as tl
26
+ from triton.language.extra import libdevice
27
+
28
+ from vllm import kvq
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # jit helpers
32
+ # ---------------------------------------------------------------------------
33
+
34
+
35
+ @triton.jit
36
+ def _e2m3_quant(az):
37
+ """RNE of az (>=0, <=7.5+eps) onto the e2m3 grid; returns grid value."""
38
+ # normal range [1, 7.5]: identical grid to e4m3 (3-bit mantissa)
39
+ q_norm = az.to(tl.float8e4nv).to(tl.float32)
40
+ # subnormal range [0, 1): step 0.125; rint(7.5)=8 -> 1.0 (correct RNE tie)
41
+ q_sub = libdevice.rint(az * 8.0) * 0.125
42
+ return tl.where(az >= 1.0, q_norm, q_sub)
43
+
44
+
45
+ @triton.jit
46
+ def _e2m3_encode(q):
47
+ """6-bit code of a grid value q (signed)."""
48
+ aq = tl.abs(q)
49
+ s = (q < 0).to(tl.int32)
50
+ is_norm = aq >= 1.0
51
+ aq_safe = tl.maximum(aq, 1.0)
52
+ eb = tl.floor(tl.log2(aq_safe)).to(tl.int32) + 1 # 1..3
53
+ eb = tl.minimum(tl.maximum(eb, 1), 3)
54
+ m_norm = libdevice.rint((aq_safe * tl.exp2(-(eb - 1).to(tl.float32)) - 1.0) * 8.0)
55
+ m_sub = libdevice.rint(aq * 8.0)
56
+ eb = tl.where(is_norm, eb, 0)
57
+ m = tl.where(is_norm, m_norm, m_sub).to(tl.int32)
58
+ m = tl.minimum(tl.maximum(m, 0), 7)
59
+ return (s << 5) | (eb << 3) | m
60
+
61
+
62
+ @triton.jit
63
+ def _e2m3_decode(code):
64
+ """Grid value from 6-bit code."""
65
+ s = (code >> 5) & 1
66
+ eb = (code >> 3) & 3
67
+ m = (code & 7).to(tl.float32)
68
+ val = tl.where(
69
+ eb == 0,
70
+ m * 0.125,
71
+ (1.0 + m * 0.125) * tl.exp2((eb - 1).to(tl.float32)),
72
+ )
73
+ return tl.where(s == 1, -val, val)
74
+
75
+
76
+ @triton.jit
77
+ def _e2m1_quant(az):
78
+ """RNE of az (>=0, <=6+eps) onto the e2m1 grid {0,.5,1,1.5,2,3,4,6}."""
79
+ return tl.where(
80
+ az > 5.0,
81
+ 6.0,
82
+ tl.where(
83
+ az >= 3.5,
84
+ 4.0,
85
+ tl.where(
86
+ az > 2.5,
87
+ 3.0,
88
+ tl.where(
89
+ az >= 1.75,
90
+ 2.0,
91
+ tl.where(
92
+ az > 1.25,
93
+ 1.5,
94
+ tl.where(az >= 0.75, 1.0, tl.where(az > 0.25, 0.5, 0.0)),
95
+ ),
96
+ ),
97
+ ),
98
+ ),
99
+ )
100
+
101
+
102
+ @triton.jit
103
+ def _e2m1_encode(q):
104
+ """4-bit code of a grid value q (signed)."""
105
+ aq = tl.abs(q)
106
+ s = (q < 0).to(tl.int32)
107
+ is_norm = aq >= 1.0
108
+ aq_safe = tl.maximum(aq, 1.0)
109
+ e = tl.floor(tl.log2(aq_safe)).to(tl.int32)
110
+ e = tl.minimum(tl.maximum(e, 0), 2)
111
+ mm = libdevice.rint(aq_safe * tl.exp2(-e.to(tl.float32)) * 2.0 - 2.0).to(tl.int32)
112
+ mm = tl.minimum(tl.maximum(mm, 0), 1)
113
+ code_norm = 2 + e * 2 + mm
114
+ code_sub = libdevice.rint(aq * 2.0).to(tl.int32) # 0 or 1
115
+ code = tl.where(is_norm, code_norm, code_sub)
116
+ return (s << 3) | code
117
+
118
+
119
+ @triton.jit
120
+ def _e2m1_decode(code):
121
+ s = (code >> 3) & 1
122
+ m = code & 7
123
+ e = m >> 1
124
+ mm = (m & 1).to(tl.float32)
125
+ val = tl.where(
126
+ m < 2,
127
+ m.to(tl.float32) * 0.5,
128
+ (1.0 + 0.5 * mm) * tl.exp2((e - 1).to(tl.float32)),
129
+ )
130
+ return tl.where(s == 1, -val, val)
131
+
132
+
133
+ @triton.jit
134
+ def _group_exps(y, GMAX: tl.constexpr, EHI: tl.constexpr):
135
+ """Per-32-group power-of-2 exponent for a [128] tile normalised to
136
+ [-448, 448]. Returns ([4] int32 exps, [128] float32 scaled values)."""
137
+ y2d = tl.reshape(y, (4, 32))
138
+ gmax = tl.max(tl.abs(y2d), axis=1) # [4]
139
+ e = tl.ceil(tl.log2(tl.maximum(gmax, 1e-30) / GMAX) - 1e-6).to(tl.int32)
140
+ e = tl.minimum(tl.maximum(e, -6), EHI)
141
+ z2d = y2d * tl.exp2(-e.to(tl.float32))[:, None]
142
+ return e, tl.reshape(z2d, (128,))
143
+
144
+
145
+ @triton.jit
146
+ def _pack2x4(vals4bit):
147
+ """[128] int32 (each 0..15) -> [64] uint8, byte i = v[2i] | v[2i+1]<<4."""
148
+ v2d = tl.reshape(vals4bit, (64, 2))
149
+ lo, hi = tl.split(v2d)
150
+ return (lo | (hi << 4)).to(tl.uint8)
151
+
152
+
153
+ @triton.jit
154
+ def _pack4x2(vals2bit):
155
+ """[128] int32 (each 0..3) -> [32] uint8, byte j = v[4j] | v[4j+1]<<2 |..."""
156
+ v3d = tl.reshape(vals2bit, (32, 2, 2))
157
+ a2, b2 = tl.split(v3d) # [32,2] each: a2 = v[4j],v[4j+2]? no: see order below
158
+ a, c = tl.split(a2)
159
+ b, d = tl.split(b2)
160
+ # reshape order: v3d[j, p, q] = vals[4j + 2p + q]
161
+ # a2 = v3d[:, :, 0] -> [32,2] = (v[4j], v[4j+2]); b2 = (v[4j+1], v[4j+3])
162
+ # a = v[4j], c = v[4j+2], b = v[4j+1], d = v[4j+3]
163
+ return (a | (b << 2) | (c << 4) | (d << 6)).to(tl.uint8)
164
+
165
+
166
+ @triton.jit
167
+ def _store_tile(
168
+ cache_ptr, row_off, x, scale, FMT: tl.constexpr, OFF: tl.constexpr
169
+ ):
170
+ """Quantise one [128] tile (float32) and store at row_off + OFF."""
171
+ d = tl.arange(0, 128)
172
+ y = x / scale
173
+ if FMT == 8:
174
+ q = y.to(tl.float8e4nv)
175
+ tl.store(cache_ptr + row_off + OFF + d, q.to(tl.uint8, bitcast=True))
176
+ elif FMT == 6:
177
+ e, z = _group_exps(y, 7.5, 6)
178
+ tl.store(
179
+ cache_ptr + row_off + OFF + tl.arange(0, 4), (e + 64).to(tl.uint8)
180
+ )
181
+ q = _e2m3_quant(tl.abs(z))
182
+ q = tl.where(z < 0, -q, q)
183
+ code = _e2m3_encode(q)
184
+ tl.store(cache_ptr + row_off + OFF + 4 + tl.arange(0, 64), _pack2x4(code & 0xF))
185
+ tl.store(
186
+ cache_ptr + row_off + OFF + 4 + 64 + tl.arange(0, 32), _pack4x2(code >> 4)
187
+ )
188
+ else: # FMT == 4
189
+ e, z = _group_exps(y, 6.0, 7)
190
+ tl.store(
191
+ cache_ptr + row_off + OFF + tl.arange(0, 4), (e + 64).to(tl.uint8)
192
+ )
193
+ q = _e2m1_quant(tl.abs(z))
194
+ q = tl.where(z < 0, -q, q)
195
+ code = _e2m1_encode(q)
196
+ tl.store(cache_ptr + row_off + OFF + 4 + tl.arange(0, 64), _pack2x4(code))
197
+
198
+
199
+ @triton.jit
200
+ def _load_tile_values(cache_ptr, row_off, FMT: tl.constexpr, OFF: tl.constexpr):
201
+ """Load one stored tile; return [128] float32 of grid_val * 2^group_exp
202
+ (i.e. the value BEFORE the fp32 tile scale is applied)."""
203
+ d = tl.arange(0, 128)
204
+ if FMT == 8:
205
+ raw = tl.load(cache_ptr + row_off + OFF + d)
206
+ return raw.to(tl.float8e4nv, bitcast=True).to(tl.float32)
207
+ else:
208
+ e_u8 = tl.load(cache_ptr + row_off + OFF + (d >> 5))
209
+ ge = e_u8.to(tl.int32) - 64
210
+ if FMT == 6:
211
+ nib_b = tl.load(cache_ptr + row_off + OFF + 4 + (d >> 1))
212
+ nib = (nib_b.to(tl.int32) >> ((d & 1) * 4)) & 0xF
213
+ hi_b = tl.load(cache_ptr + row_off + OFF + 4 + 64 + (d >> 2))
214
+ hi = (hi_b.to(tl.int32) >> ((d & 3) * 2)) & 0x3
215
+ val = _e2m3_decode(nib | (hi << 4))
216
+ else: # FMT == 4
217
+ nib_b = tl.load(cache_ptr + row_off + OFF + 4 + (d >> 1))
218
+ code = (nib_b.to(tl.int32) >> ((d & 1) * 4)) & 0xF
219
+ val = _e2m1_decode(code)
220
+ # Saturate like the e4m3 staging cast so the prefill (bf16) and
221
+ # decode (staged fp8) paths dequantise identically: with the fp4
222
+ # group exponent reaching 7, code 4 * 2^7 = 512 saturates to 448.
223
+ v = val * tl.exp2(ge.to(tl.float32))
224
+ return tl.minimum(tl.maximum(v, -448.0), 448.0)
225
+
226
+
227
+ # ---------------------------------------------------------------------------
228
+ # kernels
229
+ # ---------------------------------------------------------------------------
230
+
231
+
232
+ @triton.jit
233
+ def _quant_write_kernel(
234
+ latent_ptr,
235
+ latent_stride,
236
+ slots_ptr,
237
+ cache_ptr,
238
+ cache_f32_ptr,
239
+ ROW: tl.constexpr,
240
+ F0: tl.constexpr,
241
+ F1: tl.constexpr,
242
+ F2: tl.constexpr,
243
+ F3: tl.constexpr,
244
+ O0: tl.constexpr,
245
+ O1: tl.constexpr,
246
+ O2: tl.constexpr,
247
+ O3: tl.constexpr,
248
+ ):
249
+ i = tl.program_id(0)
250
+ slot = tl.load(slots_ptr + i).to(tl.int64)
251
+ if slot < 0:
252
+ return
253
+ row_off = slot * ROW
254
+ d = tl.arange(0, 128)
255
+ src = latent_ptr + i.to(tl.int64) * latent_stride
256
+
257
+ x0 = tl.load(src + 0 * 128 + d).to(tl.float32)
258
+ x1 = tl.load(src + 1 * 128 + d).to(tl.float32)
259
+ x2 = tl.load(src + 2 * 128 + d).to(tl.float32)
260
+ x3 = tl.load(src + 3 * 128 + d).to(tl.float32)
261
+
262
+ s0 = tl.maximum(tl.max(tl.abs(x0)) / 448.0, 1e-12)
263
+ s1 = tl.maximum(tl.max(tl.abs(x1)) / 448.0, 1e-12)
264
+ s2 = tl.maximum(tl.max(tl.abs(x2)) / 448.0, 1e-12)
265
+ s3 = tl.maximum(tl.max(tl.abs(x3)) / 448.0, 1e-12)
266
+
267
+ scale_base = (slot * ROW) // 4
268
+ tl.store(cache_f32_ptr + scale_base + 0, s0)
269
+ tl.store(cache_f32_ptr + scale_base + 1, s1)
270
+ tl.store(cache_f32_ptr + scale_base + 2, s2)
271
+ tl.store(cache_f32_ptr + scale_base + 3, s3)
272
+
273
+ _store_tile(cache_ptr, row_off, x0, s0, F0, O0)
274
+ _store_tile(cache_ptr, row_off, x1, s1, F1, O1)
275
+ _store_tile(cache_ptr, row_off, x2, s2, F2, O2)
276
+ _store_tile(cache_ptr, row_off, x3, s3, F3, O3)
277
+
278
+
279
+ @triton.jit
280
+ def _gather_dequant_kernel(
281
+ cache_ptr,
282
+ cache_f32_ptr,
283
+ dst_ptr,
284
+ dst_stride,
285
+ block_table_ptr,
286
+ bt_stride,
287
+ cu_seq_lens_ptr,
288
+ token_to_seq_ptr,
289
+ seq_starts_ptr,
290
+ block_size,
291
+ ROW: tl.constexpr,
292
+ F0: tl.constexpr,
293
+ F1: tl.constexpr,
294
+ F2: tl.constexpr,
295
+ F3: tl.constexpr,
296
+ O0: tl.constexpr,
297
+ O1: tl.constexpr,
298
+ O2: tl.constexpr,
299
+ O3: tl.constexpr,
300
+ ):
301
+ i = tl.program_id(0)
302
+ r = tl.load(token_to_seq_ptr + i)
303
+ pos = i - tl.load(cu_seq_lens_ptr + r) + tl.load(seq_starts_ptr + r)
304
+ blk = tl.load(block_table_ptr + r.to(tl.int64) * bt_stride + pos // block_size)
305
+ slot = blk.to(tl.int64) * block_size + (pos % block_size).to(tl.int64)
306
+ row_off = slot * ROW
307
+ scale_base = row_off // 4
308
+
309
+ d = tl.arange(0, 128)
310
+ dst = dst_ptr + i.to(tl.int64) * dst_stride
311
+
312
+ v0 = _load_tile_values(cache_ptr, row_off, F0, O0)
313
+ s0 = tl.load(cache_f32_ptr + scale_base + 0)
314
+ tl.store(dst + 0 * 128 + d, (v0 * s0).to(tl.bfloat16))
315
+ v1 = _load_tile_values(cache_ptr, row_off, F1, O1)
316
+ s1 = tl.load(cache_f32_ptr + scale_base + 1)
317
+ tl.store(dst + 1 * 128 + d, (v1 * s1).to(tl.bfloat16))
318
+ v2 = _load_tile_values(cache_ptr, row_off, F2, O2)
319
+ s2 = tl.load(cache_f32_ptr + scale_base + 2)
320
+ tl.store(dst + 2 * 128 + d, (v2 * s2).to(tl.bfloat16))
321
+ v3 = _load_tile_values(cache_ptr, row_off, F3, O3)
322
+ s3 = tl.load(cache_f32_ptr + scale_base + 3)
323
+ tl.store(dst + 3 * 128 + d, (v3 * s3).to(tl.bfloat16))
324
+
325
+
326
+ @triton.jit
327
+ def _stage_tile_fp8(
328
+ cache_ptr, row_off, out_ptr, out_off, FMT: tl.constexpr, OFF: tl.constexpr, T: tl.constexpr
329
+ ):
330
+ d = tl.arange(0, 128)
331
+ if FMT == 8:
332
+ raw = tl.load(cache_ptr + row_off + OFF + d)
333
+ tl.store(out_ptr + out_off + T * 128 + d, raw)
334
+ else:
335
+ v = _load_tile_values(cache_ptr, row_off, FMT, OFF)
336
+ q = v.to(tl.float8e4nv)
337
+ tl.store(out_ptr + out_off + T * 128 + d, q.to(tl.uint8, bitcast=True))
338
+
339
+
340
+ @triton.jit
341
+ def _stage_fp8_kernel(
342
+ cache_ptr,
343
+ cache_f32_ptr,
344
+ topk_ptr,
345
+ lens_ptr,
346
+ staging_ptr,
347
+ staging_f32_ptr,
348
+ C,
349
+ ROW: tl.constexpr,
350
+ F0: tl.constexpr,
351
+ F1: tl.constexpr,
352
+ F2: tl.constexpr,
353
+ F3: tl.constexpr,
354
+ O0: tl.constexpr,
355
+ O1: tl.constexpr,
356
+ O2: tl.constexpr,
357
+ O3: tl.constexpr,
358
+ ):
359
+ t = tl.program_id(0)
360
+ j = tl.program_id(1)
361
+ if j >= tl.load(lens_ptr + t):
362
+ return
363
+ slot = tl.load(topk_ptr + t.to(tl.int64) * C + j)
364
+ if slot < 0:
365
+ return
366
+ row_off = slot.to(tl.int64) * ROW
367
+ out_row = t.to(tl.int64) * C + j
368
+ out_off = out_row * 656
369
+
370
+ _stage_tile_fp8(cache_ptr, row_off, staging_ptr, out_off, F0, O0, 0)
371
+ _stage_tile_fp8(cache_ptr, row_off, staging_ptr, out_off, F1, O1, 1)
372
+ _stage_tile_fp8(cache_ptr, row_off, staging_ptr, out_off, F2, O2, 2)
373
+ _stage_tile_fp8(cache_ptr, row_off, staging_ptr, out_off, F3, O3, 3)
374
+
375
+ src_scale = (slot.to(tl.int64) * ROW) // 4
376
+ dst_scale = (out_off + 512) // 4
377
+ g = tl.arange(0, 4)
378
+ tl.store(
379
+ staging_f32_ptr + dst_scale + g, tl.load(cache_f32_ptr + src_scale + g)
380
+ )
381
+ # rope region [528:656) stays zero (buffer is zero-initialised and never
382
+ # written); q's rope half is exact zeros so it is inert anyway.
383
+
384
+
385
+ # ---------------------------------------------------------------------------
386
+ # python wrappers
387
+ # ---------------------------------------------------------------------------
388
+
389
+
390
+ def _consts():
391
+ (f0, o0), (f1, o1), (f2, o2), (f3, o3) = kvq.tile_offsets()
392
+ return dict(
393
+ ROW=kvq.row_bytes(),
394
+ F0=f0, F1=f1, F2=f2, F3=f3,
395
+ O0=o0, O1=o1, O2=o2, O3=o3,
396
+ )
397
+
398
+
399
+ def _flat_u8(cache: torch.Tensor) -> torch.Tensor:
400
+ if cache.dtype != torch.uint8:
401
+ cache = cache.view(torch.uint8)
402
+ return cache.reshape(-1)
403
+
404
+
405
+ def quant_write(latent: torch.Tensor, cache: torch.Tensor, slots: torch.Tensor):
406
+ """latent: [N, 512] bf16; cache: uint8 (any shape, ROW-multiple numel);
407
+ slots: [N] int (token slot ids, -1 = skip)."""
408
+ n = slots.numel()
409
+ if n == 0:
410
+ return
411
+ assert latent.shape[0] >= n
412
+ assert latent.shape[-1] == 512 and latent.stride(-1) == 1
413
+ flat = _flat_u8(cache)
414
+ _quant_write_kernel[(n,)](
415
+ latent,
416
+ latent.stride(0),
417
+ slots,
418
+ flat,
419
+ flat.view(torch.float32),
420
+ **_consts(),
421
+ )
422
+
423
+
424
+ def gather_dequant(
425
+ cache: torch.Tensor,
426
+ dst: torch.Tensor,
427
+ block_table: torch.Tensor,
428
+ cu_seq_lens: torch.Tensor,
429
+ token_to_seq: torch.Tensor,
430
+ num_tokens: int,
431
+ seq_starts: torch.Tensor,
432
+ block_size: int,
433
+ ):
434
+ """Gather+dequant `num_tokens` context rows into dst[:num_tokens, :512]."""
435
+ if num_tokens == 0:
436
+ return
437
+ flat = _flat_u8(cache)
438
+ assert dst.dtype == torch.bfloat16
439
+ _gather_dequant_kernel[(num_tokens,)](
440
+ flat,
441
+ flat.view(torch.float32),
442
+ dst,
443
+ dst.stride(0),
444
+ block_table,
445
+ block_table.stride(0),
446
+ cu_seq_lens,
447
+ token_to_seq,
448
+ seq_starts,
449
+ block_size,
450
+ **_consts(),
451
+ )
452
+
453
+
454
+ def stage_fp8(
455
+ cache: torch.Tensor,
456
+ topk_physical: torch.Tensor,
457
+ lens: torch.Tensor,
458
+ staging: torch.Tensor,
459
+ ):
460
+ """Stage top-k rows into the canonical 656-byte staging cache.
461
+
462
+ topk_physical: [T, C] int32 global slot ids (-1 padded);
463
+ lens: [T] int32 valid counts; staging: [T*C or more, 656] uint8."""
464
+ t, c = topk_physical.shape
465
+ if t == 0:
466
+ return
467
+ flat = _flat_u8(cache)
468
+ sflat = _flat_u8(staging)
469
+ _stage_fp8_kernel[(t, c)](
470
+ flat,
471
+ flat.view(torch.float32),
472
+ topk_physical,
473
+ lens,
474
+ sflat,
475
+ sflat.view(torch.float32),
476
+ c,
477
+ **_consts(),
478
+ )
479
+
480
+
481
+ # ---------------------------------------------------------------------------
482
+ # torch reference implementations (for testing only)
483
+ # ---------------------------------------------------------------------------
484
+
485
+
486
+ def _ref_quant_tile(x: torch.Tensor, fmt: int):
487
+ """x: [*, 128] float32 normalised by the tile scale. Returns the
488
+ dequantised (pre-tile-scale) values and (exps, codes) for inspection."""
489
+ if fmt == 8:
490
+ return x.to(torch.float8_e4m3fn).to(torch.float32), None
491
+ g = x.reshape(*x.shape[:-1], 4, 32)
492
+ gmax = g.abs().amax(dim=-1)
493
+ e = torch.ceil(torch.log2(gmax.clamp_min(1e-30) / (7.5 if fmt == 6 else 6.0)) - 1e-6)
494
+ e = e.clamp(-6, 6 if fmt == 6 else 7)
495
+ z = g * torch.exp2(-e).unsqueeze(-1)
496
+ az = z.abs()
497
+ if fmt == 6:
498
+ q = torch.where(
499
+ az >= 1.0,
500
+ az.to(torch.float8_e4m3fn).to(torch.float32),
501
+ torch.round(az * 8.0) * 0.125, # NB: torch.round is RNE
502
+ )
503
+ else:
504
+ q = torch.full_like(az, 6.0)
505
+ q = torch.where(az <= 5.0, torch.tensor(4.0), q)
506
+ q = torch.where(az < 3.5, torch.tensor(3.0), q)
507
+ q = torch.where(az <= 2.5, torch.tensor(2.0), q)
508
+ q = torch.where(az < 1.75, torch.tensor(1.5), q)
509
+ q = torch.where(az <= 1.25, torch.tensor(1.0), q)
510
+ q = torch.where(az < 0.75, torch.tensor(0.5), q)
511
+ q = torch.where(az <= 0.25, torch.tensor(0.0), q)
512
+ v = torch.where(z < 0, -q, q) * torch.exp2(e).unsqueeze(-1)
513
+ v = v.clamp(-448.0, 448.0) # match the e4m3 saturation of the stage path
514
+ return v.reshape(x.shape), e
515
+
516
+
517
+ def ref_roundtrip(latent: torch.Tensor):
518
+ """Reference quant->dequant of [N,512] bf16 latent under the active
519
+ scheme. Returns bf16 reconstruction (matches gather_dequant output)."""
520
+ x = latent.float()
521
+ out = torch.empty_like(x)
522
+ for t, (fmt, _off) in enumerate(kvq.tile_offsets()):
523
+ xt = x[:, t * 128 : (t + 1) * 128]
524
+ scale = (xt.abs().amax(dim=-1, keepdim=True) / 448.0).clamp_min(1e-12)
525
+ v, _ = _ref_quant_tile(xt / scale, fmt)
526
+ out[:, t * 128 : (t + 1) * 128] = v * scale
527
+ return out.to(torch.bfloat16)
serving/everything-lane/mla_attention.py ADDED
The diff for this file is too large to render. See raw diff
 
serving/everything-lane/model.py ADDED
@@ -0,0 +1,1253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+ from collections.abc import Iterable
5
+ from typing import ClassVar, Literal
6
+
7
+ import torch
8
+ from torch import nn
9
+
10
+ from vllm.config import ParallelConfig, VllmConfig
11
+ from vllm.distributed import (
12
+ get_ep_group,
13
+ get_pp_group,
14
+ get_tensor_model_parallel_rank,
15
+ get_tensor_model_parallel_world_size,
16
+ tensor_model_parallel_all_gather,
17
+ )
18
+ from vllm.logger import init_logger
19
+ from vllm.model_executor.layers.activation import SiluAndMul, SiluAndMulWithClamp
20
+ from vllm.model_executor.layers.fused_moe import (
21
+ FusedMoEFactory,
22
+ GateLinear,
23
+ fused_moe_make_expert_params_mapping,
24
+ )
25
+ from vllm.model_executor.layers.layernorm import RMSNorm
26
+ from vllm.model_executor.layers.linear import (
27
+ MergedColumnParallelLinear,
28
+ RowParallelLinear,
29
+ )
30
+ from vllm.model_executor.layers.logits_processor import LogitsProcessor
31
+ from vllm.model_executor.layers.mamba.mamba_utils import (
32
+ MambaStateCopyFunc,
33
+ MambaStateCopyFuncCalculator,
34
+ MambaStateDtypeCalculator,
35
+ MambaStateShapeCalculator,
36
+ )
37
+ from vllm.model_executor.layers.mhc import (
38
+ MHCFusedPostPreOp,
39
+ MHCPostOp,
40
+ MHCPreOp,
41
+ hc_contract,
42
+ hc_expand,
43
+ )
44
+ from vllm.model_executor.layers.quantization import QuantizationConfig
45
+ from vllm.model_executor.layers.quantization.utils.quant_utils import (
46
+ GroupShape,
47
+ scaled_dequantize,
48
+ )
49
+ from vllm.model_executor.layers.vocab_parallel_embedding import (
50
+ ParallelLMHead,
51
+ VocabParallelEmbedding,
52
+ )
53
+ from vllm.model_executor.model_loader.weight_utils import (
54
+ default_weight_loader,
55
+ maybe_remap_kv_scale_name,
56
+ )
57
+ from vllm.model_executor.models.deepseek_v2 import _get_moe_router_dtype
58
+ from vllm.model_executor.models.glm4_1v import (
59
+ Glm4vDummyInputsBuilder,
60
+ Glm4vForConditionalGeneration,
61
+ )
62
+ from vllm.model_executor.models.interfaces import (
63
+ HasInnerState,
64
+ IsHybrid,
65
+ MixtureOfExperts,
66
+ SupportsPP,
67
+ )
68
+ from vllm.model_executor.models.utils import (
69
+ AutoWeightsLoader,
70
+ PPMissingLayer,
71
+ init_vllm_registered_model,
72
+ is_pp_missing_parameter,
73
+ make_layers,
74
+ maybe_prefix,
75
+ sequence_parallel_chunk,
76
+ )
77
+ from vllm.models.common.ops.sequence_parallel import (
78
+ sp_all_gather,
79
+ sp_reduce_scatter,
80
+ sp_shard,
81
+ )
82
+ from vllm.multimodal import MULTIMODAL_REGISTRY
83
+ from vllm.platforms import current_platform
84
+ from vllm.sequence import IntermediateTensors
85
+ from vllm.transformers_utils.configs.glm5_next import Glm5NextConfig
86
+
87
+ from .attention import Glm5NextMLAAttention
88
+ from .kda import Glm5NextLinearAttention
89
+ from .multimodal import (
90
+ Glm5NextMultiModalProcessor,
91
+ Glm5NextProcessingInfo,
92
+ Glm5NextVisionTransformer,
93
+ )
94
+
95
+ logger = init_logger(__name__)
96
+
97
+
98
+ class Glm5NextMLP(nn.Module):
99
+ def __init__(
100
+ self,
101
+ hidden_size: int,
102
+ intermediate_size: int,
103
+ hidden_act: str,
104
+ quant_config: QuantizationConfig | None = None,
105
+ reduce_results: bool = True,
106
+ is_sequence_parallel=False,
107
+ prefix: str = "",
108
+ swiglu_limit: float | None = None,
109
+ ) -> None:
110
+ super().__init__()
111
+
112
+ # If is_sequence_parallel, the input and output tensors are sharded
113
+ # across the ranks within the tp_group. In this case the weights are
114
+ # replicated and no collective ops are needed.
115
+ # Otherwise we use standard TP with an allreduce at the end.
116
+ self.gate_up_proj = MergedColumnParallelLinear(
117
+ hidden_size,
118
+ [intermediate_size] * 2,
119
+ bias=False,
120
+ quant_config=quant_config,
121
+ disable_tp=is_sequence_parallel,
122
+ prefix=f"{prefix}.gate_up_proj",
123
+ )
124
+ self.down_proj = RowParallelLinear(
125
+ intermediate_size,
126
+ hidden_size,
127
+ bias=False,
128
+ quant_config=quant_config,
129
+ reduce_results=reduce_results,
130
+ disable_tp=is_sequence_parallel,
131
+ prefix=f"{prefix}.down_proj",
132
+ )
133
+ if hidden_act != "silu":
134
+ raise ValueError(
135
+ f"Unsupported activation: {hidden_act}. Only silu is supported for now."
136
+ )
137
+
138
+ self.swiglu_limit = swiglu_limit
139
+ if self.swiglu_limit is not None:
140
+ self.act_fn = SiluAndMulWithClamp(swiglu_limit=self.swiglu_limit)
141
+ else:
142
+ self.act_fn = SiluAndMul()
143
+
144
+ def forward(self, x):
145
+ gate_up, _ = self.gate_up_proj(x)
146
+ x = self.act_fn(gate_up)
147
+ x, _ = self.down_proj(x)
148
+ return x
149
+
150
+
151
+ class Glm5NextMoE(nn.Module):
152
+ def __init__(
153
+ self,
154
+ config: Glm5NextConfig,
155
+ parallel_config: ParallelConfig,
156
+ quant_config: QuantizationConfig | None = None,
157
+ prefix: str = "",
158
+ apply_routed_scale_to_output: bool = False,
159
+ ):
160
+ super().__init__()
161
+ self.tp_size = get_tensor_model_parallel_world_size()
162
+ self.tp_rank = get_tensor_model_parallel_rank()
163
+
164
+ self.routed_scaling_factor = getattr(config, "routed_scaling_factor", 1.0)
165
+
166
+ self.ep_group = get_ep_group().device_group
167
+ self.ep_rank = get_ep_group().rank_in_group
168
+ self.ep_size = self.ep_group.size()
169
+ self.n_routed_experts: int = config.n_routed_experts
170
+ self.n_shared_experts: int = config.n_shared_experts
171
+
172
+ self.is_sequence_parallel = parallel_config.use_sequence_parallel_moe
173
+
174
+ if config.hidden_act != "silu":
175
+ raise ValueError(
176
+ f"Unsupported activation: {config.hidden_act}. "
177
+ "Only silu is supported for now."
178
+ )
179
+
180
+ self.router_dtype = _get_moe_router_dtype(config)
181
+ self.gate = GateLinear(
182
+ config.hidden_size,
183
+ config.n_routed_experts,
184
+ out_dtype=self.router_dtype,
185
+ prefix=f"{prefix}.gate",
186
+ )
187
+ if getattr(config, "topk_method", None) == "noaux_tc":
188
+ self.gate.e_score_correction_bias = nn.Parameter(
189
+ torch.empty(config.n_routed_experts, dtype=torch.float32)
190
+ )
191
+ else:
192
+ self.gate.e_score_correction_bias = None
193
+
194
+ # Load balancing settings.
195
+ eplb_config = parallel_config.eplb_config
196
+ self.enable_eplb = parallel_config.enable_eplb
197
+
198
+ self.n_redundant_experts = eplb_config.num_redundant_experts
199
+ self.n_logical_experts = self.n_routed_experts
200
+ self.n_physical_experts = self.n_logical_experts + self.n_redundant_experts
201
+ self.n_local_physical_experts = self.n_physical_experts // self.ep_size
202
+
203
+ self.physical_expert_start = self.ep_rank * self.n_local_physical_experts
204
+ self.physical_expert_end = (
205
+ self.physical_expert_start + self.n_local_physical_experts
206
+ )
207
+
208
+ swiglu_limit = getattr(config, "swiglu_limit", None)
209
+ if config.n_shared_experts is None:
210
+ self.shared_experts = None
211
+ else:
212
+ intermediate_size = config.moe_intermediate_size * config.n_shared_experts
213
+
214
+ self.shared_experts = Glm5NextMLP(
215
+ hidden_size=config.hidden_size,
216
+ intermediate_size=intermediate_size,
217
+ hidden_act=config.hidden_act,
218
+ quant_config=quant_config,
219
+ is_sequence_parallel=self.is_sequence_parallel,
220
+ reduce_results=False,
221
+ prefix=f"{prefix}.shared_experts",
222
+ swiglu_limit=swiglu_limit,
223
+ )
224
+
225
+ self.experts = FusedMoEFactory(
226
+ shared_experts=self.shared_experts,
227
+ gate=self.gate,
228
+ num_experts=config.n_routed_experts,
229
+ top_k=config.num_experts_per_token,
230
+ hidden_size=config.hidden_size,
231
+ intermediate_size=config.moe_intermediate_size,
232
+ renormalize=getattr(config, "norm_topk_prob", True),
233
+ quant_config=quant_config,
234
+ use_grouped_topk=True,
235
+ num_expert_group=getattr(config, "n_group", 1),
236
+ topk_group=getattr(config, "topk_group", 1),
237
+ prefix=f"{prefix}.experts",
238
+ scoring_func=getattr(config, "scoring_func", "softmax"),
239
+ routed_scaling_factor=self.routed_scaling_factor,
240
+ apply_routed_scale_to_output=apply_routed_scale_to_output,
241
+ e_score_correction_bias=self.gate.e_score_correction_bias,
242
+ enable_eplb=self.enable_eplb,
243
+ num_redundant_experts=self.n_redundant_experts,
244
+ is_sequence_parallel=self.is_sequence_parallel,
245
+ n_shared_experts=None,
246
+ router_logits_dtype=self.gate.out_dtype,
247
+ swiglu_limit=swiglu_limit,
248
+ )
249
+
250
+ def forward(
251
+ self,
252
+ hidden_states: torch.Tensor,
253
+ already_sequence_parallel: bool = False,
254
+ ) -> torch.Tensor:
255
+ num_tokens, hidden_dim = hidden_states.shape
256
+
257
+ # Chunk the hidden states so they aren't replicated across TP ranks.
258
+ # This avoids duplicate computation in self.experts.
259
+ if self.is_sequence_parallel and not already_sequence_parallel:
260
+ hidden_states = sequence_parallel_chunk(hidden_states)
261
+
262
+ # The router is always external (self.gate); main's MoERunner expects
263
+ # pre-computed router_logits, so compute them here unconditionally.
264
+ router_logits, _ = self.gate(hidden_states)
265
+ final_hidden_states = self.experts(
266
+ hidden_states=hidden_states, router_logits=router_logits
267
+ )
268
+
269
+ if self.is_sequence_parallel and not already_sequence_parallel:
270
+ final_hidden_states = tensor_model_parallel_all_gather(
271
+ final_hidden_states, 0
272
+ )
273
+ final_hidden_states = final_hidden_states[:num_tokens]
274
+
275
+ return final_hidden_states.view(num_tokens, hidden_dim)
276
+
277
+
278
+ class Glm5NextDecoderLayer(nn.Module):
279
+ def __init__(
280
+ self,
281
+ vllm_config: VllmConfig,
282
+ config: Glm5NextConfig,
283
+ layer_idx: int,
284
+ prefix: str = "",
285
+ topk_indices_buffer: torch.Tensor | None = None,
286
+ is_mtp_layer: bool = False,
287
+ **kwargs,
288
+ ) -> None:
289
+ super().__init__()
290
+
291
+ cache_config = vllm_config.cache_config
292
+ quant_config = vllm_config.quant_config
293
+ parallel_config = vllm_config.parallel_config
294
+
295
+ self.hidden_size = config.hidden_size
296
+ self.layer_idx = layer_idx
297
+ self.is_moe = config.is_moe
298
+ self.num_hidden_layers = config.num_hidden_layers
299
+ self.rms_norm_eps = config.rms_norm_eps
300
+ self.num_experts = config.n_routed_experts
301
+ self.is_mtp_layer = is_mtp_layer
302
+ self.mhc = config.mhc
303
+ self.layer_kind = "kda" if config.is_kda_layer(layer_idx) else "mla"
304
+ self.is_sequence_parallel = parallel_config.use_sequence_parallel_moe
305
+
306
+ if config.is_kda_layer(layer_idx):
307
+ self.self_attn = Glm5NextLinearAttention(
308
+ config=config,
309
+ vllm_config=vllm_config,
310
+ prefix=f"{prefix}.self_attn",
311
+ )
312
+ else:
313
+ # MLA layers require the latent head dims, which are guaranteed set
314
+ # on MLA configs; narrow away the `int | None`.
315
+ assert config.v_head_dim is not None
316
+ assert config.kv_lora_rank is not None
317
+ self.self_attn = Glm5NextMLAAttention(
318
+ vllm_config=vllm_config,
319
+ config=config,
320
+ hidden_size=self.hidden_size,
321
+ num_heads=config.num_attention_heads,
322
+ qk_nope_head_dim=config.qk_nope_head_dim,
323
+ qk_rope_head_dim=config.qk_rope_head_dim,
324
+ v_head_dim=config.v_head_dim,
325
+ q_lora_rank=config.q_lora_rank,
326
+ kv_lora_rank=config.kv_lora_rank,
327
+ max_position_embeddings=config.max_position_embeddings,
328
+ cache_config=cache_config,
329
+ # LOCAL PATCH (fp8attn-r2): was quant_config=None ("MLA
330
+ # projections are BF16 in checkpoint"). Pass the real config so
331
+ # FP8-serialized MLA projections can stay FP8-resident; BF16
332
+ # checkpoints still resolve every self_attn module to
333
+ # UnquantizedLinearMethod via the config's ignore list.
334
+ quant_config=quant_config,
335
+ prefix=f"{prefix}.self_attn",
336
+ topk_indices_buffer=topk_indices_buffer,
337
+ skip_rope=getattr(config, "mla_nope", False),
338
+ )
339
+
340
+ # MTP layers sit past the base model's hidden layers (layer_idx >=
341
+ # num_hidden_layers), so they're outside mlp_layer_types; default them
342
+ # to the last base layer's MLP type (sparse/MoE for these checkpoints).
343
+ mlp_layer_types = config.mlp_layer_types
344
+ mlp_type = (
345
+ mlp_layer_types[layer_idx]
346
+ if layer_idx < len(mlp_layer_types)
347
+ else (mlp_layer_types[-1] if mlp_layer_types else "sparse")
348
+ )
349
+ if self.is_moe and self.num_experts is not None and mlp_type == "sparse":
350
+ self.mlp = Glm5NextMoE(
351
+ config=config,
352
+ parallel_config=parallel_config,
353
+ quant_config=quant_config,
354
+ prefix=f"{prefix}.mlp",
355
+ )
356
+ else:
357
+ self.mlp = Glm5NextMLP(
358
+ hidden_size=self.hidden_size,
359
+ intermediate_size=config.intermediate_size,
360
+ hidden_act=config.hidden_act,
361
+ quant_config=quant_config,
362
+ prefix=f"{prefix}.mlp",
363
+ swiglu_limit=config.swiglu_limit,
364
+ )
365
+ self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
366
+ # Cached for the hot forward path (isinstance per layer per step).
367
+ self._mlp_is_moe = isinstance(self.mlp, Glm5NextMoE)
368
+ # In SP, the attention output projection leaves a partial sum; the
369
+ # decoder-layer reduce_scatter after attention completes it (DSv4 pattern).
370
+ # MTP layers use the non-mHC path which has no sp_reduce_scatter, so
371
+ # their o_proj must still reduce normally.
372
+ if self.is_sequence_parallel and not is_mtp_layer:
373
+ self.self_attn.o_proj.reduce_results = False
374
+ self.post_attention_layernorm = RMSNorm(
375
+ config.hidden_size, eps=config.rms_norm_eps
376
+ )
377
+
378
+ if self.mhc and not is_mtp_layer:
379
+ # mhc config
380
+ self.mhc_num_residual_streams = config.mhc_num_residual_streams
381
+ self.mhc_no_norm_weight = config.mhc_no_norm_weight
382
+ self.mhc_tau = config.mhc_tau
383
+ self.hc_eps = config.hc_eps
384
+ self.mhc_sinkhorn_iterations = config.mhc_sinkhorn_iterations
385
+ self.mhc_post_mult_value = config.mhc_post_mult_value
386
+
387
+ n = config.mhc_num_residual_streams
388
+ d_model = n * self.hidden_size
389
+ mix_hc = (2 + n) * n
390
+
391
+ self.n = n
392
+
393
+ # attn hc
394
+ self.hc_attn_fn = nn.Parameter(
395
+ torch.empty(mix_hc, d_model, dtype=torch.float32)
396
+ )
397
+ self.hc_attn_base = nn.Parameter(torch.empty(mix_hc, dtype=torch.float32))
398
+ self.hc_attn_scale = nn.Parameter(torch.empty(3, dtype=torch.float32))
399
+
400
+ # ffn hc
401
+ self.hc_ffn_fn = nn.Parameter(
402
+ torch.empty(mix_hc, d_model, dtype=torch.float32)
403
+ )
404
+ self.hc_ffn_base = nn.Parameter(torch.empty(mix_hc, dtype=torch.float32))
405
+ self.hc_ffn_scale = nn.Parameter(torch.empty(3, dtype=torch.float32))
406
+
407
+ self.mhc_pre_op = MHCPreOp()
408
+ self.mhc_post_op = MHCPostOp()
409
+ self.mhc_fused_post_pre_op = MHCFusedPostPreOp()
410
+
411
+ def forward(
412
+ self,
413
+ positions: torch.Tensor,
414
+ hidden_states: torch.Tensor,
415
+ residual: torch.Tensor | None = None,
416
+ post: torch.Tensor | None = None,
417
+ comb: torch.Tensor | None = None,
418
+ ) -> tuple[
419
+ torch.Tensor,
420
+ torch.Tensor | None,
421
+ torch.Tensor | None,
422
+ torch.Tensor | None,
423
+ ]:
424
+ # 70B or MTP layers: KDA + MoE without HC.
425
+ if not self.mhc or self.is_mtp_layer:
426
+ residual = hidden_states
427
+ hidden_states = self.input_layernorm(hidden_states)
428
+
429
+ attn_output = self.self_attn(
430
+ hidden_states=hidden_states,
431
+ positions=positions,
432
+ )
433
+ hidden_states, residual = self.post_attention_layernorm(
434
+ attn_output, residual=residual
435
+ )
436
+ hidden_states = self.mlp(hidden_states)
437
+ if self.is_mtp_layer:
438
+ # Return the unsummed pair: the MTP caller feeds it straight
439
+ # into shared_head's fused_add_rms_norm (one kernel instead of
440
+ # a separate residual-add + norm). The sum itself is unchanged
441
+ # (fp32-accumulated inside the fused kernel).
442
+ return hidden_states, residual, None, None
443
+ hidden_states = residual + hidden_states
444
+ return hidden_states, residual, None, None
445
+
446
+ # mHC start. `post`/`comb` carry the previous layer's deferred
447
+ # hc_post inputs (its ffn-pre outputs); when present, fuse that
448
+ # hc_post with this layer's attn hc_pre into one kernel (inter-layer
449
+ # fusion). Layer 0 has no incoming state -> standalone hc_pre.
450
+ x = hidden_states
451
+ if post is None:
452
+ if self.layer_idx == 0:
453
+ x = hc_expand(x, self.n)
454
+ residual = x
455
+ post, comb, x = self.hc_pre(
456
+ x,
457
+ self.hc_attn_fn,
458
+ self.hc_attn_scale,
459
+ self.hc_attn_base,
460
+ norm_weight=self.input_layernorm.weight.data,
461
+ norm_eps=self.input_layernorm.variance_epsilon,
462
+ )
463
+ else:
464
+ residual, post, comb, x = self.hc_fused_post_pre(
465
+ x,
466
+ residual,
467
+ post,
468
+ comb,
469
+ self.hc_attn_fn,
470
+ self.hc_attn_scale,
471
+ self.hc_attn_base,
472
+ norm_weight=self.input_layernorm.weight.data,
473
+ norm_eps=self.input_layernorm.variance_epsilon,
474
+ )
475
+
476
+ # Attention needs the full token sequence; mHC above ran on the SP
477
+ # shard. Gather for attention, scatter back afterward (DSv4 pattern).
478
+ if self.is_sequence_parallel:
479
+ x = sp_all_gather(x)[: positions.shape[0]]
480
+
481
+ x = self.self_attn(
482
+ hidden_states=x,
483
+ positions=positions,
484
+ )
485
+
486
+ if self.is_sequence_parallel:
487
+ x = sp_reduce_scatter(x)
488
+
489
+ # Fuse post-attn hc_post + pre-FFN hc_pre (+ RMSNorm) into one kernel.
490
+ residual, post, comb, x = self.hc_fused_post_pre(
491
+ x,
492
+ residual,
493
+ post,
494
+ comb,
495
+ self.hc_ffn_fn,
496
+ self.hc_ffn_scale,
497
+ self.hc_ffn_base,
498
+ norm_weight=self.post_attention_layernorm.weight.data,
499
+ norm_eps=self.post_attention_layernorm.variance_epsilon,
500
+ )
501
+
502
+ # Fully Connected
503
+ if self._mlp_is_moe:
504
+ x = self.mlp(x, already_sequence_parallel=self.is_sequence_parallel)
505
+ else:
506
+ x = self.mlp(x)
507
+
508
+ # mHC end. The last mHC layer materializes its final hc_post (nothing
509
+ # to fuse with) then contracts; every other layer defers its hc_post to
510
+ # the next layer's fused pre, returning the state.
511
+ if self.layer_idx == self.num_hidden_layers - 1:
512
+ x = self.hc_post(x, residual, post, comb)
513
+ x = hc_contract(x, self.n)
514
+ return x, None, None, None
515
+
516
+ return x, residual, post, comb
517
+
518
+ def hc_pre(
519
+ self,
520
+ x: torch.Tensor,
521
+ hc_fn: torch.Tensor,
522
+ hc_scale: torch.Tensor,
523
+ hc_base: torch.Tensor,
524
+ norm_weight: torch.Tensor | None = None,
525
+ norm_eps: float = 0.0,
526
+ ):
527
+ post_mix, res_mix, layer_input = self.mhc_pre_op(
528
+ residual=x,
529
+ fn=hc_fn,
530
+ hc_scale=hc_scale,
531
+ hc_base=hc_base,
532
+ rms_eps=self.rms_norm_eps,
533
+ hc_pre_eps=self.hc_eps,
534
+ hc_sinkhorn_eps=self.hc_eps,
535
+ hc_post_mult_value=self.mhc_post_mult_value,
536
+ sinkhorn_repeat=self.mhc_sinkhorn_iterations,
537
+ norm_weight=norm_weight,
538
+ norm_eps=norm_eps,
539
+ )
540
+ return post_mix, res_mix, layer_input
541
+
542
+ def hc_post(
543
+ self,
544
+ x: torch.Tensor,
545
+ residual: torch.Tensor,
546
+ post: torch.Tensor,
547
+ comb: torch.Tensor,
548
+ ):
549
+ return self.mhc_post_op(x, residual, post, comb)
550
+
551
+ def hc_fused_post_pre(
552
+ self,
553
+ x: torch.Tensor,
554
+ residual: torch.Tensor,
555
+ post: torch.Tensor,
556
+ comb: torch.Tensor,
557
+ hc_fn: torch.Tensor,
558
+ hc_scale: torch.Tensor,
559
+ hc_base: torch.Tensor,
560
+ norm_weight: torch.Tensor | None = None,
561
+ norm_eps: float = 0.0,
562
+ ):
563
+ return self.mhc_fused_post_pre_op(
564
+ x=x,
565
+ residual=residual,
566
+ post_layer_mix=post,
567
+ comb_res_mix=comb,
568
+ fn=hc_fn,
569
+ hc_scale=hc_scale,
570
+ hc_base=hc_base,
571
+ rms_eps=self.rms_norm_eps,
572
+ hc_pre_eps=self.hc_eps,
573
+ hc_sinkhorn_eps=self.hc_eps,
574
+ hc_post_mult_value=self.mhc_post_mult_value,
575
+ sinkhorn_repeat=self.mhc_sinkhorn_iterations,
576
+ n_splits=1,
577
+ tile_n=1,
578
+ norm_weight=norm_weight,
579
+ norm_eps=norm_eps,
580
+ )
581
+
582
+
583
+ class Glm5NextModel(nn.Module):
584
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
585
+ super().__init__()
586
+
587
+ config = vllm_config.model_config.hf_config
588
+ self.config = config
589
+
590
+ self.vocab_size = config.vocab_size
591
+ self.device = current_platform.device_type
592
+
593
+ """
594
+ if config.index_topk is not None:
595
+ topk_indices_buffer = torch.empty(
596
+ vllm_config.scheduler_config.max_num_batched_tokens,
597
+ config.index_topk,
598
+ dtype=torch.int32,
599
+ device=self.device,
600
+ )
601
+ else:
602
+ """
603
+ # `index_topk` is declared on Glm5NextTextConfig with a default of None,
604
+ # so hasattr() is True even for full-MLA configs (no kpool indexer).
605
+ # Gate on the value being set instead.
606
+ self.is_v32 = getattr(config, "index_topk", None) is not None
607
+ if self.is_v32:
608
+ topk_tokens = config.index_topk
609
+ # kpool widens the topk buffer: selecting topk_tokens//kpool pools and
610
+ # expanding them yields topk_tokens token indices, plus an always-
611
+ # selected tail of up to kpool-1 incomplete-pool tokens. The attention
612
+ # backend reads the width dynamically via topk_indices.shape[1].
613
+ kpool = getattr(config, "index_kpool", 1) or 1
614
+ buffer_width = topk_tokens
615
+ # The sparse MLA attention kernel
616
+ # (triton_convert_req_index_to_global_index) tiles the topk
617
+ # dimension in BLOCK_N=128 columns and requires the buffer width
618
+ # to be a multiple of it; otherwise it raises
619
+ # "NUM_TOPK_TOKENS must be divisible by BLOCK_N". Round up: the
620
+ # extra slots stay -1 (the indexer op initializes the buffer to
621
+ # -1) and are masked out by the attention kernel, so they do not
622
+ # affect the softmax over the selected tokens.
623
+ sparse_topk_block_n = 128
624
+ buffer_width = (
625
+ (buffer_width + sparse_topk_block_n - 1) // sparse_topk_block_n
626
+ ) * sparse_topk_block_n
627
+ topk_indices_buffer = torch.empty(
628
+ vllm_config.scheduler_config.max_num_batched_tokens,
629
+ buffer_width,
630
+ dtype=torch.int32,
631
+ device=self.device,
632
+ )
633
+ else:
634
+ # Full-MLA config (no kpool sparse indexer): no topk buffer.
635
+ topk_indices_buffer = None
636
+
637
+ if get_pp_group().is_first_rank:
638
+ self.embed_tokens = VocabParallelEmbedding(
639
+ config.vocab_size,
640
+ config.hidden_size,
641
+ # LOCAL PATCH (glm-max EMBED): pass the quant config through so
642
+ # a manifest INT8_ROW_EMBED entry can quantize the gather.
643
+ # Checkpoints without that entry resolve to None ->
644
+ # UnquantizedEmbeddingMethod (modelopt returns a method for
645
+ # VocabParallelEmbedding only on that algo) -- bit-identical
646
+ # to r5 for every existing checkpoint.
647
+ quant_config=vllm_config.quant_config,
648
+ prefix=f"{prefix}.embed_tokens",
649
+ )
650
+ else:
651
+ self.embed_tokens = PPMissingLayer()
652
+
653
+ def get_layer(prefix: str):
654
+ layer_idx = int(prefix.rsplit(".", 1)[1])
655
+ return Glm5NextDecoderLayer(
656
+ vllm_config=vllm_config,
657
+ config=config,
658
+ layer_idx=layer_idx,
659
+ prefix=prefix,
660
+ topk_indices_buffer=topk_indices_buffer,
661
+ )
662
+
663
+ self.start_layer, self.end_layer, self.layers = make_layers(
664
+ config.num_hidden_layers,
665
+ get_layer,
666
+ prefix=f"{prefix}.layers",
667
+ )
668
+ # The active slice is fixed after construction; cache it so forward
669
+ # doesn't rebuild the slice (a fresh list) every step.
670
+ self._active_layers = self.layers[self.start_layer : self.end_layer]
671
+
672
+ if get_pp_group().is_last_rank:
673
+ self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
674
+ else:
675
+ self.norm = PPMissingLayer()
676
+
677
+ self.is_sequence_parallel = (
678
+ vllm_config.parallel_config.use_sequence_parallel_moe
679
+ )
680
+
681
+ world_size = get_tensor_model_parallel_world_size()
682
+ assert config.num_attention_heads % world_size == 0, (
683
+ "num_attention_heads must be divisible by world_size"
684
+ )
685
+
686
+ def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
687
+ return self.embed_tokens(input_ids)
688
+
689
+ def forward(
690
+ self,
691
+ input_ids: torch.Tensor | None,
692
+ positions: torch.Tensor,
693
+ intermediate_tensors: IntermediateTensors | None,
694
+ inputs_embeds: torch.Tensor | None = None,
695
+ **kwargs,
696
+ ) -> torch.Tensor:
697
+ if get_pp_group().is_first_rank:
698
+ if inputs_embeds is not None:
699
+ hidden_states = inputs_embeds
700
+ else:
701
+ hidden_states = self.embed_input_ids(input_ids)
702
+ residual = None
703
+ post = None
704
+ comb = None
705
+ else:
706
+ assert intermediate_tensors is not None
707
+ hidden_states = intermediate_tensors["hidden_states"]
708
+ residual = intermediate_tensors["residual"]
709
+ # post/comb (deferred mHC hc_post state) are not propagated across
710
+ # PP ranks; the receiving rank's first mHC layer uses standalone pre.
711
+ post = None
712
+ comb = None
713
+
714
+ full_num_tokens = positions.shape[0]
715
+ if self.is_sequence_parallel:
716
+ hidden_states = sp_shard(hidden_states)
717
+
718
+ for layer in self._active_layers:
719
+ hidden_states, residual, post, comb = layer(
720
+ positions, hidden_states, residual, post, comb
721
+ )
722
+
723
+ if not get_pp_group().is_last_rank:
724
+ # PP is gated off for GLM5Next (no make_empty_intermediate_tensors),
725
+ # so this branch is not exercised. post/comb are the deferred
726
+ # hc_post state of this rank's last mHC layer; a future PP path
727
+ # would need to propagate them, but for now they are dropped (the
728
+ # receiving rank's first layer would fall back to standalone pre).
729
+ return IntermediateTensors(
730
+ {"hidden_states": hidden_states, "residual": residual}
731
+ )
732
+
733
+ if self.is_sequence_parallel:
734
+ hidden_states = sp_all_gather(hidden_states)[:full_num_tokens]
735
+
736
+ hidden_states = self.norm(hidden_states)
737
+ return hidden_states
738
+
739
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
740
+ stacked_params_mapping = [
741
+ # (param_name, shard_name, shard_id)
742
+ (".gate_up_proj", ".gate_proj", 0),
743
+ (".gate_up_proj", ".up_proj", 1),
744
+ # MLA: fuse q_a_proj and kv_a_proj_with_mqa
745
+ (".fused_qkv_a_proj", ".q_a_proj", 0),
746
+ (".fused_qkv_a_proj", ".kv_a_proj_with_mqa", 1),
747
+ # Indexer: fuse wk and weights_proj
748
+ (".wk_weights_proj", ".wk", 0),
749
+ (".wk_weights_proj", ".weights_proj", 1),
750
+ # KDA: merge q, k, v, b, f_a, g_a projections into one GEMM
751
+ (".in_proj_qkvbfg_a", ".q_proj", 0),
752
+ (".in_proj_qkvbfg_a", ".k_proj", 1),
753
+ (".in_proj_qkvbfg_a", ".v_proj", 2),
754
+ (".in_proj_qkvbfg_a", ".b_proj", 3),
755
+ (".in_proj_qkvbfg_a", ".f_a_proj", 4),
756
+ (".in_proj_qkvbfg_a", ".g_a_proj", 5),
757
+ ]
758
+ if self.config.is_moe:
759
+ # Params for weights, fp8 weight scales, fp8 activation scales
760
+ # (param_name, weight_name, expert_id, shard_id)
761
+ expert_params_mapping = fused_moe_make_expert_params_mapping(
762
+ self,
763
+ ckpt_gate_proj_name="gate_proj",
764
+ ckpt_down_proj_name="down_proj",
765
+ ckpt_up_proj_name="up_proj",
766
+ num_experts=self.config.n_routed_experts,
767
+ )
768
+ else:
769
+ expert_params_mapping = []
770
+ params_dict = dict(self.named_parameters())
771
+ loaded_params: set[str] = set()
772
+
773
+ # GLM5-Next NoPE: checkpoint's kv_a_proj_with_mqa has only kv_lora_rank
774
+ # rows, but the model expects kv_lora_rank + qk_rope_head_dim rows.
775
+ # Pad the missing rope portion with zeros.
776
+ kv_a_pad_size = 0
777
+ if self.config.mla_nope and self.config.qk_rope_head_dim > 0:
778
+ kv_a_pad_size = self.config.qk_rope_head_dim
779
+
780
+ _pending_wk_fp8: dict = {}
781
+
782
+ for args in weights:
783
+ name, loaded_weight = args[:2]
784
+ kwargs: dict = args[2] if len(args) > 2 else {}
785
+ if "rotary_emb.inv_freq" in name:
786
+ continue
787
+
788
+ spec_layer = get_spec_layer_idx_from_weight_name(self.config, name)
789
+ if spec_layer is not None:
790
+ continue # skip spec decode layers for main model
791
+ if "rotary_emb.cos_cached" in name or "rotary_emb.sin_cached" in name:
792
+ # Models trained using ColossalAI may include these tensors in
793
+ # the checkpoint. Skip them.
794
+ continue
795
+
796
+ # Handle FP8 indexer WK: dequantize to BF16 for fusion with
797
+ # weights_proj into wk_weights_proj.
798
+ if _try_load_fp8_indexer_wk(
799
+ name,
800
+ loaded_weight,
801
+ _pending_wk_fp8,
802
+ params_dict,
803
+ loaded_params,
804
+ ):
805
+ continue
806
+
807
+ # FP8 checkpoint: dequantize BF16-kept MLA projections
808
+ # (q_a_proj / kv_a_proj_with_mqa / o_proj) to BF16.
809
+ if _try_load_fp8_attn_proj(
810
+ name,
811
+ loaded_weight,
812
+ _pending_wk_fp8,
813
+ params_dict,
814
+ loaded_params,
815
+ kv_a_pad_size,
816
+ ):
817
+ continue
818
+
819
+ # Pad kv_a_proj_with_mqa for NoPE models
820
+ if kv_a_pad_size > 0 and ".kv_a_proj_with_mqa." in name:
821
+ pad = torch.zeros(
822
+ kv_a_pad_size,
823
+ *loaded_weight.shape[1:],
824
+ dtype=loaded_weight.dtype,
825
+ device=loaded_weight.device,
826
+ )
827
+ loaded_weight = torch.cat([loaded_weight, pad], dim=0)
828
+
829
+ for param_name, weight_name, shard_id in stacked_params_mapping:
830
+ if weight_name not in name:
831
+ continue
832
+ # We have mlp.experts[0].gate_proj in the checkpoint.
833
+ # Since we handle the experts below in expert_params_mapping,
834
+ # we need to skip here BEFORE we update the name, otherwise
835
+ # name will be updated to mlp.experts[0].gate_up_proj, which
836
+ # will then be updated below in expert_params_mapping
837
+ # for mlp.experts[0].gate_gate_up_proj, which breaks load.
838
+ if ("mlp.experts." in name) and name not in params_dict:
839
+ continue
840
+ name_mapped = name.replace(weight_name, param_name)
841
+ # QKV fusion: skip if fused module doesn't exist in model
842
+ if param_name == ".fused_qkv_a_proj" and name_mapped not in params_dict:
843
+ continue
844
+ name = name_mapped
845
+ # Skip loading extra bias for GPTQ models.
846
+ if name.endswith(".bias") and name not in params_dict:
847
+ continue
848
+ if is_pp_missing_parameter(name, self):
849
+ continue
850
+ param = params_dict[name]
851
+ weight_loader = param.weight_loader
852
+ weight_loader(param, loaded_weight, shard_id)
853
+ break
854
+ else:
855
+ for idx, (
856
+ param_name,
857
+ weight_name,
858
+ expert_id,
859
+ expert_shard_id,
860
+ ) in enumerate(expert_params_mapping):
861
+ if weight_name not in name:
862
+ continue
863
+ name = name.replace(weight_name, param_name)
864
+ if is_pp_missing_parameter(name, self):
865
+ continue
866
+ param = params_dict[name]
867
+ weight_loader = param.weight_loader
868
+ weight_loader(
869
+ param,
870
+ loaded_weight,
871
+ name,
872
+ expert_id=expert_id,
873
+ shard_id=expert_shard_id,
874
+ )
875
+ break
876
+ else:
877
+ # Skip loading extra bias for GPTQ models.
878
+ if (
879
+ name.endswith(".bias")
880
+ and name not in params_dict
881
+ and not self.config.is_linear_attn
882
+ ): # noqa: E501
883
+ continue
884
+ # Remapping the name of FP8 kv-scale.
885
+ remapped_name = maybe_remap_kv_scale_name(name, params_dict)
886
+ if remapped_name is None:
887
+ continue
888
+ name = remapped_name
889
+ if is_pp_missing_parameter(name, self):
890
+ continue
891
+
892
+ param = params_dict[name]
893
+ weight_loader = getattr(
894
+ param, "weight_loader", default_weight_loader
895
+ )
896
+ weight_loader(param, loaded_weight, **kwargs)
897
+ loaded_params.add(name)
898
+ return loaded_params
899
+
900
+
901
+ class Glm5NextForCausalLM(
902
+ nn.Module, HasInnerState, SupportsPP, MixtureOfExperts, IsHybrid
903
+ ):
904
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
905
+ super().__init__()
906
+ self.model_config = vllm_config.model_config
907
+ self.vllm_config = vllm_config
908
+ self.config = self.model_config.hf_config
909
+ quant_config = vllm_config.quant_config
910
+ self.quant_config = quant_config
911
+ self.model = Glm5NextModel(
912
+ vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
913
+ )
914
+ if get_pp_group().is_last_rank:
915
+ self.lm_head = ParallelLMHead(
916
+ self.config.vocab_size,
917
+ self.config.hidden_size,
918
+ quant_config=quant_config,
919
+ prefix=maybe_prefix(prefix, "lm_head"),
920
+ )
921
+ else:
922
+ self.lm_head = PPMissingLayer()
923
+ logit_scale = getattr(self.config, "logit_scale", 1.0)
924
+ self.logits_processor = LogitsProcessor(
925
+ self.config.vocab_size, scale=logit_scale
926
+ )
927
+
928
+ def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
929
+ return self.model.embed_input_ids(input_ids)
930
+
931
+ def forward(
932
+ self,
933
+ input_ids: torch.Tensor | None,
934
+ positions: torch.Tensor,
935
+ intermediate_tensors: IntermediateTensors | None = None,
936
+ inputs_embeds: torch.Tensor | None = None,
937
+ **kwargs,
938
+ ) -> torch.Tensor | IntermediateTensors:
939
+ hidden_states = self.model(
940
+ input_ids, positions, intermediate_tensors, inputs_embeds, **kwargs
941
+ )
942
+ return hidden_states
943
+
944
+ @classmethod
945
+ def get_mamba_state_dtype_from_config(
946
+ cls,
947
+ vllm_config: "VllmConfig",
948
+ ) -> tuple[torch.dtype, torch.dtype]:
949
+ return MambaStateDtypeCalculator.kda_state_dtype(
950
+ vllm_config.model_config.dtype, vllm_config.cache_config.mamba_cache_dtype
951
+ )
952
+
953
+ @classmethod
954
+ def get_mamba_state_shape_from_config(
955
+ cls, vllm_config: "VllmConfig"
956
+ ) -> tuple[tuple[int, int], tuple[int, int, int]]:
957
+ parallel_config = vllm_config.parallel_config
958
+ hf_config = vllm_config.model_config.hf_config
959
+ tp_size = parallel_config.tensor_parallel_size
960
+ num_spec = (
961
+ vllm_config.speculative_config.num_speculative_tokens
962
+ if vllm_config.speculative_config
963
+ else 0
964
+ )
965
+ return MambaStateShapeCalculator.kda_state_shape(
966
+ tp_size,
967
+ hf_config.linear_num_heads,
968
+ hf_config.linear_head_dim,
969
+ conv_kernel_size=hf_config.linear_conv_kernel_dim,
970
+ num_spec=num_spec,
971
+ )
972
+
973
+ @classmethod
974
+ def get_mamba_state_copy_func(
975
+ cls,
976
+ ) -> tuple[
977
+ MambaStateCopyFunc, MambaStateCopyFunc, MambaStateCopyFunc, MambaStateCopyFunc
978
+ ]:
979
+ return MambaStateCopyFuncCalculator.kda_state_copy_func()
980
+
981
+ def compute_logits(
982
+ self,
983
+ hidden_states: torch.Tensor,
984
+ ) -> torch.Tensor | None:
985
+ logits = self.logits_processor(self.lm_head, hidden_states)
986
+ return logits
987
+
988
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
989
+ loader = AutoWeightsLoader(
990
+ self,
991
+ skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
992
+ )
993
+ return loader.load_weights(weights)
994
+
995
+
996
+ @MULTIMODAL_REGISTRY.register_processor(
997
+ Glm5NextMultiModalProcessor,
998
+ info=Glm5NextProcessingInfo,
999
+ dummy_inputs=Glm4vDummyInputsBuilder,
1000
+ )
1001
+ class Glm5NextForConditionalGeneration(
1002
+ Glm4vForConditionalGeneration, HasInnerState, IsHybrid
1003
+ ):
1004
+ # The text model (KDA + dense-MLA + MoE) is a hybrid mamba model. The
1005
+ # multimodal wrapper must declare the same interfaces so vLLM treats it as
1006
+ # hybrid (auto-aligns mamba/attention block sizes, sizes the mamba state
1007
+ # cache); the mamba-state classmethods delegate to the text model.
1008
+ has_inner_state: ClassVar[Literal[True]] = True
1009
+ is_hybrid: ClassVar[Literal[True]] = True
1010
+
1011
+ # NOTE: weight-prefix mapping is inherited from Glm4vForConditionalGeneration
1012
+ # (``model.visual.`` -> ``visual.``, ``model.language_model.`` ->
1013
+ # ``language_model.model.``, ``lm_head.`` -> ``language_model.lm_head.``),
1014
+ # matching the GLM-OCR / GLM-4V serialization convention. If the real
1015
+ # checkpoint's safetensors keys differ (e.g. ``language_model.model.`` with
1016
+ # no outer ``model.``), override ``hf_to_vllm_mapper`` accordingly.
1017
+
1018
+ @classmethod
1019
+ def get_mamba_state_dtype_from_config(cls, vllm_config: VllmConfig):
1020
+ from .model import Glm5NextForCausalLM
1021
+
1022
+ return Glm5NextForCausalLM.get_mamba_state_dtype_from_config(vllm_config)
1023
+
1024
+ @classmethod
1025
+ def get_mamba_state_shape_from_config(cls, vllm_config: VllmConfig):
1026
+ from .model import Glm5NextForCausalLM
1027
+
1028
+ return Glm5NextForCausalLM.get_mamba_state_shape_from_config(vllm_config)
1029
+
1030
+ @classmethod
1031
+ def get_mamba_state_copy_func(cls):
1032
+ from .model import Glm5NextForCausalLM
1033
+
1034
+ return Glm5NextForCausalLM.get_mamba_state_copy_func()
1035
+
1036
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
1037
+ super(Glm4vForConditionalGeneration, self).__init__()
1038
+ config = vllm_config.model_config.hf_config
1039
+ multimodal_config = vllm_config.model_config.multimodal_config
1040
+ assert multimodal_config is not None
1041
+
1042
+ self.config = config
1043
+ self.model_config = vllm_config.model_config
1044
+ self.multimodal_config = multimodal_config
1045
+ self.use_data_parallel = multimodal_config.mm_encoder_tp_mode == "data"
1046
+ self.is_multimodal_pruning_enabled = (
1047
+ multimodal_config.is_multimodal_pruning_enabled()
1048
+ )
1049
+
1050
+ # LOCAL PATCH (glm-max VISION): manifest-gated block-FP8 visual tower.
1051
+ # The r2-era "yields NaN image features" warning below is about
1052
+ # inheriting the global fp8 quant_config while the checkpoint ships
1053
+ # BF16 visual weights WITHOUT weight_scale_inv (loader/format
1054
+ # mismatch), not about FP8 numerics of the tower. A converted
1055
+ # checkpoint (convert_fp8visual.py) declares visual.* entries in the
1056
+ # MIXED_PRECISION quantized_layers manifest and ships the scales; only
1057
+ # then do we pass the quant config through, so unconverted checkpoints
1058
+ # keep the exact r5 behavior (quant_config=None).
1059
+ _vis_quant_config = None
1060
+ _qc = vllm_config.quant_config
1061
+ if _qc is not None and any(
1062
+ k.startswith(("visual.", "model.visual."))
1063
+ for k in (getattr(_qc, "quantized_layers", None) or {})
1064
+ ):
1065
+ _vis_quant_config = _qc
1066
+
1067
+ with self._mark_tower_model(vllm_config, {"image", "video"}):
1068
+ self.visual = Glm5NextVisionTransformer(
1069
+ config.text_config,
1070
+ config.vision_config,
1071
+ # Read eps from the VISION sub-config, not the top-level
1072
+ # `config.rms_norm_eps`: Glm5NextConfig.__getattribute__ mirrors
1073
+ # the latter onto text_config (1e-5), silently ignoring the
1074
+ # vision tower's own (1e-6) rms_norm_eps.
1075
+ norm_eps=config.vision_config.rms_norm_eps,
1076
+ # Vision tower ships BF16 weights in this fp8 checkpoint (no
1077
+ # weight_scale_inv for visual.*), so it must NOT inherit the
1078
+ # global fp8 quant_config -- doing so incorrectly quantizes
1079
+ # the tower
1080
+ # and yields NaN image features. Mirrors the MLA/KDA proj
1081
+ # pattern (quant_config=None for BF16 submodules).
1082
+ quant_config=_vis_quant_config,
1083
+ prefix=maybe_prefix(prefix, "visual"),
1084
+ )
1085
+
1086
+ with self._mark_language_model(vllm_config):
1087
+ self.language_model = init_vllm_registered_model(
1088
+ vllm_config=vllm_config,
1089
+ hf_config=config.text_config,
1090
+ prefix=maybe_prefix(prefix, "language_model"),
1091
+ architectures=["Glm5NextForCausalLM"],
1092
+ )
1093
+
1094
+ # Glm5NextForCausalLM does not implement make_empty_intermediate_tensors,
1095
+ # so pipeline parallelism is gated off (consistent with the text-only
1096
+ # model) and we intentionally do not alias it here.
1097
+
1098
+ def get_encoder_cudagraph_config(self):
1099
+ # The forked vision tower (multimodal.py) has no abs-pos embeddings, so its
1100
+ # prepare_encoder_metadata does not produce "pos_embeds". Drop it from the
1101
+ # buffer_keys inherited from Glm4vForConditionalGeneration so encoder
1102
+ # CUDA-graph capture/replay does not expect a buffer that is never filled.
1103
+ config = super().get_encoder_cudagraph_config()
1104
+ config.buffer_keys = [k for k in config.buffer_keys if k != "pos_embeds"]
1105
+ return config
1106
+
1107
+
1108
+ def get_spec_layer_idx_from_weight_name(
1109
+ config: Glm5NextConfig, weight_name: str
1110
+ ) -> int | None:
1111
+ if hasattr(config, "num_nextn_predict_layers") and (
1112
+ config.num_nextn_predict_layers > 0
1113
+ ):
1114
+ layer_idx = config.num_hidden_layers
1115
+ for i in range(config.num_nextn_predict_layers):
1116
+ if weight_name.startswith(
1117
+ f"model.layers.{layer_idx + i}."
1118
+ ) or weight_name.startswith(f"layers.{layer_idx + i}."):
1119
+ return layer_idx + i
1120
+ return None
1121
+
1122
+
1123
+ def _try_load_fp8_indexer_wk(name, tensor, buf, params_dict, loaded_params):
1124
+ if "indexer.wk." not in name or "wk_weights" in name:
1125
+ return False
1126
+ is_weight = name.endswith(".weight") and tensor.dtype == torch.float8_e4m3fn
1127
+ is_scale = "weight_scale_inv" in name
1128
+ if not is_weight and not is_scale:
1129
+ return False
1130
+ layer_prefix = name.rsplit(".wk.", 1)[0]
1131
+ entry = buf.setdefault(layer_prefix, {})
1132
+ entry["weight" if is_weight else "scale"] = tensor
1133
+ if "weight" not in entry or "scale" not in entry:
1134
+ return True
1135
+
1136
+ weight_fp8, scale_inv = entry["weight"], entry["scale"]
1137
+ del buf[layer_prefix]
1138
+ block_size = weight_fp8.shape[1] // scale_inv.shape[1]
1139
+ weight_bf16 = scaled_dequantize(
1140
+ weight_fp8,
1141
+ scale_inv,
1142
+ group_shape=GroupShape(block_size, block_size),
1143
+ out_dtype=torch.bfloat16,
1144
+ )
1145
+
1146
+ fused_name = f"{layer_prefix}.wk_weights_proj.weight"
1147
+ param = params_dict[fused_name]
1148
+ param.weight_loader(param, weight_bf16, 0)
1149
+ loaded_params.add(fused_name)
1150
+ return True
1151
+
1152
+
1153
+ def _dequant_fp8_block(
1154
+ weight_fp8: torch.Tensor,
1155
+ scale_inv: torch.Tensor,
1156
+ block_size: int = 128,
1157
+ ) -> torch.Tensor:
1158
+ """Dequantize a block-FP8 (e4m3) weight with per-block scale to BF16.
1159
+
1160
+ Unlike ``scaled_dequantize`` this tolerates a non-divisible (partial last
1161
+ block) shape by zero-padding to a multiple of ``block_size`` before the
1162
+ scale broadcast and trimming back afterwards (e.g. kv_a_proj_with_mqa is
1163
+ 576 rows = 4*128 + 64).
1164
+ """
1165
+ out_dim, in_dim = weight_fp8.shape
1166
+ pad_out = (-out_dim) % block_size
1167
+ pad_in = (-in_dim) % block_size
1168
+ w = weight_fp8
1169
+ if pad_out or pad_in:
1170
+ w = torch.nn.functional.pad(w, (0, pad_in, 0, pad_out))
1171
+ # scale_inv is (ceil(out/block), ceil(in/block)); broadcast to (out, in).
1172
+ s = scale_inv.to(torch.float32)
1173
+ s_full = s.repeat_interleave(block_size, dim=0).repeat_interleave(block_size, dim=1)
1174
+ out = (w.to(torch.float32) * s_full).to(torch.bfloat16)
1175
+ return out[:out_dim, :in_dim].contiguous()
1176
+
1177
+
1178
+ # FP8 checkpoint projections that the MODEL keeps in BF16, so the block-FP8
1179
+ # (weight + weight_scale_inv) must be dequantized to BF16 on load.
1180
+ # Maps checkpoint proj-suffix -> (buffer key, model target base, fused shard id
1181
+ # or None for a direct projection, whether NoPE rope-padding applies).
1182
+ _FP8_ATTN_PROJS = {
1183
+ ".q_a_proj.": ("q_a", "fused_qkv_a_proj", 0, False),
1184
+ ".kv_a_proj_with_mqa.": ("kv_a", "fused_qkv_a_proj", 1, True),
1185
+ ".q_b_proj.": ("q_b", "q_b_proj", None, False),
1186
+ ".o_proj.": ("o_proj", "o_proj", None, False),
1187
+ }
1188
+
1189
+
1190
+ def _try_load_fp8_attn_proj(
1191
+ name,
1192
+ tensor,
1193
+ buf,
1194
+ params_dict,
1195
+ loaded_params,
1196
+ kv_a_pad_size: int,
1197
+ ) -> bool:
1198
+ """Dequantize FP8 q_a_proj / kv_a_proj_with_mqa / o_proj to BF16 on load.
1199
+
1200
+ The FP8 checkpoint stores these as block-FP8 (weight + weight_scale_inv),
1201
+ but the model holds them in BF16 (``fused_qkv_a_proj`` is always BF16 via
1202
+ DeepSeekV2FusedQkvAProjLinear; ``o_proj`` is excluded by
1203
+ modules_to_not_convert). When the model target is BF16 (no
1204
+ ``weight_scale_inv`` param) we dequantize; otherwise we return False so the
1205
+ normal stacked/direct path loads the FP8 tensor as-is.
1206
+ """
1207
+ matched = None
1208
+ for suffix, info in _FP8_ATTN_PROJS.items():
1209
+ if suffix in name:
1210
+ matched = (suffix, info)
1211
+ break
1212
+ if matched is None:
1213
+ return False
1214
+ suffix, (key, target_base, shard_id, is_kva) = matched
1215
+ is_weight = name.endswith(".weight") and tensor.dtype == torch.float8_e4m3fn
1216
+ is_scale = "weight_scale_inv" in name
1217
+ if not is_weight and not is_scale:
1218
+ return False
1219
+
1220
+ layer_prefix = name.rsplit(suffix, 1)[0]
1221
+ target_w = f"{layer_prefix}.{target_base}.weight"
1222
+ target_s = f"{layer_prefix}.{target_base}.weight_scale_inv"
1223
+ # If the model actually kept this projection in FP8, let the normal path
1224
+ # handle it (it has a weight_scale_inv param).
1225
+ if target_s in params_dict:
1226
+ return False
1227
+
1228
+ entry = buf.setdefault(layer_prefix, {}).setdefault(key, {})
1229
+ entry["weight" if is_weight else "scale"] = tensor
1230
+ if "weight" not in entry or "scale" not in entry:
1231
+ return True
1232
+
1233
+ weight_fp8, scale_inv = entry["weight"], entry["scale"]
1234
+ buf[layer_prefix].pop(key, None)
1235
+ block_size = weight_fp8.shape[1] // scale_inv.shape[1]
1236
+ weight_bf16 = _dequant_fp8_block(weight_fp8, scale_inv, block_size)
1237
+ # NoPE: pad kv_a rope portion (kv_lora_rank -> kv_lora_rank + qk_rope_head_dim).
1238
+ if is_kva and kv_a_pad_size > 0:
1239
+ pad = torch.zeros(
1240
+ kv_a_pad_size,
1241
+ weight_bf16.shape[1],
1242
+ dtype=weight_bf16.dtype,
1243
+ device=weight_bf16.device,
1244
+ )
1245
+ weight_bf16 = torch.cat([weight_bf16, pad], dim=0)
1246
+
1247
+ param = params_dict[target_w]
1248
+ if shard_id is None:
1249
+ param.weight_loader(param, weight_bf16)
1250
+ else:
1251
+ param.weight_loader(param, weight_bf16, shard_id)
1252
+ loaded_params.add(target_w)
1253
+ return True
serving/everything-lane/modelopt.py ADDED
The diff for this file is too large to render. See raw diff
 
serving/everything-lane/sparse_mla_attention.py ADDED
@@ -0,0 +1,922 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+ """Shared MHA implementation and metadata builder for sparse MLA backends."""
4
+
5
+ import math
6
+ from shutil import which
7
+ from typing import TYPE_CHECKING, ClassVar, Generic, TypeVar, cast
8
+
9
+ import numpy as np
10
+ import torch
11
+
12
+ from vllm import _custom_ops as ops
13
+ from vllm.distributed import (
14
+ get_dcp_group,
15
+ get_tensor_model_parallel_world_size,
16
+ )
17
+ from vllm.logger import init_logger
18
+ from vllm.model_executor.layers.attention.mla_attention import (
19
+ MLACommonBaseImpl,
20
+ MLACommonMetadata,
21
+ MLACommonPrefillMetadata,
22
+ accumulate_mla_context_chunk,
23
+ align_mla_chunked_context_workspace_size,
24
+ build_mla_chunked_context_metadata,
25
+ get_mla_dims,
26
+ init_mla_context_partial,
27
+ )
28
+ from vllm.platforms import current_platform
29
+ from vllm.triton_utils import tl, triton
30
+ from vllm.utils.flashinfer import has_flashinfer
31
+ from vllm.utils.torch_utils import is_quantized_kv_cache, np_to_pinned_tensor
32
+ from vllm.v1.attention.backend import AttentionMetadata, AttentionMetadataBuilder
33
+ from vllm.v1.attention.backends.fa_utils import get_flash_attn_version
34
+ from vllm.v1.attention.backends.utils import split_decodes_and_prefills
35
+ from vllm.v1.attention.ops.dcp_utils import MLADCPManager
36
+ from vllm.v1.attention.ops.merge_attn_states import merge_attn_states
37
+
38
+ if TYPE_CHECKING:
39
+ from vllm.config import VllmConfig
40
+ from vllm.model_executor.layers.linear import ColumnParallelLinear
41
+ from vllm.v1.attention.backend import CommonAttentionMetadata
42
+ from vllm.v1.kv_cache_interface import AttentionSpec
43
+
44
+ logger = init_logger(__name__)
45
+
46
+ T = TypeVar("T", bound=AttentionMetadata)
47
+
48
+ GLOBAL_TOPK_MASK_MAX_BYTES = 128 * 1024 * 1024 # 128 MiB
49
+
50
+
51
+ def _topk_mask_shape(
52
+ batch_size: int,
53
+ max_query_len: int,
54
+ max_key_len: int,
55
+ reserve_key_starts_word: bool = False,
56
+ ) -> tuple[int, int, int]:
57
+ """Shape of a bit-packed top-k mask, shared by every site that builds one."""
58
+ tile_m = 128 if max_query_len <= 128 else 256
59
+ padded_q_len = triton.cdiv(max_query_len, tile_m) * tile_m
60
+ num_words = triton.cdiv(max_key_len, 32) + int(reserve_key_starts_word)
61
+ return batch_size, padded_q_len, num_words
62
+
63
+
64
+ def _masked_mha_workspace_fits(
65
+ batch_size: int,
66
+ max_query_len: int,
67
+ max_context_chunk_seq_len: int,
68
+ workspace_numel: int,
69
+ ) -> bool:
70
+ """Return whether the suffix and per-context-chunk masks fit the workspace.
71
+
72
+ The global mask is excluded: it always needs more, and has its own check.
73
+ """
74
+ max_key_len = max(max_query_len, max_context_chunk_seq_len)
75
+ needed = math.prod(_topk_mask_shape(batch_size, max_query_len, max_key_len))
76
+ return needed <= workspace_numel
77
+
78
+
79
+ def _is_masked_mha_available(
80
+ num_heads_total: int,
81
+ kv_lora_rank: int,
82
+ qk_nope_head_dim: int,
83
+ qk_rope_head_dim: int,
84
+ v_head_dim: int,
85
+ kv_cache_dtype: str,
86
+ ) -> bool:
87
+ """Check if masked MHA can ever fire for this model configuration."""
88
+ if not current_platform.is_device_capability_family(100):
89
+ return False
90
+ if (
91
+ num_heads_total != 128
92
+ or kv_lora_rank != 512
93
+ or qk_nope_head_dim != 128
94
+ or qk_rope_head_dim != 64
95
+ or v_head_dim != 128
96
+ ):
97
+ return False
98
+ qk_head_dim = qk_nope_head_dim + qk_rope_head_dim
99
+ fa_version = get_flash_attn_version(head_size=qk_head_dim, head_size_v=v_head_dim)
100
+ return fa_version == 4 and not is_quantized_kv_cache(kv_cache_dtype)
101
+
102
+
103
+ class SparseMLACommonMetadataBuilder(AttentionMetadataBuilder[T]):
104
+ metadata_cls: type[T]
105
+ require_uniform_decodes: ClassVar[bool] = False
106
+
107
+ def __init__(
108
+ self,
109
+ kv_cache_spec: "AttentionSpec",
110
+ layer_names: list[str],
111
+ vllm_config: "VllmConfig",
112
+ device: torch.device,
113
+ ) -> None:
114
+ super().__init__(kv_cache_spec, layer_names, vllm_config, device)
115
+ self.vllm_config = vllm_config
116
+ self.device = device
117
+ self.model_config = vllm_config.model_config
118
+ self.mla_dims = get_mla_dims(self.model_config)
119
+ self.topk_tokens: int = vllm_config.model_config.hf_config.index_topk
120
+ self.req_id_per_token_buffer = torch.empty(
121
+ (vllm_config.scheduler_config.max_num_batched_tokens,),
122
+ dtype=torch.int32,
123
+ device=device,
124
+ )
125
+ parallel_config = vllm_config.parallel_config
126
+ self.use_pcp = parallel_config.prefill_context_parallel_size > 1
127
+ try:
128
+ self.dcp_world_size = get_dcp_group().world_size
129
+ except AssertionError:
130
+ # DCP might not be initialized in testing
131
+ self.dcp_world_size = 1
132
+ self.cp_kv_cache_interleave_size = parallel_config.cp_kv_cache_interleave_size
133
+ self.dcp_local_block_size = self.cp_kv_cache_interleave_size
134
+ self.dcp_virtual_block_size = self.dcp_local_block_size * self.dcp_world_size
135
+
136
+ self.chunked_prefill_workspace_size = (
137
+ self.determine_chunked_prefill_workspace_size(vllm_config)
138
+ )
139
+ workspace_head_size = (
140
+ self.mla_dims.kv_lora_rank + self.mla_dims.qk_rope_head_dim
141
+ )
142
+ workspace_rows = self.chunked_prefill_workspace_size
143
+ if self.dcp_world_size > 1:
144
+ # DCP gathers each rank's local KV shard into the workspace, so it
145
+ # needs an extra 1/DCP rows beyond the TP allocation.
146
+ assert self.chunked_prefill_workspace_size % self.dcp_world_size == 0
147
+ workspace_rows += self.chunked_prefill_workspace_size // self.dcp_world_size
148
+ self.chunked_prefill_workspace = torch.empty(
149
+ (workspace_rows, workspace_head_size),
150
+ dtype=self.model_config.dtype,
151
+ device=device,
152
+ )
153
+ self.topk_mask_workspace: torch.Tensor | None = None
154
+ if _is_masked_mha_available(
155
+ self.model_config.model_arch_config.total_num_attention_heads,
156
+ self.mla_dims.kv_lora_rank,
157
+ self.mla_dims.qk_nope_head_dim,
158
+ self.mla_dims.qk_rope_head_dim,
159
+ self.mla_dims.v_head_dim,
160
+ vllm_config.cache_config.cache_dtype,
161
+ ):
162
+ self.topk_mask_workspace = torch.zeros(
163
+ GLOBAL_TOPK_MASK_MAX_BYTES // torch.int32.itemsize,
164
+ dtype=torch.int32,
165
+ device=device,
166
+ )
167
+ attention_layer = vllm_config.compilation_config.static_forward_context[
168
+ layer_names[0]
169
+ ]
170
+ layer_prefill_backend = attention_layer.prefill_backend
171
+ self.dcp_manager: MLADCPManager | None = None
172
+ if self.dcp_world_size > 1:
173
+ self.dcp_manager = getattr(attention_layer, "dcp_manager", None)
174
+ assert isinstance(self.dcp_manager, MLADCPManager)
175
+ if layer_prefill_backend is not None:
176
+ self.dcp_manager.init_kv_gather(
177
+ self.chunked_prefill_workspace,
178
+ self.chunked_prefill_workspace_size,
179
+ )
180
+ self._prefill_backend = (
181
+ layer_prefill_backend.clone() if layer_prefill_backend is not None else None
182
+ )
183
+
184
+ @staticmethod
185
+ def determine_chunked_prefill_workspace_size(vllm_config: "VllmConfig") -> int:
186
+ scheduler_config = vllm_config.scheduler_config
187
+ cache_config = vllm_config.cache_config
188
+ model_config = vllm_config.model_config
189
+ topk_tokens = model_config.hf_config.index_topk
190
+
191
+ workspace_size = min(
192
+ max(
193
+ 8 * model_config.max_model_len,
194
+ 4 * scheduler_config.max_num_seqs * cache_config.block_size,
195
+ ),
196
+ 64 * 1024,
197
+ scheduler_config.max_num_seqs * topk_tokens,
198
+ )
199
+ workspace_size = max(workspace_size, cache_config.block_size)
200
+ if vllm_config.parallel_config.decode_context_parallel_size > 1:
201
+ return align_mla_chunked_context_workspace_size(vllm_config, workspace_size)
202
+ return workspace_size
203
+
204
+ def _build_req_id_per_token(
205
+ self,
206
+ common_attn_metadata: "CommonAttentionMetadata",
207
+ ) -> torch.Tensor:
208
+ num_tokens = common_attn_metadata.num_actual_tokens
209
+ starts = np.asarray(common_attn_metadata.query_start_loc_cpu, dtype=np.int32)
210
+ seg_lengths = np.diff(starts)
211
+ req_id_per_token = np.repeat(
212
+ np.arange(seg_lengths.shape[0], dtype=np.int32), seg_lengths
213
+ )
214
+ self.req_id_per_token_buffer.fill_(0)
215
+ self.req_id_per_token_buffer[: req_id_per_token.shape[0]].copy_(
216
+ np_to_pinned_tensor(req_id_per_token), non_blocking=True
217
+ )
218
+ return self.req_id_per_token_buffer[:num_tokens]
219
+
220
+ def _build_chunked_context_fields(
221
+ self,
222
+ common_attn_metadata: "CommonAttentionMetadata",
223
+ num_decodes: int,
224
+ num_prefills: int,
225
+ prefill_query_lens_cpu: torch.Tensor | None,
226
+ ) -> "MLACommonPrefillMetadata.ChunkedContextMetadata | None":
227
+ if num_prefills == 0 or prefill_query_lens_cpu is None:
228
+ return None
229
+
230
+ seq_lens_cpu = common_attn_metadata.seq_lens_cpu_upper_bound
231
+ assert seq_lens_cpu is not None
232
+ context_lens_cpu = (
233
+ seq_lens_cpu[num_decodes : num_decodes + num_prefills]
234
+ - prefill_query_lens_cpu
235
+ )
236
+ qsl_cpu = common_attn_metadata.query_start_loc_cpu
237
+ prefill_query_start_loc_cpu = qsl_cpu[num_decodes:] - qsl_cpu[num_decodes]
238
+
239
+ return build_mla_chunked_context_metadata(
240
+ context_lens_cpu=context_lens_cpu,
241
+ prefill_query_start_loc_cpu=prefill_query_start_loc_cpu,
242
+ chunked_prefill_workspace=self.chunked_prefill_workspace,
243
+ chunked_prefill_workspace_size=self.chunked_prefill_workspace_size,
244
+ block_size=self.kv_cache_spec.block_size,
245
+ align_chunk_to_block=current_platform.is_cuda(),
246
+ device=self.device,
247
+ dcp_world_size=self.dcp_world_size,
248
+ dcp_local_block_size=self.dcp_local_block_size,
249
+ dcp_virtual_block_size=self.dcp_virtual_block_size,
250
+ dcp_manager=self.dcp_manager,
251
+ )
252
+
253
+ def build(
254
+ self,
255
+ common_prefix_len: int,
256
+ common_attn_metadata: "CommonAttentionMetadata",
257
+ fast_build: bool = False,
258
+ ) -> T:
259
+ req_id_per_token = self._build_req_id_per_token(common_attn_metadata)
260
+
261
+ num_decodes, num_prefills, num_decode_tokens, _ = split_decodes_and_prefills(
262
+ common_attn_metadata,
263
+ decode_threshold=self.reorder_batch_threshold or 1,
264
+ treat_short_extends_as_decodes=not self.use_pcp,
265
+ require_uniform=self.require_uniform_decodes,
266
+ )
267
+ (
268
+ prefill_query_start_loc,
269
+ prefill_max_query_len,
270
+ prefill_query_lens_cpu,
271
+ ) = self._build_prefill_fields(common_attn_metadata, num_decodes, num_prefills)
272
+
273
+ prefill_max_seq_len = 0
274
+ prefill: MLACommonPrefillMetadata | None = None
275
+ if num_prefills > 0 and self._prefill_backend is not None:
276
+ seq_lens_cpu = common_attn_metadata.seq_lens_cpu_upper_bound
277
+ assert seq_lens_cpu is not None
278
+ prefill_max_seq_len = int(
279
+ seq_lens_cpu[num_decodes : num_decodes + num_prefills].max().item()
280
+ )
281
+ prefill = MLACommonPrefillMetadata(
282
+ block_table=common_attn_metadata.block_table_tensor[num_decodes:, ...],
283
+ query_start_loc=prefill_query_start_loc,
284
+ max_query_len=prefill_max_query_len,
285
+ query_lens_cpu=prefill_query_lens_cpu,
286
+ chunked_context=self._build_chunked_context_fields(
287
+ common_attn_metadata,
288
+ num_decodes,
289
+ num_prefills,
290
+ prefill_query_lens_cpu,
291
+ ),
292
+ q_data_type=self.model_config.dtype,
293
+ output_dtype=self.model_config.dtype,
294
+ prefill_backend=self._prefill_backend,
295
+ use_dense_mha=(
296
+ prefill_max_seq_len <= self.topk_tokens
297
+ and not self.vllm_config.attention_config.sparse_mla_force_mqa
298
+ ),
299
+ topk_mask_workspace=self.topk_mask_workspace,
300
+ )
301
+ self._prefill_backend.prepare_metadata(prefill)
302
+
303
+ return self.metadata_cls( # type: ignore[call-arg]
304
+ num_reqs=common_attn_metadata.num_reqs,
305
+ max_query_len=common_attn_metadata.max_query_len,
306
+ max_seq_len=common_attn_metadata.max_seq_len,
307
+ num_actual_tokens=common_attn_metadata.num_actual_tokens,
308
+ query_start_loc=common_attn_metadata.query_start_loc,
309
+ slot_mapping=common_attn_metadata.slot_mapping,
310
+ block_table=common_attn_metadata.block_table_tensor,
311
+ req_id_per_token=req_id_per_token,
312
+ seq_lens=common_attn_metadata.seq_lens,
313
+ block_size=self.kv_cache_spec.block_size,
314
+ topk_tokens=self.topk_tokens,
315
+ num_decodes=num_decodes,
316
+ num_prefills=num_prefills,
317
+ num_decode_tokens=num_decode_tokens,
318
+ prefill_max_seq_len=prefill_max_seq_len,
319
+ prefill=prefill,
320
+ cp_kv_cache_interleave_size=self.cp_kv_cache_interleave_size,
321
+ )
322
+
323
+ @staticmethod
324
+ def _build_prefill_fields(
325
+ common_attn_metadata: "CommonAttentionMetadata",
326
+ num_decodes: int,
327
+ num_prefills: int,
328
+ ) -> tuple[
329
+ torch.Tensor | None, # prefill_query_start_loc
330
+ int, # prefill_max_query_len
331
+ torch.Tensor | None, # prefill_query_lens_cpu
332
+ ]:
333
+ if num_prefills == 0:
334
+ return None, 0, None
335
+
336
+ offset = common_attn_metadata.query_start_loc[num_decodes]
337
+ prefill_query_start_loc = (
338
+ common_attn_metadata.query_start_loc[num_decodes:] - offset
339
+ )
340
+
341
+ qsl_cpu = common_attn_metadata.query_start_loc_cpu
342
+ prefill_qsl_cpu = qsl_cpu[num_decodes:] - qsl_cpu[num_decodes]
343
+ prefill_query_lens = prefill_qsl_cpu[1:] - prefill_qsl_cpu[:-1]
344
+ prefill_max_query_len = int(prefill_query_lens.max().item())
345
+
346
+ return (
347
+ prefill_query_start_loc,
348
+ prefill_max_query_len,
349
+ prefill_query_lens,
350
+ )
351
+
352
+
353
+ @triton.jit
354
+ def _scatter_topk_kernel(
355
+ mask_ptr,
356
+ topk_ptr,
357
+ cu_q_lens_ptr,
358
+ num_words: tl.constexpr,
359
+ mask_row_stride: tl.constexpr,
360
+ num_topk: tl.constexpr,
361
+ topk_stride: tl.constexpr,
362
+ max_q_len: tl.constexpr,
363
+ BLOCK_TOPK: tl.constexpr,
364
+ BLOCK_WORDS: tl.constexpr,
365
+ ):
366
+ row_idx = tl.program_id(0)
367
+ row_ptr = mask_ptr + row_idx * mask_row_stride
368
+ word_offsets = tl.arange(0, BLOCK_WORDS)
369
+ tl.store(
370
+ row_ptr + word_offsets,
371
+ tl.zeros([BLOCK_WORDS], dtype=tl.int32),
372
+ mask=word_offsets < num_words,
373
+ )
374
+
375
+ req_idx = row_idx // max_q_len
376
+ q_local = row_idx % max_q_len
377
+ q_start = tl.load(cu_q_lens_ptr + req_idx)
378
+ q_len = tl.load(cu_q_lens_ptr + req_idx + 1) - q_start
379
+ if q_local < q_len:
380
+ src_row = q_start + q_local
381
+ offsets = tl.arange(0, BLOCK_TOPK)
382
+ in_range = offsets < num_topk
383
+ indices = tl.load(
384
+ topk_ptr + src_row * topk_stride + offsets,
385
+ mask=in_range,
386
+ other=-1,
387
+ )
388
+ valid = in_range & (indices >= 0)
389
+ word_indices = indices >> 5
390
+ bits = (1 << (indices & 31)).to(tl.int32)
391
+ tl.atomic_or(row_ptr + word_indices, bits, mask=valid)
392
+
393
+
394
+ @triton.jit
395
+ def _scatter_topk_single_req_kernel(
396
+ mask_ptr,
397
+ topk_ptr,
398
+ num_words: tl.constexpr,
399
+ mask_row_stride: tl.constexpr,
400
+ num_topk: tl.constexpr,
401
+ topk_stride: tl.constexpr,
402
+ total_q: tl.constexpr,
403
+ BLOCK_TOPK: tl.constexpr,
404
+ BLOCK_WORDS: tl.constexpr,
405
+ ):
406
+ row_idx = tl.program_id(0)
407
+ row_ptr = mask_ptr + row_idx * mask_row_stride
408
+ word_offsets = tl.arange(0, BLOCK_WORDS)
409
+ tl.store(
410
+ row_ptr + word_offsets,
411
+ tl.zeros([BLOCK_WORDS], dtype=tl.int32),
412
+ mask=word_offsets < num_words,
413
+ )
414
+ if row_idx < total_q:
415
+ offsets = tl.arange(0, BLOCK_TOPK)
416
+ in_range = offsets < num_topk
417
+ indices = tl.load(
418
+ topk_ptr + row_idx * topk_stride + offsets,
419
+ mask=in_range,
420
+ other=-1,
421
+ )
422
+ valid = in_range & (indices >= 0)
423
+ word_indices = indices >> 5
424
+ bits = (1 << (indices & 31)).to(tl.int32)
425
+ tl.atomic_or(row_ptr + word_indices, bits, mask=valid)
426
+
427
+
428
+ def _build_topk_mask(
429
+ topk_indices_per_req: list[torch.Tensor],
430
+ q_lens: list[int],
431
+ max_q_len: int,
432
+ max_seq_len: int,
433
+ out: torch.Tensor,
434
+ ) -> torch.Tensor:
435
+ """Build a bit-packed top-k mask into ``out[:B, :max_Q, :num_words]``."""
436
+ batch_size = len(q_lens)
437
+ num_words = (max_seq_len + 31) // 32
438
+ total_rows = batch_size * max_q_len
439
+ if total_rows == 0:
440
+ return out[:batch_size, :max_q_len, :num_words]
441
+
442
+ total_q = sum(q_lens)
443
+ mask_row_stride = out.stride(-2)
444
+ block_words = triton.next_power_of_2(num_words)
445
+
446
+ if batch_size == 1:
447
+ topk_packed = topk_indices_per_req[0]
448
+ num_topk = topk_packed.shape[1]
449
+ _scatter_topk_single_req_kernel[(max_q_len,)](
450
+ out,
451
+ topk_packed,
452
+ num_words=num_words,
453
+ mask_row_stride=mask_row_stride,
454
+ num_topk=num_topk,
455
+ topk_stride=topk_packed.stride(0),
456
+ total_q=total_q,
457
+ BLOCK_TOPK=triton.next_power_of_2(num_topk),
458
+ BLOCK_WORDS=block_words,
459
+ )
460
+ return out[:1, :max_q_len, :num_words]
461
+
462
+ topk_packed = torch.cat(topk_indices_per_req, dim=0)
463
+ num_topk = topk_packed.shape[1]
464
+ q_lens_tensor = np_to_pinned_tensor(np.asarray(q_lens, dtype=np.int32)).to(
465
+ out.device, non_blocking=True
466
+ )
467
+ cu_q_lens = out.new_zeros(batch_size + 1)
468
+ torch.cumsum(q_lens_tensor, dim=0, out=cu_q_lens[1:])
469
+ _scatter_topk_kernel[(total_rows,)](
470
+ out,
471
+ topk_packed,
472
+ cu_q_lens,
473
+ num_words=num_words,
474
+ mask_row_stride=mask_row_stride,
475
+ num_topk=num_topk,
476
+ topk_stride=topk_packed.stride(0),
477
+ max_q_len=max_q_len,
478
+ BLOCK_TOPK=triton.next_power_of_2(num_topk),
479
+ BLOCK_WORDS=block_words,
480
+ )
481
+ return out[:batch_size, :max_q_len, :num_words]
482
+
483
+
484
+ class SparseMLACommonImpl(MLACommonBaseImpl[T], Generic[T]):
485
+ """Sparse MLA base with dense and masked-MHA prefill paths."""
486
+
487
+ is_sparse = True
488
+
489
+ def __init__(
490
+ self,
491
+ num_heads: int,
492
+ head_size: int,
493
+ scale: float,
494
+ num_kv_heads: int,
495
+ alibi_slopes: list[float] | None,
496
+ sliding_window: int | None,
497
+ kv_cache_dtype: str,
498
+ logits_soft_cap: float | None,
499
+ attn_type: str,
500
+ kv_sharing_target_layer_name: str | None,
501
+ q_lora_rank: int | None,
502
+ kv_lora_rank: int,
503
+ qk_nope_head_dim: int,
504
+ qk_rope_head_dim: int,
505
+ qk_head_dim: int,
506
+ v_head_dim: int,
507
+ kv_b_proj: "ColumnParallelLinear",
508
+ indexer: object | None = None,
509
+ topk_indices_buffer: torch.Tensor | None = None,
510
+ q_pad_num_heads: int | None = None,
511
+ ) -> None:
512
+ super().__init__(
513
+ num_heads,
514
+ head_size,
515
+ scale,
516
+ num_kv_heads,
517
+ kv_cache_dtype,
518
+ kv_lora_rank,
519
+ qk_nope_head_dim,
520
+ qk_rope_head_dim,
521
+ qk_head_dim,
522
+ v_head_dim,
523
+ kv_b_proj,
524
+ )
525
+
526
+ # The indexer carries the shared buffer for normal layers and tests;
527
+ # the explicitly-passed buffer covers backbone skip layers, whose
528
+ # indexer is not constructed (see deepseek_v2.py).
529
+ self.topk_indices_buffer: torch.Tensor | None = (
530
+ indexer.topk_indices_buffer # type: ignore[attr-defined]
531
+ if indexer is not None
532
+ else topk_indices_buffer
533
+ )
534
+ self._use_flashinfer_concat_mla_k = (
535
+ has_flashinfer()
536
+ and which("ninja") is not None
537
+ and (self.num_heads == 128)
538
+ and (self.qk_nope_head_dim == 128)
539
+ and (self.qk_rope_head_dim == 64)
540
+ )
541
+ self.masked_mha_available = _is_masked_mha_available(
542
+ num_heads_total=num_heads * get_tensor_model_parallel_world_size(),
543
+ kv_lora_rank=kv_lora_rank,
544
+ qk_nope_head_dim=qk_nope_head_dim,
545
+ qk_rope_head_dim=qk_rope_head_dim,
546
+ v_head_dim=v_head_dim,
547
+ kv_cache_dtype=kv_cache_dtype,
548
+ )
549
+
550
+ @staticmethod
551
+ def masked_mha_workspace_fits(prefill: MLACommonPrefillMetadata) -> bool:
552
+ """Whether this prefill batch's top-k masks fit the workspace."""
553
+ workspace = prefill.topk_mask_workspace
554
+ if workspace is None or prefill.query_lens_cpu is None:
555
+ return False
556
+ max_context_chunk_seq_len = 0
557
+ if prefill.chunked_context is not None:
558
+ max_context_chunk_seq_len = max(
559
+ chunk.max_seq_len for chunk in prefill.chunked_context.chunks
560
+ )
561
+ fits = _masked_mha_workspace_fits(
562
+ batch_size=len(prefill.query_lens_cpu),
563
+ max_query_len=prefill.max_query_len,
564
+ max_context_chunk_seq_len=max_context_chunk_seq_len,
565
+ workspace_numel=workspace.numel(),
566
+ )
567
+ if not fits:
568
+ logger.warning_once(
569
+ "Sparse MLA top-k mask workspace (%d MiB) is too small for some "
570
+ "prefill batches; those fall back to slower sparse MQA.",
571
+ workspace.numel() * torch.int32.itemsize // (1024 * 1024),
572
+ )
573
+ return fits
574
+
575
+ @staticmethod
576
+ def _slice_topk_per_req(
577
+ topk_all: torch.Tensor,
578
+ q_lens: list[int],
579
+ ) -> list[torch.Tensor]:
580
+ topk_per_req = []
581
+ offset = 0
582
+ for q_len in q_lens:
583
+ topk_per_req.append(topk_all[offset : offset + q_len])
584
+ offset += q_len
585
+ return topk_per_req
586
+
587
+ @staticmethod
588
+ def _remap_topk_to_ranges(
589
+ topk_per_req: list[torch.Tensor],
590
+ range_starts: list[int] | torch.Tensor,
591
+ range_lens: list[int],
592
+ ) -> list[torch.Tensor]:
593
+ remapped = []
594
+ for topk, start, length in zip(topk_per_req, range_starts, range_lens):
595
+ valid = (topk >= start) & (topk < start + length)
596
+ remapped.append(torch.where(valid, topk - start, -1))
597
+ return remapped
598
+
599
+ def _project_kv(
600
+ self, kv_c_normed: torch.Tensor, k_pe: torch.Tensor
601
+ ) -> tuple[torch.Tensor, torch.Tensor]:
602
+ kv_nope = self.kv_b_proj(kv_c_normed)[0].view(
603
+ -1,
604
+ self.num_heads,
605
+ self.qk_nope_head_dim + self.v_head_dim,
606
+ )
607
+ k_nope, v = kv_nope.split([self.qk_nope_head_dim, self.v_head_dim], dim=-1)
608
+ return self._concat_k_nope_k_pe(k_nope, k_pe), v
609
+
610
+ @staticmethod
611
+ def _try_build_global_mask(
612
+ topk_per_req: list[torch.Tensor],
613
+ q_lens: list[int],
614
+ max_query_len: int,
615
+ max_seq_len: int,
616
+ topk_mask_workspace: torch.Tensor,
617
+ ) -> torch.Tensor | None:
618
+ """Build a full-sequence top-k mask if it fits within the budget.
619
+
620
+ When the mask fits, it is reused across the suffix and all context
621
+ chunks, avoiding per-chunk mask rebuilds. Returns None when the
622
+ mask is too large, signalling the caller to fall back to per-chunk
623
+ index remapping.
624
+ """
625
+ batch_size, padded_q_len, num_words_padded = _topk_mask_shape(
626
+ len(q_lens),
627
+ max_query_len,
628
+ max_seq_len,
629
+ reserve_key_starts_word=True,
630
+ )
631
+ needed = batch_size * padded_q_len * num_words_padded
632
+ if needed > topk_mask_workspace.numel():
633
+ return None
634
+
635
+ mask = topk_mask_workspace[:needed].view(
636
+ batch_size, padded_q_len, num_words_padded
637
+ )
638
+ _build_topk_mask(
639
+ topk_per_req,
640
+ q_lens,
641
+ padded_q_len,
642
+ max_seq_len,
643
+ mask,
644
+ )
645
+ return mask
646
+
647
+ def _run_masked_mha(
648
+ self,
649
+ q: torch.Tensor,
650
+ k: torch.Tensor,
651
+ v: torch.Tensor,
652
+ cu_seqlens_q: torch.Tensor,
653
+ cu_seqlens_k: torch.Tensor,
654
+ max_seqlen_q: int,
655
+ max_seqlen_k: int,
656
+ topk_per_req: list[torch.Tensor],
657
+ q_lens: list[int],
658
+ causal: bool,
659
+ return_softmax_lse: bool = False,
660
+ dense_mask: torch.Tensor | None = None,
661
+ key_starts: torch.Tensor | None = None,
662
+ topk_mask_workspace: torch.Tensor | None = None,
663
+ ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
664
+ from vllm.model_executor.layers.attention.sparse_mla_mask import (
665
+ dense_mask_mod,
666
+ offset_dense_mask_mod,
667
+ )
668
+ from vllm.vllm_flash_attn import flash_attn_varlen_func
669
+
670
+ if dense_mask is None:
671
+ assert topk_mask_workspace is not None
672
+ batch_size, padded_q_len, num_words = _topk_mask_shape(
673
+ len(q_lens), max_seqlen_q, max_seqlen_k
674
+ )
675
+ words_needed = batch_size * padded_q_len * num_words
676
+ if words_needed > topk_mask_workspace.numel():
677
+ raise ValueError(
678
+ f"Sparse MLA top-k mask needs {words_needed} int32 words (batch="
679
+ f"{len(q_lens)}, q={max_seqlen_q}, k={max_seqlen_k}) but the "
680
+ f"workspace holds {topk_mask_workspace.numel()}."
681
+ )
682
+ workspace_3d = topk_mask_workspace[:words_needed].view(
683
+ batch_size, padded_q_len, num_words
684
+ )
685
+ dense_mask = _build_topk_mask(
686
+ topk_per_req,
687
+ q_lens,
688
+ padded_q_len,
689
+ max_seqlen_k,
690
+ workspace_3d,
691
+ )
692
+ if key_starts is not None:
693
+ dense_mask[:, 0, -1].copy_(key_starts)
694
+ kwargs = {
695
+ "q": q,
696
+ "k": k,
697
+ "v": v,
698
+ "cu_seqlens_q": cu_seqlens_q,
699
+ "cu_seqlens_k": cu_seqlens_k,
700
+ "max_seqlen_q": max_seqlen_q,
701
+ "max_seqlen_k": max_seqlen_k,
702
+ "softmax_scale": self.scale,
703
+ "return_softmax_lse": return_softmax_lse,
704
+ "fa_version": 4,
705
+ "mask_mod": dense_mask_mod if key_starts is None else offset_dense_mask_mod,
706
+ "aux_tensors": [dense_mask],
707
+ "aux_tensor_leading_dims": [2],
708
+ "causal": causal,
709
+ }
710
+
711
+ return flash_attn_varlen_func(**kwargs)
712
+
713
+ def _compute_context_mha(
714
+ self,
715
+ q: torch.Tensor,
716
+ kv_c_and_k_pe_cache: torch.Tensor,
717
+ prefill_metadata: MLACommonPrefillMetadata,
718
+ k_scale: torch.Tensor,
719
+ q_lens: list[int],
720
+ topk_per_req: list[torch.Tensor],
721
+ dense_mask: torch.Tensor | None = None,
722
+ ) -> tuple[torch.Tensor, torch.Tensor]:
723
+ if self.dcp_world_size > 1:
724
+ raise NotImplementedError(
725
+ "Masked MHA with context does not yet support decode context "
726
+ "parallelism"
727
+ )
728
+
729
+ chunked_context = prefill_metadata.chunked_context
730
+ assert chunked_context is not None
731
+ output: torch.Tensor | None = None
732
+ output_lse: torch.Tensor | None = None
733
+ workspace = chunked_context.workspace
734
+
735
+ from vllm import kvq
736
+
737
+ for chunk in chunked_context.chunks:
738
+ toks = chunk.num_context_tokens
739
+ requests = chunk.request_slice
740
+ if kvq.enabled() and self.kv_cache_dtype == "fp8_ds_mla":
741
+ # KVQ (glm-max): compact-row cache; Triton gather+dequant.
742
+ from vllm import kvq_kernels
743
+
744
+ kvq_kernels.gather_dequant(
745
+ kv_c_and_k_pe_cache,
746
+ workspace,
747
+ prefill_metadata.block_table[requests],
748
+ chunk.cu_seq_lens,
749
+ chunk.token_to_seq,
750
+ toks,
751
+ chunk.starts,
752
+ kv_c_and_k_pe_cache.shape[1],
753
+ )
754
+ else:
755
+ ops.gather_and_maybe_dequant_cache(
756
+ src_cache=kv_c_and_k_pe_cache,
757
+ dst=workspace,
758
+ block_table=prefill_metadata.block_table[requests],
759
+ cu_seq_lens=chunk.cu_seq_lens,
760
+ token_to_seq=chunk.token_to_seq,
761
+ num_tokens=toks,
762
+ kv_cache_dtype=self.kv_cache_dtype,
763
+ scale=k_scale,
764
+ seq_starts=chunk.starts,
765
+ )
766
+
767
+ chunk_kv_c = workspace[:toks, : self.kv_lora_rank]
768
+ chunk_k_pe = workspace[:toks, self.kv_lora_rank :].unsqueeze(1)
769
+ k, v = self._project_kv(chunk_kv_c, chunk_k_pe)
770
+ if dense_mask is not None:
771
+ chunk_mask: torch.Tensor | None = dense_mask[requests]
772
+ chunk_topk = topk_per_req[requests]
773
+ key_starts: torch.Tensor | None = chunk.starts
774
+ else:
775
+ chunk_mask = None
776
+ chunk_topk = self._remap_topk_to_ranges(
777
+ topk_per_req[requests],
778
+ chunk.starts,
779
+ chunk.seq_lens.tolist(),
780
+ )
781
+ key_starts = None
782
+ attn_out, lse = self._run_masked_mha(
783
+ q=q[chunk.token_slice],
784
+ k=k,
785
+ v=v,
786
+ cu_seqlens_q=chunk.query_start_loc,
787
+ cu_seqlens_k=chunk.cu_seq_lens,
788
+ max_seqlen_q=chunk.max_query_len,
789
+ max_seqlen_k=chunk.max_seq_len,
790
+ topk_per_req=chunk_topk,
791
+ q_lens=q_lens[requests],
792
+ causal=False,
793
+ return_softmax_lse=True,
794
+ dense_mask=chunk_mask,
795
+ key_starts=key_starts,
796
+ topk_mask_workspace=prefill_metadata.topk_mask_workspace,
797
+ )
798
+
799
+ if output is None:
800
+ if (
801
+ len(chunked_context.chunks) == 1
802
+ and not chunked_context.empty_token_slices
803
+ ):
804
+ return attn_out, lse
805
+ output, output_lse = init_mla_context_partial(
806
+ chunked_context,
807
+ attn_out,
808
+ lse,
809
+ num_tokens=q.shape[0],
810
+ )
811
+ accumulate_mla_context_chunk(chunk, attn_out, lse, output, output_lse)
812
+
813
+ assert output is not None and output_lse is not None
814
+ return output, output_lse
815
+
816
+ def forward_mha( # type: ignore[override]
817
+ self,
818
+ q: torch.Tensor,
819
+ kv_c_normed: torch.Tensor,
820
+ k_pe: torch.Tensor,
821
+ kv_c_and_k_pe_cache: torch.Tensor,
822
+ attn_metadata: T,
823
+ k_scale: torch.Tensor,
824
+ output: torch.Tensor,
825
+ output_scale: torch.Tensor | None = None,
826
+ ) -> None:
827
+ prefill_max_seq_len = attn_metadata.prefill_max_seq_len # type: ignore[attr-defined]
828
+ topk_tokens = attn_metadata.topk_tokens # type: ignore[attr-defined]
829
+ force_dense = getattr(self, "_sparse_mla_force_dense_mha", False)
830
+ force_masked = getattr(self, "_sparse_mla_force_masked_mha", False)
831
+ if force_dense or (prefill_max_seq_len <= topk_tokens and not force_masked):
832
+ return super().forward_mha(
833
+ q,
834
+ kv_c_normed,
835
+ k_pe,
836
+ kv_c_and_k_pe_cache,
837
+ cast(MLACommonMetadata, attn_metadata),
838
+ k_scale,
839
+ output,
840
+ output_scale,
841
+ )
842
+
843
+ assert output_scale is None
844
+ assert self.masked_mha_available
845
+ prefill_metadata = attn_metadata.prefill # type: ignore[attr-defined]
846
+ assert prefill_metadata is not None
847
+ assert prefill_metadata.query_lens_cpu is not None
848
+ assert self.topk_indices_buffer is not None
849
+
850
+ q_lens = prefill_metadata.query_lens_cpu.tolist()
851
+ num_decode_tokens = attn_metadata.num_decode_tokens # type: ignore[attr-defined]
852
+ topk_all = self.topk_indices_buffer[
853
+ num_decode_tokens : num_decode_tokens + q.shape[0]
854
+ ]
855
+ topk_per_req = self._slice_topk_per_req(topk_all, q_lens)
856
+
857
+ k, v = self._project_kv(kv_c_normed, k_pe)
858
+ chunked_context = prefill_metadata.chunked_context
859
+ if chunked_context is None:
860
+ attn_out = self._run_masked_mha(
861
+ q=q,
862
+ k=k,
863
+ v=v,
864
+ cu_seqlens_q=prefill_metadata.query_start_loc,
865
+ cu_seqlens_k=prefill_metadata.query_start_loc,
866
+ max_seqlen_q=prefill_metadata.max_query_len,
867
+ max_seqlen_k=prefill_metadata.max_query_len,
868
+ topk_per_req=topk_per_req,
869
+ q_lens=q_lens,
870
+ causal=True,
871
+ topk_mask_workspace=prefill_metadata.topk_mask_workspace,
872
+ )
873
+ assert isinstance(attn_out, torch.Tensor)
874
+ output.copy_(attn_out[..., : self.v_head_dim].flatten(start_dim=-2))
875
+ return
876
+
877
+ context_lens = chunked_context.context_lens_list
878
+ dense_mask = self._try_build_global_mask(
879
+ topk_per_req,
880
+ q_lens,
881
+ prefill_metadata.max_query_len,
882
+ prefill_max_seq_len,
883
+ prefill_metadata.topk_mask_workspace,
884
+ )
885
+ if dense_mask is not None:
886
+ suffix_topk = topk_per_req
887
+ else:
888
+ suffix_topk = self._remap_topk_to_ranges(topk_per_req, context_lens, q_lens)
889
+ suffix_output, suffix_lse = self._run_masked_mha(
890
+ q=q,
891
+ k=k,
892
+ v=v,
893
+ cu_seqlens_q=prefill_metadata.query_start_loc,
894
+ cu_seqlens_k=prefill_metadata.query_start_loc,
895
+ max_seqlen_q=prefill_metadata.max_query_len,
896
+ max_seqlen_k=prefill_metadata.max_query_len,
897
+ topk_per_req=suffix_topk,
898
+ q_lens=q_lens,
899
+ causal=True,
900
+ return_softmax_lse=True,
901
+ dense_mask=dense_mask,
902
+ key_starts=(
903
+ chunked_context.context_lens if dense_mask is not None else None
904
+ ),
905
+ topk_mask_workspace=prefill_metadata.topk_mask_workspace,
906
+ )
907
+ context_output, context_lse = self._compute_context_mha(
908
+ q=q,
909
+ kv_c_and_k_pe_cache=kv_c_and_k_pe_cache,
910
+ prefill_metadata=prefill_metadata,
911
+ k_scale=k_scale,
912
+ q_lens=q_lens,
913
+ topk_per_req=topk_per_req,
914
+ dense_mask=dense_mask,
915
+ )
916
+ merge_attn_states(
917
+ output=output.view(-1, self.num_heads, self.v_head_dim),
918
+ prefix_output=context_output[..., : self.v_head_dim],
919
+ prefix_lse=context_lse,
920
+ suffix_output=suffix_output[..., : self.v_head_dim],
921
+ suffix_lse=suffix_lse,
922
+ )
serving/kda.py ADDED
@@ -0,0 +1,655 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+ """GLM5-Next KDA (linear-attention) layer.
4
+
5
+ Model-specific, self-contained KDA: separate q/k/v short-conv + the GLM5-Next
6
+ spec-decode verify path + the bounded ``safe_gate`` variant, and ``_forward`` is
7
+ an eager break point under Breakable CUDA Graph
8
+ (``@eager_break_during_capture``).
9
+
10
+ Moved out of the shared ``kimi_gdn_linear_attn.py`` (which reverts to Kimi
11
+ Linear's fused-conv version): the separate-conv layout + spec-verify are
12
+ GLM5-Next-only. ``forward`` calls ``self._forward`` directly (no
13
+ ``torch.ops.vllm.kda_attention`` indirection) so the only un-capturable work is
14
+ the decorated ``_forward``.
15
+ """
16
+
17
+ import torch
18
+ from torch import nn
19
+
20
+ from vllm.compilation.breakable_cudagraph import eager_break_during_capture
21
+ from vllm.config import VllmConfig, get_current_vllm_config
22
+ from vllm.distributed import divide
23
+ from vllm.forward_context import get_forward_context
24
+ from vllm.model_executor.layers.linear import (
25
+ ColumnParallelLinear,
26
+ MergedColumnParallelLinear,
27
+ RowParallelLinear,
28
+ )
29
+ from vllm.model_executor.layers.mamba.gdn.base import GatedDeltaNetAttention
30
+ from vllm.model_executor.layers.mamba.mamba_utils import (
31
+ MambaStateDtypeCalculator,
32
+ MambaStateShapeCalculator,
33
+ is_conv_state_dim_first,
34
+ )
35
+ from vllm.model_executor.layers.mamba.ops.causal_conv1d import (
36
+ causal_conv1d_fn,
37
+ causal_conv1d_update,
38
+ )
39
+ from vllm.model_executor.layers.mamba.ops.gather_initial_states import (
40
+ gather_initial_states,
41
+ )
42
+ from vllm.model_executor.layers.mamba.ops.scatter_states import scatter_states
43
+ from vllm.model_executor.model_loader.weight_utils import sharded_weight_loader
44
+ from vllm.model_executor.utils import (
45
+ maybe_disable_graph_partition,
46
+ set_weight_attrs,
47
+ )
48
+ from vllm.platforms import current_platform
49
+ from vllm.third_party.flash_linear_attention.ops.kda import (
50
+ FusedRMSNormGated,
51
+ chunk_kda_with_fused_gate,
52
+ fused_recurrent_kda,
53
+ )
54
+ from vllm.transformers_utils.configs.kimi_linear import KimiLinearConfig
55
+ from vllm.v1.attention.backends.gdn_attn import GDNAttentionMetadata
56
+
57
+
58
+ class _Glm5NextMergedColumnParallelLinear(MergedColumnParallelLinear):
59
+ """Merged projection with multiple replicated output shards.
60
+
61
+ Extends K3's ``_KimiGDNMergedColumnParallelLinear`` to support two
62
+ replicated shards (f_a, g_a) instead of one. Pre-multiplies each
63
+ replicated entry's output_size by tp_size so the per-rank shard
64
+ divides back to the full size, and forces tp_rank=0 during weight
65
+ loading for replicated shards.
66
+ """
67
+
68
+ def __init__(
69
+ self,
70
+ input_size: int,
71
+ output_sizes: list[int],
72
+ replicated_shard_ids: tuple[int, ...],
73
+ tp_size: int,
74
+ **kwargs,
75
+ ) -> None:
76
+ self.replicated_shard_ids = set(replicated_shard_ids)
77
+ output_sizes = output_sizes.copy()
78
+ for sid in self.replicated_shard_ids:
79
+ output_sizes[sid] *= tp_size
80
+ super().__init__(input_size, output_sizes, **kwargs)
81
+
82
+ def weight_loader(
83
+ self,
84
+ param: nn.Parameter,
85
+ loaded_weight: torch.Tensor,
86
+ loaded_shard_id: tuple[int, ...] | int | None = None,
87
+ ) -> None:
88
+ tp_rank = self.tp_rank
89
+ param_tp_rank = getattr(param, "tp_rank", None)
90
+ if loaded_shard_id in self.replicated_shard_ids:
91
+ self.tp_rank = 0
92
+ if param_tp_rank is not None:
93
+ param.tp_rank = 0
94
+ try:
95
+ super().weight_loader(param, loaded_weight, loaded_shard_id)
96
+ finally:
97
+ self.tp_rank = tp_rank
98
+ if param_tp_rank is not None:
99
+ param.tp_rank = param_tp_rank
100
+
101
+ def weight_loader_v2(
102
+ self,
103
+ param: nn.Parameter,
104
+ loaded_weight: torch.Tensor,
105
+ loaded_shard_id: tuple[int, ...] | int | None = None,
106
+ ) -> None:
107
+ tp_rank = self.tp_rank
108
+ param_tp_rank = getattr(param, "tp_rank", None)
109
+ if loaded_shard_id in self.replicated_shard_ids:
110
+ self.tp_rank = 0
111
+ if param_tp_rank is not None:
112
+ param.tp_rank = 0
113
+ try:
114
+ super().weight_loader_v2(param, loaded_weight, loaded_shard_id)
115
+ finally:
116
+ self.tp_rank = tp_rank
117
+ if param_tp_rank is not None:
118
+ param.tp_rank = param_tp_rank
119
+
120
+
121
+ @torch.compile(
122
+ dynamic=True,
123
+ backend=current_platform.simple_compile_backend,
124
+ options=maybe_disable_graph_partition(current_platform.simple_compile_backend),
125
+ )
126
+ def _cast_sigmoid(x: torch.Tensor) -> torch.Tensor:
127
+ """Fuse the fp32 cast + sigmoid into one Inductor kernel."""
128
+ return x.float().sigmoid()
129
+
130
+
131
+ class Glm5NextLinearAttention(GatedDeltaNetAttention):
132
+ # Declared int (set in __init__ from config) so mypy doesn't see the
133
+ # getattr-derived `Any | None` at the kernel call sites.
134
+ head_dim: int
135
+ num_heads: int
136
+ conv_size: int
137
+
138
+ def get_state_dtype(
139
+ self,
140
+ ) -> tuple[torch.dtype, torch.dtype]:
141
+ if self.model_config is None or self.cache_config is None:
142
+ raise ValueError("model_config and cache_config must be set")
143
+ return MambaStateDtypeCalculator.kda_state_dtype(
144
+ self.model_config.dtype, self.cache_config.mamba_cache_dtype
145
+ )
146
+
147
+ def get_state_shape(
148
+ self,
149
+ ) -> tuple[tuple[int, ...], tuple[int, ...]]:
150
+ # conv_state width must include num_spec so the spec-decode conv update
151
+ # (causal_conv1d_update with num_accepted_tokens + max_query_len) can
152
+ # slide the window across the draft-verify tokens without reading past
153
+ # the allocated width. Matches qwen_gdn_linear_attn.get_state_shape.
154
+ return MambaStateShapeCalculator.kda_state_shape(
155
+ self.tp_size,
156
+ self.num_heads,
157
+ self.head_dim,
158
+ conv_kernel_size=self.conv_size,
159
+ num_spec=self.num_spec,
160
+ )
161
+
162
+ def __init__(
163
+ self,
164
+ config: KimiLinearConfig,
165
+ vllm_config: VllmConfig,
166
+ prefix: str = "",
167
+ ) -> None:
168
+ # LOCAL PATCH (fp8attn-r2): pass the real quant config through so KDA
169
+ # projections can be FP8-resident when the checkpoint declares them in
170
+ # a MIXED_PRECISION quantized_layers manifest. Checkpoints that keep
171
+ # attention BF16 (e.g. the stock NVFP4 export) are unaffected: their
172
+ # quantization_config `ignore` list names every self_attn module
173
+ # (including the fused `in_proj_qkvbfg_a` spelling), so every KDA
174
+ # linear still resolves to UnquantizedLinearMethod.
175
+ # Was: save/None/restore strip of vllm_config.quant_config.
176
+ super().__init__(config, vllm_config, prefix)
177
+
178
+ # Linear-attention head config: read the flattened top-level fields when
179
+ # present (new schema); fall back to the legacy linear_attn_config dict
180
+ # otherwise (shared base is also used by KimiLinearConfig). Narrow via
181
+ # locals so the int-typed attrs are assigned a non-None value.
182
+ head_dim = getattr(config, "linear_head_dim", None)
183
+ num_heads = getattr(config, "linear_num_heads", None)
184
+ conv_size = getattr(config, "linear_conv_kernel_dim", None)
185
+ if head_dim is None or num_heads is None or conv_size is None:
186
+ kda_config = config.linear_attn_config # type: ignore[attr-defined]
187
+ assert kda_config is not None, "linear_attn_config must be set"
188
+ head_dim = kda_config["head_dim"]
189
+ num_heads = kda_config["num_heads"]
190
+ conv_size = kda_config["short_conv_kernel_size"]
191
+ assert head_dim is not None
192
+ assert num_heads is not None
193
+ assert conv_size is not None
194
+ self.head_dim = head_dim
195
+ self.num_heads = num_heads
196
+ self.conv_size = conv_size
197
+ assert self.num_heads % self.tp_size == 0
198
+ self.local_num_heads = divide(self.num_heads, self.tp_size)
199
+
200
+ projection_size = self.head_dim * self.num_heads
201
+ self.local_projection_size = divide(projection_size, self.tp_size)
202
+
203
+ # Merge q, k, v, b, f_a, g_a projections into one GEMM (6→1 launches).
204
+ # Order matches checkpoint's fused_qkvbfg_a_proj convention.
205
+ # Shards 4 (f_a) and 5 (g_a) are replicated across TP ranks.
206
+ self.in_proj_qkvbfg_a = _Glm5NextMergedColumnParallelLinear(
207
+ self.hidden_size,
208
+ [
209
+ projection_size, # q (shard 0)
210
+ projection_size, # k (shard 1)
211
+ projection_size, # v (shard 2)
212
+ self.num_heads, # b (shard 3)
213
+ self.head_dim, # f_a (shard 4, replicated)
214
+ self.head_dim, # g_a (shard 5, replicated)
215
+ ],
216
+ replicated_shard_ids=(4, 5),
217
+ tp_size=self.tp_size,
218
+ bias=False,
219
+ quant_config=self.quant_config,
220
+ prefix=f"{prefix}.in_proj_qkvbfg_a",
221
+ )
222
+
223
+ self.f_b_proj = ColumnParallelLinear(
224
+ self.head_dim,
225
+ projection_size,
226
+ bias=False,
227
+ quant_config=self.quant_config,
228
+ prefix=f"{prefix}.f_b_proj",
229
+ )
230
+ self.dt_bias = nn.Parameter(
231
+ torch.empty(divide(projection_size, self.tp_size), dtype=torch.float32)
232
+ )
233
+
234
+ set_weight_attrs(self.dt_bias, {"weight_loader": sharded_weight_loader(0)})
235
+
236
+ self.q_conv1d = ColumnParallelLinear(
237
+ input_size=self.conv_size,
238
+ output_size=projection_size,
239
+ bias=False,
240
+ params_dtype=torch.float32,
241
+ prefix=f"{prefix}.q_conv1d",
242
+ )
243
+ self.k_conv1d = ColumnParallelLinear(
244
+ input_size=self.conv_size,
245
+ output_size=projection_size,
246
+ bias=False,
247
+ params_dtype=torch.float32,
248
+ prefix=f"{prefix}.k_conv1d",
249
+ )
250
+ self.v_conv1d = ColumnParallelLinear(
251
+ input_size=self.conv_size,
252
+ output_size=projection_size,
253
+ bias=False,
254
+ params_dtype=torch.float32,
255
+ prefix=f"{prefix}.v_conv1d",
256
+ )
257
+ # unsqueeze to fit conv1d weights shape into the linear weights shape.
258
+ # Can't do this in `weight_loader` since it already exists in
259
+ # `ColumnParallelLinear` and `set_weight_attrs`
260
+ # doesn't allow to override it
261
+ self.q_conv1d.weight.data = self.q_conv1d.weight.data.unsqueeze(1)
262
+ self.k_conv1d.weight.data = self.k_conv1d.weight.data.unsqueeze(1)
263
+ self.v_conv1d.weight.data = self.v_conv1d.weight.data.unsqueeze(1)
264
+ # Lazily-built merged q|k|v conv weight (built on first forward, after
265
+ # weights are loaded). See _forward.
266
+ self._merged_conv_weight: torch.Tensor | None = None
267
+
268
+ self.A_log = nn.Parameter(
269
+ torch.empty(1, 1, self.local_num_heads, 1, dtype=torch.float32)
270
+ )
271
+ set_weight_attrs(self.A_log, {"weight_loader": sharded_weight_loader(2)})
272
+
273
+ self.g_b_proj = ColumnParallelLinear(
274
+ self.head_dim,
275
+ projection_size,
276
+ bias=False,
277
+ quant_config=self.quant_config,
278
+ prefix=f"{prefix}.g_b_proj",
279
+ )
280
+ self.o_norm = FusedRMSNormGated(self.head_dim, activation="sigmoid")
281
+ self.o_proj = RowParallelLinear(
282
+ projection_size,
283
+ self.hidden_size,
284
+ bias=False,
285
+ quant_config=self.quant_config,
286
+ prefix=f"{prefix}.o_proj",
287
+ )
288
+
289
+ compilation_config = get_current_vllm_config().compilation_config
290
+ if prefix in compilation_config.static_forward_context:
291
+ raise ValueError(f"Duplicate layer name: {prefix}")
292
+ compilation_config.static_forward_context[prefix] = self
293
+
294
+ # GLM5-Next checkpoints A_log as 1-D (num_heads,); the param is 4-D, so
295
+ # reshape on load before the sharded loader runs.
296
+ def _a_log_weight_loader(param, loaded_weight):
297
+ if loaded_weight.dim() == 1:
298
+ loaded_weight = loaded_weight.view([1, 1, -1, 1])
299
+ return sharded_weight_loader(2)(param, loaded_weight)
300
+
301
+ self.A_log.weight_loader = _a_log_weight_loader
302
+
303
+ # Bounded KDA gate variant: GLM5-Next uses
304
+ # y = lower_bound * sigmoid(exp(A)*(g+g_bias)) instead of the default
305
+ # unbounded y = -exp(A)*softplus(g+g_bias). Read by _forward.
306
+ linear_lower_bound = getattr(config, "linear_lower_bound", None)
307
+ if linear_lower_bound is not None:
308
+ self.kda_safe_gate = True
309
+ self.kda_lower_bound = linear_lower_bound
310
+ else:
311
+ legacy = getattr(config, "linear_attn_config", None) or {}
312
+ if legacy.get("safe_gate", True):
313
+ self.kda_safe_gate = True
314
+ self.kda_lower_bound = legacy.get("lower_bound", -5.0)
315
+ else:
316
+ self.kda_safe_gate = False
317
+ self.kda_lower_bound = -5.0
318
+ # Process-global conv-state layout, resolved once here instead of on
319
+ # every _forward call (it reads an env-derived flag each time).
320
+ self._conv_state_dim_first = is_conv_state_dim_first()
321
+
322
+ def forward(
323
+ self,
324
+ hidden_states: torch.Tensor,
325
+ positions: torch.Tensor,
326
+ ) -> torch.Tensor:
327
+ num_tokens = hidden_states.size(0)
328
+ # One merged GEMM for q, k, v, b, f_a, g_a (replaces 6 separate GEMMs).
329
+ projected = self.in_proj_qkvbfg_a(hidden_states)[0]
330
+ qkv, beta_raw, f_a, g_a = projected.split(
331
+ [
332
+ 3 * self.local_projection_size,
333
+ self.local_num_heads,
334
+ self.head_dim,
335
+ self.head_dim,
336
+ ],
337
+ dim=-1,
338
+ )
339
+
340
+ # Beta stays raw (bf16) here: the recurrent kernel sigmoids it in fp32
341
+ # at load (SIGMOID_BETA), and only the chunked prefill path needs the
342
+ # pre-computed fp32 sigmoid — computed lazily in _forward. Pure decode
343
+ # / spec-verify steps then skip the _cast_sigmoid kernel and its fp32
344
+ # intermediate entirely.
345
+ beta = beta_raw.unsqueeze(0)
346
+ g1 = self.f_b_proj(f_a)[0]
347
+ g1 = g1.reshape(1, -1, self.local_num_heads, self.head_dim)
348
+
349
+ g_proj_states = self.g_b_proj(g_a)[0]
350
+ # Must stay 3D: rms_norm_gated reads H from g.shape[-2].
351
+ g2 = g_proj_states.reshape(-1, self.local_num_heads, self.head_dim)
352
+
353
+ core_attn_out = torch.empty(
354
+ (1, num_tokens, self.local_num_heads, self.head_dim),
355
+ dtype=hidden_states.dtype,
356
+ device=hidden_states.device,
357
+ )
358
+ # Call _forward directly (not via the registered op) so the KDA core
359
+ # is an eager break point under Breakable CG, mirroring KimiK3's KDA
360
+ # (vllm/models/kimi_k3/nvidia/kda.py). torch.ops.vllm.kda_attention is
361
+ # neither a splitting op nor @eager_break_during_capture-decorated, so
362
+ # routing through it lets the host-branching prefill body be
363
+ # Inductor-compiled + stream-captured under PIECEWISE -> stale garbage.
364
+ # qkv stays merged through the short-conv (one conv call, not three).
365
+ self._forward(
366
+ qkv_proj_states=qkv,
367
+ g1=g1,
368
+ beta=beta,
369
+ core_attn_out=core_attn_out,
370
+ )
371
+ core_attn_out = self.o_norm(core_attn_out, g2)
372
+ core_attn_out = core_attn_out.reshape(core_attn_out.size(1), -1)
373
+ return self.o_proj(core_attn_out)[0]
374
+
375
+ @eager_break_during_capture
376
+ def _forward(
377
+ self,
378
+ qkv_proj_states: torch.Tensor,
379
+ g1: torch.Tensor,
380
+ beta: torch.Tensor,
381
+ core_attn_out: torch.Tensor,
382
+ ) -> None:
383
+ forward_context = get_forward_context()
384
+ attn_metadata_raw = forward_context.attn_metadata
385
+
386
+ if attn_metadata_raw is None:
387
+ # # V1 profile run
388
+ return
389
+
390
+ assert isinstance(attn_metadata_raw, dict)
391
+ attn_metadata_narrowed = attn_metadata_raw[self.prefix]
392
+ assert isinstance(attn_metadata_narrowed, GDNAttentionMetadata)
393
+ has_initial_state = attn_metadata_narrowed.has_initial_state
394
+ non_spec_query_start_loc = attn_metadata_narrowed.non_spec_query_start_loc
395
+ non_spec_state_indices_tensor = (
396
+ attn_metadata_narrowed.non_spec_state_indices_tensor
397
+ ) # noqa: E501
398
+ num_actual_tokens = attn_metadata_narrowed.num_actual_tokens
399
+ # Spec-decode metadata (all None when speculative decoding is disabled).
400
+ spec_sequence_masks = attn_metadata_narrowed.spec_sequence_masks
401
+ spec_query_start_loc = attn_metadata_narrowed.spec_query_start_loc
402
+ spec_state_indices_tensor = attn_metadata_narrowed.spec_state_indices_tensor
403
+ spec_token_indx = attn_metadata_narrowed.spec_token_indx
404
+ non_spec_token_indx = attn_metadata_narrowed.non_spec_token_indx
405
+ num_accepted_tokens = attn_metadata_narrowed.num_accepted_tokens
406
+ num_spec_decodes = attn_metadata_narrowed.num_spec_decodes
407
+ use_spec = spec_sequence_masks is not None and num_spec_decodes > 0
408
+ # KDA gate variant: GLM5-Next checkpoints with
409
+ # linear_attn_config["safe_gate"]=True use the bounded gate
410
+ # y=lower_bound*sigmoid(exp(A)*(g+g_bias)) instead of the default
411
+ # unbounded y=-exp(A)*softplus(g+g_bias). Both attrs are always set
412
+ # in __init__ (this class is GLM5Next-only).
413
+ safe_gate = self.kda_safe_gate
414
+ lower_bound = self.kda_lower_bound
415
+ constant_caches = self.kv_cache
416
+
417
+ qkv_proj_states = qkv_proj_states[:num_actual_tokens]
418
+ g1 = g1[:, :num_actual_tokens]
419
+ beta = beta[:, :num_actual_tokens]
420
+
421
+ (conv_state, recurrent_state) = constant_caches
422
+ # conv_state must be (..., dim, width-1) for the conv kernels.
423
+ # DS layout stores it that way directly; SD layout needs a transpose.
424
+ # Layout is process-global and resolved once at init (see __init__).
425
+ if not self._conv_state_dim_first:
426
+ conv_state = conv_state.transpose(-1, -2)
427
+
428
+ # One merged short-conv over q|k|v instead of three separate calls. The
429
+ # 1D conv is independent per channel, so concatenating q/k/v along the
430
+ # channel dim and running a single causal_conv1d is bit-identical to
431
+ # three calls. The merged weight is q|k|v conv weights concatenated;
432
+ # built once and cached (params are fixed after load). conv_state is
433
+ # already stored as the merged q|k|v state, so it is used directly.
434
+ if self._merged_conv_weight is None:
435
+
436
+ def _w(m):
437
+ return m.weight.view(m.weight.size(0), m.weight.size(2))
438
+
439
+ self._merged_conv_weight = torch.cat(
440
+ [_w(self.q_conv1d), _w(self.k_conv1d), _w(self.v_conv1d)],
441
+ dim=0,
442
+ ).contiguous()
443
+ conv_weights = self._merged_conv_weight
444
+ conv_bias = self.q_conv1d.bias
445
+
446
+ # Split projections / gating into spec (draft-verify) and non-spec token
447
+ # groups when speculative decoding is active. Spec tokens carry
448
+ # num_spec+1 recurrent-state columns each and are advanced with
449
+ # num_accepted_tokens for rejection-sampling rollback; non-spec tokens
450
+ # are one-per-request. Mirrors olmo_gdn_linear_attn.py. Projections are
451
+ # [n, *] (token dim 0); g1/beta are [1, n, h, d] (token dim 1).
452
+ if use_spec:
453
+ # In a pure spec-verify step (no non-spec tokens) the metadata
454
+ # builder sets spec_token_indx = arange(num_actual_tokens), making
455
+ # the index_select calls below identity copies. Skip them on this
456
+ # steady-state decode hot path. The outputs alias the inputs here;
457
+ # the downstream conv/recurrent kernels read them without mutating
458
+ # in place, so the aliasing is safe.
459
+ if non_spec_token_indx is None or non_spec_token_indx.numel() == 0:
460
+ qkv_spec = qkv_proj_states
461
+ g1_spec = g1
462
+ beta_spec = beta
463
+ else:
464
+ qkv_spec = qkv_proj_states.index_select(0, spec_token_indx)
465
+ g1_spec = g1.index_select(1, spec_token_indx)
466
+ beta_spec = beta.index_select(1, spec_token_indx)
467
+ if non_spec_token_indx is not None and non_spec_token_indx.numel() > 0:
468
+ qkv_ns = qkv_proj_states.index_select(0, non_spec_token_indx)
469
+ g1_ns = g1.index_select(1, non_spec_token_indx)
470
+ beta_ns = beta.index_select(1, non_spec_token_indx)
471
+ else:
472
+ qkv_ns = g1_ns = beta_ns = None
473
+ else:
474
+ qkv_spec = g1_spec = beta_spec = None
475
+ qkv_ns, g1_ns, beta_ns = qkv_proj_states, g1, beta
476
+
477
+ # --- causal conv1d: spec (draft-verify) path ---
478
+ if use_spec:
479
+ assert spec_state_indices_tensor is not None
480
+ assert num_accepted_tokens is not None
481
+ conv_idx = spec_state_indices_tensor[:, 0][:num_spec_decodes]
482
+ conv_mql = spec_state_indices_tensor.size(-1)
483
+ qkv_spec = causal_conv1d_update(
484
+ qkv_spec,
485
+ conv_state,
486
+ conv_weights,
487
+ conv_bias,
488
+ activation="silu",
489
+ conv_state_indices=conv_idx,
490
+ num_accepted_tokens=num_accepted_tokens,
491
+ query_start_loc=spec_query_start_loc,
492
+ max_query_len=conv_mql,
493
+ )
494
+ q_spec, k_spec, v_spec = qkv_spec.split(self.local_projection_size, dim=-1)
495
+
496
+ # --- causal conv1d: non-spec path (prefill or plain decode) ---
497
+ q_ns = k_ns = v_ns = None
498
+ if attn_metadata_narrowed.num_prefills > 0:
499
+ assert qkv_ns is not None
500
+ qkv_ns = causal_conv1d_fn(
501
+ qkv_ns.transpose(0, 1),
502
+ conv_weights,
503
+ conv_bias,
504
+ activation="silu",
505
+ conv_states=conv_state,
506
+ has_initial_state=has_initial_state,
507
+ cache_indices=non_spec_state_indices_tensor,
508
+ query_start_loc=non_spec_query_start_loc,
509
+ metadata=attn_metadata_narrowed,
510
+ ).transpose(0, 1)
511
+ q_ns, k_ns, v_ns = qkv_ns.split(self.local_projection_size, dim=-1)
512
+ elif attn_metadata_narrowed.num_decodes > 0:
513
+ assert non_spec_state_indices_tensor is not None
514
+ decode_conv_indices = non_spec_state_indices_tensor[
515
+ : attn_metadata_narrowed.num_decodes
516
+ ]
517
+ qkv_ns = causal_conv1d_update(
518
+ qkv_ns,
519
+ conv_state,
520
+ conv_weights,
521
+ conv_bias,
522
+ activation="silu",
523
+ conv_state_indices=decode_conv_indices,
524
+ )
525
+ q_ns, k_ns, v_ns = qkv_ns.split(self.local_projection_size, dim=-1)
526
+
527
+ def _rearr(x):
528
+ return x.reshape(1, -1, self.local_num_heads, self.head_dim)
529
+
530
+ # --- core attention: spec (draft-verify) path ---
531
+ core_attn_out_spec = None
532
+ # In a pure spec-verify step (no non-spec tokens) the recurrent kernel
533
+ # can write straight into the layer output buffer, skipping the
534
+ # fresh allocation + copy below. Mixed steps must scatter via
535
+ # spec_token_indx, so they keep the kernel-managed output.
536
+ spec_out = (
537
+ core_attn_out[0, :num_actual_tokens].unsqueeze(0)
538
+ if non_spec_token_indx is None or non_spec_token_indx.numel() == 0
539
+ else None
540
+ )
541
+ if use_spec:
542
+ assert spec_state_indices_tensor is not None
543
+ assert num_accepted_tokens is not None
544
+ assert spec_query_start_loc is not None
545
+ # Gate computed inside the recurrent kernel (COMPUTE_GATE) from
546
+ # raw g1 — replicates fused_kda_gate's arithmetic bit-for-bit and
547
+ # skips its launch + fp32 [n, H, D] intermediate per layer.
548
+ core_attn_out_spec, _ = fused_recurrent_kda(
549
+ q=_rearr(q_spec),
550
+ k=_rearr(k_spec),
551
+ v=_rearr(v_spec),
552
+ g=g1_spec,
553
+ beta=beta_spec,
554
+ initial_state=recurrent_state,
555
+ use_qk_l2norm_in_kernel=True,
556
+ cu_seqlens=spec_query_start_loc[: num_spec_decodes + 1],
557
+ ssm_state_indices=spec_state_indices_tensor,
558
+ num_accepted_tokens=num_accepted_tokens,
559
+ out=spec_out,
560
+ sigmoid_beta=True,
561
+ a_log=self.A_log,
562
+ g_bias=self.dt_bias,
563
+ compute_gate=True,
564
+ lower_bound=lower_bound,
565
+ )
566
+
567
+ # --- core attention: non-spec path (prefill or plain decode) ---
568
+ core_attn_out_non_spec = None
569
+ # Only the plain-decode recurrent kernel can write straight into the
570
+ # layer output buffer; the chunked prefill kernel cannot, so this
571
+ # stays None there and the merge copy below runs as before.
572
+ ns_out = None
573
+ if attn_metadata_narrowed.num_prefills > 0:
574
+ assert q_ns is not None
575
+ assert non_spec_state_indices_tensor is not None
576
+ assert has_initial_state is not None
577
+ initial_state = gather_initial_states(
578
+ recurrent_state, non_spec_state_indices_tensor, has_initial_state
579
+ )
580
+ (
581
+ core_attn_out_non_spec,
582
+ last_recurrent_state,
583
+ ) = chunk_kda_with_fused_gate(
584
+ q=_rearr(q_ns),
585
+ k=_rearr(k_ns),
586
+ v=_rearr(v_ns),
587
+ raw_g=g1_ns,
588
+ # Chunk path wants the pre-sigmoided fp32 beta (its kernels
589
+ # don't sigmoid); beta_ns is raw bf16 from forward.
590
+ beta=_cast_sigmoid(beta_ns.squeeze(0)).unsqueeze(0),
591
+ A_log=self.A_log,
592
+ g_bias=self.dt_bias,
593
+ initial_state=initial_state,
594
+ output_final_state=True,
595
+ use_qk_l2norm_in_kernel=True,
596
+ cu_seqlens=non_spec_query_start_loc,
597
+ safe_gate=safe_gate,
598
+ lower_bound=lower_bound,
599
+ )
600
+ # Init cache
601
+ scatter_states(
602
+ recurrent_state,
603
+ last_recurrent_state,
604
+ non_spec_state_indices_tensor,
605
+ )
606
+ elif attn_metadata_narrowed.num_decodes > 0:
607
+ assert non_spec_query_start_loc is not None
608
+ assert non_spec_state_indices_tensor is not None
609
+ # Plain decode step (no spec tokens): token order is dense, so the
610
+ # kernel can write straight into the layer output buffer. A mixed
611
+ # step scatters non-spec output via non_spec_token_indx instead.
612
+ # Gate computed in-kernel (COMPUTE_GATE), beta sigmoided in-kernel.
613
+ if not use_spec:
614
+ ns_out = spec_out
615
+ core_attn_out_non_spec, _ = fused_recurrent_kda(
616
+ q=_rearr(q_ns),
617
+ k=_rearr(k_ns),
618
+ v=_rearr(v_ns),
619
+ g=g1_ns,
620
+ beta=beta_ns,
621
+ initial_state=recurrent_state,
622
+ use_qk_l2norm_in_kernel=True,
623
+ cu_seqlens=non_spec_query_start_loc[
624
+ : attn_metadata_narrowed.num_decodes + 1
625
+ ],
626
+ ssm_state_indices=non_spec_state_indices_tensor,
627
+ out=ns_out,
628
+ sigmoid_beta=True,
629
+ a_log=self.A_log,
630
+ g_bias=self.dt_bias,
631
+ compute_gate=True,
632
+ lower_bound=lower_bound,
633
+ )
634
+
635
+ # --- merge spec / non-spec outputs back into token order ---
636
+ if use_spec and core_attn_out_non_spec is not None:
637
+ assert core_attn_out_spec is not None
638
+ merged = torch.empty(
639
+ (1, num_actual_tokens, *core_attn_out_spec.shape[2:]),
640
+ dtype=core_attn_out_non_spec.dtype,
641
+ device=core_attn_out_non_spec.device,
642
+ )
643
+ merged.index_copy_(1, spec_token_indx, core_attn_out_spec)
644
+ merged.index_copy_(1, non_spec_token_indx, core_attn_out_non_spec)
645
+ core_attn_out[0, :num_actual_tokens] = merged.squeeze(0)
646
+ elif use_spec:
647
+ assert core_attn_out_spec is not None
648
+ if spec_out is None:
649
+ core_attn_out[0, :num_actual_tokens] = core_attn_out_spec.squeeze(0)
650
+ else:
651
+ assert core_attn_out_non_spec is not None
652
+ if ns_out is None:
653
+ core_attn_out[0, :num_actual_tokens] = core_attn_out_non_spec[
654
+ 0, :num_actual_tokens
655
+ ]
serving/model.py ADDED
@@ -0,0 +1,1229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+ from collections.abc import Iterable
5
+ from typing import ClassVar, Literal
6
+
7
+ import torch
8
+ from torch import nn
9
+
10
+ from vllm.config import ParallelConfig, VllmConfig
11
+ from vllm.distributed import (
12
+ get_ep_group,
13
+ get_pp_group,
14
+ get_tensor_model_parallel_rank,
15
+ get_tensor_model_parallel_world_size,
16
+ tensor_model_parallel_all_gather,
17
+ )
18
+ from vllm.logger import init_logger
19
+ from vllm.model_executor.layers.activation import SiluAndMul, SiluAndMulWithClamp
20
+ from vllm.model_executor.layers.fused_moe import (
21
+ FusedMoEFactory,
22
+ GateLinear,
23
+ fused_moe_make_expert_params_mapping,
24
+ )
25
+ from vllm.model_executor.layers.layernorm import RMSNorm
26
+ from vllm.model_executor.layers.linear import (
27
+ MergedColumnParallelLinear,
28
+ RowParallelLinear,
29
+ )
30
+ from vllm.model_executor.layers.logits_processor import LogitsProcessor
31
+ from vllm.model_executor.layers.mamba.mamba_utils import (
32
+ MambaStateCopyFunc,
33
+ MambaStateCopyFuncCalculator,
34
+ MambaStateDtypeCalculator,
35
+ MambaStateShapeCalculator,
36
+ )
37
+ from vllm.model_executor.layers.mhc import (
38
+ MHCFusedPostPreOp,
39
+ MHCPostOp,
40
+ MHCPreOp,
41
+ hc_contract,
42
+ hc_expand,
43
+ )
44
+ from vllm.model_executor.layers.quantization import QuantizationConfig
45
+ from vllm.model_executor.layers.quantization.utils.quant_utils import (
46
+ GroupShape,
47
+ scaled_dequantize,
48
+ )
49
+ from vllm.model_executor.layers.vocab_parallel_embedding import (
50
+ ParallelLMHead,
51
+ VocabParallelEmbedding,
52
+ )
53
+ from vllm.model_executor.model_loader.weight_utils import (
54
+ default_weight_loader,
55
+ maybe_remap_kv_scale_name,
56
+ )
57
+ from vllm.model_executor.models.deepseek_v2 import _get_moe_router_dtype
58
+ from vllm.model_executor.models.glm4_1v import (
59
+ Glm4vDummyInputsBuilder,
60
+ Glm4vForConditionalGeneration,
61
+ )
62
+ from vllm.model_executor.models.interfaces import (
63
+ HasInnerState,
64
+ IsHybrid,
65
+ MixtureOfExperts,
66
+ SupportsPP,
67
+ )
68
+ from vllm.model_executor.models.utils import (
69
+ AutoWeightsLoader,
70
+ PPMissingLayer,
71
+ init_vllm_registered_model,
72
+ is_pp_missing_parameter,
73
+ make_layers,
74
+ maybe_prefix,
75
+ sequence_parallel_chunk,
76
+ )
77
+ from vllm.models.common.ops.sequence_parallel import (
78
+ sp_all_gather,
79
+ sp_reduce_scatter,
80
+ sp_shard,
81
+ )
82
+ from vllm.multimodal import MULTIMODAL_REGISTRY
83
+ from vllm.platforms import current_platform
84
+ from vllm.sequence import IntermediateTensors
85
+ from vllm.transformers_utils.configs.glm5_next import Glm5NextConfig
86
+
87
+ from .attention import Glm5NextMLAAttention
88
+ from .kda import Glm5NextLinearAttention
89
+ from .multimodal import (
90
+ Glm5NextMultiModalProcessor,
91
+ Glm5NextProcessingInfo,
92
+ Glm5NextVisionTransformer,
93
+ )
94
+
95
+ logger = init_logger(__name__)
96
+
97
+
98
+ class Glm5NextMLP(nn.Module):
99
+ def __init__(
100
+ self,
101
+ hidden_size: int,
102
+ intermediate_size: int,
103
+ hidden_act: str,
104
+ quant_config: QuantizationConfig | None = None,
105
+ reduce_results: bool = True,
106
+ is_sequence_parallel=False,
107
+ prefix: str = "",
108
+ swiglu_limit: float | None = None,
109
+ ) -> None:
110
+ super().__init__()
111
+
112
+ # If is_sequence_parallel, the input and output tensors are sharded
113
+ # across the ranks within the tp_group. In this case the weights are
114
+ # replicated and no collective ops are needed.
115
+ # Otherwise we use standard TP with an allreduce at the end.
116
+ self.gate_up_proj = MergedColumnParallelLinear(
117
+ hidden_size,
118
+ [intermediate_size] * 2,
119
+ bias=False,
120
+ quant_config=quant_config,
121
+ disable_tp=is_sequence_parallel,
122
+ prefix=f"{prefix}.gate_up_proj",
123
+ )
124
+ self.down_proj = RowParallelLinear(
125
+ intermediate_size,
126
+ hidden_size,
127
+ bias=False,
128
+ quant_config=quant_config,
129
+ reduce_results=reduce_results,
130
+ disable_tp=is_sequence_parallel,
131
+ prefix=f"{prefix}.down_proj",
132
+ )
133
+ if hidden_act != "silu":
134
+ raise ValueError(
135
+ f"Unsupported activation: {hidden_act}. Only silu is supported for now."
136
+ )
137
+
138
+ self.swiglu_limit = swiglu_limit
139
+ if self.swiglu_limit is not None:
140
+ self.act_fn = SiluAndMulWithClamp(swiglu_limit=self.swiglu_limit)
141
+ else:
142
+ self.act_fn = SiluAndMul()
143
+
144
+ def forward(self, x):
145
+ gate_up, _ = self.gate_up_proj(x)
146
+ x = self.act_fn(gate_up)
147
+ x, _ = self.down_proj(x)
148
+ return x
149
+
150
+
151
+ class Glm5NextMoE(nn.Module):
152
+ def __init__(
153
+ self,
154
+ config: Glm5NextConfig,
155
+ parallel_config: ParallelConfig,
156
+ quant_config: QuantizationConfig | None = None,
157
+ prefix: str = "",
158
+ apply_routed_scale_to_output: bool = False,
159
+ ):
160
+ super().__init__()
161
+ self.tp_size = get_tensor_model_parallel_world_size()
162
+ self.tp_rank = get_tensor_model_parallel_rank()
163
+
164
+ self.routed_scaling_factor = getattr(config, "routed_scaling_factor", 1.0)
165
+
166
+ self.ep_group = get_ep_group().device_group
167
+ self.ep_rank = get_ep_group().rank_in_group
168
+ self.ep_size = self.ep_group.size()
169
+ self.n_routed_experts: int = config.n_routed_experts
170
+ self.n_shared_experts: int = config.n_shared_experts
171
+
172
+ self.is_sequence_parallel = parallel_config.use_sequence_parallel_moe
173
+
174
+ if config.hidden_act != "silu":
175
+ raise ValueError(
176
+ f"Unsupported activation: {config.hidden_act}. "
177
+ "Only silu is supported for now."
178
+ )
179
+
180
+ self.router_dtype = _get_moe_router_dtype(config)
181
+ self.gate = GateLinear(
182
+ config.hidden_size,
183
+ config.n_routed_experts,
184
+ out_dtype=self.router_dtype,
185
+ prefix=f"{prefix}.gate",
186
+ )
187
+ if getattr(config, "topk_method", None) == "noaux_tc":
188
+ self.gate.e_score_correction_bias = nn.Parameter(
189
+ torch.empty(config.n_routed_experts, dtype=torch.float32)
190
+ )
191
+ else:
192
+ self.gate.e_score_correction_bias = None
193
+
194
+ # Load balancing settings.
195
+ eplb_config = parallel_config.eplb_config
196
+ self.enable_eplb = parallel_config.enable_eplb
197
+
198
+ self.n_redundant_experts = eplb_config.num_redundant_experts
199
+ self.n_logical_experts = self.n_routed_experts
200
+ self.n_physical_experts = self.n_logical_experts + self.n_redundant_experts
201
+ self.n_local_physical_experts = self.n_physical_experts // self.ep_size
202
+
203
+ self.physical_expert_start = self.ep_rank * self.n_local_physical_experts
204
+ self.physical_expert_end = (
205
+ self.physical_expert_start + self.n_local_physical_experts
206
+ )
207
+
208
+ swiglu_limit = getattr(config, "swiglu_limit", None)
209
+ if config.n_shared_experts is None:
210
+ self.shared_experts = None
211
+ else:
212
+ intermediate_size = config.moe_intermediate_size * config.n_shared_experts
213
+
214
+ self.shared_experts = Glm5NextMLP(
215
+ hidden_size=config.hidden_size,
216
+ intermediate_size=intermediate_size,
217
+ hidden_act=config.hidden_act,
218
+ quant_config=quant_config,
219
+ is_sequence_parallel=self.is_sequence_parallel,
220
+ reduce_results=False,
221
+ prefix=f"{prefix}.shared_experts",
222
+ swiglu_limit=swiglu_limit,
223
+ )
224
+
225
+ self.experts = FusedMoEFactory(
226
+ shared_experts=self.shared_experts,
227
+ gate=self.gate,
228
+ num_experts=config.n_routed_experts,
229
+ top_k=config.num_experts_per_token,
230
+ hidden_size=config.hidden_size,
231
+ intermediate_size=config.moe_intermediate_size,
232
+ renormalize=getattr(config, "norm_topk_prob", True),
233
+ quant_config=quant_config,
234
+ use_grouped_topk=True,
235
+ num_expert_group=getattr(config, "n_group", 1),
236
+ topk_group=getattr(config, "topk_group", 1),
237
+ prefix=f"{prefix}.experts",
238
+ scoring_func=getattr(config, "scoring_func", "softmax"),
239
+ routed_scaling_factor=self.routed_scaling_factor,
240
+ apply_routed_scale_to_output=apply_routed_scale_to_output,
241
+ e_score_correction_bias=self.gate.e_score_correction_bias,
242
+ enable_eplb=self.enable_eplb,
243
+ num_redundant_experts=self.n_redundant_experts,
244
+ is_sequence_parallel=self.is_sequence_parallel,
245
+ n_shared_experts=None,
246
+ router_logits_dtype=self.gate.out_dtype,
247
+ swiglu_limit=swiglu_limit,
248
+ )
249
+
250
+ def forward(
251
+ self,
252
+ hidden_states: torch.Tensor,
253
+ already_sequence_parallel: bool = False,
254
+ ) -> torch.Tensor:
255
+ num_tokens, hidden_dim = hidden_states.shape
256
+
257
+ # Chunk the hidden states so they aren't replicated across TP ranks.
258
+ # This avoids duplicate computation in self.experts.
259
+ if self.is_sequence_parallel and not already_sequence_parallel:
260
+ hidden_states = sequence_parallel_chunk(hidden_states)
261
+
262
+ # The router is always external (self.gate); main's MoERunner expects
263
+ # pre-computed router_logits, so compute them here unconditionally.
264
+ router_logits, _ = self.gate(hidden_states)
265
+ final_hidden_states = self.experts(
266
+ hidden_states=hidden_states, router_logits=router_logits
267
+ )
268
+
269
+ if self.is_sequence_parallel and not already_sequence_parallel:
270
+ final_hidden_states = tensor_model_parallel_all_gather(
271
+ final_hidden_states, 0
272
+ )
273
+ final_hidden_states = final_hidden_states[:num_tokens]
274
+
275
+ return final_hidden_states.view(num_tokens, hidden_dim)
276
+
277
+
278
+ class Glm5NextDecoderLayer(nn.Module):
279
+ def __init__(
280
+ self,
281
+ vllm_config: VllmConfig,
282
+ config: Glm5NextConfig,
283
+ layer_idx: int,
284
+ prefix: str = "",
285
+ topk_indices_buffer: torch.Tensor | None = None,
286
+ is_mtp_layer: bool = False,
287
+ **kwargs,
288
+ ) -> None:
289
+ super().__init__()
290
+
291
+ cache_config = vllm_config.cache_config
292
+ quant_config = vllm_config.quant_config
293
+ parallel_config = vllm_config.parallel_config
294
+
295
+ self.hidden_size = config.hidden_size
296
+ self.layer_idx = layer_idx
297
+ self.is_moe = config.is_moe
298
+ self.num_hidden_layers = config.num_hidden_layers
299
+ self.rms_norm_eps = config.rms_norm_eps
300
+ self.num_experts = config.n_routed_experts
301
+ self.is_mtp_layer = is_mtp_layer
302
+ self.mhc = config.mhc
303
+ self.layer_kind = "kda" if config.is_kda_layer(layer_idx) else "mla"
304
+ self.is_sequence_parallel = parallel_config.use_sequence_parallel_moe
305
+
306
+ if config.is_kda_layer(layer_idx):
307
+ self.self_attn = Glm5NextLinearAttention(
308
+ config=config,
309
+ vllm_config=vllm_config,
310
+ prefix=f"{prefix}.self_attn",
311
+ )
312
+ else:
313
+ # MLA layers require the latent head dims, which are guaranteed set
314
+ # on MLA configs; narrow away the `int | None`.
315
+ assert config.v_head_dim is not None
316
+ assert config.kv_lora_rank is not None
317
+ self.self_attn = Glm5NextMLAAttention(
318
+ vllm_config=vllm_config,
319
+ config=config,
320
+ hidden_size=self.hidden_size,
321
+ num_heads=config.num_attention_heads,
322
+ qk_nope_head_dim=config.qk_nope_head_dim,
323
+ qk_rope_head_dim=config.qk_rope_head_dim,
324
+ v_head_dim=config.v_head_dim,
325
+ q_lora_rank=config.q_lora_rank,
326
+ kv_lora_rank=config.kv_lora_rank,
327
+ max_position_embeddings=config.max_position_embeddings,
328
+ cache_config=cache_config,
329
+ # LOCAL PATCH (fp8attn-r2): was quant_config=None ("MLA
330
+ # projections are BF16 in checkpoint"). Pass the real config so
331
+ # FP8-serialized MLA projections can stay FP8-resident; BF16
332
+ # checkpoints still resolve every self_attn module to
333
+ # UnquantizedLinearMethod via the config's ignore list.
334
+ quant_config=quant_config,
335
+ prefix=f"{prefix}.self_attn",
336
+ topk_indices_buffer=topk_indices_buffer,
337
+ skip_rope=getattr(config, "mla_nope", False),
338
+ )
339
+
340
+ # MTP layers sit past the base model's hidden layers (layer_idx >=
341
+ # num_hidden_layers), so they're outside mlp_layer_types; default them
342
+ # to the last base layer's MLP type (sparse/MoE for these checkpoints).
343
+ mlp_layer_types = config.mlp_layer_types
344
+ mlp_type = (
345
+ mlp_layer_types[layer_idx]
346
+ if layer_idx < len(mlp_layer_types)
347
+ else (mlp_layer_types[-1] if mlp_layer_types else "sparse")
348
+ )
349
+ if self.is_moe and self.num_experts is not None and mlp_type == "sparse":
350
+ self.mlp = Glm5NextMoE(
351
+ config=config,
352
+ parallel_config=parallel_config,
353
+ quant_config=quant_config,
354
+ prefix=f"{prefix}.mlp",
355
+ )
356
+ else:
357
+ self.mlp = Glm5NextMLP(
358
+ hidden_size=self.hidden_size,
359
+ intermediate_size=config.intermediate_size,
360
+ hidden_act=config.hidden_act,
361
+ quant_config=quant_config,
362
+ prefix=f"{prefix}.mlp",
363
+ swiglu_limit=config.swiglu_limit,
364
+ )
365
+ self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
366
+ # Cached for the hot forward path (isinstance per layer per step).
367
+ self._mlp_is_moe = isinstance(self.mlp, Glm5NextMoE)
368
+ # In SP, the attention output projection leaves a partial sum; the
369
+ # decoder-layer reduce_scatter after attention completes it (DSv4 pattern).
370
+ # MTP layers use the non-mHC path which has no sp_reduce_scatter, so
371
+ # their o_proj must still reduce normally.
372
+ if self.is_sequence_parallel and not is_mtp_layer:
373
+ self.self_attn.o_proj.reduce_results = False
374
+ self.post_attention_layernorm = RMSNorm(
375
+ config.hidden_size, eps=config.rms_norm_eps
376
+ )
377
+
378
+ if self.mhc and not is_mtp_layer:
379
+ # mhc config
380
+ self.mhc_num_residual_streams = config.mhc_num_residual_streams
381
+ self.mhc_no_norm_weight = config.mhc_no_norm_weight
382
+ self.mhc_tau = config.mhc_tau
383
+ self.hc_eps = config.hc_eps
384
+ self.mhc_sinkhorn_iterations = config.mhc_sinkhorn_iterations
385
+ self.mhc_post_mult_value = config.mhc_post_mult_value
386
+
387
+ n = config.mhc_num_residual_streams
388
+ d_model = n * self.hidden_size
389
+ mix_hc = (2 + n) * n
390
+
391
+ self.n = n
392
+
393
+ # attn hc
394
+ self.hc_attn_fn = nn.Parameter(
395
+ torch.empty(mix_hc, d_model, dtype=torch.float32)
396
+ )
397
+ self.hc_attn_base = nn.Parameter(torch.empty(mix_hc, dtype=torch.float32))
398
+ self.hc_attn_scale = nn.Parameter(torch.empty(3, dtype=torch.float32))
399
+
400
+ # ffn hc
401
+ self.hc_ffn_fn = nn.Parameter(
402
+ torch.empty(mix_hc, d_model, dtype=torch.float32)
403
+ )
404
+ self.hc_ffn_base = nn.Parameter(torch.empty(mix_hc, dtype=torch.float32))
405
+ self.hc_ffn_scale = nn.Parameter(torch.empty(3, dtype=torch.float32))
406
+
407
+ self.mhc_pre_op = MHCPreOp()
408
+ self.mhc_post_op = MHCPostOp()
409
+ self.mhc_fused_post_pre_op = MHCFusedPostPreOp()
410
+
411
+ def forward(
412
+ self,
413
+ positions: torch.Tensor,
414
+ hidden_states: torch.Tensor,
415
+ residual: torch.Tensor | None = None,
416
+ post: torch.Tensor | None = None,
417
+ comb: torch.Tensor | None = None,
418
+ ) -> tuple[
419
+ torch.Tensor,
420
+ torch.Tensor | None,
421
+ torch.Tensor | None,
422
+ torch.Tensor | None,
423
+ ]:
424
+ # 70B or MTP layers: KDA + MoE without HC.
425
+ if not self.mhc or self.is_mtp_layer:
426
+ residual = hidden_states
427
+ hidden_states = self.input_layernorm(hidden_states)
428
+
429
+ attn_output = self.self_attn(
430
+ hidden_states=hidden_states,
431
+ positions=positions,
432
+ )
433
+ hidden_states, residual = self.post_attention_layernorm(
434
+ attn_output, residual=residual
435
+ )
436
+ hidden_states = self.mlp(hidden_states)
437
+ if self.is_mtp_layer:
438
+ # Return the unsummed pair: the MTP caller feeds it straight
439
+ # into shared_head's fused_add_rms_norm (one kernel instead of
440
+ # a separate residual-add + norm). The sum itself is unchanged
441
+ # (fp32-accumulated inside the fused kernel).
442
+ return hidden_states, residual, None, None
443
+ hidden_states = residual + hidden_states
444
+ return hidden_states, residual, None, None
445
+
446
+ # mHC start. `post`/`comb` carry the previous layer's deferred
447
+ # hc_post inputs (its ffn-pre outputs); when present, fuse that
448
+ # hc_post with this layer's attn hc_pre into one kernel (inter-layer
449
+ # fusion). Layer 0 has no incoming state -> standalone hc_pre.
450
+ x = hidden_states
451
+ if post is None:
452
+ if self.layer_idx == 0:
453
+ x = hc_expand(x, self.n)
454
+ residual = x
455
+ post, comb, x = self.hc_pre(
456
+ x,
457
+ self.hc_attn_fn,
458
+ self.hc_attn_scale,
459
+ self.hc_attn_base,
460
+ norm_weight=self.input_layernorm.weight.data,
461
+ norm_eps=self.input_layernorm.variance_epsilon,
462
+ )
463
+ else:
464
+ residual, post, comb, x = self.hc_fused_post_pre(
465
+ x,
466
+ residual,
467
+ post,
468
+ comb,
469
+ self.hc_attn_fn,
470
+ self.hc_attn_scale,
471
+ self.hc_attn_base,
472
+ norm_weight=self.input_layernorm.weight.data,
473
+ norm_eps=self.input_layernorm.variance_epsilon,
474
+ )
475
+
476
+ # Attention needs the full token sequence; mHC above ran on the SP
477
+ # shard. Gather for attention, scatter back afterward (DSv4 pattern).
478
+ if self.is_sequence_parallel:
479
+ x = sp_all_gather(x)[: positions.shape[0]]
480
+
481
+ x = self.self_attn(
482
+ hidden_states=x,
483
+ positions=positions,
484
+ )
485
+
486
+ if self.is_sequence_parallel:
487
+ x = sp_reduce_scatter(x)
488
+
489
+ # Fuse post-attn hc_post + pre-FFN hc_pre (+ RMSNorm) into one kernel.
490
+ residual, post, comb, x = self.hc_fused_post_pre(
491
+ x,
492
+ residual,
493
+ post,
494
+ comb,
495
+ self.hc_ffn_fn,
496
+ self.hc_ffn_scale,
497
+ self.hc_ffn_base,
498
+ norm_weight=self.post_attention_layernorm.weight.data,
499
+ norm_eps=self.post_attention_layernorm.variance_epsilon,
500
+ )
501
+
502
+ # Fully Connected
503
+ if self._mlp_is_moe:
504
+ x = self.mlp(x, already_sequence_parallel=self.is_sequence_parallel)
505
+ else:
506
+ x = self.mlp(x)
507
+
508
+ # mHC end. The last mHC layer materializes its final hc_post (nothing
509
+ # to fuse with) then contracts; every other layer defers its hc_post to
510
+ # the next layer's fused pre, returning the state.
511
+ if self.layer_idx == self.num_hidden_layers - 1:
512
+ x = self.hc_post(x, residual, post, comb)
513
+ x = hc_contract(x, self.n)
514
+ return x, None, None, None
515
+
516
+ return x, residual, post, comb
517
+
518
+ def hc_pre(
519
+ self,
520
+ x: torch.Tensor,
521
+ hc_fn: torch.Tensor,
522
+ hc_scale: torch.Tensor,
523
+ hc_base: torch.Tensor,
524
+ norm_weight: torch.Tensor | None = None,
525
+ norm_eps: float = 0.0,
526
+ ):
527
+ post_mix, res_mix, layer_input = self.mhc_pre_op(
528
+ residual=x,
529
+ fn=hc_fn,
530
+ hc_scale=hc_scale,
531
+ hc_base=hc_base,
532
+ rms_eps=self.rms_norm_eps,
533
+ hc_pre_eps=self.hc_eps,
534
+ hc_sinkhorn_eps=self.hc_eps,
535
+ hc_post_mult_value=self.mhc_post_mult_value,
536
+ sinkhorn_repeat=self.mhc_sinkhorn_iterations,
537
+ norm_weight=norm_weight,
538
+ norm_eps=norm_eps,
539
+ )
540
+ return post_mix, res_mix, layer_input
541
+
542
+ def hc_post(
543
+ self,
544
+ x: torch.Tensor,
545
+ residual: torch.Tensor,
546
+ post: torch.Tensor,
547
+ comb: torch.Tensor,
548
+ ):
549
+ return self.mhc_post_op(x, residual, post, comb)
550
+
551
+ def hc_fused_post_pre(
552
+ self,
553
+ x: torch.Tensor,
554
+ residual: torch.Tensor,
555
+ post: torch.Tensor,
556
+ comb: torch.Tensor,
557
+ hc_fn: torch.Tensor,
558
+ hc_scale: torch.Tensor,
559
+ hc_base: torch.Tensor,
560
+ norm_weight: torch.Tensor | None = None,
561
+ norm_eps: float = 0.0,
562
+ ):
563
+ return self.mhc_fused_post_pre_op(
564
+ x=x,
565
+ residual=residual,
566
+ post_layer_mix=post,
567
+ comb_res_mix=comb,
568
+ fn=hc_fn,
569
+ hc_scale=hc_scale,
570
+ hc_base=hc_base,
571
+ rms_eps=self.rms_norm_eps,
572
+ hc_pre_eps=self.hc_eps,
573
+ hc_sinkhorn_eps=self.hc_eps,
574
+ hc_post_mult_value=self.mhc_post_mult_value,
575
+ sinkhorn_repeat=self.mhc_sinkhorn_iterations,
576
+ n_splits=1,
577
+ tile_n=1,
578
+ norm_weight=norm_weight,
579
+ norm_eps=norm_eps,
580
+ )
581
+
582
+
583
+ class Glm5NextModel(nn.Module):
584
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
585
+ super().__init__()
586
+
587
+ config = vllm_config.model_config.hf_config
588
+ self.config = config
589
+
590
+ self.vocab_size = config.vocab_size
591
+ self.device = current_platform.device_type
592
+
593
+ """
594
+ if config.index_topk is not None:
595
+ topk_indices_buffer = torch.empty(
596
+ vllm_config.scheduler_config.max_num_batched_tokens,
597
+ config.index_topk,
598
+ dtype=torch.int32,
599
+ device=self.device,
600
+ )
601
+ else:
602
+ """
603
+ # `index_topk` is declared on Glm5NextTextConfig with a default of None,
604
+ # so hasattr() is True even for full-MLA configs (no kpool indexer).
605
+ # Gate on the value being set instead.
606
+ self.is_v32 = getattr(config, "index_topk", None) is not None
607
+ if self.is_v32:
608
+ topk_tokens = config.index_topk
609
+ # kpool widens the topk buffer: selecting topk_tokens//kpool pools and
610
+ # expanding them yields topk_tokens token indices, plus an always-
611
+ # selected tail of up to kpool-1 incomplete-pool tokens. The attention
612
+ # backend reads the width dynamically via topk_indices.shape[1].
613
+ kpool = getattr(config, "index_kpool", 1) or 1
614
+ buffer_width = topk_tokens
615
+ # The sparse MLA attention kernel
616
+ # (triton_convert_req_index_to_global_index) tiles the topk
617
+ # dimension in BLOCK_N=128 columns and requires the buffer width
618
+ # to be a multiple of it; otherwise it raises
619
+ # "NUM_TOPK_TOKENS must be divisible by BLOCK_N". Round up: the
620
+ # extra slots stay -1 (the indexer op initializes the buffer to
621
+ # -1) and are masked out by the attention kernel, so they do not
622
+ # affect the softmax over the selected tokens.
623
+ sparse_topk_block_n = 128
624
+ buffer_width = (
625
+ (buffer_width + sparse_topk_block_n - 1) // sparse_topk_block_n
626
+ ) * sparse_topk_block_n
627
+ topk_indices_buffer = torch.empty(
628
+ vllm_config.scheduler_config.max_num_batched_tokens,
629
+ buffer_width,
630
+ dtype=torch.int32,
631
+ device=self.device,
632
+ )
633
+ else:
634
+ # Full-MLA config (no kpool sparse indexer): no topk buffer.
635
+ topk_indices_buffer = None
636
+
637
+ if get_pp_group().is_first_rank:
638
+ self.embed_tokens = VocabParallelEmbedding(
639
+ config.vocab_size,
640
+ config.hidden_size,
641
+ prefix=f"{prefix}.embed_tokens",
642
+ )
643
+ else:
644
+ self.embed_tokens = PPMissingLayer()
645
+
646
+ def get_layer(prefix: str):
647
+ layer_idx = int(prefix.rsplit(".", 1)[1])
648
+ return Glm5NextDecoderLayer(
649
+ vllm_config=vllm_config,
650
+ config=config,
651
+ layer_idx=layer_idx,
652
+ prefix=prefix,
653
+ topk_indices_buffer=topk_indices_buffer,
654
+ )
655
+
656
+ self.start_layer, self.end_layer, self.layers = make_layers(
657
+ config.num_hidden_layers,
658
+ get_layer,
659
+ prefix=f"{prefix}.layers",
660
+ )
661
+ # The active slice is fixed after construction; cache it so forward
662
+ # doesn't rebuild the slice (a fresh list) every step.
663
+ self._active_layers = self.layers[self.start_layer : self.end_layer]
664
+
665
+ if get_pp_group().is_last_rank:
666
+ self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
667
+ else:
668
+ self.norm = PPMissingLayer()
669
+
670
+ self.is_sequence_parallel = (
671
+ vllm_config.parallel_config.use_sequence_parallel_moe
672
+ )
673
+
674
+ world_size = get_tensor_model_parallel_world_size()
675
+ assert config.num_attention_heads % world_size == 0, (
676
+ "num_attention_heads must be divisible by world_size"
677
+ )
678
+
679
+ def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
680
+ return self.embed_tokens(input_ids)
681
+
682
+ def forward(
683
+ self,
684
+ input_ids: torch.Tensor | None,
685
+ positions: torch.Tensor,
686
+ intermediate_tensors: IntermediateTensors | None,
687
+ inputs_embeds: torch.Tensor | None = None,
688
+ **kwargs,
689
+ ) -> torch.Tensor:
690
+ if get_pp_group().is_first_rank:
691
+ if inputs_embeds is not None:
692
+ hidden_states = inputs_embeds
693
+ else:
694
+ hidden_states = self.embed_input_ids(input_ids)
695
+ residual = None
696
+ post = None
697
+ comb = None
698
+ else:
699
+ assert intermediate_tensors is not None
700
+ hidden_states = intermediate_tensors["hidden_states"]
701
+ residual = intermediate_tensors["residual"]
702
+ # post/comb (deferred mHC hc_post state) are not propagated across
703
+ # PP ranks; the receiving rank's first mHC layer uses standalone pre.
704
+ post = None
705
+ comb = None
706
+
707
+ full_num_tokens = positions.shape[0]
708
+ if self.is_sequence_parallel:
709
+ hidden_states = sp_shard(hidden_states)
710
+
711
+ for layer in self._active_layers:
712
+ hidden_states, residual, post, comb = layer(
713
+ positions, hidden_states, residual, post, comb
714
+ )
715
+
716
+ if not get_pp_group().is_last_rank:
717
+ # PP is gated off for GLM5Next (no make_empty_intermediate_tensors),
718
+ # so this branch is not exercised. post/comb are the deferred
719
+ # hc_post state of this rank's last mHC layer; a future PP path
720
+ # would need to propagate them, but for now they are dropped (the
721
+ # receiving rank's first layer would fall back to standalone pre).
722
+ return IntermediateTensors(
723
+ {"hidden_states": hidden_states, "residual": residual}
724
+ )
725
+
726
+ if self.is_sequence_parallel:
727
+ hidden_states = sp_all_gather(hidden_states)[:full_num_tokens]
728
+
729
+ hidden_states = self.norm(hidden_states)
730
+ return hidden_states
731
+
732
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
733
+ stacked_params_mapping = [
734
+ # (param_name, shard_name, shard_id)
735
+ (".gate_up_proj", ".gate_proj", 0),
736
+ (".gate_up_proj", ".up_proj", 1),
737
+ # MLA: fuse q_a_proj and kv_a_proj_with_mqa
738
+ (".fused_qkv_a_proj", ".q_a_proj", 0),
739
+ (".fused_qkv_a_proj", ".kv_a_proj_with_mqa", 1),
740
+ # Indexer: fuse wk and weights_proj
741
+ (".wk_weights_proj", ".wk", 0),
742
+ (".wk_weights_proj", ".weights_proj", 1),
743
+ # KDA: merge q, k, v, b, f_a, g_a projections into one GEMM
744
+ (".in_proj_qkvbfg_a", ".q_proj", 0),
745
+ (".in_proj_qkvbfg_a", ".k_proj", 1),
746
+ (".in_proj_qkvbfg_a", ".v_proj", 2),
747
+ (".in_proj_qkvbfg_a", ".b_proj", 3),
748
+ (".in_proj_qkvbfg_a", ".f_a_proj", 4),
749
+ (".in_proj_qkvbfg_a", ".g_a_proj", 5),
750
+ ]
751
+ if self.config.is_moe:
752
+ # Params for weights, fp8 weight scales, fp8 activation scales
753
+ # (param_name, weight_name, expert_id, shard_id)
754
+ expert_params_mapping = fused_moe_make_expert_params_mapping(
755
+ self,
756
+ ckpt_gate_proj_name="gate_proj",
757
+ ckpt_down_proj_name="down_proj",
758
+ ckpt_up_proj_name="up_proj",
759
+ num_experts=self.config.n_routed_experts,
760
+ )
761
+ else:
762
+ expert_params_mapping = []
763
+ params_dict = dict(self.named_parameters())
764
+ loaded_params: set[str] = set()
765
+
766
+ # GLM5-Next NoPE: checkpoint's kv_a_proj_with_mqa has only kv_lora_rank
767
+ # rows, but the model expects kv_lora_rank + qk_rope_head_dim rows.
768
+ # Pad the missing rope portion with zeros.
769
+ kv_a_pad_size = 0
770
+ if self.config.mla_nope and self.config.qk_rope_head_dim > 0:
771
+ kv_a_pad_size = self.config.qk_rope_head_dim
772
+
773
+ _pending_wk_fp8: dict = {}
774
+
775
+ for args in weights:
776
+ name, loaded_weight = args[:2]
777
+ kwargs: dict = args[2] if len(args) > 2 else {}
778
+ if "rotary_emb.inv_freq" in name:
779
+ continue
780
+
781
+ spec_layer = get_spec_layer_idx_from_weight_name(self.config, name)
782
+ if spec_layer is not None:
783
+ continue # skip spec decode layers for main model
784
+ if "rotary_emb.cos_cached" in name or "rotary_emb.sin_cached" in name:
785
+ # Models trained using ColossalAI may include these tensors in
786
+ # the checkpoint. Skip them.
787
+ continue
788
+
789
+ # Handle FP8 indexer WK: dequantize to BF16 for fusion with
790
+ # weights_proj into wk_weights_proj.
791
+ if _try_load_fp8_indexer_wk(
792
+ name,
793
+ loaded_weight,
794
+ _pending_wk_fp8,
795
+ params_dict,
796
+ loaded_params,
797
+ ):
798
+ continue
799
+
800
+ # FP8 checkpoint: dequantize BF16-kept MLA projections
801
+ # (q_a_proj / kv_a_proj_with_mqa / o_proj) to BF16.
802
+ if _try_load_fp8_attn_proj(
803
+ name,
804
+ loaded_weight,
805
+ _pending_wk_fp8,
806
+ params_dict,
807
+ loaded_params,
808
+ kv_a_pad_size,
809
+ ):
810
+ continue
811
+
812
+ # Pad kv_a_proj_with_mqa for NoPE models
813
+ if kv_a_pad_size > 0 and ".kv_a_proj_with_mqa." in name:
814
+ pad = torch.zeros(
815
+ kv_a_pad_size,
816
+ *loaded_weight.shape[1:],
817
+ dtype=loaded_weight.dtype,
818
+ device=loaded_weight.device,
819
+ )
820
+ loaded_weight = torch.cat([loaded_weight, pad], dim=0)
821
+
822
+ for param_name, weight_name, shard_id in stacked_params_mapping:
823
+ if weight_name not in name:
824
+ continue
825
+ # We have mlp.experts[0].gate_proj in the checkpoint.
826
+ # Since we handle the experts below in expert_params_mapping,
827
+ # we need to skip here BEFORE we update the name, otherwise
828
+ # name will be updated to mlp.experts[0].gate_up_proj, which
829
+ # will then be updated below in expert_params_mapping
830
+ # for mlp.experts[0].gate_gate_up_proj, which breaks load.
831
+ if ("mlp.experts." in name) and name not in params_dict:
832
+ continue
833
+ name_mapped = name.replace(weight_name, param_name)
834
+ # QKV fusion: skip if fused module doesn't exist in model
835
+ if param_name == ".fused_qkv_a_proj" and name_mapped not in params_dict:
836
+ continue
837
+ name = name_mapped
838
+ # Skip loading extra bias for GPTQ models.
839
+ if name.endswith(".bias") and name not in params_dict:
840
+ continue
841
+ if is_pp_missing_parameter(name, self):
842
+ continue
843
+ param = params_dict[name]
844
+ weight_loader = param.weight_loader
845
+ weight_loader(param, loaded_weight, shard_id)
846
+ break
847
+ else:
848
+ for idx, (
849
+ param_name,
850
+ weight_name,
851
+ expert_id,
852
+ expert_shard_id,
853
+ ) in enumerate(expert_params_mapping):
854
+ if weight_name not in name:
855
+ continue
856
+ name = name.replace(weight_name, param_name)
857
+ if is_pp_missing_parameter(name, self):
858
+ continue
859
+ param = params_dict[name]
860
+ weight_loader = param.weight_loader
861
+ weight_loader(
862
+ param,
863
+ loaded_weight,
864
+ name,
865
+ expert_id=expert_id,
866
+ shard_id=expert_shard_id,
867
+ )
868
+ break
869
+ else:
870
+ # Skip loading extra bias for GPTQ models.
871
+ if (
872
+ name.endswith(".bias")
873
+ and name not in params_dict
874
+ and not self.config.is_linear_attn
875
+ ): # noqa: E501
876
+ continue
877
+ # Remapping the name of FP8 kv-scale.
878
+ remapped_name = maybe_remap_kv_scale_name(name, params_dict)
879
+ if remapped_name is None:
880
+ continue
881
+ name = remapped_name
882
+ if is_pp_missing_parameter(name, self):
883
+ continue
884
+
885
+ param = params_dict[name]
886
+ weight_loader = getattr(
887
+ param, "weight_loader", default_weight_loader
888
+ )
889
+ weight_loader(param, loaded_weight, **kwargs)
890
+ loaded_params.add(name)
891
+ return loaded_params
892
+
893
+
894
+ class Glm5NextForCausalLM(
895
+ nn.Module, HasInnerState, SupportsPP, MixtureOfExperts, IsHybrid
896
+ ):
897
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
898
+ super().__init__()
899
+ self.model_config = vllm_config.model_config
900
+ self.vllm_config = vllm_config
901
+ self.config = self.model_config.hf_config
902
+ quant_config = vllm_config.quant_config
903
+ self.quant_config = quant_config
904
+ self.model = Glm5NextModel(
905
+ vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
906
+ )
907
+ if get_pp_group().is_last_rank:
908
+ self.lm_head = ParallelLMHead(
909
+ self.config.vocab_size,
910
+ self.config.hidden_size,
911
+ quant_config=quant_config,
912
+ prefix=maybe_prefix(prefix, "lm_head"),
913
+ )
914
+ else:
915
+ self.lm_head = PPMissingLayer()
916
+ logit_scale = getattr(self.config, "logit_scale", 1.0)
917
+ self.logits_processor = LogitsProcessor(
918
+ self.config.vocab_size, scale=logit_scale
919
+ )
920
+
921
+ def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
922
+ return self.model.embed_input_ids(input_ids)
923
+
924
+ def forward(
925
+ self,
926
+ input_ids: torch.Tensor | None,
927
+ positions: torch.Tensor,
928
+ intermediate_tensors: IntermediateTensors | None = None,
929
+ inputs_embeds: torch.Tensor | None = None,
930
+ **kwargs,
931
+ ) -> torch.Tensor | IntermediateTensors:
932
+ hidden_states = self.model(
933
+ input_ids, positions, intermediate_tensors, inputs_embeds, **kwargs
934
+ )
935
+ return hidden_states
936
+
937
+ @classmethod
938
+ def get_mamba_state_dtype_from_config(
939
+ cls,
940
+ vllm_config: "VllmConfig",
941
+ ) -> tuple[torch.dtype, torch.dtype]:
942
+ return MambaStateDtypeCalculator.kda_state_dtype(
943
+ vllm_config.model_config.dtype, vllm_config.cache_config.mamba_cache_dtype
944
+ )
945
+
946
+ @classmethod
947
+ def get_mamba_state_shape_from_config(
948
+ cls, vllm_config: "VllmConfig"
949
+ ) -> tuple[tuple[int, int], tuple[int, int, int]]:
950
+ parallel_config = vllm_config.parallel_config
951
+ hf_config = vllm_config.model_config.hf_config
952
+ tp_size = parallel_config.tensor_parallel_size
953
+ num_spec = (
954
+ vllm_config.speculative_config.num_speculative_tokens
955
+ if vllm_config.speculative_config
956
+ else 0
957
+ )
958
+ return MambaStateShapeCalculator.kda_state_shape(
959
+ tp_size,
960
+ hf_config.linear_num_heads,
961
+ hf_config.linear_head_dim,
962
+ conv_kernel_size=hf_config.linear_conv_kernel_dim,
963
+ num_spec=num_spec,
964
+ )
965
+
966
+ @classmethod
967
+ def get_mamba_state_copy_func(
968
+ cls,
969
+ ) -> tuple[
970
+ MambaStateCopyFunc, MambaStateCopyFunc, MambaStateCopyFunc, MambaStateCopyFunc
971
+ ]:
972
+ return MambaStateCopyFuncCalculator.kda_state_copy_func()
973
+
974
+ def compute_logits(
975
+ self,
976
+ hidden_states: torch.Tensor,
977
+ ) -> torch.Tensor | None:
978
+ logits = self.logits_processor(self.lm_head, hidden_states)
979
+ return logits
980
+
981
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
982
+ loader = AutoWeightsLoader(
983
+ self,
984
+ skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
985
+ )
986
+ return loader.load_weights(weights)
987
+
988
+
989
+ @MULTIMODAL_REGISTRY.register_processor(
990
+ Glm5NextMultiModalProcessor,
991
+ info=Glm5NextProcessingInfo,
992
+ dummy_inputs=Glm4vDummyInputsBuilder,
993
+ )
994
+ class Glm5NextForConditionalGeneration(
995
+ Glm4vForConditionalGeneration, HasInnerState, IsHybrid
996
+ ):
997
+ # The text model (KDA + dense-MLA + MoE) is a hybrid mamba model. The
998
+ # multimodal wrapper must declare the same interfaces so vLLM treats it as
999
+ # hybrid (auto-aligns mamba/attention block sizes, sizes the mamba state
1000
+ # cache); the mamba-state classmethods delegate to the text model.
1001
+ has_inner_state: ClassVar[Literal[True]] = True
1002
+ is_hybrid: ClassVar[Literal[True]] = True
1003
+
1004
+ # NOTE: weight-prefix mapping is inherited from Glm4vForConditionalGeneration
1005
+ # (``model.visual.`` -> ``visual.``, ``model.language_model.`` ->
1006
+ # ``language_model.model.``, ``lm_head.`` -> ``language_model.lm_head.``),
1007
+ # matching the GLM-OCR / GLM-4V serialization convention. If the real
1008
+ # checkpoint's safetensors keys differ (e.g. ``language_model.model.`` with
1009
+ # no outer ``model.``), override ``hf_to_vllm_mapper`` accordingly.
1010
+
1011
+ @classmethod
1012
+ def get_mamba_state_dtype_from_config(cls, vllm_config: VllmConfig):
1013
+ from .model import Glm5NextForCausalLM
1014
+
1015
+ return Glm5NextForCausalLM.get_mamba_state_dtype_from_config(vllm_config)
1016
+
1017
+ @classmethod
1018
+ def get_mamba_state_shape_from_config(cls, vllm_config: VllmConfig):
1019
+ from .model import Glm5NextForCausalLM
1020
+
1021
+ return Glm5NextForCausalLM.get_mamba_state_shape_from_config(vllm_config)
1022
+
1023
+ @classmethod
1024
+ def get_mamba_state_copy_func(cls):
1025
+ from .model import Glm5NextForCausalLM
1026
+
1027
+ return Glm5NextForCausalLM.get_mamba_state_copy_func()
1028
+
1029
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
1030
+ super(Glm4vForConditionalGeneration, self).__init__()
1031
+ config = vllm_config.model_config.hf_config
1032
+ multimodal_config = vllm_config.model_config.multimodal_config
1033
+ assert multimodal_config is not None
1034
+
1035
+ self.config = config
1036
+ self.model_config = vllm_config.model_config
1037
+ self.multimodal_config = multimodal_config
1038
+ self.use_data_parallel = multimodal_config.mm_encoder_tp_mode == "data"
1039
+ self.is_multimodal_pruning_enabled = (
1040
+ multimodal_config.is_multimodal_pruning_enabled()
1041
+ )
1042
+
1043
+ with self._mark_tower_model(vllm_config, {"image", "video"}):
1044
+ self.visual = Glm5NextVisionTransformer(
1045
+ config.text_config,
1046
+ config.vision_config,
1047
+ # Read eps from the VISION sub-config, not the top-level
1048
+ # `config.rms_norm_eps`: Glm5NextConfig.__getattribute__ mirrors
1049
+ # the latter onto text_config (1e-5), silently ignoring the
1050
+ # vision tower's own (1e-6) rms_norm_eps.
1051
+ norm_eps=config.vision_config.rms_norm_eps,
1052
+ # Vision tower ships BF16 weights in this fp8 checkpoint (no
1053
+ # weight_scale_inv for visual.*), so it must NOT inherit the
1054
+ # global fp8 quant_config -- doing so incorrectly quantizes
1055
+ # the tower
1056
+ # and yields NaN image features. Mirrors the MLA/KDA proj
1057
+ # pattern (quant_config=None for BF16 submodules).
1058
+ quant_config=None,
1059
+ prefix=maybe_prefix(prefix, "visual"),
1060
+ )
1061
+
1062
+ with self._mark_language_model(vllm_config):
1063
+ self.language_model = init_vllm_registered_model(
1064
+ vllm_config=vllm_config,
1065
+ hf_config=config.text_config,
1066
+ prefix=maybe_prefix(prefix, "language_model"),
1067
+ architectures=["Glm5NextForCausalLM"],
1068
+ )
1069
+
1070
+ # Glm5NextForCausalLM does not implement make_empty_intermediate_tensors,
1071
+ # so pipeline parallelism is gated off (consistent with the text-only
1072
+ # model) and we intentionally do not alias it here.
1073
+
1074
+ def get_encoder_cudagraph_config(self):
1075
+ # The forked vision tower (multimodal.py) has no abs-pos embeddings, so its
1076
+ # prepare_encoder_metadata does not produce "pos_embeds". Drop it from the
1077
+ # buffer_keys inherited from Glm4vForConditionalGeneration so encoder
1078
+ # CUDA-graph capture/replay does not expect a buffer that is never filled.
1079
+ config = super().get_encoder_cudagraph_config()
1080
+ config.buffer_keys = [k for k in config.buffer_keys if k != "pos_embeds"]
1081
+ return config
1082
+
1083
+
1084
+ def get_spec_layer_idx_from_weight_name(
1085
+ config: Glm5NextConfig, weight_name: str
1086
+ ) -> int | None:
1087
+ if hasattr(config, "num_nextn_predict_layers") and (
1088
+ config.num_nextn_predict_layers > 0
1089
+ ):
1090
+ layer_idx = config.num_hidden_layers
1091
+ for i in range(config.num_nextn_predict_layers):
1092
+ if weight_name.startswith(
1093
+ f"model.layers.{layer_idx + i}."
1094
+ ) or weight_name.startswith(f"layers.{layer_idx + i}."):
1095
+ return layer_idx + i
1096
+ return None
1097
+
1098
+
1099
+ def _try_load_fp8_indexer_wk(name, tensor, buf, params_dict, loaded_params):
1100
+ if "indexer.wk." not in name or "wk_weights" in name:
1101
+ return False
1102
+ is_weight = name.endswith(".weight") and tensor.dtype == torch.float8_e4m3fn
1103
+ is_scale = "weight_scale_inv" in name
1104
+ if not is_weight and not is_scale:
1105
+ return False
1106
+ layer_prefix = name.rsplit(".wk.", 1)[0]
1107
+ entry = buf.setdefault(layer_prefix, {})
1108
+ entry["weight" if is_weight else "scale"] = tensor
1109
+ if "weight" not in entry or "scale" not in entry:
1110
+ return True
1111
+
1112
+ weight_fp8, scale_inv = entry["weight"], entry["scale"]
1113
+ del buf[layer_prefix]
1114
+ block_size = weight_fp8.shape[1] // scale_inv.shape[1]
1115
+ weight_bf16 = scaled_dequantize(
1116
+ weight_fp8,
1117
+ scale_inv,
1118
+ group_shape=GroupShape(block_size, block_size),
1119
+ out_dtype=torch.bfloat16,
1120
+ )
1121
+
1122
+ fused_name = f"{layer_prefix}.wk_weights_proj.weight"
1123
+ param = params_dict[fused_name]
1124
+ param.weight_loader(param, weight_bf16, 0)
1125
+ loaded_params.add(fused_name)
1126
+ return True
1127
+
1128
+
1129
+ def _dequant_fp8_block(
1130
+ weight_fp8: torch.Tensor,
1131
+ scale_inv: torch.Tensor,
1132
+ block_size: int = 128,
1133
+ ) -> torch.Tensor:
1134
+ """Dequantize a block-FP8 (e4m3) weight with per-block scale to BF16.
1135
+
1136
+ Unlike ``scaled_dequantize`` this tolerates a non-divisible (partial last
1137
+ block) shape by zero-padding to a multiple of ``block_size`` before the
1138
+ scale broadcast and trimming back afterwards (e.g. kv_a_proj_with_mqa is
1139
+ 576 rows = 4*128 + 64).
1140
+ """
1141
+ out_dim, in_dim = weight_fp8.shape
1142
+ pad_out = (-out_dim) % block_size
1143
+ pad_in = (-in_dim) % block_size
1144
+ w = weight_fp8
1145
+ if pad_out or pad_in:
1146
+ w = torch.nn.functional.pad(w, (0, pad_in, 0, pad_out))
1147
+ # scale_inv is (ceil(out/block), ceil(in/block)); broadcast to (out, in).
1148
+ s = scale_inv.to(torch.float32)
1149
+ s_full = s.repeat_interleave(block_size, dim=0).repeat_interleave(block_size, dim=1)
1150
+ out = (w.to(torch.float32) * s_full).to(torch.bfloat16)
1151
+ return out[:out_dim, :in_dim].contiguous()
1152
+
1153
+
1154
+ # FP8 checkpoint projections that the MODEL keeps in BF16, so the block-FP8
1155
+ # (weight + weight_scale_inv) must be dequantized to BF16 on load.
1156
+ # Maps checkpoint proj-suffix -> (buffer key, model target base, fused shard id
1157
+ # or None for a direct projection, whether NoPE rope-padding applies).
1158
+ _FP8_ATTN_PROJS = {
1159
+ ".q_a_proj.": ("q_a", "fused_qkv_a_proj", 0, False),
1160
+ ".kv_a_proj_with_mqa.": ("kv_a", "fused_qkv_a_proj", 1, True),
1161
+ ".q_b_proj.": ("q_b", "q_b_proj", None, False),
1162
+ ".o_proj.": ("o_proj", "o_proj", None, False),
1163
+ }
1164
+
1165
+
1166
+ def _try_load_fp8_attn_proj(
1167
+ name,
1168
+ tensor,
1169
+ buf,
1170
+ params_dict,
1171
+ loaded_params,
1172
+ kv_a_pad_size: int,
1173
+ ) -> bool:
1174
+ """Dequantize FP8 q_a_proj / kv_a_proj_with_mqa / o_proj to BF16 on load.
1175
+
1176
+ The FP8 checkpoint stores these as block-FP8 (weight + weight_scale_inv),
1177
+ but the model holds them in BF16 (``fused_qkv_a_proj`` is always BF16 via
1178
+ DeepSeekV2FusedQkvAProjLinear; ``o_proj`` is excluded by
1179
+ modules_to_not_convert). When the model target is BF16 (no
1180
+ ``weight_scale_inv`` param) we dequantize; otherwise we return False so the
1181
+ normal stacked/direct path loads the FP8 tensor as-is.
1182
+ """
1183
+ matched = None
1184
+ for suffix, info in _FP8_ATTN_PROJS.items():
1185
+ if suffix in name:
1186
+ matched = (suffix, info)
1187
+ break
1188
+ if matched is None:
1189
+ return False
1190
+ suffix, (key, target_base, shard_id, is_kva) = matched
1191
+ is_weight = name.endswith(".weight") and tensor.dtype == torch.float8_e4m3fn
1192
+ is_scale = "weight_scale_inv" in name
1193
+ if not is_weight and not is_scale:
1194
+ return False
1195
+
1196
+ layer_prefix = name.rsplit(suffix, 1)[0]
1197
+ target_w = f"{layer_prefix}.{target_base}.weight"
1198
+ target_s = f"{layer_prefix}.{target_base}.weight_scale_inv"
1199
+ # If the model actually kept this projection in FP8, let the normal path
1200
+ # handle it (it has a weight_scale_inv param).
1201
+ if target_s in params_dict:
1202
+ return False
1203
+
1204
+ entry = buf.setdefault(layer_prefix, {}).setdefault(key, {})
1205
+ entry["weight" if is_weight else "scale"] = tensor
1206
+ if "weight" not in entry or "scale" not in entry:
1207
+ return True
1208
+
1209
+ weight_fp8, scale_inv = entry["weight"], entry["scale"]
1210
+ buf[layer_prefix].pop(key, None)
1211
+ block_size = weight_fp8.shape[1] // scale_inv.shape[1]
1212
+ weight_bf16 = _dequant_fp8_block(weight_fp8, scale_inv, block_size)
1213
+ # NoPE: pad kv_a rope portion (kv_lora_rank -> kv_lora_rank + qk_rope_head_dim).
1214
+ if is_kva and kv_a_pad_size > 0:
1215
+ pad = torch.zeros(
1216
+ kv_a_pad_size,
1217
+ weight_bf16.shape[1],
1218
+ dtype=weight_bf16.dtype,
1219
+ device=weight_bf16.device,
1220
+ )
1221
+ weight_bf16 = torch.cat([weight_bf16, pad], dim=0)
1222
+
1223
+ param = params_dict[target_w]
1224
+ if shard_id is None:
1225
+ param.weight_loader(param, weight_bf16)
1226
+ else:
1227
+ param.weight_loader(param, weight_bf16, shard_id)
1228
+ loaded_params.add(target_w)
1229
+ return True
serving/modelopt.py ADDED
The diff for this file is too large to render. See raw diff