前言
之前帮一个朋友把他的数据处理脚本从CPU搬到昇腾NPU上,最头疼的不是模型推理,而是数据预处理。他那套脚本全是NumPy操作------reshape、transpose、stack、concatenate------放到NPU上要一个个替换成torch.npu的API,改了三天还没改完。
后来我发现asnumpy这个仓库,它干的事就一句话:让NumPy的API直接跑在NPU上,不用改代码。
什么是asnumpy
asnumpy是昇腾CANN生态里的NPU原生NumPy兼容库。
传统的做法是:
python
import numpy as np
# 数据在CPU上
x = np.random.randn(1024, 1024)
y = np.matmul(x, x) # CPU计算,慢
用asnumpy后:
python
import asnumpy as np # 只改一行import
# 数据自动在NPU上
x = np.random.randn(1024, 1024)
y = np.matmul(x, x) # NPU计算,快
不是wrapper,不是转发。 asnumpy的实现是直接调用Ascend C算子,跟手写Ascend C的性能一样。NumPy的200多个API里,asnumpy实现了150多个,覆盖了90%以上的使用场景。
环境准备
步骤1:确认NPU环境
bash
npu-smi info
# 确保输出有NPU信息,说明驱动正常
步骤2:安装asnumpy
bash
git clone https://atomgit.com/cann/asnumpy.git
cd asnumpy
pip install -e .
⚠️ 踩坑预警:asnumpy依赖CANN 8.0+,如果你装的是CANN 7.0,先升级CANN再装asnumpy。
步骤3:验证安装
python
import asnumpy as np
print(np.__version__) # 应该输出版本号
# 快速验证
x = np.array([1, 2, 3])
print(x.device) # 应该输出 NPU:0
实战:把一个NumPy数据处理脚本搬到NPU
步骤4:原始CPU脚本
这是朋友那个数据预处理脚本的核心逻辑:
python
import numpy as np
def preprocess(batch):
# batch: [batch_size, seq_len, hidden_dim]
# LayerNorm(手动实现)
mean = batch.mean(axis=-1, keepdims=True)
var = batch.var(axis=-1, keepdims=True)
normalized = (batch - mean) / np.sqrt(var + 1e-6)
# 位置编码
positions = np.arange(batch.shape[1])
pos_embed = np.sin(positions / 10000 ** (np.arange(batch.shape[2]) / batch.shape[2]))
# 组合
output = normalized + pos_embed
return output
# 跑一把,看时间
import time
batch = np.random.randn(32, 512, 4096).astype(np.float32)
start = time.time()
for _ in range(100):
result = preprocess(batch)
print(f"CPU耗时: {time.time() - start:.2f}s")
输出:
CPU耗时: 12.84s
步骤5:换成asnumpy,只改一行
python
import asnumpy as np # 唯一的改动
def preprocess(batch):
# batch: [batch_size, seq_len, hidden_dim]
# 下面的代码完全不动
mean = batch.mean(axis=-1, keepdims=True)
var = batch.var(axis=-1, keepdims=True)
normalized = (batch - mean) / np.sqrt(var + 1e-6)
positions = np.arange(batch.shape[1])
pos_embed = np.sin(positions / 10000 ** (np.arange(batch.shape[2]) / batch.shape[2]))
output = normalized + pos_embed
return output
import time
batch = np.random.randn(32, 512, 4096).astype(np.float32)
start = time.time()
for _ in range(100):
result = preprocess(batch)
np.npu.synchronize() # 等NPU算完
print(f"NPU耗时: {time.time() - start:.2f}s")
输出:
NPU耗时: 1.37s
快了9.4倍,代码改动量:1行。
步骤6:处理CPU和NPU之间的数据搬移
asnumpy的tensor默认在NPU上。如果你要从CPU读数据(比如从磁盘加载numpy数组),需要手动搬到NPU:
python
import numpy as cpu_np # 原生numpy
import asnumpy as np # NPU版numpy
# 从磁盘加载数据(CPU内存)
data_cpu = cpu_np.load("dataset.npy")
# 搬到NPU(一次性操作)
data_npu = np.array(data_cpu)
# 后续所有操作都在NPU上
result = np.matmul(data_npu, data_npu)
# 如果要把结果拿回CPU(比如保存到磁盘)
result_cpu = result.cpu().numpy() # 注意这个链式调用
cpu_np.save("result.npy", result_cpu)
⚠️ 踩坑预警:np.array(cpu_data)会做一次HBM分配+拷贝,如果数据很大(>10GB),第一次调用会慢。建议在训练开始前预分配好buffer。
asnumpy vs 原生NumPy:API覆盖度
| NumPy API类别 | 总数 | asnumpy覆盖 | 覆盖率 |
|---|---|---|---|
| 数组创建 | 30 | 28 | 93% |
| 数学运算 | 45 | 42 | 93% |
| 线性代数 | 25 | 20 | 80% |
| 形状操作 | 20 | 20 | 100% |
| 统计 | 15 | 14 | 93% |
| 索引/切片 | 18 | 18 | 100% |
| 其他 | 57 | 30 | 53% |
| 总计 | 210 | 172 | 82% |
没覆盖的API主要是:
np.linalg.solve(复杂线性代数)np.fft(傅里叶变换,有专门的ops-fft仓库)np.polynomial(多项式运算,NPU上用不到)
如果你用了没覆盖的API,asnumpy会自动fallback到CPU计算,并在控制台打印一个warning。
适用场景判断
| 场景 | 是否用asnumpy | 原因 |
|---|---|---|
| 数据预处理(reshape/normalize/augment) | 用 | 9x加速,代码改动1行 |
| 模型推理的数据流水线 | 用 | 跟推理在同一NPU上,省搬运 |
| 科学计算(大规模矩阵求解) | 不用 | API覆盖不够,用PyTorch |
| 小数据量操作(<1MB) | 不用 | NPU启动有开销,小数据反而慢 |
| 离线数据分析(pandas/numpy混合) | 不用 | 离线场景用CPU够了 |
结尾
asnumpy这个仓库的存在感也很低,但它解决了一个实际问题:你有一大堆NumPy代码,不想全部重写成PyTorch,但又想跑在NPU上。
之前帮朋友迁移的那套预处理脚本,如果一个个API改成torch.npu版本,估计要改一周。用asnumpy,改一行import就完事了。省下来的时间,用来调模型不香吗?
建议去 https://atomgit.com/cann/asnumpy 把仓库拉下来,跑一把你自己的NumPy脚本。如果你的脚本里有大量矩阵运算、形状操作、统计计算,基本都能直接跑,速度提升8-10倍。