Skip to content

Commit b8569af

Browse files
feat(global_ptq): add MDBF differentiable adapter
Introduce the piece needed to make MultipathMDBFLinear layers trainable during GlobalPTQ-style QAT: - mdbf_adapter.py: promotes per-path amplitude parameters (A_amp, B_amp, Q_U_amp, Q_V_amp) to fp32 nn.Parameter and, when optimize_binary=True, exposes +/-1 sign matrices as float shadow tensors trained through a smooth sign STE. Provides differentiable forward reconstruction, write-back to packed/fp16 buffers for inference, and state snapshot/restore for rollback. This module is not wired into the training loop yet; see the following commit for integration into GlobalPTQ/GlobalPTQDistributed. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b3fa345 commit b8569af

1 file changed

Lines changed: 319 additions & 0 deletions

File tree

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
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

Comments
 (0)