设想你是一家五星级餐厅的主厨,要为三百位客人筹备一场晚宴。厨房的操作台只有三米长,每道菜的食材、酱汁、半成品出锅后都要在台面上周转。菜一多,台面立刻告急:要么没地方放刚出锅的菜,要么几道菜的半成品绞在一起。更头疼的是,你根本说不清“最挤”发生在哪一刻。于是你决定在正式开宴前,把整本菜谱完整演练一遍:先画出一张“台面占用时间线”,标出每道菜从备料到收尾占多大地方、占多久,找到全场最挤的那一分钟;再拿起秒表,给切菜、焯水、颠勺、摆盘每一步计时,看哪一步最拖节奏。这场演练,就是 JAX 框架里“内存分析与 profiling”的缩影:JIT 编译器先把程序翻译成一张精确到每个张量生死的操作蓝图(XLA 中间表示),内存分析工具告诉你显存何时见顶、是哪几个张量同时驻留;profiling 工具则告诉你每个算子耗时多少、GPU 有多空闲。掌握了这套“台面管理 + 秒表计时”的方法,你就拿到了大规模训练调优的第一把钥匙。

岗位关联

职业价值:学会 JAX 的内存分析与 profiling,等于拥有定位线上 OOM、把训练吞吐翻倍的“第一反应”工具。面试官问你“峰值显存怎么找、瓶颈算子怎么看”,答案都藏在这套方法论里。

目录
  1. 可视化演示
  2. 内存分析工具:台面占用时间线
  3. 内存瓶颈识别:台面告急的四招
  4. 性能画像采集:给每一步备菜掐秒表
  5. 算子级分析:揪出最慢的炖牛骨
  6. XLA融合策略:三步合成一个动作
  7. 分片配置优化:多个厨房分头备菜
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

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

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

内存分析工具:台面占用时间线

主厨的第一件事,不是急着开火,而是把整场宴会的“台面剧本”写出来。哪道菜的半成品几点上桌、占多宽、什么时候撤走,全部记成一张时间表。只有这样,他才能回答全场最该先回答的问题:台面最挤的那一分钟,到底摆了什么?

JAX 程序也一样。当你给函数加上 jit 装饰器,Python 代码并不会被直接执行,而是先被翻译成一棵算子计算图,再降级成 XLA 的 HLO(High-Level Optimizer)中间表示。HLO 就是那张“台面剧本”,里面清清楚楚地写着每个张量(tensor)何时产生、何时被消费、存活多久、占多少字节。

基于这份 HLO,内存分析工具可以精确算出“任一时刻有多少字节同时驻留”。实践中常用两条路:一是用 jax.profiler.start_trace 抓一段 trace,在 TensorBoard 的 Memory 视图里按时间轴观察各张量的占用条;二是用 XLA_FLAGS=--xla_dump_hlo=... 把 HLO 文本导出来,再借助 XLA 的 memory 分析 pass(如 --xla_hlo_memory_analysis)批量计算 buffer 的分配与释放时刻,从而定位峰值点。峰值显存的严格定义是:

有了这张“台面时间线”,主厨终于能在复盘会上拍着桌子说:“晚上七点十二分台面失控——炖牛肉、酱汁和摆盘半成品同时占着台面。”对应到训练里,这句话就变成了“第 12 层的激活张量与第 10 层的梯度同时驻留,显存见顶”。

Mpeak=max⁡t∈[0,T]∑i=1nsizei⋅I[si≤t≤ei]\displaystyle M_{\text{peak}} = \max_{t \in [0,T]} \sum_{i=1}^{n} \text{size}_i \cdot \mathbb{I}\left[ s_i \le t \le e_i \right]
通俗说明:峰值显存 = 在程序运行的每一刻,把“还活着”的张量大小加起来,再取这些总和里的最大值。就像厨房里每分钟清点一次台面上的东西,挑出最挤那一分钟的总量。
  1. 下标 i 与求和号遍历执行图中所有 n 个张量,把每个张量的大小累加起来。
    ∑i=1nsizei\displaystyle \sum_{i=1}^{n} \text{size}_i
  2. 指示函数方括号里的条件成立时为 1、不成立时为 0,用来“筛选”在时刻 t 仍然存活的张量。
    I[si≤t≤ei]\displaystyle \mathbb{I}\left[ s_i \le t \le e_i \right]
  3. max 取最大值让时间 t 从 0 走到 T,对每个时刻的求和结果取最大值,就是整段执行的峰值显存。
    Mpeak=max⁡t∈[0,T](∑i=1nsizei⋅I[si≤t≤ei])\displaystyle M_{\text{peak}} = \max_{t \in [0,T]} \left( \sum_{i=1}^{n} \text{size}_i \cdot \mathbb{I}[s_i \le t \le e_i] \right)

