设想一家大型连锁餐饮集团要推出一道新菜:主厨在总店厨房里现场演示,品控员拿着速记本盯住每个动作,把“切丁、焯水、爆炒”记成一张不依赖具体食材的菜谱;这张菜谱随后被翻译成所有分店都认识的标准工序单;每家分店再按照自家炉灶、烤箱甚至流水线设备的特点,把工序单拆成可执行的小操作,最后同时出餐。JAX 后端 MLIR 做的正是这件事:主厨是 Python 函数,品控员是 JAX 的追踪器,菜谱是 JAXPR,标准工序单是 StableHLO,分店厨房是 GPU 和 TPU,而 MLIR 就是一套用来描述“从大菜谱到小工序”的通用语言系统。

岗位关联

理解 JAX 后端 MLIR,你将从“框架调用者”变成“编译器流水线设计者”。这是做大模型训练优化、AI 基础设施和算子开发的硬核内功,也是理解现代深度学习框架底层如何炼成的一把钥匙。

目录
  1. 可视化演示
  2. 1. 追踪机制:把掌勺动作写成菜谱
  3. 2. HLO方言:通用厨房的标准化菜单
  4. 3. 类型推导:给每份食材标注规格
  5. 4. 下降流程:从大菜拆成小工序
  6. 5. 代码生成:主厨排班与火候优化
  7. 6. 硬件加速:专用炉灶与并行炒锅
  8. 7. 大规模训练:千人宴的流水线
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示JAX MLIR后端三维度:编译流水线(Python函数经tracing追踪生成jaxpr IR再映射StableHLO经MHLO LMHLO降级到LLVM IR最终生成设备代码共8阶段)、方言层级(StableHLO可移植高层运算方言定义Add/Dot/Convolution等标准算子逐层Lowering StableHLO→MHLO→LMHLO→LLVM抽象级别递降每层增加硬件细节)、多设备部署(统一StableHLO IR经不同后端Lowering部署CPU/GPU/TPU GPU走CUDA后端TPU专用XLA编译器CPU走LLVM后端PjRt运行时协同),三模式切换理解JAX MLIR编译机制。

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

'+C1+'

1. 追踪机制:把掌勺动作写成菜谱

总店主厨研发新菜时,JAX 的追踪器就像一个拿着速记本的品控员。你调用 jax.jit(f) 后,Python 函数并不会立刻算出数值,而是由 tracer 对象代替真实输入进入函数。品控员看到的不是数字,而是“加法”“乘法”“reshape”这些动作。于是,一次 Python 执行被冻结成一张可复用的菜谱——JAXPR。

这张菜谱不依赖 Python 解释器,也不关心数据具体是什么,只关心操作之间的依赖关系。它把 x + y 记录成 add(x, y),把矩阵乘法记录成 dot_general。这种“先建图、后执行”的方式,是 JAX 能对接编译器的基石。它让 JAX 拥有自动微分和并行化改造的能力,因为编译器终于可以提前看清楚整道菜的所有步骤。

JAXPR 是 JAX 的内部表示,它还不是 MLIR 方言。它的作用是“原汁原味地保存主厨的意图”,至于怎么切菜、用什么锅,留给后面的标准化工序员去决定。从技术上讲,追踪过程可以看作把一个 Python 函数变成一张计算图:

回到餐厅:菜谱写好后,品控员不会亲自去炒菜,而是把它交给标准化工序员。这位工序员就是下一节的主角——StableHLO 方言。

J=Trace(f)(x)\displaystyle \mathcal{J} = \mathrm{Trace}(f)(\boldsymbol{x})
这个公式表示:用追踪器 Trace 把函数 f 在抽象输入 x 上跑一遍,得到 JAXPR 计算图 J。
  1. 追踪符号\mathrm{Trace} 表示 JAX 的追踪器,它会在函数执行时记录每个算子。
    Trace\mathrm{Trace}
  2. 目标函数f 表示被转换的 Python 函数,也就是主厨的掌勺动作。
    ff
  3. 输入占位\boldsymbol{x} 表示 tracer 输入,它不是真实数值,而是替代食材的占位牌。
    x\boldsymbol{x}
  4. 输出菜谱\mathcal{J} 表示生成的 JAXPR 计算图,它是一张与硬件无关的菜谱。
    J\mathcal{J}

2. HLO方言:通用厨房的标准化菜单

标准化工序员读完整本菜谱后,发现各分店对“糖色”“断生”的理解都不一样。于是他把菜谱翻译成一份所有厨房都公认的工序单:StableHLO。它是一组稳定、可移植的高层算子,比如 stablehlo.add、stablehlo.dot_general,只描述语义,不规定具体实现。

StableHLO 的全称是 Stable High-Level Operations。它从 XLA 内部 HLO 中抽取稳定子集,避免 HLO 频繁变动导致上层框架碎片化。在 MLIR 的世界里,这种“一门特定领域的语言”叫作 dialect。StableHLO 就是一个 dialect,它让 JAX 不用直接关心 GPU、TPU 各自的指令,而是统一表达为“加”“乘”“卷积”“归一化”等标准动作。

这种标准化对框架集成至关重要。传统后端需要为每个硬件各自写一套算子库,而有了 StableHLO,前端只需要把 JAXPR 映射到 StableHLO,后端再从 StableHLO 出发做优化。从数学上看,这一映射是一个方言翻译过程:

厨房有了统一工序单还不够,备菜间还得知道每份食材多大、什么规格。这就是类型推导要解决的问题。

S=Φstablehlo(J)\displaystyle \mathcal{S} = \Phi_{\mathrm{stablehlo}}(\mathcal{J})
这个公式表示:StableHLO 图中的每个节点,都是通过方言转换函数 Phi 从 JAXPR 节点映射来的。
  1. 映射函数\Phi_{\mathrm{stablehlo}} 表示从 JAXPR 到 StableHLO 的方言转换。
    Φstablehlo\Phi_{\mathrm{stablehlo}}
  2. 输入图\mathcal{J} 是上一节生成的 JAXPR 计算图。
    J\mathcal{J}
  3. 输出图\mathcal{S} 是 StableHLO 计算图,语义和 JAXPR 等价,但表示方式标准化了。
    S\mathcal{S}

3. 类型推导:给每份食材标注规格

标准工序单上写着“加入适量料酒”,但分店备菜时不知道“适量”是多少。类型推导要做的,就是给计算图中的每个张量标注明确的形状和数据类型。它像一位严格的备菜组长,逐一检查每份食材的尺寸、重量和单位,确保后面的厨师不会把“500克盐”当成“500克糖”。

在 JAX 后端流水线中,类型推导会在 StableHLO 层面检查张量的 shape 和 dtype。比如矩阵乘法要求两个输入维度匹配,卷积要求输入通道数与卷积核一致。如果发现 shape 不匹配,编译器会直接报错,而不是等到设备上运行到一半才崩溃。这种早期检查让问题暴露在编译期,而不是昂贵的训练期。

类型推导也为后续内存分配、算子选择和 LLVM 代码生成提供了依据。对于动态维度,比如 NLP 中不定长的句子,类型系统会用 ? 标记不确定维度,并生成对应的运行时检查逻辑。类型学中的经典表达是:

规格标注完毕,接下来工序员要把“红烧肉”这一道大工序,拆成“切肉、焯水、炒糖色、炖煮”等一系列小工序。这就是 Lowering 流程。

Γ⊢e:τ\displaystyle \Gamma \vdash e : \tau
这个公式表示:在类型环境 Gamma 下,表达式 e 的类型可以被推导为 tau。
  1. 类型环境\Gamma 表示一组已知的类型约束,相当于备菜组长的规格表。
    Γ\Gamma
  2. 推导符\vdash 读作“推导出”,表示左边环境能推出右边结论。
    ⊢\vdash
  3. 表达式e 表示计算图中的某个算子或张量。
    ee
  4. 结果类型\tau 表示推导出来的 shape 和 dtype,比如 f32[128, 256]。
    τ\tau

