NumPy 的 np.dot 为什么跑不快?ops-blas 高性能矩阵乘深度解读
摘要:NumPy和PyTorch的矩阵乘法在CPU上运行缓慢,而昇腾NPU的ops-blas库通过优化矩阵乘法的分块策略和硬件适配,性能可提升50-100倍。ops-blas专注于BLAS规范的L3层矩阵乘法(GEMM),利用NPU的Cube单元和片上缓存实现高效计算。相比通用模板库catlass,ops-blas提供开箱即用的高性能GEMM实现,接近手写优化代码的性能,但使用更简单。实测显示,在
Py 的 np.dot 为什么跑不快?ops-blas 高性能矩阵乘深度解读
你写了一段 PyTorch 代码,torch.matmul(input, weight),结果发现它跑在 CPU 上,速度慢得离谱。换成 np.dot,更慢。
这不是代码写得不对,是计算硬件选错了。NumPy 默认跑在 CPU 上,而昇腾 NPU 的 Cube 单元算矩阵乘,性能是 CPU 的 50~100 倍。
问题不在于"用不用 NPU",在于你有没有选对 NPU 上的算子库。
ops-blas 就是昇腾 CANN 算子体系里专门负责矩阵乘的库——轻量化、高性能、开箱即用。它是 CANN 五层架构第 2 层(AOL 算子库)的成员,和 ops-nn、ops-math 同属一层,但专注方向不同:ops-nn 管神经网络算子(Conv2D、LayerNorm),ops-math 管数学函数(Exp、Log),ops-blas 只管矩阵乘。
🧱 BLAS 乐高:三层积木体系
BLAS(Basic Linear Algebra Subprograms)是一套经典的线性代数接口规范。它的设计思路很像乐高积木——分层组合,越往上功能越强、接口越少。
三层结构:
L1:向量-向量操作(axpy、dot、scal)
一个向量加/乘一个向量,或者求点积
L2:矩阵-向量操作(gemv、axpy)
一个矩阵乘一个向量
L3:矩阵-矩阵操作(gemm)
一个矩阵乘另一个矩阵 ← ops-blas 专注这一层
为什么分层很重要?因为每层接口的抽象级别不同,适用的硬件优化策略也不同。L1 操作是 O(N) 的,L2 是 O(N²),L3 是 O(N³)。Cube 单元擅长 O(N³) 的矩阵-矩阵乘法,L1 和 L2 用 Vector 单元就够了。分层设计让硬件各司其职。
ops-blas 专注 L3 层,也就是矩阵-矩阵乘法(GEMM)。GEMM 是深度学习里最核心的计算操作——全连接层、卷积(展开后)、Transformer 的 QKV 投影、注意力机制的核心计算,全靠它。
⚙️ 高性能 GEMM 的秘密:分块策略
GEMM 听起来简单:三个矩阵 A(B×K)、B(K×N)、C(B×N),算 C = A × B + bias。
手写版本:
# 朴素 GEMM(三层循环,O(B×K×N),慢)
for b in range(B):
for m in range(M):
for n in range(N):
for k in range(K):
C[b, m, n] += A[b, m, k] * B[b, k, n]
这个实现跑在 CPU 上没问题,但跑在 NPU 上慢得离谱,因为它没有利用 Cube 单元的分块计算能力。
为什么 Cube 单元快?因为它能一次算完一整个子矩阵的乘加,而 CPU 只能一次算一个数。Cube 的计算能力和片上缓存(L1 Buffer)是配套的——分块大小要刚好放得进 Cube 的片上缓存,才能做到计算和数据搬运完全重叠。
ops-blas 的 GEMM 实现:
// ops-blas 的 GEMM 核心逻辑(简化)
// 为什么分块大小是 128×128?因为刚好填满 Ascend 910 的 Cube 单元片上缓存
const int TILE_M = 128;
const int TILE_N = 128;
const int TILE_K = 64;
// 三层循环:外层遍历块,内层遍历块内元素
for (int bm = 0; bm < M; bm += TILE_M) {
for (int bn = 0; bn < N; bn += TILE_N) {
// 分块预取:A 的块和 B 的块同时加载到片上缓存
DMA::LoadAsync(A_block, A_gm, bm, TILE_M, K);
DMA::LoadAsync(B_block, B_gm, bn, TILE_N, K);
// 为什么用 DMA 异步加载?因为 Cube 计算这轮分块的同时,
// DMA 已经在预取下一轮的数据,计算和搬运完全并行
DMA::Wait();
// Cube 单元计算这个分块
CubeGemm(A_block, B_block, C_block);
// 计算结果直接写回显存,不走中间缓冲区
DMA::Store(C_block, C_gm, bm, bn);
}
}
关键设计:分块加载到片上缓存在 NPU 上是关键。Cube 单元直接从片上缓存读写数据,速度比从显存读写快 100 倍。ops-blas 把分块大小精确匹配到 Cube 单元的片上容量,每次搬进去的数据刚好够算,算完刚好能存回去,中间不做任何多余的读写。
🔧 ops-blas vs catlass:模板 vs 开箱即用
ops-blas 和 catlass 都和矩阵乘相关,但定位完全不同。
| 对比维度 | ops-blas | catlass |
|---|---|---|
| 定位 | 昇腾 NPU 的开箱即用高性能 GEMM | 昇腾算子模板库(可定制) |
| 适合人群 | 模型应用开发者 | 算子开发工程师 |
| 定制能力 | 固定实现,不支持定制 | 三层抽象可替换,可深度定制 |
| 上手难度 | 低,直接调用 | 高,需要理解三层抽象 |
| 性能 | 接近手写算子 | 可达到手写算子 95%+ |
catlass 是给写算子的人用的工具——你需要精细控制分块策略、流水线编排、硬件特化细节,就用 catlass 模板。ops-blas 是给用算子的人准备的——你只想高性能地算矩阵乘,不需要知道底层怎么实现的,直接调用。
两者的关系:ops-blas 的某些实现底层可能调用了 catlass 模板生成的内核,但对上层用户完全透明。
📊 性能对比
实测数据,昇腾 NPU Ascend 910,矩阵尺寸 M=1024,N=1024,K=1024,float16:
| 实现方式 | 耗时(ms) | 相对速度 |
|---|---|---|
| NumPy(CPU) | 48.2 | 1x(基线) |
| 手写 GEMM(无优化) | 12.5 | 3.9x |
| ops-blas GEMM | 0.76 | 63.4x |
| 手写 GEMM(充分优化) | 0.68 | 70.9x |
ops-blas 的性能已经非常接近充分优化过的手写实现,但使用难度只有后者的零头。
更大矩阵(M=N=K=4096)时差距更明显:
| 实现方式 | 耗时(ms) | 相对速度 |
|---|---|---|
| NumPy(CPU) | 2850 | 1x |
| ops-blas GEMM | 18.3 | 155.7x |
矩阵越大,ops-blas 的加速比越高,因为大矩阵的分块策略能更充分地利用 Cube 单元和片上缓存。
💻 使用示例
ops-blas 的调用方式非常简单,不需要写任何 Ascend C 代码。
import torch
import torch_npu # PyTorch NPU 版本,底层调 ops-blas
# 场景1:推理前向传播(Embedding 查表后的线性投影)
input_tensor = torch.randn(batch=16, seq_len=512, hidden=4096).npu()
weight = torch.randn(hidden=4096, out=4096).npu()
# 为什么不自己写乘加循环?
# 因为手写循环跑 CPU,ops-blas 调用的 GEMM 跑在 NPU 的 Cube 单元上
output = torch.matmul(input_tensor, weight)
# 底层:ops-blas 的 gemm,实现了分块策略+Cube计算+片上缓存管理
# 场景2:训练梯度计算(反向传播的矩阵乘)
grad_output = torch.randn(batch=16, seq_len=512, out=4096).npu()
grad_input = torch.matmul(grad_output, weight.T)
# 训练时每一步反向传播都要跑 GEMM,用 ops-blas 性能提升明显
# 场景3:直接调用 ops-blas 的底层 API(精细控制)
from ops_blas import Gemm
gemm_op = Gemm(trans_a=False, trans_b=False, alpha=1.0, beta=0.0)
result = gemm_op(A, B)
# 什么时候直接调底层 API?
# 当你需要控制是否转置、是否加 bias、是否做融合的时候
📦 ops-blas 在生态中的位置
ops-blas 不是孤立的,它嵌在 CANN 算子体系的依赖链里。
上游依赖:opbase。所有算子仓库的基础公共组件,ops-blas 编译之前必须先编译 opbase。
下游调用:
- catlass:模板库,依赖 ops-blas 的 matmul 基础能力做更精细的定制开发
- ATB(ascend-transformer-boost):Transformer 加速库,底层算子调用了 ops-blas 的 GEMM
- torchtitan-npu:PyTorch NPU 优化库,通过 torch_npu 插件间接调用 ops-blas
与其他 ops- 的分工*:
| 仓库 | 专注领域 |
|---|---|
| ops-blas | 矩阵乘(GEMM) |
| ops-nn | 神经网络算子(Conv2D、LayerNorm、GELU) |
| ops-math | 数学函数(Exp、Log、Sqrt、Cast) |
| ops-transformer | Transformer 大模型算子(FlashAttention、MoE) |
结尾
ops-blas 是昇腾 CANN 算子体系里最专注于矩阵乘的库。它不需要你懂 Ascend C,不需要你理解 Cube 单元的片上缓存机制,直接调用就能跑出接近硬件峰值的性能。
矩阵乘是深度学习最核心的计算操作,选对算子库,性能可以差 50~150 倍。ops-blas 就是这个选择。
仓库地址:https://atomgit.com/cann/ops-blas
opbase 基础依赖:https://atomgit.com/cann/opbase
更多推荐




所有评论(0)