Skip to content

Faster prefill, tokenizer fix, draft-model speculation (1.3.0) - #9

Merged
jkyberneees merged 16 commits into
mainfrom
perf/next-pass
Nov 2, 2026
Merged

jkyberneees merged 16 commits into
mainfrom
perf/next-pass

Conversation

@jkyberneees

@jkyberneees jkyberneees commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Prefill about 1.45× faster: a 64×64 GEMM tile (about 3.5 TFLOPS, was 2.4) and a faster flash_prefill. Qwen3 0.6B 2,040 → 3,020 tok/s at 512 tokens, Mistral 7B 166 → 247. Decode is unchanged.
  • Tokenizer fix: canonically equivalent vocabulary entries (Gemma's ; and U+037E) collided, so a ; could get the wrong id, varying per process. Lookups now compare exact text.
  • Draft-model speculative decoding (--draft <model|auto>, app picker, server usage fields). Output is identical, it is off by default, and it is currently slower: Qwen3 8B goes 38.1 → 27.9 tok/s on a short prompt, because verifying 3–8 tokens costs about 3 decode steps and about 45% of drafts are accepted.
  • 2-token verification costs half (8B 78.6 → 40.7 ms). qmvx4 handles 3–8 tokens and is ALU-bound at about 3× a step.
  • Chat hover: message action buttons track the pointer themselves.
  • Docs: README, CHANGELOG and site use one measured set, with a corrected mlx-lm comparison.

Test plan

  • swift test: 153 tests pass locally with installed models
  • Greedy output on 10 models, short and long prompts, against 1.2.2: 18 of 20 identical. The other 2 (Gemma 2 and Gemma 4 E2B, long prompt) differ because the 1.2.2 run encoded 14 ; tokens with the wrong id
  • Same-prompt kernel check for Gemma 1.1, Gemma 3 1B and Gemma 4 E2B with no ;: identical to 1.2.2
  • --draft auto on Qwen3 8B matches the plain run on short and 1,890-token prompts, with the draft active
  • Token ids match transformers on Gemma 1.1, Gemma 3 1B, Gemma 4 E2B and Qwen3 0.6B; encodings stable across 6 runs on 6 models
  • Decode A/B against 1.2.2 on Qwen2.5 and Gemma 3 1B (3 pairs): level
  • Hover fix not exercised in the running app: no reproduction was possible without Accessibility permission

🤖 Generated with Claude Code

jkyberneees and others added 12 commits November 2, 2026 11:21
The first sweep ran cold (first touch of the loaded buffers, GPU clock ramp) and read
2-3x slower than the same matvec inside a decode step.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…dentical)

qgemm is now one template over the per-simdgroup accumulator grid: the 32x32
tile (2x2 accumulators) replaces the old qgemm, and a 64x64 tile (4x4
accumulators, 2 MMAs per fragment load instead of 1) runs long batches. Both
keep each output's chain (fp32 dequant, k ascending in 8-steps), so results
are bit-identical to the old kernel and to each other. Full unrolling is what
makes the 4x4 tile work (4x slower without it); padded rows and register
prefetch measured slower on M2 Pro and are not used. Linear.useGemm64 picks the
64x64 tile when it pads at most 1.2x more rows and yields >= 64 threadgroups.

flash_prefill stages K/V as half (exact: the cache is half) in two buffers
with 16-byte loads of the next block during the current one's MMAs (one
barrier per block), rescales O element-wise instead of by a diagonal MMA, and
schedules the latest query blocks first. Same key blocking and softmax order,
bit-identical output; ~1.9x faster at D=128, ~1.4-1.6x at D=256.

Tests: both tiles against the CPU reference (odd O, K % 64 != 0, T up to 517),
tiles agree bitwise across batch sizes, and prefill vs incremental decode for
a 600-token prompt (two batches, the second at offset 512).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Replaces qmvb4; maxVerifyTokens 4 -> 8. Correct (kernel tests pass), not yet faster.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ng-context verify bench

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… split attention)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A small model of the same family drafts greedy tokens (its own KV cache, catch-up and
k-1 GPU-sampled decode steps queued back to back, only the ids read back); the target
verifies them in one batched pass with the exact acceptance rule. Prompt lookup stays
as a free hybrid source. Load checks tokenizer compatibility and the memory of both
models; usage reports draft_tokens / accepted_draft_tokens.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…f-draft greedy identity

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…EADME

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Failed ❌

View logs ↗
6b19940 2026-11-02T16:01:12.333Z View logs ↗
  • Build: Failed ❌

View logs ↗
52da4cf 2026-11-02T12:54:45.967Z View logs ↗

jkyberneees and others added 4 commits November 2, 2026 16:58
String equality is canonical equivalence, so Gemma's ; and U+037E collided: which id a
semicolon got varied per process, and the losing id decoded to nothing. Token ids now match
transformers on the tested Gemma and Qwen3 tokenizers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The draft catches up on the prompt after the first token, a non-4-bit target is refused before
loading, a saved pick that does not fit is dropped instead of failing the load, tokenizer hashing
leaves the main actor, and draft statistics move from env-gated stderr into GenerationStats.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jkyberneees jkyberneees changed the title Faster prefill, draft-model speculative decoding, 2-token verify (1.3.0) Faster prefill, tokenizer fix, draft-model speculation (1.3.0) Nov 2, 2026
@jkyberneees
jkyberneees merged commit 0997f3f into main Nov 2, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant