第 11 章 多卡与层切分
问题清单 #2 与「双卡三方案」的现场。本章回答:两张 24 GB 的卡,面对一个 76.7 GB 的模型,
到底有几种合作方式——每种方式的机制在源码哪几行、实测数字是多少、为什么最后选了看起来
"最浪费"的那一种。附带一个完整的翻案故事:solo 9.3 tok/s 的真根因不是显存,是 CPU 抢核。
行号口径:截至 2026-10-19,SSEStraBed/src/program/generate.cpp(下称 generate.cpp)。
引擎行号随注释整理漂移,旧文档里的行号(如互斥检查的 :1738)已不可用,本章用重新核实的值,
并保留旧值作对照。
11.1 先把物理账摆出来
| 量 | 数字 | 出处 |
|---|---|---|
| 权重包 IQ3_XXS | 76.7 GB | docs/deployment/STRATA-DEPLOYMENT.md §1(2026-10-04 口径) |
| 单卡显存 | 2 × RTX 4090D,各 24 GB | ~/deploy/context.json |
| 专家总数 | 48 层 × 512 = 24,576 个,每 token 取 top-10 | 同上 |
| 单个专家 blob | ~1.74 MB(16.61 GiB ÷ 10,243 槽) | docs/plans/方案猜想.md §3.1 |
| 每 token 专家查找次数 | ~470–590 次(48 层 × top-8 + 投机行,引擎日志实测) | 同上 |
| 主机 RAM | 216 GiB(专家 arena + PLE 28.8 GB 页缓存) | 02 章约束 1 |
一张卡装不下全部 48 层的专家,这是所有讨论的起点。三种合作方式由此展开:
| 方案 | 一句话 | 状态 |
|---|---|---|
| 1 层切分(layer split) | 两卡合跑一个实例,各管一半层 | 可一键回(layout.sh split) |
| 2 双实例 + 钉核 | 每卡跑一个完整实例,CPU/内存/GPU 全隔离 | 现生产(layout.sh dual) |
| 3 共享专家池 | 两卡各跑 dense、专家跨卡共用 | 已否决(引擎不支持对称形态) |
11.2 方案 1:--layer-split,把 48 层切成两段
参数定义在 generate.cpp:423:
std::string layer_split; // 多 GPU 层切分:"K" / "K1,K2,.." / "auto"(空 = 单卡);只在 --serve 下可用
三种形态:
| 形态 | 含义 | 约束 |
|---|---|---|
"24" |
层 0–23 在 CUDA0,层 24–47 在另一张卡 | 需要 --split-device 指定对端卡;K 必须从 2 起递增,每个 K 配一张互不相同的卡,或多个 K 共用 CUDA0(同卡分段) |
"12,24,36" |
三段切,配多卡 | 同上,切分点数 ≤ 可见卡数 |
"auto" |
引擎按成本模型自己选切分点 | 只有一张可见卡时自动退回单卡(:1610-1611 打印 “one GPU visible, so no split”) |
参数校验集中在 #多卡切分的参数校验 段(约 :1576-1622):--layer-split 只配 --serve 用,
非法形态直接 exit(:1622 的报错原文要求 “needs --serve, rising K from 2, and one …”)。
--trim-stage-weights 必须配显式切分(:2230):
strata generate: --trim-stage-weights needs an explicit --layer-split (ignored)
它的作用是"每张卡只加载自己那半层的 dense 权重,而不是整份 dense"(选项注释 :394)。
auto 模式不带 trim——这是 10-06 落地时踩过的点:想省显存就得先确定切点。
auto 的成本模型(注释在约 :2767,标定环境是 5080 + 3090 的机器,不是本机):
- 每层耗时 ∝ 1 /(SM 数 × 主频);
- 缓存未命中的专家按每单位路由质量约 190 ms;
- 缓存装哪些专家 = 该 stage 的层里 profile 从热到冷直到显存用完;
- 第 r 名专家的路由质量取 (r+1)^-1.2(拟合实测命中率);
- 3 卡以内穷举所有切分点,更多卡按速度比例分配。
层间只传激活值:切分之后两卡之间流动的是每 token 几 KB 的激活(层 23 的输出交给层 24),
不是权重。这是方案 1 能跑快的根本原因,也是理解方案 3 的关键前提。
停车与切分的相互作用:带 --layer-split 的停车会保存每个 stage 的图像
(SavedConversation::stage_images,注释约 :1567);而"同一张卡分段 + 停车"这个组合被明确拒绝
(:1604-1606,提示用 --conversation-cache-mib 0 关掉停车)。
方案 1 的实测(2026-10-06 落地,gpu:[1,0] + layer_split:"24" + --trim-stage-weights +
--batch-groups 2 + --pool-workers 24;出处 docs/deployment/Strata-双卡部署全景.md §3 方案 1):
| 指标 | 双卡 split | 单卡基线 |
|---|---|---|
| 单流 solo | 107–141 | 70.6–76 |
| 聚合 1 / 2 / 4 / 8 路 | 106 / 94 / 149 / 153 | 96.5 / 77.0 / 118.1 / 131.3 |
| 专家驻留 / 命中率 | 84%(20,579/24,576)/ 99.6–100% | 42% / ~86% |
| prefill | 6.3–6.7k tok/s(120k 全新约 18–22s) | ~3.4k tok/s |
| 显存 | 两卡各 ~23.8 GB(基本占满) | 单卡 23.6 GB |
它的致命弱点不在吞吐,在隔离:两卡被一组模型吃满,放不下第二个模型;
所谓"快道"只能靠调度级插队,忙时被同引擎的排队拖到 ~37 tok/s(同一出处)。
11.3 方案 1 的另一种形态:--peer-device 是主从专家层,不是对称池
选项定义(generate.cpp:324-327):
/// Multi-GPU: a second expert tier on CUDA device `peer_device` (-1 = off), `peer_reserve_mib` left free
int peer_device = -1; // 把另一张卡用作自适应专家缓存层(-1 = 关);与 --layer-split、--expert-cache-device1..3 互斥
帮助文本(:606-607)说清了它的语义:
--peer-device N a second GPU as an adaptive expert-cache tier (rows over P2P; it also
computes its experts' prompt rows). Not with --layer-split or ...
它是"主卡 + 副卡当缓存层"的从属结构:引擎主线程在 device 0,副卡持有部分专家、
在自己的卡上计算自己持有的那些行(包括 prompt 的行),结果经 P2P 回到主卡的 out。
实现分两个文件,区别值得记清(两个文件的"文件职责"头注释):
| 文件 | 机制 | 关键差别 |
|---|---|---|
src/core/peer_experts.cpp |
CUDA P2P(NVLink 或 PCIe peer access):把激活传过去、在副卡调原生分组专家内核、结果行取回主机 out;按专家使用率在副卡显存内换入换出 | 副卡直接访存;主线程用 RAII 临时切到 device_ 再切回;每层条目上限 CAP = MAXT × 10(一次上传一个 Meta 块:设备端权重指针、组起始下标、紧凑行号、源 token 下标) |
src/core/remote_experts.cpp |
该 GPU 不走 P2P 直接访存:激活拷过去、用 pinned 主机缓冲做零拷贝中转、紧凑结果行传回并散射到 out | 适合非 P2P 拓扑(PCIe/USB4);多一次主机中转 |
Portable flag 这个细节(generate.cpp:1563):
strata::core::set_peer_portable(o.peer_device >= 1); // multi-GPU: the Portable flag on mapped host buffers
// only with a peer device (before any allocation)
只有存在 peer 时,mapped 锁页内存才额外加 cudaHostAllocPortable——因为副卡也要写这些缓冲
(verify.cpp:85 的注释原话:“the peer card writes its rows into them”;mtp.cpp:71 同理)。
prefill.cpp:134 则相反:if (core::peer_portable()) return false;——有 peer 时保持 MMQ 路径及其缓冲大小。
这类"开关必须在任何分配之前设置"的时序,是引擎里反复出现的坑型(对照 10 章 mtp.cpp:245)。
peer 的默认分流比例(约 :2671 注释):default 0.45 (measured: 0.3-0.6 all better than 0; 0 = off, e.g. for the gate)。
启用前置条件(:3611):--peer-device needs --expert-profile and the expert cache;
启动时会打印副卡驻留量与占比(:3621)。
11.4 为什么 peer 与 layer_split 互斥(源码 + 物理两层理由)
源码事实(generate.cpp 约 :1945-1948,旧文档记作 :1738,已漂移):
if (o.peer_device >= 1 && (!o.layer_split.empty() || o.expert_cache_remote[0] > 0)) {
std::fprintf(stderr, "strata generate: --peer-device cannot be combined with %s\n",
!o.layer_split.empty() ? "--layer-split (use one or the other)" : ...);
物理理由(方案猜想.md §3.2):两者会各放一份同一个专家。
layer split 已经让"专家与层同卡"——每个专家一份、与计算同卡;peer 又在另一张卡上维护一份自适应缓存。
两份拷贝 = 两卡显存被切得更碎,而收益(命中率)早已被 split 本身拿走。
另一处相关报错(约 :1967)指向同一逻辑:某些形态"需要完整会话,而 layer split 没有",
要求"run them without --layer-split"。
11.5 已否决:方案 3「对称共享专家池」——一次框架纠错
方案 3 的原始设想:两卡各跑一组 dense 权重,专家做成跨卡共享池。否决它的是一笔带宽账
(方案猜想.md §3.1,双卡部署全景 §3 方案 3 同表):
| "共享"的实现方式 | 每 token 跨卡流量 | 判定 |
|---|---|---|
| 搬权重:把对端专家本体(1.74 MB)拉过来算 | 470–590 次查找 × 1.74 MB ≈ 200–500 MB/token → 仅此一项 ≥8–20 ms/token | ❌ 解码上限 <100 tok/s,且 PCIe 被打满(KV、停车、专家流全部饿死) |
| 搬数据:把激活行(~8 KB)送去专家所在卡算 | ~2–3 MB/token | ✅ 带宽可行——引擎已有这个形态(--peer-device、--expert-cache-device1..3 + `stripe |
关键理解纠正(这段值得留在书里,因为它是一次典型的"用错框架否决了正确方向"):
最初按"搬权重"否决方案 3,框架错了;正确机制是数据去找专家。而方案 2 钉核版的实测随后证明:
CPU/RAM 本身就是那个共享专家层——24,576 个专家全在内存 arena 里,未命中时数据找专家、
AVX-512 就地算。方案 3 的可行内核已经被方案 2 吸收,不需要新机制。
不支持的是对称形态:peer 是单主从结构,两个实例各自 --peer-device 互指技术上能启动,
但那只是两份互不共享的拷贝。要做真正的跨卡共享池需要引擎级改动(peer 共享化/可重入 + 每卡 VRAM 分区),
且没有任何实测证据表明它比方案 1 快。→ 记入已否决清单(19 章),别重复踩。
显存账也反对它(方案猜想.md §3.3):现状态每卡 24 GB ≈ dense 半份 ~5 GB + KV/状态/缓冲/MTP ~2–3 GB
- 专家缓存 ~15.8–16.6 GB。若做"两个整模型实例 + 各自 peer 专家层",每卡要同时容纳 A 的 dense 全量
(~10 GB)+ B 的 peer 缓存,每实例的本地专家容量反而下降。
11.6 翻案故事:solo 9.3 tok/s 的真根因
这是本项目最重要的一次误判与纠正,完整过程值得逐轮记录(出处:
docs/plans/方案猜想.md §2a、docs/deployment/Strata-双卡部署全景.md §3 方案 2、
docs/reports/双通道测试报告-2026-10-07.md)。
第一轮(2026-10-07):strata-a(GPU0:8091)+ strata-b(GPU1:8092),两份都无 layer split、
各 --pool-workers 24(合计 48 线程抢 32 核),停车各 8192 MiB / 20 槽。
| 指标 | 方案 1(split) | 每卡一实例(第一轮) |
|---|---|---|
| 单流解码 | 107–141 | 9.3 |
| 2 路(每路 / 聚合) | ~47 / 94 | 8.3、8.9 / 16.1 |
| 4 路聚合 | 149 | 24.0 |
| 8 路 | 153,全活 | 17.2,6/8 拿不到 token |
| 专家缓存命中 | 99.6–100% | 77.6–85.6% |
当时的归因是"单卡装不下全部 48 层的专家,24 GB 只装得下约 1/4–1/3,缺的专家落回 CPU 池"——
这个归因本身没错,但它解释不了 9.3 这个数量级(CPU 池兜底的历史水位是 60–70 tok/s 量级,不是 9)。
于是方案 2 一度被判"否决"。
第二轮:worker 从 24 降到 13(每实例),仍然共享核段 → 仍然崩,6–17 tok/s。
说明"worker 超额"只是部分原因。
第三轮(按用户提示钉核):strata-a 用 taskset -c 0-15(贴 GPU0 的 NUMA0),
strata-b 用 taskset -c 38-53(贴 GPU1 的 NUMA1),各 13 worker → 98–107 tok/s,彻底翻盘。
真根因:两个引擎的 CPU 专家池 worker 在同一批核上互踩——
每层解码的关键路径要等 CPU 池算完未命中专家,两个进程互相把对方的 worker 挤下核,
CFS 调度把等待放大成十倍(对照证据:CPU 池窗口时间从异常水位恢复到正常 59.8 ms/window,
方案猜想.md §重要转折)。不是显存装不下,是核不够分。
钉核后的实测(2026-10-07 晚,无外部流量,直连引擎;双卡部署全景 §3 方案 2 表):
| 场景 | 每路解码 | 聚合 | 对照:方案 1 |
|---|---|---|---|
| 单实例 solo | 107 / 98 | — | 130 |
| 1+1 | 89 / 115 | 139 | 94 |
| 2+2 | 32–47 | 109 | 149 |
| 1+3(快 1 + 普 3) | 快道 119 · 普道 31–43 | 93 | 4 路全 ~37 |
| 隔离测试:普道读 60k 长文时快道短请求 | 快道 93.7 tok/s · TTFT 1.08s | — | 快道冻结 ~20s |
| 8 路并发(经网关,每通道) | 8/8 全活 | 47–61/通道 | — |
1+3 那一行是双通道存在的理由:普道满载时快道仍有 119 tok/s,而 split 布局下同一场景所有人 ~37。
最后一行是隔离的价值:split 布局里长 prefill 会把快道冻结约 20 秒(09 章的饿死问题),
双实例布局下快道 TTFT 1.08 秒。
11.7 三方案横向对比与选法
| 维度 | 方案 1 split | 方案 2 双实例 + 钉核(生产) | 方案 3 共享专家 |
|---|---|---|---|
| 单流 solo | 130(107–141) | 98–107 | —(未实施) |
| 聚合吞吐(4 路) | 149 | 109 | — |
| 快道隔离(普道满载) | ~37(同引擎排队) | 119(完全隔离) | — |
| prefill | 6.5k tok/s | ~3.4k tok/s | — |
| 专家驻留 | 84%(跨卡分摊) | ~1/3 每卡(CPU 池兜底) | 设想更高,未验证 |
| RAM | 1 份(~120 GiB) | 2 份(176/216 GiB) | 省显存设想 |
| 故障域 | 1 | 2 | 2 |
| 运维复杂度 | 最低 | 中(钉核纪律) | 高(需引擎开发) |
| 适用 | 单通道高并发 | 快/普双通道 SLA 隔离 | 不建议 |
选法(同一出处的决策图,转述):想"一个模型服务尽量多人" → 方案 1(layout.sh split 一键可回);
想"给 agent 一条不被打扰的快道" → 方案 2(现生产);想"跨卡共享专家省显存" → 方案 3 不支持,
它的可行内核已经在方案 2 的 CPU 池里。若平台再给一张卡(GPU2),最干净的形态是
快道独占整卡、普道回 split 保吞吐(待申请,19 章阶段③)。
纪律(双卡部署全景 §运维):run-iq3_xxs-a.sh 里的 taskset -c 0-15 与
run-iq3_xxs-b.sh 里的 taskset -c 38-53 是命门——改启动脚本先确认 taskset 还在,
否则崩回 9 tok/s(17 章红线第 1 条)。
11.8 本章小结
- 两张卡有三种合作方式,实测数字都在 11.2/11.6/11.7 的表里;生产选的是方案 2,
因为它买到的是隔离而不是峰值吞吐。 --layer-split的三种形态(K/K1,K2/auto)只在--serve下可用;
--trim-stage-weights必须配显式切分;auto的成本模型是在别的机器上标定的,本机要复测。--peer-device是主从专家缓存层(副卡算自己持有的行,P2P 回传),
与--layer-split、--expert-cache-device1..3互斥(约 :1945),因为会各放一份同专家。- 方案 3 的否决理由要分清两笔账:搬权重不可行(200–500 MB/token),
搬激活行可行(2–3 MB/token);后者已由 CPU/RAM 池实现。 - 9.3 tok/s 的教训:数字崩盘时先怀疑"资源争用"再怀疑"容量不足"——
容量解释不了数量级,而 CPU 池在关键路径上时,抢核就是十倍的伤害。