给昇腾做性能摸底这件事,我这两年断断续续做了不少次。每次换模型、换CANN版本、换单卡和多卡,第一件事都是先跑一把Benchmark,把“这块芯片在当前配置下到底能跑多快”这个底数摸清楚。昇腾的Benchmark基准测试工具,本质上就是一把量芯片性能的“标尺”。这篇文章就把我在实际项目里怎么选工具、怎么配参数、怎么看结果、怎么避坑的经验完整梳理一遍,适合刚接触昇腾性能调优的开发者,也适合正在做模型迁移或板卡选型的朋友参考。
我一直觉得,基准测试这件事看起来简单,真正跑起来全是细节。同样的ResNet50,换一个batch size,结果能差出两倍;同样的LLM推理脚本,跑在310P3和910B上,精度格式选FP16还是BF16,吞吐量完全不是一个量级。所以这篇文章不只是罗列命令,而是把每一个关键选择背后的原因讲清楚,把我踩过的坑也一并写出来,省得你再走一遍弯路。
1. 先搞清楚:Benchmark到底在“量”什么
1.1 为什么需要一把“标尺”
做昇腾相关的性能工作,绕不开一个灵魂拷问:这块NPU到底能跑多快?很多人在刚开始接触昇腾的时候,会习惯性地拿GPU的经验套上去,比如直接看显存大小、看核数,或者跑一个网上随手抄来的脚本,拿了个数字就当结论。这样做很容易翻车,因为NPU的架构特性和GPU并不完全一样,算力峰值、带宽表现、算子调度方式差异很大,单看某一个指标根本说明不了问题。
Benchmark基准测试的价值,就是给性能一个可复现、可对比的衡量标准。它解决的问题很具体:同一份模型代码,在昇腾设备上跑一次,拿到一批能落地的数字——比如吞吐量、单次延迟、显存占用、算力利用率。有了这些数字,你才能回答三个最关键的问题:这个模型在当前硬件上能不能跑得动?能跑到什么程度?如果要优化,瓶颈在哪?
我见过不少团队在昇腾上做模型迁移,第一步不是跑基准测试,而是直接闷头优化算子。结果优化了半天,发现其实是数据加载卡住了,NPU一直在等数据,算力利用率只有百分之十几。如果一开始就跑了Benchmark,把吞吐量和利用率两个数字摆出来,瓶颈一眼就能看出来,根本不用瞎猜。这就是“标尺”的意义——先把量级量出来,再谈优化。
1.2 昇腾场景下的Benchmark都测些什么
昇腾平台的Benchmark,通常分训练和推理两大场景,测的指标侧重点不一样。
训练场景下,最关心的是吞吐量(Throughput),也就是每秒能处理多少个样本,或者每秒能跑多少个Token。对CV模型来说就是images/s,对NLP或大模型来说就是Tokens/s。这个数字直接决定了训练一个模型要花多少时间,也决定了数据并行、流水线并行时卡间通信压力是否合理。训练Benchmark一般还会关注稳态功耗、显存占用和算力利用率,特别是跑大模型的时候,显存能不能塞下、带宽够不够,都直接体现在这几个数字里。
推理场景下,除了吞吐量,更关心延迟(Latency)和端到端首Token时间(TTFT)。比如一个在线推理服务,用户请求进来,多少毫秒能返回结果,这个指标是业务方最敏感的。推理Benchmark还要区分单请求延迟和批量吞吐,因为这两个数字在NPU上往往是跷跷板关系——batch size越大,单次请求延迟越高,但整体吞吐也越高。所以测试结果必须说清楚是在什么batch size下测的,否则数字完全没法比较。
另外,昇腾场景下还会关注精度格式的选择。比如310P3在使用FP16还是BF16做推理时,性能和精度表现差异很大,这个我会在后面专门展开讲。Benchmark的作用,就是把这些维度的数据都量化出来,形成一张可对比的性能画像。
2. 工具怎么选:官方工具、ModelZoo脚本与社区神器
2.1 先看官方家族:ModelZoo与MindSpeed
昇腾生态里最正统的Benchmark来源,是官方维护的ModelZoo模型仓库。这个仓库里的每个模型都配套了训练和推理脚本,很多脚本本身就内置了性能统计功能,跑完会直接打印出吞吐量、显存占用等信息。我拿ResNet50做过验证,ModelZoo的脚本在昇腾910B上跑出来的数字,和官方发布的白皮书数据基本能对上,说明脚本的统计逻辑是靠谱的。
大模型训练场景下,MindSpeed是绕不开的工具。它是昇腾专门做的大模型加速库,内部实现了张量并行、流水线并行、序列并行等一系列并行策略,同时也带了一套性能测试脚本。用MindSpeed跑一遍GPT类模型的pre-train脚本,你能直接拿到不同并行配置下的吞吐量数据,这对做大规模训练前的可行性评估非常有用。我建议做LLM训练的团队,把MindSpeed的performance脚本当作标配,每次改动并行配置或者优化算子之后,都跑一遍作为回归测试。
2.2 推理场景专业工具:ais_bench与msame
推理Benchmark的工具相对更专一些。msame是早期用得比较多的离线推理工具,用法是先通过ATC工具把模型转换成昇腾的离线模型OM格式,然后调用msame进行推理并统计时延。它的特点是简单直接,一条命令跑完,输出的数据很清晰,适合验证单个模型的推理性能。
但msame有个问题,它是基于acl的顶层封装,很多底层的性能细节看不出来。这两年我更推荐用ais_bench,这是昇腾社区里维护得比较活跃的推理基准测试工具。ais_bench的优势在于支持更细粒度的性能统计,包括模型加载时间、推理单次时延、吞吐量、设备利用率等,而且支持动态shape,对NLP模型特别友好——现在很多模型输入长度是可变的,msame对动态shape的支持比较弱,ais_bench就好很多。
举个例子,我在测一个BERT分类模型时,需要在真实业务下按照实际请求的token长度分布去压测,msame只能按固定shape跑,测出来的数据根本没有参考价值。换成ais_bench之后,能把动态seq_len场景下的P95时延、吞吐都测出来,这个数据才能反馈给服务端做容量规划。如果你做的是推理服务性能评估,我建议直接上手ais_bench。
2.3 深度分析工具:msprof与npu-smi
Benchmark光拿到一个总耗时是不够的,还得知道时间花在哪里。这时候就需要用到profiling工具。昇腾的官方性能分析工具是msprof,它能把NPU上每个算子的执行时间、AI Core的利用率、内存读写带宽等信息统计出来。跑一次profiling,你会看到一份算子级别的耗时排行,哪些算子是大头,哪些算子有空闲等待,一目了然。
npu-smi则更像是一个“随时开着的心电图”,它负责查看NPU的实时状态,包括芯片温度、功耗、显存使用率、AI Core利用率等。在跑Benchmark的时候,我会并排开三个终端:一个跑测试脚本,一个用npu-smi info实时看利用率,一个用msprof抓算子详情。这三样东西配合起来,才能把一次Benchmark从“知道一个结果”升级到“理解一次结果”。
很多人在Benchmark时只看最终打印的耗时,然后就开始调参,这是不够的。比如总耗时没变,但npu-smi显示AI Core利用率只有30%,那说明瓶颈根本不在计算,而在数据搬运或算子等待;如果AI Core利用率已经95%以上,那说明计算本身已经接近饱和,再调参空间也很有限。这些判断都依赖实时观测数据,不是靠猜。
3. 实战流程:从零跑通一个昇腾Benchmark任务
3.1 环境准备与版本匹配
任何Benchmark的第一步,都是先把环境检查好。昇腾这套体系,版本匹配问题非常敏感,CANN版本、固件版本、驱动版本、PyTorch适配版本之间必须严格对应。我统计过自己踩过的坑,至少有一半是版本不匹配导致的玄学报错,比如算子编译失败、设备ID找不到、莫名其妙的卡死。
环境准备阶段我建议按顺序做三件事。第一,用npu-smi info确认设备状态正常,能看到芯片型号和显存信息。注意昇腾设备不像GPU那样直接叫显存,它叫HBM,但管理理念类似。第二,检查CANN环境变量是否生效,关键看toolkit路径下的set_env.sh有没有正确source,通常安装CANN之后需要在~/.bashrc里追加source /usr/local/Ascend/ascend-toolkit/set_env.sh。第三,确认PyTorch的昇腾适配版本torch_npu和CANN版本能对上,官方文档里有对应关系表,照着表核对就行。
版本检查这一步,我踩过的坑是:在310P3机器上用了适配910B的torch_npu版本,结果一跑就报算子不支持的错。后来把torch_npu降到对应版本,问题马上消失。所以提醒大家,昇腾的版本对应关系表一定要看,别偷懒。
环境变量方面,有几个常用的需要知道。ASCEND_DEVICE_ID指定用哪张卡,跑多卡时要分别设置。ASCEND_GLOBAL_LOG_LEVEL控制日志级别,默认是1,出问题排查时我会临时改成0看完整日志,但正常跑Benchmark时一定要改回1,否则日志量大到能拖慢程序。还有个环境变量ASCEND_SLOG_PRINT_TO_STDOUT,打开后日志会输出到终端,排查问题时很有用。
3.2 模型准备与数据集预热
Benchmark的模型准备,通常有两种玩法:一种是直接跑训练脚本或推理脚本,一边跑一边统计;另一种是先把模型转成OM离线格式,再用推理工具测。前者灵活,适合训练场景;后者稳定,适合推理性能验证。
先说训练场景。以MindSpore或PyTorch的ResNet50为例,训练脚本本身就有打印吞吐量的逻辑,关键是选择合适的数据集和迭代次数。数据集我建议先用合成的dummy data,也就是不加载真实图片,直接生成随机数据喂给模型。这样做的好处是排除数据读取的干扰,专门测计算性能。真实业务场景里数据加载通常用多进程DataLoader预取,但Benchmark阶段我就是要看看纯计算能到什么程度,所以dummy data是首选。
迭代次数的设置也很有讲究。很多新手直接跑1000步就打印最终平均数据,结果数字忽高忽低,而且偏低。原因是前几十步存在warmup过程:算子首次执行要做kernel编译、缓存填充、显存分配,这些都会拖慢速度。我通常的做法是前100步作为warmup,不做统计,从第101步开始计时,至少统计500步以上,然后取平均值。这个数字才接近稳态性能。
推理场景的话,如果用msame或ais_bench,模型要先通过ATC工具转换。ATC转换时除了模型路径,还有几个关键参数:framework指定原始模型格式,ONNX是5;soc_version指定芯片型号,310P3就填Ascend310P3;input_format指定输入格式,NCHW或ND。转换成功后生成一个om文件,这才是能跑推理的格式。整个转换过程可能遇到算子不支持的情况,那是另一个故事了,后面排查部分会说。
3.3 关键参数解析与一次真实跑测
把环境、模型都准备好后,就可以开始正式跑测了。我拿一个实际做过的CV分类模型推理Benchmark为例,完整跑一遍流程。
首先用ATC把ONNX模型转成OM,命令大致是这样:
atc --model=resnet50.onnx \ --framework=5 \ --output=resnet50_310p3_fp16 \ --soc_version=Ascend310P3 \ --input_format=NCHW \ --input_shape="input:1,3,224,224" \ --precision_mode=allow_mix_precision这里有两个值得说明的点。第一,input_shape我固定成1,3,224,224,这是batch size=1的静态shape。如果是动态shape场景,要用input_shape_range参数去定义范围。第二,precision_mode我设置了allow_mix_precision,这会让ATC自动把部分算子降到FP16执行,以提升性能。310P3上FP16是主流精度,BF16的支持情况要具体算子具体分析,后面我会单独说。
转完OM后,用ais_bench跑推理,命令大致是这样:
ais_bench --model=resnet50_310p3_fp16.om \ --batch_size=1 \ --iterations=1000 \ --warmup=50--iterations是总共跑多少轮,--warmup是前多少轮不计入统计。我习惯把warmup设成50,iterations设成500以上,数据比较稳定。
跑完会得到一组输出,核心数字包括模型加载时间、单次推理时延、每秒钟处理的图片数(FPS)、设备利用率。比如我当时跑的结果大致是:单次时延1.8ms,FPS约520,利用率60%左右。这个组合说明计算还没到瓶颈,还有优化空间。后来我把batch_size提到8,FPS变成1800左右,利用率涨到85%,说明批量推理对提高吞吐非常有效。
训练场景的话,我用MindSpeed跑过一个GPT-2规模的模型Benchmark,核心关注的是吞吐量Tokens/s和显存峰值。跑的时候会加--recompute-granularity之类的参数,尽量让显存和算力都利用起来。MindSpeed脚本跑完会把每一步的耗时和Tokens/s都打出来,我取稳态阶段的平均值作为最终结论。
4. 看懂结果:精度、吞吐、延迟与资源利用率的真实意义
4.1 310P3的精度选择:FP16、BF16还是混合精度
昇腾310P3在推理场景下,精度格式的选择对性能影响极大。很多人问310P3使用什么精度合适,我的实践结论是:主流场景用FP16或混合精度,BF16在部分算子上的支持还不够全面,如果要整体走BF16,需要先做算子兼容性验证。
为什么这么说?310P3的AI Core对FP16的计算效率远高于FP32,大部分卷积、矩阵乘算子在FP16下能跑出接近峰值的性能。而BF16的优势在于动态范围和FP32一致,不容易溢出,但310P3上BF16的算子覆盖度不如FP16完整,有些算子遇到BF16输入甚至会回退到FP32,性能直接掉一截。所以我通常在ATC转换时用allow_mix_precision,让工具自动选择最优的降精度策略,这样既照顾了性能,也兼顾了精度损失。
在LLM推理场景里,精度问题会更敏感。大模型的权重动辄几十亿参数,如果全部用FP16,权重范围大的层可能出现溢出;如果全部用FP32,显存又扛不住。这时候最稳妥的方案是混合精度:敏感层(如LayerNorm、Softmax)保持FP32,矩阵计算层走FP16。昇腾的ATC和推理引擎对这类混合精度策略支持得比较成熟,实测下来精度损失控制在可接受范围内。
我在一次LLM推理Benchmark里做过对比:纯FP16推理的吞吐量比混合精度高约15%,但生成文本的困惑度略有上升;混合精度模式吞吐量只低一点,输出质量更稳。所以如果你做的是对输出质量敏感的业务,别一味追求最高吞吐,建议跑一版混合精度的对比测试再下结论。
4.2 吞吐量高不代表一切:延迟和资源利用率要一起看
很多人拿到Benchmark结果,只看FPS高不高,这其实是个误区。吞吐量高,可能只是因为batch size开得大,单次请求的延迟已经高到业务不可接受。所以我把Benchmark结果拆成三个维度看:吞吐量(Throughput)、延迟(Latency)和资源利用率(Utilization),三者必须放在一起分析。
延迟这个维度,要区分平均延迟和P95/P99延迟。平均延迟低,不代表用户体验好,因为总有少量请求特别慢,可能是排队等了很久,也可能是动态shape下某个输入特别长。我会在ais_bench里打开延迟分布的统计,把P95和P99数字单独记下来。如果P99比P50高出好几倍,说明系统里有明显的抖动源,需要排查是不是显存碎片、调度抖动或者某个算子在边界shape下耗时突增。
资源利用率这个维度,主要看AI Core利用率和HBM带宽。在推理场景,AI Core利用率低于50%很常见,因为推理请求可能稀疏,或者图中有大量标量操作和同步等待。AIN Core利用率低本身不一定是坏事,但如果你要提高吞吐,就得知道瓶颈在哪:是算力没用满,还是带宽限制了,还是算子切分粒度太粗导致流水线不饱和。msprof给出的算子流水图能清楚看到每个AI Core在哪些时间段是空闲的,这比看总耗时有用得多。
有一个经验可以分享:看到“显存占用高”不要慌,要区分是模型权重占的显存、激活值占的显存,还是框架缓存占的显存。在跑大模型推理时,有时候显存已经占了80%,但AI Core利用率才30%,说明权重和KV cache占了太多显存,推理效率并不高。这时候要不要换精度格式降低权重内存,要不要启用显存复用,都要结合这组数据来判断。
4.3 热词“昇腾960”带来的新思考
最近工程群里经常有人聊昇腾960,讨论新芯片在Benchmark上应该怎么测。我的看法是,不管硬件怎么迭代,“标尺”的逻辑不变:先定场景,再选工具,最后把吞吐、延迟、资源利用率三个维度跑齐,用同一套方法回归测试。芯片性能提升了,Benchmark的数据自然会说话。
但新芯片往往伴随新的算子库和新的CANN版本,这意味着老版本环境下测出的数据,在升级之后不一定还能复现。我建议团队在芯片或CANN升级后,专门抽时间把历史Benchmark脚本全部回归一遍,把新旧数据做对比,这样才能判断性能提升到底来自硬件还是来自软件优化,也能及时发现某些算子在新版本下性能回退的问题。
5. 常见问题排查与踩坑实录
5.1 测试结果忽高忽低怎么定位
跑Benchmark最容易遇到的现象,就是同一条命令连续跑几次,FPS数字差出一大截。这种情况通常有三个原因:系统里其他进程抢占资源、未做充分warmup、数据加载抖动。
排查思路我一般是这样的:先看npu-smi info确认NPU和HBM没有被其他任务占用,再看CPU和内存的使用情况,用top或npu-smi combo都能看到。如果系统很干净,那就把warmup步数加大,比如从50提到200,再看结果稳定性。数据加载抖动的问题,在训练场景更明显,解决办法是改用dummy data,或者确保DataLoader的prefetch足够大。
还有一个容易被忽略的因素是电源和散热。NPU在长时间高负载下如果温度升高,频率会下降,导致性能回落。我跑长时间Benchmark时,会在测试前先让设备空跑几分钟预热到稳定温度,然后再开始计时,这样拿到的数据更贴近真实长时间运行的表现。
5.2 OOM与显存不足的常见解法
昇腾设备上OOM报错也很常见,尤其是跑大模型或在动态shape场景下。有个坑要特别注意:静态shape模式下,NPU会按照你设定的最大shape预留显存,所以就算你只输入一个很小的tensor,显存也可能已经占掉很多。应对方法是尽量让shape贴近真实业务分布,而不是无脑设一个很大的上限。
动态shape场景下,显存使用会更复杂。因为框架需要为不同shape预留切换空间,频繁变化shape还可能导致内存碎片累积,跑着跑着就OOM了。我遇到过一次很诡异的OOM,重启进程后就恢复了,但跑几个小时又会复现。后来定位发现是动态shape切换次数太多导致显存碎片化。这个问题的解决思路是限制shape档位,把动态范围切成分散的几档,而不是完全连续变化,能显著减少碎片。
还有一点,不要在环境下同时跑多个CANN程序却不清理缓存。CANN有内存池和算子缓存机制,进程退出后有些缓存不会立刻释放。我养成一个习惯,跑Benchmark前用npu-smi info确认显存已经清零,必要时用工具重置设备状态,再开始测试。
5.3 精度不对和算子报错的排查经验
Benchmark跑出来的数字异常,不一定是性能问题,也可能是精度问题。我开始做迁移的时候,经常遇到推理结果完全不对的情况,后来排查出来基本都是输入数据预处理不一致:比如ONNX模型里要求输入是归一化到0~1,我的脚本却直接喂了0~255的原始像素。这类问题在跑Benchmark时不会影响速度,但结果明显不对,需要先修正数据预处理再做性能评估。
算子报错的排查相对容易一些,关键是拿到详细的报错日志。先把ASCEND_GLOBAL_LOG_LEVEL改成0,然后打开ASCEND_SLOG_PRINT_TO_STDOUT,重新跑一次,利用多输出的日志定位到具体是哪个算子、哪一步在报错。常见原因有两个:一个是模型里用了昇腾还不支持的算子或特性,比如某些自定义Op、某些控制流算子;另一个是精度模式设置导致算子用降精度执行后溢出。处理方式也简单,要么换一个精度模式,要么在算子层面加白名单强制某些算子用FP32。
说到精度,我想单独提醒一下LLM推理场景下的精度问题。用FP16跑大模型,如果输出质量明显下降,先检查是否有梯度溢出的可能,然后检查哪些层被降到了FP16执行。用msprof能导出算子执行精度信息,一目了然。
5.4 常见问题速查表
把我在昇腾Benchmark中遇到的高频问题整理成一个速查表,方便你有问题的时候直接对号入座。
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
| FPS数字忽高忽低 | 其他进程抢占、未充分warmup、数据加载抖动 | 清空环境、加大warmup、改用dummy data |
| 跑一会儿后性能和温度同步下降 | 散热不足触发降频 | 检查散热、先预热到稳定温度再测 |
| 显存OOM但模型本身不大 | 静态shape预留过大或内存碎片 | 缩小shape上限、改用分档动态shape |
| 推理结果明显不对 | 输入预处理不一致、精度溢出 | 检查归一化参数、调整精度模式 |
| 算子报错不支持 | 模型用了不支持的算子或版本不匹配 | 检查CANN版本、算子白名单 |
| MSprof抓不到算子详情 | 日志级别过低或permission不足 | 提高日志级别、检查运行权限 |
| 多卡吞吐不对等 | 环境变量未正确设置 | 逐一核对ASCEND_DEVICE_ID与卡序列 |
6. 进阶建议:如何让Benchmark成为团队的基础设施
Benchmark这件事,做一次容易,长期坚持很难。很多项目刚开始会认真测一版性能数据,后面模型一改、代码一调,就不再做回归,结果性能悄悄退化了自己都不知道。我建议团队把Benchmark脚本沉淀成基础设施,每次提交关键代码变更时,自动跑一遍耗时短、结论清晰的Benchmark,把通过/失败作为合并代码的门禁条件之一。
具体做法上,我倾向于维护一个benchmark_suite目录,里面按场景分成train和inference两个子目录,每个子目录下有多个完整的测试用例,包含启动脚本、模型清单、测试参数和环境要求说明。跑的时候一个入口脚本按顺序执行所有用例,最后汇总输出一份带时间戳的Markdown报告,方便和之前的数据做对比。
对于长期跟踪的项目,性能和稳定性数据最好自动落库,这样能画出一条性能变化曲线。曲线一旦出现突然的下滑,就能立刻定位到是哪一次代码提交、哪一次CANN升级引起的。我就是靠这套方法,在几次CANN升级后发现某个算子性能回退,及时回退了版本,避免了带病上线。
回到“标尺”这个比喻,一把标尺要真正发挥作用,得保证每次测量都用同样的方法、同样的条件,读数要能复现,误差要在可控范围内。昇腾的Benchmark基准测试也一样,它的价值不只在于第一次摸清硬件的底数,更在于持续用它跟踪每一次优化、每一次升级、每一个模型变更带来的影响。我个人的体会是,Benchmark数据积累得越久,越能形成对芯片性能的敏锐感觉,哪块板卡适合什么负载、哪个模型换到哪个卡上性能会怎样,基本上测一次心里就有数了。
最后再分享一个小技巧:跑Benchmark之前,把所有环境变量、版本号、模型文件哈希值都记下来,和测试结果一起存档。这样做的好处是,几个月后回看这份数据,还能准确还原当时的测试环境,不会出现“这个数字到底是在哪个版本下跑出来的”这种说不清的情况。基准测试的意义就在于可信、可追溯,数据档案越完整,标尺越可靠。