小雨第一天来到编译器实验室报到。师傅递给她一张实验任务卡:「把烧杯 A 和烧杯 B 混合,做 add 反应。」手册上写明了材料:「A:体积 2×3,f32 浓度;B:体积 2×3,f32 浓度。」可手册的「产物规格」一栏却是空白的。小雨正要发问,师傅指了指手册角落的小字:「同体积、同浓度混合,产物规格与输入相同。」她恍然大悟——产物的体积和浓度,根本不用等实验做完才知道,照着配方和材料就能在动手前推出来。这个故事背后,正是 StableHLO 类型推导的精髓:编译器不用真正执行模型,就能根据每个算子的规则,推断出中间张量的形状与元素类型,从而为校验、优化和显存规划提供依据。本篇文章,我们就跟着小雨,把这个「实验室推演术」彻底弄明白。

岗位关联

掌握 StableHLO 类型推导,你将成为能开发 AI 编译器方言、定位形状错误、设计推理框架内存策略的编译器工程师——这是构建现代 AI 编译器基础设施的核心能力。

目录
  1. 可视化演示
  2. 类型系统基础:溶液的双重身份
  3. 算子契约:实验手册规定动作
  4. MLIR推导机制:化学家工作台
  5. 典型推导规则:四大试管配方
  6. 形状函数体系:配方背后的计算器
  7. 推导工程价值:为什么重要
  8. 实践排查指南:当推导失败时
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

\u5c55\u793aStableHLO\u7b97\u5b50\u7684\u7c7b\u578b\u63a8\u5bfc\u8fc7\u7a0b\u4e0e\u5e7f\u64ad\u8bed\u4e49\uff0c\u5305\u542b\u5f20\u91cf\u5f62\u72b6\u9759\u6001\u63a8\u65ad\u4e0eNumPy\u5e7f\u64ad\u89c4\u5219\u7684\u53ef\u89c6\u5316\u6f14\u793a\u3002

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

\u7c7b\u578b\u63a8\u5bfc\u6a21\u5f0f \u8f93\u5165\u5f20\u91cfA/B\u7ecf\u7b97\u5b50\u63a8\u5bfc\u8f93\u51fa\u5f62\u72b6 \u9759\u6001\u786e\u5b9a\u4e2d\u95f4\u5f20\u91cf\u5f62\u72b6 shape(A\u2299B) = broadcast(A, B)

类型系统基础:溶液的双重身份

小雨来到试剂柜前,发现每一瓶溶液都贴着两张标签:一张写「体积规格」(比如 2×3,表示长方体试剂瓶的底面是 2 行 3 列),另一张写「浓度类型」(比如 f32,表示用 32 位浮点记录浓度)。这两张标签共同决定了这瓶溶液的身份。张量在 StableHLO 里也有这样两张标签,合在一起就是它的「身份证」。

这张身份证在编译器里的正式写法是 tensor<2x3xf32>:尖括号里前半段 2x3 是形状(shape),后半段 f32 是元素类型(element type)。任何 StableHLO 算子一开始接触张量,就要先核验这张身份证。形状的每个维度既可以是静态的(写死数字),也可以是动态的(用 ? 表示,留到运行时才知道);元素类型则决定每个数值占用多少存储空间、如何参与运算。两者组合成类型,这是 MLIR 所有验证与推导的地基。

静态维度好比标签上写死「精确体积 2×3」,动态维度则像「加蒸馏水至 1000mL 刻度」——前者立刻可知,后者要等操作进行到那一步才能确定。张量带着这张或清晰或模糊的标签走进算子,接下来就看实验手册如何规定动作了。

