自建大模型推理服务:PD 分离 / FP4 绑卡 / 合成压测,四个反直觉的坑
自建大模型推理服务:PD 分离 / FP4 绑卡 / 合成压测,四个反直觉的坑
不是 Linux 系列。这半年我从「能把模型加载起来出字」起步,去回答一个朴素的问题:自建算力跑大模型推理对外服务,技术上行不行、经济上划不划算。下面是四个和直觉完全相反、每个都让我重写一遍认知的坑。技术细节已脱敏,只留可公开的机制。
0. 最大的误解:推理服务 = 把模型加载起来
我入门时以为 serving 就是 vllm serve model、能出字就完事。真动手才发现,难的全在「出字之后」:上下文一长怎么办、两个档位放哪台机器、对外报价赚不赚钱、报错到底是什么病因。
这四件事我每件都先信了直觉,然后每件都被打脸。一个个拆。
1. PD 分离:对稀疏注意力是个伪命题
超长上下文(1M token)单机怕扛不住,最「先进」的方案是 PD 分离——让一批机器专做 prefill(消化输入),另一批专做 decode(吐字),中间把 KV cache 传过去:
经典思路(PD 分离):
┌─ Prefill 节点 ─┐ 传 KV cache ┌─ Decode 节点 ─┐
│ 消化 1M 输入 │ ───────────────▶ │ 拿着大 KV 吐字 │
│ 算力重、瞬时 │ │ 显存重、持续 │
└────────────────┘ └────────────────┘
PD 的全部价值 = 把"又大又重的 KV"从 decode 机卸载出去
我搭好跨机 PD,小上下文正常,一上长上下文就崩——上下文过某个长度,KV 传输必然超时。折腾很久才想明白根因不在配置,而在模型本身:
| 维度 | 经典稠密注意力 | 这一代稀疏/压缩注意力 |
|---|---|---|
| 1M 上下文的 KV 大小 | 几百 GB | 十几 GB |
| decode 阶段瓶颈 | KV 占满显存 | 根本不缺显存 |
| PD 卸载 KV 的收益 | 大 | ≈ 0 |
这就是为什么 PD 对它是伪命题:PD 的全部价值是卸载又大又重的 KV,可稀疏注意力把 KV 砍到了 1/10,decode 压根不是 KV-bound——你在为一个不存在的瓶颈,搭一套又复杂又脆弱的跨机传输。
赢的方案朴素到尴尬:
单机起 N 个副本 + 会话粘性路由(同一会话永远落同一副本,吃满前缀缓存)
┌──────┐ ┌──────┐ ┌──────┐
│副本A │ │副本B │ │副本C │ ← 横向复制,无跨机传输
└──────┘ └──────┘ └──────┘
▲sticky 路由(按会话哈希)
没有跨机 KV 传输、没有 PD,反而又快又稳。先搞清楚你的负载到底卡在哪,再决定要不要上复杂度。
2. FP4 权重:模型被硬件世代「绑架」
同一个模型有两个档:高吞吐通用版(Flash)、质量碾压但慢的硬推理版(Pro)。我以为放哪台机器是纯需求问题,直到把 Pro 塞进上一代卡(Hopper 架构的 H200)。
Pro 的权重是为新一代卡的 FP4 设计的,体积大得多。单卡显存预算一摆就清楚了:
H200 单卡 ~141GB:
Flash(FP8 权重 ~19GB): [██░░░░░░░░░░░░░░░░░░] 剩 ~122GB → cudagraph 全开 ✅
Pro (FP4 权重 ~106GB): [███████████████░░░░░] 剩 ~35GB → 装不下 cudagraph ❌
| 维度 | Flash | Pro |
|---|---|---|
| 单卡权重占用(TP8) | ~19GB | ~106GB |
| cudagraph | 全开 | 必须关 |
| 关 cudagraph 后 decode | ~2000 tok/s | ~59 tok/s(慢一个数量级) |
| 128K 首字延迟 | 亚秒 | 几分钟,不可用 |
想用两台机器拼起来分摊显存,又撞两堵谁都绕不过的墙:
墙 1(数学):权重某维度 = 192,量化块 = 128,192 % 128 ≠ 0 → 这个并行度根本拆不出来
墙 2(网络):换个拼法,跨节点的底层通信被网络层挡死 → 连接直接 reset
这就是为什么 Pro 绑新一代卡、Flash 放上一代卡:换一代卡,Pro 不是「慢一点」,而是物理意义上的死路。选型不只看模型能力,还要看它和硬件的血缘。
3. 合成压测会撒谎:真实 agent 负载的形状
技术能跑通后,灵魂拷问是:对外卖 token 赚不赚钱?我用合成负载(固定短前缀、均匀打满并发)压出吞吐去算账,结论很丧:长上下文 / agent 场景毛利为负,商业不成立。
我差点上报。但心里有个疙瘩——合成的均匀短前缀,是真实 agent 用起来的样子吗?
显然不是。于是我写了个一键采集工具,从真实会话里还原负载形状。重点全在隐私——原文绝不离开本机:
原文 ──tokenize──▶ 切 64-token 块 ──加盐哈希──▶ 只存 (哈希, 块长度)
原文不可逆、不落盘、不上传
相同前缀 → 相同哈希 → 回放时精确复现缓存命中结构(要的是"形状",不是内容)
用真实数据一跑,picture 彻底反转:
| 真实 agent 负载特征 | 实测 |
|---|---|
| r(input : output 比) | 逐请求中位数 ≈ 256(输入是输出的几百倍) |
| 上下文长度 | P50 ≈ 108K,>16K 占 97.5% |
| prefix-cache 命中率 | ≈ 94%(从块哈希实算,非估算) |
两个数改写了整个经济账:
合成假设:每个 token 都从头算 → 贵
真实负载:极度 prefill-bound(r≈256)+ 94% 输入命中缓存(≈ 免费算力)
→ 单卡能以极低成本吐出巨量 total token
这就是为什么结论从「不成立」翻成「成立」。还附带一个口径教训:真实负载下吞吐必须用 total token 口径,用 output 口径会低估约两个数量级。合成 benchmark 会撒谎,真实负载分布才是经济测算的地基。
4. 「装不下」通常是参数饿死,不是模型太大
最后一个小坑,教训最深。Pro 在某拓扑下一直报显存不足,我第一反应「模型太大,这机器装不下」,几乎写进结论。
多看一眼才发现,显存根本不是被权重占满的:
单卡显存 = 权重 + 激活(activation) + KV cache
▲
max_batched_tokens 设太大 → 激活膨胀 → 把 KV cache 挤没了 → 报"装不下"
把那个控制批大小的参数从十几万调到一万多,激活瞬间缩小,KV 腾出来——模型不仅装下了,还能服务到 1M 上下文。
现象不等于病因。「装不下」是现象,「参数饿死 KV」才是病因。别接受第一个听起来合理的解释——尤其当这个解释会让你直接放弃的时候。
小结:四条底层假设
把这半年压成四句给未来的自己:
- 直觉在 infra 里经常是反的——越是「显然该这么做」的方案,越要先实验证伪一遍(PD)。
- 模型会被硬件绑架——能力之外还要看它和卡的血缘(FP4)。
- 别信合成数据——真实负载的分布常和拍脑袋差一个数量级(r 与缓存命中)。
- 质疑诊断,而不只是执行——第一个解释往往是错的(参数饿死)。
面对一个「显然」的结论,现在我会先多问一句:这是直觉,还是数据?
注:内网信息、商业数据均已脱敏,技术数字为公开可分享口径。