4. 下降流程:从大菜拆成小工序

StableHLO 还是“做一道菜”级别的高层描述,设备没法直接执行。MLIR 的设计哲学是逐级下降:先把 StableHLO 变成 Linalg 方言,再变成 Arith、MemRef 等底层方言,最后落到 LLVM IR。每一步都像工序员把一个大步骤拆成更细的小步骤,直到每个小步骤连普通帮厨都能照做。

方言转换过程中,最重要的事情是保持 legality。所谓合法,是指转换前后计算语义完全一致。MLIR 提供了 Dialect Conversion 框架,用 pattern 描述“大算子如何拆成小算子”。JAX 后端通过注册大量转换规则,把 StableHLO 的每个算子逐步映射到更低层抽象。

这种多级下降的价值在于复用。GPU 和 TPU 后端不需要各自从 StableHLO 直接生成机器码,而是共享同一套中间转换规则,只在最后一步选择不同的硬件代码生成。整个下降流程可以看作一串转换函数的复合:

工序拆完后,主厨开始排班:哪个灶先热、哪道菜可以合并到同一个锅里。这就是 XLA 代码生成阶段。

L=ℓk∘ℓk−1∘⋯∘ℓ1\displaystyle \mathcal{L} = \ell_k \circ \ell_{k-1} \circ \cdots \circ \ell_1
这个公式表示:从高层 IR 到低层 IR,要依次经过第 1 步转换到第 k 步转换,所有步骤串联起来就是 Lowering。
  1. 转换函数\ell_1 到 \ell_k 表示每一级 Lowering 的方言转换规则。
    ℓ1,ℓ2,…,ℓk\ell_1, \ell_2, \ldots, \ell_k
  2. 复合运算\circ 表示函数复合,先做 l1,再做 l2,依此类推。
    ∘\circ
  3. 整体流水线\mathcal{L} 表示完整的 Lowering 流水线。
    L\mathcal{L}

5. 代码生成:主厨排班与火候优化

当 IR 下降到足够底层,XLA 就像总店的主厨,开始规划整个厨房的排班表:哪些算子可以合并到一个 kernel 里执行,哪些中间结果应该留在寄存器或高速缓存中,哪些计算应该并行。最经典的优化叫算子融合,它能把“A 加 B”和“A 加 B 后乘 C”合并进同一个 kernel,减少一次读写显存的开销,就像把“焯水”和“爆炒”合并到同一个锅里,省去了来回端锅的等待。

调度优化会为每个算子选择最优的执行顺序和并行策略。XLA 会构建一个 cost model,评估不同调度方案的执行时间,然后选择消耗最小的一种。对训练来说,这直接决定了 step 时间;对推理来说,这决定了延迟和吞吐量。

代码生成的本质是在巨大的搜索空间中寻找最优调度。可以写成最小化问题:

排班表做好后,菜终于要下锅了。不同的分店厨房设备差异很大,这就要看 GPU 和 TPU 如何接招。

S∗=arg⁡min⁡S∈SexecT(G,S)\displaystyle S^{*} = \arg\min_{S \in \mathcal{S}_{\mathrm{exec}}} T(G, S)
这个公式表示:在所有合法调度 S 中,选出使计算图 G 执行时间 T 最短的那个最优调度 S*。
  1. 执行时间函数T(G, S) 表示在调度方案 S 下执行计算图 G 的耗时。
    T(G,S)T(G, S)
  2. 合法调度集合\mathcal{S}_{\mathrm{exec}} 表示所有满足硬件约束的调度方案集合。
    Sexec\mathcal{S}_{\mathrm{exec}}
  3. 最优调度S^{*} 表示最终选择的执行计划。
    S∗S^{*}
  4. 最小化操作\arg\min 表示“找到使目标函数最小的那个候选值”。
    arg⁡min⁡\arg\min

6. 硬件加速:专用炉灶与并行炒锅

