File size: 31,752 Bytes
a8b0793
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
900b73c
 
 
 
 
 
 
 
 
 
be4cff1
 
 
 
 
a8b0793
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4068c44
 
 
 
 
 
 
 
 
 
 
5601477
 
 
 
 
 
 
 
 
a8b0793
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
// SDQ 聊天 / benchmark 主程式
//
//   sdq-chat -m <model.gguf> [-c ctx] [-t threads] [-n n_predict] [-p prompt] ...
//
// 這個工具存在的理由:官方 llama-cli 不會安裝我們的 tensor buffer override,
// 而且需要我們在載入模型前後插入 pager 的初始化/收尾。用最少的相依度
// (只鏈 llama + ggml)自己寫一支,行為完全可控。

#include "llama.h"
#include "sdq_pager.h"

#include <algorithm>
#include <chrono>
#include <cinttypes>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <thread>
#include <iostream>
#include <string>
#include <vector>

using namespace sdq;

namespace {

struct options {
    std::string model;
    std::string prompt;
    std::string system = "You are a helpful assistant. Answer in the user's language.";
    std::string stats_file;
    int32_t n_ctx = 4096;          // 驗證範圍:4K(理論支援 1M)
    int32_t n_predict = 256;
    int32_t n_threads = 0;
    int32_t n_ubatch = 0;
    int32_t n_keep = 0;
    float temp = 0.7f;
    float top_p = 0.95f;
    int32_t top_k = 20;
    bool chat = true;
    bool interactive = true;
    bool bench = false;
    bool no_prefetch = false;
    bool dump_logits = false;
    std::string logits_out;
    std::string selftest;
    std::string span_test;
    std::string dump_tensors;   // ggml eval callback:把指定張量的資料存檔(A/B 比對用)
    bool no_pager = false;      // A/B 測試用:不啟用分頁,走上游 mul_mat_id(整個模型在 RAM)
    int32_t arena_mb = 0;
    int32_t ram_budget_mb = 0;
    bool fadvise = true;
};

double now_s() {
    static auto t0 = std::chrono::steady_clock::now();
    return std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
}

size_t rss_mb() { return process_rss_bytes() / (1024 * 1024); }

// 從 /proc/self/status 讀 VmHWM(峰值 RSS)/ VmSwap(這個行程真的用了多少 swap)
size_t proc_status_kb(const char * key) {
    FILE * f = fopen("/proc/self/status", "r");
    if (!f) return 0;
    char line[256];
    const size_t klen = strlen(key);
    size_t val = 0;
    while (fgets(line, sizeof(line), f)) {
        if (strncmp(line, key, klen) == 0) {
            val = (size_t) strtoull(line + klen, nullptr, 10);
            break;
        }
    }
    fclose(f);
    return val;
}

size_t peak_rss_mb() { return proc_status_kb("VmHWM:") / 1024; }
size_t swap_mb()    { return proc_status_kb("VmSwap:") / 1024; }

// /proc/self/smaps_rollup:把 RSS 拆成「檔案對映」與「匿名」兩塊。
// 這很重要:模型檔是 mmap 進來的,核心預讀會讓「檔案對映頁」也算進 RSS,
// 報告時必須分開看,否則會誤以為 RAM 用量比實際少(或多)。
size_t smaps_kb(const char * key) {
    FILE * f = fopen("/proc/self/smaps_rollup", "r");
    if (!f) return 0;
    char line[256];
    const size_t klen = strlen(key);
    size_t val = 0;
    while (fgets(line, sizeof(line), f)) {
        if (strncmp(line, key, klen) == 0) {
            val = (size_t) strtoull(line + klen, nullptr, 10);
            break;
        }
    }
    fclose(f);
    return val;
}

void print_mem_report(const char * when) {
    fprintf(stderr,
            "[sdq] 記憶體(%s):匿名 RSS %zu MB / 檔案對映 RSS %zu MB / 總 RSS %zu MB / 峰值 %zu MB / swap %zu MB\n",
            when, smaps_kb("Anonymous:") / 1024, smaps_kb("RssFile:") / 1024,
            smaps_kb("Rss:") / 1024, peak_rss_mb(), swap_mb());
}

// 讀 /proc/self/io:從 block device 實際讀了多少(SSD 讀取)
uint64_t read_bytes_io() {
    FILE * f = fopen("/proc/self/io", "r");
    if (!f) return 0;
    char line[256];
    uint64_t v = 0;
    while (fgets(line, sizeof(line), f)) {
        if (strncmp(line, "read_bytes:", 11) == 0) {
            v = strtoull(line + 11, nullptr, 10);
        }
    }
    fclose(f);
    return v;
}

struct chat_state {
    std::vector<llama_chat_message> msgs;
};

std::string build_prompt(const options & opt, const chat_state & st) {
    if (!opt.chat) {
        return opt.prompt;
    }
    const char * tmpl = llama_model_chat_template(nullptr, nullptr);
    // 沒有 model 指標時用內建模板;下面 main() 會用實際模型的模板重做
    (void) tmpl;
    size_t n_chars = 0;
    for (const auto & m : st.msgs) {
        n_chars += strlen(m.role) + strlen(m.content) + 32;
    }
    std::vector<char> buf(2 * n_chars + 1024);
    const char * t = st.msgs.empty() ? nullptr : nullptr;
    (void) t;
    int32_t len = llama_chat_apply_template(tmpl, st.msgs.data(), st.msgs.size(), true,
                                            buf.data(), (int32_t) buf.size());
    if (len > (int32_t) buf.size()) {
        buf.resize(len + 1);
        llama_chat_apply_template(tmpl, st.msgs.data(), st.msgs.size(), true,
                                  buf.data(), (int32_t) buf.size());
    }
    return std::string(buf.data());
}

} // namespace

