类型系统与属性
上一章讲了 Operation、Value、Block、Region 的数据结构。但一个 IR 不仅是操作序列——每个 Value 有类型,每个 Op 可以带编译期属性。这一章讲 MLIR 的类型系统和属性机制。
一、Type 与 Attribute:两种元数据的根本区别
这是一个初学 MLIR 时最容易搞混的概念。简单说:
- Type:描述
Value的形状和语义——一个 SSA 值是什么类型,决定了哪些 Op 可以消费它、它在内存中占据多少空间。 - Attribute:描述 Op、Type 或函数的编译期常量——不会在运行时改变的元数据,如常量值、函数名、dimension 大小。
在 .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) |
任意位宽的整数;i1、i8、i32、i64 |
FloatType |
FloatType::getF32(&ctx) |
IEEE
浮点数;f16、f32、f64、bf16 |
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(); // f32Tensor 在 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) -> i12.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 : i32 是
IntegerAttr。在 C++ 中访问:
auto constOp = cast<arith::ConstantOp>(op);
auto value = cast<IntegerAttr>(constOp.getValue()).getInt(); // 42TypedAttr
是所有带类型属性的基类——IntegerAttr、FloatAttr、DenseElementsAttr
都属于 TypedAttr。不带类型的属性有
StringAttr、ArrayAttr、UnitAttr。
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());关键概念:
- InsertionPoint:控制新 Op 插入的位置——可以在 Block 的任意位置、特定 Op 之前或之后。
- Location:每个 Op 都有一个
Location,记录其源位置信息。通过builder.getUnknownLoc()获得默认位置,或用NameLoc、FileLineColLoc指定精确来源。 - Listener:
OpBuilder可以附加Listener,在 Op 的 insert/update/remove 时触发回调——这在 Pass 框架中被用于跟踪 IR 变化。
六、类型与属性在方言间的一致性
MLIR 的类型和属性是跨方言的统一体系。i32
在所有方言中都是相同的
IntegerType。tensor<256x256xf32>
是全局唯一的内建 TensorType。
方言可以定义自己的类型和属性——这些类型和属性在方言加载时注册到
MLIRContext,之后所有方言都可以引用它们(即使不依赖那个方言)。但通常的约定是:自定义类型只在定义它的方言和那些显式依赖它的方言中使用。
七、本篇后续
理解了 Operation、Value、Type、Attribute 的体系后,下一个问题是:怎么高效地定义新的方言和 Op? 答案是 ODS(Operation Definition Specification)+ TableGen——这是下一篇的主题。
参考资料
官方文档(A 级)
- MLIR Type System — https://mlir.llvm.org/docs/Dialects/BuiltinDialect/
- MLIR Attribute Documentation — https://mlir.llvm.org/docs/Dialects/BuiltinDialect/
源码(A 级)
mlir/include/mlir/IR/Types.hmlir/include/mlir/IR/BuiltinTypes.hmlir/include/mlir/IR/Attributes.hmlir/include/mlir/IR/BuiltinAttributes.hmlir/include/mlir/IR/OpBuilder.h
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【编译器与 MLIR】从零构建一个微型 Tensor DSL
手把手构建微型 Tensor DSL:ODS 定义方言、写 tiny-to-linalg 降阶 Pass,经标准管线生成 LLVM IR,走完编译链闭环(参考 MLIR Toy 教程)。
【编译器与 MLIR】AI 时代的编译器基础设施
从三阶段编译器局限出发,系统讲解 MLIR 方言、渐进降阶与 Pass 基础设施,覆盖 Tensor/Linalg/Affine/GPU 到框架桥接的完整编译链。
【编译器与 MLIR】MLIR 全景图与设计哲学
从 Module-Operation-Region-Block 四层结构出发,系统讲解 MLIR 的三条核心设计原则:渐进降阶、方言可组合性、基础设施复用,配合 IREE、CIRCT、Torch-MLIR 等实际案例建立心智模型。
【编译器与 MLIR】表驱动定义:ODS 与声明式编程
深入 MLIR 的 ODS 与 TableGen 工具链:从 .td 定义到自动生成的构建器、验证器、解析/打印器,理解声明式 IR 定义如何减少手写样板代码。