Type(T)=Shape(T)×ElementType(T)\displaystyle \text{Type}(T) = \text{Shape}(T) \times \text{ElementType}(T)
这个公式说明:一个张量类型由「形状」和「元素类型」两个信息拼装而成,缺一不可。
  1. 张量完整类型 Type(T)指张量 T 的完整类型描述,例如 tensor<2x3xf32>,这是编译器识别数据的第一手资料。
    Type(T)\text{Type}(T)
  2. 形状 Shape(T)描述各维长度,例如 2x3 表示第 0 维长 2、第 1 维长 3,类似溶液瓶的「体积规格」。
    Shape(T)=[2,3]\text{Shape}(T) = [2, 3]
  3. 元素类型 ElementType(T)描述每个元素的精度,例如 f32 表示 32 位浮点,类似溶液的「浓度类型」。
    ElementType(T)=f32\text{ElementType}(T) = \text{f32}
  4. 组合成完整类型把形状和元素类型用 tensor<> 拼起来,「体积」与「浓度」共同定义一瓶溶液的身份。
    Type(T)=tensor<Shape(T)×ElementType(T)>\text{Type}(T) = \text{tensor}<\text{Shape}(T) \times \text{ElementType}(T)>

算子契约:实验手册规定动作

实验室里有一排「实验手册」,每本都写死了三栏:材料、条件、产物。比如 add 手册——材料:A、B 两瓶溶液;条件:浓度类型必须一致;产物:留白,待推断。而 broadcast_in_dim 手册更讲究,材料栏之外还有一行小字:「注明每个输入维度搬到输出的第几位」,这就是属性(attribute)。每本手册都构成一份「契约」,规定操作成立的全部前提。

在 StableHLO 中,每个算子都遵循这样的契约:声明自己的输入操作数(operands)、属性(attributes)、区域(regions)和输出结果(results)。输出类型往往由推导完成,但契约里写明了推导所需的全部前提。属性是推导形状的关键「实验条件」:stablehlo.broadcast_in_dim 用 broadcast_dimensions 属性说明输入的每一维对应输出形状的哪一维;stablehlo.dot_general 用 batch_dims 与 contracting_dims 指出批量维度与收缩维度;stablehlo.reduce 用 dimensions 属性列出哪些维度要被归约掉。

区域(region)则是手册里的「循环小节」,像 while 算子携带一个 body 区域,区域内部还有自己的类型约束——循环携带的张量每次迭代类型必须保持稳定。这些前提合在一起,就是推断产物规格的「充分条件」。手册把材料与条件写清楚,产物栏留给统一机制去推导——这就是 StableHLO 算子的设计哲学:不重复描述已知,把类型推导交给下一节的「推演仪器」。

MLIR推导机制:化学家工作台

小雨桌上有一台「推演仪器」:把输入试管插进左槽,翻到手册对应细则,摇动把手,右槽就会掉出一张写着产物规格的纸片。这台仪器不是水晶球,而是一套严谨的机械传动:读类型、套规则、写结论。在 MLIR 中,这套传动就是类型推导机制,由两类接口驱动:InferTypeOpInterface 提供静态方法 inferReturnTypes,直接返回完整的结果类型列表;InferShapedTypeOpInterface 提供 inferReturnTypeComponents,返回更灵活的「形状组件」(ShapedTypeComponents)。

形状组件的价值在于「可以不把话说死」:当某些输出维度是动态的,组件只记录已知的维度,对未知维度保留约束而不填具体数字。推导在什么时候触发?最典型的是算子被构造时(builder 调用)和 IR 验证阶段(verifier 调用)。如果推理失败,接口通过 emitError 产生一条包含算子位置、输入类型和冲突细节的诊断消息,编译随即停止——化学家发现配方有冲突,绝不会硬做实验。

有了这台仪器,小雨不用每次「把实验做完」才知道产物规格——编译器同样不用「把模型跑完」就知道中间张量的类型。接下来我们看看仪器里最常用的四套「配方」。

infer(op,operands,attrs)→types∣error\displaystyle \text{infer}(\text{op}, \text{operands}, \text{attrs}) \to \text{types} \mid \text{error}
推导引擎吃进算子、输入类型与属性,吐出类型列表或错误诊断,像推演仪器吃进试管、吐出规格单。
  1. op待推导的算子实例,比如一个 stablehlo.add,对应手册里的实验步骤。
    op\text{op}
  2. operands算子输入操作数的类型集合,即「材料栏」里的各瓶溶液。
    operands=[Type(A),Type(B)]\text{operands} = [\text{Type}(A), \text{Type}(B)]
  3. attrs算子携带的属性集合,即手册「条件栏」里的实验参数。
    attrs={broadcast_dimensions,…}\text{attrs} = \{\text{broadcast\_dimensions}, \ldots\}
  4. types | error推导成功的类型列表或失败诊断,二选一,不会有第三种结果。
    types∣error\text{types} \mid \text{error}

