长上下文推理的工程问题
处理百万上下文(1M tokens)时,架构层面的优化(如 MLA、Engram Memory)解决了”能不能装下”的问题,但工程层面还有大量”能不能跑顺”的问题。这篇聚焦 prefill、decode 和调度三个环节的实际瓶颈。
Prefill:算不完
Prefill 阶段要一口气把 1M tokens 编码成 KV Cache。Attention 的计算量:
代入 60 层、128 头、64 head_dim:
单卡 H100 按 50% 利用率算,一个请求需要 30 分钟左右。即使 8 卡 Ring Attention 并行,也要 4-5 分钟。用户不可能等这么久才看到第一个字。
显存峰值比平均占用更致命
1M prefill 时,FlashAttention 需要维护 attention score 累加器。峰值显存:
KV Cache (输出): 60 GB (MLA FP16)
Attention 中间值: ~10-20 GB
MLP 激活: ~5-10 GB
输入 Embedding: ~2 GB
────────────────────────────
峰值: ~90+ GB
单卡 H100 (80GB) 直接 OOM。所以百万上下文的 prefill 必须序列并行,不是为了快,是为了能跑起来。
Decode:读不动
Decode 每次只生成 1 个 token,但要读取全部 1M 个历史 token 的 KV Cache。
sequenceDiagram
participant GPU as GPU
participant HBM as HBM (KV Cache)
participant SRAM as SRAM
participant REG as Register
Note over GPU,REG: 每生成 1 个 token
loop 1M tokens
HBM->>SRAM: load cKV[i] (512 bytes)
SRAM->>REG: cKV
REG->>REG: up-project K/V
end
REG->>GPU: output token
MLA 压缩后,每层读取 1M × 512 × 2 = 1GB,60 层就是 60GB。H100 HBM 带宽 3.35 TB/s,有效带宽按 50% 算:
这还只是读 cKV 的时间,加上 up-project 和 attention 计算,实际 TBT(Time Between Tokens)≈ 50-100 ms。每秒钟输出 10-20 个 token,刚好在”能忍”和”卡”的临界点。
Prefill 和 Decode 的调度冲突
这是百万上下文下最痛苦的工程问题。一个 1M 请求的 prefill 跑 5 分钟,期间所有 decode 请求完全卡住。
gantt
title 无 Chunked Prefill 时的调度灾难
dateFormat X
axisFormat %s
section 请求 A (1M prefill)
Prefill : 0, 300
section 请求 B (decode)
卡住 : 0, 300
section 请求 C (decode)
卡住 : 0, 300
Chunked Prefill:切分与权衡
把 1M prefill 切成固定大小的 trunk(如 8K),每个 trunk 作为一个调度单元,和 decode 请求混排:
gantt
title Chunked Prefill 调度(trunk=8K)
dateFormat X
axisFormat %s
section 请求 A
chunk0 : 0, 2
chunk1 : 5, 7
chunk2 : 10, 12
chunk3 : 15, 17
section 请求 B
decode0 : 2, 3
decode1 : 7, 8
decode2 : 12, 13
section 请求 C
decode0 : 3, 4
decode1 : 8, 9
decode2 : 13, 14
TTFT 从 4 分钟变成 20 分钟(被 decode 穿插),但其他请求的 TBT 从”卡住 4 分钟”变成”偶尔慢 200ms”。
Trunk Size 的选择
| Trunk Size | TTFT 损失 | TBT 影响 | Kernel 效率 | 适用场景 |
|---|---|---|---|---|
| 512 | 极大 | 极小 | 差 | 实时语音 |
| 2048 | 大 | 小 | 一般 | 通用聊天 |
| 8192 | 中 | 中 | 好 | 文档问答 |
| 32K | 小 | 大 | 很好 | 低并发批处理 |
| 不切 | 无 | 阻塞致死 | 最好 | 离线处理 |
没有最优解。vLLM 默认 512-2048,实际中会根据 GPU 负载动态调整。
调度的其他工程陷阱
抢占的代价
显存不够时,vLLM 会把低优先级请求的 KV Cache swap out 到 CPU DRAM。对于 1M 上下文:
- Swap out 60GB:HBM → DRAM 带宽 ~10GB/s,需要 6 秒
- Swap in 又要 6 秒
- 一个抢占可能让用户等 12 秒
百万上下文的请求几乎不能被抢占。
动态长度 batching 的效率损失
Continuous batching 下,batch 内请求序列长度不同:
Batch:
请求 A: seq_len = 1M (decode)
请求 B: seq_len = 4K (decode)
请求 C: seq_len = 8K (prefill)
Attention mask 的构造开销大,短请求被长请求的 padding 浪费算力。解法是分桶(bucketization),但 1M 请求只能自己一个桶,batch size = 1,GPU 利用率低。
Prefix Caching 的局限
graph LR
A[用户上传文档] --> B[Prefill 1M tokens]
B --> C[KV Cache 存入 Prefix Cache]
D[用户提问] --> E[Query 命中 Cache?]
E -->|命中| F[跳过 Prefill]
E -->|未命中| G[重新计算]
1M 上下文中,Prefix Cache 的实际命中率:
| 场景 | 命中率 |
|---|---|
| 多轮对话 | 高(上一轮是下一轮的 prefix) |
| RAG / Agent | 低(每次 retrieved context 不同) |
| 代码补全 | 中(文件前缀相同,光标位置变化) |
| 开放域聊天 | 极低 |
总结
百万上下文的 prefill 瓶颈是算力( 计算量),decode 瓶颈是带宽(每次遍历 1M token 的 KV Cache),调度瓶颈是长请求阻塞一切。当前工业界没有完整优雅的解法,全靠 tradeoff:
- Chunked Prefill 用 TTFT 换 TBT
- MLA 和量化用计算换带宽
- PD 分离用资源利用率换延迟
这些手段叠加在一起,才让 1M 上下文从”理论上可行”变成”工程上能跑”。