Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions param_decomp/core/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ class SmoothL0ImportanceMinimalityLossConfig(LossMetricConfig):


class CIMaskedReconLossConfig(LossMetricConfig, HiddenActsReconstructionMixin):
slow: ClassVar[bool] = False
type: Literal["CIMaskedReconLoss"] = "CIMaskedReconLoss"


Expand Down Expand Up @@ -467,6 +468,15 @@ class ComponentActivationDensityConfig(BaseConfig):
ci_alive_threshold: float = 0.0


class WeightMagnitudeConfig(BaseConfig):

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New plot. slow: ClassVar[bool] is required of every eval metric — assert_every_metric_declares_its_tier enforces that each class declares its own tier rather than inheriting one.

[written by Claude]

"""Per-site `‖V_c‖·‖U_c‖` scatter in descending magnitude order, log y.

Reads the trained V/U alone — no forward pass and no eval batch."""

slow: ClassVar[bool] = True
type: Literal["WeightMagnitude"] = "WeightMagnitude"


class IdentityCITargetSpec(BaseConfig):
"""A layer expected to produce an Identity CI pattern over `n_features` features."""

Expand Down Expand Up @@ -657,6 +667,7 @@ class UnmaskedNoDeltaReconLossConfig(LossMetricConfig):
exists here, and it is non-target-only — the plain and target-pass unions have no
member for it."""

slow: ClassVar[bool] = False
type: Literal["UnmaskedNoDeltaReconLoss"] = "UnmaskedNoDeltaReconLoss"


Expand Down
5 changes: 4 additions & 1 deletion param_decomp/core/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,10 @@ def log(self, step: int, record: "LogRecord") -> None:
self._last_committed_step = step
record = {
_METRIC_KEYS.get(
k, f"train/{k}" if k.startswith(("grad_norms/", "loss/", "schedules/")) else k
k,
f"train/{k}"
if k.startswith(("grad_norms/", "loss/", "schedules/", "nontarget_data/"))

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The renamed non-target keys start with nontarget_data/ rather than loss/, so without this entry they'd fall through to the else arm and be logged with no train/ tier at all.

[written by Claude]

else k,
): v
for k, v in record.items()
} # keys already starting "train/" or "eval/" pass through verbatim
Expand Down
93 changes: 91 additions & 2 deletions param_decomp/core/slow_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
lower_leaky_hard_sigmoid,
upper_leaky_hard_sigmoid,
)
from param_decomp.core.components import ComponentStacks
from param_decomp.core.configs import (
DenseCITargetSpec,
IdentityCIErrorConfig,
Expand Down Expand Up @@ -198,6 +199,11 @@ def slow_eval_step(
return filter_jit(slow_eval_step, compiler_options=compiler_options)


def _raw_sample(chunks: dict[str, list[np.ndarray]], site: str) -> np.ndarray:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug fix, not a refactor. n_batches_accum=0 means 'keep no raw sample', but the constructor used to np.concatenate(chunks[site]) unconditionally — with no chunks that's a KeyError. TwoStreamCIMeanPerComponent passes 0, so it crashed on every invocation before this PR and has never actually run.

[written by Claude]

"""A site's kept raw values, or empty when `n_batches_accum` kept none."""
return np.concatenate(chunks[site]) if site in chunks else np.empty(0, np.float32)


