bitsandbytes-cpu-fork 0.50.2.dev2__py3-none-win_amd64.whl → 0.50.2.dev4__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. bitsandbytes/__init__.py +1 -1
  2. bitsandbytes/__main__.py +4 -4
  3. bitsandbytes/_cpu_caps.py +110 -0
  4. bitsandbytes/_ops.py +526 -526
  5. bitsandbytes/autograd/_functions.py +491 -491
  6. bitsandbytes/backends/cpu/ops.py +805 -805
  7. bitsandbytes/backends/cuda/ops.py +1212 -1212
  8. bitsandbytes/backends/default/ops.py +632 -632
  9. bitsandbytes/backends/hpu/ops.py +53 -53
  10. bitsandbytes/backends/mps/ops.py +277 -277
  11. bitsandbytes/backends/triton/kernels_4bit.py +583 -577
  12. bitsandbytes/backends/triton/kernels_8bit_quant.py +195 -195
  13. bitsandbytes/backends/triton/kernels_optim.py +1177 -1177
  14. bitsandbytes/backends/triton/ops.py +304 -304
  15. bitsandbytes/backends/utils.py +94 -94
  16. bitsandbytes/backends/xpu/ops.py +305 -305
  17. bitsandbytes/cextension.py +447 -405
  18. bitsandbytes/cli.py +50 -50
  19. bitsandbytes/consts.py +12 -12
  20. bitsandbytes/cuda_specs.py +111 -111
  21. bitsandbytes/detect_cpu.py +189 -189
  22. bitsandbytes/diagnostics/cuda.py +193 -193
  23. bitsandbytes/diagnostics/main.py +134 -134
  24. bitsandbytes/diagnostics/utils.py +12 -12
  25. bitsandbytes/disk_balancer.py +1276 -1224
  26. bitsandbytes/efst.py +777 -0
  27. bitsandbytes/functional.py +1836 -1836
  28. bitsandbytes/gdn_cpu.py +616 -602
  29. bitsandbytes/libbitsandbytes_cpu.dll +0 -0
  30. bitsandbytes/nn/__init__.py +19 -19
  31. bitsandbytes/nn/modules.py +1220 -1220
  32. bitsandbytes/nn/parametrize.py +206 -206
  33. bitsandbytes/optim/__init__.py +0 -4
  34. bitsandbytes/optim/adagrad.py +187 -187
  35. bitsandbytes/optim/adam.py +346 -346
  36. bitsandbytes/optim/adamw.py +337 -337
  37. bitsandbytes/optim/ademamix.py +410 -410
  38. bitsandbytes/optim/lamb.py +190 -190
  39. bitsandbytes/optim/lars.py +259 -259
  40. bitsandbytes/optim/lion.py +266 -266
  41. bitsandbytes/optim/optimizer.py +756 -756
  42. bitsandbytes/optim/rmsprop.py +170 -170
  43. bitsandbytes/optim/sgd.py +152 -152
  44. bitsandbytes/torch_cpu_kit.py +468 -468
  45. bitsandbytes/utils.py +208 -208
  46. bitsandbytes_cpu_cli.py +1012 -949
  47. {bitsandbytes_cpu_fork-0.50.2.dev2.dist-info → bitsandbytes_cpu_fork-0.50.2.dev4.dist-info}/METADATA +12 -4
  48. bitsandbytes_cpu_fork-0.50.2.dev4.dist-info/RECORD +64 -0
  49. {bitsandbytes_cpu_fork-0.50.2.dev2.dist-info → bitsandbytes_cpu_fork-0.50.2.dev4.dist-info}/licenses/LICENSE +21 -21
  50. bitsandbytes/optim/adamw4bit.py +0 -183
  51. bitsandbytes_cpu_fork-0.50.2.dev2.dist-info/RECORD +0 -63
  52. {bitsandbytes_cpu_fork-0.50.2.dev2.dist-info → bitsandbytes_cpu_fork-0.50.2.dev4.dist-info}/WHEEL +0 -0
  53. {bitsandbytes_cpu_fork-0.50.2.dev2.dist-info → bitsandbytes_cpu_fork-0.50.2.dev4.dist-info}/entry_points.txt +0 -0
  54. {bitsandbytes_cpu_fork-0.50.2.dev2.dist-info → bitsandbytes_cpu_fork-0.50.2.dev4.dist-info}/top_level.txt +0 -0