内存瓶颈识别:台面告急的四招

台面最挤的时刻找到了,可问题还没解决:台面就三米长,菜又必须一次做完。主厨开始想对策——能不能错峰出菜?能不能少留半成品?能不能换成小份容器?这几招,正是显存优化的几个大方向。

当峰值显存逼近甚至超过 GPU 容量上限时,训练就会报 OOM(out of memory)。这个“显存墙”是大模型训练的常客。内存分析的价值在于,它不只是告诉你“爆了”,而是告诉你“是哪几个张量同时驻留才爆的”,于是对策就有了靶心。

对策通常有四类。缓存与复用:让新张量复用已死亡张量的显存块,减少碎片与峰值;中间结果剪枝:让算子尽早消费、尽早释放,缩短张量生命周期;梯度检查点:不保存每一层的激活,每隔 K 层存一个检查点,反向传播时重算中间激活;降精度:把 fp32 换成 bf16/fp16,张量体积直接减半。梯度检查点的内存变化可以写成:

主厨最终选择了“把半成品分冰箱”:不再让每道菜都把半成品摆在台面上,而是每隔几道菜存一个“半成品冰箱”,要用时快速回锅。台面压力从“随菜品数量线性上涨”变成“只随平方根上涨”,换来的是少量重算的时间成本——这买卖,在厨房和显存里都划算。

Macts′=K⋅s+nK⋅s  →K=n  2n s\displaystyle M_{\text{acts}}' = K \cdot s + \frac{n}{K} \cdot s \;\xrightarrow{K=\sqrt{n}}\; 2\sqrt{n}\,s
通俗说明:原本每一层都要存一份激活(共 n 份),现在只存 K 份检查点,区间里没存的层在反向时重算,总占用从“跟着层数线性涨”变成“只随平方根涨”。
  1. K·s 检查点开销每隔 K 层存一个检查点,共 K/K 份约 K 份,每份大小 s。
    K⋅s\displaystyle K \cdot s
  2. n/K·s 重算开销相邻检查点之间的区间需要重算,任一时刻最多额外驻留一个区间的激活。
    nK⋅s\displaystyle \frac{n}{K} \cdot s
  3. 取 K=√n 最优两项一增一减,在 K=√n 时总和最小,得到 2√n·s。
    K=n  ⇒  Macts′=2n s\displaystyle K = \sqrt{n} \;\Rightarrow\; M_{\text{acts}}' = 2\sqrt{n}\,s

性能画像采集:给每一步备菜掐秒表

台面搞定了,主厨的第二步是掐秒表。他站在厨房中央,记录每一位厨师的动作:切菜多少秒、焯水多少秒、颠勺多少秒、装盘多少秒。几轮下来他发现,有些厨师忙得脚不沾地,有些厨师却站在那里等锅——厨房的“开工率”低得吓人。

JAX 的 profiling 就是这把秒表。jax.profiler.start_trace 与 stop_trace 会记录一次运行中所有算子的起止时间,导出的 trace 可以用 TensorBoard 的 Profiler 插件打开,看到一张按时间轴铺开的算子长条图,以及 GPU/TPU 的利用率曲线。

从 trace 里,我们能读出每个算子(matmul、conv、reshape……)的独立耗时,合起来就是“算子级耗时”报表。其中最核心的指标是设备利用率 U——设备真正做浮点运算的时间占总执行时间的比例,内存搬运、kernel 启动、跨设备通信都会把 U 拉低:

秒表数据让主厨大吃一惊:真正用来颠勺的时间只占四成,其余时间都耗在“等蒸锅”“等传菜”上。这不是手艺问题,是流程问题。训练同理——U 只有 40% 的脚本,瓶颈往往不在算力,而在数据搬运和等待,盲目加算力毫无意义。

