1. How to see the devices tinygrad sees
# since renderer from AMD and METAL conflict in device.py:17
# we put AMD before METAL in ALL_DEVICES = ["AMD", "METAL", "NV", "CUDA", "QCOM", "CL", "CPU", "DSP", "WEBGPU"]
python3 -m tinygrad.device
# tinygrad/device.py:418-444, runs a real Tensor per renderer
python3 -c "from tinygrad import Device; print(Device.DEFAULT)"
python3 -c "from tinygrad.device import Device; print(list(Device.get_available_devices()))"On your Mac mini right now:
DEFAULT: METAL
available: ['METAL', 'AMD', 'CL', 'CPU']
Selection logic is device.py:14 (ALL_DEVICES preference order: METAL, AMD, NV, CUDA, ...) and device.py:47-55 (_select_device — first one that constructs wins). Your 7900 XTX is detected as 1002:744c and opens fine:
iface: PCIIface pci_dev: APLRemotePCIDevice arch: gfx1100 renderer: HIPRenderer
2. What the PCIe part is in your setup
Your system_profiler output shows the 7900 XTX at Slot: Thunderbolt@5,0,0 with Link Status: Link down — the Mac has no PCIe BAR access to it. tinygrad works around this in three layers:
ops_amd.py:1014—AMD.ifaces = [KFDIface, PCIIface, USBIface, ...]. Your path isPCIIface.system.py:58-80pci_scan_bus— on OSX it walks IOKitIOPCIDeviceservices to find1002:744c(matches the device-id whitelist atops_amd.py:919).system.py:416-447APLRemotePCIDevice— this is the macOS-specific bit. It launches/Applications/TinyGPU.app(your "tinygpu") as a helper and talks to it over a Unix socket at$TMPDIR/tinygpu.sockwith a 13-command RPC (RemoteCmdatsystem.py:311-312):PROBE,MAP_BAR,CFG_READ/WRITE,MMIO_READ/WRITE,SYSMEM_READ/WRITE,MAP_SYSMEM_FD. That helper is what actually does the USB4/TB5 tunneling. Confirmed running: PID 29708.
So the real chain is: tinygrad → Unix socket RPC → TinyGPU.app → USB4/PCIe tunnel → ASMedia 246x → 7900 XTX.
3. The disk → 7900 XTX path
engine/realize.py:149-158 exec_copy has a four-way dispatch, and the zero-copy branch is gated:
elif src.device.startswith("DISK") and getattr(src.allocator.dev, 'fd', None) is not None \
and hasattr(dest.allocator, 'copy_from_disk') and src.nbytes >= 4096 and dest.allocator.supports_copy_from_disk:
dest.allocator.copy_from_disk(dest._buf, src._buf, src.nbytes)For AMD, supports_copy_from_disk = dev.has_sdma_queue (ops_amd.py:645), which is True on your card. So it takes the fast path: hcq.py:573-589 copy_from_disk drives DiskAllocator._copyout_sharded (ops_disk.py:98-139) with SDMA doing the DMA, so bytes go file → page cache → SDMA → VRAM without a full CPU memcpy. Note use_ioring at hcq.py:585 is False here since iouring is Linux-only (ops_disk.py:51), so it takes the plain readinto loop at ops_disk.py:104-113. METAL by contrast has no copy_from_disk and falls to the host memcpy at realize.py:157.
Single file, python extra/gpuprobe.py [section], sections run in order, no args = all. Imports only tinygrad, ctypes, struct, socket, os. Every section must be read-only: no _collect_interrupts(reset=True), no AM_RESET, no doorbell writes, no synchronize() calls that could block 30s. Where a check would mutate state, print the call you could make and move on.
Section devices — reproduces what python -m tinygrad.device does but without crashing:
Device._devices(theops_*.pyfilesystem glob,device.py:17) vsALL_DEVICES(device.py:14), showing the 6 that are never auto-selected (DISK,HIP,NPY,NULL,PYTHON,RDMA).- Replay
_select_device(device.py:47-55) by hand: for each name inALL_DEVICES, tryDevice[name], catch, record. Print which one wins and why. Never touchos.environ["DEV"]—device.py:53writes it as a side effect, which would poison child processes. - The
DEVgrammar:Target.parse(helpers.py:200-207) applied to the currentDEVvalue, showingdevice:renderer:arch+ifaceprefix + indices. _select_renderer(device.py:364-369) and_select_iface(device.py:371-379) resolved explicitly, listing the rejected candidates with their exception messages.
Section pci — get 1002:744c with zero macOS commands:
- Call
System.pci_scan_bus(0x1002, ((0xffff, (0x744c,)),))directly →['1002:744c']. On macOS this is IOKit only (system.py:60-71,IOServiceGetMatchingServices/IOServiceMatching(b"IOPCIDevice")), nosystem_profilerinvolved. - Show the full filter table from
ops_amd.py:919(0x74a1,0x74b5,0x744c,0x7480,0x7550,0x7551,0x7590,0x75a0,0x75a8) and which of your IDs match. - Then walk
system.py:82-90:list_devices→pci_probe_device→hcq_filter_visible_devices→ the class actually chosen. Print(APLRemotePCIDevice, '1002:744c')— i.e. the answer to "how do I get this from tinygrad". - Note the
pcibusstring is discarded atsystem.py:439(super().__init__(devpref, "usb4", sock=sock)), sodev_idis always 0 on macOS.
Section rpc — the Unix socket protocol, decoded from Python:
- Print the wire format: 33-byte request
<BIIQQQ(cmd, dev_id, bar, arg0, arg1, arg2), 17-byte reply<BQQ(status, resp0, resp1), matching the packed structs atextra/usbgpu/tbgpu/installer/Shared/server.c:35-36. Include the arg table per command. - Which of the 13
RemoteCmds TinyGPU actually implements:{MAP_BAR, MAP_SYSMEM_FD, CFG_READ, CFG_WRITE, RESET, MMIO_READ, MMIO_WRITE, RESIZE_BAR}.PROBE,PING,MAP_SYSMEM,SYSMEM_*returnstatus=1inserver.c— important, because it means you cannot enumerate over this socket, only overpci_scan_bus. - Socket path:
getenv("APL_REMOTE_SOCK", temp("tinygpu.sock"))(system.py:431) →/var/folders/66/.../T/tinygpu.sock. - Liveness probe that does not disturb tinygrad:
bind()returningEADDRINUSEmeans a server is listening. Then warn thatserver.c:274accepts one client at a time and closes extras silently, so do not open a second connection while tinygrad holds it. - The
_bulk_read/_bulk_writeasymmetry (system.py:394-399): reads send a header + receive header-then-body, writes send header + payload and get no reply.
Section helper — finding PID 29708 without ps:
- There is no pid file and
system.py:436discards thePopenobject.psutilis not a dependency;/procdoesn't exist;sysctl kern.procis unavailable on modern macOS (verified: returns ENOENT). - So:
ctypes.CDLL("/usr/lib/libproc.dylib")— note it loads from the dyld shared cache even thoughos.statsays ENOENT, so do not gate onctypes.util.find_library. Thenproc_listallpids+proc_pidpath, matching"/Applications/TinyGPU.app/Contents/MacOS/TinyGPU". - Two fallbacks that are more robust: the IOKit service check (
IOServiceGetMatchingService(kIOMainPortDefault, IOServiceNameMatching("tinygpu")) != 0proves the dext is loaded — all bindings exist intinygrad/runtime/autogen/iokit.py), and the flock attemp("pc_usb4.lock")which tells you tinygrad holds the client side. - Also print the
ensure_apppin (system.py:421, commitc0d024f9…) and whether the cached zip exists, so you know your app matches the client.
Section iface — the PCIIface / APLRemotePCIDevice / gfx1100 / HIPRenderer line you asked for, expanded:
iface=PCIIfacefromifaces[1](ops_amd.py:1014) afterKFD(Linux-only,/dev/kfd) fails.is_am()→True(ops_amd.py:1016), which is what setscan_recover=True.pci_dev=APLRemotePCIDevice,is_local()→False(system.py:256), which skipsreserve_vaatsystem.py:262and P2P mapping atsystem.py:293.arch=gfx1100, derived fromprops['gfx_target_version'](ops_amd.py:1022-1023) read out of the GPU over MMIO — not hardcoded.- Dump
iface.props(ops_amd.py:941-944):simd_count,cu_per_simd_array,lds_size_in_kb,max_waves_per_simd, etc., plus the derivedcu_cnt/se_cnt/wave_cntfromops_amd.py:1027-1031. - BAR layout:
vram_bar=0,bar_info(0)over the RPC,large_barstate. - Renderers: instantiate each of
HIPRenderer,AMDLLVMRenderer,HIPCCRenderer(ops_amd.py:1064) in isolation and report pass/fail with the real exception, instead of letting one crash kill the process.
Section copies — proves the DISK→SDMA claim instead of asserting it:
- Read the exact gate at
realize.py:154-155term by term and print each value:src.device.startswith("DISK"),src.allocator.dev.fd is not None,hasattr(dest.allocator,'copy_from_disk'),src.nbytes >= 4096,dest.allocator.supports_copy_from_disk. This is the honest way to answer "is the fast path actually taken" — including showing it is not taken for files under 4 KiB. - Then monkeypatch nothing and just report:
has_sdma_queue(ops_amd.py:1062),hw_copy_queue_t(hcq.py:396),supports_copy_from_disk/supports_transfer(ops_amd.py:645),AMD_DISABLE_SDMA(ops_amd.py:1126),max_copy_size(ops_amd.py:1060). - Bounce ring:
len(allocator.b),allocator.b[0].size,b_next, and the1 << 64in-flight sentinel count — fromhcq.py:527-530and the_get_temp_bufhandshake athcq.py:574-579. use_ioring: evaluatetype(allocator.b[0].cpu_view()) is MMIOInterfaceathcq.py:585and print the class name. ExpectFalsehere (macOS:io_uringnever initializes atops_disk.py:51), so you land in thereadintoloop atops_disk.py:104-113.- Drive
_copyout_shardedwith a fake_get_free_bufand a stub allocator to show the 4-tuple(batch_info, dst_off, src_off, copy_size)sequence for a chosen size, including the leadingminor_offsetnon-zero case. This runs entirely in Python, no GPU, no device state touched — safe even when the card is wedged. - Flag the two unbounded spins that explain your hangs:
ops_disk.py:106while (copy_batch := _get_free_buf()) is None: passandops_amd.py:546while ... sdma_queue.put_value + total_bytes - sdma_queue.read_ptr[0] > sdma_queue.ring.nbytes: pass.
Section hang — read-only hang diagnosis:
- Print
dev.error_state(hcq.py:415),dev.iface.dev_impl.is_err_state(amdev.py:222), and explain thaterror_stateis set at exactly two sites (hcq.py:429on wait timeout,ops_amd.py:761on doorbell fault) and cleared at exactly one (ops_amd.py:970). - Print the three scratch-register state flags that decide boot behavior at
amdev.py:185-191:regSCRATCH_REG7(AM version),regSCRATCH_REG6(did the last session finalize cleanly),gmc.pf_status_reg('GC'). Explain thatregSCRATCH_REG6 != 0means the last process died hard, forcing a full re-boot. - Explain the detection asymmetry that explains your manual reboots:
can_recover=self.is_am()(ops_amd.py:1068) is only consulted for a caller-suppliedtimeoutathcq.py:427; the real detector is the 30 sHCQDEV_WAIT_TIMEOUT_MSdefault athcq.py:282. AndAMDev.recover(amdev.py:277-284) resets GFX only — neverAM_SDMA. So a hung SDMA packet survives recovery,is_err_statereads clean,recover()returns True,error_stategets cleared atops_amd.py:970, and the nextsynchronize()hangs again. That is the mechanism behind "must reboot by hand", and it is why this script does not attempt recovery. - Note
ip.py:491:SDMA_TRAPinterrupt sources arecontinued, so a trapping SDMA packet produces no log output at all. - Exit with a clear "REBOOT REQUIRED" verdict instead of trying to fix it.
Run the script section by section against the live card. Since the HIP renderer currently SIGBUSes in comgr on this machine, the iface and copies sections must not compile any kernel — they only read device properties, so they work even while compute is broken. Confirm pci prints 1002:744c and iface prints gfx1100 + PCIIface + APLRemotePCIDevice, matching what we established earlier. Then run the existing suites: python -m pytest test/device/test_hcq.py -x -q -n12, python -m mypy tinygrad/, python -m ruff check ..
- this is not true, it cannot test if the device is not up and running: Fix explicit renderer suffixes such as
Device["AMD:HIP"]failing during interface selection, and add a focused regression test. - Create a python script against every part of the setup ?
- Create
extra/gpuprobe.py; user will run the resulting tests.
- Current failure:
ValueError: invalid literal for int() with base 10: 'HIP'. - Bug:
/Users/plagache/workspace/tinyagent/tinygrad/device.py:372parses every second:component as an integer. Compiled.__init__attinygrad/device.py:348already uses the required.isdigit()guard.- Regression introduced by commit
e7bf2a811. - Test belongs in
test/null/test_device.py; it should exercise parsing without compiling a HIP kernel or relying unnecessarily on live hardware. - Test numeric suffixes remain integers and renderer suffixes default
device_idto0. - User requires observe-only behavior: never attempt reset/recovery or reboot without asking; their macOS/TinyGPU AMD setup can hang.
- Confirmed hardware path:
PCIIface→APLRemotePCIDevice→$TMPDIR/tinygpu.sock→ TinyGPU.app; AMD ID1002:744c, archgfx1100, rendererHIPRenderer.
- Confirmed
Device["AMD:HIP"],Device["AMD:LLVM"], andDevice["AMD:HIP"]explicit-index construction fail atdevice.py:372. - Located existing guarded parsing at
device.py:348. - Located appropriate test home and existing conventions in
test/null/test_device.py. - No files have been changed yet.
- Apply the minimal
.isdigit()-style guard inCompiled._select_iface. - Add a hardware-free regression test around
_select_ifacedevice-ID parsing if practical.
- HIP kernel compilation currently crashes on this Mac with
Fatal Python error: Bus errorattinygrad/runtime/support/compiler_amd.py:65; tests must avoid invoking kernel compilation. - Live AMD tests may also require TinyGPU.app and exclusive
/tmp/am_usb4.lock.
- Patch
Compiled._select_ifaceintinygrad/device.pyto parse a suffix as a device ID only when numeric, otherwise use0. - Add focused tests in
test/null/test_device.py, then show the user the exact test command without running the full suite.
/Users/plagache/workspace/tinyagent/tinygrad/device.py:_select_ifacebug at line 372 and guarded parsing at line 348./Users/plagache/workspace/tinyagent/test/null/test_device.py: canonicalization and device-target tests; proposed regression-test location./Users/plagache/workspace/tinyagent/tinygrad/runtime/ops_amd.py: AMD calls_select_ifaceat line 1020 and defines renderer/device setup./Users/plagache/workspace/tinyagent/tinygrad/runtime/support/system.py:System.pci_scan_bus,APLRemotePCIDevice, and Unix-socket RPC./Users/plagache/workspace/tinyagent/tinygrad/runtime/support/compiler_amd.py: current HIP/comgr SIGBUS blocker.
tinygrad/device.py:372 — _select_iface parsed any second : component as an integer, so a renderer suffix blew up:
Device["AMD:HIP"] -> ValueError: invalid literal for int() with base 10: 'HIP'
Now it mirrors the guard already used for self.device_id in Compiled.__init__ (device.py:348), falling back to 0 when the token isn't numeric:
self.device_id = (int(idx) if ":" in device and (idx:=device.split(":")[1]).isdigit() else 0)_select_iface is shared with NVIDIA (ops_nv.py:591), and NV had the identical bug — I confirmed Device["NV:CUDA"] raised the same ValueError pre-fix. So the fix covers both, and I added a matching NV test rather than leaving half the affected classes of device broken.
Two tests, one per backend, following the existing test_dev_id_out_of_range subprocess convention. They use DEV=MOCKKFD+AMD / DEV=MOCK+NV, so they need no real GPU — MOCKKFD+AMD is already used in CI at .github/workflows/test.yml:519. This also sidesteps the comgr SIGBUS issue, since no kernel is compiled.
Each asserts both directions: non-numeric suffixes resolve to device_id=0, and numeric indices still work (AMD:1:HIP → 1, NV:1:CUDA → 1), guarding against an over-broad fix that broke indexing.
I confirmed each test genuinely catches the regression by stashing the fix and re-running:
| pre-fix | post-fix | |
|---|---|---|
| AMD test | FAILED (3 subtests), ValueError: 'HIPCC' |
ok |
| NV test | FAILED (2 subtests), ValueError: 'CUDA' |
ok |
Also green: full test/null/test_device.py under DEV=AMD (22 passed, 6 skipped), under DEV=NULL (22 passed, 9 skipped), ruff check ., and mypy tinygrad/ (217 files).
Two notes, no action taken:
- pytest isn't installed in any interpreter here, so I ran via
python3 -m unittest. On your side:DEV=AMD python3 -m pytest test/null/test_device.py -k iface_with_renderer_suffix. - Unrelated pre-existing staged file:
test/runtime/test_remote.pywas already staged in the index when I started (git statusshowsA) and isn't mine. I left it untouched. HEAD is also detached at1691120a6— worth knowing before you commit.