昇腾训练营报名链接:https://www.hiascend.com/developer/activities/cann20252#cann-camp-2502-intro

训练营简介:2025年昇腾CANN训练营第二季,基于CANN开源开放全场景,推出0基础入门系列、码力全开特辑、开发者案例等专题课程,助力不同阶段开发者快速提升算子开发技能。获得Ascend C算子中级认证,即可领取精美证书,完成社区任务更有机会赢取华为手机、平板、开发板等大奖。


前言

在深度学习中,Tensor(张量)是最核心的数据结构。理解Tensor在NPU中如何存储、如何操作,是写好CANN算子的关键。

昇腾CANN训练营

昇腾CANN训练营提供系统化的Tensor操作课程,从基础到进阶,手把手教你掌握昇腾NPU上的Tensor编程技巧!

我刚开始时,总搞不清楚Tensor的shape、stride、layout这些概念。后来通过大量实践,才真正理解Tensor在内存中的排布方式。今天就系统讲解Tensor的方方面面。

一、什么是Tensor?

1.1 Tensor的定义

Tensor是多维数组:

# 0维Tensor (标量)
scalar = 3.14

# 1维Tensor (向量)
vector = [1, 2, 3, 4]

# 2维Tensor (矩阵)
matrix = [[1, 2],
          [3, 4]]

# 3维Tensor
tensor3d = [[[1, 2], [3, 4]],
            [[5, 6], [7, 8]]]

# 4维Tensor (常用于图像: NCHW)
tensor4d = [batch, channel, height, width]

1.2 Tensor的属性

// CANN中的Tensor定义
LocalTensor<half> tensor;

// 主要属性:
// 1. 数据类型 (dtype): half/float/int32等
// 2. 形状 (shape): (N, C, H, W)
// 3. 大小 (size): 元素总数
// 4. 内存布局 (layout): NCHW/NHWC等

1.3 在CANN中创建Tensor

// 方式1:从Queue分配
LocalTensor<half> tensor1 = queue.AllocTensor<half>();

// 方式2:设置Global Buffer
GlobalTensor<half> tensor2;
tensor2.SetGlobalBuffer((__gm__ half*)ptr);

// 方式3:指定shape(高级用法)
TensorShape shape = {batch, channel, height, width};
LocalTensor<half> tensor3 = queue.AllocTensor<half>(shape);

二、Tensor的内存布局

2.1 连续内存 vs 非连续内存

Tensor在内存中是连续存储的一维数组:

// 逻辑上:2x3矩阵
[[1, 2, 3],
 [4, 5, 6]]

// 物理上:连续的内存
[1, 2, 3, 4, 5, 6]

通过索引计算访问多维数据:

// 访问tensor[i][j]
// 物理地址 = base + i * col_count + j
int index = i * 3 + j;
value = data[index];

2.2 NCHW vs NHWC

图像数据有两种常见布局:

NCHW布局(Channel-first)

Shape: (N=1, C=3, H=2, W=2)

内存排布:
[R00, R01, R10, R11,  // Red channel
 G00, G01, G10, G11,  // Green channel
 B00, B01, B10, B11]  // Blue channel

NHWC布局(Channel-last)

Shape: (N=1, H=2, W=2, C=3)

内存排布:
[R00, G00, B00,  // Pixel (0,0)
 R01, G01, B01,  // Pixel (0,1)
 R10, G10, B10,  // Pixel (1,0)
 R11, G11, B11]  // Pixel (1,1)

对比:

布局 优势 劣势 适用场景
NCHW 卷积计算友好 某些操作需要转换 PyTorch默认,CANN推荐
NHWC 某些硬件友好 卷积可能慢 TensorFlow默认

我的经验:CANN算子优先用NCHW,硬件对这种布局优化更好。

2.3 Stride(步长)

Stride描述如何在内存中跳跃访问:

// 2x3矩阵,NCHW布局
Shape: (2, 3)
Strides: (3, 1)
// 意思是:
// - 跳到下一行,需要跳3个元素
// - 跳到下一列,需要跳1个元素

// 访问[i][j]
index = i * stride[0] + j * stride[1]
      = i * 3 + j * 1

对于4维Tensor (N, C, H, W):

Strides = (C*H*W, H*W, W, 1)

// 访问[n][c][h][w]
index = n * (C*H*W) + c * (H*W) + h * W + w

三、常见Tensor操作

3.1 Reshape

改变shape,但不改变数据:

// 原始: (2, 6)
[[1, 2, 3, 4, 5, 6],
 [7, 8, 9, 10, 11, 12]]

// Reshape成 (3, 4)
[[1, 2, 3, 4],
 [5, 6, 7, 8],
 [9, 10, 11, 12]]

// 内存不变: [1,2,3,4,5,6,7,8,9,10,11,12]

CANN中的实现:

// Reshape通常不需要数据拷贝(如果连续)
// 只需要修改shape信息
TensorShape newShape = {3, 4};
// 设置新shape(具体API依版本而异)