典型推导规则:四大试管配方

配方一:逐元素同型。 add 这类逐元素算子,像两瓶同规格溶液直接混合。条件是两瓶溶液规格完全一致,产物规格也随之确定。

配方二:广播对齐。 当两个输入形状不同但可兼容时,广播规则把「小溶液」按维度扩展成「大网格」再混合。维度的对齐从右向左,大小为 1 的维度自动扩展到对方大小。

配方三:矩阵乘维度。 矩阵乘像「串联反应器」:A 管出口的 K 号管径必须接上 B 管入口的 K 号管径。

配方四:归约去维度。 reduce 像「把指定行的数据汇总成一行」,输出形状就是删掉这些维度。

这四大配方覆盖了 StableHLO 绝大多数算子的形状推理。把它们固化成公式后,就能交给一个统一的函数去执行——这就是下一节的形状函数。

out=tensor<shape(A)×elem(A)>if shape(A)=shape(B), elem(A)=elem(B)\displaystyle \text{out} = \text{tensor}<\text{shape}(A) \times \text{elem}(A)> \quad \text{if } \text{shape}(A)=\text{shape}(B), \text{ elem}(A)=\text{elem}(B)
reduce 的输出形状等于输入形状删去归约维度后剩下的部分。
  1. dims 指明删除对象属性 dimensions 列出要归约掉的维度编号,像手册圈出「这几行要汇总」。
    dims=[d1,d2,…]\text{dims} = [d_1, d_2, \ldots]
  2. 按位置删除从形状中删去这些维,剩余维度顺序不变。
    remove(shape(A),dims)\text{remove}(\text{shape}(A), \text{dims})
  3. 成功与失败维度编号越界或重复时推导失败,其余情况输出剩余形状。
    out_shape\text{out\_shape}

形状函数体系:配方背后的计算器

手册角落挂着一台小小的「计算器」,上面刻着配方对应的数学式。小雨只要把输入规格拨进去,计算器就咔哒咔哒吐出产物形状。这台计算器,就是形状函数(shape function)。StableHLO 为每个算子导出一个 compute_shape_fn:当算子无法直接给出输出类型时,MLIR 的 shape function 机制把「如何算形状」从算子定义中分离出来,做成可复用、可测试的纯函数。

形状函数支持符号化推导:当输入包含动态维度(?),它把未知维度当作符号变量参与运算,产出一个符号表达式而不是立即报错。符号化推导让编译器可以「纸上谈兵」,把不确定的维度作为变量保留,等到后续 pass 再做具体化(specialization)。对于 while 循环,类型推导还施加一条「循环不变约束」——每次迭代携带的(carried)张量类型必须与上一次完全一致,否则推导直接拒绝,防止出现越跑形状越乱的失控反应器。

形状函数把「实验操作」抽象成「数学计算」,让蒸馏、配料这些工艺脱离实物,在纸面上先推演一遍。这正是编译器静态分析的浪漫:不用点火,就能知道反应的产物规格。

type(carryi)=type(carryi+1),∀i\displaystyle \text{type}(\text{carry}_i) = \text{type}(\text{carry}_{i+1}), \quad \forall i
while 循环要求每次迭代携带的张量类型严格不变,这是循环推导的「压舱石」。
  1. carry_i第 i 次迭代开始时携带的张量,像反应器里上一轮留下的产物。
    carryi\text{carry}_i
  2. type(……)取该张量的完整类型,包括形状与元素类型。
    type(carryi)\text{type}(\text{carry}_i)
  3. 迭代间相等第 i 次的类型必须等于第 i+1 次,类型严格不变。
    type(carryi)=type(carryi+1)\text{type}(\text{carry}_i) = \text{type}(\text{carry}_{i+1})
  4. 对全部迭代成立约束覆盖每一次迭代,任何一步漂移都导致推导失败。
    ∀i\forall i

