JAX 如何处理扩展窗口以避免按行数重复编译?
结论
要避免扩展窗口每增加一行就触发一次编译,传给 jax.jit 的数组形状必须保持不变。可以将完整数据(或预设上限大小的数据)作为固定形状输入,再用布尔掩码表示当前时间窗口。模型计算时通过掩码排除窗口外的数据。
原示例中的 X[X[:, 0] <= t, ...] 会根据数组中的值筛出不定行数的结果。JAX 在 jit 跟踪期间无法确定结果形状,因此会触发 NonConcreteBooleanIndexError。即使在 JIT 函数外筛选,只要 X_filtered 的行数发生变化,输入形状也会随之变化,需要重新编译。
固定形状输入与掩码
逐行计算时,可以在 JIT 函数中保留所有行,并用掩码标记当前窗口内有效的行:
import jax
import jax.numpy as jnp
@jax.jit
def test_func(X, t):
in_window = X[:, 0] <= t
values = X[:, 1:] ** 2
return jnp.where(in_window[:, None], values, 0)
X = jnp.array([
[0, 1, 1],
[0, 1, 1],
[1, 2, 4],
[2, 3, 9],
[2, 3, 9],
[3, 4, 16],
[4, 5, 25],
])
for t in range(5):
result = test_func(X, jnp.asarray(t))
print(result)
每次调用传入的 X 形状都相同,t 是一个标量数组。结果会保留所有输入行,并将窗口外的行置为零。如果零本身也可能是有效数据,就要同时保留并使用 in_window 掩码,不能只根据输出值判断行是否有效。
将掩码用于模型目标
如果模型通过逐行损失来估计参数,可以先计算每行的损失,再用掩码汇总:
@jax.jit
def masked_mean_loss(prediction, target, in_window):
row_loss = (prediction - target) ** 2
row_loss = jnp.where(in_window, row_loss, 0.0)
count = jnp.maximum(in_window.sum(), 1)
return row_loss.sum() / count
实际使用时,应将掩码传入目标函数、似然或统计量计算,确保窗口外的观测不会影响参数估计。masked_mean_loss 展示的是一种掩码汇总方式,不能直接套用到所有统计模型中。
如果模型必须接收紧凑的变长矩阵,或包含依赖当前样本数的运算,就可能需要重新设计计算方式。可以将行数填充到有限个固定大小的 bucket,并在模型内屏蔽填充行。这样,编译次数最多与 bucket 数量相关。如果固定形状和分桶都不可行,就需要承担不同输入形状分别编译的成本。
关于重编译
JAX 会按输入类型和形状缓存编译结果。输入形状不同,通常需要重新跟踪和编译。将完整数据保持为同一形状,并把时间索引作为数组值传入,有助于复用同一编译结果。也要避免在循环中反复创建新的 JIT 函数;将参数声明为静态参数时,不同取值也可能触发重新编译。JAX JIT 文档
扩展窗口应通过掩码值的变化来表示,传入 JIT 函数的数组行数则保持不变。JAX 限制动态布尔索引,是因为编译时必须确定数组形状;掩码运算可以保持形状静态。JAX 动态形状说明;NonConcreteBooleanIndexError 说明
备注:内容仅供参考。
