阿楠第一天到“鑫泰智能制造”厂报到,车间里机器轰鸣,传送带一刻不停。他注意到每台设备上都钉着一块不锈钢铭牌:额定功率 3kW、模具编号 M12、压力上限 5 吨。传送带上的物料不断变化——这块是钢板、下块是铝件——但铭牌上的字纹丝不动。老师傅拍拍他肩膀:“想听懂这条流水线,先学会读铭牌。” MLIR 编译器就是这座智能工厂。方言是不同车间,Operation 是每台加工设备,操作数(operand)是传送带上流动的物料——运行时数据。而本文的主角属性(Attribute),就是钉在设备上的那块参数铭牌:它在编译期被写死,优化器一遍遍读取它做决策,可运行时流水线从来不会去改它。 从“铭牌是什么”讲到“铭牌怎么分类”,再讲到“铭牌如何帮优化器做纸面计算”,这篇文章用一座工厂的视角,带你彻底看懂 MLIR 的属性 Attributes。

岗位关联

属性是 MLIR 里“编译期元数据”的载体。掌握它,你才能真正读懂 IR、写出靠谱的模式匹配与优化 Pass,是编译器工程师的基础功。

目录
  1. 可视化演示
  2. 属性基本概念
  3. 属性类型体系
  4. 特殊属性类型
  5. 属性接口机制
  6. 属性经典用法
  7. 属性与重写优化
  8. 跨方言属性处理
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

本节配有交互动效(进入视口后自动加载,约 3KB)。

属性基本概念

阿楠在车间里发现,每台设备的不锈钢铭牌上都刻着三行字:额定功率、模具编号、压力上限。传送带上的工件换来换去,可铭牌从来不改。老师傅说:“铭牌管的是这台设备怎么用,不是这批工件是什么。” 一句话点醒了他——设备参数和流水线物料,是两回事。

MLIR 里每个 Operation(设备)同样挂着一组 Attribute(铭牌)。比如 arith.addi 这个加法操作:它究竟加哪两个数,由操作数(传送带上的工件)决定;但它的“可折叠性”“结果类型”这些特性,则由属性描述。编译器优化器会反复读铭牌做决策,运行时的数据流却永远不会去修改铭牌。

形式上,Attribute 是编译期不可变的静态元数据。一个操作可以通过 op->getAttr("name") 读取属性,修改和删除则必须走 setAttr / removeAttr 接口。属性既不是寄存器里的值,也不是内存里的张量,它与操作数是两条完全独立的通道。属性的生命周期由 MLIRContext 统一管理,同一份内容在上下文内只保存一份——这就是后文的唯一化机制。

阿楠在笔记本上写道:传送带上的工件会流动、会被加工,但设备铭牌纹丝不动,是设计师定好的“出厂参数”。理解这条界线,就理解了属性的第一课。

Attr:Op→(Key→Value)\text{Attr}: \text{Op} \rightarrow (\text{Key} \rightarrow \text{Value})
属性是从 Operation 到“键值对清单”的映射:每个操作拥有一张属性表,键是属性名,值是对应的属性实例。
  1. OpMLIR 中的 Operation,IR 的基本节点,相当于车间里一台具体的加工设备,是整套编译流水线的最小工作单元。
    Op\text{Op}
  2. Key → Value键值对表示一条参数项。Key 是属性名(如 name、pad),Value 是具体值(如字符串 "conv2d"、整数 1),合在一起就是铭牌上的一行字。
    Key→Value\text{Key} \rightarrow \text{Value}
  3. 整体映射每个 Operation 都拥有一张“参数清单”,把各个键映射到各自的值。这张清单在编译期定型,之后只被读取、不被修改。
    Attr:Op→(Key→Value)\text{Attr}: \text{Op} \rightarrow (\text{Key} \rightarrow \text{Value})

属性类型体系

铭牌上的字段格式各不相同:功率写数字,型号写字母串,配件清单则是一串多行参数。如果所有信息都糊成一行文字,读起来必然打架。工厂因此规定:不同字段使用不同的书写格式。

