Tensor操作基础:理解张量在NPU中的运作
在深度学习中,Tensor(张量)是最核心的数据结构。理解Tensor在NPU中如何存储、如何操作,是写好CANN算子的关键。昇腾CANN训练营提供系统化的Tensor操作课程,从基础到进阶,手把手教你掌握昇腾NPU上的Tensor编程技巧!我刚开始时,总搞不清楚Tensor的shape、stride、layout这些概念。后来通过大量实践,才真正理解Tensor在内存中的排布方式。今天就系统讲解
Tensor操作基础:理解张量在NPU中的运作
昇腾训练营报名链接:https://www.hiascend.com/developer/activities/cann20252#cann-camp-2502-intro
训练营简介:2025年昇腾CANN训练营第二季,基于CANN开源开放全场景,推出0基础入门系列、码力全开特辑、开发者案例等专题课程,助力不同阶段开发者快速提升算子开发技能。获得Ascend C算子中级认证,即可领取精美证书,完成社区任务更有机会赢取华为手机、平板、开发板等大奖。
前言
在深度学习中,Tensor(张量)是最核心的数据结构。理解Tensor在NPU中如何存储、如何操作,是写好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");
}
}
更多推荐



所有评论(0)