大语言模型推理部署优化:延迟吞吐、显存与量化调优实战
大语言模型的优化与加速推理部署说到底就是把一个“能在单卡上吭哧吭哧跑通”的权重文件改造成一套对外扛得住并发、延迟可预测、单位成本算得过来的线上服务。我第一次被拉去干这件事的时候手上只有一个跑得通的推理脚本和一句“下周要压测”结果模型能出字但一上并发就排队p99 延迟从 800 毫秒飙到 12 秒显存还时不时被打爆。后来踩了一圈坑才明白这件事不是“换个更快的框架”能解决的它是一套从指标定义、显存核算、量化取舍、调度机制到部署拓扑的完整工程。这篇内容写给两类人一类是刚要把模型推上线、对着一堆参数不知道从哪下手的同学另一类是线上已经跑起来了但吞吐上不去、延迟抖动压不住、单卡成本降不下来的同行。我会把每个环节“为什么这么选”讲透把能直接抄的参数和脚本给出来也会把那些文档里不会写、只有被报警电话叫醒过才会记住的经验一起放进去。1. 先定目标延迟、吞吐、成本这三笔账怎么算1.1 延迟和吞吐天生是冤家很多人一上来就问“你这个框架能跑多少 tokens/s”这个问题其实没有意义因为脱离批大小和序列长度谈吞吐都是耍流氓。推理服务里真正要盯的是四个量TTFT也就是首 token 延迟决定用户按下回车之后等多久看到第一个字TPOT也就是每输出一个 token 的平均耗时决定字是一个一个蹦出来还是成段刷出来端到端时延等于 TTFT 加上输出长度乘 TPOT以及系统吞吐单位时间内所有请求加起来产出的 token 数。前三个是用户体验最后一个是钱。这四个量之间的关系本质上是被硬件特性决定的。预填充阶段是算力密集的输入 2000 个 token计算量大概是 2 乘参数量乘 token 数对 70 亿参数的模型来说差不多 28 TFLOPs短期靠算力堆解码阶段是访存密集的每生成一个 token 都要把全部权重从显存里读一遍7B 模型 FP16 权重 14GB显存带宽按 2TB/s 算理论下限就是 7 毫秒一个 token你算力再翻倍也没用。这就解释了一个很反直觉的现象单条请求的 TPOT 几乎不随算力提升而下降只有把批大小拉起来让一次权重读取服务多条请求单位 token 的成本才会降下去。所以吞吐和延迟的矛盾不是调度器写得不好而是物理规律你要做的就是在这个矛盾里找一个符合业务的位置。我一般的做法是先跟业务方把 SLO 敲死比如对话场景 TTFT p99 不超过 1.5 秒、TPOT p99 不超过 80 毫秒批量摘要场景只要求整体任务在 2 小时内跑完延迟完全不管。这两类需求的优化路线几乎是相反的前者要保延迟、宁可牺牲批大小后者要榨干每一分显存、把批尽量撑大。定不下来 SLO 就别急着调参方向错了后面全是白干。1.2 把显存账本摊开算一遍显存是这类服务里最硬的约束几乎所有“跑不起来”“跑着跑着崩了”的问题根子都在账没算清。显存占用可以拆成四块模型权重、KV Cache、前向激活和临时缓冲区、框架自身开销。权重最好算参数量乘每参数字节数FP16 是 2 字节INT8 是 1 字节INT4 是 0.5 字节7B 模型分别是 14GB、7GB、3.5GB70B 模型分别是 140GB、70GB、35GB。这里要注意 INT4 实际占用会略高因为分组量化要额外存缩放因子和零点一般比理论值多 5% 到 10%。真正容易被低估的是 KV Cache。它的公式是2键和值乘层数乘 KV 头数乘每头维度乘序列长度乘并发序列数乘每元素字节数。以 32 层、隐藏维度 4096、32 个注意力头的 7B 模型为例MHA 结构下每个 token 的 KV 占用是 2×32×4096×2 字节正好 0.5MB一条 4096 token 的会话就是 2GB。也就是说哪怕权重只占 3.5GB只要你想同时服务 20 条长会话显存立刻见底。换成 GQA 结构8 个 KV 头、每头 128 维每 token 降到 128KB同样 4096 token 只要 512MB差了四倍。这就是为什么现在的新模型几乎都上 GQA省的不是权重是缓存。模型规模精度权重显存每 token KVMHA每 token KVGQA-87B / 32 层FP1614 GB0.5 MB128 KB7B / 32 层INT87 GB0.5 MB128 KB7B / 32 层INT4约 3.8 GB0.5 MB128 KB70B / 80 层FP16140 GB2.5 MB320 KB70B / 80 层INT4约 37 GB2.5 MB320 KB拿 70B INT4 举个完整的例子单张 80GB 加速卡权重占 37GB框架和激活留 8GB剩下 35GB 给 KV Cache。每 token 320KB总共能放约 11 万个 token。如果平均每会话 4096 token理论并发上限就是 26 条左右。这个数字决定了你的单卡吞吐天花板也决定了要不要上多卡并行。我见过太多人先调参数后算账结果调了半天发现显存根本不够撑起目标并发纯属浪费时间。提示算这笔账的时候一定要按“输入长度加输出长度”的最大值算不能按平均值。线上一定是长短请求混合到达的平均值好看没用峰值一到就被打爆。1.3 场景不同路线完全不同同一个模型放在在线对话和放在离线批处理里优化手段几乎是两套东西。在线对话的特点是请求短、到达随机、对首字延迟敏感优化重点是调度器的抢占策略、分块预填充、前缀缓存命中率批大小通常压在 8 到 32 之间不敢往上冲。离线批处理的特点是一次性丢过来几十万条、可以等、对单条延迟完全不敏感优化重点是尽可能打满显存和算力批大小往 128 甚至 256 冲甚至可以关掉流式输出、关掉部分安全兜底逻辑来省开销。还有一种场景容易被忽略就是长文档问答和代码补全。前者输入动辄几万 token预填充一次要几百毫秒甚至几秒这时候前缀缓存和分块预填充的收益极大后者输出短、并发高、请求极碎真正的瓶颈在调度开销和 kernel 启动开销上图捕获和算子融合带来的收益比量化还明显。所以我在动手之前一定会先抽样看线上真实的请求分布输入长度分布、输出长度分布、并发曲线、缓存命中潜力。这份分布图比任何调参指南都有用。2. 模型侧的瘦身量化、蒸馏与结构裁剪的取舍2.1 权重量化的三档精度与真实收益量化是投入产出比最高的一刀几乎不需要改代码换个权重文件就能把显存砍掉一半以上。但量化不是无脑选最低位宽得看你卡在哪。常见的三档是W8A8权重和激活都量化到 8 位W4A16只量化权重到 4 位计算时反量化回 16 位还有 W4A8 这类更激进的组合。W8A8 的收益是把权重显存砍半、同时因为用了整数矩阵乘算力利用率能提一截精度损失通常在千分位级别绝大多数场景可以放心用。它对激活的异常值比较敏感需要做通道级的缩放均衡把难量化的通道压力摊到好量化的通道上否则个别层的输出会明显劣化。W4A16 的收益是把权重压到四分之一7B 模型从 14GB 降到 3.8GB 左右代价是计算前要反量化理论上会多一点开销但实测下来因为访存量大降解码阶段的 TPOT 反而经常是下降的。这条路线最适合显存吃紧、又想保持精度的场景。W4A8 更激进权重和激活都压理论吞吐最高但对校准和 Kernel 支持要求高我一般只在对延迟极度敏感、且已经做完充分评测的场景才上。选择的时候有个简单的判断法如果瓶颈是“显存装不下”优先 W4A16因为收益直接且精度可控如果瓶颈是“算力打不满、吞吐上不去”优先 W8A8因为整数算力才是吞吐的来源如果两个都卡才考虑更激进的组合。另外要提醒一句量化对预填充和解码的收益是不对称的预填充吃算力、解码吃带宽同一份量化权重对两者的提升幅度可能差一倍压测的时候一定要分开测别拿一个平均值当结论。2.2 校准集怎么选精度怎么验量化一定会掉精度问题是怎么把损失控制在业务可接受的范围内而不是自欺欺人地说“几乎无损”。校准集的选取是第一道关。我的经验是校准数据必须贴近真实线上分布不能随便拿几百条通用语料糊弄。如果业务是法律合同问答校准集里就该有合同文本如果是代码补全就该有真实代码片段。校准样本量一般 128 到 512 条就够再多边际收益很低但一定要覆盖长文本因为长序列下的激活分布和短文本差别很大。验证环节我一般分三层。第一层是困惑度在留出集上对比量化前后W8A8 一般上升不到 1%W4A16 上升 2% 到 5% 属于正常超过 8% 就要怀疑校准集或者分组大小有问题。第二层是下游任务指标拿业务里真实的评测集跑一遍看准确率、抽取的字段完整率、格式合规率这些硬指标有没有掉这一步最能反映真实影响。第三层是人工抽检随机抽一两百条真实请求让熟悉业务的人盲评看有没有出现重复、跑题、指令遵循变差这类软性退化。三层都过才算量化验收通过。注意困惑度不掉不代表业务不掉。我遇到过量化后困惑度几乎没变但模型对 JSON 格式的遵循率从 98% 掉到 82% 的情况原因是个别层对结构化标记的敏感度被量化误差放大了。所以格式类、工具调用类的业务一定要单独构造评测集。2.3 KV Cache 量化和稀疏化省钱但危险的地方权重之外KV Cache 是第二块大头尤其是长上下文场景。KV Cache 量化到 FP8 在实践中已经比较成熟收益是把缓存占用直接砍半等效于并发数翻倍精度损失通常很小因为缓存本身的数值分布比激活温和。再往下的 INT8 就需要更细的粒度按 token、按头分别算缩放因子否则长序列尾部的误差会累积得很厉害。我一般的原则是上下文长度在 8K 以内KV 量化收益有限、风险不小可以先不做上下文超过 32KKV 缓存已经比权重还大这时候不做量化基本没法上生产。稀疏化这条路要谨慎得多。结构化的头剪枝、层剪枝确实能转化成真实的速度提升因为剪掉的部分是真的不计算了非结构化稀疏在绝大多数通用硬件上只是省了存储计算还是按稠密走跑起来该多慢还多慢属于看着很美但不落地。至于“只保留最近的若干 token 的 KV”这种滑窗策略效果取决于业务摘要类任务里丢掉早期内容问题不大多轮对话里丢掉开头的系统提示和关键约束会直接翻车。这类激进手段我建议只在明确能接受信息损失的场景试并且一定要有 A/B 对比数据兜底。3. 推理引擎侧的加速机制怎么用3.1 连续批处理吞吐提升最猛的一刀传统批处理是静态的一个 batch 里所有请求必须等最慢的那条跑完才能释放一条生成长回答的请求能把整个 batch 卡住短请求的槽位全被浪费。连续批处理把这个粒度打散到每一轮解码每一步都重新组批谁生成完了谁立刻退出、空出的槽位马上让排队的请求补进来。这个改动听起来简单实际对吞吐的影响是数倍的尤其是在请求长度差异很大的真实流量里。我在一个混合长度场景里实测过静态批处理下 GPU 利用率长期在 30% 上下晃换成连续批处理后稳定在 70% 以上同样的硬件吞吐翻了差不多 2.4 倍而且 p99 延迟还降了因为短请求不再被长请求拖住。这个机制现在是标配如果你在用的推理服务没有它那第一个该换的就是它优先级远高于量化。要注意的是连续批处理会带来一个副作用批内请求数在动态变化显存占用也就跟着忽高忽低如果对显存余量估计不足很容易在高峰期触发抢占导致请求被换出重算延迟反而更糟。所以并发上限一定要留 15% 到 20% 的余量。3.2 PagedAttention 与显存碎片治理KV Cache 的另一个问题是内存管理。早期做法是给每条请求预分配一块连续的、按最大长度算的显存实际用了 500 token、预分配了 4096 token剩下 87% 全浪费了。业界常见的统计是这种预分配方式下缓存利用率只有两三成剩下全是碎片。分页机制借的是操作系统虚拟内存的思路把 KV Cache 切成固定大小的块常见块大小是 16 个 token逻辑上连续的块通过一张块表映射到物理上不连续的显存按需分配、用完即还。这一刀的价值不只是省显存更关键的是让“并发数”这个数字变得可预测。预分配模式下并发数是一个拍脑袋的保守值分页之后可以按“总 token 数除以平均序列长度”来精确估算缓存浪费率能压到个位数百分比等效并发往往能提升 2 到 4 倍。块大小这个参数值得调一调块取小一点内部碎片少但块表变大、访问开销略增块取大一点反过来。16 是个比较均衡的默认值超长上下文场景可以试 32短请求密集的场景可以试 8。提示开了分页之后显存不足时的处理策略要从“直接拒绝”改成“抢占加重算或换出到主机内存”。重算的代价是浪费算力换出的代价是占用带宽长请求密集时换出更划算短请求密集时重算更划算。3.3 分块预填充与前缀缓存治 p99 抖动的两副药线上延迟抖动最常见的原因是一条几万 token 的长请求突然进来预填充要霸占算力好几百毫秒这段时间里所有正在解码的请求全被卡住p99 直接爆炸。分块预填充就是治这个的把长请求的预填充切成若干小块比如每块 512 或 1024 个 token每算完一块就插回去和正在解码的请求混着跑解码请求的步进不会被打断代价是长请求自己的首字延迟略微变长。这是一个用少量首字延迟换整体稳定性的交易非常划算。块大小选择上取太小会让预填充的整体效率下降取太大又起不到平滑作用我一般从 1024 起调观察 TTFT 和 TPOT 的 p99 曲线找一个拐点。前缀缓存治的是另一类浪费很多请求共享同一段开头比如一大段系统提示、固定的工具定义、同一篇被反复提问的文档。这些内容每次都要重新做一遍预填充纯属浪费。前缀缓存把已经算过的 KV 按块存起来下次遇到相同前缀直接复用命中率高的时候首字延迟能降一个数量级。判断要不要开很简单看两个数前缀长度占输入长度的比例以及相同前缀的请求占比。如果系统提示有 1500 token而用户提问平均只有 200 token那命中率接近 100%收益极其明显如果每条请求内容都不同那开了也没用还多占显存。我见过一些团队系统提示写了两三千字的角色设定这种情况下前缀缓存基本是必选项。3.4 投机解码与算子融合用算力换延迟投机解码的思路是拿一个小模型先猜后面几个 token再让大模型一次性验证这几个位置猜对了就一次吐出好几个。理论上限很诱人但实际收益完全取决于接受率。假设草稿长度是 4接受率 0.7期望能接受的 token 数大概是 2.8 个如果草稿本身的成本是大模型的 15%整体加速比大约 1.7 倍但如果接受率掉到 0.4期望接受 token 数只有 1.6 个加速比连 1.2 都不到还要多占一份草稿模型显存纯属亏本。所以这条路只适合高可预测的文本比如代码补全、格式化输出、模板化改写开放式创作里接受率普遍偏低不建议上。还有一种不占额外模型的变体直接从当前请求的历史文本里匹配草稿也就是检索式的草稿生成在摘要、改错、复制类任务上接受率很高成本几乎为零性价比比独立草稿模型更好。至于算子融合和图捕获这是延迟优化的“细活”解码阶段每生成一个 token 要启动上百个 kernel单次启动开销三四微秒累计下来就是好几毫秒占 TPOT 的比例不小。把归一化、投影、门控这些相邻算子融合成一个再用图捕获把整个解码循环固化成一次提交能把大量启动开销消掉。这一块的收益在模型小、批大小小、延迟要求严的场景里特别明显我见过融合加图捕获把 TPOT 从 45 毫秒压到 28 毫秒的案例。4. 从单卡到多卡并行与部署拓扑4.1 张量并行、流水并行的选择与代价单卡装不下就必须切。张量并行是把每一层的矩阵按列或按行切开分到多张卡上算每层算完要做一次全规约把结果拼回来。好处是单层延迟随卡数下降坏处是通信极其频繁每层两次集合通信通信量跟批大小和隐藏维度成正比。所以张量并行的度数强烈依赖卡间互联带宽同一台机器内部的高带宽互联做到 8 路张量并行还算舒适跨机做张量并行带宽掉一到两个数量级延迟会不降反升我基本不考虑。实践中张量并行度不超过单机卡数这是条硬线。流水并行是另一个思路把不同层整段分到不同卡上一张卡算完把激活传给下一张。它通信量小、对带宽要求低适合跨机切分大模型但代价是“气泡”前面的卡在算的时候后面的卡在等除非把批切成足够多的微批塞满流水线。经验上微批数量要至少是流水线度数的 4 倍以上气泡才能压到 10% 以内而这又会让显存占用上升。所以常见组合是机器内用张量并行机器间用流水并行规模再大的时候再叠加专家并行处理稀疏结构。切分方案不是越复杂越好能两级解决就别上三级多一级就多一堆通信和调试成本。并行方式切分维度通信频率对带宽要求适用位置张量并行层内矩阵每层两次极高单机内流水并行层间每微批两次中等跨机专家并行稀疏专家按路由高超大稀疏模型数据并行副本无无提高并发容量4.2 服务拓扑、灰度与弹性单机调好之后真正的麻烦在集群层。一个完整的推理服务至少要分四层接入网关做鉴权和限流路由层按模型版本和租户把请求分发到不同实例推理实例组按模型和精度分组最后是监控和日志。限流这一层特别容易被省掉然后就被上游的重试风暴打死。我的经验是每个实例都要有排队上限队列满了直接快速失败返回而不是无限排队——排队的请求最终都会超时白占显存还给用户更差的体验早拒绝反而更好。灰度发布是另一条命。模型换了量化版本、框架升了级、参数动了都必须走小流量验证。我一般的节奏是先起一个影子实例把 1% 的真实流量复制过去只做推理不出结果对比输出一致性和延迟没问题再切 5% 真实流量观察 30 分钟再逐步放量到全量。中间任何一步指标异常就立刻回滚。推理服务的回滚必须做到秒级因为模型加载本身就要几十秒到几分钟不可能靠重启回滚得保证旧版本实例一直热着切换只在路由层做。弹性伸缩这块要认清一个事实推理实例的冷启动非常慢加载权重、编译图、预热都算上大模型动辄几分钟。所以扩容策略不能等到负载上来了再扩必须按预测提前扩或者保留一批低优先级的常驻实例。缩容也要慢宁可多留十分钟也不要刚缩完又来一波流量。5. 一次完整的压测与调优实录5.1 压测脚本与指标口径压测最容易犯的错是拿固定长度的请求测出一个漂亮数字然后上线被真实流量打脸。真实流量的输入长度是长尾分布输出长度也不可控所以压测必须按真实分布来。我一般从线上采样一千条真实请求统计输入输出长度的分布压测时按这个分布随机采样并用泊松到达模拟真实并发节奏而不是让所有请求同时发出。下面这个脚本不依赖特定框架只要服务提供标准的对话接口就能跑重点是它分别记录了首 token 时间和后续每个 token 的时间。import asyncio, time, json, random, statistics import httpx URL http://127.0.0.1:8000/v1/chat/completions async def one_request(client, in_len, out_len, results): payload { messages: [{role: user, content: 测 * in_len}], max_tokens: out_len, stream: True, } t0 time.perf_counter() ttft, last, n None, t0, 0 async with client.stream(POST, URL, jsonpayload) as resp: async for line in resp.aiter_lines(): if not line.startswith(data:): continue body line[5:].strip() if body [DONE]: break now time.perf_counter() if ttft is None: ttft now - t0 n 1 last now total last - t0 tpot (total - ttft) / max(n - 1, 1) results.append({ttft: ttft, tpot: tpot, total: total, n: n}) def pct(xs, p): xs sorted(xs) return xs[min(int(len(xs) * p), len(xs) - 1)] async def main(concurrency32, duration120): results [] limits httpx.Limits(max_connectionsconcurrency * 2) async with httpx.AsyncClient(timeout300, limitslimits) as client: end time.perf_counter() duration tasks [] while time.perf_counter() end: in_len random.choice([256, 512, 1024, 2048, 4096]) out_len random.randint(64, 512) tasks.append(asyncio.create_task( one_request(client, in_len, out_len, results))) # 泊松到达间隔均值 1/concurrency 秒 await asyncio.sleep(random.expovariate(concurrency)) await asyncio.gather(*tasks, return_exceptionsTrue) ttfts [r[ttft] for r in results] tpots [r[tpot] for r in results] tokens sum(r[n] for r in results) print(json.dumps({ requests: len(results), throughput_tok_s: round(tokens / duration, 1), ttft_p50: round(pct(ttfts, 0.5), 3), ttft_p99: round(pct(ttfts, 0.99), 3), tpot_p50: round(pct(tpots, 0.5), 4), tpot_p99: round(pct(tpots, 0.99), 4), }, indent2)) asyncio.run(main())指标口径要统一不然横向对比没意义。TTFT 从发出请求算到收到第一个内容块TPOT 用总时长减去 TTFT 再除输出 token 数吞吐用总输出 token 数除以压测时长——注意只算输出 token输入 token 的处理速度单独用“预填充吞吐”衡量。压测时还要同步采集服务端的显存占用、运行中请求数、排队数、抢占次数这几项客户端看到的延迟只是结果服务端指标才是病因。5.2 参数调优的先后顺序调参最忌讳一把梭改了十几个参数然后不知道哪个起了作用。我的顺序是固定的从上到下逐层收敛。第一步定精度。先选量化方案跑通功能做精度验收这一步不改任何服务参数。第二步定显存上限比例。一般设 0.85 到 0.9留出余量给临时缓冲和碎片设到 0.95 以上短期看着能多塞几条请求实际很容易在某个峰值被系统直接杀掉非常不划算。第三步定最大序列长度。这个值设得越大调度器预留的显存就越多实际业务只要 8K就别设 32K白白浪费并发能力。第四步调单批最大序列数这是吞吐和延迟的核心旋钮从小到大加每加一次都跑一轮压测直到 p99 开始明显恶化然后退回上一档。第五步开分块预填充观察 TTFT 和 TPOT 的 p99 是否被抚平。第六步开前缀缓存前提是命中率够高。第七步再看要不要投机解码、图捕获这些锦上添花的。这个顺序背后的逻辑是前三步决定“能装多少”第四步决定“用多满”后面几步都是在这个基础上做体验优化。顺序颠倒的典型后果是先调了一堆高级特性结果发现显存根本不够、并发上限只有个位数前面的调优全部作废。5.3 一份可以直接抄的配置基线下面这份启动参数是我在中等规模在线对话场景下比较常用的一套基线模型的量化格式假定是 INT4 权重、FP16 激活单机 8 卡张量并行。参数名各家框架略有差异逻辑是通用的。# 单机 8 卡INT4 权重在线对话基线 MODEL/models/xxx-int4 python -m serve \ --model $MODEL \ --tensor-parallel-size 8 \ --max-model-len 8192 \ --gpu-memory-utilization 0.88 \ --max-num-seqs 64 \ --max-num-batched-tokens 8192 \ --enable-chunked-prefill \ --chunked-prefill-size 1024 \ --enable-prefix-caching \ --kv-cache-dtype fp8 \ --swap-space 16 \ --disable-log-requests几个参数值得单独说。max-num-batched-tokens控制的是一个调度步里最多处理多少个 token设小了调度频繁、吞吐上不去设大了单步耗时变长、延迟抖动明显一般按预填充块大小的 4 到 8 倍取。max-num-seqs是并发请求上限和 KV Cache 容量强相关设超了会频繁抢占重算我在 8K 上下文、FP8 缓存的配置下一般从 64 开始试。swap-space是主机内存换出空间单位 GB设太小了长请求直接被拒设太大了会掩盖显存规划问题16 到 32 比较合适。--disable-log-requests这条别小看请求日志在高并发下会带来明显的 CPU 开销和磁盘压力压测和生产环境都该关掉只在排查问题时临时打开。6. 常见问题排查速查表6.1 六类典型故障的定位路径线上出问题的时候最怕的是没有排查路径东看一眼西看一眼。我把遇到过的高频故障整理成了一张表基本覆盖了八成场景。现象最可能的原因快速验证方法处理动作启动即 OOM显存上限设太高、序列长度预留过大把显存比例降到 0.8 重试降比例、减最大长度跑几分钟后 OOM并发突增、KV 缓存撑爆看缓存使用率曲线是否触顶降并发上限、开分页吞吐上不去批大小太小、调度开销大看硬件利用率是否低于 50%加批、开连续批处理p99 延迟尖刺长请求预填充霸占算力看延迟尖刺是否与长输入同时出现开分块预填充输出重复、乱码量化过度、KV 量化误差累积换回 FP16 对比降低量化激进程度请求卡住不返回队列积压、实例假死看排队数是否持续增长加限流、健康检查重启排查顺序我一般是从外往内先看客户端错误率再看网关排队和超时再看实例的运行中请求数和排队数最后才看硬件指标。因为绝大多数“服务变慢”其实是排队问题不是计算问题。判断方法很简单如果硬件利用率不高但延迟很高那一定是调度或排队出了问题如果硬件利用率拉满了延迟还高那才是真的算力不够该加卡或者该上量化了。6.2 几条踩出来的经验第一条别信“默认参数能跑就先不管”。默认配置往往是保守的为了兼容各种场景牺牲了性能我在一个项目里只调了三个参数——并发上限、批处理 token 数、分块预填充大小——吞吐就翻了一倍多代码一行没改。第二条量化之后一定要重新压测不能沿用 FP16 的参数。四比特权重让显存大幅释放同样的并发上限下显存根本用不满等于白白浪费了一半容量同时反量化带来的额外计算可能让解码变慢最优批大小也会跟着变。换了权重就像换了台机器所有参数都要重来一遍。提示每次只改一个参数改完立刻压测记录数据。我用一张简单的表格记录每次实验的参数组合和四项指标一周之后回头看哪个参数影响最大一目了然。凭感觉调参是最容易原地打转的。第三条监控一定要盯“抢占次数”这个指标。它不像延迟、吞吐那么直观但它是很多隐性问题的信号。抢占次数持续增长意味着 KV 缓存不足、调度器在反复换出重算表现出来就是吞吐莫名其妙地低、算力被浪费在重算上。我遇到过吞吐只有理论值一半的情况查了半天最后发现就是这个指标在报警把并发上限从 96 降到 64 之后吞吐反而涨了 30%。第四条长上下文场景要单独设一条限制。哪怕模型支持 128K也不要对所有请求放开因为一条 100K 的请求能瞬间吃掉几十条普通请求的缓存。我的做法是按租户或者按接口分组普通对话接口限制 8K文档分析接口单独开一组实例限制到 32K两组资源物理隔离互不干扰。这样长请求再疯也影响不到主链路这是最省心的隔离方式。第五条别迷信“更快的框架”。换个推理框架能带来百分之二三十的提升已经算多了而搞清楚自己的瓶颈在哪、把批大小和缓存调对往往是两三倍的差距。我建议先把现有服务压到极限、把指标看明白再考虑换技术栈否则换了也未必知道新东西好在哪。最后分享一个我在实际部署里养成的习惯每次上线前我都会用同一份固定的压测脚本、同一份固定的请求样本跑一遍把结果存成一份基线。之后不管是升级框架、换量化版本还是调参数都拿新结果和基线对比而不是凭印象说“好像快了点”。这份基线在半年里帮我拦下过两次看起来“优化了”实际是回退的变更也让我对每个参数到底值多少钱有了具体的数字感。这套流程跑顺之后再去看那些关于推理加速的新技术判断值不值得试心里就有底了。