JAX/反向自动微分

通过部分求值选择保存中间值还是在求导时重算

JAX 的 checkpoint 把函数表示成 Jaxpr,在求导变换中根据策略保留必要中间值,并为其余计算生成重算部分。

efe43f1bd内存管理核验 2026-10-12
相关概念

工程问题#

反向自动微分需要前向计算的中间值。全部保存会占用较多设备内存,全部重算又可能重复执行代价很高的矩阵乘法;选择还必须受反向计算真正用到哪些值约束。

实际实现#

checkpoint 在此版本默认路径中绑定 remat_p。其部分求值规则把 Jaxpr 拆为已知计算和延后计算,调用保存策略后继续做死代码删除,只保留延后部分需要的 residuals。已知部分计算出输出与 residuals,未知部分重新绑定 remat_p,并携带这些 residuals 和仍需使用的输入。

处理流程#

  1. 把被 checkpoint 包装的函数跟踪为 Jaxpr,并绑定保存策略
  2. 按已知输入和策略拆出前向可计算部分与延后部分
  3. 删除延后部分及前向部分中无用的计算与 residuals
  4. 保存剩余 residuals;求导时用保留输入和这些值执行所需重算

源码片段#

固定版本 · 原文摘录
jax/_src/config.pyefe43f1bd5456089f4276336462ed51d1f13f052 · L1322–L1329
查看原文件
1322remat3 = bool_state(1323    name='jax_remat3',1324    default=False,1325    upgrade=True,1326    help='If True, embrace the future of remat.',1327    include_in_jit_key=True,1328    include_in_trace_context=True,1329)
jax_remat3

固定提交将 remat3 默认设为 False,因此下文分析 remat_p 分支;运行环境显式打开开关时不适用这条内部调用链。

jax/_src/ad_checkpoint.pyefe43f1bd5456089f4276336462ed51d1f13f052 · L378–L389
查看原文件
378    jaxpr, consts, out_tree = _trace_to_jaxpr(fun_, in_tree, tuple(in_avals), debug)379    if config.mutable_array_checks.value:380      api_util._check_no_aliased_closed_over_refs(debug, consts, args_flat)381    if isinstance(prevent_cse, tuple):382      cse_args = (tuple(args), kwargs) if kwargs else tuple(args)383      cse = (False,) * len(consts) + tuple(broadcast_prefix(prevent_cse, cse_args))384    else:385      cse = prevent_cse386    out_flat = remat_p.bind(387        *consts, *args_flat, jaxpr=jaxpr, prevent_cse=cse, differentiated=False,388        policy=policy)389    return tree_unflatten(out_tree, out_flat)
checkpoint.fun_remat

函数被跟踪为 Jaxpr,随后连同 policy 绑定 remat_p;它不是运行时直接删除任意中间张量的钩子。

jax/_src/ad_checkpoint.pyefe43f1bd5456089f4276336462ed51d1f13f052 · L83–L93
查看原文件
83def dots_with_no_batch_dims_saveable(prim, *args, **params) -> bool:84  """This is a useful heuristic for transformers."""85  if prim is lax_internal.dot_general_p:86    (_, _), (lhs_b, rhs_b) = params['dimension_numbers']87    if not lhs_b and not rhs_b:88      return True8990  if prim.name == "scaled_matmul_wrapper":  # avoid cudnn import91    return args[0].shape[0] == 19293  return False
dots_with_no_batch_dims_saveable

一个具体策略允许保存没有批维的 dot 结果,并对该版本的 scaled_matmul_wrapper 单独判断;这里只是可保存性启发式。

jax/_src/ad_checkpoint.pyefe43f1bd5456089f4276336462ed51d1f13f052 · L586–L607
查看原文件
586def remat_partial_eval(trace: pe.JaxprTrace, *tracers: core.Tracer,587                       jaxpr: core.Jaxpr, prevent_cse, **params):588  assert not jaxpr.constvars589  disallowed_effects = effects.remat_allowed_effects.filter_not_in(jaxpr.effects)590  if disallowed_effects:591    raise NotImplementedError(592        f'Effects not supported in AD of `checkpoint`/`remat`: {disallowed_effects}')593  policy = params['policy'] or nothing_saveable594  in_unknowns = [not t.is_known() for t in tracers]595  jaxpr_known, jaxpr_staged, out_unknowns, out_inst, num_res = \596      pe.partial_eval_jaxpr_custom(597          jaxpr, in_unknowns, [True] * len(in_unknowns), False, False, policy)598599  # DCE jaxpr_staged, keeping only instantiated outputs which are unknown600  _, out_inst_unknown = partition_list(out_inst, out_unknowns)601  jaxpr_unknown, in_used_staged = pe.dce_jaxpr(jaxpr_staged, out_inst_unknown)602  used_res, in_used_staged = split_list(in_used_staged, [num_res])603604  # DCE jaxpr_known, keeping all known outputs but discarding dce'd res605  out_used_known = [True] * (len(out_unknowns) - sum(out_unknowns)) + used_res606  jaxpr_known, in_used_known = pe.dce_jaxpr(jaxpr_known, out_used_known)607  num_res = sum(used_res)
remat_partial_eval:拆分与 DCE

