欢迎光临
我们一直在努力

别等了!那个 bug 没修,我 64G 内存换个模型,144G 大模型照样跑起来了

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 搜一下,那四连撞我一次都不用撞。这大概就是折腾的代价:有些弯路,只有自己走一遍,才记得住下次怎么绕开。

三条线索拼起来的图景非常清晰:

  • HIP/Vulkan 对 deepseek4 的支持是半成品(#26369)——模型能加载,但推理时某些 ops 会触发 assertion。这是最直接的证据,我的四连撞全在这条解释里。
  • 连 CUDA 也不行(#25582)——注意,是"输出逐渐劣化",不是崩溃。这说明即使在全家桶最成熟的 CUDA 后端上,把专家放 GPU 都会导致数值问题。
  • 有人用老破卡跑通了(#26576)——Quadro P5000,2016 年的卡,16GB 显存,靠 –n-cpu-moe 43(专家全 CPU、attention 在 GPU)跑通了。
  • 而恰恰是第二条线索最扎心——它值得单独展开,因为它比我的四连撞更反直觉。

    最反直觉的一条:双 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。)


    两个架构的对比

    折腾完这两篇,终于有条件把两个架构摆在同一张桌子上比了:

    GLM-5.2DeepSeek-V4-Flash
    总参数 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 (实测,正常)

    关键差异有三条:

  • GLM-5.2 的 MLA 在 Windows 上有浮点精度 bug(MSVC vs GCC),deepseek4 的 MLA 实现没有这个问题。这是本篇最大的谜题,前文已用"三层防线"拆解。
  • deepseek4 的 GPU 后端(HIP/Vulkan)还没完成,需要 –cpu-moe 绕过。讽刺的是:glm-dsa 那边 GPU 路径是坏的(HIP 算错),deepseek4 这边 GPU 路径是"没写完"(HIP 崩)。同样是坏,坏法不一样。
  • DeepSeek-V4-Flash 专家更小、激活更少(3.5GB vs 13GB),单层 per-token I/O 从 8×20.25=162MB 降到 6×13.5=81MB,理论上应该更快——但实测差距不大(都是 1.77 / 3 这个量级)。这说明两个模型遇到的瓶颈是同一个:SSD 随机读的物理极限,而不是参数规模本身。

  • 给你的建议

    你的硬件 → 方案
    ═══════════════════════════════════════════════
    🟢 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秒了解全貌(点击展开)

    如果你只有半分钟,以下是这篇文章的全部干货。想看排查过程、技术细节和情绪起伏的,往上翻。

  • 跑通了:同一台 64GB 笔记本(AMD Strix Halo),换 DeepSeek-V4-Flash(284B MoE,144GB GGUF),CPU-only 就正确输出(1.37 tok/s)——同样的配方,上次乱码,这次全对。
  • 为什么没乱码:三层防线——① deepseek4 是全新代码路径,绕开 glm-dsa 的 MSVC 敏感链;② b10276 比报告 bug 时新了 189 个版本;③ –no-repack 跳过重打包。三者都无铁证,留了回退 b10087 的验证实验。
  • GPU 四连撞:ngl=99 OOM → ngl=3 assertion 崩 → Vulkan ngl≥2 崩 → ngl=1 更慢(1.21)。真相在 GitHub:deepseek4 的 HIP/Vulkan 后端没写完,连 CUDA 都建议专家放 CPU(#26369 / #25582 / #26576)。
  • 最终方案:llama-server -m model.gguf -c 128 -ngl 99 –cpu-moe -t 12 –no-repack –cache-type-k q8_0 –cache-type-v q8_0 → 1.77 tok/s,输出正确。
  • 瓶颈不是算力:每 token 要从 SSD 随机读 81MB 专家数据(~100MB/s)。内存就是正义:128GB → ~2.5 tok/s,256GB → 4+ tok/s。

  • 🔧 附录:完整测试数据 & 复现命令 & 工具脚本(点击展开)

    完整测试结果

    测试输出速度结论
    普通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

    赞(0)
    未经允许不得转载:171主机测评 » 别等了!那个 bug 没修,我 64G 内存换个模型,144G 大模型照样跑起来了
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址