MLIR 属性也有自己的类型系统。最基础的是 IntegerAttr(整型属性)和 StringAttr(字符串属性):前者放 padding=1 这类数值参数,后者放 name="conv2d" 这类标识信息。它们是铭牌上最常见的两种“字体”。

组合场景则需要更复杂的格式。ArrayAttr(数组属性)保存一串同类型属性,比如 strides=[1,2] 就是一个包含两个整型属性的数组;DictionaryAttr(字典属性)则保存键值对集合,把一组关联参数捆绑成一个整体。所有属性都继承自 Attribute 基类,在 IR 文本打印时会带上类型信息——例如 #arith.constant<value=5 : i32> 明确说明这是一个 32 位整型属性。

懂了,铭牌上“功率=3kW”用的是数字格式,“型号=A3”用的是字母格式,“配件清单”则是一串数组。格式门当户对,才不会读错行。

A=[a1,a2,…,an],ai∈AttrA = [a_1, a_2, \dots, a_n], \quad a_i \in \text{Attr}
数组属性就是一张“一维清单”:每个元素本身也是一个属性,因此可以嵌套组合出 [[1,2],[3,4]] 这类结构化数据。
  1. A数组属性的整体表示,相当于铭牌上“配件清单”那一栏。
    AA
  2. ai ∈ Attr数组中每个元素都是一个合法的属性,说明“清单的每一项都遵守统一格式规范”。
    ai∈Attra_i \in \text{Attr}
  3. 整体等式的含义数组属性把多个属性按顺序收拢成一个整体,读取时可以用下标 [i] 取到第 i 项(从 1 开始编号直观对应)。
    A=[a1,a2,…,an]A = [a_1, a_2, \dots, a_n]

特殊属性类型

有些铭牌不走寻常路。阿楠在冲压车间看到一块铭牌,上面不是数字,而是钢印的模具三维图——它描述的是“部件应该长成什么样”;另一台检测设备上只敲了一个“已质检”章,没有任何数值;仓库货架旁的设备,铭牌上写的是一个“货位编号”,指向另一个地方。

这三类特殊属性在 MLIR 里都有对应。TypeAttr 的值本身是一个类型,比如 tensor<4x4xf32>,它描述“这一块数据应该以什么形状存在”。UnitAttr 是无值属性,不携带任何数据,只要存在就表示“该选项开启”,类似代码里的 flag(标志位)。FlatSymbolRefAttr(符号引用属性)保存一个符号名,用来引用函数或全局变量,比如 call @main 中的 @main 就是通过它完成的。

这三类属性看似简单,却在框架里高频出现:TypeAttr 帮助类型推导系统快速拿到张量形状;UnitAttr 常用来标注 no_inline、overflow 等可选项;FlatSymbolRefAttr 是模块级“跳转”的基石。新手最容易把它们混淆,因为它们打印出来都不是普通数值。

阿楠把三类记成三个形象:模具图(TypeAttr)、质检章(UnitAttr)和仓库货位号(FlatSymbolRefAttr)。都是铭牌,但“内容”完全不同——读铭牌前,先分清它是哪一种。

属性接口机制

工厂新来了一位质检员,面对各车间的五花八门设备,他只需要一个读卡器:所有铭牌都统一钉在机身右侧,字段命名规范,一刷就出数据。工厂为此专门立了《铭牌管理制度》。

MLIR 通过统一的 Attribute 基类接口约束所有属性。每个具体属性类都要实现 getId(获得唯一标识)、getDialect(返回所属方言)、print(打印文本)等接口。上层代码不需要知道具体类型,就能遍历、比较、打印任意属性——这就是“统一读卡器”的意义。

在 ODS(算子定义规范)中声明属性后,TableGen 会自动生成强类型访问器。例如给 StrideOp 声明 stride: I64ArrayAttr,框架就会生成 getStride() 方法,让 C++ 代码直接拿到数组值。访问器内部调用 getAttrOfType<ArrayAttr>("stride") 做类型转换,把“读铭牌”变成“点菜单”。此外,TypedAttr 接口进一步要求“属性必须携带类型信息”,使类型相关的算法可以统一处理所有带类型的属性。每个属性也通过 getDialect 知道自己属于哪个方言,跨方言时才能找到正确的翻译规则。

