同一个矩阵乘法ND 排列跑 1200 tokens/sNZ 排列跑 1800——数据在内存里怎么摆直接影响达芬奇 Cube Unit 一次能吃进多少。Tensor Layout 不是什么神秘设定它是为了对齐 Cube Unit 的硬件约束而设计的。Cube Unit 一次算 16×16 个 FP16 的矩阵乘法。这意味着如果数据在内存里不是 16 对齐的Cube Unit 每次取数要跨行跨列做 Gather 操作——效率直接腰斩。Tensor排列就是围绕这个 16×16 硬约束展开的。ND 和 NZ两种排列两种场景ND 就是普通的行主序排列跟 NumPy 的默认行为一样。一个 32×32 的矩阵在 ND 下就是第一行 0-31、第二行 32-63……直到底。NZ 把矩阵切成 16×16 的块块内按行主序排。同一个 32×32 矩阵在 NZ 下前 256 个数是左上角 16×16 块的第一行原矩阵第 0-15 元素接着是这 16×16 块的第二行原矩阵第 32-47 元素……256 个元素刚好是 Cube Unit 一次吃进去的连续内存段。同一个 32×32 矩阵在 ND 和 NZ 下的内存布局 ND行主序: 地址 0-31: 第 0 行 地址 32-63: 第 1 行 地址 64-95: 第 2 行 ... NZ16×16 分块块内行主序: 地址 0-15: 块[0,0] 第 0 行 → 原矩阵元素 [0:16] 地址 16-31: 块[0,0] 第 1 行 → 原矩阵元素 [32:48] ... 地址 240-255: 块[0,0] 第 15 行 → 原矩阵元素 [480:496] 地址 256-271: 块[0,1] 第 0 行 → 原矩阵元素 [16:32] ...Cube Unit 一次 Load 读 16×16256 个连续元素。ND 下要跳 16 次 stride32 访问 16 行——16 次 Memory访问。NZ 下一次性连续读 256 个——1 次 Memory访问。这就是 NZ 加速 GEMM 的根本原因。5D 格式 NC1HWC0专为 Cube 设计图像处理里最常见的 4D 格式是 NCHWBatch × Channel × Height × Width。昇腾NPU 上跑 Conv 时ATC 编译器自动把 NCHW 转成 NC1HWC0。C1 和 C0 把通道维度切成了两块C0 16对齐 Cube Unit 的 16×16 计算C1 ceil(C/16)。C 不够 16 的倍数就补零。NCHW → NC1HWC0 转换以 C3 为例 NCHW: [B, 3, H, W] NC1HWC0: [B, ceil(3/16)1, H, W, 16] → C0 维度补齐到 16多余 13 个通道补零 → Cube Unit 一次处理 C016 个通道的 16×16 块布局转换的开销格式转换不是免费的。NCHW → NC1HWC0 一次转换约 10-15μs。一次 Attention 里有 QKV 三个投影——从 ND 进 NZ 出 GEMM、NZ 转回 ND 给 Softmax、再转 NZ 进下一个 GEMM——单层约 4 次转换32 层下来约 1.5-2ms。这些转换由 ATC 编译期自动插入一般不需要手动干预。但如果你在 msprof 里看到 FormatTrans 算子占推理总时间超过 8%可以检查是不是有不需要的重复转换——比如两个相邻 MatMul 之间如果数据直接留在 NZ 格式就能省掉一次 ND↔NZ 转换。# 同一个 Tensor 在不同 Layout 下的性能差异importnumpyasnp,time anp.random.randn(1024,1024).astype(np.float16)# ND 排列——连续行访问defaccess_nd(a,times1000):for_inrange(times):forrowinrange(1024):_a[row,:]# 每行连续Cache 友好# NZ 排列——连续块访问a_nza.reshape(64,16,64,16).transpose(0,2,1,3).reshape(1024,1024)defaccess_nz(a,times1000):for_inrange(times):forblockinrange(0,1024,16):_a[block:block16,:16]# 16×16 块连续# 在 NPU 上NZ 排列的 16×16 块是一次 Cube 取数的连续地址# ND 排列的同行访问也是连续的——但跨 16 行的 Gather 就不一样了C016 的硬件根源为什么 C0 是 16因为达芬奇 Core 的 Cube Unit 每个 cycle 算 16×16×16 的矩阵乘法L0C 矩阵乘 L0A——数据路径宽度 256BFP16 下刚好 16 个元素。C016 意味着 Cube Unit 取一次数就对齐了计算宽度。这不是软件设计选择是硬件决定的。参考仓库ops-tensor 张量操作库ATC 编译工具GE 图引擎CANN 学习中心
Tensor Layout:你的矩阵在 NPU 上到底怎么摆
同一个矩阵乘法ND 排列跑 1200 tokens/sNZ 排列跑 1800——数据在内存里怎么摆直接影响达芬奇 Cube Unit 一次能吃进多少。Tensor Layout 不是什么神秘设定它是为了对齐 Cube Unit 的硬件约束而设计的。Cube Unit 一次算 16×16 个 FP16 的矩阵乘法。这意味着如果数据在内存里不是 16 对齐的Cube Unit 每次取数要跨行跨列做 Gather 操作——效率直接腰斩。Tensor排列就是围绕这个 16×16 硬约束展开的。ND 和 NZ两种排列两种场景ND 就是普通的行主序排列跟 NumPy 的默认行为一样。一个 32×32 的矩阵在 ND 下就是第一行 0-31、第二行 32-63……直到底。NZ 把矩阵切成 16×16 的块块内按行主序排。同一个 32×32 矩阵在 NZ 下前 256 个数是左上角 16×16 块的第一行原矩阵第 0-15 元素接着是这 16×16 块的第二行原矩阵第 32-47 元素……256 个元素刚好是 Cube Unit 一次吃进去的连续内存段。同一个 32×32 矩阵在 ND 和 NZ 下的内存布局 ND行主序: 地址 0-31: 第 0 行 地址 32-63: 第 1 行 地址 64-95: 第 2 行 ... NZ16×16 分块块内行主序: 地址 0-15: 块[0,0] 第 0 行 → 原矩阵元素 [0:16] 地址 16-31: 块[0,0] 第 1 行 → 原矩阵元素 [32:48] ... 地址 240-255: 块[0,0] 第 15 行 → 原矩阵元素 [480:496] 地址 256-271: 块[0,1] 第 0 行 → 原矩阵元素 [16:32] ...Cube Unit 一次 Load 读 16×16256 个连续元素。ND 下要跳 16 次 stride32 访问 16 行——16 次 Memory访问。NZ 下一次性连续读 256 个——1 次 Memory访问。这就是 NZ 加速 GEMM 的根本原因。5D 格式 NC1HWC0专为 Cube 设计图像处理里最常见的 4D 格式是 NCHWBatch × Channel × Height × Width。昇腾NPU 上跑 Conv 时ATC 编译器自动把 NCHW 转成 NC1HWC0。C1 和 C0 把通道维度切成了两块C0 16对齐 Cube Unit 的 16×16 计算C1 ceil(C/16)。C 不够 16 的倍数就补零。NCHW → NC1HWC0 转换以 C3 为例 NCHW: [B, 3, H, W] NC1HWC0: [B, ceil(3/16)1, H, W, 16] → C0 维度补齐到 16多余 13 个通道补零 → Cube Unit 一次处理 C016 个通道的 16×16 块布局转换的开销格式转换不是免费的。NCHW → NC1HWC0 一次转换约 10-15μs。一次 Attention 里有 QKV 三个投影——从 ND 进 NZ 出 GEMM、NZ 转回 ND 给 Softmax、再转 NZ 进下一个 GEMM——单层约 4 次转换32 层下来约 1.5-2ms。这些转换由 ATC 编译期自动插入一般不需要手动干预。但如果你在 msprof 里看到 FormatTrans 算子占推理总时间超过 8%可以检查是不是有不需要的重复转换——比如两个相邻 MatMul 之间如果数据直接留在 NZ 格式就能省掉一次 ND↔NZ 转换。# 同一个 Tensor 在不同 Layout 下的性能差异importnumpyasnp,time anp.random.randn(1024,1024).astype(np.float16)# ND 排列——连续行访问defaccess_nd(a,times1000):for_inrange(times):forrowinrange(1024):_a[row,:]# 每行连续Cache 友好# NZ 排列——连续块访问a_nza.reshape(64,16,64,16).transpose(0,2,1,3).reshape(1024,1024)defaccess_nz(a,times1000):for_inrange(times):forblockinrange(0,1024,16):_a[block:block16,:16]# 16×16 块连续# 在 NPU 上NZ 排列的 16×16 块是一次 Cube 取数的连续地址# ND 排列的同行访问也是连续的——但跨 16 行的 Gather 就不一样了C016 的硬件根源为什么 C0 是 16因为达芬奇 Core 的 Cube Unit 每个 cycle 算 16×16×16 的矩阵乘法L0C 矩阵乘 L0A——数据路径宽度 256BFP16 下刚好 16 个元素。C016 意味着 Cube Unit 取一次数就对齐了计算宽度。这不是软件设计选择是硬件决定的。参考仓库ops-tensor 张量操作库ATC 编译工具GE 图引擎CANN 学习中心