首页
/ OpenCV DNN 的 MLAS 集成:vendored SGEMM 与 FlashAttention 内核的裁剪、补丁与构建全解

OpenCV DNN 的 MLAS 集成:vendored SGEMM 与 FlashAttention 内核的裁剪、补丁与构建全解

2026-09-06 12:00:36作者:邓越浪Henry

本文基于 3rdparty/mlas/README.md 展开,系统讲解 OpenCV 如何把 ONNX Runtime 的 MLAS(Microsoft Linear Algebra Subprograms)数学内核库裁剪为一个仅含 SGEMM 与 FlashAttention 的 vendored 子集,并将其编入 opencv_dnn 模块。读完后你能掌握:vendored 源码子集的取舍逻辑、五处本地补丁(含线程层替换为 cv::parallel_for_)的具体内容、CMake 架构探测与编译宏(HAVE_MLASMLAS_GEMM_ONLY 等)的作用机制,以及 DNN 各层(GEMM/MatMul/Attention/SDPA)调用 MLAS 的完整入口与回退路径。

1. MLAS 是什么,vendored 了哪些内容

MLAS 是一个包含处理器优化 GEMM 内核与平台特定线程代码的计算库,是 ONNX Runtime 内部默认使用的数学内核库(见 README 原文)。OpenCV 引入它的目的,是为 dnn 模块的前向计算提供两条高性能路径:单精度 SGEMM(覆盖全连接层、MatMul、线性投影)和融合多头注意力(FlashAttention,覆盖 Attention/SDPA 层),而不必把整个 MLAS 库拖进来。

1.1 版本溯源

README 完整记录了上游溯源信息,便于日后重新 vendoring 时对齐:

  • 上游来源:ONNX Runtime 的 onnxruntime/core/mlas/
  • 导入日期:2026-05-04;
  • 上游 commit62f742f1aa0c3102745ed35e3d869eaee845b9ac(2026-04-30,导入时 main 上最后一次触碰 MLAS 的提交,随 ORT v1.26.0 发布);
  • 许可证:MIT,许可证文本见 LICENSE

1.2 只导入 SGEMM 子集与 MlasFlashAttention

README 明确说明,vendored 的是 SGEMM(单精度 GEMM)子集加上 MlasFlashAttention(融合多头注意力),其余部分(量化 GEMM、卷积、FP16-dispatch SoftMax 等)全部排除——这样 DNN 模块拿到快的 SGEMM 与 FlashAttention 路径,而不会引入整库的构建负担。逐文件来看:

文件 作用
lib/sgemm.cpp SGEMM 调度与 host 侧粘合代码
lib/compute.cpp softmax / exp / row-max / sum-exp 内核。实际只运行 MlasReduceMaximumF32KernelMlasComputeSumExpF32Kernel 的可移植 C++ 回退版本,没有导入任何按架构特化的 .S softmax 内核(文件中 FP16/GQA 模板特化会编译但永不运行)
lib/flashattn.cpp MlasFlashAttention / MlasFlashAttentionThreaded 入口,依赖 sgemm.cpp 中的 MlasSgemmOperation 和上述两个可移植内核
lib/softmax.h compute.cpp 引用的头文件,仅含纯 FP16-dispatch typedef,在纯 FP32 构建下无害
lib/<arch>/ 各架构的 SGEMM 内核(见第 4 节)

顶层布局为:

  • inc/ — 公开 MLAS 头文件(inc/mlas.hinc/mlas_float16.h),从上游逐字保留;
  • lib/ — 实现,除第 3 节列出的本地补丁外逐字保留上游,按架构放在子目录(x86_64/aarch64/arm/power/riscv64/loongarch64/s390x/sve/kleidiai/);
  • CMakeLists.txt — OpenCV 侧构建粘合,构建 OBJECT 库 opencv_dnn_mlas,其目标文件直接链接进 opencv_dnn
  • threading_opencv.cpp — 对上游 lib/threading.cpp 的 OpenCV 侧替换(见第 3 节),携带 OpenCV 许可证头。

1.3 版权说明

