关键词: Decode、KV Cache、模型权重、显存带宽、MoE、Dense、GQA、MLA、Sliding Window Attention、TTFT、Output Speed、Batch Size、长上下文推理

先看结论

  • 结论 1: decode 阶段如果对上下文长度非常敏感,通常更像是 KV cache 读取瓶颈
  • 结论 2: decode 阶段如果对 batch size 非常敏感,batch 增大后总吞吐明显改善,通常更像是 权重读取被摊薄 / 小 batch 利用率不足
  • 结论 3: MoE 能减少每个 token 激活的 FFN 专家权重和计算,但它不天然减少 attention 的 KV cache 大小。
  • 结论 4: 最快的定位方式不是先上 profiler,而是先做 context scalingbatch scaling 两个小实验。

如果只用一句话概括:

随上下文变长而明显变慢,多半是 KV cache;随 batch 变大而明显变好,多半是权重读取和计算利用率问题。

一、为什么这个问题值得单独拆开讲

大模型推理一般拆成两个阶段:

Prefill:处理 prompt,生成第一枚 token 之前
Decode:一个 token 一个 token 持续生成

很多性能分析文章会说:

decode 阶段更容易受显存带宽影响。

这句话本身没错,但还不够细。

因为 decode 阶段至少有两类重要的数据要读:

1. 模型权重
2. 历史上下文的 KV cache

这两者都会消耗显存带宽,但它们的行为不一样。

模型权重的读取,主要和模型结构、激活参数量、量化格式、batch size 有关。

KV cache 的读取,主要和上下文长度、层数、KV head 数量、head dim、数据类型、并发数有关。

所以,当看到 decode tokens/s 下降时,不能直接说:

这是带宽瓶颈。

更应该继续问一句:

到底是读权重拖慢了,还是读 KV cache 拖慢了?

二、Decode 阶段每生成一个 token,主要在读什么

假设模型已经读完 prompt,现在开始生成回答。

每生成一个新 token,模型大致要做这些事:

1. 当前 token 经过每一层 Transformer
2. 每一层生成当前 token 的 Q/K/V
3. 当前 Q 去匹配历史所有 K
4. 根据注意力权重读取历史所有 V
5. 当前 token 再经过 FFN / MLP
6. 最后输出下一个 token 的概率分布

这里有两块特别重要。

第一块是模型自身权重。

每一层 attention、FFN、输出层都需要读权重。对于 dense 模型来说,每个 token 基本都要经过完整的 dense 权重路径。

第二块是 KV cache。

当前 token 的 Q 要去看历史 token 的 K/V。上下文越长,历史 K/V 越多;并发越高,同时存在的 KV cache 越多。

所以 decode 阶段的核心矛盾不是“有没有读数据”,而是:

到底读的是模型参数更多,还是读历史上下文更多。

三、为什么 MoE 会让这个问题更微妙

很多人会说:

MoE 每次只激活少数专家,所以推理更省。

这句话是对的,但要说清楚省在哪里。

MoE 通常替换的是 Transformer 里的 FFN / MLP 部分:

Dense Transformer Layer:
Attention + Dense FFN

MoE Transformer Layer:
Attention + Router + Top-k Experts

如果一个 MoE 层有 8 个专家,每个 token 只激活 top-2 专家,那么它确实减少了:

  • FFN 部分的计算量
  • FFN 专家权重的读取量
  • 每个 token 的激活参数量

但是它没有天然减少:

  • prompt token 数量
  • attention 的上下文范围
  • KV cache 的长度
  • 每层需要保存和读取的历史 K/V

所以,MoE 对 decode 阶段的帮助取决于你当前到底卡在哪里。

如果瓶颈主要在:

每个 token 都要读大量 FFN 权重、做大量 FFN 计算

那 MoE 的优势会比较明显。

如果瓶颈主要在:

长上下文下每个 token 都要读大量 KV cache

那 MoE 不一定能直接解决问题。