GPU 像一家有几千个小炒锅的大排档,每个锅都能独立翻炒,适合“同一道菜同时做很多份”。TPU 则更像一条专门做固定菜品的流水线烤盘,用脉动阵列把矩阵乘法按固定节奏推过去,单位功耗下吞吐更高。JAX 后端通过 XLA,把同一份 StableHLO 翻译成不同设备的机器码:GPU 拿到 CUDA 内核,TPU 拿到定制的 HLO 优化指令。

硬件加速不只是换一个执行引擎,还包括内存布局优化。GPU 需要把数据排布成适合 coalesced access 的格式,TPU 则需要考虑片上存储和主存之间的搬运。MLIR 的作用在这里体现为 expose 显式的 layout 信息,让每一级转换都能针对硬件特性做调整。

并行带来的加速并不是免费的。多个计算单元协同工作时,会引入通信和同步开销。理想加速比可以用一个简单的模型表示:

一道菜可以同时做多份,但如果是千人宴,单靠多个厨师炒同一道菜还不够,还得把整道菜拆成不同工作站。这就来到大规模训练。

Sp=TserialTparallel=TserialTserial/n+Tovh\displaystyle S_p = \frac{T_{\mathrm{serial}}}{T_{\mathrm{parallel}}} = \frac{T_{\mathrm{serial}}}{T_{\mathrm{serial}} / n + T_{\mathrm{ovh}}}
这个公式表示:用 n 个设备并行执行,并不是快 n 倍,还要扣除设备间通信和同步开销 T_ovh。
  1. 串行时间T_{\mathrm{serial}} 表示单设备执行完整任务的时间。
    TserialT_{\mathrm{serial}}
  2. 并行时间T_{\mathrm{parallel}} 表示多设备执行的总时间,包含计算时间和通信时间。
    TparallelT_{\mathrm{parallel}}
  3. 设备数量n 表示并行设备个数。
    nn
  4. 额外开销T_{\mathrm{ovh}} 表示通信、同步和内存搬运等额外开销。
    TovhT_{\mathrm{ovh}}

7. 大规模训练:千人宴的流水线

千人宴不能让一个厨师从头做到尾,更不能让每桌都单独开火。大规模训练用的是同样的思路:数据并行让每个 GPU 各拿一份 batch 同时前向和反向,模型并行把一个大模型切成几段放不同设备上,异步流水让设备不必每步都停下来等全局同步。

数据并行最直观:每个设备算出自己的梯度,然后把所有梯度汇总,更新一份共享参数。模型并行则用于大模型无法塞进单卡显存的情况,比如把 Transformer 的 Layer 切到不同设备上。MLIR 和 XLA 会插入集合通信算子,比如 all-reduce,来完成梯度汇总。

在大规模训练中,通信常常成为瓶颈。梯度压缩、梯度累积和异步流水都是为了减少等待。最终所有局部梯度需要汇总成全局梯度,用公式表示:

从追踪到菜谱,从标准工序单到逐级拆解,再到设备上的并行炒菜,JAX 后端 MLIR 就像一条完整的餐饮流水线:把灵活性留在前端,把性能释放到后端。

g=∑i=1Pgi\displaystyle \boldsymbol{g} = \sum_{i=1}^{P} \boldsymbol{g}_i
这个公式表示:全局梯度 g 等于所有设备局部梯度 g_i 的和。
  1. 局部梯度\boldsymbol{g}_i 表示第 i 个设备算出的梯度。
    gi\boldsymbol{g}_i
  2. 设备数量P 表示参与训练的并行设备总数。
    PP
  3. 求和符号\sum_{i=1}^{P} 表示把所有设备的梯度逐项累加。
    ∑i=1P\sum_{i=1}^{P}
  4. 全局梯度\boldsymbol{g} 表示聚合后用于更新模型参数的最终梯度。
    g\boldsymbol{g}

AI 应用场景

场景1:大模型云端训练

阶段:编译器代码生成与硬件加速

