File size: 7,706 Bytes
ae9bb4c 446c618 ae9bb4c 446c618 ae9bb4c 446c618 ae9bb4c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | # 硬體與執行環境(實測)
這個 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
``` |