Skip to content

fix: run compiled functions on the task-scoped default device - #17

Merged
solderzzc merged 2 commits into
mainfrom
fix/compile-honor-scoped-device
Sep 24, 2026
Merged

solderzzc merged 2 commits into
mainfrom
fix/compile-honor-scoped-device

Conversation

@solderzzc

Copy link
Copy Markdown
Member

Compiled functions ignore Device.withDefaultDevice. A function first traced on the GPU is reused inside a CPU-scoped call and runs its fused kernel on the GPU.

Why

Device.withDefaultDevice(_:_:) sets only a Swift TaskLocal; the C++ default device is not touched. The backend's compile cache, however, is keyed on the C++ default stream (default_stream(default_device()) in compile.cpp), and that stream is also the one the Compiled primitive is built for.

In SwiftLM this breaks --gpu-layers N. partitionedLayerCall runs the CPU layers under withDefaultDevice(.cpu). Gemma 4's MoE calls compiled helpers there, such as weightedExpertSum and the activation products. Those helpers were traced in a GPU layer, so they run on the GPU, with inputs produced by CPU GatherQMM. The GPU command buffer then waits on the CPU fence for longer than the Metal watchdog allows, and the process aborts with kIOGPUCommandBufferCallbackErrorTimeout.

Change

CompiledFunction.innerCall now sets the C++ default device to Device.defaultDevice() around mlx_closure_apply, and restores the previous device afterwards. This happens while evalLock is held. With the correct device set, a CPU-scoped call gets its own cache entry and traces for the CPU stream.

Tests

  • New testCompileHonorsScopedDefaultDevice: traces on the GPU, then calls under withDefaultDevice(.cpu), and asserts that the call retraces and that a later GPU call reuses the original entry. It fails without this change (1 trace where 2 are expected).
  • TransformTests: 19/19 pass.
  • SwiftLM built against this change: 306 unit tests, 0 failures.
  • gemma-4-26b-a4b-it-4bit --gpu-layers 23 on an M5 Pro now completes a 490-token request instead of aborting after ~105 s. Full-GPU Qwen3.8-27B is unchanged.

🤖 Generated with Claude Code

The backend keys its compile cache on, and builds the Compiled primitive
for, the C++ default stream. Device.withDefaultDevice only sets a Swift
TaskLocal, so a function first traced on the GPU was reused inside a
CPU-scoped call and ran its fused kernel on the GPU against CPU-stream
inputs. Set the C++ default device for the duration of the call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
solderzzc pushed a commit to SharpAI/SwiftLM that referenced this pull request Sep 24, 2026
…eam-experts

- TurboKV-on-M5 note → tracking issue #175.
- --gpu-layers GPU timeout: reproduced on M5 as well as M6; tracked in #176
  with the fix in SharpAI/mlx-swift#17. CPU-resident MoE layers stay very slow
  after the fix (~0.4 tok/s prefill), so the note points 32 GB users at
  --stream-experts first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant