Add pinned GLM-5.3 vLLM runtime patches
Browse files- serving/Dockerfile +17 -0
- serving/README.md +40 -0
- serving/configs/N=12576,K=4096,device_name=NVIDIA_RTX_PRO_6000_Blackwell_Workstation_Edition,dtype=fp8_w8a8,block_shape=[32,32].json +90 -0
- serving/everything-lane/Dockerfile +26 -0
- serving/everything-lane/flashinfer_mla_sparse.py +553 -0
- serving/everything-lane/flashinfer_mla_sparse_sm120.py +327 -0
- serving/everything-lane/indexer.py +1471 -0
- serving/everything-lane/kv_cache_interface.py +1141 -0
- serving/everything-lane/kvq.py +86 -0
- serving/everything-lane/kvq_kernels.py +527 -0
- serving/everything-lane/mla_attention.py +0 -0
- serving/everything-lane/model.py +1253 -0
- serving/everything-lane/modelopt.py +0 -0
- serving/everything-lane/sparse_mla_attention.py +922 -0
- serving/kda.py +655 -0
- serving/model.py +1229 -0
- serving/modelopt.py +0 -0
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
|
|
|