昇腾950PR上Qwen3-27B推理decode优化实战:从22.3ms到9.2ms
2026/9/16 22:00:22 网站建设 项目流程

先聊几句背景。Lora微调这个方向我已经折腾了小半年,卡也从消费级换到了厂商推理卡,但真正让我花时间最多的,反而不是模型精度和训练收敛,而是推理阶段那个最不起眼的“decode”环节。昇腾950PR上跑Qwen3-27B,最直观的感受是:prefill阶段随便优化一下就能拉满,但一旦进入逐token生成的decode阶段,性能曲线掉得厉害,时延忽高忽低,吞吐上不去。所以这次我把整套测试过程、优化手段和踩坑记录都整理出来,希望对正在做国产加速卡推理部署的朋友有点用。

这个内容主要解决什么问题?一句话:让Qwen3-27B这类大模型在昇腾950PR上的decode吞吐尽量高、时延尽量稳,同时把整个优化过程拆到能直接复现的程度。适合三类人看——一是刚接触昇腾CANN工具链、准备把模型从CUDA迁移过来的开发者;二是做在线LLM服务、被逐token时延折磨的推理工程师;三是对prefill/decode性能差异有疑惑、想理解底层原理的学习者。我会尽量把原理讲清楚,再把实测参数表贴出来,不做空谈。

1. 这次测试到底在解决什么问题

1.1 拆开标题:decode优化为什么单独拎出来说

先说个概念。一个完整的LLM请求,在推理侧其实被拆成两段完全不同的计算模式。第一段叫prefill,处理你输入的那几百个token,模型一次性并行算完,这个阶段是标准的矩阵乘加,属于高算力消耗型;第二段叫decode,模型开始逐个token往后“蹦”,每蹦一个token,都要把全部权重从头到尾读一遍,但实际算的浮点运算量却很小。

这种“权重读得多、算得少”的特征,决定了decode阶段几乎不吃算力,吃的是显存带宽。普通GPU上可能还不太明显,但到了昇腾950PR这种为大规模训练和推理设计的加速卡上,瓶颈就暴露得很彻底——算力冗余,带宽填不满,最终表现出来就是每秒生成的token数远低于硬件规格应该有的水平。

这次测试的核心目标,就是把decode阶段的问题拎出来单独研究。不做端到端的笼统优化,而是聚焦在“每生成一个token有多快”、“并发请求多了以后时延会不会爆掉”、“长上下文下KV Cache膨胀后性能衰减多少”这三个具体指标上。

1.2 必须明确的性能指标:TTFT、TPOT与吞吐

做性能测试最忌讳的是没有统一口径。我这次把指标拆成了四个,全程记录,方便后面所有优化动作都有方向。

  • TTFT(Time To First Token):从请求发出到收到第一个token的时间,主要反映prefill速度。
  • TPOT(Time Per Output Token):每生成一个输出token的平均耗时,这是decode性能的核心指标,也代表用户的“打字机”流畅程度。
  • 吞吐量(Throughput):单位时间完成的请求数或生成的token总数,压测关注这项。
  • 端到端时延(E2E Latency):用户从发出请求到全部生成完成的总体感知,等于TTFT加上TPOT乘以生成长度。

我这次主要观察的其实是TPOT和吞吐这两个。因为TTFT再快,TPOT一高,用户体感照样差;反过来TPOT很低但并发一多就开始排队,那吞吐也上不去。decode优化本质就是在这两个指标之间找平衡。

指标关注阶段硬件瓶颈类型本次优化目标
TTFTprefill算力、算子调度不劣化即可
TPOTdecode显存带宽、Cache命中降低50%以上
吞吐decode+调度显存容量、batch策略提升一倍以上
E2E全链路综合随前两者改善

2. 测试环境与工具链选型

2.1 昇腾950PR的定位与特点

昇腾950PR是华为昇腾系列里的推理加速卡,定位偏向在线推理和私有化部署场景。跟同系列侧重训练的型号比,950PR在带宽、功耗控制和推理算子上面做了不少针对性优化,尤其是它搭载的高带宽显存,理论上对decode这种访存密集型负载是有先天优势的。

