土法炼钢兴趣小组的算法知识备份

【编译器与 MLIR】类型系统与属性

文章导航

分类入口
compilerarchitecture
标签入口
#mlir#llvm#compiler#type-system#attribute#dialect#tensor#memref#opbuilder

目录

类型系统与属性

上一章讲了 Operation、Value、Block、Region 的数据结构。但一个 IR 不仅是操作序列——每个 Value 有类型,每个 Op 可以带编译期属性。这一章讲 MLIR 的类型系统和属性机制。

一、Type 与 Attribute:两种元数据的根本区别

这是一个初学 MLIR 时最容易搞混的概念。简单说:

.mlir 文本格式中的位置可以帮你区分:

// Type 出现在冒号后面,描述 Value
%0 = arith.constant 42 : i32         // i32 是 Type(描述 %0 的类型)
// Attribute 出现在 Op 内部,或作为 #{...} 标注
%0 = arith.constant 42 : i32         // 42 (IntegerAttr) 是属性(常量值)
%cst = arith.constant dense<[[1,2],[3,4]]> : tensor<2x2xi32>  // dense<...> 是属性

设计上,Type 由 MLIR 的 TypeUniquer 管理——相同参数的 IntegerType::get(&ctx, 32) 总是返回同一个对象指针(字符串驻留式 uniquing)。Attribute 类似,由 AttributeUniquer 管理。

二、内建类型

MLIR 提供了一组分层的通用类型。这些类型直接可用,不需要加载任何方言。

2.1 标量类型

类型 构造方式 说明
IntegerType IntegerType::get(&ctx, 32) 任意位宽的整数;i1i8i32i64
FloatType FloatType::getF32(&ctx) IEEE 浮点数;f16f32f64bf16
IndexType IndexType::get(&ctx) 目标平台指针宽度整数,用于循环变量和数组索引

IndexType 的存在理由:编译器不知道目标平台的指针宽度(32 还是 64 位),但循环计数和数组索引在语义上是”下标”而不是”32 位整数”。使用 index 类型让 Pass 不需要知道目标字长——最终降阶到 LLVM 时,index 才被具体化为目标平台的指针宽度。

2.2 容器类型

TensorType:不可变的多维数组,形状在编译期已知(静态)或部分已知(动态 ?):

// 静态形状
tensor<256x256xf32>
tensor<3x224x224xf16>

// 动态形状
tensor<?x?xf32>
tensor<?x256xi32>
RankedTensorType t = RankedTensorType::get({256, 256}, FloatType::getF32(&ctx));
int64_t rank = t.getRank();            // 2
ArrayRef<int64_t> shape = t.getShape(); // {256, 256}
Type elementType = t.getElementType();  // f32

Tensor 在 MLIR 中是不可变的(immutable)。你不能”写入 tensor 的一个元素”——你只能创建一个新的 tensor。这来自函数式编程的设计,使得依赖分析不需要处理别名和副作用。当需要可变的缓冲区语义时,降阶到 MemRef

MemRefType:可变的、有内存布局的多维数组:

memref<256x256xf32>                       // 默认行列主序
memref<256x256xf32, affine_map<(d0,d1)->(d1,d0)>>  // 显式布局映射(列主序)
memref<256x256xf32, 3>                    // 地址空间 3(GPU shared memory)
memref<?x?xf32, offset: 16, strides: [128, 1]>  // 自定义基址偏移和步长
MemRefType m = MemRefType::get({256, 256}, FloatType::getF32(&ctx));
MemRefType m_strided = MemRefType::get(
    {256, 256}, FloatType::getF32(&ctx),
    MemRefLayoutAttrInterface{},  // layout
    0                              // memory space
);

MemRef 的布局字段(affine_map)允许表示自定义步长、数据对齐和地址空间——这是为 GPU 和异构内存模型设计的。memref<256x256xf32, 3> 中的 3 在 GPU 方言降阶路径中通常对应 workgroup(shared)内存,但具体地址空间编号因目标后端(NVPTX、SPIR-V 等)和 MLIR 版本而异,以你所用后端的文档为准。

