C 高性能推理引擎实战用 ONNX Runtime 把模型推理延迟压到 10ms 以下前言最近在做一个实时风控系统需要对每笔交易做 AI 模型推理。业务要求单次推理延迟必须控制在 15ms 以内。第一版直接用 Python FastAPI 跑延迟 80ms。换成 C ONNX Runtime 之后压到了 8ms。这篇文章记录整个优化过程。不讲废话直接上数据。一、为什么选 ONNX Runtime1.1 推理框架对比先看数据。同一个 BERT-base 模型相同硬件Intel Xeon 8375C32核不同框架的推理延迟框架平均延迟P99 延迟吞吐量 (QPS)Python PyTorch82ms120ms12TorchScript45ms68ms22ONNX Runtime (Python)28ms42ms35ONNX Runtime (C)8ms12ms125TensorRT6ms9ms166ONNX Runtime C 版本的性能已经非常接近 TensorRT但部署复杂度低很多。对于大多数场景这就是最优解。1.2 架构总览graph TD A[模型训练 PyTorch] -- B[导出 ONNX 格式] B -- C[ONNX Runtime C 加载] C -- D{推理优化} D -- E[图优化 Graph Optimization] D -- F[算子融合 Operator Fusion] D -- G[内存预分配 Memory Arena] D -- H[线程池配置 Thread Pool] E -- I[生产部署] F -- I G -- I H -- I I -- J[延迟 10ms ✅]二、快速上手2.1 环境准备# 下载 ONNX Runtime C 预编译包Linux x64 wget https://github.com/microsoft/onnxruntime/releases/download/v1.18.0/onnxruntime-linux-x64-1.18.0.tgz tar -xzf onnxruntime-linux-x64-1.18.0.tgz # CMakeLists.txt 中链接 # find_package(onnxruntime REQUIRED) # target_link_libraries(你的目标 onnxruntime)2.2 最小可运行示例#include onnxruntime_cxx_api.h #include iostream #include vector #include chrono int main() { // 创建运行环境 Ort::Env env(ORT_LOGGING_LEVEL_WARNING, 风控推理引擎); Ort::SessionOptions session_options; // 核心优化开启图优化 session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); // 加载模型 Ort::Session session(env, 风控模型.onnx, session_options); // 准备输入数据模拟一笔交易的特征向量 std::vectorfloat input_data(128, 0.5f); // 128维特征 std::vectorint64_t input_shape {1, 128}; // 创建输入张量 auto memory_info Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); Ort::Value input_tensor Ort::Value::CreateTensorfloat( memory_info, input_data.data(), input_data.size(), input_shape.data(), input_shape.size() ); // 执行推理并计时 const char* input_names[] {输入特征}; const char* output_names[] {风险概率}; auto start std::chrono::high_resolution_clock::now(); auto output session.Run( Ort::RunOptions{nullptr}, input_names, input_tensor, 1, output_names, 1 ); auto end std::chrono::high_resolution_clock::now(); // 输出结果 float* result output[0].GetTensorMutableDatafloat(); auto duration std::chrono::duration_caststd::chrono::microseconds(end - start); std::cout 风险概率: result[0] std::endl; std::cout 推理延迟: duration.count() / 1000.0 ms std::endl; return 0; }跑一下大概 15ms 左右。还不够。继续压。三、核心优化从 15ms 压到 8ms3.1 优化一线程池精调默认情况下ONNX Runtime 会用所有 CPU 核心。但核心数越多不代表越快——线程切换和缓存失效会拖后腿。// 关键线程数不是越多越好 // 实测数据 // 32线程 - 15ms默认 // 16线程 - 12ms // 8线程 - 9ms最优 // 4线程 - 11ms // 2线程 - 18ms session_options.SetIntraOpNumThreads(8); // 算子内并行线程数 session_options.SetInterOpNumThreads(1); // 算子间并行线程数 // 经验法则IntraOp 线程数设为 物理核心数 / 4 // 原因避免超线程带来的缓存竞争⚠️踩坑提醒SetInterOpNumThreads设成 1 就好。多了反而引入调度开销。除非你的模型有大量可并行的独立子图否则别动这个参数。3.2 优化二内存分配策略每次推理都 malloc/free太慢了。用 Arena 预分配。// 启用内存 Arena预分配内存池 session_options.EnableMemPattern(); // 记忆内存使用模式 session_options.EnableCpuMemArena(); // 启用 CPU 内存 Arena // Arena 的工作原理 // 第一次推理时记录所有内存分配模式 // 后续推理直接从预分配池中获取 // 省掉了反复 malloc/free 的系统调用开销 // 实测这一项就省了 2ms3.3 优化三图优化 算子融合// 最高级别图优化包含算子融合 session_options.SetGraphOptimizationLevel( GraphOptimizationLevel::ORT_ENABLE_ALL ); // 可选将优化后的模型保存到磁盘 // 下次启动时直接加载优化后的版本跳过优化过程 session_options.SetOptimizedModelFilePath(风控模型_优化版.onnx);ORT_ENABLE_ALL 具体做了什么常量折叠把编译期能算出来的东西提前算好冗余节点消除去掉不影响输出的中间节点算子融合比如 Conv BN ReLU 合成一个算子减少内存拷贝布局优化自动选择对 CPU 缓存最友好的数据布局3.4 完整的生产级配置#include onnxruntime_cxx_api.h #include vector #include chrono #include iostream #include stdexcept class 推理引擎 { public: 推理引擎(const std::string 模型路径, int 线程数 8) { // 初始化环境 env_ std::make_uniqueOrt::Env( ORT_LOGGING_LEVEL_WARNING, 风控推理 ); // 配置会话参数 Ort::SessionOptions options; options.SetIntraOpNumThreads(线程数); options.SetInterOpNumThreads(1); options.SetGraphOptimizationLevel( GraphOptimizationLevel::ORT_ENABLE_ALL ); options.EnableMemPattern(); options.EnableCpuMemArena(); // 设置执行模式为顺序执行减少调度开销 options.SetExecutionMode(ExecutionMode::ORT_SEQUENTIAL); // 加载模型 session_ std::make_uniqueOrt::Session( *env_, 模型路径.c_str(), options ); // 预分配内存信息 memory_info_ Ort::MemoryInfo::CreateCpu( OrtArenaAllocator, OrtMemTypeDefault ); // 预热跑几次让 Arena 记住内存模式 预热(5); } float 推理(const std::vectorfloat 特征) { if (特征.size() ! 128) { throw std::invalid_argument(特征维度必须为128); } // 创建输入张量零拷贝直接引用外部数据 std::vectorint64_t shape {1, 128}; Ort::Value input Ort::Value::CreateTensorfloat( memory_info_, const_castfloat*(特征.data()), 特征.size(), shape.data(), shape.size() ); // 执行推理 const char* input_names[] {features}; const char* output_names[] {probability}; auto output session_-Run( Ort::RunOptions{nullptr}, input_names, input, 1, output_names, 1 ); return output[0].GetTensorMutableDatafloat()[0]; } private: void 预热(int 次数) { std::vectorfloat dummy(128, 0.0f); for (int i 0; i 次数; i) { 推理(dummy); } } std::unique_ptrOrt::Env env_; std::unique_ptrOrt::Session session_; Ort::MemoryInfo memory_info_{nullptr}; }; int main() { try { 推理引擎 engine(风控模型.onnx, 8); // 模拟 1000 次推理统计延迟 std::vectordouble latencies; std::vectorfloat 测试特征(128, 0.5f); for (int i 0; i 1000; i) { auto start std::chrono::high_resolution_clock::now(); float result engine.推理(测试特征); auto end std::chrono::high_resolution_clock::now(); double ms std::chrono::duration_caststd::chrono::microseconds( end - start ).count() / 1000.0; latencies.push_back(ms); } // 计算统计数据 std::sort(latencies.begin(), latencies.end()); double avg 0; for (auto l : latencies) avg l; avg / latencies.size(); std::cout 平均延迟: avg ms std::endl; std::cout P50 延迟: latencies[499] ms std::endl; std::cout P99 延迟: latencies[989] ms std::endl; std::cout 最大延迟: latencies.back() ms std::endl; } catch (const std::exception e) { std::cerr 推理失败: e.what() std::endl; return 1; } return 0; }四、性能对比结果优化前后的数据对比指标优化前默认配置优化后全链路调优提升幅度平均延迟15.2ms8.1ms↓ 46.7%P99 延迟23.4ms11.8ms↓ 49.6%吞吐量65 QPS123 QPS↑ 89.2%内存占用340MB280MB↓ 17.6%每一毫秒都有出处。别跟我说差不多就行。五、避坑指南5.1 模型导出的坑⚠️ PyTorch 导出 ONNX 时dynamic_axes别忘了设。否则 batch size 会被固死。# Python 侧导出这步别省 import torch model torch.load(风控模型.pth) model.eval() dummy_input torch.randn(1, 128) torch.onnx.export( model, dummy_input, 风控模型.onnx, input_names[features], output_names[probability], dynamic_axes{ features: {0: batch_size}, # batch 维度动态 probability: {0: batch_size} }, opset_version17 # 用最新的 opset算子融合效果更好 )5.2 线程亲和性在 NUMA 架构服务器上线程跨 NUMA 节点调度会导致延迟剧烈抖动。# 绑定到 NUMA node 0 的前 8 个核心 numactl --cpunodebind0 --membind0 ./推理服务 # 或者在代码中用 pthread_setaffinity_np 手动绑核5.3 批量推理如果场景允许攒批延迟换吞吐很划算Batch Size单条延迟吞吐量18ms125 QPS412ms333 QPS818ms444 QPS1628ms571 QPS六、总结三个核心优化点线程数精调不是越多越好物理核心数 / 4 是个好起点内存 Arena预分配干掉 malloc/free 的系统调用图优化全开算子融合 常量折叠 布局优化从 Python 的 80ms 到 C 的 8ms10 倍提升。每一毫秒都是真金白银。