单卡5090跑125B 大模型:Strata 把服务器级 MoE 拉进普通 PC

单卡5090跑125B 大模型:Strata 把服务器级 MoE 拉进普通 PC

作者:吴佳浩

撰稿时间:2026-10-08

测试环境:

  • GPU:RTX 5090(32GB VRAM,驱动 617.14,PCIe Gen5 ×8)
  • CPU:AMD Ryzen 9 9950X
  • Memory:94GB RAM
  • Storage:NVMe SSD
  • OS:Windows 11(10.0.26200)
  • Engine:Strata v0.1.40.2(CUDA 13.0)
  • Model:Qwen3.8-Flash-Next IQ3_S(125B 级 MoE,24,576 experts)

引言

面向工程师与研究者的体系化分析。 不止讨论「跑起来了没有」,更追问「这件事对本地部署意味着什么、代价又落在哪一层」。 125B 级 MoE 模型过去只活在几百 GB 显存的服务器上。Strata 把它拉进了一台游戏 PC:不租云、不调 API、数据不出本机。 本文用一次完整实测回答三个问题------它怎么做到的、真实性能是多少、值不值得用,并把过程中三处真实故障的根因一并摊开。

注:全文所有性能数字均为本机实测,未经验证的推测会明确标注为推测。因为阿拉好奇所以我们一道来白相相,看看是徒有虚名,还是真的结棍。