推导工程价值:为什么重要

知道产物规格后,小雨一口气解决了三件大事:提前预订烧杯(内存)、规划工序(优化)、核对禁配表(校验)。类型推导的工程价值,正是把这三件事从运行时提前到编译期。第一是编译期正确性:形状不匹配、元素类型冲突在编译期就被拦下,像「禁配表」防止把不相容的试剂倒在一起,避免模型跑了一半才崩溃。

第二是优化触发器:算子融合、向量化、重排都需要精确的形状信息。知道每个中间张量的形状,编译器才能判断「这两个算子的布局是否兼容、能否合并」,像工序规划必须知道产物尺寸。第三是内存规划依据:推导输出张量大小后,编译器可以为显存池预留空间。

第四是跨方言稳定 ABI:StableHLO 与 HLO 互相转换时,类型推导保证两侧形状语义一致,像不同实验室共用一套容器规格标准,任何一侧的改动都不会悄悄破坏另一侧。有了推导,编译器不用等运行时才「大吃一惊」——这正是工业级编译器的底气。

bytes=∏d∈shaped×sizeof(elem)\displaystyle \text{bytes} = \prod_{d \in \text{shape}} d \times \text{sizeof}(\text{elem})
输出张量的字节数等于各维度乘积再乘以单个元素大小,是显存规划的基本公式。
  1. 维度乘积把形状里所有维度相乘,得到元素总数,像数清烧杯网格的格子数。
    ∏d∈shaped\prod_{d \in \text{shape}} d
  2. 单个元素大小sizeof(elem) 表示每个元素占多少字节,f32 占 4 字节。
    sizeof(elem)=4\text{sizeof}(\text{elem}) = 4
  3. 总字节数两者相乘,就是中间张量在显存中占用的字节数。
    bytes\text{bytes}

实践排查指南:当推导失败时

小雨第一次接 while 循环反应器,仪器吐出一张红条:「循环体携带值形状漂移!」她盯着红条愣了半天,最后乖乖把 body 区域里的 reshape 删掉,故障消失。推导失败并不可怕,可怕的是不会读诊断信息。最常见的失败场景有四类:操作数元素类型不一致(f32 与 s32 混用)、广播维度无法对齐(2x3 与 3x1)、必要属性缺失(broadcast_in_dim 忘写 broadcast_dimensions)、循环携带值形状漂移。

错误消息解读有章可循:inferReturnTypes 产出的诊断消息包含算子位置和冲突细节,例如 "requires the same element type for all operands"。读消息时先定位「哪个算子推导失败」,再看「哪个前提被违反」,最后顺着 IR 回溯到出错的那一层。调试工具链方面,mlir-opt --debug 可以逐 pass 观察 IR,-print-ir-after-all 打印每个 pass 之后的 IR,帮助找出类型「从哪一步开始失真」。而 stablehlo-opt -verify-diagnostics 支持在测试文件里用 // expected-error 声明错误位置,把验证诊断写进回归测试,防止「改一个算子弄坏十个下游」。化学家靠红条和检修记录积累经验,编译器工程师靠诊断消息和回归测试一次次加固推导引擎——每一次红条,都是通往更可靠编译器的台阶。

---

AI 应用场景

ONNX 模型转换时的形状补齐

阶段:IR 转换与类型完善

解决的问题:从 PyTorch 导出的 ONNX 图常带形状缺失(例如动态 batch),传统做法是手写 shape 推断脚本,算子一多维护成本就失控。AI 编译器可以复用 MLIR 的类型推导引擎,把「补形状」变成标准的编译期步骤。

具体案例:某推理团队把 ONNX 图转 StableHLO 时,通过 MLIR 的推导接口自动补齐所有中间张量形状;batch 维度先用 ? 保留,在后续 specialization pass 中固化为 8,整个流程无需手写一行推断代码。

推理引擎的显存池规划

阶段:编译期优化与内存分配

