File size: 7,736 Bytes
fb5b18e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# quantization/cfrn/quantlib/quant_utils.py

from __future__ import annotations

import math
from dataclasses import dataclass
from typing import Optional, Tuple

import torch

# ---------------------------
# Helper: int4 pack / unpack
# ---------------------------
def pack_int4(q_int8: torch.Tensor) -> Tuple[torch.Tensor, torch.Size]:
    """
    Pack int4 values stored in int8 tensor (expected range [-8,7]) into uint8.
    Two int4 values per byte: low nibble is first, high nibble is second.

    Returns: packed_uint8, original_shape
    """
    if q_int8.dtype != torch.int8:
        raise TypeError(f"pack_int4 expects int8, got {q_int8.dtype}")
    shape = q_int8.shape
    flat = q_int8.reshape(-1).to(torch.int16)  # avoid overflow
    # map [-8,7] -> [0,15]
    flat = flat + 8
    flat = torch.clamp(flat, 0, 15).to(torch.uint8)

    if flat.numel() % 2 == 1:
        flat = torch.cat([flat, torch.zeros(1, device=flat.device, dtype=flat.dtype)], dim=0)

    lo = flat[0::2]
    hi = flat[1::2]
    packed = lo | (hi << 4)
    return packed, shape


def unpack_int4(packed_uint8: torch.Tensor, shape: torch.Size) -> torch.Tensor:
    """
    Unpack uint8 -> int8 int4 values in [-8,7], restoring given shape.
    """
    if packed_uint8.dtype != torch.uint8:
        raise TypeError(f"unpack_int4 expects uint8, got {packed_uint8.dtype}")
    flat = packed_uint8.reshape(-1)
    lo = flat & 0x0F
    hi = (flat >> 4) & 0x0F
    out = torch.empty((flat.numel() * 2,), device=flat.device, dtype=torch.int16)
    out[0::2] = lo.to(torch.int16)
    out[1::2] = hi.to(torch.int16)
    out = out[: int(torch.tensor(shape).prod().item())]
    out = out.to(torch.int16) - 8
    return out.to(torch.int8).reshape(shape)


# ---------------------------
# Symmetric quant/dequant
# ---------------------------
@dataclass
class QParams:
    scale: torch.Tensor  # float tensor, can be scalar or per-channel/group
    qmax: int
    qmin: int
    bits: int
    per_channel: bool = False
    ch_axis: int = 0
    group_size: Optional[int] = None  # for group-wise along last dim (Linear style)


def _signed_qrange(bits: int) -> Tuple[int, int]:
    qmax = (1 << (bits - 1)) - 1
    qmin = - (1 << (bits - 1))
    return qmin, qmax


@torch.no_grad()
def calc_qparams_symmetric(
    x: torch.Tensor,
    bits: int,
    per_channel: bool,
    ch_axis: int,
    clip_percentile: Optional[float] = None,
    sample: Optional[torch.Tensor] = None,
    eps: float = 1e-8,
) -> QParams:
    """
    Compute symmetric scale such that x/scale is in [qmin,qmax].
    If clip_percentile is provided, use percentile of |x| instead of max(|x|).
    For percentile: pass a 1D sample of abs(x) values (optional); otherwise uses abs(x).flatten() which can be heavy.
    """
    qmin, qmax = _signed_qrange(bits)

    if per_channel:
        # reduce all dims except ch_axis
        dims = list(range(x.ndim))
        dims.pop(ch_axis)
        if clip_percentile is None:
            max_abs = x.abs().amax(dim=dims)
        else:
            # approximate percentile per-channel by using max as fallback (cheap)
            # For exact percentile per-channel, provide 'sample' externally.
            max_abs = x.abs().amax(dim=dims)
        scale = max_abs / float(qmax)
    else:
        if clip_percentile is None:
            max_abs = x.abs().max()
        else:
            if sample is None:
                # approximate: take a smaller random sample
                flat = x.abs().flatten()
                if flat.numel() > 4096:
                    idx = torch.randint(0, flat.numel(), (4096,), device=flat.device)
                    flat = flat[idx]
            else:
                flat = sample
            k = max(0.0, min(1.0, float(clip_percentile)))
            max_abs = torch.quantile(flat, k)
        scale = max_abs / float(qmax)

    scale = torch.clamp(scale, min=eps)
    return QParams(scale=scale, qmax=qmax, qmin=qmin, bits=bits, per_channel=per_channel, ch_axis=ch_axis, group_size=None)


