← 返回

125B 稀疏模型塞进一张 32GB 消费卡

2026-08 ~ 09 · 一个 125B-A6B 的 MoE 模型,4bit 量化后非表权重 37.37 GiB,而卡只有 32GB

为什么想干这件事

MoE 的卖点就是"总参数很大、每次激活很少"(这个模型是 125B 总量、约 6B 激活)。 直觉上这意味着"一张消费卡跑得动 125B"。实际上跑不动全常驻,但可以跑起来—— 把一部分专家层留在内存里、其余常驻显存,每 token 只把用到的那几层搬上来算。 问题在于:这么做之后,速度到底被什么限制住。

先说结论

瓶颈不是带宽,是跨设备串行。GPU 上那 24 层算完,要等 CPU 上那 24 层算完, 再回到 GPU。GPU 大约一半时间在等。这个结论是量出来的,不是推的。

排除法:三个"看起来像瓶颈"的东西

nvidia-smi dmon/proc 计数,在 49.4 t/s 稳态下同时采样:

怀疑对象实测判定
PCIe 带宽rxpci 中位 1054 MB/s ≈ 21.3 MB/token,约占 PCIe5 x16 的 2%远没满,排除
显存带宽利用率 14%远没满,排除
磁盘换页盘读 0.035 MiB/token、major fault 0、Swap 0根本没发生,排除
GPU SM / CPUSM 50%、CPU 50%(16 线程跑满)两边各自都在等对方

这里有个反直觉的点值得单独说:--n-cpu-moe N在 CPU 上算这 N 层的专家 FFN, 不是"把权重放到硬盘然后往显存搬"。所以每 token 过总线的只有隐状态,量级小到可以忽略。 我一开始按"参数在硬盘上、带宽是墙"去理解,方向完全错了——真正的代价是串行执行

真正卡死容量的是一块计算缓冲

37.37 GiB 权重装不进 32GB,但原因不是"30 减 KV"这么简单。开最高日志等级看实际分配:

ub=2048ub=1024
预填充计算缓冲(固定开销)5334 MiB2933 MiB
KV(200K,4bit)+ 草稿 KV + 其他2265 MiB
留给权重的≈22.6 GiB≈24.9 GiB

也就是说:37.37 − 22.6 = 还差 14.8 GiB,约等于 23 层专家。 而 ub(micro-batch)是唯一能撬动那块死账的杠杆——从 2048 降到 1024 直接省出 2.4 GiB, 腾出来的空间正好多常驻几层专家。最终配置定成 ncmoe20 + ub1024

配置峰值显存短文 decode100K prefill / decode190K prefill / decode结果
ncmoe24 + ub2048(旧默认)30303 MiB (92.9%)54.6766 / 26.3642 / 18.5
ncmoe20 + ub1024(新默认)31004 MiB (95.1%)59.7725 / 27.7651 / 18.9活,检索校验通过
ncmoe18 + ub102431875 MiB (97.8%)61.6一次 100K 预填充直接 CUDA out of memory 打死进程
ncmoe14 / ncmoe10加载阶段就 failed to allocate buffer for kv cache(计算缓冲不随窗口缩小,权重多进去之后 KV 挤不下)起不来

97.8% 是一条静默崩溃线。这一档最讽刺的地方在于:它跑分最好看(61.6 t/s), 然后在一次普通的长文预填充时把整个进程打死。如果你的基准测试只跑短文,你会以为这是最优配置。

反过来算,要让这份权重全常驻:37.37 权重 + 2.87 计算 + 1.93 KV + 0.44 其他 + 约 0.6 余量 ≈ 43.2 GiB,比这张卡多 10.6 GiB。所以这不是"再优化优化就行"的问题,是物理不够。

顺带量到的几件事

还修了上游仓库的数

中途换过一版别人做的稀疏化权重(把专家数从 512 剪到 320 的那类),它的 README 里的实测数字有问题, 我按同口径重测后写了一节更正回去。这件事的收获比数字本身有用: 那份 README 的错法是"把不同 micro-batch、不同并发槽数下测出来的速度混在一张表里比较"—— 恰好就是我前面差点在自己笔记里犯的错。所以现在凡是给速度数,都要先把口径写全。

为什么最后停掉了

这套东西能跑、也确实有用过一段时间,但它的日常体验是:短文约 60 t/s,长上下文 decode 掉到 19–28 t/s, 一次 100K 输入要等一分多钟。而同期一个 27B 稠密模型全常驻在同一张卡上,配好投机解码能稳定在 100 t/s 以上, 并且 200K 窗口照样立得住。

也就是说:稀疏带来的"容量幻觉",在单卡上会被跨设备串行吃回去。 总参数大不等于跑得动,激活参数小只保证了算力不爆,不保证带宽和调度不爆。 这套权重后来被我清掉了,但这一整套测法(分层落位、显存死账、97.8% 崩溃线)留着,下次换模型还要用。