跳到主要内容

模型集成

本章以 ONNX Runtime 1.23.0 + ep 镜像为例,介绍在容器内将 ONNX 模型接入 ONNX Runtime-MUSA 的方式,以及上线前的两项验证。Python 与 C++ 的接入方式分节说明,验证方法两者通用。

挂载自有工程​

将模型与工程目录挂载进容器:

mkdir -p work

docker run -d \
-e MTHREADS_VISIBLE_DEVICES=0 \
--network host \
-v "$PWD/work:/workspace" -w /workspace \
--name ort-service \
[DOCKER_IMAGE_ADDRESS] \
sleep infinity

宿主机当前目录下的 work 用于存放模型与工程,容器内对应 /workspace。

注意:请先创建该目录再启动容器。目录不存在时 docker 会以 root 身份自动创建它,宿主机上的普通用户之后无法向其中写入。

Python 接入​

接入方式与上游 ONNX Runtime 一致,差异仅在于构造 InferenceSession 时将 MUSAExecutionProvider 置于 providers 列表首位。模型本身无需修改。

import onnxruntime as ort
import numpy as np

session = ort.InferenceSession(
"your_model.onnx",
providers=[
("MUSAExecutionProvider", {"device_id": "0"}),
"CPUExecutionProvider",
],
)

inp = session.get_inputs()[0]
x = np.random.default_rng(0).standard_normal(
[d if isinstance(d, int) else 1 for d in inp.shape]
).astype(np.float16 if "float16" in inp.type else np.float32)

outputs = session.run(None, {inp.name: x})
print("output:", outputs[0].shape)

将 "your_model.onnx" 替换为实际模型路径后即可运行。

列表中的 CPUExecutionProvider 用于个别算子未被支持时由引擎自动改用 CPU 执行。

上例只设置了 device_id,这是基础接入配置。prefer_nhwc、allow_tf32 等性能开关的收益与模型强相关,请按性能特性开关完成实测对比后再决定是否启用。

首次运行包含初始化开销,测量延迟前先执行 2 至 3 次预热再统计。

输入输出适配​

上述代码为单输入模板。其他情形按下表在业务侧适配:

情形适配方式
多输入遍历 session.get_inputs(),按各输入的 name、shape、type 分别构造数组,以 session.run(None, {name1: x1, name2: x2, ...}) 执行
fp16 输入输出输入数组的 dtype 使用 np.float16。注意示例模型 mobilenet_v2_fp16.onnx 为 fp16 权重、fp32 输入输出
动态维度模板中将动态维度置 1 仅为占位。实际须按业务显式固定 batch、分辨率等维度,而非置 1
多输出session.run(None, feeds) 按 session.get_outputs() 的顺序返回列表,逐个按各自的 dtype 与 shape 读取

图像缩放、归一化、检测框解码等前后处理在业务侧完成,与推理过程解耦。

C++ 接入​

接入自有工程​

镜像内的 C++ 运行库位于 /opt/onnxruntime-musa。将下列片段加入工程的 CMakeLists.txt,your_target 替换为工程中的可执行或库 target:

set(ORTMUSA_ROOT "/opt/onnxruntime-musa" CACHE PATH "OrtMusa C++ runtime root")

find_library(location_onnxruntime onnxruntime
PATHS "${ORTMUSA_ROOT}/lib" NO_DEFAULT_PATH REQUIRED)
add_library(onnxruntime SHARED IMPORTED)
set_target_properties(onnxruntime PROPERTIES
IMPORTED_LOCATION ${location_onnxruntime}
INTERFACE_INCLUDE_DIRECTORIES "${ORTMUSA_ROOT}/include")

target_link_libraries(your_target PRIVATE onnxruntime)
target_compile_features(your_target PRIVATE cxx_std_17)

set_target_properties(your_target PROPERTIES
BUILD_RPATH "${ORTMUSA_ROOT}/lib"
INSTALL_RPATH "${ORTMUSA_ROOT}/lib")

完整的可编译工程见镜像内的 /opt/onnxruntime-musa/demo/cpp/CMakeLists.txt。

注意:libonnxruntime_providers_musa.so 无需显式链接,它在创建 session 时由引擎按 MUSAExecutionProvider 动态加载,与 libonnxruntime.so 位于同一目录即可。

推理代码​

#include <onnxruntime_cxx_api.h>
#include <musa_provider_options.h>

Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "app");
Ort::SessionOptions so;

OrtMUSAProviderOptions musa{}; // 花括号不可省略
musa.device_id = 0;
so.AppendExecutionProvider_MUSA(musa);

Ort::Session session(env, "your_model.onnx", so);
// 构造输入 CPU tensor → session.Run(...) → 读取输出

完整可运行版本见镜像内的 /opt/onnxruntime-musa/demo/cpp/main.cc,其中包含从 session 自动查询输入输出名称与形状、构造 tensor、执行与读取输出的全过程。

注意:OrtMUSAProviderOptions musa{}; 的花括号不可省略。省略后各字段为未初始化值,行为不确定。各字段的含义与默认值见性能特性开关。