U=TcomputeTtotal=TcomputeTcompute+Tmemory+Tcomm\displaystyle U = \frac{T_{\text{compute}}}{T_{\text{total}}} = \frac{T_{\text{compute}}}{T_{\text{compute}} + T_{\text{memory}} + T_{\text{comm}}}
通俗说明:设备利用率就像厨房的“开工率”:真正在切菜炒菜的时间 ÷ 总上班时间。等锅烧热、等食材运来的时间越多,开工率越低。
  1. 分子 T_compute设备真正执行浮点运算的时间,是“有效工作时间”。
    Tcompute\displaystyle T_{\text{compute}}
  2. 分母 T_total总执行时间,由计算、访存、通信三部分构成,任何等待都算在里面。
    Tcompute+Tmemory+Tcomm\displaystyle T_{\text{compute}} + T_{\text{memory}} + T_{\text{comm}}
  3. 比值 UU 越接近 1,说明设备越“满”,优化空间越小;U 低则应当先解决等待问题。
    U=TcomputeTcompute+Tmemory+Tcomm\displaystyle U = \frac{T_{\text{compute}}}{T_{\text{compute}} + T_{\text{memory}} + T_{\text{comm}}}

算子级分析:揪出最慢的炖牛骨

秒表记完,主厨把每个步骤的耗时做成条形图。五小时的备餐里,炖牛骨一个人占了两小时。他立刻明白:与其优化只占一分钟的摆盘,不如死磕炖牛骨——这就是“热点”的直觉。

profiling 采集到的算子耗时经过排序,耗时占比最高的那个算子叫热点(hotspot)。优化热点收益最大,这是 Amdahl 定律告诉我们的朴素真理:整体提速的极限,被“未被优化的部分”死死卡住。

假设热点算子耗时占比为 p,把它加速到原来的 s 倍,整体加速比 S 为:

“就算你把炖牛骨‘优化’到一秒出锅,五小时的总时间也不会低于三小时——剩下三小时是切菜、摆盘这些雷打不动的步骤。”这句话就是主厨对 Amdahl 定律的理解。对模型训练而言,把占 60% 耗时的 matmul 抠细节,远比优化占 5% 的变形算子划算。

S=1(1−p)+ps\displaystyle S = \frac{1}{(1 - p) + \dfrac{p}{s}}
通俗说明:即使把最耗时的算子加速到无限快(s 趋向无穷),整体加速也最多到 1/(1-p)。就像炖牛骨再快,5 小时里还有 3 小时的别的活儿雷打不动。
  1. 分母中的 1-p未被优化的部分耗时占比,是“永远省不掉”的部分。
    1−p\displaystyle 1 - p
  2. p/s 加速后残余热点算子被加速 s 倍后,剩下耗时的占比。
    ps\displaystyle \frac{p}{s}
  3. 整体加速比 S总耗时从 1 变成两项之和,加速比是它的倒数。
    S=1(1−p)+p/s\displaystyle S = \frac{1}{(1 - p) + p/s}

XLA融合策略:三步合成一个动作

主厨还发现一个规律:很多菜的流程是“切好放碗、焯好放碗、下锅前再放碗”,每一步都占一个碗、都要洗一次手、端一次盘。他干脆让同一位厨师把三步连着做完——中间不换碗,半成品直接在手边流转。台面立刻空出一大片,节奏也快了。

这正是 XLA 的算子融合(fusion)。编译器把多个相邻算子合并成一个融合核(fused kernel),中间张量不再写进显存,而是在片上寄存器或 SRAM 里直接流转。内存分析和 profiling 在这里合流:某个算子组合值不值得融合,取决于中间张量有多大、融合后能省多少显存和带宽。

粗略估算,融合对峰值显存的影响可以写成:

不过,主厨不是所有菜都敢连做——炖菜必须等蒸锅空出来,有些步骤之间有硬依赖。XLA 的编译器同样会掂量依赖关系和访存成本,结合 profiling 数据决定哪些算子值得融合,哪些融合反而拖慢节奏。这就是“编译决策”。

