Kimi K3:架构与 SGLang Day-0 Support

从 MHA 到 DeltaNet 再到 KDA,详解不同注意力机制;Kimi K3 架构如何支撑 2.8T 参数 + 1M 上下文的混合架构;以及 SGLang 如何用统一显存池、chunked PP 和 DCP 等方案把 K3 高效跑起来。

Kimi K3 的架构

K3 是一个 2.8T 参数的 MoE 模型,每个 token 激活 104B 参数。它原生支持 1M 上下文。K3 一共有 93 层,其中 69 层是 KDA 线性注意力,24 层是带门控的全局 MLA;第一层使用 dense FFN,其余 92 层使用 Stable LatentMoE。并且,93 层按每 12 层切成 8 个 block,Attention Residual 会跨 block 聚合。点击下图可以先看完整结构:

K3 整体结构
点击图中部件看说明
Outputwα除首层 dense FFN 外,其余层从 896 个 routed expert 中选 16 个,另有 2 个 shared expert;路由在 7168 维 hidden 上打分,被选中的 expert 计算在 3584 维隐空间进行。Stable LatentMoE+wα全局 softmax attention(带输出门),KV cache 随上下文增长;每 3 层 KDA 配 1 层,负责跨全文的信息交互。Gated MLA+wα除首层 dense FFN 外,其余层从 896 个 routed expert 中选 16 个,另有 2 个 shared expert;路由在 7168 维 hidden 上打分,被选中的 expert 计算在 3584 维隐空间进行。Stable LatentMoE+wα线性注意力:固定大小的递归状态,每步原地覆写,解码每步 O(1)。更新规则是 delta rule 加逐通道门控,下文展开。KDA+wα1×3×⋮Block n−1Block n−2全模型不加 RoPE。位置信息由 KDA 层的递归隐式提供,MLA 层做无位置编码的全局 attention。Embedding⋮原生视觉通路MLPK3 的原生视觉塔;图像和视频经过编码与轻量 projector 后进入共享 embedding 空间。MoonViT-V2Stable LatentMoE 模块shared expertrouted expertRouterLinear12123⋯N+NormLinear+KDA 模块LinearLinearLinearConvConvσσσ⊘⊘L2qkvαβ线性注意力:固定大小的递归状态,每步原地覆写,解码每步 O(1)。更新规则是 delta rule 加逐通道门控,下文展开。Kimi Delta AttentionNorm⊗Linear总参数 2.8T,每 token 激活 104B(≈3.7%);原生 1M token 上下文。2.8T / 104B从 SFT 阶段起做量化感知训练:MoE expert 权重使用 MXFP4、输入激活使用 MXFP8;attention、LatentMoE projection、shared expert 和 router 等非 expert 模块保持更高精度。MXFP4

KV cache memory estimation

先从显存看:无论 MLA 还是 MHA,KV cache 都会随上下文增长。Kimi K3 Day-0 Support 博客给的口径是:MLA 压缩后,K3 每个 token、每张 GPU 约需 27KB KV cache(24 层 MLA 合计),折算下来每层约 1.125KB。如果 93 层全部使用 MLA,单张 GPU 上的 1M-token 请求要占约 105GB(1.125KB × 93 层 × 1M token);同规模的未压缩 MHA,每层每 token 要存 48KB(96 head × 128 维 × K/V × bf16),是 MLA latent 的 40 倍以上。 把这部分显存降下来是必须先解决的问题,所以显然,K3 不能在大部分层保留随上下文增长的 softmax attention。

因此,K3 采用 3 KDA + 1 MLA 的交错结构。KDA 作为 linear attention 的变体, 递归状态大小固定,不会随上下文增长(具体方法后文会阐述)。每张 GPU 每请求约 54MB, 每步原地更新;MLA 保留全局 attention。全模型不使用显式位置编码(NoPE),位置信息由 KDA 的递归隐式提供。下图展示了全部使用 MLA 和 KDA+MLA 混合方案的显存使用量区别:

KV cache 对比图
KKKMKKKM3 层 KDA + 1 层 MLA 交错,×23 组,末尾再加 1 层 MLA,共 93 层
假想:93 层全 MLA上下文 0K token · cache 0.0 GB · 每 token 105 KB
020406080100GB
K3:24 层 MLA + 69 层 KDA上下文 0K token · cache 0.1 GB · 每 token 27 KB
020406080100GB
t = 0/16

