TL;DR:上回书说到,753B 的 GLM-5.2 在我的 64GB 笔记本上跑出满屏问号,真凶锁定 MSVC 编译器的浮点精度差异。这次我换了 DeepSeek-V4-Flash(284B MoE,144GB 为 GGUF 量化文件体积),在 64GB 内存上终于正确输出了。但真正有意思的问题是——同样是 MLA,同样是 Windows,凭什么这次没乱码? 凶手的孪生兄弟在案发现场溜达了一圈,居然没犯案。本文记录这 12 小时的折腾:GPU 四连撞、翻 GitHub 找真相、–cpu-moe 柳暗花明,以及一个留给所有人的验证实验。
关于本文:这是上一篇《破案了!64G 内存跑 435G 大模型没 OOM 却满屏乱码》的续集。没读过上一篇的建议先看,不然你不知道我在兴奋什么。所有测试数据、命令、速度数字均为实测,与第一篇同机、同环境。
先告诉你结局
1.77 tok/s。不快。
但这一次,从第一个字符到最后一个,全是对的。
Answer: Hi there! 👋 How can I help you today?
Speed: 1.77 tok/s
中英文都正常,推理都正常。我盯着屏幕看了大概 5 秒,然后笑了。
上一次,速度对了,输出是问号。这一次,速度对了,输出也对了。同一台笔记本,同一个 Windows,同样 64GB 内存硬扛超大模型,同样是 MLA 架构——但这一次,大模型终于跑起来了。
问题来了:
为什么上次满屏问号,这次全对了?
同样是 MLA,同样是 MoE,同样有 Lightning Indexer 类似物,同样是 Windows CPU-only 起步。上次把 bug 摁死在 MSVC 的浮点精度上,这次它却乖乖认输?
如果上一篇是"破案",这一篇就是"凶手的孪生兄弟在案发现场溜达了一圈,居然没犯案"。
而为了想明白这件事,我又折腾了 12 个小时。
(理科生的倔,是不分模型的。凶手的兄弟也不行。)
上回书说到
上一篇结尾我写了一句话:
至于我的模型,它还在那儿。435GB,安安静静地躺在 SSD 里。等待那个 bug 被修好的早晨。
GLM-5.2 的 MLA 浮点精度 bug(Issue #26027)至今没修。llama.cpp 的 glm-dsa 架构在 Windows 上输出乱码,Linux 上正常。我的笔记本是 Windows。所以那 435GB 的模型文件,就一直在那儿躺着。
但那个 435GB 的 bug 教会了我一件事:能跑多快是工程问题,能不能跑对是理解问题。 上次我卡在"跑对"上,这次我至少知道该往哪看。
而且我没闲着。Llama.cpp 在迭代,DeepSeek 在发新模型,我的 SSD 在涨价。一切都在变,只有那个 issue 还是 open。
一个新的目标
DeepSeek 发了 V4-Flash。Unsloth 量化了 GGUF 版。我看了一眼架构参数:
┌────────── DeepSeek-V4-Flash (0731) ──────────┐
│ Architecture: deepseek4 (全新架构) │
│ Parameters: ~284B 总参数 (MoE) │
│ Active: 6 专家/层 × 43层 │
│ Attention: 64头, 1 KV头 (MLA) │
│ Experts: 256个, 每token激活6个 │
│ Indexer: Lightning Indexer (类DSA) │
│ Context: 1M tokens │
│ Quantization: UD-Q4_K_XL (Unsloth) │
│ Files: 144GB, 5个分片 │
└──────────────────────────────────────────────┘
等等。又是 MLA,又是 MoE,又是 Indexer。
上一次 MLA 在 Windows 上把我坑惨了。满屏的 ? 至今还印在我脑子里。这次我犹豫了大概 0.3 秒,然后点了下载。
(犹豫那 0.3 秒里我想到的其实是另一个问题:如果 deepseek4 也有同样的浮点 bug,我是不是要等第二个 issue open 躺在那里?但转念一想,144GB 而已,上次是 435GB 都下了,还差这点?)
第一关:它能跑吗?
144GB 的模型,64GB 的内存。差距是 2.25 倍——比 GLM-5.2 的 6.8 倍小多了,但仍然装不下整个模型。
好消息是 MoE 的老规矩还在:每 token 只激活 6 个专家。而且这次专家更小了。两家旗舰的专家尺寸一对比,差距立刻出来:
┌────────────── 专家对比 ──────────────┐
│ │
│ GLM-5.2 DeepSeek-V4-Flash │
│ ┌──────────┐ ┌──────────┐ │
│ │ 维度 │ │ 维度 │ │
│ │[6144,2048]│ │[4096,2048]│ │
│ └──────────┘ └──────────┘ │
│ 20.25MB/专家 13.5MB/专家 │
│ │
│ 8专家/层 × 76层 6专家/层 × 43层 │
│ = 热数据13GB = 热数据3.5GB │
│ │
│ per-token I/O (单层) │
│ ~162MB ~81MB │
└─────────────────────────────────────┘
激活参数 = 6 专家 × 43 层 × 13.5MB ≈ 3.5GB
(口径说明:上表中两家模型的 per-token I/O 是"单层实际参与路由的激活专家矩阵读量":GLM 8×20.25≈162MB、DeepSeek 6×13.5≈81MB。与上一篇"每专家 21.75MB(gate+up+down 全量)"的统计口径略有出入,本篇为便于同层对比统一了算法。)
3.5GB 热数据,64GB 内存,理论上绰绰有余。剩下的 141GB 专家权重安安静静躺在 SSD 里,靠 mmap 按需加载。这套逻辑上一篇已经验证过——mmap + LRU 热缓存,是行的。
先下载了 llama.cpp b10276(最新版)。有两个版本可选:普通 CPU 版(18MB)和 HIP 版(324MB,给 AMD GPU 准备的)。HIP 版下载花了整整一小时——GitHub 国内速度,你懂的。
(324MB 下了 1 小时。我一度怀疑是不是回到了拨号上网年代。后来想想也合理:上次 435GB 下了一整天,这次 324MB 一小时,四舍五入算提速了。)
先用普通 CPU 版试:
llama-server -m model.gguf -c 128 -ngl 0 -t 12 –no-repack
等了 48 秒。这 48 秒里我盯着任务管理器——内存从 8GB 涨到 43GB,然后停住了。风扇啸叫了一阵,然后安静下来。和上次一模一样的节奏。
model loaded
listening on http://0.0.0.0:8081
加载成功。颤抖着发了第一个请求。返回:
Answer: Hi there! 😊 How can I help you today?
Speed: 1.37 tok/s
中文英文都对了。 上一次"什么都对,就输出不对",这一次是"什么都对,全都对"。
那一刻是晚上十一点多。风扇已经安静下来,屋子里只剩下空调的嗡嗡声。我把这个截图发给了朋友,就四个字:"它说话了。"他回了个问号。但那晚我睡得很香——上一次满屏问号的时候,我失眠到凌晨两点。
之后 HIP 版下好了,同样的命令跑了一遍:
Speed: 1.66 tok/s
HIP 版比普通 CPU 版快了 21%。虽然 GPU offload 还没开,但 HIP 后端的 CPU 矩阵运算本身就比通用 CPU 版优化更好。
(那一刻的心情,大概就像你修了一个月的 bug,突然发现不是你的代码有问题,是别人的代码有问题。释然,但又有点不服气。)
但跑通只是开始。真正的问题就在眼前:
为什么这次没乱码?
真凶不是 MSVC——三层防线,但都不是铁证
按上一篇的排查逻辑,MLA 架构 + Windows + MSVC = 乱码。这次同样的配方,结果却不同。我必须给个说法。
先声明:目前没有一个 100% 的实验能证明"哪一层防住了 bug"。我只能在现象、版本历史、和已知 bug 之间做交叉定位。以下是我能找到的三个最有说服力的解释,按"嫌疑度"从高到低,外加一个可以 10 分钟验证的实验。
和上一篇相比,这次没有"真凶落网"的爽快结局——罪案现场确实没有乱码,但凶手留下了三组脚印,每一组都不完整。这不是讲故事偷懒,而是事实:证据不足时,把嫌疑名单列全、承认它们都不是铁证,本身就是一种严谨。侦探最怕的不是抓不到人,是抓错人。上一篇我们差点就把 MSVC 当成唯一的凶手——这一篇,我们先学会不轻信。
┌──────────── 三层防线 ─────────────┐
│ │
│ 第一层:架构隔离 (嫌疑度最高) │
│ deepseek4 是全新代码路径 │
│ 不经过 glm-dsa 那条 MSVC 敏感链 │
│ │
│ 第二层:版本迭代 │
│ b10276 相比报告 bug 的版本 │
│ 新了 189 个版本 │
│ 浮点/MLA/DSA 修复可能已合入 │
│ │
│ 第三层:–no-repack │
│ 主动跳过权重重打包 │
│ 规避已知的 tensor 布局 bug │
│ │
└───────────────────────────────────┘
第一层防线:架构隔离
GLM-5.2 的架构名是 glm-dsa。这是 llama.cpp 里后来加进来的专用分支——在既有 MLA absorbed 计算路径上,叠了一层 DSA Indexer 和双缓存。问题就出在那条对数值精度极度敏感的 absorbed 路径上:吸收矩阵 Q_nope_absorbed = W_kb × Q,一个 512×6144 的大矩阵乘,累加顺序稍微变一点,softmax 概率分布就全乱了。
而 DeepSeek-V4-Flash 的架构名是 deepseek4——这是一个全新的、独立的实现,有自己的底层算子集合,不经过 glm-dsa 那条链。
关键推理是现象层面的:两模型都在同一台 Windows 上用同一个 MSVC 编译的 llama.cpp 跑,一个乱码一个不乱。既然编译器和操作系统完全一致,差异只能出在代码路径上。glm-dsa 那套对 MSVC 浮点行为敏感的算子是首要怀疑对象——而 deepseek4 这条路,从头到尾没碰它。
而且 DeepSeek 家族(V2/V3 系列)的 MLA 路径在 llama.cpp 里打磨了两年多(DeepSeek-V2 发布于 2024 年 5 月),被无数 AMD/Windows 用户踩过。deepseek4 虽然新,但走的是同一条"祖传稳定"的主干。
第二层防线:版本迭代
上一篇我们卡在 b10107 / b10142。这一篇用的是 b10276——比报告 bug 时新了 189 个版本。
这 189 个版本里,llama.cpp 合入了大量可能影响浮点行为的修复:Indexer 逻辑、MLA 吸收矩阵、量化反量化路径、各后端的 buffer 管理…… 我不能确定具体是哪一个 commit 修好了什么(我甚至不确定是不是"修好了"),但版本前进本身就是一次概率修复。上次我们验证过"b10142 的 Indexer 修复没用",但 b10142 到 b10276 之间还隔着 189 个版本的累积变化。
第三层防线:–no-repack
注意我这次第一条命令里带了一个参数:–no-repack。上一篇的命令里没有它。
它的作用是:加载 GGUF 时跳过权重重打包,让 3D expert tensor(shape 类似 [4096, 2048, 256],最后一个维度是专家索引)保持文件里的原始内存布局。llama.cpp 为了某些后端的效率,有时会把 tensor 重新打包成更有利于内存访问的布局——大多数时候这是安全的,但 deepseek4 这种全新架构在刚合入的版本里,某些 fused ops 对 tensor 布局有硬性要求,repack 之后可能悄悄走了一条不同的计算路径。
我不确定这是不是关键。但既然它出现在我第一条命令里,而那次输出是对的,它就应该被记录在案。
把三条线收束成一条因果链
三层防线是并列的,但读者大概率想要一个"所以呢"的最终答案。把它们串成一条链,是这样的:
┌──────────── 最终因果链 ────────────┐
│ │
│ deepseek4 是全新架构 │
│ (不经过 glm-dsa 的 MSVC 敏感链) │
│ ↓ │
│ 加上 189 个版本的累积修复 │
│ ↓ │
│ 再加上 –no-repack 保持原始布局 │
│ ↓ │
│ MLA absorbed 路径无浮点崩塌 │
│ ↓ │
│ softmax 每次都选对了 token │
│ ↓ │
│ 输出全对,1.77 tok/s │
│ │
│ 但——因果链 ≠ 铁证 │
│ 这条链的每一环都是"至少没帮倒忙" │
│ 不是"必然是它救的" │
└────────────────────────────────────┘
这就是为什么我坚持要留那个回退实验:因果链只能说明"这条链上每个环节都正常",不能说明"换掉任一环节就一定会坏"。要证明哪一环是生死攸关的,只有一个办法——回退,看会不会坏。
留给你的验证实验
三层防线听起来很丰满,但科学精神要求可证伪。这三个解释里,第一层和第二层可以用一个 10 分钟的实验区分开:
实验:回退到 b10087(报告 GLM-5.2 bug 时的版本),
用同样的命令跑 DeepSeek-V4-Flash
→ 如果输出乱码 → 证明是「版本迭代」修好的
→ 如果输出正常 → 证明是「架构隔离」救的
→ 如果干脆加载失败 → 恭喜,你发现了一个新 bug
b10087 和 b10276 之间隔了几十个版本,期间 deepseek4 的支持一直在打磨,所以回退实验能干净地剥开第一层和第二层防线。如果你也好奇,并且 SSD 还有 144GB 空间——这个实验值得做。
(我在写完这篇文章之后才想到这个实验。所以别问我结果,顺手写进附录了。下一篇文章的素材,先预定。)
对了,如果你读完"三层防线"心里已经有了押注——你觉得哪一层嫌疑最大?评论区留个言,等有人真的做了 b10087 回退实验,我们回来对答案。
第二关:GPU 加速——一场连环车祸
CPU 能跑了,输出全对,但 1.66 tok/s 的 HIP 版还是太慢——1 秒蹦一个半 token,聊不了两句就睡着了。既然慢,就想快。我有 AMD Radeon 8060S 集显,虽然是集成的,但 Strix Halo 是 UMA 统一内存架构——CPU 和 GPU 共享 64GB 物理内存,没有"4GB 显存"的限制。
理论上,我可以把注意力计算放到 GPU 上,利用集显加速。
实际上,这是一场连环车祸,而且比上一篇的排查更残忍。上一篇至少每撞一次还能学到点东西;这一次的四连撞,前两撞连"学习"都算不上,纯粹是踩坑。
先给全貌,免得到处找:
┌────── 连环车祸全貌 ──────┐
│ │
│ 撞1: ngl=99 (全GPU) │
│ → OOM 崩溃 │
│ │
│ 撞2: ngl=3 (3层GPU) │
│ → CUDA assertion │
│ │
│ 撞3: Vulkan ngl=2 │
│ → 推理时崩溃 │
│ (ngl=1 反而能跑) │
│ │
│ 撞4: ngl=1 (仅输出层) │
│ → 1.21 tok/s 更慢 │
│ │
│ 逃逸: –cpu-moe + KV量化 │
│ → 1.77 tok/s ✅ │
└──────────────────────────┘
下面依次来。
车祸现场一:HIP 版 ngl=99(全 GPU,未加 –cpu-moe)
cudaMalloc failed: out of memory
alloc_tensor_range: failed to allocate ROCm0 buffer of size 154527282176
144GB 的模型想全部塞进 GPU?cudaMalloc 说不行。它只看到 4GB 的 AdapterRAM,不知道 UMA 架构下它可以借用全部 64GB 系统内存。
这个错误很有意思——不是不知道 144GB 放不下,而是它连 64GB 都没看见,只看见 4GB。Windows 驱动上报给 ROCm 的只有显卡专用显存那部分,UMA 的共享内存池根本没被识别。集显空有一身力气,愣是被人按着双手。
车祸现场二:HIP 版 ngl=3(3层 GPU)
加载成功!我兴奋了 70 秒。然后推理时:
GGML_ASSERT(buf->buft == ggml_backend_cuda_buffer_type(cuda_ctx->device)
&& "unsupported buffer type") failed
CUDA assertion 崩溃。不是内存不够,是 buffer type 不匹配——deepseek4 的某些 fused ops 在 HIP 后端上根本没实现,数据换手的时候就炸了。
车祸现场三:Vulkan 版 ngl=2
Lightning Indexer not supported, set to disabled
fused DeepSeek V4 HC pre/comb/post not supported, set to disabled
Vulkan 后端更惨——Indexer 不支持,HC pre/comb/post 都不支持。加载能过,推理就崩。
但这里有个值得记录的细节:Vulkan ngl=1(仅输出层)反而能跑,速度大概 1.2 tok/s 出头;ngl≥2 就崩。这说明 Vulkan 后端的问题在 attention 层附近——输出层它扛得住,一层 attention 就现原形。
车祸现场四:HIP 版 ngl=1(仅输出层 GPU)
Speed: 1.21 tok/s
比纯 CPU 还慢。集显的计算能力弱于 CPU,加上数据搬运开销,GPU 反而拖了后腿。
┌──────────── GPU offload 结果 ────────────┐
│ │
│ ngl=99 (全GPU) → OOM 崩溃 │
│ ngl=3 (3层GPU) → CUDA assertion 崩溃 │
│ ngl=2 (Vulkan) → 推理时崩溃 │
│ ngl=1 (Vulkan) → ~1.2 tok/s (能跑) │
│ ngl=1 (仅输出层) → 1.21 tok/s (更慢) │
│ ngl=0 (纯CPU) → 1.37 tok/s (基准) │
│ │
│ 结论:GPU offload 在当前版本不可用 │
│ "可用"的定义:不崩且比 CPU 快 → 0/6 │
│ │
└──────────────────────────────────────────┘
(四连撞。如果这是赛车游戏,我已经在排行榜最后一名了。但注意 Vulkan ngl=1 那条——它是断断续续的信号,说明后端不是全灭,只是没写完。)
(口径说明:上表中 ngl=0 一行的 1.37 是"普通 CPU 版"的基准,意在与它之上的各 GPU 配置做同版对照;正文里的 1.66 是 HIP 版 CPU-only 的基准,是参数调优表的参照点。两个基准因为后端优化差异本来就有约 21% 的速度差,此处各以其自身后端为参照,没有混用。)
第三关:翻 GitHub,找到真相
我不信这是硬件问题。64GB 统一内存的集显,怎么可能连几层 transformer 都跑不动?
去 llama.cpp 的 GitHub 一搜,发现了关键信息:
┌───────────── GitHub 线索 ─────────────┐
│ │
│ #26369 维护者 am17an 的跟踪 issue │
│ "Other backends support │
│ for DSV4 ops" │
│ HIP/Vulkan 适配状态: 未完成 │
│ │
│ #25582 双 RTX 3090 + CUDA │
│ 专家放 GPU → 输出逐渐劣化 │
│ 官方结论: 专家要放 CPU │
│ │
│ #26576 2× Quadro P5000 │
│ + –n-cpu-moe 43 │
│ = 跑通了 │
│ │
└───────────────────────────────────────┘
原来如此。不是我的硬件不行,是 deepseek4 的 GPU 后端还没写完。
那一刻我想笑又想骂。想笑,是因为四连撞终于有了交代——不是我蠢,是这版本就没写完;想骂,是因为如果能早半天来 GitHub 搜一下,那四连撞我一次都不用撞。这大概就是折腾的代价:有些弯路,只有自己走一遍,才记得住下次怎么绕开。
三条线索拼起来的图景非常清晰:
而恰恰是第二条线索最扎心——它值得单独展开,因为它比我的四连撞更反直觉。
最反直觉的一条:双 RTX 3090 也不行
报告 #25582 的兄弟有两张 RTX 3090——24GB × 2,48GB 显存,放 2020 年发售时也是顶配。他试着把专家放到 GPU 上,结果输出逐渐劣化:不是一步崩,而是越跑越歪,最后整个对话没法看。
注意这个"逐渐劣化"和我的"四连撞"是两种完全不同的坏法。我是"没写完直接崩",他是"写完了但算歪"。但官方结论殊途同归:专家要放 CPU。
这才是最扎心的地方。不是"Windows 的 MSVC 有毛病所以专家不能上 GPU",不是"AMD 集显太弱所以跑不动",而是 deepseek4 这个架构本身,在 llama.cpp 当前的实现里,MoE 专家就是设计给 CPU 跑的。连最成熟、最稳的 CUDA 后端都建议你:专家放 CPU。
类比一下:你花大价钱买了两台最强发动机(3090),结果修车师傅告诉你,这辆车的传动系统(deepseek4 的 GPU 实现)还没造好,发动机再强也只能当配重。不是发动机不行,是传动没跟上。我那四连撞,冤归冤,但至少证明被"半成品"绊倒的不止我一个。
而第三条线索给出了正确姿势的参数名:–cpu-moe(以及它的变体 –n-cpu-moe,指定放多少个专家到 CPU)。
第四关:–cpu-moe,柳暗花明
GitHub 上的线索指向同一个答案:–cpu-moe。把 MoE 专家留在 CPU,只把 attention 放 GPU。
等一下,这里要先解释一下这个参数为什么合理——不然你以为我是在念官方咒语:
为什么 –cpu-moe 是对的方向?
════════════════════════════════
MoE 专家的计算量 = 密集矩阵乘 (每个专家)
× 激活数量 (6专家/层 × 43层)
× 数据搬运 (SSD→内存→计算)
attention 的计算量 = 注意力打分 + 加权求和
(量级比专家小一个数量级)
CPU 最强的地方:内存带宽 + 大量本地缓存
集成 GPU 最强的地方:并行矩阵乘
→ 专家吃 I/O,CPU 的缓存和内存路径管够
→ attention 吃并行度,GPU 的并行计算单元管够
分工明确,才叫流水线。
于是:
llama-server -m model.gguf -c 128 -ngl 99 –cpu-moe -t 12 –no-repack
加载。等待。70 秒后:
Answer: Hi there! 👋 How can I help you today?
Speed: 1.71 tok/s
不崩溃了。 1.71 tok/s,比 HIP 版 CPU-only 的 1.66 快了 3%。不多,但至少 GPU 没拖后腿了——它终于做回了它擅长的部分。
这就是"柳暗花明"的感觉:前面四连撞,每一撞都以为是自己配置错了、硬件不行、Windows 有毒;结果真相在 GitHub 上一躺,一个参数就翻盘。翻盘的不是速度——1.71 比 1.66 也就快 3%——翻盘的是心态:从"这台机器不行"变成"是这版软件还没写完"。
再加 KV 缓存量化(把 KV cache 从 FP16 压到 INT8,腾出内存给专家缓存):
–cache-type-k q8_0 –cache-type-v q8_0
Speed: 1.77 tok/s
又快了一点,最终定格 1.77 tok/s。
参数调优:6 组配置全测
在找到最优解之前,我测了 6 组线程数和上下文长度的组合。别小看这张表,它用血泪证明了两件事:线程不是越多越好,上下文不是越短越好。
┌── 参数调优结果 ──────────────────────────┐
│ │
│ 配置 速度 │
│ ────────────────────────────────────── │
│ HIP CPU-only (t=12, c=128) 1.66 tok/s │ ← 基准
│ t=8, c=64 1.33 tok/s │
│ t=12, c=64 1.42 tok/s │
│ t=16, c=64 连接断开 │
│ t=20, c=64 1.05 tok/s │ ← 线程过多反而更慢
│ t=8, c=128 1.22 tok/s │
│ t=16, c=128 1.42 tok/s │
│ –cpu-moe (t=12, c=128) 1.71 tok/s │
│ –cpu-moe + KV量化 1.77 tok/s │ ← 最优
│ –cpu-moe (t=12, c=64) 1.44 tok/s │
│ │
│ 最终:1.77 tok/s │
│ 较 HIP CPU-only 基准提升:6.6% │
│ │
└──────────────────────────────────────────┘
发现:
- 12 线程是最优的(匹配物理核心数 12C),20 线程反而更慢——线程调度开销大于收益。这个机器的 CPU 是 12 核 24 线程,12 线程刚好一个物理核心一个,不用抢。
- 上下文 128 比 64 快——可能是更大的上下文减少了 KV cache 的频繁分配。
- –cpu-moe + KV 量化组合效果最好——分工 + 省内存,双管齐下。
其他试过但失败的参数
| –mlock | OOM | 144GB 模型试图锁进 64GB 内存 |
| –lazy-experts | 不支持 | 新版已移除此参数 |
| –flash-attn | 不兼容 | MLA 架构不支持 flash attention |
瓶颈分析:为什么到不了 3 tok/s?
上一篇里我估算 GLM-5.2 能到 3 tok/s——那个数字有两个过于乐观的假设:SSD 顺序读 2GB/s(实际 MoE 专家加载是随机读,只有 ~100MB/s),以及 95% 缓存命中率(实际只有 30-60%)。所以 3 tok/s 与其说是目标,不如说是一个"如果一切完美"的上限。DeepSeek-V4-Flash 更小(284B vs 753B),热数据也更少(3.5GB vs 13GB),理论上离那个上限更近。
但实测只有 1.77 tok/s。差距在哪?
答案在 I/O,不在计算。
┌──────────── 瓶颈分析 ─────────────┐
│ │
│ 每 token I/O: │
│ 激活专家 6 × 13.5MB = 81MB │
│ (分布在 144GB 文件的不同位置) │
│ │
│ SSD 随机读: │
│ NVMe Gen3/4 ~75-150 MB/s │
│ 实测瓶颈 ~100 MB/s │
│ │
│ 无缓存理论极限: │
│ 81MB ÷ 100MB/s = 0.81 秒/token │
│ → 约 1.2 tok/s │
│ │
│ 实测 1.77 tok/s(每 token ~0.56s)│
│ 每 token 最多从磁盘读 ~56MB │
│ → 约 31% 的数据命中页缓存 │
│ │
│ 要到 3 tok/s 需要: │
│ 每 token 只有 ~0.33s 给磁盘 │
│ 100MB/s × 0.33s ≈ 最多读 33MB │
│ 剩余 48MB 必须靠缓存 │
│ → 缓存命中率需达到 ~59% │
│ │
└────────────────────────────────────┘
瓶颈不是 CPU 计算,不是 GPU 能力,是 SSD 随机 I/O。
而这个结论,上一篇其实已经露出过马脚——当时 HIP 版加载比纯 CPU 版快 6 倍(2.5 分钟 vs 15 分钟),推理速度却几乎一样(3.03 vs 2.94 tok/s)。原因正是:加载阶段吃满 I/O,靠后端优化发力;推理阶段卡在页缓存和随机读上,后端再快也白搭。这一篇换了模型、换了架构、换了 Bug,最后撞的却是同一堵墙:SSD 随机读的物理极限。这不是巧合,这是 MoE 大模型本地部署的宿命——你再怎么优化算力,也绕不开"每次生成一个 token 都要把激活的那几个专家从磁盘里捞出来"这件事。
那缓存命中率为什么上不去?算给你看:
┌──────────── 缓存容量计算 ─────────────┐
│ │
│ 专家槽位: 256 专家 × 43 层 │
│ = 11,008 个 │
│ │
│ 64GB 内存里能放多少? │
│ 系统 ~20GB + 模型基础权重 ~5GB │
│ → 剩 ~38GB 给专家缓存 │
│ │
│ 38GB ÷ 13.5MB/专家 ≈ 2,815 个 │
│ │
│ 命中率 = 2,815 / 11,008 ≈ 25.6% │
│ (LRU 加持 + 实测反推 → ~31%) │
│ │
│ 如果内存是 128GB: │
│ 102GB ÷ 13.5MB ≈ 7,555 个 │
│ → 动态命中率约 51% │
│ → 速度大概 2.5 tok/s │
│ │
│ 如果内存是 256GB: │
│ → 4+ tok/s │
│ │
│ 结论:内存就是正义 │
└───────────────────────────────────────┘
(内存就是正义。这句话我上一篇就想说了,但当时数据不够硬。现在你可以拿去跟人争论了:64→128GB,速度 1.77→2.5;128→256,速度 2.5→4+。每 64GB 内存,大约值 0.8 tok/s。)
两个架构的对比
折腾完这两篇,终于有条件把两个架构摆在同一张桌子上比了:
| 总参数 | 753B | 284B |
| 专家数 | 256 | 256 |
| 激活专家 | 8/层 | 6/层 |
| 每专家大小 | 20.25MB | 13.5MB |
| 每 token I/O(单层激活量) | ~162MB | ~81MB |
| 架构 | glm-dsa | deepseek4 |
| MLA | ✅ | ✅ |
| Indexer | DSA Lightning | Lightning Indexer |
| Windows CPU | ❌ 乱码 | ✅ 正常 |
| Windows GPU | ❌ 乱码 | ⚠️ 需 –cpu-moe |
| 实测速度 | 3 tok/s (理论,乱码) | 1.77 tok/s (实测,正常) |
关键差异有三条:
给你的建议
你的硬件 → 方案
═══════════════════════════════════════════════
🟢 NVIDIA GPU (24GB+) → KTransformers + CUDA
MoE专家CPU + attention GPU,已验证 11 tok/s
🟢 Mac (256GB UMA) → Metal + llama.cpp
统一内存天然适合,Metal 路径成熟
🟢 Linux CPU-only → 可用
无 MSVC 浮点问题,无 HIP assertion 问题
🟡 Windows + 64GB + AMD集显 → 本文方案
–cpu-moe + –no-repack + KV量化
1.77 tok/s,输出正确
注意别开 GPU offload,会崩
🟡 25GB+ 纯 CPU → Colibri
能跑通,0.05 tok/s,教学意义大
⚪ AMD 集显 + 想等更快的 → 等 deepseek4 后端写完
当前 1.77 tok/s 已是可用状态
适配完成去掉 –cpu-moe,GPU 全力有望翻倍
启动命令
llama-server -m DeepSeek-V4-Flash-0731-UD-Q4_K_XL-00001-of-00005.gguf ^
-c 128 -ngl 99 –cpu-moe -t 12 ^
–port 8081 –no-repack –host 0.0.0.0 ^
–cache-type-k q8_0 –cache-type-v q8_0
几个参数的用处,怕你手痒乱删:
| –cpu-moe | 专家留在 CPU | GPU offload → assertion 崩溃 |
| –no-repack | 保持原始 tensor 布局 | 可能走不同的计算路径(未验证) |
| -t 12 | 匹配 12 物理核心 | 20 线程实测更慢 |
| –cache-type-k/v q8_0 | KV 量化省内存 | 慢约 3%(1.77 → 1.71) |
已经试过但没走通的路
WSL2:一个"差点就成了"的弯路
Windows 的 MSVC 有浮点问题?那就绕开它——装 WSL2,在 Ubuntu 里跑 Linux 版 llama.cpp。这个思路理论上完美:绕开 MSVC,绕开 HIP,直接在 Linux 的 GCC 上跑,顺便还能复现 GitHub 上"HIP 支持未完成"的问题是不是 Windows 特有。
然后现实给了我一巴掌:WSL2 里编译出来的版本是 b5463696——一个旧版本,不支持 deepseek4 架构。
(b5463696。这个数字我背得比自己的生日还熟。WSL2 里 apt 装的 llama.cpp 就是那个版本,官方仓库还没跟进。你想 update?好,clone 源码自己编。依赖装到一半,网络超时。重试。又超时。GitHub 国内速度,你懂的。)
路线没错,时机不对。WSL2 方案留给"等 llama.cpp 的 Linux 版更新到支持 deepseek4"之后。
页面文件:和操作系统的斗智斗勇
第一步:打开「系统属性 → 高级 → 性能 → 虚拟内存」
第二步:选自定义大小,设初始 64GB / 最大 128GB
第三步:重启
第四步:打开虚拟内存一看——
「由系统自动管理」← 被系统"贴心"地改回来了
或者:显示 60GB,我设的 128GB 呢?
Windows 有个很倔的脾气:自动管理虚拟内存时,它会给你一个"足够用"的数字(比如 60GB),但你要手动设更大的,它反而会在重启后悄悄覆盖回自己的判断——有时候是直接改回自动管理,有时候是把你设的 128GB 圆成一个它觉得合理的 60GB。
我试过注册表,试过命令行 wmic,试过硬核的 fsutil…… 每一步都像在和系统玩"我偏不"的游戏。最终部分成功:页面文件确实扩大了,但远没到 128GB 的理想值。缓存命中率没起来多少,这 1.66 tok/s 到 1.77 tok/s 的最终提升,还是靠 –cpu-moe 和 KV 量化拉开的。
(有朋友在评论区问"为什么要扩页面文件"——因为想给 mmap 更多"假内存"去缓存专家页面。Windows 默认的页面文件大小,对 144GB 的模型来说太抠了。可惜这局我输了。)
一张完整的失败清单
| WSL2 + Linux llama.cpp | ❌ | WSL2 里编译的版本太旧(b5463696),不支持 deepseek4 架构 |
| 页面文件扩到 128GB | ⚠️ 部分成功 | Windows 自动管理限制在 60GB,手动设置被系统覆盖 |
| –mlock 锁定内存 | ❌ OOM | 144GB 模型锁进 64GB 内存,直接爆 |
| Vulkan 后端 | ❌ | deepseek4 的 fused ops 在 Vulkan 上不支持,ngl≥2 推理时崩溃 |
| Ollama | ❌ | 不支持多分片 GGUF,合并后磁盘空间不足 |
后续能做的
| 等 llama.cpp 完成 HIP/Vulkan deepseek4 适配 | GPU offload 不再崩溃 | ⭐ 等 |
| 增大页面文件到 128GB(换工具,绕过 Windows 的倔强) | 缓存命中率提升,可能到 2+ tok/s | ⭐⭐ |
| 换更快的 SSD (Gen5) | 随机读 300MB/s,可能到 2.5 tok/s | ⭐⭐⭐ |
| 提 GitHub issue | 推动官方修复 GPU 后端 | ⭐ |
| 等 KTransformers 支持 deepseek4 | 可能突破 5+ tok/s | ⭐⭐ |
| b10087 回退实验 | 区分"架构隔离"还是"版本迭代" | ⭐ |
给你的机器正名:64GB 集显不是废铁
读到这里的,大概率跟我是同款 Strix Halo。上一篇我说过这台机器被低估——这一篇它真的干成了:在 GPU offload 全线崩溃的情况下,纯靠 CPU + 统一内存,把 144GB 的模型跑出了正确输出。
这听起来像个反讽:“折腾了半天 GPU,最后根本没用上 GPU?”——但恰恰说明了一件事:
┌────── 这台机器真正干成了什么 ──────┐
│ │
│ 144GB 模型 / 64GB 内存 │
│ ↓ │
│ mmap 按需加载 + LRU 专家缓存 │
│ ↓ │
│ 每 token 只从磁盘捞 ~56MB 专家 │
│ ↓ │
│ 输出全对,1.77 tok/s │
│ │
│ 对比:24GB 独显 + KTransformers │
│ 需要额外框架处理专家卸载 │
│ Strix Halo 统一内存天然适合 mmap │
└─────────────────────────────────────┘
那些觉得"64GB 跑不了大模型"的人,大概没意识到一件事:统一内存 + mmap 就是为"模型大于内存"这种场景设计的。24GB 独显想啃 144GB 也不是不行——KTransformers 就是干这个的,但你要额外搭一套框架,专门负责把专家卸载到 CPU。而 Strix Halo 的 64GB 是 CPU 和 GPU 共享的内存墙,配合按需加载,墙可以"往后挪"。
GPU offload 四连撞,不丢人。那是软件的锅,不是机器的锅。等 deepseek4 的 HIP 后端写完,去掉 –cpu-moe 再测一次——这台机器的翻身仗还没打完。
写在最后
上一篇结尾我说:“435GB,安安静静地躺在 SSD 里。等待那个 bug 被修好的早晨。”
现在,那个早晨还没来。GLM-5.2 的 bug 还在,Issue #26027 还是 open。
但我换了一个模型,用另一种方式,在同一台笔记本上,跑通了一个 284B 的推理模型。
1.77 tok/s。不快。但每一个 token 都是正确的。中英文都对,推理都准。
这台 Strix Halo 笔记本,64GB 内存,没有独显,没有集群,没有 Mac Studio——但它能跑 144GB 的大模型,能正确输出,能在 1 秒内给你一个完整的回答。更重要的是,它这次跑通,靠的不是"等 bug 被修好",而是弄明白了藏在 bug 背后的原理,然后换一条路绕过去。
有时候"能不能跑"比"跑多快"更重要。有时候"知道为什么能跑"比"能跑"更重要。
而这一次,我还想补一个比"跑通"更值钱的道理:上一篇我们轰轰烈烈地"锁定了真凶",这一篇我们发现,同样的证据能拼出三种解释,而凶手可能只是恰好路过的另一个 bug。真正的功力,从来不是敢下结论,而是每下一个结论,都留一个能戳穿它的实验——这就是我留着后面那个回退实验的原因。它比这整篇文章的结论都重要。
这也是为什么我愿意把这 12 小时原原本本写下来:参数抄走很容易,但我想让你带走的,不是 1.77 这个数字,而是"这条路我也能走一遍"的底气。技术文章最好的结局,不是读者记住了结论,而是读者合上文章,真的去试了一下。
至于等 bug 被修好的那个早晨——我还在等,但不等 GLM-5.2 了。我在等的是 deepseek4 的 HIP/Vulkan 后端写完的那个早晨。到那天,同样的命令,去掉 –cpu-moe,GPU 全力开工,这个 1.77 tok/s 应该有希望再翻一倍。
(至于那个 CUDA assertion 的 bug,我已经提了 issue。如果修了,也许下一篇博客的标题是:《GPU 加速终于不崩了!64GB 笔记本跑 284B 模型突破 3 tok/s》。)
📋 速读版:30秒了解全貌(点击展开)
如果你只有半分钟,以下是这篇文章的全部干货。想看排查过程、技术细节和情绪起伏的,往上翻。
🔧 附录:完整测试数据 & 复现命令 & 工具脚本(点击展开)
完整测试结果
| 普通CPU版 (t=12, c=128) | ✅ 正确 | 1.37 tok/s | 首战告捷 |
| HIP CPU-only (t=12, c=128) | ✅ 正确 | 1.66 tok/s | 快 21% |
| HIP ngl=99 (全GPU) | OOM | — | UMA 未识别 |
| HIP ngl=3 | assertion 崩溃 | — | fused ops 未实现 |
| Vulkan ngl=1 | ✅ 能跑 | ~1.2 tok/s | 输出层没问题 |
| Vulkan ngl=2 | 推理崩溃 | — | attention 层 fused ops 不支持 |
| HIP ngl=1 (仅输出层) | ✅ 正确 | 1.21 tok/s | 更慢,GPU 拖后腿 |
| t=8, c=64 | ✅ 正确 | 1.33 tok/s | |
| t=12, c=64 | ✅ 正确 | 1.42 tok/s | |
| t=16, c=64 | 连接断开 | — | |
| t=20, c=64 | ✅ 正确 | 1.05 tok/s | 线程过多更慢 |
| t=8, c=128 | ✅ 正确 | 1.22 tok/s | |
| t=16, c=128 | ✅ 正确 | 1.42 tok/s | |
| –cpu-moe (t=12, c=128) | ✅ 正确 | 1.71 tok/s | 柳暗花明 |
| –cpu-moe + KV量化 | ✅ 正确 | 1.77 tok/s | 最优 |
| –cpu-moe (t=12, c=64) | ✅ 正确 | 1.44 tok/s | |
| –mlock | OOM | — | 144GB > 64GB |
| –lazy-experts | 不支持 | — | 已移除 |
| –flash-attn | 不兼容 | — | MLA 不支持 |
复现命令
# 最优配置
llama-server -m DeepSeek-V4-Flash-0731-UD-Q4_K_XL-00001-of-00005.gguf ^
-c 128 -ngl 99 –cpu-moe -t 12 ^
–port 8081 –no-repack –host 0.0.0.0 ^
–cache-type-k q8_0 –cache-type-v q8_0
# 测试请求
curl http://localhost:8081/v1/chat/completions \\
-H "Content-Type: application/json" \\
-d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"hi"}],"max_tokens":10}'
# 测速(同上的请求,解析出 tok/s,预期 1.77 左右)
curl -s http://localhost:8081/v1/chat/completions \\
-H "Content-Type: application/json" \\
-d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"hi"}],"max_tokens":10}' \\
| python -c "import sys,json;print('tok/s:',json.load(sys.stdin)['timings']['predicted_per_second'])"
每个参数的作用
| -c 128 | 上下文长度 | 测试用,省内存 |
| -ngl 99 | 全部层尝试 GPU | 测试 GPU offload |
| –cpu-moe | 专家留 CPU | 官方推荐的正确姿势 |
| –no-repack | 跳过权重重打包 | 规避可能的 tensor 布局 bug |
| -t 12 | 线程数 | 匹配 12 物理核心 |
| –cache-type-k/v q8_0 | KV 量化 | 省内存给专家缓存 |
验证实验(写给自己和读者)
# 回退到 b10087(报告 GLM-5.2 bug 时的版本),验证「三层防线」中哪层是真的
# 预期结果见正文「留给你的验证实验」一节
git clone –branch b10087 https://github.com/ggml-org/llama.cpp
工具脚本
GGUF 解析(同上一篇)
from gguf import GGUFReader
reader = GGUFReader('model.gguf', mode='r')
for tensor in reader.tensors:
print(f"{tensor.name}: shape={tensor.shape}, dtype={tensor.dtype}")
快速测速
curl -s http://localhost:8081/v1/chat/completions \\
-H "Content-Type: application/json" \\
-d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"hi"}],"max_tokens":10}' \\
| python -c "import sys,json;print('tok/s:',json.load(sys.stdin)['timings']['predicted_per_second'])"
如果你读到了这里,这篇文章对你大概不止是"刷到一条推送"。
上一篇我说"等 bug 被修好的早晨"。这一篇我自己动手,绕过了那个 bug。有时候等待是对的,但更多时候,理解问题本身,然后绕过去,才是更快的路。
如果你也在折腾本地大模型,评论区聊聊你的配置和踩过的坑——看到会回。如果 #26369 或者我的 CUDA assertion issue 有进展,我会在评论区更新。
觉得有用的话,点个赞我大概能感知到(精神上的)。然后,请转发给同样在跟 144GB 模型较劲的群友——技术排查这种事,多一个人知道,少一个人撞墙。
至于那 435GB 的 GLM-5.2,它还在 SSD 里躺着。但这次我不等了——因为我已经跑起来了。
谢谢你能阅读到这里。我们下一篇博客再见吧~
#DeepSeek-V4-Flash #llama.cpp #MoE #大模型本地部署 #284B #Windows #AMD #HIP #CPU-MOE


