Repository navigation
--stream-experts: intermittent kIOGPUCommandBufferCallbackErrorTimeout (server crash), correlates with cold page cache #216
Copy link
Copy link
Closed
Description
Summary
With --stream-experts, the server intermittently dies with a Metal GPU watchdog timeout right after prefill starts:
MLX/ErrorHandler.swift:345: Fatal error: [METAL] Command buffer execution failed: Caused GPU Timeout Error (00000002:kIOGPUCommandBufferCallbackErrorTimeout)
It happens right after slot_launch ... prefilling..., and the whole server process exits. The crash is intermittent and correlates with a cold OS page cache.
Environment
- Apple Silicon Mac (M-series), 64 GB RAM, macOS with Xcode 27
- Model: Qwen3.5-35B-A3B-4bit (19 GB, VLM-loaded
qwen3_5_moe) - Command:
SwiftLM --model <dir> --port 5413 --stream-experts - Request: non-streaming
/v1/chat/completions, temperature 0,max_tokens120. The prompt is repeated lines ofItem i: the quick brown fox number i jumps over lazy dog i*7.plus a summarise instruction.
Observations (all measured)
- Pre-existing. Reproduced on the pre-bump build (SwiftLM
06bac92) and on current main + mlx-swift3764829(mlx core 0.32.3) + mlx-swift-lm with fix: memory auto-cap strategy for SSD MoE streaming + speculative decoding (Issue #72) #77/feat: bump mlx-swift-lm for DeepSeek-V4 support #79. It is not caused by the 0.32 bump or by fix: memory auto-cap strategy for SSD MoE streaming + speculative decoding (Issue #72) #77. - Streaming-specific. The same 873-token prompt WITHOUT
--stream-expertscompletes in about 10 s with no crash. - Intermittent and not length-dependent. Crashes at 213, 873 and 104 tokens; passes at 433 and 653 tokens. Once the cache was warm, 7 consecutive runs (213/433/873 tokens) all passed, including the 873-token prompt that had crashed earlier.
- Correlates with a cold page cache.
- I read a 40 GB scratch file twice to evict the model files. The first 213-token request then crashed after 350 s. The second passed but took 196 s (normal warm: 22-51 s).
- Earlier crashes were also unusually slow: one 104-token request ran 2m05s before crashing, where warm it takes about 9 s.
- The controlled cold test is only 1 crash / 1 pass, so this is a strong correlation, not proof.
Repro recipe
- Start the server with
--stream-expertsright after evicting the page cache (for example, read a file larger than free RAM twice). - Send a ~200-token prompt.
- The request stalls for minutes and the server then aborts with the GPU timeout.
Root cause
Not identified. Suspects from code reading (all unverified):
- Lazily mmap'd non-expert weights are first touched by the GPU, so page faults from SSD occur inside a command buffer.
LoadSSDExpert::eval_gpuin mlx-swift (Source/Cmlx/mlx/mlx/core/moe_stream_op.cpp) does a blocking synchronouspread(SSDStreamer::load_sync) while the command buffer is being encoded. Prefill touches up to 256 experts per projection per layer, so cold reads could stall long enough to trip the GPU watchdog.- The per-expert
preadInto/gatherQuantizedMMloop in mlx-swift-lmSwitchLayers.swift(QuantizedSwitchLinear.callAsFunction,ExpertStreamingConfigpath).
A follow-up instrumentation experiment (timing load_sync and command-buffer commits) is being run separately. Results will be added as a comment on this issue.
Workarounds
- Warm the page cache before serving, for example
cat <model>/*.safetensors > /dev/null. - Run without
--stream-expertsif the model fits in RAM.
🤖 Generated with Claude Code
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
solderzzc commentedon Oct 7, 2026
Update after reading the code paths (no runtime timing yet, so this is a code-derived hypothesis, not a confirmed root cause).
Likely mechanism: the GPU command buffer waits on a CPU file read, and the watchdog counts that wait.
--stream-expertssetsmodelConfig.lazyLoad = true(Sources/SwiftLM/Server.swift). Inmlx-swift-lmLoad.swift, only the!lazyLoadbranch callsmaterializeModelForInference, so with streaming no weights are read at load time, including the non-expert ones (attention, norms, router, shared expert).Loadprimitive that runs on the CPU stream:Load::eval_cpuenqueues apreadonio::thread_pool(mlx/backend/common/load.cpp);Load::eval_gputhrows "Not implemented".CommandEncoder::wait_event->encodeWait,mlx/backend/metal/device.cpp). So after commit the GPU sits idle inside the command buffer until the CPU read finishes.kIOGPUCommandBufferCallbackErrorTimeout. The comment inssd_streamer.mmalready notes that GPU waits on pread completion can produce this same error.This fits every observation: streaming-only (non-streaming loads everything up front), cold-cache-only, intermittent, not prompt-length dependent, pre-existing before the mlx 0.32.3 bump.
Proposed fix: in streaming mode, after weights are loaded and before the server starts accepting requests, explicitly evaluate (read) all non-expert parameters on the CPU, leaving only the streamed expert tensors lazy. Then no GPU command buffer ever waits on a first-touch file read for those weights. I'll try this in a small PR and validate with a plain restart + first request (no cache-eviction tricks).