解决的问题:传统手写 CUDA 内核几乎不可能覆盖大模型中的海量算子组合。手写内核不仅开发周期长,而且难以在不同 GPU 型号间迁移。JAX 后端通过 MLIR 把模型统一编译成 GPU/TPU 设备代码,自动完成算子融合和内存优化。

具体案例:一家 AI 公司要在 TPU 集群上训练千亿参数语言模型。工程师用 JAX 定义模型,jax.jit 自动追踪生成 JAXPR,再经 StableHLO 和 MLIR 下降为 TPU 可执行代码。模型并行和数据并行由 XLA 编译器统一调度,省去大量手写分布式代码。

场景2:同一模型多硬件部署

阶段:中间表示与后端移植

解决的问题:传统框架为 CPU、GPU、NPU 分别维护算子库,导致新模型接入每种硬件都要重复适配。StableHLO 作为设备无关的中间表示,让模型只写一次,就能通过不同后端编译到多种硬件。

具体案例:一家自动驾驶公司需要把视觉模型同时部署到车端英伟达 GPU 和自研 NPU。他们选择把模型导出为 StableHLO,再由各硬件团队基于 MLIR 写后端转换。前端模型变更时,后端无需改动。

场景3:推理延迟优化

阶段:XLA 代码生成与调度优化

解决的问题:在线推理服务对延迟极其敏感,未融合的算子会频繁读写显存,成为主要瓶颈。XLA 的算子融合把多个算子合成一个 kernel,减少内核启动和访存次数。

具体案例:一个语音识别服务原先用 TensorRT 手写融合,后来改用 JAX 导出 StableHLO,再利用 XLA 自动融合和调度,P99 延迟降低了约 30%,同时减少了显存占用。

Python 演示

python89 行
# mini_jax_pipeline.py# A tiny demo of JAX backend pipeline: trace -> types -> HLO -> codegen. class Node:    def __init__(self, op, args, dtype='f32'):        self.op = op        self.args = args        self.dtype = dtype  class MiniJAX:    def __init__(self):        self.tape = []        self.counter = 0     def new_tracer(self, name, val):        node = Node('param', [name], 'f32')        self.tape.append(node)        return node     def add(self, a, b):        node = Node('add', [a, b])        self.tape.append(node)        return node     def mul(self, a, b):        node = Node('mul', [a, b])        self.tape.append(node)        return node     def trace(self, fn, x):        self.tape = []        x_t = self.new_tracer('x', x)        y = fn(x_t)        return y, list(self.tape)     def infer_types(self):        for node in self.tape:            if node.op in ('add', 'mul'):                node.dtype = node.args[0].dtype        return self.tape     def lower_to_hlo(self):        ops = []        for node in self.tape:            op_map = {'add': 'stablehlo.add', 'mul': 'stablehlo.dot'}            name = op_map.get(node.op, node.op)            arg_names = [a.op for a in node.args]            ops.append('%s(%s)' % (name, ', '.join(arg_names)))        return ops     def codegen(self):        lines = []        for node in self.tape:            if node.op == 'add':                a = id(node.args[0])                b = id(node.args[1])                lines.append('x.%d = x.%d + x.%d' % (id(node), a, b))            elif node.op == 'mul':                a = id(node.args[0])                b = id(node.args[1])                lines.append('x.%d = x.%d * x.%d' % (id(node), a, b))            else:                lines.append('x.%d = input %s' % (id(node), node.args[0]))        return lines  if __name__ == '__main__':    jax = MiniJAX()     def model(x):        h = jax.mul(x, x)        y = jax.add(h, x)        return y     y, tape = jax.trace(model, 3.0)     print('JAXPR:')    for n in tape:        print(n.op, ':', [a.op for a in n.args])     jax.infer_types()    print('HLO:')    for line in jax.lower_to_hlo():        print(line)     print('Codegen:')    for line in jax.codegen():        print(line)
目的:用极简 Python 类模拟 JAX 从追踪到代码生成的流水线,帮助理解 JAXPR 如何变成 StableHLO,再变成设备代码的核心思想。
  1. class MiniJAX:定义迷你框架类,相当于一个简易 JAX 后端。
  2. self.tape = []磁带 tape 记录所有被追踪的算子,相当于 JAXPR 菜谱。
  3. def trace(self, fn, x):追踪函数把 Python 函数放进记录环境执行。
  4. x_t = self.new_tracer('x', x)用 tracer 占位符代替真实输入。
  5. h = jax.mul(x, x)在追踪环境里调用乘法,会被记录为 mul 算子。
  6. y = jax.add(h, x)加法同样被记录到 tape 上。
  7. jax.infer_types()模拟类型推导,为每个中间张量标记 dtype。
  8. jax.lower_to_hlo()把内部算子映射成 StableHLO 风格的名字。
  9. ops.append('%s(%s)' % ...)生成字符串形式的 HLO 表示,方便后续 Lowering。
  10. jax.codegen()模拟最终代码生成,把计算图变成逐行赋值代码。