解决的问题:端侧 NPU 显存紧张,运行时才发现「这个中间张量要 96MB」为时已晚。通过类型推导,编译器在编译期就拿到每个中间张量的精确形状,为显存池复用提供依据。

具体案例:在 x86 服务器上跑 StableHLO 的 conv 算子,推导出输出形状 [1, 64, 112, 112],按 f32 计算占用约 96MB,内存池据此预分配并复用,碎片率下降 30%。

自定义算子接入 StableHLO

阶段:算子扩展

解决的问题:为 StableHLO 新增自定义算子(如 LayerNorm)时,手写所有输入组合的类型表不现实,且容易漏掉边缘情况。

具体案例:算子库工程师参照 add 实现自己的 inferReturnTypes,8 行代码完成类型推导注册,新算子立即享受编译期校验,任何形状错误都在模型运行之前暴露。

Python 演示

python88 行
from itertools import zip_longest  class TensorType:    """模拟 StableHLO 的张量类型:形状 + 元素类型。"""     def __init__(self, shape, elem_type):        self.shape = list(shape)   # 动态维度用 None 表示        self.elem_type = elem_type     def __repr__(self):        dims = []        for d in self.shape:            dims.append(str(d) if d is not None else "?")        return "tensor<{}x{}>".format("x".join(dims), self.elem_type)  def _element_must_match(types, op_name):    first = types[0].elem_type    for t in types[1:]:        if t.elem_type != first:            raise ValueError(                "{} 要求所有输入元素类型一致, 但收到 {} 与 {}".format(                    op_name, first, t.elem_type))  def infer_add(a, b):    """stablehlo.add: 逐元素加法, 输出形状 = 广播(a.shape, b.shape)."""    _element_must_match([a, b], "stablehlo.add")    s1, s2 = list(reversed(a.shape)), list(reversed(b.shape))    out = []    for d1, d2 in zip_longest(s1, s2, fillvalue=1):        if d1 is None or d2 is None:            out.append(None)                 # 动态维度保持动态        elif d1 == 1:            out.append(d2)        elif d2 == 1:            out.append(d1)        elif d1 == d2:            out.append(d1)        else:            raise ValueError("广播失败: 维度 {} 与 {} 不兼容".format(d1, d2))    return TensorType(list(reversed(out)), a.elem_type)  def infer_dot(a, b):    """stablehlo.dot: 批量矩阵乘 [...]MxK x [...]KxN -> [...]MxN."""    _element_must_match([a, b], "stablehlo.dot")    m, k1 = a.shape[-2], a.shape[-1]    k2, n = b.shape[-2], b.shape[-1]    if k1 != k2:        raise ValueError("矩阵乘契约维度不匹配: {} vs {}".format(k1, k2))    batch = infer_add(TensorType(a.shape[:-2], a.elem_type),                      TensorType(b.shape[:-2], b.elem_type)).shape    return TensorType(batch + [m, n], a.elem_type)  def infer_reduce(t, dims):    """stablehlo.reduce: 删除归约维后的形状."""    kept = [d for i, d in enumerate(t.shape) if i not in dims]    return TensorType(kept, t.elem_type)  def demo_failure():    a = TensorType([2, 3], "f32")    b = TensorType([3, 1], "f32")    try:        infer_add(a, b)    except ValueError as ex:        print("推导失败:", ex)  def build_demo_graph():    """构造一条 mini StableHLO 数据流图: add -> dot -> reduce."""    x = TensorType([2, 3], "f32")    y = TensorType([3], "f32")    w = TensorType([3, 4], "f32")    add_out = infer_add(x, y)    dot_out = infer_dot(add_out, w)    red_out = infer_reduce(dot_out, [1])    print("add   :", add_out)    print("dot   :", dot_out)    print("reduce:", red_out)    demo_failure()  if __name__ == "__main__":    build_demo_graph()
目的:用约百行 Python 模拟 StableHLO 三大典型推导规则(逐元素广播、矩阵乘、归约),并演示广播失败时的诊断行为。
  1. class TensorType用 shape 列表和 elem_type 两个字段模拟张量的「身份证」,动态维度用 None 表示。
  2. infer_add(a, b)实现逐元素算子的类型推导:先校验元素类型一致,再把两个形状右对齐逐维做广播。
  3. zip_longest(s1, s2, fillvalue=1)右对齐的关键:短形状缺失的前导维度补 1,对应广播规则中「视为 1」。
  4. out.append(None)任何一侧是动态维度时,输出该维保持动态,不强行填死。
  5. raise ValueError(...)维度既不相等又非 1 时抛出推导失败,模拟 MLIR 的 emitError。
  6. infer_dot(a, b)矩阵乘推导:校验内维 K 对齐,输出末两维取 M 与 N,批量维复用广播推理。
  7. infer_reduce(t, dims)归约推导:按编号删除指定维度,剩余维度顺序不变。
  8. demo_failure()演示 2x3 与 3x1 的广播冲突,说明「非 1 且不等」的维度是推导失败的重灾区。

