Sealessland logo Sealessland
LLM Inference

长上下文推理的工程问题

整理百万上下文下 prefill、decode 和调度的实际瓶颈。

长上下文推理的工程问题

处理百万上下文(1M tokens)时,架构层面的优化(如 MLA、Engram Memory)解决了”能不能装下”的问题,但工程层面还有大量”能不能跑顺”的问题。这篇聚焦 prefill、decode 和调度三个环节的实际瓶颈。

Prefill:算不完

Prefill 阶段要一口气把 1M tokens 编码成 KV Cache。Attention 的计算量:

FLOPs2nlayersnheadsdheadL2\text{FLOPs} \approx 2 \cdot n_{\text{layers}} \cdot n_{\text{heads}} \cdot d_{\text{head}} \cdot L^2

代入 60 层、128 头、64 head_dim:

2×60×128×64×(106)29.8×1017 FLOPs2 \times 60 \times 128 \times 64 \times (10^6)^2 \approx 9.8 \times 10^{17} \text{ FLOPs}

单卡 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% 算:

读取时间=60 GB1.675 TB/s36 ms\text{读取时间} = \frac{60 \text{ GB}}{1.675 \text{ TB/s}} \approx 36 \text{ ms}

这还只是读 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 SizeTTFT 损失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 瓶颈是算力O(L2)O(L^2) 计算量),decode 瓶颈是带宽(每次遍历 1M token 的 KV Cache),调度瓶颈是长请求阻塞一切。当前工业界没有完整优雅的解法,全靠 tradeoff:

  • Chunked Prefill 用 TTFT 换 TBT
  • MLA 和量化用计算换带宽
  • PD 分离用资源利用率换延迟

这些手段叠加在一起,才让 1M 上下文从”理论上可行”变成”工程上能跑”。