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

> 不是 Linux 系列。这半年我从「能把模型加载起来出字」起步，去回答一个朴素的问题：自建算力跑大模型推理对外服务，技术上行不行、经济上划不划算。下面是四个和直觉完全相反、每个都让我重写一遍认知的坑。技术细节已脱敏，只留可公开的机制。

- URL: https://tenggouwa.com/posts/pd-fp4-linux-0-serving-vllm-serve-model-1-pd-1m-token-pd-prefill-decode-kv-cache/
- 发布: 2026-06-14
- 标签: AI, Infra, Agent, Deepseek

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

> 不是 Linux 系列。这半年我从「能把模型加载起来出字」起步，去回答一个朴素的问题：自建算力跑大模型推理对外服务，技术上行不行、经济上划不划算。下面是四个和直觉完全相反、每个都让我重写一遍认知的坑。技术细节已脱敏，只留可公开的机制。

## 0. 最大的误解：推理服务 = 把模型加载起来

我入门时以为 serving 就是 `vllm serve model`、能出字就完事。真动手才发现，**难的全在「出字之后」**：上下文一长怎么办、两个档位放哪台机器、对外报价赚不赚钱、报错到底是什么病因。

这四件事我每件都先信了直觉，然后每件都被打脸。一个个拆。

---

## 1. PD 分离：对稀疏注意力是个伪命题

超长上下文（1M token）单机怕扛不住，最「先进」的方案是 **PD 分离**——让一批机器专做 prefill（消化输入），另一批专做 decode（吐字），中间把 KV cache 传过去：

```text
经典思路（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——你在为一个不存在的瓶颈，搭一套又复杂又脆弱的跨机传输。

赢的方案朴素到尴尬：

```text
单机起 N 个副本 + 会话粘性路由（同一会话永远落同一副本，吃满前缀缓存）
  ┌──────┐ ┌──────┐ ┌──────┐
  │副本A  │ │副本B  │ │副本C  │   ← 横向复制，无跨机传输
  └──────┘ └──────┘ └──────┘
       ▲sticky 路由（按会话哈希）
```

没有跨机 KV 传输、没有 PD，反而又快又稳。**先搞清楚你的负载到底卡在哪，再决定要不要上复杂度。**

---

## 2. FP4 权重：模型被硬件世代「绑架」

同一个模型有两个档：高吞吐通用版（Flash）、质量碾压但慢的硬推理版（Pro）。我以为放哪台机器是纯需求问题，直到把 Pro 塞进上一代卡（Hopper 架构的 H200）。

Pro 的权重是为**新一代卡的 FP4** 设计的，体积大得多。单卡显存预算一摆就清楚了：

```text
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 首字延迟 | 亚秒 | **几分钟，不可用** |

想用两台机器拼起来分摊显存，又撞两堵谁都绕不过的墙：

```text
墙 1（数学）：权重某维度 = 192，量化块 = 128，192 % 128 ≠ 0  → 这个并行度根本拆不出来
墙 2（网络）：换个拼法，跨节点的底层通信被网络层挡死        → 连接直接 reset
```

**这就是为什么 Pro 绑新一代卡、Flash 放上一代卡**：换一代卡，Pro 不是「慢一点」，而是物理意义上的死路。选型不只看模型能力，还要看它和硬件的**血缘**。

---

## 3. 合成压测会撒谎：真实 agent 负载的形状

技术能跑通后，灵魂拷问是：对外卖 token 赚不赚钱？我用**合成负载**（固定短前缀、均匀打满并发）压出吞吐去算账，结论很丧：长上下文 / agent 场景毛利为负，**商业不成立**。

我差点上报。但心里有个疙瘩——**合成的均匀短前缀，是真实 agent 用起来的样子吗？**

显然不是。于是我写了个一键采集工具，从真实会话里还原负载形状。重点全在**隐私**——原文绝不离开本机：

```text
原文 ──tokenize──▶ 切 64-token 块 ──加盐哈希──▶ 只存 (哈希, 块长度)
                                          原文不可逆、不落盘、不上传
相同前缀 → 相同哈希 → 回放时精确复现缓存命中结构（要的是"形状"，不是内容）
```

用真实数据一跑，picture 彻底反转：

| 真实 agent 负载特征 | 实测 |
| --- | --- |
| r（input : output 比） | **逐请求中位数 ≈ 256**（输入是输出的几百倍） |
| 上下文长度 | P50 ≈ 108K，>16K 占 **97.5%** |
| prefix-cache 命中率 | **≈ 94%**（从块哈希实算，非估算） |

两个数改写了整个经济账：

```text
合成假设：每个 token 都从头算 → 贵
真实负载：极度 prefill-bound（r≈256）+ 94% 输入命中缓存（≈ 免费算力）
        → 单卡能以极低成本吐出巨量 total token
```

**这就是为什么结论从「不成立」翻成「成立」**。还附带一个口径教训：真实负载下吞吐**必须用 total token 口径**，用 output 口径会低估约两个数量级。**合成 benchmark 会撒谎，真实负载分布才是经济测算的地基。**

---

## 4. 「装不下」通常是参数饿死，不是模型太大

最后一个小坑，教训最深。Pro 在某拓扑下一直报显存不足，我第一反应「模型太大，这机器装不下」，几乎写进结论。

多看一眼才发现，显存根本不是被权重占满的：

```text
单卡显存 = 权重 + 激活(activation) + KV cache
                      ▲
        max_batched_tokens 设太大 → 激活膨胀 → 把 KV cache 挤没了 → 报"装不下"
```

把那个控制批大小的参数从十几万调到一万多，激活瞬间缩小，KV 腾出来——模型不仅装下了，还能服务到 1M 上下文。

**现象不等于病因。**「装不下」是现象，「参数饿死 KV」才是病因。别接受第一个听起来合理的解释——**尤其当这个解释会让你直接放弃的时候**。

---

## 小结：四条底层假设

把这半年压成四句给未来的自己：

- **直觉在 infra 里经常是反的**——越是「显然该这么做」的方案，越要先实验证伪一遍（PD）。
- **模型会被硬件绑架**——能力之外还要看它和卡的血缘（FP4）。
- **别信合成数据**——真实负载的分布常和拍脑袋差一个数量级（r 与缓存命中）。
- **质疑诊断，而不只是执行**——第一个解释往往是错的（参数饿死）。

面对一个「显然」的结论，现在我会先多问一句：**这是直觉，还是数据？**

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