对照文章DeepSeek V4.1 Flash 大规模推理部署实践 和开源代码仓库DeepInfer,仓库里还有一部分实现没有放出,但结合原文也能看出大致轮廓。

负载均衡

Attention 侧的数据并行(DP)和 MoE 侧的专家并行(EP),都需要关注组内负载是否均衡。各 rank 的工作量差异越大,同步时等待最慢 rank 的时间就越长,GPU 利用率也越低。

Prefill

Prefill阶段通常以计算开销为主,那么先对一个请求需要的计算量建模,然后再均衡每个计算节点的计算量。可以用下面的示意式理解计算配额,它并非开源代码中的精确公式: compute_quota = alpha * (total_token - hit_token + theta) ** 2 + beta * hit_token, 其中total_tokens 是输入总长度,hit_tokens 是缓存命中的长度, alpha beta theta 为拟合参数。

在此基础上,按输入总长分桶为 4 档,将计算量量级差异显著的请求隔离到不同引擎组中各自调度。每个桶分配到的引擎数量,根据该桶上一轮的 compute-quota 需求占比动态调整。分桶将计算规模差异较大的请求放进不同引擎组,避免长短请求在同一引擎池中互相阻塞——短请求被长请求堵死在队列里、长请求又因引擎被短请求占满而排不上队。

即使请求分配时已经尽量均衡计算配额,但是由于每张卡上运行的请求不完全一样,各 rank 一步的实际耗时仍不同,导致DP rank同步还是会有一点时间是空转等待,为了让每个卡都能同时算完Attention,每个step都以固定的时间预算(纳秒级别)反推本轮需要的chunk size。

Decode

Decode 阶段的负载与并发请求数、KV Cache 总长度都有关。仅按请求数均衡,可能把大量长上下文请求集中到同一个实例;仅按 KV 长度均衡,又可能造成 batch size 差异过大。同时均衡请求数和 KV Cache 总长度两个维度,可以写成 score = alpha * kv_usage + beta * batch_size,按score做均衡

EPLB

MoE内使用EPLB(Expert-Parallel Load Balancer),就是在常驻专家之外多备若干冗余专家,用来放热门专家的副本。通过统计最近 N 步各专家的负载,每 M 步根据统计结果重新分配专家到卡实现动态调整,这个在vLLM 和SGLang 内也都有实现,但是一些超参可能不是很好调试,这次DeepSeek直接告诉我们了:Prefill 的策略:每卡 4 个冗余专家起步(向上填充至总专家数为 ep_size 的倍数);统计窗 100 步,每 1000 步重排;Decode 的策略:每卡 1 个冗余专家起步(向上填充至总专家数为 ep_size 的倍数),例如 EP64 取整后每卡共 7 个专家(不包括共享专家);统计窗 1000 步,每 10000 步重排一次。DeepInfer 中已经包含 EPLB 的负载统计、重排和专家权重迁移实现。

Vision Encoder

多模态请求还需要单独均衡视觉编码负载,路由策略是,每台机器作为一个组,Prefill 随机选一个组,然后由组内leader选择patch数目最少的卡处理图片embedding。(图片 patch 数目由宽高直接算出,(width/patch_size) × (height/patch_size))相比只统计图片数量,patch 数更能反映不同尺寸图片的计算开销。

自动扩缩容

Prefill 和 Decode 的资源需求会随输入长度、输出长度和缓存命中率变化。两阶段中任意一侧成为瓶颈,都会限制整个服务的吞吐,因此需要根据负载动态调整 P/D 资源比例。

文章中,Prefill 根据未命中的 token 数估算所需实例数,Decode 根据正在处理的请求数估算所需实例数,并为调整设置速率限制和上下界,避免频繁震荡。

在大规模EP部署中,一个decode实例上可能承载大量请求(1000+),在缩容时需要妥善处理好,除了提高宽限期等待未完成生成的请求结束,针对输出特别长的请求也不能一直等下去,DeepSeek 还做了运行中迁移,宽限期结束后仍未完成的请求及其 KV Cache 一次性全部转发给其他实例继续 decode 计算。

为了应对流量形态突变(比如Agent整点定时任务,在同一时刻大家的agent都开始发请求),留一部分冗余实例,包括两方面:一是池子里留有尚未被占用的 ready 机器作为扩容库存;二是始终保留比测算需求更多的实例在线,具体来说,配额占用率高于 0.8 时禁止缩容。

在大规模EP的部署下,一个 P/D 实例的负载容量粒度很大,一个卡的故障会导致整个实例的请求都卡住,而恢复重启即使经过优化,恢复仍可能需要分钟级时间。容灾主要依赖冗余实例和重新路由的机制。当发现一个实例故障,马上把上面的请求KV Cache 转发到其他健康的实例,尽可能让用户无感,而不是stream直接断开。

KV Cache 存储

Block KV Cache 存储在3FS上,由 decode 实例落盘存储,因此缓存既覆盖输入前缀,也覆盖生成结果。任意Prefill都能直接加载这份共享缓存,故不需要做cache 亲和,相比与使用CPU内存的Offloading在Prefill负载均衡策略上更简单一点,容灾和自动扩缩容也不受影响。SWA KV Cache 存在机器内存里,然后有一个逻辑中心记录存放位置,如果本机没有则去另一台机器拉,它同样是一种池化存储,但容量更有限,保留时间也更短。

在处理Cache命中时,两类 KV 需要分别检查。命中长度由 Block KV Cache 决定,若 SWA KV Cache 未命中,就回退一块重算,原文中也讨论了SWA Bounded Replay如何粗略计算,以及V4 部署中全量存储SWA 遇到的问题。

匹配到cache后的KV Cache 加载全是异步的,逐层H2D传输与计算重叠,使用 GDR 实现,用 RDMA 网卡就能打满 PCIe 带宽,并且不占 SM。

这套共享缓存设计解释了跨实例复用的方式,但文章没有展开普通 P→D 之间的完整传输协议,不能据此断言 P 计算完的 KV 一定经由 3FS 交给 D。

DSpark投机解码

具体可参考DSpark论文,在开源代码里实现了DSpark的变长验证,把验证算力分配给边际收益最高的 token,在定长验证的基础上更进一步提高了Decode实例的利用率。同时调整了 Asynchronous Scheduling,CPU 调度时只确定总预算,逐请求长度在 GPU 上由一个 TileLang kernel 重算(累乘、全局排序取 top-k,并融合 remap scatter),结果回写 pinned buffer,尽可能一个step都在GPU上算完再返回给CPU,减少CPU-GPU之间频繁流转的调度延迟。

DeepSeek Recipe & 结构化输出

本次还开源了deepseek-recipe负责推理引擎前后的协议转换、prompt 构造、分词和流式输出解析,将这些工作从模型计算中分离出来。

输入阶段在tokenizer encode时区分“控制标记”和“用户写出的同名文字”,即在encode时用的词表中清除special token,这样可以防止用户通过同名文字注入结构控制 token,同时支持对长 prompt 的文本片段并行分词。

输出阶段,它通过状态机处理跨 chunk 的结构标记。例如,工具调用的标签或参数可能被拆到多个 chunk 中,解析器会保留尚未匹配完成的片段,等待后续输入;识别出工具名和参数后,再将模型输出的 DSML 格式转换成接口所需的 tool-call 增量。

结构化输出(Structured Output)是通过语法约束模型的输出,比如tool call,json schema。是通过把schema 约束编译成语法自动机实现的。我们在实际生产环境中也注意到部分请求的tool call数量多且格式复杂,导致编译耗时长(大部分耗时在50ms左右,少数请求编译耗时10s以上)导致TTFT(Time to first token)极高。这里DeepInfer的方案是编译使用独立的机器异步编译,随请求一起发给推理引擎。