接下来,我们会详细讲解一下从最初的传统 MHA 到 KDA,attention 机制有什么异同, 帮助大家理解为什么 Kimi K3 会选择 linear attention 的变体 KDA。

attention 机制详解:传统 MHA

在普通 MHA 中,解码到第 tt 个 token 时,它自己的 KV 也会先写进 cache,然后读 cache 里的全部 tt 份,分三步计算:

  1. 打分:对每个 i≤ti \le t 计算分数 zt,i=qt⋅kidkz_{t,i} = \frac{q_t \cdot k_i}{\sqrt{d_k}}。下标 t,it,i 读作「第 tt 步的 query 对第 ii 个 key」。
  2. 归一化:先求 Z=∑j=1texp⁡(zt,j)Z = \sum_{j=1}^{t} \exp(z_{t,j}),再得到注意力权重 at,i=exp⁡(zt,i)/Za_{t,i} = \exp(z_{t,i}) / Z。
  3. 汇总:用这些权重对 value 加权求和,ot=∑i=1tat,i⋅vio_t = \sum_{i=1}^{t} a_{t,i} \cdot v_i。

长度为 NN 的序列累计约有 N2/2N^2/2 次点积,计算为 O(N2)O(N^2),cache 为 O(N)O(N)。 如图所示,每一个 token 都和之前的所有 history token 有连线。

MHA 图示
cache 0 格 · 本步点积 0 次 · 累计 0 次
KV cache(本步结束后)
t = 0/8

softmax attention 换来了逐位置读取能力,因此有最完整的全局记忆,但是代价也很明显。 因为每个新 token 都要重新扫描全部历史,所以我们需要存储全部历史,并且计算量是 O(N2)O(N^2)。

attention 机制详解:linear attention

线性注意力不再保存每个 token 的 KV,而是把历史写入累加到固定大小的矩阵 S=∑ikivi⊤S = \sum_i k_i v_i^{\top} 里。S 作为固定大小的记忆矩阵,每一步新 token 算完都会重新写入 S:k 决定写入方向,也就是这个 token 在向量空间里指向哪儿,v 是沿这个方向写入的内容,q 再用 o=S⊤qo = S^{\top} q 读取。无论上下文多长, 每步都只读写这个 S。

下图中,分别有数学角度的解释,也有和刚才例子中相同的句子的可视化, 可以直接看到两者的差别:

Naive linear attention 图示
二维教学切片:真实每个 head 的 S 为 128×128
二维 key 空间(教学切片)
ch₁ch₂kₐkᵦ
kₐ=(1/√2)[1,1]ᵀkᵦ=(1/√2)[1,−1]ᵀ
箭头 = 完整 key;坐标轴 = channels
本步进入的 S
ch₁
0
ch₂
0
−0+
S 为空
→直接累加 kvᵀS←S+kvᵀ
本步结束后的 S
ch₁
0
ch₂
0
−0+
S 为空
A 的旧贡献B 的贡献A=4 的新贡献颜色仅追踪贡献来源;真实 S 只保存每个格子的总和
t = 0/4

MHA 中,token「它」可以直接指向 token「猫」,因为每个历史 token 都有独立的 KV 位置; 而线性注意力的 S 没有 token 位置轴,读出的是所有历史写入在固定状态中的混合。上图的 naive linear attention 展示的就是最简单的累加关系。

attention 机制详解:DeltaNet

Naive linear attention 的问题是,状态只会不断累加,不会覆盖旧值。最简单的例子来说, 同一个 key 方向先写入 1、再写入 4,两个值都会留在 S 里;在这个简化例子中, 下次读取会得到 1+41+4。虽然上下文不同,通常会使同一个词产生不同的 key,但在 1M token 这样的长序列下,有限容量的状态仍可能难以清晰地区分这些信息。比如, 前面先读到「张三住在北京」,于是状态里写入「张三 → 北京」; 后面又读到「张三搬到了上海」。那此时问到张三住在哪里, 读出来的结果很可能就是北京和上海这两条信息的混合,而不是明确地用「上海」覆盖「北京」 。

DeltaNet 的解决方案是对状态做基于误差的定向修改。它引入动态生成的标量门控 βt\beta_t,控制当前更新的强度。写入前,模型先沿当前 key 读出状态已有的预测 rold=St−1⊤ktr_{\text{old}} = S_{t-1}^{\top} k_t,随后不再累加 vtv_t, 而是写入目标值与已有预测之间的差: St=St−1+βtkt(vt−rold)⊤S_t = S_{t-1} + \beta_t k_t (v_t - r_{\text{old}})^{\top}。对已归一化的 key, 更新后的读出为 rnew=(1−βt) rold+βtvtr_{\text{new}} = (1-\beta_t)\, r_{\text{old}} + \beta_t v_t。

