RTX 5060 8GB + 31GB RAM 运行 510GB 专家混合模型,通过磁盘只读和 RAM 缓存方案达 1.6-2.4 tokens/s,包含三个不报错的 bug 排查过程。
我家里跑本地模型的机器配置一般:RTX 5060(8 GB)、Core Ultra 5 225F、31 GiB 内存、一块 Gen5 NVMe 硬盘(只用来存模型文件)。DeepSeek-V4.1-Flash 磁盘占用 510 GB,现在已经能在这台机器上作为普通聊天模型运行了,工具是 Open WebUI:纯从磁盘读取专家约 1.6 tokens/s,加 16 GB 内存缓存约 2.4 tokens/s。这个速度很慢,这篇文章会如实说明原因——但它确实能跑,而且每一项优化都严格保证输出 token 与纯原版(用 DeepSeek 官方代码做全部运算)逐 token 完全一致。
代码仓库:https://github.com/helgard-orlm/deepseek-v41-flash-8gb
先说清楚谁做了什么:我设定目标、选模型、提出部分思路(包括预测下一层专家的方案),并做最终拍板。引擎、服务器和 bug 排查由 Claude(Anthropic)在我的一系列 session 中完成;重叠读取方案和单 token 短路径来自 Codex(OpenAI)审查引擎时的建议。下面的"我们"指我们三个人。
这是我们以这种方式运行的第二个模型。第一个是 133 GB 的 Qwen,速度 11 tokens/s,详见前一篇。这篇文章最有价值的是两者之间的对比。
对于混合专家模型,磁盘占用大小几乎无关紧要,真正重要的是每个 token 拉取了多少专家字节。
DeepSeek-V4.1-Flash:40 层,每层 384 个专家,每个 token 选 6 个,每个专家在 FP4(带 scale)下 17.93 MiB。
6 experts × 40 layers × 17.93 MiB ≈ 4.2 GiB per token
Qwen 模型每个 token 需要 1.27 GiB。同样的机器、同样的硬盘、同样的方法——DeepSeek 的字节数是 Qwen 的 3.3 倍,光这个就解释了其大部分速度差距。
那 475 GiB 都去哪了:
DeepSeek 发布了参考推理代码(model.py + kernel.py,MIT 许可证)。我们原样用它做所有算术运算,只替换权重来源那一部分。这样有一个很好的附带好处:不需要任何转换步骤。在原始 safetensors 文件中,每个专家的三个矩阵已经紧挨着存放(17.7 MB),它的 scale 是另外一块(1.1 MB),所以一个专家就是两次带 O_DIRECT 的 pread 调用,直接从 Hugging Face 文件读取。读性能测试显示这一步已经用掉了硬盘能力的 94–97%,所以在磁盘上重新布局数据没有收益。
我们最初是在租来的 RTX 5090(VRAM 人工限制在 7.3 GiB)上测量的全流程,然后才买了 Gen5 硬盘。当数字看起来值得之后,模型才搬回家。
这部分是我在 Blackwell 消费显卡上尝试类似操作前最想看到的。
1. TileLang 0.1.8 在 sm_120 上算出的结果是垃圾,但静默无声。 参考代码指定了这个版本。在我们的卡上,FP4 矩阵乘法的余弦相似度相对普通 torch 计算只有 0.0006(即结果毫不相关),FP8 版本返回 NaN,模型却愉快地输出"athaatha Stone"。官方自检通过了——它只检查 shape,不检查值。TileLang 0.1.9:余弦相似度 0.9996。
2. 参考代码 act_quant kernel 中的竞态。 DeepSeek 的 scale 格式(ue8m0,该模型始终启用)构建 kernel 时 num_stages = 0。在 RTX 5060 上,超过 64 行时,部分 FP8 输出会变成 NaN——而且每次都不一样:同一个输入跑五次,分别得到 367、……、最多 2065 个 NaN 值。逐个 token 生成是干净的,所以中招的只有 prompt。这不是编译器的问题(CUDA 12.8 同样如此)。设 num_stages = 2 则零 NaN,并在 1、17、300 和 1024 行上与 torch 实现逐 bit 一致。那一行改动是 setup 脚本对 DeepSeek 代码的唯一修改。
3. Sparse attention kernel 需要 141 KB 共享内存,消费级 Blackwell 只有约 99 KB。 Heads 互相独立,所以我们按每 16 个 head 一组调用同一个 kernel。这里有个小陷阱:对 1-token tensor 的某个 head 切片调用 .contiguous() 不会改变其 strides,而 kernel 会检查 strides——需要用 clone(memory_format=torch.contiguous_format)。
共同教训:在新硬件上,"没报错运行成功"说明不了任何问题。在信任任何一个生成的词之前,用真实权重将每个 kernel 与简单的 torch 版本做对比。
每一步的规则:固定 4 题检查的 136 个生成 token 必须与纯原版(v1)完全一致。任何改变了 token 的方案都会被拒绝或修复。要精确说明这证明了什么:这些提速方案没有改变任何东西。它并不能证明 v1 与 DeepSeek 原版程序并列运行的结果一致——因为原版程序在 8 GB 上加载不了模型,所以我们从未跑过。v1 的背书来自:它调用 DeepSeek 自己的 model 和 kernel 代码做每一次运算,各个 kernel 都与真实权重下的 plain torch 做了对比(如上),展开的 wo_a 与 DeepSeek 的 convert.py 逐 bit 匹配,答案也是正确的(堪培拉、正确的 TCP/UDP 解释、可用的 merge 代码)。
v1 — 朴素流式处理: 纯磁盘读取 0.89 tokens/s,加 14 GB 内存缓存 1.17 tokens/s。读取该层的 6 个专家、复制到 GPU、等待、计算。磁盘、总线和计算就是简单相加。
v2 — 重叠读取(Codex 的方案): 1.28 tokens/s。用独立的复制线程加 CUDA events 替代全局 synchronize。意外发现:同时发起全部 6 个专家读取反而更慢。因为它们共用磁盘,所以会一起在层末尾完成,计算也无法提前开始。真正有效的是深队列但逐个专家读取:最多 2 个在飞,每次读取分 4 个并行块。每个 token 等待磁盘的时间从 0.65 秒降到 0.31 秒。(这个版本的早期版本也有自己的竞态——环形缓冲区覆盖了一个还没到 GPU 的专家,第 29 个 token 变了。token 检查发现了它。)
v3 — 单 token 短路径: +5.7%。参考代码用 torch.where(indices == e) 找每个专家的 token——这是 CPU↔GPU 同步,每 token 240 次,每次都延迟下一次复制。对于单个 token,现在每层只把 indices 移到 CPU 一次。算术相同。
v4 — 预取下一层的两个专家填入间隙: 纯磁盘 1.64 tokens/s(+21%),加 RAM 2.3 tokens/s。用 layer i+1 的路由器作用于 layer i 的输入(按两层 norm weight 重新缩放),71% 的情况下能得到正确的 top-6。一旦 layer i 的真实读取在飞,就读 top-2 猜测中还没在 RAM 里的。94% 的预取被用上了。预取三个或四个反而更差:它们开始与真正需要的读取抢资源。
这一路我们犯过一个错:我尝试通过把已在 RAM 中的猜测计入预算来减少"白读字节"。白读的字节少了,速度反而降了。落在间隙里的读取——在磁盘本来会闲着的时候——是免费的;唯一重要的是有多少专家能及时到达。
一个 311-token 的 prompt 需要 18 秒(v1 需要 27 秒)。峰值 VRAM 7.06 GiB。后续消息不需要重读对话:服务器在每次回答后对完整模型状态做快照(30 MiB——KV、滑动窗口、压缩尾、索引器缓存、Engram 历史),只处理新的 tail 部分。
其中有些是我的想法,有些是 Claude 的;全部都是在真实权重上测量过的,不是靠争论。
存储专家之间或层之间的差值,压缩率会更好。 FP4 码字有 3.895 比特熵;同一专家在下一层或相邻专家的差值,熵是 3.997——比原始值更差。即使做最好的神经元置换和每神经元缩放,剩余差值仍是原始值的 0.999–1.000,与两个随机矩阵得到的结果完全一样。
跳过"沉默"神经元,不读它们对应 down projection 的部分。 要把每个专家误差控制在 5% 以内,你仍然需要 61–75% 的神经元:减少约 12% 字节,但读取需要等前半部分计算完成。该模型的 SwiGLU 神经元根本不会像 ReLU² 那样变安静。
更智能的缓存策略。 在记录的路由上做模拟:带老化的 LRU 击败了 LFU、逐层 LRU 和 SLRU。理论最优比这些高约 15 个点,但没有简单的策略能接近它。
更多读取线程,把读取拆成更小的块—— 硬盘已经接近极限。
从磁盘来看,一个 token 约花 0.6 秒,其中纯读取 4.2 GiB(按这块盘在这些读取模式下 8.2 GiB/s 的速度)约 0.5 秒。软件层面已经几乎没有开销了。要更快只有两条路:减少字节数(给缓存更多 RAM——完整专家集 269 GiB,所以缓存永远只是部分的)或再加一块硬盘。用同样的方法跑 Qwen 能到 11 tokens/s,因为每个 token 只需要三分之一的字节数。如果你在选一个要在家用 PC 上流式播放的 MoE 模型,先看每 token 字节数,再看参数量。
你需要一块高速 NVMe 硬盘(空余约 520 GB)和一块 Blackwell GPU(上面的修复是针对 sm_120 的,其他卡可能只需要其中部分)。setup.sh 会下载我们使用的精确模型版本(如果还没有的话),检查 kernel.py 是否为预期文件,并构建一份带那一行修复的 DeepSeek 代码副本。发布前,我们在同一台机器上全新克隆仓库,用下载的模型跑 setup,然后从克隆启动服务器:所有参数都被正确加载,答案也正确。引擎和服务器文件与在家里跑的一模一样。完整的 510 GB 下载(从零开始)没有为这次测试重复。
引擎、服务器、bug 排查和这篇文章:Claude(Anthropic)。重叠读取方案和单 token 短路径:Codex(OpenAI)。目标、模型选择、专家预测想法和拍板:我。所有数字均来自上述机器上的日志。