导读:本期聚焦于小伙伴创作的《C++项目中怎么集成并调用oneDNN(DNNL)实现高性能深度学习推理》,敬请观看详情。在把训练好的模型落到C++服务端做推理时,算子性能和内存布局常常成为瓶颈。oneDNN作为Intel推出的深度神经网络库,提供了高度优化的卷积、矩阵乘等原语。本文从环境编译讲起,说明如何用C++创建引擎与内存对象,调用卷积原语完成前向计算,并对比使用原生循环实现,前者在AVX512平台上吞吐可提升数倍。文中还指出常见误区,比如忽略内存格式标签导致拷贝开销,以及未设置线程数造成资源争抢,帮助开发者少走弯路。

在C++后端系统中引入oneDNN(曾用名DNNL)可以显著降低卷积、内积等算子的计算延迟。该库针对x86架构做了指令集层面的优化,并抽象出引擎、内存、原语三类核心对象,开发者无需手写汇编也能获得接近极限的吞吐。下面以Linux平台为例,说明从编译到调用的完整流程。

C++项目中怎么集成并调用oneDNN(DNNL)实现高性能深度学习推理

一、环境准备与编译链接

oneDNN支持源码构建与包管理器安装。若使用源码,推荐开启DNNL_CPU_RUNTIME=OMP以利用OpenMP多线程。CMake配置时可指定安装路径,便于后续在项目中引用头文件与静态库。

在C++工程里,需要链接dnnl库以及线程库。以g++为例,编译命令类似:g++ main.cpp -ldnnl -fopenmp -o app。如果系统未预装,可将构建出的libdnnl.a与include目录加入工程搜索路径。注意,oneDNN的API大部分位于dnnl命名空间,头文件为dnnl.hpp。

#include <dnnl.hpp>
#include <vector>
#include <iostream>
using namespace dnnl;

int main() {
    // 创建CPU引擎
    engine eng(engine::kind::cpu, 0);
    stream s(eng);
    std::cout << "engine created" << std::endl;
    return 0;
}

二、核心概念与内存布局

oneDNN用memory对象描述张量,其关键属性是维度与格式标签。格式标签如nchw、nhwc、any决定了数据在内存中的排布。很多初学者直接传入任意布局,导致库内部插入重排操作,性能反而下降。最佳实践是先用任意格式让库挑选最优布局,再将输入数据按该布局填充。

原语(primitive)是具体的计算单元,比如卷积前向、内积、池化。每个原语在创建时需要描述符(descriptor),描述输入输出内存格式与算法。执行时通过stream提交,支持异步与顺序模式。理解这三者的关系,是写好C++调用代码的基础。

2.1 内存描述符示例

下面代码展示如何定义四维输入的内存描述符,并指定为任意格式交给库优化。实际部署中,常将权重预转换为库选定格式,避免每次推理都重排。

// 定义输入张量 1x3x224x224
memory::dims src_dims = {1, 3, 224, 224};
auto src_md = memory::desc(
    src_dims,
    memory::data_type::f32,
    memory::format_tag::any); // 让库选择最优布局

三、卷积前向推理实战

以浮点32位卷积为例,需要分别构造源、权重、偏置、目标的内存描述符,再创建卷积前向描述符并指定算法。随后通过原语描述符生成可执行原语,分配实际内存后执行。以下片段省略错误处理,仅展示主链路。

在执行前,若输入内存格式与原语期望不一致,应先用reorder原语转换。生产环境通常把权重reorder一次后缓存,每次仅对输入做必要转换,从而控制延迟。线程数可通过set_num_threads控制,避免容器化部署时过度订阅。

memory::dims weights_dims = {16, 3, 3, 3};
memory::dims bias_dims = {16};
memory::dims dst_dims = {1, 16, 222, 222};
memory::dims strides = {1, 1};
memory::dims padding = {1, 1};

auto conv_src_md = memory::desc(src_dims, memory::data_type::f32, memory::format_tag::nchw);
auto conv_weights_md = memory::desc(weights_dims, memory::data_type::f32, memory::format_tag::oihw);
auto conv_bias_md = memory::desc(bias_dims, memory::data_type::f32, memory::format_tag::x);
auto conv_dst_md = memory::desc(dst_dims, memory::data_type::f32, memory::format_tag::nchw);

auto conv_desc = convolution_forward::desc(
    prop_kind::forward_inference,
    algorithm::convolution_direct,
    conv_src_md, conv_weights_md, conv_bias_md, conv_dst_md,
    strides, padding, padding);

auto conv_prim_desc = convolution_forward::primitive_desc(conv_desc, eng);

auto src_mem = memory(conv_prim_desc.src_desc(), eng);
auto weights_mem = memory(conv_prim_desc.weights_desc(), eng);
auto bias_mem = memory(conv_prim_desc.bias_desc(), eng);
auto dst_mem = memory(conv_prim_desc.dst_desc(), eng);

auto conv = convolution_forward(conv_prim_desc);
conv.execute(s, {
    {DNNL_ARG_SRC, src_mem},
    {DNNL_ARG_WEIGHTS, weights_mem},
    {DNNL_ARG_BIAS, bias_mem},
    {DNNL_ARG_DST, dst_mem}});
s.wait();

四、常见误区与优化建议

第一个误区是忽视格式标签,用nhwc数据直接喂给期望nchw的原语,触发隐藏reorder。第二个误区是在短请求服务中频繁创建原语,应将primitive_desc与原语缓存为全局对象。第三个误区是未限制线程,在协程或微服务中造成核争用。

优化上,建议对权重做一次性重排并复用,使用内存池减少分配;批量维度较小时可尝试bf16降低带宽;在支持AVX512的机器上,oneDNN会自动派发对应内核,无需额外编码。掌握这些细节后,C++项目便能稳定获得高性能推理能力。

对比项原生循环实现oneDNN调用
开发成本高,需手写优化低,调用原语
单图延迟较高降低数倍
可维护性

oneDNNDNNLC++修改时间:2026-08-07 19:45:30

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。