βt\beta_t 控制改写强度:βt=1\beta_t = 1 代表完全替换旧关联,βt\beta_t 越小, 旧值保留得越多;βt=0\beta_t = 0 则完全不改写。

DeltaNet 图示调整 β,观察 A=4 如何改写 A=1
二维 key 空间(教学切片)
ch₁ch₂kₐkᵦ
kₐ=(1/√2)[1,1]ᵀkᵦ=(1/√2)[1,−1]ᵀ
箭头 = 完整 key;坐标轴 = channels
本步进入的 S
ch₁
0
ch₂
0
−0+
S 为空
→等待 token
本步结束后的 S
ch₁
0
ch₂
0
−0+
S 为空
A 的旧贡献B 的贡献A=4 的新贡献颜色仅追踪贡献来源;真实 S 只保存每个格子的总和
t = 0/4

这解决了同方向改写,却还有一个容量问题:无关历史不断进入固定大小的 S 后, 什么应该衰减?即使是无关的 key 向量,在真实情况下大部分也并不完全正交, 一个方向的写入会投影到另一个方向,产生串扰。在超长上下文中,影响只会越来越大。

数学细节:图中的 A/B 和串扰项

A、B 是 key 空间 Rdk\mathbb{R}^{d_k} 中的两个单位方向 ka,kbk_a, k_b(∥ka∥=∥kb∥=1\lVert k_a \rVert = \lVert k_b \rVert = 1),不是 token 槽位,也不是单条 channel。图中的 A=1A=1 只是“key 指向 kak_a、value 为标量 1”的简写;彩色只用于追踪各次写入的来源,真实 S 只保存它们的和。

设 S 中已写入两条关联:S=kava⊤+kbvb⊤S = k_a v_a^{\top} + k_b v_b^{\top}。用 qa=kaq_a = k_a 读出,得 oa=S⊤qa=(ka⊤ka) va+(kb⊤ka) vb=va+cos⁡θ⋅vbo_a = S^{\top} q_a = (k_a^{\top} k_a)\, v_a + (k_b^{\top} k_a)\, v_b = v_a + \cos\theta \cdot v_b,其中 θ\theta 是两个 key 的夹角。第二项就是 B 漏进 A 的串扰,系数恰好是内积 ka⊤kbk_a^{\top} k_b。

推广到任意一组写入 {(ki,vi)}\{(k_i, v_i)\}:读出为 o(q)=∑i(ki⊤q) vio(q) = \sum_i (k_i^{\top} q)\, v_i,各条写入之间的干扰由 Gram 矩阵 Gij=ki⊤kjG_{ij} = k_i^{\top} k_j 决定。当且仅当所有 key 两两正交时读出零串扰;而 Rdk\mathbb{R}^{d_k} 中至多只有 dkd_k 个两两正交的方向,存的关联一旦超过这个数,固定大小状态里的串扰就无法消除,只能被管理。

串扰图示固定 kₐ,拖动 kᵦ;示例取 qₐ=kₐ、vₐ=vᵦ=1
θ=60°kₐ = qₐkᵦ拖动 Bcos θ = 0.50S = kₐ + kᵦkₐᵀkᵦ = cos θ = 0.50S = kₐ·vₐ + kᵦ·vᵦoₐ = qₐᵀS = 1 + cos θA = 1B = 0.50A 的目标信号 = 1B 漏进 A 的串扰 = 0.50夹角越小,串扰越大

attention 机制详解:KDA

KDA 的门控是逐 channel 进行的,所以先说明 channel 是什么。K3 的 hidden size 是 7168;每个 KDA head(共 96 个)各用一份学习出的投影,把整模型共享的 7168 维 hidden state 压到 128 维, channel 就是这 128 维投影空间里的一维。key 的第 jj 条 channel 决定内容以多大的系数写进 S 的第 jj 行:

Channel 图示
hidden statexₜ ∈ ℝ⁷¹⁶⁸
一个 head 的 kkₜ ∈ ℝ¹²⁸高亮的一维 = 一条 channel

