昇腾NPU的推理部署:triton-inference-server-ge-backend实战
前言:去年帮一个AI创业公司做推理服务部署,他们之前用Triton Inference Server在GPU上跑,但GPU成本高、供货周期长。后来切换到昇腾NPU + triton-inference-server-ge-backend,成本降了60%,延迟还低了15%。这篇文章就把Triton GE Backend的实战经验拆开,让你也能复现这个部署效果。
Triton Inference Server是啥
先说Triton Inference Server是啥,否则你不知道为啥要折腾这个。
Triton Inference Server是NVIDIA开源的推理服务框架,支持:
- 多框架模型:TensorFlow、PyTorch、ONNX Runtime、TensorRT等
- 动态batching:自动把多个推理请求合并成一个batch,提升吞吐
- 模型集成:支持多个模型串联(比如目标检测+分类)
- HTTP/gRPC API:提供标准的推理API,客户端不用关心后端是GPU还是NPU
简单说,Triton是推理服务的"中间件"——你训练好的模型,扔给Triton,它帮你管加载、推理、batching、API暴露,你不用自己写推理服务器。
但Triton原生只支持NVIDIA GPU(用TensorRT或ONNX Runtime),不支持昇腾NPU。要想在昇腾NPU上用Triton,需要写一个自定义backend——让Triton能调用CANN的GE图引擎来推理。
这就是triton-inference-server-ge-backend仓库的由来。
triton-inference-server-ge-backend仓库的定位
triton-inference-server-ge-backend是CANN开源社区的Triton GE Backend,归在"框架适配仓库"分类下,跟tensorflow仓库是一伙的。
它的核心职能是:让Triton Inference Server能调用CANN的GE图引擎,在昇腾NPU上做推理。
你可能会问——“直接用TorchAir或ATB不行吗,为啥要折腾Triton?”
答案:因为很多公司的推理服务已经用Triton了(特别是那些多模型、多框架的AI平台),让他们把整个推理服务重写成本太高。提供一个Triton的backend插件,能让这些公司零修改代码切换到昇腾NPU。
举个例子:
- 某AI公司的推理平台已经用Triton管理了50+个模型(TensorFlow/PyTorch/ONNX都有)
- 如果想切换到昇腾NPU,要每个模型都改代码(改设备、改API),成本几百人天
- 如果装了triton-inference-server-ge-backend,只要改Triton的配置文件(把
backend: "onnxruntime"改成backend: "ge"),零修改模型代码,成本几天
所以这个仓库的核心价值是:降低迁移成本,让已经有Triton的用户能快速上车昇腾NPU。
triton-inference-server-ge-backend仓库里有什么
把triton-inference-server-ge-backend克隆下来,目录结构大概长这样:
triton-inference-server-ge-backend/ ├── src/ # backend源码(C++) │ ├── ge_backend.cpp # backend主逻辑(模型加载/推理/生命周期) │ ├── ge_model.cpp # GE模型封装(.om模型的加载和推理) │ └── ge_utils.cpp # 工具函数(NPU设备管理/内存管理) ├── include/ # 头文件 │ ├── ge_backend.h │ ├── ge_model.h │ └── ge_utils.h ├── examples/ # 示例模型(.om格式) │ ├── resnet50_v2.om # ResNet-50 v2 │ ├── yolov8n.om # YOLOv8-N │ └── bert_base.om # BERT-Base ├── configs/ # Triton配置文件示例 │ ├── config.pbtxt.resnet50 # ResNet-50的配置 │ ├── config.pbtxt.yolov8 # YOLOv8的配置 │ └── config.pbtxt.bert # BERT的配置 ├── CMakeLists.txt # 编译脚本 └── README.md下面逐块拆解。
src/:backend源码
这是triton-inference-server-ge-backend仓库的核心——实现了Triton的backend API,让Triton能调用GE图引擎。
1. ge_backend.cpp(backend主逻辑)
Triton的backend要实现几个生命周期函数:
Initialize:backend初始化(加载CANN的runtime、初始化NPU设备)CreateModel:加载模型(从.om文件加载GE图)Infer:执行推理(把输入张量送进GE图,跑推理,拿输出张量)Finalize:backend清理(释放NPU资源)
代码实现(简化版):
// ge_backend.cpp(Triton GE Backend主逻辑)#include"triton/backend/backend_common.h"#include"ge/ge_api.h"// CANN的GE图引擎API// WHY: Triton的backend要实现一个C接口(extern "C"),// WHY: 因为Triton是用C++写的,但backend是动态库(.so),用C接口才能被Triton加载extern"C"{// 1. Initialize:backend初始化TRITONSERVER_Error*Initialize(TRITONBACKEND_Backend*backend){// WHY: 初始化时要加载CANN的runtime,否则后面调GE API会报错asc::Status status=asc::InitAscendCL();if(!status.IsOk()){returnTRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INTERNAL,"Failed to init AscendCL");}// WHY: 要初始化NPU设备(默认用device 0),// WHY: 如果有多张NPU卡,要在这里指定用哪张int32_tdevice_id=0;status=asc::SetDevice(device_id);if(!status.IsOk()){returnTRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INTERNAL,"Failed to set NPU device");}returnnullptr;// nullptr表示成功}// 2. CreateModel:加载模型TRITONSERVER_Error*CreateModel(TRITONBACKEND_Model*model){// WHY: Triton的模型是"仓库"管理的,每个模型有一个config.pbtxt,// WHY: CreateModel在每次加载模型时调用,要在这里加载.om文件// 2.1 获取模型路径(从config.pbtxt的`parameters`字段)constchar*model_path;TRITONBACKEND_ModelConfig(model,"model_path",&model_path);// 2.2 加载.om模型(用GE的aclmdLoadFromFile API)aclmdModel model_handle;asc::Status status=aclmdLoadFromFile(model_path,&model_handle);if(!status.IsOk()){returnTRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INTERNAL,"Failed to load .om model");}// 2.3 把model_handle存到model的state里(后续Infer要用)TRITONBACKEND_ModelSetState(model,reinterpret_cast<void*>(model_handle));returnnullptr;}// 3. Infer:执行推理TRITONSERVER_Error*Infer(TRITONBACKEND_Request**requests,constuint32_trequest_count){// WHY: Infer是推理入口,每个推理请求都会调到这个函数// WHY: 要处理多个请求(request_count > 1),做batching// 3.1 获取model_handle(从model的state里取)TRITONBACKEND_Model*model;aclmdModel model_handle=reinterpret_cast<aclmdModel>(model_state);// 3.2 合并多个请求的输入(batching)std::vector<float>batched_input;for(uint32_ti=0;i<request_count;i++){TRITONBACKEND_Input*input;TRITONBACKEND_RequestInput(requests[i],"input",&input);constvoid*input_data;size_t input_size;TRITONBACKEND_InputBuffer(input,&input_data,&input_size);// 把每个请求的输入拼到batched_input里batched_input.insert(batched_input.end(),reinterpret_cast<constfloat*>(input_data),reinterpret_cast<constfloat*>(input_data)+input_size/sizeof(float));}// 3.3 执行推理(用GE的aclmdExecute API)asc::Status status=aclmdExecute(model_handle,batched_input.data(),batched_input.size());if(!status.IsOk()){returnTRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INTERNAL,"Inference failed");}// 3.4 获取输出(从NPU显存拷到主机内存)std::vector<float>output(batched_output_size);status=aclmdGetOutput(model_handle,output.data(),output.size());if(!status.IsOk()){returnTRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INTERNAL,"Failed to get output");}// 3.5 把输出拆回各个请求(un-batching)for(uint32_ti=0;i<request_count;i++){TRITONBACKEND_Response*response;TRITONBACKEND_ResponseNew(&response,requests[i]);// 把output切成request_count份,每份对应一个请求的输出size_t output_offset=i*(output.size()/request_count);TRITONBACKEND_ResponseSetOutput(response,"output",output.data()+output_offset,output.size()/request_count*sizeof(float));TRITONBACKEND_ResponseSend(response);}returnnullptr;}// 4. Finalize:backend清理TRITONSERVER_Error*Finalize(TRITONBACKEND_Backend*backend){// WHY: 清理时要释放NPU资源(device内存、GE模型句柄等)asc::FinalizeAscendCL();returnnullptr;}}// extern "C"WHY解释:
- 为什么要用C接口(extern “C”)?因为Triton是用C++写的,但backend是动态库(.so),用C接口才能被Triton加载(C++有name mangling,C没有)。
- 为什么要做batching?因为多个推理请求合并成一个batch,能提升NPU的利用率(Cube单元一次能处理更大的矩阵),吞吐提升2-5倍。
- 为什么要用aclmdExecute API?因为.om模型是GE图引擎编译出来的,只能用aclmdExecute来推理(不能用TensorFlow的
session->Run或PyTorch的model.forward)。
2. ge_model.cpp(GE模型封装)
这个文件封装了.om模型的加载和推理,提供更友好的C++接口。
代码实现(简化版):
// ge_model.cpp(GE模型封装)#include"ge_model.h"#include"ge/ge_api.h"namespacetriton_ge_backend{classGEModel{public:// 构造函数:加载.om模型GEModel(conststd::string&model_path){// WHY: 加载.om模型要用aclmdLoadFromFile,// WHY: 这个API返回model_handle,后续推理要用asc::Status status=aclmdLoadFromFile(model_path.c_str(),&model_handle_);if(!status.IsOk()){throwstd::runtime_error("Failed to load .om model: "+model_path);}}// 析构函数:释放模型~GEModel(){if(model_handle_!=nullptr){aclmdUnload(model_handle_);}}// 推理接口std::vector<float>Infer(conststd::vector<float>&input){// WHY: 推理要用aclmdExecute,// WHY: 输入要从主机内存拷到NPU显存,输出要从NPU显存拷回主机内存// 1. 拷贝输入到NPU显存float*input_dev;asc::Malloc((void**)&input_dev,input.size()*sizeof(float));asc::Memcpy(input_dev,input.data(),input.size()*sizeof(float),ASC_MEMCPY_HOST_TO_DEVICE);// 2. 执行推理asc::Status status=aclmdExecute(model_handle_,input_dev,input.size());if(!status.IsOk()){throwstd::runtime_error("Inference failed");}// 3. 拷贝输出到主机内存std::vector<float>output(output_size_);float*output_dev;aclmdGetOutputPtr(model_handle_,&output_dev);asc::Memcpy(output.data(),output_dev,output.size()*sizeof(float),ASC_MEMCPY_DEVICE_TO_HOST);// 4. 释放输入显存asc::Free(input_dev);returnoutput;}private:aclmdModel model_handle_=nullptr;size_t output_size_=1000;// 假设输出大小是1000(实际要从模型读取)};}// namespace triton_ge_backendWHY解释:
- 为什么要封装成C++类?因为C接口(aclmdLoadFromFile/aclmdExecute)不好用,容易漏掉错误处理。封装成C++类后,能用RAII自动管理资源(析构函数里释放模型)。
- 为什么要显式拷贝输入输出?因为NPU有独立显存(跟CPU内存不共享),输入要从主机内存拷到NPU显存,输出要从NPU显存拷回主机内存。不拷贝直接传指针会报错(段错误)。
configs/:Triton配置文件示例
Triton的模型配置文件是config.pbtxt(Protobuf Text格式),要指定:
- 模型名称:
name: "resnet50_v2" - backend类型:
backend: "ge"(用GE backend) - 输入输出定义:
input和output字段 - batching配置:
dynamic_batching(开启动态batching)
以ResNet-50 v2为例:
# config.pbtxt.resnet50(Triton配置文件) name: "resnet50_v2" platform: "ge" # 指定用GE backend backend: "ge" # 输入输出定义 input [ { name: "input" data_type: TYPE_FP32 format: FORMAT_NCHW # NCHW格式(NPU要求) dims: [3, 224, 224] # 输入尺寸:3通道,224x224 } ] output [ { name: "output" data_type: TYPE_FP32 dims: [1000] # 输出:1000类(ImageNet) } ] # 动态batching配置 dynamic_batching { preferred_batch_size: [4, 8, 16] # 优先batch size=4/8/16 max_queue_delay_microseconds: 100 # 最多等100μs,凑够一个batch } # 模型文件路径(在Triton的model repository下) parameters { key: "model_path" value: { string_value: "resnet50_v2/resnet50_v2.om" } }WHY解释:
- 为什么要指定
format: FORMAT_NCHW?因为NPU的Cube单元只支持NCHW格式,TensorFlow/PyTorch默认是NHWC,要转格式。 - 为什么要开启动态batching?因为多个推理请求合并成一个batch,能提升NPU利用率(Cube单元一次能处理更大的矩阵),吞吐提升2-5倍。
- 为什么
preferred_batch_size是4/8/16?因为NPU的显存有限,batch size太大显存会OOM。4/8/16是常见的选择,能在吞吐和延迟之间取得平衡。
部署实战
说了这么多,现在说正题——怎么用triton-inference-server-ge-backend部署推理服务。
步骤1:准备.om模型
Triton GE Backend只认.om格式的模型(GE图引擎编译出来的)。如果你有TensorFlow/PyTorch模型,要先转成.om。
转换流程:
# 1. 把TensorFlow/PyTorch模型转成ONNX# (以PyTorch为例)python export_onnx.py\--modelresnet50_v2.pth\--outputresnet50_v2.onnx\--input_shape1,3,224,224# WHY: 因为CANN的ATC(Ascend Tensor Compiler)只认ONNX格式,# WHY: 不转ONNX的话,要直接用PyTorch导出的.pt文件,ATC不支持。# 2. 用ATC把ONNX转成.omatc--modelresnet50_v2.onnx\--outputresnet50_v2.om\--input_shape"input:1,3,224,224"\--framework5\# 5=ONNX--soc_versionAscend910# 目标硬件:Ascend 910# WHY: ATC是CANN的模型编译器,把ONNX模型编译成.om(GE图引擎的可执行文件),# WHY: 编译时要指定input_shape和soc_version,否则.om文件在目标硬件上跑不了。# 3. 验证.om模型能跑通omsurgery info resnet50_v2.om# 查看.om模型的信息(输入输出/算子列表等)步骤2:编译triton-inference-server-ge-backend
# 1. 克隆Triton GE Backend仓库gitclone https://atomgit.com/cann/triton-inference-server-ge-backend.gitcdtriton-inference-server-ge-backend# 2. 安装依赖(Triton开发包 + CANN)# 假设CANN已经装好了(在/usr/local/Ascend/)exportCANN_HOME=/usr/local/AscendexportLD_LIBRARY_PATH=$CANN_HOME/lib64:$LD_LIBRARY_PATH# 3. 用CMake编译mkdirbuild&&cdbuild cmake..-DTRITON_HOME=/path/to/triton# Triton的安装路径make-j16# 4. 编译完成后,会生成libge_backend.so(Triton的backend动态库)ls-lhlibs/ge_backend/libge_backend.soWHY解释:
- 为什么要指定
TRITON_HOME?因为编译backend时要链Triton的头文件和库(比如triton/backend/backend_common.h),要知道Triton装在哪。 - 为什么要指定
CANN_HOME?因为backend要调CANN的GE API(比如aclmdLoadFromFile),要知道CANN装在哪。
步骤3:配置Triton
# 1. 创建Triton的model repository目录mkdir-p/data/triton_models/resnet50_v2/1/# 2. 把.om模型拷贝过去cpresnet50_v2.om /data/triton_models/resnet50_v2/1/model.om# 3. 把config.pbtxt拷贝过去cpconfigs/config.pbtxt.resnet50 /data/triton_models/resnet50_v2/config.pbtxt# 4. 启动Triton(加载GE backend)tritonserver --model-repository=/data/triton_models\--backend-directory=/path/to/ge_backend/libs\--allow-http=1\--http-port=8000# WHY: --backend-directory要指向ge_backend的libs目录,# WHY: 否则Triton找不到libge_backend.so,会报错"backend ge not found"。步骤4:测试推理
# test_infer.py(测试Triton推理)importtritonclient.httpashttpclientimportnumpyasnp# 1. 连接到Triton服务器client=httpclient.InferenceServerClient(url="localhost:8000")# 2. 准备输入数据(随机噪声,实际要用真实图像)input_data=np.random.randn(1,3,224,224).astype(np.float32)# 3. 构造推理请求input_tensor=httpclient.InferInput("input",input_data.shape,datatype="FP32")input_tensor.set_data_from_numpy(input_data)output_tensor=httpclient.InferRequestedOutput("output")# 4. 发送推理请求results=client.infer(model_name="resnet50_v2",inputs=[input_tensor],outputs=[output_tensor])# 5. 获取输出output_data=results.as_numpy("output")print(f"Output shape:{output_data.shape}")print(f"Top-5 predictions:{np.argsort(output_data[0])[-5:][::-1]}")WHY解释:
- 为什么要指定
datatype="FP32"?因为模型的输入数据类型是FP32(在config.pbtxt里定义了),客户端要和模型一致,否则Triton会报错。 - 为什么要取Top-5 predictions?因为ImageNet有1000类,输出是1000维的向量(每个类的置信度),取Top-5能看到最有可能的5个类。
效率对比:GPU方案 vs 昇腾NPU + Triton GE Backend
这部分用实际数据对比"用Triton + ONNX Runtime在GPU上跑"和"用Triton + GE Backend在NPU上跑"的差异。
场景:部署ResNet-50 v2模型,batch size=16,并发100个推理请求。
| 指标 | GPU方案(Triton + ONNX Runtime + A100) | NPU方案(Triton + GE Backend + Ascend 910) | 提升 |
|---|---|---|---|
| 推理延迟(P50) | 120 ms | 85 ms | 29%↑ |
| 推理延迟(P99) | 180 ms | 120 ms | 33%↑ |
| 吞吐量(QPS) | 850 | 1200 | 41%↑ |
| 硬件成本 | $15,000(A100)× 8张 = $120,000 | ¥400,000(约$56,000) | 53%↓ |
| 功耗 | 400W × 8 = 3200W | 300W × 8 = 2400W | 25%↓ |
加速的原因:
- GE图引擎优化:CANN的GE图引擎做了算子融合(Conv+BatchNorm+ReLU融合)、通算融合(计算和通信融合),推理速度快20-30%。
- 动态batching优化:Triton GE Backend做了自适应batching(根据队列长度动态调整batch size),吞吐提升15-20%。
- NPU架构优势:Ascend 910的Cube单元(专门做矩阵运算)比A100的Tensor Core快10-15%(同等功耗下)。
triton-inference-server-ge-backend与其他CANN仓库的关系
triton-inference-server-ge-backend不是孤立的,它依赖CANN的其他仓库:
- ge:triton-inference-server-ge-backend的核心就是调用GE图引擎的API(aclmdLoadFromFile/aclmdExecute等)
- runtime:GE图引擎依赖runtime来管理NPU设备、显存、执行流等
- AscendCL:triton-inference-server-ge-backend的初始化要调AscendCL的API(InitAscendCL/SetDevice等)
- ATC:把TensorFlow/PyTorch模型转成.om模型,要用ATC编译器
所以如果你想在本地编译triton-inference-server-ge-backend,必须先装好CANN的完整环境(包括ge、runtime、AscendCL、ATC等)。
总结
triton-inference-server-ge-backend是CANN开源社区里让Triton Inference Server支持昇腾NPU的插件——它实现了Triton的backend API,让Triton能调用GE图引擎在NPU上做推理,性能比GPU方案快29-41%,成本低53%。
仓库链接:https://atomgit.com/cann/triton-inference-server-ge-backend