阿楠体会到了“统一读卡器”的好处:无论哪个车间的设备,只要铭牌规范,编译器框架的通用代码就能无障碍读取。接口机制,就是那本《铭牌管理制度》。

属性经典用法

真正讲究的铭牌,除了固定参数,还要处理“出生证明”和“运行档案”。阿楠看到仓库里一张巨型库存表:如果全量记录每个货位的状态,几百页纸都写不完;但老师傅只在一张稀疏表格上记录“哪排哪架有货”,其余空白。

DenseElementsAttr(稠密元素属性)以密集方式保存张量元素,权重、常量数据都常用它——每个位置都写,简单直接。SparseElementsAttr(稀疏元素属性)则只保存非零元素及其坐标,适合零值占比极高的矩阵。此外,布局属性(LayoutAttr)记录数据的内存排布,比如 NCHW 还是 NHWC,tile 形状是什么;名称属性(NameAttr)给操作或名字赋值,在符号表和调试信息里大量出现。选哪种属性,直接决定 IR 的体积和优化器遍历成本。

阿楠学会了:先看铭牌上的数据长什么样——全量密排、零值稀疏、还是带布局描述?选对属性格式,比硬塞进一个标准字段省得多。

memsparse=nnz×s≪memdense=n×m×s\text{mem}_{\text{sparse}} = \text{nnz} \times s \quad \ll \quad \text{mem}_{\text{dense}} = n \times m \times s
稀疏存储的内存只跟非零元素个数相关;当非零元素远小于总元素数时,比稠密全量存储省下几个数量级的空间。
  1. nnzNon-Zero,矩阵中非零元素的个数,是稀疏存储的核心变量。
    nnz\text{nnz}
  2. s单个元素占用的字节数(如 f32 类型占 4 字节)。
    ss
  3. n × m稠密矩阵的行数与列数,乘积就是总元素数。
    n×mn \times m
  4. 整体不等式稀疏只记“有值的位置”,当 nnz 远小于 n×m 时(比如 1% 非零),稀疏存储完胜。
    nnz×s≪n×m×s\text{nnz} \times s \ll n \times m \times s

属性与重写优化

老师傅每天做“纸面审查”:看到标着“可折叠”的设备,直接在排产表上算出结果,根本不用开机试运行;只有标着“必须现场加工”的,才放上流水线。纸面计算不耗电、不占机床,是最便宜的优化。

属性在模式匹配(Pattern-Matching)和常量折叠(Constant Folding)中扮演关键角色。匹配一段 IR 时,属性值就是“指纹”:匹配器会比较 op->getAttr("pad") 是否等于期望值,不满足就跳过当前候选操作。约束检查越严格,重写规则越精准。

折叠算法则检查每个操作:若所有操作数都是 IntegerAttr 等常量属性,且操作本身是纯函数(无副作用),就当场计算结果,把整个操作替换成一个新的常量操作。这个机制把大量“运行时的重复计算”变成“编译期的一次算术”。更广义的规范化(Canonicalization)会反复应用折叠、合并、消除规则,直到 IR 收敛到精简形态——这正是优化器质量的关键指标。

老师傅的“纸面计算”就是编译期的常量折叠。铭牌上的参数决定了能不能直接算答案——所有性能优化里,这是最便宜的一种。

fold(f(c1,c2,…,cn))=cres,若 f 纯函数且 ci∈Attrconst\text{fold}\big(f(c_1, c_2, \dots, c_n)\big) = c_{\text{res}}, \quad \text{若}~ f~ \text{纯函数且}~ c_i \in \text{Attr}_{\text{const}}
折叠规则:一个纯函数操作的所有输入都是编译期常量属性时,编译器可以在编译期算出结果,并替换为新的常量。
  1. f被折叠的操作,比如加法、乘法。要求它是纯函数——同样的输入永远得到同样的输出,且不修改外部状态。
    ff
  2. ci ∈ Attrconst所有输入都是编译期常量属性,例如 IntegerAttr(2)、IntegerAttr(3),而不是运行时计算出来的值。
    ci∈Attrconstc_i \in \text{Attr}_{\text{const}}
  3. cres算出的常量结果。折叠后,原来的 f(c1, c2, ..., cn) 整体被替换成一个常量操作,运行时只需读取,无需计算。
    cresc_{\text{res}}

