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

385 阅读29分钟

单卡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 机制:把平方复杂度问题转成带宽问题
  3. 部署实录:三个真实故障的根因推导
  4. 接口验收:推理验证与健壮性
  5. 性能基准:首 Token、Decode、预填
  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(官方公布,已比对通过)
shard154,817,524,2244c1eb2ceb4915e1192f4f386021897bde56a97f40a0bb78bb86465e0f7d2aca3
shard2(PLE 表)28,800,138,432316b46f3a2dbd68c900f43136ab9449f9dcc3725dfd8c794847c204bc161e113

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

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

mermaid diagram

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

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

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

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

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

mermaid diagram


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

2.1 三层存储的实际配置

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

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

整理成架构图:

mermaid diagram

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

2.2 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 做窗口。这是一个显式取舍:用「长上下文时略慢」换「显存里能多放专家」。 它的实际效果在第 5 章与第 7 章用数据说话。

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

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

mermaid diagram

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


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

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

3.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"      # ← 正确

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

3.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。于是:

mermaid diagram

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

mermaid diagram

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

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

3.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、可以续传」。

这个结论是错的。

mermaid diagram

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

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

mermaid diagram

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

3.4 部署收口

# 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.com0.45 MB/s
mirrors.tuna.tsinghua.edu.cn1.55 MB/s
registry.npmmirror.com1.75 MB/s
ModelScope CDN3.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/modelsqwen3.8-flash-next-iq3_s,status=loaded,n_ctx=131072,输入/输出模态均为 textPASS
/metrics返回 engine / live / requests / conversation_cache / requests_kept / totals / hardware / hardware_static 八组PASS

api_key: false 说明服务只监听 127.0.0.1 且未启用密钥——这与上游安装文档的默认安全姿态一致:不设密钥就不对外暴露。

4.2 首个真实 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."}]}
指标实测
HTTP200
wall3.862 s
prompt_n / generated31 / 92
引擎 prefill56.7 tok/s
引擎 decode27.9 tok/s
usageprompt_tokens 31 / completion_tokens 92 / total_tokens 123 / cached_tokens 0
finish_reasonstop
输出一段关于稀疏激活与「每 token 只激活一小部分参数」的正确三句解释

注意这个 27.9 tok/s。 它比第 5 章稳态的 64–68 tok/s 低一半以上,因为它量的是冷启动后的第一个请求。把首请求数字和稳态数字混在一起引用,是这类测试最常见的误读。

4.3 流式首 Token 复测

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

指标实测
客户端 TTFT1.1104 s
wall2.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 tokensPrefill 中位 (tok/s)Decode 中位 (tok/s)Decode 区间客户端 TTFT 中位
1K1,035593.155.453.4 – 59.71.92 s
4K4,226685.837.836.1 – 45.55.98 s
16K17,6621,214.791.956.2 – 101.415.58 s
32K33,3491,232.770.267.5 – 98.928.54 s
64K63,9101,065.687.382.1 – 105.762.57 s
128K128,8461,032.665.454.2 – 65.5131.41 s

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

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

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

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

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

686 tok/s (4K)  →  1,215 tok/s (16K)  ≈ 1.8 倍

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

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

1,233 (32K) → 1,066 (64K) → 1,033 (128K)

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

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

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 更快」:

原因一:投机接受率随内容而变
  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 tokensdepth 10%depth 50%depth 90%
8K8,064✅ FOUND(9.7 s)✅ FOUND(9.6 s)✅ FOUND(8.3 s)
32K32,894✅ FOUND(38.3 s)✅ FOUND(41.3 s)✅ FOUND(21.3 s)
128K125,833✅ FOUND(147.7 s)✅ FOUND(185.6 s)✅ FOUND(162.1 s)

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

mermaid diagram

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


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

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

指标min中位max解读
VRAM 占用31,556 MiB31,747 MiB31,938 MiB总 32,607 MiB → 97.4% 打满
GPU 利用率1%99%100%mean 85.3%(请求间隙归零拉低)
显存控制器利用率0%6%21%显存带宽不是瓶颈
功耗75 W192.6 W265 W上限 600 W → 仅用 32–44%
温度39 °C49 °C55 °C远未触发热限
系统 RAM75.8 GiB78.5 GiB81.2 GiB94 GB 的 83.7%
SM 时钟2,602 MHz2,947 MHz2,962 MHz接近满频

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