Mpeakafter≈Mpeakbefore−∑i=1msizeiintermediate+sizefusedout\displaystyle M_{\text{peak}}^{\text{after}} \approx M_{\text{peak}}^{\text{before}} - \sum_{i=1}^{m} \text{size}_i^{\text{intermediate}} + \text{size}_{\text{fused}}^{\text{out}}
通俗说明:融合省下的显存 ≈ 被合并掉的中间张量大小之和。就像把三只碗换成一只锅,中间那两份“半成品”再也不用占台面。
  1. 被合并的中间量求和项是原本要写进显存、现在在核内流转的 m 个中间张量。
    ∑i=1msizeiintermediate\displaystyle \sum_{i=1}^{m} \text{size}_i^{\text{intermediate}}
  2. 融合核的输出最后只需写回一个输出张量,尺寸通常远小于中间量之和。
    sizefusedout\displaystyle \text{size}_{\text{fused}}^{\text{out}}
  3. 近似相等融合对峰值的收益主要来自“少落地”的中间张量,其余项基本不变。
    Mpeakafter≈Mpeakbefore−ΔMintermediate\displaystyle M_{\text{peak}}^{\text{after}} \approx M_{\text{peak}}^{\text{before}} - \Delta M_{\text{intermediate}}

分片配置优化:多个厨房分头备菜

晚宴越办越大,一个厨房实在摆不下了。主厨索性把菜单分成三份,交给三个厨房同时备菜。可新的麻烦来了:A 厨房做好的半成品,B 厨房要用,就得走一段“传菜走廊”——走廊越窄、菜越大、份数越多,传菜越慢。

分布式训练正是这个思路:单卡显存放不下,就把张量切分成多份,分到多张 GPU/TPU 上。JAX 的 jax.sharding 机制(早期叫 pjit)允许你声明张量如何切、放到哪张卡。但分片必有代价——梯度聚合、张量收集都要跨设备通信,profiling 会把这段通信时间明明白白标出来。

以环状 all-gather 为例,要把一份大小为 M 的模型张量从 n 张卡同步到每张卡,通信时间近似为:

三个厨房传菜只要几分钟,五十个厨房的传菜走廊会直接堵死。分片不是越多越好——profiling 算出的通信耗时,就是决定数据并行、张量并行该用几路分片的最硬依据,这正是“分片配置优化”的全部意义。

Tallgather≈2⋅n−1n⋅MB\displaystyle T_{\text{allgather}} \approx 2 \cdot \frac{n - 1}{n} \cdot \frac{M}{B}
通俗说明:每张卡要把自己那一份菜分享给所有厨房,分享总量接近“菜的大小 × (n-1)/n”,走廊带宽 B 越大、菜越小,传得越快。
  1. 因子 2all-gather 在环状拓扑上要沿环走两圈(收发各一圈)。
    2\displaystyle 2
  2. M/B 单份传输时间张量大小除以链路带宽,得到千兆网等假设下传 M 字节的时间。
    MB\displaystyle \frac{M}{B}
  3. 权重 (n-1)/n每张卡只需要收到别人那 n-1 份,自己那份不用收,所以总量打个折扣。
    n−1n\displaystyle \frac{n - 1}{n}

AI 应用场景

大模型训练 OOM 的分钟级定位

阶段:模型训练调优 / 性能工程

解决的问题:传统做法是看到 CUDA OOM 错误后盲目降 batch size,反复试错,既慢又治标不治本。用 JAX 内存分析(导出 XLA HLO 分析张量生命周期 + TensorBoard 内存视图),能直接指出“第 12 层激活与第 12 层梯度在 T 时刻同时驻留导致峰值”,精准决定是开梯度检查点、降精度还是改分片。

具体案例:某团队训练 7B 规模的 LLM,预训练第 3 小时必现 OOM。他们用 jax.profiler.start_trace 采集 50 步 trace,在 TensorBoard Memory 视图看到峰值集中在长序列注意力层的激活张量;随后对注意力层启用梯度检查点、把其余层切到 bf16,峰值显存从 78GB 降到 51GB,训练稳定跑通。

Profiling 驱动分片策略选择

阶段:分布式训练配置

解决的问题:手动猜测“数据并行=8 还是张量并行=4”很容易踩坑:并行度不够时显存爆,并行度过大时通信把收益吃光。先用 profiling 采集算子耗时与设备利用率,再对关键集合通信(all-gather / reduce-scatter)单独计时,就能用数据做决策。