目录

  1. 问题的本质:显存装不下时的必然选择
  2. [Spill 机制:把平方复杂度问题转成带宽问题](#Spill 机制:把平方复杂度问题转成带宽问题 "#%E4%BA%8Cspill-%E6%9C%BA%E5%88%B6%E6%8A%8A%E5%B9%B3%E6%96%B9%E5%A4%8D%E6%9D%82%E5%BA%A6%E9%97%AE%E9%A2%98%E8%BD%AC%E6%88%90%E5%B8%A6%E5%AE%BD%E9%97%AE%E9%A2%98")
  3. 部署实录:三个真实故障的根因推导
  4. 接口验收:推理验证与健壮性
  5. [性能基准:首 Token、Decode、预填](#性能基准:首 Token、Decode、预填 "#%E4%BA%94%E6%80%A7%E8%83%BD%E5%9F%BA%E5%87%86%E9%A6%96-tokendecode%E9%A2%84%E5%A1%AB")
  6. 长上下文:全深度召回的意义
  7. 硬件资源占用:瓶颈的定量定位
  8. 稳定性与并发:排队语义的实证
  9. 优化建议与工程取舍
  10. 总结

一、问题的本质:显存装不下时的必然选择

Strata 的意义可以先用一句话讲清:它让一个原本必须住在服务器机柜里的模型,住进了一台游戏 PC。 而且不是演示级的小模型,是 125B 级、24,576 个 experts 的 MoE;跑起来之后,你的数据一个字节都不用离开本机。

这件事值得认真拆解,是因为「服务器」与「消费级显卡」之间那道鸿沟是可量化的。所以这一章先把算术摆出来。

1.1 先算清楚这道题的边界

125B 级 MoE 模型放进 IQ3_S(3.5-bit)量化后,权重总量是 83.6 GB:

分片 字节数 SHA-256(官方公布,已比对通过)
shard1 54,817,524,224 4c1eb2ceb4915e1192f4f386021897bde56a97f40a0bb78bb86465e0f7d2aca3
shard2(PLE 表) 28,800,138,432 316b46f3a2dbd68c900f43136ab9449f9dcc3725dfd8c794847c204bc161e113

而 RTX 5090 的显存是 32,607 MiB,约 31.8 GiB。83.6 GB 塞进 31.8 GiB,缺口是 2.6 倍。 这不是调参能解决的问题,是算术问题。

1.2 「塞不下」有三种应对,只有一种能保住质量

方案 C 是唯一既保住全部 experts、又能跑起来的路径。它的代价很明确:把「显存不够」的问题,换成了「带宽够不够」的问题。

1.3 这个交易为什么划算:MoE 的稀疏性

MoE 的关键性质是稀疏激活:模型有 24,576 个 experts,但每个 token 只路由到其中极少数。这带来一个反常的结果:

markdown 复制代码
稠密模型:算力 ∝ 总参数量
MoE 模型:算力 ∝ 激活参数量(远小于总量)
         显存 ∝ 总参数量(没有变少)

MoE 把「算力」和「显存」解耦了。 它降低了算力需求,但没有降低存储需求。所以 MoE 天然就是「存不下、但算得动」的形态------而这恰恰是消费级显卡的处境:算力富余(实测功耗只用掉 1/3),显存贫瘠。


二、Spill 机制:把平方复杂度问题转成带宽问题

2.1 三层存储的实际配置

启动后的引擎日志给出了这台机器上的真实分层:

yaml 复制代码
experts loaded: 46.84 GiB at 0.24 GiB/s (228 s so far)
expert cache auto: 24.52 GiB free → 9527 slots → 收缩至 11784 slots / 22.36 GiB
mtp: draft layer loaded, 835 MiB of VRAM (experts 675, dense 111)
KV streaming: 32768 of 131072 cells per QSA layer in VRAM, the K/V in 1.55 GiB of pinned RAM

整理成架构图:

三层各自承担不同职责,而层与层之间的搬运就是全部性能代价的来源。这张图里有两个关键的数字指标,后面会展开:专家缓存命中率、KV 流式命中率。

2.2 KV 缓存是第二个隐性瓶颈

长上下文下还有第二个增长项:KV 缓存。它随序列长度线性增长,而上下文窗口开到 131,072 时不可忽略。这台机器的配置是:

bash 复制代码
--kv int8                     # 每 cell 1,056 字节(q4_0 为 576)
--kv-resident 32768           # 只把前 32,768 个 cell 锁在显存

超出 32K 的部分走 RAM 流式,用 1.55 GiB 的 pinned RAM 做窗口。这是一个显式取舍:用「长上下文时略慢」换「显存里能多放专家」。 它的实际效果在第 5 章与第 7 章用数据说话。

2.3 猜测---校验:为什么能快 1.6--1.8 倍

Spill 架构下 decode 是逐 token 的,每个 token 都要搬一次专家。工程上的破解办法是投机解码:用小模型(MTP draft layer)先猜接下来几个 token,再用大模型一次性并行校验。

关键收益在于多个 token 共享一次专家搬运 :搬运成本被摊薄,摊薄倍数取决于接受率。这里埋了一个测量陷阱------接受率取决于待生成文本的可预测性,所以 decode 速度本质上会随内容波动。这一点在第 5.3 节诚实展开。


三、部署实录:三个真实故障的根因推导

这一章是本文最该看的部分。三个故障都不是「配置写错了」这种低级问题,而是上游代码的隐含假设 与静默的数据损坏,其中两个会直接导致「看起来成功、实际是坏的」。

3.1 故障一:解释器与环境变量污染

安装脚本会自己找 Python。这台机器上 py -3 指向一个损坏的解释器:

vbnet 复制代码
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:

bash 复制代码
# 错误方式:继承了宿主 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 风格路径:

bash 复制代码
python -m venv /g/strata/.venv        # ← exit 0,但目录根本没建
python -m venv "G:/strata/.venv"      # ← 正确

静默失败是最贵的失败:它不报错,只在下游某处突然炸掉。

3.2 故障二:setup.py 的 Content-Length 假设(最严重)

模型有 83.6 GB,下载是必经环节。第一次下载 shard2(28.8 GB)时,脚本在 19.3 GB 处「成功完成」了。

源码:

python 复制代码
# 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。于是:

真正的危险不是那次报错,而是那个 .done 标记。 它的语义是「这一步已完成」:

教训:进度标记的语义必须比文件还严格。 任何「先标记完成、后校验」的顺序,都会在失败时留下毒标记。正确顺序是:校验通过 → 才写标记。

处置办法:删掉毒标记,改用自写下载器,尺寸与哈希取自 ModelScope API 自己公布的值(28,800,138,432 / 316b46f3...),完全不依赖 HEAD。

3.3 故障三:静默数据损坏,以及我自己的一个错误结论

重下到正确长度后,哈希校验失败:

复制代码
got      8ae37d5797657c00e47cc61eefc88c5b574e7798807d7920a443115b46b2da39
expected 316b46f3a2dbd68c900f43136ab9449f9dcc3725dfd8c794847c204bc161e113

长度完全正确,内容错误。 这类损坏最难查,因为它骗过了所有基于长度的检查。

根因在于下载器只检查了状态码,没检查区间对齐:

python 复制代码
# 有缺陷的版本
if have and r.status != 206:
    f.seek(0); f.truncate(); have = 0        # 只判断"服务端是否忽略了 Range"
f.write(b)                                    # 然后无条件追加

一旦 CDN 返回 206 但区间起点不是请求的偏移 (缓存代理错位是常见成因),错位的字节会被当成「文件的这一段」直接追加进去。修法是校验 Content-Range 的起点:

python 复制代码
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、可以续传」。

这个结论是错的。

平行扫描跑完 200/430 个块(0--13.35 GB)一个不匹配都没有 ,而全量哈希明确判定文件是坏的------两个结果放在一起,正好说明稀疏采样在原理上无法证明大文件的完整性。

于是删档重下。重下后哈希一次通过。

这条经验可以脱离本项目单独成立:大文件完整性只认全量哈希;任何采样率低于「可能坏区大小 / 文件大小」的探测,都只是心理安慰。

3.4 部署收口

bash 复制代码
# 1. 取源码
git clone --depth 1 https://github.com/Niko1221/Strata.git G:/strata

# 2. 建隔离 venv(绕开坏解释器 + 清空 PYTHONPATH + 原生路径)
env -u PYTHONPATH -u PYTHONHOME /f/mambaforge/python -m venv "G:/strata/.venv"

# 3. 硬件自检
env -u PYTHONPATH -u PYTHONHOME ./.venv/Scripts/python.exe setup.py --check

# 4. 安装(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

# 5. 启动
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。

下载源是实测选出来的,不是随手选的:

源 单连接实测
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 封顶而非服务端限速------并行下载无法提速,换源也不会更快。

冷启动实测 :专家装载 46.84 GiB @ 0.24 GiB/s = 228 s,加上 GPU 专家缓存填充,到 /health 返回 loaded: true 约 3 分钟。


四、接口验收:推理验证与健壮性

部署完成不等于能用。这一章用最小代价确认三件事:接口活着、输出是真的、坏输入不会把服务搞崩。

4.1 接口自检

端点 实测响应 判定
/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 且未启用密钥------这与上游安装文档的默认安全姿态一致:不设密钥就不对外暴露。

4.2 首个真实 completion

请求体:

json 复制代码
{"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 s
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 低一半以上,因为它量的是冷启动后的第一个请求。把首请求数字和稳态数字混在一起引用,是这类测试最常见的误读。

4.3 流式首 Token 复测

同一路径改用 SSE 流式,客户端独立计时:

指标 实测
客户端 TTFT 1.1104 s
wall 2.715 s

这一条与第 5 章六档 sweep 里 1K 档的 1.92 s 属同一量级;差异来自提示长度(此处仅十余 token)。它顺带说明一件事:TTFT 里真正可压缩的是预填,固定开销只占很小一块。

4.4 健壮性探针

请求 实测结果 判定
空 messages 数组 HTTP 400 ,{"error":{"type":"invalid_request_error","message":"No messages provided."}} 正确拒绝
正常短请求 HTTP 200,返回 chatcmpl-edb74ab1112a4ed7bbe2371b 正确响应

坏输入被明确拒绝、正常请求不受影响,没有出现「一次错误请求导致后续全部失败」的情况。


五、性能基准:首 Token、Decode、预填

5.1 测量方法(以及为什么必须说清)

基准数字只有在方法公开时才有意义。本次测量遵守以下约束:

约束 做法
每个测量真实读取全量提示 每次请求带唯一首行 + cache_prompt=false,引擎回传 cache_n = 0
预热不计入 每档 1 次预热 + 3 次测量,取中位
吞吐来源 取引擎自身的 timings(prompt_per_second / predicted_per_second)
首 Token 延迟 客户端 SSE 独立实测,不取服务端自报值
思考模式固定 reasoning_effort: none,避免思考 token 长度污染对比

5.2 六档实测

提示长度 实际 prompt tokens Prefill 中位 (tok/s) Decode 中位 (tok/s) Decode 区间 客户端 TTFT 中位
1K 1,035 593.1 55.4 53.4 -- 59.7 1.92 s
4K 4,226 685.8 37.8 36.1 -- 45.5 5.98 s
16K 17,662 1,214.7 91.9 56.2 -- 101.4 15.58 s
32K 33,349 1,232.7 70.2 67.5 -- 98.9 28.54 s
64K 63,910 1,065.6 87.3 82.1 -- 105.7 62.57 s
128K 128,846 1,032.6 65.4 54.2 -- 65.5 131.41 s

5.3 原始数据里的一个形态差异:长档位以 tool_calls 收尾

这一条不属于性能规律,但不该被忽略:32K / 64K / 128K 三档的 finish_reason 全部是 tool_calls,而 1K / 4K / 16K 是 stop 或 length。 长提示之下,模型倾向以工具调用形式收尾,而不是直接给出文本结尾。

这意味着长档位统计的「生成 token 数」里包含工具调用的 JSON 结构,与其他档位不是同一种输出形态------跨档比较 decode 时,这一点同样要纳入考虑。

5.4 三条可解释的规律,以及一条必须承认的缺陷

规律一:预填在 16K 附近翻倍。

bash 复制代码
686 tok/s (4K)  →  1,215 tok/s (16K)  ≈ 1.8 倍

原因:预填分块策略在长提示上切换到大分块
     短提示逐块读,长提示按最大 8,192 token/块吞吐
     拐点实测落在 4K 与 16K 之间

规律二:64K 之后预填回落。

scss 复制代码
1,233 (32K) → 1,066 (64K) → 1,033 (128K)

原因:--kv-resident 32768 只把前 32K 的 KV 锁在显存
     超出部分走 RAM 流式,预填需要逐块取 KV
     这是"显存换容量"的显式代价

规律三:TTFT ≈ 预填时间 + 常数。

scss 复制代码
1K  → 1.92 s      (1,035 / 593  ≈ 1.75 s)
32K → 28.54 s     (33,349 / 1,233 ≈ 27.0 s)
128K → 131.41 s   (128,846 / 1,033 ≈ 124.8 s)

TTFT 基本等于把提示读一遍的时间,与"约每分钟读 30,000 token"的估计一致

必须承认的缺陷:Decode 的跨长度对比不可靠。

上表里 4K 的 37.8 低于 16K 的 91.9,看起来反直觉。两个原因,都不是「16K 更快」:

arduino 复制代码
原因一:投机接受率随内容而变
  MTP 候选是否被接受,取决于待生成文本的可预测性
  不同 run 生成内容不同 → decode 天然波动
  → 跨长度比较只能作参考
  → 同档 min-max 区间才是它真实的不确定范围(如 16K:56.2-101.4)

原因二:"缓存未热"这个解释不成立
  看似可以怪 4K 测得早、缓存还没热,但原始数据不支持:
  1K 的 3 次测量排在 4K 之前,decode 反而更高(53.4-59.7 vs 36.1-45.5)
  若真是冷启动所致,更早的 1K 应该更慢 ------ 事实相反
  → 4K 的低值同样只能归因于投机接受率的 run 间波动

结论:不要把 decode 当作随上下文长度单调变化的曲线来解读。它的分母里有一个依赖生成内容的变量(投机接受率),而它既不正比于提示长度,也不随缓存变热而单调上升。


六、长上下文:全深度召回的意义

6.1 测什么

「支持 128K 上下文」这句话本身没有信息量------真正要问的是:第 100K 个 token 上的信息,模型在输出时还能不能用? 于是用大海捞针:从仓库源码与文档构造长文,把暗号插进 10%/50%/90% 三个位置,贪心解码、关闭思考,问暗号。

6.2 结果:9/9

长度 prompt tokens depth 10% depth 50% depth 90%
8K 8,064 ✅ FOUND(9.7 s) ✅ FOUND(9.6 s) ✅ FOUND(8.3 s)
32K 32,894 ✅ FOUND(38.3 s) ✅ FOUND(41.3 s) ✅ FOUND(21.3 s)
128K 125,833 ✅ FOUND(147.7 s) ✅ FOUND(185.6 s) ✅ FOUND(162.1 s)

6.3 为什么「全深度命中」比「128K 支持」更有价值

KV 命中率 98.8% 是这里的关键旁证。 它告诉我们:虽然 KV 在 32K 之后是流式的,但流式窗口的局部性好到几乎没有真的从 RAM 里读------这也是长上下文召回能全命中的系统层原因。


七、硬件资源占用:瓶颈的定量定位

采样 1 Hz,基准阶段 2,400 s / 2,280 个采样点。

指标 min 中位 max 解读
VRAM 占用 31,556 MiB 31,747 MiB 31,938 MiB 总 32,607 MiB → 97.4% 打满
GPU 利用率 1% 99% 100% mean 85.3%(请求间隙归零拉低)
显存控制器利用率 0% 6% 21% 显存带宽不是瓶颈
功耗 75 W 192.6 W 265 W 上限 600 W → 仅用 32--44%
温度 39 °C 49 °C 55 °C 远未触发热限
系统 RAM 75.8 GiB 78.5 GiB 81.2 GiB 94 GB 的 83.7%
SM 时钟 2,602 MHz 2,947 MHz 2,962 MHz 接近满频

第二份采样覆盖长上下文与稳定性阶段(750 点 / 789.9 s),数字与基准阶段一致,说明这套负载下的资源画像稳定,不是某一阶段的偶发读数:

指标 中位 max
VRAM 占用 31,573 MiB 31,657 MiB
GPU 利用率 99% 100%
功耗 180.6 W 213.4 W
温度 49 °C 52 °C
系统 RAM 75.9 GiB 76.6 GiB

7.1 这组数字是在做排除法

7.2 引擎自身的运行统计给出更硬的证据

sql 复制代码
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
strata serve: 96 MiB of VRAM free with everything loaded - LOW:
   requests may stall; add --vram-reserve-mib 1116 to the config's args

三条读出来的事实:

事实 数值 含义
专家缓存命中率 91--95% 绝大多数专家在 VRAM;但 2.4--5.3% 要走 PCIe
KV 流式命中率 98.8% 长上下文几乎全部命中显存,设计有效
显存余量告警 仅剩 96 MiB 低于引擎建议的 700 MiB 保留,存在潜在卡顿
命中率背后藏着一个更值得算的数字

把命中率和缓存容量放在一起看,能推出一个不显眼、但决定优化方向的事实:
缓存覆盖率= 11,78424,576 =48.0%实测命中率=91.3%∼95.3% \text{缓存覆盖率} = \frac{11{,}784}{24{,}576} = 48.0\% \qquad \text{实测命中率} = 91.3\% \sim 95.3\% 缓存覆盖率=24,57611,784=48.0%实测命中率=91.3%∼95.3%

两者之间的落差,就是路由分布的偏斜程度:

再算单次未命中的搬运成本:
单个 expert 体积= 22.36 GiB11,784 ≈1.94 MiB \text{单个 expert 体积} = \frac{22.36\ \text{GiB}}{11{,}784} \approx 1.94\ \text{MiB} 单个 expert 体积=11,78422.36 GiB≈1.94 MiB

而 PCIe 5.0 的理论单向带宽是 ×16 约 63 GB/s、×8 约 31.5 GB/s。这台机器的专家搬运通道,上限只有标准配置的一半------而它承载的偏偏是「把模型从内存搬进显存」这个唯一的性能决定因素。

这组数字合起来指向同一个结论:优化这台机器的正确方向不是提高算力,而是提高这 48% 的命中率、或拓宽这条 PCIe 通道。

7.3 PCIe 才是这台机器的短板

引擎自检与驱动查询双向印证了同一个事实:

ini 复制代码
[引擎自检] 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,这部分带宽被砍半。

这里需要把「实测」与「推测」分开说清:

  • 实测:链路确实跑在 ×8;专家 2.4--5.3% 走 PCIe;功耗/温度/显存带宽都远未饱和。
  • 推测(需修好插槽后 A/B 才能定量):修到 ×16 应当有收益,但具体增益本文不宣称。仓库另一份同型号参考(RTX 5090 + 5800 系列 CPU,PCIe Gen4 ×16)用同量化档跑出约 1,200 tok/s 预填,与本机 Gen5 ×8 接近;这既可能说明预填主要受 CPU/分块策略约束,也可能说明对照条件不够干净,不足以据此下结论。

八、稳定性与并发:排队语义的实证

8.1 连续请求:0 错误,但有冷启动爬坡

12 次连续请求(256 token 上限),12/12 成功、0 错误。但要报告一个现象:

arduino 复制代码
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%

另外两个只有翻原始 run 才看得见的细节:

  • 短请求从第 2 次起就复用了前缀缓存 :run 1 是 prompt_n=36 / cache_n=0,run 2--12 变成 prompt_n=7 / cache_n=29。这 12 次「连续请求」里只有第 1 次是真正全量读取提示的,后 11 次都吃了会话前缀缓存------读上面那组数字时需要知道这一点。
  • 12 次请求只产生了 6 个不同输出 (sha256 去重后)。每次请求的 seed 不同(100--111),输出随之变化;deterministic=False 是测试设计的结果,不构成引擎不确定性的证据。

8.2 并发扫描:吞吐不涨,延迟线性拉长

并发 聚合吞吐 请求延迟中位 首次延迟 错误
1 worker 64.04 tok/s 4.03 s 3.82 s 0
2 workers 63.72 tok/s 8.03 s 3.97 s 0
4 workers 60.47 tok/s 16.47 s 4.11 s 0
arduino 复制代码
1 → 2 → 4 workers
聚合吞吐:64.04 → 63.72 → 60.47   (几乎不变,甚至略降)
延迟中位:4.03  → 8.03  → 16.47   (严格线性)
首次延迟:3.82  → 3.97  → 4.11    (不变)

结论:并发不提升吞吐,只排队
      每 worker 的首个请求立即执行,其余顺序等待
      这是引擎默认"一次处理一个请求"语义的直接证据

原始量如下:c1 生成 768 token / 11.99 s,c2 1536 token / 24.11 s,c4 3072 token / 50.8 s;单请求 decode 中位分别为 65.1 / 65.95 / 62.65 tok/s。单请求速度几乎不变,变的只是等待时长,与上面的结论一致。

这解释了为什么不能靠加客户端来解决慢的问题 :单张显卡的专家搬运带宽是共享的,多客户端只会把等待拉长。真要并行,必须显式开 --parallel 并用显存换(每槽约 0.6 GB VRAM,从专家缓存里扣)。


九、优化建议与工程取舍

按预期收益排序,每条都给出依据与代价。

# 建议 依据 代价 / 验证方式
1 PCIe 从 ×8 修到 ×16 引擎自检 + nvidia-smi 双向确认 width.current=8 / max=16;2.4--5.3% 专家走 PCIe 零金钱成本,一次开机。检查插槽是否 CPU 直连 ×16、是否与 M.2 共享 lane、BIOS bifurcation
2 加 --vram-reserve-mib 1116 引擎自身告警:仅剩 96 MiB,低于建议的 700 MiB 保留 少放一点专家或降 --max-context;消除长尾卡顿
3 试 --prefill auto:32768 仓库同型号参考实测在 14.7K--28.9K 提示上比 auto 快 14--25%,不影响 decode;本机预填拐点恰在 4K→16K 之间 改一个参数后 A/B
4 启动后预热再对外服务 实测前 4--5 次请求慢 30--55% 启动脚本加几次丢弃式请求
5 多客户端时配 --conversation-cache-mib 8192 上游 setup 自己提示:不加会重读约 90% 的提示 占少量显存
6 KV 显存化的取舍 64K 后预填从 1,233 掉到 1,066/1,033,因 32K 以上 KV 走 RAM 放大 --kv-resident,或改 --kv q4_0(576 B/cell vs 1,056)用少量精度换更多 KV 驻留
7 单用户保持 parallel=1 实测并发 64.0 → 63.7 → 60.5,吞吐不涨延迟线性涨 多客户端才开 --parallel 2
8 不要做功耗墙 / 超频调优 193--265 W of 600 W、49 °C,离限值很远 调这些没有收益
9 可选:--calibrate 会实测几个引擎参数并保留最快组合,仓库在 Coder 上实测 +7% 5--10 分钟

9.1 显存与长上下文的取舍矩阵

这是 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 的显存。 没有全局最优解,只有针对具体工作负载的最优解------这也是为什么第 9 条 --calibrate 值得跑:它用实测代替猜。

9.2 要不要换更小的量化档?

本文只测了 IQ3_S。换 Q2_0 / IQ2_XS 会把体积降到 66--68 GB,但:

所以本文的选择是 IQ3_S,而不是"能跑就行"的最小档。


十、总结

核心推导链

一句关键判断

消费级显卡跑 125B MoE,难的不是让它算得动------它算得动,功耗只用掉三分之一; 难的是让数据搬得动。所以真正的调优点不在算力,而在搬运路径:PCIe 宽度、专家缓存命中率、KV 驻留策略。

全部实测数据一览

类别 关键结果
首 Token 延迟 1.92 s(1K)· 28.54 s(32K)· 131.41 s(128K)
Decode 稳态 64--68 tok/s;单次区间 53--106 tok/s
Prefill 593(1K)→ 1,233(32K) → 1,033 tok/s(128K)
长上下文召回 8K/32K/128K × 深度 10/50/90% = 9/9 全命中
KV 流式命中率 98.8%(仅 1.5 MiB 来自 RAM)
专家缓存命中率 91--95%
稳定性 12/12 成功、0 错误;前 4--5 次慢 30--55%(冷启动)
并发 1/2/4 workers 聚合 64.0 → 63.7 → 60.5,延迟 4.0 → 8.0 → 16.5 s
资源占用 VRAM 31,747 / 32,607 MiB(97.4%) 、GPU 99%、功耗 192.6 W / 600 W、49 °C、RAM 78.5 / 94 GiB
冷启动 专家装载 46.84 GiB @ 0.24 GiB/s = 228 s;到 /health 就绪约 3 分钟

本文的测量限制

列出这些是为了让上面的数字能被正确使用:

  • Decode 跨长度不可直接比较:受投机接受率与缓存热度影响(5.4)。
  • 4K 档 decode 偏低的原因未定论:曾归因于「测量顺序靠前、缓存未热」,但排在其前的 1K 反而更快,该解释不成立;剩余最可能的解释是投机接受率的 run 间波动。
  • 未做固定 seed 的确定性测试 :稳定性测试中每次请求换了 seed,deterministic=False 属测试设计所致,不构成引擎不确定性的证据。
  • 只测了 IQ3_S 一档,不能跨量化档外推。
  • 没有与 llama.cpp 等其他引擎横向对比:表中全部是 Strata 的绝对值。
  • 未测多卡、未测 vision (本次部署 images: off)。
  • 资源采样率 1 Hz:亚秒级尖峰可能漏采。
  • 客户端 TTFT 含 HTTP 与分词开销 :服务端自身的 prompt_ms 已随每次请求记录,可交叉核对。
  • 单机、单网络环境:下载速率受本机出口带宽限制(实测聚合封顶约 5.26 MiB/s,16 条并行连接不提速 → 每 IP 封顶),与引擎本身无关。

附录一:六档 sweep 的全部原始 run

正文取的是中位与区间;这里是 18 次测量的每一个原始值,包含服务端自报的 prompt_ms(预填耗时)、每次实际生成的 token 数与 finish_reason。

档位 run prompt_n prefill tok/s 服务端 prompt_ms 生成 tok decode tok/s wall s 客户端 TTFT s finish_reason
1k 1 1,035 611.3 1,693.1 130 53.4 4.14 1.9411 stop
1k 2 1,035 566.1 1,828.3 130 55.4 4.186 1.9209 stop
1k 3 1,035 593.1 1,745.2 130 59.7 3.942 1.8609 stop
4k 1 4,226 689.3 6,131.1 142 45.5 9.277 6.323 stop
4k 2 4,226 685.8 6,161.7 256 36.1 13.267 5.5384 length
4k 3 4,226 674.7 6,263.9 179 37.8 11.023 5.9789 stop
16k 1 17,662 637.6 27,701.5 222 56.2 31.725 17.6846 stop
16k 2 17,662 1214.7 14,540.7 224 101.4 16.817 14.953 stop
16k 3 17,662 1236.8 14,279.9 222 91.9 16.754 15.5758 stop
32k 1 33,349 1243.6 26,816.2 94 67.5 28.33 28.538 tool_calls
32k 2 33,349 1208.2 27,601.6 186 70.2 30.367 28.8359 tool_calls
32k 3 33,349 1232.7 27,052.6 170 98.9 28.87 26.7174 tool_calls
64k 1 63,910 1065.6 59,975.7 42 105.7 60.57 66.1798 tool_calls
64k 2 63,910 949.6 67,302.2 51 82.1 68.127 60.2617 tool_calls
64k 3 63,910 1141.2 56,001.4 60 87.3 56.906 62.5729 tool_calls
128k 1 128,846 1032.6 124,777.3 143 65.4 127.379 144.1314 tool_calls
128k 2 128,846 1098.6 117,283.6 49 54.2 118.608 131.4074 tool_calls
128k 3 128,846 1008.7 127,738.9 179 65.5 130.893 118.2726 tool_calls

附录二:稳定性与并发的全部原始数据

连续 12 次请求

# prompt_n cache_n 生成 tok decode tok/s wall s finish_reason 输出 sha256
1 36 0 256 29.9 9.519 length 3d068a586493adb3
2 7 29 256 48.8 5.35 length 63afd2f37fa5e99e
3 7 29 256 56.4 4.61 length 813cae4bc5639853
4 7 29 256 58.5 4.473 length 97c0e665f97ca853
5 7 29 256 64.3 4.113 length 0a2bcb27d08ab6c8
6 7 29 256 66.6 3.954 length 813cae4bc5639853
7 7 29 256 65.7 3.977 length 09553781c46164ea
8 7 29 256 63.9 4.114 length 09553781c46164ea
9 7 29 256 67.7 3.855 length 3d068a586493adb3
10 7 29 256 64.5 4.106 length 63afd2f37fa5e99e
11 7 29 256 64.1 4.095 length 813cae4bc5639853
12 7 29 256 64.4 4.072 length 3d068a586493adb3

并发扫描(每 worker 3 次请求)

并发 聚合 tok/s 请求数 tokens span s 延迟 min 延迟 中位 延迟 P90 延迟 max 单请求 decode 中位 错误
1 64.04 3 768 11.99 3.82 4.03 4.029 4.133 65.1 0
2 63.72 6 1536 24.11 3.966 8.03 8.128 8.142 65.95 0
4 60.47 12 3072 50.8 4.109 16.47 17.638 17.713 62.65 0

长上下文召回明细

长度 深度 prompt tokens 耗时 s 命中
8k 10% 8,064 9.7 ✅ FOUND
8k 50% 8,064 9.6 ✅ FOUND
8k 90% 8,065 8.3 ✅ FOUND
32k 10% 32,896 38.3 ✅ FOUND
32k 50% 32,894 41.3 ✅ FOUND
32k 90% 32,894 21.3 ✅ FOUND
128k 10% 125,833 147.7 ✅ FOUND
128k 50% 125,833 185.6 ✅ FOUND
128k 90% 125,834 162.1 ✅ FOUND

包主:这台机器的数字就摆在这里了。如果你也在消费级显卡上跑过大 MoE,欢迎把你的专家缓存命中率、PCIe 宽度和 decode 数字发给我

相关推荐
Data-Miner1 小时前
AI做表格软件哪个好?专业评测:五维对比看清差距
人工智能
Raspberry_Pi_官方账号2 小时前
观察、理解与响应:Raspberry Pi 5 上的低功耗 CNN、VLM 和 SLM 工作负载
人工智能·神经网络·cnn·树莓派·raspberrypi
AI日报派送佬2 小时前
2026年10月8日AI行业日报|GPT-6可视化UI全面上线、AI PC硬件革新、智能体安全与商业化提速
人工智能·gpt·openai·谷歌·deepseek·ai日报·智能体技术
MobotStone3 小时前
做了几个 Agent 项目后,我发现:真正拉开 AI 产品经理差距的,不是技术
人工智能·架构
孟健3 小时前
Qwen3.8-27B 本地推理实测:从 14 tok/s 到 159 tok/s 的投机解码调优
人工智能·llm·ai编程
思无邪663 小时前
用 AI 做 JS 逆向:从抓包到复现的完整方法论
开发语言·javascript·人工智能
KeyAction66663 小时前
AI改写战争规则,也在改写商业规则:体系对抗时代已经到来
大数据·人工智能
小蒋观天下3 小时前
专项方案:大场景港口AI安防、多干扰环境下的算法调优与落地实操
人工智能·深度学习·算法·安全·机器学习·计算机视觉·ai大模型
冬奇Lab3 小时前
一天一个开源项目(第231篇):MiniMind —— 花3块钱、2小时,从零训练一个 64M 参数的大语言模型
人工智能·开源·资讯