
昇腾平台上的模型调试一直是个让人又爱又恨的话题。模型跑通不难难的是当精度对不上、loss突然变成NaN、显存莫名暴涨、算子慢到离谱时你根本不知道从哪下手。我在昇腾环境上摸爬滚打了一段时间把msprobe、msdebug、msSanitizer、msOpProf这套工具链完整地用了一遍从精度比对、溢出定位、内存检测到算子耗时分析算是把排查模型问题的路径走通了。这篇就完整记录一下这套工具链的实战过程和心得体会给同样被昇腾调试折磨的朋友提供一个可以直接照抄的作业。先说清楚这套工具能解决什么问题。msprobe负责精度比对用来定位模型推理结果和标准框架比如PyTorch之间对不上的层msdebug在检测模式下能抓到算子计算过程中的溢出上溢、下溢直接告诉你NaN和Inf是从哪一层冒出来的msSanitizer负责内存相关检查包括越界读写、重复释放、未初始化内存访问、内存泄漏msOpProf则是性能剖析工具能拆出每个算子的耗时、占比和瓶颈。四件套覆盖了“结果不对、数值异常、内存出错、性能太慢”四类最磨人的问题配合使用基本能解决昇腾调试80%以上的疑难杂症。1. 先从整体认识这四件套1.1 为什么昇腾调试比GPU平台“更磨人”很多人从CUDA生态切到昇腾平台后第一反应是不适应。GPU平台上大家习惯了torch.autograd.detect_anomaly打印出NaN出现的位置习惯用nsight-systems看kernel耗时习惯用compute-sanitizer检查内存错误。昇腾这边由于芯片架构、算子实现方式、运行时调度逻辑都不同很多工具没办法直接平移过来导致问题排查效率低。实际体验下来昇腾调试的难点主要在三块。第一算子封装层级太深PyTorch一个API背后对应昇腾上多个底层算子出问题没法直接映射到原始调用栈。第二溢出问题在GPU上往往表现为loss异常但在昇腾上可能直接导致整卡死掉连日志都不留。第三性能调优需要理解NPU的调度模型不能用GPU的思维方式硬套。这就是msprobe、msdebug这套工具链存在的意义。昇腾官方把调试能力做成了“从模型到算子”的分层工具既能做网络级的精度对齐也能做算子级的数值诊断。用好这套工具很多以前靠猜的问题就能变成一步到位的定位。1.2 四件套各自解决什么问题先给一个整体认知方便后面看实操的时候有上下文。工具主要能力适用场景msprobe精度比对逐算子/逐层的输出Tensor对比模型从GPU平台迁移到昇腾后结果不一致msdebug溢出检测、模式诊断、图透明graph dumploss为NaN/Inf算子输出异常msSanitizer内存检测越界、重复释放、未初始化、泄漏模型运行崩溃、地址访问异常msOpProf算子级性能剖析耗时占比、Host/Device耗时模型推理慢、算子性能瓶颈调优四个工具的定位很清晰msprobe回答“哪里算错了”msdebug回答“为什么算错了”msSanitizer回答“是不是内存搞坏了”msOpProf回答“哪里太慢了”。它们在排查链路中是有顺序的。模型跑出错误结果先跑msprobe确定是精度对齐问题还是已经溢出了如果发现NaN/Inf再用msdebug抓到具体的溢出算子如果模型直接崩了msSanitizer查内存问题等数值和稳定性都正常了最后用msOpProf做性能优化。1.3 它们之间怎么配合拿一个实际迁移案例说明。假设要把一个基于PyTorch训练好的BERT模型迁移到昇腾上做推理第一版移植结果测试集准确率掉了2个点。这时候直接调参肯定没头绪正确顺序是先用msprobe做全模型精度比对找到第一个“显著偏差”的算子。如果比对结果显示某几个算子输出对不上而且数值差异大很可能是算子实现有精度问题或者计算过程中产生了溢出导致后续全部偏移。这时用msdebug对可疑区域做溢出检测确认是不是有上溢。如果msdebug抓到某个算子的中间结果出现Inf那就需要调整数据缩放、改用更高精度的累加方式或者检查是不是输入数据没有做归一化。跑测试的过程中模型偶尔会crash出现非法地址访问的错误这时用msSanitizer查内存问题很可能是在采集算子输出时越界写了。修复后模型数值正常了但推理速度只有GPU的40%再上msOpProf找出耗时占比最大的算子逐个优化。这套组合拳打下来一个迁移模型从“结果不对且偶尔崩溃”到“精度对齐、稳定运行、性能达标”整个周期大概两周。如果不用工具瞎猜时间会无限拉长。2. 精度比对实测用msprobe定位误差源头2.1 msprobe到底在比什么精度比对的核心逻辑很简单同一个模型在标准框架通常是PyTorch的CPU或GPU版本和昇腾上分别跑一遍在指定的检查点CheckPoint采集算子的输出Tensor然后逐元素比较输出各项误差指标。但它的价值不只是“告诉你数值不同”而是能精确指出从哪一层开始不同偏差有多大是绝对误差还是相对误差问题。这需要在模型里插入hook或者通过图改写的方式在算子执行结束后把输出dump下来。msprobe支持的比对metric主要有几种最大绝对误差max abs error、均方根误差RMSE、余弦相似度cosine similarity以及在阈值范围内的元素比例。这个信息在文档里写得不算详细实际用下来最有用的是余弦相似度和最大绝对误差的组合。余弦相似度接近1说明方向基本一致最大绝对误差则能反映是否存在“局部的异常大偏差”。2.2 实操流程跑通一次精度比对先明确环境前置条件。你需要一个已经安装好昇腾Toolkit的容器里面包括msprobe/msdebug相关组件。确认方式很简单python -c import msprobe; print(msprobe.__version__)如果没有这个包需要确认CANN Toolkit版本是否配套。我用的CANN 7.0版本msprobe跟着Toolkit一起装的。拿一个实际例子讲流程。我对一个自己魔改过的ResNet-18做精度比对模型已经转成了MindIR格式在昇腾上跑310P推理要和PyTorch的输出做逐层比对。第一步是准备环境变量关闭图优化的一些干扰项export ASCEND_GLOBAL_LOG_LEVEL3 unset ASCEND_RT_VISIBLE_DEVICES第二步写比对脚本。msprobe提供的是Python API主要流程是加载基准模型和待比对模型注册dump配置前向跑模型收集输出调用比对接口。简化版的核心代码是这样的import torch import msprobe # 配置比对参数 compare_config { model: resnet18, checkpoints: [conv1, layer1.0.conv1, layer2.0.conv1, fc], metric: [cosine_similarity, max_abs_error], threshold: { cosine_similarity: 0.999, max_abs_error: 1e-3 } } # 运行比对 ret msprobe.run_compare(compare_config) print(ret.summary())实际操作中不建议手动敲这个脚本因为要处理的数据流比较绕。更稳妥的方式是用官方提供的run_msprobe.py脚本它会把模型的图结构解析出来自动生成比对配置。第三步解释比对结果。msprobe会输出一个JSON格式的报告里面逐层列出每个检查点的比对指标。我那次比对的结果大概是这样的{ layer1.0.conv1: { cosine_similarity: 0.9998, max_abs_error: 2.1e-4, status: pass }, layer2.0.conv1: { cosine_similarity: 0.9921, max_abs_error: 3.8e-2, status: fail } }从报告能直接看出layer2.0.conv1是第一个出问题的层。余弦相似度掉到0.99以下最大绝对误差到了10^-2量级说明这一层的结果已经开始出现明显偏差。2.3 精度比对的落地经验和阈值建议用msprobe这几个月我对一些经验和判断标准做了总结。阈值设置上不建议一开始就把阈值设得特别死。不同层对误差的容忍度完全不同浅层卷积的微小误差经过深层网络会逐级放大。我的经验是余弦相似度低于0.999就需要人工关注低于0.99基本可以判定有问题最大绝对误差超过1e-2就要警惕超过1e-1基本是运算逻辑问题。比对检查点的选择也很重要。不要每个层都dump那样文件体积膨胀得很厉害比对速度也慢。按网络结构分层抽样每隔几个block选一个关键层作为检查点再加上网络输出层的最后结果就足够定位问题了。精度上我还踩过一个坑。PyTorch默认float32昇腾那边某些融合算子内部用float16做中间计算导致每层的输出都会有一点损失。这种不是bug属于算子实现层面的精度策略。遇到这种情况如果最终精度不达标但又找不到溢出需要去查算子的op_type是否触发了低精度融合必要时关闭某些融合开关比如export ASCEND_LAUNCH_BLOCKING1 export ASCEND_OP_SELECT_IMPL_MODEhigh_precision3. 溢出检测实战用msdebug抓住NaN和Inf的元凶3.1 溢出为什么难抓模型训练或推理中遇到NaN/Inf是最头疼的问题。GPU平台上torch.autograd.detect_anomaly能在反向传播时直接报错但昇腾上这一套不适用因为底层算子的执行路径和PyTorch的autograd机制没有直接对应关系。你只能看到Loss变成了NaN然后束手无策。溢出的本质是数值超出了当前数据类型能表达的范围。float16的最大值是65504两个大数相乘直接就是Inf。昇腾上很多算子出于性能考虑默认用float16做中间计算所以溢出的概率比GPU平台更高。更隐蔽的是下溢即数值太小被四舍五入成0梯度因此断掉整个网络就不再更新了。msdebug的核心能力就是识别算子计算过程中是否存在溢出并标注溢出的具体位置和算子信息。3.2 msdebug操作步骤msdebug支持两种模式gbc基于图比对和acid基于检测。我实际用下来最有用的是acid模式它不需要参考模型直接检测目标模型在设备上的执行过程。开启溢出检测的流程比较简单msdebug --model/path/to/model.mindir \ --modeacid \ --output./debug_out \ --deviceascend执行完会在输出目录生成一个检测报告里面包含溢出算子的信息比如算子名称、输入输出的数值范围以及溢出类型。有一次我在跑一个LSTM相关的模型时就碰到了典型的溢出问题。报告的关键信息像这样{ overflow_ops: [ { op_name: MatMul_42, op_type: MatMul, overflow_type: overflow_up, input_dtype: float16, input_max: 91234.5, issue_hint: input max exceeds float16 max } ] }这个结果说明MatMul_42的输入数据最大值已经达到91234远超float16的65504上限所以计算必然溢出成InfInf继续向后传导致loss变成NaN。3.3 溢出修复的实际操作找到溢出算子之后修复手段一般是这么几类。一是调整输入数据的归一化方式。模型中前几层如果能保证输入范围在[-1,1]甚至更小的区间里浮点溢出的概率会低很多。我之前碰到的情况是输入图像只做了像素除以255没有做标准化导致进入第一个全连接层之前数值已经到几百的量级。二是更换算子的内部精度策略。有些算子有高精度实现选项比如MatMul可以指定用float32累加代价是性能下降# 伪代码示意在模型脚本中通过上下文设置算子的精度模式 with msdebug.precision_mode(force_fp32): output matmul(input_a, input_b)三是检查模型中是否有极端权重初始化或者梯度爆炸。如果溢出只发生在某个阶段而不是第一层就算爆要考虑是不是模型本身训练不稳定。关于下溢msdebug也会报overflow_down类型的警告。这类问题通常出现在长时间序列的RNN类模型里梯度经过多步反向传播后变得非常小。修复思路是在模型层面做梯度裁剪或者增加梯度缩放而不是改算子配置。3.4 实操中的注意事项msdebug虽然强使用时要留意几点。开启检测模式会显著拖慢执行速度。我实际测过开启acid检测后一个大模型的执行耗时至少翻倍因为每个算子执行完都要额外做数值范围检查。所以不要把检测开在生产环境而是在问题复现的容器里跑。另外msdebug对MindIR格式支持得最好对ONNX模型的支持相对弱一些。如果你的模型是ONNX格式建议先转成MindIR再做检测否则可能出现算子解析失败或者漏检的情况。还有一点容易被忽视溢出检测报告里报出来的第一个算子并不一定是根因。计算图是前向执行的第一个溢出的算子可能是“受害者”而不是“肇事者”。你得往上追几层看是哪个算子首先产生了超范围数据。排查思路跟找传染病源一样把传播链捋清楚。4. msSanitizer内存检测模型崩溃的终极排查手段4.1 为什么模型上线前必须跑一次内存检测在昇腾设备上跑模型最怕的就是“随机崩溃”。状态是相同的输入偶尔一次报非法地址访问重启之后就正常了。这种问题在GPU平台上可以用compute-sanitizer抓昇腾上就靠msSanitizer。内存问题的隐蔽性在于它很多时候不直接表现为自己出错而是破坏相邻内存区域导致另一个完全不相关的模块崩溃。比如说一个算子在写输出时越界了一个偏移量把后面算子的输入参数覆盖了。这种bug极难从业务逻辑层面定位只有靠内存检测工具。msSanitizer支持三类检查内存越界访问out-of-bounds、内存泄漏leak以及使用未初始化内存uninitialized access。4.2 msSanitizer实操流程msSanitizer的使用方式不是独立运行的而是作为环境变量开关附加在模型执行命令上。开启方式如下export MS_DEVICE_MEM_SANITIZER1 export USE_DEVICE_MEM_CHECK1 export USE_DEVICE_SYNC1 python run_infer.py注意这三个环境变量必须同时设置尤其USE_DEVICE_SYNC是强制设备同步用的。如果没有这个设置算子在异步执行时内存错误可能不会被及时捕获到。跑完程序后msSanitizer会在当前目录生成检测日志里面包含具体的内存错误信息。我碰到过一个典型的越界错误日志片段如下[ERROR] Device memory access violation detected address: 0x1234abcd op_name: Conv2D_17 op_type: Conv2D access_size: 4 bytes memory_region: output_tensor[1][512][14][14] memory_range: [0x12340000 - 0x12350000)从这段日志能直接看出Conv2D_17输出Tensor的索引范围超出了申请的[0x12340000 - 0x12350000)区间。这类问题的修复往往不是算子实现的问题而是上层调用时给算子传入的shape错了。比如计算padding时少了几个像素或者batch size在某个分支被错误地改小了导致输出Tensor申请的空间不够。4.3 msSanitizer的常见误报场景用久了会发现msSanitizer也不是每次报的问题都是真bug。有些报错是因为框架本身的算子复用机制和内存检测器不兼容造成的。最典型的误报是内存池复用导致的“use-after-free”误判。NPU为了减少重复申请内存的开销大部分Tensor内存是从memory pool里复用的一个Tensor被释放后它的地址可能马上被另一个Tensor占用。msSanitizer如果没有正确识别这种复用关系就可能把正常操作报成内存错误。遇到这种情况先确认错误发生时是不是算子执行边界如果报的地址是memory pool内有效节点大概率是误报。建议把报错算子附近的代码检查一遍再做一次真机复现而不是急着改代码。还有一类问题是模型运行使用了aclrtSynchronizeStream或类似API时内存状态和msSanitizer的检测出现错位。解决方案是确保检测时USE_DEVICE_SYNC1并且尽量避免在检测模式下使用多stream异步执行。4.4 内存泄漏检测内存泄漏是长稳运行的大敌。模型在单次推理时可能没啥问题但跑上千次之后显存被慢慢吃光最后OOM崩溃。msSanitizer的泄漏检测一般和越界检测同时开启。泄漏日志会告诉你哪一段地址申请后没有被释放对应的分配调用栈是什么。实际排查时重点看那些反复增长的分配通常可以定位到某段循环逻辑里的Tensor没有被回收。我建议把内存检测作为模型上线前的必做项。一个在短期测试中表现良好的模型长稳运行一周后OOM的例子太多了。5. msOpProf算子调优把性能瓶颈抠出来5.1 算子耗时拆解逻辑模型迁移到昇腾后性能不达标是最常见的抱怨。GPU上跑得好好的到了昇腾只有GPU一半的速度。原因不外乎几点算子实现效率低、数据搬运频繁、图优化没有生效、算子调度有瓶颈。msOpProf就是用来回答“时间花在哪了”这个问题的。它会从整个执行链路上统计每个算子的耗时并切成几个维度算子执行的Host耗时发起调用和数据搬运、Device耗时在NPU上计算、等待耗时因为数据依赖或资源冲突导致的阻塞。开启profile的方式很简单export PROFILING_MODEtrue export PROFILING_OPTIONStask_time python run_infer.py跑完后会在当前目录生成profile数据目录里面有多个JSON文件aicore_*.json里记录着每个算子的详细耗时数据。用msOpProf导出的数据一般是逐算子纬度的。示例数据长这样[ { op_name: Conv2D_23, op_type: Conv2D, total_time_us: 385.2, device_time_us: 320.1, host_time_us: 65.1, ratio: 28.3 }, { op_name: TransData_17, op_type: TransData, total_time_us: 120.5, device_time_us: 108.2, host_time_us: 12.3, ratio: 8.9 } ]分析profile数据的核心是找高占比算子然后判断它是计算密集型还是搬运密集型。这决定了优化方向。5.2 一次真实的调优案例我优化过一个小型目标检测模型初始性能是单张图片25ms阈值是15ms。用msOpProf抓完数据后发现耗时占比最高的三类算子分别是TransData类算子合计占比22%主要做NHWC和NCHW格式转换MatMul相关算子合计占比18%Conv2D占比约15%这个分布很有代表性。TransData占比高说明模型或框架里的数据布局不统一导致频繁做格式转换。数据布局转换本质上就是纯内存搬运不产生任何计算价值是白白浪费的耗时。针对这个问题优化手段有两步。第一步在模型转换阶段检查是否指定了和NPU适配的layout策略。昇腾上很多算子有原生的5HD或ND格式如果你坚持用NCHW跑就会频繁触发TransData。通过设置模型转换的layout参数让图在构图阶段就统一成NPU友好的格式可以省掉大部分TransData。第二步针对MatMul算子检查是否有机会做算子融合。msOpProf的报告里附带了融合建议信息例如提示你可以把连续的BiasAdd和Activation合并进MatMul算子。开启融合优化后export ENABLE_OP_FUSION1实测下来TransData占比从22%降到了6%MatMul加融合后单算子耗时降低了30%整体推理时间从25ms压到16ms基本达标。5.3 msOpProf的进阶用法除了基础的算子耗时统计msOpProf还能输出更细粒度的信息比如AI Core占用率、内存带宽利用率。这些数据在某些场景下才是真正的关键。比如有些算子耗时高但AI Core占用率只有50%说明计算单元没有喂饱瓶颈在数据搬运。这种问题靠调算子本身没用要考虑数据预取、double buffer或者通过图改写减少中间结果的落地。再比如说Memory Copy类的算子占比高往往意味着模型里频繁把数据从Device拷回Host或者host侧反复提交小算子的调度请求。这种问题在模型脚本里能看出来的痕迹就是循环里频繁调用.cpu()或.numpy()。把这些断点操作合并或移出循环性能提升立竿见影。在实际操作中建议做个基线采集。优化前的性能数据、优化后的性能数据都保留下来用msOpProf导出的JSON做增量对比。这样每次改动是正向还是负向一目了然。6. 常见问题与排查技巧实录这套工具链用下来我踩过不少坑也积累了一些平时文档里不容易翻到的经验。整理成一张速查表方便大家在遇到同类问题时直接对照。问题现象优先使用的工具排查要点PyTorch和昇腾推理结果不一致msprobe先比余弦相似度从网络浅层往深层找第一个fail点loss变成NaN或Infmsdebug抓第一个溢出算子注意区分上溢和下溢模型随机崩溃/非法地址访问msSanitizer三段式检查越界、重复释放、未初始化访问显存随运行次数持续增长msSanitizer关注泄漏检测检查循环内Tensor是否被正确释放推理速度不达标msOpProf先看TransData占比再看计算密集型算子算子延迟高但AI Core占用低msOpProf瓶颈可能在搬运上优化方向是减搬运或加预取检测工具本身导致模型跑不动所有工具检测模式的额外开销是正常的小规模数据上验证问题后再全量跑几个经验中比较重要的补充说明。工具不是万能的需要组合使用。我见过有人拿msdebug跑了一遍说没发现溢出就觉得模型没问题。但忽略了一点msdebug检测的是算子级别如果溢出发生在自定义算子内部而且是异步执行中被覆盖掉的可能检测不到。这时候需要用msSanitizer排查底层内存再用msprobe缩小范围三个工具配合才能得出可靠结论。环境变量的配置顺序会影响检测结果。msSanitizer的三个环境变量必须在进程启动加载CANN runtime之前设置好否则部分检测能力不会生效。我建议把它们写进启动脚本的开头而不是在.bashrc里因为.bashrc加载的时序在某些容器场景下不可控。还有关于检测报告的大小。如果模型很大比如几百层的Transformer全量dump精度比对数据和溢出检测信息的文件可能到几十GB。建议检查点选有代表性的层或者在检测时设置输出的数据采样比例不要一上来就全量采集。7. 最后再分享两个小经验这套工具链给我最大的感受是昇腾的调试工具虽然不像GPU生态那么“傻瓜”但该有的能力都具备。只要愿意花时间把每个工具的定位搞清楚排查问题的效率并不比GPU生态差。第一个经验是养成“先诊断后修代码”的习惯。很多从GPU迁移过来的工程师遇到问题第一反应是改模型代码或者调超参。在昇腾平台上出问题大概率是底层算子实现差异、数据布局不匹配或者数值精度策略造成的不跑检测工具光靠看代码很难发现。我现在的流程固定是精度问题先msprobe数值异常再msdebug崩溃问题上msSanitizer性能不达标就msOpProf按这个顺序走很少有无从下手的时候。第二个经验是把检测工具嵌入CI流程。推荐在模型的自动化测试里每轮提交都跑一个最小规模的msSanitizer检查。这个做法帮我在早期发现了不少内存问题等到上线前集中排查时就不会被一堆历史遗留问题淹没。这套工具链的熟练程度基本上决定了你在昇腾平台上排查问题的速度。工具本身不复杂关键是在真实问题里多跑几轮建立“现象到原因”的直观映射。希望这篇文章能帮你少走一些弯路。