推理 Infra:从计算特征推导系统架构


推理 Infra:从计算特征推导系统架构

推理 Infra:从计算特征推导系统架构封面图

推理系统不应从组件名开始,而应先问三个问题:一次请求怎样消耗算力和显存,哪些状态能复用,用户能等多久。架构是这些约束推出来的分工,不是产品清单。

本文讨论常见 decoder-only、自回归 Transformer,不把结论直接套到所有多模态或非自回归模型。

一、先看 prefill 和 decode

Prefill 处理输入 token。已知输入可以并行计算,长输入通常更容易利用 GPU 的矩阵计算能力,并建立生成所需的历史 KV。

Decode 每次只生成一个或少量 token,下一步依赖上一步结果。它仍要读取权重、访问历史状态并追加 KV;小批量时常受显存带宽限制,上下文变长后,KV 读取也会变贵。因此,不能简单说 prefill 只吃算力、decode 只吃带宽,实际瓶颈取决于模型、批大小、并行方式和硬件。

flowchart LR
    A["输入 token"] --> B["Prefill:建立历史 KV"]
    B --> C["采样首 token"]
    C --> D["Decode:生成下一 token"]
    D --> E["追加 KV"]
    E --> F{"停止?"}
    F -->|否| D
    F -->|是| G["释放请求状态"]

这里有两个常被混淆的缓存概念:

  • KV Cache 是单请求内的复用。 生成第 10 个 token 时,前面 token 的 key、value 已经算过,不必为每一步重算整段历史;新的 query 仍要与可见历史交互。
  • Prefix Cache 是跨请求的复用。 只有新请求与旧请求拥有兼容的 token 前缀,且模型版本、模板、tokenizer、适配器等条件一致,才能减少对应的 prefill。语义相似或文字相同都不够。

二、引擎把计算变成执行计划

vLLM、SGLang、TensorRT-LLM 等推理引擎,负责算子执行、显存管理、批处理和请求生命周期。它们解决的是“拿到 GPU 后怎样跑得更好”,不是资源分配或业务路由。

FlashAttention 通过分块减少注意力中间结果的读写,但不会消除注意力计算;收益也随序列长度和硬件变化。PagedAttention 按块管理 KV,减少连续分配造成的碎片,便于动态扩展,但不会自动把 KV 变成跨节点缓存。

Continuous batching 允许完成的请求退出、新请求在调度边界加入,不必等整批最长请求结束。它通常提高 GPU 填充率,也可能让长 prefill 干扰 decode,所以引擎还需要分块 prefill、每轮 token 预算、活动序列上限和必要的抢占。

参数没有脱离负载的最优值。扩大批次可能提升吞吐,却拉长等待;抢占能缓解显存压力,却可能带来状态搬运或重算。量化、蒸馏和稀疏化也应先验证质量,再比较延迟、吞吐和成本;微调本身不会缩小模型参数规模。

三、Kubernetes 提供资源,不自动调度模型

引擎解决执行,Kubernetes 解决 Pod 生命周期和资源放置。GPU 数量只是起点,还要匹配显存、型号、驱动、运行时、并行配置和卡间拓扑;跨节点并行还会增加网络依赖。

Kubernetes 不会自动完成模型级调度。 它能依据资源声明放置 Pod,却不知道某个实例的队列、缓存命中率和真实 token 容量。多集群容灾、模型版本选择和按服务能力分流,需要控制器、路由层或专门调度逻辑补上。

弹性也不等于修改副本数。新 Pod 还要等待 GPU、拉镜像、读取权重、加载模型并预热;Readiness 应表示模型已经能服务,缩容则先停止接收新请求,再排空在途生成。

扩缩容不能只盯 GPU 利用率。高利用率可能是有效批处理,也可能是队列失控;排队时长、待处理 token、KV 压力和 SLO 更接近用户体验,并应配合冷启动余量和滞回防止反复震荡。

四、网关要感知计算代价

轮询适合作为基线,但请求数量不等于负载。一个短问答和一个长文生成各算一次请求,消耗的计算、显存和连接时间可能完全不同。

网关至少应使用模型版本、租户配额、输入长度、输出预算、截止时间,以及实例队列和缓存提示。它负责认证、限流、路由和流式协议,不应理解业务提示词来决定业务优先级;应用层可以声明已鉴权的优先级与预算。

