Skip to content

metal : split graphs into 4 command buffers on iOS - #275

Open
jasontitus wants to merge 2 commits into
PrismML-Eng:prismfrom
jasontitus:downstream/metal-ios-command-buffers
Open

jasontitus wants to merge 2 commits into
PrismML-Eng:prismfrom
jasontitus:downstream/metal-ios-command-buffers

Conversation

@jasontitus

Copy link
Copy Markdown

Overview

On iOS, a Metal command buffer that runs for about 5 s of GPU time while another GPU client (such as the display compositor) is waiting gets discarded, and the graph fails with Discarded (victim of GPU error/recovery) (00000005:kIOGPUCommandBufferCallbackErrorInnocentVictim) (llama_decode returns -3). The Metal backend encodes a graph as a small first command buffer plus n_cb = 1 more holding ~90% of the nodes, so a long prefill ubatch on a phone is one multi-second command buffer.

This PR makes the default n_cb 4 on iPhone-class OSes (iOS, iPadOS, visionOS, tvOS; not Mac Catalyst) and keeps 1 elsewhere; GGML_METAL_N_CB=1..8 overrides it. Output is unchanged: splitting only moves command-buffer boundaries.

It also fixes two latent issues that a default above 1 exposes:

  • with an abort callback set, graph_compute commits only command buffers 0 and 1 (later ones only when capturing). The main thread's buffer is index n_cb, so n_cb > 1 hung in synchronize. n_cb is now clamped to 1 while an abort callback is set.
  • ggml_metal_free released GGML_METAL_MAX_COMMAND_BUFFERS of the + 1 command buffers.

Additional information

iPhone 17 Pro Max (A19 Pro), Ternary Bonsai 2 27B PTQ1_0, 512-token prefill, every run starting at nominal temperature:

Command buffers Runs Failed Longest command buffer
default (1) 50 10 5-8 s (each failure: discarded after 5.0 s)
4 10 0 1.6-2.3 s
1, 256-token ubatches 10 0 3.0-4.3 s

pp512 with 4: 71.7 vs 71.4 tok/s. The decode cost of 4 on the phone is not measured yet; macOS is unchanged (the default stays 1 there).

M5 Max (macOS): full-model logits bitwise identical with GGML_METAL_N_CB 1, 4 and 8 (700 tokens); test-backend-ops MUL_MAT and FLASH_ATTN_EXT pass with 1 and 4; an abort-callback reproducer completes with 1, 4 and 8 (it hung with 4 before the clamp). iOS device build checked.

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: YES. Claude (Anthropic) helped with the implementation, testing, measurements and this description; I reviewed the change and own it.

jasontitus and others added 2 commits September 25, 2026 16:32
iOS discards a command buffer that has run for about 5 s of GPU time when another GPU client (such as the
display compositor) is waiting, and the graph fails with "Discarded (victim of GPU error/recovery)"
(kIOGPUCommandBufferCallbackErrorInnocentVictim). With the default of one extra command buffer, the one holding
~90% of a 512-token prefill graph of a 27B model runs 5-7 s on an iPhone 17 Pro Max and failed in 10 of 50
runs; each recorded failure was that command buffer, discarded after 5.0 s. With 4 (1.6-2.3 s each) none of
10 runs failed and pp512 speed was unchanged. Output is bitwise identical for 1, 4 and 8 (M5 Max, full model).

- default n_cb = 4 on iPhone-class OSes (iOS, iPadOS, visionOS, tvOS; not Mac Catalyst), 1 elsewhere;
  GGML_METAL_N_CB=1..8 overrides
- with an abort callback set, use 1: only command buffers 0 and 1 are committed on that path, so the main
  thread's buffer (index n_cb) would never run with n_cb > 1 and synchronize would hang
- ggml_metal_free releases all GGML_METAL_MAX_COMMAND_BUFFERS + 1 command buffers
- warn above 4 instead of above 2

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHqq1nTe46u7euASncLsee
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHqq1nTe46u7euASncLsee
jasontitus added a commit to jasontitus/llama.cpp that referenced this pull request Sep 26, 2026
…(PQ2_0 two columns) measured and ready

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHqq1nTe46u7euASncLsee
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant