Python 生态里最常用的库不是 PyTorch,是 NumPy。数据预处理、特征工程、科学计算——大量代码都是 NumPy 写的。把这些代码搬上昇腾NPU,常规路径是先把 NumPy 数组转成 PyTorch Tensor,再搬到 NPU,算完搬回来转回 NumPy。每一步都是开销。

asnumpy 的思路直截了当:提供一个和 NumPy API 完全兼容的接口,底层计算直接跑在昇腾NPU 上。数组在 NPU 的 HBM 里,计算调用 CANN 的算子,用户代码一行不用改——import 换一下就行。

不是 NumPy 替代,是 NumPy 加速

asnumpy 正确的定位不是「在 NPU 上重新实现一遍 NumPy」,而是「给 NumPy 换个计算后端」。它的 ndarray 是一个瘦封装,内部存储指向 NPU 的 HBM,计算时调 CANN ops-math / ops-blas / ops-tensor 里的算子。

这跟 PyTorch 的 tensor.npu() 是一个思路,区别在于 asnumpy 保持了 NumPy 的广播语义和 API 风格——np.dotnp.einsumnp.fft 这些高频函数的签名和返回值类型完全一致。

import asnumpy as anp

# API 和 NumPy 完全一致,数据直接存在 NPU HBM 里
a = anp.random.randn(4096, 4096)  # 直接在 HBM 上生成随机矩阵
b = anp.random.randn(4096, 4096)
c = anp.dot(a, b)  # 矩阵乘在 NPU 的 Cube 单元上跑

# 对标 np.linalg.svd
u, s, v = anp.linalg.svd(a, full_matrices=False)

# 对标 np.fft 全家桶
freq = anp.fft.fft2(a)

# 对标 np.einsum(爱因斯坦求和约定)
d = anp.einsum('ij,jk,kl->il', a, b, c)

关键点:数据类型、形状、broadcasting 规则、返回值结构——和原生 NumPy 一模一样。改的就是 import numpy 变成 import asnumpy

矩阵乘的开销从哪省出来

NumPy 的 dot 在 CPU 上调的是 BLAS 库(通常是 OpenBLAS 或 MKL)。4096×4096 的 FP32 矩阵乘,CPU 上大概 200ms,NPU 上 asnumpy 同样的操作大概 3ms。

差距不在算力。4096×4096 矩阵乘的计算量是 64G FLOPS,一张 Ascend 910 的 Cube 单元 FP32 算力是 64 TFLOPS——理论上一毫秒就干完了。多出来的 2ms 全花在数据搬运上。

asnumpy 怎么压缩这个搬运开销:

原地操作尽量不触发 HBM 读写。 NumPy 的 a += b 语义上需要读 a、读 b、写 a。如果 a 和 b 本身就在 HBM 里,asnumpy 让 Vector 单元直接在 HBM 上做 element-wise add,不走 L1 中转——省掉了一次「HBM→L1」和「L1→HBM」的来回。不是所有操作都能原地,但 element-wise 加法、乘法和 relu 类激活函数都可以。

广播不复制数据。 NumPy 的 broadcasting 在 CPU 上通常会对小数组做隐式复制。比如 (4096, 4096) 的矩阵加 (1, 4096) 的向量,CPU 版会先把向量沿第 0 维复制 4096 次变成 (4096, 4096),再逐个元素加。asnumpy 不复制——Vector 单元直接按广播规则做跨步访问:

// asnumpy 内部:广播加法的简化实现
// (4096, 4096) + (1, 4096) → 不复制,用 stride 跳
__aicore__ void broadcast_add(
    LocalTensor<float>& out,
    LocalTensor<float>& big,    // shape [4096, 4096]
    LocalTensor<float>& small   // shape [1, 4096]
) {
    // 对每一行,big 的该行加 small 的那一行
    // small 每行相同的 stride,Vector 单元直接跳到对应地址
    for (int i = 0; i < 4096; ++i) {
        vadd(out[i * 4096], big[i * 4096], small[0], 4096);
        // small[0] 是恒定地址,不用每次重算偏移
    }
}

不复制省的是 HBM 带宽,大矩阵广播场景下能省 30-50% 的搬运时间。

FFT 的加速逻辑

np.fft 是科学计算里的高频操作。asnumpy 的 FFT 实现不自己写——CANN 有专门的 ops-fft 仓库,基于 Ascend Vector 单元的高度并行特性做了 Radix-2 和混合基分解优化。

4096 点的 1D FFT,CPU 上 NumPy 跑 50 次需要 2.8 秒,asnumpy 在 NPU 上同样操作 200ms 不到。加速比的来源是 Vector 单元能同时跑数十个蝶形运算,而 CPU 的 SIMD 宽度只有 8(AVX-512 下)或 16。

import asnumpy as anp
import time

# 模拟信号频谱分析(100 次 4096 点 FFT)
signal = anp.random.randn(100, 4096).astype(anp.float32)

# 直接在 NPU 上跑这 100 个 FFT,不开 batch
t0 = time.time()
for _ in range(50):
    freq = anp.fft.fft(signal, axis=1)  # axis=1,沿时间轴
t1 = time.time()

print(f"50 次 100×4096 FFT:{t1 - t0:.3f}s")

# 搬回 CPU 只需要加一行
signal_cpu = signal.to_host()  # HBM → CPU 内存

和 PyTorch 的关系

asnumpy 不是 PyTorch 的替代品。两者有明确的场景划分:

场景 用什么 原因
数据预处理管道 asnumpy 天然 NumPy 兼容,零改造
深度学习训练/推理 PyTorch + torch_npu 自动微分、优化器、分布式
科学计算 / 仿真 asnumpy FFT/SVD/einsum 直接上 NPU
混合场景(预处理→模型) asnumpy → torch torch.as_tensor(anp_array.to_host())

从 asnumpy 把数据传给 PyTorch 只有一次 HBM→CPU→HBM 的拷贝开销。如果预处理和模型都在 NPU 上跑,可以用零拷贝共享内存的方式传数据——这是 asnumpy 和 torch_npu 之间的一个未来优化方向。

接入只需要两行改动

# 原生 NumPy 代码
import numpy as np
data = np.load("sensor_data.npy")  # shape (10000, 512)
result = np.linalg.svd(data, full_matrices=False)

# 搬到 asnumpy:只改 import
import asnumpy as anp
data = anp.load("sensor_data.npy")  # 自动加载到 NPU HBM
result = anp.linalg.svd(data, full_matrices=False)

需要注意的点:asnumpy 默认只支持 float32 和 float16 精度,float64int64 会回退到 CPU 执行。对于大部分科学计算和特征工程来说,float32 精度已经足够——原始传感器数据通常是 float32,深度学习的输入也以 float32 为主。


asnumpy 的价值不在于「比 NumPy 更快」——任何加速库都比 NumPy 快。它的价值在于「不改代码就比 NumPy 快」。科学计算和数据处理管道里大量已经写好的 NumPy 逻辑,换一个 import 就能拿到昇腾NPU 的算力,不用重写、不用适配、不用学新 API。

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