Qwen3.8 Flash-Next · mlx-serve 26.10.1 · Mac Studio M4 Max 128 GB · 2026-10-17 to 2026-10-18
We ran several agent sessions at once against one local server: the Dankpaws Swift 1.5 Qwen3.8 Flash-Next MLX 4.7 bpw model on mlx-serve 26.10.1, on the test machine (Mac Studio M4 Max 128 GB), on 2026-10-17 and 2026-10-18. Two sessions work and stay within the pass rule, but each turn takes about twice as long, so two tasks run together finish no sooner than the same tasks one after the other.
Maximum that passes
2 sessions
Two sessions pass the rule with a warning: worst slowdown 1.76× at 32K and 1.94× at 128K. Three and four sessions fail.
Each session
~2× slower
When two sessions share the server, each turn takes about twice as long as it does alone.
Time saved
0 s saved
Two real Claude Code sessions together finished in 147.6 s on average; the same two tasks one after the other took 137.6 s. Together is 7% later.
Each session replays a coding agent’s conversation at a fixed context size. It starts cold: the server reads the whole prompt once (prefill, reading the prompt before the first token). Then come warm turns that add a tool result to a prompt the server already holds in its prompt cache and ask for a 512-token reply (decode, generating the reply token by token).
The turn time is the time from sending a tool result to receiving the full reply. The slowdown is a session’s turn time divided by the same session alone: 7.9 s at 32K, 8.6 s at 128K and 11.1 s at 256K.
A test configuration is a number of sessions × a context size × with or without short side requests: one-second permission checks, like an agent asking “may I run this?”, sent while the sessions work. Each configuration ran 3 times unless noted.
Pass rule: worst slowdown ≤ 1.5× passes; 1.5–2.0× passes with a warning; above 2.0× fails. The slowest session decides. A configuration also fails if the server refuses a request or a session misses more recall checks (a fact planted in the prompt that the reply must find) than its rule allows: at least 11 of every 13 answered checks (85%) per session at 256K; none missed at 32K and 128K (a reply cut off at its length limit does not count); none missed with the 8-bit KV cache. Recall counts only answered requests.
Dashed lines: at or below 1.5× passes; between 1.5× and 2.0× is a warning (passes); above 2.0× fails.
Worst slowdown per configuration, with short side requests running. One session at 128K is the session alone. Without side requests: 2 sessions 1.75× at 32K and 1.78× at 128K; 4 sessions 2.84× and 3.66×.
The server already batches the sessions’ generation: it writes one token for each session in the same step. On this model that step gets slower as sessions are added, so the total grows little. Going from 1 to 4 sessions, the best total grows at most 1.44× the speed of one session alone at 32K and 1.26× at 128K, while each session slows down by more.
Generation speed per session, tokens per second (average over the sessions in the run).
| Sessions | 32K per session | 32K total | 128K per session | 128K total |
|---|---|---|---|---|
| 1 | 81.1 | 81.1 | 77.3 | 77.3 |
| 2 | 57.5 | 115.1 | 48.7 | 97.5 |
| 3 | 35.8 | 107.5 | 32.0 | 96.1 |
| 4 | 29.3 | 117.1 | 19.4 | 77.6 |
In the 2-session runs the first session started before the second and ran some turns alone, which lifts the 2-session average. An independent benchmark of the same model agrees from 128K up: about 1.4× total throughput with short prompts, but no gain over one session alone at long contexts.
A side request takes about a second when the server is free. When it arrives while another session is reading a long prompt, it waits for that read to finish: a cold 128K prompt takes about 166 s to read and a 256K one about 351 s (one session alone, median). At 32K the slowest side requests (95th percentile) took 22.0 s–41.1 s; at 128K and 256K, 168.1 s–358.6 s.
Side-request response time, seconds. The dashed line marks 60 s. “limit 1” runs the two sessions with --max-concurrent 1: the wait is the same, so the flag does not cause it.
| Configuration | p50 | p95 | Over 60 s |
|---|---|---|---|
| 1 session, 32K | 0.9 s | 22.0 s | 0 |
| 2 sessions, 32K | 1.0 s | 33.8 s | 0 |
| 2 (limit 1), 32K | 0.9 s | 25.3 s | 0 |
| 3 sessions, 32K | 1.8 s | 29.2 s | 1 |
| 4 sessions, 32K | 1.3 s | 41.1 s | 1 |
| 2 sessions, 128K | 1.0 s | 322.6 s | 6 |
| 2 (limit 1), 128K | 1.0 s | 169.7 s | 8 |
| 3 sessions, 128K | 1.1 s | 168.1 s | 9 |
| 4 sessions, 128K | 1.4 s | 351.3 s | 9 |
| 256K + 32K | 0.9 s | 353.6 s | 4 |
| 2×256K, 4 GB | 1.5 s | 355.9 s | 14 |
| 2×256K, 8-bit | 1.0 s | 351.4 s | 8 |
| 2×256K, 4GB+8bit | 1.7 s | 358.6 s | 11 |
Upstream, PR #884 lets small requests in between prefill chunks, but it excludes this model’s architecture and is not in a release yet.
--max-concurrent matter?--max-concurrent sets how many requests the server runs at once. On mlx-serve 26.10.1 the default is 1, yet the boot log already says Concurrency: --max-concurrent=1, batched decode on: two sessions share generation either way. The flag is not a queue, and setting it to 2 changes nothing measurable. At 128K the side-request p95 is higher with the flag at 2 because of a single very slow request; the count of side requests over 60 s is close in both runs.
| Two sessions + side requests | 32K, --max-concurrent 1 | 32K, --max-concurrent 2 | 128K, --max-concurrent 1 | 128K, --max-concurrent 2 |
|---|---|---|---|---|
| Worst slowdown | 1.76× | 1.76× | 1.83× | 1.94× |
| Generation per session, tokens/s | 61.3 | 57.5 | 50.8 | 48.7 |
| Side-request p95 | 25.3 s | 33.8 s | 169.7 s | 322.6 s |
| Side requests over 60 s | 0 | 0 | 8 | 6 |
| Peak memory held by the server | 100.1 GB | 99.9 GB | 113.5 GB | 113.5 GB |
Two 256K sessions fit only when the prompt cache kept in RAM is cut from 16 GB to 4 GB. That configuration passes with a warning: worst slowdown 1.68× against the 11.1 s reference, or 1.81× against the 10.3 s we measured for one 256K session alone over 3 runs. With the 4 GB cache, warm turns lose about a second restoring the cache from the SSD.
Recall rule at 256K: a session must get at least 11 of every 13 answered checks (85%) right, so it may miss up to 2 of 13, the same as one 256K session alone. With the 8-bit KV cache it must miss none. The table shows each session’s count.
Dashed lines: at or below 1.5× passes; between 1.5× and 2.0× is a warning (passes); above 2.0× fails. † the server refused a request; ‡ a session missed more recall checks than the rule allows. In the 256K + 32K configuration the slowest session is the 32K one.
| Sessions | Prompt cache in RAM | KV cache | Peak memory | Refused / HTTP errors (client) | Recall per session | Worst slowdown | Result |
|---|---|---|---|---|---|---|---|
| one 256K + one 32K | 16 GB | full | 115.0 GB | 1 | session 1: 10/12 (miss) session 2: 13/13 | 1.96× | fail |
| two 256K | 4 GB | full | 113.3 GB | 0 | session 1: 11/13 session 2: 13/13 | 1.68× | pass with a warning |
| two 256K | 16 GB | 8-bit | 113.7 GB | 1 | session 1: 13/13 session 2: 12/12 | 1.56× | fail |
| two 256K | 4 GB | 8-bit | 107.7 GB | 0 | session 1: 12/13 (miss) session 2: 13/13 | 1.50× | fail |
Refused / HTTP errors (client): requests the client saw fail. The server log can count more refusals than the client sees, so the two numbers can differ.
Two real Claude Code 2.1.292 sessions (claude -p) shared the everyday server setup (128K context, default --max-concurrent 1), each on a fixed coding task with the same model as both main and fast model. Every task was done, with no errors and no refusals. Each session took 1.84×–2.36× its time alone, above the 1.5× rule.
Wall time per task, seconds. The “alone” control has 2 repetitions per task and “together” has 3.
Functional pass, time fail. The pair finished in 147.6 s on average; the two tasks one after the other take 137.6 s. Running them together finishes 7% later.
--prefill-decode-share 0.3 and 0.5--prefill-decode-share makes generation keep a share of the time while another request reads its prompt. We tried it at 128K to clear the side-request wait. It did not: as many side requests waited over 60 s as without it, and the cold first token took about twice as long. Not adopted.
| Setting (two sessions + side requests, 128K) | Worst slowdown | Side requests over 60 s | Side-request p95 | Cold first token (median) |
|---|---|---|---|---|
off (default), --max-concurrent 2 | 1.94× | 6 | 322.6 s | 168 s |
off (default), --max-concurrent 1 | 1.83× | 8 | 169.7 s | 169 s |
--prefill-decode-share 0.3 | 2.02× | 8 | 176.4 s | 319 s |
--prefill-decode-share 0.5 | 1.92× | 8 | 175.4 s | 327 s |
--max-concurrent 1; the flag is not a queue.--max-concurrent nor --prefill-decode-share removes the wait.256k profile for two long sessions.| Link | Type | State | How it relates |
|---|---|---|---|
| PR #568 | PR | merged, in 26.10.1 | Adds --prefill-decode-share: generation keeps a share of the time while another request reads its prompt; the waiting request gets its first token later by the same amount. Tested here at 0.3 and 0.5. |
| PR #884 | PR | merged 2026-10-17, not in a release | Lets small requests in at prefill chunk boundaries, which is the fix for side requests stuck behind a long prompt. It excludes the qwen4_exp architecture (Flash-Next). |
| issue #528 | issue | closed by PR #621 | Requests refused after the prompt cache was restored from the SSD: the restored part was billed again at admission. The closest match to the refusals seen here. |
| PR #621 | PR | merged, in 26.10.1 | The fix for #528: a restore from the SSD is credited, so a warm turn is not billed as a cold one. It is in the version tested here, so the refusals below are a different or remaining case. |
| issue #567 | issue | closed | Batched generation is not batch-invariant: a request’s output can change with the request it shares a batch with. |
| independent benchmark | external benchmark | external | Same model on mlx-serve: about 1.4× total throughput with short prompts, and less than one session alone from 32K up. |
We have not opened an issue about the admission refusals below, and their cause is not confirmed.
Some requests were refused before the server started reading them: 4 sessions + side requests, 128K; 256K + 32K sessions, 16 GB prompt cache; 2 × 256K, 8-bit KV cache. One of the server’s messages, verbatim:
prompt 125549 tokens needs ~6266MB at prefill chunk 512 (the narrowest width tried), ~5968MB available + ~13863MB the hot cache can give back (~17420MB resident, ~3556MB of it pinned by this request's own prefix) — refused before prefillRead literally, the message lists more memory available plus memory the prompt cache can give back than the request needs, even after leaving out the part pinned by the request’s own prefix. It looks like an admission accounting problem in this server version, similar to issue #528, whose fix is already in this version. Cause not confirmed.
Server commands, from the repository root ($MODEL is the model directory):
tools/scripts/serve-flashnext-daily-driver.sh # everyday setup: 128K, 16 GB prompt cache
tools/scripts/serve-flashnext-daily-driver.sh 256k # two long sessions: 256K, 4 GB prompt cache
# the 256k profile runs:
mlx-serve --model "$MODEL" --serve --host 0.0.0.0 --port 11234 --metrics --mtp \
--ssm-checkpoint-max 16 --prefix-cache-mem 4GB --prefix-cache-disk 100GB \
--prefix-cache-entries 64 --ctx-size 262144 --max-concurrent 2Runbook: plan.md. Raw results and stage reports: results/. The data behind this page is embedded in it as JSON (#data).
Result applies the pass rule to the worst slowdown (session, ÷ the session alone), then fails any configuration with a refused request or a recall miss. Recall counts only answered requests. Refused / HTTP errors (client) counts what the client saw; the server log can count more refusals. 256K slowdowns use the 11.1 s reference.
| Code | Configuration | Sessions | Context | Side requests | Worst slowdown | Refused / HTTP errors (client) | Recall | Peak memory | Result |
|---|---|---|---|---|---|---|---|---|---|
| w1c1 | 1 session, 32K | 1 | 32K | no | 1.00× | 0 | 13/13 | 96.0 GB | pass |
| w1c1a | 1 session + side requests, 32K | 1 | 32K | yes | 1.04× | 0 | 13/13 | 96.9 GB | pass |
| w1c2 | 2 sessions, 32K | 2 | 32K | no | 1.75× | 0 | 26/26 | 100.7 GB | pass with a warning |
| w1c2a | 2 sessions + side requests, 32K | 2 | 32K | yes | 1.76× | 0 | 25/26 | 99.9 GB | pass with a warning |
| w1c1x2 | 2 sessions, --max-concurrent 1, 32K | 2 | 32K | yes | 1.76× | 0 | 26/26 | 100.1 GB | pass with a warning |
| w1c3a | 3 sessions + side requests, 32K | 3 | 32K | yes | 2.37× | 0 | 39/39 | 101.4 GB | fail |
| w1c4 | 4 sessions, 32K | 4 | 32K | no | 2.84× | 0 | 52/52 | 103.6 GB | fail |
| w1c4a | 4 sessions + side requests, 32K | 4 | 32K | yes | 3.37× | 0 | 52/52 | 104.3 GB | fail |
| w1c1 | 1 session, 128K | 1 | 128K | no | 1.00× | 0 | 12/13 | 110.5 GB | pass |
| w1c2 | 2 sessions, 128K | 2 | 128K | no | 1.78× | 0 | 26/26 | 112.6 GB | pass with a warning |
| w1c2a | 2 sessions + side requests, 128K | 2 | 128K | yes | 1.94× | 0 | 26/26 | 113.5 GB | pass with a warning |
| w1c1x2 | 2 sessions, --max-concurrent 1, 128K | 2 | 128K | yes | 1.83× | 0 | 26/26 | 113.5 GB | pass with a warning |
| w1c3a | 3 sessions + side requests, 128K | 3 | 128K | yes | 2.36× | 0 | 39/39 | 114.7 GB | fail |
| w1c4 | 4 sessions, 128K | 4 | 128K | no | 3.66× | 0 | 51/52 (miss) | 116.0 GB | fail |
| w1c4a | 4 sessions + side requests, 128K | 4 | 128K | yes | 3.85× | 1 | 49/51 (miss) | 115.9 GB | fail |
| w1c2ap3 | 2 sessions + side requests, prefill-decode-share 0.3, 128K | 2 | 128K | yes | 2.02× | 0 | 26/26 | n/a | fail |
| w1c2ap5 | 2 sessions + side requests, prefill-decode-share 0.5, 128K | 2 | 128K | yes | 1.92× | 0 | 26/26 | n/a | pass with a warning |
| w1c1 | 1 session, 256K | 1 | 256K | no | 0.93× | 0 | 12/13 | 113.9 GB | pass |
| w1c2L | 256K + 32K sessions, 16 GB prompt cache | 2 | 256K + 32K | yes | 1.96× | 1 | 23/25 (miss) | 115.0 GB | fail |
| w1c2m4 | 2 × 256K, 4 GB prompt cache | 2 | 256K | yes | 1.68× | 0 | 24/26 | 113.3 GB | pass with a warning |
| w1c2q8 | 2 × 256K, 8-bit KV cache | 2 | 256K | yes | 1.56× | 1 | 25/25 | 113.7 GB | fail |
| w1c2m4q8 | 2 × 256K, 4 GB cache + 8-bit KV | 2 | 256K | yes | 1.50× | 0 | 25/26 (miss) | 107.7 GB | fail |