Repository navigation
metal: scale rsrc_limit fallback with hw.memsize - #3512
Closed
electron-rare wants to merge 1 commit into
Closed
electron-rare wants to merge 1 commit into
electron-rare wants to merge 1 commit into
Conversation
Replace the hardcoded 499000 fallback with a 4-tier table indexed on hw.memsize so that only large unified-memory devices get the uplift: memsize >= 384 GB -> 3x (M3 Ultra 512 GB) memsize >= 64 GB -> 2x (64-128 GB Max/Studio) memsize >= 24 GB -> 1.5x (24-36 GB M-series Pro) memsize < 24 GB -> 1x stock (MBA, Mini, MBP 16 GB) The sysctl path is unchanged: an explicit 'sudo sysctl -w iogpu.rsrc_limit=...' still wins. Only the kernel 'use default' sentinel (rsrc_limit == 0) goes through the tier table. Rationale: on 8/16 GB machines the handle ceiling is never the binding constraint (physical memory saturates first), so raising the fallback there has no benefit and adds kernel allocator pressure. On 32 GB+ Pro machines and Ultra-class systems the 499K cap truncates Metal command buffers during sustained bf16 fine-tuning of 35B+ models.
zcbenz
reviewed
May 11, 2026
zcbenz
left a comment
Member
There was a problem hiding this comment.
The referenced code is used to get the limit not set it.
To change it you can try calling sudo sysctl iogpu.rsrc_limit but I'm not sure if it would work.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The current fallback for
iogpu.rsrc_limitis hardcoded to499000inbackend/metal/device_info.cpp. That value is appropriate for small-RAM Apple Silicon devices, but it truncates Metal command buffers during sustained bf16 fine-tuning of large models on Pro/Max/Ultra-class machines.This PR replaces the hardcoded fallback with a 4-tier table indexed on
hw.memsize, which is already queried two lines above:hw.memsize< 24 GB≥ 24 GB≥ 64 GB≥ 384 GBThe sysctl path is unchanged: when
iogpu.rsrc_limitis configured explicitly (e.g.sudo sysctl -w iogpu.rsrc_limit=...), that value still wins. The tiered fallback only kicks in when the kernel reports0, which is the documented "use default" sentinel.Why
The 499 K handle ceiling becomes the binding constraint when training or running inference on dense bf16 models that approach unified-memory saturation:
MTLResourceobjects at peak.Conversely, on 8 / 16 GB machines the handle ceiling is not the binding constraint — physical memory saturates first. A typical Q4 7 B model + KV cache reaches ~6 GB resident on a 16 GB MBA before approaching even 30 K active resources, ~1 / 16 of the stock ceiling. Raising the fallback there costs ~10–20 MB of kernel allocator state per tier with zero observable benefit.
Tiering by
hw.memsizekeeps the behavior local to the machines that actually need it, and leaves the defaults untouched for the dominant 8 / 16 GB MacBook installed base.Why these thresholds?
The boundaries map 1:1 to shipping Apple Silicon SKUs rather than arbitrary GB cutoffs:
A continuous formula like
clamp(1 + memsize / 256GB, 1, 3)was considered but rejected as harder to reason about — discrete tiers are explicit and a user can read the table to know what their machine gets.Test plan
Validated on real hardware via a standalone probe replicating the tier logic:
hw.memsizeAdditional coverage by symmetric
if/elsereasoning:sudo sysctl -w iogpu.rsrc_limit=N→ returnsN(sysctl path wins).Coordination with
mx.set_cache_limit()The two limits are orthogonal:
mx.set_cache_limit(bytes)controls the GPU memory allocator cache (frontend, runtime, byte budget).rsrc_limitcontrols the Metal resource handle count (backend, build-time, object count).Both are sometimes needed in different combinations. Happy to add a paragraph in
docs/usage/memory_management.rstif reviewers think it would help.Precedent in this codebase
PR #1064 ("Change initial memory limits and add memory size to device info") introduced
memsizeexposure intodevice_info.cppprecisely so downstream limits can scale with hardware. This PR is the natural follow-through for thersrc_limitfallback that was added by PR #1718 ("Track resource limit and throw if exceeded"). The literalrsrc_limit = 499000;has been unchanged since that PR landed; this is the first proposed scaling.Related user-reported failures
The exact
Resource limit (499000) exceededceiling has been hit and reported across the ecosystem with no upstream fix in flight:mlx_lm.serverdistributedgenerate()crashes on Metal OOM instead of recovering gracefully499000error on M4 Pro 48 GBToday, every workaround in the wild is either
sudo sysctl -w iogpu.rsrc_limit=…ormx.set_wired_limit()(a different lever — bytes budget, not handle count). This PR offers the in-tree, automatic alternative.Validation status
Hardware-validated on real machines:
hw.memsize = 549755813888, 512 GB) → 3× tier →rsrc_limit = 1497000✓hw.memsize = 17179869184, 16 GB) → stock tier →rsrc_limit = 499000✓ (no behavior change on small-RAM machines)Benchmarks (M3 Ultra 512 GB, macOS 26.4 / Darwin 25.4.0, mlx 0.31.2 + mlx-lm 0.31.3)
Apertus 70B BF16 inference on a single Mac Studio M3 Ultra, with
mx.set_memory_limit(440 * 1024**3)to leave headroom for the OS. The machine is in its pre-PR state (mx.device_info()["resource_limit"] == 499000).resource_limitreported bymx.device_info()Reproducer (the full script is checked in at
KIKI-Mac_tunner/lib/mlx_bench_3512.py):Handle budget — why 499 000 is the binding ceiling
Apertus 70B is a 64-layer dense transformer (BF16, ~141 GB on disk). A single forward pass keeps live:
At the 2 K-context point measured here we sit at roughly
5 000 + 64*8 + 128*2048 ≈ 267 Klive handles — already more than half of the 499 K ceiling, and the test completed cleanly. Extrapolating linearly: a 4 K-context inference run lands at ~530 K handles, which exceeds the stock ceiling and is exactly the regime where users reportResource limit (499000) exceeded(see the cluster of upstream issues linked above). The 3× tier introduced by this PR (1 497 000) gives a ~5.6× headroom over the 2 K-context working point and ~2.8× over the 4 K crossover, matching the empirical budget for 70 B–class bf16 workloads at conversational context lengths.sudo sysctl -wThe PR's 3× ceiling is therefore tight-but-sufficient for the dominant use case (bf16 70 B at 4–8 K context) and intentionally does not cover indefinitely-long contexts, for which the existing manual sysctl override remains the correct escape hatch.
Full benchmarks above were collected on
2026-05-11; the M3 Ultra was reachable over Tailscale and was simultaneously hosting an unrelated Mistral-Medium-3.5-128B Q8 server (~116 GiB resident), so the load-time and peak-memory numbers here are conservative — a freshly booted machine would see slightly better peak headroom.Happy to also switch to a continuous formula derived from
recommendedMaxWorkingSetSize()if reviewers prefer that direction over the discrete tier table.