bitsandbytes-cpu-fork 0.50.2.dev2__py3-none-win_amd64.whl → 0.50.2.dev4__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 (54) hide show
  1. bitsandbytes/__init__.py +1 -1
  2. bitsandbytes/__main__.py +4 -4
  3. bitsandbytes/_cpu_caps.py +110 -0
  4. bitsandbytes/_ops.py +526 -526
  5. bitsandbytes/autograd/_functions.py +491 -491
  6. bitsandbytes/backends/cpu/ops.py +805 -805
  7. bitsandbytes/backends/cuda/ops.py +1212 -1212
  8. bitsandbytes/backends/default/ops.py +632 -632
  9. bitsandbytes/backends/hpu/ops.py +53 -53
  10. bitsandbytes/backends/mps/ops.py +277 -277
  11. bitsandbytes/backends/triton/kernels_4bit.py +583 -577
  12. bitsandbytes/backends/triton/kernels_8bit_quant.py +195 -195
  13. bitsandbytes/backends/triton/kernels_optim.py +1177 -1177
  14. bitsandbytes/backends/triton/ops.py +304 -304
  15. bitsandbytes/backends/utils.py +94 -94
  16. bitsandbytes/backends/xpu/ops.py +305 -305
  17. bitsandbytes/cextension.py +447 -405
  18. bitsandbytes/cli.py +50 -50
  19. bitsandbytes/consts.py +12 -12
  20. bitsandbytes/cuda_specs.py +111 -111
  21. bitsandbytes/detect_cpu.py +189 -189
  22. bitsandbytes/diagnostics/cuda.py +193 -193
  23. bitsandbytes/diagnostics/main.py +134 -134
  24. bitsandbytes/diagnostics/utils.py +12 -12
  25. bitsandbytes/disk_balancer.py +1276 -1224
  26. bitsandbytes/efst.py +777 -0
  27. bitsandbytes/functional.py +1836 -1836
  28. bitsandbytes/gdn_cpu.py +616 -602
  29. bitsandbytes/libbitsandbytes_cpu.dll +0 -0
  30. bitsandbytes/nn/__init__.py +19 -19
  31. bitsandbytes/nn/modules.py +1220 -1220
  32. bitsandbytes/nn/parametrize.py +206 -206
  33. bitsandbytes/optim/__init__.py +0 -4
  34. bitsandbytes/optim/adagrad.py +187 -187
  35. bitsandbytes/optim/adam.py +346 -346
  36. bitsandbytes/optim/adamw.py +337 -337
  37. bitsandbytes/optim/ademamix.py +410 -410
  38. bitsandbytes/optim/lamb.py +190 -190
  39. bitsandbytes/optim/lars.py +259 -259
  40. bitsandbytes/optim/lion.py +266 -266
  41. bitsandbytes/optim/optimizer.py +756 -756
  42. bitsandbytes/optim/rmsprop.py +170 -170
  43. bitsandbytes/optim/sgd.py +152 -152
  44. bitsandbytes/torch_cpu_kit.py +468 -468
  45. bitsandbytes/utils.py +208 -208
  46. bitsandbytes_cpu_cli.py +1012 -949
  47. {bitsandbytes_cpu_fork-0.50.2.dev2.dist-info → bitsandbytes_cpu_fork-0.50.2.dev4.dist-info}/METADATA +12 -4
  48. bitsandbytes_cpu_fork-0.50.2.dev4.dist-info/RECORD +64 -0
  49. {bitsandbytes_cpu_fork-0.50.2.dev2.dist-info → bitsandbytes_cpu_fork-0.50.2.dev4.dist-info}/licenses/LICENSE +21 -21
  50. bitsandbytes/optim/adamw4bit.py +0 -183
  51. bitsandbytes_cpu_fork-0.50.2.dev2.dist-info/RECORD +0 -63
  52. {bitsandbytes_cpu_fork-0.50.2.dev2.dist-info → bitsandbytes_cpu_fork-0.50.2.dev4.dist-info}/WHEEL +0 -0
  53. {bitsandbytes_cpu_fork-0.50.2.dev2.dist-info → bitsandbytes_cpu_fork-0.50.2.dev4.dist-info}/entry_points.txt +0 -0
  54. {bitsandbytes_cpu_fork-0.50.2.dev2.dist-info → bitsandbytes_cpu_fork-0.50.2.dev4.dist-info}/top_level.txt +0 -0