跨方言属性处理

一台设备从 A 车间搬到 B 车间,铭牌不能原样带过去。B 车间有自己的字段规范,“功率=3kW”必须翻译成 B 车间写法的"power=3000mW",否则新车间的老师傅看不懂。

跨方言转换(DialectConversion)时,属性同样要“翻译”。新方言可以接收并保留原生属性,也可以定义转换规则把旧属性重写为新方言的属性。在 ConversionPattern 中,通常通过 rewriter.getRemappedAttr 把源属性映射为目标属性;规则没覆盖的属性则默认保留,由新方言决定是否接管。

做好属性转换要盯住三点:打印格式——IR 文本中 #dialect.attr 前缀让每个属性都有归属,读文本就能看出它来自哪个方言;内存唯一化——自定义属性必须实现 hash 和 ==,才能复用 MLIRContext 的缓存,否则每个对象各存一份,内存白白膨胀;常见陷阱——转换时丢了属性、类型不匹配、数组长度变化,都会导致后续 Pass 读到的铭牌“缺字”。处理好这三点,跨方言的 IR 才能正确往返(round-trip)。

阿楠想起老师傅的话:铭牌不是一成不变,而是“入乡随俗”地翻译。编译器的降级(lower)过程,就是一次又一次把铭牌翻译成更底层的方言语言,直到车间里的每一台机器都能接住。

AI 应用场景

深度学习编译器前端算子解析

阶段:训练模型导出 → 计算图编译(Graph Compilation)

解决的问题:TensorFlow 或 PyTorch 导出的计算图里,卷积核大小、padding、stride 这些超参散落在框架内部对象中。传统方式是每个前端重新解析一遍,代码重复且易错。利用 MLIR 属性,前端把 kernel=3、pad=1 固化为 IntegerAttr,把 data_format="NHWC" 固化为 StringAttr,统一挂在对应 Operation 上,后续所有优化 Pass 只要读属性即可。

具体案例:在框架适配层中,把 TF Conv2D 节点的 strides、padding 映射为 MLIR toy.conv2d 操作的 ArrayAttr 与 IntegerAttr,完成前端到 MLIR 的桥接。

推理引擎量化部署

阶段:模型量化(Quantization)→ 目标后端代码生成

解决的问题:量化参数(scale、zero_point)是推理正确性的关键元数据。以前用单独的结构体保存,在 Pass 里容易被遗漏;把 scale 与 zero_point 作为属性挂到量化算子(如 quant.dcast)上,跟随 IR 一起转换、一起打印、一起唯一化,生命周期由上下文管理,不容易丢失。

具体案例:对 ResNet 做 INT8 量化时,给每个权重操作附加 scale=0.0289, zero_point=34 两个浮点属性,后端代码生成直接读取属性生成定点乘加指令。

算子融合与硬件调度

阶段:后端优化(Tiling / Fusion)

解决的问题:GPU 或 NPU 上,tile 大小、流水线级数这些调度参数需要从编译期传递到后端。传统方式是全局配置表,模块化差。利用属性标注调度策略(如布局属性存储 tile 形状、UnitAttr 标注是否允许融合),同一个方言 IR 在不同硬件后端只需重新解释属性,而不用改 IR 结构。

具体案例:在 MLIR 的 GPU 方言中,给 matmul 操作附加 tile=[16,16] 的 ArrayAttr 和 unroll=1 的 IntegerAttr,GPU Pass 读取后生成对应 CUDA kernel 参数。

Python 演示

