# 硬體與執行環境(實測) 這個 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. 重現這些量測 ```bash # 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 ```