固定状态的大小不会随上下文增长,这正是它省显存的原因; 代价是越来越多的历史必须共享同一个 S。Delta rule 能改写相同或相近的 key 方向, 却不会主动清理所有旧的无关信息,也就是上面的串扰。KDA 因此再加入逐 channel 门控: 对每个 token、每个 head,模型根据当前 hidden state 动态算出向量 αt\alpha_t,先用 Diag(αt)\mathrm{Diag}(\alpha_t) 缩放旧状态的各条 channel,再执行 DeltaNet 改写。

数学细节:KDA 如何门控和改写同一个 S

完整更新为 St=(I−βtktkt⊤) Diag(αt) St−1+βtktvt⊤S_t = \big(I - \beta_t k_t k_t^{\top}\big)\,\mathrm{Diag}(\alpha_t)\, S_{t-1} + \beta_t k_t v_t^{\top}。逐项看:αt∈(0,1)dk\alpha_t \in (0,1)^{d_k} 是由当前 hidden state 算出的向量门控,Diag(αt)\mathrm{Diag}(\alpha_t) 把 S 的第 jj 行整体缩放 αt[j]\alpha_t[j] 倍——同一行里所有历史贡献一起衰减,它不会直接选择 A 或 B,也不会把 key 旋转成正交;βt∈(0,1)\beta_t \in (0,1) 是标量写入强度。

把一步更新拆开看。先门控:S~=Diag(αt) St−1\tilde S = \mathrm{Diag}(\alpha_t)\, S_{t-1};再沿当前 key 做 delta 改写:记 rold=S~⊤ktr_{\text{old}} = \tilde S^{\top} k_t(衰减后状态对当前 key 的预测),则 St=S~+βtkt(vt−rold)⊤S_t = \tilde S + \beta_t k_t (v_t - r_{\text{old}})^{\top}。对单位 ktk_t,更新后沿同一 key 的读出为 St⊤kt=(1−βt) rold+βtvtS_t^{\top} k_t = (1-\beta_t)\, r_{\text{old}} + \beta_t v_t:βt=1\beta_t = 1 完全替换,βt=0\beta_t = 0 完全保留。

把递推展开可以看到位置信息的来源:St=∑s≤t[∏u=s+1t(I−βukuku⊤)Diag(αu)] βsksvs⊤S_t = \sum_{s \le t} \Big[\prod_{u=s+1}^{t} \big(I - \beta_u k_u k_u^{\top}\big)\mathrm{Diag}(\alpha_u)\Big]\, \beta_s k_s v_s^{\top}。越早的写入要穿过越多次门控与改写,衰减随距离自然累积——这正是 K3 可以不用 RoPE 的机制来源。K3 还把 α\alpha 的 log-decay 用 scaled sigmoid 钉了个下界(gmin⁡=−5g_{\min} = -5,即每步保留率 >e−5≈6.7×10−3> e^{-5} \approx 6.7\times10^{-3}):chunkwise 计算要按累积衰减的倒数重缩放 key,下界把这个数值范围限住之后,对角和非对角 tile 都能直接走 Tensor Core 稠密矩阵乘,省掉 Kimi Linear 里按 position-pair 处理对角块的那条路径。output gate 则从 Kimi Linear 的低秩参数化换成输入相关的全秩投影。

下图把真实的 128 维 key 空间压成二维教学切片:ka=12[1,1]⊤k_a = \tfrac{1}{\sqrt{2}}[1,1]^{\top}、kb=12[1,−1]⊤k_b = \tfrac{1}{\sqrt{2}}[1,-1]^{\top},A/B 都横跨 ch1\mathrm{ch}_1 和 ch2\mathrm{ch}_2。门控先缩放两行,delta rule 再沿完整 kak_a 写入到达目标值 4 所需的残差。

KDA 图示调整 α₁、α₂ 和 β,观察状态如何变化α₁α₂β
二维 key 空间(教学切片)
ch₁ch₂kₐkᵦ
kₐ=(1/√2)[1,1]ᵀkᵦ=(1/√2)[1,−1]ᵀ
箭头 = 完整 key;坐标轴 = channels
本步进入的 S
ch₁
0
ch₂
0
−0+
S 为空
→Diag(α)α=1
② Diag(α) 后的 S
ch₁
0
ch₂
0
−0+
S 为空
→沿完整 kₐ 做 delta首次写入
③ 沿 kₐ delta 后的 S
ch₁
0
ch₂
0
−0+
S 为空
A 的旧贡献B 的贡献本步沿 kₐ 的残差更新颜色仅追踪贡献来源;真实 S 只保存每个格子的总和
t = 0/4

