"要不要自己部署模型"通常以两种错误方式被回答:拿一张 GPU 报价单去比 API 单价,或者以"数据安全"为由直接买卡。前者漏了利用率和人力,后者漏了机会成本。
这篇给判据和代价结构:什么时候自托管划算、托管 API 有哪些隐性约束、混合与切换要付什么代价。
1. 先算清四类成本
| 成本项 | 自托管口径 | 托管 API 口径 |
|---|---|---|
| 算力 | 购卡摊销或按小时租,按峰值配置 | 按 token 计费,按实际用量 |
| 运维 | 驱动/框架升级、值班、扩容 | 含在单价里 |
| 利用率损失 | 低峰空转,典型量级三到五成 | 不存在 |
| 机会成本 | 团队时间投在推理栈而非业务 | 供应商锁定与迁移 |
第三项最容易漏。算法很直接:
单卡可用小时 = 24 × 30 × 0.95(预留故障与升级窗口)
有效吞吐 = 单卡 tokens/s(实测,不是标称)
月供给 token = 单卡可用小时 × 3600 × 有效吞吐 × 卡数 × 实际利用率
月成本/token = (购卡摊销 + 电费 + 运维人力) / 月供给 token
把 实际利用率 从 0.5 改成 0.9 再算一遍。如果结论翻转,说明决策押在利用率上——先解决利用率,别急着加卡。
2. 对比口径:按峰值还是按平均
| 口径 | 自托管看到的成本 | 托管看到的成本 | 倾向 |
|---|---|---|---|
| 按平均负载 | 偏高(卡闲着也算钱) | 偏低(用多少付多少) | 托管 |
| 按峰值负载 | 与平均接近 | 偏高(峰时单价与限流) | 自托管 |
| 按三年总拥有 | 含扩容与换卡周期 | 含调价与模型换代 | 逐年建模 |
只在一种口径下比较,结论必然是预设的。两条负载曲线的差距,就是弹性本身的价格。
3. 自托管的适用判据
| 判据 | 成立时 | 反例 |
|---|---|---|
| 数据不出域 | 合同或监管明文禁止外发 | "担心泄露"但无具体要求 |
| 量大且稳定 | 月 token 足够摊平固定成本 | 淡旺季差数倍 |
| 需要微调 | 通用模型不达标且有标注数据 | 提示词还没试透 |
| 延迟要求特殊 | 需要固定 P99 或极短首字延迟 | 供应商其实能满足,只是没测 |
| 需要离线/内网 | 部署环境无外网 | 只是开发环境没开白名单 |
判据要写成能被反驳的句子。"数据安全"不是判据,"客户合同第 7 条禁止把数据传出 VPC"是。
4. 托管 API 的隐性约束
| 约束 | 表现 | 缓解 |
|---|---|---|
| 限流 | 并发与 TPM/RPM 双限,峰时 429 | 客户端排队 + 退避,别重试风暴 |
| 区域 | 模型只在部分 region 提供 | 多区域端点,或接受跨区延迟 |
| 版本下线 | 旧模型有退役日期 | 固定快照名 + 离线回归集 |
| 能力漂移 | 同版本输出分布轻微变化 | 关键路径抽样比对 |
| 内容策略 | 部分输入被拒答,错误码混在业务错误里 | 错误分类,单独成类 |
版本固定要落到配置,不能靠默认值:
llm:
provider: vendor-a
model: model-x-2025-06-01 # 写全快照名,不写 latest 别名
fallback-model: model-x-2024-11-01
timeout: { connect: 2s, first-token: 8s, total: 60s }
写 model-x-latest 等于把线上效果交给供应商的发布节奏。
5. 混合架构与它的复杂度
| 形态 | 用途 | 新增复杂度 |
|---|---|---|
| 托管主 + 自托管兜底 | 供应商故障或限流时接住流量 | 两套提示词、两套质量基线 |
| 自托管主 + 托管兜底 | 常规自给,尖峰溢出 | 溢出判据、成本突增 |
| 按任务分流 | 简单任务小模型本地,复杂任务远程 | 路由误判、双份评测 |
混合的代价不在部署,在两份输出要能被同一套下游消费,所以兜底模型必须过同一套回归集:
def route(req):
if breaker.is_open("vendor-a"): # 供应商熔断
return serve_local(req, "small")
if req.est_tokens > LOCAL_MAX_CTX: # 超出本地上下文
return serve_remote(req)
return serve_remote(req)
路由函数要能被单测覆盖,每条分支至少一个用例。没被测过的兜底路径,故障当天就是第一次运行。
6. 版本变更如何灰度
| 方式 | 能回答的问题 | 成本 |
|---|---|---|
| 影子流量 | 新旧输出差多少 | 双倍推理费用,不影响用户 |
| 小流量对比 | 业务指标有没有变差 | 需要指标口径与分流能力 |
| A/B 长跑 | 细微差异是否显著 | 周期长,样本量要求高 |
| 直接切 + 回滚开关 | 出问题能不能退 | 最省事,风险最高 |
影子流量比不出"用户是否更满意",因为影子结果没有进入业务。通过标准要提前写死:
1) 影子比对:500 条历史请求,新旧一致性不低于基线
2) 小流量 5%:错误率不升,首字延迟 P95 升幅 < 10%
3) 小流量 50%:核心指标(转化/采纳率)无显著下降
4) 全量:保留 24 小时一键回退
7. 部署形态与显存约束
| 形态 | 适用 | 主要约束 |
|---|---|---|
| 单机单副本 | 内部工具、低并发 | 无冗余,升级即中断 |
| 多副本 + 负载均衡 | 在线服务 | KV Cache 不共享,前缀命中率打折 |
| 批处理队列 | 离线标注、批量摘要 | 与在线争卡,需配额隔离 |
| 单卡多模型分时加载 | 模型多、流量低 | 换模型的加载时间进入延迟 |
显存 ≈ 权重(参数量 × 每参数字节)
+ KV Cache(并发序列 × 长度 × 层数 × 头维度 × 2 × 字节)
+ 激活与框架开销(典型量级为前两项的 10%~20%)
量化只改第一项,不改第二项。长上下文场景下,量化省下的显存常被 KV Cache 吃掉——这是"量化后仍 OOM"的常见原因。精度损失必须用自己的评测集测。
8. 缓存与批处理放在哪
| 手段 | 命中条件 | 收益 | 风险 |
|---|---|---|---|
| 精确缓存 | 输入完全一致 | 高,命中即零算力 | 键要含模型版本,否则串版本 |
| 语义缓存 | 向量相似度超阈值 | 中 | 误命中给出错误答案 |
| 前缀缓存 | 相同 system prompt 前缀 | 中高,省 prefill | 前缀一变全失效 |
| 连续批处理 | 同批请求 | 吞吐提升明显 | 单请求延迟上升 |
缓存键必须含模型版本、温度与全部影响输出的参数。少一个,就会出现"升级后仍返回旧答案"。
9. 容量规划:并发、队列、超时的三角
并发上限 C ≈ 队列容量 Q / 平均处理时长 T (Little's Law 稳态)
| 定哪两个 | 后果 |
|---|---|
| 并发 + 超时 | 队列随流量自由增长,直到拒绝 |
| 并发 + 队列 | 超时被迫拉长,等待不可控 |
| 队列 + 超时 | 并发被流量决定,显存可能被打爆 |
在线服务一般定"并发 + 超时",用拒绝消化溢出;离线批处理定"并发 + 队列",让超时退化为慢而不失败。
10. 可观测与错误分类
| 指标 | 看什么 | 异常信号 |
|---|---|---|
| token 吞吐 | 输入/输出 tokens per second | 吞吐掉而 QPS 不降 → 长输入占比上升 |
| 首字延迟 P95 | 请求到第一个 token | 与总延迟脱节 → prefill 排队 |
| 队列深度 | 等待调度的请求数 | 持续 > 0 说明并发到顶 |
| 错误分类 | 限流 / 超时 / 策略拒绝 / 上游 5xx | 混在一起就无法定责 |
class LLMErrorKind(Enum):
RATE_LIMITED = "rate_limited" # 可退避重试
TIMEOUT = "timeout" # 可重试,需降级
CONTENT_BLOCKED = "blocked" # 不可重试,改输入
UPSTREAM_5XX = "upstream_5xx" # 可重试,需熔断
把 CONTENT_BLOCKED 与 UPSTREAM_5XX 一起重试是最常见的浪费:一个永远失败,一个会把供应商打到熔断。
11. 成本治理落在哪里
| 落点 | 能回答的问题 | 落地难度 |
|---|---|---|
| 按功能 | 哪个功能最贵 | 低,请求头打标即可 |
| 按租户 | 谁在用、是否要配额 | 中,需要身份贯通 |
| 按团队 | 预算归属与超支预警 | 中高,需要组织口径 |
最小可行做法是入口打三个标签,随日志落库:
meta = {"feature": "order-assistant",
"tenant": current_tenant_id(),
"trace_id": current_trace_id()}
没有这三个标签,月末只有一张总额账单,没法归因,也就没法治理。
12. 切换与回退
| 故障类型 | 能否临时切换 | 前提 |
|---|---|---|
| 供应商 5xx / 限流 | 能 | 兜底常备、提示词兼容 |
| 区域故障 | 能 | 多区域端点已测过 |
| 模型版本下线 | 能(切旧版) | 旧版仍在服务期内 |
| 能力漂移 | 难 | 只能靠离线回归集发现 |
| 自托管显存不足 | 能(溢出到托管) | 溢出判据与配额已设 |
回退要演练,不演练的回退等于没有。每季度执行一次并记录耗时:打开熔断 → 观察兜底路径 10 分钟的 P95 与错误率 → 核对输出能被下游解析 → 复位阈值、记录差异点。
13. 排错
| 现象 | 原因 | 处理 |
|---|---|---|
| 量化后仍 OOM | KV Cache 未随量化下降 | 降并发或降 max-model-len,别只降精度 |
| 吞吐高但用户抱怨慢 | 连续批处理拉高单请求延迟 | 在线与离线队列分开 |
| 升级后错误率上升 | 输出格式变了,解析器没跟上 | 灰度前跑解析层回归 |
| 429 集中在整点 | 定时任务与在线流量同池 | 定时任务单独配额或错峰 |
| 兜底切换后风格突变 | 两套提示词未适配 | 兜底模型单独维护提示词变体 |
| 缓存命中率虚高 | 键漏了模型版本或温度 | 补全键,历史缓存整体失效一次 |
14. 边界
| 场景 | 建议 | 理由 |
|---|---|---|
| 月调用量小 | 直接用托管 | 固定成本摊不平 |
| 团队无 GPU 运维能力 | 托管 | 驱动与框架问题会吃掉全部排期 |
| 需要最新模型能力 | 托管 | 自托管落后一个版本周期 |
| 一次性/短期项目 | 托管 | 采购折旧周期长于项目周期 |
| 数据合规硬性要求 | 自托管或专有部署 | 唯一不需要算账的判据 |
一句话:自托管的门槛不是买得起卡,是能持续把卡用满。 用不满的卡比按量计费贵得多,只是账单出现在采购单上,而不是每月账单上。
15. 自测
- 自托管成本四项里,哪一项最常被漏算,怎么估?
- 同一套负载为什么要按"平均"和"峰值"各算一遍?
- "担心数据泄露"为什么不构成自托管的判据?
- 影子流量能回答什么问题,不能回答什么问题?
- 量化省了权重显存,为什么长上下文还是 OOM?
- 并发、队列、超时只能定两个,在线服务通常定哪两个?