10 月 16 日,deepseek-ai/DeepInfer 建仓,四天后 7.2k star。README 第一句是"the high-performance inference framework that powers DeepSeek's large-scale production services."

分量在于它不是在开源做生态,而是线上正在跑的那套东西,只剥掉了 KV cache 系统、E/P/D 分离部署和跨副本负载均衡没开源。下载到的代码,是把生产集群的骨架摆在了桌上。

它处理了一个通用框架很难处理好的问题:带稀疏注意力和多 token 预测的 MoE 模型,怎么在 64 卡以上的专家并行下跑出接近线性的吞吐。通用框架要照顾几百个模型,很多决策只能做成可插拔;DeepInfer 只服务一个模型家族,所有决策都能往极致推。

下面按四个最有信息量的点走:专家负载均衡、SWA 有界重放、DSpark 变长验证、异步调度。材料来自仓库里的 doc/inference_deployment_qa.md 和源码。

一、Wide-EP:解码用 64 到 128 路专家并行

部署形态是 E/P/D 三段分离,视觉编码器、prefill、decode 各自独立扩容,这个思路从 V3/R1 沿用下来。有意思的是 prefill 和 decode 用了完全相反的并行策略:

Prefill: EP2–8 / DP2–8        # 用 HBM 允许的最小 EP/DP
Decode:  EP64–128 / DP64–128  # Wide-EP,用 GPU 允许的最大 EP/DP

prefill 是计算密集的,大 batch 喂满矩阵乘法就够了,EP 开太大反而增加通信开销;decode 是访存密集的,每步只出一个 token,瓶颈是每个 step 要搬多少 MoE 专家权重。EP 开到 64,每张卡只放 7 个左右的专家,单步权重搬运量降到 1/8。

代价很实在:EP64 意味着一个实例的 64 张卡构成单一通信域,挂一张整实例不可用。恢复策略是重启时"从所有当前健康的 GPU 重新确定 EP size",故障后你不是修好 64 卡再启动,而是拿剩下的卡降级接着跑。

最小有效部署单元是 64 卡 prefill + 64 卡 decode(P:D 约 1:1)。这个数字基本把个人开发者挡在门外,它是给有集群的人看的代码。

二、EPLB:用贪心把热点专家复制出去

MoE 训练的负载均衡靠 auxiliary loss,推理时没有 loss 可用,只能靠统计加搬运。EPLB 策略在 eplb/policy.py,只做两件事:决定给哪些 logical expert 增加冗余副本,再把所有副本均衡铺到各 rank 上。

阶段 冗余专家起点 统计窗口 重排周期 实测 moe_lb
Prefill 每卡 4 个 100 步 1000 步 1.074 (EP4DP4) / 1.045 (EP2DP2)
Decode 每卡 1 个 1000 步 10000 步 1.090

moe_lb 是"最慢 rank 负载 / 平均负载",1.09 意味着最慢的卡只比平均多干 9% 的活。考虑到专家分布本身是长尾的,这个数字干净得有点意外。

第一段是选谁复制。代码先用 tokens_per_expert + arange_logical * 1e-4 给每个专家加一个极小的偏移,再算每副本负载 score = tokens / logical_count。

1e-4 的偏移是为了让各专家的 score 严格互不相等。这不是洁癖,后面 score.max(-1) 取的是峰值位置,出现并列时 argmax 会在不同的层上返回不同结果,冗余分配就失去确定性。0.0001 的扰动换来的是一套可复现的调度。

评分逻辑值得看清。score 是每副本负载,score_added 是加一个副本之后的每副本负载。代码先取当前最大值,把最大值位置替换成 score_added,再重新取 max,按 step 求和取 argmin。它回答的是"复制哪个专家,能让所有 step 的最坏情况降得最多",这是 minimax 目标,不是简单复制最热的那个。

第二段是摆位置,在 _batch_greedy_indices 里。对固定的 layer,目标是最小化 sum over step i of max over rank j of (rank j 在 step i 的负载),也就是让历史上每一个 batch 里最重的那个 rank 尽量轻。贪心做法是逐个专家放到"当前累计峰值增加最少"的 rank 上,用 occupancy 保证每 rank 恰好放 capacity 个。这比按总量均分严格:总量均分只保证平均负载接近,控制不住单步毛刺,而 decode 的 TTFB 恰恰被毛刺决定。

权重怎么搬:文档说"在组内 over NVLink symm_mem 做 shuffle",用对称内存直接改指针,不做 host 侧拷贝。10000 步一次的 decode 重排要是真搬权重,那点收益会被搬运开销吃干净。

三、YoCo 有界重放:把重算代价压成常数

这是全文我认为最漂亮的一处设计。