@@ -1,805 +1,805 @@
1
- from collections.abc import Sequence
2
- import ctypes as ct
3
- import logging
4
- import math
5
- from math import prod
6
- from typing import Optional
7
-
8
- import torch
9
-
10
- from bitsandbytes.functional import get_ptr, has_avx512bf16
11
-
12
- from ..._ops import register_kernel
13
- from ...cextension import ErrorHandlerMockBNBNativeLibrary, lib
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
- # 256 项 fp32 8-bit 码表缓存(见 _gemm_8bit_cpu 的说明)
18
- _GEMM_8BIT_CODE = None
19
-
20
- _has_avx512 = torch.backends.cpu.get_cpu_capability() == "AVX512"
21
-
22
- # torch._int_mm for s8@s8->s32 is supported on CPU from torch 2.4+.
23
- # However, we can overflow if we use this without AVX512_VNNI support.
24
- # This is fixed in torch 2.6+, so we set this as the minimum to be safe.
25
- # For more information: https://github.com/pytorch/pytorch/pull/136942
26
- #
27
- # Without AVX-512 (including aarch64), torch._int_mm uses a scalar fallback
28
- # that is much slower than fp32 matmul. Only use it when AVX-512 is available.
29
- if torch.__version__ >= (2, 6) and _has_avx512:
30
-
31
- @register_kernel("bitsandbytes::int8_linear_matmul", "cpu")
32
- def _(A: torch.Tensor, B: torch.Tensor):
33
- return torch._int_mm(
34
- A.reshape(-1, A.shape[-1]),
35
- B.t(),
36
- ).reshape(*A.shape[:-1], B.shape[0])
37
-
38
-
39
- if not isinstance(lib, ErrorHandlerMockBNBNativeLibrary):
40
-
41
- @register_kernel("bitsandbytes::quantize_blockwise", "cpu")
42
- def _(A: torch.Tensor, code: torch.Tensor, blocksize: int) -> tuple[torch.Tensor, torch.Tensor]:
43
- A = A.contiguous()
44
- n = A.numel()
45
- blocks = -(n // -blocksize)
46
-
47
- absmax = torch.empty((blocks,), device=A.device, dtype=torch.float32)
48
- out = torch.empty(A.shape, device=A.device, dtype=torch.uint8)
49
-
50
- if A.dtype == torch.float32:
51
- lib.cquantize_blockwise_cpu_fp32(
52
- get_ptr(code),
53
- get_ptr(A),
54
- get_ptr(absmax),
55
- get_ptr(out),
56
- ct.c_longlong(blocksize),
57
- ct.c_longlong(n),
58
- )
59
- elif A.dtype == torch.bfloat16:
60
- lib.cquantize_blockwise_cpu_bf16(
61
- get_ptr(code),
62
- get_ptr(A),
63
- get_ptr(absmax),
64
- get_ptr(out),
65
- ct.c_longlong(blocksize),
66
- ct.c_longlong(n),
67
- )
68
- elif A.dtype == torch.float16:
69
- lib.cquantize_blockwise_cpu_fp16(
70
- get_ptr(code),
71
- get_ptr(A),
72
- get_ptr(absmax),
73
- get_ptr(out),
74
- ct.c_longlong(blocksize),
75
- ct.c_longlong(n),
76
- )
77
- else:
78
- # Generic fallback for other dtypes
79
- A_flat = A.reshape(n).float()
80
- rem = n % blocksize
81
- has_rem = rem > 0
82
- A_com = A_flat[: n - rem]
83
- A_com_reshaped = A_com.reshape(n // blocksize, blocksize)
84
- absmax[: blocks - has_rem] = torch.abs(A_com_reshaped).max(dim=-1)[0]
85
- scaled_A = torch.clamp(A_com_reshaped * (1 / absmax[: blocks - has_rem].view(-1, 1)), -1, 1)
86
- scaled_A = scaled_A.reshape(-1)
87
- if has_rem:
88
- absmax[-1] = torch.abs(A_flat[n - rem :]).max()
89
- scaled_A_rem = torch.clamp(A_flat[n - rem :] * (1 / absmax[-1]), -1, 1)
90
- scaled_A = torch.cat([scaled_A, scaled_A_rem], dim=0)
91
-
92
- diff = torch.abs(scaled_A.unsqueeze(-1) - code.to(scaled_A.device))
93
- out = torch.argmin(diff, dim=-1).to(torch.uint8).to(scaled_A.device).reshape(A.shape)
94
-
95
- return out, absmax
96
-
97
- @register_kernel("bitsandbytes::dequantize_blockwise", "cpu")
98
- def _(
99
- A: torch.Tensor, absmax: torch.Tensor, code: torch.Tensor, blocksize: int, dtype: torch.dtype
100
- ) -> torch.Tensor:
101
- A = A.contiguous()
102
- out = torch.empty_like(A, dtype=dtype)
103
- if dtype == torch.float32:
104
- lib.cdequantize_blockwise_cpu_fp32(
105
- get_ptr(code),
106
- get_ptr(A),
107
- get_ptr(absmax),
108
- get_ptr(out),
109
- ct.c_longlong(blocksize),
110
- ct.c_longlong(A.numel()),
111
- )
112
- elif dtype == torch.bfloat16:
113
- lib.cdequantize_blockwise_cpu_bf16(
114
- get_ptr(code),
115
- get_ptr(A),
116
- get_ptr(absmax),
117
- get_ptr(out),
118
- ct.c_longlong(blocksize),
119
- ct.c_longlong(A.numel()),
120
- )
121
- elif dtype == torch.float16:
122
- lib.cdequantize_blockwise_cpu_fp16(
123
- get_ptr(code),
124
- get_ptr(A),
125
- get_ptr(absmax),
126
- get_ptr(out),
127
- ct.c_longlong(blocksize),
128
- ct.c_longlong(A.numel()),
129
- )
130
- else:
131
- out = code[A.reshape(-1).int()]
132
- blocks = out.shape[-1] // blocksize
133
- res = out.shape[-1] % blocksize
134
- if res != 0:
135
- out = torch.nn.functional.pad(out, (0, blocksize - res), mode="constant", value=0)
136
- out = (out.view(-1, blocksize) * absmax.view(-1, 1)).to(dtype).reshape(-1)
137
- out = out[: blocks * blocksize + res]
138
- out = out.reshape(A.shape)
139
-
140
- return out
141
-
142
- @register_kernel("bitsandbytes::dequantize_4bit", "cpu")
143
- def _(
144
- A: torch.Tensor,
145
- absmax: torch.Tensor,
146
- blocksize: int,
147
- quant_type: str,
148
- shape: Sequence[int],
149
- dtype: torch.dtype,
150
- ) -> torch.Tensor:
151
- # Fallback as AVX512 implementation has accuracy issues with blocksize >= 2048.
152
- # Note: this is not a common use case.
153
- avx512_fallback = _has_avx512 and blocksize >= 2048
154
-
155
- # Odd shape is not supported by this kernel; fallback to generic implementation
156
- shape_fallback = shape[-1] % 2 != 0
157
-
158
- if avx512_fallback or shape_fallback:
159
- from ..default.ops import _dequantize_4bit_compute
160
- from ..utils import _get_4bit_code
161
-
162
- if A.dtype != torch.uint8:
163
- A = A.view(torch.uint8)
164
- code = _get_4bit_code(quant_type, A.device)
165
- return _dequantize_4bit_compute(A.reshape(-1), absmax, code, blocksize, shape, dtype)
166
-
167
- # Enable non uint8 dtype
168
- if A.dtype != torch.uint8:
169
- A = A.view(torch.uint8)
170
-
171
- # TODO: support half precision absmax
172
- if absmax.dtype != torch.float32:
173
- absmax = absmax.float()
174
-
175
- if len(shape) == 1:
176
- shape = (1, shape[0])
177
-
178
- m = prod(shape[:-1])
179
- n = shape[-1]
180
-
181
- A = A.reshape(m, n // 2)
182
- out = torch.empty(shape, dtype=dtype, device=A.device)
183
-
184
- if quant_type == "fp4":
185
- if dtype == torch.float32:
186
- lib.cdequantize_blockwise_cpu_fp4_fp32(
187
- get_ptr(A),
188
- get_ptr(absmax),
189
- get_ptr(out),
190
- ct.c_longlong(blocksize),
191
- ct.c_longlong(m),
192
- ct.c_longlong(n),
193
- )
194
- elif dtype == torch.bfloat16:
195
- lib.cdequantize_blockwise_cpu_fp4_bf16(
196
- get_ptr(A),
197
- get_ptr(absmax),
198
- get_ptr(out),
199
- ct.c_longlong(blocksize),
200
- ct.c_longlong(m),
201
- ct.c_longlong(n),
202
- )
203
- elif dtype == torch.float16:
204
- lib.cdequantize_blockwise_cpu_fp4_fp16(
205
- get_ptr(A),
206
- get_ptr(absmax),
207
- get_ptr(out),
208
- ct.c_longlong(blocksize),
209
- ct.c_longlong(m),
210
- ct.c_longlong(n),
211
- )
212
- elif quant_type == "nf4":
213
- if dtype == torch.float32:
214
- lib.cdequantize_blockwise_cpu_nf4_fp32(
215
- get_ptr(A),
216
- get_ptr(absmax),
217
- get_ptr(out),
218
- ct.c_longlong(blocksize),
219
- ct.c_longlong(m),
220
- ct.c_longlong(n),
221
- )
222
- elif dtype == torch.bfloat16:
223
- lib.cdequantize_blockwise_cpu_nf4_bf16(
224
- get_ptr(A),
225
- get_ptr(absmax),
226
- get_ptr(out),
227
- ct.c_longlong(blocksize),
228
- ct.c_longlong(m),
229
- ct.c_longlong(n),
230
- )
231
- elif dtype == torch.float16:
232
- lib.cdequantize_blockwise_cpu_nf4_fp16(
233
- get_ptr(A),
234
- get_ptr(absmax),
235
- get_ptr(out),
236
- ct.c_longlong(blocksize),
237
- ct.c_longlong(m),
238
- ct.c_longlong(n),
239
- )
240
- else:
241
- raise ValueError
242
-
243
- return out
244
-
245
- # NOTE 2026-09-22: the registration below used to live INSIDE `if has_avx512bf16():`,
246
- # which meant AVX2-only CPUs (Zen2 4500U / Comet Lake i5 / EPYC Zen2-3) never
247
- # registered a CPU kernel for gemv_4bit at all. They silently fell through to the
248
- # default backend, which dequantizes the whole weight matrix and then runs a dense
249
- # linear -- measured 12.3 ms per call vs 1.16 ms for the fused kernel below (10.6x).
250
- # The guard is therefore narrowed to cover only the AVX512-BF16-only HuggingFace
251
- # kernel load; the fused registration is unconditional.
252
- gemm_4bit_forward_kernel = None
253
- if has_avx512bf16():
254
- gemm_4bit_forward_kernel = None
255
- try:
256
- from kernels import get_kernel
257
-
258
- gemm_4bit_forward_kernel = get_kernel(
259
- "kernels-community/quantization-bitsandbytes", version=1
260
- ).gemm_4bit_forward
261
- except Exception as exc: # pragma: no cover - best effort fallback
262
- gemm_4bit_forward_kernel = None
263
- logger.warning(
264
- "Failed to load CPU gemm_4bit_forward from kernels-community: %s. Please make sure you already `pip install kernels` and the kernels >= 0.11.1",
265
- exc,
266
- )
267
-
268
- @register_kernel("bitsandbytes::gemv_4bit", "cpu")
269
- def _(
270
- A: torch.Tensor,
271
- B: torch.Tensor,
272
- shapeB: Sequence[int],
273
- absmax: torch.Tensor,
274
- code: torch.Tensor,
275
- blocksize: int,
276
- ) -> torch.Tensor:
277
- if B.dtype != torch.uint8:
278
- B = B.contiguous().view(torch.uint8)
279
- dtype = A.dtype
280
- A_orig = A
281
- absmax_orig = absmax
282
- quant_type = "fp4" if code[1] > 0 else "nf4"
283
- # cpu fused op only support bf16 for now.
284
- if dtype != torch.bfloat16:
285
- A = A.to(torch.bfloat16)
286
- if absmax.dtype != torch.bfloat16:
287
- absmax = absmax.to(torch.bfloat16)
288
-
289
- final_out_shape = (*A.shape[:-1], shapeB[0])
290
- A = A.reshape(-1, A.shape[-1])
291
- out_shape = (*A.shape[:-1], shapeB[0])
292
- if gemm_4bit_forward_kernel is not None:
293
- quant_type_num = 1 if quant_type == "fp4" else 0
294
- # C++ kernel expects weight shape (N, K_packed), ensure 2D contiguous
295
- B_2d = B.reshape(shapeB[0], -1).contiguous()
296
- out = gemm_4bit_forward_kernel(A, B_2d, absmax, blocksize, quant_type_num)
297
- else:
298
- # AVX2 回退:用所有 CPU 构建都导出的 cgemv_4bit_inference_cpu_* 融合内核
299
- # (lib.gemv_4bit_inference_cpu_fp4/nf4_bf16 只存在于 AVX512+BF16 构建,
300
- # AVX2 机器走到会 AttributeError;data_type: FP4=1 / NF4=2)
301
- A_c = A_orig.reshape(-1, A_orig.shape[-1]).contiguous()
302
- B_c = B.reshape(shapeB[0], -1).contiguous()
303
- absmax_c = absmax_orig.float().contiguous()
304
- data_type_num = 1 if quant_type == "fp4" else 2
305
- M, K = A_c.shape[0], A_c.shape[1]
306
- N = shapeB[0]
307
- out = torch.empty(out_shape, dtype=A_c.dtype, device=A_c.device)
308
- if dtype == torch.float32:
309
- lib.cgemv_4bit_inference_cpu_fp32(
310
- get_ptr(A_c), get_ptr(B_c), get_ptr(absmax_c), get_ptr(out),
311
- ct.c_longlong(M), ct.c_longlong(N), ct.c_longlong(K),
312
- ct.c_longlong(A_c.stride(0)), ct.c_longlong(K // 2),
313
- ct.c_longlong(out.stride(0)), ct.c_longlong(blocksize),
314
- ct.c_int(data_type_num),
315
- )
316
- elif dtype == torch.bfloat16:
317
- lib.cgemv_4bit_inference_cpu_bf16(
318
- get_ptr(A_c), get_ptr(B_c), get_ptr(absmax_c), get_ptr(out),
319
- ct.c_longlong(M), ct.c_longlong(N), ct.c_longlong(K),
320
- ct.c_longlong(A_c.stride(0)), ct.c_longlong(K // 2),
321
- ct.c_longlong(out.stride(0)), ct.c_longlong(blocksize),
322
- ct.c_int(data_type_num),
323
- )
324
- else:
325
- lib.cgemv_4bit_inference_cpu_fp16(
326
- get_ptr(A_c), get_ptr(B_c), get_ptr(absmax_c), get_ptr(out),
327
- ct.c_longlong(M), ct.c_longlong(N), ct.c_longlong(K),
328
- ct.c_longlong(A_c.stride(0)), ct.c_longlong(K // 2),
329
- ct.c_longlong(out.stride(0)), ct.c_longlong(blocksize),
330
- ct.c_int(data_type_num),
331
- )
332
-
333
- if dtype != torch.bfloat16:
334
- out = out.to(dtype)
335
-
336
- return out.reshape(final_out_shape)
337
-
338
- # ---- gemm_4bit: CPU 注册(2026-09-22 新增)----
339
- # 此前这个 op 【没有 cpu 注册】,于是退到 default 后端做"反量化 + 稠密线性"。
340
- # autograd/_functions.py:324/334/462/472 调的就是它 ⇒ 真实模型里
341
- # 【每一次批量 4-bit 线性前向】都在走慢路径。实测(K=N=4096):
342
- # M=1 gemm_4bit 14.483 ms vs 融合内核 1.448 ms ⇒ 10.00x
343
- # M=8 gemm_4bit 32.494 ms vs 融合内核 4.124 ms ⇒ 7.88x
344
- # M=64 gemm_4bit 38.576 ms vs 融合内核 30.382 ms ⇒ 1.27x
345
- # M=256 gemm_4bit 75.532 ms vs 融合内核 164.840 ms ⇒ 0.46x ← 融合反而慢
346
- # 融合内核的 m 循环每 4 行重读一遍权重,所以大 M 时它输给 oneDNN 稠密。
347
- # ⇒ 按 M 分档:小 M 走融合内核,大 M 交回 default。
348
- # ---- 2026-09-22 实测三方交叉点(K=N=4096,OMP=6)----
349
- # M 融合 ms default ms 融合优势
350
- # 1 1.20 ~14.5 12x
351
- # 8 4.71 25.14 5.3x
352
- # 16 7.04 25.75 3.7x
353
- # 17 7.79 26.43 3.4x
354
- # 32 16.20 28.41 1.75x
355
- # 64 28.79 31.37 1.09x
356
- # 96 43.45 37.01 default 反超
357
- # 128 62.11 41.77 default 快 1.49x
358
- # 192 85.78 54.67 default 快 1.57x
359
- # 256 166.11 64.94 default 快 2.56x
360
- # ⇒ 融合内核一直赢到 M=64,default 要到 M≈96 才反超。
361
- # 阈值原先写 16 ⇒ M=17~64 被错误地分给 default,白慢最多 3.4x。
362
- #
363
- # ⚠️ 一个被实测否决的尝试:给 C 层加"权重行解码一次、对全部 M 行复用"的路径,
364
- # 结果是【负优化】(M=17 慢 1.52x、M=192 慢 1.44x)——
365
- # 它把权重解码从 O(M/4) 降到 O(1),却让 A 被每个输出列重读一次,
366
- # A 流量变成 O(N*M*K)(M=17 时 1.14 GB),远比重新解码权重贵。
367
- # 已删除,只在 cpu_ops.cpp 里留了说明。
368
- _GEMM_4BIT_FUSED_MAX_M = 64
369
-
370
- @register_kernel("bitsandbytes::gemm_4bit", "cpu")
371
- def _gemm_4bit_cpu(
372
- A: torch.Tensor,
373
- B: torch.Tensor,
374
- shapeB: Sequence[int],
375
- absmax: torch.Tensor,
376
- blocksize: int,
377
- quant_type: str,
378
- bias: Optional[torch.Tensor] = None,
379
- absmax_8bit: Optional[torch.Tensor] = None,
380
- absmax_code: Optional[torch.Tensor] = None,
381
- absmax_offset: Optional[torch.Tensor] = None,
382
- ) -> torch.Tensor:
383
- m_rows = A.numel() // A.shape[-1] if A.numel() else 0
384
- # 融合路径的限制:无 bias、非 nested absmax、张量是 2D 可 reshape 的、
385
- # 且 M 足够小(大 M 时 oneDNN 稠密更快)。
386
- can_fuse = (
387
- gemm_4bit_forward_kernel is None # 没有 HF kernel 时才用我们的融合内核
388
- and bias is None
389
- and absmax_8bit is None
390
- and m_rows > 0
391
- and m_rows <= _GEMM_4BIT_FUSED_MAX_M
392
- )
393
- if can_fuse:
394
- dtype = A.dtype
395
- A_c = A.reshape(-1, A.shape[-1]).contiguous()
396
- B_c = B.reshape(shapeB[0], -1)
397
- if B_c.dtype != torch.uint8:
398
- B_c = B_c.contiguous().view(torch.uint8)
399
- else:
400
- B_c = B_c.contiguous()
401
- absmax_c = absmax.float().contiguous()
402
- M, K = A_c.shape[0], A_c.shape[1]
403
- N = shapeB[0]
404
- data_type_num = 1 if quant_type == "fp4" else 2
405
- out = torch.empty((M, N), dtype=dtype, device=A_c.device)
406
- fn = {
407
- torch.float32: lib.cgemv_4bit_inference_cpu_fp32,
408
- torch.bfloat16: lib.cgemv_4bit_inference_cpu_bf16,
409
- torch.float16: lib.cgemv_4bit_inference_cpu_fp16,
410
- }[dtype]
411
- fn(
412
- get_ptr(A_c), get_ptr(B_c), get_ptr(absmax_c), get_ptr(out),
413
- ct.c_longlong(M), ct.c_longlong(N), ct.c_longlong(K),
414
- ct.c_longlong(A_c.stride(0)), ct.c_longlong(K // 2),
415
- ct.c_longlong(out.stride(0)), ct.c_longlong(blocksize),
416
- ct.c_int(data_type_num),
417
- )
418
- return out.reshape(*A.shape[:-1], N)
419
-
420
- from ..default.ops import _gemm_4bit_default_impl
421
-
422
- return _gemm_4bit_default_impl(
423
- A, B, shapeB, absmax, blocksize, quant_type,
424
- bias, absmax_8bit, absmax_code, absmax_offset,
425
- )
426
-
427
- # ---- 大 M 分档(2026-09-23 实测)----
428
- # 融合内核的内层是 m0 += 4,每 4 行就把整块权重重新读一遍 ⇒ 权重流量 O(M/4),
429
- # 大 M 时输给"反量化 + oneDNN 稠密 matmul"。
430
- # 干净基准(gemm8_test.py:每格子进程 + 取 7 次最小值),融合/两步 的比值:
431
- # M=16 0.18x / 0.15x / 0.25x 融合远优
432
- # M=64 0.89x / 0.65x / 0.77x 融合仍优
433
- # M=128 1.34x / 1.14x / 1.32x ← 交叉点在这一带
434
- # M=256 1.54x / 1.34x / 1.51x
435
- # M=512 2.04x / 1.82x / 2.04x 两步快约一倍
436
- # ⇒ 阈值取 64(保守:M=64 时融合还是赢的,且真实推理的 M 是 seq_len)。
437
- #
438
- # ⚠️ 这个 op 【没有 default 后端回退】(全库只注册了 cpu 与 cuda),
439
- # 所以大 M 路径必须在这里自己实现。8-bit 的码表是 create_dynamic_map(),
440
- # 与 quantize_blockwise 默认用的同一个 ⇒ 数值一致。
441
- _GEMM_8BIT_FUSED_MAX_M = 64
442
-
443
- @register_kernel("bitsandbytes::gemm_8bit", "cpu")
444
- def _gemm_8bit_cpu(
445
- A: torch.Tensor,
446
- B: torch.Tensor,
447
- absmax: torch.Tensor,
448
- blocksize: int,
449
- ) -> torch.Tensor:
450
- """融合 8bit 反量化 GEMM:out = A @ dequant8(B)(B 为 uint8 码流,逐行块量化)。
451
-
452
- 权重在整个乘法中保持 uint8(DRAM 流量为 fp32 的 1/4),无 fp32 临时张量。
453
- 小 M(<= 64)走融合内核;大 M 退到"反量化 + 稠密 matmul"(见上面的分档说明)。
454
- """
455
- if A.numel() == 0:
456
- return torch.empty((*A.shape[:-1], B.shape[0]), dtype=torch.float32, device=A.device)
457
- A_f = A.reshape(-1, A.shape[-1]).contiguous()
458
- B_c = B.contiguous()
459
- absmax_c = absmax.contiguous().float()
460
- M, K = A_f.shape
461
- N = B_c.shape[0]
462
-
463
- if M > _GEMM_8BIT_FUSED_MAX_M:
464
- # 大 M:两步更快(实测 M=512 时 1.8~2.0x)
465
- from bitsandbytes.functional import create_dynamic_map, dequantize_blockwise
466
-
467
- code = create_dynamic_map().to(B_c.device)
468
- W = dequantize_blockwise(B_c, absmax=absmax_c, code=code, blocksize=blocksize)
469
- return (A_f @ W.t()).reshape(*A.shape[:-1], N)
470
-
471
- out = torch.empty((M, N), dtype=torch.float32, device=A_f.device)
472
- lib.cgemm_8bit_inference_cpu_fp32(
473
- get_ptr(A_f),
474
- get_ptr(B_c),
475
- get_ptr(absmax_c),
476
- get_ptr(out),
477
- ct.c_longlong(M),
478
- ct.c_longlong(N),
479
- ct.c_longlong(K),
480
- ct.c_longlong(A_f.stride(0)),
481
- ct.c_longlong(B_c.stride(0)),
482
- ct.c_longlong(out.stride(0)),
483
- ct.c_longlong(blocksize),
484
- )
485
- return out.reshape(*A.shape[:-1], N)
486
-
487
-
488
- # ==================== CPU Optimizer Kernels ====================
489
-
490
-
491
- def _compute_update_norm_and_scale(
492
- update: torch.Tensor,
493
- unorm_vec: Optional[torch.Tensor],
494
- max_unorm: float,
495
- param_norm: float,
496
- ) -> float:
497
- """Compute trust-ratio scaling factor for LAMB/LARS and store update norm."""
498
- if max_unorm <= 0.0:
499
- return 1.0
500
- unorm = torch.norm(update).item()
501
- if unorm_vec is not None:
502
- unorm_vec.fill_(unorm)
503
- if unorm > max_unorm * param_norm:
504
- return (max_unorm * param_norm) / unorm
505
- return 1.0
506
-
507
-
508
- @torch.no_grad()
509
- def _optimizer_update_32bit_cpu(
510
- optimizer_name: str,
511
- g: torch.Tensor,
512
- p: torch.Tensor,
513
- state1: torch.Tensor,
514
- state2: Optional[torch.Tensor],
515
- unorm_vec: Optional[torch.Tensor],
516
- max_unorm: float,
517
- param_norm: float,
518
- beta1: float,
519
- beta2: float,
520
- beta3: float,
521
- alpha: float,
522
- eps: float,
523
- weight_decay: float,
524
- step: int,
525
- lr: float,
526
- gnorm_scale: float,
527
- skip_zeros: bool = False,
528
- ) -> None:
529
- g_float = g.float() * gnorm_scale
530
- p_float = p.data.float()
531
-
532
- if optimizer_name in ("adam", "lamb"):
533
- # Adam / LAMB (2-state): m and v
534
- state1.mul_(beta1).add_(g_float, alpha=1.0 - beta1)
535
- state2.mul_(beta2).addcmul_(g_float, g_float, value=1.0 - beta2)
536
-
537
- correction1 = 1.0 - beta1**step
538
- correction2 = math.sqrt(1.0 - beta2**step)
539
- step_size = -lr * correction2 / correction1
540
-
541
- if weight_decay > 0.0:
542
- p_float.mul_(1.0 - lr * weight_decay)
543
-
544
- update = state1 / (state2.sqrt() + eps * correction2)
545
-
546
- update_scale = _compute_update_norm_and_scale(update, unorm_vec, max_unorm, param_norm)
547
- p_float.add_(update, alpha=step_size * update_scale)
548
-
549
- elif optimizer_name == "ademamix":
550
- # AdEMAMix (2-state): state1 shape is (2, *p.shape), state1[0]=m1, state1[1]=m2
551
- m1 = state1[0]
552
- m2 = state1[1]
553
- nu = state2
554
-
555
- m1.mul_(beta1).add_(g_float, alpha=1.0 - beta1)
556
- m2.mul_(beta3).add_(g_float, alpha=1.0 - beta3)
557
- nu.mul_(beta2).addcmul_(g_float, g_float, value=1.0 - beta2)
558
-
559
- correction1 = 1.0 - beta1**step
560
- correction2 = math.sqrt(1.0 - beta2**step)
561
-
562
- if weight_decay > 0.0:
563
- p_float.mul_(1.0 - lr * weight_decay)
564
-
565
- mixed_momentum = (m1 / correction1) + (alpha * m2)
566
- adaptive_term = (nu.sqrt() / correction2) + eps
567
- p_float.add_(mixed_momentum / adaptive_term, alpha=-lr)
568
-
569
- elif optimizer_name in ("momentum", "lars"):
570
- # SGD with momentum / LARS (1-state)
571
- g_wd = g_float.add(p_float, alpha=weight_decay) if weight_decay > 0.0 else g_float
572
-
573
- if step == 1:
574
- state1.copy_(g_wd)
575
- else:
576
- state1.mul_(beta1).add_(g_wd)
577
-
578
- update_scale = _compute_update_norm_and_scale(state1, unorm_vec, max_unorm, param_norm)
579
- p_float.add_(state1, alpha=-lr * update_scale)
580
-
581
- elif optimizer_name == "lion":
582
- # Lion (2-state sign update)
583
- if weight_decay > 0.0:
584
- p_float.mul_(1.0 - lr * weight_decay)
585
-
586
- update = state1.mul(beta1).add(g_float, alpha=1.0 - beta1)
587
- p_float.add_(update.sign(), alpha=-lr)
588
-
589
- state1.mul_(beta2).add_(g_float, alpha=1.0 - beta2)
590
-
591
- elif optimizer_name == "rmsprop":
592
- # RMSprop (1-state)
593
- g_wd = g_float.add(p_float, alpha=weight_decay) if weight_decay > 0.0 else g_float
594
- state1.mul_(beta1).addcmul_(g_wd, g_wd, value=1.0 - beta1)
595
-
596
- update = g_wd / (state1.sqrt() + eps)
597
- update_scale = _compute_update_norm_and_scale(update, unorm_vec, max_unorm, param_norm)
598
- p_float.add_(update, alpha=-lr * update_scale)
599
-
600
- elif optimizer_name == "adagrad":
601
- # Adagrad (1-state)
602
- g_wd = g_float.add(p_float, alpha=weight_decay) if weight_decay > 0.0 else g_float
603
- state1.addcmul_(g_wd, g_wd, value=1.0)
604
-
605
- update = g_wd / (state1.sqrt() + eps)
606
- p_float.add_(update, alpha=-lr)
607
-
608
- else:
609
- raise ValueError(f"Unsupported optimizer for CPU: {optimizer_name}")
610
-
611
- # Write back to original precision
612
- p.data.copy_(p_float)
613
-
614
-
615
- register_kernel("bitsandbytes::optimizer_update_32bit", "cpu")(_optimizer_update_32bit_cpu)
616
-
617
-
618
- @torch.no_grad()
619
- def _dequant_blockwise_fp32_direct(
620
- A_uint8: torch.Tensor, absmax: torch.Tensor, code: torch.Tensor, blocksize: int
621
- ) -> torch.Tensor:
622
- return torch.ops.bitsandbytes.dequantize_blockwise(A_uint8, absmax, code, blocksize, torch.float32)
623
-
624
-
625
- def _quant_blockwise_fp32_direct(
626
- A_fp32: torch.Tensor, code: torch.Tensor, absmax_out: torch.Tensor, out_uint8: torch.Tensor, blocksize: int
627
- ) -> None:
628
- out, absmax = torch.ops.bitsandbytes.quantize_blockwise(A_fp32, code, blocksize)
629
- out_uint8.copy_(out)
630
- absmax_out.copy_(absmax)
631
-
632
-
633
- def _optimizer_update_8bit_blockwise_cpu(
634
- optimizer_name: str,
635
- g: torch.Tensor,
636
- p: torch.Tensor,
637
- state1: torch.Tensor,
638
- state2: Optional[torch.Tensor],
639
- beta1: float,
640
- beta2: float,
641
- beta3: float,
642
- alpha: float,
643
- eps: float,
644
- step: int,
645
- lr: float,
646
- qmap1: torch.Tensor,
647
- qmap2: Optional[torch.Tensor],
648
- absmax1: torch.Tensor,
649
- absmax2: Optional[torch.Tensor],
650
- weight_decay: float,
651
- gnorm_scale: float,
652
- skip_zeros: bool = False,
653
- ) -> None:
654
- # Fused single-pass C kernel (dequant + update + p update + requant in one
655
- # sweep). Falls back to the composite PyTorch path below when the native
656
- # library is unavailable or the optimizer is not supported by the kernel.
657
- _OPT_IDS = {"adam": 0, "lamb": 0, "momentum": 1, "lars": 1, "lion": 2, "rmsprop": 3, "adagrad": 4, "ademamix": 5}
658
- n = p.numel()
659
- # The C kernel walks raw pointers and assumes C-contiguous layouts: a
660
- # non-contiguous grad (transposed / sliced views are common in training)
661
- # would silently read the wrong elements and corrupt the states.
662
- contiguous_layout = (
663
- g.is_contiguous()
664
- and p.is_contiguous()
665
- and state1.is_contiguous()
666
- and (state2 is None or state2.is_contiguous())
667
- and qmap1.is_contiguous()
668
- and (qmap2 is None or qmap2.is_contiguous())
669
- and absmax1.is_contiguous()
670
- and (absmax2 is None or absmax2.is_contiguous())
671
- )
672
- if (
673
- not isinstance(lib, ErrorHandlerMockBNBNativeLibrary)
674
- and contiguous_layout
675
- and optimizer_name in _OPT_IDS
676
- and g.dtype in (torch.float32, torch.bfloat16, torch.float16)
677
- # ademamix packs a second momentum into state1[n:2n] with its absmax in
678
- # absmax1[blocks:2*blocks]; the flat C layout requires that packing.
679
- and (optimizer_name != "ademamix" or state1.numel() >= 2 * n)
680
- ):
681
- try:
682
- dtype_id = {torch.float32: 0, torch.bfloat16: 1, torch.float16: 2}[g.dtype]
683
- # Every scalar needs an explicit ctypes width: a bare Python int is
684
- # marshalled as 32-bit c_int, but `n` is a long long. libffi writes
685
- # only the low 4 bytes of its 8-byte stack slot; the C side reads
686
- # all 8, so stale stack bits leak into n (observed: 0x7FFF00004000
687
- # for n=16384) and the kernel walks out of bounds -> SIGSEGV.
688
- lib.coptimizer_update_8bit_blockwise_cpu(
689
- ct.c_int(_OPT_IDS[optimizer_name]),
690
- get_ptr(g),
691
- get_ptr(p),
692
- get_ptr(state1),
693
- get_ptr(state2) if state2 is not None else ct.c_void_p(0),
694
- ct.c_float(beta1),
695
- ct.c_float(beta2),
696
- ct.c_float(beta3),
697
- ct.c_float(alpha),
698
- ct.c_float(eps),
699
- ct.c_int(step),
700
- ct.c_float(lr),
701
- get_ptr(qmap1),
702
- get_ptr(qmap2) if qmap2 is not None else ct.c_void_p(0),
703
- get_ptr(absmax1),
704
- get_ptr(absmax2) if absmax2 is not None else ct.c_void_p(0),
705
- ct.c_float(weight_decay),
706
- ct.c_float(gnorm_scale),
707
- ct.c_int(1 if skip_zeros else 0),
708
- ct.c_longlong(n),
709
- ct.c_int(dtype_id),
710
- )
711
- return
712
- except (AttributeError, RuntimeError):
713
- pass # old native library without the fused entry point
714
-
715
- blocksize = 256
716
-
717
- # Dequantize states
718
- if optimizer_name == "ademamix" and absmax1.ndim == 2:
719
- s1_1 = _dequant_blockwise_fp32_direct(state1[0], absmax1[0], qmap1, blocksize)
720
- s1_2 = _dequant_blockwise_fp32_direct(state1[1], absmax1[1], qmap1, blocksize)
721
- state1_fp32 = torch.stack([s1_1, s1_2])
722
- else:
723
- state1_fp32 = _dequant_blockwise_fp32_direct(state1, absmax1, qmap1, blocksize)
724
-
725
- state2_fp32 = None
726
- if state2 is not None and qmap2 is not None and absmax2 is not None:
727
- state2_fp32 = _dequant_blockwise_fp32_direct(state2, absmax2, qmap2, blocksize)
728
-
729
- grad = g.float() * gnorm_scale
730
- p_fp32 = p.data.float()
731
-
732
- if optimizer_name in ("adam", "lamb"):
733
- state1_fp32.mul_(beta1).add_(grad, alpha=1.0 - beta1)
734
- state2_fp32.mul_(beta2).addcmul_(grad, grad, value=1.0 - beta2)
735
-
736
- correction1 = 1.0 - beta1**step
737
- correction2 = math.sqrt(1.0 - beta2**step)
738
-
739
- denom = (state2_fp32.sqrt() / correction2).add_(eps)
740
- if weight_decay > 0.0:
741
- p_fp32.mul_(1.0 - lr * weight_decay)
742
- p_fp32.addcdiv_(state1_fp32, denom, value=-lr / correction1)
743
-
744
- elif optimizer_name == "ademamix":
745
- m1_fp32, m2_fp32 = state1_fp32[0], state1_fp32[1]
746
- nu_fp32 = state2_fp32
747
-
748
- m1_fp32.mul_(beta1).add_(grad, alpha=1.0 - beta1)
749
- m2_fp32.mul_(beta3).add_(grad, alpha=1.0 - beta3)
750
- nu_fp32.mul_(beta2).addcmul_(grad, grad, value=1.0 - beta2)
751
-
752
- correction1 = 1.0 - beta1**step
753
- correction2 = math.sqrt(1.0 - beta2**step)
754
-
755
- update = (m1_fp32 / correction1 + alpha * m2_fp32) / (nu_fp32.sqrt() / correction2 + eps)
756
- if weight_decay > 0.0:
757
- p_fp32.mul_(1.0 - lr * weight_decay)
758
- p_fp32.add_(update, alpha=-lr)
759
-
760
- state1_fp32 = torch.stack([m1_fp32, m2_fp32])
761
-
762
- elif optimizer_name in ("momentum", "lars"):
763
- grad.add_(p_fp32, alpha=weight_decay)
764
- if step == 1:
765
- state1_fp32.copy_(grad)
766
- else:
767
- state1_fp32.mul_(beta1).add_(grad)
768
- p_fp32.add_(state1_fp32, alpha=-lr)
769
-
770
- elif optimizer_name == "lion":
771
- if weight_decay > 0.0:
772
- p_fp32.mul_(1.0 - lr * weight_decay)
773
-
774
- update_dir = torch.sign(state1_fp32.mul(beta1) + grad.mul(1.0 - beta1))
775
- p_fp32.add_(update_dir, alpha=-lr)
776
-
777
- state1_fp32.mul_(beta2).add_(grad, alpha=1.0 - beta2)
778
-
779
- elif optimizer_name == "rmsprop":
780
- grad.add_(p_fp32, alpha=weight_decay)
781
- state1_fp32.mul_(beta1).addcmul_(grad, grad, value=1.0 - beta1)
782
- p_fp32.addcdiv_(grad, state1_fp32.sqrt().add_(eps), value=-lr)
783
-
784
- elif optimizer_name == "adagrad":
785
- grad.add_(p_fp32, alpha=weight_decay)
786
- state1_fp32.addcmul_(grad, grad, value=1.0)
787
- p_fp32.addcdiv_(grad, state1_fp32.sqrt().add_(eps), value=-lr)
788
-
789
- else:
790
- raise ValueError(f"Unsupported optimizer for CPU 8-bit: {optimizer_name}")
791
-
792
- p.data.copy_(p_fp32)
793
-
794
- # Re-quantize states
795
- if optimizer_name == "ademamix":
796
- _quant_blockwise_fp32_direct(state1_fp32[0], qmap1, absmax1[0], state1[0], blocksize)
797
- _quant_blockwise_fp32_direct(state1_fp32[1], qmap1, absmax1[1], state1[1], blocksize)
798
- _quant_blockwise_fp32_direct(state2_fp32, qmap2, absmax2, state2, blocksize)
799
- else:
800
- _quant_blockwise_fp32_direct(state1_fp32, qmap1, absmax1, state1, blocksize)
801
- if state2_fp32 is not None:
802
- _quant_blockwise_fp32_direct(state2_fp32, qmap2, absmax2, state2, blocksize)
803
-
804
-
805
- register_kernel("bitsandbytes::optimizer_update_8bit_blockwise", "cpu")(_optimizer_update_8bit_blockwise_cpu)
1
+ from collections.abc import Sequence
2
+ import ctypes as ct
3
+ import logging
4
+ import math
5
+ from math import prod
6
+ from typing import Optional
7
+
8
+ import torch
9
+
10
+ from bitsandbytes.functional import get_ptr, has_avx512bf16
11
+
12
+ from ..._ops import register_kernel
13
+ from ...cextension import ErrorHandlerMockBNBNativeLibrary, lib
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # 256 项 fp32 8-bit 码表缓存(见 _gemm_8bit_cpu 的说明)
18
+ _GEMM_8BIT_CODE = None
19
+
20
+ _has_avx512 = torch.backends.cpu.get_cpu_capability() == "AVX512"
21
+
22
+ # torch._int_mm for s8@s8->s32 is supported on CPU from torch 2.4+.
23
+ # However, we can overflow if we use this without AVX512_VNNI support.
24
+ # This is fixed in torch 2.6+, so we set this as the minimum to be safe.
25
+ # For more information: https://github.com/pytorch/pytorch/pull/136942
26
+ #
27
+ # Without AVX-512 (including aarch64), torch._int_mm uses a scalar fallback
28
+ # that is much slower than fp32 matmul. Only use it when AVX-512 is available.
29
+ if torch.__version__ >= (2, 6) and _has_avx512:
30
+
31
+ @register_kernel("bitsandbytes::int8_linear_matmul", "cpu")
32
+ def _(A: torch.Tensor, B: torch.Tensor):
33
+ return torch._int_mm(
34
+ A.reshape(-1, A.shape[-1]),
35
+ B.t(),
36
+ ).reshape(*A.shape[:-1], B.shape[0])
37
+
38
+
39
+ if not isinstance(lib, ErrorHandlerMockBNBNativeLibrary):
40
+
41
+ @register_kernel("bitsandbytes::quantize_blockwise", "cpu")
42
+ def _(A: torch.Tensor, code: torch.Tensor, blocksize: int) -> tuple[torch.Tensor, torch.Tensor]:
43
+ A = A.contiguous()
44
+ n = A.numel()
45
+ blocks = -(n // -blocksize)
46
+
47
+ absmax = torch.empty((blocks,), device=A.device, dtype=torch.float32)
48
+ out = torch.empty(A.shape, device=A.device, dtype=torch.uint8)
49
+
50
+ if A.dtype == torch.float32:
51
+ lib.cquantize_blockwise_cpu_fp32(
52
+ get_ptr(code),
53
+ get_ptr(A),
54
+ get_ptr(absmax),
55
+ get_ptr(out),
56
+ ct.c_longlong(blocksize),
57
+ ct.c_longlong(n),
58
+ )
59
+ elif A.dtype == torch.bfloat16:
60
+ lib.cquantize_blockwise_cpu_bf16(
61
+ get_ptr(code),
62
+ get_ptr(A),
63
+ get_ptr(absmax),
64
+ get_ptr(out),
65
+ ct.c_longlong(blocksize),
66
+ ct.c_longlong(n),
67
+ )
68
+ elif A.dtype == torch.float16:
69
+ lib.cquantize_blockwise_cpu_fp16(
70
+ get_ptr(code),
71
+ get_ptr(A),
72
+ get_ptr(absmax),
73
+ get_ptr(out),
74
+ ct.c_longlong(blocksize),
75
+ ct.c_longlong(n),
76
+ )
77
+ else:
78
+ # Generic fallback for other dtypes
79
+ A_flat = A.reshape(n).float()
80
+ rem = n % blocksize
81
+ has_rem = rem > 0
82
+ A_com = A_flat[: n - rem]
83
+ A_com_reshaped = A_com.reshape(n // blocksize, blocksize)
84
+ absmax[: blocks - has_rem] = torch.abs(A_com_reshaped).max(dim=-1)[0]
85
+ scaled_A = torch.clamp(A_com_reshaped * (1 / absmax[: blocks - has_rem].view(-1, 1)), -1, 1)
86
+ scaled_A = scaled_A.reshape(-1)
87
+ if has_rem:
88
+ absmax[-1] = torch.abs(A_flat[n - rem :]).max()
89
+ scaled_A_rem = torch.clamp(A_flat[n - rem :] * (1 / absmax[-1]), -1, 1)
90
+ scaled_A = torch.cat([scaled_A, scaled_A_rem], dim=0)
91
+
92
+ diff = torch.abs(scaled_A.unsqueeze(-1) - code.to(scaled_A.device))
93
+ out = torch.argmin(diff, dim=-1).to(torch.uint8).to(scaled_A.device).reshape(A.shape)
94
+
95
+ return out, absmax
96
+
97
+ @register_kernel("bitsandbytes::dequantize_blockwise", "cpu")
98
+ def _(
99
+ A: torch.Tensor, absmax: torch.Tensor, code: torch.Tensor, blocksize: int, dtype: torch.dtype
100
+ ) -> torch.Tensor:
101
+ A = A.contiguous()
102
+ out = torch.empty_like(A, dtype=dtype)
103
+ if dtype == torch.float32:
104
+ lib.cdequantize_blockwise_cpu_fp32(
105
+ get_ptr(code),
106
+ get_ptr(A),
107
+ get_ptr(absmax),
108
+ get_ptr(out),
109
+ ct.c_longlong(blocksize),
110
+ ct.c_longlong(A.numel()),
111
+ )
112
+ elif dtype == torch.bfloat16:
113
+ lib.cdequantize_blockwise_cpu_bf16(
114
+ get_ptr(code),
115
+ get_ptr(A),
116
+ get_ptr(absmax),
117
+ get_ptr(out),
118
+ ct.c_longlong(blocksize),
119
+ ct.c_longlong(A.numel()),
120
+ )
121
+ elif dtype == torch.float16:
122
+ lib.cdequantize_blockwise_cpu_fp16(
123
+ get_ptr(code),
124
+ get_ptr(A),
125
+ get_ptr(absmax),
126
+ get_ptr(out),
127
+ ct.c_longlong(blocksize),
128
+ ct.c_longlong(A.numel()),
129
+ )
130
+ else:
131
+ out = code[A.reshape(-1).int()]
132
+ blocks = out.shape[-1] // blocksize
133
+ res = out.shape[-1] % blocksize
134
+ if res != 0:
135
+ out = torch.nn.functional.pad(out, (0, blocksize - res), mode="constant", value=0)
136
+ out = (out.view(-1, blocksize) * absmax.view(-1, 1)).to(dtype).reshape(-1)
137
+ out = out[: blocks * blocksize + res]
138
+ out = out.reshape(A.shape)
139
+
140
+ return out
141
+
142
+ @register_kernel("bitsandbytes::dequantize_4bit", "cpu")
143
+ def _(
144
+ A: torch.Tensor,
145
+ absmax: torch.Tensor,
146
+ blocksize: int,
147
+ quant_type: str,
148
+ shape: Sequence[int],
149
+ dtype: torch.dtype,
150
+ ) -> torch.Tensor:
151
+ # Fallback as AVX512 implementation has accuracy issues with blocksize >= 2048.
152
+ # Note: this is not a common use case.
153
+ avx512_fallback = _has_avx512 and blocksize >= 2048
154
+
155
+ # Odd shape is not supported by this kernel; fallback to generic implementation
156
+ shape_fallback = shape[-1] % 2 != 0
157
+
158
+ if avx512_fallback or shape_fallback:
159
+ from ..default.ops import _dequantize_4bit_compute
160
+ from ..utils import _get_4bit_code
161
+
162
+ if A.dtype != torch.uint8:
163
+ A = A.view(torch.uint8)
164
+ code = _get_4bit_code(quant_type, A.device)
165
+ return _dequantize_4bit_compute(A.reshape(-1), absmax, code, blocksize, shape, dtype)
166
+
167
+ # Enable non uint8 dtype
168
+ if A.dtype != torch.uint8:
169
+ A = A.view(torch.uint8)
170
+
171
+ # TODO: support half precision absmax
172
+ if absmax.dtype != torch.float32:
173
+ absmax = absmax.float()
174
+
175
+ if len(shape) == 1:
176
+ shape = (1, shape[0])
177
+
178
+ m = prod(shape[:-1])
179
+ n = shape[-1]
180
+
181
+ A = A.reshape(m, n // 2)
182
+ out = torch.empty(shape, dtype=dtype, device=A.device)
183
+
184
+ if quant_type == "fp4":
185
+ if dtype == torch.float32:
186
+ lib.cdequantize_blockwise_cpu_fp4_fp32(
187
+ get_ptr(A),
188
+ get_ptr(absmax),
189
+ get_ptr(out),
190
+ ct.c_longlong(blocksize),
191
+ ct.c_longlong(m),
192
+ ct.c_longlong(n),
193
+ )
194
+ elif dtype == torch.bfloat16:
195
+ lib.cdequantize_blockwise_cpu_fp4_bf16(
196
+ get_ptr(A),
197
+ get_ptr(absmax),
198
+ get_ptr(out),
199
+ ct.c_longlong(blocksize),
200
+ ct.c_longlong(m),
201
+ ct.c_longlong(n),
202
+ )
203
+ elif dtype == torch.float16:
204
+ lib.cdequantize_blockwise_cpu_fp4_fp16(
205
+ get_ptr(A),
206
+ get_ptr(absmax),
207
+ get_ptr(out),
208
+ ct.c_longlong(blocksize),
209
+ ct.c_longlong(m),
210
+ ct.c_longlong(n),
211
+ )
212
+ elif quant_type == "nf4":
213
+ if dtype == torch.float32:
214
+ lib.cdequantize_blockwise_cpu_nf4_fp32(
215
+ get_ptr(A),
216
+ get_ptr(absmax),
217
+ get_ptr(out),
218
+ ct.c_longlong(blocksize),
219
+ ct.c_longlong(m),
220
+ ct.c_longlong(n),
221
+ )
222
+ elif dtype == torch.bfloat16:
223
+ lib.cdequantize_blockwise_cpu_nf4_bf16(
224
+ get_ptr(A),
225
+ get_ptr(absmax),
226
+ get_ptr(out),
227
+ ct.c_longlong(blocksize),
228
+ ct.c_longlong(m),
229
+ ct.c_longlong(n),
230
+ )
231
+ elif dtype == torch.float16:
232
+ lib.cdequantize_blockwise_cpu_nf4_fp16(
233
+ get_ptr(A),
234
+ get_ptr(absmax),
235
+ get_ptr(out),
236
+ ct.c_longlong(blocksize),
237
+ ct.c_longlong(m),
238
+ ct.c_longlong(n),
239
+ )
240
+ else:
241
+ raise ValueError
242
+
243
+ return out
244
+
245
+ # NOTE 2026-09-22: the registration below used to live INSIDE `if has_avx512bf16():`,
246
+ # which meant AVX2-only CPUs (Zen2 4500U / Comet Lake i5 / EPYC Zen2-3) never
247
+ # registered a CPU kernel for gemv_4bit at all. They silently fell through to the
248
+ # default backend, which dequantizes the whole weight matrix and then runs a dense
249
+ # linear -- measured 12.3 ms per call vs 1.16 ms for the fused kernel below (10.6x).
250
+ # The guard is therefore narrowed to cover only the AVX512-BF16-only HuggingFace
251
+ # kernel load; the fused registration is unconditional.
252
+ gemm_4bit_forward_kernel = None
253
+ if has_avx512bf16():
254
+ gemm_4bit_forward_kernel = None
255
+ try:
256
+ from kernels import get_kernel
257
+
258
+ gemm_4bit_forward_kernel = get_kernel(
259
+ "kernels-community/quantization-bitsandbytes", version=1
260
+ ).gemm_4bit_forward
261
+ except Exception as exc: # pragma: no cover - best effort fallback
262
+ gemm_4bit_forward_kernel = None
263
+ logger.warning(
264
+ "Failed to load CPU gemm_4bit_forward from kernels-community: %s. Please make sure you already `pip install kernels` and the kernels >= 0.11.1",
265
+ exc,
266
+ )
267
+
268
+ @register_kernel("bitsandbytes::gemv_4bit", "cpu")
269
+ def _(
270
+ A: torch.Tensor,
271
+ B: torch.Tensor,
272
+ shapeB: Sequence[int],
273
+ absmax: torch.Tensor,
274
+ code: torch.Tensor,
275
+ blocksize: int,
276
+ ) -> torch.Tensor:
277
+ if B.dtype != torch.uint8:
278
+ B = B.contiguous().view(torch.uint8)
279
+ dtype = A.dtype
280
+ A_orig = A
281
+ absmax_orig = absmax
282
+ quant_type = "fp4" if code[1] > 0 else "nf4"
283
+ # cpu fused op only support bf16 for now.
284
+ if dtype != torch.bfloat16:
285
+ A = A.to(torch.bfloat16)
286
+ if absmax.dtype != torch.bfloat16:
287
+ absmax = absmax.to(torch.bfloat16)
288
+
289
+ final_out_shape = (*A.shape[:-1], shapeB[0])
290
+ A = A.reshape(-1, A.shape[-1])
291
+ out_shape = (*A.shape[:-1], shapeB[0])
292
+ if gemm_4bit_forward_kernel is not None:
293
+ quant_type_num = 1 if quant_type == "fp4" else 0
294
+ # C++ kernel expects weight shape (N, K_packed), ensure 2D contiguous
295
+ B_2d = B.reshape(shapeB[0], -1).contiguous()
296
+ out = gemm_4bit_forward_kernel(A, B_2d, absmax, blocksize, quant_type_num)
297
+ else:
298
+ # AVX2 回退:用所有 CPU 构建都导出的 cgemv_4bit_inference_cpu_* 融合内核
299
+ # (lib.gemv_4bit_inference_cpu_fp4/nf4_bf16 只存在于 AVX512+BF16 构建,
300
+ # AVX2 机器走到会 AttributeError;data_type: FP4=1 / NF4=2)
301
+ A_c = A_orig.reshape(-1, A_orig.shape[-1]).contiguous()
302
+ B_c = B.reshape(shapeB[0], -1).contiguous()
303
+ absmax_c = absmax_orig.float().contiguous()
304
+ data_type_num = 1 if quant_type == "fp4" else 2
305
+ M, K = A_c.shape[0], A_c.shape[1]
306
+ N = shapeB[0]
307
+ out = torch.empty(out_shape, dtype=A_c.dtype, device=A_c.device)
308
+ if dtype == torch.float32:
309
+ lib.cgemv_4bit_inference_cpu_fp32(
310
+ get_ptr(A_c), get_ptr(B_c), get_ptr(absmax_c), get_ptr(out),
311
+ ct.c_longlong(M), ct.c_longlong(N), ct.c_longlong(K),
312
+ ct.c_longlong(A_c.stride(0)), ct.c_longlong(K // 2),
313
+ ct.c_longlong(out.stride(0)), ct.c_longlong(blocksize),
314
+ ct.c_int(data_type_num),
315
+ )
316
+ elif dtype == torch.bfloat16:
317
+ lib.cgemv_4bit_inference_cpu_bf16(
318
+ get_ptr(A_c), get_ptr(B_c), get_ptr(absmax_c), get_ptr(out),
319
+ ct.c_longlong(M), ct.c_longlong(N), ct.c_longlong(K),
320
+ ct.c_longlong(A_c.stride(0)), ct.c_longlong(K // 2),
321
+ ct.c_longlong(out.stride(0)), ct.c_longlong(blocksize),
322
+ ct.c_int(data_type_num),
323
+ )
324
+ else:
325
+ lib.cgemv_4bit_inference_cpu_fp16(
326
+ get_ptr(A_c), get_ptr(B_c), get_ptr(absmax_c), get_ptr(out),
327
+ ct.c_longlong(M), ct.c_longlong(N), ct.c_longlong(K),
328
+ ct.c_longlong(A_c.stride(0)), ct.c_longlong(K // 2),
329
+ ct.c_longlong(out.stride(0)), ct.c_longlong(blocksize),
330
+ ct.c_int(data_type_num),
331
+ )
332
+
333
+ if dtype != torch.bfloat16:
334
+ out = out.to(dtype)
335
+
336
+ return out.reshape(final_out_shape)
337
+
338
+ # ---- gemm_4bit: CPU 注册(2026-09-22 新增)----
339
+ # 此前这个 op 【没有 cpu 注册】,于是退到 default 后端做"反量化 + 稠密线性"。
340
+ # autograd/_functions.py:324/334/462/472 调的就是它 ⇒ 真实模型里
341
+ # 【每一次批量 4-bit 线性前向】都在走慢路径。实测(K=N=4096):
342
+ # M=1 gemm_4bit 14.483 ms vs 融合内核 1.448 ms ⇒ 10.00x
343
+ # M=8 gemm_4bit 32.494 ms vs 融合内核 4.124 ms ⇒ 7.88x
344
+ # M=64 gemm_4bit 38.576 ms vs 融合内核 30.382 ms ⇒ 1.27x
345
+ # M=256 gemm_4bit 75.532 ms vs 融合内核 164.840 ms ⇒ 0.46x ← 融合反而慢
346
+ # 融合内核的 m 循环每 4 行重读一遍权重,所以大 M 时它输给 oneDNN 稠密。
347
+ # ⇒ 按 M 分档:小 M 走融合内核,大 M 交回 default。
348
+ # ---- 2026-09-22 实测三方交叉点(K=N=4096,OMP=6)----
349
+ # M 融合 ms default ms 融合优势
350
+ # 1 1.20 ~14.5 12x
351
+ # 8 4.71 25.14 5.3x
352
+ # 16 7.04 25.75 3.7x
353
+ # 17 7.79 26.43 3.4x
354
+ # 32 16.20 28.41 1.75x
355
+ # 64 28.79 31.37 1.09x
356
+ # 96 43.45 37.01 default 反超
357
+ # 128 62.11 41.77 default 快 1.49x
358
+ # 192 85.78 54.67 default 快 1.57x
359
+ # 256 166.11 64.94 default 快 2.56x
360
+ # ⇒ 融合内核一直赢到 M=64,default 要到 M≈96 才反超。
361
+ # 阈值原先写 16 ⇒ M=17~64 被错误地分给 default,白慢最多 3.4x。
362
+ #
363
+ # ⚠️ 一个被实测否决的尝试:给 C 层加"权重行解码一次、对全部 M 行复用"的路径,
364
+ # 结果是【负优化】(M=17 慢 1.52x、M=192 慢 1.44x)——
365
+ # 它把权重解码从 O(M/4) 降到 O(1),却让 A 被每个输出列重读一次,
366
+ # A 流量变成 O(N*M*K)(M=17 时 1.14 GB),远比重新解码权重贵。
367
+ # 已删除,只在 cpu_ops.cpp 里留了说明。
368
+ _GEMM_4BIT_FUSED_MAX_M = 64
369
+
370
+ @register_kernel("bitsandbytes::gemm_4bit", "cpu")
371
+ def _gemm_4bit_cpu(
372
+ A: torch.Tensor,
373
+ B: torch.Tensor,
374
+ shapeB: Sequence[int],
375
+ absmax: torch.Tensor,
376
+ blocksize: int,
377
+ quant_type: str,
378
+ bias: Optional[torch.Tensor] = None,
379
+ absmax_8bit: Optional[torch.Tensor] = None,
380
+ absmax_code: Optional[torch.Tensor] = None,
381
+ absmax_offset: Optional[torch.Tensor] = None,
382
+ ) -> torch.Tensor:
383
+ m_rows = A.numel() // A.shape[-1] if A.numel() else 0
384
+ # 融合路径的限制:无 bias、非 nested absmax、张量是 2D 可 reshape 的、
385
+ # 且 M 足够小(大 M 时 oneDNN 稠密更快)。
386
+ can_fuse = (
387
+ gemm_4bit_forward_kernel is None # 没有 HF kernel 时才用我们的融合内核
388
+ and bias is None
389
+ and absmax_8bit is None
390
+ and m_rows > 0
391
+ and m_rows <= _GEMM_4BIT_FUSED_MAX_M
392
+ )
393
+ if can_fuse:
394
+ dtype = A.dtype
395
+ A_c = A.reshape(-1, A.shape[-1]).contiguous()
396
+ B_c = B.reshape(shapeB[0], -1)
397
+ if B_c.dtype != torch.uint8:
398
+ B_c = B_c.contiguous().view(torch.uint8)
399
+ else:
400
+ B_c = B_c.contiguous()
401
+ absmax_c = absmax.float().contiguous()
402
+ M, K = A_c.shape[0], A_c.shape[1]
403
+ N = shapeB[0]
404
+ data_type_num = 1 if quant_type == "fp4" else 2
405
+ out = torch.empty((M, N), dtype=dtype, device=A_c.device)
406
+ fn = {
407
+ torch.float32: lib.cgemv_4bit_inference_cpu_fp32,
408
+ torch.bfloat16: lib.cgemv_4bit_inference_cpu_bf16,
409
+ torch.float16: lib.cgemv_4bit_inference_cpu_fp16,
410
+ }[dtype]
411
+ fn(
412
+ get_ptr(A_c), get_ptr(B_c), get_ptr(absmax_c), get_ptr(out),
413
+ ct.c_longlong(M), ct.c_longlong(N), ct.c_longlong(K),
414
+ ct.c_longlong(A_c.stride(0)), ct.c_longlong(K // 2),
415
+ ct.c_longlong(out.stride(0)), ct.c_longlong(blocksize),
416
+ ct.c_int(data_type_num),
417
+ )
418
+ return out.reshape(*A.shape[:-1], N)
419
+
420
+ from ..default.ops import _gemm_4bit_default_impl
421
+
422
+ return _gemm_4bit_default_impl(
423
+ A, B, shapeB, absmax, blocksize, quant_type,
424
+ bias, absmax_8bit, absmax_code, absmax_offset,
425
+ )
426
+
427
+ # ---- 大 M 分档(2026-09-23 实测)----
428
+ # 融合内核的内层是 m0 += 4,每 4 行就把整块权重重新读一遍 ⇒ 权重流量 O(M/4),
429
+ # 大 M 时输给"反量化 + oneDNN 稠密 matmul"。
430
+ # 干净基准(gemm8_test.py:每格子进程 + 取 7 次最小值),融合/两步 的比值:
431
+ # M=16 0.18x / 0.15x / 0.25x 融合远优
432
+ # M=64 0.89x / 0.65x / 0.77x 融合仍优
433
+ # M=128 1.34x / 1.14x / 1.32x ← 交叉点在这一带
434
+ # M=256 1.54x / 1.34x / 1.51x
435
+ # M=512 2.04x / 1.82x / 2.04x 两步快约一倍
436
+ # ⇒ 阈值取 64(保守:M=64 时融合还是赢的,且真实推理的 M 是 seq_len)。
437
+ #
438
+ # ⚠️ 这个 op 【没有 default 后端回退】(全库只注册了 cpu 与 cuda),
439
+ # 所以大 M 路径必须在这里自己实现。8-bit 的码表是 create_dynamic_map(),
440
+ # 与 quantize_blockwise 默认用的同一个 ⇒ 数值一致。
441
+ _GEMM_8BIT_FUSED_MAX_M = 64
442
+
443
+ @register_kernel("bitsandbytes::gemm_8bit", "cpu")
444
+ def _gemm_8bit_cpu(
445
+ A: torch.Tensor,
446
+ B: torch.Tensor,
447
+ absmax: torch.Tensor,
448
+ blocksize: int,
449
+ ) -> torch.Tensor:
450
+ """融合 8bit 反量化 GEMM:out = A @ dequant8(B)(B 为 uint8 码流,逐行块量化)。
451
+
452
+ 权重在整个乘法中保持 uint8(DRAM 流量为 fp32 的 1/4),无 fp32 临时张量。
453
+ 小 M(<= 64)走融合内核;大 M 退到"反量化 + 稠密 matmul"(见上面的分档说明)。
454
+ """
455
+ if A.numel() == 0:
456
+ return torch.empty((*A.shape[:-1], B.shape[0]), dtype=torch.float32, device=A.device)
457
+ A_f = A.reshape(-1, A.shape[-1]).contiguous()
458
+ B_c = B.contiguous()
459
+ absmax_c = absmax.contiguous().float()
460
+ M, K = A_f.shape
461
+ N = B_c.shape[0]
462
+
463
+ if M > _GEMM_8BIT_FUSED_MAX_M:
464
+ # 大 M:两步更快(实测 M=512 时 1.8~2.0x)
465
+ from bitsandbytes.functional import create_dynamic_map, dequantize_blockwise
466
+
467
+ code = create_dynamic_map().to(B_c.device)
468
+ W = dequantize_blockwise(B_c, absmax=absmax_c, code=code, blocksize=blocksize)
469
+ return (A_f @ W.t()).reshape(*A.shape[:-1], N)
470
+
471
+ out = torch.empty((M, N), dtype=torch.float32, device=A_f.device)
472
+ lib.cgemm_8bit_inference_cpu_fp32(
473
+ get_ptr(A_f),
474
+ get_ptr(B_c),
475
+ get_ptr(absmax_c),
476
+ get_ptr(out),
477
+ ct.c_longlong(M),
478
+ ct.c_longlong(N),
479
+ ct.c_longlong(K),
480
+ ct.c_longlong(A_f.stride(0)),
481
+ ct.c_longlong(B_c.stride(0)),
482
+ ct.c_longlong(out.stride(0)),
483
+ ct.c_longlong(blocksize),
484
+ )
485
+ return out.reshape(*A.shape[:-1], N)
486
+
487
+
488
+ # ==================== CPU Optimizer Kernels ====================
489
+
490
+
491
+ def _compute_update_norm_and_scale(
492
+ update: torch.Tensor,
493
+ unorm_vec: Optional[torch.Tensor],
494
+ max_unorm: float,
495
+ param_norm: float,
496
+ ) -> float:
497
+ """Compute trust-ratio scaling factor for LAMB/LARS and store update norm."""
498
+ if max_unorm <= 0.0:
499
+ return 1.0
500
+ unorm = torch.norm(update).item()
501
+ if unorm_vec is not None:
502
+ unorm_vec.fill_(unorm)
503
+ if unorm > max_unorm * param_norm:
504
+ return (max_unorm * param_norm) / unorm
505
+ return 1.0
506
+
507
+
508
+ @torch.no_grad()
509
+ def _optimizer_update_32bit_cpu(
510
+ optimizer_name: str,
511
+ g: torch.Tensor,
512
+ p: torch.Tensor,
513
+ state1: torch.Tensor,
514
+ state2: Optional[torch.Tensor],
515
+ unorm_vec: Optional[torch.Tensor],
516
+ max_unorm: float,
517
+ param_norm: float,
518
+ beta1: float,
519
+ beta2: float,
520
+ beta3: float,
521
+ alpha: float,
522
+ eps: float,
523
+ weight_decay: float,
524
+ step: int,
525
+ lr: float,
526
+ gnorm_scale: float,
527
+ skip_zeros: bool = False,
528
+ ) -> None:
529
+ g_float = g.float() * gnorm_scale
530
+ p_float = p.data.float()
531
+
532
+ if optimizer_name in ("adam", "lamb"):
533
+ # Adam / LAMB (2-state): m and v
534
+ state1.mul_(beta1).add_(g_float, alpha=1.0 - beta1)
535
+ state2.mul_(beta2).addcmul_(g_float, g_float, value=1.0 - beta2)
536
+
537
+ correction1 = 1.0 - beta1**step
538
+ correction2 = math.sqrt(1.0 - beta2**step)
539
+ step_size = -lr * correction2 / correction1
540
+
541
+ if weight_decay > 0.0:
542
+ p_float.mul_(1.0 - lr * weight_decay)
543
+
544
+ update = state1 / (state2.sqrt() + eps * correction2)
545
+
546
+ update_scale = _compute_update_norm_and_scale(update, unorm_vec, max_unorm, param_norm)
547
+ p_float.add_(update, alpha=step_size * update_scale)
548
+
549
+ elif optimizer_name == "ademamix":
550
+ # AdEMAMix (2-state): state1 shape is (2, *p.shape), state1[0]=m1, state1[1]=m2
551
+ m1 = state1[0]
552
+ m2 = state1[1]
553
+ nu = state2
554
+
555
+ m1.mul_(beta1).add_(g_float, alpha=1.0 - beta1)
556
+ m2.mul_(beta3).add_(g_float, alpha=1.0 - beta3)
557
+ nu.mul_(beta2).addcmul_(g_float, g_float, value=1.0 - beta2)
558
+
559
+ correction1 = 1.0 - beta1**step
560
+ correction2 = math.sqrt(1.0 - beta2**step)
561
+
562
+ if weight_decay > 0.0:
563
+ p_float.mul_(1.0 - lr * weight_decay)
564
+
565
+ mixed_momentum = (m1 / correction1) + (alpha * m2)
566
+ adaptive_term = (nu.sqrt() / correction2) + eps
567
+ p_float.add_(mixed_momentum / adaptive_term, alpha=-lr)
568
+
569
+ elif optimizer_name in ("momentum", "lars"):
570
+ # SGD with momentum / LARS (1-state)
571
+ g_wd = g_float.add(p_float, alpha=weight_decay) if weight_decay > 0.0 else g_float
572
+
573
+ if step == 1:
574
+ state1.copy_(g_wd)
575
+ else:
576
+ state1.mul_(beta1).add_(g_wd)
577
+
578
+ update_scale = _compute_update_norm_and_scale(state1, unorm_vec, max_unorm, param_norm)
579
+ p_float.add_(state1, alpha=-lr * update_scale)
580
+
581
+ elif optimizer_name == "lion":
582
+ # Lion (2-state sign update)
583
+ if weight_decay > 0.0:
584
+ p_float.mul_(1.0 - lr * weight_decay)
585
+
586
+ update = state1.mul(beta1).add(g_float, alpha=1.0 - beta1)
587
+ p_float.add_(update.sign(), alpha=-lr)
588
+
589
+ state1.mul_(beta2).add_(g_float, alpha=1.0 - beta2)
590
+
591
+ elif optimizer_name == "rmsprop":
592
+ # RMSprop (1-state)
593
+ g_wd = g_float.add(p_float, alpha=weight_decay) if weight_decay > 0.0 else g_float
594
+ state1.mul_(beta1).addcmul_(g_wd, g_wd, value=1.0 - beta1)
595
+
596
+ update = g_wd / (state1.sqrt() + eps)
597
+ update_scale = _compute_update_norm_and_scale(update, unorm_vec, max_unorm, param_norm)
598
+ p_float.add_(update, alpha=-lr * update_scale)
599
+
600
+ elif optimizer_name == "adagrad":
601
+ # Adagrad (1-state)
602
+ g_wd = g_float.add(p_float, alpha=weight_decay) if weight_decay > 0.0 else g_float
603
+ state1.addcmul_(g_wd, g_wd, value=1.0)
604
+
605
+ update = g_wd / (state1.sqrt() + eps)
606
+ p_float.add_(update, alpha=-lr)
607
+
608
+ else:
609
+ raise ValueError(f"Unsupported optimizer for CPU: {optimizer_name}")
610
+
611
+ # Write back to original precision
612
+ p.data.copy_(p_float)
613
+
614
+
615
+ register_kernel("bitsandbytes::optimizer_update_32bit", "cpu")(_optimizer_update_32bit_cpu)
616
+
617
+
618
+ @torch.no_grad()
619
+ def _dequant_blockwise_fp32_direct(
620
+ A_uint8: torch.Tensor, absmax: torch.Tensor, code: torch.Tensor, blocksize: int
621
+ ) -> torch.Tensor:
622
+ return torch.ops.bitsandbytes.dequantize_blockwise(A_uint8, absmax, code, blocksize, torch.float32)
623
+
624
+
625
+ def _quant_blockwise_fp32_direct(
626
+ A_fp32: torch.Tensor, code: torch.Tensor, absmax_out: torch.Tensor, out_uint8: torch.Tensor, blocksize: int
627
+ ) -> None:
628
+ out, absmax = torch.ops.bitsandbytes.quantize_blockwise(A_fp32, code, blocksize)
629
+ out_uint8.copy_(out)
630
+ absmax_out.copy_(absmax)
631
+
632
+
633
+ def _optimizer_update_8bit_blockwise_cpu(
634
+ optimizer_name: str,
635
+ g: torch.Tensor,
636
+ p: torch.Tensor,
637
+ state1: torch.Tensor,
638
+ state2: Optional[torch.Tensor],
639
+ beta1: float,
640
+ beta2: float,
641
+ beta3: float,
642
+ alpha: float,
643
+ eps: float,
644
+ step: int,
645
+ lr: float,
646
+ qmap1: torch.Tensor,
647
+ qmap2: Optional[torch.Tensor],
648
+ absmax1: torch.Tensor,
649
+ absmax2: Optional[torch.Tensor],
650
+ weight_decay: float,
651
+ gnorm_scale: float,
652
+ skip_zeros: bool = False,
653
+ ) -> None:
654
+ # Fused single-pass C kernel (dequant + update + p update + requant in one
655
+ # sweep). Falls back to the composite PyTorch path below when the native
656
+ # library is unavailable or the optimizer is not supported by the kernel.
657
+ _OPT_IDS = {"adam": 0, "lamb": 0, "momentum": 1, "lars": 1, "lion": 2, "rmsprop": 3, "adagrad": 4, "ademamix": 5}
658
+ n = p.numel()
659
+ # The C kernel walks raw pointers and assumes C-contiguous layouts: a
660
+ # non-contiguous grad (transposed / sliced views are common in training)
661
+ # would silently read the wrong elements and corrupt the states.
662
+ contiguous_layout = (
663
+ g.is_contiguous()
664
+ and p.is_contiguous()
665
+ and state1.is_contiguous()
666
+ and (state2 is None or state2.is_contiguous())
667
+ and qmap1.is_contiguous()
668
+ and (qmap2 is None or qmap2.is_contiguous())
669
+ and absmax1.is_contiguous()
670
+ and (absmax2 is None or absmax2.is_contiguous())
671
+ )
672
+ if (
673
+ not isinstance(lib, ErrorHandlerMockBNBNativeLibrary)
674
+ and contiguous_layout
675
+ and optimizer_name in _OPT_IDS
676
+ and g.dtype in (torch.float32, torch.bfloat16, torch.float16)
677
+ # ademamix packs a second momentum into state1[n:2n] with its absmax in
678
+ # absmax1[blocks:2*blocks]; the flat C layout requires that packing.
679
+ and (optimizer_name != "ademamix" or state1.numel() >= 2 * n)
680
+ ):
681
+ try:
682
+ dtype_id = {torch.float32: 0, torch.bfloat16: 1, torch.float16: 2}[g.dtype]
683
+ # Every scalar needs an explicit ctypes width: a bare Python int is
684
+ # marshalled as 32-bit c_int, but `n` is a long long. libffi writes
685
+ # only the low 4 bytes of its 8-byte stack slot; the C side reads
686
+ # all 8, so stale stack bits leak into n (observed: 0x7FFF00004000
687
+ # for n=16384) and the kernel walks out of bounds -> SIGSEGV.
688
+ lib.coptimizer_update_8bit_blockwise_cpu(
689
+ ct.c_int(_OPT_IDS[optimizer_name]),
690
+ get_ptr(g),
691
+ get_ptr(p),
692
+ get_ptr(state1),
693
+ get_ptr(state2) if state2 is not None else ct.c_void_p(0),
694
+ ct.c_float(beta1),
695
+ ct.c_float(beta2),
696
+ ct.c_float(beta3),
697
+ ct.c_float(alpha),
698
+ ct.c_float(eps),
699
+ ct.c_int(step),
700
+ ct.c_float(lr),
701
+ get_ptr(qmap1),
702
+ get_ptr(qmap2) if qmap2 is not None else ct.c_void_p(0),
703
+ get_ptr(absmax1),
704
+ get_ptr(absmax2) if absmax2 is not None else ct.c_void_p(0),
705
+ ct.c_float(weight_decay),
706
+ ct.c_float(gnorm_scale),
707
+ ct.c_int(1 if skip_zeros else 0),
708
+ ct.c_longlong(n),
709
+ ct.c_int(dtype_id),
710
+ )
711
+ return
712
+ except (AttributeError, RuntimeError):
713
+ pass # old native library without the fused entry point
714
+
715
+ blocksize = 256
716
+
717
+ # Dequantize states
718
+ if optimizer_name == "ademamix" and absmax1.ndim == 2:
719
+ s1_1 = _dequant_blockwise_fp32_direct(state1[0], absmax1[0], qmap1, blocksize)
720
+ s1_2 = _dequant_blockwise_fp32_direct(state1[1], absmax1[1], qmap1, blocksize)
721
+ state1_fp32 = torch.stack([s1_1, s1_2])
722
+ else:
723
+ state1_fp32 = _dequant_blockwise_fp32_direct(state1, absmax1, qmap1, blocksize)
724
+
725
+ state2_fp32 = None
726
+ if state2 is not None and qmap2 is not None and absmax2 is not None:
727
+ state2_fp32 = _dequant_blockwise_fp32_direct(state2, absmax2, qmap2, blocksize)
728
+
729
+ grad = g.float() * gnorm_scale
730
+ p_fp32 = p.data.float()
731
+
732
+ if optimizer_name in ("adam", "lamb"):
733
+ state1_fp32.mul_(beta1).add_(grad, alpha=1.0 - beta1)
734
+ state2_fp32.mul_(beta2).addcmul_(grad, grad, value=1.0 - beta2)
735
+
736
+ correction1 = 1.0 - beta1**step
737
+ correction2 = math.sqrt(1.0 - beta2**step)
738
+
739
+ denom = (state2_fp32.sqrt() / correction2).add_(eps)
740
+ if weight_decay > 0.0:
741
+ p_fp32.mul_(1.0 - lr * weight_decay)
742
+ p_fp32.addcdiv_(state1_fp32, denom, value=-lr / correction1)
743
+
744
+ elif optimizer_name == "ademamix":
745
+ m1_fp32, m2_fp32 = state1_fp32[0], state1_fp32[1]
746
+ nu_fp32 = state2_fp32
747
+
748
+ m1_fp32.mul_(beta1).add_(grad, alpha=1.0 - beta1)
749
+ m2_fp32.mul_(beta3).add_(grad, alpha=1.0 - beta3)
750
+ nu_fp32.mul_(beta2).addcmul_(grad, grad, value=1.0 - beta2)
751
+
752
+ correction1 = 1.0 - beta1**step
753
+ correction2 = math.sqrt(1.0 - beta2**step)
754
+
755
+ update = (m1_fp32 / correction1 + alpha * m2_fp32) / (nu_fp32.sqrt() / correction2 + eps)
756
+ if weight_decay > 0.0:
757
+ p_fp32.mul_(1.0 - lr * weight_decay)
758
+ p_fp32.add_(update, alpha=-lr)
759
+
760
+ state1_fp32 = torch.stack([m1_fp32, m2_fp32])
761
+
762
+ elif optimizer_name in ("momentum", "lars"):
763
+ grad.add_(p_fp32, alpha=weight_decay)
764
+ if step == 1:
765
+ state1_fp32.copy_(grad)
766
+ else:
767
+ state1_fp32.mul_(beta1).add_(grad)
768
+ p_fp32.add_(state1_fp32, alpha=-lr)
769
+
770
+ elif optimizer_name == "lion":
771
+ if weight_decay > 0.0:
772
+ p_fp32.mul_(1.0 - lr * weight_decay)
773
+
774
+ update_dir = torch.sign(state1_fp32.mul(beta1) + grad.mul(1.0 - beta1))
775
+ p_fp32.add_(update_dir, alpha=-lr)
776
+
777
+ state1_fp32.mul_(beta2).add_(grad, alpha=1.0 - beta2)
778
+
779
+ elif optimizer_name == "rmsprop":
780
+ grad.add_(p_fp32, alpha=weight_decay)
781
+ state1_fp32.mul_(beta1).addcmul_(grad, grad, value=1.0 - beta1)
782
+ p_fp32.addcdiv_(grad, state1_fp32.sqrt().add_(eps), value=-lr)
783
+
784
+ elif optimizer_name == "adagrad":
785
+ grad.add_(p_fp32, alpha=weight_decay)
786
+ state1_fp32.addcmul_(grad, grad, value=1.0)
787
+ p_fp32.addcdiv_(grad, state1_fp32.sqrt().add_(eps), value=-lr)
788
+
789
+ else:
790
+ raise ValueError(f"Unsupported optimizer for CPU 8-bit: {optimizer_name}")
791
+
792
+ p.data.copy_(p_fp32)
793
+
794
+ # Re-quantize states
795
+ if optimizer_name == "ademamix":
796
+ _quant_blockwise_fp32_direct(state1_fp32[0], qmap1, absmax1[0], state1[0], blocksize)
797
+ _quant_blockwise_fp32_direct(state1_fp32[1], qmap1, absmax1[1], state1[1], blocksize)
798
+ _quant_blockwise_fp32_direct(state2_fp32, qmap2, absmax2, state2, blocksize)
799
+ else:
800
+ _quant_blockwise_fp32_direct(state1_fp32, qmap1, absmax1, state1, blocksize)
801
+ if state2_fp32 is not None:
802
+ _quant_blockwise_fp32_direct(state2_fp32, qmap2, absmax2, state2, blocksize)
803
+
804
+
805
+ register_kernel("bitsandbytes::optimizer_update_8bit_blockwise", "cpu")(_optimizer_update_8bit_blockwise_cpu)