@torch.no_grad()
def quantize_symmetric(x: torch.Tensor, qparams: QParams) -> torch.Tensor:
    """
    Return int8 tensor containing quantized values (still stored as int8 even for int4).
    """
    scale = qparams.scale
    if qparams.per_channel:
        view_shape = [1] * x.ndim
        view_shape[qparams.ch_axis] = -1
        scale_b = scale.view(view_shape)
    else:
        scale_b = scale
    q = torch.round(x / scale_b)
    q = torch.clamp(q, qparams.qmin, qparams.qmax)
    return q.to(torch.int8)


@torch.no_grad()
def dequantize_symmetric(q: torch.Tensor, qparams: QParams) -> torch.Tensor:
    scale = qparams.scale
    if qparams.per_channel:
        view_shape = [1] * q.ndim
        view_shape[qparams.ch_axis] = -1
        scale_b = scale.view(view_shape)
    else:
        scale_b = scale
    return q.to(torch.float32) * scale_b.to(torch.float32)


# ---------------------------
# Group-wise weight quant (LLM-style, along last dim of weight row)
# ---------------------------
@dataclass
class GroupQParams:
    scale: torch.Tensor  # [out, n_groups]
    qmax: int
    qmin: int
    bits: int
    group_size: int
    in_features: int
    padded_in: int


@torch.no_grad()
def calc_group_qparams_symmetric(W_2d: torch.Tensor, bits: int, group_size: int, eps: float = 1e-8) -> GroupQParams:
    """
    W_2d shape [out, in]. Group along in dimension for each out row.
    """
    if W_2d.ndim != 2:
        raise ValueError("calc_group_qparams_symmetric expects 2D weight")
    out, in_features = W_2d.shape
    qmin, qmax = _signed_qrange(bits)
    padded_in = int(math.ceil(in_features / group_size) * group_size)
    if padded_in != in_features:
        pad = padded_in - in_features
        Wp = torch.cat([W_2d, torch.zeros((out, pad), device=W_2d.device, dtype=W_2d.dtype)], dim=1)
    else:
        Wp = W_2d
    Wg = Wp.view(out, padded_in // group_size, group_size)
    max_abs = Wg.abs().amax(dim=2)  # [out, n_groups]
    scale = torch.clamp(max_abs / float(qmax), min=eps)
    return GroupQParams(scale=scale, qmax=qmax, qmin=qmin, bits=bits, group_size=group_size, in_features=in_features, padded_in=padded_in)


@torch.no_grad()
def quantize_group_symmetric(W_2d: torch.Tensor, qparams: GroupQParams) -> torch.Tensor:
    out, in_features = W_2d.shape
    if in_features != qparams.in_features:
        raise ValueError("in_features mismatch")
    padded_in = qparams.padded_in
    if padded_in != in_features:
        pad = padded_in - in_features
        Wp = torch.cat([W_2d, torch.zeros((out, pad), device=W_2d.device, dtype=W_2d.dtype)], dim=1)
    else:
        Wp = W_2d
    Wg = Wp.view(out, padded_in // qparams.group_size, qparams.group_size)
    scale = qparams.scale.unsqueeze(-1)  # [out, n_groups, 1]
    q = torch.round(Wg / scale)
    q = torch.clamp(q, qparams.qmin, qparams.qmax).to(torch.int8)
    return q.view(out, padded_in)


@torch.no_grad()
def dequantize_group_symmetric(q_int8: torch.Tensor, qparams: GroupQParams) -> torch.Tensor:
    out, padded_in = q_int8.shape
    if padded_in != qparams.padded_in:
        raise ValueError("padded_in mismatch")
    qg = q_int8.view(out, padded_in // qparams.group_size, qparams.group_size).to(torch.float32)
    scale = qparams.scale.unsqueeze(-1).to(torch.float32)
    W = qg * scale
    W = W.view(out, padded_in)[:, : qparams.in_features]
    return W


# ---------------------------
# Prob/Softmax power-of-two quantization
# ---------------------------
@torch.no_grad()
def quantize_prob_power2(p: torch.Tensor, k: int) -> torch.Tensor:
    """
    Quantize probabilities p in [0,1] to multiples of 1/2^k.
    """
    k = int(k)
    base = 1 << k
    q = torch.round(p * base) / base
    return torch.clamp(q, 0.0, 1.0)