输入输出的适配方式与 Python 同理,对应的 C++ API 为:

情形关键 API
多输入遍历 session.GetInputCount(),逐输入取 GetInputTypeInfo(i) 的 name、shape、dtype 各建一个 tensor,Run 传入名称数组与 tensor 数组
fp16 输入输出用 std::vector<Ort::Float16_t> 缓冲配合 CreateTensor<Ort::Float16_t>()。不要使用 CreateTensor<uint16_t>(),它创建的是 UINT16 tensor 而非 FLOAT16
动态维度须按业务在构造输入 shape 时显式固定 batch、分辨率等维度
输出读取Run 返回的 Ort::Value 用 GetTensorTypeAndShapeInfo().GetShape() 取形状、GetTensorData<T>() 取数据指针,T 与输出 dtype 一致

精度验证​

注意

上线前须对自研模型执行精度验证。

同一模型、同一输入,分别以纯 CPUExecutionProvider 与 MUSAExecutionProvider 各建一个 session 执行一次,对比两者输出。判定标准为余弦相似度不低于 0.999(该指标与数值尺度无关),且 MUSA 侧输出非全零。

Python​

将下列脚本中的 MODEL 替换为实际模型路径后运行,多输入模型按注释补齐 feed:

import numpy as np, onnxruntime as ort

MODEL = "your_model.onnx"

def run(providers):
s = ort.InferenceSession(MODEL, providers=providers)
inp = s.get_inputs()[0]
shape = [d if isinstance(d, int) else 1 for d in inp.shape] # 动态维度置 1 为占位,按业务修改
x = np.random.default_rng(42).standard_normal(shape).astype(
np.float16 if "float16" in inp.type else np.float32)
return s.run(None, {inp.name: x}) # 多输入时改为 {n1: x1, n2: x2, ...}

musa = run([("MUSAExecutionProvider", {"device_id": "0"}), "CPUExecutionProvider"])
cpu = run(["CPUExecutionProvider"])
for i, (m, c) in enumerate(zip(musa, cpu)):
m, c = m.ravel().astype(np.float64), c.ravel().astype(np.float64)
cos = float(m @ c / (np.linalg.norm(m) * np.linalg.norm(c) + 1e-12))
print(f"output[{i}] cosine={cos:.8f} musa_all_zero={not np.any(m)}")
assert cos >= 0.999 and np.any(m), f"output[{i}] 精度不达标(cosine={cos:.8f})"
print("PASS: MUSA vs CPU 精度验证通过")

预期输出末行:

PASS: MUSA vs CPU 精度验证通过

注意:脚本在不达标时会以 AssertionError 中止并打印该输出的 cosine 值。

C++​

下列 precision_check.cc 适用于单输入、fp32 输入输出的模型:

// 用法: ./precision_check <model.onnx> [device_id]
#include <onnxruntime_cxx_api.h>
#include <musa_provider_options.h>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <random>
#include <vector>

static std::vector<Ort::Value> Run(const char* model, bool use_musa, int device_id,
const std::vector<float>& input,
const std::vector<int64_t>& shape, Ort::Env& env) {
Ort::SessionOptions so;
if (use_musa) {
OrtMUSAProviderOptions musa{};
musa.device_id = device_id;
so.AppendExecutionProvider_MUSA(musa);
}
Ort::Session session(env, model, so);
Ort::AllocatorWithDefaultOptions alloc;
auto in_name = session.GetInputNameAllocated(0, alloc);
auto out_name = session.GetOutputNameAllocated(0, alloc);
const char* in_names[] = {in_name.get()};
const char* out_names[] = {out_name.get()};
auto mem = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
auto tensor = Ort::Value::CreateTensor<float>(mem, const_cast<float*>(input.data()),
input.size(), shape.data(), shape.size());
return session.Run(Ort::RunOptions{nullptr}, in_names, &tensor, 1, out_names, 1);
}

int main(int argc, char** argv) {
if (argc < 2) { std::cerr << "用法: " << argv[0] << " <model.onnx> [device_id]\n"; return 2; }
const char* model = argv[1];
int device_id = argc >= 3 ? std::atoi(argv[2]) : 0;
Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "precision_check");

std::vector<int64_t> shape;
{ Ort::SessionOptions so; Ort::Session s(env, model, so);
shape = s.GetInputTypeInfo(0).GetTensorTypeAndShapeInfo().GetShape();
for (auto& d : shape) if (d < 0) d = 1; } // 动态维度置 1 为占位,按业务修改
size_t n = 1; for (auto d : shape) n *= static_cast<size_t>(d);

std::vector<float> input(n);
std::mt19937 rng(42);
std::normal_distribution<float> dist(0.f, 1.f);
for (auto& v : input) v = dist(rng);

auto musa_out = Run(model, true, device_id, input, shape, env);
auto cpu_out = Run(model, false, device_id, input, shape, env);