3.2 Transpose

交换维度:

// 原始: (2, 3)
[[1, 2, 3],
 [4, 5, 6]]

// Transpose成 (3, 2)
[[1, 4],
 [2, 5],
 [3, 6]]

// 内存排布变了: [1,4,2,5,3,6]

CANN实现:

// Transpose需要实际搬运数据
__aicore__ void TransposeKernel(...) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            dst[j * rows + i] = src[i * cols + j];
        }
    }
}

性能优化:分块处理,提高cache命中率

3.3 Concat

拼接多个Tensor:

// 两个Tensor: (2,3)
A = [[1, 2, 3],
     [4, 5, 6]]
     
B = [[7, 8, 9],
     [10, 11, 12]]

// 在axis=0拼接,结果 (4,3)
Concat(A, B, axis=0) = 
[[1, 2, 3],
 [4, 5, 6],
 [7, 8, 9],
 [10, 11, 12]]

// 在axis=1拼接,结果 (2,6)
Concat(A, B, axis=1) = 
[[1, 2, 3, 7, 8, 9],
 [4, 5, 6, 10, 11, 12]]

CANN实现:

// axis=0: 直接按顺序拷贝
DataCopy(dst, src1, size1);
DataCopy(dst[size1], src2, size2);

// axis=1: 需要交错拷贝
for (int i = 0; i < rows; i++) {
    DataCopy(dst[i*newCols], src1[i*cols1], cols1);
    DataCopy(dst[i*newCols + cols1], src2[i*cols2], cols2);
}

3.4 Split

Concat的逆操作:

// 输入: (4, 3)
Input = [[1, 2, 3],
         [4, 5, 6],
         [7, 8, 9],
         [10, 11, 12]]

// 在axis=0 split成2份
Output1 = [[1, 2, 3], [4, 5, 6]]
Output2 = [[7, 8, 9], [10, 11, 12]]

四、数据拷贝与搬运

4.1 基本拷贝

// 完整拷贝
DataCopy(dst, src, count);

// 带偏移拷贝
DataCopy(dst[dstOffset], src[srcOffset], count);

4.2 跨步拷贝

// 每隔stride个元素拷贝一次
for (int i = 0; i < count; i++) {
    dst[i] = src[i * stride];
}

// CANN提供的API(某些版本)
DataCopyWithStride(dst, src, count, stride);

4.3 Padding拷贝

// 拷贝时填充0
DataCopyPad(dst, src, srcSize, dstSize, padValue);

// 示例:src有10个元素,dst要12个
// src: [1,2,3,4,5,6,7,8,9,10]
// dst: [1,2,3,4,5,6,7,8,9,10,0,0]

4.4 我踩过的坑

// ❌ 错误:越界拷贝
constexpr int TILE_SIZE = 256;
DataCopy(dst, src, 512);  // dst只有256,越界!

// ✅ 正确:检查边界
uint32_t copySize = std::min(TILE_SIZE, remainSize);
DataCopy(dst, src, copySize);

五、Tensor的向量化操作

5.1 Element-wise操作

逐元素操作,最常见:

// 加法
Add(z, x, y, count);  // z = x + y

// 乘法
Mul(z, x, y, count);  // z = x * y

// 乘加(融合)
Mla(z, x, y, w, count);  // z = x * y + w

// ReLU
Relu(z, x, count);  // z = max(0, x)

5.2 Reduce操作

沿某个轴归约:

// Sum reduce
// Input: (4, 256) -> Output: (4, 1)
for (int i = 0; i < 4; i++) {
    sum = 0;
    for (int j = 0; j < 256; j++) {
        sum += input[i][j];
    }
    output[i] = sum;
}

// CANN API
ReduceSum(output, input, axis, count);

5.3 矩阵运算

// 矩阵乘法: C = A * B
// A: (M, K), B: (K, N), C: (M, N)

// 朴素实现
for (int i = 0; i < M; i++) {
    for (int j = 0; j < N; j++) {
        sum = 0;
        for (int k = 0; k < K; k++) {
            sum += A[i][k] * B[k][j];
        }
        C[i][j] = sum;
    }
}

// CANN向量化(后续文章详解)
MatMul(C, A, B, M, K, N);

六、Tensor的高级技巧

6.1 In-place操作

直接在原Tensor上修改:

// ❌ 不好:需要额外内存
LocalTensor<half> temp = queue.AllocTensor<half>();
Relu(temp, input, count);
DataCopy(input, temp, count);
queue.FreeTensor(temp);

// ✅ 好:in-place
Relu(input, input, count);  // 直接在input上修改

注意:不是所有操作都支持in-place!

6.2 View操作(零拷贝)

只改变解释方式,不拷贝数据:

// 原始: (12,) 一维
data = [1,2,3,4,5,6,7,8,9,10,11,12]

// View成 (3, 4)
view1 = Reshape(data, (3, 4))
// 只改变shape,不拷贝数据

// View成 (2, 6)
view2 = Reshape(data, (2, 6))