常见误区

误区我以为 `jax.jit` 就是把函数包了一层缓存,第二次调用直接返回上一次的结果,和 `functools.lru_cache` 差不多。

正解不是缓存结果,而是追踪计算过程生成 JAXPR 并编译成设备代码。菜谱不是剩菜,而是“做法记录”。相同输入形状下编译产物会被复用,但核心价值在于生成计算图,而不是缓存数值。

为何常见:因为 jit 的名字里带“即时编译”,人们容易把它类比成运行时缓存工具。

误区我以为 StableHLO 像 Java 字节码一样,由设备解释执行,是一种运行时文件格式。

正解StableHLO 是编译期中间表示,不是运行时指令集。它像标准化工序单,最终会被 MLIR 和 XLA 编译成 GPU/TPU 的机器码。Python 用户接触到的都是编译前产物,所以看不到它直接执行。

为何常见:因为 JIT、字节码、中间表示这些词都带“中间”,很容易混淆编译期和运行期。

误区我以为 MLIR Lowering 的最终结果是一段 C++ 代码,然后交给普通编译器编译。

正解Lowering 是多级方言转换,路线通常是 StableHLO -> Linalg -> Arith -> LLVM IR -> 机器码,并不会生成 C++ 源码。MLIR 和 LLVM 共用基础设施,但 MLIR 是更高层的多级 IR 框架。

为何常见:看到 LLVM 就想到 C++ 工具链,忽略了 MLIR 是面向编译器的“模型描述语言”。

误区Python 里写代码从不标注类型,JAX 也能运行,所以类型推导只是锦上添花。

正解类型推导决定张量形状和 dtype,是编译器分配内存、选择内核、执行布局转换和检测动态维度的基础。菜谱上不标食材分量,分店根本没法备菜。

为何常见:因为运行时 JAX 会自动处理 shape 检查,用户感知不到类型推导的存在,误以为没有它也能工作。

误区只要把训练任务塞满所有 GPU/TPU,速度一定线性提升,甚至不用管通信。

正解通信开销、同步屏障和内存瓶颈会让加速比远低于设备数。数据并行需要 all-reduce 汇总梯度,模型并行需要频繁传输中间激活。工程上要用梯度压缩、异步流水等方式降低等待。

为何常见:直观上“人多力量大”,却忽略了协作成本,也就是公式里的 T_ovh。

面试题

Q1. JAX 中 jax.jit 对 Python 函数做的第一件事是什么?

单选题 · easy

  • A.直接编译成机器码
  • B.追踪执行并生成计算图← 正确答案
  • C.缓存函数的返回值
  • D.调用 C++ 运行时

答案:B。jax.jit 先用 tracer 代替真实输入执行 Python 函数,记录算子依赖并生成 JAXPR。它不像 lru_cache 那样缓存结果,而是先建图后编译。机器码要等后续 XLA 阶段才生成。

Q2. JAX 追踪后得到的中间表示通常称为?

单选题 · easy

  • A.JAXPR 计算图← 正确答案
  • B.LLVM IR 指令
  • C.ONNX 模型文件
  • D.Python 字节码

