2026年了,64GB 内存真的能跑 753B 大模型吗?
起因
刷到 Unsloth 发布了 GLM-5.2 的 GGUF 量化版,753B 参数,256 个专家。然后我看了一眼自己的笔记本——AMD Strix Halo,64GB 内存。
按常理,753B 模型光权重就要 375GB(FP16),64GB 内存连零头都不够。但 MoE 模型有个特性:每 token 只激活 8 个专家,实际参与计算的参数量只有总参数的 1% 左右。
如果用 mmap 按需加载,配合 LRU 缓存热专家……理论上,64GB 跑 753B 是可能的。
于是开干。结论先说:Windows 上跑不通。 但排查过程中有不少值得记录的发现,写下来给同样在折腾的人参考,也希望能吸引到懂 llama.cpp 底层的大佬帮忙看看。
一、环境
| 组件 | 规格 |
|---|---|
| CPU | AMD Ryzen AI MAX+ 392 (Strix Halo, Zen5, 12核24线程) |
| GPU | AMD Radeon 8060S (集成, RDNA 3.5, 本次未使用) |
| 内存 | 64GB DDR5 |
| 硬盘 | 2x 1TB NVMe SSD |
| 系统 | Windows 11 |
| llama.cpp | b10107 (commit c0bc8591e) |
模型:GLM-5.2 UD-Q4_K_XL(Unsloth Dynamic 量化),11 个分片,435GB。
这里有个关键细节:Strix Halo 是 UMA 架构,CPU 和 GPU 共享 64GB 内存。但我故意不用 GPU,全程 -ngl 0 纯 CPU 推理。原因很简单——GLM-5.2 在 GPU 路径上已经有已知 bug(Issue #26027),我需要先确认 CPU 路径是否正常。
结果?CPU 路径也有问题。 这是后话。
二、为什么 64GB 能跑 753B?
在开始测试之前,先解释一下为什么这个看似不可能的组合实际上是可以工作的。理解了这一点,后面的速度分析才有意义。
2.1 MoE 的稀疏激活
GLM-5.2 的架构很特殊。前 3 层是普通的 dense transformer,从第 4 层开始变成 MoE(Mixture of Experts)。每层有 256 个专家,但每次只激活 8 个。这意味着:
dense 层参数: 3 层 x 约 200M = 约 600M
MoE 层参数: 76 层 x 8 个专家 x 21.75MB/专家 + 共享部分 = 约 15GB
总计: 约 15-20 GB 的激活参数
753B 的模型,实际参与计算的只有 15-20GB。这就是 MoE 的魔力。
2.2 mmap + LRU 缓存
llama.cpp 使用 mmap(memory-mapped file)加载模型。操作系统把文件映射到虚拟地址空间,但不立即读取数据。只有当程序实际访问某个地址时,才从磁盘加载对应的页面。
对于 MoE 模型:
- 共享权重(embedding、attention、layer norm)被频繁访问,自然留在内存中
- 热门专家的权重被频繁访问,形成 LRU 缓存
- 冷门专家的权重可能永远不会被加载
实测 3 tok/s 的速度,说明缓存命中率约 95%——大部分专家数据都从内存缓存中读取,不需要从 SSD 加载。
2.3 速度理论模型
| 缓存命中率 | 实际 I/O | 预期速度 |
|---|---|---|
| 0% (全冷) | 13.2 GB/token | 0.15 tok/s |
| 80% | 2.6 GB/token | 0.75 tok/s |
| 95% | 0.66 GB/token | 3 tok/s |
| 99% | 0.13 GB/token | 5+ tok/s |
计算过程:每 token 激活 8 个专家 x 76 MoE 层 x 21.75 MB/专家 = 13.2 GB。SSD 顺序读取约 2 GB/s,全冷加载需要 6.6 秒/token。95% 缓存命中率只需 0.66 GB,约 0.33 秒/token。
三、模型加载:第一次激动
启动命令:
llama-server -m GLM-5.2-UD-Q4_K_XL-00001-of-00011.gguf -c 256 -ngl 0 -t 22 --port 8080
2 分 29 秒后,看到这行日志:
2.29.877.692 I srv llama_server: model loaded
2.29.877.858 I srv llama_server: listening on http://127.0.0.1:8080
内存占用 43GB。435GB 的模型,只用了 43GB 就加载完成——mmap 的按需加载确实生效了。
但日志里有一大堆 warning:
0.00.921.030 W model has unused tensor blk.78.attn_norm.weight -- ignoring
0.00.921.134 W model has unused tensor blk.78.ffn_gate_exps.weight (1811939328 bytes) -- ignoring
0.00.921.140 W model has unused tensor blk.78.ffn_down_exps.weight (2214592512 bytes) -- ignoring
0.00.921.145 W model has unused tensor blk.78.ffn_up_exps.weight (1811939328 bytes) -- ignoring
...(blk.78 的 26 个 tensor 全部被跳过)
blk.78 是 NextN(Multi-Token Prediction)草稿层。NextN 是一种推测解码技术——模型在生成一个 token 的同时,预测接下来的多个 token,然后验证这些预测是否正确。如果正确,就可以一次生成多个 token,大幅提升速度。但当前版本的 llama.cpp 尚未完全支持 GLM-5.2 的 NextN 功能,所以这些 tensor 被跳过了。
跳过 NextN 层不影响主模型的推理正确性,只影响速度(没有推测解码加速)。
看到 model loaded 的那一刻,我是真的激动了。
四、推理测试:从激动到困惑
4.1 中文测试
{
"messages": [{"role": "user", "content": "你好"}],
"max_tokens": 30,
"temperature": 0.1
}
返回:
{
"choices": [{
"message": {
"content": "",
"reasoning_content": "??????????????????????????????"
}
}],
"timings": {
"prompt_per_second": 4.16,
"predicted_per_second": 3.03
}
}
速度到了——3 tok/s。 但输出全是问号。
注意两个细节:
- content 为空,reasoning_content 全是问号:GLM-5.2 是推理模型,会先在 reasoning_content 中"思考",再在 content 中输出回答。但这里 reasoning_content 全是
?,content 为空——模型完全没有产生有意义的输出。 - cache_n = 11:有 11 个 token 命中了 KV cache,说明 prompt 的部分内容被缓存了。
4.2 英文测试
{"messages": [{"role": "user", "content": "hi"}], "max_tokens": 10}
返回:"reasoning_content": "??????????"
英文也是乱码。 排除了中文 tokenizer 的嫌疑。
4.3 Raw completion(绕过 chat template)
{"prompt": "The capital of France is", "n_predict": 20}
同样是乱码。排除了 chat template 的嫌疑。
4.4 纯 CPU 版测试
为了排除 HIP 库的干扰,下载了 llama.cpp 的纯 CPU 版(17.4MB)重新测试:
Prompt processing: 10.67 tok/s
Token generation: 2.94 tok/s
Output: ??????????????????
纯 CPU 版也是乱码。 说明问题不在 HIP 库,而在 llama.cpp 的 GLM-5.2 推理核心代码。
这里需要特别说明:纯 CPU 版的加载时间从 HIP 版的 2.5 分钟变成了 15 分钟。这是因为 HIP 版使用了 Zen4 优化的 CPU 后端(ggml-cpu-zen4.dll),而纯 CPU 版使用的是通用 x64 后端(ggml-cpu-x64.dll),计算效率低很多。但推理速度差异不大(3.03 vs 2.94 tok/s),说明瓶颈在 I/O 而不在计算。
这时候我有两个猜测:
- Tokenizer 坏了(中文词表丢失?)
- 推理引擎有 bug
我先去排查 tokenizer——因为如果 tokenizer 坏了,修起来简单;如果是引擎 bug,那就麻烦了。
五、排查 Tokenizer:一场意外的发现
5.1 看起来像是词表损坏
查看 GGUF 的 tokenizer 元数据:
tokenizer.ggml.model: gpt2
tokenizer.ggml.pre: glm4
vocab_size: 154,880
merges: 321,649
154,880 个 token 中,0 个 CJK 字符。
第一反应:完了,中文词表丢了。这不就是乱码的原因吗?
但仔细一想,不对。如果中文词表真的丢了,那 llama-tokenize 也应该报错才对。试了一下:
$ llama-tokenize -p "你好"
109377 -> '你好'
Tokenizer 完全正常。 “你好” 被正确编码成了 token 109377。
这是怎么回事?词表里 0 个 CJK 字符,但中文编码完全正常?
5.2 Byte-Level BPE:一个容易被误解的机制
查了 HuggingFace 上 GLM-5.2 的 tokenizer.json(19.7MB),发现了关键线索:
{
"pre_tokenizer": [
{"type": "Split", "pattern": {"Regex": "..."}},
{"type": "ByteLevel", "add_prefix_space": false}
]
}
GLM-5.2 使用的是 GPT-2 风格的 byte-level BPE。
这种 tokenizer 不在 Unicode 字符级别工作,而是在字节级别工作。核心思想是:把每个 UTF-8 字节映射到一个 printable Unicode 字符,然后在这些字符上做 BPE 合并。这样做的好处是:
- 词表大小固定(基础词表只有 256 个字节),不需要 UNK token
- 任何 Unicode 字符都可以表示(包括 emoji、CJK、特殊符号)
- 语言无关,同一个 tokenizer 可以处理任何语言
具体流程:
输入: "你好"
|
UTF-8 编码: [0xE4, 0xBD, 0xA0] (3 个字节)
|
Byte-to-Unicode 映射:
byte 0xE4 -> chr(0x00E4) (a with diaeresis)
byte 0xBD -> chr(0x00BD) (vulgar fraction one half)
byte 0xA0 -> chr(0x0142) (l with stroke)
|
BPE 合并: 查 merges 表,3 个字符合并成 1 个 token
|
结果: [109377]
所以词表里没有 CJK 字符是正常的! 中文字符通过 UTF-8 字节编码,每个字节映射到一个 Latin 字符,然后在 Latin 字符级别做 BPE 合并。这就是 byte-level BPE 的设计。
5.3 彻底验证
写了个 Python 脚本,从 HF 的 tokenizer.json 加载词表,逐条对比 GGUF 的词表:
from gguf import GGUFReader
import json
# 加载 GGUF tokens
reader = GGUFReader('GLM-5.2-UD-Q4_K_XL-00001-of-00011.gguf', mode='r')
gguf_tokens = [reader.fields['tokenizer.ggml.tokens'].parts[i]
for i in reader.fields['tokenizer.ggml.tokens'].data]
# 加载 HF tokens
with open('tokenizer.json', 'r', encoding='utf-8') as f:
hf_vocab = json.load(f)['model']['vocab']
hf_token_list = [t[0] for t in sorted(hf_vocab.items(), key=lambda x: x[1])]
# 对比
mismatches = sum(1 for i in range(min(len(gguf_tokens), len(hf_token_list)))
if gguf_tokens[i] != hf_token_list[i].encode('utf-8'))
print(f"GGUF tokens: {len(gguf_tokens)}")
print(f"HF tokens: {len(hf_token_list)}")
print(f"Mismatches: {mismatches}")
输出:
GGUF tokens: 154,880
HF tokens: 154,820
Mismatches: 0
词表顺序完全一致。 GGUF 比 HF 多 60 个 special tokens,核心词表没有任何错位。
又用 llama-tokenize 测试了更多用例:
$ llama-tokenize -p "GLM-5.2是一个大语言模型"
3825 -> 'GLM' 44 -> '-' 12 -> '5' 20 -> '.' 13 -> '2'
99950 -> '是一个' 98324 -> '大' 100132 -> '语言' 100484 -> '模型'
$ llama-tokenize -p "人工智能"
104668 -> '人工智能'
$ llama-tokenize -p "今天天气真好"
99637 -> '今天' 101791 -> '天气' 117913 -> '真好'
Tokenizer 完全正常。 中英文都能正确编码,BPE 合并也工作正常。
结论:Tokenizer 排查完毕,不是问题。问题在推理引擎。
六、定位 Bug:从 GitHub 到实锤
6.1 找到 Issue #26027
搜索 GitHub,发现 3 天前(2026-07-23)有人提交了一个 issue:
Issue #26027: GLM-5.2 (glm_moe_dsa) dense-MLA CUDA path produces corrupted output for ANY real transformer layer offloaded to GPU
报告者用 2 张 RTX PRO 6000(各 96GB)测试,发现:
| 模式 | 结果 |
|---|---|
-ngl 0 (CPU only) |
正确 |
-ngl 1 (output layer on GPU) |
正确 |
-ngl 2 (output + MTP layer on GPU) |
正确 |
-ngl 3 (first real transformer layer on GPU) |
乱码 |
关键结论:只要有任何一个真正的 transformer 层在 GPU 上,输出就损坏。 CPU-only 模式是正常的。
6.2 但我们的情况更严重
Issue #26027 说 CPU-only 是正常的。但我们的测试显示:
| 环境 | 平台 | 量化 | 模式 | 结果 |
|---|---|---|---|---|
| Issue #26027 | Linux | UD-IQ2_M | CPU only | 正确 |
| Issue #26027 | Linux | UD-IQ2_M | 部分 GPU | 乱码 |
| 我们 | Windows | UD-Q4_K_XL | CPU only (HIP) | 乱码 |
| 我们 | Windows | UD-Q4_K_XL | CPU only (纯CPU) | 乱码 |
三个可能的差异点:
- 平台: Linux vs Windows
- 量化: UD-IQ2_M vs UD-Q4_K_XL
- 构建: CUDA build vs HIP/CPU build
我个人倾向于认为是 Windows 平台的浮点行为差异 导致的。MSVC 和 GCC/Clang 在浮点精度、舍入模式、NaN 处理上都有微妙差异,而 MLA 的计算涉及大量矩阵乘法和低秩投影,对数值精度非常敏感。
七、硬核发现:排查过程中的关键洞察
7.1 Expert Tensor 是 3D 打包的
分析 GGUF 的 tensor 结构时,发现了一个重要细节:
blk.N.ffn_gate_exps.weight: shape=[6144, 2048, 256] -> 1728 MB
blk.N.ffn_up_exps.weight: shape=[6144, 2048, 256] -> 1728 MB
blk.N.ffn_down_exps.weight: shape=[2048, 6144, 256] -> 2112 MB
所有 256 个专家打包在一个 3D tensor 里,不是 256 个独立 tensor。最后一个维度 256 就是专家数量。
单个专家的实际大小:
- gate: 1728 MB / 256 = 6.75 MB
- up: 1728 MB / 256 = 6.75 MB
- down: 2112 MB / 256 = 8.25 MB
- 总计: 21.75 MB / expert
一个常见的误解是每个专家有 1.3GB,实际上只有 21.75MB。这个发现直接影响速度模型——LRU 缓存可以容纳更多热专家,缓存命中率更高。
7.2 MLA 机制详解
GLM-5.2 使用 MLA(Multi-head Latent Attention),这是它和 DeepSeek-V2 共享的注意力创新。
标准 MHA 需要缓存每个头的 K 和 V,内存开销与头数成正比。MLA 通过低秩投影把 KV 压缩到 512 维(kv_lora_rank=512),推理时只缓存这个压缩向量。这就是为什么 GLM-5.2 能支持 1M token 的 context length。
但 MLA 的计算路径比标准 MHA 复杂得多:涉及 absorbed wk_b/wv_b、YaRN mscale、sigmoid-gated MoE 等特殊处理。这些复杂性正是 bug 的温床。
7.3 DSA Indexer
GLM-5.2 的架构名是 glm-dsa,DSA = Dynamic Sparse Attention。每个 MoE 层都有一个 indexer,用于决定哪些 token 需要被关注:
blk.N.indexer.attn_k.weight (6144 x 128)
blk.N.indexer.attn_q_b.weight (2048 x 4096)
blk.N.indexer.k_norm.weight (128)
blk.N.indexer.proj.weight (6144 x 32)
indexer 输出一个 32 维的向量,用于稀疏注意力的 token 选择。这种机制可以进一步降低计算量,但也增加了实现的复杂性。
7.4 每层的完整 tensor 结构
每个 MoE 层包含 21 种 tensor:
blk.N.attn_q_a.weight (6144 x 2048) - Q LoRA 降维
blk.N.attn_q_a_norm.weight (2048) - Q LoRA 归一化
blk.N.attn_q_b.weight (2048 x 16384) - Q LoRA 升维
blk.N.attn_kv_a_mqa.weight (6144 x 576) - KV LoRA (MQA)
blk.N.attn_kv_a_norm.weight (512) - KV LoRA 归一化
blk.N.attn_k_b.weight (192 x 512 x 64) - K 投影
blk.N.attn_v_b.weight (192 x 512 x 64) - V 投影
blk.N.attn_output.weight (16384 x 6144) - 注意力输出投影
blk.N.attn_norm.weight (6144) - 注意力层归一化
blk.N.ffn_norm.weight (6144) - FFN 层归一化
blk.N.ffn_gate_inp.weight (6144 x 256) - MoE router
blk.N.ffn_gate_shexp.weight (6144 x 2048) - 共享专家 gate
blk.N.ffn_up_shexp.weight (6144 x 2048) - 共享专家 up
blk.N.ffn_down_shexp.weight (2048 x 6144) - 共享专家 down
blk.N.ffn_gate_exps.weight (6144 x 2048 x 256) - 256 个专家 gate
blk.N.ffn_up_exps.weight (6144 x 2048 x 256) - 256 个专家 up
blk.N.ffn_down_exps.weight (2048 x 6144 x 256) - 256 个专家 down
blk.N.indexer.attn_k.weight (6144 x 128) - DSA indexer K
blk.N.indexer.attn_q_b.weight (2048 x 4096) - DSA indexer Q
blk.N.indexer.k_norm.weight (128) - DSA indexer K 归一化
blk.N.indexer.proj.weight (6144 x 32) - DSA indexer 投影
八、可能的修复方向
根据排查结果,我梳理了几条可能的修复路径。不一定对,但至少能缩小排查范围:
方向 1:检查 MSVC vs GCC 的浮点行为
MLA 的计算涉及 absorbed wk_b/wv_b 矩阵乘法和 YaRN mscale 缩放。如果 MSVC 和 GCC 在浮点舍入模式(round-to-nearest vs round-toward-zero)或 FMA(fused multiply-add)行为上有差异,可能导致数值发散。验证方法:在 Linux 上用 --fp32 强制 FP32 计算,看输出是否正确。
方向 2:检查 UD-Q4_K_XL 的 3D expert tensor 解包
3D expert tensor(shape=[6144, 2048, 256])的解包逻辑可能和标准 Q4_K 不同。Unsloth 的 Dynamic 量化可能在某些层使用了非标准的 block size 或 dequantization formula。验证方法:用标准 Q4_K_M 量化重新转换模型,看是否乱码。
方向 3:检查 Windows 上的 mmap 行为
Windows 的 mmap(CreateFileMapping / MapViewOfFile)和 Linux 的 mmap 在页面预取、缓存策略上有差异。如果 MLA 的计算依赖于特定的内存访问模式,Windows 的 mmap 行为可能导致权重加载顺序不同。验证方法:用 --no-mmap 强制全量加载(需要大量内存),看是否乱码。
方向 4:检查 DSA Indexer 的 Windows 兼容性
DSA Indexer 使用了 indexer.proj.weight(6144 x 32)做稀疏注意力的 token 选择。如果这个投影的计算在 Windows 上有数值问题,可能导致选错了 token,进而导致注意力输出损坏。验证方法:禁用 DSA Indexer(如果 llama.cpp 支持),改用标准注意力,看是否乱码。
九、求助
这个问题需要改 llama.cpp 的 C++ 代码,超出了我的能力范围。
我最想知道的一个问题:
llama.cpp 的
glm-dsaMLA 计算路径在 Windows(MSVC 编译)和 Linux(GCC/Clang 编译)上是否有数值行为差异?特别是 absorbed wk_b/wv_b 的矩阵乘法和 YaRN mscale 的缩放因子计算。
如果你有以下经验,恳请评论区指点:
- llama.cpp 的 MLA 实现细节 – absorbed wk_b/wv_b 的计算路径在 Windows 和 Linux 上有差异吗?
- GLM-5.2 的 glm-dsa 架构 – DSA indexer 的实现是否完整?和 DeepSeek-V2 的 MLA 有什么区别?
- UD-Q4_K_XL 量化格式 – Unsloth 的 Dynamic 量化和标准 Q4_K 在 tensor 布局上有区别吗?3D expert tensor 的解包逻辑正确吗?
- Windows 浮点行为 – MSVC 和 GCC/Clang 的浮点精度差异会影响 MLA 计算吗?
Issue 链接: llama.cpp #26027
十、排查过程中踩过的坑
如果你也在折腾 GLM-5.2 本地部署,这些信息可以帮你少走弯路:
常见疑问:
| 疑问 | 结论 |
|---|---|
| 词表里 0 个 CJK 字符是不是坏了? | 不是,byte-level BPE 就是这样的,中文通过字节编码处理 |
| blk.78 的 tensor 全是 unused 是不是加载出错了? | 不是,NextN 草稿层暂不支持,不影响主模型 |
| KTransformers 也乱码,是不是 KTransformers 的问题? | 不是,底层也是 llama.cpp,同样的 bug |
| Ollama 能跑吗? | 不能,Ollama 不支持多分片 GGUF |
我试过的路(都走不通):
| 方案 | 结果 |
|---|---|
| llama.cpp HIP 版 (-ngl 0) | 乱码 |
| llama.cpp 纯 CPU 版 (-ngl 0) | 乱码 |
| KTransformers v0.6.4 | 乱码 |
| Ollama | 不支持多分片 |
如果你也有 GLM-5.2 的调试经验,特别是 Windows 平台 的,恳请评论区交流。我目前卡在 llama.cpp 的 MLA 计算路径上,需要有 C++ 推理引擎开发经验的人帮忙看看。
十一、结论
| 项目 | 结果 |
|---|---|
| 64GB 能加载 753B 模型? | 能(mmap,43GB 内存) |
| 速度如何? | 3 tok/s(LRU 缓存命中率约 95%) |
| 输出正确吗? | 乱码(glm-dsa MLA bug) |
| Tokenizer 有问题? | 没有,byte-level BPE 验证通过 |
| 能修吗? | 需要改 llama.cpp C++ 代码,见 #26027 |
给不同情况的建议:
- 如果你想在 Windows 上跑 GLM-5.2:目前不行,等 llama.cpp 修复。别浪费时间重复排查。
- 如果你在 Linux 上:CPU-only 模式(
-ngl 0)可能可以,但 GPU offload 也有 bug。 - 如果你想先跑个能用的:Qwen3.6 35B A3B(18GB,64GB 内存轻松跑)是目前性价比最高的选择。
- 如果你懂 llama.cpp 的 MLA 实现:Issue #26027 需要你,评论区欢迎指点。
调试工具(GGUF 解析器、byte-level BPE tokenizer、词表对比脚本)需要的话评论区留言
最后,如果您觉得这篇博客对您有帮助,可否拜托各位看官点点赞和关注呢?谢谢您啦~
#GLM-5.2 #llama.cpp #MoE #大模型本地部署 #753B
更多推荐




所有评论(0)