前提:Tensor是连续的

6.3 广播(Broadcasting)

// Tensor A: (4, 1)
[[1],
 [2],
 [3],
 [4]]

// Tensor B: (1, 3)
[[10, 20, 30]]

// A + B 自动广播成 (4, 3)
[[11, 21, 31],
 [12, 22, 32],
 [13, 23, 33],
 [14, 24, 34]]

CANN中需要显式处理广播:

// 手动扩展维度
for (int i = 0; i < 4; i++) {
    for (int j = 0; j < 3; j++) {
        C[i][j] = A[i][0] + B[0][j];
    }
}

七、实战案例

案例1:图像转置(NCHW -> NHWC)

__aicore__ void TransposeNCHWtoNHWC(
    LocalTensor<half> dst,  // NHWC
    LocalTensor<half> src,  // NCHW
    uint32_t N, uint32_t C, uint32_t H, uint32_t W) {
    
    for (uint32_t n = 0; n < N; n++) {
        for (uint32_t h = 0; h < H; h++) {
            for (uint32_t w = 0; w < W; w++) {
                for (uint32_t c = 0; c < C; c++) {
                    // NCHW索引: n*C*H*W + c*H*W + h*W + w
                    uint32_t src_idx = n*C*H*W + c*H*W + h*W + w;
                    
                    // NHWC索引: n*H*W*C + h*W*C + w*C + c
                    uint32_t dst_idx = n*H*W*C + h*W*C + w*C + c;
                    
                    dst.SetValue(dst_idx, src.GetValue(src_idx));
                }
            }
        }
    }
}

优化版本(分块+向量化):

// 按tile处理,提高cache命中率
const int TILE_H = 16, TILE_W = 16;
for (int th = 0; th < H; th += TILE_H) {
    for (int tw = 0; tw < W; tw += TILE_W) {
        // 处理一个tile
        ProcessTile(dst, src, th, tw, TILE_H, TILE_W);
    }
}

案例2:Tensor拼接

__aicore__ void ConcatKernel(
    LocalTensor<half> output,
    LocalTensor<half> input1,
    LocalTensor<half> input2,
    uint32_t size1, uint32_t size2) {
    
    // 拷贝第一个Tensor
    DataCopy(output, input1, size1);
    
    // 拷贝第二个Tensor(带偏移)
    DataCopy(output[size1], input2, size2);
}

八、性能优化建议

8.1 内存访问模式

// ❌ 不好:跳跃访问
for (int i = 0; i < N; i++) {
    for (int j = 0; j < M; j++) {
        sum += data[j][i];  // 列优先,cache miss多
    }
}

// ✅ 好:顺序访问
for (int i = 0; i < N; i++) {
    for (int j = 0; j < M; j++) {
        sum += data[i][j];  // 行优先,cache友好
    }
}

8.2 避免不必要的拷贝

// ❌ 不好:多次拷贝
DataCopy(temp1, input, size);
DataCopy(temp2, temp1, size);
DataCopy(output, temp2, size);

// ✅ 好:直接拷贝
DataCopy(output, input, size);

8.3 使用向量操作

// ❌ 慢:标量循环
for (int i = 0; i < 1024; i++) {
    output[i] = input1[i] + input2[i];
}

// ✅ 快:向量操作
Add(output, input1, input2, 1024);

九、调试技巧

9.1 打印Tensor内容

void PrintTensor(LocalTensor<half> tensor, uint32_t count) {
    // 拷回CPU打印
    std::vector<half> hostData(count);
    DataCopyToHost(hostData.data(), tensor, count);
    
    for (uint32_t i = 0; i < std::min(count, 10u); i++) {
        printf("tensor[%d] = %.3f\n", i, (float)hostData[i]);
    }
}

9.2 验证shape

void VerifyShape(TensorShape expected, TensorShape actual) {
    if (expected != actual) {
        printf("Shape mismatch!\n");
        printf("Expected: ");
        PrintShape(expected);
        printf("Actual: ");
        PrintShape(actual);
        assert(false);
    }
}

9.3 检查数值范围

void CheckRange(LocalTensor<half> tensor, uint32_t count) {
    half minVal = tensor.GetValue(0);
    half maxVal = tensor.GetValue(0);
    
    for (uint32_t i = 1; i < count; i++) {
        half val = tensor.GetValue(i);
        minVal = std::min(minVal, val);
        maxVal = std::max(maxVal, val);
    }
    
    printf("Range: [%.3f, %.3f]\n", (float)minVal, (float)maxVal);
    
    // 检查是否有异常值
    if (std::isnan((float)maxVal) || std::isinf((float)maxVal)) {
        printf("⚠️ Warning: NaN or Inf detected!\n");
    }
}
Logo

CANN开发者社区旨在汇聚广大开发者,围绕CANN架构重构、算子开发、部署应用优化等核心方向,展开深度交流与思想碰撞,携手共同促进CANN开放生态突破!

更多推荐