答案:A。JAX 的追踪器输出 JAXPR,它是 JAX 内部的计算图。LLVM IR 在更底层,ONNX 是模型交换格式,Python 字节码则是解释器产物。

Q3. StableHLO 在 JAX 后端流水线中的角色是?

单选题 · mid

  • A.统一的设备无关 IR← 正确答案
  • B.设备相关的汇编器
  • C.Python 的语法解析器
  • D.分布式通信协议

答案:A。StableHLO 是稳定、可移植的高层算子集,属于设备无关 IR。它让 JAX 前端和 GPU/TPU 后端解耦。汇编器、语法解析器、通信协议都不是 StableHLO 的职责。

Q4. 类型推导公式 Γ ⊢ e : τ 中的 Γ 表示?

单选题 · mid

  • A.编译期类型环境← 正确答案
  • B.设备内存地址
  • C.算子执行日志
  • D.训练超参数表

答案:A。Γ 是类型上下文,记录已知变量的类型约束。它像备菜组长的规格表,是推导张量 shape 和 dtype 的依据,与运行时日志或超参数无关。

Q5. MLIR Lowering 过程中“legality”指的是?

单选题 · mid

  • A.转换前后语义保持一致← 正确答案
  • B.转换后代码符合 PEP8
  • C.转换结果通过安全审查
  • D.转换过程不需要验证

答案:A。legality 是方言转换的核心约束,表示每一步下降后计算语义不变。它保证高层 StableHLO 拆成底层算子后,仍等于原来的数学含义。PEP8、安全审查都不属于编译器 legality。

Q6. 算子融合最主要的性能收益来自?

单选题 · hard

  • A.减少内核启动和访存次数← 正确答案
  • B.提高 Python 解释速度
  • C.增大训练 batch 大小
  • D.降低模型参数量

答案:A。算子融合把多个计算合并到一个 kernel,减少中间结果写回显存和重新读取的开销,也减少内核启动次数。它不会显著改变 Python 速度、batch 大小或参数总量。

Q7. 数据并行与模型并行的核心区别是?

单选题 · hard

  • A.前者切数据后者切模型← 正确答案
  • B.前者切模型后者切数据
  • C.两者在原理上完全等价
  • D.两者在实际执行中相同

答案:A。数据并行是每台设备拿不同 batch,算出梯度后汇总;模型并行是把模型不同层或参数切到不同设备上。二者解决不同瓶颈,常组合使用,并不等价。

Q8. 以下哪些是 JAX 后端 MLIR 流水线环节?

多选题 · mid

  • A.追踪生成 JAXPR← 正确答案
  • B.映射到 StableHLO← 正确答案
  • C.手工执行梯度下降
  • D.XLA 生成设备代码← 正确答案

答案:ABD。流水线包含 JAX 追踪生成 JAXPR、方言映射到 StableHLO、MLIR 多级下降,以及 XLA 最终生成设备代码。梯度下降是训练算法,不是编译流水线环节。

Q9. TPU 上常用的硬件加速手段包括?

多选题 · extreme

  • A.脉动阵列矩阵乘法← 正确答案
  • B.高带宽片上存储← 正确答案
  • C.删除所有类型推导
  • D.调度与算子融合优化← 正确答案

答案:ABD。TPU 通过脉动阵列高效执行矩阵乘法,并利用高带宽片上存储减少搬运开销;XLA 还会做调度和算子融合。类型推导是编译基础,不能删除,否则无法生成正确代码。

Q10. 大规模训练中用于缓解通信瓶颈的手段有?

多选题 · mid

  • A.梯度压缩降低通信量← 正确答案
  • B.异步流水隐藏等待时间← 正确答案
  • C.禁止任何模型并行
  • D.每步强制全局同步

答案:AB。梯度压缩减少 all-reduce 的数据量,异步流水让通信和计算重叠,从而隐藏等待。禁止模型并行和强制全局同步只会让扩展更困难,不是缓解手段。