尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

昇腾推理引擎开源:从模型部署到自定义算子开发实战

昇腾推理引擎开源:从模型部署到自定义算子开发实战 1. 昇腾推理引擎开源这件事到底在解决什么问题第一次在昇腾社区看到推理引擎开源的消息时我正蹲在一个边缘计算项目里调模型部署。当时用的还是闭源工具链每次版本升级都得等官方发版遇到算子不支持只能干等。所以看到“开源”两个字第一反应不是兴奋而是“终于能自己动手改底层了”。昇腾这套推理引擎核心解决的是把训练好的模型高效跑在昇腾NPU上这件事。你训练完一个模型不管是PyTorch还是TensorFlow格式最终要落地到实际业务里做推理中间需要一层“翻译优化”的工具。这层工具要做的事情包括把模型图转换成昇腾能识别的格式、做算子融合、做量化压缩、调度内存和计算资源、管理多卡多流的并发。以前这些能力封装在闭源SDK里你只能调API出了问题只能提工单。开源之后整个推理链路的核心代码摆在台面上你可以看到每一步做了什么也可以自己改。适合谁来关注这个事三类人最应该花时间研究。第一类是做模型部署的工程师你手头有模型要上昇腾硬件开源意味着你可以针对自己的模型结构做定制优化而不是被动等官方适配。第二类是做推理框架开发的同行昇腾的图编译、内存复用、算子调度这些设计思路本身就是很好的学习材料尤其是它怎么处理动态shape、怎么做子图切分这些细节。第三类是做边缘计算和嵌入式AI的开发者昇腾310P这类低功耗芯片在边缘场景用得很多开源之后你可以裁剪掉不需要的模块把引擎体积压到最小。我见过太多团队在模型部署这一步卡住不是模型精度不够而是推理引擎的“黑盒”特性导致调优无从下手。开源推理引擎的价值就在这——它把黑盒变成了白盒你可以看到每一层算子是怎么被调度的内存是怎么被复用的量化误差是在哪一步引入的。这种透明度对于需要极致性能的场景来说比任何文档都管用。2. 昇腾推理引擎的核心架构拆解2.1 图编译层从模型文件到可执行图昇腾推理引擎的图编译层是整个链路的第一道关口。你丢进去一个ONNX模型或者MindSpore导出的AIR模型它首先要做的是图解析和IR转换。这一步会把不同框架的模型统一转换成昇腾自己的中间表示我习惯叫它“昇腾IR”。这个IR的设计很有意思它把计算节点和内存节点分开描述计算节点只关心做什么运算内存节点只关心数据放在哪。这种分离设计的好处是后续做内存复用和算子融合时不需要反复遍历整个计算图。图解析完之后是算子融合。举个例子你模型里有一个Conv2D后面跟着BatchNorm再跟着ReLU在原始图里这是三个独立算子。昇腾的图编译器会把它们融合成一个算子这样中间结果不需要写回内存直接在寄存器或者片上缓存里传递。我实测过一个ResNet50的模型融合之后算子数量从原来的两百多个降到八十几个推理延迟降低了将近百分之三十。这个优化在闭源时代你是感知不到的开源之后你可以看到融合规则是怎么定义的甚至可以自己加规则。再往后是内存分配和复用。昇腾推理引擎采用了一种基于生命周期分析的内存复用策略。简单说它会分析每个张量从什么时候开始被使用、什么时候最后一次被读取然后让生命周期不重叠的张量共享同一块内存。这个策略在开源代码里有完整的实现你可以看到它怎么构建内存池、怎么做地址对齐、怎么处理动态shape带来的内存需求变化。对于显存紧张的边缘设备来说这个机制直接决定了你能不能跑起来一个大模型。注意图编译层的优化效果和模型结构强相关。如果你的模型里有大量动态控制流比如循环、条件分支融合效果会打折扣。这种情况下建议先做图结构简化把能静态化的部分尽量静态化。2.2 运行时调度多流并发与任务分发图编译完成之后运行时调度层负责把计算图变成实际的任务序列分发到NPU的各个计算单元上。昇腾推理引擎的运行时支持多流并发你可以创建多个执行流每个流里跑不同的推理任务它们之间可以并行执行。这个能力在多路视频分析场景里特别有用比如你同时处理八路摄像头的目标检测每路一个流互不阻塞。任务分发的核心是任务队列和依赖管理。引擎会把计算图拆成一个个任务每个任务有明确的输入依赖和输出依赖。调度器根据依赖关系决定哪些任务可以并行、哪些必须串行。开源代码里这部分逻辑写得很清楚你可以看到它怎么用有向无环图来管理依赖怎么处理跨流的同步事件。我印象比较深的是它对小算子合并的处理——如果连续几个算子计算量都很小调度器会把它们合并成一个任务批量提交减少任务切换的开销。内存管理在运行时这一层也很关键。昇腾推理引擎支持静态内存和动态内存混合分配。静态内存用于存放模型权重和固定的中间结果动态内存用于处理变长输入。开源之后你可以自己调整静态内存池的大小对于输入shape固定的场景把静态内存调大可以减少运行时的分配开销。我一般会先用小批量数据跑一遍用profiling工具看看内存峰值然后按峰值的1.2倍来设置静态内存池。2.3 算子库从通用到定制的演进路径算子库是推理引擎的“弹药库”昇腾开源推理引擎里包含了两类算子基础算子和融合算子。基础算子就是Conv、MatMul、Pooling这些标准操作融合算子则是针对特定模型结构预定义好的组合操作比如Transformer里的MultiHeadAttention融合算子。开源带来的最大变化是你可以自己写算子。昇腾提供了一套算子开发框架你用C写计算逻辑用DSL描述调度策略编译成二进制后注册到引擎里就能用。我试过给一个自定义的激活函数写算子从写代码到跑通大概花了两天时间主要时间花在理解它的调度DSL上。一旦跑通之后这个算子就可以像内置算子一样被图编译器识别和融合。对于不想写底层代码的开发者昇腾还提供了算子插件机制。你可以用Python写一个算子实现通过插件接口注册进去。这种方式性能不如原生算子但胜在开发快适合做原型验证。我一般建议先用插件方式验证算法正确性确认没问题之后再改成原生算子追求性能。算子类型开发语言性能适用场景内置基础算子官方实现最优标准模型结构内置融合算子官方实现最优Transformer/CNN常见结构自定义原生算子C/DSL接近内置需要极致性能的定制算子自定义插件算子Python中等原型验证、低频算子3. 从零上手昇腾推理引擎的实操流程3.1 环境搭建与依赖安装先说环境。昇腾推理引擎开源之后安装方式比之前友好了不少。你需要先装好CANN工具包这是昇腾的基础软件栈推理引擎依赖它提供的驱动和运行时。CANN的版本要和推理引擎的版本匹配我一般会去开源仓库的release页面看版本对应表别自己瞎猜。装完CANN之后推理引擎本身可以通过源码编译安装。开源仓库里提供了build.sh脚本但直接跑大概率会报错因为依赖项没装全。我踩过的坑是protobuf版本冲突——系统里已经装了一个版本的protobuf推理引擎需要另一个版本编译的时候会报符号找不到。解决办法是用虚拟环境隔离或者用CMake的find_package指定路径。# 典型的编译流程 git clone 推理引擎开源仓库地址 cd inference-engine mkdir build cd build cmake .. -DCMAKE_INSTALL_PREFIX/usr/local/ascend-infer \ -DPROTOBUF_DIR/path/to/protobuf \ -DCANN_DIR/usr/local/Ascend/ascend-toolkit/latest make -j$(nproc) make install编译过程中如果遇到算子编译失败大概率是DSL语法不兼容。昇腾的调度DSL在不同版本之间有细微差异开源仓库的文档不一定跟得上代码更新。我的经验是直接看仓库里的测试用例照着测试用例的写法来改比看文档靠谱。提示编译之前先跑一遍仓库自带的单元测试确认基础环境没问题。如果单元测试都跑不过后面部署模型肯定一堆问题。3.2 模型转换与精度配置模型转换是推理部署的第一步。昇腾推理引擎支持从ONNX、TensorFlow、MindSpore等格式导入模型。我主要用ONNX因为PyTorch导出ONNX最方便。转换命令的核心参数是输入shape和精度模式。精度模式这里要重点说一下。昇腾310P3支持FP16和INT8两种推理精度。FP16是默认选项精度损失小适合大多数场景。INT8需要做量化校准精度会有一定下降但吞吐量能提升一倍以上。我一般会先用FP16跑一遍记录精度指标然后再试INT8对比精度下降是否在可接受范围内。# 模型转换示例 atc --modelmodel.onnx \ --framework5 \ --outputmodel_昇腾 \ --soc_versionAscend310P3 \ --input_shapeinput:1,3,224,224 \ --precision_modeallow_fp32_to_fp16 \ --out_nodesoutput:0--precision_mode这个参数有几个选项force_fp32强制用FP32310P3不支持会报错、allow_fp32_to_fp16允许自动降精度、must_keep_origin_dtype保持原始精度。我一般用allow_fp32_to_fp16让编译器自己决定哪些算子可以降精度。如果发现精度掉得厉害再用must_keep_origin_dtype把关键算子锁住。INT8量化需要额外做校准。你需要准备一批校准数据大概几百张图片就够了覆盖各种典型场景。校准过程是让模型在FP32下跑一遍校准数据统计每个算子的激活值分布然后计算量化参数。开源代码里包含了校准工具你可以看到它怎么统计直方图、怎么计算scale和zero_point。3.3 推理服务封装与性能调优模型转换完之后你需要写一个推理服务把引擎跑起来。昇腾推理引擎提供了C和Python两套API。Python API适合快速验证C API适合生产部署。我一般先用Python跑通流程确认模型输出正确然后再用C重写。Python API的核心是Session管理。你需要创建一个Session加载离线模型文件然后准备输入输出张量。输入数据需要拷贝到Device内存推理完成后从Device内存拷贝回Host。这个拷贝过程是性能瓶颈之一开源代码里提供了零拷贝的接口你可以直接把Host内存映射到Device地址空间省掉一次拷贝。import acl # 初始化 acl.init() acl.rt.set_device(0) # 加载模型 model_id, _ acl.mdl.load_from_file(model_昇腾.om) # 准备输入输出 input_data np.random.randn(1, 3, 224, 224).astype(np.float16) # 执行推理 acl.mdl.execute(model_id, [input_data], [output_data])性能调优这块我总结下来主要抓三个点批大小、并发流数、内存池大小。批大小决定了单次推理能处理多少数据太小吃不满算力太大内存扛不住。我一般从batch1开始测逐步增加到内存峰值的百分之八十。并发流数决定了能同时跑多少个推理任务310P3一般设4到8个流比较合适。内存池大小前面说过了按峰值的一点二倍设置。还有一个容易被忽略的点是算子精度选择。有些算子对精度不敏感比如ReLU、MaxPool用FP16完全没问题。但有些算子对精度很敏感比如Softmax、LayerNorm用FP16可能会导致数值不稳定。开源之后你可以逐个算子设置精度模式把敏感的算子锁在FP32不敏感的降到FP16这样能在精度和性能之间找到最佳平衡。4. 实际部署中踩过的坑与排查思路4.1 模型转换失败的常见原因模型转换失败是我遇到最多的问题没有之一。最常见的报错是不支持的算子。ONNX里有几千个算子昇腾推理引擎不可能全部支持。遇到不支持的算子你有三个选择找替代算子、自己写算子、或者改模型结构。找替代算子是最省事的。比如ONNX里的HardSwish如果不支持你可以用HardSigmoid乘以输入来等价实现。开源仓库里有一个算子支持列表转换之前先查一下心里有数。自己写算子前面说过了适合高频使用的算子。改模型结构是最下策但有时候不得不做比如把动态shape改成静态shape。第二个常见问题是shape推断失败。ONNX模型有时候不包含完整的shape信息转换的时候引擎推断不出来。解决办法是在转换命令里显式指定--input_shape把所有输入的shape都写清楚。如果模型里有动态维度比如batch维度是-1你需要指定一个具体的值或者用--dynamic_batch_size参数让引擎支持动态batch。第三个坑是数据类型不匹配。ONNX模型里的权重可能是FP32但310P3只支持FP16和INT8。转换的时候引擎会自动做类型转换但如果某个算子的输入类型和权重类型不一致就会报错。我一般会在导出ONNX的时候就把模型转成FP16这样转换的时候少一层麻烦。报错类型典型信息排查方向解决方案算子不支持Op type XXX is not supported查算子支持列表替换算子或自定义算子Shape推断失败Cannot infer shape for node检查输入shape显式指定input_shape类型不匹配DataType mismatch检查权重和输入类型统一转FP16内存不足Out of memory检查模型大小减小batch或优化内存4.2 推理精度异常的定位方法精度异常比转换失败更隐蔽因为模型能跑起来但输出结果不对。我遇到过一次模型转换成功推理也能跑但检测框的位置总是偏一点。排查了半天最后发现是某个融合算子的精度模式设置错了。定位精度问题我一般用逐层对比法。先把模型在CPU上用ONNX Runtime跑一遍保存每一层的输出。然后在昇腾上跑一遍也保存每一层输出。逐层对比找到第一个出现明显差异的层。开源推理引擎提供了dump中间结果的功能你可以在图编译的时候打开--dump_mode把每个算子的输入输出都存下来。找到问题层之后看这个层用了什么精度模式。如果是FP16试着改成FP32再跑一遍。如果精度恢复正常说明这个算子对精度敏感需要锁在FP32。如果改成FP32还是不对那可能是算子实现本身有问题需要看开源代码里的实现逻辑。还有一种精度问题是量化引入的。INT8量化之后精度下降是正常的但如果下降太多比如mAP掉了十个点那就不正常了。这时候需要检查校准数据是否具有代表性校准数据的分布要和实际推理数据一致。我一般会从实际业务数据里随机抽几百张做校准而不是用公开数据集。注意精度排查一定要有耐心逐层对比虽然笨但最有效。我见过有人直接换模型结果换了三个模型都没解决问题最后发现是输入数据预处理不一致。4.3 性能不达预期的调优实录性能不达预期是另一个高频问题。官方文档里写的性能指标是在理想条件下测的实际部署中能跑到百分之七十就算不错了。我遇到过一次ResNet50在310P3上官方标称是XX毫秒我实测跑了将近两倍的时间。排查性能问题第一步是用profiling工具看时间花在哪。昇腾提供了性能分析工具可以看到每个算子的执行时间、内存拷贝时间、任务调度开销。我那次跑下来发现算子执行时间只占了百分之四十剩下百分之六十都花在Host到Device的数据拷贝上。解决办法是用零拷贝接口。传统方式是先把数据从Host内存拷贝到Device内存推理完再从Device拷贝回Host。零拷贝是让Host内存和Device内存映射到同一块物理地址省掉两次拷贝。这个接口在开源代码里有实现但需要你的Host内存满足对齐要求。我改成零拷贝之后整体延迟降了将近百分之四十。第二个性能问题是算子融合没生效。图编译器默认会做算子融合但如果你的模型结构比较特殊融合规则可能匹配不上。开源之后你可以看到融合规则的定义也可以自己加规则。我遇到过一个情况模型里的Conv后面跟了一个自定义的激活函数融合规则不认识这个激活函数就没融合。后来我把激活函数改成内置的融合就生效了。第三个问题是并发流数设置不合理。流数太少算力吃不满流数太多任务切换开销大。我一般会做一个简单的压测从1个流开始逐步增加看吞吐量的变化曲线。曲线拐点就是最佳流数。310P3上我实测下来4到6个流是比较合适的区间。5. 开源之后推理引擎的扩展玩法5.1 自定义算子开发的完整流程开源最大的好处就是可以自己写算子。昇腾的算子开发流程分四步定义算子原型、实现计算逻辑、编写调度策略、注册编译。定义算子原型是告诉引擎这个算子叫什么名字、有几个输入几个输出、输入输出的数据类型和shape是什么。这部分用C写继承一个基类实现几个虚函数就行。实现计算逻辑是核心。你需要用C写一个函数输入是张量数据输出是计算结果。这里要注意的是昇腾的算子是在Device上执行的你不能直接访问Host内存。所有数据操作都要通过引擎提供的内存接口。编写调度策略是最难的部分。昇腾用一套DSL来描述算子怎么在NPU上调度包括数据怎么分块、怎么用片上缓存、怎么并行。这套DSL的语法和CUDA的kernel写法不太一样需要花时间适应。我的经验是先从简单的逐元素算子开始写比如ReLU、Add熟悉了DSL之后再写复杂的卷积类算子。注册编译是把写好的算子编译成二进制注册到引擎的算子库里。注册之后图编译器就能识别这个算子转换模型的时候就不会报“算子不支持”了。// 算子原型定义示例 REGISTER_OP(MyActivation) .Input(x: float16) .Output(y: float16) .Attr(alpha: float 1.0) .SetShapeFn([](InferenceContext* ctx) { ctx-set_output_shape(0, ctx-input_shape(0)); return Status::OK(); });5.2 推理引擎与训练框架的协同开源之后推理引擎和训练框架之间的边界变得模糊了。以前训练用MindSpore推理用推理引擎两边是割裂的。现在你可以在训练框架里直接调用推理引擎的接口做在线推理比如在训练过程中做验证集评估不用等训练完再转换模型。另一个玩法是量化感知训练。传统做法是训练完再做量化精度损失比较大。量化感知训练是在训练过程中模拟量化误差让模型适应量化。昇腾推理引擎开源了量化工具你可以把量化节点插入到训练图里训练完直接导出量化模型省掉校准步骤。还有一个方向是动态shape支持。以前推理引擎主要支持静态shape动态shape支持有限。开源之后你可以自己扩展动态shape的处理逻辑比如支持变长序列输入。这对于NLP类模型很重要因为文本长度是不固定的。5.3 社区生态与贡献指南昇腾推理引擎开源之后社区生态还在建设中。目前主要的贡献方式有三种提交算子实现、修复bug、完善文档。提交算子实现是最受欢迎的贡献。如果你实现了一个通用性强的算子比如某个新的激活函数或者注意力机制可以提PR到主仓库。维护团队会review代码通过之后合并到主分支。我提过一个自定义的Swish算子从提交到合并大概花了两周时间主要时间花在代码风格调整和测试用例补充上。修复bug是另一种贡献方式。开源代码里难免有bug如果你在使用过程中发现了可以提issue也可以直接提PR修复。提issue的时候要附上复现步骤和环境信息方便维护者定位。完善文档是最容易被忽略但很重要的贡献。开源项目的文档往往跟不上代码更新如果你发现文档和实际行为不一致可以提PR修正。我一般会在使用过程中记录下文档没写清楚的地方攒一批之后一起提PR。提示贡献代码之前先看一遍仓库的CONTRIBUTING.md了解代码风格和提交规范。不符合规范的PR会被打回浪费时间。6. 一些实操心得和后续扩展方向我在实际使用昇腾推理引擎的过程中最大的体会是不要把它当成黑盒。开源之后遇到问题的第一反应应该是去看代码而不是去搜文档。文档可能过时但代码不会骗人。我遇到过好几次文档里写的参数和代码里实际的不一致以代码为准就对了。另一个心得是版本管理要严格。CANN版本、推理引擎版本、模型转换工具版本三者必须匹配。我吃过一次亏CANN升级了但推理引擎没升级结果模型转换报了一堆莫名其妙的错。后来我养成了习惯每次升级之前先看release note里的版本对应表确认兼容性再动手。对于后续扩展我觉得有几个方向值得关注。一是推理引擎和开源模型的结合现在很多开源模型比如LLaMA系列都需要定制化的推理优化昇腾推理引擎的开源正好提供了这个能力。二是边缘端的轻量化部署310P3这类芯片在边缘场景很有优势开源之后可以做更激进的裁剪和优化。三是多模态推理视觉加文本的联合推理对引擎的调度能力要求很高开源代码里的多流并发机制正好可以派上用场。最后分享一个小技巧如果你在转换模型时遇到不支持的算子先别急着写自定义算子。去开源仓库的issue列表里搜一下大概率有人遇到过同样的问题而且可能已经有解决方案了。我至少有三四次是在issue里找到的替代方案省了自己写算子的一两天时间。
返回列表