← cd ../posts

自建大模型推理服务:PD 分离 / FP4 绑卡 / 合成压测,四个反直觉的坑

2026-06-14

自建大模型推理服务: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 与缓存命中)。
  • 质疑诊断,而不只是执行——第一个解释往往是错的(参数饿死)。

面对一个「显然」的结论,现在我会先多问一句:这是直觉,还是数据?

注:内网信息、商业数据均已脱敏,技术数字为公开可分享口径。