python85 行
# 模拟 MLIR 属性机制:唯一化 + 常量折叠class Attribute:    """属性基类:相同值的属性只保留一个实例(uniquing)。"""    _interned = {}     def __new__(cls, value):        key = (cls.__name__, value)        if key not in cls._interned:            node = super().__new__(cls)            node.value = value            cls._interned[key] = node        return cls._interned[key]     def __repr__(self):        return "{}({!r})".format(type(self).__name__, self.value)  class IntegerAttr(Attribute):    """整型属性:保存编译期整数常量。"""  class StringAttr(Attribute):    """字符串属性:保存编译期字符串,如算子名称。"""  class Operation:    """操作:模拟 MLIR Operation,携带操作数和属性字典。"""     def __init__(self, name, operands, attrs):        self.name = name        self.operands = operands        self.attrs = attrs     def get_attr(self, key):        return self.attrs.get(key)     def __repr__(self):        ops = ", ".join(repr(o) for o in self.operands)        attrs = ", ".join("{}={}".format(k, v) for k, v in self.attrs.items())        return "{}({}) [{}]".format(self.name, ops, attrs)  class ConstantOp(Operation):    """常量操作:只有一个 value 属性,是编译期常量。"""     def __init__(self, value_attr):        super().__init__("arith.constant", [], {"value": value_attr})  def fold_constants(ops):    """常量折叠:add 的两个操作数都是 IntegerAttr 时,编译期直接求和。"""    folded = []    for op in ops:        if op.name == "add":            left, right = op.operands            if isinstance(left, IntegerAttr) and isinstance(right, IntegerAttr):                folded.append(ConstantOp(IntegerAttr(left.value + right.value)))                continue        folded.append(op)    return folded  def main():    a2 = IntegerAttr(2)    a3 = IntegerAttr(3)    a2_again = IntegerAttr(2)    print("唯一化: IntegerAttr(2) is IntegerAttr(2) ->", a2 is a2_again)    print("唯一化: IntegerAttr(2) is not StringAttr('2') ->", a2 is not StringAttr("2"))    print("interned 表大小(折叠前):", len(Attribute._interned))     ops = [        ConstantOp(a2),        ConstantOp(a3),        Operation("add", [a2, a3], {}),    ]    print("折叠前:", ops)    folded = fold_constants(ops)    print("折叠后:", folded)    print("interned 表大小(折叠后):", len(Attribute._interned))    assert isinstance(folded[-1], ConstantOp)    assert folded[-1].attrs["value"].value == 5  if __name__ == "__main__":    main()
目的:用 60 行 Python 复刻 MLIR 属性的两个核心行为——同一上下文内相同属性的唯一化,以及基于属性条件的常量折叠。
  1. key = (cls.__name__, value)用“类型名 + 值”作为唯一键,保证 IntegerAttr(2) 与 StringAttr("2") 互不干扰,这正是 MLIR 唯一化表的简化版。
  2. node.value = value只在新实例创建时赋值,重复获取已有属性不会再次初始化。
  3. class IntegerAttr(Attribute):空类体即可区分属性类型,体现 MLIR 中“类型本身也是元信息”。
  4. def get_attr(self, key)模拟 op->getAttr() 的按名读取接口。
  5. isinstance(left, IntegerAttr) and isinstance(right, IntegerAttr)折叠判定条件:操作数必须是整型常量属性。
  6. folded.append(ConstantOp(IntegerAttr(left.value + right.value)))折叠发生时,整个加法操作被替换为一个新的常量操作。
  7. assert folded[-1].attrs["value"].value == 5最终验证折叠结果正确,体现“编译期纸面计算”的可信度。

常见误区

误区“属性不就是操作数吗?反正都是挂在 op 上的数据,运行时需要哪个就读哪个。”

正解二者是完全不同的两个通道。操作数是传送带上的物料——运行时数据流,由 SSA 值引用;属性是设备铭牌——编译期静态元数据,不参与运行时的值传递。在 MLIR 里,操作数会出现在 operands 列表中并被数据流追踪,属性则挂在 attributes 字典里,由编译器优化器读取。混用两者会导致优化器无法区分“可折叠常量”和“真实运行时依赖”。

为何常见:初看 IR 文本时,两者都出现在操作附近,形式上都像“附加信息”,容易视觉混淆。

误区“我要保存一个常量,统统一口气用 DenseElementsAttr 存大数据,省事。”