常见误区

误区我以为类型推导要等模型真正跑起来才能确定形状,动态维度的张量必须实际执行一遍才知道长什么样。

正解类型推导发生在编译期,是纯静态的符号分析。遇到动态维度时,推导器不会去执行模型,而是把未知维度标记为 ? 或符号变量,表示「此处运行时才确定」。推导产出的不是具体数值,而是类型约束;真正固化为静态形状要靠后续的 shape specialization pass。

为何常见:日常接触 PyTorch 时形状经常运行时才报错,容易让人误以为编译器也拿动态形状没办法,于是把推导与执行混为一谈。

误区我一直以为写 StableHLO 算子时,输出类型必须逐个声明,不声明就会编译失败,推导接口只是可选的加分项。

正解恰恰相反,MLIR 的 InferTypeOpInterface / InferShapedTypeOpInterface 允许算子省略输出类型,由推导接口自动补齐。这是 StableHLO 可扩展性的关键:新算子只要实现接口,下游所有依赖类型信息的 pass 都能自动工作。没有实现接口的算子才需要显式标注。

为何常见:写普通函数时返回类型必须声明,这个直觉被带到了编译器开发里,忽略了「接口推导」这一层抽象。

误区我觉得两个张量形状不同没关系,编译器会像变魔术一样自动把短维度补长,2x3 和 3x1 肯定能广播成 2x3。

正解广播的规则精确且严格:维度从右向左对齐,只有「某维为 1」或「两维相等」两种情况允许通过。2x3 与 3x1 从右对齐时,第 0 维是 2 与 3——既不相等又非 1,直接失败。真正能广播的是 2x3 与 3(缺失维度视为 1)。

为何常见:很多框架的 Reduce 或 tile 操作会默默补维,让人误以为广播是「自动补任何维度」,忽略了「非 1 且不等则报错」的硬约束。

误区编译器推导出 `tensor<2x3xf32>` 后,我就以为运行时这个张量一定是 2x3,不会再变了。

正解推导只保证类型与算子语义一致,是「契约层面」的正确性。当输入含动态维度且算子支持动态输出时,推导器返回 ?;即使某个 pass 把它固化为静态值,也只是在特定假设(如 batch=8)下的 specialization,换一个输入配置形状可能完全不同。

为何常见:看到 IR 里出现静态数字就以为「铁板钉钉」,忽略了这些数字可能来自某个 pass 的临时假设,而非张量的本质属性。

面试题

Q1. 张量类型 tensor<2x3xf32> 中,2x3 表示什么?

单选题 · easy

  • A.元素存储字节数
  • B.形状和元素类型← 正确答案
  • C.张量的数据总长
  • D.张量的维度个数

答案:B。张量类型由形状与元素类型两要素组成,2x3 是形状(两维分别长 2、3),f32 是元素类型,二者合起来是张量的「身份证」。A 说的是字节数,C 是元素总数,D 只描述了维度个数,都不完整。

Q2. stablehlo.add 的两个输入都是 tensor<2x3xf32>,输出类型是?

单选题 · easy

  • A.tensor<2x3xf32>← 正确答案
  • B.tensor<3x2xf32>
  • C.tensor<2x2xf32>
  • D.tensor<3x3xf32>

