
1. 推理 GPU 为什么“显存满了、算力却闲着”如果你正在做推理服务大概率见过这个画面nvidia-smi里显存已经吃到 80% 以上请求队列也一直有积压但 GPU 的 SM 利用率在 30% 到 50% 之间来回晃。加机器吧成本线性上涨单卡吞吐却没怎么动换模型吧效果和成本又要重新评估。这时候真正该怀疑的往往不是硬件而是批处理的调度粒度。大模型推理分两段prefill 把整段 prompt 并行算完并建立 KV cache计算密度高decode 每次只生成一个 token要串行推进很多步更吃显存带宽和调度效率。静态批处理的做法是“凑齐 N 个请求一起 prefill一起 decode直到最慢的那个生成完才释放整批”。问题就出在这里同一批里有的请求 20 个 token 就结束有的要 800 个短请求完成后只能干等GPU 的计算单元就在这种“陪跑”里空转。显存被 KV cache 和预留空间占住算力却没被有效利用这就是“显存满、算力闲”的根源。连续批处理continuous batching也叫 iteration-level scheduling 或 in-flight batching要解决的就是这一层浪费。它不再以整批请求为调度单位而是每完成一次前向迭代就重新调度某个请求生成结束符后立即退出并释放槽位等待队列里的新请求在下一步补进来批成员可以随每次迭代变化。Orca 论文在 GPT-3 175B 实验里相对当时的 FasterTransformer在相同延迟水平下报告了最高 36.9 倍吞吐提升vLLM 把调度器和 PagedAttention 结合后报告常见模型在相同延迟下吞吐提升 2 到 4 倍。这些数字不能直接搬到你的模型和卡上但它说明迭代级调度能消掉大量结构性空转。这篇就围绕“连续批处理如何把推理 GPU 的空转时间填满”这件事用 TaoToken 统一 API 通道做接入背景把请求排队、动态合批、显存占用三者的关系拆开给出可复制的调度参数配置和压测验证动作。适合已经在跑推理服务、想在不加卡的前提下把单卡吞吐榨出来的同学。下面先从接入通道讲起因为调度参数调得再细请求入口不稳定压测数据也没法信。2. TaoToken 统一 API 通道把请求入口先固定下来调连续批处理参数最怕变量太多。你这边改max_batch_size那边请求来源换了、鉴权方式变了、超时策略动了压测结果就没法归因。所以我的习惯是先把请求入口固定成一条统一通道再动调度参数。TaoToken 在这里扮演的角色就是统一 Key / API 通道不管你后端挂的是哪种推理服务前端请求都从同一个 Base URL 和同一把 Key 出去压测流量分布才可控。接入本身不复杂核心就三件套Base URL、API Key、Model ID。Base URL 用https://taotoken.net/apiKey 在控制台的 API Keys 页面生成Model ID 按你实际要压的模型填。这三样凑齐任何兼容 OpenAI 协议的客户端都能直接指过来。我试过用同一把 Key 分别压两个不同后端切换只改 Model ID前端代码一行不动对比起来干净很多。如果你用的是 Claude Code 这类编码工具或者 Cline、Codex 这类带 MCP 的客户端配置逻辑是一样的只是文件位置不同。Claude Code 走的是 Anthropic 兼容入口Cline 的 MCP 配置写在 settings 里Codex 则读auth.json。不管哪种只要出现自定义 Base URL 的地方就填https://taotoken.net/apiKey 填控制台生成的那串Model ID 填你要用的模型名。三件套缺一个最常见的结果就是 401 或者 model not found。这里有个容易踩的坑有人只改了 Base URL忘了 Key 还是旧的结果请求一直 401却以为是调度参数的问题白白调了半天。所以压测前先做一次最小连通性验证确认通道通了再进调度调优。验证命令很简单用 curl 打一次 chat completions 就行curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: your-model-id, messages: [{role: user, content: ping}], max_tokens: 8 }返回里能看到choices数组和正常的usage说明通道通了。如果返回 401先查 Key返回 model 相关错误先查 Model ID 拼写。这一步过了再谈调度。通道固定之后压测流量的并发数、prompt 长度分布、输出长度分布都由你自己控制后面调max_batch_size、max_num_tokens、显存水位才有意义。想先直观感受一下模型响应也可以直接在模型对话页面发几条请求确认通道和模型都对得上再进压测环节。3. 可复制的调度参数配置批大小、等待窗口、显存水位连续批处理的调度循环抛开框架封装本质就是每步执行一次的函数先回收已完成请求、释放 KV 块再在显存和 token 预算允许时补入新请求最后拼成一次前向计算各请求推进一步。第 2 步必须同时受显存预算和 token 预算约束——前者防 KV cache 被塞爆导致 OOM后者限制单步 token 总量避免长 prefill 把单步耗时拉高。以 TensorRT-LLM 为例当前 API 提供max_batch_size、max_num_tokens、enable_chunked_prefill等参数。下面这份配置是结构示意数值必须通过你自己的模型、硬件和流量分布压测后确定别直接抄# TensorRT-LLM 调度参数示意数值需压测确定 max_batch_size: 64 # 单批最大请求数越高并发越强KV 水位和 OOM 风险也越高 max_num_tokens: 8192 # 单次迭代 token 预算调大吞吐可能升但单步耗时和 TBT 可能变差 enable_chunked_prefill: true # 长 prompt 切块分散到多次迭代缓解 decode 被 prefill 拖住 kv_cache_free_gpu_memory_fraction: 0.85 # 显存水位留出余量防 OOM如果你用的是 vLLM对应的参数名不一样但语义相通。vLLM 里常用的是--max-num-seqs对应批上限、--max-num-batched-tokens对应 token 预算、--gpu-memory-utilization对应显存水位。写成启动参数大概是这样python -m vllm.entrypoints.openai.api_server \ --model your-model-path \ --max-num-seqs 64 \ --max-num-batched-tokens 8192 \ --gpu-memory-utilization 0.85 \ --enable-chunked-prefill三个旋钮互相牵制调的时候要一起看。token 预算调大单步能塞更多 prefill吞吐可能上去但单步耗时和 TBT 会变差token 预算调小decode 更平稳长 prompt 的 TTFT 却会上升。批上限越高并发能力越强但 KV cache 水位和 OOM 风险同步升高。上下文越长单请求占的 KV cache 越多能同时容纳的请求就越少。所以显存水位不能拉满留 10% 到 15% 余量给峰值和碎片比事后救 OOM 划算得多。等待窗口这块很多框架没有直接叫这个名字的参数它体现在调度器“什么时候把新请求补进来”的策略里。激进一点每步都尝试补入空槽复用最快但 prefill 和 decode 混在一起TBT 抖动可能变大保守一点攒一小段再补TBT 平稳但空槽回收慢。我的做法是先用激进策略压一轮看 p99 TBT 能不能接受不能接受再往保守调。这套配置改完别急着上生产先跑压测验证。4. 压测验证对比开启前后 GPU 空转与吞吐变化压测的关键是固定流量分布。并发数、prompt 长度分布、输出长度分布这三样一变结果就没法对比。我一般用固定随机种子生成一批请求prompt 长度覆盖短中长三档输出长度也拉开差距这样才能复现“短请求陪跑长请求”的场景。压测脚本用 asyncio 并发打请求记录每个请求的 TTFT、TBT 和总耗时同时用nvidia-smi或 DCGM 采样 GPU 利用率。先跑静态批处理基线关掉连续批处理或者把调度粒度设成整批。记录吞吐tokens/s、TTFT、p99 TBT、KV cache 水位、OOM 次数。然后开连续批处理同样的流量再跑一遍。对比时重点看两个指标GPU SM 利用率是否从 30% 到 50% 抬到 70% 以上以及相同延迟水平下吞吐涨了多少。如果显存水位没变但吞吐上去了说明空转被填上了如果吞吐没动、延迟还涨了说明参数没调对或者瓶颈根本不在调度。压测时建议把请求入口统一走 TaoToken 通道这样并发控制和超时策略都在你手里不会因为上游网关的限流把数据搅乱。采样 GPU 利用率可以用下面这条命令每 0.5 秒采一次压测结束后看均值nvidia-smi --query-gpuutilization.gpu,memory.used --formatcsv -l 0.5实测下来静态批处理在长短请求混合的场景里SM 利用率经常在 40% 上下短请求一结束就出现空槽长请求还在慢慢 decode。切到连续批处理后短请求退出、新请求补入空槽被及时复用SM 利用率能稳定在 70% 以上。吞吐提升幅度取决于你的长度分布差异差异越大收益越明显。但要注意如果所有请求长度都差不多连续批处理的收益会小很多因为本来就没有多少陪跑浪费。验证通过的标准不是“吞吐涨了”就完事而是吞吐、TTFT、p99 TBT、KV 水位、OOM 次数一起过线。只看总吞吐很容易把 TBT 抖动藏起来。压测报告里把这五个指标列成表开启前后各一行对比一目了然。只有这些指标一起达标才算真正把空转填满而不是把延迟问题从一个队列挪到另一个队列。5. 常见报错排查401、local proxy failed、reading choices、OAuth调参过程中遇到的报错一半跟调度无关是接入层的问题。先把这几类排掉再怀疑参数。401 Unauthorized 最常见。原因基本是 Key 没填、填错、或者环境变量没生效。检查TAOTOKEN_API_KEY是否真的导出到了当前 shellecho $TAOTOKEN_API_KEY看一眼。如果用的是 Claude Code 或 Cline检查配置文件里的 Key 字段有没有被引号包错。三件套里 Base URL、Key、Model ID 任何一个不对都可能报 401 或 model not found。local proxy failed这类报错通常是客户端配置了本地转发但目标地址写错或者网络出口不通。检查 Base URL 是不是https://taotoken.net/api别多写或少写路径。如果是 MCP 客户端检查 MCP server 的启动命令和参数确认它指向的地址和 Key 一致。reading choices报错一般是响应体解析失败。可能是返回了非 JSON 内容比如网关的错误页也可能是流式响应被中途截断。先用 curl 打一次非流式请求确认返回结构正常再开流式。如果 curl 正常、客户端报错那就是客户端解析逻辑的问题检查它期望的字段名和实际返回是否一致。OAuth 相关报错多出现在 Claude Code 这类走 Anthropic 兼容入口的工具上。确认你用的是 API Key 模式而不是 OAuth 登录模式两者鉴权头不一样。Codex 的auth.json里如果混了旧的 OAuth token也会报鉴权失败清掉重新填 Key 即可。Cline 的 MCP 配置里Base URL、Key、Model ID 三件套要写全缺一个就可能在初始化阶段报错。排完这些如果压测还是 OOM再回头看显存水位是不是拉太高、max_batch_size是不是超过 KV cache 能容纳的请求数。把水位降到 0.8 再试通常能稳住。如果 TBT 抖动大把max_num_tokens调小或者开 chunked prefill让长 prompt 分块处理。这些调整都要重新压测别凭感觉改。6. 把调度权交给服务端之后SLA 要先写清楚连续批处理提高吞吐不是让单次矩阵计算变快而是把自回归解码里的等待和空槽重新利用起来调度粒度从整批降到单步短请求即时退出新请求及时补位再由 selective batching 和分块 KV cache 管理托底。论文里的倍数只对对应的模型、硬件、并发和长度分布负责你的验收标准得自己压出来。真正要调的是一组互相牵制的旋钮没有一组放之四海皆准的最优值。业务到底优先吞吐、TTFT 还是 p99 TBT必须先写进 SLA再用稳定流量分布的压测确定参数。上线验收至少同时记录吞吐、TTFT、p99 TBT、KV cache 水位和 OOM 次数固定压测流量分布五项一起过线才算数。如果你还在选接入方式建议先把请求入口统一到 TaoToken 通道Base URL 用https://taotoken.net/apiKey 在控制台生成Model ID 按实际模型填。通道固定了调度参数才有可比性。想先验证模型响应可以去模型对话页面发几条请求要长期跑编码或 Agent 任务可以看 Coding Plan接入和排障的细节都在接入文档里。把入口和参数都固定下来再压一轮你就能看到 GPU 空转时间被填满的那条曲线。