指标中位max
VRAM 占用31,573 MiB31,657 MiB
GPU 利用率99%100%
功耗180.6 W213.4 W
温度49 °C52 °C
系统 RAM75.9 GiB76.6 GiB

7.1 这组数字是在做排除法

mermaid diagram

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

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\%

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

mermaid diagram

再算单次未命中的搬运成本:

单个 expert 体积=22.36 GiB11,784≈1.94 MiB\text{单个 expert 体积} = \frac{22.36\ \text{GiB}}{11{,}784} \approx 1.94\ \text{MiB}

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

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

7.3 PCIe 才是这台机器的短板

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

[引擎自检] 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 错误。但要报告一个现象:

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%

mermaid diagram

另外两个只有翻原始 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 worker64.04 tok/s4.03 s3.82 s0
2 workers63.72 tok/s8.03 s3.97 s0
4 workers60.47 tok/s16.47 s4.11 s0
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,从专家缓存里扣)。


九、优化建议与工程取舍

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

#建议依据代价 / 验证方式
1PCIe 从 ×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% 的提示占少量显存
6KV 显存化的取舍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,但:

mermaid diagram

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


十、总结

核心推导链

mermaid diagram

一句关键判断

消费级显卡跑 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
Prefill593(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。

档位runprompt_nprefill tok/s服务端 prompt_ms生成 tokdecode tok/swall s客户端 TTFT sfinish_reason
1k11,035611.31,693.113053.44.141.9411stop
1k21,035566.11,828.313055.44.1861.9209stop
1k31,035593.11,745.213059.73.9421.8609stop
4k14,226689.36,131.114245.59.2776.323stop
4k24,226685.86,161.725636.113.2675.5384length
4k34,226674.76,263.917937.811.0235.9789stop
16k117,662637.627,701.522256.231.72517.6846stop
16k217,6621214.714,540.7224101.416.81714.953stop
16k317,6621236.814,279.922291.916.75415.5758stop
32k133,3491243.626,816.29467.528.3328.538tool_calls
32k233,3491208.227,601.618670.230.36728.8359tool_calls
32k333,3491232.727,052.617098.928.8726.7174tool_calls
64k163,9101065.659,975.742105.760.5766.1798tool_calls
64k263,910949.667,302.25182.168.12760.2617tool_calls
64k363,9101141.256,001.46087.356.90662.5729tool_calls
128k1128,8461032.6124,777.314365.4127.379144.1314tool_calls
128k2128,8461098.6117,283.64954.2118.608131.4074tool_calls
128k3128,8461008.7127,738.917965.5130.893118.2726tool_calls

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

连续 12 次请求

#prompt_ncache_n生成 tokdecode tok/swall sfinish_reason输出 sha256
136025629.99.519length3d068a586493adb3
272925648.85.35length63afd2f37fa5e99e
372925656.44.61length813cae4bc5639853
472925658.54.473length97c0e665f97ca853
572925664.34.113length0a2bcb27d08ab6c8
672925666.63.954length813cae4bc5639853
772925665.73.977length09553781c46164ea
872925663.94.114length09553781c46164ea
972925667.73.855length3d068a586493adb3
1072925664.54.106length63afd2f37fa5e99e
1172925664.14.095length813cae4bc5639853
1272925664.44.072length3d068a586493adb3

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

并发聚合 tok/s请求数tokensspan s延迟 min延迟 中位延迟 P90延迟 max单请求 decode 中位错误
164.04376811.993.824.034.0294.13365.10
263.726153624.113.9668.038.1288.14265.950
460.4712307250.84.10916.4717.63817.71362.650

长上下文召回明细

长度深度prompt tokens耗时 s命中
8k10%8,0649.7✅ FOUND
8k50%8,0649.6✅ FOUND
8k90%8,0658.3✅ FOUND
32k10%32,89638.3✅ FOUND
32k50%32,89441.3✅ FOUND
32k90%32,89421.3✅ FOUND
128k10%125,833147.7✅ FOUND
128k50%125,833185.6✅ FOUND
128k90%125,834162.1✅ FOUND

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