bitsandbytes-cpu-fork 0.50.2.dev0__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bitsandbytes/__init__.py +81 -0
- bitsandbytes/__main__.py +4 -0
- bitsandbytes/_ops.py +526 -0
- bitsandbytes/autograd/__init__.py +0 -0
- bitsandbytes/autograd/_functions.py +491 -0
- bitsandbytes/backends/__init__.py +0 -0
- bitsandbytes/backends/cpu/__init__.py +0 -0
- bitsandbytes/backends/cpu/ops.py +805 -0
- bitsandbytes/backends/cuda/__init__.py +0 -0
- bitsandbytes/backends/cuda/ops.py +1212 -0
- bitsandbytes/backends/default/__init__.py +0 -0
- bitsandbytes/backends/default/ops.py +632 -0
- bitsandbytes/backends/hpu/__init__.py +0 -0
- bitsandbytes/backends/hpu/ops.py +53 -0
- bitsandbytes/backends/mps/__init__.py +0 -0
- bitsandbytes/backends/mps/ops.py +277 -0
- bitsandbytes/backends/triton/__init__.py +0 -0
- bitsandbytes/backends/triton/kernels_4bit.py +577 -0
- bitsandbytes/backends/triton/kernels_8bit_quant.py +195 -0
- bitsandbytes/backends/triton/kernels_optim.py +1177 -0
- bitsandbytes/backends/triton/ops.py +304 -0
- bitsandbytes/backends/utils.py +94 -0
- bitsandbytes/backends/xpu/__init__.py +0 -0
- bitsandbytes/backends/xpu/ops.py +305 -0
- bitsandbytes/cextension.py +405 -0
- bitsandbytes/consts.py +12 -0
- bitsandbytes/cuda_specs.py +111 -0
- bitsandbytes/diagnostics/__init__.py +0 -0
- bitsandbytes/diagnostics/cuda.py +193 -0
- bitsandbytes/diagnostics/main.py +134 -0
- bitsandbytes/diagnostics/utils.py +12 -0
- bitsandbytes/functional.py +1836 -0
- bitsandbytes/gdn_cpu.py +602 -0
- bitsandbytes/libbitsandbytes_cpu.dll +0 -0
- bitsandbytes/nn/__init__.py +19 -0
- bitsandbytes/nn/modules.py +1220 -0
- bitsandbytes/nn/parametrize.py +206 -0
- bitsandbytes/optim/__init__.py +22 -0
- bitsandbytes/optim/adagrad.py +187 -0
- bitsandbytes/optim/adam.py +346 -0
- bitsandbytes/optim/adamw.py +337 -0
- bitsandbytes/optim/ademamix.py +410 -0
- bitsandbytes/optim/lamb.py +190 -0
- bitsandbytes/optim/lars.py +259 -0
- bitsandbytes/optim/lion.py +266 -0
- bitsandbytes/optim/optimizer.py +756 -0
- bitsandbytes/optim/rmsprop.py +170 -0
- bitsandbytes/optim/sgd.py +152 -0
- bitsandbytes/py.typed +0 -0
- bitsandbytes/utils.py +208 -0
- bitsandbytes/vcomp140.dll +0 -0
- bitsandbytes_cpu_fork-0.50.2.dev0.dist-info/METADATA +346 -0
- bitsandbytes_cpu_fork-0.50.2.dev0.dist-info/RECORD +56 -0
- bitsandbytes_cpu_fork-0.50.2.dev0.dist-info/WHEEL +5 -0
- bitsandbytes_cpu_fork-0.50.2.dev0.dist-info/licenses/LICENSE +21 -0
- bitsandbytes_cpu_fork-0.50.2.dev0.dist-info/top_level.txt +1 -0
bitsandbytes/gdn_cpu.py
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
"""Fused Gated DeltaNet (gated delta rule) recurrent kernels for CPU.
|
|
2
|
+
|
|
3
|
+
A drop-in CPU replacement for ``fla.ops.gated_delta_rule.fused_recurrent_gated_delta_rule``
|
|
4
|
+
(targets the Qwen3-Next / Qwen3.5 linear-attention layers). On CPU there is no
|
|
5
|
+
triton, so fla falls back to a per-timestep eager loop with a T-deep autograd
|
|
6
|
+
graph, which is what makes one backward pass take minutes (728 s observed).
|
|
7
|
+
The native kernels in ``csrc/cpu_gdn.cpp`` (AVX2/FMA, OpenMP over heads,
|
|
8
|
+
chunk-checkpointed backward) fix that: 4-5x faster fwd/bwd, O(ceil(T/C))
|
|
9
|
+
checkpoint memory instead of O(T) saved tensors, bit-identical for any thread
|
|
10
|
+
count.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
from bitsandbytes.gdn_cpu import fused_recurrent_gated_delta_rule
|
|
14
|
+
o, ht = fused_recurrent_gated_delta_rule(q, k, v, beta, g, scale=s,
|
|
15
|
+
initial_state=s0,
|
|
16
|
+
output_final_state=True)
|
|
17
|
+
|
|
18
|
+
# or patch an installed stack once, then run the model as usual:
|
|
19
|
+
from bitsandbytes.gdn_cpu import patch_fla, patch_transformers
|
|
20
|
+
patch_fla(); patch_transformers()
|
|
21
|
+
|
|
22
|
+
Layouts follow fla: q/k [B,T,H,K], v [B,T,H,V], beta/g [B,T,H],
|
|
23
|
+
initial/final state [B,H,K,V]. Contiguous inputs are read in place (zero
|
|
24
|
+
copies, saved for backward without duplication - the big memory win on
|
|
25
|
+
long sequences); strided inputs fall back to a per-head-contiguous copy.
|
|
26
|
+
Inputs may be any mix of fp32/bf16/fp16 for q/k/v (beta/g are converted to
|
|
27
|
+
fp32, as the kernel requires), so strided tensors are handled at the
|
|
28
|
+
boundary and never silently misread.
|
|
29
|
+
|
|
30
|
+
Env knobs:
|
|
31
|
+
GDN_CPU_LIB path to the shared library (default: search next to this
|
|
32
|
+
file, then the package dir for libgdn_cpu.so /
|
|
33
|
+
libbitsandbytes_cpu.so)
|
|
34
|
+
GDN_CPU_CHUNK chunk length C for the backward checkpoints. Unset (the
|
|
35
|
+
default) auto-sizes C to minimize peak memory
|
|
36
|
+
(checkpoints = B*H*ceil(T/C)*K*V*4 bytes); set a positive
|
|
37
|
+
int to pin it.
|
|
38
|
+
GDN_CPU_DISABLE set to 1 to make the op raise, so callers can verify the
|
|
39
|
+
fallback path still works
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import ctypes
|
|
45
|
+
import os
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
from typing import Optional
|
|
48
|
+
|
|
49
|
+
import torch
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"fused_recurrent_gated_delta_rule",
|
|
53
|
+
"FusedRecurrentGatedDeltaRuleFunction",
|
|
54
|
+
"patch_fla",
|
|
55
|
+
"patch_transformers",
|
|
56
|
+
"load_native",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
_DTYPE_CODE = {torch.float32: 0, torch.bfloat16: 1, torch.float16: 2}
|
|
60
|
+
_LIB = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _candidate_paths() -> list[Path]:
|
|
64
|
+
env = os.environ.get("GDN_CPU_LIB")
|
|
65
|
+
here = Path(__file__).resolve().parent
|
|
66
|
+
pkg = here.parent
|
|
67
|
+
out = []
|
|
68
|
+
if env:
|
|
69
|
+
out.append(Path(env))
|
|
70
|
+
# Windows 上优先 .dll(.so 是 Linux 产物,Windows 载入会以 0xc000012f 崩溃),
|
|
71
|
+
# 非 Windows 依次 .so/.dylib
|
|
72
|
+
dll_names = [
|
|
73
|
+
"libbitsandbytes_cpu.dll", # CMake CPU backend output on Windows
|
|
74
|
+
"bitsandbytes_cpu.dll",
|
|
75
|
+
"bitsandbytes.dll",
|
|
76
|
+
"gdn_cpu.dll",
|
|
77
|
+
]
|
|
78
|
+
so_names = [
|
|
79
|
+
"libgdn_cpu.so",
|
|
80
|
+
"libbitsandbytes_cpu.so",
|
|
81
|
+
]
|
|
82
|
+
if os.name == "nt":
|
|
83
|
+
order = [here / n for n in dll_names] + [pkg / "libbitsandbytes_cpu.dll"]
|
|
84
|
+
else:
|
|
85
|
+
order = [here / n for n in so_names] + [here / "libgdn_cpu.dylib", pkg / "libbitsandbytes_cpu.so"]
|
|
86
|
+
out += order
|
|
87
|
+
return out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def load_native() -> ctypes.CDLL:
|
|
91
|
+
"""Load and memoize the native library holding gdn_fwd_cpu/gdn_bwd_cpu."""
|
|
92
|
+
global _LIB
|
|
93
|
+
if _LIB is not None:
|
|
94
|
+
return _LIB
|
|
95
|
+
tried = []
|
|
96
|
+
for p in _candidate_paths():
|
|
97
|
+
if not p.is_file():
|
|
98
|
+
continue
|
|
99
|
+
try:
|
|
100
|
+
lib = ctypes.CDLL(str(p))
|
|
101
|
+
except OSError as e: # pragma: no cover - environment dependent
|
|
102
|
+
tried.append(f"{p}: {e}")
|
|
103
|
+
continue
|
|
104
|
+
for fn in ("gdn_fwd_cpu", "gdn_bwd_cpu"):
|
|
105
|
+
getattr(lib, fn) # AttributeError if the symbol is missing
|
|
106
|
+
_bind_signatures(lib)
|
|
107
|
+
_LIB = lib
|
|
108
|
+
return lib
|
|
109
|
+
raise ImportError(
|
|
110
|
+
"gdn_cpu native library not found. Build it with\n"
|
|
111
|
+
" g++ -O2 -mavx2 -mfma -fopenmp -ffp-contract=off -fPIC -shared \\\n"
|
|
112
|
+
" -I bitsandbytes/csrc bitsandbytes/csrc/cpu_gdn.cpp \\\n"
|
|
113
|
+
" -o bitsandbytes/libgdn_cpu.so\n"
|
|
114
|
+
"or point GDN_CPU_LIB at it. Tried: "
|
|
115
|
+
+ ("; ".join(tried) if tried else "no candidates existed")
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _bind_signatures(lib: ctypes.CDLL) -> None:
|
|
120
|
+
fptr = ctypes.c_void_p
|
|
121
|
+
ip = ctypes.c_int
|
|
122
|
+
lib.gdn_fwd_cpu.restype = ip
|
|
123
|
+
lib.gdn_fwd_cpu.argtypes = [fptr] * 9 + [ip] * 8 # 9 ptr + B,H,T,K,V,dtype,C,layout
|
|
124
|
+
lib.gdn_bwd_cpu.restype = ip
|
|
125
|
+
lib.gdn_bwd_cpu.argtypes = [fptr] * 14 + [ip] * 8 # 14 ptr + B,H,T,K,V,dtype,C,layout
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _ptr(t: Optional[torch.Tensor]) -> int:
|
|
129
|
+
return t.data_ptr() if t is not None else 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _avail_ram_bytes() -> int:
|
|
133
|
+
"""Best-effort available RAM (no new deps). psutil 优先(跨平台),
|
|
134
|
+
Linux: MemAvailable; Windows: GlobalMemoryStatusEx; elsewhere assume 8 GB."""
|
|
135
|
+
try:
|
|
136
|
+
import psutil
|
|
137
|
+
return int(psutil.virtual_memory().available)
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
try:
|
|
141
|
+
with open("/proc/meminfo") as f:
|
|
142
|
+
for line in f:
|
|
143
|
+
if line.startswith("MemAvailable:"):
|
|
144
|
+
return int(line.split()[1]) * 1024
|
|
145
|
+
except OSError:
|
|
146
|
+
pass
|
|
147
|
+
if os.name == "nt":
|
|
148
|
+
try:
|
|
149
|
+
import ctypes
|
|
150
|
+
|
|
151
|
+
class _MS(ctypes.Structure):
|
|
152
|
+
# 完整 MEMORYSTATUSEX(缺字段 -> GlobalMemoryStatusEx 返回 FALSE)
|
|
153
|
+
_fields_ = [
|
|
154
|
+
("dwLength", ctypes.c_uint), ("dwMemoryLoad", ctypes.c_uint),
|
|
155
|
+
("ullTotalPhys", ctypes.c_uint64), ("ullAvailPhys", ctypes.c_uint64),
|
|
156
|
+
("ullTotalPageFile", ctypes.c_uint64), ("ullAvailPageFile", ctypes.c_uint64),
|
|
157
|
+
("ullTotalVirtual", ctypes.c_uint64), ("ullAvailVirtual", ctypes.c_uint64),
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
ms = _MS(dwLength=ctypes.sizeof(_MS))
|
|
161
|
+
k32 = ctypes.windll.kernel32
|
|
162
|
+
k32.GlobalMemoryStatusEx.argtypes = [ctypes.POINTER(_MS)]
|
|
163
|
+
k32.GlobalMemoryStatusEx.restype = ctypes.c_int
|
|
164
|
+
if k32.GlobalMemoryStatusEx(ctypes.byref(ms)):
|
|
165
|
+
return int(ms.ullAvailPhys)
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
return 8 << 30
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _chunk_len(B: int, H: int, T: int, K: int, V: int) -> int:
|
|
172
|
+
"""Checkpoint chunk length C.
|
|
173
|
+
|
|
174
|
+
GDN_CPU_CHUNK (if set) pins it explicitly. Otherwise minimize peak
|
|
175
|
+
TRAINING memory. Two terms compete:
|
|
176
|
+
|
|
177
|
+
- checkpoints B*H*ceil(T/C)*K*V*4 B: alive from each layer's forward
|
|
178
|
+
until that layer's backward, so during training they coexist across
|
|
179
|
+
ALL layers -> shrink with larger C (persistent term);
|
|
180
|
+
- the per-thread chunk-recompute buffer ((C+2)*K*V*4 B): alive only
|
|
181
|
+
while ONE layer's backward runs (transient term) -> grows with C.
|
|
182
|
+
|
|
183
|
+
Speed is flat in C over 32..512 (measured), so the persistent term wins:
|
|
184
|
+
take the largest C (<= 512) whose p-thread recompute buffer fits a budget
|
|
185
|
+
of 25% of available RAM (clamped to [512 MB, 4 GB]); floor 64. For
|
|
186
|
+
T=8192, H=16, K=V=128 this gives C=512 -> 16 MB checkpoints/layer vs
|
|
187
|
+
128 MB at C=64 (a 12-layer model saves ~1.3 GB).
|
|
188
|
+
"""
|
|
189
|
+
env = os.environ.get("GDN_CPU_CHUNK")
|
|
190
|
+
if env: # explicit pin (empty/garbage falls back to auto)
|
|
191
|
+
try:
|
|
192
|
+
c = int(env)
|
|
193
|
+
except ValueError:
|
|
194
|
+
c = 0
|
|
195
|
+
if c > 0:
|
|
196
|
+
return c
|
|
197
|
+
p = max(1, min(os.cpu_count() or 1, B * H))
|
|
198
|
+
row = max(4 * K * V, 4) # bytes of one K*V state row (guard: no /0)
|
|
199
|
+
budget = min(max(_avail_ram_bytes() // 4, 512 << 20), 4 << 30)
|
|
200
|
+
c = budget // (p * row) - 2
|
|
201
|
+
c = max(64, min(c, 512))
|
|
202
|
+
return min(c, max(T, 1))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class FusedRecurrentGatedDeltaRuleFunction(torch.autograd.Function):
|
|
206
|
+
"""Autograd wrapper around the C kernels.
|
|
207
|
+
|
|
208
|
+
Inputs are already fla-layout; every tensor is made C-contiguous here so
|
|
209
|
+
the raw-pointer kernel can never read strided memory as dense.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def forward(
|
|
214
|
+
ctx,
|
|
215
|
+
q: torch.Tensor, # [B,T,H,K] (pre-scaled by caller)
|
|
216
|
+
k: torch.Tensor, # [B,T,H,K]
|
|
217
|
+
v: torch.Tensor, # [B,T,H,V]
|
|
218
|
+
beta: torch.Tensor, # [B,T,H]
|
|
219
|
+
g: Optional[torch.Tensor], # [B,T,H] log decay per step (fp32-ish)
|
|
220
|
+
initial_state: Optional[torch.Tensor], # [B,H,K,V]
|
|
221
|
+
need_state_grad: bool,
|
|
222
|
+
grad_mode: Optional[bool] = None, # torch.is_grad_enabled() at apply()
|
|
223
|
+
):
|
|
224
|
+
if os.environ.get("GDN_CPU_DISABLE"):
|
|
225
|
+
raise RuntimeError("GDN_CPU_DISABLE is set")
|
|
226
|
+
lib = load_native()
|
|
227
|
+
|
|
228
|
+
B, T, H, K = q.shape
|
|
229
|
+
V = v.shape[-1]
|
|
230
|
+
dt = q.dtype
|
|
231
|
+
assert dt in _DTYPE_CODE and k.dtype == dt and v.dtype == dt, \
|
|
232
|
+
f"q/k/v must share an fp32/bf16/fp16 dtype, got {q.dtype}/{k.dtype}/{v.dtype}"
|
|
233
|
+
assert q.device.type == "cpu" and k.is_cpu and v.is_cpu
|
|
234
|
+
|
|
235
|
+
# -> kernel layout. layout 1 ([B,T,H,D] batch-contiguous, the layout
|
|
236
|
+
# model tensors already come in) is read in place with ZERO copies;
|
|
237
|
+
# layout 0 is the per-head-contiguous [B,H,T,D] packing used when any
|
|
238
|
+
# input is strided. beta/g -> fp32 [B,H,T] in both cases.
|
|
239
|
+
if q.is_contiguous() and k.is_contiguous() and v.is_contiguous():
|
|
240
|
+
qc, kc, vc, layout = q, k, v, 1
|
|
241
|
+
else:
|
|
242
|
+
qc = q.permute(0, 2, 1, 3).contiguous()
|
|
243
|
+
kc = k.permute(0, 2, 1, 3).contiguous()
|
|
244
|
+
vc = v.permute(0, 2, 1, 3).contiguous()
|
|
245
|
+
layout = 0
|
|
246
|
+
bt = beta.permute(0, 2, 1).float().contiguous()
|
|
247
|
+
gt = g.permute(0, 2, 1).float().contiguous() if g is not None \
|
|
248
|
+
else torch.zeros(B, H, T, dtype=torch.float32)
|
|
249
|
+
s0 = initial_state.float().contiguous() if initial_state is not None else None
|
|
250
|
+
|
|
251
|
+
C = _chunk_len(B, H, T, K, V)
|
|
252
|
+
nc = (T + C - 1) // C
|
|
253
|
+
o = torch.empty(B, T, H, V, dtype=dt) if layout == 1 \
|
|
254
|
+
else torch.empty(B, H, T, V, dtype=dt)
|
|
255
|
+
sf = torch.empty(B, H, K, V, dtype=torch.float32)
|
|
256
|
+
# Checkpoints are only consumed by the backward. NOTE: inside a
|
|
257
|
+
# classic autograd.Function forward, torch.is_grad_enabled() is ALWAYS
|
|
258
|
+
# False (autograd does not record here), so the decision must come
|
|
259
|
+
# from the caller's grad mode and ctx.needs_input_grad. Skipping the
|
|
260
|
+
# allocation saves B*H*nc*K*V*4 bytes whenever no graph node is
|
|
261
|
+
# created (inference under no_grad, or grad-enabled validation loops
|
|
262
|
+
# where no input requires grad - autograd builds no node there, so
|
|
263
|
+
# backward can never run).
|
|
264
|
+
will_backward = (grad_mode is not False) and any(ctx.needs_input_grad)
|
|
265
|
+
ck = torch.empty(B, H, nc, K, V, dtype=torch.float32) if will_backward else None
|
|
266
|
+
|
|
267
|
+
rc = lib.gdn_fwd_cpu(
|
|
268
|
+
_ptr(qc), _ptr(kc), _ptr(vc), _ptr(bt), _ptr(gt), _ptr(s0),
|
|
269
|
+
_ptr(o), _ptr(sf), _ptr(ck), B, H, T, K, V, _DTYPE_CODE[dt], C, layout,
|
|
270
|
+
)
|
|
271
|
+
if rc != 0:
|
|
272
|
+
raise RuntimeError(f"gdn_fwd_cpu failed with code {rc}")
|
|
273
|
+
|
|
274
|
+
# Only materialize the dtype-cast final state when the caller asked
|
|
275
|
+
# for it (fla/transformers convention: state dtype matches the input
|
|
276
|
+
# dtype); otherwise hand back the fp32 buffer - the wrapper discards
|
|
277
|
+
# it, and skipping the copy saves B*H*K*V per layer in training.
|
|
278
|
+
s_out = sf.to(dt) if need_state_grad else sf
|
|
279
|
+
ctx.save_for_backward(qc, kc, vc, bt, gt, ck)
|
|
280
|
+
ctx.gdn_meta = (B, H, T, K, V, _DTYPE_CODE[dt], C, dt,
|
|
281
|
+
need_state_grad, initial_state is not None,
|
|
282
|
+
g is not None, layout, beta.dtype,
|
|
283
|
+
g.dtype if g is not None else None)
|
|
284
|
+
return (o if layout == 1 else o.permute(0, 2, 1, 3)), s_out
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
def backward(ctx, do_: torch.Tensor, ds: Optional[torch.Tensor]):
|
|
288
|
+
lib = load_native()
|
|
289
|
+
qc, kc, vc, bt, gt, ck = ctx.saved_tensors
|
|
290
|
+
(B, H, T, K, V, dtc, C, dt, need_state_grad, had_s0, had_g, layout,
|
|
291
|
+
bdt, gdt) = ctx.gdn_meta
|
|
292
|
+
if ck is None:
|
|
293
|
+
raise RuntimeError(
|
|
294
|
+
"GDN backward called without checkpoints: the forward ran "
|
|
295
|
+
"under torch.no_grad() (or grad was disabled), so no "
|
|
296
|
+
"checkpoint tensor was saved. Re-run the forward with grad "
|
|
297
|
+
"enabled to train."
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
if layout == 1:
|
|
301
|
+
# dO in the same [B,T,H,V] batch-contiguous layout: usually the
|
|
302
|
+
# upstream grad already is that (zero copy), otherwise materialize
|
|
303
|
+
doc = do_ if (do_.is_contiguous() and do_.dtype == dt) \
|
|
304
|
+
else do_.to(dt).contiguous()
|
|
305
|
+
dq = torch.empty(B, T, H, K, dtype=dt)
|
|
306
|
+
dk = torch.empty(B, T, H, K, dtype=dt)
|
|
307
|
+
dv = torch.empty(B, T, H, V, dtype=dt)
|
|
308
|
+
else:
|
|
309
|
+
doc = do_.permute(0, 2, 1, 3).to(dt).contiguous()
|
|
310
|
+
dq = torch.empty(B, H, T, K, dtype=dt)
|
|
311
|
+
dk = torch.empty(B, H, T, K, dtype=dt)
|
|
312
|
+
dv = torch.empty(B, H, T, V, dtype=dt)
|
|
313
|
+
# grad of the (possibly dtype-cast) final state; already [B,H,K,V]
|
|
314
|
+
# kernel layout - NO permute here (only [B,T,H,*] tensors get one)
|
|
315
|
+
dsf = None
|
|
316
|
+
if ds is not None:
|
|
317
|
+
dsf = ds.float().contiguous()
|
|
318
|
+
|
|
319
|
+
dbeta = torch.empty(B, H, T, dtype=torch.float32)
|
|
320
|
+
dg = torch.empty(B, H, T, dtype=torch.float32)
|
|
321
|
+
dsi = torch.empty(B, H, K, V, dtype=torch.float32)
|
|
322
|
+
|
|
323
|
+
rc = lib.gdn_bwd_cpu(
|
|
324
|
+
_ptr(qc), _ptr(kc), _ptr(vc), _ptr(bt), _ptr(gt), _ptr(doc),
|
|
325
|
+
_ptr(dsf), _ptr(ck), _ptr(dq), _ptr(dk), _ptr(dv),
|
|
326
|
+
_ptr(dbeta), _ptr(dg), _ptr(dsi), B, H, T, K, V, dtc, C, layout,
|
|
327
|
+
)
|
|
328
|
+
if rc != 0:
|
|
329
|
+
raise RuntimeError(f"gdn_bwd_cpu failed with code {rc}")
|
|
330
|
+
|
|
331
|
+
if layout == 0: # -> [B,T,H,*]
|
|
332
|
+
dq = dq.permute(0, 2, 1, 3)
|
|
333
|
+
dk = dk.permute(0, 2, 1, 3)
|
|
334
|
+
dv = dv.permute(0, 2, 1, 3)
|
|
335
|
+
# cast the scalar-path grads to the inputs' dtypes (they may be
|
|
336
|
+
# bf16/fp16 in a real model; the kernel always emits fp32)
|
|
337
|
+
dbeta = dbeta.permute(0, 2, 1).to(bdt)
|
|
338
|
+
# g was a None input: autograd REQUIRES None here, not a tensor
|
|
339
|
+
dgt = dg.permute(0, 2, 1).to(gdt) if had_g else None
|
|
340
|
+
ds0 = dsi if had_s0 else None
|
|
341
|
+
return dq, dk, dv, dbeta, dgt, ds0, None, None # last None: grad_mode
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def fused_recurrent_gated_delta_rule(
|
|
345
|
+
q: Optional[torch.Tensor] = None,
|
|
346
|
+
k: Optional[torch.Tensor] = None,
|
|
347
|
+
v: Optional[torch.Tensor] = None,
|
|
348
|
+
beta: Optional[torch.Tensor] = None,
|
|
349
|
+
g: Optional[torch.Tensor] = None,
|
|
350
|
+
scale: Optional[float] = None,
|
|
351
|
+
initial_state: Optional[torch.Tensor] = None,
|
|
352
|
+
output_final_state: bool = False,
|
|
353
|
+
use_qk_l2norm_in_kernel: bool = False,
|
|
354
|
+
head_first: bool = False,
|
|
355
|
+
cu_seqlens: Optional[torch.Tensor] = None,
|
|
356
|
+
**kwargs,
|
|
357
|
+
):
|
|
358
|
+
"""fla-compatible fused_recurrent_gated_delta_rule for CPU tensors.
|
|
359
|
+
|
|
360
|
+
``q/k`` [B,T,H,K], ``v`` [B,T,H,V], ``beta``/``g`` [B,T,H] (g = per-step
|
|
361
|
+
log decay), ``initial_state`` [B,H,K,V]. ``scale`` multiplies q before the
|
|
362
|
+
recurrence (fla semantics); q/k l2-normalization, when requested, is done
|
|
363
|
+
with regular torch ops so its backward stays exact. ``head_first=True``
|
|
364
|
+
accepts/returns the legacy [B,H,T,D] fla layout. Returns ``(o, final_state)``
|
|
365
|
+
like fla (final_state is None unless output_final_state=True). ``r=`` is
|
|
366
|
+
accepted as an alias for ``q`` (newer fla renamed the first argument).
|
|
367
|
+
"""
|
|
368
|
+
if q is None and "r" in kwargs:
|
|
369
|
+
q = kwargs.pop("r") # fla >= 0.3 calls the query r
|
|
370
|
+
if q is None or k is None or v is None:
|
|
371
|
+
raise TypeError("fused_recurrent_gated_delta_rule() requires q, k, v tensors")
|
|
372
|
+
if initial_state is None and kwargs.get("s0") is not None:
|
|
373
|
+
initial_state = kwargs.pop("s0") # some callers use s0/h0
|
|
374
|
+
if kwargs:
|
|
375
|
+
raise TypeError(f"unexpected arguments: {sorted(kwargs)}")
|
|
376
|
+
if cu_seqlens is not None:
|
|
377
|
+
raise NotImplementedError("varlen (cu_seqlens) is not supported on the CPU path")
|
|
378
|
+
if head_first: # legacy fla layout [B,H,T,D] -> [B,T,H,D]
|
|
379
|
+
q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
|
|
380
|
+
beta = None if beta is None else beta.transpose(1, 2)
|
|
381
|
+
g = None if g is None else g.transpose(1, 2)
|
|
382
|
+
assert q.dim() == 4 and k.dim() == 4 and v.dim() == 4, \
|
|
383
|
+
f"expected [B,T,H,D], got q {tuple(q.shape)} k {tuple(k.shape)} v {tuple(v.shape)}"
|
|
384
|
+
assert q.shape == k.shape and q.shape[:3] == v.shape[:3], \
|
|
385
|
+
f"incompatible shapes q {tuple(q.shape)} k {tuple(k.shape)} v {tuple(v.shape)}"
|
|
386
|
+
if beta is None:
|
|
387
|
+
beta = torch.ones(*q.shape[:3], dtype=q.dtype, device=q.device)
|
|
388
|
+
if scale is not None:
|
|
389
|
+
try:
|
|
390
|
+
scale = float(scale)
|
|
391
|
+
except (TypeError, ValueError) as e:
|
|
392
|
+
raise TypeError(
|
|
393
|
+
f"scale must be a float, got {type(scale).__name__} (positional "
|
|
394
|
+
"arg #6 is `scale`; `initial_state` is #7)"
|
|
395
|
+
) from e
|
|
396
|
+
# l2-normalize FIRST, then apply scale: normalize() is scale-invariant,
|
|
397
|
+
# so the reverse order would silently cancel a non-1 scale (transformers'
|
|
398
|
+
# torch fallbacks do normalize-then-scale).
|
|
399
|
+
if use_qk_l2norm_in_kernel:
|
|
400
|
+
q = torch.nn.functional.normalize(q, dim=-1, eps=1e-6)
|
|
401
|
+
k = torch.nn.functional.normalize(k, dim=-1, eps=1e-6)
|
|
402
|
+
if scale is not None and scale != 1.0:
|
|
403
|
+
q = q * scale
|
|
404
|
+
|
|
405
|
+
o, s = FusedRecurrentGatedDeltaRuleFunction.apply(
|
|
406
|
+
q, k, v, beta, g, initial_state, bool(output_final_state),
|
|
407
|
+
torch.is_grad_enabled(),
|
|
408
|
+
)
|
|
409
|
+
if head_first:
|
|
410
|
+
o = o.transpose(1, 2)
|
|
411
|
+
return o, (s if output_final_state else None)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
# ---------------------------------------------------------------------------
|
|
415
|
+
# monkey-patch entries: swap in our CPU kernel, keep the original for CUDA
|
|
416
|
+
# ---------------------------------------------------------------------------
|
|
417
|
+
def patch_fla() -> bool:
|
|
418
|
+
"""Replace fla's fused_recurrent_gated_delta_rule with a CPU-dispatching
|
|
419
|
+
wrapper. Returns True if fla was found and patched."""
|
|
420
|
+
try:
|
|
421
|
+
import fla.ops.gated_delta_rule as fgr
|
|
422
|
+
except Exception:
|
|
423
|
+
return False
|
|
424
|
+
orig = getattr(fgr, "fused_recurrent_gated_delta_rule", None)
|
|
425
|
+
if orig is None or getattr(orig, "_gdn_cpu_patched", False):
|
|
426
|
+
return orig is not None
|
|
427
|
+
|
|
428
|
+
def cpu_dispatch(*args, **kw):
|
|
429
|
+
# NOTE: never use `kw.get("q") or kw.get("r")` - bool(Tensor) raises
|
|
430
|
+
# for numel > 1 ("Boolean value of Tensor is ambiguous").
|
|
431
|
+
lead = args[0] if args and isinstance(args[0], torch.Tensor) else \
|
|
432
|
+
kw.get("q", kw.get("r"))
|
|
433
|
+
if isinstance(lead, torch.Tensor) and lead.is_cpu:
|
|
434
|
+
return fused_recurrent_gated_delta_rule(*args, **kw)
|
|
435
|
+
return orig(*args, **kw)
|
|
436
|
+
|
|
437
|
+
cpu_dispatch._gdn_cpu_patched = True
|
|
438
|
+
fgr.fused_recurrent_gated_delta_rule = cpu_dispatch
|
|
439
|
+
# some versions re-export at package level
|
|
440
|
+
try:
|
|
441
|
+
import fla.ops as fo
|
|
442
|
+
if getattr(fo, "fused_recurrent_gated_delta_rule", None) is orig:
|
|
443
|
+
fo.fused_recurrent_gated_delta_rule = cpu_dispatch
|
|
444
|
+
except Exception:
|
|
445
|
+
pass
|
|
446
|
+
return True
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def patch_transformers() -> bool:
|
|
450
|
+
"""Patch transformers' Qwen3-Next/Qwen3.5 GatedDeltaNet to use the CPU kernel.
|
|
451
|
+
|
|
452
|
+
Returns True if at least one module was found and patched.
|
|
453
|
+
|
|
454
|
+
Covers the symbols a GatedDeltaNet forward can dispatch to:
|
|
455
|
+
- ``fused_recurrent_gated_delta_rule`` (older transformers / some forks);
|
|
456
|
+
- ``torch_recurrent_gated_delta_rule`` / ``torch_chunk_gated_delta_rule``
|
|
457
|
+
(transformers 5.15+): both are module attributes that the model's
|
|
458
|
+
``forward`` looks up at call time, so replacing them redirects the
|
|
459
|
+
torch fallback paths to the fused kernel. The recurrent kernel is
|
|
460
|
+
mathematically identical to the chunked scan, so substituting it for
|
|
461
|
+
the chunk path is exact (fla guarantees the same for its own pair).
|
|
462
|
+
"""
|
|
463
|
+
targets = [
|
|
464
|
+
"transformers.models.qwen3_next.modeling_qwen3_next",
|
|
465
|
+
"transformers.models.qwen3_5.modeling_qwen3_5",
|
|
466
|
+
]
|
|
467
|
+
import importlib
|
|
468
|
+
patched = False
|
|
469
|
+
for name in targets:
|
|
470
|
+
try:
|
|
471
|
+
mod = importlib.import_module(name)
|
|
472
|
+
except Exception:
|
|
473
|
+
continue
|
|
474
|
+
|
|
475
|
+
def make_dispatch(orig):
|
|
476
|
+
def cpu_dispatch(*args, **kw):
|
|
477
|
+
# `or` on tensors raises for numel > 1; resolve explicitly
|
|
478
|
+
q = args[0] if args and isinstance(args[0], torch.Tensor) else \
|
|
479
|
+
kw.get("q", kw.get("r"))
|
|
480
|
+
if not (isinstance(q, torch.Tensor) and q.is_cpu):
|
|
481
|
+
return orig(*args, **kw)
|
|
482
|
+
# varlen (cu_seqlens) is not implemented on the CPU kernel
|
|
483
|
+
if kw.get("cu_seqlens") is not None:
|
|
484
|
+
return orig(*args, **kw)
|
|
485
|
+
# forward passes a grab-bag of extra kwargs (attn masks,
|
|
486
|
+
# cache, etc.) - hand the kernel only what it understands
|
|
487
|
+
supported = ("q", "r", "k", "v", "beta", "g", "scale",
|
|
488
|
+
"initial_state", "s0", "output_final_state",
|
|
489
|
+
"use_qk_l2norm_in_kernel", "head_first")
|
|
490
|
+
clean = {k: v for k, v in kw.items() if k in supported}
|
|
491
|
+
# transformers' torch fallbacks multiply q by 1/sqrt(K) INSIDE
|
|
492
|
+
# the function (the caller never passes `scale`); our kernel
|
|
493
|
+
# does not scale unless asked, so replicate that convention
|
|
494
|
+
# here, otherwise the patched output would be off by 1/sqrt(K)
|
|
495
|
+
if "scale" not in clean:
|
|
496
|
+
k_t = args[1] if len(args) > 1 and isinstance(args[1], torch.Tensor) \
|
|
497
|
+
else kw.get("k")
|
|
498
|
+
if k_t is not None:
|
|
499
|
+
clean["scale"] = k_t.shape[-1] ** -0.5
|
|
500
|
+
return fused_recurrent_gated_delta_rule(*args, **clean)
|
|
501
|
+
cpu_dispatch._gdn_cpu_patched = True
|
|
502
|
+
return cpu_dispatch
|
|
503
|
+
|
|
504
|
+
for attr in ("fused_recurrent_gated_delta_rule",
|
|
505
|
+
"torch_recurrent_gated_delta_rule",
|
|
506
|
+
"torch_chunk_gated_delta_rule"):
|
|
507
|
+
orig = getattr(mod, attr, None)
|
|
508
|
+
if orig is None or getattr(orig, "_gdn_cpu_patched", False):
|
|
509
|
+
continue
|
|
510
|
+
setattr(mod, attr, make_dispatch(orig))
|
|
511
|
+
patched = True
|
|
512
|
+
return patched
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
# ---------------------------------------------------------------------------
|
|
516
|
+
# self-test: python -m bitsandbytes.gdn_cpu
|
|
517
|
+
# ---------------------------------------------------------------------------
|
|
518
|
+
def _reference(q, k, v, beta, g, s0, do_, ds_final):
|
|
519
|
+
"""Naive double-precision per-step reference with exact autograd grads."""
|
|
520
|
+
q64, k64, v64 = (t.detach().double().requires_grad_(True) for t in (q, k, v))
|
|
521
|
+
b64 = beta.detach().double().requires_grad_(True)
|
|
522
|
+
g64 = g.detach().double().requires_grad_(True) if g is not None else None
|
|
523
|
+
s64 = s0.detach().double().requires_grad_(True) if s0 is not None \
|
|
524
|
+
else torch.zeros(q.shape[0], q.shape[2], q.shape[-1], v.shape[-1],
|
|
525
|
+
dtype=torch.float64, requires_grad=True)
|
|
526
|
+
B, T, H, K = q.shape
|
|
527
|
+
V = v.shape[-1]
|
|
528
|
+
S = s64.clone()
|
|
529
|
+
outs = []
|
|
530
|
+
for t in range(T):
|
|
531
|
+
eg = g64[:, t].exp() if g64 is not None else 1.0
|
|
532
|
+
S = S * eg[:, :, None, None]
|
|
533
|
+
w = torch.einsum("bhi,bhij->bhj", k64[:, t], S)
|
|
534
|
+
u = v64[:, t] - w
|
|
535
|
+
up = b64[:, t].unsqueeze(-1) * u
|
|
536
|
+
S = S + torch.einsum("bhi,bhj->bhij", k64[:, t], up)
|
|
537
|
+
outs.append(torch.einsum("bhi,bhij->bhj", q64[:, t], S))
|
|
538
|
+
o = torch.stack(outs, dim=1)
|
|
539
|
+
loss = (o * do_.detach().double()).sum()
|
|
540
|
+
if ds_final is not None:
|
|
541
|
+
loss = loss + (S * ds_final.double()).sum()
|
|
542
|
+
grads = torch.autograd.grad(loss, [q64, k64, v64, b64] + ([g64] if g64 is not None else []) + [s64],
|
|
543
|
+
allow_unused=True)
|
|
544
|
+
return o.detach(), S.detach(), grads
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _selftest() -> int:
|
|
548
|
+
torch.manual_seed(7)
|
|
549
|
+
dev = torch.device("cpu")
|
|
550
|
+
B, T, H, K, V = 2, 33, 3, 16, 24
|
|
551
|
+
fails = 0
|
|
552
|
+
|
|
553
|
+
for dt, tol in ((torch.float32, 2e-3), (torch.bfloat16, 4e-2), (torch.float16, 4e-2)):
|
|
554
|
+
q = torch.randn(B, T, H, K, dtype=dt) * K ** -0.5
|
|
555
|
+
k = torch.randn(B, T, H, K, dtype=dt) * K ** -0.5
|
|
556
|
+
v = torch.randn(B, T, H, V, dtype=dt) * 0.3
|
|
557
|
+
beta = torch.rand(B, T, H, dtype=torch.float32) * 0.8 + 0.2
|
|
558
|
+
g = -torch.rand(B, T, H, dtype=torch.float32) * 1.5 - 0.05
|
|
559
|
+
s0 = torch.randn(B, H, K, V, dtype=dt) * 0.2
|
|
560
|
+
do_ = torch.randn(B, T, H, V, dtype=torch.float32)
|
|
561
|
+
dsf = torch.randn(B, H, K, V, dtype=torch.float32) * 0.5
|
|
562
|
+
|
|
563
|
+
o, ht = fused_recurrent_gated_delta_rule(
|
|
564
|
+
q, k, v, beta, g, initial_state=s0, output_final_state=True)
|
|
565
|
+
o_ref, ht_ref, grads_ref = _reference(
|
|
566
|
+
q, k, v, beta, g, s0, do_, dsf)
|
|
567
|
+
|
|
568
|
+
err_o = (o.float() - o_ref.float()).abs().max().item()
|
|
569
|
+
err_s = (ht.float() - ht_ref.float()).abs().max().item()
|
|
570
|
+
ok = err_o < tol and err_s < tol
|
|
571
|
+
fails += not ok
|
|
572
|
+
print(f" {str(dt):15s} fwd: |dO|max {err_o:.2e} |dS|max {err_s:.2e} {'ok' if ok else 'FAIL'}")
|
|
573
|
+
|
|
574
|
+
# backward vs double-precision autograd on the naive reference
|
|
575
|
+
qa = q.clone().detach().requires_grad_(True)
|
|
576
|
+
ka = k.clone().detach().requires_grad_(True)
|
|
577
|
+
va = v.clone().detach().requires_grad_(True)
|
|
578
|
+
ba = beta.clone().requires_grad_(True)
|
|
579
|
+
ga = g.clone().requires_grad_(True)
|
|
580
|
+
sa = s0.clone().detach().requires_grad_(True)
|
|
581
|
+
o3, ht3 = FusedRecurrentGatedDeltaRuleFunction.apply(
|
|
582
|
+
qa, ka, va, ba, ga, sa, True)
|
|
583
|
+
loss3 = (o3.float() * do_).sum() + (ht3.float() * dsf).sum()
|
|
584
|
+
loss3.backward()
|
|
585
|
+
|
|
586
|
+
rq, rk, rv, rb, rg, rs = grads_ref
|
|
587
|
+
rel = lambda a, b: ((a.float() - b.float()).abs().max() /
|
|
588
|
+
(b.float().abs().max() + 1e-6)).item()
|
|
589
|
+
e_q, e_k, e_v = rel(qa.grad, rq), rel(ka.grad, rk), rel(va.grad, rv)
|
|
590
|
+
e_b, e_g, e_s = rel(ba.grad, rb), rel(ga.grad, rg), rel(sa.grad, rs)
|
|
591
|
+
tolg = 3e-2 if dt != torch.float32 else 3e-3
|
|
592
|
+
okg = max(e_q, e_k, e_v, e_b, e_g, e_s) < tolg
|
|
593
|
+
fails += not okg
|
|
594
|
+
print(f" {str(dt):15s} bwd rel: dq {e_q:.2e} dk {e_k:.2e} dv {e_v:.2e} "
|
|
595
|
+
f"dbeta {e_b:.2e} dg {e_g:.2e} ds0 {e_s:.2e} {'ok' if okg else 'FAIL'}")
|
|
596
|
+
|
|
597
|
+
print("selftest", "PASSED" if fails == 0 else f"FAILED ({fails})")
|
|
598
|
+
return 1 if fails else 0
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
if __name__ == "__main__":
|
|
602
|
+
raise SystemExit(_selftest())
|
|
Binary file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
|
2
|
+
#
|
|
3
|
+
# This source code is licensed under the MIT license found in the
|
|
4
|
+
# LICENSE file in the root directory of this source tree.
|
|
5
|
+
from .modules import (
|
|
6
|
+
Embedding,
|
|
7
|
+
Embedding4bit,
|
|
8
|
+
Embedding8bit,
|
|
9
|
+
EmbeddingFP4,
|
|
10
|
+
EmbeddingNF4,
|
|
11
|
+
Int8Params,
|
|
12
|
+
Linear4bit,
|
|
13
|
+
Linear8bitLt,
|
|
14
|
+
LinearFP4,
|
|
15
|
+
LinearNF4,
|
|
16
|
+
OutlierAwareLinear,
|
|
17
|
+
Params4bit,
|
|
18
|
+
StableEmbedding,
|
|
19
|
+
)
|