昇腾CANN tensorflow:让 TensorFlow 在昇腾 NPU 上跑起来的适配层
本文介绍了TensorFlow框架适配昇腾NPU的技术实现方案。适配层主要由三个核心模块组成: op_kernel注册模块:通过为每个CANN支持的算子编写OpKernel实现,将TF算子映射到CANN算子库。与PyTorch的dispatcher机制不同,TensorFlow需要显式注册每个算子。 graph_rewrite模块:通过自定义图优化Pass实现算子融合,将连续算子组合成CANN融合
TensorFlow 是业界主流训练框架之一。要让它识别昇腾 NPU、把图里的算子映射到 CANN 的算子库、把训练循环调度到 NPU 上——中间需要一整层适配代码。这个适配层就是 CANN tensorflow 仓库。
它和 torchtitan-npu 的定位类似(都是框架适配),但技术路径完全不同。PyTorch 用 eager mode + dispatch key,TensorFlow 用 graph mode + op kernel 注册。适配层做的事情本质上一样:把框架的算子调用翻译成 CANN 算子库的调用,但实现机制不一样。
适配层的三块拼图
| 模块 | 功能 | 对应 torchtitan-npu 的模块 |
|---|---|---|
| op_kernel 注册 | 把 TF Op 映射到 CANN 算子 | PyTorch dispatcher 注册 |
| graph_rewrite | 图优化:算子融合、计算图切分 | TorchAir graph pass |
| device_plugin | NPU 设备发现、内存分配、Stream 管理 | torch.npu 设备后端 |
op_kernel 注册
TensorFlow 的每一个算子是一个 OpKernel 子类。适配层对每一个 CANN 支持的算子写一个 OpKernel 实现:
// tensorflow/ops/cann/matmul_op.cc
#include "tensorflow/core/framework/op_kernel.h"
#include "ascendc/matmul.h" // CANN ops-nn 的 MatMul
// 注册 MatMul 算子到 TensorFlow
REGISTER_KERNEL_BUILDER(
Name("MatMul")
.Device(DEVICE_NPU), // 自定义设备类型
MatMulOpKernel
);
class MatMulOpKernel : public OpKernel {
public:
explicit MatMulOpKernel(OpKernelConstruction* ctx) {
OP_REQUIRES_OK(ctx, ctx->GetAttr("transpose_a", &transpose_a_));
OP_REQUIRES_OK(ctx, ctx->GetAttr("transpose_b", &transpose_b_));
}
void Compute(OpKernelContext* ctx) override {
const Tensor& a = ctx->input(0); // [M, K]
const Tensor& b = ctx->input(1); // [K, N]
// 分配输出 Tensor(在 NPU HBM 上)
Tensor* output = nullptr;
OP_REQUIRES_OK(ctx, ctx->allocate_output(
0, TensorShape({a.dim_size(0), b.dim_size(1)}), &output
));
// 调 CANN ops-nn 的 MatMul 算子
// 通过 Runtime API 调
aclrtStream stream = ctx->eigen_gpu_device().stream();
aclblasHandle_t handle;
aclblasCreate(&handle);
aclblasSetStream(handle, stream);
aclblasSgemm(
handle,
transpose_a_ ? ACL_TRANS_N : ACL_TRANS_T,
transpose_b_ ? ACL_TRANS_N : ACL_TRANS_T,
a.dim_size(0), // M
b.dim_size(1), // N
a.dim_size(1), // K
1.0f,
a.flat<float>().data(),
a.dim_size(1), // lda
b.flat<float>().data(),
b.dim_size(1), // ldb
0.0f,
output->flat<float>().data(),
b.dim_size(1), // ldc
stream
);
aclblasDestroy(handle);
}
private:
bool transpose_a_;
bool transpose_b_;
};
和 PyTorch 的区别:PyTorch 的 dispatcher 根据 tensor 的 device 类型(torch.device("npu"))自动路由到 CANN 算子。TensorFlow 需要显式注册 OpKernel——每一个算子都要写一个类。
graph_rewrite:图优化 Pass
TensorFlow 的计算图在运行前会经过 graph rewrite Pass。适配层注入自定义 Pass,把图中连续的算子融合成 CANN 的融合算子:
// tensorflow/compiler/plugin/cann/graph_fusion_pass.cc
class CannFusionPass : public GraphOptimizationPass {
public:
Status Run(const GraphOptimizationPassOptions& options) override {
Graph* g = options.graph->get();
// Pass 1:Conv2D + BiasAdd + ReLU → Conv2DFusion
fuse_conv_bias_relu(g);
// Pass 2:MatMul + BiasAdd + GELU → MatMulFusion
fuse_matmul_bias_gelu(g);
// Pass 3:Transpose + MatMul + Transpose → BertIntermediate
fuse_transpose_matmul(g);
return Status::OK();
}
private:
void fuse_conv_bias_relu(Graph* g) {
// 在图里找模式:Conv2D → BiasAdd → ReLU
// 替换成一个 CANN 融合算子节点
for (Node* relu : g->nodes()) {
if (relu->type_string() != "Relu") continue;
Node* bias_add = relu->in_nodes()[0];
if (bias_add->type_string() != "BiasAdd") continue;
Node* conv = bias_add->in_nodes()[0];
if (conv->type_string() != "Conv2D") continue;
// 创建融合算子节点
Node* fused = g->AddNode(
"Conv2DBiasAddRelu",
conv->attrs() // 继承 Conv2D 的属性
);
// 重连边:fused 的输入 = conv 的输入和 bias
g->AddEdge(conv->in_nodes()[0], 0, fused, 0);
g->AddEdge(bias_add->in_nodes()[1], 0, fused, 1);
// fused 的输出 = relu 的输出
g->ReplaceEdge(fused, 0, relu->out_nodes()[0], relu->out_slot(0));
// 删除旧节点
g->RemoveNode(conv);
g->RemoveNode(bias_add);
g->RemoveNode(relu);
}
}
};
融合效果:Conv2D + BiasAdd + ReLU 三次 HBM 读写变成一次——中间结果全在 L1/L2 缓存里。ImageNet 训练时这层融合省掉约 18% 的 HBM 带宽。
device_plugin:NPU 设备管理
TensorFlow 的设备插件接口管理和 NPU 的通信。适配层实现一个 NpuDeviceFactory,让 TensorFlow 能识别 /device:NPU:0 到 /device:NPU:7:
// tensorflow/stream_executor/npu/npu_device.cc
class NpuDevice : public StreamExecutor {
public:
Status Init() override {
// 1. 枚举 NPU 设备(通过 driver 的 sysfs 接口)
int num_npus = read_sysfs_int("/sys/class/ascend/npu_num");
for (int i = 0; i < num_npus; i++) {
// 2. 初始化每个 NPU(加载固件、分配 HBM 池)
aclrtSetDevice(i);
aclrtReserveMem(32UL * 1024 * 1024 * 1024); // 预留 32GB HBM
}
// 3. 注册内存分配器(给 TensorFlow 的 BFC Allocator 用)
set_memory_allocator(new NpuBFCAllocator(num_npus));
return Status::OK();
}
Status Allocate(int64_t size, int64_t* ptr) override {
// 通过 CANN Runtime API 分配 HBM
void* hbm_ptr = nullptr;
aclrtMalloc(&hbm_ptr, size, ACL_MEM_MALLOC_HUGE_FIRST);
*ptr = reinterpret_cast<int64_t>(hbm_ptr);
return Status::OK();
}
Status Deallocate(int64_t ptr) override {
aclrtFree(reinterpret_cast<void*>(ptr));
return Status::OK();
}
};
// 注册到 TensorFlow 的设备工厂
REGISTER_LOCAL_DEVICE_FACTORY("NPU", 100, NpuDevice);
踩坑一:TF 的 eager mode 和 graph mode 混用
TensorFlow 2.x 默认是 eager mode(立即执行),但适配层的 graph_rewrite Pass 只在 graph mode 下生效。如果模型在 eager mode 下跑,融合 Pass 不会触发。
错误写法:
import tensorflow as tf
# 错误:eager mode 下跑,graph_rewrite 不生效
tf.config.set_visible_devices([], "GPU") # 禁用 GPU
# NPU 插件在 eager mode 下只做算子映射,不做图融合
model = tf.keras.applications.ResNet50()
output = model(tf.random.normal([32, 224, 224, 3]))
# 每个 Conv2D 单独调 CANN 算子,没有融合
# HBM 读写次数是融合后的 3 倍
正确写法:
import tensorflow as tf
# 正确:用 tf.function 把模型包成 graph
# graph_rewrite Pass 在 trace 时注入
@tf.function
def forward(x):
return model(x)
output = forward(tf.random.normal([32, 224, 224, 3]))
# graph 被 trace 后,Conv2D+BiasAdd+ReLU 已经被融合
# 只调一次融合算子,HBM 读写次数 1/3
C++ 侧原理:tf.function 把 Python 函数 trace 成 tf.Graph,然后调 Run() 执行——这时 graph optimization pass 才会运行。eager mode 下每个算子单独调 OpKernel::Compute(),不经过图优化。
踩坑二:NPU 内存分配器和 TensorFlow BFC Allocator 的 bin 大小不匹配
TensorFlow 的 BFC Allocator 把内存分成 256 个 bin(每个 bin 管理一种大小的内存块)。默认最大的 bin 是 2GB。但 NPU 的 HBM 分配器(aclrtMalloc)对超过 1GB 的连续分配会用 huge page,huge page 的分配成功率和碎片率有关。
错误现象:训练跑到一半,aclrtMalloc 返回 ACL_ERROR_RT_MEMORY_ALLOCATION_FAILED——HBM 还有空闲,但 continuous 分配失败(huge page 分配失败)。
缓解方法:调小 TensorFlow 的最大 bin 大小,让 BFC Allocator 多用小块分配:
import tensorflow as tf
# 限制 TensorFlow Allocator 的最大分配块为 512MB
# 减少 huge page 分配失败的概率
os.environ['TF_GPU_ALLOCATOR_MAX_BIN_SIZE'] = str(512 * 1024 * 1024)
# 或者用 CANN 的 memory pool 代替 TensorFlow BFC
os.environ['ASCEND_MEMORY_POOL'] = 'on'
踩坑三:算子类型注册遗漏
CANN 的算子支持多种 dtype(float16, float32, bfloat16)。适配层需要为每一种 dtype 组合注册 OpKernel。如果漏掉了某种组合,TF 在运行时报 No OpKernel registered。
错误现象:
import tensorflow as tf
# MatMul 的 OpKernel 只注册了 float32,没注册 float16
# 运行时报错:
# No OpKernel was registered to support Op 'MatMul' with these attrs:
# T in [DT_HALF]
output = tf.matmul(a.half(), b.half()) # 报错
正确写法:注册时加 ::type 约束,覆盖所有 dtype:
// 正确:为 float16 和 float32 都注册
REGISTER_KERNEL_BUILDER(
Name("MatMul")
.Device(DEVICE_NPU)
.TypeConstraint<float16>("T"),
MatMulOpKernel<float16>
);
REGISTER_KERNEL_BUILDER(
Name("MatMul")
.Device(DEVICE_NPU)
.TypeConstraint<float32>("T"),
MatMulOpKernel<float32>
);
性能实测
在 Atlas 900 PoD(8×Ascend 910)上跑 TensorFlow ResNet50 v1.5,batch_size=128:
| 配置 | 吞吐 (images/s) | 说明 |
|---|---|---|
| 无融合(eager mode) | 5,200 | 每个算子单独调 |
| 融合后(graph mode) | 7,800 | Conv 融合生效 |
| 融合 + XLA | 8,400 | XLA 额外 fusion |
融合 Pass 带来 50% 的吞吐提升。XLA 在 CANN 上的效果和 NVIDIA GPU 上类似——额外 7-10%。
tensorflow 适配层和 torchtitan-npu 做的事情本质一样:把框架算子映射到 CANN 算子库。但 TensorFlow 的 graph mode 优化空间更大——图融合 Pass 可以在整个计算图上做全局优化,而 PyTorch 的 eager mode 只能做局部融合(通过 TorchScript 或 dynamo)。这也是为什么 TensorFlow 在大规模分布式训练上仍有竞争力的原因之一。
更多推荐

所有评论(0)