L4.3 性能、成本与混合部署
三维坐标
layer: L4(推理与 Serving)|level: Senior / Architect|pillar: 训推框架这是 L4「推理与 Serving」层的收官篇。前两篇我们把单引擎的吞吐与延迟榨到了极限;本篇升一个维度——当你手里有一组性能/成本各异的模型与算力(小模型、大模型、边缘卡、云端卡),如何用路由、混合部署、容量规划把「每一分钱的 token 价值」最大化,并在慢请求来临时快速定位瓶颈。这是从「会调一个引擎」到「会设计一个服务体系」的分水岭。
学习目标
- 前置知识:读过 L4.1(推理服务器与运行时:知道 vLLM/TGI 怎么把一张卡的吞吐榨满、continuous batching 与 PagedAttention)与 L4.2(推理加速机制:量化、投机解码、KV Cache 的延迟/显存账);对 L0.1 端到端全景里「训练—推理—服务」的分层落地版图有整体印象。会写基础 Python、用过
curl打 HTTP 接口即可,无需 GPU。 - 学完产出:① 能画出一个生产级路由网关的决策流(语义缓存 → 复杂度分类器 → SLA 分层 → Fast/Slow 路径 → 级联升级),并说清每一跳的成本与延迟代价;② 能对比「成本优先 / 延迟优先 / 质量优先级联」三种路由策略,并解释为什么真实系统是「按租户分层应用不同策略」而非「选一个最好的」;③ 能把边缘-云混合推理的「升级 or 不升级」决策写成一个含网络 RTT、两档模型成本、质量增益的不等式,并理解网络为什么是新瓶颈;④ 能用 Little's Law 与 M/M/c 排队模型反推「该买几张卡」,并解释为什么把利用率 ρ 跑到 1 是延迟灾难而非好事;⑤ 亲手跑通一个纯 CPU 的双模型路由网关,用分段时间戳日志区分瓶颈出在「路由 / 排队 / 服务」哪一段。
- 阅读姿势:盯住一条主线——「在一组性能/成本各异的异构模型与算力上,把每一分钱换成最多的 token 价值」。无论是 路由分流、边缘下沉,还是容量规划,本质都在回答同一个问题:这个请求,到底值得用多贵的算力去伺候?
背景与现状
到了 L4 的成本治理阶段,主要矛盾已经从「单卡能不能跑」变成「整个推理车队(fleet)的单位智能成本」。一个朴素但被反复忽视的事实是:绝大多数线上请求并不需要最强的模型。把「今天天气怎么样」这种简单 query 也路由到 70B 大模型,等于用跑车送外卖——延迟高、成本高、还挤占了真正需要大模型的复杂请求的算力。
由此诞生了三个相互咬合的工程命题:
- 多模型路由(Model Routing):按成本 / 延迟 / 质量把请求分流到不同档位的模型。简单 query 走小模型(便宜、快),复杂 query 走大模型(贵、慢但准)。这是 LLM 时代的 「智能负载均衡」。
- 边缘-云混合推理(Edge-Cloud Hybrid):把对延迟敏感、隐私敏感、或可被小模型搞定的部分下沉到边缘,把需要大算力的部分留在云端。本质是一次空间维度的成本/延迟权衡。
- 容量规划与预测(Capacity Planning):用排队论回答「我到底需要几张卡」——既不能买少了导致排队雪崩,也不能买多了让 GPU 空转烧钱。
业界信号:从 OpenAI 的
gpt-4o/gpt-4o-mini分档、Anthropic 的 Haiku / Sonnet / Opus 三级,到开源侧 RouteLLM、FrugalGPT 等「级联路由」工作——「用对的模型做对的事」已经成为推理 FinOps 的第一性原理。路由器本身甚至成了一个独立的、可被训练优化的小模型。
一句话定位:L4.3 解决的是「在一组异构模型与算力上,把成本、延迟、质量三者做帕累托最优」的工程问题。
原理与架构
2.1 多模型路由的决策流
路由的核心是一个决策函数 route(request) -> model,输入是请求特征(prompt 长度、领域、是否需要工具调用、SLA 等级),输出是目标模型。下图是一个生产级路由网关的决策流:
读这张图的三个要点:
- 语义缓存优先:命中缓存的请求成本与延迟趋近于零,是性价比最高的一层,必须放在最前。
- 分类器是路由的大脑:可以是简单规则(prompt 长度 > 阈值 → 大模型),也可以是一个被训练过的轻量打分模型(如 RouteLLM 的做法)。规则路由便宜但粗糙,模型路由准确但本身有开销。
- 级联(Cascade)兜底:先用小模型试,置信度不够再升级到大模型。FrugalGPT 证明这能在保持质量的同时大幅降本——因为大部分请求小模型就够了。
2.2 三种路由策略的权衡
| 策略 | 决策依据 | 优点 | 代价 | 典型场景 |
|---|---|---|---|---|
| 成本优先 | 在满足质量下限的前提下选最便宜模型 | 单位 token 成本最低 | 长尾质量波动 | 内部批处理、非关键摘要 |
| 延迟优先 | 选 P99 最低 / 队列最空的实例 | TTFT 与端到端延迟稳定 | 可能动用更贵的预留算力 | 实时对话、语音交互 |
| 质量优先 / 级联 | 小模型试 → 不达标升级 | 质量有保障、平均成本仍低 | 升级请求延迟翻倍 | 代码生成、复杂推理 |
这里的关键在于:路由不是「选一个最好的策略」,而是按租户 / 按 SLA 分层应用不同策略。尊享档租户走延迟优先,标准档租户走成本优先级联——这才是 FinOps 与 SLA 同时达标的解法。
2.3 边缘-云混合推理
混合部署的本质是把路由的「Fast/Slow」映射到物理空间:边缘=Fast(近用户、低延迟、保隐私),云端=Slow(大算力、高质量)。设计时三条红线:
- 网络是新瓶颈:边缘上云的往返延迟(RTT)可能比模型本身推理还久,升级决策要算上回传成本。
- 边缘模型必须量化:边缘卡显存小,INT8/INT4 量化是入场券。
- 一致性与回退:边缘离线时要能降级(缓存兜底 / 拒绝复杂请求),不能整条链路挂掉。
2.4 容量规划:用排队论说话
「我该买几张卡」不是拍脑袋,而是排队论问题。把推理服务建模为 M/M/c 队列(泊松到达、指数服务时间、c 个服务台/GPU 副本):
Little's Law(利特尔法则) 是地基:
其中 L = 系统中平均请求数,λ = 到达率(QPS),W = 平均停留时间(排队 + 服务)。
由此推 导关键的利用率:
μ 是单副本服务率(每秒能处理多少请求),c 是副本数。核心反直觉结论:
当 ρ → 1,排队延迟趋于 无穷。容量规划的目标不是把 GPU 跑满(ρ=1 是延迟灾难),而是把
ρ稳定在 0.6~0.75 这个甜区——既留出突发缓冲,又不浪费太多算力。这就是为什么「GPU 利用率 100%」在 Serving 场景往往是个危险信号而非好事。
容量预测的实操闭环:采集历史 QPS 时序 → 预测峰值(按周/日季节性)→ 反推所需副本数 c(令 ρ≈0.7)→ 配置 HPA 弹性扩缩 + 预留 buffer 应对冷启动。
2.5 推理慢路径排障 Playbook
当 P99 延迟飙升,按下面这张分层 playbook 自上而下排:
| 层 | 症状 | 高频根因 | 定位手段 |
|---|---|---|---|
| 路由层 | 慢请求集中在某模型 | 路由把简单请求误判到大模型 / 缓存失效 | 按 model 维度切分延迟直方图 |
| 排队层 | TTFT(首 token 时延)高,但单 token 快 | batch 等待 / 队列堆积(ρ→1) | 看 queue_time 与队列长度指标 |
| Prefill 阶段 | 长 prompt 请求慢,短 prompt 快 | prefill-bound:长上下文算力打满 | 按 prompt 长度分桶看延迟 |
| Decode 阶段 | 输出长的请求慢 | decode-bound:显存带宽 / KV Cache 瓶颈 | 看 decode 速率 (token/s) 是否随 batch 下降 |
| 长尾抖动 | 平均正常,P99 突刺 | GC / 抢占 / 某请求超长 / 节点异常 | 抓 P99 请求的 trace 逐段计时 |
核心方法论:先分层、再分桶、最后抓 trace。永远不要在「整体平均延迟」上找原因——长尾问题在平均值里是隐形的。区分 prefill-bound(算力密集,吃 FLOPS) 和 decode-bound(访存密集,吃 HBM 带宽) 是 LLM 推理排障的分水岭,二者的优化手段完全相反。
动手实践:极简代码实操
实验目标:用 FastAPI 搭一个按成本/延迟路由的双模型网关——根据 prompt 长度把请求路由到 fast(小模型,快)或 slow(大模型,慢)两个 mock 端点,注入慢请求,再用结构化时间戳日志定位瓶颈出在「路由 / 排队 / 服务」哪一段。纯 CPU 可跑,无需 GPU。产出物:一份带分段耗时的请求日志 + 一句瓶颈定位结论。
3.1 环境准备
# 推荐 Python 3.11 ;用 uv 或 venv 隔离
python3 -m venv .venv && source .venv/bin/activate
pip install fastapi "uvicorn[standard]" httpx
3.2 代码:双模型路由网关(含分段计时)
把下面内容存为 gateway.py。它在同一个进程内用 FastAPI 同时挂三类端点:两个 mock 模型(fast/slow)和一个路由网关。
import asyncio
import time
import random
import logging
from fastapi import FastAPI, Request
from pydantic import BaseModel
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
log = logging.getLogger("gw")
app = FastAPI()
# ---- 路由阈值与成本表(单位:相对成本) ----
LEN_THRESHOLD = 40 # prompt 长度阈值:超过走 slow
COST = {"fast": 1.0, "slow": 8.0}
# ---- 用信号量模拟两个模型的有限并发(容量) ----
SEM = {"fast": asyncio.Semaphore(8), "slow": asyncio.Semaphore(2)}
class Req(BaseModel):
prompt: str
slow_inject: bool = False # 实验开关:是否注入慢请求
async def mock_model(name: str, prompt: str, slow_inject: bool) -> dict:
"""模拟一次推理:fast 快、slow 慢;prefill 随 prompt 长度增长。"""
t0 = time.perf_counter()
async with SEM[name]: # 排队点:拿不到信号量就在这等
t_acquired = time.perf_counter()
queue_ms = (t_acquired - t0) * 1000
# prefill:随 prompt 长度线性增长(模拟 prefill-bound)
prefill = len(prompt) * (0.002 if name == "fast" else 0.006)
# decode:固定基线
decode = 0.05 if name == "fast" else 0.4
if slow_inject: # 注入一个长尾慢请求
decode += 2.0
await asyncio.sleep(prefill + decode)
svc_ms = (time.perf_counter() - t_acquired) * 1000
return {"model": name, "queue_ms": round(queue_ms, 1),
"service_ms": round(svc_ms, 1), "cost": COST[name]}
@app.post("/v1/fast")
async def fast(r: Req):
return await mock_model("fast", r.prompt, r.slow_inject)
@app.post("/v1/slow")
async def slow(r: Req):
return await mock_model("slow", r.prompt, r.slow_inject)
# ---------------- 路由网关 ----------------
@app.post("/route")
async def route(r: Req):
t_in = time.perf_counter()
# 1) 决策:按 prompt 长度路由(成本/延迟策略的最简形态)
target = "slow" if len(r.prompt) > LEN_THRESHOLD else "fast"
t_decide = time.perf_counter()
# 2) 转发到对应 mock 模型(此处直接进程内调用以保持纯 CPU 单文件)
resp = await mock_model(target, r.prompt, r.slow_inject)
t_out = time.perf_counter()
# 3) 分段计时日志:路由 / 排队 / 服务 三段拆开
route_ms = (t_decide - t_in) * 1000
total_ms = (t_out - t_in) * 1000
log.info(
f"[ROUTE] target={target} len={len(r.prompt)} "
f"route_ms={route_ms:.2f} queue_ms={resp['queue_ms']} "
f"service_ms={resp['service_ms']} total_ms={total_ms:.1f} "
f"cost={resp['cost']} inject={r.slow_inject}"
)
return {"routed_to": target, "total_ms": round(total_ms, 1), **resp}
3.3 运行与注入慢请求
开两个终端。终端 A 起网关:
uvicorn gateway:app --port 8000 --log-level warning
终端 B 打流量:先打正常请求观察路由分流,再注入慢请求并发压测:
# 1) 短 prompt → 应路由到 fast
curl -s localhost:8000/route -H 'content-type: application/json' \
-d '{"prompt":"今天天气如何"}'
# 2) 长 prompt → 应路由到 slow(超过长度阈值)
curl -s localhost:8000/route -H 'content-type: application/json' \
-d '{"prompt":"请详细分析分布式训练中数据并行张量并行流水并行三者在通信开销显存占用上的取舍"}'
# 3) 注入慢请求 + 并发压测 slow 端点(slow 容量只有 2,制 造排队)
for i in $(seq 1 6); do
curl -s localhost:8000/route -H 'content-type: application/json' \
-d '{"prompt":"超长复杂推理请求需要路由到大模型并注入慢路径来观察排队与服务耗时的拆分情况","slow_inject":true}' &
done; wait
3.4 观察与定位瓶颈
回到终端 A 看结构化日志,关键在三个字段:
route_ms:路由决策耗时——通常极小(<1ms)。如果它大,说明分类器/特征提取是瓶颈。queue_ms:在信号量上排队的时间——并发压 slow(容量=2)时,第 3 个之后的请求queue_ms会显著上涨,这就是 排队层瓶颈(ρ→1) 的直接证据。service_ms:真正「推理」耗时——注入slow_inject后会多出约 2000ms,对应 decode-bound 的长尾。
定位结论模板:
6 个并发 slow 请求中,前 2 个
queue_ms≈0、后 4 个queue_ms递增 → 瓶颈在排队层(slow 容量不足,ρ 超 1);同时service_ms因注入比基线高 ~2000ms → 叠加了 decode 长尾。解法:给 slow 扩容(提高c)+ 对长请求做限流/降级。
踩坑预警 (Gotchas)
- 不拆分段计时 = 白测:只记
total_ms永远分不清慢在「排队」还是「服务」。排障的第一性原理就是分段打时间戳。 - 信号量并发数要 < 压测并发数才能复现排队**:slow 容量设 2、并发打 6,才能看到
queue_ms上涨。若容量 ≥ 并发,队列永远是空的,演示不出排队瓶颈。 async def里别用阻塞time.sleep:必须用await asyncio.sleep,否则会卡死事件循环,所有请求被串行化,假象成「全在排队」。perf_counter而非time.time:墙钟时间受 NTP 校时影响,计时务必用单调时钟time.perf_counter()。- CPU 机器别把 sleep 调太大:本实验靠 sleep 模拟耗时,纯 CPU 即可;真要换成真实模型,记得区分 prefill(吃 CPU/FLOPS)与 decode(吃带宽)。
深入思考
下面三题每题先给题干,再用
<details>折叠一份图文并茂的参考答案。建议先合上答案自己想 3 分钟,再展开对照。
思考题 1:路由的成本-质量帕累托
你设计的级联路由「先小模型、不达标升级大模型」。但置信度阈值定高了升级太频繁(成本逼近全走大模型),定低了质量下滑。结合 2.1 节的级联兜底 与 2.2 节的策略权衡,给出一套用线上数据自适应调阈值的方案,并说明如何量化「每省一块钱损失多少质量」——即如何画出这条帕累托前沿曲线?
展开参考答案(含阈值自适应闭环图 + 算一遍)
结论:置信度阈值不是一个拍死的常数,而是一个由「线上影子评测 + 业务质量底线」反馈调节的旋钮;扫一遍阈值就能画出「平均成本 vs 平均质量」的散点,连成帕累托前沿,再在前沿上挑一个满足质量底线的最便宜点作为工作点。
用具体数字算一遍(设小模型每千次 1 美元、大模型每千次 8 美元;级联模式下升级请求已先付过一次小模型试算,故平均成本 = 1 + 升级比例 × 8):
| 阈值 τ | 升级比例 | 平均成本(每千次) | 平均质量(答对率) | 边际:每降 1 个质量点省多少 |
|---|---|---|---|---|
| 0.95(保守) | 60% | 1 + 0.6 × 8 = 5.8 美元 | 96% | — |
| 0.80(甜区) | 30% | 1 + 0.3 × 8 = 3.4 美元 | 93% | 省 2.4 美元 / 掉 3 点 ≈ 0.8 美元/点 |
| 0.65(激进) | 12% | 1 + 0.12 × 8 = 1.96 美元 | 88% | 再省 1.44 美元 / 再掉 5 点 ≈ 0.29 美元/点 |
怎么读这条曲线:从 τ=0.95 往下走,前期「降本快、掉质慢」(甜区),越往后「再降本就要付出陡峭的质量代价」。帕累托前沿的拐点附近就是最优工作点——若业务质量底线是 90%,那么 τ=0.80 这一档(3.1 美元、93%)就是「在达标前提下最便宜」的选择。落地时把 τ 做成可热更新的配置,用滑动窗口的影子评测每天重算一遍前沿,让阈值随模型版本、流量分布漂移自适应,而不是上线时定死。
思考题 2:混合部署的回传决策
边缘小模型对一个请求置信度不足,要不要上云?上云能提质量但要付出 RTT + 云端推理 + 回传的总延迟与成本。结合 2.3 节的混合推理三条红线,写出这个「升级 or 不升级」的决策不等式(含网络 RTT、两档模型成本、质量增益的货币化),并讨论网络抖动会如何动摇这个决策。
展开参考答案(含上云决策不等式流图)
结论:上云当且仅当「质量增益折算成的钱」大于「上云多付的钱 + 多花的延迟折算成的钱」;网络 RTT 同时进了成本项和延迟项,所以网络一抖动,这个不等式可能从成立翻转成不成立——这就是为什么混合部署必须把网络当成一等公民来建模。
决策不等式(货币化后统一量纲,「上云」当且仅当下式成立):
V_q · ΔQ > ΔC_infer + C_net + V_lat · (RTT + T_cloud − T_edge)
V_q · ΔQ:质量增益的货币价值。ΔQ是大模型相对小模型的答对率提升,V_q是「一个质量点值多少钱」(由业务定,如客服场景一次答错的返工成本)。ΔC_infer:云端大模型与边缘小模型的单次推理成本差。沿用正文口径,约为 7 美元每千次(8 减 1)量级,单次按比例折算。C_net:上行请求 + 回传结果的流量与带宽成本。V_lat · (RTT + T_cloud − T_edge):上云多出来的延迟(网络往返 + 云端推理减去本可在边缘出的时间)折算成的钱。
算一遍(直觉量级):若上云能把答对率从 80% 提到 95%(ΔQ=0.15),每个质量点对该业务值 0.5 美元,则收益侧 ≈ 0.075 美元;若该次上云的推理成本差 + 流量 ≈ 0.01 美元、RTT 80ms + 云端 300ms 折算延迟成本 ≈ 0.03 美元,则代价侧 ≈ 0.04 美元 < 0.075 美元 → 上云划算。
网络抖动如何动摇决策:RTT 同时出现在「延迟项」里——一旦跨区拥塞把 RTT 从 80ms 飙到 1.5 秒以上(跨区丢包重传并不罕见),按同一线性折价,延迟项的钱会从约 0.03 美元涨到 0.12 美元以上,代价侧合计突破 0.13 美元 > 0.075 美元收益,原本划算的上云翻转为不划算。所以正文 2.3 才把「网络是新瓶颈」列为第一条红线:实践中要用实时 RTT 动态喂入这个不等式,并在网络恶化时自动收紧上云策略、转向缓存兜底或边缘降级,而不是用一个静态阈值一刀切。
思考题 3:容量规划的弹性悖论
你按 Little's Law 把稳态 ρ 控在 0.7,但 GPU 冷启动(拉镜像 + 加载权重)要 90 秒,而流量峰值在 30 秒内到来。HPA 反应不过来就会雪崩。结合 2.4 节的排队论 与 2.5 节的排障分层,设计一套**「预测式扩容 + 预留 warm pool + 排队降级」**的三层防线,并论证为什么「只靠基于当前指标的被动 HPA」在 LLM Serving 场景必然失效。
展开参考答案(含三层弹性防线时序图 + 对比表)
结论:被动 HPA 的致命伤是「它在峰值已经到了才开始扩容,而 LLM 冷启动比峰值上升还慢」——等新副本就绪时雪崩早已发生;正解是在时间轴上提前布防:预测式扩容抢在峰值前拉起、warm pool 把冷启动从 90 秒压到秒级、排队降级在最坏情况下保住核心请求不雪崩。
为什么被动 HPA 必然失效(算一遍时间账):
| 时刻 | 流量事件 | 被动 HPA 的反应 | 后果 |
|---|---|---|---|
| t=0s | QPS 开始爬升 | 指标尚未越线,不动 | ρ 仍 < 1,正常 |
| t=10s | ρ 越过 0.8,触发扩容 | 开始拉新副本(冷启动) | 排队开始堆积 |
| t=30s | 峰值到顶,ρ 冲破 1 | 新副本仍在加载权重(还差 70s) | queue_ms 爆炸式上涨,雪崩 |
| t=100s | 新副本终于就绪 | 算力补上 | 但 SLA 早已被击穿、请求已超时 |
根因:HPA 是「看当前指标的反馈控制」,而 LLM 副本的冷启动延迟(拉镜像 + 加载几十 GB 权重)远大于流量峰值的上升时间。等它反应过来,ρ 早已冲过 1——正文 2.4 已经点明「ρ→1 排队延迟趋于无穷」,2.5 的排障分层也会显示这是典型的「排队层瓶颈,queue_time 暴涨」。反馈控制治不了「反应比扰动慢」的系统,必须叠加前馈(预测):第一层用预测把扩容提前到峰值之前(前馈);第二层用 warm pool 把单副本就绪时间从 90 秒压到秒级,缩短前馈所需的提前量;第三层在预测失准、池子被打穿的最坏情况下,用主动限流 + 低优降级守住「ρ 不破 1」的红线,让系统优雅降级而非雪崩。三层叠加,才能在「不浪费算力空转」与「不被突发打垮」之间同时站稳。