VectorType:小规模、固定形状、适合 SIMD 向量化的类型:

vector<4xf32>          // 4 元素 f32 向量
vector<8x8xf32>        // 8×8 f32 矩阵(向量化的 tile)

TupleType:异构元素的固定顺序组合——类似 C 的 struct,但不命名字段:

tuple<i32, f64, tensor<256xf32>>

FunctionType:描述函数签名的输入和输出类型:

auto funcType = FunctionType::get(&ctx,
    {IntegerType::get(&ctx, 32), FloatType::getF64(&ctx)},  // 输入类型
    {IntegerType::get(&ctx, 1)}                              // 输出类型
);
// 等价于 (i32, f64) -> i1

2.3 NoneType

NoneType 是单元类型——只有一个值,没有数据。func.return 不带参数时返回 NoneType;没有返回值的 terminator 也用它。它的存在是为了让类型系统在面对”空”时有统一表示,而不是用 null 指针或异常值。

三、方言自定义类型

每个方言可以定义自己的类型——这些类型是”一等公民”,与其他方言共享 MLIR 的类型系统。定义流程:

第 1 步:在 TableGen 中声明(或者手动用 C++):

// MyDialectTypes.td
def My_TensorType : MyDialect_Type<"Tensor"> {
  let summary = "A custom tensor type in My dialect";
  let parameters = (ins
    "int64_t":$rank,
    "mlir::Type":$elementType
  );
}

或者直接在 C++ 中定义存储结构:

// MyDialectType.h
class MyTensorType : public Type::TypeBase<MyTensorType, Type, TypeStorage> {
public:
  using Base::Base;
  static MyTensorType get(MLIRContext *ctx, int64_t rank, Type elementType);

  int64_t getRank() const;
  Type getElementType() const;
};

第 2 步:实现 TypeStorage(类型参数的存储类,支持 uniquing、哈希和相等性判断):

struct MyTensorTypeStorage : public TypeStorage {
  using KeyTy = std::pair<int64_t, Type>;

  int64_t rank;
  Type elementType;

  // 构造函数——从 key 创建存储
  MyTensorTypeStorage(KeyTy key) : rank(key.first), elementType(key.second) {}

  // 相等性——决定两个 key 是否引用相同类型
  bool operator==(const KeyTy &key) const {
    return key == KeyTy(rank, elementType);
  }

  // 从 key 构造存储实例
  static MyTensorTypeStorage *construct(TypeStorageAllocator &allocator,
                                         const KeyTy &key) {
    return new (allocator.allocate<MyTensorTypeStorage>())
        MyTensorTypeStorage(key);
  }
};

第 3 步:在方言的 initialize() 中注册

void MyDialect::initialize() {
  addTypes<MyTensorType>();
}

TypeStorage 的 Key-Hash-Unique 模式确保了类型去重——两个 MyTensorType::get(&ctx, 2, FloatType::getF32(&ctx)) 调用返回同一个指针。这是编译器中处理大量类型对象的性能基础。

四、Attribute:编译期常量元数据

Attribute 是 MLIR 的编译期常量系统。凡是不在运行时流动的数据都用 Attribute 表示。

4.1 常用内建属性

Attribute 构造示例 含义
IntegerAttr IntegerAttr::get(intTy, 42) 整数常量(总是带类型)
FloatAttr FloatAttr::get(floatTy, 3.14) 浮点常量
StringAttr StringAttr::get(&ctx, "relu") 字符串常量
ArrayAttr ArrayAttr::get(&ctx, {a1, a2}) 属性数组
DictionaryAttr DictionaryAttr::get(&ctx, {kv1, kv2}) 键值对集合
DenseElementsAttr DenseElementsAttr::get(tensorTy, values) 密集张量数据
SymbolRefAttr SymbolRefAttr::get(&ctx, "func_name") 符号引用(函数名等)
UnitAttr UnitAttr::get(&ctx) 布尔标记(存在即真,不存在即假)
TypeAttr TypeAttr::get(type) 将 Type 包装为 Attribute

4.2 IntegerAttr 与 TypedAttr

