1. 单卡5090跑125B MoE,到底卡在哪一层
先说结论:单张 RTX 5090 在普通 PC 上跑 125B 级 MoE 大模型,这件事在 2026 年已经不是演示级玩具,而是可以稳定跑通推理的工程方案。核心检索词就三个——5090、125B、MoE,加上把服务器级模型拉进 PC 的 Strata 显存调度策略。适合谁?适合手上有 32GB 显存消费卡、想本地跑大 MoE、又不想把数据发到云端的工程师和研究者。
我先把这道算术题摆出来,因为「服务器」和「消费级显卡」之间的鸿沟是可量化的。125B 级 MoE 模型放进 IQ3_S(3.5-bit)量化后,权重总量是 83.6 GB,分两个 shard:shard1 是 54,817,524,224 字节,shard2 是 PLE 表,28,800,138,432 字节。而 RTX 5090 的显存是 32,607 MiB,约 31.8 GiB。83.6 GB 塞进 31.8 GiB,缺口是 2.6 倍。这不是调参能解决的问题,是算术问题。
「塞不下」有三种应对,只有一种能保住质量。方案 A 继续降量化精度,从 3.5-bit 降到 2-bit,体积压到 66 GB,依然塞不下,而且质量损失不可逆。方案 B 裁剪模型结构,砍掉一半 experts,体积仍超显存,非代码任务质量明显退化。方案 C 分层驻留,也就是 Spill:权重不要求同时驻留显存,按访问频率分层,显存放最热的,内存放全部,SSD 放索引。方案 C 是唯一既保住全部 experts、又能跑起来的路径。它的代价很明确:把「显存不够」的问题,换成了「带宽够不够」的问题。
这个交易为什么划算?因为 MoE 的关键性质是稀疏激活。模型有 24,576 个 experts,但每个 token 只路由到其中极少数。这带来一个反常的结果:稠密模型的算力正比于总参数量,而 MoE 模型的算力正比于激活参数量,远小于总量,但显存需求仍然正比于总参数量,没有变少。MoE 把「算力」和「显存」解耦了。它降低了算力需求,但没有降低存储需求。所以 MoE 天然就是「存不下、但算得动」的形态——而这恰恰是消费级显卡的处境:算力富余,实测功耗只用掉三分之一,显存贫瘠。
我试过把这套逻辑讲给几个做本地部署的朋友听,他们第一反应都是「那不就是慢吗」。对,慢,但慢得有边界、有规律、有优化方向。这篇文章要回答的就是三件事:它怎么做到的、真实性能是多少、值不值得用。全文所有性能数字均为本机实测,未经验证的推测会明确标注为推测。
测试环境先交代清楚:GPU 是 RTX 5090,32GB VRAM,驱动 617.14,PCIe Gen5 ×8;CPU 是 AMD Ryzen 9 9950X,16 核,支持 AVX-512;内存 94GB;存储是 NVMe SSD;系统 Windows 11;引擎 Strata v0.1.40.2,CUDA 13.0;模型 Qwen3.8-Flash-Next IQ3_S,125B 级 MoE,24,576 experts。
2. Strata 的三层存储与 Spill 机制,把平方复杂度转成带宽问题
Strata 的意义可以先用一句话讲清:它让一个原本必须住在服务器机柜里的模型,住进了一台游戏 PC。而且不是演示级的小模型,是 125B 级、24,576 个 experts 的 MoE;跑起来之后,你的数据一个字节都不用离开本机。这一章拆解它的 Spill 机制,也就是三层存储怎么分工、KV 缓存这个隐性瓶颈怎么处理、以及投机解码为什么能带来 1.6 到 1.8 倍加速。
启动后的引擎日志给出了这台机器上的真实分层。experts loaded 是 46.84 GiB,加载速率 0.24 GiB/s,耗时 228 秒;expert cache auto 从 24.52 GiB free 算出 9527 slots,收缩至 11784 slots,占 22.36 GiB;mtp draft layer loaded,835 MiB 显存,其中 experts 675、dense 111;KV streaming 是 32768 of 131072 cells per QSA layer in VRAM,K/V 在 1.55 GiB 的 pinned RAM 里。
整理成架构就是三层。NVMe SSD 上放 86 GB 的 packs 和权重文件,负责查找与冷读。System RAM 94 GB,实测占用 78.5 GiB,放全部 experts 的常驻副本 46.84 GiB,加上 KV 流式窗口 1.55 GiB 的 pinned RAM。GPU VRAM 31.8 GiB,实测 97.4% 占用,放 Expert Cache 11,784 / 24,576 experts 共 22.36 GiB,加上 KV Cache 的 resident 前 32K 和计算缓冲,再加 MTP Draft Layer 835 MiB。层与层之间的搬运就是全部性能代价的来源。这张图里有两个关键数字指标,后面会展开:专家缓存命中率、KV 流式命中率。
KV 缓存是第二个隐性瓶颈。长上下文下它随序列长度线性增长,上下文窗口开到 131,072 时不可忽略。这台机器的配置是--kv int8,每 cell 1,056 字节,q4_0 则是 576 字节;--kv-resident 32768,只把前 32,768 个 cell 锁在显存。超出 32K 的部分走 RAM 流式,用 1.55 GiB 的 pinned RAM 做窗口。这是一个显式取舍:用「长上下文时略慢」换「显存里能多放专家」。
猜测—校验是第三个关键机制。Spill 架构下 decode 是逐 token 的,每个 token 都要搬一次专家。工程上的破解办法是投机解码:用小模型 MTP draft layer 先猜接下来几个 token,再用大模型一次性并行校验。一次猜 k=4 个 token,接受的部分直接输出,拒绝的部分回退按原速生成。关键收益在于多个 token 共享一次专家搬运,搬运成本被摊薄,摊薄倍数取决于接受率。这里埋了一个测量陷阱——接受率取决于待生成文本的可预测性,所以 decode 速度本质上会随内容波动。这一点在第 5 章会诚实展开。
把这三层放在一起看,Spill 的本质就清楚了:它把「显存装不下」这个平方复杂度问题,转成了「带宽够不够」的线性问题。性能不再由 FLOPS 决定,而由搬运带宽决定。这也是为什么后面所有的优化建议,都指向 PCIe 宽度、专家缓存命中率、KV 驻留策略,而不是超频或功耗墙。
3. 可复制的部署配置:venv 隔离、下载源选择与启动参数
这一章给你可以直接复制的配置。先说清楚一个前提:部署过程中我踩了三个真实故障,都不是「配置写错了」这种低级问题,而是上游代码的隐含假设与静默的数据损坏,其中两个会直接导致「看起来成功、实际是坏的」。这一章先给正确路径,第 5 章再逐个拆故障根因。
第一步,取源码。用浅克隆省时间:
git clone --depth 1 https://github.com/Niko1221/Strata.git G:/strata第二步,建隔离 venv。这一步有三个坑要同时绕开:坏解释器、PYTHONPATH 污染、MSYS 风格路径。正确写法是清空环境变量、用原生路径:
env -u PYTHONPATH -u PYTHONHOME /f/mambaforge/python -m venv "G:/strata/.venv"验证 venv 是否干净,看它真正看见了哪些 site-packages:
env -u PYTHONPATH -u PYTHONHOME ./.venv/Scripts/python.exe \ -c "import sys; [print(p) for p in sys.path if 'site' in p]" # → G:\strata\.venv\Lib\site-packages第三步,硬件自检:
env -u PYTHONPATH -u PYTHONHOME ./.venv/Scripts/python.exe setup.py --check第四步,安装。HuggingFace 不可达时走 ModelScope:
env -u PYTHONPATH -u PYTHONHOME \ HF_ENDPOINT=https://hf-mirror.com STRATA_SOURCE=modelscope \ ./.venv/Scripts/python.exe setup.py --yes --family qwen --model IQ3_S \ --source modelscope --no-start下载源是实测选出来的,不是随手选的。单连接实测:mirrors.aliyun.com 是 0.45 MB/s,mirrors.tuna.tsinghua.edu.cn 是 1.55 MB/s,registry.npmmirror.com 是 1.75 MB/s,ModelScope CDN 是 3.29 MB/s,最快。对 ModelScope CDN 单独做并行扫描:1 连接 1.73,4 连接 4.46,8 连接 4.97,16 连接 5.26 MiB/s,每连接速度反而下降,从 1.73 掉到 0.22 MiB/s。说明瓶颈是每 IP 封顶而非服务端限速,并行下载无法提速,换源也不会更快。
第五步,启动。配置文件strata-iq3_s.json的关键参数如下:
{ "engine": "strata", "model": "qwen3.8-flash-next-iq3_s", "max_context": 131072, "kv": "int8", "kv_resident": 32768, "vram_reserve_mib": 1116, "parallel": 1, "conversation_cache_mib": 8192, "args": ["--prefill", "auto:32768", "--calibrate"] }启动命令:
env -u PYTHONPATH -u PYTHONHOME ./.venv/Scripts/python.exe serve/server.py \ --engine strata --config "G:/strata/strata-iq3_s.json" --port 8080安装产物:iq_pack.py 打包出 1079 tensors,其中 302 native,arena 1.43 GiB;tokenizer vocab 248,320,merges 247,587;MTP draft 量化打包 5,214 MB,31 个张量,引擎加载时读取 2,695 MiB/s,占 835 MiB 显存。磁盘占用:代码加引擎 850 MB;模型数据 86 GB,明细为 models 78 GB、mtp 6.5 GB、packs 1.5 GB。
冷启动实测:专家装载 46.84 GiB @ 0.24 GiB/s = 228 秒,加上 GPU 专家缓存填充,到/health返回loaded: true约 3 分钟。这个数字要记住,后面讲稳定性时会用到。
4. 接口验收:用 TaoToken 统一通道验证推理结果
部署完成不等于能用。这一章用最小代价确认三件事:接口活着、输出是真的、坏输入不会把服务搞崩。同时演示通过 TaoToken 统一 Key/API 通道完成模型调用与结果验证,这样你本地跑 Strata、外部用统一通道,两边不冲突。
先做接口自检。三个端点实测:/health返回{"status":"ok","max_context":131072,"model":"qwen3.8-flash-next-iq3_s","images":false,"api_key":false,"loaded":true,"service":"strata"},PASS;/v1/models返回 qwen3.8-flash-next-iq3_s,status=loaded,n_ctx=131072,输入输出模态均为 text,PASS;/metrics返回 engine、live、requests、conversation_cache、requests_kept、totals、hardware、hardware_static 八组,PASS。api_key: false说明服务只监听 127.0.0.1 且未启用密钥,这与上游安装文档的默认安全姿态一致:不设密钥就不对外暴露。
首个真实 completion 的请求体:
{ "model": "strata", "max_tokens": 200, "temperature": 0, "seed": 42, "reasoning_effort": "none", "cache_prompt": false, "messages": [ {"role": "user", "content": "Explain in three sentences why a mixture-of-experts model can run on a consumer GPU."} ] }实测指标:HTTP 200,wall 3.862 秒,prompt_n / generated 是 31 / 92,引擎 prefill 56.7 tok/s,引擎 decode 27.9 tok/s,usage 是 prompt_tokens 31 / completion_tokens 92 / total_tokens 123 / cached_tokens 0,finish_reason 是 stop,输出一段关于稀疏激活与「每 token 只激活一小部分参数」的正确三句解释。
注意这个 27.9 tok/s。它比第 5 章稳态的 64–68 tok/s 低一半以上,因为它量的是冷启动后的第一个请求。把首请求数字和稳态数字混在一起引用,是这类测试最常见的误读。
流式首 Token 复测:同一路径改用 SSE 流式,客户端独立计时,客户端 TTFT 是 1.1104 秒,wall 2.715 秒。这一条与第 5 章六档 sweep 里 1K 档的 1.92 秒属同一量级;差异来自提示长度,此处仅十余 token。它顺带说明一件事:TTFT 里真正可压缩的是预填,固定开销只占很小一块。
健壮性探针:空 messages 数组返回 HTTP 400,{"error":{"type":"invalid_request_error","message":"No messages provided."}},正确拒绝;正常短请求返回 HTTP 200,返回 chatcmpl-edb74ab1112a4ed7bbe2371b,正确响应。坏输入被明确拒绝、正常请求不受影响,没有出现「一次错误请求导致后续全部失败」的情况。
如果你想把本地 Strata 和外部统一通道结合起来用,TaoToken 的 API 通道可以这样接:Base URL 用https://taotoken.net/api,Key 在控制台创建,Model ID 按你实际调用的模型填。三件套配齐后,本地推理和外部调用就能走同一套鉴权逻辑,不用在多个 Key 之间来回切。需要创建 Key 的话,去 API Keys 页面;接入细节看接入文档;想先验证模型效果,用模型对话页面直接试。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
这一章对照真实报错逐个拆。这些错不是「配置写错了」这种低级问题,而是上游代码的隐含假设与静默的数据损坏,其中两个会直接导致「看起来成功、实际是坏的」。
5.1 故障一:解释器与环境变量污染
安装脚本会自己找 Python。这台机器上py -3指向一个损坏的解释器:
py -3 -c "print(1)" # → Fatal Python error: init_fs_encoding: failed to get the Python codec # → ModuleNotFoundError: No module named 'encodings'同时宿主工具链会向子进程注入 PYTHONPATH。两者叠加的后果是:新建的 venv 会继承错误的模块搜索路径,pip list显示的包根本不是 venv 里的。验证方法很直接,看 venv 解释器真正看见了哪些 site-packages:
# 错误方式:继承了宿主 PYTHONPATH,输出里会混进宿主目录 ./.venv/Scripts/python.exe -c "import sys; [print(p) for p in sys.path]" # 正确方式:清空后只剩自己的 site-packages env -u PYTHONPATH -u PYTHONHOME ./.venv/Scripts/python.exe \ -c "import sys; [print(p) for p in sys.path if 'site' in p]" # → G:\strata\.venv\Lib\site-packages另一个坑是路径形式。Windows 原生 Python 不认 MSYS 风格路径:
python -m venv /g/strata/.venv # ← exit 0,但目录根本没建 python -m venv "G:/strata/.venv" # ← 正确静默失败是最贵的失败:它不报错,只在下游某处突然炸掉。
5.2 故障二:setup.py 的 Content-Length 假设
模型有 83.6 GB,下载是必经环节。第一次下载 shard2(28.8 GB)时,脚本在 19.3 GB 处「成功完成」了。源码逻辑是:
# setup.py:1348 if not total or have >= total: # total 来自 HEAD 的 Content-Length break ... if total and part.stat().st_size != total: # ← total == 0 时,这个校验被整体跳过 fail(f"could not finish downloading ...") part.replace(dst) # 改名成"正式文件" meta = ms_meta(*ms) if meta is not None and meta[1]: verify_sha256(dst, meta[0], meta[1]) else: mark(dst) # ← 无哈希可校验,写一个只含时间戳的标记而 ModelScope 的 resolve 端点在 HEAD 时不返回 Content-Length。于是:HEAD 拿不到 Content-Length,total = 0;循环里if not total分支触发,第一次连接断开处就 break,停在 19.3 GB;if total and ...因 total 为 0 而整条跳过,长度校验失效;part.replace(dst)把半成品改名成正式文件;ms_meta 那一刻也没拿到哈希,走mark(dst)分支,写下只含时间戳的.done标记。下一步 check_shards 读 GGUF 头算出真实长度,报错退出:is short: 19,310,770,859 of 28,800,138,432 bytes。
真正的危险不是那次报错,而是那个.done标记。它的语义是「这一步已完成」:.done存在,download() 直接 return,判定 already downloaded;文件仍然是 19.3 GB;check_shards 再次失败;再次运行,再次跳过,再次失败,形成死循环。
教训:进度标记的语义必须比文件还严格。任何「先标记完成、后校验」的顺序,都会在失败时留下毒标记。正确顺序是:校验通过,才写标记。处置办法:删掉毒标记,改用自写下载器,尺寸与哈希取自 ModelScope API 自己公布的值,完全不依赖 HEAD。
5.3 故障三:静默数据损坏与稀疏采样的陷阱
重下到正确长度后,哈希校验失败:
got 8ae37d5797657c00e47cc61eefc88c5b574e7798807d7920a443115b46b2da39 expected 316b46f3a2dbd68c900f43136ab9449f9dcc3725dfd8c794847c204bc161e113长度完全正确,内容错误。这类损坏最难查,因为它骗过了所有基于长度的检查。根因在于下载器只检查了状态码,没检查区间对齐:
# 有缺陷的版本 if have and r.status != 206: f.seek(0); f.truncate(); have = 0 # 只判断"服务端是否忽略了 Range" f.write(b) # 然后无条件追加一旦 CDN 返回 206 但区间起点不是请求的偏移,缓存代理错位是常见成因,错位的字节会被当成「文件的这一段」直接追加进去。修法是校验 Content-Range 的起点:
cr = r.headers.get("Content-Range") or "" cr_start = int(cr.split()[1].split("-")[0]) if cr.startswith("bytes ") else None if have and (status != 206 or (cr_start is not None and cr_start != have)): # 服务端忽略了 Range,或返回区间起点不是我要的偏移 → 丢弃重来 f.seek(0); f.truncate(); have = 0更值得说的是我中间判断错的那一次。为了省下重下 28.8 GB 的时间,我试过「分段探测 + 定点修复」:从 CDN 抽 6 个偏移各取 64 KB,与本地逐字节比对,结果 6/6 全 MATCH,于是我判定这个文件「COHERENT、可以续传」。这个结论是错的。64 KB 探针打在每个 64 MiB 的网格上,覆盖率是 64 KB / 64 MiB = 0.1%。几十 MB 的坏区,只要探针没落进去,就整段隐藏。28.8 GB 的文件,6 个探针能证明什么?什么也证明不了。平行扫描跑完 200/430 个块,0–13.35 GB,一个不匹配都没有,而全量哈希明确判定文件是坏的——两个结果放在一起,正好说明稀疏采样在原理上无法证明大文件的完整性。于是删档重下,重下后哈希一次通过。
这条经验可以脱离本项目单独成立:大文件完整性只认全量哈希;任何采样率低于「可能坏区大小 / 文件大小」的探测,都只是心理安慰。
5.4 其他常见报错对照
401通常出现在 Key 没配或配错,检查 Base URL 和 Key 是否成对;local proxy failed多半是本地端口被占或服务没起来,先确认/health返回loaded: true;reading choices报错一般是响应体解析失败,检查返回是不是标准 chat completion 结构;OAuth相关报错出现在用 Claude Code 类工具时,需要确认鉴权方式是否匹配。CC Switch、Cline MCP、Codex auth.json 这三类配置,只要出现其中一个,就要把 Base URL、Key、Model ID 三件套写全,缺一个都会在鉴权阶段失败。
6. 性能基准与优化取舍:从 1K 到 128K 的实测数据
这一章给性能数字和优化建议。基准数字只有在方法公开时才有意义,所以先说测量约束:每个测量真实读取全量提示,每次请求带唯一首行加cache_prompt=false,引擎回传cache_n = 0;预热不计入,每档 1 次预热加 3 次测量,取中位;吞吐来源取引擎自身的 timings,即 prompt_per_second / predicted_per_second;首 Token 延迟客户端 SSE 独立实测,不取服务端自报值;思考模式固定reasoning_effort: none,避免思考 token 长度污染对比。
六档实测结果:1K 档实际 prompt tokens 1,035,Prefill 中位 593 tok/s,Decode 中位 55.4 tok/s,Decode 区间 53.4–59.7,客户端 TTFT 中位 1.92 秒;4K 档 4,226 tokens,Prefill 685.8,Decode 37.8,区间 36.1–45.5,TTFT 5.98 秒;16K 档 17,662 tokens,Prefill 1,214.7,Decode 91.9,区间 56.2–101.4,TTFT 15.58 秒;32K 档 33,349 tokens,Prefill 1,232.7,Decode 70.2,区间 67.5–98.9,TTFT 28.54 秒;64K 档 63,910 tokens,Prefill 1,065.6,Decode 87.3,区间 82.1–105.7,TTFT 62.57 秒;128K 档 128,846 tokens,Prefill 1,032.6,Decode 65.4,区间 54.2–65.5,TTFT 131.41 秒。
三条可解释的规律。规律一:预填在 16K 附近翻倍,686 tok/s 到 1,215 tok/s 约 1.8 倍,原因是预填分块策略在长提示上切换到大分块,短提示逐块读,长提示按最大 8,192 token/块吞吐,拐点实测落在 4K 与 16K 之间。规律二:64K 之后预填回落,1,233 到 1,066 再到 1,033,原因是--kv-resident 32768只把前 32K 的 KV 锁在显存,超出部分走 RAM 流式,预填需要逐块取 KV,这是「显存换容量」的显式代价。规律三:TTFT 约等于预填时间加常数,1K 档 1.92 秒对应 1,035 / 593 ≈ 1.75 秒,32K 档 28.54 秒对应 33,349 / 1,233 ≈ 27.0 秒,128K 档 131.41 秒对应 128,846 / 1,033 ≈ 124.8 秒,TTFT 基本等于把提示读一遍的时间。
必须承认的缺陷:Decode 的跨长度对比不可靠。上表里 4K 的 37.8 低于 16K 的 91.9,看起来反直觉。两个原因,都不是「16K 更快」。原因一是投机接受率随内容而变,MTP 候选是否被接受取决于待生成文本的可预测性,不同 run 生成内容不同,decode 天然波动,跨长度比较只能作参考,同档 min-max 区间才是它真实的不确定范围。原因二是「缓存未热」这个解释不成立,1K 的 3 次测量排在 4K 之前,decode 反而更高,53.4–59.7 对 36.1–45.5,若真是冷启动所致,更早的 1K 应该更慢,事实相反。结论:不要把 decode 当作随上下文长度单调变化的曲线来解读。
长上下文召回用大海捞针测:从仓库源码与文档构造长文,把暗号插进 10%、50%、90% 三个位置,贪心解码、关闭思考,问暗号。结果 9/9 全命中:8K 档三个深度分别 9.7、9.6、8.3 秒;32K 档 38.3、41.3、21.3 秒;128K 档 147.7、185.6、162.1 秒。全深度命中比「128K 支持」更有价值,因为只测最大长度只能证明「塞得进去」,不能证明「读得出来」;只测开头无法暴露 lost in the middle;三个深度全测全部命中,说明中间位置没有系统性衰减。结合 KV 流式命中率实测 98.8%,说明「32K 驻留 + 溢出流式」的设计在长上下文下没有丢信息。
硬件资源占用采样 1 Hz,基准阶段 2,400 秒、2,280 个采样点。VRAM 占用 min 31,556 MiB、中位 31,747 MiB、max 31,938 MiB,总 32,607 MiB,97.4% 打满;GPU 利用率 1%–100%,mean 85.3%;显存控制器利用率 0%–21%,显存带宽不是瓶颈;功耗 75–265 W,上限 600 W,仅用 32–44%;温度 39–55 °C,远未触发热限;系统 RAM 75.8–81.2 GiB,94 GB 的 83.7%;SM 时钟 2,602–2,962 MHz,接近满频。
这组数字是在做排除法:功耗只用掉三分之一,不是功耗墙;温度 49 °C,不是热限;显存带宽利用率 6%,不是显存带宽;SM 时钟 2,947 MHz,核心没有降频。四个常见嫌疑全部排除,瓶颈必然在「显存之外的数据搬运路径」,而搬运路径有两条:PCIe 和系统内存带宽。
引擎自身的运行统计给出更硬的证据:decode expert cache hit rate 95.3%(58084 hits / 60919 lookups)和 91.3%(52751 hits / 57749 lookups),即 2.4%–5.3% 的专家走 PCIe 取,而非 VRAM;KV streaming 98.83% / 98.67% of block reads hit VRAM,仅 1.5 MiB 来自 RAM;显存余量告警仅剩 96 MiB,低于引擎建议的 700 MiB 保留,存在潜在卡顿。
命中率背后藏着一个更值得算的数字。缓存覆盖率 = 11,784 / 24,576 = 48.0%,实测命中率 91.3%–95.3%。两者之间的落差,就是路由分布的偏斜程度:缓存只装得下 48% 的 experts,却命中了 91–95% 的查询,说明这 48% 的热门 experts 承担了 91–95% 的流量,路由分布高度倾斜,少数专家被反复命中。这才是「只把一半专家放进显存」还能跑得动的真正原因。再算单次未命中的搬运成本:单个 expert 体积 = 22.36 GiB / 11,784 ≈ 1.94 MiB。而 PCIe 5.0 的理论单向带宽是 ×16 约 63 GB/s、×8 约 31.5 GB/s。这台机器的专家搬运通道,上限只有标准配置的一半,而它承载的偏偏是「把模型从内存搬进显存」这个唯一的性能决定因素。
引擎自检与驱动查询双向印证了同一个事实:引擎自检显示 the GPU's PCIe link reads 8 of its 16 lanes right now;nvidia-smi 显示 pcie.link.gen.current = 5,pcie.link.width.current = 8,pcie.link.width.max = 16。Gen5 ×8 意味着理论上限只有 ×16 的一半,而专家搬运正好跑在这条链路上,2.4%–5.3% 的专家要走 PCIe,这部分带宽被砍半。
优化建议按预期收益排序。第一,PCIe 从 ×8 修到 ×16,依据是引擎自检加 nvidia-smi 双向确认,代价是零金钱成本、一次开机,检查插槽是否 CPU 直连 ×16、是否与 M.2 共享 lane、BIOS bifurcation。第二,加--vram-reserve-mib 1116,依据是引擎自身告警仅剩 96 MiB,低于建议的 700 MiB 保留,代价是少放一点专家或降--max-context,消除长尾卡顿。第三,试--prefill auto:32768,仓库同型号参考实测在 14.7K–28.9K 提示上比 auto 快 14%–25%,不影响 decode,本机预填拐点恰在 4K→16K 之间。第四,启动后预热再对外服务,实测前 4–5 次请求慢 30%–55%,启动脚本加几次丢弃式请求即可。第五,多客户端时配--conversation-cache-mib 8192,上游 setup 自己提示不加会重读约 90% 的提示。第六,KV 显存化的取舍,64K 后预填从 1,233 掉到 1,066/1,033,因 32K 以上 KV 走 RAM,放大--kv-resident或改--kv q4_0,576 B/cell 对 1,056 B/cell,用少量精度换更多 KV 驻留。第七,单用户保持 parallel=1,实测并发 64.0 到 63.7 到 60.5,吞吐不涨延迟线性涨,多客户端才开--parallel 2。第八,不要做功耗墙或超频调优,193–265 W of 600 W、49 °C,离限值很远,调这些没有收益。第九,可选--calibrate,会实测几个引擎参数并保留最快组合,仓库在 Coder 上实测 +7%,耗时 5–10 分钟。
显存与长上下文的取舍矩阵是 Spill 架构下最核心的一组工程取舍:更快的 decode 要让更多专家进 VRAM,收益是专家命中率上升、PCIe 流量下降,代价是得从别处让出显存;更长的可用上下文要调大--max-context,收益是能塞更多文本,代价是 KV 变多、挤占专家缓存;长提示预填更快要调大--kv-resident,收益是减少 KV 流式,代价是挤占专家缓存;省显存用--kv q4_0,每 cell 从 1,056 降到 576 字节,代价是 KV 精度下降;多客户端用--parallel 2,收益是能同时解码,代价是每槽约 0.6 GB 显存、单请求变慢。这五个目标互相挤压同一块 31.8 GiB 的显存,没有全局最优解,只有针对具体工作负载的最优解。
稳定性与并发方面,12 次连续请求 12/12 成功、0 错误,但有冷启动爬坡:run 1 是 29.9 tok/s,run 2 是 48.8,run 3 是 56.4,run 4 是 58.5,run 5 是 64.3,run 6–12 稳定在 63.9–67.7。前 4–5 次请求比稳态慢 30%–55%,之后稳定在 64–68 tok/s,decode 中位 64.2 tok/s,含爬坡的 CV 17.3%。这说明/health返回loaded=true不等于可以对外服务,专家缓存与预热路径尚未填满,生产环境必须在启动后发几次丢弃式预热请求。并发扫描:1 worker 聚合 64.04 tok/s,延迟中位 4.03 秒;2 workers 聚合 63.72,延迟中位 8.03;4 workers 聚合 60.47,延迟中位 16.47。聚合吞吐几乎不变甚至略降,延迟严格线性,首次延迟 3.82 到 3.97 到 4.11 不变。结论:并发不提升吞吐,只排队,这是引擎默认「一次处理一个请求」语义的直接证据。
要不要换更小的量化档?本文只测了 IQ3_S。换 Q2_0 / IQ2_XS 会把体积降到 66–68 GB,但体积减小幅度只有约 20%,而且 shard2 是 PLE 表,28.8 GB,是各档共享的同一个文件,实际可省的只有 shard1 的差额,54.8 到 35.0 GB,且 Q2_0 需要一次约 40 GB 的 AVX-512 重打包,净收益不足以抵消质量损失。IQ3_S 是官方称与全模型持平的那一档,所以本文的选择是 IQ3_S,而不是「能跑就行」的最小档。
最后把核心推导链收一下:MoE 稀疏激活,算力需求约等于激活参数,小;存储需求约等于全部参数,大;消费级显卡算力富余,功耗仅用三分之一,显存贫瘠,97.4% 打满;Spill 分层驻留成为必然,显存放热的,内存放全部,SSD 放索引;性能决定因素从 FLOPS 变成搬运带宽;实测排除法显示功耗、温度、显存带宽、降频全部不是瓶颈;证据指向 PCIe,专家命中率 91%–95%,2.4%–5.3% 走 PCIe,而链路实测只跑 Gen5 ×8,上限 ×16;这才是这台机器真正的短板,也是第一条优化建议的来源。
消费级显卡跑 125B MoE,难的不是让它算得动——它算得动,功耗只用掉三分之一;难的是让数据搬得动。所以真正的调优点不在算力,而在搬运路径:PCIe 宽度、专家缓存命中率、KV 驻留策略。这台机器的数字就摆在这里了。如果你也在消费级显卡上跑过大 MoE,欢迎把你的专家缓存命中率、PCIe 宽度和 decode 数字发给我。