#include <execinfo.h>
#include <signal.h>
#include <unistd.h>
static void sdq_segv(int sig) {
    void * bt[64];
    const int n = backtrace(bt, 64);
    fprintf(stderr, "\n*** sdq: signal %d, backtrace:\n", sig);
    backtrace_symbols_fd(bt, n, 2);
    _exit(9);
}

// eval callback:把感興趣的張量(用 SDQ_DUMP_NAMES 指定)寫進檔案
struct dump_state {
    FILE * fp = nullptr;
    uint64_t step = 0;
    std::vector<std::string> names;
};

static dump_state g_dump;

static bool sdq_eval_callback(ggml_tensor * t, bool ask, void * user_data) {
    (void) user_data;
    auto & ds = g_dump;
    if (ask) {
        // 除錯模式:每個節點都要資料 → 會強制逐節點同步(慢,但能逐節點比對)
        return ds.fp != nullptr;
    }
    if (!ds.fp) {
        return false;
    }
    const char * nm = ggml_get_name(t);
    const size_t n_len = nm ? strlen(nm) : 0;
    bool want = ds.names.empty();
    for (const auto & n : ds.names) {
        if (nm && strstr(nm, n.c_str())) {
            want = true;
            break;
        }
    }
    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]};
    const int32_t type = (int32_t) t->type;
    fwrite(&n_len, sizeof(size_t), 1, ds.fp);
    if (n_len) {
        fwrite(nm, 1, n_len, ds.fp);
    }
    fwrite(&ds.step, sizeof(uint64_t), 1, ds.fp);
    fwrite(ne, sizeof(int32_t), 4, ds.fp);
    fwrite(&type, sizeof(int32_t), 1, ds.fp);

    // 永遠寫「摘要」(前 8 個值 + 平方和 + 最大值),這樣即使沒有名字也能 A/B 找出第一個分歧點
    float summary[10] = {0};
    if (t->type == GGML_TYPE_F32 && t->data) {
        const size_t n = (size_t) ne[0] * ne[1] * ne[2] * ne[3];
        const float * p = (const float *) t->data;
        double ss = 0;
        float mx = 0;
        for (size_t i = 0; i < n; i++) {
            ss += (double) p[i] * p[i];
            if (std::fabs(p[i]) > std::fabs(mx)) {
                mx = p[i];
            }
        }
        for (int i = 0; i < 8 && (size_t) i < n; i++) {
            summary[i] = p[i];
        }
        summary[8] = (float) std::sqrt(ss);
        summary[9] = mx;
        fwrite(summary, sizeof(float), 10, ds.fp);
        if (want) {
            fwrite(p, sizeof(float), n, ds.fp);
        }
    } else {
        fwrite(summary, sizeof(float), 10, ds.fp);
    }
    ds.step++;
    return true;   // 這個節點的資料我們已經讀過了,標記成已完成
}

