OpenCV DNN 的 MLAS 集成:vendored SGEMM 与 FlashAttention 内核的裁剪、补丁与构建全解
本文基于 3rdparty/mlas/README.md 展开,系统讲解 OpenCV 如何把 ONNX Runtime 的 MLAS(Microsoft Linear Algebra Subprograms)数学内核库裁剪为一个仅含 SGEMM 与 FlashAttention 的 vendored 子集,并将其编入 opencv_dnn 模块。读完后你能掌握:vendored 源码子集的取舍逻辑、五处本地补丁(含线程层替换为 cv::parallel_for_)的具体内容、CMake 架构探测与编译宏(HAVE_MLAS、MLAS_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;
- 上游 commit:
62f742f1aa0c3102745ed35e3d869eaee845b9ac(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 内核。实际只运行 MlasReduceMaximumF32Kernel 与 MlasComputeSumExpF32Kernel 的可移植 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.h、inc/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.txt、threading_opencv.cpp、README)采用 OpenCV 顶层 Apache 2.0 许可。
2. DNN 模块如何接入 MLAS
在 modules/dnn/CMakeLists.txt 中,DNN 模块的处理逻辑是:
- 先把
HAVE_MLAS置 0,并清空OPENCV_DNN_MLAS_ENABLED、OPENCV_DNN_MLAS_SKIP_REASON等缓存变量(防止上一次配置残留); add_subdirectory(3rdparty/mlas)——只有当 3rdparty/mlas/CMakeLists.txt 成功走完时才会在父作用域设置HAVE_MLAS 1、MLAS_OBJECTS与MLAS_INCLUDE_DIRS(见 CMakeLists.txt 尾部);- 若
HAVE_MLAS为真,add_definitions(-DHAVE_MLAS=1)并加入头文件目录,打印 “DNN: MLAS (vendored). enabled.”;否则打印 “disabled — host arch/OS not wired up.”; - 最后通过
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 的三个线程入口(MlasExecuteThreaded、MlasTrySimpleParallel、MlasTryBatchParallel)被整体移除(不 vendor),改为在 modules/dnn/src/layers/cpu_kernels/mlas_threading.cpp 中基于 cv::parallel_for_ 重新实现。从源码结构看这个替换是必要且巧妙的:
- 上游线程文件在
BUILD_MLAS_NO_ONNXRUNTIME且ThreadPool为 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.h 的 MLAS_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.h、softmax_kernel_neon.h); MLAS_GEMM_ONLY下的替换构造函数只赋值sgemm.cpp实际读取的四个调度字段:GemmFloatKernel、KernelM1Routine(x86_64)、KernelM1TransposeBRoutine(x86_64)、TransposePackB16x4Routine(x86_64/loongarch);同时把ReduceMaximumF32Kernel与ComputeSumExpF32Kernel指向compute.cpp中的可移植回退实现,让MlasFlashAttention在没有按架构 softmax 内核的情况下也能工作;- 原始完整 ORT 构造函数原样保留在
#else分支中,方便未来 re-vendor。
从 diff 注释可以推断:在这个构建里调用任何非 SGEMM / 非 FlashAttention 的 MLAS API 都是未定义行为,这也与 README “只导入 SGEMM 子集”的边界一致。
3.4 mlas.h 与两个 ORT 内部 shim
- 对应 patches/0003-mlas-h-msc-ver-guard.diff:把 inc/mlas.h 中的
_MSC_VER检查包进defined(),避免 GCC/Clang 下-Wundef告警。 - lib/core/common/narrow.h 与 lib/core/common/common.h:MLAS 会调用的 ORT 内部头文件(上游只作为 ORT include 存在,MLAS 源码树里没有),因此这是新增文件而非编辑,无需
.diff。
4. 构建体系:架构探测、内核选择与编译宏
CMakeLists.txt 头部注释即是一份构建契约摘要:vendored 子集 = SGEMM + MlasFlashAttention,lib/platform.cpp 中非 SGEMM 调度行被 #if 0 掉(搜索 MLAS_GEMM_ONLY 可见),重新 vendor 后必须重放补丁。
4.1 构建标志
README 与 CMake 源码共同确认的编译定义:
HAVE_MLAS:由 CMakeLists.txt#L269 以set(HAVE_MLAS 1 PARENT_SCOPE)交给 dnn 模块;BUILD_MLAS_NO_ONNXRUNTIME=1、MLAS_OPENCV_THREADING=1、MLAS_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 编译器探测
两条硬性跳过规则值得注意:
- Windows 直接返回(CMakeLists.txt#L74-L76)——
WIN32下不启用 MLAS,DNN 回退到内置 SGEMM; - 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_64、MLAS_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.S、x86/SgemmKernelAvx.S;Android 额外加 x86.get_pc_thunk.S |
-msse2、-mavx |
| ARM 32 位 | 纯 C++ 的 arm/sgemmc.cpp(无 .S 内核) |
全局追加 -mfpu=neon |
| aarch64 | aarch64/SgemmKernelNeon.S、aarch64/SgemvKernelNeon.S | — |
| POWER | power/SgemmKernelPower.cpp 恒编译;若编译器支持 -mcpu=power10 则追加 SgemmKernelPOWER10.cpp 与 SgemmKernelPackA.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.cpp 与 sgemm_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.cpp的MlasInitAMX()调用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后调用MlasGemm(mlas_gemm.cpp#L25-L56);mlasSgemmBatch():批处理 SGEMM,M/N/K 与 leading dim 全批共享,用逐批元素偏移(A_offsets/B_offsets/C_offsets)从三个基址指针定位各批数据,对应MlasGemmBatch(mlas_gemm.cpp#L58-L96);mlasSgemmPackBSize()/mlasSgemmPackB()/mlasSgemmPacked():B 矩阵“打包一次、多次复用”的三段式 API——先按(trans_a, trans_b, N, K)查询打包缓冲字节数,由调用方分配并执行打包,之后每次mlasSgemmPacked直接传入预打包 B(BIsPacked = true时ldb被置 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 行块调用mlasSgemmPacked(gemm_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.cpp 与 sdpa_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”状态变量存在的原因。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00