bitsandbytes-cpu-fork 0.50.2.dev2__py3-none-win_amd64.whl → 0.50.2.dev3__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_cpu_cli.py CHANGED
@@ -1,949 +1,1012 @@
1
- """The `bitsandbytes-cpu` command: one entry point that answers "what is this and how do
2
- I call it" without a browser.
3
-
4
- Why a CLI rather than only a document: a reference that lives in a file is read by the
5
- people who already know it exists. `bitsandbytes-cpu help` is discoverable from the
6
- moment pip finishes.
7
-
8
- WHY THIS MODULE IS NOT INSIDE THE PACKAGE
9
- -----------------------------------------
10
- `bitsandbytes/__init__.py` imports torch, and torch is deliberately not a dependency of
11
- this distribution: on PyPI that name is the CUDA build, so depending on it would pull
12
- 2.5 GB of nvidia-* wheels onto a machine with no NVIDIA device. The direct consequence
13
- is that a user can have this package installed with torch absent -- and that is exactly
14
- the user who runs `help`, because the help is what says to install torch. Pointed at
15
- `bitsandbytes.cli:main`, the console script died with
16
- `ModuleNotFoundError: No module named 'torch'` before printing a word (observed on
17
- Ubuntu, Python 3.14, after a plain `pip install` of the sdist).
18
-
19
- So the reference lives in a module that imports nothing but the standard library.
20
- `detect` and `doctor` load what they need out of the package directory by path, which
21
- works with no torch; only `selftest` needs it, and says so instead of raising.
22
-
23
- What it is built around is not the upstream API surface -- that is documented upstream --
24
- but the things this fork adds and the way they have to be called on a CPU. Every example
25
- under a heading is a call that was executed on an AVX2 machine before it was written
26
- down, including the ones that are supposed to fail.
27
-
28
- The output follows the shape people already know from tools like nmap: a usage line,
29
- ALL-CAPS sections, one `thing: what it does` per line, no blank lines inside a section,
30
- and the code for the entry indented under it. A signature alone does not tell anyone how
31
- to use a feature; the example does.
32
-
33
- Sections can be read one at a time (`bitsandbytes-cpu help 8bitopt`), and the same tables
34
- render as markdown (`help --markdown`), so the terminal output and
35
- docs_cpu/API_REFERENCE.md cannot drift apart.
36
-
37
- bitsandbytes-cpu help
38
- bitsandbytes-cpu help 8bitopt
39
- bitsandbytes-cpu detect
40
- bitsandbytes-cpu doctor
41
- bitsandbytes-cpu selftest
42
- bitsandbytes-cpu version
43
- """
44
- from __future__ import annotations
45
-
46
- import argparse
47
- import importlib.util
48
- import os
49
- import platform
50
- import sys
51
- import textwrap
52
-
53
- WIDTH = 78
54
-
55
- USAGE = ("Usage: bitsandbytes-cpu {help|detect|doctor|selftest|version} [section]\n"
56
- " bitsandbytes-cpu help 8bitopt one section instead of all of them")
57
-
58
- # Sections are (key, title, [(left, right, [example lines], lang), ...]).
59
- # key the word to pass to `help <key>`
60
- # title printed upper-case by help, as-is by --markdown
61
- # left what you type or call
62
- # right one clause about it
63
- # example the actual call, executed by tools/verify_help_examples.py
64
- # lang "python" (verified), "bash"/"text" (not executable)
65
- # Keep `right` to a single clause; the examples carry the detail.
66
- PY = "python"
67
- SH = "bash"
68
-
69
- SECTIONS = [
70
- ("intro", "What the fork adds", [
71
- ("CPU kernels",
72
- "the CUDA kernels are gone: AVX2 (x86-64) and NEON (ARM64) C, compiled into "
73
- "the wheel", [], PY),
74
- ("bitsandbytes.optim",
75
- "8-bit optimizers: ~2.3 bytes of state per parameter instead of 8", [], PY),
76
- ("bitsandbytes.nn",
77
- "4-bit and 8-bit layers that run on a cpu tensor", [], PY),
78
- ("bitsandbytes.functional",
79
- "the quantisation primitives plus the fused CPU kernels", [], PY),
80
- ("torch_cpu_kit",
81
- "env vars, thread counts, oneDNN, memory watch, DataLoader defaults", [], PY),
82
- ("detect_cpu",
83
- "one dict describing this machine and the settings that follow from it", [], PY),
84
- ("gdn_cpu",
85
- "fused Gated DeltaNet for Qwen3-Next / Qwen3.5 linear attention", [], PY),
86
- ("disk_balancer",
87
- "spills cold parameters to another disk when RAM runs out", [], PY),
88
- ("bitsandbytes-cpu",
89
- "this command; pip also installs it as bnb-cpu", [], PY),
90
- ]),
91
- ("start", "First run", [
92
- ("bitsandbytes-cpu detect",
93
- "what the CPU is, and the thread count it recommends", [], SH),
94
- ("bitsandbytes-cpu doctor",
95
- "which native library will load, and whether every kernel symbol is in it", [], SH),
96
- ("bitsandbytes-cpu selftest",
97
- "runs a 4-bit layer, an 8-bit optimizer step and the GDN kernel; exit 1 on failure",
98
- [], SH),
99
- ("bitsandbytes-cpu version", "package, python, torch, platform", [], SH),
100
- ("import bitsandbytes on a CPU host",
101
- "succeeds with no CUDA anywhere; the library loads on first kernel use", [
102
- "import bitsandbytes as bnb",
103
- "print(bnb.__version__, bnb.functional.has_avx512bf16())",
104
- ], PY),
105
- ]),
106
- ("4bit", "4-bit on CPU", [
107
- ("quantise one Linear layer",
108
- "assign Params4bit, then call .to('cpu') -- .to() is what quantises", [
109
- "import torch, torch.nn as nn, bitsandbytes as bnb",
110
- "",
111
- "src = nn.Linear(4096, 4096, bias=False) # an fp32 layer to convert",
112
- "q = bnb.nn.Linear4bit(4096, 4096, bias=False,",
113
- " compute_dtype=torch.float32, # see `help threads`",
114
- " quant_type='nf4', compress_statistics=True)",
115
- "q.weight = bnb.nn.Params4bit(src.weight.data, requires_grad=False,",
116
- " quant_type='nf4', compress_statistics=True)",
117
- "q = q.to('cpu') # <- without this the first forward raises",
118
- "",
119
- "x = torch.randn(8, 4096) # your input",
120
- "y = q(x) # weight goes back to fp32 inside the forward",
121
- ], PY),
122
- ("what the forward does",
123
- "M=1 takes the fused gemv_4bit kernel; M>1 unpacks to fp32 and calls a dense GEMM",
124
- [], PY),
125
- ("quantise a whole model in place", "swap every nn.Linear, leave the head alone", [
126
- "def to_4bit(module, skip=('lm_head',)):",
127
- " for name, child in module.named_children():",
128
- " if name in skip:",
129
- " continue",
130
- " if isinstance(child, nn.Linear):",
131
- " new = bnb.nn.Linear4bit(child.in_features, child.out_features,",
132
- " bias=child.bias is not None,",
133
- " compute_dtype=torch.float32,",
134
- " quant_type='nf4', compress_statistics=True)",
135
- " new.weight = bnb.nn.Params4bit(child.weight.data,",
136
- " requires_grad=False,",
137
- " quant_type='nf4',",
138
- " compress_statistics=True)",
139
- " setattr(module, name, new.to('cpu'))",
140
- " else:",
141
- " to_4bit(child, skip)",
142
- " return module",
143
- "",
144
- "to_4bit(model)",
145
- "print(sum(p.numel() * p.element_size() for p in model.parameters()) / 1e6, 'MB')",
146
- ], PY),
147
- ("save and load a 4-bit layer",
148
- "the state dict carries the packed weight and the quantisation state; a "
149
- "fresh Linear4bit cannot load it back, so the parameter is rebuilt", [
150
- "src = nn.Linear(4096, 4096, bias=False)",
151
- "q = bnb.nn.Linear4bit(4096, 4096, bias=False, compute_dtype=torch.float32,",
152
- " quant_type='nf4', compress_statistics=True)",
153
- "q.weight = bnb.nn.Params4bit(src.weight.data, requires_grad=False,",
154
- " quant_type='nf4', compress_statistics=True)",
155
- "q = q.to('cpu')",
156
- "x = torch.randn(8, 4096)",
157
- "y0 = q(x)",
158
- "",
159
- "torch.save(q.state_dict(), 'layer4bit.pt') # weight, weight.absmax,",
160
- " # weight.quant_map, nested_*",
161
- "",
162
- "sd = torch.load('layer4bit.pt')",
163
- "state = bnb.functional.QuantState.from_dict(",
164
- " {k[len('weight.') :]: v for k, v in sd.items() if k.startswith('weight.')},",
165
- " device=torch.device('cpu'))",
166
- "fresh = bnb.nn.Linear4bit(4096, 4096, bias=False,",
167
- " compute_dtype=torch.float32, quant_type='nf4')",
168
- "pw = bnb.nn.Params4bit(sd['weight'], requires_grad=False,",
169
- " quant_type=state.quant_type, blocksize=state.blocksize,",
170
- " bnb_quantized=True, quant_storage=sd['weight'].dtype)",
171
- "pw.quant_state = state",
172
- "fresh.weight = pw",
173
- "assert (fresh(x) - y0).abs().max() < 1e-5",
174
- ], PY),
175
- ("Embedding4bit",
176
- "the same for a token table; there is no padding_idx argument", [
177
- "# table: a [vocab, dim] matrix, e.g. model.get_input_embeddings().weight",
178
- "emb = bnb.nn.Embedding4bit(table.shape[0], table.shape[1], quant_type='nf4')",
179
- "emb.weight = bnb.nn.Params4bit(table, requires_grad=False, quant_type='nf4')",
180
- "emb = emb.to('cpu')",
181
- ], PY),
182
- ]),
183
- ("qlora", "Training a frozen 4-bit base", [
184
- ("freeze the base, train adapters",
185
- "4-bit is a storage format: the base still computes in fp32", [
186
- "to_4bit(model) # the helper from `help 4bit`",
187
- "for p in model.parameters():",
188
- " p.requires_grad_(False)",
189
- "",
190
- "adapters = nn.Linear(8, 8) # wherever your LoRA layers come from",
191
- "opt = AdamW8bit(adapters.parameters(), lr=1e-4)",
192
- "loss = loss_fn(adapters(model(x)), y)",
193
- "loss.backward()",
194
- "opt.step(); opt.zero_grad()",
195
- ], PY),
196
- ("4-bit does not make the matmul cheaper",
197
- "the forward unpacks to fp32, so FLOPs are unchanged; only RAM and disk shrink",
198
- [], PY),
199
- ("gradients into a 4-bit layer",
200
- "requires_grad=True on a Params4bit is not supported here; keep the base frozen",
201
- [], PY),
202
- ]),
203
- ("8bitopt", "8-bit optimizer (start here)", [
204
- ("swap the optimizer",
205
- "same constructor arguments as the torch optimizer it replaces", [
206
- "import bitsandbytes as bnb",
207
- "",
208
- "opt = bnb.optim.AdamW8bit(model.parameters(), lr=2e-5, weight_decay=0.01)",
209
- "opt.zero_grad()",
210
- "loss = loss_fn(model(x), y)",
211
- "loss.backward()",
212
- "opt.step() # the loop is otherwise unchanged",
213
- ], PY),
214
- ("what you get",
215
- "18688 bytes of state per parameter here, against torch AdamW's 65540 (3.5x)", [
216
- "p = nn.Parameter(torch.randn(8192))",
217
- "opt = bnb.optim.AdamW8bit([p], lr=1e-3)",
218
- "p.grad = torch.randn_like(p)",
219
- "opt.step()",
220
- "nbytes = sum(v.numel() * v.element_size()",
221
- " for v in opt.state[p].values() if torch.is_tensor(v))",
222
- "print(nbytes, 'bytes for', p.numel(), 'params =', nbytes / p.numel(), 'B/param')",
223
- "print(sorted(opt.state[p])) # absmax1 absmax2 qmap1 qmap2 state1 state2 step",
224
- ], PY),
225
- ("every optimizer has the same three names",
226
- "<Name>8bit, <Name>32bit and Paged<Name>8bit", [
227
- "from bitsandbytes.optim import (Adam8bit, AdamW8bit, AdamW32bit, Lion8bit,",
228
- " SGD8bit, LAMB8bit, LARS8bit, RMSprop8bit,",
229
- " Adagrad8bit, AdEMAMix8bit, PagedAdamW8bit)",
230
- "opt = Lion8bit(model.parameters(), lr=1e-4, weight_decay=0.1)",
231
- ], PY),
232
- ("only the parameters you step",
233
- "a frozen base needs no optimizer state; passing it wastes the memory you saved", [
234
- "opt = bnb.optim.AdamW8bit([p for p in model.parameters() if p.requires_grad],",
235
- " lr=1e-4)",
236
- ], PY),
237
- ("min_8bit_size=4096",
238
- "anything smaller keeps float32 state, so tiny tensors show no saving", [
239
- "small = [nn.Parameter(torch.randn(64))]",
240
- "opt = bnb.optim.AdamW8bit(small, lr=1e-3) # default: fp32 state",
241
- "p = small[0]",
242
- "p.grad = torch.randn_like(p)",
243
- "opt.step()",
244
- "print({k: str(v.dtype) for k, v in opt.state[p].items() if torch.is_tensor(v)})",
245
- "# {'state1': 'torch.float32', 'state2': 'torch.float32'} -- no absmax, no gain",
246
- "",
247
- "opt = bnb.optim.AdamW8bit(small, lr=1e-3, min_8bit_size=0) # force 8-bit",
248
- "opt.step()",
249
- "print({k: str(v.dtype) for k, v in opt.state[p].items() if torch.is_tensor(v)})",
250
- "# {'state1': 'torch.uint8', ..., 'absmax1': 'torch.float32', ...}",
251
- ], PY),
252
- ("state_dict", "saves and loads like any other torch optimizer", [
253
- "opt = bnb.optim.AdamW8bit(model.parameters(), lr=1e-3)",
254
- "torch.save(opt.state_dict(), 'opt.pt')",
255
- "opt.load_state_dict(torch.load('opt.pt', weights_only=False))",
256
- ], PY),
257
- ("the step is not slower",
258
- "the fused kernel drops the dequantise-then-update round trip: 0.74x an fp32 "
259
- "AdamW step, measured in one process", [], PY),
260
- ]),
261
- ("kernels", "Fused kernels (call them directly)", [
262
- ("fused_dequant_linear_8bit(...)",
263
- "out = A @ dequant8(wq).T with the weight never unpacked to fp32; the "
264
- "arguments are (A, wq, absmax, blocksize)", [
265
- "w = torch.randn(512, 512) * 0.02 # your fp32 weight",
266
- "A = torch.randn(4, 512) # your activations",
267
- "code = bnb.functional.create_linear_map() # the 8-bit codebook",
268
- "wq, state = bnb.functional.quantize_blockwise(w, code=code, blocksize=64)",
269
- "out = bnb.functional.fused_dequant_linear_8bit(A, wq, state.absmax, 64)",
270
- "ref = A @ w.t()",
271
- "assert ((out - ref).norm() / ref.norm()).item() < 0.02",
272
- ], PY),
273
- ("A may have leading dimensions",
274
- "A.shape == (..., K) in, (..., N) out -- flatten (B,T,H,W) yourself first", [
275
- "w = torch.randn(512, 512) * 0.02",
276
- "code = bnb.functional.create_linear_map()",
277
- "wq, state = bnb.functional.quantize_blockwise(w, code=code, blocksize=64)",
278
- "v = torch.randn(2, 3, 512)",
279
- "out = bnb.functional.fused_dequant_linear_8bit(v, wq, state.absmax, 64)",
280
- "print(out.shape) # (2, 3, 512) -> (2, 3, 512)",
281
- ], PY),
282
- ("K % blocksize must be 0",
283
- "otherwise the kernel refuses rather than reading the wrong row", [], PY),
284
- ("optimizer_update_8bit_blockwise(...)",
285
- "the fused 8-bit step the optimizers call, with the whole argument list", [
286
- "N, BLOCK = 4096, 64",
287
- "p = nn.Parameter(torch.randn(N) * 0.1)",
288
- "g = torch.randn(N) * 0.05",
289
- "state1 = torch.zeros(N, dtype=torch.uint8) # quantised exp_avg",
290
- "state2 = torch.zeros(N, dtype=torch.uint8) # quantised exp_avg_sq",
291
- "absmax1 = torch.zeros(N // BLOCK)",
292
- "absmax2 = torch.zeros(N // BLOCK)",
293
- "qmap1 = bnb.functional.create_dynamic_map(signed=True, total_bits=8)",
294
- "qmap2 = qmap1.clone()",
295
- "bnb.functional.optimizer_update_8bit_blockwise(",
296
- " 'adam', g, p, state1, state2,",
297
- " 0.9, 0.999, 0.0, # beta1, beta2, beta3",
298
- " 1.0, 1e-8, # alpha, eps",
299
- " 1, 1e-3, # step, lr",
300
- " qmap1, qmap2, absmax1, absmax2,",
301
- " weight_decay=0.0, gnorm_scale=1.0)",
302
- "# checked against torch.optim.AdamW in tests/test_cpu_e2e.py: the two agree",
303
- "# to 1e-5 after one step",
304
- ], PY),
305
- ("gemv_4bit(A, q, state=state)",
306
- "the M=1 4-bit path on its own; state comes from quantize_4bit", [
307
- "w = torch.randn(512, 512) * 0.02",
308
- "A = torch.randn(1, 512)",
309
- "q, state = bnb.functional.quantize_4bit(w, quant_type='nf4', blocksize=64,",
310
- " compress_statistics=True)",
311
- "row = bnb.functional.gemv_4bit(A, q, state=state)",
312
- "print(q.shape, q.dtype, row.shape) # packed uint8, one row out",
313
- ], PY),
314
- ("int8_linear_matmul(A, B)",
315
- "int8 x int8 -> int32, both operands (M,K) and (N,K); the LLM.int8() core", [
316
- "Ai = torch.randint(-100, 100, (8, 64), dtype=torch.int8)",
317
- "Bi = torch.randint(-100, 100, (16, 64), dtype=torch.int8)",
318
- "out = bnb.functional.int8_linear_matmul(Ai, Bi)",
319
- "assert torch.equal(out, Ai.to(torch.int32) @ Bi.to(torch.int32).t())",
320
- ], PY),
321
- ("int8_vectorwise_quant(A)",
322
- "returns (int8 data, per-row fp32 scales, outlier column indices or None)", [
323
- "A = torch.randn(256, 512)",
324
- "qi, scales, outliers = bnb.functional.int8_vectorwise_quant(A)",
325
- "print(qi.dtype, qi.shape, scales.shape) # int8, (256, 512), (256,)",
326
- ], PY),
327
- ]),
328
- ("toolkit", "torch_cpu_kit and detect_cpu", [
329
- ("apply_env() BEFORE import torch",
330
- "OpenMP reads these once, when the runtime loads; later calls do nothing", [
331
- "import bitsandbytes.torch_cpu_kit as tck",
332
- "tck.apply_env() # OMP/MKL_NUM_THREADS = physical cores, OMP_WAIT_POLICY",
333
- "import torch # torch now starts with those settings",
334
- ], PY),
335
- ("init(model_kind=...) after import torch",
336
- "thread pools, interop=1, oneDNN on, fp32 matmul precision left alone on AVX2", [
337
- "import torch, bitsandbytes.torch_cpu_kit as tck",
338
- "print(tck.init(model_kind='lm')) # 'lm', 'vision', 'diffusion', or None",
339
- ], PY),
340
- ("physical_cores()",
341
- "half of os.cpu_count() on an SMT machine -- the number to use for GEMM", [
342
- "torch.set_num_threads(tck.physical_cores()) # 6, not 12, on an i5-10400",
343
- ], PY),
344
- ("mem_report()", "(process RSS MB, system available MB)", [
345
- "rss, avail = tck.mem_report()",
346
- "print(f'{rss:.0f} MB in use, {avail:.0f} MB available')",
347
- ], PY),
348
- ("start_mem_monitor(interval=20.0)",
349
- "background log line; returns the Event that stops it", [
350
- "stop = tck.start_mem_monitor(interval=20.0)",
351
- "tck.suspend_mem_monitor(stop) # or stop.set()",
352
- ], PY),
353
- ("fast_loader(dataset, batch_size=32)",
354
- "DataLoader with pin_memory=False and 2 persistent workers", [
355
- "loader = tck.fast_loader(dataset, batch_size=32)",
356
- "print(loader.num_workers, loader.pin_memory) # 2 False",
357
- ], PY),
358
- ("make_contiguous_(model)",
359
- "in place: transposed parameters make the C kernels fall back to a slow path", [
360
- "tck.make_contiguous_(model)",
361
- ], PY),
362
- ("detect()", "the dict behind `bitsandbytes-cpu detect`", [
363
- "from bitsandbytes.detect_cpu import detect",
364
- "d = detect()",
365
- "threads = d['recommend_threads']['lm'] # also 'diffusion', 'image'",
366
- "print(d['cpu_capability'], d['physical_cores'], threads)",
367
- ], PY),
368
- ]),
369
- ("threads", "Threads and precision", [
370
- ("threads = physical cores",
371
- "measured on an i5-10400: 6 threads, 203 s per 1024x1024 image step; 12 threads, 225 s",
372
- [], PY),
373
- ("bf16 is emulated on AVX2",
374
- "no hardware bf16 means software conversion: 5-12x slower at 512x512, worse at LLM shapes",
375
- [], PY),
376
- ("compute_dtype=torch.float32",
377
- "for Linear4bit, Linear8bitLt and Embedding4bit; bf16 there is the single "
378
- "biggest performance mistake on these CPUs", [], PY),
379
- ("tck.autocast()",
380
- "a no-op on AVX2: recommended_autocast() returns float32, so the block stays fp32", [
381
- "with tck.autocast(): # nullcontext on this machine, real on AMX/AVX512-BF16",
382
- " out = model(x)",
383
- ], PY),
384
- ("float32_matmul_precision",
385
- "left at 'highest' on AVX2; only a CPU with bf16 hardware is put on 'high'", [], PY),
386
- ]),
387
- ("memory", "Memory", [
388
- ("plan for the peak, not the weights",
389
- "a 4-bit model still materialises fp32 activations and one unpacked layer at a time",
390
- [], PY),
391
- ("start_mem_monitor", "a log line every 20 s, on a daemon thread", [], PY),
392
- ("RAM below ~8 GB free",
393
- "detect() sets recommend_disk_balancer, and `help disks` is the answer", [], PY),
394
- ("MIN_8BIT_SIZE",
395
- "the optimizer, not you, decides: below 4096 elements a parameter stays 32-bit", [], PY),
396
- ]),
397
- ("gdn", "Gated DeltaNet kernel (Qwen3-Next)", [
398
- ("patch_transformers()",
399
- "routes HuggingFace Qwen3-Next / Qwen3.5 through the fused kernel; True if applied", [
400
- "from bitsandbytes import gdn_cpu",
401
- "print(gdn_cpu.patch_transformers()) # True once the patch is in place",
402
- "",
403
- "# then load the model as usual -- the layer now runs on the fused kernel:",
404
- "# model = AutoModelForCausalLM.from_pretrained('Qwen/Qwen3-Next-80B-A3B-Instruct')",
405
- ], PY),
406
- ("patch_fla()", "the same for the fla library", [], PY),
407
- ("load_native()",
408
- "loads the shared library without patching anything; raises with the paths it tried", [
409
- "from bitsandbytes import gdn_cpu",
410
- "lib = gdn_cpu.load_native()",
411
- ], PY),
412
- ("fused_recurrent_gated_delta_rule(...)",
413
- "the kernel itself; the arguments are q, k, v, beta, g, scale, initial_state, "
414
- "output_final_state, use_qk_l2norm_in_kernel, head_first, cu_seqlens", [], PY),
415
- ("it has an autograd Function",
416
- "so it trains, not just infers; the reference path is kept for verification", [], PY),
417
- ]),
418
- ("disks", "Disk balancer", [
419
- ("add_flash_args(parser)",
420
- "adds --flash, --flash-paths, --flash-sizes, --flash-speed, --flash-threshold "
421
- "and --flash-keep to your own training script", [], PY),
422
- ("parse_flash_args(args)",
423
- "args -> DiskBalancerConfig, or None when --flash was not given", [
424
- "import argparse, bitsandbytes.disk_balancer as db",
425
- "ap = argparse.ArgumentParser()",
426
- "db.add_flash_args(ap)",
427
- "cfg = db.parse_flash_args(ap.parse_args(['--flash']))",
428
- "print(cfg.mode, cfg.memory_threshold, cfg.min_param_size)",
429
- ], PY),
430
- ("DiskLoadBalancer(config)",
431
- "attach_model() then start(); update_step() once per training step checks RAM "
432
- "and migrates the cold parameters", [], PY),
433
- ("in a training loop",
434
- "the whole integration is three lines and one call per step", [
435
- "# cfg comes from parse_flash_args(...); see the entry above",
436
- "if cfg:",
437
- " bal = db.DiskLoadBalancer(cfg).attach_model(model).start()",
438
- " for xb, yb in loader:",
439
- " loss = loss_fn(model(xb), yb)",
440
- " loss.backward(); opt.step(); opt.zero_grad()",
441
- " bal.update_step()",
442
- ], PY),
443
- ("cold tensors by hand",
444
- "put_cold(key, tensor) / get_cold(key) / drop(key) / stats(); put_cold frees "
445
- "the tensor by replacing its storage with an empty one", [
446
- "if cfg:",
447
- " bal.put_cold('text_embeds', embeds) # any tensor you want off the heap",
448
- " embeds = bal.get_cold('text_embeds')",
449
- " print(bal.stats())",
450
- ], PY),
451
- ("it is a context manager",
452
- "cleanup() flushes the write queue and removes the staging files", [
453
- "if cfg:",
454
- " with db.DiskLoadBalancer(cfg) as bal:",
455
- " ...",
456
- ], PY),
457
- ]),
458
- ("layers", "Reference: bitsandbytes.nn", [
459
- ("Linear4bit(input_features, output_features, bias=True, compute_dtype=None,\n"
460
- " compress_statistics=True, quant_type='fp4',\n"
461
- " quant_storage=torch.uint8, device=None)",
462
- "4-bit weights; quant_type='nf4' for model weights -- note that quant_type comes "
463
- "after compress_statistics, so pass it by name", [], PY),
464
- ("Linear8bitLt(input_features, output_features, bias=True, has_fp16_weights=True,\n"
465
- " threshold=0.0, index=None, device=None)",
466
- "LLM.int8(): 8-bit matmul, outlier columns kept in fp16; needs has_fp16_weights=False",
467
- [], PY),
468
- ("LinearNF4(...) / LinearFP4(...)", "Linear4bit with quant_type already fixed", [], PY),
469
- ("Params4bit(data, requires_grad=False, quant_state=None, blocksize=None,\n"
470
- " compress_statistics=True, quant_type='fp4',\n"
471
- " quant_storage=torch.uint8, bnb_quantized=False)",
472
- "the weight type; it only quantises when .to(device) is called on it, and "
473
- "bnb_quantized=True tells it a checkpoint is already packed", [], PY),
474
- ("Int8Params(data, requires_grad=True, has_fp16_weights=False)",
475
- "the weight type of Linear8bitLt", [], PY),
476
- ("Embedding4bit(num_embeddings, embedding_dim, dtype=None, quant_type='fp4',\n"
477
- " quant_storage=torch.uint8, device=None)",
478
- "quantised token table; no padding_idx, unlike nn.Embedding", [], PY),
479
- ("Embedding8bit(num_embeddings, embedding_dim, device=None, dtype=None)",
480
- "LLM.int8() token table", [], PY),
481
- ("OutlierAwareLinear(input_features, output_features, bias=True, device=None)",
482
- "Linear8bitLt's building block", [], PY),
483
- ]),
484
- ("optim", "Reference: bitsandbytes.optim", [
485
- ("families",
486
- "Adam AdamW AdEMAMix Adagrad LAMB LARS Lion RMSprop SGD", [], PY),
487
- ("variants", "<Name>8bit, <Name>32bit, Paged<Name>8bit, and the bare name", [], PY),
488
- ("AdamW8bit(params, lr=1e-3, betas=(0.9,0.999), eps=1e-8, weight_decay=0.01,\n"
489
- " min_8bit_size=4096)",
490
- "the common case; identical signature to torch.optim.AdamW plus min_8bit_size",
491
- [], PY),
492
- ("8bit vs 32bit",
493
- "8bit quantises the optimizer state; 32bit is the same optimizer with fp32 state",
494
- [], PY),
495
- ("optim_bits=32 in that signature is not the state width",
496
- "with args=None the class name decides: AdamW8bit stores uint8 state1, AdamW32bit "
497
- "float32", [
498
- "p = nn.Parameter(torch.randn(8192))",
499
- "p.grad = torch.randn_like(p)",
500
- "for cls in (bnb.optim.AdamW8bit, bnb.optim.AdamW32bit):",
501
- " o = cls([p], lr=1e-3)",
502
- " o.step()",
503
- " print(cls.__name__, {k: str(v.dtype) for k, v in o.state[p].items()",
504
- " if torch.is_tensor(v) and k.startswith('state')})",
505
- "# AdamW8bit {'state1': 'torch.uint8', 'state2': 'torch.uint8'}",
506
- "# AdamW32bit {'state1': 'torch.float32', 'state2': 'torch.float32'}",
507
- ], PY),
508
- ("Paged<Name>8bit",
509
- "upstream's paged variant; on a CPU there is no unified memory to page to", [], PY),
510
- ("state_dict / load_state_dict", "supported; the 8-bit state survives a round trip", [], PY),
511
- ]),
512
- ("functional", "Reference: bitsandbytes.functional", [
513
- ("quantize_blockwise(A, code=None, blocksize=4096)",
514
- "-> (uint8 codes, QuantState); code=create_linear_map() for a linear 8-bit map", [], PY),
515
- ("dequantize_blockwise(A, quant_state)", "the inverse", [], PY),
516
- ("quantize_4bit(A, absmax=None, out=None, blocksize=None,\n"
517
- " compress_statistics=False, quant_type='fp4',\n"
518
- " quant_storage=torch.uint8)",
519
- "-> (packed uint8, QuantState); quantize_nf4 / quantize_fp4 fix the type", [], PY),
520
- ("dequantize_4bit(A, quant_state)",
521
- "the inverse; this is what the M>1 forward does", [], PY),
522
- ("fused_dequant_linear_8bit(A, wq, absmax, blocksize)",
523
- "FORK: dequantise and multiply in one AVX2 pass, weight stays uint8", [], PY),
524
- ("optimizer_update_8bit_blockwise(...)",
525
- "FORK: the CPU port of the fused 8-bit optimizer step", [], PY),
526
- ("gemv_4bit / igemm / batched_igemm / int8_linear_matmul",
527
- "the int8 and 4-bit matmul primitives", [], PY),
528
- ("int8_vectorwise_quant / int8_vectorwise_dequant / int8_double_quant",
529
- "the row-wise statistics LLM.int8() is built from", [], PY),
530
- ("create_linear_map / create_dynamic_map / create_normal_map / create_fp8_map",
531
- "build a quantisation codebook", [], PY),
532
- ("has_avx512bf16()",
533
- "whether this build has the AVX512-BF16 kernels; False on AVX2-only machines", [], PY),
534
- ("QuantState.from_dict(qs_dict, device)",
535
- "rebuild the quantisation state when loading a checkpoint by hand", [], PY),
536
- ]),
537
- ("notes", "Traps, and the errors they produce", [
538
- ("forgot .to('cpu')", "the layer exists but has no quantisation state yet", [
539
- "AssertionError: FP4 quantization state not initialized. Please call .cuda()",
540
- "or .to(device) on the LinearFP4 layer first.",
541
- ], "text"),
542
- ("K is not a multiple of blocksize",
543
- "the fused kernel refuses rather than reading the wrong row", [
544
- "ValueError: fused_dequant_linear_8bit requires K % blocksize == 0, got",
545
- "K=500 blocksize=64 (per-row block layout). Pad K or pick a blocksize that",
546
- "divides K.",
547
- ], "text"),
548
- ("quantised state dict, fresh layer",
549
- "rebuild the parameter instead: Params4bit(..., bnb_quantized=True), as in 4bit",
550
- [
551
- "RuntimeError: Unexpected key(s) in state_dict: weight.absmax,",
552
- "weight.quant_map, weight.nested_absmax, ...",
553
- ], "text"),
554
- ("compute_dtype=bfloat16 on AVX2",
555
- "5-12x slower at 512x512 and worse at LLM shapes; the weights stay 4-bit, so "
556
- "nothing is saved", [], PY),
557
- ("torch is not a dependency",
558
- "on PyPI that name is the CUDA build; install the CPU wheel first", [], PY),
559
- ("Linux compiles on install",
560
- "needs g++ and libomp; the artefact is built against your own glibc", [
561
- "sudo apt install g++ libomp-dev # Debian / Ubuntu",
562
- "sudo dnf install gcc-c++ libomp-devel # Fedora / RHEL",
563
- ], SH),
564
- ("num_workers > 0 on Windows",
565
- "spawns processes, so the entry script needs if __name__ == '__main__':", [
566
- "RuntimeError: An attempt has been made to start a new process before the",
567
- "current process has finished its bootstrapping phase.",
568
- ], "text"),
569
- ("unsupported",
570
- "no CUDA, no ROCm, no training through a 4-bit base on a CPU, no bf16 hardware "
571
- "on AVX2", [], PY),
572
- ]),
573
- ]
574
-
575
- EXAMPLES = [
576
- "bitsandbytes-cpu detect",
577
- "bitsandbytes-cpu selftest",
578
- "bitsandbytes-cpu help 8bitopt",
579
- "bitsandbytes-cpu help --markdown > API_REFERENCE.md",
580
- ]
581
-
582
-
583
- def _row(row):
584
- left, right = row[0], row[1]
585
- example = row[2] if len(row) > 2 else []
586
- lang = row[3] if len(row) > 3 else PY
587
- return left, right, list(example), lang
588
-
589
-
590
- def _select(pattern):
591
- """(sections, error) for an optional `help <pattern>` argument."""
592
- if not pattern:
593
- return SECTIONS, None
594
- p = pattern.strip().lower()
595
- exact = [s for s in SECTIONS if s[0] == p]
596
- if exact:
597
- return exact, None
598
- hits = [s for s in SECTIONS if p in s[1].lower()]
599
- if hits:
600
- return hits, None
601
- return [], p
602
-
603
-
604
- def _wrap(left, right, indent=2, width=WIDTH):
605
- """One entry, nmap-style.
606
-
607
- Two layouts, chosen by how long the left side is:
608
- short left -> ` left: right`, continuation aligned under `right`
609
- long left -> ` left:` then the description on its own lines at indent 6
610
-
611
- Aligning the continuation under the description is what nmap does and it reads well
612
- while the left side stays under about 30 columns. Doing it unconditionally is what
613
- the first version got wrong: a 70-column signature pushed the text to column 75 and
614
- every wrapped line became three words.
615
- """
616
- left_lines = left.split("\n")
617
- lines = [" " * indent + left_lines[0]]
618
- lines += [" " * (indent + 1) + l for l in left_lines[1:]]
619
- last = lines[-1]
620
- left_width = max(len(l) for l in lines) - indent
621
-
622
- # Inline whenever it fits, whatever the left width: refusing to use a line that has
623
- # room is how the previous version pushed "LinearNF4(...) / LinearFP4(...)" onto two
624
- # lines while a 75-column line sat there half empty.
625
- if len(last) + 2 + len(right) <= width:
626
- return lines[:-1] + [f"{last}: {right}"]
627
-
628
- # Otherwise the description needs its own lines. Align them under the description
629
- # only while that leaves a usable column; past about 40 the alignment eats the line.
630
- if left_width <= 40:
631
- avail = max(20, width - len(last) - 2)
632
- wrapped = textwrap.wrap(right, avail, break_on_hyphens=False)
633
- hang = " " * (len(last) + 2)
634
- return lines[:-1] + [f"{last}: {wrapped[0]}"] + [hang + w for w in wrapped[1:]]
635
-
636
- block = " " * 6
637
- lines[-1] = last + ":"
638
- return lines + [block + w
639
- for w in textwrap.wrap(right, width - len(block),
640
- break_on_hyphens=False)]
641
-
642
-
643
- def render_text(sections=None):
644
- sections = SECTIONS if sections is None else sections
645
- out = [USAGE, ""]
646
- for _key, title, rows in sections:
647
- out.append(title.upper() + ":")
648
- for row in rows:
649
- left, right, example, _lang = _row(row)
650
- out.extend(_wrap(left, right))
651
- # The example is the part that answers "how do I actually call it", so it is
652
- # indented under the entry and copied verbatim, not wrapped.
653
- for line in example:
654
- out.append((" " + line).rstrip())
655
- out.append("")
656
- out.append("EXAMPLES:")
657
- for e in EXAMPLES:
658
- out.append(" " + e)
659
- out.append("")
660
- out.append("EVERY SECTION BY KEY: " + ", ".join(s[0] for s in SECTIONS))
661
- out.append("SEE docs_cpu/QUICKSTART.md AND docs_cpu/API_REFERENCE.md FOR MORE.")
662
- return "\n".join(out)
663
-
664
-
665
- def render_markdown(sections=None):
666
- sections = SECTIONS if sections is None else sections
667
- out = ["# CPU fork reference", "",
668
- f"`{USAGE.splitlines()[0].strip()}`", "",
669
- "Generated by `bitsandbytes-cpu help --markdown`; edit "
670
- "`bitsandbytes_cpu_cli.py` instead of this file.", ""]
671
- for _key, title, rows in sections:
672
- out += [f"## {title}", ""]
673
- for row in rows:
674
- left, right, example, lang = _row(row)
675
- out.append(f"**`{left.replace(chr(10) + ' ', ' ')}`** -- {right}")
676
- out.append("")
677
- if example:
678
- fence = "python" if lang == PY else lang
679
- out += [f"```{fence}"] + example + ["```", ""]
680
- out += ["## Examples", "", "```bash"]
681
- out += EXAMPLES
682
- out += ["```", ""]
683
- out += ["## Section keys", "",
684
- "`bitsandbytes-cpu help <key>` prints one section:", "",
685
- "`" + "`, `".join(s[0] for s in SECTIONS) + "`", ""]
686
- return "\n".join(out)
687
-
688
-
689
- # ---------------------------------------------------------------------------------
690
- # Reaching into the package without importing it
691
- #
692
- # `import bitsandbytes` runs bitsandbytes/__init__.py, which imports torch. On a machine
693
- # that has this package but not torch that raises, so nothing below uses it: the package
694
- # directory is located with find_spec (which does not execute the package) and the two
695
- # modules that are stdlib-only are loaded from their paths.
696
- # ---------------------------------------------------------------------------------
697
-
698
-
699
- def _package_dir():
700
- """Path of the installed bitsandbytes package, without importing it."""
701
- spec = importlib.util.find_spec("bitsandbytes")
702
- if spec is not None and spec.submodule_search_locations:
703
- return list(spec.submodule_search_locations)[0]
704
- # Running from a source checkout with no metadata installed.
705
- here = os.path.dirname(os.path.abspath(__file__))
706
- guess = os.path.join(here, "bitsandbytes")
707
- return guess if os.path.isdir(guess) else None
708
-
709
-
710
- def _load_from_package(name, alias=None):
711
- """Load `bitsandbytes/<name>.py` as a top-level module, bypassing __init__.py."""
712
- root = _package_dir()
713
- if root is None:
714
- raise ImportError("the bitsandbytes package directory was not found")
715
- path = os.path.join(root, name + ".py")
716
- if not os.path.isfile(path):
717
- raise ImportError(f"{path} is missing from the installation")
718
- alias = alias or name
719
- spec = importlib.util.spec_from_file_location(alias, path)
720
- module = importlib.util.module_from_spec(spec)
721
- sys.modules[alias] = module
722
- spec.loader.exec_module(module)
723
- return module
724
-
725
-
726
- def _detect():
727
- """detect_cpu.detect(), with no torch required.
728
-
729
- detect_cpu falls back to `import torch_cpu_kit` when the relative import fails, and
730
- that is the path taken here; loading it under its bare name first is what makes the
731
- physical-core count right. Without it the module falls through to os.cpu_count(),
732
- which reports LOGICAL cores -- 12 on a 6C/12T i5-10400 -- and the recommendation this
733
- command exists to print would be wrong by a factor the user cannot see.
734
- """
735
- _load_from_package("torch_cpu_kit")
736
- return _load_from_package("detect_cpu").detect()
737
-
738
-
739
- def _torch_hint():
740
- print(" torch is not installed.")
741
- print(" This package does not depend on it on purpose: on PyPI the name `torch`")
742
- print(" is the CUDA build, about 2.5 GB of nvidia-* wheels. Install the CPU one:")
743
- print()
744
- print(" pip install torch --index-url https://download.pytorch.org/whl/cpu")
745
- print()
746
- print(" On Windows and Linux the x86-64 CPU build is also published on PyPI as")
747
- print(" `torch` for some versions; the index above is the one that always is CPU.")
748
-
749
-
750
- # ---------------------------------------------------------------------------------
751
-
752
-
753
- def cmd_help(args) -> int:
754
- sections, err = _select(getattr(args, "section", None))
755
- if err is not None:
756
- print(f" no section matches {err!r}")
757
- print(" sections: " + ", ".join(s[0] for s in SECTIONS))
758
- return 1
759
- print(render_markdown(sections) if args.markdown else render_text(sections))
760
- return 0
761
-
762
-
763
- def cmd_version(args) -> int:
764
- print(f" bitsandbytes-cpu-fork : {_safe(_pkg_version)}")
765
- print(f" python : {platform.python_version()}")
766
- print(f" platform : {platform.platform()}")
767
- print(f" torch : {_safe(lambda: __import__('torch').__version__)}")
768
- print(f" torch cuda available : {_safe(lambda: __import__('torch').cuda.is_available())}")
769
- print(f" package path : {_package_dir() or '<not found>'}")
770
- return 0
771
-
772
-
773
- def cmd_detect(args) -> int:
774
- import json as _json
775
- try:
776
- d = _detect()
777
- except ImportError as exc:
778
- print(f" could not read the CPU: {exc}")
779
- return 1
780
- if args.json:
781
- print(_json.dumps(d, ensure_ascii=False, indent=2))
782
- return 0
783
- print(f" CPU : {d['cpu_model']}")
784
- print(f" cores : {d['physical_cores']} physical / {d['logical_cores']} logical")
785
- cap = d["cpu_capability"]
786
- # Without torch there is nothing to ask about SIMD, and "unknown" on its own reads
787
- # like a defect in this command rather than a missing dependency.
788
- print(f" SIMD : {cap}"
789
- + (" (install torch to read it)" if cap in ("unknown", "DEFAULT") else ""))
790
- if d["ram_total_gb"]:
791
- print(f" memory : {d['ram_total_gb']} GB total, "
792
- f"{d['ram_available_gb']} GB free")
793
- else:
794
- # _mem() returns zeros when psutil is absent, and a "0.0 GB / 0.0 GB" line reads
795
- # as a machine out of memory rather than a missing dependency.
796
- print(" memory : unavailable (pip install psutil)")
797
- print(f" threads : {d['recommend_threads']['lm']} for LLM, "
798
- f"{d['recommend_threads']['diffusion']} for diffusion")
799
- print(f" bf16 : {d['recommend_bf16']}")
800
- print(f" 8-bit opt : {d['recommend_8bit_optimizer']}")
801
- print(f" disk bal : {d['recommend_disk_balancer']}")
802
- return 0
803
-
804
-
805
- def cmd_doctor(args) -> int:
806
- import ctypes
807
- import glob
808
- pkg = _package_dir()
809
- if pkg is None:
810
- print(" bitsandbytes is not installed anywhere importable")
811
- return 1
812
- print(f" package {pkg}")
813
- found = glob.glob(os.path.join(pkg, "libbitsandbytes*"))
814
- print(" files in the package")
815
- for p in found:
816
- print(f" {os.path.basename(p):<28} {os.path.getsize(p)/1e6:.2f} MB")
817
- if not found:
818
- print(" none")
819
- suffix = ".dll" if os.name == "nt" else ".dylib" if sys.platform == "darwin" else ".so"
820
- target = os.path.join(pkg, f"libbitsandbytes_cpu{suffix}")
821
- print(f" this platform wants {os.path.basename(target)}")
822
- if not os.path.isfile(target):
823
- print(" MISSING -- install from a wheel for this platform, or from the sdist")
824
- print(" which compiles it (Linux needs g++ and libomp).")
825
- return 1
826
- try:
827
- lib = ctypes.CDLL(target)
828
- print(" loads OK")
829
- except OSError as exc:
830
- print(f" FAILED to load: {exc}")
831
- return 1
832
- print(" symbols")
833
- missing = 0
834
- for n in ("cdequantize_blockwise_cpu_fp32", "cquantize_blockwise_cpu_fp32",
835
- "cgemv_4bit_inference_cpu_fp32", "cgemm_8bit_inference_cpu_fp32",
836
- "coptimizer_update_8bit_blockwise_cpu"):
837
- ok = hasattr(lib, n)
838
- missing += 0 if ok else 1
839
- print(f" {'yes' if ok else 'NO '} {n}")
840
- print(f" {'yes' if hasattr(lib, 'has_avx512bf16_cpu') else 'no '} "
841
- f"has_avx512bf16_cpu (optional: AVX512-BF16 builds only)")
842
- return 1 if missing else 0
843
-
844
-
845
- def cmd_selftest(args) -> int:
846
- try:
847
- import torch
848
- import torch.nn as nn
849
- import bitsandbytes as bnb
850
- except ImportError:
851
- _torch_hint()
852
- return 1
853
-
854
- fails = 0
855
- print(" [1/3] 4-bit layer against fp32")
856
- torch.manual_seed(0)
857
- lin = nn.Linear(512, 256, bias=True)
858
- q = bnb.nn.Linear4bit(512, 256, bias=True, compute_dtype=torch.float32,
859
- quant_type="nf4", compress_statistics=True)
860
- q.weight = bnb.nn.Params4bit(lin.weight.data, requires_grad=False,
861
- quant_type="nf4", compress_statistics=True)
862
- q.weight = q.weight.to("cpu")
863
- q.bias = nn.Parameter(lin.bias.detach().clone())
864
- x = torch.randn(8, 512)
865
- y, ref = q(x), lin(x)
866
- rel = ((y - ref).norm() / ref.norm()).item()
867
- ok = bool(torch.isfinite(y).all()) and 0.02 < rel < 0.30
868
- print(f" relative error {rel:.4f} (nf4 normally 0.05-0.15) "
869
- f"{'OK' if ok else 'FAILED'}")
870
- fails += 0 if ok else 1
871
-
872
- print(" [2/3] 8-bit optimizer")
873
- try:
874
- opt = bnb.optim.AdamW8bit([nn.Parameter(torch.randn(8192))], lr=1e-3)
875
- p = opt.param_groups[0]["params"][0]
876
- p.grad = torch.randn_like(p)
877
- before = p.detach().clone()
878
- opt.step()
879
- moved = (p.detach() - before).abs().max().item()
880
- ok = moved > 0 and bool(torch.isfinite(p).all())
881
- print(f" parameter moved by {moved:.3e} {'OK' if ok else 'FAILED'}")
882
- fails += 0 if ok else 1
883
- except Exception as exc: # noqa: BLE001
884
- print(f" FAILED {type(exc).__name__}: {exc}")
885
- fails += 1
886
-
887
- print(" [3/3] fused Gated DeltaNet kernel")
888
- try:
889
- try:
890
- from . import gdn_cpu
891
- except ImportError:
892
- from bitsandbytes import gdn_cpu
893
- gdn_cpu.load_native()
894
- print(" native kernel loaded OK")
895
- except Exception as exc: # noqa: BLE001
896
- print(f" FAILED {type(exc).__name__}: {exc}")
897
- fails += 1
898
-
899
- print(f"\n {fails} failure(s)")
900
- return 1 if fails else 0
901
-
902
-
903
- def _safe(fn):
904
- try:
905
- return fn()
906
- except Exception as exc: # noqa: BLE001
907
- return f"<{type(exc).__name__}: {exc}>"
908
-
909
-
910
- def _pkg_version():
911
- """The installed version, from distribution metadata.
912
-
913
- Not `bitsandbytes.__version__`: that import needs torch, and `version` is one of the
914
- commands that has to work without it.
915
- """
916
- try:
917
- from importlib.metadata import version
918
- return version("bitsandbytes-cpu-fork")
919
- except Exception: # noqa: BLE001 (not installed; running from a checkout)
920
- return "<not installed>"
921
-
922
-
923
- def main(argv=None) -> int:
924
- ap = argparse.ArgumentParser(
925
- prog="bitsandbytes-cpu", add_help=False,
926
- description="Reference and diagnostics for the bitsandbytes CPU fork.")
927
- ap.add_argument("-h", "--help", action="store_true",
928
- help="same as `help`")
929
- sub = ap.add_subparsers(dest="cmd")
930
- p = sub.add_parser("help", add_help=False)
931
- p.add_argument("section", nargs="?", default=None,
932
- help="one section key, e.g. 8bitopt (default: everything)")
933
- p.add_argument("--markdown", action="store_true")
934
- p.set_defaults(func=cmd_help)
935
- d = sub.add_parser("detect", add_help=False)
936
- d.add_argument("--json", action="store_true")
937
- d.set_defaults(func=cmd_detect)
938
- sub.add_parser("doctor", add_help=False).set_defaults(func=cmd_doctor)
939
- sub.add_parser("selftest", add_help=False).set_defaults(func=cmd_selftest)
940
- sub.add_parser("version", add_help=False).set_defaults(func=cmd_version)
941
-
942
- a, _ = ap.parse_known_args(argv)
943
- if getattr(a, "help", False) or not getattr(a, "func", None):
944
- return cmd_help(argparse.Namespace(markdown=False, section=None))
945
- return a.func(a)
946
-
947
-
948
- if __name__ == "__main__":
949
- sys.exit(main())
1
+ """The `bitsandbytes-cpu` command: one entry point that answers "what is this and how do
2
+ I call it" without a browser.
3
+
4
+ Why a CLI rather than only a document: a reference that lives in a file is read by the
5
+ people who already know it exists. `bitsandbytes-cpu help` is discoverable from the
6
+ moment pip finishes.
7
+
8
+ WHY THIS MODULE IS NOT INSIDE THE PACKAGE
9
+ -----------------------------------------
10
+ `bitsandbytes/__init__.py` imports torch, and torch is deliberately not a dependency of
11
+ this distribution: on PyPI that name is the CUDA build, so depending on it would pull
12
+ 2.5 GB of nvidia-* wheels onto a machine with no NVIDIA device. The direct consequence
13
+ is that a user can have this package installed with torch absent -- and that is exactly
14
+ the user who runs `help`, because the help is what says to install torch. Pointed at
15
+ `bitsandbytes.cli:main`, the console script died with
16
+ `ModuleNotFoundError: No module named 'torch'` before printing a word (observed on
17
+ Ubuntu, Python 3.14, after a plain `pip install` of the sdist).
18
+
19
+ So the reference lives in a module that imports nothing but the standard library.
20
+ `detect` and `doctor` load what they need out of the package directory by path, which
21
+ works with no torch; only `selftest` needs it, and says so instead of raising.
22
+
23
+ What it is built around is not the upstream API surface -- that is documented upstream --
24
+ but the things this fork adds and the way they have to be called on a CPU. Every example
25
+ under a heading is a call that was executed on an AVX2 machine before it was written
26
+ down, including the ones that are supposed to fail.
27
+
28
+ The output follows the shape people already know from tools like nmap: a usage line,
29
+ ALL-CAPS sections, one `thing: what it does` per line, no blank lines inside a section,
30
+ and the code for the entry indented under it. A signature alone does not tell anyone how
31
+ to use a feature; the example does.
32
+
33
+ Sections can be read one at a time (`bitsandbytes-cpu help 8bitopt`), and the same tables
34
+ render as markdown (`help --markdown`), so the terminal output and
35
+ docs_cpu/API_REFERENCE.md cannot drift apart.
36
+
37
+ bitsandbytes-cpu help
38
+ bitsandbytes-cpu help 8bitopt
39
+ bitsandbytes-cpu detect
40
+ bitsandbytes-cpu doctor
41
+ bitsandbytes-cpu selftest
42
+ bitsandbytes-cpu version
43
+ """
44
+ from __future__ import annotations
45
+
46
+ import argparse
47
+ import importlib.util
48
+ import os
49
+ import platform
50
+ import sys
51
+ import textwrap
52
+
53
+ WIDTH = 78
54
+
55
+ USAGE = ("Usage: bitsandbytes-cpu {help|detect|doctor|selftest|version} [section]\n"
56
+ " bitsandbytes-cpu help 8bitopt one section instead of all of them")
57
+
58
+ # Sections are (key, title, [(left, right, [example lines], lang), ...]).
59
+ # key the word to pass to `help <key>`
60
+ # title printed upper-case by help, as-is by --markdown
61
+ # left what you type or call
62
+ # right one clause about it
63
+ # example the actual call, executed by tools/verify_help_examples.py
64
+ # lang "python" (verified), "bash"/"text" (not executable)
65
+ # Keep `right` to a single clause; the examples carry the detail.
66
+ PY = "python"
67
+ SH = "bash"
68
+
69
+ SECTIONS = [
70
+ ("intro", "What the fork adds", [
71
+ ("CPU kernels",
72
+ "the CUDA kernels are gone: AVX2 (x86-64) and NEON (ARM64) C, compiled into "
73
+ "the wheel", [], PY),
74
+ ("bitsandbytes.optim",
75
+ "8-bit optimizers: ~2.3 bytes of state per parameter instead of 8; the 4-bit "
76
+ "optimizer takes ~1.03", [], PY),
77
+ ("bitsandbytes.nn",
78
+ "4-bit and 8-bit layers that run on a cpu tensor", [], PY),
79
+ ("bitsandbytes.functional",
80
+ "the quantisation primitives plus the fused CPU kernels", [], PY),
81
+ ("torch_cpu_kit",
82
+ "env vars, thread counts, oneDNN, memory watch, DataLoader defaults", [], PY),
83
+ ("detect_cpu",
84
+ "one dict describing this machine and the settings that follow from it", [], PY),
85
+ ("gdn_cpu",
86
+ "fused Gated DeltaNet for Qwen3-Next / Qwen3.5 linear attention", [], PY),
87
+ ("disk_balancer",
88
+ "spills cold parameters to another disk when RAM runs out", [], PY),
89
+ ("bitsandbytes-cpu",
90
+ "this command; pip also installs it as bnb-cpu", [], PY),
91
+ ]),
92
+ ("start", "First run", [
93
+ ("bitsandbytes-cpu detect",
94
+ "what the CPU is, and the thread count it recommends", [], SH),
95
+ ("bitsandbytes-cpu doctor",
96
+ "which native library will load, and whether every kernel symbol is in it", [], SH),
97
+ ("bitsandbytes-cpu selftest",
98
+ "runs a 4-bit layer, an 8-bit optimizer step and the GDN kernel; exit 1 on failure",
99
+ [], SH),
100
+ ("bitsandbytes-cpu version", "package, python, torch, platform", [], SH),
101
+ ("import bitsandbytes on a CPU host",
102
+ "succeeds with no CUDA anywhere; the library loads on first kernel use", [
103
+ "import bitsandbytes as bnb",
104
+ "print(bnb.__version__, bnb.functional.has_avx512bf16())",
105
+ ], PY),
106
+ ]),
107
+ ("4bit", "4-bit on CPU", [
108
+ ("quantise one Linear layer",
109
+ "assign Params4bit, then call .to('cpu') -- .to() is what quantises", [
110
+ "import torch, torch.nn as nn, bitsandbytes as bnb",
111
+ "",
112
+ "src = nn.Linear(4096, 4096, bias=False) # an fp32 layer to convert",
113
+ "q = bnb.nn.Linear4bit(4096, 4096, bias=False,",
114
+ " compute_dtype=torch.float32, # see `help threads`",
115
+ " quant_type='nf4', compress_statistics=True)",
116
+ "q.weight = bnb.nn.Params4bit(src.weight.data, requires_grad=False,",
117
+ " quant_type='nf4', compress_statistics=True)",
118
+ "q = q.to('cpu') # <- without this the first forward raises",
119
+ "",
120
+ "x = torch.randn(8, 4096) # your input",
121
+ "y = q(x) # weight goes back to fp32 inside the forward",
122
+ ], PY),
123
+ ("what the forward does",
124
+ "M=1 takes the fused gemv_4bit kernel; M>1 unpacks to fp32 and calls a dense GEMM",
125
+ [], PY),
126
+ ("quantise a whole model in place", "swap every nn.Linear, leave the head alone", [
127
+ "def to_4bit(module, skip=('lm_head',)):",
128
+ " for name, child in module.named_children():",
129
+ " if name in skip:",
130
+ " continue",
131
+ " if isinstance(child, nn.Linear):",
132
+ " new = bnb.nn.Linear4bit(child.in_features, child.out_features,",
133
+ " bias=child.bias is not None,",
134
+ " compute_dtype=torch.float32,",
135
+ " quant_type='nf4', compress_statistics=True)",
136
+ " new.weight = bnb.nn.Params4bit(child.weight.data,",
137
+ " requires_grad=False,",
138
+ " quant_type='nf4',",
139
+ " compress_statistics=True)",
140
+ " setattr(module, name, new.to('cpu'))",
141
+ " else:",
142
+ " to_4bit(child, skip)",
143
+ " return module",
144
+ "",
145
+ "to_4bit(model)",
146
+ "print(sum(p.numel() * p.element_size() for p in model.parameters()) / 1e6, 'MB')",
147
+ ], PY),
148
+ ("save and load a 4-bit layer",
149
+ "the state dict carries the packed weight and the quantisation state; a "
150
+ "fresh Linear4bit cannot load it back, so the parameter is rebuilt", [
151
+ "src = nn.Linear(4096, 4096, bias=False)",
152
+ "q = bnb.nn.Linear4bit(4096, 4096, bias=False, compute_dtype=torch.float32,",
153
+ " quant_type='nf4', compress_statistics=True)",
154
+ "q.weight = bnb.nn.Params4bit(src.weight.data, requires_grad=False,",
155
+ " quant_type='nf4', compress_statistics=True)",
156
+ "q = q.to('cpu')",
157
+ "x = torch.randn(8, 4096)",
158
+ "y0 = q(x)",
159
+ "",
160
+ "torch.save(q.state_dict(), 'layer4bit.pt') # weight, weight.absmax,",
161
+ " # weight.quant_map, nested_*",
162
+ "",
163
+ "sd = torch.load('layer4bit.pt')",
164
+ "state = bnb.functional.QuantState.from_dict(",
165
+ " {k[len('weight.') :]: v for k, v in sd.items() if k.startswith('weight.')},",
166
+ " device=torch.device('cpu'))",
167
+ "fresh = bnb.nn.Linear4bit(4096, 4096, bias=False,",
168
+ " compute_dtype=torch.float32, quant_type='nf4')",
169
+ "pw = bnb.nn.Params4bit(sd['weight'], requires_grad=False,",
170
+ " quant_type=state.quant_type, blocksize=state.blocksize,",
171
+ " bnb_quantized=True, quant_storage=sd['weight'].dtype)",
172
+ "pw.quant_state = state",
173
+ "fresh.weight = pw",
174
+ "assert (fresh(x) - y0).abs().max() < 1e-5",
175
+ ], PY),
176
+ ("Embedding4bit",
177
+ "the same for a token table; there is no padding_idx argument", [
178
+ "# table: a [vocab, dim] matrix, e.g. model.get_input_embeddings().weight",
179
+ "emb = bnb.nn.Embedding4bit(table.shape[0], table.shape[1], quant_type='nf4')",
180
+ "emb.weight = bnb.nn.Params4bit(table, requires_grad=False, quant_type='nf4')",
181
+ "emb = emb.to('cpu')",
182
+ ], PY),
183
+ ]),
184
+ ("qlora", "Training a frozen 4-bit base", [
185
+ ("freeze the base, train adapters",
186
+ "4-bit is a storage format: the base still computes in fp32", [
187
+ "to_4bit(model) # the helper from `help 4bit`",
188
+ "for p in model.parameters():",
189
+ " p.requires_grad_(False)",
190
+ "",
191
+ "adapters = nn.Linear(8, 8) # wherever your LoRA layers come from",
192
+ "opt = AdamW8bit(adapters.parameters(), lr=1e-4)",
193
+ "loss = loss_fn(adapters(model(x)), y)",
194
+ "loss.backward()",
195
+ "opt.step(); opt.zero_grad()",
196
+ ], PY),
197
+ ("4-bit does not make the matmul cheaper",
198
+ "the forward unpacks to fp32, so FLOPs are unchanged; only RAM and disk shrink",
199
+ [], PY),
200
+ ("gradients into a 4-bit layer",
201
+ "requires_grad=True on a Params4bit is not supported here; keep the base frozen",
202
+ [], PY),
203
+ ]),
204
+ ("8bitopt", "8-bit optimizer (start here)", [
205
+ ("swap the optimizer",
206
+ "same constructor arguments as the torch optimizer it replaces", [
207
+ "import bitsandbytes as bnb",
208
+ "",
209
+ "opt = bnb.optim.AdamW8bit(model.parameters(), lr=2e-5, weight_decay=0.01)",
210
+ "opt.zero_grad()",
211
+ "loss = loss_fn(model(x), y)",
212
+ "loss.backward()",
213
+ "opt.step() # the loop is otherwise unchanged",
214
+ ], PY),
215
+ ("what you get",
216
+ "18688 bytes of state per parameter here, against torch AdamW's 65540 (3.5x)", [
217
+ "p = nn.Parameter(torch.randn(8192))",
218
+ "opt = bnb.optim.AdamW8bit([p], lr=1e-3)",
219
+ "p.grad = torch.randn_like(p)",
220
+ "opt.step()",
221
+ "nbytes = sum(v.numel() * v.element_size()",
222
+ " for v in opt.state[p].values() if torch.is_tensor(v))",
223
+ "print(nbytes, 'bytes for', p.numel(), 'params =', nbytes / p.numel(), 'B/param')",
224
+ "print(sorted(opt.state[p])) # absmax1 absmax2 qmap1 qmap2 state1 state2 step",
225
+ ], PY),
226
+ ("every optimizer has the same three names",
227
+ "<Name>8bit, <Name>32bit and Paged<Name>8bit", [
228
+ "from bitsandbytes.optim import (Adam8bit, AdamW8bit, AdamW32bit, Lion8bit,",
229
+ " SGD8bit, LAMB8bit, LARS8bit, RMSprop8bit,",
230
+ " Adagrad8bit, AdEMAMix8bit, PagedAdamW8bit)",
231
+ "opt = Lion8bit(model.parameters(), lr=1e-4, weight_decay=0.1)",
232
+ ], PY),
233
+ ("only the parameters you step",
234
+ "a frozen base needs no optimizer state; passing it wastes the memory you saved", [
235
+ "opt = bnb.optim.AdamW8bit([p for p in model.parameters() if p.requires_grad],",
236
+ " lr=1e-4)",
237
+ ], PY),
238
+ ("min_8bit_size=4096",
239
+ "anything smaller keeps float32 state, so tiny tensors show no saving", [
240
+ "small = [nn.Parameter(torch.randn(64))]",
241
+ "opt = bnb.optim.AdamW8bit(small, lr=1e-3) # default: fp32 state",
242
+ "p = small[0]",
243
+ "p.grad = torch.randn_like(p)",
244
+ "opt.step()",
245
+ "print({k: str(v.dtype) for k, v in opt.state[p].items() if torch.is_tensor(v)})",
246
+ "# {'state1': 'torch.float32', 'state2': 'torch.float32'} -- no absmax, no gain",
247
+ "",
248
+ "opt = bnb.optim.AdamW8bit(small, lr=1e-3, min_8bit_size=0) # force 8-bit",
249
+ "opt.step()",
250
+ "print({k: str(v.dtype) for k, v in opt.state[p].items() if torch.is_tensor(v)})",
251
+ "# {'state1': 'torch.uint8', ..., 'absmax1': 'torch.float32', ...}",
252
+ ], PY),
253
+ ("state_dict", "saves and loads like any other torch optimizer", [
254
+ "opt = bnb.optim.AdamW8bit(model.parameters(), lr=1e-3)",
255
+ "torch.save(opt.state_dict(), 'opt.pt')",
256
+ "opt.load_state_dict(torch.load('opt.pt', weights_only=False))",
257
+ ], PY),
258
+ ("the step is not slower",
259
+ "the fused kernel drops the dequantise-then-update round trip: 0.74x an fp32 "
260
+ "AdamW step, measured in one process", [], PY),
261
+ ]),
262
+ ("4bitopt", "4-bit optimizer: half the state of 8-bit", [
263
+ ("swap the optimizer",
264
+ "same shape of API as the 8-bit one; fp32 parameters only", [
265
+ "from bitsandbytes.optim import AdamW4bit",
266
+ "",
267
+ "opt = AdamW4bit(model.parameters(), lr=1e-4, weight_decay=0.01)",
268
+ "opt.zero_grad()",
269
+ "loss = loss_fn(model(x), y)",
270
+ "loss.backward()",
271
+ "opt.step() # the loop is otherwise unchanged",
272
+ ], PY),
273
+ ("what you get",
274
+ "1.031 bytes of state per parameter, against 2.062 for the 8-bit optimizer "
275
+ "and 8.0 for torch AdamW, all measured at 1M parameters", [
276
+ "p = nn.Parameter(torch.randn(4096))",
277
+ "opt = AdamW4bit([p], lr=1e-3)",
278
+ "p.grad = torch.randn_like(p)",
279
+ "opt.step()",
280
+ "print(opt.state_bytes(), 'bytes for', p.numel(), 'params =',",
281
+ " opt.state_bytes() / p.numel(), 'B/param')",
282
+ "# 4224 bytes for 4096 params = 1.031 B/param (torch AdamW: 8.0)",
283
+ "",
284
+ "# on a real model, 30.49M parameters: 232.6 MB -> 29.5 MB, 7.9x less",
285
+ ], PY),
286
+ ("state layout",
287
+ "two 4-bit codes per byte, one absmax per 256 elements", [
288
+ "# state1 / state2 : ceil(n/2) uint8, high nibble = even index",
289
+ "# absmax1/absmax2 : one fp32 per 256 elements",
290
+ "# 16 symmetric levels: code 0 is -1, code 15 is +1, step 2/15",
291
+ "# exact zero is therefore not representable; an all-zero block writes",
292
+ "# code 8 (+1/15), whose bias is absmax/15 == 0 because absmax is 0",
293
+ ], PY),
294
+ ("does it train as well as fp32",
295
+ "it tracks fp32 rather than matching it: quantised state cannot reproduce an "
296
+ "fp32 trajectory, and that is not the criterion", [
297
+ "# measured over 120 steps on the real 30.49M image-AR model:",
298
+ "# the two loss curves stay within 0.27% of each other",
299
+ "# on a 200-step MLP run the 4-bit run ended at 0.0198 against fp32's",
300
+ "# 0.0240 -- neither worse nor identical, simply a different path",
301
+ "# the kernel's state codes are identical to an independent numpy",
302
+ "# implementation over 50 steps, so the maths agrees exactly",
303
+ ], PY),
304
+ ("the step is faster, unlike 8-bit",
305
+ "0.658 ms against 1.682 ms for the 8-bit kernel at 1M parameters (2.56x), "
306
+ "because it moves half the bytes", [
307
+ "# both are AVX2 kernels in the same library, so the comparison isolates",
308
+ "# state width rather than implementation language",
309
+ "# the scalar fallback is 7.99 ms -- the AVX2 path is 12x faster than it",
310
+ ], PY),
311
+ ("traps",
312
+ "the failure modes seen while building this, each one silent at first", [
313
+ "# 1. torch.optim.AdamW is NOT replaced: this is a separate class, and",
314
+ "# AdamW4bit requires fp32 params (the kernel's dtype 0).",
315
+ "# 2. blocksize is compiled in at 256; passing another value raises.",
316
+ "# 3. if the shared library does not export",
317
+ "# coptimizer_update_4bit_blockwise_cpu, import fails loudly rather",
318
+ "# than falling back -- three earlier versions of the search silently",
319
+ "# picked a DLL without the kernel and only failed at the first step.",
320
+ "# 4. quantised state is not a drop-in for exact reproducibility runs:",
321
+ "# same seed, same data, different trajectory from fp32.",
322
+ ], PY),
323
+ ]),
324
+ ("kernels", "Fused kernels (call them directly)", [
325
+ ("fused_dequant_linear_8bit(...)",
326
+ "out = A @ dequant8(wq).T with the weight never unpacked to fp32; the "
327
+ "arguments are (A, wq, absmax, blocksize)", [
328
+ "w = torch.randn(512, 512) * 0.02 # your fp32 weight",
329
+ "A = torch.randn(4, 512) # your activations",
330
+ "code = bnb.functional.create_linear_map() # the 8-bit codebook",
331
+ "wq, state = bnb.functional.quantize_blockwise(w, code=code, blocksize=64)",
332
+ "out = bnb.functional.fused_dequant_linear_8bit(A, wq, state.absmax, 64)",
333
+ "ref = A @ w.t()",
334
+ "assert ((out - ref).norm() / ref.norm()).item() < 0.02",
335
+ ], PY),
336
+ ("A may have leading dimensions",
337
+ "A.shape == (..., K) in, (..., N) out -- flatten (B,T,H,W) yourself first", [
338
+ "w = torch.randn(512, 512) * 0.02",
339
+ "code = bnb.functional.create_linear_map()",
340
+ "wq, state = bnb.functional.quantize_blockwise(w, code=code, blocksize=64)",
341
+ "v = torch.randn(2, 3, 512)",
342
+ "out = bnb.functional.fused_dequant_linear_8bit(v, wq, state.absmax, 64)",
343
+ "print(out.shape) # (2, 3, 512) -> (2, 3, 512)",
344
+ ], PY),
345
+ ("K % blocksize must be 0",
346
+ "otherwise the kernel refuses rather than reading the wrong row", [], PY),
347
+ ("optimizer_update_8bit_blockwise(...)",
348
+ "the fused 8-bit step the optimizers call, with the whole argument list", [
349
+ "N, BLOCK = 4096, 64",
350
+ "p = nn.Parameter(torch.randn(N) * 0.1)",
351
+ "g = torch.randn(N) * 0.05",
352
+ "state1 = torch.zeros(N, dtype=torch.uint8) # quantised exp_avg",
353
+ "state2 = torch.zeros(N, dtype=torch.uint8) # quantised exp_avg_sq",
354
+ "absmax1 = torch.zeros(N // BLOCK)",
355
+ "absmax2 = torch.zeros(N // BLOCK)",
356
+ "qmap1 = bnb.functional.create_dynamic_map(signed=True, total_bits=8)",
357
+ "qmap2 = qmap1.clone()",
358
+ "bnb.functional.optimizer_update_8bit_blockwise(",
359
+ " 'adam', g, p, state1, state2,",
360
+ " 0.9, 0.999, 0.0, # beta1, beta2, beta3",
361
+ " 1.0, 1e-8, # alpha, eps",
362
+ " 1, 1e-3, # step, lr",
363
+ " qmap1, qmap2, absmax1, absmax2,",
364
+ " weight_decay=0.0, gnorm_scale=1.0)",
365
+ "# checked against torch.optim.AdamW in tests/test_cpu_e2e.py: the two agree",
366
+ "# to 1e-5 after one step",
367
+ ], PY),
368
+ ("gemv_4bit(A, q, state=state)",
369
+ "the M=1 4-bit path on its own; state comes from quantize_4bit", [
370
+ "w = torch.randn(512, 512) * 0.02",
371
+ "A = torch.randn(1, 512)",
372
+ "q, state = bnb.functional.quantize_4bit(w, quant_type='nf4', blocksize=64,",
373
+ " compress_statistics=True)",
374
+ "row = bnb.functional.gemv_4bit(A, q, state=state)",
375
+ "print(q.shape, q.dtype, row.shape) # packed uint8, one row out",
376
+ ], PY),
377
+ ("int8_linear_matmul(A, B)",
378
+ "int8 x int8 -> int32, both operands (M,K) and (N,K); the LLM.int8() core", [
379
+ "Ai = torch.randint(-100, 100, (8, 64), dtype=torch.int8)",
380
+ "Bi = torch.randint(-100, 100, (16, 64), dtype=torch.int8)",
381
+ "out = bnb.functional.int8_linear_matmul(Ai, Bi)",
382
+ "assert torch.equal(out, Ai.to(torch.int32) @ Bi.to(torch.int32).t())",
383
+ ], PY),
384
+ ("int8_vectorwise_quant(A)",
385
+ "returns (int8 data, per-row fp32 scales, outlier column indices or None)", [
386
+ "A = torch.randn(256, 512)",
387
+ "qi, scales, outliers = bnb.functional.int8_vectorwise_quant(A)",
388
+ "print(qi.dtype, qi.shape, scales.shape) # int8, (256, 512), (256,)",
389
+ ], PY),
390
+ ]),
391
+ ("toolkit", "torch_cpu_kit and detect_cpu", [
392
+ ("apply_env() BEFORE import torch",
393
+ "OpenMP reads these once, when the runtime loads; later calls do nothing", [
394
+ "import bitsandbytes.torch_cpu_kit as tck",
395
+ "tck.apply_env() # OMP/MKL_NUM_THREADS = physical cores, OMP_WAIT_POLICY",
396
+ "import torch # torch now starts with those settings",
397
+ ], PY),
398
+ ("init(model_kind=...) after import torch",
399
+ "thread pools, interop=1, oneDNN on, fp32 matmul precision left alone on AVX2", [
400
+ "import torch, bitsandbytes.torch_cpu_kit as tck",
401
+ "print(tck.init(model_kind='lm')) # 'lm', 'vision', 'diffusion', or None",
402
+ ], PY),
403
+ ("physical_cores()",
404
+ "half of os.cpu_count() on an SMT machine -- the number to use for GEMM", [
405
+ "torch.set_num_threads(tck.physical_cores()) # 6, not 12, on an i5-10400",
406
+ ], PY),
407
+ ("mem_report()", "(process RSS MB, system available MB)", [
408
+ "rss, avail = tck.mem_report()",
409
+ "print(f'{rss:.0f} MB in use, {avail:.0f} MB available')",
410
+ ], PY),
411
+ ("start_mem_monitor(interval=20.0)",
412
+ "background log line; returns the Event that stops it", [
413
+ "stop = tck.start_mem_monitor(interval=20.0)",
414
+ "tck.suspend_mem_monitor(stop) # or stop.set()",
415
+ ], PY),
416
+ ("fast_loader(dataset, batch_size=32)",
417
+ "DataLoader with pin_memory=False and 2 persistent workers", [
418
+ "loader = tck.fast_loader(dataset, batch_size=32)",
419
+ "print(loader.num_workers, loader.pin_memory) # 2 False",
420
+ ], PY),
421
+ ("make_contiguous_(model)",
422
+ "in place: transposed parameters make the C kernels fall back to a slow path", [
423
+ "tck.make_contiguous_(model)",
424
+ ], PY),
425
+ ("detect()", "the dict behind `bitsandbytes-cpu detect`", [
426
+ "from bitsandbytes.detect_cpu import detect",
427
+ "d = detect()",
428
+ "threads = d['recommend_threads']['lm'] # also 'diffusion', 'image'",
429
+ "print(d['cpu_capability'], d['physical_cores'], threads)",
430
+ ], PY),
431
+ ]),
432
+ ("threads", "Threads and precision", [
433
+ ("threads = physical cores",
434
+ "measured on an i5-10400: 6 threads, 203 s per 1024x1024 image step; 12 threads, 225 s",
435
+ [], PY),
436
+ ("bf16 is emulated on AVX2",
437
+ "no hardware bf16 means software conversion: 5-12x slower at 512x512, worse at LLM shapes",
438
+ [], PY),
439
+ ("compute_dtype=torch.float32",
440
+ "for Linear4bit, Linear8bitLt and Embedding4bit; bf16 there is the single "
441
+ "biggest performance mistake on these CPUs", [], PY),
442
+ ("tck.autocast()",
443
+ "a no-op on AVX2: recommended_autocast() returns float32, so the block stays fp32", [
444
+ "with tck.autocast(): # nullcontext on this machine, real on AMX/AVX512-BF16",
445
+ " out = model(x)",
446
+ ], PY),
447
+ ("float32_matmul_precision",
448
+ "left at 'highest' on AVX2; only a CPU with bf16 hardware is put on 'high'", [], PY),
449
+ ]),
450
+ ("memory", "Memory", [
451
+ ("plan for the peak, not the weights",
452
+ "a 4-bit model still materialises fp32 activations and one unpacked layer at a time",
453
+ [], PY),
454
+ ("start_mem_monitor", "a log line every 20 s, on a daemon thread", [], PY),
455
+ ("RAM below ~8 GB free",
456
+ "detect() sets recommend_disk_balancer, and `help disks` is the answer", [], PY),
457
+ ("MIN_8BIT_SIZE",
458
+ "the optimizer, not you, decides: below 4096 elements a parameter stays 32-bit", [], PY),
459
+ ]),
460
+ ("gdn", "Gated DeltaNet kernel (Qwen3-Next)", [
461
+ ("patch_transformers()",
462
+ "routes HuggingFace Qwen3-Next / Qwen3.5 through the fused kernel; True if applied", [
463
+ "from bitsandbytes import gdn_cpu",
464
+ "print(gdn_cpu.patch_transformers()) # True once the patch is in place",
465
+ "",
466
+ "# then load the model as usual -- the layer now runs on the fused kernel:",
467
+ "# model = AutoModelForCausalLM.from_pretrained('Qwen/Qwen3-Next-80B-A3B-Instruct')",
468
+ ], PY),
469
+ ("patch_fla()", "the same for the fla library", [], PY),
470
+ ("load_native()",
471
+ "loads the shared library without patching anything; raises with the paths it tried", [
472
+ "from bitsandbytes import gdn_cpu",
473
+ "lib = gdn_cpu.load_native()",
474
+ ], PY),
475
+ ("fused_recurrent_gated_delta_rule(...)",
476
+ "the kernel itself; the arguments are q, k, v, beta, g, scale, initial_state, "
477
+ "output_final_state, use_qk_l2norm_in_kernel, head_first, cu_seqlens", [], PY),
478
+ ("it has an autograd Function",
479
+ "so it trains, not just infers; the reference path is kept for verification", [], PY),
480
+ ]),
481
+ ("disks", "Disk balancer", [
482
+ ("add_flash_args(parser)",
483
+ "adds --flash, --flash-paths, --flash-sizes, --flash-speed, --flash-threshold "
484
+ "and --flash-keep to your own training script", [], PY),
485
+ ("parse_flash_args(args)",
486
+ "args -> DiskBalancerConfig, or None when --flash was not given", [
487
+ "import argparse, bitsandbytes.disk_balancer as db",
488
+ "ap = argparse.ArgumentParser()",
489
+ "db.add_flash_args(ap)",
490
+ "cfg = db.parse_flash_args(ap.parse_args(['--flash']))",
491
+ "print(cfg.mode, cfg.memory_threshold, cfg.min_param_size)",
492
+ ], PY),
493
+ ("DiskLoadBalancer(config)",
494
+ "attach_model() then start(); update_step() once per training step checks RAM "
495
+ "and migrates the cold parameters", [], PY),
496
+ ("in a training loop",
497
+ "the whole integration is three lines and one call per step", [
498
+ "# cfg comes from parse_flash_args(...); see the entry above",
499
+ "if cfg:",
500
+ " bal = db.DiskLoadBalancer(cfg).attach_model(model).start()",
501
+ " for xb, yb in loader:",
502
+ " loss = loss_fn(model(xb), yb)",
503
+ " loss.backward(); opt.step(); opt.zero_grad()",
504
+ " bal.update_step()",
505
+ ], PY),
506
+ ("cold tensors by hand",
507
+ "put_cold(key, tensor) / get_cold(key) / drop(key) / stats(); put_cold frees "
508
+ "the tensor by replacing its storage with an empty one", [
509
+ "if cfg:",
510
+ " bal.put_cold('text_embeds', embeds) # any tensor you want off the heap",
511
+ " embeds = bal.get_cold('text_embeds')",
512
+ " print(bal.stats())",
513
+ ], PY),
514
+ ("it is a context manager",
515
+ "cleanup() flushes the write queue and removes the staging files", [
516
+ "if cfg:",
517
+ " with db.DiskLoadBalancer(cfg) as bal:",
518
+ " ...",
519
+ ], PY),
520
+ ]),
521
+ ("layers", "Reference: bitsandbytes.nn", [
522
+ ("Linear4bit(input_features, output_features, bias=True, compute_dtype=None,\n"
523
+ " compress_statistics=True, quant_type='fp4',\n"
524
+ " quant_storage=torch.uint8, device=None)",
525
+ "4-bit weights; quant_type='nf4' for model weights -- note that quant_type comes "
526
+ "after compress_statistics, so pass it by name", [], PY),
527
+ ("Linear8bitLt(input_features, output_features, bias=True, has_fp16_weights=True,\n"
528
+ " threshold=0.0, index=None, device=None)",
529
+ "LLM.int8(): 8-bit matmul, outlier columns kept in fp16; needs has_fp16_weights=False",
530
+ [], PY),
531
+ ("LinearNF4(...) / LinearFP4(...)", "Linear4bit with quant_type already fixed", [], PY),
532
+ ("Params4bit(data, requires_grad=False, quant_state=None, blocksize=None,\n"
533
+ " compress_statistics=True, quant_type='fp4',\n"
534
+ " quant_storage=torch.uint8, bnb_quantized=False)",
535
+ "the weight type; it only quantises when .to(device) is called on it, and "
536
+ "bnb_quantized=True tells it a checkpoint is already packed", [], PY),
537
+ ("Int8Params(data, requires_grad=True, has_fp16_weights=False)",
538
+ "the weight type of Linear8bitLt", [], PY),
539
+ ("Embedding4bit(num_embeddings, embedding_dim, dtype=None, quant_type='fp4',\n"
540
+ " quant_storage=torch.uint8, device=None)",
541
+ "quantised token table; no padding_idx, unlike nn.Embedding", [], PY),
542
+ ("Embedding8bit(num_embeddings, embedding_dim, device=None, dtype=None)",
543
+ "LLM.int8() token table", [], PY),
544
+ ("OutlierAwareLinear(input_features, output_features, bias=True, device=None)",
545
+ "Linear8bitLt's building block", [], PY),
546
+ ]),
547
+ ("optim", "Reference: bitsandbytes.optim", [
548
+ ("families",
549
+ "Adam AdamW AdEMAMix Adagrad LAMB LARS Lion RMSprop SGD", [], PY),
550
+ ("variants", "<Name>8bit, <Name>32bit, Paged<Name>8bit, and the bare name", [], PY),
551
+ ("AdamW8bit(params, lr=1e-3, betas=(0.9,0.999), eps=1e-8, weight_decay=0.01,\n"
552
+ " min_8bit_size=4096)",
553
+ "the common case; identical signature to torch.optim.AdamW plus min_8bit_size",
554
+ [], PY),
555
+ ("8bit vs 32bit",
556
+ "8bit quantises the optimizer state; 32bit is the same optimizer with fp32 state",
557
+ [], PY),
558
+ ("optim_bits=32 in that signature is not the state width",
559
+ "with args=None the class name decides: AdamW8bit stores uint8 state1, AdamW32bit "
560
+ "float32", [
561
+ "p = nn.Parameter(torch.randn(8192))",
562
+ "p.grad = torch.randn_like(p)",
563
+ "for cls in (bnb.optim.AdamW8bit, bnb.optim.AdamW32bit):",
564
+ " o = cls([p], lr=1e-3)",
565
+ " o.step()",
566
+ " print(cls.__name__, {k: str(v.dtype) for k, v in o.state[p].items()",
567
+ " if torch.is_tensor(v) and k.startswith('state')})",
568
+ "# AdamW8bit {'state1': 'torch.uint8', 'state2': 'torch.uint8'}",
569
+ "# AdamW32bit {'state1': 'torch.float32', 'state2': 'torch.float32'}",
570
+ ], PY),
571
+ ("Paged<Name>8bit",
572
+ "upstream's paged variant; on a CPU there is no unified memory to page to", [], PY),
573
+ ("state_dict / load_state_dict", "supported; the 8-bit state survives a round trip", [], PY),
574
+ ]),
575
+ ("functional", "Reference: bitsandbytes.functional", [
576
+ ("quantize_blockwise(A, code=None, blocksize=4096)",
577
+ "-> (uint8 codes, QuantState); code=create_linear_map() for a linear 8-bit map", [], PY),
578
+ ("dequantize_blockwise(A, quant_state)", "the inverse", [], PY),
579
+ ("quantize_4bit(A, absmax=None, out=None, blocksize=None,\n"
580
+ " compress_statistics=False, quant_type='fp4',\n"
581
+ " quant_storage=torch.uint8)",
582
+ "-> (packed uint8, QuantState); quantize_nf4 / quantize_fp4 fix the type", [], PY),
583
+ ("dequantize_4bit(A, quant_state)",
584
+ "the inverse; this is what the M>1 forward does", [], PY),
585
+ ("fused_dequant_linear_8bit(A, wq, absmax, blocksize)",
586
+ "FORK: dequantise and multiply in one AVX2 pass, weight stays uint8", [], PY),
587
+ ("optimizer_update_8bit_blockwise(...)",
588
+ "FORK: the CPU port of the fused 8-bit optimizer step", [], PY),
589
+ ("gemv_4bit / igemm / batched_igemm / int8_linear_matmul",
590
+ "the int8 and 4-bit matmul primitives", [], PY),
591
+ ("int8_vectorwise_quant / int8_vectorwise_dequant / int8_double_quant",
592
+ "the row-wise statistics LLM.int8() is built from", [], PY),
593
+ ("create_linear_map / create_dynamic_map / create_normal_map / create_fp8_map",
594
+ "build a quantisation codebook", [], PY),
595
+ ("has_avx512bf16()",
596
+ "whether this build has the AVX512-BF16 kernels; False on AVX2-only machines", [], PY),
597
+ ("QuantState.from_dict(qs_dict, device)",
598
+ "rebuild the quantisation state when loading a checkpoint by hand", [], PY),
599
+ ]),
600
+ ("notes", "Traps, and the errors they produce", [
601
+ ("forgot .to('cpu')", "the layer exists but has no quantisation state yet", [
602
+ "AssertionError: FP4 quantization state not initialized. Please call .cuda()",
603
+ "or .to(device) on the LinearFP4 layer first.",
604
+ ], "text"),
605
+ ("K is not a multiple of blocksize",
606
+ "the fused kernel refuses rather than reading the wrong row", [
607
+ "ValueError: fused_dequant_linear_8bit requires K % blocksize == 0, got",
608
+ "K=500 blocksize=64 (per-row block layout). Pad K or pick a blocksize that",
609
+ "divides K.",
610
+ ], "text"),
611
+ ("quantised state dict, fresh layer",
612
+ "rebuild the parameter instead: Params4bit(..., bnb_quantized=True), as in 4bit",
613
+ [
614
+ "RuntimeError: Unexpected key(s) in state_dict: weight.absmax,",
615
+ "weight.quant_map, weight.nested_absmax, ...",
616
+ ], "text"),
617
+ ("compute_dtype=bfloat16 on AVX2",
618
+ "5-12x slower at 512x512 and worse at LLM shapes; the weights stay 4-bit, so "
619
+ "nothing is saved", [], PY),
620
+ ("torch is not a dependency",
621
+ "on PyPI that name is the CUDA build; install the CPU wheel first", [], PY),
622
+ ("Linux compiles on install",
623
+ "needs g++ and libomp; the artefact is built against your own glibc", [
624
+ "sudo apt install g++ libomp-dev # Debian / Ubuntu",
625
+ "sudo dnf install gcc-c++ libomp-devel # Fedora / RHEL",
626
+ ], SH),
627
+ ("num_workers > 0 on Windows",
628
+ "spawns processes, so the entry script needs if __name__ == '__main__':", [
629
+ "RuntimeError: An attempt has been made to start a new process before the",
630
+ "current process has finished its bootstrapping phase.",
631
+ ], "text"),
632
+ ("unsupported",
633
+ "no CUDA, no ROCm, no training through a 4-bit base on a CPU, no bf16 hardware "
634
+ "on AVX2", [], PY),
635
+ ]),
636
+ ]
637
+
638
+ EXAMPLES = [
639
+ "bitsandbytes-cpu detect",
640
+ "bitsandbytes-cpu selftest",
641
+ "bitsandbytes-cpu help 8bitopt",
642
+ "bitsandbytes-cpu help --markdown > API_REFERENCE.md",
643
+ ]
644
+
645
+
646
+ def _row(row):
647
+ left, right = row[0], row[1]
648
+ example = row[2] if len(row) > 2 else []
649
+ lang = row[3] if len(row) > 3 else PY
650
+ return left, right, list(example), lang
651
+
652
+
653
+ def _select(pattern):
654
+ """(sections, error) for an optional `help <pattern>` argument."""
655
+ if not pattern:
656
+ return SECTIONS, None
657
+ p = pattern.strip().lower()
658
+ exact = [s for s in SECTIONS if s[0] == p]
659
+ if exact:
660
+ return exact, None
661
+ hits = [s for s in SECTIONS if p in s[1].lower()]
662
+ if hits:
663
+ return hits, None
664
+ return [], p
665
+
666
+
667
+ def _wrap(left, right, indent=2, width=WIDTH):
668
+ """One entry, nmap-style.
669
+
670
+ Two layouts, chosen by how long the left side is:
671
+ short left -> ` left: right`, continuation aligned under `right`
672
+ long left -> ` left:` then the description on its own lines at indent 6
673
+
674
+ Aligning the continuation under the description is what nmap does and it reads well
675
+ while the left side stays under about 30 columns. Doing it unconditionally is what
676
+ the first version got wrong: a 70-column signature pushed the text to column 75 and
677
+ every wrapped line became three words.
678
+ """
679
+ left_lines = left.split("\n")
680
+ lines = [" " * indent + left_lines[0]]
681
+ lines += [" " * (indent + 1) + l for l in left_lines[1:]]
682
+ last = lines[-1]
683
+ left_width = max(len(l) for l in lines) - indent
684
+
685
+ # Inline whenever it fits, whatever the left width: refusing to use a line that has
686
+ # room is how the previous version pushed "LinearNF4(...) / LinearFP4(...)" onto two
687
+ # lines while a 75-column line sat there half empty.
688
+ if len(last) + 2 + len(right) <= width:
689
+ return lines[:-1] + [f"{last}: {right}"]
690
+
691
+ # Otherwise the description needs its own lines. Align them under the description
692
+ # only while that leaves a usable column; past about 40 the alignment eats the line.
693
+ if left_width <= 40:
694
+ avail = max(20, width - len(last) - 2)
695
+ wrapped = textwrap.wrap(right, avail, break_on_hyphens=False)
696
+ hang = " " * (len(last) + 2)
697
+ return lines[:-1] + [f"{last}: {wrapped[0]}"] + [hang + w for w in wrapped[1:]]
698
+
699
+ block = " " * 6
700
+ lines[-1] = last + ":"
701
+ return lines + [block + w
702
+ for w in textwrap.wrap(right, width - len(block),
703
+ break_on_hyphens=False)]
704
+
705
+
706
+ def render_text(sections=None):
707
+ sections = SECTIONS if sections is None else sections
708
+ out = [USAGE, ""]
709
+ for _key, title, rows in sections:
710
+ out.append(title.upper() + ":")
711
+ for row in rows:
712
+ left, right, example, _lang = _row(row)
713
+ out.extend(_wrap(left, right))
714
+ # The example is the part that answers "how do I actually call it", so it is
715
+ # indented under the entry and copied verbatim, not wrapped.
716
+ for line in example:
717
+ out.append((" " + line).rstrip())
718
+ out.append("")
719
+ out.append("EXAMPLES:")
720
+ for e in EXAMPLES:
721
+ out.append(" " + e)
722
+ out.append("")
723
+ out.append("EVERY SECTION BY KEY: " + ", ".join(s[0] for s in SECTIONS))
724
+ out.append("SEE docs_cpu/QUICKSTART.md AND docs_cpu/API_REFERENCE.md FOR MORE.")
725
+ return "\n".join(out)
726
+
727
+
728
+ def render_markdown(sections=None):
729
+ sections = SECTIONS if sections is None else sections
730
+ out = ["# CPU fork reference", "",
731
+ f"`{USAGE.splitlines()[0].strip()}`", "",
732
+ "Generated by `bitsandbytes-cpu help --markdown`; edit "
733
+ "`bitsandbytes_cpu_cli.py` instead of this file.", ""]
734
+ for _key, title, rows in sections:
735
+ out += [f"## {title}", ""]
736
+ for row in rows:
737
+ left, right, example, lang = _row(row)
738
+ out.append(f"**`{left.replace(chr(10) + ' ', ' ')}`** -- {right}")
739
+ out.append("")
740
+ if example:
741
+ fence = "python" if lang == PY else lang
742
+ out += [f"```{fence}"] + example + ["```", ""]
743
+ out += ["## Examples", "", "```bash"]
744
+ out += EXAMPLES
745
+ out += ["```", ""]
746
+ out += ["## Section keys", "",
747
+ "`bitsandbytes-cpu help <key>` prints one section:", "",
748
+ "`" + "`, `".join(s[0] for s in SECTIONS) + "`", ""]
749
+ return "\n".join(out)
750
+
751
+
752
+ # ---------------------------------------------------------------------------------
753
+ # Reaching into the package without importing it
754
+ #
755
+ # `import bitsandbytes` runs bitsandbytes/__init__.py, which imports torch. On a machine
756
+ # that has this package but not torch that raises, so nothing below uses it: the package
757
+ # directory is located with find_spec (which does not execute the package) and the two
758
+ # modules that are stdlib-only are loaded from their paths.
759
+ # ---------------------------------------------------------------------------------
760
+
761
+
762
+ def _package_dir():
763
+ """Path of the installed bitsandbytes package, without importing it."""
764
+ spec = importlib.util.find_spec("bitsandbytes")
765
+ if spec is not None and spec.submodule_search_locations:
766
+ return list(spec.submodule_search_locations)[0]
767
+ # Running from a source checkout with no metadata installed.
768
+ here = os.path.dirname(os.path.abspath(__file__))
769
+ guess = os.path.join(here, "bitsandbytes")
770
+ return guess if os.path.isdir(guess) else None
771
+
772
+
773
+ def _load_from_package(name, alias=None):
774
+ """Load `bitsandbytes/<name>.py` as a top-level module, bypassing __init__.py."""
775
+ root = _package_dir()
776
+ if root is None:
777
+ raise ImportError("the bitsandbytes package directory was not found")
778
+ path = os.path.join(root, name + ".py")
779
+ if not os.path.isfile(path):
780
+ raise ImportError(f"{path} is missing from the installation")
781
+ alias = alias or name
782
+ spec = importlib.util.spec_from_file_location(alias, path)
783
+ module = importlib.util.module_from_spec(spec)
784
+ sys.modules[alias] = module
785
+ spec.loader.exec_module(module)
786
+ return module
787
+
788
+
789
+ def _detect():
790
+ """detect_cpu.detect(), with no torch required.
791
+
792
+ detect_cpu falls back to `import torch_cpu_kit` when the relative import fails, and
793
+ that is the path taken here; loading it under its bare name first is what makes the
794
+ physical-core count right. Without it the module falls through to os.cpu_count(),
795
+ which reports LOGICAL cores -- 12 on a 6C/12T i5-10400 -- and the recommendation this
796
+ command exists to print would be wrong by a factor the user cannot see.
797
+ """
798
+ _load_from_package("torch_cpu_kit")
799
+ return _load_from_package("detect_cpu").detect()
800
+
801
+
802
+ def _torch_hint():
803
+ print(" torch is not installed.")
804
+ print(" This package does not depend on it on purpose: on PyPI the name `torch`")
805
+ print(" is the CUDA build, about 2.5 GB of nvidia-* wheels. Install the CPU one:")
806
+ print()
807
+ print(" pip install torch --index-url https://download.pytorch.org/whl/cpu")
808
+ print()
809
+ print(" On Windows and Linux the x86-64 CPU build is also published on PyPI as")
810
+ print(" `torch` for some versions; the index above is the one that always is CPU.")
811
+
812
+
813
+ # ---------------------------------------------------------------------------------
814
+
815
+
816
+ def cmd_help(args) -> int:
817
+ sections, err = _select(getattr(args, "section", None))
818
+ if err is not None:
819
+ print(f" no section matches {err!r}")
820
+ print(" sections: " + ", ".join(s[0] for s in SECTIONS))
821
+ return 1
822
+ print(render_markdown(sections) if args.markdown else render_text(sections))
823
+ return 0
824
+
825
+
826
+ def cmd_version(args) -> int:
827
+ print(f" bitsandbytes-cpu-fork : {_safe(_pkg_version)}")
828
+ print(f" python : {platform.python_version()}")
829
+ print(f" platform : {platform.platform()}")
830
+ print(f" torch : {_safe(lambda: __import__('torch').__version__)}")
831
+ print(f" torch cuda available : {_safe(lambda: __import__('torch').cuda.is_available())}")
832
+ print(f" package path : {_package_dir() or '<not found>'}")
833
+ return 0
834
+
835
+
836
+ def cmd_detect(args) -> int:
837
+ import json as _json
838
+ try:
839
+ d = _detect()
840
+ except ImportError as exc:
841
+ print(f" could not read the CPU: {exc}")
842
+ return 1
843
+ if args.json:
844
+ print(_json.dumps(d, ensure_ascii=False, indent=2))
845
+ return 0
846
+ print(f" CPU : {d['cpu_model']}")
847
+ print(f" cores : {d['physical_cores']} physical / {d['logical_cores']} logical")
848
+ cap = d["cpu_capability"]
849
+ # Without torch there is nothing to ask about SIMD, and "unknown" on its own reads
850
+ # like a defect in this command rather than a missing dependency.
851
+ print(f" SIMD : {cap}"
852
+ + (" (install torch to read it)" if cap in ("unknown", "DEFAULT") else ""))
853
+ if d["ram_total_gb"]:
854
+ print(f" memory : {d['ram_total_gb']} GB total, "
855
+ f"{d['ram_available_gb']} GB free")
856
+ else:
857
+ # _mem() returns zeros when psutil is absent, and a "0.0 GB / 0.0 GB" line reads
858
+ # as a machine out of memory rather than a missing dependency.
859
+ print(" memory : unavailable (pip install psutil)")
860
+ print(f" threads : {d['recommend_threads']['lm']} for LLM, "
861
+ f"{d['recommend_threads']['diffusion']} for diffusion")
862
+ print(f" bf16 : {d['recommend_bf16']}")
863
+ print(f" 8-bit opt : {d['recommend_8bit_optimizer']}")
864
+ print(f" disk bal : {d['recommend_disk_balancer']}")
865
+ return 0
866
+
867
+
868
+ def cmd_doctor(args) -> int:
869
+ import ctypes
870
+ import glob
871
+ pkg = _package_dir()
872
+ if pkg is None:
873
+ print(" bitsandbytes is not installed anywhere importable")
874
+ return 1
875
+ print(f" package {pkg}")
876
+ found = glob.glob(os.path.join(pkg, "libbitsandbytes*"))
877
+ print(" files in the package")
878
+ for p in found:
879
+ print(f" {os.path.basename(p):<28} {os.path.getsize(p)/1e6:.2f} MB")
880
+ if not found:
881
+ print(" none")
882
+ suffix = ".dll" if os.name == "nt" else ".dylib" if sys.platform == "darwin" else ".so"
883
+ target = os.path.join(pkg, f"libbitsandbytes_cpu{suffix}")
884
+ print(f" this platform wants {os.path.basename(target)}")
885
+ if not os.path.isfile(target):
886
+ print(" MISSING -- install from a wheel for this platform, or from the sdist")
887
+ print(" which compiles it (Linux needs g++ and libomp).")
888
+ return 1
889
+ try:
890
+ lib = ctypes.CDLL(target)
891
+ print(" loads OK")
892
+ except OSError as exc:
893
+ print(f" FAILED to load: {exc}")
894
+ return 1
895
+ print(" symbols")
896
+ missing = 0
897
+ for n in ("cdequantize_blockwise_cpu_fp32", "cquantize_blockwise_cpu_fp32",
898
+ "cgemv_4bit_inference_cpu_fp32", "cgemm_8bit_inference_cpu_fp32",
899
+ "coptimizer_update_8bit_blockwise_cpu"):
900
+ ok = hasattr(lib, n)
901
+ missing += 0 if ok else 1
902
+ print(f" {'yes' if ok else 'NO '} {n}")
903
+ print(f" {'yes' if hasattr(lib, 'has_avx512bf16_cpu') else 'no '} "
904
+ f"has_avx512bf16_cpu (optional: AVX512-BF16 builds only)")
905
+ return 1 if missing else 0
906
+
907
+
908
+ def cmd_selftest(args) -> int:
909
+ try:
910
+ import torch
911
+ import torch.nn as nn
912
+ import bitsandbytes as bnb
913
+ except ImportError:
914
+ _torch_hint()
915
+ return 1
916
+
917
+ fails = 0
918
+ print(" [1/3] 4-bit layer against fp32")
919
+ torch.manual_seed(0)
920
+ lin = nn.Linear(512, 256, bias=True)
921
+ q = bnb.nn.Linear4bit(512, 256, bias=True, compute_dtype=torch.float32,
922
+ quant_type="nf4", compress_statistics=True)
923
+ q.weight = bnb.nn.Params4bit(lin.weight.data, requires_grad=False,
924
+ quant_type="nf4", compress_statistics=True)
925
+ q.weight = q.weight.to("cpu")
926
+ q.bias = nn.Parameter(lin.bias.detach().clone())
927
+ x = torch.randn(8, 512)
928
+ y, ref = q(x), lin(x)
929
+ rel = ((y - ref).norm() / ref.norm()).item()
930
+ ok = bool(torch.isfinite(y).all()) and 0.02 < rel < 0.30
931
+ print(f" relative error {rel:.4f} (nf4 normally 0.05-0.15) "
932
+ f"{'OK' if ok else 'FAILED'}")
933
+ fails += 0 if ok else 1
934
+
935
+ print(" [2/3] 8-bit optimizer")
936
+ try:
937
+ opt = bnb.optim.AdamW8bit([nn.Parameter(torch.randn(8192))], lr=1e-3)
938
+ p = opt.param_groups[0]["params"][0]
939
+ p.grad = torch.randn_like(p)
940
+ before = p.detach().clone()
941
+ opt.step()
942
+ moved = (p.detach() - before).abs().max().item()
943
+ ok = moved > 0 and bool(torch.isfinite(p).all())
944
+ print(f" parameter moved by {moved:.3e} {'OK' if ok else 'FAILED'}")
945
+ fails += 0 if ok else 1
946
+ except Exception as exc: # noqa: BLE001
947
+ print(f" FAILED {type(exc).__name__}: {exc}")
948
+ fails += 1
949
+
950
+ print(" [3/3] fused Gated DeltaNet kernel")
951
+ try:
952
+ try:
953
+ from . import gdn_cpu
954
+ except ImportError:
955
+ from bitsandbytes import gdn_cpu
956
+ gdn_cpu.load_native()
957
+ print(" native kernel loaded OK")
958
+ except Exception as exc: # noqa: BLE001
959
+ print(f" FAILED {type(exc).__name__}: {exc}")
960
+ fails += 1
961
+
962
+ print(f"\n {fails} failure(s)")
963
+ return 1 if fails else 0
964
+
965
+
966
+ def _safe(fn):
967
+ try:
968
+ return fn()
969
+ except Exception as exc: # noqa: BLE001
970
+ return f"<{type(exc).__name__}: {exc}>"
971
+
972
+
973
+ def _pkg_version():
974
+ """The installed version, from distribution metadata.
975
+
976
+ Not `bitsandbytes.__version__`: that import needs torch, and `version` is one of the
977
+ commands that has to work without it.
978
+ """
979
+ try:
980
+ from importlib.metadata import version
981
+ return version("bitsandbytes-cpu-fork")
982
+ except Exception: # noqa: BLE001 (not installed; running from a checkout)
983
+ return "<not installed>"
984
+
985
+
986
+ def main(argv=None) -> int:
987
+ ap = argparse.ArgumentParser(
988
+ prog="bitsandbytes-cpu", add_help=False,
989
+ description="Reference and diagnostics for the bitsandbytes CPU fork.")
990
+ ap.add_argument("-h", "--help", action="store_true",
991
+ help="same as `help`")
992
+ sub = ap.add_subparsers(dest="cmd")
993
+ p = sub.add_parser("help", add_help=False)
994
+ p.add_argument("section", nargs="?", default=None,
995
+ help="one section key, e.g. 8bitopt (default: everything)")
996
+ p.add_argument("--markdown", action="store_true")
997
+ p.set_defaults(func=cmd_help)
998
+ d = sub.add_parser("detect", add_help=False)
999
+ d.add_argument("--json", action="store_true")
1000
+ d.set_defaults(func=cmd_detect)
1001
+ sub.add_parser("doctor", add_help=False).set_defaults(func=cmd_doctor)
1002
+ sub.add_parser("selftest", add_help=False).set_defaults(func=cmd_selftest)
1003
+ sub.add_parser("version", add_help=False).set_defaults(func=cmd_version)
1004
+
1005
+ a, _ = ap.parse_known_args(argv)
1006
+ if getattr(a, "help", False) or not getattr(a, "func", None):
1007
+ return cmd_help(argparse.Namespace(markdown=False, section=None))
1008
+ return a.func(a)
1009
+
1010
+
1011
+ if __name__ == "__main__":
1012
+ sys.exit(main())