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 聚合。点击下图可以先看完整结构:
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 混合方案的显存使用量区别:
接下来,我们会详细讲解一下从最初的传统 MHA 到 KDA,attention 机制有什么异同, 帮助大家理解为什么 Kimi K3 会选择 linear attention 的变体 KDA。
attention 机制详解:传统 MHA
在普通 MHA 中,解码到第 个 token 时,它自己的 KV 也会先写进 cache,然后读 cache 里的全部 份,分三步计算:
- 打分:对每个 计算分数 。下标 读作「第 步的 query 对第 个 key」。
- 归一化:先求 ,再得到注意力权重 。
- 汇总:用这些权重对 value 加权求和,。
长度为 的序列累计约有 次点积,计算为 ,cache 为 。 如图所示,每一个 token 都和之前的所有 history token 有连线。
softmax attention 换来了逐位置读取能力,因此有最完整的全局记忆,但是代价也很明显。 因为每个新 token 都要重新扫描全部历史,所以我们需要存储全部历史,并且计算量是 。
attention 机制详解:linear attention
线性注意力不再保存每个 token 的 KV,而是把历史写入累加到固定大小的矩阵 里。S 作为固定大小的记忆矩阵,每一步新 token 算完都会重新写入 S:k 决定写入方向,也就是这个 token 在向量空间里指向哪儿,v 是沿这个方向写入的内容,q 再用 读取。无论上下文多长, 每步都只读写这个 S。
下图中,分别有数学角度的解释,也有和刚才例子中相同的句子的可视化, 可以直接看到两者的差别:
kₐ=(1/√2)[1,1]ᵀkᵦ=(1/√2)[1,−1]ᵀMHA 中,token「它」可以直接指向 token「猫」,因为每个历史 token 都有独立的 KV 位置; 而线性注意力的 S 没有 token 位置轴,读出的是所有历史写入在固定状态中的混合。上图的 naive linear attention 展示的就是最简单的累加关系。
attention 机制详解:DeltaNet
Naive linear attention 的问题是,状态只会不断累加,不会覆盖旧值。最简单的例子来说, 同一个 key 方向先写入 1、再写入 4,两个值都会留在 S 里;在这个简化例子中, 下次读取会得到 。虽然上下文不同,通常会使同一个词产生不同的 key,但在 1M token 这样的长序列下,有限容量的状态仍可能难以清晰地区分这些信息。比如, 前面先读到「张三住在北京」,于是状态里写入「张三 → 北京」; 后面又读到「张三搬到了上海」。那此时问到张三住在哪里, 读出来的结果很可能就是北京和上海这两条信息的混合,而不是明确地用「上海」覆盖「北京」 。
DeltaNet 的解决方案是对状态做基于误差的定向修改。它引入动态生成的标量门控 ,控制当前更新的强度。写入前,模型先沿当前 key 读出状态已有的预测 ,随后不再累加 , 而是写入目标值与已有预测之间的差: 。对已归一化的 key, 更新后的读出为 。
控制改写强度: 代表完全替换旧关联, 越小, 旧值保留得越多; 则完全不改写。
kₐ=(1/√2)[1,1]ᵀkᵦ=(1/√2)[1,−1]ᵀ这解决了同方向改写,却还有一个容量问题:无关历史不断进入固定大小的 S 后, 什么应该衰减?即使是无关的 key 向量,在真实情况下大部分也并不完全正交, 一个方向的写入会投影到另一个方向,产生串扰。在超长上下文中,影响只会越来越大。
数学细节:图中的 A/B 和串扰项
A、B 是 key 空间 中的两个单位方向 (),不是 token 槽位,也不是单条 channel。图中的 只是“key 指向 、value 为标量 1”的简写;彩色只用于追踪各次写入的来源,真实 S 只保存它们的和。
设 S 中已写入两条关联:。用 读出,得 ,其中 是两个 key 的夹角。第二项就是 B 漏进 A 的串扰,系数恰好是内积 。
推广到任意一组写入 :读出为 ,各条写入之间的干扰由 Gram 矩阵 决定。当且仅当所有 key 两两正交时读出零串扰;而 中至多只有 个两两正交的方向,存的关联一旦超过这个数,固定大小状态里的串扰就无法消除,只能被管理。
attention 机制详解:KDA
KDA 的门控是逐 channel 进行的,所以先说明 channel 是什么。K3 的 hidden size 是 7168;每个 KDA head(共 96 个)各用一份学习出的投影,把整模型共享的 7168 维 hidden state 压到 128 维, channel 就是这 128 维投影空间里的一维。key 的第 条 channel 决定内容以多大的系数写进 S 的第 行:
xₜ ∈ ℝ⁷¹⁶⁸kₜ ∈ ℝ¹²⁸高亮的一维 = 一条 channel固定状态的大小不会随上下文增长,这正是它省显存的原因; 代价是越来越多的历史必须共享同一个 S。Delta rule 能改写相同或相近的 key 方向, 却不会主动清理所有旧的无关信息,也就是上面的串扰。KDA 因此再加入逐 channel 门控: 对每个 token、每个 head,模型根据当前 hidden state 动态算出向量 ,先用 缩放旧状态的各条 channel,再执行 DeltaNet 改写。
数学细节:KDA 如何门控和改写同一个 S
完整更新为 。逐项看: 是由当前 hidden state 算出的向量门控, 把 S 的第 行整体缩放 倍——同一行里所有历史贡献一起衰减,它不会直接选择 A 或 B,也不会把 key 旋转成正交; 是标量写入强度。
把一步更新拆开看。先门控:;再沿当前 key 做 delta 改写:记 (衰减后状态对当前 key 的预测),则 。对单位 ,更新后沿同一 key 的读出为 : 完全替换, 完全保留。
把递推展开可以看到位置信息的来源:。越早的写入要穿过越多次门控与改写,衰减随距离自然累积——这正是 K3 可以不用 RoPE 的机制来源。K3 还把 的 log-decay 用 scaled sigmoid 钉了个下界(,即每步保留率 ):chunkwise 计算要按累积衰减的倒数重缩放 key,下界把这个数值范围限住之后,对角和非对角 tile 都能直接走 Tensor Core 稠密矩阵乘,省掉 Kimi Linear 里按 position-pair 处理对角块的那条路径。output gate 则从 Kimi Linear 的低秩参数化换成输入相关的全秩投影。
下图把真实的 128 维 key 空间压成二维教学切片:、,A/B 都横跨 和 。门控先缩放两行,delta rule 再沿完整 写入到达目标值 4 所需的残差。
kₐ=(1/√2)[1,1]ᵀkᵦ=(1/√2)[1,−1]ᵀ由此可见,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 内由这份部分和接管:
hℓ = hℓ₋₁ + Fℓ(hℓ₋₁)浅层信息必须穿过沿途每次相加;深层不能单独取回某一层的原始输出。
hℓ = Σᵢ αℓᵢ bᵢ深层对多个旧摘要做 softmax 加权混合,不是精确选择某一层;信息和梯度因此获得更短路径。
数学与成本:为什么 K3 每 12 层保存一份摘要
记 为 embedding, 为第 个 block 的摘要(该 block 内各层输出的累加), 为 block 内前 层输出的部分和。取回是逐层做的:block 里的第 层有自己学到的 pseudo-query ,候选集在 时是 , 时再加上 ;打分 ( 为点积型打分), 直接就是这一层的输入。注意 AttnRes 不是在 residual 上再加一项,而是用这个 softmax 混合替换掉「逐层累加」本身。权重可以集中到某一份摘要上,但永远不是硬选择。
成本:full AttnRes 每 token 要存全部 层输出,即 ;block 版只存 份摘要加 embedding,即 。K3 取 、块大小 12,即 份摘要,每 token 保存 个数。这些摘要还要随 activation 一起走 pipeline,所以 直接进入通信量。要注意 block 化省的是候选集的条目数(93 → 9),不是取回次数:93 层每层照样各查一次,只是 kernel 把跨 block 的读取按 block 批处理掉、每层再用 online softmax 合并自己那份块内部分和。
粒度与成本的折中:块越小(极端是一层一块),跨深度取回越精细,但状态、显存带宽和 pipeline 通信按 线性变贵;块越大越便宜,但块内各层输出已经相加,无法再单独取回。AttnRes 论文报告的开销是:典型推理负载下端到端延迟开销低于 2%;训练侧不开 pipeline parallelism 时开销可忽略,开 pipeline parallelism 时实测端到端开销低于 4%。
LatentMoE
除第一层 dense FFN 外,K3 的其余 FFN 使用 MoE:896 个 routed experts 中每个 token 选 16 个,另有 2 个 shared experts 始终激活。因此 routed pool 的选中比例是 ;整模型每 token 激活参数的另一个口径是 。
路由仍然在 7168 维 hidden 上打分选人,但被选中的 expert 计算发生在降到 3584 维的隐空间 里,算完再升回原宽度。这使 16 份 dispatch 的 all-to-all 流量和 expert 权重体积都减半:
到这里,我们已经看完 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 的递归状态 每读入一个 token 都会原地覆写。 假设 D 和 E 都命中前缀 ABC,如果它们直接共用同一份 ,D 先跑一步就会把它变成 ,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 淘汰等机制:
图中各分支都命中 ABC。树上的 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:
图中的 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 中, 这些开销会被稀释,所以整体效率会提升。
每层都把矩阵切成 8 份;算完必须 AllReduce,最慢的卡没到齐,所有卡都不能进入下一层。
G1–G8 各自执行一段完整层;长 prompt 切成多个 chunks,前一张 GPU 算完一个 chunk 后,把 activation 直接交给下一张。
当然,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 位置分片:每张卡先在本地 KV 上计算 partial attention, 如图所示进行分开分配,再通过一次 packed all-to-all 精确合并:
MLA 虽然有多个 attention heads,但所有 heads 共享同一份压缩 KV latent,cache 没有可供 TP 按 head 切分的轴。TP4 因而在 4 卡上复制这份 KV latent;GPU 变多了,逻辑上下文容量却没有变大。
Query 很小,复制给所有 GPU;长而占显存的 KV 按 token 位置轮转分片,每个位置只存一份。
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:
集群口径是另一回事:PD disaggregation 下要在总吞吐和每用户速度之间做取舍, 不同部署目标对应不同的并行组合:
整机效率(tok/s/GPU)
单用户速度(tok/s/user)
致谢
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 服务。
延伸阅读
可视化教学由 Yichi Zhang 写作。