推理服务不是"把模型包成 HTTP 接口",而是一组互相牵制的取舍:算力归谁、请求怎么排队、显存里放什么、延迟由谁兜底。同一个模型,POC 式与生产式部署的代码可能只差几十行,成本曲线却差一个量级。
1. 五个决策点
| 决策点 | 偏"自建/集群/异步/批/共享" | 偏"API/单机/同步/不批/独占" | 拍错的代价 |
|---|---|---|---|
| 自建还是调 API | 量大且稳、数据不出域、需锁版本 | 流量波动、模型迭代快、无 GPU 运维 | 卡长期闲置,折旧吃掉预算 |
| 单机还是集群 | 单卡装不下、要多副本抗故障 | 单卡放得下、QPS 低 | 调度与网络开销超过收益 |
| 同步还是异步 | 单请求数十秒以上、可离线 | 交互式、需即时反馈 | 连接池被长任务耗尽 |
| 批还是不批 | 离线批量、延迟容忍高 | 首 token 即体验核心 | TTFT 崩塌,交互不可用 |
| 共享还是独占 | 多模型、低负载、成本敏感 | 硬 SLA、显存紧、干扰不可接受 | OOM 连锁,一模型拖垮整卡 |
判据一句话:先量化请求量与延迟目标,再决定形态。没有 QPS 与 TTFT 目标的架构讨论都是审美。
2. 成本结构:自建与托管 API 的对照
自建成本不是一个 GPU 单价,而是四项之和:算力折旧 + 闲置率 + 运维人力 + 机会成本。
| 成本项 | 自建(GPU 为主) | 托管 API | 反转点 |
|---|---|---|---|
| 计费单位 | 卡·小时(按厂商报价核对) | 按输入/输出 token | 月 token 跨阈值后自建更省 |
| 固定性 | 全额固定,闲时照付 | 随量线性,无请求即零 | 波动越大越偏 API |
| 闲置率 | 典型量级 30%–60% 空转 | 不适用 | 过半则自建账面优势消失 |
| 运维人力 | 需专人管驱动、显存、调度 | 由厂商承担 | 团队小则人力超过差价 |
| 版本风险 | 升级自己重测、自己回滚 | 厂商升级即生效(也是风险) | 需锁版本时自建可控 |
| 峰值能力 | 按峰值采购,峰后闲置 | 弹性,但可能限流 | 峰均比大于 2 偏 API |
把它变成可算的式子,比拍脑袋可靠:
# 粗略估算(单价按厂商报价核对,不要用记忆中的数字)
gpu_month = GPU_HOUR_PRICE * 24 * 30 * replica_count # 算力折旧
idle_loss = gpu_month * idle_ratio # 闲置浪费
ops_cost = engineer_month_cost * ops_headcount_fraction # 运维分摊
self_host = gpu_month + idle_loss + ops_cost
tokens_month = qps_peak * avg_tokens_per_req * 3600 * 24 * 30 * duty_cycle
api_cost = tokens_month / 1e6 * price_per_million_tokens
结论通常不是谁更便宜,而是分层:稳定基座自建、峰值溢出走 API。混合结构把闲置率转嫁给厂商,代价是两套链路都要维护。
3. 吞吐与延迟的对立
连续批处理与 PagedAttention 解决同一类问题:不让 GPU 等请求。它们不是免费的。
| 手段 | 收益 | 代价 | 判据 |
|---|---|---|---|
| 静态批处理 | 简单,吞吐明显提升 | 整批等最慢一条,尾延迟拉长 | 离线、批内长度相近 |
| 连续批处理 | 每步重排,吞吐显著提升 | 调度开销,长度差异致抖动 | 在线默认 |
| PagedAttention | KV 按页分配,碎片大降 | 实现复杂,页表管理开销 | 并发高、长度方差大 |
| 前缀缓存 | 相同前缀免重算,TTFT 降 | 命中率依赖提示结构 | 系统提示长且共享 |
| 投机解码 | 单请求延迟降 | 换时间,吞吐可能降 | 低 QPS、延迟敏感 |
一条判据:批大小存在吞吐饱和点,越过之后吞吐不再涨、TTFT 继续恶化。该点只能实测,可先用下面参数逼近:
# 服务端参数骨架(数值按实测与厂商文档核对)
max_num_seqs: 256 # 同时在批请求数上限
max_num_batched_tokens: 8192 # 单步 token 上限,吞吐/延迟折中点
gpu_memory_utilization: 0.90 # KV 预分配比例,留碎片余量
enable_prefix_caching: true
enable_chunked_prefill: true # 长 prompt 分块,改善 TTFT 抖动
gpu_memory_utilization 调到 0.95 以上看似榨干,实际把 OOM 从启动期推到运行期,后者代价高得多。
4. 四种部署形态
| 形态 | 延迟 | 隔离性 | 成本 | 判据 |
|---|---|---|---|---|
| 进程内 | 最低,无网络 | 无,与业务抢内存 | 最低 | 单模型、低 QPS、小模型 |
| 独立服务 | 一次往返 | 强,可独立扩缩 | 中 | 多语言客户端、需独立伸缩 |
| 边车 | 本机回环 | 中,随 Pod 生命期 | 中高,按 Pod 复制 | 每实例需专属模型 |
| Serverless | 冷启动不定 | 强 | 低频最优、高频最贵 | 突发、低频、可容忍冷启 |
边车最易误用:它把模型副本数与业务副本数绑死,业务扩容时显存跟着翻倍。判据是模型副本需求曲线是否与业务一致。
resources:
limits:
nvidia.com/gpu: "1" # 整卡独占;共享需 device plugin 支持
requests:
cpu: "4"
memory: "16Gi"
5. GPU 利用率的三层浪费
| 浪费层 | 表现 | 量化手段 | 处置 |
|---|---|---|---|
| 显存碎片 | 总量够但分配失败 | reserved 与 allocated 差值 | PagedAttention、统一预分配 |
| batch 排不满 | GPU 空转等请求 | 批大小分布、每步 token 数 | 连续批处理、调队列深度 |
| KV 膨胀 | 长会话吃满显存 | 单请求 KV × 并发数 | 限上下文、前缀缓存、驱逐 |
显存估算不必精确到 MB,但要能算量级:
weights ≈ params × bytes_per_param # fp16 约 2,int8 约 1,int4 约 0.5
kv_cache ≈ 2 × layers × kv_heads × head_dim × ctx_len × concurrency × bytes
total ≈ weights + kv_cache + activation_peak + 碎片余量(10%~20%)
长上下文场景里 KV cache 往往超过权重本身——"装得下"和"跑得动并发"是两件事。
6. 算力调度与公平性
| 策略 | 机制 | 公平性风险 | 适用 |
|---|---|---|---|
| 静态分配 | 每模型固定卡数 | 无争抢,闲置不可回收 | 有硬 SLA 的核心模型 |
| 队列调度 | 排队按容量消费 | 长请求饿死短请求(或反之) | 异步任务、批处理 |
| 优先级抢占 | 高优抢低优 | 低优反复被中断,进度不可预期 | 在线优先、离线可断 |
| 多模型混布 | 共享卡,切时间片或显存 | 相互干扰,尾延迟不可控 | 多小模型、低负载 |
抢占的关键不是能不能抢,而是被抢的任务是否可恢复。离线任务可抢;长事务型请求被抢即失败,应走重试而非抢占。
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: offline-batch
value: 100
preemptionPolicy: PreemptLowerPriority
7. 多模型路由与降级链
路由依据只有三类:任务类型、成本上限、SLA。冲突时以 SLA 为准——SLA 违约可观测,成本超支往往月底才发现。按任务分流的风险在分类器自身会错,按成本升级的风险在判据难定,按 SLA 分层的风险在抖动传导。
routes:
- match: intent == "chat"
primary: MODEL_LARGE
fallback:
- on: [timeout > T1, error_rate > R1]
use: MODEL_SMALL
- on: [all_providers_unavailable]
use: STATIC_TEMPLATE # 终点必须是确定性输出
终点若还是另一个模型,只是延长故障,不是降级。
8. 容量规划与扩容滞后
容量规划回答三件事:峰值 QPS、TTFT 与 TPOT 目标、单副本并发上限。前两个业务给,第三个只能实测。
副本数 ≈ ceil( peak_qps × p99_latency / 单副本并发上限 )
单副本并发上限 ≈ min( 显存可容 KV 数, 调度器可排深度, CPU 预处理能力 )
扩容滞后来自三层:指标窗口(数十秒到数分钟)、调度与拉镜像(数十秒)、权重加载(数十秒到数分钟)。缓解依次是用队列长度而非 CPU 触发扩容、预热镜像、本地缓存权重。就绪探针要等到能出 token 再放流量。结论:扩容只能应对缓变,突发必须靠预留容量或排队削峰。
9. 可观测性与演进路径
| 指标 | 回答的问题 | 异常信号 |
|---|---|---|
| 排队时长 | 算力不够还是调度慢 | 排队占总延迟一半以上 |
| 批大小分布 | 吞吐是否被排不满拖累 | 长期低于阈值 |
| KV 使用率 | 显存被谁占着 | 持续贴近上限即 OOM 前兆 |
| OOM 重启次数 | 容量是否真够 | 任何非零值都要查 |
演进不要跳级:单卡试点(先钉质量基线)→ 多副本(负载均衡与健康检查)→ 批处理服务(队列、优先级、可中断)→ 混布调度(隔离与抢占,接受尾延迟上升)。每级都该有触发条件,而非按计划表推进。
10. 排错
| 现象 | 可能原因 | 处置方向 |
|---|---|---|
| 首 token 慢但总时长正常 | 排队或 prefill 拥塞 | 看排队时长与 prefill 占比 |
| 吞吐低、GPU 利用率低 | 批排不满或 CPU 预处理瓶颈 | 看批大小分布、tokenize 耗时 |
| 偶发 OOM 集中在高峰 | KV 预分配比例过高 | 降 utilization、限上下文、加驱逐 |
| 混布后尾延迟变差 | 共享卡相互干扰 | 改独占或加时间片隔离 |
| 扩容后延迟没改善 | 冷启动慢、流量没打到新副本 | 查就绪探针与负载均衡 |
| 成本超预算但 QPS 未涨 | 长输出、重试放大、缓存未命中 | 按 token 维度拆账 |
11. 边界
| 场景 | 不该做的事 | 更合适的做法 |
|---|---|---|
| QPS 很低、请求稀疏 | 建集群、上调度器 | 单进程或托管 API |
| 单卡放得下且负载平稳 | 分布式推理 | 单卡多副本,简单可靠 |
| 延迟不敏感(离线) | 优化 TTFT | 把预算花在吞吐与成本 |
| 任务强确定(规则可判) | 用模型兜底 | 规则优先,模型处理长尾 |
| 无 GPU 运维能力 | 自建集群 | 托管 API + 峰值混合 |
过度设计的信号很一致:说得出每一层,却说不出它解决了哪个已观测到的问题。
12. 自测
- 判断"自建还是调用 API"时,哪两个量化指标必须先有?
- 闲置率对自建成本的影响,为什么比 GPU 单价更关键?
- 连续批处理提升吞吐,为什么仍可能让尾延迟变差?
gpu_memory_utilization设到 0.95 以上,风险为什么落在运行期?- 降级链的终点为什么不能是另一个模型?
- 扩容滞后里哪一层通常最大,它对"抗突发"意味着什么?