File size: 11,127 Bytes
7ccb33d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
"""Core Sherry 3:4 ternary fake quantization and Arenas scheduling."""

from __future__ import annotations

import math
from typing import Any

import torch
import torch.nn as nn
import torch.nn.functional as F


def _validate_quant_shape(weight: torch.Tensor, group_size: int) -> None:
    """Validate constraints that the original implementation left to reshapes."""

    if weight.ndim != 2:
        raise ValueError(f"Sherry only supports 2-D linear weights, got {weight.ndim}-D")
    if not weight.is_floating_point():
        raise ValueError("Sherry fake quantization requires a floating-point weight")
    if not isinstance(group_size, int) or group_size <= 0:
        raise ValueError("group_size must be a positive integer")
    if group_size % 4:
        raise ValueError("group_size must be divisible by the 3:4 block width (4)")
    if weight.shape[1] % group_size:
        raise ValueError(
            f"in_features ({weight.shape[1]}) must be divisible by group_size ({group_size}); "
            "groups may not cross output-channel rows"
        )


class _NMQuantizeSTE(torch.autograd.Function):
    """Autograd implementation whose backward is the unmodified identity STE."""

    @staticmethod
    def forward(ctx: Any, weight: torch.Tensor, group_size: int) -> torch.Tensor:
        del ctx
        _validate_quant_shape(weight, group_size)

        original_shape = weight.shape
        blocks = weight.reshape(original_shape[0], original_shape[1] // 4, 4)
        survivor_indices = blocks.abs().topk(k=3, dim=-1).indices
        mask = torch.zeros_like(blocks, dtype=torch.bool)
        mask.scatter_(-1, survivor_indices, True)
        sparse = blocks.masked_fill(~mask, 0).reshape(-1, group_size)

        # The original accumulates in the latent dtype (often bf16).  Sherry notes
        # identify that as a precision bug, so accumulation is deliberately fp32.
        scale = sparse.float().abs().sum(dim=-1, keepdim=True) / (group_size * 3 / 4)
        signs = sparse.sign().float()
        quantized = (signs * scale).reshape(original_shape)
        return quantized.to(dtype=weight.dtype)

    @staticmethod
    def backward(ctx: Any, grad_output: torch.Tensor) -> tuple[torch.Tensor, None]:
        del ctx
        # Unlike mask-aware pruning estimators, the original Sherry rule passes
        # gradients through pruned lanes too.  Preserve that pure STE exactly.
        return grad_output, None


def nm_quantize(weight: torch.Tensor, group_size: int = 128) -> torch.Tensor:
    """Fake-quantize a linear weight with dynamic 3:4 sparsity and ternary scales.

    Every consecutive four input lanes retains the three largest magnitudes.  A
    group shares ``delta = abs(nonzero).mean()`` and survivors become ``+/-delta``.

    Differences from AngelSlim Sherry: the public API contains only live
    arguments, validates row-local grouping, and computes scale reductions in
    fp32.  Its backward remains the original, unclipped, mask-free STE.
    """

    return _NMQuantizeSTE.apply(weight, group_size)


class SherryLinear(nn.Linear):
    """``nn.Linear``-compatible Sherry layer with an externally driven residual.

    Differences from AngelSlim ``Arenas``: ``eps`` is a persistent buffer, no
    forward counter exists, schedule constants are not hard-coded, the typo in
    the terminal branch is removed with that branch, and the master parameters
    remain fp32.  The bias is added once rather than once per parallel path.
    """

    def __init__(
        self,
        in_features: int,
        out_features: int,
        bias: bool = True,
        *,
        group_size: int = 128,
        device: torch.device | str | None = None,
    ) -> None:
        if in_features % group_size:
            raise ValueError(
                f"in_features ({in_features}) must be divisible by group_size ({group_size})"
            )
        super().__init__(
            in_features,
            out_features,
            bias=bias,
            device=device,
            dtype=torch.float32,
        )
        self.group_size = group_size
        self.register_buffer("eps", torch.tensor(0.0, dtype=torch.float32))
        # Cached host float so forward can skip the residual path without
        # synchronizing on ``self.eps.item()`` every call.
        self._eps_py = 0.0

    def set_eps(self, value: float | torch.Tensor) -> None:
        """Update the residual coefficient buffer and its host-side cache."""

        eps = float(value.item() if isinstance(value, torch.Tensor) else value)
        self.eps.fill_(eps)
        self._eps_py = eps

    def _load_from_state_dict(
        self,
        state_dict: dict[str, torch.Tensor],
        prefix: str,
        local_metadata: dict[str, Any],
        strict: bool,
        missing_keys: list[str],
        unexpected_keys: list[str],
        error_msgs: list[str],
    ) -> None:
        """Accept native Linear checkpoints while still persisting ``eps``.

        AngelSlim's Python ``eps`` never appeared in checkpoints.  Here it is a
        buffer, but treating an absent epsilon as zero preserves strict loading
        compatibility with existing native ``nn.Linear`` state dictionaries.
        """

        eps_key = prefix + "eps"
        if eps_key not in state_dict:
            state_dict[eps_key] = self.eps.detach().clone()
        super()._load_from_state_dict(
            state_dict,
            prefix,
            local_metadata,
            strict,
            missing_keys,
            unexpected_keys,
            error_msgs,
        )
        self._eps_py = float(self.eps.detach().item())

    @classmethod
    def from_linear(cls, linear: nn.Linear, *, group_size: int = 128) -> "SherryLinear":
        """Copy a native linear into an fp32-master Sherry layer.

        Unlike the original model-construction path, this wrapper works on any
        already-created Transformers model and upgrades bf16/fp16 weights to the
        fp32 latent master representation required for stable QAT.
        """

        converted = cls(
            linear.in_features,
            linear.out_features,
            bias=linear.bias is not None,
            group_size=group_size,
            device=linear.weight.device,
        )
        with torch.no_grad():
            converted.weight.copy_(linear.weight.detach().float())
            if linear.bias is not None and converted.bias is not None:
                converted.bias.copy_(linear.bias.detach().float())
        converted.weight.requires_grad_(linear.weight.requires_grad)
        if linear.bias is not None and converted.bias is not None:
            converted.bias.requires_grad_(linear.bias.requires_grad)
        converted.train(linear.training)
        return converted

    def forward(self, input: torch.Tensor) -> torch.Tensor:
        """Apply quantized and annealed full-precision paths.

        AngelSlim mutates its schedule on every forward (including checkpoint
        recomputation).  This implementation only reads the ``eps`` buffer; an
        :class:`ArenasScheduler` must update it once per optimizer step.
        When ``eps`` is zero the full-precision residual matmul is skipped.
        """

        quantized_weight = nm_quantize(self.weight, self.group_size).to(input.dtype)
        output = F.linear(input, quantized_weight, None)
        if self._eps_py != 0.0:
            master_for_compute = self.weight.to(input.dtype)
            output = output + self.eps.to(input.dtype) * F.linear(
                input, master_for_compute, None
            )
        if self.bias is not None:
            output = output + self.bias.to(input.dtype)
        return output


class ArenasScheduler:
    """Optimizer-step scheduler for the additive Arenas residual coefficient.

    ``eps`` rises linearly from zero to one during warmup and then decays to zero.
    The scheduler is intentionally external: unlike AngelSlim's forward-local
    counter, gradient accumulation and activation checkpointing cannot advance it
    accidentally.  Layer ``eps`` values persist in model checkpoints.
    """

    def __init__(
        self,
        total_steps: int,
        warmup_frac: float = 0.1,
        schedule: str = "cosine",
    ) -> None:
        if not isinstance(total_steps, int) or total_steps < 2:
            raise ValueError("total_steps must be an integer of at least 2")
        if not 0.0 < warmup_frac < 1.0:
            raise ValueError("warmup_frac must be strictly between 0 and 1")
        if schedule not in {"cosine", "linear"}:
            raise ValueError("schedule must be 'cosine' or 'linear'")
        self.total_steps = total_steps
        self.warmup_frac = float(warmup_frac)
        self.schedule = schedule
        self.warmup_steps = min(total_steps - 1, max(1, int(total_steps * warmup_frac)))
        self.current_step = 0

    @property
    def eps(self) -> float:
        """Return epsilon at the current optimizer step."""

        step = min(self.current_step, self.total_steps)
        if step <= self.warmup_steps:
            return step / self.warmup_steps
        progress = (step - self.warmup_steps) / (self.total_steps - self.warmup_steps)
        if self.schedule == "linear":
            return max(0.0, 1.0 - progress)
        return 0.5 * (1.0 + math.cos(math.pi * progress))

    def step(self) -> float:
        """Advance exactly one optimizer step and return the new epsilon."""

        self.current_step = min(self.current_step + 1, self.total_steps)
        return self.eps

    @torch.no_grad()
    def set_eps(self, model: nn.Module) -> list[str]:
        """Write the current epsilon to every Sherry layer and return its names.

        This replaces AngelSlim's non-resumable Python scalar mutation.  Call it
        after ``optimizer.step()`` (and after :meth:`step`) so one scheduler tick
        always corresponds to one optimizer update.
        """

        updated: list[str] = []
        for name, module in model.named_modules():
            if isinstance(module, SherryLinear):
                module.set_eps(self.eps)
                updated.append(name)
        return updated

    def state_dict(self) -> dict[str, int | float | str]:
        """Serialize external progress; layer epsilon itself is model-persistent."""

        return {
            "total_steps": self.total_steps,
            "warmup_frac": self.warmup_frac,
            "schedule": self.schedule,
            "current_step": self.current_step,
        }

    def load_state_dict(self, state: dict[str, int | float | str]) -> None:
        """Restore progress after validating immutable schedule parameters."""

        expected = (self.total_steps, self.warmup_frac, self.schedule)
        found = (state["total_steps"], state["warmup_frac"], state["schedule"])
        if found != expected:
            raise ValueError(f"scheduler configuration mismatch: expected {expected}, got {found}")
        step = int(state["current_step"])
        if not 0 <= step <= self.total_steps:
            raise ValueError("invalid current_step in scheduler state")
        self.current_step = step