avlp12 commited on
Commit
ff79e7b
·
verified ·
1 Parent(s): e4e3d46

Restate speculative-decoding tables under the EOS-cut protocol; add real-world sampling numbers

Browse files
Files changed (4) hide show
  1. README.md +30 -21
  2. assets/builds.png +2 -2
  3. assets/builds.svg +27 -45
  4. assets/make_builds_png.py +40 -55
README.md CHANGED
@@ -17,7 +17,7 @@ tags:
17
 
18
  # Qwen3.8-27B-Alis-MLX-8bit
19
 
20
- ![Build size vs decode speed across the 8-bit, 6-bit and 4-bit builds, with Korean corpus perplexity and both speculative paths, MTP k=2 and the DSpark drafter](assets/builds.png)
21
 
22
  An 8-bit MLX quantization of [Qwen/Qwen3.8-27B](https://huggingface.co/Qwen/Qwen3.8-27B) for Apple silicon, with the **vision tower and the MTP head both preserved**. 27.9 GB on disk, 21.8 tok/s decode, and corpus perplexity **statistically indistinguishable from the bf16 original** on English, Korean, and code alike — paired over ≈103K scored tokens. This is the fidelity build of the set.
23
 
@@ -150,25 +150,33 @@ mlx_lm.generate --model avlp12/Qwen3.8-27B-Alis-MLX-8bit \
150
  --max-tokens 512
151
  ```
152
 
153
- **Every speculative figure below was measured on the 4-bit build of this setits earlier uniform 4-bit weights not on this one.** It is quoted here because it is what the fork's two speculative paths do on this checkpoint, and because the ordering between them is informative but the multiples do not transfer to this build, for a reason spelled out under the table. Measured on one Apple M3 Ultra (512 GB), greedy, 240 tokens per prompt, timed after an 8-token warmup, every configuration in the same process, over four prompts: chat, code, math, and Korean.
154
 
155
- | Configuration, 4-bit build | 4-prompt avg | 3 English prompts | chat | code | math | ko |
156
- |---|---|---|---|---|---|---|
157
- | plain | 37.63 | 37.64 | 37.7 | 37.6 | 37.6 | 37.6 |
158
- | MTP k=2 | 50.36 (1.34×) | 55.71 (1.48×) | 50.6 | 54.6 | 62.0 | 34.3 |
159
- | DSpark drafter | 62.21 (1.65×) | 71.86 (1.91×) | 49.0 | 75.1 | 91.5 | 33.3 |
160
 
161
- Lossless on that build: all four prompts reproduced plain greedy decoding **token for token through 80 tokens**.
 
 
 
 
 
162
 
163
- **Three footnotes on this table, added after an audit of the measurement harness.** First, the harness did **not stop at end-of-sequence** every run decoded a fixed 240 tokens, so on prompts whose answer ends early, part of the window measures post-termination behavior, and per-prompt figures are sensitive to what a build happens to do after its answer ends. Read the averages, not the per-prompt cells. Second, acceptance-rate comparisons **between builds** are only meaningful on a common token sequence, scored paired — each build free-running its own text measures the text as much as the build. Third, the 4-bit build measured here was that repo's earlier uniform release; the AWQ weights now hosted there measure **within ±1%** of it on both speculative paths under an EOS-cut paired protocol. A full re-measurement under the corrected harness is a separate track.
164
 
165
- **Why this build cannot inherit those multiples.** What lifted them is a small-M quantized-GEMM kernel that the fork now enables from inside `mlx_lm.utils.load()`. It fires only in the multi-token verification window (M = 6..8) and only on **4-bit, group-64** weights, so it never engages on an 8-bit build; plain decode at M = 1 is untouched on every build either way. The gap it fills is upstream: [ml-explore/mlx#4265](https://github.com/ml-explore/mlx/issues/4265). Set `MLXLM_NO_FAST_QMM=1` to opt out.
 
 
 
 
 
166
 
167
- On this build MTP was measured at **21.9 to 31.3 tok/s, a 43% gain**but treat that as a **pre-kernel measurement on an older three-prompt English-and-code set**, not a row of the table above, and not re-measured since. k=2 remains the sensible starting point here; measure your own workload rather than borrowing a number from either source.
168
 
169
- ## DSpark: an external drafter, measured on the 4-bit build
170
 
171
- DSpark is the fork's other speculative path. Unlike MTP it is **not** self-speculation: it drives an external drafter, [`RadixArk/Qwen3.8-27B-DSpark`](https://huggingface.co/RadixArk/Qwen3.8-27B-DSpark) — 1.36B parameters, trained against this checkpoint, published by a third party. The drafter carries no embedding and no output head of its own; it borrows the target's, and drafts a whole block of positions in one forward pass instead of chaining them.
 
 
172
 
173
  Two things to settle first:
174
 
@@ -194,23 +202,24 @@ prompt = mx.array(tokenizer.apply_chat_template(
194
  add_generation_prompt=True,
195
  ))
196
 
197
- for token, _accepted in dspark_generate_step(prompt, model, draft, max_tokens=512):
 
198
  print(tokenizer.decode([token.item()]), end="", flush=True)
199
  ```
200
 
201
- The defaults are the measured operating point, not a permissive setting block size 8, verification width capped at 8, the drafter's confidence head offso the call above needs no tuning to reproduce the 4-bit row.
202
 
203
- **Temper your expectations on this build specifically.** DSpark's margin over MTP rests on the kernel described above, which does not engage at 8 bits, and in pre-kernel measurements an 8-bit target ran DSpark **below** plain decode. It has not been re-measured since the kernel landed. Download 2.7 GB on the strength of the 4-bit row and you may find nothing here to collect.
204
 
205
- ### Do not turn speculative decoding on for Korean
206
 
207
- The Korean column is the one to read before enabling any of this. On the Korean prompt **both** speculative paths came in **slower than plain decode** DSpark 33.3 and MTP 34.3 against 37.6 tok/s plain while the same two configurations returned 75.1 and 54.6 on the code prompt. Speculation only pays when the drafted tokens survive verification; on Korean they do not survive often enough to cover what drafting and verifying cost, so the machinery becomes pure overhead. **Run Korean workloads plain.**
208
 
209
  ### Measurement protocol, and why single-prompt numbers lie
210
 
211
- Every figure in the table is the **average over the fixed four-prompt set**, never a single run, and the spread inside one row is why. In the DSpark row the code prompt reads **75.1 tok/s** and the math prompt **91.5 tok/s**, while the honest four-prompt average is **62.2**. Quote the math prompt on its own and you are advertising 91.5 for a build that delivers 62.2 across mixed traffic — the same measurement, the same process, differing by nothing but the choice of prompt.
212
 
213
- Speculative decoding pays off in proportion to how predictable the continuation is, so a benchmark built from one favorable prompt **overstates the gain you will see in real traffic** by a wide margin. Treat any single-prompt speculative figure, including mine, as an upper bound.
214
 
215
  ## KV cache quantization
216
 
@@ -278,7 +287,7 @@ Two honest caveats on the table itself:
278
  - **Vision quality is unquantified.** The tower is preserved bit-exact and verified to work on a shapes probe. That is a functional check, not a VQA evaluation. No image benchmark was run at any bit-width.
279
  - **Shape-dependent numerics.** The hybrid stack takes different kernel paths depending on the shape of the input it receives — the batched scoring pass behind the perplexity table is not the same shape as incremental decode, and near-tie positions can resolve differently between those paths. Perplexity is far less sensitive to this than an argmax metric, but do not expect bit-identical numbers from a different harness.
280
  - **Long-form generated reasoning is unprobed.** Perplexity scores 2048-token windows of existing text; it does not exercise long generated reasoning chains, where low-bit damage tends to concentrate. Spot-check your own thinking-mode prompts.
281
- - **Speculative-decoding gains are workload-dependent, and the harness caveat applies.** See the footnotes under the speculative table.
282
 
283
  ## License and attribution
284
 
 
17
 
18
  # Qwen3.8-27B-Alis-MLX-8bit
19
 
20
+ ![Build size vs decode speed across the 8-bit, 6-bit and 4-bit builds, with Korean corpus perplexity and the speculative ladder on the 4-bit build: MTP k=2 and the gated MTP k=4 operating point](assets/builds.png)
21
 
22
  An 8-bit MLX quantization of [Qwen/Qwen3.8-27B](https://huggingface.co/Qwen/Qwen3.8-27B) for Apple silicon, with the **vision tower and the MTP head both preserved**. 27.9 GB on disk, 21.8 tok/s decode, and corpus perplexity **statistically indistinguishable from the bf16 original** on English, Korean, and code alike — paired over ≈103K scored tokens. This is the fidelity build of the set.
23
 
 
150
  --max-tokens 512
151
  ```
152
 
153
+ **Correction (2026-08-16).** The speculative table earlier revisions of this card quoted from the 4-bit build plain 37.63, MTP k=2 50.36, DSpark 62.21 came from a harness that **did not stop at end-of-sequence**: on prompts whose answer ended inside the fixed window, the remainder measured post-termination behavior, and on the math prompt the model spent that tail copying its own finished answer (acceptance 4.53), which carried most of the headline. Those figures are retracted, as the footnote they carried promised they would be if unreproducible. Everything below is the re-measurement: **the harness stops at EOS, the four prompts (chat / code / math / Korean) are long-form** so every scored token is genuine answer decode, sampled rows are medians of three runs per prompt averaged across prompts, and stop-detection stays out of the timed loop.
154
 
155
+ The headline tables are still **measured on the 4-bit build of this set** (its earlier uniform weights; the AWQ swap measures within ±1%, paired) quoted because they are what the fork's two speculative paths do on this checkpoint. This build now also has its own EOS-clean measurement, further below. Greedy first:
 
 
 
 
156
 
157
+ | Configuration (greedy, 4-bit build) | tok/s | vs plain |
158
+ |---|---|---|
159
+ | plain | 37.6 | 1.00 |
160
+ | DSpark drafter (block 8) | 48.3 | 1.28× |
161
+ | MTP k=2 | 46.8 | 1.24× |
162
+ | **MTP k=4 + `min_draft_p` 0.6** | **52.8** | **1.40×** |
163
 
164
+ And under the shipped sampling defaultstemperature 1.0, top_p 0.95, top_k 20, what a chat deployment actually runs using truncated rejection sampling, which passes target and draft distributions through the same temperature/top-p/top-k chain and so **preserves the requested output distribution exactly** (total variation 0.0014 against a synthetic oracle; 160/160 token match in the greedy limit):
165
 
166
+ | Configuration (temp 1.0 · top_p 0.95 · top_k 20, 4-bit build) | 240 tok | vs plain | 1024 tok | vs plain |
167
+ |---|---|---|---|---|
168
+ | plain, sampler on | 37.16 | 1.00 | 36.91 | 1.00 |
169
+ | DSpark drafter | 41.94 | 1.13× | 39.95 | 1.08× |
170
+ | MTP k=2 | 47.74 | 1.28× | 44.10 | 1.19× |
171
+ | **MTP k=4 + `min_draft_p` 0.6** | **48.05** | **1.29×** | **45.13** | **1.22×** |
172
 
173
+ The gated k=4 row is the 4-bit recommendation (`mtp_num_draft_tokens=4, min_draft_p=0.6` via the Python API, plus `mtp_spec_temp=1.0, mtp_spec_top_k=20, mtp_spec_top_p=0.95` under the sampling defaults the gate is what makes k=4 win; ungated deeper k measured below k=2). The small-M quantized-GEMM kernel behind the 4-bit verification window fires only on **4-bit, group-64** weights ([ml-explore/mlx#4265](https://github.com/ml-explore/mlx/issues/4265); kill switch `MLXLM_NO_FAST_QMM=1`), so it never engages here which is priced into the next paragraph.
174
 
175
+ **This build's own numbers, EOS-clean, same four-prompt protocol, greedy:** plain **21.8 tok/s** · MTP k=2 **31.1 (1.42×)** · DSpark at its 8-bit optimum, block 4, **33.6 (1.54×)**. The multiples are larger than the 4-bit build's because plain decode is slower here while drafting costs about the same — but mind the absolute ceiling: 8-bit with DSpark (33.6) is still slower than the 4-bit build running plain (37.6). Take this build for fidelity and speculate on it by all means; do not take it for speed.
176
 
177
+ ## DSpark: an external drafter
178
+
179
+ DSpark is the fork's other speculative path, and on this build it is the faster of the two — 33.6 tok/s (1.54×) at block 4, EOS-clean, against MTP k=2's 31.1. Unlike MTP it is **not** self-speculation: it drives an external drafter, [`RadixArk/Qwen3.8-27B-DSpark`](https://huggingface.co/RadixArk/Qwen3.8-27B-DSpark) — 1.36B parameters, trained against this checkpoint, published by a third party. The drafter carries no embedding and no output head of its own; it borrows the target's, and drafts a whole block of positions in one forward pass instead of chaining them.
180
 
181
  Two things to settle first:
182
 
 
202
  add_generation_prompt=True,
203
  ))
204
 
205
+ for token, _accepted in dspark_generate_step(prompt, model, draft, max_tokens=512,
206
+ block_size=4):
207
  print(tokenizer.decode([token.item()]), end="", flush=True)
208
  ```
209
 
210
+ **On this build, run block 4, not the block-8 default.** The default block size of 8 is the 4-bit operating point, where the small-M kernel makes wide verification windows nearly free. That kernel never engages at 8 bits, so width costs what it costs here and the measured optimum shrinks to block 4 36.3 tok/s against 35.1 at block 8 on the English-prompt block sweep, 33.6 on the full four-prompt set. The call above pins it. An earlier revision of this card warned that a pre-kernel 8-bit measurement ran DSpark below plain decode and that there might be "nothing here to collect" the EOS-clean re-measurement retires that warning: 1.54× is real. Just weigh the download against the arithmetic above before collecting it.
211
 
212
+ ### Korean: the recommendation reversed
213
 
214
+ Until this revision the card said **"run Korean workloads plain,"** on the strength of 4-bit Korean cells that read below plain decode. **That was an artifact of the retracted protocol** — short answers plus a harness that measured past their end. Under the corrected protocol the gated MTP configuration on the 4-bit build is **faster on Korean than plain**: +34% greedy, +27–31% under the shipped sampling defaults. And on this build's own EOS-clean measurement both paths clear plain on Korean as well — MTP k=2 23.9 and DSpark 24.6 against 21.8 plain, greedy.
215
 
216
+ The honest exception sits on the 4-bit tier: **DSpark under the sampling defaults stays at or below plain on Korean there** (well below at 240 tokens, break-even at best by 1024). The reversal is about the gated MTP path and about this build; it is not a blanket license for every path on every tier.
217
 
218
  ### Measurement protocol, and why single-prompt numbers lie
219
 
220
+ Every figure above is the **average over the fixed four-prompt set**, never a single run. Speculative decoding pays off in proportion to how predictable the continuation is, so a benchmark built from one favorable prompt **overstates the gain you will see in real traffic** by a wide margin treat any single-prompt speculative figure, including mine, as an upper bound.
221
 
222
+ The correction at the top of this section adds a second rule, learned at the cost of a retraction: **a speculative benchmark that does not stop at end-of-sequence is measuring luck** — what the model happens to do after its answer ends. The retracted 62.21 headline, and the 91.5 tok/s math cell behind it, were mostly that luck. The per-cell records behind the restated tables ship in the [campaign repository](https://github.com/avlp12/qwen38_alis_mlx).
223
 
224
  ## KV cache quantization
225
 
 
287
  - **Vision quality is unquantified.** The tower is preserved bit-exact and verified to work on a shapes probe. That is a functional check, not a VQA evaluation. No image benchmark was run at any bit-width.
288
  - **Shape-dependent numerics.** The hybrid stack takes different kernel paths depending on the shape of the input it receives — the batched scoring pass behind the perplexity table is not the same shape as incremental decode, and near-tie positions can resolve differently between those paths. Perplexity is far less sensitive to this than an argmax metric, but do not expect bit-identical numbers from a different harness.
289
  - **Long-form generated reasoning is unprobed.** Perplexity scores 2048-token windows of existing text; it does not exercise long generated reasoning chains, where low-bit damage tends to concentrate. Spot-check your own thinking-mode prompts.
290
+ - **Speculative-decoding gains are workload-dependent.** The speculative tables were restated on 2026-08-16 under an EOS-cut protocol after the earlier harness was found to measure past end-of-sequence; the correction in the MTP section is the full account.
291
 
292
  ## License and attribution
293
 
assets/builds.png CHANGED

Git LFS Details

  • SHA256: 6f372f51817899c6dd7102c11a8abab817d8369a29c931c05cebd96713f8b609
  • Pointer size: 131 Bytes
  • Size of remote file: 177 kB

Git LFS Details

  • SHA256: 71b32ce37c0742515f66a2781471d23e584d4a0611438551e3a0f5306329c184
  • Pointer size: 131 Bytes
  • Size of remote file: 178 kB
assets/builds.svg CHANGED
assets/make_builds_png.py CHANGED
@@ -17,7 +17,7 @@ import matplotlib
17
 
18
  matplotlib.use("Agg")
19
  import matplotlib.pyplot as plt
20
- from matplotlib.patches import Circle, FancyBboxPatch, Polygon
21
 
22
  plt.rcParams["font.family"] = ["Helvetica Neue", "Helvetica", "Arial", "DejaVu Sans"]
23
 
@@ -93,46 +93,28 @@ ax.plot([179.1, 275.0, 372.4, 736.1], [407.5, 478.9, 517.4, 581.8],
93
  line(100, 581.8, 800, 581.8, CB, 1.4, 0.8, dash=(6, 6), z=2)
94
  txt(108, 573.8, "bf16 baseline — 12.6 tok/s", 12.5, CB)
95
 
96
- # ============ speculative spread on the 4-bit build (all at 15.2 GB) ============
97
- # solid marker = 4-prompt average · caps = 3-English-prompt high and Korean low
98
-
99
- # upper spread: DSpark 4-prompt 62.21 -> 3-prompt 71.86
100
- line(179.1, 218, 179.1, 167, CD, 1.3, 0.55, z=3)
101
- line(171.1, 167, 187.1, 167, CD, 2.4, 1.0, z=4)
102
- txt(200, 171, "71.9 — 3 English prompts", 12.5, CD)
103
-
104
- # MTP 3-prompt cap 55.71
105
- line(172.1, 280, 186.1, 280, CM, 2.2, 1.0, z=4)
106
- txt(200, 284, "55.7 — 3 English prompts", 12.5, CM)
107
-
108
- # lower spread: plain 4-bit -> Korean-only lows
109
- line(179.1, 415.5, 179.1, 436.9, GREY, 1.3, 0.35, z=3)
110
- line(172.1, 429.9, 186.1, 429.9, CM, 2.2, 1.0, z=4)
111
- line(171.1, 436.9, 187.1, 436.9, CD, 2.4, 1.0, z=4)
112
- txt(166, 428, "ko 34.3", 12.5, CM, ha="right")
113
- txt(166, 446, "ko 33.3", 12.5, CD, ha="right")
114
-
115
- # ============ lift connectors: plain 37.6 -> MTP 50.36 -> DSpark 62.21 ============
116
- line(179.1, 399.5, 179.1, 329.5, CM, 1.6, 1.0, dash=(5, 5), z=3)
117
- txt(166, 370, "+34%", 14, CM, "bold", ha="right")
118
-
119
- line(179.1, 306.5, 179.1, 250.5, CD, 1.6, 1.0, dash=(5, 5), z=3)
120
- txt(166, 265, "+65%", 14, CD, "bold", ha="right")
121
-
122
- # ============ 4-bit + DSpark drafter (triangle, ringed) ============
123
- ax.add_patch(Circle((179.1, 234.5), 15.5, fill=False, edgecolor=CD,
124
- alpha=0.45, linewidth=1.3 * PT, zorder=5))
125
- ax.add_patch(Polygon([(179.1, 223.5), (191.1, 245.5), (167.1, 245.5)],
126
- closed=True, facecolor=CD, linewidth=0, zorder=6))
127
- txt(200, 229, "4-bit + DSpark drafter", 15.5, CD, "bold")
128
- txt(200, 248, "62.21 tok/s · 1.65× vs plain 4-bit", 13.5, CD)
129
-
130
- # ============ 4-bit + MTP k=2 (diamond) ============
131
- ax.add_patch(Polygon([(179.1, 306.5), (190.1, 317.5), (179.1, 328.5),
132
- (168.1, 317.5)],
133
  closed=True, facecolor=CM, linewidth=0, zorder=6))
134
- txt(200, 312, "4-bit + MTP k=2", 15.5, CM, "bold")
135
- txt(200, 331, "50.36 tok/s · 1.34× vs plain 4-bit", 13.5, CM)
 
 
 
 
 
 
 
 
 
136
 
137
  # ============ 4-bit ============
138
  ax.plot([179.1], [407.5], "o", markersize=16 * PT, color=C4, zorder=6)
@@ -158,14 +140,15 @@ txt(790, 512, "bf16 reference", 15.5, CB, "bold", ha="right")
158
  txt(790, 531, "51.8 GB · 12.6 tok/s", 13.5, CB, ha="right")
159
  txt(790, 549, "ko PPL 6.095 — reference", 12.5, CB, ha="right")
160
 
161
- # ============ in-plot callout: the Korean caveat ============
162
  ax.add_patch(FancyBboxPatch((459.5, 190.5), 0.1, 15,
163
  boxstyle="round,pad=1.5,rounding_size=1.5",
164
  linewidth=0, facecolor=CD, zorder=4))
165
- txt(470, 200, "Speculation is prompt-dependent", 15, INK, "bold")
166
- txt(470, 222, "Plotted values are 4-prompt averages. On the three", 13, MUT)
167
- txt(470, 240, "English prompts DSpark reads 71.9 tok/s; on Korean", 13, MUT)
168
- txt(470, 260, "alone 33.3 below plain 37.6. Run Korean plain.", 13, MUT)
 
169
 
170
  # ============ side facts ============
171
  ax.add_patch(FancyBboxPatch((836, 122), 330, 468,
@@ -194,7 +177,8 @@ txt(858, 438, "with --kv-bits 4 — top-1 unchanged.", 13, MUT)
194
  txt(858, 498, "Speculation is opt-in", 15, INK, "bold")
195
  txt(858, 520, "Both paths need the avlp12/mlx-lm fork.", 13, MUT)
196
  txt(858, 538, "The MTP head ships inside this build; the", 13, MUT)
197
- txt(858, 556, "DSpark drafter is a separate 0.76 GB download.", 13, MUT)
 
198
 
199
  # ============ footnotes ============
200
  for fy, s in [
@@ -204,15 +188,16 @@ for fy, s in [
204
  (701, "wiki 35,179, code CPython 32,295 tokens. 8-bit ≈ bf16 on every "
205
  "slice, 6-bit ≈ bf16 on Korean; 4-bit AWQ: +1.2% en, +2.7% ko, "
206
  "+7.7% code. No standard benchmarks were run."),
207
- (720, "Speculative decoding: greedy, 240 tokens per prompt, timed after an "
208
- "8-token warmup, in one process on the 4-bit repo's earlier uniform "
209
- "weights (the AWQ swap measures within ±1%,"),
210
- (739, "EOS-cut paired). Both lossless each reproduced plain greedy "
211
- "decoding token for token through 80 tokens. MTP is the vendor "
212
- "multi-token head at k=2; DSpark drives an external 1.36B drafter."),
213
- (758, "Plotted values are the 4-prompt average single prompts overstate "
214
- "speculation: DSpark reads 75.1 tok/s on code and 91.5 on math "
215
- "against that 62.2 average.")]:
 
216
  txt(40, fy, s, 12, MUT)
217
 
218
  out = "/Users/gesicht/qwen38/cards/assets/builds.png"
 
17
 
18
  matplotlib.use("Agg")
19
  import matplotlib.pyplot as plt
20
+ from matplotlib.patches import FancyBboxPatch, Polygon
21
 
22
  plt.rcParams["font.family"] = ["Helvetica Neue", "Helvetica", "Arial", "DejaVu Sans"]
23
 
 
93
  line(100, 581.8, 800, 581.8, CB, 1.4, 0.8, dash=(6, 6), z=2)
94
  txt(108, 573.8, "bf16 baseline — 12.6 tok/s", 12.5, CB)
95
 
96
+ # ============ speculative ladder on the 4-bit build (all at 15.2 GB) ============
97
+ # EOS-cut protocol, restated 2026-08-16 · greedy 4-prompt averages
98
+
99
+ # lift connectors: plain 37.6 -> MTP k=2 46.8 -> gated MTP k=4 52.8
100
+ line(179.1, 399.5, 179.1, 355.5, CM, 1.6, 1.0, dash=(5, 5), z=3)
101
+ line(179.1, 330.4, 179.1, 312.4, CM, 1.6, 1.0, dash=(5, 5), z=3)
102
+
103
+ # ============ 4-bit + gated MTP k=4 (filled diamond, top) ============
104
+ ax.add_patch(Polygon([(179.1, 289.4), (190.1, 300.4), (179.1, 311.4),
105
+ (168.1, 300.4)],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  closed=True, facecolor=CM, linewidth=0, zorder=6))
107
+ txt(166, 304, "+40%", 14, CM, "bold", ha="right")
108
+ txt(200, 296, "4-bit + MTP k=4, p-gated", 15.5, CM, "bold")
109
+ txt(200, 315, "52.8 tok/s · 1.40× vs plain 4-bit (greedy)", 13.5, CM)
110
+
111
+ # ============ 4-bit + MTP k=2 (open diamond) ============
112
+ ax.add_patch(Polygon([(179.1, 331.4), (190.1, 342.4), (179.1, 353.4),
113
+ (168.1, 342.4)],
114
+ closed=True, fill=False, edgecolor=CM,
115
+ linewidth=2 * PT, zorder=6))
116
+ txt(166, 346, "+24%", 14, CM, "bold", ha="right")
117
+ txt(200, 347, "MTP k=2 — 46.8 tok/s · 1.24×", 13.5, CM)
118
 
119
  # ============ 4-bit ============
120
  ax.plot([179.1], [407.5], "o", markersize=16 * PT, color=C4, zorder=6)
 
140
  txt(790, 531, "51.8 GB · 12.6 tok/s", 13.5, CB, ha="right")
141
  txt(790, 549, "ko PPL 6.095 — reference", 12.5, CB, ha="right")
142
 
143
+ # ============ in-plot callout: the restatement ============
144
  ax.add_patch(FancyBboxPatch((459.5, 190.5), 0.1, 15,
145
  boxstyle="round,pad=1.5,rounding_size=1.5",
146
  linewidth=0, facecolor=CD, zorder=4))
147
+ txt(470, 200, "Restated 2026-08-16 (EOS-cut)", 15, INK, "bold")
148
+ txt(470, 222, "Speculative points are EOS-cut 4-prompt averages,", 13, MUT)
149
+ txt(470, 240, "greedy. The old 62.2 / 50.4 headline decoded past", 13, MUT)
150
+ txt(470, 258, "EOS and is retracted. DSpark restates to 48.3", 13, MUT)
151
+ txt(470, 276, "(1.28×); Korean gains +27–31% with the gated path.", 13, MUT)
152
 
153
  # ============ side facts ============
154
  ax.add_patch(FancyBboxPatch((836, 122), 330, 468,
 
177
  txt(858, 498, "Speculation is opt-in", 15, INK, "bold")
178
  txt(858, 520, "Both paths need the avlp12/mlx-lm fork.", 13, MUT)
179
  txt(858, 538, "The MTP head ships inside this build; the", 13, MUT)
180
+ txt(858, 556, "DSpark drafter is a separate 0.76 GB", 13, MUT)
181
+ txt(858, 574, "download — greedy 48.3 tok/s (1.28×).", 13, MUT)
182
 
183
  # ============ footnotes ============
184
  for fy, s in [
 
188
  (701, "wiki 35,179, code CPython 32,295 tokens. 8-bit ≈ bf16 on every "
189
  "slice, 6-bit ≈ bf16 on Korean; 4-bit AWQ: +1.2% en, +2.7% ko, "
190
  "+7.7% code. No standard benchmarks were run."),
191
+ (720, "Speculative decoding (restated 2026-08-16): EOS-cut, long-form four "
192
+ "prompts (chat/code/math/Korean), medians of 3 for sampled rows, one "
193
+ "process, on the 4-bit repo's earlier uniform weights"),
194
+ (739, "(the AWQ swap measures within ±1%, paired). Greedy: plain 37.6 · "
195
+ "MTP k=2 46.8 (1.24×) · DSpark block-8 48.3 (1.28×) · MTP k=4 + "
196
+ "min_draft_p 0.6 = 52.8 (1.40×). The old 62.2/50.4 figures"),
197
+ (758, "decoded past EOS; retracted. Real-world sampling (temp 1.0 · top-p "
198
+ "0.95 · top-k 20): gated MTP 1.29× @240 tok, 1.22× @1024, "
199
+ "distribution-preserving; Korean +27–31%; plain pays −1.2% for the "
200
+ "sampler.")]:
201
  txt(40, fy, s, 12, MUT)
202
 
203
  out = "/Users/gesicht/qwen38/cards/assets/builds.png"