具体案例:在 16 卡 A100 上训练 13B 模型,工程师对比 dp=8/tp=2 与 dp=4/tp=4 两组配置的 trace:后者利用率从 62% 升到 71%,虽然通信多了 15%,但单卡显存压力小、batch 更大,整体吞吐反升 18%。

融合策略的收益预评估

阶段:编译优化

解决的问题:XLA 自动融合不透明,工程师想知道手写改写是否值得。用 HLO 分析算出“可融合算子组”的中间张量总量,估算融合后显存与带宽收益,再决定是否调整代码结构(如把多步 elementwise 写进同一个函数)。

具体案例:视觉模型里连续 6 个归一化/激活算子,HLO 分析显示中间量共 24MB、融合后只剩 8MB 输出,于是把代码重构为单函数并用 jax.jit 编译,Trace 中该区域耗时下降 31%。

Python 演示

python67 行
# -*- coding: utf-8 -*-"""JIT内存分析迷你模拟器: 张量生命周期 -> 峰值显存 -> profiler 报表""" class OpSpec:    """一条profile记录: 名称/输出张量大小/执行耗时/开始时刻"""     def __init__(self, name, out_mb, time_ms, start_ms):        self.name = name        self.out_mb = out_mb        self.time_ms = time_ms        self.start_ms = start_ms        self.end_ms = start_ms + time_ms  def build_timeline(do_fusion=False):    """构造一条链式计算图的时间线; 融合后中间张量不再落地"""    if not do_fusion:        return [            OpSpec("matmul:0", 128, 12.0, 0.0),            OpSpec("relu:0", 128, 4.0, 12.0),            OpSpec("matmul:1", 256, 22.0, 16.0),            OpSpec("dropout", 256, 3.0, 40.0),            OpSpec("layer_norm", 128, 5.0, 43.0),            OpSpec("softmax", 128, 6.0, 49.0),        ]    return [        OpSpec("fused(matmul0,relu0)", 128, 14.0, 0.0),        OpSpec("matmul:1", 256, 22.0, 14.0),        OpSpec("fused(dropout,layer_norm)", 128, 6.0, 38.0),        OpSpec("softmax", 128, 6.0, 45.0),    ]  def analyze(ops):    """模拟XLA的buffer生命周期: 返回(峰值MB, 设备利用率)"""    events = []    for i, op in enumerate(ops):        born = op.start_ms  # 输出缓冲区在算子执行时被写入        die = ops[i + 1].end_ms if i + 1 < len(ops) else op.end_ms        events.append((born, op.out_mb))        events.append((die, -op.out_mb))    alive = 0    peak = 0    for _, delta in sorted(events):        alive += delta        if alive > peak:            peak = alive    compute = sum(op.time_ms for op in ops)    total = ops[-1].end_ms    return peak, compute / total  def report(ops, label):    """打印profiler风格的算子级报表"""    print("=== %s ===" % label)    peak, util = analyze(ops)    total = sum(o.time_ms for o in ops)    for op in ops:        ratio = 100.0 * op.time_ms / total        print("  %-26s  %6.1f ms  %5d MB  %5.1f%%" % (op.name, op.time_ms, op.out_mb, ratio))    print("  峰值显存: %d MB,  设备利用率: %.1f%%" % (peak, 100.0 * util))    print()  if __name__ == "__main__":    report(build_timeline(False), "融合前")    report(build_timeline(True), "融合后")
目的:用一个小型模拟器演示“张量生命周期 → 峰值显存 → 算子耗时占比”的分析链路,最后对比融合前后的峰值。
  1. class OpSpec:一条 profile 记录,等价于 JAX trace 里每一行的算子信息。
  2. born = op.start_ms张量的“出生时刻”不是算子结束,而是算子开始申请输出缓冲的那一刻。
  3. die = ops[i + 1].end_ms if i + 1 < len(ops) else op.end_ms张量活到“消费它的下一个算子跑完”才释放,这就是生命周期边界。
  4. for _, delta in sorted(events):按时刻累加正负变化取峰值,等价于对张量做一次扫描,比每个时刻重算高效得多。
  5. compute = sum(op.time_ms for op in ops)` 与 `total = ops[-1].end_ms设备利用率 = 计算时间 ÷ 总执行时间。
  6. ratio = 100.0 * op.time_ms / total耗时占比,正是 Amdahl 定律里那个决定优化上限的 p。

