文章目录
- 1、前言
- [2、视频:DeepInfer 推理框架源码精讲(专业版,2 小时 20 分)](#2、视频:DeepInfer 推理框架源码精讲(专业版,2 小时 20 分))
- 3、全片地图:一个请求的旅程
- [4、文字版:12 章速读](#4、文字版:12 章速读)
-
- [4.1 推理的物理学(0:02:42)](#4.1 推理的物理学(0:02:42))
- [4.2 模型解剖:V4.1 Flash(0:21:04)](#4.2 模型解剖:V4.1 Flash(0:21:04))
- [4.3 一个请求的一生(0:43:23)](#4.3 一个请求的一生(0:43:23))
- [4.4 调度器(0:54:51)](#4.4 调度器(0:54:51))
- [4.5 显存(1:05:00)](#4.5 显存(1:05:00))
- [4.6 并行与专家负载均衡(1:13:46)](#4.6 并行与专家负载均衡(1:13:46))
- [4.7 算子与低精度(1:26:30)](#4.7 算子与低精度(1:26:30))
- [4.8 采样与投机解码(1:35:44)](#4.8 采样与投机解码(1:35:44))
- [4.9 长上下文的省钱术(1:43:44)](#4.9 长上下文的省钱术(1:43:44))
- [4.10 确定性、结构化输出与 Recipe(1:51:47)](#4.10 确定性、结构化输出与 Recipe(1:51:47))
- [4.11 没开源的那一半(2:00:07)](#4.11 没开源的那一半(2:00:07))
- [4.12 上手、补齐与对比(2:09:45)](#4.12 上手、补齐与对比(2:09:45))
- 5、总结
🍃作者介绍:AI 应用负责人/AI产品架构师,阿里云专家博主。专注 LLM 应用开发、Agent 系统设计、具身智能与工业 AI 落地。日常在大模型训练、Coding Agent 工具链、AI 产品商业化等方向持续输出实战内容。
🦅个人主页:@逐梦苍穹
🐼GitHub主页:https://github.com/XZL-CODE
✈ 您的一键三连,是我创作的最大动力🌹
1、前言
上一篇入门版讲了推理框架是什么、DeepSeek 线上怎么部署。
推理框架到底在做什么?从 Ollama 到 DeepSeek 开源的 DeepInfer(46 分钟视频+文字版)
这一篇进入源码:DeepSeek 开源的推理框架 DeepInfer,一部 2 小时 20 分的专业版讲解视频,加一份文字版。
先交代对象。DeepInfer 只服务一个模型,DeepSeek V4.1 Flash:一共 7632 亿参数、约 510 GB,每个 token 只用到约 161 亿;40 层,每层 384 个路由专家里选 6 个;每个 token 的 KV 压到 890 字节。硬件只支持 NVIDIA 的 SM100(B200 这一代)和华为昇腾 950。开源的是推理引擎、模型代码和算子,另外单独开源了协议编解码的 deepseek-recipe;没开源的有三块:KV 缓存系统、视觉编码 / Prefill / Decode 的分离部署、副本之间的负载均衡,这几块的设计写在官方的推理部署问答里。
几条讲法上的约定:
- 代码逐行取自固定提交 :DeepInfer
9bd9cf2、deepseek-recipe935817c。视频画面右上角是文件路径和行号,可以直接对照。 - 构造的例子标明"构造"或"示意",按代码里的规则算,可以自己再算一遍;没标的是代码里或官方给的真实数字。
- 从代码推出来、但代码里没写明的结论,会说"从用途看"或"推导";代码和注释不一致时,以代码为准,并指出来。
适合熟悉 Transformer 和 API 调用、但还没拆过推理系统内部的工程师。怎么看:赶时间,先看第 3 节的全片地图,再读第 4 节的 12 章速读;想看源码和推导,看视频,视频里带章节标记,可以按章、按节跳。
2、视频:DeepInfer 推理框架源码精讲(专业版,2 小时 20 分)
【2小时20分源码精讲】DeepSeek 开源推理框架 DeepInfer:调度器、显存、专家并行、投机解码逐行读
| 章 | 内容 | 时间 |
|---|---|---|
| 00 | 序章:一个模型、两种硬件、一半开源 | 0:00:00 |
| 01 | 推理的物理学 | 0:02:42 |
| 02 | 模型解剖:V4.1 Flash | 0:21:04 |
| 03 | 一个请求的一生 | 0:43:23 |
| 04 | 调度器 | 0:54:51 |
| 05 | 显存 | 1:05:00 |
| 06 | 并行与专家负载均衡 | 1:13:46 |
| 07 | 算子与低精度 | 1:26:30 |
| 08 | 采样与投机解码 | 1:35:44 |
| 09 | 长上下文的省钱术 | 1:43:44 |
| 10 | 确定性、结构化输出与 Recipe | 1:51:47 |
| 11 | 没开源的那一半 | 2:00:07 |
| 12 | 上手、补齐与对比 | 2:09:45 |
| 13 | 结尾:一个请求,从进到出 | 2:16:52 |
1080p 原片(302 MB)可以在 GitHub 下载:deepinfer-videos。
3、全片地图:一个请求的旅程
把一个请求从进到出走一遍,每一段对应一章:

- 进来:Recipe 把协议转成对话格式,模板里的特殊 token 记下位置,普通文本分段并行编码(第 10 章)。C++ 调度器决定这一步全做 Prefill 还是全做 Decode,按 token 预算装批(第 3、4 章)。显存里,请求分到 128 个 token 一块的 KV 块,再分到一个状态槽,放滑窗环和压缩器状态(第 5 章)。
- 前向:40 层。前 2 层只有滑动窗口,第 2 到 19 层两个 token 压成一条,第 20 到 39 层共用第 20 层的 KV(第 2 章)。注意力在本卡算;MoE 在 384 个专家里选 6 个,token 发到专家所在的卡,算完送回,副本由 EPLB 按负载重排(第 6 章)。权重、KV 和激活用 FP4 / FP8(第 7 章)。
- 出去:融合采样,随机数按种子和位置生成;DSpark 一次前向出 5 个草稿,下一步主模型一起验证,接受了 a 个就输出 a+1 个 token(第 8、10 章)。输出回到 Recipe,攒齐 UTF-8,解析成正文、思考和工具调用,流式返回。
- 线上:这条路被拆在视觉编码、Prefill、Decode 三类机器上;块 KV 存在 3FS 集群里,滑窗 KV 缺了就有界重放,后 20 层在 Prefill 里只算末尾一小段(第 9、11 章)。
4、文字版:12 章速读
每章三部分:这一章回答什么、要点、记住这三句话。
4.1 推理的物理学(0:02:42)
这一章回答:生成一个 token 时,GPU 的时间花在计算上,还是花在读数据上?为什么一次多算一些请求,耗时几乎不变,哪一部分省不掉?官方那几张吞吐和速度的曲线,每个点是什么意思?

要点
- 一次矩阵乘法的账 :拿 V4.1 每层注意力的第一步投影来算,5120 维变成 1792 维(1280 维给 query、512 维存进 KV cache),FP8 权重约 9.2 MB。计算量是 2mkn,m 是这一步处理的 token 数,m=1 时约 1835 万次;读写量几乎全是权重。两者相除是算术强度:m=1 时约 2,m=64 时约 120。
- B200 的拐点是 584:FP8 稠密算力每秒 4.5 千万亿次(4.5 PFLOPS),显存带宽每秒 7.7 TB,一除约 584。强度低于 584,耗时由读数据决定(带宽受限)。m=1 只用到算力上限的 0.3%;m=64 时耗时只多 6%,计算却是 64 倍;m 到 400 左右才到拐点。
- Prefill 和 Decode 的 m 完全不同:Prefill 的 m 是这一步的 token 数,官方测试每卡每步 4096 到 65536 个,强度远在拐点右边;Decode 的 m 是同时生成的请求数,只服务一个请求时强度约 2。所以 Decode 要拼 batch;投机解码也在提高 m:默认最多猜 5 个,加上本来的 1 个,每个请求一步算 6 个 token。
- MoE 把 batch 打散了:384 个专家选 6 个,batch 64 时每个专家平均只分到 1 个 token;一个专家约 18.8 MB,384 个乘 40 层约 289 GB,一张 B200 只有 180 GB。专家并行把专家分到多张卡:64 卡、每卡 64 个请求,每个专家平均分到 64 个 token,每卡放 7 个专家,每步最多读约 5.3 GB 专家权重;换成 8 卡,每卡 52 个,约 39 GB。
- 注意力的账,拼 batch 省不掉 :每个请求的 KV 各读各的。对照组(40 层、每层一个 512 维 BF16 向量、不压缩)每个 token 40960 字节;V4.1 做了三件事:跨层共享(主 KV 只由第 2、8、14、20 层存)、第 2 到 19 层两个 token 合成一条、FP4,再加索引器用的 key,每个 token 只存 890 字节,是对照的四十六分之一。每个 query 每层最多读 640 条(滑窗 128 + 索引器挑的 512),和上下文多长无关。
- 读官方曲线:每卡吞吐 = SPS × τ × 每卡请求数,τ = 1 + 草稿长度 × 接受率(官方图里是 1 + 3 × 0.75 = 3.25)。64 卡 Decode:每卡 8 个请求时单用户每秒约 325 个 token、每卡约 2600;64 个请求时约 246、约 15750;256 个时约 103、约 26300。请求数涨 32 倍,每步只慢 3.2 倍。同样每卡 64 个请求,64 卡比 8 卡的每卡吞吐高约 25%。
记住这三句话
- 判断快慢看算术强度:每读写 1 字节做多少次运算。B200 的拐点约 584,Decode 只服务一个请求时,强度只有 2 左右。
- 提高 m 是关键:拼 batch、专家并行、投机解码,都是让同一份权重一次服务更多 token;但每个请求的 KV 各读各的,所以 V4.1 把每个 token 的 KV 压到 890 字节。
- 吞吐和单用户速度此消彼长:每卡吞吐等于 SPS 乘 τ 再乘每卡请求数,曲线上每个点是一种 batch 大小。
4.2 模型解剖:V4.1 Flash(0:21:04)
这一章回答:DeepInfer 服务的这个模型有多大,7632 亿个参数都放在哪?一层里有哪些结构,KV 是怎么被压到每个 token 890 字节的?这些结构分别给推理系统提了什么要求?

要点
- 参数账:按代码里每个权重的形状逐项加起来,约 7632 亿参数、510 GB,和 Hugging Face 页面一致。大头只有两项:路由专家约 289 GB(一个专家 3 个矩阵、3539 万参数,FP4 加每 32 个 1 字节缩放,约 18.8 MB),Engram 查找表约 203 GB(第 1、14 层各一张,FP8,在 NVIDIA 上默认放 CPU 内存)。每个 token 只用到约 161 亿,占 2.1%。
- 注意力:64 个 query 头,1 个 KV 头:query 先降到 1280 维再升到 64 个头 × 512 维;KV 只有一个 512 维的头,key 和 value 是同一个向量(MQA)。Decode 读进来的每一条 KV 被 64 个头各用一次,算术强度高了 64 倍。
- 40 层,三种注意力(config 里的 compress_ratios):第 0、1 层只有滑动窗口;第 2 到 19 层两个 token 压成一条(按通道做 softmax 加权求和);第 20 到 39 层不合并。每层都有最近 128 个 token 的滑窗 KV,MXFP8,每条 528 字节,放在每个请求一个的环形缓冲里,134 格(128 + 6 格给投机解码回退)。
- 索引器挑 512 条:32 个头的索引 query,分数是 Σ 权重 × relu(q·k),用 DeepSelect 选最高的 512 条。第 20 层先把每 8 条相邻分数取最大、选 2048 个候选块(16384 条),第 24、28、32、36 层只在候选里打分,和上下文多长无关。
- 跨层共享与 YOCO:每一层指向不晚于它的最近源层,主 KV 的源层是 2、8、14、20;消费层的 KV 张量就是源层那块显存的视图,不占新空间。第 20 到 39 层共用第 20 层的 KV(YOCO,You Only Cache Once),这给 Prefill 留了一个口子:后 20 层可以只算末尾一小段,开关默认关闭(第 9 章)。
- mHC、Engram、门控:残差是 4 条流,每个子层每个 token 算 24 个系数,其中 4×4 的矩阵做 20 次 Sinkhorn 归一化,写回、混合、归一化融合在 DeepGEMM 的 mega_mhc 内核里。Engram 在第 1、14 层按 2/3/4-gram × 8 个哈希查 24 行。门控给 384 个专家打分(sqrtsoftplus),加纠偏量选 6 个,权重归一化后乘 1.5,共享专家直接加上。位置编码用 YaRN:65536 × 16 = 1048576,撑到 1M 上下文。另有 3 个草稿层(DSpark)和 32 层的 ViT 视觉编码器。
记住这三句话
- V4.1 Flash 一共 7632 亿参数、510 GB:路由专家 289 GB,Engram 表 203 GB;每个 token 只用到约 161 亿。
- 40 层分三段:前 2 层只有滑动窗口,2 到 19 层两个 token 压成一条,20 到 39 层共用第 20 层的 KV;每层每个 query 最多读 640 条。
- 单 KV 头、压缩、共享和 FP4 换来小 KV;384 选 6 的专家要宽专家并行;Engram 表放在 CPU 内存里按需取。
4.3 一个请求的一生(0:43:23)
这一章回答:调用 llm.generate() 之后,一个请求经过了哪些对象、哪些进程?一个推理步里,CPU 和 GPU 各做什么,按什么顺序?开源版只能离线推理,做在线服务的接口留在了哪里?
要点
- 唯一的入口是阻塞的 LLM:generate 把所有请求一次加进引擎,循环到全部完成才返回。LLM 改了几个默认值:关掉统计日志,被抢占的请求放回等待队列重算,显存默认最多用 90%;它传的 offline_inference=True,全仓库没有地方用到。
- 进程:单机 8 卡、专家并行 8 路时,每个数据并行副本一个引擎进程,共 8 个,各带一个多模态预处理进程;图片走一块约 8.6 GB 的共享内存。只要任何一个副本还有请求,所有副本都要跑 step,没请求的跑 1 个 token 的占位步:MoE 的专家通信要求每张卡每一步都参与。
- 一步的固定顺序 :C++ 调度器先选出这一步的请求(挑请求的核心部分会释放 Python 的全局解释器锁);然后发射本步前向、处理上一步结果、发射本步采样,最后处理本步结果,Prefill 当场处理,Decode 留到下一步。"发射"就是放进 GPU 队列就返回,所以 Decode 的结果晚一步,最后一步靠 flush 取回。
- CUDA Graph:195 张图:只用于 Decode,按这一步的 token 总数分档:1、2、4,再从 8 开始每 8 一档到 1528,最后补 1536(最大请求数 256 × 每请求 6 个 token),一共 195 张。1002 个 token 用 1008 那张,补 6 个占位请求;超过最大一档直接报错。195 张图共用最大那张的显存池。
- 为在线服务留的接口:引擎有 NORMAL、PROMPT(只做 Prefill)、COMPLETION(只做 Decode)三种模式,调度路径都已实现。LLMEngine 里留了一排 12 个虚函数,8 个是空的,比如初始化 PD 分离、保存 KV、把处理完的 prompt 发给 Decode 引擎,它们被安排在一步里的固定时刻。
记住这三句话
- 开源版只有一个阻塞的入口 LLM:所有请求一次加进去,循环调用 step,直到全部完成才返回。
- 每一步先由 C++ 调度器选一批,再按"发射本步计算、处理上一步结果、发射本步采样"的顺序走;Decode 的结果晚一步返回。
- Decode 用 CUDA Graph,按 token 数分档,默认配置下 195 档;PD 分离和 KV 落盘的位置,留成了一排虚函数。
4.4 调度器(0:54:51)
这一章回答 :每一步做 Prefill 还是 Decode,由谁、按什么规则决定?一步 Prefill 装多少 token,装不下怎么办?KV 块不够时,Decode 先牺牲谁?异步调度怎么让 CPU 和 GPU 同时干活,乐观的记账又怎么修正?

要点
- 一步只做一种事:要么全是 Prefill,要么全是 Decode,代码里有断言检查。多个数据并行副本靠投票统一:Decode 期间步数计数器走到 70 的倍数,问"是不是所有副本都想做 Prefill",走到 100 的倍数,问"是不是至少一个想做";想做的条件是等待队列队头可调度、KV 块使用率不超过 90%。还有第二轮投票,避免运行队列满、等待队列又不空时死循环。
- Prefill 按 token 数装批:每步预算 max_num_batched_tokens(示例脚本 32768),先来先服务;打开 chunked_prefill(引擎默认关,示例脚本开)时,装不下的最后一条切成一块。块按整条 prompt 一次分齐。构造的例子:预算 512、20 个空闲块,A、B、C 长 300、150、400,第一步装 A、B 和 C 的 62 个;把空闲块改成 10 个,第二步块使用率超过 90%,转去 Decode,C 只算了 62 个 token 却占着 4 个块。
- 和官方问答的出入:问答说 Prefill 按纳秒时间预算、用拟合的二次代价模型反解 token 数,开源代码里没有,开源版只按 token 数。代码里的 FLOPs 估算器只在打开统计日志时用来算算力利用率。
- Decode 补块与抢占 :跨块边界时补一块,草稿没被接受多出来的块还回去。块不够时,按已输出长度排序,从最短的开始安排,没有空闲块就抢占剩下里输出最长的那条。函数上方的注释写的是抢占最短的,和代码相反,以代码为准。被抢的要么直接结束(引擎默认),要么清空 KV、放回队头重算(LLM 改成了这个);开源版没有把 KV 换出到 CPU 内存的路径。
- 异步调度(零开销调度):GPU 还在算第 N 步,CPU 已经调度并发射第 N+1 步。和官方问答有一处出入:问答写的是之后处理第 N−1 步的结果,按代码是处理第 N 步,而且要先等它的采样完成。代价是乐观记账:调度时假设在途的草稿全部被接受,再在 GPU 上按实际接受数修正(构造例子:CPU 算出能看到 312 个 token,实际是 307);结束符之后还会多算一步。
记住这三句话
- 一步只做一种事:要么全是 Prefill,要么全是 Decode;多个副本靠投票统一;Decode 中只在步数计数器走到 70 或 100 的倍数时试 Prefill。
- Prefill 按 token 数装批,开了 chunked_prefill 时,装不下的最后一条切成一块;Decode 缺块时先保输出最短的,抢占输出最长的;离线入口里,被抢的从头重算。
- 异步调度先假设草稿全部被接受,再在 GPU 上按实际接受数修正;调度领先一步,所以结束符之后还会多算一步。
4.5 显存(1:05:00)
这一章回答:一张卡上能放多少个 token 的 KV,这个数是怎么算出来的?一个 KV 块、一个状态槽里,每个字节放的是什么?多个请求怎么共享同一段前缀的 KV?
要点
- 先量峰值,剩下的才给 KV:启动时用假数据把模型完整跑一遍、记下显存峰值(故意让 MoE 负载不均,不均衡比 1.8)。可用显存 = 总显存 × 0.9 − 峰值,再扣两倍的状态槽(保存和复制各一份),剩下的除以每块字节数就是块数。构造的例子:100 GiB 的卡、峰值 60 GiB,得到 265,544 块,约 3399 万个 token。各卡的块数取平均(文档写的是取最小,以代码为准)。
- 一个块 114,048 字节:一块 128 个 token,只有 4 个源层有数据:第 2、8、14 层压缩过,每块 64 条;第 20 层每块 128 条。主 KV 每条 288 字节、索引 key 68 字节,四层合计 113,920,对齐到 1152 的倍数是 114,048,折合每个 token 891 字节。空闲块用双向链表加引用计数管理,0 号块永远不用。
- 每个请求一个状态槽:43 层(40 层加 3 个草稿层)的滑窗环形缓冲,每层 134 格、每格 528 字节;第 2、8、14 层还有压缩器状态。一个槽 3,128,928 字节。前 20 层的滑窗是落盘区,1,415,040 字节,和官方问答的数完全一致;官方问答说的显存副本区,开源代码只留了位置,判断要不要复制的函数固定返回 False。
- 环形缓冲:位置 p 写在第 state_id × 134 + p mod 134 格。构造的例子:状态槽 3、位置 1000,写在第 464 格,读的窗口是 873 到 1000。环要 128 + 6 格,是因为同一步最多写 6 个位置(草稿 5 个加 1)。
- fork 共享前缀:复制出的新请求直接用源请求的块表,引用计数加一;第一次调度时才拆开:写满的块继续共享,写了一半的尾块和状态槽复制一份。这不是写时复制。仓库的基准测试就用 fork 模拟前缀命中:248 个请求共享 28672 个 token 的前缀,只占 8160 个块,不共享要 63,488 个。
记住这三句话
- 启动时先用假数据跑一遍、量出峰值;剩下的显存扣掉两倍的状态槽,再除以每块 114,048 字节,就是块数。
- 一个块装 128 个 token:4 个源层的主 KV 和索引 key;每个请求另有一个约 3.1 MB 的状态槽,放 43 层的滑窗环形缓冲。
- fork 靠块的引用计数共享前缀:写满的块直接共享,写了一半的尾块和状态槽,在第一次调度时复制一份。
4.6 并行与专家负载均衡(1:13:46)
这一章回答:一张卡上,注意力和专家分别怎么分工?一个 token 在一层 MoE 里要跨几次卡?384 个专家加冗余副本怎么分到 64 张卡上,为什么是每卡 7 个?热门专家怎么办,EPLB 按什么规则决定复制谁、放在哪张卡?
要点
- 注意力和专家分开切:注意力用数据并行,每张卡有一份完整的注意力权重、只算自己的请求;专家用专家并行,分到一个 EP 组的所有卡上。代码里没有张量并行。一个 token 进 MoE:门控选 6 个专家,查表换成物理槽位,槽位号除以每卡槽位数就是目标卡;dispatch 发出 FP8 激活,专家算完 combine 送回、加权求和,再加上本卡的共享专家。
- 两条通信路径:整个 EP 组在同一个 NVLink 域、精度组合支持时,走 DeepGEMM 的 Mega MoE,发送、两次专家矩阵乘、SwiGLU、收回求和、共享专家一次内核调用做完;否则走 DeepEP 的 dispatch / combine,combine 的通信和共享专家的计算可以同时进行。
- 冗余数怎么取整:物理槽位 = 384 + 冗余数,必须能被卡数整除。冗余从下限开始补到整除:64 卡、下限 1,385 还差 63 才整除,所以冗余 64 个、448 个槽位、每卡 7 个。代码默认下限 0,64 卡时每卡 6 个、没有冗余;示例脚本下限 32,8 卡时每卡 52 个。刚启动时副本按编号依次放,和负载无关。
- EPLB 两阶段贪心 :每一步记下各槽位收到的 token 数,存进默认 100 步的窗口;重排周期默认 0(不开)。第一阶段决定复制谁,第二阶段决定放哪张卡,目标都是每一步的峰值加起来最小。构造的例子(8 个专家、4 张卡、4 个冗余):依次复制专家 3、6、1、7,总量排第三的专家 5 一个也没分到,因为它在哪一步都不是最忙的;放完后最忙的卡除以平均,从 1.75 降到 1.04。策略不追求少搬权重,贪心也不保证最优。
- 搬权重与 moe_lb:同一个 NVLink 域里,每张卡开一块对称显存当镜像,逐层拷贝、屏障,缺的专家直接从对方镜像读。日志里的 moe_lb 是最忙的卡除以平均(数量之比,不是时间之比);官方实测 1.074、1.045,但仓库的基准测试用的是随机路由,测出的值不能直接比。
- 平衡 CP 与 Engram:上下文并行(只用于 Prefill)按首尾配对切块,16 个 token、2 张卡时,连续切两张卡要看的 key 是 36 和 100,首尾配对都是 68;代码实现了,官方问答说线上没开。Engram 表按行切到所有卡的主机内存,取数走网卡 RDMA。

记住这三句话
- 注意力按数据并行,每张卡算自己的请求;专家按专家并行分到 EP 组里;每层 MoE,一个 token 发出去一次、收回来一次。
- 冗余数从下限开始,补到能被卡数整除:64 卡时 448 个槽位,每卡 7 个;同一个 NVLink 域里,用 Mega MoE 一次调用做完通信和计算。
- EPLB 用最近若干步的计数做两阶段贪心:先决定复制谁,再决定放哪张卡,目标都是每一步的峰值加起来最小。
4.7 算子与低精度(1:26:30)
这一章回答:FP4 的绝对值只有 8 种,怎么存得下权重和 KV,缩放因子是怎么算的?DeepInfer 的算子,哪些是自己写的,哪些来自外部库?为什么开源版只能跑在 B200 这一代和昇腾 950 上?
要点
- 三种格式:FP8 e4m3 最大 448;FP4 e2m1 的绝对值只有 8 种:0、0.5、1、1.5、2、3、4、6;ue8m0 只能表示 2 的整数次幂。办法是分组:每 32 或 16 个数共用一个缩放因子,MXFP4、MXFP8 就是每 32 个数配一个 ue8m0。
- 谁用哪种:稠密权重 FP8,路由专家 MXFP4,激活 FP8(每次前向现算缩放因子);主 KV 是 FP4、每 16 个数一个 e4m3 缩放因子(288 字节),索引 key 是 MXFP4(68 字节),滑窗 KV 是 MXFP8(528 字节)。
- 手算一组(构造的 32 个数,最大绝对值 25):25 ÷ 6 ≈ 4.17,向上取到 2 的幂是 8;25 存成 24,20 存成 16(正好在两个值中间,就近取偶),12 没有误差;整组最大误差 4、平均约 1.22。主 KV 用 e4m3 当缩放因子,同样是 25,缩放取 4,最小步长从 4 变成 2,网格细了一倍。
- 算子地图:重的来自外部库:注意力 FlashMLA,矩阵乘、Mega MoE、索引器打分 DeepGEMM,通信 DeepEP,选前 512 个 DeepSelect,量化 TileKernels。DeepInfer 自己用 TileLang 写读写 KV、融合、索引和元数据的算子,每个都有 cuda 和 asc 两份实现。
- 融合:写 KV 的那个内核把 RMSNorm、旋转位置编码、可选的 Hadamard、量化、写进分页缓存五步合成一个,读一次、写一次;每一步 Decode 前的一堆元数据,也由一个内核整批算完。
- 为什么只认 SM100 和昇腾:返回算力上限的函数里只有昇腾 432、SM100 2250 或 2500,其他一律报错;更根本的是 FP4 矩阵乘需要 Blackwell 这一代 Tensor Core 的原生支持,H100 没有。昇腾版换成 asc 后端和 HCCL 通信,量化算法逐位一致。
记住这三句话
- 低精度靠分组缩放:每 32 或 16 个数共用一个缩放因子;ue8m0 是向上取的 2 的幂,主 KV 用 e4m3 当缩放因子,更细。
- 大算子来自 FlashMLA、DeepGEMM、DeepEP、DeepSelect 和 TileKernels;DeepInfer 用 TileLang 写读写 KV、融合和元数据的算子,每个都有 NVIDIA 和昇腾两份。
- FP4 矩阵乘和 FP8 KV 缓存依赖 Blackwell,代码只认 SM100 和昇腾;昇腾版换内核后端、换通信库,量化的算法逐位一致。
4.8 采样与投机解码(1:35:44)
这一章回答:一步采样在 GPU 上做了哪些事,top-p 和 top-k 谁先谁后?草稿 token 怎么被接受或拒绝,DeepInfer 和标准投机采样差在哪?每一步验证几个草稿,是怎么定的?
要点
- 一步采样是一个融合算子 :logits 先加掩码(全局黑名单、结构化输出、首 token 禁止结束符),再进 fused_sample:温度、top-p、top-k、归一化、按概率抽样,一行一个线程块。先 top-p 后 top-k:在温度缩放后的完整分布上做 top-p,再在剩下的里面做 top-k。
- DSpark 一次前向出 5 个草稿:3 层,和主模型共用词表矩阵;输入是 5 个位置的块(刚输出的 token 加 4 个占位),注意力读滑窗 128 加块内 5 个;上下文的 key 来自主模型第 37 到 39 层的特征。5 个位置逐个采样,每个位置加一个由上一个草稿决定的 Markov 修正;每个位置还有一个置信度,预测这个草稿被接受的概率。
- 验证:一个请求最多占 6 行,按前缀接受,接受了 a 个草稿就输出 a+1 个 token。构造的例子:目标分布 q 是 0.5、0.3、0.15、0.05,草稿分布 p 是 0.2、0.6、0.1、0.1,草稿采到 B,接受概率 min(1, 0.3 ÷ 0.6) = 0.5;拒绝后从 q 减 p 的正部里重采,A 约 0.857、C 约 0.143。
- α 略小于 1:DeepInfer 在残差里给 p 乘了 α = e^(−5×10⁻⁵),常量注释说是防止拒绝采样采不出东西。代价是输出分布不再严格等于 q,代码注释给了上下界:在 q − ε 和 q(1+ε) 之间,ε 约 5×10⁻⁵;例子里的偏差约 3×10⁻⁶。
- 变长验证(默认关):置信度累乘,得到每个草稿的期望收益;所有请求的草稿放在一起排序,用预先测好的耗时算每毫秒期望输出多少 token,取最大的点。构造的例子:置信度 0.9、0.8、0.6、0.4、0.2 时只验证前 3 个最划算。官方问答说,这样每步验证的草稿数从 5 个降到 3.2 到 4.4 个。异步调度下,CPU 只定总行数,每个请求分几行由 GPU 用最新的置信度分。
- 回退 :只追加被接受的草稿;被拒位置写进的 KV 不用清,下一步直接覆盖;滑窗环多留的 6 格、压缩器多存的 5 格回退历史,就是为这个准备的。

记住这三句话
- 采样是一个融合算子:温度、top-p、top-k、归一化、按概率抽样,一行一个线程块;top-p 在完整分布上算,再做 top-k。
- 草稿以 q 比 p 的概率被接受;拒绝后从 q 减 α 倍 p 的正部重采,α 略小于 1,输出分布的偏差有明确的上下界。
- DSpark 一次前向出 5 个草稿和置信度;开了变长验证,就按累乘的置信度全局排序,取吞吐最高的那个前缀。
4.9 长上下文的省钱术(1:43:44)
这一章回答:命中了前缀缓存,滑窗 KV 却不在,怎么办?后 20 层为什么可以只算 prompt 的最后一段?截断以后结果还对吗,代码怎么验证?
要点
- 两种 KV,两种存法:块 KV 进持久化存储(官方问答说约 100 PiB 的 SSD);滑窗 KV 只存前 20 层、每个请求 1,415,040 字节,留在产生它的那台机器的内存里,而且只在 128 的整倍数位置存。构造的例子:prompt 1000 个 token,留出 1 个后向下取整,存 896 之前的那段窗口。
- 精确重建很贵:一层的滑窗 KV 取决于这一层的输入,而上一层又看了前面 127 个位置,每往下一层多回溯 127 个;官方问答的数是,后 20 层精确重建要把最后 2,560 个 token 跑一遍。
- 有界重放:只重算最近 128 个 token,把注意力窗口截在重放段里面(构造:从 640 开始重放,位置 640 只看到自己,到 767 才看满 128 个)。得到的是近似的滑窗 KV,代价是常数。命中策略:输入长 L,最多命中到 L − 134;块 KV 命中长度是 128 的倍数,滑窗 KV 不在时少命中 128 个,这 128 个就是重放窗口。开源代码里有这套机制的零件,触发重放的命中逻辑在仓库外面。
- YOCO 截断(默认关):后 20 层共用的块 KV 只取决于进入第 20 层的隐状态,它们自己的滑窗只服务 decode,所以 Prefill 在后 20 层只需要算末尾 KEEP 行(至少 134,README 推荐 compact 模式、保留 140)。官方问答说 Prefill 的总计算量差不多减半。构造的例子:5000 个 token 的 prompt、每块 2048,后 20 层只算 142 行。
- 验证的是什么:仓库里的脚本分两级:先只比元数据,再用 8 张卡和权重逐位比较 compact 和 mask 两种模式的 logprob 与 top-K。它验证的是 compact 等于 mask,不是等于不截断;截断本身是近似,误差不在检查范围里。

记住这三句话
- 命中长度只看块 KV;滑窗 KV 缺了,只重放命中前缀的最后 128 个 token,窗口截在重放段里面,代价是常数。
- 后 20 层的块 KV 只取决于进入第 20 层的隐状态,它们自己的滑窗只服务 decode;所以打开截断后,Prefill 在后 20 层只需要算末尾 KEEP 个 token,KEEP 至少 134,这个开关默认关闭。
- 验证脚本检查的是 compact 和 mask 逐位一致,不是和不截断一致;截断本身是近似。
4.10 确定性、结构化输出与 Recipe(1:51:47)
这一章回答:同样的输入,怎样保证每次输出的 token 都一样?结构化输出每一步做了什么,和投机解码怎么配合?用户在输入里写一个 <|User|>,为什么不会被当成控制 token?
要点
- 随机数按键生成:每个请求一个 64 位种子,序号是 token 位置 × 3 再加 0、1、2(新采样、拒绝后重采、接受判断),同一步里第 k 行再加 k × 10000019。随机数只取决于种子和位置,和这个请求跟谁组批、排在第几行都无关。
- 批次无关模式:浮点加法不满足结合律,批次变了,分块和求和顺序可能变,logits 的末位可能不同,整条序列就可能分叉。打开 DEEPINFER_BATCH_INVARIANT,代码里能看到四处变化,昇腾上不能开;其中两处 V4.1 碰不到,这是从代码推导的。README 的确定性配方是:打开批次无关、关掉变长验证、传固定种子。仓库里有两个测试,要求整批和单独生成的 logprob 逐位相等。
- 结构化输出:用 xgrammar 把 JSON schema、正则或 EBNF 编译成自动机,线程池异步编译,超过 20 秒或失败就以失败结束。每一步为每个验证行生成一份位掩码(129280 个 token 对应 4040 个 32 位整数),不合法 token 的 logits 写成负无穷。草稿不加掩码,但不合法草稿的目标概率是 0,按接受规则必然被拒。
- Recipe:进出模型的那一层:deepseek-recipe 用 Rust 写,带 Python 绑定;Chat Completions、Responses、Messages 三种协议先转成同一种对话格式。拼 prompt 时只把模板里显式插入的位置编码成特殊 token,用户文本里同样的字符串按普通文本切,所以写一个 <|User|> 不会变成控制 token;输出端也只认真正的特殊 token。
- 流式解析:特殊 token 之间的普通文本分段、批内并行编码;输出时攒齐不完整的 UTF-8,用增量的 KMP 匹配分隔标记,工具名一解析出来就先返回,参数边生成边返回。
记住这三句话
- 随机数只由种子和位置决定,和批次无关;要整条输出确定,还要打开批次无关模式、关掉变长验证、传固定种子。
- 结构化输出每步推进 xgrammar 自动机,为每个验证行生成一份位掩码,不合法的 token 写成负无穷;草稿不加掩码,不合法的草稿必然被拒。
- Recipe 只把模板里显式插入的位置编码成特殊 token,用户文本里同样的字符串按普通文本编码;输出端也只认真正的特殊 token。
4.11 没开源的那一半(2:00:07)
这一章回答 :线上的 DeepSeek V4.1 Flash 是怎么部署的?块 KV 存在哪,为什么不放在本机内存?四个负载均衡器各管什么,坏了一张卡怎么办?(这一章的内容来自官方推理部署问答,能和代码对上的地方会单独指出。)

要点
- 三段分离:视觉编码、Prefill、Decode 分开部署,加专用存储节点。Prefill 用 EP2 到 8,Decode 用 EP64 到 128(Wide-EP),视觉编码一般单卡一个实例;张量并行和上下文并行目前都不用。Prefill 和 Decode 的卡数约 1 比 1,所以最小高效部署单元是 64 卡 Prefill 加 64 卡 Decode。Wide-EP 的收益:每卡 64 个请求时,EP64 每卡每秒约 15,750 个 token,EP8 约 12,650。
- 块 KV 放在专用的 3FS 集群:不放本机内存,是为了任何一台 Prefill 机器都能命中任何前缀,存储和计算各自扩缩容,而且计算节点的故障率比存储节点高一个数量级以上。数据用 10 加 2 的纠删码,整个集群约 100 PiB,agent 流量的缓存留 3 天,聊天留 7 天。
- 四个负载均衡器:Prefill 按上下文总长度分四档,引擎按剩余算力配额拉取请求,单卡用时间预算加二次代价模型(开源版没有);Decode 同时平衡请求数和 KV 总长度;视觉编码按图块数挑最空的卡;专家负载均衡的线上参数是 Prefill 每 1000 步、Decode 每 10000 步重排一次,Decode 实测 moe_lb 1.090。官方说整体能做到完美均衡离线基准的约 80%。
- 扩缩容与容错:Decode 引擎下线有 1 分钟宽限期,到期没结束的请求连同 KV 转给别的引擎;配额利用率超过 0.8 禁止缩容。重启要几分钟,所以容错靠冗余配额、请求级重路由,以及按健康卡数重定 EP:只剩 60 张健康的卡时,按第 6 章的取整规则,冗余 96 个、480 个槽位、每卡 8 个。
- 开源版和线上版差多少:内核库和内部版本大体相同;FlashMLA 少一个手调的 SASS 优化,解码内核慢 5% 到 10%;端到端最多差 5%。
记住这三句话
- 线上是视觉编码、Prefill、Decode 三段分离,加专用存储节点:Prefill 用 EP2 到 8,Decode 用 EP64 到 128,最小高效单元是 64 卡加 64 卡。
- 块 KV 放在专用的 3FS 集群上,10 加 2 纠删码;任何 Prefill 机器都能命中任何前缀,存储和计算各自扩缩容。
- 四个负载均衡器分管 Prefill、Decode、视觉编码和专家;容错不靠缩短重启,靠冗余配额、请求级重路由和按健康卡数重定 EP。
4.12 上手、补齐与对比(2:09:45)
这一章回答:拿到仓库,怎么编译、怎么跑起来?benchmark 报的三个数各是什么意思?做成在线服务还缺什么,它和通用推理框架是什么关系?
要点
- 编译:Linux、GCC 13 以上、Python 3.12;NVIDIA 要 CUDA 13,昇腾要 CANN 9.2。先导出 DEEPINFER_ACCELERATOR(nvidia 或 ascend),再跑 build.sh:装依赖、固定 NCCL 版本、把 DeepSelect、FlashMLA、DeepEP、DeepGEMM 编成 wheel 装上、编译 C++ 部分。
- 第一次运行:offline_inference.py 的拓扑由 ep、dp、cp 决定,总卡数 dp × cp 必须能被 ep 整除。只给 ep 8,就是 8 个副本组成一个 EP8 组。每条输出会打印 decode 的困惑度,回归测试拿它和基线逐位比较。
- benchmark 的三个数:先用一条请求算好共享前缀,再 fork 出整批。报 Prefill 每卡每秒处理的未缓存 token、decode 每秒走多少步(avg_SPS)、按草稿全部接受折算的 decode 吞吐。注意 benchmark 里写死打开了强制负载均衡(门控打分换成随机数),测的是专家完全均衡时的性能。
- 评测:AIME 2026(30 题、每题 32 次)、GPQA Diamond(198 题、每题 8 次)、HLE(2500 题、每题 1 次),README 的参考成绩分别是 99.9%、93.4%、42.6%;数据集要自己准备。
- 还缺什么:README 列了 KV 缓存系统、分离部署、DP 副本之间的负载均衡;仓库里唯一的 HTTP 接口是性能分析用的;协议转换可以用 deepseek-recipe;引擎这边的接口已经留好。
- 和通用框架的关系:仓库里有 23 个文件开头写着"从 vLLM 派生,经 DeepSeek 修改",入口、引擎、worker 这些骨架和 vLLM 对得上;调度器、块管理、序列状态、零开销调度和元数据准备换成了约 5200 行 C++,算子用 TileLang 加上 DeepGEMM、FlashMLA、DeepEP 这些库。模型注册表里只有一个架构,不做张量并行。
读源码的路线:先跑通 offline_inference,跟一个请求从 LLM.generate 走到 step;再读 C++ 的 scheduler.hpp,它是调度和显存的中心;然后读 deepseek.py 和 v4_modules,对照第 2 章的结构;算子从 tilelang_ops 顶层文件里 PyTorch 写的参考实现读起;最后读 sampler 和 mtp,对照第 8 章。每读一处,找对应的测试:测试里的断言,就是这段代码承诺的行为。
记住这三句话
- 先导出 DEEPINFER_ACCELERATOR 再跑 build.sh;离线推理的拓扑由 ep、dp、cp 决定,总卡数 dp 乘 cp 必须能被 ep 整除。
- benchmark 在强制均衡路由下测:Prefill 每卡每秒的 token、decode 每秒的步数、按全部接受折算的 decode 吞吐。
- 缺 KV 缓存系统、分离部署和 DP 负载均衡;骨架从 vLLM 派生,调度、显存和算子围着 V4.1 一个模型重写。
5、总结
全片最后留了三句话:
- 这部片子里的大部分设计,都在做两件事:让同一份权重一次服务更多 token,把每个 token 要读的 KV 压小。
- DeepInfer 围着一个模型来设计:C++ 调度器、分页块加状态槽、DP 注意力加 EP 专家、FP4 和 FP8 的融合算子、DSpark 投机解码。
- 开源的是引擎、模型和算子;KV 存储、分离部署和 DP 之间的负载均衡在官方问答里。读代码时,分清哪些是代码事实,哪些是推导。
还没看入门版的,可以先看上一篇:一次 API 调用背后算了什么、为什么拼 batch 便宜、KV cache 为什么要分页。
|--------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| 🚀 持续探索 AI 与前沿技术 分享大模型应用、软件开发实战与行业洞察。 欢迎关注GongZhongHao 【龙哥AI】,加入 7000+ 技术同行的交流圈! 🌟 探索技术边界,让开发更有效率 |
|