这也是为什么同样是“更省”的架构,MoEGQAMLASliding Window Attention 解决的问题并不一样。

MoE:主要减少 FFN 路径的激活权重和计算
GQA:主要减少 KV head 数量,从而减少 KV cache
MLA:主要压缩 KV 表示,从而减少 KV cache
Sliding Window Attention:主要减少每个 token 能看的历史范围

四、先建立一个简单判断模型

我们可以先用一个非常朴素的判断模型。

decode 阶段,每生成一个 token:

权重读取成本 ≈ 与模型激活参数量相关
KV cache 读取成本 ≈ 与上下文长度相关

所以:

如果上下文变长,decode 速度明显下降:
更像 KV cache 压力变大

如果上下文变长,decode 速度变化不大:
更像权重读取 / FFN 计算 / 调度开销在主导

这就是第一个关键判断。

再看 batch。

如果 batch 从 1 增加到 4、8、16,总吞吐明显提升,说明 GPU 原来没有被充分利用。

尤其在小 batch decode 时,很多模型会出现一种情况:

每个 token 都要读模型权重,但计算量太小,Tensor Core 吃不满,权重读取和 kernel 调度成本很难摊薄。

batch 增大后,多个序列一起生成,权重读取可以被更好地摊薄,总吞吐就会上去。

所以第二个关键判断是:

如果 batch 增大后总 tokens/s 明显提升:
更像权重读取 / 算力利用率 / 调度效率问题

如果 batch 增大后总 tokens/s 很快不涨,甚至下降:
更像 KV cache / 显存容量 / 显存带宽开始成为主瓶颈

五、第一个小实验:Context Scaling

第一个实验只改上下文长度。

保持这些条件不变:

  • 同一个模型
  • 同一个推理引擎
  • 同一个量化格式
  • 同一个输出长度
  • 同一个 batch size

只改变 prompt 长度。

例如:

1K prompt  -> 生成 256 tokens
8K prompt  -> 生成 256 tokens
32K prompt -> 生成 256 tokens

重点看:

decode tokens/s 是否随上下文长度明显下降

情况 1:速度基本不变

假设结果是:

1K prompt  -> 45 tok/s
8K prompt  -> 43 tok/s
32K prompt -> 40 tok/s

这说明上下文长度对 decode 速度影响不大。

此时更可能是:

  • 模型权重读取占主导
  • FFN / MLP 路径占主导
  • 小 batch 下 GPU 利用率不够
  • 推理框架调度开销占比明显

这时可以重点看:

  • MoE 是否能减少激活参数量
  • 权重量化是否能提升 decode
  • batch 增大后总吞吐能不能上去
  • 引擎 kernel 和调度是否足够好

情况 2:速度明显下降

假设结果是:

1K prompt  -> 45 tok/s
8K prompt  -> 28 tok/s
32K prompt -> 10 tok/s

这就很明显了。

上下文越长,decode tokens/s 掉得越厉害,说明每生成一个 token 需要读取的历史 K/V 已经开始主导性能。

这时更像:

KV cache / attention 读取 / 显存带宽瓶颈。

这时应该重点看:

  • 模型是否使用 MHA、GQA、MLA
  • KV cache 是否支持 FP8 / INT8
  • 推理框架是否支持 paged KV cache
  • 是否存在 sliding window attention
  • 长上下文是否真的需要完整放进 prompt
  • 是否可以用 RAG、摘要、memory 降低上下文压力

六、第二个小实验:Batch Scaling

第二个实验只改 batch 或并发。

保持这些条件不变:

  • 同一个模型
  • 同一个推理引擎
  • 同一个上下文长度
  • 同一个输出长度

只改变并发数。

例如:

batch 1
batch 4
batch 8
batch 16

重点看两个指标:

1. 单请求 decode tokens/s
2. 系统总 decode tokens/s

情况 1:总吞吐明显提升

比如:

batch 1  -> 总吞吐 45 tok/s
batch 4  -> 总吞吐 140 tok/s
batch 8  -> 总吞吐 230 tok/s

