大语言模型能写诗、能写代码,可是一旦真实跑到线上去服务用户,推理优化与部署才是真正决定系统能不能撑住的那道关卡。很多团队花大力气训好了模型,结果一上线就暴露各种问题:单请求时延太高、并发上来直接OOM、一个长对话把显存吃光。这周我正好在整理一套LLM工程系统的推理优化与部署环节,把从指标拆解、算力优化、服务架构到上线排查的完整思路梳理了一遍。这篇文章适合正在或准备做LLM应用落地的算法工程师、后端开发同学,也适合想搞懂大模型服务成本与性能瓶颈的技术负责人。这里不聊训练,只聊推理,而且尽量说人话,把原理和步骤都摊开讲。1. 推理优化到底在解决什么问题1.1 从自回归生成看瓶颈来源大模型推理和传统深度学习推理最大的不同在于“自回归解码”。模型每次只能根据已经生成的 token 预测下一个 token,然后把新生成的 token 拼到输入序列尾巴上,再接着预测下一个。这个串行过程意味着一个长度为 1000 的回复,至少要经历 1000 次前向计算,每一步都依赖上一步的输出,无法像图像分类那样一次性算出全部结果。这个特性带来了两个直接后果:第一,单个请求的延迟天然偏大,尤其当上下文长度不断增大时,每一步需要处理的序列都在变长;第二,显存消耗不仅是模型权重本身,还包括不断累加的中间状态,也就是 KV Cache。如果你把模型类比成一个特别认真的作者,每写一个新字都要把前面所有内容重新读一遍,那就很容易理解为什么大模型推理会又慢又费内存。在实际服务中,模型权重当然要占显存,KV Cache 往往比权重更占地方。以常见的 70 亿参数模型为例,BF16 格式的权重大约 14GB,而每个请求的 KV Cache 会随着上下文长度和并发用户数成倍增长。如果同时服务几十个用户,每个用户都有一长段上下文,显存通常会在权重之上再占用几十甚至上百GB。这也是为什么很多团队一开始天真地用“模型权重大小”来估算显存需求,结果一上线就翻车。1.2 必须死磕的四个核心指标搞推理优化之前,先把评价指标定下来,否则很容易陷入“调了一堆参数,但不知道有没有变好”的尴尬。我通常把这四个指标作为项目对齐的基准:指标代表什么一般目标首token延迟(TTFT)用户发出请求到收到第一个字的时间通常希望小于 1 秒,流式聊天场景越短越好每token生成延迟(TPOT)后续每个字之间的间隔通常希望小于 50ms,否则有明显的“卡顿”感吞吐量单位时间内系统总共生成的 token 数尽可能高,直接决定服务成本显存占用量权重、KV Cache 和框架开销的总和决定单卡能支撑多少并发与多长上下文TTFT 反映的是“预填充”阶段的效率。预填充阶段要把用户输入的所有 prompt 一次性并行算完,计算密集,所以非常吃算力,不能简单通过增加并发来解决。TPOT 反映的是“解码”阶段的效率,是系统连续推理能力的体现。吞吐量则跟批处理策略、显存管理、量化程度强相关。实际优化时,这四个指标经常互相矛盾:无限并发可以提高吞吐,但会让每个请求互相争抢算力,导致 TPOT 变差;量化可以减少显存,但如果量化校准没做好,输出质量会明显下降。所以做推理优化的本质不是追求某一项指标登顶,而是在给定算力预算下,找到延迟和吞吐、成本和效果的平衡点。2. 模型与计算层面的优化手段2.1 量化:不只是降低精度这么简单量化的核心思路,是用更低精度的数值表示模型参数,从而减少显存占用和计算量。最常用的是把 FP16/BF16 降到 INT8 或 INT4。为什么神经网络可以接受这种压缩?因为模型参数存在大量冗余,而且很多权重的分布集中在某个较小的数值范围内,我们可以在尽量不损失信息的前提下用更少的比特去近似它。量化方式分两大类:训练后量化(PTQ)和量化感知训练(QAT)。推理部署场景里,PTQ 最常用,因为它不需要重新训练模型。PTQ 又分为只量化权重的 Weight-only Quantization,以及同时量化激活值和 KV Cache 的 W8A8 等方案。对 LLM 来说,权重量化是最直接省显存的方式:把 70 亿参数从 FP16 压成 INT4,权重可以从约 14GB 降到约 3.5GB,单卡能容纳的模型规模立刻变大。不过量化不是白嫖。如果只做简单的 round-to-nearest 量化,模型能力往往会明显退化;如今主流方案会结合激活值分布、通道离散度做优化,这类方法能显著降低量化误差。实操中我一般建议:如果 GPU 显存充足,先用 BF16/FP16 跑通全部业务场景,再尝试 INT8;只有当显存确实放不下更长上下文或更高并发时,才考虑 INT4。千万不能为了节省显存而盲目上低比特,最后模型输出内容质量崩塌,反而得不偿失。另外要注意,量化对注意力层的影响往往比其他层更大。某些架构中,K、V、Q 这些投影层的权重对精度更敏感,社区里也有针对特定敏感层做“跳过量化”的做法。所以上线前一定要准备一个评估集,对比量化前后的输出效果,不要只看显存省了多少。2.2 投机采样:让小模型帮你打头阵自回归解码的串行本质看似无解,但投机采样(Speculative Decoding)提供了一条“用小模型探路,大模型验证”的思路。简单说:首先用一个很小的草稿模型尝试生成接下来几步的 token;然后把这些 token 一次性交给大模型进行并行验证。如果草稿模型足够准,大模型一次前向就能确认多个 token,把原本多次串行计算压缩成一次,延迟自然大幅下降。这个方案听起来很美好,但对草稿模型质量要求极高。如果草稿模型预测的 token 经常被大模型拒绝,验证时会浪费算力,整体反而变慢。我实测下来,草稿模型最好和主模型来自同语系或同任务训练,且与主模型的 tokenizer 完全一致;另外,生成温度等解码参数也会影响接受率,低温确定性场景下收益更明显。投机采样适合算力有冗余、且对单请求延迟敏感的场景。如果系统本来就已经满负荷运行,草稿模型还会抢占本来属于主模型的算力,吞吐量可能不升反降。做方案选型前先跑几组小规模实验,对比“直出”和“投机采样”的 TTFT 与吞吐差异,再决定是否上线。2.3 连续批处理与动态请求调度传统批处理是将一批请求集合到最大值再统一计算,空闲时 GPU 只能干等。LLM 场景中不同请求的长度极不均匀:有人问“你好”,有人粘贴了一整篇论文。静态批处理会让短请求被长请求拖住,长请求又因为 batch 未满而浪费时间,整体效率很低。连续批处理(也叫在途批处理)改变了游戏规则:GPU 不再等待整个 batch 全部完成,而是每步结束就释放已完成或已经到最大长度的请求,同时立即把新请求插入空位。这个机制让显存和算力始终处于“边来边算”的流水线状态,吞吐提升非常明显。和连续批处理配套的是现代化的 KV Cache 管理方式。传统显存空间需要预先为每个请求预留整段连续空间,容易产生大量碎片;现在业界普遍采用类似操作系统内存分页的思路,把 KV Cache 切分成更小的块,按需分配,显存利用率可以大幅提升。实际部署中,这两个特性基本是靠推理框架层面实现的,选择框架时要格外关注是否支持,这直接影响你能同时服务多少并发。2.4 KV Cache 优化与上下文长度管理KV Cache 是自回归推理中为了省掉重复计算的缓存:前向计算时会算出每层 attention 的 key 和 value,若全部丢弃,每次生成新 token 都要重新计算前面所有 token 的 K/V,那成本是不可接受的。所以推理框架会把历史 K/V 缓存起来,代价就是显存占用随序列长度线性增长。KV Cache 的显存开销可以做这样近似估算:每个 token 的 KV Cache 大小 = 2(K 和 V 两份) × 层数 × 注意力头数 × 每个头的维度 × 数据类型字节数。举一个常见例子:假设一个模型有 32 层、32 个注意力头、每个头维度 128,BF16 类型占 2 字节,那么每 token 的 KV Cache 约 2 × 32 × 32 × 128 × 2 = 512KB。如果每个用户对话平均上下文是 2000 token,单用户就需要约 1GB 的 KV Cache。并发 100 个用户,光 KV Cache 就有 100GB 级别。这也是为什么很多团队想让模型支持“超长上下文”,却被显存卡得死死的。工程上除了依赖框架的显存管理外,还有几个常用手段:第一是 prefix caching,多个请求的公共前缀(比如系统的 system prompt)复用 KV Cache,能省下一大块重复计算;第二是 KV Cache 量化,用 INT8 甚至更低精度保存缓存;第三是结合业务场景,对单请求上下文长度做限制,不要给无限长的 prompt 留太多显存。我见过太多服务因为有人发了超长输入,直接把整卡显存打爆,结果所有用户一起遭殃。3. 服务架构与部署形态3.1 推理引擎选型的关键维度后端推理服务到底用哪套引擎,直接决定了优化手段能不能落地。我的习惯是先列出一张需求清单,再逐项打勾,而不是看哪个框架热度高就无脑上车。最关键的几个维度:是否支持量化模型加载,特别是 INT4/INT8 权重格式。是否支持连续批处理和分页式 KV Cache 管理。是否支持多卡张量并行,以便把大模型拆到多块 GPU 上。是否提供与主流生态兼容的 HTTP API,方便业务接入。是否支持流式输出,聊天场景几乎必备。社区活跃度和运维文档是否完善,这个在出问题时尤其重要。目前开源的推理后端基本都覆盖了上面大部分能力,但每个框架在显存分配策略、调度算法和算子优化上各有取舍。我的建议是:不要只看 benchmark,最好拿你自己的模型、自己的真实 prompt 分布,在同一批 GPU 上做压测对比。不同框架对长上下文的支持差异很大,有的在 batch 较大时表现好,有的在单请求延迟上更极致,适合的场景不一样。部署形态上,优先考虑把“推理引擎”作为一个有状态的服务独立部署,不要和业务后端进程混在一起。大模型推理进程容易因为显存问题被系统杀掉,混部会造成整个业务崩溃;独立部署还能方便单独扩缩容。3.2 多卡多机:张量并行与流水线并行当模型单卡放不下,或者需要更高吞吐时,多卡并行不可避免。最常用的是张量并行(把一层的权重矩阵切成多份,分布到多张卡上协同计算)和流水线并行(把模型层按顺序切分到多张卡,每张卡负责一段)。张量并行速度快,但每算一步都要在卡间做大量数据同步,通信压力极大,通常需要高速卡间互联才能发挥效果。流水线并行通信压力小,但天然存在流水线气泡,算力利用率不够满。生产环境里,小规模部署一般用张量并行更省心;到了千亿级别模型,纯张量并行会因为单卡无法容纳一层而被迫使用流水线并行。另一个容易被忽略的点是:多卡部署后,推理引擎对外要能根据显存余量自动调度请求。框架内部需要把 batch 里的不同请求均衡地分发到各卡,不要出现一张卡满载、另一张卡空闲的情况。上线前可以专门测试一下多卡下的负载均衡表现,方法很简单:压测时分别观察每张卡的利用率,如果某个卡明显偏低,说明均衡策略有问题。3.3 服务化:网关、限流与流式输出推理引擎本身更像一个“计算函数”,对外暴露模型能力,但生产环境还需要一层服务化工程。我的典型架构是:客户端 → 接入网关/负载均衡 → 推理引擎实例池 → 监控系统。网关负责请求路由、超时控制、限流和鉴权。LLM 推理一个请求可能要执行几十秒甚至几分钟,网关超时时间不能按普通 HTTP 接口的几秒来设置;但同时也要防止个别超长请求无限占用资源,所以一般会设置“最大执行时间”和“最大生成 token 数”。限流更是必要,否则一旦有突发流量进来,所有请求全部排队,延迟会急剧恶化,甚至拖垮整个实例。流式输出对用户体验影响非常大。聊天类场景里,用户看到首字出现的时间比等到全部答案再一次性返回要“快”得多。实现上可以基于 SSE 或 WebSocket,让推理引擎每生成一个 token 就推送一次。做流式输出时要注意客户端的断连处理,用户中途关掉页面后,后端如果还在傻傻地继续生成,会产生无效计算;不少推理框架支持取消机制,接入时要确认这一点。3.4 显存估算与实例规格选择搭建服务前,先做一个显存预算表,能省掉很多上线后的麻烦。预算表包括:模型权重、KV Cache、激活值、CUDA 上下文和推理框架开销。简单估算方式:模型权重:参数量 × 单参数字节数。7B 模型 FP16 约 13GB,BF16 约 14GB,INT8 约 7GB,INT4 约 3.5GB。KV Cache:按上文公式估算,再乘上目标并发数和平均上下文长度。激活值:与 batch size 和隐藏层维度相关,预填充阶段比较吃显存,但通常比权重和 KV Cache 小。框架开销:一般预留 1~2GB。举个例子:假设我们在单张 48GB 的卡上部署一个 13B BF16 模型,权重约 26GB,剩下约 20GB 可以给 KV Cache。如果设置平均单用户 KV Cache 约 0.5GB,那么理论并发上限约 40 个。但实际还要考虑激活值和碎片,通常只能跑到理论值的 70% 左右。想要提高并发,要么换更大显存卡,要么降低精度,要么限制上下文长度。这个预算表建议做成动态脚本,方便在模型或并发目标变化时快速重新计算。4. 一个从加载到调优的实操示例4.1 环境准备与模型加载我拿一个模拟项目 X 举例,模型是一个开源的 7B 规模中文对话模型,服务目标是线上聊天场景。硬件条件只是普通的 4 卡 48GB GPU 服务器,但初期只用单卡做验证。环境准备阶段,先把推理框架、模型权重和依赖库准备好。这里用一段示意性的启动命令,具体名称已隐去,表达通用的配置方式:# 拉取模型权重到本地目录 # 启动推理服务 llm-engine serve ./model_7b \ --host 0.0.0.0 \ --port 8000 \ --dtype auto \ --max-model-len 8192 \ --gpu-memory-utilization 0.9 \ --max-num-seqs 16其中--max-model-len表示模型支持的最大序列长度,--gpu-memory-utilization表示预留多少比例的显存给 KV Cache 和运行时,--max-num-seqs控制了单个实例内部同时计算的请求数。初次启动时,我一般把max-model-len设为业务实际需要的长度,而不是盲目拉满。如果设置太长,框架会为极长上下文预留大量 KV Cache,导致实际并发数降低。启动后先用 curl 发送一个最简单的请求验证服务健康,再检查日志里有没有显存不足的警告。这里有个容易被忽略点:模型加载本身也可能触发 CUDA 初始化,第一次请求往往比后续请求慢很多,所以不要拿第一次请求的耗时当作真实性能。4.2 压测与关键参数调整服务跑起来后,先做一轮基线压测。压测脚本我这里用一个简单的 Python 并发请求来做说明:import asyncio import time import httpx async def send_one(client, prompt: str): payload = { "prompt": prompt, "max_tokens": 256, "temperature": 0.7, } start = time.perf_counter() first_token_time = None async with client.stream("POST", "http://localhost:8000/generate", json=payload) as resp: async for chunk in resp.aiter_bytes(): if first_token_time is None: first_token_time = time.perf_counter() - start return first_token_time, time.perf_counter() - start async def main(): prompts = ["介绍一下大模型推理优化。" for _ in range(40)] async with httpx.AsyncClient(timeout=120) as client: tasks = [send_one(client, p) for p in prompts] results = await asyncio.gather(*tasks) avg_ttft = sum(r[0] for r in results) / len(results) avg_total = sum(r[1] for r in results) / len(results) print(f"AVG TTFT: {avg_ttft:.2f}s, AVG total: {avg_total:.2f}s")跑完基线后记录三件事:TTFT 均值、生成 256 个 token 的总耗时、GPU 显存和利用率。如果发现 TTFT 明显偏高,优先检查是不是并发请求太多导致的排队;如果显存利用率接近 100%,说明max-num-seqs或max-model-len设置过大,需要往回收。调参顺序我总结为一个原则:先调整显存利用率,再调整并发数,最后才考虑量化。具体操作是先把gpu-memory-utilization从默认值调整到比如 0.85~0.95,观察显存是否稳定;然后逐步提高max-num-seqs,每次加一倍并压测,找到吞吐增长的“拐点”——通常加到某个值后,TPOT 会显著上升,再继续加就没意义了。如果显存确实不够,再上量化或者限制上下文长度。4.3 上线后的监控与扩缩容推理服务上线后,我建议至少盯住这几项指标:GPU 利用率、显存占用、请求排队长度、TTFT/TPOT 的 P50 和 P99、错误率和超时率。这个环节最容易被低估的是“排队长度”。很多人看 GPU 利用率挺高就放心了,但实际所有请求都在队列里等着,用户侧延迟已经爆炸。所以监控里一定要有排队请求数,超过设定阈值就触发扩容或限流。扩缩容策略不要只盯着 CPU 或内存,LLM 推理瓶颈在显存和算力,应该以 GPU 利用率和队列深度为核心信号。自动扩缩容时,还要考虑推理服务的“冷启动时间”。一个实例从创建到真正能接流量,往往要加载权重、构建 CUDA 内核,耗时可能几分钟。所以不能等队列爆了才扩容,应该设置稍小的队列阈值,提前扩容;缩容时则要谨慎,避免把正在处理长请求的实例杀掉。5. 常见问题与排查技巧实录5.1 OOM 与显存突然爆掉这是推理部署第一大坑。症状通常是:服务日志里出现显存不足报错,或者整个进程被操作系统杀掉,请求全部失败。排查思路按顺序来:先看是什么请求触发。如果只是某些超长输入导致,说明上下文长度控制没做好,需要在网关层限制max_prompt_length。再看并发设置是否过高。max-num-seqs调太高时,即使平均显存够用,峰值也会因为多个长请求叠加而被击穿。检查推理框架的显存碎片情况。如果频繁出现显存不够但重新启动后又能撑一会儿,大概率是碎片化问题,这时可以尝试开启更积极的内存整理,或者降低gpu-memory-utilization留下一部分余量。预防 OOM 的实用手段:在网关处按上下文长度分级限流,比如“长 prompt 请求最多同时 N 个”;或者让推理框架拒绝超过max-model-len的请求,并返回明确错误提示,而不是硬着头皮算。5.2 首字很慢:TTFT 过高用户反馈“半天不说话”,十有八九是 TTFT 问题。调试时先分清:是排队慢,还是预填充计算慢?如果是排队慢,压测结果会看到大量请求积压,此时优先优化调度:降低并发请求数、增加实例、开启连续批处理。如果是预填充计算慢,说明单请求第一次前向计算开销太大。常见原因是输入 prompt 长度过长,预填充阶段要对全部 token 做并行注意力计算;此时可以考虑对 prompt 做压缩或摘要,也可以把公共 system prompt 提前缓存,避免每次都重新计算。此外,预填充和解码阶段可以拆分执行。有些框架支持把 prefill 操作放在单独的 GPU 流或单独的实例上,避免长 prompt 请求拖慢所有在线生成请求。能拆分的话,TTFT 能稳定不少。5.3 量化后输出质量明显下降很多人第一次跑 INT4 模型时,会惊讶地发现“模型变傻了”。这不是模型坏了,而是量化策略没有校准好。先用一个不量化的同版本模型跑同样的测试集,对比输出,确认差异范围。如果确实差异明显,查看量化配置文件,确认是否把所有层都压到低比特。此时可以尝试只对部分敏感层保持 FP16、其他层量化。另一个思路是增加校准数据集,让它更贴近线上真实 prompt 分布,而不是用公开数据集。量化模型上线前,至少要跑一遍黄金测试集,包含业务侧的高频问题、长文档摘要、多轮对话等场景。5.4 几个容易忽略的小窍门模型预热。新实例启动后,先发送几个普通请求,让 CUDA kernel、显存分配、缓存都初始化完成,再接入真实流量。否则第一批请求可能极其缓慢,甚至触发超时。压测时的 prompt 不要用完全重复的文本。如果所有请求内容都一样,系统可能命中 prompt cache,性能数据会虚高。压测数据要尽量模拟真实长度分布和话题多样性。流式输出时,网络层容易成为瓶颈。如果每次只推一个字符,HTTP 头开销占比太高。可以配置批量推送,比如每 5~10 个 token 合并发送一次,这样在保证流畅度的同时减少网络压力。我在实际项目里踩过最深的坑,是上线前只测了平均延迟,没测 P99。结果线上突然来了一波长文本请求,P99 直接飙到 30 秒以上,用户反馈汹涌而来。后来我把压测的重点放在长尾延迟和不同长度混合请求场景上,并且在监控面板里长期展示队列深度与显存碎片率,这才算真正把推理服务做稳。推理优化不是一锤子买卖,先拿到干净基线,再一个变量一个变量地调,每一步都有数据支撑,远比盲目追求某张 benchmark 截图靠谱。
大语言模型能写诗、能写代码,可是一旦真实跑到线上去服务用户,推理优化与部署才是真正决定系统能不能撑住的那道关卡。很多团队花大力气训好了模型,结果一上线就暴露各种问题:单请求时延太高、并发上来直接OOM、一个长对话把显存吃光。这周我正好在整理一套LLM工程系统的推理优化与部署环节,把从指标拆解、算力优化、服务架构到上线排查的完整思路梳理了一遍。这篇文章适合正在或准备做LLM应用落地的算法工程师、后端开发同学,也适合想搞懂大模型服务成本与性能瓶颈的技术负责人。这里不聊训练,只聊推理,而且尽量说人话,把原理和步骤都摊开讲。1. 推理优化到底在解决什么问题1.1 从自回归生成看瓶颈来源大模型推理和传统深度学习推理最大的不同在于“自回归解码”。模型每次只能根据已经生成的 token 预测下一个 token,然后把新生成的 token 拼到输入序列尾巴上,再接着预测下一个。这个串行过程意味着一个长度为 1000 的回复,至少要经历 1000 次前向计算,每一步都依赖上一步的输出,无法像图像分类那样一次性算出全部结果。这个特性带来了两个直接后果:第一,单个请求的延迟天然偏大,尤其当上下文长度不断增大时,每一步需要处理的序列都在变长;第二,显存消耗不仅是模型权重本身,还包括不断累加的中间状态,也就是 KV Cache。如果你把模型类比成一个特别认真的作者,每写一个新字都要把前面所有内容重新读一遍,那就很容易理解为什么大模型推理会又慢又费内存。在实际服务中,模型权重当然要占显存,KV Cache 往往比权重更占地方。以常见的 70 亿参数模型为例,BF16 格式的权重大约 14GB,而每个请求的 KV Cache 会随着上下文长度和并发用户数成倍增长。如果同时服务几十个用户,每个用户都有一长段上下文,显存通常会在权重之上再占用几十甚至上百GB。这也是为什么很多团队一开始天真地用“模型权重大小”来估算显存需求,结果一上线就翻车。1.2 必须死磕的四个核心指标搞推理优化之前,先把评价指标定下来,否则很容易陷入“调了一堆参数,但不知道有没有变好”的尴尬。我通常把这四个指标作为项目对齐的基准:指标代表什么一般目标首token延迟(TTFT)用户发出请求到收到第一个字的时间通常希望小于 1 秒,流式聊天场景越短越好每token生成延迟(TPOT)后续每个字之间的间隔通常希望小于 50ms,否则有明显的“卡顿”感吞吐量单位时间内系统总共生成的 token 数尽可能高,直接决定服务成本显存占用量权重、KV Cache 和框架开销的总和决定单卡能支撑多少并发与多长上下文TTFT 反映的是“预填充”阶段的效率。预填充阶段要把用户输入的所有 prompt 一次性并行算完,计算密集,所以非常吃算力,不能简单通过增加并发来解决。TPOT 反映的是“解码”阶段的效率,是系统连续推理能力的体现。吞吐量则跟批处理策略、显存管理、量化程度强相关。实际优化时,这四个指标经常互相矛盾:无限并发可以提高吞吐,但会让每个请求互相争抢算力,导致 TPOT 变差;量化可以减少显存,但如果量化校准没做好,输出质量会明显下降。所以做推理优化的本质不是追求某一项指标登顶,而是在给定算力预算下,找到延迟和吞吐、成本和效果的平衡点。2. 模型与计算层面的优化手段2.1 量化:不只是降低精度这么简单量化的核心思路,是用更低精度的数值表示模型参数,从而减少显存占用和计算量。最常用的是把 FP16/BF16 降到 INT8 或 INT4。为什么神经网络可以接受这种压缩?因为模型参数存在大量冗余,而且很多权重的分布集中在某个较小的数值范围内,我们可以在尽量不损失信息的前提下用更少的比特去近似它。量化方式分两大类:训练后量化(PTQ)和量化感知训练(QAT)。推理部署场景里,PTQ 最常用,因为它不需要重新训练模型。PTQ 又分为只量化权重的 Weight-only Quantization,以及同时量化激活值和 KV Cache 的 W8A8 等方案。对 LLM 来说,权重量化是最直接省显存的方式:把 70 亿参数从 FP16 压成 INT4,权重可以从约 14GB 降到约 3.5GB,单卡能容纳的模型规模立刻变大。不过量化不是白嫖。如果只做简单的 round-to-nearest 量化,模型能力往往会明显退化;如今主流方案会结合激活值分布、通道离散度做优化,这类方法能显著降低量化误差。实操中我一般建议:如果 GPU 显存充足,先用 BF16/FP16 跑通全部业务场景,再尝试 INT8;只有当显存确实放不下更长上下文或更高并发时,才考虑 INT4。千万不能为了节省显存而盲目上低比特,最后模型输出内容质量崩塌,反而得不偿失。另外要注意,量化对注意力层的影响往往比其他层更大。某些架构中,K、V、Q 这些投影层的权重对精度更敏感,社区里也有针对特定敏感层做“跳过量化”的做法。所以上线前一定要准备一个评估集,对比量化前后的输出效果,不要只看显存省了多少。2.2 投机采样:让小模型帮你打头阵自回归解码的串行本质看似无解,但投机采样(Speculative Decoding)提供了一条“用小模型探路,大模型验证”的思路。简单说:首先用一个很小的草稿模型尝试生成接下来几步的 token;然后把这些 token 一次性交给大模型进行并行验证。如果草稿模型足够准,大模型一次前向就能确认多个 token,把原本多次串行计算压缩成一次,延迟自然大幅下降。这个方案听起来很美好,但对草稿模型质量要求极高。如果草稿模型预测的 token 经常被大模型拒绝,验证时会浪费算力,整体反而变慢。我实测下来,草稿模型最好和主模型来自同语系或同任务训练,且与主模型的 tokenizer 完全一致;另外,生成温度等解码参数也会影响接受率,低温确定性场景下收益更明显。投机采样适合算力有冗余、且对单请求延迟敏感的场景。如果系统本来就已经满负荷运行,草稿模型还会抢占本来属于主模型的算力,吞吐量可能不升反降。做方案选型前先跑几组小规模实验,对比“直出”和“投机采样”的 TTFT 与吞吐差异,再决定是否上线。2.3 连续批处理与动态请求调度传统批处理是将一批请求集合到最大值再统一计算,空闲时 GPU 只能干等。LLM 场景中不同请求的长度极不均匀:有人问“你好”,有人粘贴了一整篇论文。静态批处理会让短请求被长请求拖住,长请求又因为 batch 未满而浪费时间,整体效率很低。连续批处理(也叫在途批处理)改变了游戏规则:GPU 不再等待整个 batch 全部完成,而是每步结束就释放已完成或已经到最大长度的请求,同时立即把新请求插入空位。这个机制让显存和算力始终处于“边来边算”的流水线状态,吞吐提升非常明显。和连续批处理配套的是现代化的 KV Cache 管理方式。传统显存空间需要预先为每个请求预留整段连续空间,容易产生大量碎片;现在业界普遍采用类似操作系统内存分页的思路,把 KV Cache 切分成更小的块,按需分配,显存利用率可以大幅提升。实际部署中,这两个特性基本是靠推理框架层面实现的,选择框架时要格外关注是否支持,这直接影响你能同时服务多少并发。2.4 KV Cache 优化与上下文长度管理KV Cache 是自回归推理中为了省掉重复计算的缓存:前向计算时会算出每层 attention 的 key 和 value,若全部丢弃,每次生成新 token 都要重新计算前面所有 token 的 K/V,那成本是不可接受的。所以推理框架会把历史 K/V 缓存起来,代价就是显存占用随序列长度线性增长。KV Cache 的显存开销可以做这样近似估算:每个 token 的 KV Cache 大小 = 2(K 和 V 两份) × 层数 × 注意力头数 × 每个头的维度 × 数据类型字节数。举一个常见例子:假设一个模型有 32 层、32 个注意力头、每个头维度 128,BF16 类型占 2 字节,那么每 token 的 KV Cache 约 2 × 32 × 32 × 128 × 2 = 512KB。如果每个用户对话平均上下文是 2000 token,单用户就需要约 1GB 的 KV Cache。并发 100 个用户,光 KV Cache 就有 100GB 级别。这也是为什么很多团队想让模型支持“超长上下文”,却被显存卡得死死的。工程上除了依赖框架的显存管理外,还有几个常用手段:第一是 prefix caching,多个请求的公共前缀(比如系统的 system prompt)复用 KV Cache,能省下一大块重复计算;第二是 KV Cache 量化,用 INT8 甚至更低精度保存缓存;第三是结合业务场景,对单请求上下文长度做限制,不要给无限长的 prompt 留太多显存。我见过太多服务因为有人发了超长输入,直接把整卡显存打爆,结果所有用户一起遭殃。3. 服务架构与部署形态3.1 推理引擎选型的关键维度后端推理服务到底用哪套引擎,直接决定了优化手段能不能落地。我的习惯是先列出一张需求清单,再逐项打勾,而不是看哪个框架热度高就无脑上车。最关键的几个维度:是否支持量化模型加载,特别是 INT4/INT8 权重格式。是否支持连续批处理和分页式 KV Cache 管理。是否支持多卡张量并行,以便把大模型拆到多块 GPU 上。是否提供与主流生态兼容的 HTTP API,方便业务接入。是否支持流式输出,聊天场景几乎必备。社区活跃度和运维文档是否完善,这个在出问题时尤其重要。目前开源的推理后端基本都覆盖了上面大部分能力,但每个框架在显存分配策略、调度算法和算子优化上各有取舍。我的建议是:不要只看 benchmark,最好拿你自己的模型、自己的真实 prompt 分布,在同一批 GPU 上做压测对比。不同框架对长上下文的支持差异很大,有的在 batch 较大时表现好,有的在单请求延迟上更极致,适合的场景不一样。部署形态上,优先考虑把“推理引擎”作为一个有状态的服务独立部署,不要和业务后端进程混在一起。大模型推理进程容易因为显存问题被系统杀掉,混部会造成整个业务崩溃;独立部署还能方便单独扩缩容。3.2 多卡多机:张量并行与流水线并行当模型单卡放不下,或者需要更高吞吐时,多卡并行不可避免。最常用的是张量并行(把一层的权重矩阵切成多份,分布到多张卡上协同计算)和流水线并行(把模型层按顺序切分到多张卡,每张卡负责一段)。张量并行速度快,但每算一步都要在卡间做大量数据同步,通信压力极大,通常需要高速卡间互联才能发挥效果。流水线并行通信压力小,但天然存在流水线气泡,算力利用率不够满。生产环境里,小规模部署一般用张量并行更省心;到了千亿级别模型,纯张量并行会因为单卡无法容纳一层而被迫使用流水线并行。另一个容易被忽略的点是:多卡部署后,推理引擎对外要能根据显存余量自动调度请求。框架内部需要把 batch 里的不同请求均衡地分发到各卡,不要出现一张卡满载、另一张卡空闲的情况。上线前可以专门测试一下多卡下的负载均衡表现,方法很简单:压测时分别观察每张卡的利用率,如果某个卡明显偏低,说明均衡策略有问题。3.3 服务化:网关、限流与流式输出推理引擎本身更像一个“计算函数”,对外暴露模型能力,但生产环境还需要一层服务化工程。我的典型架构是:客户端 → 接入网关/负载均衡 → 推理引擎实例池 → 监控系统。网关负责请求路由、超时控制、限流和鉴权。LLM 推理一个请求可能要执行几十秒甚至几分钟,网关超时时间不能按普通 HTTP 接口的几秒来设置;但同时也要防止个别超长请求无限占用资源,所以一般会设置“最大执行时间”和“最大生成 token 数”。限流更是必要,否则一旦有突发流量进来,所有请求全部排队,延迟会急剧恶化,甚至拖垮整个实例。流式输出对用户体验影响非常大。聊天类场景里,用户看到首字出现的时间比等到全部答案再一次性返回要“快”得多。实现上可以基于 SSE 或 WebSocket,让推理引擎每生成一个 token 就推送一次。做流式输出时要注意客户端的断连处理,用户中途关掉页面后,后端如果还在傻傻地继续生成,会产生无效计算;不少推理框架支持取消机制,接入时要确认这一点。3.4 显存估算与实例规格选择搭建服务前,先做一个显存预算表,能省掉很多上线后的麻烦。预算表包括:模型权重、KV Cache、激活值、CUDA 上下文和推理框架开销。简单估算方式:模型权重:参数量 × 单参数字节数。7B 模型 FP16 约 13GB,BF16 约 14GB,INT8 约 7GB,INT4 约 3.5GB。KV Cache:按上文公式估算,再乘上目标并发数和平均上下文长度。激活值:与 batch size 和隐藏层维度相关,预填充阶段比较吃显存,但通常比权重和 KV Cache 小。框架开销:一般预留 1~2GB。举个例子:假设我们在单张 48GB 的卡上部署一个 13B BF16 模型,权重约 26GB,剩下约 20GB 可以给 KV Cache。如果设置平均单用户 KV Cache 约 0.5GB,那么理论并发上限约 40 个。但实际还要考虑激活值和碎片,通常只能跑到理论值的 70% 左右。想要提高并发,要么换更大显存卡,要么降低精度,要么限制上下文长度。这个预算表建议做成动态脚本,方便在模型或并发目标变化时快速重新计算。4. 一个从加载到调优的实操示例4.1 环境准备与模型加载我拿一个模拟项目 X 举例,模型是一个开源的 7B 规模中文对话模型,服务目标是线上聊天场景。硬件条件只是普通的 4 卡 48GB GPU 服务器,但初期只用单卡做验证。环境准备阶段,先把推理框架、模型权重和依赖库准备好。这里用一段示意性的启动命令,具体名称已隐去,表达通用的配置方式:# 拉取模型权重到本地目录 # 启动推理服务 llm-engine serve ./model_7b \ --host 0.0.0.0 \ --port 8000 \ --dtype auto \ --max-model-len 8192 \ --gpu-memory-utilization 0.9 \ --max-num-seqs 16其中--max-model-len表示模型支持的最大序列长度,--gpu-memory-utilization表示预留多少比例的显存给 KV Cache 和运行时,--max-num-seqs控制了单个实例内部同时计算的请求数。初次启动时,我一般把max-model-len设为业务实际需要的长度,而不是盲目拉满。如果设置太长,框架会为极长上下文预留大量 KV Cache,导致实际并发数降低。启动后先用 curl 发送一个最简单的请求验证服务健康,再检查日志里有没有显存不足的警告。这里有个容易被忽略点:模型加载本身也可能触发 CUDA 初始化,第一次请求往往比后续请求慢很多,所以不要拿第一次请求的耗时当作真实性能。4.2 压测与关键参数调整服务跑起来后,先做一轮基线压测。压测脚本我这里用一个简单的 Python 并发请求来做说明:import asyncio import time import httpx async def send_one(client, prompt: str): payload = { "prompt": prompt, "max_tokens": 256, "temperature": 0.7, } start = time.perf_counter() first_token_time = None async with client.stream("POST", "http://localhost:8000/generate", json=payload) as resp: async for chunk in resp.aiter_bytes(): if first_token_time is None: first_token_time = time.perf_counter() - start return first_token_time, time.perf_counter() - start async def main(): prompts = ["介绍一下大模型推理优化。" for _ in range(40)] async with httpx.AsyncClient(timeout=120) as client: tasks = [send_one(client, p) for p in prompts] results = await asyncio.gather(*tasks) avg_ttft = sum(r[0] for r in results) / len(results) avg_total = sum(r[1] for r in results) / len(results) print(f"AVG TTFT: {avg_ttft:.2f}s, AVG total: {avg_total:.2f}s")跑完基线后记录三件事:TTFT 均值、生成 256 个 token 的总耗时、GPU 显存和利用率。如果发现 TTFT 明显偏高,优先检查是不是并发请求太多导致的排队;如果显存利用率接近 100%,说明max-num-seqs或max-model-len设置过大,需要往回收。调参顺序我总结为一个原则:先调整显存利用率,再调整并发数,最后才考虑量化。具体操作是先把gpu-memory-utilization从默认值调整到比如 0.85~0.95,观察显存是否稳定;然后逐步提高max-num-seqs,每次加一倍并压测,找到吞吐增长的“拐点”——通常加到某个值后,TPOT 会显著上升,再继续加就没意义了。如果显存确实不够,再上量化或者限制上下文长度。4.3 上线后的监控与扩缩容推理服务上线后,我建议至少盯住这几项指标:GPU 利用率、显存占用、请求排队长度、TTFT/TPOT 的 P50 和 P99、错误率和超时率。这个环节最容易被低估的是“排队长度”。很多人看 GPU 利用率挺高就放心了,但实际所有请求都在队列里等着,用户侧延迟已经爆炸。所以监控里一定要有排队请求数,超过设定阈值就触发扩容或限流。扩缩容策略不要只盯着 CPU 或内存,LLM 推理瓶颈在显存和算力,应该以 GPU 利用率和队列深度为核心信号。自动扩缩容时,还要考虑推理服务的“冷启动时间”。一个实例从创建到真正能接流量,往往要加载权重、构建 CUDA 内核,耗时可能几分钟。所以不能等队列爆了才扩容,应该设置稍小的队列阈值,提前扩容;缩容时则要谨慎,避免把正在处理长请求的实例杀掉。5. 常见问题与排查技巧实录5.1 OOM 与显存突然爆掉这是推理部署第一大坑。症状通常是:服务日志里出现显存不足报错,或者整个进程被操作系统杀掉,请求全部失败。排查思路按顺序来:先看是什么请求触发。如果只是某些超长输入导致,说明上下文长度控制没做好,需要在网关层限制max_prompt_length。再看并发设置是否过高。max-num-seqs调太高时,即使平均显存够用,峰值也会因为多个长请求叠加而被击穿。检查推理框架的显存碎片情况。如果频繁出现显存不够但重新启动后又能撑一会儿,大概率是碎片化问题,这时可以尝试开启更积极的内存整理,或者降低gpu-memory-utilization留下一部分余量。预防 OOM 的实用手段:在网关处按上下文长度分级限流,比如“长 prompt 请求最多同时 N 个”;或者让推理框架拒绝超过max-model-len的请求,并返回明确错误提示,而不是硬着头皮算。5.2 首字很慢:TTFT 过高用户反馈“半天不说话”,十有八九是 TTFT 问题。调试时先分清:是排队慢,还是预填充计算慢?如果是排队慢,压测结果会看到大量请求积压,此时优先优化调度:降低并发请求数、增加实例、开启连续批处理。如果是预填充计算慢,说明单请求第一次前向计算开销太大。常见原因是输入 prompt 长度过长,预填充阶段要对全部 token 做并行注意力计算;此时可以考虑对 prompt 做压缩或摘要,也可以把公共 system prompt 提前缓存,避免每次都重新计算。此外,预填充和解码阶段可以拆分执行。有些框架支持把 prefill 操作放在单独的 GPU 流或单独的实例上,避免长 prompt 请求拖慢所有在线生成请求。能拆分的话,TTFT 能稳定不少。5.3 量化后输出质量明显下降很多人第一次跑 INT4 模型时,会惊讶地发现“模型变傻了”。这不是模型坏了,而是量化策略没有校准好。先用一个不量化的同版本模型跑同样的测试集,对比输出,确认差异范围。如果确实差异明显,查看量化配置文件,确认是否把所有层都压到低比特。此时可以尝试只对部分敏感层保持 FP16、其他层量化。另一个思路是增加校准数据集,让它更贴近线上真实 prompt 分布,而不是用公开数据集。量化模型上线前,至少要跑一遍黄金测试集,包含业务侧的高频问题、长文档摘要、多轮对话等场景。5.4 几个容易忽略的小窍门模型预热。新实例启动后,先发送几个普通请求,让 CUDA kernel、显存分配、缓存都初始化完成,再接入真实流量。否则第一批请求可能极其缓慢,甚至触发超时。压测时的 prompt 不要用完全重复的文本。如果所有请求内容都一样,系统可能命中 prompt cache,性能数据会虚高。压测数据要尽量模拟真实长度分布和话题多样性。流式输出时,网络层容易成为瓶颈。如果每次只推一个字符,HTTP 头开销占比太高。可以配置批量推送,比如每 5~10 个 token 合并发送一次,这样在保证流畅度的同时减少网络压力。我在实际项目里踩过最深的坑,是上线前只测了平均延迟,没测 P99。结果线上突然来了一波长文本请求,P99 直接飙到 30 秒以上,用户反馈汹涌而来。后来我把压测的重点放在长尾延迟和不同长度混合请求场景上,并且在监控面板里长期展示队列深度与显存碎片率,这才算真正把推理服务做稳。推理优化不是一锤子买卖,先拿到干净基线,再一个变量一个变量地调,每一步都有数据支撑,远比盲目追求某张 benchmark 截图靠谱。