File size: 9,317 Bytes
92edcfa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
"""Quality gates that atomically commit or roll back a ternary transaction."""
from __future__ import annotations

import hashlib
from dataclasses import dataclass
from typing import Dict, Mapping

import torch

from .transaction import TransactionalTernaryMatrix
from .wal import HashChainWAL


@dataclass(frozen=True)
class GateDecision:
    passed: bool
    ratios: Dict[str, float]
    violations: Dict[str, float]


@dataclass(frozen=True)
class TransactionSizeDecision:
    """Auditable update of the next fraction of groups to attempt."""

    previous_fraction: float
    next_fraction: float
    reason: str
    worst_ratio: float
    gate_ratio: float
    normalized_headroom: float


class AdaptiveTransactionSizer:
    """Shrink risky ternary commits and cautiously regrow after safe streaks.

    Headroom is normalized by the gate's total allowance above one. This keeps
    the policy meaningful for strict cumulative gates such as 1.00194 as well
    as for wider diagnostic gates. A rollback always halves the atom; a pass
    close to the gate also halves it. Growth requires several roomy passes so
    that one unusually easy transaction cannot immediately undo the shrink.
    """

    def __init__(
        self,
        initial_fraction: float,
        *,
        minimum_fraction: float = 1 / 1024,
        maximum_fraction: float | None = None,
        shrink_factor: float = 0.5,
        grow_factor: float = 2.0,
        tight_headroom: float = 0.2,
        roomy_headroom: float = 0.75,
        grow_after: int = 2,
        roomy_passes: int = 0,
    ):
        maximum_fraction = initial_fraction if maximum_fraction is None else maximum_fraction
        if not 0 < minimum_fraction <= initial_fraction <= maximum_fraction <= 1:
            raise ValueError("fractions must satisfy 0 < minimum <= initial <= maximum <= 1")
        if not 0 < shrink_factor < 1:
            raise ValueError("shrink_factor must be in (0, 1)")
        if grow_factor <= 1:
            raise ValueError("grow_factor must be greater than 1")
        if not 0 <= tight_headroom < roomy_headroom <= 1:
            raise ValueError("headroom thresholds must satisfy 0 <= tight < roomy <= 1")
        if grow_after < 1:
            raise ValueError("grow_after must be positive")
        if not isinstance(roomy_passes, int) or not 0 <= roomy_passes < grow_after:
            raise ValueError("roomy_passes must satisfy 0 <= roomy_passes < grow_after")
        self.current_fraction = float(initial_fraction)
        self.minimum_fraction = float(minimum_fraction)
        self.maximum_fraction = float(maximum_fraction)
        self.shrink_factor = float(shrink_factor)
        self.grow_factor = float(grow_factor)
        self.tight_headroom = float(tight_headroom)
        self.roomy_headroom = float(roomy_headroom)
        self.grow_after = int(grow_after)
        self._roomy_passes = int(roomy_passes)

    @property
    def roomy_passes(self) -> int:
        """Number of consecutive roomy passes retained for the next decision."""

        return self._roomy_passes

    def observe(
        self, *, passed: bool, worst_ratio: float, gate_ratio: float
    ) -> TransactionSizeDecision:
        if gate_ratio <= 1:
            raise ValueError("gate_ratio must be greater than 1")
        if worst_ratio <= 0:
            raise ValueError("worst_ratio must be positive")
        previous = self.current_fraction
        headroom = (gate_ratio - worst_ratio) / (gate_ratio - 1)
        normalized_headroom = min(1.0, max(0.0, headroom))

        if not passed or worst_ratio > gate_ratio:
            self._roomy_passes = 0
            next_fraction = max(self.minimum_fraction, previous * self.shrink_factor)
            reason = "rollback_shrink"
        elif normalized_headroom <= self.tight_headroom:
            self._roomy_passes = 0
            next_fraction = max(self.minimum_fraction, previous * self.shrink_factor)
            reason = "tight_gate_shrink"
        elif normalized_headroom >= self.roomy_headroom:
            self._roomy_passes += 1
            if self._roomy_passes >= self.grow_after:
                next_fraction = min(self.maximum_fraction, previous * self.grow_factor)
                self._roomy_passes = 0
                reason = "safe_streak_grow"
            else:
                next_fraction = previous
                reason = "safe_streak_hold"
        else:
            self._roomy_passes = 0
            next_fraction = previous
            reason = "middle_headroom_hold"

        self.current_fraction = next_fraction
        return TransactionSizeDecision(
            previous_fraction=previous,
            next_fraction=next_fraction,
            reason=reason,
            worst_ratio=float(worst_ratio),
            gate_ratio=float(gate_ratio),
            normalized_headroom=normalized_headroom,
        )

    def group_count(self, total_groups: int) -> int:
        if total_groups < 1:
            raise ValueError("total_groups must be positive")
        return max(1, round(total_groups * self.current_fraction))