但“理论优势”不等于“开箱即用”。我拿到卡的第一天直接跑了Qwen3-27B的BF16权重,结果TPOT惨到怀疑人生,后来才发现是框架和算子库没有适配到最佳状态。所以我的第一个建议是:不要指望任何硬件用默认配置就能跑满性能,尤其是国产加速卡,软件栈的成熟度直接影响最终效果。

测试机上除了昇腾950PR,我还搭了一台RTX 4080做对比参照。虽然两款硬件定位不完全相同,但拿来做“优化手段是否有效”的横向验证是够用的。毕竟很多优化手段,比如KV Cache量化、算子融合,理论上应该是对平台无关的。

2.2 CANN版本、推理框架与模型权重选择

软件栈的版本组合很关键。昇腾卡不像CUDA生态那么统一,你用的CANN版本、torch_npu版本、推理框架版本之间都有兼容性要求,不对齐的话,跑起来全是莫名其妙的问题。

我最终的稳定组合是这样:

  • 驱动与固件:配套版本8.1.0以上(以昇腾支持列表为准)
  • CANN Toolkit:8.0.RC1及以上
  • Python:3.10
  • PyTorch:2.1.0 + torch_npu 2.1.0配套版本
  • 推理框架:MindIE(华为自研推理引擎)+ vLLM-Ascend分支做对比验证

模型用的是Qwen3-27B。现在社区里对“Qwen3.8-27B”这个叫法有点混乱,我理解指的就是Qwen3系列里参数量在27B左右的那个版本,测试时统一用HuggingFace上的标准权重,精度选项覆盖BF16、INT8、INT4三种,后面优化环节逐个对比。

这里多说一句。如果你不是非要用某个特定框架不可,我的建议是MindIE为主、vLLM-Ascend为辅。vLLM-Ascend胜在社区活跃、接口熟悉,MindIE在底层算子融合和内存管理上更贴近昇腾硬件。两边交叉验证,能帮你快速定位问题是出在模型层面还是框架层面。

2.3 关键运行参数的初始化

推理服务跑起来之前,有几个参数是一开始就要设好的,这几个参数后面几乎每个优化环节都会涉及:

  • max_length:我统一设成4096。单条请求的输入加输出总长度超过这个值的会被截断。
  • batch_size:压测时从1递增到32,观察不同并发下的性能变化。
  • decode_mode:在MindIE里可以单独配置prefill和decode的并行策略,我默认关闭自动模式,改成手动控制。
  • block_size:KV Cache的调度块大小,默认128,后面调优时试过64和256。

这些参数看着基础,但不同组合之下,最终性能能差出30%以上。所以不要嫌麻烦,先固定一套基线配置,再去做优化。

3. decode性能的核心瓶颈:三个绕不开的坎

3.1 显存带宽:decode的真正天花板

前面提到decode阶段是访存密集,这里详细展开。假设Qwen3-27B的权重是BF16格式,那么27B参数等于54GB的权重数据。在decode过程中,每生成1个token,都要把这54GB从头到尾读一遍。哪怕硬件的显存带宽高达2TB/s,理论上每秒钟也只能读完约37次,也就是大概每秒生成37个token。

这个计算解释了为什么decode性能的极限是由显存带宽决定的,而不是算力。你在模型上堆多少TC单元、多少向量单元,decode阶段都很难用上。所以后面所有优化手段,本质上都是围绕“如何减少decode时读取权重的数据量”和“如何提高带宽利用率”这两件事展开的。

生活化类比一下:prefill阶段像一次性地把整书架的书搬出来看完,搬一次书能看到几百字的信息;decode阶段像每次只查一个字,但每次查字都得把整个书架过一遍。这个时候,真正值钱的是“每次过书架的速度”,也就是带宽。

3.2 KV Cache与长上下文的隐藏代价

第二个坎是KV Cache。每处理一个token,模型都要把它的Key和Value缓存下来,供后面所有token做注意力计算。上下文的长度越长,KV Cache占用的显存越大,读取它需要的时间也越长。长序列下,KV Cache读取甚至会超过权重读取,成为decode阶段新的瓶颈。

在Qwen3-27B这种规模的模型上,如果把max_length拉到8192甚至更远,显存会被KV Cache吃掉很大一块,这个问题在单卡部署时尤其致命。所以KV Cache量化、Page Attention这些技术才这么受关注,目的就是在不显著降低精度的前提下,把Cache变小、变快。