正解DenseElementsAttr 适合保存真正的稠密张量数据(如权重、常量矩阵)。对于单个整数参数、字符串标记,用 IntegerAttr / StringAttr 更轻量,内存占用小、打印清晰、比较更快。把一个小整数包装成大张量属性,是典型的资源浪费。

为何常见:教程里高频出现 DenseElementsAttr,新手以为它是“存储常量数据的通用容器”。

误区“属性不就是一个映射表吗?我在 A 方言里写上 `unroll=4`,B 方言里直接读就行,不需要额外处理。”

正解每个属性都有所属方言(通过 getDialect() 查询),IR 打印时带 #dialect.attr 前缀。跨方言时,B 方言不一定认识 A 方言的属性,需要显式转换或保留默认处理。正确做法是在 ConversionPattern 中通过 getRemappedAttr 做映射,或确认 B 方言会保留未知属性。

为何常见:属性形式上长得像通用 JSON,容易忽略它和方言的绑定关系。

误区“反正是字典结构,拿到 attrs 后直接 `op.attrs['pad'] = 1` 不就行了?”

正解MLIR 中属性是不可变且唯一化的。直接替换字典值可能绕过唯一化机制,导致同一逻辑值在上下文里出现多份实例。正确做法是调用 op->setAttr() / op->removeAttr() 接口,让框架完成唯一化注册和 IR 一致性维护。本 Python 演示中的 _interned 表正是为了强调这一点。

为何常见:Python 的 dict 随手可改,习惯性思维把 C++ 的 Attribute 也当成普通可变容器。

误区“检测到两个输入都是 IntegerAttr,就立刻折叠这个操作。”

正解常量折叠还有一个隐含前提:操作必须是纯函数(无副作用)。如果操作会修改内存、读取外部状态或产生非确定性结果(比如随机数),即使输入是常量也不能折叠。此外,输出类型必须能在编译期确定。折叠前请检查操作的 hasNoSideEffect 特性(ODS 中的 NoSideEffect trait)。

为何常见:教材常拿 add、mul 举例,让人误以为所有操作都满足折叠条件。

面试题

Q1. MLIR 中属性与操作数最本质的区别是?

单选题 · easy

  • A.属性是运行期信息,操作数是编译期信息
  • B.属性是编译期静态信息,操作数是运行期值流← 正确答案
  • C.属性和操作数语义一致,可以互换使用
  • D.属性优化后会消失,操作数不会消失

答案:B。属性是钉在设备上的铭牌(编译期静态信息),操作数是传送带上的物料(运行时值流)。A 说反了;C 说可互换完全错误;D 中属性优化后通常保留在最终 IR 中,不会自动消失。选 B,它准确描述了两条通道的本质分工。

Q2. IntegerAttr 最适合用来存什么?

单选题 · easy

  • A.整个计算图的运行时权重大小
  • B.算子名称这样的字符串标志
  • C.pad=1 这样的编译期整型参数← 正确答案
  • D.神经网络模型的全部权重张量

答案:C。IntegerAttr 就像铭牌上“功率=3kW”这样的数字字段,适合保存 pad、stride、unroll 等编译期整型参数。字符串标志应该用 StringAttr(B),权重张量应该用 DenseElementsAttr(D),运行时权重大小不是属性该管的事(A)。

Q3. 属性唯一化(uniquing)带来的好处包括?

多选题 · mid

  • A.相同内容的属性在上下文中只保存一份← 正确答案
  • B.不同方言的属性无法在同一 IR 中共存
  • C.节省内存,并让属性比较退化为指针比较← 正确答案
  • D.属性的值可以在编译期自动被修改

答案:AC。唯一化相当于“同一型号铭牌只铸一块”,让 IntegerAttr(2) 和 IntegerAttr(2) 指向同一实例(A),从而节省内存并加速比较(C)。B 错误,不同方言属性可以共存,各有 getDialect() 标识;D 错误,属性编译期不可自动修改,变更要走 setAttr 生成新实例。

Q4. UnitAttr 的正确语义是?