这说明 batch 增大后,GPU 被利用得更充分。

这种情况下,单请求慢,不一定是 KV cache 问题,也可能是:

  • batch 太小
  • 权重读取成本没有被摊薄
  • Tensor Core 没吃满
  • kernel launch / 调度开销占比高

如果这时上下文长度变化对速度影响不大,那就更偏:

权重读取 / FFN 计算 / 小 batch 利用率瓶颈。

MoE 在这种场景下往往更有机会体现优势,因为它减少了每个 token 激活的 FFN 专家权重和计算。

情况 2:总吞吐很快不涨

比如:

batch 1  -> 总吞吐 45 tok/s
batch 4  -> 总吞吐 80 tok/s
batch 8  -> 总吞吐 85 tok/s
batch 16 -> 总吞吐 70 tok/s

这说明并发上来以后,系统很快撞到了新的瓶颈。

如果此时显存占用快速上升,长上下文下更明显,那通常要怀疑:

  • KV cache 占用太大
  • KV cache 读取带宽压力太高
  • 显存容量开始接近边界
  • 调度系统管理 KV cache 的开销上升

这类问题不是单纯换 MoE 就一定能解决。

你更应该关注:

  • GQA / MLA 这类 KV cache 友好架构
  • KV cache 量化
  • sliding window attention
  • paged attention / paged KV cache
  • 更合理的上下文裁剪
  • 降低并发下的最大上下文长度

七、把两个实验合起来看

最推荐的判断方式是把两个实验组合起来。

组合 1:上下文不敏感,batch 很敏感

表现:

context 从 1K 到 32K,decode tokens/s 只小幅下降
batch 从 1 到 8,总吞吐明显提升

判断:

更像权重读取、FFN 计算、小 batch 利用率问题。

这种情况下,MoE、权重量化、更好的 batching、推理引擎优化,通常更值得优先尝试。

组合 2:上下文很敏感,batch 也很快撞墙

表现:

context 从 1K 到 32K,decode tokens/s 明显下降
batch 增大后,总吞吐很快不再提升

判断:

更像 KV cache / 显存带宽 / 显存容量问题。

这种情况下,GQA、MLA、KV cache 量化、sliding window、paged attention、上下文裁剪更关键。

组合 3:上下文敏感,但 batch 还能提升

表现:

长上下文会让单请求 decode 变慢
但 batch 增大后,总吞吐仍然能继续上升

判断:

混合瓶颈,但还没有完全被 KV cache 压死。

这种情况通常还有优化空间。

可以继续尝试:

  • 更好的推理框架
  • KV cache 量化
  • 合理增大 batch
  • 限制最大上下文
  • 提升显存带宽更高的 GPU

组合 4:上下文不敏感,batch 也不提升

表现:

context 变长影响不大
batch 增大总吞吐也不怎么提升

判断:

可能是其他瓶颈,例如框架调度、CPU 侧开销、采样逻辑、网络、同步点、实现不成熟。

这时候不要急着归因到模型架构,应该先排查服务链路和推理引擎实现。

八、和 Dense / MoE 选型有什么关系

现在回到 Dense 和 MoE 的比较。

如果你的测试发现:

decode 阶段更像权重读取 / FFN 计算瓶颈

那么 MoE 的优势会更明显。

因为 MoE 的核心是:

总参数可以很大,但每个 token 只激活一部分专家。

这会减少每个 token 实际参与的 FFN 权重读取和计算。

但如果你的测试发现:

decode 阶段更像 KV cache 瓶颈

那 MoE 不能直接解决核心问题。

因为 KV cache 的大小主要取决于:

层数
KV head 数量
head dim
上下文长度
数据类型
并发数

而不是总专家数量。

所以你会看到一个很重要的工程结论:

MoE 不是万能省钱架构。它主要省 FFN 路径,不是天然省 KV cache。

这也是为什么有些模型用 MoE,有些模型用 GQA,有些模型用 MLA,有些模型用 sliding window attention。

