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.
- bitsandbytes/__init__.py +1 -1
- bitsandbytes/__main__.py +4 -4
- bitsandbytes/_cpu_caps.py +110 -0
- bitsandbytes/_ops.py +526 -526
- bitsandbytes/autograd/_functions.py +491 -491
- bitsandbytes/backends/cpu/ops.py +805 -805
- bitsandbytes/backends/cuda/ops.py +1212 -1212
- bitsandbytes/backends/default/ops.py +632 -632
- bitsandbytes/backends/hpu/ops.py +53 -53
- bitsandbytes/backends/mps/ops.py +277 -277
- bitsandbytes/backends/triton/kernels_4bit.py +583 -577
- bitsandbytes/backends/triton/kernels_8bit_quant.py +195 -195
- bitsandbytes/backends/triton/kernels_optim.py +1177 -1177
- bitsandbytes/backends/triton/ops.py +304 -304
- bitsandbytes/backends/utils.py +94 -94
- bitsandbytes/backends/xpu/ops.py +305 -305
- bitsandbytes/cextension.py +447 -405
- bitsandbytes/cli.py +50 -50
- bitsandbytes/consts.py +12 -12
- bitsandbytes/cuda_specs.py +111 -111
- bitsandbytes/detect_cpu.py +189 -189
- bitsandbytes/diagnostics/cuda.py +193 -193
- bitsandbytes/diagnostics/main.py +134 -134
- bitsandbytes/diagnostics/utils.py +12 -12
- bitsandbytes/disk_balancer.py +1276 -1224
- bitsandbytes/efst.py +777 -0
- bitsandbytes/functional.py +1836 -1836
- bitsandbytes/gdn_cpu.py +616 -602
- bitsandbytes/libbitsandbytes_cpu.dll +0 -0
- bitsandbytes/nn/__init__.py +19 -19
- bitsandbytes/nn/modules.py +1220 -1220
- bitsandbytes/nn/parametrize.py +206 -206
- bitsandbytes/optim/__init__.py +0 -4
- bitsandbytes/optim/adagrad.py +187 -187
- bitsandbytes/optim/adam.py +346 -346
- bitsandbytes/optim/adamw.py +337 -337
- bitsandbytes/optim/ademamix.py +410 -410
- bitsandbytes/optim/lamb.py +190 -190
- bitsandbytes/optim/lars.py +259 -259
- bitsandbytes/optim/lion.py +266 -266
- bitsandbytes/optim/optimizer.py +756 -756
- bitsandbytes/optim/rmsprop.py +170 -170
- bitsandbytes/optim/sgd.py +152 -152
- bitsandbytes/torch_cpu_kit.py +468 -468
- bitsandbytes/utils.py +208 -208
- bitsandbytes_cpu_cli.py +1012 -949
- {bitsandbytes_cpu_fork-0.50.2.dev2.dist-info → bitsandbytes_cpu_fork-0.50.2.dev4.dist-info}/METADATA +12 -4
- bitsandbytes_cpu_fork-0.50.2.dev4.dist-info/RECORD +64 -0
- {bitsandbytes_cpu_fork-0.50.2.dev2.dist-info → bitsandbytes_cpu_fork-0.50.2.dev4.dist-info}/licenses/LICENSE +21 -21
- bitsandbytes/optim/adamw4bit.py +0 -183
- bitsandbytes_cpu_fork-0.50.2.dev2.dist-info/RECORD +0 -63
- {bitsandbytes_cpu_fork-0.50.2.dev2.dist-info → bitsandbytes_cpu_fork-0.50.2.dev4.dist-info}/WHEEL +0 -0
- {bitsandbytes_cpu_fork-0.50.2.dev2.dist-info → bitsandbytes_cpu_fork-0.50.2.dev4.dist-info}/entry_points.txt +0 -0
- {bitsandbytes_cpu_fork-0.50.2.dev2.dist-info → bitsandbytes_cpu_fork-0.50.2.dev4.dist-info}/top_level.txt +0 -0
bitsandbytes/torch_cpu_kit.py
CHANGED
|
@@ -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")
|