由此可见,Kimi K3 不需要 RoPE,是因为 KDA 的状态更新本身就带有位置信息。越早出现的 token,在传递到当前位置前会经历更多次衰减和状态变换;越近的 token 经历的变换更少, 因此模型可以据此感知顺序和距离。相比 RoPE 用固定旋转显式编码位置,KDA 通过可学习、 输入相关的衰减隐式建模位置。K3 还给衰减加了下界约束,把 chunkwise 计算的数值范围限住, 让 KDA kernel 的所有 causal tile 都能走 Tensor Core;output gate 则换成了全秩投影。

Attention Residual

传统 Transformer 的 residual stream 像一份从底层一路传到顶层的草稿: 每一层都会在上面继续修改。它保证信息能够向上传递,但到了第 90 多层, 浅层细节已经和几十次更新叠在一起,深层无法单独翻回某个较早版本。

Attention Residual 相当于额外保存几份「历史版本」。K3 把 93 层切成 8 个 block(前 7 个各 12 层,最后一个 9 层),每个 block 把自己的层输出累加成一份 block 表示。这样每一层 都不必只依赖当前草稿:它用自己学到的 pseudo-query,给 embedding、之前各 block 的摘要、 以及当前 block 到上一层为止的部分和分配权重,混合出的结果就是这一层的输入——原来那条 一路累加的 residual,在 block 内由这份部分和接管:

Attention Residual 图示
传统 residualhℓ = hℓ₋₁ + Fℓ(hℓ₋₁)
同一个 residual stream:Emb + F₁ + F₂ + …EmbL1h1=h0+F1L2h2=h1+F2L3h3=h2+F3…L93第 ℓ 层只直接收到上层传来的累计结果 hℓ₋₁

浅层信息必须穿过沿途每次相加;深层不能单独取回某一层的原始输出。

Attention Residualhℓ = Σᵢ αℓᵢ bᵢ
旧 block 摘要分别保留α=0.22Embα=0.03B₁α=0.05B₂α=0.10B₃…α=0.22B₇ΣαB₈送入 B₈ 的第 1 层

深层对多个旧摘要做 softmax 加权混合,不是精确选择某一层;信息和梯度因此获得更短路径。

数学与成本:为什么 K3 每 12 层保存一份摘要

记 b0b_0 为 embedding,bnb_n 为第 nn 个 block 的摘要(该 block 内各层输出的累加),bn i−1b_n^{\,i-1} 为 block nn 内前 i−1i-1 层输出的部分和。取回是逐层做的:block nn 里的第 ii 层有自己学到的 pseudo-query qq,候选集在 i=1i = 1 时是 {b0,b1,…,bn−1}\{b_0, b_1, \ldots, b_{n-1}\},i≥2i \ge 2 时再加上 bn i−1b_n^{\,i-1};打分 α=softmax(s(q,⋅))\alpha = \mathrm{softmax}\big(s(q, \cdot)\big)(ss 为点积型打分),h=∑α⋅bh = \sum \alpha \cdot b 直接就是这一层的输入。注意 AttnRes 不是在 residual 上再加一项,而是用这个 softmax 混合替换掉「逐层累加」本身。权重可以集中到某一份摘要上,但永远不是硬选择。

成本:full AttnRes 每 token 要存全部 LL 层输出,即 O(Ld)O(Ld);block 版只存 N=⌈L/12⌉N = \lceil L/12 \rceil 份摘要加 embedding,即 O(Nd)O(Nd)。K3 取 L=93L = 93、块大小 12,即 N=8N = 8 份摘要,每 token 保存 9×71689 \times 7168 个数。这些摘要还要随 activation 一起走 pipeline,所以 NN 直接进入通信量。要注意 block 化省的是候选集的条目数(93 → 9),不是取回次数:93 层每层照样各查一次,只是 kernel 把跨 block 的读取按 block 批处理掉、每层再用 online softmax 合并自己那份块内部分和。

粒度与成本的折中:块越小(极端是一层一块),跨深度取回越精细,但状态、显存带宽和 pipeline 通信按 NN 线性变贵;块越大越便宜,但块内各层输出已经相加,无法再单独取回。AttnRes 论文报告的开销是:典型推理负载下端到端延迟开销低于 2%;训练侧不开 pipeline parallelism 时开销可忽略,开 pipeline parallelism 时实测端到端开销低于 4%。

LatentMoE

