|
| 1 | +"""MDBF differentiable parameter management for global PTQ. |
| 2 | +
|
| 3 | +Makes MultipathMDBFLinear layers trainable by exposing amplitude parameters |
| 4 | +(A_amp, B_amp, Q_U_amp, Q_V_amp) per path as optimisable tensors, and |
| 5 | +optionally enabling differentiable binary-sign optimisation via smooth sign STE. |
| 6 | +
|
| 7 | +Architecture recap (per MDBFLinear path): |
| 8 | + F = A_sign * (A_amp @ Q_U_amp^T) shape: (n, r) |
| 9 | + G = B_sign * (Q_V_amp @ B_amp^T) shape: (r, m) |
| 10 | + y = x @ G^T @ F^T |
| 11 | +
|
| 12 | +Trainable (continuous): |
| 13 | + A_amp, B_amp, Q_U_amp, Q_V_amp — amplitude/scale factors per path. |
| 14 | +Trainable (discrete, opt-in): |
| 15 | + A_sign, B_sign — ±1 binary factor matrices per path, via sign STE. |
| 16 | +
|
| 17 | +Copyright 2025-2026 Fujitsu Ltd. |
| 18 | +
|
| 19 | +Authors: Keiji Kimura |
| 20 | +
|
| 21 | +""" |
| 22 | + |
| 23 | +from types import MethodType |
| 24 | +from typing import Dict, List, Tuple |
| 25 | + |
| 26 | +import torch |
| 27 | +import torch.nn as nn |
| 28 | + |
| 29 | +from .helpers import smooth_sign_ste |
| 30 | + |
| 31 | +_AMP_ATTRS = ("A_amp", "B_amp", "Q_U_amp", "Q_V_amp") |
| 32 | +_BINARY_SIGN_NAMES = ("A", "B") |
| 33 | + |
| 34 | +# Sharpness for sign-STE: same rationale as DBF adapter (values near ±1, |
| 35 | +# tanh saturation avoided by using k=2 instead of the GPTQ default k=100). |
| 36 | +_BINARY_STE_K = 2.0 |
| 37 | + |
| 38 | + |
| 39 | +# --------------------------------------------------------------------------- |
| 40 | +# Finding MDBF modules |
| 41 | +# --------------------------------------------------------------------------- |
| 42 | + |
| 43 | + |
| 44 | +def find_mdbf_modules(model: nn.Module) -> List[Tuple[str, nn.Module]]: |
| 45 | + """Return all ``MultipathMDBFLinear`` modules as ``(name, module)`` pairs.""" |
| 46 | + from onecomp.quantizer.mdbf.mdbf_layer import MultipathMDBFLinear |
| 47 | + |
| 48 | + from .helpers import find_target_modules |
| 49 | + |
| 50 | + return find_target_modules(model, MultipathMDBFLinear) |
| 51 | + |
| 52 | + |
| 53 | +# --------------------------------------------------------------------------- |
| 54 | +# Differentiable forward (per MDBFLinear path) |
| 55 | +# --------------------------------------------------------------------------- |
| 56 | + |
| 57 | + |
| 58 | +def _make_mdbf_differentiable_forward(): |
| 59 | + """Build a differentiable ``forward`` for a ``MultipathMDBFLinear``. |
| 60 | +
|
| 61 | + Each path's computation graph is reconstructed from the (possibly |
| 62 | + optimisable) amplitude parameters and, when ``_opt_A_sign_{p}`` / |
| 63 | + ``_opt_B_sign_{p}`` exist, through :func:`smooth_sign_ste` so that |
| 64 | + gradients flow to the float sign-weight tensors as well. |
| 65 | + """ |
| 66 | + from onecomp.quantizer.mdbf.mdbf_layer import unpack_binary |
| 67 | + |
| 68 | + def differentiable_forward(self, x: torch.Tensor) -> torch.Tensor: |
| 69 | + dtype = x.dtype |
| 70 | + k = getattr(self, "_binary_ste_k", _BINARY_STE_K) |
| 71 | + |
| 72 | + y = None |
| 73 | + for path in self.paths: |
| 74 | + # ---- amplitude parameters (always trainable) ---- |
| 75 | + A_amp = getattr(path, "_opt_A_amp", path.A_amp).to(dtype) |
| 76 | + B_amp = getattr(path, "_opt_B_amp", path.B_amp).to(dtype) |
| 77 | + Q_U_amp = getattr(path, "_opt_Q_U_amp", path.Q_U_amp).to(dtype) |
| 78 | + Q_V_amp = getattr(path, "_opt_Q_V_amp", path.Q_V_amp).to(dtype) |
| 79 | + |
| 80 | + # ---- sign matrices (STE or packed buffer) ---- |
| 81 | + if hasattr(path, "_opt_A_sign"): |
| 82 | + A_sign = smooth_sign_ste(path._opt_A_sign, k=k).to(dtype) |
| 83 | + else: |
| 84 | + A_sign = unpack_binary(path._packed_sign("A", x.device), (path.n, path.r)).to( |
| 85 | + dtype |
| 86 | + ) |
| 87 | + |
| 88 | + if hasattr(path, "_opt_B_sign"): |
| 89 | + B_sign = smooth_sign_ste(path._opt_B_sign, k=k).to(dtype) |
| 90 | + else: |
| 91 | + B_sign = unpack_binary(path._packed_sign("B", x.device), (path.r, path.m)).to( |
| 92 | + dtype |
| 93 | + ) |
| 94 | + |
| 95 | + # F = A_sign * (A_amp @ Q_U_amp^T) shape: (n, r) |
| 96 | + F = A_sign * (A_amp @ Q_U_amp.T) |
| 97 | + # G = B_sign * (Q_V_amp @ B_amp^T) shape: (r, m) |
| 98 | + G = B_sign * (Q_V_amp @ B_amp.T) |
| 99 | + |
| 100 | + # y += x @ G^T @ F^T |
| 101 | + path_out = x @ G.T @ F.T |
| 102 | + |
| 103 | + y = path_out if y is None else y + path_out |
| 104 | + |
| 105 | + if self.bias is not None: |
| 106 | + y = y + self.bias.to(dtype) |
| 107 | + return y |
| 108 | + |
| 109 | + return differentiable_forward |
| 110 | + |
| 111 | + |
| 112 | +# --------------------------------------------------------------------------- |
| 113 | +# Parameter setup |
| 114 | +# --------------------------------------------------------------------------- |
| 115 | + |
| 116 | + |
| 117 | +def setup_mdbf_differentiable( |
| 118 | + mdbf_modules: List[Tuple[str, nn.Module]], |
| 119 | + optimize_binary: bool = False, |
| 120 | + ste_k: float = _BINARY_STE_K, |
| 121 | +) -> Tuple[Dict[str, object], List[torch.Tensor], List[torch.Tensor]]: |
| 122 | + """Make MDBF amplitude (and optionally sign) parameters trainable. |
| 123 | +
|
| 124 | + For each ``MultipathMDBFLinear`` module, the original ``forward`` is |
| 125 | + replaced with a differentiable version. Amplitude parameters |
| 126 | + (A_amp, B_amp, Q_U_amp, Q_V_amp) of every path are promoted to |
| 127 | + float32 ``nn.Parameter`` objects stored as ``_opt_*`` attributes on the |
| 128 | + individual ``MDBFLinear`` path modules. |
| 129 | +
|
| 130 | + Args: |
| 131 | + mdbf_modules: List of ``(name, module)`` pairs from |
| 132 | + :func:`find_mdbf_modules`. |
| 133 | + optimize_binary: When True, also expose unpacked ±1 sign matrices |
| 134 | + as float tensors with ``requires_grad=True`` so that gradients |
| 135 | + flow through :func:`smooth_sign_ste`. |
| 136 | + ste_k: Sharpness for binary sign STE (``tanh(k*x)`` backward). |
| 137 | + Default is :data:`_BINARY_STE_K` (2.0). |
| 138 | +
|
| 139 | + Returns: |
| 140 | + ``(original_forwards, amp_params, binary_params)`` |
| 141 | +
|
| 142 | + *original_forwards* maps module name → original forward (for restore). |
| 143 | + *amp_params* is a flat list of float32 ``nn.Parameter`` objects. |
| 144 | + *binary_params* is a flat list of float tensors (empty when |
| 145 | + *optimize_binary* is ``False``). |
| 146 | + """ |
| 147 | + from onecomp.quantizer.mdbf.mdbf_layer import unpack_binary |
| 148 | + |
| 149 | + original_forwards: Dict[str, object] = {} |
| 150 | + amp_params: List[torch.Tensor] = [] |
| 151 | + binary_params: List[torch.Tensor] = [] |
| 152 | + |
| 153 | + for name, mod in mdbf_modules: |
| 154 | + for path in mod.paths: |
| 155 | + # ---- continuous amplitude parameters ---- |
| 156 | + for attr in _AMP_ATTRS: |
| 157 | + buf = getattr(path, attr) |
| 158 | + fp32 = buf.data.detach().clone().float() |
| 159 | + new_param = nn.Parameter(fp32, requires_grad=True) |
| 160 | + setattr(path, f"_opt_{attr}", new_param) |
| 161 | + amp_params.append(new_param) |
| 162 | + |
| 163 | + # ---- discrete sign parameters (optional) ---- |
| 164 | + if optimize_binary: |
| 165 | + for which in _BINARY_SIGN_NAMES: |
| 166 | + shape = (path.n, path.r) if which == "A" else (path.r, path.m) |
| 167 | + packed_key = f"{which}_sign_packed" |
| 168 | + packed = path._buffers.get(packed_key) |
| 169 | + if packed is None: |
| 170 | + # GemLite mode: stashed on CPU |
| 171 | + packed = path._packed_cpu.get(which) |
| 172 | + if packed is None: |
| 173 | + continue |
| 174 | + unpacked = unpack_binary(packed, shape).float().detach().clone() |
| 175 | + new_param = nn.Parameter(unpacked, requires_grad=True) |
| 176 | + setattr(path, f"_opt_{which}_sign", new_param) |
| 177 | + binary_params.append(new_param) |
| 178 | + |
| 179 | + original_forwards[name] = mod.forward |
| 180 | + mod._binary_ste_k = float(ste_k) |
| 181 | + mod.forward = MethodType(_make_mdbf_differentiable_forward(), mod) |
| 182 | + |
| 183 | + return original_forwards, amp_params, binary_params |
| 184 | + |
| 185 | + |
| 186 | +# --------------------------------------------------------------------------- |
| 187 | +# Forward restore / re-install |
| 188 | +# --------------------------------------------------------------------------- |
| 189 | + |
| 190 | + |
| 191 | +def restore_mdbf_original( |
| 192 | + mdbf_modules: List[Tuple[str, nn.Module]], |
| 193 | + original_forwards: Dict[str, object], |
| 194 | + cleanup: bool = False, |
| 195 | +) -> None: |
| 196 | + """Restore every module's original ``forward`` method.""" |
| 197 | + for name, mod in mdbf_modules: |
| 198 | + if name in original_forwards: |
| 199 | + mod.__dict__.pop("forward", None) |
| 200 | + if not hasattr(mod, "forward") or mod.forward != original_forwards[name]: |
| 201 | + mod.forward = original_forwards[name] |
| 202 | + |
| 203 | + if cleanup: |
| 204 | + if hasattr(mod, "_binary_ste_k"): |
| 205 | + delattr(mod, "_binary_ste_k") |
| 206 | + for path in mod.paths: |
| 207 | + for attr in _AMP_ATTRS: |
| 208 | + opt_attr = f"_opt_{attr}" |
| 209 | + if hasattr(path, opt_attr): |
| 210 | + delattr(path, opt_attr) |
| 211 | + for which in _BINARY_SIGN_NAMES: |
| 212 | + opt_attr = f"_opt_{which}_sign" |
| 213 | + if hasattr(path, opt_attr): |
| 214 | + delattr(path, opt_attr) |
| 215 | + |
| 216 | + |
| 217 | +def setup_mdbf_forwards_only( |
| 218 | + mdbf_modules: List[Tuple[str, nn.Module]], |
| 219 | + original_forwards: Dict[str, object], |
| 220 | +) -> None: |
| 221 | + """Re-install differentiable forwards for continued training after eval.""" |
| 222 | + for name, mod in mdbf_modules: |
| 223 | + if name not in original_forwards: |
| 224 | + original_forwards[name] = mod.forward |
| 225 | + mod.forward = MethodType(_make_mdbf_differentiable_forward(), mod) |
| 226 | + |
| 227 | + |
| 228 | +# --------------------------------------------------------------------------- |
| 229 | +# Write-back |
| 230 | +# --------------------------------------------------------------------------- |
| 231 | + |
| 232 | + |
| 233 | +def write_back_mdbf_binary(mdbf_modules: List[Tuple[str, nn.Module]]) -> None: |
| 234 | + """Write optimised float sign tensors back to packed uint8 buffers.""" |
| 235 | + from onecomp.quantizer.mdbf.mdbf_layer import pack_binary |
| 236 | + |
| 237 | + with torch.no_grad(): |
| 238 | + for _name, mod in mdbf_modules: |
| 239 | + for path in mod.paths: |
| 240 | + for which in _BINARY_SIGN_NAMES: |
| 241 | + opt_attr = f"_opt_{which}_sign" |
| 242 | + if not hasattr(path, opt_attr): |
| 243 | + continue |
| 244 | + w = getattr(path, opt_attr) |
| 245 | + q = w.sign() |
| 246 | + q[q == 0] = 1 |
| 247 | + shape = (path.n, path.r) if which == "A" else (path.r, path.m) |
| 248 | + packed, _ = pack_binary(q.to(torch.int8).reshape(shape)) |
| 249 | + buf_key = f"{which}_sign_packed" |
| 250 | + if buf_key in path._buffers: |
| 251 | + path._buffers[buf_key].copy_(packed.to(path._buffers[buf_key].device)) |
| 252 | + elif which in path._packed_cpu: |
| 253 | + path._packed_cpu[which].copy_(packed.cpu()) |
| 254 | + |
| 255 | + |
| 256 | +def write_back_mdbf_amp(mdbf_modules: List[Tuple[str, nn.Module]]) -> None: |
| 257 | + """Copy float32 optimised amp params back to fp16 buffers for inference.""" |
| 258 | + with torch.no_grad(): |
| 259 | + for _name, mod in mdbf_modules: |
| 260 | + for path in mod.paths: |
| 261 | + for attr in _AMP_ATTRS: |
| 262 | + opt_attr = f"_opt_{attr}" |
| 263 | + if not hasattr(path, opt_attr): |
| 264 | + continue |
| 265 | + opt_param = getattr(path, opt_attr) |
| 266 | + buf = getattr(path, attr) |
| 267 | + buf.copy_(opt_param.data.half()) |
| 268 | + |
| 269 | + |
| 270 | +# --------------------------------------------------------------------------- |
| 271 | +# State save / load (for rollback) |
| 272 | +# --------------------------------------------------------------------------- |
| 273 | + |
| 274 | + |
| 275 | +def save_mdbf_state(mdbf_modules: List[Tuple[str, nn.Module]]) -> Dict: |
| 276 | + """Snapshot amplitude buffers and packed sign buffers.""" |
| 277 | + state: Dict[str, dict] = {} |
| 278 | + for name, mod in mdbf_modules: |
| 279 | + paths_state = {} |
| 280 | + for p, path in enumerate(mod.paths): |
| 281 | + d: dict = {} |
| 282 | + for attr in _AMP_ATTRS: |
| 283 | + d[attr] = getattr(path, attr).data.clone() |
| 284 | + for which in _BINARY_SIGN_NAMES: |
| 285 | + buf_key = f"{which}_sign_packed" |
| 286 | + if buf_key in path._buffers: |
| 287 | + d[buf_key] = path._buffers[buf_key].clone() |
| 288 | + elif which in path._packed_cpu: |
| 289 | + d[buf_key] = path._packed_cpu[which].clone() |
| 290 | + paths_state[p] = d |
| 291 | + state[name] = paths_state |
| 292 | + return state |
| 293 | + |
| 294 | + |
| 295 | +def load_mdbf_state( |
| 296 | + mdbf_modules: List[Tuple[str, nn.Module]], |
| 297 | + state: Dict, |
| 298 | +) -> None: |
| 299 | + """Restore a previously saved snapshot.""" |
| 300 | + with torch.no_grad(): |
| 301 | + for name, mod in mdbf_modules: |
| 302 | + if name not in state: |
| 303 | + continue |
| 304 | + paths_state = state[name] |
| 305 | + for p, path in enumerate(mod.paths): |
| 306 | + if p not in paths_state: |
| 307 | + continue |
| 308 | + d = paths_state[p] |
| 309 | + for attr in _AMP_ATTRS: |
| 310 | + if attr in d: |
| 311 | + getattr(path, attr).copy_(d[attr]) |
| 312 | + for which in _BINARY_SIGN_NAMES: |
| 313 | + buf_key = f"{which}_sign_packed" |
| 314 | + if buf_key not in d: |
| 315 | + continue |
| 316 | + if buf_key in path._buffers: |
| 317 | + path._buffers[buf_key].copy_(d[buf_key]) |
| 318 | + elif which in path._packed_cpu: |
| 319 | + path._packed_cpu[which].copy_(d[buf_key].cpu()) |
0 commit comments