V4.1 用了混合注意力结构:前 20 层是滑窗注意力(SWA),后面是压缩过的表示。SWA 的 KV 不用全存,只要保住窗口内那一段。但一旦 prefix cache 只命中一部分,缺掉的 SWA KV 怎么补?直觉做法是重算整个前缀,代价随上下文线性增长。DeepInfer 的答案是只重算最近 128 个 token:

replaying from position s, the query at position i is computed only against keys in the range max(s, i−127) to i

位置 i 的 query 只和 [max(s, i−127), i] 内的 key 算注意力。它算的是 SWA,本来就只看 128 个 token,所以截断到重放段之内不损失窗口内的任何信息,窗口外的部分本来就会被丢弃。重算代价于是变成常数,和上下文长度、层数都无关。

机制用在两处,第二个更狠。编码器层缺 SWA KV 时重算命中前缀的最后 128 个 token,block KV 照用不误,不重算也不覆盖。解码器层利用 CED 的结构特性:decoder 层的 block KV 是从最后一个 encoder 层的 hidden state 投影出来的,所以只有 decoder 的 SWA KV 要逐层算。文档算了账:精确重建需要把最后 2560 个 token 跑过 20 个 decoder 层;只重放 128 个,且结果只用于 decode、不进 prefix cache,于是"nearly halving total prefill computation"。

命中策略写成了能直接对照实现的分支:先查 block KV 得到 L1(128 的倍数),再查 [L1−128, L1] 有没有 SWA KV;有就是 L1,没有就退一格到 L1−128。上限还有 L − 134 的帽子,134 = 128 窗口 + 6 个 token 的投机回溯余量。

那个 6 不是拍脑袋。SWA 存储位置只认 128 的倍数,decode 时跨越 128 边界就把当前窗口拷到 device 的副本区,请求结束时从最近一份完整拷贝落盘。拷贝位置最多越过边界 5 个 token,靠 slot 环形缓冲多出的 6 个位置吸收。128 存、134 用,差的 6 个是投机解码的回溯余量,两个子系统在这里咬合住了。

仓库还配了等价性验证脚本 examples/yoco/yoco_equiv.py,分两级:L1 比对 SWA pivot、可见 key、store slot 和压缩 key 区间,单卡几秒跑完不加载权重;L2 用 8 卡加固定 teacher-forcing 轨迹,逐元素比对 logprob 和 top-K。运行时校验 n_prefill_layer < n_layer,不满足直接报错。把"这段优化在数学上等价"做成脚本而不是一句注释,是我最欣赏的地方。

四、DSpark:不修验证长度,按置信度分配算力

投机解码的常规做法是固定草稿长度,比如一次猜 5 个 token 全送进去验证。问题是草稿 token 的接受率随请求和位置剧烈波动,固定长度会把验证算力浪费在大概率被拒的 token 上。

DSpark 加了一个 Confidence Head 预测每个草稿 token 的接受概率。models/mtp/dflash.py 里的实现很小:

class ConfidenceHead(nn.Module):
    """Predict the acceptance probability of each draft token."""

    def __init__(self, n_embed, markov_rank, num_steps, weight):
        self.proj = ReplicatedLinear(
            input_size=n_embed + markov_rank,
            output_size=1,
            weight=weight,
        )
        temperature = envs.DEEPINFER_DFLASH_CONFIDENCE_HEAD_TEMPERATURE
        temperatures = temperature if isinstance(temperature, tuple) else (temperature,) * num_steps
        self.temperature = tuple(max(value, SAMPLING_EPS) for value in temperatures)

输入是 hidden state 加 markov embedding,输出一个标量。temperature 支持按 step 给不同值,每个草稿位置可以有自己的校准温度,因为第 1 个草稿 token 和第 5 个的接受率分布完全不同,共用一个温度会系统性高估末端位置。

调度侧:草稿模型一次出 5 个,按预测接受率排序,结合实测的硬件吞吐曲线,选"有效吞吐最大"的前缀子集送验证。效果数字给得诚实:16 seqs/rank 时验证长度 4.44、接受长度 3.24,吞吐反而掉 1.5%;32 时 3.65 / 3.17,+1.6%;60 时 3.58 / 3.21,+5.4%;96 时 3.24 / 3.15,+11.0%。

低并发时它是负收益。 16 seqs/rank 掉 1.5%:并发低的时候 GPU 本来就没吃饱,省下的验证算力换不成吞吐,反而多付了置信度预测的开销。收益区间在 48 以上,生产工作点约 60 seqs/rank 时 +5.4%,96 时到 +11%。

