Text Generation
llama-cpp-python
GGUF
llama.cpp
Mixture of Experts
ssd-offload
smallthinker
expert-paging
low-ram
Instructions to use HelloSun/SmallThinker4b with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- llama-cpp-python
How to use HelloSun/SmallThinker4b with llama-cpp-python:
# !pip install llama-cpp-python from llama_cpp import Llama llm = Llama.from_pretrained( repo_id="HelloSun/SmallThinker4b", filename="{{GGUF_FILE}}", )output = llm( "Once upon a time,", max_tokens=512, echo=True ) print(output)
- Notebooks
- Google Colab
- Kaggle
Auto Upload Agent
v8: 修 io_probe 的 pread 計數 bug(SSD 337/667/1328/2558 MB/s)+ launcher arena 語意修正
84fbb3b Download llama_server.sh from HelloSun/SmallThinker4b: direct link, hf CLI and curl.
- Browser
- Download file 22.5 kB
-
https://huggingface.co/HelloSun/SmallThinker4b/resolve/main/llama_server.sh
- Command line
-
hf download hf://HelloSun/SmallThinker4b/llama_server.sh
-
curl -L -o llama_server.sh https://huggingface.co/HelloSun/SmallThinker4b/resolve/main/llama_server.sh
22.5 kB
| # ============================================================================== | |
| # 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[@]}" |