File size: 12,393 Bytes
75f45b9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Make transformers' block-FP8 linears differentiable for LoRA fine-tuning.

The on-disk GLM-5.2 checkpoint stores weights as block-FP8 (e4m3, [128,128]
block scales). transformers' FP8 matmul kernels have NO autograd formula, so
loss.backward() raises:
  "Trying to backward through ...w8a8_block_dynamic_fp8_matmul... no autograd formula".

For LoRA the base weight is FROZEN: we only need grad_INPUT to flow (no weight grad).
We replace the FP8 matmul paths with a differentiable path that, WHEN GRAD IS NEEDED,
dequantizes the FP8 weight on the fly (transient, freed after the op) and runs a normal
F.linear / bmm. When no grad is needed (inference, or grad-checkpoint no_grad fwd) we
keep the fast native FP8 kernel.

Key lever: patch module-level `fp8_linear`. Python resolves module globals at call time,
so FP8Linear.forward AND the eager FP8Experts.linear (both call `fp8_linear(...)`) pick
up the patched version automatically. We also patch the grouped/batched experts-interface
paths in case the model dispatches there.

`_need_grad(input)` gate => only layers downstream of the earliest LoRA adapter run the
(slower) differentiable path; earlier layers keep the fast FP8 kernel. So a LATE-band
LoRA keeps both compute and transient memory bounded.

