Download tools/io_probe.py from HelloSun/minicpm47woek_EPQ: direct link, hf CLI and curl.
- Browser
- Download file 4.33 kB
-
https://huggingface.co/HelloSun/minicpm47woek_EPQ/resolve/main/tools/io_probe.py
- Command line
-
hf download hf://HelloSun/minicpm47woek_EPQ/tools/io_probe.py
-
curl -L -o io_probe.py https://huggingface.co/HelloSun/minicpm47woek_EPQ/resolve/main/tools/io_probe.py
4.33 kB
| #!/usr/bin/env python3 | |
| """量測「這個儲存裝置到底能有多快」——本專案所有 I/O 最佳化的前提。 | |
| 為什麼需要這個工具:分頁引擎的每一層都要等 SSD。如果不知道單次讀取的延遲與 | |
| 頻寬,就只能靠猜(例如「多開執行緒會更快」)。實測結果常常相反: | |
| **單一 1.9 MB 隨機讀約 7 ms,而且執行緒再多也不會變快**(見下方輸出), | |
| 因為瓶頸是單流頻寬(約 275 MB/s),不是延遲也不是佇列深度。這正是 | |
| 「I/O 段平行」這個最佳化能生效的原因:它不是開更多執行緒,而是把 | |
| 「一個 expert 的三段循序讀」變成「同時讀」。 | |
| 用法 | |
| ---- | |
| python tools/io_probe.py --model /path/to/model.gguf | |
| python tools/io_probe.py --model xxx.gguf --chunk 1900544 --parallel 1,2,4,8,16,24 | |
| 輸出 | |
| ---- | |
| 每個併發度一列:median / mean / p90 的單次讀取延遲,以及整體聚合頻寬。 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import os | |
| import random | |
| import statistics | |
| import threading | |
| import time | |
| PAGE = os.sysconf("SC_PAGE_SIZE") | |
| def drop_cache(fd: int, off: int, length: int) -> None: | |
| """把剛讀過的範圍從 kernel page cache 丟掉,否則下一輪量到的是 RAM 不是 SSD。""" | |
| start = off - (off % PAGE) | |
| end = ((off + length + PAGE - 1) // PAGE) * PAGE | |
| try: | |
| os.posix_fadvise(fd, start, end - start, os.POSIX_FADV_DONTNEED) | |
| except OSError: | |
| pass | |
| def measure(fd: int, size: int, chunk: int, parallel: int, n: int) -> dict: | |
| random.seed(11) | |
| max_off = max(1, size - chunk - PAGE) | |
| offsets = [random.randrange(0, max_off) for _ in range(n)] | |
| bufs = [bytearray(chunk) for _ in range(parallel)] | |
| idx = [0] | |
| lock = threading.Lock() | |
| lat: list[float] = [] | |
| def work(tid: int) -> None: | |
| local: list[float] = [] | |
| while True: | |
| with lock: | |
| i = idx[0] | |
| if i >= len(offsets): | |
| break | |
| idx[0] += 1 | |
| off = offsets[i] | |
| mv = memoryview(bufs[tid]) | |
| t0 = time.perf_counter() | |
| os.preadv(fd, [mv], off) | |
| local.append((time.perf_counter() - t0) * 1000.0) | |
| drop_cache(fd, off, chunk) | |
| with lock: | |
| lat.extend(local) | |
| ths = [threading.Thread(target=work, args=(i,)) for i in range(parallel)] | |
| t_start = time.perf_counter() | |
| for t in ths: | |
| t.start() | |
| for t in ths: | |
| t.join() | |
| wall = time.perf_counter() - t_start | |
| lat.sort() | |
| return { | |
| "parallel": parallel, | |
| "n": len(lat), | |
| "median_ms": round(statistics.median(lat), 2), | |
| "mean_ms": round(statistics.mean(lat), 2), | |
| "p90_ms": round(lat[int(len(lat) * 0.9)], 2), | |
| "wall_s": round(wall, 3), | |
| "agg_MBps": round(len(lat) * chunk / wall / 1e6, 1), | |
| } | |
| def main() -> int: | |
| ap = argparse.ArgumentParser(description="SDQ 儲存裝置 I/O 特性量測") | |
| ap.add_argument("--model", required=True, help="拿來當讀取目標的檔案(通常就是 gguf)") | |
| ap.add_argument("--chunk", type=int, default=1_900_000, help="單次讀取的位元組數") | |
| ap.add_argument("--parallel", default="1,2,4,8,16,24", help="要量哪幾種併發度") | |
| ap.add_argument("--n", type=int, default=64, help="每個併發度讀幾次") | |
| args = ap.parse_args() | |
| size = os.path.getsize(args.model) | |
| fd = os.open(args.model, os.O_RDONLY) | |
| print(f"檔案:{args.model}({size / 2**30:.1f} GiB)") | |
| print(f"單次讀取 {args.chunk} B,每個併發度 {args.n} 次") | |
| print() | |
| print(f"{'併發':>5} {'中位延遲':>10} {'平均':>8} {'p90':>8} {'聚合頻寬':>12}") | |
| try: | |
| for p in [int(x) for x in args.parallel.split(",")]: | |
| r = measure(fd, size, args.chunk, p, args.n) | |
| print(f"{r['parallel']:5d} {r['median_ms']:9.2f}ms {r['mean_ms']:7.2f}ms " | |
| f"{r['p90_ms']:7.2f}ms {r['agg_MBps']:9.1f} MB/s") | |
| finally: | |
| os.close(fd) | |
| print() | |
| print("讀法:如果『中位延遲』不隨併發度下降,代表單次讀取是頻寬受限的;") | |
| print(" 這時候開更多執行緒只會讓聚合頻寬變高,不會縮短關鍵路徑。") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) |