常见误区

误区一看到显存 OOM,我就去翻 CUDA 错误栈,以为它能指出是哪一行代码、哪个张量吃满了显存。其实错误信息通常只告诉你“分配失败”,剩下的全靠猜。

正解OOM 只是“最后一根稻草”:显存是全局共享的资源池,分配失败发生在“此刻要新申请一块显存但不够了”的瞬间,它既不反映峰值发生的时刻,也不告诉你哪些张量同时驻留。要定位,必须回到 XLA HLO 做张量生命周期分析,找出“同时存活”的组合。就像厨房台面爆掉的那一刻,摔碎的往往只是最后端上来的那碗汤,但它绝不是占台面的元凶。

为何常见:因为 OOM 报错最显眼、最即时,而 HLO 分析需要额外步骤,人们习惯先找最省事的信息。

误区我以为 profiling 的全部意义就是找出哪些算子跑得慢,然后把它们换成就地实现。优化之后设备利用率却没怎么变。

正解算子自身的耗时只是冰山一角。设备利用率低时,瓶颈往往在访存等待、kernel 启动、通信同步这些“看不见的时间”里。U = T_compute / T_total 告诉我们:把 compute 时间优化得再快,如果总时间里三成都是搬运和等待,整体收益也有限。主厨的教训是:颠勺再快,也没办法让蒸锅提前沸腾。

为何常见:算子耗时最直观、最好比较,而等待类开销藏在 trace 的缝隙里,容易被忽略。

误区JIT 编译不是全自动的魔法吗?XLA 不是会自动融合所有算子吗?那我何必还花时间分析和调配置?

正解XLA 的融合受依赖关系和性能模型的约束,并非所有算子组合都值得融合——有些融合会增大寄存器压力反而变慢,有些中间张量被多个分支使用无法就地消费。融合决策需要 profiling 数据来验证收益。自动编译器做的是“一般情况下的优化”,具体到你的模型结构,仍要靠内存分析来判断要不要调整代码写法。

为何常见:JIT 的“自动”两字让人误以为无需人工干预,忽略了编译器只是在通用策略下做取舍。

误区显存放不下就多加卡,卡越多越好,反正数据被切得更碎,每张卡的负担更小。

正解分片在降低单卡显存和计算压力的同时,引入了通信开销。all-gather 的 (n-1)/n 因子说明:n 越大,聚合交换的数据总量越接近一整份 M,而链路带宽不变,通信时间只会变长。超过某个临界点后,加卡反而让吞吐下降。3D 并行里数据并行、张量并行、流水线并行的配比,必须以 profiling 的通信耗时为依据。

为何常见:直觉上“人多好办事”,但忽略了人多之后的“协调成本”——分布式里协调成本就是通信。

面试题

Q1. 在 JAX 中定位训练 OOM 的根因,最合理的做法是?

单选题 · easy

  • A.查看 GPU 驱动的报错日志
  • B.分析 XLA HLO 的张量生命周期← 正确答案
  • C.反复缩小 batch size 来试错
  • D.打印每个张量的具体大小

答案:B。OOM 只是“最后一根稻草”,报错日志和试错都无法告诉你峰值时刻到底是哪几个张量同时驻留。XLA HLO 记录了每个张量的产生、消费与释放时刻,像主厨的台面时间线一样,能精确算出峰值与“同时存活组合”,是对症下药的第一步。

Q2. 梯度检查点降低显存占用的原理是?

单选题 · mid

  • A.用更低精度保存全部激活
  • B.只保存部分激活并在反向时重算← 正确答案
  • C.复用已释放张量的显存块
  • D.压缩优化器状态大小

答案:B。检查点类似“半成品冰箱”:每隔 K 层存一份激活,其余层不存,反向传播需要时重算,显存从 O(n) 降到 O(√n)。A 是混合精度而非检查点,C 是缓存复用,D 是优化器状态处理,都与检查点无关。

Q3. profiling 显示设备利用率偏低,可能的原因有?

