minicpm47woek_EPQ / tools /io_probe.py
HelloSun's picture
EPM: expert paging CPU inference for MiniCPM-V-4.7-35B-A3B Q4_K_M
12eb3a5 verified
Raw History Blame Contribute Delete
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())