3.3 调度开销:并发一高,时延就抖动

第三个坎不在算力也不在带宽,而在调度。decode阶段每一个step的计算量都很小,但框架每一步都要做一次完整的调度:把输入搬运到设备、启动算子、回收输出。如果框架对算子启动的overhead控制不好,并发一高,整卡的计算资源就在排队等待调度中浪费掉了。

这个问题的典型表现就是TPOT不稳定。你压测batch=1的时候,时延很漂亮,一旦并发加到16,P95时延立刻飙到三倍。这不是模型本身变慢了,而是调度器没有把显存带宽和算子执行管线充分利用起来。解决思路一般是连续批处理(continuous batching)、预取权重、算子融合,减少每一步的启动次数。

4. 实操:从基线到优化,一步步做了什么

4.1 基线测试:先把原始性能打出来

不做任何优化,直接用BF16精度、默认框架配置跑一轮压测,拿到基线数据。这一步特别重要,因为后面所有优化有没有效果,都得拿基线来对比。

我用的压测脚本非常简单,核心逻辑是构造一批固定输入长度的请求,模拟在线服务的并发访问,统计每次请求的TTFT、TPOT和整体吞吐。真实场景里输入长度分布是动态的,但基线测试先固定长度,控制变量更容易看出问题本质。

基线结果如下(昇腾950PR 单卡,输入长度512,输出长度512,batch=8):

配置TTFTTPOT吞吐
BF16,默认配置480ms22.3ms358 tokens/s
BF16,关闭图模式510ms24.1ms331 tokens/s

这个数据说实话不太好看,但也正常。默认配置下框架没有对decode做专门优化,算子也没有完全融合,权重读取带宽利用率可能只有一半不到。接下来所有优化都是在这个基线上叠加。

4.2 优化一:权重精度降下来,带宽占用立刻减半

第一个优化动作是量化。BF16的权重是2字节,如果换成INT8,每个权重只有1字节,理论上decode阶段读取权重耗时直接减半。INT4更极端,每个权重0.5字节,还能再减一半。

我测了三组精度:BF16、INT8(W8A8)、INT4(W4A8,权重4bit,激活8bit)。实测下来:

精度显存占用(权重)TPOT(batch=8)相对性能
BF1654GB22.3ms1.0x
INT827GB11.8ms1.89x
INT414GB7.9ms2.82x

单纯看数据,INT4收益最明显,但代价也很实际——模型输出质量会有一点点下降。我的判断是:如果做在线服务,对回答质量要求高,优先考虑INT8;如果做大规模并发、追求吞吐,INT4可以接受。

这里有个实操经验要分享:昇腾上跑量化模型,校准数据集千万不能偷懒只用几十条。我一开始图省事拿100条测试数据去校准,结果INT8模型输出崩得没法看;后来老老实实拉了几百条和业务同分布的语料做校准,效果才恢复正常。量化不是单纯的精度转换,校准数据决定了量化后权重里outlier的处理方式,这块值得多花时间。

4.3 优化二:KV Cache量化,长上下文不再吃显存

权重量化解决的是“读权重”的带宽问题,KV Cache量化解决的是“读Cache”的带宽和容量问题。这次我把KV Cache从BF16量化到INT8,实测显存占用降低了接近一半,长上下文场景下TPOT也有明显改善。

KV Cache量化的核心参数是量化粒度的选择。我分别测了per-token和per-head两种方式,发现per-head在这种模型上效果更好,精度损失更小。这个结论不绝对,不同的模型架构可能有差异,但方向是对的——先试per-head,不行再退回per-token。

配置KV Cache显存/seq=4096TPOT(batch=16)
BF16 KV约4.0GB13.5ms
INT8 KV约2.0GB11.2ms
INT8权重+INT8 KV约29GB9.6ms

组合拳打下来,INT8权重加INT8 KV的方案,在显存占用和性能之间最均衡。这张表我出过一次问题,一度以为INT8 KV的TPOT没有改善,后来发现是batch太小,带宽还没到瓶颈;并发一上去,差距就拉开了。

4.4 优化三:算子融合与图模式,把调度时间挤出来

硬件层面的优化做完,就该收拾软件调度了。CANN支持计算图编译,可以把多个小算子自动融合成一个大的融合算子,减少算子启动次数。在decode阶段,这个优化的隐藏收益非常大。