每步验证的草稿数从 5.00 降到 3.2–4.4,少验证约 30%,平均接受长度几乎不变(3.29 对 3.20)。省的是无效验证,不是砍掉有用的推断。只看标题"变长验证提升吞吐"会以为这是无条件优化,那一格 −1.5% 才是真正有用的信息。

五、异步调度:把 GPU 的空转填掉

decode 每步是 schedule(N) → forward(N) → sample(N) → post-process(N)。schedule 和后处理都是纯 CPU 逻辑,而 post-process(N) 之前需要一次 GPU-CPU 同步拿 sample 结果,这段时间 GPU 空转。

做法是流水化:GPU 跑 forward(N) → sample(N) 的同时,CPU 并行跑 schedule(N+1) → launch_forward(N+1) → post-process(N−1) → launch_sample(N+1)。文档坦白说是"SGLang、NanoFlow 等已有实现"的思路,没宣称原创。

但和变长验证撞在一起时出现了新问题:schedule(N+1) 执行时第 N 步的置信度还没回来,CPU 不知道每个请求该验证几个 token。解法是把这步挪到 GPU 上,用 TileLang kernel 做累积乘积、全局排序取 top-k,再加一个融合的 remap scatter,结果写回 pinned buffer。CPU 只固定总预算,具体长度由 GPU 现算。

这说明异步调度和变长验证不是两个独立优化,前者会把后者的输入变成"未来数据",必须重新安排计算位置才能共存。这类耦合关系在框架文档里通常不会写。

六、几条可以直接抄的工程结论

存储不能跟着计算节点扩缩容。 block KV cache 没走本地内存,而是放在独立的 3FS 集群上。理由文档写得很直白:分层存储会让"前缀亲和性"和"负载均衡路由"打架,你想命中缓存就得把请求发给持有它的机器,那样就没法按负载均衡派活。放到独立存储集群,prefill 机器可以命中任何前缀。顺带一条:计算节点的故障率比存储节点高一个数量级以上,这么做 KV 也不会跟着计算节点一起没。

3FS 用 10+2 纠删码,一个 EC 组容忍两个节点同时故障,读时向 11 个节点并发请求,任意 10 个响应就能解码返回。容量换算:在线 GPU 每卡约 14.4 TB 逻辑大小。TTL 按流量类型分,agent 3 天、chatbot 7 天,因为 chatbot 的命中长尾重得多。实测 3 天 TTL 会让 chatbot 的 KV 命中率掉 0.5%–1%。

低精度按张量分别定。 主 KV 用 per-16 的 e4m3(像 NVFP4 但没有全局 scale),indexer KV 和 MoE 路由专家权重用 per-32 ue8m0 的 MXFP4。不是一刀切 FP8,也不是一刀切 FP4。

swap-in 别用 CUDA copy engine。 KV block 拷贝粒度太细,copy engine 在这种小拷贝上效率很差。DeepInfer 改用 GDR:RDMA 网卡把数据灌进 PCIe,不占 SM,还能和逐层 H2D 传输、前向计算重叠。很多人第一反应是 cudaMemcpyAsync,然后发现带宽只跑出一半。

一个开发者的视角

以前只能从论文和社区文章里猜 DeepSeek 的推理系统长什么样,现在能直接看到 EPLB 的贪心怎么打分、置信度头怎么接、SWA 的 128/134 怎么咬合。

但要说清楚它适合谁。硬件门槛是 NVIDIA SM100(或昇腾 950 NPU),依赖 CUDA 13、torch 2.13.0+cu132、GCC 13+、Python 3.12,build.sh 里还要把 NCCL 强刷到 2.32.3。最小有效部署是 128 张卡。个人开发者下载下来跑不起来文档里那些 benchmark。

更实际的用法是当设计参考读,尤其是三处。一是 eplb/policy.py,那 140 行是完整自洽、可直接迁移的 min-max 放置算法,换成任何 MoE 推理或训练场景都能用,跟 DeepSeek 的模型无关。二是 examples/yoco/yoco_equiv.py 的两级验证结构,任何"用一个近似换性能"的改动都能照这个形状写验证:先验元数据,再验端到端输出。三是 Q&A 文档里的数字,moe_lb 1.074、16 并发下变长验证的 −1.5%、TTL 3 天换 0.5%–1% 命中率,这些是真实的 trade-off 坐标,自己调参时至少知道合理区间长什么样。

至于要不要把服务迁过去,除非你正好在 SM100 上跑 V4.1-Flash 并且有 64 卡以上的 decode 集群,否则答案是不用。它连 KV cache 系统和 E/P/D 分离部署都没开源,本来也不是按开箱即用设计的。

代码在 github.com/deepseek-ai/DeepInfer,Apache-2.0。