int main(int argc, char ** argv) {
    signal(SIGSEGV, sdq_segv);
    signal(SIGBUS, sdq_segv);
    signal(SIGABRT, sdq_segv);
    options opt;
    const char * env_model = getenv("SDQ_MODEL_PATH");
    if (env_model) {
        opt.model = env_model;
    }

    for (int i = 1; i < argc; i++) {
        std::string a = argv[i];
        auto next = [&](const char * what) -> std::string {
            if (i + 1 >= argc) {
                fprintf(stderr, "%s 需要參數\n", what);
                exit(1);
            }
            return argv[++i];
        };
        if (a == "-m" || a == "--model")        opt.model = next("-m");
        else if (a == "-p" || a == "--prompt")  opt.prompt = next("-p");
        else if (a == "--sys")                  opt.system = next("--sys");
        else if (a == "-c" || a == "--ctx")     opt.n_ctx = atoi(next("-c").c_str());
        else if (a == "-n")                     opt.n_predict = atoi(next("-n").c_str());
        else if (a == "-t")                     opt.n_threads = atoi(next("-t").c_str());
        else if (a == "-b" || a == "--ubatch")  opt.n_ubatch = atoi(next("-b").c_str());
        else if (a == "--temp")                 opt.temp = atof(next("--temp").c_str());
        else if (a == "--top-p")                opt.top_p = atof(next("--top-p").c_str());
        else if (a == "--top-k")                opt.top_k = atoi(next("--top-k").c_str());
        else if (a == "--no-chat")              opt.chat = false;
        else if (a == "--no-interactive")       opt.interactive = false;
        else if (a == "--bench")                opt.bench = true;
        else if (a == "--no-prefetch")          opt.no_prefetch = true;
        else if (a == "--arena-mb")             opt.arena_mb = atoi(next("--arena-mb").c_str());
        else if (a == "--ram-budget-mb")        opt.ram_budget_mb = atoi(next("--ram-budget-mb").c_str());
        else if (a == "--no-fadvise")           opt.fadvise = false;
        else if (a == "--stats-file")           opt.stats_file = next("--stats-file");
        else if (a == "--dump-logits")          opt.dump_logits = true;
        else if (a == "--logits-out")           opt.logits_out = next("--logits-out");
        else if (a == "--selftest")            opt.selftest = next("--selftest");
        else if (a == "--dump-tensors")        opt.dump_tensors = next("--dump-tensors");
        else if (a == "--no-pager")            opt.no_pager = true;
        else if (a == "--span-test")           opt.span_test = next("--span-test");
        else if (a == "-h" || a == "--help") {
            printf(
                "用法:sdq-chat -m <model.gguf> [選項]\n"
                "  -c, --ctx N          上下文長度(預設 4096;模型原生 262144)\n"
                "  -n N                 要生成的 token 數\n"
                "  -t N                 CPU 執行緒數\n"
                "  -b N                 ubatch 大小\n"
                "  -p, --prompt TEXT    提示詞\n"
                "  --sys TEXT           system 訊息\n"
                "  --no-chat            不要套 chat template(純 completion)\n"
                "  --no-interactive     跑完提示詞就結束\n"
                "  --bench              benchmark 模式(輸出 JSON 到 stats 檔)\n"
                "  --arena-mb N         expert arena 大小(MB)\n"
                "  --ram-budget-mb N    RAM 總預算(MB),arena 會配合縮減\n"
                "  --no-prefetch        關閉背景預取\n"
                "  --no-fadvise         讀完不丟 kernel page cache(會讓 RAM 帳目失真)\n"
                "  --stats-file PATH    指標 JSONL 輸出\n"
                "  --dump-logits        把每個 token 的 logits 存檔(驗證用)\n"
                "  --logits-out PATH    logits 輸出路徑\n");
            return 0;
        } else {
            fprintf(stderr, "未知參數:%s(用 --help 看用法)\n", a.c_str());
            return 1;
        }
    }

    if (opt.model.empty()) {
        fprintf(stderr, "缺少模型檔(-m 或 SDQ_MODEL_PATH)\n");
        return 1;
    }

    // ---- 環境變數(pager 讀)
    setenv("SDQ_MODEL_PATH", opt.model.c_str(), 1);
    // I/O 讀取執行緒數:預設 3 × 計算執行緒(上限 48)。
    // 必須在這裡設定 —— pager 的 I/O 池是在「載入模型」時啟動的,那已經比
    // context 建立早了;放到後面設定會讓池子只拿到下限 4 條執行緒。
    // 量測(repeat=3,中位數):序列讀 3.79 → 12 條 5.84 → 24 條 6.68 tok/s。
    if (!getenv("SDQ_IO_THREADS")) {
        const int hw_io = (int) std::max(1u, std::thread::hardware_concurrency());
        const int dflt_io = hw_io < 8 ? hw_io : 8;
        const int n_io   = opt.n_threads > 0 ? opt.n_threads : dflt_io;
        setenv("SDQ_IO_THREADS", std::to_string(n_io * 3).c_str(), 1);
    }
    // 只有明確指定時才設定,否則保留呼叫者從環境變數傳入的值
    // (舊版無條件 setenv 會讓 SDQ_PREFETCH=0 被悄悄蓋回 1,除錯時極易誤判)
    if (opt.no_prefetch) {
        setenv("SDQ_PREFETCH", "0", 1);
    }
    setenv("SDQ_FADV_DONTNEED", opt.fadvise ? "1" : "0", 1);
    if (opt.arena_mb > 0) {
        setenv("SDQ_ARENA_MB", std::to_string(opt.arena_mb).c_str(), 1);
    }
    if (opt.ram_budget_mb > 0) {
        setenv("SDQ_RAM_BUDGET_MB", std::to_string(opt.ram_budget_mb).c_str(), 1);
    }
    if (!opt.stats_file.empty()) {
        setenv("SDQ_STATS_FILE", opt.stats_file.c_str(), 1);
    }

    llama_backend_init();
    llama_numa_init(GGML_NUMA_STRATEGY_DISABLED);

    // ---- 模型載入:expert 權重交給 pager 的 buffer type(0 RAM)
    llama_model_params mparams = llama_model_default_params();
    mparams.n_gpu_layers = 0;
    mparams.load_mode = LLAMA_LOAD_MODE_MMAP;
    if (!opt.no_pager) {
        sdq_set_enabled(true);
        sdq_install_buft_overrides(&mparams);
    } else {
        printf("[sdq] --no-pager:對照模式,專家權重全部留在 RAM(需要 %s RAM)\n", "22GB");
    }

    printf("[sdq] 載入模型:%s\n", opt.model.c_str());
    const double t_load0 = now_s();
    llama_model * model = llama_model_load_from_file(opt.model.c_str(), mparams);
    if (!model) {
        fprintf(stderr, "[sdq] 模型載入失敗\n");
        return 1;
    }
    const double t_load = now_s() - t_load0;

    // ---- pager 初始化(讀 GGUF 檔頭、建頁面表、配置 arena)
    auto & P = pager::instance();
    if (!opt.no_pager && !sdq_init(opt.model)) {
        fprintf(stderr, "[sdq] pager 初始化失敗\n");
        return 1;
    }

    // ---- 張量 dump(A/B 比對用):在 op 真的算完的那一刻把資料寫出來
    FILE * tensor_fp = nullptr;
    dump_state dump_state_;
    if (!opt.dump_tensors.empty()) {
        tensor_fp = fopen(opt.dump_tensors.c_str(), "wb");
        dump_state_.fp = tensor_fp;
        if (const char * names = getenv("SDQ_DUMP_NAMES")) {
            std::string s = names;
            size_t pos = 0;
            while (pos <= s.size()) {
                size_t c = s.find(',', pos);
                if (c == std::string::npos) c = s.size();
                dump_state_.names.push_back(s.substr(pos, c - pos));
                pos = c + 1;
            }
        }
        g_dump = dump_state_;
    }

    // ---- context
    llama_context_params cparams = llama_context_default_params();
    cparams.n_ctx = (uint32_t) opt.n_ctx;
    cparams.n_batch = (uint32_t) opt.n_ctx;
    // ubatch 預設 128(不是 512)。理由是「8 GB 預算要真的裝得下」:
    // llama.cpp 的 CPU compute buffer 是按 n_ubatch 線性成長的,實測
    //   ubatch=512 → 502 MiB、256 → 251 MiB、128 → 125.5 MiB
    // 而 compute buffer 是在 pager 算完 arena 大小之後才配置的,所以它不算在
    // arena 的扣減裡。實測在 RLIMIT_DATA 8 GB 下:
    //   ubatch=512 → 提示詞超過 ~256 token 直接 std::bad_alloc(崩潰)
    //   ubatch=256 → 同一個提示詞一樣崩潰
    //   ubatch=128 → 正常,681 token 的 prefill 跑到 28.8 tok/s
    // 而且 ubatch=128 還更快(docs/04 實測 +7%),因為 prefill 分更多批、
    // 每批的 expert 聯集比較小、命中率比較高。
    cparams.n_ubatch = opt.n_ubatch > 0 ? (uint32_t) opt.n_ubatch : (uint32_t) std::min(opt.n_ctx, 128);
    // 執行緒預設值:不要直接用 hardware_concurrency()。
    // 實測(docs/04,repeat=3):mode C 下 t=8 最快,給更多執行緒反而變慢 ——
    //   t=8 4.02 / t=10 3.79 / t=12 3.55 / t=16 3.60 tok/s
    // 原因很直接:每層每 token 只有 n_expert_used = 8 個 expert 可以平行,
    // 執行緒超過 8 之後就只是多開執行緒搶同一批 CPU。
    // 下限取實際核心數,避免在 4 核心的目標機器上開太多。
    const int hw = (int) std::max(1u, std::thread::hardware_concurrency());
    const int dflt_threads = hw < 8 ? hw : 8;
    cparams.n_threads = opt.n_threads > 0 ? opt.n_threads : (int32_t) dflt_threads;
    cparams.n_threads_batch = cparams.n_threads;
    if (!opt.stats_file.empty()) {
        cparams.offload_kqv = false;
    }

    if (tensor_fp) {
        cparams.cb_eval = sdq_eval_callback;
        cparams.cb_eval_user_data = &g_dump;
    }

    llama_context * ctx = llama_init_from_model(model, cparams);
    if (!ctx) {
        fprintf(stderr, "[sdq] context 建立失敗(ctx=%d 太大?)\n", opt.n_ctx);
        return 1;
    }

    printf("[sdq] ctx=%d  threads=%d  ubatch=%u  RAM(RSS)=%zu MB  模型載入 %.1fs\n",
           opt.n_ctx, cparams.n_threads, cparams.n_ubatch, rss_mb(), t_load);
    print_mem_report("載入 + pager 後");

    // ---- sampler
    auto sparams = llama_sampler_chain_default_params();
    sparams.no_perf = true;
    llama_sampler * smpl = llama_sampler_chain_init(sparams);
    llama_sampler_chain_add(smpl, llama_sampler_init_top_k(opt.top_k));
    llama_sampler_chain_add(smpl, llama_sampler_init_top_p(opt.top_p, 1));
    llama_sampler_chain_add(smpl, llama_sampler_init_temp(opt.temp));
    llama_sampler_chain_add(smpl, llama_sampler_init_dist(LLAMA_DEFAULT_SEED));

    chat_state st;
    if (!opt.system.empty()) {
        st.msgs.push_back({"system", opt.system.c_str()});
    }

    std::vector<llama_token> decoded;

    const llama_vocab * vocab = llama_model_get_vocab(model);

    auto tokenize = [&](const std::string & text) {
        std::vector<llama_token> toks(text.size() + 16);
        int32_t n = llama_tokenize(vocab, text.c_str(), (int32_t) text.size(),
                                   toks.data(), (int32_t) toks.size(), true, true);
        if (n < 0) {
            toks.resize((size_t) -n);
            n = llama_tokenize(vocab, text.c_str(), (int32_t) text.size(),
                               toks.data(), (int32_t) toks.size(), true, true);
        }
        toks.resize(n);
        return toks;
    };

    // ---- 送進去一個 ubatch,回傳生成的 token(-1 = 需要繼續生成)
    std::function<int(llama_token, bool)> submit;
    uint64_t n_prefill_tok = 0;
    double t_prefill = 0;
    uint64_t n_decode_tok = 0;
    double t_decode = 0;
    uint64_t io_read0 = read_bytes_io();

    std::vector<float> logits_store;
    FILE * logits_fp = nullptr;
    if (opt.dump_logits) {
        logits_fp = fopen(opt.logits_out.empty() ? "sdq_logits.bin" : opt.logits_out.c_str(), "wb");
    }

    submit = [&](llama_token tok, bool last) -> int {
        const uint32_t ctx_size = llama_n_ctx(ctx);
        llama_memory_t mem = llama_get_memory(ctx);
        if (tok == 1) { // BOS:重開記憶體
            llama_memory_clear(mem, true);
            decoded.clear();
        }
        if (decoded.size() >= opt.n_ctx - 4) {
            // 環形丟掉最舊的 token(保留 opt.n_keep)
            const size_t drop = decoded.size() - (size_t) opt.n_keep;
            llama_memory_seq_rm(mem, 0, 0, (int32_t) drop);
            decoded.erase(decoded.begin(), decoded.begin() + (long) drop);
        }

        llama_batch b = llama_batch_get_one((llama_token *) &tok, 1);

        const bool is_prefill = decoded.empty();
        const double t0 = now_s();
        const int rc = llama_decode(ctx, b);
        const double dt = now_s() - t0;
        if (rc != 0) {
            fprintf(stderr, "\n[sdq] llama_decode 失敗 rc=%d(記憶體不足?)\n", rc);
            exit(3);
        }
        decoded.push_back(tok);

        // 每個 ubatch 結束 → 更新路由歷史並觸發下一輪預取
        P.end_token();

        if (is_prefill) {
            n_prefill_tok++;
            t_prefill += dt;
        } else {
            n_decode_tok++;
            t_decode += dt;
            P.get_stats().tokens.fetch_add(1, std::memory_order_relaxed);
        }

        if (logits_fp) {
            const int32_t n_logits = llama_vocab_n_tokens(vocab);
            const float * lg = llama_get_logits_ith(ctx, -1);
            if (lg) {
                logits_store.assign(lg, lg + n_logits);
                fwrite(&n_logits, sizeof(int32_t), 1, logits_fp);
                fwrite(logits_store.data(), sizeof(float), n_logits, logits_fp);
                fflush(logits_fp);
            }
        }

        if (last) {
            return -1;
        }
        const llama_token id = llama_sampler_sample(smpl, ctx, -1);
        return (int) id;
    };

    // ---- 產生 prompt
    auto run_prompt = [&](const std::string & user_text, bool add_to_history) {
        if (opt.chat) {
            st.msgs.push_back({"user", user_text.c_str()});
        }
        std::string text;
        if (opt.chat) {
            const char * tmpl = llama_model_chat_template(model, nullptr);
            size_t n_chars = 0;
            for (const auto & m : st.msgs) {
                n_chars += strlen(m.role) + strlen(m.content) + 32;
            }
            std::vector<char> buf(2 * n_chars + 1024);
            int32_t len = llama_chat_apply_template(tmpl, st.msgs.data(), st.msgs.size(),
                                                    true, buf.data(), (int32_t) buf.size());
            if (len > (int32_t) buf.size()) {
                buf.resize((size_t) len + 1);
                llama_chat_apply_template(tmpl, st.msgs.data(), st.msgs.size(), true,
                                          buf.data(), (int32_t) buf.size());
            }
            text.assign(buf.data());
        } else {
            text = user_text;
        }

        auto toks = tokenize(text);
        const int n_prompt = (int) toks.size();
        const uint64_t io0 = read_bytes_io();

        // 分批送進去(每批 = 一個 ubatch,控制記憶體)
        const int32_t ub = (int32_t) cparams.n_ubatch;
        llama_sampler_reset(smpl);
        int32_t pos = 0;
        for (int32_t i = 0; i < n_prompt; i += ub) {
            const int32_t n = std::min(ub, n_prompt - i);
            llama_batch b = llama_batch_init((uint32_t) n, 0, 1);
            for (int32_t j = 0; j < n; j++) {
                b.token[j] = toks[i + j];
                b.pos[j] = pos++;
                b.n_seq_id[j] = 1;
                b.seq_id[j][0] = 0;
            }
            b.n_tokens = (uint32_t) n;
            const double t0 = now_s();
            const int rc = llama_decode(ctx, b);
            t_prefill += now_s() - t0;
            n_prefill_tok += (uint64_t) n;
            if (rc != 0) {
                fprintf(stderr, "\n[sdq] prefill 失敗 rc=%d(%d tokens)\n", rc, n);
                exit(3);
            }
            P.end_token();
            llama_batch_free(b);
        }
        const double t_p = now_s();
        (void) t_p;
        (void) io0;

        if (add_to_history && opt.chat) {
            st.msgs.push_back({"assistant", ""});
        }
        return n_prompt;
    };

    // ---- 產生
    auto generate = [&](int32_t n_max) {
        const uint64_t io0 = read_bytes_io();
        const uint64_t io_read_before = io0;
        (void) io_read_before;
        const double t0 = now_s();
        int32_t n_gen = 0;
        llama_token id = llama_sampler_sample(smpl, ctx, -1);
        fputs("assistant: ", stdout);
        fflush(stdout);
        while (n_gen < n_max) {
            const int rc = submit(id, /*last=*/n_gen + 1 >= n_max);
            if (rc < 0) {
                break;
            }
            id = (llama_token) rc;
            char piece_buf[256];
            const int n_piece = llama_token_to_piece(vocab, id, piece_buf, sizeof(piece_buf), 0, true);
            const std::string piece(piece_buf, (size_t) std::max(0, n_piece));
            fputs(piece.c_str(), stdout);
            fflush(stdout);
            n_gen++;
        }
        fputs("\n", stdout);
        const double dt = now_s() - t0;
        const uint64_t io1 = read_bytes_io();
        fprintf(stderr, "[sdq] decode: %d tok / %.2fs = %.2f tok/s | SSD 讀 %.1f MB (%.1f MB/tok) | RSS %zu MB\n",
                n_gen, dt, dt > 0 ? n_gen / dt : 0.0,
                (io1 - io0) / 1048576.0, n_gen ? (io1 - io0) / 1048576.0 / n_gen : 0.0, rss_mb());
        if (opt.chat) {
            for (auto & m : st.msgs) {
                if (strcmp(m.role, "assistant") == 0) {
                    m.content = "";
                }
            }
        }
        P.dump_stats("decode");
        return n_gen;
    };

    // ---- 合併讀取自我測試:--span-test IL:E0:E1:...
    if (!opt.span_test.empty()) {
        std::vector<int> parts;
        std::string cur_s;
        for (char ch : opt.span_test + ":") {
            if (ch == ':') { parts.push_back(atoi(cur_s.c_str())); cur_s.clear(); }
            else cur_s += ch;
        }
        if (parts.size() < 3) {
            fprintf(stderr, "用法:--span-test IL:E0:E1[:E2...](E 要連續)\n");
            return 1;
        }
        const int il = parts[0];
        std::vector<int32_t> ex;
        for (size_t i = 1; i < parts.size(); i++) {
            ex.push_back(parts[i]);
        }
        std::vector<int32_t> slots(ex.size(), -1);
        for (size_t i = 0; i < ex.size(); i++) {
            slots[i] = P.reserve_slot(il, ex[i]);
        }
        const uint64_t got = P.read_expert_span(il, ex.data(), (int) ex.size(), slots.data());
        printf("[sdq-span-test] L%d experts=%zu 讀取 %.2f MB\n", il, ex.size(), got / 1048576.0);
        int bad = 0;
        for (size_t i = 0; i < ex.size(); i++) {
            std::vector<uint8_t> one((size_t) P.layer(il)->experts[ex[i]].bytes, 0);
            if (!P.read_expert(il, ex[i], one.data())) {
                printf("  單獨讀取失敗\n");
                return 1;
            }
            const uint8_t * span_ptr = P.slot_ptr(slots[i]);
            size_t diff_at = (size_t) -1;
            for (size_t b = 0; b < one.size(); b++) {
                if (one[b] != span_ptr[b]) { diff_at = b; break; }
            }
            printf("  expert %4d: %s", ex[i], diff_at == (size_t) -1 ? "一致 ✅" : "不一致 ❌");
            if (diff_at != (size_t) -1) {
                printf("(第一個不同的 byte 在 %zu / %zu,slot=%d)", diff_at, one.size(), slots[i]);
                bad++;
            }
            printf("\n");
        }
        sdq_shutdown();
        llama_free(ctx);
        llama_model_free(model);
        llama_backend_free();
        return bad ? 1 : 0;
    }

    // ---- 自我測試模式:--selftest IL:IE:SEED:OUT.bin
    if (!opt.selftest.empty()) {
        int il = 0, ie = 0, seed = 1234, n_tok = 1;
        std::string outpath;
        {
            std::vector<std::string> parts;
            std::string cur_s;
            for (char c : opt.selftest + ":") {
                if (c == ':') { parts.push_back(cur_s); cur_s.clear(); }
                else cur_s += c;
            }
            if (parts.size() >= 3) {
                il = atoi(parts[0].c_str());
                ie = atoi(parts[1].c_str());
                seed = atoi(parts[2].c_str());
            }
            if (parts.size() >= 4) {
                outpath = parts[3];
            }
            if (parts.size() >= 5) {
                n_tok = atoi(parts[4].c_str());
            }
        }
        const int64_t n_embd = P.info().n_embd;
        const int64_t n_ff = P.info().n_ff_exp;
        std::vector<float> x((size_t) n_embd * n_tok);
        uint32_t rng = (uint32_t) seed * 2654435761u + 1u;
        for (auto & v : x) {
            rng = rng * 1664525u + 1013904223u;
            v = ((float) (rng >> 8) / (float) (1 << 24)) * 2.0f - 1.0f;
        }
        std::vector<float> io((size_t) n_embd * n_tok * 2);
        const int rc = sdq_moe_selftest(il, ie, x.data(), io.data(), n_embd, n_ff, n_tok);
        if (rc == 0 && !outpath.empty()) {
            FILE * f = fopen(outpath.c_str(), "wb");
            fwrite(io.data(), sizeof(float), io.size(), f);
            fclose(f);
            printf("[sdq] selftest 結果已寫到 %s(layer=%d expert=%d seed=%d n_embd=%lld n_ff=%lld)\n",
                   outpath.c_str(), il, ie, seed, (long long) n_embd, (long long) n_ff);
        }
        sdq_shutdown();
        llama_free(ctx);
        llama_model_free(model);
        llama_backend_free();
        return rc;
    }

    // ---- 主流程
    if (!opt.prompt.empty()) {
        run_prompt(opt.prompt, true);
        generate(opt.n_predict);
    }

    if (opt.bench) {
        // benchmark:固定提示詞 ×3,量 prefill / decode / 分頁指標
        const std::vector<std::string> prompts = {
            "請用中文簡短介紹 SSD、RAM、CPU 之間的資料分層是什麼,以及為什麼快取很重要。",
            "Write a short paragraph explaining how a mixture-of-experts model routes tokens to experts.",
            "請列出十種日常會用到電腦的工作,並各用一句話說明。",
        };
        for (int round = 0; round < 3; round++) {
            fprintf(stderr, "[bench] round %d\n", round);
            llama_memory_clear(llama_get_memory(ctx), true);
            decoded.clear();
            P.get_stats().tokens.store(0);
            run_prompt(prompts[round % prompts.size()], false);
            generate(48);
            P.dump_stats(round == 0 ? "bench_warm" : "bench_hot");
        }
    }

    if (opt.interactive) {
        printf("\n(輸入文字開始對話;/exit 離開、/reset 重設、/stats 顯示指標)\n");
        std::string line;
        while (true) {
            printf("\nuser> ");
            fflush(stdout);
            if (!std::getline(std::cin, line)) {
                break;
            }
            if (line == "/exit" || line == "/quit") {
                break;
            }
            if (line == "/reset") {
                llama_memory_clear(llama_get_memory(ctx), true);
                decoded.clear();
                st.msgs.clear();
                if (!opt.system.empty()) {
                    st.msgs.push_back({"system", opt.system.c_str()});
                }
                printf("(已重設)\n");
                continue;
            }
            if (line == "/stats") {
                P.dump_stats("manual");
                fprintf(stderr, "hit_rate=%.1f%%  arena=%zu MB  RSS=%zu MB\n",
                        P.hit_rate() * 100, P.used_bytes() / 1048576, rss_mb());
                continue;
            }
            if (line.empty()) {
                continue;
            }
            run_prompt(line, true);
            generate(opt.n_predict);
        }
    }

    P.get_stats().moe_calls.fetch_add(0);
    sdq_shutdown();

    fprintf(stderr, "\n[sdq] ===== 總結 =====\n");
    fprintf(stderr, "[sdq] prefill: %" PRIu64 " tok / %.2fs = %.1f tok/s\n",
            n_prefill_tok, t_prefill, t_prefill > 0 ? n_prefill_tok / t_prefill : 0.0);
    const uint64_t io_end = read_bytes_io();
    fprintf(stderr, "[sdq] 全程 SSD 實際讀取: %.2f GB\n", (io_end - io_read0) / 1073741824.0);
    print_mem_report("結束時");

    if (logits_fp) {
        fclose(logits_fp);
    }
    llama_sampler_free(smpl);
    llama_free(ctx);
    llama_model_free(model);
    llama_backend_free();
    return 0;
}