bitsandbytes-cpu-fork 0.50.2.dev4__py3-none-win_amd64.whl → 0.50.2.dev5__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/backends/cpu/ops.py +19 -5
- bitsandbytes/disk_balancer.py +474 -63
- bitsandbytes/torch_cpu_kit.py +326 -0
- {bitsandbytes_cpu_fork-0.50.2.dev4.dist-info → bitsandbytes_cpu_fork-0.50.2.dev5.dist-info}/METADATA +3 -3
- {bitsandbytes_cpu_fork-0.50.2.dev4.dist-info → bitsandbytes_cpu_fork-0.50.2.dev5.dist-info}/RECORD +10 -10
- {bitsandbytes_cpu_fork-0.50.2.dev4.dist-info → bitsandbytes_cpu_fork-0.50.2.dev5.dist-info}/WHEEL +0 -0
- {bitsandbytes_cpu_fork-0.50.2.dev4.dist-info → bitsandbytes_cpu_fork-0.50.2.dev5.dist-info}/entry_points.txt +0 -0
- {bitsandbytes_cpu_fork-0.50.2.dev4.dist-info → bitsandbytes_cpu_fork-0.50.2.dev5.dist-info}/licenses/LICENSE +0 -0
- {bitsandbytes_cpu_fork-0.50.2.dev4.dist-info → bitsandbytes_cpu_fork-0.50.2.dev5.dist-info}/top_level.txt +0 -0
bitsandbytes/__init__.py
CHANGED
bitsandbytes/backends/cpu/ops.py
CHANGED
|
@@ -253,15 +253,29 @@ if not isinstance(lib, ErrorHandlerMockBNBNativeLibrary):
|
|
|
253
253
|
if has_avx512bf16():
|
|
254
254
|
gemm_4bit_forward_kernel = None
|
|
255
255
|
try:
|
|
256
|
-
|
|
256
|
+
import importlib.util
|
|
257
|
+
|
|
258
|
+
if importlib.util.find_spec("kernels") is None:
|
|
259
|
+
# `kernels` is an OPTIONAL HuggingFace hub-kernel package. The fused native
|
|
260
|
+
# CPU kernel below is the default and needs no extra dependency, so a
|
|
261
|
+
# missing optional package is not a warning -- otherwise every CPU-only
|
|
262
|
+
# `import bitsandbytes` printed a scary line on AVX512-BF16 machines.
|
|
263
|
+
logger.debug(
|
|
264
|
+
"optional package 'kernels' not installed; using the native CPU "
|
|
265
|
+
"gemv_4bit kernel"
|
|
266
|
+
)
|
|
267
|
+
else:
|
|
268
|
+
from kernels import get_kernel
|
|
257
269
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
270
|
+
gemm_4bit_forward_kernel = get_kernel(
|
|
271
|
+
"kernels-community/quantization-bitsandbytes", version=1
|
|
272
|
+
).gemm_4bit_forward
|
|
261
273
|
except Exception as exc: # pragma: no cover - best effort fallback
|
|
262
274
|
gemm_4bit_forward_kernel = None
|
|
263
275
|
logger.warning(
|
|
264
|
-
"Failed to load CPU gemm_4bit_forward from kernels-community: %s.
|
|
276
|
+
"Failed to load CPU gemm_4bit_forward from kernels-community: %s. "
|
|
277
|
+
"The optional 'kernels' package is installed but the hub kernel could not "
|
|
278
|
+
"be loaded; falling back to the native CPU kernel.",
|
|
265
279
|
exc,
|
|
266
280
|
)
|
|
267
281
|
|
bitsandbytes/disk_balancer.py
CHANGED
|
@@ -9,6 +9,14 @@ Windows 上也会直接导入失败。已确认这段 docstring 内除这一处
|
|
|
9
9
|
也没有 `\u`/`\x` 这类加了 r 会改变含义的转义,所以加 r 前缀不改变任何现有语义。
|
|
10
10
|
(是 Android/ARM64 上跑测试时暴露的: 本机 Python 3.11 只给警告,3.14 明确提示。)
|
|
11
11
|
|
|
12
|
+
支持平台(2026-10 起):
|
|
13
|
+
- **Windows**:盘符检测(ctypes,无需额外依赖),缓存默认放在脚本同目录 `.flash_cache/`;
|
|
14
|
+
- **Linux**:psutil + /proc/mounts,缓存默认放 `$XDG_CACHE_HOME/bnb_flash_cache`(或
|
|
15
|
+
`~/.cache/bnb_flash_cache`),按 `/sys/block/*/queue/rotational` 识别 SSD/HDD;
|
|
16
|
+
- **WSL**:**支持**。只使用 WSL 发行版自己的 ext4(通常是 `/`),9P 宿主盘
|
|
17
|
+
(`/mnt/c`、`/mnt/d` …)、网络盘、只读挂载和 tmpfs/ramfs 一律拒绝(9P 上高负载
|
|
18
|
+
卸载曾扰乱宿主 MFT)。显式 `--flash_paths` 配到这些位置会直接报错而不是静默忽略。
|
|
19
|
+
|
|
12
20
|
目标:
|
|
13
21
|
- 对抗 Windows 虚拟内存在 AI 训练时频繁擦写导致硬盘活动时间 100% 的问题;
|
|
14
22
|
- 异步写盘线程(queue.Queue),不阻塞训练循环;
|
|
@@ -64,7 +72,9 @@ from __future__ import annotations
|
|
|
64
72
|
import ctypes
|
|
65
73
|
import mmap
|
|
66
74
|
import os
|
|
75
|
+
import posixpath
|
|
67
76
|
import queue
|
|
77
|
+
import re
|
|
68
78
|
import shutil
|
|
69
79
|
import threading
|
|
70
80
|
import time
|
|
@@ -234,25 +244,34 @@ def detect_disks() -> List[DiskInfo]:
|
|
|
234
244
|
跨平台:Windows 用 ctypes(GetDriveTypeW),Linux/WSL 用 psutil.disk_partitions。
|
|
235
245
|
"""
|
|
236
246
|
disks: List[DiskInfo] = []
|
|
237
|
-
if os.name != "nt": # Linux / WSL / macOS:psutil
|
|
247
|
+
if os.name != "nt": # Linux / WSL / macOS:psutil 枚举 + /proc/mounts 安全判定
|
|
238
248
|
import psutil as _ps
|
|
249
|
+
seen = set()
|
|
239
250
|
for part in _ps.disk_partitions(all=False):
|
|
240
|
-
#
|
|
241
|
-
# 系统语义)在高负载下对 NTFS 做冷参数卸载会扰乱宿主 MFT(已实测炸机)。
|
|
242
|
-
# 必须排除 9p/fuse/宿主 NTFS 挂载,绝不对它们卸载冷参数。
|
|
251
|
+
# 先排除 WSL 宿主 NTFS / 9P / 网络挂载(历史炸机点)
|
|
243
252
|
if _is_wsl_host_mount(part):
|
|
244
253
|
continue
|
|
254
|
+
mount = part.mountpoint
|
|
255
|
+
norm = os.path.normpath(mount)
|
|
256
|
+
if norm in seen:
|
|
257
|
+
continue
|
|
258
|
+
info = _mount_for_path(mount)
|
|
259
|
+
if info is None:
|
|
260
|
+
continue
|
|
261
|
+
mnt, fs, src, opts = info
|
|
262
|
+
ok, _reason = _fs_safety(fs, mnt, opts)
|
|
263
|
+
if not ok:
|
|
264
|
+
continue # 9P / ntfs / 网络 / 只读 / 伪文件系统 / 内存盘
|
|
245
265
|
try:
|
|
246
|
-
usg = _ps.disk_usage(
|
|
266
|
+
usg = _ps.disk_usage(mount)
|
|
247
267
|
except Exception:
|
|
248
268
|
continue
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
drive_type = "ssd" if _fs_ssd_like(part.fstype) else "hdd"
|
|
269
|
+
if usg.total <= 0 or usg.free <= 0:
|
|
270
|
+
continue
|
|
271
|
+
seen.add(norm)
|
|
253
272
|
disks.append(DiskInfo(
|
|
254
273
|
mount=mount,
|
|
255
|
-
drive_type=
|
|
274
|
+
drive_type=_linux_drive_type(src or part.device),
|
|
256
275
|
total_bytes=usg.total,
|
|
257
276
|
free_bytes=usg.free,
|
|
258
277
|
))
|
|
@@ -348,6 +367,185 @@ def _in_wsl() -> bool:
|
|
|
348
367
|
return False
|
|
349
368
|
|
|
350
369
|
|
|
370
|
+
# ---------------------------------------------------------------------------
|
|
371
|
+
# Linux / WSL 文件系统安全(WSL 支持的核心)
|
|
372
|
+
# ---------------------------------------------------------------------------
|
|
373
|
+
# 9P / 网络 / 宿主盘:绝不允许做冷参数缓存(WSL 的 /mnt/c、/mnt/d 属于这一类,历史上
|
|
374
|
+
# 在高负载下扰乱过宿主 MFT)。pseudo/RAM 文件系统则没有持久化/卸载意义。
|
|
375
|
+
_UNSAFE_FS_PREFIXES = (
|
|
376
|
+
"9p", "9pfs", "drvfs", "fuseblk", "fuse.", "ntfs", "ntfs3", "cifs", "smb",
|
|
377
|
+
"nfs", "nfs4", "vfat", "exfat", "iso9660", "udf", "sshfs", "ceph", "glusterfs",
|
|
378
|
+
"afs", "lustre", "beegfs",
|
|
379
|
+
)
|
|
380
|
+
_PSEUDO_FS = frozenset((
|
|
381
|
+
"proc", "sysfs", "devtmpfs", "devpts", "cgroup", "cgroup2", "binfmt_misc",
|
|
382
|
+
"securityfs", "debugfs", "tracefs", "pstore", "configfs", "mqueue", "hugetlbfs",
|
|
383
|
+
"autofs", "efivarfs", "nsfs", "tracefs",
|
|
384
|
+
))
|
|
385
|
+
_RAM_FS = frozenset(("tmpfs", "ramfs"))
|
|
386
|
+
_WSL_HOST_MOUNT_RE = re.compile(r"^/mnt/[A-Za-z](/|$)")
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _read_mounts(mounts_path: str = "/proc/mounts") -> List[Tuple[str, str, str, str]]:
|
|
390
|
+
"""[(mountpoint, fstype, source, opts)],按 mountpoint 长度降序(最长前缀优先)。
|
|
391
|
+
|
|
392
|
+
Linux/WSL 上用 /proc/mounts 而不是只信 psutil:psutil 在 WSL 里可能看不到 9P 宿主盘,
|
|
393
|
+
而我们恰恰需要识别并拒绝它们。参数化路径是为了单测能喂假 mount 表。
|
|
394
|
+
"""
|
|
395
|
+
rows: List[Tuple[str, str, str, str]] = []
|
|
396
|
+
try:
|
|
397
|
+
with open(mounts_path, "r", encoding="utf-8", errors="replace") as fh:
|
|
398
|
+
for line in fh:
|
|
399
|
+
parts = line.split()
|
|
400
|
+
if len(parts) < 4:
|
|
401
|
+
continue
|
|
402
|
+
src, mnt, fs, opts = parts[0], parts[1], parts[2], parts[3]
|
|
403
|
+
mnt = mnt.replace("\\040", " ").replace("\\011", "\t")
|
|
404
|
+
rows.append((mnt, fs.lower(), src, opts))
|
|
405
|
+
except OSError:
|
|
406
|
+
return []
|
|
407
|
+
rows.sort(key=lambda r: len(r[0]), reverse=True)
|
|
408
|
+
return rows
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _norm_mount_path(path: str) -> str:
|
|
412
|
+
"""把路径规范成用于 /proc/mounts 前缀比较的形式。
|
|
413
|
+
|
|
414
|
+
在 Linux 上就是 abspath;在 Windows 上跑 Linux mount 表单测时,os.path.abspath("/mnt/x")
|
|
415
|
+
会变成 "D:\\mnt\\x",所以纯 POSIX 路径改用 posixpath.normpath。
|
|
416
|
+
"""
|
|
417
|
+
s = str(path)
|
|
418
|
+
if s.startswith("/") and os.name == "nt":
|
|
419
|
+
return posixpath.normpath(s)
|
|
420
|
+
return os.path.abspath(os.path.expanduser(s))
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _mount_for_path(path: str, mounts_path: str = "/proc/mounts"):
|
|
424
|
+
"""路径所在挂载行 (mountpoint, fstype, source, opts);解析不到返回 None。"""
|
|
425
|
+
try:
|
|
426
|
+
ap = _norm_mount_path(path)
|
|
427
|
+
except Exception:
|
|
428
|
+
return None
|
|
429
|
+
for row in _read_mounts(mounts_path):
|
|
430
|
+
mnt = row[0]
|
|
431
|
+
if ap == mnt or ap.startswith(mnt.rstrip("/") + "/"):
|
|
432
|
+
return row
|
|
433
|
+
return None
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _fs_safety(fs: str, mount: str, opts: str = "", is_wsl: Optional[bool] = None):
|
|
437
|
+
"""(ok, reason):该挂载能否承载冷参数缓存。"""
|
|
438
|
+
if is_wsl is None:
|
|
439
|
+
is_wsl = _in_wsl()
|
|
440
|
+
if fs.startswith(_UNSAFE_FS_PREFIXES):
|
|
441
|
+
return False, f"{fs} 是 9P/网络/宿主盘文件系统(挂载 {mount})"
|
|
442
|
+
if fs in _PSEUDO_FS:
|
|
443
|
+
return False, f"{fs} 是伪文件系统(挂载 {mount})"
|
|
444
|
+
if fs in _RAM_FS:
|
|
445
|
+
return False, f"{fs} 是内存盘,卸载到它没有意义(挂载 {mount})"
|
|
446
|
+
if "ro" in opts.split(","):
|
|
447
|
+
return False, f"挂载 {mount} 是只读的"
|
|
448
|
+
if is_wsl and (_WSL_HOST_MOUNT_RE.match(mount) or mount.startswith("/mnt/wsl")):
|
|
449
|
+
return False, f"{mount} 是 WSL 宿主/虚拟挂载,不能写冷参数"
|
|
450
|
+
return True, ""
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _cache_dir_verdict(path: str, mounts_path: str = "/proc/mounts"):
|
|
454
|
+
"""(ok, reason):路径能否作为 disk_balancer 的缓存目录。
|
|
455
|
+
|
|
456
|
+
比 _fs_safety 多两条:不能直接是 "/"(cleanup 会 rmtree 它),且不能落在 WSL 宿主盘。
|
|
457
|
+
"""
|
|
458
|
+
try:
|
|
459
|
+
ap = _norm_mount_path(path)
|
|
460
|
+
except Exception:
|
|
461
|
+
return False, "路径无法解析"
|
|
462
|
+
if not ap.rstrip("/") or ap == os.sep:
|
|
463
|
+
return False, "缓存目录不能直接是根目录 /"
|
|
464
|
+
info = _mount_for_path(ap, mounts_path)
|
|
465
|
+
if info is None:
|
|
466
|
+
return False, "无法解析该路径的挂载信息(需要 Linux /proc/mounts)"
|
|
467
|
+
mnt, fs, _src, opts = info
|
|
468
|
+
ok, reason = _fs_safety(fs, mnt, opts)
|
|
469
|
+
if not ok:
|
|
470
|
+
return False, reason
|
|
471
|
+
if _in_wsl() and _WSL_HOST_MOUNT_RE.match(ap):
|
|
472
|
+
return False, f"{ap} 在 WSL 宿主盘 /mnt/[盘符] 下"
|
|
473
|
+
return True, f"{fs} @ {mnt}"
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _cache_name_for_mount(mount: str) -> str:
|
|
477
|
+
"""把挂载点变成安全的目录名:"/" -> "root","/mnt/data" -> "mnt_data"。"""
|
|
478
|
+
name = str(mount).strip("/").replace("/", "_").replace(":", "").replace("\\", "_")
|
|
479
|
+
return name or "root"
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _default_cache_base() -> str:
|
|
483
|
+
"""Linux 默认缓存根:$XDG_CACHE_HOME/bnb_flash_cache 或 ~/.cache/bnb_flash_cache。
|
|
484
|
+
|
|
485
|
+
不用脚本所在目录做默认:WSL 里脚本常在 /mnt/d(9P),那是必须拒绝的位置。
|
|
486
|
+
"""
|
|
487
|
+
xdg = os.environ.get("XDG_CACHE_HOME")
|
|
488
|
+
if not xdg:
|
|
489
|
+
xdg = os.path.join(os.path.expanduser("~"), ".cache")
|
|
490
|
+
return os.path.join(xdg, "bnb_flash_cache")
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _linux_drive_type(source: str) -> str:
|
|
494
|
+
"""按 /sys/block/<dev>/queue/rotational 判断 SSD/HDD/NVMe(Linux 真实介质类型)。"""
|
|
495
|
+
name = os.path.basename(str(source or "").strip())
|
|
496
|
+
if not name:
|
|
497
|
+
return "ssd"
|
|
498
|
+
if name.startswith("nvme"):
|
|
499
|
+
return "nvme"
|
|
500
|
+
try:
|
|
501
|
+
with open(f"/sys/class/block/{name}/queue/rotational", "r", encoding="utf-8") as fh:
|
|
502
|
+
rotational = fh.read().strip() == "1"
|
|
503
|
+
except OSError:
|
|
504
|
+
return "ssd"
|
|
505
|
+
if rotational and _in_wsl():
|
|
506
|
+
# WSL 的虚拟盘(Hyper-V VHDX)永远报告 rotational=1,但宿主一般是 SSD;WSL 里
|
|
507
|
+
# 也只有一个可用路径,分错层没有意义,按 SSD 标更符合实际。
|
|
508
|
+
return "ssd"
|
|
509
|
+
return "hdd" if rotational else "ssd"
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _unix_cache_candidates(mount: str, cfg_paths: List[str]) -> List[str]:
|
|
513
|
+
"""按优先级给出某个挂载点上的缓存目录候选(都会再过 _cache_dir_verdict 检查)。"""
|
|
514
|
+
cands: List[str] = []
|
|
515
|
+
for cp in cfg_paths or []:
|
|
516
|
+
info = _mount_for_path(cp)
|
|
517
|
+
if info is not None and os.path.normpath(info[0]) == os.path.normpath(mount):
|
|
518
|
+
cands.append(os.path.join(os.path.abspath(os.path.expanduser(cp)),
|
|
519
|
+
".bnb_flash_cache"))
|
|
520
|
+
if os.path.normpath(mount) != os.sep:
|
|
521
|
+
cands.append(os.path.join(mount, ".bnb_flash_cache"))
|
|
522
|
+
cands.append(os.path.join(_default_cache_base(), _cache_name_for_mount(mount)))
|
|
523
|
+
return cands
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _writable_dir(path: str) -> bool:
|
|
527
|
+
try:
|
|
528
|
+
os.makedirs(path, exist_ok=True)
|
|
529
|
+
probe = os.path.join(path, f".wtest_{os.getpid()}")
|
|
530
|
+
with open(probe, "wb") as fh:
|
|
531
|
+
fh.write(b"x")
|
|
532
|
+
os.remove(probe)
|
|
533
|
+
return True
|
|
534
|
+
except OSError:
|
|
535
|
+
return False
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _pick_unix_cache_dir(mount: str, cfg_paths: List[str]) -> Optional[str]:
|
|
539
|
+
"""在某个挂载点上选一个安全且可写的缓存目录;没有就返回 None。"""
|
|
540
|
+
for cand in _unix_cache_candidates(mount, cfg_paths):
|
|
541
|
+
ok, reason = _cache_dir_verdict(cand)
|
|
542
|
+
if not ok:
|
|
543
|
+
continue
|
|
544
|
+
if _writable_dir(cand):
|
|
545
|
+
return cand
|
|
546
|
+
return None
|
|
547
|
+
|
|
548
|
+
|
|
351
549
|
def _fs_ssd_like(fstype: str) -> bool:
|
|
352
550
|
"""粗略判断文件系统/挂载点是否更像 SSD(Linux 无设备级 SSD 检测,用 psutil
|
|
353
551
|
disk_io_counters 无法区分 ssd/hdd,这里用 fstype 兜底:NVMe/常见 SSD 挂载)。
|
|
@@ -408,6 +606,10 @@ def get_disk_activity(disk: DiskInfo) -> float:
|
|
|
408
606
|
"""获取磁盘当前活动率(估算值,0.0~1.0)。
|
|
409
607
|
|
|
410
608
|
通过两次采样间的 I/O 增量估算。
|
|
609
|
+
|
|
610
|
+
Linux 上还有两个可选环境变量:``BNB_FLASH_FADV_DONTNEED=1`` 让写完后尽早释放页缓存
|
|
611
|
+
(内存极紧时有用、下次读会慢);``BNB_FLASH_FSYNC=1`` 等价于
|
|
612
|
+
``DiskBalancerConfig(fsync=True)`` 的默认值(每个缓存文件落盘后 fsync)。
|
|
411
613
|
"""
|
|
412
614
|
r, w, now = _get_disk_io(disk)
|
|
413
615
|
if disk._io_counters is None:
|
|
@@ -437,9 +639,13 @@ class DiskBalancerConfig:
|
|
|
437
639
|
|
|
438
640
|
Attributes:
|
|
439
641
|
mode: "auto" 自动检测磁盘并分配;"manual" 手动指定。
|
|
440
|
-
sizes:
|
|
441
|
-
|
|
442
|
-
|
|
642
|
+
sizes: 手动模式下每盘缓存大小(MB)。Windows 用盘符 ``{"C:": 2048}``;
|
|
643
|
+
Linux/WSL 用挂载点 ``{"/": 2048, "/mnt/data": 4096}``。
|
|
644
|
+
paths: 指定缓存路径列表,如 Windows ``["D:\\cache", "E:\\cache"]``,
|
|
645
|
+
Linux/WSL ``["/home/user/fastcache"]``(会落在其下 `.bnb_flash_cache/`)。
|
|
646
|
+
不指定则 Windows 默认在脚本所在目录 `.flash_cache/`,Linux/WSL 默认在
|
|
647
|
+
``$XDG_CACHE_HOME/bnb_flash_cache``(或 ``~/.cache/bnb_flash_cache``)。
|
|
648
|
+
Linux/WSL 上 9P/网络/只读/内存盘路径会被拒绝并报错。
|
|
443
649
|
speed_first: True=速度优先(不管硬盘寿命),False=保护硬盘优先。
|
|
444
650
|
keep_cache: True=训练结束后保留缓存文件,False=默认删除。
|
|
445
651
|
memory_threshold: 内存使用率阈值(0.0~1.0),超过此值触发冷参数卸载。
|
|
@@ -467,6 +673,12 @@ class DiskBalancerConfig:
|
|
|
467
673
|
min_free_ratio: float = 0.05
|
|
468
674
|
offload_prefix: str = "" # 只卸载匹配前缀的冷参数(空=全部),如 "_cold_"
|
|
469
675
|
offload_per_step: int = 0 # 每次 update_step 最多迁移几个(0=不限)
|
|
676
|
+
# 零拷贝写盘队列里每个待写参数都会继续占用源张量内存直到写完。不设上限时,
|
|
677
|
+
# 训练线程可以一次排进几十个大参数(几百 MB 内存没真正释放)。下面两个旋钮把
|
|
678
|
+
# "在飞字节"限制住,写盘线程消费后自动放行。
|
|
679
|
+
max_inflight_bytes: int = 0 # 0=自动(max(256MB, min(1GB, 5% 物理内存)))
|
|
680
|
+
write_queue_depth: int = 0 # 0=不限(内存由 max_inflight_bytes 兜底)
|
|
681
|
+
fsync: bool = False # True=每个缓存文件落盘后 fsync(更安全、更慢)
|
|
470
682
|
|
|
471
683
|
def __post_init__(self):
|
|
472
684
|
"""校验关键配置,避免非法值导致静默禁用 / 负缓存 / None 崩。"""
|
|
@@ -489,6 +701,12 @@ class DiskBalancerConfig:
|
|
|
489
701
|
if self.throttle_threshold is None or not (0.0 <= self.throttle_threshold <= 1.0):
|
|
490
702
|
raise ValueError(
|
|
491
703
|
f"throttle_threshold 必须在 [0,1] 内,得到 {self.throttle_threshold}")
|
|
704
|
+
if self.max_inflight_bytes is None or self.max_inflight_bytes < 0:
|
|
705
|
+
raise ValueError(
|
|
706
|
+
f"max_inflight_bytes 必须是非负整数(0=自动),得到 {self.max_inflight_bytes}")
|
|
707
|
+
if self.write_queue_depth is None or self.write_queue_depth < 0:
|
|
708
|
+
raise ValueError(
|
|
709
|
+
f"write_queue_depth 必须是非负整数(0=不限),得到 {self.write_queue_depth}")
|
|
492
710
|
|
|
493
711
|
|
|
494
712
|
# ---------------------------------------------------------------------------
|
|
@@ -511,6 +729,9 @@ class DiskLoadBalancer:
|
|
|
511
729
|
- 检测所有磁盘,按 SSD→HDD 排序路径列表
|
|
512
730
|
- 按容量自动分配缓存配额(保留 min_free_ratio 安全空间)
|
|
513
731
|
- 单一磁盘时:优先不拖慢训练速度,其次是保护硬盘
|
|
732
|
+
- Linux/WSL:只选本机磁盘文件系统;没有任何安全位置时 `start()` 直接报错并说明原因
|
|
733
|
+
- 零拷贝写盘队列有在飞字节上限(`max_inflight_bytes`),超过时本次卸载放弃、
|
|
734
|
+
参数留在内存,避免"写盘没完成、内存也没真正释放"的假卸载
|
|
514
735
|
|
|
515
736
|
用法::
|
|
516
737
|
|
|
@@ -529,19 +750,24 @@ class DiskLoadBalancer:
|
|
|
529
750
|
|
|
530
751
|
def __init__(self, config: Optional[DiskBalancerConfig] = None):
|
|
531
752
|
self._cfg = config or DiskBalancerConfig()
|
|
532
|
-
#
|
|
533
|
-
# 9P
|
|
534
|
-
|
|
535
|
-
self.
|
|
753
|
+
# WSL 不再整体禁用:WSL 发行版自己的 ext4(/dev/sdX 挂 /)是安全的本机磁盘;
|
|
754
|
+
# 只拒绝 9P 宿主盘(/mnt/c、/mnt/d 等)。_disabled_reason 记录最终为什么没跑。
|
|
755
|
+
self._wsl = _in_wsl()
|
|
756
|
+
self._disabled_reason = ""
|
|
536
757
|
self._disks: List[DiskInfo] = []
|
|
537
758
|
self._paths: List[str] = [] # 排序后的缓存路径(SSD→HDD)
|
|
538
|
-
self._write_queue: queue.Queue = queue.Queue(
|
|
759
|
+
self._write_queue: queue.Queue = queue.Queue(
|
|
760
|
+
maxsize=self._cfg.write_queue_depth or 0)
|
|
539
761
|
self._writer_thread: Optional[threading.Thread] = None
|
|
540
762
|
self._model: Optional[torch.nn.Module] = None
|
|
541
763
|
self._cold_index: Dict[str, Tuple[str, int, torch.dtype]] = {} # name -> (path, numel, dtype)
|
|
542
764
|
self._migrated_count: int = 0
|
|
543
765
|
self._started = False
|
|
544
766
|
self._lock = threading.Lock()
|
|
767
|
+
self._inflight_bytes = 0 # 已入队、未写完的源张量字节数
|
|
768
|
+
self._inflight_cv = threading.Condition(self._lock)
|
|
769
|
+
self._max_inflight_bytes = 0 # start() 里算
|
|
770
|
+
self._write_bytes_total = 0 # 累计入队字节数(统计)
|
|
545
771
|
self._script_dir: str = ""
|
|
546
772
|
self._ssd_paths: List[str] = []
|
|
547
773
|
self._hdd_paths: List[str] = []
|
|
@@ -566,21 +792,28 @@ class DiskLoadBalancer:
|
|
|
566
792
|
def start(self, script_dir: Optional[str] = None) -> "DiskLoadBalancer":
|
|
567
793
|
"""初始化磁盘、创建缓存目录、启动异步写盘线程。
|
|
568
794
|
|
|
795
|
+
在 WSL / Linux 上只会选择本机磁盘文件系统(ext4/xfs/btrfs/...);9P 宿主盘
|
|
796
|
+
(/mnt/c、/mnt/d)、网络盘、只读挂载和内存盘会被拒绝。没有任何安全位置时
|
|
797
|
+
本次实例自动禁用(不写任何文件),并在日志里说明原因。
|
|
798
|
+
|
|
569
799
|
Args:
|
|
570
|
-
script_dir:
|
|
800
|
+
script_dir: 训练脚本所在目录(Windows 默认缓存位置;Linux 用 ~/.cache)。
|
|
571
801
|
"""
|
|
572
|
-
if getattr(self, "_9p_skip", False):
|
|
573
|
-
print("[disk_balancer] 检测到 WSL 9P/宿主 NTFS 挂载,均衡负载已整体禁用——"
|
|
574
|
-
"为避免 9P 写宿主 NTFS 扰乱 MFT(会炸机),本次不卸载任何冷参数"
|
|
575
|
-
"(用户 --flash 参数已忽略)。建议在纯 Linux 分区使用才对磁盘做均衡。")
|
|
576
|
-
return self
|
|
577
|
-
|
|
578
802
|
if self._started:
|
|
579
803
|
return self
|
|
580
804
|
|
|
581
805
|
self._script_dir = script_dir or os.path.dirname(os.path.abspath(
|
|
582
806
|
__import__("sys").argv[0] if __import__("sys").argv else __file__))
|
|
583
807
|
|
|
808
|
+
# 用户显式 --flash_paths 必须安全:Linux/WSL 上如果配到 9P 宿主盘/网络盘/根目录,
|
|
809
|
+
# 直接报错而不是悄悄忽略(否则用户以为缓存落在指定位置,实际没有)。
|
|
810
|
+
if os.name != "nt" and self._cfg.paths:
|
|
811
|
+
for cp in self._cfg.paths:
|
|
812
|
+
ok, reason = _cache_dir_verdict(cp)
|
|
813
|
+
if not ok:
|
|
814
|
+
raise ValueError(
|
|
815
|
+
f"disk_balancer: --flash_paths 中的 {cp!r} 不安全: {reason}。"
|
|
816
|
+
f"请换成 ext4/xfs/btrfs 等本机磁盘上的目录(例如 $HOME 下)")
|
|
584
817
|
if self._cfg.mode == "auto":
|
|
585
818
|
self._start_auto()
|
|
586
819
|
elif self._cfg.mode == "manual":
|
|
@@ -588,6 +821,27 @@ class DiskLoadBalancer:
|
|
|
588
821
|
else:
|
|
589
822
|
raise ValueError(f"Unknown mode: {self._cfg.mode}")
|
|
590
823
|
|
|
824
|
+
# 在任何写操作之前,把每个缓存目录再验一遍(防配置绕过;Linux 上同时保证
|
|
825
|
+
# 绝不会是 "/" 或 9P 宿主盘)。Windows 没有 /proc/mounts,跳过该检查。
|
|
826
|
+
if os.name != "nt":
|
|
827
|
+
for p in self._paths:
|
|
828
|
+
ok, reason = _cache_dir_verdict(p)
|
|
829
|
+
if not ok:
|
|
830
|
+
raise RuntimeError(
|
|
831
|
+
f"disk_balancer: 拒绝不安全缓存目录 {p}: {reason}。"
|
|
832
|
+
f"请改用本机 Linux 文件系统上的路径(例如 $HOME)。")
|
|
833
|
+
|
|
834
|
+
# 在飞字节上限:零拷贝队列持有源张量内存,必须给训练线程一个背压。
|
|
835
|
+
if self._cfg.max_inflight_bytes:
|
|
836
|
+
self._max_inflight_bytes = int(self._cfg.max_inflight_bytes)
|
|
837
|
+
else:
|
|
838
|
+
try:
|
|
839
|
+
import psutil as _ps
|
|
840
|
+
total = int(_ps.virtual_memory().total)
|
|
841
|
+
self._max_inflight_bytes = max(256 << 20, min(1 << 30, total // 20))
|
|
842
|
+
except Exception:
|
|
843
|
+
self._max_inflight_bytes = 512 << 20
|
|
844
|
+
|
|
591
845
|
# Only directories we actually create here are ever removed by cleanup().
|
|
592
846
|
self._owned_cache_dirs = []
|
|
593
847
|
for p in self._paths:
|
|
@@ -617,7 +871,31 @@ class DiskLoadBalancer:
|
|
|
617
871
|
def _start_auto(self):
|
|
618
872
|
all_disks = detect_disks()
|
|
619
873
|
if not all_disks:
|
|
620
|
-
|
|
874
|
+
hint = ("(WSL:只检测到 9P 宿主盘?把缓存目录放到 WSL 自己的 ext4 上,"
|
|
875
|
+
"或确保 $HOME 可写)" if self._wsl else "")
|
|
876
|
+
raise RuntimeError(f"disk_balancer: 未检测到任何可用磁盘{hint}")
|
|
877
|
+
|
|
878
|
+
if os.name != "nt":
|
|
879
|
+
kept: List[DiskInfo] = []
|
|
880
|
+
for d in all_disks:
|
|
881
|
+
usable = int(d.free_bytes * (1.0 - self._cfg.min_free_ratio))
|
|
882
|
+
d.max_cache_bytes = min(usable, 4 * (1 << 30))
|
|
883
|
+
cache_dir = _pick_unix_cache_dir(d.mount, self._cfg.paths)
|
|
884
|
+
if cache_dir is None:
|
|
885
|
+
continue
|
|
886
|
+
d.cache_dir = cache_dir
|
|
887
|
+
if d.drive_type in ("ssd", "nvme"):
|
|
888
|
+
self._ssd_paths.append(cache_dir)
|
|
889
|
+
else:
|
|
890
|
+
self._hdd_paths.append(cache_dir)
|
|
891
|
+
kept.append(d)
|
|
892
|
+
if not kept:
|
|
893
|
+
raise RuntimeError(
|
|
894
|
+
"disk_balancer: 没有安全的 Linux 缓存目录可用"
|
|
895
|
+
"(9P/网络/只读/内存盘都不能用;请把缓存放到 ext4/xfs/btrfs 等本机磁盘)")
|
|
896
|
+
self._disks = kept
|
|
897
|
+
self._paths = self._ssd_paths + self._hdd_paths
|
|
898
|
+
return
|
|
621
899
|
|
|
622
900
|
# 按 SSD→HDD 排序路径
|
|
623
901
|
for d in all_disks:
|
|
@@ -650,6 +928,33 @@ class DiskLoadBalancer:
|
|
|
650
928
|
all_disks = detect_disks()
|
|
651
929
|
disk_map = {d.mount: d for d in all_disks}
|
|
652
930
|
|
|
931
|
+
if os.name != "nt":
|
|
932
|
+
# Linux:sizes 的 key 是挂载点路径("/"、"/mnt/data"),不是盘符。
|
|
933
|
+
norm_map = {os.path.normpath(k): v for k, v in disk_map.items()}
|
|
934
|
+
for mount_key, size_mb in self._cfg.sizes.items():
|
|
935
|
+
m = os.path.normpath(str(mount_key))
|
|
936
|
+
if m not in norm_map:
|
|
937
|
+
raise ValueError(
|
|
938
|
+
f"disk_balancer: 挂载点 {mount_key!r} 不在可用磁盘列表中: "
|
|
939
|
+
f"{sorted(norm_map)}")
|
|
940
|
+
d = norm_map[m]
|
|
941
|
+
usable = int(d.free_bytes * (1.0 - self._cfg.min_free_ratio))
|
|
942
|
+
d.max_cache_bytes = min(int(size_mb) << 20, usable)
|
|
943
|
+
cache_dir = _pick_unix_cache_dir(d.mount, self._cfg.paths)
|
|
944
|
+
if cache_dir is None:
|
|
945
|
+
raise RuntimeError(
|
|
946
|
+
f"disk_balancer: 挂载点 {d.mount} 上没有安全可写的缓存目录")
|
|
947
|
+
d.cache_dir = cache_dir
|
|
948
|
+
if d.drive_type in ("ssd", "nvme"):
|
|
949
|
+
self._ssd_paths.append(cache_dir)
|
|
950
|
+
else:
|
|
951
|
+
self._hdd_paths.append(cache_dir)
|
|
952
|
+
self._disks.append(d)
|
|
953
|
+
if not self._disks:
|
|
954
|
+
raise RuntimeError("disk_balancer: manual 模式下没有有效的磁盘配置")
|
|
955
|
+
self._paths = self._ssd_paths + self._hdd_paths
|
|
956
|
+
return
|
|
957
|
+
|
|
653
958
|
for mount, size_mb in self._cfg.sizes.items():
|
|
654
959
|
m = mount.upper()
|
|
655
960
|
if m not in disk_map:
|
|
@@ -693,8 +998,13 @@ class DiskLoadBalancer:
|
|
|
693
998
|
m = "SSD" if d.drive_type in ("ssd", "nvme") else "HDD"
|
|
694
999
|
lines.append(f" [{d.mount}] {m} 缓存 {d.max_cache_gb:.1f}GB @ {d.cache_dir}")
|
|
695
1000
|
lines.append(f" 路径优先级: {self._paths}")
|
|
1001
|
+
if self._max_inflight_bytes:
|
|
1002
|
+
lines.append(f" 在飞写盘上限: {self._max_inflight_bytes / (1 << 20):.0f} MB"
|
|
1003
|
+
f"(超过则本次卸载放弃,参数留在内存)")
|
|
696
1004
|
if self._cfg.speed_first:
|
|
697
1005
|
lines.append(" ⚠ 速度优先模式:不保护硬盘,不节流")
|
|
1006
|
+
if self._wsl:
|
|
1007
|
+
lines.append(" WSL:仅使用本机 ext4 等磁盘文件系统,9P 宿主盘 /mnt/[盘符] 绝不写入")
|
|
698
1008
|
print("\n".join(lines), flush=True)
|
|
699
1009
|
|
|
700
1010
|
# ------------------------------------------------------------------
|
|
@@ -710,33 +1020,50 @@ class DiskLoadBalancer:
|
|
|
710
1020
|
"""
|
|
711
1021
|
while True:
|
|
712
1022
|
try:
|
|
713
|
-
key, payload, path = self._write_queue.get()
|
|
714
|
-
try:
|
|
715
|
-
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
716
|
-
with open(path, "wb") as f:
|
|
717
|
-
f.write(payload)
|
|
718
|
-
except Exception as e:
|
|
719
|
-
# 写盘失败:不能静默丢数据 —— 从索引摘除该 key(内存已释放,必须显式
|
|
720
|
-
# 摘除并报警,否则调用方拿回一个"已登记但文件不存在/未被占用"的参数,
|
|
721
|
-
# 冻结层前向会悄悄变垃圾)。
|
|
722
|
-
with self._lock:
|
|
723
|
-
entry = self._cold_index.pop(key, None)
|
|
724
|
-
if entry:
|
|
725
|
-
cache_dir = os.path.dirname(entry[0])
|
|
726
|
-
try:
|
|
727
|
-
nbytes = int(entry[1]) * torch.empty(0, dtype=entry[2]).element_size()
|
|
728
|
-
except (TypeError, ValueError):
|
|
729
|
-
nbytes = 0
|
|
730
|
-
self._cache_bytes[cache_dir] = max(
|
|
731
|
-
0, self._cache_bytes.get(cache_dir, 0) - nbytes)
|
|
732
|
-
print(f"[disk_balancer] 写盘失败 {key}: {e} —— 已从索引摘除,该参数将被丢弃(无法读回)", flush=True)
|
|
733
|
-
finally:
|
|
734
|
-
if isinstance(payload, memoryview):
|
|
735
|
-
payload.release()
|
|
736
|
-
self._write_queue.task_done()
|
|
1023
|
+
key, payload, path, nbytes = self._write_queue.get()
|
|
737
1024
|
except Exception:
|
|
738
|
-
# queue 关闭或线程退出
|
|
739
|
-
|
|
1025
|
+
break # queue 关闭或线程退出
|
|
1026
|
+
try:
|
|
1027
|
+
cache_dir = os.path.dirname(path)
|
|
1028
|
+
self._wait_disk_idle(cache_dir)
|
|
1029
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
1030
|
+
with open(path, "wb") as f:
|
|
1031
|
+
f.write(payload)
|
|
1032
|
+
if self._cfg.fsync:
|
|
1033
|
+
f.flush()
|
|
1034
|
+
os.fsync(f.fileno())
|
|
1035
|
+
# 内存紧张时可让内核尽快丢掉刚写的页缓存(下次读会从盘上来,慢一点)。
|
|
1036
|
+
if os.name != "nt" and os.environ.get("BNB_FLASH_FADV_DONTNEED"):
|
|
1037
|
+
try:
|
|
1038
|
+
fd = os.open(path, os.O_RDONLY)
|
|
1039
|
+
try:
|
|
1040
|
+
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
|
|
1041
|
+
finally:
|
|
1042
|
+
os.close(fd)
|
|
1043
|
+
except OSError:
|
|
1044
|
+
pass
|
|
1045
|
+
except Exception as e:
|
|
1046
|
+
# 写盘失败:不能静默丢数据 —— 从索引摘除该 key(内存已释放,必须显式
|
|
1047
|
+
# 摘除并报警,否则调用方拿回一个"已登记但文件不存在/未被占用"的参数,
|
|
1048
|
+
# 冻结层前向会悄悄变垃圾)。
|
|
1049
|
+
with self._lock:
|
|
1050
|
+
entry = self._cold_index.pop(key, None)
|
|
1051
|
+
if entry:
|
|
1052
|
+
cache_dir = os.path.dirname(entry[0])
|
|
1053
|
+
try:
|
|
1054
|
+
nbytes_entry = int(entry[1]) * torch.empty(0, dtype=entry[2]).element_size()
|
|
1055
|
+
except (TypeError, ValueError):
|
|
1056
|
+
nbytes_entry = 0
|
|
1057
|
+
self._cache_bytes[cache_dir] = max(
|
|
1058
|
+
0, self._cache_bytes.get(cache_dir, 0) - nbytes_entry)
|
|
1059
|
+
print(f"[disk_balancer] 写盘失败 {key}: {e} —— 已从索引摘除,该参数将被丢弃(无法读回)", flush=True)
|
|
1060
|
+
finally:
|
|
1061
|
+
if isinstance(payload, memoryview):
|
|
1062
|
+
payload.release()
|
|
1063
|
+
with self._inflight_cv:
|
|
1064
|
+
self._inflight_bytes = max(0, self._inflight_bytes - nbytes)
|
|
1065
|
+
self._inflight_cv.notify_all()
|
|
1066
|
+
self._write_queue.task_done()
|
|
740
1067
|
|
|
741
1068
|
# ------------------------------------------------------------------
|
|
742
1069
|
# 冷参数检测与卸载
|
|
@@ -774,6 +1101,19 @@ class DiskLoadBalancer:
|
|
|
774
1101
|
# 兜底:第一个路径
|
|
775
1102
|
return self._paths[0] if self._paths else ""
|
|
776
1103
|
|
|
1104
|
+
def _wait_disk_idle(self, cache_dir: str, timeout: float = 2.0) -> None:
|
|
1105
|
+
"""写盘前等磁盘活动率降下来,避免训练线程和卸载线程一起把盘打满。
|
|
1106
|
+
|
|
1107
|
+
speed_first=True 时完全跳过(该模式的卖点就是不管硬盘)。
|
|
1108
|
+
"""
|
|
1109
|
+
if self._cfg.speed_first or not self._disks:
|
|
1110
|
+
return
|
|
1111
|
+
deadline = time.time() + timeout
|
|
1112
|
+
while time.time() < deadline:
|
|
1113
|
+
if self._check_disk_activity(cache_dir):
|
|
1114
|
+
return
|
|
1115
|
+
time.sleep(0.02)
|
|
1116
|
+
|
|
777
1117
|
def _check_disk_activity(self, cache_dir: str) -> bool:
|
|
778
1118
|
"""检查磁盘是否过载。"""
|
|
779
1119
|
if self._cfg.speed_first:
|
|
@@ -815,7 +1155,7 @@ class DiskLoadBalancer:
|
|
|
815
1155
|
def maybe_offload_latent(self, key: str, latent: torch.Tensor,
|
|
816
1156
|
t0: Optional[int] = None, t1: Optional[int] = None) -> Optional[torch.Tensor]:
|
|
817
1157
|
"""内存接近阈值时自动把 latent 分块卸载;返回 None 表示已卸载,调用方应释放原张量。"""
|
|
818
|
-
if
|
|
1158
|
+
if not self._started or self._disabled_reason or self._latent_store is None:
|
|
819
1159
|
return latent
|
|
820
1160
|
if not self._mem_pressure():
|
|
821
1161
|
return latent
|
|
@@ -853,9 +1193,7 @@ class DiskLoadBalancer:
|
|
|
853
1193
|
Returns:
|
|
854
1194
|
本次迁移的参数数量。
|
|
855
1195
|
"""
|
|
856
|
-
|
|
857
|
-
# 直接跳过、忽略用户参数(不迁移任何冷参数),宁可不做也要安全。
|
|
858
|
-
if getattr(self, "_9p_skip", False):
|
|
1196
|
+
if not self._started or self._disabled_reason:
|
|
859
1197
|
return 0
|
|
860
1198
|
if self._model is None:
|
|
861
1199
|
return 0
|
|
@@ -928,7 +1266,7 @@ class DiskLoadBalancer:
|
|
|
928
1266
|
注意:成功卸载后 ``tensor`` 的 storage 会被置空(``data = empty(0)``)。
|
|
929
1267
|
调用方必须自己保存原始 shape,读回时用 ``get_cold_shape(key, shape)``。
|
|
930
1268
|
"""
|
|
931
|
-
if not self._started:
|
|
1269
|
+
if not self._started or self._disabled_reason:
|
|
932
1270
|
return ""
|
|
933
1271
|
|
|
934
1272
|
self._ensure_dirs()
|
|
@@ -936,6 +1274,22 @@ class DiskLoadBalancer:
|
|
|
936
1274
|
if not cache_dir:
|
|
937
1275
|
return ""
|
|
938
1276
|
nbytes = int(tensor.numel()) * tensor.element_size()
|
|
1277
|
+
|
|
1278
|
+
# min_free_ratio 要真正生效:写之前看该挂载的实时剩余空间(配置里的 DiskInfo
|
|
1279
|
+
# 是 start() 时刻的快照,训练几小时后可能已经不准)。
|
|
1280
|
+
try:
|
|
1281
|
+
usg = shutil.disk_usage(cache_dir)
|
|
1282
|
+
if usg.total > 0 and usg.free < usg.total * self._cfg.min_free_ratio:
|
|
1283
|
+
print(
|
|
1284
|
+
f"[disk_balancer] 磁盘剩余空间不足({cache_dir}: "
|
|
1285
|
+
f"{usg.free / (1 << 30):.1f}GB free < "
|
|
1286
|
+
f"{self._cfg.min_free_ratio * 100:.0f}%),跳过卸载 {key}",
|
|
1287
|
+
flush=True,
|
|
1288
|
+
)
|
|
1289
|
+
return ""
|
|
1290
|
+
except OSError:
|
|
1291
|
+
pass
|
|
1292
|
+
|
|
939
1293
|
quota = 0
|
|
940
1294
|
for d in self._disks:
|
|
941
1295
|
if d.cache_dir == cache_dir:
|
|
@@ -949,18 +1303,55 @@ class DiskLoadBalancer:
|
|
|
949
1303
|
flush=True,
|
|
950
1304
|
)
|
|
951
1305
|
return ""
|
|
952
|
-
path = os.path.join(cache_dir, f"{key}.bin")
|
|
953
1306
|
|
|
954
|
-
#
|
|
1307
|
+
# 背压:零拷贝队列持有源张量内存,写盘线程消费前内存并未真正释放。
|
|
1308
|
+
# 超过在飞上限时短暂等待;等不到就放弃这次卸载(参数留在内存,行为安全)。
|
|
1309
|
+
deadline = time.time() + 5.0
|
|
1310
|
+
with self._inflight_cv:
|
|
1311
|
+
while (self._max_inflight_bytes
|
|
1312
|
+
and self._inflight_bytes + nbytes > self._max_inflight_bytes):
|
|
1313
|
+
remain = deadline - time.time()
|
|
1314
|
+
if remain <= 0:
|
|
1315
|
+
break
|
|
1316
|
+
self._inflight_cv.wait(min(remain, 0.1))
|
|
1317
|
+
if (self._max_inflight_bytes
|
|
1318
|
+
and self._inflight_bytes + nbytes > self._max_inflight_bytes):
|
|
1319
|
+
print(
|
|
1320
|
+
f"[disk_balancer] 在飞写盘队列已满({self._inflight_bytes / (1 << 20):.0f}MB"
|
|
1321
|
+
f" / {self._max_inflight_bytes / (1 << 20):.0f}MB),"
|
|
1322
|
+
f"跳过卸载 {key},参数保留在内存",
|
|
1323
|
+
flush=True,
|
|
1324
|
+
)
|
|
1325
|
+
return ""
|
|
1326
|
+
self._inflight_bytes += nbytes
|
|
1327
|
+
|
|
1328
|
+
path = os.path.join(cache_dir, f"{key}.bin")
|
|
955
1329
|
dtype = tensor.dtype
|
|
956
1330
|
numel = tensor.numel()
|
|
957
|
-
|
|
958
1331
|
with self._lock:
|
|
959
1332
|
self._cold_index[key] = (path, numel, dtype)
|
|
960
1333
|
self._cache_bytes[cache_dir] = self._cache_bytes.get(cache_dir, 0) + nbytes
|
|
961
1334
|
|
|
962
1335
|
# 异步写盘(零拷贝:队列里放的是源 storage 的视图,不复制字节)
|
|
963
|
-
|
|
1336
|
+
payload = None
|
|
1337
|
+
try:
|
|
1338
|
+
payload = self._payload_memoryview(tensor)
|
|
1339
|
+
self._write_queue.put((key, payload, path, nbytes), timeout=5)
|
|
1340
|
+
except Exception as e:
|
|
1341
|
+
with self._lock:
|
|
1342
|
+
self._cold_index.pop(key, None)
|
|
1343
|
+
self._cache_bytes[cache_dir] = max(
|
|
1344
|
+
0, self._cache_bytes.get(cache_dir, 0) - nbytes)
|
|
1345
|
+
with self._inflight_cv:
|
|
1346
|
+
self._inflight_bytes = max(0, self._inflight_bytes - nbytes)
|
|
1347
|
+
self._inflight_cv.notify_all()
|
|
1348
|
+
if isinstance(payload, memoryview):
|
|
1349
|
+
payload.release()
|
|
1350
|
+
print(f"[disk_balancer] 入队失败 {key}: {e};参数保留在内存", flush=True)
|
|
1351
|
+
return ""
|
|
1352
|
+
|
|
1353
|
+
with self._inflight_cv:
|
|
1354
|
+
self._write_bytes_total += nbytes
|
|
964
1355
|
|
|
965
1356
|
# 释放内存
|
|
966
1357
|
tensor.data = torch.empty(0, device=tensor.device, dtype=tensor.dtype)
|
|
@@ -994,6 +1385,17 @@ class DiskLoadBalancer:
|
|
|
994
1385
|
if not os.path.exists(path):
|
|
995
1386
|
return None
|
|
996
1387
|
|
|
1388
|
+
if os.name != "nt":
|
|
1389
|
+
# 告诉内核这文件马上要顺序读,尽量提前拉起 I/O。
|
|
1390
|
+
try:
|
|
1391
|
+
fd = os.open(path, os.O_RDONLY)
|
|
1392
|
+
try:
|
|
1393
|
+
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_WILLNEED)
|
|
1394
|
+
finally:
|
|
1395
|
+
os.close(fd)
|
|
1396
|
+
except OSError:
|
|
1397
|
+
pass
|
|
1398
|
+
|
|
997
1399
|
try:
|
|
998
1400
|
with open(path, "rb") as f:
|
|
999
1401
|
mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
|
|
@@ -1084,6 +1486,8 @@ class DiskLoadBalancer:
|
|
|
1084
1486
|
如果 keep_cache=True,只清空索引,保留磁盘文件。
|
|
1085
1487
|
如果 keep_cache=False(默认),删除所有磁盘缓存文件。
|
|
1086
1488
|
"""
|
|
1489
|
+
if not self._started:
|
|
1490
|
+
return
|
|
1087
1491
|
self.wait_writes()
|
|
1088
1492
|
|
|
1089
1493
|
if self._latent_store is not None:
|
|
@@ -1117,6 +1521,12 @@ class DiskLoadBalancer:
|
|
|
1117
1521
|
"memory_percent": mem.percent,
|
|
1118
1522
|
"memory_available_mb": mem.available / (1 << 20),
|
|
1119
1523
|
"queue_size": self._write_queue.qsize(),
|
|
1524
|
+
"inflight_mb": self._inflight_bytes / (1 << 20),
|
|
1525
|
+
"max_inflight_mb": self._max_inflight_bytes / (1 << 20),
|
|
1526
|
+
"write_total_mb": self._write_bytes_total / (1 << 20),
|
|
1527
|
+
"fsync": self._cfg.fsync,
|
|
1528
|
+
"wsl": self._wsl,
|
|
1529
|
+
"disabled_reason": self._disabled_reason,
|
|
1120
1530
|
"latent_chunks": len(self._latent_keys),
|
|
1121
1531
|
"latent_offloaded": self._latent_offloaded,
|
|
1122
1532
|
"disks": [
|
|
@@ -1156,11 +1566,12 @@ def add_flash_args(parser) -> None:
|
|
|
1156
1566
|
)
|
|
1157
1567
|
parser.add_argument(
|
|
1158
1568
|
"--flash_sizes", type=str, default=None,
|
|
1159
|
-
help="手动模式每盘缓存大小(MB)
|
|
1569
|
+
help="手动模式每盘缓存大小(MB)。Windows: C:2048,D:4096;Linux/WSL: /:2048,/mnt/data:4096",
|
|
1160
1570
|
)
|
|
1161
1571
|
parser.add_argument(
|
|
1162
1572
|
"--flash_paths", type=str, default=None,
|
|
1163
|
-
help="
|
|
1573
|
+
help="缓存路径,逗号分隔。Windows: D:\\cache,E:\\cache;"
|
|
1574
|
+
"Linux/WSL: /home/me/fastcache(9P/网络/只读/内存盘会被拒绝)",
|
|
1164
1575
|
)
|
|
1165
1576
|
parser.add_argument(
|
|
1166
1577
|
"--flash_speed", action="store_true", default=False,
|
bitsandbytes/torch_cpu_kit.py
CHANGED
|
@@ -27,6 +27,11 @@ from contextlib import nullcontext
|
|
|
27
27
|
from typing import Any, Optional
|
|
28
28
|
|
|
29
29
|
__all__ = [
|
|
30
|
+
"plan_concurrency",
|
|
31
|
+
"plan_concurrency_for_training",
|
|
32
|
+
"estimate_training_memory_gb",
|
|
33
|
+
"spawn_training_jobs",
|
|
34
|
+
"tune_threads",
|
|
30
35
|
"apply_env",
|
|
31
36
|
"init",
|
|
32
37
|
"report",
|
|
@@ -169,6 +174,9 @@ def init(
|
|
|
169
174
|
的 im2col/分块吃满);attention 小块反而 6 线程好,但整体 conv 主导;
|
|
170
175
|
- ``None``:回退物理核数。
|
|
171
176
|
|
|
177
|
+
注意:这些是静态建议(GEMM 密集假设)。混合注意力 / 短序列用 ``tune_threads()``
|
|
178
|
+
在真实 step 上实测更准——6 核机器上实测 4 线程可能比 6 线程快 7~10%。
|
|
179
|
+
|
|
172
180
|
返回调优报告文本(同 report() 的字符串,不是 dict)。
|
|
173
181
|
"""
|
|
174
182
|
import torch
|
|
@@ -453,6 +461,324 @@ def suspend_mem_monitor(ev: Optional[threading.Event] = None) -> None:
|
|
|
453
461
|
(ev or _MON_STOP).set()
|
|
454
462
|
|
|
455
463
|
|
|
464
|
+
# ---------------------------------------------------------------------------
|
|
465
|
+
# 并发规划:多进程 x 单线程 通常优于 单进程 x 多线程(2026-10-19 实测)
|
|
466
|
+
# ---------------------------------------------------------------------------
|
|
467
|
+
def plan_concurrency(
|
|
468
|
+
n_jobs: Optional[int] = None,
|
|
469
|
+
per_job_ram_gb: Optional[float] = None,
|
|
470
|
+
cores: Optional[int] = None,
|
|
471
|
+
available_ram_gb: Optional[float] = None,
|
|
472
|
+
) -> dict:
|
|
473
|
+
"""给「同时跑几个训练 job、每个开几个线程」一个可落地的建议。
|
|
474
|
+
|
|
475
|
+
背景(R5-4500U,6C/6T,合成 3-block 训练 + AdamW8bit 实测)::
|
|
476
|
+
|
|
477
|
+
jobs x threads/job aggregate tok/s 相对最佳
|
|
478
|
+
1 x 6 3843 62.8%
|
|
479
|
+
2 x 3 4672 76.3%
|
|
480
|
+
3 x 2 4973 81.2%
|
|
481
|
+
6 x 1 5619 91.8%
|
|
482
|
+
8 x 1(超订) 6123 100.0%
|
|
483
|
+
|
|
484
|
+
结论:**多进程 x 单线程** 比 **单进程吃满多线程** 的聚合吞吐高约 60%——
|
|
485
|
+
OpenMP 线程池的 barrier/自旋等待在进程间是纯浪费。推荐打包 = 每个物理核一个
|
|
486
|
+
job、每 job 1 线程(6C 机器就是 6 个单线程 job;超订到 8 个还能再涨一点,但收益变平,
|
|
487
|
+
且每个 job 都要一份内存)。RAM 是第二约束:每 job 的模型 + 优化器状态各自独立。
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
n_jobs: 想跑几个 job;None = 按物理核数推荐(每核 1 个)。
|
|
491
|
+
per_job_ram_gb: 单 job 预计占用的 RAM(GB)。None = 用当前进程 RSS 估算,
|
|
492
|
+
所以**请在加载完一个模型后调用**(或显式传入训练脚本的常驻内存)。
|
|
493
|
+
cores: 物理核数;None = ``physical_cores()``。
|
|
494
|
+
available_ram_gb: 系统可用内存;None = psutil 读取。
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
dict: ``n_jobs`` / ``threads_per_job`` / ``cores`` / ``per_job_ram_gb`` /
|
|
498
|
+
``max_by_ram`` / ``env``(给子进程用的 OMP/MKL 环境变量) / ``advice``。
|
|
499
|
+
|
|
500
|
+
用法::
|
|
501
|
+
|
|
502
|
+
import torch_cpu_kit as tck
|
|
503
|
+
plan = tck.plan_concurrency(per_job_ram_gb=1.5)
|
|
504
|
+
for i in range(plan["n_jobs"]):
|
|
505
|
+
subprocess.Popen([sys.executable, "train.py"], env={**os.environ, **plan["env"]})
|
|
506
|
+
"""
|
|
507
|
+
cores = max(1, int(cores or physical_cores()))
|
|
508
|
+
if available_ram_gb is None:
|
|
509
|
+
try:
|
|
510
|
+
import psutil
|
|
511
|
+
available_ram_gb = psutil.virtual_memory().available / 1e9
|
|
512
|
+
except Exception:
|
|
513
|
+
available_ram_gb = 8.0
|
|
514
|
+
if per_job_ram_gb is None:
|
|
515
|
+
try:
|
|
516
|
+
import psutil
|
|
517
|
+
per_job_ram_gb = max(0.3, psutil.Process().memory_info().rss / 1e9)
|
|
518
|
+
except Exception:
|
|
519
|
+
per_job_ram_gb = 1.0
|
|
520
|
+
|
|
521
|
+
# 给系统/页缓存留 10%
|
|
522
|
+
max_by_ram = max(1, int((available_ram_gb * 0.9) // max(0.1, per_job_ram_gb)))
|
|
523
|
+
if n_jobs is None:
|
|
524
|
+
n_jobs = min(cores, max_by_ram)
|
|
525
|
+
else:
|
|
526
|
+
n_jobs = max(1, int(n_jobs))
|
|
527
|
+
if n_jobs > max_by_ram:
|
|
528
|
+
n_jobs = max_by_ram
|
|
529
|
+
|
|
530
|
+
if n_jobs <= 1:
|
|
531
|
+
threads_per_job = cores
|
|
532
|
+
else:
|
|
533
|
+
# 按核数均分;n_jobs >= cores 时每 job 1 线程(实测最优点)
|
|
534
|
+
threads_per_job = max(1, cores // n_jobs)
|
|
535
|
+
|
|
536
|
+
env = {
|
|
537
|
+
"OMP_NUM_THREADS": str(threads_per_job),
|
|
538
|
+
"MKL_NUM_THREADS": str(threads_per_job),
|
|
539
|
+
"OMP_WAIT_POLICY": "passive" if threads_per_job == 1 else "active",
|
|
540
|
+
"OMP_DYNAMIC": "FALSE",
|
|
541
|
+
"MKL_DYNAMIC": "FALSE",
|
|
542
|
+
}
|
|
543
|
+
advice = (
|
|
544
|
+
"recommended: %d job(s) x %d thread(s) each (physical cores=%d, "
|
|
545
|
+
"RAM %.1fGB available / %.2fGB per job -> RAM allows %d)"
|
|
546
|
+
% (n_jobs, threads_per_job, cores, available_ram_gb, per_job_ram_gb, max_by_ram)
|
|
547
|
+
)
|
|
548
|
+
return {
|
|
549
|
+
"n_jobs": n_jobs,
|
|
550
|
+
"threads_per_job": threads_per_job,
|
|
551
|
+
"cores": cores,
|
|
552
|
+
"per_job_ram_gb": round(per_job_ram_gb, 2),
|
|
553
|
+
"available_ram_gb": round(available_ram_gb, 1),
|
|
554
|
+
"max_by_ram": max_by_ram,
|
|
555
|
+
"env": env,
|
|
556
|
+
"advice": advice,
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def apply_job_env(plan: dict) -> dict:
|
|
561
|
+
"""把 plan_concurrency() 的 env 写进当前进程(给 launcher 自己用;子进程可直接用 env=)。"""
|
|
562
|
+
env = dict(plan.get("env") or {})
|
|
563
|
+
os.environ.update(env)
|
|
564
|
+
return env
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
# ---------------------------------------------------------------------------
|
|
568
|
+
# 单 job 内存估算 + 训练并发规划 + 多 job 启动器
|
|
569
|
+
# ---------------------------------------------------------------------------
|
|
570
|
+
_DTYPE_BYTES = {"fp32": 4, "float32": 4, "bf16": 2, "bfloat16": 2, "fp16": 2, "float16": 2}
|
|
571
|
+
_OPT_BYTES_PER_PARAM = {
|
|
572
|
+
# 8-bit blockwise: 1-byte code x 2 states + 每 256 元素一个 fp32 absmax
|
|
573
|
+
"adamw8bit": 2.0 + 4.0 / 256.0,
|
|
574
|
+
# ademamix 8-bit: 2 个 8-bit 状态 + 1 个 8-bit 额外状态 + absmax
|
|
575
|
+
"ademamix8bit": 3.0 + 8.0 / 256.0,
|
|
576
|
+
"adamw": 8.0, # fp32 m + v
|
|
577
|
+
"adam": 8.0,
|
|
578
|
+
"sgd": 0.0,
|
|
579
|
+
"sgd_momentum": 4.0, # fp32 momentum
|
|
580
|
+
"none": 0.0,
|
|
581
|
+
}
|
|
582
|
+
_RUNTIME_GB = 0.4 # torch + CUDA-less 运行时 + Python 基线(实测约 0.35-0.40GB)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def estimate_training_memory_gb(
|
|
586
|
+
param_count: int,
|
|
587
|
+
mode: str = "full",
|
|
588
|
+
optimizer: str = "adamw8bit",
|
|
589
|
+
trainable_fraction: float = 1.0,
|
|
590
|
+
trainable_params: Optional[int] = None,
|
|
591
|
+
dtype: str = "fp32",
|
|
592
|
+
activation_gb: float = 1.0,
|
|
593
|
+
runtime_gb: float = _RUNTIME_GB,
|
|
594
|
+
) -> dict:
|
|
595
|
+
"""粗估一个 CPU 训练 job 的常驻内存(GB)。
|
|
596
|
+
|
|
597
|
+
组成:权重(全部参数,fp32/bf16 按 dtype)+ 梯度(可训练参数)+ 优化器状态(可训练参数)
|
|
598
|
+
+ 激活/工作区(由调用者按 batch*seq 估,默认 1GB)+ 运行时基线(默认 0.4GB)。
|
|
599
|
+
|
|
600
|
+
``mode`` 只影响默认的 ``trainable_fraction``:
|
|
601
|
+
- ``full`` / ``finetune``:默认全部参数可训练(1.0);
|
|
602
|
+
- ``lora`` / ``efst``:默认按调用方给的 ``trainable_fraction``(如 0.005),
|
|
603
|
+
冻结的基座**不产生梯度**(这是 LoRA/EFST 省内存的核心)。
|
|
604
|
+
|
|
605
|
+
注意:这是**估算**,用于决定并发数,不是精确值;首次跑起来后用
|
|
606
|
+
``tck.mem_report()`` 或 ``psutil`` 校准。
|
|
607
|
+
|
|
608
|
+
返回 dict:``total_gb`` 以及 weights/grads/optimizer/activations/runtime 分项。
|
|
609
|
+
"""
|
|
610
|
+
if param_count is None or param_count <= 0:
|
|
611
|
+
raise ValueError("param_count must be > 0")
|
|
612
|
+
mode = (mode or "full").lower()
|
|
613
|
+
if trainable_params is not None:
|
|
614
|
+
trainable = max(0, int(trainable_params))
|
|
615
|
+
else:
|
|
616
|
+
frac = 1.0 if mode in ("full", "finetune") else float(trainable_fraction)
|
|
617
|
+
frac = min(max(frac, 0.0), 1.0)
|
|
618
|
+
trainable = int(param_count * frac)
|
|
619
|
+
|
|
620
|
+
bpp = _DTYPE_BYTES.get((dtype or "fp32").lower())
|
|
621
|
+
if bpp is None:
|
|
622
|
+
raise ValueError(f"unsupported dtype {dtype!r}; use fp32/bf16/fp16")
|
|
623
|
+
opt_key = (optimizer or "none").lower()
|
|
624
|
+
if opt_key not in _OPT_BYTES_PER_PARAM:
|
|
625
|
+
raise ValueError(f"unsupported optimizer {optimizer!r}; "
|
|
626
|
+
f"choose from {sorted(_OPT_BYTES_PER_PARAM)}")
|
|
627
|
+
|
|
628
|
+
weights = param_count * bpp
|
|
629
|
+
grads = trainable * bpp # frozen base has no grads
|
|
630
|
+
opt_states = trainable * _OPT_BYTES_PER_PARAM[opt_key]
|
|
631
|
+
activations = float(activation_gb) * (1 << 30)
|
|
632
|
+
runtime = float(runtime_gb) * (1 << 30)
|
|
633
|
+
total = weights + grads + opt_states + activations + runtime
|
|
634
|
+
return {
|
|
635
|
+
"total_gb": total / (1 << 30),
|
|
636
|
+
"param_count": int(param_count),
|
|
637
|
+
"trainable_params": trainable,
|
|
638
|
+
"weights_gb": weights / (1 << 30),
|
|
639
|
+
"grads_gb": grads / (1 << 30),
|
|
640
|
+
"optimizer_gb": opt_states / (1 << 30),
|
|
641
|
+
"activations_gb": activations / (1 << 30),
|
|
642
|
+
"runtime_gb": runtime / (1 << 30),
|
|
643
|
+
"dtype": dtype,
|
|
644
|
+
"optimizer": opt_key,
|
|
645
|
+
"mode": mode,
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def plan_concurrency_for_training(
|
|
650
|
+
param_count: int,
|
|
651
|
+
mode: str = "full",
|
|
652
|
+
optimizer: str = "adamw8bit",
|
|
653
|
+
trainable_fraction: float = 1.0,
|
|
654
|
+
trainable_params: Optional[int] = None,
|
|
655
|
+
dtype: str = "fp32",
|
|
656
|
+
activation_gb: float = 1.0,
|
|
657
|
+
n_jobs: Optional[int] = None,
|
|
658
|
+
**kwargs: Any,
|
|
659
|
+
) -> dict:
|
|
660
|
+
"""``estimate_training_memory_gb`` + ``plan_concurrency`` 的组合。
|
|
661
|
+
|
|
662
|
+
例:0.8B 模型全参数微调(fp32 + AdamW8bit + 1GB 激活):
|
|
663
|
+
plan = plan_concurrency_for_training(8e8)
|
|
664
|
+
plan["memory_estimate"]["total_gb"] # 约 5.3GB
|
|
665
|
+
plan["n_jobs"] / plan["threads_per_job"] / plan["env"]
|
|
666
|
+
"""
|
|
667
|
+
est = estimate_training_memory_gb(
|
|
668
|
+
param_count, mode=mode, optimizer=optimizer,
|
|
669
|
+
trainable_fraction=trainable_fraction, trainable_params=trainable_params,
|
|
670
|
+
dtype=dtype, activation_gb=activation_gb)
|
|
671
|
+
plan = plan_concurrency(n_jobs=n_jobs, per_job_ram_gb=est["total_gb"], **kwargs)
|
|
672
|
+
plan["memory_estimate"] = est
|
|
673
|
+
return plan
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def spawn_training_jobs(cmds, plan: Optional[dict] = None, cwd=None, echo: bool = True):
|
|
677
|
+
"""并行启动多个训练命令,自动给日志加 ``[jobN]`` 前缀(不会混在一起)。
|
|
678
|
+
|
|
679
|
+
Args:
|
|
680
|
+
cmds: 命令列表,如 ``[[sys.executable, "train.py", "--seed", "1"], [...]]``。
|
|
681
|
+
plan: ``plan_concurrency()`` 的返回值;其 ``env`` 会合并进每个子进程的环境变量
|
|
682
|
+
(OMP/MKL 线程数等)。None = 继承当前进程环境。
|
|
683
|
+
cwd: 子进程工作目录。
|
|
684
|
+
echo: True 时实时打印带前缀的日志。
|
|
685
|
+
|
|
686
|
+
Returns:
|
|
687
|
+
``[(cmd, returncode), ...]``。
|
|
688
|
+
"""
|
|
689
|
+
env = dict(os.environ)
|
|
690
|
+
if plan:
|
|
691
|
+
env.update(plan.get("env") or {})
|
|
692
|
+
procs = []
|
|
693
|
+
readers = []
|
|
694
|
+
for i, cmd in enumerate(cmds):
|
|
695
|
+
proc = subprocess.Popen(
|
|
696
|
+
cmd, cwd=cwd, env=env, stdout=subprocess.PIPE,
|
|
697
|
+
stderr=subprocess.STDOUT, text=True, bufsize=1)
|
|
698
|
+
procs.append((cmd, proc))
|
|
699
|
+
|
|
700
|
+
def _pump(p=proc, idx=i):
|
|
701
|
+
try:
|
|
702
|
+
for line in p.stdout:
|
|
703
|
+
if echo:
|
|
704
|
+
print(f"[job{idx}] {line.rstrip()}", flush=True)
|
|
705
|
+
except Exception:
|
|
706
|
+
pass
|
|
707
|
+
|
|
708
|
+
th = threading.Thread(target=_pump, daemon=True)
|
|
709
|
+
th.start()
|
|
710
|
+
readers.append(th)
|
|
711
|
+
|
|
712
|
+
results = []
|
|
713
|
+
for cmd, proc in procs:
|
|
714
|
+
rc = proc.wait()
|
|
715
|
+
results.append((cmd, rc))
|
|
716
|
+
for th in readers:
|
|
717
|
+
th.join(timeout=2.0)
|
|
718
|
+
return results
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def tune_threads(step_fn, candidates=None, warmup=1, iters=2, verbose=True):
|
|
722
|
+
"""在真实训练 step 上自动挑 ``torch.set_num_threads`` 的最优值。
|
|
723
|
+
|
|
724
|
+
为什么需要:静态建议(物理核数 / 逻辑核×2/3)是按 GEMM 密集假设给的;混合注意力模型
|
|
725
|
+
(Qwen3.5 这类 GDN+全注意力)在短序列下有很多小算子和逐元素操作,实测 6 核机器上
|
|
726
|
+
**4 线程比 6 线程快 7~10%**(seq64: 2034 vs 2188ms;seq256: 6107 vs 6797ms)。
|
|
727
|
+
所以"压榨"之前先量一下。
|
|
728
|
+
|
|
729
|
+
Args:
|
|
730
|
+
step_fn: 无参可调用,执行一次真实训练 step(前向+反向+optimizer step 全部);
|
|
731
|
+
会重复调用,训练参数会变化,但相对耗时通常足够稳定。想更干净可以传
|
|
732
|
+
"只做前向+反向、不 step 优化器"的版本。
|
|
733
|
+
candidates: 候选线程数列表;默认 ``[2,3,4,5,6,8]`` 里不超过 CPU 核数的。
|
|
734
|
+
warmup: 每个候选先跑几次热机(不计时)。
|
|
735
|
+
iters: 每个候选计时次数,取中位数。
|
|
736
|
+
verbose: 打印每个候选的耗时。
|
|
737
|
+
|
|
738
|
+
Returns:
|
|
739
|
+
dict: ``best``(最优线程数)、``timings``({线程数: 毫秒})、``speedup_vs_worst``。
|
|
740
|
+
结束时 ``torch.set_num_threads(best)``,方便直接继续训练。
|
|
741
|
+
"""
|
|
742
|
+
import time as _time
|
|
743
|
+
|
|
744
|
+
import torch
|
|
745
|
+
|
|
746
|
+
import statistics as _st
|
|
747
|
+
|
|
748
|
+
cores = os.cpu_count() or 1
|
|
749
|
+
if candidates is None:
|
|
750
|
+
candidates = [n for n in (2, 3, 4, 5, 6, 8) if n <= max(1, cores)]
|
|
751
|
+
candidates = [int(n) for n in candidates if int(n) >= 1]
|
|
752
|
+
if not candidates:
|
|
753
|
+
candidates = [1]
|
|
754
|
+
|
|
755
|
+
timings = {}
|
|
756
|
+
for n in candidates:
|
|
757
|
+
torch.set_num_threads(n)
|
|
758
|
+
for _ in range(max(0, warmup)):
|
|
759
|
+
step_fn()
|
|
760
|
+
samples = []
|
|
761
|
+
for _ in range(max(1, iters)):
|
|
762
|
+
t0 = _time.perf_counter()
|
|
763
|
+
step_fn()
|
|
764
|
+
samples.append(_time.perf_counter() - t0)
|
|
765
|
+
timings[n] = _st.median(samples) * 1e3
|
|
766
|
+
if verbose:
|
|
767
|
+
print("[torch_cpu_kit] threads=%d -> %.1f ms/step" % (n, timings[n]))
|
|
768
|
+
|
|
769
|
+
best = min(timings, key=timings.get)
|
|
770
|
+
torch.set_num_threads(best)
|
|
771
|
+
worst = max(timings.values())
|
|
772
|
+
if verbose:
|
|
773
|
+
print("[torch_cpu_kit] best threads=%d (%.1f ms/step, %.1f%% faster than worst)"
|
|
774
|
+
% (best, timings[best], 100.0 * (1.0 - timings[best] / worst)))
|
|
775
|
+
return {
|
|
776
|
+
"best": best,
|
|
777
|
+
"timings": timings,
|
|
778
|
+
"speedup_vs_worst": (worst / timings[best]) if timings[best] else 1.0,
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
|
|
456
782
|
if __name__ == "__main__": # 自检:python torch_cpu_kit.py
|
|
457
783
|
apply_env(verbose=True)
|
|
458
784
|
import torch
|
{bitsandbytes_cpu_fork-0.50.2.dev4.dist-info → bitsandbytes_cpu_fork-0.50.2.dev5.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bitsandbytes-cpu-fork
|
|
3
|
-
Version: 0.50.2.
|
|
3
|
+
Version: 0.50.2.dev5
|
|
4
4
|
Summary: bitsandbytes with a pure-CPU training backend (AVX2 / NEON): 8-bit optimizers, fused GDN, gemm_8bit, disk balancer. No GPU required.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: repository, https://github.com/Ganyvnanoaa09-0906/bitsandbytes-CPU
|
|
@@ -336,7 +336,7 @@ code and the Transformers / PEFT / Diffusers integrations keep working as-is:
|
|
|
336
336
|
|
|
337
337
|
```python
|
|
338
338
|
import bitsandbytes as bnb # the fork, not upstream
|
|
339
|
-
print(bnb.__version__) # 0.50.2.
|
|
339
|
+
print(bnb.__version__) # 0.50.2.dev5
|
|
340
340
|
```
|
|
341
341
|
|
|
342
342
|
Building from source instead (any platform) is described in
|
|
@@ -381,7 +381,7 @@ build_manual\build_manual.bat amd # (in VS x64 Native Tools prompt)
|
|
|
381
381
|
|
|
382
382
|
> `.gitattributes` 强制 C/C++ 源码与构建脚本用 LF 换行,clone 到 Windows/Linux 均可
|
|
383
383
|
> 直接编译(CRLF 会让 MSVC 吞行、Linux `g++` 报 `bad interpreter`)。
|
|
384
|
-
> `disk_balancer`(`--flash`)跨平台:Windows 用 ctypes
|
|
384
|
+
> `disk_balancer`(`--flash`)跨平台:Windows 用 ctypes;Linux 用 `/proc/mounts` + psutil + `/sys/block/*/queue/rotational`;**WSL 已支持**——缓存只落在 WSL 自己的 ext4(默认 `~/.cache/bnb_flash_cache`),9P 宿主盘 `/mnt/[盘符]`、网络盘、只读挂载、tmpfs 一律拒绝(显式 `--flash_paths` 指过去会直接报错)。零拷贝写盘队列有在飞字节上限(`max_inflight_bytes`),卸载不会把源张量内存偷偷留住。
|
|
385
385
|
|
|
386
386
|
### Prebuilt wheels (optional)
|
|
387
387
|
|
{bitsandbytes_cpu_fork-0.50.2.dev4.dist-info → bitsandbytes_cpu_fork-0.50.2.dev5.dist-info}/RECORD
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
bitsandbytes_cpu_cli.py,sha256=rSRi1aba5Ztr5HGrC0is8wG_hWxJx2W8CD9FD-rm96U,50540
|
|
2
|
-
bitsandbytes/__init__.py,sha256=
|
|
2
|
+
bitsandbytes/__init__.py,sha256=8HcKBjAwoTyG93Vv0f6JYEAfh4vcLi8InNzYJYq7O3g,2370
|
|
3
3
|
bitsandbytes/__main__.py,sha256=A3iA9QH8yfn-60uN6w_7WUiuRIM777rZ_pf_HsZtse4,90
|
|
4
4
|
bitsandbytes/_cpu_caps.py,sha256=LgG3yRHdf0YGxTACxrweqiunghmuJbDAol4s02mwmHA,4691
|
|
5
5
|
bitsandbytes/_ops.py,sha256=VrOBIbain2ftHpBgxUHkf8fRpq1RCnsFi16tS-YGj6E,20640
|
|
@@ -8,13 +8,13 @@ bitsandbytes/cli.py,sha256=_nF8KmWvu2jqqpoGvTf-spt2GxA7F11TstTK3FBXbgE,1125
|
|
|
8
8
|
bitsandbytes/consts.py,sha256=xfDadQAax-maxqkHrVNbZ6UITJoS0XgcUfopmjLgilM,380
|
|
9
9
|
bitsandbytes/cuda_specs.py,sha256=akFTR2ZxQ5CFpFLeg5QKFbVOZ4xz3USax43RxazP22k,3236
|
|
10
10
|
bitsandbytes/detect_cpu.py,sha256=7L0QgAU_a6w6Ckm33vnaLgUwqAhU6ce9pQ1fwkk52Y4,8071
|
|
11
|
-
bitsandbytes/disk_balancer.py,sha256=
|
|
11
|
+
bitsandbytes/disk_balancer.py,sha256=lyNgcqfBSsoflBDMffXKA4oNld4jBTbzugja988IN9w,74135
|
|
12
12
|
bitsandbytes/efst.py,sha256=D2U8NACi_p_qJi2-_xbi3EZblPWQDhaX-h7hXO1iKMs,30772
|
|
13
13
|
bitsandbytes/functional.py,sha256=chLY-f592gXbSVOHA78ME3d-6Jq8rU3d2xJwx3GYDII,66814
|
|
14
14
|
bitsandbytes/gdn_cpu.py,sha256=QT5pdWwxPob6SL33wj4kjJLGnIkApivxvXjn7a--8Ys,27412
|
|
15
15
|
bitsandbytes/libbitsandbytes_cpu.dll,sha256=DZrZqVfXiCtJaQEI_NNkCFLIbbxBYMYJBgSA35Zu1ao,375808
|
|
16
16
|
bitsandbytes/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
-
bitsandbytes/torch_cpu_kit.py,sha256=
|
|
17
|
+
bitsandbytes/torch_cpu_kit.py,sha256=P5Bk5jI86sT3Z2cleWDwv9XV0ydZ2rPflNdxOP2zzPU,32844
|
|
18
18
|
bitsandbytes/utils.py,sha256=G1oZbgIAt07m9-7SpLBRtIjsipI8kCptdfTWKz96g9I,6928
|
|
19
19
|
bitsandbytes/vcomp140.dll,sha256=ldTOSmgC0eGLXg4XIsww6nLKfgM_g4KPBcC3uZP-fL8,212920
|
|
20
20
|
bitsandbytes/autograd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -22,7 +22,7 @@ bitsandbytes/autograd/_functions.py,sha256=d8pUyHtprUcKxz5dfd6UlI8g3p3ouHZJV3KLw
|
|
|
22
22
|
bitsandbytes/backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
23
|
bitsandbytes/backends/utils.py,sha256=3FZPL78T26gTiKI9TIcWfdVNoEr48XsNtSaF_kKxHIQ,2322
|
|
24
24
|
bitsandbytes/backends/cpu/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
-
bitsandbytes/backends/cpu/ops.py,sha256=
|
|
25
|
+
bitsandbytes/backends/cpu/ops.py,sha256=uDGAbpa8d9Wv-wf1QMxG5rsWO5NKAoaN8cR1T2-7zm4,33290
|
|
26
26
|
bitsandbytes/backends/cuda/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
27
|
bitsandbytes/backends/cuda/ops.py,sha256=1Y-goMBT1aDs0ek9hHUx1eAiagvYBIRy7gymMkZMwg8,42583
|
|
28
28
|
bitsandbytes/backends/default/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -56,9 +56,9 @@ bitsandbytes/optim/lion.py,sha256=OUu1aysIM85NNHNFu-LDTCLI6HoqPNMp-bLVgZAGtHk,83
|
|
|
56
56
|
bitsandbytes/optim/optimizer.py,sha256=LgegbqBzbk5s57uCEgcmSqXC5wRHGMiAIbe646Fd3y4,28127
|
|
57
57
|
bitsandbytes/optim/rmsprop.py,sha256=2sHSY1hfTr83aZvBmIEfzoTiKER8LZc_BDkmyJG3lPs,6134
|
|
58
58
|
bitsandbytes/optim/sgd.py,sha256=gVjbjjYrm197RiOt6vLoXaMxaWVK6De91Ap_KL6WFtg,4916
|
|
59
|
-
bitsandbytes_cpu_fork-0.50.2.
|
|
60
|
-
bitsandbytes_cpu_fork-0.50.2.
|
|
61
|
-
bitsandbytes_cpu_fork-0.50.2.
|
|
62
|
-
bitsandbytes_cpu_fork-0.50.2.
|
|
63
|
-
bitsandbytes_cpu_fork-0.50.2.
|
|
64
|
-
bitsandbytes_cpu_fork-0.50.2.
|
|
59
|
+
bitsandbytes_cpu_fork-0.50.2.dev5.dist-info/licenses/LICENSE,sha256=UkEte8fOQVfqYou6rLiCngqcs8WPV_mRdhJryM8r_IU,1086
|
|
60
|
+
bitsandbytes_cpu_fork-0.50.2.dev5.dist-info/METADATA,sha256=T92ZHoDNh9Wb4aabyHdOzrnizF0ndnbLrfO2An0rARQ,19077
|
|
61
|
+
bitsandbytes_cpu_fork-0.50.2.dev5.dist-info/WHEEL,sha256=5s7VO_HUPfjwcO4LINqxGe8JSFZAGgaurQKPGA4cx_g,97
|
|
62
|
+
bitsandbytes_cpu_fork-0.50.2.dev5.dist-info/entry_points.txt,sha256=GqLu5Rm-j-9_CvhqX0jVecyJShA1XGmxRoPEiibJpcI,99
|
|
63
|
+
bitsandbytes_cpu_fork-0.50.2.dev5.dist-info/top_level.txt,sha256=l9ip7vjQq1JV6CB_ZGntWtjfxpkH119NDxupnJho9k0,34
|
|
64
|
+
bitsandbytes_cpu_fork-0.50.2.dev5.dist-info/RECORD,,
|
{bitsandbytes_cpu_fork-0.50.2.dev4.dist-info → bitsandbytes_cpu_fork-0.50.2.dev5.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|