class RatioGate:
    """Accept when every candidate loss is within its baseline ratio limit."""

    def __init__(self, limits: float | Mapping[str, float] = 1.02):
        self.limits = float(limits) if isinstance(limits, (float, int)) else dict(limits)

    def evaluate(
        self, baseline: Mapping[str, float], candidate: Mapping[str, float]
    ) -> GateDecision:
        if baseline.keys() != candidate.keys() or not baseline:
            raise ValueError("baseline and candidate must have identical non-empty domains")
        ratios: Dict[str, float] = {}
        violations: Dict[str, float] = {}
        for domain, before in baseline.items():
            if before <= 0:
                raise ValueError(f"baseline for {domain!r} must be positive")
            ratio = float(candidate[domain]) / float(before)
            limit = self.limits if isinstance(self.limits, float) else self.limits[domain]
            ratios[domain] = ratio
            if ratio > limit:
                violations[domain] = ratio - limit
        return GateDecision(not violations, ratios, violations)


class TransactionController:
    """Connect an in-memory matrix transaction to the durable WAL v2."""

    def __init__(self, wal: HashChainWAL, matrix_name: str, gate: RatioGate | None = None):
        self.wal = wal
        self.matrix_name = matrix_name
        self.gate = gate or RatioGate()

    def begin(
        self,
        matrix: TransactionalTernaryMatrix,
        mask: torch.Tensor,
        *,
        selector: str,
        selected_damage_mean: float | None = None,
    ) -> str:
        transaction_id = matrix.begin(mask)
        mask_bytes = mask.detach().to("cpu", dtype=torch.uint8).numpy().tobytes()
        self.wal.append(
            "begin",
            transaction_id,
            {
                "matrix": self.matrix_name,
                "selector": selector,
                "groups": int(mask.sum().item()),
                "total_groups": mask.numel(),
                "mask_sha256": hashlib.sha256(mask_bytes).hexdigest(),
                "selected_damage_mean": selected_damage_mean,
            },
        )
        return transaction_id

    def progress(
        self,
        matrix: TransactionalTernaryMatrix,
        *,
        step: int,
        pressure: float,
        temperature: float,
        loss: float | None = None,
    ) -> None:
        if not matrix.in_transaction:
            raise RuntimeError("no active transaction")
        matrix.set_candidate_state(pressure, temperature)
        self.wal.append(
            "progress",
            matrix.transaction_id,
            {
                "matrix": self.matrix_name,
                "step": int(step),
                "pressure": float(pressure),
                "temperature": float(temperature),
                "loss": loss,
                "code_churn": matrix.current_code_churn(),
            },
        )

    def decide(
        self,
        matrix: TransactionalTernaryMatrix,
        *,
        baseline: Mapping[str, float],
        candidate: Mapping[str, float],
    ) -> GateDecision:
        if not matrix.in_transaction:
            raise RuntimeError("no active transaction")
        transaction_id = matrix.transaction_id
        decision = self.gate.evaluate(baseline, candidate)
        kind = "commit" if decision.passed else "rollback"
        gate_payload = {
            "matrix": self.matrix_name,
            "baseline": dict(baseline),
            "candidate": dict(candidate),
            "ratios": decision.ratios,
            "violations": decision.violations,
        }
        # This record is durable before the in-memory state transition. If the
        # process stops here, recovery can distinguish an intent from an
        # applied commit/rollback and restart from the last model checkpoint.
        self.wal.append(f"{kind}_intent", transaction_id, gate_payload)
        if decision.passed:
            result = matrix.commit()
        else:
            result = matrix.rollback()
        self.wal.append(
            kind,
            transaction_id,
            {
                **gate_payload,
                **result,
            },
        )
        return decision