bool ok = true;
for (size_t i = 0; i < musa_out.size(); ++i) {
size_t cnt = musa_out[i].GetTensorTypeAndShapeInfo().GetElementCount();
const float* m = musa_out[i].GetTensorData<float>();
const float* c = cpu_out[i].GetTensorData<float>();
double dot = 0, nm = 0, nc = 0; bool any = false;
for (size_t k = 0; k < cnt; ++k) {
dot += double(m[k]) * c[k]; nm += double(m[k]) * m[k]; nc += double(c[k]) * c[k];
if (m[k] != 0.f) any = true;
}
double cos = dot / (std::sqrt(nm) * std::sqrt(nc) + 1e-12);
std::cout << "output[" << i << "] cosine=" << cos
<< " musa_all_zero=" << (any ? "false" : "true") << "\n";
if (cos < 0.999 || !any) ok = false;
}
std::cout << (ok ? "PASS: MUSA vs CPU 精度验证通过\n" : "FAIL: 精度不达标\n");
return ok ? 0 : 1;
}

编译方式:新建一个目录,将上述代码保存为其中的 precision_check.cc,再以镜像内示例的 CMakeLists.txt 为模板生成构建文件并编译运行:

mkdir -p /workspace/precision_check && cd /workspace/precision_check
# 将上述代码保存为本目录下的 precision_check.cc 后执行:
sed -e 's/main\.cc/precision_check.cc/' -e 's/ort_musa_cpp_demo/precision_check/g' \
/opt/onnxruntime-musa/demo/cpp/CMakeLists.txt > CMakeLists.txt
cmake -S . -B build
cmake --build build -j4
./build/precision_check /opt/onnxruntime-musa/demo/cpp/models/mobilenet_v2_fp16.onnx 0

./build/precision_check 的第一个参数替换为实际模型路径即可验证自研模型。

预期输出末行:

PASS: MUSA vs CPU 精度验证通过

注意:不达标时程序打印 FAIL: 精度不达标 并以退出码 1 结束,同时逐输出打印 cosine 值。

出现 musa_all_zero=True(C++ 为 true)或 cosine 明显偏低时,请按常见问题提供的信息联系摩尔线程技术支持。

确认节点在 GPU 上执行​

providers 中含 MUSAExecutionProvider 只表示 Execution Provider 已加载。模型中个别算子未被支持时,相关节点会由引擎自动改用 CPU 执行,此时 providers 列表不变。

如需确认模型的全部节点都在 GPU 上执行,创建 session 时禁用 CPU 回退。Python 写法如下,providers 只传 MUSAExecutionProvider:

import numpy as np, onnxruntime as ort

so = ort.SessionOptions()
so.add_session_config_entry("session.disable_cpu_ep_fallback", "1")
session = ort.InferenceSession(
"your_model.onnx",
sess_options=so,
providers=[("MUSAExecutionProvider", {"device_id": "0"})],
)
print("session created")

inp = session.get_inputs()[0]
x = np.random.default_rng(0).standard_normal(
[d if isinstance(d, int) else 1 for d in inp.shape]
).astype(np.float16 if "float16" in inp.type else np.float32)

outputs = session.run(None, {inp.name: x})
print("output:", outputs[0].shape)
print("PASS: all nodes executed with CPU fallback disabled")

预期输出:

session created
output: (1, 1000)
PASS: all nodes executed with CPU fallback disabled

注意:output 的形状随模型不同,上例取自快速开始的示例模型。存在无法在 GPU 上执行的节点时,session 创建阶段即报错,看不到 session created;能够创建成功并完整跑完一次推理、打印出末行,才表示全部节点都在 GPU 上执行。

C++ 写法为在创建 session 前加入:

so.AddConfigEntry("session.disable_cpu_ep_fallback", "1");

注意:禁用回退的同时在 providers 中显式列出 CPUExecutionProvider 会导致配置冲突并报 INVALID_ARGUMENT : Conflicting session configuration,因此只传 MUSAExecutionProvider。

该配置用于确认节点分布,不作为常规运行配置。若某模型启用后报错,关闭该配置即可正常运行,相关节点将由引擎自动改用 CPU 执行。

IOBinding 使用约束​

本节仅适用于通过 IOBinding(Python io_binding(),C++ Ort::IoBinding)将输入绑定到设备内存的工程。使用普通 run() / Run() 传入 host 侧数据的写法不受影响。

注意

当绑定的内存位置与该输入在执行计划中的目标位置不一致时,绑定保存的是绑定那一刻的一份拷贝,不是对该内存的引用。

绑定之后再原地更新这块内存,引擎不会看到更新,且不会报错。典型症状是每轮输出完全相同且与输入无关。

这是 ONNX Runtime 的通用行为,各 Execution Provider 一致,见上游 OrtIoBinding 文档。一个输入的目标位置可能在 host 内存上,常见于两种情形:该输入被当作控制参数使用(例如 Reshape 的 shape、Slice 的 starts/ends);或者消费该输入的某个节点由引擎安排在 CPU 上执行。

判定方法:绑定一次 → 执行一次 → 原地更新该输入的内存、不重新绑定 → 再执行一次。两次输出相同,即该输入保存的是拷贝。更新时选一个会改变输出的取值。

处理方式:该输入内容变化后重新绑定一次。这种写法对任何输入都成立。