HelloSun
勘誤:修正與實測資料不符的敘述
446c618
|
Raw History Blame Contribute Delete
7.71 kB

硬體與執行環境(實測)

這個 repo 的所有數字都是在下面這台機器上量的。這裡把「量到什麼」與 「怎麼重量」都寫出來,避免出現別處那種「16 threads 是核心數嗎 / GB 還是 GiB / RAM 預算是不是硬性上限」的歧義。

0. 一句話版本

CPU Intel Xeon Platinum 8559C,2 socket × 48 core × 2 thread = 192 logical CPU 可見
實際算力配額 cgroup v2 cpu.max = 1600000 100000 → 16.0 CPU
實體 RAM 2,000.0 GiB = 1.953 TiB(MemTotal 2,097,117,292 kB)
Swap 1,600.0 GiB = 1.5625 TiB(SwapTotal 1,677,721,504 kB)
cgroup v2 記憶體上限 memory.max = 104,000,000,000 B = 96.86 GiB / 104.0 GB
實際綁住本專案的上限 /root/memkill.py 80 2 64 → 單一子行程 64 GiB、opencode 及其全部子行程合計 80 GiB、取樣間隔 2 s
儲存 / 是 overlayfs,底層是 nvme0n1 = Amazon Elastic Block Store(網路區塊裝置,不是本地 NVMe SSD)7.8 TB
磁碟單流(dd bs=4M oflag=direct / iflag=direct) 寫 1.1 GB/s、讀 2.0 GB/s
cgroup 檔案系統 唯讀(/sys/fs/cgroup 寫入失敗,cgroup.subtree_control 為空)
ISA avx2 / fma / f16c / avx512f,dq,bw,vl,vbmi,ifma,bf16 / avx512_vnni / amx_int8, amx_bf16, amx_tile

1. CPU:三個數字不要搞混

這台機器上「16」這個數字有三個完全不同的來源,混用會得到錯的結論:

量測指令 結果 意義
lscpu | grep ^CPU(s) 192 實體 logical CPU 數(2×48×2)
python3 -c "len(os.sched_getaffinity(0))" 192 cgroup 沒有縮 CPU set
cat /sys/fs/cgroup/cpu.max 1600000 100000 配額 = 16.0 CPU(這才是真正限制你的東西)
nproc 16 ⚠️ 不是因為配額或 affinity,是因為環境變數 OMP_NUM_THREADS=16

nproc 會尊重 OMP_NUM_THREADS,所以在這台機器上 nproc 剛好等於配額是 巧合,不是因果。hardware_concurrency() 回的是 192(實體數), sched_getaffinity() 回的也是 192(沒有縮)—— 所以「容器裡 hardware_concurrency() 會回宿主核心數」這句話在這台機器上無法用觀察區分 「回實體數」和「回 affinity 數」,因為兩者剛好都是 192。

**本專案的實驗一律用 -t 16**(= 配額),n_threads_batch 相同。 -t 超過 16 只會讓執行緒排程互相搶時間,不會變快。

2. 記憶體:三個不同的上限

機制 值 是硬限制嗎 誰在用
cgroup v2 memory.max 96.86 GiB / 104.0 GB 是(OOM kill) 整個容器
memkill.py 單一子行程 64 GiB 是(SIGKILL,連同子樹) 每個 sdq-chat
memkill.py 合計(opencode + 全部子行程) 80 GiB 是(SIGKILL 最大的那個子樹) 整個工作階段
--ram-budget-mb(SDQ pager budget) 4/8/16/32 GiB 不是,只是算 arena 大小用的輸入 見下

memkill.py 的實際行為(讀 /root/memkill.py 確認): 每 2 秒掃一次 /proc,把 comm 以 opencode 開頭的行程當 root, 把 root 的所有後代加總 RSS;超過 80 GiB 就挑記憶體最大的那個直接子行程 連同它的子樹 SIGKILL;任何單一行程超過 64 GiB 也直接殺。 opencode 本體永遠不會被殺。

⚠️ 這意味著 peak RSS 的天花板是 64 GiB,不是 cgroup 的 96.86 GiB。 這也是為什麼這個 repo 沒有做「原生路徑 vs 分頁路徑」的端到端 A/B: 原生路徑要把 127.27 GiB 的權重載入 RSS,一定會被這支守衛砍掉。

為什麼 RAM 實驗只能是「pager budget」

/sys/fs/cgroup 是唯讀(echo 0 > memory.high 直接 Read-only file system), cgroup.subtree_control 是空的,所以沒辦法在這個容器裡為 4/8/16/32 GiB 各建一個獨立的 cgroup 限額。