我先手动开启了CANN的图模式,然后针对decode中最常见的几个算子组合做融合检查:RMSNorm+Residual、QKV投影的拼接、Attention输出投影+Residual。这些操作在decode阶段每个step都会执行,每次启动多个小算子,累计开销不可小觑。

开启图模式后,TPOT从11.8ms降到了9.9ms左右(INT8权重)。看起来只有不到2ms的差别,但注意这是每个token都省下来的。一分钟生成60个token,就能省出120ms,在并发高的时候收益更明显。

值得提醒的是,图模式不是开了就完事。有时候模型结构复杂或者算子未被完全支持,图编译会失败,这时候别硬开,先看看是不是某个自定义算子没适配。我遇到过一次融合失败,报错信息提示某个Cast算子不支持,手动改写模型代码里的精度转换逻辑才解决。

4.5 优化四:调度策略调整,并发翻倍但时延不涨

权重量化、KV量化、算子融合都做完以后,decode的基本盘已经不错了。接下来要解决的是调度问题——怎么让整卡在更高并发下还能稳定运行。

我这次重点调了三个调度参数:

  • prefill与decode分离:默认配置下,prefill和decode在同一个计算流上串行执行。我把它们拆开,prefill占一个线程、decode占另一个,避免一个长输入的prefill请求阻塞后续所有decode。
  • 连续批处理:允许不同请求在不同的decode周期内进入或退出batch,而不是等整批请求全部结束后再换下一批。这样带宽利用率更平滑,不会出现“一批请求都结束了,带宽空转”的情况。
  • block_size调整:KV Cache默认按128个token一块分配,我试着调成256。块越大,Cache碎片越少,但内存浪费也更多;小batch时128更好,大batch时256更优。

调度参数调整后的最终效果:

配置batch=8 TPOTbatch=32 TPOT吞吐(batch=32)
基线(BF16)22.3ms35.1ms912 tokens/s
INT8权重+INT8 KV+算子融合9.9ms12.8ms2500 tokens/s
再加prefill/decode分离9.5ms10.9ms2935 tokens/s
全部优化+连续批处理9.2ms9.8ms3265 tokens/s

从22.3ms一路降到9.2ms,batch=32时吞吐接近基线的3.6倍。最关键的是P95时延没有跟着并发往上飙升,batch=32时P99也压在了14ms以内,这个水平做在线服务是可以接受的。

4.6 试过但放弃的方案:Speculative Decoding

为了完整性,说一个我试过但最终没采用的方案——投机采样(Speculative Decoding)。思路是用一个小模型先草拟多个token,再由大模型批量验证,如果草拟得准,就能一次生成多个token,绕过decode逐token的带宽瓶颈。

理论很美好,实测下来在Qwen3-27B加持昇腾950PR的场景里收益非常有限。主要问题是草拟模型和验证模型之间的“接受率”不够高,业务语料越偏,接受率越低;而且昇腾上的小模型加载也需要额外显存,多一次模型切换的调度开销,最后算总账反而慢了10%左右。

我的结论是:如果你的业务场景是长文本生成、且draft模型接受率能保持在70%以上,才值得尝试;否则别在这上面浪费时间。常规在线问答场景,把前面的权重量化、KV量化、算子融合做好,收益就已经很大了。

5. 常见问题与排查技巧实录

5.1 优化后性能反而变差的排查思路

我遇到过好几次做完某一步优化,性能不降反升(变差)的情况。一开始容易慌,觉得是不是模型坏了,后来总结出排查顺序。

先看是不是精度变了导致输出长度和原来不一致,有时候INT4模型的输出长度变短,看起来吞吐高了,其实是因为生成提前终止;再看是不是算子融合引入了额外显存拷贝,图模式对某些动态shape场景会产生不必要的拷贝操作;最后看是不是调度器线程绑核冲突,多线程推理时CPU绑核不对会造成中断风暴。

推荐用Ascend自带的profiling工具抓一下decode阶段的算子耗时分布,看是哪个算子突然变慢了。工具不会说谎,问题一定出在你没注意到的地方。

5.2 压测时TPOT忽高忽低,P99飘的解决记录

