AnonMLuser's picture
Anonymous artefact release
a1a56a2 verified
Raw
History Blame Contribute Delete
6.19 kB
"""Gradient-space diagnostics for RL fine-tuning analysis.
All functions return JAX arrays and are fully JIT-compatible.
Python-side wrappers (returning dataclasses) are provided for
non-compiled call sites; the inner ``_*`` functions are used
directly inside ``jax.lax.scan``.
"""
from __future__ import annotations
from collections.abc import Callable
from typing import Any
import jax
import jax.numpy as jnp
def make_grad_alignment_fn(
apply_fn: Callable,
schedule_fn: Callable,
schedule_deriv_fn: Callable,
num_actions: int,
sigma_t: float = 0.0,
) -> Callable:
"""Build a JIT-compiled gradient alignment function.
Returns a function that computes cosine similarity, RL grad norm, and
BC grad norm as a tuple of three JAX scalars — safe for use inside
``jax.lax.scan`` / ``jax.lax.cond``.
Args:
apply_fn: Training apply fn (params, obs, z_t, t, rng) -> logits.
schedule_fn: alpha(t) noise schedule.
schedule_deriv_fn: d(alpha)/dt analytic derivative.
num_actions: Action vocabulary size.
sigma_t: ReMDM remasking correction.
Returns:
JIT-compiled fn(params, ref_params, acts, obs, valid, rng, advantages)
-> (cos_sim, rl_norm, bc_norm) as JAX scalars.
"""
from src.diffusion.loss import compute_loss
@jax.jit
def grad_alignment(
params: Any,
ref_params: Any,
acts: jax.Array,
obs: jax.Array,
valid: jax.Array,
rng: jax.Array,
advantages: jax.Array,
) -> tuple[jax.Array, jax.Array, jax.Array]:
"""Compute cosine similarity between RL and BC gradient vectors.
Both gradients are taken on the same batch **and the same
``(z_t, t)`` draw**: ``compute_loss`` samples its timestep and its
masking from the key it is given, so the two losses are handed one
key rather than two halves of a split. At independent draws the
metric reports Monte-Carlo noise as objective disagreement. Measured
over six trials on one fixed batch: two draws give a mean cosine of
0.309 ranging over 0.680, including two trials that report
*anti*-alignment, where one draw gives 0.968 ranging over 0.066.
The BC gradient is taken at ``ref_params``, the pretrained
parameters, not at ``params``. A fixed reference is comparable
across iterations and is the quantity the forgetting framing needs.
Args:
params: Current model parameters.
ref_params: Pretrained reference parameters.
acts: ``[B, H]`` int32 action sequences.
obs: ``[B, obs_dim]`` float32 observations.
valid: ``[B]`` validity mask.
rng: PRNG key, shared by both losses.
advantages: ``[B]`` return weights.
Returns:
Tuple of (cos_sim, rl_grad_norm, bc_grad_norm) as JAX scalars.
"""
def rl_loss(p: Any) -> jax.Array:
loss, _ = compute_loss(
apply_fn,
p,
rng,
acts,
obs,
valid,
num_actions,
schedule_fn,
schedule_deriv_fn,
sigma_t=sigma_t,
advantages=advantages,
)
return loss
def bc_loss(p: Any) -> jax.Array:
loss, _ = compute_loss(
apply_fn,
p,
rng,
acts,
obs,
valid,
num_actions,
schedule_fn,
schedule_deriv_fn,
sigma_t=sigma_t,
advantages=None,
)
return loss
g_rl = jax.grad(rl_loss)(params)
g_bc = jax.grad(bc_loss)(ref_params)
rl_flat = jnp.concatenate(
[g.ravel() for g in jax.tree.leaves(g_rl)]
) # [D_total]
bc_flat = jnp.concatenate(
[g.ravel() for g in jax.tree.leaves(g_bc)]
) # [D_total]
rl_norm = jnp.linalg.norm(rl_flat)
bc_norm = jnp.linalg.norm(bc_flat)
cos_sim = jnp.dot(rl_flat, bc_flat) / (rl_norm * bc_norm + 1e-10)
return cos_sim, rl_norm, bc_norm
return grad_alignment
def compute_per_layer_grad_norms_jax(grads: Any) -> jax.Array:
"""Compute L2 gradient norm per parameter leaf as a JAX array.
Unlike the old ``PerLayerGradNorms`` dataclass, this returns a single
1-D array of norms — one entry per pytree leaf — that is JIT-safe.
The mapping from index to layer name can be recovered outside JIT via
``jax.tree.structure(params)``.
Args:
grads: Gradient pytree (same structure as params).
Returns:
``[num_leaves]`` JAX array of per-leaf L2 norms.
"""
leaves = jax.tree.leaves(grads)
return jnp.stack([jnp.linalg.norm(leaf) for leaf in leaves])
def compute_surgery_metrics_jax(
g_rl_before: Any,
g_rl_after: Any,
) -> tuple[jax.Array, jax.Array]:
"""Measure gradient mass removed by PCGrad, returning JAX scalars.
Args:
g_rl_before: RL gradient pytree before projection.
g_rl_after: RL gradient pytree after projection.
Returns:
Tuple of (projected_mass_fraction, n_conflicting_params) as JAX arrays.
"""
before_leaves = jax.tree.leaves(g_rl_before)
after_leaves = jax.tree.leaves(g_rl_after)
before_sq = jnp.stack([jnp.sum(g**2) for g in before_leaves])
after_sq = jnp.stack([jnp.sum(g**2) for g in after_leaves])
total_before = jnp.sum(before_sq)
total_after = jnp.sum(after_sq)
projected_mass = jnp.maximum(total_before - total_after, 0.0)
fraction = projected_mass / jnp.maximum(total_before, 1e-10)
# Count leaves where projection was applied (dot product of diff > 0)
n_conflicting = jnp.sum(
jnp.stack(
[
(jnp.sum(g_r * (g_r - g_a)) > 0).astype(jnp.int32)
for g_r, g_a in zip(before_leaves, after_leaves, strict=False)
]
)
)
return fraction, n_conflicting