Repository navigation
fix: awake host steps run as plain ones on Apple7/8 - #38
Conversation
On Apple7/8 the buffer of host_step_wait that an awake host step commits starts as soon as it is committed, before the work ahead of its step has ended, and spins out its bound (2^18 loads, 141 ms on the M2 Max) beside that work. A Flash-Next decode cycle with streamed experts has 49 awake steps (one per expert load), so its spins pile up and the cycle runs 5-8x slower. The spin check passes: the spin does see the CPU's release within 3-8 us; it just starts too early. Apple7/8 now skip the check and run awake steps as plain ones, without loading host-step.metallib or a warning. Apple9/10 are unchanged. M2 Max, Flash-Next Q2_0, A B B A, before -> after: - server, --expert-memory 24G (experts stream): 4.57 / 4.54 -> 44.56 / 44.60 tok/s (5 greedy 400-token requests, median per run) - server, --max-memory 44G (experts resident; one awake step per cycle): mean of 6 runs 55.50 -> 55.29 tok/s, within the 1.3% spread of base runs - decode-profile 2K, --expert-memory 40G, fused B1-B4 ms: 529.9 / 807.0 / 842.8 / 983.7 and 513.3 / 789.5 / 820.5 / 710.1 -> 93.3 / 98.9 / 117.9 / 143.9 and 92.8 / 98.8 / 119.9 / 142.1 A spin that waits for its step's ready event also removes the slowdown but gains nothing over plain steps here (decode-profile B4 +5.5%, 24G server -1.5%). Tested only on the M2 Max (Apple8); M1 (Apple7) is untested. metal-backend's spin tests expect no spin on Apple7/8: the fresh-backend and 50-step tests check that the spin is decided off, and the fallback test checks that a broken host-step.metallib beside the backend's library is neither loaded nor warned about there.
|
@abssoul if you have time, could you check this on your M1 Max? I could only test Apple8. Flash-Next Q2_0 with |
|
Checked on my M1 Max — the fix works, and the bug it removes is worse here than your M2 numbers suggested. Environment: MacBook Pro 14" (2021), Apple M1 Max — 32-core GPU, 64 GB unified memory; macOS 27.2 (26B123) (updated from 27.0.1 since my previous test); quiet machine, no background GPU/CPU load. Before side = the installed splash 1.4.0-m1 release; after side = your branch One addition your recipe needs on a 64 GB machine: Median fused decode cycle (ms):
With the file beside the metallib the streamed B1 cycle is 292–564 ms here — 2.5–4.4× the same stack with your fix (115–129 ms). The buggy rows also swing hard between runs while every other row is stable to a few percent, which matches the spin starting at a random point. With your fix the file's presence stops mattering, and B2/B3 come out slightly better than the stock no-file control (145–152 vs 171–172, ~194 vs 203–212). Two side observations: decode-profile stops after B3 with |
|
@abssoul thank you for checking it on M1 Max! Good to know Apple7 behaves the same, and even worse. The exit 1 after B3 is decode-profile itself, not a bug. Its first lane stays in every width, so it decodes 8 x --cycles cycles, and its request is capped at 256 tokens. At ~2 tokens per cycle --cycles 20 runs out in B4. I hit the same with --cycles 30 (in B3), with --cycles 12 B4 completes. |
|
Thanks, merged. On an M1 Max 64 GB it matters even with every expert resident. Flash-Next Q2_0 through the server, three runs each: before, a decode cycle took anywhere from 69 to 114 ms (1 lane 42.5, 50.9, 65.0 tok/s), with this branch it stays at 71 to 74 ms (62.7, 62.8, 65.5). So the spin seems to steal time on the 32-core M1 even with one awake step. |
On Apple7/8, Flash-Next decodes about 10x slower when its routed experts don't all fit in memory. An awake host step commits a buffer that spins (
host_step_wait) until the CPU releases it. On my M2 Max that buffer starts as soon as it is committed, before the work ahead of its step is done, so it spins up to its bound (2^18 loads, 141 ms here) beside the model's work. With streamed experts a decode cycle has 49 awake steps, one per expert load, and the cycle takes 0.5–1 s instead of ~95 ms. The spin check still passes, because the spin does see the release (3–8 µs after it), it just starts too early.With all experts resident (
--max-memory 44Gon a 64 GB Mac) a cycle has one awake step, so it doesn't show. It does show on any Mac that has to stream experts, like a 32 GB M1/M2 or with--expert-memoryset, and indecode-profilewheneverhost-step.metallibsits beside the metallib.This makes Apple7/8 skip the spin check and run awake steps as plain ones, without loading
host-step.metallibor a warning. Apple9/10 are unchanged. I also tried letting the spin's buffer wait for its step's ready event. That removes the slowdown too, but it gained nothing over plain steps here (decode-profile B4 +5.5%, 24G server −1.5%), so I kept the simpler one. As before, I used Claude Code to author it and tested it by hand.What changed
runtime/metal/MetalBackend.mm:checkHostStepSpin()returns early on family < 9 and never re-checks, soencodeHostStepruns awake steps as plain ones. The comment above it says why.dev/tests/engine/metal_backend_test.mm: the three awake-step tests assumed a spin on every GPU. On Apple7/8 they now check that the spin is decided off, and that a brokenhost-step.metallibbeside the library is neither loaded nor warned about. Also fixedawake && spins, which was true for a decided "no" (anoptionalin a bool context); it is nowspins.value_or(false).Results (M2 Max, 38-core GPU, Flash-Next Q2_0, A B B A)
Server decode, tok/s, median of 5 greedy 400-token requests per run:
--expert-memory 24G(experts stream)--max-memory 44G(experts resident), mean of 6 runsThe 44G difference (−0.4%) is inside the 1.3% spread between the before runs.
decode-profile, 2048 prompt tokens,--expert-memory 40G, fused GPU ms:Testing
metal-backend,expert-store,kv-block-cacheandqwen4-kernels: PASS without validation, PASS withMTL_SHADER_VALIDATION=1.qwen4-kernelsbitwise: PASS (without validation, asmakeruns it).make check: CPU tests and the Python suite (1320 tests) pass. In the Metal tests everything passes butgguf-moe, which times out under validation onapple7-m1-1.4itself on my M2 Max (the 120 s command limit, as in perf: decode PQ2_0 & Q2_0 with its zero point as a min on Apple7/8 #31; with fix(tests): gguf-moe forces only GPU families the device supports #37 it passes). That stopstest-engine-metal, so I ran the Metal tests after it separately with validation, and they all pass.decode-profilewith--expert-memoryon Flash-Next before and after would show it.