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/__init__.py +1 -1
- bitsandbytes_cpu_cli.py +1012 -949
- {bitsandbytes_cpu_fork-0.50.2.dev2.dist-info → bitsandbytes_cpu_fork-0.50.2.dev3.dist-info}/METADATA +4 -3
- {bitsandbytes_cpu_fork-0.50.2.dev2.dist-info → bitsandbytes_cpu_fork-0.50.2.dev3.dist-info}/RECORD +8 -8
- {bitsandbytes_cpu_fork-0.50.2.dev2.dist-info → bitsandbytes_cpu_fork-0.50.2.dev3.dist-info}/WHEEL +0 -0
- {bitsandbytes_cpu_fork-0.50.2.dev2.dist-info → bitsandbytes_cpu_fork-0.50.2.dev3.dist-info}/entry_points.txt +0 -0
- {bitsandbytes_cpu_fork-0.50.2.dev2.dist-info → bitsandbytes_cpu_fork-0.50.2.dev3.dist-info}/licenses/LICENSE +0 -0
- {bitsandbytes_cpu_fork-0.50.2.dev2.dist-info → bitsandbytes_cpu_fork-0.50.2.dev3.dist-info}/top_level.txt +0 -0
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
("
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
"",
|
|
111
|
-
"
|
|
112
|
-
"
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
"
|
|
117
|
-
"
|
|
118
|
-
"",
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
-
"
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
"
|
|
143
|
-
"",
|
|
144
|
-
"
|
|
145
|
-
"
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
"
|
|
150
|
-
|
|
151
|
-
"
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
"
|
|
158
|
-
"",
|
|
159
|
-
"
|
|
160
|
-
"
|
|
161
|
-
"",
|
|
162
|
-
"
|
|
163
|
-
"
|
|
164
|
-
"
|
|
165
|
-
"
|
|
166
|
-
"
|
|
167
|
-
"
|
|
168
|
-
"
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
"
|
|
172
|
-
"
|
|
173
|
-
"
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
"
|
|
179
|
-
"emb
|
|
180
|
-
"emb =
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
"
|
|
188
|
-
"
|
|
189
|
-
"",
|
|
190
|
-
"
|
|
191
|
-
"
|
|
192
|
-
"
|
|
193
|
-
"loss
|
|
194
|
-
"
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
"",
|
|
208
|
-
"
|
|
209
|
-
"opt.
|
|
210
|
-
"
|
|
211
|
-
"loss
|
|
212
|
-
"
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
"
|
|
218
|
-
"
|
|
219
|
-
"
|
|
220
|
-
"
|
|
221
|
-
"
|
|
222
|
-
"
|
|
223
|
-
"print(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
"
|
|
229
|
-
"
|
|
230
|
-
"
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
"
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
"
|
|
241
|
-
"
|
|
242
|
-
"p
|
|
243
|
-
"
|
|
244
|
-
"
|
|
245
|
-
"
|
|
246
|
-
"",
|
|
247
|
-
"
|
|
248
|
-
"opt.
|
|
249
|
-
"
|
|
250
|
-
"
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
"
|
|
255
|
-
"
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
"
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
"
|
|
265
|
-
"
|
|
266
|
-
"
|
|
267
|
-
"
|
|
268
|
-
"
|
|
269
|
-
"
|
|
270
|
-
"
|
|
271
|
-
"
|
|
272
|
-
], PY),
|
|
273
|
-
("
|
|
274
|
-
"
|
|
275
|
-
|
|
276
|
-
"
|
|
277
|
-
"
|
|
278
|
-
"
|
|
279
|
-
"
|
|
280
|
-
"print(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
"
|
|
289
|
-
"
|
|
290
|
-
"
|
|
291
|
-
"
|
|
292
|
-
"
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
"
|
|
298
|
-
"
|
|
299
|
-
"
|
|
300
|
-
"
|
|
301
|
-
"
|
|
302
|
-
"#
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
"
|
|
307
|
-
"
|
|
308
|
-
"
|
|
309
|
-
"
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
"
|
|
317
|
-
"
|
|
318
|
-
"
|
|
319
|
-
"
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
"
|
|
332
|
-
"
|
|
333
|
-
"
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
"
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
"
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
"
|
|
351
|
-
"
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
"
|
|
356
|
-
"
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
"
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
"
|
|
364
|
-
"
|
|
365
|
-
"
|
|
366
|
-
"
|
|
367
|
-
], PY),
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
"
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
"
|
|
382
|
-
"
|
|
383
|
-
], PY),
|
|
384
|
-
("
|
|
385
|
-
"
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
("
|
|
393
|
-
"
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
("
|
|
399
|
-
"
|
|
400
|
-
"
|
|
401
|
-
"print(
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
("
|
|
408
|
-
|
|
409
|
-
"
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
"
|
|
427
|
-
"
|
|
428
|
-
"
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
("
|
|
434
|
-
"
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
("
|
|
452
|
-
"
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
], PY),
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
"
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
("
|
|
470
|
-
|
|
471
|
-
"
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
"
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
("
|
|
486
|
-
"
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
"
|
|
498
|
-
"
|
|
499
|
-
"
|
|
500
|
-
"
|
|
501
|
-
"
|
|
502
|
-
"
|
|
503
|
-
"
|
|
504
|
-
"
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
(
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
("
|
|
523
|
-
"
|
|
524
|
-
|
|
525
|
-
"
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
"
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
("
|
|
533
|
-
"
|
|
534
|
-
|
|
535
|
-
"
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
("
|
|
543
|
-
"
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
("
|
|
549
|
-
"
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
"
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
"
|
|
561
|
-
"
|
|
562
|
-
"
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
"
|
|
567
|
-
"
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
]
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
#
|
|
692
|
-
#
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
return
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
def
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
if
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
print("
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
print(
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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())
|