Metal capability unlock
Enable a configured, tested Metal capability profile for one workload inside a Lume macOS VM.
Lume VMs use Apple's paravirtualized GPU bridge, but a stock macOS guest can report conservative Metal capabilities. Applications that query those values may skip newer GPU paths even when the paravirtualized device can execute the work.
This guide builds a small MIT-licensed shim and activates it for one process. The shim changes selected capability answers; it does not pass a physical GPU to the guest, patch the host, or change the guest kernel. If you're looking for computer-use automation instead, start with Cua Driver or Cua Sandbox.
Read the technical writeup for the mechanism, benchmark evidence, and architectural context. Apple's Metal capability tables and feature-detection guide explain how applications select behavior from reported device support.
What the shim changes#
The tested profile can raise two values returned to the injected process:
- the result of
supportsFamily:through a configured Apple-family ceiling; and - the reported maximum threadgroup memory.
The release candidate contains no timing or clock interposition, mesh substitution, ray-tracing
override, private feature-profile hook, argument-layout guard, or pipeline-compilation fallback. It
leaves the Common, Mac, and Metal family ranges unchanged. A missing, out-of-range, or malformed
LUME_METAL_APPLE_FAMILY_MAX leaves the process on its stock capability path.
Reported support is not proof that every Metal API associated with a family works through virtualization. Test each workload and host/guest version independently.
Requirements#
- Apple Silicon host
- A Lume macOS VM
- Xcode Command Line Tools on the machine where you build the shim
- A workload you control; hardened or platform-protected executables may reject injected libraries
Our release-candidate validation used Lume 0.5.1, an Apple M1 Ultra host running macOS 26.6.1, and the public Tahoe Cua guest image at macOS 26.5.2. Other combinations are experimental until tested.
Build and verify the shim#
Clone the Cua repository on an Apple Silicon Mac, then build the architecture-specific dylibs and capability probe:
git clone https://github.com/trycua/cua.git
cd cua/libs/lume/metal-capability-shim
./Scripts/build.sh
./Scripts/verify.shverify.sh checks the architectures, ad-hoc code signatures, absence of research-only timing and
compatibility symbols, and the generated checksums. The output is written to dist/:
dist/
├── LumeMetalCapabilities-arm64.dylib
├── LumeMetalCapabilities-arm64e.dylib
├── SHA256SUMS
└── metal-capabilitiesMost guest workloads are arm64. Check the target executable before selecting a dylib:
lipo -archs /absolute/path/to/your-workload
shasum -a 256 -c dist/SHA256SUMSEnable the host capability path#
The preference below applies to VMs launched by your macOS user. Stop the VM before changing it so the graphics device is recreated with the requested feature level:
lume stop my-vm
defaults write com.apple.gpusw.ParavirtualizedGraphics \
ForceUnrestrictedDeviceFeatureLevel -bool true
defaults read com.apple.gpusw.ParavirtualizedGraphics \
ForceUnrestrictedDeviceFeatureLevel
lume run my-vmThe defaults read command should print 1.
To restore the stock host setting later, stop the VM, delete the preference, and start the VM again:
lume stop my-vm
defaults delete com.apple.gpusw.ParavirtualizedGraphics \
ForceUnrestrictedDeviceFeatureLevel
lume run my-vmCopy and verify the artifact in the guest#
Copy the arm64 dylib and probe into a stable, workload-specific directory. Replace the local path
below with the path to your Cua checkout:
lume ssh my-vm "mkdir -p '/Users/lume/.local/share/lume/metal-capabilities'"
VM_IP=$(lume get my-vm --format json | jq -r '.[0].ipAddress')
scp ./dist/LumeMetalCapabilities-arm64.dylib ./dist/metal-capabilities \
"lume@${VM_IP}:/Users/lume/.local/share/lume/metal-capabilities/"For a standard unattended Lume image, scp prompts for the default guest password, lume. Use the
credentials configured for your image if they differ.
Record the local checksums, then compare them with the guest copies:
shasum -a 256 dist/LumeMetalCapabilities-arm64.dylib dist/metal-capabilities
lume ssh my-vm \
"shasum -a 256 \
'/Users/lume/.local/share/lume/metal-capabilities/LumeMetalCapabilities-arm64.dylib' \
'/Users/lume/.local/share/lume/metal-capabilities/metal-capabilities'"Verify stock and unlocked capability reporting#
Run the probe without injection first:
lume ssh my-vm \
"'/Users/lume/.local/share/lume/metal-capabilities/metal-capabilities' 1009"Then run the same probe with the tested profile:
lume ssh my-vm \
"DYLD_INSERT_LIBRARIES='/Users/lume/.local/share/lume/metal-capabilities/LumeMetalCapabilities-arm64.dylib' \
LUME_METAL_APPLE_FAMILY_MAX=1009 \
'/Users/lume/.local/share/lume/metal-capabilities/metal-capabilities' 1009"In our Tahoe guest, supportsFamily:1009 changed from false to true, and maximum threadgroup
memory changed from 32,768 to 65,536 bytes. Your result may differ on another host or guest. Stop if
the injected process crashes, reports Metal errors, or behaves incorrectly.
The tested command uses these values:
| Variable | Default when active | Behavior |
|---|---|---|
LUME_METAL_APPLE_FAMILY_MAX | required | Answer supportsFamily: through this Apple-family ceiling. The tested value is Apple 9 (1009). |
LUME_METAL_MAX_THREADGROUP_MEMORY | 65536 | Raise maximum threadgroup memory to at least this many bytes. |
LUME_METAL_RECOMMENDED_WORKING_SET_SIZE | unchanged | Raise the reported working-set size only when explicitly set. |
Keep the defaults unless you have isolated evidence for another profile. Do not substitute a Common,
Mac, or Metal-family enum: the shim intentionally accepts only Apple-family values from 1001 to
1999.
Run one workload#
Injection is per-process, so only the selected workload and its child processes see the changed answers:
lume ssh my-vm \
"DYLD_INSERT_LIBRARIES='/Users/lume/.local/share/lume/metal-capabilities/LumeMetalCapabilities-arm64.dylib' \
LUME_METAL_APPLE_FAMILY_MAX=1009 \
/absolute/path/to/your-workload first-argument"Do not use launchctl setenv DYLD_INSERT_LIBRARIES ...; that applies injection across the login
session and can affect unrelated applications.
Run a workload with launchd#
For a long-running inference server, renderer, or worker, create a per-user LaunchAgent inside the guest. The environment applies only to that program and its child processes.
Save this as
/Users/lume/Library/LaunchAgents/com.trycua.lume.metal-workload.plist, replacing the executable and
arguments with absolute paths that exist in the guest:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.trycua.lume.metal-workload</string>
<key>ProgramArguments</key>
<array>
<string>/absolute/path/to/your-workload</string>
<string>first-argument</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>DYLD_INSERT_LIBRARIES</key>
<string>/Users/lume/.local/share/lume/metal-capabilities/LumeMetalCapabilities-arm64.dylib</string>
<key>LUME_METAL_APPLE_FAMILY_MAX</key>
<string>1009</string>
</dict>
<key>StandardOutPath</key>
<string>/Users/lume/Library/Logs/Lume/metal-workload.stdout.log</string>
<key>StandardErrorPath</key>
<string>/Users/lume/Library/Logs/Lume/metal-workload.stderr.log</string>
</dict>
</plist>Each ProgramArguments entry is one argument. Remove first-argument if the program takes no
arguments, or add one <string> per argument. Do not combine the executable and arguments into one
string.
Create the required directories, validate the plist, and start the LaunchAgent from a logged-in guest session:
mkdir -p "$HOME/Library/LaunchAgents" "$HOME/Library/Logs/Lume"
PLIST="$HOME/Library/LaunchAgents/com.trycua.lume.metal-workload.plist"
SERVICE="gui/$(id -u)/com.trycua.lume.metal-workload"
plutil -lint "$PLIST"
launchctl bootstrap "gui/$(id -u)" "$PLIST"
launchctl kickstart -k "$SERVICE"
launchctl print "$SERVICE"Inspect the service output:
tail -f "$HOME/Library/Logs/Lume/metal-workload.stdout.log"
tail -f "$HOME/Library/Logs/Lume/metal-workload.stderr.log"If the program fails after injection, unload the service and run the executable directly without the shim before debugging further:
launchctl bootout "gui/$(id -u)/com.trycua.lume.metal-workload"
/absolute/path/to/your-workload first-argumentRemove the configuration#
For a one-shot command, omit DYLD_INSERT_LIBRARIES and all LUME_METAL_* variables the next time
you start the workload.
For the LaunchAgent:
SERVICE="gui/$(id -u)/com.trycua.lume.metal-workload"
PLIST="$HOME/Library/LaunchAgents/com.trycua.lume.metal-workload.plist"
launchctl bootout "$SERVICE"
mv "$PLIST" "$HOME/.Trash/com.trycua.lume.metal-workload.plist"Restore the host preference with the earlier defaults delete sequence if you no longer need the
unrestricted feature level.
Limitations#
- Experimental and version-sensitive. The shim relies on private guest Metal implementation details that may change in any macOS release.
- Per-process, not global. Only an injected process and its children receive changed answers.
- Configured profile, not hardware discovery. The shim raises selected Apple-family values; it does not mirror every property of the physical GPU.
- Narrow test scope. The current release-candidate evidence covers a capability probe, two llama.cpp workloads, and one MLX-LM compatibility run on the documented M1 Ultra/Tahoe configuration. MLX-LM was already fast in the stock VM and showed no material uplift.
- Reported support is not complete support. A positive family response does not establish that every shader, renderer, framework, or API in that family works correctly in the VM.
- Still a VM. Existing
Virtualization.frameworklimits remain.