因此 results/memory_levels_dsv4-4-8-16-32g.md 的四列是「把 RAM 預算交給 pager 去算 arena 大小」再量 peak RSS,不是四個各自受限的容器。 各列的實際 peak RSS 都如實列出(2233 / 6325 / 14508 / 30894 MB),可以直接 對照 memory.max,四列都沒有接近任何上限。

3. 儲存:這不是「SSD」

$ stat -fc %T /root          →  overlayfs
$ lsblk -o NAME,TYPE,SIZE,ROTA,MODEL      # 2026-10-12 實測
nvme0n1   disk   7.8T  0  Amazon Elastic Block Store     ← /  在這上面
nvme1n1   disk   3.5T  0  Amazon EC2 NVMe Instance Storage
nvme2n1   disk   3.5T  0  Amazon EC2 NVMe Instance Storage
nvme3n1   disk   3.5T  0  Amazon EC2 NVMe Instance Storage
nvme4n1   disk   3.5T  0  Amazon EC2 NVMe Instance Storage

附帶 instance storage 的磁碟編號會隨開機/熱插拔變動(nvme1n1–nvme4n1 之間, 這台機器上是 1、2、3、4 都在,md0 是其中的 RAID0)。只有 nvme0n1 對本專案 有意義,因為它才是 /(overlayfs)底下的那一顆;其餘都是 instance storage。

/ 是 overlayfs,底層是 Amazon Elastic Block Store(EBS)—— 那是網路-attached 區塊裝置,不是本地 NVMe SSD。 把指標命名為 SSD MiB/token 在這個環境下是有誤導性的: 它量到的是「這個容器讀檔案的速率」,不是 SSD 控制器速率。

本機實測單流(bs=4M):

寫   dd if=/dev/zero of=.ddtest bs=4M count=512 oflag=direct  → 1.1 GB/s
讀   dd if=.ddtest of=/dev/null bs=4M iflag=direct             → 2.0 GB/s

pager 用的是 O_DIRECT(SDQ_FADV_DONTNEED=1 + 每個 shard 各開一組 O_DIRECT fd),所以實測的 2.0 GB/s 是它真正能吃到的上限。--stats-file 報的 SSD MiB/tok 要拿這個數字去對照,不是拿消費級 SSD 的 7 GB/s 去對照。

4. ISA 與編譯

CPU 有 amx_int8 / amx_bf16 / amx_tile / avx512_vnni,但這個專案刻意 不使用 AMX,原因見 README 第 5 節 (e)(會讓 deepseek4 的 attn_wo_a 撞 sched assert)。所以:

  • -march=native 會定義 __AMX_INT8__ → llama.cpp 註冊 AMX extra buffer type → 踩到那個 assert。不要用 -march=native。
  • 正確做法是明確列 ISA(README 4.2),AMX 的 CMake 選項維持預設 OFF。
  • 三個 x86 基線(AVX2 / AVX512+VNNI+BF16 / native+-mno-amx-*)的 decode tok/s 在同一台機器上量不出差異(各自 3 輪落在 1.03–1.71 這個區間裡)。

K-quant / IQ3_S / MXFP4 的點積走的是 ggml_vec_dot_* 的 AVX2 / AVX-512-VNNI 路徑,不是 AMX。

5. 重現這些量測

# CPU / 配額
lscpu | grep -E '^CPU\(s\)|^Model name|^Thread|^Core|^Socket'
python3 -c "import os; print('affinity', len(os.sched_getaffinity(0)), 'cpu_count', os.cpu_count())"
nproc; echo "OMP_NUM_THREADS=$OMP_NUM_THREADS"
cat /sys/fs/cgroup/cpu.max

# 記憶體 / cgroup
grep -E 'MemTotal|SwapTotal' /proc/meminfo
for f in memory.max memory.high memory.swap.max; do printf '%s = %s\n' "$f" "$(cat /sys/fs/cgroup/$f)"; done
stat -fc %T /sys/fs/cgroup; cat /sys/fs/cgroup/cgroup.subtree_control
( echo 0 > /sys/fs/cgroup/memory.high ) 2>&1 | head -1   # 預期 Read-only

# 守衛
tr '\0' ' ' < /proc/$(pgrep -f memkill.py | head -1)/cmdline; echo
head -25 /root/memkill.py

# 儲存
stat -fc %T /root ; lsblk -o NAME,TYPE,SIZE,ROTA,MODEL
dd if=/dev/zero of=/tmp/.ddtest bs=4M count=512 oflag=direct 2>&1 | tail -1
dd if=/tmp/.ddtest of=/dev/null bs=4M iflag=direct 2>&1 | tail -1; rm -f /tmp/.ddtest

# ISA
lscpu | grep -oE '\b(avx512_vnni|amx_int8|amx_bf16|amx_tile|avx512f|avx2)\b' | sort -u