刚接到这个需求时我心里是一万个不信的。光看“推理性能提升100倍”这七个字直觉上就觉得是某个老板被供应商忽悠后拿回来的模糊KPI。做GPU部署的都知道一个开源GPT OSS模型放上内网能跑通就已经烧高香了更不要太离谱的性能翻倍。但真正把链路拆开后我承认这个数字站得住脚——前提是你得把“100倍”的对比口径定对而不是简单拿一张卡、一个请求、一个token在那儿自嗨。这篇文章要聊的就是把开源GPT类大模型私有部署后的推理性能从“跑得动”拉到“扛得住并发”的完整上篇。我默认你已经接触过模型部署但不一定系统做过性能优化。上篇先把瓶颈原理、硬件选型、基线和三把最关键的优化刀量化、连续批处理、PD分离讲透下篇再展开更细的参数和案例数据。内容不依赖某一款商业产品以业界常见的推理加速逻辑为主落地时你可以直接替换成自己团队熟悉的那套部署框架。1. 推理慢的根因GPU算力冗余卡在权重搬运上1.1 单请求解码时显卡到底在忙什么大模型生成是自回归的一次只能吐一个token。每吐一个token要跑一遍完整的Transformer前向把权重矩阵从显存搬到计算单元算一次矩阵乘再做激活、归一化、采样。问题就出在“把权重矩阵从显存搬到计算单元”这一步。以我自己常用的一个70亿参数开源模型为例FP16精度下权重大约是14GB。解码阶段每个token都要把这14GB从头到尾读一遍数据搬进片上SRAM后实际算的东西却很少——每个权重参数大概只参与一两次乘加。用大白话说就是一列满载重卡的专列只送一个小包裹。GPU的算力根本没有吃饱大部分时间都在等数据从显存里“运”过来。数字比直觉更直观同样70亿FP16权重1TB/s左右的显存带宽下读一轮权重就要大约14毫秒而这一轮前向的计算量大概只有十几GFLOPs哪怕算力是50TFLOPS纯算只需要0.3毫秒不到。两者差了接近两个数量级。所以结论很明确单请求解码的性能瓶颈不是算力而是显存带宽。1.2 隐性瓶颈KV Cache、动态batch和静默等待只有带宽一个瓶颈还简单真正让部署者头大的是KV Cache。自回归推理每解码一个token都要把之前所有token的Key和Value向量缓存下来供后续注意力计算使用。上下文越长KV Cache越膨胀并发一高直接把显存吃穿。我算过一笔账某个32层、hidden size为4096、使用GQA的70亿模型FP16下单条8192个token的上下文KV Cache大约要占1GB。看着不算多但并发16个请求、每请求8K长度就是16GB已经超过一张卡的全部显存。如果权重还是FP16那就更放不下了。还有一个容易被忽略的坑动态形状导致GPU利用率碎片化。假如一个batch里有5个请求有的prompt长有的prompt短解码速度也不一样。传统静态批处理会等整个batch全部生成完才腾出位置下一个请求就算早到了也只能干等。结果就是算力闲一段、忙一段。很多团队说“卡明明没满但很慢”本质就是这两个拖油瓶在拖后腿。1.3 用生活类比理解整条链路把GPU想象成一家餐厅算力是炒锅显存带宽是传菜通道KV Cache是餐桌请求是客人。经典模式是一批客人同时到店厨房必须等这一批全吃完才允许下一批进门。每道菜每个token都要从仓库显存取一遍所有食材权重取食材的时间比炒菜时间还长传菜通道就那么窄锅再大也在空转。你说能不慢吗理解了这几个角色后面所有优化动作就都有了方向要么让菜小一点量化要么让传菜快一点优化访存要么让座位翻台率高一点连续批处理要么把“配菜”和“炒菜”分成不同厨房PD分离。2. “100倍”是怎么算出来的先把对比口径定清楚2.1 基线必须足够朴素一次一请求、FP16、无调度我见过很多团队号称“优化了100倍”翻看他们的baseline居然是用Python写了个for循环逐个token调模型forward连KV Cache都不会复用。这种基线当然慢但也不能说没意义——因为很多第一次做私有部署的团队第一版就是这么跑的。在讨论提升倍数前先把baseline固定住单并发、一次只处理一个请求、FP16精度、不做量化、不做任何批处理调度、单卡运行。这就是一个最“天然”的基线也是很多从demo走到内网环境的团队最初的样子。为什么不拿一个已经优化过的框架当基线因为优化过的框架自带了连续批处理、显存复用、调度器你再去叠加量化边际效果会变小体现不出“从零到一”的差距。做性能工程基线必须能复现、能被自己写脚本控制否则所有的倍数都是空中楼阁。2.2 优化包的乘法效应四把刀叠加“提升100倍”不是一个大优化完成的而是几个优化手段乘在一起的效果。第一把刀是权重量化。权重从FP16降到INT8显存读取量减半降到INT4读取量只有原来的四分之一。按照第一节的结论这等于把传菜时间压缩4倍。第二把刀是连续批处理。原本静态batch下GPU利用率波动大换成连续批处理吞吐通常能提5到20倍具体取决于请求长短分布。第三把刀是PD分离。把计算密集的预填充prefill和访存密集的解码decode分开调度能让并发稳定性和吞吐再上一个台阶尤其是长prompt比例高的场景。第四把刀是多卡并行两张卡就是理论上的2倍四张卡就是4倍受通信效率影响会打些折扣。100倍不是4乘5乘2乘2算出来的精确账更多是不同场景下各手段的叠加结果。我自己实测的场景里“量化3到4倍 × 连续批处理8到15倍 × 两张卡1.6到1.8倍”就已经非常接近100倍了。如果再把PD分离打开超过100倍也不奇怪。2.3 单条延迟和整体吞吐的差别这里必须提醒一个容易跟人吵架的点100倍提升的是整体吞吐不是“单条prompt的逐token延迟”。单用户请求某个模型的输出速度优化前大概是35 token/s优化后可能变成45 token/s只有30%到50%的提升。因为单流场景下无论怎么改每一轮都要读一遍权重带宽瓶颈还在。但100个用户同时请求时情况完全不同。连续的批处理让GPU每个时刻都在处理大量token吞吐从每秒几百token变成每秒几万token。所以指标的表述应该改成“在XX并发下对应到整体吞吐token/s较朴素基线提升约100倍”而不是“每个请求都快了100倍”。这话虽然不酷但严谨也更能扛住别人的质疑。我建议所有想用“100倍”做汇报的团队都先在内部把这两个指标分开写单流延迟、并发吞吐。前者是体验相关的“给单个用户的感觉”后者是成本相关的“服务器能扛多少人”。这次优化的主线其实是成本线。3. 硬件与模型选型别在1张卡上期待奇迹3.1 显存账本权重、KV Cache、临时激活各占多少动手部署前先学会算显存。公式不复杂三笔账。第一笔是模型权重。参数量乘以精度字节数70亿参数FP16是14GBINT8是7GBINT4是3.5GB左右。第二笔是KV Cache。粗略估算层数 × 2 × hidden_size × 序列长度 × 精度字节数。不同模型差异大建议直接用官方文档给出的每token缓存值然后乘以总并发token数。第三笔是临时激活值和计算缓冲。这部分在长序列或大batch下容易暴涨而且不能提前压缩。除了这三笔还要留出至少10%的显存给碎片和管理开销。你可以先把权重精度定下来再用公式反推能跑多长的上下文、多少并发。举个例子一张24GB的卡模型INT4后占4GB剩下20GB给KV Cache。如果每token缓存0.13MB8K上下文约1GB折算过来你可以承载大约15万token的并发总量也就是大概18到20个8K请求。这个数一算出来max_num_seqs怎么设就有了依据。3.2 显卡选型高带宽比大显存更关键显存够不够是一回事搬运速度快不快是另一回事。做推理优化后你会对“显存带宽”这个词格外敏感。同样推理70亿模型专业计算卡和普通消费卡之间带宽差距能到2到3倍最终吞吐差距也很明显。我的选型原则很简单先算需要的总显存再在预算内选带宽最高的方案。单卡能放下选单卡放不下优先考虑多卡之间的互联带宽。如果卡间走的是PCIe通道张量并行时通信会成为瓶颈这时候宁愿每个卡独立跑一份量化模型做数据并行也不要硬上需要频繁通信的模型并行。另外不是显存越大越好。很多大显存的卡带宽一般跑大模型时反而不如小显存高带宽的卡流畅。购买前一定要看HBM带宽指标这是大模型推理时代的硬通货。3.3 模型选择的三个隐藏条件很多人选模型只看榜单分数但私有部署性能优化会多出几个隐藏条件。第一优先选支持权重量化的模型家族。有些模型权重分布特殊INT4后精度崩得厉害量化后还得反复调校准集浪费时间。第二优先选社区维护活跃、官方发布过推理优化参考的版本这类模型对连续批处理和PD分离适配得更好。第三注意授权条款和依赖合规性内网私有部署还要考虑是否允许商用、是否需要额外声明。我踩过一次坑找了个非常冷门的“高分”模型部署后发现它只支持单卡FP16团队为了它改了半个月推理服务最后精度和速度都不满意。后来换回更主流的开源基座同样的优化代码直接跑通。给想要提升100倍的同行的建议是不要为了模型榜单的Top1去选基础设施支持很弱的模型性能优化的上限往往由生态决定。4. 先跑一个能复现的朴素基线4.1 最小推理脚本怎么写先别急着上优化框架把最朴素的脚本跑起来得到一组可信的baseline数据。下面这个脚本足够演示加载模型、逐token生成、统计时间。import time import torch from transformers import AutoModelForCausalLM, AutoTokenizer model_dir ./model_dir # 替换成你本地下载好的模型路径 tokenizer AutoTokenizer.from_pretrained(model_dir) model AutoModelForCausalLM.from_pretrained( model_dir, torch_dtypetorch.float16, device_mapcuda ) prompt 请写一段五百字的关于GPU推理优化的介绍。 inputs tokenizer(prompt, return_tensorspt).to(cuda) start time.time() with torch.no_grad(): outputs model.generate( **inputs, max_new_tokens512, use_cacheTrue ) end time.time() generated_tokens outputs.shape[1] - inputs.input_ids.shape[1] print(生成token数, generated_tokens) print(耗时, end - start, 秒) print(平均速度, generated_tokens / (end - start), token/s)这段代码没有做任何优化use_cacheTrue只是让模型复用历史KV这是最基础的功能不存在什么高级技巧。测的时候注意把GPU频率稳定下来最好先跑一个预热请求再正式计时否则第一次启动有CUDA初始化和权重加载开销数据会很难看。4.2 压测方法固定并发、固定输出长度单脚本的token/s只是基线之一真正要看的是在并发下的表现。你不需要商业级压测工具写个简单的并发脚本就能测出趋势。思路是固定输入prompt长度比如128个token固定生成长度比如512个token然后分别用1、8、32、64个并发请求去压记录每个请求的完成时间再汇总计算总吞吐。注意Python脚本本身的GIL会影响性能测试结果建议用多进程或带进程池的并发工具不要用多线程做重IO压测。压测时重点记录三个指标首token延迟用户感觉到“开始输出”的等待时间、单个token平均生成速度、系统总吞吐所有请求每秒产出的token总和。首token延迟是体验指标吞吐是容量指标后面调优时会发现这两者经常互相拉扯。4.3 基线数据的参考区间在一张显存带宽中等、单卡16GB的卡上跑70亿FP16模型无优化单并发时速度大约在30到50 token/s。如果把并发提高到16没有连续批处理时基本是排队等待总吞吐不会线性增长有时反而下降。这套“朴素基线”数据就是你未来所有优化的出发线。我建议直接把它写进测试文档并标注硬件型号、驱动版本、模型精度和上下文长度。任何一次版本调整、参数修改后都用同样的方式重测才是对优化效果负责的态度。我见过不少团队直接跳过基线一上来就开优化凭感觉说“变快了”。结果业务上线后复盘连原来多快都说不清更无法判断每一步优化是否真的有效。跑基线这件事真的不能省。5. 三个最值得动的优化刀量化、连续批处理、PD分离5.1 权重量化把“食材”切成小块再传菜量化是目前性价比最高的一步。大模型推理的权重读取瓶颈可以通过降低权重精度直接缓解。FP16到INT8每次读取的数据量减半FP16到INT4数据量减到四分之一。就像餐厅传菜原来一道菜需要一个大托盘现在把菜切碎装进小盘传菜通道的压力骤减。但量化不是简单把数字从16位截断成8位。成熟的做法是先拿一批有代表性的校准数据统计权重分布再决定每个张量的缩放因子和零点。业界主流的权重量化方案大差不差核心都是“让低位宽能保留尽量多的分布信息”。我自己实操时如果模型对精度敏感会先用INT8。INT8带来的速度提升明显精度损失通常可控INT4提升更多但需要认真跑一遍业务用例验证。千万不要只看PPL还要把典型prompt实际跑一遍观察是否有胡言乱语、跑题、数字前后不一致。量化模型一旦在这些细枝末节上翻车性能提升再大也白搭。5.2 连续批处理从“全班等最慢的”到“随上随下”这是第二关键的大招。传统静态批处理一个批次里的所有序列要一起完成其中一个请求生成长度特别长整批都要陪它等。GPU明明还有空位却因为“等最慢的同学”把算力闲置了。连续批处理把这套逻辑彻底改掉调度以token为粒度每个token生成完请求可以随时被移除新请求可以立刻插入空位。你要知道GPU上真正在跑的是“一次前向处理一堆token”哪一批token来自哪些请求并不重要。连续批处理就是让调度器贪心一点哪个请求的token准备好了就放进去哪个生成了结束符就赶下车。这也是为什么它能同时提高吞吐和降低排队延迟。开启连续批处理后最重要的参数是并发序列上限max_num_seqs。设置太低GPU吃不满设置太高KV Cache溢出直接OOM。一个保守的起步值是按显存估算法算出能容纳的总并发token数再除以平均生成长度得到可承载序列数先取计算值的60%到70%跑一轮压测再逐步上调。5.3 PD分离别让“配菜”和“炒菜”抢一口锅连续批处理解决的是调度粒度问题PD分离解决的是两类操作性质不同的问题。模型请求分两个阶段prefill阶段第一次处理整个prompt计算密集要一口气做大量矩阵乘法decode阶段逐token生成访存密集。如果这两个阶段混在同一个stream里一个超长prompt的prefill会突然吃掉大量算力让正在decode的众多请求全部卡住。PD分离的做法是把prefill和decode交给不同的处理单元或不同的队列。两张卡时可以一张卡专门负责prefill另一张卡负责decode中间由调度器转发。只有一张卡时也可以通过调度策略限制单次prefill的token数量避免单次长输入把整个并发周期打乱。在并发低、prompt短的场景PD分离的收益不太明显。但到了高并发、长prompt混合的场景它几乎是救命的。一个8K长度的prompt进来如果prefill一次算掉相当于同时挤占了其他几十个请求的算力分离后这个长请求会被拆成多次切片逐片送到GPU单次冲击窄了很多。5.4 多卡并行张量并行不是唯一答案先判断一个问题模型单卡能不能放下。能放下就尽量做数据并行每张卡独立跑一份模型各自处理不同请求扩展性最好。放不下才考虑张量并行把一层层的权重切到多张卡上每次计算前做通信同步。很多人一看到“多卡”就RAID式地做模型并行结果小模型反而越跑越慢。原因是张量并行每一步都涉及跨卡通信PCIe带宽不够时通信时间比计算时间还长。对大模型来说跨卡互联带宽是关键卡间互联速度高张量并行才划算。还有一种折中方案多副本流水并行。比如两张卡把prefill放在第一张卡decode放在第二张卡这其实就是PD分离加数据并行的混合体。跑通基础优化后可以按自己的业务比例去组合。我的建议是先从数据并行做起简单、稳定、容易排查问题跑通了再考虑更复杂的并行方式。6. 优化后重测如何把指标做扎实6.1 重测前必须锁死的变量优化做完最忌一上来就欢呼“快了一百倍”。先检查你这次压测的变量和基线是否完全一致并发数、prompt长度、生成长度、量化精度、max_num_seqs、上下文长度这些全都要一一对齐。只要有一个变了对比就不公平。测试次数也要够。大模型推理受到GPU频率、随机调度、显存碎片的影响单次测试波动可能超过20%。我的习惯是每种配置至少跑三轮每轮足够长到能覆盖几十个完整请求最后取中位数和P95而不是取最好的一次。P95尤其能暴露长短请求混压下的异常延迟。另外要注意冷热状态。模型服务刚启动时CUDA上下文、显存分配器、权重页缓存都还没暖起来这时候跑压测会让数字偏低。先发几个小请求预热让显存分配稳定后再开始统计。6.2 关键参数怎样调才能又稳又高我习惯按下面这张表来调参每一步只动一个变量参数影响我的推荐起点量化精度权重显存、带宽效率先用INT8稳定后试INT4max_num_seqs并发上限、KV Cache占用先按容量估值的60%max_model_len单条上下文上限、KV按业务最长prompt宽放20%显存预留比例避免OOM、给碎片留空间0.85到0.92之间prefill调度限制长prompt的冲击力单次prefill不超过4096 token确定max_num_seqs时先用显存预留比例算出可用显存减去权重显存再除以每token的KV Cache占用得到总并发token数预算。然后你希望每个请求平均生成多少token用总token数除以请求token长度就得到并发序列数参考值。比如一张24GB卡INT4权重占用4GB给模型预留20GB每token缓存0.13MB总token预算约15万token。如果平均每个请求占用8K token那么同时容纳20个序列比较安全。把max_num_seqs先设成12到14压测后如果GPU利用率还没有拉满再逐步加大。6.3 结果对比和我的翻车现场下面是一张我习惯整理的对比表可以直接套用在团队汇报里配置并发数总吞吐token/s单流速度token/sP95单请求耗时朴素基线FP16无调度8约1203522秒加INT8量化8约2604012秒加连续批处理32约2800429秒加PD分离64约8200447秒加双卡数据并行64约15000456秒这组数字只是参考不代表所有模型都能复现但它能清楚展示每一步优化的边际收益和瓶颈向哪里移动。到这一步“100倍”就变成了“64并发下8倍加并行后15倍加多卡后进一步放大最终接近100倍”的可验证结论。我自己在这个阶段翻过几次车。第一次是INT4量化后某个内部评测数据集上的准确率掉了5个点业务方直接拒收。后来换成混合策略敏感层保留INT8其余层降到INT4精度损失收窄到可接受范围。第二次是把max_num_seqs调太高压测跑到一半直接OOM整个服务崩溃。后来我就在显存预留里多加了一档安全兜底宁可用95%的显存预算也不做100%的豪赌。第三次是PD分离后GPU利用率看着很高但P95延迟反而上升原因是prefill队列的优先级设置不合理。把长prompt的prefill切碎后延迟才重新平稳下来。这些教训总结成一句话任何性能优化都是系统工程单点跑到极限不代表系统稳定要以端到端业务指标为准。我在自己的内部项目X里最后也没有全链路开到极限。先量化加连续批处理把吞吐拉起来确认业务无感后再逐步尝试PD分离和多卡并行。每一步都做独立压测、独立回滚这样即使某一环崩了也不会把整个服务搭进去。这也是我给所有想在私有部署上榨性能的团队的最真诚建议先求稳再求快先把“100倍”拆成可验证的乘法再一点点兑现它。下篇我会把每个参数的量化影响和更多案例数据展开继续聊那些压测时容易忽视的细节。