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.

# class

CheckpointSequence

Source

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
Build a chained sequence from (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
A content id for the whole chain (the head link), used to key a resumable sweep to it.
verify_chain() -> ChainVerification
src
Recompute the hash chain and report the first inconsistency (DESIGN 2.2.5).

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
Load each checkpoint and recompute its fingerprint against the record (DESIGN 2.2.5).

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
Verify the chain (and, when 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
Return a copy with one checkpoint altered, modelling an attacker’s edit (DESIGN 2.2.5).

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.

# func

bias_entry_curve

Source

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).

# func

faithfulness_rho_trajectory

Source

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-nn sampling, KL(n)=logn(n1)/n\mathrm{KL}(n) = \log n - (n-1)/n, so you can price optimization pressure in nats before you spend it. bon_ladder sweeps a range of nn and reports how the reward and the KL climb together. See best-of-N.

# func

bon_kl

Source

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.

# func

bon_ladder

Source

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 nn the sweep walks, from 1 up to 10000.

# attr

DEFAULT_NS

Source

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, χi=Cov0(fi,r)\chi_i = \mathrm{Cov}_0(f_i, r), 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 λc\lambda_c from the reward’s right tail, and tilt_sweep emulates the tilted family across a grid of λ\lambda by importance sampling, with the effective-sample-size guards that refuse when the reweighting grows too thin. The narrated version is tilt and susceptibility.

# func

susceptibility

Source

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.

# func

flag_hack_modes

Source

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.

# func

critical_lambda_from_tail

Source

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).

# func

tilt_sweep

Source

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.

# class

RolloutRecorder

Source

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
Record one rollout step from its activation batch and rewards.

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
Assemble the 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
The 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.

# class

OnsetAlarm

Source

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
# func

cusum_changepoint

Source

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.

# func

synthetic_hack_rollout

Source

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.

# func

make_reward_fn

Source

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.

# class

GeometryLogger

Source

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