squirty mcgirt commited on
Commit
df9ff2e
·
verified ·
1 Parent(s): 795208f

DSV4-Flash homunculus: scale-model instrument + qualification (median 1.4% prediction error across 10 configs)

Browse files
.gitattributes CHANGED
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ h0-skeleton-f16.gguf filter=lfs diff=lfs merge=lfs -text
37
+ h1-topology-f16.gguf filter=lfs diff=lfs merge=lfs -text
38
+ h1-topology-mxfp4.gguf filter=lfs diff=lfs merge=lfs -text
39
+ homunculus-poster.png filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ tags:
4
+ - gguf
5
+ - llama.cpp
6
+ - benchmark
7
+ - infrastructure
8
+ - deepseek
9
+ - not-a-language-model
10
+ library_name: gguf
11
+ ---
12
+
13
+ # DSV4-Flash Homunculus
14
+
15
+ **A scale model of DeepSeek-V4-Flash, for measuring inference rigs.**
16
+
17
+ > ## ⚠️ The weights are random. This is not a language model.
18
+ >
19
+ > It has DeepSeek-V4-Flash's architecture and op graph, but every tensor is random and it was
20
+ > never trained or distilled from anything. It will happily load and generate at 300 tokens/s,
21
+ > and every one of those tokens will be garbage. **Do not download this expecting a small
22
+ > DeepSeek-V4.** It is a test article — a piece of measuring equipment shaped like a model.
23
+ >
24
+ > The one thing it does carry from the real model is **the tokenizer**, verbatim (129,280
25
+ > tokens) — see [License and provenance](#license-and-provenance).
26
+
27
+ ---
28
+
29
+ ## What it's for
30
+
31
+ Tuning a large model across multiple machines is slow because every data point costs a model
32
+ load. Changing one flag on a 145 GiB two-node setup costs five minutes before you learn
33
+ anything, so a sweep of ten configurations is an hour of mostly waiting.
34
+
35
+ The homunculus is the same graph with the width taken out: same operators, same expert count,
36
+ same routing, same cross-device split — 606× less weight. A configuration that takes **316
37
+ seconds** to measure on the real model takes **2.9 seconds** on this one.
38
+
39
+ The question is whether the toy's answers transfer. On a two-node Strix Halo pair they do,
40
+ with a stated and measured domain of validity.
41
+
42
+ ![qualification results](homunculus-poster.png)
43
+
44
+ ## The result
45
+
46
+ Across 10 configurations measured on both models, predicted decode throughput for
47
+ DeepSeek-V4-Flash landed within a **median 1.4%** of measured, **9 of 10 inside 5%**, with **no
48
+ fitted parameters**:
49
+
50
+ ```
51
+ predicted_tg = 1000 / (read_budget_gb / gbs * 1000 + n_layer_real * f_toy)
52
+ ```
53
+
54
+ - `read_budget_gb` — bytes read per token, computed from the **real** model's tensor table.
55
+ Free: no GPU, no load, it is just arithmetic over the GGUF header.
56
+ - `gbs` — the rig's memory bandwidth, measured once.
57
+ - `n_layer_real` — the real model's layer count.
58
+ - `f_toy` — per-layer overhead in ms, **measured on the homunculus in about three seconds**.
59
+
60
+ | configuration | f toy | f real | predicted | measured | error |
61
+ |---|---|---|---|---|---|
62
+ | two-node, default | 240 µs | 238 µs | 16.28 | 16.29 | −0.0% |
63
+ | expert parallel OFF | 252 µs | 253 µs | 16.14 | 16.13 | +0.1% |
64
+ | two-node, `-ts 1/1` | 239 µs | 239 µs | 16.29 | 16.27 | +0.1% |
65
+ | batch 4096 / 2048 | 239 µs | 239 µs | 16.29 | 16.27 | +0.1% |
66
+ | `mmap` | 239 µs | 241 µs | 16.29 | 16.25 | +0.2% |
67
+ | `mmap` + `mlock` | 239 µs | 218 µs | 16.29 | 16.52 | −1.4% |
68
+ | direct I/O | 240 µs | 216 µs | 16.28 | 16.54 | −1.5% |
69
+ | `--no-mmap` | 240 µs | 216 µs | 16.28 | 16.54 | −1.6% |
70
+ | `mlock` | 240 µs | 216 µs | 16.28 | 16.54 | −1.6% |
71
+ | expert parallel ON | 601 µs | 810 µs | 13.00 | 11.64 | **+11.7%** |
72
+
73
+ Decode t/s, two nodes over 100GbE, `llama-bench` pp512/tg128, 3 reps.
74
+
75
+ ## Why it works
76
+
77
+ A token costs `bytes / bandwidth + n_layer × f`.
78
+
79
+ The first term is **bandwidth** and scales with the weights, so a toy cannot see it — but you
80
+ do not need to measure it, because it falls out of the tensor table.
81
+
82
+ The second is **latency per layer**: scheduler dispatch, the cross-device hop,
83
+ `ggml_backend_synchronize`. That is a property of *the rig and the graph*, not of how many bytes
84
+ the tensors happen to hold. So it should survive being scaled down — and it does:
85
+
86
+ > **Fence per layer: 253 µs on the 144.3 GiB model, 252 µs on the 244 MiB homunculus.**
87
+ > Same rig, same build. 0.4% apart.
88
+
89
+ That agreement holds across regimes that could hardly be more different: the per-layer residual
90
+ is 17.6% of the real model's token time and 92.8% of the toy's.
91
+
92
+ ## Where it fails
93
+
94
+ Both failures are on the bandwidth side, which is the useful kind — you can tell in advance
95
+ which questions not to ask it.
96
+
97
+ **1. Overheads that are themselves proportional to bytes.** Expert parallelism is the worked
98
+ example and the only prediction outside 5%. Its overhead is a cross-device *exchange of expert
99
+ tensors*, so it scales with bytes moved; the toy's experts are 606× smaller, its exchange is
100
+ nearly free, and it under-charges the fence (601 vs 810 µs/layer). It still gets the sign right
101
+ — real EP/serial 0.721, toy 0.438, both say EP loses — so it would have correctly rejected
102
+ expert parallelism without a single 145 GiB load. Trust the ranking, not the magnitude.
103
+
104
+ **2. Anything measured in bytes per token.** Load mode moves real prefill by 8.0% and the toy by
105
+ 2.4% with no consistent ordering. At 244 MiB the weights are resident under every mode. Do not
106
+ use this to size a quantization win or a load-mode change.
107
+
108
+ ## The models
109
+
110
+ | | h0-skeleton | h1-topology | DeepSeek-V4-Flash-0731 |
111
+ |---|---|---|---|
112
+ | purpose | does it load at all | the qualified instrument | the real thing |
113
+ | size | 177 MiB (f16) | 735 MiB f16 / **244 MiB MXFP4** | 144.3 GiB |
114
+ | tensors | 150 | 364 | 1328 |
115
+ | layers | 5 | 12 | 43 |
116
+ | embedding dim | 256 | 256 | 4096 |
117
+ | attention heads | 32 | 32 | 64 |
118
+ | **KV heads (MLA)** | **1** | **1** | **1** |
119
+ | **experts** | 32 | **256** | **256** |
120
+ | **experts used** | **6** | **6** | **6** |
121
+ | **shared experts** | **1** | **1** | **1** |
122
+ | expert FFN | 128 | 128 | 2048 |
123
+ | **hyper-connections** | **4** | **4** | **4** |
124
+ | **hash layers** | **3** | **3** | **3** |
125
+ | indexer heads | 4 | 4 | 64 |
126
+
127
+ Bold rows are **preserved exactly** — they determine which operators run and how the graph
128
+ branches. The unbolded rows are width, and width is what gets scaled away.
129
+
130
+ **Use `h1-topology-mxfp4.gguf`** for measurement: it is the file every number above was produced
131
+ with, and MXFP4 experts match how the real model is quantized. `h0-skeleton` exists to answer
132
+ "does a generated `deepseek4` GGUF load and generate at all" and is not qualified for anything
133
+ else.
134
+
135
+ ## Reproducing
136
+
137
+ Requires a llama.cpp build with `deepseek4` support.
138
+
139
+ ```bash
140
+ # one configuration, ~3 seconds
141
+ python qualify.py run \
142
+ --model h1-topology-mxfp4.gguf \
143
+ --profile m6 --bench /path/to/llama-bench \
144
+ --rpc <peer>:50052 --straddle \
145
+ --roofline-gbs <your measured GB/s> \
146
+ --only rpc_split_even --out toy.json
147
+ ```
148
+
149
+ `qualify.py` derives `f` per configuration and prints it. Feed that into the formula above with
150
+ your real model's `read_budget_gb` (which `qualify.py` also computes, from the tensor table
151
+ alone) to get a prediction.
152
+
153
+ `gen_homunculus.py` builds these files. It has presets `h0`, `h1`, `h2`, and the parameters are
154
+ plain constants — pointing it at a different architecture is the intended way to make a
155
+ homunculus for something other than DSV4.
156
+
157
+ ```bash
158
+ PYTHONPATH=/path/to/llama.cpp/gguf-py python gen_homunculus.py --preset h1 --out h1.gguf
159
+ ```
160
+
161
+ ### Two caveats for anyone reproducing
162
+
163
+ - **`f` is normalised per layer, and h1 is 12 layers, not 43.** The predictor multiplies `f_toy`
164
+ by the *real* model's layer count. The toy does not preserve depth and does not need to.
165
+ - **Expert-parallel configurations must run against a build that actually has the TP code.**
166
+ With a build that doesn't, the environment variables are silently inert and you measure a
167
+ plain layer split while believing you measured EP.
168
+
169
+ ## Files
170
+
171
+ | file | what |
172
+ |---|---|
173
+ | `h1-topology-mxfp4.gguf` | the qualified instrument — use this one |
174
+ | `h1-topology-f16.gguf` | same topology, unquantized |
175
+ | `h0-skeleton-f16.gguf` | minimal load-and-generate test article |
176
+ | `gen_homunculus.py` | builds homunculi from an architecture spec |
177
+ | `qualify.py` | the measurement harness; runs the config sweep and derives `f` |
178
+ | `assets/dsv4_tokenizer.json.gz` | the DSV4 tokenizer — `gen_homunculus.py` requires it |
179
+ | `results/*.json` | raw `llama-bench` output behind every number on this page |
180
+ | `homunculus-poster.png` | the qualification summary above |
181
+
182
+ ## Measurement conditions
183
+
184
+ Two Strix Halo boxes (128 GB unified each), 100GbE, llama.cpp with two-node RPC. Roofline
185
+ 209.25 GB/s measured per node. Both nodes pinned to `dpm=high`. `llama-bench` pp512/tg128,
186
+ 3 repetitions. The real model is `DeepSeek-V4-Flash-0731`, MXFP4 routed experts with Q8_0 dense
187
+ tensors, 144.3 GiB.
188
+
189
+ Numbers here are specific to that rig. The *method* is not — the claim is about which terms
190
+ survive scaling, and that argument is hardware-independent even though the constants are not.
191
+
192
+ ## License and provenance
193
+
194
+ `gen_homunculus.py`, `qualify.py`, and the results are MIT, matching llama.cpp's `gguf-py`
195
+ which the generator builds on. **All tensor data in the GGUFs is randomly generated** and carries
196
+ nothing from DeepSeek-V4-Flash.
197
+
198
+ Two things *are* taken from the real model, and neither is a weight:
199
+
200
+ - **The tokenizer, verbatim** — 129,280 tokens, embedded in every GGUF here and shipped as
201
+ `assets/dsv4_tokenizer.json.gz` because the generator cannot run without it. It is
202
+ DeepSeek's, redistributed from DeepSeek-V4-Flash, and it is why `h0-skeleton` is 177 MiB
203
+ despite having only 5 layers: `token_embd` dominates a model this small.
204
+ - **The config values** — layer counts, expert counts, and the hyper-connection / indexer
205
+ parameters, i.e. the numbers in the comparison table above.
206
+
207
+ The tokenizer is present so that a homunculus tokenizes identically to the real model, which
208
+ keeps prompt lengths and therefore batch shapes honest. Nothing about the measurement needs
209
+ the tokenizer to be *this* tokenizer — any 129k-vocab tokenizer would give the same timings —
210
+ so if its redistribution is inconvenient for you, substitute your own and regenerate.
assets/dsv4_tokenizer.json.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:22a17c579c95db443cae029790254a47af4033c98711d1a39fa84d2d45d11e15
3
+ size 1729594
gen_homunculus.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ gen_homunculus.py -- emit a scale-model DeepSeek-V4 ("homunculus") GGUF.
4
+
5
+ A homunculus is a toy DSV4: small enough to iterate on in minutes, but structurally
6
+ complete -- every pathway the real model has (hyper-connections, MLA + sparse indexer,
7
+ the 0/4/128 compressor pattern, hash-routed layers, 256-way MoE, the nextn/MTP block)
8
+ is present so that parallelization findings transfer.
9
+
10
+ Ground truth for the real model was read from the GGUF headers on m6 (2026-08-03):
11
+ main model m6:/mnt/weights/models/dsv4-flash/dsv4-0731-MXFP4.gguf block_count 43, no nextn
12
+ MTP draft m6:/mnt/weights/models/dsv4-mtp/...-MXFP4_MOE.gguf block_count 44, nextn 1
13
+ See REAL below, and the akb parent card 2adc38bb65.
14
+
15
+ Usage:
16
+ python3 gen_homunculus.py --preset h0 --out /path/to/homunculus-h0-f16.gguf
17
+ ../llama.cpp/build/bin/llama-quantize <f16.gguf> <mxfp4.gguf> MXFP4_MOE
18
+ """
19
+
20
+ import argparse, gzip, json, os, sys
21
+
22
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "llama.cpp", "gguf-py"))
23
+ import numpy as np
24
+ import gguf
25
+
26
+ ASSETS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets")
27
+
28
+ # --------------------------------------------------------------------------------------
29
+ # Measured real-DSV4 configuration. Do not edit without re-reading the GGUF header.
30
+ # --------------------------------------------------------------------------------------
31
+ REAL = dict(
32
+ n_layer=43, n_layer_nextn=0, # main file: 43 blocks, MTP ships separately
33
+ n_embd=4096, n_head=64, n_head_kv=1,
34
+ n_embd_head=512, # attention.key_length == value_length (MLA latent)
35
+ rope_dim=64, rope_freq_base=10000.0,
36
+ q_lora_rank=1024, o_group_count=8, o_lora_rank=1024,
37
+ n_expert=256, n_expert_used=6, n_expert_shared=1, n_ff_exp=2048,
38
+ expert_weights_scale=1.5, expert_weights_norm=True,
39
+ hc_mult=4, sinkhorn_iters=20, hc_eps=1e-6,
40
+ indexer_n_head=64, indexer_head_size=128, indexer_top_k=512,
41
+ hash_layer_count=3, sliding_window=128,
42
+ compress_rope_freq_base=160000.0, swiglu_clamp=10.0,
43
+ rms_eps=1e-6, n_ctx=1048576, n_vocab=129280,
44
+ )
45
+
46
+ # compress_ratios for the real 43-block trunk: two dense layers, then 4/128 alternating.
47
+ # 4 -> compressor (coff=2) PLUS the full sparse-indexer stack
48
+ # 128 -> compressor only (coff=1)
49
+ # 0 -> no compressor at all
50
+ REAL_RATIOS_43 = [0, 0] + [4 if i % 2 == 0 else 128 for i in range(2, 43)]
51
+ assert len(REAL_RATIOS_43) == 43 and REAL_RATIOS_43[42] == 4
52
+
53
+
54
+ def ratios_for(n_trunk, n_nextn):
55
+ """Real ratio pattern truncated/extended to n_trunk, with ratio 0 for nextn blocks."""
56
+ if n_trunk <= len(REAL_RATIOS_43):
57
+ r = REAL_RATIOS_43[:n_trunk]
58
+ else:
59
+ r = REAL_RATIOS_43 + [4 if i % 2 == 0 else 128 for i in range(43, n_trunk)]
60
+ # keep at least one of each variant so no pathway is lost to truncation
61
+ if n_trunk >= 4:
62
+ assert 0 in r and 4 in r and 128 in r, f"truncation lost a compressor variant: {r}"
63
+ return r + [0] * n_nextn
64
+
65
+
66
+ # --------------------------------------------------------------------------------------
67
+ # Presets. Every preset preserves the DIMENSIONLESS invariants exactly:
68
+ # hc_mult, n_expert/n_expert_used, n_expert_shared, hash_layer_count, the ratio pattern,
69
+ # n_ff_exp/n_embd, n_head*n_embd_head/n_embd, sinkhorn_iters, gating func.
70
+ # Only absolute widths and depth scale down.
71
+ # --------------------------------------------------------------------------------------
72
+ def scaled(n_embd, n_layer, n_expert, *, n_nextn=0, n_vocab=None, name=""):
73
+ """Build a config by scaling widths off n_embd while holding real ratios."""
74
+ s = REAL["n_embd"] // n_embd # width divisor
75
+ assert REAL["n_embd"] % n_embd == 0, "n_embd must divide 4096 to keep ratios exact"
76
+ # HARD CONSTRAINT, measured: llama_kv_cache::build_input_k_rot derives the Hadamard
77
+ # rotation size with `nrot=64; do nrot*=2 while (n_embd_head_k_all % nrot == 0); nrot/=2`.
78
+ # That only resolves back to the head dim when the head dim is a multiple of 64. Anything
79
+ # smaller yields a 64x64 rotation against an N-dim head and aborts in ggml_reshape_2d.
80
+ n_embd_head = max(64, (REAL["n_embd_head"] // s) // 64 * 64)
81
+ # hold n_head*n_embd_head / n_embd == 8 (the real MLA expansion)
82
+ n_head = (8 * n_embd) // n_embd_head
83
+ rope_dim = max(4, (REAL["rope_dim"] * n_embd_head) // REAL["n_embd_head"])
84
+ rope_dim -= rope_dim % 2
85
+ cfg = dict(REAL)
86
+ cfg.update(
87
+ name=name,
88
+ n_embd=n_embd, n_layer=n_layer, n_layer_nextn=n_nextn,
89
+ n_head=n_head, n_embd_head=n_embd_head, rope_dim=rope_dim,
90
+ q_lora_rank=max(16, REAL["q_lora_rank"] // s),
91
+ o_lora_rank=max(16, REAL["o_lora_rank"] // s),
92
+ n_ff_exp=max(16, REAL["n_ff_exp"] // s),
93
+ n_expert=n_expert,
94
+ # indexer_head_size DOES NOT SCALE. For DSV4 the lightning-indexer KV cache always has
95
+ # attn_rot_k forced on, so it is subject to the same power-of-two Hadamard resolution as
96
+ # above; the real 128 is kept verbatim. 64 also works; below that it aborts.
97
+ indexer_head_size=REAL["indexer_head_size"],
98
+ indexer_n_head=max(4, REAL["indexer_n_head"] // s),
99
+ indexer_top_k=64,
100
+ n_ctx=8192,
101
+ n_vocab=n_vocab or REAL["n_vocab"],
102
+ )
103
+ # o_group_count must divide n_head*n_embd_head
104
+ while (n_head * n_embd_head) % cfg["o_group_count"] != 0:
105
+ cfg["o_group_count"] //= 2
106
+ return cfg
107
+
108
+
109
+ PRESETS = {
110
+ # H0 -- skeleton. Does it wake up? Minimum dims, every pathway present.
111
+ "h0": lambda: scaled(n_embd=256, n_layer=5, n_expert=32, name="h0-skeleton"),
112
+ # H1 -- topology-true. Real expert count and ratios, ~1-2 GiB.
113
+ "h1": lambda: scaled(n_embd=256, n_layer=12, n_expert=256, name="h1-topology"),
114
+ # H2 -- latency-true. The REAL trunk depth (43), no nextn: the deployed main GGUF
115
+ # has block_count 43 and ships MTP as a separate draft file. Narrow.
116
+ "h2": lambda: scaled(n_embd=256, n_layer=43, n_expert=256, name="h2-latency"),
117
+ }
118
+
119
+
120
+ # --------------------------------------------------------------------------------------
121
+ def load_tokenizer():
122
+ path = os.path.join(ASSETS, "dsv4_tokenizer.json.gz")
123
+ with gzip.open(path, "rt", encoding="utf-8") as f:
124
+ return json.load(f)
125
+
126
+
127
+ def write_tokenizer(w, tok, n_vocab):
128
+ """Replay the real DSV4 tokenizer KVs. n_vocab must match the real vocab (129280);
129
+ truncating would desync merges from tokens, so we do not support it."""
130
+ real_n = len(tok["tokenizer.ggml.tokens"]["v"]["v"])
131
+ if n_vocab != real_n:
132
+ raise SystemExit(f"n_vocab {n_vocab} != real tokenizer size {real_n}; truncation not supported")
133
+ for k, ent in tok.items():
134
+ t, v = ent["t"], ent["v"]
135
+ if t == 9: # array
136
+ w.add_array(k, v["v"])
137
+ elif t == 8:
138
+ w.add_string(k, v)
139
+ elif t == 7:
140
+ w.add_bool(k, v)
141
+ elif t == 4:
142
+ w.add_uint32(k, v)
143
+ elif t == 6:
144
+ w.add_float32(k, v)
145
+ else:
146
+ w.add_key_value(k, v, gguf.GGUFValueType(t))
147
+
148
+
149
+ def add_kv(w, c, ratios):
150
+ n_all = c["n_layer"] + c["n_layer_nextn"]
151
+ w.add_context_length(c["n_ctx"])
152
+ w.add_embedding_length(c["n_embd"])
153
+ w.add_block_count(n_all)
154
+ w.add_head_count(c["n_head"])
155
+ w.add_head_count_kv(c["n_head_kv"])
156
+ w.add_key_length(c["n_embd_head"])
157
+ w.add_value_length(c["n_embd_head"])
158
+ w.add_rope_dimension_count(c["rope_dim"])
159
+ w.add_rope_freq_base(c["rope_freq_base"])
160
+ w.add_layer_norm_rms_eps(c["rms_eps"])
161
+ w.add_q_lora_rank(c["q_lora_rank"])
162
+ w.add_sliding_window(c["sliding_window"])
163
+ w.add_expert_count(c["n_expert"])
164
+ w.add_expert_used_count(c["n_expert_used"])
165
+ w.add_expert_shared_count(c["n_expert_shared"])
166
+ w.add_expert_feed_forward_length(c["n_ff_exp"])
167
+ w.add_expert_weights_scale(c["expert_weights_scale"])
168
+ w.add_expert_weights_norm(c["expert_weights_norm"])
169
+ w.add_expert_gating_func(gguf.ExpertGatingFuncType.SQRTSOFTPLUS)
170
+ w.add_swiglu_clamp_exp([c["swiglu_clamp"]] * n_all)
171
+ w.add_swiglu_clamp_shexp([c["swiglu_clamp"]] * n_all)
172
+ w.add_indexer_head_count(c["indexer_n_head"])
173
+ w.add_indexer_key_length(c["indexer_head_size"])
174
+ w.add_indexer_top_k(c["indexer_top_k"])
175
+ w.add_attention_output_group_count(c["o_group_count"])
176
+ w.add_attention_output_lora_rank(c["o_lora_rank"])
177
+ w.add_attention_compress_ratios(ratios)
178
+ w.add_attention_compress_rope_freq_base(c["compress_rope_freq_base"])
179
+ w.add_hyper_connection_count(c["hc_mult"])
180
+ w.add_hyper_connection_sinkhorn_iterations(c["sinkhorn_iters"])
181
+ w.add_hyper_connection_epsilon(c["hc_eps"])
182
+ w.add_hash_layer_count(c["hash_layer_count"])
183
+ if c["n_layer_nextn"]:
184
+ w.add_nextn_predict_layers(c["n_layer_nextn"])
185
+
186
+
187
+ def build(cfg, out_path, seed=0, ratios=None):
188
+ rng = np.random.default_rng(seed)
189
+ c = cfg
190
+ n_all = c["n_layer"] + c["n_layer_nextn"]
191
+ ratios = ratios if ratios is not None else ratios_for(c["n_layer"], c["n_layer_nextn"])
192
+ hc_dim = c["hc_mult"] * c["n_embd"]
193
+ hc_mix = (2 + c["hc_mult"]) * c["hc_mult"]
194
+ n_qk = c["n_head"] * c["n_embd_head"]
195
+
196
+ w = gguf.GGUFWriter(out_path, "deepseek4")
197
+ w.add_name(f"homunculus-{c['name']}")
198
+ w.add_type(gguf.GGUFType.MODEL)
199
+ add_kv(w, c, ratios)
200
+ write_tokenizer(w, load_tokenizer(), c["n_vocab"])
201
+
202
+ def rnd(*ne, dtype=np.float16, std=0.02):
203
+ """ne is in ggml order (ne0, ne1, ...); numpy wants it reversed."""
204
+ return (rng.standard_normal(tuple(reversed(ne))) * std).astype(dtype)
205
+
206
+ def ones(*ne):
207
+ return np.ones(tuple(reversed(ne)), dtype=np.float32)
208
+
209
+ w.add_tensor("token_embd.weight", rnd(c["n_embd"], c["n_vocab"]))
210
+ w.add_tensor("output_norm.weight", ones(c["n_embd"]))
211
+ w.add_tensor("output.weight", rnd(c["n_embd"], c["n_vocab"]))
212
+ w.add_tensor("output_hc_fn.weight", rnd(hc_dim, c["hc_mult"], dtype=np.float32))
213
+ w.add_tensor("output_hc_base.weight", ones(c["hc_mult"]))
214
+ w.add_tensor("output_hc_scale.weight", ones(1))
215
+
216
+ for i in range(n_all):
217
+ p = f"blk.{i}."
218
+ w.add_tensor(p + "attn_norm.weight", ones(c["n_embd"]))
219
+ w.add_tensor(p + "attn_sinks.weight", np.zeros(c["n_head"], dtype=np.float32))
220
+ w.add_tensor(p + "attn_q_a.weight", rnd(c["n_embd"], c["q_lora_rank"]))
221
+ w.add_tensor(p + "attn_q_a_norm.weight", ones(c["q_lora_rank"]))
222
+ w.add_tensor(p + "attn_q_b.weight", rnd(c["q_lora_rank"], n_qk))
223
+ w.add_tensor(p + "attn_kv.weight", rnd(c["n_embd"], c["n_embd_head"]))
224
+ w.add_tensor(p + "attn_kv_a_norm.weight", ones(c["n_embd_head"]))
225
+ w.add_tensor(p + "attn_output_a.weight",
226
+ rnd(n_qk // c["o_group_count"], c["o_lora_rank"] * c["o_group_count"]))
227
+ w.add_tensor(p + "attn_output_b.weight",
228
+ rnd(c["o_group_count"] * c["o_lora_rank"], c["n_embd"]))
229
+
230
+ for tag in ("attn", "ffn"):
231
+ w.add_tensor(p + f"hc_{tag}_fn.weight", rnd(hc_dim, hc_mix, dtype=np.float32))
232
+ w.add_tensor(p + f"hc_{tag}_base.weight", np.zeros(hc_mix, dtype=np.float32))
233
+ w.add_tensor(p + f"hc_{tag}_scale.weight", ones(3))
234
+
235
+ ratio = ratios[i]
236
+ if ratio != 0:
237
+ coff = 2 if ratio == 4 else 1
238
+ cd = coff * c["n_embd_head"]
239
+ w.add_tensor(p + "attn_compressor_kv.weight", rnd(c["n_embd"], cd))
240
+ w.add_tensor(p + "attn_compressor_gate.weight", rnd(c["n_embd"], cd))
241
+ w.add_tensor(p + "attn_compressor_ape.weight", rnd(cd, ratio, dtype=np.float32))
242
+ w.add_tensor(p + "attn_compressor_norm.weight", ones(c["n_embd_head"]))
243
+ if ratio == 4:
244
+ ihs = c["indexer_head_size"]
245
+ w.add_tensor(p + "indexer.proj.weight", rnd(c["n_embd"], c["indexer_n_head"]))
246
+ w.add_tensor(p + "indexer.attn_q_b.weight",
247
+ rnd(c["q_lora_rank"], c["indexer_n_head"] * ihs))
248
+ w.add_tensor(p + "indexer_compressor_kv.weight", rnd(c["n_embd"], 2 * ihs))
249
+ w.add_tensor(p + "indexer_compressor_gate.weight", rnd(c["n_embd"], 2 * ihs))
250
+ w.add_tensor(p + "indexer_compressor_ape.weight", rnd(2 * ihs, ratio, dtype=np.float32))
251
+ w.add_tensor(p + "indexer_compressor_norm.weight", ones(ihs))
252
+
253
+ w.add_tensor(p + "ffn_gate_inp.weight", rnd(c["n_embd"], c["n_expert"]))
254
+ if i < c["hash_layer_count"]:
255
+ # deterministic token-id -> expert-id table, I32, never quantized
256
+ tid2eid = rng.integers(0, c["n_expert"],
257
+ size=(c["n_vocab"], c["n_expert_used"]), dtype=np.int32)
258
+ w.add_tensor(p + "ffn_gate_tid2eid.weight", tid2eid)
259
+ else:
260
+ w.add_tensor(p + "exp_probs_b.bias", np.zeros(c["n_expert"], dtype=np.float32))
261
+ w.add_tensor(p + "ffn_norm.weight", ones(c["n_embd"]))
262
+
263
+ ne, nf, nx = c["n_embd"], c["n_ff_exp"], c["n_expert"]
264
+ w.add_tensor(p + "ffn_gate_exps.weight", rnd(ne, nf, nx))
265
+ w.add_tensor(p + "ffn_down_exps.weight", rnd(nf, ne, nx))
266
+ w.add_tensor(p + "ffn_up_exps.weight", rnd(ne, nf, nx))
267
+
268
+ nsh = nf * c["n_expert_shared"]
269
+ w.add_tensor(p + "ffn_gate_shexp.weight", rnd(ne, nsh))
270
+ w.add_tensor(p + "ffn_down_shexp.weight", rnd(nsh, ne))
271
+ w.add_tensor(p + "ffn_up_shexp.weight", rnd(ne, nsh))
272
+
273
+ if i >= c["n_layer"]:
274
+ w.add_tensor(p + "nextn.eh_proj.weight", rnd(2 * c["n_embd"], c["n_embd"]))
275
+ w.add_tensor(p + "nextn.enorm.weight", ones(c["n_embd"]))
276
+ w.add_tensor(p + "nextn.hnorm.weight", ones(c["n_embd"]))
277
+
278
+ w.write_header_to_file()
279
+ w.write_kv_data_to_file()
280
+ w.write_tensors_to_file()
281
+ w.close()
282
+
283
+
284
+ def main():
285
+ ap = argparse.ArgumentParser()
286
+ ap.add_argument("--preset", required=True, choices=sorted(PRESETS))
287
+ ap.add_argument("--out", required=True)
288
+ ap.add_argument("--seed", type=int, default=0)
289
+ # bisect / override knobs -- for isolating which structural pathway breaks a build
290
+ ap.add_argument("--ratios", help="'dense' (all 0), 'hca' (all 128), 'csa' (all 4), or a comma list")
291
+ ap.add_argument("--rope-dim", type=int)
292
+ ap.add_argument("--head-dim", type=int)
293
+ ap.add_argument("--n-layer", type=int)
294
+ ap.add_argument("--nextn", type=int, default=0,
295
+ help="append N nextn/MTP blocks (default 0 -- spec decode is out of scope; "
296
+ "the real main GGUF has none)")
297
+ a = ap.parse_args()
298
+
299
+ cfg = PRESETS[a.preset]()
300
+ if a.n_layer:
301
+ cfg["n_layer"] = a.n_layer
302
+ cfg["n_layer_nextn"] = a.nextn
303
+ if a.head_dim:
304
+ cfg["n_embd_head"] = a.head_dim
305
+ cfg["n_head"] = (8 * cfg["n_embd"]) // a.head_dim
306
+ while (cfg["n_head"] * a.head_dim) % cfg["o_group_count"] != 0:
307
+ cfg["o_group_count"] //= 2
308
+ if a.rope_dim:
309
+ cfg["rope_dim"] = a.rope_dim
310
+
311
+ n_all = cfg["n_layer"] + cfg["n_layer_nextn"]
312
+ if a.ratios:
313
+ fixed = {"dense": 0, "hca": 128, "csa": 4}.get(a.ratios)
314
+ if fixed is not None:
315
+ ratios = [fixed] * cfg["n_layer"] + [0] * cfg["n_layer_nextn"]
316
+ else:
317
+ ratios = [int(x) for x in a.ratios.split(",")]
318
+ assert len(ratios) == n_all, f"--ratios needs {n_all} entries"
319
+ else:
320
+ ratios = ratios_for(cfg["n_layer"], cfg["n_layer_nextn"])
321
+ print(f"preset {a.preset}: n_embd={cfg['n_embd']} n_head={cfg['n_head']} "
322
+ f"head_dim={cfg['n_embd_head']} rope_dim={cfg['rope_dim']} "
323
+ f"n_layer={cfg['n_layer']}+{cfg['n_layer_nextn']}nextn "
324
+ f"experts={cfg['n_expert']}/{cfg['n_expert_used']} n_ff_exp={cfg['n_ff_exp']} "
325
+ f"hc_mult={cfg['hc_mult']} o_groups={cfg['o_group_count']}")
326
+ print(f" ratios: {ratios}")
327
+ build(cfg, a.out, a.seed, ratios)
328
+ print(f" wrote {a.out} ({os.path.getsize(a.out)/2**20:.1f} MiB)")
329
+
330
+
331
+ if __name__ == "__main__":
332
+ main()
h0-skeleton-f16.gguf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:56973c8573d810c69c77535953e94d92f438a8ba3f7255f99ab0a3c66cb3b9ac
3
+ size 185779328
h1-topology-f16.gguf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c20c2ef57dcb4e2ab6b5fdb32ea27b4d3747897db17df432767b835660d4faae
3
+ size 770349760
h1-topology-mxfp4.gguf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4c00f22850c7324d0395400b7dd7a88b0e587ed76c35f8ac0094c2aae57b8a24
3
+ size 255728448
homunculus-poster.png ADDED

Git LFS Details

  • SHA256: b772ba80fcb83e1a0ea824780613341846ab31cc401d7cd088254678b2cdd65c
  • Pointer size: 131 Bytes
  • Size of remote file: 450 kB
qualify.py ADDED
@@ -0,0 +1,633 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ qualify.py -- the homunculus qualification harness.
4
+
5
+ Runs a fixed set of parallelization configurations against ONE model via llama-bench, and
6
+ records throughput per configuration. Run it on a homunculus, run it on real DSV4, then
7
+ `compare` the two result files.
8
+
9
+ THE CRITERION IS A RELATIONSHIP, NOT AN ABSOLUTE. A toy will never reproduce DSV4's t/s and
10
+ trying to fit that would just be chasing the bandwidth term, which is analytic anyway. What
11
+ qualifies a homunculus is that its per-configuration CHANGES track the real model's changes:
12
+ same ordering, and ideally a stable scale factor between the two sets of speedups, so a toy
13
+ delta PREDICTS a real delta.
14
+
15
+ Everything here is stdlib-only (no numpy) so it runs unchanged on m5/m6 system python.
16
+
17
+ # measure
18
+ python3 qualify.py run --model h1-mxfp4.gguf --profile bigboy --out toy-h1.json
19
+ python3 qualify.py run --model dsv4-0731-MXFP4.gguf --profile m6 --out real.json
20
+
21
+ # qualify
22
+ python3 qualify.py compare --real real.json --toy toy-h1.json [--toy toy-h2.json ...]
23
+ """
24
+
25
+ import argparse, json, math, os, re, shutil, statistics, subprocess, sys, time
26
+
27
+ HERE = os.path.dirname(os.path.abspath(__file__))
28
+ DEFAULT_BENCH = os.path.join(HERE, "..", "llama.cpp", "build", "bin", "llama-bench")
29
+
30
+ # --------------------------------------------------------------------------------------
31
+ # Configurations. The NAMES are the contract -- a config name means the same thing on the
32
+ # toy and on the real model, which is what makes the deltas comparable.
33
+ #
34
+ # `needs` gates a config on host capability:
35
+ # gpu -- at least one offload device
36
+ # dev2 -- at least two devices to split across
37
+ # rpc -- an --rpc endpoint was supplied
38
+ # --------------------------------------------------------------------------------------
39
+ CONFIGS = {
40
+ # --- baselines -------------------------------------------------------------------
41
+ "cpu_only": dict(args=["-ngl", "0"], needs=[]),
42
+ "gpu_all": dict(args=["-ngl", "999"], needs=["gpu"]),
43
+
44
+ # --- expert placement: the axis that produced the real 2.58 -> 4.0 t/s jump -------
45
+ "experts_cpu_ot": dict(args=["-ngl", "999", "-ot", "exps=CPU"], needs=["gpu"]),
46
+ "experts_cpu_all": dict(args=["-ngl", "999", "-ncmoe", "999"], needs=["gpu"]),
47
+ "experts_cpu_half":dict(args=["-ngl", "999", "-ncmoe", "{half_layers}"], needs=["gpu"]),
48
+
49
+ # --- single-node multi-device split ----------------------------------------------
50
+ "dev_split_even": dict(args=["-ngl", "999", "-dev", "{dev0},{dev1}", "-ts", "1/1"],
51
+ needs=["dev2"]),
52
+ "dev_split_skew": dict(args=["-ngl", "999", "-dev", "{dev0},{dev1}", "-ts", "3/1"],
53
+ needs=["dev2"]),
54
+
55
+ # --- load path: the mmap-vs-resident axis that dominates single-node m5 -----------
56
+ # Full --load-mode sweep. -lm none is what the meld serve actually uses (it passes the
57
+ # deprecated --no-mmap). mmap+mlock beat every other config on BOTH metrics in the first
58
+ # real run (+1.5 pct tg, +7.6 pct pp vs default), so the serve's choice is worth re-testing.
59
+ # -lm dio is recorded as a dead end in the DSV4 memory; kept so the toy can be checked
60
+ # against a KNOWN-BAD config, which is as useful as checking it against a known-good one.
61
+ "load_none": dict(args=["-ngl", "999", "-lm", "none"], needs=["gpu"]),
62
+ "load_mmap": dict(args=["-ngl", "999", "-lm", "mmap"], needs=["gpu"]),
63
+ "load_mlock_only": dict(args=["-ngl", "999", "-lm", "mlock"], needs=["gpu"]),
64
+ "load_mlock": dict(args=["-ngl", "999", "-lm", "mmap+mlock"], needs=["gpu"]),
65
+ "load_dio": dict(args=["-ngl", "999", "-lm", "dio"], needs=["gpu"]),
66
+
67
+ # --- batch shape: the -ub 256 -b 256 axis from the Nemotron two-node run ----------
68
+ "batch_small": dict(args=["-ngl", "999", "-b", "256", "-ub", "256"], needs=["gpu"]),
69
+ "batch_large": dict(args=["-ngl", "999", "-b", "2048", "-ub", "512"], needs=["gpu"]),
70
+
71
+ # --- two-node RPC layer split -----------------------------------------------------
72
+ # This is the CURRENT PRODUCTION SHAPE and the bar every other strategy has to clear.
73
+ # Measured 2026-08-03 on real DSV4: 15.14 t/s serial layer-split; EP-only lost at 13.85.
74
+ "rpc_split_even": dict(args=["-ngl", "999", "--rpc", "{rpc}", "-ts", "1/1"], needs=["rpc"]),
75
+ "rpc_split_local": dict(args=["-ngl", "999", "--rpc", "{rpc}", "-ts", "3/1"], needs=["rpc"]),
76
+ "rpc_split_remote":dict(args=["-ngl", "999", "--rpc", "{rpc}", "-ts", "1/3"], needs=["rpc"]),
77
+
78
+ # --- tensor parallel --------------------------------------------------------------
79
+ # -ts 0,1 NOT 1,0: model.devices puts RPC0 FIRST even though --list-devices prints
80
+ # Vulkan0 first. That gotcha cost session 0cc8d352 twenty minutes; do not re-derive it.
81
+ # TP wins were pure graph-node ORDERING, which is exactly the fence term a toy measures:
82
+ # Cydonia-24B serial-order TP 10.83 -> join-on-peer 16.49 -> +uid graph cache 17.21 t/s
83
+ # --- expert parallelism: the axis with a KNOWN real answer -------------------------
84
+ # Real DSV4 measured serial 15.14 vs EP 13.85 t/s, i.e. EP = 0.915x. EP LOSES. If the toy
85
+ # reproduces that ordering the instrument is qualified for the fence/topology axis; if the
86
+ # toy says EP wins, it is not. Both are memory-safe on the real model (EP measured m5 68.4 /
87
+ # m6 77.6 GiB) so this is the comparison the sweep was missing.
88
+ # REQUIRES build-ep: build-ds is branch ds-merged and contains NO TP code, so LLAMA_TP_EP
89
+ # would be an inert env var and this would silently measure plain -ts 0/1 instead.
90
+ # guard_exempt: -ts 0/1 nominally puts the whole model on one device, but EP then moves half
91
+ # the routed experts to the peer, so the naive -ts share guard would wrongly reject it.
92
+ "ep_serial": dict(args=["-ngl", "999", "--rpc", "{rpc}", "-sm", "layer", "-ts", "1/1"],
93
+ needs=["rpc"]),
94
+ "ep_split": dict(args=["-ngl", "999", "--rpc", "{rpc}", "-sm", "layer", "-ts", "0/1"],
95
+ env={"LLAMA_TP": "1", "LLAMA_TP_EP": "1",
96
+ "LLAMA_TP_JOIN": "cpu", "GGML_RPC_PUSH": "1"},
97
+ guard_exempt=True, needs=["rpc"]),
98
+
99
+ "tp_join_peer": dict(args=["-ngl", "999", "--rpc", "{rpc}", "-ts", "0/1"],
100
+ env={"LLAMA_TP": "1"}, needs=["rpc", "tp"]),
101
+ # LLAMA_TP_JOIN=cpu reruns the measured-NEGATIVE CPU-join experiment (16.98 vs 17.21).
102
+ # Kept because reproducing a known loss is how we validate the instrument.
103
+ "tp_join_cpu": dict(args=["-ngl", "999", "--rpc", "{rpc}", "-ts", "0/1"],
104
+ env={"LLAMA_TP": "1", "LLAMA_TP_JOIN": "cpu"}, needs=["rpc", "tp"]),
105
+ }
106
+
107
+ # Host profiles. `devs` are llama-bench -dev names; discover them with --list-devices.
108
+ # caps -- which config classes this host can run
109
+ # env -- applied to every config on this host
110
+ #
111
+ # bigboy_cpu exists because of a MEASURED bigboy limitation (2026-08-03): any DSV4-arch model
112
+ # aborts the moment the CUDA backend is merely PRESENT. The lightning indexer's Hadamard op
113
+ # (ggml_cuda_op_fwht) fails inside ggml_cuda_kernel_can_use_pdl on compute capability 8.6 --
114
+ # PDL is a Hopper-era feature. -ngl 0 is NOT enough, the op still gets scheduled to CUDA; the
115
+ # device has to be hidden outright. So bigboy generates homunculi, it does not measure them.
116
+ PROFILES = {
117
+ "bigboy": dict(caps=["gpu"], devs=["CUDA0"], env={}),
118
+ "bigboy_cpu": dict(caps=[], devs=[], env={"CUDA_VISIBLE_DEVICES": ""}),
119
+ "m5": dict(caps=["gpu", "dev2", "tp"], devs=["Vulkan0", "Vulkan1"], env={}),
120
+ "m6": dict(caps=["gpu", "dev2", "tp"], devs=["Vulkan0", "Vulkan1"], env={}),
121
+ "cpu": dict(caps=[], devs=[], env={}),
122
+ }
123
+
124
+
125
+ # --------------------------------------------------------------------------------------
126
+ # GGUF introspection + the analytic bandwidth term.
127
+ #
128
+ # The q8dense result (2026-08-03) proved the bandwidth term is predictable from the tensor
129
+ # table to within 0.1 percent: read budget 11.594 -> 10.71 GB/token predicted +8.2 percent,
130
+ # measured +8.1 percent. So we COMPUTE it here rather than measuring it, and spend measurement
131
+ # only on the fence term, which is the part a toy can actually reproduce.
132
+ #
133
+ # Pure stdlib -- this has to run on m5/m6 system python, which has no numpy (so no gguf-py).
134
+ # --------------------------------------------------------------------------------------
135
+ QUANT_SIZES = { # ggml type -> (block_size, bytes_per_block)
136
+ 0: (1, 4), 1: (1, 2), 2: (32, 18), 3: (32, 20), 6: (32, 22), 7: (32, 24),
137
+ 8: (32, 34), 9: (32, 36), 10: (256, 84), 11: (256, 110), 12: (256, 144),
138
+ 13: (256, 176), 14: (256, 210), 15: (256, 292), 24: (1, 1), 25: (1, 2),
139
+ 26: (1, 4), 27: (1, 8), 28: (1, 8), 30: (1, 2), 39: (32, 17),
140
+ }
141
+ _FMT = {0: ("<B", 1), 1: ("<b", 1), 2: ("<H", 2), 3: ("<h", 2), 4: ("<I", 4), 5: ("<i", 4),
142
+ 6: ("<f", 4), 7: ("<?", 1), 10: ("<Q", 8), 11: ("<q", 8), 12: ("<d", 8)}
143
+
144
+
145
+ def gguf_meta(path):
146
+ """Parse a GGUF header: selected KVs + the full tensor table. No numpy."""
147
+ import struct
148
+ with open(path, "rb") as f:
149
+ def raw(n):
150
+ return f.read(n)
151
+
152
+ def scalar(t):
153
+ fmt, n = _FMT[t]
154
+ return struct.unpack(fmt, raw(n))[0]
155
+
156
+ def string():
157
+ return raw(scalar(10)).decode("utf-8", "replace")
158
+
159
+ def value(t):
160
+ if t == 8:
161
+ return string()
162
+ if t == 9:
163
+ et, n = scalar(4), scalar(10)
164
+ return [value(et) for _ in range(n)]
165
+ return scalar(t)
166
+
167
+ assert raw(4) == b"GGUF", "not a GGUF file"
168
+ scalar(4)
169
+ n_tensors, n_kv = scalar(10), scalar(10)
170
+ kv = {}
171
+ for _ in range(n_kv):
172
+ k = string()
173
+ t = scalar(4)
174
+ v = value(t)
175
+ if not k.startswith("tokenizer."): # skip the bulky vocab arrays
176
+ kv[k] = v
177
+ tensors = []
178
+ for _ in range(n_tensors):
179
+ name = string()
180
+ ne = [scalar(10) for _ in range(scalar(4))]
181
+ ttype = scalar(4)
182
+ scalar(10) # offset
183
+ tensors.append((name, ne, ttype))
184
+ return dict(kv=kv, tensors=tensors)
185
+
186
+
187
+ def tensor_bytes(ne, ttype):
188
+ n = 1
189
+ for d in ne:
190
+ n *= d
191
+ blk, size = QUANT_SIZES.get(ttype, (1, 4))
192
+ return n // blk * size if n % blk == 0 else (n + blk - 1) // blk * size
193
+
194
+
195
+ def read_budget_gb(meta):
196
+ """Bytes read per decoded token, in GB.
197
+
198
+ Accounting matches the q8dense analysis exactly:
199
+ - token_embd is a ROW GATHER and does NOT count
200
+ - ffn_gate_tid2eid is likewise indexed by token id -- does not count
201
+ - routed experts count only n_expert_used / n_expert of their bytes
202
+ - everything else (including output.weight) is read every token
203
+ """
204
+ kv = meta["kv"]
205
+ arch = kv.get("general.architecture", "")
206
+ n_exp = kv.get(f"{arch}.expert_count", 0) or 0
207
+ n_used = kv.get(f"{arch}.expert_used_count", 0) or 0
208
+ frac = (n_used / n_exp) if n_exp else 1.0
209
+ total = 0
210
+ for name, ne, ttype in meta["tensors"]:
211
+ if name.startswith("token_embd") or "tid2eid" in name:
212
+ continue
213
+ b = tensor_bytes(ne, ttype)
214
+ if "_exps." in name:
215
+ b *= frac
216
+ total += b
217
+ return total / 1e9
218
+
219
+
220
+ def n_layer_of(meta):
221
+ kv = meta["kv"]
222
+ arch = kv.get("general.architecture", "")
223
+ return kv.get(f"{arch}.block_count", 0) or 0
224
+
225
+
226
+ def fence_cost_ms(tg, n_layer, budget_gb, roofline_gbs):
227
+ """Per-layer fence cost: the scale-invariant term a homunculus can actually reproduce.
228
+
229
+ f = (measured ms/token - analytic read time) / n_layer
230
+
231
+ Returns None when it cannot be derived. A NEGATIVE f means the roofline is wrong for this
232
+ host (or the run was not bandwidth-bound), not that fences are free -- report it, do not
233
+ clamp it.
234
+
235
+ READ THE NAME CAREFULLY. This is a RESIDUAL: everything per layer that is not weight reads.
236
+ On a two-node split that residual really is dominated by fences (RTTs, graph serialize,
237
+ submit latency), which is the quantity we claim transfers from toy to real. On a SINGLE-NODE
238
+ config there are no cross-node fences at all, so f there is compute + kernel launch, and it
239
+ is NOT comparable to a two-node f. Only compare f within the same topology class.
240
+ """
241
+ if not tg or not n_layer or not budget_gb or not roofline_gbs:
242
+ return None
243
+ measured_ms = 1000.0 / tg
244
+ reads_ms = budget_gb / roofline_gbs * 1000.0
245
+ return (measured_ms - reads_ms) / n_layer
246
+
247
+
248
+ def substitute(args, subs):
249
+ out = []
250
+ for a in args:
251
+ for k, v in subs.items():
252
+ a = a.replace("{" + k + "}", str(v))
253
+ out.append(a)
254
+ return out
255
+
256
+
257
+ def straddle(args, rpc):
258
+ """Force a config onto BOTH nodes.
259
+
260
+ Real DSV4 is 145.6 GiB and does not fit one 112 GiB device, so any single-node config
261
+ (gpu_all, batch_*, load_*, experts_*) is TOY-ONLY and can never appear in a toy-vs-real
262
+ ranking. But nothing about those axes REQUIRES one node - batch shape and load mode are
263
+ orthogonal to the split. Adding the two-node base makes the whole sweep comparable
264
+ instead of throwing twelve of thirteen configs away.
265
+
266
+ Configs that already name --rpc keep their own split. Configs that drive a
267
+ single-host multi-device split (-dev) are left alone: they are a different axis.
268
+ """
269
+ if "--rpc" in args or "-dev" in args:
270
+ return args
271
+ return ["--rpc", rpc, "-ts", "1/1"] + args
272
+
273
+
274
+ _TS_RE = re.compile(r"^\s*(\d+)\s*[/,]\s*(\d+)\s*$")
275
+
276
+
277
+ def device_share_gib(args, model_gib):
278
+ """Largest per-device share this config implies, in GiB.
279
+
280
+ -ts 3/1 on a 145.6 GiB model puts 109.2 GiB on ONE device against ~112 GiB of usable
281
+ GTT. That is the condition that wedged m5 on 2026-08-04 and cost a physical power
282
+ cycle. Toy-safe does not imply real-safe and the harness could not previously tell.
283
+ """
284
+ ts = None
285
+ for i, x in enumerate(args):
286
+ if x == "-ts" and i + 1 < len(args):
287
+ ts = args[i + 1]
288
+ if ts is None:
289
+ return model_gib # unsplit: the whole model lands on one device
290
+ m = _TS_RE.match(ts)
291
+ if not m:
292
+ return model_gib
293
+ a, b = int(m.group(1)), int(m.group(2))
294
+ if a + b == 0:
295
+ return model_gib
296
+ return model_gib * max(a, b) / (a + b)
297
+
298
+
299
+ def runnable(cfg, caps, have_rpc):
300
+ for need in cfg["needs"]:
301
+ if need == "rpc":
302
+ if not have_rpc:
303
+ return False
304
+ elif need not in caps:
305
+ return False
306
+ return True
307
+
308
+
309
+ def bench_one(bench, model, args, n_prompt, n_gen, reps, timeout, env=None):
310
+ cmd = [bench, "-m", model, "-p", str(n_prompt), "-n", str(n_gen),
311
+ "-r", str(reps), "-o", "json"] + args
312
+ runenv = dict(os.environ)
313
+ runenv.update(env or {})
314
+ t0 = time.time()
315
+ p = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, env=runenv)
316
+ wall = time.time() - t0
317
+ if p.returncode != 0:
318
+ return dict(ok=False, error=(p.stderr or p.stdout)[-800:], wall=wall, cmd=cmd)
319
+ try:
320
+ recs = json.loads(p.stdout)
321
+ except json.JSONDecodeError:
322
+ return dict(ok=False, error="unparseable llama-bench json: " + p.stdout[-400:],
323
+ wall=wall, cmd=cmd)
324
+ out = dict(ok=True, wall=wall, cmd=cmd, pp=None, tg=None)
325
+ for r in recs:
326
+ ts = r.get("avg_ts")
327
+ if r.get("n_prompt", 0) > 0 and r.get("n_gen", 0) == 0:
328
+ out["pp"] = ts
329
+ elif r.get("n_gen", 0) > 0:
330
+ out["tg"] = ts
331
+ return out
332
+
333
+
334
+ def cmd_run(a):
335
+ bench = a.bench or DEFAULT_BENCH
336
+ if not os.path.exists(bench):
337
+ sys.exit(f"llama-bench not found at {bench} (build it, or pass --bench)")
338
+ prof = PROFILES[a.profile]
339
+ caps = list(prof["caps"])
340
+ devs = prof["devs"]
341
+ subs = dict(
342
+ dev0=devs[0] if devs else "",
343
+ dev1=devs[1] if len(devs) > 1 else "",
344
+ rpc=a.rpc or "",
345
+ half_layers=a.half_layers,
346
+ )
347
+
348
+ # analytic side: compute the read budget from the tensor table rather than measuring it
349
+ meta = gguf_meta(a.model)
350
+ n_layer = n_layer_of(meta)
351
+ budget = a.read_budget_gb or read_budget_gb(meta)
352
+ print(f"model: {os.path.basename(a.model)} arch={meta['kv'].get('general.architecture')} "
353
+ f"n_layer={n_layer}")
354
+ print(f"analytic read budget: {budget:.4f} GB/token"
355
+ + ("" if a.read_budget_gb else " (computed from the tensor table)"))
356
+ if a.roofline_gbs:
357
+ print(f"roofline {a.roofline_gbs} GB/s -> {budget/a.roofline_gbs*1000:.2f} ms/token of reads; "
358
+ f"fence cost f will be derived per config")
359
+ else:
360
+ print("no --roofline-gbs given: fence cost f cannot be derived, only raw t/s recorded")
361
+
362
+ only = set(a.only.split(",")) if a.only else None
363
+
364
+ # total model size on disk, for the per-device memory guard
365
+ try:
366
+ model_gib = os.path.getsize(a.model) / (1024 ** 3)
367
+ except OSError:
368
+ model_gib = 0.0
369
+ if a.straddle and not a.rpc:
370
+ sys.exit("--straddle needs --rpc: it works by adding the two-node base to every config")
371
+ if a.straddle:
372
+ print(f"straddle: single-node configs get --rpc {a.rpc} -ts 1/1 so they are "
373
+ f"comparable against a model that cannot fit one device")
374
+ if a.max_device_gib:
375
+ print(f"memory guard: skipping any config whose largest per-device share exceeds "
376
+ f"{a.max_device_gib} GiB (model is {model_gib:.1f} GiB)")
377
+
378
+ results, skipped = {}, {}
379
+ for name, cfg in CONFIGS.items():
380
+ if only and name not in only:
381
+ continue
382
+ if not runnable(cfg, caps, bool(a.rpc)):
383
+ skipped[name] = "host lacks " + ",".join(cfg["needs"])
384
+ continue
385
+ args = substitute(cfg["args"], subs)
386
+ if a.straddle:
387
+ args = straddle(args, a.rpc)
388
+ if a.max_device_gib and model_gib and not cfg.get("guard_exempt"):
389
+ share = device_share_gib(args, model_gib)
390
+ if share > a.max_device_gib:
391
+ skipped[name] = (f"per-device share {share:.1f} GiB > "
392
+ f"--max-device-gib {a.max_device_gib}")
393
+ continue
394
+ env = dict(prof.get("env") or {})
395
+ env.update(cfg.get("env") or {})
396
+ print(f" {name:18s} ...", end="", flush=True)
397
+ try:
398
+ r = bench_one(bench, a.model, args, a.n_prompt, a.n_gen, a.reps, a.timeout, env)
399
+ except subprocess.TimeoutExpired:
400
+ r = dict(ok=False, error=f"timeout after {a.timeout}s", cmd=None)
401
+ r["env"] = env
402
+ r["fence_ms_per_layer"] = fence_cost_ms(r.get("tg"), n_layer, budget, a.roofline_gbs)
403
+ results[name] = r
404
+ if r["ok"]:
405
+ f = r["fence_ms_per_layer"]
406
+ print(f" pp={r.get('pp')} tg={r.get('tg')}"
407
+ + (f" f={f*1000:.0f}us/layer" if f is not None else ""))
408
+ else:
409
+ print(f" FAILED: {r['error'][:120]}")
410
+
411
+ doc = dict(
412
+ label=a.label or os.path.basename(a.model),
413
+ model=os.path.abspath(a.model),
414
+ profile=a.profile, rpc=a.rpc,
415
+ n_prompt=a.n_prompt, n_gen=a.n_gen, reps=a.reps,
416
+ host=os.uname().nodename,
417
+ arch=meta["kv"].get("general.architecture"),
418
+ n_layer=n_layer,
419
+ read_budget_gb=budget,
420
+ roofline_gbs=a.roofline_gbs,
421
+ results=results, skipped=skipped,
422
+ )
423
+ with open(a.out, "w") as f:
424
+ json.dump(doc, f, indent=2)
425
+ print(f"\nwrote {a.out} ({sum(1 for r in results.values() if r['ok'])} ok, "
426
+ f"{sum(1 for r in results.values() if not r['ok'])} failed, {len(skipped)} skipped)")
427
+ if skipped:
428
+ for k, v in skipped.items():
429
+ print(f" SKIPPED {k}: {v}")
430
+
431
+
432
+ # --------------------------------------------------------------------------------------
433
+ # statistics -- pure python, no numpy (must run on m5/m6 system python)
434
+ # --------------------------------------------------------------------------------------
435
+ def ranks(xs):
436
+ order = sorted(range(len(xs)), key=lambda i: xs[i])
437
+ r = [0.0] * len(xs)
438
+ i = 0
439
+ while i < len(order):
440
+ j = i
441
+ while j + 1 < len(order) and xs[order[j + 1]] == xs[order[i]]:
442
+ j += 1
443
+ avg = (i + j) / 2.0 + 1.0
444
+ for k in range(i, j + 1):
445
+ r[order[k]] = avg
446
+ i = j + 1
447
+ return r
448
+
449
+
450
+ def pearson(xs, ys):
451
+ n = len(xs)
452
+ if n < 2:
453
+ return None
454
+ mx, my = statistics.fmean(xs), statistics.fmean(ys)
455
+ num = sum((x - mx) * (y - my) for x, y in zip(xs, ys))
456
+ dx = math.sqrt(sum((x - mx) ** 2 for x in xs))
457
+ dy = math.sqrt(sum((y - my) ** 2 for y in ys))
458
+ if dx == 0 or dy == 0:
459
+ return None
460
+ return num / (dx * dy)
461
+
462
+
463
+ def spearman(xs, ys):
464
+ return pearson(ranks(xs), ranks(ys))
465
+
466
+
467
+ def linfit(xs, ys):
468
+ """least-squares slope/intercept of ys ~ a*xs + b"""
469
+ n = len(xs)
470
+ if n < 2:
471
+ return None, None
472
+ mx, my = statistics.fmean(xs), statistics.fmean(ys)
473
+ den = sum((x - mx) ** 2 for x in xs)
474
+ if den == 0:
475
+ return None, None
476
+ a = sum((x - mx) * (y - my) for x, y in zip(xs, ys)) / den
477
+ return a, my - a * mx
478
+
479
+
480
+ def series(doc, metric):
481
+ return {k: v[metric] for k, v in doc["results"].items()
482
+ if v.get("ok") and v.get(metric) is not None}
483
+
484
+
485
+ def cmd_compare(a):
486
+ real = json.load(open(a.real))
487
+ toys = [json.load(open(p)) for p in a.toy]
488
+
489
+ for metric in ("tg", "pp"):
490
+ rs = series(real, metric)
491
+ if not rs:
492
+ continue
493
+ print(f"\n{'='*78}\nmetric: {metric} real = {real['label']} on {real['host']}")
494
+ print(f"{'='*78}")
495
+ for toy in toys:
496
+ ts = series(toy, metric)
497
+ common = [k for k in CONFIGS if k in rs and k in ts]
498
+ if len(common) < 3:
499
+ print(f"\n{toy['label']}: only {len(common)} shared configs -- not enough to qualify")
500
+ continue
501
+
502
+ # baseline-relative speedups make the comparison scale-free
503
+ base = a.baseline if a.baseline in common else common[0]
504
+ rsp = [rs[k] / rs[base] for k in common]
505
+ tsp = [ts[k] / ts[base] for k in common]
506
+ lr = [math.log(v) for v in rsp]
507
+ lt = [math.log(v) for v in tsp]
508
+
509
+ sp = spearman([rs[k] for k in common], [ts[k] for k in common])
510
+ pe = pearson(lt, lr)
511
+ slope, _ = linfit(lt, lr)
512
+
513
+ print(f"\n{toy['label']} ({toy['host']}, {len(common)} shared configs, baseline={base})")
514
+ print(f" rank correlation (ordering) : {fmt(sp)}")
515
+ print(f" log-speedup correlation : {fmt(pe)} <- is there a RELATIONSHIP")
516
+ print(f" log-speedup slope : {fmt(slope)} <- 1.0 = toy deltas are real deltas")
517
+
518
+ # per-config table + inversions against the real ordering
519
+ real_order = sorted(common, key=lambda k: -rs[k])
520
+ toy_order = sorted(common, key=lambda k: -ts[k])
521
+ rf = {k: real["results"][k].get("fence_ms_per_layer") for k in common}
522
+ tf = {k: toy["results"][k].get("fence_ms_per_layer") for k in common}
523
+ print(f" {'config':<18} {'real t/s':>10} {'toy t/s':>10} {'real x':>8} {'toy x':>8}"
524
+ f" {'real f':>9} {'toy f':>9}")
525
+ for k in real_order:
526
+ fr = f"{rf[k]*1000:.0f}us" if rf.get(k) is not None else "-"
527
+ ft = f"{tf[k]*1000:.0f}us" if tf.get(k) is not None else "-"
528
+ print(f" {k:<18} {rs[k]:>10.2f} {ts[k]:>10.2f} "
529
+ f"{rs[k]/rs[base]:>8.2f} {ts[k]/ts[base]:>8.2f} {fr:>9} {ft:>9}")
530
+
531
+ # The fence cost is the quantity we actually claim transfers, so score it directly.
532
+ fk = [k for k in common if rf.get(k) is not None and tf.get(k) is not None]
533
+ if len(fk) >= 3:
534
+ fsp = spearman([rf[k] for k in fk], [tf[k] for k in fk])
535
+ fsl, _ = linfit([tf[k] for k in fk], [rf[k] for k in fk])
536
+ print(f" fence-cost rank correlation : {fmt(fsp)} over {len(fk)} configs")
537
+ print(f" fence-cost slope (real/toy) : {fmt(fsl)} <- 1.0 = the toy measures "
538
+ f"the real fence directly")
539
+
540
+ inv = [(real_order[i], real_order[j])
541
+ for i in range(len(real_order)) for j in range(i + 1, len(real_order))
542
+ if ts[real_order[i]] < ts[real_order[j]]]
543
+ if inv:
544
+ print(f" INVERSIONS ({len(inv)}) -- each one localises a broken invariant:")
545
+ for x, y in inv[:12]:
546
+ print(f" real says {x} > {y}; toy says the opposite")
547
+ else:
548
+ print(" no inversions: the toy reproduces the real ordering exactly")
549
+
550
+ missing = sorted(set(rs) - set(ts))
551
+ if missing:
552
+ print(f" NOT COMPARED (real has, toy lacks): {', '.join(missing)}")
553
+
554
+
555
+ def cmd_budget(a):
556
+ """Predict the bandwidth term without running anything.
557
+
558
+ This is the q8dense workflow: requantize, compute both budgets, and you already know the
559
+ decode gain before you benchmark. That prediction landed within 0.1 percent on the real
560
+ model (predicted +8.2, measured +8.1).
561
+ """
562
+ rows = []
563
+ for path in a.model:
564
+ meta = gguf_meta(path)
565
+ gb = read_budget_gb(meta)
566
+ rows.append((os.path.basename(path), n_layer_of(meta), gb,
567
+ gb / a.roofline_gbs * 1000.0))
568
+ w = max(len(r[0]) for r in rows)
569
+ print(f"{'model':<{w}} {'layers':>7} {'GB/token':>10} {'ms reads':>9} {'roof t/s':>9}")
570
+ for name, nl, gb, ms in rows:
571
+ print(f"{name:<{w}} {nl:>7} {gb:>10.4f} {ms:>9.2f} {1000.0/ms:>9.2f}")
572
+ if len(rows) == 2:
573
+ a0, a1 = rows[0][2], rows[1][2]
574
+ print(f"\nread budget {a0:.4f} -> {a1:.4f} GB/token"
575
+ f" = predicted decode gain {(a0/a1 - 1)*100:+.2f}%")
576
+ print("(decode-only; prefill is compute-bound and will move far less)")
577
+
578
+
579
+ def fmt(v):
580
+ return "n/a" if v is None else f"{v:+.3f}"
581
+
582
+
583
+ def main():
584
+ ap = argparse.ArgumentParser(description=__doc__,
585
+ formatter_class=argparse.RawDescriptionHelpFormatter)
586
+ sub = ap.add_subparsers(dest="cmd", required=True)
587
+
588
+ r = sub.add_parser("run", help="benchmark one model across the configuration set")
589
+ r.add_argument("--model", required=True)
590
+ r.add_argument("--profile", required=True, choices=sorted(PROFILES))
591
+ r.add_argument("--out", required=True)
592
+ r.add_argument("--label")
593
+ r.add_argument("--bench", help=f"path to llama-bench (default {DEFAULT_BENCH})")
594
+ r.add_argument("--rpc", help="rpc endpoint host:port; enables the rpc_* configs")
595
+ r.add_argument("--only", help="comma-separated config names to run")
596
+ r.add_argument("--straddle", action="store_true",
597
+ help="add the two-node base (--rpc, -ts 1/1) to single-node configs "
598
+ "so they are comparable on a model that needs both nodes")
599
+ r.add_argument("--max-device-gib", type=float,
600
+ help="skip configs whose largest per-device share exceeds this "
601
+ "(guards the -ts 3/1 wedge on big models; ~108 for a 112 GiB device)")
602
+ r.add_argument("--n-prompt", type=int, default=512)
603
+ r.add_argument("--n-gen", type=int, default=128)
604
+ r.add_argument("--reps", type=int, default=3)
605
+ r.add_argument("--half-layers", type=int, default=22,
606
+ help="value for -ncmoe in experts_cpu_half (half the block count)")
607
+ r.add_argument("--timeout", type=int, default=1800)
608
+ r.add_argument("--read-budget-gb", type=float,
609
+ help="override the computed read budget (GB/token)")
610
+ r.add_argument("--roofline-gbs", type=float,
611
+ help="host memory bandwidth in GB/s; required to derive the fence cost f. "
612
+ "The DSV4 two-node analysis used 209.25")
613
+ r.set_defaults(func=cmd_run)
614
+
615
+ b = sub.add_parser("budget", help="compute the analytic read budget of a GGUF, no benchmarking")
616
+ b.add_argument("--model", required=True, action="append",
617
+ help="repeatable; pass two to see the delta between quantizations")
618
+ b.add_argument("--roofline-gbs", type=float, default=209.25)
619
+ b.set_defaults(func=cmd_budget)
620
+
621
+ c = sub.add_parser("compare", help="qualify one or more toys against a real-model run")
622
+ c.add_argument("--real", required=True)
623
+ c.add_argument("--toy", required=True, action="append")
624
+ c.add_argument("--baseline", default="gpu_all",
625
+ help="config used as the 1.0 reference for speedups")
626
+ c.set_defaults(func=cmd_compare)
627
+
628
+ a = ap.parse_args()
629
+ a.func(a)
630
+
631
+
632
+ if __name__ == "__main__":
633
+ main()
results/real-dsv4.json ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "label": "dsv4-0731-q8dense.gguf",
3
+ "model": "/mnt/weights/models/dsv4-flash/dsv4-0731-q8dense.gguf",
4
+ "profile": "m6",
5
+ "rpc": "10.10.10.1:50052",
6
+ "n_prompt": 512,
7
+ "n_gen": 128,
8
+ "reps": 2,
9
+ "host": "m6",
10
+ "arch": "deepseek4",
11
+ "n_layer": 43,
12
+ "read_budget_gb": 10.693818268,
13
+ "roofline_gbs": 209.0,
14
+ "results": {
15
+ "gpu_all": {
16
+ "ok": true,
17
+ "wall": 267.69734954833984,
18
+ "cmd": [
19
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
20
+ "-m",
21
+ "/mnt/weights/models/dsv4-flash/dsv4-0731-q8dense.gguf",
22
+ "-p",
23
+ "512",
24
+ "-n",
25
+ "128",
26
+ "-r",
27
+ "2",
28
+ "-o",
29
+ "json",
30
+ "--rpc",
31
+ "10.10.10.1:50052",
32
+ "-ts",
33
+ "1/1",
34
+ "-ngl",
35
+ "999"
36
+ ],
37
+ "pp": 219.225,
38
+ "tg": 16.292123,
39
+ "env": {},
40
+ "fence_ms_per_layer": 0.23750605430712518
41
+ },
42
+ "load_mmap": {
43
+ "ok": true,
44
+ "wall": 296.21423840522766,
45
+ "cmd": [
46
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
47
+ "-m",
48
+ "/mnt/weights/models/dsv4-flash/dsv4-0731-q8dense.gguf",
49
+ "-p",
50
+ "512",
51
+ "-n",
52
+ "128",
53
+ "-r",
54
+ "2",
55
+ "-o",
56
+ "json",
57
+ "--rpc",
58
+ "10.10.10.1:50052",
59
+ "-ts",
60
+ "1/1",
61
+ "-ngl",
62
+ "999",
63
+ "-lm",
64
+ "mmap"
65
+ ],
66
+ "pp": 217.077537,
67
+ "tg": 16.247997,
68
+ "env": {},
69
+ "fence_ms_per_layer": 0.24138263291319575
70
+ },
71
+ "load_mlock": {
72
+ "ok": true,
73
+ "wall": 310.26967692375183,
74
+ "cmd": [
75
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
76
+ "-m",
77
+ "/mnt/weights/models/dsv4-flash/dsv4-0731-q8dense.gguf",
78
+ "-p",
79
+ "512",
80
+ "-n",
81
+ "128",
82
+ "-r",
83
+ "2",
84
+ "-o",
85
+ "json",
86
+ "--rpc",
87
+ "10.10.10.1:50052",
88
+ "-ts",
89
+ "1/1",
90
+ "-ngl",
91
+ "999",
92
+ "-lm",
93
+ "mmap+mlock"
94
+ ],
95
+ "pp": 233.925787,
96
+ "tg": 16.522971,
97
+ "env": {},
98
+ "fence_ms_per_layer": 0.21756299228459522
99
+ },
100
+ "batch_small": {
101
+ "ok": false,
102
+ "error": "ERK12llama_ubatch14llm_graph_typeP22llama_memory_context_iR11ggml_status+0x11a)[0x73bb01ef599a]\n/home/dad/llama.cpp-ds/build-ds/bin/libllama.so.0(_ZN13llama_context6decodeERK11llama_batch+0x3b8)[0x73bb01efc718]\n/home/dad/llama.cpp-ds/build-ds/bin/libllama.so.0(llama_decode+0xf)[0x73bb01efe39f]\n/home/dad/llama.cpp-ds/build-ds/bin/libllama-bench-impl.so(+0x1bd61)[0x73bb0305fd61]\n/home/dad/llama.cpp-ds/build-ds/bin/libllama-bench-impl.so(_Z11llama_benchiPPc+0xe1e)[0x73bb0306e64e]\n/lib/x86_64-linux-gnu/libc.so.6(+0x2a1ca)[0x73bb0282a1ca]\n/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x8b)[0x73bb0282a28b]\n/home/dad/llama.cpp-ds/build-ds/bin/llama-bench(+0x12a5)[0x5eac31aa52a5]\nterminate called after throwing an instance of 'vk::DeviceLostError'\n what(): vk::Queue::submit: ErrorDeviceLost\n",
103
+ "wall": 298.254257440567,
104
+ "cmd": [
105
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
106
+ "-m",
107
+ "/mnt/weights/models/dsv4-flash/dsv4-0731-q8dense.gguf",
108
+ "-p",
109
+ "512",
110
+ "-n",
111
+ "128",
112
+ "-r",
113
+ "2",
114
+ "-o",
115
+ "json",
116
+ "--rpc",
117
+ "10.10.10.1:50052",
118
+ "-ts",
119
+ "1/1",
120
+ "-ngl",
121
+ "999",
122
+ "-b",
123
+ "256",
124
+ "-ub",
125
+ "256"
126
+ ],
127
+ "env": {},
128
+ "fence_ms_per_layer": null
129
+ },
130
+ "batch_large": {
131
+ "ok": true,
132
+ "wall": 313.57823181152344,
133
+ "cmd": [
134
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
135
+ "-m",
136
+ "/mnt/weights/models/dsv4-flash/dsv4-0731-q8dense.gguf",
137
+ "-p",
138
+ "512",
139
+ "-n",
140
+ "128",
141
+ "-r",
142
+ "2",
143
+ "-o",
144
+ "json",
145
+ "--rpc",
146
+ "10.10.10.1:50052",
147
+ "-ts",
148
+ "1/1",
149
+ "-ngl",
150
+ "999",
151
+ "-b",
152
+ "2048",
153
+ "-ub",
154
+ "512"
155
+ ],
156
+ "pp": 217.886656,
157
+ "tg": 16.272132,
158
+ "env": {},
159
+ "fence_ms_per_layer": 0.2392597083660938
160
+ },
161
+ "rpc_split_even": {
162
+ "ok": true,
163
+ "wall": 330.1122715473175,
164
+ "cmd": [
165
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
166
+ "-m",
167
+ "/mnt/weights/models/dsv4-flash/dsv4-0731-q8dense.gguf",
168
+ "-p",
169
+ "512",
170
+ "-n",
171
+ "128",
172
+ "-r",
173
+ "2",
174
+ "-o",
175
+ "json",
176
+ "-ngl",
177
+ "999",
178
+ "--rpc",
179
+ "10.10.10.1:50052",
180
+ "-ts",
181
+ "1/1"
182
+ ],
183
+ "pp": 217.329445,
184
+ "tg": 16.27393,
185
+ "env": {},
186
+ "fence_ms_per_layer": 0.2391018075663653
187
+ }
188
+ },
189
+ "skipped": {
190
+ "rpc_split_local": "per-device share 108.3 GiB > --max-device-gib 108.0",
191
+ "rpc_split_remote": "per-device share 108.3 GiB > --max-device-gib 108.0",
192
+ "tp_join_peer": "per-device share 144.3 GiB > --max-device-gib 108.0",
193
+ "tp_join_cpu": "per-device share 144.3 GiB > --max-device-gib 108.0"
194
+ }
195
+ }
results/real-ep.json ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "label": "real-ep",
3
+ "model": "/mnt/weights/models/dsv4-flash/dsv4-0731-q8dense.gguf",
4
+ "profile": "m6",
5
+ "rpc": "10.10.10.1:50052",
6
+ "n_prompt": 512,
7
+ "n_gen": 128,
8
+ "reps": 3,
9
+ "host": "m6",
10
+ "arch": "deepseek4",
11
+ "n_layer": 43,
12
+ "read_budget_gb": 10.693818268,
13
+ "roofline_gbs": 209.25,
14
+ "results": {
15
+ "ep_serial": {
16
+ "ok": true,
17
+ "wall": 488.03170013427734,
18
+ "cmd": [
19
+ "/home/dad/llama.cpp-ds/build-ep/bin/llama-bench",
20
+ "-m",
21
+ "/mnt/weights/models/dsv4-flash/dsv4-0731-q8dense.gguf",
22
+ "-p",
23
+ "512",
24
+ "-n",
25
+ "128",
26
+ "-r",
27
+ "3",
28
+ "-o",
29
+ "json",
30
+ "-ngl",
31
+ "999",
32
+ "--rpc",
33
+ "10.10.10.1:50052",
34
+ "-sm",
35
+ "layer",
36
+ "-ts",
37
+ "1/1"
38
+ ],
39
+ "pp": 207.340335,
40
+ "tg": 16.130248,
41
+ "env": {},
42
+ "fence_ms_per_layer": 0.25325263706681345
43
+ },
44
+ "ep_split": {
45
+ "ok": true,
46
+ "wall": 491.44082021713257,
47
+ "cmd": [
48
+ "/home/dad/llama.cpp-ds/build-ep/bin/llama-bench",
49
+ "-m",
50
+ "/mnt/weights/models/dsv4-flash/dsv4-0731-q8dense.gguf",
51
+ "-p",
52
+ "512",
53
+ "-n",
54
+ "128",
55
+ "-r",
56
+ "3",
57
+ "-o",
58
+ "json",
59
+ "-ngl",
60
+ "999",
61
+ "--rpc",
62
+ "10.10.10.1:50052",
63
+ "-sm",
64
+ "layer",
65
+ "-ts",
66
+ "0/1"
67
+ ],
68
+ "pp": 135.456388,
69
+ "tg": 11.637701,
70
+ "env": {
71
+ "LLAMA_TP": "1",
72
+ "LLAMA_TP_EP": "1",
73
+ "LLAMA_TP_JOIN": "cpu",
74
+ "GGML_RPC_PUSH": "1"
75
+ },
76
+ "fence_ms_per_layer": 0.8098176911308856
77
+ }
78
+ },
79
+ "skipped": {}
80
+ }
results/real-loadmodes.json ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "label": "dsv4-0731-q8dense.gguf",
3
+ "model": "/mnt/weights/models/dsv4-flash/dsv4-0731-q8dense.gguf",
4
+ "profile": "m6",
5
+ "rpc": "10.10.10.1:50052",
6
+ "n_prompt": 512,
7
+ "n_gen": 128,
8
+ "reps": 3,
9
+ "host": "m6",
10
+ "arch": "deepseek4",
11
+ "n_layer": 43,
12
+ "read_budget_gb": 10.693818268,
13
+ "roofline_gbs": 209.0,
14
+ "results": {
15
+ "load_none": {
16
+ "ok": true,
17
+ "wall": 243.08253479003906,
18
+ "cmd": [
19
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
20
+ "-m",
21
+ "/mnt/weights/models/dsv4-flash/dsv4-0731-q8dense.gguf",
22
+ "-p",
23
+ "512",
24
+ "-n",
25
+ "128",
26
+ "-r",
27
+ "3",
28
+ "-o",
29
+ "json",
30
+ "--rpc",
31
+ "10.10.10.1:50052",
32
+ "-ts",
33
+ "1/1",
34
+ "-ngl",
35
+ "999",
36
+ "-lm",
37
+ "none"
38
+ ],
39
+ "pp": 235.564841,
40
+ "tg": 16.53925,
41
+ "env": {},
42
+ "fence_ms_per_layer": 0.21617765566267108
43
+ },
44
+ "load_mmap": {
45
+ "ok": true,
46
+ "wall": 280.40933322906494,
47
+ "cmd": [
48
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
49
+ "-m",
50
+ "/mnt/weights/models/dsv4-flash/dsv4-0731-q8dense.gguf",
51
+ "-p",
52
+ "512",
53
+ "-n",
54
+ "128",
55
+ "-r",
56
+ "3",
57
+ "-o",
58
+ "json",
59
+ "--rpc",
60
+ "10.10.10.1:50052",
61
+ "-ts",
62
+ "1/1",
63
+ "-ngl",
64
+ "999",
65
+ "-lm",
66
+ "mmap"
67
+ ],
68
+ "pp": 219.095126,
69
+ "tg": 16.328015,
70
+ "env": {},
71
+ "fence_ms_per_layer": 0.23436830579279866
72
+ },
73
+ "load_mlock_only": {
74
+ "ok": true,
75
+ "wall": 248.1985890865326,
76
+ "cmd": [
77
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
78
+ "-m",
79
+ "/mnt/weights/models/dsv4-flash/dsv4-0731-q8dense.gguf",
80
+ "-p",
81
+ "512",
82
+ "-n",
83
+ "128",
84
+ "-r",
85
+ "3",
86
+ "-o",
87
+ "json",
88
+ "--rpc",
89
+ "10.10.10.1:50052",
90
+ "-ts",
91
+ "1/1",
92
+ "-ngl",
93
+ "999",
94
+ "-lm",
95
+ "mlock"
96
+ ],
97
+ "pp": 235.030111,
98
+ "tg": 16.539419,
99
+ "env": {},
100
+ "fence_ms_per_layer": 0.2161632881301883
101
+ },
102
+ "load_mlock": {
103
+ "ok": true,
104
+ "wall": 294.4846339225769,
105
+ "cmd": [
106
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
107
+ "-m",
108
+ "/mnt/weights/models/dsv4-flash/dsv4-0731-q8dense.gguf",
109
+ "-p",
110
+ "512",
111
+ "-n",
112
+ "128",
113
+ "-r",
114
+ "3",
115
+ "-o",
116
+ "json",
117
+ "--rpc",
118
+ "10.10.10.1:50052",
119
+ "-ts",
120
+ "1/1",
121
+ "-ngl",
122
+ "999",
123
+ "-lm",
124
+ "mmap+mlock"
125
+ ],
126
+ "pp": 234.170396,
127
+ "tg": 16.527009,
128
+ "env": {},
129
+ "fence_ms_per_layer": 0.21721910552501877
130
+ },
131
+ "load_dio": {
132
+ "ok": true,
133
+ "wall": 225.16271948814392,
134
+ "cmd": [
135
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
136
+ "-m",
137
+ "/mnt/weights/models/dsv4-flash/dsv4-0731-q8dense.gguf",
138
+ "-p",
139
+ "512",
140
+ "-n",
141
+ "128",
142
+ "-r",
143
+ "3",
144
+ "-o",
145
+ "json",
146
+ "--rpc",
147
+ "10.10.10.1:50052",
148
+ "-ts",
149
+ "1/1",
150
+ "-ngl",
151
+ "999",
152
+ "-lm",
153
+ "dio"
154
+ ],
155
+ "pp": 235.83676,
156
+ "tg": 16.535795,
157
+ "env": {},
158
+ "fence_ms_per_layer": 0.2164714468096389
159
+ }
160
+ },
161
+ "skipped": {}
162
+ }
results/toy-ep.json ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "label": "toy-ep",
3
+ "model": "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
4
+ "profile": "m6",
5
+ "rpc": "10.10.10.1:50052",
6
+ "n_prompt": 512,
7
+ "n_gen": 128,
8
+ "reps": 3,
9
+ "host": "m6",
10
+ "arch": "deepseek4",
11
+ "n_layer": 12,
12
+ "read_budget_gb": 0.049312852,
13
+ "roofline_gbs": 209.25,
14
+ "results": {
15
+ "ep_serial": {
16
+ "ok": true,
17
+ "wall": 5.138993501663208,
18
+ "cmd": [
19
+ "/home/dad/llama.cpp-ds/build-ep/bin/llama-bench",
20
+ "-m",
21
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
22
+ "-p",
23
+ "512",
24
+ "-n",
25
+ "128",
26
+ "-r",
27
+ "3",
28
+ "-o",
29
+ "json",
30
+ "-ngl",
31
+ "999",
32
+ "--rpc",
33
+ "10.10.10.1:50052",
34
+ "-sm",
35
+ "layer",
36
+ "-ts",
37
+ "1/1"
38
+ ],
39
+ "pp": 15341.239918,
40
+ "tg": 306.631127,
41
+ "env": {},
42
+ "fence_ms_per_layer": 0.2521318956054091
43
+ },
44
+ "ep_split": {
45
+ "ok": true,
46
+ "wall": 4.609577178955078,
47
+ "cmd": [
48
+ "/home/dad/llama.cpp-ds/build-ep/bin/llama-bench",
49
+ "-m",
50
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
51
+ "-p",
52
+ "512",
53
+ "-n",
54
+ "128",
55
+ "-r",
56
+ "3",
57
+ "-o",
58
+ "json",
59
+ "-ngl",
60
+ "999",
61
+ "--rpc",
62
+ "10.10.10.1:50052",
63
+ "-sm",
64
+ "layer",
65
+ "-ts",
66
+ "0/1"
67
+ ],
68
+ "pp": 5997.841429,
69
+ "tg": 134.253405,
70
+ "env": {
71
+ "LLAMA_TP": "1",
72
+ "LLAMA_TP_EP": "1",
73
+ "LLAMA_TP_JOIN": "cpu",
74
+ "GGML_RPC_PUSH": "1"
75
+ },
76
+ "fence_ms_per_layer": 0.6010779906036318
77
+ }
78
+ },
79
+ "skipped": {}
80
+ }
results/toy-h1-straddle.json ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "label": "h1-topology-mxfp4.gguf",
3
+ "model": "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
4
+ "profile": "m6",
5
+ "rpc": "10.10.10.1:50052",
6
+ "n_prompt": 512,
7
+ "n_gen": 128,
8
+ "reps": 2,
9
+ "host": "m6",
10
+ "arch": "deepseek4",
11
+ "n_layer": 12,
12
+ "read_budget_gb": 0.049312852,
13
+ "roofline_gbs": null,
14
+ "results": {
15
+ "cpu_only": {
16
+ "ok": true,
17
+ "wall": 1.1713976860046387,
18
+ "cmd": [
19
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
20
+ "-m",
21
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
22
+ "-p",
23
+ "512",
24
+ "-n",
25
+ "128",
26
+ "-r",
27
+ "2",
28
+ "-o",
29
+ "json",
30
+ "--rpc",
31
+ "10.10.10.1:50052",
32
+ "-ts",
33
+ "1/1",
34
+ "-ngl",
35
+ "0"
36
+ ],
37
+ "pp": 3570.430355,
38
+ "tg": 505.44811,
39
+ "env": {},
40
+ "fence_ms_per_layer": null
41
+ },
42
+ "gpu_all": {
43
+ "ok": true,
44
+ "wall": 2.617534875869751,
45
+ "cmd": [
46
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
47
+ "-m",
48
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
49
+ "-p",
50
+ "512",
51
+ "-n",
52
+ "128",
53
+ "-r",
54
+ "2",
55
+ "-o",
56
+ "json",
57
+ "--rpc",
58
+ "10.10.10.1:50052",
59
+ "-ts",
60
+ "1/1",
61
+ "-ngl",
62
+ "999"
63
+ ],
64
+ "pp": 15582.895451,
65
+ "tg": 321.452023,
66
+ "env": {},
67
+ "fence_ms_per_layer": null
68
+ },
69
+ "experts_cpu_ot": {
70
+ "ok": true,
71
+ "wall": 3.5321099758148193,
72
+ "cmd": [
73
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
74
+ "-m",
75
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
76
+ "-p",
77
+ "512",
78
+ "-n",
79
+ "128",
80
+ "-r",
81
+ "2",
82
+ "-o",
83
+ "json",
84
+ "--rpc",
85
+ "10.10.10.1:50052",
86
+ "-ts",
87
+ "1/1",
88
+ "-ngl",
89
+ "999",
90
+ "-ot",
91
+ "exps=CPU"
92
+ ],
93
+ "pp": 3907.97141,
94
+ "tg": 146.2889,
95
+ "env": {},
96
+ "fence_ms_per_layer": null
97
+ },
98
+ "experts_cpu_all": {
99
+ "ok": true,
100
+ "wall": 4.073307514190674,
101
+ "cmd": [
102
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
103
+ "-m",
104
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
105
+ "-p",
106
+ "512",
107
+ "-n",
108
+ "128",
109
+ "-r",
110
+ "2",
111
+ "-o",
112
+ "json",
113
+ "--rpc",
114
+ "10.10.10.1:50052",
115
+ "-ts",
116
+ "1/1",
117
+ "-ngl",
118
+ "999",
119
+ "-ncmoe",
120
+ "999"
121
+ ],
122
+ "pp": 3777.039085,
123
+ "tg": 148.639188,
124
+ "env": {},
125
+ "fence_ms_per_layer": null
126
+ },
127
+ "experts_cpu_half": {
128
+ "ok": true,
129
+ "wall": 3.53596568107605,
130
+ "cmd": [
131
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
132
+ "-m",
133
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
134
+ "-p",
135
+ "512",
136
+ "-n",
137
+ "128",
138
+ "-r",
139
+ "2",
140
+ "-o",
141
+ "json",
142
+ "--rpc",
143
+ "10.10.10.1:50052",
144
+ "-ts",
145
+ "1/1",
146
+ "-ngl",
147
+ "999",
148
+ "-ncmoe",
149
+ "22"
150
+ ],
151
+ "pp": 3845.900727,
152
+ "tg": 147.245819,
153
+ "env": {},
154
+ "fence_ms_per_layer": null
155
+ },
156
+ "dev_split_even": {
157
+ "ok": false,
158
+ "error": "ggml_vulkan: Found 1 Vulkan devices:\nggml_vulkan: 0 = Radeon 8060S Graphics (RADV STRIX_HALO) (radv) | uma: 1 | fp16: dot2 | bf16: 0 | fp4: 0 | warp size: 64 | shared memory: 65536 | int dot: 0 | matrix cores: KHR_coopmat\nerror: invalid device: Vulkan1\nerror: invalid parameter for argument: -dev\n",
159
+ "wall": 0.010591983795166016,
160
+ "cmd": [
161
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
162
+ "-m",
163
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
164
+ "-p",
165
+ "512",
166
+ "-n",
167
+ "128",
168
+ "-r",
169
+ "2",
170
+ "-o",
171
+ "json",
172
+ "-ngl",
173
+ "999",
174
+ "-dev",
175
+ "Vulkan0,Vulkan1",
176
+ "-ts",
177
+ "1/1"
178
+ ],
179
+ "env": {},
180
+ "fence_ms_per_layer": null
181
+ },
182
+ "dev_split_skew": {
183
+ "ok": false,
184
+ "error": "ggml_vulkan: Found 1 Vulkan devices:\nggml_vulkan: 0 = Radeon 8060S Graphics (RADV STRIX_HALO) (radv) | uma: 1 | fp16: dot2 | bf16: 0 | fp4: 0 | warp size: 64 | shared memory: 65536 | int dot: 0 | matrix cores: KHR_coopmat\nerror: invalid device: Vulkan1\nerror: invalid parameter for argument: -dev\n",
185
+ "wall": 0.010843038558959961,
186
+ "cmd": [
187
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
188
+ "-m",
189
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
190
+ "-p",
191
+ "512",
192
+ "-n",
193
+ "128",
194
+ "-r",
195
+ "2",
196
+ "-o",
197
+ "json",
198
+ "-ngl",
199
+ "999",
200
+ "-dev",
201
+ "Vulkan0,Vulkan1",
202
+ "-ts",
203
+ "3/1"
204
+ ],
205
+ "env": {},
206
+ "fence_ms_per_layer": null
207
+ },
208
+ "load_mmap": {
209
+ "ok": true,
210
+ "wall": 2.610311508178711,
211
+ "cmd": [
212
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
213
+ "-m",
214
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
215
+ "-p",
216
+ "512",
217
+ "-n",
218
+ "128",
219
+ "-r",
220
+ "2",
221
+ "-o",
222
+ "json",
223
+ "--rpc",
224
+ "10.10.10.1:50052",
225
+ "-ts",
226
+ "1/1",
227
+ "-ngl",
228
+ "999",
229
+ "-lm",
230
+ "mmap"
231
+ ],
232
+ "pp": 15446.844927,
233
+ "tg": 321.834936,
234
+ "env": {},
235
+ "fence_ms_per_layer": null
236
+ },
237
+ "load_mlock": {
238
+ "ok": true,
239
+ "wall": 2.610318660736084,
240
+ "cmd": [
241
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
242
+ "-m",
243
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
244
+ "-p",
245
+ "512",
246
+ "-n",
247
+ "128",
248
+ "-r",
249
+ "2",
250
+ "-o",
251
+ "json",
252
+ "--rpc",
253
+ "10.10.10.1:50052",
254
+ "-ts",
255
+ "1/1",
256
+ "-ngl",
257
+ "999",
258
+ "-lm",
259
+ "mmap+mlock"
260
+ ],
261
+ "pp": 15565.496584,
262
+ "tg": 321.876554,
263
+ "env": {},
264
+ "fence_ms_per_layer": null
265
+ },
266
+ "batch_small": {
267
+ "ok": true,
268
+ "wall": 2.623216152191162,
269
+ "cmd": [
270
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
271
+ "-m",
272
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
273
+ "-p",
274
+ "512",
275
+ "-n",
276
+ "128",
277
+ "-r",
278
+ "2",
279
+ "-o",
280
+ "json",
281
+ "--rpc",
282
+ "10.10.10.1:50052",
283
+ "-ts",
284
+ "1/1",
285
+ "-ngl",
286
+ "999",
287
+ "-b",
288
+ "256",
289
+ "-ub",
290
+ "256"
291
+ ],
292
+ "pp": 12934.798133,
293
+ "tg": 321.866921,
294
+ "env": {},
295
+ "fence_ms_per_layer": null
296
+ },
297
+ "batch_large": {
298
+ "ok": true,
299
+ "wall": 2.612264394760132,
300
+ "cmd": [
301
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
302
+ "-m",
303
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
304
+ "-p",
305
+ "512",
306
+ "-n",
307
+ "128",
308
+ "-r",
309
+ "2",
310
+ "-o",
311
+ "json",
312
+ "--rpc",
313
+ "10.10.10.1:50052",
314
+ "-ts",
315
+ "1/1",
316
+ "-ngl",
317
+ "999",
318
+ "-b",
319
+ "2048",
320
+ "-ub",
321
+ "512"
322
+ ],
323
+ "pp": 15505.585415,
324
+ "tg": 322.102516,
325
+ "env": {},
326
+ "fence_ms_per_layer": null
327
+ },
328
+ "rpc_split_even": {
329
+ "ok": true,
330
+ "wall": 2.607177495956421,
331
+ "cmd": [
332
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
333
+ "-m",
334
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
335
+ "-p",
336
+ "512",
337
+ "-n",
338
+ "128",
339
+ "-r",
340
+ "2",
341
+ "-o",
342
+ "json",
343
+ "-ngl",
344
+ "999",
345
+ "--rpc",
346
+ "10.10.10.1:50052",
347
+ "-ts",
348
+ "1/1"
349
+ ],
350
+ "pp": 15514.418971,
351
+ "tg": 321.84373,
352
+ "env": {},
353
+ "fence_ms_per_layer": null
354
+ },
355
+ "rpc_split_local": {
356
+ "ok": true,
357
+ "wall": 3.292717218399048,
358
+ "cmd": [
359
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
360
+ "-m",
361
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
362
+ "-p",
363
+ "512",
364
+ "-n",
365
+ "128",
366
+ "-r",
367
+ "2",
368
+ "-o",
369
+ "json",
370
+ "-ngl",
371
+ "999",
372
+ "--rpc",
373
+ "10.10.10.1:50052",
374
+ "-ts",
375
+ "3/1"
376
+ ],
377
+ "pp": 15716.61865,
378
+ "tg": 302.911374,
379
+ "env": {},
380
+ "fence_ms_per_layer": null
381
+ },
382
+ "rpc_split_remote": {
383
+ "ok": true,
384
+ "wall": 1.8777427673339844,
385
+ "cmd": [
386
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
387
+ "-m",
388
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
389
+ "-p",
390
+ "512",
391
+ "-n",
392
+ "128",
393
+ "-r",
394
+ "2",
395
+ "-o",
396
+ "json",
397
+ "-ngl",
398
+ "999",
399
+ "--rpc",
400
+ "10.10.10.1:50052",
401
+ "-ts",
402
+ "1/3"
403
+ ],
404
+ "pp": 16355.534594,
405
+ "tg": 332.586155,
406
+ "env": {},
407
+ "fence_ms_per_layer": null
408
+ },
409
+ "tp_join_peer": {
410
+ "ok": true,
411
+ "wall": 0.9517486095428467,
412
+ "cmd": [
413
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
414
+ "-m",
415
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
416
+ "-p",
417
+ "512",
418
+ "-n",
419
+ "128",
420
+ "-r",
421
+ "2",
422
+ "-o",
423
+ "json",
424
+ "-ngl",
425
+ "999",
426
+ "--rpc",
427
+ "10.10.10.1:50052",
428
+ "-ts",
429
+ "0/1"
430
+ ],
431
+ "pp": 34327.399852,
432
+ "tg": 403.44726,
433
+ "env": {
434
+ "LLAMA_TP": "1"
435
+ },
436
+ "fence_ms_per_layer": null
437
+ },
438
+ "tp_join_cpu": {
439
+ "ok": true,
440
+ "wall": 0.9493029117584229,
441
+ "cmd": [
442
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
443
+ "-m",
444
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
445
+ "-p",
446
+ "512",
447
+ "-n",
448
+ "128",
449
+ "-r",
450
+ "2",
451
+ "-o",
452
+ "json",
453
+ "-ngl",
454
+ "999",
455
+ "--rpc",
456
+ "10.10.10.1:50052",
457
+ "-ts",
458
+ "0/1"
459
+ ],
460
+ "pp": 34328.079727,
461
+ "tg": 403.314183,
462
+ "env": {
463
+ "LLAMA_TP": "1",
464
+ "LLAMA_TP_JOIN": "cpu"
465
+ },
466
+ "fence_ms_per_layer": null
467
+ }
468
+ },
469
+ "skipped": {}
470
+ }
results/toy-loadmodes.json ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "label": "h1-topology-mxfp4.gguf",
3
+ "model": "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
4
+ "profile": "m6",
5
+ "rpc": "10.10.10.1:50052",
6
+ "n_prompt": 512,
7
+ "n_gen": 128,
8
+ "reps": 3,
9
+ "host": "m6",
10
+ "arch": "deepseek4",
11
+ "n_layer": 12,
12
+ "read_budget_gb": 0.049312852,
13
+ "roofline_gbs": 209.0,
14
+ "results": {
15
+ "load_none": {
16
+ "ok": true,
17
+ "wall": 3.1830012798309326,
18
+ "cmd": [
19
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
20
+ "-m",
21
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
22
+ "-p",
23
+ "512",
24
+ "-n",
25
+ "128",
26
+ "-r",
27
+ "3",
28
+ "-o",
29
+ "json",
30
+ "--rpc",
31
+ "10.10.10.1:50052",
32
+ "-ts",
33
+ "1/1",
34
+ "-ngl",
35
+ "999",
36
+ "-lm",
37
+ "none"
38
+ ],
39
+ "pp": 15826.95342,
40
+ "tg": 320.659779,
41
+ "env": {},
42
+ "fence_ms_per_layer": 0.24021862021984272
43
+ },
44
+ "load_mmap": {
45
+ "ok": true,
46
+ "wall": 3.049060821533203,
47
+ "cmd": [
48
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
49
+ "-m",
50
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
51
+ "-p",
52
+ "512",
53
+ "-n",
54
+ "128",
55
+ "-r",
56
+ "3",
57
+ "-o",
58
+ "json",
59
+ "--rpc",
60
+ "10.10.10.1:50052",
61
+ "-ts",
62
+ "1/1",
63
+ "-ngl",
64
+ "999",
65
+ "-lm",
66
+ "mmap"
67
+ ],
68
+ "pp": 15831.616962,
69
+ "tg": 321.29154,
70
+ "env": {},
71
+ "fence_ms_per_layer": 0.2397076121162177
72
+ },
73
+ "load_mlock_only": {
74
+ "ok": true,
75
+ "wall": 3.054117441177368,
76
+ "cmd": [
77
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
78
+ "-m",
79
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
80
+ "-p",
81
+ "512",
82
+ "-n",
83
+ "128",
84
+ "-r",
85
+ "3",
86
+ "-o",
87
+ "json",
88
+ "--rpc",
89
+ "10.10.10.1:50052",
90
+ "-ts",
91
+ "1/1",
92
+ "-ngl",
93
+ "999",
94
+ "-lm",
95
+ "mlock"
96
+ ],
97
+ "pp": 15780.65358,
98
+ "tg": 320.533263,
99
+ "env": {},
100
+ "fence_ms_per_layer": 0.24032119642141203
101
+ },
102
+ "load_mlock": {
103
+ "ok": true,
104
+ "wall": 3.041682720184326,
105
+ "cmd": [
106
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
107
+ "-m",
108
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
109
+ "-p",
110
+ "512",
111
+ "-n",
112
+ "128",
113
+ "-r",
114
+ "3",
115
+ "-o",
116
+ "json",
117
+ "--rpc",
118
+ "10.10.10.1:50052",
119
+ "-ts",
120
+ "1/1",
121
+ "-ngl",
122
+ "999",
123
+ "-lm",
124
+ "mmap+mlock"
125
+ ],
126
+ "pp": 15785.781199,
127
+ "tg": 321.214943,
128
+ "env": {},
129
+ "fence_ms_per_layer": 0.23976946151517323
130
+ },
131
+ "load_dio": {
132
+ "ok": true,
133
+ "wall": 3.102926731109619,
134
+ "cmd": [
135
+ "/home/dad/llama.cpp-ds/build-ds/bin/llama-bench",
136
+ "-m",
137
+ "/mnt/weights/models/dev/h1-topology-mxfp4.gguf",
138
+ "-p",
139
+ "512",
140
+ "-n",
141
+ "128",
142
+ "-r",
143
+ "3",
144
+ "-o",
145
+ "json",
146
+ "--rpc",
147
+ "10.10.10.1:50052",
148
+ "-ts",
149
+ "1/1",
150
+ "-ngl",
151
+ "999",
152
+ "-lm",
153
+ "dio"
154
+ ],
155
+ "pp": 15783.352468,
156
+ "tg": 321.35924,
157
+ "env": {},
158
+ "fence_ms_per_layer": 0.2396529712795368
159
+ }
160
+ },
161
+ "skipped": {}
162
+ }