多选题 · mid

  • A.频繁的显存搬运与等待← 正确答案
  • B.集合通信的同步等待← 正确答案
  • C.Kernel 启动的开销累积← 正确答案
  • D.浮点运算本身过慢

答案:ABC。U = T_compute / T_total,分子之外的一切都在分母里。搬运(T_memory)、通信同步(T_comm)、kernel 启动开销都会拉低 U。D 说“运算本身慢”对应的是 T_compute 大,那反而会让 U 升高,是“忙不过来”而非“闲得发慌”。

Q4. profiling 报表中耗时占比最高的算子通常被称为?

单选题 · easy

  • A.热点算子← 正确答案
  • B.关键路径
  • C.融合算子
  • D.尾延迟算子

答案:A。耗时占比最高即“热点”(hotspot),是 Amdahl 定律里那个决定优化上限的 p。关键路径是调度术语,融合算子是 XLA 编译产物,尾延迟算子与高耗时占比无关。

Q5. 某热点算子耗时占总时间 80%,将它加速 4 倍后,整体加速比约为?

单选题 · hard

  • A.1.25 倍
  • B.1.50 倍
  • C.2.00 倍
  • D.2.50 倍← 正确答案

答案:D。套用 Amdahl 定律 S = 1 / ((1-p) + p/s)。p=0.8、s=4 时,S = 1 / (0.2 + 0.2) = 2.5。即使这个算子快得接近零耗时,整体也不会超过 5 倍,因为那 20% 的“其他步骤”永远省不掉——就像炖牛骨再快,摆盘的时间也少不了。

Q6. XLA 算子融合带来的最主要内存收益是?

单选题 · mid

  • A.显著减少模型参数存储
  • B.中间张量不再写回显存← 正确答案
  • C.反向传播的梯度变小
  • D.优化器状态占用减少

答案:B。融合核把多个算子的中间结果留在片上寄存器/SRAM,不落地到全局显存,直接消掉了“中间量求和”那一项。参数、梯度、优化器状态都不受融合影响,所以 A/C/D 不成立。

Q7. 以下哪些组合能有效判断 XLA 融合是否值得做?

多选题 · hard

  • A.用 HLO 分析中间张量大小← 正确答案
  • B.对比融合前后的峰值显存← 正确答案
  • C.对比融合前后的设备利用率← 正确答案
  • D.只看融合核的代码行数

答案:ABC。融合收益 = 省掉的中间张量 − 融合核本身的代价。HLO 给出中间量大小,profiling 给出融合前后的峰值与利用率,三者结合才能拍板。D 的代码行数与性能没有因果关系,不能作为依据。

Q8. 环状 all-gather 通信时间公式中的因子 (n-1)/n 表示?

单选题 · extreme

  • A.每张卡只需接收其余 n-1 张卡的数据← 正确答案
  • B.张量需要绕环传送 n-1 圈
  • C.有效带宽只剩下原来的 1/n
  • D.张量被切分成 n 份后再合并

答案:A。all-gather 的目标是让每张卡拥有完整张量:自己的那份不用收,只需收别人那 n-1 份,于是总量是 M·(n-1),均摊到 n 张卡并发传输就得到 (n-1)/n · M。n 越大,这个因子越接近 1,通信总量越接近一整份 M,说明加卡会放大通信压力。

Q9. 应对“显存墙”问题的常用策略包括?

多选题 · easy

  • A.对激活启用梯度检查点← 正确答案
  • B.使用 bf16 混合精度训练← 正确答案
  • C.把模型分片到多张设备← 正确答案
  • D.无限扩大主机内存空间

答案:ABC。检查点降低激活存储、bf16 减半张量字节、分片把压力分摊到多卡,都是对症的“台面管理”手段。D 扩大主机内存只能救 CPU,GPU 显存的“墙”依然在那里,治标不治本。

Q10. 融合前后对比 profiling 数据,最应重点观察的指标是?

单选题 · mid

  • A.采样器频率是否变化
  • B.峰值显存与设备利用率← 正确答案
  • C.张量的数值精度变化
  • D.优化器更新轮数变化

答案:B。融合的目标有两个:一是减少中间张量落地,表现为峰值显存下降;二是减少 kernel 启动与访存等待,表现为设备利用率提升。两者直接由 profiling 的显存曲线与利用率曲线刻画,其余选项与融合无直接关系。