IntegerAttr 总是带类型的——它不只是一个数字,而是一个带类型的编译期整数。这意味着 IntegerAttr::get(IntegerType::get(&ctx, 32), 0)IntegerAttr::get(IntegerType::get(&ctx, 64), 0) 是两个不同的属性,即使值都是 0。

对于常量 Op:

%c0 = arith.constant 42 : i32

其中 42 : i32IntegerAttr。在 C++ 中访问:

auto constOp = cast<arith::ConstantOp>(op);
auto value = cast<IntegerAttr>(constOp.getValue()).getInt();  // 42

TypedAttr 是所有带类型属性的基类——IntegerAttrFloatAttrDenseElementsAttr 都属于 TypedAttr。不带类型的属性有 StringAttrArrayAttrUnitAttr

4.3 DenseElementsAttr

DenseElementsAttr 是 AI 编译用的最多的属性——它表示一个密集张量的完整数据:

// 创建 2x2 的密集 f32 张量
auto tensorType = RankedTensorType::get({2, 2}, FloatType::getF32(&ctx));
std::vector<float> data = {1.0, 2.0, 3.0, 4.0};
auto denseAttr = DenseElementsAttr::get(tensorType, llvm::ArrayRef<float>(data));

运行时张量数据是通过 Value 流动的(tensor<2x2xf32>),但编译期已知的张量常量(如权重矩阵)存储在 DenseElementsAttr 中。

4.4 属性字典

每个 Operation 都有一个 DictionaryAttr——存储该 Op 的属性键值对:

Operation *op = ...;

// 检查属性是否存在
if (op->hasAttr("some_property")) { /* ... */ }

// 获取属性
IntegerAttr attr = op->getAttrOfType<IntegerAttr>("some_property");

// 设置/删除属性
op->setAttr("new_attr", StringAttr::get(&ctx, "value"));
op->removeAttr("obsolete_attr");

属性的修改是轻量的——DictionaryAttr 内部使用不可变的哈希表,修改操作返回新的 DictionaryAttr,类似于函数式数据结构的路径拷贝。

五、OpBuilder:在代码中构造 IR

OpBuilder 是程序化构造 MLIR IR 的主要接口——相当于手工写 .mlir 文件的 C++ 等价物:

OpBuilder builder(&ctx);

// 1. 创建类型
auto i32 = builder.getI32Type();
auto f32 = builder.getF32Type();

// 2. 在指定位置插入 Op
builder.setInsertionPointToStart(block);
auto constant = builder.create<arith::ConstantOp>(
    loc, i32, builder.getI32IntegerAttr(42));

// 3. 创建使用前一个结果的 Op
auto addOp = builder.create<arith::AddIOp>(
    loc, constant.getResult(), anotherValue);

// 4. 创建带有属性的 Op
auto forOp = builder.create<scf::ForOp>(
    loc, lowerBound, upperBound, step, iterArgs);
// 在 forOp 的 body Region 内继续构建 IR
builder.setInsertionPointToStart(forOp.getBody());

关键概念:

六、类型与属性在方言间的一致性

MLIR 的类型和属性是跨方言的统一体系。i32 在所有方言中都是相同的 IntegerTypetensor<256x256xf32> 是全局唯一的内建 TensorType

方言可以定义自己的类型和属性——这些类型和属性在方言加载时注册到 MLIRContext,之后所有方言都可以引用它们(即使不依赖那个方言)。但通常的约定是:自定义类型只在定义它的方言和那些显式依赖它的方言中使用。

七、本篇后续

理解了 Operation、Value、Type、Attribute 的体系后,下一个问题是:怎么高效地定义新的方言和 Op? 答案是 ODS(Operation Definition Specification)+ TableGen——这是下一篇的主题。

参考资料

官方文档(A 级)

源码(A 级)

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-06-09 · compiler / architecture

【编译器与 MLIR】MLIR 全景图与设计哲学

从 Module-Operation-Region-Block 四层结构出发,系统讲解 MLIR 的三条核心设计原则:渐进降阶、方言可组合性、基础设施复用,配合 IREE、CIRCT、Torch-MLIR 等实际案例建立心智模型。


By .