API reference
Dynamics and loops
What does a reward model look like over a training run rather than at a single snapshot? Two subsystems answer that. reward_lens.dynamics reads a chain of checkpoints, and reward_lens.loops rides inside optimization: best-of-N accounting, susceptibility to tilt, and a recorder that watches rollouts for the moment a hack takes hold. The analysis paths are torch-free; the parts that need a live trainer say so.
Dynamics across checkpoints
A CheckpointSequence is an ordered chain of reward-model states you can measure the same way at each step. bias_entry_curve traces the effect size of a bias across training and reports the step where it first crosses threshold, the moment it entered. faithfulness_rho_trajectory follows the attribution-versus-patching agreement across training, the same rho that runs negative on the flagship models, and asks whether the anti-correlation is something training develops.
reward_lens.dynamics.checkpoints.CheckpointSequence
CheckpointSequence(checkpoints: list[Checkpoint]) An ordered, fingerprint-chained sequence of checkpoints over training time (DESIGN 2.12).
Build one with CheckpointSequence.build from (step, ModelFP, loader) triples; the constructor
computes the hash chain so the sequence is self-verifying from the moment it exists. Iterate it to
sweep an Observable or index across training time (sweep.sweep_over_checkpoints); the sequence’s
signature is what keys a resumable sweep to this exact chain.
Verification comes in two depths. verify_chain recomputes the links and is pure and instant: it
catches an edited fingerprint, a reordered or dropped checkpoint, or a broken link. verify_fingerprints
additionally loads each model and recomputes runtime.fingerprint, catching weights swapped under
an unchanged record. verify runs the chain check and, when deep=True, the fingerprint check,
raising ProvenanceError on the first failure so a study cannot sweep a chain it has not trusted.
Properties
-
steps : list[int]: The training steps in order (the developmental covariate). -
head_link : str: The final chain link, which transitively commits to every checkpoint’s identity.
Methods
build(
triples: list[tuple[int, ModelFP, CheckpointLoader]],
*,
meta: dict[int, dict[str, Any]] | None = None
) -> CheckpointSequence
src
(step, model_fp, loader) triples (DESIGN 2.2.5).
The triples are sorted by step and linked in order, so the caller may pass them in any order.
meta optionally attaches per-step metadata (a local path, an HF revision id, an epoch
fraction) that rides along on each Checkpoint. Steps must be distinct; a duplicate step is a
ProvenanceError because it makes the trajectory covariate ambiguous.
signature() -> str
src
verify_chain() -> ChainVerification
src
Pure and instant: for each checkpoint it recomputes link from (step, model_fp, prev)
and checks it matches the recorded link, that the recorded prev_link matches the running
previous link, and that the steps strictly increase. Any edit to a recorded fingerprint or
step, any reordering, or any dropped checkpoint surfaces here as the first bad step.
verify_fingerprints() -> ChainVerification
src
This is the deep check the chain cannot do on its own: it materializes each signal through its
loader and recomputes runtime.fingerprint, so a weight file swapped under an unchanged record
is caught even though the recorded metadata is internally consistent. It loads models, so it is
torch-gated in effect (the loaders build models) and is the expensive verification; a study
runs it once before trusting a chain, not on every sweep.
verify(*, deep: bool = False) -> ChainVerification
src
deep, the fingerprints), raising on the first failure.
Returns the passing ChainVerification so a caller may log it. Raises ProvenanceError with
the localized reason when either check fails, which is the guard the sweep relies on: a chain
that does not verify is never swept, so a developmental result is never read off an untrusted
run (DESIGN 2.2.5, RK9).
tampered(
index: int,
*,
model_fp: ModelFP | None = None,
loader: CheckpointLoader | None = None,
reseal: bool = False
) -> CheckpointSequence
src
With reseal=False (the default) the links are left untouched, so replacing a recorded
model_fp models an edited manifest and verify_chain will reject it; replacing only the
loader (keeping model_fp) models swapped weights that pass the chain but fail
verify_fingerprints. With reseal=True the chain is rebuilt around the change, which is
what an honest re-release would do (and then verification passes). This exists for tests and
audit drills; production sequences are built by build.
reward_lens.dynamics.curves.bias_entry_curve
bias_entry_curve(
sequence: CheckpointSequence,
probes: list[Probe],
view: Any,
*,
readout: str = 'reward',
store: EvidenceStore | None = None,
entry_threshold: float = 0.5,
resume: bool = True
) -> BiasEntryCurves The per-probe effect-size-versus-training-step curve, the bias-entry order (DESIGN 2.12).
Sweeps the reward score across the checkpoint sequence (cached and resumable through sweep), then
for each probe computes the signed effect size of the bias on the reward at every checkpoint. A
bias that is uncorrelated with the reward early and strongly loaded late traces a rising curve; the
step at which it first crosses entry_threshold is where it entered. On the planted synthetic
sequence, whose reward loading onto the probe grows by construction, the curve is monotone rising,
which is the calibration that the estimator faithfully tracks a known developmental signal before
it is trusted on a real run (DESIGN 2.10).
reward_lens.dynamics.curves.faithfulness_rho_trajectory
faithfulness_rho_trajectory(
score_pairs: list[tuple[int, np.ndarray, np.ndarray]],
*,
n_resamples: int = 2000,
seed: int = 0
) -> FaithfulnessRhoTrajectory The E04 attribution-vs-patching rho as a function of training step (DESIGN 2.12, 4.4 M9).
score_pairs is one (step, attribution_scores, patching_scores) triple per checkpoint, where
the two arrays are the per-component attribution and patching effects the battery produces. For each
checkpoint this computes the Spearman rho with a bootstrap CI (stats.spearman_with_ci) and returns
the trajectory. Whether the anti-correlation is developmental is read from the trajectory as a whole:
the reward is developmental when the late portion of the run is anti-correlated and meaningfully more
anti-correlated than the early portion (an anti-correlation that emerges from near zero, or one that
is present early and deepens). The comparison uses the mean rho of the first and last thirds rather
than the two endpoint checkpoints, so a single noisy checkpoint cannot set the flag and a constant
anti-correlation present from the first checkpoint is not called developmental. The attribution and
patching arrays come from the battery on each checkpoint; passing them in keeps this CPU-provable and
decoupled from the concurrently-built battery (DESIGN 2.9, 2.12).
Best-of-N
bon_kl is the exact KL cost of best-of- sampling, , so you can price optimization pressure in nats before you spend it. bon_ladder sweeps a range of and reports how the reward and the KL climb together. See best-of-N.
reward_lens.loops.bon.bon_kl
bon_kl(n: int | Sequence[int] | np.ndarray) -> np.ndarray The exact KL divergence of the best-of-n policy from the base policy, in nats.
KL(bo_n || base) = log(n) - (n - 1) / n (Beirami et al. 2401.01879). Exact in the
continuous-reward, no-ties limit, and a function of n alone. bon_kl(1) == 0 because
best-of-1 is the base policy. This is the identity the acceptance test reproduces.
Accepts a scalar or an array of n and returns a float array so a whole ladder is one call.
reward_lens.loops.bon.bon_ladder
bon_ladder(
scores_per_prompt: Sequence[Sequence[float]] | np.ndarray,
ns: Sequence[int] = DEFAULT_NS,
*,
subject: SubjectRef | None = None,
parents: Sequence['EvidenceID'] = ()
) -> Evidence[BoNLadder] Compute the best-of-n ladder and the exact KL frontier from scored base-policy samples.
scores_per_prompt is a per-prompt bank of reward-model scores: either a 2-D array
(n_prompts, m) or a ragged list of per-prompt arrays. For each n in ns the expected
best-of-n reward is computed per prompt with the plug-in estimator and averaged across prompts,
and the exact KL(bo_n || base) is attached. The result previews the optimization frontier
with no RL (DESIGN 2.13).
Returns Evidence[BoNLadder]. Gauge is INVARIANT: the KL is a real divergence in nats and
the reward is in the model’s own score units, both gauge-free (a raw reward-model score is
INVARIANT in this kernel, DESIGN 2.3.3). subject names the signal/dataset when the caller
has them; parents links the score Evidence this consumed so the store stays a DAG.
DEFAULT_NS is the standard ladder of the sweep walks, from 1 up to 10000.
reward_lens.loops.bon.DEFAULT_NS
DEFAULT_NS: tuple[int, ...] = (1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 10000) No docstring. See the source for detail.
Tilt and susceptibility
susceptibility is the first-order response of a feature to reward tilt, , the quantity that flags which features a little optimization pressure will amplify. flag_hack_modes names the predicted hack modes: the features the proxy loves while the gold reward does not. critical_lambda_from_tail estimates the critical pressure from the reward’s right tail, and tilt_sweep emulates the tilted family across a grid of by importance sampling, with the effective-sample-size guards that refuse when the reweighting grows too thin. The narrated version is tilt and susceptibility.
reward_lens.loops.tilt.susceptibility
susceptibility(
scores: Sequence[float] | np.ndarray,
features: np.ndarray,
feature_names: Sequence[str] | None = None,
*,
subject: SubjectRef | None = None,
parents: Sequence['EvidenceID'] = ()
) -> Evidence['SusceptibilitySpectrum'] The susceptibility spectrum chi_i = Cov_0(f_i, r) from base-policy samples (Appendix A12).
scores is the base-policy reward per sample (shape (N,)); features is the per-sample
scalar activation of each feature (shape (N, k)). Returns chi_i = Cov_0(f_i, r), the
predicted initial drift of feature i under optimization, computed as a population covariance
mean(f_i r) - mean(f_i) mean(r) (the covariance is an expectation under pi_0, so the
population form, ddof = 0, is the estimator of the theory object). The f = r diagonal
Var_0(r) is reported as teacher_variance (Appendix A3).
Gauge is INVARIANT: a covariance of two scalar readouts does not depend on the activation basis (DESIGN 2.17 step 6). Trust is EXPLORATORY until a planted-chi organism scorecard calibrates it (gate 1); that calibration is a study, not this function’s job.
reward_lens.loops.tilt.flag_hack_modes
flag_hack_modes(
spectrum: SusceptibilitySpectrum,
gold_covariance: Sequence[float] | np.ndarray
) -> list[str] Names of the predicted hack modes: chi_i > 0 while Cov_0(f_i, gold) <= 0 (A12).
gold_covariance[i] = Cov_0(f_i, gold) for a reference/gold signal. A feature the proxy loves
(positive susceptibility) but the gold objective does not is exactly the direction optimization
exploits, which is what the forecasting science (S12) pre-registers.
reward_lens.loops.tilt.critical_lambda_from_tail
critical_lambda_from_tail(
scores: Sequence[float] | np.ndarray,
tail_quantile: float = 0.9
) -> float Estimate the critical pressure lambda_c from the reward’s right-tail scale (A4/A5).
Fits an exponential right tail by peaks-over-threshold: the scale tau is the mean excess of
the scores above their tail_quantile quantile, and lambda_c = 1 / tau (Appendix A5, an
exponential tail with scale tau has critical pressure 1 / tau). This is the cheap
scalar the tilt guard needs; the full Hill / peaks-over-threshold tail index lives in the tail
index observable (Appendix A4), which this does not replace. Returns inf when the excess is
degenerate (a bounded tail imposes no ceiling on lambda).
reward_lens.loops.tilt.tilt_sweep
tilt_sweep(
scores: Sequence[float] | np.ndarray,
features: np.ndarray,
lambdas: Sequence[float],
lambda_c: float,
*,
feature_names: Sequence[str] | None = None,
min_ess_frac: float = 0.05,
subject: SubjectRef | None = None,
parents: Sequence['EvidenceID'] = ()
) -> Evidence['TiltPrediction'] Emulate the tilted family across a lambda grid by SNIS, with the validity guards.
For each lambda in lambdas the tilted feature means E_lambda[f_i] and reward mean
E_lambda[r] are estimated by reweighting the base-policy bank, with no gradient updates. The
susceptibility chi_i = Cov_0(f_i, r) (the lambda -> 0 slope) is attached so the initial
drift the sweep traces can be checked against the closed-form prediction.
Refuses, via ESSGuardError, when any requested |lambda| exceeds lambda_c / 2 (past
that the tilt no longer emulates practical optimization, DESIGN 2.13; past lambda_c there is
no tilted optimum for a heavy tail, Appendix A4), or when the SNIS effective sample size at some
valid lambda falls below min_ess_frac of the bank. Returning a confident number from a
handful of dominating samples is the failure this guard exists to prevent.
Gauge is INVARIANT (tilted means of scalar readouts and their base covariance are gauge-free).
The rollout recorder
RolloutRecorder logs a rollout stream and can raise an OnsetAlarm when a hack signature appears, sometimes ahead of the reward curve noticing. cusum_changepoint is the CUSUM changepoint test underneath the alarm, and synthetic_hack_rollout builds a CPU rollout that drifts along a planted hack direction to exercise the detector. See the rollout recorder.
reward_lens.loops.recorder.RolloutRecorder
RolloutRecorder(
feature_bank: FeatureBank,
reward_direction: Sequence[float] | np.ndarray,
baseline_activations: np.ndarray,
*,
effective_subspace: np.ndarray | None = None,
ridge: float = 0.001,
mahalanobis_quantile: float = 0.975
) Monitor a rollout in reward-feature space, step by step (DESIGN 2.13, science S13).
Construct with the feature bank whose doses to track, the reward direction w_r the policy is
paid to excite, a batch of baseline (step-0) activations that fixes the reference mean and
covariance, and optionally an explicit reward-defining subspace (default: the span of w_r).
Call observe once per rollout step with that step’s activation batch and rewards, then
report to get the DriftReport, or evidence to get it wrapped as Evidence.
Everything is CPU-cheap and pure-numpy. The recorder holds no model; it consumes activations a caller extracts, which is what lets it run in shadow mode on production serving with no behavior change (DESIGN 2.13) and what makes the synthetic organism a faithful stand-in for the GPU rollout.
Methods
observe(
activations: np.ndarray,
proxy_reward: float | Sequence[float] | None = None,
gold_reward: float | Sequence[float] | None = None
) -> None
src
activations is (n_samples, d) for the step. proxy_reward is the RM’s score for
the step (a scalar or per-sample, averaged); gold_reward is the ground-truth reward
where a study has one (the synthetic organism, or an offline eval). If gold is omitted on
any step, the report simply carries no gold onset and no lead time.
report(*, alpha: float = 0.05, n_perm: int = 1000, seed: int = 0) -> DriftReport
src
DriftReport: dose changepoints, the named direction, and the lead time.
Each feature’s dose trajectory gets a CUSUM changepoint; the exploited direction is the
feature with the largest changepoint magnitude among those significant at alpha. The
gold reward (when present) gets its own changepoint, and the lead time is the gap between the
exploited feature’s onset and the gold divergence. An OnsetAlarm is emitted for the
exploited feature and for the gold divergence.
evidence(
*,
subject: SubjectRef | None = None,
parents: Sequence['EvidenceID'] = (),
alpha: float = 0.05,
n_perm: int = 1000,
seed: int = 0
) -> Evidence[DriftReport]
src
DriftReport wrapped as Evidence (DESIGN 2.13).
Gauge is RAW_ONLY: concept doses and drift magnitudes are projections in one model’s activation basis, so they are raw coordinates, honest within a rollout but not comparable across models without a Frame (DESIGN 2.7.1, gate 2). The lead time and outlier rate are frame-free, but the payload as a whole carries raw-coordinate arrays, so RAW_ONLY is the conservative correct label.
reward_lens.loops.recorder.OnsetAlarm
@dataclass
A changepoint-based onset: which signal moved, when, and how significantly (DESIGN 2.13).
Attributes
| Name | Type | Default |
|---|---|---|
signal | str | |
kind | str | |
step | int | |
statistic | float | |
p_value | float |
reward_lens.loops.recorder.cusum_changepoint
cusum_changepoint(
series: Sequence[float] | np.ndarray,
n_perm: int = 1000,
seed: int = 0
) -> Changepoint Detect a single mean-shift changepoint by CUSUM with a permutation p-value (Taylor’s method).
Builds the cumulative sum of deviations from the mean, S_j = sum_{t<=j}(x_t - xbar) with
S_0 = S_T = 0; the changepoint is the index where |S| is largest, and the magnitude
max(S) - min(S) is the test statistic. Significance is a permutation null: shuffle the
series n_perm times, recompute the magnitude, and report the fraction at least as large
(with the (count + 1) / (n_perm + 1) correction). This is the dependency-light stand-in for
the BOCPD detector DESIGN 2.11 puts in stats/changepoint; the return contract is the same.
A flat or trendless series returns a non-significant changepoint (large p-value). The reported
index is the split point in [0, T]: samples before it are one regime, samples after another.
reward_lens.loops.recorder.synthetic_hack_rollout
synthetic_hack_rollout(
*,
d: int = 16,
n_features: int = 6,
n_samples: int = 64,
n_baseline: int = 256,
steps: int = 40,
dose_onset: int = 6,
gold_tolerance: float = 1.2,
drift_rate: float = 0.14,
gold_slope: float = 1.6,
noise: float = 1.0,
seed: int = 0
) -> SyntheticRollout Generate a CPU rollout that drifts along a planted hack direction (DESIGN 2.13, science S13).
The policy is paid to excite the reward direction w_r, which here is the hack feature’s
direction (feature 0). From dose_onset on, the activation mean drifts along that direction at
drift_rate per step, so the hack feature’s dose, the proxy reward, and the crystallization
along w_r all climb together. The gold reward is flat until the accumulated hack dose crosses
gold_tolerance, then falls at gold_slope: the anti-correlation with gold only bites once
enough hack has accumulated (the chi_i > 0, Cov(f_i, gold) <= 0 signature of Appendix
A12). That construction is what makes the feature-space onset precede the gold divergence, which
is the lead time the recorder must recover. The remaining features are distractors with no
systematic drift.
Deterministic given seed. This is the synthetic organism the recorder’s acceptance test runs
on; the real RL rollout is GPU-gated and enters through the same RolloutRecorder.observe API.
Training-loop integration
make_reward_fn wraps a signal as a plain reward function, and GeometryLogger logs the reward model’s own geometry every few steps on fixed probes. Both are framework-agnostic and run now; the TRL, OpenRLHF, and veRL bindings raise until their framework is installed. Import them from reward_lens.loops.integrations, and see training-loop hooks.
reward_lens.loops.integrations.base.make_reward_fn
make_reward_fn(
signal: Any,
readout: str = 'reward',
*,
batch_size: int | None = None
) -> RewardFn Wrap a RewardSignal as the reward function a training loop calls (DESIGN 2.13).
Returns reward_fn(prompts, responses) -> list[float]: it pairs each prompt with its response,
scores them through signal.score under readout, and returns plain floats, which is the
contract PPO/GRPO reward hooks expect. This is the real, framework-agnostic reward path; the
framework files add only the callback/worker plumbing around it.
reward_lens.loops.integrations.base.GeometryLogger
GeometryLogger(probe: GeometryProbe, readout: str = 'reward') Log the RM’s geometry every k steps on a fixed probe (DESIGN 2.13).
The framework-agnostic core the three integration callbacks wrap: hold a probe, and on each
training step call maybe_log(signal, step), which returns a ProbeLog on the logging steps
(every probe.k) and None otherwise, appending each log to logs. This is the piece the
tiny-model test exercises directly, so the geometry logging is proven without any training
framework installed; the callbacks add only the framework’s step hook around it.
Methods
maybe_log(signal: Any, step: int) -> ProbeLog | None
src