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 commited on
Commit ·
d2defb7
1
Parent(s): 625b02c
v2: expert 級 SSD 分頁(patches/0001)— 熱在 RAM 冷在 SSD,輸出與上游一致
Browse files- llama_server.sh +11 -1
- patches/0001-st-expert-pager.patch +695 -0
llama_server.sh
CHANGED
|
@@ -318,8 +318,14 @@ build_if_needed() {
|
|
| 318 |
command -v ninja >/dev/null && gen=(-G Ninja)
|
| 319 |
mkdir -p "$LOG_DIR"
|
| 320 |
# 不開 -march=native:編譯機與執行機不一定同一台,native 會在別的機器 SIGILL。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
cmake "${gen[@]}" -S "$SRC_DIR" -B "$SRC_DIR/build" \
|
| 322 |
-
-DCMAKE_BUILD_TYPE=Release -DGGML_NATIVE=OFF \
|
| 323 |
-DGGML_CURL=OFF -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=OFF \
|
| 324 |
-DLLAMA_BUILD_SERVER=ON -DLLAMA_BUILD_TOOLS=ON \
|
| 325 |
>"$LOG_DIR/cmake.log" 2>&1 || die "cmake 失敗,見 $LOG_DIR/cmake.log"
|
|
@@ -395,6 +401,8 @@ plan_report() {
|
|
| 395 |
threads $THREADS(配額 ${QUOTA:-無}/可用核心 $(affinity_cpus))
|
| 396 |
ubatch $UBATCH ctx $CTX(模型上限 $MODEL_MAX_CTX)
|
| 397 |
GPU ${GPU_MB} MiB
|
|
|
|
|
|
|
| 398 |
監聽 http://0.0.0.0:$PORT
|
| 399 |
環境 ST_RAM_BUDGET_MB=$RAM_BUDGET_MB ST_RESERVE_MB=$RESERVE_MB \
|
| 400 |
ST_KV_RESERVE_MB=$KV_RESERVE_MB ST_THREADS=$THREADS ST_UBATCH=$UBATCH \
|
|
@@ -438,6 +446,8 @@ fi
|
|
| 438 |
|
| 439 |
export ST_RAM_BUDGET_MB="$RAM_BUDGET_MB"
|
| 440 |
export ST_RESERVE_MB="$RESERVE_MB"
|
|
|
|
|
|
|
| 441 |
export ST_KV_RESERVE_MB="$KV_RESERVE_MB"
|
| 442 |
export ST_THREADS="$THREADS"
|
| 443 |
export ST_UBATCH="$UBATCH"
|
|
|
|
| 318 |
command -v ninja >/dev/null && gen=(-G Ninja)
|
| 319 |
mkdir -p "$LOG_DIR"
|
| 320 |
# 不開 -march=native:編譯機與執行機不一定同一台,native 會在別的機器 SIGILL。
|
| 321 |
+
# -DGGML_CPU_REPACK=OFF 是硬性要求,不能拿掉:
|
| 322 |
+
# 開著時 ggml 會把 Q4_K 權重轉成 repack 格式放進**匿名**緩衝區,
|
| 323 |
+
# 權重就整個離開 mmap。這時對它 madvise(MADV_DONTNEED) 不是「讀 SSD」,
|
| 324 |
+
# 而是**把那塊記憶體歸零** → 輸出變亂碼,但行程看起來完全正常。
|
| 325 |
+
# (這個坑真的踩過,症狀是「輸出變成 'papers bases abstract' 之類的亂碼」,
|
| 326 |
+
# 而且 tok/s 只掉一點點 —— 非常不像出錯。st_pager.cpp 也有 in_map 檢查擋著。)
|
| 327 |
cmake "${gen[@]}" -S "$SRC_DIR" -B "$SRC_DIR/build" \
|
| 328 |
+
-DCMAKE_BUILD_TYPE=Release -DGGML_NATIVE=OFF -DGGML_CPU_REPACK=OFF \
|
| 329 |
-DGGML_CURL=OFF -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=OFF \
|
| 330 |
-DLLAMA_BUILD_SERVER=ON -DLLAMA_BUILD_TOOLS=ON \
|
| 331 |
>"$LOG_DIR/cmake.log" 2>&1 || die "cmake 失敗,見 $LOG_DIR/cmake.log"
|
|
|
|
| 401 |
threads $THREADS(配額 ${QUOTA:-無}/可用核心 $(affinity_cpus))
|
| 402 |
ubatch $UBATCH ctx $CTX(模型上限 $MODEL_MAX_CTX)
|
| 403 |
GPU ${GPU_MB} MiB
|
| 404 |
+
分頁 ST_PAGER=${ST_PAGER:-1}(0 = 關閉,走上游行為)
|
| 405 |
+
expert 槽 $(( N_LAYER * N_EXPERT )) 個 × $(( EXPERT_BYTES / 1024 )) KiB = ${EXPERT_TOTAL_MIB} MiB
|
| 406 |
監聽 http://0.0.0.0:$PORT
|
| 407 |
環境 ST_RAM_BUDGET_MB=$RAM_BUDGET_MB ST_RESERVE_MB=$RESERVE_MB \
|
| 408 |
ST_KV_RESERVE_MB=$KV_RESERVE_MB ST_THREADS=$THREADS ST_UBATCH=$UBATCH \
|
|
|
|
| 446 |
|
| 447 |
export ST_RAM_BUDGET_MB="$RAM_BUDGET_MB"
|
| 448 |
export ST_RESERVE_MB="$RESERVE_MB"
|
| 449 |
+
export ST_ARENA_MB="$ARENA_MB"
|
| 450 |
+
export ST_STATS_FILE="${ST_STATS_FILE:-$WORK_DIR/st-stats.json}"
|
| 451 |
export ST_KV_RESERVE_MB="$KV_RESERVE_MB"
|
| 452 |
export ST_THREADS="$THREADS"
|
| 453 |
export ST_UBATCH="$UBATCH"
|
patches/0001-st-expert-pager.patch
ADDED
|
@@ -0,0 +1,695 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
From b16d956b457292e37565cdd501fd5901e9ff1844 Mon Sep 17 00:00:00 2001
|
| 2 |
+
From: c <a@b>
|
| 3 |
+
Date: Wed, 7 Oct 2026 16:07:37 +0200
|
| 4 |
+
Subject: [PATCH] st pager wip
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
.st-unpatched | 0
|
| 8 |
+
src/CMakeLists.txt | 1 +
|
| 9 |
+
src/llama-graph.cpp | 10 +
|
| 10 |
+
src/llama-model.cpp | 32 +++
|
| 11 |
+
src/st_pager.cpp | 494 ++++++++++++++++++++++++++++++++++++++++++++
|
| 12 |
+
src/st_pager.h | 79 +++++++
|
| 13 |
+
6 files changed, 616 insertions(+)
|
| 14 |
+
create mode 100644 .st-unpatched
|
| 15 |
+
create mode 100644 src/st_pager.cpp
|
| 16 |
+
create mode 100644 src/st_pager.h
|
| 17 |
+
|
| 18 |
+
diff --git a/.st-unpatched b/.st-unpatched
|
| 19 |
+
new file mode 100644
|
| 20 |
+
index 000000000..e69de29bb
|
| 21 |
+
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
|
| 22 |
+
index afdaddc79..31b2c2896 100644
|
| 23 |
+
--- a/src/CMakeLists.txt
|
| 24 |
+
+++ b/src/CMakeLists.txt
|
| 25 |
+
@@ -18,6 +18,7 @@ set(LLAMA_CORE_SOURCES
|
| 26 |
+
llama-cparams.cpp
|
| 27 |
+
llama-grammar.cpp
|
| 28 |
+
llama-graph.cpp
|
| 29 |
+
+ st_pager.cpp
|
| 30 |
+
llama-hparams.cpp
|
| 31 |
+
llama-impl.cpp
|
| 32 |
+
llama-io.cpp
|
| 33 |
+
diff --git a/src/llama-graph.cpp b/src/llama-graph.cpp
|
| 34 |
+
index 1112ad885..e335a9d5f 100644
|
| 35 |
+
--- a/src/llama-graph.cpp
|
| 36 |
+
+++ b/src/llama-graph.cpp
|
| 37 |
+
@@ -1,5 +1,7 @@
|
| 38 |
+
#include "llama-graph.h"
|
| 39 |
+
|
| 40 |
+
+#include "st_pager.h"
|
| 41 |
+
+
|
| 42 |
+
#include "llama-impl.h"
|
| 43 |
+
#include "llama-model.h"
|
| 44 |
+
#include "llama-batch.h"
|
| 45 |
+
@@ -2205,6 +2207,14 @@ ggml_tensor * llm_graph_context::build_moe_ffn(
|
| 46 |
+
//call early so that topk-moe can be used
|
| 47 |
+
ggml_build_forward_expand(gf, weights);
|
| 48 |
+
|
| 49 |
+
+ // ST expert pager:在真正算 expert 之前,先知道這一層要用哪些 expert,
|
| 50 |
+
+ // 然後把用不到的 expert 權重丟回 SSD。計算本身完全不動 ——
|
| 51 |
+
+ // 下面仍然是 llama.cpp 自己的 mul_mat_id,所以輸出與上游一致。
|
| 52 |
+
+ if (ggml_tensor * st_note = st::build_routing_op(ctx0, selected_experts, il,
|
| 53 |
+
+ (int) n_expert, (int) n_expert_used)) {
|
| 54 |
+
+ ggml_build_forward_expand(gf, st_note);
|
| 55 |
+
+ }
|
| 56 |
+
+
|
| 57 |
+
cur = ggml_reshape_3d(ctx0, cur, n_embd, 1, n_tokens);
|
| 58 |
+
|
| 59 |
+
if (weight_before_ffn) {
|
| 60 |
+
diff --git a/src/llama-model.cpp b/src/llama-model.cpp
|
| 61 |
+
index 1f3b80b08..f863a15fa 100644
|
| 62 |
+
--- a/src/llama-model.cpp
|
| 63 |
+
+++ b/src/llama-model.cpp
|
| 64 |
+
@@ -1,5 +1,7 @@
|
| 65 |
+
#include "llama-model.h"
|
| 66 |
+
|
| 67 |
+
+#include "st_pager.h"
|
| 68 |
+
+
|
| 69 |
+
#include "llama-arch.h"
|
| 70 |
+
#include "llama-ext.h"
|
| 71 |
+
#include "llama-hparams.h"
|
| 72 |
+
@@ -1969,6 +1971,36 @@ bool llama_model_base::load_tensors(llama_model_loader & ml) {
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
+ // ST expert pager:把 MoE expert 權重記成 (layer, expert) 的檔案/記憶體區間,
|
| 77 |
+
+ // 讓 build_moe_ffn 插入的 op 能在用不到時把它們丟回 SSD。
|
| 78 |
+
+ // 註冊失敗不是致命錯誤 —— 那只是不分頁,模型照上游路徑跑。
|
| 79 |
+
+ {
|
| 80 |
+
+ st::set_config_from_env();
|
| 81 |
+
+ st::install_signal_handler();
|
| 82 |
+
+
|
| 83 |
+
+ void * map_base = nullptr;
|
| 84 |
+
+ size_t map_size = 0;
|
| 85 |
+
+ int map_fd = -1;
|
| 86 |
+
+ if (!pimpl->mappings.empty() && !ml.files.empty()) {
|
| 87 |
+
+ map_base = pimpl->mappings[0]->addr();
|
| 88 |
+
+ map_size = pimpl->mappings[0]->size();
|
| 89 |
+
+ map_fd = ml.files[0]->file_id();
|
| 90 |
+
+ }
|
| 91 |
+
+
|
| 92 |
+
+ std::map<std::string, ggml_tensor *> by_name;
|
| 93 |
+
+ for (const auto & [nm, t] : tensors_by_name) {
|
| 94 |
+
+ by_name.emplace(nm, t);
|
| 95 |
+
+ }
|
| 96 |
+
+
|
| 97 |
+
+ if (getenv("ST_VERBOSE") && !map_base) {
|
| 98 |
+
+ LLAMA_LOG_INFO("%s: [st] 沒有 mapping(mappings=%zu files=%zu),無法分頁\n",
|
| 99 |
+
+ __func__, pimpl->mappings.size(), ml.files.size());
|
| 100 |
+
+ }
|
| 101 |
+
+ if (map_base && !st::register_model(arch_name().c_str(), map_base, map_size, map_fd, by_name)) {
|
| 102 |
+
+ LLAMA_LOG_INFO("%s: [st] expert 分頁未啟用(arch=%s)\n", __func__, arch_name().c_str());
|
| 103 |
+
+ }
|
| 104 |
+
+ }
|
| 105 |
+
+
|
| 106 |
+
return true;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
diff --git a/src/st_pager.cpp b/src/st_pager.cpp
|
| 110 |
+
new file mode 100644
|
| 111 |
+
index 000000000..79bd1d4cb
|
| 112 |
+
--- /dev/null
|
| 113 |
+
+++ b/src/st_pager.cpp
|
| 114 |
+
@@ -0,0 +1,494 @@
|
| 115 |
+
+// ST expert pager —— 熱 expert 留在 RAM,冷 expert 丟回 SSD
|
| 116 |
+
+//
|
| 117 |
+
+// 設計取捨(為什麼不照抄 sddqwen35a3b_v01 的 pread arena):
|
| 118 |
+
+// sddqwen35a3b 做的是「把 expert 權重 pread 進自己的 arena,用自訂 ggml op
|
| 119 |
+
+// 重寫量化點積」。那在 22 GB 權重 / 小 RAM 機器上是必要的,代價是要自己重寫
|
| 120 |
+
+// Q4_K 的點積 kernel,數值正確性得另外驗證。
|
| 121 |
+
+// SmallThinker 只有 2.45 GiB,權重可以用 file-backed mmap 留在 SSD,
|
| 122 |
+
+// 所以本專案改成「**計算路徑完全不動,只把用不到的 expert 頁丟掉**」:
|
| 123 |
+
+// - 計算仍是 llama.cpp 自己的 kernel → 輸出與上游一致,沒有重寫 kernel 的風險。
|
| 124 |
+
+// - 淘汰的最小單位是「一個 expert 的三段權重」,不是 page。
|
| 125 |
+
+// 代價:不能像 arena 那樣用 prefetch 提前重疊 I/O,SSD 讀取是 demand-paging
|
| 126 |
+
+// (第一次碰到缺頁才讀)。這是本專案目前的取捨,量測見 docs。
|
| 127 |
+
+//
|
| 128 |
+
+// 淘汰的兩步驟(順序不能換,這是實測踩出來的):
|
| 129 |
+
+// a. madvise(MADV_DONTNEED) → 拿掉本行程 PTEs,行程總 RSS 立刻下降。
|
| 130 |
+
+// b. posix_fadvise(DONTNEED) → 核心的 invalidate_mapping_pages() 會跳過
|
| 131 |
+
+// 「仍被某個 VMA 映射」的頁,所以 (a) 必須先做;(b) 做完這些頁才真的從
|
| 132 |
+
+// page cache 消失,下次碰到才是真的讀 SSD。
|
| 133 |
+
+// 只做 (a):RSS 下降但 read_bytes 不變(頁還在 page cache)。
|
| 134 |
+
+// 只做 (b):page cache 下降但行程 RSS 不變(VMA 還映射著)。
|
| 135 |
+
+
|
| 136 |
+
+#include "st_pager.h"
|
| 137 |
+
+
|
| 138 |
+
+#include "ggml.h"
|
| 139 |
+
+#include "ggml-alloc.h"
|
| 140 |
+
+#include "ggml-backend.h"
|
| 141 |
+
+
|
| 142 |
+
+#include <algorithm>
|
| 143 |
+
+#include <array>
|
| 144 |
+
+#include <csignal>
|
| 145 |
+
+#include <cstdarg>
|
| 146 |
+
+#include <cstdio>
|
| 147 |
+
+#include <cstdlib>
|
| 148 |
+
+#include <cstring>
|
| 149 |
+
+#include <map>
|
| 150 |
+
+#include <mutex>
|
| 151 |
+
+#include <string>
|
| 152 |
+
+#include <vector>
|
| 153 |
+
+
|
| 154 |
+
+#include <fcntl.h>
|
| 155 |
+
+#include <sys/mman.h>
|
| 156 |
+
+#include <unistd.h>
|
| 157 |
+
+
|
| 158 |
+
+namespace st {
|
| 159 |
+
+
|
| 160 |
+
+// ------------------------------------------------------------------ 設定
|
| 161 |
+
+
|
| 162 |
+
+static bool g_enabled = false;
|
| 163 |
+
+static bool g_verbose = false;
|
| 164 |
+
+static size_t g_budget = 0; // expert 權重可以常駐 RAM 的上限(bytes)
|
| 165 |
+
+static int g_hot_tokens = 8; // 最近 N 個 tick 用過的 expert 視為熱
|
| 166 |
+
+
|
| 167 |
+
+struct span {
|
| 168 |
+
+ uint8_t * addr = nullptr;
|
| 169 |
+
+ size_t bytes = 0;
|
| 170 |
+
+ size_t file_off = 0;
|
| 171 |
+
+};
|
| 172 |
+
+
|
| 173 |
+
+struct slot {
|
| 174 |
+
+ span parts[st::N_PART];
|
| 175 |
+
+ uint64_t last_used = 0;
|
| 176 |
+
+ uint64_t uses = 0;
|
| 177 |
+
+ bool resident = false;
|
| 178 |
+
+};
|
| 179 |
+
+
|
| 180 |
+
+// 一個 expert 的三段位元組數**每層可能不同**(實測:down_exps 有些層是 Q6_K、
|
| 181 |
+
+// 有些是 Q4_K),所以逐 slot 算,不能用一個全模型常數。
|
| 182 |
+
+static inline size_t slot_bytes_of(const slot & sl) {
|
| 183 |
+
+ return sl.parts[0].bytes + sl.parts[1].bytes + sl.parts[2].bytes;
|
| 184 |
+
+}
|
| 185 |
+
+
|
| 186 |
+
+static std::vector<slot> g_slots; // [layer * n_expert + expert]
|
| 187 |
+
+static int g_n_layer = 0;
|
| 188 |
+
+static int g_n_expert = 0;
|
| 189 |
+
+static size_t g_slot_bytes = 0; // 三段加總(= 一個 expert 的權重)
|
| 190 |
+
+static size_t g_part_bytes[st::N_PART] = { 0, 0, 0 };
|
| 191 |
+
+static uint8_t * g_map_base = nullptr;
|
| 192 |
+
+static int g_fd = -1;
|
| 193 |
+
+static size_t g_map_size = 0;
|
| 194 |
+
+static uint64_t g_tick = 0;
|
| 195 |
+
+static std::mutex g_mu;
|
| 196 |
+
+static stats g_st;
|
| 197 |
+
+
|
| 198 |
+
+static size_t page_size() {
|
| 199 |
+
+ static const size_t ps = (size_t) sysconf(_SC_PAGESIZE);
|
| 200 |
+
+ return ps;
|
| 201 |
+
+}
|
| 202 |
+
+
|
| 203 |
+
+static uint64_t env_u64(const char * name, uint64_t dflt) {
|
| 204 |
+
+ const char * e = getenv(name);
|
| 205 |
+
+ if (!e || !*e) return dflt;
|
| 206 |
+
+ char * end = nullptr;
|
| 207 |
+
+ const unsigned long long v = strtoull(e, &end, 10);
|
| 208 |
+
+ if (end == e) return dflt;
|
| 209 |
+
+ return v;
|
| 210 |
+
+}
|
| 211 |
+
+
|
| 212 |
+
+void set_config_from_env() {
|
| 213 |
+
+ g_enabled = env_u64("ST_PAGER", 1) != 0;
|
| 214 |
+
+ g_verbose = env_u64("ST_VERBOSE", 0) != 0;
|
| 215 |
+
+
|
| 216 |
+
+ const uint64_t budget_mb = env_u64("ST_RAM_BUDGET_MB", 0);
|
| 217 |
+
+ const uint64_t reserve_mb = env_u64("ST_RESERVE_MB", 0);
|
| 218 |
+
+ const uint64_t arena_mb = env_u64("ST_ARENA_MB", 0);
|
| 219 |
+
+
|
| 220 |
+
+ uint64_t mb;
|
| 221 |
+
+ if (arena_mb > 0) {
|
| 222 |
+
+ mb = arena_mb; // llama_server.sh 已經算好
|
| 223 |
+
+ } else if (budget_mb > reserve_mb) {
|
| 224 |
+
+ mb = budget_mb - reserve_mb;
|
| 225 |
+
+ } else {
|
| 226 |
+
+ mb = budget_mb;
|
| 227 |
+
+ }
|
| 228 |
+
+ g_budget = (size_t) mb * 1024 * 1024;
|
| 229 |
+
+
|
| 230 |
+
+ g_hot_tokens = (int) env_u64("ST_HOT_TOKENS", 8);
|
| 231 |
+
+ if (g_hot_tokens < 1) g_hot_tokens = 1;
|
| 232 |
+
+}
|
| 233 |
+
+
|
| 234 |
+
+bool enabled() { return g_enabled && !g_slots.empty(); }
|
| 235 |
+
+
|
| 236 |
+
+// ------------------------------------------------------------------ 統計
|
| 237 |
+
+
|
| 238 |
+
+stats get_stats() {
|
| 239 |
+
+ std::lock_guard<std::mutex> lk(g_mu);
|
| 240 |
+
+ stats s = g_st;
|
| 241 |
+
+ s.budget_bytes = g_budget;
|
| 242 |
+
+ size_t rb = 0;
|
| 243 |
+
+ for (const auto & sl : g_slots) {
|
| 244 |
+
+ if (sl.resident) rb += slot_bytes_of(sl);
|
| 245 |
+
+ }
|
| 246 |
+
+ s.resident_bytes = rb;
|
| 247 |
+
+ return s;
|
| 248 |
+
+}
|
| 249 |
+
+
|
| 250 |
+
+void dump_stats(const char * path) {
|
| 251 |
+
+ const stats s = get_stats();
|
| 252 |
+
+ FILE * f = fopen(path, "w");
|
| 253 |
+
+ if (!f) return;
|
| 254 |
+
+ fprintf(f,
|
| 255 |
+
+ "{\n"
|
| 256 |
+
+ " \"routing_calls\": %llu,\n"
|
| 257 |
+
+ " \"expert_touches\": %llu,\n"
|
| 258 |
+
+ " \"hits\": %llu,\n"
|
| 259 |
+
+ " \"misses\": %llu,\n"
|
| 260 |
+
+ " \"evictions\": %llu,\n"
|
| 261 |
+
+ " \"evicted_bytes\": %llu,\n"
|
| 262 |
+
+ " \"sweeps\": %llu,\n"
|
| 263 |
+
+ " \"resident_bytes\": %llu,\n"
|
| 264 |
+
+ " \"budget_bytes\": %llu,\n"
|
| 265 |
+
+ " \"hit_rate\": %.4f\n"
|
| 266 |
+
+ "}\n",
|
| 267 |
+
+ (unsigned long long) s.routing_calls,
|
| 268 |
+
+ (unsigned long long) s.expert_touches,
|
| 269 |
+
+ (unsigned long long) s.hits,
|
| 270 |
+
+ (unsigned long long) s.misses,
|
| 271 |
+
+ (unsigned long long) s.evictions,
|
| 272 |
+
+ (unsigned long long) s.evicted_bytes,
|
| 273 |
+
+ (unsigned long long) s.sweeps,
|
| 274 |
+
+ (unsigned long long) s.resident_bytes,
|
| 275 |
+
+ (unsigned long long) s.budget_bytes,
|
| 276 |
+
+ s.expert_touches ? (double) s.hits / (double) s.expert_touches : 0.0);
|
| 277 |
+
+ fclose(f);
|
| 278 |
+
+}
|
| 279 |
+
+
|
| 280 |
+
+// ------------------------------------------------------------------ 註冊
|
| 281 |
+
+
|
| 282 |
+
+static bool expert_layout_ok(const ggml_tensor * t, const char * what) {
|
| 283 |
+
+ // 關鍵前提:expert 維度必須是最外層且整段連續,單一 expert 才是
|
| 284 |
+
+ // 「一段連續位元組」。若上游改了 layout,這裡直接擋下來,
|
| 285 |
+
+ // 而不是算出錯誤的位址把別人的記憶體丟掉。
|
| 286 |
+
+ //
|
| 287 |
+
+ // 注意:量化張量的 nb[0] 是「一個 block 的位元組數」,不是 1,
|
| 288 |
+
+ // 所以 nb[1] != ne[0]*nb[0]。唯一要檢查的是 nb[2] == ne[1]*nb[1]
|
| 289 |
+
+ // —— expert 之間沒有任何 padding/stride。
|
| 290 |
+
+ if (t->ne[2] <= 1) return false;
|
| 291 |
+
+ if (t->nb[2] != (size_t) t->ne[1] * t->nb[1]) return false;
|
| 292 |
+
+ if (g_verbose) {
|
| 293 |
+
+ fprintf(stderr, "[st] %s: ne=[%lld,%lld,%lld] nb=[%zu,%zu,%zu] 每 expert %zu bytes\n",
|
| 294 |
+
+ what, (long long) t->ne[0], (long long) t->ne[1], (long long) t->ne[2],
|
| 295 |
+
+ t->nb[0], t->nb[1], t->nb[2], t->nb[2]);
|
| 296 |
+
+ }
|
| 297 |
+
+ return true;
|
| 298 |
+
+}
|
| 299 |
+
+
|
| 300 |
+
+bool register_model(const char * arch,
|
| 301 |
+
+ void * mapping_base, size_t g_map_size_hint, int fd,
|
| 302 |
+
+ const std::map<std::string, ggml_tensor *> & by_name) {
|
| 303 |
+
+ if (!g_enabled) return false;
|
| 304 |
+
+ if (strcmp(arch, "smallthinker") != 0) {
|
| 305 |
+
+ if (g_verbose) fprintf(stderr, "[st] arch=%s 不支援 expert 分頁\n", arch);
|
| 306 |
+
+ return false;
|
| 307 |
+
+ }
|
| 308 |
+
+
|
| 309 |
+
+ g_map_base = (uint8_t *) mapping_base;
|
| 310 |
+
+ g_map_size = g_map_size_hint;
|
| 311 |
+
+ g_fd = fd;
|
| 312 |
+
+
|
| 313 |
+
+ auto get = [&](int il, int p) -> ggml_tensor * {
|
| 314 |
+
+ char nm[64];
|
| 315 |
+
+ snprintf(nm, sizeof(nm), "blk.%d.ffn_%s_exps.weight", il,
|
| 316 |
+
+ p == st::GATE ? "gate" : (p == st::UP ? "up" : "down"));
|
| 317 |
+
+ auto it = by_name.find(nm);
|
| 318 |
+
+ return it == by_name.end() ? nullptr : it->second;
|
| 319 |
+
+ };
|
| 320 |
+
+
|
| 321 |
+
+ // 層數由第一層之後往後掃到空為止。
|
| 322 |
+
+ std::vector<std::array<ggml_tensor *, st::N_PART>> layers;
|
| 323 |
+
+ int n_expert = 0;
|
| 324 |
+
+ size_t slot_bytes = 0; // 三段加總
|
| 325 |
+
+
|
| 326 |
+
+ for (int il = 0; ; il++) {
|
| 327 |
+
+ std::array<ggml_tensor *, st::N_PART> row{};
|
| 328 |
+
+ bool ok = true;
|
| 329 |
+
+ for (int p = 0; p < st::N_PART; p++) {
|
| 330 |
+
+ row[p] = get(il, p);
|
| 331 |
+
+ if (!row[p]) { ok = false; break; }
|
| 332 |
+
+ }
|
| 333 |
+
+ if (!ok) break;
|
| 334 |
+
+
|
| 335 |
+
+ for (int p = 0; p < st::N_PART; p++) {
|
| 336 |
+
+ char what[80];
|
| 337 |
+
+ snprintf(what, sizeof(what), "blk.%d part %d", il, p);
|
| 338 |
+
+ if (!expert_layout_ok(row[p], what)) {
|
| 339 |
+
+ fprintf(stderr, "[st] %s 的 expert 維度不連續,放棄分頁(回到上游行為)\n", what);
|
| 340 |
+
+ return false;
|
| 341 |
+
+ }
|
| 342 |
+
+ const int64_t ne2 = row[p]->ne[2];
|
| 343 |
+
+ const size_t nb2 = row[p]->nb[2];
|
| 344 |
+
+ if (il == 0) {
|
| 345 |
+
+ n_expert = (int) ne2;
|
| 346 |
+
+ g_part_bytes[p] = nb2;
|
| 347 |
+
+ slot_bytes += nb2;
|
| 348 |
+
+ } else if ((int) ne2 != n_expert) {
|
| 349 |
+
+ // 只要求 expert **數量**一致。三段的量化型別可以逐層不同
|
| 350 |
+
+ // (實測 down_exps 有些層是 Q6_K、有些是 Q4_K),
|
| 351 |
+
+ // 所以位元組數逐 slot 存,不拿 layer 0 當基準。
|
| 352 |
+
+ fprintf(stderr, "[st] layer %d part %d 的 expert 數量不一致(%lld vs %d)\n",
|
| 353 |
+
+ il, p, (long long) ne2, n_expert);
|
| 354 |
+
+ return false;
|
| 355 |
+
+ }
|
| 356 |
+
+ }
|
| 357 |
+
+ layers.push_back(row);
|
| 358 |
+
+ }
|
| 359 |
+
+
|
| 360 |
+
+ if (layers.empty()) return false;
|
| 361 |
+
+
|
| 362 |
+
+ g_n_expert = n_expert;
|
| 363 |
+
+ g_slot_bytes = slot_bytes;
|
| 364 |
+
+ g_n_layer = (int) layers.size();
|
| 365 |
+
+ const size_t total_expert_bytes = slot_bytes * (size_t) g_n_expert * (size_t) g_n_layer;
|
| 366 |
+
+
|
| 367 |
+
+ g_slots.assign((size_t) g_n_layer * g_n_expert, slot{});
|
| 368 |
+
+ for (int il = 0; il < g_n_layer; il++) {
|
| 369 |
+
+ for (int e = 0; e < g_n_expert; e++) {
|
| 370 |
+
+ auto & sl = g_slots[(size_t) il * g_n_expert + e];
|
| 371 |
+
+ for (int p = 0; p < st::N_PART; p++) {
|
| 372 |
+
+ const ggml_tensor * t = layers[il][p];
|
| 373 |
+
+ uint8_t * a = (uint8_t *) t->data + (size_t) e * t->nb[2];
|
| 374 |
+
+ sl.parts[p].addr = a;
|
| 375 |
+
+ sl.parts[p].bytes = t->nb[2];
|
| 376 |
+
+ sl.parts[p].file_off = (size_t) (a - g_map_base);
|
| 377 |
+
+
|
| 378 |
+
+ // 診斷:確認 t->data 真的在 mmap 區間內。
|
| 379 |
+
+ // 若不在,madvise 打在匿名記憶體上 = MADV_DONTNEED 會把它歸零
|
| 380 |
+
+ // → 輸出直接變成亂碼。這是本專案真的踩到的坑。
|
| 381 |
+
+ {
|
| 382 |
+
+ const uintptr_t mb = (uintptr_t) g_map_base;
|
| 383 |
+
+ const uintptr_t msz = (uintptr_t) g_map_size;
|
| 384 |
+
+ const uintptr_t ua = (uintptr_t) a;
|
| 385 |
+
+ const int in_map = (ua >= mb && ua < mb + msz) ? 1 : 0;
|
| 386 |
+
+ if (!in_map) {
|
| 387 |
+
+ // 這是本專案踩過最嚴重的一個坑:
|
| 388 |
+
+ // 若 ggml 用 CPU_REPACK 把權重轉成 repack 格式放在**匿名**緩衝區,
|
| 389 |
+
+ // t->data 就不在 mmap 內。這時對它 madvise(MADV_DONTNEED)
|
| 390 |
+
+ // 不是「讀 SSD」,而是**把那塊記憶體歸零** → 輸出直接變亂碼,
|
| 391 |
+
+ // 而且看起來「有在跑」、速度也只慢一點,完全不像出錯。
|
| 392 |
+
+ // 所以這裡必須硬性擋下,並且把 buffer 型別印出來。
|
| 393 |
+
+ const ggml_tensor * tt = layers[il][p];
|
| 394 |
+
+ fprintf(stderr,
|
| 395 |
+
+ "[st] 權重不在 mmap 內(layer %d part %d expert %d: %p,buffer=%s)"
|
| 396 |
+
+ " → 分頁停用(需要 -DGGML_CPU_REPACK=OFF,否則 madvise 會歸零權重)\n",
|
| 397 |
+
+ il, p, e, (void *) a,
|
| 398 |
+
+ (tt->buffer ? ggml_backend_buffer_name(tt->buffer) : "(null)"));
|
| 399 |
+
+ return false;
|
| 400 |
+
+ }
|
| 401 |
+
+ }
|
| 402 |
+
+ }
|
| 403 |
+
+ sl.resident = true; // llama.cpp 剛載入時全部在 RAM
|
| 404 |
+
+ }
|
| 405 |
+
+ }
|
| 406 |
+
+
|
| 407 |
+
+ fprintf(stderr,
|
| 408 |
+
+ "[st] expert pager 就緒:%d 層 × %d experts = %d 個,"
|
| 409 |
+
+ "每個 %zu bytes(gate %zu + up %zu + down %zu,以 layer 0 為例),"
|
| 410 |
+
+ "expert 總量 %.1f MiB,預算 %.1f MiB,hot window %d tick\n",
|
| 411 |
+
+ g_n_layer, g_n_expert, g_n_layer * g_n_expert,
|
| 412 |
+
+ slot_bytes, g_part_bytes[0], g_part_bytes[1], g_part_bytes[2],
|
| 413 |
+
+ (double) total_expert_bytes / (1024.0 * 1024.0),
|
| 414 |
+
+ (double) g_budget / (1024.0 * 1024.0), g_hot_tokens);
|
| 415 |
+
+ return true;
|
| 416 |
+
+}
|
| 417 |
+
+
|
| 418 |
+
+// ------------------------------------------------------------------ 淘汰
|
| 419 |
+
+
|
| 420 |
+
+// ST_EVICT_MODE 診斷用(實測發現只做其中一步會導致輸出損毀,見 docs):
|
| 421 |
+
+// both(預設)| madvise | fadvise | none
|
| 422 |
+
+static int evict_mode() {
|
| 423 |
+
+ static const int m = [] {
|
| 424 |
+
+ const char * e = getenv("ST_EVICT_MODE");
|
| 425 |
+
+ if (!e || !*e) return 0;
|
| 426 |
+
+ if (!strcmp(e, "none")) return 3;
|
| 427 |
+
+ if (!strcmp(e, "madvise")) return 1;
|
| 428 |
+
+ if (!strcmp(e, "fadvise")) return 2;
|
| 429 |
+
+ return 0;
|
| 430 |
+
+ }();
|
| 431 |
+
+ return m;
|
| 432 |
+
+}
|
| 433 |
+
+
|
| 434 |
+
+static void evict_slot(const slot & sl) {
|
| 435 |
+
+ const size_t ps = page_size();
|
| 436 |
+
+ const int mode = evict_mode();
|
| 437 |
+
+ if (mode == 3) return;
|
| 438 |
+
+
|
| 439 |
+
+ for (int p = 0; p < st::N_PART; p++) {
|
| 440 |
+
+ const span & s = sl.parts[p];
|
| 441 |
+
+ if (!s.addr || !s.bytes) continue;
|
| 442 |
+
+
|
| 443 |
+
+ if (mode == 0 || mode == 1) {
|
| 444 |
+
+ // (a) 拿掉本行程的 PTE
|
| 445 |
+
+ const uintptr_t a = (uintptr_t) s.addr;
|
| 446 |
+
+ const uintptr_t a0 = a & ~(uintptr_t) (ps - 1);
|
| 447 |
+
+ madvise((void *) a0, s.bytes + (size_t) (a - a0), MADV_DONTNEED);
|
| 448 |
+
+ }
|
| 449 |
+
+
|
| 450 |
+
+ if ((mode == 0 || mode == 2) && g_fd >= 0) {
|
| 451 |
+
+ // (b) 把 page cache 裡的頁真的丟掉
|
| 452 |
+
+ const size_t fo0 = s.file_off & ~(ps - 1);
|
| 453 |
+
+ posix_fadvise(g_fd, (off_t) fo0,
|
| 454 |
+
+ (off_t) (s.bytes + (s.file_off - fo0)), POSIX_FADV_DONTNEED);
|
| 455 |
+
+ }
|
| 456 |
+
+ }
|
| 457 |
+
+}
|
| 458 |
+
+
|
| 459 |
+
+static void enforce_budget(uint64_t now) {
|
| 460 |
+
+ size_t resident = 0;
|
| 461 |
+
+ for (const auto & sl : g_slots) {
|
| 462 |
+
+ if (sl.resident) resident += slot_bytes_of(sl);
|
| 463 |
+
+ }
|
| 464 |
+
+ if (resident <= g_budget) return;
|
| 465 |
+
+
|
| 466 |
+
+ g_st.sweeps++;
|
| 467 |
+
+
|
| 468 |
+
+ // 候選 = 還在 RAM、且「不是這個 tick 用到的」。
|
| 469 |
+
+ // last_used == 0 代表「從來沒被用到過」→ 一定是冷的。
|
| 470 |
+
+ // 注意不能用 `last_used < now - hot_tokens`:現在 tick 才剛開始時
|
| 471 |
+
+ // 那個門檻會是 0,而 last_used 也是 0,於是**永遠沒有候選**,
|
| 472 |
+
+ // 第一輪 sweep 必定什麼都不做(這個 bug 真的踩過)。
|
| 473 |
+
+ std::vector<size_t> cand;
|
| 474 |
+
+ cand.reserve(g_slots.size());
|
| 475 |
+
+ const uint64_t hot_floor = now >= (uint64_t) g_hot_tokens ? now - (uint64_t) g_hot_tokens : 0;
|
| 476 |
+
+ for (size_t i = 0; i < g_slots.size(); i++) {
|
| 477 |
+
+ const auto & sl = g_slots[i];
|
| 478 |
+
+ if (!sl.resident) continue;
|
| 479 |
+
+ if (sl.last_used == now) continue; // 這輪正在用,不能動
|
| 480 |
+
+ if (sl.last_used != 0 && sl.last_used > hot_floor) continue; // hot window 內
|
| 481 |
+
+ cand.push_back(i);
|
| 482 |
+
+ }
|
| 483 |
+
+
|
| 484 |
+
+ // 淘汰順序:先用得少的(LFU)→ 比較久沒用的(LRU)。
|
| 485 |
+
+ std::sort(cand.begin(), cand.end(), [&](size_t a, size_t b) {
|
| 486 |
+
+ const auto & sa = g_slots[a];
|
| 487 |
+
+ const auto & sb = g_slots[b];
|
| 488 |
+
+ if (sa.uses != sb.uses) return sa.uses < sb.uses;
|
| 489 |
+
+ return sa.last_used < sb.last_used;
|
| 490 |
+
+ });
|
| 491 |
+
+
|
| 492 |
+
+ std::vector<size_t> to_evict;
|
| 493 |
+
+ size_t freed = 0;
|
| 494 |
+
+ for (size_t i : cand) {
|
| 495 |
+
+ if (resident - freed <= g_budget) break;
|
| 496 |
+
+ to_evict.push_back(i);
|
| 497 |
+
+ freed += slot_bytes_of(g_slots[i]);
|
| 498 |
+
+ }
|
| 499 |
+
+ for (size_t i : to_evict) {
|
| 500 |
+
+ g_slots[i].resident = false;
|
| 501 |
+
+ evict_slot(g_slots[i]);
|
| 502 |
+
+ g_st.evictions++;
|
| 503 |
+
+ g_st.evicted_bytes += slot_bytes_of(g_slots[i]);
|
| 504 |
+
+ }
|
| 505 |
+
+
|
| 506 |
+
+ if (g_verbose && !to_evict.empty()) {
|
| 507 |
+
+ fprintf(stderr,
|
| 508 |
+
+ "[st] sweep #%llu:丟 %zu 個 expert(%.1f MiB)→ resident %.1f MiB / budget %.1f MiB\n",
|
| 509 |
+
+ (unsigned long long) g_st.sweeps, to_evict.size(),
|
| 510 |
+
+ freed / (1024.0 * 1024.0),
|
| 511 |
+
+ (resident - freed) / (1024.0 * 1024.0),
|
| 512 |
+
+ g_budget / (1024.0 * 1024.0));
|
| 513 |
+
+ }
|
| 514 |
+
+}
|
| 515 |
+
+
|
| 516 |
+
+void install_signal_handler() {
|
| 517 |
+
+ static const char * path = getenv("ST_STATS_FILE");
|
| 518 |
+
+ if (!path || !*path) return;
|
| 519 |
+
+ static char buf[1024];
|
| 520 |
+
+ snprintf(buf, sizeof(buf), "%s", path);
|
| 521 |
+
+
|
| 522 |
+
+ struct sigaction sa {};
|
| 523 |
+
+ sa.sa_handler = [](int) {
|
| 524 |
+
+ // signal handler 裡只能呼叫 async-signal-safe 的東西;
|
| 525 |
+
+ // dump_stats 用 stdio(有鎖),實務上這裡只會在沒人在同一瞬間
|
| 526 |
+
+ // 寫檔時觸發,風險可接受 —— 但不要從這裡再呼叫 exit()。
|
| 527 |
+
+ st::dump_stats(buf);
|
| 528 |
+
+ };
|
| 529 |
+
+ sigemptyset(&sa.sa_mask);
|
| 530 |
+
+ sigaction(SIGUSR2, &sa, nullptr);
|
| 531 |
+
+}
|
| 532 |
+
+
|
| 533 |
+
+// ------------------------------------------------------------------ routing op
|
| 534 |
+
+
|
| 535 |
+
+namespace {
|
| 536 |
+
+
|
| 537 |
+
+struct routing_ctx {
|
| 538 |
+
+ int layer = -1;
|
| 539 |
+
+ int n_expert = 0;
|
| 540 |
+
+ int n_expert_used = 0;
|
| 541 |
+
+};
|
| 542 |
+
+
|
| 543 |
+
+// ids 一定要在 op 執行時從 dst->src[0] 拿,不能在建圖時把指標存起來:
|
| 544 |
+
+// llama.cpp 每次 decode 都會重新建圖,graph context 的配置器可能把張量
|
| 545 |
+
+// 搬到不同位址(形狀不同時尤其明顯)。這是本專案踩過的坑。
|
| 546 |
+
+void routing_op(ggml_tensor * dst, int /*ith*/, int /*nth*/, void * userdata) {
|
| 547 |
+
+ auto * rc = (routing_ctx *) userdata;
|
| 548 |
+
+ if (!dst || dst->op != GGML_OP_CUSTOM) return;
|
| 549 |
+
+
|
| 550 |
+
+ ggml_tensor * ids = dst->src[0];
|
| 551 |
+
+ if (!ids || !ids->data) return;
|
| 552 |
+
+
|
| 553 |
+
+ const int n_used = (int) ids->ne[0];
|
| 554 |
+
+ const int n_tok = (int) (ids->ne[1] * ids->ne[2] * ids->ne[3]);
|
| 555 |
+
+ const int32_t * p = (const int32_t *) ids->data;
|
| 556 |
+
+
|
| 557 |
+
+ std::lock_guard<std::mutex> lk(g_mu);
|
| 558 |
+
+ g_st.routing_calls++;
|
| 559 |
+
+ const uint64_t now = ++g_tick;
|
| 560 |
+
+
|
| 561 |
+
+ // 這一層、這批 token 用到的所有 expert(去重)
|
| 562 |
+
+ bool touched[256] = { false };
|
| 563 |
+
+ std::vector<int> uniq;
|
| 564 |
+
+ for (int t = 0; t < n_tok; t++) {
|
| 565 |
+
+ for (int k = 0; k < n_used; k++) {
|
| 566 |
+
+ const int e = p[(size_t) k * ids->nb[0] / sizeof(int32_t)
|
| 567 |
+
+ + (size_t) t * ids->nb[1] / sizeof(int32_t)];
|
| 568 |
+
+ if (e < 0 || e >= rc->n_expert) continue;
|
| 569 |
+
+ g_st.expert_touches++;
|
| 570 |
+
+ if (!touched[e]) { touched[e] = true; uniq.push_back(e); }
|
| 571 |
+
+ }
|
| 572 |
+
+ }
|
| 573 |
+
+
|
| 574 |
+
+ for (int e : uniq) {
|
| 575 |
+
+ auto & sl = g_slots[(size_t) rc->layer * rc->n_expert + e];
|
| 576 |
+
+ sl.last_used = now;
|
| 577 |
+
+ sl.uses++;
|
| 578 |
+
+ if (sl.resident) {
|
| 579 |
+
+ g_st.hits++;
|
| 580 |
+
+ } else {
|
| 581 |
+
+ // 這一段會被 matmul 讀到 → 核心會從 SSD 補回來。
|
| 582 |
+
+ // 標記 resident 只是為了讓 hit/miss 統計誠實:
|
| 583 |
+
+ // 實際上 miss 之後這一層的 matmul 結束前不會再被丟掉。
|
| 584 |
+
+ g_st.misses++;
|
| 585 |
+
+ sl.resident = true;
|
| 586 |
+
+ }
|
| 587 |
+
+ }
|
| 588 |
+
+
|
| 589 |
+
+ enforce_budget(now);
|
| 590 |
+
+}
|
| 591 |
+
+
|
| 592 |
+
+} // namespace
|
| 593 |
+
+
|
| 594 |
+
+ggml_tensor * build_routing_op(ggml_context * ctx, ggml_tensor * selected_experts,
|
| 595 |
+
+ int layer, int n_expert, int n_expert_used) {
|
| 596 |
+
+ if (!enabled()) return nullptr;
|
| 597 |
+
+ if (!selected_experts || selected_experts->ne[0] != n_expert_used) return nullptr;
|
| 598 |
+
+
|
| 599 |
+
+ auto * rc = new routing_ctx{ layer, n_expert, n_expert_used };
|
| 600 |
+
+
|
| 601 |
+
+ // 輸出 1 個元素:我們只要副作用,不要資料。
|
| 602 |
+
+ // 不要用 ne0 = 0 —— 有些 ggml 版本對空張量的排程處理會直接跳過這個 node。
|
| 603 |
+
+ ggml_tensor * args[1] = { selected_experts };
|
| 604 |
+
+ return ggml_custom_4d(ctx, GGML_TYPE_I32, 1, 1, 1, 1,
|
| 605 |
+
+ args, 1, routing_op, 1, rc);
|
| 606 |
+
+}
|
| 607 |
+
+
|
| 608 |
+
+} // namespace st
|
| 609 |
+
|
| 610 |
+
diff --git a/src/st_pager.h b/src/st_pager.h
|
| 611 |
+
new file mode 100644
|
| 612 |
+
index 000000000..700a6f648
|
| 613 |
+
--- /dev/null
|
| 614 |
+
+++ b/src/st_pager.h
|
| 615 |
+
@@ -0,0 +1,79 @@
|
| 616 |
+
+// ST expert pager —— 熱 expert 留在 RAM,冷 expert 丟回 SSD
|
| 617 |
+
+//
|
| 618 |
+
+// 對外介面。llama.cpp 端(llama-model.cpp / llama-graph.cpp)只include 這個檔案。
|
| 619 |
+
+//
|
| 620 |
+
+// 設計取捨(為什麼不照抄 sddqwen35a3b_v01 的 pread arena):
|
| 621 |
+
+// sddqwen35a3b 做的是「把 expert 權重 pread 進自己的 arena,用自訂 ggml op
|
| 622 |
+
+// 重寫量化點積」。那在 22 GB 權重 / 小 RAM 機器上是必要的,代價是要自己重寫
|
| 623 |
+
+// Q4_K 的點積 kernel,數值正確性得另外驗證。
|
| 624 |
+
+// SmallThinker 只有 2.45 GiB,權重可以用 file-backed mmap 留在 SSD,
|
| 625 |
+
+// 所以本專案改成「**計算路徑完全不動,只把用不到的 expert 頁丟掉**」:
|
| 626 |
+
+// - 計算仍是 llama.cpp 自己的 kernel → 輸出與上游一致,沒有重寫 kernel 的風險。
|
| 627 |
+
+// - 淘汰的最小單位是「一個 expert 的三段權重」(gate/up/down),不是 page。
|
| 628 |
+
+// 代價:不能像 arena ���樣用 prefetch 提前重疊 I/O,SSD 讀取是 demand-paging
|
| 629 |
+
+// (第一次碰到缺頁才讀)。這是本專案目前的取捨。
|
| 630 |
+
+//
|
| 631 |
+
+// 淘汰的兩步驟(順序不能換,這是實測踩出來的):
|
| 632 |
+
+// a. madvise(MADV_DONTNEED) → 拿掉本行程 PTEs,行程總 RSS 立刻下降。
|
| 633 |
+
+// b. posix_fadvise(DONTNEED) → 核心的 invalidate_mapping_pages() 會跳過
|
| 634 |
+
+// 「仍被某個 VMA 映射」的頁,所以 (a) 必須先做;(b) 做完這些頁才真的從
|
| 635 |
+
+// page cache 消失,下次碰到才是真的讀 SSD。
|
| 636 |
+
+// 只做 (a):RSS 下降但 read_bytes 不變(頁還在 page cache)。
|
| 637 |
+
+// 只做 (b):page cache 下降但行程 RSS 不變(VMA 還映射著)。
|
| 638 |
+
+
|
| 639 |
+
+#pragma once
|
| 640 |
+
+
|
| 641 |
+
+#include <cstddef>
|
| 642 |
+
+#include <cstdint>
|
| 643 |
+
+#include <map>
|
| 644 |
+
+#include <string>
|
| 645 |
+
+
|
| 646 |
+
+struct ggml_context;
|
| 647 |
+
+struct ggml_tensor;
|
| 648 |
+
+
|
| 649 |
+
+namespace st {
|
| 650 |
+
+
|
| 651 |
+
+// 三段權重(對應 llama.cpp 的 smallthinker 命名)
|
| 652 |
+
+enum part { GATE = 0, UP = 1, DOWN = 2, N_PART = 3 };
|
| 653 |
+
+
|
| 654 |
+
+struct stats {
|
| 655 |
+
+ uint64_t routing_calls = 0; // 每層每個 ubatch 算一次
|
| 656 |
+
+ uint64_t expert_touches = 0; // 「某個 expert 被某個 token 選中」的總次數
|
| 657 |
+
+ uint64_t hits = 0; // 選中時它還在 RAM
|
| 658 |
+
+ uint64_t misses = 0; // 選中時它已被丟回 SSD → 這次要重讀
|
| 659 |
+
+ uint64_t evictions = 0;
|
| 660 |
+
+ uint64_t evicted_bytes = 0;
|
| 661 |
+
+ uint64_t sweeps = 0;
|
| 662 |
+
+ size_t resident_bytes = 0;
|
| 663 |
+
+ size_t budget_bytes = 0;
|
| 664 |
+
+};
|
| 665 |
+
+
|
| 666 |
+
+// 讀環境變數。必須在 register_model() 之前呼叫一次。
|
| 667 |
+
+// ST_PAGER=0 完整關閉(回到上游行為,做 A/B 用)
|
| 668 |
+
+// ST_RAM_BUDGET_MB RAM 預算(llama_server.sh 算好)
|
| 669 |
+
+// ST_RESERVE_MB KV + compute 預留,從預算裡扣掉
|
| 670 |
+
+// ST_ARENA_MB 直接指定 expert 可常駐的量(有值就優先用)
|
| 671 |
+
+// ST_HOT_TOKENS=8 最近 N 個 tick 用過的 expert 不淘汰
|
| 672 |
+
+// ST_VERBOSE=1 印淘汰紀錄
|
| 673 |
+
+// ST_STATS_FILE=path 收到 SIGUSR2 時把統計寫成 JSON
|
| 674 |
+
+void set_config_from_env();
|
| 675 |
+
+
|
| 676 |
+
+// 模型載入完之後呼叫。tensors_by_name 是 llama.cpp 的 tensors_by_name。
|
| 677 |
+
+// 回傳 false 表示這個模型沒有被分頁(呼叫端繼續走上游路徑)。
|
| 678 |
+
+bool register_model(const char * arch,
|
| 679 |
+
+ void * mapping_base, size_t mapping_size, int fd,
|
| 680 |
+
+ const std::map<std::string, ggml_tensor *> & tensors_by_name);
|
| 681 |
+
+
|
| 682 |
+
+// 在每層 top-k 選完 expert 之後插入的副作用 op。
|
| 683 |
+
+// 回傳 nullptr 表示不插入。呼叫端要把回傳值 ggml_build_forward_expand 進圖。
|
| 684 |
+
+ggml_tensor * build_routing_op(ggml_context * ctx, ggml_tensor * selected_experts,
|
| 685 |
+
+ int layer, int n_expert, int n_expert_used);
|
| 686 |
+
+
|
| 687 |
+
+bool enabled();
|
| 688 |
+
+stats get_stats();
|
| 689 |
+
+void dump_stats(const char * path);
|
| 690 |
+
+
|
| 691 |
+
+// 讓 SIGUSR2 可以觸發 dump(由 llama.cpp 呼叫端註冊)
|
| 692 |
+
+void install_signal_handler();
|
| 693 |
+
+
|
| 694 |
+
+} // namespace st
|
| 695 |
+
|