SmallThinker4b / llama_server.sh
Auto Upload Agent
v8: 修 io_probe 的 pread 計數 bug(SSD 337/667/1328/2558 MB/s)+ launcher arena 語意修正
84fbb3b
Raw History Blame Contribute Delete
22.5 kB
#!/usr/bin/env bash
# ==============================================================================
# SmallThinker-4B-A0.6B-Instruct — 熱參數在 RAM、冷參數在 SSD 的啟動器
#
# ./llama_server.sh # port 8080
# ./llama_server.sh --port 9000 # 換 port
# ./llama_server.sh --plan # 只印推導出來的參數,不動任何東西
# ./llama_server.sh --verify # 啟動後打一次 /v1/chat/completions 並量記憶體
#
# 這個檔案假設「環境還沒建立」:什麼都沒有時,它會自己抓 llama.cpp、套 patch、
# 編譯、把權重抓回來,最後啟動 llama-server。已經建立好的話就跳過對應步驟。
#
# 三個必須先講清楚的前提(不然這個設計看起來很怪):
#
# 1. **模型與量化不變。** 權重永遠是 Tiiny/SmallThinker-4BA0.6B-Instruct-GGUF 的
# SmallThinker-4B-A0.6B-Instruct.Q4_K.gguf,2630212704 bytes(2.45 GiB),
# 永遠只做 SSD 上的 file-backed mmap。RAM 少的機器**不會**換一個小一點的 GGUF。
#
# 2. **跟著機器變的是「RAM 放得下多少權重」。** RAM 越大 → arena 越大 → 命中率越高
# → SSD 讀取越少。分頁器自己會算,這裡只需要把 RAM 預算餵對(ST_RAM_BUDGET_MB)。
#
# 3. **不打開 swap。** 匿名記憶體超出預算就是 crash,而不是安靜地 swap 掉。
# ==============================================================================
set -uo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
# ------------------------------------------------------------------ 設定
# 這些是「這個模型的事實」,不是可以調的參數;要改改這裡,不要在呼叫時傳參數。
HF_REPO="${ST_HF_REPO:-HelloSun/SmallThinker4b}"
LLAMA_REPO="${ST_LLAMA_REPO:-https://github.com/ggml-org/llama.cpp.git}"
# patch 是針對這個 commit 生成的。換 commit 一定要重新產生 patch。
LLAMA_COMMIT="${ST_LLAMA_COMMIT:-b9acf138a1e28ce1fc23b5a4fc4b12444b50f7ea}"
MODEL_REPO="Tiiny/SmallThinker-4BA0.6B-Instruct-GGUF"
MODEL_FILE="SmallThinker-4B-A0.6B-Instruct.Q4_K.gguf"
MODEL_SIZE=2630212704 # bytes,驗證下載用
MODEL_ARCH="smallthinker"
N_LAYER=32 # smallthinker.block_count
N_EMBD=1536 # smallthinker.embedding_length
N_EXPERT=32 # smallthinker.expert_count
N_EXPERT_USED=4 # smallthinker.expert_used_count
EXPERT_FFN=768 # smallthinker.expert_feed_forward_length
N_HEAD=12 # smallthinker.attention.head_count
N_HEAD_KV=2 # smallthinker.attention.head_count_kv
KEY_LEN=128 # smallthinker.attention.key_length
VAL_LEN=128 # smallthinker.attention.value_length
MODEL_MAX_CTX=32768 # smallthinker.context_length
# 單一 expert 權重 = gate + up + down 三段,元素數 3 * N_EMBD * EXPERT_FFN。
# Q4_K 每元素實際約 0.5625 B/elem(GGUF metadata 實測),這裡保守用 0.6。
EXPERT_ELEMS=$(( 3 * N_EMBD * EXPERT_FFN ))
EXPERT_BYTES=$(( EXPERT_ELEMS * 6 / 10 ))
EXPERT_TOTAL_MIB=$(( N_LAYER * N_EXPERT * EXPERT_BYTES / 1048576 ))
# 工作目錄:原始碼、patch、模型、執行檔都放這裡。
WORK_DIR="${ST_HOME:-${XDG_CACHE_HOME:-$HOME/.cache}/smallthinker4b}"
SRC_DIR="$WORK_DIR/llama.cpp"
BIN_DIR="$WORK_DIR/bin"
MODEL_DIR="${ST_MODEL_DIR:-$WORK_DIR/models}"
PATCH_DIR="$SCRIPT_DIR/patches"
LOG_DIR="$WORK_DIR/logs"
# ------------------------------------------------------------------ 參數
PORT=8080
PLAN_ONLY=0
VERIFY=0
while [ $# -gt 0 ]; do
case "$1" in
--port) PORT="${2:?--port 需要一個數字}"; shift ;;
--plan) PLAN_ONLY=1 ;;
--verify) VERIFY=1 ;;
-h|--help) sed -n '2,20p' "$0"; exit 0 ;;
*) die() { printf '[st4b] 未知參數:%s(只有 --port / --plan / --verify)\n' "$1" >&2; exit 2; }; die "$1" ;;
esac
shift
done
say() { printf '[st4b] %s\n' "$*" >&2; }
step() { printf '\033[1;34m==>\033[0m %s\n' "$*" >&2; }
die() { printf '[st4b] ERROR: %s\n' "$*" >&2; exit 1; }
command -v git >/dev/null || die "需要 git"
command -v cmake >/dev/null || die "需要 cmake(apt install cmake build-essential)"
command -v curl >/dev/null || die "需要 curl"
command -v python3 >/dev/null || die "需要 python3"
# ======================================================== 1. 偵測系統資源
# 放在最前面:後面所有步驟(尤其是 RAM 預算)都要用到。
MIB=1048576
meminfo_kb() {
awk -v k="$1:" '$1==k {print $2; exit} END{}' /proc/meminfo 2>/dev/null || echo 0
}
cgroup_mem_limit() {
local v
if [ -r /sys/fs/cgroup/memory.max ]; then
v=$(tr -d '[:space:]' </sys/fs/cgroup/memory.max)
case "$v" in ''|max) ;; *) printf '%s' "$v"; return 0 ;; esac
fi
if [ -r /sys/fs/cgroup/memory/memory.limit_in_bytes ]; then
v=$(tr -d '[:space:]' </sys/fs/cgroup/memory/memory.limit_in_bytes)
if [ -n "$v" ] && [ "$v" -lt 4611686018427387904 ] 2>/dev/null; then
printf '%s' "$v"; return 0
fi
fi
return 1
}
cgroup_mem_used() {
if [ -r /sys/fs/cgroup/memory.current ]; then tr -d '[:space:]' </sys/fs/cgroup/memory.current
elif [ -r /sys/fs/cgroup/memory/memory.usage_in_bytes ]; then tr -d '[:space:]' </sys/fs/cgroup/memory/memory.usage_in_bytes
else echo 0
fi
}
cgroup_cpu_quota() {
local q p
if [ -r /sys/fs/cgroup/cpu.max ]; then
read -r q p < /sys/fs/cgroup/cpu.max
if [ "$q" != "max" ] && [ -n "${p:-}" ] && [ "$p" -gt 0 ] 2>/dev/null; then
echo $(( q / p )); return 0
fi
return 1
fi
return 1
}
affinity_cpus() { nproc 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1; }
gpu_mb() {
command -v nvidia-smi >/dev/null 2>&1 || { echo 0; return; }
nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null \
| awk '{s+=$1} END{print s+0}'
}
# --- RAM -------------------------------------------------------------------
# 容器裡 /proc/meminfo 的 MemAvailable 是**宿主**的餘量,直接用會高估,
# 必須跟 cgroup 上限取較小值。這個坑在 sddqwen35a3b_v01 踩過一次。
AVAIL_KB=$(meminfo_kb MemAvailable)
[ "${AVAIL_KB:-0}" -gt 0 ] || AVAIL_KB=$(meminfo_kb MemFree)
AVAIL_BYTES=$(( ${AVAIL_KB:-0} * 1024 ))
if LIMIT=$(cgroup_mem_limit); then
FREE=$(( LIMIT - $(cgroup_mem_used) ))
[ "$FREE" -lt 0 ] 2>/dev/null && FREE=0
AVAIL_BYTES=$(( AVAIL_BYTES < FREE ? AVAIL_BYTES : FREE ))
fi
[ "$AVAIL_BYTES" -gt 0 ] || die "讀不到可用的 RAM"
USABLE_MB=$(( AVAIL_BYTES / MIB ))
CGROUP_MB=$( { cgroup_mem_limit || echo 0; } | awk -v m=$MIB '{printf "%d", $1/m}')
# --- CPU -------------------------------------------------------------------
CPUS=$(affinity_cpus)
QUOTA=$(cgroup_cpu_quota || true)
[ -n "${QUOTA:-}" ] && [ "$QUOTA" -lt "$CPUS" ] 2>/dev/null && CPUS="$QUOTA"
# SmallThinker 每層每 token 只有 n_expert_used=4 個 expert 可以平行,
# threads 超過 4 之後只是多開執行緒搶同一批 CPU。
THREADS=$(( CPUS > 8 ? 8 : CPUS ))
[ "$THREADS" -lt 1 ] && THREADS=1
GPU_MB=$(gpu_mb)
# --- 推導啟動參數 -----------------------------------------------------------
# ST_RAM_BUDGET_MB 可以手動壓低做實驗(冷權重真的留在 SSD);沒給就自動偵測。
CTX=${ST_CTX:-4096}
UBATCH=${ST_UBATCH:-512}
# KV:每個 token = N_LAYER * N_HEAD_KV * (KEY_LEN + VAL_LEN) 個元素,f16 = 2 bytes。
KV_ELEMS=$(( N_LAYER * N_HEAD_KV * (KEY_LEN + VAL_LEN) ))
KV_ESTIMATE_MB=$(( CTX * KV_ELEMS * 2 / MIB ))
KV_RESERVE_MB=${ST_KV_RESERVE_MB:-$KV_ESTIMATE_MB}
[ "$KV_RESERVE_MB" -lt "$KV_ESTIMATE_MB" ] && KV_RESERVE_MB="$KV_ESTIMATE_MB"
COMPUTE_RESERVE_MB=${ST_COMPUTE_RESERVE_MB:-$(( UBATCH * 2 + 800 ))}
RESERVE_MB=$(( KV_RESERVE_MB + COMPUTE_RESERVE_MB ))
# 非 expert 的權重(embedding / attention / norms / router)必須常駐 RAM,
# 否則每 token 都要重讀。粗估 = 模型總大小 − expert 總量。
NON_EXPERT_MIB=$(( $(stat -c %s "$MODEL_DIR/$MODEL_FILE" 2>/dev/null || echo $MODEL_SIZE) / MIB - EXPERT_TOTAL_MIB ))
[ "$NON_EXPERT_MIB" -lt 0 ] && NON_EXPERT_MIB=0
if [ -n "${ST_RAM_BUDGET_MB:-}" ]; then
RAM_BUDGET_MB=$ST_RAM_BUDGET_MB
BUDGET_SRC="ST_RAM_BUDGET_MB(手動)"
else
# 自動:可用 RAM 扣掉系統保留就是模型預算。
SYSTEM_RESERVE_MB=${ST_SYSTEM_RESERVE_MB:-2048}
RAM_BUDGET_MB=$(( USABLE_MB - SYSTEM_RESERVE_MB ))
[ "$RAM_BUDGET_MB" -gt 0 ] || die "可用 RAM ${USABLE_MB} MiB 不足系統保留 ${SYSTEM_RESERVE_MB} MiB"
BUDGET_SRC="自動偵測(可用 ${USABLE_MB} − 系統保留 ${SYSTEM_RESERVE_MB})"
fi
# arena = 預算 − 非 expert 權重(常駐) − KV − compute
ARENA_MB=$(( RAM_BUDGET_MB - NON_EXPERT_MIB - RESERVE_MB ))
PAGER_BUDGET_NOTE=""
if [ "$ARENA_MB" -gt 0 ]; then
PAGER_BUDGET_MB="$ARENA_MB"
else
# 預算已經不足以同時負擔「非 expert 權重 + KV + compute」→ 讓分頁器
# 用整個預算當 expert 額度。這不是繞過,而是因為此時 KV/compute
# 根本不可能用到那麼多,算式本身失去意義。
PAGER_BUDGET_MB="$RAM_BUDGET_MB"
PAGER_BUDGET_NOTE="(預算不足以扣掉常駐權重與 KV/compute,故用整個預算)"
fi
EXPERT_SLOTS=$(( PAGER_BUDGET_MB * MIB / EXPERT_BYTES ))
say "RAM 可用 ${USABLE_MB} MiB(cgroup 上限 ${CGROUP_MB} MiB)"
say "CPU ${THREADS} threads(配額 ${QUOTA:-無}/可用核心 $(affinity_cpus))、GPU ${GPU_MB} MiB"
say "模型結構:${N_LAYER} 層 × ${N_EXPERT} experts(每 token 用 ${N_EXPERT_USED}),單一 expert ≈ $(( EXPERT_BYTES / 1024 )) KiB"
say "RAM 預算 ${RAM_BUDGET_MB} MiB(來源:${BUDGET_SRC})"
say "推導:非 expert 權重=${NON_EXPERT_MIB} MiB(常駐) KV=${KV_RESERVE_MB} MiB compute=${COMPUTE_RESERVE_MB} MiB"
say "可配置 arena=${ARENA_MB} MiB → 實際分頁預算 ${PAGER_BUDGET_MB} MiB(約 ${EXPERT_SLOTS} 個 expert 槽;expert 總量 ${EXPERT_TOTAL_MIB} MiB)"
refresh_memory_budget() {
# Build/download 可能改變 page-cache/cgroup 用量,啟動前一定要重新讀一次。
local avail_kb avail_bytes limit free
avail_kb=$(meminfo_kb MemAvailable)
[ "${avail_kb:-0}" -gt 0 ] || avail_kb=$(meminfo_kb MemFree)
avail_bytes=$(( ${avail_kb:-0} * 1024 ))
if limit=$(cgroup_mem_limit); then
free=$(( limit - $(cgroup_mem_used) ))
[ "$free" -lt 0 ] 2>/dev/null && free=0
avail_bytes=$(( avail_bytes < free ? avail_bytes : free ))
CGROUP_MB=$(( limit / MIB ))
fi
[ "$avail_bytes" -gt 0 ] || die "啟動前讀不到可用 RAM"
USABLE_MB=$(( avail_bytes / MIB ))
if [ -z "${ST_RAM_BUDGET_MB:-}" ]; then
SYSTEM_RESERVE_MB=${ST_SYSTEM_RESERVE_MB:-2048}
RAM_BUDGET_MB=$(( USABLE_MB - SYSTEM_RESERVE_MB ))
[ "$RAM_BUDGET_MB" -gt 0 ] || die "啟動前可用 RAM ${USABLE_MB} MiB 不足系統保留 ${SYSTEM_RESERVE_MB} MiB"
BUDGET_SRC="自動偵測(可用 ${USABLE_MB} − 系統保留 ${SYSTEM_RESERVE_MB})"
fi
ARENA_MB=$(( RAM_BUDGET_MB - NON_EXPERT_MIB - RESERVE_MB ))
if [ "$ARENA_MB" -gt 0 ]; then
PAGER_BUDGET_MB="$ARENA_MB"
else
PAGER_BUDGET_MB="$RAM_BUDGET_MB"
fi
EXPERT_SLOTS=$(( PAGER_BUDGET_MB * MIB / EXPERT_BYTES ))
[ "$EXPERT_SLOTS" -lt 0 ] && EXPERT_SLOTS=0
say "啟動前重新偵測:可用 RAM=${USABLE_MB} MiB,預算=${RAM_BUDGET_MB} MiB,arena=${ARENA_MB} MiB"
}
# ============================================ 2. 取得 patch(自我安裝)
hf_get() { # hf_get <repo> <path> <dest>
local url="https://huggingface.co/$1/resolve/main/$2"
mkdir -p "$(dirname "$3")"
curl -fsSL -H "Authorization: Bearer ${HF_TOKEN:-}" -o "$3" "$url" \
|| die "抓不到 $2(請確認 $HF_REPO 有這個檔案,或把 patches/ 放在腳本旁邊)"
}
ensure_patches() {
mkdir -p "$PATCH_DIR"
if [ -n "$(ls -A "$PATCH_DIR"/*.patch 2>/dev/null)" ]; then
say "使用本地的 patch:$(cd "$PATCH_DIR" && ls *.patch | tr '\n' ' ')"
return
fi
# 遠端還沒有 patch(第一版就是這樣)是正常狀況,不是錯誤:
# 沒有 patch 時走上游行為,權重一樣是 SSD 上的 file-backed mmap。
step "從 $HF_REPO 取得 patch"
local p got=0
for p in 0001-st-expert-pager.patch; do
if curl -fsSL -H "Authorization: Bearer ${HF_TOKEN:-}" -o "$PATCH_DIR/$p.part" \
"https://huggingface.co/$HF_REPO/resolve/main/patches/$p" 2>/dev/null; then
mv -f "$PATCH_DIR/$p.part" "$PATCH_DIR/$p"; say " patches/$p"; got=1
else
rm -f "$PATCH_DIR/$p.part"; say " (遠端沒有 $p)"
fi
done
[ "$got" = 1 ] || say " 用上游行為:權重走 SSD file-backed mmap,沒有 expert 級分頁"
}
# ================================================== 3. 取得 llama.cpp 原始碼
ensure_source() {
if [ -d "$SRC_DIR/.git" ]; then
local have
have=$(git -C "$SRC_DIR" rev-parse HEAD 2>/dev/null || echo none)
if [ "$have" = "$LLAMA_COMMIT" ] && [ -f "$SRC_DIR/.st-patched" -o -f "$SRC_DIR/.st-unpatched" ]; then
say "llama.cpp 已經在 $LLAMA_COMMIT 且已處理 patch"
return
fi
fi
step "取得 llama.cpp $LLAMA_COMMIT"
mkdir -p "$WORK_DIR"
if [ ! -d "$SRC_DIR/.git" ]; then
rm -rf "$SRC_DIR"
git clone -q --filter=blob:none "$LLAMA_REPO" "$SRC_DIR" \
|| die "git clone 失敗(檢查網路)"
fi
git -C "$SRC_DIR" fetch -q --depth 1 origin "$LLAMA_COMMIT" 2>/dev/null || true
git -C "$SRC_DIR" checkout -q --force --detach "$LLAMA_COMMIT" \
|| die "拿不到 commit $LLAMA_COMMIT"
git -C "$SRC_DIR" reset -q --hard
rm -f "$SRC_DIR/.st-patched" "$SRC_DIR/.st-unpatched"
}
apply_patches() {
ensure_patches
[ -f "$SRC_DIR/.st-patched" ] && { say "patch 已套用"; return; }
local any=0
for p in "$PATCH_DIR"/*.patch; do
[ -e "$p" ] || continue
any=1
step "套用 $(basename "$p")"
# 已經套過就直接跳過(避免重跑時 patch 報錯)
if git -C "$SRC_DIR" apply --reverse --check "$p" >/dev/null 2>&1; then
say " 已套用,略過"
continue
fi
git -C "$SRC_DIR" apply --check "$p" 2>/dev/null \
|| die "$(basename "$p") 套不上 llama.cpp $LLAMA_COMMIT(patch 過期了,要重新產生)"
git -C "$SRC_DIR" apply "$p" || die "$(basename "$p") 套用失敗"
say " $(basename "$p")"
done
if [ "$any" = 1 ]; then touch "$SRC_DIR/.st-patched"; else touch "$SRC_DIR/.st-unpatched"; fi
}
# ============================================================= 4. 編譯
build_if_needed() {
local server="$SRC_DIR/build/bin/llama-server"
if [ -x "$server" ] && { [ -f "$SRC_DIR/.st-patched" ] || [ -f "$SRC_DIR/.st-unpatched" ]; }; then
if [ -z "$(find "$SRC_DIR/src" -newer "$server" -name '*.cpp' -o -newer "$server" -name '*.h' 2>/dev/null | head -1)" ]; then
say "已經編譯好:$server"
return
fi
fi
step "編譯 llama-server(第一次會比較久)"
local jobs="${ST_BUILD_JOBS:-$(nproc 2>/dev/null || echo 2)}"
local gen=()
command -v ninja >/dev/null && gen=(-G Ninja)
mkdir -p "$LOG_DIR"
# 不開 -march=native:編譯機與執行機不一定同一台,native 會在別的機器 SIGILL。
# -DGGML_CPU_REPACK=OFF 是硬性要求,不能拿掉:
# 開著時 ggml 會把 Q4_K 權重轉成 repack 格式放進**匿名**緩衝區,
# 權重就整個離開 mmap。這時對它 madvise(MADV_DONTNEED) 不是「讀 SSD」,
# 而是**把那塊記憶體歸零** → 輸出變亂碼,但行程看起來完全正常。
# (這個坑真的踩過,症狀是「輸出變成 'papers bases abstract' 之類的亂碼」,
# 而且 tok/s 只掉一點點 —— 非常不像出錯。st_pager.cpp 也有 in_map 檢查擋著。)
cmake "${gen[@]}" -S "$SRC_DIR" -B "$SRC_DIR/build" \
-DCMAKE_BUILD_TYPE=Release -DGGML_NATIVE=OFF -DGGML_CPU_REPACK=OFF \
-DGGML_CURL=OFF -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=OFF \
-DLLAMA_BUILD_SERVER=ON -DLLAMA_BUILD_TOOLS=ON \
>"$LOG_DIR/cmake.log" 2>&1 || die "cmake 失敗,見 $LOG_DIR/cmake.log"
cmake --build "$SRC_DIR/build" --target llama-server -j "$jobs" \
>"$LOG_DIR/build.log" 2>&1 || die "編譯失敗,見 $LOG_DIR/build.log"
[ -x "$server" ] || die "編譯完但找不到 llama-server"
mkdir -p "$BIN_DIR"
cp -f "$server" "$BIN_DIR/llama-server"
say "編譯完成:$BIN_DIR/llama-server"
}
# ====================================================== 5. 下載模型權重
ensure_model() {
mkdir -p "$MODEL_DIR"
local path="$MODEL_DIR/$MODEL_FILE"
if [ -f "$path" ]; then
local have
have=$(stat -c %s "$path")
if [ "$have" = "$MODEL_SIZE" ]; then
say "模型已經在硬碟上($(( MODEL_SIZE / MIB )) MiB)"
printf '%s' "$path"; return
fi
say "模型檔大小不對($have,應該是 $MODEL_SIZE),重新下載"
rm -f "$path"
fi
step "下載模型 $MODEL_FILE($(( MODEL_SIZE / MIB )) MiB,可續傳)"
curl -fL -C - --retry 10 --retry-delay 5 --retry-all-errors \
-H "Authorization: Bearer ${HF_TOKEN:-}" \
-o "$path.part" \
"https://huggingface.co/$MODEL_REPO/resolve/main/$MODEL_FILE" \
|| die "下載失敗,再跑一次會從斷點繼續"
local got
got=$(stat -c %s "$path.part")
[ "$got" = "$MODEL_SIZE" ] || die "大小不符:$got != $MODEL_SIZE(再跑一次續傳)"
mv -f "$path.part" "$path"
printf '%s' "$path"
}
# =========================================================== 6. 啟動參數
build_args() {
SERVER_ARGS=(
--model "$MODEL_PATH"
--host 0.0.0.0
--port "$PORT"
-t "$THREADS" # decode
-tb "$THREADS" # prefill
-b "$UBATCH" # n_ubatch
-c "$CTX"
--jinja # 用 GGUF 內建的 chat template
--cache-reuse 256 # 多輪對話重用前綴
# 權重永遠走 file-backed mmap(不用 --mlock),所以 RAM 只放 arena。
# 分頁器的參數由 ST_* 環境變數帶進去(patch 0001)。
)
}
plan_report() {
cat >&2 <<EOF
設定摘要
─────────────────────────────────────────────
模型 $MODEL_FILE($(( MODEL_SIZE / MIB )) MiB,只在 SSD)
量化 Q4_K(不隨 RAM 改變)
架構 ${MODEL_ARCH} ${N_LAYER} 層 hidden ${N_EMBD} ${N_EXPERT} experts(用 ${N_EXPERT_USED})
RAM 可用 ${USABLE_MB} MiB / cgroup 上限 ${CGROUP_MB} MiB
RAM 預算 ${RAM_BUDGET_MB} MiB(${BUDGET_SRC})
非expert權重 ${NON_EXPERT_MIB} MiB(常駐 RAM)
KV 預留 ${KV_RESERVE_MB} MiB(ctx $CTX,f16;理論值 $KV_ESTIMATE_MB MiB,${KV_ELEMS} 元素/token)
compute ${COMPUTE_RESERVE_MB} MiB
arena ${ARENA_MB} MiB → 約 ${EXPERT_SLOTS} 個 expert 槽(expert 總量 ${EXPERT_TOTAL_MIB} MiB)
分頁預算 實際給分頁器的額度 ${PAGER_BUDGET_MB} MiB${PAGER_BUDGET_NOTE}
threads $THREADS(配額 ${QUOTA:-無}/可用核心 $(affinity_cpus))
ubatch $UBATCH ctx $CTX(模型上限 $MODEL_MAX_CTX)
GPU ${GPU_MB} MiB
分頁 ST_PAGER=${ST_PAGER:-1}(0 = 關閉,走上游行為)
expert 槽 $(( N_LAYER * N_EXPERT )) 個 × $(( EXPERT_BYTES / 1024 )) KiB = ${EXPERT_TOTAL_MIB} MiB
監聽 http://0.0.0.0:$PORT
環境 ST_RAM_BUDGET_MB=$RAM_BUDGET_MB ST_RESERVE_MB=$RESERVE_MB \
ST_KV_RESERVE_MB=$KV_RESERVE_MB ST_THREADS=$THREADS ST_UBATCH=$UBATCH \
ST_CTX=$CTX ST_IO_THREADS=$(( THREADS * 3 )) ST_PREFETCH=0
指令 ${SERVER_ARGS[*]}
─────────────────────────────────────────────
EOF
}
mkdir -p "$LOG_DIR"
MODEL_PATH=""
if [ "$PLAN_ONLY" = 0 ]; then
ensure_source
apply_patches
build_if_needed
MODEL_PATH=$(ensure_model)
else
MODEL_PATH="$MODEL_DIR/$MODEL_FILE"
[ -x "$SRC_DIR/build/bin/llama-server" ] || say "尚未編譯(--plan 不會替你編)"
fi
[ -n "$MODEL_PATH" ] || MODEL_PATH="$MODEL_DIR/$MODEL_FILE"
refresh_memory_budget
build_args
plan_report
if [ "$PLAN_ONLY" = 1 ]; then
exit 0
fi
[ -f "$MODEL_PATH" ] || die "找不到模型 $MODEL_PATH"
SERVER="$SRC_DIR/build/bin/llama-server"
[ -x "$SERVER" ] || die "找不到 $SERVER"
# 啟動前丟掉模型檔的 page cache,否則第一次量到的 SSD 讀取量會偏低。
if [ -x "$SCRIPT_DIR/tools/drop_model_cache.py" ]; then
python3 "$SCRIPT_DIR/tools/drop_model_cache.py" "$MODEL_PATH" >/dev/null 2>&1 || true
fi
export ST_RAM_BUDGET_MB="$RAM_BUDGET_MB"
export ST_RESERVE_MB="$RESERVE_MB"
# 只有算得出正數才匯出:st_pager 看到 0 會忽略它,改用
# ST_RAM_BUDGET_MB − ST_RESERVE_MB。手動把預算壓到比「非 expert 權重 + KV +
# compute」還小時,arena 必然是 0,這時讓分頁器用整個預算當 expert 額度
# 反而是對的(此時 KV/compute 根本不可能有那麼多)。
if [ "$ARENA_MB" -gt 0 ]; then
export ST_ARENA_MB="$ARENA_MB"
else
unset ST_ARENA_MB
fi
export ST_STATS_FILE="${ST_STATS_FILE:-$WORK_DIR/st-stats.json}"
export ST_KV_RESERVE_MB="$KV_RESERVE_MB"
export ST_THREADS="$THREADS"
export ST_UBATCH="$UBATCH"
export ST_CTX="$CTX"
export ST_IO_THREADS=$(( THREADS * 3 ))
[ "$ST_IO_THREADS" -gt 48 ] && ST_IO_THREADS=48
step "啟動 llama-server(port $PORT)"
if [ "$VERIFY" = 1 ]; then
"$SERVER" "${SERVER_ARGS[@]}" &
SRV_PID=$!
trap 'kill "$SRV_PID" 2>/dev/null' EXIT
for _ in $(seq 1 180); do
curl -fsS "http://127.0.0.1:$PORT/health" >/dev/null 2>&1 && break
sleep 2
done
python3 "$SCRIPT_DIR/tools/verify_run.py" --port "$PORT" \
--pid "$SRV_PID" --model "$MODEL_PATH" --out "$SCRIPT_DIR/validate/last-run.json" \
--ram-budget-mb "$PAGER_BUDGET_MB" --arena-mb "$ARENA_MB" --expert-slots "$EXPERT_SLOTS"
exit $?
fi
exec "$SERVER" "${SERVER_ARGS[@]}"