File size: 3,506 Bytes
bdd9175
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Post-training INT8 quantization (fake-quant) for TinyCast.

Measures the GIFT-Eval accuracy of an INT8-deployed model by simulating INT8 arithmetic in
floating point. Per-output-channel symmetric INT8 weights on every Linear and Conv1d; optional
per-tensor dynamic INT8 activations. RMSNorm, the SiLU gate, the min-max (de)normalization,
and the rFFT period detector stay in full precision: they are off the convolutional mixing path
and run as fp/LUT ops on the target runtime.

Modes (env ``TINYCAST_INT8``):
  ``w8``    per-channel INT8 weights, fp activations. The ~145 KB weight footprint; isolates the
            weight-quantization error.
  ``w8a8``  + per-tensor dynamic INT8 activations (scale from each tensor's own range, so no
            calibration set is needed; an optimistic but faithful estimate of full INT8 compute).

The weight quant is applied in-place to the parameter tensors, so it is correct regardless of
whether a module is invoked via ``__call__`` or functionally (the separable pointwise conv is run
as ``F.linear(weight.squeeze(-1))``). Activation quant (``w8a8``) uses a forward-pre-hook on every
Linear/Conv1d to quantize inputs, plus a forward-hook on Conv1d to quantize the depthwise output
that feeds the functional pointwise, covering every activation site on the mixing path.
"""
from __future__ import annotations

import torch
import torch.nn as nn

_QMIN, _QMAX = -128, 127  # int8 symmetric (zero-point 0)


@torch.no_grad()
def _fq_weight_per_outchannel(w: torch.Tensor) -> torch.Tensor:
    """Symmetric per-output-channel (axis 0) int8 fake-quant of a weight tensor.

    Linear weight is (out, in); Conv1d weight is (out, in/groups, k). Axis 0 is the output
    channel in both, so a per-axis-0 scale is the standard per-channel weight scheme.
    """
    red = tuple(d for d in range(w.dim()) if d != 0)
    amax = w.abs().amax(dim=red, keepdim=True).clamp_(min=1e-12)
    scale = amax / _QMAX
    return (torch.round(w / scale).clamp_(_QMIN, _QMAX) * scale).to(w.dtype)


def _fq_act_dynamic(x: torch.Tensor) -> torch.Tensor:
    """Per-tensor symmetric int8 fake-quant with a dynamic (this-tensor) scale."""
    if not torch.is_floating_point(x):
        return x
    amax = x.detach().abs().amax().clamp(min=1e-12)
    scale = amax / _QMAX
    return torch.round(x / scale).clamp(_QMIN, _QMAX) * scale


def _pre_hook(_mod, inp):
    if not inp:
        return None
    return (_fq_act_dynamic(inp[0]),) + tuple(inp[1:])


def _post_hook(_mod, _inp, out):
    return _fq_act_dynamic(out)


def quantize_int8_(model: nn.Module, mode: str = "w8") -> nn.Module:
    """In-place INT8 fake-quant of ``model``. ``mode`` in {"w8", "w8a8"}. Returns ``model``."""
    mode = mode.strip().lower()
    if mode not in ("w8", "w8a8"):
        raise ValueError(f"unknown INT8 mode {mode!r} (expected 'w8' or 'w8a8')")
    n_w = 0
    with torch.no_grad():
        for m in model.modules():
            if isinstance(m, (nn.Linear, nn.Conv1d)):
                m.weight.data.copy_(_fq_weight_per_outchannel(m.weight.data))
                n_w += 1
                if mode == "w8a8":
                    m.register_forward_pre_hook(_pre_hook)
                    if isinstance(m, nn.Conv1d):
                        m.register_forward_hook(_post_hook)
    print(
        f"[quant] INT8 {mode}: fake-quantized {n_w} Linear/Conv1d weight tensors"
        + (" + per-tensor dynamic activation quant" if mode == "w8a8" else ""),
        flush=True,
    )
    return model