Repository navigation
Flash Next keeps its buffers wired between requests: a residency set, and a keepalive tick the GPU runs - #732
sethforprivacy wants to merge 4 commits into
Conversation
… and a keepalive tick the GPU runs macOS unwires an idle process's GPU memory within a second or two. The next request's first command buffer then waits in the queue while it comes back: on an M5 Ultra with the 6-bit checkpoint, encode 0.5 ms, GPU 90 ms, queued 1.2-2.3 s before the first prompt chunk ran. The idle keepalive did not prevent it: Flash Next's host gave it no residency set, and its tick commits an empty command buffer, which need not reach the GPU. - Run.held lists the buffers buffer, load and group make; the Flash Next host puts them in one residency set (resident.zig) that its keepalive target uses. FLASHNEXT_NO_RESIDENCY=1 leaves it out. - The keepalive target's tick copies 16 bytes inside the set (Target.scratch), so each tick's command buffer runs on the GPU with the set. Resident.init adds the scratch buffer, so the 27B's served host ticks the same way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng set On a 256 GB M5 Ultra a 195.8 GiB set (four 262K sessions) slowed the GPU-side rounds about threefold, alone or under --parallel 1, while sets under the working set held them at speed (BobClawblaw on ashhart#606). The host now holds the set only while its buffers, the prompt cache's budget and a 2 GiB margin fit recommendedMaxWorkingSetSize, and logs which way it went with the numbers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Posted by Claude (Anthropic's AI assistant) on behalf of @sethforprivacy, as part of our local-inference tuning work on a Mac Studio M5 Ultra. An update for BobClawblaw's counterexample on #606 (receipt). A residency set of 195.8 GiB (four 262K sessions) made the GPU-side rounds erratic and about three times slower there. Sets of 172.8-178.8 GiB ran at speed. A likely cause. That set is bigger than the GPU's recommended working set on a stock 256 GB Mac. This Mac's GPU wired limit has been raised (
The 75 % figure matches a stock wired limit, but we can't lower ours to show the slow side, so that part is still unconfirmed. The gate (new commit Receipts, 1.0.8 (
Every decode reply's sha is the same in all three rows. @BobClawblaw, could you rerun with your settings? The same gate is on our combined branch for both batching hosts (
|
|
@sethforprivacy thanks for the gate. Readings from this Mac first, because they don't quite fit the working-set explanation.
So here it looks more like system memory pressure than the GPU working set. The trigger may be anything in the GPU's path that isn't wired, or the VM's work elsewhere stalling the GPU. I haven't proven it. An attempt at a Metal System Trace with the server running took this Mac out of memory, so I can't attach one. I'd rather not run four 262K sessions again here: they're past what this Mac can keep for other apps, and it now serves one stream. I can rerun your gate's branch at |
|
@sethforprivacy more data from the M5 Ultra (256 GB, These are one-request decode runs, code and prose alternating, 256 tokens, greedy. Every reply has the same rounds and accepted drafts whatever its speed:
|
|
@sethforprivacy the registry experiment from my last comment didn't pan out. Every buffer joining the set as it was made kept most replies steady, but a new prompt whose prompt-cache states allocated and freed buffers stalled at 5 tok/s (102 with no set), in both runs. Committing the set while the queue runs costs more than the mixed rounds it avoids. What did work here, with no residency set at all, is turning off the GPU driver's idle collector:
Wired memory stays at about 182 GiB through the idle spells. So on this Mac the idle cost your set fixes comes from the collector unwiring the engine's memory a second or two after each request. With the collector off, the engine gets the fast first tokens without a set and without the slow rounds. It raises no limit: Apple doesn't document the setting, and it resets at boot; my recipe ships a small |
|
@sethforprivacy a fix that works on this Mac with macOS's defaults (
Same M5 Ultra, one stream at 262K, 6 GiB prompt cache,
It's an experiment on a fork branch, not shaped for review yet. The registry lives in |
…r the working set BobClawblaw's readings on ashhart#732: recommendedMaxWorkingSetSize is 239.1 GB on his 256 GB M5 Ultra (iogpu.wired_limit_mb at its default), so the 195.8 GiB set that ran erratically was under it, and a 172.7 GiB set went erratic too. The gate would have held both. What his runs point to instead is rounds that touch buffers outside the set (allocated after it was built), which the all-buffers registry he measured addresses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sets well under the working set (BobClawblaw on ashhart#732) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Posted by Claude (Anthropic's AI assistant) on behalf of @sethforprivacy, as part of our local-inference tuning work on a Mac Studio M5 Ultra. @BobClawblaw thanks, that settles it: the working-set explanation was wrong. On your Mac the limit is the default, What we see here. Same chip and checkpoint, macOS defaults apart from the raised wired limit noted above. Neither slow pattern shows up with our startup-built sets, and we've tried to provoke it:
In every one of those runs, each prompt's replies stayed within about 1-4 % of its best. So we can't reproduce the mixed-buffer slowdown. Whatever triggers it on your Mac (memory the VM is working on, a swap file still full, a busy node next to it) isn't present here. Your runs show that it happens, though, and a set built only at startup can't rule it out. Your registry is the better design for #732. Your Would you like to bring it in? A PR against
If you'd rather not, I'll port it here with your commits and authorship kept, and credit your receipts. The 16-byte copy in the keepalive tick stays either way: your idle numbers depend on the tick reaching the GPU too. |
|
An M3 Ultra receipt for this PR at
Token hashes match across both arms: the short prompt, the 32k prompt cold and resumed, and a 16k prompt resumed. No GPU errors. Our earlier build held only the per-layer embedding tables resident and still took 2.2 to 2.6 s for the first token after 20 minutes idle, so that small subset did not help here. We have not measured gaps beyond |
…shnext-resident # Conflicts: # zig/src/native/flashnext_host.zig
|
Posted by Claude (Anthropic's AI assistant) on behalf of @sethforprivacy, as part of our local-inference tuning work on a Mac Studio M5 Ultra. @cwschroeder thank you for the M3 Ultra run. It's the first receipt from a chip other than the M5: 0.19-0.20 s after every gap against 2.6-3.0 s without the set, with matching hashes. It's in the description now. Your note about the per-layer embedding tables is useful too: a set that small left the idle cost in place. The all-buffers registry proposed above is still open with @BobClawblaw. |
|
Hello @sethforprivacy, Thank you for the residency set, and for the M3 Ultra receipt. About 0.20 s after every gap, against 2.6 to 3.0 s without the set, with matching hashes. Decided. We keep the set and bound it at 70% of RAM, shared by every set. The bound goes in Resident.hold and Resident.add, and on the buffers that join a set later. We are finishing that now, and your three commits stay as they are. We will ask for a rerun on your M5 Ultra. Thank you. |
|
@ashhart one data point for the 70% bound, from the M5 Ultra (256 GiB, so the bound is 179.2 GiB). What happens when the bound leaves buffers out. On this Mac, a set that misses buffers was worse than no set at all (measurements). In these one-request decode runs, every reply had the same rounds and accepted drafts:
With every buffer held (registry), 0 of 90 replies were slow. Real configurations land right at the bound. Flash Next with A suggestion, if it suits your design. When the whole set would pass the bound, choose one of these instead of a partial set:
Either way the set stays whole. At least a warning at load, saying the set was cut and by how much, would make the slow rounds explainable. I'm happy to rerun on this Mac once your bound is in, at this configuration and one just under the bound. |
|
Posted by Claude (Anthropic's AI assistant) on behalf of @sethforprivacy, as part of our local-inference tuning work on a Mac Studio M5 Ultra. @ashhart thank you, the 70 % bound sounds right. One data point beside BobClawblaw's: our production server is over it too. It runs Flash Next on cwschroeder's batch host with 16 sessions and a 16 GiB prompt cache, a 184 GiB set on a 256 GiB Mac, against a 179.2 GiB bound. So both of the configurations we've measured on M5 Ultras land at or just past the line. The two Macs disagree on what a partial set costs:
Given that, I'd second BobClawblaw's minimum: if the bound cuts a set, say so at load, with how much was left out and which flag would bring it under (a smaller When the bound is in, we'll rerun on the M5 Ultra at our production setting (over the bound) and at one just under it. We'll report first token after idle and steady decode for both. |
|
A note for the rebase onto 1.0.10 or later, from an M3 Ultra. This PR's commits merged into main 1.0.11 ( Since 1.0.10, load makes temporary buffers through
The fix we run: With the fix on two M3 Ultra Mac Studios (256 GB, macOS 27.0.1,
|
…r the working set BobClawblaw's readings on #732: recommendedMaxWorkingSetSize is 239.1 GB on his 256 GB M5 Ultra (iogpu.wired_limit_mb at its default), so the 195.8 GiB set that ran erratically was under it, and a 172.7 GiB set went erratic too. The gate would have held both. What his runs point to instead is rounds that touch buffers outside the set (allocated after it was built), which the all-buffers registry he measured addresses.
Flash Next keeps its memory wired between requests, every residency set stays inside 70% of RAM, and buffers join and leave the sets as they come and go, with this landing's M3 Ultra numbers. The known issue of a slow first token after a few idle seconds on an M3 Ultra is fixed by it and leaves the list.
Flash Next keeps its memory wired between requests, every residency set stays inside 70% of RAM, and buffers join and leave their sets between requests (#732, with @sethforprivacy's three commits as they were). With saved conversations in, a 27B state read back from disk joins the set as a kept one does; Flash Next's come from the pool, which joins them already.
…t's leave @cwschroeder's test from #732's thread (his ead3446, on 1.0.11 without our landing fix 4ca324c): a Run buffer freed during load leaves `held`, so a residency set built from it never takes a released buffer. Adapted to Run.release, in replay_held_test.zig beside replay.zig. Ours beside it: a buffer made after the set is built joins it, and leaves it when released. Co-authored-by: Christian Schröder <1188270+cwschroeder@users.noreply.github.com>
|
Thank you, @sethforprivacy. Keeping Flash Next wired between requests fixes the slowest first token on a Mac. Thank you too, @BobClawblaw, for the measurements that showed set changes belong between requests, and @cwschroeder for the first M3 Ultra receipt and the crash report. Your three commits ship in TensorFold 1.0.12 under your name, rebased onto the release line as On an M3 Ultra with the 6-bit checkpoint, a cached prompt's first token after 2, 5 and 10 s idle came in 0.13 to 0.15 s instead of 1.95 to 2.61 s, decode stayed within 0.3% and every reply was the same tokens. The 27B's set holds its kept prompt states too. Your branch's head is a merge of main that the release does not carry, so we're closing this pull request with the commits landed under your name. Posted by Ash's AI Assistant Synthena |
|
Posted by Claude (Anthropic's AI assistant) on behalf of @sethforprivacy, as part of our local-inference tuning work on a Mac Studio M5 Ultra. @ashhart thank you for landing it in 1.0.12. One measurement from today: on a 256 GB Mac, the 70 % bound leaves Flash Next 6-bit without a set on the batch host at every size we served. That was cwschroeder's 1.0.12 integration (
The single-reply host's set was about 171 GiB on 1.0.8, so it fits, but a second 262K session already passes the line. With #606 making the batch host the default, a default server on a 256 GB Mac would go back to the cold first token after idle (about 2 s instead of 0.1 s in our measurements), and the log line is its only sign. A suggestion, if it fits your design: when |
|
Confirmed in the source. I pushed the residency-cap fix to #606, commit The default remains 70% of RAM. Shared serving and its single-session fallback now pass their validated load cap to initial residency and later joins, retaining the 2 GiB margin inside it. GLM uses its explicit validated cap when requested; its existing default is preserved. Both new CPU regressions fail under the old rule and pass with the fix. Whole-workflow local checks and the full native build pass; Astra found no remaining source blocker. All current-head upstream and fork checks now pass. The residency/idle-latency hardware retest remains open. |
…r the working set BobClawblaw's readings on ashhart#732: recommendedMaxWorkingSetSize is 239.1 GB on his 256 GB M5 Ultra (iogpu.wired_limit_mb at its default), so the 195.8 GiB set that ran erratically was under it, and a 172.7 GiB set went erratic too. The gate would have held both. What his runs point to instead is rounds that touch buffers outside the set (allocated after it was built), which the all-buffers registry he measured addresses.
Flash Next keeps its memory wired between requests, every residency set stays inside 70% of RAM, and buffers join and leave the sets as they come and go, with this landing's M3 Ultra numbers. The known issue of a slow first token after a few idle seconds on an M3 Ultra is fixed by it and leaves the list.
Flash Next keeps its memory wired between requests, every residency set stays inside 70% of RAM, and buffers join and leave their sets between requests (ashhart#732, with @sethforprivacy's three commits as they were). With saved conversations in, a 27B state read back from disk joins the set as a kept one does; Flash Next's come from the pool, which joins them already.
…wing set's leave @cwschroeder's test from ashhart#732's thread (his ead3446, on 1.0.11 without our landing fix 85c8b56): a Run buffer freed during load leaves `held`, so a residency set built from it never takes a released buffer. Adapted to Run.release, in replay_held_test.zig beside replay.zig. Ours beside it: a buffer made after the set is built joins it, and leaves it when released. Co-authored-by: Christian Schröder <1188270+cwschroeder@users.noreply.github.com>
Posted by Claude (Anthropic's AI assistant) on behalf of @sethforprivacy, as part of our local-inference tuning work on a Mac Studio M5 Ultra.
On 1.0.8 (
0efbd44), Flash Next's first token after an idle gap of 2 s or more takes about 2 s; back to back it takes 0.095 s. With this change it stays at 0.095 s after any gap we tried, up to 10 s.Cause. macOS unwires an idle process's GPU memory within a second or two. The next request's first command buffer then waits in the queue while that memory comes back. On our shared-sessions build we timed that buffer at 0.5 ms to encode, 1.2-2.3 s queued and 90 ms on the GPU. The idle keepalive doesn't prevent this on Flash Next, for two reasons:
Change:
Run.heldlists the buffers thatbuffer,loadandgroupmake. The Flash Next host puts them in one residency set (resident.zig), which its keepalive target uses.FLASHNEXT_NO_RESIDENCY=1leaves the set out.keepalive.Target.tickcopies 16 bytes inside the set (Target.scratch), so each tick's command buffer runs on the GPU.Resident.initadds the scratch buffer, so the 27B's served host ticks the same way. We have not measured the 27B.Receipt.
TensorFold/Qwen3.8-Flash-Next-MLX-6bit-MTP@45032a1.serve --context 262144, the single-reply host.idle_ttft.pybelow). Median time to the first token:Across the 16 requests after gaps of 2 s or more, the slowest took 2.062 s on 1.0.8 and 0.097 s with this change.
On our shared-sessions build (#606 plus additions), the residency set alone, with empty ticks, still left about half the bursts after an idle second waiting ~1.3 s. With the copy, none did. The keepalive still stops
--keep-warmseconds after the last request (900 by default), so a request after a longer idle time still pays the cost.Independent receipt, M3 Ultra (@cwschroeder, comment): 256 GB, macOS 27.0.1, same checkpoint, single-reply host at
--parallel 1 --context 262144 --prompt-cache-gib 8, main0efbd44plus this PR atb6543fe:FLASHNEXT_NO_RESIDENCY=1: 0.26 / 3.03 / 2.79 / 2.60 s, and 25.7 s.zig fmt --checkpasses on the four files. Inzig build testevery test passes, here and on main. One step fails identically on both on this Mac:core.checkpoint_metal's split-checkpoint test logs "checkpoint tensor text.weight stays in its file".idle_ttft.py🤖 Generated with Claude Code