除第一层 dense FFN 外,K3 的其余 FFN 使用 MoE:896 个 routed experts 中每个 token 选 16 个,另有 2 个 shared experts 始终激活。因此 routed pool 的选中比例是 16/896≈1.8%16/896 \approx 1.8\%;整模型每 token 激活参数的另一个口径是 104B/2.8T≈3.7%104\text{B}/2.8\text{T} \approx 3.7\%。

路由仍然在 7168 维 hidden 上打分选人,但被选中的 expert 计算发生在降到 3584 维的隐空间 里,算完再升回原宽度。这使 16 份 dispatch 的 all-to-all 流量和 expert 权重体积都减半:

LatentMoE 图示

到这里,我们已经看完 K3 如何用 KDA、MLA、Attention Residual 和 LatentMoE 组成混合架构。但模型结构只解决了「怎么算」;真正上线时,SGLang 还要解决「这些不同状态如何放进显存、如何并行 prefill」等优化相关的问题。 下一部分就从这些系统问题展开。

SGLang 的 Day-0 适配

K3 的混合架构会产生两种生命周期完全不同的推理状态:

MLA(24 层)KDA(69 层)
状态形式KV cache,逐 token 追加递归状态,固定大小
增长方式每 token、每 GPU 约 27KB,只增不减每请求、每 GPU 约 54MB(TP=8),每步原地覆写

MLA 的 KV 随 token 追加,KDA 的状态却按请求固定占用并且每步覆写。下面的适配重点, 就是如何同时管理这两种状态,并据此选择 prefill 和 decode 的并行方式。

RadixAttention:如何给 KDA 做前缀缓存

传统 RadixAttention 能让多个请求共享前缀,是因为传统的 attention KV 只随 token 增多而追加,不改写原有历史。但 KDA 的递归状态 SS 每读入一个 token 都会原地覆写。 假设 D 和 E 都命中前缀 ABC,如果它们直接共用同一份 S(ABC)S(\text{ABC}),D 先跑一步就会把它变成 S(ABCD)S(\text{ABCD}),E 也就无法再从 ABC 的状态开始, 因此从机制上破坏了 prefix cache 的可能性。那么我们该如何维持 RadixAttention 的缓存树呢?

所以,SGLang 的方法是:不让请求直接改 radix tree 上的共享状态。它把树上的 KDA 状态当作只读 checkpoint:命中前缀后,先 copy-on-write 到请求的私有工作槽,再让 forward 原地改写这份副本,通过复制与分叉,避免了覆写造成的 prefix 被破坏。下图中, 图一对比了 KDA 和 MLA 在两种负载下的 radix cache 增长方式,图二逐步演示 copy-on-write、snapshot、donate 和 checkpoint 淘汰等机制:

KDA 前缀缓存图示KV 可以共享同一份前缀;KDA 要把共享 checkpoint 恢复到私有工作槽后再改写
负载变化时,两种状态跟着什么增长?只对比增长方向,四张图不共用纵轴。
负载变化KDA stateMLA KV cache
同一请求变长
约 54MB / 活跃请求(TP=8),不随 token 数增长
约 27KB / token,随上下文线性增长
缓存 token 总数不变,活跃分支变多
每个活跃分支需要自己的可变工作状态
只看缓存的 token 总数;分支拓扑本身不增加 KV
一次前缀复用按 1 → 2 → 3 → 4 发生;请求继续生成时,会在后续 checkpoint 边界重复 3 → 4。

图中各分支都命中 ABC。树上的 S(ABC) 是只读 checkpoint,不能直接当作请求的工作状态。

ABCDEFXGHIQJK
树上的 S(ABC) 是只读 checkpoint;所有活跃请求都从这个共享前缀开始。

双状态显存管理

因为 K3 使用了 KDA 和 MLA 的混合层架构,所以在显存管理上,我们也需要切出不同的分区。 原方案在启动时预先切出 KDA 区和 MLA 区。但 KDA 需求取决于并发数,MLA 需求取决于总上下文长度,一旦负载偏离预估,就会出现一边耗尽、另一边空着的情况。 其实解决方法非常直接,SGLang 把两者放进同一个池:KDA 固定块从左边分配,MLA 页从右边分配,共用中间的空闲区。请求结束、abort 或被 retract 时如果中间空出一个洞, 就把末端的一块搬进去补上,保证空闲区始终连成一片、两端始终是紧实的,这样 54MB 的 KDA 块和 27KB 的 MLA 页可以从同一批字节里取,不必被迫统一页大小。Day-0 版本通过 --enable-unified-memory opt-in:

