You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Running the split DeepSeek V4 PRO Q4 GGUF across two Mac Studio M3 Ultra 512GB machines (coordinator 0:30, worker 31:output), the distributed route builds fine and text is coherent, but generation runs at 0.14 t/s (gen_tps=0.14, ~7.1 s/token) versus the README's 11.47 t/s smoke test — ~80x slower.
Both halves are symmetrically slow. The remote worker (layers 31:60) reports ~3.64 s/token, and by subtraction the coordinator's local layers 0:30 is ~3.5 s/token (7.1 s total − 3.64 s remote). Since the coordinator's local half never touches the network, this rules out the interconnect and the distributed protocol entirely — the problem is PRO Q4 per-token decode compute on the M3 Ultra.
The smoking gun: decoding 1 token (~7.1 s) takes longer than prefilling 512 tokens (~6.3 s, prefill_tps=80.71). For a 6-of-384 routed MoE this is impossible unless the sparse-expert fast path (the README's "queue-resident exact expert tables") is inactive during decode and each token re-binds the broad multi-GiB routed tensors. Each machine maps ~420 GB resident (426 / 411 GB) on a 512 GB box, leaving little headroom for the resident expert tables — consistent with the README's warning about hitting "Metal memory accounting limits."
Environment
Hardware: 2x Mac Studio Mac15,14, Apple M3 Ultra (32-core CPU: 24P+8E / 80-core GPU), 512 GB unified memory (one 2TB SSD, one 4TB SSD — no perf difference)
OS: macOS 26.4.1 (build 25E253), system firmware 18000.101.7; built with Command Line Tools (Xcode.app not installed)
Commit: 80ebbc3 (current main HEAD, 2026-06-17) — confirmed identical on both machines (coordinator and worker both report 80ebbc3)
Link: direct Thunderbolt bridge, 192.168.2.1 to 192.168.2.2 (static IPs), ping 0.5–0.9 ms — matches the README's Thunderbolt reference (~0.45 ms), so the interconnect is not the bottleneck
Decomposition of the ~7.1 s/token (from gen_tps=0.14):
segment
per-token
where measured
remote worker layers 31:60
~3.64 s
reported in telemetry
local coordinator layers 0:30
~3.5 s
= 7.1 s total − 3.64 s remote
network (downstream_wait + forward_send)
~0 ms
telemetry
Key observations:
Both halves are ~3.5 s/token, symmetrically. The local half runs entirely on the coordinator with no network involved, so this is a compute problem, not a distributed/interconnect problem.
Decode is slower than prefill of 512 tokens: 1 decode token ~7.1 s vs 512-token prefill ~6.3 s (prefill_tps=80.71). This indicates the sparse-MoE fast path is inactive during decode and each token re-binds/reads the broad routed tensors. Per half, ~420 GB / 3.5 s ≈ 120 GB/s effective, ~1/7 of the M3 Ultra's ~819 GB/s.
PRO Q4 decode isn't using the "queue-resident exact expert tables" path and instead re-binds/reads the broad routed tensors every token (the README's known slow/early failure mode). This happens on both halves, so it is not distributed-specific. Most likely trigger: each machine maps ~420 GB resident on a 512 GB box, leaving too little headroom for the resident expert tables, so Metal memory-accounting forces the fallback. Prefill (which reads broadly anyway, once per chunk) is only mildly affected; decode — where the sparse-gather fast path should dominate — pays the broad-binding cost per token and ends up slower than a 512-token prefill.
Questions
Is the "queue-resident exact expert tables" decode path expected to be active for split PRO Q4 on 2x 512 GB (each half ~420 GB resident), or does that headroom already trip the "Metal memory accounting limits" fallback the README mentions?
Is there a memory-headroom threshold for the fast path? Would freeing more RAM, or a more headroom-preserving split, restore it?
Any flag to force/verify the fast path, or extra telemetry (e.g. a per-token line for the coordinator's local layers) to confirm which decode path each half takes?
Diagnostics (note: --trace does not apply here)
--trace is not the right diagnostic for this bug. --trace is a ds4-eval / ds4-server flag (defined in ds4_eval.c). ds4-bench rejects it, and plain ds4 produces no trace file for this run path. It also logs agent-session prompts / cache decisions / tool-parser events, not decode timing or Metal residency. The evidence for this performance bug is already inline above: the --debug per-hop telemetry, the ds4-bench CSV (gen_tps=0.14 / prefill_tps=80.71), and the memory_pressure wired-residency snapshots.
Optional extra evidence, easy to capture: the per-token Metal graph timing that --debug prints (same shape as #465, e.g. ds4: metal graph token pos=N encode=... execute=... total=...), and a memory_pressure snapshot taken DURING the slow decode on both machines to show the wired collapse with Swapouts=0.
# run on BOTH machines mid-decode to show wired collapse + zero swap:
memory_pressure
# per-token Metal timing already emitted by --debug (grep the run log):
# ds4: metal graph token pos=N encode=... execute=... total=...
UPDATE — Root cause confirmed: wired residency collapses every token, on BOTH halves
Captured memory_pressure during an actual slow run on both machines. The wired (pinned/GPU-resident) footprint is NOT stable across the generation loop: it is wired up for each request's compute and released again — on BOTH the coordinator and the worker. This is Metal residency churn in physical RAM, not OS paging: Swapins/Swapouts stay 0 and Pageins increase by only ~34–44 pages between phases, so the hundreds of GiB that leave "wired" stay resident in RAM (as "inactive") and never touch SSD.
machine / phase
wired
free %
swapouts
coordinator — prefill (request=1)
~433 GiB
15%
0
coordinator — decode (request>1)
~38 GiB
92%
0
worker — compute phase
~232 GiB
54%
0
worker — released phase
~5.5 GiB
98%
0
The coordinator wires ~433 GiB (nearly its full 426 GiB local half) during compute, then drops to ~38 GiB; the worker peaks at only ~232 GiB (well below its 411 GiB half) and drops to ~5.5 GiB. Both oscillate per request. This directly explains: (a) decode being slower than a 512-token prefill (prefill wires once for 512 tokens; decode re-wires per token), and (b) both halves being symmetrically slow.
Upgraded hypothesis (supersedes the section above): during decode the PRO Q4 path does not keep the routed expert tables queue-resident; it releases their GPU/Metal residency after each request and re-binds the broad multi-GiB routed tensors for every token — the exact slow path the README's "queue-resident exact expert tables" was meant to avoid. The cost is residency re-establishment, not disk I/O (confirmed: Swapouts=0, Pageins flat). Whatever keeps the expert tables resident across the prefill call is not being kept alive across the per-token decode calls.
Added ruled-out row: OS memory pressure / swap — memory_pressure shows Swapins/Swapouts = 0 throughout and compressor ~8 MB (coordinator); the slowdown is not OS paging.
Related issues / prior art (checked before filing)
I searched the README and existing issues. This scenario is not a duplicate — the closest reports are either single-machine, SSD-streaming, CUDA, or a different trigger. Mapping:
closest symptom: multi-second per-token Metal execute, ~0.24 t/s
single machine, triggered by --power, reporter suspects macOS 27 Beta GPU ramping, warmup mitigates
#455 Metal SSD streaming: exact path misses routed expert views
same subsystem: "Metal cannot wrap routed-expert model ranges"
SSD-streaming test path, not distributed in-RAM residency
#491 Expert bundle sidecar for contiguous MoE expert reads
same pain point (routed expert reads)
targets SSD-streaming cold-miss disk I/O; here there is no disk I/O
#437 / #439 Metal SSD-streaming benchmarks / cache pressure
same area (Metal routed-expert residency)
SSD streaming (model does not fit)
#293 Distributed worker host-registers the whole GGUF
distributed + whole-model registration concept
CUDA / DGX Spark, not Metal
#477 page-cache residency when RAM ≥ model (decode +52%)
residency optimization
CUDA, not Metal
README (queue-resident exact expert tables)
names the exact mechanism and the "Metal memory accounting limits" failure mode; flags distributed as not release-stable
documents it as a known hard area, but not this fits-in-RAM per-token residency collapse
What makes this report distinct (and, I think, a clean repro):
The model fully fits and stays in RAM — this is NOT SSD streaming. Wired peaks at ~433 GiB (coordinator) / ~232 GiB (worker), Swapouts stay 0, Pageins are flat across the prefill→decode transition. No disk I/O is involved.
The wired residency of the routed expert tables collapses in RAM between prefill and decode and is re-established per token, on BOTH halves symmetrically.
Additional evidence: the slowdown is sustained (rules out warmup; distinguishes from #465)
A gen_tokens=256 run shows 24 consecutive decode tokens all at ~3.6 s (eval 3565–3774 ms) — steady state, not a first-token/warmup artifact. This is the key difference from #465, where a warmup period mitigated the slowness. Two independent gen_tokens=8 runs reproduce the CSV: 80.71/0.14 and 81.40/0.14. Precise ping on the Thunderbolt bridge: 0.209 ms avg to 192.168.2.1, 0.762 ms avg to 192.168.2.2.
Summary
Running the split DeepSeek V4 PRO Q4 GGUF across two Mac Studio M3 Ultra 512GB machines (coordinator 0:30, worker 31:output), the distributed route builds fine and text is coherent, but generation runs at 0.14 t/s (gen_tps=0.14, ~7.1 s/token) versus the README's 11.47 t/s smoke test — ~80x slower.
Both halves are symmetrically slow. The remote worker (layers 31:60) reports ~3.64 s/token, and by subtraction the coordinator's local layers 0:30 is ~3.5 s/token (7.1 s total − 3.64 s remote). Since the coordinator's local half never touches the network, this rules out the interconnect and the distributed protocol entirely — the problem is PRO Q4 per-token decode compute on the M3 Ultra.
The smoking gun: decoding 1 token (~7.1 s) takes longer than prefilling 512 tokens (~6.3 s, prefill_tps=80.71). For a 6-of-384 routed MoE this is impossible unless the sparse-expert fast path (the README's "queue-resident exact expert tables") is inactive during decode and each token re-binds the broad multi-GiB routed tensors. Each machine maps ~420 GB resident (426 / 411 GB) on a 512 GB box, leaving little headroom for the resident expert tables — consistent with the README's warning about hitting "Metal memory accounting limits."
Environment
Repro
Worker (machine B):
Coordinator (machine A):
Coordinator telemetry (the smoking gun)
Decomposition of the ~7.1 s/token (from gen_tps=0.14):
Key observations:
What I've ruled out
Single-machine Flash Q4 on the same hardware is normal:
Hypothesis
PRO Q4 decode isn't using the "queue-resident exact expert tables" path and instead re-binds/reads the broad routed tensors every token (the README's known slow/early failure mode). This happens on both halves, so it is not distributed-specific. Most likely trigger: each machine maps ~420 GB resident on a 512 GB box, leaving too little headroom for the resident expert tables, so Metal memory-accounting forces the fallback. Prefill (which reads broadly anyway, once per chunk) is only mildly affected; decode — where the sparse-gather fast path should dominate — pays the broad-binding cost per token and ends up slower than a 512-token prefill.
Questions
Diagnostics (note: --trace does not apply here)
--trace is not the right diagnostic for this bug. --trace is a ds4-eval / ds4-server flag (defined in ds4_eval.c). ds4-bench rejects it, and plain ds4 produces no trace file for this run path. It also logs agent-session prompts / cache decisions / tool-parser events, not decode timing or Metal residency. The evidence for this performance bug is already inline above: the --debug per-hop telemetry, the ds4-bench CSV (gen_tps=0.14 / prefill_tps=80.71), and the memory_pressure wired-residency snapshots.
Optional extra evidence, easy to capture: the per-token Metal graph timing that --debug prints (same shape as #465, e.g. ds4: metal graph token pos=N encode=... execute=... total=...), and a memory_pressure snapshot taken DURING the slow decode on both machines to show the wired collapse with Swapouts=0.
UPDATE — Root cause confirmed: wired residency collapses every token, on BOTH halves
Captured memory_pressure during an actual slow run on both machines. The wired (pinned/GPU-resident) footprint is NOT stable across the generation loop: it is wired up for each request's compute and released again — on BOTH the coordinator and the worker. This is Metal residency churn in physical RAM, not OS paging: Swapins/Swapouts stay 0 and Pageins increase by only ~34–44 pages between phases, so the hundreds of GiB that leave "wired" stay resident in RAM (as "inactive") and never touch SSD.
The coordinator wires ~433 GiB (nearly its full 426 GiB local half) during compute, then drops to ~38 GiB; the worker peaks at only ~232 GiB (well below its 411 GiB half) and drops to ~5.5 GiB. Both oscillate per request. This directly explains: (a) decode being slower than a 512-token prefill (prefill wires once for 512 tokens; decode re-wires per token), and (b) both halves being symmetrically slow.
Upgraded hypothesis (supersedes the section above): during decode the PRO Q4 path does not keep the routed expert tables queue-resident; it releases their GPU/Metal residency after each request and re-binds the broad multi-GiB routed tensors for every token — the exact slow path the README's "queue-resident exact expert tables" was meant to avoid. The cost is residency re-establishment, not disk I/O (confirmed: Swapouts=0, Pageins flat). Whatever keeps the expert tables resident across the prefill call is not being kept alive across the per-token decode calls.
Added ruled-out row: OS memory pressure / swap — memory_pressure shows Swapins/Swapouts = 0 throughout and compressor ~8 MB (coordinator); the slowdown is not OS paging.
Related issues / prior art (checked before filing)
I searched the README and existing issues. This scenario is not a duplicate — the closest reports are either single-machine, SSD-streaming, CUDA, or a different trigger. Mapping:
What makes this report distinct (and, I think, a clean repro):
Additional evidence: the slowdown is sustained (rules out warmup; distinguishes from #465)
A gen_tokens=256 run shows 24 consecutive decode tokens all at ~3.6 s (eval 3565–3774 ms) — steady state, not a first-token/warmup artifact. This is the key difference from #465, where a warmup period mitigated the slowness. Two independent gen_tokens=8 runs reproduce the CSV: 80.71/0.14 and 81.40/0.14. Precise ping on the Thunderbolt bridge: 0.209 ms avg to 192.168.2.1, 0.762 ms avg to 192.168.2.2.
Full raw logs (ping, per-token telemetry for the 256- and 8-token runs, and both memory_pressure captures): https://gist.github.com/treemanz/271e62fe85dd3f9cfd815e997a917f63