单选题 · mid

  • A.表示值为零的整型常量标记
  • B.无值属性,存在即表示某个开关开启← 正确答案
  • C.存储单位张量所需的所有元素
  • D.等价于一个空的字典属性

答案:B。UnitAttr 就是“质检章”——不携带数值,只要出现在属性表里就表示选项开启,比如 no_inline。A 把它误认为零值整数,C 把它和张量混淆,D 忽视它与 DictionaryAttr 的根本差异。记住“存在即有效”即可。

Q5. 关于 DenseElementsAttr 的描述正确的是?

单选题 · mid

  • A.只用于存储稀疏矩阵数据
  • B.密集保存张量元素的编译期副本← 正确答案
  • C.只能存储布尔类型的数组
  • D.存放运行时计算的临时结果

答案:B。DenseElementsAttr 像一张全量填写的表格,把张量每个元素都作为编译期数据保存,是权重和常量矩阵的常规载体。A 说反了,稀疏矩阵用 SparseElementsAttr;C 限制过窄,它能存整数、浮点等;D 错,它属于编译期 IR,不参与运行时临时存储。

Q6. 常量折叠成立的必要条件包括?

多选题 · hard

  • A.操作本身是纯函数,没有副作用← 正确答案
  • B.所有操作数都是编译期常量属性← 正确答案
  • C.折叠结果必须是更大的张量
  • D.输出类型在编译期能够确定← 正确答案

答案:ABD。常量折叠的“纸面计算”要求三件事:操作纯函数(A)、输入全是常量属性(B)、输出类型可以确定(D)。C 完全错误——折叠往往把大计算变成一个小常量,而不是更大的张量。这题对应正文中折叠规则的三个前置条件。

Q7. 跨方言转换时,属性最常见的默认处理是?

单选题 · hard

  • A.所有属性必须被转换为新方言属性
  • B.属性保留原样,由新方言决定是否接管← 正确答案
  • C.所有属性都转换为普通全局字符串
  • D.属性必须全部丢弃以简化转换流程

答案:B。跨方言转换时,规则未覆盖的属性默认会原样保留,新方言根据自己的认识决定是否接管或忽略,而不是强制全部转换(A)、字符串化(C)或丢弃(D)。这就像设备搬车间后,新车间没登记的铭牌字段会暂时挂着,直到有明确规则来翻译它。

Q8. 创建字符串属性最常用的 API 是?

单选题 · easy

  • A.IntegerAttr.get(……)
  • B.StringAttr.get(……)← 正确答案
  • C.UnitAttr.get(……)
  • D.TypeAttr.get(……)

答案:B。字符串属性自然由 StringAttr 创建,比如 StringAttr.get("conv2d")。A 创建整型,C 创建无值标记,D 创建类型属性。记住“想存什么类型,就用对应类型的工厂方法”,和铭牌字段“功率用数字格式、型号用字母格式”一个道理。

Q9. 属性在模式匹配中发挥的作用有?

多选题 · mid

  • A.读取操作属性并与期望值比对← 正确答案
  • B.用属性值来计算重写的收益← 正确答案
  • C.属性会自动修改被匹配的操作数
  • D.属性决定匹配后选择的模式分支← 正确答案

答案:ABD。属性在重写系统中是“指纹”:匹配时先读属性做比对(A),匹配成功后用属性值估算收益(B),多个模式并存时还能按属性区分分支(D)。C 错误,属性不会被自动修改,操作数由重写器显式更新——铭牌不会自己改动工件。

Q10. TypeAttr 的特殊之处在于?

单选题 · hard

  • A.它的值本身是一个 MLIR 类型定义← 正确答案
  • B.它只用于符号表的名字映射
  • C.它在打印时会自动被省略掉
  • D.它的值是一个 UnitAttr 标志

答案:A。TypeAttr 的“值”本身是一个 Type 对象,比如 tensor<4x4xf32>,相当于铭牌上刻着模具图而不是数值。B 说的是 FlatSymbolRefAttr 的职责;C 错误,TypeAttr 打印时清晰可见;D 混淆了 TypeAttr 与 UnitAttr。记住“描述长什么样”的属性就是 TypeAttr。