📦 GitHub: https://github.com/geekchow/deepseek-v41-flash-inference-guide
背景:本文选自开源学习指南《DeepSeek V4.1 Flash 大规模推理部署实践》。全书用一个贯穿示例串联:编程 Agent 通过 Messages 协议发起第三轮请求,上下文 10 万 token、带一张截图、以
edit_file工具调用结束;文中的“第 N 步”即指该示例中的步骤,完整场景见 04 贯穿示例。
你在哪里:DeepInfer 子系统第 1 篇,贯穿示例的第 ④(截图编码)和第 ⑤(预填充)。
读完本文你将知道:prefill 实例的并行配置、两种 SWA Bounded Replay(编码器侧、解码器侧)的精确定义与收益、视觉编码如何接入,以及 prefill 的实测吞吐。
1. 角色回顾
- 负责:计算未命中后缀的 KV,重建缺失的 SWA KV,把新 KV 写回 3FS 并传给 decode。
- 配置:普通 8 卡机器;EP2–8 / DP2–8,在显存允许的前提下用最小的 EP/DP(减少通信);不用 TP / CP。
- 出现在示例:④⑤。
2. 内部设计
2.1 为什么 prefill 用小 EP
prefill 每步处理成千上万个 token,矩阵乘已经足够“胖”,算力是瓶颈,没必要靠扩大 EP 来增大批;反而 EP 越大,all-to-all 通信越多。所以“显存放得下就行”,EP 越小越好。
2.2 SWA Bounded Replay:核心定义
传统做法:如果某段前缀的 SWA KV 缺失,要精确重建它,就得把窗口内的 token 逐层重算——而每层的输入依赖上一层输出,窗口效应逐层叠加,精确重建需要回溯的长度随层数增长。
Bounded Replay 的做法:只重算最近 128 个 token,并把注意力窗口截断在重放段内。从位置s开始重放时,位置i的 query 只与[max(s, i − 127), i]范围内的 key 计算,得到一个近似的SWA KV。
下图回答:精确重建与有界重放分别要算多少。
结果:重算代价变为常数,与上下文长度和层数都无关。这是一个“用极小近似换大量算力”的工程取舍;V4.1 论文摘要把“持久化 KV 降到 V4-Flash 的约 1/8”直接归功于 SWA Bounded Replay。近似对输出质量的影响官方 Q&A 未给出数字,开源仓库提供了 mask / compact 两种截断实现的等价性校验(见 2.4)。
2.3 两处使用
| 位置 | 场景 | 做法 | 收益 |
|---|---|---|---|
| 编码器侧 | 前缀命中,但 SWA KV 缺失 | 命中长度只看 Block KV;把命中前缀的最后 128 token 与未命中后缀一起前向;重放段只重建 SWA KV,Block KV 原样复用,既不重算也不覆写 | SWA KV 可以只存单副本在本机内存,丢了也只多算 128 token |
| 解码器侧 | 每次 prefill | CED 结构中 prefill 只在编码器层计算,解码器层的 Block KV 由最后一个编码器层隐状态投影得到,只剩解码器 SWA KV要逐层算。精确重建需要最后 2,560 token 跑 20 个解码器层;改为只对 prompt 最后 128 token 在解码器层重算,结果只给 decode 用、不进前缀缓存 | prefill 总计算量接近减半 |
2.4 开源版里的开关
# V4.1 默认配置exportDEEPINFER_YOCO_PREFILL_TRUNCATION_MODE=compactexportDEEPINFER_YOCO_PREFILL_KEEP_TOKENS=140# 保留后缀需覆盖 SWA 窗口 128 + 最大回退长度- 140 而不是 128:保留后缀必须覆盖SWA 窗口 + 最大投机回退长度。(命中策略中的 134 = 128 + 6,同样考虑了回退。)
mask与compact两种截断模式输出等价,可用examples/yoco/yoco_equiv.py验证:L1 级只比对元数据(单卡、秒级、不加载权重);L2 级在 8 卡上用固定 teacher-forcing 轨迹逐元素比对 logprob 与 top-K。
2.5 视觉编码(VE)接入
- 部署上是 EPD 分离:VE 单卡一个实例,推荐便宜的卡;
- 引擎侧:CPU 预处理由
MultimodalProcessor工作进程异步完成,不在单步关键路径上;GPU 侧流程为 pad → ViT → downsample → aligner,产生的 embedding 填入 prompt 中<|image|>占位处; - 开源
benchmark.py --prompt_mode image时 ViT 与 LLM 在同一引擎内(没有做分离)。
2.6 实测吞吐
EP4DP4,384+32 个专家,上下文 131,072,开启解码器 Bounded Replay(140 token)。点标签为每 DP rank 的 prefill 批大小。
| 每 rank 批大小(token) | 单步时延 | 未命中输入吞吐(token/秒/卡) |
|---|---|---|
| 4,096 | ~70 ms | ~60,000 |
| 8,192 | ~115 ms | ~71,500 |
| 16,384 | ~200 ms | ~81,500 |
| 32,768 | ~375 ms | ~87,000 |
| 65,536 | ~730 ms | ~89,500 |
32,768 之后吞吐几乎不再增长、时延翻倍——这正是 Prefill LB 把单卡 chunk 上限定为32,768的原因。
3. 交互契约
| 对端 | 输入 | 输出 |
|---|---|---|
| Prefill LB | 请求 + 命中长度 + 本步时间预算 | — |
| KVCache Storage | 命中块(GDR 逐层换入)、SWA 快照(本地或 400G 拉回) | 新的完整块写回 3FS |
| VE Service | 图像 embedding | — |
| Decode Service | — | Block KV + SWA KV(同一条 P→D 路径) |
4. ⚓ 回到示例
第 ④ 步:截图
prefill 引擎在 prompt 中看到<|image|>占位,经 VE LB 交给一张 VE 卡;该卡做 pad → ViT → downsample → aligner,返回对应数量的图像 token embedding,填入占位位置。这些图像 token 也计入L = 100,000。
第 ⑤ 步:预填充
假设命中正常(96,896),这一步内部依次发生:
- 换入:96,896 token 的 Block KV 从共享内存经 GDR 逐层进入 HBM,与下面的计算重叠;SWA 快照(1.35 MiB)从 D-17 拉回。
- 编码器层前向:只对 3,104 个未命中 token 计算,注意力能看到全部 100,000 token 的 Block KV。MoE 部分 4 卡 EP,由 EPLB 均衡。
- 解码器层:不对 3,104 个 token 全部算 SWA,而是只对 prompt最后 140 个 token(
KEEP_TOKENS=140)在解码器层重放,窗口截断在段内,得到 decode 所需的近似解码器 SWA KV——它不进前缀缓存。 - 落盘:新的完整块写入 3FS:
⌊100,000 / 128⌋ = 781块,第 758–781 块(24 块,覆盖到 99,968)为新增。 - SWA 存储位置:prefill 结束后存储位置 =
⌊(100,000 − 1) / 128⌋ × 128 = 99,968,写入本机内存并登记到 Entrypoint 目录。 - 交给 decode:Block KV + SWA KV 经 P→D 路径发送。
粗略耗时(以 87,000 token/秒/卡的满载吞吐估算):3,104 token 本身只需几十毫秒量级,实际 TTFT 主要由排队、换入和 P→D 传输决定。(媒体转述的线上平均首 token 约 0.4 秒。)
变体(D-17 宕机):命中回退到 96,768,第 2 步变为“最后 128 个命中 token + 3,104 个未命中 token”一起前向;那 128 个 token 只重建编码器侧 SWA KV,Block KV 不重算不覆写。
5. 故障行为
| 情况 | 行为 |
|---|---|
| SWA KV 缺失 | 编码器侧 Bounded Replay,多算 128 token |
| 显存不足 | 调大 EP(2 → 4 → 8),这是“最小 EP”原则的边界 |
| prefill 实例故障 | 请求级重路由到其他 PE(Prefill Engine);靠 PE 配额冗余而非缩短单实例重启时间(重启为分钟级) |
| 截图过多 / 过大 | VE 按 patch 数均衡;VE 吞吐可用examples/sweep/ve_sweep.py单独测 |
上一篇:05.4 DeepInfer 总览 | 下一篇:05.4.2 Decode | 返回:概念地图
📚 返回专栏目录