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.
Files changed (56) hide show
  1. bitsandbytes/__init__.py +81 -0
  2. bitsandbytes/__main__.py +4 -0
  3. bitsandbytes/_ops.py +526 -0
  4. bitsandbytes/autograd/__init__.py +0 -0
  5. bitsandbytes/autograd/_functions.py +491 -0
  6. bitsandbytes/backends/__init__.py +0 -0
  7. bitsandbytes/backends/cpu/__init__.py +0 -0
  8. bitsandbytes/backends/cpu/ops.py +805 -0
  9. bitsandbytes/backends/cuda/__init__.py +0 -0
  10. bitsandbytes/backends/cuda/ops.py +1212 -0
  11. bitsandbytes/backends/default/__init__.py +0 -0
  12. bitsandbytes/backends/default/ops.py +632 -0
  13. bitsandbytes/backends/hpu/__init__.py +0 -0
  14. bitsandbytes/backends/hpu/ops.py +53 -0
  15. bitsandbytes/backends/mps/__init__.py +0 -0
  16. bitsandbytes/backends/mps/ops.py +277 -0
  17. bitsandbytes/backends/triton/__init__.py +0 -0
  18. bitsandbytes/backends/triton/kernels_4bit.py +577 -0
  19. bitsandbytes/backends/triton/kernels_8bit_quant.py +195 -0
  20. bitsandbytes/backends/triton/kernels_optim.py +1177 -0
  21. bitsandbytes/backends/triton/ops.py +304 -0
  22. bitsandbytes/backends/utils.py +94 -0
  23. bitsandbytes/backends/xpu/__init__.py +0 -0
  24. bitsandbytes/backends/xpu/ops.py +305 -0
  25. bitsandbytes/cextension.py +405 -0
  26. bitsandbytes/consts.py +12 -0
  27. bitsandbytes/cuda_specs.py +111 -0
  28. bitsandbytes/diagnostics/__init__.py +0 -0
  29. bitsandbytes/diagnostics/cuda.py +193 -0
  30. bitsandbytes/diagnostics/main.py +134 -0
  31. bitsandbytes/diagnostics/utils.py +12 -0
  32. bitsandbytes/functional.py +1836 -0
  33. bitsandbytes/gdn_cpu.py +602 -0
  34. bitsandbytes/libbitsandbytes_cpu.dll +0 -0
  35. bitsandbytes/nn/__init__.py +19 -0
  36. bitsandbytes/nn/modules.py +1220 -0
  37. bitsandbytes/nn/parametrize.py +206 -0
  38. bitsandbytes/optim/__init__.py +22 -0
  39. bitsandbytes/optim/adagrad.py +187 -0
  40. bitsandbytes/optim/adam.py +346 -0
  41. bitsandbytes/optim/adamw.py +337 -0
  42. bitsandbytes/optim/ademamix.py +410 -0
  43. bitsandbytes/optim/lamb.py +190 -0
  44. bitsandbytes/optim/lars.py +259 -0
  45. bitsandbytes/optim/lion.py +266 -0
  46. bitsandbytes/optim/optimizer.py +756 -0
  47. bitsandbytes/optim/rmsprop.py +170 -0
  48. bitsandbytes/optim/sgd.py +152 -0
  49. bitsandbytes/py.typed +0 -0
  50. bitsandbytes/utils.py +208 -0
  51. bitsandbytes/vcomp140.dll +0 -0
  52. bitsandbytes_cpu_fork-0.50.2.dev0.dist-info/METADATA +346 -0
  53. bitsandbytes_cpu_fork-0.50.2.dev0.dist-info/RECORD +56 -0
  54. bitsandbytes_cpu_fork-0.50.2.dev0.dist-info/WHEEL +5 -0
  55. bitsandbytes_cpu_fork-0.50.2.dev0.dist-info/licenses/LICENSE +21 -0
  56. bitsandbytes_cpu_fork-0.50.2.dev0.dist-info/top_level.txt +1 -0
@@ -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
+ )