Dequant mirrors FineGrainedFP8HfQuantizer._dequantize_one:
  W_dq[r,c] = W_fp8[r,c] * scale[r//block_m, c//block_n]
"""
import torch
import torch.nn.functional as F
from torch.utils.checkpoint import checkpoint

_FP8_DTYPE = torch.float8_e4m3fn

class _FP8LinearFn(torch.autograd.Function):
    """Differentiable block-FP8 linear for a FROZEN base weight.

    Forward: dequant W (bf16, transient) -> F.linear -> FREE Wdq (only the small
    output activation is kept in the graph). Backward: RECOMPUTE Wdq transiently
    and return grad_input = grad_out @ Wdq. No weight grad (base is frozen), so the
    bf16 weight is never stored across the whole forward -> bounded memory.
    The fp8 weight + scale are resident base params (saved by reference, ~free).
    """
    @staticmethod
    def forward(ctx, x, weight_fp8, scale_inv, bias, block_size, out_dtype):
        Wdq = dequant_block_fp8(weight_fp8, scale_inv, out_dtype=out_dtype, block_size=block_size)
        y = F.linear(x, Wdq, bias)
        ctx.save_for_backward(weight_fp8, scale_inv)
        ctx.block_size = block_size
        ctx.out_dtype = out_dtype
        del Wdq
        return y

    @staticmethod
    def backward(ctx, grad_out):
        weight_fp8, scale_inv = ctx.saved_tensors
        grad_x = None
        if ctx.needs_input_grad[0]:
            Wdq = dequant_block_fp8(weight_fp8, scale_inv, out_dtype=ctx.out_dtype,
                                    block_size=ctx.block_size)
            grad_x = grad_out.matmul(Wdq)   # y = x @ W^T  ->  dx = grad_out @ W
            del Wdq
        return grad_x, None, None, None, None, None


def _ckpt_linear(input, weight, scale_inv, bias, block_size, out_dtype):
    """grad_input flows; dequant recomputed in backward (frozen base, no weight grad)."""
    if torch.is_grad_enabled() and isinstance(input, torch.Tensor) and input.requires_grad:
        return _FP8LinearFn.apply(input, weight, scale_inv, bias, block_size, out_dtype)
    Wdq = dequant_block_fp8(weight, scale_inv, out_dtype=out_dtype, block_size=block_size)
    return F.linear(input, Wdq, bias)


def _to_local(t):
    try:
        from torch.distributed.tensor import DTensor
        if isinstance(t, DTensor):
            return t.to_local()
    except Exception:
        pass
    return t


def dequant_block_fp8(weight, scale_inv, out_dtype=torch.bfloat16, block_size=None):
    """weight:(out,in) fp8 ; scale_inv:(sr,sc) fp32/ue8m0 -> (out,in) out_dtype.

    Block scales use a FIXED block size (default [128,128] from the checkpoint's
    quant config); the scale grid is ceil(out/bm) x ceil(in/bn), so the final
    block along each dim may be PARTIAL (e.g. out=576 -> 5 blocks, last is 64).
    We expand scales via repeat_interleave at the fixed block size, then slice to
    the weight shape -- this correctly handles partial trailing blocks."""
    W = weight.to(torch.float32)
    out, inp = W.shape[-2], W.shape[-1]
    if scale_inv.dim() == 0 or scale_inv.numel() == 1:
        return (W * scale_inv.to(torch.float32)).to(out_dtype)
    if scale_inv.dtype == torch.uint8:  # ue8m0 packed exponent
        s = (scale_inv.to(torch.float32) - 127.0).exp2()
    else:
        s = scale_inv.to(torch.float32)
    sr, sc = s.shape[-2], s.shape[-1]
    if block_size is not None and len(block_size) == 2:
        bm, bn = int(block_size[0]), int(block_size[1])
    elif out % sr == 0 and inp % sc == 0:
        bm, bn = out // sr, inp // sc
    else:
        # partial trailing block (e.g. 576 -> 5x128); default to 128 block edge
        bm = bn = 128
    s_full = s.repeat_interleave(bm, dim=0).repeat_interleave(bn, dim=1)
    s_full = s_full[:out, :inp]
    return (W * s_full).to(out_dtype)


def _need_grad(x):
    return torch.is_grad_enabled() and isinstance(x, torch.Tensor) and x.requires_grad


def _diff_batched_expert_mm(hidden, weight, scale, expert_ids, num_experts, block_size=None):
    """Differentiable replacement for finegrained_fp8.batched_matmul.
    hidden:(S,in) weight:(E,out,in) scale:(E,sr,sc) -> (S,out). One expert at a time."""
    S = hidden.size(0)
    out_dim = weight.size(1)
    y = hidden.new_zeros(S, out_dim)
    for e in torch.unique(expert_ids).tolist():
        if e >= num_experts:
            continue
        mask = expert_ids == e
        rows = hidden[mask]
        if rows.numel() == 0:
            continue
        y[mask] = _ckpt_linear(rows, weight[e], scale[e], None, block_size, hidden.dtype)
    return y


def install(verbose=True):
    from transformers.integrations import finegrained_fp8 as fp8

    # ---- core: module-level fp8_linear (dense attn LoRA targets + eager experts) ----
    if not getattr(fp8, "_fp8_linear_diff_patched", False):
        _orig_fp8_linear = fp8.fp8_linear

        def diff_fp8_linear(input, weight, weight_scale_inv, block_size=None,
                            activation_scale=None, output_dtype=None, bias=None):
            if not _need_grad(input):
                return _orig_fp8_linear(input, weight, weight_scale_inv,
                                        block_size=block_size,
                                        activation_scale=activation_scale,
                                        output_dtype=output_dtype, bias=bias)
            return _ckpt_linear(input, weight, weight_scale_inv, bias,
                                block_size, (output_dtype or input.dtype))

        fp8.fp8_linear = diff_fp8_linear
        fp8._fp8_linear_diff_patched = True
        if verbose:
            print("[fp8_diff_patch] patched fp8_linear", flush=True)

    # ---- grouped FP8GroupedLinear (shared/fused grouped) ----
    if hasattr(fp8, "FP8GroupedLinear") and not getattr(fp8.FP8GroupedLinear, "_diff_patched", False):
        _orig_grp = fp8.FP8GroupedLinear.forward

        def grp_forward(self, x):
            if self.weight.element_size() > 1 or not _need_grad(x):
                return _orig_grp(self, x)
            input_shape = x.shape[:-2]
            hidden_dim = x.shape[-1]
            w = _to_local(self.weight)
            s = _to_local(self.weight_scale_inv)
            ng = self.n_groups
            Wdq = dequant_block_fp8(w, s, out_dtype=x.dtype, block_size=self.block_size).view(ng, -1, hidden_dim).transpose(1, 2)
            xg = x.reshape(-1, ng, hidden_dim).transpose(0, 1)
            y = torch.bmm(xg, Wdq).transpose(0, 1).reshape(*input_shape, ng, -1)
            if getattr(self, "has_bias", False):
                y = y + self.bias.view(ng, -1)
            return y

        fp8.FP8GroupedLinear.forward = grp_forward
        fp8.FP8GroupedLinear._diff_patched = True
        if verbose:
            print("[fp8_diff_patch] patched FP8GroupedLinear.forward", flush=True)

    # ---- experts batched_mm dispatch ----
    if not getattr(fp8, "_experts_batched_diff_patched", False):
        _orig_experts = fp8.fp8_batched_mm_experts_forward

        def diff_experts_forward(self, hidden_states, top_k_index, top_k_weights):
            if not _need_grad(hidden_states):
                return _orig_experts(self, hidden_states, top_k_index, top_k_weights)
            num_top_k = top_k_index.size(-1)
            num_tokens = hidden_states.size(0)
            hidden_dim = hidden_states.size(-1)
            selected = hidden_states.repeat_interleave(num_top_k, dim=0)
            sample_weights = top_k_weights.reshape(-1)
            expert_ids = top_k_index.reshape(-1)
            sentinel = (expert_ids >= self.num_experts).unsqueeze(-1)
            w_up = _to_local(self.gate_up_proj if self.has_gate else self.up_proj)
            s_up = _to_local(self.gate_up_proj_scale_inv if self.has_gate else self.up_proj_scale_inv)
            w_dn = _to_local(self.down_proj)
            s_dn = _to_local(self.down_proj_scale_inv)
            proj = _diff_batched_expert_mm(selected, w_up, s_up, expert_ids, self.num_experts, self.block_size)
            proj = self._apply_gate(proj) if self.has_gate else self.act_fn(proj)
            proj = _diff_batched_expert_mm(proj, w_dn, s_dn, expert_ids, self.num_experts, self.block_size)
            weighted = (proj * sample_weights.to(proj.dtype).unsqueeze(-1)).masked_fill(sentinel, 0.0)
            return weighted.view(num_tokens, num_top_k, hidden_dim).sum(dim=1).to(hidden_states.dtype)

        fp8.fp8_batched_mm_experts_forward = diff_experts_forward
        for tgt in (getattr(fp8.FP8ExpertsInterface, "_global_mapping", None),
                    getattr(fp8, "ALL_FP8_EXPERTS_FUNCTIONS", None)):
            try:
                if tgt is not None and "batched_mm" in tgt:
                    tgt["batched_mm"] = diff_experts_forward
            except Exception:
                pass
        fp8._experts_batched_diff_patched = True
        if verbose:
            print("[fp8_diff_patch] patched fp8_batched_mm_experts_forward", flush=True)

    # ---- experts grouped_mm dispatch (GLM-5.2 default) ----
    if not getattr(fp8, "_experts_grouped_diff_patched", False):
        _orig_grouped = fp8.fp8_grouped_mm_experts_forward

        def diff_grouped_experts_forward(self, hidden_states, top_k_index, top_k_weights):
            if not _need_grad(hidden_states):
                return _orig_grouped(self, hidden_states, top_k_index, top_k_weights)
            num_top_k = top_k_index.size(-1)
            num_tokens = hidden_states.size(0)
            hidden_dim = hidden_states.size(-1)
            sample_weights = top_k_weights.reshape(-1)            # (S,)
            expert_ids = top_k_index.reshape(-1)                  # (S,)
            # token i of pair p is hidden_states[p // num_top_k]
            sel = hidden_states.repeat_interleave(num_top_k, dim=0)  # (S, H)
            sentinel = (expert_ids >= self.num_experts).unsqueeze(-1)
            w_up = _to_local(self.gate_up_proj if self.has_gate else self.up_proj)
            s_up = _to_local(self.gate_up_proj_scale_inv if self.has_gate else self.up_proj_scale_inv)
            w_dn = _to_local(self.down_proj)
            s_dn = _to_local(self.down_proj_scale_inv)
            proj = _diff_batched_expert_mm(sel, w_up, s_up, expert_ids, self.num_experts, self.block_size)
            proj = self._apply_gate(proj) if self.has_gate else self.act_fn(proj)
            proj = _diff_batched_expert_mm(proj, w_dn, s_dn, expert_ids, self.num_experts, self.block_size)
            weighted = (proj * sample_weights.to(proj.dtype).unsqueeze(-1)).masked_fill(sentinel, 0.0)
            return weighted.view(num_tokens, num_top_k, hidden_dim).sum(dim=1).to(hidden_states.dtype)

        fp8.fp8_grouped_mm_experts_forward = diff_grouped_experts_forward
        for tgt in (getattr(fp8.FP8ExpertsInterface, "_global_mapping", None),
                    getattr(fp8, "ALL_FP8_EXPERTS_FUNCTIONS", None)):
            try:
                if tgt is not None and "grouped_mm" in tgt:
                    tgt["grouped_mm"] = diff_grouped_experts_forward
            except Exception:
                pass
        fp8._experts_grouped_diff_patched = True
        if verbose:
            print("[fp8_diff_patch] patched fp8_grouped_mm_experts_forward", flush=True)

    return fp8