4、张量系统设计:张量数据结构定义、内存管理策略、数据布局(NHWC vs NCHW)

好,我们进入第四章。张量系统设计。

说实话,这是整个推理引擎的「地基」。你想想看,后面所有的算子、图优化、内存池,全都在这个数据结构上跑。地基没打好,后面全是坑。我自己最早做推理引擎时,就吃过这个亏——张量设计得太随意,结果后面加量化、加混合精度时,改得我头皮发麻。

4.1 张量数据结构定义

张量是什么?说白了,就是一个多维数组,外加一些元信息。但具体怎么定义,这里面有讲究。

我个人习惯把张量结构拆成三块:

  • 形状信息:维度、步长、元素个数
  • 数据信息:数据指针、数据类型、数据布局
  • 设备信息:在哪个设备上(CPU/GPU)、设备ID

嗯,这里要注意:千万别把形状和步长混为一谈。我见过不少新手把形状当步长用,结果做切片时维度全乱了。

来看一个我常用的定义:

typedef struct {
    int32_t ndim;           // 维度数量
    int32_t* dims;          // 各维度大小,比如 [1, 3, 224, 224]
    int32_t* strides;       // 各维度步长,单位是元素个数
    int64_t num_elements;   // 总元素数
    
    void* data;             // 数据指针
    DataType dtype;         // 数据类型:float32, int8, uint8...
    DataLayout layout;      // 数据布局:NHWC 或 NCHW
    
    DeviceType device;      // 设备类型:CPU, CUDA, OpenCL...
    int32_t device_id;      // 设备编号
    
    // 引用计数,用于内存管理
    int32_t ref_count;
} Tensor;

这里我特别想强调一下 strides。为什么要有它?因为推理引擎里经常要做「视图变换」——比如把一个 4D 张量 reshape 成 2D,或者做 transpose。如果没有 strides,每次都要重新分配内存,性能直接崩掉。

核心原则:张量的数据定义,要支持「零拷贝」的视图操作。这是高性能推理引擎的基石。

4.2 内存管理策略

内存管理,是推理引擎里最容易出 bug 的地方。我曾经在一个项目里,因为内存泄漏排查了整整三天,最后发现是张量析构时没释放设备内存。

我推荐三种策略,按场景选:

4.2.1 引用计数(Reference Counting)

这是最基础的做法。每个张量维护一个 ref_count,每被引用一次就加 1,释放时减 1。减到 0 就真正释放内存。

优点:简单,适合小规模推理。缺点:循环引用时会有泄漏风险。不过推理引擎里很少出现循环引用,所以够用了。

4.2.2 内存池(Memory Pool)

推理引擎里,张量频繁创建和销毁。每次都 malloc/free,性能太差。我建议用内存池:

  • 预分配一大块内存
  • 张量创建时从池里取
  • 张量销毁时还回池里
  • 池里按大小分桶,减少碎片

嗯,这里有个坑:GPU 内存池比 CPU 内存池难做得多。因为 GPU 内存分配本身就很慢,而且显存碎片更严重。我建议 GPU 内存池用「伙伴算法」或者「 slab 分配器」。

4.2.3 延迟分配(Lazy Allocation)

这个技巧我特别喜欢。很多张量在创建时,其实还不知道最终大小。比如动态 shape 的场景。这时候可以先分配一个「空壳」,等真正需要数据时再分配内存。

我的经验:在 ONNX 模型里,很多中间张量的 shape 是动态的。用延迟分配,可以避免大量无效的内存分配。我曾经在一个 BERT 模型上,用这个策略减少了 40% 的内存峰值。

4.3 数据布局:NHWC vs NCHW

好,到了最经典的话题。NHWC 还是 NCHW?

说白了,就是数据在内存里怎么排的。N 是 batch,C 是通道,H 是高度,W 是宽度。

  • NCHW:先排所有通道的第一个像素,再排第二个像素……
  • NHWC:先排第一个像素的所有通道,再排第二个像素的所有通道……

举个例子,一张 2x2 的 RGB 图片:

// NCHW 内存布局:[R00, G00, B00, R01, G01, B01, R10, G10, B10, R11, G11, B11]
// NHWC 内存布局:[R00, R01, R10, R11, G00, G01, G10, G11, B00, B01, B10, B11]

你可能会问:哪个更好?答案是——看硬件。

布局 优点 缺点 典型场景
NCHW 通道内连续,适合卷积计算 跨通道访问不连续 GPU(cuDNN 默认)
NHWC 像素内连续,适合逐元素操作 卷积时需转置 CPU(ARM、x86)、TensorFlow

我个人在项目中遇到过这样一个情况:一个 MobileNet 模型,在 GPU 上用 NCHW 跑得飞快,但部署到手机端(ARM CPU)时,性能直接掉了 3 倍。后来改成 NHWC,性能才恢复正常。

避坑指南:我曾经在一个项目里,模型训练用 NCHW,推理引擎默认 NHWC,结果没做 layout 转换,跑出来的结果全是错的。嗯,这个坑我踩过,你别踩。

4.3.1 如何选择?

我的建议是:

  1. GPU 推理:用 NCHW。cuDNN 和 TensorRT 对 NCHW 优化最好。
  2. CPU 推理:用 NHWC。ARM 的 NEON 指令集对 NHWC 更友好。
  3. 混合场景:在引擎内部统一用一种布局,在边界处做 layout 转换。

嗯,这里要注意:layout 转换本身也有开销。我建议在模型加载时一次性转换,不要在推理过程中反复转。

4.3.2 扩展:其他布局

除了 NHWC 和 NCHW,还有一些变种:

  • CHWN:Caffe 用的,现在已经很少见了
  • NC4HW4:Intel 的 oneDNN 用的,把 C 维度拆成 4 的倍数
  • NCHW16C:华为昇腾用的,16 通道对齐

这些变种说白了,都是为了适配特定硬件的 SIMD 指令宽度。你如果做通用推理引擎,建议先支持 NHWC 和 NCHW,其他的按需扩展。

总结一下:张量系统设计,核心就三件事——数据结构要灵活、内存管理要高效、数据布局要适配硬件。这三件事做好了,后面的算子实现会轻松很多。

好,这一章就到这里。下一章我们讲算子注册与调度,那是让张量「动起来」的关键。