asnumpy 实战:用 NumPy 语法在昇腾 NPU 上跑计算
如果你会写 NumPy,就应该能在 NPU 上跑出结果。asnumpy 做的事情看起来简单,但要做好并不容易。
我之前帮同事查一个性能问题,发现他的代码里有个循环——用 np.sum 对一个三维数组沿着两个轴做规约,在 CPU 上跑了 15 秒。整个推理流程被这个循环卡死了。他问我有没有办法加速,我说你用的 NumPy 不支持 GPU/NPU,换个接口试试。他说「那要重写吗?」
不用重写。用 asnumpy 就行。
一、asnumpy 是做什么的?
asnumpy 是 CANN 生态中的一个 NPU 原生计算库,它让你用 NumPy 风格的 API 在昇腾 NPU 上做计算。
它不是 NumPy 的替代品,而是 NumPy 的昇腾加速通道。你在代码里写的 np.sum() 换成 asnumpy.sum(),逻辑不用变,但计算跑在 NPU 上而不是 CPU 上。
从架构位置看,asnumpy 在 CANN 五层架构中横跨计算服务层和执行层。它不直接调用硬件指令,而是通过调用底层算子——AscendCL 执行层的算子来完成计算。你可以把它理解成一个 NumPy 语法的算子编排层。
链接:https://atomgit.com/cann/asnumpy
二、环境准备
开始之前,确认环境:
# 1. 确认 NPU 驱动正常
npu-smi info
# 你应该看到类似:Device 0, Memory: 32768 MB
# 2. 确认 CANN 包已安装
cat /usr/local/Ascend/ascend-toolkit/version.cfg
# 应显示 CANN 8.0 或更高版本
⚠️ 踩坑预警:如果你用的是 Atlas A3 服务器的 AICC 环境,CANN 的安装路径可能在 /home/ma-user/Ascend/ 而不是默认的 /usr/local/Ascend/。先确认路径再做后续操作,否则 import 会找不到动态库。
三、第一个 asnumpy 程序
从最简单的开始——在 NPU 上创建一个数组并做加法:
import asnumpy
# 在 NPU 上创建数组
a = asnumpy.array([1.0, 2.0, 3.0, 4.0])
b = asnumpy.array([5.0, 6.0, 7.0, 8.0])
# NPU 设备上做加法
c = asnumpy.add(a, b)
# 把结果拉回主机
result = c.to_host()
print(f"a + b = {result}")
# 输出: a + b = [ 6. 8. 10. 12.]
跟 NumPy 比,区别只有两个:
array创建在 NPU 显存上,不是在 CPU 内存上- 需要用
to_host()把结果从 NPU 显存搬回 CPU 才能打印或存盘
有一件事值得注意:to_host() 会触发一次显存→内存的数据拷贝。如果你的计算结果是中间产物、马上要被下一轮计算消费,不要频繁 to_host()。在 NPU 上流转,只在最终要取出结果时才搬一次。
四、实战案例:矩阵乘法性能对比
asnumpy 真正的价值在大规模计算上。下面用一个矩阵乘法对比 CPU 和 NPU 的性能差异:
import asnumpy
import time
SIZE = 4096 # 4096x4096 矩阵
# === CPU 版本(NumPy)===
import numpy as np
a_cpu = np.random.randn(SIZE, SIZE).astype(np.float32)
b_cpu = np.random.randn(SIZE, SIZE).astype(np.float32)
start = time.time()
c_cpu = np.matmul(a_cpu, b_cpu)
cpu_time = time.time() - start
# === NPU 版本(asnumpy)===
# 先在主机内存预热一次,避免第一次包含 JIT 编译时间
a_npu = asnumpy.from_array(a_cpu)
b_npu = asnumpy.from_array(b_cpu)
# 预热
_ = asnumpy.matmul(a_npu, b_npu)
# 计时
start = time.time()
c_npu = asnumpy.matmul(a_npu, b_npu)
npu_time = time.time() - start
print(f"CPU 耗时: {cpu_time:.3f}s")
print(f"NPU 耗时: {npu_time:.3f}s")
print(f"加速比: {cpu_time/npu_time:.1f}x")
这个测试的关键细节是预热(warmup)。第一次在 NPU 上调用算子时,会有 JIT 编译开销,如果不预热就拿第一次的时间当性能数据,会被误报成「NPU 比 CPU 还慢」。实际用户拿到的数据应该是预热后的稳态性能。
在 4096x4096 规模的 FP32 矩阵乘法上,单卡 Ascend 910 对比 16 核 CPU,加速比通常在 15-20 倍。注意,这不是说 NPU 在任何规模上都比 CPU 快——小规模(比如 128x128)数据传输开销可能吃掉加速收益,大规模才是 asnumpy 的主场。
五、常用操作速查
这里列出常用操作及其 asnumpy 与 NumPy 的对比:
| 操作 | NumPy | asnumpy |
|---|---|---|
| 创建数组 | np.array([1,2,3]) |
asnumpy.array([1,2,3]) |
| 从主机导入 | - | asnumpy.from_array(arr) |
| 导出回主机 | - | np_arr = asnp_arr.to_host() |
| 矩阵乘法 | np.matmul(a,b) |
asnumpy.matmul(a,b) |
| 逐元素乘法 | a * b |
asnumpy.multiply(a,b) |
| 求和 | np.sum(a, axis=0) |
asnumpy.sum(a, axis=0) |
| 形状变换 | a.reshape(2,3) |
asnumpy.reshape(a,(2,3)) |
| 转置 | a.T |
asnumpy.transpose(a) |
| 三角函数 | np.sin(a) |
asnumpy.sin(a) |
大部分常用操作的接口名和 NumPy 一致,差异主要在数据驻留位置(NPU 显存 vs CPU 内存)。注意 asnumpy.sum 支持 axis 参数——这个在早期版本里是没有的,CANN 8.0 后才完整支持多轴规约。
六、三个必知的坑
坑一:别在循环里 to_host()
# ❌ 错误做法:每步都搬回主机
for x in inputs:
result = asnumpy.process(x).to_host() # 每次搬一次数据
results.append(result)
# ✅ 正确做法:在 NPU 上累积,只搬一次
batch_data = asnumpy.stack(inputs) # 一次搬到 NPU
batch_result = asnumpy.process(batch_data) # 全在 NPU 上算
results = batch_result.to_host() # 最后搬一次
每做一次 to_host(),数据就要穿越一次 PCIe 总线。在大规模计算中,这个开销比计算本身还大。
坑二:小规模计算用 NumPy 即可
如果数据量很小(比如 100x100 矩阵),搬一次数据的开销可能比计算本身大几倍。asnumpy 的优势在大规模计算——BatchSize 越大,NPU 的并行度利用率越高,对比 CPU 的加速效果越明显。
坑三:数据类型需要显式指定
NumPy 会默认用 float64,但昇腾 NPU 对 float64 的支持需要手动开启。如果你不指定 dtype,可能会触发隐式转换,导致精度问题。建议 float32 场景全程显式声明:
# ✅ 显式指定 float32
a = asnumpy.array(data, dtype='float32')
七、进阶方向
掌握基础操作后,可以往三个方向发展:
-
NPU 原生广播机制:asnumpy 的广播语义和 NumPy 兼容,但在 NPU 上的实现是通过算子融合来优化的。如果你在低维和高维数组之间做操作,asnumpy 会自动寻找最优的融合路径,不需要手动调。
-
与 AscendCL 混合使用:asnumpy 的可访问性不错,但有些高级功能(比如自定义算子、Stream 管理)需要下沉到 AscendCL 层面。你可以用 asnumpy 处理数据预处理和结果后处理,用 AscendCL 的 IR 构图 API 处理核心推理逻辑。
-
性能监控:通过
asnumpy.profiler可以收集每次操作的时间消耗和显存占用,帮助你发现性能瓶颈。在大批量计算任务中,profiler 数据是判断「是不是去搬结果了」的有效依据。
链接:https://atomgit.com/cann/asnumpy
更多推荐




所有评论(0)