显存池对比图请求准入时同时分配两类状态;之后 KDA 固定,MLA 随 token 增长
静态双池(启动时切死)
当前请求 0
KDA 0= 0 请求 × 3 页MLA 0随 token 逐页累积空闲 44 页未使用容量失败 0
KDA 区MLA 区
统一池(SGLang)
当前请求 0
KDA 0= 0 请求 × 3 页MLA 0随 token 逐页累积空闲 44 页未使用容量失败 0
KDA →← MLA
t = 0/32

图中的 baseline 在两个静态分区内随机挑选空位;当 MLA 区耗尽时, KDA 区仍有空位,却无法借给 MLA。统一池把 KDA 从左端、MLA 从右端放入, 因此仍能继续运行所有请求。这里的请求序列是手工构造的教学示例,不是线上 trace。

Chunked pipeline prefill

Prefill 一次处理大量 prompt tokens,有足够工作可以切成 chunks 灌满流水线。TP8 让 8 张卡同时进行 prefill 工作,但是问题在于:每经过一层,都需要锁步进行 AllReduce, 以获得完整结果进入下一层。而 chunked PP8 则不同,PP8 把 93 层按深度分给 G1–G8, 让不同 GPU 同时处理不同 chunk。GPU 之间只传递上一段层的 activation,这些 P2P 传输可以与下一 chunk 的计算重叠。如图所示,G1–G8 各自跑的都是一段完整的层, 而不是把单层切成 8 份,所以不需要每一层之间都进行 AllReduce。 当然代价是灌入和排空需要一定过程,没办法上来就打满,不过在长上下文的 prefill 中, 这些开销会被稀释,所以整体效率会提升。

Chunked pipeline prefill 图示同样 8 张 GPU;上面 TP8,下面 chunked PP8
问题TP8:每层之后 8 卡同步一次

每层都把矩阵切成 8 份;算完必须 AllReduce,最慢的卡没到齐,所有卡都不能进入下一层。

G1计算AllReduce计算AllReduce计算AllReduce
G2计算AllReduce计算AllReduce计算AllReduce
G3计算AllReduce计算AllReduce计算AllReduce
G4计算AllReduce计算AllReduce计算AllReduce
G5计算AllReduce计算AllReduce计算AllReduce
G6计算AllReduce计算AllReduce计算AllReduce
G7计算AllReduce计算AllReduce计算AllReduce
G8计算AllReduce计算AllReduce计算AllReduce
✓ 93 层反复同步✓ 通信处在关键路径✓ GEMM 被切窄,效率下降
解决Chunked PP8:层切 8 段,prompt 切成 chunks

G1–G8 各自执行一段完整层;长 prompt 切成多个 chunks,前一张 GPU 算完一个 chunk 后,把 activation 直接交给下一张。

G1L1–12C1C2C3C4C5
G2L13–24C1C2C3C4C5
G3L25–36C1C2C3C4C5
G4L37–48C1C2C3C4C5
G5L49–60C1C2C3C4C5
G6L61–72C1C2C3C4C5
G7L73–84C1C2C3C4C5
G8L85–93C1C2C3C4C5
斜线 = 同一 chunk 依次流过 G1→G8;P2P(point-to-point)= 相邻两张 GPU 直接传 activation,不需要 8 卡共同汇总。层号按 93 层近似均分示意。
✓ 整层 GEMM,更宽更高效✓ P2P 的 91% 被下一 chunk 的计算隐藏✓ 每张 GPU 只保留约 12 层的 KV/activation
8K prefill 实测(2×4 GB300,仅改变并行拓扑)
每节点 prefill capacity
TEP81.00×
PP8×TP11.45–1.72×(代表点 1.64×)
暴露在关键路径上的通信 / 1K tokens
TP8 · 9.38 ms
PP8 · 0.88 ms约减少 91%

当然,decode 是另一个故事。Decode 每步通常只有一个新 token,不足以灌满 PP8 这样的深流水线,所以 TP8 仍然更合适。PD disaggregation 下的典型分工是 PP8 prefill → TP8 / DCP8 decode。前文也讲过,PP 也会产生灌入和排空开销,当请求或 chunk 太少时并不占优。

Decode context parallelism(DCP)

