Python · 3208 bytes Raw Blame History
1 """Backend proxy used by ``NullAdapterProbe`` to drive per-kind calibration.
2
3 The proxy wraps a :class:`~dlm_sway.core.scoring.NullCalibratedBackend`
4 so that ``as_finetuned()`` actually yields a null-adapter view at a
5 fixed seed. From the proxied probe's perspective it sees a regular
6 ``DifferentialBackend``: ``as_base()`` is the real base and
7 ``as_finetuned()`` is a structural-noise (random-init LoRA) view.
8
9 Running a probe with this proxy substituted into the
10 :class:`~dlm_sway.probes.base.RunContext` produces "what does this
11 probe report when the fine-tune is just noise?" — exactly the
12 denominator each numeric probe needs to z-score itself.
13
14 The proxy only implements :class:`DifferentialBackend`; it does *not*
15 forward :class:`ScalableDifferentialBackend` (because adapter ablation
16 makes no sense on a null adapter) or :class:`NullCalibratedBackend`
17 (probes shouldn't recursively null-calibrate themselves). This is a
18 deliberate narrowing.
19 """
20
21 from __future__ import annotations
22
23 from collections.abc import Iterator
24 from contextlib import contextmanager
25
26 from dlm_sway.core.scoring import (
27 NullCalibratedBackend,
28 ScoringModel,
29 )
30
31
32 class NullCalibrationBackendProxy:
33 """Wraps a backend so ``as_finetuned()`` yields the null view.
34
35 Constructed once per calibration seed by
36 :class:`~dlm_sway.probes.null_adapter.NullAdapterProbe`. Each
37 seed produces an independent draw from the null distribution.
38
39 Attributes
40 ----------
41 inner:
42 The real backend whose null views we substitute.
43 seed:
44 Seed handed to ``inner.as_null_adapter(seed)`` on every
45 ``as_finetuned()`` entry. Reusing the proxy across multiple
46 ``as_finetuned()`` blocks therefore yields the *same* null
47 view (deterministic).
48 init_scale:
49 Forwarded to ``as_null_adapter(init_scale=…)``.
50 rank_scale:
51 Forwarded to ``as_null_adapter(rank_scale=…)``. Defaults to
52 1.0 for parity with pre-S10 behavior. ``NullAdapterProbe``
53 builds one proxy per ``rank_multipliers`` entry when calibrating
54 across ranks.
55 """
56
57 def __init__(
58 self,
59 inner: NullCalibratedBackend,
60 *,
61 seed: int,
62 init_scale: float = 0.02,
63 rank_scale: float = 1.0,
64 ) -> None:
65 self._inner = inner
66 self._seed = seed
67 self._init_scale = init_scale
68 self._rank_scale = rank_scale
69
70 @contextmanager
71 def as_base(self) -> Iterator[ScoringModel]:
72 """Forwarded straight to the inner backend's base view."""
73 with self._inner.as_base() as view:
74 yield view
75
76 @contextmanager
77 def as_finetuned(self) -> Iterator[ScoringModel]:
78 """The substitution: yield a null-adapter view, not the real ft.
79
80 A probe that calls ``as_finetuned()`` against the proxy is
81 *actually* asking "what does this metric look like when the
82 adapter is structural noise" — which is the calibration
83 question.
84 """
85 with self._inner.as_null_adapter(
86 self._seed, init_scale=self._init_scale, rank_scale=self._rank_scale
87 ) as view:
88 yield view
89
90
91 __all__ = ["NullCalibrationBackendProxy"]