File size: 8,923 Bytes
00d4f8c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 | // SDQ expert buffer type
//
// 這是一個「假」buffer type:它宣稱接受所有張量,但實際上
// * 不配置任何 RAM(alloc_buffer 回傳 0 大小的 buffer)
// * set_tensor 忽略傳入的資料(資料來自 llama.cpp 的 mmap,我們不碰)
// * 真正的資料由 sdq::pager 從檔案 pread 到自己的 arena
//
// 用途:讓 llama.cpp 把 18.2 GiB 的 MoE 專家權重「掛在模型上」卻不放進 RAM,
// 改由分頁器管理 SSD → RAM → CPU 的三層資料流。
#include "sdq_pager.h"
#include "ggml-backend-impl.h"
#include "llama-mmap.h"
#include <cstdio>
#include <string>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <mutex>
#include <unordered_map>
#include <vector>
namespace sdq {
// 給 tensor->data 的假位址。每個 expert 權重張量分到一小塊(預設 4 KB),
// 指標必須有效(ggml 會做指標運算),但內容永遠不會被讀。
struct expert_buffer {
std::vector<uint8_t> scratch;
};
static bool g_enabled = false;
void sdq_set_enabled(bool on) { g_enabled = on; }
bool sdq_enabled() { return g_enabled; }
static config g_cfg;
void sdq_set_config(const config & c) { g_cfg = c; }
const config & sdq_get_config() { return g_cfg; }
// ---------------------------------------------------------------- buffer iface
// ggml_backend_buffer_i 的欄位依賴下面的實作,先給宣告
struct ggml_backend_buffer_i sdq_buffer_iface();
static const char * sdq_buft_get_name(ggml_backend_buffer_type_t buft) {
(void) buft;
return "SDQ_ExpertSSD";
}
static size_t sdq_buft_get_alignment(ggml_backend_buffer_type_t buft) {
(void) buft;
return 4096;
}
static size_t sdq_buft_get_max_size(ggml_backend_buffer_type_t buft) {
(void) buft;
return SIZE_MAX;
}
// 每個 expert 權重張量只回報 4 KB:18.2 GiB 的權重「不會」被載入 RAM 的關鍵。
static size_t sdq_buft_get_alloc_size(ggml_backend_buffer_type_t buft, const ggml_tensor * tensor) {
(void) buft;
(void) tensor;
return 4096;
}
static bool sdq_buft_is_host(ggml_backend_buffer_type_t buft) {
(void) buft;
return false;
}
static ggml_backend_buffer_t sdq_buft_alloc_buffer(ggml_backend_buffer_type_t buft, size_t size) {
// llama.cpp 會用 get_alloc_size() 計算總大小後呼叫本函式;
// 我們只保留真正需要的假位址空間(每個張量 4 KB)。
const size_t bytes = size < 4096 ? 4096 : size;
auto * buf = new expert_buffer();
buf->scratch.assign(bytes, 0);
auto * b = new ggml_backend_buffer{
/* .iface = */ sdq_buffer_iface(),
/* .buft = */ buft,
/* .context = */ buf,
/* .size = */ bytes,
/* .usage = */ GGML_BACKEND_BUFFER_USAGE_WEIGHTS,
};
return b;
}
// 一次配置整個 context 的張量:每個張量 4 KB 假位址,實際權重 0 RAM。
static ggml_backend_buffer_t sdq_buft_alloc_buffer_n(ggml_backend_buffer_type_t buft,
ggml_tensor ** tensors, int n_tensors) {
const size_t slot = 4096;
const size_t bytes = slot * (size_t) std::max(n_tensors, 1);
ggml_backend_buffer_t buffer = sdq_buft_alloc_buffer(buft, bytes);
if (!buffer) {
return nullptr;
}
uint8_t * base = (uint8_t *) buffer->iface.get_base(buffer);
size_t off = 0;
for (int i = 0; i < n_tensors; i++) {
ggml_tensor * t = tensors[i];
t->data = base + off;
t->buffer = buffer;
off += slot;
buffer->iface.init_tensor(buffer, t);
}
return buffer;
}
static void sdq_buft_free_buffer(ggml_backend_buffer_t buffer) {
// 只釋放自己的資料;ggml_backend_buffer_free() 會負責 delete buffer 本體,
// 這裡再 delete 一次會造成 double free。
delete (expert_buffer *) buffer->context;
buffer->context = nullptr;
}
static void * sdq_buft_get_base(ggml_backend_buffer_t buffer) {
return ((expert_buffer *) buffer->context)->scratch.data();
}
static ggml_status sdq_buft_init_tensor(ggml_backend_buffer_t buffer, ggml_tensor * tensor) {
(void) buffer;
(void) tensor;
return GGML_STATUS_SUCCESS;
}
static void sdq_buft_memset_tensor(ggml_backend_buffer_t buffer, ggml_tensor * tensor,
uint8_t value, size_t offset, size_t size) {
(void) buffer; (void) tensor; (void) value; (void) offset; (void) size;
}
static void sdq_buft_set_tensor(ggml_backend_buffer_t buffer, ggml_tensor * tensor,
const void * data, size_t offset, size_t size) {
// 關鍵:忽略。llama.cpp 會把 mmap 的資料交給我們,這裡一個位元組都不落地,
// 專家權利永遠不會佔用 RAM。
(void) buffer; (void) tensor; (void) data; (void) offset; (void) size;
}
static void sdq_buft_get_tensor(ggml_backend_buffer_t buffer, const ggml_tensor * tensor,
void * data, size_t offset, size_t size) {
(void) buffer; (void) tensor; (void) offset;
memset(data, 0, size);
}
static bool sdq_buft_cpy_tensor(ggml_backend_buffer_t buffer, const ggml_tensor * src, ggml_tensor * dst) {
(void) buffer; (void) src; (void) dst;
return true;
}
static void sdq_buft_clear(ggml_backend_buffer_t buffer, uint8_t value) {
(void) buffer; (void) value;
}
struct ggml_backend_buffer_i sdq_buffer_iface() {
static ggml_backend_buffer_i i = [] {
ggml_backend_buffer_i x {};
x.free_buffer = sdq_buft_free_buffer;
x.get_base = sdq_buft_get_base;
x.init_tensor = sdq_buft_init_tensor;
x.memset_tensor = sdq_buft_memset_tensor;
x.set_tensor = sdq_buft_set_tensor;
x.get_tensor = sdq_buft_get_tensor;
x.cpy_tensor = sdq_buft_cpy_tensor;
x.clear = sdq_buft_clear;
return x;
}();
return i;
}
ggml_backend_buffer_type_t sdq_expert_buft() {
static ggml_backend_buffer_type_t buft = [] {
return new ggml_backend_buffer_type{
/* .iface = */ {
/* .get_name = */ sdq_buft_get_name,
/* .alloc_buffer = */ sdq_buft_alloc_buffer,
/* .alloc_buffer_n = */ sdq_buft_alloc_buffer_n,
/* .get_alignment = */ sdq_buft_get_alignment,
/* .get_max_size = */ sdq_buft_get_max_size,
/* .get_alloc_size = */ sdq_buft_get_alloc_size,
/* .get_alloc_size_n = */ nullptr,
/* .is_host = */ sdq_buft_is_host,
},
/* .device = */ nullptr,
/* .context = */ nullptr,
};
}();
return buft;
}
// llama.cpp 端需要的介面宣告
void sdq_install_buft_overrides(llama_model_params * params);
// SDQ_BUFT_KEEP 保留開關:std::regex 的負向先行斷言在 llama.cpp 的比對方式下
// 不可靠(會讓整個 override 失效、權重被載進 RAM),所以先不開放這個 knob。
static std::string sdq_expert_pattern() {
return "blk\\.[0-9]+\\.ffn_(gate|up|down|gate_up)_exps\\.weight";
}
// 這兩個 static 只能用「初始化一次」的方式寫:libstdc++ 的 std::string move-assign
// 會把舊的 buffer 交給右邊的 temporary,temporary 析構時就 free 掉。若改成每次呼叫
// pattern = sdq_expert_pattern(),先前存進 override 陣列的 .pattern 會變成懸空指標;
// 讀到空字串時 std::regex("") 會匹配「任何」張量名,於是整個模型(含 token_embd)
// 都被塞進假 buffer,server 直接在 sched_reserve 裡 ggml_abort。
static const llama_model_tensor_buft_override * sdq_overrides() {
static const std::string pattern = sdq_expert_pattern();
static const llama_model_tensor_buft_override ov[2] = {
{ pattern.c_str(), sdq_expert_buft() },
{ nullptr, nullptr },
};
return ov;
}
void sdq_install_buft_overrides(llama_model_params * params) {
if (!params) {
return;
}
if (params->tensor_buft_overrides == nullptr) {
params->tensor_buft_overrides = sdq_overrides();
} else {
// 使用者已有 override:把手動指定的順序原樣保留,附加在尾端
size_t n = 0;
while (params->tensor_buft_overrides[n].pattern != nullptr) {
n++;
}
static std::vector<llama_model_tensor_buft_override> merged;
merged.clear();
for (size_t i = 0; i < n; i++) {
merged.push_back(params->tensor_buft_overrides[i]);
}
const llama_model_tensor_buft_override * sdq_ov = sdq_overrides();
for (size_t i = 0; sdq_ov[i].pattern != nullptr; i++) {
merged.push_back(sdq_ov[i]);
}
merged.push_back({ nullptr, nullptr });
params->tensor_buft_overrides = merged.data();
}
}
} // namespace sdq |