大部分文件 © Microsoft Corporation,MIT 授权。lib/ 中部分上游贡献保留额外的 MIT 版权头,README 指出其中两个来源:lib/kleidiai/mlasi_kleidiai.h(© Arm Limited 2025)与 erf_neon_fp16.{h,cpp}gelu_neon_fp16.{h,cpp}(© FUJITSU LIMITED 2025,与 Microsoft 联合)。OpenCV 自己编写的三个文件(CMakeLists.txtthreading_opencv.cpp、README)采用 OpenCV 顶层 Apache 2.0 许可。

2. DNN 模块如何接入 MLAS

modules/dnn/CMakeLists.txt 中,DNN 模块的处理逻辑是:

  1. 先把 HAVE_MLAS 置 0,并清空 OPENCV_DNN_MLAS_ENABLEDOPENCV_DNN_MLAS_SKIP_REASON 等缓存变量(防止上一次配置残留);
  2. add_subdirectory(3rdparty/mlas)——只有当 3rdparty/mlas/CMakeLists.txt 成功走完时才会在父作用域设置 HAVE_MLAS 1MLAS_OBJECTSMLAS_INCLUDE_DIRS(见 CMakeLists.txt 尾部);
  3. HAVE_MLAS 为真,add_definitions(-DHAVE_MLAS=1) 并加入头文件目录,打印 “DNN: MLAS (vendored). enabled.”;否则打印 “disabled — host arch/OS not wired up.”;
  4. 最后通过 ocv_glob_module_sources(... ${MLAS_OBJECTS}) 把 OBJECT 库的目标文件并入 opencv_dnn 的编译列表(modules/dnn/CMakeLists.txt#L575)。

因此 HAVE_MLAS 就是编译期总开关:它驱动 mlas_gemm.hpp 提供真实实现或全部返回 false 的内联桩(见第 6 节)。

3. 五处本地补丁(re-vendor 时必须重放)

README 的 “Local patches against upstream” 一节是维护这套 vendored 代码的关键:每一处对上游的修改都在 patches/ 下存有 unified-diff 形式(与 3rdparty/zlib/patches 同一约定),重新导入上游后执行 git apply --directory=3rdparty/mlas patches/*.diff 即可恢复。五处修改逐一说明:

3.1 丢弃 lib/threading.cpp,改用 cv::parallel_for_

上游 lib/threading.cpp 的三个线程入口(MlasExecuteThreadedMlasTrySimpleParallelMlasTryBatchParallel)被整体移除(不 vendor),改为在 modules/dnn/src/layers/cpu_kernels/mlas_threading.cpp 中基于 cv::parallel_for_ 重新实现。从源码结构看这个替换是必要且巧妙的:

  • 上游线程文件在 BUILD_MLAS_NO_ONNXRUNTIMEThreadPool 为 nullptr 时退化为串行 for 循环;
  • 更关键的是 MLAS 内部会调用 MlasGetMaximumThreadCount() 决定分区数,standalone 构建下它返回 1——即使 MlasTrySimpleParallel 是并行的,最终也只发射一次迭代;
  • 所以补丁同时修了两半:入口函数改走 cv::parallel_for_mlas_threading.cpp#L44-L96,对 Iterations == 1 直接同步执行,否则切 cv::Range 分片),加上 3.2 中让线程数查询走 OpenCV。

由于该文件被 #if defined(MLAS_OPENCV_THREADING) 包裹(mlas_threading.cpp#L24),它只编译进 opencv_dnn_mlas OBJECT 库;当 opencv_dnn 的递归 src glob 捡到它时,因该宏不在其编译作用域内而成为空文件,避免重复符号。文件内还提供了一个针对 MLAS_GEMM_ONLY 的桩:mlasi.hMLAS_PLATFORM 结构体以 &MlasGemmQuantDispatchDefault 作为类内初始化器,但该真实定义在未编译的 qgemm_kernel_default.cpp 里,故此处声明一个零初始化实例保证可链接(mlas_threading.cpp#L31-L36)。

3.2 mlasi.h:线程数改由 OpenCV 提供,头文件路径改写

对应 patches/0001-mlasi-opencv-threading.diff

  • #include "core/mlas/inc/mlas.h"(ORT 树内路径)改写为 #include "../inc/mlas.h",因为 vendored 后该树内路径不存在;
  • 当定义了 MLAS_OPENCV_THREADING 时,MlasGetMaximumThreadCount() 返回 cv::getNumThreads()。补丁通过一个 extern "C" int opencv_dnn_mlas_max_threads(); 自由函数间接调用,避免把 <opencv2/core/utility.hpp> 拖进每个 MLAS 翻译单元;实现即 mlas_threading.cpp#L38-L42,对返回值做了 n > 0 ? n : 1 的保底。

这一补丁直接决定了第 6 节中 FlashAttention 的 thread_count 与 SGEMM 的 M 维分区都能按 OpenCV 的线程预算切分,而不是 ORT 的(或 standalone 的 1)。

3.3 platform.cpp:MLAS_GEMM_ONLY 裁剪构造函数

对应 patches/0002-platform-gemm-only.diff,是改动最大的一处:

  • 非 SGEMM 的调度字段全部包进 MLAS_GEMM_ONLY 门禁,使 SGEMM-only 子集无需其余 MLAS 源码即可构建;
  • 文件顶部的 erf_neon_fp16.h / gelu_neon_fp16.h 头文件包含也被 !defined(MLAS_GEMM_ONLY) 门禁,因为它们会传递引入未 vendored 的 FP16 源码(fp16_common.hsoftmax_kernel_neon.h);
  • MLAS_GEMM_ONLY 下的替换构造函数只赋值 sgemm.cpp 实际读取的四个调度字段:GemmFloatKernelKernelM1Routine(x86_64)、KernelM1TransposeBRoutine(x86_64)、TransposePackB16x4Routine(x86_64/loongarch);同时把 ReduceMaximumF32KernelComputeSumExpF32Kernel 指向 compute.cpp 中的可移植回退实现,让 MlasFlashAttention 在没有按架构 softmax 内核的情况下也能工作;
  • 原始完整 ORT 构造函数原样保留在 #else 分支中,方便未来 re-vendor。

从 diff 注释可以推断:在这个构建里调用任何非 SGEMM / 非 FlashAttention 的 MLAS API 都是未定义行为,这也与 README “只导入 SGEMM 子集”的边界一致。

3.4 mlas.h 与两个 ORT 内部 shim

4. 构建体系:架构探测、内核选择与编译宏

CMakeLists.txt 头部注释即是一份构建契约摘要:vendored 子集 = SGEMM + MlasFlashAttentionlib/platform.cpp 中非 SGEMM 调度行被 #if 0 掉(搜索 MLAS_GEMM_ONLY 可见),重新 vendor 后必须重放补丁。

4.1 构建标志

README 与 CMake 源码共同确认的编译定义:

  • HAVE_MLAS:由 CMakeLists.txt#L269set(HAVE_MLAS 1 PARENT_SCOPE) 交给 dnn 模块;
  • BUILD_MLAS_NO_ONNXRUNTIME=1MLAS_OPENCV_THREADING=1MLAS_GEMM_ONLY=1:作为私有编译宏挂在 OBJECT 库上(CMakeLists.txt#L243-L247);
  • 另有 OPENCV_DNN_MLAS_ENABLED / OPENCV_DNN_MLAS_SKIP_REASON 两个内部状态变量(CMakeLists.txt#L66-L72),用于在顶层配置摘要里标记 MLAS 是否启用、未启用时给出人读原因。

4.2 平台跳过与 ASM 编译器探测

两条硬性跳过规则值得注意:

  1. Windows 直接返回CMakeLists.txt#L74-L76)——WIN32 下不启用 MLAS,DNN 回退到内置 SGEMM;
  2. ASM 语言探测check_language(ASM) 在 Android NDK 工具链下是 no-op(NDK 为每个 ABI 预设了 CMAKE_ASM_COMPILER),随后 enable_language(ASM) 会在 NDK + CMake 3.22.1 组合下于 generate 阶段失败。因此需要汇编的架构(x86_64/x86/aarch64/loongarch64)在无 ASM 编译器时直接跳过,写入 skip 原因并告警(CMakeLists.txt#L78-L104);而 Android armv7a 仍可通过纯 C++ 的 sgemmc.cpp 路径启用。

4.3 各架构内核与编译标志

架构探测逻辑(CMakeLists.txt#L42-L62)依据 CMAKE_SYSTEM_NAME/CMAKE_SYSTEM_PROCESSOR 设置 MLAS_X86_64MLAS_ARM64 等标志,随后按架构选源文件并逐文件设置 ISA 编译标志:

架构 内核源文件 关键编译标志
x86_64 SSE2、AVX(含 M=1 快速路径与 TransposePackB16x4)、FMA3、AVX512F 共 8 个 .S 文件(CMakeLists.txt#L108-L131 -msse2 / -mavx / -mavx2 -mfma -mf16c / -mavx512f,逐文件经 set_source_files_properties 指定
32 位 x86 x86/SgemmKernelSse2.Sx86/SgemmKernelAvx.S;Android 额外加 x86.get_pc_thunk.S -msse2-mavx
ARM 32 位 纯 C++ 的 arm/sgemmc.cpp(无 .S 内核) 全局追加 -mfpu=neon
aarch64 aarch64/SgemmKernelNeon.Saarch64/SgemvKernelNeon.S
POWER power/SgemmKernelPower.cpp 恒编译;若编译器支持 -mcpu=power10 则追加 SgemmKernelPOWER10.cppSgemmKernelPackA.S(AIX 除外) -DSINGLE-O2 -mcpu=power10
loongarch64 LSX 与 LASX 两档 .S 内核及 TransposePackB16x4 变体 全局追加 -mlsx -mlasx
s390x s390x/SgemmKernelZVECTOR.cpp -DSINGLE-mvx -mzvector -march=z15
riscv64 通过 check_cxx_source_compiles-march=rv64gcv -mabi=lp64d 编译一段 __riscv_vsetvl_e32m1 测试代码探测 RVV,支持则编译 riscv64/sgemm_kernel_rvv.cppsgemm_pack_b_rvv.cpp,否则只有标量回退 逐文件 -march=rv64gcv -mabi=lp64d
WASM / 未知架构 标量路径 scalar/SgemmKernelScalar.cpp

此外还有两处 GCC/Clang 专属的“胶水”修复(CMakeLists.txt#L249-L267):

  • .S 汇编加 -Wa,--noexecstack,因为 MLAS 的 .S 文件缺少 .note.GNU-stack;C++ 侧用 -include cstring 强制包含;
  • platform.cppMlasInitAMX() 调用 syscall() 却只包含 <sys/syscall.h>(glibc 在 <unistd.h> 声明它),故在 x86/x86_64 非 Windows 下对该文件追加 -include unistd.h

5. DNN 调用入口:mlas_gemm 包装层

README “Caller in OpenCV” 一节指出,把 OpenCV GEMM 调度到 MLAS 的薄包装位于 mlas_gemm.hpp / mlas_gemm.cpp。它只包含公开头文件 mlas.h,且在 MLAS 不可用或请求的形状不受支持时,回退到既有的 fast_gemm 路径。包装层提供 6 个 API(mlas_gemm.hpp#L16-L93):

  • mlasAvailable():可用性探测。实现是一次性静态初始化,调用 MlasGetPreferredBufferAlignment() 并要求对齐值落在 (0, 256] 区间(mlas_gemm.cpp#L17-L23);
  • mlasSgemm():行主序 C := alpha*op(A)*op(B) + beta*C,内部组装 MLAS_SGEMM_DATA_PARAMS 后调用 MlasGemmmlas_gemm.cpp#L25-L56);
  • mlasSgemmBatch():批处理 SGEMM,M/N/K 与 leading dim 全批共享,用逐批元素偏移(A_offsets/B_offsets/C_offsets)从三个基址指针定位各批数据,对应 MlasGemmBatchmlas_gemm.cpp#L58-L96);
  • mlasSgemmPackBSize() / mlasSgemmPackB() / mlasSgemmPacked():B 矩阵“打包一次、多次复用”的三段式 API——先按 (trans_a, trans_b, N, K) 查询打包缓冲字节数,由调用方分配并执行打包,之后每次 mlasSgemmPacked 直接传入预打包 B(BIsPacked = trueldb 被置 0 忽略,mlas_gemm.cpp#L98-L155);
  • mlasFlashAttentionBufferBytesPerThread()mlasFlashAttention():见 5.1。

所有函数在 M <= 0 || N <= 0 || K <= 0 或 MLAS 不可用时返回 false,由上层触发 fast_gemm 回退;HAVE_MLAS 未定义时头文件提供全套返回 false 的 inline 桩(mlas_gemm.hpp#L95-L118),使调用方代码无需条件编译。

5.1 FlashAttention 的内存布局与 scratch 计算

mlas_gemm.hpp#L66-L93 的注释完整定义了语义:

  • 计算 output[b, i, h, :] = softmax(scale * Q[b,h,i,:] @ K[b,h,:,:]^T) @ V[b,h,:,:],融合进单个 tiled 内核,不物化 q_seq × kv_seq 的注意力矩阵;
  • 布局(行主序、FP32 连续):query[batch, num_heads, q_seq_len, qk_head_size]key[batch, num_heads, kv_seq_len, qk_head_size]value[batch, num_heads, kv_seq_len, v_head_size],而 output[batch, q_seq_len, num_heads, v_head_size]——注意输出头维在序列维之后,与输入不同;
  • scale 通常为 1/sqrt(qk_head_size)q_block_size / kv_block_size 是 q、kv 序列方向的 tile 大小(注释示例取 256);
  • scratch 必须是调用方拥有的、至少 thread_count * mlasFlashAttentionBufferBytesPerThread(...) 字节的缓冲;thread_count 是 MLAS 工作线程数(典型取值即 cv::getNumThreads())。

单线程 scratch 的公式实现为 (q*(2 + kv + vd)) * sizeof(float)mlas_gemm.cpp#L157-L170),注释解释了它对应 flashattn.cpp 中的排布:l[q] + m[q] + intermediate[q*kv] + temp_output[q*vd](row-max/row-sum 各一份、q×kv 中间注意力分块、q×vd 部分输出)。mlasFlashAttention 本身则把这些参数填入 MlasFlashAttentionThreadedArgs 并调用 MlasFlashAttention(&args, nullptr)mlas_gemm.cpp#L172-L211),ThreadPool 传 null——实际并行由第 3.1 节的 cv::parallel_for_ 实现承担。

5.2 DNN 各层对 MLAS 的落点

从源码可以确认四个层的落点(搜索 mlasAvailable 的结果):

  • GEMM 层gemm_layer.cpp 在权重侧走“预打包 B”快路径——当 mlasAvailable() && !opt.use_rvv 时先 mlasSgemmPackBSize 查询缓冲,分配后 mlasSgemmPackB 打包权重(gemm_layer.cpp#L298-L313),前向时对每个 batch 行块调用 mlasSgemmPackedgemm_layer.cpp#L489)。这与 5 节三段式 API 的设计意图吻合:B(权重)打包一次,多 batch 复用;
  • fast_gemm 回退路径fast_gemm.cpp#L604 在单 GEMM 处直接尝试 mlasSgemm;批处理路径(fast_gemm.cpp#L715-L724)先把 lda 约定翻译成 MLAS 的转置标志(lda1 == 1 即 A 视为行主、lda0 == 1 即转置),再调 mlasSgemmBatch,失败则回到 fast_gemm 内核;
  • Attention / SDPA 层attention_layer.cppsdpa_layer.cpp 引用了 mlasFlashAttention 路径,即第 5.1 节的融合注意力入口;
  • MatMul 层matmul_layer.cpp 经由 fast_gemm 间接受益于 MLAS 的 SGEMM/BatchGEMM。

6. 上游单元测试与许可边界

README 最后说明:SGEMM 内核的单元测试位于上游 ONNX Runtime 的 onnxruntime/test/mlas没有被 vendor 进本仓库;集成验证由 OpenCV 自己的 DNN 测试承担。这也给出了一个使用边界提示:若需要针对 MLAS 内核本身的独立回归,只能在上游语境中进行,本仓库内的可信度依据是 DNN 模块测试的整体行为。

综合来看,这套 vendored MLAS 的完整工作链是:modules/dnn/CMakeLists.txt 触发 3rdparty/mlas/CMakeLists.txt 的架构探测 → 成功则生成 opencv_dnn_mlas OBJECT 库(上游 platform.cpp/sgemm.cpp/compute.cpp/flashattn.cpp + OpenCV 的 mlas_threading.cpp)并定义 HAVE_MLAS → 编译期 mlas_gemm 提供 SGEMM/BatchGEMM/打包/FlashAttention 六个 API → 运行期 GEMM、MatMul、Attention、SDPA 层先探 mlasAvailable(),不可用或形状不支持时无缝回退 fast_gemm。任何一环失败(Windows、Emscripten、无 ASM 编译器)都不会使构建中断,只会让 DNN 退回既有 SGEMM 路径,这也是 README 中“host arch/OS not wired up”状态变量存在的原因。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388