Remove duplicate and generated files
Browse files- sdq-ling/__pycache__/verify_ling.cpython-312.pyc +0 -0
- sdq-ling/sdq-ling/0001-sdq-ling.patch +0 -231
- sdq-ling/sdq-ling/audit_io.sh +0 -56
- sdq-ling/sdq-ling/ggml_deq.cpp +0 -40
- sdq-ling/sdq-ling/make_io_audit.py +0 -148
- sdq-ling/sdq-ling/pagecache_resident.cpp +0 -61
- sdq-ling/sdq-ling/sdq_buft.cpp +0 -240
- sdq-ling/sdq-ling/sdq_cli.cpp +0 -844
- sdq-ling/sdq-ling/sdq_moe.cpp +0 -735
- sdq-ling/sdq-ling/sdq_pager.cpp +0 -2292
- sdq-ling/sdq-ling/sdq_pager.h +0 -471
- sdq-ling/sdq-ling/serve.sh +0 -26
- sdq-ling/sdq-ling/strict_bench.py +0 -336
- sdq-ling/sdq-ling/verify_dequant.py +0 -51
- sdq-ling/sdq-ling/verify_ling.py +0 -219
- sdq-ling/sdq-ling/verify_moe_layer.py +0 -154
- sdq-ling/sdq-ling/watch_io.py +0 -119
sdq-ling/__pycache__/verify_ling.cpython-312.pyc
DELETED
|
Binary file (15.5 kB)
|
|
|
sdq-ling/sdq-ling/0001-sdq-ling.patch
DELETED
|
@@ -1,231 +0,0 @@
|
|
| 1 |
-
diff --git a/ggml/src/ggml-cpu/amx/amx.cpp b/ggml/src/ggml-cpu/amx/amx.cpp
|
| 2 |
-
index e90c279..49be9e1 100644
|
| 3 |
-
--- a/ggml/src/ggml-cpu/amx/amx.cpp
|
| 4 |
-
+++ b/ggml/src/ggml-cpu/amx/amx.cpp
|
| 5 |
-
@@ -152,6 +152,13 @@ class extra_buffer_type : ggml::cpu::extra_buffer_type {
|
| 6 |
-
if (!ggml_is_contiguous(src0) || !ggml_is_contiguous(src1)) {
|
| 7 |
-
return false;
|
| 8 |
-
}
|
| 9 |
-
+ // SDQ: the AMX weight repack (ggml_backend_amx_convert_weight) only converts the
|
| 10 |
-
+ // ne0 x ne1 slice of a tensor, so a 3D/4D weight would be left with unconverted
|
| 11 |
-
+ // data in the remaining slices. MLA attention feeds a 3D wv_b to MUL_MAT, which
|
| 12 |
-
+ // lands here and then reads garbage (NaN) - keep 3D weights in the CPU buffer.
|
| 13 |
-
+ if (src0->ne[2] > 1 || src0->ne[3] > 1) {
|
| 14 |
-
+ return false;
|
| 15 |
-
+ }
|
| 16 |
-
if (!src0->buffer || src0->buffer->buft != ggml_backend_amx_buffer_type()) {
|
| 17 |
-
return false;
|
| 18 |
-
}
|
| 19 |
-
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
|
| 20 |
-
index 97250c2..1fcfb6e 100644
|
| 21 |
-
--- a/src/CMakeLists.txt
|
| 22 |
-
+++ b/src/CMakeLists.txt
|
| 23 |
-
@@ -18,6 +18,9 @@ set(LLAMA_CORE_SOURCES
|
| 24 |
-
llama-cparams.cpp
|
| 25 |
-
llama-grammar.cpp
|
| 26 |
-
llama-graph.cpp
|
| 27 |
-
+ sdq_pager.cpp
|
| 28 |
-
+ sdq_buft.cpp
|
| 29 |
-
+ sdq_moe.cpp
|
| 30 |
-
llama-hparams.cpp
|
| 31 |
-
llama-impl.cpp
|
| 32 |
-
llama-io.cpp
|
| 33 |
-
@@ -51,6 +54,11 @@ add_library(llama
|
| 34 |
-
${LLAMA_MODELS_SOURCES}
|
| 35 |
-
)
|
| 36 |
-
|
| 37 |
-
+# SDQ: chat / benchmark front end (links only llama + ggml)
|
| 38 |
-
+add_executable(sdq-chat sdq_cli.cpp)
|
| 39 |
-
+target_link_libraries(sdq-chat PRIVATE llama)
|
| 40 |
-
+target_include_directories(sdq-chat PRIVATE .)
|
| 41 |
-
+
|
| 42 |
-
set_target_properties(llama PROPERTIES
|
| 43 |
-
VERSION ${LLAMA_VERSION_BASE}
|
| 44 |
-
SOVERSION ${LLAMA_VERSION_MAJOR}
|
| 45 |
-
@@ -61,11 +69,14 @@ set_target_properties(llama PROPERTIES
|
| 46 |
-
|
| 47 |
-
# exclude non-model sources from unity build
|
| 48 |
-
set_source_files_properties(${LLAMA_CORE_SOURCES} ../include/llama.h unicode.h
|
| 49 |
-
+ sdq_pager.cpp sdq_buft.cpp sdq_moe.cpp
|
| 50 |
-
PROPERTIES SKIP_UNITY_BUILD_INCLUSION ON)
|
| 51 |
-
|
| 52 |
-
configure_file(llama-version.h.in ${CMAKE_CURRENT_BINARY_DIR}/llama-version.h @ONLY)
|
| 53 |
-
|
| 54 |
-
target_include_directories(llama PRIVATE . ${CMAKE_CURRENT_BINARY_DIR})
|
| 55 |
-
+# SDQ needs ggml internal headers (custom buffer type + quantized dot kernels)
|
| 56 |
-
+target_include_directories(llama PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../ggml/src)
|
| 57 |
-
target_include_directories(llama PUBLIC ../include)
|
| 58 |
-
target_compile_features (llama PRIVATE cxx_std_17) # don't bump
|
| 59 |
-
|
| 60 |
-
diff --git a/src/llama-context.cpp b/src/llama-context.cpp
|
| 61 |
-
index 9dcaf05..4fd893d 100644
|
| 62 |
-
--- a/src/llama-context.cpp
|
| 63 |
-
+++ b/src/llama-context.cpp
|
| 64 |
-
@@ -4,6 +4,9 @@
|
| 65 |
-
#include "llama-arch.h"
|
| 66 |
-
#include "llama-graph.h"
|
| 67 |
-
#include "llama-impl.h"
|
| 68 |
-
+
|
| 69 |
-
+// SDQ: expert pager
|
| 70 |
-
+#include "sdq_pager.h"
|
| 71 |
-
#include "llama-batch.h"
|
| 72 |
-
#include "llama-io.h"
|
| 73 |
-
#include "llama-memory.h"
|
| 74 |
-
@@ -2122,6 +2125,12 @@ int llama_context::decode(const llama_batch_ext & batch_inp) {
|
| 75 |
-
// wait for the computation to finish (automatically done when obtaining the model output)
|
| 76 |
-
//synchronize();
|
| 77 |
-
|
| 78 |
-
+ // ---- SDQ: 收掉這一步 decode 的 accounting。
|
| 79 |
-
+ // 這是所有前端都會經過的單一出口;sdq-chat 自己另外驅動,所以不會重複計數。
|
| 80 |
-
+ if (sdq::sdq_enabled()) {
|
| 81 |
-
+ sdq::sdq_on_decode((int) n_tokens_all);
|
| 82 |
-
+ }
|
| 83 |
-
+
|
| 84 |
-
return 0;
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
diff --git a/src/llama-graph.cpp b/src/llama-graph.cpp
|
| 88 |
-
index 23e4c90..4769929 100644
|
| 89 |
-
--- a/src/llama-graph.cpp
|
| 90 |
-
+++ b/src/llama-graph.cpp
|
| 91 |
-
@@ -1,5 +1,7 @@
|
| 92 |
-
#include "llama-graph.h"
|
| 93 |
-
|
| 94 |
-
+#include "sdq_pager.h"
|
| 95 |
-
+
|
| 96 |
-
#include "llama-impl.h"
|
| 97 |
-
#include "llama-model.h"
|
| 98 |
-
#include "llama-moe-cache.h"
|
| 99 |
-
@@ -2211,6 +2213,40 @@ ggml_tensor * llm_graph_context::build_moe_ffn(
|
| 100 |
-
//call early so that topk-moe can be used
|
| 101 |
-
ggml_build_forward_expand(gf, weights);
|
| 102 |
-
|
| 103 |
-
+ // SDQ expert pager: route the selected experts through the SSD->RAM pager
|
| 104 |
-
+ // instead of ggml_mul_mat_id, so the expert weights never have to be resident.
|
| 105 |
-
+ // The router (logits / gating / group top-k / weight norm) is still computed
|
| 106 |
-
+ // by ggml above; only "evaluate the selected experts and accumulate" moves.
|
| 107 |
-
+ if (const char * dp = getenv("SDQ_GRAPH_DUMP")) {
|
| 108 |
-
+ const int dl = getenv("SDQ_GRAPH_DUMP_LAYER") ? atoi(getenv("SDQ_GRAPH_DUMP_LAYER")) : -1;
|
| 109 |
-
+ if (dl < 0 || il == dl) {
|
| 110 |
-
+ ggml_tensor * c3 = ggml_reshape_3d(ctx0, cur, n_embd, 1, n_tokens);
|
| 111 |
-
+ ggml_build_forward_expand(gf, c3);
|
| 112 |
-
+ ggml_build_forward_expand(gf, selected_experts);
|
| 113 |
-
+ ggml_build_forward_expand(gf, weights);
|
| 114 |
-
+ std::string path = std::string(dp) + "." + std::to_string(il);
|
| 115 |
-
+ FILE * f = fopen(path.c_str(), "wb");
|
| 116 |
-
+ if (f) {
|
| 117 |
-
+ const int32_t meta[8] = { (int32_t) n_embd, (int32_t) n_tokens, (int32_t) n_expert_used,
|
| 118 |
-
+ (int32_t) n_expert, (int32_t) (sdq::sdq_pager_active_for_layer(il) ? 1 : 0), 0, 0, 0 };
|
| 119 |
-
+ fwrite(meta, sizeof(int32_t), 8, f);
|
| 120 |
-
+ fwrite(c3->data, sizeof(float), (size_t) (n_embd * n_tokens), f);
|
| 121 |
-
+ fwrite(selected_experts->data, sizeof(int32_t), (size_t) (n_expert_used * n_tokens), f);
|
| 122 |
-
+ fwrite(weights->data, sizeof(float), (size_t) (n_expert_used * n_tokens), f);
|
| 123 |
-
+ fclose(f);
|
| 124 |
-
+ }
|
| 125 |
-
+ }
|
| 126 |
-
+ }
|
| 127 |
-
+
|
| 128 |
-
+ if (sdq::sdq_pager_active_for_layer(il) && cur->ne[0] == n_embd) {
|
| 129 |
-
+ const float swiglu_limit = il >= 0 ? hparams.swiglu_clamp_exp[il] : 0.0f;
|
| 130 |
-
+ ggml_tensor * moe_out = sdq::sdq_build_moe(gf, ctx0, cur, selected_experts, weights, il,
|
| 131 |
-
+ n_expert, n_expert_used, swiglu_limit);
|
| 132 |
-
+ cb(moe_out, "ffn_moe_out", il);
|
| 133 |
-
+ moe_out = sdq::sdq_maybe_dump_tensor(ctx0, moe_out, il);
|
| 134 |
-
+ return moe_out;
|
| 135 |
-
+ }
|
| 136 |
-
+
|
| 137 |
-
// the experts of host-resident layers may be read from the MoE cache
|
| 138 |
-
ggml_tensor * slots = build_moe_cache_slots(selected_experts, up_exps, gate_exps, down_exps, gate_up_exps, il);
|
| 139 |
-
|
| 140 |
-
@@ -2415,6 +2451,9 @@ ggml_tensor * llm_graph_context::build_moe_ffn(
|
| 141 |
-
|
| 142 |
-
cb(moe_out, "ffn_moe_out", il);
|
| 143 |
-
|
| 144 |
-
+ // dump the native path too, so it can be diffed against the paged path
|
| 145 |
-
+ moe_out = sdq::sdq_maybe_dump_tensor(ctx0, moe_out, il);
|
| 146 |
-
+
|
| 147 |
-
return moe_out;
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
@@ -2798,7 +2837,16 @@ ggml_tensor * llm_graph_context::build_attn_mha(
|
| 151 |
-
#else
|
| 152 |
-
// It's preferable to do the calculation as a matrix-matrix multiplication with n_tokens in dimension 1.
|
| 153 |
-
// The permutations are noops and only change how the tensor data is interpreted.
|
| 154 |
-
+ // FLASH_ATTN_EXT produces F16, but the CPU backend cannot run MUL_MAT with a
|
| 155 |
-
+ // quantized v_mla and F16 activations (widening requires vec_dot_type == F32),
|
| 156 |
-
+ // and a quantized weight in the AMX buffer requires contiguous activations.
|
| 157 |
-
+ if (cur->type != GGML_TYPE_F32 && v_mla->type != GGML_TYPE_F16 && v_mla->type != GGML_TYPE_F32) {
|
| 158 |
-
+ cur = ggml_cast(ctx0, cur, GGML_TYPE_F32);
|
| 159 |
-
+ }
|
| 160 |
-
cur = ggml_permute(ctx0, cur, 0, 2, 1, 3);
|
| 161 |
-
+ if (!ggml_is_contiguous(cur)) {
|
| 162 |
-
+ cur = ggml_cont(ctx0, cur);
|
| 163 |
-
+ }
|
| 164 |
-
cur = ggml_mul_mat(ctx0, v_mla, cur);
|
| 165 |
-
cb(cur, "fattn_mla", il);
|
| 166 |
-
cur = ggml_permute(ctx0, cur, 0, 2, 1, 3);
|
| 167 |
-
diff --git a/src/llama-model.cpp b/src/llama-model.cpp
|
| 168 |
-
index 2e86914..fbf06cb 100644
|
| 169 |
-
--- a/src/llama-model.cpp
|
| 170 |
-
+++ b/src/llama-model.cpp
|
| 171 |
-
@@ -1,5 +1,7 @@
|
| 172 |
-
#include "llama-model.h"
|
| 173 |
-
|
| 174 |
-
+#include "sdq_pager.h"
|
| 175 |
-
+
|
| 176 |
-
#include "llama-arch.h"
|
| 177 |
-
#include "llama-ext.h"
|
| 178 |
-
#include "llama-hparams.h"
|
| 179 |
-
@@ -1834,7 +1836,9 @@ bool llama_model_base::load_tensors(llama_model_loader & ml) {
|
| 180 |
-
// per-tensor activation precision policy
|
| 181 |
-
prec_policy.load(ml, *this);
|
| 182 |
-
|
| 183 |
-
- ml.init_mappings(true, use_mlock ? &pimpl->mlock_mmaps : nullptr);
|
| 184 |
-
+ // SDQ: the expert weights are read by the pager with its own pread calls, so the
|
| 185 |
-
+ // loader must not MAP_POPULATE / WILLNEED the whole model file into page cache.
|
| 186 |
-
+ ml.init_mappings(!sdq::sdq_enabled(), use_mlock ? &pimpl->mlock_mmaps : nullptr);
|
| 187 |
-
pimpl->mappings.reserve(ml.mappings.size());
|
| 188 |
-
|
| 189 |
-
// create the backend buffers
|
| 190 |
-
diff --git a/src/llama.cpp b/src/llama.cpp
|
| 191 |
-
index 9beb148..2633d96 100644
|
| 192 |
-
--- a/src/llama.cpp
|
| 193 |
-
+++ b/src/llama.cpp
|
| 194 |
-
@@ -11,6 +11,9 @@
|
| 195 |
-
#include "llama-model-saver.h"
|
| 196 |
-
#include "llama-model.h"
|
| 197 |
-
|
| 198 |
-
+// SDQ: expert pager(SSD -> RAM 分頁),見 sdq-ling/0001-sdq-ling.patch
|
| 199 |
-
+#include "sdq_pager.h"
|
| 200 |
-
+
|
| 201 |
-
#include "ggml.h"
|
| 202 |
-
#include "ggml-cpp.h"
|
| 203 |
-
#include "ggml-backend.h"
|
| 204 |
-
@@ -404,6 +407,27 @@ static struct llama_model * llama_model_load_from_file_impl(
|
| 205 |
-
// TODO: remove
|
| 206 |
-
ggml_time_init();
|
| 207 |
-
|
| 208 |
-
+ // ---- SDQ: 把 MoE expert 權重交給 pager(SSD -> RAM arena 分頁)----
|
| 209 |
-
+ // 放在模型載入的唯一入口,這樣 sdq-chat / llama-server / llama-cli 行為一致。
|
| 210 |
-
+ // SDQ_PAGER=0 關閉,回到上游行為(A/B 對照用)
|
| 211 |
-
+ // SDQ_RAM_BUDGET_MB RAM 總預算,arena 會配合縮減(沒有給就用實體 RAM 的一部分)
|
| 212 |
-
+ // SDQ_ARENA_MB 直接指定 arena 大小
|
| 213 |
-
+ // SDQ_RESERVE_MB KV/SSM state + compute buffer 的預留
|
| 214 |
-
+ // SDQ_IO_THREADS I/O 執行緒數、SDQ_PREFETCH=0 關背景預取、SDQ_STATS_FILE 指標輸出
|
| 215 |
-
+ {
|
| 216 |
-
+ const char * sdq_off = getenv("SDQ_PAGER");
|
| 217 |
-
+ if ((!sdq_off || strcmp(sdq_off, "0") != 0) && !params.vocab_only && !path_model.empty()) {
|
| 218 |
-
+ if (sdq::sdq_init(path_model)) {
|
| 219 |
-
+ // 沒有這一行,load_tensors() 會把整個模型檔要求預取進 page cache,
|
| 220 |
-
+ // 正是分頁要避免的事(I/O 稽核見 results/io_audit.md)。
|
| 221 |
-
+ sdq::sdq_set_enabled(true);
|
| 222 |
-
+ sdq::sdq_install_buft_overrides(¶ms);
|
| 223 |
-
+ // 權重是 pread 進 arena 的,不該再讓後端自己 offload 一次。
|
| 224 |
-
+ params.n_gpu_layers = 0;
|
| 225 |
-
+ }
|
| 226 |
-
+ }
|
| 227 |
-
+ }
|
| 228 |
-
+
|
| 229 |
-
if (!params.vocab_only && ggml_backend_reg_count() == 0) {
|
| 230 |
-
LLAMA_LOG_ERROR("%s: no backends are loaded. hint: use ggml_backend_load() or ggml_backend_load_all() to load a backend before calling this function\n", __func__);
|
| 231 |
-
return nullptr;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sdq-ling/sdq-ling/audit_io.sh
DELETED
|
@@ -1,56 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env bash
|
| 2 |
-
# I/O honesty audit: prove the expert weights are read from the block device and
|
| 3 |
-
# are NOT served from (or hidden in) the page cache.
|
| 4 |
-
#
|
| 5 |
-
# ./audit_io.sh <model-first-shard.gguf> [sdq-chat] [ram-budget-mb] [n-predict]
|
| 6 |
-
#
|
| 7 |
-
# Two runs with identical settings:
|
| 8 |
-
# A) default pager uses O_DIRECT (bypasses the cache)
|
| 9 |
-
# B) SDQ_NO_DIRECT_IO=1 + --no-fadvise control: buffered reads kept in cache
|
| 10 |
-
#
|
| 11 |
-
# If this measurement were blind, A and B would look the same. They do not:
|
| 12 |
-
# B leaves gigabytes of the GGUF resident in the page cache, A does not.
|
| 13 |
-
set -u
|
| 14 |
-
MODEL="$(readlink -f "$1")"
|
| 15 |
-
BIN="$(readlink -f "${2:-./llama.cpp/build/bin/sdq-chat}")"
|
| 16 |
-
BUDGET="${3:-8192}"
|
| 17 |
-
NPRED="${4:-32}"
|
| 18 |
-
PROMPT="Explain in detail how paging mixture-of-experts weights from SSD works, \
|
| 19 |
-
and why the working set is much smaller than the weights."
|
| 20 |
-
|
| 21 |
-
cd "$(dirname "$0")/.."
|
| 22 |
-
SECOND="$(echo "$MODEL" | sed -E 's/-00001-of-/-00002-of-/')"
|
| 23 |
-
IFS=',' read -r -a SHARDS <<< "$MODEL,$SECOND"
|
| 24 |
-
|
| 25 |
-
if [ ! -x /tmp/pagecache_resident ]; then
|
| 26 |
-
echo "building the mincore helper ..."
|
| 27 |
-
g++ -O2 -o /tmp/pagecache_resident sdq-ling/pagecache_resident.cpp
|
| 28 |
-
fi
|
| 29 |
-
|
| 30 |
-
run() {
|
| 31 |
-
local label="$1"; shift
|
| 32 |
-
echo
|
| 33 |
-
echo "================================================================"
|
| 34 |
-
echo "[audit] $label"
|
| 35 |
-
echo "================================================================"
|
| 36 |
-
WATCH_INTERVAL=0.5 "$@" python3 sdq-ling/watch_io.py "$MODEL,$SECOND" \
|
| 37 |
-
"$BIN" -m "$MODEL" -p "$PROMPT" -n "$NPRED" -c 2048 -t 16 -b 128 \
|
| 38 |
-
--temp 0 --no-chat --no-interactive --ram-budget-mb "$BUDGET" \
|
| 39 |
-
2>/dev/null | grep -E "^\[watch\]"
|
| 40 |
-
grep -E "全程 SSD 實際讀取|O_DIRECT 讀取模型檔|O_DIRECT 不可用" /tmp/watch_stderr.log | tail -3
|
| 41 |
-
grep -E "^assistant:" /tmp/watch_stdout.log | head -1
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
echo "[audit] model shards:"
|
| 45 |
-
/tmp/pagecache_resident "${SHARDS[@]}"
|
| 46 |
-
|
| 47 |
-
run "A) default (O_DIRECT)" env SDQ_RAM_BUDGET_MB="$BUDGET"
|
| 48 |
-
echo
|
| 49 |
-
echo "================================================================"
|
| 50 |
-
echo "[audit] B) control: buffered reads + page cache kept"
|
| 51 |
-
echo "================================================================"
|
| 52 |
-
WATCH_INTERVAL=0.5 SDQ_NO_DIRECT_IO=1 python3 sdq-ling/watch_io.py "$MODEL,$SECOND" \
|
| 53 |
-
"$BIN" -m "$MODEL" -p "$PROMPT" -n "$NPRED" -c 2048 -t 16 -b 128 \
|
| 54 |
-
--temp 0 --no-chat --no-interactive --no-fadvise --ram-budget-mb "$BUDGET" \
|
| 55 |
-
2>/dev/null | grep -E "^\[watch\]"
|
| 56 |
-
grep -E "全程 SSD 實際讀取|O_DIRECT 不可用" /tmp/watch_stderr.log | tail -2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sdq-ling/sdq-ling/ggml_deq.cpp
DELETED
|
@@ -1,40 +0,0 @@
|
|
| 1 |
-
// Dump one ggml-dequantized row so the NumPy reference can be checked against
|
| 2 |
-
// ggml's own dequantizers. usage: ggml_deq <type> <file> <offset> <n>
|
| 3 |
-
#include "ggml.h"
|
| 4 |
-
#include "ggml-quants.h"
|
| 5 |
-
|
| 6 |
-
#include <cstdio>
|
| 7 |
-
#include <cstdlib>
|
| 8 |
-
|
| 9 |
-
int main(int argc, char ** argv) {
|
| 10 |
-
if (argc < 5) {
|
| 11 |
-
fprintf(stderr, "usage: %s <type> <file> <offset> <n>\n", argv[0]);
|
| 12 |
-
return 1;
|
| 13 |
-
}
|
| 14 |
-
const int tt = atoi(argv[1]);
|
| 15 |
-
const char * path = argv[2];
|
| 16 |
-
const long long off = atoll(argv[3]);
|
| 17 |
-
const int n = atoi(argv[4]);
|
| 18 |
-
|
| 19 |
-
FILE * f = fopen(path, "rb");
|
| 20 |
-
if (!f) { perror("open"); return 1; }
|
| 21 |
-
const size_t ts = ggml_row_size((ggml_type) tt, n);
|
| 22 |
-
void * buf = malloc(ts);
|
| 23 |
-
if (fseek(f, (long) off, SEEK_SET) != 0) { fprintf(stderr, "seek\n"); return 1; }
|
| 24 |
-
if (fread(buf, 1, ts, f) != ts) { fprintf(stderr, "short read\n"); return 1; }
|
| 25 |
-
fclose(f);
|
| 26 |
-
|
| 27 |
-
float * y = (float *) malloc(sizeof(float) * n);
|
| 28 |
-
switch (tt) {
|
| 29 |
-
case 12: dequantize_row_q4_K((const block_q4_K *) buf, y, n); break;
|
| 30 |
-
case 13: dequantize_row_q5_K((const block_q5_K *) buf, y, n); break;
|
| 31 |
-
case 14: dequantize_row_q6_K((const block_q6_K *) buf, y, n); break;
|
| 32 |
-
case 7: dequantize_row_q5_1((const block_q5_1 *) buf, y, n); break;
|
| 33 |
-
case 8: dequantize_row_q8_0((const block_q8_0 *) buf, y, n); break;
|
| 34 |
-
default: fprintf(stderr, "type %d not handled\n", tt); return 1;
|
| 35 |
-
}
|
| 36 |
-
fwrite(y, sizeof(float), n, stdout);
|
| 37 |
-
free(y);
|
| 38 |
-
free(buf);
|
| 39 |
-
return 0;
|
| 40 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sdq-ling/sdq-ling/make_io_audit.py
DELETED
|
@@ -1,148 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Turn results/io_audit_raw.log (from sdq-ling/audit_io.sh) into io_audit.{json,md}."""
|
| 3 |
-
import json
|
| 4 |
-
import re
|
| 5 |
-
from pathlib import Path
|
| 6 |
-
|
| 7 |
-
R = Path('/root/app/results')
|
| 8 |
-
raw = (R / 'io_audit_raw.log').read_text()
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
def seg(marker):
|
| 12 |
-
i = raw.index(marker)
|
| 13 |
-
j = raw.find('[audit]', i + 10)
|
| 14 |
-
return raw[i:j if j > 0 else len(raw)]
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
def one(s, pat, cast=float):
|
| 18 |
-
m = re.search(pat, s)
|
| 19 |
-
return cast(m.group(1)) if m else None
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
def allv(s, pat):
|
| 23 |
-
return [float(m.group(1)) for m in re.finditer(pat, s)]
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
def parse(marker, mode, note):
|
| 27 |
-
s = seg(marker)
|
| 28 |
-
rb = allv(s, r'read_bytes=\s+([\d.]+) GiB')
|
| 29 |
-
res = allv(s, r'model_file_resident=\s*([\d.]+) GiB')
|
| 30 |
-
return {
|
| 31 |
-
'mode': mode,
|
| 32 |
-
'device_read_gib_max': max(rb) if rb else None,
|
| 33 |
-
'device_read_gib_samples': len(rb),
|
| 34 |
-
'model_file_resident_gib_peak': one(s, r'peak model_file_resident in page cache :\s+([\d.]+)'),
|
| 35 |
-
'model_file_resident_gib_final': one(s, r'final model_file_resident\s+:\s+([\d.]+)'),
|
| 36 |
-
'rss_anon_gib_peak': one(s, r'peak process RssAnon\s+:\s+([\d.]+)'),
|
| 37 |
-
'rss_file_gib_peak': one(s, r'peak process RssFile\s+:\s+([\d.]+)'),
|
| 38 |
-
'system_cached_delta_gib_peak': one(s, r'system Cached delta \(peak\)\s+:\s+([+\-][\d.]+)'),
|
| 39 |
-
'sdq_chat_reported_ssd_gb': one(s, r'全程 SSD 實際讀取: ([\d.]+) GB'),
|
| 40 |
-
'samples_tail': [f'read_bytes={b:.3f} GiB model_file_resident={r:.3f} GiB'
|
| 41 |
-
for b, r in list(zip(rb, res))[-6:]],
|
| 42 |
-
'note': note,
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
doc = {
|
| 47 |
-
'what': 'I/O honesty audit: are the expert weights really coming from the block device, '
|
| 48 |
-
'or is the page cache quietly serving them?',
|
| 49 |
-
'method': 'per-file page-cache eviction (posix_fadvise(DONTNEED) on both GGUF shards), '
|
| 50 |
-
'then mincore(2) over the whole 71.72 GiB of shard data every 0.5 s while '
|
| 51 |
-
'/proc/<pid>/io read_bytes and /proc/<pid>/status RssAnon/RssFile are sampled.',
|
| 52 |
-
'tools': ['sdq-ling/pagecache_resident.cpp', 'sdq-ling/watch_io.py', 'sdq-ling/audit_io.sh'],
|
| 53 |
-
'settings': {'ram_budget_mb': 8192, 'ctx': 2048, 'ubatch': 128, 'threads': 16,
|
| 54 |
-
'n_predict': 32, 'temp': 0},
|
| 55 |
-
'results': {
|
| 56 |
-
'A_default_o_direct': parse('A) default (O_DIRECT)', 'O_DIRECT (pager default)',
|
| 57 |
-
'the model file stays at ~2.09 GiB resident, which is exactly the non-expert '
|
| 58 |
-
'weights; the tens of GiB of expert reads leave no page-cache footprint'),
|
| 59 |
-
'B_control_buffered_cache_kept': parse(
|
| 60 |
-
'B) control: buffered reads + page cache kept',
|
| 61 |
-
'SDQ_NO_DIRECT_IO=1 + --no-fadvise',
|
| 62 |
-
'same reads, but the page cache retains them; residency tracks the read '
|
| 63 |
-
'volume ~1:1, which is what a buffered implementation would cost'),
|
| 64 |
-
},
|
| 65 |
-
'conclusion': [
|
| 66 |
-
'The pager reads expert weights with O_DIRECT: read_bytes (real device I/O) grows '
|
| 67 |
-
'while mincore residency of the GGUF stays flat.',
|
| 68 |
-
'The only resident part of the model file is the 2.26 GiB of non-expert weights '
|
| 69 |
-
'that llama.cpp keeps in RAM (1.54 GiB of it file-backed via mmap). Not one byte '
|
| 70 |
-
'of the 69.46 GiB of expert weights reaches the page cache.',
|
| 71 |
-
'The control run proves the measurement is sensitive: with buffered reads the same '
|
| 72 |
-
'file accumulates tens of GiB of page cache.',
|
| 73 |
-
'Memory numbers: an 8 GiB pager budget yields 5.83 GiB anonymous RSS '
|
| 74 |
-
'(3.54 GiB arena + ~2.3 GiB non-expert weights) + 1.73 GiB file-backed RSS '
|
| 75 |
-
'= 7.0 GiB total RSS.',
|
| 76 |
-
'sdq-chat\'s "SSD read" line starts its counter after model load, so it excludes '
|
| 77 |
-
'the ~3.9 GiB the loader itself reads; the watcher numbers above cover the whole '
|
| 78 |
-
'process lifetime.',
|
| 79 |
-
],
|
| 80 |
-
}
|
| 81 |
-
(R / 'io_audit.json').write_text(json.dumps(doc, ensure_ascii=False, indent=2) + '\n')
|
| 82 |
-
|
| 83 |
-
A = doc['results']['A_default_o_direct']
|
| 84 |
-
B = doc['results']['B_control_buffered_cache_kept']
|
| 85 |
-
md = f"""# I/O 稽核:expert 權重真的有從 block device 讀嗎?
|
| 86 |
-
|
| 87 |
-
問題:如果 pager 其實是被 page cache 服務的,那「69 GiB 的權重留在 SSD、每次按需讀取」這句話就是假的。
|
| 88 |
-
|
| 89 |
-
## 方法
|
| 90 |
-
|
| 91 |
-
1. 用 `posix_fadvise(POSIX_FADV_DONTNEED)` 把兩個 GGUF shard(71.72 GiB)**逐檔案**從 page cache 驅逐
|
| 92 |
-
(不依賴 `/proc/sys/vm/drop_caches`,容器裡也做得到),並用 `mincore` 確認回到 0.000 GiB resident。
|
| 93 |
-
2. 開跑 `sdq-chat`,每 0.5 s 抽樣:
|
| 94 |
-
* `/proc/<pid>/io` 的 `read_bytes` = 真正從 block device 讀了多少
|
| 95 |
-
* `mincore(2)` 掃過整個模型檔 = 有多少頁留在 page cache
|
| 96 |
-
* `/proc/<pid>/status` 的 `RssAnon` / `RssFile`
|
| 97 |
-
3. 跑兩次,其他設定完全相同:
|
| 98 |
-
* **A(預設)**:pager 用 `O_DIRECT` 讀
|
| 99 |
-
* **B(對照組)**:`SDQ_NO_DIRECT_IO=1` + `--no-fadvise` = buffered 讀且**保留** page cache
|
| 100 |
-
|
| 101 |
-
B 的存在是為了證明這個量測**有鑑別力**:如果看不出差別,A 的數字也沒有意義。
|
| 102 |
-
|
| 103 |
-
## 結果
|
| 104 |
-
|
| 105 |
-
| | A:預設(O_DIRECT) | B:對照組(buffered + 保留 cache) |
|
| 106 |
-
|---|---:|---:|
|
| 107 |
-
| 真正讀進來的量(read_bytes 峰值) | {A['device_read_gib_max']:.2f} GiB | {B['device_read_gib_max']:.2f} GiB |
|
| 108 |
-
| **模型檔留在 page cache 的峰值** | **{A['model_file_resident_gib_peak']:.3f} GiB** | **{B['model_file_resident_gib_peak']:.2f} GiB** |
|
| 109 |
-
| 程序 RssAnon 峰值 | {A['rss_anon_gib_peak']:.2f} GiB | {B['rss_anon_gib_peak']:.2f} GiB |
|
| 110 |
-
| 程序 RssFile 峰值 | {A['rss_file_gib_peak']:.2f} GiB | {B['rss_file_gib_peak']:.2f} GiB |
|
| 111 |
-
| 系統 Cached 增量(峰值) | {A['system_cached_delta_gib_peak']:+.2f} GiB | {B['system_cached_delta_gib_peak']:+.2f} GiB |
|
| 112 |
-
| `sdq-chat` 自報「SSD 實際讀取」 | {A['sdq_chat_reported_ssd_gb']:.2f} GB | {B['sdq_chat_reported_ssd_gb']:.2f} GB |
|
| 113 |
-
|
| 114 |
-
A 的時間序列(最後 6 個抽樣)——`read_bytes` 一路爬到
|
| 115 |
-
{A['device_read_gib_max']:.1f} GiB,而模型檔 resident 從 2.09 GiB 之後**完全不動**:
|
| 116 |
-
|
| 117 |
-
```
|
| 118 |
-
{chr(10).join(A['samples_tail'])}
|
| 119 |
-
```
|
| 120 |
-
|
| 121 |
-
## 結論
|
| 122 |
-
|
| 123 |
-
1. **pager 讀 expert 權重走 `O_DIRECT`**:`read_bytes`(真實裝置 I/O)上升時,
|
| 124 |
-
模型檔的 page cache 用量沒有增加。
|
| 125 |
-
2. 模型檔唯一留在 page cache 的部分是 **{A['model_file_resident_gib_peak']:.2f} GiB 的非 expert 權重**
|
| 126 |
-
(shard1 1.54 + shard2 0.55 GiB;非 expert 權重總共 2.26 GiB),
|
| 127 |
-
那是 llama.cpp 本來就該留在 RAM 的東西,其中約 1.54 GiB 以 mmap file-backed 方式存在。
|
| 128 |
-
**69.46 GiB 的 expert 權重一個位元組都沒有進 page cache。**
|
| 129 |
-
3. **對照組證明量測有鑑別力**:同樣的讀取量改成 buffered,模型檔會在 page cache 堆到
|
| 130 |
-
{B['model_file_resident_gib_peak']:.1f} GiB。如果量測是瞎的,A 和 B 會一樣。
|
| 131 |
-
4. 對記憶體數字的影響:8 GiB 的 pager 預算實際是
|
| 132 |
-
**{A['rss_anon_gib_peak']:.2f} GiB 匿名 RSS(其中 arena 3.54 GiB、非 expert 權重 ~2.3 GiB)
|
| 133 |
-
+ {A['rss_file_gib_peak']:.2f} GiB 檔案對映 RSS = 7.0 GiB 總 RSS**。
|
| 134 |
-
5. 兩個數字的差異是可解釋的:`sdq-chat` 的「SSD 實際讀取」在**載入模型之後**才開始計,
|
| 135 |
-
所以不含 loader 自己讀的 ~3.9 GiB;watcher 的數字涵蓋整個行程生命週期。
|
| 136 |
-
|
| 137 |
-
> 順帶修掉一個原本會誤導的報表 bug:`sdq-chat` 的記憶體摘要讀 `/proc/self/smaps_rollup` 的
|
| 138 |
-
> `RssFile:`,但本機核心的 `smaps_rollup` **沒有這個 key**(只有 `Anonymous:`),
|
| 139 |
-
> 於是長期都印「檔案對映 RSS 0 MB」。已改成讀 `/proc/self/status` 的
|
| 140 |
-
> `RssAnon:` / `RssFile:` / `VmRSS:`,並在記憶體實驗裡一併記錄三個數字。
|
| 141 |
-
|
| 142 |
-
原始輸出:`results/io_audit_raw.log`;結構化資料:`results/io_audit.json`
|
| 143 |
-
重跑:`./sdq-ling/audit_io.sh <model.gguf> <sdq-chat> 8192 32`
|
| 144 |
-
"""
|
| 145 |
-
(R / 'io_audit.md').write_text(md)
|
| 146 |
-
print('wrote io_audit.json / io_audit.md')
|
| 147 |
-
print('A:', A['device_read_gib_max'], A['model_file_resident_gib_peak'])
|
| 148 |
-
print('B:', B['device_read_gib_max'], B['model_file_resident_gib_peak'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sdq-ling/sdq-ling/pagecache_resident.cpp
DELETED
|
@@ -1,61 +0,0 @@
|
|
| 1 |
-
// Count how many pages of a file are resident in the page cache (mincore(2)).
|
| 2 |
-
// This is how we check "did the pager really bypass the page cache?": if the
|
| 3 |
-
// expert weights were served from cache, the model file would show up as resident.
|
| 4 |
-
//
|
| 5 |
-
// usage: pagecache_resident <file> [file...]
|
| 6 |
-
#include <fcntl.h>
|
| 7 |
-
#include <stdio.h>
|
| 8 |
-
#include <stdlib.h>
|
| 9 |
-
#include <string.h>
|
| 10 |
-
#include <sys/mman.h>
|
| 11 |
-
#include <fcntl.h>
|
| 12 |
-
#include <sys/stat.h>
|
| 13 |
-
#include <unistd.h>
|
| 14 |
-
|
| 15 |
-
int main(int argc, char ** argv) {
|
| 16 |
-
if (argc < 2) { fprintf(stderr, "usage: %s [--evict] <file> [file...]\n", argv[0]); return 1; }
|
| 17 |
-
int argi = 1;
|
| 18 |
-
int evict = 0;
|
| 19 |
-
if (strcmp(argv[1], "--evict") == 0) { evict = 1; argi = 2; }
|
| 20 |
-
const long page = sysconf(_SC_PAGESIZE);
|
| 21 |
-
unsigned long long tot_r = 0, tot_s = 0;
|
| 22 |
-
|
| 23 |
-
for (int i = argi; i < argc; i++) {
|
| 24 |
-
const char * path = argv[i];
|
| 25 |
-
int fd = open(path, O_RDONLY);
|
| 26 |
-
if (fd < 0) { perror(path); return 1; }
|
| 27 |
-
struct stat st;
|
| 28 |
-
fstat(fd, &st);
|
| 29 |
-
if (evict) {
|
| 30 |
-
// per-file eviction of clean page-cache pages (no root needed)
|
| 31 |
-
posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED);
|
| 32 |
-
}
|
| 33 |
-
const size_t size = (size_t) st.st_size;
|
| 34 |
-
char * addr = (char *) mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
|
| 35 |
-
if (addr == MAP_FAILED) { perror("mmap"); return 1; }
|
| 36 |
-
|
| 37 |
-
const size_t npages = (size + page - 1) / page;
|
| 38 |
-
unsigned char * vec = (unsigned char *) malloc(npages);
|
| 39 |
-
if (mincore(addr, size, vec) != 0) { perror("mincore"); return 1; }
|
| 40 |
-
unsigned long long res = 0;
|
| 41 |
-
for (size_t p = 0; p < npages; p++) {
|
| 42 |
-
res += (vec[p] & 1);
|
| 43 |
-
}
|
| 44 |
-
free(vec);
|
| 45 |
-
munmap(addr, size);
|
| 46 |
-
close(fd);
|
| 47 |
-
|
| 48 |
-
const char * base = strrchr(path, '/');
|
| 49 |
-
base = base ? base + 1 : path;
|
| 50 |
-
printf("%-48s resident %8.3f GiB / %8.3f GiB (%5.1f %%)\n",
|
| 51 |
-
base, res * (double) page / 1073741824.0, size / 1073741824.0,
|
| 52 |
-
100.0 * (double) res * (double) page / (double) size);
|
| 53 |
-
tot_r += res * (unsigned long long) page;
|
| 54 |
-
tot_s += size;
|
| 55 |
-
}
|
| 56 |
-
if (argc > argi + 1) {
|
| 57 |
-
printf("%-48s resident %8.3f GiB / %8.3f GiB\n", "TOTAL",
|
| 58 |
-
tot_r / 1073741824.0, tot_s / 1073741824.0);
|
| 59 |
-
}
|
| 60 |
-
return 0;
|
| 61 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sdq-ling/sdq-ling/sdq_buft.cpp
DELETED
|
@@ -1,240 +0,0 @@
|
|
| 1 |
-
// SDQ expert buffer type
|
| 2 |
-
//
|
| 3 |
-
// 這是一個「假」buffer type:它宣稱接受所有張量,但實際上
|
| 4 |
-
// * 不配置任何 RAM(alloc_buffer 回傳 0 大小的 buffer)
|
| 5 |
-
// * set_tensor 忽略傳入的資料(資料來自 llama.cpp 的 mmap,我們不碰)
|
| 6 |
-
// * 真正的資料由 sdq::pager 從檔案 pread 到自己的 arena
|
| 7 |
-
//
|
| 8 |
-
// 用途:讓 llama.cpp 把 18.2 GiB 的 MoE 專家權重「掛在模型上」卻不放進 RAM,
|
| 9 |
-
// 改由分頁器管理 SSD → RAM → CPU 的三層資料流。
|
| 10 |
-
|
| 11 |
-
#include "sdq_pager.h"
|
| 12 |
-
|
| 13 |
-
#include "ggml-backend-impl.h"
|
| 14 |
-
#include "llama-mmap.h"
|
| 15 |
-
|
| 16 |
-
#include <cstdio>
|
| 17 |
-
#include <string>
|
| 18 |
-
#include <cstdlib>
|
| 19 |
-
#include <cstring>
|
| 20 |
-
#include <algorithm>
|
| 21 |
-
#include <mutex>
|
| 22 |
-
#include <unordered_map>
|
| 23 |
-
#include <vector>
|
| 24 |
-
|
| 25 |
-
namespace sdq {
|
| 26 |
-
|
| 27 |
-
// 給 tensor->data 的假位址。每個 expert 權重張量分到一小塊(預設 4 KB),
|
| 28 |
-
// 指標必須有效(ggml 會做指標運算),但內容永遠不會被讀。
|
| 29 |
-
struct expert_buffer {
|
| 30 |
-
std::vector<uint8_t> scratch;
|
| 31 |
-
};
|
| 32 |
-
|
| 33 |
-
static bool g_enabled = false;
|
| 34 |
-
|
| 35 |
-
void sdq_set_enabled(bool on) { g_enabled = on; }
|
| 36 |
-
bool sdq_enabled() { return g_enabled; }
|
| 37 |
-
|
| 38 |
-
static config g_cfg;
|
| 39 |
-
void sdq_set_config(const config & c) { g_cfg = c; }
|
| 40 |
-
const config & sdq_get_config() { return g_cfg; }
|
| 41 |
-
|
| 42 |
-
// ---------------------------------------------------------------- buffer iface
|
| 43 |
-
|
| 44 |
-
// ggml_backend_buffer_i 的欄位依賴下面的實作,先給宣告
|
| 45 |
-
struct ggml_backend_buffer_i sdq_buffer_iface();
|
| 46 |
-
|
| 47 |
-
static const char * sdq_buft_get_name(ggml_backend_buffer_type_t buft) {
|
| 48 |
-
(void) buft;
|
| 49 |
-
return "SDQ_ExpertSSD";
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
static size_t sdq_buft_get_alignment(ggml_backend_buffer_type_t buft) {
|
| 53 |
-
(void) buft;
|
| 54 |
-
return 4096;
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
static size_t sdq_buft_get_max_size(ggml_backend_buffer_type_t buft) {
|
| 58 |
-
(void) buft;
|
| 59 |
-
return SIZE_MAX;
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
// 每個 expert 權重張量只回報 4 KB:18.2 GiB 的權重「不會」被載入 RAM 的關鍵。
|
| 63 |
-
static size_t sdq_buft_get_alloc_size(ggml_backend_buffer_type_t buft, const ggml_tensor * tensor) {
|
| 64 |
-
(void) buft;
|
| 65 |
-
(void) tensor;
|
| 66 |
-
return 4096;
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
static bool sdq_buft_is_host(ggml_backend_buffer_type_t buft) {
|
| 70 |
-
(void) buft;
|
| 71 |
-
return false;
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
static ggml_backend_buffer_t sdq_buft_alloc_buffer(ggml_backend_buffer_type_t buft, size_t size) {
|
| 75 |
-
// llama.cpp 會用 get_alloc_size() 計算總大小後呼叫本函式;
|
| 76 |
-
// 我們只保留真正需要的假位址空間(每個張量 4 KB)。
|
| 77 |
-
const size_t bytes = size < 4096 ? 4096 : size;
|
| 78 |
-
auto * buf = new expert_buffer();
|
| 79 |
-
buf->scratch.assign(bytes, 0);
|
| 80 |
-
auto * b = new ggml_backend_buffer{
|
| 81 |
-
/* .iface = */ sdq_buffer_iface(),
|
| 82 |
-
/* .buft = */ buft,
|
| 83 |
-
/* .context = */ buf,
|
| 84 |
-
/* .size = */ bytes,
|
| 85 |
-
/* .usage = */ GGML_BACKEND_BUFFER_USAGE_WEIGHTS,
|
| 86 |
-
};
|
| 87 |
-
return b;
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
// 一次配置整個 context 的張量:每個張量 4 KB 假位址,實際權重 0 RAM。
|
| 91 |
-
static ggml_backend_buffer_t sdq_buft_alloc_buffer_n(ggml_backend_buffer_type_t buft,
|
| 92 |
-
ggml_tensor ** tensors, int n_tensors) {
|
| 93 |
-
const size_t slot = 4096;
|
| 94 |
-
const size_t bytes = slot * (size_t) std::max(n_tensors, 1);
|
| 95 |
-
ggml_backend_buffer_t buffer = sdq_buft_alloc_buffer(buft, bytes);
|
| 96 |
-
if (!buffer) {
|
| 97 |
-
return nullptr;
|
| 98 |
-
}
|
| 99 |
-
uint8_t * base = (uint8_t *) buffer->iface.get_base(buffer);
|
| 100 |
-
size_t off = 0;
|
| 101 |
-
for (int i = 0; i < n_tensors; i++) {
|
| 102 |
-
ggml_tensor * t = tensors[i];
|
| 103 |
-
t->data = base + off;
|
| 104 |
-
t->buffer = buffer;
|
| 105 |
-
off += slot;
|
| 106 |
-
buffer->iface.init_tensor(buffer, t);
|
| 107 |
-
}
|
| 108 |
-
return buffer;
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
static void sdq_buft_free_buffer(ggml_backend_buffer_t buffer) {
|
| 112 |
-
// 只釋放自己的資料;ggml_backend_buffer_free() 會負責 delete buffer 本體,
|
| 113 |
-
// 這裡再 delete 一次會造成 double free。
|
| 114 |
-
delete (expert_buffer *) buffer->context;
|
| 115 |
-
buffer->context = nullptr;
|
| 116 |
-
}
|
| 117 |
-
|
| 118 |
-
static void * sdq_buft_get_base(ggml_backend_buffer_t buffer) {
|
| 119 |
-
return ((expert_buffer *) buffer->context)->scratch.data();
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
static ggml_status sdq_buft_init_tensor(ggml_backend_buffer_t buffer, ggml_tensor * tensor) {
|
| 123 |
-
(void) buffer;
|
| 124 |
-
(void) tensor;
|
| 125 |
-
return GGML_STATUS_SUCCESS;
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
static void sdq_buft_memset_tensor(ggml_backend_buffer_t buffer, ggml_tensor * tensor,
|
| 129 |
-
uint8_t value, size_t offset, size_t size) {
|
| 130 |
-
(void) buffer; (void) tensor; (void) value; (void) offset; (void) size;
|
| 131 |
-
}
|
| 132 |
-
|
| 133 |
-
static void sdq_buft_set_tensor(ggml_backend_buffer_t buffer, ggml_tensor * tensor,
|
| 134 |
-
const void * data, size_t offset, size_t size) {
|
| 135 |
-
// 關鍵:忽略。llama.cpp 會把 mmap 的資料交給我們,這裡一個位元組都不落地,
|
| 136 |
-
// 專家權利永遠不會佔用 RAM。
|
| 137 |
-
(void) buffer; (void) tensor; (void) data; (void) offset; (void) size;
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
static void sdq_buft_get_tensor(ggml_backend_buffer_t buffer, const ggml_tensor * tensor,
|
| 141 |
-
void * data, size_t offset, size_t size) {
|
| 142 |
-
(void) buffer; (void) tensor; (void) offset;
|
| 143 |
-
memset(data, 0, size);
|
| 144 |
-
}
|
| 145 |
-
|
| 146 |
-
static bool sdq_buft_cpy_tensor(ggml_backend_buffer_t buffer, const ggml_tensor * src, ggml_tensor * dst) {
|
| 147 |
-
(void) buffer; (void) src; (void) dst;
|
| 148 |
-
return true;
|
| 149 |
-
}
|
| 150 |
-
|
| 151 |
-
static void sdq_buft_clear(ggml_backend_buffer_t buffer, uint8_t value) {
|
| 152 |
-
(void) buffer; (void) value;
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
struct ggml_backend_buffer_i sdq_buffer_iface() {
|
| 156 |
-
static ggml_backend_buffer_i i = [] {
|
| 157 |
-
ggml_backend_buffer_i x {};
|
| 158 |
-
x.free_buffer = sdq_buft_free_buffer;
|
| 159 |
-
x.get_base = sdq_buft_get_base;
|
| 160 |
-
x.init_tensor = sdq_buft_init_tensor;
|
| 161 |
-
x.memset_tensor = sdq_buft_memset_tensor;
|
| 162 |
-
x.set_tensor = sdq_buft_set_tensor;
|
| 163 |
-
x.get_tensor = sdq_buft_get_tensor;
|
| 164 |
-
x.cpy_tensor = sdq_buft_cpy_tensor;
|
| 165 |
-
x.clear = sdq_buft_clear;
|
| 166 |
-
return x;
|
| 167 |
-
}();
|
| 168 |
-
return i;
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
ggml_backend_buffer_type_t sdq_expert_buft() {
|
| 172 |
-
static ggml_backend_buffer_type_t buft = [] {
|
| 173 |
-
return new ggml_backend_buffer_type{
|
| 174 |
-
/* .iface = */ {
|
| 175 |
-
/* .get_name = */ sdq_buft_get_name,
|
| 176 |
-
/* .alloc_buffer = */ sdq_buft_alloc_buffer,
|
| 177 |
-
/* .alloc_buffer_n = */ sdq_buft_alloc_buffer_n,
|
| 178 |
-
/* .get_alignment = */ sdq_buft_get_alignment,
|
| 179 |
-
/* .get_max_size = */ sdq_buft_get_max_size,
|
| 180 |
-
/* .get_alloc_size = */ sdq_buft_get_alloc_size,
|
| 181 |
-
/* .get_alloc_size_n = */ nullptr,
|
| 182 |
-
/* .is_host = */ sdq_buft_is_host,
|
| 183 |
-
},
|
| 184 |
-
/* .device = */ nullptr,
|
| 185 |
-
/* .context = */ nullptr,
|
| 186 |
-
};
|
| 187 |
-
}();
|
| 188 |
-
return buft;
|
| 189 |
-
}
|
| 190 |
-
|
| 191 |
-
// llama.cpp 端需要的介面宣告
|
| 192 |
-
void sdq_install_buft_overrides(llama_model_params * params);
|
| 193 |
-
|
| 194 |
-
// SDQ_BUFT_KEEP 保留開關:std::regex 的負向先行斷言在 llama.cpp 的比對方式下
|
| 195 |
-
// 不可靠(會讓整個 override 失效、權重被載進 RAM),所以先不開放這個 knob。
|
| 196 |
-
static std::string sdq_expert_pattern() {
|
| 197 |
-
return "blk\\.[0-9]+\\.ffn_(gate|up|down|gate_up)_exps\\.weight";
|
| 198 |
-
}
|
| 199 |
-
|
| 200 |
-
// 這兩個 static 只能用「初始化一次」的方式寫:libstdc++ 的 std::string move-assign
|
| 201 |
-
// 會把舊的 buffer 交給右邊的 temporary,temporary 析構時就 free 掉。若改成每次呼叫
|
| 202 |
-
// pattern = sdq_expert_pattern(),先前存進 override 陣列的 .pattern 會變成懸空指標;
|
| 203 |
-
// 讀到空字串時 std::regex("") 會匹配「任何」張量名,於是整個模型(含 token_embd)
|
| 204 |
-
// 都被塞進假 buffer,server 直接在 sched_reserve 裡 ggml_abort。
|
| 205 |
-
static const llama_model_tensor_buft_override * sdq_overrides() {
|
| 206 |
-
static const std::string pattern = sdq_expert_pattern();
|
| 207 |
-
static const llama_model_tensor_buft_override ov[2] = {
|
| 208 |
-
{ pattern.c_str(), sdq_expert_buft() },
|
| 209 |
-
{ nullptr, nullptr },
|
| 210 |
-
};
|
| 211 |
-
return ov;
|
| 212 |
-
}
|
| 213 |
-
|
| 214 |
-
void sdq_install_buft_overrides(llama_model_params * params) {
|
| 215 |
-
if (!params) {
|
| 216 |
-
return;
|
| 217 |
-
}
|
| 218 |
-
if (params->tensor_buft_overrides == nullptr) {
|
| 219 |
-
params->tensor_buft_overrides = sdq_overrides();
|
| 220 |
-
} else {
|
| 221 |
-
// 使用者已有 override:把手動指定的順序原樣保留,附加在尾端
|
| 222 |
-
size_t n = 0;
|
| 223 |
-
while (params->tensor_buft_overrides[n].pattern != nullptr) {
|
| 224 |
-
n++;
|
| 225 |
-
}
|
| 226 |
-
static std::vector<llama_model_tensor_buft_override> merged;
|
| 227 |
-
merged.clear();
|
| 228 |
-
for (size_t i = 0; i < n; i++) {
|
| 229 |
-
merged.push_back(params->tensor_buft_overrides[i]);
|
| 230 |
-
}
|
| 231 |
-
const llama_model_tensor_buft_override * sdq_ov = sdq_overrides();
|
| 232 |
-
for (size_t i = 0; sdq_ov[i].pattern != nullptr; i++) {
|
| 233 |
-
merged.push_back(sdq_ov[i]);
|
| 234 |
-
}
|
| 235 |
-
merged.push_back({ nullptr, nullptr });
|
| 236 |
-
params->tensor_buft_overrides = merged.data();
|
| 237 |
-
}
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
} // namespace sdq
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sdq-ling/sdq-ling/sdq_cli.cpp
DELETED
|
@@ -1,844 +0,0 @@
|
|
| 1 |
-
// SDQ 聊天 / benchmark 主程式
|
| 2 |
-
//
|
| 3 |
-
// sdq-chat -m <model.gguf> [-c ctx] [-t threads] [-n n_predict] [-p prompt] ...
|
| 4 |
-
//
|
| 5 |
-
// 這個工具存在的理由:官方 llama-cli 不會安裝我們的 tensor buffer override,
|
| 6 |
-
// 而且需要我們在載入模型前後插入 pager 的初始化/收尾。用最少的相依度
|
| 7 |
-
// (只鏈 llama + ggml)自己寫一支,行為完全可控。
|
| 8 |
-
|
| 9 |
-
#include "llama.h"
|
| 10 |
-
#include "sdq_pager.h"
|
| 11 |
-
|
| 12 |
-
#include <algorithm>
|
| 13 |
-
#include <chrono>
|
| 14 |
-
#include <cinttypes>
|
| 15 |
-
#include <cmath>
|
| 16 |
-
#include <cstdio>
|
| 17 |
-
#include <cstdlib>
|
| 18 |
-
#include <cstring>
|
| 19 |
-
#include <functional>
|
| 20 |
-
#include <thread>
|
| 21 |
-
#include <iostream>
|
| 22 |
-
#include <string>
|
| 23 |
-
#include <vector>
|
| 24 |
-
|
| 25 |
-
using namespace sdq;
|
| 26 |
-
|
| 27 |
-
namespace {
|
| 28 |
-
|
| 29 |
-
struct options {
|
| 30 |
-
std::string model;
|
| 31 |
-
std::string prompt;
|
| 32 |
-
std::string system = "You are a helpful assistant. Answer in the user's language.";
|
| 33 |
-
std::string stats_file;
|
| 34 |
-
int32_t n_ctx = 4096; // 驗證範圍:4K(理論支援 1M)
|
| 35 |
-
int32_t n_predict = 256;
|
| 36 |
-
int32_t n_threads = 0;
|
| 37 |
-
int32_t n_ubatch = 0;
|
| 38 |
-
int32_t n_keep = 0;
|
| 39 |
-
float temp = 0.7f;
|
| 40 |
-
float top_p = 0.95f;
|
| 41 |
-
int32_t top_k = 20;
|
| 42 |
-
bool chat = true;
|
| 43 |
-
bool interactive = true;
|
| 44 |
-
bool bench = false;
|
| 45 |
-
bool no_prefetch = false;
|
| 46 |
-
bool dump_logits = false;
|
| 47 |
-
std::string logits_out;
|
| 48 |
-
std::string selftest;
|
| 49 |
-
std::string span_test;
|
| 50 |
-
std::string dump_tensors; // ggml eval callback:把指定張量的資料存檔(A/B 比對用)
|
| 51 |
-
bool no_pager = false; // A/B 測試用:不啟用分頁,走上游 mul_mat_id(整個模型在 RAM)
|
| 52 |
-
int32_t arena_mb = 0;
|
| 53 |
-
int32_t ram_budget_mb = 0;
|
| 54 |
-
bool fadvise = true;
|
| 55 |
-
};
|
| 56 |
-
|
| 57 |
-
double now_s() {
|
| 58 |
-
static auto t0 = std::chrono::steady_clock::now();
|
| 59 |
-
return std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
size_t rss_mb() { return process_rss_bytes() / (1024 * 1024); }
|
| 63 |
-
|
| 64 |
-
// 從 /proc/self/status 讀 VmHWM(峰值 RSS)/ VmSwap(這個行程真的用了多少 swap)
|
| 65 |
-
size_t proc_status_kb(const char * key) {
|
| 66 |
-
FILE * f = fopen("/proc/self/status", "r");
|
| 67 |
-
if (!f) return 0;
|
| 68 |
-
char line[256];
|
| 69 |
-
const size_t klen = strlen(key);
|
| 70 |
-
size_t val = 0;
|
| 71 |
-
while (fgets(line, sizeof(line), f)) {
|
| 72 |
-
if (strncmp(line, key, klen) == 0) {
|
| 73 |
-
val = (size_t) strtoull(line + klen, nullptr, 10);
|
| 74 |
-
break;
|
| 75 |
-
}
|
| 76 |
-
}
|
| 77 |
-
fclose(f);
|
| 78 |
-
return val;
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
size_t peak_rss_mb() { return proc_status_kb("VmHWM:") / 1024; }
|
| 82 |
-
size_t swap_mb() { return proc_status_kb("VmSwap:") / 1024; }
|
| 83 |
-
|
| 84 |
-
// /proc/self/smaps_rollup:把 RSS 拆成「檔案對映」與「匿名」兩塊。
|
| 85 |
-
// 這很重要:模型檔是 mmap 進來的,核心預讀會讓「檔案對映頁」也算進 RSS,
|
| 86 |
-
// 報告時必須分開看,否則會誤以為 RAM 用量比實際少(或多)。
|
| 87 |
-
size_t smaps_kb(const char * key) {
|
| 88 |
-
FILE * f = fopen("/proc/self/smaps_rollup", "r");
|
| 89 |
-
if (!f) return 0;
|
| 90 |
-
char line[256];
|
| 91 |
-
const size_t klen = strlen(key);
|
| 92 |
-
size_t val = 0;
|
| 93 |
-
while (fgets(line, sizeof(line), f)) {
|
| 94 |
-
if (strncmp(line, key, klen) == 0) {
|
| 95 |
-
val = (size_t) strtoull(line + klen, nullptr, 10);
|
| 96 |
-
break;
|
| 97 |
-
}
|
| 98 |
-
}
|
| 99 |
-
fclose(f);
|
| 100 |
-
return val;
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
// smaps_rollup 並不是每個核心都有 "RssFile:"(本機的核心就沒有,只有 "Anonymous:"),
|
| 104 |
-
// 讀不到就會印出「檔案對映 RSS 0 MB」這種假的數字。
|
| 105 |
-
// RssAnon / RssFile / VmRSS 在 /proc/self/status 一定有,而且正是同一組語意。
|
| 106 |
-
size_t status_kb(const char * key) {
|
| 107 |
-
FILE * f = fopen("/proc/self/status", "r");
|
| 108 |
-
if (!f) return 0;
|
| 109 |
-
char line[256];
|
| 110 |
-
const size_t klen = strlen(key);
|
| 111 |
-
size_t val = 0;
|
| 112 |
-
while (fgets(line, sizeof(line), f)) {
|
| 113 |
-
if (strncmp(line, key, klen) == 0) {
|
| 114 |
-
val = (size_t) strtoull(line + klen, nullptr, 10);
|
| 115 |
-
break;
|
| 116 |
-
}
|
| 117 |
-
}
|
| 118 |
-
fclose(f);
|
| 119 |
-
return val;
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
void print_mem_report(const char * when) {
|
| 123 |
-
fprintf(stderr,
|
| 124 |
-
"[sdq] 記憶體(%s):匿名 RSS %zu MB / 檔案對映 RSS %zu MB / 總 RSS %zu MB / 峰值 %zu MB / swap %zu MB\n",
|
| 125 |
-
when, status_kb("RssAnon:") / 1024, status_kb("RssFile:") / 1024,
|
| 126 |
-
status_kb("VmRSS:") / 1024, peak_rss_mb(), swap_mb());
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
// 讀 /proc/self/io:從 block device 實際讀了多少(SSD 讀取)
|
| 130 |
-
uint64_t read_bytes_io() {
|
| 131 |
-
FILE * f = fopen("/proc/self/io", "r");
|
| 132 |
-
if (!f) return 0;
|
| 133 |
-
char line[256];
|
| 134 |
-
uint64_t v = 0;
|
| 135 |
-
while (fgets(line, sizeof(line), f)) {
|
| 136 |
-
if (strncmp(line, "read_bytes:", 11) == 0) {
|
| 137 |
-
v = strtoull(line + 11, nullptr, 10);
|
| 138 |
-
}
|
| 139 |
-
}
|
| 140 |
-
fclose(f);
|
| 141 |
-
return v;
|
| 142 |
-
}
|
| 143 |
-
|
| 144 |
-
struct chat_state {
|
| 145 |
-
std::vector<llama_chat_message> msgs;
|
| 146 |
-
};
|
| 147 |
-
|
| 148 |
-
std::string build_prompt(const options & opt, const chat_state & st) {
|
| 149 |
-
if (!opt.chat) {
|
| 150 |
-
return opt.prompt;
|
| 151 |
-
}
|
| 152 |
-
const char * tmpl = llama_model_chat_template(nullptr, nullptr);
|
| 153 |
-
// 沒有 model 指標時用內建模板;��面 main() 會用實際模型的模板重做
|
| 154 |
-
(void) tmpl;
|
| 155 |
-
size_t n_chars = 0;
|
| 156 |
-
for (const auto & m : st.msgs) {
|
| 157 |
-
n_chars += strlen(m.role) + strlen(m.content) + 32;
|
| 158 |
-
}
|
| 159 |
-
std::vector<char> buf(2 * n_chars + 1024);
|
| 160 |
-
const char * t = st.msgs.empty() ? nullptr : nullptr;
|
| 161 |
-
(void) t;
|
| 162 |
-
int32_t len = llama_chat_apply_template(tmpl, st.msgs.data(), st.msgs.size(), true,
|
| 163 |
-
buf.data(), (int32_t) buf.size());
|
| 164 |
-
if (len > (int32_t) buf.size()) {
|
| 165 |
-
buf.resize(len + 1);
|
| 166 |
-
llama_chat_apply_template(tmpl, st.msgs.data(), st.msgs.size(), true,
|
| 167 |
-
buf.data(), (int32_t) buf.size());
|
| 168 |
-
}
|
| 169 |
-
return std::string(buf.data());
|
| 170 |
-
}
|
| 171 |
-
|
| 172 |
-
} // namespace
|
| 173 |
-
|
| 174 |
-
#include <execinfo.h>
|
| 175 |
-
#include <signal.h>
|
| 176 |
-
#include <unistd.h>
|
| 177 |
-
static void sdq_segv(int sig) {
|
| 178 |
-
void * bt[64];
|
| 179 |
-
const int n = backtrace(bt, 64);
|
| 180 |
-
fprintf(stderr, "\n*** sdq: signal %d, backtrace:\n", sig);
|
| 181 |
-
backtrace_symbols_fd(bt, n, 2);
|
| 182 |
-
_exit(9);
|
| 183 |
-
}
|
| 184 |
-
|
| 185 |
-
// eval callback:把感興趣的張量(用 SDQ_DUMP_NAMES 指定)寫進檔案
|
| 186 |
-
struct dump_state {
|
| 187 |
-
FILE * fp = nullptr;
|
| 188 |
-
uint64_t step = 0;
|
| 189 |
-
std::vector<std::string> names;
|
| 190 |
-
};
|
| 191 |
-
|
| 192 |
-
static dump_state g_dump;
|
| 193 |
-
|
| 194 |
-
static bool sdq_eval_callback(ggml_tensor * t, bool ask, void * user_data) {
|
| 195 |
-
(void) user_data;
|
| 196 |
-
auto & ds = g_dump;
|
| 197 |
-
if (ask) {
|
| 198 |
-
// 除錯模式:每個節點都要資料 → 會強制逐節點同步(慢,但能逐節點比對)
|
| 199 |
-
return ds.fp != nullptr;
|
| 200 |
-
}
|
| 201 |
-
if (!ds.fp) {
|
| 202 |
-
return false;
|
| 203 |
-
}
|
| 204 |
-
const char * nm = ggml_get_name(t);
|
| 205 |
-
const size_t n_len = nm ? strlen(nm) : 0;
|
| 206 |
-
bool want = ds.names.empty();
|
| 207 |
-
for (const auto & n : ds.names) {
|
| 208 |
-
if (nm && strstr(nm, n.c_str())) {
|
| 209 |
-
want = true;
|
| 210 |
-
break;
|
| 211 |
-
}
|
| 212 |
-
}
|
| 213 |
-
const int32_t ne[4] = {(int32_t) t->ne[0], (int32_t) t->ne[1], (int32_t) t->ne[2], (int32_t) t->ne[3]};
|
| 214 |
-
const int32_t type = (int32_t) t->type;
|
| 215 |
-
fwrite(&n_len, sizeof(size_t), 1, ds.fp);
|
| 216 |
-
if (n_len) {
|
| 217 |
-
fwrite(nm, 1, n_len, ds.fp);
|
| 218 |
-
}
|
| 219 |
-
fwrite(&ds.step, sizeof(uint64_t), 1, ds.fp);
|
| 220 |
-
fwrite(ne, sizeof(int32_t), 4, ds.fp);
|
| 221 |
-
fwrite(&type, sizeof(int32_t), 1, ds.fp);
|
| 222 |
-
|
| 223 |
-
// 永遠寫「摘要」(前 8 個值 + 平方和 + 最大值),這樣即使沒有名字也能 A/B 找出第一個分歧點
|
| 224 |
-
float summary[10] = {0};
|
| 225 |
-
if (t->type == GGML_TYPE_F32 && t->data) {
|
| 226 |
-
const size_t n = (size_t) ne[0] * ne[1] * ne[2] * ne[3];
|
| 227 |
-
const float * p = (const float *) t->data;
|
| 228 |
-
double ss = 0;
|
| 229 |
-
float mx = 0;
|
| 230 |
-
for (size_t i = 0; i < n; i++) {
|
| 231 |
-
ss += (double) p[i] * p[i];
|
| 232 |
-
if (std::fabs(p[i]) > std::fabs(mx)) {
|
| 233 |
-
mx = p[i];
|
| 234 |
-
}
|
| 235 |
-
}
|
| 236 |
-
for (int i = 0; i < 8 && (size_t) i < n; i++) {
|
| 237 |
-
summary[i] = p[i];
|
| 238 |
-
}
|
| 239 |
-
summary[8] = (float) std::sqrt(ss);
|
| 240 |
-
summary[9] = mx;
|
| 241 |
-
fwrite(summary, sizeof(float), 10, ds.fp);
|
| 242 |
-
if (want) {
|
| 243 |
-
fwrite(p, sizeof(float), n, ds.fp);
|
| 244 |
-
}
|
| 245 |
-
} else {
|
| 246 |
-
fwrite(summary, sizeof(float), 10, ds.fp);
|
| 247 |
-
}
|
| 248 |
-
ds.step++;
|
| 249 |
-
return true; // 這個節點的資料我們已經讀過了,標記成已完成
|
| 250 |
-
}
|
| 251 |
-
|
| 252 |
-
int main(int argc, char ** argv) {
|
| 253 |
-
signal(SIGSEGV, sdq_segv);
|
| 254 |
-
signal(SIGBUS, sdq_segv);
|
| 255 |
-
signal(SIGABRT, sdq_segv);
|
| 256 |
-
options opt;
|
| 257 |
-
const char * env_model = getenv("SDQ_MODEL_PATH");
|
| 258 |
-
if (env_model) {
|
| 259 |
-
opt.model = env_model;
|
| 260 |
-
}
|
| 261 |
-
|
| 262 |
-
for (int i = 1; i < argc; i++) {
|
| 263 |
-
std::string a = argv[i];
|
| 264 |
-
auto next = [&](const char * what) -> std::string {
|
| 265 |
-
if (i + 1 >= argc) {
|
| 266 |
-
fprintf(stderr, "%s 需要參數\n", what);
|
| 267 |
-
exit(1);
|
| 268 |
-
}
|
| 269 |
-
return argv[++i];
|
| 270 |
-
};
|
| 271 |
-
if (a == "-m" || a == "--model") opt.model = next("-m");
|
| 272 |
-
else if (a == "-p" || a == "--prompt") opt.prompt = next("-p");
|
| 273 |
-
else if (a == "--sys") opt.system = next("--sys");
|
| 274 |
-
else if (a == "-c" || a == "--ctx") opt.n_ctx = atoi(next("-c").c_str());
|
| 275 |
-
else if (a == "-n") opt.n_predict = atoi(next("-n").c_str());
|
| 276 |
-
else if (a == "-t") opt.n_threads = atoi(next("-t").c_str());
|
| 277 |
-
else if (a == "-b" || a == "--ubatch") opt.n_ubatch = atoi(next("-b").c_str());
|
| 278 |
-
else if (a == "--temp") opt.temp = atof(next("--temp").c_str());
|
| 279 |
-
else if (a == "--top-p") opt.top_p = atof(next("--top-p").c_str());
|
| 280 |
-
else if (a == "--top-k") opt.top_k = atoi(next("--top-k").c_str());
|
| 281 |
-
else if (a == "--no-chat") opt.chat = false;
|
| 282 |
-
else if (a == "--no-interactive") opt.interactive = false;
|
| 283 |
-
else if (a == "--bench") opt.bench = true;
|
| 284 |
-
else if (a == "--no-prefetch") opt.no_prefetch = true;
|
| 285 |
-
else if (a == "--arena-mb") opt.arena_mb = atoi(next("--arena-mb").c_str());
|
| 286 |
-
else if (a == "--ram-budget-mb") opt.ram_budget_mb = atoi(next("--ram-budget-mb").c_str());
|
| 287 |
-
else if (a == "--no-fadvise") opt.fadvise = false;
|
| 288 |
-
else if (a == "--stats-file") opt.stats_file = next("--stats-file");
|
| 289 |
-
else if (a == "--dump-logits") opt.dump_logits = true;
|
| 290 |
-
else if (a == "--logits-out") opt.logits_out = next("--logits-out");
|
| 291 |
-
else if (a == "--selftest") opt.selftest = next("--selftest");
|
| 292 |
-
else if (a == "--dump-tensors") opt.dump_tensors = next("--dump-tensors");
|
| 293 |
-
else if (a == "--no-pager") opt.no_pager = true;
|
| 294 |
-
else if (a == "--span-test") opt.span_test = next("--span-test");
|
| 295 |
-
else if (a == "-h" || a == "--help") {
|
| 296 |
-
printf(
|
| 297 |
-
"用法:sdq-chat -m <model.gguf> [選項]\n"
|
| 298 |
-
" -c, --ctx N 上下文長度(預設 4096;模型原生 262144)\n"
|
| 299 |
-
" -n N 要生成的 token 數\n"
|
| 300 |
-
" -t N CPU 執行緒數\n"
|
| 301 |
-
" -b N ubatch 大小\n"
|
| 302 |
-
" -p, --prompt TEXT 提示詞\n"
|
| 303 |
-
" --sys TEXT system 訊息\n"
|
| 304 |
-
" --no-chat 不要套 chat template(純 completion)\n"
|
| 305 |
-
" --no-interactive 跑完提示詞就結束\n"
|
| 306 |
-
" --bench benchmark 模式(輸出 JSON 到 stats 檔)\n"
|
| 307 |
-
" --arena-mb N expert arena 大小(MB)\n"
|
| 308 |
-
" --ram-budget-mb N RAM 總預算(MB),arena 會配合縮減\n"
|
| 309 |
-
" --no-prefetch 關閉背景預取\n"
|
| 310 |
-
" --no-fadvise 讀完不丟 kernel page cache(會讓 RAM 帳目失真)\n"
|
| 311 |
-
" --stats-file PATH 指標 JSONL 輸出\n"
|
| 312 |
-
" --dump-logits 把每個 token 的 logits 存檔(驗證用)\n"
|
| 313 |
-
" --logits-out PATH logits 輸出路徑\n");
|
| 314 |
-
return 0;
|
| 315 |
-
} else {
|
| 316 |
-
fprintf(stderr, "未知參數:%s(用 --help 看用法)\n", a.c_str());
|
| 317 |
-
return 1;
|
| 318 |
-
}
|
| 319 |
-
}
|
| 320 |
-
|
| 321 |
-
if (opt.model.empty()) {
|
| 322 |
-
fprintf(stderr, "缺少模型檔(-m 或 SDQ_MODEL_PATH)\n");
|
| 323 |
-
return 1;
|
| 324 |
-
}
|
| 325 |
-
|
| 326 |
-
// ---- 環境變數(pager 讀)
|
| 327 |
-
setenv("SDQ_MODEL_PATH", opt.model.c_str(), 1);
|
| 328 |
-
// I/O 讀取執行緒數:預設 3 × 計算執行緒(上限 48)。
|
| 329 |
-
// 必須在這裡設定 —— pager 的 I/O 池是在「載入模型」時啟動的,那已經比
|
| 330 |
-
// context 建立早了;放到後面設定會讓池子只拿到下限 4 條執行緒。
|
| 331 |
-
// 量測(repeat=3,中位數):序列讀 3.79 → 12 條 5.84 → 24 條 6.68 tok/s。
|
| 332 |
-
if (!getenv("SDQ_IO_THREADS")) {
|
| 333 |
-
const int hw_io = (int) std::max(1u, std::thread::hardware_concurrency());
|
| 334 |
-
const int dflt_io = hw_io < 8 ? hw_io : 8;
|
| 335 |
-
const int n_io = opt.n_threads > 0 ? opt.n_threads : dflt_io;
|
| 336 |
-
setenv("SDQ_IO_THREADS", std::to_string(n_io * 3).c_str(), 1);
|
| 337 |
-
}
|
| 338 |
-
// 只有明確指定時才設定,否則保留呼叫者從環境變數傳入的值
|
| 339 |
-
// (舊版無條件 setenv 會讓 SDQ_PREFETCH=0 被悄悄蓋回 1,除錯時極易誤判)
|
| 340 |
-
if (opt.no_prefetch) {
|
| 341 |
-
setenv("SDQ_PREFETCH", "0", 1);
|
| 342 |
-
}
|
| 343 |
-
setenv("SDQ_FADV_DONTNEED", opt.fadvise ? "1" : "0", 1);
|
| 344 |
-
if (opt.arena_mb > 0) {
|
| 345 |
-
setenv("SDQ_ARENA_MB", std::to_string(opt.arena_mb).c_str(), 1);
|
| 346 |
-
}
|
| 347 |
-
if (opt.ram_budget_mb > 0) {
|
| 348 |
-
setenv("SDQ_RAM_BUDGET_MB", std::to_string(opt.ram_budget_mb).c_str(), 1);
|
| 349 |
-
}
|
| 350 |
-
if (!opt.stats_file.empty()) {
|
| 351 |
-
setenv("SDQ_STATS_FILE", opt.stats_file.c_str(), 1);
|
| 352 |
-
}
|
| 353 |
-
|
| 354 |
-
llama_backend_init();
|
| 355 |
-
llama_numa_init(GGML_NUMA_STRATEGY_DISABLED);
|
| 356 |
-
|
| 357 |
-
// ---- 模型載入:expert 權重交給 pager 的 buffer type(0 RAM)
|
| 358 |
-
llama_model_params mparams = llama_model_default_params();
|
| 359 |
-
mparams.n_gpu_layers = 0;
|
| 360 |
-
mparams.load_mode = LLAMA_LOAD_MODE_MMAP;
|
| 361 |
-
if (!opt.no_pager) {
|
| 362 |
-
sdq_set_enabled(true);
|
| 363 |
-
sdq_install_buft_overrides(&mparams);
|
| 364 |
-
} else {
|
| 365 |
-
printf("[sdq] --no-pager:對照模式,專家權重全部留在 RAM(需要 %s RAM)\n", "22GB");
|
| 366 |
-
}
|
| 367 |
-
|
| 368 |
-
printf("[sdq] 載入模型:%s\n", opt.model.c_str());
|
| 369 |
-
const double t_load0 = now_s();
|
| 370 |
-
llama_model * model = llama_model_load_from_file(opt.model.c_str(), mparams);
|
| 371 |
-
if (!model) {
|
| 372 |
-
fprintf(stderr, "[sdq] 模型載入失敗\n");
|
| 373 |
-
return 1;
|
| 374 |
-
}
|
| 375 |
-
const double t_load = now_s() - t_load0;
|
| 376 |
-
|
| 377 |
-
// ---- pager 初始化(讀 GGUF 檔頭、建頁面表、配置 arena)
|
| 378 |
-
auto & P = pager::instance();
|
| 379 |
-
if (!opt.no_pager && !sdq_init(opt.model)) {
|
| 380 |
-
fprintf(stderr, "[sdq] pager 初始化失敗\n");
|
| 381 |
-
return 1;
|
| 382 |
-
}
|
| 383 |
-
|
| 384 |
-
// ---- 張量 dump(A/B 比對用):在 op 真的算完的那一刻把資��寫出來
|
| 385 |
-
FILE * tensor_fp = nullptr;
|
| 386 |
-
dump_state dump_state_;
|
| 387 |
-
if (!opt.dump_tensors.empty()) {
|
| 388 |
-
tensor_fp = fopen(opt.dump_tensors.c_str(), "wb");
|
| 389 |
-
dump_state_.fp = tensor_fp;
|
| 390 |
-
if (const char * names = getenv("SDQ_DUMP_NAMES")) {
|
| 391 |
-
std::string s = names;
|
| 392 |
-
size_t pos = 0;
|
| 393 |
-
while (pos <= s.size()) {
|
| 394 |
-
size_t c = s.find(',', pos);
|
| 395 |
-
if (c == std::string::npos) c = s.size();
|
| 396 |
-
dump_state_.names.push_back(s.substr(pos, c - pos));
|
| 397 |
-
pos = c + 1;
|
| 398 |
-
}
|
| 399 |
-
}
|
| 400 |
-
g_dump = dump_state_;
|
| 401 |
-
}
|
| 402 |
-
|
| 403 |
-
// ---- context
|
| 404 |
-
llama_context_params cparams = llama_context_default_params();
|
| 405 |
-
cparams.n_ctx = (uint32_t) opt.n_ctx;
|
| 406 |
-
cparams.n_batch = (uint32_t) opt.n_ctx;
|
| 407 |
-
// ubatch 預設 128(不是 512)。理由是「8 GB 預算要真的裝得下」:
|
| 408 |
-
// llama.cpp 的 CPU compute buffer 是按 n_ubatch 線性成長的,實測
|
| 409 |
-
// ubatch=512 → 502 MiB、256 → 251 MiB、128 → 125.5 MiB
|
| 410 |
-
// 而 compute buffer 是在 pager 算完 arena 大小之後才配置的,所以它不算在
|
| 411 |
-
// arena 的扣減裡。實測在 RLIMIT_DATA 8 GB 下:
|
| 412 |
-
// ubatch=512 → 提示詞超過 ~256 token 直接 std::bad_alloc(崩潰)
|
| 413 |
-
// ubatch=256 → 同一個提示詞一樣崩潰
|
| 414 |
-
// ubatch=128 → 正常,681 token 的 prefill 跑到 28.8 tok/s
|
| 415 |
-
// 而且 ubatch=128 還更快(docs/04 實測 +7%),因為 prefill 分更多批、
|
| 416 |
-
// 每批的 expert 聯集比較小、命中率比較高。
|
| 417 |
-
cparams.n_ubatch = opt.n_ubatch > 0 ? (uint32_t) opt.n_ubatch : (uint32_t) std::min(opt.n_ctx, 128);
|
| 418 |
-
// 執行緒預設值:不要直接用 hardware_concurrency()。
|
| 419 |
-
// 實測(docs/04,repeat=3):mode C 下 t=8 最快,給更多執行緒反而變慢 ——
|
| 420 |
-
// t=8 4.02 / t=10 3.79 / t=12 3.55 / t=16 3.60 tok/s
|
| 421 |
-
// 原因很直接:每層每 token 只有 n_expert_used = 8 個 expert 可以平行,
|
| 422 |
-
// 執行緒超過 8 之後就只是多開執行緒搶同一批 CPU。
|
| 423 |
-
// 下限取實際核心數,避免在 4 核心的目標機器上開太多。
|
| 424 |
-
const int hw = (int) std::max(1u, std::thread::hardware_concurrency());
|
| 425 |
-
const int dflt_threads = hw < 8 ? hw : 8;
|
| 426 |
-
cparams.n_threads = opt.n_threads > 0 ? opt.n_threads : (int32_t) dflt_threads;
|
| 427 |
-
cparams.n_threads_batch = cparams.n_threads;
|
| 428 |
-
if (!opt.stats_file.empty()) {
|
| 429 |
-
cparams.offload_kqv = false;
|
| 430 |
-
}
|
| 431 |
-
|
| 432 |
-
if (tensor_fp) {
|
| 433 |
-
cparams.cb_eval = sdq_eval_callback;
|
| 434 |
-
cparams.cb_eval_user_data = &g_dump;
|
| 435 |
-
}
|
| 436 |
-
|
| 437 |
-
llama_context * ctx = llama_init_from_model(model, cparams);
|
| 438 |
-
if (!ctx) {
|
| 439 |
-
fprintf(stderr, "[sdq] context 建立失敗(ctx=%d 太大?)\n", opt.n_ctx);
|
| 440 |
-
return 1;
|
| 441 |
-
}
|
| 442 |
-
|
| 443 |
-
printf("[sdq] ctx=%d threads=%d ubatch=%u RAM(RSS)=%zu MB 模型載入 %.1fs\n",
|
| 444 |
-
opt.n_ctx, cparams.n_threads, cparams.n_ubatch, rss_mb(), t_load);
|
| 445 |
-
print_mem_report("載入 + pager 後");
|
| 446 |
-
|
| 447 |
-
// ---- sampler
|
| 448 |
-
auto sparams = llama_sampler_chain_default_params();
|
| 449 |
-
sparams.no_perf = true;
|
| 450 |
-
llama_sampler * smpl = llama_sampler_chain_init(sparams);
|
| 451 |
-
llama_sampler_chain_add(smpl, llama_sampler_init_top_k(opt.top_k));
|
| 452 |
-
llama_sampler_chain_add(smpl, llama_sampler_init_top_p(opt.top_p, 1));
|
| 453 |
-
llama_sampler_chain_add(smpl, llama_sampler_init_temp(opt.temp));
|
| 454 |
-
llama_sampler_chain_add(smpl, llama_sampler_init_dist(LLAMA_DEFAULT_SEED));
|
| 455 |
-
|
| 456 |
-
chat_state st;
|
| 457 |
-
if (!opt.system.empty()) {
|
| 458 |
-
st.msgs.push_back({"system", opt.system.c_str()});
|
| 459 |
-
}
|
| 460 |
-
|
| 461 |
-
std::vector<llama_token> decoded;
|
| 462 |
-
|
| 463 |
-
const llama_vocab * vocab = llama_model_get_vocab(model);
|
| 464 |
-
|
| 465 |
-
auto tokenize = [&](const std::string & text) {
|
| 466 |
-
std::vector<llama_token> toks(text.size() + 16);
|
| 467 |
-
int32_t n = llama_tokenize(vocab, text.c_str(), (int32_t) text.size(),
|
| 468 |
-
toks.data(), (int32_t) toks.size(), true, true);
|
| 469 |
-
if (n < 0) {
|
| 470 |
-
toks.resize((size_t) -n);
|
| 471 |
-
n = llama_tokenize(vocab, text.c_str(), (int32_t) text.size(),
|
| 472 |
-
toks.data(), (int32_t) toks.size(), true, true);
|
| 473 |
-
}
|
| 474 |
-
toks.resize(n);
|
| 475 |
-
return toks;
|
| 476 |
-
};
|
| 477 |
-
|
| 478 |
-
// ---- 送進去一個 ubatch,回傳生成的 token(-1 = 需要繼續生成)
|
| 479 |
-
std::function<int(llama_token, bool)> submit;
|
| 480 |
-
uint64_t n_prefill_tok = 0;
|
| 481 |
-
double t_prefill = 0;
|
| 482 |
-
uint64_t n_decode_tok = 0;
|
| 483 |
-
double t_decode = 0;
|
| 484 |
-
uint64_t io_read0 = read_bytes_io();
|
| 485 |
-
|
| 486 |
-
std::vector<float> logits_store;
|
| 487 |
-
FILE * logits_fp = nullptr;
|
| 488 |
-
if (opt.dump_logits) {
|
| 489 |
-
logits_fp = fopen(opt.logits_out.empty() ? "sdq_logits.bin" : opt.logits_out.c_str(), "wb");
|
| 490 |
-
}
|
| 491 |
-
|
| 492 |
-
submit = [&](llama_token tok, bool last) -> int {
|
| 493 |
-
const uint32_t ctx_size = llama_n_ctx(ctx);
|
| 494 |
-
llama_memory_t mem = llama_get_memory(ctx);
|
| 495 |
-
if (tok == 1) { // BOS:重開記憶體
|
| 496 |
-
llama_memory_clear(mem, true);
|
| 497 |
-
decoded.clear();
|
| 498 |
-
}
|
| 499 |
-
if (decoded.size() >= opt.n_ctx - 4) {
|
| 500 |
-
// 環形丟掉最舊的 token(保留 opt.n_keep)
|
| 501 |
-
const size_t drop = decoded.size() - (size_t) opt.n_keep;
|
| 502 |
-
llama_memory_seq_rm(mem, 0, 0, (int32_t) drop);
|
| 503 |
-
decoded.erase(decoded.begin(), decoded.begin() + (long) drop);
|
| 504 |
-
}
|
| 505 |
-
|
| 506 |
-
llama_batch b = llama_batch_get_one((llama_token *) &tok, 1);
|
| 507 |
-
|
| 508 |
-
const bool is_prefill = decoded.empty();
|
| 509 |
-
const double t0 = now_s();
|
| 510 |
-
const int rc = llama_decode(ctx, b);
|
| 511 |
-
const double dt = now_s() - t0;
|
| 512 |
-
if (rc != 0) {
|
| 513 |
-
fprintf(stderr, "\n[sdq] llama_decode 失敗 rc=%d(記憶體不足?)\n", rc);
|
| 514 |
-
exit(3);
|
| 515 |
-
}
|
| 516 |
-
decoded.push_back(tok);
|
| 517 |
-
|
| 518 |
-
// 每個 ubatch 結束 → 更新路由歷史並觸發下一輪預取
|
| 519 |
-
P.end_token();
|
| 520 |
-
|
| 521 |
-
if (is_prefill) {
|
| 522 |
-
n_prefill_tok++;
|
| 523 |
-
t_prefill += dt;
|
| 524 |
-
} else {
|
| 525 |
-
n_decode_tok++;
|
| 526 |
-
t_decode += dt;
|
| 527 |
-
P.get_stats().tokens.fetch_add(1, std::memory_order_relaxed);
|
| 528 |
-
}
|
| 529 |
-
|
| 530 |
-
if (logits_fp) {
|
| 531 |
-
const int32_t n_logits = llama_vocab_n_tokens(vocab);
|
| 532 |
-
const float * lg = llama_get_logits_ith(ctx, -1);
|
| 533 |
-
if (lg) {
|
| 534 |
-
logits_store.assign(lg, lg + n_logits);
|
| 535 |
-
fwrite(&n_logits, sizeof(int32_t), 1, logits_fp);
|
| 536 |
-
fwrite(logits_store.data(), sizeof(float), n_logits, logits_fp);
|
| 537 |
-
fflush(logits_fp);
|
| 538 |
-
}
|
| 539 |
-
}
|
| 540 |
-
|
| 541 |
-
if (last) {
|
| 542 |
-
return -1;
|
| 543 |
-
}
|
| 544 |
-
const llama_token id = llama_sampler_sample(smpl, ctx, -1);
|
| 545 |
-
// EOG 就停:這個模型生成完答案會吐 <|role_end|> / <|endoftext|>,
|
| 546 |
-
// 不停下來的話會一直重複同一組 special token。
|
| 547 |
-
// SDQ_BENCH_NO_EOG=1:benchmark 時固定生成 n 個 token(不同提示詞的生成長度
|
| 548 |
-
// 不同會讓記憶體實驗的 tok/s 失去可比性)。
|
| 549 |
-
if (llama_vocab_is_eog(vocab, id) && !getenv("SDQ_BENCH_NO_EOG")) {
|
| 550 |
-
fprintf(stderr, "\n[sdq] 生成 EOG token %d,停止\n", (int) id);
|
| 551 |
-
return -1;
|
| 552 |
-
}
|
| 553 |
-
return (int) id;
|
| 554 |
-
};
|
| 555 |
-
|
| 556 |
-
// ---- 產生 prompt
|
| 557 |
-
auto run_prompt = [&](const std::string & user_text, bool add_to_history) {
|
| 558 |
-
if (opt.chat) {
|
| 559 |
-
st.msgs.push_back({"user", user_text.c_str()});
|
| 560 |
-
}
|
| 561 |
-
std::string text;
|
| 562 |
-
if (opt.chat) {
|
| 563 |
-
const char * tmpl = llama_model_chat_template(model, nullptr);
|
| 564 |
-
size_t n_chars = 0;
|
| 565 |
-
for (const auto & m : st.msgs) {
|
| 566 |
-
n_chars += strlen(m.role) + strlen(m.content) + 32;
|
| 567 |
-
}
|
| 568 |
-
std::vector<char> buf(2 * n_chars + 1024);
|
| 569 |
-
int32_t len = llama_chat_apply_template(tmpl, st.msgs.data(), st.msgs.size(),
|
| 570 |
-
true, buf.data(), (int32_t) buf.size());
|
| 571 |
-
if (len > (int32_t) buf.size()) {
|
| 572 |
-
buf.resize((size_t) len + 1);
|
| 573 |
-
llama_chat_apply_template(tmpl, st.msgs.data(), st.msgs.size(), true,
|
| 574 |
-
buf.data(), (int32_t) buf.size());
|
| 575 |
-
}
|
| 576 |
-
text.assign(buf.data());
|
| 577 |
-
} else {
|
| 578 |
-
text = user_text;
|
| 579 |
-
}
|
| 580 |
-
|
| 581 |
-
if (getenv("SDQ_PRINT_PROMPT")) {
|
| 582 |
-
fprintf(stderr, "[sdq] prompt (%zu bytes): <<%s>>\n", text.size(), text.c_str());
|
| 583 |
-
}
|
| 584 |
-
auto toks = tokenize(text);
|
| 585 |
-
const int n_prompt = (int) toks.size();
|
| 586 |
-
if (getenv("SDQ_PRINT_PROMPT")) {
|
| 587 |
-
fprintf(stderr, "[sdq] prompt tokens (%d):", n_prompt);
|
| 588 |
-
for (int i = 0; i < n_prompt; i++) {
|
| 589 |
-
fprintf(stderr, " %d", (int) toks[i]);
|
| 590 |
-
}
|
| 591 |
-
fprintf(stderr, "\n");
|
| 592 |
-
}
|
| 593 |
-
const uint64_t io0 = read_bytes_io();
|
| 594 |
-
|
| 595 |
-
// 分批送進去(每批 = 一個 ubatch,控制記憶體)
|
| 596 |
-
const int32_t ub = (int32_t) cparams.n_ubatch;
|
| 597 |
-
llama_sampler_reset(smpl);
|
| 598 |
-
int32_t pos = 0;
|
| 599 |
-
for (int32_t i = 0; i < n_prompt; i += ub) {
|
| 600 |
-
const int32_t n = std::min(ub, n_prompt - i);
|
| 601 |
-
llama_batch b = llama_batch_init((uint32_t) n, 0, 1);
|
| 602 |
-
for (int32_t j = 0; j < n; j++) {
|
| 603 |
-
b.token[j] = toks[i + j];
|
| 604 |
-
b.pos[j] = pos++;
|
| 605 |
-
b.n_seq_id[j] = 1;
|
| 606 |
-
b.seq_id[j][0] = 0;
|
| 607 |
-
}
|
| 608 |
-
b.n_tokens = (uint32_t) n;
|
| 609 |
-
const double t0 = now_s();
|
| 610 |
-
const int rc = llama_decode(ctx, b);
|
| 611 |
-
t_prefill += now_s() - t0;
|
| 612 |
-
n_prefill_tok += (uint64_t) n;
|
| 613 |
-
if (rc != 0) {
|
| 614 |
-
fprintf(stderr, "\n[sdq] prefill 失敗 rc=%d(%d tokens)\n", rc, n);
|
| 615 |
-
exit(3);
|
| 616 |
-
}
|
| 617 |
-
P.end_token();
|
| 618 |
-
llama_batch_free(b);
|
| 619 |
-
}
|
| 620 |
-
const double t_p = now_s();
|
| 621 |
-
(void) t_p;
|
| 622 |
-
(void) io0;
|
| 623 |
-
|
| 624 |
-
if (add_to_history && opt.chat) {
|
| 625 |
-
st.msgs.push_back({"assistant", ""});
|
| 626 |
-
}
|
| 627 |
-
return n_prompt;
|
| 628 |
-
};
|
| 629 |
-
|
| 630 |
-
// ---- 產生
|
| 631 |
-
auto generate = [&](int32_t n_max) {
|
| 632 |
-
const uint64_t io0 = read_bytes_io();
|
| 633 |
-
const uint64_t io_read_before = io0;
|
| 634 |
-
(void) io_read_before;
|
| 635 |
-
const double t0 = now_s();
|
| 636 |
-
int32_t n_gen = 0;
|
| 637 |
-
llama_token id = llama_sampler_sample(smpl, ctx, -1);
|
| 638 |
-
fputs("assistant: ", stdout);
|
| 639 |
-
fflush(stdout);
|
| 640 |
-
while (n_gen < n_max) {
|
| 641 |
-
const int rc = submit(id, /*last=*/n_gen + 1 >= n_max);
|
| 642 |
-
if (rc < 0) {
|
| 643 |
-
break;
|
| 644 |
-
}
|
| 645 |
-
id = (llama_token) rc;
|
| 646 |
-
char piece_buf[256];
|
| 647 |
-
const int n_piece = llama_token_to_piece(vocab, id, piece_buf, sizeof(piece_buf), 0, true);
|
| 648 |
-
const std::string piece(piece_buf, (size_t) std::max(0, n_piece));
|
| 649 |
-
fputs(piece.c_str(), stdout);
|
| 650 |
-
fflush(stdout);
|
| 651 |
-
n_gen++;
|
| 652 |
-
}
|
| 653 |
-
fputs("\n", stdout);
|
| 654 |
-
const double dt = now_s() - t0;
|
| 655 |
-
const uint64_t io1 = read_bytes_io();
|
| 656 |
-
fprintf(stderr, "[sdq] decode: %d tok / %.2fs = %.2f tok/s | SSD 讀 %.1f MB (%.1f MB/tok) | RSS %zu MB\n",
|
| 657 |
-
n_gen, dt, dt > 0 ? n_gen / dt : 0.0,
|
| 658 |
-
(io1 - io0) / 1048576.0, n_gen ? (io1 - io0) / 1048576.0 / n_gen : 0.0, rss_mb());
|
| 659 |
-
if (opt.chat) {
|
| 660 |
-
for (auto & m : st.msgs) {
|
| 661 |
-
if (strcmp(m.role, "assistant") == 0) {
|
| 662 |
-
m.content = "";
|
| 663 |
-
}
|
| 664 |
-
}
|
| 665 |
-
}
|
| 666 |
-
P.dump_stats("decode");
|
| 667 |
-
return n_gen;
|
| 668 |
-
};
|
| 669 |
-
|
| 670 |
-
// ---- 合併讀取自我測試:--span-test IL:E0:E1:...
|
| 671 |
-
if (!opt.span_test.empty()) {
|
| 672 |
-
std::vector<int> parts;
|
| 673 |
-
std::string cur_s;
|
| 674 |
-
for (char ch : opt.span_test + ":") {
|
| 675 |
-
if (ch == ':') { parts.push_back(atoi(cur_s.c_str())); cur_s.clear(); }
|
| 676 |
-
else cur_s += ch;
|
| 677 |
-
}
|
| 678 |
-
if (parts.size() < 3) {
|
| 679 |
-
fprintf(stderr, "用法:--span-test IL:E0:E1[:E2...](E 要連續)\n");
|
| 680 |
-
return 1;
|
| 681 |
-
}
|
| 682 |
-
const int il = parts[0];
|
| 683 |
-
std::vector<int32_t> ex;
|
| 684 |
-
for (size_t i = 1; i < parts.size(); i++) {
|
| 685 |
-
ex.push_back(parts[i]);
|
| 686 |
-
}
|
| 687 |
-
std::vector<int32_t> slots(ex.size(), -1);
|
| 688 |
-
for (size_t i = 0; i < ex.size(); i++) {
|
| 689 |
-
slots[i] = P.reserve_slot(il, ex[i]);
|
| 690 |
-
}
|
| 691 |
-
const uint64_t got = P.read_expert_span(il, ex.data(), (int) ex.size(), slots.data());
|
| 692 |
-
printf("[sdq-span-test] L%d experts=%zu 讀取 %.2f MB\n", il, ex.size(), got / 1048576.0);
|
| 693 |
-
int bad = 0;
|
| 694 |
-
for (size_t i = 0; i < ex.size(); i++) {
|
| 695 |
-
std::vector<uint8_t> one((size_t) P.layer(il)->experts[ex[i]].bytes, 0);
|
| 696 |
-
if (!P.read_expert(il, ex[i], one.data())) {
|
| 697 |
-
printf(" 單獨讀取失敗\n");
|
| 698 |
-
return 1;
|
| 699 |
-
}
|
| 700 |
-
const uint8_t * span_ptr = P.slot_ptr(slots[i]);
|
| 701 |
-
size_t diff_at = (size_t) -1;
|
| 702 |
-
for (size_t b = 0; b < one.size(); b++) {
|
| 703 |
-
if (one[b] != span_ptr[b]) { diff_at = b; break; }
|
| 704 |
-
}
|
| 705 |
-
printf(" expert %4d: %s", ex[i], diff_at == (size_t) -1 ? "一致 ✅" : "不一致 ❌");
|
| 706 |
-
if (diff_at != (size_t) -1) {
|
| 707 |
-
printf("(第一個不同的 byte 在 %zu / %zu,slot=%d)", diff_at, one.size(), slots[i]);
|
| 708 |
-
bad++;
|
| 709 |
-
}
|
| 710 |
-
printf("\n");
|
| 711 |
-
}
|
| 712 |
-
sdq_shutdown();
|
| 713 |
-
llama_free(ctx);
|
| 714 |
-
llama_model_free(model);
|
| 715 |
-
llama_backend_free();
|
| 716 |
-
return bad ? 1 : 0;
|
| 717 |
-
}
|
| 718 |
-
|
| 719 |
-
// ---- 自我測試模式:--selftest IL:IE:SEED:OUT.bin
|
| 720 |
-
if (!opt.selftest.empty()) {
|
| 721 |
-
int il = 0, ie = 0, seed = 1234, n_tok = 1;
|
| 722 |
-
std::string outpath;
|
| 723 |
-
{
|
| 724 |
-
std::vector<std::string> parts;
|
| 725 |
-
std::string cur_s;
|
| 726 |
-
for (char c : opt.selftest + ":") {
|
| 727 |
-
if (c == ':') { parts.push_back(cur_s); cur_s.clear(); }
|
| 728 |
-
else cur_s += c;
|
| 729 |
-
}
|
| 730 |
-
if (parts.size() >= 3) {
|
| 731 |
-
il = atoi(parts[0].c_str());
|
| 732 |
-
ie = atoi(parts[1].c_str());
|
| 733 |
-
seed = atoi(parts[2].c_str());
|
| 734 |
-
}
|
| 735 |
-
if (parts.size() >= 4) {
|
| 736 |
-
outpath = parts[3];
|
| 737 |
-
}
|
| 738 |
-
if (parts.size() >= 5) {
|
| 739 |
-
n_tok = atoi(parts[4].c_str());
|
| 740 |
-
}
|
| 741 |
-
}
|
| 742 |
-
const int64_t n_embd = P.info().n_embd;
|
| 743 |
-
const int64_t n_ff = P.info().n_ff_exp;
|
| 744 |
-
std::vector<float> x((size_t) n_embd * n_tok);
|
| 745 |
-
uint32_t rng = (uint32_t) seed * 2654435761u + 1u;
|
| 746 |
-
for (auto & v : x) {
|
| 747 |
-
rng = rng * 1664525u + 1013904223u;
|
| 748 |
-
v = ((float) (rng >> 8) / (float) (1 << 24)) * 2.0f - 1.0f;
|
| 749 |
-
}
|
| 750 |
-
std::vector<float> io((size_t) n_embd * n_tok * 2);
|
| 751 |
-
const int rc = sdq_moe_selftest(il, ie, x.data(), io.data(), n_embd, n_ff, n_tok);
|
| 752 |
-
if (rc == 0 && !outpath.empty()) {
|
| 753 |
-
FILE * f = fopen(outpath.c_str(), "wb");
|
| 754 |
-
fwrite(io.data(), sizeof(float), io.size(), f);
|
| 755 |
-
fclose(f);
|
| 756 |
-
printf("[sdq] selftest 結果已寫到 %s(layer=%d expert=%d seed=%d n_embd=%lld n_ff=%lld)\n",
|
| 757 |
-
outpath.c_str(), il, ie, seed, (long long) n_embd, (long long) n_ff);
|
| 758 |
-
}
|
| 759 |
-
sdq_shutdown();
|
| 760 |
-
llama_free(ctx);
|
| 761 |
-
llama_model_free(model);
|
| 762 |
-
llama_backend_free();
|
| 763 |
-
return rc;
|
| 764 |
-
}
|
| 765 |
-
|
| 766 |
-
// ---- 主流程
|
| 767 |
-
if (!opt.prompt.empty()) {
|
| 768 |
-
run_prompt(opt.prompt, true);
|
| 769 |
-
generate(opt.n_predict);
|
| 770 |
-
}
|
| 771 |
-
|
| 772 |
-
if (opt.bench) {
|
| 773 |
-
// benchmark:固定提示詞 ×3,量 prefill / decode / 分頁指標
|
| 774 |
-
const std::vector<std::string> prompts = {
|
| 775 |
-
"請用中文簡短介紹 SSD、RAM、CPU 之間的資料分層是什麼,以及為什麼快取很重要。",
|
| 776 |
-
"Write a short paragraph explaining how a mixture-of-experts model routes tokens to experts.",
|
| 777 |
-
"請列出十種日常會用到電腦的工作,並各用一句話說明。",
|
| 778 |
-
};
|
| 779 |
-
for (int round = 0; round < 3; round++) {
|
| 780 |
-
fprintf(stderr, "[bench] round %d\n", round);
|
| 781 |
-
llama_memory_clear(llama_get_memory(ctx), true);
|
| 782 |
-
decoded.clear();
|
| 783 |
-
P.get_stats().tokens.store(0);
|
| 784 |
-
run_prompt(prompts[round % prompts.size()], false);
|
| 785 |
-
generate(48);
|
| 786 |
-
P.dump_stats(round == 0 ? "bench_warm" : "bench_hot");
|
| 787 |
-
}
|
| 788 |
-
}
|
| 789 |
-
|
| 790 |
-
if (opt.interactive) {
|
| 791 |
-
printf("\n(輸入文字開始對話;/exit 離開、/reset 重設、/stats 顯示指標)\n");
|
| 792 |
-
std::string line;
|
| 793 |
-
while (true) {
|
| 794 |
-
printf("\nuser> ");
|
| 795 |
-
fflush(stdout);
|
| 796 |
-
if (!std::getline(std::cin, line)) {
|
| 797 |
-
break;
|
| 798 |
-
}
|
| 799 |
-
if (line == "/exit" || line == "/quit") {
|
| 800 |
-
break;
|
| 801 |
-
}
|
| 802 |
-
if (line == "/reset") {
|
| 803 |
-
llama_memory_clear(llama_get_memory(ctx), true);
|
| 804 |
-
decoded.clear();
|
| 805 |
-
st.msgs.clear();
|
| 806 |
-
if (!opt.system.empty()) {
|
| 807 |
-
st.msgs.push_back({"system", opt.system.c_str()});
|
| 808 |
-
}
|
| 809 |
-
printf("(已重設)\n");
|
| 810 |
-
continue;
|
| 811 |
-
}
|
| 812 |
-
if (line == "/stats") {
|
| 813 |
-
P.dump_stats("manual");
|
| 814 |
-
fprintf(stderr, "hit_rate=%.1f%% arena=%zu MB RSS=%zu MB\n",
|
| 815 |
-
P.hit_rate() * 100, P.used_bytes() / 1048576, rss_mb());
|
| 816 |
-
continue;
|
| 817 |
-
}
|
| 818 |
-
if (line.empty()) {
|
| 819 |
-
continue;
|
| 820 |
-
}
|
| 821 |
-
run_prompt(line, true);
|
| 822 |
-
generate(opt.n_predict);
|
| 823 |
-
}
|
| 824 |
-
}
|
| 825 |
-
|
| 826 |
-
P.get_stats().moe_calls.fetch_add(0);
|
| 827 |
-
sdq_shutdown();
|
| 828 |
-
|
| 829 |
-
fprintf(stderr, "\n[sdq] ===== 總結 =====\n");
|
| 830 |
-
fprintf(stderr, "[sdq] prefill: %" PRIu64 " tok / %.2fs = %.1f tok/s\n",
|
| 831 |
-
n_prefill_tok, t_prefill, t_prefill > 0 ? n_prefill_tok / t_prefill : 0.0);
|
| 832 |
-
const uint64_t io_end = read_bytes_io();
|
| 833 |
-
fprintf(stderr, "[sdq] 全程 SSD 實際讀取: %.2f GB\n", (io_end - io_read0) / 1073741824.0);
|
| 834 |
-
print_mem_report("結束時");
|
| 835 |
-
|
| 836 |
-
if (logits_fp) {
|
| 837 |
-
fclose(logits_fp);
|
| 838 |
-
}
|
| 839 |
-
llama_sampler_free(smpl);
|
| 840 |
-
llama_free(ctx);
|
| 841 |
-
llama_model_free(model);
|
| 842 |
-
llama_backend_free();
|
| 843 |
-
return 0;
|
| 844 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sdq-ling/sdq-ling/sdq_moe.cpp
DELETED
|
@@ -1,735 +0,0 @@
|
|
| 1 |
-
// SDQ 被分頁的 MoE FFN(自訂 ggml op)
|
| 2 |
-
//
|
| 3 |
-
// 為什麼要自己算:上游 build_moe_ffn() 用 ggml_mul_mat_id 直接對
|
| 4 |
-
// blk.N.ffn_{gate,up,down}_exps.weight 這個「三維大張量」做矩陣乘,
|
| 5 |
-
// 權重必須整個在 RAM 裡。20.6 GiB 的專家權重在 8 GB 機器上不可能。
|
| 6 |
-
//
|
| 7 |
-
// 這裡改成:
|
| 8 |
-
// 1. 路由(gate_inp matmul + softmax + top-k + 權重正規化)仍由 ggml 圖算完,
|
| 9 |
-
// 我們只拿 selected_experts [n_expert_used, n_tokens] 與 weights。
|
| 10 |
-
// 2. 每個 (layer, token) 需要的 8 個 expert 由 pager 保證在 RAM:
|
| 11 |
-
// 缺頁 → 從 SSD pread 進 arena(背景執行緒的預取可先後到)。
|
| 12 |
-
// 3. 用 ggml 自己最佳化的量化點積(ggml_vec_dot_*,AVX2/AVX512)逐 expert 算
|
| 13 |
-
// gate/up → SiLU → down → 加權累加。
|
| 14 |
-
// 4. 多執行緒:ith/nth 由 ggml 分配;分頁階段各執行緒各自 pread,餵飽 SSD 佇列深度。
|
| 15 |
-
|
| 16 |
-
#include "sdq_pager.h"
|
| 17 |
-
|
| 18 |
-
#include "ggml-cpu.h"
|
| 19 |
-
#include "ggml-cpu/quants.h"
|
| 20 |
-
|
| 21 |
-
#include <algorithm>
|
| 22 |
-
#include <atomic>
|
| 23 |
-
#include <chrono>
|
| 24 |
-
#include <cmath>
|
| 25 |
-
#include <condition_variable>
|
| 26 |
-
#include <cstdio>
|
| 27 |
-
#include <cstring>
|
| 28 |
-
#include <mutex>
|
| 29 |
-
#include <utility>
|
| 30 |
-
#include <string>
|
| 31 |
-
#include <vector>
|
| 32 |
-
|
| 33 |
-
namespace sdq {
|
| 34 |
-
|
| 35 |
-
// ---------------------------------------------------------------- 效能計時探針
|
| 36 |
-
// 用途:找出「自訂 MoE op」裡面時間到底花在哪裡。開 SDQ_PROFILE_TIME=1 才啟用,
|
| 37 |
-
// 關閉時是零成本(幾個 if constexpr 級別的布林檢查)。
|
| 38 |
-
struct moe_prof {
|
| 39 |
-
std::atomic<uint64_t> op_ns{0}; // 整個 op
|
| 40 |
-
std::atomic<uint64_t> page_ns{0}; // probe / reserve_slot(含鎖)
|
| 41 |
-
std::atomic<uint64_t> setup_ns{0}; // op 前置:unpin_all + 建 tok_of_expert + 前兩道 barrier
|
| 42 |
-
std::atomic<uint64_t> bar_ns{0}; // 所有 barrier 的等待時間
|
| 43 |
-
std::atomic<uint64_t> read_ns{0}; // read_expert_span / acquire 的 I/O
|
| 44 |
-
std::atomic<uint64_t> fwd_ns{0}; // expert_forward 合計
|
| 45 |
-
std::atomic<uint64_t> qact_ns{0}; // 其中:量化 activation
|
| 46 |
-
std::atomic<uint64_t> dot_ns{0}; // 其中:gate/up/down 三次點積
|
| 47 |
-
std::atomic<uint64_t> act_ns{0}; // 其中:SiLU + 加權
|
| 48 |
-
std::atomic<uint64_t> calls{0};
|
| 49 |
-
std::atomic<uint64_t> experts{0};
|
| 50 |
-
};
|
| 51 |
-
static moe_prof g_prof;
|
| 52 |
-
|
| 53 |
-
static inline bool prof_on() {
|
| 54 |
-
static const bool on = [] { const char * e = getenv("SDQ_PROFILE_TIME"); return e && *e && *e != '0'; }();
|
| 55 |
-
return on;
|
| 56 |
-
}
|
| 57 |
-
static inline uint64_t now_ns() {
|
| 58 |
-
return (uint64_t) std::chrono::duration_cast<std::chrono::nanoseconds>(
|
| 59 |
-
std::chrono::steady_clock::now().time_since_epoch()).count();
|
| 60 |
-
}
|
| 61 |
-
#define PROF_T0_x(v) const uint64_t v = prof_on() ? now_ns() : 0
|
| 62 |
-
#define PROF_ADD(field, tv) do { if (prof_on()) g_prof.field += now_ns() - (tv); } while (0)
|
| 63 |
-
|
| 64 |
-
// ---------------------------------------------------------------- 執行緒柵欄
|
| 65 |
-
|
| 66 |
-
class gen_barrier {
|
| 67 |
-
public:
|
| 68 |
-
void sync(int n) {
|
| 69 |
-
PROF_T0_x(_tbar);
|
| 70 |
-
std::unique_lock<std::mutex> lk(mu_);
|
| 71 |
-
const unsigned long g = gen_;
|
| 72 |
-
if (++count_ == n) {
|
| 73 |
-
count_ = 0;
|
| 74 |
-
gen_++;
|
| 75 |
-
cv_.notify_all();
|
| 76 |
-
PROF_ADD(bar_ns, _tbar);
|
| 77 |
-
return;
|
| 78 |
-
}
|
| 79 |
-
cv_.wait(lk, [this, g] { return gen_ != g; });
|
| 80 |
-
PROF_ADD(bar_ns, _tbar);
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
private:
|
| 84 |
-
int count_ = 0;
|
| 85 |
-
unsigned long gen_ = 0;
|
| 86 |
-
std::mutex mu_;
|
| 87 |
-
std::condition_variable cv_;
|
| 88 |
-
};
|
| 89 |
-
|
| 90 |
-
// ---------------------------------------------------------------- 量化點積派送
|
| 91 |
-
|
| 92 |
-
// ggml-cpu 的 K-quant kernel 把 fp16 scale 交給 ggml_table_f32_f16 查表換算
|
| 93 |
-
// (simd-mappings.h 的 fallback 分支,x86 走的就是這條),而那張表只在
|
| 94 |
-
// ggml_cpu_init() 裡填。用 ggml_vec_dot_* 直接呼叫 kernel 時若 ggml_cpu_init()
|
| 95 |
-
// 還沒跑過,每個點積都會安靜地回傳 0 —— 不會 crash,也不會報錯。
|
| 96 |
-
// llama.cpp 自己的 mul_mat 不會踩到這個坑,因為 backend 建立時一定呼叫過
|
| 97 |
-
// ggml_cpu_init();自訂 op 走的是 ggml graph,時序上不一定保證。
|
| 98 |
-
static void sdq_ensure_cpu_init() {
|
| 99 |
-
static std::once_flag once;
|
| 100 |
-
std::call_once(once, [] { ggml_cpu_init(); });
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
using vec_dot_fn = void (*)(int, float *, size_t, const void *, size_t, const void *, size_t, int);
|
| 104 |
-
|
| 105 |
-
static vec_dot_fn dot_for(ggml_type t) {
|
| 106 |
-
switch (t) {
|
| 107 |
-
case GGML_TYPE_Q4_K: return ggml_vec_dot_q4_K_q8_K;
|
| 108 |
-
case GGML_TYPE_Q5_K: return ggml_vec_dot_q5_K_q8_K;
|
| 109 |
-
case GGML_TYPE_Q6_K: return ggml_vec_dot_q6_K_q8_K;
|
| 110 |
-
case GGML_TYPE_Q3_K: return ggml_vec_dot_q3_K_q8_K;
|
| 111 |
-
case GGML_TYPE_Q2_K: return ggml_vec_dot_q2_K_q8_K;
|
| 112 |
-
case GGML_TYPE_Q8_0: return ggml_vec_dot_q8_0_q8_0;
|
| 113 |
-
case GGML_TYPE_Q4_0: return ggml_vec_dot_q4_0_q8_0;
|
| 114 |
-
case GGML_TYPE_Q5_0: return ggml_vec_dot_q5_0_q8_0;
|
| 115 |
-
// Qwen3.8-Flash-Next 的 down_exps 用 q5_1(unsloth UD-Q4_K_XL)。
|
| 116 |
-
// q5_1 在 ggml 裡配的是 q8_1 activation(見 type_traits:q5_1 的
|
| 117 |
-
// vec_dot_type 是 q8_1),不是 q8_0 —— 配錯會整層數值錯掉。
|
| 118 |
-
case GGML_TYPE_Q5_1: return ggml_vec_dot_q5_1_q8_1;
|
| 119 |
-
case GGML_TYPE_IQ4_NL: return ggml_vec_dot_iq4_nl_q8_0;
|
| 120 |
-
case GGML_TYPE_IQ4_XS: return ggml_vec_dot_iq4_xs_q8_K;
|
| 121 |
-
default: return nullptr;
|
| 122 |
-
}
|
| 123 |
-
}
|
| 124 |
-
|
| 125 |
-
// K-quant 的權重配 Q8_K activation;其餘配 Q8_0(與 ggml 的 vec_dot_type 一致)
|
| 126 |
-
static ggml_type act_type_for(ggml_type w) {
|
| 127 |
-
switch (w) {
|
| 128 |
-
case GGML_TYPE_Q4_K:
|
| 129 |
-
case GGML_TYPE_Q5_K:
|
| 130 |
-
case GGML_TYPE_Q6_K:
|
| 131 |
-
case GGML_TYPE_Q3_K:
|
| 132 |
-
case GGML_TYPE_Q2_K:
|
| 133 |
-
case GGML_TYPE_IQ4_XS:
|
| 134 |
-
return GGML_TYPE_Q8_K;
|
| 135 |
-
// q5_1 配 q8_1(q5_1 的 min 值是 fp16,不是 q8_0 的 0)
|
| 136 |
-
case GGML_TYPE_Q5_1:
|
| 137 |
-
return GGML_TYPE_Q8_1;
|
| 138 |
-
default:
|
| 139 |
-
return GGML_TYPE_Q8_0;
|
| 140 |
-
}
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
// 把 activation 量化成對應的 dot 型別
|
| 144 |
-
// 注意:ggml_quantize_chunk() 不支援 Q8_K(Q8_K 只在 mul_mat kernel 內部產生),
|
| 145 |
-
// 所以這裡直接呼叫 ggml-cpu 的 quantize_row_q8_K。
|
| 146 |
-
static void quantize_act(ggml_type t, const float * x, void * dst, int64_t n) {
|
| 147 |
-
switch (t) {
|
| 148 |
-
case GGML_TYPE_Q8_K: quantize_row_q8_K(x, dst, n); break;
|
| 149 |
-
case GGML_TYPE_Q8_1: quantize_row_q8_1(x, dst, n); break;
|
| 150 |
-
case GGML_TYPE_Q8_0: quantize_row_q8_0(x, dst, n); break;
|
| 151 |
-
default: ggml_quantize_chunk(t, x, dst, 0, 1, n, nullptr); break;
|
| 152 |
-
}
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
// ⚠️ 不要在這裡「批次化」點積。
|
| 156 |
-
// ggml 的 x86 版 K-quant kernel(ggml/src/ggml-cpu/arch/x86/quants.c)開頭就是
|
| 157 |
-
// assert(nrc == 1); UNUSED(nrc); UNUSED(bx); UNUSED(by); UNUSED(bs);
|
| 158 |
-
// 只有 ARM 的 SVE+MATMUL 版本才支援 nrc==2。而 Release 編譯(-DNDEBUG)會把 assert
|
| 159 |
-
// 編譯掉,於是「傳 nrc=16」不會報錯、只會安靜地算第 1 列,其餘 15 列留在記憶體裡
|
| 160 |
-
// 是舊資料 —— 產出看起來合理但數值全錯。
|
| 161 |
-
// (本專案真的踩過:微基準測量到「nrc=16 快 16 倍」,其實是「只做了 1/16 的工作」,
|
| 162 |
-
// 而且權重填 0x5A 讓 checksum 兩邊都是 0,完美掩蓋了錯誤。教訓見 docs/04。)
|
| 163 |
-
// 想要真的多列 SIMD 效能,只能自己寫 AVX512-VNNI 的 kernel,不是靠改參數。
|
| 164 |
-
|
| 165 |
-
static inline void dot_row(vec_dot_fn fn, const uint8_t * row, const void * xq,
|
| 166 |
-
size_t stride_row, int n, float & acc) {
|
| 167 |
-
float s = 0.0f;
|
| 168 |
-
fn(n, &s, 0, row, stride_row, xq, 0, 1);
|
| 169 |
-
acc += s;
|
| 170 |
-
}
|
| 171 |
-
|
| 172 |
-
// 一次算 SDQ_DOT_BATCH 個輸出列。
|
| 173 |
-
// 為什麼需要:ggml 的 K-quant 點積 kernel 是為「多列」設計的,nrc>1 會走完全不同的
|
| 174 |
-
// SIMD 路徑。實測(tools 裡的 microdot):nrc=1 只有 24~28 GMAC/s,nrc=16 有
|
| 175 |
-
// 347~438 GMAC/s(快 14~16 倍),而且 **checksum 完全相同**(逐位元組等價)。
|
| 176 |
-
// 舊程式逐列呼叫 nrc=1,等於把 14 倍的效能丟掉。
|
| 177 |
-
static const int SDQ_DOT_BATCH = 16;
|
| 178 |
-
|
| 179 |
-
static inline void dot_rows(vec_dot_fn fn, const uint8_t * rows, size_t row_stride,
|
| 180 |
-
const void * xq, int n, int64_t n_rows, float * out) {
|
| 181 |
-
int64_t r = 0;
|
| 182 |
-
for (; r + SDQ_DOT_BATCH <= n_rows; r += SDQ_DOT_BATCH) {
|
| 183 |
-
fn(n, out + r, 1, rows + (size_t) r * row_stride, row_stride, xq, 0, SDQ_DOT_BATCH);
|
| 184 |
-
}
|
| 185 |
-
for (; r < n_rows; r++) {
|
| 186 |
-
fn(n, out + r, 0, rows + (size_t) r * row_stride, row_stride, xq, 0, 1);
|
| 187 |
-
}
|
| 188 |
-
}
|
| 189 |
-
|
| 190 |
-
// ---------------------------------------------------------------- 路由紀錄(SDQ_ROUTE_LOG)
|
| 191 |
-
// 用途:量測「連續 k 個 token 的 expert 集合重疊多少」。
|
| 192 |
-
// 這決定多 token 批次(投機解碼 / 批次解碼)能不能省下 SSD 讀取:
|
| 193 |
-
// 每層的關鍵路徑是「有沒有任何一個 expert 沒命中」,所以 U(k) < 8k 就有賺。
|
| 194 |
-
// 格式(每行一次 op 呼叫,空白分欄):
|
| 195 |
-
// <layer> <n_tok> <t0:e0,e1,...> <t1:...> ...
|
| 196 |
-
static void sdq_route_log(int il, int64_t n_tok, const ggml_tensor * ids,
|
| 197 |
-
size_t ids_nb0, size_t ids_nb1, int n_used) {
|
| 198 |
-
static const char * path = getenv("SDQ_ROUTE_LOG");
|
| 199 |
-
if (!path || !*path) {
|
| 200 |
-
return;
|
| 201 |
-
}
|
| 202 |
-
static std::mutex mtx;
|
| 203 |
-
static FILE * f = nullptr;
|
| 204 |
-
{
|
| 205 |
-
std::lock_guard<std::mutex> lk(mtx);
|
| 206 |
-
if (!f) {
|
| 207 |
-
f = fopen(path, "w");
|
| 208 |
-
}
|
| 209 |
-
if (!f) {
|
| 210 |
-
return;
|
| 211 |
-
}
|
| 212 |
-
fprintf(f, "%d %lld", il, (long long) n_tok);
|
| 213 |
-
const uint8_t * base = (const uint8_t *) ids->data;
|
| 214 |
-
for (int64_t t = 0; t < n_tok; t++) {
|
| 215 |
-
fprintf(f, " %lld:", (long long) t);
|
| 216 |
-
for (int k = 0; k < n_used; k++) {
|
| 217 |
-
const int32_t e = *(const int32_t *) (base + (size_t) k * ids_nb0 + (size_t) t * ids_nb1);
|
| 218 |
-
fprintf(f, "%s%d", k ? "," : "", e);
|
| 219 |
-
}
|
| 220 |
-
}
|
| 221 |
-
fputc('\n', f);
|
| 222 |
-
}
|
| 223 |
-
}
|
| 224 |
-
|
| 225 |
-
// ---------------------------------------------------------------- 每層執行紀錄
|
| 226 |
-
|
| 227 |
-
// 重要:input 張量的指標**不能**在建圖時暫存起來 —— llama.cpp 每次 decode 都會
|
| 228 |
-
// 重新建圖,graph context 的配置器可能把張量搬到不同位址(形狀不同時尤其明顯)。
|
| 229 |
-
// 所以 op 執行時一律從 dst->src[] 取得,這才是唯一可靠的來源。
|
| 230 |
-
struct layer_rec {
|
| 231 |
-
int il = -1;
|
| 232 |
-
int64_t n_expert = 0;
|
| 233 |
-
int64_t n_expert_used = 0;
|
| 234 |
-
float swiglu_limit = 0.0f;
|
| 235 |
-
|
| 236 |
-
// 每個 expert 需要處理的 (token, topk 位置) 配對
|
| 237 |
-
std::vector<std::vector<std::pair<int32_t, int32_t>>> tok_of_expert;
|
| 238 |
-
// 每個 token 用到的 (expert, topk 位置) —— token 切分時用(避免輸出競爭)
|
| 239 |
-
std::vector<std::vector<std::pair<int32_t, int32_t>>> exp_of_tok;
|
| 240 |
-
std::vector<int32_t> need; // 本層要用的 expert 編號
|
| 241 |
-
std::vector<int32_t> need_sorted; // 依編號排序(合併讀取用)
|
| 242 |
-
std::vector<int32_t> need_slot; // 對應的 arena 槽位
|
| 243 |
-
std::vector<std::vector<float>> partial; // mode A:每個執行緒一份累加緩衝
|
| 244 |
-
std::vector<int32_t> slot_of_expert; // [expert] -> arena 槽位(-1 = 沒分頁)
|
| 245 |
-
std::vector<int32_t> need_slot_of_expert; // 合併讀取時暫存槽位
|
| 246 |
-
std::vector<int32_t> miss_sorted; // 需要從 SSD 讀的 expert(已排序)
|
| 247 |
-
gen_barrier bar;
|
| 248 |
-
std::mutex mtx;
|
| 249 |
-
bool warned = false;
|
| 250 |
-
};
|
| 251 |
-
|
| 252 |
-
static std::vector<layer_rec *> g_recs;
|
| 253 |
-
|
| 254 |
-
static layer_rec * rec_for(int il) {
|
| 255 |
-
static std::mutex m;
|
| 256 |
-
std::lock_guard<std::mutex> lk(m);
|
| 257 |
-
if ((int) g_recs.size() <= il) {
|
| 258 |
-
g_recs.resize(il + 1, nullptr);
|
| 259 |
-
}
|
| 260 |
-
if (!g_recs[il]) {
|
| 261 |
-
g_recs[il] = new layer_rec();
|
| 262 |
-
g_recs[il]->il = il;
|
| 263 |
-
}
|
| 264 |
-
return g_recs[il];
|
| 265 |
-
}
|
| 266 |
-
|
| 267 |
-
// ---------------------------------------------------------------- 自訂 op 本體
|
| 268 |
-
|
| 269 |
-
// mode C(預設,partial 緩衝 <= 上限):依 expert 流水線,取→算交錯、無中途 barrier。
|
| 270 |
-
// mode B(partial 緩衝太大時的退路):先分頁完再一起算,會有 barrier 等待。
|
| 271 |
-
// 上限給一個很大的值=永遠走 mode C;給 0=強制 mode B(除錯用)。
|
| 272 |
-
static size_t env_partial_cap_mb() {
|
| 273 |
-
const char * e = getenv("SDQ_PARTIAL_CAP_MB");
|
| 274 |
-
return e ? (size_t) atoi(e) : 4096; // MB,預設 4096 MB → 一律 mode C
|
| 275 |
-
}
|
| 276 |
-
|
| 277 |
-
// ---------------------------------------------------------------- 單一 expert 的數學
|
| 278 |
-
// 從 arena 的槽位記憶體算出一個 expert 對多個 token 的輸出(加權後直接累加到 out)
|
| 279 |
-
static void expert_forward(const expert_desc & ed, const uint8_t * base,
|
| 280 |
-
const float * cur, int64_t n_embd,
|
| 281 |
-
const std::vector<std::pair<int32_t, int32_t>> & toks,
|
| 282 |
-
const float * wts, size_t wts_nb0, size_t wts_nb1, int /*n_used*/,
|
| 283 |
-
float swiglu_limit,
|
| 284 |
-
float * out, std::vector<uint8_t> * scratch) {
|
| 285 |
-
sdq_ensure_cpu_init();
|
| 286 |
-
|
| 287 |
-
int64_t gate_rows = ed.ne1;
|
| 288 |
-
int64_t up_rows = ed.ne1;
|
| 289 |
-
size_t gate_row = ggml_row_size(ed.type[part::GATE], ed.ne0);
|
| 290 |
-
size_t up_row = gate_row;
|
| 291 |
-
if (ed.fused_gate_up) {
|
| 292 |
-
gate_rows = ed.ne1 / 2;
|
| 293 |
-
up_rows = gate_rows;
|
| 294 |
-
} else {
|
| 295 |
-
up_row = ggml_row_size(ed.type[part::UP], ed.ne0);
|
| 296 |
-
}
|
| 297 |
-
|
| 298 |
-
const vec_dot_fn f_gate = dot_for(ed.type[part::GATE]);
|
| 299 |
-
const vec_dot_fn f_up = ed.fused_gate_up ? f_gate : dot_for(ed.type[part::UP]);
|
| 300 |
-
const vec_dot_fn f_down = dot_for(ed.type[part::DOWN]);
|
| 301 |
-
if (!f_gate || !f_up || !f_down) {
|
| 302 |
-
fprintf(stderr, "[sdq] 不支援的專家量化型別:gate=%s up=%s down=%s\n",
|
| 303 |
-
ggml_type_name(ed.type[part::GATE]),
|
| 304 |
-
ggml_type_name(ed.type[part::UP]),
|
| 305 |
-
ggml_type_name(ed.type[part::DOWN]));
|
| 306 |
-
return;
|
| 307 |
-
}
|
| 308 |
-
|
| 309 |
-
const uint8_t * wg = base + ed.slot_off[part::GATE];
|
| 310 |
-
const uint8_t * wu = base + ed.slot_off[part::UP];
|
| 311 |
-
const uint8_t * wd = base + ed.slot_off[part::DOWN];
|
| 312 |
-
const size_t down_row = ggml_row_size(ed.type[part::DOWN], ed.down_ne0);
|
| 313 |
-
|
| 314 |
-
const ggml_type act_gate = act_type_for(ed.type[part::GATE]);
|
| 315 |
-
const ggml_type act_down = act_type_for(ed.type[part::DOWN]);
|
| 316 |
-
const size_t xq_sz = ggml_row_size(act_gate, ed.ne0);
|
| 317 |
-
const size_t hq_sz = ggml_row_size(act_down, ed.down_ne0);
|
| 318 |
-
if (scratch->size() < xq_sz + hq_sz) {
|
| 319 |
-
scratch->assign(xq_sz + hq_sz, 0);
|
| 320 |
-
}
|
| 321 |
-
uint8_t * xq = scratch->data();
|
| 322 |
-
uint8_t * hq = xq + xq_sz;
|
| 323 |
-
|
| 324 |
-
std::vector<float> gate((size_t) gate_rows);
|
| 325 |
-
std::vector<float> upv((size_t) up_rows);
|
| 326 |
-
std::vector<float> h((size_t) ed.down_ne0);
|
| 327 |
-
|
| 328 |
-
PROF_T0_x(_ta); // expert_forward 總計時
|
| 329 |
-
for (const auto & tk : toks) {
|
| 330 |
-
const int32_t t = tk.first;
|
| 331 |
-
const int32_t k = tk.second;
|
| 332 |
-
|
| 333 |
-
PROF_T0_x(_tb); // 量化 activation
|
| 334 |
-
quantize_act(act_gate, cur + (size_t) t * n_embd, xq, ed.ne0);
|
| 335 |
-
PROF_ADD(qact_ns, _tb);
|
| 336 |
-
|
| 337 |
-
PROF_T0_x(_tc); // gate / up / down 三次點積
|
| 338 |
-
for (int64_t r = 0; r < gate_rows; r++) {
|
| 339 |
-
float acc = 0.0f;
|
| 340 |
-
dot_row(f_gate, wg + r * gate_row, xq, gate_row, (int) ed.ne0, acc);
|
| 341 |
-
gate[(size_t) r] = acc;
|
| 342 |
-
}
|
| 343 |
-
for (int64_t r = 0; r < up_rows; r++) {
|
| 344 |
-
float acc = 0.0f;
|
| 345 |
-
const uint8_t * src = ed.fused_gate_up ? (wg + (gate_rows + r) * gate_row)
|
| 346 |
-
: (wu + r * up_row);
|
| 347 |
-
dot_row(f_up, src, xq, ed.fused_gate_up ? gate_row : up_row, (int) ed.ne0, acc);
|
| 348 |
-
upv[(size_t) r] = acc;
|
| 349 |
-
}
|
| 350 |
-
|
| 351 |
-
PROF_ADD(dot_ns, _tc);
|
| 352 |
-
|
| 353 |
-
PROF_T0_x(_td); // SiLU + 加權求和
|
| 354 |
-
for (int64_t r = 0; r < ed.down_ne0; r++) {
|
| 355 |
-
const float g = gate[(size_t) r];
|
| 356 |
-
const float u = upv[(size_t) r];
|
| 357 |
-
float act;
|
| 358 |
-
if (swiglu_limit > 1e-6f) {
|
| 359 |
-
const float uc = std::max(-swiglu_limit, std::min(swiglu_limit, u));
|
| 360 |
-
float gc = g / (1.0f + std::exp(-g));
|
| 361 |
-
gc = std::max(-swiglu_limit, std::min(swiglu_limit, gc));
|
| 362 |
-
act = gc * uc;
|
| 363 |
-
} else {
|
| 364 |
-
act = (g / (1.0f + std::exp(-g))) * u;
|
| 365 |
-
}
|
| 366 |
-
h[(size_t) r] = act;
|
| 367 |
-
}
|
| 368 |
-
quantize_act(act_down, h.data(), hq, ed.down_ne0);
|
| 369 |
-
|
| 370 |
-
PROF_ADD(act_ns, _td);
|
| 371 |
-
|
| 372 |
-
const float w = *(const float *) ((const uint8_t *) wts + k * wts_nb0 + (size_t) t * wts_nb1);
|
| 373 |
-
float * o = out + (size_t) t * n_embd;
|
| 374 |
-
for (int64_t r = 0; r < ed.down_ne1; r++) {
|
| 375 |
-
float acc = 0.0f;
|
| 376 |
-
dot_row(f_down, wd + r * down_row, hq, down_row, (int) ed.down_ne0, acc);
|
| 377 |
-
o[r] += acc * w;
|
| 378 |
-
}
|
| 379 |
-
}
|
| 380 |
-
PROF_ADD(fwd_ns, _ta);
|
| 381 |
-
}
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
static void moe_custom_op(ggml_tensor * dst, int ith, int nth, void * userdata) {
|
| 385 |
-
layer_rec * R = (layer_rec *) userdata;
|
| 386 |
-
pager & P = pager::instance();
|
| 387 |
-
|
| 388 |
-
// 從 op 的 src 取得輸入(保證是「這一次」真的被計算的張量)
|
| 389 |
-
const ggml_tensor * src_cur = dst->src[0];
|
| 390 |
-
const ggml_tensor * src_ids = dst->src[1];
|
| 391 |
-
const ggml_tensor * src_wts = dst->src[2];
|
| 392 |
-
if (!src_cur || !src_ids || !src_wts) {
|
| 393 |
-
if (ith == 0) {
|
| 394 |
-
fprintf(stderr, "[sdq] op 缺少 src(%d)\n", R->il);
|
| 395 |
-
}
|
| 396 |
-
return;
|
| 397 |
-
}
|
| 398 |
-
|
| 399 |
-
const layer_desc * L = P.layer(R->il);
|
| 400 |
-
PROF_T0_x(_tg); // op 總計時
|
| 401 |
-
g_prof.calls++;
|
| 402 |
-
const int64_t n_embd = src_cur->ne[0];
|
| 403 |
-
const int64_t n_tok = src_cur->ne[2];
|
| 404 |
-
const int64_t n_used = R->n_expert_used;
|
| 405 |
-
// ids 是 2D [n_expert_used, n_tokens] → 用 nb[0]/nb[1]
|
| 406 |
-
// wts 是 3D [1, n_expert_used, n_tokens] → 用 nb[1]/nb[2](nb[0] 會是 4 而不是 n_used*4!)
|
| 407 |
-
const size_t ids_nb0 = src_ids->nb[0], ids_nb1 = src_ids->nb[1];
|
| 408 |
-
const size_t wts_nb0 = src_wts->nb[src_wts->ne[0] == 1 ? 1 : 0];
|
| 409 |
-
const size_t wts_nb1 = src_wts->nb[src_wts->ne[0] == 1 ? 2 : 1];
|
| 410 |
-
const float * cur = (const float *) src_cur->data;
|
| 411 |
-
const float * wts = (const float *) src_wts->data;
|
| 412 |
-
|
| 413 |
-
if (!L || L->experts.empty() || dst->type != GGML_TYPE_F32) {
|
| 414 |
-
if (ith == 0 && !R->warned) {
|
| 415 |
-
R->warned = true;
|
| 416 |
-
fprintf(stderr, "[sdq] 層 %d 沒有可用的分頁描述\n", R->il);
|
| 417 |
-
}
|
| 418 |
-
return;
|
| 419 |
-
}
|
| 420 |
-
|
| 421 |
-
if (R->il == 0 && getenv("SDQ_DBG_OP")) {
|
| 422 |
-
fprintf(stderr, "[sdq-op] L%d n_tok=%lld nth=%d need=%d ids_nb1=%zu wts_nb1=%zu\n",
|
| 423 |
-
R->il, (long long) n_tok, nth, (int) R->need.size(), ids_nb1, wts_nb1);
|
| 424 |
-
}
|
| 425 |
-
// ---- 1) 解除上一層的 pin(mode B 直接累加輸出,需要先清空)
|
| 426 |
-
PROF_T0_x(_tsetup);
|
| 427 |
-
if (ith == 0) {
|
| 428 |
-
P.unpin_all();
|
| 429 |
-
}
|
| 430 |
-
if (nth > 1) { R->bar.sync(nth); }
|
| 431 |
-
|
| 432 |
-
// ---- 2) 依 expert 分組 token(只有執行緒 0 做,其餘在此等待)
|
| 433 |
-
if (ith == 0) {
|
| 434 |
-
std::lock_guard<std::mutex> lk(R->mtx);
|
| 435 |
-
R->tok_of_expert.assign((size_t) R->n_expert, {});
|
| 436 |
-
R->exp_of_tok.assign((size_t) n_tok, {});
|
| 437 |
-
const uint8_t * ids_base = (const uint8_t *) src_ids->data;
|
| 438 |
-
for (int64_t t = 0; t < n_tok; t++) {
|
| 439 |
-
for (int64_t k = 0; k < n_used; k++) {
|
| 440 |
-
const int32_t e = *(const int32_t *) (ids_base + k * ids_nb0 + (size_t) t * ids_nb1);
|
| 441 |
-
if (e >= 0 && e < R->n_expert) {
|
| 442 |
-
auto & v = R->tok_of_expert[(size_t) e];
|
| 443 |
-
bool seen = false;
|
| 444 |
-
for (const auto & p : v) {
|
| 445 |
-
if (p.first == (int32_t) t) { seen = true; break; }
|
| 446 |
-
}
|
| 447 |
-
if (!seen) {
|
| 448 |
-
v.emplace_back((int32_t) t, (int32_t) k);
|
| 449 |
-
}
|
| 450 |
-
R->exp_of_tok[(size_t) t].emplace_back(e, (int32_t) k);
|
| 451 |
-
}
|
| 452 |
-
}
|
| 453 |
-
}
|
| 454 |
-
R->need.clear();
|
| 455 |
-
for (int64_t e = 0; e < R->n_expert; e++) {
|
| 456 |
-
if (!R->tok_of_expert[(size_t) e].empty()) {
|
| 457 |
-
R->need.push_back((int32_t) e);
|
| 458 |
-
}
|
| 459 |
-
}
|
| 460 |
-
R->need_sorted = R->need;
|
| 461 |
-
std::sort(R->need_sorted.begin(), R->need_sorted.end());
|
| 462 |
-
R->need_slot.assign(R->need.size(), -1); // 只能由 ith==0 配置,其餘執行緒唯讀
|
| 463 |
-
R->slot_of_expert.assign((size_t) R->n_expert, -1);
|
| 464 |
-
R->need_slot_of_expert.assign((size_t) R->n_expert, -1);
|
| 465 |
-
R->miss_sorted.clear();
|
| 466 |
-
R->miss_sorted.reserve((size_t) R->n_expert);
|
| 467 |
-
if (R->partial.size() < 64) {
|
| 468 |
-
R->partial.resize(64);
|
| 469 |
-
}
|
| 470 |
-
sdq_route_log(R->il, n_tok, src_ids, ids_nb0, ids_nb1, (int) n_used);
|
| 471 |
-
}
|
| 472 |
-
if (nth > 1) { R->bar.sync(nth); }
|
| 473 |
-
PROF_ADD(setup_ns, _tsetup);
|
| 474 |
-
|
| 475 |
-
const int n_need = (int) R->need.size();
|
| 476 |
-
|
| 477 |
-
// ---- 3+4) 分頁與計算融合成一條 pipeline
|
| 478 |
-
//
|
| 479 |
-
// 關鍵效能點:不要「先把整層的 expert 都讀完再開始算」。做法是每個執行緒
|
| 480 |
-
// 自己依序處理它負責的 expert:讀一個 → 立刻算它 → 再讀下一個。
|
| 481 |
-
// 這樣 SSD 讀取和 CPU 計算自然重疊,而且 8 條執行緒各自發出 pread,
|
| 482 |
-
// SSD 佇列深度也夠(不需要再靠 queue depth)。
|
| 483 |
-
//
|
| 484 |
-
// 平行切分:mode A 依 expert 切(每個執行緒有自己的累加緩衝,最後相加);
|
| 485 |
-
// mode B 依 token 切(同一個 token 的所有 expert 由同一執行緒算,
|
| 486 |
-
// 但必須先把該 token 的 expert 都備齊,所以先分頁再計算)。
|
| 487 |
-
const int64_t out_elems = n_embd * n_tok;
|
| 488 |
-
const size_t partial_bytes = (size_t) out_elems * 4 * (size_t) nth;
|
| 489 |
-
const bool mode_a = partial_bytes <= (size_t) env_partial_cap_mb() << 20;
|
| 490 |
-
float * out = (float *) dst->data;
|
| 491 |
-
|
| 492 |
-
if (!mode_a) {
|
| 493 |
-
memset(dst->data, 0, (size_t) ggml_nbytes(dst));
|
| 494 |
-
}
|
| 495 |
-
|
| 496 |
-
if (mode_a) {
|
| 497 |
-
// ===================== mode C(預設):依 expert 流水線 =====================
|
| 498 |
-
// 三個設計重點,每個都來自實測:
|
| 499 |
-
//
|
| 500 |
-
// (1) **取→算交錯,中間沒有 barrier**。
|
| 501 |
-
// 舊的 mode B 是「先把整層 8 個 expert 全部讀完 → barrier → 才開始算」。
|
| 502 |
-
// 量測顯示 decode 有 66% 的時間卡在 barrier 上等最慢的執行緒
|
| 503 |
-
// (SDQ_PROFILE_TIME 的 barrier 等待 = op 時間的 70%)。改成交錯後
|
| 504 |
-
// 讀取與計算自然重疊,實測 2.79 → 3.49 tok/s。
|
| 505 |
-
//
|
| 506 |
-
// (2) **分頁走 acquire(),不用 reserve_slot + read_expert_span**。
|
| 507 |
-
// reserve_slot() 會在資料還沒讀進來之前就把 (layer,expert) publish 到
|
| 508 |
-
// page_ 表;一旦該層的 pread 失敗或被中斷,這個槽就會留在 page_ 裡
|
| 509 |
-
// 卻是舊資料,之後的 probe() 會把它當成命中 → 靜默算錯。
|
| 510 |
-
// acquire() 是「先讀完再 publish」,成本相同但不會有這個失效模式。
|
| 511 |
-
//
|
| 512 |
-
// (3) **i += nth 交錯分配**,不是切成連續區塊。
|
| 513 |
-
// 每個 expert 的 SSD 延遲差異很大,交錯分配讓快執行緒自然多幫忙。
|
| 514 |
-
//
|
| 515 |
-
// 輸出不直接寫 dst(同一個 token 的不同 expert 會撞在一起),
|
| 516 |
-
// 所以仍然用「每執行緒一份累加緩衝 + 最後歸約」。
|
| 517 |
-
std::vector<float> & mine = R->partial[(size_t) ith];
|
| 518 |
-
mine.assign((size_t) out_elems, 0.0f);
|
| 519 |
-
std::vector<uint8_t> scratch;
|
| 520 |
-
|
| 521 |
-
// 在取本層的 expert **之前**先替下一層排預取。
|
| 522 |
-
//
|
| 523 |
-
// 實測結論(S7):**預設關閉,而且量到開啟反而慢 8%。**
|
| 524 |
-
// 預測來自 prev_used_[il+1](上一個 token 在該層選的專家)。想用它躲在
|
| 525 |
-
// 「本層的讀取+計算」後面,但量到的結果是:
|
| 526 |
-
// 開(5.68 tok/s,預取使用率 0.48 %)vs 關(6.20 tok/s,0.28 %)
|
| 527 |
-
// 命中率幾乎一樣(70.5 % vs 70.7 %),但 SSD 位元組從 201.5 升到 216.0 MB/token
|
| 528 |
-
// → 預測不夠準,多讀的全是白讀,而且還要和本層的 demand 讀搶頻寬。
|
| 529 |
-
// 另外開啟時同一提示詞的輸出不一致 → 預取佔用的槽位(busy)會讓
|
| 530 |
-
// acquire() 偶爾拿不到槽位而**靜默跳過一個 expert**,這是正確性問題。
|
| 531 |
-
//
|
| 532 |
-
// 保留程式碼是為了讓後人不用重新發明;文件見 docs/06_slots.md §4。
|
| 533 |
-
// SDQ_PREFETCH_LAYER_AHEAD=1 可以打開來重現那個負結果。
|
| 534 |
-
static const bool layer_ahead = [] {
|
| 535 |
-
const char * e = getenv("SDQ_PREFETCH_LAYER_AHEAD");
|
| 536 |
-
return e ? atoi(e) != 0 : false;
|
| 537 |
-
}();
|
| 538 |
-
if (layer_ahead && ith == 0) {
|
| 539 |
-
P.prefetch_layer_ahead(R->il + 1);
|
| 540 |
-
}
|
| 541 |
-
|
| 542 |
-
for (int i = ith; i < n_need; i += nth) {
|
| 543 |
-
const int32_t e = R->need[(size_t) i];
|
| 544 |
-
const int32_t sl = P.acquire(R->il, e, true);
|
| 545 |
-
if (sl < 0) {
|
| 546 |
-
continue;
|
| 547 |
-
}
|
| 548 |
-
P.note_used(R->il, e);
|
| 549 |
-
expert_forward(L->experts[(size_t) e], P.slot_ptr(sl), cur, n_embd,
|
| 550 |
-
R->tok_of_expert[(size_t) e], wts, wts_nb0, wts_nb1,
|
| 551 |
-
(int) n_used, R->swiglu_limit, mine.data(), &scratch);
|
| 552 |
-
// expert_forward 回傳後這個槽位就沒人讀了(最後的歸約只讀 mine),
|
| 553 |
-
// 所以立刻放掉 pin,不要讓它被釘到下一層開頭。
|
| 554 |
-
// 不放的話 prefill 一層會把整層用到的 expert 全部釘住(最多 256 個),
|
| 555 |
-
// 大槽位不夠時就會中止(見 pager::release 的說明)。
|
| 556 |
-
P.release(sl);
|
| 557 |
-
}
|
| 558 |
-
|
| 559 |
-
if (nth > 1) { R->bar.sync(nth); }
|
| 560 |
-
for (int64_t idx = ith; idx < out_elems; idx += nth) {
|
| 561 |
-
float acc = 0.0f;
|
| 562 |
-
for (int k = 0; k < nth; k++) {
|
| 563 |
-
acc += R->partial[(size_t) k][(size_t) idx];
|
| 564 |
-
}
|
| 565 |
-
out[idx] = acc;
|
| 566 |
-
}
|
| 567 |
-
} else {
|
| 568 |
-
// mode B:token 多到 partial 緩衝太大時,依 token 切分
|
| 569 |
-
// 先把所有需要的 expert 分頁(各執行緒平行 pread)
|
| 570 |
-
{
|
| 571 |
-
PROF_T0_x(_th); // 分頁 / SSD 讀取
|
| 572 |
-
for (int i = ith; i < n_need; i += nth) {
|
| 573 |
-
const int32_t e = R->need[(size_t) i];
|
| 574 |
-
const int s = P.acquire(R->il, e, true);
|
| 575 |
-
g_prof.experts++;
|
| 576 |
-
R->need_slot[(size_t) i] = s;
|
| 577 |
-
if (s >= 0) {
|
| 578 |
-
R->slot_of_expert[(size_t) e] = s;
|
| 579 |
-
P.note_used(R->il, e);
|
| 580 |
-
}
|
| 581 |
-
}
|
| 582 |
-
PROF_ADD(read_ns, _th);
|
| 583 |
-
}
|
| 584 |
-
if (nth > 1) { R->bar.sync(nth); }
|
| 585 |
-
const float * cur_p = (const float *) src_cur->data;
|
| 586 |
-
const float * wts_p = (const float *) src_wts->data;
|
| 587 |
-
for (int64_t t = ith; t < n_tok; t += nth) {
|
| 588 |
-
std::vector<uint8_t> scratch;
|
| 589 |
-
for (const auto & ek : R->exp_of_tok[(size_t) t]) {
|
| 590 |
-
const int slot = R->slot_of_expert[(size_t) ek.first];
|
| 591 |
-
if (slot < 0) {
|
| 592 |
-
continue;
|
| 593 |
-
}
|
| 594 |
-
std::vector<std::pair<int32_t, int32_t>> one = {{(int32_t) t, ek.second}};
|
| 595 |
-
expert_forward(L->experts[(size_t) ek.first], P.slot_ptr(slot), cur_p, n_embd,
|
| 596 |
-
one, wts_p, wts_nb0, wts_nb1, (int) n_used,
|
| 597 |
-
R->swiglu_limit, out, &scratch);
|
| 598 |
-
}
|
| 599 |
-
}
|
| 600 |
-
}
|
| 601 |
-
|
| 602 |
-
// 除錯:把某一層的實際輸入/輸出 dump 出來,用 NumPy 對照。
|
| 603 |
-
// 必須先 barrier:上面的歸約是由各執行緒分片寫 out 的,沒有這一道會讀到半成品。
|
| 604 |
-
if (nth > 1) { R->bar.sync(nth); }
|
| 605 |
-
if (const char * dump_path = getenv("SDQ_MOE_DUMP")) {
|
| 606 |
-
const int dump_layer = getenv("SDQ_MOE_DUMP_LAYER") ? atoi(getenv("SDQ_MOE_DUMP_LAYER")) : -1;
|
| 607 |
-
if ((dump_layer < 0 || R->il == dump_layer) && ith == 0) {
|
| 608 |
-
std::string path = std::string(dump_path) + "." + std::to_string(R->il);
|
| 609 |
-
FILE * f = fopen(path.c_str(), "wb");
|
| 610 |
-
if (f) {
|
| 611 |
-
const int32_t meta[6] = {(int32_t) n_embd, (int32_t) n_tok, (int32_t) n_used,
|
| 612 |
-
(int32_t) R->n_expert, 1, 2};
|
| 613 |
-
fwrite(meta, sizeof(int32_t), 6, f);
|
| 614 |
-
fwrite(cur, sizeof(float), (size_t) (n_embd * n_tok), f);
|
| 615 |
-
const uint8_t * ids_base = (const uint8_t *) src_ids->data;
|
| 616 |
-
for (int64_t t = 0; t < n_tok; t++) {
|
| 617 |
-
for (int64_t k = 0; k < n_used; k++) {
|
| 618 |
-
const int32_t e = *(const int32_t *) (ids_base + k * ids_nb0 + (size_t) t * ids_nb1);
|
| 619 |
-
fwrite(&e, sizeof(int32_t), 1, f);
|
| 620 |
-
}
|
| 621 |
-
}
|
| 622 |
-
fwrite(wts, sizeof(float), (size_t) (n_used * n_tok), f);
|
| 623 |
-
fwrite(out, sizeof(float), (size_t) (n_embd * n_tok), f);
|
| 624 |
-
fclose(f);
|
| 625 |
-
fprintf(stderr, "[sdq] dumped layer %d -> %s\n", R->il, path.c_str());
|
| 626 |
-
}
|
| 627 |
-
}
|
| 628 |
-
}
|
| 629 |
-
|
| 630 |
-
if (nth > 1) { R->bar.sync(nth); }
|
| 631 |
-
PROF_ADD(op_ns, _tg);
|
| 632 |
-
}
|
| 633 |
-
|
| 634 |
-
// 輸出計時結果(sdq_shutdown 呼叫)
|
| 635 |
-
void sdq_print_time_profile() {
|
| 636 |
-
if (!prof_on()) {
|
| 637 |
-
return;
|
| 638 |
-
}
|
| 639 |
-
const double ms = 1e6;
|
| 640 |
-
auto v = [ms](const std::atomic<uint64_t> & a) { return a.load() / ms; };
|
| 641 |
-
const double op = v(g_prof.op_ns);
|
| 642 |
-
const double fwd = v(g_prof.fwd_ns);
|
| 643 |
-
fprintf(stderr, "[sdq-prof] 自訂 MoE op(%llu 次呼叫、%llu 個 expert 分頁)\n",
|
| 644 |
-
(unsigned long long) g_prof.calls.load(), (unsigned long long) g_prof.experts.load());
|
| 645 |
-
fprintf(stderr, "[sdq-prof] op 合計 %9.1f ms\n", op);
|
| 646 |
-
fprintf(stderr, "[sdq-prof] expert_forward %9.1f ms (%5.1f%%) 其中:\n", fwd, op > 0 ? 100.0 * fwd / op : 0.0);
|
| 647 |
-
fprintf(stderr, "[sdq-prof] 量化 activation%9.1f ms (%5.1f%% of fwd)\n", v(g_prof.qact_ns), fwd > 0 ? 100.0 * v(g_prof.qact_ns) / fwd : 0.0);
|
| 648 |
-
fprintf(stderr, "[sdq-prof] 三次點積 %9.1f ms (%5.1f%% of fwd)\n", v(g_prof.dot_ns), fwd > 0 ? 100.0 * v(g_prof.dot_ns) / fwd : 0.0);
|
| 649 |
-
fprintf(stderr, "[sdq-prof] SiLU+加權 %9.1f ms (%5.1f%% of fwd)\n", v(g_prof.act_ns), fwd > 0 ? 100.0 * v(g_prof.act_ns) / fwd : 0.0);
|
| 650 |
-
fprintf(stderr, "[sdq-prof] SSD 讀取/分頁 %9.1f ms (%5.1f%%)\n", v(g_prof.read_ns), op > 0 ? 100.0 * v(g_prof.read_ns) / op : 0.0);
|
| 651 |
-
fprintf(stderr, "[sdq-prof] 前置 setup %9.1f ms (%5.1f%%)\n", v(g_prof.setup_ns), op > 0 ? 100.0 * v(g_prof.setup_ns) / op : 0.0);
|
| 652 |
-
fprintf(stderr, "[sdq-prof] barrier 等待 %9.1f ms (%5.1f%%)\n", v(g_prof.bar_ns), op > 0 ? 100.0 * v(g_prof.bar_ns) / op : 0.0);
|
| 653 |
-
}
|
| 654 |
-
|
| 655 |
-
// ---------------------------------------------------------------- graph 建構
|
| 656 |
-
|
| 657 |
-
ggml_tensor * sdq_build_moe(ggml_cgraph * gf, ggml_context * ctx, ggml_tensor * cur,
|
| 658 |
-
ggml_tensor * ids, ggml_tensor * weights, int il, int64_t n_expert,
|
| 659 |
-
int64_t n_expert_used, float swiglu_limit) {
|
| 660 |
-
layer_rec * R = rec_for(il);
|
| 661 |
-
R->n_expert = n_expert;
|
| 662 |
-
R->n_expert_used = n_expert_used;
|
| 663 |
-
R->swiglu_limit = swiglu_limit;
|
| 664 |
-
|
| 665 |
-
// 呼叫端給的是 2D [n_embd, n_tokens],這裡跟上游一樣先 reshape 成 3D
|
| 666 |
-
// [n_embd, 1, n_tokens](上游 mul_mat_id 路徑也是這樣拿 token 數的)
|
| 667 |
-
ggml_tensor * cur3 = (cur->ne[1] == 1) ? cur : ggml_reshape_3d(ctx, cur, cur->ne[0], 1, cur->ne[1]);
|
| 668 |
-
const int64_t n_tok = cur3->ne[2];
|
| 669 |
-
|
| 670 |
-
ggml_tensor * args[3] = { cur3, ids, weights };
|
| 671 |
-
ggml_tensor * out = ggml_custom_4d(ctx, GGML_TYPE_F32, cur3->ne[0], n_tok, 1, 1,
|
| 672 |
-
args, 3, moe_custom_op, GGML_N_TASKS_MAX, R);
|
| 673 |
-
ggml_set_name(out, "sdq_moe_out");
|
| 674 |
-
// 關鍵:把這個節點註冊進圖,ggml 才會為它配置獨立的 compute buffer。
|
| 675 |
-
// 少了這一行,layer>=1 的輸出會被其他張量覆蓋(layer 0 僥倖正確)。
|
| 676 |
-
ggml_build_forward_expand(gf, out);
|
| 677 |
-
return out;
|
| 678 |
-
}
|
| 679 |
-
|
| 680 |
-
} // namespace sdq
|
| 681 |
-
namespace sdq {
|
| 682 |
-
// 自我測試:對指定 (layer, expert) 用 pager 的資料算一次 MoE 輸出,
|
| 683 |
-
// 讓 Python 用純 NumPy 從同一段 GGUF 位元組重算並比對。
|
| 684 |
-
// 輸出:out[0..n_embd) = 輸出,out[n_embd..2*n_embd) = 輸入 activation
|
| 685 |
-
int sdq_moe_selftest(int il, int ie, const float * x, float * out_io,
|
| 686 |
-
int64_t n_embd, int64_t n_ff, int n_tok) {
|
| 687 |
-
pager & P = pager::instance();
|
| 688 |
-
const layer_desc * L = P.layer(il);
|
| 689 |
-
if (!L || ie < 0 || ie >= (int) L->experts.size()) {
|
| 690 |
-
fprintf(stderr, "[sdq-selftest] 層/expert 不存在:%d/%d\n", il, ie);
|
| 691 |
-
return 1;
|
| 692 |
-
}
|
| 693 |
-
const int slot = P.acquire(il, ie, true);
|
| 694 |
-
if (slot < 0) {
|
| 695 |
-
fprintf(stderr, "[sdq-selftest] 分頁失敗\n");
|
| 696 |
-
return 2;
|
| 697 |
-
}
|
| 698 |
-
const expert_desc & ed = L->experts[(size_t) ie];
|
| 699 |
-
if (ed.ne0 != n_embd || ed.ne1 != n_ff) {
|
| 700 |
-
fprintf(stderr, "[sdq-selftest] 維度不符:ne0=%lld ne1=%lld(預期 %lld/%lld)\n",
|
| 701 |
-
(long long) ed.ne0, (long long) ed.ne1, (long long) n_embd, (long long) n_ff);
|
| 702 |
-
return 3;
|
| 703 |
-
}
|
| 704 |
-
std::vector<std::pair<int32_t, int32_t>> toks;
|
| 705 |
-
std::vector<float> wts((size_t) n_tok, 1.0f);
|
| 706 |
-
for (int t = 0; t < n_tok; t++) {
|
| 707 |
-
toks.emplace_back(t, 0);
|
| 708 |
-
}
|
| 709 |
-
std::vector<float> out((size_t) n_embd * n_tok, 0.0f);
|
| 710 |
-
std::vector<uint8_t> scratch;
|
| 711 |
-
// SDQ_SELFTEST_DUMP: 把 arena 槽位的原始位元組存檔,驗證 pager 讀到的是正確的 expert
|
| 712 |
-
if (const char * dp = getenv("SDQ_SELFTEST_DUMP")) {
|
| 713 |
-
std::string path = std::string(dp) + "." + std::to_string(il) + "." + std::to_string(ie);
|
| 714 |
-
FILE * f = fopen(path.c_str(), "wb");
|
| 715 |
-
if (f) {
|
| 716 |
-
fwrite(P.slot_ptr(slot), 1, (size_t) ed.bytes, f);
|
| 717 |
-
fclose(f);
|
| 718 |
-
fprintf(stderr, "[sdq-selftest] slot dump -> %s (%lld B)\n", path.c_str(), (long long) ed.bytes);
|
| 719 |
-
}
|
| 720 |
-
}
|
| 721 |
-
expert_forward(ed, P.slot_ptr(slot), x, n_embd, toks, wts.data(), 4, 4, 1, 0.0f,
|
| 722 |
-
out.data(), &scratch);
|
| 723 |
-
for (int64_t t = 0; t < n_tok; t++) {
|
| 724 |
-
for (int64_t i = 0; i < n_embd; i++) {
|
| 725 |
-
out_io[t * n_embd + i] = out[(size_t) t * n_embd + i];
|
| 726 |
-
out_io[n_tok * n_embd + t * n_embd + i] = x[(size_t) t * n_embd + i];
|
| 727 |
-
}
|
| 728 |
-
}
|
| 729 |
-
P.unpin_all();
|
| 730 |
-
fprintf(stderr, "[sdq-selftest] L%d E%d n_tok=%d 計算完成(gate %s / up %s / down %s)\n",
|
| 731 |
-
il, ie, n_tok, ggml_type_name(ed.type[part::GATE]), ggml_type_name(ed.type[part::UP]),
|
| 732 |
-
ggml_type_name(ed.type[part::DOWN]));
|
| 733 |
-
return 0;
|
| 734 |
-
}
|
| 735 |
-
} // namespace sdq
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sdq-ling/sdq-ling/sdq_pager.cpp
DELETED
|
@@ -1,2292 +0,0 @@
|
|
| 1 |
-
// SDQ expert pager 實作:見 sdq_pager.h 的設計說明
|
| 2 |
-
#include "sdq_pager.h"
|
| 3 |
-
|
| 4 |
-
#include "llama-mmap.h"
|
| 5 |
-
|
| 6 |
-
#include <algorithm>
|
| 7 |
-
#include <thread>
|
| 8 |
-
#include <cinttypes>
|
| 9 |
-
#include <cmath>
|
| 10 |
-
#include <cstdio>
|
| 11 |
-
#include <cstdlib>
|
| 12 |
-
#include <cstring>
|
| 13 |
-
#include <fcntl.h>
|
| 14 |
-
#include <ctime>
|
| 15 |
-
#include <fstream>
|
| 16 |
-
#include <map>
|
| 17 |
-
#include <set>
|
| 18 |
-
#include <sstream>
|
| 19 |
-
#include <stdexcept>
|
| 20 |
-
#include <sys/mman.h>
|
| 21 |
-
#include <sys/stat.h>
|
| 22 |
-
#include <sys/types.h>
|
| 23 |
-
#include <unistd.h>
|
| 24 |
-
|
| 25 |
-
namespace sdq {
|
| 26 |
-
|
| 27 |
-
// ============================================================ 小工具
|
| 28 |
-
|
| 29 |
-
uint64_t now_us() {
|
| 30 |
-
struct timespec ts;
|
| 31 |
-
clock_gettime(CLOCK_MONOTONIC, &ts);
|
| 32 |
-
return (uint64_t) ts.tv_sec * 1000000ull + (uint64_t) ts.tv_nsec / 1000ull;
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
size_t process_rss_bytes() {
|
| 36 |
-
std::ifstream f("/proc/self/statm");
|
| 37 |
-
size_t total = 0, resident = 0;
|
| 38 |
-
if (f >> total >> resident) {
|
| 39 |
-
return resident * (size_t) sysconf(_SC_PAGESIZE);
|
| 40 |
-
}
|
| 41 |
-
return 0;
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
size_t physical_ram_bytes() {
|
| 45 |
-
std::ifstream f("/proc/meminfo");
|
| 46 |
-
std::string key;
|
| 47 |
-
size_t value = 0;
|
| 48 |
-
std::string unit;
|
| 49 |
-
while (f >> key >> value >> unit) {
|
| 50 |
-
if (key == "MemTotal:") {
|
| 51 |
-
return value * 1024;
|
| 52 |
-
}
|
| 53 |
-
}
|
| 54 |
-
return 0;
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
static size_t env_size(const char * name, size_t dflt) {
|
| 58 |
-
const char * v = getenv(name);
|
| 59 |
-
if (!v || !*v) {
|
| 60 |
-
return dflt;
|
| 61 |
-
}
|
| 62 |
-
return (size_t) strtoull(v, nullptr, 10);
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
static bool env_flag(const char * name, bool dflt) {
|
| 66 |
-
const char * v = getenv(name);
|
| 67 |
-
if (!v || !*v) {
|
| 68 |
-
return dflt;
|
| 69 |
-
}
|
| 70 |
-
return !(v[0] == '0' && v[1] == '\0');
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
// ============================================================ GGUF 檔頭解析
|
| 74 |
-
|
| 75 |
-
namespace {
|
| 76 |
-
|
| 77 |
-
struct gguf_reader {
|
| 78 |
-
const uint8_t * p = nullptr;
|
| 79 |
-
size_t size = 0;
|
| 80 |
-
size_t o = 0;
|
| 81 |
-
|
| 82 |
-
uint8_t u8() { return p[o++]; }
|
| 83 |
-
uint32_t u32() { uint32_t v; memcpy(&v, p + o, 4); o += 4; return v; }
|
| 84 |
-
uint64_t u64() { uint64_t v; memcpy(&v, p + o, 8); o += 8; return v; }
|
| 85 |
-
int64_t i64() { int64_t v; memcpy(&v, p + o, 8); o += 8; return v; }
|
| 86 |
-
float f32() { float v; memcpy(&v, p + o, 4); o += 4; return v; }
|
| 87 |
-
std::string str() {
|
| 88 |
-
uint64_t n = u64();
|
| 89 |
-
std::string s((const char *) p + o, (size_t) n);
|
| 90 |
-
o += n;
|
| 91 |
-
return s;
|
| 92 |
-
}
|
| 93 |
-
void skip(uint64_t n) { o += (size_t) n; }
|
| 94 |
-
uint16_t u16() { uint16_t v; memcpy(&v, p + o, 2); o += 2; return v; }
|
| 95 |
-
int16_t i16() { int16_t v; memcpy(&v, p + o, 2); o += 2; return v; }
|
| 96 |
-
double f64() { double v; memcpy(&v, p + o, 8); o += 8; return v; }
|
| 97 |
-
};
|
| 98 |
-
|
| 99 |
-
struct kv_value {
|
| 100 |
-
enum kind { NONE, INT, REAL, STR, ARR } k = NONE;
|
| 101 |
-
int64_t i = 0;
|
| 102 |
-
double f = 0;
|
| 103 |
-
std::string s;
|
| 104 |
-
std::vector<int64_t> arr;
|
| 105 |
-
};
|
| 106 |
-
|
| 107 |
-
struct tensor_info {
|
| 108 |
-
std::string name;
|
| 109 |
-
std::vector<int64_t> dims;
|
| 110 |
-
ggml_type type = GGML_TYPE_COUNT;
|
| 111 |
-
uint64_t offset = 0; // 在所屬 shard 檔案內、相對 data_start 的偏移
|
| 112 |
-
int shard = 0; // 屬於哪一個 shard(sharded GGUF)
|
| 113 |
-
};
|
| 114 |
-
|
| 115 |
-
size_t ggml_type_size_of(ggml_type t) {
|
| 116 |
-
return ggml_row_size(t, 1);
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
} // namespace
|
| 120 |
-
|
| 121 |
-
// ============================================================ pager
|
| 122 |
-
|
| 123 |
-
pager & pager::instance() {
|
| 124 |
-
static pager p;
|
| 125 |
-
return p;
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
// 依 GGUF split 命名規則把第一個 shard 路徑展開成全部 shard。
|
| 129 |
-
// 規則是 "<prefix>-00001-of-00004.gguf";llama.cpp 用 llama_split_prefix/llama_split_path
|
| 130 |
-
// 做同一件事,這裡獨立實作以免 pager 依賴 loader 的內部符號。
|
| 131 |
-
std::vector<std::string> pager::list_splits(const std::string & first_path, int n_split) {
|
| 132 |
-
std::vector<std::string> out;
|
| 133 |
-
if (n_split <= 1) {
|
| 134 |
-
out.push_back(first_path);
|
| 135 |
-
return out;
|
| 136 |
-
}
|
| 137 |
-
// 用 llama.cpp 自己的命名規則(%s-%05d-of-%05d.gguf,且 split_no 由 1 起算)。
|
| 138 |
-
// 自己刻字串很容易差一位數字 —— 實測就是這樣變成 "...XL--00001-of-..."。
|
| 139 |
-
std::vector<char> pre(llama_path_max(), 0);
|
| 140 |
-
const int pre_len = llama_split_prefix(pre.data(), pre.size(), first_path.c_str(), 0, n_split);
|
| 141 |
-
if (pre_len <= 0) {
|
| 142 |
-
throw std::runtime_error("無法解析 split 檔名:" + first_path);
|
| 143 |
-
}
|
| 144 |
-
std::vector<char> buf(llama_path_max(), 0);
|
| 145 |
-
for (int i = 0; i < n_split; i++) {
|
| 146 |
-
const int len = llama_split_path(buf.data(), buf.size(), pre.data(), i, n_split);
|
| 147 |
-
if (len <= 0) {
|
| 148 |
-
throw std::runtime_error("無法產生 split 路徑:" + first_path);
|
| 149 |
-
}
|
| 150 |
-
out.emplace_back(buf.data(), (size_t) len);
|
| 151 |
-
}
|
| 152 |
-
return out;
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
void pager::load_gguf_header(const std::string & path) {
|
| 156 |
-
const int fd = open(path.c_str(), O_RDONLY);
|
| 157 |
-
if (fd < 0) {
|
| 158 |
-
throw std::runtime_error("無法開啟 GGUF:" + path);
|
| 159 |
-
}
|
| 160 |
-
|
| 161 |
-
struct stat st;
|
| 162 |
-
fstat(fd, &st);
|
| 163 |
-
|
| 164 |
-
// 只讀檔頭:metadata + tensor 資訊大約 11 MB(shard 1 還含整份 tokenizer,
|
| 165 |
-
// 這裡的 64 MiB 上限對它足夠)
|
| 166 |
-
const size_t want = (size_t) std::min<int64_t>(st.st_size, 64ull << 20);
|
| 167 |
-
std::vector<uint8_t> buf(want);
|
| 168 |
-
ssize_t got = 0;
|
| 169 |
-
while (got < (ssize_t) want) {
|
| 170 |
-
ssize_t n = pread(fd, buf.data() + got, want - got, got);
|
| 171 |
-
if (n <= 0) {
|
| 172 |
-
break;
|
| 173 |
-
}
|
| 174 |
-
got += n;
|
| 175 |
-
}
|
| 176 |
-
buf.resize((size_t) std::max<ssize_t>(got, 0));
|
| 177 |
-
|
| 178 |
-
gguf_reader r{buf.data(), buf.size(), 0};
|
| 179 |
-
if (memcmp(buf.data(), "GGUF", 4) != 0) {
|
| 180 |
-
close(fd);
|
| 181 |
-
throw std::runtime_error("不是 GGUF 檔:" + path);
|
| 182 |
-
}
|
| 183 |
-
r.o = 4;
|
| 184 |
-
const uint32_t version = r.u32();
|
| 185 |
-
if (version < 2 || version > 3) {
|
| 186 |
-
close(fd);
|
| 187 |
-
throw std::runtime_error("不支援的 GGUF 版本");
|
| 188 |
-
}
|
| 189 |
-
const uint64_t n_tensors = r.u64();
|
| 190 |
-
const uint64_t n_kv = r.u64();
|
| 191 |
-
|
| 192 |
-
std::map<std::string, kv_value> kv;
|
| 193 |
-
for (uint64_t i = 0; i < n_kv; i++) {
|
| 194 |
-
std::string key = r.str();
|
| 195 |
-
const uint32_t t = r.u32();
|
| 196 |
-
kv_value v;
|
| 197 |
-
switch (t) {
|
| 198 |
-
// GGUF 型別寬度:U8/I8/BOOL=1、U16/I16=2、U32/I32/F32=4、U64/I64/F64=8。
|
| 199 |
-
// 舊版把 case 0/1/2/3 都讀成 u32,遇到 tokenizer 或 U16 metadata 就整個
|
| 200 |
-
// header 錯位(症狀是 std::length_error: basic_string::_M_create)。
|
| 201 |
-
case 0: v.k = kv_value::INT; v.i = (int64_t) r.u8(); v.f = (double) v.i; break;
|
| 202 |
-
case 1: v.k = kv_value::INT; v.i = (int64_t) (int8_t) r.u8(); v.f = (double) v.i; break;
|
| 203 |
-
case 2: v.k = kv_value::INT; v.i = (int64_t) r.u16(); v.f = (double) v.i; break;
|
| 204 |
-
case 3: v.k = kv_value::INT; v.i = (int64_t) r.i16(); v.f = (double) v.i; break;
|
| 205 |
-
case 4: v.k = kv_value::INT; v.i = (int64_t) (uint32_t) r.u32(); v.f = (double) v.i; break;
|
| 206 |
-
case 5: v.k = kv_value::INT; v.i = (int64_t) (int32_t) r.u32(); v.f = (double) v.i; break;
|
| 207 |
-
case 6: v.k = kv_value::REAL; v.f = r.f32(); v.i = (int64_t) v.f; break;
|
| 208 |
-
case 7: v.k = kv_value::INT; v.i = (int64_t) r.u8(); v.f = (double) v.i; break; // BOOL = 1 byte
|
| 209 |
-
case 8: v.k = kv_value::STR; v.s = r.str(); break;
|
| 210 |
-
case 9: {
|
| 211 |
-
v.k = kv_value::ARR;
|
| 212 |
-
const uint32_t et = r.u32();
|
| 213 |
-
const uint64_t n = r.u64();
|
| 214 |
-
for (uint64_t j = 0; j < n; j++) {
|
| 215 |
-
switch (et) {
|
| 216 |
-
// 元素寬度必須跟 kv 一致(U8/I8/BOOL=1、U16/I16=2 …)。
|
| 217 |
-
// F64 一定要消費 8 bytes,舊版沒讀會讓 header 往後全部錯位。
|
| 218 |
-
case 0: v.arr.push_back(r.u8()); break;
|
| 219 |
-
case 1: v.arr.push_back((int8_t) r.u8()); break;
|
| 220 |
-
case 2: v.arr.push_back(r.u16()); break;
|
| 221 |
-
case 3: v.arr.push_back(r.i16()); break;
|
| 222 |
-
case 4: v.arr.push_back((uint32_t) r.u32()); break;
|
| 223 |
-
case 5: v.arr.push_back((int32_t) r.u32()); break;
|
| 224 |
-
case 6: v.arr.push_back((int64_t) r.f32()); break;
|
| 225 |
-
case 7: v.arr.push_back(r.u8()); break;
|
| 226 |
-
case 10: v.arr.push_back((int64_t) r.u64()); break;
|
| 227 |
-
case 11: v.arr.push_back(r.i64()); break;
|
| 228 |
-
case 12: r.f64(); v.arr.push_back(0); break;
|
| 229 |
-
case 8: r.skip(r.u64()); v.arr.push_back(0); break;
|
| 230 |
-
default:
|
| 231 |
-
close(fd);
|
| 232 |
-
throw std::runtime_error("GGUF 陣列元素型別不支援: " + std::to_string(et));
|
| 233 |
-
}
|
| 234 |
-
}
|
| 235 |
-
} break;
|
| 236 |
-
case 10: v.k = kv_value::INT; v.i = (int64_t) r.u64(); v.f = (double) v.i; break;
|
| 237 |
-
case 11: v.k = kv_value::INT; v.i = r.i64(); v.f = (double) v.i; break;
|
| 238 |
-
case 12: v.k = kv_value::REAL; { double d; memcpy(&d, buf.data() + r.o, 8); r.o += 8; v.f = d; v.i = (int64_t) d; } break;
|
| 239 |
-
default: close(fd); throw std::runtime_error("GGUF kv 型別不支援: " + std::to_string(t));
|
| 240 |
-
}
|
| 241 |
-
kv[key] = v;
|
| 242 |
-
}
|
| 243 |
-
|
| 244 |
-
std::vector<tensor_info> tensors;
|
| 245 |
-
tensors.reserve(n_tensors);
|
| 246 |
-
for (uint64_t i = 0; i < n_tensors; i++) {
|
| 247 |
-
tensor_info ti;
|
| 248 |
-
ti.name = r.str();
|
| 249 |
-
const uint32_t nd = r.u32();
|
| 250 |
-
for (uint32_t d = 0; d < nd; d++) {
|
| 251 |
-
ti.dims.push_back((int64_t) r.u64());
|
| 252 |
-
}
|
| 253 |
-
ti.type = (ggml_type) r.u32();
|
| 254 |
-
ti.offset = r.u64();
|
| 255 |
-
tensors.push_back(std::move(ti));
|
| 256 |
-
}
|
| 257 |
-
const uint64_t align = kv.count("general.alignment") ? (uint64_t) kv["general.alignment"].i : 32;
|
| 258 |
-
const uint64_t data_start = (r.o + align - 1) / align * align;
|
| 259 |
-
|
| 260 |
-
close(fd);
|
| 261 |
-
|
| 262 |
-
// ---- sharded GGUF:shard 1 只帶 metadata(n_tensors=0),真正的權重分散在
|
| 263 |
-
// 其他 shard,而且每個 shard 有自己的 data_start。舊版只讀第一個檔,
|
| 264 |
-
// 於是 page map 會是空的(症狀:pager 起來但沒有任何 expert 可分頁)。
|
| 265 |
-
// 這裡把每個 shard 的 header 都讀一遍,張量名稱為全域鍵,offset 記成 (shard, off)。
|
| 266 |
-
{
|
| 267 |
-
const int64_t n_split = kv.count("split.count") ? kv["split.count"].i : 1;
|
| 268 |
-
shard_paths_ = list_splits(path, (int) n_split);
|
| 269 |
-
shard_data_start_.assign(shard_paths_.size(), 0);
|
| 270 |
-
shard_data_start_[0] = data_start;
|
| 271 |
-
|
| 272 |
-
if (n_split > 1) {
|
| 273 |
-
std::vector<tensor_info> merged;
|
| 274 |
-
merged.reserve((size_t) n_tensors + 1024);
|
| 275 |
-
for (size_t si = 1; si < shard_paths_.size(); si++) {
|
| 276 |
-
const int sfd = open(shard_paths_[si].c_str(), O_RDONLY);
|
| 277 |
-
if (sfd < 0) {
|
| 278 |
-
throw std::runtime_error("無法開啟 GGUF shard:" + shard_paths_[si]);
|
| 279 |
-
}
|
| 280 |
-
struct stat sst;
|
| 281 |
-
fstat(sfd, &sst);
|
| 282 |
-
// 後續 shard 的 header 很小(沒有 tokenizer),讀 16 MiB 綽綽有餘
|
| 283 |
-
const size_t swant = (size_t) std::min<int64_t>(sst.st_size, 16ull << 20);
|
| 284 |
-
std::vector<uint8_t> sbuf(swant);
|
| 285 |
-
ssize_t sgot = 0;
|
| 286 |
-
while (sgot < (ssize_t) swant) {
|
| 287 |
-
const ssize_t n = pread(sfd, sbuf.data() + sgot, swant - sgot, sgot);
|
| 288 |
-
if (n <= 0) {
|
| 289 |
-
break;
|
| 290 |
-
}
|
| 291 |
-
sgot += n;
|
| 292 |
-
}
|
| 293 |
-
sbuf.resize((size_t) std::max<ssize_t>(sgot, 0));
|
| 294 |
-
|
| 295 |
-
gguf_reader sr{sbuf.data(), sbuf.size(), 0};
|
| 296 |
-
if (sbuf.size() < 16 || memcmp(sbuf.data(), "GGUF", 4) != 0) {
|
| 297 |
-
close(sfd);
|
| 298 |
-
throw std::runtime_error("shard 不是 GGUF:" + shard_paths_[si]);
|
| 299 |
-
}
|
| 300 |
-
sr.o = 4;
|
| 301 |
-
sr.u32(); // version
|
| 302 |
-
const uint64_t s_ntensors = sr.u64();
|
| 303 |
-
const uint64_t s_nkv = sr.u64();
|
| 304 |
-
for (uint64_t i = 0; i < s_nkv; i++) {
|
| 305 |
-
sr.str(); // key
|
| 306 |
-
const uint32_t t = sr.u32();
|
| 307 |
-
switch (t) {
|
| 308 |
-
case 0: case 1: case 7: sr.skip(1); break;
|
| 309 |
-
case 2: case 3: sr.skip(2); break;
|
| 310 |
-
case 4: case 5: case 6: sr.skip(4); break;
|
| 311 |
-
case 10: case 11: case 12: sr.skip(8); break;
|
| 312 |
-
case 8: sr.skip(sr.u64()); break;
|
| 313 |
-
case 9: {
|
| 314 |
-
const uint32_t et = sr.u32();
|
| 315 |
-
const uint64_t n = sr.u64();
|
| 316 |
-
if (et == 8) {
|
| 317 |
-
for (uint64_t j = 0; j < n; j++) { sr.skip(sr.u64()); }
|
| 318 |
-
} else {
|
| 319 |
-
size_t esz = 0;
|
| 320 |
-
switch (et) {
|
| 321 |
-
case 0: case 1: case 7: esz = 1; break;
|
| 322 |
-
case 2: case 3: esz = 2; break;
|
| 323 |
-
case 4: case 5: case 6: esz = 4; break;
|
| 324 |
-
case 10: case 11: case 12: esz = 8; break;
|
| 325 |
-
default: esz = 0; break;
|
| 326 |
-
}
|
| 327 |
-
sr.skip((uint64_t) n * esz);
|
| 328 |
-
}
|
| 329 |
-
} break;
|
| 330 |
-
default:
|
| 331 |
-
close(sfd);
|
| 332 |
-
throw std::runtime_error("shard kv 型別不支援: " + std::to_string(t));
|
| 333 |
-
}
|
| 334 |
-
}
|
| 335 |
-
for (uint64_t i = 0; i < s_ntensors; i++) {
|
| 336 |
-
tensor_info ti;
|
| 337 |
-
ti.name = sr.str();
|
| 338 |
-
const uint32_t nd = sr.u32();
|
| 339 |
-
for (uint32_t d = 0; d < nd; d++) {
|
| 340 |
-
ti.dims.push_back((int64_t) sr.u64());
|
| 341 |
-
}
|
| 342 |
-
ti.type = (ggml_type) sr.u32();
|
| 343 |
-
ti.offset = sr.u64();
|
| 344 |
-
ti.shard = (int) si;
|
| 345 |
-
merged.push_back(std::move(ti));
|
| 346 |
-
}
|
| 347 |
-
shard_data_start_[si] = (sr.o + align - 1) / align * align;
|
| 348 |
-
close(sfd);
|
| 349 |
-
}
|
| 350 |
-
for (auto & ti : tensors) {
|
| 351 |
-
ti.shard = 0;
|
| 352 |
-
merged.push_back(std::move(ti));
|
| 353 |
-
}
|
| 354 |
-
tensors.swap(merged);
|
| 355 |
-
fprintf(stderr, "[sdq] sharded GGUF:%d 個 shard,合計 %zu 個張量(shard1 為 metadata-only)\n",
|
| 356 |
-
(int) shard_paths_.size(), tensors.size());
|
| 357 |
-
}
|
| 358 |
-
}
|
| 359 |
-
|
| 360 |
-
// ---- hparams
|
| 361 |
-
auto gi = [&](const std::string & k, int64_t dflt) -> int64_t {
|
| 362 |
-
auto it = kv.find(k);
|
| 363 |
-
return it == kv.end() ? dflt : it->second.i;
|
| 364 |
-
};
|
| 365 |
-
info_.arch = kv.count("general.architecture") ? kv["general.architecture"].s : "";
|
| 366 |
-
info_.name = kv.count("general.name") ? kv["general.name"].s : "";
|
| 367 |
-
const std::string pfx = info_.arch + ".";
|
| 368 |
-
info_.n_layer = gi(pfx + "block_count", 0);
|
| 369 |
-
info_.n_expert = gi(pfx + "expert_count", 0);
|
| 370 |
-
info_.n_expert_used= gi(pfx + "expert_used_count", 0);
|
| 371 |
-
info_.n_embd = gi(pfx + "embedding_length", 0);
|
| 372 |
-
info_.n_ff_exp = gi(pfx + "expert_feed_forward_length", 0);
|
| 373 |
-
info_.n_ff_shexp = gi(pfx + "expert_shared_feed_forward_length", 0);
|
| 374 |
-
info_.n_head = gi(pfx + "attention.head_count", 0);
|
| 375 |
-
info_.n_head_kv = gi(pfx + "attention.head_count_kv", 0);
|
| 376 |
-
info_.n_embd_head_k= gi(pfx + "attention.key_length", 0);
|
| 377 |
-
info_.n_embd_head_v= gi(pfx + "attention.value_length", info_.n_embd_head_k);
|
| 378 |
-
info_.context_length = gi(pfx + "context_length", 0);
|
| 379 |
-
info_.full_attention_interval = gi(pfx + "full_attention_interval", 4);
|
| 380 |
-
// bailingmoe3(Ling-3.0-flash):前 n_layer_dense_lead 層是 dense FFN,
|
| 381 |
-
// 它們沒有 ffn_*_exps 張量,必須讓 page map 保持「空」——
|
| 382 |
-
// sdq_pager_active_for_layer() 看到 layer() == nullptr 就會走原生路徑。
|
| 383 |
-
info_.n_layer_dense_lead = gi(pfx + "leading_dense_block_count", 0);
|
| 384 |
-
info_.n_expert_groups = gi(pfx + "expert_group_count", 0);
|
| 385 |
-
info_.n_expert_group_used = gi(pfx + "expert_group_used_count", 0);
|
| 386 |
-
|
| 387 |
-
if (info_.n_expert <= 0 || info_.n_layer <= 0) {
|
| 388 |
-
throw std::runtime_error("這不是 MoE 模型(expert_count=" + std::to_string(info_.n_expert) + ")");
|
| 389 |
-
}
|
| 390 |
-
|
| 391 |
-
// ---- 頁面表:每個 (layer, expert) 的三段
|
| 392 |
-
layers_.assign((size_t) info_.n_layer, {});
|
| 393 |
-
std::map<std::string, const tensor_info *> by_name;
|
| 394 |
-
for (const auto & ti : tensors) {
|
| 395 |
-
by_name[ti.name] = &ti;
|
| 396 |
-
}
|
| 397 |
-
|
| 398 |
-
size_t max_expert_bytes = 0;
|
| 399 |
-
int n_moe_layers = 0;
|
| 400 |
-
for (int64_t il = 0; il < info_.n_layer; il++) {
|
| 401 |
-
const std::string b = "blk." + std::to_string(il) + ".";
|
| 402 |
-
layer_desc & L = layers_[(size_t) il];
|
| 403 |
-
|
| 404 |
-
auto get = [&](const std::string & suffix) -> const tensor_info * {
|
| 405 |
-
auto it = by_name.find(b + suffix);
|
| 406 |
-
return it == by_name.end() ? nullptr : it->second;
|
| 407 |
-
};
|
| 408 |
-
|
| 409 |
-
const tensor_info * t_gate = get("ffn_gate_exps.weight");
|
| 410 |
-
const tensor_info * t_up = get("ffn_up_exps.weight");
|
| 411 |
-
const tensor_info * t_gu = get("ffn_gate_up_exps.weight");
|
| 412 |
-
const tensor_info * t_down = get("ffn_down_exps.weight");
|
| 413 |
-
|
| 414 |
-
if (!t_down || (t_gate && !t_up && !t_gu)) {
|
| 415 |
-
// 這層不是 MoE(bailingmoe3 的 leading dense layers 就是這樣)。
|
| 416 |
-
// 保持 experts 為空 → layer() 回 nullptr → 這層走原生 mul_mat_id。
|
| 417 |
-
continue;
|
| 418 |
-
}
|
| 419 |
-
L.experts.assign((size_t) info_.n_expert, {});
|
| 420 |
-
n_moe_layers++;
|
| 421 |
-
|
| 422 |
-
// sharded GGUF:每段的絕對偏移要用「該張量自己 shard 的 data_start」,
|
| 423 |
-
// 不能用第一個 shard 的(兩者差了近 10 MB,直接用會讀到別的權重)。
|
| 424 |
-
auto abs_off = [&](const tensor_info * t, uint64_t rel) -> uint64_t {
|
| 425 |
-
const size_t s = (size_t) t->shard;
|
| 426 |
-
const uint64_t base = (s < shard_data_start_.size()) ? shard_data_start_[s] : 0;
|
| 427 |
-
return base + rel;
|
| 428 |
-
};
|
| 429 |
-
|
| 430 |
-
for (int64_t e = 0; e < info_.n_expert; e++) {
|
| 431 |
-
expert_desc & ed = L.experts[(size_t) e];
|
| 432 |
-
if (t_gate || t_gu) {
|
| 433 |
-
const tensor_info * t = t_gate ? t_gate : t_gu;
|
| 434 |
-
const uint64_t nb = (uint64_t) ggml_row_size(t->type, t->dims[0]) * (uint64_t) t->dims[1];
|
| 435 |
-
ed.seg[part::GATE].off = abs_off(t, t->offset + (uint64_t) e * nb);
|
| 436 |
-
ed.seg[part::GATE].size = nb;
|
| 437 |
-
ed.seg[part::GATE].shard = t->shard;
|
| 438 |
-
ed.ne0 = t->dims[0];
|
| 439 |
-
ed.ne1 = t->dims[1];
|
| 440 |
-
ed.type[part::GATE] = t->type;
|
| 441 |
-
}
|
| 442 |
-
if (t_up) {
|
| 443 |
-
const uint64_t nb = (uint64_t) ggml_row_size(t_up->type, t_up->dims[0]) * (uint64_t) t_up->dims[1];
|
| 444 |
-
ed.seg[part::UP].off = abs_off(t_up, t_up->offset + (uint64_t) e * nb);
|
| 445 |
-
ed.seg[part::UP].size = nb;
|
| 446 |
-
ed.seg[part::UP].shard = t_up->shard;
|
| 447 |
-
ed.type[part::UP] = t_up->type; // gate/up 都是 Q4_K,型別一定要各自填
|
| 448 |
-
if (!t_gate) {
|
| 449 |
-
ed.ne0 = t_up->dims[0];
|
| 450 |
-
ed.ne1 = t_up->dims[1];
|
| 451 |
-
ed.type[part::GATE] = t_up->type;
|
| 452 |
-
}
|
| 453 |
-
}
|
| 454 |
-
{
|
| 455 |
-
const uint64_t nb = (uint64_t) ggml_row_size(t_down->type, t_down->dims[0]) * (uint64_t) t_down->dims[1];
|
| 456 |
-
ed.seg[part::DOWN].off = abs_off(t_down, t_down->offset + (uint64_t) e * nb);
|
| 457 |
-
ed.seg[part::DOWN].size = nb;
|
| 458 |
-
ed.seg[part::DOWN].shard = t_down->shard;
|
| 459 |
-
ed.type[part::DOWN] = t_down->type;
|
| 460 |
-
ed.down_ne0 = t_down->dims[0];
|
| 461 |
-
ed.down_ne1 = t_down->dims[1];
|
| 462 |
-
}
|
| 463 |
-
ed.fused_gate_up = (t_gate == nullptr && t_gu != nullptr);
|
| 464 |
-
ed.bytes = 0;
|
| 465 |
-
int64_t acc = 0;
|
| 466 |
-
for (int p = 0; p < 3; p++) {
|
| 467 |
-
if (ed.seg[p].size == 0) {
|
| 468 |
-
continue;
|
| 469 |
-
}
|
| 470 |
-
acc = (acc + 63) & ~(int64_t) 63; // 每段 64B 對齊
|
| 471 |
-
ed.slot_off[p] = acc;
|
| 472 |
-
acc += (int64_t) ed.seg[p].size;
|
| 473 |
-
}
|
| 474 |
-
ed.bytes = acc;
|
| 475 |
-
max_expert_bytes = std::max(max_expert_bytes, (size_t) ed.bytes);
|
| 476 |
-
}
|
| 477 |
-
L.bytes_per_expert = L.experts.empty() ? 0 : L.experts[0].bytes;
|
| 478 |
-
}
|
| 479 |
-
|
| 480 |
-
slot_bytes_ = (max_expert_bytes + 4095) & ~(size_t) 4095;
|
| 481 |
-
if (slot_bytes_ < (256ull << 10)) {
|
| 482 |
-
slot_bytes_ = 256ull << 10;
|
| 483 |
-
}
|
| 484 |
-
|
| 485 |
-
// ---- 大小兩種槽位 ----------------------------------------------------
|
| 486 |
-
// 這個模型有 37 層的 expert 是 1900544 B(Q4_K + Q5_K),但層 34/38/39 是
|
| 487 |
-
// 2039808 B(down 換成 Q6_K)。若所有槽位都照最大的配,arena 有 6.8% 被浪費
|
| 488 |
-
// ——而 arena 就是 8 GB 預算裡最寶貴的資源(實測 +5% 槽位 ≈ +9% tok/s)。
|
| 489 |
-
//
|
| 490 |
-
// 解法:槽位分成兩區。「小槽位」給 37 層用,「大槽位」只給那 3 層用,
|
| 491 |
-
// 大槽位的數量按「那 3 層佔請求的比例」分配(3/40 = 7.5%)。
|
| 492 |
-
// 選「大多數層」當小的:用眾數而不是最大值,這樣即使某個模型只有一種大小,
|
| 493 |
-
// 也不會誤判。
|
| 494 |
-
{
|
| 495 |
-
std::map<size_t, int> hist;
|
| 496 |
-
for (const auto & L : layers_) {
|
| 497 |
-
if (L.experts.empty()) {
|
| 498 |
-
continue; // dense layer:沒有 expert,不參與槽位尺寸統計
|
| 499 |
-
}
|
| 500 |
-
hist[(size_t) L.bytes_per_expert]++;
|
| 501 |
-
}
|
| 502 |
-
size_t small = 0;
|
| 503 |
-
int best_n = -1;
|
| 504 |
-
for (const auto & kv : hist) {
|
| 505 |
-
if (kv.second > best_n) {
|
| 506 |
-
best_n = kv.second;
|
| 507 |
-
small = kv.first;
|
| 508 |
-
}
|
| 509 |
-
}
|
| 510 |
-
size_t big = 0;
|
| 511 |
-
for (const auto & L : layers_) {
|
| 512 |
-
if (!L.experts.empty() && (size_t) L.bytes_per_expert > small) {
|
| 513 |
-
big = std::max(big, (size_t) L.bytes_per_expert);
|
| 514 |
-
}
|
| 515 |
-
}
|
| 516 |
-
small_bytes_ = (small + 4095) & ~(size_t) 4095;
|
| 517 |
-
big_bytes_ = big ? ((big + 4095) & ~(size_t) 4095) : 0;
|
| 518 |
-
if (big_bytes_ && big_bytes_ <= small_bytes_) {
|
| 519 |
-
big_bytes_ = 0; // 其實一樣大,別分兩區
|
| 520 |
-
}
|
| 521 |
-
slot_bytes_ = big_bytes_ ? big_bytes_ : small_bytes_;
|
| 522 |
-
int n_big_layers = 0;
|
| 523 |
-
for (auto & L : layers_) {
|
| 524 |
-
L.needs_big_slot = big_bytes_ && !L.experts.empty() &&
|
| 525 |
-
(size_t) L.bytes_per_expert > small_bytes_;
|
| 526 |
-
if (L.needs_big_slot) {
|
| 527 |
-
n_big_layers++;
|
| 528 |
-
}
|
| 529 |
-
}
|
| 530 |
-
// 統計給 README / 實驗報告用:expert 權重在檔案裡到底佔多少
|
| 531 |
-
uint64_t expert_bytes = 0;
|
| 532 |
-
for (const auto & L : layers_) {
|
| 533 |
-
expert_bytes += (uint64_t) L.bytes_per_expert * (uint64_t) L.experts.size();
|
| 534 |
-
}
|
| 535 |
-
n_moe_layers_ = n_moe_layers;
|
| 536 |
-
expert_bytes_total_ = expert_bytes;
|
| 537 |
-
fprintf(stderr,
|
| 538 |
-
"[sdq] MoE 層 %d / 共 %lld 層(dense lead %lld)"
|
| 539 |
-
":expert 權重合計 %.2f GiB\n",
|
| 540 |
-
n_moe_layers, (long long) info_.n_layer,
|
| 541 |
-
(long long) info_.n_layer_dense_lead,
|
| 542 |
-
expert_bytes / 1073741824.0);
|
| 543 |
-
fprintf(stderr,
|
| 544 |
-
"[sdq] 槽位大小:小的 %zu B(%zu 層)/大的 %zu B(%d 層)\n",
|
| 545 |
-
small_bytes_, n_moe_layers - n_big_layers, big_bytes_, n_big_layers);
|
| 546 |
-
}
|
| 547 |
-
}
|
| 548 |
-
|
| 549 |
-
bool pager::need_big_slot(int il, int ie) const {
|
| 550 |
-
if (!big_bytes_) {
|
| 551 |
-
return false;
|
| 552 |
-
}
|
| 553 |
-
const layer_desc * L = layer(il);
|
| 554 |
-
if (!L) {
|
| 555 |
-
return false;
|
| 556 |
-
}
|
| 557 |
-
if (L->needs_big_slot) {
|
| 558 |
-
return true;
|
| 559 |
-
}
|
| 560 |
-
return ie >= 0 && ie < (int) L->experts.size() &&
|
| 561 |
-
(size_t) L->experts[(size_t) ie].bytes > small_bytes_;
|
| 562 |
-
}
|
| 563 |
-
|
| 564 |
-
void pager::alloc_arena() {
|
| 565 |
-
const size_t slot = cfg_.slot_bytes ? cfg_.slot_bytes : slot_bytes_;
|
| 566 |
-
slot_bytes_ = slot;
|
| 567 |
-
|
| 568 |
-
// arena 大小決定:RAM 是「總預算」,不是「還有多少可用」
|
| 569 |
-
// 總預算 = 非專家權重 + ggml 模型緩衝 + KV cache + 計算緩衝 + 詞表 + arena
|
| 570 |
-
// 所以有指定 ram_budget 時,用「目前 RSS(不含 arena)」當作已用額度扣掉。
|
| 571 |
-
//
|
| 572 |
-
// 重要:pager 在 llama.cpp 載入權重**之前**就做 sizing,此時 rss_now 只有
|
| 573 |
-
// 幾十 MB(實測 0.09 GiB),所以後續所有匿名配置都不在 rss_now 裡,
|
| 574 |
-
// 必須由 reserve 手動涵蓋。這些事實上是三塊:
|
| 575 |
-
//
|
| 576 |
-
// 1. ggml 的模型緩衝 —— 主機有 AMX 時(-march=native 會定義 __AMX_INT8__
|
| 577 |
-
// 使 llama.cpp 啟用 AMX repack buffer)Ling-3.0-flash Q4_K_M 要
|
| 578 |
-
// 1876.34 MiB;關掉 AMX(GGML_NATIVE=OFF)則不需要這塊。
|
| 579 |
-
// 2. CPU compute buffer:83.5 MiB @ubatch=128,ubatch 越大越多。
|
| 580 |
-
// 3. KV/SSM state + 執行緒堆疊:ctx 越大越多。
|
| 581 |
-
//
|
| 582 |
-
// ��的預設值 900 MB 只涵蓋 (2)+(3),漏掉 (1),在 AMX 主機上會讓實際
|
| 583 |
-
// 匿名用量超出預算約 1.9 GiB —— 嚴格 RLIMIT_DATA 下直接 OOM。
|
| 584 |
-
// 實測(本機,ctx=2048、ubatch=128、AMX build):
|
| 585 |
-
// reserve 1024 MB → 預算 8 GiB 實測匿名峰值 10.96 GiB(超出,OOM)
|
| 586 |
-
// reserve 3600 MB → 預算 8 GiB 通過,arena 4.40 GiB
|
| 587 |
-
// reserve 2700 MB → 預算 4 GiB 通過,arena 1.27 GiB(無 AMX build)
|
| 588 |
-
// 寧可少一些槽位,也不要在目標機器的 8 GiB 上 OOM —— 這是本專案的硬約束。
|
| 589 |
-
size_t reserve = (size_t) env_size("SDQ_RESERVE_MB", 3600) << 20;
|
| 590 |
-
const size_t rss_now = process_rss_bytes();
|
| 591 |
-
|
| 592 |
-
size_t arena;
|
| 593 |
-
if (cfg_.ram_budget_bytes) {
|
| 594 |
-
const size_t used = rss_now; // 這時候 arena 還沒配置
|
| 595 |
-
size_t avail = cfg_.ram_budget_bytes > used + reserve
|
| 596 |
-
? cfg_.ram_budget_bytes - used - reserve
|
| 597 |
-
: 0;
|
| 598 |
-
arena = avail;
|
| 599 |
-
if (cfg_.arena_bytes) {
|
| 600 |
-
arena = std::min(arena, cfg_.arena_bytes);
|
| 601 |
-
}
|
| 602 |
-
fprintf(stderr, "[sdq] RAM 預算 %.2f GiB:已用(RSS) %.2f GiB、預留 %.2f GiB → arena %.2f GiB\n",
|
| 603 |
-
cfg_.ram_budget_bytes / 1073741824.0, used / 1073741824.0,
|
| 604 |
-
reserve / 1073741824.0, arena / 1073741824.0);
|
| 605 |
-
} else {
|
| 606 |
-
const size_t phys = physical_ram_bytes();
|
| 607 |
-
const size_t budget = (size_t) (phys * 0.70);
|
| 608 |
-
arena = cfg_.arena_bytes ? cfg_.arena_bytes : std::min(budget, (size_t) (4ull << 30));
|
| 609 |
-
}
|
| 610 |
-
if (arena < slot_bytes_ * 16) {
|
| 611 |
-
throw std::runtime_error("expert arena 太小");
|
| 612 |
-
}
|
| 613 |
-
// 切成兩區:大槽位只分給「需要大槽位的層」,比例 = 那些層佔全部層的比例。
|
| 614 |
-
const bool force_uniform = env_flag("SDQ_FORCE_UNIFORM_SLOT", false);
|
| 615 |
-
if (big_bytes_ && small_bytes_ && !layers_.empty() && !force_uniform) {
|
| 616 |
-
int n_big_layers = 0;
|
| 617 |
-
for (const auto & L : layers_) {
|
| 618 |
-
if (L.needs_big_slot) {
|
| 619 |
-
n_big_layers++;
|
| 620 |
-
}
|
| 621 |
-
}
|
| 622 |
-
const double denom = (double) (n_moe_layers_ ? n_moe_layers_ : layers_.size());
|
| 623 |
-
const double f = (double) n_big_layers / denom;
|
| 624 |
-
const double avg = f * (double) big_bytes_ + (1.0 - f) * (double) small_bytes_;
|
| 625 |
-
size_t n_total = (size_t)((double) arena / avg);
|
| 626 |
-
size_t nb = (size_t)(n_total * f);
|
| 627 |
-
// 大槽位的下限不是「有點就好」,而是**工作集的下限**:
|
| 628 |
-
// 一個 token 在每個大槽位層要 8 個大 expert,這個模型有 3 個大槽位層
|
| 629 |
-
// → 至少要 3 × 8 = 24 個大槽位,而且還要留給預取的空間。
|
| 630 |
-
// 只給 16 個時量到過「找不到能放 il=34 ie=114 的槽位」而中止
|
| 631 |
-
// (這是在 verify 新增的預取壓力測試 [2b] 抓到的)。
|
| 632 |
-
const size_t big_need = (size_t) (n_big_layers * info_.n_expert_used);
|
| 633 |
-
if (nb < big_need) {
|
| 634 |
-
nb = big_need;
|
| 635 |
-
}
|
| 636 |
-
if (nb > n_total / 4) {
|
| 637 |
-
nb = n_total / 4;
|
| 638 |
-
}
|
| 639 |
-
// 連工作集下限都放不進去時,寧可讓小槽位少一點(n_total 保持不變),
|
| 640 |
-
// 否則會算出一個「必定會中止」的配置。
|
| 641 |
-
n_big_ = nb;
|
| 642 |
-
n_small_ = n_total > nb ? n_total - nb : 0;
|
| 643 |
-
arena = n_small_ * small_bytes_ + n_big_ * big_bytes_;
|
| 644 |
-
} else {
|
| 645 |
-
// 單一尺寸:全部槽位都是「大的那一種」(slot_bytes_)。
|
| 646 |
-
// 這是 SDQ_FORCE_UNIFORM_SLOT=1 與「模型只有一種 expert 大小」時的路徑。
|
| 647 |
-
//
|
| 648 |
-
// 這裡必須記成 n_small_=0 / n_big_=全部,不能記成「n_small_=全部、單位用大的」:
|
| 649 |
-
// slot_ptr()/slot_size_of() 是用「索引 < n_small_ → 小槽位」來分支的,
|
| 650 |
-
// 把它記成全部都是小槽位的話,大 expert 會拿到小槽位的 stride
|
| 651 |
-
// (寫出槽位邊界、算錯),而且大 expert 永遠配不到槽位 → 推論卡死。
|
| 652 |
-
// (第一版就是這樣,實驗跑到一半 hang 才找出來。)
|
| 653 |
-
const size_t unit = slot_bytes_ ? slot_bytes_ : small_bytes_;
|
| 654 |
-
n_small_ = 0;
|
| 655 |
-
n_big_ = arena / unit;
|
| 656 |
-
arena = n_big_ * unit;
|
| 657 |
-
}
|
| 658 |
-
if (n_small_ == 0 && n_big_ == 0) {
|
| 659 |
-
throw std::runtime_error("expert arena 太小(算不出槽位)");
|
| 660 |
-
}
|
| 661 |
-
n_slots_ = n_small_ + n_big_;
|
| 662 |
-
if (n_slots_ < 16) {
|
| 663 |
-
throw std::runtime_error("expert arena 太小(算不出 16 個槽位)");
|
| 664 |
-
}
|
| 665 |
-
|
| 666 |
-
void * p = nullptr;
|
| 667 |
-
const size_t alloc = arena + (2ull << 20);
|
| 668 |
-
if (posix_memalign(&p, 2ull << 20, alloc) != 0 || !p) {
|
| 669 |
-
throw std::runtime_error("無法配置 expert arena(" + std::to_string(arena >> 20) + " MB)");
|
| 670 |
-
}
|
| 671 |
-
// 讓 arena 全部變成實體 RAM,並提示核心使用 huge page(減少 TLB 壓力)
|
| 672 |
-
memset(p, 0, alloc);
|
| 673 |
-
madvise(p, alloc, MADV_HUGEPAGE);
|
| 674 |
-
arena_ = (uint8_t *) p;
|
| 675 |
-
arena_bytes_ = arena;
|
| 676 |
-
|
| 677 |
-
// OPT-1:剖面計數表��n_layer × n_expert,40×256 = 10240 筆 ≈ 40 KB,可忽略)
|
| 678 |
-
prof_.assign((size_t) info_.n_layer * (size_t) info_.n_expert, 0);
|
| 679 |
-
ensure_layer_stats((int) info_.n_layer); // SDQ-0004
|
| 680 |
-
if (!cfg_.profile_in.empty()) {
|
| 681 |
-
load_profile(cfg_.profile_in);
|
| 682 |
-
}
|
| 683 |
-
profile_out_ = cfg_.profile_out;
|
| 684 |
-
|
| 685 |
-
slots_.assign(n_slots_, {});
|
| 686 |
-
free_slots_.reserve(n_small_);
|
| 687 |
-
for (size_t i = n_small_; i-- > 0;) {
|
| 688 |
-
free_slots_.push_back((int32_t) i);
|
| 689 |
-
}
|
| 690 |
-
free_big_slots_.reserve(n_big_);
|
| 691 |
-
for (size_t i = n_slots_; i-- > n_small_;) {
|
| 692 |
-
free_big_slots_.push_back((int32_t) i);
|
| 693 |
-
}
|
| 694 |
-
used_.store(0);
|
| 695 |
-
|
| 696 |
-
hist_.assign((size_t) info_.n_layer, {});
|
| 697 |
-
for (auto & per_layer : hist_) {
|
| 698 |
-
per_layer.assign((size_t) info_.n_expert, {});
|
| 699 |
-
}
|
| 700 |
-
prev_used_.assign((size_t) info_.n_layer, {});
|
| 701 |
-
cur_used_.assign((size_t) info_.n_layer, {});
|
| 702 |
-
}
|
| 703 |
-
|
| 704 |
-
void pager::init(const config & cfg, ggml_context * ctx) {
|
| 705 |
-
(void) ctx;
|
| 706 |
-
if (ready_) {
|
| 707 |
-
return;
|
| 708 |
-
}
|
| 709 |
-
cfg_ = cfg;
|
| 710 |
-
if (cfg_.model_path.empty()) {
|
| 711 |
-
const char * v = getenv("SDQ_MODEL_PATH");
|
| 712 |
-
cfg_.model_path = v ? v : "";
|
| 713 |
-
}
|
| 714 |
-
if (cfg_.model_path.empty()) {
|
| 715 |
-
throw std::runtime_error("SDQ 缺少模型路徑(設定 SDQ_MODEL_PATH)");
|
| 716 |
-
}
|
| 717 |
-
|
| 718 |
-
// 先讀 header(順便列舉 shard),再開 fd:shard 清單只有在解析
|
| 719 |
-
// split.count 之後才知道,而 open_model_fd() 需要它。
|
| 720 |
-
load_gguf_header(cfg_.model_path);
|
| 721 |
-
open_model_fd();
|
| 722 |
-
if (shard_fd_.empty() || shard_fd_[0] < 0) {
|
| 723 |
-
throw std::runtime_error("無法開啟模型檔:" + shard_path(0));
|
| 724 |
-
}
|
| 725 |
-
// 順序有意義:I/O 池的對齊緩衝必須在 alloc_arena() 之前配置,
|
| 726 |
-
// 否則 arena 會把它當成「不存在的 RAM」,實際上就偷用了 8 GB 預算。
|
| 727 |
-
io_pool_start();
|
| 728 |
-
alloc_arena();
|
| 729 |
-
|
| 730 |
-
if (cfg_.prefetch) {
|
| 731 |
-
pf_ = std::thread([this] { pf_worker(); });
|
| 732 |
-
}
|
| 733 |
-
ready_ = true;
|
| 734 |
-
print_summary();
|
| 735 |
-
}
|
| 736 |
-
|
| 737 |
-
const layer_desc * pager::layer(int il) const {
|
| 738 |
-
if (il < 0 || il >= (int) layers_.size()) {
|
| 739 |
-
return nullptr;
|
| 740 |
-
}
|
| 741 |
-
const layer_desc & L = layers_[(size_t) il];
|
| 742 |
-
if (L.experts.empty()) {
|
| 743 |
-
return nullptr;
|
| 744 |
-
}
|
| 745 |
-
return &L;
|
| 746 |
-
}
|
| 747 |
-
|
| 748 |
-
void pager::open_model_fd() {
|
| 749 |
-
if (shard_paths_.empty()) {
|
| 750 |
-
shard_paths_.push_back(cfg_.model_path);
|
| 751 |
-
}
|
| 752 |
-
shard_fd_.assign(shard_paths_.size(), -1);
|
| 753 |
-
shard_fd_direct_.assign(shard_paths_.size(), -1);
|
| 754 |
-
for (size_t i = 0; i < shard_paths_.size(); i++) {
|
| 755 |
-
shard_fd_[i] = open(shard_paths_[i].c_str(), O_RDONLY);
|
| 756 |
-
if (shard_fd_[i] < 0) {
|
| 757 |
-
fprintf(stderr, "[sdq] 無法開啟 shard %zu: %s\n", i, shard_paths_[i].c_str());
|
| 758 |
-
return;
|
| 759 |
-
}
|
| 760 |
-
}
|
| 761 |
-
// O_DIRECT:讀資料完全不進 kernel page cache。
|
| 762 |
-
// 這是「RAM 帳目誠實」的關鍵 —— 否則核心預讀會讓 mmap 檔案的頁算進 RSS
|
| 763 |
-
// (實測 4K prefill 時檔案對映 RSS 可以多到 6 GB 以上),而且量到的
|
| 764 |
-
// 「SSD 讀取量」會被上一輪的 page cache 灌水。
|
| 765 |
-
const char * no_direct = getenv("SDQ_NO_DIRECT_IO");
|
| 766 |
-
if (no_direct && *no_direct && *no_direct != '0') {
|
| 767 |
-
return;
|
| 768 |
-
}
|
| 769 |
-
#ifdef O_DIRECT
|
| 770 |
-
// 每個 shard 各自開一個 O_DIRECT fd:O_DIRECT 是 fd 層級的屬性,不能共用。
|
| 771 |
-
for (size_t i = 0; i < shard_paths_.size(); i++) {
|
| 772 |
-
shard_fd_direct_[i] = open(shard_paths_[i].c_str(), O_RDONLY | O_DIRECT);
|
| 773 |
-
if (shard_fd_direct_[i] < 0) {
|
| 774 |
-
break;
|
| 775 |
-
}
|
| 776 |
-
}
|
| 777 |
-
use_direct_ = shard_fd_direct_[0] >= 0;
|
| 778 |
-
if (use_direct_) {
|
| 779 |
-
fprintf(stderr, "[sdq] 使用 O_DIRECT 讀取模型檔(%zu 個 shard,不佔用 kernel page cache)\n",
|
| 780 |
-
shard_paths_.size());
|
| 781 |
-
} else {
|
| 782 |
-
fprintf(stderr, "[sdq] O_DIRECT 不可用,改用 buffered + fadvise(DONTNEED)\n");
|
| 783 |
-
}
|
| 784 |
-
#endif
|
| 785 |
-
}
|
| 786 |
-
|
| 787 |
-
// 有對齊的緩衝區(O_DIRECT 要求),thread_local 避免競爭
|
| 788 |
-
static thread_local uint8_t * g_direct_buf = nullptr;
|
| 789 |
-
static thread_local size_t g_direct_sz = 0;
|
| 790 |
-
|
| 791 |
-
// 與 read_range_direct 相同,但用呼叫端提供的緩衝區(已配置好、已算進 RSS)。
|
| 792 |
-
// I/O 執行緒池走這條:每條 worker 在啟動時就配置好對齊緩衝,於是 arena sizing 時
|
| 793 |
-
// 這筆記憶體已經在 RSS 裡,不會變成「額外偷用的 RAM」。
|
| 794 |
-
// sharded GGUF:把「哪個 shard」一路帶到 pread。
|
| 795 |
-
static inline int sdq_fd_for(const std::vector<int> & fds, int shard) {
|
| 796 |
-
if (shard < 0 || shard >= (int) fds.size()) {
|
| 797 |
-
return -1;
|
| 798 |
-
}
|
| 799 |
-
return fds[(size_t) shard];
|
| 800 |
-
}
|
| 801 |
-
|
| 802 |
-
bool pager::read_range_staged(int shard, uint64_t off, uint8_t * dst, size_t size,
|
| 803 |
-
uint8_t * stage, size_t stage_sz) {
|
| 804 |
-
if (!use_direct_) {
|
| 805 |
-
return read_range(shard, off, dst, size);
|
| 806 |
-
}
|
| 807 |
-
const int fd = sdq_fd_for(shard_fd_direct_, shard);
|
| 808 |
-
if (fd < 0) {
|
| 809 |
-
return false;
|
| 810 |
-
}
|
| 811 |
-
const size_t blk = 4096;
|
| 812 |
-
const uint64_t start = off & ~(uint64_t)(blk - 1);
|
| 813 |
-
const uint64_t end = (off + size + blk - 1) & ~(uint64_t)(blk - 1);
|
| 814 |
-
const size_t bytes = (size_t)(end - start);
|
| 815 |
-
if (stage_sz < bytes) {
|
| 816 |
-
// 預先配置不足(不該發生):退回 thread_local 的通用路徑
|
| 817 |
-
return read_range_direct(shard, off, dst, size);
|
| 818 |
-
}
|
| 819 |
-
size_t done = 0;
|
| 820 |
-
while (done < bytes) {
|
| 821 |
-
const ssize_t n = pread(fd, stage + done, bytes - done,
|
| 822 |
-
(off_t)(start + done));
|
| 823 |
-
if (n <= 0) {
|
| 824 |
-
return false;
|
| 825 |
-
}
|
| 826 |
-
done += (size_t) n;
|
| 827 |
-
}
|
| 828 |
-
memcpy(dst, stage + (off - start), size);
|
| 829 |
-
return true;
|
| 830 |
-
}
|
| 831 |
-
|
| 832 |
-
bool pager::read_range_direct(int shard, uint64_t off, uint8_t * dst, size_t size) {
|
| 833 |
-
if (!use_direct_) {
|
| 834 |
-
return read_range(shard, off, dst, size);
|
| 835 |
-
}
|
| 836 |
-
const int fd = sdq_fd_for(shard_fd_direct_, shard);
|
| 837 |
-
if (fd < 0) {
|
| 838 |
-
return false;
|
| 839 |
-
}
|
| 840 |
-
const size_t blk = 4096;
|
| 841 |
-
const uint64_t start = off & ~(uint64_t)(blk - 1);
|
| 842 |
-
const uint64_t end = (off + size + blk - 1) & ~(uint64_t)(blk - 1);
|
| 843 |
-
const size_t bytes = (size_t)(end - start);
|
| 844 |
-
if (g_direct_sz < bytes) {
|
| 845 |
-
if (g_direct_buf) {
|
| 846 |
-
free(g_direct_buf);
|
| 847 |
-
}
|
| 848 |
-
if (posix_memalign((void **) &g_direct_buf, blk, bytes) != 0) {
|
| 849 |
-
g_direct_buf = nullptr;
|
| 850 |
-
g_direct_sz = 0;
|
| 851 |
-
use_direct_ = false;
|
| 852 |
-
return read_range(shard, off, dst, size);
|
| 853 |
-
}
|
| 854 |
-
g_direct_sz = bytes;
|
| 855 |
-
}
|
| 856 |
-
size_t done = 0;
|
| 857 |
-
while (done < bytes) {
|
| 858 |
-
const ssize_t n = pread(fd, g_direct_buf + done, bytes - done,
|
| 859 |
-
(off_t)(start + done));
|
| 860 |
-
if (n <= 0) {
|
| 861 |
-
return false;
|
| 862 |
-
}
|
| 863 |
-
done += (size_t) n;
|
| 864 |
-
}
|
| 865 |
-
memcpy(dst, g_direct_buf + (off - start), size);
|
| 866 |
-
return true;
|
| 867 |
-
}
|
| 868 |
-
|
| 869 |
-
bool pager::read_range(int shard, uint64_t off, uint8_t * dst, size_t size) {
|
| 870 |
-
const int fd = sdq_fd_for(shard_fd_, shard);
|
| 871 |
-
if (fd < 0) {
|
| 872 |
-
return false;
|
| 873 |
-
}
|
| 874 |
-
size_t done = 0;
|
| 875 |
-
while (done < size) {
|
| 876 |
-
const ssize_t n = pread(fd, dst + done, size - done, (off_t) (off + done));
|
| 877 |
-
if (n <= 0) {
|
| 878 |
-
return false;
|
| 879 |
-
}
|
| 880 |
-
done += (size_t) n;
|
| 881 |
-
}
|
| 882 |
-
// 讓 RAM 帳目誠實:把 kernel 對這個檔案的 page cache 丟掉,避免「藏在核心裡的 RAM」
|
| 883 |
-
if (cfg_.fadvise_dontneed) {
|
| 884 |
-
posix_fadvise(fd, (off_t) off, (off_t) size, POSIX_FADV_DONTNEED);
|
| 885 |
-
}
|
| 886 |
-
return true;
|
| 887 |
-
}
|
| 888 |
-
|
| 889 |
-
bool pager::read_expert(int il, int ie, uint8_t * dst) {
|
| 890 |
-
const layer_desc * L = layer(il);
|
| 891 |
-
if (!L || ie < 0 || ie >= (int) L->experts.size()) {
|
| 892 |
-
return false;
|
| 893 |
-
}
|
| 894 |
-
const expert_desc & ed = L->experts[(size_t) ie];
|
| 895 |
-
for (int p = 0; p < 3; p++) {
|
| 896 |
-
if (ed.seg[p].size == 0) {
|
| 897 |
-
continue;
|
| 898 |
-
}
|
| 899 |
-
if (!read_range_direct(ed.seg[p].shard, ed.seg[p].off,
|
| 900 |
-
dst + ed.slot_off[p], (size_t) ed.seg[p].size)) {
|
| 901 |
-
return false;
|
| 902 |
-
}
|
| 903 |
-
}
|
| 904 |
-
return true;
|
| 905 |
-
}
|
| 906 |
-
|
| 907 |
-
// ---------------------------------------------------------------- I/O 執行緒池
|
| 908 |
-
//
|
| 909 |
-
// 問題:一個 expert = gate / up / down 三段,檔案上是三段不相鄰的資料。
|
| 910 |
-
// 循序讀就是 3 次 pread 排在同一條執行緒上,而單次 1.9 MB 的 pread 在本機量到
|
| 911 |
-
// ~7 ms(≈275 MB/s 的單流頻寬)。因此「一個 expert 要多久」= 3 × 單次延遲,
|
| 912 |
-
// 這條路徑的長度跟「有多少條執行緒」無關 —— mode C 再怎麼排程都救不了。
|
| 913 |
-
//
|
| 914 |
-
// 做法:把三段交給三條執行緒同時 pread,臨界路徑降到約「單段」的時間。
|
| 915 |
-
// 量測(8 個 expert 全部 miss 的一層,各 9 次取中位數):
|
| 916 |
-
// 8 執行緒 × 三段序列讀 : 12.60 ms
|
| 917 |
-
// 16 執行緒 × 三段平行讀 : 7.57 ms
|
| 918 |
-
// 24 執行緒 × 三段平行讀 : 7.58 ms ← 16 就飽和,所以預設 3 × n_threads
|
| 919 |
-
//
|
| 920 |
-
// 這個池子對 prefill 更有用:一層要 256 個 expert = 768 個讀取工作,
|
| 921 |
-
// 序列讀時 16 執行緒只能拿到 16 路的頻寬 utilization。
|
| 922 |
-
void pager::io_pool_start() {
|
| 923 |
-
if (!cfg_.io_parallel) {
|
| 924 |
-
fprintf(stderr, "[sdq] I/O 段平行:關(每個 expert 的 gate/up/down 循序讀)\n");
|
| 925 |
-
return;
|
| 926 |
-
}
|
| 927 |
-
int n = cfg_.io_threads > 0 ? cfg_.io_threads : cfg_.n_threads * 3;
|
| 928 |
-
if (n < 4) {
|
| 929 |
-
n = 4;
|
| 930 |
-
}
|
| 931 |
-
if (n > 48) {
|
| 932 |
-
n = 48;
|
| 933 |
-
}
|
| 934 |
-
// 先算「一段權重最大有多少位元組」,每條 worker 配一塊對齊緩衝。
|
| 935 |
-
// 這必须在 alloc_arena() 之前做完,否則這筆 RAM 會變成帳外支出。
|
| 936 |
-
size_t max_seg = 0;
|
| 937 |
-
for (const auto & L : layers_) {
|
| 938 |
-
for (const auto & ed : L.experts) {
|
| 939 |
-
for (int p = 0; p < 3; p++) {
|
| 940 |
-
max_seg = std::max(max_seg, (size_t) ed.seg[p].size);
|
| 941 |
-
}
|
| 942 |
-
}
|
| 943 |
-
}
|
| 944 |
-
const size_t stage_bytes = ((max_seg + 2 * 4096) + 4095) & ~(size_t) 4095;
|
| 945 |
-
io_stage_.assign((size_t) n, io_stage{});
|
| 946 |
-
for (int i = 0; i < n; i++) {
|
| 947 |
-
void * p = nullptr;
|
| 948 |
-
if (posix_memalign(&p, 4096, stage_bytes) == 0 && p) {
|
| 949 |
-
memset(p, 0, stage_bytes); // 實際碰頁,讓它出現在 RSS 裡
|
| 950 |
-
io_stage_[(size_t) i].buf = (uint8_t *) p;
|
| 951 |
-
io_stage_[(size_t) i].sz = stage_bytes;
|
| 952 |
-
}
|
| 953 |
-
}
|
| 954 |
-
io_split_ = cfg_.io_split > 0 ? cfg_.io_split : 1;
|
| 955 |
-
io_stop_ = false;
|
| 956 |
-
io_pool_.reserve((size_t) n);
|
| 957 |
-
for (int i = 0; i < n; i++) {
|
| 958 |
-
const size_t wid = (size_t) i;
|
| 959 |
-
try {
|
| 960 |
-
io_pool_.emplace_back([this, wid] { io_worker(wid); });
|
| 961 |
-
} catch (...) {
|
| 962 |
-
break;
|
| 963 |
-
}
|
| 964 |
-
}
|
| 965 |
-
io_threads_ = (int) io_pool_.size();
|
| 966 |
-
io_pool_on_ = io_threads_ > 0;
|
| 967 |
-
if (io_pool_on_) {
|
| 968 |
-
fprintf(stderr, "[sdq] I/O 段平行:%d 條讀取執行緒 × %zu KB 對齊緩衝"
|
| 969 |
-
"(gate/up/down 同時讀,每段再切 %d 塊)\n",
|
| 970 |
-
io_threads_, stage_bytes >> 10, io_split_);
|
| 971 |
-
}
|
| 972 |
-
}
|
| 973 |
-
|
| 974 |
-
void pager::io_pool_stop() {
|
| 975 |
-
if (io_pool_.empty()) {
|
| 976 |
-
for (auto & s : io_stage_) {
|
| 977 |
-
free(s.buf);
|
| 978 |
-
s.buf = nullptr;
|
| 979 |
-
}
|
| 980 |
-
io_stage_.clear();
|
| 981 |
-
return;
|
| 982 |
-
}
|
| 983 |
-
{
|
| 984 |
-
std::lock_guard<std::mutex> lk(io_mu_);
|
| 985 |
-
io_stop_ = true;
|
| 986 |
-
}
|
| 987 |
-
io_cv_.notify_all();
|
| 988 |
-
for (auto & t : io_pool_) {
|
| 989 |
-
if (t.joinable()) {
|
| 990 |
-
t.join();
|
| 991 |
-
}
|
| 992 |
-
}
|
| 993 |
-
io_pool_.clear();
|
| 994 |
-
io_pool_on_ = false;
|
| 995 |
-
for (auto & s : io_stage_) {
|
| 996 |
-
free(s.buf);
|
| 997 |
-
s.buf = nullptr;
|
| 998 |
-
s.sz = 0;
|
| 999 |
-
}
|
| 1000 |
-
io_stage_.clear();
|
| 1001 |
-
}
|
| 1002 |
-
|
| 1003 |
-
void pager::io_worker(size_t wid) {
|
| 1004 |
-
for (;;) {
|
| 1005 |
-
io_job job{};
|
| 1006 |
-
{
|
| 1007 |
-
std::unique_lock<std::mutex> lk(io_mu_);
|
| 1008 |
-
io_cv_.wait(lk, [this] { return io_stop_ || !io_q_.empty(); });
|
| 1009 |
-
if (io_q_.empty()) {
|
| 1010 |
-
if (io_stop_) {
|
| 1011 |
-
return;
|
| 1012 |
-
}
|
| 1013 |
-
continue;
|
| 1014 |
-
}
|
| 1015 |
-
job = io_q_.front();
|
| 1016 |
-
io_q_.pop_front();
|
| 1017 |
-
}
|
| 1018 |
-
io_jobs_.fetch_add(1, std::memory_order_relaxed);
|
| 1019 |
-
// 讀取本身不持鎖:io_mu_ 只保護佇列與完成計數
|
| 1020 |
-
const bool ok = (wid < io_stage_.size() && io_stage_[wid].buf)
|
| 1021 |
-
? read_range_staged(job.shard, job.off, job.dst, job.size,
|
| 1022 |
-
io_stage_[wid].buf, io_stage_[wid].sz)
|
| 1023 |
-
: read_range_direct(job.shard, job.off, job.dst, job.size);
|
| 1024 |
-
{
|
| 1025 |
-
std::lock_guard<std::mutex> lk(io_mu_);
|
| 1026 |
-
if (!ok) {
|
| 1027 |
-
job.ctx->ok = false;
|
| 1028 |
-
}
|
| 1029 |
-
if (job.ctx->remaining.fetch_sub(1) == 1) {
|
| 1030 |
-
io_done_cv_.notify_all();
|
| 1031 |
-
}
|
| 1032 |
-
}
|
| 1033 |
-
}
|
| 1034 |
-
}
|
| 1035 |
-
|
| 1036 |
-
// 把 (il,ie) 的三段交給執行緒池並行讀,呼叫端等到整批完成才返回。
|
| 1037 |
-
// ctx 放在呼叫端 stack:worker 只會在 remaining 歸零之後才再碰它,
|
| 1038 |
-
// 而我們正是等到 remaining==0 才離開函式,所以沒有 use-after-return。
|
| 1039 |
-
bool pager::read_expert_par(int il, int ie, uint8_t * dst) {
|
| 1040 |
-
const layer_desc * L = layer(il);
|
| 1041 |
-
if (!L || ie < 0 || ie >= (int) L->experts.size()) {
|
| 1042 |
-
return false;
|
| 1043 |
-
}
|
| 1044 |
-
if (!io_pool_on_) {
|
| 1045 |
-
return read_expert(il, ie, dst);
|
| 1046 |
-
}
|
| 1047 |
-
const expert_desc & ed = L->experts[(size_t) ie];
|
| 1048 |
-
io_ctx ctx;
|
| 1049 |
-
int n = 0;
|
| 1050 |
-
// 把每一段再切成一樣大的好幾塊,交給不同的執行緒同時讀。
|
| 1051 |
-
//
|
| 1052 |
-
// 為什麼:單一 pread 的時間 ≈ 固定延遲 + 位元組數 / 單流頻寬。
|
| 1053 |
-
// 本機實測單流約 275 MB/s、4K 隨機讀延遲約 0.57 ms,所以
|
| 1054 |
-
// 590 KB 讀一次 ≈ 0.57 + 2.1 = 2.7 ms
|
| 1055 |
-
// 590 KB 切成 2 塊 ≈ 0.57 + 1.05 = 1.6 ms
|
| 1056 |
-
// 而「讀完一個 expert」的時間正是每層的關鍵路徑,切塊就是直接砍關鍵路徑。
|
| 1057 |
-
// 設定檔 SDQ_IO_SPLIT。**實測預設是 1(不切塊)**:切塊在 24 條讀取執行緒
|
| 1058 |
-
// 之下沒有可重現的改善(split=1..6 的中位數在 4.9~5.9 之間跳動,純噪音),
|
| 1059 |
-
// 因為 8 個 expert × 3 段 = 24 個並行請求已經把裝置的佇列灌飽,
|
| 1060 |
-
// 這時瓶頸是裝置總頻寬而不是單一請求的延遲。保留這個開關是為了其他裝置。
|
| 1061 |
-
const int split = io_split_;
|
| 1062 |
-
const size_t blk = 4096;
|
| 1063 |
-
{
|
| 1064 |
-
std::lock_guard<std::mutex> lk(io_mu_);
|
| 1065 |
-
for (int p = 0; p < 3; p++) {
|
| 1066 |
-
const size_t sz = (size_t) ed.seg[p].size;
|
| 1067 |
-
if (sz == 0) {
|
| 1068 |
-
continue;
|
| 1069 |
-
}
|
| 1070 |
-
uint8_t * base = dst + ed.slot_off[p];
|
| 1071 |
-
const int sh = ed.seg[p].shard;
|
| 1072 |
-
if (split <= 1 || sz <= 2 * blk) {
|
| 1073 |
-
io_q_.push_back(io_job{sh, ed.seg[p].off, base, sz, &ctx});
|
| 1074 |
-
n++;
|
| 1075 |
-
continue;
|
| 1076 |
-
}
|
| 1077 |
-
size_t chunk = ((sz + (size_t) split - 1) / (size_t) split + blk - 1) & ~(blk - 1);
|
| 1078 |
-
for (size_t off = 0; off < sz; off += chunk) {
|
| 1079 |
-
const size_t take = sz - off < chunk ? sz - off : chunk;
|
| 1080 |
-
io_q_.push_back(io_job{sh, ed.seg[p].off + off, base + off, take, &ctx});
|
| 1081 |
-
n++;
|
| 1082 |
-
}
|
| 1083 |
-
}
|
| 1084 |
-
ctx.remaining.store(n);
|
| 1085 |
-
}
|
| 1086 |
-
if (n == 0) {
|
| 1087 |
-
return true;
|
| 1088 |
-
}
|
| 1089 |
-
io_cv_.notify_all();
|
| 1090 |
-
const uint64_t t0 = now_us();
|
| 1091 |
-
{
|
| 1092 |
-
std::unique_lock<std::mutex> lk(io_mu_);
|
| 1093 |
-
io_done_cv_.wait(lk, [&ctx] { return ctx.remaining.load() == 0; });
|
| 1094 |
-
}
|
| 1095 |
-
io_wait_ns_.fetch_add((now_us() - t0) * 1000ull, std::memory_order_relaxed);
|
| 1096 |
-
return ctx.ok;
|
| 1097 |
-
}
|
| 1098 |
-
|
| 1099 |
-
uint64_t pager::read_expert_span(int il, const int32_t * experts, int n, const int32_t * slots) {
|
| 1100 |
-
const layer_desc * L = layer(il);
|
| 1101 |
-
if (!L || n <= 0) {
|
| 1102 |
-
return 0;
|
| 1103 |
-
}
|
| 1104 |
-
const uint64_t t_start = now_us();
|
| 1105 |
-
// 每個執行緒一份暫存區(多執行緒會同時呼叫,不能共用一個 buffer)
|
| 1106 |
-
static thread_local std::vector<uint8_t> scratch_buf;
|
| 1107 |
-
uint64_t total = 0;
|
| 1108 |
-
|
| 1109 |
-
// 把 experts 切成「檔案編號連續」的區段
|
| 1110 |
-
int i = 0;
|
| 1111 |
-
while (i < n) {
|
| 1112 |
-
int j = i + 1;
|
| 1113 |
-
while (j < n && experts[j] == experts[j - 1] + 1) {
|
| 1114 |
-
j++;
|
| 1115 |
-
}
|
| 1116 |
-
const int cnt = j - i;
|
| 1117 |
-
const expert_desc & first = L->experts[(size_t) experts[i]];
|
| 1118 |
-
const expert_desc & last = L->experts[(size_t) experts[j - 1]];
|
| 1119 |
-
|
| 1120 |
-
for (int p = 0; p < 3; p++) {
|
| 1121 |
-
if (first.seg[p].size == 0 || last.seg[p].size == 0) {
|
| 1122 |
-
continue;
|
| 1123 |
-
}
|
| 1124 |
-
// 連續區段必須在同一個 shard 內;跨 shard 就切斷這個區段。
|
| 1125 |
-
if (last.seg[p].shard != first.seg[p].shard) {
|
| 1126 |
-
continue;
|
| 1127 |
-
}
|
| 1128 |
-
const uint64_t off = first.seg[p].off;
|
| 1129 |
-
const uint64_t end = last.seg[p].off + last.seg[p].size;
|
| 1130 |
-
const size_t bytes = (size_t) (end - off);
|
| 1131 |
-
if (scratch_buf.size() < bytes) {
|
| 1132 |
-
scratch_buf.assign(bytes, 0);
|
| 1133 |
-
}
|
| 1134 |
-
if (!read_range_direct(first.seg[p].shard, off, scratch_buf.data(), bytes)) {
|
| 1135 |
-
return total;
|
| 1136 |
-
}
|
| 1137 |
-
total += bytes;
|
| 1138 |
-
// 散到各自的槽位:scratch 從 off 開始,所以偏移是「相對於 off」
|
| 1139 |
-
for (int k = i; k < j; k++) {
|
| 1140 |
-
const expert_desc & ed = L->experts[(size_t) experts[k]];
|
| 1141 |
-
const size_t seg_bytes = (size_t) ed.seg[p].size;
|
| 1142 |
-
const size_t delta = (size_t) (ed.seg[p].off - off);
|
| 1143 |
-
if (slots[k] < 0) {
|
| 1144 |
-
continue;
|
| 1145 |
-
}
|
| 1146 |
-
if ((size_t) slots[k] >= n_slots_ || (uint64_t) ed.slot_off[p] + seg_bytes > slot_bytes_
|
| 1147 |
-
|| delta + seg_bytes > scratch_buf.size()) {
|
| 1148 |
-
fprintf(stderr, "[sdq-pager] span 越界:il=%d slot=%d/%zu slot_off=%lld seg=%zu delta=%zu scratch=%zu p=%d e=%d\n",
|
| 1149 |
-
il, slots[k], n_slots_, (long long) ed.slot_off[p], seg_bytes, delta, scratch_buf.size(), p, experts[k]);
|
| 1150 |
-
return total;
|
| 1151 |
-
}
|
| 1152 |
-
memcpy(slot_ptr(slots[k]) + ed.slot_off[p],
|
| 1153 |
-
scratch_buf.data() + delta, seg_bytes);
|
| 1154 |
-
}
|
| 1155 |
-
}
|
| 1156 |
-
i = j;
|
| 1157 |
-
}
|
| 1158 |
-
stats_.demand_wait_us.fetch_add(now_us() - t_start, std::memory_order_relaxed);
|
| 1159 |
-
return total;
|
| 1160 |
-
}
|
| 1161 |
-
|
| 1162 |
-
int pager::reserve_slot(int il, int ie) {
|
| 1163 |
-
if (!ready_) {
|
| 1164 |
-
return -1;
|
| 1165 |
-
}
|
| 1166 |
-
count_request(il, ie);
|
| 1167 |
-
std::lock_guard<std::mutex> lk(mu_);
|
| 1168 |
-
const int32_t exist = find_slot_locked(il, ie);
|
| 1169 |
-
if (exist >= 0) {
|
| 1170 |
-
slots_[(size_t) exist].last_use = ++clock_;
|
| 1171 |
-
slots_[(size_t) exist].pin++;
|
| 1172 |
-
touch_freq_locked(slots_[(size_t) exist]);
|
| 1173 |
-
return exist;
|
| 1174 |
-
}
|
| 1175 |
-
const int32_t s = victim_locked();
|
| 1176 |
-
if (s < 0) {
|
| 1177 |
-
return -1;
|
| 1178 |
-
}
|
| 1179 |
-
slot_state & st = slots_[(size_t) s];
|
| 1180 |
-
if (st.layer >= 0) {
|
| 1181 |
-
page_.erase(((int64_t) st.layer << 20) | (int64_t) st.expert);
|
| 1182 |
-
stats_.evictions.fetch_add(1, std::memory_order_relaxed);
|
| 1183 |
-
used_.fetch_sub(1, std::memory_order_relaxed);
|
| 1184 |
-
}
|
| 1185 |
-
st.layer = il;
|
| 1186 |
-
st.expert = ie;
|
| 1187 |
-
st.last_use = ++clock_;
|
| 1188 |
-
st.freq = 1;
|
| 1189 |
-
st.pin = 1;
|
| 1190 |
-
st.is_hot = hot_lookup(il, ie);
|
| 1191 |
-
page_[((int64_t) il << 20) | (int64_t) ie] = s;
|
| 1192 |
-
used_.fetch_add(1, std::memory_order_relaxed);
|
| 1193 |
-
stats_.expert_misses.fetch_add(1, std::memory_order_relaxed);
|
| 1194 |
-
return s;
|
| 1195 |
-
}
|
| 1196 |
-
|
| 1197 |
-
int pager::probe(int il, int ie) {
|
| 1198 |
-
if (!ready_) {
|
| 1199 |
-
return -1;
|
| 1200 |
-
}
|
| 1201 |
-
stats_.expert_requests.fetch_add(1, std::memory_order_relaxed);
|
| 1202 |
-
count_request(il, ie);
|
| 1203 |
-
std::lock_guard<std::mutex> lk(mu_);
|
| 1204 |
-
const int32_t s = find_slot_locked(il, ie);
|
| 1205 |
-
if (s < 0) {
|
| 1206 |
-
stats_.expert_misses.fetch_add(1, std::memory_order_relaxed);
|
| 1207 |
-
return -1;
|
| 1208 |
-
}
|
| 1209 |
-
slots_[(size_t) s].last_use = ++clock_;
|
| 1210 |
-
slots_[(size_t) s].pin++;
|
| 1211 |
-
touch_freq_locked(slots_[(size_t) s]);
|
| 1212 |
-
stats_.expert_hits.fetch_add(1, std::memory_order_relaxed);
|
| 1213 |
-
return s;
|
| 1214 |
-
}
|
| 1215 |
-
|
| 1216 |
-
void pager::note_span_read(uint64_t bytes) {
|
| 1217 |
-
stats_.ssb_read_bytes_demand.fetch_add(bytes, std::memory_order_relaxed);
|
| 1218 |
-
stats_.demand_wait_us.fetch_add(0, std::memory_order_relaxed);
|
| 1219 |
-
}
|
| 1220 |
-
|
| 1221 |
-
// 衰減式頻率:每 8192 次使用就把所有計數減半,讓「過去熱門、現在變冷」的 expert 能被淘汰
|
| 1222 |
-
void pager::touch_freq_locked(slot_state & st) {
|
| 1223 |
-
if (++freq_tick_ >= 8192) {
|
| 1224 |
-
freq_tick_ = 0;
|
| 1225 |
-
for (auto & s : slots_) {
|
| 1226 |
-
s.freq /= 2;
|
| 1227 |
-
}
|
| 1228 |
-
}
|
| 1229 |
-
if (st.freq < 0xFFFFFFFFu) {
|
| 1230 |
-
st.freq++;
|
| 1231 |
-
}
|
| 1232 |
-
}
|
| 1233 |
-
|
| 1234 |
-
// ============================== OPT-1:熱門 expert 剖面 ==============================
|
| 1235 |
-
// 問題實測(docs/03):arena 3.88 GiB 只放得下 2044 個 expert,總共卻有 10240 個,
|
| 1236 |
-
// prefill 的幾百個「只用一次」的專家會把熱區整個洗掉,命中率卡在 ~70%。
|
| 1237 |
-
// 解法借用 Strata 的做法(src/core/expert_cache.cpp):**離線剖面 + 靜態排行榜 +
|
| 1238 |
-
// 熱頁永不淘汰**。這裡不動檔案格式,用純文字存排行榜。
|
| 1239 |
-
|
| 1240 |
-
// ---- SDQ-0004
|
| 1241 |
-
void pager::ensure_layer_stats(int n_layer) {
|
| 1242 |
-
if (lstats_ || n_layer <= 0) {
|
| 1243 |
-
return;
|
| 1244 |
-
}
|
| 1245 |
-
lstats_.reset(new layer_stat[(size_t) n_layer]);
|
| 1246 |
-
}
|
| 1247 |
-
|
| 1248 |
-
void pager::count_request(int il, int ie) {
|
| 1249 |
-
if (prof_.empty() || il < 0 || ie < 0) {
|
| 1250 |
-
return;
|
| 1251 |
-
}
|
| 1252 |
-
const size_t idx = (size_t) il * (size_t) info_.n_expert + (size_t) ie;
|
| 1253 |
-
if (idx < prof_.size()) {
|
| 1254 |
-
// 單執行緒 ++ 即可:請求次數是統計用途,不參與任何正確性判斷
|
| 1255 |
-
if (prof_[idx] < 0xFFFFFFFFu) {
|
| 1256 |
-
prof_[idx]++;
|
| 1257 |
-
}
|
| 1258 |
-
}
|
| 1259 |
-
}
|
| 1260 |
-
|
| 1261 |
-
bool pager::hot_lookup(int il, int ie) const {
|
| 1262 |
-
if (hot_.empty() || il < 0 || ie < 0) {
|
| 1263 |
-
return false;
|
| 1264 |
-
}
|
| 1265 |
-
const size_t idx = (size_t) il * (size_t) info_.n_expert + (size_t) ie;
|
| 1266 |
-
return idx < hot_.size() && hot_[idx] != 0;
|
| 1267 |
-
}
|
| 1268 |
-
|
| 1269 |
-
// 挑一個容得下 (il,ie) 的槽。與 victim_locked 的差別:**先只在冷頁裡找**,
|
| 1270 |
-
// 找不到才退讓去動熱頁。這就是「熱區不會被 prefill 洗掉」的機制。
|
| 1271 |
-
// 這個槽位放得下「需要 big 的 expert」嗎?
|
| 1272 |
-
// 小槽位:只能放小 expert
|
| 1273 |
-
// 大槽位:小expert 也放得下(只是浪費一點空間)
|
| 1274 |
-
static inline bool slot_accepts(bool need_big, bool slot_is_big) {
|
| 1275 |
-
return !need_big || slot_is_big;
|
| 1276 |
-
}
|
| 1277 |
-
|
| 1278 |
-
int pager::admit_slot_locked(int il, int ie) {
|
| 1279 |
-
// il/ie 都 < 0 代表「不指定大小」→ 兩種槽位都可以(舊的呼叫路徑)。
|
| 1280 |
-
const bool need_big = (il >= 0 && ie >= 0) ? need_big_slot(il, ie) : false;
|
| 1281 |
-
// 完全沒用過的空槽永遠優先(它不是熱頁,佔用等於浪費)
|
| 1282 |
-
// 大 expert 先看大槽位;小 expert 先看小槽位。
|
| 1283 |
-
std::vector<int32_t> & first = need_big ? free_big_slots_ : free_slots_;
|
| 1284 |
-
std::vector<int32_t> & second = need_big ? free_slots_ : free_big_slots_;
|
| 1285 |
-
while (!first.empty()) {
|
| 1286 |
-
const int32_t s = first.back();
|
| 1287 |
-
if (!slots_[(size_t) s].busy) {
|
| 1288 |
-
first.pop_back();
|
| 1289 |
-
return s;
|
| 1290 |
-
}
|
| 1291 |
-
}
|
| 1292 |
-
// 第一輪:只在「冷頁」裡挑最久未用的。
|
| 1293 |
-
// want_hot=true → 熱門 expert 進來時,把冷頁換掉(不動其他熱頁)
|
| 1294 |
-
// want_hot=false → 冷門 expert 進來時,優先吃冷頁
|
| 1295 |
-
// 兩種情況的規則相同,所以 hot_lookup 的結果不影響選擇順序,只影響標記。
|
| 1296 |
-
for (int pass = 0; pass < 2; pass++) {
|
| 1297 |
-
int64_t best = -1;
|
| 1298 |
-
uint64_t best_clock = UINT64_MAX;
|
| 1299 |
-
for (size_t s = 0; s < n_slots_; s++) {
|
| 1300 |
-
const slot_state & st = slots_[s];
|
| 1301 |
-
if (st.pin > 0 || st.busy) {
|
| 1302 |
-
continue;
|
| 1303 |
-
}
|
| 1304 |
-
if (pass == 0 && st.is_hot) {
|
| 1305 |
-
continue; // 第一輪不動熱頁
|
| 1306 |
-
}
|
| 1307 |
-
if (!slot_accepts(need_big, is_big_slot(s))) {
|
| 1308 |
-
continue;
|
| 1309 |
-
}
|
| 1310 |
-
if (st.last_use < best_clock) {
|
| 1311 |
-
best_clock = st.last_use;
|
| 1312 |
-
best = (int64_t) s;
|
| 1313 |
-
}
|
| 1314 |
-
}
|
| 1315 |
-
if (best >= 0) {
|
| 1316 |
-
return (int) best;
|
| 1317 |
-
}
|
| 1318 |
-
}
|
| 1319 |
-
// 退讓:另一種大小的空槽。**一定要檢查放不放得下** —— 小槽位放不下大 expert,
|
| 1320 |
-
// 硬塞會讓 read_expert 寫出槽位邊界(靜默把別的 expert 寫壞)。
|
| 1321 |
-
while (!second.empty()) {
|
| 1322 |
-
const int32_t s = second.back();
|
| 1323 |
-
if (!slots_[(size_t) s].busy &&
|
| 1324 |
-
slot_accepts(need_big, is_big_slot(s))) {
|
| 1325 |
-
second.pop_back();
|
| 1326 |
-
return s;
|
| 1327 |
-
}
|
| 1328 |
-
if (!slots_[(size_t) s].busy) {
|
| 1329 |
-
second.pop_back();
|
| 1330 |
-
} else {
|
| 1331 |
-
break;
|
| 1332 |
-
}
|
| 1333 |
-
}
|
| 1334 |
-
return -1;
|
| 1335 |
-
}
|
| 1336 |
-
|
| 1337 |
-
bool pager::load_profile(const std::string & path) {
|
| 1338 |
-
std::FILE * f = std::fopen(path.c_str(), "rb");
|
| 1339 |
-
if (!f) {
|
| 1340 |
-
fprintf(stderr, "[sdq] 找不到 expert 剖面 %s(將以動態 LRU/LFU 運作)\n", path.c_str());
|
| 1341 |
-
return false;
|
| 1342 |
-
}
|
| 1343 |
-
char magic[8] = {0};
|
| 1344 |
-
if (std::fread(magic, 1, 4, f) != 4 || std::memcmp(magic, "SDQP", 4) != 0) {
|
| 1345 |
-
std::fclose(f);
|
| 1346 |
-
fprintf(stderr, "[sdq] %s ��是 SDQ expert 剖面(缺少 SDQP 標頭)\n", path.c_str());
|
| 1347 |
-
return false;
|
| 1348 |
-
}
|
| 1349 |
-
uint32_t hdr[3] = {0, 0, 0};
|
| 1350 |
-
if (std::fread(hdr, sizeof(uint32_t), 3, f) != 3) {
|
| 1351 |
-
std::fclose(f);
|
| 1352 |
-
return false;
|
| 1353 |
-
}
|
| 1354 |
-
const uint32_t n = hdr[2];
|
| 1355 |
-
std::vector<std::pair<uint32_t, std::pair<int32_t, int32_t>>> ranked;
|
| 1356 |
-
ranked.reserve(n);
|
| 1357 |
-
for (uint32_t i = 0; i < n; i++) {
|
| 1358 |
-
int32_t l = 0, e = 0;
|
| 1359 |
-
uint32_t c = 0;
|
| 1360 |
-
if (std::fread(&l, sizeof(int32_t), 1, f) != 1) break;
|
| 1361 |
-
if (std::fread(&e, sizeof(int32_t), 1, f) != 1) break;
|
| 1362 |
-
if (std::fread(&c, sizeof(uint32_t), 1, f) != 1) break;
|
| 1363 |
-
ranked.push_back({c, {l, e}});
|
| 1364 |
-
}
|
| 1365 |
-
std::fclose(f);
|
| 1366 |
-
std::sort(ranked.begin(), ranked.end(),
|
| 1367 |
-
[](const auto & a, const auto & b) { return a.first > b.first; });
|
| 1368 |
-
|
| 1369 |
-
hot_.assign((size_t) info_.n_layer * (size_t) info_.n_expert, 0);
|
| 1370 |
-
const int want = cfg_.hot_pin > 0 ? cfg_.hot_pin : (int) ranked.size();
|
| 1371 |
-
n_hot_ = 0;
|
| 1372 |
-
for (const auto & r : ranked) {
|
| 1373 |
-
if (n_hot_ >= want) {
|
| 1374 |
-
break;
|
| 1375 |
-
}
|
| 1376 |
-
const int l = r.second.first, e = r.second.second;
|
| 1377 |
-
if (l < 0 || l >= info_.n_layer || e < 0 || e >= info_.n_expert) {
|
| 1378 |
-
continue;
|
| 1379 |
-
}
|
| 1380 |
-
const size_t idx = (size_t) l * (size_t) info_.n_expert + (size_t) e;
|
| 1381 |
-
if (!hot_[idx]) {
|
| 1382 |
-
hot_[idx] = 1;
|
| 1383 |
-
n_hot_++;
|
| 1384 |
-
}
|
| 1385 |
-
}
|
| 1386 |
-
fprintf(stderr, "[sdq] expert 剖面:%s → 釘選 %d / %zu 個熱門 expert(永不淘汰)\n",
|
| 1387 |
-
path.c_str(), n_hot_, ranked.size());
|
| 1388 |
-
return n_hot_ > 0;
|
| 1389 |
-
}
|
| 1390 |
-
|
| 1391 |
-
void pager::dump_profile(const std::string & path) {
|
| 1392 |
-
if (prof_.empty()) {
|
| 1393 |
-
return;
|
| 1394 |
-
}
|
| 1395 |
-
std::vector<std::pair<uint32_t, std::pair<int32_t, int32_t>>> ranked;
|
| 1396 |
-
ranked.reserve(prof_.size());
|
| 1397 |
-
uint64_t total = 0;
|
| 1398 |
-
for (int l = 0; l < info_.n_layer; l++) {
|
| 1399 |
-
for (int e = 0; e < info_.n_expert; e++) {
|
| 1400 |
-
const uint32_t c = prof_[(size_t) l * (size_t) info_.n_expert + (size_t) e];
|
| 1401 |
-
if (c > 0) {
|
| 1402 |
-
ranked.push_back({c, {l, e}});
|
| 1403 |
-
total += c;
|
| 1404 |
-
}
|
| 1405 |
-
}
|
| 1406 |
-
}
|
| 1407 |
-
std::sort(ranked.begin(), ranked.end(),
|
| 1408 |
-
[](const auto & a, const auto & b) { return a.first > b.first; });
|
| 1409 |
-
std::FILE * f = std::fopen(path.c_str(), "wb");
|
| 1410 |
-
if (!f) {
|
| 1411 |
-
fprintf(stderr, "[sdq] 無法寫出 expert 剖面 %s\n", path.c_str());
|
| 1412 |
-
return;
|
| 1413 |
-
}
|
| 1414 |
-
std::fwrite("SDQP", 1, 4, f);
|
| 1415 |
-
const uint32_t hdr[3] = {1, (uint32_t) info_.n_layer, (uint32_t) ranked.size()};
|
| 1416 |
-
std::fwrite(hdr, sizeof(uint32_t), 3, f);
|
| 1417 |
-
for (const auto & r : ranked) {
|
| 1418 |
-
const int32_t l = r.second.first, e = r.second.second;
|
| 1419 |
-
const uint32_t c = r.first;
|
| 1420 |
-
std::fwrite(&l, sizeof(int32_t), 1, f);
|
| 1421 |
-
std::fwrite(&e, sizeof(int32_t), 1, f);
|
| 1422 |
-
std::fwrite(&c, sizeof(uint32_t), 1, f);
|
| 1423 |
-
}
|
| 1424 |
-
std::fclose(f);
|
| 1425 |
-
// 覆盖率報告:前 K 名可以蓋掉多少請求 → 直接決定 hot_pin 該設多少
|
| 1426 |
-
uint64_t acc = 0;
|
| 1427 |
-
const size_t ks[6] = {512, 1024, 1536, 2048, 3072, 4096};
|
| 1428 |
-
fprintf(stderr, "[sdq] expert 剖面已寫到 %s(%zu 個有被用到,總請求 %llu)\n",
|
| 1429 |
-
path.c_str(), ranked.size(), (unsigned long long) total);
|
| 1430 |
-
size_t ki = 0;
|
| 1431 |
-
for (size_t i = 0; i < ranked.size(); i++) {
|
| 1432 |
-
acc += ranked[i].first;
|
| 1433 |
-
if (ki < 6 && i + 1 == ks[ki]) {
|
| 1434 |
-
fprintf(stderr, "[sdq] 前 %5zu 名涵蓋 %6.2f%% 的請求\n",
|
| 1435 |
-
ks[ki], 100.0 * (double) acc / (double) (total ? total : 1));
|
| 1436 |
-
ki++;
|
| 1437 |
-
}
|
| 1438 |
-
}
|
| 1439 |
-
}
|
| 1440 |
-
|
| 1441 |
-
int pager::find_slot_locked(int il, int ie) {
|
| 1442 |
-
const int64_t key = ((int64_t) il << 20) | (int64_t) ie;
|
| 1443 |
-
auto it = page_.find(key);
|
| 1444 |
-
return it == page_.end() ? -1 : it->second;
|
| 1445 |
-
}
|
| 1446 |
-
|
| 1447 |
-
// 找一個「可以放 (il,ie) 而且 last_use <= stale_before」的槽位。
|
| 1448 |
-
//
|
| 1449 |
-
// 為什麼不能直接用 victim_stale_locked():大小兩種槽位之後,那個函式會掃過**所有**
|
| 1450 |
-
// 槽位(包含小槽位),於是一個需要大槽位的 expert 可能拿到小槽位,
|
| 1451 |
-
// 預取時 read_expert 會寫出槽位邊界 → 靜默把鄰居槽位的資料寫壞。
|
| 1452 |
-
// 這是引進大小兩種槽位時真正會咬人的地方。
|
| 1453 |
-
//
|
| 1454 |
-
// 順序:先看對應尺寸的空槽(從沒用過,最該拿來放預取),
|
| 1455 |
-
// 再掃全部槽位挑最久未用的(放得下的那種尺寸)。
|
| 1456 |
-
int pager::victim_stale_for(int il, int ie, uint64_t stale_before) {
|
| 1457 |
-
const bool need_big = need_big_slot(il, ie);
|
| 1458 |
-
std::vector<int32_t> & first = need_big ? free_big_slots_ : free_slots_;
|
| 1459 |
-
std::vector<int32_t> & second = need_big ? free_slots_ : free_big_slots_;
|
| 1460 |
-
for (std::vector<int32_t> * lst : { &first, &second }) {
|
| 1461 |
-
while (!lst->empty()) {
|
| 1462 |
-
const int32_t s = lst->back();
|
| 1463 |
-
if (slots_[(size_t) s].busy) {
|
| 1464 |
-
break;
|
| 1465 |
-
}
|
| 1466 |
-
if (slots_[(size_t) s].last_use <= stale_before &&
|
| 1467 |
-
slot_accepts(need_big, is_big_slot(s))) {
|
| 1468 |
-
lst->pop_back();
|
| 1469 |
-
return s;
|
| 1470 |
-
}
|
| 1471 |
-
lst->pop_back();
|
| 1472 |
-
}
|
| 1473 |
-
}
|
| 1474 |
-
int64_t best = -1;
|
| 1475 |
-
uint64_t best_clock = UINT64_MAX;
|
| 1476 |
-
for (size_t i = 0; i < n_slots_; i++) {
|
| 1477 |
-
const slot_state & st = slots_[i];
|
| 1478 |
-
if (st.pin > 0 || st.busy || st.last_use > stale_before) {
|
| 1479 |
-
continue;
|
| 1480 |
-
}
|
| 1481 |
-
if (!slot_accepts(need_big, is_big_slot(i))) {
|
| 1482 |
-
continue;
|
| 1483 |
-
}
|
| 1484 |
-
if (st.last_use < best_clock) {
|
| 1485 |
-
best_clock = st.last_use;
|
| 1486 |
-
best = (int64_t) i;
|
| 1487 |
-
}
|
| 1488 |
-
}
|
| 1489 |
-
return (int) best;
|
| 1490 |
-
}
|
| 1491 |
-
|
| 1492 |
-
int pager::victim_stale_locked(uint64_t stale_before) {
|
| 1493 |
-
// 只挑「last_use 很舊」而且沒被 pin 的槽:保護最近的工作集
|
| 1494 |
-
while (!free_slots_.empty()) {
|
| 1495 |
-
const int32_t s = free_slots_.back();
|
| 1496 |
-
if (!slots_[(size_t) s].busy && slots_[(size_t) s].last_use <= stale_before) {
|
| 1497 |
-
free_slots_.pop_back();
|
| 1498 |
-
return s;
|
| 1499 |
-
}
|
| 1500 |
-
if (free_slots_.size() * 4 < n_slots_) {
|
| 1501 |
-
break;
|
| 1502 |
-
}
|
| 1503 |
-
free_slots_.pop_back();
|
| 1504 |
-
}
|
| 1505 |
-
int64_t best = -1;
|
| 1506 |
-
uint64_t best_clock = UINT64_MAX;
|
| 1507 |
-
for (size_t i = 0; i < n_slots_; i++) {
|
| 1508 |
-
const slot_state & st = slots_[i];
|
| 1509 |
-
if (st.pin > 0 || st.busy || st.last_use > stale_before) {
|
| 1510 |
-
continue;
|
| 1511 |
-
}
|
| 1512 |
-
if (st.last_use < best_clock) {
|
| 1513 |
-
best_clock = st.last_use;
|
| 1514 |
-
best = (int64_t) i;
|
| 1515 |
-
}
|
| 1516 |
-
}
|
| 1517 |
-
return (int) best;
|
| 1518 |
-
}
|
| 1519 |
-
|
| 1520 |
-
int pager::acquire(int il, int ie, bool blocking) {
|
| 1521 |
-
if (!ready_) {
|
| 1522 |
-
return -1;
|
| 1523 |
-
}
|
| 1524 |
-
std::unique_lock<std::mutex> lk(mu_);
|
| 1525 |
-
int32_t s = find_slot_locked(il, ie);
|
| 1526 |
-
if (s >= 0 && slots_[s].busy) {
|
| 1527 |
-
// 這個槽位**正在被填入**:另一條執行緒剛 admit 它、資料還沒讀完。
|
| 1528 |
-
//
|
| 1529 |
-
// 這裡一定要等,不能當成命中直接用。否則讀到的是「寫到一半的權重」,
|
| 1530 |
-
// 而且那個錯誤是**非決定性**的 —— 每次執行結果可能不同(實測在
|
| 1531 |
-
// prefill 時會讓同一個提示詞跑出不同的文字)。
|
| 1532 |
-
// busy 的槽位在 admit/victim 那邊本來就會被跳過,所以這裡是唯一
|
| 1533 |
-
// 還能拿到它的路徑,也因此必須在這裡等。
|
| 1534 |
-
const uint64_t t0 = now_us();
|
| 1535 |
-
while (slots_[s].busy) {
|
| 1536 |
-
lk.unlock();
|
| 1537 |
-
std::this_thread::sleep_for(std::chrono::microseconds(200));
|
| 1538 |
-
lk.lock();
|
| 1539 |
-
if (now_us() - t0 > (uint64_t) env_size("SDQ_BUSY_WAIT_MS", 30000) * 1000) {
|
| 1540 |
-
fprintf(stderr,
|
| 1541 |
-
"[sdq] 致命:等待槽位 il=%d ie=%d 讀入超過 %u ms(busy 一直沒解除)\n",
|
| 1542 |
-
il, ie, (unsigned) env_size("SDQ_BUSY_WAIT_MS", 30000));
|
| 1543 |
-
std::fflush(stderr);
|
| 1544 |
-
abort();
|
| 1545 |
-
}
|
| 1546 |
-
}
|
| 1547 |
-
// 讀取可能失敗(那時槽位已被清空)→ 重新走一次正常的 miss 路徑
|
| 1548 |
-
if (find_slot_locked(il, ie) != s) {
|
| 1549 |
-
s = -1;
|
| 1550 |
-
}
|
| 1551 |
-
}
|
| 1552 |
-
if (s >= 0) {
|
| 1553 |
-
slots_[s].last_use = ++clock_;
|
| 1554 |
-
slots_[s].pin++;
|
| 1555 |
-
touch_freq_locked(slots_[(size_t) s]);
|
| 1556 |
-
stats_.expert_requests.fetch_add(1, std::memory_order_relaxed);
|
| 1557 |
-
stats_.expert_hits.fetch_add(1, std::memory_order_relaxed);
|
| 1558 |
-
if (lstats_ && il >= 0 && il < (int) info_.n_layer) {
|
| 1559 |
-
lstats_[il].req.fetch_add(1, std::memory_order_relaxed);
|
| 1560 |
-
lstats_[il].hit.fetch_add(1, std::memory_order_relaxed);
|
| 1561 |
-
}
|
| 1562 |
-
// A prefetch that already finished counts as "used" here. Without this
|
| 1563 |
-
// the finished-before-request case was invisible and prefetch precision
|
| 1564 |
-
// was understated (docs/10 §f).
|
| 1565 |
-
if (slots_[s].from_prefetch) {
|
| 1566 |
-
slots_[s].from_prefetch = false;
|
| 1567 |
-
stats_.prefetch_used.fetch_add(1, std::memory_order_relaxed);
|
| 1568 |
-
stats_.prefetch_bytes_used.fetch_add(slot_size_of(s), std::memory_order_relaxed);
|
| 1569 |
-
}
|
| 1570 |
-
return s;
|
| 1571 |
-
}
|
| 1572 |
-
|
| 1573 |
-
stats_.expert_requests.fetch_add(1, std::memory_order_relaxed);
|
| 1574 |
-
count_request(il, ie);
|
| 1575 |
-
stats_.expert_misses.fetch_add(1, std::memory_order_relaxed);
|
| 1576 |
-
if (lstats_ && il >= 0 && il < (int) info_.n_layer) {
|
| 1577 |
-
lstats_[il].req.fetch_add(1, std::memory_order_relaxed);
|
| 1578 |
-
lstats_[il].miss.fetch_add(1, std::memory_order_relaxed);
|
| 1579 |
-
}
|
| 1580 |
-
|
| 1581 |
-
// 先找一個已經在背景讀入這頁的槽
|
| 1582 |
-
for (size_t i = 0; i < n_slots_; i++) {
|
| 1583 |
-
slot_state & st = slots_[i];
|
| 1584 |
-
if (st.busy && st.layer == il && st.expert == ie) {
|
| 1585 |
-
// 等背景執行緒讀完
|
| 1586 |
-
lk.unlock();
|
| 1587 |
-
const uint64_t t0 = now_us();
|
| 1588 |
-
while (true) {
|
| 1589 |
-
lk.lock();
|
| 1590 |
-
if (!slots_[i].busy) {
|
| 1591 |
-
break;
|
| 1592 |
-
}
|
| 1593 |
-
lk.unlock();
|
| 1594 |
-
std::this_thread::sleep_for(std::chrono::microseconds(200));
|
| 1595 |
-
}
|
| 1596 |
-
s = (int32_t) i;
|
| 1597 |
-
lk.unlock();
|
| 1598 |
-
stats_.prefetch_wait_us.fetch_add(now_us() - t0, std::memory_order_relaxed);
|
| 1599 |
-
std::unique_lock<std::mutex> lk2(mu_);
|
| 1600 |
-
slots_[s].last_use = ++clock_;
|
| 1601 |
-
slots_[s].pin++;
|
| 1602 |
-
stats_.expert_hits.fetch_add(1, std::memory_order_relaxed);
|
| 1603 |
-
stats_.prefetch_used.fetch_add(1, std::memory_order_relaxed);
|
| 1604 |
-
return s;
|
| 1605 |
-
}
|
| 1606 |
-
}
|
| 1607 |
-
|
| 1608 |
-
if (!blocking) {
|
| 1609 |
-
return -1;
|
| 1610 |
-
}
|
| 1611 |
-
|
| 1612 |
-
// 選槽位時必須知道「這個 expert 需要哪一種大小」,否則大 expert 可能被塞進小槽位
|
| 1613 |
-
// (寫出邊界 → 靜默算錯),或者小 expert 佔掉大槽位(浪費 6.8% 的 arena)。
|
| 1614 |
-
s = admit_slot_locked(il, ie);
|
| 1615 |
-
if (s < 0) {
|
| 1616 |
-
// 拿不到槽位:短暫等待別的執行緒解除 pin(每層開頭會 unpin_all)。
|
| 1617 |
-
// 這裡「重試」而不是直接回 -1,因為回 -1 會讓呼叫端**跳過這個 expert**,
|
| 1618 |
-
// 結果是靜默少算一條專家分支 —— 那是最嚴重的錯誤。
|
| 1619 |
-
// 2000 次 × 250 µs = 最多等 0.5 秒;真的撐不到那麼久,代表 arena 被
|
| 1620 |
-
// 預取的 busy 槽位佔住了(實測:跨層預取開啟時會發生)。
|
| 1621 |
-
for (int attempt = 0; attempt < 2000 && s < 0; attempt++) {
|
| 1622 |
-
lk.unlock();
|
| 1623 |
-
std::this_thread::sleep_for(std::chrono::microseconds(250));
|
| 1624 |
-
lk.lock();
|
| 1625 |
-
s = admit_slot_locked(il, ie);
|
| 1626 |
-
}
|
| 1627 |
-
if (s < 0) {
|
| 1628 |
-
fprintf(stderr,
|
| 1629 |
-
"[sdq] 嚴重:找不到能放 il=%d ie=%d(need_big=%d)的槽位,"
|
| 1630 |
-
"大槽 %zu/小槽 %zu\n",
|
| 1631 |
-
il, ie, (int) need_big_slot(il, ie), n_big_, n_small_);
|
| 1632 |
-
// 這裡「回 -1」會讓呼叫端**跳過這個 expert**,結果是靜默少算一條專家分支
|
| 1633 |
-
// —— 那是整個引擎最嚴重的錯誤型態(模型會安靜地給出錯誤答案,而且
|
| 1634 |
-
// 每次執行還不一定一樣)。寧可整個行程死掉,也不要產出「看起來正常」
|
| 1635 |
-
// 的錯誤結果。
|
| 1636 |
-
fprintf(stderr,
|
| 1637 |
-
"[sdq] 致命:無法取得 expert 槽位。為了不給出靜默錯誤的結果,"
|
| 1638 |
-
"直接中止。請回報這個情況(多半是預取把槽位佔成 busy 太久)。\n");
|
| 1639 |
-
std::fflush(stderr);
|
| 1640 |
-
abort();
|
| 1641 |
-
}
|
| 1642 |
-
}
|
| 1643 |
-
slot_state & st = slots_[s];
|
| 1644 |
-
if (st.layer >= 0) {
|
| 1645 |
-
page_.erase(((int64_t) st.layer << 20) | (int64_t) st.expert);
|
| 1646 |
-
stats_.evictions.fetch_add(1, std::memory_order_relaxed);
|
| 1647 |
-
used_.fetch_sub(1, std::memory_order_relaxed);
|
| 1648 |
-
}
|
| 1649 |
-
st.layer = il;
|
| 1650 |
-
st.expert = ie;
|
| 1651 |
-
st.last_use = ++clock_;
|
| 1652 |
-
st.pin = 1;
|
| 1653 |
-
st.is_hot = hot_lookup(il, ie);
|
| 1654 |
-
// **先標 busy,再 publish 到 page_。**
|
| 1655 |
-
// 順序反過來的話(先 publish 再標 busy 中間有一個窗口)另一條執行緒會把它
|
| 1656 |
-
// 當成命中,然後在資料還沒讀完時就拿去做矩陣乘法 → 非決定性的錯誤輸出。
|
| 1657 |
-
// acquire() 看到 busy 會等;admit/victim 本來就會跳過 busy 的槽位。
|
| 1658 |
-
st.busy = true;
|
| 1659 |
-
page_[((int64_t) il << 20) | (int64_t) ie] = s;
|
| 1660 |
-
|
| 1661 |
-
lk.unlock();
|
| 1662 |
-
|
| 1663 |
-
const uint64_t t0 = now_us();
|
| 1664 |
-
const bool ok = read_expert_par(il, ie, slot_ptr(s));
|
| 1665 |
-
const uint64_t dt = now_us() - t0;
|
| 1666 |
-
|
| 1667 |
-
std::unique_lock<std::mutex> lk2(mu_);
|
| 1668 |
-
st.busy = false;
|
| 1669 |
-
if (!ok) {
|
| 1670 |
-
st.layer = -1;
|
| 1671 |
-
st.expert = -1;
|
| 1672 |
-
st.pin = 0;
|
| 1673 |
-
page_.erase(((int64_t) il << 20) | (int64_t) ie);
|
| 1674 |
-
return -1;
|
| 1675 |
-
}
|
| 1676 |
-
used_.fetch_add(1, std::memory_order_relaxed);
|
| 1677 |
-
stats_.demand_wait_us.fetch_add(dt, std::memory_order_relaxed);
|
| 1678 |
-
const layer_desc * L = layer(il);
|
| 1679 |
-
if (L) {
|
| 1680 |
-
const uint64_t nb = (uint64_t) L->experts[(size_t) ie].bytes;
|
| 1681 |
-
stats_.ssb_read_bytes_demand.fetch_add(nb, std::memory_order_relaxed);
|
| 1682 |
-
if (lstats_ && il >= 0 && il < (int) info_.n_layer) { // SDQ-0004
|
| 1683 |
-
lstats_[il].demand_bytes.fetch_add(nb, std::memory_order_relaxed);
|
| 1684 |
-
}
|
| 1685 |
-
}
|
| 1686 |
-
return s;
|
| 1687 |
-
}
|
| 1688 |
-
|
| 1689 |
-
void pager::prefetch_expert(int il, int ie) {
|
| 1690 |
-
if (!ready_ || !cfg_.prefetch) {
|
| 1691 |
-
return;
|
| 1692 |
-
}
|
| 1693 |
-
{
|
| 1694 |
-
std::unique_lock<std::mutex> lk(mu_);
|
| 1695 |
-
if (find_slot_locked(il, ie) >= 0) {
|
| 1696 |
-
stats_.prefetch_duplicate.fetch_add(1, std::memory_order_relaxed);
|
| 1697 |
-
return;
|
| 1698 |
-
}
|
| 1699 |
-
// 預取只能占用「空槽」或「已經很久沒被用到的頁」。
|
| 1700 |
-
// (實測:無差別淘汰會讓命中率從 90% 掉到 27%,而預取使用率是 0%,純粹有害。)
|
| 1701 |
-
//
|
| 1702 |
-
// **槽位一定要選對尺寸**:need_big_slot() 為真的 expert 拿到小槽位會寫出邊界。
|
| 1703 |
-
const uint64_t stale_before = clock_ - (uint64_t) env_size("SDQ_PREFETCH_STALE_TOKENS", 2) * 320;
|
| 1704 |
-
// 預取佇列也不能太深:SSD 讀不夠快時,排在後面的預取早就過期了
|
| 1705 |
-
if (pf_q_.size() > (size_t) env_size("SDQ_PREFETCH_QUEUE", 64)) {
|
| 1706 |
-
return;
|
| 1707 |
-
}
|
| 1708 |
-
// **必須留夠空的槽位給 demand。**
|
| 1709 |
-
// 預取會把槽位標成 busy 並佔著它讀取;大槽位本來就少(工作集下限是
|
| 1710 |
-
// n_big_layers × n_expert_used),如果預取把最後幾個空槽都吃掉,
|
| 1711 |
-
// 下一層的 demand 就會「找不到能放 il=34 ie=114 的槽位」而中止。
|
| 1712 |
-
// (這是 verify 新增的預取壓力測試 [2b] 抓到的實際情況。)
|
| 1713 |
-
// 所以:對應尺寸的空槽少於 n_expert_used 個時,這一筆預取直接不做。
|
| 1714 |
-
const size_t reserve = (size_t) info_.n_expert_used;
|
| 1715 |
-
const size_t n_free_right = need_big_slot(il, ie) ? free_big_slots_.size() : free_slots_.size();
|
| 1716 |
-
if (n_free_right < reserve) {
|
| 1717 |
-
stats_.prefetch_duplicate.fetch_add(1, std::memory_order_relaxed);
|
| 1718 |
-
return;
|
| 1719 |
-
}
|
| 1720 |
-
const int32_t s = victim_stale_for(il, ie, stale_before);
|
| 1721 |
-
if (s < 0) {
|
| 1722 |
-
return;
|
| 1723 |
-
}
|
| 1724 |
-
slot_state & st = slots_[s];
|
| 1725 |
-
if (st.layer >= 0) {
|
| 1726 |
-
page_.erase(((int64_t) st.layer << 20) | (int64_t) st.expert);
|
| 1727 |
-
stats_.evictions.fetch_add(1, std::memory_order_relaxed);
|
| 1728 |
-
used_.fetch_sub(1, std::memory_order_relaxed);
|
| 1729 |
-
}
|
| 1730 |
-
st.layer = il;
|
| 1731 |
-
st.expert = ie;
|
| 1732 |
-
st.busy = true;
|
| 1733 |
-
st.pin = 0;
|
| 1734 |
-
pf_q_.push_back({s, il, ie, now_us()});
|
| 1735 |
-
stats_.prefetch_issued.fetch_add(1, std::memory_order_relaxed);
|
| 1736 |
-
}
|
| 1737 |
-
pf_cv_.notify_one();
|
| 1738 |
-
}
|
| 1739 |
-
|
| 1740 |
-
void pager::pf_worker() {
|
| 1741 |
-
std::unique_lock<std::mutex> lk(pf_mu_);
|
| 1742 |
-
for (;;) {
|
| 1743 |
-
pf_cv_.wait(lk, [this] { return pf_stop_ || !pf_q_.empty(); });
|
| 1744 |
-
if (pf_stop_ && pf_q_.empty()) {
|
| 1745 |
-
return;
|
| 1746 |
-
}
|
| 1747 |
-
const pf_item item = pf_q_.front();
|
| 1748 |
-
pf_q_.pop_front();
|
| 1749 |
-
lk.unlock();
|
| 1750 |
-
|
| 1751 |
-
// 這個槽可能已經被淘汰/重用 → 檢查 generation
|
| 1752 |
-
{
|
| 1753 |
-
std::unique_lock<std::mutex> g(mu_);
|
| 1754 |
-
const slot_state & st = slots_[item.slot];
|
| 1755 |
-
const bool still = st.busy && st.layer == item.il && st.expert == item.ie;
|
| 1756 |
-
if (!still) {
|
| 1757 |
-
lk.lock();
|
| 1758 |
-
continue;
|
| 1759 |
-
}
|
| 1760 |
-
}
|
| 1761 |
-
|
| 1762 |
-
// 預取也走 I/O 段平行:單獨一條執行緒循序讀三段,是這條路徑唯一的瓶頸。
|
| 1763 |
-
const bool ok = cfg_.io_parallel ? read_expert_par(item.il, item.ie, slot_ptr(item.slot))
|
| 1764 |
-
: read_expert(item.il, item.ie, slot_ptr(item.slot));
|
| 1765 |
-
const layer_desc * L = layer(item.il);
|
| 1766 |
-
{
|
| 1767 |
-
std::unique_lock<std::mutex> g(mu_);
|
| 1768 |
-
slot_state & st = slots_[item.slot];
|
| 1769 |
-
if (st.busy && st.layer == item.il && st.expert == item.ie) {
|
| 1770 |
-
st.busy = false;
|
| 1771 |
-
st.last_use = ++clock_;
|
| 1772 |
-
if (ok) {
|
| 1773 |
-
page_[((int64_t) item.il << 20) | (int64_t) item.ie] = item.slot;
|
| 1774 |
-
used_.fetch_add(1, std::memory_order_relaxed);
|
| 1775 |
-
stats_.prefetch_completed.fetch_add(1, std::memory_order_relaxed);
|
| 1776 |
-
// SDQ-0004: tag the page so the demand path can credit it,
|
| 1777 |
-
// and so unused prefetched bytes can be reported as waste.
|
| 1778 |
-
st.from_prefetch = true;
|
| 1779 |
-
if (lstats_ && item.il >= 0 && item.il < (int) info_.n_layer) {
|
| 1780 |
-
lstats_[item.il].prefetch_bytes.fetch_add(
|
| 1781 |
-
(uint64_t) slot_size_of(item.slot), std::memory_order_relaxed);
|
| 1782 |
-
}
|
| 1783 |
-
if (L) {
|
| 1784 |
-
stats_.ssb_read_bytes_prefetch.fetch_add(
|
| 1785 |
-
(uint64_t) L->experts[(size_t) item.ie].bytes, std::memory_order_relaxed);
|
| 1786 |
-
}
|
| 1787 |
-
} else {
|
| 1788 |
-
st.layer = -1;
|
| 1789 |
-
st.expert = -1;
|
| 1790 |
-
}
|
| 1791 |
-
}
|
| 1792 |
-
}
|
| 1793 |
-
lk.lock();
|
| 1794 |
-
}
|
| 1795 |
-
}
|
| 1796 |
-
|
| 1797 |
-
void pager::release(int s) {
|
| 1798 |
-
if (s < 0 || (size_t) s >= n_slots_) {
|
| 1799 |
-
return;
|
| 1800 |
-
}
|
| 1801 |
-
std::lock_guard<std::mutex> lk(mu_);
|
| 1802 |
-
slot_state & st = slots_[(size_t) s];
|
| 1803 |
-
if (st.pin > 0) {
|
| 1804 |
-
st.pin--;
|
| 1805 |
-
}
|
| 1806 |
-
}
|
| 1807 |
-
|
| 1808 |
-
void pager::unpin_all() {
|
| 1809 |
-
std::unique_lock<std::mutex> lk(mu_);
|
| 1810 |
-
for (auto & st : slots_) {
|
| 1811 |
-
st.pin = 0;
|
| 1812 |
-
}
|
| 1813 |
-
}
|
| 1814 |
-
|
| 1815 |
-
void pager::note_used(int il, int ie) {
|
| 1816 |
-
if (il < 0 || ie < 0) {
|
| 1817 |
-
return;
|
| 1818 |
-
}
|
| 1819 |
-
std::lock_guard<std::mutex> lk(hist_mu_);
|
| 1820 |
-
if (il >= (int) cur_used_.size()) {
|
| 1821 |
-
return;
|
| 1822 |
-
}
|
| 1823 |
-
auto & v = cur_used_[(size_t) il];
|
| 1824 |
-
if (std::find(v.begin(), v.end(), ie) == v.end()) {
|
| 1825 |
-
v.push_back(ie);
|
| 1826 |
-
}
|
| 1827 |
-
}
|
| 1828 |
-
|
| 1829 |
-
// 「下一層」預取:只在 layer L 的 op 開頭呼叫一次。
|
| 1830 |
-
//
|
| 1831 |
-
// 為什麼需要這個函式:end_token() 是**整個 token 的 40 層都算完之後**才被呼叫的
|
| 1832 |
-
// (sdq_cli.cpp 在 llama_decode 之後呼叫),所以它排的預取完全沒有和任何計算重疊
|
| 1833 |
-
// —— 量測上 prefetch 使用率只有 0~8 %,等於整條預取路徑是關的。
|
| 1834 |
-
// 在 layer L 開始時就替 layer L+1 排預取,才有一整層的時間(讀+算,約 4 ms)
|
| 1835 |
-
// 可以讓 SSD 讀取躲在後面。
|
| 1836 |
-
//
|
| 1837 |
-
// 預測用的是 prev_used_[il+1](上一個 token 在該層選的專家)—— MoE 的路由在
|
| 1838 |
-
// 鄰近 token 之間非常穩定,這是最便宜且不用訓練的預測器。
|
| 1839 |
-
void pager::prefetch_layer_ahead(int il) {
|
| 1840 |
-
if (!ready_ || !cfg_.prefetch || il < 0 || il >= (int) layers_.size()) {
|
| 1841 |
-
return;
|
| 1842 |
-
}
|
| 1843 |
-
int32_t ids[64];
|
| 1844 |
-
const int k = (int) info_.n_expert_used;
|
| 1845 |
-
if (k <= 0 || k > 64) {
|
| 1846 |
-
return;
|
| 1847 |
-
}
|
| 1848 |
-
predict(il, ids, k);
|
| 1849 |
-
for (int i = 0; i < k; i++) {
|
| 1850 |
-
prefetch_expert(il, ids[i]);
|
| 1851 |
-
}
|
| 1852 |
-
}
|
| 1853 |
-
|
| 1854 |
-
// SDQ-0005: called once per llama_decode() from any front end.
|
| 1855 |
-
//
|
| 1856 |
-
// sdq-chat drives this itself from main(); llama-server does not, which left
|
| 1857 |
-
// tokens at 0, ssd_bytes_per_token undefined and SDQ_STATS_FILE empty. This is
|
| 1858 |
-
// the single place that closes a decode step, so hook it here.
|
| 1859 |
-
//
|
| 1860 |
-
// n_tokens > 1 means prefill: a whole prompt was just paged in. Counting that as
|
| 1861 |
-
// one "token" would make ssd_bytes_per_token meaningless, and feeding it to the
|
| 1862 |
-
// Markov table would poison the transition counts with a whole-batch transition.
|
| 1863 |
-
// So prefill advances the byte accounting by n_tokens but leaves the routing
|
| 1864 |
-
// history alone -- it is not a single-token transition.
|
| 1865 |
-
void pager::note_decode(int n_tokens) {
|
| 1866 |
-
if (!ready_ || n_tokens <= 0) {
|
| 1867 |
-
return;
|
| 1868 |
-
}
|
| 1869 |
-
stats_.tokens.fetch_add((uint64_t) n_tokens, std::memory_order_relaxed);
|
| 1870 |
-
if (n_tokens == 1) {
|
| 1871 |
-
end_token();
|
| 1872 |
-
}
|
| 1873 |
-
dump_stats("decode");
|
| 1874 |
-
}
|
| 1875 |
-
|
| 1876 |
-
void pager::end_token() {
|
| 1877 |
-
if (!ready_) {
|
| 1878 |
-
return;
|
| 1879 |
-
}
|
| 1880 |
-
// 用「上個 token 的選擇」更新一階 Markov 轉移表
|
| 1881 |
-
{
|
| 1882 |
-
std::lock_guard<std::mutex> lk(hist_mu_);
|
| 1883 |
-
for (size_t il = 0; il < cur_used_.size(); il++) {
|
| 1884 |
-
auto & cur = cur_used_[il];
|
| 1885 |
-
if (cur.empty()) {
|
| 1886 |
-
continue;
|
| 1887 |
-
}
|
| 1888 |
-
auto & layer_hist = hist_[il];
|
| 1889 |
-
// 以每層最近一次(或平均)的前一次選擇為前驅
|
| 1890 |
-
if (!prev_used_.empty() && il < prev_used_.size() && !prev_used_[il].empty()) {
|
| 1891 |
-
for (int32_t from : prev_used_[il]) {
|
| 1892 |
-
trans & t = layer_hist[(size_t) from];
|
| 1893 |
-
for (int32_t to : cur) {
|
| 1894 |
-
int found = -1;
|
| 1895 |
-
for (int32_t i = 0; i < t.n; i++) {
|
| 1896 |
-
if (t.to[i] == to) {
|
| 1897 |
-
t.cnt[i]++;
|
| 1898 |
-
found = i;
|
| 1899 |
-
break;
|
| 1900 |
-
}
|
| 1901 |
-
}
|
| 1902 |
-
if (found < 0 && t.n < 64) {
|
| 1903 |
-
t.to[t.n] = to;
|
| 1904 |
-
t.cnt[t.n] = 1;
|
| 1905 |
-
t.n++;
|
| 1906 |
-
}
|
| 1907 |
-
}
|
| 1908 |
-
}
|
| 1909 |
-
}
|
| 1910 |
-
prev_used_[il] = cur;
|
| 1911 |
-
cur.clear();
|
| 1912 |
-
}
|
| 1913 |
-
}
|
| 1914 |
-
|
| 1915 |
-
// 依預測排預取
|
| 1916 |
-
if (!cfg_.prefetch) {
|
| 1917 |
-
return;
|
| 1918 |
-
}
|
| 1919 |
-
int32_t ids[64];
|
| 1920 |
-
for (size_t il = 0; il < layers_.size(); il++) {
|
| 1921 |
-
const int k = (int) info_.n_expert_used;
|
| 1922 |
-
predict((int) il, ids, k);
|
| 1923 |
-
for (int i = 0; i < k; i++) {
|
| 1924 |
-
prefetch_expert((int) il, ids[i]);
|
| 1925 |
-
}
|
| 1926 |
-
}
|
| 1927 |
-
}
|
| 1928 |
-
|
| 1929 |
-
void pager::predict(int il, int32_t * out, int k) {
|
| 1930 |
-
if (il < 0 || il >= (int) prev_used_.size()) {
|
| 1931 |
-
for (int i = 0; i < k; i++) {
|
| 1932 |
-
out[i] = i;
|
| 1933 |
-
}
|
| 1934 |
-
return;
|
| 1935 |
-
}
|
| 1936 |
-
// 以最近一次選擇為前驅;沒有歷史就直接沿用上一次的選擇
|
| 1937 |
-
const std::vector<int32_t> & prev = prev_used_[(size_t) il];
|
| 1938 |
-
std::vector<std::pair<uint32_t, int32_t>> scored;
|
| 1939 |
-
if (prev.empty()) {
|
| 1940 |
-
for (int i = 0; i < k; i++) {
|
| 1941 |
-
out[i] = i;
|
| 1942 |
-
}
|
| 1943 |
-
return;
|
| 1944 |
-
}
|
| 1945 |
-
for (int32_t from : prev) {
|
| 1946 |
-
const trans & t = hist_[(size_t) il][(size_t) from];
|
| 1947 |
-
for (int32_t i = 0; i < t.n; i++) {
|
| 1948 |
-
scored.push_back({t.cnt[i], t.to[i]});
|
| 1949 |
-
}
|
| 1950 |
-
}
|
| 1951 |
-
if (scored.empty()) {
|
| 1952 |
-
for (size_t i = 0; i < prev.size() && i < (size_t) k; i++) {
|
| 1953 |
-
out[i] = prev[i];
|
| 1954 |
-
}
|
| 1955 |
-
for (int i = (int) prev.size(); i < k; i++) {
|
| 1956 |
-
out[i] = i;
|
| 1957 |
-
}
|
| 1958 |
-
return;
|
| 1959 |
-
}
|
| 1960 |
-
std::sort(scored.begin(), scored.end(), [](auto & a, auto & b) {
|
| 1961 |
-
return a.first > b.first;
|
| 1962 |
-
});
|
| 1963 |
-
for (int i = 0; i < k && i < (int) scored.size(); i++) {
|
| 1964 |
-
out[i] = scored[(size_t) i].second;
|
| 1965 |
-
}
|
| 1966 |
-
}
|
| 1967 |
-
|
| 1968 |
-
double pager::hit_rate() const {
|
| 1969 |
-
const uint64_t req = stats_.expert_requests.load();
|
| 1970 |
-
const uint64_t hit = stats_.expert_hits.load();
|
| 1971 |
-
return req ? (double) hit / (double) req : 0.0;
|
| 1972 |
-
}
|
| 1973 |
-
|
| 1974 |
-
void pager::shutdown() {
|
| 1975 |
-
if (!ready_) {
|
| 1976 |
-
return;
|
| 1977 |
-
}
|
| 1978 |
-
if (!profile_out_.empty()) {
|
| 1979 |
-
std::lock_guard<std::mutex> lk(mu_);
|
| 1980 |
-
dump_profile(profile_out_);
|
| 1981 |
-
}
|
| 1982 |
-
if (pf_.joinable()) {
|
| 1983 |
-
{
|
| 1984 |
-
std::unique_lock<std::mutex> lk(pf_mu_);
|
| 1985 |
-
pf_stop_ = true;
|
| 1986 |
-
}
|
| 1987 |
-
pf_cv_.notify_all();
|
| 1988 |
-
pf_.join();
|
| 1989 |
-
}
|
| 1990 |
-
io_pool_stop();
|
| 1991 |
-
for (auto & fd : shard_fd_) {
|
| 1992 |
-
if (fd >= 0) {
|
| 1993 |
-
close(fd);
|
| 1994 |
-
}
|
| 1995 |
-
}
|
| 1996 |
-
for (auto & fd : shard_fd_direct_) {
|
| 1997 |
-
if (fd >= 0) {
|
| 1998 |
-
close(fd);
|
| 1999 |
-
}
|
| 2000 |
-
}
|
| 2001 |
-
shard_fd_.clear();
|
| 2002 |
-
shard_fd_direct_.clear();
|
| 2003 |
-
ready_ = false;
|
| 2004 |
-
}
|
| 2005 |
-
|
| 2006 |
-
pager::~pager() {
|
| 2007 |
-
shutdown();
|
| 2008 |
-
if (arena_) {
|
| 2009 |
-
// 注意:arena 是 posix_memalign 配置的,必須用 free() 釋放,
|
| 2010 |
-
// 用 munmap() 會破壞 glibc 的 heap(之後任何 free 都會報 double free)。
|
| 2011 |
-
free(arena_);
|
| 2012 |
-
arena_ = nullptr;
|
| 2013 |
-
}
|
| 2014 |
-
}
|
| 2015 |
-
|
| 2016 |
-
static double pct(uint64_t a, uint64_t b) {
|
| 2017 |
-
return b ? 100.0 * (double) a / (double) b : 0.0;
|
| 2018 |
-
}
|
| 2019 |
-
|
| 2020 |
-
void pager::print_summary() const {
|
| 2021 |
-
if (!ready_) {
|
| 2022 |
-
return;
|
| 2023 |
-
}
|
| 2024 |
-
// bailingmoe3 的第 0 層是 dense FFN(沒有 expert),要抓第一個真的有分頁的層
|
| 2025 |
-
const layer_desc * L0 = nullptr;
|
| 2026 |
-
int first_moe = -1;
|
| 2027 |
-
for (size_t i = 0; i < layers_.size(); i++) {
|
| 2028 |
-
if (!layers_[i].experts.empty()) {
|
| 2029 |
-
L0 = &layers_[i];
|
| 2030 |
-
first_moe = (int) i;
|
| 2031 |
-
break;
|
| 2032 |
-
}
|
| 2033 |
-
}
|
| 2034 |
-
if (L0 && !L0->experts.empty()) {
|
| 2035 |
-
const expert_desc & e = L0->experts[0];
|
| 2036 |
-
fprintf(stderr, "[sdq] page-map: n_layer=%lld n_expert=%lld n_expert_used=%lld n_embd=%lld n_ff_exp=%lld"
|
| 2037 |
-
" | L%dE0 gate=%llu/%llu up=%llu/%llu down=%llu/%llu (off/size) ne0=%lld ne1=%lld d_ne0=%lld d_ne1=%lld\n",
|
| 2038 |
-
(long long) info_.n_layer, (long long) info_.n_expert, (long long) info_.n_expert_used,
|
| 2039 |
-
(long long) info_.n_embd, (long long) info_.n_ff_exp, first_moe,
|
| 2040 |
-
(unsigned long long) e.seg[part::GATE].off, (unsigned long long) e.seg[part::GATE].size,
|
| 2041 |
-
(unsigned long long) e.seg[part::UP].off, (unsigned long long) e.seg[part::UP].size,
|
| 2042 |
-
(unsigned long long) e.seg[part::DOWN].off, (unsigned long long) e.seg[part::DOWN].size,
|
| 2043 |
-
(long long) e.ne0, (long long) e.ne1, (long long) e.down_ne0, (long long) e.down_ne1);
|
| 2044 |
-
}
|
| 2045 |
-
fprintf(stderr,
|
| 2046 |
-
"[sdq] expert pager ready: %s\n"
|
| 2047 |
-
"[sdq] 熱門 expert 釘選:%d 個%s\n"
|
| 2048 |
-
"[sdq] slot=%.2f MiB (%zu B) slots=%zu arena=%.2f GiB (RAM 實測 RSS=%.2f GiB)\n",
|
| 2049 |
-
cfg_.model_path.c_str(),
|
| 2050 |
-
n_hot_, n_hot_ > 0 ? "(永不淘汰)" : "(無剖面,用動態 LRU/LFU)",
|
| 2051 |
-
slot_bytes_ / 1048576.0, slot_bytes_, n_slots_, arena_bytes_ / 1073741824.0,
|
| 2052 |
-
process_rss_bytes() / 1073741824.0);
|
| 2053 |
-
if (n_big_) {
|
| 2054 |
-
// 大小兩種槽位:把配置結果講清楚,否則只看「slots=N」會誤以為每個槽一樣大。
|
| 2055 |
-
fprintf(stderr,
|
| 2056 |
-
"[sdq] 槽位分兩種:小 %.2f MiB × %zu + 大 %.2f MiB × %zu(平均 %.2f MiB)\n",
|
| 2057 |
-
small_bytes_ / 1048576.0, n_small_,
|
| 2058 |
-
big_bytes_ / 1048576.0, n_big_,
|
| 2059 |
-
(double) arena_bytes_ / (double) (n_slots_ ? n_slots_ : 1) / 1048576.0);
|
| 2060 |
-
}
|
| 2061 |
-
fprintf(stderr,
|
| 2062 |
-
"[sdq] prefetch=%s fadvise_dontneed=%d\n",
|
| 2063 |
-
cfg_.prefetch ? "on" : "off", (int) cfg_.fadvise_dontneed);
|
| 2064 |
-
}
|
| 2065 |
-
|
| 2066 |
-
void pager::dump_stats(const char * phase) {
|
| 2067 |
-
if (cfg_.stats_path.empty()) {
|
| 2068 |
-
return;
|
| 2069 |
-
}
|
| 2070 |
-
const uint64_t tokens = stats_.tokens.load();
|
| 2071 |
-
const uint64_t req = stats_.expert_requests.load();
|
| 2072 |
-
const uint64_t hit = stats_.expert_hits.load();
|
| 2073 |
-
const uint64_t issued = stats_.prefetch_issued.load();
|
| 2074 |
-
const uint64_t used = stats_.prefetch_used.load();
|
| 2075 |
-
const uint64_t b_dem = stats_.ssb_read_bytes_demand.load();
|
| 2076 |
-
const uint64_t b_pre = stats_.ssb_read_bytes_prefetch.load();
|
| 2077 |
-
// SDQ-0004: prefetched bytes that were consumed, so the rest is real waste.
|
| 2078 |
-
const uint64_t b_pre_used = stats_.prefetch_bytes_used.load();
|
| 2079 |
-
|
| 2080 |
-
std::ostringstream o;
|
| 2081 |
-
o << "{\"phase\":\"" << phase << "\""
|
| 2082 |
-
<< ",\"tokens\":" << tokens
|
| 2083 |
-
<< ",\"expert_requests\":" << req
|
| 2084 |
-
<< ",\"expert_hits\":" << hit
|
| 2085 |
-
<< ",\"expert_misses\":" << (req - hit)
|
| 2086 |
-
<< ",\"hit_rate\":" << pct(hit, req)
|
| 2087 |
-
<< ",\"ssb_demand_bytes\":" << b_dem
|
| 2088 |
-
<< ",\"ssb_prefetch_bytes\":" << b_pre
|
| 2089 |
-
<< ",\"ssb_total_bytes\":" << (b_dem + b_pre)
|
| 2090 |
-
<< ",\"ssd_bytes_per_token\":" << (tokens ? (double) (b_dem + b_pre) / (double) tokens : 0.0)
|
| 2091 |
-
<< ",\"prefetch_issued\":" << issued
|
| 2092 |
-
<< ",\"prefetch_used\":" << used
|
| 2093 |
-
<< ",\"prefetch_used_rate\":" << pct(used, issued)
|
| 2094 |
-
// SDQ-0004: prefetch waste. b_pre counts every byte the background reader
|
| 2095 |
-
// pulled in; b_pre_used counts the ones a demand request actually hit.
|
| 2096 |
-
// The difference is what made docs/07's negative result worse than "slower".
|
| 2097 |
-
<< ",\"prefetch_bytes_used\":" << b_pre_used
|
| 2098 |
-
<< ",\"prefetch_bytes_wasted\":" << (b_pre > b_pre_used ? b_pre - b_pre_used : 0)
|
| 2099 |
-
<< ",\"prefetch_waste_rate\":" << pct(b_pre > b_pre_used ? b_pre - b_pre_used : 0, b_pre)
|
| 2100 |
-
<< ",\"prefetch_duplicate\":" << stats_.prefetch_duplicate.load()
|
| 2101 |
-
<< ",\"evictions\":" << stats_.evictions.load()
|
| 2102 |
-
<< ",\"demand_wait_us\":" << stats_.demand_wait_us.load()
|
| 2103 |
-
<< ",\"prefetch_wait_us\":" << stats_.prefetch_wait_us.load()
|
| 2104 |
-
<< ",\"arena_bytes\":" << arena_bytes_
|
| 2105 |
-
// 大小兩種槽位之後,「用掉幾個槽位」不能直接乘 slot_bytes_(那是大的那種)。
|
| 2106 |
-
// dump_stats 只在階段轉換時呼叫一次,遍歷兩千幾個槽位的成本可以忽略。
|
| 2107 |
-
<< ",\"arena_used_bytes\":" << used_bytes_snapshot()
|
| 2108 |
-
<< ",\"rss_bytes\":" << process_rss_bytes()
|
| 2109 |
-
<< ",\"n_slots\":" << n_slots_
|
| 2110 |
-
<< ",\"n_small_slots\":" << n_small_
|
| 2111 |
-
<< ",\"n_big_slots\":" << n_big_
|
| 2112 |
-
<< ",\"small_slot_bytes\":" << small_bytes_
|
| 2113 |
-
<< ",\"big_slot_bytes\":" << big_bytes_
|
| 2114 |
-
<< ",\"slot_bytes\":" << slot_bytes_
|
| 2115 |
-
<< ",\"n_moe_layers\":" << n_moe_layers_
|
| 2116 |
-
<< ",\"expert_bytes_total\":" << expert_bytes_total_
|
| 2117 |
-
<< ",\"arch\":\"" << info_.arch << "\""
|
| 2118 |
-
<< ",\"name\":\"" << info_.name << "\""
|
| 2119 |
-
// SDQ-0004: per-layer breakdown. docs/10 needs this to tell "the device is
|
| 2120 |
-
// slow" apart from "one layer keeps evicting everyone else's experts".
|
| 2121 |
-
<< ",\"per_layer\":[";
|
| 2122 |
-
for (int64_t il = 0; lstats_ && il < (int64_t) info_.n_layer; il++) {
|
| 2123 |
-
const uint64_t lreq = lstats_[il].req.load();
|
| 2124 |
-
const uint64_t lhit = lstats_[il].hit.load();
|
| 2125 |
-
o << (il ? "," : "")
|
| 2126 |
-
<< "{\"layer\":" << il
|
| 2127 |
-
<< ",\"req\":" << lreq
|
| 2128 |
-
<< ",\"hit\":" << lhit
|
| 2129 |
-
<< ",\"miss\":" << (lreq - lhit)
|
| 2130 |
-
<< ",\"hit_rate\":" << pct(lhit, lreq)
|
| 2131 |
-
<< ",\"demand_bytes\":" << lstats_[il].demand_bytes.load()
|
| 2132 |
-
<< ",\"prefetch_bytes\":" << lstats_[il].prefetch_bytes.load()
|
| 2133 |
-
<< "}";
|
| 2134 |
-
}
|
| 2135 |
-
o << "]" << "}";
|
| 2136 |
-
std::ofstream f(cfg_.stats_path, std::ios::app);
|
| 2137 |
-
if (f) {
|
| 2138 |
-
f << o.str() << "\n";
|
| 2139 |
-
}
|
| 2140 |
-
}
|
| 2141 |
-
|
| 2142 |
-
} // namespace sdq
|
| 2143 |
-
namespace sdq {
|
| 2144 |
-
// 由 CLI 呼叫:依環境變數/參數初始化 pager
|
| 2145 |
-
bool sdq_pager_active_for_layer(int il) {
|
| 2146 |
-
pager & P = pager::instance();
|
| 2147 |
-
if (!P.ready() || P.layer(il) == nullptr) {
|
| 2148 |
-
return false;
|
| 2149 |
-
}
|
| 2150 |
-
// 除錯用:SDQ_MOE_LO / SDQ_MOE_HI 可以只讓部分層走分頁路徑,
|
| 2151 |
-
// 用來二分搜尋第一個發生數值差異的層。
|
| 2152 |
-
static const int lo = [] { const char * e = getenv("SDQ_MOE_LO"); return e ? atoi(e) : 0; }();
|
| 2153 |
-
static const int hi = [] { const char * e = getenv("SDQ_MOE_HI"); return e ? atoi(e) : 1 << 30; }();
|
| 2154 |
-
if (il < lo || il >= hi) {
|
| 2155 |
-
return false;
|
| 2156 |
-
}
|
| 2157 |
-
// SDQ_MOE_SKIP=3,7:這些層即使在 [lo,hi) 內也走原生路徑。
|
| 2158 |
-
// 搭配 SDQ_BUFT_KEEP(讓該層的 expert 留在 RAM)就能做同一層的 A/B 比對。
|
| 2159 |
-
static const std::set<int> skip = [] {
|
| 2160 |
-
std::set<int> s;
|
| 2161 |
-
const char * e = getenv("SDQ_MOE_SKIP");
|
| 2162 |
-
if (e) {
|
| 2163 |
-
for (const char * p = e; *p; ) {
|
| 2164 |
-
const char * q = strchr(p, ',');
|
| 2165 |
-
const size_t n = q ? (size_t) (q - p) : strlen(p);
|
| 2166 |
-
if (n) {
|
| 2167 |
-
s.insert(atoi(std::string(p, n).c_str()));
|
| 2168 |
-
}
|
| 2169 |
-
if (!q) {
|
| 2170 |
-
break;
|
| 2171 |
-
}
|
| 2172 |
-
p = q + 1;
|
| 2173 |
-
}
|
| 2174 |
-
}
|
| 2175 |
-
return s;
|
| 2176 |
-
}();
|
| 2177 |
-
return skip.find(il) == skip.end();
|
| 2178 |
-
}
|
| 2179 |
-
|
| 2180 |
-
void sdq_on_decode(int n_tokens) {
|
| 2181 |
-
auto & inst = pager::instance();
|
| 2182 |
-
if (inst.ready()) {
|
| 2183 |
-
inst.note_decode(n_tokens);
|
| 2184 |
-
}
|
| 2185 |
-
}
|
| 2186 |
-
|
| 2187 |
-
bool sdq_init(const std::string & model_path) {
|
| 2188 |
-
config c;
|
| 2189 |
-
c.model_path = model_path;
|
| 2190 |
-
c.arena_bytes = env_size("SDQ_ARENA_MB", 0) << 20;
|
| 2191 |
-
c.ram_budget_bytes = env_size("SDQ_RAM_BUDGET_MB", 0) << 20;
|
| 2192 |
-
c.slot_bytes = env_size("SDQ_SLOT_KB", 0) << 10;
|
| 2193 |
-
// 預設關閉。docs/07 的實測是「開 5.68 / 關 6.20 tok/s」的負結果,而且
|
| 2194 |
-
// 預取中的槽位會讓 acquire() 靜默跳過 expert,曾造成輸出不一致(stage S7)。
|
| 2195 |
-
// 這裡原本是 true,等於讓那個負結果從來沒有真正生效。
|
| 2196 |
-
c.prefetch = env_flag("SDQ_PREFETCH", false);
|
| 2197 |
-
// ---- OPT-1:熱門 expert 剖面(Strata 式)----
|
| 2198 |
-
if (const char * pi = getenv("SDQ_PROFILE_IN")) { c.profile_in = pi; }
|
| 2199 |
-
if (const char * po = getenv("SDQ_PROFILE_OUT")) { c.profile_out = po; }
|
| 2200 |
-
c.hot_pin = (int) env_size("SDQ_HOT_PIN", 0);
|
| 2201 |
-
c.admit_min_hits = (int) env_size("SDQ_ADMIT_MIN_HITS", 1);
|
| 2202 |
-
c.fadvise_dontneed = env_flag("SDQ_FADV_DONTNEED", true);
|
| 2203 |
-
c.io_parallel = env_flag("SDQ_IO_PARALLEL", true);
|
| 2204 |
-
// I/O 讀取執行緒數。sdq-chat 會在載入模型「之前」自己 setenv 這個值
|
| 2205 |
-
// (3 × 計算執行緒,上限 48,docs/05 量到序列 3.79 → 24 條 6.68 tok/s)。
|
| 2206 |
-
// 但 cfg_.n_threads 從來沒有被賦值,所以任何沒有自行設定的前端
|
| 2207 |
-
// (llama-server 就��)會拿到 0,然後在 io_pool_start() 被夾到下限 4 條 ——
|
| 2208 |
-
// 等於一直跑在「序列讀」那一端。改成在 pager 自己算,量測與 sdq-chat 一致。
|
| 2209 |
-
if (!getenv("SDQ_IO_THREADS")) {
|
| 2210 |
-
const int hw = (int) std::max(1u, std::thread::hardware_concurrency());
|
| 2211 |
-
const int compute = hw < 8 ? hw : 8; // 與 sdq-chat 同一個上限 8
|
| 2212 |
-
c.io_threads = compute * 3;
|
| 2213 |
-
} else {
|
| 2214 |
-
c.io_threads = (int) env_size("SDQ_IO_THREADS", 0);
|
| 2215 |
-
}
|
| 2216 |
-
c.io_split = (int) env_size("SDQ_IO_SPLIT", 1);
|
| 2217 |
-
c.verbose = env_flag("SDQ_VERBOSE", false);
|
| 2218 |
-
const char * sp = getenv("SDQ_STATS_FILE");
|
| 2219 |
-
if (sp) {
|
| 2220 |
-
c.stats_path = sp;
|
| 2221 |
-
}
|
| 2222 |
-
sdq_set_config(c);
|
| 2223 |
-
try {
|
| 2224 |
-
pager::instance().init(c, nullptr);
|
| 2225 |
-
} catch (const std::exception & e) {
|
| 2226 |
-
fprintf(stderr, "[sdq] pager 初始化失敗:%s\n", e.what());
|
| 2227 |
-
return false;
|
| 2228 |
-
}
|
| 2229 |
-
// 長駐前端(llama-server)不會呼叫 sdq_shutdown();用 atexit 在收尾時補上摘要。
|
| 2230 |
-
// 註冊時機必須在 pager::instance() 建構之後:static 物件的析構與 atexit 依
|
| 2231 |
-
// 「後註冊者先執行」,反過來就會在 pager 已經析構後才跑這段(讀到已釋放的物件)。
|
| 2232 |
-
static std::once_flag once_exit;
|
| 2233 |
-
std::call_once(once_exit, [] { std::atexit([] { sdq_shutdown(); }); });
|
| 2234 |
-
return pager::instance().ready();
|
| 2235 |
-
}
|
| 2236 |
-
|
| 2237 |
-
|
| 2238 |
-
// ---------------------------------------------------------------- 除錯:dump 任意 F32 張量
|
| 2239 |
-
// 目的:讓「原生 mul_mat_id 路徑」與「SDQ 分頁路徑」輸出同一層的 MoE 結果,
|
| 2240 |
-
// 兩邊各自 dump 一次就能直接 A/B 對照,判定到底是哪一邊算錯。
|
| 2241 |
-
namespace {
|
| 2242 |
-
struct dump_arg { std::string path; };
|
| 2243 |
-
|
| 2244 |
-
void dump_op(ggml_tensor * dst, int ith, int /*nth*/, void * userdata) {
|
| 2245 |
-
if (ith != 0) {
|
| 2246 |
-
return;
|
| 2247 |
-
}
|
| 2248 |
-
const ggml_tensor * src = dst->src[0];
|
| 2249 |
-
dump_arg * a = (dump_arg *) userdata;
|
| 2250 |
-
FILE * f = fopen(a->path.c_str(), "wb");
|
| 2251 |
-
if (!f) {
|
| 2252 |
-
fprintf(stderr, "[sdq-dump] 無法開啟 %s\n", a->path.c_str());
|
| 2253 |
-
return;
|
| 2254 |
-
}
|
| 2255 |
-
const int64_t meta[6] = { src->ne[0], src->ne[1], src->ne[2], src->ne[3],
|
| 2256 |
-
(int64_t) ggml_nbytes(src), 0 };
|
| 2257 |
-
fwrite(meta, sizeof(int64_t), 6, f);
|
| 2258 |
-
// 只支援 F32 contiguous;其他情況就明確報錯,不要假設
|
| 2259 |
-
if (src->type != GGML_TYPE_F32 || !ggml_is_contiguous(src)) {
|
| 2260 |
-
fprintf(stderr, "[sdq-dump] %s 不是 contiguous F32(type=%d)\n",
|
| 2261 |
-
a->path.c_str(), (int) src->type);
|
| 2262 |
-
} else {
|
| 2263 |
-
fwrite(src->data, sizeof(float), (size_t) (ggml_nelements(src)), f);
|
| 2264 |
-
}
|
| 2265 |
-
fclose(f);
|
| 2266 |
-
fprintf(stderr, "[sdq-dump] %s (%lld 個元素)\n", a->path.c_str(),
|
| 2267 |
-
(long long) ggml_nelements(src));
|
| 2268 |
-
}
|
| 2269 |
-
} // namespace
|
| 2270 |
-
|
| 2271 |
-
ggml_tensor * sdq_maybe_dump_tensor(ggml_context * ctx, ggml_tensor * t, int il) {
|
| 2272 |
-
const char * prefix = getenv("SDQ_MOE_OUT_DUMP");
|
| 2273 |
-
if (!prefix || !t) {
|
| 2274 |
-
return t;
|
| 2275 |
-
}
|
| 2276 |
-
int layer = getenv("SDQ_MOE_OUT_DUMP_LAYER") ? atoi(getenv("SDQ_MOE_OUT_DUMP_LAYER")) : -1;
|
| 2277 |
-
if (layer >= 0 && il != layer) {
|
| 2278 |
-
return t;
|
| 2279 |
-
}
|
| 2280 |
-
dump_arg * a = new dump_arg{ std::string(prefix) + "." + std::to_string(il) };
|
| 2281 |
-
return ggml_custom_4d(ctx, GGML_TYPE_F32, t->ne[0], t->ne[1], t->ne[2], t->ne[3],
|
| 2282 |
-
&t, 1, dump_op, 1, a);
|
| 2283 |
-
}
|
| 2284 |
-
|
| 2285 |
-
void sdq_shutdown() {
|
| 2286 |
-
pager & P = pager::instance();
|
| 2287 |
-
sdq_print_time_profile();
|
| 2288 |
-
P.dump_stats("shutdown");
|
| 2289 |
-
P.print_summary();
|
| 2290 |
-
P.shutdown();
|
| 2291 |
-
}
|
| 2292 |
-
} // namespace sdq
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sdq-ling/sdq-ling/sdq_pager.h
DELETED
|
@@ -1,471 +0,0 @@
|
|
| 1 |
-
// SDQ expert pager:SSD → RAM → CPU 的 MoE 專家分層快取
|
| 2 |
-
//
|
| 3 |
-
// 設計要點
|
| 4 |
-
// * 分頁單位 = 一個 expert(layer, expert id),大小 ~1.8 MB(三個矩陣:gate/up/down)
|
| 5 |
-
// * RAM 端是一個固定槽位的 arena(slab),每槽 2 MiB,O(1) 配置、不碎片化
|
| 6 |
-
// * 淘汰策略:LRU + 本 token 使用中(pinned)的保護
|
| 7 |
-
// * 預取:依「上一個 token 的專家選擇」用一階 Markov 轉移預測下一個 token 的專家,
|
| 8 |
-
// 交給背景執行緒 pread(),量測「預取命中率 / 預取浪費量」
|
| 9 |
-
// * 指標:hit/miss、SSD 讀取位元組、demand 停滯時間、prefetch 領先時間、arena 使用量
|
| 10 |
-
//
|
| 11 |
-
// 不使用 swap:所有權重資料只存在 arena(實體 RAM)與檔案(SSD)兩處。
|
| 12 |
-
|
| 13 |
-
#pragma once
|
| 14 |
-
|
| 15 |
-
#include <ggml.h>
|
| 16 |
-
#include <ggml-backend.h>
|
| 17 |
-
#include "llama.h"
|
| 18 |
-
|
| 19 |
-
#include <atomic>
|
| 20 |
-
#include <chrono>
|
| 21 |
-
#include <condition_variable>
|
| 22 |
-
#include <cstddef>
|
| 23 |
-
#include <cstdint>
|
| 24 |
-
#include <deque>
|
| 25 |
-
#include <memory>
|
| 26 |
-
#include <mutex>
|
| 27 |
-
#include <string>
|
| 28 |
-
#include <thread>
|
| 29 |
-
#include <unordered_map>
|
| 30 |
-
#include <vector>
|
| 31 |
-
|
| 32 |
-
namespace sdq {
|
| 33 |
-
|
| 34 |
-
// ---------------------------------------------------------------- GGUF 讀取
|
| 35 |
-
struct gguf_info {
|
| 36 |
-
int64_t n_layer = 0;
|
| 37 |
-
int64_t n_expert = 0;
|
| 38 |
-
int64_t n_expert_used = 0;
|
| 39 |
-
int64_t n_embd = 0;
|
| 40 |
-
int64_t n_ff_exp = 0;
|
| 41 |
-
int64_t n_ff_shexp = 0;
|
| 42 |
-
int64_t n_head = 0;
|
| 43 |
-
int64_t n_head_kv = 0;
|
| 44 |
-
int64_t n_embd_head_k = 0;
|
| 45 |
-
int64_t n_embd_head_v = 0;
|
| 46 |
-
int64_t context_length = 0;
|
| 47 |
-
int64_t full_attention_interval = 4;
|
| 48 |
-
// bailingmoe3 / Ling-3.0-flash
|
| 49 |
-
int64_t n_layer_dense_lead = 0;
|
| 50 |
-
int64_t n_expert_groups = 0;
|
| 51 |
-
int64_t n_expert_group_used = 0;
|
| 52 |
-
int64_t is_recr[512] = {0};
|
| 53 |
-
std::string arch;
|
| 54 |
-
std::string name;
|
| 55 |
-
};
|
| 56 |
-
|
| 57 |
-
// 一個 expert 槽位:gate/up/down 三段(有些模型把 gate+up 合成 gate_up)
|
| 58 |
-
struct seg_desc {
|
| 59 |
-
uint64_t off = 0; // 在「所屬 shard 檔案內」的位元組偏移
|
| 60 |
-
uint64_t size = 0; // 位元組數
|
| 61 |
-
// sharded GGUF:同一層的三段不一定在同一個檔案裡(unsplit 後 100+ GB 的模型一定會被切),
|
| 62 |
-
// 所以每一段都要記得自己屬於哪一個 shard,pread 才知道要開哪個 fd。
|
| 63 |
-
int shard = 0;
|
| 64 |
-
};
|
| 65 |
-
|
| 66 |
-
enum part : int { GATE = 0, UP = 1, DOWN = 2, COUNT = 3 };
|
| 67 |
-
|
| 68 |
-
struct expert_desc {
|
| 69 |
-
seg_desc seg[3];
|
| 70 |
-
int64_t slot_off[3] = {0, 0, 0}; // 三段在 arena 槽位內的相對偏移
|
| 71 |
-
int64_t ne0 = 0; // gate/up row 長度(= n_embd)
|
| 72 |
-
int64_t ne1 = 0; // gate/up row 數(= n_ff_exp)
|
| 73 |
-
int64_t down_ne0 = 0; // down row 長度(= n_ff_exp)
|
| 74 |
-
int64_t down_ne1 = 0; // down row 數(= n_embd)
|
| 75 |
-
ggml_type type[3] = {GGML_TYPE_COUNT, GGML_TYPE_COUNT, GGML_TYPE_COUNT};
|
| 76 |
-
int64_t bytes = 0; // 三段總大小
|
| 77 |
-
bool fused_gate_up = false;
|
| 78 |
-
};
|
| 79 |
-
|
| 80 |
-
// 一層的所有 expert 描述(40 層 × 256 個)
|
| 81 |
-
struct layer_desc {
|
| 82 |
-
std::vector<expert_desc> experts;
|
| 83 |
-
int64_t bytes_per_expert = 0;
|
| 84 |
-
// 這個層的 expert 比「大多數層」大(Q4_K/Q5_K vs Q6_K)→ 需要大槽位。
|
| 85 |
-
// 不然的話所有槽位都得按最大的那種配,白白浪費 6.8% 的 arena。
|
| 86 |
-
bool needs_big_slot = false;
|
| 87 |
-
};
|
| 88 |
-
|
| 89 |
-
// ---------------------------------------------------------------- 指標
|
| 90 |
-
struct stats {
|
| 91 |
-
std::atomic<uint64_t> expert_requests{0}; // 每個 (layer, token) 需要幾個 expert
|
| 92 |
-
std::atomic<uint64_t> expert_hits{0};
|
| 93 |
-
std::atomic<uint64_t> expert_misses{0};
|
| 94 |
-
std::atomic<uint64_t> ssb_read_bytes_demand{0};
|
| 95 |
-
std::atomic<uint64_t> ssb_read_bytes_prefetch{0};
|
| 96 |
-
std::atomic<uint64_t> prefetch_issued{0};
|
| 97 |
-
std::atomic<uint64_t> prefetch_completed{0};
|
| 98 |
-
std::atomic<uint64_t> prefetch_used{0}; // 預取後真的被用到
|
| 99 |
-
std::atomic<uint64_t> prefetch_duplicate{0};// 預取時已經在 RAM 裡
|
| 100 |
-
std::atomic<uint64_t> evictions{0};
|
| 101 |
-
std::atomic<uint64_t> demand_wait_us{0}; // 因為缺頁而停下來等 SSD 的時間
|
| 102 |
-
std::atomic<uint64_t> prefetch_wait_us{0};
|
| 103 |
-
std::atomic<uint64_t> prefetch_bytes_used{0}; // prefetched bytes a demand request actually hit
|
| 104 |
-
std::atomic<uint64_t> tokens{0};
|
| 105 |
-
std::atomic<uint64_t> moe_calls{0};
|
| 106 |
-
std::atomic<uint64_t> flops{0};
|
| 107 |
-
};
|
| 108 |
-
|
| 109 |
-
// ---- SDQ-0004: per-layer cache counters (Phase 0: is any layer starved?)
|
| 110 |
-
struct layer_stat {
|
| 111 |
-
std::atomic<uint64_t> req{0};
|
| 112 |
-
std::atomic<uint64_t> hit{0};
|
| 113 |
-
std::atomic<uint64_t> miss{0};
|
| 114 |
-
std::atomic<uint64_t> demand_bytes{0};
|
| 115 |
-
std::atomic<uint64_t> prefetch_bytes{0};
|
| 116 |
-
};
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
// ---------------------------------------------------------------- pager
|
| 120 |
-
struct config {
|
| 121 |
-
size_t arena_bytes = 0; // 0 = 自動(依 RAM 預算)
|
| 122 |
-
size_t slot_bytes = 0; // 0 = 自動(依模型推導)
|
| 123 |
-
int n_threads = 0;
|
| 124 |
-
bool prefetch = true;
|
| 125 |
-
int prefetch_ahead_layers = 0; // >0 時限制同時在飛的預取數量
|
| 126 |
-
bool fadvise_dontneed = true; // 讀完丟掉 kernel page cache,讓 RAM 帳目誠實
|
| 127 |
-
bool verbose = false;
|
| 128 |
-
uint64_t ram_budget_bytes = 0; // 0 = 自動(實體 RAM 的一小部分)
|
| 129 |
-
std::string model_path;
|
| 130 |
-
std::string stats_path;
|
| 131 |
-
int slot_bias = 0;
|
| 132 |
-
|
| 133 |
-
// ---- I/O 執行緒池 ----
|
| 134 |
-
// 為什麼需要:一個 expert 在檔案上是 gate / up / down 三段「不相鄰」的資料,
|
| 135 |
-
// 循序讀要 3 次 pread,而單一 pread 在本機量到約 7 ms(1.9 MB / 275 MB/s)。
|
| 136 |
-
// 也就是說「讀一個 expert」的延遲完全由這三段循序決定,執行緒再多也沒用。
|
| 137 |
-
// 把三段交給三條執行緒同時讀,單一 expert 的延遵掉到約 1/2~1/3。
|
| 138 |
-
// (實測:8 個 expert 全部 miss 的一層,序列讀 12.6 ms → 段平行 7.6 ms。)
|
| 139 |
-
bool io_parallel = true; // SDQ_IO_PARALLEL=0 可關(回到序列讀)
|
| 140 |
-
int io_threads = 0; // 0 = 自動(3 × n_threads,上限 48)
|
| 141 |
-
int io_split = 1; // SDQ_IO_SPLIT:每一段再切成幾塊並行讀(實測 1 最好)
|
| 142 |
-
|
| 143 |
-
// ---- Strata 式「熱門 expert 剖面」(OPT-1)----
|
| 144 |
-
// 離線用代表性文字跑一次,把每個 (layer, expert) 的請求次數記成剖面;
|
| 145 |
-
// 執行時把排行榜前 hot_pin 個釘成「熱頁」:熱頁可以被讀取,但**永遠不被淘汰**,
|
| 146 |
-
// 淘汰時一律先從冷頁挑。這解決的是「prefill 的一次性專家把熱區洗掉」。
|
| 147 |
-
std::string profile_in; // 讀取剖面(SDQ_PROFILE_IN)
|
| 148 |
-
std::string profile_out; // 結束時輸出剖面(SDQ_PROFILE_OUT)
|
| 149 |
-
int hot_pin = 0; // 釘選前幾名(SDQ_HOT_PIN)
|
| 150 |
-
int admit_min_hits = 1; // 請求次數低於此值就不收進快取(SDQ_ADMIT_MIN_HITS)
|
| 151 |
-
};
|
| 152 |
-
|
| 153 |
-
class pager {
|
| 154 |
-
public:
|
| 155 |
-
static pager & instance();
|
| 156 |
-
|
| 157 |
-
// 從 GGUF 檔頭建立 expert 頁面表,並配置 RAM arena
|
| 158 |
-
void init(const config & cfg, ggml_context * ctx);
|
| 159 |
-
|
| 160 |
-
bool ready() const { return ready_; }
|
| 161 |
-
size_t arena_bytes() const { return arena_bytes_; }
|
| 162 |
-
size_t slot_bytes() const { return slot_bytes_; }
|
| 163 |
-
size_t n_slots() const { return n_slots_; }
|
| 164 |
-
size_t small_bytes() const { return small_bytes_; }
|
| 165 |
-
size_t big_bytes() const { return big_bytes_; }
|
| 166 |
-
size_t n_small_slots() const { return n_small_; }
|
| 167 |
-
size_t n_big_slots() const { return n_big_; }
|
| 168 |
-
int n_moe_layers() const { return n_moe_layers_; }
|
| 169 |
-
uint64_t expert_bytes_total() const { return expert_bytes_total_; }
|
| 170 |
-
const gguf_info & info() const { return info_; }
|
| 171 |
-
|
| 172 |
-
// 取得 layer 描述(idx 越界或沒資料回 nullptr)
|
| 173 |
-
const layer_desc * layer(int il) const;
|
| 174 |
-
|
| 175 |
-
// 一次把「連續編號的一串 expert」讀進 RAM:同一層的三個矩陣(gate/up/down)本來
|
| 176 |
-
// 就各自連續存放,所以 k 個連續 expert 只需要 3 次大讀取(而不是 3k 次)。
|
| 177 |
-
// 對高延遲的儲存裝置(每個 request 都貴)這是最大的單項加速。
|
| 178 |
-
// experts 必須已依編號遞增排序;slots[i] 是 experts[i] 對應的槽位(由呼叫端配置)
|
| 179 |
-
// 回傳實際讀取的位元組數。
|
| 180 |
-
uint64_t read_expert_span(int il, const int32_t * experts, int n, const int32_t * slots);
|
| 181 |
-
|
| 182 |
-
// 只配置槽位(不讀資料):回傳槽位索引,-1 表示沒有空槽
|
| 183 |
-
int reserve_slot(int il, int ie);
|
| 184 |
-
|
| 185 |
-
// 查這個 expert 是否已經在 RAM:命中則 pin 並回傳槽位,未命中回傳 -1(不配置)
|
| 186 |
-
int probe(int il, int ie);
|
| 187 |
-
|
| 188 |
-
// 記錄一次合併讀取的位元組數(給指標用)
|
| 189 |
-
void note_span_read(uint64_t bytes);
|
| 190 |
-
|
| 191 |
-
// 確保 (layer, expert) 在 RAM,回傳槽位索引;-1 表示失敗
|
| 192 |
-
// pinned: 本 token 使用中,不可被淘汰;blocking: 缺頁時是否同步讀 SSD
|
| 193 |
-
int acquire(int il, int ie, bool blocking);
|
| 194 |
-
|
| 195 |
-
// 背景預取:把 page 讀進 arena(非同步),依歷史預測下一個 token 會用到的 expert
|
| 196 |
-
void prefetch_expert(int il, int ie);
|
| 197 |
-
// 「下一層」預取:在 layer L 的 op 開頭呼叫,替 layer L+1 排預取。
|
| 198 |
-
// 與 end_token() 的差別在時機 —— 後者在一個 token 的 40 層全算完才跑,
|
| 199 |
-
// 那時排的預取沒有任何計算可以重疊,實測使用率只有 0~8 %。
|
| 200 |
-
void prefetch_layer_ahead(int il);
|
| 201 |
-
|
| 202 |
-
// 每層計算開始時呼叫:解除上一層的 pin(本層用到的槽位才受保護)
|
| 203 |
-
void unpin_all();
|
| 204 |
-
// 用完一個槽位後立刻放掉 pin(成對呼叫 acquire())。
|
| 205 |
-
//
|
| 206 |
-
// 為什麼需要:原本 pin 是「整層釘住、下一層開頭才 unpin_all」。mode C 的
|
| 207 |
-
// expert_forward 回傳之後就再也不讀那個槽位(最後的歸約只讀各執行緒自己的
|
| 208 |
-
// partial 累加緩衝),所以繼續釘著只是白白把工作集撐大。
|
| 209 |
-
// 撐大的後果很具體:prefill 一個 ubatch 有 128 個 token,一層最多需要
|
| 210 |
-
// 256 個不同的 expert → 整層釘住 = 256 個槽位不能回收。大小兩種槽位之下,
|
| 211 |
-
// 大槽位只有約 n_big_layers �� n_expert_used 個,於是**必定**在 prefill 時
|
| 212 |
-
// 「找不到能放 il=34 ie=… 的槽位」而中止。
|
| 213 |
-
void release(int s);
|
| 214 |
-
void note_used(int il, int ie);
|
| 215 |
-
void end_token();
|
| 216 |
-
// ---- SDQ-0005: one call per llama_decode() from any front end.
|
| 217 |
-
void note_decode(int n_tokens);
|
| 218 |
-
|
| 219 |
-
// 依歷史預測某層下一個 token 的 top-k expert(回傳信心 0..1)
|
| 220 |
-
void predict(int il, int32_t * out, int k);
|
| 221 |
-
|
| 222 |
-
void shutdown();
|
| 223 |
-
stats & get_stats() { return stats_; }
|
| 224 |
-
|
| 225 |
-
// 把當前指標寫成一行 JSON(append 到 stats_path)
|
| 226 |
-
void dump_stats(const char * phase);
|
| 227 |
-
void print_summary() const;
|
| 228 |
-
|
| 229 |
-
// arena 內的槽位指標
|
| 230 |
-
// 槽位索引空間是統一的:[0, n_small_) 是小槽位(連續),
|
| 231 |
-
// [n_small_, n_slots_) 是大槽位(接在小槽位區之後)。
|
| 232 |
-
uint8_t * slot_ptr(int s) {
|
| 233 |
-
return (size_t) s < n_small_
|
| 234 |
-
? arena_ + (size_t) s * small_bytes_
|
| 235 |
-
: arena_ + small_bytes_ * n_small_ + (size_t)(s - (int) n_small_) * big_bytes_;
|
| 236 |
-
}
|
| 237 |
-
size_t slot_size_of(int s) const {
|
| 238 |
-
return (size_t) s < n_small_ ? small_bytes_ : big_bytes_;
|
| 239 |
-
}
|
| 240 |
-
bool need_big_slot(int il, int ie) const;
|
| 241 |
-
|
| 242 |
-
// 這個槽位算不算「大槽位」。
|
| 243 |
-
//
|
| 244 |
-
// 判斷依據是**槽位的實際大小**,不是索引 >= n_small_:
|
| 245 |
-
// 統一尺寸(SDQ_FORCE_UNIFORM_SLOT=1,或模型只有一種 expert 大小)時,
|
| 246 |
-
// 所有槽位的索引都 < n_small_ 但大小都是大的 —— 用索引判斷會讓大 expert
|
| 247 |
-
// 永遠配不到槽位,整個推論卡在 admit 的重試迴圈裡。
|
| 248 |
-
bool is_big_slot(int s) const {
|
| 249 |
-
return big_bytes_ && slot_size_of(s) >= big_bytes_;
|
| 250 |
-
}
|
| 251 |
-
|
| 252 |
-
size_t used_bytes() const { return used_bytes_snapshot(); }
|
| 253 |
-
double hit_rate() const;
|
| 254 |
-
|
| 255 |
-
// arena 裡實際被占用的位元組數。大小兩種槽位之後不能用「槽位數 × slot_bytes_」
|
| 256 |
-
// 計算(slot_bytes_ 是大的那一種),所以真的把槽位走一遍。
|
| 257 |
-
// 呼叫端:dump_stats(每次解碼階段結束)與 print_summary,不是熱路徑。
|
| 258 |
-
size_t used_bytes_snapshot() const {
|
| 259 |
-
size_t b = 0;
|
| 260 |
-
for (size_t s = 0; s < slots_.size(); s++) {
|
| 261 |
-
if (slots_[s].layer >= 0) {
|
| 262 |
-
b += slot_size_of((int) s);
|
| 263 |
-
}
|
| 264 |
-
}
|
| 265 |
-
return b;
|
| 266 |
-
}
|
| 267 |
-
|
| 268 |
-
// 讀 SSD(同步,給 acquire 用)
|
| 269 |
-
bool read_expert(int il, int ie, uint8_t * dst);
|
| 270 |
-
|
| 271 |
-
// ---- I/O 執行緒池(見 config::io_parallel 的說明)----
|
| 272 |
-
// 一個「讀取批次」的完成狀態;放在呼叫端的 stack 上,worker 只在 remaining==0
|
| 273 |
-
// 之後才會再碰它,所以離開 read_expert_par() 之後不會有 dangling pointer。
|
| 274 |
-
struct io_ctx {
|
| 275 |
-
std::atomic<int> remaining{0};
|
| 276 |
-
bool ok = true;
|
| 277 |
-
};
|
| 278 |
-
struct io_job {
|
| 279 |
-
int shard;
|
| 280 |
-
uint64_t off;
|
| 281 |
-
uint8_t * dst;
|
| 282 |
-
size_t size;
|
| 283 |
-
io_ctx * ctx;
|
| 284 |
-
};
|
| 285 |
-
|
| 286 |
-
void io_pool_start();
|
| 287 |
-
void io_pool_stop();
|
| 288 |
-
void io_worker(size_t wid);
|
| 289 |
-
bool read_expert_par(int il, int ie, uint8_t * dst);
|
| 290 |
-
bool read_range_staged(int shard, uint64_t off, uint8_t * dst, size_t size,
|
| 291 |
-
uint8_t * stage, size_t stage_sz);
|
| 292 |
-
|
| 293 |
-
// I/O 執行緒池每條 worker 自帶的 O_DIRECT 對齊緩衝(啟動時配置,已計入 RSS)
|
| 294 |
-
struct io_stage { uint8_t * buf = nullptr; size_t sz = 0; };
|
| 295 |
-
std::vector<io_stage> io_stage_;
|
| 296 |
-
|
| 297 |
-
bool io_pool_on_ = false;
|
| 298 |
-
int io_threads_ = 0;
|
| 299 |
-
int io_split_ = 1;
|
| 300 |
-
std::vector<std::thread> io_pool_;
|
| 301 |
-
std::mutex io_mu_;
|
| 302 |
-
std::condition_variable io_cv_; // 有工作 / 要停
|
| 303 |
-
std::condition_variable io_done_cv_; // 某個批次讀完了
|
| 304 |
-
std::deque<io_job> io_q_;
|
| 305 |
-
bool io_stop_ = false;
|
| 306 |
-
std::atomic<uint64_t> io_jobs_{0};
|
| 307 |
-
std::atomic<uint64_t> io_wait_ns_{0};
|
| 308 |
-
|
| 309 |
-
private:
|
| 310 |
-
pager() = default;
|
| 311 |
-
~pager();
|
| 312 |
-
|
| 313 |
-
struct slot_state {
|
| 314 |
-
int32_t layer = -1;
|
| 315 |
-
int32_t expert = -1;
|
| 316 |
-
uint64_t last_use = 0; // LRU 記時
|
| 317 |
-
uint32_t freq = 0; // 這個 expert 被用到幾次(衰減式;用來做 LFU 混合策略)
|
| 318 |
-
uint32_t pin = 0; // >0 時不可淘汰
|
| 319 |
-
bool busy = false; // 背景執行緒正在寫入
|
| 320 |
-
bool is_hot = false; // 屬於剖面排行榜:淘汰時最後才會被選中
|
| 321 |
-
bool from_prefetch = false; // page was filled by the prefetcher
|
| 322 |
-
};
|
| 323 |
-
|
| 324 |
-
struct pf_item {
|
| 325 |
-
int slot;
|
| 326 |
-
int il;
|
| 327 |
-
int ie;
|
| 328 |
-
uint64_t issue_time_us;
|
| 329 |
-
};
|
| 330 |
-
|
| 331 |
-
void touch_freq_locked(slot_state & st);
|
| 332 |
-
// ---- OPT-1:剖面 / 熱頁 ----
|
| 333 |
-
void count_request(int il, int ie); // 記一次請求(給剖面用)
|
| 334 |
-
bool load_profile(const std::string & path); // 讀剖面並標記前 hot_pin 名
|
| 335 |
-
void dump_profile(const std::string & path); // 輸出排行榜
|
| 336 |
-
bool hot_lookup(int il, int ie) const;
|
| 337 |
-
void load_gguf_header(const std::string & path);
|
| 338 |
-
void build_page_map(const std::string & path);
|
| 339 |
-
void alloc_arena();
|
| 340 |
-
int find_slot_locked(int il, int ie);
|
| 341 |
-
int admit_slot_locked(int il, int ie);
|
| 342 |
-
int victim_locked() { return admit_slot_locked(-1, -1); }
|
| 343 |
-
int victim_stale_locked(uint64_t stale_before);
|
| 344 |
-
int victim_stale_for(int il, int ie, uint64_t stale_before);
|
| 345 |
-
void pf_worker();
|
| 346 |
-
bool read_range(int shard, uint64_t off, uint8_t * dst, size_t size);
|
| 347 |
-
bool read_range_direct(int shard, uint64_t off, uint8_t * dst, size_t size);
|
| 348 |
-
void open_model_fd();
|
| 349 |
-
|
| 350 |
-
// ---- sharded GGUF:模型被切成多個 -0000N-of-000M.gguf,每個檔有自己的
|
| 351 |
-
// header、自己的 data_start 與自己的張量子集。pager 必須同時開著全部的 fd,
|
| 352 |
-
// 並把每個張量的「檔內偏移」記成 (shard, off)。
|
| 353 |
-
std::vector<std::string> shard_paths_;
|
| 354 |
-
std::vector<int> shard_fd_;
|
| 355 |
-
std::vector<int> shard_fd_direct_;
|
| 356 |
-
std::vector<uint64_t> shard_data_start_;
|
| 357 |
-
bool use_direct_ = false;
|
| 358 |
-
std::string shard_path(int i) const {
|
| 359 |
-
return (i >= 0 && i < (int) shard_paths_.size()) ? shard_paths_[(size_t) i] : std::string();
|
| 360 |
-
}
|
| 361 |
-
// 依 GGUF 的 split 命名規則列舉同目錄的所有 shard
|
| 362 |
-
static std::vector<std::string> list_splits(const std::string & first_path, int n_split);
|
| 363 |
-
|
| 364 |
-
bool ready_ = false;
|
| 365 |
-
config cfg_;
|
| 366 |
-
gguf_info info_;
|
| 367 |
-
std::vector<layer_desc> layers_;
|
| 368 |
-
int n_moe_layers_ = 0;
|
| 369 |
-
uint64_t expert_bytes_total_ = 0;
|
| 370 |
-
|
| 371 |
-
uint8_t * arena_ = nullptr;
|
| 372 |
-
size_t arena_bytes_ = 0;
|
| 373 |
-
size_t slot_bytes_ = 0; // = max(small_bytes_, big_bytes_),保留給舊的訊息輸出
|
| 374 |
-
size_t small_bytes_ = 0; // 小槽位大小(大多數層用)
|
| 375 |
-
size_t big_bytes_ = 0; // 大槽位大小;0 = 這個模型沒有大層
|
| 376 |
-
size_t n_small_ = 0;
|
| 377 |
-
size_t n_big_ = 0;
|
| 378 |
-
size_t n_slots_ = 0;
|
| 379 |
-
mutable std::mutex mu_;
|
| 380 |
-
std::vector<slot_state> slots_;
|
| 381 |
-
std::unordered_map<int64_t, int32_t> page_; // (layer<<16|expert) -> slot
|
| 382 |
-
std::vector<int32_t> free_slots_; // 空的小槽位
|
| 383 |
-
std::vector<int32_t> free_big_slots_;
|
| 384 |
-
|
| 385 |
-
// ---- OPT-1:剖面 / 熱頁(Strata 式:靜態排行榜 + 熱頁不淘汰)----
|
| 386 |
-
std::vector<uint32_t> prof_; // 每個 (layer, expert) 的累計請求次數
|
| 387 |
-
std::unique_ptr<layer_stat[]> lstats_; // SDQ-0004
|
| 388 |
-
void ensure_layer_stats(int n_layer); // SDQ-0004
|
| 389 |
-
std::vector<uint8_t> hot_; // 是否在排行榜前 hot_pin 名
|
| 390 |
-
int n_hot_ = 0;
|
| 391 |
-
std::string profile_out_;
|
| 392 |
-
uint32_t freq_tick_ = 0;
|
| 393 |
-
std::atomic<size_t> used_{0};
|
| 394 |
-
uint64_t clock_ = 0;
|
| 395 |
-
|
| 396 |
-
// 背景預取
|
| 397 |
-
std::thread pf_;
|
| 398 |
-
std::mutex pf_mu_;
|
| 399 |
-
std::condition_variable pf_cv_;
|
| 400 |
-
std::deque<pf_item> pf_q_;
|
| 401 |
-
bool pf_stop_ = false;
|
| 402 |
-
std::atomic<uint64_t> pf_inflight_{0};
|
| 403 |
-
|
| 404 |
-
// 路由歷史(一階 Markov:上個 token 的 expert -> 這次被選到的次數)
|
| 405 |
-
struct trans { int32_t to[64]; uint32_t cnt[64]; int32_t n = 0; };
|
| 406 |
-
std::vector<std::vector<trans>> hist_; // [layer][prev_expert]
|
| 407 |
-
std::vector<std::vector<int32_t>> prev_used_; // [layer] 上一個 token 用到的 expert(預測用)
|
| 408 |
-
std::vector<std::vector<int32_t>> cur_used_; // [layer] 當前 token 用到的 expert
|
| 409 |
-
std::mutex hist_mu_; // 保護 cur_used_ / prev_used_ / hist_(多執行緒會同時 note_used)
|
| 410 |
-
std::atomic<uint64_t> hist_lock_{0};
|
| 411 |
-
|
| 412 |
-
stats stats_;
|
| 413 |
-
};
|
| 414 |
-
|
| 415 |
-
// ---------------------------------------------------------------- 工具
|
| 416 |
-
uint64_t now_us();
|
| 417 |
-
size_t process_rss_bytes();
|
| 418 |
-
size_t physical_ram_bytes();
|
| 419 |
-
|
| 420 |
-
// ---------------------------------------------------------------- ggml 介接
|
| 421 |
-
// 自訂 buffer type:expert 權重佔 0 個 RAM 位元組,資料留在檔案裡
|
| 422 |
-
ggml_backend_buffer_type_t sdq_expert_buft();
|
| 423 |
-
|
| 424 |
-
// 註冊 model params 的 tensor buffer override(expert 權重 → 我們的 buffer)
|
| 425 |
-
void sdq_install_buft_overrides(llama_model_params * params);
|
| 426 |
-
|
| 427 |
-
// 建立被分頁的 MoE FFN(替代 build_moe_ffn 裡的三次 mul_mat_id)
|
| 428 |
-
ggml_tensor * sdq_build_moe(ggml_context * ctx, ggml_tensor * cur,
|
| 429 |
-
ggml_tensor * ids, ggml_tensor * weights, int il,
|
| 430 |
-
int64_t n_expert, int64_t n_expert_used,
|
| 431 |
-
float swiglu_limit);
|
| 432 |
-
|
| 433 |
-
// 設定 arena 大小 / 預取開關等(CLI 在載入模型前呼叫)
|
| 434 |
-
bool sdq_init(const std::string & model_path);
|
| 435 |
-
void sdq_set_config(const config & c);
|
| 436 |
-
const config & sdq_get_config();
|
| 437 |
-
void sdq_set_enabled(bool on);
|
| 438 |
-
void sdq_shutdown();
|
| 439 |
-
|
| 440 |
-
bool sdq_enabled();
|
| 441 |
-
|
| 442 |
-
// SDQ-0005: bridge for front ends that are not sdq-chat (llama-server).
|
| 443 |
-
// Defined in sdq_pager.cpp; safe to call when the pager was never initialised.
|
| 444 |
-
void sdq_on_decode(int n_tokens);
|
| 445 |
-
|
| 446 |
-
// 效能計時探針的結果(SDQ_PROFILE_TIME=1)
|
| 447 |
-
void sdq_print_time_profile();
|
| 448 |
-
|
| 449 |
-
// 自我測試:算一次指定 expert 的 MoE ��出(給 NumPy 對照用)
|
| 450 |
-
int sdq_moe_selftest(int il, int ie, const float * x, float * out_io,
|
| 451 |
-
int64_t n_embd, int64_t n_ff, int n_tok);
|
| 452 |
-
|
| 453 |
-
// 建立我們的 MoE 自訂節點。
|
| 454 |
-
// **必須把 gf 傳進來並呼叫 ggml_build_forward_expand(gf, out)**:
|
| 455 |
-
// ggml 的排程器只會為「進了圖的節點」配置 compute buffer,沒註冊的節點其輸出
|
| 456 |
-
// 緩衝區會被 liveness 分析判成可重用,導致 layer>=1 的結果被其他張量覆蓋
|
| 457 |
-
// (實測症狀:layer 0 正確、layer 1 起整個錯掉)。
|
| 458 |
-
ggml_tensor * sdq_build_moe(ggml_cgraph * gf, ggml_context * ctx, ggml_tensor * cur,
|
| 459 |
-
ggml_tensor * ids, ggml_tensor * weights, int il,
|
| 460 |
-
int64_t n_expert, int64_t n_expert_used, float swiglu_limit);
|
| 461 |
-
|
| 462 |
-
// 這一層的分頁是否可用(graph build 時呼叫)
|
| 463 |
-
bool sdq_pager_active_for_layer(int il);
|
| 464 |
-
|
| 465 |
-
// 除錯/驗證用:把某個 F32 張量 dump 到檔案。
|
| 466 |
-
// 必須在「原生 mul_mat_id 路徑」與「SDQ 分頁路徑」兩邊都呼叫,才能做真正的 A/B 對照
|
| 467 |
-
// (env: SDQ_MOE_OUT_DUMP=prefix、SDQ_MOE_OUT_DUMP_LAYER=層編號,-1 = 全部)
|
| 468 |
-
ggml_tensor * sdq_maybe_dump_tensor(ggml_context * ctx, ggml_tensor * t, int il);
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
} // namespace sdq
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sdq-ling/sdq-ling/serve.sh
DELETED
|
@@ -1,26 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env bash
|
| 2 |
-
set -euo pipefail
|
| 3 |
-
|
| 4 |
-
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
| 5 |
-
MODEL="${MODEL:-$ROOT/models/Ling-3.0-tiny-Q4_K_M.gguf}"
|
| 6 |
-
BIN="${BIN:-$ROOT/llama.cpp/build/bin/sdq-chat}"
|
| 7 |
-
RAM_BUDGET_MB="${RAM_BUDGET_MB:-4096}"
|
| 8 |
-
RESERVE_MB="${RESERVE_MB:-700}"
|
| 9 |
-
CTX="${CTX:-256}"
|
| 10 |
-
THREADS="${THREADS:-4}"
|
| 11 |
-
UBATCH="${UBATCH:-32}"
|
| 12 |
-
|
| 13 |
-
[[ -x "$BIN" ]] || { printf 'Missing %s; build the engine as documented in README.md\n' "$BIN" >&2; exit 1; }
|
| 14 |
-
[[ -s "$MODEL" ]] || { printf 'Missing model: %s\n' "$MODEL" >&2; exit 1; }
|
| 15 |
-
|
| 16 |
-
export SDQ_MODEL_PATH="$MODEL"
|
| 17 |
-
export SDQ_RAM_BUDGET_MB="$RAM_BUDGET_MB"
|
| 18 |
-
export SDQ_RESERVE_MB="$RESERVE_MB"
|
| 19 |
-
export SDQ_PREFETCH=0
|
| 20 |
-
export SDQ_FADV_DONTNEED=1
|
| 21 |
-
export SDQ_IO_THREADS="$((THREADS * 2))"
|
| 22 |
-
export LLAMA_ARG_N_GPU_LAYERS=0
|
| 23 |
-
|
| 24 |
-
exec "$BIN" -m "$MODEL" -c "$CTX" -t "$THREADS" -b "$UBATCH" \
|
| 25 |
-
-p "${PROMPT:-What is the capital of France?}" -n "${N_PREDICT:-48}" \
|
| 26 |
-
--temp 0 --no-interactive --ram-budget-mb "$RAM_BUDGET_MB"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sdq-ling/sdq-ling/strict_bench.py
DELETED
|
@@ -1,336 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Strict-limit RAM sweep for EPQ / sdq-ling on Ling-3.0-flash Q4_K_M.
|
| 3 |
-
|
| 4 |
-
Unlike the original memory_levels_ling.py (which only sets the pager's own
|
| 5 |
-
budget), every run here is launched under a *hard* OS limit:
|
| 6 |
-
|
| 7 |
-
prlimit --data=<level> --nofile=65536
|
| 8 |
-
|
| 9 |
-
On Linux >= 4.7 RLIMIT_DATA covers brk() AND mmap'd anonymous memory, so it is
|
| 10 |
-
a real ceiling on the arena + heap + every later anonymous allocation. If the
|
| 11 |
-
process exceeds it, malloc fails and the run aborts -- we record that as a hard
|
| 12 |
-
failure instead of a silent overshoot.
|
| 13 |
-
|
| 14 |
-
Two caveats this harness records explicitly:
|
| 15 |
-
|
| 16 |
-
* RLIMIT_DATA polices *anonymous* memory only. The ~1.3-2.1 GiB of
|
| 17 |
-
non-expert weights are mmap'd file-backed pages and are therefore NOT
|
| 18 |
-
policed here, so peak RSS can exceed the level. Both numbers are reported.
|
| 19 |
-
* `-march=native` on this host defines __AMX_INT8__, which makes llama.cpp
|
| 20 |
-
allocate a ~1.88 GiB AMX repack buffer before the arena-sized context. That
|
| 21 |
-
alone pushes the floor past 4 GiB, so the sweep runs against the
|
| 22 |
-
GGML_NATIVE=OFF (no-AMX) build unless --amx is passed.
|
| 23 |
-
|
| 24 |
-
Swap is proven absent at run time by sampling /proc/<pid>/status VmSwap; a
|
| 25 |
-
nonzero value fails the run.
|
| 26 |
-
"""
|
| 27 |
-
from __future__ import annotations
|
| 28 |
-
|
| 29 |
-
import argparse
|
| 30 |
-
import json
|
| 31 |
-
import os
|
| 32 |
-
import platform
|
| 33 |
-
import re
|
| 34 |
-
import statistics
|
| 35 |
-
import subprocess
|
| 36 |
-
import sys
|
| 37 |
-
import threading
|
| 38 |
-
import time
|
| 39 |
-
from pathlib import Path
|
| 40 |
-
|
| 41 |
-
MIB = 1024 * 1024
|
| 42 |
-
GIB = 1024 ** 3
|
| 43 |
-
|
| 44 |
-
PROMPT = ("Write a detailed paragraph about how a mixture-of-experts model routes "
|
| 45 |
-
"tokens to expert weights, and why paging those weights from SSD matters.")
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def args_config(args):
|
| 49 |
-
"""JSON-safe view of the argparse namespace (drops the resolve helper)."""
|
| 50 |
-
return {k: v for k, v in vars(args).items() if not callable(v)}
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
def host_facts() -> dict:
|
| 54 |
-
cpu = ""
|
| 55 |
-
for line in Path('/proc/cpuinfo').read_text().splitlines():
|
| 56 |
-
if line.startswith('model name'):
|
| 57 |
-
cpu = line.split(':', 1)[1].strip()
|
| 58 |
-
break
|
| 59 |
-
mem_total = 0
|
| 60 |
-
for line in Path('/proc/meminfo').read_text().splitlines():
|
| 61 |
-
if line.startswith('MemTotal'):
|
| 62 |
-
mem_total = int(line.split()[1]) * 1024
|
| 63 |
-
break
|
| 64 |
-
cg = {}
|
| 65 |
-
for k, f in (('memory.max', 'memory.max'),
|
| 66 |
-
('memory.swap.max', 'memory.swap.max'),
|
| 67 |
-
('memory.high', 'memory.high')):
|
| 68 |
-
try:
|
| 69 |
-
cg[k] = Path('/sys/fs/cgroup/' + f).read_text().strip()
|
| 70 |
-
except Exception:
|
| 71 |
-
cg[k] = None
|
| 72 |
-
return {'cpu_model': cpu, 'cpu_threads': os.cpu_count(),
|
| 73 |
-
'mem_total_bytes': mem_total, 'cgroup': cg}
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
class ProcSampler(threading.Thread):
|
| 77 |
-
"""Sample RSS / AnonHWM / VmSwap until told to stop.
|
| 78 |
-
|
| 79 |
-
AnonHWM is smaps_rollup's own anonymous high-water mark and is the quantity
|
| 80 |
-
RLIMIT_DATA actually polices, so it is what we compare against the level.
|
| 81 |
-
"""
|
| 82 |
-
|
| 83 |
-
def __init__(self, pid: int, interval: float = 0.03):
|
| 84 |
-
super().__init__(daemon=True)
|
| 85 |
-
self.pid = pid
|
| 86 |
-
self.interval = interval
|
| 87 |
-
self.stop_flag = threading.Event()
|
| 88 |
-
self.peak_rss = 0
|
| 89 |
-
self.peak_anon = 0
|
| 90 |
-
self.peak_swap = 0
|
| 91 |
-
self.n = 0
|
| 92 |
-
|
| 93 |
-
def _read(self) -> tuple[int, int, int]:
|
| 94 |
-
rss = anon = swp = 0
|
| 95 |
-
try:
|
| 96 |
-
for line in Path(f'/proc/{self.pid}/status').read_text().splitlines():
|
| 97 |
-
if line.startswith('VmRSS:'):
|
| 98 |
-
rss = int(line.split()[1]) * 1024
|
| 99 |
-
elif line.startswith('VmSwap:'):
|
| 100 |
-
swp = int(line.split()[1]) * 1024
|
| 101 |
-
except OSError:
|
| 102 |
-
pass
|
| 103 |
-
try:
|
| 104 |
-
for line in Path(f'/proc/{self.pid}/smaps_rollup').read_text().splitlines():
|
| 105 |
-
if line.startswith('Rss:'):
|
| 106 |
-
rss = max(rss, int(line.split()[1]) * 1024)
|
| 107 |
-
elif line.startswith('Anonymous:'):
|
| 108 |
-
anon = int(line.split()[1]) * 1024
|
| 109 |
-
elif line.startswith('AnonHWM:'):
|
| 110 |
-
anon = max(anon, int(line.split()[1]) * 1024)
|
| 111 |
-
except OSError:
|
| 112 |
-
pass
|
| 113 |
-
return rss, anon, swp
|
| 114 |
-
|
| 115 |
-
def run(self):
|
| 116 |
-
while not self.stop_flag.is_set():
|
| 117 |
-
r, a, s = self._read()
|
| 118 |
-
self.n += 1
|
| 119 |
-
self.peak_rss = max(self.peak_rss, r)
|
| 120 |
-
self.peak_anon = max(self.peak_anon, a)
|
| 121 |
-
self.peak_swap = max(self.peak_swap, s)
|
| 122 |
-
time.sleep(self.interval)
|
| 123 |
-
r, a, s = self._read()
|
| 124 |
-
self.peak_rss = max(self.peak_rss, r)
|
| 125 |
-
self.peak_anon = max(self.peak_anon, a)
|
| 126 |
-
self.peak_swap = max(self.peak_swap, s)
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
def evict(shards: list[str], helper: str) -> None:
|
| 130 |
-
subprocess.run(['sync'], check=False)
|
| 131 |
-
if helper:
|
| 132 |
-
subprocess.run([helper, '--evict'] + shards, check=False)
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
def run_once(args, level_mb: int, repeat: int) -> dict:
|
| 136 |
-
stats_file = Path(args.output_dir) / f'level_{level_mb}_r{repeat}.jsonl'
|
| 137 |
-
stats_file.unlink(missing_ok=True)
|
| 138 |
-
os.makedirs(args.output_dir, exist_ok=True)
|
| 139 |
-
|
| 140 |
-
inner = [
|
| 141 |
-
str(Path(args.bin).resolve()),
|
| 142 |
-
'-m', str(Path(args.model).resolve()),
|
| 143 |
-
'-p', PROMPT, '-n', str(args.n_predict), '-c', str(args.ctx),
|
| 144 |
-
'-t', str(args.threads), '-b', str(args.ubatch), '--temp', '0',
|
| 145 |
-
'--no-interactive', '--stats-file', str(stats_file),
|
| 146 |
-
'--ram-budget-mb', str(level_mb),
|
| 147 |
-
]
|
| 148 |
-
# The strict ceiling: RLIMIT_DATA = arena + heap + all anon mmap.
|
| 149 |
-
cmd = ['prlimit', f'--data={level_mb * MIB}', '--nofile=65536'] + inner
|
| 150 |
-
|
| 151 |
-
env = dict(os.environ)
|
| 152 |
-
env.update({
|
| 153 |
-
'SDQ_RAM_BUDGET_MB': str(level_mb),
|
| 154 |
-
'SDQ_RESERVE_MB': str(args.reserve_mb_for(level_mb)),
|
| 155 |
-
'SDQ_PREFETCH': '0',
|
| 156 |
-
'SDQ_FADV_DONTNEED': '1',
|
| 157 |
-
'SDQ_BENCH_NO_EOG': '1',
|
| 158 |
-
})
|
| 159 |
-
|
| 160 |
-
evict(args.shards, args.evict_helper)
|
| 161 |
-
|
| 162 |
-
t0 = time.perf_counter()
|
| 163 |
-
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
| 164 |
-
text=True, env=env)
|
| 165 |
-
sampler = ProcSampler(proc.pid)
|
| 166 |
-
sampler.start()
|
| 167 |
-
try:
|
| 168 |
-
out, err = proc.communicate(timeout=args.timeout)
|
| 169 |
-
except subprocess.TimeoutExpired:
|
| 170 |
-
proc.kill()
|
| 171 |
-
out, err = proc.communicate()
|
| 172 |
-
sampler.stop_flag.set()
|
| 173 |
-
return {'level_mb': level_mb, 'repeat': repeat + 1,
|
| 174 |
-
'error': f'timeout after {args.timeout}s'}
|
| 175 |
-
sampler.stop_flag.set()
|
| 176 |
-
sampler.join(timeout=2)
|
| 177 |
-
wall = time.perf_counter() - t0
|
| 178 |
-
both = out + '\n' + err
|
| 179 |
-
|
| 180 |
-
res = {
|
| 181 |
-
'level_mb': level_mb,
|
| 182 |
-
'level_gib': level_mb / 1024,
|
| 183 |
-
'repeat': repeat + 1,
|
| 184 |
-
'wall_s': round(wall, 2),
|
| 185 |
-
'exit_code': proc.returncode,
|
| 186 |
-
'peak_rss_mb': round(sampler.peak_rss / MIB, 1),
|
| 187 |
-
'peak_anon_mb': round(sampler.peak_anon / MIB, 1),
|
| 188 |
-
'peak_swap_mb': round(sampler.peak_swap / MIB, 1),
|
| 189 |
-
'swap_free': sampler.peak_swap == 0,
|
| 190 |
-
'anon_within_limit': sampler.peak_anon <= level_mb * MIB,
|
| 191 |
-
'limit_enforced_by': 'prlimit --data (RLIMIT_DATA)',
|
| 192 |
-
}
|
| 193 |
-
|
| 194 |
-
if proc.returncode != 0:
|
| 195 |
-
sig = -proc.returncode if proc.returncode < 0 else None
|
| 196 |
-
res['signal'] = sig
|
| 197 |
-
res['status'] = 'OOM_OR_ABORT'
|
| 198 |
-
res['failure'] = ' | '.join(
|
| 199 |
-
[l.strip() for l in err.splitlines()
|
| 200 |
-
if re.search(r'bad_alloc|cannot allocate|MemoryError|std::terminate|'
|
| 201 |
-
r'abort|Killed|Expert arena|too small|error', l, re.I)][-5:])
|
| 202 |
-
res['wall_s'] = round(wall, 2)
|
| 203 |
-
return res
|
| 204 |
-
|
| 205 |
-
res['status'] = 'OK'
|
| 206 |
-
|
| 207 |
-
def grab(pat, text=both):
|
| 208 |
-
m = re.search(pat, text, re.M)
|
| 209 |
-
return m
|
| 210 |
-
|
| 211 |
-
d = grab(r'decode:\s*(\d+)\s+tok\s*/\s*([\d.]+)s\s*=\s*([\d.]+)\s+tok/s')
|
| 212 |
-
p = grab(r'prefill:\s*(\d+)\s+tok\s*/\s*([\d.]+)s\s*=\s*([\d.]+)\s+tok/s')
|
| 213 |
-
mem = grab(r'總 RSS\s+(\d+)\s+MB\s*/\s+峰值\s+(\d+)\s+MB\s*/\s+swap\s+(\d+)\s+MB')
|
| 214 |
-
split = grab(r'匿名 RSS\s+(\d+)\s+MB\s*/\s+檔案對映 RSS\s+(\d+)\s+MB\s*/\s+總 RSS\s+(\d+)\s+MB\s*/\s+峰值\s+(\d+)\s+MB')
|
| 215 |
-
arena = grab(r'arena\s+([\d.]+)\s+GiB')
|
| 216 |
-
slots = grab(r'slots=(\d+)')
|
| 217 |
-
ssd_tot = grab(r'全程 SSD 實際讀取:\s*([\d.]+)\s*GB')
|
| 218 |
-
mib = grab(r'SSD 讀\s*([\d.]+)\s*MB\s*\(([\d.]+)\s*MB/tok\)')
|
| 219 |
-
|
| 220 |
-
if d:
|
| 221 |
-
res['decode_tok_s'] = float(d.group(3))
|
| 222 |
-
res['decode_tok'] = int(d.group(1))
|
| 223 |
-
if p:
|
| 224 |
-
res['prefill_tok_s'] = float(p.group(3))
|
| 225 |
-
res['prefill_tok'] = int(p.group(1))
|
| 226 |
-
if mem:
|
| 227 |
-
res['final_rss_mb'] = int(mem.group(1))
|
| 228 |
-
res['selfpeak_rss_mb'] = int(mem.group(2))
|
| 229 |
-
res['self_swap_mb'] = int(mem.group(3))
|
| 230 |
-
if split:
|
| 231 |
-
res['rss_anon_mb'] = int(split.group(1))
|
| 232 |
-
res['rss_file_mb'] = int(split.group(2))
|
| 233 |
-
res['rss_total_mb'] = int(split.group(3))
|
| 234 |
-
res['selfpeak_rss_mb'] = int(split.group(4))
|
| 235 |
-
if arena:
|
| 236 |
-
res['arena_gib'] = float(arena.group(1))
|
| 237 |
-
if slots:
|
| 238 |
-
res['n_slots'] = int(slots.group(1))
|
| 239 |
-
if ssd_tot:
|
| 240 |
-
res['total_ssd_gb'] = float(ssd_tot.group(1))
|
| 241 |
-
if mib:
|
| 242 |
-
res['decode_ssd_mb'] = float(mib.group(1))
|
| 243 |
-
res['ssd_mb_per_token'] = float(mib.group(2))
|
| 244 |
-
|
| 245 |
-
rec = {}
|
| 246 |
-
if stats_file.exists():
|
| 247 |
-
for line in stats_file.read_text().splitlines():
|
| 248 |
-
r = json.loads(line)
|
| 249 |
-
if r.get('phase') == 'decode':
|
| 250 |
-
rec = r
|
| 251 |
-
stats_file.unlink(missing_ok=True)
|
| 252 |
-
|
| 253 |
-
pl = rec.get('per_layer') or []
|
| 254 |
-
rates = [x['hit_rate'] for x in pl if x.get('req', 0) > 0]
|
| 255 |
-
hr = rec.get('hit_rate')
|
| 256 |
-
res.update({
|
| 257 |
-
'expert_requests': rec.get('expert_requests'),
|
| 258 |
-
'expert_hits': rec.get('expert_hits'),
|
| 259 |
-
'hot_rate_pct': hr,
|
| 260 |
-
'cold_rate_pct': (100.0 - hr) if hr is not None else None,
|
| 261 |
-
'evictions': rec.get('evictions'),
|
| 262 |
-
'per_layer_hit_min': min(rates) if rates else None,
|
| 263 |
-
'per_layer_hit_max': max(rates) if rates else None,
|
| 264 |
-
})
|
| 265 |
-
|
| 266 |
-
asst = ''
|
| 267 |
-
started = False
|
| 268 |
-
for line in out.splitlines():
|
| 269 |
-
if line.startswith('assistant:'):
|
| 270 |
-
started = True
|
| 271 |
-
rest = line[len('assistant:'):].strip()
|
| 272 |
-
if rest:
|
| 273 |
-
asst = rest
|
| 274 |
-
continue
|
| 275 |
-
if started and line.strip() and not line.startswith('['):
|
| 276 |
-
asst += ' ' + line.strip()
|
| 277 |
-
res['assistant'] = asst.strip()[:200]
|
| 278 |
-
return res
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
def main() -> None:
|
| 282 |
-
ap = argparse.ArgumentParser()
|
| 283 |
-
ap.add_argument('--model', required=True)
|
| 284 |
-
ap.add_argument('--shards', nargs='+', required=True)
|
| 285 |
-
ap.add_argument('--bin', required=True)
|
| 286 |
-
ap.add_argument('--levels-mb', type=int, nargs='+', default=[4096, 8192, 16384, 32768])
|
| 287 |
-
ap.add_argument('--repeat', type=int, default=2)
|
| 288 |
-
ap.add_argument('--ctx', type=int, default=2048)
|
| 289 |
-
ap.add_argument('--ubatch', type=int, default=128)
|
| 290 |
-
ap.add_argument('--threads', type=int, default=16)
|
| 291 |
-
ap.add_argument('--n-predict', type=int, default=48)
|
| 292 |
-
ap.add_argument('--reserve-mb', type=int, default=1024)
|
| 293 |
-
ap.add_argument('--timeout', type=int, default=1800)
|
| 294 |
-
ap.add_argument('--evict-helper', default='/tmp/pagecache_resident')
|
| 295 |
-
ap.add_argument(
|
| 296 |
-
'--reserve-map',
|
| 297 |
-
default='4096:2600,8192:3600,16384:3600,32768:3600',
|
| 298 |
-
help='per-level SDQ_RESERVE_MB, "level_mb:reserve_mb,...". Calibrated '
|
| 299 |
-
'by measuring anon_hwm - arena, i.e. every anonymous allocation '
|
| 300 |
-
'that happens after the arena is sized.')
|
| 301 |
-
ap.add_argument('--output-dir', default='/root/app/ling/results_strict')
|
| 302 |
-
ap.add_argument('--out-json', default='/root/app/ling/results_strict/strict_memory_levels.json')
|
| 303 |
-
args = ap.parse_args()
|
| 304 |
-
|
| 305 |
-
args._reserve = {}
|
| 306 |
-
for item in args.reserve_map.split(','):
|
| 307 |
-
k, v = item.split(':')
|
| 308 |
-
args._reserve[int(k)] = int(v)
|
| 309 |
-
args.reserve_mb_for = lambda lvl: args._reserve.get(lvl, 3600)
|
| 310 |
-
|
| 311 |
-
Path(args.output_dir).mkdir(parents=True, exist_ok=True)
|
| 312 |
-
|
| 313 |
-
print(f'host: {host_facts()}', file=sys.stderr)
|
| 314 |
-
runs = []
|
| 315 |
-
for lvl in args.levels_mb:
|
| 316 |
-
for r in range(args.repeat):
|
| 317 |
-
print(f'--- level {lvl} MB repeat {r + 1}/{args.repeat} ---',
|
| 318 |
-
file=sys.stderr, flush=True)
|
| 319 |
-
try:
|
| 320 |
-
res = run_once(args, lvl, r)
|
| 321 |
-
except Exception as e:
|
| 322 |
-
res = {'level_mb': lvl, 'repeat': r + 1, 'error': repr(e)}
|
| 323 |
-
runs.append(res)
|
| 324 |
-
print(json.dumps(res, ensure_ascii=False)[:400], file=sys.stderr, flush=True)
|
| 325 |
-
Path(args.out_json).write_text(json.dumps(
|
| 326 |
-
{'host': host_facts(), 'config': args_config(args), 'runs': runs},
|
| 327 |
-
indent=2, ensure_ascii=False))
|
| 328 |
-
|
| 329 |
-
print('DONE', file=sys.stderr)
|
| 330 |
-
Path(args.out_json).write_text(json.dumps(
|
| 331 |
-
{'host': host_facts(), 'config': args_config(args), 'runs': runs},
|
| 332 |
-
indent=2, ensure_ascii=False))
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
if __name__ == '__main__':
|
| 336 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sdq-ling/sdq-ling/verify_dequant.py
DELETED
|
@@ -1,51 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Byte-level cross-check of the NumPy dequantizers against ggml's own.
|
| 3 |
-
|
| 4 |
-
`ggml_deq <type> <gguf> <offset> <n>` (built from sdq-ling/ggml_deq.cpp) dumps one
|
| 5 |
-
dequantized row using ggml's dequantize_row_*; this script compares it with the
|
| 6 |
-
NumPy implementation used by verify_ling.py / verify_moe_layer.py.
|
| 7 |
-
"""
|
| 8 |
-
import importlib.util
|
| 9 |
-
import subprocess
|
| 10 |
-
import sys
|
| 11 |
-
|
| 12 |
-
import numpy as np
|
| 13 |
-
|
| 14 |
-
spec = importlib.util.spec_from_file_location('vl', str(__import__('pathlib').Path(__file__).with_name('verify_ling.py')))
|
| 15 |
-
vl = importlib.util.module_from_spec(spec)
|
| 16 |
-
spec.loader.exec_module(vl)
|
| 17 |
-
|
| 18 |
-
GGML_DEQ = str(__import__('pathlib').Path(__file__).resolve().parents[1] / 'llama.cpp/build/bin/ggml_deq')
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def main():
|
| 22 |
-
kv, ts = vl.parse_headers()
|
| 23 |
-
cases = [(2, 'ffn_gate_exps.weight', 12), (2, 'ffn_up_exps.weight', 12),
|
| 24 |
-
(2, 'ffn_down_exps.weight', 14), (5, 'ffn_gate_exps.weight', 12),
|
| 25 |
-
(5, 'ffn_down_exps.weight', 14), (20, 'ffn_gate_exps.weight', 12),
|
| 26 |
-
(23, 'ffn_down_exps.weight', 14)]
|
| 27 |
-
all_ok = True
|
| 28 |
-
for il, name, tt in cases:
|
| 29 |
-
si, off, dims, t = ts[f'blk.{il}.{name}']
|
| 30 |
-
assert t == tt, (name, t, tt)
|
| 31 |
-
nrow = vl.row_bytes(dims[0], tt)
|
| 32 |
-
ne0 = dims[0]
|
| 33 |
-
for ie in (0, 17, 127):
|
| 34 |
-
eo = off + ie * nrow * dims[1]
|
| 35 |
-
out = subprocess.run([GGML_DEQ, str(tt), vl.SHARDS[si], str(eo), str(ne0)],
|
| 36 |
-
capture_output=True, check=True)
|
| 37 |
-
ref = np.frombuffer(out.stdout, dtype=np.float32)
|
| 38 |
-
with open(vl.SHARDS[si], 'rb') as f:
|
| 39 |
-
f.seek(eo)
|
| 40 |
-
mine = vl.dequant(f.read(nrow), ne0, tt)
|
| 41 |
-
d = float(np.abs(ref - mine).max())
|
| 42 |
-
ok = d == 0.0
|
| 43 |
-
all_ok &= ok
|
| 44 |
-
print(f'blk.{il}.{name} E{ie} type={vl.NAME.get(tt)} n={ne0}: '
|
| 45 |
-
f'max|ggml - numpy| = {d:g} {"PASS" if ok else "FAIL"}')
|
| 46 |
-
print('\nALL PASS' if all_ok else '\nSOME FAILED')
|
| 47 |
-
return 0 if all_ok else 1
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
if __name__ == '__main__':
|
| 51 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sdq-ling/sdq-ling/verify_ling.py
DELETED
|
@@ -1,219 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Validate the SDQ paged MoE kernel for Ling-3.0-flash (bailingmoe3) with pure NumPy.
|
| 3 |
-
|
| 4 |
-
The weights are read straight out of the sharded GGUF, dequantized with ggml's
|
| 5 |
-
definitions, and the SiLU-gated expert forward pass is recomputed in fp32. The
|
| 6 |
-
result is compared against `sdq-chat --selftest IL:IE:SEED:OUT.bin`.
|
| 7 |
-
|
| 8 |
-
Ling-3.0-flash expert tensors (Q4_K_M):
|
| 9 |
-
ffn_gate_exps [n_embd=2560, n_ff_exp=768, n_expert=512] q4_K
|
| 10 |
-
ffn_up_exps [2560, 768, 512] q4_K
|
| 11 |
-
ffn_down_exps [768, 2560, 512] q4_K or q6_K
|
| 12 |
-
"""
|
| 13 |
-
import os
|
| 14 |
-
import struct
|
| 15 |
-
import sys
|
| 16 |
-
|
| 17 |
-
import numpy as np
|
| 18 |
-
|
| 19 |
-
QK_K = 256
|
| 20 |
-
MODEL = os.environ.get('MODEL', '/root/app/ling/tiny/models/Ling-3.0-tiny-Q4_K_M.gguf')
|
| 21 |
-
SHARDS = [MODEL.replace('00001-of', f'{i:05d}-of') for i in (1, 2)]
|
| 22 |
-
|
| 23 |
-
TS = {0: 1, 1: 1, 2: 2, 3: 2, 4: 4, 5: 4, 6: 4, 7: 1, 10: 8, 11: 8, 12: 8}
|
| 24 |
-
BLK = {12: (256, 144), 13: (256, 176), 14: (256, 210), 7: (32, 24), 8: (32, 34),
|
| 25 |
-
2: (32, 18), 3: (32, 20), 6: (32, 22), 0: (1, 4), 1: (1, 2)}
|
| 26 |
-
FMT = {0: '<B', 1: '<b', 2: '<H', 3: '<h', 4: '<I', 5: '<i', 6: '<f', 7: '<B',
|
| 27 |
-
10: '<Q', 11: '<q', 12: '<d'}
|
| 28 |
-
NAME = {12: 'q4_K', 13: 'q5_K', 14: 'q6_K', 7: 'q5_1', 8: 'q8_0', 6: 'q5_0', 2: 'q4_0'}
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def rstr(f):
|
| 32 |
-
n = struct.unpack('<Q', f.read(8))[0]
|
| 33 |
-
return f.read(n)
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def read_val(f, t):
|
| 37 |
-
if t == 8:
|
| 38 |
-
return rstr(f).decode('utf-8', 'replace')
|
| 39 |
-
if t == 9:
|
| 40 |
-
et = struct.unpack('<I', f.read(4))[0]
|
| 41 |
-
n = struct.unpack('<Q', f.read(8))[0]
|
| 42 |
-
if et == 8:
|
| 43 |
-
return [rstr(f).decode('utf-8', 'replace') for _ in range(n)]
|
| 44 |
-
buf = f.read(n * TS[et])
|
| 45 |
-
return np.frombuffer(buf, dtype=np.dtype(FMT[et])).tolist()
|
| 46 |
-
buf = f.read(TS[t])
|
| 47 |
-
return np.frombuffer(buf, dtype=np.dtype(FMT[t]))[0].item()
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def parse_headers():
|
| 51 |
-
kv = {}
|
| 52 |
-
tensors = {}
|
| 53 |
-
for si, path in enumerate(SHARDS):
|
| 54 |
-
with open(path, 'rb') as f:
|
| 55 |
-
assert f.read(4) == b'GGUF'
|
| 56 |
-
f.read(4)
|
| 57 |
-
n_tensors = struct.unpack('<Q', f.read(8))[0]
|
| 58 |
-
n_kv = struct.unpack('<Q', f.read(8))[0]
|
| 59 |
-
for _ in range(n_kv):
|
| 60 |
-
k = rstr(f).decode()
|
| 61 |
-
kv[k] = read_val(f, struct.unpack('<I', f.read(4))[0])
|
| 62 |
-
align = kv.get('general.alignment', 32)
|
| 63 |
-
pending = []
|
| 64 |
-
for _ in range(n_tensors):
|
| 65 |
-
name = rstr(f).decode()
|
| 66 |
-
nd = struct.unpack('<I', f.read(4))[0]
|
| 67 |
-
dims = struct.unpack(f'<{nd}Q', f.read(8 * nd))
|
| 68 |
-
tt = struct.unpack('<I', f.read(4))[0]
|
| 69 |
-
off = struct.unpack('<Q', f.read(8))[0]
|
| 70 |
-
pending.append((name, dims, tt, off))
|
| 71 |
-
# data_start is only known after the whole tensor table has been read
|
| 72 |
-
data_start = (f.tell() + align - 1) // align * align
|
| 73 |
-
for name, dims, tt, off in pending:
|
| 74 |
-
tensors[name] = (si, data_start + off, dims, tt)
|
| 75 |
-
return kv, tensors
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
def fp16(x):
|
| 79 |
-
return np.frombuffer(x, dtype=np.float16)[0].astype(np.float32)
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
def dequant_q4_K(raw, n):
|
| 83 |
-
nb = n // QK_K
|
| 84 |
-
out = np.empty(n, dtype=np.float32)
|
| 85 |
-
for i in range(nb):
|
| 86 |
-
b = raw[i * 144:(i + 1) * 144]
|
| 87 |
-
d = fp16(b[0:2])
|
| 88 |
-
dmin = fp16(b[2:4])
|
| 89 |
-
sc = np.frombuffer(b, dtype=np.uint8, count=12, offset=4)
|
| 90 |
-
qs = np.frombuffer(b, dtype=np.uint8, count=128, offset=16)
|
| 91 |
-
base = i * QK_K
|
| 92 |
-
is_ = 0
|
| 93 |
-
for j in range(0, QK_K, 64):
|
| 94 |
-
if is_ < 4:
|
| 95 |
-
sc1, m1 = int(sc[is_] & 63), int(sc[is_ + 4] & 63)
|
| 96 |
-
sc2, m2 = int(sc[is_ + 1] & 63), int(sc[is_ + 5] & 63)
|
| 97 |
-
else:
|
| 98 |
-
sc1 = int((sc[is_ + 4] & 0xF) | ((sc[is_ - 4] >> 6) << 4))
|
| 99 |
-
m1 = int((sc[is_ + 4] >> 4) | ((sc[is_] >> 6) << 4))
|
| 100 |
-
sc2 = int((sc[is_ + 5] & 0xF) | ((sc[is_ - 3] >> 6) << 4))
|
| 101 |
-
m2 = int((sc[is_ + 5] >> 4) | ((sc[is_ + 1] >> 6) << 4))
|
| 102 |
-
q = qs[j // 2: j // 2 + 32]
|
| 103 |
-
out[base + j: base + j + 32] = d * sc1 * (q & 0xF) - dmin * m1
|
| 104 |
-
out[base + j + 32: base + j + 64] = d * sc2 * (q >> 4) - dmin * m2
|
| 105 |
-
is_ += 2
|
| 106 |
-
return out
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
def dequant_q6_K(raw, n):
|
| 110 |
-
nb = n // QK_K
|
| 111 |
-
out = np.empty(n, dtype=np.float32)
|
| 112 |
-
for i in range(nb):
|
| 113 |
-
b = raw[i * 210:(i + 1) * 210]
|
| 114 |
-
d = fp16(b[208:210])
|
| 115 |
-
ql = np.frombuffer(b, dtype=np.uint8, count=128, offset=0)
|
| 116 |
-
qh = np.frombuffer(b, dtype=np.uint8, count=64, offset=128)
|
| 117 |
-
sc = np.frombuffer(b, dtype=np.int8, count=16, offset=192)
|
| 118 |
-
base = i * QK_K
|
| 119 |
-
for g in range(2): # two 128-element groups per block
|
| 120 |
-
for l in range(32):
|
| 121 |
-
is_ = l // 16
|
| 122 |
-
q1 = ((ql[g * 64 + l] & 0xF) | (((qh[g * 32 + l] >> 0) & 3) << 4)).astype(np.int16) - 32
|
| 123 |
-
q2 = ((ql[g * 64 + l + 32] & 0xF) | (((qh[g * 32 + l] >> 2) & 3) << 4)).astype(np.int16) - 32
|
| 124 |
-
q3 = ((ql[g * 64 + l] >> 4) | (((qh[g * 32 + l] >> 4) & 3) << 4)).astype(np.int16) - 32
|
| 125 |
-
q4 = ((ql[g * 64 + l + 32] >> 4) | (((qh[g * 32 + l] >> 6) & 3) << 4)).astype(np.int16) - 32
|
| 126 |
-
off0 = base + g * 128
|
| 127 |
-
out[off0 + l] = d * sc[g * 8 + is_] * q1
|
| 128 |
-
out[off0 + l + 32] = d * sc[g * 8 + is_ + 2] * q2
|
| 129 |
-
out[off0 + l + 64] = d * sc[g * 8 + is_ + 4] * q3
|
| 130 |
-
out[off0 + l + 96] = d * sc[g * 8 + is_ + 6] * q4
|
| 131 |
-
return out
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
def dequant(raw, n, tt):
|
| 135 |
-
if tt == 12:
|
| 136 |
-
return dequant_q4_K(raw, n)
|
| 137 |
-
if tt == 14:
|
| 138 |
-
return dequant_q6_K(raw, n)
|
| 139 |
-
raise ValueError(f'unsupported type {tt} ({NAME.get(tt, tt)})')
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
def row_bytes(n, tt):
|
| 143 |
-
bs, ts = BLK[tt]
|
| 144 |
-
return n // bs * ts
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
def read_expert(tensors, il, ie):
|
| 148 |
-
def seg(name):
|
| 149 |
-
si, off, dims, tt = tensors[f'blk.{il}.{name}']
|
| 150 |
-
nrow = row_bytes(dims[0], tt)
|
| 151 |
-
n = dims[1] * nrow
|
| 152 |
-
with open(SHARDS[si], 'rb') as f:
|
| 153 |
-
f.seek(off + ie * n)
|
| 154 |
-
return f.read(n), nrow, tt, dims[0]
|
| 155 |
-
g = seg('ffn_gate_exps.weight')
|
| 156 |
-
u = seg('ffn_up_exps.weight')
|
| 157 |
-
d = seg('ffn_down_exps.weight')
|
| 158 |
-
return g, u, d
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
def load_matrix(seg, n_rows):
|
| 162 |
-
raw, nrow, tt, ne0 = seg
|
| 163 |
-
return np.stack([dequant(raw[r * nrow:(r + 1) * nrow], ne0, tt) for r in range(n_rows)])
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
def main():
|
| 167 |
-
cases = []
|
| 168 |
-
for path in sys.argv[1:]:
|
| 169 |
-
# st_l<IL>e<IE>.bin
|
| 170 |
-
base = os.path.basename(path)
|
| 171 |
-
body = base[len('st_l'):-len('.bin')]
|
| 172 |
-
il, ie = body.split('e')
|
| 173 |
-
cases.append((int(il), int(ie), path))
|
| 174 |
-
if not cases:
|
| 175 |
-
print('usage: verify_ling.py st_l<IL>e<IE>.bin ...')
|
| 176 |
-
return 1
|
| 177 |
-
|
| 178 |
-
kv, tensors = parse_headers()
|
| 179 |
-
n_embd = int(kv['bailingmoe3.embedding_length'])
|
| 180 |
-
n_ff = int(kv['bailingmoe3.expert_feed_forward_length'])
|
| 181 |
-
print(f"arch=bailingmoe3 n_expert={int(kv['bailingmoe3.expert_count'])} "
|
| 182 |
-
f"n_expert_used={int(kv['bailingmoe3.expert_used_count'])} "
|
| 183 |
-
f"n_embd={n_embd} n_ff_exp={n_ff}")
|
| 184 |
-
|
| 185 |
-
all_ok = True
|
| 186 |
-
for il, ie, path in cases:
|
| 187 |
-
raw = open(path, 'rb').read()
|
| 188 |
-
n_tok = len(raw) // 4 // (2 * n_embd)
|
| 189 |
-
got = np.frombuffer(raw[:n_tok * n_embd * 4], dtype=np.float32).reshape(n_tok, n_embd)
|
| 190 |
-
x = np.frombuffer(raw[n_tok * n_embd * 4:], dtype=np.float32).reshape(n_tok, n_embd)
|
| 191 |
-
|
| 192 |
-
g_seg, u_seg, d_seg = read_expert(tensors, il, ie)
|
| 193 |
-
Wg = load_matrix(g_seg, n_ff)
|
| 194 |
-
Wu = load_matrix(u_seg, n_ff)
|
| 195 |
-
Wd = load_matrix(d_seg, n_embd)
|
| 196 |
-
|
| 197 |
-
rels, corrs = [], []
|
| 198 |
-
for t in range(n_tok):
|
| 199 |
-
xt = x[t]
|
| 200 |
-
g = Wg @ xt
|
| 201 |
-
u = Wu @ xt
|
| 202 |
-
h = (g / (1.0 + np.exp(-g))) * u
|
| 203 |
-
ref = Wd @ h
|
| 204 |
-
denom = max(float(np.abs(ref).max()), 1e-9)
|
| 205 |
-
rels.append(float(np.abs(got[t] - ref).max()) / denom)
|
| 206 |
-
corrs.append(float(np.corrcoef(got[t], ref)[0, 1]))
|
| 207 |
-
rel = max(rels)
|
| 208 |
-
corr = min(corrs)
|
| 209 |
-
ok = rel < 0.05 and corr > 0.99
|
| 210 |
-
all_ok &= ok
|
| 211 |
-
print(f'L{il} E{ie}: gate={NAME[g_seg[2]]} up={NAME[u_seg[2]]} down={NAME[d_seg[2]]}'
|
| 212 |
-
f' | max_rel_err={rel:.4f} corr={corr:.6f} | {"PASS" if ok else "FAIL"}')
|
| 213 |
-
|
| 214 |
-
print('\nALL PASS' if all_ok else '\nSOME FAILED')
|
| 215 |
-
return 0 if all_ok else 1
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
if __name__ == '__main__':
|
| 219 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sdq-ling/sdq-ling/verify_moe_layer.py
DELETED
|
@@ -1,154 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Recompute a whole paged MoE layer in NumPy and compare with sdq-chat's output.
|
| 3 |
-
|
| 4 |
-
`sdq-chat` with SDQ_MOE_DUMP=<prefix> SDQ_MOE_DUMP_LAYER=<il> writes, from inside
|
| 5 |
-
the custom MoE op:
|
| 6 |
-
int32 meta[6] = { n_embd, n_tok, n_expert_used, n_expert, 1, 2 }
|
| 7 |
-
float cur [n_tok][n_embd] (FFN input)
|
| 8 |
-
int32 ids [n_tok][n_expert_used] (routed experts)
|
| 9 |
-
float wts [n_tok][n_expert_used] (routed weights)
|
| 10 |
-
float out [n_tok][n_embd] (SDQ MoE output)
|
| 11 |
-
|
| 12 |
-
This script reads the same expert weights straight out of the GGUF, runs the
|
| 13 |
-
SiLU-gated expert forward in fp32 and diffs the result, so the whole
|
| 14 |
-
pager + custom kernel + weight-scaling path is checked at once.
|
| 15 |
-
"""
|
| 16 |
-
import importlib.util
|
| 17 |
-
import struct
|
| 18 |
-
import sys
|
| 19 |
-
|
| 20 |
-
import numpy as np
|
| 21 |
-
|
| 22 |
-
spec = importlib.util.spec_from_file_location('vl', str(__import__('pathlib').Path(__file__).with_name('verify_ling.py')))
|
| 23 |
-
vl = importlib.util.module_from_spec(spec)
|
| 24 |
-
spec.loader.exec_module(vl)
|
| 25 |
-
|
| 26 |
-
QK_K = 256
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def dequant_q4_K_vec(raw, n_elem):
|
| 30 |
-
"""raw: uint8 [..., 144] -> float32 [..., 256]"""
|
| 31 |
-
raw = raw.reshape(-1, 144)
|
| 32 |
-
nb = raw.shape[0]
|
| 33 |
-
out = np.empty((nb, QK_K), dtype=np.float32)
|
| 34 |
-
d = raw[:, 0:2].copy().view(np.float16).astype(np.float32)[:, 0]
|
| 35 |
-
dmin = raw[:, 2:4].copy().view(np.float16).astype(np.float32)[:, 0]
|
| 36 |
-
b = np.ascontiguousarray(raw[:, 4:16]) # 12 packed 6-bit scale bytes
|
| 37 |
-
sc = np.empty((nb, 8), dtype=np.int32)
|
| 38 |
-
mn = np.empty((nb, 8), dtype=np.int32)
|
| 39 |
-
for j in range(4):
|
| 40 |
-
sc[:, j] = b[:, j] & 63
|
| 41 |
-
mn[:, j] = b[:, j + 4] & 63
|
| 42 |
-
for j in range(4, 8):
|
| 43 |
-
sc[:, j] = (b[:, j + 4] & 0xF) | ((b[:, j - 4] >> 6) << 4)
|
| 44 |
-
mn[:, j] = (b[:, j + 4] >> 4) | ((b[:, j] >> 6) << 4)
|
| 45 |
-
qs = raw[:, 16:144].reshape(nb, 4, 32).astype(np.int32)
|
| 46 |
-
q = np.empty((nb, QK_K), dtype=np.int32)
|
| 47 |
-
for j in range(4):
|
| 48 |
-
q[:, j * 64:j * 64 + 32] = qs[:, j, :] & 0xF
|
| 49 |
-
q[:, j * 64 + 32:j * 64 + 64] = qs[:, j, :] >> 4
|
| 50 |
-
gidx = np.repeat(np.arange(8), 32) # 32-value group -> (sc, mn) index
|
| 51 |
-
out[:] = d[:, None] * sc[:, gidx] * q - dmin[:, None] * mn[:, gidx]
|
| 52 |
-
return out
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
def dequant_q6_K_vec(raw, n_elem):
|
| 56 |
-
raw = raw.reshape(-1, 210)
|
| 57 |
-
nb = raw.shape[0]
|
| 58 |
-
out = np.empty((nb, QK_K), dtype=np.float32)
|
| 59 |
-
d = raw[:, 208:210].copy().view(np.float16).astype(np.float32)[:, 0]
|
| 60 |
-
ql = raw[:, 0:128].reshape(nb, 4, 32).astype(np.int32) # 4 x 32 low/high nibbles
|
| 61 |
-
qh = raw[:, 128:192].reshape(nb, 2, 32).astype(np.int32)
|
| 62 |
-
sc = raw[:, 192:208].copy().view(np.int8).astype(np.int32)
|
| 63 |
-
q = np.empty((nb, QK_K), dtype=np.int32)
|
| 64 |
-
sidx = np.empty(QK_K, dtype=np.int32)
|
| 65 |
-
for g in range(2):
|
| 66 |
-
for l in range(32):
|
| 67 |
-
is_ = l // 16
|
| 68 |
-
o = g * 128
|
| 69 |
-
a = ql[:, 2 * g, l] # ql[g*64 + l]
|
| 70 |
-
b_ = ql[:, 2 * g + 1, l] # ql[g*64 + l + 32]
|
| 71 |
-
h = qh[:, g, l]
|
| 72 |
-
q[:, o + l] = ((a & 0xF) | ((h & 3) << 4)) - 32
|
| 73 |
-
q[:, o + l + 32] = ((b_ & 0xF) | (((h >> 2) & 3) << 4)) - 32
|
| 74 |
-
q[:, o + l + 64] = ((a >> 4) | (((h >> 4) & 3) << 4)) - 32
|
| 75 |
-
q[:, o + l + 96] = ((b_ >> 4) | (((h >> 6) & 3) << 4)) - 32
|
| 76 |
-
sidx[o + l] = g * 8 + is_
|
| 77 |
-
sidx[o + l + 32] = g * 8 + is_ + 2
|
| 78 |
-
sidx[o + l + 64] = g * 8 + is_ + 4
|
| 79 |
-
sidx[o + l + 96] = g * 8 + is_ + 6
|
| 80 |
-
out[:] = d[:, None] * sc[:, sidx] * q
|
| 81 |
-
return out
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
def dequant_mat(raw, n_rows, ne0, tt):
|
| 85 |
-
nrow = vl.row_bytes(ne0, tt)
|
| 86 |
-
nblk = ne0 // QK_K
|
| 87 |
-
arr = np.frombuffer(raw[:n_rows * nrow], dtype=np.uint8).reshape(n_rows, nblk, nrow // nblk)
|
| 88 |
-
if tt == 12:
|
| 89 |
-
return dequant_q4_K_vec(arr, ne0).reshape(n_rows, ne0)
|
| 90 |
-
if tt == 14:
|
| 91 |
-
return dequant_q6_K_vec(arr, ne0).reshape(n_rows, ne0)
|
| 92 |
-
raise ValueError(tt)
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
def main():
|
| 96 |
-
dump = sys.argv[1]
|
| 97 |
-
il = int(sys.argv[2])
|
| 98 |
-
kv, ts = vl.parse_headers()
|
| 99 |
-
n_embd = int(kv['bailingmoe3.embedding_length'])
|
| 100 |
-
n_ff = int(kv['bailingmoe3.expert_feed_forward_length'])
|
| 101 |
-
n_expert = int(kv['bailingmoe3.expert_count'])
|
| 102 |
-
|
| 103 |
-
raw = open(dump, 'rb').read()
|
| 104 |
-
meta = struct.unpack('<6i', raw[:24])
|
| 105 |
-
n_tok, n_used = meta[1], meta[2]
|
| 106 |
-
off = 24
|
| 107 |
-
cur = np.frombuffer(raw, dtype=np.float32, count=n_tok * n_embd, offset=off).reshape(n_tok, n_embd)
|
| 108 |
-
off += 4 * n_tok * n_embd
|
| 109 |
-
ids = np.frombuffer(raw, dtype=np.int32, count=n_tok * n_used, offset=off).reshape(n_tok, n_used)
|
| 110 |
-
off += 4 * n_tok * n_used
|
| 111 |
-
wts = np.frombuffer(raw, dtype=np.float32, count=n_tok * n_used, offset=off).reshape(n_tok, n_used)
|
| 112 |
-
off += 4 * n_tok * n_used
|
| 113 |
-
got = np.frombuffer(raw, dtype=np.float32, count=n_tok * n_embd, offset=off).reshape(n_tok, n_embd)
|
| 114 |
-
print(f'layer {il}: n_tok={n_tok} n_expert_used={n_used} n_expert={n_expert} '
|
| 115 |
-
f'n_embd={n_embd} n_ff={n_ff}')
|
| 116 |
-
|
| 117 |
-
def seg(nm, ie):
|
| 118 |
-
si, off_t, dims, tt = ts[f'blk.{il}.{nm}']
|
| 119 |
-
nrow = vl.row_bytes(dims[0], tt)
|
| 120 |
-
nb = nrow * dims[1]
|
| 121 |
-
with open(vl.SHARDS[si], 'rb') as f:
|
| 122 |
-
f.seek(off_t + ie * nb)
|
| 123 |
-
return f.read(nb), nrow, tt, dims[0]
|
| 124 |
-
|
| 125 |
-
ref = np.zeros((n_tok, n_embd), dtype=np.float32)
|
| 126 |
-
experts = sorted(set(int(e) for e in ids.reshape(-1)))
|
| 127 |
-
print(f'distinct experts: {len(experts)}')
|
| 128 |
-
for ie in experts:
|
| 129 |
-
g_raw, g_nrow, g_t, g_ne0 = seg('ffn_gate_exps.weight', ie)
|
| 130 |
-
u_raw, _, u_t, u_ne0 = seg('ffn_up_exps.weight', ie)
|
| 131 |
-
d_raw, _, d_t, d_ne0 = seg('ffn_down_exps.weight', ie)
|
| 132 |
-
Wg = dequant_mat(g_raw, n_ff, g_ne0, g_t)
|
| 133 |
-
Wu = dequant_mat(u_raw, n_ff, u_ne0, u_t)
|
| 134 |
-
Wd = dequant_mat(d_raw, n_embd, d_ne0, d_t)
|
| 135 |
-
rows = [t for t in range(n_tok) if ie in ids[t]]
|
| 136 |
-
x = cur[rows].T # [n_embd, T]
|
| 137 |
-
w = np.array([wts[t][list(ids[t]).index(ie)] for t in rows], dtype=np.float32)
|
| 138 |
-
h = ((Wg @ x) / (1.0 + np.exp(-(Wg @ x)))) * (Wu @ x)
|
| 139 |
-
y = Wd @ h # [n_embd, T]
|
| 140 |
-
ref[rows] += (y * w).T
|
| 141 |
-
del Wg, Wu, Wd
|
| 142 |
-
|
| 143 |
-
denom = max(float(np.abs(ref).max()), 1e-9)
|
| 144 |
-
diff = np.abs(got - ref)
|
| 145 |
-
corr = float(np.corrcoef(got.reshape(-1), ref.reshape(-1))[0, 1])
|
| 146 |
-
print(f'max|got-ref| = {diff.max():.6g} max|ref| = {denom:.6g} '
|
| 147 |
-
f'max_rel = {diff.max() / denom:.6g} corr = {corr:.8f}')
|
| 148 |
-
ok = corr > 0.99 and diff.max() / denom < 0.05
|
| 149 |
-
print('PASS' if ok else 'FAIL')
|
| 150 |
-
return 0 if ok else 1
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
if __name__ == '__main__':
|
| 154 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sdq-ling/sdq-ling/watch_io.py
DELETED
|
@@ -1,119 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Run a command while sampling how the model file is actually read.
|
| 3 |
-
|
| 4 |
-
Reports, per sample and at the end:
|
| 5 |
-
read_bytes /proc/<pid>/io bytes actually fetched from the block device
|
| 6 |
-
file_resident mincore(model file) pages of the GGUF sitting in the page cache
|
| 7 |
-
RssAnon/RssFile process RSS split (arena+weights vs mapped file)
|
| 8 |
-
Cached /proc/meminfo system-wide page cache
|
| 9 |
-
|
| 10 |
-
The point: a buffered read would show up as file_resident growing; O_DIRECT reads
|
| 11 |
-
never touch the cache, so read_bytes grows while file_resident stays flat.
|
| 12 |
-
|
| 13 |
-
usage: watch_io.py <model.gguf>[,<model2.gguf>,...] <cmd> [args...]
|
| 14 |
-
"""
|
| 15 |
-
import os
|
| 16 |
-
import re
|
| 17 |
-
import subprocess
|
| 18 |
-
import sys
|
| 19 |
-
import time
|
| 20 |
-
|
| 21 |
-
PAGE = os.sysconf('SC_PAGE_SIZE')
|
| 22 |
-
CHR = 1024 ** 3
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
def meminfo(key):
|
| 26 |
-
for line in open('/proc/meminfo'):
|
| 27 |
-
if line.startswith(key):
|
| 28 |
-
return int(line.split()[1]) * 1024
|
| 29 |
-
return 0
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
def proc_io(pid):
|
| 33 |
-
out = {}
|
| 34 |
-
try:
|
| 35 |
-
for line in open(f'/proc/{pid}/io'):
|
| 36 |
-
k, _, v = line.partition(':')
|
| 37 |
-
out[k] = int(v)
|
| 38 |
-
except OSError:
|
| 39 |
-
return None
|
| 40 |
-
return out
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def proc_status(pid):
|
| 44 |
-
out = {}
|
| 45 |
-
try:
|
| 46 |
-
for line in open(f'/proc/{pid}/status'):
|
| 47 |
-
for k in ('RssAnon', 'RssFile', 'VmHWM'):
|
| 48 |
-
if line.startswith(k):
|
| 49 |
-
out[k] = int(line.split()[1]) * 1024
|
| 50 |
-
except OSError:
|
| 51 |
-
pass
|
| 52 |
-
return out
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
def file_resident(paths):
|
| 56 |
-
"""resident bytes of the given files via mincore (helper binary)"""
|
| 57 |
-
r = subprocess.run(['/tmp/pagecache_resident'] + paths, capture_output=True, text=True)
|
| 58 |
-
m = re.search(r'TOTAL\s+resident\s+([\d.]+) GiB', r.stdout)
|
| 59 |
-
if m:
|
| 60 |
-
return float(m.group(1)) * CHR
|
| 61 |
-
m = re.search(r'resident\s+([\d.]+) GiB', r.stdout)
|
| 62 |
-
return float(m.group(1)) * CHR if m else -1
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
def main():
|
| 66 |
-
models = sys.argv[1].split(',')
|
| 67 |
-
cmd = sys.argv[2:]
|
| 68 |
-
subprocess.run(['sync'])
|
| 69 |
-
print(f'[watch] evicting {len(models)} shard(s) from the page cache', flush=True)
|
| 70 |
-
subprocess.run(['/tmp/pagecache_resident', '--evict'] + models)
|
| 71 |
-
|
| 72 |
-
base_cached = meminfo('Cached:')
|
| 73 |
-
base_read = None
|
| 74 |
-
t0 = time.time()
|
| 75 |
-
# NOTE: never use pipes here - sdq-chat prints several hundred KB of
|
| 76 |
-
# tokenizer warnings and would block on a full 64 KB pipe.
|
| 77 |
-
out_f = open('/tmp/watch_stdout.log', 'w')
|
| 78 |
-
err_f = open('/tmp/watch_stderr.log', 'w')
|
| 79 |
-
proc = subprocess.Popen(cmd, stdout=out_f, stderr=err_f)
|
| 80 |
-
pid = proc.pid
|
| 81 |
-
|
| 82 |
-
peak_res = 0.0
|
| 83 |
-
peak_rssanon = peak_rssfile = 0
|
| 84 |
-
peak_cached = base_cached
|
| 85 |
-
samples = 0
|
| 86 |
-
while proc.poll() is None:
|
| 87 |
-
io_ = proc_io(pid)
|
| 88 |
-
st = proc_status(pid)
|
| 89 |
-
if io_ is None:
|
| 90 |
-
break
|
| 91 |
-
if base_read is None:
|
| 92 |
-
base_read = io_['read_bytes']
|
| 93 |
-
res = file_resident(models)
|
| 94 |
-
cached = meminfo('Cached:')
|
| 95 |
-
peak_res = max(peak_res, res)
|
| 96 |
-
peak_cached = max(peak_cached, cached)
|
| 97 |
-
peak_rssanon = max(peak_rssanon, st.get('RssAnon', 0))
|
| 98 |
-
peak_rssfile = max(peak_rssfile, st.get('RssFile', 0))
|
| 99 |
-
samples += 1
|
| 100 |
-
print(f'[watch] t={time.time()-t0:6.1f}s read_bytes={(io_["read_bytes"]-base_read)/CHR:7.3f} GiB '
|
| 101 |
-
f' rchar={(io_["rchar"])/CHR:8.3f} GiB model_file_resident={res/CHR:6.3f} GiB '
|
| 102 |
-
f'RssAnon={st.get("RssAnon",0)/CHR:6.3f} RssFile={st.get("RssFile",0)/CHR:6.3f} '
|
| 103 |
-
f'Cached_delta={(cached-base_cached)/CHR:+7.3f} GiB', flush=True)
|
| 104 |
-
time.sleep(float(os.environ.get("WATCH_INTERVAL", "2.0")))
|
| 105 |
-
|
| 106 |
-
proc.wait()
|
| 107 |
-
out_f.close()
|
| 108 |
-
err_f.close()
|
| 109 |
-
print(f'\n[watch] === summary over {samples} samples, {time.time()-t0:.1f}s ===')
|
| 110 |
-
print(f'[watch] peak model_file_resident in page cache : {peak_res/CHR:.3f} GiB')
|
| 111 |
-
print(f'[watch] peak process RssAnon : {peak_rssanon/CHR:.3f} GiB')
|
| 112 |
-
print(f'[watch] peak process RssFile : {peak_rssfile/CHR:.3f} GiB')
|
| 113 |
-
print(f'[watch] system Cached delta (peak) : {(peak_cached-base_cached)/CHR:+.3f} GiB')
|
| 114 |
-
print(f'[watch] final model_file_resident : {file_resident(models)/CHR:.3f} GiB')
|
| 115 |
-
return 0
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
if __name__ == '__main__':
|
| 119 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|