1. 为什么你的 GPU 集群总在“加卡”和“降延迟”之间反复横跳如果你正在维护一套大模型推理服务大概率遇到过这种场景业务方说“首 Token 太慢”你加了两张卡TTFT 从 2.1 秒降到 1.8 秒但 QPS 一上来又打回原形。问题出在哪不是卡不够而是你手里没有一把能丈量“延迟预算”的尺子。模型推理服务的 SLO 体系本质上是一套把用户体验翻译成硬件资源的数学语言。它要回答三个问题用户能忍受多长的首 Token 等待TTFT每个 Token 之间的间隔TPOT多少才算流畅在给定并发下我需要多少张 GPU 才能同时守住这两条线没有这套语言“加多少卡够用”就只能靠猜而猜出来的容量水位要么浪费预算要么在流量高峰时崩盘。这篇文章面向正在做推理服务容量规划的工程师我会把 SLO 指标定义、延迟预算拆解表、压测脚本、以及通过统一 API 通道做多模型基准验证的完整流程拆开讲。你不需要先有一套完美的监控体系跟着步骤走就能从零搭出一套能指导扩容决策的 SLO 框架。核心检索词就三个SLO、延迟预算、容量规划——它们分别对应“定目标”“拆阶段”“算卡数”三个动作。我试过在 70B 模型上直接用“端到端 P95 5s”做 SLO结果发现排队延迟和 Decode 延迟混在一起根本定位不到瓶颈。后来把延迟预算按请求生命周期拆成网络、排队、Pre-fill、Decode 四段每一段单独设阈值扩容决策才变得可解释。下面从场景问题开始一步步把这条链路走通。2. 推理 SLO 分阶段定义与延迟预算拆解表2.1 一个请求到底经过了哪些延迟阶段大模型推理请求和传统微服务最大的区别在于它的延迟不是单一环节产生的而是随并发量和序列长度动态漂移的。一个请求从发出到最后一个 Token 返回至少穿过五个阶段网络传输与负载均衡请求从客户端到推理网关的往返时间通常受机房位置和 LB 策略影响交互式场景下 P99 控制在 20ms 以内比较合理。请求排队请求进入推理引擎后如果当前并发已满会进入调度队列等待。这是最容易被忽视、也最容易在流量高峰时爆炸的一段。vLLM 的 continuous batching 虽然能缓解但队列延迟仍然存在。Pre-fill 阶段模型处理完整输入 prompt、生成第一个 Token 的过程。这段直接决定 TTFT是用户感知最强的延迟。Decode 阶段从第二个 Token 开始每个 Token 的生成间隔即 TPOT。它决定“流式输出”是否流畅。流式返回Token 通过 SSE 逐块推送到客户端网络抖动可能导致到达时间不均匀。把这五段拆开之后延迟预算就变成了一张可分配的表。下面这张表是我在实际项目中用的模板你可以直接改成自己业务的数值阶段指标交互式聊天 SLO代码补全 SLO批量分析 SLO网络 RTTP99 20ms 10ms 100ms排队 Queue TimeP99 1s 200ms 30sPre-fillTTFT P99 2s 500ms 10sDecodeTPOT P95 40ms 20ms 100ms端到端Total P95 5s 1s 60s注意这张表里的数值不是拍脑袋来的。交互式聊天的 TTFT P99 2s 来自一个经验用户在输入问题后如果 2 秒内看不到任何输出会开始怀疑服务是否卡死。而代码补全场景对 TTFT 更敏感因为补全是在用户打字间隙触发的超过 500ms 就会打断输入节奏。2.2 延迟预算如何反推并发满足系数有了分阶段 SLO下一步是把它们合成一个能用于容量规划的公式。核心逻辑是 Littles Law系统中的平均并发数等于到达率乘以平均驻留时间。平均驻留时间 TTFT 中位数 平均输出 Token 数 × TPOT 中位数假设你的交互式聊天场景TTFT 中位数 0.3s平均输出 256 个 TokenTPOT 中位数 0.025s那么单个请求平均驻留时间约 6.7s。如果目标 QPS 是 10平均并发就是 67。但这只是平均值。要守住 P99 TTFT 2s你还得留出排队余量。工程上常用的做法是引入一个并发满足系数并发满足系数 目标 QPS × P95 TTFT × (1 Headroom)Headroom 一般取 0.3 到 0.5用来吸收流量突发和长尾请求。这个系数直接决定你需要多少张卡来承载并发。没有 P95 TTFT 这个 SLO系数就无从算起容量规划也就失去了锚点。2.3 错误预算SLO 不是“永远达标”SLO 的另一个关键概念是错误预算。如果你定义月度可用性 99.9%那么一个月允许的不可用时间约 43 分钟或者允许 0.1% 的请求超时。错误预算的作用是给团队一个“可以犯错的空间”而不是追求零违规。在推理服务里错误预算通常按请求数计算月度 Error Budget (1 - SLO%) × 总请求数比如月总请求 1000 万SLO 99.9%那么允许 1 万次超时。一旦某周就用掉了 80% 的预算就应该触发告警暂停非必要的发布优先排查延迟劣化原因。这套机制比单纯看“平均延迟”更能反映真实的服务健康度。3. 用 TaoToken 统一 Key 接入多模型做基准验证3.1 为什么基准验证需要统一通道做容量规划之前你得先知道不同模型在你的硬件上的真实表现。但现实是你可能同时要对比 7B、14B、70B 甚至不同厂商的模型如果每个模型都单独配一套 Key 和 endpoint压测脚本会变得难以维护。TaoToken 提供的是一个统一 Key 和 API 通道兼容 OpenAI 风格的接口。你可以用同一个 Key 调用不同模型压测脚本只需要改 model 字段不用改鉴权逻辑。这对做多模型基准验证非常省事。接入信息如下官网入口https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI Base URLhttps://taotoken.net/api模型对话入口https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewriteAPI Keys 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite3.2 可复制的配置片段如果你用的是 OpenAI SDK 或兼容的客户端配置只需要三件套Base URL、API Key、Model ID。下面是一个 Python 的配置示例路径和字段名保持和官方一致# config.py import os TAOTOKEN_BASE_URL https://taotoken.net/api TAOTOKEN_API_KEY os.environ.get(TAOTOKEN_API_KEY, sk-your-key-here) # 用于基准验证的模型列表 MODELS [ gpt-4o-mini, claude-3-5-sonnet, deepseek-chat, ] # 压测参数 BENCH_CONFIG { target_qps: 10, duration_sec: 60, max_tokens: 256, temperature: 0.7, }如果你用的是 Cline 或 Claude Code 这类编码工具配置方式类似。以 Cline 的 MCP 配置为例在 settings 里填入{ mcpServers: { taotoken: { command: npx, args: [-y, taotoken/mcp-server], env: { TAOTOKEN_BASE_URL: https://taotoken.net/api, TAOTOKEN_API_KEY: sk-your-key-here, TAOTOKEN_MODEL: claude-3-5-sonnet } } } }注意Base URL 后面不要加/v1TaoToken 的 API 路径已经内置了兼容层。如果你用的是 Codex 的 auth.json格式如下{ base_url: https://taotoken.net/api, api_key: sk-your-key-here, model: gpt-4o-mini }这三件套Base URL Key Model ID是接入任何兼容 OpenAI 接口的工具的通用配置。配好之后你就可以用同一个 Key 跑不同模型的基准测试了。3.3 压测脚本测量 TTFT 和 TPOT下面这个脚本用 asyncio 并发发请求分别记录 TTFT 和每个 Token 的到达时间。它不依赖特定的压测框架直接跑就能出数据# bench_ttft.py import asyncio import time import aiohttp import statistics from config import TAOTOKEN_BASE_URL, TAOTOKEN_API_KEY, MODELS, BENCH_CONFIG async def single_request(session, model, prompt): url f{TAOTOKEN_BASE_URL}/chat/completions headers { Authorization: fBearer {TAOTOKEN_API_KEY}, Content-Type: application/json, } payload { model: model, messages: [{role: user, content: prompt}], max_tokens: BENCH_CONFIG[max_tokens], temperature: BENCH_CONFIG[temperature], stream: True, } start time.perf_counter() ttft None token_times [] async with session.post(url, jsonpayload, headersheaders) as resp: async for line in resp.content: if not line: continue now time.perf_counter() if ttft is None: ttft now - start token_times.append(now) tpot None if len(token_times) 1: intervals [token_times[i1] - token_times[i] for i in range(len(token_times)-1)] tpot statistics.median(intervals) return {ttft: ttft, tpot: tpot, total: time.perf_counter() - start} async def bench_model(model): prompt 用一句话解释什么是延迟预算。 async with aiohttp.ClientSession() as session: tasks [single_request(session, model, prompt) for _ in range(BENCH_CONFIG[target_qps])] results await asyncio.gather(*tasks, return_exceptionsTrue) ttfts [r[ttft] for r in results if isinstance(r, dict) and r[ttft]] tpots [r[tpot] for r in results if isinstance(r, dict) and r[tpot]] print(f模型: {model}) print(f TTFT P50: {statistics.median(ttfts):.3f}s) print(f TTFT P99: {sorted(ttfts)[int(len(ttfts)*0.99)]:.3f}s) print(f TPOT P50: {statistics.median(tpots):.4f}s) async def main(): for model in MODELS: await bench_model(model) if __name__ __main__: asyncio.run(main())跑之前先设置环境变量export TAOTOKEN_API_KEYsk-your-key-here python bench_ttft.py这个脚本会输出每个模型的 TTFT P50、P99 和 TPOT P50。你可以把 target_qps 逐步调高观察 TTFT 在哪个 QPS 点开始明显上升那个点就是当前副本数的容量水位。4. 从 SLO 逆推 GPU 数量的容量规划函数4.1 显存约束下的单卡最大并发容量规划的第一步是算清单卡能扛多少并发。这取决于模型权重占用的显存和每个请求的 KV Cache 大小。以 70B 模型、BF16 精度、TP8 为例模型权重显存 70e9 × 2 bytes / 1024^3 ≈ 140 GB单卡分摊 140 / 8 17.5 GBH100 80GB 扣除 CUDA Context 和运行时开销可用约 72 GB。剩余给 KV Cache 的空间约 54.5 GB。如果每个请求 8K 序列的 KV Cache 约 2.6 GB那么单卡最大并发约 20。这个数字是显存硬约束超过就会 OOM。但它不代表你能跑满 20 并发还能守住 TTFT SLO因为并发一高排队延迟就会上升。4.2 用 Littles Law 算平均并发需求平均并发 目标 QPS × 平均驻留时间平均驻留时间 TTFT 中位数 平均输出 Token 数 × TPOT 中位数假设 TTFT 中位数 0.3s输出 256 TokenTPOT 0.025s驻留时间约 6.7s。目标 QPS 20平均并发就是 134。如果单卡最大并发 20134 / 20 ≈ 7 张卡。但这是平均值要守住 P99 TTFT还得留余量。工程上取 0.7 的安全系数即实际可用并发按单卡 14 算134 / 14 ≈ 10 张卡。再考虑 TP8 的约束最终取 16 张卡比较稳妥。4.3 可复用的容量规划函数把上面的逻辑写成一个函数输入模型参数和 SLO 目标输出建议 GPU 数# capacity.py def capacity_planning( model_params: int, dtype_bytes: int, kv_cache_per_req: float, target_qps: int, ttft_median: float, tpot_median: float, avg_output_tokens: int, tp_size: int 8, gpu_mem: int 80, safety_factor: float 0.7, ) - dict: weight_memory model_params * dtype_bytes / (1024**3) usable_mem gpu_mem * 0.90 mem_after_weight usable_mem - (weight_memory / tp_size) max_seqs_per_gpu int(mem_after_weight / kv_cache_per_req) avg_residence ttft_median avg_output_tokens * tpot_median avg_concurrency target_qps * avg_residence effective_seqs max_seqs_per_gpu * safety_factor min_gpus int(avg_concurrency / effective_seqs) 1 return { weight_memory_gb: round(weight_memory, 1), max_seqs_per_gpu: max_seqs_per_gpu, avg_concurrency: round(avg_concurrency, 1), min_gpu_count: max(min_gpus, tp_size), recommended_gpu_count: max(min_gpus, tp_size) 1, } # 示例 result capacity_planning( model_params70_000_000_000, dtype_bytes2, kv_cache_per_req2.6, target_qps20, ttft_median0.3, tpot_median0.025, avg_output_tokens256, ) print(result)输出大致是weight_memory_gb 140.0max_seqs_per_gpu 20avg_concurrency 134.0min_gpu_count 10recommended_gpu_count 11。再结合 TP8实际取 16 卡。这个函数的价值在于当你调整 SLO 目标比如把 TTFT 中位数从 0.3 改成 0.5它能立刻告诉你 GPU 数量会怎么变。容量规划从“拍脑袋”变成了“改参数看结果”。5. 常见报错与排查401、local proxy failed、reading choices、OAuth5.1 401 UnauthorizedKey 没传对这是最常见的报错。如果你看到{error: {message: Invalid API key, code: 401}}先检查三件事Key 是否复制完整有没有多余空格、环境变量是否生效、请求头格式是否正确。TaoToken 的鉴权头是Authorization: Bearer sk-xxx注意 Bearer 后面有一个空格。如果你用的是 Cline 或 Claude Code检查 settings 里的TAOTOKEN_API_KEY是否和 API Keys 页面生成的一致。Key 泄露后可以在控制台重新生成旧 Key 会立即失效。5.2 local proxy failed本地代理配置冲突这个报错通常出现在你本地开了代理工具但代理规则没有放行taotoken.net。表现是连接超时或 SSL 握手失败Error: local proxy failed: connection refused排查方法是先确认你的网络环境能直接访问 API 地址。如果你在公司内网可能需要配置 NO_PROXY 环境变量export NO_PROXYtaotoken.net注意这里说的是本地网络配置不涉及任何跨境网络工具。如果你在容器里跑压测脚本检查容器的 DNS 和出网策略是否放行了 443 端口。5.3 reading choices响应格式不匹配这个报错一般出现在你用非流式请求但代码按流式解析或者反过来KeyError: choices检查你的请求体里stream字段是否和解析逻辑一致。流式请求返回的是 SSE 格式每行以data:开头最后以data: [DONE]结束。非流式请求返回的是完整 JSONchoices在顶层。如果你用的是 OpenAI SDK确认base_url设置正确不要手动拼接/v1/chat/completionsSDK 会自动处理路径。5.4 OAuth 相关报错认证方式不匹配如果你在 Claude Code 或类似工具里看到 OAuth 报错通常是因为工具默认走 OAuth 流程但你配置的是 API Key 模式。解决方法是在工具的认证设置里选择“API Key”而不是“OAuth”然后填入 TaoToken 的 Key。以 Claude Code 为例在 settings 里把认证方式改为 API KeyBase URL 填https://taotoken.net/apiModel ID 填你需要的模型。三件套配齐后OAuth 报错就会消失。5.5 压测时 TTFT 突然飙升如果你在压测中发现 TTFT 从 0.3s 突然跳到 3s先别急着加卡。检查两个地方一是 GPU 温度是否过高导致降频H100 在高负载下 SM 频率可能从 1980 MHz 降到 1830 MHz直接影响 TPOT 和 TTFT二是队列延迟是否已经成为瓶颈用 vLLM 的 metrics 看queue_time指标如果它占了 TTFT 的 80% 以上说明瓶颈在排队而不是计算。这时候加卡确实有效但如果是单请求 Forward Pass 太慢加卡反而可能因为 All-Reduce 通信开销让延迟更差。判断方法很简单如果单请求在低并发下 TTFT 就已经超标那是模型或 Kernel 的问题不是容量问题。6. 用实测数据校准容量水位并持续迭代容量规划不是一次性的计算而是一个持续校准的过程。你从公式里算出的 GPU 数量只是起点真实水位要靠压测数据来修正。具体做法是先用容量规划函数算出建议卡数然后在这个卡数下跑阶梯压测。从目标 QPS 的 50% 开始每 2 分钟增加 20%观察 TTFT P99 和 TPOT P95 的变化。当 TTFT P99 超过 SLO 阈值的 80% 时记录当前的 QPS这就是你的安全水位。如果安全水位低于目标 QPS说明需要加卡如果高于目标 QPS 很多说明可以适当缩容。我踩过的一个坑是在冷启动状态下跑压测GPU 温度还没上来数据很好看。但持续跑 30 分钟后温度稳定在高位TPOT 上升了约 8%TTFT P99 也跟着涨。所以压测至少要跑 30 分钟以上让 GPU 进入热稳态数据才有参考价值。另一个经验是不同模型的容量水位差异很大。7B 模型可能单卡就能扛 50 QPS70B 模型在 TP8 下可能 20 QPS 就到顶。用 TaoToken 的统一通道做多模型基准验证可以快速摸清每个模型的真实水位避免用同一个容量假设套所有模型。最后把 SLO 监控和错误预算追踪接进你的告警系统。当月度错误预算消耗超过 50% 时触发预警超过 80% 时冻结非必要变更。这样 SLO 就不只是一张表而是真正驱动容量决策和发布节奏的工程工具。