流式响应必须传递取消、超时和背压。客户端已经收到部分 token 后,不应像普通幂等查询一样透明重试;取消若没有传到引擎,连接虽已断开,GPU 仍可能继续做无用计算。

五、缓存命中与排队的权衡

Prefix Cache 的键应包含模型权重版本、tokenizer、模板、适配器及相关注意力配置。缓存通常按块共享,命中块数不等于整段请求命中;网关索引也可能过期,最终必须由引擎验证,不兼容时安全回退。

多租户要隔离缓存命名空间、权限和淘汰预算。日志不要直接记录敏感提示词;即使使用哈希,也要考虑存在性和时延侧信道,公共前缀应通过明确策略共享。

看一个具体场景:缓存实例 A 很繁忙,但已有请求需要的共享文档前缀;实例 B 空闲,却没有这段缓存。路由可以:

  1. 留在 A,省掉重算,但承担排队。
  2. 把兼容 KV 搬到 B,减少等待,却支付定位、传输、布局转换和显存分配成本。
  3. 让 B 重算前缀,计算多一些,但可能更快开始 decode。

选择目标应比较预计端到端时延和资源成本,而不是孤立的命中率。 热点前缀还可能形成“越命中,越拥堵”的反馈;可以在多个实例复制热点、限制亲和权重,或排队超阈值后退回负载优先。缓存下沉到主机内存或 SSD 也只是容量换恢复延迟。

flowchart TD
    A["请求与候选实例"] --> B["检查版本、权限和容量"]
    B --> C["估计队列、命中和迁移代价"]
    C --> D{"满足 SLO 的低代价路径"}
    D --> E["在缓存实例执行"]
    D --> F["迁移兼容 KV"]
    D --> G["在空闲实例重算"]
    F --> H{"导入成功?"}
    H -->|是| I["继续生成"]
    H -->|否| G

六、PD 分离不是默认最优

Prefill 和 decode 的干扰较大时,可以把它们放进不同资源池,分别配置批处理和容量。这样可能改善 decode 的稳定性,但 decode 必须接收 prefill 建立的 KV,网络、格式、并行布局和目标显存都会进入关键路径。

sequenceDiagram
    participant R as 路由协调层
    participant P as Prefill 池
    participant D as Decode 池
    R->>P: 提交输入和请求标识
    P->>P: 建立 KV
    R->>D: 申请接收容量
    P->>D: 传输兼容 KV
    D->>D: 执行后续生成
    D-->>R: 流式返回输出

两类容量必须联动。只扩 prefill 会造成中间状态堆积,只扩 decode 又会让输入处理排队;低并发、短上下文、网络较弱或资源池较小时,共置部署往往更简单。PD 分离只有在隔离收益覆盖搬运、碎片化和运维成本时才值得采用。

七、最小实验与指标

先固定模型、引擎、精度和生成配置,测单实例基线,再加入两个副本和一个薄 Go 网关。第一版只做流式转发、取消、超时、有限队列和轮询;之后再加入队列信息与前缀亲和,状态缺失时回退基线。

测试覆盖冷缓存、公共前缀、热点集中、长短输入输出混合和跨租户隔离。可用模拟后端验证发现、流式协议和故障处理,但模拟不能证明 GPU 加速或生产容量;本文不提供未经测量的性能数字。

实验前固定指标口径:

  • TTFT:请求发出到首个有效输出 token,包含网络、排队和执行时间。
  • TPOT:首 token 后到最后 token 的耗时除以后续 token 数,并另记 token 间隔。
  • P95:在同一窗口计算请求级 TTFT、TPOT、总时延,并按请求长度分组。
  • Goodput:单位时间内同时满足 TTFT、TPOT 等约定 SLO 的成功请求数;同时记录吞吐、拒绝、失败和超时。

还要记录输入输出 token、排队时间、显存、KV 命中 token、传输字节和故障回退。一次只改变一个主要因素:先比较轮询与负载感知,再比较缓存亲和,最后在相同预算下比较共置与 PD 分离;任何结论都只适用于对应硬件、负载和测量边界。

结语

推理 Infra 的因果链可以压缩为:计算特征决定引擎策略,资源层提供可用执行环境,网关决定请求放置,缓存和 PD 分离把部分计算换成状态与网络成本。最终应优化满足服务承诺的有效产出,而不是单独追求 GPU 利用率或缓存命中率。


文章作者: Onefly
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Onefly !
评论
  目录