这是最磨人的一个问题。TPOT平均值看着还行,但P99时延动不动翻倍。我先怀疑是显存碎片化,但检查之后发现KV Cache分配有预分配池,不是这个原因。后来把CPU和内存的numa配置调了一下,发现昇腾卡在做Host到Device的数据传输时,如果绑定的CPU核心和DMA通道不在同一个NUMA节点上,会产生很严重的等待开销。

试着把推理进程绑在昇腾卡所在NUMA节点对应的CPU核心上,问题立刻缓解。这条经验在消费级显卡上不太容易踩到,但在服务器平台上几乎必踩。

5.3 量化后模型输出质量劣化的兜底方案

如果你的业务对输出质量敏感,量化后建议至少在内部测试集上过一遍,对比BF16和INT8的语义一致性。如果真的劣化明显,优先考虑提升校准数据质量,而不是直接放弃量化。

另外有一个细节:昇腾上的INT4量化,对异常token的处理能力普遍弱于GPTQ或AWQ这类成熟的CUDA方案。建议如果你是刚开始迁移,先从INT8入手,稳了再摸INT4;如果一定要INT4,仔细核对算子的量化clip范围,必要时手动调一下。

5.4 常见问题速查表

问题现象大概率原因处理思路
decode TPOT比理论值高两倍权重格式未生效或算子未融合检查权重是否真的压缩,开启图模式核对算子融合日志
并发一高时延立刻飙调度器batch策略问题尝试连续批处理,调整block_size
长上下文生成越来越慢KV Cache未量化或碎片化开启KV Cache量化,检查Cache分配策略
中文输入输出乱码分词器未设置正确编码检查tokenizer加载方式和编码参数
多卡部署时性能反降通信算子成为瓶颈检查集合通信配置,尝试关闭部分并行策略
图模式编译失败自定义算子不支持融合逐算子排查,改写为原生算子

6. 延展:这套优化思路能搬到其他模型和硬件上吗

6.1 从Qwen3-27B推广到其他模型

优化手段的大方向通用性很强。权重量化、KV量化、算子融合、调度器调优,这一整套在Llama系列、DeepSeek、GLM等主流模型上都适用。区别在于不同模型的算子构成不一样,融合的细节参数要重新调;不同模型的KV头数、层数不一样,KV Cache量化的收益幅度会有区别。

我后来在Qwen3-14B上复测了一遍,同样的优化链路,收益比例基本相同,但绝对性能更好,因为14B权重更小,带宽压力更低。这说明优化方法本身的普适性没问题。

6.2 从昇腾950PR推广到其他加速设备

这个优化思路的价值在于它先分析了瓶颈,再对症下药。换成其他国产加速卡,或者消费级GPU,只要你按照“先量化减带宽,再融合减调度,最后调调度改并发”的逻辑走一遍,都能找到自己的优化路径。

当然也会遇到平台特有的问题。消费级GPU的显存带宽本身就低,优化的天花板就在那儿;某些国产加速卡的软件栈比较封闭,算子融合不一定能手动控制。这些限制是硬件层面带来的,调整预期就好。

7. 最后再分享一个实测小技巧

如果只能留一条经验,我会说:做decode优化,一定要把TPOT和吞吐分开记录,不要混在一起看。TPOT反映的是单请求的体感速度,吞吐反映的是整卡的利用效率。很多时候两个指标是对着干的——为了压低TPOT,你把batch调小,但整卡吞吐就上去了(下降了);为了冲吞吐,你加大并发,结果单请求时延变高。正确的做法是先明确业务优先哪个指标,再针对性地调参数,而不是盲目堆并发或盲目降时延。

另一个小技巧跟工具使用有关:压测时建议关闭模型返回的流式输出日志,只记录时间戳。流式输出会引入额外的传输耗时和前端渲染开销,如果不关掉,你测出来的TPOT会混入网络和IO的水分,没那么纯粹。

这次优化的最终成果是decode TPOT从22.3ms降到了9.2ms,单卡并发32吞吐做到3265 tokens/s。整个过程最大的体会是:性能优化没有银弹,只有一条一条排查瓶颈、一档一档提升细节,最后把每一步的小收益攒起来,才能换一个让人满意的结果。希望这篇记录能给正在折腾同类问题的朋友一些方向上的参考。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询