按策略部分求值,然后两次进行死代码删除:先知道延后部分用到哪些 residuals,再清理前向保存集合。

jax/_src/ad_checkpoint.pyefe43f1bd5456089f4276336462ed51d1f13f052 · L609–L618
查看原文件
609  # To avoid precision mismatches in fwd and bwd passes due to XLA excess610  # precision, insert explicit x = reduce_precision(x, **finfo(x.dtype)) calls611  # on producers of any residuals. See https://github.com/jax-ml/jax/pull/22244.612  jaxpr_known_ = _insert_reduce_precision(jaxpr_known, num_res)613614  # Compute known outputs and residuals (hoisted out of remat primitive)615  _, in_consts_ = unzip2(t.pval for t in tracers if t.pval.is_known())616  _, in_consts = partition_list(in_used_known, in_consts_)617  out_consts = core.eval_jaxpr(jaxpr_known_, (), *in_consts)618  out_knowns, residuals = split_list(out_consts, [len(out_consts)-num_res])
remat_partial_eval:计算 residuals

精度规整后执行已知部分,分离出已知输出与确实保留的 residuals。

jax/_src/ad_checkpoint.pyefe43f1bd5456089f4276336462ed51d1f13f052 · L620–L633
查看原文件
620  # set up unknown outputs with a recipe to call remat621  res_tracers = map(trace.new_instantiated_const, residuals)622  _, tracers_staged = partition_list(in_used_staged, tracers)623  in_jaxpr_tracers = res_tracers + map(trace.instantiate_const, tracers_staged)  # pyrefly: ignore[bad-argument-type]624  out_jaxpr_tracers = [pe.JaxprTracer(trace, pe.PartialVal.unknown(x.aval), None)625                       for x in jaxpr_unknown.outvars]626  if isinstance(prevent_cse, tuple):627    _, prevent_cse_ = partition_list(in_used_staged, prevent_cse)628    prevent_cse = (True,) * len(res_tracers) + tuple(prevent_cse_)629  new_params = dict(params, jaxpr=jaxpr_unknown, differentiated=True,630                    prevent_cse=prevent_cse)631  recipe = pe.new_eqn_recipe(trace, in_jaxpr_tracers, out_jaxpr_tracers, remat_p,632                             new_params, core.positional_effects(jaxpr_unknown),633                             source_info_util.current())
remat_partial_eval:重算配方

把 residuals 与仍需使用的输入组装为新的 remat_p 方程;延后部分的执行依赖在这里明确记录。

片段保持固定提交的连续原文;完整源码与许可证保存在本地。每段说明交代所在分支,省略部分可通过出处查看。

收益与取舍#

工程分析

带来的收益

  • 策略可以允许保留矩阵乘法结果,同时重算较便宜的中间计算
  • 死代码删除使“允许保存”不等于“必须保存”,减少无用 residuals

付出的代价

  • 省内存需要付出重算成本,策略不是全局最优内存规划;还需针对模型和硬件测量
  • 部分副作用不支持 remat;源码显式拒绝不允许的 effects
  • 本提交会对部分 residual 的生产者插入精度规整,避免前后向重算的精度不一致;不能把 checkpoint 简化为丢掉张量后原样再跑一次

可比较的选择

不使用 checkpoint让自动微分保存所需线性化中间值,重算较少,但显存占用可能较高。

中间值转存主机内存用设备与主机之间的数据传输换取设备内存,成本与重算策略不同;JAX 另有 offload 策略。

版本与适用范围#

对应固定提交 efe43f1 的 remat_p 路径;config.py 中 jax_remat3 默认 False。启用 jax_remat3 后会走另一个实现,本文不混用两条调用链。checkpoint 策略决定可保存性,不保证固定节省比例。

参考来源#

源码、官方文档与原始论文
01
JAX · jax_remat3

jax/_src/config.py · jax_remat3

源码efe43f1bd5456089f4276336462ed51d1f13f052
02
JAX · checkpoint.fun_remat

jax/_src/ad_checkpoint.py · checkpoint.fun_remat

源码efe43f1bd5456089f4276336462ed51d1f13f052

相关实现与比较#

1 个相关案例
19 个匹配案例