@@ -1,468 +1,468 @@
1
- """torch_cpu_kit — 无 CUDA 环境(纯 CPU)训练压榨工具包。
2
-
3
- 适用场景:Windows 10 / Linux,无 NVIDIA 显卡,用 pip 装的官方 PyTorch
4
- (>= 2.0),目标是把 i5-10400(6C12T, AVX2) / R5-4500U(6C6T, AVX2)
5
- 这类 AVX2-only CPU 的训练吞吐和内存占用压到最优。
6
-
7
- 用法(两行上手)::
8
-
9
- import torch_cpu_kit as tck # 1) 先 import 本 kit
10
- tck.apply_env() # 2) 设置 OMP/MKL 环境变量(必须在 import torch 之前)
11
- import torch # 3) 再 import torch
12
- tck.init(verbose=True) # 4) 线程数 / oneDNN / matmul 精度一键调优
13
-
14
- 设计原则:
15
- - 纯 Python,不重编 torch,对任何 pip 安装的 torch 生效;
16
- - 所有函数都可以反复调用;检测失败时安全回退,绝不抛异常打断训练;
17
- - 与 bitsandbytes CPU 版(GDN 内核 + 8-bit 优化器)完全兼容,
18
- 配合 GDN_CPU_CHUNK 环境变量一起用效果最佳。
19
- """
20
-
21
- from __future__ import annotations
22
-
23
- import os
24
- import subprocess
25
- import threading
26
- from contextlib import nullcontext
27
- from typing import Any, Optional
28
-
29
- __all__ = [
30
- "apply_env",
31
- "init",
32
- "report",
33
- "physical_cores",
34
- "recommended_autocast",
35
- "autocast",
36
- "fast_loader",
37
- "make_contiguous_",
38
- "mem_report",
39
- "start_mem_monitor",
40
- "suspend_mem_monitor",
41
- ]
42
-
43
- _state: dict[str, Any] = {"env": None, "init": None}
44
-
45
-
46
- # ---------------------------------------------------------------------------
47
- # 物理核检测(关键:i5-10400 是 6C12T,线程数应为 6 而不是 12;
48
- # R5-4500U 是 6C6T 无超线程,直接用逻辑核数 6)
49
- # ---------------------------------------------------------------------------
50
- _cores_cache: Optional[int] = None
51
-
52
-
53
- def physical_cores() -> int:
54
- """返回物理核数(结果缓存)。Linux 读 sysfs,Windows 读 wmic/PowerShell,
55
- 都失败则回退逻辑核数(os.cpu_count())。"""
56
- global _cores_cache
57
- if _cores_cache is not None:
58
- return _cores_cache
59
- # Linux: 每个 (package, core) 组合计一个物理核
60
- try:
61
- import glob
62
-
63
- ids = []
64
- for d in glob.glob("/sys/devices/system/cpu/cpu[0-9]*/topology"):
65
- try:
66
- with open(os.path.join(d, "physical_package_id")) as f:
67
- pkg = f.read().strip()
68
- with open(os.path.join(d, "core_id")) as f:
69
- core = f.read().strip()
70
- ids.append((pkg, core))
71
- except OSError:
72
- continue
73
- if ids:
74
- _cores_cache = max(1, len(set(ids)))
75
- return _cores_cache
76
- except Exception:
77
- pass
78
- # Windows: wmic(Win10 自带)-> PowerShell 兜底
79
- if os.name == "nt":
80
- for cmd in (
81
- ["wmic", "cpu", "get", "NumberOfCores", "/value"],
82
- ["powershell", "-NoProfile", "-Command",
83
- "(Get-CimInstance Win32_Processor | Measure-Object -Property NumberOfCores -Sum).Sum"],
84
- ):
85
- try:
86
- out = subprocess.run(
87
- cmd, capture_output=True, text=True, timeout=15
88
- ).stdout
89
- nums = [
90
- int(line.split("=", 1)[1])
91
- for line in out.splitlines()
92
- if "=" in line and line.split("=", 1)[1].strip().isdigit()
93
- ]
94
- if not nums and out.strip().isdigit(): # powershell 直接输出一个数
95
- nums = [int(out.strip())]
96
- if nums:
97
- _cores_cache = max(1, sum(nums))
98
- return _cores_cache
99
- except Exception:
100
- continue
101
- _cores_cache = os.cpu_count() or 1
102
- return _cores_cache
103
-
104
-
105
- # ---------------------------------------------------------------------------
106
- # 环境变量(必须在 import torch 之前调用才生效)
107
- # ---------------------------------------------------------------------------
108
- def apply_env(
109
- num_threads: Optional[int] = None,
110
- wait_policy: str = "active",
111
- verbose: bool = False,
112
- ) -> dict[str, str]:
113
- """设置 OpenMP/MKL 环境变量。**必须在 import torch 之前调用**(这些
114
- 变量在 OpenMP 运行库加载时读取,之后改无效)。
115
-
116
- - OMP_NUM_THREADS / MKL_NUM_THREADS = 物理核数(见 physical_cores())。
117
- 超线程对 GEMM 帮助很小,却会和 bitsandbytes 内核自己的 OpenMP 线程
118
- 池打架(12 逻辑核 x 2 套线程池 = 24 线程在 6 个物理核上空转)。
119
- - OMP_WAIT_POLICY=active:自旋等待,短生命周期并行区(比如每层都跑
120
- 的 GDN kernel)唤醒延迟最低。
121
- - OMP_DYNAMIC=FALSE / MKL_DYNAMIC=FALSE:禁止运行库偷懒减线程。
122
-
123
- 已存在的环境变量不会被覆盖;显式传 num_threads 则强制覆盖。
124
- 返回实际生效的键值 dict。
125
- """
126
- n = num_threads or physical_cores()
127
- want = {
128
- "OMP_NUM_THREADS": str(n),
129
- "MKL_NUM_THREADS": str(n),
130
- "OMP_WAIT_POLICY": wait_policy,
131
- "OMP_DYNAMIC": "FALSE",
132
- "MKL_DYNAMIC": "FALSE",
133
- }
134
- set_vals: dict[str, str] = {}
135
- for k, v in want.items():
136
- if num_threads is not None or k not in os.environ or not os.environ[k]:
137
- os.environ[k] = v
138
- set_vals[k] = os.environ[k]
139
- _state["env"] = set_vals
140
- if verbose:
141
- print("[torch_cpu_kit] env:", set_vals)
142
- return set_vals
143
-
144
-
145
- # ---------------------------------------------------------------------------
146
- # torch 侧一键调优(import torch 之后调用)
147
- # ---------------------------------------------------------------------------
148
- def init(
149
- num_threads: Optional[int] = None,
150
- interop_threads: int = 1,
151
- verbose: bool = False,
152
- model_kind: Optional[str] = None,
153
- ) -> dict[str, Any]:
154
- """import torch 之后调用:线程池 / oneDNN / matmul 精度一键调优。
155
-
156
- - torch.set_num_threads(模型类型最优线程):见下;
157
- - torch.set_num_interop_threads(1):训练是单进程单图的串行图,interop
158
- 池只会抢核(若 torch 已启动并行工作会抛 RuntimeError,内部吞掉);
159
- - oneDNN 打开(AVX2 GEMM/conv 走它最快);
160
- - 仅当 CPU 支持 AVX512-BF16/AMX 时才放宽 float32 matmul 精度到
161
- "high";AVX2-only 的 CPU 保持 fp32 不降精度。
162
-
163
- ``model_kind`` 按任务选线程(i5-10400 6C12T 实测,GFLOP/s):
164
-
165
- - ``'lm'``(LLM 微调,GEMM 密集):**8 线程**最优(277→8线程313→12线程273,
166
- 超线程帮不到 GEMM 还拖后腿);
167
- - ``'vision'`` / ``'diffusion'``(生图 U-Net / 视觉 tower,**conv 密集**):
168
- **12 线程**最优(conv 6线程329→8线程489→12线程618 GFLOP/s,超线程被 conv
169
- 的 im2col/分块吃满);attention 小块反而 6 线程好,但整体 conv 主导;
170
- - ``None``:回退物理核数。
171
-
172
- 返回调优报告文本(同 report() 的字符串,不是 dict)。
173
- """
174
- import torch
175
-
176
- if num_threads is None:
177
- logical = os.cpu_count() or 1
178
- physical = physical_cores()
179
- if model_kind in ("vision", "diffusion", "image", "img"):
180
- _n = logical # conv 受益超线程:用逻辑核(i5-10400 = 12)
181
- elif model_kind == "lm":
182
- # GEMM 密集:有超线程时逻辑核×2/3(i5-10400 12→8);无超线程
183
- # (R5-4500U 6C6T)直接拉满物理核,别打折——实测 6 线程比
184
- # 4/5 线程快 16%/8%(GEMM 125 vs 101/108 GFLOP/s)
185
- _n = physical if physical == logical else max(1, int(logical * 2 // 3))
186
- else:
187
- _n = physical
188
- n = _n
189
- else:
190
- n = num_threads
191
- torch.set_num_threads(max(1, n))
192
- try:
193
- torch.set_num_interop_threads(max(1, interop_threads))
194
- except RuntimeError:
195
- pass # 已经有并行工作启动了,保持原值
196
- torch.backends.mkldnn.enabled = True
197
- if _bf16_capable():
198
- torch.set_float32_matmul_precision("high")
199
-
200
- _state["init"] = {"threads": n, "interop": interop_threads, "model_kind": model_kind}
201
- rep = report()
202
- if verbose:
203
- print("[torch_cpu_kit] init:\n" + rep)
204
- return rep
205
-
206
-
207
- def _capability() -> str:
208
- try:
209
- import torch
210
-
211
- return torch.backends.cpu.get_cpu_capability()
212
- except Exception:
213
- return "DEFAULT"
214
-
215
-
216
- def _bf16_capable() -> bool:
217
- cap = _capability()
218
- return "AVX512" in cap and "BF16" in cap or cap == "AMX"
219
-
220
-
221
- def report() -> str:
222
- """人类可读的当前 CPU/线程/精度状态报告(不修改任何东西)。"""
223
- import torch
224
-
225
- lines = [
226
- f" CPU capability : {_capability()}",
227
- f" logical cores : {os.cpu_count()}",
228
- f" physical cores : {physical_cores()}",
229
- f" torch threads : {torch.get_num_threads()} "
230
- f"(interop {torch.get_num_interop_threads()})",
231
- f" oneDNN (mkldnn): {torch.backends.mkldnn.enabled}",
232
- f" f32 matmul : {torch.get_float32_matmul_precision()}",
233
- f" autocast advice: {recommended_autocast()}",
234
- ]
235
- rss, avail = mem_report()
236
- lines.append(f" memory : RSS {rss:.0f} MB, available {avail:.0f} MB")
237
- env = _state.get("env")
238
- if env:
239
- lines.append(f" env applied : {env}")
240
- else:
241
- lines.append(" env applied : (none - call apply_env() BEFORE import torch)")
242
- return "\n".join(lines)
243
-
244
-
245
- # ---------------------------------------------------------------------------
246
- # 混合精度决策:AVX2-only 的 CPU 上 bf16 反而更慢(没有硬件 bf16 指令,
247
- # 走软件模拟),所以只有 AVX512-BF16 / AMX 才推荐 bf16
248
- # ---------------------------------------------------------------------------
249
- def recommended_autocast():
250
- """返回推荐的 CPU autocast dtype:AVX512-BF16/AMX -> torch.bfloat16,
251
- 否则 -> torch.float32(i5-10400 / R5-4500U 都会是 fp32)。"""
252
- import torch
253
-
254
- return torch.bfloat16 if _bf16_capable() else torch.float32
255
-
256
-
257
- def autocast(dtype=None, enabled: bool = True):
258
- """返回 CPU autocast 上下文管理器::
259
-
260
- with tck.autocast():
261
- out = model(x)
262
-
263
- dtype 省略时用 recommended_autocast() 的建议(AVX2-only 机器上返回
264
- nullcontext,即 fp32 原样训练——这本身就是这类 CPU 的最优解)。
265
- """
266
- import torch
267
-
268
- dt = dtype or recommended_autocast()
269
- if dt == torch.float32:
270
- return nullcontext()
271
- return torch.autocast(device_type="cpu", dtype=dt, enabled=enabled)
272
-
273
-
274
- # ---------------------------------------------------------------------------
275
- # DataLoader / 模型布局
276
- # ---------------------------------------------------------------------------
277
- def fast_loader(dataset, batch_size: int = 32, **kwargs):
278
- """带 CPU 最优默认值的 DataLoader:
279
-
280
- - pin_memory=False(没有 GPU,pin 只是无谓的锁定内存页);
281
- - num_workers 默认 2(训练循环里 fwd/bwd 已经吃满物理核,worker 太多
282
- 反而和主进程抢核;再靠 prefetch 提前备好 batch);
283
- - num_workers > 0 时自动 persistent_workers=True(省掉每 epoch 重启
284
- worker 的进程创建开销——Windows 上进程创建尤其贵)。
285
-
286
- 其余参数原样透传给 torch.utils.data.DataLoader。
287
- """
288
- import torch
289
-
290
- kw = dict(kwargs)
291
- workers = kw.pop("num_workers", 2)
292
- kw.setdefault("pin_memory", False)
293
- if workers > 0:
294
- kw.setdefault("persistent_workers", True)
295
- kw.setdefault("prefetch_factor", 2)
296
- return torch.utils.data.DataLoader(dataset, batch_size=batch_size,
297
- num_workers=workers, **kw)
298
-
299
-
300
- def make_contiguous_(module):
301
- """把 module 的所有参数/缓冲区改成 C 连续布局(原地)。
302
-
303
- 非连续参数(transpose 出来的等)每次前向都要隐式拷贝;训练前一次性
304
- 转正,配合 bitsandbytes 的 8-bit 优化器(C 内核只走连续内存)尤其
305
- 重要——非连续梯度会被自动回退到慢路径。返回 module 本身。
306
- """
307
- for p in list(module.parameters()) + list(module.buffers()):
308
- if not p.data.is_contiguous():
309
- p.data = p.data.contiguous()
310
- return module
311
-
312
-
313
- # ---------------------------------------------------------------------------
314
- # 内存监控(防 swap:available 快耗尽时就该减 batch / 序列长了)
315
- # ---------------------------------------------------------------------------
316
- def mem_report() -> tuple[float, float]:
317
- """返回 (进程RSS_MB, 系统可用内存_MB)。优先 psutil(跨平台且不依赖
318
- ctypes 的 GlobalMemoryStatusEx,后者在部分环境下返回 INVALID_PARAMETER),
319
- 否则 Linux 走 /proc、Windows 走 GlobalMemoryStatusEx。训练循环里周期性
320
- 打点,available 逼近 0 就是在 swap 拷打 SSD 的前兆。"""
321
- rss = avail = -1.0
322
- try: # psutil:跨平台最稳(Windows/Linux 都是 C 扩展实现)
323
- import psutil
324
- rss = float(psutil.Process().memory_info().rss) / (1 << 20)
325
- avail = float(psutil.virtual_memory().available) / (1 << 20)
326
- return rss, avail
327
- except Exception:
328
- pass
329
- try: # Linux RSS
330
- with open("/proc/self/status") as f:
331
- for line in f:
332
- if line.startswith("VmRSS:"):
333
- rss = int(line.split()[1]) / 1024
334
- break
335
- except OSError:
336
- pass
337
- try: # Linux available
338
- with open("/proc/meminfo") as f:
339
- for line in f:
340
- if line.startswith("MemAvailable:"):
341
- avail = int(line.split()[1]) / 1024
342
- break
343
- except OSError:
344
- pass
345
- if rss < 0 or avail < 0: # Windows / 兜底
346
- try:
347
- import ctypes
348
-
349
- class _MS(ctypes.Structure):
350
- # 完整 MEMORYSTATUSEX(缺 ullTotalVirtual/ullAvailVirtual 会让
351
- # GlobalMemoryStatusEx 因 dwLength 校验失败返回 FALSE)
352
- _fields_ = [
353
- ("dwLength", ctypes.c_uint), ("dwMemoryLoad", ctypes.c_uint),
354
- ("ullTotalPhys", ctypes.c_uint64), ("ullAvailPhys", ctypes.c_uint64),
355
- ("ullTotalPageFile", ctypes.c_uint64), ("ullAvailPageFile", ctypes.c_uint64),
356
- ("ullTotalVirtual", ctypes.c_uint64), ("ullAvailVirtual", ctypes.c_uint64),
357
- ]
358
-
359
- ms = _MS(dwLength=ctypes.sizeof(_MS))
360
- k32 = ctypes.windll.kernel32
361
- k32.GlobalMemoryStatusEx.argtypes = [ctypes.POINTER(_MS)]
362
- k32.GlobalMemoryStatusEx.restype = ctypes.c_int
363
- if k32.GlobalMemoryStatusEx(ctypes.byref(ms)):
364
- if avail < 0:
365
- avail = float(ms.ullAvailPhys) / (1 << 20)
366
- if rss < 0: # Windows 无 /proc,用工作集近似
367
- import ctypes.wintypes as wt
368
-
369
- class _PMC(ctypes.Structure):
370
- _fields_ = [("cb", wt.DWORD), ("PageFaultCount", wt.DWORD),
371
- ("PeakWorkingSetSize", ctypes.c_size_t),
372
- ("WorkingSetSize", ctypes.c_size_t)] + \
373
- [(n, ctypes.c_size_t) for n in (
374
- "QuotaPeakPagedPoolUsage", "QuotaPagedPoolUsage",
375
- "QuotaPeakNonPagedPoolUsage", "QuotaNonPagedPoolUsage",
376
- "PagefileUsage", "PeakPagefileUsage")]
377
-
378
- pmc = _PMC(cb=ctypes.sizeof(_PMC))
379
- psapi = ctypes.windll.psapi
380
- # 显式签名:默认 c_int 会截断 64 位 HANDLE/指针,导致调用静默失败
381
- psapi.GetProcessMemoryInfo.argtypes = [wt.HANDLE, ctypes.POINTER(_PMC), wt.DWORD]
382
- psapi.GetProcessMemoryInfo.restype = wt.BOOL
383
- h = ctypes.windll.kernel32.GetCurrentProcess()
384
- if psapi.GetProcessMemoryInfo(h, ctypes.byref(pmc), pmc.cb):
385
- rss = float(pmc.WorkingSetSize) / (1 << 20)
386
- except Exception:
387
- pass
388
- return rss, avail
389
-
390
-
391
- # ---------------------------------------------------------------------------
392
- # 后台内存/swap 监视器(防 SSD 拷打)
393
- # ---------------------------------------------------------------------------
394
- _MON_STOP = threading.Event()
395
-
396
-
397
- def start_mem_monitor(interval: float = 20.0) -> threading.Event:
398
- """启动后台线程,每 ``interval`` 秒打印一次物理内存占用 + 提交量。
399
-
400
- 提交量(commit)逼近物理内存即进入 swap(页面文件)——"拷打 SSD"的信号。
401
- 返回一个 Event,调用 ``suspend_mem_monitor(ev)`` 停止。
402
-
403
- 用法::
404
-
405
- from torch_cpu_kit import start_mem_monitor, suspend_mem_monitor
406
- _ev = start_mem_monitor(interval=30)
407
- ... 训练循环 ...
408
- suspend_mem_monitor(_ev)
409
- """
410
- _MON_STOP.clear()
411
-
412
- def loop():
413
- while not _MON_STOP.is_set():
414
- try:
415
- try: # psutil 优先(ctypes 的 GlobalMemoryStatusEx 部分环境失败)
416
- import psutil
417
- vm = psutil.virtual_memory()
418
- tg, ug = vm.total / (1 << 30), (vm.total - vm.available) / (1 << 30)
419
- sm = psutil.swap_memory()
420
- cg = sm.used / (1 << 30) if sm.total > 0 else ug # 实际换页用量
421
- flag = " <-- SWAP! 降低batch/序列或检查页面文件" if cg > tg * 0.97 else ""
422
- print(f"[tck.mem] RAM {ug:5.2f}/{tg:.2f} GB swap {cg:6.2f} GB{flag}")
423
- except Exception:
424
- import ctypes
425
-
426
- class _MS(ctypes.Structure):
427
- # 完整 MEMORYSTATUSEX(缺字段 -> GlobalMemoryStatusEx 返回 FALSE)
428
- _fields_ = [("l", ctypes.c_uint), ("m", ctypes.c_uint),
429
- ("t", ctypes.c_uint64), ("a", ctypes.c_uint64),
430
- ("tp", ctypes.c_uint64), ("ap", ctypes.c_uint64),
431
- ("tv", ctypes.c_uint64), ("av", ctypes.c_uint64)]
432
-
433
- ms = _MS(l=ctypes.sizeof(_MS))
434
- k32 = ctypes.windll.kernel32
435
- k32.GlobalMemoryStatusEx.argtypes = [ctypes.POINTER(_MS)]
436
- k32.GlobalMemoryStatusEx.restype = ctypes.c_int
437
- if k32.GlobalMemoryStatusEx(ctypes.byref(ms)):
438
- tg, ug = ms.t / (1 << 30), (ms.t - ms.a) / (1 << 30)
439
- cg = (ms.tp - ms.ap) / (1 << 30)
440
- flag = " <-- SWAP! 降低batch/序列或检查页面文件" if cg > tg * 0.97 else ""
441
- print(f"[tck.mem] RAM {ug:5.2f}/{tg:.2f} GB commit {cg:6.2f} GB{flag}")
442
- except Exception:
443
- pass
444
- _MON_STOP.wait(interval)
445
-
446
- t = threading.Thread(target=loop, daemon=True)
447
- t.start()
448
- return _MON_STOP
449
-
450
-
451
- def suspend_mem_monitor(ev: Optional[threading.Event] = None) -> None:
452
- """停止 ``start_mem_monitor`` 启动的监视线程。"""
453
- (ev or _MON_STOP).set()
454
-
455
-
456
- if __name__ == "__main__": # 自检:python torch_cpu_kit.py
457
- apply_env(verbose=True)
458
- import torch
459
-
460
- init(verbose=True)
461
- ds = torch.utils.data.TensorDataset(torch.randn(64, 8), torch.randn(64))
462
- dl = fast_loader(ds, batch_size=16)
463
- for xb, yb in dl:
464
- pass
465
- with autocast():
466
- y = torch.nn.Linear(8, 8)(torch.randn(4, 8))
467
- assert y.shape == (4, 8)
468
- print("[torch_cpu_kit] selftest OK")
1
+ """torch_cpu_kit — 无 CUDA 环境(纯 CPU)训练压榨工具包。
2
+
3
+ 适用场景:Windows 10 / Linux,无 NVIDIA 显卡,用 pip 装的官方 PyTorch
4
+ (>= 2.0),目标是把 i5-10400(6C12T, AVX2) / R5-4500U(6C6T, AVX2)
5
+ 这类 AVX2-only CPU 的训练吞吐和内存占用压到最优。
6
+
7
+ 用法(两行上手)::
8
+
9
+ import torch_cpu_kit as tck # 1) 先 import 本 kit
10
+ tck.apply_env() # 2) 设置 OMP/MKL 环境变量(必须在 import torch 之前)
11
+ import torch # 3) 再 import torch
12
+ tck.init(verbose=True) # 4) 线程数 / oneDNN / matmul 精度一键调优
13
+
14
+ 设计原则:
15
+ - 纯 Python,不重编 torch,对任何 pip 安装的 torch 生效;
16
+ - 所有函数都可以反复调用;检测失败时安全回退,绝不抛异常打断训练;
17
+ - 与 bitsandbytes CPU 版(GDN 内核 + 8-bit 优化器)完全兼容,
18
+ 配合 GDN_CPU_CHUNK 环境变量一起用效果最佳。
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import os
24
+ import subprocess
25
+ import threading
26
+ from contextlib import nullcontext
27
+ from typing import Any, Optional
28
+
29
+ __all__ = [
30
+ "apply_env",
31
+ "init",
32
+ "report",
33
+ "physical_cores",
34
+ "recommended_autocast",
35
+ "autocast",
36
+ "fast_loader",
37
+ "make_contiguous_",
38
+ "mem_report",
39
+ "start_mem_monitor",
40
+ "suspend_mem_monitor",
41
+ ]
42
+
43
+ _state: dict[str, Any] = {"env": None, "init": None}
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # 物理核检测(关键:i5-10400 是 6C12T,线程数应为 6 而不是 12;
48
+ # R5-4500U 是 6C6T 无超线程,直接用逻辑核数 6)
49
+ # ---------------------------------------------------------------------------
50
+ _cores_cache: Optional[int] = None
51
+
52
+
53
+ def physical_cores() -> int:
54
+ """返回物理核数(结果缓存)。Linux 读 sysfs,Windows 读 wmic/PowerShell,
55
+ 都失败则回退逻辑核数(os.cpu_count())。"""
56
+ global _cores_cache
57
+ if _cores_cache is not None:
58
+ return _cores_cache
59
+ # Linux: 每个 (package, core) 组合计一个物理核
60
+ try:
61
+ import glob
62
+
63
+ ids = []
64
+ for d in glob.glob("/sys/devices/system/cpu/cpu[0-9]*/topology"):
65
+ try:
66
+ with open(os.path.join(d, "physical_package_id")) as f:
67
+ pkg = f.read().strip()
68
+ with open(os.path.join(d, "core_id")) as f:
69
+ core = f.read().strip()
70
+ ids.append((pkg, core))
71
+ except OSError:
72
+ continue
73
+ if ids:
74
+ _cores_cache = max(1, len(set(ids)))
75
+ return _cores_cache
76
+ except Exception:
77
+ pass
78
+ # Windows: wmic(Win10 自带)-> PowerShell 兜底
79
+ if os.name == "nt":
80
+ for cmd in (
81
+ ["wmic", "cpu", "get", "NumberOfCores", "/value"],
82
+ ["powershell", "-NoProfile", "-Command",
83
+ "(Get-CimInstance Win32_Processor | Measure-Object -Property NumberOfCores -Sum).Sum"],
84
+ ):
85
+ try:
86
+ out = subprocess.run(
87
+ cmd, capture_output=True, text=True, timeout=15
88
+ ).stdout
89
+ nums = [
90
+ int(line.split("=", 1)[1])
91
+ for line in out.splitlines()
92
+ if "=" in line and line.split("=", 1)[1].strip().isdigit()
93
+ ]
94
+ if not nums and out.strip().isdigit(): # powershell 直接输出一个数
95
+ nums = [int(out.strip())]
96
+ if nums:
97
+ _cores_cache = max(1, sum(nums))
98
+ return _cores_cache
99
+ except Exception:
100
+ continue
101
+ _cores_cache = os.cpu_count() or 1
102
+ return _cores_cache
103
+
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # 环境变量(必须在 import torch 之前调用才生效)
107
+ # ---------------------------------------------------------------------------
108
+ def apply_env(
109
+ num_threads: Optional[int] = None,
110
+ wait_policy: str = "active",
111
+ verbose: bool = False,
112
+ ) -> dict[str, str]:
113
+ """设置 OpenMP/MKL 环境变量。**必须在 import torch 之前调用**(这些
114
+ 变量在 OpenMP 运行库加载时读取,之后改无效)。
115
+
116
+ - OMP_NUM_THREADS / MKL_NUM_THREADS = 物理核数(见 physical_cores())。
117
+ 超线程对 GEMM 帮助很小,却会和 bitsandbytes 内核自己的 OpenMP 线程
118
+ 池打架(12 逻辑核 x 2 套线程池 = 24 线程在 6 个物理核上空转)。
119
+ - OMP_WAIT_POLICY=active:自旋等待,短生命周期并行区(比如每层都跑
120
+ 的 GDN kernel)唤醒延迟最低。
121
+ - OMP_DYNAMIC=FALSE / MKL_DYNAMIC=FALSE:禁止运行库偷懒减线程。
122
+
123
+ 已存在的环境变量不会被覆盖;显式传 num_threads 则强制覆盖。
124
+ 返回实际生效的键值 dict。
125
+ """
126
+ n = num_threads or physical_cores()
127
+ want = {
128
+ "OMP_NUM_THREADS": str(n),
129
+ "MKL_NUM_THREADS": str(n),
130
+ "OMP_WAIT_POLICY": wait_policy,
131
+ "OMP_DYNAMIC": "FALSE",
132
+ "MKL_DYNAMIC": "FALSE",
133
+ }
134
+ set_vals: dict[str, str] = {}
135
+ for k, v in want.items():
136
+ if num_threads is not None or k not in os.environ or not os.environ[k]:
137
+ os.environ[k] = v
138
+ set_vals[k] = os.environ[k]
139
+ _state["env"] = set_vals
140
+ if verbose:
141
+ print("[torch_cpu_kit] env:", set_vals)
142
+ return set_vals
143
+
144
+
145
+ # ---------------------------------------------------------------------------
146
+ # torch 侧一键调优(import torch 之后调用)
147
+ # ---------------------------------------------------------------------------
148
+ def init(
149
+ num_threads: Optional[int] = None,
150
+ interop_threads: int = 1,
151
+ verbose: bool = False,
152
+ model_kind: Optional[str] = None,
153
+ ) -> dict[str, Any]:
154
+ """import torch 之后调用:线程池 / oneDNN / matmul 精度一键调优。
155
+
156
+ - torch.set_num_threads(模型类型最优线程):见下;
157
+ - torch.set_num_interop_threads(1):训练是单进程单图的串行图,interop
158
+ 池只会抢核(若 torch 已启动并行工作会抛 RuntimeError,内部吞掉);
159
+ - oneDNN 打开(AVX2 GEMM/conv 走它最快);
160
+ - 仅当 CPU 支持 AVX512-BF16/AMX 时才放宽 float32 matmul 精度到
161
+ "high";AVX2-only 的 CPU 保持 fp32 不降精度。
162
+
163
+ ``model_kind`` 按任务选线程(i5-10400 6C12T 实测,GFLOP/s):
164
+
165
+ - ``'lm'``(LLM 微调,GEMM 密集):**8 线程**最优(277→8线程313→12线程273,
166
+ 超线程帮不到 GEMM 还拖后腿);
167
+ - ``'vision'`` / ``'diffusion'``(生图 U-Net / 视觉 tower,**conv 密集**):
168
+ **12 线程**最优(conv 6线程329→8线程489→12线程618 GFLOP/s,超线程被 conv
169
+ 的 im2col/分块吃满);attention 小块反而 6 线程好,但整体 conv 主导;
170
+ - ``None``:回退物理核数。
171
+
172
+ 返回调优报告文本(同 report() 的字符串,不是 dict)。
173
+ """
174
+ import torch
175
+
176
+ if num_threads is None:
177
+ logical = os.cpu_count() or 1
178
+ physical = physical_cores()
179
+ if model_kind in ("vision", "diffusion", "image", "img"):
180
+ _n = logical # conv 受益超线程:用逻辑核(i5-10400 = 12)
181
+ elif model_kind == "lm":
182
+ # GEMM 密集:有超线程时逻辑核×2/3(i5-10400 12→8);无超线程
183
+ # (R5-4500U 6C6T)直接拉满物理核,别打折——实测 6 线程比
184
+ # 4/5 线程快 16%/8%(GEMM 125 vs 101/108 GFLOP/s)
185
+ _n = physical if physical == logical else max(1, int(logical * 2 // 3))
186
+ else:
187
+ _n = physical
188
+ n = _n
189
+ else:
190
+ n = num_threads
191
+ torch.set_num_threads(max(1, n))
192
+ try:
193
+ torch.set_num_interop_threads(max(1, interop_threads))
194
+ except RuntimeError:
195
+ pass # 已经有并行工作启动了,保持原值
196
+ torch.backends.mkldnn.enabled = True
197
+ if _bf16_capable():
198
+ torch.set_float32_matmul_precision("high")
199
+
200
+ _state["init"] = {"threads": n, "interop": interop_threads, "model_kind": model_kind}
201
+ rep = report()
202
+ if verbose:
203
+ print("[torch_cpu_kit] init:\n" + rep)
204
+ return rep
205
+
206
+
207
+ def _capability() -> str:
208
+ try:
209
+ import torch
210
+
211
+ return torch.backends.cpu.get_cpu_capability()
212
+ except Exception:
213
+ return "DEFAULT"
214
+
215
+
216
+ def _bf16_capable() -> bool:
217
+ cap = _capability()
218
+ return "AVX512" in cap and "BF16" in cap or cap == "AMX"
219
+
220
+
221
+ def report() -> str:
222
+ """人类可读的当前 CPU/线程/精度状态报告(不修改任何东西)。"""
223
+ import torch
224
+
225
+ lines = [
226
+ f" CPU capability : {_capability()}",
227
+ f" logical cores : {os.cpu_count()}",
228
+ f" physical cores : {physical_cores()}",
229
+ f" torch threads : {torch.get_num_threads()} "
230
+ f"(interop {torch.get_num_interop_threads()})",
231
+ f" oneDNN (mkldnn): {torch.backends.mkldnn.enabled}",
232
+ f" f32 matmul : {torch.get_float32_matmul_precision()}",
233
+ f" autocast advice: {recommended_autocast()}",
234
+ ]
235
+ rss, avail = mem_report()
236
+ lines.append(f" memory : RSS {rss:.0f} MB, available {avail:.0f} MB")
237
+ env = _state.get("env")
238
+ if env:
239
+ lines.append(f" env applied : {env}")
240
+ else:
241
+ lines.append(" env applied : (none - call apply_env() BEFORE import torch)")
242
+ return "\n".join(lines)
243
+
244
+
245
+ # ---------------------------------------------------------------------------
246
+ # 混合精度决策:AVX2-only 的 CPU 上 bf16 反而更慢(没有硬件 bf16 指令,
247
+ # 走软件模拟),所以只有 AVX512-BF16 / AMX 才推荐 bf16
248
+ # ---------------------------------------------------------------------------
249
+ def recommended_autocast():
250
+ """返回推荐的 CPU autocast dtype:AVX512-BF16/AMX -> torch.bfloat16,
251
+ 否则 -> torch.float32(i5-10400 / R5-4500U 都会是 fp32)。"""
252
+ import torch
253
+
254
+ return torch.bfloat16 if _bf16_capable() else torch.float32
255
+
256
+
257
+ def autocast(dtype=None, enabled: bool = True):
258
+ """返回 CPU autocast 上下文管理器::
259
+
260
+ with tck.autocast():
261
+ out = model(x)
262
+
263
+ dtype 省略时用 recommended_autocast() 的建议(AVX2-only 机器上返回
264
+ nullcontext,即 fp32 原样训练——这本身就是这类 CPU 的最优解)。
265
+ """
266
+ import torch
267
+
268
+ dt = dtype or recommended_autocast()
269
+ if dt == torch.float32:
270
+ return nullcontext()
271
+ return torch.autocast(device_type="cpu", dtype=dt, enabled=enabled)
272
+
273
+
274
+ # ---------------------------------------------------------------------------
275
+ # DataLoader / 模型布局
276
+ # ---------------------------------------------------------------------------
277
+ def fast_loader(dataset, batch_size: int = 32, **kwargs):
278
+ """带 CPU 最优默认值的 DataLoader:
279
+
280
+ - pin_memory=False(没有 GPU,pin 只是无谓的锁定内存页);
281
+ - num_workers 默认 2(训练循环里 fwd/bwd 已经吃满物理核,worker 太多
282
+ 反而和主进程抢核;再靠 prefetch 提前备好 batch);
283
+ - num_workers > 0 时自动 persistent_workers=True(省掉每 epoch 重启
284
+ worker 的进程创建开销——Windows 上进程创建尤其贵)。
285
+
286
+ 其余参数原样透传给 torch.utils.data.DataLoader。
287
+ """
288
+ import torch
289
+
290
+ kw = dict(kwargs)
291
+ workers = kw.pop("num_workers", 2)
292
+ kw.setdefault("pin_memory", False)
293
+ if workers > 0:
294
+ kw.setdefault("persistent_workers", True)
295
+ kw.setdefault("prefetch_factor", 2)
296
+ return torch.utils.data.DataLoader(dataset, batch_size=batch_size,
297
+ num_workers=workers, **kw)
298
+
299
+
300
+ def make_contiguous_(module):
301
+ """把 module 的所有参数/缓冲区改成 C 连续布局(原地)。
302
+
303
+ 非连续参数(transpose 出来的等)每次前向都要隐式拷贝;训练前一次性
304
+ 转正,配合 bitsandbytes 的 8-bit 优化器(C 内核只走连续内存)尤其
305
+ 重要——非连续梯度会被自动回退到慢路径。返回 module 本身。
306
+ """
307
+ for p in list(module.parameters()) + list(module.buffers()):
308
+ if not p.data.is_contiguous():
309
+ p.data = p.data.contiguous()
310
+ return module
311
+
312
+
313
+ # ---------------------------------------------------------------------------
314
+ # 内存监控(防 swap:available 快耗尽时就该减 batch / 序列长了)
315
+ # ---------------------------------------------------------------------------
316
+ def mem_report() -> tuple[float, float]:
317
+ """返回 (进程RSS_MB, 系统可用内存_MB)。优先 psutil(跨平台且不依赖
318
+ ctypes 的 GlobalMemoryStatusEx,后者在部分环境下返回 INVALID_PARAMETER),
319
+ 否则 Linux 走 /proc、Windows 走 GlobalMemoryStatusEx。训练循环里周期性
320
+ 打点,available 逼近 0 就是在 swap 拷打 SSD 的前兆。"""
321
+ rss = avail = -1.0
322
+ try: # psutil:跨平台最稳(Windows/Linux 都是 C 扩展实现)
323
+ import psutil
324
+ rss = float(psutil.Process().memory_info().rss) / (1 << 20)
325
+ avail = float(psutil.virtual_memory().available) / (1 << 20)
326
+ return rss, avail
327
+ except Exception:
328
+ pass
329
+ try: # Linux RSS
330
+ with open("/proc/self/status") as f:
331
+ for line in f:
332
+ if line.startswith("VmRSS:"):
333
+ rss = int(line.split()[1]) / 1024
334
+ break
335
+ except OSError:
336
+ pass
337
+ try: # Linux available
338
+ with open("/proc/meminfo") as f:
339
+ for line in f:
340
+ if line.startswith("MemAvailable:"):
341
+ avail = int(line.split()[1]) / 1024
342
+ break
343
+ except OSError:
344
+ pass
345
+ if rss < 0 or avail < 0: # Windows / 兜底
346
+ try:
347
+ import ctypes
348
+
349
+ class _MS(ctypes.Structure):
350
+ # 完整 MEMORYSTATUSEX(缺 ullTotalVirtual/ullAvailVirtual 会让
351
+ # GlobalMemoryStatusEx 因 dwLength 校验失败返回 FALSE)
352
+ _fields_ = [
353
+ ("dwLength", ctypes.c_uint), ("dwMemoryLoad", ctypes.c_uint),
354
+ ("ullTotalPhys", ctypes.c_uint64), ("ullAvailPhys", ctypes.c_uint64),
355
+ ("ullTotalPageFile", ctypes.c_uint64), ("ullAvailPageFile", ctypes.c_uint64),
356
+ ("ullTotalVirtual", ctypes.c_uint64), ("ullAvailVirtual", ctypes.c_uint64),
357
+ ]
358
+
359
+ ms = _MS(dwLength=ctypes.sizeof(_MS))
360
+ k32 = ctypes.windll.kernel32
361
+ k32.GlobalMemoryStatusEx.argtypes = [ctypes.POINTER(_MS)]
362
+ k32.GlobalMemoryStatusEx.restype = ctypes.c_int
363
+ if k32.GlobalMemoryStatusEx(ctypes.byref(ms)):
364
+ if avail < 0:
365
+ avail = float(ms.ullAvailPhys) / (1 << 20)
366
+ if rss < 0: # Windows 无 /proc,用工作集近似
367
+ import ctypes.wintypes as wt
368
+
369
+ class _PMC(ctypes.Structure):
370
+ _fields_ = [("cb", wt.DWORD), ("PageFaultCount", wt.DWORD),
371
+ ("PeakWorkingSetSize", ctypes.c_size_t),
372
+ ("WorkingSetSize", ctypes.c_size_t)] + \
373
+ [(n, ctypes.c_size_t) for n in (
374
+ "QuotaPeakPagedPoolUsage", "QuotaPagedPoolUsage",
375
+ "QuotaPeakNonPagedPoolUsage", "QuotaNonPagedPoolUsage",
376
+ "PagefileUsage", "PeakPagefileUsage")]
377
+
378
+ pmc = _PMC(cb=ctypes.sizeof(_PMC))
379
+ psapi = ctypes.windll.psapi
380
+ # 显式签名:默认 c_int 会截断 64 位 HANDLE/指针,导致调用静默失败
381
+ psapi.GetProcessMemoryInfo.argtypes = [wt.HANDLE, ctypes.POINTER(_PMC), wt.DWORD]
382
+ psapi.GetProcessMemoryInfo.restype = wt.BOOL
383
+ h = ctypes.windll.kernel32.GetCurrentProcess()
384
+ if psapi.GetProcessMemoryInfo(h, ctypes.byref(pmc), pmc.cb):
385
+ rss = float(pmc.WorkingSetSize) / (1 << 20)
386
+ except Exception:
387
+ pass
388
+ return rss, avail
389
+
390
+
391
+ # ---------------------------------------------------------------------------
392
+ # 后台内存/swap 监视器(防 SSD 拷打)
393
+ # ---------------------------------------------------------------------------
394
+ _MON_STOP = threading.Event()
395
+
396
+
397
+ def start_mem_monitor(interval: float = 20.0) -> threading.Event:
398
+ """启动后台线程,每 ``interval`` 秒打印一次物理内存占用 + 提交量。
399
+
400
+ 提交量(commit)逼近物理内存即进入 swap(页面文件)——"拷打 SSD"的信号。
401
+ 返回一个 Event,调用 ``suspend_mem_monitor(ev)`` 停止。
402
+
403
+ 用法::
404
+
405
+ from torch_cpu_kit import start_mem_monitor, suspend_mem_monitor
406
+ _ev = start_mem_monitor(interval=30)
407
+ ... 训练循环 ...
408
+ suspend_mem_monitor(_ev)
409
+ """
410
+ _MON_STOP.clear()
411
+
412
+ def loop():
413
+ while not _MON_STOP.is_set():
414
+ try:
415
+ try: # psutil 优先(ctypes 的 GlobalMemoryStatusEx 部分环境失败)
416
+ import psutil
417
+ vm = psutil.virtual_memory()
418
+ tg, ug = vm.total / (1 << 30), (vm.total - vm.available) / (1 << 30)
419
+ sm = psutil.swap_memory()
420
+ cg = sm.used / (1 << 30) if sm.total > 0 else ug # 实际换页用量
421
+ flag = " <-- SWAP! 降低batch/序列或检查页面文件" if cg > tg * 0.97 else ""
422
+ print(f"[tck.mem] RAM {ug:5.2f}/{tg:.2f} GB swap {cg:6.2f} GB{flag}")
423
+ except Exception:
424
+ import ctypes
425
+
426
+ class _MS(ctypes.Structure):
427
+ # 完整 MEMORYSTATUSEX(缺字段 -> GlobalMemoryStatusEx 返回 FALSE)
428
+ _fields_ = [("l", ctypes.c_uint), ("m", ctypes.c_uint),
429
+ ("t", ctypes.c_uint64), ("a", ctypes.c_uint64),
430
+ ("tp", ctypes.c_uint64), ("ap", ctypes.c_uint64),
431
+ ("tv", ctypes.c_uint64), ("av", ctypes.c_uint64)]
432
+
433
+ ms = _MS(l=ctypes.sizeof(_MS))
434
+ k32 = ctypes.windll.kernel32
435
+ k32.GlobalMemoryStatusEx.argtypes = [ctypes.POINTER(_MS)]
436
+ k32.GlobalMemoryStatusEx.restype = ctypes.c_int
437
+ if k32.GlobalMemoryStatusEx(ctypes.byref(ms)):
438
+ tg, ug = ms.t / (1 << 30), (ms.t - ms.a) / (1 << 30)
439
+ cg = (ms.tp - ms.ap) / (1 << 30)
440
+ flag = " <-- SWAP! 降低batch/序列或检查页面文件" if cg > tg * 0.97 else ""
441
+ print(f"[tck.mem] RAM {ug:5.2f}/{tg:.2f} GB commit {cg:6.2f} GB{flag}")
442
+ except Exception:
443
+ pass
444
+ _MON_STOP.wait(interval)
445
+
446
+ t = threading.Thread(target=loop, daemon=True)
447
+ t.start()
448
+ return _MON_STOP
449
+
450
+
451
+ def suspend_mem_monitor(ev: Optional[threading.Event] = None) -> None:
452
+ """停止 ``start_mem_monitor`` 启动的监视线程。"""
453
+ (ev or _MON_STOP).set()
454
+
455
+
456
+ if __name__ == "__main__": # 自检:python torch_cpu_kit.py
457
+ apply_env(verbose=True)
458
+ import torch
459
+
460
+ init(verbose=True)
461
+ ds = torch.utils.data.TensorDataset(torch.randn(64, 8), torch.randn(64))
462
+ dl = fast_loader(ds, batch_size=16)
463
+ for xb, yb in dl:
464
+ pass
465
+ with autocast():
466
+ y = torch.nn.Linear(8, 8)(torch.randn(4, 8))
467
+ assert y.shape == (4, 8)
468
+ print("[torch_cpu_kit] selftest OK")