MLA 虽然有多个 attention heads,但所有 heads 共享同一份压缩 KV latent,所以当 TP 按 head 分片的时候,不像 MHA,压缩后的 KV latent 没有一个清晰的轴去分片。因此,TP8 的每张卡都必须复制完整 MLA KV latent。这意味着,每一张 GPU 上都有重复的 MLA KV latent,因此,就算增加 GPU 并行,也不会增加逻辑上下文容量。而 SGLang 使用 DCP, 改为按 token 位置分片:每张卡先在本地 1/N1/N KV 上计算 partial attention, 如图所示进行分开分配,再通过一次 packed all-to-all 精确合并:

DCP 图示同一段 12-token MLA context;上面是 naive TP,下面是 DCP
并行 GPU 数 N每卡只扫约 1/4 KV
问题Naive TP:每张卡都存一份完整 KV

MLA 虽然有多个 attention heads,但所有 heads 共享同一份压缩 KV latent,cache 没有可供 TP 按 head 切分的轴。TP4 因而在 4 卡上复制这份 KV latent;GPU 变多了,逻辑上下文容量却没有变大。

T1T2T3T4T5T6T7T8T9T10T11T12
GPU 1
GPU 2
GPU 3
GPU 4
同一段 12-token context物理 KV cells:48(复制 4×)
解决DCP:KV 按 token 位置轮流分到各卡

Query 很小,复制给所有 GPU;长而占显存的 KV 按 token 位置轮转分片,每个位置只存一份。

T1T2T3T4T5T6T7T8T9T10T11T12
GPU 1
GPU 2
GPU 3
GPU 4
物理 KV cells:12(每位置仅 1 份)同样 48-cell 显存:逻辑容量 12 → 48 tokens
一次 MLA decode 为什么仍然精确
① 各 GPU 本地生成完整 qq 很小,无需广播→② 各 GPU 本地 attention只扫自己 1/N 的 KV→③ 一次 packed all-to-all每段只有 o 的 1/N→④ 按 LSE 加权合并结果与完整 softmax 一致
实色 = 该 GPU 保存空框 = 此位置在其他 GPU

DCP 主要增加容量和并发,不是降低单条短请求的延迟。它让更多长会话留在 GPU,避免 host offload 和 re-prefill 造成吞吐坍塌。DCP 组是建在 TP 组内部的,所以 TP8 配 DCP8 仍然 只是 8 张卡:K3 上逻辑容量从约 1.5M 提到 12.2M tokens(约 7.9×),48 个 agent sessions 达到 541 tok/s。代价是每个 MLA 层多一次 all-to-all;当然,KDA 状态 S 不会随上下文增长,也没有 token 的位置轴,所以仍按 TP/head 切分。

性能数字与 Benchmark 结果

BS=1 decode、按 GPU 归一化的系统吞吐和每用户速度是三种不同口径。先看单请求速度: 15 轮 kernel 与通信优化把非投机 BS=1 decode 从 44.3 提到 112.5 tok/s;DSpark 投机解码是另一条线,约 423 tok/s:

BS=1 decode 优化图8×GB300 · TP8 · BF16 KV cache · 非投机
44.3 → 112.5 tok/s408012044.3112.5P0P1544.3 → 112.5 tok/s
加上 DSpark112.5 tok/s→约 423 tok/s

集群口径是另一回事:PD disaggregation 下要在总吞吐和每用户速度之间做取舍, 不同部署目标对应不同的并行组合:

部署取舍图GB300 · 8K input / 1K output · PD disaggregation
部署目标
总吞吐优先增加 decode 实例 →单用户优先
总吞吐:2,808 tok/s/GPU;单用户 18.7 tok/s。

致谢

Kimi K3 的 Day-0 支持由 RadixArk 的 SGLang & Miles 团队、Moonshot AI 团队,以及 NVIDIA、AMD、Approaching AI、Baseten 和 Modal 共同完成。

  • AMD: Wun-guo Huang、Xinyi Song、Hai Xiao、Soga Lin、Duyi Wang、Thomas Wang
  • Approaching AI: Huanming Shen、Xiaohao Zhang、Nan Li、Mingxing Zhang

感谢 DigitalOcean 提供 AMD 测试实例。也感谢 Google Cloud、DigitalOcean、Nebius、fal、 RunPod、DeepInfra 和 GMI Cloud 使用 SGLang 提供 Kimi K3 服务。

上手试试 · Try on SGLang 打开 SGLang 仓库,在真实推理引擎里跑一跑这套机制

延伸阅读

可视化教学由 Yichi Zhang 写作。