Coming from 1.0
Did the rewrite break your 1.0 code? No. Every v1 name still imports from the top level, and the pure layers stay torch-free, so nothing you wrote stopped working the day 2.0 landed. The migration is not a rewrite under duress. It is moving one call at a time to the version that hands back a number you can trust instead of a bare float.
Your 1.0 code still runs
import reward_lens pulls nothing heavier than numpy. Touch a v1 name and the module that needs torch is imported on demand, which is the correct moment for it to load.
import reward_lens
print(reward_lens.__version__) # 2.0.1
from reward_lens import RewardModel, RewardLens, ComponentAttribution # still here
These names resolve through a lazy accessor to reward_lens.legacy, the one sanctioned home for the 1.0 surface. The rest of the classic toolkit returns the same way: ActivationPatcher, PathPatcher, DistortionAnalyzer, DivergenceAwarePatching, MisalignmentCascadeDetector, RewardConflictAnalyzer, ConceptExtractor, and quick_concept_analysis. The full list is in the 1.0 API reference.
Three tools never had a top-level export in 1.0, and still do not. Import them from their own module:
from reward_lens.hacking import HackingDetector
from reward_lens.comparison import ModelComparator
from reward_lens.sae import TopKSAE, SAETrainer
Both blocks import cleanly on CPU. They pull torch because they touch models, which is the only reason the top-level import stays lazy in the first place.
Where each primitive moved
The left column keeps working; the right column is where to write new code.
| 1.0 primitive | What it did | 2.0 home |
|---|---|---|
RewardLens | reward projected across depth | LensCrystallization |
ComponentAttribution | per-component reward ledger | DirectLinearAttribution |
ActivationPatcher, PathPatcher | causal patching | PatchGrid, PathEffect, and the intervention algebra |
ConceptExtractor, quick_concept_analysis | concept directions and steering | concepts with ConceptDoseResponse |
RewardConflictAnalyzer | inter-objective conflict | ConflictMatrix |
HackingDetector (reward_lens.hacking) | bias and hacking scan | still reward_lens.hacking; the ported pieces are the bias battery and the index library |
ModelComparator (reward_lens.comparison) | cross-model comparison | still reward_lens.comparison; the framed 2.0 way is gauge and frames |
TopKSAE, SAETrainer (reward_lens.sae) | SAE features on the reward | still reward_lens.sae; see feature-reward alignment |
There are two tiers here. The classic analysis primitives (model, lens, attribution, patching, conflict, concepts) resolve from the top level and from reward_lens.legacy. The three scanners in the bottom rows do not. They keep their 1.0 implementation, they still return their old report objects rather than Evidence, and they have not yet been reworked behind the protocols. If you depend on the exact output of HackingDetector, ModelComparator, or the SAE trainer, keep importing it from its module. For new work, reach for the instrument in the right column.
What moving buys you
The 1.0 library was a bag of primitives that returned numpy arrays and floats. Running them at scale is what motivated the rebuild, so the thing you gain is not more tools. It is a receipt on every number.
- Evidence, not a bare float. A 2.0 measurement returns an
Evidenceobject carrying the value, its uncertainty with an effective sample size that counts unique content rather than cloned rows, a gauge status, a calibration reference, provenance back to the inputs, and a trust level. See a measurement you can trust. - Trust is computed, not asserted. You never set the trust level. Three gates do: calibration, gauge, and registration. An uncalibrated instrument caps at exploratory no matter how confident the number looks.
- Calibration against organisms. An instrument earns a scorecard by recovering structure that was planted by construction, so its number on a real model cites a case with known ground truth. See calibration and organisms.
- A frame for cross-model comparison. Two reward directions in raw coordinates are not comparable, and 2.0 refuses the comparison rather than handing back a coordinate artifact. See gauge and frames.
- More than a classifier. 1.0 spoke one reward dialect. The
RewardSignalprotocol now covers eight substrates (classifier, generative judge, process, implicit DPO, rubric, trajectory, dense, ensemble), and every instrument attaches to all of them unchanged. See models and signals.
The honesty thread runs straight through this. Rank a model’s components by how much attribution assigns them, rank them again by how much causal patching says they carry, and on Skywork-Reward-Llama-3.1-8B-v0.2 the two rankings correlate at Spearman . Negative. On ArmoRM the same comparison sits near zero at . The place a reward visibly accumulates is not always the place that causes it, and 1.0 had no way to flag that a cheap observational number was being quoted as a causal one. 2.0 does. See observational vs causal and interpreting results honestly.
The same analysis, before and after
A 1.0 script that traces a preference across depth and attributes it, next to its 2.0 equivalent. The shape is the same. The output is not.
# 1.0: loads an 8B reward model, returns bare arrays
from reward_lens import RewardModel, RewardLens, ComponentAttribution
model = RewardModel.from_pretrained("Skywork/Skywork-Reward-Llama-3.1-8B-v0.2")
lens = RewardLens(model)
result = lens.trace(prompt, chosen, rejected) # margin traced across layers
attr = ComponentAttribution(model)
comp = attr.attribute(prompt, chosen, rejected)
comp.differential_contributions # a numpy array, no provenance
The 2.0 version reads from a typed data view and runs each observable through the gated runner, so LensCrystallization and DirectLinearAttribution come back wrapped in Evidence.
from reward_lens.signals import load_signal
from reward_lens.measure import base as mb
from reward_lens.measure.battery import LensCrystallization, DirectLinearAttribution
from reward_lens.data.builtin.diagnostic_v3 import load_diagnostic_v3
from reward_lens.data.schema import DataView
signal = load_signal("Skywork/Skywork-Reward-Llama-3.1-8B-v0.2", allow_download=True)
view = DataView(list(load_diagnostic_v3()["helpfulness"].items)[:8])
ctx = mb.Context(signal=signal, view=view)
lens = mb.run(LensCrystallization(), ctx) # Evidence: the depth where the margin half-forms
attr = mb.run(DirectLinearAttribution(), ctx) # Evidence: value + uncertainty + gauge + trust + provenance
attr.value["differential"] # the same per-component array, now inside a receipt
On the canonical “why is the sky blue” pair, the committed 8B artifacts put the margin at and the preference crystallizing at layer 30 of 32. Those are measured results, not something this hardware reproduces, but the call that produces them is the one above. To run the 2.0 API right now, keep every line and swap in the tiny model, which is a real reward model that builds on CPU with no download:
from reward_lens.signals import from_tiny
from reward_lens.measure import base as mb
from reward_lens.measure.battery import DirectLinearAttribution
from reward_lens.data.builtin.diagnostic_v3 import load_diagnostic_v3
from reward_lens.data.schema import DataView
signal = from_tiny(seed=0)
view = DataView(list(load_diagnostic_v3()["helpfulness"].items)[:8])
attr = mb.run(DirectLinearAttribution(), mb.Context(signal=signal, view=view))
print(attr.trust) # EXPLORATORY
print(attr.gauge) # invariant
The trust level is EXPLORATORY because this observable has not yet earned a scorecard, not because anything is wrong with the number. That distinction is the whole point of the trust ladder. Signatures for both instruments are in the measure reference.
If you used per-head attribution
ComponentAttribution.attribute_heads had a real bug in 1.0. It sliced each attention head’s o_proj contribution incorrectly, so per-head attention attribution numbers from that version are wrong. This build fixes the slicing: each head’s contribution is projected through its own o_proj block. If you have per-head attribution figures from 1.0, recompute them.
For causal per-head effects rather than attribution, the 2.0 path is PatchGrid at head granularity.
Once your scripts run on 2.0, the next thing to read is getting started, which builds the same measurement from the epistemics layer up.