Add full report (moved from model card)
Browse files- docs/REPORT.md +175 -0
docs/REPORT.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Full report (moved from the model card)
|
| 2 |
+
|
| 3 |
+
Detailed build, accuracy, memory-tier, throughput and hot/cold-expert results. Raw logs are in `../results/`.
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
# Flint EPQ for llama.cpp
|
| 7 |
+
|
| 8 |
+
CPU expert paging for the Unsloth `flint-UD-Q4_K_XL.gguf` checkpoint. Routed gate/up/down expert weights stay on disk and are copied into a bounded RAM arena as needed. Flint's router, selected-expert weights, shared expert, attention and dense layers remain on the upstream llama.cpp graph.
|
| 9 |
+
|
| 10 |
+
All numbers in this card come from one build: llama.cpp `970b51c` plus `flint-epq.patch` (sha256 `94559012e2ea65cad6ce2175927c19095d13256bb4ec8fb2fdcd163570e00385`). Raw logs are in `results/`.
|
| 11 |
+
|
| 12 |
+
Configurations:
|
| 13 |
+
|
| 14 |
+
- **native**: unmodified llama.cpp MoE path (`SDQ_PAGER=0`), the reference.
|
| 15 |
+
- **v1**: expert paging with the per-expert custom op ("mode C"). `SDQ_PRELOAD=0 SDQ_ROW_PARALLEL=0`.
|
| 16 |
+
- **v2**: v1 plus preload of all experts into the arena and row-parallel decode for gated experts. `SDQ_PRELOAD=1 SDQ_ROW_PARALLEL=1`, arena ≥ 28.6 GiB.
|
| 17 |
+
- **v2 forced** (ablation): v2 with `SDQ_ROWPAR_MAX_MISS=100000`. Row-parallel runs even when some needed experts are not resident.
|
| 18 |
+
|
| 19 |
+
## Source model
|
| 20 |
+
|
| 21 |
+
- GGUF: [`unsloth/flint-GGUF`](https://huggingface.co/unsloth/flint-GGUF), `flint-UD-Q4_K_XL.gguf` (25,054,688,704 bytes)
|
| 22 |
+
- Flint architecture: 49 layers, first 2 dense, followed by 47 MoE layers; 256 experts, top-6; 12,032 routed experts in total
|
| 23 |
+
- Routed expert tensors: 21.47 GiB. Gate/up quantization types vary by layer (`IQ3_S` 1 layer, `IQ4_XS` 25, `Q4_K` 3, `Q5_K` 18); down uses `IQ4_XS` 16, `Q5_K` 29, `Q6_K` 1, `Q8_0` 1. Every type is covered by the pager's dot-product and activation-quantization tables.
|
| 24 |
+
- Size per expert (gate + up + down): 2.44 MiB at most (2,555,904 bytes), 1.83 MiB on average. One expert is far smaller than any memory tier.
|
| 25 |
+
|
| 26 |
+
## Test system
|
| 27 |
+
|
| 28 |
+
| Component | Specification |
|
| 29 |
+
|---|---|
|
| 30 |
+
| CPU | Intel Xeon Platinum 8559C, 2 sockets × 48 cores, SMT enabled (192 host logical CPUs) |
|
| 31 |
+
| CPU features | AVX2, AVX-512, VNNI, BF16; AMX present on the host but disabled in this build |
|
| 32 |
+
| Container CPU quota | 16 CPUs (`cpu.max` = 1,600,000 / 100,000). All runs below used 16 threads |
|
| 33 |
+
| Host memory | 2.0 TiB |
|
| 34 |
+
| Container memory limit | 104,000,000,000 bytes (about 96.9 GiB), cgroup v2; no writable cgroup, so no kernel-enforced limit |
|
| 35 |
+
| Container swap limit | 2,936,848,384 bytes (about 2.74 GiB); no test used swap |
|
| 36 |
+
| GPU | None; CPU-only build and inference |
|
| 37 |
+
| OS / kernel | Amazon Linux 2023 x86_64, Linux 6.18.48 |
|
| 38 |
+
|
| 39 |
+
## Build
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
git clone https://github.com/ggml-org/llama.cpp
|
| 43 |
+
cd llama.cpp
|
| 44 |
+
git checkout 970b51ce7c9ba5fff0726c5e9994f1c2f0e4eedc
|
| 45 |
+
curl -L 'https://huggingface.co/HelloSun/fint-epqv2/resolve/main/flint-epq.patch' -o flint-epq.patch
|
| 46 |
+
git apply flint-epq.patch
|
| 47 |
+
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DLLAMA_CURL=OFF \
|
| 48 |
+
-DGGML_NATIVE=OFF -DGGML_AVX2=ON -DGGML_AVX512=ON \
|
| 49 |
+
-DGGML_AVX512_VBMI=ON -DGGML_AVX512_VNNI=ON -DGGML_AVX512_BF16=ON \
|
| 50 |
+
-DGGML_AMX_INT8=OFF -DGGML_AMX_TILE=OFF -DGGML_AMX_BF16=OFF
|
| 51 |
+
cmake --build build -j 8 --target llama-cli llama-server llama-completion llama-perplexity
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
Run commands. The model is `models/flint-UD-Q4_K_XL.gguf` (`hf download unsloth/flint-GGUF flint-UD-Q4_K_XL.gguf --local-dir models`).
|
| 55 |
+
|
| 56 |
+
```bash
|
| 57 |
+
# native
|
| 58 |
+
SDQ_PAGER=0 build/bin/llama-cli -m models/flint-UD-Q4_K_XL.gguf -c 512 -p 'Explain in one sentence why the sky is blue.' -n 32 -t 16 --temp 0 -st --simple-io
|
| 59 |
+
|
| 60 |
+
# v1 with a 1 GiB expert arena (set the size in MiB with SDQ_ARENA_MB)
|
| 61 |
+
SDQ_PAGER=1 SDQ_ARENA_MB=1024 SDQ_PREFETCH=0 SDQ_PRELOAD=0 SDQ_ROW_PARALLEL=0 build/bin/llama-cli -m models/flint-UD-Q4_K_XL.gguf -c 512 -p 'Explain in one sentence why the sky is blue.' -n 32 -t 16 --temp 0 -st --simple-io
|
| 62 |
+
|
| 63 |
+
# v2 (arena must hold all 12,032 experts: >= 28.6 GiB)
|
| 64 |
+
SDQ_PAGER=1 SDQ_ARENA_MB=29400 SDQ_PREFETCH=0 SDQ_PRELOAD=1 SDQ_ROW_PARALLEL=1 build/bin/llama-cli -m models/flint-UD-Q4_K_XL.gguf -c 512 -p 'Explain in one sentence why the sky is blue.' -n 32 -t 16 --temp 0 -st --simple-io
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
At exit the program prints `MoE op 路徑計數:row-parallel N 次,mode C M 次`, which shows which path each MoE op used.
|
| 68 |
+
|
| 69 |
+
`bench.sh <GiB> <pager 0|1>` is the memory benchmark used below. Version is chosen with `SDQ_PRELOAD` and `SDQ_ROW_PARALLEL`, and `ARENA_MB` overrides the arena size.
|
| 70 |
+
|
| 71 |
+
## Accuracy
|
| 72 |
+
|
| 73 |
+
Reference: native llama.cpp on the same build. Text: the first 6,000 bytes of llama.cpp's `README.md` (`results/acc/corpus.txt`).
|
| 74 |
+
|
| 75 |
+
| Test | Native | v1 | v2 |
|
| 76 |
+
|---|---|---|---|
|
| 77 |
+
| Greedy, 24 tokens, 8 threads | reference | identical to native | identical to native |
|
| 78 |
+
| Greedy, 24 tokens, 16 threads | reference | differs from native | identical to v1 |
|
| 79 |
+
| Prefill perplexity (`-ub 64`, 8 threads, 4 chunks) | 10.7790 ± 1.6026 | 10.8014 ± 1.6076 (+0.21 %) | 10.8014 ± 1.6076 |
|
| 80 |
+
| Prefill KL vs native (mean / median / max) | — | 0.019120 / 0.004286 / 0.455 | 0.019120 / 0.004286 / 0.455 |
|
| 81 |
+
| Prefill top-1 agreement vs native | — | 92.3 % ± 1.2 | 92.3 % ± 1.2 |
|
| 82 |
+
| Decode KL vs native (`-ub 1`, 16 threads, 2 chunks) | reference (PPL 14.7355) | 0.044057; top-1 92.5 % | 0.044057; top-1 92.5 % |
|
| 83 |
+
| Decode, v2 forced with 1 GiB arena, KL vs native | — | — | 0.044057; top-1 92.5 % |
|
| 84 |
+
| Control: native `-ub 64` vs native `-ub 1` | — | — | KL 0.034464; top-1 90.2 % |
|
| 85 |
+
| Decode, v2 vs v1 | — | — | KL 0.000001 (max 0.000038); top-1 100 % |
|
| 86 |
+
|
| 87 |
+
Findings:
|
| 88 |
+
|
| 89 |
+
- v2 matches v1 in prefill exactly. Prefill does not use row-parallel, so this shows the preload does not change results.
|
| 90 |
+
- In decode, v2 and v1 differ by KL 1e-6 with 100 % top-1 agreement. Row-parallel is numerically equivalent to mode C but not bit-identical.
|
| 91 |
+
- EPQ is not bit-identical to native. Float summation order differs, and 8-bit activation quantization amplifies that difference through the layers. The native `-ub 64` vs `-ub 1` control shows the same magnitude of divergence (KL 0.034, top-1 90 %). The EPQ deviation is in the range of reordering sensitivity for this checkpoint.
|
| 92 |
+
- Greedy text identity is fragile. At 8 threads v1 and v2 match native for 24 tokens; at 16 threads both diverge from native by a near-tie argmax flip.
|
| 93 |
+
|
| 94 |
+
## Memory tiers (enforced RSS watchdog)
|
| 95 |
+
|
| 96 |
+
`bench.sh` samples process-tree RSS every 20 ms and kills the run if it exceeds the tier. This is an enforced sampled limit, not a kernel limit: RSS can exceed the threshold briefly between samples, and the count includes file-backed pages as reported by `/proc`.
|
| 97 |
+
|
| 98 |
+
Workload: context 128, batch 32, ubatch 16, 8 generated tokens, 16 threads. Each row is the median of 3 runs. "killed" means the watchdog terminated all 3 runs (exit −9), with the peak RSS at the kill shown.
|
| 99 |
+
|
| 100 |
+
| Tier | Native | v1 | v2 (as designed) | v2 forced |
|
| 101 |
+
|---|---|---|---|---|
|
| 102 |
+
| 2 GiB, arena 256 MiB | killed (peak 2.04–2.07 GiB) | killed (peak 2.00 GiB) | killed (peak 2.00 GiB) | killed (peak 2.00 GiB) |
|
| 103 |
+
| 2 GiB, arena 128 MiB | — | pass, peak 1.887 GiB; prompt 6.2 t/s; gen 3.9 t/s | pass, peak 1.887 GiB; prompt 6.1 t/s; gen 3.9 t/s; RP 0 / mode C 516 | pass, peak 1.883 GiB; prompt 6.1 t/s; gen 3.9 t/s; RP 425 / mode C 91 |
|
| 104 |
+
| 4 GiB, arena 1 GiB | killed (peak 4.09–4.34 GiB) | pass, 2.764 GiB; prompt 7.0; gen 4.6 | pass, 2.764 GiB; prompt 7.3; gen 4.6 | pass, 2.758 GiB; prompt 7.0; gen 4.6; RP 425 |
|
| 105 |
+
| 8 GiB, arena 3 GiB | killed (peak 8.12–8.14 GiB) | pass, 4.764 GiB; prompt 5.5; gen 3.6 | pass, 4.764 GiB; prompt 8.7; gen 4.9 | pass, 4.757 GiB; prompt 9.2; gen 4.8; RP 425 |
|
| 106 |
+
| 16 GiB, arena 10 GiB | killed (peak 16.01 GiB) | pass, 11.766 GiB; prompt 10.3; gen 5.4 | pass, 11.766 GiB; prompt 11.8; gen 5.5; RP 12 / mode C 504 | pass, 11.759 GiB; prompt 12.1; gen 5.3; RP 425 |
|
| 107 |
+
| 32 GiB | pass, 24.286 GiB; prompt 58.1; gen 21.3 | pass, 19.765 GiB (arena 18 GiB, 7,561 of 12,032 experts); prompt 12.2; gen 5.5 | pass, 30.473 GiB (arena 28.7 GiB, all experts); prompt 31.6; gen 14.6; RP 425 / mode C 91 | pass, 19.759 GiB (arena 18 GiB); prompt 11.7; gen 5.5; RP 425 |
|
| 108 |
+
|
| 109 |
+
Generation speeds are noisy. Some runs with identical code paths differ by 20–30 % (for example v1 at 8 GiB ranged 3.2–4.9 t/s). Treat differences under about 20 % as unresolved.
|
| 110 |
+
|
| 111 |
+
What this shows:
|
| 112 |
+
|
| 113 |
+
- **Native llama.cpp needs about 24.3 GiB.** It is killed at every tier below 32 GiB.
|
| 114 |
+
- **The 2 GiB tier requires an arena of about 128 MiB.** A 256 MiB arena is killed at the threshold.
|
| 115 |
+
- **v2 changes behavior only at 32 GiB with all experts resident** (425 row-parallel decode ops). Below that, the path is the same as v1 (16 GiB: 12 of 516 ops).
|
| 116 |
+
- **At 32 GiB, v2 generates 14.6 t/s against 5.5 t/s for v1 (about 2.7×)**, and it uses 30.5 GiB of RAM.
|
| 117 |
+
- **Forced row-parallel shows no reliable speed gain** at any tier. Its medians fall within the run-to-run spread of v1 and v2. Its output is unchanged.
|
| 118 |
+
|
| 119 |
+
## Throughput
|
| 120 |
+
|
| 121 |
+
Workload: context 512, batch 128, ubatch 32, 32 generated tokens, 16 threads, 3 runs each (median; range in brackets). No watchdog applied.
|
| 122 |
+
|
| 123 |
+
| Configuration | Prompt | Generation | Peak RSS |
|
| 124 |
+
|---|---:|---:|---:|
|
| 125 |
+
| native (`SDQ_PAGER=0`) | 47.4 t/s | 19.4 t/s (17.9–21.0) | 24.31 GiB |
|
| 126 |
+
| v1, arena 18 GiB | 12.7 t/s | 5.5 t/s (5.2–6.0) | 19.79 GiB |
|
| 127 |
+
| v1, arena 28.7 GiB, no preload | 11.8 t/s | 5.3 t/s (5.3–5.6) | 30.51 GiB |
|
| 128 |
+
| **v2**, arena 28.7 GiB, preload + row-parallel | **34.1 t/s** | **13.7 t/s** (13.4–13.8) | 30.50 GiB |
|
| 129 |
+
|
| 130 |
+
v2 generates about 2.5× faster than v1 (13.7 vs 5.5 t/s) and about 70 % of native generation speed. The prompt phase is also faster in v2, probably because the preload removes SSD reads during prefill (not separately measured).
|
| 131 |
+
|
| 132 |
+
## Hot and cold experts
|
| 133 |
+
|
| 134 |
+
Routing is not uniform. The hot/cold code (profile-pinned experts) was tested to see whether a cache that keeps frequently used experts resident would help.
|
| 135 |
+
|
| 136 |
+
- **Skew is real.** Per layer, the Gini coefficient of expert request counts is 0.38–0.62. The top 6 % of experts take 19–36 % of requests (uniform would be 6 %).
|
| 137 |
+
- **A profile transfers across texts moderately.** The top 10 % of experts per layer, taken from a profiling text, cover 28 % of requests in a held-out text (uniform: 10 %). Per-layer rank correlation between the two texts has median 0.58.
|
| 138 |
+
- **The working set is larger than small arenas.** One token uses 47 layers × 6 = 282 distinct (layer, expert) pairs, and an expert is typically reused only on a later token. Under LRU a cache of about 282 slots (≈ 0.7 GiB) or less gets almost no hits.
|
| 139 |
+
- **Pinning a profiled hot set helps only in a narrow band.** Measured on the held-out text, 514 tokens by the pager's count, with pinning at 25 % of arena slots:
|
| 140 |
+
|
| 141 |
+
| Arena | LRU hit rate | Profile-pinned hit rate | Difference |
|
| 142 |
+
|---:|---:|---:|---:|
|
| 143 |
+
| 128 MiB (52 slots) | 0 % | 0 % | 0 |
|
| 144 |
+
| 256 MiB (105 slots) | 0 % | 0 % | 0 |
|
| 145 |
+
| 1 GiB (420 slots) | 19.3 % | 21.5 % | +2.2 points |
|
| 146 |
+
| 3 GiB (1,260 slots) | 40.3 % | 37.5 % | −2.8 points |
|
| 147 |
+
|
| 148 |
+
Perplexity is identical (6.4793) for every LRU and pinned run, so caching changes speed only.
|
| 149 |
+
|
| 150 |
+
A trace replay (`results/hot/sim.py`, 1,017 tokens, uniform slots) gives a similar picture: +3.2 points at 1 GiB and +0.7 at 3 GiB, and it predicts a small hit rate at 128–256 MiB that the real pager does not reach.
|
| 151 |
+
|
| 152 |
+
Two problems in the earlier hot/cold code were fixed in this build. Hits were not counted in the profile (only misses were), and with no `SDQ_HOT_PIN` every profiled expert was pinned, which left no cold slot to evict. The default is now 25 % of arena slots, and pinning still requires a profile (`SDQ_PROFILE_IN`). Without one the cache is plain LRU.
|
| 153 |
+
|
| 154 |
+
**Conclusion.** The hot/cold effect is real but modest, and it changes sign with arena size. The main limit is the per-token working set: below about 0.7 GiB, most experts are re-read from SSD on every token, which is why the 2–4 GiB tiers are slow. Pinning stays opt-in.
|
| 155 |
+
|
| 156 |
+
Files: `results/hot/` (profile, scripts, route traces as `.gz`, stats, logs, analysis and simulation output).
|
| 157 |
+
|
| 158 |
+
## Version coverage
|
| 159 |
+
|
| 160 |
+
| Feature | Version | Status for Flint |
|
| 161 |
+
|---|---|---|
|
| 162 |
+
| Expert paging with per-expert custom op ("mode C") | v1 | Active in every run (prefill always uses it) |
|
| 163 |
+
| Preload all experts into the arena | v2 | Active when the arena holds all 12,032 experts (≥ 28.6 GiB) |
|
| 164 |
+
| Row-parallel decode for gated experts | v2 | Active when all needed experts are resident and the thread count exceeds the number of needed experts (decode, ≥ 7 threads). `SDQ_ROWPAR_MAX_MISS` relaxes the residency guard (ablation) |
|
| 165 |
+
| Nemotron no-gate / ReLU² path | v1, v2 | Not used by Flint |
|
| 166 |
+
|
| 167 |
+
The gated SiLU path routes through the pager. The v2 row-parallel path computes gate and up for the same row block and applies the same SwiGLU as mode C.
|
| 168 |
+
|
| 169 |
+
Flint's upstream `build_moe_ffn` applies a power-of-two amax rescale to the down-projection input and undoes it on the output. The rescale is exact in F32 and Q8_K quantization is scale-invariant, so the paged path omits it. The accuracy results above, measured against native on the same build, are the test of this choice.
|
| 170 |
+
|
| 171 |
+
## Attribution
|
| 172 |
+
|
| 173 |
+
The pager is adapted from HelloSun's Nemotron EPQ [v1 and v2](https://huggingface.co/HelloSun/NVIDIA-Nemotron-3.5-Bolt-74B-A9B-24GB-GGUF_EPQ).
|
| 174 |
+
|
| 175 |
+
The GGUF weights remain hosted by Unsloth. This repository contains source, patch, scripts, logs and test notes only.
|