MemRef Dialect:内存引用与多维数组抽象

MemRef Dialect:内存引用与多维数组抽象

在控制流有了SCF支撑之后,数据在内存中的存储与访问便成为了下一个需要解决的核心问题。在底层编译中,直接操作扁平的一维指针(如float*)会丢失大量的高维空间信息。为了在底层依然能够高效地进行内存优化,MLIR 引入了MemRef Dialect(Memory Reference,内存引用方言)

MemRef填补了高层「张量(Tensor)」与底层「裸指针(Raw Pointer)」之间的物理鸿沟。它代表了一个驻留在内存中的多维数组,不仅包含了数据的首地址,还打包了维度(Shape)、步长(Strides)以及布局(Layout)等关键元数据。


核心操作(Operations)

MemRef Dialect 专注于内存的分配、读写和视图重塑,其核心 Op 包括:

  • memref.alloc:在堆上动态分配一块内存。
  • memref.alloca:在栈上分配一块局部内存(通常随作用域结束自动释放)。
  • memref.load:从多维坐标指定的内存位置读取一个标量值。
  • memref.store:将一个标量值写入到多维坐标指定的内存位置。
  • memref.dealloc:显式释放之前分配的内存,避免内存泄漏。

语法示例:二维矩阵的分配与读写

以下是一个标准的memref操作序列:

// 1. 分配一个 16x32 的单精度浮点数二维数组 %buffer = memref.alloc() : memref<16x32xf32> // 2. 定义坐标索引(在 MLIR 中控制坐标的类型通常是 index) %c0 = arith.constant 0 : index %c1 = arith.constant 1 : index // 3. 从坐标 (0, 1) 处加载数据 %val = memref.load %buffer[%c0, %c1] : memref<16x32xf32> // 4. 对数据进行处理(假设加了某个值,得到 %new_val) // ... // 5. 将新数据写回坐标 (0, 1) memref.store %val, %buffer[%c0, %c1] : memref<16x32xf32> // 6. 显式释放内存 memref.dealloc %buffer : memref<16x32xf32>

为什么 MemRef 至关重要?

  1. 保留多维几何信息:如果直接降级到硬件底层的扁平指针,原本形如matrix[i][j]的访问会被立即拍平为matrix[i * 32 + j]。这种乘加组合一旦形成,后端的硬件流水线优化器(如数据预取、缓存局部性优化)就很难反向推导其本来的访问规律。MemRef显式保留了多维索引,让编译器能清晰地看到内存访问模式。
  2. 零成本的视图切片(SubView):MemRef支持创建memref.subview操作。比如从一个100x100的大矩阵中切出一个10x10的子块。这个操作不需要发生任何实际的数据拷贝,它仅仅是创建了一个包含新步长(Strides)和偏移量(Offset)的新MemRef描述符。这对于矩阵乘法中的分块(Tiling)优化至关重要。
  3. 明确的数据边界:显式的allocdealloc使得生存期分析(Liveness Analysis)变得非常直接,方便编译器在后续阶段将其平滑地映射到 GPU 的共享内存(Shared Memory)或者专用的硬件缓冲区(SRAM)中。