Repository navigation
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
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).