它们都在省成本,但省的是不同位置的成本。

九、一个可直接复用的最小压测清单

如果只想快速定位一个模型的 decode 瓶颈,建议这样跑:

第一组:上下文长度测试

1K prompt  + 256 output + batch 1
8K prompt  + 256 output + batch 1
32K prompt + 256 output + batch 1

看:

decode tokens/s 是否随 context length 明显下降

第二组:并发测试

4K prompt + 256 output + batch 1
4K prompt + 256 output + batch 4
4K prompt + 256 output + batch 8
4K prompt + 256 output + batch 16

看:

系统总 tokens/s 是否随 batch 增大明显提升

第三组:长上下文并发测试

32K prompt + 256 output + batch 1
32K prompt + 256 output + batch 4
32K prompt + 256 output + batch 8

看:

长上下文下并发是否快速压垮 decode

如果只看最终判断,最小版可以写成:

上下文长度敏感 -> KV cache 瓶颈
batch size 敏感 -> 权重读取 / 计算利用率瓶颈
两者都敏感 -> 混合瓶颈
两者都不敏感 -> 查框架和服务链路

十、如果有监控,应该看哪些指标

如果你能拿到 GPU 监控,可以进一步提高判断置信度。

优先看:

  • GPU compute utilization
  • memory bandwidth utilization
  • 显存占用
  • batch 增大后的总吞吐
  • context 增大后的单请求 decode tokens/s
  • KV cache pool 使用量
  • 请求排队时间

一个常见判断是:

GPU compute utilization 不高
memory bandwidth utilization 很高
decode tokens/s 随上下文明显下降

这通常更像 KV cache / 显存带宽瓶颈。

另一个常见判断是:

batch 1 时 GPU 利用率不高
batch 增大后总吞吐明显上升
context 增大对 decode 影响不大

这通常更像权重读取被 batch 摊薄,或者小 batch 下计算利用率不足。

不过要注意:

没有 profiler 时,不要把这些判断说成严格硬件结论。它们更适合做工程选型和快速排查。

十一、这套方法和 attention 优化有什么关系

理解了权重读取和 KV cache 的区别后,再看各种架构优化会清楚很多。

MoE 的重点是:

少激活 FFN 专家,减少每个 token 的专家权重读取和计算。

GQA 的重点是:

多个 Query head 共享更少的 KV head,减少 KV cache。

MLA 的重点是:

把 K/V 压缩成 latent 表示,减少 KV cache,同时尽量保留表达能力。

Sliding Window Attention 的重点是:

每个 token 只看局部窗口,减少长上下文 attention 的历史读取。

所以,不同优化不是谁绝对先进,而是它们处理的瓶颈不同。

如果你的瓶颈是 FFN 权重和计算,MoE 很香。

如果你的瓶颈是 KV cache,GQA、MLA、KV cache 量化、sliding window attention 更直接。

如果你的瓶颈是框架调度和并发管理,PagedAttention、RadixAttention、continuous batching 这类工程优化更关键。

十二、最后总结

如果你想快速判断 decode 阶段到底卡在读权重,还是读 KV cache,建议不要先猜架构,而是先做两个实验:

1. Context Scaling:拉长上下文
2. Batch Scaling:拉高并发 / batch

判断口径非常简单:

上下文越长,decode 越慢:
更像 KV cache 瓶颈

batch 越大,总吞吐越好:
更像权重读取 / 计算利用率瓶颈

上下文和 batch 都敏感:
更像混合瓶颈

这套方法不是 profiler 级硬件分析,但对模型选型、推理引擎选择、长上下文部署和 Dense vs MoE 判断已经非常实用。

现在越来越觉得,大模型部署选型不能只问:

这个模型 benchmark 高不高?

更应该问:

它的成本主要消耗在哪里:权重、计算、KV cache、显存容量,还是推理框架调度?

只要这个问题问对了,后面的模型选择、量化方式、上下文长度、并发策略和硬件配置,都会清晰很多。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