答案:A。逐元素算子遵循「同型共形」规则:输入形状与元素类型都相同,输出沿用输入规格,像两瓶同体积同浓度的溶液混合后规格不变。形状既不交换也不缩减,因此 A 正确。

Q3. tensor<2x3xf32> 与 tensor<3xf32> 做广播加法,输出形状是?

单选题 · mid

  • A.tensor<2x3xf32>← 正确答案
  • B.tensor<3x3xf32>
  • C.tensor<2x2xf32>
  • D.tensor<3x2xf32>

答案:A。广播维度从右向左对齐:3 与 3 相等保留,2 与缺失维度(视为 1)中 1 让位,得到 2x3。像一排 3 格量杯复制成两排,每格一一对应相加。

Q4. 矩阵乘 [...]MxK 乘 [...]KxN 的输出末两维是?

单选题 · mid

  • A.批量维加M和N← 正确答案
  • B.批量维加N和M
  • C.批量维加M和K
  • D.批量维加K和N

答案:A。矩阵乘的契约是 A 的最后一维 K 必须等于 B 的倒数第二维 K,输出末两维取 A 的 M 与 B 的 N,批量维按广播对齐。K 是被消掉的「接驳口径」,不会出现在输出中。

Q5. 类型推导失败时,MLIR 编译器通常会怎么做?

单选题 · easy

  • A.报错并停止编译← 正确答案
  • B.猜测一个默认类型
  • C.忽略错误继续编译
  • D.推迟到运行时检查

答案:A。推导失败时接口通过 emitError 产生诊断消息,编译随即停止,像化学家发现配方冲突立刻停手,绝不随便拿一瓶溶液冒充产物。猜测、忽略、推迟都与静态分析的原则相悖。

Q6. InferShapedTypeOpInterface 的 inferReturnTypeComponents 返回什么?

单选题 · mid

  • A.形状组件列表← 正确答案
  • B.完整张量类型
  • C.元素类型列表
  • D.属性查询结果

答案:A。该接口返回 ShapedTypeComponents(形状组件)列表,允许某些维度未知或仅保留约束,比直接返回完整类型更灵活,特别适合动态维度场景。其他选项描述的是不同层面的信息。

Q7. stablehlo.reduce 输入 tensor<4x6x8xf32>,dimensions = [1],输出形状是?

单选题 · hard

  • A.tensor<4x8xf32>← 正确答案
  • B.tensor<6x8xf32>
  • C.tensor<4x6xf32>
  • D.tensor<6x4xf32>

答案:A。归约推导按维度编号删去指定维:[1] 表示删除第 1 维(长度 6),剩余维度顺序不变,得到 4x8。像把中间一排数据汇总掉,两端尺寸原样保留。

Q8. 以下哪些维度对可以通过广播规则?

多选题 · mid

  • A.3 与 1← 正确答案
  • B.2 与 3
  • C.1 与 4← 正确答案
  • D.3 与 3← 正确答案

答案:ACD。广播只认两种情形:某维为 1(让位给对方)或两维相等(原样保留)。3 与 1、1 与 4、3 与 3 都满足;2 与 3 既不相等又非 1,触发推导失败。

Q9. StableHLO 类型推导的工程价值包括哪些?

多选题 · hard

  • A.运行前发现形状错误← 正确答案
  • B.为融合优化提供形状← 正确答案
  • C.替代训练流程的损失计算
  • D.为显存池规划提供依据← 正确答案

答案:ABD。推导的价值集中在编译期:静态校验格式错误(禁配表)、为算子融合提供形状前提(工序规划)、为显存分配提供字节数(订烧杯)。损失计算是算法层的职责,与类型推导无关。

Q10. 以下哪些属于类型推导失败的常见场景?

多选题 · hard

  • A.操作数元素类型不匹配← 正确答案
  • B.广播维度无法对齐← 正确答案
  • C.循环体携带值形状漂移← 正确答案
  • D.输出与输入类型完全相同

答案:ABC。元素类型不一致、广播冲突、while 循环携带值类型不稳定都是典型推导失败场景。输出与输入类型完全相同反而是许多算子的正常行为(如逐元素 add),不构成失败。