Repository navigation
[Engine] The Mac (Metal) engine ignores a request's sampling (temperature, top_p, top_k, min_p, seed, penalties) and drops logit_bias #3006
Description
Area
Engine
Desktop app, Terminal or source?
Source (./setup.sh on a Mac builds the Metal engine)
What happened
On the Mac engine every request decodes greedily, whatever it asks for. temperature, top_p, top_k, min_p,
seed, repetition_penalty, frequency_penalty and presence_penalty reach the engine (they are on its GEN line)
but change nothing. Different seeds at temperature 1.0 give the same text, and it is the temperature-0 text.
logit_bias is dropped too: a token banned with -100 is still picked.
Three consequences:
- Sampling settings do nothing on a Mac.
- The model card's defaults from 0.1.45 (Strata serves Qwen3.8-Flash-Next with strange behavior - really bad results #2685) do not take effect on a Mac. The server now sends
temperature 0.7, top_p 0.8, top_k 20 and presence_penalty 1.5 (instruct) when the request and the config give
none. The Mac engine answers those requests greedily, so Mac users still get the greedy decoding that Strata serves Qwen3.8-Flash-Next with strange behavior - really bad results #2685 was
about. logit_biasis dropped on Metal.Verifier::set_logit_bias(src/core/verify.cpp) returns before it stores
the bias whenle_ < g_->n_layers. The Metal init returns beforele_is set to the layer count, sole_stays
-1 and every bias is treated as belonging to a later stage. The request gets no error.
Expected: the same behaviour as the PC engine. The settings act in the CUDA sampler's order, a fixed seed gives the
same text again, another seed gives another text, temperature 0 (or none, without the card's defaults) is greedy, and
a banned token is never picked.
The cause is that the Mac decode window always takes its own argmax (argmax_part / argmax_final in the window's
head), and Verifier::run's Metal branch returns that token. The host-side sampling that the CUDA path runs after
its window is not ported. Nothing in the log or the response says that the settings were ignored.
Reproduction
Re-checked on 0.2.0-beta (main 3fa1b52, built from source); first found on main 477ac13 plus #2895 (the 0.1.45 Mac build fix).
I used the official two-file Qwen3.8-Flash-Next IQ3_S (SC117's abliterated build), resident, as written by
./setup.sh. Start the server on a free port with the model's config, then send the same chat request with
different sampling:
python3 serve/server.py --engine strata --config strata-<model>.json --port 8197 &
ask() { # the same prompt, thinking off, 40 tokens; $1 = extra JSON fields
curl -s http://127.0.0.1:8197/v1/chat/completions -H 'Content-Type: application/json' -d '{
"model": "strata", "max_tokens": 40, "chat_template_kwargs": {"enable_thinking": false},
"messages": [{"role": "user", "content": "Write one sentence about the sea."}] '"$1"' }' |
python3 -c 'import json,sys; print(json.load(sys.stdin)["choices"][0]["message"]["content"])'
}
ask ', "temperature": 0, "presence_penalty": 0' # greedy
ask ', "temperature": 0' # the card adds presence_penalty 1.5 (#2940)
ask ', "temperature": 1.0, "seed": 1'
ask ', "temperature": 1.0, "seed": 2'
ask ', "temperature": 1.0, "seed": 3'
ask ', "temperature": 1.5, "top_k": 50, "top_p": 0.95, "min_p": 0.01, "seed": 7, "presence_penalty": 0'
ask '' # no sampling fields: the model card's defaults
ask ', "temperature": 0, "presence_penalty": 0, "repetition_penalty": 1.5, "frequency_penalty": 1.0'
ask ', "temperature": 0, "presence_penalty": 0, "logit_bias": {"760": -100}' # 760 = "The", the greedy first token
(760 is the first token of the greedy answer with this model's tokenizer; use the first token of your greedy answer.)
Every line prints the same sentence.
Measured
M5 Max (128 GB), macOS 26.7.1, Qwen3.8-Flash-Next GSQ-RCO IQ3_S, resident, --spec 2 with MTP,
--max-context 262144, 2026-10-18. These are the answers to the requests above, all in one server run:
| Request (besides the prompt) | Answer |
|---|---|
temperature 0, presence_penalty 0 (greedy) |
The vast, endless sea stretches to the horizon, its rhythmic waves whispering ancient secrets to the shore. |
temperature 0 (the card adds presence_penalty 1.5, #2940) |
the same |
temperature 1.0, seed 1 |
the same |
temperature 1.0, seed 2 |
the same |
temperature 1.0, seed 3 |
the same |
temperature 1.5, top_k 50, top_p 0.95, min_p 0.01, seed 7 |
the same |
| no sampling fields (the model card's defaults: temperature 0.7, top_p 0.8, top_k 20, presence_penalty 1.5) | the same |
temperature 0, repetition_penalty 1.5, frequency_penalty 1.0 |
the same |
temperature 0, logit_bias {"760": -100} (760 = "The", the greedy answer's first token) |
the same, starting with "The" |
The engine reported 0.1.45-metal in /v1/status. An earlier run that day, with nearly the same requests, gave
the same: every answer equal to the greedy one.
With a port of the CUDA sampler's chain to the Mac engine (on the host, over the window's logits in unified
memory; I will open it as a PR once #2895 is merged), the same requests give a different answer for every row but
the first. Seed 1 gives the same text each
time it is sent, seeds 2 and 3 give other texts, and the banned token is not picked ("Beneath the shimmering
surface, ...").
At the engine protocol, a test I wrote for that port (tools/metal_sampling_test.py, run against my Metal branch
without the port) fails 12 of 18 checks. Seeds 1 and 2 give the greedy text. 100 one-token draws at temperature 1.0, top_k 40,
top_p 0.95 all give the greedy token, which has probability 0.81 on those logits; 15 of the 100 picks differ from what
a reference sampler written from the CUDA kernels draws with the same seeds. Greedy with penalty_repeat 1.5 gives the same tokens as
without it. A token banned by logit_bias is still picked first.
Not tested
- The PC engines (CUDA, HIP, SYCL): not built here. The reference behaviour is read from their code
(src/kernels/cuda/sampler.cu,Verifier::run) and from the test's reference sampler written from it, not run. - Other Macs and chips, models that stream their experts from the SSD, other packs (Q2_0, IQ3_XXS, Unsloth's),
vision requests. The cause is in code that all of them share, so I expect the same result. - Thinking on. The requests above use thinking off; the card's thinking defaults (temperature 1.0, top_p 0.95,
top_k 20) should be ignored the same way.
Strata version
0.2.0-beta (main 3fa1b52, built from source). First seen on 0.1.45 (main 477ac13 + #2895).
Graphics card, memory and system
Apple M5 Max, 128 GB unified memory (Metal) · macOS 26.7.1
Reproduction status
Reproduced locally on 0.2.0-beta (main 3fa1b52): the script below prints the same sentence for 9 of 9 requests. A fix is in #3301.
Engine log
Not attached. Nothing in it mentions sampling: the engine prints no line about the settings it ignored.
Activity
Niko1221 commentedon Oct 19, 2026
Thanks for taking the time to report this.
We're resetting the tracker for 0.2.0-beta. There are more than 1,300 open issues and pull requests, too many for us to read properly, so we're closing everything that isn't being worked on right now. Closed here does not mean rejected.
To reopen it takes about two minutes:
/reopen. A check runs and either reopens the issue or lists what is still missing.The rules are in CONTRIBUTING.md.
oinani0721 commentedon Oct 19, 2026
Two of the "Not tested" rows, on another M5 Max (128 GB, macOS 27.0.1): Unsloth's pack with its experts streamed from the SSD, and thinking on. Same result: every answer is the greedy one. The build is older than yours,
main296b59d (0.1.44.1) from source, with #2503, #2631, #2819 and two local cache/memory patches in the engine; the 0.2.0-beta part at the end is reading only.Sampling sent, thinking on. Unsloth's Qwen3.8-Flash-Next UD-Q6_K_XL,
STRATA_METAL_STREAM=1,--mmap-experts,--spec 3with MTP. One prompt ("用一句话描述一下秋天。", one sentence about autumn), three times per endpoint, non-streaming,temperature 2.0, top_p 1.0, top_k 64:/v1/responseswithreasoning.effortlow,max_output_tokens300: the same reasoning and answer 3 of 3 times, byte for byte./v1/chat/completionswithreasoning_effortlow,max_tokens300: the same 3 of 3 times, and the same text as on/v1/responses.Agent requests. #2731's end reproduces on this server with a Codex-shaped
/v1/responsesrequest (one function tool, effortxhigh; request and numbers in #2731 (comment)). The reply is the same reasoning-only stub, 80 output tokens with the same ending, in 4 of 4 runs without sampling fields and in 10 of 10 with the card's thinking sampling sent (temperature 1.0, top_p 0.95, top_k 20). On a Mac, resending a request that went wrong gets the same reply back.v0.2.0-beta (3d1fb48), reading only:
Verifier::runstill returns the Metal window's own pick (src/core/verify.cpp:2691-2695) before its host-side sampling (2974), the window still picks withargmax_part/argmax_final(metal/src/engine/window.cpp:1097-1103), andstrata::kernels::sample_tokensis still a stub (metal/src/engine/stubs.cpp:317). So I expect the same on the beta, but have not run it.@oceanio, in case you want it reopened: the bot takes
/reopenonly from you, and wants[Engine]at the start of the title and a 0.2.0-beta retest under "Strata version".[-][Bug]: The Mac (Metal) engine ignores a request's sampling: temperature, top_p, top_k, min_p, seed and the penalties do nothing, logit_bias is dropped, and every answer is the greedy one[/-][+][Engine] The Mac (Metal) engine ignores a request's sampling (temperature, top_p, top_k, min_p, seed, penalties) and drops logit_bias[/+]oceanio commentedon Oct 20, 2026
Re-checked on 0.2.0-beta (
main3fa1b52, M5 Max 128 GB, macOS 26.7.1, Qwen3.8-Flash-Next IQ3_S resident): unchanged.serve/server.py: 9 of 9 requests print the same sentence ("The vast, endless seastretches to the horizon, ..."), including seeds 1-3 at temperature 1.0, top_k/top_p/min_p, the model card's
defaults, penalties and
logit_bias{"760": -100}.tools/metal_sampling_test.pyin the fix): 11 of 13 checks fail on 0.2.0-beta.Reproduction status: reproduced locally. A fix is in #3301 (draft until a CUDA/HIP/SYCL build of its one shared line).
/reopen
oceanio commentedon Oct 20, 2026
/reopen
github-actions commentedon Oct 20, 2026
Reopened, thank you. A maintainer will pick it up from here.
joseedson18jc commentedon Oct 21, 2026
Same on an M1 Max (64 GB, macOS 27.0), on 0.2.1.1 (
9ae93f7b, engine0.1.46-metal) and on 0.2.0-beta (3fa1b524,0.1.45.3-metal). Qwen3.8-Flash-Next Q2_0 (SC117's abliterated build) resident,--mmap-experts --expert-cache 64 --vision, throughserve/server.py.It does not depend on speculative decoding. With
--spec,--mtpand--lookup-chainremoved and--suffix-draft 0, 12 of 12 requests gave the same answer:temperature 1.0, top_p 0.95, top_k 20temperature 1.5, top_k 64With the Mac setup's flags (
--spec 2+ MTP +--lookup-chain 8) it is the same: 9 of 9, includingpresence_penalty 1.5.Two more consequences, besides the three in the report:
serve/server.pyretries a looping reply with temperature raised to at least 1.0 andpresence_penaltyto at least 1.5, but the engine decodes greedily either way. On a 40-question hard set (medium effort, 3000-token budget), one answer recomputed 2^100 by hand untilmax_tokens(12,000), the same withpresence_penalty0 and 1.5. 38 of 40 answers in both runs had exactly the token count of a temperature-0 run./propsreports sampling that is not applied. It showstemperature 1.0, top_p 0.95, top_k 20while every reply is the greedy one, so a client cannot tell.No
has no Metal implementation yetline appears in the engine log: the window's argmax (metal/kernels/sample.metal) is used andsample_tokens(stub inmetal/src/engine/stubs.cpp) is never reached.I can test #3301 on the M1 Max if that helps.
oceanio commentedon Oct 21, 2026
Thanks, this is very useful: a second chip (M1 Max) and a second model (Qwen Q2_0) with the same result, and the two extra consequences are right. The loop retry and
/propsboth assume the engine applies sampling, and on Metal it doesn't.Yes, please test #3301 on the M1 Max. It merges cleanly onto current main (0.2.1.4,
9de04cd4):With #3301 the CPU picks each token from the window's logits (unified memory), with the same chain and seeds as the CUDA engine. Your
askloop should then give different names attemperature 1.5, top_k 64, and the same name every time attemperature 0, or with the sameseed. Alogit_biasof -100 on the name's first token should also stop it from appearing. With no sampling fields the server's model-card defaults now apply, so/propsmatches what you get, and the loop retry's higher temperature and presence penalty actually take effect.It would help to know:
temperature 1.5, top_k 64now gives different answers, and 4xseed: 1gives the same one;