Skip to content

GPU.deviceInfo(): select the device like MLX, without the window server - #498

Merged
davidkoski merged 1 commit into
ml-explore:mainfrom
Layr-Labs:deviceinfo-without-window-server
Oct 20, 2026
Merged

davidkoski merged 1 commit into
ml-explore:mainfrom
Layr-Labs:deviceinfo-without-window-server

Conversation

@davidtai

@davidtai davidtai commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Proposed changes

GPU.deviceInfo() calls MTLCreateSystemDefaultDevice() on every call. In a command-line process (not an app bundle), the first such call opens a window server session. That step can stall for many seconds.

A sample of a stalled process shows this stack:

GPU.deviceInfo()                      (Source/MLX/GPU+Metal.swift)
 MTLCreateSystemDefaultDevice
  SLSCreateDefaultMetalDevice
   SLSMainDisplayID
    SLSInitialize                      (dispatch_once)
     CGSDisplayInitialize / _CGSGetEventShmem
     TCCAccessPreflight

In the unified log, the process logs TCCAccessRequest for kTCCServiceListenEvent and a com.apple.SkyLight session line, and then nothing until the call returns. On an M5 Max, over 25 runs of the same command-line process, the gap from that SkyLight line to the next event was 0.35 to 0.89 s in 23 runs, 7.1 s once and 30.1 s once. In our case the call happened inside a timed step, so the 30 s stall failed the run.

MLX itself does not use the window server to pick its device. metal::load_device (mlx/backend/metal/device.cpp) takes the first device of MTL::CopyAllDevices() and uses MTL::CreateSystemDefaultDevice() only when that list is empty.

This PR:

  • selects the device the same way as metal::load_device: MTLCopyAllDevices().first, else MTLCreateSystemDefaultDevice(). MTLCopyAllDevices is macOS-only, so other platforms keep MTLCreateSystemDefaultDevice().
  • computes the value once per process (private static let). The device properties it reads (architecture, maxBufferLength, recommendedMaxWorkingSetSize, HW_MEMSIZE) do not change while the process runs.
  • keeps the public API and the returned fields the same.

Testing

  • Added MemoryTests.testDeviceInfo: on a GPU device, deviceInfo() reports a real architecture, a nonzero buffer size and working set size, and a second call returns the same values.
  • xcodebuild build-for-testing -scheme mlx-swift-Package -destination 'platform=macOS', then xctest for CmlxTests (1 passed), MLXTests (385 executed, 2 skipped, 0 failures) and MLXIntegrationTests (669 passed).
  • xcodebuild build -scheme mlx-swift-Package -destination 'generic/platform=iOS' succeeds.
  • A small command-line probe on an M5 Mac: MTLCopyAllDevices().first and MTLCreateSystemDefaultDevice() return the same device, architecture, maxBufferLength and recommendedMaxWorkingSetSize. The unified log for the MTLCopyAllDevices path has no TCC or SkyLight lines. The MTLCreateSystemDefaultDevice path logs TCCAccessRequest(kTCCServiceListenEvent) and a SkyLight session.

Checklist

Put an x in the boxes that apply.

  • I have read the CONTRIBUTING document
  • I have run pre-commit run --all-files to format my code / installed pre-commit prior to committing changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the necessary documentation (if needed)

🤖 Generated with Claude Code

GPU.deviceInfo() called MTLCreateSystemDefaultDevice() on every call. In a
command-line process the first such call opens a window server session
(SLSCreateDefaultMetalDevice -> SLSMainDisplayID -> SLSInitialize ->
CGSDisplayInitialize). That step can stall for many seconds.

Select the device the same way as metal::load_device in MLX: the first
device of MTLCopyAllDevices(), else MTLCreateSystemDefaultDevice(). Compute
the value once per process. The public API and the returned fields do not
change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
// device of MTLCopyAllDevices(), else the system default device. In a
// command-line process MTLCreateSystemDefaultDevice() opens a window
// server session, which can stall for several seconds.
#if os(macOS)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might also want to cover macCatalyst here too -- the same calls are available but it isn't os(macOS):

#if os(macOS) || targetEnvironment(macCatalyst)

Though if the primary concern is for command line tools, I am not sure you can create one that is macCatalyst.

@davidkoski davidkoski left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change looks good, but see comment on macCatalyst -- change that or let me know if you think it isn't needed.

Thank you!

@davidkoski

Copy link
Copy Markdown
Member

I am going to merge as-is -- the macCatalyst change can be made as another PR if needed, but given how those binaries work it may not be a useful case.

@davidkoski
davidkoski merged commit d0e62b2 into ml-explore:main Oct 20, 2026
10 of 11 checks passed
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.

2 participants