Repository navigation
Conversation
c84c18c to
3a49b02
Compare
3a49b02 to
c612924
Compare
…d set_logit_bias line The Metal init returned before the line that sets le_ to n_layers, so set_logit_bias read the -1 as an earlier stage and never stored the bias. It was fixed in set_logit_bias, a line CUDA, HIP and SYCL compile too; now the Metal init sets le_ like the others, and set_logit_bias is upstream's again (the same change as pr/metal-sampling-solo, Niko1221#3301). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d set_logit_bias line The Metal init returned before the line that sets le_ to n_layers, so set_logit_bias read the -1 as an earlier stage and never stored the bias. It was fixed in set_logit_bias, a line CUDA, HIP and SYCL compile too; now the Metal init sets le_ like the others, and set_logit_bias is upstream's again (the same change as pr/metal-sampling-solo, Niko1221#3301). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
c612924 to
fb6edaa
Compare
|
Tested on an M1 Max (64 GB, macOS 27), ported onto The sampler works on the Mac:
Cost: sampled decode is ~5% slower than greedy (34.0 -> 32.4 tok/s), because fewer MTP drafts match the drawn token (62.6% -> 54.1% kept). Offsetting that cost on M1/M2: #4272. For a Q2_0 pack, the routed experts' tiles 8,1,2 (gate/up) + 4,2,4 (down) cut the expert block 17-24% at 1-3 tokens in
With both, sampled decode (33.3-33.8) is back at the old greedy-only engine's speed. #4272 is an opt-in env knob and is independent of this PR (one file, This build has served a production site since 26 Oct with no sampler issues. |
|
Thanks @joseedson18jc for the M1 Max run, the parity numbers and the time in production. The ~5% matches what I'd expect. With sampling, a draft is kept only when it equals the token actually drawn, so fewer MTP drafts survive than under greedy (your 62.6% -> 54.1%). #4272 is independent of this PR, as you say, so I'll leave it to its own review. |
…ver the window's head logits) The Mac engine picked the window's argmax for every request, so temperature, top_p, top_k, min_p, seed, the penalties and logit_bias changed nothing. Now Verifier::run's Metal branch does what the CUDA path does after its window: a sampled request, one with penalties or with a logit_bias, draws row t from the window's logits (unified memory, Window::logits_row) with Philox(seed, pos0 + t) and its own penalty row; drafts are kept while the sampled token equals them. Greedy without penalties or bias keeps the window's argmax. metal/src/engine/host_sampler.cpp: logit_bias -> penalties -> top_k (one NEON-filtered pass, the kernels' order: score descending, ids ascending) -> top_p -> min_p -> temperature -> draw, the kernels' float/double arithmetic. 9-46 us per row on 248,320 logits (M5 Max, host_sampler_parity --bench), so no GPU kernel. Verifier::set_logit_bias returned before storing the bias on Metal: the Metal init returned before the line that sets le_ to n_layers, and the -1 it left read as an earlier stage of a layer split. The Metal init now sets it as the others do, so the C++ that CUDA, HIP and SYCL compile is unchanged (unifdef -USTRATA_GPU_METAL gives v0.2.6's file). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-serve: reproducible seeds, picks draw for draw against a reference sampler on the dumped logits, penalties, logit_bias) and metal/tests/engine/host_sampler_parity.cpp metal_sampling_test.py: a reference sampler written from src/kernels/cuda/sampler.cu (Philox 4x32-10 at (seed, position), logit_bias, penalties, top_k, top_p, min_p, temperature) picks from each one-token request's first-window logits (STRATA_DUMP_FIRST_LOGITS); every engine pick must be its pick. 13 checks; on 0.2.0 without the engine change 11 fail (every seed gives the greedy text). --bench: decode tok/s greedy vs sampled. host_sampler_parity: the Mac host sampler (host_sampler.h) against a serial transcription of the CUDA kernels (sampler_parity.cpp's mirror_select/mirror_pick), 592 picks on 248,320-logit rows with ties, -inf, NaN, bans and penalties; --bench times a row. CPU only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…host pick's cost, greedy vs sampled decode) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fb6edaa to
f8fa790
Compare
Problem and result
On the Mac (Metal) engine every request decodes greedily.
temperature,top_p,top_k,min_p,seedand thepenalties reach the engine on its
GENline but change nothing, andlogit_biasis dropped. Since 0.1.45 the serveralso sends the model card's sampling when a request gives none, and on a Mac that has no effect either. Nothing in the
log or the response says so (#3006).
With this PR the Mac engine samples as the PC engine does: the same chain and order (logit_bias, penalties, top_k,
top_p, min_p, temperature), one Philox draw per token keyed by the seed and the position, so a fixed seed repeats its
text and another seed gives another. Temperature 0 (or none) stays greedy and gives the same tokens as before.
Related discussion
#3006 (the bug, reproduced again on 0.2.0-beta; see Validation). A focused fix, so no scope issue first. #4126 takes
on the same bug; the differences are in my comment there (#4126 (comment)).
Approach
Verifier::run's Metal branch returned the window's argmax. The CUDA path samples on the host after its window; thisPR does the same on the Mac, on the CPU, over the window's head logits, which are already in unified memory (no copy).
A request that is greedy and has no penalties and no
logit_biaskeeps the window's argmax, so its tokens are exactlythe old ones. The MTP drafts stay greedy and a draft is kept when the sampled token equals it, as on the PC. Each kept
token is then the target's own draw at its position (Philox keyed by the seed and the position), so the drafts change
the speed, not the distribution of the text.
metal/src/engine/host_sampler.cpp/.h: the CUDA sampler's chain with the kernels' float/double arithmetic andtie order. 9-46 µs per row on 248,320 logits (M5 Max), so no GPU kernel.
metal/src/engine/window.cpp: an accessor for row t of the last window's head logits.src/core/verify.cpp: the sampling after the Metal window, and one line in the Metal init,if (le_ < 0) le_ = g.n_layers;, as the CUDA init does further down. The Metal init returned before that line, sole_stayed -1,set_logit_biasread it as an earlier stage of a layer split, and the bias was never stored. Bothchanges are inside
#if defined(STRATA_GPU_METAL):unifdef -USTRATA_GPU_METALon thisverify.cppgives main'sfile byte for byte, so CUDA, HIP and SYCL compile the same code as before.
Validation
Re-run on v0.2.7 (2026-10-28). Rebased onto main
5e8a4cca(v0.2.7). No conflicts; the code is the same lines (one context line inVerifier::runmoved: v0.2.7 added a guard above it). At the new tipf8fa7906, same Mac and model:unifdef -USTRATA_GPU_METAL src/core/verify.cppis identical to main's. The other files outsidemetal/aredocs/MAC.md and the test script.
host_sampler_parity: 592 picks, all equal the reference.tools/metal_sampling_test.py: all 13 checks pass, among themlogit_biasand the same seed giving the same tokensin a second engine process.
15,099 prompt tokens, among them a shared 1.6K-token prefix, a follow-up and a rewind. 8 of 8 give identical text
(the 9th carries a per-run tag). Decode on the 4 short prompts is 92.4-96.1 tok/s on main and 92.2-95.5 here.
The rest of this section is the first run, on
3fa1b524.Environment: M5 Max 128 GB, macOS 26.7.1, Command Line Tools 27.0 (Apple clang 21). Qwen3.8-Flash-Next GSQ-RCO IQ3_S
(SC117's abliterated build, the official two-file layout), resident,
--spec 2with MTP,--mmap-experts,--max-context 262144. Base =main3fa1b52 (0.2.0-beta + 7 commits), built from the same tree.Rebased since onto
maine88ee39 (2026-10-20; its 5 new commits change no engine, Metal or server file): the3 commits build again with all Metal targets,
host_sampler_paritypasses again; the runs below are on 3fa1b52.Passed:
ninja -C build-metal(all Metal targets), at each of the 3 commits: 0 errors, no new warnings.build-metal/host_sampler_parity(CPU): 592 picks, all equal a serial transcription of the CUDA kernels(
sampler_parity.cpp's mirror); Philox matches 4,096 draws.python3 tools/metal_sampling_test.py --exe build-metal/strata --config <cfg>: 13 of 13 pass. On the base build thesame script fails 11 of 13 (every seed gives the greedy text; penalties and
logit_biaschange nothing). 208one-token requests drew exactly the token a reference sampler (written in the test from
sampler.cu) draws from thesame dumped logits.
serve/server.py(9 requests, the curl script in the issue): base, 9 of 9 print thegreedy sentence; this PR, the greedy request prints the same sentence as the base, and the other 8 (seeds 1-3 at
temperature 1.0, top_k/top_p/min_p, the card's defaults, penalties,
logit_bias760:-100) each print anothersentence; the banned first token is not picked.
the base build.
Failed: none observed.
Not run:
main,--batchon the Metal backend is accepted at boot and exits the engine with code 86 on the first request that runs two sequences together #2715).STRATA_SPEC_COUPLED,STRATA_SPEC_PROB,STRATA_SPEC_GUMBEL(the Mac draftsgreedily).
Limits
(4 short prompts, 256 tokens, MTP;
--bench), because fewer drafts are kept (0.70 against 0.72) and the textsdiffer. The pick itself costs under 0.05 ms a token.
temperature: 0andpresence_penalty: 0(documented in docs/MAC.md).🤖 Generated with Claude Code