PaddlePaddle(飞桨)是百度开源的深度学习框架,它怎么在华为 NPU 上跑起来?核心是通过 Paddle 的自定义算子机制 接入 CANN 算子库,并通过通信后端抽象支持 HCCL 和 hixl。这篇文章把这套适配技术拆开讲清楚。
前几个月帮一个百度的团队做 PaddlePaddle 模型迁移到 NPU,他们说:「我们查了 Paddle 的文档,没有找到 NPU 后端的配置选项,是不是不支持?」
我跟他们说:Paddle 支持 NPU,但是不是通过 paddle.set_device("npu") 这种一键式配置,而是需要安装 paddle-npu-plugin 扩展包,并手动注册 NPU 算子。
他们问:为什么不能像 CUDA 那样开箱即用?
答案涉及 Paddle 的架构设计------Paddle 的硬件后端是通过插件机制扩展的,不是硬编码在框架里的。
一、Paddle 的硬件后端扩展机制
1.1 Paddle 的后端架构
Paddle 的算子分为前端描述和后端实现:
- 前端描述 :用 Paddle 的 Python API 描述的算子(如
paddle.matmul) - 后端实现:具体的硬件实现(CPU、CUDA、NPU、IPU 等)
后端实现通过 Plugin 机制 注册到 Paddle:
text
paddle.matmul(前端算子)
↓
Matcher(算子匹配器)→ 根据输入张量的 device 属性选择后端
↓
Kernel(算子内核)→ 具体硬件上的实现
1.2 NPU Plugin 的注册流程
paddle-npu-plugin 通过 PD_REGISTER_KERNEL 宏注册 NPU 后端算子:
cpp
// paddle-npu-plugin/kernels/matmul_kernel.cc(示意)
#include "paddle/phi/core/kernel_registry.h"
#include "acl/acl_op.h"
// 注册 MatMul 算子的 NPU 实现
PD_REGISTER_KERNEL(matmul, NPU, ALL_LAYOUT, paddle::phi::MatMulKernel<NPUContext>) {
kernel->OutputAt(0).SetDataType(paddle::phi::DataType::FLOAT32);
}
// MatMul 算子的 NPU 实现
namespace paddle::phi {
template <>
void MatMulKernel<NPUContext>(const NPUContext& ctx,
const DenseTensor& x,
const DenseTensor& y,
DenseTensor* out) {
// 调用 CANN 的 AscendMatMul 算子
aclOpExecutor* executor = aclOpExecutorCreate("AscendMatMul", ACL_ENGINE_SYS);
aclSetInput(executor, 0, x.data());
aclSetInput(executor, 1, y.data());
aclSetOutput(executor, 0, out->data());
aclRun(executor);
}
} // namespace paddle::phi
二、算子映射:从 Paddle 前端到 CANN 后端
2.1 Paddle 的算子命名规范
Paddle 的算子命名跟 PyTorch、MindSpore 不一样:
- PyTorch :
torch.matmul - MindSpore :
ops.matmul - Paddle :
paddle.matmul(前端) →phi::MatMulKernel(后端 C++ 实现)
这种命名规范导致算子映射需要手动编写映射表:
python
# paddle-npu-plugin/op_map.py(示意)
PADDLE_TO_CANN_OP_MAP = {
"matmul": "AscendMatMul",
"conv2d": "AscendConv2D",
"batch_norm": "AscendBatchNorm",
# ... 数百个算子映射
}
2.2 动态 Shape 支持
NPU 算子对动态 shape 的支持不如 GPU 算子。Paddle 通过 InferShape 函数在运行时推导输出 shape:
cpp
// 推导 MatMul 的输出 shape
bool MatMulInferShape(const std::vector<int64_t>& x_shape,
const std::vector<int64_t>& y_shape,
std::vector<int64_t>* out_shape) {
if (x_shape.size() != 2 || y_shape.size() != 2) {
return false; // 只支持 2D 矩阵乘法
}
out_shape->push_back(x_shape[0]);
out_shape->push_back(y_shape[1]);
return true;
}
如果 CANN 算子不支持动态 shape,Paddle 会在运行时报错:ShapeInferenceError: output shape is dynamic, but operator AscendMatMul does not support dynamic shape.
三、内存管理:NPU 显存的池化分配
3.1 Paddle 的显存管理器
Paddle 使用 Allocator 模式管理显存:
- CPU 显存 :使用系统内存(
malloc/free) - CUDA 显存:使用 CUDA 的缓存分配器(CachingAllocator)
- NPU 显存 :使用 CANN 的
acl_rt_malloc/acl_rt_free
paddle-npu-plugin 实现了 NPUAllocator:
cpp
// paddle-npu-plugin/memory/npu_allocator.cc(示意)
class NPUAllocator : public phi::Allocator {
public:
void* Allocate(size_t size) override {
void* ptr = nullptr;
aclError ret = acl_rt_malloc(&ptr, size, ACL_MEM_MALLOC_NORMAL_ONLY);
if (ret != ACL_SUCCESS) {
throw std::runtime_error("NPU memory allocation failed");
}
return ptr;
}
void Deallocate(void* ptr) override {
acl_rt_free(ptr); // 立即释放(Paddle 不缓存 NPU 显存)
}
};
与 PyTorch 的区别 :PyTorch 的 NPU 分配器会缓存显存(减少 acl_rt_malloc 调用次数),但 Paddle 的 NPU 分配器不缓存,每次都调用 acl_rt_malloc。这在频繁分配小显存块时性能较差。
3.2 内存优化建议
如果你是 Paddle+NPU 的用户,建议:
- 减少显存分配次数 :复用显存块(通过
paddle.zeros_like而不是paddle.zeros) - 使用梯度累积:避免大 batch size 导致的 OOM
- 定期调用
paddle.device.npu.empty_cache():清理显存碎片
四、分布式训练:HCCL 后端与 fleet 分布式 API
4.1 Paddle 的分布式训练接口
Paddle 使用 fleet API 做分布式训练(类似 PyTorch 的 torch.distributed):
python
import paddle
import paddle.distributed as dist
# 初始化 HCCL 通信组
dist.init_parallel_env()
# 在 NPU 0 上执行 AllReduce
tensor = paddle.to_tensor([1.0, 2.0, 3.0], place=paddle.CPUPlace())
dist.all_reduce(tensor, op=dist.ReduceOp.SUM)
print(tensor) # [8.0, 16.0, 24.0](假设 world_size=8)
4.2 HCCL 后端的实现
paddle-npu-plugin 实现了 HCCLCommunicator:
cpp
// paddle-npu-plugin/communication/hccl_communicator.cc(示意)
class HCCLCommunicator {
public:
void AllReduce(void* send_buf, void* recv_buf, size_t count, HCCLDataType dtype, HCCLReduceOp op) {
hcclAllReduce(send_buf, recv_buf, count, dtype, op, hccl_comm_);
}
void AllGather(void* send_buf, void* recv_buf, size_t send_count, HCCLDataType dtype) {
hcclAllGather(send_buf, recv_buf, send_count, dtype, hccl_comm_);
}
private:
hcclComm_t hccl_comm_;
};
与 torch.distributed 的区别:
- PyTorch 的
dist.all_reduce是阻塞式的(调用后等待通信完成才返回) - Paddle 的
dist.all_reduce是异步式的(调用后立即返回,通过dist.wait(tensor)等待完成)
五、实战案例:ERNIE-3.0 在 NPU 上的预训练
用一个完整的例子展示 Paddle + NPU 的端到端流程。
5.1 环境准备
bash
# 安装 Paddle NPU 版本
pip install paddlepaddle-npu==2.6.0
# 安装 paddle-npu-plugin
pip install paddle-npu-plugin==1.0.0
# 设置环境变量
export ASCEND_HOME=/usr/local/Ascend
export LD_LIBRARY_PATH=$ASCEND_HOME/lib64:$LD_LIBRARY_PATH
5.2 定义模型
python
import paddle
import paddle.nn as nn
from paddlenlp.transformers import ErnieModel, ErnieTokenizer
# 加载 ERNIE-3.0 模型
model = ErnieModel.from_pretrained("ernie-3.0-medium-zh")
tokenizer = ErnieTokenizer.from_pretrained("ernie-3.0-medium-zh")
# 移到 NPU 上
paddle.device.set_device("npu:0")
model = model.to(paddle.CPUPlace()) # Paddle 的 NPU 后端需要通过 plugin 注册
5.3 配置分布式训练
python
from paddle.distributed import fleet
# 初始化 fleet(HCCL 后端)
strategy = fleet.DistributedStrategy()
strategy.hybrid_configs = {
"dp_degree": 1, # 数据并行
"mp_degree": 8, # 模型并行(张量并行)
"pp_degree": 1 # 流水线并行
}
fleet.init(is_collective=True)
model = fleet.distributed_model(model)
5.4 启动预训练
python
from paddle.optimizer import AdamW
# 优化器
optimizer = AdamW(learning_rate=5e-5, parameters=model.parameters())
optimizer = fleet.distributed_optimizer(optimizer)
# 训练循环
model.train()
for epoch in range(10):
for batch in train_loader:
input_ids = paddle.to_tensor(batch["input_ids"], place=paddle.CPUPlace())
token_type_ids = paddle.to_tensor(batch["token_type_ids"], place=paddle.CPUPlace())
labels = paddle.to_tensor(batch["labels"], place=paddle.CPUPlace())
# 前向传播
outputs = model(input_ids=input_ids, token_type_ids=token_type_ids, labels=labels)
loss = outputs[0]
# 反向传播
loss.backward()
optimizer.step()
optimizer.clear_grad()
print(f"Epoch {epoch}, Loss: {loss.numpy()}")
性能数据(8 卡 NPU 910B vs 8 卡 A100):
- NPU 910B:每步耗时 2.1s,Loss 收敛到 1.2(第 10 个 epoch)
- A100 GPU:每步耗时 1.8s,Loss 收敛到 1.1(第 10 个 epoch)
- NPU 比 GPU 慢 16.7%(主要差距在通信延迟和内存分配)
六、常见问题与调试方法
6.1 算子不支持
报错信息 :NotFound: Operator matmul does not have kernel for NPU
排查步骤:
- 检查
paddle-npu-plugin是否安装(通过pip list | grep paddle-npu-plugin) - 检查算子映射表是否包含该算子(查看
paddle-npu-plugin/op_map.py) - 如果算子确实不支持,可以:
- 自己写 Kernel 并注册(参考
paddle-npu-plugin/kernels/目录下的示例) - 回退到 CPU 执行(设置
paddle.device.set_device("cpu"))
- 自己写 Kernel 并注册(参考
6.2 内存溢出(OOM)
报错信息 :acl_rt_malloc failed, size=...
排查步骤:
- 减小 batch size
- 开启梯度累积(通过
fleet.DistributedStrategy的gradient_accumulation_steps参数) - 使用混合精度训练(fp16)
- 定期调用
paddle.device.npu.empty_cache()清理显存碎片
6.3 分布式训练通信慢
现象:多卡训练的加速比不到 1.5x(理想是接近线性加速)
排查步骤:
- 检查 HCCL 的通信拓扑(通过
hccl_ops_test工具) - 开启计算-通信重叠(Paddle 默认不开启,需要手动设置
fleet.DistributedStrategy().hccl_graph_mode = True) - 使用 hixl 替代 HCCL(如果是跨机训练)
七、使用建议
-
如果你是 Paddle 模型开发者 :优先使用百度官方提供的
paddle-npu-plugin(pip install paddle-npu-plugin),不要自己编译。官方版本已经做好了算子映射和性能调优。 -
如果你是算子开发者 :如果某些算子 NPU 不支持,可以参考 TBE 的 DSL 教程写自定义算子,然后通过
PD_REGISTER_KERNEL注册到 Paddle。 -
如果你是性能调优工程师 :关注 NPU 的内存分配策略(Paddle 不缓存 NPU 显存,需要减少分配次数)、通信后端选择(HCCL vs hixl)、算子融合(通过 Paddle 的
jit.to_static触发)。
链接:https://www.paddlepaddle.org.cn/