def accumulate_site_reductions(
slow_eval_step: SlowEvalStep,
model: DecomposedModel,
Expand Down Expand Up @@ -246,8 +252,8 @@ def accumulate_site_reductions(
density_counts=density[site],
ci_sums=sums[site],
n_positions=total_positions,
lower_sample=np.concatenate(lower_chunks[site]),
preactivations_sample=np.concatenate(preactivations_chunks[site]),
lower_sample=_raw_sample(lower_chunks, site),
preactivations_sample=_raw_sample(preactivations_chunks, site),
density_hist=hist.get(site),
)
for site in density
Expand Down Expand Up @@ -578,6 +584,89 @@ def _plot_ci_matrices(matrices: dict[str, np.ndarray], colormap: str, title_pref
return _render_figure(fig)


def _component_weight_magnitudes(components: ComponentStacks) -> dict[str, Array]:
return {
name: jnp.linalg.norm(sc.V.astype(jnp.float32), axis=0)
* jnp.linalg.norm(sc.U.astype(jnp.float32), axis=1)
for name, sc in components.sites_items()
}


def weight_magnitudes(components: ComponentStacks) -> dict[str, np.ndarray]:
"""Per-site `‖V_c‖·‖U_c‖` as host `(C,)` vectors. The norms reduce ON DEVICE, so only
C floats per site cross the boundary — never the V/U matrices themselves."""
return {
name: np.asarray(value) for name, value in _component_weight_magnitudes(components).items()
}


def mean_cis(reductions: dict[str, SiteReduction]) -> dict[str, np.ndarray]:
"""Per-site token-weighted mean CI."""
assert all(r.n_positions > 0 for r in reductions.values())
return {site: r.ci_sums / r.n_positions for site, r in reductions.items()}


def plot_weight_magnitudes(magnitudes: dict[str, np.ndarray]) -> bytes:
"""Per-site `‖V_c‖·‖U_c‖` in descending magnitude order, log y. x is a component's rank
within its site, NOT its component id."""
n_rows, n_cols = _grid_dims(len(magnitudes))
fig = Figure(figsize=(8 * n_cols, 3 * n_rows))
axs = fig.subplots(n_rows, n_cols, squeeze=False)
flat_axes = axs.T.ravel()
for ax in flat_axes[len(magnitudes) :]:
ax.set_visible(False)
for ax, (name, values) in zip(flat_axes, magnitudes.items(), strict=False):
ax.scatter(range(len(values)), np.sort(values)[::-1], marker="x", s=10)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorted descending, so x is a component's rank within its site, not its component id — you can no longer cross-reference a point against other per-component plots.

[written by Claude]

ax.set_yscale("log")
ax.set_xlabel("Component (descending ‖V‖·‖U‖)")
ax.set_ylabel("‖V‖·‖U‖")
ax.set_title(name, fontsize=10)
fig.tight_layout()
return _render_figure(fig)


def plot_mean_component_cis_two_streams(
target_mean_cis: dict[str, np.ndarray],
nontarget_mean_cis: dict[str, np.ndarray],
) -> tuple[bytes, bytes]:
"""Both streams' mean CI on one axis per site, ordered by descending TARGET mean.

The nontarget series takes the same permutation rather than its own, so a component's
two series line up vertically."""
assert target_mean_cis.keys() == nontarget_mean_cis.keys(), (
sorted(target_mean_cis),
sorted(nontarget_mean_cis),
)
n_rows, n_cols = _grid_dims(len(target_mean_cis))
ordered = {
name: (target[order], nontarget_mean_cis[name][order])
for name, target in target_mean_cis.items()
for order in [np.argsort(target)[::-1]]
}
images: list[bytes] = []
for log_y in (False, True):
fig = Figure(figsize=(8 * n_cols, 3 * n_rows))
axs = fig.subplots(n_rows, n_cols, squeeze=False)
flat_axes = axs.T.ravel()
for ax in flat_axes[len(ordered) :]:
ax.set_visible(False)
for ax, (name, (target, nontarget)) in zip(flat_axes, ordered.items(), strict=False):
x = np.arange(len(target))
if log_y:
ax.set_yscale("log")
ax.fill_between(x, target, step="mid", color="#1f77b4", label="target")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ax.bar here creates one matplotlib patch per component: measured ~120s per figure at C=1456 over 32 sites, and it renders two figures on the background thread that competes with the train loop for the GIL. fill_between(step='mid') draws the same picture in ~23s for both.

[written by Claude]

ax.fill_between(
x, nontarget, step="mid", color="#d62728", label="non-target", alpha=0.6
)
ax.set_xlabel("Component (sorted by target mean CI)")
ax.set_ylabel("mean CI")
ax.set_title(name, fontsize=10)
ax.legend(fontsize=7)
fig.tight_layout()
images.append(_render_figure(fig))
return images[0], images[1]


def plot_permuted_ci_heatmaps(
position_ci: dict[str, PositionCI], permutation: dict[str, "Literal['identity', 'dense']"]
) -> tuple[bytes, bytes]:
Expand Down
2 changes: 2 additions & 0 deletions param_decomp/core/tests/test_eval_averaging_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,14 @@ def step(
jnp.array([0, 0], dtype=jnp.uint32),
train_steps=0,
eval_steps=2,
stream="nontarget",
)
context = LMEvalContext(
state=_state_stub(), # pyright: ignore[reportArgumentType]
now_step=0,
pass_index=0,
batches=(jnp.asarray(1.0), jnp.asarray(3.0)),
target_batches=None,
)
assert operation.run(context)["eval/loss/probe/hidden_acts_reconstruction"] == 2.0

Expand Down
22 changes: 16 additions & 6 deletions param_decomp/core/train.py
Original file line number Diff line number Diff line change
Expand Up @@ -1185,14 +1185,18 @@ def make_targeted_train_step[PreparedT](
for term in objective.target.recon
if term.hidden_acts_reconstruction is not None
},
"nontarget/impmin": objective.nontarget.impmin_coeff,
**{f"nontarget/{term.name}": term.coeff for term in nt_terms},
}
if objective.target.imp.cfg.frequency is not None:
coeff_schedules[f"{objective.target.imp.name}/frequency"] = (
objective.target.imp.cfg.frequency.coeff
)

imp_name = objective.target.imp.name
nontarget_coeff_schedules: dict[str, LossCoeff] = {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split out so the non-target coefficients can take the nontarget_data/ prefix at the one call site that needs it, leaving _scheduled_coeff_metrics exactly as upstream has it (the plain step's call is unchanged).

[written by Claude]

imp_name: objective.nontarget.impmin_coeff,
**{term.name: term.coeff for term in nt_terms},
}

def nontarget_draw_loss(
model: DecomposedModel[PreparedT],
prepared_weights: PreparedT,
Expand Down Expand Up @@ -1360,8 +1364,8 @@ def loss_fn(
nt_imp_lp, nt_imp_freq = imp_min_terms(nt_ci.upper, atoms.imp_min, imp_min_param)
nt_total = nt_imp_coeff * nt_imp_lp + freq_coeff * nt_imp_freq
nt_aux = {
f"loss/nontarget/{atoms.imp_loss_key}": nt_imp_lp,
"loss/nontarget/freq": nt_imp_freq,
f"nontarget_data/loss/{imp_name}": nt_imp_lp,
"nontarget_data/loss/FrequencyMinimalityLoss": nt_imp_freq,
}
nt_breakdowns = atoms.grid_losses(
nt_terms,
Expand All @@ -1374,8 +1378,8 @@ def loss_fn(
nt_terms, nt_recon_coeffs, nt_breakdowns, strict=True
):
nt_total = nt_total + coeff * breakdown.total
nt_aux[f"loss/nontarget/{term.name}"] = breakdown.total
nt_aux["loss/nontarget/total"] = nt_total
nt_aux[f"nontarget_data/loss/{term.name}"] = breakdown.total
nt_aux["nontarget_data/loss/total"] = nt_total
total_loss = total_loss + nt_total
reported_total = reported_total + nt_total
return total_loss, (reported_total, imp_lp, imp_freq, term_breakdowns, nt_aux)
Expand Down Expand Up @@ -1446,6 +1450,12 @@ def loss_fn(
| nt_aux
| wd_metrics
| _scheduled_coeff_metrics(step_f32, atoms.total_steps, coeff_schedules)
| {
f"nontarget_data/{key}": value
for key, value in _scheduled_coeff_metrics(
step_f32, atoms.total_steps, nontarget_coeff_schedules
).items()
}
)
return new_state, metrics

Expand Down
8 changes: 8 additions & 0 deletions param_decomp/experiments/eval_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
CI_L0Config,
CIHiddenActsReconLossConfig,
CIHistogramsConfig,
CIMaskedReconLossConfig,
CIMeanPerComponentConfig,
ComponentActivationDensityConfig,
HiddenActsReconstructionMixin,
Expand All @@ -22,7 +23,9 @@
PermutedCIPlotsConfig,
PGDReconLossConfig,
StochasticHiddenActsReconLossConfig,
UnmaskedNoDeltaReconLossConfig,
UVPlotsConfig,
WeightMagnitudeConfig,
WellTemperednessConfig,
)
from param_decomp.core.eval_schedule import EvalSchedule, Every, FirstThenEvery
Expand All @@ -31,6 +34,7 @@
CEandKLLossesConfig,
CIMaskedAttnPatternsReconLossConfig,
StochasticAttnPatternsReconLossConfig,
TwoStreamCIMeanPerComponentConfig,
)

AnyEvalMetricConfig = Annotated[
Expand All @@ -40,14 +44,18 @@
| CIHistogramsConfig
| CI_L0Config
| CIMaskedAttnPatternsReconLossConfig
| CIMaskedReconLossConfig
| CIMeanPerComponentConfig
| ComponentActivationDensityConfig
| IdentityCIErrorConfig
| PermutedCIPlotsConfig
| PGDReconLossConfig
| StochasticAttnPatternsReconLossConfig
| StochasticHiddenActsReconLossConfig
| TwoStreamCIMeanPerComponentConfig
| UnmaskedNoDeltaReconLossConfig
| UVPlotsConfig
| WeightMagnitudeConfig
| WellTemperednessConfig,
Discriminator("type"),
]
Expand Down
Loading