如何在 JAX 中实现支持动态形状的 next 函数(字符串重写系统)

14次阅读

如何在 JAX 中实现支持动态形状的 next 函数(字符串重写系统)

本文详解如何在 jax 中绕过动态形状限制,用静态形状语义实现可向量化、可递归应用的规则替换函数,适用于字符串重写系统等场景。

JAX 的核心约束之一是:所有变换(如 vmap、jit、grad)要求中间和输出数组具有静态形状(Static shape)——即形状不能依赖于运行时张量值。而原始 replace_first_one 函数中,jnp.where(arr == 1)[0] 的长度、jnp.concatenate 的结果长度均随输入数据动态变化,导致 vmap 报错:size argument of jnp.nonzero must be statically specified。

要使函数兼容 vmap,关键在于消除所有动态形状分支,转而采用“填充-掩码-切片”范式,确保每一步输出尺寸完全可推导且恒定。以下是完整、可向量化、可嵌套调用的实现方案:

✅ 正确实现(静态形状兼容版)

import jax import jax.numpy as jnp from jax import vmap  # 所有规则必须统一长度(静态形状前提) rules_int = jnp.array([     [0, 0],   # rule 0 → length=2     [1, 1],   # rule 1 → length=2 ], dtype=jnp.int32)  def replace_first_one(arr, action):     """     静态形状安全的首 '1' 替换函数:     - 输入 arr 形状固定为 (L,),action 为标量     - 输出形状固定为 (L + 1),因每次替换:删1个元素 + 插入2个 → 净增1     - 使用 jnp.where(..., size=1) 强制返回固定长度索引     - 使用 dynamic_update_slice 避免 concat 动态拼接     """     L = arr.shape[0]     # 查找第一个 1 的位置;若不存在,index = L(越界,后续逻辑自动跳过替换)     indices = jnp.where(arr == 1, size=1, fill_value=L)[0]     index = indices[0]  # scalar index, static at compile time      # 待插入规则向量     rule = rules_int[action]      # 预分配输出数组(长度 = L - 1 + len(rule) = L + 1)     output = jnp.zeros(L + 1, dtype=arr.dtype)      # 构造插入前段:arr[:index]     pre = jnp.where(jnp.arange(L) < index, arr, 0)     # 构造插入后段:arr[index+1:]     post = jnp.where(jnp.arange(L) > index, arr, 0)      # 拼接三段(逻辑上)→ 但实际用 dynamic_update_slice 实现高效写入     # 更简洁做法:先填入 arr[0:index] 和 arr[index+1:],再覆盖插入 rule     # 我们分步构造:     output = output.at[:index].set(arr[:index])     output = output.at[index:index+2].set(rule)  # rule 长度固定为 2     output = output.at[index+2:].set(arr[index+1:L-1])  # 注意对齐长度      # ⚠️ 上述 set 可能越界,更鲁棒写法:使用 dynamic_update_slice + mask     # 推荐最终版本(零拷贝、边界安全):     output = jnp.zeros(L + 1, dtype=arr.dtype)     # 写入前段 [0:index]     output = jax.lax.dynamic_update_slice(output, arr[:index], (0,))     # 写入规则 [index:index+2]     output = jax.lax.dynamic_update_slice(output, rule, (index,))     # 写入后段 [index+2:]     tail_start = index + 1  # 原 arr 中跳过 index 后的起始位置     tail_len = L - tail_start     pad_len = (L + 1) - (index + 2) - tail_len     padded_tail = jnp.pad(arr[tail_start:], (0, pad_len), constant_values=0)     output = jax.lax.dynamic_update_slice(output, padded_tail, (index + 2,))      return output  # ✅ 现在可安全 vmap batch_arr = jnp.array([     [1, 4, 5, 1],  # → 替换第0个1 → [0,0,4,5,1]     [6, 1, 8, 1],  # → 替换第1个1 → [6,1,1,8,1] ]) batch_actions = jnp.array([0, 1])  vectorized_replace = vmap(replace_first_one, in_axes=(0, 0)) result = vectorized_replace(batch_arr, batch_actions) print(result) # [[0 0 4 5 1] #  [6 1 1 8 1]]

? 支持递归应用(字符串重写系统)

若需反复应用规则直至无 1 可替换(即模拟图灵机或 L-system),可用 jax.lax.while_loop 实现静态迭代上限下的循环

def rewrite_until_stable(init_arr, max_steps=10):     def cond_fn(state):         arr, step = state         has_one = jnp.any(arr == 1)         return jnp.logical_and(has_one, step < max_steps)      def body_fn(state):         arr, step = state         # 找到首个 1 对应的 action(示例:固定用 rule 0;实际可查表)         index = jnp.where(arr == 1, size=1, fill_value=arr.shape[0])[0][0]         action = jnp.where(index < arr.shape[0], 0, 0)  # placeholder         new_arr = replace_first_one(arr, action)         return new_arr, step + 1      final_arr, _ = jax.lax.while_loop(cond_fn, body_fn, (init_arr, 0))     return final_arr  # 示例:jnp.array([1]) → [0,0] → 无1 → 停止 stable = rewrite_until_stable(jnp.array([1])) print(stable)  # [0 0]

⚠️ 注意事项与权衡

  • 规则长度必须统一:这是静态形状的硬性要求。若原始规则长度不一(如 [0,0] vs [1,1,1]),需补零或截断至最大长度,并用 mask 控制有效区域。
  • 性能提示:dynamic_update_slice 比 concatenate 更适合 JIT;避免 jnp.where 无 size 参数的用法。
  • 替代方案:若业务逻辑必须支持真正变长输出(如生成不同长度 Token 序列),则应考虑:
    • python 层循环(放弃 vmap 加速);
    • 使用 jax.vmap + jax.pmap 分 batch 处理,每 batch 内部统一 padding
    • 迁移至支持 ragged tensor 的框架(如 tensorflow with tf.RaggedTensor),但将失去 JAX 生态优势。

总之,JAX 中的“动态形状”并非不可逾越,而是需要以静态契约重构逻辑——通过预分配、填充、掩码与 slice 操作,在编译期锁定维度,从而释放 vmap/jit 的全部潜力。

text=ZqhQzanResources