TensorFlow 是业界主流训练框架之一。要让它识别昇腾 NPU、把图里的算子映射到 CANN 的算子库、把训练循环调度到 NPU 上——中间需要一整层适配代码。这个适配层就是 CANN tensorflow 仓库。它和 torchtitan-npu 的定位类似都是框架适配但技术路径完全不同。PyTorch 用 eager mode dispatch keyTensorFlow 用 graph mode op kernel 注册。适配层做的事情本质上一样把框架的算子调用翻译成 CANN 算子库的调用但实现机制不一样。适配层的三块拼图模块功能对应 torchtitan-npu 的模块op_kernel 注册把 TF Op 映射到 CANN 算子PyTorch dispatcher 注册graph_rewrite图优化算子融合、计算图切分TorchAir graph passdevice_pluginNPU 设备发现、内存分配、Stream 管理torch.npu 设备后端op_kernel 注册TensorFlow 的每一个算子是一个 OpKernel 子类。适配层对每一个 CANN 支持的算子写一个 OpKernel 实现// tensorflow/ops/cann/matmul_op.cc#includetensorflow/core/framework/op_kernel.h#includeascendc/matmul.h// CANN ops-nn 的 MatMul// 注册 MatMul 算子到 TensorFlowREGISTER_KERNEL_BUILDER(Name(MatMul).Device(DEVICE_NPU),// 自定义设备类型MatMulOpKernel);classMatMulOpKernel:publicOpKernel{public:explicitMatMulOpKernel(OpKernelConstruction*ctx){OP_REQUIRES_OK(ctx,ctx-GetAttr(transpose_a,transpose_a_));OP_REQUIRES_OK(ctx,ctx-GetAttr(transpose_b,transpose_b_));}voidCompute(OpKernelContext*ctx)override{constTensoractx-input(0);// [M, K]constTensorbctx-input(1);// [K, N]// 分配输出 Tensor在 NPU HBM 上Tensor*outputnullptr;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 streamctx-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),// Mb.dim_size(1),// Na.dim_size(1),// K1.0f,a.flatfloat().data(),a.dim_size(1),// ldab.flatfloat().data(),b.dim_size(1),// ldb0.0f,output-flatfloat().data(),b.dim_size(1),// ldcstream);aclblasDestroy(handle);}private:booltranspose_a_;booltranspose_b_;};和 PyTorch 的区别PyTorch 的 dispatcher 根据 tensor 的 device 类型torch.device(npu)自动路由到 CANN 算子。TensorFlow 需要显式注册 OpKernel——每一个算子都要写一个类。graph_rewrite图优化 PassTensorFlow 的计算图在运行前会经过 graph rewrite Pass。适配层注入自定义 Pass把图中连续的算子融合成 CANN 的融合算子// tensorflow/compiler/plugin/cann/graph_fusion_pass.ccclassCannFusionPass:publicGraphOptimizationPass{public:StatusRun(constGraphOptimizationPassOptionsoptions)override{Graph*goptions.graph-get();// Pass 1Conv2D BiasAdd ReLU → Conv2DFusionfuse_conv_bias_relu(g);// Pass 2MatMul BiasAdd GELU → MatMulFusionfuse_matmul_bias_gelu(g);// Pass 3Transpose MatMul Transpose → BertIntermediatefuse_transpose_matmul(g);returnStatus::OK();}private:voidfuse_conv_bias_relu(Graph*g){// 在图里找模式Conv2D → BiasAdd → ReLU// 替换成一个 CANN 融合算子节点for(Node*relu:g-nodes()){if(relu-type_string()!Relu)continue;Node*bias_addrelu-in_nodes()[0];if(bias_add-type_string()!BiasAdd)continue;Node*convbias_add-in_nodes()[0];if(conv-type_string()!Conv2D)continue;// 创建融合算子节点Node*fusedg-AddNode(Conv2DBiasAddRelu,conv-attrs()// 继承 Conv2D 的属性);// 重连边fused 的输入 conv 的输入和 biasg-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_pluginNPU 设备管理TensorFlow 的设备插件接口管理和 NPU 的通信。适配层实现一个NpuDeviceFactory让 TensorFlow 能识别/device:NPU:0到/device:NPU:7// tensorflow/stream_executor/npu/npu_device.ccclassNpuDevice:publicStreamExecutor{public:StatusInit()override{// 1. 枚举 NPU 设备通过 driver 的 sysfs 接口intnum_npusread_sysfs_int(/sys/class/ascend/npu_num);for(inti0;inum_npus;i){// 2. 初始化每个 NPU加载固件、分配 HBM 池aclrtSetDevice(i);aclrtReserveMem(32UL*1024*1024*1024);// 预留 32GB HBM}// 3. 注册内存分配器给 TensorFlow 的 BFC Allocator 用set_memory_allocator(newNpuBFCAllocator(num_npus));returnStatus::OK();}StatusAllocate(int64_tsize,int64_t*ptr)override{// 通过 CANN Runtime API 分配 HBMvoid*hbm_ptrnullptr;aclrtMalloc(hbm_ptr,size,ACL_MEM_MALLOC_HUGE_FIRST);*ptrreinterpret_castint64_t(hbm_ptr);returnStatus::OK();}StatusDeallocate(int64_tptr)override{aclrtFree(reinterpret_castvoid*(ptr));returnStatus::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 不会触发。错误写法importtensorflowastf# 错误eager mode 下跑graph_rewrite 不生效tf.config.set_visible_devices([],GPU)# 禁用 GPU# NPU 插件在 eager mode 下只做算子映射不做图融合modeltf.keras.applications.ResNet50()outputmodel(tf.random.normal([32,224,224,3]))# 每个 Conv2D 单独调 CANN 算子没有融合# HBM 读写次数是融合后的 3 倍正确写法importtensorflowastf# 正确用 tf.function 把模型包成 graph# graph_rewrite Pass 在 trace 时注入tf.functiondefforward(x):returnmodel(x)outputforward(tf.random.normal([32,224,224,3]))# graph 被 trace 后Conv2DBiasAddReLU 已经被融合# 只调一次融合算子HBM 读写次数 1/3C 侧原理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 pagehuge page 的分配成功率和碎片率有关。错误现象训练跑到一半aclrtMalloc返回ACL_ERROR_RT_MEMORY_ALLOCATION_FAILED——HBM 还有空闲但 continuous 分配失败huge page 分配失败。缓解方法调小 TensorFlow 的最大 bin 大小让 BFC Allocator 多用小块分配importtensorflowastf# 限制 TensorFlow Allocator 的最大分配块为 512MB# 减少 huge page 分配失败的概率os.environ[TF_GPU_ALLOCATOR_MAX_BIN_SIZE]str(512*1024*1024)# 或者用 CANN 的 memory pool 代替 TensorFlow BFCos.environ[ASCEND_MEMORY_POOL]on踩坑三算子类型注册遗漏CANN 的算子支持多种 dtypefloat16, float32, bfloat16。适配层需要为每一种 dtype 组合注册 OpKernel。如果漏掉了某种组合TF 在运行时报No OpKernel registered。错误现象importtensorflowastf# MatMul 的 OpKernel 只注册了 float32没注册 float16# 运行时报错# No OpKernel was registered to support Op MatMul with these attrs:# T in [DT_HALF]outputtf.matmul(a.half(),b.half())# 报错正确写法注册时加::type约束覆盖所有 dtype// 正确为 float16 和 float32 都注册REGISTER_KERNEL_BUILDER(Name(MatMul).Device(DEVICE_NPU).TypeConstraintfloat16(T),MatMulOpKernelfloat16);REGISTER_KERNEL_BUILDER(Name(MatMul).Device(DEVICE_NPU).TypeConstraintfloat32(T),MatMulOpKernelfloat32);性能实测在 Atlas 900 PoD8×Ascend 910上跑 TensorFlow ResNet50 v1.5batch_size128配置吞吐 (images/s)说明无融合eager mode5,200每个算子单独调融合后graph mode7,800Conv 融合生效融合 XLA8,400XLA 额外 fusion融合 Pass 带来 50% 的吞吐提升。XLA 在 CANN 上的效果和 NVIDIA GPU 上类似——额外 7-10%。tensorflow 适配层和 torchtitan-npu 做的事情本质一样把框架算子映射到 CANN 算子库。但 TensorFlow 的 graph mode 优化空间更大——图融合 Pass 可以在整个计算图上做全局优化而 PyTorch 的 eager mode 只能做局部融合通过 TorchScript 或 dynamo。这也是为什么 TensorFlow 在大规模分布式训练上仍有竞争力的原因之一。
昇腾CANN tensorflow:让 TensorFlow 在昇腾 NPU 上跑起来的适配层
发布时间:2026/5/22 20:49:05
TensorFlow 是业界主流训练框架之一。要让它识别昇腾 NPU、把图里的算子映射到 CANN 的算子库、把训练循环调度到 NPU 上——中间需要一整层适配代码。这个适配层就是 CANN tensorflow 仓库。它和 torchtitan-npu 的定位类似都是框架适配但技术路径完全不同。PyTorch 用 eager mode dispatch keyTensorFlow 用 graph mode op kernel 注册。适配层做的事情本质上一样把框架的算子调用翻译成 CANN 算子库的调用但实现机制不一样。适配层的三块拼图模块功能对应 torchtitan-npu 的模块op_kernel 注册把 TF Op 映射到 CANN 算子PyTorch dispatcher 注册graph_rewrite图优化算子融合、计算图切分TorchAir graph passdevice_pluginNPU 设备发现、内存分配、Stream 管理torch.npu 设备后端op_kernel 注册TensorFlow 的每一个算子是一个 OpKernel 子类。适配层对每一个 CANN 支持的算子写一个 OpKernel 实现// tensorflow/ops/cann/matmul_op.cc#includetensorflow/core/framework/op_kernel.h#includeascendc/matmul.h// CANN ops-nn 的 MatMul// 注册 MatMul 算子到 TensorFlowREGISTER_KERNEL_BUILDER(Name(MatMul).Device(DEVICE_NPU),// 自定义设备类型MatMulOpKernel);classMatMulOpKernel:publicOpKernel{public:explicitMatMulOpKernel(OpKernelConstruction*ctx){OP_REQUIRES_OK(ctx,ctx-GetAttr(transpose_a,transpose_a_));OP_REQUIRES_OK(ctx,ctx-GetAttr(transpose_b,transpose_b_));}voidCompute(OpKernelContext*ctx)override{constTensoractx-input(0);// [M, K]constTensorbctx-input(1);// [K, N]// 分配输出 Tensor在 NPU HBM 上Tensor*outputnullptr;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 streamctx-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),// Mb.dim_size(1),// Na.dim_size(1),// K1.0f,a.flatfloat().data(),a.dim_size(1),// ldab.flatfloat().data(),b.dim_size(1),// ldb0.0f,output-flatfloat().data(),b.dim_size(1),// ldcstream);aclblasDestroy(handle);}private:booltranspose_a_;booltranspose_b_;};和 PyTorch 的区别PyTorch 的 dispatcher 根据 tensor 的 device 类型torch.device(npu)自动路由到 CANN 算子。TensorFlow 需要显式注册 OpKernel——每一个算子都要写一个类。graph_rewrite图优化 PassTensorFlow 的计算图在运行前会经过 graph rewrite Pass。适配层注入自定义 Pass把图中连续的算子融合成 CANN 的融合算子// tensorflow/compiler/plugin/cann/graph_fusion_pass.ccclassCannFusionPass:publicGraphOptimizationPass{public:StatusRun(constGraphOptimizationPassOptionsoptions)override{Graph*goptions.graph-get();// Pass 1Conv2D BiasAdd ReLU → Conv2DFusionfuse_conv_bias_relu(g);// Pass 2MatMul BiasAdd GELU → MatMulFusionfuse_matmul_bias_gelu(g);// Pass 3Transpose MatMul Transpose → BertIntermediatefuse_transpose_matmul(g);returnStatus::OK();}private:voidfuse_conv_bias_relu(Graph*g){// 在图里找模式Conv2D → BiasAdd → ReLU// 替换成一个 CANN 融合算子节点for(Node*relu:g-nodes()){if(relu-type_string()!Relu)continue;Node*bias_addrelu-in_nodes()[0];if(bias_add-type_string()!BiasAdd)continue;Node*convbias_add-in_nodes()[0];if(conv-type_string()!Conv2D)continue;// 创建融合算子节点Node*fusedg-AddNode(Conv2DBiasAddRelu,conv-attrs()// 继承 Conv2D 的属性);// 重连边fused 的输入 conv 的输入和 biasg-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_pluginNPU 设备管理TensorFlow 的设备插件接口管理和 NPU 的通信。适配层实现一个NpuDeviceFactory让 TensorFlow 能识别/device:NPU:0到/device:NPU:7// tensorflow/stream_executor/npu/npu_device.ccclassNpuDevice:publicStreamExecutor{public:StatusInit()override{// 1. 枚举 NPU 设备通过 driver 的 sysfs 接口intnum_npusread_sysfs_int(/sys/class/ascend/npu_num);for(inti0;inum_npus;i){// 2. 初始化每个 NPU加载固件、分配 HBM 池aclrtSetDevice(i);aclrtReserveMem(32UL*1024*1024*1024);// 预留 32GB HBM}// 3. 注册内存分配器给 TensorFlow 的 BFC Allocator 用set_memory_allocator(newNpuBFCAllocator(num_npus));returnStatus::OK();}StatusAllocate(int64_tsize,int64_t*ptr)override{// 通过 CANN Runtime API 分配 HBMvoid*hbm_ptrnullptr;aclrtMalloc(hbm_ptr,size,ACL_MEM_MALLOC_HUGE_FIRST);*ptrreinterpret_castint64_t(hbm_ptr);returnStatus::OK();}StatusDeallocate(int64_tptr)override{aclrtFree(reinterpret_castvoid*(ptr));returnStatus::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 不会触发。错误写法importtensorflowastf# 错误eager mode 下跑graph_rewrite 不生效tf.config.set_visible_devices([],GPU)# 禁用 GPU# NPU 插件在 eager mode 下只做算子映射不做图融合modeltf.keras.applications.ResNet50()outputmodel(tf.random.normal([32,224,224,3]))# 每个 Conv2D 单独调 CANN 算子没有融合# HBM 读写次数是融合后的 3 倍正确写法importtensorflowastf# 正确用 tf.function 把模型包成 graph# graph_rewrite Pass 在 trace 时注入tf.functiondefforward(x):returnmodel(x)outputforward(tf.random.normal([32,224,224,3]))# graph 被 trace 后Conv2DBiasAddReLU 已经被融合# 只调一次融合算子HBM 读写次数 1/3C 侧原理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 pagehuge page 的分配成功率和碎片率有关。错误现象训练跑到一半aclrtMalloc返回ACL_ERROR_RT_MEMORY_ALLOCATION_FAILED——HBM 还有空闲但 continuous 分配失败huge page 分配失败。缓解方法调小 TensorFlow 的最大 bin 大小让 BFC Allocator 多用小块分配importtensorflowastf# 限制 TensorFlow Allocator 的最大分配块为 512MB# 减少 huge page 分配失败的概率os.environ[TF_GPU_ALLOCATOR_MAX_BIN_SIZE]str(512*1024*1024)# 或者用 CANN 的 memory pool 代替 TensorFlow BFCos.environ[ASCEND_MEMORY_POOL]on踩坑三算子类型注册遗漏CANN 的算子支持多种 dtypefloat16, float32, bfloat16。适配层需要为每一种 dtype 组合注册 OpKernel。如果漏掉了某种组合TF 在运行时报No OpKernel registered。错误现象importtensorflowastf# MatMul 的 OpKernel 只注册了 float32没注册 float16# 运行时报错# No OpKernel was registered to support Op MatMul with these attrs:# T in [DT_HALF]outputtf.matmul(a.half(),b.half())# 报错正确写法注册时加::type约束覆盖所有 dtype// 正确为 float16 和 float32 都注册REGISTER_KERNEL_BUILDER(Name(MatMul).Device(DEVICE_NPU).TypeConstraintfloat16(T),MatMulOpKernelfloat16);REGISTER_KERNEL_BUILDER(Name(MatMul).Device(DEVICE_NPU).TypeConstraintfloat32(T),MatMulOpKernelfloat32);性能实测在 Atlas 900 PoD8×Ascend 910上跑 TensorFlow ResNet50 v1.5batch_size128配置吞吐 (images/s)说明无融合eager mode5,200每个算子单独调融合后graph mode7,800Conv 融合生效融合 XLA8,400XLA 额外 fusion融合 Pass 带来 50% 的吞吐提升。XLA 在 CANN 上的效果和 NVIDIA GPU 上类似——额外 7-10%。tensorflow 适配层和 torchtitan-npu 做的事情本质一样把框架算子映射到 CANN 算子库。但 TensorFlow 的 graph mode 优化空间更大——图融合 Pass 可以在整个计算图上做全局优化而 PyTorch 的 eager mode 只能做局部融合通过 TorchScript 或 dynamo。这也是为什么 TensorFlow 在大规模分布式训练上仍有竞争力的原因之一。