Compare commits

..
Author SHA1 Message Date
geohot 6c4e1bde6d fixes 2026-09-06 09:26:14 -07:00
geohot 0351dd169e ugh 2026-09-06 09:01:47 -07:00
geohot c33066d6f9 simpler 2026-09-06 08:30:32 -07:00
geohot 9fd8636e6e revert that 2026-09-06 08:19:50 -07:00
geohot 03d5102fc9 Fix pending view assignments and restrict functional setitem undo 2026-09-06 08:07:47 -07:00
geohot 5892110562 Extract independent storage and precompiled-call improvements 2026-09-06 08:07:47 -07:00
geohot 7c672d4022 Extract master-compatible CALLIFY test changes 2026-09-06 08:07:47 -07:00
chenyuandGitHub f5528f3eb5 support int BITCAST in validate (#18005) 2026-09-06 10:46:28 -04:00
nimlgenandGitHub 2b787196b3 hcq2 core usb (#18003)
* HCQ2: add batch and memory lowering hooks

* HCQ2: revert range renumbering changes

* HCQ2: keep extraction limited to core runtime changes

* x

* x

* UOp: preserve enclosing ranges in external calls and conditional ends

* UOp: remove conditional END comment

* HCQ2: move UOp range fixes to a separate branch
2026-09-06 15:24:33 +03:00
nimlgenandGitHub f34f308b61 ext calls preserve rngs (#18004) 2026-09-06 15:24:06 +03:00
nimlgenandGitHub 6a6c3042f4 deps: rm disjoint ranges on writes (#18002) 2026-09-06 14:54:54 +03:00
qazalandGitHub 5ae6526d47 add simple profiler test (#18001)
* add simple profiler test

* dev cpu err
2026-09-06 14:10:16 +09:00
chenyuandGitHub 020c7a14fd more validate cleanup (#18000) 2026-09-06 00:17:50 -04:00
chenyuandGitHub f9ae840f91 fix validate for casted index (#17998) 2026-09-05 23:11:19 -04:00
chenyuandGitHub 371ac77173 fix casted index gather [pr] (#17997) 2026-09-05 22:53:40 -04:00
George HotzandGitHub 1d878ac67a Fix independent AMD and mock NV regressions with test coverage (#17995)
* Fix independent AMD and mock NV regressions with test coverage

* better fix

* fixes
2026-09-05 17:30:15 -07:00
chenyuandGitHub 7e14f3690d more no-op x86 deletions [PR] (#17996) 2026-09-05 18:33:49 -04:00
geohot 479e077ecf hotfix: fix am_smi + tell agents to not insert kernel modules 2026-09-05 14:46:16 -07:00
chenyuandGitHub 2731aa27f7 failing tests for X86 wait_loops (#17994)
issue with LOOP_CMP
2026-09-05 17:26:17 -04:00
George HotzandGitHub 405f292dae fixes for navi 32 + memory savings (#17974)
* fixes for navi 32

* dynamic tmr

* Revert "dynamic tmr"

This reverts commit f2dd36be71.

* minimum change

* scratch reg 5
2026-09-05 13:16:33 -07:00
nimlgenandGitHub c84876fdd2 move nv to hcq2 (#17970)
* env

* x

* Dx

* Dx

* x

* Dx

* x

* x

* x

* x

* cl

* x

* cl
2026-09-05 22:54:55 +03:00
nimlgenandGitHub 1f3c0ac89c hcq2 tests: account staging kernels (#17993) 2026-09-05 20:30:23 +03:00
nimlgenandGitHub a609a0a29d mocknv: respect sema sizes (#17992) 2026-09-05 20:24:47 +03:00
nimlgenandGitHub 24af3a0941 modernize speed_v_theoretical (#17991) 2026-09-05 20:24:35 +03:00
chenyuandGitHub ac40497945 fix x86 copy spec [PR] (#17990) 2026-09-05 13:13:22 -04:00
nimlgenandGitHub b536514c83 hcq2 schedule cache (#17986)
* hcq2: cache small eager schedules

* fix

* x

* x

* lac
2026-09-05 18:23:35 +03:00
chenyuandGitHub 82bd6d5476 delete dead x86 codes [PR] (#17985) 2026-09-05 10:45:51 -04:00
nimlgenandGitHub 226556ddf3 remove hcq1 remote for now (#17984) 2026-09-05 13:35:28 +03:00
nimlgenandGitHub 33cd373ad3 hcq2: buffer copy with args (#17983) 2026-09-05 13:27:06 +03:00
nimlgenandGitHub 707d87e97b hcq2: simpler link (#17982) 2026-09-05 13:11:36 +03:00
nimlgenandGitHub ded106b183 more bitcasted buf (#17981) 2026-09-05 12:23:45 +03:00
nimlgenandGitHub 39e246848c hcq2: fix some leaks (#17980)
* hcq2: fix some leaks

* x

* fixed
2026-09-05 10:10:45 +03:00
wozeparrotandGitHub 8e9c929a51 gptoss: fp8 lmhead (#17979)
* gptoss: fp8 lmhead

* clean: function imports
2026-09-05 01:06:58 -04:00
chenyuandGitHub 0dc55feddc weak.py cast_consts cleanup [PR] (#17978) 2026-09-04 23:21:48 -04:00
sirhcmandGitHub 0319b1e75f qcomcl: migrate sysfs url (#17975) 2026-09-04 20:22:43 -04:00
chenyuandGitHub aaf76ca406 fix regalloc crash on x86 wait loops (#17973) 2026-09-04 19:04:12 -04:00
George HotzandGitHub 6fd714d069 switch usb fast path to one byte fence to prevent tearing (#17972) 2026-09-04 14:53:24 -07:00
George HotzandGitHub e8c8ba1c77 Add regression coverage for assignment and callify (#17971)
* Add regression coverage for assignment and callify

* Remove unnecessary SPEC override from assignment regression test
2026-09-04 14:06:04 -07:00
George HotzandGitHub 4f4f8e4f95 fix race condition in fast USB path (GPT-6) (#17969)
* fix race condition in fast USB path (GPT-6)

* don't lose all speed

* junk test

* simpler

* a second bug gpt-6 found
2026-09-04 13:15:39 -07:00
ben fattoriandGitHub 39f9bd0461 amd_custom_kernels_supported requires HIPRenderer (#17968) 2026-09-04 12:39:14 -07:00
George HotzandGitHub e1ba1755b7 amdflash bugfixes (#17967) 2026-09-04 11:44:48 -07:00
George HotzandGitHub 4f44116bd6 amdflash tools (gpt 5.6) (#17966) 2026-09-04 10:18:34 -07:00
George HotzandGitHub 5231b5274c disk clone at tensor time + elf bugfix (#17964)
* to(DISK) inserts a clone: the disk buffer is the storage of the copied value

* setitem: only unwrap self-referential stores (__iadd__), not clone storage

* delete the on_disk half of disk_copy_is_buffer: copies to disk persist via clone at tensor time (to(DISK), shard of creation devices)

* specific

* fix to_elf signature slots: compact in globals order, not raw call-arg positions

kernels that touch a sparse subset of a call's buffers bake sparse call positions
(e.g. (0, 2)) into the ELF signature. dense calls hide it; a prior call consuming
unique slots exposes it. CL's binder indexes the compact bufs list by sig slot, so
bufs[2] overruns a 2-element list (IndexError in ops_cl on multi-test runs, e.g.
test_setitem_consecutive_inplace_operator after test_assign_add)
2026-09-04 10:09:26 -07:00
chenyuandGitHub 01647028fb assign fixup (#17963) 2026-09-04 12:04:18 -04:00
George HotzandGitHub 404cda437a new precompile tests (#17960)
* add new precompile tests

* relax that
2026-09-04 07:53:55 -07:00
chenyuandGitHub 6c26eaf724 trim and clean up test_dtype (#17961) 2026-09-04 10:51:03 -04:00
qazalandGitHub a3e85c297a bump beam cache (#17959) 2026-09-04 23:45:10 +09:00
77 changed files with 2352 additions and 1227 deletions
+3 -19
View File
@@ -319,7 +319,7 @@ jobs:
fail-fast: false
matrix:
dev: ['METAL', 'AMD', 'NV']
timeout-minutes: 10
timeout-minutes: 11
defaults:
run:
shell: bash -e -o pipefail {0}
@@ -436,13 +436,7 @@ jobs:
- name: UsbGPU tiny tests
run: GMMU=0 DEV=USB+AMD python3.11 test/test_tiny.py
- name: UsbGPU copy speeds
run: SIZE=64000000 PYTHONPATH=. GMMU=0 DEV=USB+AMD python3.11 test/external/external_test_usb_asm24.py TestDevCopySpeeds
- name: UsbGPU (USB4/TB) install script
run: sh extra/setup_tinygpu_osx.sh
- name: UsbGPU (USB4/TB) boot time
run: DEBUG=3 DEV=PCI+NV:NAK time python3.11 test/test_tiny.py TestTiny.test_plus
- name: UsbGPU (USB4/TB) tiny tests
run: DEV=PCI+NV:NAK python3.11 test/test_tiny.py
run: SIZE=64000000 PYTHONPATH=. GMMU=0 DEV=USB+AMD python3.11 test/external/external_test_usb_asm24.py
testcomma:
strategy:
@@ -562,7 +556,7 @@ jobs:
- name: openpilot run_pickle big_driving_supercombo
run: BENCHMARK_LOG=usbgpu_openpilot_big_driving_supercombo_run_pickle RUN_PICKLE=1 PICKLE_OOB=1 PYTHONPATH="." GMMU=0 DEV=USB+AMD ASSERT_MIN_STEP_TIME=50 python3 examples/openpilot/compile3.py - openpilot.pkl
- name: Test copy speeds
run: SIZE=64000000 PYTHONPATH=. GMMU=0 DEV=USB+AMD python3 test/external/external_test_usb_asm24.py TestDevCopySpeeds
run: SIZE=64000000 PYTHONPATH=. GMMU=0 DEV=USB+AMD python3 test/external/external_test_usb_asm24.py
driverbenchmarks:
name: PCI Driver Benchmark (DEV=${{ matrix.dev }})
@@ -629,16 +623,6 @@ jobs:
- name: Run 10 MLPerf Bert training steps (1 gpu)
# TODO: remove BERT_LAYERS once scheduler is fast
run: BENCHMARK_LOG=bert_10steps CAPTURE_PROCESS_REPLAY=0 DEFAULT_FLOAT=HALF BENCHMARK=10 BS=66 GPUS=1 BERT_LAYERS=2 MODEL=bert python3 examples/mlperf/model_train.py
- name: Remote
run: |
pkill -f 'extra/remote/serve.py' || true
PYTHONPATH=. python3 extra/remote/serve.py 6482 &
sleep 1
DEBUG=2 PYTHONPATH=. REMOTE=127.0.0.1:6482 AM_RESET=1 python3 test/test_tiny.py
if [[ "${{ matrix.dev }}" == "AMD" ]]; then
DEBUG=2 PYTHONPATH=. REMOTE=127.0.0.1:6482 AM_RESET=1 AMD_AQL=1 python3 test/test_tiny.py
fi
pkill -f 'extra/remote/serve.py' || true
- name: Run process replay tests
uses: ./.github/actions/process-replay
+2 -1
View File
@@ -76,11 +76,12 @@ jobs:
- name: Run pytest (ptx)
env:
DEV: "MOCK+NV:PTX"
HCQ_RUNTIME_DEV: PYTHON
FORWARD_ONLY: 1
# TODO: failing due to library loading error
CAPTURE_PROCESS_REPLAY: 0
run: |
python3 -m pytest -n=auto test/device/test_hcq.py test/test_tiny.py \
python3 -m pytest -n=auto test/device/test_hcq2.py test/test_tiny.py \
test/testextra/test_hevc.py::TestHevc::test_hevc_decode_compile --durations=20
- name: Run process replay tests
uses: ./.github/actions/process-replay
+1 -9
View File
@@ -544,14 +544,6 @@ jobs:
run: python -m pytest test/device/test_hcq2.py
- name: Run disk copy tests on MOCKPCI
run: python -m pytest test/unit/test_disk_tensor.py -k test_copy_from_disk
- name: Run test_tiny on MOCKPCI Remote
env:
HCQ2: 0
run: |
python extra/remote/serve.py 6667 &
sleep 2
REMOTE=127.0.0.1:6667 python test/test_tiny.py
REMOTE=127.0.0.1:6667 python -m pytest test/unit/test_disk_tensor.py -k test_copy_from_disk; kill %1
testamd:
strategy:
@@ -619,7 +611,7 @@ jobs:
cuda: 'true'
ocelot: 'true'
- name: Set env
run: printf "${{ matrix.backend == 'ptx' && 'DEV=MOCK+CUDA:PTX' || matrix.backend == 'nv' && 'DEV=MOCK+NV\nSKIP_SLOW_TEST=1' }}" >> $GITHUB_ENV
run: printf "${{ matrix.backend == 'ptx' && 'DEV=MOCK+CUDA:PTX' || matrix.backend == 'nv' && 'DEV=MOCK+NV\nSKIP_SLOW_TEST=1\nHCQ_RUNTIME_DEV=PYTHON' }}" >> $GITHUB_ENV
- name: Check Device.DEFAULT and print some source
run: |
python3 -c "from tinygrad import Device; assert Device.DEFAULT in ['CUDA','NV'], Device.DEFAULT"
+1
View File
@@ -5,3 +5,4 @@
- Run `python -m ruff check .` to lint
- Read `./tinygrad/viz/README.md` for profiling and debugging rewrite rules
- Do not do amend commits. Always do a new commit if a force push to origin would be required.
- tinygrad has user space PCI drivers for AMD and NVIDIA GPUs. Do not insert the unneeded kernel modules.
+1 -1
View File
@@ -57,7 +57,7 @@ class TransformerBlock:
def __call__(self, x:Tensor, start_pos:Variable, mask:Optional[Tensor]):
h = x + self.attn(self.ln_1(x), start_pos, mask).float()
return (h + self.mlp(self.ln_2(h))).contiguous()
return (h + self.mlp(self.ln_2(h))).clone()
class Transformer:
def __init__(self, dim, n_heads, n_layers, norm_eps, vocab_size, max_seq_len=1024):
+20 -5
View File
@@ -12,7 +12,7 @@ from tinygrad.helpers import Timing, colored, GlobalCounters, profile_marker
from tinygrad.uop.ops import Ops, UOp
from extra.models.llama import apply_rotary_emb
from extra.llama_kernels.rmsnorm import rmsnorm
from extra.gemm.cdna_asm_gemm import _mx_block_scale, _mx_block_scale_3d, quantize_mxfp8, asm_gemm, can_use_asm_gemm
from extra.gemm.cdna_asm_gemm import _mx_block_scale, _mx_block_scale_3d, quantize_mxfp8, asm_gemm, can_use_asm_gemm, mx_pack
from extra.gemm.moe_gemm import grouped_mx_gemm
from extra.gemm.moe_routing import route, dispatch, combine, router_mfma
@@ -305,10 +305,25 @@ class GPTOSS:
h, *_ = self.run_layer(h, freqs_cis, mask_full, i % 2 == 0, attn_kwargs, ffn_kwargs, save=save)
h_normed = self.norm(h)
pad = (-self.dim) % 256
h_padded, w_padded = h_normed.pad((None, None, (0, pad))), self.output.pad(((0, 0), (0, pad)))
if ASM_GEMM and can_use_asm_gemm(h_padded, w_padded.T): logits = asm_gemm(h_padded, w_padded.T)
else: logits = h_normed @ self.output.T
if getenv("FP8_LMHEAD", 0) and ASM_GEMM:
pad = (-self.dim) % 256
h2 = h_normed.reshape(-1, self.dim).pad(((0, 0), (0, pad)))
w2 = self.output.pad(((0, 0), (0, pad)))
hq, he8, hsi = quantize_mxfp8(h2)
oq, oe8, _ = quantize_mxfp8(w2)
if hsi is not None and can_use_asm_gemm(hq, oq.T):
logits = asm_gemm(hq, oq.T, mx=True, mx_scales=(hsi, he8, mx_pack(oe8), oe8), mx_w_stored=False)
logits = logits.reshape(bsz, seqlen, self.vocab_size).cast(dtypes.bfloat16)
else:
logits = h_normed @ self.output.T
elif ASM_GEMM:
pad = (-self.dim) % 256
h_padded, w_padded = h_normed.pad((None, None, (0, pad))), self.output.pad(((0, 0), (0, pad)))
logits = asm_gemm(h_padded, w_padded.T) if can_use_asm_gemm(h_padded, w_padded.T) and getenv("VOCAB_ASM", 1) else h_normed @ self.output.T
else:
logits = h_normed @ self.output.T
return logits
def _get_pads(uop:UOp) -> list[UOp]:
+116
View File
@@ -0,0 +1,116 @@
# Navi31 flash tools
Utilities for reading and recovering the 2 MiB SPI flash on Navi31 boards.
Run them from the tinygrad repository root. No image is bundled; keep a verified
full-ROM backup before performing any write.
`fw_live.py` accesses BAR5 through tinygrad's `PCIDevice.map_bar()` abstraction
and supports either the custom ASM24 USB-PCIe bridge or native PCIe. Select the
transport before the subcommand:
```sh
python3 extra/amdflash/fw_live.py --transport usb probe
python3 extra/amdflash/fw_live.py --transport pci probe
```
The default, `--transport auto`, considers USB devices first and then native
PCI devices. Native PCI access requires the usual tinygrad PCI permissions and
an unbound kernel driver.
## Access paths and hardware state
The paths are state-dependent and are not interchangeable:
* **`romless.py`** drives SMUIO `ROM_SW_*` directly through the ASM24 bridge.
Use it only when an empty or corrupt flash has stalled the PSP PBL. Healthy
autonomous boot gates this engine; the usual gated status is
`ROM_SW_STATUS=0x04000800`.
* **`fw_live.py probe`** queries the early PSP boot-firmware mailbox.
* Firmware-mediated write commands are retained for protocol documentation but
are disabled because an exact stock reflash did not validate safely.
* **`fw_live.py dump`** reads an exact 2 MiB raw image through
`ROM_INDEX/ROM_DATA`. It refuses devices where the raw SMUIO controller is
unavailable; the NBIO SOC15 function-ROM aperture is not a physical SPI
mapping and is deliberately not used as a fallback.
The tools do not reset or power-cycle the board.
## Raw ROM_SW recovery
Identification and read-only operations:
```sh
python3 extra/amdflash/romless.py info
python3 extra/amdflash/romless.py read 0 0x40
python3 extra/amdflash/romless.py dump spi.bin
python3 extra/amdflash/romless.py verify known-good.bin
```
Restore an exact 2 MiB image:
```sh
python3 extra/amdflash/romless.py flash known-good.bin --yes
```
If GD25 status-register bit `SR2.CMP` protects the complete array, clearing it
requires separate authorization:
```sh
python3 extra/amdflash/romless.py flash known-good.bin --clear-cmp --yes
```
Programming is sector-granular. Every written 4 KiB sector is immediately read
back and compared with the input. A range can be resumed independently:
```sh
python3 extra/amdflash/romless.py flash known-good.bin \
--start-sector 128 --sector-count 64 --yes
```
Navi31 ROM_SW details used by the implementation:
* `ROM_SW_COMMAND = (address << 8) | opcode`
* TX data uses big-endian stream dwords
* `RETURN_DATA_EN` (bit 19) is clear for TX and set for RX
* the RX window exposes the preceding transaction, so reads are primed once
## Firmware-mediated access
The read-only commands are:
```sh
python3 extra/amdflash/fw_live.py probe
python3 extra/amdflash/fw_live.py dump current-spi.bin
```
`dump` produces exactly `0x200000` bytes, requires the raw IFWI magic at offset
zero, rejects mirrored 1 MiB apertures, and restores the ROM controller/index
state before writing output.
The validated early-firmware sequence is available as:
```sh
python3 extra/amdflash/fw_live.py --transport usb ifwi-all full-ifwi.bin --yes
```
It resolves at most Navi31's configured 19 items, streams the item associated
with terminal phase `0x2xx`, and then stops. PSP selects the destination
partition; item `0x08` always comes from the payload referenced by the first
ISH descriptor, matching AMDVBFlash. A hard power cycle is required afterward.
A successful PSP update is not a byte-identical raw rewrite. On the validated
stock test, both A/B payloads matched the source exactly, PSP selected and
booted the updated B partition, and firmware changed only its update cookie,
B descriptor counter/checksum, and generated metadata near `0x1ef000`.
The `stream`, `ifwi-step`, and `live-flash` commands remain disabled. Testing
showed that the PSP live path parses a raw stock IFWI but fails with status
`0xC` (`PSP Write To SPI Error`) after writing an `$AMDVBFL` cookie. Use the
verified ROM_SW path for recovery.
## Safety
ROM_SW erase/program and `ifwi-all` commands require `--yes`; other
firmware-streaming commands are disabled. Read-only commands still touch controller and mailbox registers but
do not issue SPI program/erase or PSP transfer-start commands. Preserve a
known-good full dump outside the repository.
+53
View File
@@ -0,0 +1,53 @@
from __future__ import annotations
import struct, sys, time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT))
from tinygrad.runtime.support.usb import USB3
from tinygrad.runtime.support.system import PCIDevice, System, USBPCIDevice
USB_IDS = ((0x3801, 0x0001), (0xADD1, 0x0001))
NAVI31_DEVICES = ((0xffff, (0x744c,)),)
def open_gpu(index: int = 0, transport: str = 'auto') -> PCIDevice:
"""Open an AMD GPU through tinygrad's transport-independent PCI interface."""
if transport not in ('auto', 'usb', 'pci'): raise ValueError(f"unsupported transport {transport!r}")
candidates = []
if transport in ('auto', 'usb'):
for vendor, product in USB_IDS:
candidates += [(USBPCIDevice, dev) for dev in USB3.list_devices(vendor, product)]
if transport in ('auto', 'pci'):
candidates += System.list_devices(0x1002, NAVI31_DEVICES)
if not candidates: raise RuntimeError(f"no supported {transport} AMD GPU found")
if not 0 <= index < len(candidates): raise RuntimeError(f"device index {index} out of range (found {len(candidates)})")
cls, descriptor = candidates[index]
return cls("AM", *descriptor) if cls is USBPCIDevice else cls("AM", descriptor)
class MMIO:
"""Transport-independent byte view of BAR5."""
def __init__(self, pci_dev: PCIDevice): self.bar = pci_dev.map_bar(5, fmt='B')
def read32(self, offset: int) -> int:
return struct.unpack('<I', bytes(self.bar[offset:offset+4]))[0]
def write32(self, offset: int, value: int):
self.write(offset, struct.pack('<I', value & 0xffffffff))
def read(self, offset: int, size: int) -> bytes:
return bytes(self.bar[offset:offset+size])
def write(self, offset: int, data: bytes):
self.bar[offset:offset+len(data)] = data
def wait_until(fn, timeout: float, message: str, interval: float = 0.001):
if timeout <= 0 or timeout > 60: raise ValueError("timeout must be in (0, 60] seconds")
end = time.monotonic() + timeout
while True:
value = fn()
if value: return value
if time.monotonic() >= end: raise TimeoutError(message)
time.sleep(interval)
+292
View File
@@ -0,0 +1,292 @@
#!/usr/bin/env python3
"""Navi31 firmware-mediated flash access and ROM aperture dumping.
Early item streaming must run after autonomous PSP boot but before a host
driver or AMDev loads SOS. A fully initialized SOS rejects those commands.
"""
from __future__ import annotations
import argparse, struct, sys, time
from pathlib import Path
from common import MMIO, open_gpu, wait_until
ROM_CNTL, ROM_INDEX, ROM_DATA = 0x5A380, 0x5A390, 0x5A394
FLASH_SIZE, INDEX_PAGE = 0x200000, 0x10000
def bswap32(value: int) -> int: return int.from_bytes(value.to_bytes(4, 'little'), 'big')
COMMAND_DATA, COMMAND, DOORBELL = 0x582D0, 0x582CC, 0x58224
GET_BOOT_PARTITION, GET_FB_STATE, GET_TRANSFER_TYPE = 0x01, 0x06, 0x07
START_TRANSFER, DATA_TRANSFER, END_TRANSFER = 0x08, 0x09, 0x0A
SPI_GET_MODEL_ID = 0x0B
LIVE_ADDR_LO, LIVE_ADDR_HI, LIVE_UPDATE = 0x02, 0x03, 0x04
PSP_ERRORS = {
0x01: "generic error", 0x02: "out of bounds", 0x03: "invalid parameter",
0x04: "off-chip boot error", 0x05: "address not set", 0x06: "parse off-chip error",
0x07: "address map error", 0x08: "parse on-chip error", 0x09: "full update error",
0x0A: "partition update error", 0x0B: "map on-chip error", 0x0C: "write to SPI error",
0x0D: "signature validation error", 0x0E: "invalid command", 0x0F: "signature not found",
0x10: "state machine not initialized", 0x11: "state machine transfer error",
0x12: "initialization error",
}
class PSPFlashMailbox:
def __init__(self, pci_dev): self.mmio = MMIO(pci_dev)
def command(self, command: int, data: int | None = None, *, timeout: float = 10.0) -> tuple[int, int]:
status = self.mmio.read32(COMMAND)
if not status & 0x80000000:
raise RuntimeError(f"PSP mailbox is not ready before command {command:#x}: status={status:#010x}")
if data is not None: self.mmio.write32(COMMAND_DATA, data)
self.mmio.write32(COMMAND, command << 16)
self.mmio.write32(DOORBELL, 1)
wait_until(lambda: self.mmio.read32(COMMAND) & 0x80000000, timeout,
f"PSP mailbox command {command:#x} timed out")
value = self.mmio.read32(COMMAND)
return value & 0xffff, self.mmio.read32(COMMAND_DATA)
def require(self, command: int, data: int | None = None, *, timeout: float = 10.0, name: str = '') -> int:
error, response = self.command(command, data, timeout=timeout)
if error:
detail = PSP_ERRORS.get(error, "unknown error")
raise RuntimeError(f"PSP {name or hex(command)} failed: error={error:#x} ({detail})")
return response
def probe(self) -> dict[str, tuple[int, int]]:
result = {}
for name, command in (("boot_partition", GET_BOOT_PARTITION), ("fb_state", GET_FB_STATE),
("model_id", SPI_GET_MODEL_ID), ("transfer_type", GET_TRANSFER_TYPE)):
result[name] = self.command(command)
return result
def stream(self, payload: bytes, item_type: int, transfer_type: int | None = None):
if not payload: raise ValueError("payload is empty")
if len(payload) > 0xFFFFFF: raise ValueError("payload exceeds the mailbox's 24-bit size field")
if len(payload) & 3: raise ValueError("payload size must be divisible by four")
if not 0 <= item_type <= 0xff: raise ValueError("item type must fit in eight bits")
if transfer_type is None: transfer_type = self.require(GET_TRANSFER_TYPE, name="GET_TRANSFER_TYPE")
requested = transfer_type & 0xff
print(f"firmware transfer_type={transfer_type:#x}", flush=True)
if requested != item_type:
raise RuntimeError(f"firmware requests item {requested:#x}, not {item_type:#x}")
self.require(START_TRANSFER, (len(payload) << 8) | item_type, name="START_TRANSFER")
sent, started = 0, time.monotonic()
try:
for offset in range(0, len(payload), 4):
word = struct.unpack_from('<I', payload, offset)[0]
self.require(DATA_TRANSFER, word, name=f"DATA_TRANSFER@{offset:#x}")
sent = offset + 4
if sent % 0x1000 == 0:
print(f"{sent:#x}/{len(payload):#x} ({sent/(time.monotonic()-started)/1024:.1f} KiB/s)", flush=True)
self.require(END_TRANSFER, (sent << 8) | item_type, timeout=60.0, name="END_TRANSFER")
except BaseException:
# Give firmware a chance to terminate an interrupted partial session. Do
# not submit END_TRANSFER twice if firmware rejected the original END.
if sent != len(payload):
try: self.command(END_TRANSFER, (sent << 8) | item_type, timeout=10.0)
except Exception: pass
raise
print(f"stream complete: type={item_type:#x} size={sent:#x} elapsed={time.monotonic()-started:.1f}s")
def resolve_ifwi_item(image: bytes, item_type: int) -> tuple[int, bytes]:
"""Resolve AMDVBFlash recovery-layout item types to exact IFWI bytes."""
if item_type == 0x01: offset, size = 0, 0x54
elif item_type in (0x02, 0x03):
offset = 0x2000 if item_type == 0x02 else 0x3000
if image[offset:offset+4] != b'$PSP': raise ValueError(f"invalid PSP directory at {offset:#x}")
size = (struct.unpack_from('<I', image, offset + 8)[0] + 1) * 0x10
elif item_type == 0x04: offset, size = 0x10000, 0x1000
elif item_type == 0x05: offset, size = 0x11000, 0x1000
elif item_type == 0x06: offset, size = 0x12000, 0x20
elif item_type == 0x07: offset, size = 0x13000, 0x20
elif item_type == 0x80: offset, size = 0x1000, 4
elif item_type == 0x81:
offset = struct.unpack_from('<I', image, 0x1000)[0]
if image[offset:offset+4] != b'$SGN': raise ValueError("invalid $SGN table pointer")
size = (struct.unpack_from('<I', image, offset + 8)[0] + 1) * 0x10
elif 0x82 <= item_type <= 0x88:
table = struct.unpack_from('<I', image, 0x1000)[0]
if image[table:table+4] != b'$SGN': raise ValueError("invalid $SGN table pointer")
wanted = item_type - 0x81 # 82h..88h map to SIGN_TYPE 1..7
count = struct.unpack_from('<I', image, table + 8)[0]
entries = [struct.unpack_from('<IIII', image, table + 0x10 + i*0x10) for i in range(count)]
match = [entry for entry in entries if entry[0] == wanted]
if len(match) != 1: raise ValueError(f"missing $SGN type {wanted}")
_, _, size, offset = match[0]
elif item_type == 0x89: offset, size = 0x1f0000, 0x100
elif item_type == 0x08:
# AMDVBFlash's GetPartitionDetails follows the first ISH entry (firmware ID
# 0x13c) and streams its payload. PSP, not the host resolver, selects the
# destination partition.
offset = struct.unpack_from('<I', image, 0x12000 + 0x10)[0]
size = struct.unpack_from('<I', image, 0x12000 + 0x18)[0]
else:
raise ValueError(f"IFWI resolver does not yet support requested item {item_type:#x}")
payload = image[offset:offset+size]
if len(payload) != size: raise ValueError(f"item {item_type:#x} extends beyond IFWI")
print(f"resolved requested item {item_type:#x}: offset={offset:#x} size={size:#x}")
return offset, payload
class LivePSPFlash:
"""Linux psp_v13_0_update_spirom protocol, used with SOS and trained VRAM."""
def __init__(self, pci_dev): self.mailbox = PSPFlashMailbox(pci_dev)
def command(self, command: int, data: int | None = None, timeout: float = 10.0):
# Same C2PMSG registers, but the live PSP command set uses IDs 2/3/4.
return self.mailbox.require(command, data, timeout=timeout, name=f"LIVE_SPI_{command:#x}")
def update(self, mc_address: int):
status = self.mailbox.mmio.read32(COMMAND)
if not status & 0x80000000: raise RuntimeError(f"live PSP mailbox is not ready: {status:#x}")
self.command(LIVE_ADDR_LO, mc_address & 0xffffffff)
self.command(LIVE_ADDR_HI, mc_address >> 32)
self.command(LIVE_UPDATE, timeout=60.0)
def open_mailbox(args): return PSPFlashMailbox(open_gpu(args.device, args.transport))
def reject_unvalidated_firmware_write():
raise RuntimeError("firmware writes are disabled: stock reflash validation failed; use romless.py for recovery")
def cmd_probe(args):
result = open_mailbox(args).probe()
for name, (error, response) in result.items(): print(f"{name}: error={error:#x} response={response:#x}")
if result['transfer_type'][0] == 0xA: print("update commands gated: reset card and do not initialize AMDev/SOS", file=sys.stderr)
def cmd_stream(args):
if not args.yes: raise RuntimeError("refusing to stream without --yes")
reject_unvalidated_firmware_write()
payload = Path(args.image).read_bytes()
open_mailbox(args).stream(payload, args.item_type)
def cmd_ifwi_step(args):
if not args.yes: raise RuntimeError("refusing to stream without --yes")
reject_unvalidated_firmware_write()
image = Path(args.ifwi).read_bytes()
if len(image) != 0x200000: raise ValueError("Navi31 IFWI image must be exactly 2 MiB")
mailbox = open_mailbox(args)
state = mailbox.require(GET_TRANSFER_TYPE, name="GET_TRANSFER_TYPE")
request = state & 0xff
_, payload = resolve_ifwi_item(image, request)
mailbox.stream(payload, request, transfer_type=state)
next_request = mailbox.require(GET_TRANSFER_TYPE, name="GET_TRANSFER_TYPE")
print(f"next firmware transfer_type={next_request:#x}")
def cmd_ifwi_all(args):
if not args.yes: raise RuntimeError("refusing to stream without --yes")
image = Path(args.ifwi).read_bytes()
if len(image) != 0x200000: raise ValueError("Navi31 IFWI image must be exactly 2 MiB")
mailbox = open_mailbox(args)
current = mailbox.require(GET_TRANSFER_TYPE, name="GET_TRANSFER_TYPE")
for step in range(19): # Navi31 ROMItemCount from AMDVBFlash ASICDetails.xml
request, phase = current & 0xff, current >> 8
print(f"IFWI step {step}: state={current:#x} item={request:#x} phase={phase}", flush=True)
_, payload = resolve_ifwi_item(image, request)
mailbox.stream(payload, request, transfer_type=current)
# AMDVBFlash tests the high byte belonging to the item just streamed. Phase
# 2 terminates the loop only after that item has completed successfully.
if phase == 2:
print(f"IFWI stream complete after terminal state {current:#x}; hard power cycle required")
return
current = mailbox.require(GET_TRANSFER_TYPE, name="GET_TRANSFER_TYPE")
raise RuntimeError(f"IFWI stream did not reach terminal phase after 19 items (state={current:#x})")
def cmd_live_flash(args):
if not args.yes: raise RuntimeError("refusing to flash without --yes")
reject_unvalidated_firmware_write()
image = Path(args.ifwi).read_bytes()
if not image or len(image) > 16 * 1024 * 1024 or len(image) & 3:
raise ValueError("live PSP image must be non-empty, 4-byte aligned, and at most 16 MiB")
pci_dev = open_gpu(args.device, args.transport)
from tinygrad.runtime.support.am.amdev import AMDev
started = time.monotonic()
adev = AMDev(pci_dev)
print(f"AMDev booted, SOS alive={adev.psp.is_sos_alive()}", flush=True)
paddr = adev.mm.palloc(len(image), align=0x1000, zero=False)
try:
adev.vram.view(paddr, len(image), 'B')[:] = image
adev.gmc.flush_hdp()
mc_address = adev.paddr2mc(paddr)
print(f"staged IFWI at VRAM paddr={paddr:#x} mc={mc_address:#x}", flush=True)
LivePSPFlash(pci_dev).update(mc_address)
print(f"live PSP flash update complete in {time.monotonic()-started:.1f}s")
finally:
adev.mm.pfree(paddr)
def cmd_dump(args):
import hashlib
pci_dev = open_gpu(args.device, args.transport)
mmio, output, started = MMIO(pci_dev), bytearray(), time.monotonic()
original_cntl, original_index = mmio.read32(ROM_CNTL), mmio.read32(ROM_INDEX)
if original_cntl == 0xFFFFFFFF:
raise RuntimeError("raw SMUIO ROM controller is unavailable; the SOC15 function-ROM aperture is not a raw SPI dump")
try:
# ROM_DATA must be read one dword at a time; a block read increments MMIO
# addresses rather than repeatedly reading the flash aperture register.
mmio.write32(ROM_CNTL, bswap32(original_cntl | (1 << 29)))
for page in range(0, FLASH_SIZE, INDEX_PAGE):
mmio.write32(ROM_INDEX, bswap32(page >> 8))
for _ in range(INDEX_PAGE // 4): output += struct.pack('<I', mmio.read32(ROM_DATA))
print(f"{page+INDEX_PAGE:#08x}/{FLASH_SIZE:#08x}", flush=True)
finally:
mmio.write32(ROM_INDEX, bswap32(original_index))
mmio.write32(ROM_CNTL, bswap32(original_cntl))
if len(output) != FLASH_SIZE or output[:4] != b'\xaa\x55\xaa\x55':
raise RuntimeError(f"invalid raw flash dump: size={len(output):#x} magic={output[:4].hex()}")
if output[:FLASH_SIZE//2] == output[FLASH_SIZE//2:]:
raise RuntimeError("ROM aperture contains mirrored 1 MiB halves; refusing to write a non-raw 2 MiB dump")
Path(args.output).write_bytes(output)
print(f"dumped {len(output):#x} bytes in {time.monotonic()-started:.1f}s sha256={hashlib.sha256(output).hexdigest()}")
def parser():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--device', type=int, default=0, help='device index for the selected transport')
p.add_argument('--transport', choices=('auto', 'usb', 'pci'), default='auto', help='PCIe transport (default: USB first, then native PCI)')
sub = p.add_subparsers(dest='command', required=True)
sub.add_parser('probe', help='query firmware mailbox state without writing').set_defaults(func=cmd_probe)
s = sub.add_parser('stream', help='stream one exact PSP ROM-item payload')
s.add_argument('item_type', type=lambda x:int(x, 0))
s.add_argument('image')
s.add_argument('--yes', action='store_true')
s.set_defaults(func=cmd_stream)
v = sub.add_parser('ifwi-step', help='resolve and stream the next early-firmware-requested item from a 2 MiB IFWI')
v.add_argument('ifwi')
v.add_argument('--yes', action='store_true')
v.set_defaults(func=cmd_ifwi_step)
a = sub.add_parser('ifwi-all', help='stream requested IFWI items until firmware reports completion')
a.add_argument('ifwi')
a.add_argument('--yes', action='store_true')
a.set_defaults(func=cmd_ifwi_all)
l = sub.add_parser('live-flash', help='stage an image in VRAM and invoke the PSP v13 live-update command')
l.add_argument('ifwi')
l.add_argument('--yes', action='store_true')
l.set_defaults(func=cmd_live_flash)
d = sub.add_parser('dump', help='dump the exact 2 MiB flash through ROM_INDEX/ROM_DATA')
d.add_argument('output')
d.set_defaults(func=cmd_dump)
return p
def main():
args = parser().parse_args()
try: args.func(args)
except (RuntimeError, TimeoutError, ValueError, OSError) as error:
print(f"error: {error}", file=sys.stderr)
raise SystemExit(1)
if __name__ == '__main__': main()
+249
View File
@@ -0,0 +1,249 @@
#!/usr/bin/env python3
"""Direct Navi31 ROM_SW access for GD25LQ16E-class 2 MiB SPI flash."""
from __future__ import annotations
import argparse, hashlib, sys, time
from pathlib import Path
from common import MMIO, open_gpu, wait_until
FLASH_SIZE, SECTOR_SIZE, PAGE_SIZE, MAX_DATA = 0x200000, 0x1000, 0x100, 0x100
ROM_CNTL, PAGE_MIRROR_CNTL = 0x5A380, 0x5A384
ROM_SW_CNTL, ROM_SW_STATUS, ROM_SW_COMMAND, ROM_SW_DATA = 0x5A3A0, 0x5A3A4, 0x5A3A8, 0x5A3B0
GPIO_PAD_MASK, GPIO_PAD_A, GPIO_PAD_EN = 0x5A504, 0x5A508, 0x5A510
SPI_GPIO_BITS, RETURN_DATA_EN = 0x780, 0x80000
EXPECTED_JEDEC = b'\xc8\x60\x15'
class Navi31SPI:
def __init__(self, pci_dev, prescale: int = 8):
if not 0 <= prescale <= 15: raise ValueError("prescale must be 0..15")
self.mmio = MMIO(pci_dev)
rc = self.mmio.read32(ROM_CNTL)
# Select the prescaler instead of inheriting a potentially unusable BL value.
self.mmio.write32(ROM_CNTL, (rc & 0xE0FFFFFF) | (1 << 28) | (prescale << 24) | 1)
def transfer(self, opcode: int, *, address: int = 0, address_len: int = 0,
data_out: bytes = b'', data_in: int = 0, timeout: float = 2.0) -> bytes:
if data_out and data_in: raise ValueError("simultaneous TX and RX is unsupported")
if not 0 <= address_len <= 3: raise ValueError("address_len must be 0..3")
count = len(data_out) if data_out else data_in
if not 0 <= count <= MAX_DATA: raise ValueError(f"transfer data must be <= {MAX_DATA} bytes")
ncmd = 1 + address_len
m = self.mmio
gpio_mask, gpio_a, gpio_en = m.read32(GPIO_PAD_MASK), m.read32(GPIO_PAD_A), m.read32(GPIO_PAD_EN)
page_mirror, rom_cntl = m.read32(PAGE_MIRROR_CNTL), m.read32(ROM_CNTL)
try:
m.write32(GPIO_PAD_MASK, gpio_mask & ~SPI_GPIO_BITS)
m.write32(GPIO_PAD_A, gpio_a & ~SPI_GPIO_BITS)
m.write32(GPIO_PAD_EN, gpio_en & ~SPI_GPIO_BITS)
m.write32(PAGE_MIRROR_CNTL, (page_mirror & 0xF1FFFFFF) | 0x06000000)
m.write32(ROM_CNTL, (rom_cntl & ~0xF) | 8)
m.write32(ROM_SW_CNTL, 0)
m.write32(ROM_SW_STATUS, 0)
if m.read32(ROM_SW_STATUS) != 0: raise RuntimeError("ROM_SW_STATUS did not clear")
# Navi31 serializes the low instruction byte first, followed by ADDRESS[23:0].
m.write32(ROM_SW_COMMAND, ((address & 0xFFFFFF) << 8) | (opcode & 0xFF))
for offset in range(0, len(data_out), 4):
word = data_out[offset:offset+4].ljust(4, b'\0')
m.write32(ROM_SW_DATA + offset, int.from_bytes(word, 'big'))
control = ((ncmd - 1) << 16) | (RETURN_DATA_EN if data_in else 0) | count
m.write32(ROM_SW_CNTL, control)
m.read32(ROM_SW_CNTL) # posted-write flush
wait_until(lambda: m.read32(ROM_SW_STATUS) & 1, timeout,
f"ROM_SW transaction timeout (status={m.read32(ROM_SW_STATUS):#x}); engine may be gated after SOS boot")
return m.read(ROM_SW_DATA, (data_in + 3) & ~3)[:data_in] if data_in else b''
finally:
m.write32(ROM_SW_CNTL, 0)
m.write32(ROM_SW_STATUS, 0)
m.write32(ROM_CNTL, rom_cntl)
m.write32(PAGE_MIRROR_CNTL, page_mirror)
m.write32(GPIO_PAD_A, gpio_a)
m.write32(GPIO_PAD_EN, gpio_en)
m.write32(GPIO_PAD_MASK, gpio_mask)
class GD25LQ16E:
def __init__(self, spi: Navi31SPI): self.spi = spi
def read_register(self, opcode: int, count: int = 1) -> bytes:
# Navi31 exposes the preceding transaction's RX capture. Prime identically.
self.spi.transfer(opcode, data_in=max(2, count))
return self.spi.transfer(opcode, data_in=count)
def status(self, opcode: int = 0x05) -> int: return self.read_register(opcode)[0]
def rdid(self) -> bytes: return self.read_register(0x9F, 4)
def sfdp(self, count: int = 20) -> bytes:
# 5Ah has one dummy byte after its 24-bit address; retain it for diagnostics.
self.spi.transfer(0x5A, address_len=3, data_in=count)
return self.spi.transfer(0x5A, address_len=3, data_in=count)
def wait_idle(self, timeout: float = 2.0) -> int:
end = time.monotonic() + timeout
while time.monotonic() < end:
sr1 = self.status()
if not sr1 & 1: return sr1
time.sleep(0.002)
raise TimeoutError(f"flash remained busy for {timeout}s")
def write_enable(self):
self.spi.transfer(0x06)
sr1 = self.status()
if not sr1 & 2: raise RuntimeError(f"WREN failed (SR1={sr1:#04x})")
def clear_cmp(self):
sr1, sr2 = self.status(), self.status(0x35)
if not sr2 & 0x40: return False
self.write_enable()
# BUSY/WEL are not writable; preserve all protection/QE fields except CMP.
self.spi.transfer(0x01, data_out=bytes((sr1 & 0xFC, sr2 & ~0x40)))
self.wait_idle(1.0)
new_sr2 = self.status(0x35)
if new_sr2 & 0x40: raise RuntimeError(f"failed to clear CMP (SR2={new_sr2:#04x})")
return True
def erase_sector(self, address: int):
if address & (SECTOR_SIZE - 1): raise ValueError("sector address is not 4 KiB aligned")
self.write_enable()
self.spi.transfer(0x20, address=address, address_len=3)
self.wait_idle(2.0)
def program_page(self, address: int, data: bytes):
if not data or len(data) > PAGE_SIZE or (address & 0xFF) + len(data) > PAGE_SIZE:
raise ValueError("page program crosses a 256-byte boundary")
self.write_enable()
self.spi.transfer(0x02, address=address, address_len=3, data_out=data)
self.wait_idle(1.0)
def read(self, address: int, count: int) -> bytes:
if address < 0 or count < 0 or address + count > FLASH_SIZE: raise ValueError("read outside 2 MiB flash")
output = bytearray()
while count:
size = min(count, MAX_DATA)
self.spi.transfer(0x03, address=address, address_len=3, data_in=size)
output += self.spi.transfer(0x03, address=address, address_len=3, data_in=size)
address, count = address + size, count - size
return bytes(output)
def has_jedec(raw: bytes) -> bool:
return EXPECTED_JEDEC in raw + raw[:2]
def open_flash(args) -> GD25LQ16E:
flash = GD25LQ16E(Navi31SPI(open_gpu(args.device, 'usb'), args.prescale))
raw = flash.rdid()
if not has_jedec(raw): raise RuntimeError(f"unexpected GD25LQ16E JEDEC capture: {raw.hex()}")
return flash
def cmd_info(args):
f = open_flash(args)
sr1, sr2, sr3 = f.status(), f.status(0x35), f.status(0x15)
sfdp = f.sfdp(24)
pos = sfdp.find(b'SFDP')
print(f"JEDEC capture: {f.rdid().hex()} (C8 60 15 detected)")
print(f"SR1/SR2/SR3: {sr1:02x}/{sr2:02x}/{sr3:02x} CMP={'set' if sr2 & 0x40 else 'clear'}")
print(f"SFDP capture: {sfdp.hex()} signature_offset={pos}")
def cmd_read(args):
data = open_flash(args).read(args.address, args.size)
if args.output: Path(args.output).write_bytes(data)
else: print(data.hex())
def cmd_dump(args):
f = open_flash(args)
out = Path(args.output)
digest = hashlib.sha256()
with out.open('wb') as file:
for address in range(0, FLASH_SIZE, SECTOR_SIZE):
data = f.read(address, SECTOR_SIZE)
file.write(data)
digest.update(data)
if not (address & 0xFFFF): print(f"{address + SECTOR_SIZE:#08x}/{FLASH_SIZE:#08x}", flush=True)
print(f"wrote {out} sha256={digest.hexdigest()}")
def cmd_verify(args):
expected = Path(args.image).read_bytes()
if len(expected) != FLASH_SIZE: raise ValueError(f"image must be exactly {FLASH_SIZE:#x} bytes")
f = open_flash(args)
digest = hashlib.sha256()
for address in range(0, FLASH_SIZE, SECTOR_SIZE):
got, wanted = f.read(address, SECTOR_SIZE), expected[address:address+SECTOR_SIZE]
digest.update(got)
if got != wanted:
index = next(i for i, (a, b) in enumerate(zip(got, wanted)) if a != b)
raise RuntimeError(f"verify mismatch at {address+index:#x}: flash={got[index]:02x} image={wanted[index]:02x}")
print(f"verified {FLASH_SIZE:#x} bytes sha256={digest.hexdigest()}")
def cmd_flash(args):
if not args.yes: raise RuntimeError("refusing to write without --yes")
image = Path(args.image).read_bytes()
if len(image) != FLASH_SIZE: raise ValueError(f"image must be exactly {FLASH_SIZE:#x} bytes")
total_sectors = FLASH_SIZE // SECTOR_SIZE
start, count = args.start_sector, args.sector_count if args.sector_count is not None else total_sectors - args.start_sector
if not 0 <= start < total_sectors or not 1 <= count <= total_sectors - start: raise ValueError("invalid sector range")
f = open_flash(args)
if f.status(0x35) & 0x40:
if not args.clear_cmp: raise RuntimeError("CMP protects the full array; rerun with --clear-cmp")
f.clear_cmp()
print("cleared SR2.CMP", flush=True)
begin = time.monotonic()
for sector in range(start, start + count):
address = sector * SECTOR_SIZE
wanted = image[address:address+SECTOR_SIZE]
f.erase_sector(address)
for offset in range(0, SECTOR_SIZE, PAGE_SIZE):
page = wanted[offset:offset+PAGE_SIZE]
if page != b'\xff' * PAGE_SIZE: f.program_page(address + offset, page)
got = f.read(address, SECTOR_SIZE)
if got != wanted:
index = next(i for i, (a, b) in enumerate(zip(got, wanted)) if a != b)
raise RuntimeError(f"verify mismatch at {address+index:#x}: flash={got[index]:02x} image={wanted[index]:02x}")
print(f"OK sector {sector:03d}/{total_sectors-1} @{address:#07x} elapsed={time.monotonic()-begin:.1f}s", flush=True)
def parser():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--device', type=int, default=0, help='USB bridge device index')
p.add_argument('--prescale', type=int, default=8, help='SCK prescaler 0..15 (default: 8)')
sub = p.add_subparsers(dest='command', required=True)
sub.add_parser('info', help='read JEDEC, status and SFDP').set_defaults(func=cmd_info)
r = sub.add_parser('read', help='read a flash range')
r.add_argument('address', type=lambda x:int(x, 0))
r.add_argument('size', type=lambda x:int(x, 0))
r.add_argument('-o', '--output')
r.set_defaults(func=cmd_read)
d = sub.add_parser('dump', help='dump the complete 2 MiB flash')
d.add_argument('output')
d.set_defaults(func=cmd_dump)
v = sub.add_parser('verify', help='compare the complete flash with an image')
v.add_argument('image')
v.set_defaults(func=cmd_verify)
w = sub.add_parser('flash', help='erase, program, and verify one or more sectors')
w.add_argument('image')
w.add_argument('--start-sector', type=lambda x:int(x, 0), default=0)
w.add_argument('--sector-count', type=lambda x:int(x, 0))
w.add_argument('--clear-cmp', action='store_true')
w.add_argument('--yes', action='store_true')
w.set_defaults(func=cmd_flash)
return p
def main():
args = parser().parse_args()
try: args.func(args)
except (RuntimeError, TimeoutError, ValueError, OSError) as error:
print(f"error: {error}", file=sys.stderr)
raise SystemExit(1)
if __name__ == '__main__': main()
+2
View File
@@ -66,6 +66,8 @@ class AMSMI(AMDev):
def __init__(self, pcibus, vram_bar:MMIOInterface, doorbell_bar:MMIOInterface, mmio_bar:MMIOInterface):
self.pcibus, self.devfmt = pcibus, pcibus
self.vram, self.doorbell64, self.mmio = vram_bar, doorbell_bar, mmio_bar
self.is_vf = bool(self.mmio[am.mmRCC_IOV_FUNC_IDENTIFIER] & 1)
self.vf_rlc_gated:list[tuple[int, int]] = []
self.pci_state = self.read_pci_state()
if self.pci_state == "D0": self._init_from_d0()
+2 -2
View File
@@ -2,7 +2,7 @@ import numpy as np
from tinygrad import dtypes, Tensor
from tinygrad.helpers import getenv, get_single_element
from tinygrad.dtype import _to_np_dtype
from tinygrad.engine.realize import compile_linear
from tinygrad.engine.realize import lower_and_compile
from tinygrad.codegen.opt import OptOps
dtype_in = (dtypes.half if getenv("HALF") else dtypes.bfloat16 if getenv("BFLOAT16") else
@@ -39,7 +39,7 @@ if __name__ == "__main__":
c = a.matmul(b, dtype=acc_dtype).realize()
if getenv("SHOULD_USE_TC"):
linear = compile_linear(a.matmul(b, dtype=acc_dtype).schedule_linear())
linear = lower_and_compile(a.matmul(b, dtype=acc_dtype).schedule_linear())
call = get_single_element(list(linear.src))
applied_opts = call.src[0].src[0].arg.applied_opts
assert any(opt.op is OptOps.TC for opt in applied_opts), f"TC not triggered, {applied_opts}"
+2 -2
View File
@@ -145,7 +145,7 @@ class AMDComputeQueue(HWQueue):
def wait(self, signal:UOp, value:UOp): self.wait_reg_mem(value.cast(dtypes.uint32), mem=signal.getaddr(self.devs))
def timestamp(self, signal:UOp):
self.release_mem(signal.getaddr(self.devs), 0, self.pm4.data_sel__mec_release_mem__send_gpu_clock_counter,
self.release_mem(signal.getaddr(self.devs) + UOp.const(8, dtypes.uint64), 0, self.pm4.data_sel__mec_release_mem__send_gpu_clock_counter,
self.pm4.int_sel__mec_release_mem__none)
def signal(self, signal:UOp, value:UOp):
@@ -197,7 +197,7 @@ class AMDSDMAQueue(HWQueue):
def timestamp(self, signal:UOp):
self.q(self.sdma.SDMA_OP_TIMESTAMP | self.sdma.SDMA_PKT_TIMESTAMP_GET_HEADER_SUB_OP(self.sdma.SDMA_SUBOP_TIMESTAMP_GET_GLOBAL),
signal.getaddr(self.devs))
signal.getaddr(self.devs) + UOp.const(8, dtypes.uint64))
def signal(self, signal:UOp, value:UOp): # a fence packet then a trap
op = self.sdma.SDMA_OP_FENCE | (self.sdma.SDMA_PKT_FENCE_HEADER_MTYPE(3) if self.target[0] != 9 else 0)
+2 -2
View File
@@ -139,7 +139,7 @@ class TransformerBlock:
def __call__(self, x:Tensor, start_pos:Union[Variable,int], freqs_cis:Tensor, mask:Optional[Tensor]):
h = x + self.attention(self.attention_norm(x), start_pos, freqs_cis, mask)
return (h + self.feed_forward(self.ffn_norm(h))).contiguous().contiguous_backward()
return (h + self.feed_forward(self.ffn_norm(h))).clone().contiguous_backward()
# standard openai sampling
def sample(logits: Tensor, temp: float, k: int, p: float, af: float, ap: float):
@@ -201,7 +201,7 @@ class Transformer:
self.tok_embeddings = embedding(vocab_size, dim)
self.output = nn.Linear(dim, vocab_size, bias=False) if embedding == nn.Embedding else linear(dim, vocab_size, bias=False)
self.max_context = max_context
self.freqs_cis = precompute_freqs_cis(dim // n_heads, self.max_context * 2, rope_theta).contiguous().is_param_(False)
self.freqs_cis = precompute_freqs_cis(dim // n_heads, self.max_context * 2, rope_theta).clone().is_param_(False)
self.forward_jit = TinyJit(self.forward) if jit else None
def forward(self, tokens:Tensor, start_pos:Union[Variable,int], temperature:float, top_k:int, top_p:float, alpha_f:float, alpha_p:float):
+2 -2
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
import os, sys, time
from tinygrad.runtime.support.system import RemotePCIDevice
from extra.remote.hcq1_remote import RemotePCIDevice
LAT_N_RUNS = 500
THROUGHPUT_N_RUNS = 8
@@ -18,7 +18,7 @@ if __name__ == "__main__":
print(f"connected to {os.environ['REMOTE']}, device: {name}\n")
# ping (minimal server round-trip, no device I/O)
from tinygrad.runtime.support.system import RemoteCmd
from extra.remote.hcq1_remote import RemoteCmd
sock = pci.sock
for _ in range(10): RemotePCIDevice._rpc(sock, 0, RemoteCmd.PING)
st = time.perf_counter()
+143
View File
@@ -0,0 +1,143 @@
from __future__ import annotations
import os, mmap, array, functools, contextlib, itertools, struct, socket, subprocess, time, enum, atexit
from tinygrad.helpers import getenv, temp, ceildiv, unwrap, fetch, system, _ensure_downloads_dir, DEBUG, flatten
from tinygrad.runtime.support.hcq import FileIOInterface, MMIOInterface
from tinygrad.runtime.support.system import PCIDevice, System
class RemoteCmd(enum.IntEnum):
PROBE,MAP_BAR,MAP_SYSMEM_FD,CFG_READ,CFG_WRITE,RESET,MMIO_READ,MMIO_WRITE,MAP_SYSMEM,SYSMEM_READ,SYSMEM_WRITE,RESIZE_BAR,PING = range(13)
class RemoteMMIOInterface(MMIOInterface):
def __init__(self, dev:RemotePCIDevice, residx:int, nbytes:int, fmt='B', off=0, rd_cmd=RemoteCmd.MMIO_READ, wr_cmd=RemoteCmd.MMIO_WRITE):
self.dev, self.residx, self.nbytes, self.fmt, self.off, self.el_sz = dev, residx, nbytes, fmt, off, struct.calcsize(fmt)
self.rd_cmd, self.wr_cmd = rd_cmd, wr_cmd
def __getitem__(self, index):
sl = index if isinstance(index, slice) else slice(index, index + 1)
start, stop = (sl.start or 0) * self.el_sz, (sl.stop or len(self)) * self.el_sz
data = self.dev._bulk_read(self.rd_cmd, self.residx, self.off + start, stop - start)
result = data if self.fmt == 'B' else list(struct.unpack(f'<{(stop - start) // self.el_sz}{self.fmt}', data))
return result if isinstance(index, slice) else result[0]
def __setitem__(self, index, val):
start = (index.start or 0) * self.el_sz if isinstance(index, slice) else index * self.el_sz
data = (val if self.fmt == 'B' else struct.pack(f'<{len(val)}{self.fmt}', *val)) if isinstance(index, slice) else struct.pack(f'<{self.fmt}', val)
self.dev._bulk_write(self.wr_cmd, self.residx, self.off + start, data)
def view(self, offset:int=0, size:int|None=None, fmt=None):
return RemoteMMIOInterface(self.dev, self.residx, size or (self.nbytes - offset), fmt or self.fmt, self.off + offset, self.rd_cmd, self.wr_cmd)
class RemotePCIDevice(PCIDevice):
_bulk_sent:int = 0
_bulk_recv:int = 0
_rpc_count:int = 0
_start_time:float = 0.0
@staticmethod
@functools.cache
def remote_sock(host:str, port:int) -> socket.socket:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
sock.settimeout(getenv("REMOTE_TIMEOUT", 3))
sock.connect((host, port))
sock.settimeout(None)
if DEBUG >= 1 and RemotePCIDevice._start_time == 0.0:
RemotePCIDevice._start_time = time.perf_counter()
def _print_stats():
dt = time.perf_counter() - RemotePCIDevice._start_time
sent_mb, recv_mb = RemotePCIDevice._bulk_sent / 1e6, RemotePCIDevice._bulk_recv / 1e6
print(f"remote: sent {sent_mb:,.2f} MB ({sent_mb/dt:,.2f} MB/s), recv {recv_mb:,.2f} MB ({recv_mb/dt:,.2f} MB/s), "
f"{RemotePCIDevice._rpc_count:,} roundtrips in {dt:.2f}s")
atexit.register(_print_stats)
return sock
@staticmethod
@functools.cache
def remote_list(vendor:int, devices:tuple[tuple[int, tuple[int, ...]], ...], base_class:int|None) -> list[tuple[socket.socket, str]]:
payload = array.array('I', itertools.chain.from_iterable((m, d) for m, ds in devices for d in ds)).tobytes()
def q(r:str) -> list[tuple[socket.socket, str]]:
sock = RemotePCIDevice.remote_sock((host:=r.strip().split(":")[0]), (port:=int(r.strip().split(":")[1]) if ":" in r else 6667))
data_len, _, _, _ = RemotePCIDevice._rpc(sock, 0, RemoteCmd.PROBE, base_class or 0, len(payload), vendor, payload=payload)
return [(sock, f"remote:{host}:{port}:{d}") for d in RemotePCIDevice._recvall(sock, data_len).decode().split('\n')]
return flatten([q(r) for r in getenv("REMOTE", "").split(",") if r.strip()])
@staticmethod
def _recvall(sock:socket.socket, n:int) -> bytes:
data = b''
while len(data) < n and (chunk:=sock.recv(n - len(data))): data += chunk
if len(data) < n: raise RuntimeError("Connection closed")
return data
@staticmethod
def _rpc(sock:socket.socket, dev_id:int, cmd:int, *args:int, bar:int=0, readout_size:int=0, payload:bytes=b'', has_fd=False):
sock.sendall(struct.pack('<BIIQQQ', cmd, dev_id, bar, *(*args, 0, 0, 0)[:3]) + payload)
if has_fd:
msg, anc, _, _ = sock.recvmsg(17, socket.CMSG_LEN(4))
fd = struct.unpack('<i', anc[0][2][:4])[0]
else: msg, fd = RemotePCIDevice._recvall(sock, 17), None
if (resp:=struct.unpack('<BQQ', msg))[0] != 0:
raise RuntimeError(f"RPC failed: {RemotePCIDevice._recvall(sock, resp[1]).decode('utf-8') if resp[1] > 0 else 'unknown error'}")
RemotePCIDevice._rpc_count += 1
return (resp[1], resp[2]) + ((RemotePCIDevice._recvall(sock, readout_size) if readout_size > 0 else None),) + (fd,)
def __init__(self, devpref:str, pcibus:str, sock:socket.socket):
self.sock, self.pcibus, self.dev_id = sock, pcibus, int(pcibus.split(':')[-1]) if ':' in pcibus else 0
self.peer_group = sock.getpeername()[0]
for buft in [socket.SO_SNDBUF, socket.SO_RCVBUF]: self.sock.setsockopt(socket.SOL_SOCKET, buft, 64 << 20)
self.lock_fd = System.flock_acquire(f"{devpref.lower()}_{pcibus.lower()}.lock")
def _bulk_read(self, cmd:int, idx:int, offset:int, size:int) -> bytes:
RemotePCIDevice._bulk_recv += size
return unwrap(self._rpc(self.sock, self.dev_id, cmd, offset, size, bar=idx, readout_size=size)[2])
def _bulk_write(self, cmd:int, idx:int, offset:int, data:bytes):
RemotePCIDevice._bulk_sent += len(data)
self.sock.sendall(struct.pack('<BIIQQQ', cmd, self.dev_id, idx, offset, len(data), 0) + data)
def alloc_sysmem(self, size:int, vaddr:int=0, contiguous:bool=False) -> tuple[MMIOInterface, list[int]]:
paddrs_len, handle, _, _ = self._rpc(self.sock, self.dev_id, RemoteCmd.MAP_SYSMEM, size, int(contiguous))
paddrs = list(struct.unpack(f'<{paddrs_len // 8}Q', self._recvall(self.sock, paddrs_len)))
return RemoteMMIOInterface(self, handle, size, fmt='B', rd_cmd=RemoteCmd.SYSMEM_READ, wr_cmd=RemoteCmd.SYSMEM_WRITE), paddrs
def reset(self): self._rpc(self.sock, self.dev_id, RemoteCmd.RESET)
def read_config(self, offset:int, size:int): return self._rpc(self.sock, self.dev_id, RemoteCmd.CFG_READ, offset, size)[0]
def write_config(self, offset:int, value:int, size:int): self._rpc(self.sock, self.dev_id, RemoteCmd.CFG_WRITE, offset, size, value)
@functools.cache
def bar_info(self, bar_idx:int) -> tuple[int, int]: return self._rpc(self.sock, self.dev_id, RemoteCmd.MAP_BAR, bar=bar_idx)[:2]
def map_bar(self, bar:int, off:int=0, addr:int=0, size:int|None=None, fmt='B') -> MMIOInterface:
return RemoteMMIOInterface(self, bar, size or self.bar_info(bar)[1], fmt).view(off, size, fmt)
def resize_bar(self, bar_idx:int): self._rpc(self.sock, self.dev_id, RemoteCmd.RESIZE_BAR, bar=bar_idx)
class APLRemotePCIDevice(RemotePCIDevice):
APP_PATH = "/Applications/TinyGPU.app/Contents/MacOS/TinyGPU"
@classmethod
def ensure_app(cls):
commit = "c0d024f9ff0e1dc8fdf217f255da7101d91e8323"
app_name = f"TinyGPU_{commit}.zip"
if (_ensure_downloads_dir() / app_name).is_file() and os.path.exists(cls.APP_PATH): return
print("Downloading TinyGPU.app...")
with contextlib.suppress(RuntimeError): system("pkill -f TinyGPU")
system(f"ditto -xk {fetch(f'https://github.com/tinygrad/tinygpu_releases/raw/{commit}/TinyGPU.zip', name=app_name)} /Applications")
print(system(f"{cls.APP_PATH} install"))
def __init__(self, devpref:str, pcibus:str):
self.ensure_app()
sock_path, sock = getenv("APL_REMOTE_SOCK", temp("tinygpu.sock")), socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
for i in range(100):
with contextlib.suppress(ConnectionRefusedError, FileNotFoundError):
sock.connect(sock_path)
break
if i == 0: subprocess.Popen([self.APP_PATH, "server", sock_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(0.05)
else: raise RuntimeError(f"Failed to connect to TinyGPU server at {sock_path}.")
super().__init__(devpref, "usb4", sock=sock)
def alloc_sysmem(self, size:int, vaddr:int=0, contiguous:bool=False) -> tuple[MMIOInterface, list[int]]:
mapped_size, _, _, fd = self._rpc(self.sock, self.dev_id, RemoteCmd.MAP_SYSMEM_FD, size, int(contiguous), has_fd=True)
memview = MMIOInterface(FileIOInterface(fd=fd).mmap(0, mapped_size, mmap.PROT_READ | mmap.PROT_WRITE, mmap.MAP_SHARED, 0), mapped_size, fmt='B')
# paddrs are returned as (paddr, size) pairs until a (paddr=0, size=0) terminator in the beginning of the mapping.
paddrs_raw = list(itertools.takewhile(lambda p: p[1] != 0, zip(memview.view(fmt='Q')[0::2], memview.view(fmt='Q')[1::2])))
return memview, [p + i for p, sz in paddrs_raw for i in range(0, sz, 0x1000)][:ceildiv(size, 0x1000)]
+2 -1
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import socket, struct, sys
from tinygrad.runtime.support.system import PCIDevice, RemoteCmd, System
from tinygrad.runtime.support.system import PCIDevice, System
from extra.remote.hcq1_remote import RemoteCmd
from tinygrad.helpers import DEBUG, OSX
def resp(resp0=0, resp1=0, status=0): return struct.pack('<BQQ', status, resp0, resp1)
+23
View File
@@ -7,6 +7,29 @@ Includes: ds_store_b32, ds_load_b32, ds_store_2addr_*, ds_load_2addr_*,
import unittest
from test.amd.hw.helpers import *
class TestDSSwizzle(unittest.TestCase):
def test_modes_and_overlapping_registers(self):
for offset in (0x041f, 0x401f, 0x7c1f, 0x00a0, 0x801b, 0xc020, 0xc420, 0xc021, 0xe000, 0xe010, 0xe01f):
for dst in (0, 1):
with self.subTest(offset=hex(offset), dst=dst):
st = run_program([
v_add_nc_u32_e32(v[0], 1, v[255]),
ds_swizzle_b32(vdst=v[dst], addr=v[0], offset0=offset & 255, offset1=offset >> 8),
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
], n_lanes=32)
self.assertEqual(sorted(st.vgpr[i][dst] for i in range(32)), [6]*32 if offset == 0x00a0 else list(range(1, 33)))
def test_inactive_sources_and_destinations(self):
st = run_program([
v_add_nc_u32_e32(v[0], 1, v[255]),
v_mov_b32_e32(v[1], 99),
s_mov_b32(EXEC_LO, 0x55555555),
ds_swizzle_b32(vdst=v[1], addr=v[0], offset0=0x1f, offset1=4),
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
s_mov_b32(EXEC_LO, 0xffffffff),
], n_lanes=32)
self.assertEqual([st.vgpr[i][1] for i in range(32)], [0, 99]*16)
class TestDS2Addr(unittest.TestCase):
"""Tests for DS_*_2ADDR instructions."""
+28
View File
@@ -17,6 +17,34 @@ def _srcs():
class TestBasicParsing(unittest.TestCase):
"""Test basic pcode parsing for common instruction patterns."""
def test_c_style_blocks_and_array_access(self):
code = """
for (i = 0; i < 4; i+=2) {
if (mode == 0) {
out[i+0] = input[i+1];
out[i+1] = input[i+0];
} elsif (mode == 1) {
out[i+0] = 7;
out[i+1] = 8;
} else { // identity
out[i+0] = input[i+0];
out[i+1] = input[i+1];
}
}
"""
for mode, expected in enumerate(([11, 10, 13, 12], [7, 8, 7, 8], [10, 11, 12, 13])):
with self.subTest(mode=mode):
result, _ = parse_pcode(code, {'mode': UOp.const(mode, dtypes.uint32)}, {'input': lambda i: i + 10})
self.assertEqual([result[f'out@{i}'].simplify().val for i in range(4)], expected)
def test_colon_concatenation(self):
result, _ = parse_pcode('offset = hi:lo;', {'hi': UOp.const(0x12, dtypes.uint8), 'lo': UOp.const(0x34, dtypes.uint8)})
self.assertEqual(result['offset'].simplify().val, 0x1234)
def test_unclosed_c_block(self):
with self.assertRaisesRegex(AssertionError, 'unclosed pcode block'):
parse_pcode('if (1) {\nvalue = 2;')
def test_v_add_f32(self):
"""Test parsing V_ADD_F32 pcode."""
_, assigns = parse_pcode(PCODE[VOP2Op.V_ADD_F32_E32], _srcs())
+10
View File
@@ -43,6 +43,16 @@ class TestPcodePDF(unittest.TestCase):
self.assertEqual(pcode[('S_CMOVK_I32', 2)],
"if SCC then\nD0.i32 = 32'I(signext(SIMM16.i16))\nendif")
def test_swizzle_spans_blocks_and_pages(self):
for arch in ('rdna3', 'rdna4'):
with self.subTest(arch=arch):
code = self.pcode[arch][('DS_SWIZZLE_B32', 53)]
self.assertIn('} elsif (offset >= 0xc000) {', code)
self.assertIn('thread_out[i+3]', code)
self.assertIn('xor_mask = offset[14:10];', code)
self.assertEqual(code.count('{'), code.count('}'))
self.assertTrue(code.endswith('\n}'))
def test_pcode_no_examples(self):
"""Pseudocode should not contain example lines with '=>'."""
for name in ARCHS:
+2 -2
View File
@@ -4,7 +4,7 @@ from tinygrad import Tensor, GlobalCounters, dtypes, nn, Device, Variable
from tinygrad.helpers import Context, getenv, DEV
from tinygrad.engine.realize import run_linear, estimate_uop, compile_linear
from tinygrad.renderer.ptx import PTXRenderer
from test.helpers import needs_second_gpu, check_schedule, assert_kernel_count, KernelCountException
from test.helpers import needs_second_gpu, check_schedule, assert_kernel_count, KernelCountException, is_hcq2_device
class TestArange(unittest.TestCase):
def _get_flops(self, tensor, desired):
@@ -153,7 +153,7 @@ class TestIndexing(unittest.TestCase):
GlobalCounters.reset()
z = emb(x).realize()
self.assertLessEqual(GlobalCounters.global_ops, op_limit)
assert_kernel_count(2)
assert_kernel_count(3 if is_hcq2_device() else 2)
if getenv("CHECK", 1):
import torch
with torch.no_grad():
+50 -28
View File
@@ -4,7 +4,7 @@ import numpy as np
from tinygrad import Device, dtypes, Tensor, TinyJit, GlobalCounters, Variable
from tinygrad.uop.ops import Ops, UOp
from tinygrad.helpers import temp, DEV, Context
from test.helpers import assert_kernel_count, needs_second_gpu
from test.helpers import assert_kernel_count, needs_second_gpu, is_hcq2_device
N = 200 # has to be bigger than the cache to fail
@@ -43,7 +43,7 @@ class TestAssign(unittest.TestCase):
# it should copy into the empty buffer
GlobalCounters.reset()
c.realize()
assert_kernel_count(1)
assert_kernel_count(2 if is_hcq2_device() else 1)
def test_assign_slice(self):
X = Tensor([1,2,3,4]).realize()
@@ -499,14 +499,10 @@ class TestAssign(unittest.TestCase):
np.testing.assert_allclose(c.numpy(), [4.0, 3.0, 3.0, 4.0])
def test_assign_bitcast_different_size(self):
# assign to a shape-changing bitcast view (only works on DISK currently)
# assign to a shape-changing bitcast view
a = Tensor([0]*8, dtype=dtypes.uint8).realize()
a.bitcast(dtypes.int64).assign(Tensor([12345], dtype=dtypes.int64)).realize()
try:
np.testing.assert_equal(a.numpy(), [57, 48, 0, 0, 0, 0, 0, 0])
except AssertionError:
# TODO: broken now
np.testing.assert_equal(a.numpy(), [0]*8)
np.testing.assert_equal(a.numpy(), [57, 48, 0, 0, 0, 0, 0, 0])
def test_assign_dtype_mismatch(self):
# assign should not implicitly cast dtypes - this can lose precision
@@ -623,7 +619,7 @@ class TestAssign(unittest.TestCase):
contig.assign(Tensor([1, 4, 3], dtype=dtypes.int64))
GlobalCounters.reset()
base.assign(contig).realize()
assert_kernel_count(2) # TODO: first copy is dead, could be 1
assert_kernel_count(4 if is_hcq2_device() else 2) # TODO: first copy is dead, could be 1
self.assertEqual(base.tolist(), [1,4,3])
def test_nested_after_contiguous_store_no_init(self):
@@ -633,9 +629,17 @@ class TestAssign(unittest.TestCase):
contig.assign(Tensor([1, 4, 3], dtype=dtypes.int64))
GlobalCounters.reset()
base.assign(contig).realize()
assert_kernel_count(1)
assert_kernel_count(2 if is_hcq2_device() else 1)
self.assertEqual(base.tolist(), [1,4,3])
def test_assign_temporary_copy_reshape(self):
a = Tensor([[1., 2], [3, 4]], device="PYTHON")
c = Tensor.empty(2, 2).assign(a.to(None))
GlobalCounters.reset()
c.realize()
assert_kernel_count(2 if is_hcq2_device() else 1)
self.assertEqual(c.tolist(), [[1., 2], [3, 4]])
class TestAssignOrdering(unittest.TestCase):
"""Tests for complex assign orderings that could differ between lazy and eager execution.
@@ -969,20 +973,15 @@ class TestAssignOrdering(unittest.TestCase):
b.assign(a)
b.assign(Tensor.zeros(4))
b.realize()
try:
self.assertListEqual(a.tolist(), [7., 7., 7., 7.])
except AssertionError:
# TODO: broken now, b shares a's buffer, so the second assign to b overwrites a
self.assertListEqual(a.tolist(), [0., 0., 0., 0.])
self.assertListEqual(a.tolist(), [7., 7., 7., 7.])
def test_assign_to_function_output(self):
from tinygrad import function
@function
def f(x:Tensor) -> Tensor: return x*2
out = f(Tensor.ones(4).realize())
with self.assertRaisesRegex(RuntimeError, "UOp verification failed"): # TODO: broken now, raises
out.assign(Tensor.full((4,), 9.).realize())
self.assertListEqual(out.tolist(), [9., 9., 9., 9.])
out.assign(Tensor.full((4,), 9.).realize())
self.assertListEqual(out.tolist(), [9., 9., 9., 9.])
def test_nested_function_assign(self):
from tinygrad import function
@@ -1020,11 +1019,14 @@ class TestAssignToUnrealizedView(unittest.TestCase):
c = t.permute(1,0).contiguous() # unrealized CONTIGUOUS
self.assertIs(c.uop.base.op, Ops.CONTIGUOUS)
c[:, 1:2].assign(Tensor.ones(2,1, dtype=dtypes.int).contiguous().realize())
try:
self.assertEqual(c.tolist(), [[1,1],[2,1]])
except AssertionError:
# TODO: broken now
self.assertEqual(c.tolist(), [[1,3],[2,4]])
self.assertEqual(c.tolist(), [[1,1],[2,1]])
def test_clone(self):
t = Tensor([[1,2],[3,4]]).contiguous().realize()
c = t.permute(1,0).clone()
c[:, 1:2].assign(Tensor.ones(2,1, dtype=dtypes.int).contiguous().realize())
self.assertEqual(c.tolist(), [[1,1],[2,1]])
self.assertEqual(t.tolist(), [[1,2],[3,4]])
def test_contiguous_backward(self):
t = Tensor([[1,2],[3,4]]).contiguous().realize()
@@ -1037,6 +1039,13 @@ class TestAssignToUnrealizedView(unittest.TestCase):
# TODO: broken now
self.assertEqual(cb.tolist(), [[1,2],[3,4]])
def test_detach_realized_buffer_assignment(self):
base = Tensor([1., 2., 3.]).realize()
detached = base.detach()
detached.assign(detached + 1).realize()
self.assertEqual(detached.tolist(), [2., 3., 4.])
self.assertEqual(base.tolist(), [2., 3., 4.])
def test_detach_copy(self):
t = Tensor.zeros(2,2, dtype=dtypes.int).to("CPU:0").contiguous().realize()
d = t.to("CPU:1").detach() # DETACH(unrealized COPY)
@@ -1053,11 +1062,14 @@ class TestAssignToUnrealizedView(unittest.TestCase):
d = t.permute(1,0).contiguous().detach() # DETACH(unrealized CONTIGUOUS)
self.assertIs(d.uop.base.op, Ops.CONTIGUOUS)
d[:, 1:2].assign(Tensor.ones(2,1, dtype=dtypes.int).contiguous().realize())
try:
self.assertEqual(d.tolist(), [[1,1],[2,1]])
except AssertionError:
# TODO: broken now
self.assertEqual(d.tolist(), [[1,3],[2,4]])
self.assertEqual(d.tolist(), [[1,1],[2,1]])
def test_detach_clone(self):
t = Tensor([[1,2],[3,4]]).contiguous().realize()
d = t.permute(1,0).clone().detach()
d[:, 1:2].assign(Tensor.ones(2,1, dtype=dtypes.int).contiguous().realize())
self.assertEqual(d.tolist(), [[1,1],[2,1]])
self.assertEqual(t.tolist(), [[1,2],[3,4]])
def test_alu(self):
a = Tensor([1,2,3,4]).contiguous().realize()
@@ -1103,6 +1115,16 @@ class TestAssignToUnrealizedView(unittest.TestCase):
# TODO: broken now, silently dropped
self.assertEqual(c.tolist(), [[5,5],[5,5]])
def test_detach_assignment_preserves_earlier_update(self):
x = Tensor([1., 2.]).detach()
state = Tensor([0., 0.]).detach()
state.assign(state + x * 2)
result = state + 1
x.assign(x + 1).realize(state, result)
self.assertEqual(x.tolist(), [2., 3.])
self.assertEqual(state.tolist(), [2., 4.])
self.assertEqual(result.tolist(), [3., 5.])
class TestPartialAssignToSharedBuffer(unittest.TestCase):
def test_five_slices(self):
big = Tensor.zeros(50).contiguous().realize()
+16 -21
View File
@@ -9,7 +9,7 @@ from tinygrad.renderer.nir import NIRRenderer
from tinygrad import Context, Device, Tensor, dtypes
from hypothesis import given, settings, strategies as strat
from test.helpers import rand_for_dtype, min_normal
from test.unit.test_dtype_spec import _assert_eq, core_dtypes, dtype_ints, dtype_floats, FP8E4M3_MAX, FP8E5M2_MAX, FP8E4M3FNUZ_MAX, FP8E5M2FNUZ_MAX
from test.unit.test_dtype_spec import _assert_eq, core_dtypes, FP8E4M3_MAX, FP8E5M2_MAX, FP8E4M3FNUZ_MAX, FP8E5M2FNUZ_MAX
import pytest
pytestmark = pytest.mark.filterwarnings("ignore")
@@ -19,7 +19,8 @@ settings.load_profile("my_profile")
supported_dtypes = Device[Device.DEFAULT].renderer.supported_dtypes()
def get_available_cast_dtypes(dtype: DType) -> List[DType]:
dts = [v for k, v in DTYPES_DICT.items() if v != dtype and v in supported_dtypes or v in dtypes.fp8s+(dtypes.half,dtypes.bfloat16,dtypes.long)]
emulatable = dtypes.fp8s+(dtypes.half,dtypes.bfloat16,dtypes.long)
dts = [v for v in dict.fromkeys(DTYPES_DICT.values()) if v != dtype and (v in supported_dtypes or v in emulatable)]
if dtype in (dtypes.long, dtypes.ulong) and (dtype not in supported_dtypes or dtypes.long in EMULATED_DTYPES.tolist(dtypes)):
return [dt for dt in dts if dt != dtypes.double] # can't bitcast with no 64-bit support
if dtype not in supported_dtypes and dtype not in dtypes.fp8s+(dtypes.half,dtypes.bfloat16): return []
@@ -71,14 +72,14 @@ class TestDType(unittest.TestCase):
self.assertEqual(a.dtype, self.DTYPE)
_test_to_np(a, _to_np_dtype(self.DTYPE), np.array(self.DATA, dtype=_to_np_dtype(self.DTYPE)))
def test_casts_to(self):
for dtype in get_available_cast_dtypes(self.DTYPE):
_test_cast(Tensor(self.DATA, dtype=dtype), self.DTYPE)
def test_casts_from(self):
for dtype in get_available_cast_dtypes(self.DTYPE):
_test_cast(Tensor(self.DATA, dtype=self.DTYPE), dtype)
def test_const_kernel(self):
if not get_available_cast_dtypes(self.DTYPE): raise unittest.SkipTest("dtype does not run here")
_assert_eq(Tensor.ones((4,4), dtype=self.DTYPE).clone(), self.DTYPE, np.ones((4,4)))
def test_same_size_ops(self):
for dtype in get_available_cast_dtypes(self.DTYPE):
if dtype.itemsize == self.DTYPE.itemsize:
@@ -89,10 +90,10 @@ class TestDType(unittest.TestCase):
if dtype.itemsize > self.DTYPE.itemsize:
_test_ops(a_dtype=self.DTYPE, b_dtype=dtype)
def test_upcast_to_ops(self):
def test_downcast_ops(self):
for dtype in get_available_cast_dtypes(self.DTYPE):
if dtype.itemsize < self.DTYPE.itemsize:
_test_ops(a_dtype=dtype, b_dtype=self.DTYPE)
_test_ops(a_dtype=self.DTYPE, b_dtype=dtype)
def test_bitcast(self):
if self.DTYPE == dtypes.bool: raise unittest.SkipTest("no bools in bitcast")
@@ -112,12 +113,7 @@ def _test_ops(a_dtype:DType, b_dtype:DType, target_dtype=None):
target_dtype = target_dtype or least_upper_dtype(a_dtype, b_dtype)
if a_dtype == dtypes.bool or b_dtype == dtypes.bool: return
_assert_eq(Tensor([1,2,3,4], dtype=a_dtype)+Tensor([1,2,3,4], dtype=b_dtype), target_dtype, [2,4,6,8])
_assert_eq((Tensor([1], dtype=a_dtype).cast(b_dtype)+Tensor([1], dtype=a_dtype).cast(b_dtype)).cast(a_dtype), a_dtype, [2])
_assert_eq(Tensor([1,2,3,4], dtype=a_dtype)*Tensor([1,2,3,4], dtype=b_dtype), target_dtype, [1,4,9,16])
_assert_eq(Tensor([[1,2],[3,4]], dtype=a_dtype)@Tensor.eye(2, dtype=b_dtype), target_dtype, [[1,2],[3,4]])
_assert_eq(Tensor([1,1,1,1], dtype=a_dtype)+Tensor.ones((4,4), dtype=b_dtype), target_dtype, 2*np.ones((4,4)))
_assert_eq(Tensor([1,1,1,1], dtype=a_dtype)+Tensor.ones((4,4), dtype=b_dtype).clone(), target_dtype, 2*np.ones((4,4)))
_assert_eq(Tensor.ones((4,4), dtype=b_dtype).clone(), b_dtype, np.ones((4,4)))
class TestFp8sConversions(unittest.TestCase):
@given(strat.floats(width=32, allow_subnormal=True, allow_nan=False, allow_infinity=False, min_value=-FP8E4M3_MAX, max_value=FP8E4M3_MAX))
@@ -288,14 +284,10 @@ class TestUint8DType(TestDType):
_test_op(lambda: Tensor([255, 254, 253, 252], dtype=dtypes.uint8).cast(dtypes.int8), dtypes.int8, [-1, -2, -3, -4])
class TestBitCast(unittest.TestCase):
@given(strat.sampled_from(dtype_ints + dtype_floats), strat.sampled_from(dtype_ints + dtype_floats))
def test_shape_change_bitcast(self, dt1, dt2):
data = rand_for_dtype(dt1, 32).reshape(2, 2, 8)
a = Tensor(data, dtype=dt1)
expected = _to_torch_storage(a).view(_to_torch_dtype(dt2))
if dt2 in dtypes.fp8s:
expected = torch.tensor([fp8_to_float(x, dt2) for x in expected.view(-1).tolist()]).view_as(expected)
_test_op(lambda: a.bitcast(dt2), dt2, expected.tolist())
def test_shape_change_bitcast(self):
for dt1, dt2 in [(dtypes.uint8, dtypes.int64), (dtypes.int64, dtypes.uint8)]:
a = Tensor(rand_for_dtype(dt1, 32).reshape(2, 2, 8), dtype=dt1)
_test_op(lambda: a.bitcast(dt2), dt2, _to_torch_storage(a).view(_to_torch_dtype(dt2)).tolist())
def test_shape_change_bitcast_exceptions(self):
with self.assertRaises(RuntimeError):
@@ -401,6 +393,9 @@ class TestEmulatedFp8e5m2(TestFp8e5m2):
@classmethod
def tearDownClass(cls): cls.stack.close()
class TestFp8e4m3fnuz(TestDType): DTYPE = dtypes.fp8e4m3fnuz
class TestFp8e5m2fnuz(TestDType): DTYPE = dtypes.fp8e5m2fnuz
class TestImplicitFunctionTypeChange(unittest.TestCase):
def test_functions(self):
result = []
+2 -3
View File
@@ -2,12 +2,11 @@
import unittest
import numpy as np
from test.helpers import assert_jit_cache_len, call_is_graph, not_support_multi_device, needs_second_gpu, KernelCountException
from test.helpers import is_hcq2_device, assert_jit_cache_len, call_is_graph, not_support_multi_device, needs_second_gpu, KernelCountException
from test.unit.test_jit import _simple_test
from tinygrad import Tensor, TinyJit, Device, dtypes
from tinygrad.engine.jit import graph_class
from tinygrad.helpers import JIT, DEV, GlobalCounters
from tinygrad.runtime.support.hcq2 import HCQ_DEVS
from tinygrad.uop.ops import Ops
from tinygrad.renderer.isa.x86 import X86Renderer
@@ -223,7 +222,7 @@ class TestJitPrune(unittest.TestCase):
assert_jit_cache_len(w2_prune, 1)
class TestJitFree(unittest.TestCase):
@unittest.skipIf(Device.DEFAULT.split(":")[0] in HCQ_DEVS - {"CPU"}, "hcq2 keeps refs to intermediate buffers")
@unittest.skipIf(is_hcq2_device(), "hcq2 keeps refs to intermediate buffers")
def test_free_intermediates(self):
ext_tensor = Tensor([1,24,23,45,1])
@TinyJit
+11 -2
View File
@@ -3,7 +3,7 @@ from tinygrad import Tensor, Device, nn, GlobalCounters, TinyJit, dtypes, Variab
from tinygrad.uop.ops import Ops, UOp, AxisType, graph_rewrite
from tinygrad.helpers import getenv, prod, Context
from tinygrad.nn.state import get_parameters
from tinygrad.engine.realize import run_linear, compile_linear, lower_and_compile, pm_beam
from tinygrad.engine.realize import run_linear, lower_and_compile, pm_beam
import numpy as np
from hypothesis import given, strategies as strat, settings
from test.helpers import not_support_multi_device, needs_second_gpu, slow, call_is_graph, check_schedule, assert_kernel_count, KernelCountException
@@ -76,7 +76,7 @@ class TestMultiTensor(unittest.TestCase):
X = Tensor.ones(256).contiguous().realize()
X.shard_(devices_2, 0)
out = (X + X)
linear = compile_linear(out.schedule_linear())
linear = lower_and_compile(out.schedule_linear())
uops = [call.src[0].src[0] for call in linear.src if call.src[0].op is Ops.PROGRAM]
run_linear(linear)
self.assertEqual(len(set(uops)), 1, "function was relinearized")
@@ -460,6 +460,15 @@ class Test2DShard(unittest.TestCase):
out = t.contiguous().realize()
np.testing.assert_equal(out.numpy(), ref.numpy())
def test_2d_shard_clone(self):
ref = Tensor.arange(16).reshape(4, 4).realize()
t = self._shard_2d(ref)
out = t.clone().realize()
np.testing.assert_equal(out.numpy(), ref.numpy())
out.assign(out + 1).realize()
np.testing.assert_equal(out.numpy(), ref.numpy() + 1)
np.testing.assert_equal(t.numpy(), ref.numpy())
def test_2d_shard_elementwise(self):
ref = Tensor.arange(16).reshape(4, 4).contiguous().realize()
t = self._shard_2d(ref)
+7 -3
View File
@@ -6,7 +6,6 @@ from tinygrad.helpers import getenv, DEBUG, DEV, IMAGE, Context
from tinygrad import Tensor, Device, dtypes
from tinygrad.tensor import _to_np_dtype
from tinygrad.renderer.nir import NIRRenderer
from tinygrad.renderer.isa.x86 import X86Renderer
TINY_BACKEND = getenv("TINY_BACKEND")
if TINY_BACKEND:
@@ -816,8 +815,6 @@ class TestOps(unittest.TestCase):
helper_test_op([], lambda: tor^0x1337, lambda: ten^0x1337, forward_only=True)
helper_test_op([], lambda: 0x1337^tor, lambda: 0x1337^ten, forward_only=True)
# TODO: x86 PARAM dtype fails SPEC=2
@Context(SPEC=1 if isinstance(Device[Device.DEFAULT].renderer, X86Renderer) else 2)
def test_and(self):
data = [[1,-8,1],[32,1,6]]
tor = torch.tensor(data, dtype=torch.int)
@@ -3110,6 +3107,13 @@ class TestOps(unittest.TestCase):
lambda x: x.gather(dim=0, index=Tensor([2, 1, 0, 1, 2])),
vals=[[-float("inf"), 2., 3.]])
def test_gather_bool_index(self):
helper_test_op(None, lambda x,y: x.gather(dim=0, index=y.bool().long()),
lambda x,y: x.gather(dim=0, index=y.cast(dtypes.bool).cast(dtypes.int)),
vals=[[1., 2., 3.], [0.5, 0., 2.]], forward_only=True)
helper_test_op(None, lambda x,y: x[y.bool().long()], lambda x,y: x[y.cast(dtypes.bool).cast(dtypes.int)],
vals=[[1., 2., 3.], [0.5, 0., 2.]], forward_only=True)
def test_scatter(self):
b = torch.randint(3, size=[3,4,5], dtype=torch.int64, requires_grad=False)
a = Tensor(b.detach().cpu().numpy().astype(np.int32), dtype=dtypes.int32)
+12
View File
@@ -3,6 +3,7 @@ from tinygrad import Device, Tensor, dtypes, TinyJit
from tinygrad.helpers import DEV, Context, ProfileRangeEvent, cpu_profile, cpu_events, ProfilePointEvent, dedup
from tinygrad.device import Buffer, BufferSpec, Compiled, ProfileDeviceEvent, ProfileGraphEvent
from tinygrad.runtime.support.hcq import HCQCompiled
from tinygrad.runtime.support.hcq2 import HCQ2Compiled
from tinygrad.engine.realize import get_runtime
from tinygrad.codegen import to_program
@@ -34,7 +35,18 @@ def helper_profile_filter_device(profile, device:str):
assert len(dev_events) == 1, "only one device registration event is expected"
return [x for x in profile if getattr(x, "device", None) == device], dev_events[0]
@unittest.skipUnless(isinstance(Device[Device.DEFAULT], (HCQCompiled, HCQ2Compiled)) or Device.DEFAULT == "METAL", "Dev not supported")
class TestSimpleProfiler(unittest.TestCase):
@unittest.skipIf(Device.DEFAULT == "CPU", "fails in CPU")
def test_profiler(self):
start = len(Compiled.profile_events)
with Context(PROFILE=1):
Tensor.empty(32).add(1).realize()
Device[Device.DEFAULT].synchronize()
self.assertTrue(any(isinstance(e, (ProfileRangeEvent, ProfileGraphEvent)) for e in Compiled.profile_events[start:]))
# TODO: support in HCQCompiled
# TODO: support these tests in HCQ2
is_cpu_hcq = Device.DEFAULT in {"CPU"}
@unittest.skipUnless((issubclass(type(Device[Device.DEFAULT]), HCQCompiled) and not is_cpu_hcq) or Device.DEFAULT in {"METAL"}, "Dev not supported")
+122 -1
View File
@@ -1,4 +1,4 @@
import unittest
import unittest, operator
from tinygrad import Tensor, TinyJit, Variable, dtypes, Device
from tinygrad.helpers import Context
import numpy as np
@@ -75,6 +75,29 @@ class TestSetitem(unittest.TestCase):
t.detach()[1, 2] = 5
self.assertEqual(t[1, 2].item(), 5.0)
def test_setitem_depends_on_earlier_view_assignment(self):
x = Tensor.ones(4).clone()
x[:2].assign(x[:2] + 1)
x[2:] = x[:2] * 3
self.assertEqual(x.tolist(), [2., 2., 6., 6.])
def test_invalid_setitem_preserves_earlier_view_assignment(self):
x = Tensor.ones(4).clone()
x[:2].assign(x[:2] + 1)
with self.assertRaises(IndexError): x[99] = (x[:2] * 3).sum()
self.assertEqual(x.tolist(), [2., 2., 1., 1.])
def test_setitem_identity_after_other_view_assignment(self):
x = Tensor.ones(4).clone()
x[:2].assign(x[:2] + 1)
x[2:] = x[2:]
self.assertEqual(x.tolist(), [2., 2., 1., 1.])
def test_setitem_detach_whole(self):
t = Tensor.zeros((3, 3)).realize()
t.detach()[:] = 5
np.testing.assert_equal(t.numpy(), np.full((3, 3), 5.))
def test_setitem_permute(self):
# setitem on permuted tensor should modify original
t = Tensor.zeros((2, 3)).contiguous().realize()
@@ -342,6 +365,78 @@ class TestWithGrad(unittest.TestCase):
np.testing.assert_allclose(x.grad.numpy(), [1, 1, 0, 0])
np.testing.assert_allclose(y.grad.numpy(), np.ones(4))
def test_set_iadd_clone_backward(self):
source = Tensor([1., 2., 3., 4.]).realize()
x = source.clone()
increment = Tensor([10., 20.]).realize()
x[:2] += increment
x.sum().backward()
self.assertEqual(x.tolist(), [11., 22., 3., 4.])
self.assertEqual(source.grad.tolist(), [1., 1., 1., 1.])
self.assertEqual(increment.grad.tolist(), [1., 1.])
def test_set_imul_clone_backward(self):
source = Tensor([1., 2., 3., 4.]).realize()
x = source.clone()
factor = Tensor([10., 20.]).realize()
x[::2] *= factor
x.sum().backward()
self.assertEqual(x.tolist(), [10., 2., 60., 4.])
self.assertEqual(source.grad.tolist(), [10., 1., 20., 1.])
self.assertEqual(factor.grad.tolist(), [1., 3.])
def test_set_imul_clone_chained_backward(self):
source = Tensor([1., 2., 3., 4.]).realize()
x = source.clone()
f, g = Tensor([10., 20.]).realize(), Tensor([30., 40.]).realize()
x[:2] *= f
x[1:3] *= g
x.sum().backward()
self.assertEqual(x.tolist(), [10., 1200., 120., 4.])
self.assertEqual(source.grad.tolist(), [10., 600., 40., 1.])
self.assertEqual(f.grad.tolist(), [1., 60.])
self.assertEqual(g.grad.tolist(), [40., 3.])
def test_imul_clone_backward(self):
source = Tensor([1., 2., 3.]).realize()
x = source.clone()
factor = Tensor([10., 20., 30.]).realize()
x *= factor
x.sum().backward()
self.assertEqual(x.tolist(), [10., 40., 90.])
self.assertEqual(source.grad.tolist(), [10., 20., 30.])
self.assertEqual(factor.grad.tolist(), [1., 2., 3.])
def test_imul_clone_squared_backward(self):
for dtype in (dtypes.float32, dtypes.bfloat16):
with self.subTest(dtype=dtype):
source = Tensor([2., 3.], dtype=dtype).realize()
x = source.float().clone()
x *= source.float()
grad = (x*x).sum().gradient(source)[0]
self.assertEqual(x.tolist(), [4., 9.])
self.assertEqual(grad.tolist(), [32., 108.])
self.assertEqual(source.tolist(), [2., 3.])
def test_set_imul_transposed_clone_backward(self):
source = Tensor([[1., 2.], [3., 4.]]).realize()
x = source.clone()
factor = Tensor([[10., 20.]]).realize()
x.T[:1] *= factor
grad = x.sum().gradient(factor)[0]
self.assertEqual(x.tolist(), [[10., 2.], [60., 4.]])
self.assertEqual(grad.tolist(), [[1., 3.]])
def test_partial_assign_clone_backward(self):
source = Tensor([1., 2., 3., 4.]).realize()
x = source.clone()
replacement = Tensor([10., 20.]).realize()
x[::2].assign(replacement)
(x * Tensor([1., 2., 3., 4.])).sum().backward()
self.assertEqual(x.tolist(), [10., 2., 20., 4.])
self.assertEqual(source.grad.tolist(), [0., 2., 0., 4.])
self.assertEqual(replacement.grad.tolist(), [1., 3.])
def test_set_iadd_backward(self):
z = Tensor([1.0, 2.0, 3.0, 4.0])
x = Tensor([10.0, 20.0])
@@ -378,6 +473,32 @@ class TestWithGrad(unittest.TestCase):
with self.assertRaises(RuntimeError):
y[0] = 99.0
def test_unrealized_inplace_keeps_storage(self):
x = Tensor([1., 2.]).clone()
view = x[:1]
x += 3
x.realize()
self.assertEqual(x.tolist(), [4., 5.])
self.assertEqual(view.tolist(), [4.])
def test_unrealized_view_inplace_keeps_storage(self):
x = Tensor([1., 2.]).clone()
view = x[:1]
view += 3
view.realize()
self.assertEqual(x.tolist(), [4., 2.])
self.assertEqual(view.tolist(), [4.])
def test_set_augmented_backward(self):
for op, expected in ((operator.isub, [-1., -1.]), (operator.imul, [1., 2.]), (operator.itruediv, [-0.01, -0.005])):
with self.subTest(op=op.__name__):
z = Tensor([1.0, 2.0, 3.0, 4.0])
x = Tensor([10.0, 20.0])
z[:2] = op(z[:2], x)
z.sum().backward()
np.testing.assert_allclose(z.grad.numpy(), np.ones(4))
np.testing.assert_allclose(x.grad.numpy(), expected)
class TestSetitemLoop(unittest.TestCase):
def test_arange(self):
N = 10
+13 -2
View File
@@ -11,6 +11,7 @@ from tinygrad.engine.realize import run_linear
from tinygrad.codegen import to_program
from tinygrad.codegen.opt import Opt, OptOps
from tinygrad.renderer.ptx import PTXRenderer
from tinygrad.runtime.ops_python import PythonRenderer
from test.helpers import to_uops_list
def run_uops(uops_list:list[UOp], bufs:list[Buffer]):
@@ -56,8 +57,8 @@ def _test_uops_result(output_dtype, uops, res):
run_uops([out], [buf])
return np.frombuffer(buf.as_memoryview(), _to_np_dtype(output_dtype))[0]
@unittest.skipUnless(isinstance(Device[Device.DEFAULT].renderer, CStyleLanguage) and
dtypes.uint64 in Device[Device.DEFAULT].renderer.supported_dtypes(), "requires C-style pointer bitcast and 64-bit ints")
@unittest.skipUnless(isinstance(Device[Device.DEFAULT].renderer, (CStyleLanguage, PythonRenderer)) and
dtypes.uint64 in Device[Device.DEFAULT].renderer.supported_dtypes(), "requires buffer bitcast and 64-bit ints")
class TestBitcastBufferView(unittest.TestCase):
@Context(SPEC=2)
def test_render(self):
@@ -85,6 +86,16 @@ class TestBitcastBufferView(unittest.TestCase):
run_uops([view.index(0).store(val ^ 0xff), view.index(1).store(val)], [buf])
self.assertEqual(np.frombuffer(buf.as_memoryview(), dtype=np.uint64, count=2, offset=4).tolist(), [val ^ 0xff, val])
def test_vector_load_store(self):
for src_dt, dst_dt in [(dtypes.uint8, dtypes.uint32), (dtypes.uint32, dtypes.uint8)]:
with self.subTest(src=src_dt, dst=dst_dt):
src, dst = [UOp.param(i, dt, 16 // dt.itemsize) for i, dt in enumerate((src_dt, dst_dt))]
src, dst = [b.bitcast(dtypes.uint32).index(UOp.stack(*[UOp.const(i) for i in range(4)])) for b in (src, dst)]
bufs = [Buffer(Device.DEFAULT, 16 // dt.itemsize, dt, initial_value=bytes(range(16)) if i == 0 else bytes(16))
for i, dt in enumerate((src_dt, dst_dt))]
run_uops([dst.store(src.load())], bufs)
self.assertEqual(bytes(bufs[1].as_memoryview()), bytes(range(16)))
class TestUOps(unittest.TestCase):
def _equal(self, v1, v2):
assert isinstance(v2, (float, int, bool))
+36 -5
View File
@@ -1,13 +1,12 @@
import unittest, threading
from tinygrad import Tensor, UOp
import unittest, threading, functools
from tinygrad import Tensor, UOp, Context
from tinygrad.device import Device, Buffer, BufferSpec
from tinygrad.dtype import AddrSpace, dtypes
from tinygrad.engine.realize import run_linear
from tinygrad.uop.ops import Ops, KernelInfo
from tinygrad.renderer.isa.x86 import X86Renderer
def wait_loop_kernel(C:UOp) -> UOp:
N = 10
def wait_loop_kernel(C:UOp, N=10) -> UOp:
# a RANGE with no src is a bound-less loop header: a jump target with no induction variable.
# the compare and conditional backedge are expanded by the renderers from the loop RANGE/END
l = UOp.loop(0)
@@ -42,6 +41,19 @@ def nested_loop_kernel(C:UOp) -> UOp:
return C[0].store(i[0].load()).sink(arg=KernelInfo(name="nested_loop", opts_to_apply=()))
def pressure_loop_kernel(C:UOp, n=13) -> UOp:
vs = [C[j+1].load() for j in range(n)]
l = UOp.loop(0)
i = UOp.placeholder((1,), dtypes.int, 0, addrspace=AddrSpace.REG)
i = i.after(i[0].store(0))
inc = i.after(l)[0].load() + 1
st = i[0].store(inc)
i = i.after(st.end(l, inc < sum(v & inc for v in vs)))
return C[0].store(i[0].load()).sink(arg=KernelInfo(name="pressure_loop", opts_to_apply=()))
def wait_ext_kernel() -> UOp:
sig = UOp.param(0, dtypes.int, 1, volatile=True)
l = UOp.loop(0)
@@ -100,6 +112,25 @@ class TestWaitLoop(unittest.TestCase):
c.realize()
self.assertEqual(c.item(), 25)
# TODO: x86's lower_loop builds an Ops.IF node after regalloc, which fails spec_full
@(unittest.expectedFailure if isinstance(Device[Device.DEFAULT].renderer, X86Renderer) else lambda f: f)
def test_wait_loop_spec(self):
c = Tensor.custom_kernel(Tensor.empty(1, dtype=dtypes.int), fxn=functools.partial(wait_loop_kernel, N=7))[0]
with Context(SPEC=2): c.realize()
self.assertEqual(c.item(), 7)
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, X86Renderer), "TODO: do-while loop under register pressure segfaults on x86")
def test_loop_carried_registers(self):
# more loads live across the backedge than any register file (x86 15 gprs, arm64 31, sass 255, rdna3 256 vgprs)
c = Tensor.custom_kernel(Tensor.ones(301, dtype=dtypes.int), fxn=functools.partial(pressure_loop_kernel, n=300))[0]
self.assertEqual(c[0].item(), 2)
def test_register_pressure_loop(self):
c = Tensor.zeros(16, dtype=dtypes.int).contiguous()
c = Tensor.custom_kernel(c, fxn=pressure_loop_kernel)[0]
c.realize()
self.assertEqual(c[0].item(), 1)
def test_loop_in_loop(self):
c = Tensor.empty(1, dtype=dtypes.int)
c = Tensor.custom_kernel(c, fxn=loop_in_loop_kernel)[0]
+151 -38
View File
@@ -1,11 +1,11 @@
import unittest, contextlib, ctypes, numpy as np
import unittest, contextlib, ctypes, gc, numpy as np
from unittest.mock import patch
from tinygrad import Device, Tensor, TinyJit, Variable, dtypes
from tinygrad.device import Buffer
from tinygrad import Device, Tensor, TinyJit, Variable, dtypes, GlobalCounters
from tinygrad.device import Buffer, BufferSpec
from tinygrad.dtype import AddrSpace
from tinygrad.helpers import Context, dedup, partition
from tinygrad.uop.ops import Ops, UOp, KernelInfo
from tinygrad.engine.realize import lower_and_compile, run_linear
from tinygrad.uop.ops import Ops, UOp, UPat, PatternMatcher, KernelInfo
from tinygrad.engine.realize import compile_linear, link_linear, lower_and_compile, run_linear
from tinygrad.renderer.cstyle import CStyleLanguage
from tinygrad.runtime.autogen import libc
from tinygrad.runtime.support.c import init_c_struct_t
@@ -16,7 +16,14 @@ from test.helpers import call_is_hcq
@contextlib.contextmanager
def rt_views():
calls, orig = [], HCQ2Compiled.rt_view
with patch.object(HCQ2Compiled, "rt_view", lambda s, *a, **kw: (calls.append(s), orig(s, *a, **kw))[1]): yield calls
def track(dev, *args, **kwargs):
calls.append(dev)
return orig(dev, *args, **kwargs)
with patch.object(HCQ2Compiled, "rt_view", track): yield calls
def chain(x:Tensor, n:int) -> Tensor:
for _ in range(n): x = (x + 1).contiguous()
return x
@contextlib.contextmanager
def encoded_batches():
@@ -24,6 +31,10 @@ def encoded_batches():
with patch.object(hcq2, "lower_and_compile", lambda l, *a, **kw: (batches.extend(c for c in l.src if call_is_hcq(c)), orig(l, *a, **kw))[1]):
yield batches
def eager_chain(x:Tensor, n:int=64) -> Tensor: # at hcq_compile's use_rt bound: an eager linear this big bakes its inputs and borrows ring slots
for _ in range(n): x = (x + 1).contiguous()
return x.realize()
def patch_words(batch:UOp) -> list[UOp]:
return [w for s in batch.src[0].toposort() if s.op is Ops.STORE and s.src[0].op is Ops.INDEX and s.src[0].src[1].op is Ops.STACK
and s.src[1].op is Ops.STACK for w in s.src[1].src]
@@ -31,41 +42,90 @@ def patch_words(batch:UOp) -> list[UOp]:
def rt_params(batch:UOp) -> list[str]:
return dedup([u.arg.name for w in patch_words(batch) for u in w.toposort() if u.op is Ops.PARAM and u.arg.addrspace is AddrSpace.GLOBAL])
class TestHCQ2Deps(unittest.TestCase):
def test_disjoint_write_preserves_dependencies(self):
b = UOp.param(0, dtypes.uint8, 16, device="CPU")
for write in ([], [0]):
tracker = hcq2.HCQDepsTracker()
tracker.access_resources([b.shrink(((0, 4),))], write, 0)
self.assertEqual(tracker.access_resources([b.shrink(((4, 8),))], [0], 1), [])
self.assertEqual(tracker.access_resources([b.shrink(((0, 4),))], [0], 2), [0])
def test_partial_write_preserves_dependencies(self):
b = UOp.param(0, dtypes.uint8, 16, device="CPU")
for write in ([], [0]):
tracker = hcq2.HCQDepsTracker()
tracker.access_resources([b], write, 0)
self.assertEqual(tracker.access_resources([b.shrink(((4, 12),))], [0], 1), [0])
self.assertEqual(tracker.access_resources([b.shrink(((0, 4),))], [0], 2), [0])
self.assertEqual(tracker.access_resources([b.shrink(((12, 16),))], [0], 3), [0])
self.assertEqual(tracker.access_resources([b.shrink(((4, 12),))], [], 4), [1])
@unittest.skipUnless(all_devices_in(Device.DEFAULT, HCQ_DEVS - {"CPU"}), "non-CPU hcq2 device required")
class TestHCQ2Core(unittest.TestCase):
@staticmethod
def input(value:int=2) -> Tensor: return Tensor.full((4,), value, dtype=dtypes.int32).contiguous().realize()
def compiled(self, n:int, jit=False):
x, inputs = self.input(), []
if jit:
f = TinyJit(lambda a: chain(a, n).realize())
f(x)
return f(x), f.captured._linear, [x.uop.base]
out = chain(x, n)
return out, compile_linear(out.schedule_linear(), input_uops=inputs), inputs
def test_jit_has_no_rt_buffers(self):
x = Tensor.ones(16).contiguous().realize()
@TinyJit
def f(a): return (a + 2).contiguous().realize()
f(x)
before = len(link_linear_cache)
with rt_views() as calls:
out = f(x)
self.assertGreater(len(link_linear_cache), before)
self.assertEqual(len(calls), 0)
(x + 1).contiguous().realize()
self.assertGreater(len(calls), 0)
self.assertEqual(out.tolist(), [3.0] * 16)
def test_jit_survives_ring_wrap(self):
# the ring recycles with no liveness tracking, so eager work that wraps it must not land on the jit's buffers
dev = Device[Device.DEFAULT]
allocs = {host:dev.rt_allocator(True, host) for host in (False, True)}
for host in allocs: dev.rt_buffer(True, host) # cache the full-sized backing buffers before temporarily shrinking their allocators
with patch.object(allocs[False], "size", 1 << 13), patch.object(allocs[True], "size", 1 << 13):
x = Tensor.ones(24).contiguous().realize()
@TinyJit
def g(a): return (a * 3 - 1).contiguous().realize()
for _ in range(3): g(x)
rings = [dev.rt_buffer(True, host) for host in (False, True)]
ranges = [(b._buf.va_addr, b._buf.va_addr + b.nbytes) for b in rings]
for n in (1, 65):
with self.subTest(kernels=n):
x, f = self.input(), TinyJit(lambda a: chain(a, n).realize())
for _ in range(2): f(x)
for u in f.captured.linear.toposort():
if u.op is Ops.BUFFER and (buf:=u.buffer).device == dev.device:
addr = buf._buf.va_addr
self.assertFalse(any(addr < end and start < addr + buf.nbytes for start, end in ranges))
wrapped = 0
for i in range(48):
before = dev.rt_allocator(True, False).ptr
(x + i).contiguous().realize()
wrapped += dev.rt_allocator(True, False).ptr < before
self.assertEqual(g(x).tolist(), [2.0] * 24)
self.assertGreater(wrapped, 0)
def test_small_eager_cached(self):
_, compiled, inputs = self.compiled(1)
linked = link_linear(compiled, input_uops=inputs)
self.assertIs(link_linear(compiled, input_uops=inputs), linked)
def test_large_eager_not_cached(self):
_, compiled, inputs = self.compiled(65)
linked = link_linear(compiled, input_uops=inputs)
self.assertIsNot(link_linear(compiled, input_uops=inputs), linked)
self.assertNotIn(compiled, link_linear_cache)
def test_double_compile(self):
for n in (1, 65):
for jit in (False, True):
with self.subTest(kernels=n, jit=jit):
out, compiled, inputs = self.compiled(n, jit=jit)
linked = link_linear(compiled, input_uops=inputs, allow_cache=not jit)
before = tuple(inputs)
with rt_views() as borrowed:
for linear in (compiled, linked):
self.assertIs(compile_linear(linear, input_uops=None if jit else inputs), linear)
self.assertEqual(tuple(inputs), before)
self.assertFalse(borrowed)
run_linear(linked, input_uops=inputs, jit=True, wait=True)
self.assertEqual(out.tolist(), [2 + n] * 4)
def test_double_link(self):
for n in (1, 65):
for jit in (False, True):
with self.subTest(kernels=n, jit=jit):
out, compiled, inputs = self.compiled(n, jit=jit)
linked = link_linear(compiled, input_uops=inputs, allow_cache=not jit)
with rt_views() as borrowed:
again = link_linear(linked, input_uops=inputs, allow_cache=not jit)
self.assertIs(again, linked)
self.assertFalse(borrowed)
run_linear(again, input_uops=inputs, jit=True, wait=True)
self.assertEqual(out.tolist(), [2 + n] * 4)
def test_jit_new_inputs_each_call(self):
@TinyJit
@@ -85,6 +145,13 @@ class TestHCQ2Core(unittest.TestCase):
vi = Variable("i", 1, 10).bind(i)
np.testing.assert_allclose(f(a[:, :vi]).item(), (a[:, :i] + 1).sum().item(), atol=1e-5, rtol=1e-5)
def test_map_cpu_buffer_preserves_contents(self):
src = Buffer("CPU", 16, dtypes.uint8, preallocate=True)
data = bytes(range(16))
src.as_memoryview(force_zero_copy=True)[:] = data
src.get_buf(Device.DEFAULT)
self.assertEqual(bytes(src.as_memoryview(force_zero_copy=True)), data)
def test_staged_copy_roundtrip(self):
# a host buffer the device cannot read copies in chunks through a small ring of staging slots: every rotation must land bit-exact
stage = Buffer("CPU", size:=1 << 16, dtypes.uint8, preallocate=True)
@@ -102,6 +169,7 @@ class TestHCQ2Core(unittest.TestCase):
@TinyJit
def f(a): return (a.sin() * 3).contiguous().realize()
for _ in range(3): f(x)
eager_chain(x)
jit, eager = partition(batches, lambda c: c.arg.aux.table >= 0)
self.assertTrue(jit and eager, f"want both kinds of batch, got {len(jit)} jit and {len(eager)} eager")
@@ -124,9 +192,24 @@ class TestHCQ2Core(unittest.TestCase):
return max(c.arg.aux.nargs for c in batches)
self.assertEqual(nargs(2), nargs(12))
def test_caches_hold_no_buffers(self):
# an eager template caches without its buffers and the jit's linear compiles once uncached: freeing the tensors frees the device memory
def step(i):
x = Tensor(np.full(1024, i, np.float32)).to(Device.DEFAULT).realize()
@TinyJit
def f(a): return (a * 2 + 1).contiguous().realize()
for _ in range(3): out = f(x)
self.assertEqual(out.tolist(), [2.0 * i + 1] * 1024)
step(1) # warms the programs, templates and rings
gc.collect()
used = GlobalCounters.mem_used
for i in range(2, 5): step(i)
gc.collect()
self.assertEqual(GlobalCounters.mem_used, used)
def test_device_state_survives_as_link_refs(self):
# a buffer the commands only address, never a param of the body, is kept by the linked call as a ref of what its getaddr resolved into
dev, names = Device[Device.DEFAULT], {"AMD": ("scratch",), "QCOM": ("_stack", "dummy")}[Device.DEFAULT.split(":")[0]]
dev, names = Device[Device.DEFAULT], {"AMD": ("scratch",), "NV": ("timeline",), "QCOM": ("_stack", "dummy")}[Device.DEFAULT.split(":")[0]]
@TinyJit
def f(a): return (a * 2 + 1).contiguous().realize()
x = Tensor.ones(16).contiguous().realize()
@@ -142,7 +225,7 @@ class TestHCQ2FFI(unittest.TestCase):
def _run(body:UOp) -> list[Buffer]:
call = hcq2.lower_call(UOp.sink(body, arg=KernelInfo("test_ffi")).call(aux=hcq2.HCQInfo(("CPU",))))
assert call is not None
linear = hcq2.hcq_link(lower_and_compile(UOp(Ops.LINEAR, src=(call,))), cache=False)
linear = hcq2.hcq_link(lower_and_compile(UOp(Ops.LINEAR, src=(call,))), allow_cache=False)
run_linear(linear, jit=True)
return [u.buffer for u in linear.src[0].without_after.src[1:] if u.op is Ops.BUFFER]
@@ -162,6 +245,36 @@ class TestHCQ2FFI(unittest.TestCase):
got = struct_t.from_buffer_copy(bytes(next(b for b in bufs if b.nbytes == ctypes.sizeof(struct_t))._buf.cpu_view()))
self.assertEqual((got.u8, got.u16, got.u32, got.u64), (0x12, 0x3456, 0x789ABCDE, 0xFEDCBA9876543210))
def test_device_lower_after_encode(self):
with Context(HCQ_RUNTIME_DEV="CPU"):
out = UOp.placeholder((1,), dtypes.int32, device="CPU", tag="result")
encode = PatternMatcher([(UPat(Ops.CUSTOM_FUNCTION, arg="test_encode"), lambda: UOp.custom_function("test_lower"))])
lower = PatternMatcher([(UPat(Ops.CUSTOM_FUNCTION, arg="test_lower"), lambda out=out: out.index(0).store(42))])
with patch.object(Device["CPU"], "pm_encode", encode), patch.object(Device["CPU"], "pm_lower", lower):
bufs = self._run(UOp.custom_function("test_encode"))
self.assertEqual(next(b for b in bufs if b.dtype is dtypes.int)._buf.cpu_view().view(fmt='i')[0], 42)
def test_nested_cstruct_patches(self):
with Context(HCQ_RUNTIME_DEV="CPU"):
inner = hcq2.cstruct(init_c_struct_t(4, (("value", ctypes.c_uint32, 0),)), value=42)
outer = hcq2.cstruct(init_c_struct_t(8, (("ptr", ctypes.c_uint64, 0),)), ptr=inner.getaddr("CPU"))
out = UOp.placeholder((1,), dtypes.uint32, device="CPU", tag="result")
copied = hcq2.ccall(libc.memcpy, out.index(0), outer.bitcast(dtypes.uint64).index(0).load(), 4)
bufs = self._run(out.after(copied).index(0).load())
self.assertEqual(next(b for b in bufs if b.dtype is dtypes.uint32)._buf.cpu_view().view(fmt='I')[0], 42)
class TestHCQ2Timeline(unittest.TestCase):
def test_reused_timeline_is_zeroed(self):
buf = Buffer("CPU", 2, dtypes.uint64, options=BufferSpec(host=True, uncached=True, cpu_access=True), preallocate=True)
addr = buf._buf.va_addr
buf._buf.cpu_view().view(fmt='B')[:] = b'\xff' * 16
buf.deallocate()
dev = HCQ2Compiled.__new__(HCQ2Compiled)
dev.device = "CPU"
self.assertEqual(dev.timeline._buf.va_addr, addr)
self.assertEqual(bytes(dev.timeline._buf.cpu_view()), bytes(16))
if __name__ == "__main__":
unittest.main()
+65 -1
View File
@@ -3,13 +3,14 @@ from tinygrad.helpers import Timing, getenv
from tinygrad import Tensor, Device
import numpy as np
class TestDevCopySpeeds(unittest.TestCase):
class USBTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.sz = getenv("SIZE", 2000000)
cls.dev = Device["AMD"]
if not cls.dev.is_usb(): raise unittest.SkipTest("only test this on USB devices")
class TestDevCopySpeeds(USBTestCase):
def testCopyCPUtoDefault(self):
for _ in range(10):
t = Tensor.ones(self.sz, device="CPU", dtype='uchar').contiguous().realize()
@@ -24,6 +25,7 @@ class TestDevCopySpeeds(unittest.TestCase):
with Timing(f"copyout of {t.nbytes()/1e6:.2f} MB: ", on_exit=lambda ns: f" @ {t.nbytes()/ns * 1e3:.2f} MB/s"):
t.to('CPU').realize()
class TestUSBIntegrity(USBTestCase):
def testValidateCopies(self):
t = Tensor.randn(self.sz, device="CPU", dtype='uchar').contiguous().realize()
x = t.to(Device.DEFAULT).realize()
@@ -34,5 +36,67 @@ class TestDevCopySpeeds(unittest.TestCase):
np.testing.assert_equal(t.numpy(), y.numpy())
del x, y, t
def testCopyinBoundaries(self):
rng, chunk = np.random.default_rng(0), 0x40000 - 4
for size in (1, 3, 508, 509, 0x3ffc, 0x3ffd, chunk, chunk+1, 2*chunk+31):
with self.subTest(size=size):
a = rng.integers(0, 256, size, dtype=np.uint8)
np.testing.assert_array_equal(a, Tensor(a, device="AMD").numpy())
def testCopyinFenceWrap(self):
a = np.arange(2*(0x40000-4)+31, dtype=np.uint8)
np.testing.assert_array_equal(a[:31], Tensor(a[:31], device="AMD").numpy())
self.dev.synchronize()
alloc, usb = self.dev.allocator, self.dev.iface.pci_dev.usb
clear = usb.read(0xA808, 1)
# Model a completed 256-chunk copy instead of the one-chunk warmup. The next clear tag must still change.
alloc._usb_seq += 255
usb.write(0xA800, bytes([alloc._usb_seq & 0xff]))
np.testing.assert_array_equal(a[:31], Tensor(a[:31], device="AMD").numpy())
self.assertNotEqual(clear, usb.read(0xA808, 1))
for bits in (8, 24):
with self.subTest(bits=bits):
alloc._usb_seq = ((alloc._usb_seq >> bits)+2)*(1 << bits)-2
usb.write(0xA800, bytes([alloc._usb_seq & 0xff]))
np.testing.assert_array_equal(a, Tensor(a, device="AMD").numpy())
def testCopyinRingWrap(self):
rng = np.random.default_rng(0)
a = rng.integers(0, 256, 1 << 20, dtype=np.uint8)
np.testing.assert_array_equal(a, Tensor(a, device="AMD").numpy())
ring = self.dev.sdma_queue(0)
# A 16 MiB copyin needs more than 4 KiB of SDMA packets, forcing the submission to wrap.
target = ring.ring.nbytes - 0x1000
padding = target - ring.put_value % ring.ring.nbytes - 16 # four-dword timeline fence
self.assertGreaterEqual(padding, 0)
q = self.dev.hw_copy_queue_t()
q.q(*([0] * (padding // 4)))
q.signal(self.dev.timeline_signal, self.dev.next_timeline()).submit(self.dev)
self.dev.synchronize()
before = ring.put_value // ring.ring.nbytes
a = rng.integers(0, 256, 16 << 20, dtype=np.uint8)
t = Tensor(a, device="AMD").realize()
self.assertGreater(ring.put_value // ring.ring.nbytes, before)
np.testing.assert_array_equal(a, t.numpy())
def testCopyinStaleSentinel(self):
a = np.arange(16, dtype=np.uint8)
np.testing.assert_array_equal(a, Tensor(a, device="AMD").numpy())
chunk = 0x40000 - 4
for case in ("copyout", "reuse"):
with self.subTest(case=case):
if case == "copyout":
# A 512 KiB copyin takes three chunks. Copyout then fills both SRAM windows with the next expected tag.
tag = 0x51000000 | ((self.dev.allocator._usb_seq + 3) & 0xFFFFFF)
a = np.full(0x80000 // 4, tag, dtype=np.uint32)
np.testing.assert_array_equal(a, Tensor(a, device="AMD").numpy())
a = np.arange(31, dtype=np.uint8)
else:
# The first full chunk contains the tag expected by the short third chunk in the same window.
tag = 0x51000000 | ((self.dev.allocator._usb_seq + 2) & 0xFFFFFF)
a = np.arange(2 * chunk + 31, dtype=np.uint8)
a[:chunk].view(np.uint32)[:] = tag
np.testing.assert_array_equal(a, Tensor(a, device="AMD").numpy())
if __name__ == "__main__":
unittest.main()
+18 -30
View File
@@ -1,6 +1,10 @@
import unittest
from tinygrad import Tensor, TinyJit, Device
from tinygrad.helpers import Context, DEBUG, GlobalCounters
from dataclasses import replace
from itertools import islice
from tinygrad import Tensor, Device
from tinygrad.codegen import to_program
from tinygrad.engine.realize import time_call
from tinygrad.helpers import Context, DEBUG
from tinygrad.nn import Conv2d
from tinygrad.nn.state import get_parameters
@@ -10,6 +14,13 @@ class TestKernelSpeed(unittest.TestCase):
# TODO: randn is 20% faster than rand for gemv
return Tensor.randn(shape, dtype="half").realize()
def _time_kernel(self, out:Tensor, beam:int):
linear = out.schedule_linear()
self.assertEqual(len(linear.src), 1, "expected a single kernel")
call = linear.src[0]
prg = to_program(call.src[0].replace(arg=replace(call.src[0].arg, beam=beam)), Device[out.device].renderer)
return min(islice(time_call(call.replace(src=(prg, *call.src[1:])), clear_l2=True), 3, 10))
def _compare(self, tm, tflops, gbs, nv_tflops=None, nv_gbs=None, amd_tflops=None, amd_gbs=None):
if DEBUG >= 1:
print(f"{tm=:.6f}")
@@ -34,53 +45,30 @@ class TestKernelSpeed(unittest.TestCase):
def _test_matmul(self, M, K=None, N=None, nv_tflops=None, nv_gbs=None, amd_tflops=None, amd_gbs=None):
# (MxK) @ (KxN)
@TinyJit
def f(a, b) -> Tensor: return (a @ b).realize()
if N is None: N = M
if K is None: K = M
tms = []
with Context(BEAM=3):
for i in range(10):
a = self._get_tensor(M, K)
b = self._get_tensor(K, N)
if i >= 3:
GlobalCounters.time_sum_s = 0
with Context(DEBUG=max(DEBUG.value, 2)): c = f(a, b)
tms.append(GlobalCounters.time_sum_s)
else:
c = f(a, b)
a = self._get_tensor(M, K)
b = self._get_tensor(K, N)
tm = self._time_kernel(c:=a @ b, beam=3)
ops = 2 * M * N * K
mems = a.dtype.itemsize * M * K + b.dtype.itemsize * K * N + c.dtype.itemsize * M * N
tm = min(tms)
tflops = ops / tm / 1e12
gbs = mems / tm / 1e9
self._compare(tm, tflops, gbs, nv_tflops, nv_gbs, amd_tflops, amd_gbs)
def _test_conv_3x3(self, BS, CIN, COUT, H, W, nv_tflops=None, nv_gbs=None, amd_tflops=None, amd_gbs=None):
@TinyJit
def f(conv, x) -> Tensor: return conv(x).realize()
tms = []
K = 3
with Context(BEAM=0, DEBUG=0):
conv = Conv2d(CIN, COUT, K, padding=1)
Tensor.realize(*get_parameters(conv))
with Context(BEAM=2):
for i in range(10):
x = self._get_tensor(BS, CIN, H, W)
if i >= 3:
GlobalCounters.time_sum_s = 0
with Context(DEBUG=max(DEBUG.value, 2)): _c = f(conv, x)
tms.append(GlobalCounters.time_sum_s)
else:
_c = f(conv, x)
x = self._get_tensor(BS, CIN, H, W)
tm = self._time_kernel(_c:=conv(x), beam=2)
# naive algo
ops = 2 * BS * CIN * COUT * K * K * H * W
mems = x.nbytes() + conv.weight.nbytes() + conv.bias.nbytes() + _c.nbytes()
tm = min(tms)
tflops = ops / tm / 1e12
gbs = mems / tm / 1e9
self._compare(tm, tflops, gbs, nv_tflops, nv_gbs, amd_tflops, amd_gbs)
+4
View File
@@ -65,6 +65,10 @@ def assert_kernel_count(expected:int):
got = GlobalCounters.kernel_count
if got != expected: raise KernelCountException(expected, got)
def is_hcq2_device() -> bool: # an hcq2 device stages every copy from the host through a pinned buffer: such a copy is two calls, not one
from tinygrad.runtime.support.hcq2 import HCQ_DEVS
return Device.DEFAULT.split(":")[0] in HCQ_DEVS - {"CPU"}
def call_is_graph(call:UOp) -> bool:
ast = call.src[0]
return ast.op is Ops.CUSTOM_FUNCTION and ast.arg == "graph"
+12 -2
View File
@@ -69,7 +69,7 @@ from tinygrad.runtime.autogen.amd.cdna import ins as irc
from tinygrad.renderer.amd.dsl import VCC_LO, EXEC_LO, SCC, ttmp, Inst
from tinygrad.runtime.autogen.amd.common import Fmt, OpType
from test.amd.helpers import decode_dpp16
from test.mockgpu.amd.pcode import parse_pcode, _FUNCS, _set_bits, _to_bool, _to_u32, _val_to_bits, _ftz_f32
from test.mockgpu.amd.pcode import parse_pcode, _FUNCS, _set_bits, _to_bool, _to_u32, _val_to_bits, _ftz_f32, _bitreverse, _countbits
MASK32 = 0xFFFFFFFF
@@ -1566,9 +1566,19 @@ def _compile_mem_op(inst: ir3.DS|ir3.FLAT|ir3.GLOBAL|ir3.SCRATCH|ir4.DS|ir4.VFLA
has_data1 = is_lds and hasattr(inst, 'data1') and inst.data1 is not None
data1_reg = ctx.inst_field(type(inst).data1) if is_lds else _c(0) # type: ignore[union-attr]
if is_lds and op_name == 'DS_SWIZZLE_B32':
# The manual's reverse_bits operates on five-bit lane indices; thread indices wrap within the wave.
funcs = {'reverse_bits': lambda x: _bitreverse(x, 32) >> _c(27), 'count_ones': _countbits,
'thread_in': lambda x: ctx.rvgpr_dyn(addr_reg, x & _c(ctx.wave_size - 1)),
'thread_valid': lambda x: _lane_active(exec_mask, x & _c(ctx.wave_size - 1))}
result, _ = parse_pcode(pcode, {'offset0': offset0.cast(dtypes.uint8), 'offset1': offset1.cast(dtypes.uint8)}, funcs)
values = [result[f'thread_out@{i}'] for i in range(ctx.wave_size)]
# Snapshot every source before writing: destination and source registers may be identical.
reads = UOp(Ops.STACK, src=tuple(values))
return UOp.sink(*(ctx.wvgpr_dyn(vdst_reg, _c(i), val, exec_mask, after=reads) for i, val in enumerate(values)), *ctx.inc_pc())
# DS_PERMUTE/DS_BPERMUTE: cross-lane VGPR access via pcode
if is_lds and 'PERMUTE' in op_name:
pcode = get_pcode(inst.op)
srcs = {'ADDR': addr_reg, 'DATA0': vdata_reg, 'VDST': vdst_reg, 'OFFSET': offset,
'EXEC': exec_mask.cast(dtypes.uint64), '_vgpr': ctx.vgpr, '_wave_size': ctx.wave_size}
_, assigns = parse_pcode(pcode, srcs)
+45 -25
View File
@@ -630,6 +630,10 @@ class Parser:
self.eat('DOT')
dt_name = self.eat('IDENT').val
return self._handle_mem_load(addr, DTYPES.get(dt_name, dtypes.uint32))
if name in self.funcs and self.try_eat('LBRACKET'):
index = self.parse()
self.eat('RBRACKET')
return self.funcs[name](index)
if name == 'VGPR' and self.at('LBRACKET'):
self.eat('LBRACKET')
lane = self.parse()
@@ -1006,20 +1010,24 @@ def parse_block(lines: list[str], start: int, env: dict[str, VarVal], funcs: dic
# for loop
if first == 'for':
# Parse: for VAR in [SIZE']START : [SIZE']END do
p = Parser(toks, env, funcs)
p.eat_val('for', 'IDENT')
loop_var = p.eat('IDENT').val
p.eat_val('in', 'IDENT')
def parse_bound():
if p.at('NUM') and p.peek(1).type == 'QUOTE':
p.eat('NUM')
p.eat('QUOTE')
if p.at('NUM'): return int(p.eat('NUM').val.rstrip('UuLl'))
return int(p.parse())
start_val = parse_bound()
p.eat('COLON')
end_val = parse_bound()
# C-style loops use an exclusive bound; for/in loops use an inclusive bound.
if m := re.fullmatch(r'for\s*\(\s*(\w+)\s*=\s*(\d+);\s*\1\s*<\s*(\d+);\s*\1\s*(\+\+|\+=\s*\d+)\s*\)', line):
loop_var, start_val, end_val = m[1], int(m[2]), int(m[3]) - 1
step = 1 if m[4] == '++' else int(m[4][2:])
else:
p = Parser(toks, env, funcs)
p.eat_val('for', 'IDENT')
loop_var = p.eat('IDENT').val
p.eat_val('in', 'IDENT')
def parse_bound():
if p.at('NUM') and p.peek(1).type == 'QUOTE':
p.eat('NUM')
p.eat('QUOTE')
if p.at('NUM'): return int(p.eat('NUM').val.rstrip('UuLl'))
return int(p.parse())
start_val = parse_bound()
p.eat('COLON')
end_val, step = parse_bound(), 1
# Collect body
i += 1
body_lines: list[str] = []
@@ -1035,7 +1043,7 @@ def parse_block(lines: list[str], start: int, env: dict[str, VarVal], funcs: dic
has_break = any('break' in bl.lower() for bl in body_lines)
found_var = f'_found_{next(_break_var_ids)}' if has_break else None
if found_var: env[found_var] = block_assigns[found_var] = _const(dtypes.bool, False)
for loop_i in range(start_val, end_val + 1):
for loop_i in range(start_val, end_val + 1, step):
subst_lines = [_subst_loop_var(bl, loop_var, loop_i) for bl in body_lines if not (has_break and bl.strip().lower() == 'break')]
_, iter_assigns, _ = parse_block(subst_lines, 0, {**env, **block_assigns}, funcs, assigns)
if has_break:
@@ -1224,9 +1232,9 @@ def parse_block(lines: list[str], start: int, env: dict[str, VarVal], funcs: dic
var = toks[0].val
j, idx_toks = _match_bracket(toks, 1)
if j < len(toks) and toks[j].type == 'EQUALS':
idx_expr = parse_tokens(idx_toks, env, funcs)
# Static index: var[NUM] = value
if len(idx_toks) == 1 and idx_toks[0].type == 'NUM':
idx = int(idx_toks[0].val.rstrip('UuLl'))
if isinstance(idx := _single_value(idx_expr), int):
val = parse_tokens(toks[j+1:], env, funcs)
existing = block_assigns.get(var, env.get(var))
if existing is not None and isinstance(existing, UOp):
@@ -1238,7 +1246,6 @@ def parse_block(lines: list[str], start: int, env: dict[str, VarVal], funcs: dic
# Dynamic index: var[expr] = value where var has @-elements
elems = [(k.split('@')[1], v) for k, v in {**env, **block_assigns}.items() if k.startswith(f'{var}@') and isinstance(v, UOp)]
if elems:
idx_expr = parse_tokens(idx_toks, env, funcs)
val = parse_tokens(toks[j+1:], env, funcs)
for elem_idx_str, old_elem in elems:
elem_idx = int(elem_idx_str)
@@ -1407,16 +1414,29 @@ def parse_block(lines: list[str], start: int, env: dict[str, VarVal], funcs: dic
def parse_expr(expr: str, env: dict[str, VarVal], funcs: dict | None = None) -> UOp:
return parse_tokens(tokenize(expr.strip().rstrip(';')), env, funcs)
def parse_pcode(pcode: str, srcs: dict[str, UOp | int] | None = None) -> tuple[dict, list]:
def parse_pcode(pcode: str, srcs: dict[str, UOp | int] | None = None, funcs: dict | None = None) -> tuple[dict, list]:
env: dict = srcs.copy() if srcs else {}
assigns: list[tuple[str, UOp]] = []
raw_lines = [l.strip().rstrip(';') for l in pcode.split('\n') if l.strip() and not l.strip().startswith('//')]
# TODO: pcode.py should tokenize full pcode string instead of line-by-line, then this hack can be removed
lines: list[str] = []
for l in raw_lines:
if lines and re.search(r'(&&|\|\||[&|+\-*/^])\s*$', lines[-1]): lines[-1] = lines[-1] + ' ' + l
else: lines.append(l)
_, final, _ = parse_block(lines, 0, env, assigns=assigns)
blocks: list[str] = []
for raw in pcode.splitlines():
line = raw.split('//')[0].strip().rstrip(';')
if not line: continue
# Both block syntaxes share the same parser; braces supply the implicit end markers.
if line.startswith('}') and blocks:
end = blocks.pop()
line = line[1:].strip()
if not line.startswith(('elsif', 'else')): lines.append(end)
if m := re.match(r'(if|elsif|else|for)\b.*\{$', line):
blocks.append('endfor' if m[1] == 'for' else 'endif')
line = line[:-1].rstrip()
if m[1] in ('if', 'elsif'): line += ' then'
if not line: continue
line = re.sub(r'=\s*(\w+):(\w+)$', r'= {\1, \2}', line)
if lines and re.search(r'(&&|\|\||[&|+\-*/^])\s*$', lines[-1]): lines[-1] += ' ' + line
else: lines.append(line)
assert not blocks, "unclosed pcode block"
_, final, _ = parse_block(lines, 0, env, {**_FUNCS, **funcs} if funcs else None, assigns=assigns)
sliced = set(d.split('[')[0] for d, _ in assigns if '[' in d)
for var, val in final.items():
if var in ['D0', 'S0', 'SCC', 'VCC', 'EXEC', 'PC', 'RETURN_DATA', 'VDATA'] and isinstance(val, UOp):
+6 -1
View File
@@ -53,6 +53,7 @@ class NVDriver(VirtDriver):
VirtFile('/dev/nvidia-uvm', functools.partial(NVUVMFileDesc, driver=self))]
self.root_handle = None
self.host_ranges: set[int] = set()
self.gpus = {}
self.next_fd = (1 << 29)
@@ -251,7 +252,9 @@ class NVDriver(VirtDriver):
elif nr == nv_gpu.UVM_ENABLE_PEER_ACCESS: pass # uvm and shared spaced are setup already, no emulation for now
elif nr == nv_gpu.UVM_CREATE_EXTERNAL_RANGE:
st = nv_gpu.UVM_CREATE_EXTERNAL_RANGE_PARAMS.from_address(argp)
libc.mmap(st.base, st.length, mmap.PROT_READ|mmap.PROT_WRITE, libc.MAP_FIXED|mmap.MAP_SHARED|mmap.MAP_ANONYMOUS, -1, 0)
# Registered host memory already has a CPU mapping; MAP_FIXED would discard its contents.
if st.base not in self.host_ranges:
libc.mmap(st.base, st.length, mmap.PROT_READ|mmap.PROT_WRITE, libc.MAP_FIXED|mmap.MAP_SHARED|mmap.MAP_ANONYMOUS, -1, 0)
elif nr == nv_gpu.UVM_MAP_EXTERNAL_ALLOCATION:
st = nv_gpu.UVM_MAP_EXTERNAL_ALLOCATION_PARAMS.from_address(argp)
for gpu_attr_id in range(st.gpuAttributesCount):
@@ -265,6 +268,7 @@ class NVDriver(VirtDriver):
elif nr == nv_gpu.UVM_REGISTER_CHANNEL: pass
elif nr == nv_gpu.UVM_FREE:
st = nv_gpu.UVM_FREE_PARAMS.from_address(argp)
self.host_ranges.discard(st.base)
libc.munmap(st.base, st.length)
else: raise RuntimeError(f"Unknown {nr} to nvidia-uvm")
return 0
@@ -276,6 +280,7 @@ class NVDriver(VirtDriver):
st:Any = nv_gpu.nv_ioctl_nvos02_parameters_with_fd.from_address(argp)
# Track host memory (signal memory) - progress queues when written to
if st.params.hClass == nv_gpu.NV01_MEMORY_SYSTEM_OS_DESCRIPTOR:
self.host_ranges.add(st.params.pMemory)
self.track_address(st.params.pMemory, st.params.pMemory + st.params.limit + 1,
lambda mv,off: None, lambda mv, off: self._gpu_mmio_write(mv, off, None))
return 0
+6 -7
View File
@@ -100,11 +100,11 @@ class GPFIFO:
if qmd.release0_enable:
rel0 = to_mv(qmd.release0_address_lower + (qmd.release0_address_upper << 32), 0x10).cast('Q')
rel0[0] = qmd.release0_payload_lower + (qmd.release0_payload_upper << 32)
rel0[1] = int(time.perf_counter() * 1e9)
if qmd.release0_structure_size == 0: rel0[1] = int(time.perf_counter() * 1e9) # four words: the timestamp after the payload
if qmd.release1_enable:
rel1 = to_mv(qmd.release1_address_lower + (qmd.release1_address_upper << 32), 0x10).cast('Q')
rel1[0] = qmd.release1_payload_lower + (qmd.release1_payload_upper << 32)
rel1[1] = int(time.perf_counter() * 1e9)
if qmd.release1_structure_size == 0: rel1[1] = int(time.perf_counter() * 1e9)
if qmd.dependent_qmd0_enable:
if qmd.dependent_qmd0_action == 1: self.execute_qmd(qmd.dependent_qmd0_pointer << 8)
else: raise RuntimeError("unsupported dependent qmd action")
@@ -192,11 +192,10 @@ class GPFIFO:
sz = self._state(nv_gpu.NVC6B5_LINE_LENGTH_IN)
assert flags == 0x182, f"unsupported flags in _exec_nvc6b5_dma: {flags}"
ctypes.memmove(dst, src, sz)
elif ((flags >> 3) & 0b11) != 0:
src = to_mv(self._state64(nv_gpu.NVC6B5_SET_SEMAPHORE_A), 0x10).cast('Q')
val = self._state(nv_gpu.NVC6B5_SET_SEMAPHORE_PAYLOAD)
src[0] = val
src[1] = int(time.perf_counter() * 1e9)
elif (semaphore_type:=((flags >> 3) & 0b11)) != 0:
to_mv(addr:=self._state64(nv_gpu.NVC6B5_SET_SEMAPHORE_A), 4).cast('I')[0] = self._state(nv_gpu.NVC6B5_SET_SEMAPHORE_PAYLOAD)
if semaphore_type == nv_gpu.NVC6B5_LAUNCH_DMA_SEMAPHORE_TYPE_RELEASE_FOUR_WORD_SEMAPHORE:
to_mv(addr + 8, 8).cast('Q')[0] = int(time.perf_counter() * 1e9)
else: raise RuntimeError("unknown nvc6b5_dma flags")
def _exec_pcas2(self):
+2 -2
View File
@@ -246,8 +246,8 @@ class TestTensorUOpRand(unittest.TestCase):
self.assertIs(Tensor._threefry_random_bits(Tensor(key), Tensor(c0), Tensor(c1)).uop, UOp._threefry_random_bits(key, c0, c1))
def test_rand(self):
k, c = UOp.empty((2,), dtype=dtypes.uint32), UOp.zeros(2, dtype=dtypes.uint32)
self.assertIs(Tensor._rand(Tensor(k), Tensor(c), (2, 2), dtypes.float32).uop, UOp._rand(k, c, (2, 2), dtypes.float32))
self.assertIs(Tensor._rand(Tensor(k), Tensor(c), (0, 3), dtypes.float32).uop, UOp._rand(k, c, (0, 3), dtypes.float32))
self.assertIs(Tensor._rand(Tensor(k), Tensor(c), (2, 2), dtypes.float32, clone=False).uop, UOp._rand(k, c, (2, 2), dtypes.float32, clone=False))
self.assertIs(Tensor._rand(Tensor(k), Tensor(c), (0, 3), dtypes.float32, clone=False).uop, UOp._rand(k, c, (0, 3), dtypes.float32, clone=False))
class TestTensorUOpGather(unittest.TestCase):
def _check(self, t, dim, idx):
+12
View File
@@ -406,6 +406,18 @@ class TestUOpGraph(unittest.TestCase):
a = c.after(e)
self.assertNotIn(r, a.ranges)
def test_external_call_preserves_ranges(self):
r = UOp.range(4, 0, dtype=dtypes.int)
fn = UOp.custom_function("external", UOp.const(0, dtypes.uint64))
call = fn.call(r + 1, ret_dtype=dtypes.int)
self.assertEqual(set(call.ranges), {r})
def test_conditional_end_preserves_outer_range(self):
outer, inner = UOp.range(4, 0), UOp.loop(1)
end = UOp.const(1).end(inner, outer < 2)
self.assertEqual(set(end.ranges), {outer})
self.assertEqual(set((outer + 1).after(end).ranges), {outer})
class TestReduceCollapse(unittest.TestCase):
def test_multi_range_reduce_add(self):
"""Test that (x + y).reduce(r1, r2) distributes over multiple ranges"""
+25
View File
@@ -123,6 +123,31 @@ class TestValidateOOB(unittest.TestCase):
i = (r.cast(dtypes.float) * 0.68).trunc().cast(dtypes.int)
to_uops_list([buf.index(i.valid((i >= 0) & (i < 16))).load()])
def test_float_cast_in_mask(self):
with Context(CHECK_OOB=1, SPEC=2):
buf = UOp.param(0, dtypes.int, 1)
r = UOp.range(20, 0)
unknown = r.cast(dtypes.float).cast(dtypes.bool) # a bool from a float is unconstrained
to_uops_list([buf.index(r.valid((r < 1) & unknown)).load()])
with self.assertRaises(RuntimeError):
to_uops_list([buf.index(r.valid(unknown)).load()])
def test_bitcast_in_index(self):
with Context(CHECK_OOB=1, SPEC=2):
buf = UOp.param(0, dtypes.int, 16)
r = UOp.range(16, 0)
# the WEBGPU shift: int -> uint, shift, back to int
i = (r.cast(dtypes.int).bitcast(dtypes.uint) << UOp.const(1).cast(dtypes.uint)).bitcast(dtypes.int)
to_uops_list([buf.index(i.valid(i < 16)).load()])
with self.assertRaises(RuntimeError):
to_uops_list([buf.index(i).load()]) # 0..30 oob
# a negative char reads as a large uchar
c = Variable("c", -128, -113).cast(dtypes.char)
to_uops_list([UOp.param(1, dtypes.int, 144).index(c.bitcast(dtypes.uchar).cast(dtypes.int)).load()]) # 128..143 valid
# the bits of a float are any int
with self.assertRaises(RuntimeError):
to_uops_list([buf.index(r.cast(dtypes.float).bitcast(dtypes.int)).load()])
def test_bool_cast_in_mask(self):
with Context(CHECK_OOB=1, SPEC=2):
buf = UOp.param(0, dtypes.int, 1)
+4 -3
View File
@@ -1073,10 +1073,11 @@ class TestCLI(unittest.TestCase):
out = run_cli(*files, "-s", "NULL")
aggregate = run_cli(*files, "-s", "NULL", "-t")
self.assertEqual(len(out), 3*2)
# flops increases as N gets larger
# Operation count increases with N; FLOPS is a rate and also depends on the measured duration.
gflops = [row["fmt"]["FLOPS"] for row in out]
self.assertGreater(gflops[4], gflops[2])
self.assertGreater(gflops[5], gflops[3])
flops = [rate * row["dur_ms"] * 1e-3 for rate, row in zip(gflops, out)]
self.assertGreater(flops[4], flops[2])
self.assertGreater(flops[5], flops[3])
# aggregate flops
self.assertEqual(len(aggregate), 2)
agg_gflops = [row["fmt"]["FLOPS"] for row in aggregate]
+1 -6
View File
@@ -164,16 +164,11 @@ class TestCallSchedule(unittest.TestCase):
np.testing.assert_equal(big.numpy(), ref)
def test_precompile_full_buffer_assign(self):
from test.helpers import assert_kernel_count
from tinygrad.helpers import GlobalCounters
@function(precompile=True)
def f(x:Tensor) -> Tensor: return x * 2 + 1
a = Tensor.arange(8).float().realize()
cache = Tensor.zeros(8).realize()
GlobalCounters.reset()
# the call writes the whole assign target directly: it binds into the output position, no copy kernel
cache.assign(f(a)).realize()
assert_kernel_count(1)
np.testing.assert_equal(cache.numpy(), np.arange(8)*2+1)
def test_reshape_precompile(self):
@@ -375,7 +370,7 @@ class TestArgOrder(unittest.TestCase):
x = Tensor.arange(3, dtype=dtypes.int).realize()
call = self.make_intersperse_call(x, precompile=True)[0].src[1]
# the transform must preserve the RETURNED's src position: its placeholder is at src 1, the input stays at src 2
from tinygrad.schedule.prepare import transform_precompiled_call
from tinygrad.tensor import transform_precompiled_call
new = transform_precompiled_call(call)
new_call = new.src[0].src[1].src[1]
# the out buffer takes the RETURNED's position (src 1), the input value keeps its position (src 2)
+15
View File
@@ -107,5 +107,20 @@ class TestCallify(unittest.TestCase):
self.assertListEqual(c.tolist(), [5.0, 7.0, 9.0])
self.assertListEqual(d.tolist(), [4.0, 10.0, 18.0])
def test_intermediate_clone_persists(self):
x = (Tensor([1, 2, 3]).realize() + 1).clone()
y = (x * 2).realize()
self.assertTrue(x.uop.has_buffer_identity())
self.assertEqual(x.tolist(), [2, 3, 4])
self.assertEqual(y.tolist(), [4, 6, 8])
def test_zero_size_cat_with_rng(self):
# Empty outputs must not replay a pending RNG counter update.
a = Tensor.rand(2, 2)
b = Tensor.rand(2, 0)
t = a.cat(b, dim=1).realize()
self.assertEqual(t.shape, (2, 2))
self.assertListEqual(t.tolist(), a.tolist())
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -439,7 +439,7 @@ def do_linearize(ctx:Renderer, prg:UOp, sink:UOp) -> UOp:
lst = line_rewrite(linearize(sink), pm_linearize_cleanups)
# isa renderers need to allocate registers
if isinstance(ctx, ISARenderer):
if ctx.pre_regalloc_matcher is not None: lst = line_rewrite(lst, ctx.pre_regalloc_matcher, PreRegAllocContext())
lst = line_rewrite(lst, ctx.pre_regalloc_matcher, PreRegAllocContext())
# register definitions (INS without srcs) move to the top so regalloc sees their live ranges span the whole program (callee saved regs)
lst = sorted(lst, key=lambda u: u.op is not Ops.INS or bool(u.src))
regalloc_ctx = LinearScanRegallocContext(lst, ctx)
+6 -6
View File
@@ -19,15 +19,15 @@ class LinearScanRegallocContext:
# compute live ranges
self.live_range: dict[Register, list[int]] = {}
lr = self.live_range
ranges: list[Register] = []
for i,u in enumerate(reversed(uops)):
loops: dict[int, int] = {} # the interval of each loop, from its RANGE to the last uop that reads that RANGE
for idx,u in reversed(list(enumerate(uops))):
if u.op in PSEUDO_OPS: continue
defs = u.tag if isinstance(u.tag, tuple) else ()
for v in defs + tuple(greg(s) for s in dedup(u.src)):
if isinstance(v, Register): lr.setdefault(v, []).insert(0, len(uops) - 1 - i)
if isinstance(v, Register): lr.setdefault(v, []).insert(0, idx)
for v in defs:
if v in lr and (n:=max((lr[rng][-1] for rng in ranges if lr[rng][0] <= lr[v][-1] < lr[rng][-1]), default=None)): lr[v].append(n)
if u.op is Ops.RANGE: ranges.append(greg(u))
if v in lr and (n:=max((e for s,e in loops.items() if s <= lr[v][-1] < e), default=None)): lr[v].append(n)
if u.op is Ops.RANGE: loops[idx] = max(j for j,x in enumerate(uops) if u in x.src)
# allocate registers
self.stack_size: int = 0
@@ -90,7 +90,7 @@ class LinearScanRegallocContext:
# loop prologue, avoid loading inside the loop
if u.op is Ops.RANGE:
# we move to registers vars used in the loop sorted by next use, vars not used in the loop will not be reloaded in the epilogue
used_in_loop = [v for v in live.keys() | self.spills.keys() if any(i <= l < lr[greg(u)][-1] for l in lr[v])]
used_in_loop = [v for v in live.keys() | self.spills.keys() if any(i <= l < loops[i] for l in lr[v])]
sorted_uses = sorted(used_in_loop, key=lambda k: (next(l-i for l in lr[k] if l >= i), lr[k][0], k.name, k.index))
live_in: dict[Register, Register] = {}
for v in sorted_uses:
+11 -6
View File
@@ -187,7 +187,7 @@ class Buffer:
# zero copy with as_memoryview (disabled by default due to use after free)
if (force_zero_copy or allow_zero_copy) and hasattr(self.allocator, '_as_buffer'):
if not no_sync: self.allocator.dev.synchronize()
return self.allocator._as_buffer(self._buf)
if (mv:=self.allocator._as_buffer(self._buf)) is not None: return mv
assert not force_zero_copy, "force zero copy was passed, but copy is required"
Buffer("PYTHON", self.size, self.dtype, opaque=(mv:=memoryview(bytearray(self.nbytes)))).copy_from(self)
return mv
@@ -280,9 +280,13 @@ class DepsTracker:
if i in write:
for dmap in [self.w_dependency_map, self.r_dependency_map]:
kept = []
for st,en,dep in dmap[key]:
if st < min(s, en): kept.append((st, min(s, en), dep))
if max(e, st) < en: kept.append((max(e, st), en, dep))
for entry in dmap[key]:
st, en, dep = entry
if st == en: continue
if en <= s or e <= st: kept.append(entry)
else:
if st < s: kept.append((st, s, dep))
if e < en: kept.append((e, en, dep))
dmap[key] = kept
self.w_dependency_map[key].append((s, e, new_dependency))
else: self.r_dependency_map[key].append((s, e, new_dependency))
@@ -337,8 +341,9 @@ class Compiled:
has_copy_queue:bool = True
pm_encode:Any = None # per queue kind: queue ops -> flat command words
pm_lower:Any = None # per queue kind: custom_function(submit, cmdbuf) -> the queue push
pm_batch:Any = None
pm_encode:Any = None
pm_lower:Any = None
pm_bufferize:Any = None
def __init__(self, device:str, allocator:Allocator, renderers:list[type[Renderer]], runtime:type[Program[Self]]|None, graph=None, arch=None):
+1 -1
View File
@@ -166,7 +166,7 @@ class CapturedJit(Generic[ReturnType]):
expected_input_info: list[tuple[UOp, tuple[Variable, ...], DType, str]] # (view, variables, dtype, device) per input
@functools.cached_property
def linear(self) -> UOp: return link_linear(self._linear)
def linear(self) -> UOp: return link_linear(self._linear, allow_cache=False) # do not cache jit
def __reduce__(self): return self.__class__, (self.ret, self._linear, self.expected_names, self.expected_input_info)
+5 -4
View File
@@ -130,7 +130,7 @@ class ExecContext:
cache: bool = True
def _resolve(b:UOp, inputs:tuple[UOp, ...]) -> UOp:
if b.op in (Ops.MSELECT, Ops.SHRINK): return b.replace(src=(_resolve(b.src[0], inputs), *b.src[1:]))
if b.op in (Ops.MSELECT, Ops.SHRINK, Ops.BITCAST): return b.replace(src=(_resolve(b.src[0], inputs), *b.src[1:]))
if b.op is Ops.MSTACK: return b.replace(src=tuple(_resolve(x, inputs) for x in b.src))
return inputs[b.arg.slot] if b.op is Ops.PARAM else b
def resolve_params(call:UOp, inputs:tuple[UOp, ...]) -> list[UOp]: return [_resolve(b, inputs) for b in get_call_arg_uops(call)]
@@ -283,17 +283,18 @@ def compile_linear(linear:UOp, beam:int|None=None, validate=False, input_uops:li
linear = hcq_compile(linear, input_uops, bool(PROFILE or DEBUG >= 2) if profile is None else profile)
return linear
def link_linear(linear:UOp, cache=True) -> UOp: return hcq_link(linear, cache=cache)
def link_linear(linear:UOp, input_uops:list[UOp]|None=None, allow_cache=True) -> UOp:
return hcq_link(linear, input_uops=input_uops, allow_cache=allow_cache)
def run_linear(linear:UOp, var_vals:dict[str, int]|None=None, input_uops:Sequence[UOp]=(), update_stats=True, jit=False, wait=False):
inputs = list(input_uops)
if not jit: linear = link_linear(compile_linear(linear, validate=VALIDATE_WITH_CPU, input_uops=inputs), cache=False) # a one-shot link
if not jit: linear = link_linear(compile_linear(linear, validate=VALIDATE_WITH_CPU, input_uops=inputs), input_uops=inputs)
ctx = ExecContext(var_vals or {}, tuple(inputs), update_stats, jit, wait or DEBUG>=2)
for call in linear.src: track_stats(ctx, call.without_after, perf_counter_us(), pm_exec.rewrite(call.without_after, ctx))
def time_call(call:UOp, var_vals:dict[str, int]|None=None, timeout:int|None=None, clear_l2:bool=False) -> Iterator[float]:
ctx = ExecContext(var_vals or {}, update_stats=False, wait=True, timeout=timeout, cache=False)
linear = link_linear(compile_linear(UOp(Ops.LINEAR, src=(call,)), beam=0, profile=True), cache=ctx.cache)
linear = link_linear(compile_linear(UOp(Ops.LINEAR, src=(call,)), beam=0, profile=True), allow_cache=ctx.cache)
while True:
if clear_l2:
if hasattr(dev:=Device[call.src[1].device], 'invalidate_caches'): dev.invalidate_caches()
+1 -1
View File
@@ -395,7 +395,7 @@ if getenv("DEBUG_GC"):
cache_dir: str = os.path.join(getenv("XDG_CACHE_HOME", os.path.expanduser("~/Library/Caches" if OSX else "~/.cache")), "tinygrad")
CACHEDB: str = getenv("CACHEDB", os.path.abspath(os.path.join(cache_dir, "cache.db")))
VERSION = 22
VERSION = 23
_db_connection = threading.local()
def db_connection():
if (conn:=getattr(_db_connection, "conn", None)) is None:
+5 -3
View File
@@ -6,6 +6,7 @@ from tinygrad.device import Buffer
from tinygrad.dtype import AddrSpace, dtypes
from tinygrad.helpers import prod
from tinygrad.uop.ops import AxisType, KernelInfo, Ops, resolve
from tinygrad.renderer.cstyle import HIPRenderer
BLOCK_M, BLOCK_N, WARP_SIZE = 32, 32, 32
WMMA_M, WMMA_N, WMMA_K = 16, 16, 16
@@ -31,7 +32,7 @@ def amd_custom_kernels_supported(device:str|tuple[str, ...]|None) -> bool:
if device is None or device.split(":")[0] != "AMD": return False
# @function contexts set ALLOW_DEVICE_USAGE=0 (scheduling must not open devices); the device is always open here
with Context(ALLOW_DEVICE_USAGE=1):
return (t:=getattr(Device[device], "target", None)) is not None and t[0] == 11
return (t:=getattr(Device[device], "target", None)) is not None and t[0] == 11 and isinstance(Device[device].renderer, HIPRenderer)
def warp_reduce(val:UOp, maximum:bool=False, full_wave:bool=False) -> UOp:
for offset in ((16, 8, 4, 2, 1) if full_wave else (8, 4, 2, 1)):
@@ -74,7 +75,7 @@ class Linear(nn.Linear):
nbytes, nblocks = raw.max_numel(), raw.max_numel() // Q6_BYTES
byte_view = Tensor(UOp.from_buffer(cast(Buffer, raw.buf_uop.buffer).view(nbytes, dtypes.uint8, raw_offset)))
padded = byte_view.reshape((nblocks, Q6_BYTES)).pad_to((nblocks, Q6_PADDED)).bitcast(dtypes.uint32)
self.weight = padded.contiguous().reshape(nblocks * Q6_WORDS)
self.weight = padded.clone().reshape(nblocks * Q6_WORDS)
else:
self.weight = Tensor(UOp.from_buffer(cast(Buffer, raw.buf_uop.buffer)
.view(raw.max_numel() * raw.dtype.itemsize // dtypes.uint32.itemsize, dtypes.uint32, raw_offset)))
@@ -540,7 +541,8 @@ def _amd_flash_attention(o:UOp, q:UOp, cache:UOp, valid_kv_len:int|UOp, q_start:
S_frag = S_reg.reshape(TM // WMMA_ACC, WMMA_ACC, TN).permute(0, 2, 1)[tm1, tn1]
q_frag = Q_lds.reshape(WAVES_M, TM // WMMA_ACC, WMMA_M, D // WMMA_K, WMMA_K)[wave_m, tm1, lane_n, k_qk]
k_frag = KV_lds_k.reshape(TN, WMMA_N, D // WMMA_K, WMMA_K)[tn1, lane_n, k_qk]
qk_done = S_frag.store(UOp.wmma(q_frag, k_frag, S_frag.after(k_qk), *WMMA_ARG)).end(tm1, tn1).end(k_qk)
# All waves must finish reading Q/K before their shared memory is reused for P/V.
qk_done = S_frag.store(UOp.wmma(q_frag, k_frag, S_frag.after(k_qk), *WMMA_ARG)).end(tm1, tn1).end(k_qk).barrier()
S_reg = S_reg.after(qk_done, S_reg.store(S_reg * SCALE))
rm, rn = UOp.range(TM, 250), UOp.range(TN, 251)
q_idx = q_base + block_m * BLOCK_M + wave_m * WMMA_M + rm * LANES_PER_WAVE_M + lane_m
+38 -1
View File
@@ -1,6 +1,6 @@
from typing import cast
import math, dataclasses
from tinygrad.uop.ops import UOp, PatternMatcher, UPat, Ops, all_metadata, broadcast_axes
from tinygrad.uop.ops import UOp, PatternMatcher, UPat, Ops, GroupOp, all_metadata, broadcast_axes
from tinygrad.helpers import argsort
from tinygrad.dtype import sum_acc_dtype
from tinygrad.function import renumber_invalid_outputs
@@ -64,6 +64,25 @@ def call_gradient(ctx:UOp, k:UOp, needed:set[int]) -> tuple[UOp|None, ...]:
ret_set = set(ret_pos)
return (None,) + tuple(None if i in ret_set else (bwd_outs[gb_map[i]] if i in gb_map else None) for i in range(len(args)))
def view_assign_gradient(ctx:UOp, base:UOp, view:UOp):
source = view
while source is not base and source.op in GroupOp.Movement: source = source.src[0]
if source is not base: return None
# Only the written region flows to the assignment; the rest flows to the previous value.
mask = compute_gradient(view, view.const_like(1), {base})[base]
return (mask.eq(0).where(ctx, 0), view.substitute({base: ctx}, walk=True))
def substitute_values(root:UOp, values:dict[UOp, UOp]) -> UOp:
rewritten:dict[UOp, UOp] = {}
for u in root.toposort(enter_calls=False):
if u in values: rewritten[u] = values[u]
else:
src = tuple(rewritten.get(s, s) for s in u.src)
# STORE destinations and AFTER storage keep their identity; only value inputs are substituted.
if u.op in {Ops.STORE, Ops.AFTER}: src = (u.src[0], *src[1:])
rewritten[u] = u.replace(src=src)
return rewritten[root]
# ctx is grad_output
pm_gradient = PatternMatcher([
(UPat(Ops.CAST, name="ret"), lambda ctx, ret: (ctx.cast(ret.src[0].dtype),)),
@@ -96,6 +115,7 @@ pm_gradient = PatternMatcher([
(UPat(Ops.SINK), lambda ctx: ctx.src),
(UPat(Ops.AFTER, src=(UPat.var("d"), UPat(Ops.CALL, name="k"))), lambda ctx, d, k:
(ctx, UOp.sink(*([ctx if i == k.src.index(d)-1 else UOp(Ops.NOOP) for i in range(len(k.src)-1)])))),
(UPat(Ops.AFTER, src=(UPat(name="base"), UPat(name="view").after(UPat(name="view").store(UPat())))), view_assign_gradient),
# clone/assign gradient passes through to val
(UPat(Ops.AFTER, src=(UPat(), UPat(Ops.STORE))), lambda ctx: (None, ctx)),
(UPat(Ops.STORE, src=(UPat(), UPat())), lambda ctx: (None, ctx)),
@@ -140,4 +160,21 @@ def compute_gradient(root:UOp, root_grad:UOp, targets:set[UOp]) -> dict[UOp, UOp
# we add the backward metadata to everything new in the graph
for bw_uop in v.toposort(lambda x: x not in (t0, *t0.src, grads[t0])):
all_metadata[bw_uop] = all_metadata.get(bw_uop, ())+backward_metadata
# Gradients may outlive an in-place write. Read the previous value's expression, not its overwritten buffer.
overwritten = {u.src[0].base for u in grads if u.op is Ops.STORE}
values:dict[UOp, UOp] = {}
for u in UOp.sink(*overwritten).toposort():
if u.op is not Ops.AFTER or len(u.src) != 2: continue
base, effect = u.src
view = base
if effect.op is Ops.AFTER and len(effect.src) == 2: view, effect = effect.src
if effect.op is not Ops.STORE or effect.src[0] is not view: continue
value = effect.src[1]
if view is not base:
mask = compute_gradient(view, view.const_like(1), {base}).get(base)
if mask is None: continue
value = mask.eq(0).where(base, compute_gradient(view, value, {base})[base])
values[u] = substitute_values(value, values)
if replacements := {u: values[u] for u in overwritten if u in values}:
grads = {u: substitute_values(v, replacements) for u,v in grads.items()}
return grads
+5 -5
View File
@@ -37,17 +37,17 @@ class RandMixin(OpMixin):
return uint_bits.rshift(dtype.bitsize - nmant).bitwise_or(float_one_bits).bitcast(dtype)[:prod(shape)].sub(1).reshape(shape)
@classmethod
def _rand(cls, key:Self, counter:Self, shape:tuple[int, ...], dtype:DType, contiguous:bool=True) -> Self:
def _rand(cls, key:Self, counter:Self, shape:tuple[int, ...], dtype:DType, clone:bool=True) -> Self:
bits = cls.random_bits(key, counter, ceildiv(prod(shape) * dtype.itemsize, 4))
out = cls._bits_to_rand(bits, shape, dtype)
return out.contiguous() if contiguous else out
return out.clone() if clone else out
@staticmethod
def _next_counter(device:str, num:int):
raise NotImplementedError("_next_counter requires the stateful per-device RNG counter, only implemented on Tensor")
@classmethod
def rand(cls, *shape, device:str|None=None, dtype:DTypeLike|None=None, contiguous:bool=True) -> Self:
def rand(cls, *shape, device:str|None=None, dtype:DTypeLike|None=None, clone:bool=True) -> Self:
"""
Creates a tensor with the given shape, filled with random values from a uniform distribution over the interval `[0, 1)`.
@@ -65,7 +65,7 @@ class RandMixin(OpMixin):
if device is not None and not isinstance(device, str): raise ValueError(f"rand only supports single device, got {device=}")
device = cast(str, canonicalize_device(device))
key, counter = cls._next_counter(device, ceildiv(prod(shape) * dt.itemsize, 4))
return cls._rand(key, counter, shape, dt, contiguous=contiguous)
return cls._rand(key, counter, shape, dt, clone=clone)
def rand_like(self, **kwargs) -> Self:
"""
@@ -293,7 +293,7 @@ class RandMixin(OpMixin):
if not 0 <= p <= 1: raise ValueError(f"{p=} is out of range [0, 1]")
if not TRAINING or p == 0: return self
if p == 1: return self.const_like(0)
return (self.rand_like(dtype=dtypes.default_float, contiguous=False) >= p).contiguous().where(self, 0) / (1.0 - p)
return (self.rand_like(dtype=dtypes.default_float, clone=False) >= p).clone().where(self, 0) / (1.0 - p)
def scaled_dot_product_attention(self, key:Self, value:Self, attn_mask:Self|None=None, dropout_p:float=0.0,
is_causal:bool=False, enable_gqa:bool=False) -> Self:
+5 -2
View File
@@ -233,12 +233,15 @@ def extract_pcode(pages: list[list[tuple[float, float, str, str]]], name_to_op:
sorted_lines = sorted(lines, key=lambda x: (x[0], -x[1]))
# Stop at large Y gaps (>30) - indicates section break
filtered = [sorted_lines[0]]
depth = sorted_lines[0][2].count("{") - sorted_lines[0][2].count("}")
for j in range(1, len(sorted_lines)):
prev_page, prev_y, _ = sorted_lines[j-1]
curr_page, curr_y, _ = sorted_lines[j]
if curr_page == prev_page and prev_y - curr_y > 30: break
if curr_page != prev_page and prev_y > 60 and curr_y < 730: break
if depth == 0 and curr_page == prev_page and prev_y - curr_y > 30: break
if depth == 0 and curr_page != prev_page and prev_y > 60 and curr_y < 730: break
filtered.append(sorted_lines[j])
code = sorted_lines[j][2].split("//")[0]
depth += code.count("{") - code.count("}")
pcode_lines = [t.replace('Ê', '').strip() for _, _, t in filtered]
if pcode_lines: pcode[(name, opcode)] = '\n'.join(pcode_lines)
return pcode
+4 -9
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
import itertools
from dataclasses import dataclass, field
from tinygrad.renderer import Renderer
from tinygrad.uop.ops import PatternMatcher, UOp, Ops, consumer_map_from_toposort
from tinygrad.uop.ops import PatternMatcher, UOp, Ops
@dataclass(frozen=True)
class Register:
@@ -16,12 +16,9 @@ class Register:
class IselContext:
def __init__(self, sink:UOp):
self.uses = consumer_map_from_toposort(sink.toposort())
self.reg_n = itertools.count()
def arg_key(u:UOp):
if u.op is Ops.SPECIAL: return (2, u.arg)
return (0, u.arg.slot) if u.arg.addrspace is not None else (1, u.expr)
self.func_args = sorted([u for u in self.uses if u.op in {Ops.PARAM, Ops.SPECIAL}], key=arg_key)
def arg_key(u:UOp): return (1, u.arg) if u.op is Ops.SPECIAL else (0, u.arg.slot)
self.func_args = sorted([u for u in sink.toposort() if u.op in {Ops.PARAM, Ops.SPECIAL}], key=arg_key)
def vreg(self, cons:tuple[Register, ...]|Register):
return Register(f"v{next(self.reg_n)}", 0, _cons=cons if isinstance(cons, tuple) else (cons,))
@@ -34,17 +31,15 @@ def greg(u:UOp):
@dataclass
class PreRegAllocContext:
lock: UOp|None = None
clobbered: set[UOp] = field(default_factory=set)
class ISARenderer(Renderer):
pre_isel_matcher: PatternMatcher
isel_matcher: PatternMatcher
pre_regalloc_matcher: PatternMatcher|None = None
pre_regalloc_matcher: PatternMatcher
post_regalloc_matcher: PatternMatcher
def is_two_address(self, x:UOp) -> bool: return False
def stack_pointer(self) -> UOp: raise NotImplementedError("arch specific")
def copy(self, x:UOp, reg:Register) -> UOp: raise NotImplementedError("arch specific")
def spill(self, disp:UOp, x:UOp) -> UOp: raise NotImplementedError("arch specific")
def fill(self, disp:UOp, x:UOp, reg:Register) -> UOp: raise NotImplementedError("arch specific")
def asm_str(self, uops:list[UOp], function_name:str) -> str: raise NotImplementedError("arch specific")
+71 -198
View File
@@ -23,18 +23,13 @@ class X86Ops(FastEnum):
VMOVSSm = auto(); VMOVSDm = auto(); VMOVUPSm = auto()
# casts
MOVZX = auto(); MOVSX = auto(); MOVSXD = auto()
VPMOVZXBW = auto(); VPMOVZXBD = auto(); VPMOVZXBQ = auto()
VPMOVZXWD = auto(); VPMOVZXWQ = auto(); VPMOVZXDQ = auto()
VPMOVSXBW = auto(); VPMOVSXBD = auto(); VPMOVSXBQ = auto()
VPMOVSXWD = auto(); VPMOVSXWQ = auto(); VPMOVSXDQ = auto()
VCVTDQ2PS = auto(); VCVTDQ2PD = auto(); VCVTTPS2DQ = auto(); VCVTTPD2DQ = auto()
VCVTPH2PS = auto(); VCVTPS2PH = auto(); VCVTPS2PD = auto(); VCVTPD2PS = auto()
VCVTPH2PS = auto(); VCVTPS2PH = auto()
VCVTSS2SD = auto(); VCVTSD2SS = auto(); VCVTSI2SS = auto(); VCVTSI2SD = auto()
VCVTTSS2SI = auto(); VCVTTSD2SI = auto()
# bitcasts
VMOVD = auto(); VMOVQ = auto(); VMOVDm = auto(); VMOVQm = auto()
# comparisons
VCMPSS = auto(); VCMPSD = auto(); VCMPPS = auto(); VCMPPD = auto()
VCMPSS = auto(); VCMPSD = auto()
SETNE = auto(); SETE = auto(); SETL = auto(); SETB = auto()
# where
CMOVNE = auto(); CMOVE = auto(); CMOVL = auto(); CMOVB = auto()
@@ -43,29 +38,17 @@ class X86Ops(FastEnum):
JNE = auto(); JE = auto(); JL = auto(); JB = auto(); JGE = auto(); JMP = auto()
# vectorize / gep
VINSERTPS = auto(); VPSRLDQ = auto()
VPEXTRB = auto(); VPEXTRW = auto(); VPEXTRD = auto(); VPEXTRQ = auto()
VPINSRB = auto(); VPINSRW = auto(); VPINSRD = auto(); VPINSRQ = auto()
VPEXTRW = auto(); VPEXTRD = auto()
VPINSRW = auto(); VPINSRD = auto()
# int binary
IDIV = auto(); DIV = auto()
ADD = auto(); ADDi = auto(); SUB = auto(); SUBi = auto(); IMUL = auto(); IMULi = auto()
AND = auto(); ANDi = auto(); XOR = auto(); XORi = auto(); OR = auto(); ORi = auto()
SHL = auto(); SHLi = auto(); SHR = auto(); SHRi = auto(); SAR = auto(); SARi = auto(); CMP = auto(); CMPi = auto()
# float unary (sometimes not unary)
VROUNDSS = auto(); VROUNDSD = auto(); VROUNDPS = auto(); VROUNDPD = auto()
VSQRTSS = auto(); VSQRTSD = auto(); VSQRTPS = auto(); VSQRTPD = auto()
# float scalar / vector binary
VADDSS = auto(); VADDSD = auto(); VADDPS = auto(); VADDPD = auto()
VSUBSS = auto(); VSUBSD = auto(); VSUBPS = auto(); VSUBPD = auto()
VMULSS = auto(); VMULSD = auto(); VMULPS = auto(); VMULPD = auto()
VDIVSS = auto(); VDIVSD = auto(); VDIVPS = auto(); VDIVPD = auto()
# int vector binary
VPADDB = auto(); VPADDW = auto(); VPADDD = auto(); VPADDQ = auto()
VPSUBB = auto(); VPSUBW = auto(); VPSUBD = auto(); VPSUBQ = auto()
VPMULLW = auto(); VPMULLD = auto()
# packed bitwise
VPAND = auto(); VPOR = auto(); VPXOR = auto()
# packed variable shifts
VPSLLVD = auto(); VPSLLVQ = auto(); VPSRLVD = auto(); VPSRLVQ = auto(); VPSRAVD = auto()
VROUNDSS = auto(); VROUNDSD = auto(); VSQRTSS = auto(); VSQRTSD = auto()
# float binary
VADDSS = auto(); VADDSD = auto(); VSUBSS = auto(); VSUBSD = auto(); VMULSS = auto(); VMULSD = auto(); VDIVSS = auto(); VDIVSD = auto()
# return
RET = auto()
@@ -75,30 +58,18 @@ class X86GroupOp:
X86Ops.SUB, X86Ops.SUBi, X86Ops.SHL, X86Ops.SHLi, X86Ops.SHR, X86Ops.SHRi, X86Ops.SAR, X86Ops.SARi,
X86Ops.IDIV, X86Ops.DIV, X86Ops.CMOVNE, X86Ops.CMOVE, X86Ops.CMOVL, X86Ops.CMOVB}
# X86Ops whose first src can read from memory
ReadMem1st = {X86Ops.MOV, X86Ops.VMOVSS, X86Ops.VMOVSD, X86Ops.VMOVUPS, X86Ops.MOVZX, X86Ops.MOVSX, X86Ops.MOVSXD, X86Ops.VMOVD, X86Ops.VMOVQ,
X86Ops.VPMOVZXBW, X86Ops.VPMOVZXBD, X86Ops.VPMOVZXBQ, X86Ops.VPMOVZXWD, X86Ops.VPMOVZXWQ, X86Ops.VPMOVZXDQ,
X86Ops.VPMOVSXBW, X86Ops.VPMOVSXBD, X86Ops.VPMOVSXBQ, X86Ops.VPMOVSXWD, X86Ops.VPMOVSXWQ, X86Ops.VPMOVSXDQ,
X86Ops.VCVTDQ2PS, X86Ops.VCVTDQ2PD, X86Ops.VCVTTPS2DQ, X86Ops.VCVTTPD2DQ, X86Ops.VCVTTSS2SI, X86Ops.VCVTTSD2SI,
X86Ops.VCVTPH2PS, X86Ops.VCVTPS2PD, X86Ops.VCVTPD2PS, X86Ops.VROUNDPS, X86Ops.VROUNDPD, X86Ops.VSQRTPS, X86Ops.VSQRTPD,
X86Ops.CMPi, X86Ops.IMULi, X86Ops.LEA}
# X86Ops whose second src can read from memory NOTE: some of these are TwoAddress so the second src is actually the first
ReadMem2nd = {X86Ops.ADD, X86Ops.SUB, X86Ops.AND, X86Ops.OR, X86Ops.XOR, X86Ops.IMUL, X86Ops.CMP,
X86Ops.VADDSS, X86Ops.VADDSD, X86Ops.VADDPS, X86Ops.VADDPD, X86Ops.VSUBSS, X86Ops.VSUBSD, X86Ops.VSUBPS, X86Ops.VSUBPD,
X86Ops.VMULSS, X86Ops.VMULSD, X86Ops.VMULPS, X86Ops.VMULPD, X86Ops.VDIVSS, X86Ops.VDIVSD, X86Ops.VDIVPS, X86Ops.VDIVPD,
X86Ops.VPADDB, X86Ops.VPADDW, X86Ops.VPADDD, X86Ops.VPADDQ, X86Ops.VPSUBB, X86Ops.VPSUBW, X86Ops.VPSUBD, X86Ops.VPSUBQ,
X86Ops.VBLENDVPS, X86Ops.VBLENDVPD, X86Ops.VCMPSS, X86Ops.VCMPSD, X86Ops.VCMPPS, X86Ops.VCMPPD,
X86Ops.VPMULLW, X86Ops.VPMULLD, X86Ops.VROUNDSS, X86Ops.VROUNDSD, X86Ops.VSQRTSS, X86Ops.VSQRTSD, X86Ops.VINSERTPS,
X86Ops.VPINSRB, X86Ops.VPINSRW, X86Ops.VPINSRD, X86Ops.VPINSRQ, X86Ops.VPAND, X86Ops.VPOR, X86Ops.VPXOR, X86Ops.VPSLLVD,
X86Ops.VPSLLVQ, X86Ops.VPSRLVD, X86Ops.VPSRLVQ, X86Ops.VPSRAVD, X86Ops.CMOVNE, X86Ops.CMOVE, X86Ops.CMOVL, X86Ops.CMOVB,
X86Ops.VCVTSI2SS, X86Ops.VCVTSI2SD, X86Ops.VCVTSS2SD, X86Ops.VCVTSD2SS, X86Ops.IDIV, X86Ops.DIV}
# X86Ops whose second src is the rm field, so that src is what can be a memory operand
Rm2nd = {X86Ops.ADD, X86Ops.SUB, X86Ops.AND, X86Ops.OR, X86Ops.XOR, X86Ops.IMUL, X86Ops.CMP,
X86Ops.VADDSS, X86Ops.VADDSD, X86Ops.VSUBSS, X86Ops.VSUBSD, X86Ops.VMULSS, X86Ops.VMULSD, X86Ops.VDIVSS, X86Ops.VDIVSD,
X86Ops.VBLENDVPS, X86Ops.VBLENDVPD, X86Ops.VCMPSS, X86Ops.VCMPSD, X86Ops.VROUNDSS, X86Ops.VROUNDSD, X86Ops.VSQRTSS, X86Ops.VSQRTSD,
X86Ops.VINSERTPS, X86Ops.VPINSRW, X86Ops.VPINSRD, X86Ops.CMOVNE, X86Ops.CMOVE, X86Ops.CMOVL, X86Ops.CMOVB,
X86Ops.VCVTSI2SS, X86Ops.VCVTSI2SD, X86Ops.VCVTSS2SD, X86Ops.VCVTSD2SS, X86Ops.IDIV, X86Ops.DIV}
# X86Ops that can write to memory
WriteMem = {X86Ops.MOVm, X86Ops.MOVi, X86Ops.VMOVSSm, X86Ops.VMOVSDm, X86Ops.VMOVUPSm, X86Ops.VMOVDm, X86Ops.VMOVQm,
X86Ops.ADDi, X86Ops.SUBi, X86Ops.ANDi, X86Ops.ORi, X86Ops.XORi, X86Ops.SHL, X86Ops.SHLi, X86Ops.SHR, X86Ops.SHRi, X86Ops.SAR,
X86Ops.SARi, X86Ops.SETNE, X86Ops.SETE, X86Ops.SETL, X86Ops.SETB,
X86Ops.VCVTPS2PH, X86Ops.VPEXTRB, X86Ops.VPEXTRW, X86Ops.VPEXTRD, X86Ops.VPEXTRQ}
X86Ops.VCVTPS2PH, X86Ops.VPEXTRW, X86Ops.VPEXTRD}
# X86Ops that read flags
ReadFlags = {X86Ops.CMOVB, X86Ops.CMOVL, X86Ops.CMOVE, X86Ops.CMOVNE, X86Ops.SETB, X86Ops.SETL, X86Ops.SETE, X86Ops.SETNE, X86Ops.JB, X86Ops.JL,
@@ -109,11 +80,9 @@ class X86GroupOp:
X86Ops.SHL, X86Ops.SHLi, X86Ops.SHR, X86Ops.SHRi, X86Ops.SAR, X86Ops.SARi, X86Ops.AND, X86Ops.ANDi, X86Ops.XOR, X86Ops.XORi,
X86Ops.OR, X86Ops.ORi}
# X86Ops whose first src is the rm field
Rm1st = ReadMem1st | (ReadMem2nd & TwoAddress) | {X86Ops.VPSRLDQ}
# X86Ops whose second src is the rm field
Rm2nd = ReadMem2nd
# X86Ops whose first src is the rm field. a TwoAddress op drops its first src post regalloc, so its Rm2nd src ends up first
Rm1st = {X86Ops.MOV, X86Ops.VMOVSS, X86Ops.VMOVSD, X86Ops.VMOVUPS, X86Ops.MOVZX, X86Ops.MOVSX, X86Ops.MOVSXD, X86Ops.VMOVD, X86Ops.VMOVQ,
X86Ops.VCVTTSS2SI, X86Ops.VCVTTSD2SI, X86Ops.VCVTPH2PS, X86Ops.CMPi, X86Ops.IMULi, X86Ops.LEA, X86Ops.VPSRLDQ} | (Rm2nd & TwoAddress)
# ***** X86 legalization *****
@@ -136,15 +105,12 @@ extra_matcher = PatternMatcher([
# no int8 mul or cmove, cast to int16
(UPat.var("a", dtypes.int8s) * UPat.var("b"), lambda a,b: (a.cast(dtypes.int16) * b.cast(dtypes.int16)).cast(a.dtype)),
(UPat.var("m").where(UPat.var("a", (dtypes.bool,)+dtypes.int8s), UPat.var("b")),
lambda m,a,b: m.where(a.cast(dtypes.int16), b.cast(dtypes.int16)).cast(a.dtype) if a.max_numel() == 1 else None),
lambda m,a,b: m.where(a.cast(dtypes.int16), b.cast(dtypes.int16)).cast(a.dtype)),
# float16 alus are done in float32
(UPat(GroupOp.ALU, dtypes.float16, name="x"), lambda x: UOp(x.op,
src=tuple(s.cast(dtypes.float) if s.dtype != dtypes.bool else s for s in x.src)).cast(x.dtype)),
(UPat(GroupOp.Comparison, src=[UPat(dtype=dtypes.float16), UPat()], name="x"),
lambda x: UOp(x.op, src=tuple(s.cast(dtypes.float32) for s in x.src)).cast(x.dtype)),
# no cmpne for packed ints, y != x => !(y==x)
(UPat(Ops.CMPNE, src=(UPat.var("y", dtypes.ints), UPat.var("x")), name="cmp"),
lambda y,x,cmp: UOp(Ops.CMPEQ, src=(y,x))^True if y.max_numel() > 1 else None),
# a float WHERE blends at the width of its value, so it needs a comparison at that width to make the mask
(UPat.var("m", dtypes.bool).where(UPat.var("a", dtypes.floats+(dtypes.weakfloat,)), UPat.var("b")).named("w"),
lambda m,a,b,w: m.cast(w.dtype).ne(0).where(a, b) if w.dtype in dtypes.floats and promo_dtype(m.src) is not w.dtype else None),
@@ -179,8 +145,8 @@ def flag_gate(m:UOp) -> UOp|None:
# legalize the new style graph for isel. NOTE: this runs after the spec is verified, some of these rewrites violate it
pre_isel_matcher = PatternMatcher([
# widening a scalar uint32 is free, the 32bit write that produced it already zeroed the upper half
(UPat.var("y", dtypes.uint32).cast(dtypes.int64s, name="x"), lambda y,x: x.replace(op=Ops.BITCAST) if y.max_numel() == 1 else None),
# widening a uint32 is free, the 32bit write that produced it already zeroed the upper half
(UPat(dtype=dtypes.uint32).cast(dtypes.int64s, name="x"), lambda x: x.replace(op=Ops.BITCAST)),
(UPat.var("y", dtypes.ints+(dtypes.bool,)).cast(dtypes.ints, name="x"),
lambda y,x: x.replace(op=Ops.BITCAST) if x.dtype.itemsize == y.dtype.itemsize else None),
# gated load/store become a conditional move on the address, the load/store are unconditional
@@ -231,8 +197,7 @@ def cmp(x:UOp) -> UOp:
# comparisons that produce masks, the mask has the width of the operands
def mask(x:UOp) -> UOp:
dt, v = x.src[0].dtype, imm(dtypes.uint8, {Ops.CMPLT: 1, Ops.CMPNE: 4, Ops.CMPEQ: 0}[x.op])
if dt is dtypes.float32: return x.ins(X86Ops.VCMPSS if x.max_numel() == 1 else X86Ops.VCMPPS, dtype=dt, src=x.src + (v,))
return x.ins(X86Ops.VCMPSD if x.max_numel() == 1 else X86Ops.VCMPPD, dtype=dt, src=x.src + (v,))
return x.ins(X86Ops.VCMPSS if dt is dtypes.float32 else X86Ops.VCMPSD, dtype=dt, src=x.src + (v,))
# vinsertps xmm2, xmm0, xmm1, imm
# inserts any 32 bit element in xmm1 into any position in xmm0 according to immm, result is written to xmm2
@@ -243,10 +208,10 @@ def vinsertps(x:UOp) -> UOp:
return x.ins(X86Ops.VINSERTPS, src=(ret, s, imm(dtypes.uint8, v << 6 | i << 4)))
return functools.reduce(_insert, range(len(x.src)), def_reg(x.dtype))
# vpinsq xmm2, xmm0, rax, imm
# inserts element in rax into any position in xmm0, result is written to xmm2 according to imm
# vpinsrd xmm2, xmm0, eax, imm
# inserts the element in eax into any position in xmm0, result is written to xmm2 according to imm
def vpins(x:UOp, srcs:tuple[UOp, ...]) -> UOp:
op = {1: X86Ops.VPINSRB, 2: X86Ops.VPINSRW, 4: X86Ops.VPINSRD, 8: X86Ops.VPINSRQ}[x.dtype.itemsize]
op = {2: X86Ops.VPINSRW, 4: X86Ops.VPINSRD}[x.dtype.itemsize]
return functools.reduce(lambda ret,i: x.ins(op, src=(ret, srcs[i], imm(dtypes.uint8, i))), range(len(srcs)), def_reg(x.dtype))
# we don't call ctx.vreg on the srcs to avoid duplicates, a rewrite will assign the tuple of valid registers to a vreg
@@ -306,8 +271,7 @@ def abi(ctx:IselContext, x:UOp) -> UOp|None:
# this move "cleanses" the abi register constraint
return x.ins(X86Ops.MOV, dtype=dt, src=src)
GPR_DEST_OPS = {X86Ops.VPEXTRB, X86Ops.VPEXTRW, X86Ops.VPEXTRD, X86Ops.VPEXTRQ, X86Ops.VCVTTSS2SI, X86Ops.VCVTTSD2SI,
X86Ops.VMOVDm, X86Ops.VMOVQm}
GPR_DEST_OPS = {X86Ops.VPEXTRW, X86Ops.VPEXTRD, X86Ops.VCVTTSS2SI, X86Ops.VCVTTSD2SI, X86Ops.VMOVDm, X86Ops.VMOVQm}
XMM_OPS = {op for op in X86Ops if op.name.startswith('V')} - GPR_DEST_OPS
def _is_vec_xmm(y: UOp) -> bool:
@@ -351,7 +315,6 @@ isel_matcher = PatternMatcher([
# **** Op -> Op ****
# range is lowered to acc, cmp, jmp after regalloc
(UPat(Ops.RANGE, src=(UPat.cvar("c").cast(),), allow_any_len=True, name="x"), lambda c,x: x.replace(src=(imm(x.dtype, c.val),) + x.src[1:])),
(UPat(Ops.RANGE, name="x"), lambda ctx,x: x.replace(tag=(ctx.vreg(WGPR),)) if not isinstance(x.tag, tuple) else None),
# really all a backedge END is is an IF with a tag referencing the RANGE start label
(UPat(Ops.END, src=(UPat(), UPat(), UPat(GroupOp.Comparison, name="cond")), name="x"),
lambda x,cond: cond.ins(X86Ops.LOOP_CMP, tag=cond.op, src=cond.src + x.src[:2])),
@@ -399,50 +362,22 @@ isel_matcher = PatternMatcher([
(UPat(Ops.CMPEQ, name="x"), lambda x: x.ins(X86Ops.SETE, src=(cmp(x),))),
(UPat(Ops.CMPNE, name="x"), lambda x: x.ins(X86Ops.SETNE, src=(cmp(x),))),
# float unary
(UPat.var("y", dtypes.float32).sqrt().named("x"), lambda y,x: x.ins(X86Ops.VSQRTSS, src=(y, y)) if x.max_numel() == 1 else x.ins(X86Ops.VSQRTPS)),
(UPat.var("y", dtypes.float64).sqrt().named("x"), lambda y,x: x.ins(X86Ops.VSQRTSD, src=(y, y)) if x.max_numel() == 1 else x.ins(X86Ops.VSQRTPD)),
(UPat.var("y", dtypes.float32).trunc().named("x"), lambda y,x:
x.ins(X86Ops.VROUNDSS, src=(y, y, imm(dtypes.uint8, 3))) if x.max_numel() == 1 else x.ins(X86Ops.VROUNDPS, src=(y, imm(dtypes.uint8, 3)))),
(UPat.var("y", dtypes.float64).trunc().named("x"), lambda y,x:
x.ins(X86Ops.VROUNDSD, src=(y, y, imm(dtypes.uint8, 3))) if x.max_numel() == 1 else x.ins(X86Ops.VROUNDPD, src=(y, imm(dtypes.uint8, 3)))),
(UPat.var("y", dtypes.float32).sqrt().named("x"), lambda y,x: x.ins(X86Ops.VSQRTSS, src=(y, y))),
(UPat.var("y", dtypes.float64).sqrt().named("x"), lambda y,x: x.ins(X86Ops.VSQRTSD, src=(y, y))),
(UPat.var("y", dtypes.float32).trunc().named("x"), lambda y,x: x.ins(X86Ops.VROUNDSS, src=(y, y, imm(dtypes.uint8, 3)))),
(UPat.var("y", dtypes.float64).trunc().named("x"), lambda y,x: x.ins(X86Ops.VROUNDSD, src=(y, y, imm(dtypes.uint8, 3)))),
# for float16 we route the srcs through gprs, this is suboptimal for values in xmms, in that case we want vpunpcklwd
(UPat(Ops.STACK, dtypes.float16, name="x"), lambda x: vpins(x, tuple(s.bitcast(dtypes.int16) for s in x.src))),
(UPat(Ops.STACK, dtypes.float32, name="x"), vinsertps),
(UPat(Ops.STACK, dtypes.ints+(dtypes.bool,), name="x"), lambda x: vpins(x, x.src)),
(UPat(Ops.STACK, dtypes.int32s, name="x"), lambda x: vpins(x, x.src)),
# INDEX on a vector register value extracts a single element
(UPat.var("y", dtypes.int8s+(dtypes.bool,)).index(UPat.cvar("c").cast(), name="x"),
lambda y,c,x: x.ins(X86Ops.VPEXTRB, src=(y, imm(dtypes.uint8, c.val))) if _is_vec_xmm(y) else None),
(UPat.var("y", dtypes.int16s).index(UPat.cvar("c").cast(), name="x"),
lambda y,c,x: x.ins(X86Ops.VPEXTRW, src=(y, imm(dtypes.uint8, c.val))) if _is_vec_xmm(y) else None),
(UPat.var("y", dtypes.int32s).index(UPat.cvar("c").cast(), name="x"),
lambda y,c,x: x.ins(X86Ops.VPEXTRD, src=(y, imm(dtypes.uint8, c.val))) if _is_vec_xmm(y) else None),
(UPat.var("y", dtypes.int64s).index(UPat.cvar("c").cast(), name="x"),
lambda y,c,x: x.ins(X86Ops.VPEXTRQ, src=(y, imm(dtypes.uint8, c.val))) if _is_vec_xmm(y) else None),
(UPat.var("y", dtypes.floats).index(UPat.cvar("c").cast(), name="x"),
lambda y,c,x: x.ins(X86Ops.VPSRLDQ, src=(y, imm(dtypes.uint8, c.val * x.dtype.itemsize))) if _is_vec_xmm(y) else None),
# packed bitwise
((UPat() & UPat()).named("x"), lambda x: x.ins(X86Ops.VPAND) if x.max_numel() > 1 else None),
((UPat() | UPat()).named("x"), lambda x: x.ins(X86Ops.VPOR) if x.max_numel() > 1 else None),
((UPat() ^ UPat()).named("x"), lambda x: x.ins(X86Ops.VPXOR) if x.max_numel() > 1 else None),
# packed int binary
((UPat(dtype=dtypes.int32s) << UPat()).named("x"), lambda x: x.ins(X86Ops.VPSLLVD) if x.max_numel() > 1 else None),
((UPat(dtype=dtypes.int64s) << UPat()).named("x"), lambda x: x.ins(X86Ops.VPSLLVQ) if x.max_numel() > 1 else None),
((UPat(dtype=dtypes.uint32) >> UPat()).named("x"), lambda x: x.ins(X86Ops.VPSRLVD) if x.max_numel() > 1 else None),
((UPat(dtype=dtypes.uint64) >> UPat()).named("x"), lambda x: x.ins(X86Ops.VPSRLVQ) if x.max_numel() > 1 else None),
((UPat(dtype=dtypes.int32) >> UPat()).named("x"), lambda x: x.ins(X86Ops.VPSRAVD) if x.max_numel() > 1 else None),
((UPat(dtype=dtypes.int8s) + UPat()).named("x"), lambda x: x.ins(X86Ops.VPADDB) if x.max_numel() > 1 else None),
((UPat(dtype=dtypes.int16s) + UPat()).named("x"), lambda x: x.ins(X86Ops.VPADDW) if x.max_numel() > 1 else None),
((UPat(dtype=dtypes.int32s) + UPat()).named("x"), lambda x: x.ins(X86Ops.VPADDD) if x.max_numel() > 1 else None),
((UPat(dtype=dtypes.int64s) + UPat()).named("x"), lambda x: x.ins(X86Ops.VPADDQ) if x.max_numel() > 1 else None),
(UPat(Ops.SUB, dtypes.int8s, name="x"), lambda x: x.ins(X86Ops.VPSUBB) if x.max_numel() > 1 else None),
(UPat(Ops.SUB, dtypes.int16s, name="x"), lambda x: x.ins(X86Ops.VPSUBW) if x.max_numel() > 1 else None),
(UPat(Ops.SUB, dtypes.int32s, name="x"), lambda x: x.ins(X86Ops.VPSUBD) if x.max_numel() > 1 else None),
(UPat(Ops.SUB, dtypes.int64s, name="x"), lambda x: x.ins(X86Ops.VPSUBQ) if x.max_numel() > 1 else None),
(UPat(Ops.MUL, dtypes.int16s, name="x"), lambda x: x.ins(X86Ops.VPMULLW) if x.max_numel() > 1 else None),
(UPat(Ops.MUL, dtypes.int32s, name="x"), lambda x: x.ins(X86Ops.VPMULLD) if x.max_numel() > 1 else None),
# scalar int binary
# int binary
((UPat(dtype=dtypes.ints).alu(Ops.CDIV, UPat())).named("x"), idiv),
# scalar int binary with immediate
# int binary with immediate
(UPat.var("a", dtypes.ints) << UPat.cvar("c").cast(), lambda a,c: a.ins(X86Ops.SHLi, src=(a, imm(dtypes.uint8, c.val)))),
(UPat.var("a", dtypes.uints) >> UPat.cvar("c").cast(), lambda a,c: a.ins(X86Ops.SHRi, src=(a, imm(dtypes.uint8, c.val)))),
(UPat.var("a", dtypes.sints) >> UPat.cvar("c").cast(), lambda a,c: a.ins(X86Ops.SARi, src=(a, imm(dtypes.uint8, c.val)))),
@@ -456,7 +391,7 @@ isel_matcher = PatternMatcher([
lambda a,c: a.ins(X86Ops.XORi, src=(a, i)) if (i:=to_imm(c)) is not None else None),
(UPat(Ops.SUB, dtypes.ints, (UPat.var("a"), UPat.cvar().cast(name="c"))),
lambda a,c: a.ins(X86Ops.SUBi, src=(a, i)) if (i:=to_imm(c)) is not None else None),
# scalar int binary with register
# int binary with register
((UPat(dtype=dtypes.ints) << UPat()).named("x"), lambda x: shift(x, X86Ops.SHL)),
((UPat(dtype=dtypes.uints) >> UPat()).named("x"), lambda x: shift(x, X86Ops.SHR)),
((UPat(dtype=dtypes.sints) >> UPat()).named("x"), lambda x: shift(x, X86Ops.SAR)),
@@ -467,21 +402,15 @@ isel_matcher = PatternMatcher([
(UPat.var("a", dtypes.ints+(dtypes.bool,)) ^ UPat.var("b"), lambda a,b: a.ins(X86Ops.XOR, src=(a, b))),
(UPat(Ops.SUB, dtypes.ints, (UPat.var("a"), UPat.var("b"))), lambda a,b: a.ins(X86Ops.SUB, src=(a, b))),
# float binary
((UPat(dtype=dtypes.float32) + UPat()).named("x"), lambda x: x.ins(X86Ops.VADDSS if x.max_numel() == 1 else X86Ops.VADDPS)),
((UPat(dtype=dtypes.float64) + UPat()).named("x"), lambda x: x.ins(X86Ops.VADDSD if x.max_numel() == 1 else X86Ops.VADDPD)),
((UPat(dtype=dtypes.float32) * UPat()).named("x"), lambda x: x.ins(X86Ops.VMULSS if x.max_numel() == 1 else X86Ops.VMULPS)),
((UPat(dtype=dtypes.float64) * UPat()).named("x"), lambda x: x.ins(X86Ops.VMULSD if x.max_numel() == 1 else X86Ops.VMULPD)),
(UPat(Ops.SUB, dtypes.float32, name="x"), lambda x: x.ins(X86Ops.VSUBSS if x.max_numel() == 1 else X86Ops.VSUBPS)),
(UPat(Ops.SUB, dtypes.float64, name="x"), lambda x: x.ins(X86Ops.VSUBSD if x.max_numel() == 1 else X86Ops.VSUBPD)),
(UPat(Ops.FDIV, dtypes.float32, name="x"), lambda x: x.ins(X86Ops.VDIVSS if x.max_numel() == 1 else X86Ops.VDIVPS)),
(UPat(Ops.FDIV, dtypes.float64, name="x"), lambda x: x.ins(X86Ops.VDIVSD if x.max_numel() == 1 else X86Ops.VDIVPD)),
((UPat(dtype=dtypes.float32) + UPat()).named("x"), lambda x: x.ins(X86Ops.VADDSS)),
((UPat(dtype=dtypes.float64) + UPat()).named("x"), lambda x: x.ins(X86Ops.VADDSD)),
((UPat(dtype=dtypes.float32) * UPat()).named("x"), lambda x: x.ins(X86Ops.VMULSS)),
((UPat(dtype=dtypes.float64) * UPat()).named("x"), lambda x: x.ins(X86Ops.VMULSD)),
(UPat(Ops.SUB, dtypes.float32, name="x"), lambda x: x.ins(X86Ops.VSUBSS)),
(UPat(Ops.SUB, dtypes.float64, name="x"), lambda x: x.ins(X86Ops.VSUBSD)),
(UPat(Ops.FDIV, dtypes.float32, name="x"), lambda x: x.ins(X86Ops.VDIVSS)),
(UPat(Ops.FDIV, dtypes.float64, name="x"), lambda x: x.ins(X86Ops.VDIVSD)),
# casts
(UPat(dtype=dtypes.int32).cast(dtypes.float32, name="x"), lambda x: x.ins(X86Ops.VCVTDQ2PS) if x.max_numel() > 1 else None),
(UPat(dtype=dtypes.int32).cast(dtypes.float64, name="x"), lambda x: x.ins(X86Ops.VCVTDQ2PD) if x.max_numel() > 1 else None),
(UPat(dtype=dtypes.float32).cast(dtypes.int32s, name="x"), lambda x: x.ins(X86Ops.VCVTTPS2DQ) if x.max_numel() > 1 else None),
(UPat(dtype=dtypes.float64).cast(dtypes.int32s, name="x"), lambda x: x.ins(X86Ops.VCVTTPD2DQ) if x.max_numel() > 1 else None),
(UPat(dtype=dtypes.float32).cast(dtypes.float64, name="x"), lambda x: x.ins(X86Ops.VCVTPS2PD) if x.max_numel() > 1 else None),
(UPat(dtype=dtypes.float64).cast(dtypes.float32, name="x"), lambda x: x.ins(X86Ops.VCVTPD2PS) if x.max_numel() > 1 else None),
(UPat(dtype=dtypes.float32).cast(dtypes.float16, name="x"), lambda x: x.ins(X86Ops.VCVTPS2PH, src=x.src + (imm(dtypes.uint8, 4),))),
(UPat(dtype=dtypes.float16).cast(dtypes.float32, name="x"), lambda x: x.ins(X86Ops.VCVTPH2PS)),
(UPat(dtype=dtypes.float32).cast(dtypes.int32s+dtypes.int64s, name="x"), lambda x: x.ins(X86Ops.VCVTTSS2SI)),
@@ -491,23 +420,10 @@ isel_matcher = PatternMatcher([
(UPat.var("y", (dtypes.int32, dtypes.int64)).cast(dtypes.float32, name="x"), lambda y,x: x.ins(X86Ops.VCVTSI2SS, src=(def_reg(x.dtype), y))),
(UPat.var("y", (dtypes.int32, dtypes.int64)).cast(dtypes.float64, name="x"), lambda y,x: x.ins(X86Ops.VCVTSI2SD, src=(def_reg(x.dtype), y))),
(UPat(dtype=(dtypes.uint8, dtypes.uint16, dtypes.bool)).cast(dtypes.ints, name="x"), lambda x:
x.ins(X86Ops.MOVZX) if x.max_numel() == 1 and x.src[0].dtype.itemsize < x.dtype.itemsize else None),
(UPat(dtype=dtypes.int32).cast(dtypes.int64s, name="x"), lambda x: x.ins(X86Ops.MOVSXD) if x.max_numel() == 1 else None),
(UPat(dtype=dtypes.sints).cast(dtypes.ints, name="x"), lambda x:
x.ins(X86Ops.MOVSX) if x.max_numel() == 1 and x.src[0].dtype.itemsize < x.dtype.itemsize else None),
(UPat(dtype=dtypes.ints).cast(dtypes.ints, name="x"), lambda x: x.ins(X86Ops.MOV) if x.max_numel() == 1 else None),
(UPat(dtype=(dtypes.uint8, dtypes.bool)).cast(dtypes.int16s, name="x"), lambda x: x.ins(X86Ops.VPMOVZXBW)),
(UPat(dtype=(dtypes.uint8, dtypes.bool)).cast(dtypes.int32s, name="x"), lambda x: x.ins(X86Ops.VPMOVZXBD)),
(UPat(dtype=(dtypes.uint8, dtypes.bool)).cast(dtypes.int64s, name="x"), lambda x: x.ins(X86Ops.VPMOVZXBQ)),
(UPat(dtype=dtypes.uint16).cast(dtypes.int32s, name="x"), lambda x: x.ins(X86Ops.VPMOVZXWD)),
(UPat(dtype=dtypes.uint16).cast(dtypes.int64s, name="x"), lambda x: x.ins(X86Ops.VPMOVZXWQ)),
(UPat(dtype=dtypes.uint32).cast(dtypes.int64s, name="x"), lambda x: x.ins(X86Ops.VPMOVZXDQ)),
(UPat(dtype=dtypes.int8).cast(dtypes.int16s, name="x"), lambda x: x.ins(X86Ops.VPMOVSXBW)),
(UPat(dtype=dtypes.int8).cast(dtypes.int32s, name="x"), lambda x: x.ins(X86Ops.VPMOVSXBD)),
(UPat(dtype=dtypes.int8).cast(dtypes.int64s, name="x"), lambda x: x.ins(X86Ops.VPMOVSXBQ)),
(UPat(dtype=dtypes.int16).cast(dtypes.int32s, name="x"), lambda x: x.ins(X86Ops.VPMOVSXWD)),
(UPat(dtype=dtypes.int16).cast(dtypes.int64s, name="x"), lambda x: x.ins(X86Ops.VPMOVSXWQ)),
(UPat(dtype=dtypes.int32).cast(dtypes.int64s, name="x"), lambda x: x.ins(X86Ops.VPMOVSXDQ)),
x.ins(X86Ops.MOVZX) if x.src[0].dtype.itemsize < x.dtype.itemsize else None),
(UPat(dtype=dtypes.int32).cast(dtypes.int64s, name="x"), lambda x: x.ins(X86Ops.MOVSXD)),
(UPat(dtype=dtypes.sints).cast(dtypes.ints, name="x"), lambda x: x.ins(X86Ops.MOVSX) if x.src[0].dtype.itemsize < x.dtype.itemsize else None),
(UPat(dtype=dtypes.ints).cast(dtypes.ints, name="x"), lambda x: x.ins(X86Ops.MOV)),
# bitcasts between scalar floats and ints
(UPat.var("y", dtypes.float16).bitcast(dtypes.int16s).named("x"), lambda y,x: x.ins(X86Ops.VPEXTRW, src=(y, imm(dtypes.uint8, 0)))),
(UPat(dtype=dtypes.int16s).bitcast(dtypes.float16).named("x"), lambda x: vpins(x, x.src)),
@@ -518,40 +434,32 @@ isel_matcher = PatternMatcher([
# index on a buffer (or the stack pointer) computes an address, addresses are 64bit values
(UPat((Ops.INDEX, Ops.SHRINK), name="x"), lambda x: lea(x) if not _is_vec_xmm(x.src[0]) else None),
# TODO: fuse stores, very few cases -- store cmp becomes setcc, store gep int becomes vpextr, store bitcast to int becomes vmovd/q
# copy, load, store
# NOTE: copy here violates the spec, it only happens post register allocation when a reg to reg move needs to be inserted
(UPat(Ops.COPY, dtypes.floats, name="x"), lambda x: x.ins(_xmm_sz(x))),
(UPat(Ops.COPY, dtypes.ints+(dtypes.bool,), name="x"), lambda x: x.ins(X86Ops.MOV) if x.max_numel() == 1 else x.ins(_xmm_sz(x))),
# load, store
(UPat(Ops.LOAD, dtypes.floats, src=(UPat(name="a"),), name="x"), lambda x,a:
x.ins(X86Ops.VPINSRW, src=(def_reg(x.dtype, x.tag),) + fold_address(a) + (imm(dtypes.uint8, 0),)) if x.max_numel() * x.dtype.itemsize == 2 else
x.ins(_xmm_sz(x), src=fold_address(a))),
(UPat(Ops.LOAD, dtypes.ints+(dtypes.bool,), src=(UPat(name="a"),), name="x"), lambda x,a:
x.ins(X86Ops.MOV, src=fold_address(a)) if x.max_numel() == 1 else
x.ins(X86Ops.VPINSRW, src=(def_reg(x.dtype, x.tag),) + fold_address(a) + (imm(dtypes.uint8, 0),)) if x.max_numel() * x.dtype.itemsize == 2 else
x.ins(_xmm_sz(x), src=fold_address(a))),
x.ins(X86Ops.MOV, src=fold_address(a)) if x.max_numel() == 1 else x.ins(_xmm_sz(x), src=fold_address(a))),
(UPat.var("a").store(UPat.var("b", dtypes.floats), name="x"), lambda a,b,x:
x.ins(X86Ops.VPEXTRW, src=fold_address(a) + (b, imm(dtypes.uint8, 0))) if b.max_numel() * b.dtype.itemsize == 2 else
x.ins(_xmm_sz_m(b), src=fold_address(a) + (b,))),
(UPat.var("a").store(UPat.var("b", dtypes.ints+(dtypes.bool,)), name="x"), lambda a,b,x:
x.ins(X86Ops.VPEXTRW, src=fold_address(a) + (b, imm(dtypes.uint8, 0))) if b.max_numel() > 1 and b.max_numel() * b.dtype.itemsize == 2 else
x.ins(_xmm_sz_m(b), src=fold_address(a) + (b,)) if b.max_numel() > 1 else
x.ins(X86Ops.MOVm, src=fold_address(a) + (b,)) if (i:=to_imm(b)) is None else x.ins(X86Ops.MOVi, src=fold_address(a) + (i,))),
# allocate virtual registers
(UPat((Ops.INS, Ops.BUFFER), name="x"), alloc_vregs),
(UPat((Ops.INS, Ops.BUFFER, Ops.RANGE), name="x"), alloc_vregs),
])
# ***** pre register allocation *****
# this handles flag clobbers. Unfortunately x86 doesn't have a good way to store/restore the flag register (then regalloc would handle it)
# so we rematerialize. This is different from rematerialization you might want to do in regalloc because it is not optional,
# regalloc shouldn't rematerialize if a src of the instruction is dead, but here you need to as there's no fallback load from stack
# the flags belong to the last instruction that wrote them. x86 has no good way to store/restore them (then regalloc would
# handle it), so a consumer that no longer owns its compare re-emits it. Unlike a regalloc rematerialization this is not
# optional, there is no fallback load from stack
def flag_rematerialize(ctx:PreRegAllocContext, x:UOp):
flag_def = x if x.op in (Ops.RANGE, Ops.END) or x.arg[0] in X86GroupOp.WriteFlags else x.src[-1] if x.arg[0] in X86GroupOp.ReadFlags else None
if flag_def is None: return None
if ctx.lock is not None and ctx.lock is not flag_def: ctx.clobbered.add(ctx.lock)
ctx.lock = flag_def
if flag_def not in ctx.clobbered: return None
ctx.clobbered.remove(flag_def)
return (x, [flag_def, x])
if x.op in (Ops.RANGE, Ops.END) or x.arg[0] in X86GroupOp.WriteFlags: ctx.lock = x
elif x.arg[0] in X86GroupOp.ReadFlags and ctx.lock is not (flag_def:=x.src[-1]):
ctx.lock = flag_def
return (x, [flag_def, x])
return None
pre_regalloc_matcher = PatternMatcher([
(UPat((Ops.INS, Ops.RANGE, Ops.END), name="x"), flag_rematerialize),
@@ -707,20 +615,11 @@ encodings = {
# casts
X86Ops.MOVZX: lambda x: encode(x, 0x0FB7),
X86Ops.MOVSX: lambda x: encode(x, 0x0FBF), X86Ops.MOVSXD: lambda x: encode(x, 0x63),
X86Ops.VPMOVZXBW: lambda x: encode(x, 0x30, pp=1, sel=2), X86Ops.VPMOVZXBD: lambda x: encode(x, 0x31, pp=1, sel=2),
X86Ops.VPMOVZXBQ: lambda x: encode(x, 0x32, pp=1, sel=2), X86Ops.VPMOVZXWD: lambda x: encode(x, 0x33, pp=1, sel=2),
X86Ops.VPMOVZXWQ: lambda x: encode(x, 0x34, pp=1, sel=2), X86Ops.VPMOVZXDQ: lambda x: encode(x, 0x35, pp=1, sel=2),
X86Ops.VPMOVSXBW: lambda x: encode(x, 0x20, pp=1, sel=2), X86Ops.VPMOVSXBD: lambda x: encode(x, 0x21, pp=1, sel=2),
X86Ops.VPMOVSXBQ: lambda x: encode(x, 0x22, pp=1, sel=2), X86Ops.VPMOVSXWD: lambda x: encode(x, 0x23, pp=1, sel=2),
X86Ops.VPMOVSXWQ: lambda x: encode(x, 0x24, pp=1, sel=2), X86Ops.VPMOVSXDQ: lambda x: encode(x, 0x25, pp=1, sel=2),
X86Ops.VCVTSS2SD: lambda x: encode(x, 0x5A, pp=2, sel=1), X86Ops.VCVTSD2SS: lambda x: encode(x, 0x5A, pp=3, sel=1),
X86Ops.VCVTPH2PS: lambda x: encode(x, 0x13, pp=1, sel=2), X86Ops.VCVTPS2PH: lambda x: encode(x, 0x1D, pp=1, sel=3),
X86Ops.VCVTDQ2PS: lambda x: encode(x, 0x5B, pp=0, sel=1), X86Ops.VCVTDQ2PD: lambda x: encode(x, 0xE6, pp=2, sel=1),
X86Ops.VCVTPS2PD: lambda x: encode(x, 0x5A, pp=0, sel=1), X86Ops.VCVTPD2PS: lambda x: encode(x, 0x5A, pp=1, sel=1),
X86Ops.VCVTTPS2DQ: lambda x: encode(x, 0x5B, pp=2, sel=1), X86Ops.VCVTTPD2DQ: lambda x: encode(x, 0xE6, pp=1, sel=1),
# the int src is the 2nd src (the rm field), if it was folded into a memory operand its width is the element size of the address
X86Ops.VCVTSI2SS: lambda x: encode(x, 0x2A, pp=2, sel=1, we=(x.src[4].src[0].val if len(x.src) > 4 else x.src[1].dtype.itemsize) == 8),
X86Ops.VCVTSI2SD: lambda x: encode(x, 0x2A, pp=3, sel=1, we=(x.src[4].src[0].val if len(x.src) > 4 else x.src[1].dtype.itemsize) == 8),
# the int src is the 2nd src (the rm field), its width picks the 32 or 64 bit form
X86Ops.VCVTSI2SS: lambda x: encode(x, 0x2A, pp=2, sel=1, we=x.src[1].dtype.itemsize == 8),
X86Ops.VCVTSI2SD: lambda x: encode(x, 0x2A, pp=3, sel=1, we=x.src[1].dtype.itemsize == 8),
X86Ops.VCVTTSS2SI: lambda x: encode(x, 0x2C, pp=2, sel=1, we=x.dtype.itemsize == 8),
X86Ops.VCVTTSD2SI: lambda x: encode(x, 0x2C, pp=3, sel=1, we=x.dtype.itemsize == 8),
# int division
@@ -738,46 +637,25 @@ encodings = {
X86Ops.IMUL: lambda x: encode(x, 0x0FAF), X86Ops.IMULi: lambda x: encode(x, 0x69),
X86Ops.SETB: lambda x: encode(x, 0x0F92, reg=0), X86Ops.SETL: lambda x: encode(x, 0x0F9C, reg=0),
X86Ops.SETE: lambda x: encode(x, 0x0F94, reg=0), X86Ops.SETNE: lambda x: encode(x, 0x0F95, reg=0),
# packed bitwise NOTE: only bitwise and packed
X86Ops.VPAND: lambda x: encode(x, 0xDB, pp=1, sel=1), X86Ops.VPXOR: lambda x: encode(x, 0xEF, pp=1, sel=1),
X86Ops.VPOR: lambda x: encode(x, 0xEB, pp=1, sel=1),
# unary
X86Ops.VSQRTSS: lambda x: encode(x, 0x51, pp=2, sel=1), X86Ops.VSQRTPS: lambda x: encode(x, 0x51, pp=0, sel=1),
X86Ops.VSQRTSD: lambda x: encode(x, 0x51, pp=3, sel=1), X86Ops.VSQRTPD: lambda x: encode(x, 0x51, pp=1, sel=1),
X86Ops.VROUNDSS: lambda x: encode(x, 0x0A, pp=1, sel=3), X86Ops.VROUNDPS: lambda x: encode(x, 0x08, pp=1, sel=3),
X86Ops.VROUNDSD: lambda x: encode(x, 0x0B, pp=1, sel=3), X86Ops.VROUNDPD: lambda x: encode(x, 0x09, pp=1, sel=3),
# packed int binary
X86Ops.VPSLLVD: lambda x: encode(x, 0x47, pp=1, sel=2), X86Ops.VPSLLVQ: lambda x: encode(x, 0x47, pp=1, sel=2, we=1),
X86Ops.VPSRLVD: lambda x: encode(x, 0x45, pp=1, sel=2), X86Ops.VPSRLVQ: lambda x: encode(x, 0x45, pp=1, sel=2, we=1),
X86Ops.VPMULLW: lambda x: encode(x, 0xD5, pp=1, sel=1), X86Ops.VPMULLD: lambda x: encode(x, 0x40, pp=1, sel=2),
X86Ops.VPADDB: lambda x: encode(x, 0xFC, pp=1, sel=1), X86Ops.VPADDW: lambda x: encode(x, 0xFD, pp=1, sel=1),
X86Ops.VPADDD: lambda x: encode(x, 0xFE, pp=1, sel=1), X86Ops.VPADDQ: lambda x: encode(x, 0xD4, pp=1, sel=1),
X86Ops.VPSUBB: lambda x: encode(x, 0xF8, pp=1, sel=1), X86Ops.VPSUBW: lambda x: encode(x, 0xF9, pp=1, sel=1),
X86Ops.VPSUBD: lambda x: encode(x, 0xFA, pp=1, sel=1), X86Ops.VPSUBQ: lambda x: encode(x, 0xFB, pp=1, sel=1),
X86Ops.VPSRAVD: lambda x: encode(x, 0x46, pp=1, sel=2),
# scalar / packed float binary
X86Ops.VADDSS: lambda x: encode(x, 0x58, pp=2, sel=1), X86Ops.VADDPS: lambda x: encode(x, 0x58, pp=0, sel=1),
X86Ops.VADDSD: lambda x: encode(x, 0x58, pp=3, sel=1), X86Ops.VADDPD: lambda x: encode(x, 0x58, pp=1, sel=1),
X86Ops.VSUBSS: lambda x: encode(x, 0x5C, pp=2, sel=1), X86Ops.VSUBPS: lambda x: encode(x, 0x5C, pp=0, sel=1),
X86Ops.VSUBSD: lambda x: encode(x, 0x5C, pp=3, sel=1), X86Ops.VSUBPD: lambda x: encode(x, 0x5C, pp=1, sel=1),
X86Ops.VMULSS: lambda x: encode(x, 0x59, pp=2, sel=1), X86Ops.VMULPS: lambda x: encode(x, 0x59, pp=0, sel=1),
X86Ops.VMULSD: lambda x: encode(x, 0x59, pp=3, sel=1), X86Ops.VMULPD: lambda x: encode(x, 0x59, pp=1, sel=1),
X86Ops.VDIVSS: lambda x: encode(x, 0x5E, pp=2, sel=1), X86Ops.VDIVPS: lambda x: encode(x, 0x5E, pp=0, sel=1),
X86Ops.VDIVSD: lambda x: encode(x, 0x5E, pp=3, sel=1), X86Ops.VDIVPD: lambda x: encode(x, 0x5E, pp=1, sel=1),
X86Ops.VCMPSS: lambda x: encode(x, 0xC2, pp=2, sel=1), X86Ops.VCMPPS: lambda x: encode(x, 0xC2, pp=0, sel=1),
X86Ops.VCMPSD: lambda x: encode(x, 0xC2, pp=3, sel=1), X86Ops.VCMPPD: lambda x: encode(x, 0xC2, pp=1, sel=1),
X86Ops.VSQRTSS: lambda x: encode(x, 0x51, pp=2, sel=1), X86Ops.VSQRTSD: lambda x: encode(x, 0x51, pp=3, sel=1),
X86Ops.VROUNDSS: lambda x: encode(x, 0x0A, pp=1, sel=3), X86Ops.VROUNDSD: lambda x: encode(x, 0x0B, pp=1, sel=3),
# float binary
X86Ops.VADDSS: lambda x: encode(x, 0x58, pp=2, sel=1), X86Ops.VADDSD: lambda x: encode(x, 0x58, pp=3, sel=1),
X86Ops.VSUBSS: lambda x: encode(x, 0x5C, pp=2, sel=1), X86Ops.VSUBSD: lambda x: encode(x, 0x5C, pp=3, sel=1),
X86Ops.VMULSS: lambda x: encode(x, 0x59, pp=2, sel=1), X86Ops.VMULSD: lambda x: encode(x, 0x59, pp=3, sel=1),
X86Ops.VDIVSS: lambda x: encode(x, 0x5E, pp=2, sel=1), X86Ops.VDIVSD: lambda x: encode(x, 0x5E, pp=3, sel=1),
X86Ops.VCMPSS: lambda x: encode(x, 0xC2, pp=2, sel=1), X86Ops.VCMPSD: lambda x: encode(x, 0xC2, pp=3, sel=1),
# ternary
X86Ops.CMOVB: lambda x: encode(x, 0x0F42), X86Ops.CMOVL: lambda x: encode(x, 0x0F4C),
X86Ops.CMOVE: lambda x: encode(x, 0x0F44), X86Ops.CMOVNE: lambda x: encode(x, 0x0F45),
X86Ops.VBLENDVPS: lambda x: encode(x, 0x4A, pp=1, sel=3), X86Ops.VBLENDVPD: lambda x: encode(x, 0x4B, pp=1, sel=3),
# shuffles
X86Ops.VPSRLDQ: lambda x: encode(x, 0x73, reg=3, pp=1, sel=1),
X86Ops.VPINSRB: lambda x: encode(x, 0x20, pp=1, sel=3), X86Ops.VPINSRW: lambda x: encode(x, 0xC4, pp=1, sel=1),
X86Ops.VPINSRD: lambda x: encode(x, 0x22, pp=1, sel=3), X86Ops.VPINSRQ: lambda x: encode(x, 0x22, pp=1, sel=3, we=1),
X86Ops.VPINSRW: lambda x: encode(x, 0xC4, pp=1, sel=1), X86Ops.VPINSRD: lambda x: encode(x, 0x22, pp=1, sel=3),
X86Ops.VINSERTPS: lambda x: encode(x, 0x21, pp=1, sel=3),
# extract
X86Ops.VPEXTRB: lambda x: encode(x, 0x14, pp=1, sel=3), X86Ops.VPEXTRW: lambda x: encode(x, 0x15, pp=1, sel=3),
X86Ops.VPEXTRD: lambda x: encode(x, 0x16, pp=1, sel=3), X86Ops.VPEXTRQ: lambda x: encode(x, 0x16, pp=1, sel=3, we=1),
X86Ops.VPEXTRW: lambda x: encode(x, 0x15, pp=1, sel=3), X86Ops.VPEXTRD: lambda x: encode(x, 0x16, pp=1, sel=3),
# jumps are encoded with a placeholder which gets patched later once the real offset is known
X86Ops.JE: lambda x: bytes([0x0F, 0x84]) + int(0).to_bytes(4),
X86Ops.JNE: lambda x: bytes([0x0F, 0x85]) + int(0).to_bytes(4),
@@ -805,19 +683,14 @@ class X86Renderer(ISARenderer):
self.compiler = X86Compiler()
def is_two_address(self, x:UOp) -> bool: return x.op is Ops.INS and x.arg[0] in X86GroupOp.TwoAddress
def stack_pointer(self) -> UOp: return def_reg(dtypes.uint64, RSP)
# the value of a BUFFER is its address, it moves through registers and the stack as a 64bit int
def copy(self, x:UOp, reg:Register):
if x.op is Ops.BUFFER: x = x.replace(arg=replace(x.arg, dtype=dtypes.uint64))
ret = isel_matcher.rewrite(UOp(Ops.COPY, src=(x,), tag=reg))
assert ret is not None, f"failed to copy {x}"
return ret
def copy(self, x:UOp, reg:Register) -> UOp: return x.ins(X86Ops.MOV, src=(x,), tag=reg)
def spill(self, disp:UOp, x:UOp) -> UOp:
if x.op is Ops.BUFFER: x = x.replace(arg=replace(x.arg, dtype=dtypes.uint64))
is_xmm = isinstance(x.tag, tuple) and x.tag[0].cons[0].size == 16
op = X86Ops.VMOVUPSm if is_xmm else X86Ops.MOVm
return UOp(Ops.INS, src=fold_address(self.stack_pointer().index(disp)) + (x,), arg=(op, dtypes.void), tag=x.tag)
# the value of a BUFFER is its address, it moves through registers and the stack as a 64bit int
def fill(self, disp:UOp, x:UOp, reg:Register) -> UOp:
is_xmm = reg.cons[0].size == 16
dt = dtypes.uint64 if x.op is Ops.BUFFER else x.dtype
+1 -1
View File
@@ -22,7 +22,7 @@ reg_files = {
reg_patterns = {
"gc": ["GCVM", "GCMC_VM", "CP_(HQD|MQD|MEC|ME_CNTL|PERFMON|RB_WPTR_POLL_CNTL|INT_CNTL|STAT|PFP_PRGRM|ME_PRGRM|COHER_START)", "COMPUTE_",
"(SQ|GL2C|TCC)_PERFCOUNTER", "SQ_THREAD_TRACE", "SPI_(CONFIG_CNTL|COMPUTE_QUEUE_RESET)", "GRBM", "SH_MEM", "RLC", "TCP", "GB_ADDR_CONFIG",
"SDMA[01]_(WATCHDOG_CNTL|UTCL1_(CNTL|PAGE)|MCU_CNTL|F32_CNTL|CNTL|QUEUE0_|RLC_CGCG_CTRL)", "SCRATCH_REG[0-367]"],
"SDMA[01]_(WATCHDOG_CNTL|UTCL1_(CNTL|PAGE)|MCU_CNTL|F32_CNTL|CNTL|QUEUE0_|RLC_CGCG_CTRL)", "SCRATCH_REG[0-35-7]"],
"mmhub": ["MMVM", "MMMC_VM", "MM_ATC_L2_MISC_CG"],
"nbio": (nbio:=["BIF_BX_PF[01]_GPU_HDP_FLUSH", "BIF_BX_PF0_RSMU", "BIF_BX0_(REMAP_HDP_MEM_FLUSH_CNTL|BIF_DOORBELL_INT_CNTL|PCIE_INDEX2|PCIE_DATA2)",
"BIFC_(DOORBELL_ACCESS_EN_PF|GFX_INT_MONITOR_MASK)", "XCC_DOORBELL_FENCE", "DOORBELL0_CTRL_ENTRY", "GDC_S2A0_S2A_DOORBELL_ENTRY",
+5
View File
@@ -514,6 +514,7 @@ gc_9_4_3 = {
'regSCRATCH_REG1': (8257, 1, {'scratch_reg1': (0, 31)}),
'regSCRATCH_REG2': (8258, 1, {'scratch_reg2': (0, 31)}),
'regSCRATCH_REG3': (8259, 1, {'scratch_reg3': (0, 31)}),
'regSCRATCH_REG5': (8261, 1, {'scratch_reg5': (0, 31)}),
'regSCRATCH_REG6': (8262, 1, {'scratch_reg6': (0, 31)}),
'regSCRATCH_REG7': (8263, 1, {'scratch_reg7': (0, 31)}),
'regCP_COHER_START_DELAY': (8315, 1, {'start_delay_count': (0, 5)}),
@@ -1799,6 +1800,7 @@ gc_11_0_0 = {
'regSCRATCH_REG1': (8257, 1, {'scratch_reg1': (0, 31)}),
'regSCRATCH_REG2': (8258, 1, {'scratch_reg2': (0, 31)}),
'regSCRATCH_REG3': (8259, 1, {'scratch_reg3': (0, 31)}),
'regSCRATCH_REG5': (8261, 1, {'scratch_reg5': (0, 31)}),
'regSCRATCH_REG6': (8262, 1, {'scratch_reg6': (0, 31)}),
'regSCRATCH_REG7': (8263, 1, {'scratch_reg7': (0, 31)}),
'regRLC_GPM_PERF_COUNT_0': (8512, 1, {'feature_sel': (0, 3), 'se_index': (4, 7), 'sa_index': (8, 11), 'wgp_index': (12, 15), 'event_sel': (16, 17), 'unused': (18, 19), 'enable': (20, 20), 'reserved': (21, 31)}),
@@ -3378,6 +3380,7 @@ gc_11_0_3 = {
'regSCRATCH_REG1': (8257, 1, {'scratch_reg1': (0, 31)}),
'regSCRATCH_REG2': (8258, 1, {'scratch_reg2': (0, 31)}),
'regSCRATCH_REG3': (8259, 1, {'scratch_reg3': (0, 31)}),
'regSCRATCH_REG5': (8261, 1, {'scratch_reg5': (0, 31)}),
'regSCRATCH_REG6': (8262, 1, {'scratch_reg6': (0, 31)}),
'regSCRATCH_REG7': (8263, 1, {'scratch_reg7': (0, 31)}),
'regRLC_GPM_PERF_COUNT_0': (8512, 1, {'feature_sel': (0, 3), 'se_index': (4, 7), 'sa_index': (8, 11), 'wgp_index': (12, 15), 'event_sel': (16, 17), 'unused': (18, 19), 'enable': (20, 20), 'reserved': (21, 31)}),
@@ -4807,6 +4810,7 @@ gc_11_5_0 = {
'regSCRATCH_REG1': (8257, 1, {'scratch_reg1': (0, 31)}),
'regSCRATCH_REG2': (8258, 1, {'scratch_reg2': (0, 31)}),
'regSCRATCH_REG3': (8259, 1, {'scratch_reg3': (0, 31)}),
'regSCRATCH_REG5': (8261, 1, {'scratch_reg5': (0, 31)}),
'regSCRATCH_REG6': (8262, 1, {'scratch_reg6': (0, 31)}),
'regSCRATCH_REG7': (8263, 1, {'scratch_reg7': (0, 31)}),
'regRLC_GPM_PERF_COUNT_0': (8512, 1, {'feature_sel': (0, 3), 'se_index': (4, 7), 'sa_index': (8, 11), 'wgp_index': (12, 15), 'event_sel': (16, 17), 'unused': (18, 19), 'enable': (20, 20), 'reserved': (21, 31)}),
@@ -6068,6 +6072,7 @@ gc_12_0_0 = {
'regSCRATCH_REG1': (8257, 1, {'scratch_reg1': (0, 31)}),
'regSCRATCH_REG2': (8258, 1, {'scratch_reg2': (0, 31)}),
'regSCRATCH_REG3': (8259, 1, {'scratch_reg3': (0, 31)}),
'regSCRATCH_REG5': (8261, 1, {'scratch_reg5': (0, 31)}),
'regSCRATCH_REG6': (8262, 1, {'scratch_reg6': (0, 31)}),
'regSCRATCH_REG7': (8263, 1, {'scratch_reg7': (0, 31)}),
'regRLC_GPM_PERF_COUNT_0': (8512, 1, {'feature_sel': (0, 3), 'se_index': (4, 7), 'sa_index': (8, 11), 'wgp_index': (12, 15), 'event_sel': (16, 17), 'unused': (18, 19), 'enable': (20, 20), 'reserved': (21, 31)}),
@@ -47,7 +47,7 @@ PCODE = {
DSOp.DS_MIN_RTN_F32: 'tmp = MEM[ADDR].f32;\nsrc = DATA.f32;\nMEM[ADDR].f32 = src < tmp ? src : tmp;\nRETURN_DATA.f32 = tmp',
DSOp.DS_MAX_RTN_F32: 'tmp = MEM[ADDR].f32;\nsrc = DATA.f32;\nMEM[ADDR].f32 = src > tmp ? src : tmp;\nRETURN_DATA.f32 = tmp',
DSOp.DS_WRAP_RTN_B32: 'tmp = MEM[ADDR].u32;\nMEM[ADDR].u32 = tmp >= DATA.u32 ? tmp - DATA.u32 : tmp + DATA2.u32;\nRETURN_DATA = tmp',
DSOp.DS_SWIZZLE_B32: 'offset = offset1:offset0;\nif (offset >= 0xe000) {\n// FFT decomposition\nmask = offset[4:0];\nfor (i = 0; i < 64; i++) {\nj = reverse_bits(i & 0x1f);\nj = (j >> count_ones(mask));\nj |= (i & mask);\nj |= i & 0x20;\nthread_out[i] = thread_valid[j] ? thread_in[j] : 0;\n}',
DSOp.DS_SWIZZLE_B32: 'offset = offset1:offset0;\nif (offset >= 0xe000) {\n// FFT decomposition\nmask = offset[4:0];\nfor (i = 0; i < 64; i++) {\nj = reverse_bits(i & 0x1f);\nj = (j >> count_ones(mask));\nj |= (i & mask);\nj |= i & 0x20;\nthread_out[i] = thread_valid[j] ? thread_in[j] : 0;\n}\n} elsif (offset >= 0xc000) {\n// rotate\nrotate = offset[9:5];\nmask = offset[4:0];\nif (offset[10]) {\nrotate = -rotate;\n}\nfor (i = 0; i < 64; i++) {\nj = (i & mask) | ((i + rotate) & ~mask);\nj |= i & 0x20;\nthread_out[i] = thread_valid[j] ? thread_in[j] : 0;\n}\n} elsif (offset[15]) {\n// full data sharing within 4 consecutive threads\nfor (i = 0; i < 64; i+=4) {\nthread_out[i+0] = thread_valid[i+offset[1:0]]?thread_in[i+offset[1:0]]:0;\nthread_out[i+1] = thread_valid[i+offset[3:2]]?thread_in[i+offset[3:2]]:0;\nthread_out[i+2] = thread_valid[i+offset[5:4]]?thread_in[i+offset[5:4]]:0;\nthread_out[i+3] = thread_valid[i+offset[7:6]]?thread_in[i+offset[7:6]]:0;\n}\n} else { // offset[15] == 0\n// limited data sharing within 32 consecutive threads\nxor_mask = offset[14:10];\nor_mask = offset[9:5];\nand_mask = offset[4:0];\nfor (i = 0; i < 64; i++) {\nj = (((i & 0x1f) & and_mask) | or_mask) ^ xor_mask;\nj |= (i & 0x20); // which group of 32\nthread_out[i] = thread_valid[j] ? thread_in[j] : 0;\n}\n}',
DSOp.DS_LOAD_B32: 'RETURN_DATA[31 : 0] = MEM[ADDR + OFFSET.u32].b32',
DSOp.DS_LOAD_2ADDR_B32: 'RETURN_DATA[31 : 0] = MEM[ADDR + OFFSET0.u32 * 4U].b32;\nRETURN_DATA[63 : 32] = MEM[ADDR + OFFSET1.u32 * 4U].b32',
DSOp.DS_LOAD_2ADDR_STRIDE64_B32: 'RETURN_DATA[31 : 0] = MEM[ADDR + OFFSET0.u32 * 256U].b32;\nRETURN_DATA[63 : 32] = MEM[ADDR + OFFSET1.u32 * 256U].b32',
@@ -44,7 +44,7 @@ PCODE = {
DSOp.DS_CMPSTORE_RTN_B32: 'addr = CalcDsAddr(vgpr_a.b32, offset.b32);\ntmp = MEM[addr].b32;\nsrc = DATA.b32;\ncmp = DATA2.b32;\nMEM[addr].b32 = tmp == cmp ? src : tmp;\nRETURN_DATA.b32 = tmp',
DSOp.DS_MIN_NUM_RTN_F32: "tmp = MEM[ADDR].f32;\nsrc = DATA.f32;\nif (isNAN(64'F(src.f32)) && isNAN(64'F(tmp.f32))) then\nMEM[ADDR].f32 = 32'F(cvtToQuietNAN(64'F(src.f32)))\nelsif isNAN(64'F(src.f32)) then\nMEM[ADDR].f32 = tmp.f32\nelsif isNAN(64'F(tmp.f32)) then\nMEM[ADDR].f32 = src.f32\nelsif ((src.f32 < tmp.f32) || ((abs(src.f32) == 0.0F) && (abs(tmp.f32) == 0.0F) && sign(src.f32) &&\n!sign(tmp.f32))) then\n// NOTE: -0<+0 is TRUE in this comparison\nMEM[ADDR].f32 = src.f32\nelse\nMEM[ADDR].f32 = tmp.f32\nendif;\nRETURN_DATA.f32 = tmp",
DSOp.DS_MAX_NUM_RTN_F32: "tmp = MEM[ADDR].f32;\nsrc = DATA.f32;\nif (isNAN(64'F(src.f32)) && isNAN(64'F(tmp.f32))) then\nMEM[ADDR].f32 = 32'F(cvtToQuietNAN(64'F(src.f32)))\nelsif isNAN(64'F(src.f32)) then\nMEM[ADDR].f32 = tmp.f32\nelsif isNAN(64'F(tmp.f32)) then\nMEM[ADDR].f32 = src.f32\nelsif ((src.f32 > tmp.f32) || ((abs(src.f32) == 0.0F) && (abs(tmp.f32) == 0.0F) && !sign(src.f32) &&\nsign(tmp.f32))) then\n// NOTE: +0>-0 is TRUE in this comparison\nMEM[ADDR].f32 = src.f32\nelse\nMEM[ADDR].f32 = tmp.f32\nendif;\nRETURN_DATA.f32 = tmp",
DSOp.DS_SWIZZLE_B32: 'offset = offset1:offset0;\nif (offset >= 0xe000) {\n// FFT decomposition\nmask = offset[4:0];\nfor (i = 0; i < 64; i++) {\nj = reverse_bits(i & 0x1f);\nj = (j >> count_ones(mask));\nj |= (i & mask);\nj |= i & 0x20;\nthread_out[i] = thread_valid[j] ? thread_in[j] : 0;\n}',
DSOp.DS_SWIZZLE_B32: 'offset = offset1:offset0;\nif (offset >= 0xe000) {\n// FFT decomposition\nmask = offset[4:0];\nfor (i = 0; i < 64; i++) {\nj = reverse_bits(i & 0x1f);\nj = (j >> count_ones(mask));\nj |= (i & mask);\nj |= i & 0x20;\nthread_out[i] = thread_valid[j] ? thread_in[j] : 0;\n}\n} elsif (offset >= 0xc000) {\n// rotate\nrotate = offset[9:5];\nmask = offset[4:0];\nif (offset[10]) {\nrotate = -rotate;\n}\nfor (i = 0; i < 64; i++) {\nj = (i & mask) | ((i + rotate) & ~mask);\nj |= i & 0x20;\nthread_out[i] = thread_valid[j] ? thread_in[j] : 0;\n}\n} elsif (offset[15]) {\n// full data sharing within 4 consecutive threads\nfor (i = 0; i < 64; i+=4) {\nthread_out[i+0] = thread_valid[i+offset[1:0]]?thread_in[i+offset[1:0]]:0;\nthread_out[i+1] = thread_valid[i+offset[3:2]]?thread_in[i+offset[3:2]]:0;\nthread_out[i+2] = thread_valid[i+offset[5:4]]?thread_in[i+offset[5:4]]:0;\nthread_out[i+3] = thread_valid[i+offset[7:6]]?thread_in[i+offset[7:6]]:0;\n}\n} else { // offset[15] == 0\n// limited data sharing within 32 consecutive threads\nxor_mask = offset[14:10];\nor_mask = offset[9:5];\nand_mask = offset[4:0];\nfor (i = 0; i < 64; i++) {\nj = (((i & 0x1f) & and_mask) | or_mask) ^ xor_mask;\nj |= (i & 0x20); // which group of 32\nthread_out[i] = thread_valid[j] ? thread_in[j] : 0;\n}\n}',
DSOp.DS_LOAD_B32: 'addr = CalcDsAddr(vgpr_a.b32, 0x0);\nRETURN_DATA[31 : 0] = MEM[addr + OFFSET.u32].b32',
DSOp.DS_LOAD_2ADDR_B32: 'addr = CalcDsAddr(vgpr_a.b32, 0x0);\nRETURN_DATA[31 : 0] = MEM[addr + OFFSET0.u32 * 4U].b32;\naddr = CalcDsAddr(vgpr_a.b32, 0x0);\nRETURN_DATA[63 : 32] = MEM[addr + OFFSET1.u32 * 4U].b32',
DSOp.DS_LOAD_2ADDR_STRIDE64_B32: 'addr = CalcDsAddr(vgpr_a.b32, 0x0);\nRETURN_DATA[31 : 0] = MEM[addr + OFFSET0.u32 * 256U].b32;\naddr = CalcDsAddr(vgpr_a.b32, 0x0);\nRETURN_DATA[63 : 32] = MEM[addr + OFFSET1.u32 * 256U].b32',
+46 -27
View File
@@ -538,7 +538,7 @@ class AMDCopyQueue(HWQueue):
tail_blit_dword += cmdsz
# Force align of submits to hit our usb layer write cache.
if (rem_packet_cnt := len(cmds) - tail_blit_dword) > 0 and dev.is_usb(): tail_blit_dword = 0
if (rem_packet_cnt := len(cmds) - tail_blit_dword) > 0 and dev.is_usb(): tail_blit_dword, rem_packet_cnt = 0, len(cmds)
# USB devices run in single-step mode, so they can't overrun the queue.
total_bytes = (tail_blit_dword * 4 if rem_packet_cnt == 0 else -sdma_queue.put_value % sdma_queue.ring.nbytes) + rem_packet_cnt * 4
@@ -661,48 +661,67 @@ class AMDAllocator(HCQAllocator['AMDDevice']):
dev, usb, ts, sdma = self.dev, self.dev.iface.pci_dev.usb, self.dev.timeline_signal, self.dev.sdma
CHUNK, src_mv = 0x40000 - 4, src.cast('B') # payload per chunk: the 256KB window minus the 4B trailing sentinel
nchunks = ceildiv(src.nbytes, CHUNK)
FENCE = 0xA800 # drain fence: the GPU writes it via sys_buf (PCIe 0x820800), the host reads it here (xdata)
if not hasattr(self, '_usb_seq'): # one-time: clear the fence and zero both windows so garbage can't match a sentinel
self._usb_seq, self._usb_stage = 0, [alloc_cbuffer(0x40000) for _ in range(2)] # (backing array, memoryview) pairs
if nchunks == 0: return
# Keep each submit below half of the 1 MiB SDMA ring, leaving room for wrap padding.
if src.nbytes > (batch := 1 << 30):
for off in range(0, src.nbytes, batch): self._copyin(dest.offset(off), src_mv[off:off+batch])
return
FENCE = 0xA800 # byte fences at FENCE and FENCE+8; GPU sys_buf offsets 0x800 and 0x808
if not hasattr(self, '_usb_seq'):
self._usb_seq, self._usb_clear, self._usb_stage = 0, 0, [alloc_cbuffer(0x40000) for _ in range(2)]
self._usb_wins = (self.b[0].offset(0, 0x40000), self.b[0].offset(0x40000, 0x40000)) # two windows, engine slots 0/16
usb.write(FENCE, bytes(8))
for bi in range(2): usb.scsi_write(bytes(0x40000), slot_start=bi * 16)
usb.write(FENCE, bytes(16))
def wait_drain(count): # spin until the drain fence reaches count, i.e. chunks 0..count-1 are fully in VRAM
def wait_fence(count, addr=FENCE, next_ok=False, current=None):
expected = (count & 0xff, (count + int(next_ok)) & 0xff)
t0 = time.perf_counter()
while int.from_bytes(usb.read(FENCE, 8), 'little') < count:
if time.perf_counter() - t0 > 10: raise RuntimeError(f"GPU failed to drain USB copyin chunk {count - 1} (10s, hung GPU?)")
while (current if current is not None else usb.read(addr, 1)[0]) not in expected:
current = None
if time.perf_counter() - t0 > 10: raise RuntimeError(f"GPU failed to reach USB copyin fence {addr:#x} value {count} (10s, hung GPU?)")
# build the whole ring upfront: per chunk, poll the sentinel, copy SRAM->VRAM, bump the fence; then one doorbell
POLL_EQ = sdma.SDMA_OP_POLL_REGMEM | sdma.SDMA_PKT_POLL_REGMEM_HEADER_FUNC(3) | sdma.SDMA_PKT_POLL_REGMEM_HEADER_MEM_POLL(1)
POLL_DW5 = sdma.SDMA_PKT_POLL_REGMEM_DW5_INTERVAL(0x04) | sdma.SDMA_PKT_POLL_REGMEM_DW5_RETRY_COUNT(0xfff)
q = dev.hw_copy_queue_t().wait(ts, dev.timeline_value - 1)
for c in range(nchunks):
seq, size = self._usb_seq + c, min(CHUNK, src.nbytes - c * CHUNK)
q.q(POLL_EQ, *data64_le(self._usb_wins[seq & 1].va_addr + round_up(size + 4, 512) - 4), 0x51000000 | (seq & 0xFFFFFF), 0xFFFFFFFF, POLL_DW5)
q.copy(dest.offset(c * CHUNK), self._usb_wins[seq & 1], size)
q.write(dev.iface.sys_buf.offset(0x800, 8), seq + 1, b64=True)
q.signal(ts, dev.next_timeline()).submit(dev)
def sentinel(c):
size = min(CHUNK, src.nbytes - c * CHUNK)
return self._usb_wins[(self._usb_seq + c) & 1].offset(round_up(size + 4, 512) - 4, 4)
# stream the chunks: stage the wire image [payload][sentinel], arm the window, send. A window is reusable once
# its previous occupant (seq-2) is both fully sent (tag reaped) and fully drained to VRAM (the fence).
inflight = [None, None]
q = dev.hw_copy_queue_t().wait(ts, dev.timeline_value - 1)
# A short chunk's sentinel can lie in old payload (including copyout data). Clear it before USB can fill the window.
for c in range(min(2, nchunks)): q.write(sentinel(c), 0)
# Toggle per submit, not per chunk: a 256-chunk transfer must not leave the next clear fence already satisfied.
self._usb_clear ^= 1
q.write(dev.iface.sys_buf.offset(0x808, 4), self._usb_clear)
for c in range(nchunks):
seq, size = self._usb_seq + c, min(CHUNK, src.nbytes - c * CHUNK)
if inflight[seq & 1] is not None: usb.usb.bulk_wait(inflight[seq & 1])
q.q(POLL_EQ, *data64_le(sentinel(c).va_addr), 0x51000000 | (seq & 0xFFFFFF), 0xFFFFFFFF, POLL_DW5)
q.copy(dest.offset(c * CHUNK), self._usb_wins[seq & 1], size)
# Clear the next occupant's sentinel only after this payload is copied, and before publishing the drain fence.
if c + 2 < nchunks: q.write(sentinel(c + 2), 0)
q.write(dev.iface.sys_buf.offset(0x800, 4), (seq + 1) & 0xff)
q.signal(ts, dev.next_timeline()).submit(dev)
wait_fence(self._usb_clear, FENCE + 8)
# Stage the next window while USB sends the previous one. F2 configures a single engine shared by both windows.
inflight = None
for c in range(nchunks):
seq, size = self._usb_seq + c, min(CHUNK, src.nbytes - c * CHUNK)
# Reading the drain fence does not reconfigure F2, so overlap it with USB sending the previous chunk and CPU staging.
rd_tag, rd_mv = usb.usb.control_read_async(0xE4, 1, value=FENCE)
buf = self._usb_stage[seq & 1][1]
buf[:size] = src_mv[c * CHUNK : c * CHUNK + size]
wire = round_up(size + 4, 512) # payload plus the sentinel, padded to 512B sectors (full window for max chunks)
struct.pack_into('<I', buf, wire - 4, 0x51000000 | (seq & 0xFFFFFF)) # the sentinel is the last dword of the wire
arm_tag = usb.usb.control_write_async(0xF2, wire // 512, (seq & 1) * 16 | (ceildiv(wire, 0x4000) << 8)) # wValue=sectors, wIndex=slot|count
rd_tag, rd_mv = usb.usb.control_read_async(0xE4, 8, value=FENCE) # arm and fence read fly in one round-trip window
usb.usb.bulk_wait(arm_tag)
# Rearming F2 recycles the slots immediately, before bulk data arrives. Drain seq-2 before the arm itself.
usb.usb.bulk_wait(rd_tag)
if int.from_bytes(rd_mv, 'little') < seq - 1: wait_drain(seq - 1) # rare: the drain lagged; spin on fresh reads
inflight[seq & 1] = usb.usb.bulk_write_async(buf[:wire])
for tag in inflight: usb.usb.bulk_wait(tag)
# One-byte reads cannot tear. At most two chunks are outstanding, so modulo-256 equality is unambiguous.
wait_fence(seq - 1, next_ok=True, current=rd_mv[0])
if inflight is not None: usb.usb.bulk_wait(inflight)
usb.usb.control_write(0xF2, wire // 512, (seq & 1) * 16 | (ceildiv(wire, 0x4000) << 8)) # wValue=sectors, wIndex=slot|count
inflight = usb.usb.bulk_write_async(buf[:wire])
if inflight is not None: usb.usb.bulk_wait(inflight)
self._usb_seq += nchunks
wait_drain(self._usb_seq) # copyin is synchronous: everything must be in VRAM before returning
wait_fence(self._usb_seq) # copyin is synchronous: everything must be in VRAM before returning
def _copyout(self, dest:memoryview, src:HCQBuffer):
if not self.dev.is_usb(): return super()._copyout(dest, src)
+1 -1
View File
@@ -87,7 +87,7 @@ class CPUAllocator(HCQAllocator['CPUDevice']):
ctypes.memmove(int(dest.va_addr), from_mv(src), len(src))
def _copyout(self, dest:memoryview, src:HCQBuffer):
self.dev.synchronize()
ctypes.memmove(from_mv(dest), int(src.va_addr), len(dest))
dest[:] = self._as_buffer(src)[:len(dest)]
def _do_map(self, buf:HCQBuffer):
if buf.view is None or not isinstance(buf.view, MMIOInterface): raise RuntimeError("Cannot map buffer without view to cpu")
return HCQBuffer(buf.view.addr, buf.size, view=buf.view, owner=buf.owner)
+300 -300
View File
@@ -1,13 +1,16 @@
from __future__ import annotations
import os, ctypes, contextlib, re, functools, mmap, struct, array, sys, weakref
import os, ctypes, contextlib, re, functools, mmap, struct, array, sys, itertools
assert sys.platform != 'win32'
from typing import cast
from typing import Any
from dataclasses import dataclass
from tinygrad.runtime.support.hcq import HCQCompiled, HCQAllocator, HCQBuffer, HWQueue, CLikeArgsState, HCQProgram, HCQSignal, BumpAllocator
from tinygrad.runtime.support.hcq import MMIOInterface, FileIOInterface, hcq_filter_visible_devices, hcq_profile
from tinygrad.uop.ops import sint
from tinygrad.device import Compiled, BufferSpec, TinyELF
from tinygrad.helpers import getenv, mv_address, round_up, data64, data64_le, prod, OSX, hi32, lo32, PROFILE, ContextVar, VIZ, ProfileEvent
from tinygrad.runtime.support.hcq2 import HCQ2Compiled, HCQAllocator, HWQueue, encode_submit, patch, to_name, unwrap_view
from tinygrad.runtime.support.hcq import HCQBuffer, MMIOInterface, FileIOInterface, BumpAllocator, hcq_filter_visible_devices
from tinygrad.uop.ops import Ops, UOp, UPat, PatternMatcher
from tinygrad.engine.realize import get_call_arg_uops, get_call_var_uops
from tinygrad.device import Buffer, BufferSpec, Compiled, Device, TinyELF
from tinygrad.dtype import dtypes, DType
from tinygrad.helpers import getenv, mv_address, round_up, data64, data64_le, prod, OSX, PROFILE, ContextVar, VIZ
from tinygrad.helpers import ProfileEvent
from tinygrad.renderer.ptx import PTXRenderer
from tinygrad.renderer.cstyle import CUDARenderer, NVCCRenderer
from tinygrad.runtime.autogen import nv_570, nv_580, nv_610, mesa
@@ -24,10 +27,7 @@ PMA = ContextVar("PMA", abs(VIZ.value)>=2)
@dataclass(frozen=True)
class ProfilePMAEvent(ProfileEvent): device:str; kern:str; blob:bytes; exec_tag:int; profile_key:bytes|None=None # noqa: E702
class NVSignal(HCQSignal):
def _sleep(self, time_spent_since_last_sleep_ms:int):
# Reasonable to sleep for long workloads (which take more than 200ms) and only timeline signals.
if time_spent_since_last_sleep_ms > 200 and self.owner is not None: self.owner.iface.sleep(200)
def hilo(addr:UOp) -> tuple[UOp, UOp]: return (addr >> 32).cast(dtypes.uint32), addr.cast(dtypes.uint32)
def get_error_str(status): return f"{status}: {nv_gpu.nv_status_codes.get(status, 'Unknown error')}"
@@ -41,10 +41,13 @@ def nv_iowr(fd:FileIOInterface, nr, args, cmd=None):
ret = fd.ioctl(cmd or ((3 << 30) | (ctypes.sizeof(args) & 0x1FFF) << 16 | (ord('F') & 0xFF) << 8 | (nr & 0xFF)), args)
if ret != 0: raise RuntimeError(f"ioctl returned {ret}")
def nvm(subc:int, mthd:int, *vals, typ=2) -> list:
return [(typ << 28) | (sum(v.dtype.itemsize // 4 if isinstance(v, UOp) else 1 for v in vals) << 16) | (subc << 13) | (mthd >> 2), *vals]
class QMD:
fields: dict[str, dict[str, tuple[int, int]]] = {}
def __init__(self, dev:NVDevice, view:MMIOInterface|None=None, **kwargs):
def __init__(self, dev:NVDevice, blob:bytearray|None=None):
self.ver, self.sz = (5, 0x60) if dev.iface.compute_class >= nv_gpu.BLACKWELL_COMPUTE_A else (3, 0x40)
# Init fields from module
@@ -52,272 +55,240 @@ class QMD:
QMD.fields[pref] = {**{name[len(pref)+1:]: dt for name,dt in nv_gpu.__dict__.items() if name.startswith(pref) and isinstance(dt, tuple)},
**{name[len(pref)+1:]+f"_{i}": dt(i) for name,dt in nv_gpu.__dict__.items() for i in range(8) if name.startswith(pref) and callable(dt)}}
self.mv, self.pref = (memoryview(bytearray(self.sz * 4)) if view is None else view), pref
if kwargs: self.write(**kwargs)
self.mv, self.pref = (bytearray(self.sz * 4) if blob is None else blob), pref
self.patches:dict[int, UOp] = {}
def _rw_bits(self, hi:int, lo:int, value:int|None=None):
mask = ((1 << (width:=hi - lo + 1)) - 1) << (lo % 8)
num = int.from_bytes(self.mv[lo//8:hi//8+1], "little")
def read(self, k:str) -> int:
hi, lo = QMD.fields[self.pref][k.upper()]
return (int.from_bytes(self.mv[lo//8:hi//8+1], "little") >> (lo % 8)) & ((1 << (hi - lo + 1)) - 1)
if value is None: return (num & mask) >> (lo % 8)
def write(self, **kwargs:int|UOp):
for k, v in kwargs.items():
hi, lo = QMD.fields[self.pref][k.upper()]
if isinstance(v, UOp):
assert lo % 8 == 0, f"{k} is not byte aligned"
self.patches[lo // 8] = v.ccast(next(t for t in (dtypes.uint64, dtypes.uint32, dtypes.uint16, dtypes.uint8) if t.itemsize * 8 <= hi - lo + 1))
else:
if v >> (hi - lo + 1): raise ValueError(f"{k}={v:#x} does not fit")
mask, num = ((1 << (hi - lo + 1)) - 1) << (lo % 8), int.from_bytes(self.mv[lo//8:hi//8+1], "little")
self.mv[lo//8:hi//8+1] = ((num & ~mask) | (v << (lo % 8))).to_bytes(hi//8 - lo//8 + 1, "little")
if value >= (1 << width): raise ValueError(f"{value:#x} does not fit.")
self.mv[lo//8:hi//8+1] = int((num & ~mask) | ((value << (lo % 8)) & mask)).to_bytes((hi//8 - lo//8 + 1), "little")
def set_addr(self, name:str, addr:UOp, sfx:str=""): self.write(**{f"{name}_lower{sfx}": addr, f"{name}_upper{sfx}": addr >> 32})
def set_constant_buf_addr(self, i:int, addr:UOp):
self.set_addr("constant_buffer_addr", addr >> (6 if self.ver >= 4 else 0), f"_shifted6_{i}" if self.ver >= 4 else f"_{i}")
def set_program_addr(self, addr:UOp):
self.set_addr("program_address", addr >> (4 if self.ver >= 4 else 0), "_shifted4" if self.ver >= 4 else "")
self.set_addr("program_prefetch_addr", addr >> 8, "_shifted")
def set_release(self, addr:UOp, payload:UOp, timestamp:bool=False) -> bool:
if (i:=next((i for i in range(2) if not self.read(f"release{i}_enable")), None)) is None: return False
self.set_addr(f"release_semaphore{i}_addr" if self.ver >= 4 else f"release{i}_address", addr)
self.set_addr(f"release_semaphore{i}_payload" if self.ver >= 4 else f"release{i}_payload", payload)
self.write(**{f"release{i}_enable": 1, f"release_structure_size_{i}" if self.ver >= 4 else f"release{i}_structure_size": 0 if timestamp else 2},
**({} if self.ver >= 4 else {f"release{i}_payload64b": 1}))
return True
@property
def grid(self) -> tuple[str, ...]:
return ("grid_width", "grid_height", "grid_depth") if self.ver >= 4 else ("cta_raster_width", "cta_raster_height", "cta_raster_depth")
def write(self, **kwargs):
for k,val in kwargs.items(): self._rw_bits(*QMD.fields[self.pref][k.upper()], value=val) # type: ignore [misc]
# *****************
# queues
def read(self, k, val=0): return self._rw_bits(*QMD.fields[self.pref][k.upper()])
class NVQueue(HWQueue):
dev:NVDevice
q_rewrite = PatternMatcher([
(UPat(Ops.INS, arg=("wait", dtypes.void), src=(UPat(name="dst"), UPat(name="val"))), lambda ctx, dst, val: ctx.wait(dst, val)),
(UPat(Ops.INS, arg=("timestamp", dtypes.void), src=(UPat(name="dst"),)), lambda ctx, dst: ctx.timestamp(dst)),
(UPat(Ops.INS, arg=("store", dtypes.void), src=(UPat(name="dst"), UPat(name="val"))), lambda ctx, dst, val: ctx.signal(dst, val)),
])
def field_offset(self, k): return QMD.fields[self.pref][k.upper()][1] // 8
def nvm(self, subc:int, mthd:int, *vals, typ=2): self.q(*nvm(subc, mthd, *vals, typ=typ))
def set_constant_buf_addr(self, i, addr):
if self.ver < 4: self.write(**{f'constant_buffer_addr_upper_{i}':hi32(addr), f'constant_buffer_addr_lower_{i}':lo32(addr)})
else: self.write(**{f'constant_buffer_addr_upper_shifted6_{i}':hi32(addr >> 6), f'constant_buffer_addr_lower_shifted6_{i}':lo32(addr >> 6)})
def sem(self, addr:UOp, value:UOp, **flags:str):
self.nvm(0, nv_gpu.NVC56F_SEM_ADDR_LO, addr, value.ccast(dtypes.uint64), nv_flags("NVC56F_SEM_EXECUTE", payload_size="64bit", **flags))
class NVCommandQueue(HWQueue[HCQSignal, 'NVDevice', 'NVProgram', 'NVArgsState']):
def __init__(self):
self.active_qmd = None
super().__init__()
def wait(self, signal:UOp, value:UOp): self.sem(signal.getaddr(self.devs), value, operation="acq_circ_geq")
def signal(self, signal:UOp, value:UOp): self.release(signal, value)
def timestamp(self, signal:UOp): self.release(signal, UOp.const(0, dtypes.uint64), timestamp=True)
def release(self, signal:UOp, value:UOp, timestamp:bool=False):
self.sem(signal.getaddr(self.devs), value, operation="release", release_wfi="en", release_timestamp="en" if timestamp else "dis")
if not timestamp: self.nvm(0, nv_gpu.NVC56F_NON_STALL_INTERRUPT, 0x0)
def __del__(self):
if self.binded_device is not None: self.binded_device.allocator.free(self.hw_page, self.hw_page.size, BufferSpec(cpu_access=True, nolru=True))
def submit(self, cmdbuf:UOp) -> UOp:
fifo, ib, off = self.dev.fifos[self.queue], *unwrap_view(cmdbuf)
def nvm(self, subchannel, mthd, *args, typ=2): self.q((typ << 28) | (len(args) << 16) | (subchannel << 13) | (mthd >> 2), *args)
ring, gpput, doorbell, put, gpentry = [UOp.placeholder((sz,), dt, device=self.devs, volatile=True, tag=to_name(nm, self.queue))
for nm, dt, sz in (("ring", dtypes.uint64, fifo.entries), ("gpput", dtypes.uint32, 1), ("doorbell", dtypes.uint32, 1),
("put_value", dtypes.uint64, 1), ("gpentry", dtypes.uint64, 1))]
gpentry = patch(gpentry, [(0, ib.getaddr(self.devs) + UOp.const(off | (cmdbuf.max_numel() // 4 << 42) | (1 << 41), dtypes.uint64))])
def setup(self, compute_class=None, copy_class=None, local_mem_window=None, shared_mem_window=None, local_mem=None, local_mem_tpc_bytes=None):
if compute_class: self.nvm(1, nv_gpu.NVC6C0_SET_OBJECT, compute_class)
if copy_class: self.nvm(4, nv_gpu.NVC6C0_SET_OBJECT, copy_class)
if local_mem_window: self.nvm(1, nv_gpu.NVC6C0_SET_SHADER_LOCAL_MEMORY_WINDOW_A, *data64(local_mem_window))
if shared_mem_window: self.nvm(1, nv_gpu.NVC6C0_SET_SHADER_SHARED_MEMORY_WINDOW_A, *data64(shared_mem_window))
if local_mem: self.nvm(1, nv_gpu.NVC6C0_SET_SHADER_LOCAL_MEMORY_A, *data64(local_mem))
if local_mem_tpc_bytes: self.nvm(1, nv_gpu.NVC6C0_SET_SHADER_LOCAL_MEMORY_NON_THROTTLED_A, *data64(local_mem_tpc_bytes), 0xff)
return self
p = put.index(0).load()
written = UOp.barrier(ring.after(cmdbuf).index((p % fifo.entries).cast(dtypes.int)).store(gpentry.index(0).load()), put.index(0).store(p + 1))
queued = UOp.barrier(gpput.after(written).index(0).store(((p + 1) % fifo.entries).cast(dtypes.uint32)))
return doorbell.after(queued).index(0).store(UOp.const(fifo.token, dtypes.uint32))
def wait(self, signal:HCQSignal, value:sint=0):
self.nvm(0, nv_gpu.NVC56F_SEM_ADDR_LO, *data64_le(signal.value_addr), *data64_le(value),
nv_flags("NVC56F_SEM_EXECUTE", operation="acq_circ_geq", payload_size="64bit"))
self.active_qmd = None
return self
class NVComputeQueue(NVQueue):
q_rewrite = PatternMatcher([
(UPat(Ops.CALL, src=(UPat(Ops.PROGRAM, name="prg"),), name="call", allow_any_len=True), lambda ctx, call, prg: ctx.exec(call, prg)),
(UPat(Ops.INS, arg=("barrier", dtypes.void)), lambda ctx: ctx.memory_barrier()),
]) + NVQueue.q_rewrite
def timestamp(self, signal:HCQSignal): return self.signal(signal, 0)
def __init__(self, ctx, submit):
super().__init__(ctx, submit)
def bind(self, dev:NVDevice):
self.binded_device = dev
self.hw_page = dev.allocator.alloc(len(self._q) * 4, BufferSpec(cpu_access=True, nolru=True))
hw_view = self.hw_page.cpu_view().view(fmt='I')
for i, value in enumerate(self._q): hw_view[i] = value
progs = [nv_build_program(self.dev, u.src[0], self.devs)[0] for u in self.lin.src if u.op is Ops.CALL]
self.qmd_sz = round_up(QMD(self.dev).sz * 4, 256)
self.stride = self.qmd_sz + max([p.kernargs_size for p in progs], default=0)
self.qmd_buf = UOp.placeholder((len(progs) * self.stride,), dtypes.uint8, device=self.devs, tag=to_name("qmd", self.queue))
self.qmds:list[QMD] = []
self.prev_qmd:QMD|None = None # the launch the next one chains onto
# From now on, the queue is on the device for faster submission.
self._q = hw_view
def wait(self, signal:UOp, value:UOp):
self.prev_qmd = None
super().wait(signal, value)
def _submit_to_gpfifo(self, dev:NVDevice, gpfifo:GPFifo):
if dev == self.binded_device: cmdq_addr = self.hw_page.va_addr
else:
cmdq_addr = dev.cmdq_allocator.alloc(len(self._q) * 4, 16)
cmdq_wptr = (cmdq_addr - dev.cmdq_page.va_addr) // 4
dev.cmdq[cmdq_wptr : cmdq_wptr + len(self._q)] = array.array('I', self._q)
def release(self, signal:UOp, value:UOp, timestamp:bool=False):
if self.prev_qmd is None or not self.prev_qmd.set_release(signal.getaddr(self.devs), value, timestamp):
self.prev_qmd = None
super().release(signal, value, timestamp)
gpfifo.ring[gpfifo.put_value % gpfifo.entries_count] = (cmdq_addr//4 << 2) | (len(self._q) << 42) | (1 << 41)
gpfifo.gpput[0] = (gpfifo.put_value + 1) % gpfifo.entries_count
def submit(self, cmdbuf:UOp) -> UOp:
if self.qmds:
patches = [(i * self.stride + off, w) for i, q in enumerate(self.qmds) for off, w in q.patches.items()]
cmdbuf = cmdbuf.after(patch(self.qmd_buf, patches, b"".join(q.mv for q in self.qmds)))
return super().submit(cmdbuf)
System.memory_barrier()
dev.gpu_mmio[0x90 // 4] = gpfifo.token
gpfifo.put_value += 1
class NVComputeQueue(NVCommandQueue):
def memory_barrier(self):
self.prev_qmd = None
self.nvm(1, nv_gpu.NVC6C0_INVALIDATE_SHADER_CACHES_NO_WFI,
nv_flags("NVC6C0_INVALIDATE_SHADER_CACHES_NO_WFI", instruction="true", global_data="true", constant="true"))
self.active_qmd:QMD|None = None
return self
def exec(self, prg:NVProgram, args_state:NVArgsState, global_size:tuple[sint, ...], local_size:tuple[sint, ...]):
self.bind_args_state(args_state)
def exec(self, call:UOp, prg:UOp):
data, lib = nv_build_program(self.dev, prg, self.devs)
global_size, local_size = prg.arg.global_size, prg.arg.local_size
if prod(local_size) > 1024 or data.max_threads < prod(local_size):
raise RuntimeError(f"Too many resources requested for launch, {prod(local_size)=}, {data.max_threads=}")
if any(g > mx for g,mx in zip(global_size, [2147483647, 65535, 65535]) if isinstance(g, int)) or \
any(l > mx for l,mx in zip(local_size, [1024, 1024, 64])):
raise RuntimeError(f"Invalid global/local dims {global_size=}, {local_size=}")
qmd_buf = args_state.buf.offset(round_up(prg.constbufs[0][1], 1 << 8))
qmd_buf.cpu_view().view(size=prg.qmd.mv.nbytes, fmt='B')[:] = prg.qmd.mv
assert qmd_buf.va_addr < (1 << 40), f"large qmd addr {qmd_buf.va_addr:x}"
qmd_addr = self.qmd_buf.getaddr(self.devs) + UOp.const(len(self.qmds) * self.stride, dtypes.uint64)
qmd = QMD(self.dev, data.qmd.mv.ljust(self.stride, b"\0")) # the program's template, in a slot of its own
qmd.write(**dict(zip(qmd.grid, global_size)), **{f"cta_thread_dimension{j}": l for j, l in enumerate(local_size)})
qmd.set_program_addr(lib.getaddr(self.devs) + data.prog_off)
for j, (off, _) in data.constbufs.items():
qmd.set_constant_buf_addr(j, qmd_addr + UOp.const(self.qmd_sz, dtypes.uint64) if j == 0 else lib.getaddr(self.devs) + off)
bufs, vals = [get_call_arg_uops(call)[j] for j in prg.arg.globals], get_call_var_uops(call, prg)
qmd.mv[self.qmd_sz:(at:=self.qmd_sz + len(data.cbuf_0) * 4)] = array.array('I', data.cbuf_0).tobytes() # constant buffer 0: the driver params
qmd.patches |= {at + j * 8: b.getaddr(self.devs) for j, b in enumerate(bufs)} | {at + o: v.ccast(dt) for v, (o, dt) in zip(vals, data.vars)}
qmd = QMD(dev=prg.dev, view=qmd_buf.cpu_view()) # Save qmd for later update
if self.prev_qmd is None:
if self.dev.pma_enabled: self.nvm(1, nv_gpu.NVC6C0_PM_TRIGGER, 0)
self.nvm(1, nv_gpu.NVC6C0_SEND_PCAS_A, (qmd_addr >> 8).cast(dtypes.uint32))
self.nvm(1, nv_gpu.NVC6C0_SEND_SIGNALING_PCAS2_B, nv_gpu.NVC6C0_SEND_SIGNALING_PCAS2_B_PCAS_ACTION_PREFETCH_SCHEDULE)
else: self.prev_qmd.write(dependent_qmd0_pointer=qmd_addr >> 8, dependent_qmd0_action=1, dependent_qmd0_prefetch=1, dependent_qmd0_enable=1)
self.qmds.append(qmd)
self.prev_qmd = qmd
self.bind_sints_to_mem(*global_size, mem=qmd_buf.cpu_view(), fmt='I', offset=qmd.field_offset('cta_raster_width' if qmd.ver<4 else 'grid_width'))
self.bind_sints_to_mem(*(local_size[:2]), mem=qmd_buf.cpu_view(), fmt='H', offset=qmd.field_offset('cta_thread_dimension0'))
self.bind_sints_to_mem(local_size[2], mem=qmd_buf.cpu_view(), fmt='B', offset=qmd.field_offset('cta_thread_dimension2'))
qmd.set_constant_buf_addr(0, args_state.buf.va_addr)
class NVCopyQueue(NVQueue):
q_rewrite = PatternMatcher([
(UPat(Ops.CALL, src=(UPat(Ops.COPY),), name="call", allow_any_len=True), lambda ctx, call: ctx.copy(call)),
(UPat(Ops.INS, arg=("barrier", dtypes.void)), lambda ctx: ()),
]) + NVQueue.q_rewrite
if self.active_qmd is None:
if prg.dev.pma_enabled: self.nvm(1, nv_gpu.NVC6C0_PM_TRIGGER, 0)
self.nvm(1, nv_gpu.NVC6C0_SEND_PCAS_A, qmd_buf.va_addr >> 8)
self.nvm(1, nv_gpu.NVC6C0_SEND_SIGNALING_PCAS2_B, 9)
else:
self.active_qmd.write(dependent_qmd0_pointer=qmd_buf.va_addr >> 8, dependent_qmd0_action=1, dependent_qmd0_prefetch=1, dependent_qmd0_enable=1)
self.active_qmd, self.active_qmd_buf = qmd, qmd_buf
return self
def signal(self, signal:HCQSignal, value:sint=0):
if self.active_qmd is not None:
for i in range(2):
if self.active_qmd.read(f'release{i}_enable') == 0:
self.active_qmd.write(**{f'release{i}_enable': 1})
addr_off = self.active_qmd.field_offset(f'release{i}_address_lower' if self.active_qmd.ver<4 else f'release_semaphore{i}_addr_lower')
self.bind_sints_to_mem(signal.value_addr & 0xffffffff, mem=self.active_qmd_buf.cpu_view(), fmt='I', offset=addr_off)
self.bind_sints_to_mem(signal.value_addr >> 32, mem=self.active_qmd_buf.cpu_view(), fmt='I', mask=0xf, offset=addr_off+4)
val_off = self.active_qmd.field_offset(f'release{i}_payload_lower' if self.active_qmd.ver<4 else f'release_semaphore{i}_payload_lower')
self.bind_sints_to_mem(value & 0xffffffff, mem=self.active_qmd_buf.cpu_view(), fmt='I', offset=val_off)
self.bind_sints_to_mem(value >> 32, mem=self.active_qmd_buf.cpu_view(), fmt='I', offset=val_off+4)
return self
self.nvm(0, nv_gpu.NVC56F_SEM_ADDR_LO, *data64_le(signal.value_addr), *data64_le(value),
nv_flags("NVC56F_SEM_EXECUTE", operation="release", release_wfi="en", payload_size="64bit", release_timestamp="en"))
self.nvm(0, nv_gpu.NVC56F_NON_STALL_INTERRUPT, 0x0)
self.active_qmd = None
return self
def write(self, b:HCQBuffer, val:sint, b64:bool=False):
self.nvm(0, nv_gpu.NVC56F_SEM_ADDR_LO, *data64_le(b.va_addr), *data64_le(val),
nv_flags("NVC56F_SEM_EXECUTE", operation="release", release_wfi="en", payload_size="64bit" if b64 else "32bit"))
self.active_qmd = None
return self
def poll_bit(self, b:HCQBuffer, val:sint, mask:int):
self.nvm(0, nv_gpu.NVC56F_SEM_ADDR_LO, *data64_le(b.va_addr), *data64_le((~mask & 0xFFFFFFFF) if val == 0 else val),
nv_flags("NVC56F_SEM_EXECUTE", operation="acq_nor" if val == 0 else "acq_and", payload_size="32bit"))
self.active_qmd = None
return self
def _submit(self, dev:NVDevice): self._submit_to_gpfifo(dev, dev.compute_gpfifo)
class NVCopyQueue(NVCommandQueue):
def __init__(self, queue_idx=0):
self.queue_idx = queue_idx
super().__init__()
def copy(self, dest:HCQBuffer, src:HCQBuffer, copy_size:int):
for off in range(0, copy_size, step:=(1 << 31)):
self.nvm(4, nv_gpu.NVC6B5_OFFSET_IN_UPPER, *data64(src.va_addr+off), *data64(dest.va_addr+off))
self.nvm(4, nv_gpu.NVC6B5_LINE_LENGTH_IN, min(copy_size-off, step))
def copy(self, call:UOp):
dest, src = (a.getaddr(self.devs) for a in call.src[1:3])
for off in range(0, sz:=call.src[2].max_numel() * call.src[2].dtype.itemsize, step:=(1 << 31)):
self.nvm(4, nv_gpu.NVC6B5_OFFSET_IN_UPPER, *hilo(src + UOp.const(off, dtypes.uint64)), *hilo(dest + UOp.const(off, dtypes.uint64)))
self.nvm(4, nv_gpu.NVC6B5_LINE_LENGTH_IN, min(sz - off, step))
self.nvm(4, nv_gpu.NVC6B5_LAUNCH_DMA,
nv_flags("NVC6B5_LAUNCH_DMA", data_transfer_type="non_pipelined", src_memory_layout="pitch", dst_memory_layout="pitch"))
return self
def signal(self, signal:HCQSignal, value:sint=0):
self.nvm(4, nv_gpu.NVC6B5_SET_SEMAPHORE_A, *data64(signal.value_addr), value)
self.nvm(4, nv_gpu.NVC6B5_LAUNCH_DMA, nv_flags("NVC6B5_LAUNCH_DMA", flush_enable="true", semaphore_type="release_four_word_semaphore"))
return self
def semaphore(self, addr:UOp, value:UOp, typ:str): # a one word release writes just the payload, a four word one the timestamp after it
self.nvm(4, nv_gpu.NVC6B5_SET_SEMAPHORE_A, *hilo(addr), value.ccast(dtypes.uint32))
self.nvm(4, nv_gpu.NVC6B5_LAUNCH_DMA, nv_flags("NVC6B5_LAUNCH_DMA", flush_enable="true", semaphore_type=f"release_{typ}_word_semaphore"))
def timestamp(self, signal:UOp): self.semaphore(signal.getaddr(self.devs), UOp.const(0, dtypes.uint32), "four")
def signal(self, signal:UOp, value:UOp): self.semaphore(signal.getaddr(self.devs), value, "one")
def _submit(self, dev:NVDevice): self._submit_to_gpfifo(dev, dev.dma_gpfifo)
# *****************
# programs
class NVVideoQueue(NVCommandQueue):
def decode_hevc_chunk(self, pic_desc:HCQBuffer, in_buf:HCQBuffer, out_buf:HCQBuffer, out_buf_pos:int, hist_bufs:list[HCQBuffer], hist_pos:list[int],
chroma_off:int, coloc_buf:HCQBuffer, filter_buf:HCQBuffer, intra_top_off:int, intra_unk_off:int|None, status_buf:HCQBuffer):
self.nvm(4, nv_gpu.NVC9B0_SET_APPLICATION_ID, nv_gpu.NVC9B0_SET_APPLICATION_ID_ID_HEVC)
self.nvm(4, nv_gpu.NVC9B0_SET_CONTROL_PARAMS, nv_flags("NVC9B0_SET_CONTROL_PARAMS", codec_type="hevc", testrun_env="prod_run", gptimer_on=1,
err_conceal_on=1, mbtimer_on=1, event_trace_logging_on=1))
self.nvm(4, nv_gpu.NVC9B0_SET_DRV_PIC_SETUP_OFFSET, pic_desc.va_addr >> 8)
self.nvm(4, nv_gpu.NVC9B0_SET_IN_BUF_BASE_OFFSET, in_buf.va_addr >> 8)
for pos, buf in zip(hist_pos + [out_buf_pos], hist_bufs + [out_buf]):
self.nvm(4, nv_gpu.NVC9B0_SET_PICTURE_LUMA_OFFSET0 + pos*4, buf.va_addr >> 8)
self.nvm(4, nv_gpu.NVC9B0_SET_PICTURE_CHROMA_OFFSET0 + pos*4, buf.offset(chroma_off).va_addr >> 8)
self.nvm(4, nv_gpu.NVC9B0_SET_COLOC_DATA_OFFSET, coloc_buf.va_addr >> 8)
self.nvm(4, nv_gpu.NVC9B0_SET_NVDEC_STATUS_OFFSET, status_buf.va_addr >> 8)
self.nvm(4, nv_gpu.NVC9B0_HEVC_SET_TILE_SIZES_OFFSET, pic_desc.offset(0x200).va_addr >> 8)
self.nvm(4, nv_gpu.NVC9B0_HEVC_SET_FILTER_BUFFER_OFFSET, filter_buf.va_addr >> 8)
self.nvm(4, nv_gpu.NVC9B0_SET_INTRA_TOP_BUF_OFFSET, (filter_buf.va_addr + intra_top_off) >> 8)
if intra_unk_off is not None: self.nvm(4, 0x4dc, (filter_buf.va_addr + intra_unk_off) >> 8)
self.nvm(4, nv_gpu.NVC9B0_EXECUTE, 0)
return self
def signal(self, signal:HCQSignal, value:sint=0):
self.nvm(4, nv_gpu.NVC9B0_SEMAPHORE_A, *data64(signal.value_addr), value)
self.nvm(4, nv_gpu.NVC9B0_SEMAPHORE_D, nv_flags("NVC9B0_SEMAPHORE_D", structure_size="four", payload_size="64bit"))
return self
def _submit(self, dev:NVDevice): self._submit_to_gpfifo(dev, dev.vid_gpfifo)
class NVArgsState(CLikeArgsState):
def __init__(self, buf:HCQBuffer, prg:NVProgram, bufs:tuple[HCQBuffer, ...], vals:tuple[int, ...]=()):
if (is_mock:=isinstance(prg.dev.iface, MOCKIface)): prg.cbuf_0[80:82] = [len(bufs), len(vals)]
super().__init__(buf, prg, bufs, vals=() if is_mock else vals, prefix=prg.cbuf_0 or None)
# mock expects all vars to be 64 bit
if is_mock and vals: self.bind_sints_to_buf(*vals, buf=self.buf, fmt='q', offset=len(prg.cbuf_0)*4 + len(bufs)*8)
class NVProgram(HCQProgram['NVDevice']):
class NVProgramData:
def __init__(self, dev:NVDevice, obj:TinyELF):
self.dev, self.name, self.lib = dev, obj.name, obj.lib
self.constbufs: dict[int, tuple[int, int]] = {0: (0, 0x160)} # dict[constbuf index, tuple[va_addr, size]]
name, signature, mock = obj.name, obj.signature, isinstance(dev.iface, MOCKIface)
self.constbufs: dict[int, tuple[int, int]] = {0: (0, 0x160)} # dict[constbuf index, tuple[offset in the image, size]]
self.relocs: list[tuple[int, int, DType, int]] = [] # (byte offset in the image, symbol offset, width, shift) of the program's address
self.prog_off, self.cbuf_0, sections, relocs = 0, [], list[Any](), list[Any]()
image:bytes = obj.lib
if (NAK:=isinstance(dev.renderer, NAKRenderer)):
image, self.cbuf_0 = memoryview(bytearray(obj.lib[ctypes.sizeof(info:=mesa.struct_nak_shader_info.from_buffer_copy(obj.lib)):])), []
self.regs_usage, self.shmem_usage, self.lcmem_usage = info.num_gprs, round_up(info.cs.smem_size, 128), round_up(info.slm_size, 16)
elif isinstance(dev.iface, MOCKIface): image, sections, relocs = memoryview(bytearray(obj.lib) + b'\x00' * (4 - len(obj.lib)%4)).cast("I"), [], [] # type: ignore
else: image, sections, relocs = elf_loader(self.lib, force_section_align=128)
# NOTE: Ensure at least 4KB of space after the program to mitigate prefetch memory faults.
self.lib_gpu = self.dev.allocator.alloc(round_up((prog_sz:=image.nbytes), 0x1000) + 0x1000, buf_spec:=BufferSpec(nolru=True))
prog_addr = self.lib_gpu.va_addr
image = obj.lib[ctypes.sizeof(info:=mesa.struct_nak_shader_info.from_buffer_copy(obj.lib)):]
regs, shmem, lcmem = info.num_gprs, round_up(info.cs.smem_size, 128), round_up(info.slm_size, 16)
elif mock: image = obj.lib.ljust(round_up(len(obj.lib), 4), b'\x00') # for MOCKGPU the lib is PTX code, not an elf
else:
img, sections, relocs = elf_loader(obj.lib, force_section_align=128)
image = bytes(img)
prog_sz = len(image)
if not NAK:
# For MOCKGPU, the lib is PTX code, so some values are emulated.
self.regs_usage, self.shmem_usage, self.lcmem_usage, cbuf0_size = 0, 0x400, 0x240, 0x160 if isinstance(dev.iface, MOCKIface) else 0
for sh in sections: # pylint: disable=possibly-used-before-assignment
if sh.name == f".nv.shared.{self.name}": self.shmem_usage = round_up(0x400 + sh.header.sh_size, 128)
if sh.name == f".text.{self.name}": prog_addr, prog_sz = self.lib_gpu.va_addr+sh.header.sh_addr, sh.header.sh_size
elif m:=re.match(r'\.nv\.constant(\d+)', sh.name):
self.constbufs[int(m.group(1))] = (self.lib_gpu.va_addr+sh.header.sh_addr, sh.header.sh_size)
regs, shmem, lcmem, cbuf0_size = 0, 0x400, 0x240, 0x160 if mock else 0
for sh in sections:
if sh.name == f".nv.shared.{name}": shmem = round_up(0x400 + sh.header.sh_size, 128)
if sh.name == f".text.{name}": self.prog_off, prog_sz = sh.header.sh_addr, sh.header.sh_size
elif m:=re.match(r'\.nv\.constant(\d+)', sh.name): self.constbufs[int(m.group(1))] = (sh.header.sh_addr, sh.header.sh_size)
elif sh.name.startswith(".nv.info"):
for typ, param, data in self._parse_elf_info(sh):
if sh.name == f".nv.info.{obj.name}" and param == 0xa: cbuf0_size = struct.unpack_from("IH", data)[1] # EIATTR_PARAM_CBANK
elif sh.name == ".nv.info" and param == 0x12: self.lcmem_usage = struct.unpack_from("II", data)[1] + 0x240 # EIATTR_MIN_STACK_SIZE
elif sh.name == ".nv.info" and param == 0x2f: self.regs_usage = struct.unpack_from("II", data)[1] # EIATTR_REGCOUNT
elif sh.name == ".nv.info" and param == 0x12: lcmem = struct.unpack_from("II", data)[1] + 0x240 # EIATTR_MIN_STACK_SIZE
elif sh.name == ".nv.info" and param == 0x2f: regs = struct.unpack_from("II", data)[1] # EIATTR_REGCOUNT
# Apply relocs
for apply_image_offset, rel_sym_offset, typ, _ in relocs: # pylint: disable=possibly-used-before-assignment
# These types are CUDA-specific, applying them here
if typ == 2: image[apply_image_offset:apply_image_offset+8] = struct.pack('<Q', self.lib_gpu.va_addr + rel_sym_offset) # R_CUDA_64
elif typ == 0x38: image[apply_image_offset+4:apply_image_offset+8] = struct.pack('<I', (self.lib_gpu.va_addr + rel_sym_offset) & 0xffffffff)
elif typ == 0x39: image[apply_image_offset+4:apply_image_offset+8] = struct.pack('<I', (self.lib_gpu.va_addr + rel_sym_offset) >> 32)
# These reloc types are CUDA-specific: they all want the program's own address, which is only known once the linear links.
for apply_image_offset, rel_sym_offset, typ, _ in relocs:
if typ == 2: self.relocs.append((apply_image_offset, rel_sym_offset, dtypes.uint64, 0)) # R_CUDA_64
elif typ == 0x38: self.relocs.append((apply_image_offset + 4, rel_sym_offset, dtypes.uint32, 0))
elif typ == 0x39: self.relocs.append((apply_image_offset + 4, rel_sym_offset, dtypes.uint32, 32))
else: raise RuntimeError(f"unknown NV reloc {typ}")
# Minimum cbuf_0 size for driver params: Blackwell needs index 223 (224 entries), older GPUs need index 11 (12 entries)
min_cbuf0_entries = 224 if dev.iface.compute_class >= nv_gpu.BLACKWELL_COMPUTE_A else 12
self.cbuf_0 = [0] * max(cbuf0_size // 4, min_cbuf0_entries)
# the arguments follow the driver params in constant buffer 0: the buffers as 64 bit addresses, then the vars packed by their width
nbufs = sum(name is None for name, *_ in signature)
self.vars = list(TinyELF.iter_sig(signature[nbufs:], nbufs * 8))
if mock: # mockgpu reads the arg counts out of cbuf0 and wants every var 64 bit
self.cbuf_0[80:82], self.vars = [nbufs, len(self.vars)], [(nbufs * 8 + i * 8, dtypes.uint64) for i in range(len(self.vars))]
# NOTE: Ensure at least 4KB of space after the program to mitigate prefetch memory faults.
self.image = image.ljust(round_up(len(image), 0x1000) + 0x1000, b'\x00')
# constant buffer 0 holds the driver params and every argument after them, and starts 256 aligned like all constant buffers
self.kernargs_size = round_up(max(self.constbufs[0][1], len(self.cbuf_0) * 4 + len(signature) * 8), 256)
# Ensure device has enough local memory to run the program
self.dev._ensure_has_local_memory(self.lcmem_usage)
self.dev.allocator._copyin(self.lib_gpu, image)
self.dev.synchronize()
dev._ensure_has_local_memory(lcmem)
if dev.iface.compute_class >= nv_gpu.BLACKWELL_COMPUTE_A:
if not NAK: self.cbuf_0[188:192], self.cbuf_0[223] = [*data64_le(self.dev.shared_mem_window), *data64_le(self.dev.local_mem_window)], 0xfffdc0
qmd = {'qmd_major_version':5, 'qmd_type':nv_gpu.NVCEC0_QMDV05_00_QMD_TYPE_GRID_CTA, 'program_address_upper_shifted4':hi32(prog_addr>>4),
'program_address_lower_shifted4':lo32(prog_addr>>4), 'register_count':self.regs_usage, 'shared_memory_size_shifted7':self.shmem_usage>>7,
f'shader_local_memory_{"low" if NAK else "high"}_size_shifted4': self.dev.slm_per_thread>>4}
if not NAK: self.cbuf_0[188:192], self.cbuf_0[223] = [*data64_le(dev.shared_mem_window), *data64_le(dev.local_mem_window)], 0xfffdc0
qmd = {'qmd_major_version':5, 'qmd_type':nv_gpu.NVCEC0_QMDV05_00_QMD_TYPE_GRID_CTA, 'register_count':regs,
'shared_memory_size_shifted7':shmem>>7, f'shader_local_memory_{"low" if NAK else "high"}_size_shifted4':dev.slm_per_thread>>4}
else:
if not NAK: self.cbuf_0[6:12] = [*data64_le(self.dev.shared_mem_window), *data64_le(self.dev.local_mem_window), *data64_le(0xfffdc0)]
qmd = {'qmd_major_version':3, 'sm_global_caching_enable':1, 'program_address_upper':hi32(prog_addr), 'program_address_lower':lo32(prog_addr),
'shared_memory_size':self.shmem_usage, 'register_count_v':self.regs_usage,
f'shader_local_memory_{"low" if NAK else "high"}_size':self.dev.slm_per_thread}
if not NAK: self.cbuf_0[6:12] = [*data64_le(dev.shared_mem_window), *data64_le(dev.local_mem_window), *data64_le(0xfffdc0)]
qmd = {'qmd_major_version':3, 'sm_global_caching_enable':1, 'shared_memory_size':shmem, 'register_count_v':regs,
f'shader_local_memory_{"low" if NAK else "high"}_size':dev.slm_per_thread}
smem_cfg = min(shmem_conf * 1024 for shmem_conf in [32, 64, 100] if shmem_conf * 1024 >= self.shmem_usage) // 4096 + 1
smem_cfg = min(shmem_conf * 1024 for shmem_conf in [32, 64, 100] if shmem_conf * 1024 >= shmem) // 4096 + 1
self.qmd:QMD = QMD(dev, **qmd, qmd_group_id=0x3f, invalidate_texture_header_cache=1, invalidate_texture_sampler_cache=1,
# the program and constant buffer addresses are patched into a copy of this at exec, everything else is the same for every launch
self.qmd = QMD(dev)
self.qmd.write(**qmd, qmd_group_id=0x3f, invalidate_texture_header_cache=1, invalidate_texture_sampler_cache=1,
invalidate_texture_data_cache=1, invalidate_shader_data_cache=1, api_visible_call_limit=1, sampler_index=1, barrier_count=1,
cwd_membar_type=nv_gpu.NVC6C0_QMDV03_00_CWD_MEMBAR_TYPE_L1_SYSMEMBAR, constant_buffer_invalidate_0=1, min_sm_config_shared_mem_size=smem_cfg,
target_sm_config_shared_mem_size=smem_cfg, max_sm_config_shared_mem_size=0x1a, program_prefetch_size=min(prog_sz>>8, 0x1ff),
sass_version=dev.sass_version, program_prefetch_addr_upper_shifted=prog_addr>>40, program_prefetch_addr_lower_shifted=prog_addr>>8)
for i,(addr,sz) in self.constbufs.items():
self.qmd.set_constant_buf_addr(i, addr)
self.qmd.write(**{f'constant_buffer_size_shifted4_{i}': sz, f'constant_buffer_valid_{i}': 1})
sass_version=dev.sass_version)
for i,(_,sz) in self.constbufs.items(): self.qmd.write(**{f'constant_buffer_size_shifted4_{i}': sz, f'constant_buffer_valid_{i}': 1})
# Registers allocation granularity per warp is 256, warp allocation granularity is 4. Register file size is 65536.
self.max_threads = ((65536 // round_up(max(1, self.regs_usage) * 32, 256)) // 4) * 4 * 32
# NV's kernargs is constbuffer, then arguments to the kernel follows. Kernargs also appends QMD at the end of the kernel.
super().__init__(NVArgsState, self.dev, obj, kernargs_alloc_size=round_up(self.constbufs[0][1], 1 << 8) + (8 << 8))
weakref.finalize(self, self._fini, self.dev, self.lib_gpu, buf_spec)
self.max_threads = ((65536 // round_up(max(1, regs) * 32, 256)) // 4) * 4 * 32
def _parse_elf_info(self, sh, start_off=0):
while start_off < sh.header.sh_size:
@@ -325,18 +296,14 @@ class NVProgram(HCQProgram['NVDevice']):
yield typ, param, sh.content[start_off+4:start_off+sz+4] if typ == 0x4 else sz
start_off += (sz if typ == 0x4 else 0) + 4
def __call__(self, *bufs, global_size:tuple[int,int,int]=(1,1,1), local_size:tuple[int,int,int]=(1,1,1), vals:tuple[int|None, ...]=(),
wait=False, timeout:int|None=None):
if prod(local_size) > 1024 or self.max_threads < prod(local_size) or self.lcmem_usage > self.dev.slm_per_thread:
raise RuntimeError(f"Too many resources requested for launch, {prod(local_size)=}, {self.max_threads=}")
if any(cur > mx for cur,mx in zip(global_size, [2147483647, 65535, 65535])) or any(cur > mx for cur,mx in zip(local_size, [1024, 1024, 64])):
raise RuntimeError(f"Invalid global/local dims {global_size=}, {local_size=}")
res = super().__call__(*bufs, global_size=global_size, local_size=local_size, vals=vals, wait=wait, timeout=timeout)
if self.dev.pma_enabled:
self.dev.synchronize()
if pma_blob:=self.dev._prof_readback():
Compiled.profile_events += [ProfilePMAEvent(self.dev.device, self.name, pma_blob, self.dev.prof_exec_counter, self.profile_key)]
return res
_nv_program_cache:dict[tuple[bytes, tuple[str, ...]], tuple[NVProgramData, UOp]] = {}
def nv_build_program(dev:NVDevice, prg:UOp, devs:tuple[str, ...]) -> tuple[NVProgramData, UOp]:
if (cached:=_nv_program_cache.get(key:=(prg.src[3].arg, devs))) is None:
data = NVProgramData(dev, prg.to_elf())
buf = UOp.placeholder((len(data.image),), dtypes.uint8, next(UOp.unique_num), device=devs).rtag("program")
rows = [(off, ((buf.getaddr(devs) + sym) >> sh).ccast(dt)) for off, sym, dt, sh in data.relocs]
cached = _nv_program_cache[key] = (data, patch(buf, rows, data.image))
return cached
class NVAllocator(HCQAllocator['NVDevice']):
def _alloc(self, size:int, options:BufferSpec) -> HCQBuffer:
@@ -350,22 +317,31 @@ class NVAllocator(HCQAllocator['NVDevice']):
assert all(h.va_addr % 0x100 == 0 for h in hist + [bufin, bufout, desc_buf]), "all buffers must be 0x100 aligned"
h, w = ((2 * shape[0]) // 3 if shape[0] % 3 == 0 else (2 * shape[0] - 1) // 3), shape[1]
self.dev._ensure_has_vid_hw(w, h)
dev, chroma_off = self.dev, round_up(w, 64) * round_up(h, 64)
dev._ensure_has_vid_hw(w, h)
q = NVVideoQueue().wait(self.dev.timeline_signal, self.dev.timeline_value - 1)
with hcq_profile(self.dev, queue=q, desc="HEVC Decode", enabled=PROFILE, dev_suff="NVDEC"):
q.decode_hevc_chunk(desc_buf, bufin, bufout, frame_pos, hist, [(frame_pos-x) % (len(hist) + 1) for x in range(len(hist), 0, -1)],
round_up(w, 64)*round_up(h, 64), self.dev.vid_coloc_buf, self.dev.vid_filter_buf, self.dev.intra_top_off,
self.dev.intra_unk_off, self.dev.vid_stat_buf)
q.signal(self.dev.timeline_signal, self.dev.next_timeline()).submit(self.dev)
cmds = nvm(4, nv_gpu.NVC9B0_SET_APPLICATION_ID, nv_gpu.NVC9B0_SET_APPLICATION_ID_ID_HEVC)
cmds += nvm(4, nv_gpu.NVC9B0_SET_CONTROL_PARAMS, nv_flags("NVC9B0_SET_CONTROL_PARAMS", codec_type="hevc", testrun_env="prod_run", gptimer_on=1,
err_conceal_on=1, mbtimer_on=1, event_trace_logging_on=1))
cmds += nvm(4, nv_gpu.NVC9B0_SET_DRV_PIC_SETUP_OFFSET, desc_buf.va_addr >> 8)
cmds += nvm(4, nv_gpu.NVC9B0_SET_IN_BUF_BASE_OFFSET, bufin.va_addr >> 8)
for pos, buf in zip([(frame_pos-x) % (len(hist) + 1) for x in range(len(hist), 0, -1)] + [frame_pos], hist + [bufout]):
cmds += nvm(4, nv_gpu.NVC9B0_SET_PICTURE_LUMA_OFFSET0 + pos*4, buf.va_addr >> 8)
cmds += nvm(4, nv_gpu.NVC9B0_SET_PICTURE_CHROMA_OFFSET0 + pos*4, buf.offset(chroma_off).va_addr >> 8)
cmds += nvm(4, nv_gpu.NVC9B0_SET_COLOC_DATA_OFFSET, dev.vid_coloc_buf._buf.va_addr >> 8)
cmds += nvm(4, nv_gpu.NVC9B0_SET_NVDEC_STATUS_OFFSET, dev.vid_stat_buf._buf.va_addr >> 8)
cmds += nvm(4, nv_gpu.NVC9B0_HEVC_SET_TILE_SIZES_OFFSET, desc_buf.offset(0x200).va_addr >> 8)
cmds += nvm(4, nv_gpu.NVC9B0_HEVC_SET_FILTER_BUFFER_OFFSET, (filter_addr:=dev.vid_filter_buf._buf.va_addr) >> 8)
cmds += nvm(4, nv_gpu.NVC9B0_SET_INTRA_TOP_BUF_OFFSET, (filter_addr + dev.intra_top_off) >> 8)
if dev.intra_unk_off is not None: cmds += nvm(4, 0x4dc, (filter_addr + dev.intra_unk_off) >> 8)
cmds += nvm(4, nv_gpu.NVC9B0_EXECUTE, 0)
dev._submit_cmds(dev.fifos["NVDEC:0"], *cmds)
# *****************
# device
@dataclass
class GPFifo:
ring: MMIOInterface
gpput: MMIOInterface
entries_count: int
token: int
put_value: int = 0
class GPFifo: ring: Buffer; gpput: Buffer; doorbell: Buffer; put_value: Buffer; entries: int; token: int # noqa: E702
class NVKIface:
root = None
@@ -454,7 +430,7 @@ class NVKIface:
self.uvm(nv_gpu.UVM_REGISTER_GPU_VASPACE, nv_gpu.UVM_REGISTER_GPU_VASPACE_PARAMS(
gpuUuid=self.gpu_uuid, rmCtrlFd=self.fd_ctl.fd, hClient=self.root, hVaSpace=vaspace))
for dev in [d for pg in HCQCompiled.peer_groups.values() for d in pg if isinstance(d, NVDevice) and not d.is_nvd()]:
for dev in [d for x in Device._opened_devices if isinstance(d:=Device[x], NVDevice) and not d.is_nvd()]:
try: self.uvm(nv_gpu.UVM_ENABLE_PEER_ACCESS, nv_gpu.UVM_ENABLE_PEER_ACCESS_PARAMS(gpuUuidA=self.gpu_uuid, gpuUuidB=dev.iface.gpu_uuid))
except RuntimeError as e: raise RuntimeError(f"{e}. Make sure GPUs #{self.gpu_minor} & #{dev.iface.gpu_minor} have P2P enabled.") from e
@@ -582,8 +558,13 @@ class PCIIface(PCIIfaceBase):
class MOCKIface(NVKIface): count = 1
class NVDevice(HCQCompiled[NVSignal]):
class NVDevice(HCQ2Compiled):
ifaces = [NVKIface, PCIIface, MOCKIface]
sleep_timeout_ms = 200
pm_encode = PatternMatcher([
(UPat(Ops.CUSTOM_FUNCTION, arg="submit_nv_compute", name="submit"), lambda ctx, submit: encode_submit(NVComputeQueue(ctx, submit))),
(UPat(Ops.CUSTOM_FUNCTION, arg="submit_nv_copy", name="submit"), lambda ctx, submit: encode_submit(NVCopyQueue(ctx, submit))),
])
def is_nvd(self) -> bool: return isinstance(self.iface, PCIIface)
@@ -610,19 +591,11 @@ class NVDevice(HCQCompiled[NVSignal]):
channel_params = nv_gpu.NV_CHANNEL_GROUP_ALLOCATION_PARAMETERS(engineType=nv_gpu.NV2080_ENGINE_TYPE_GRAPHICS)
self.channel_group = self.iface.rm_alloc(self.nvdevice, nv_gpu.KEPLER_CHANNEL_GROUP_A, channel_params)
self.gpfifo_area = self.iface.alloc(0x300000, contiguous=True, cpu_access=True, force_devmem=True,
self.gpfifo_mem = self.iface.alloc(0x300000, contiguous=True, cpu_access=True, force_devmem=True,
map_flags=(nv_gpu.NVOS33_FLAGS_CACHING_TYPE_WRITECOMBINED<<23))
ctxshare_params = nv_gpu.NV_CTXSHARE_ALLOCATION_PARAMETERS(hVASpace=vaspace, flags=nv_gpu.NV_CTXSHARE_ALLOCATION_FLAGS_SUBCONTEXT_ASYNC)
ctxshare = self.iface.rm_alloc(self.channel_group, nv_gpu.FERMI_CONTEXT_SHARE_A, ctxshare_params)
self.compute_gpfifo = self._new_gpu_fifo(self.gpfifo_area, ctxshare, self.channel_group, offset=0, entries=0x10000, compute=True)
self.dma_gpfifo = self._new_gpu_fifo(self.gpfifo_area, ctxshare, self.channel_group, offset=0x100000, entries=0x10000, compute=False)
self.iface.rm_control(self.channel_group, nv_gpu.NVA06C_CTRL_CMD_GPFIFO_SCHEDULE, nv_gpu.NVA06C_CTRL_GPFIFO_SCHEDULE_PARAMS(bEnable=1))
self.cmdq_page:HCQBuffer = self.iface.alloc(0x200000, cpu_access=True)
self.cmdq_allocator = BumpAllocator(size=self.cmdq_page.size, base=int(self.cmdq_page.va_addr), wrap=True)
self.cmdq = self.cmdq_page.cpu_view().view(fmt='I')
self.ctxshare = self.iface.rm_alloc(self.channel_group, nv_gpu.FERMI_CONTEXT_SHARE_A,
nv_gpu.NV_CTXSHARE_ALLOCATION_PARAMETERS(hVASpace=vaspace, flags=nv_gpu.NV_CTXSHARE_ALLOCATION_FLAGS_SUBCONTEXT_ASYNC))
self.num_gpcs, self.num_tpc_per_gpc, self.num_sm_per_tpc, self.max_warps_per_sm, self.sm_version = self._query_gpu_info('num_gpcs',
'num_tpc_per_gpc', 'num_sm_per_tpc', 'max_warps_per_sm', 'sm_version')
@@ -631,19 +604,37 @@ class NVDevice(HCQCompiled[NVSignal]):
self.arch: str = "sm_120" if self.sm_version==0xa04 else f"sm_{(self.sm_version>>8)&0xff}{(val>>4) if (val:=self.sm_version&0xff) > 0xf else val}"
self.sass_version = ((self.sm_version & 0xf00) >> 4) | (self.sm_version & 0xf)
super().__init__(device, NVAllocator(self), [CUDARenderer, PTXRenderer, NVCCRenderer, NAKRenderer], NVProgram, NVSignal, NVComputeQueue,
NVCopyQueue, arch=self.arch)
self.slm_per_thread = 0
self.shader_local_mem:Buffer|None = None
# Set windows addresses to not collide with other allocated buffers.
self.shared_mem_window, self.local_mem_window = 0x729400000000, 0x729300000000
self.pma_enabled = PMA.value > 0 and PROFILE >= 1
if self.pma_enabled: self._prof_init()
super().__init__(device, NVAllocator(self), [CUDARenderer, PTXRenderer, NVCCRenderer, NAKRenderer], None, arch=self.arch)
self._setup_gpfifos()
self.pma_enabled, self.pma_exec_counter = PMA.value > 0 and PROFILE >= 1, itertools.count(0)
def _new_gpu_fifo(self, gpfifo_area, ctxshare, channel_group, offset=0, entries=0x400, compute=False, video=False) -> GPFifo:
@functools.cached_property
def fifos(self) -> dict[str, GPFifo]:
self.gpfifo_buf = Buffer(self.device, self.gpfifo_mem.size, dtypes.uint8, options=BufferSpec(external_ptr=self.gpfifo_mem.va_addr, nolru=True)) \
.allocate(opaque=self.gpfifo_mem)
compute = self._new_gpu_fifo("COMPUTE:0", self.ctxshare, self.channel_group, offset=0, entries=0x10000, compute=True)
copy = self._new_gpu_fifo("COPY:0", self.ctxshare, self.channel_group, offset=0x100000, entries=0x10000)
self.iface.rm_control(self.channel_group, nv_gpu.NVA06C_CTRL_CMD_GPFIFO_SCHEDULE, nv_gpu.NVA06C_CTRL_GPFIFO_SCHEDULE_PARAMS(bEnable=1))
self._submit_cmds(compute, *nvm(1, nv_gpu.NVC6C0_SET_OBJECT, self.iface.compute_class),
*nvm(1, nv_gpu.NVC6C0_SET_SHADER_LOCAL_MEMORY_WINDOW_A, *data64(self.local_mem_window)),
*nvm(1, nv_gpu.NVC6C0_SET_SHADER_SHARED_MEMORY_WINDOW_A, *data64(self.shared_mem_window)))
self._submit_cmds(copy, *nvm(4, nv_gpu.NVC6C0_SET_OBJECT, self.iface.dma_class))
if self.pma_enabled: self._prof_init() # the sampler binds to the channel group, so it only comes up once the channels do
return {"COMPUTE:0": compute, "COPY:0": copy}
def _new_gpu_fifo(self, name:str, ctxshare, channel_group, offset=0, entries=0x400, compute=False, video=False) -> GPFifo:
notifier = self.iface.alloc(48 << 20, uncached=True)
params = nv_gpu.NV_CHANNELGPFIFO_ALLOCATION_PARAMETERS(gpFifoOffset=gpfifo_area.va_addr+offset, gpFifoEntries=entries, hContextShare=ctxshare,
hObjectError=notifier.meta.hMemory, hObjectBuffer=self.virtmem if video else gpfifo_area.meta.hMemory,
hUserdMemory=(ctypes.c_uint32*8)(gpfifo_area.meta.hMemory), userdOffset=(ctypes.c_uint64*8)(entries*8+offset), engineType=19 if video else 0,
params = nv_gpu.NV_CHANNELGPFIFO_ALLOCATION_PARAMETERS(gpFifoOffset=self.gpfifo_mem.va_addr+offset, gpFifoEntries=entries, hContextShare=ctxshare,
hObjectError=notifier.meta.hMemory, hObjectBuffer=self.virtmem if video else self.gpfifo_mem.meta.hMemory,
hUserdMemory=(ctypes.c_uint32*8)(self.gpfifo_mem.meta.hMemory), userdOffset=(ctypes.c_uint64*8)(entries*8+offset),
engineType=19 if video else 0,
hVASpace=self.vaspace if video and self.is_nvd() else 0) # gsp has no default vaspace, rm maps the decoder ctx into its own
gpfifo = self.iface.rm_alloc(channel_group, self.iface.gpfifo_class, params)
@@ -662,8 +653,14 @@ class NVDevice(HCQCompiled[NVSignal]):
nv_gpu.NVC36F_CTRL_CMD_GPFIFO_GET_WORK_SUBMIT_TOKEN_PARAMS(workSubmitToken=-1))
if ctxshare != 0: self.iface.setup_gpfifo_vm(gpfifo)
return GPFifo(ring=gpfifo_area.cpu_view().view(offset, entries*8, fmt='Q'), entries_count=entries, token=ws_token_params.workSubmitToken,
gpput=gpfifo_area.cpu_view().view(offset + entries*8 + getattr(nv_gpu.AmpereAControlGPFifo, 'GPPut').offset, fmt='I'))
gpput_off = offset + entries*8 + getattr(nv_gpu.AmpereAControlGPFifo, 'GPPut').offset
fifo = GPFifo(ring=self.gpfifo_buf.view(entries, dtypes.uint64, offset).ensure_allocated(),
gpput=self.gpfifo_buf.view(1, dtypes.uint32, gpput_off).ensure_allocated(),
doorbell=Buffer("CPU", 1, dtypes.uint32, options=BufferSpec(external_ptr=self.gpu_mmio.addr + 0x90), preallocate=True),
put_value=Buffer("CPU", 1, dtypes.uint64, preallocate=True), entries=entries, token=ws_token_params.workSubmitToken)
self.pm_bufferize = PatternMatcher([(UPat(Ops.PARAM, tag=to_name(n, name)), lambda ctx, b=getattr(fifo, n): b)
for n in ("ring", "gpput", "doorbell", "put_value")]) + self.pm_bufferize
return fifo
def _query_gpu_info(self, *reqs):
nvrs = [getattr(nv_gpu,'NV2080_CTRL_GR_INFO_INDEX_'+r.upper(), getattr(nv_gpu,'NV2080_CTRL_GR_INFO_INDEX_LITTER_'+r.upper(), None)) for r in reqs]
@@ -678,34 +675,35 @@ class NVDevice(HCQCompiled[NVSignal]):
nv_gpu.NV2080_CTRL_GR_GET_INFO_PARAMS(grInfoListSize=len(infos), grInfoList=ctypes.addressof(infos)))
return [x.data for x in infos]
def _setup_gpfifos(self):
self.slm_per_thread, self.shader_local_mem = 0, None
def _push(self, fifo:GPFifo, cmds:list[int]): # a pushbuffer built in python: channel setup and video decode
(buf:=self.rt_view(len(cmds) * 4))._buf.cpu_view().view(fmt='I')[:] = array.array('I', cmds)
# Set windows addresses to not collide with other allocated buffers.
self.shared_mem_window, self.local_mem_window = 0x729400000000, 0x729300000000
put = fifo.put_value._buf.view.view(fmt='Q')
fifo.ring._buf.cpu_view().view(fmt='Q')[put[0] % fifo.entries] = buf._buf.va_addr | (len(cmds) << 42) | (1 << 41)
fifo.gpput._buf.cpu_view().view(fmt='I')[0] = (put[0] + 1) % fifo.entries
NVComputeQueue().setup(compute_class=self.iface.compute_class, local_mem_window=self.local_mem_window, shared_mem_window=self.shared_mem_window) \
.signal(self.timeline_signal, self.next_timeline()).submit(self)
System.memory_barrier()
self.gpu_mmio[0x90 // 4] = fifo.token
put[0] += 1
NVCopyQueue().wait(self.timeline_signal, self.timeline_value - 1) \
.setup(copy_class=self.iface.dma_class) \
.signal(self.timeline_signal, self.next_timeline()).submit(self)
self.synchronize()
def _submit_cmds(self, fifo:GPFifo, *cmds:int): # runs cmds once everything already submitted is done, then bumps the timeline
tl, addr = self.timeline._buf.cpu_view().view(fmt='Q'), self.timeline._buf.va_addr
self._push(fifo, nvm(0, nv_gpu.NVC56F_SEM_ADDR_LO, *data64_le(addr), *data64_le(tl[1]),
nv_flags("NVC56F_SEM_EXECUTE", operation="acq_circ_geq", payload_size="64bit")) + list(cmds) +
nvm(0, nv_gpu.NVC56F_SEM_ADDR_LO, *data64_le(addr), *data64_le(tl[1] + 1),
nv_flags("NVC56F_SEM_EXECUTE", operation="release", release_wfi="en", payload_size="64bit")))
tl[1] += 1
def _ensure_has_local_memory(self, required):
if self.slm_per_thread >= required: return
self.slm_per_thread, old_slm_per_thread = round_up(required, 32), self.slm_per_thread
self.slm_per_thread = round_up(required, 32)
bytes_per_tpc = round_up(round_up(self.slm_per_thread * 32, 0x200) * self.max_warps_per_sm * self.num_sm_per_tpc, 0x8000)
self.shader_local_mem, ok = self._realloc(self.shader_local_mem, round_up(bytes_per_tpc*self.num_tpc_per_gpc*self.num_gpcs, 0x20000))
self.shader_local_mem = Buffer(self.device, round_up(bytes_per_tpc*self.num_tpc_per_gpc*self.num_gpcs, 0x20000), dtypes.uint8,
options=BufferSpec(nolru=True), preallocate=True)
# Realloc failed, restore the old value.
if not ok: self.slm_per_thread = old_slm_per_thread
cast(NVComputeQueue, NVComputeQueue().wait(self.timeline_signal, self.timeline_value - 1)) \
.setup(local_mem=self.shader_local_mem.va_addr, local_mem_tpc_bytes=bytes_per_tpc) \
.signal(self.timeline_signal, self.next_timeline()).submit(self)
self._submit_cmds(self.fifos["COMPUTE:0"], *nvm(1, nv_gpu.NVC6C0_SET_SHADER_LOCAL_MEMORY_A, *data64(self.shader_local_mem._buf.va_addr)),
*nvm(1, nv_gpu.NVC6C0_SET_SHADER_LOCAL_MEMORY_NON_THROTTLED_A, *data64(bytes_per_tpc), 0xff))
def _ensure_has_vid_hw(self, w, h):
if self.iface.viddec_class is None: raise RuntimeError(f"{self.device} Video decoder class not available.")
@@ -716,18 +714,20 @@ class NVDevice(HCQCompiled[NVSignal]):
self.intra_unk_off = (round_up(self.intra_top_off, 0x10000) + (64 << 10)) if intra_unk_size > 0 else None
filter_sz = round_up(round_up(self.intra_top_off, 0x10000) + (64 << 10) + intra_unk_size, 2 << 20)
if not hasattr(self, 'vid_gpfifo'):
self.vid_gpfifo = self._new_gpu_fifo(self.gpfifo_area, 0, self.nvdevice, offset=0x200000, entries=2048, compute=False, video=True)
self.vid_coloc_buf, self.vid_filter_buf = (self.allocator.alloc(sz, BufferSpec(zero=True)) for sz in [coloc_sz, filter_sz])
self.vid_stat_buf = self.allocator.alloc(0x1000, BufferSpec(zero=True))
NVVideoQueue().wait(self.timeline_signal, self.timeline_value - 1) \
.setup(copy_class=self.iface.viddec_class) \
.signal(self.timeline_signal, self.next_timeline()).submit(self)
def _vid_buf(sz): return Buffer(self.device, sz, dtypes.uint8, options=BufferSpec(zero=True, nolru=True), preallocate=True)
if "NVDEC:0" not in self.fifos:
self.fifos["NVDEC:0"] = self._new_gpu_fifo("NVDEC:0", 0, self.nvdevice, offset=0x200000, entries=2048, video=True)
self.vid_coloc_buf, self.vid_filter_buf, self.vid_stat_buf = _vid_buf(coloc_sz), _vid_buf(filter_sz), _vid_buf(0x1000)
self._submit_cmds(self.fifos["NVDEC:0"], *nvm(4, nv_gpu.NVC6C0_SET_OBJECT, self.iface.viddec_class))
else:
if coloc_sz > self.vid_coloc_buf.size: self.vid_coloc_buf,_= self._realloc(self.vid_coloc_buf, coloc_sz, BufferSpec(zero=True), force=True)
if filter_sz > self.vid_filter_buf.size: self.vid_filter_buf,_= self._realloc(self.vid_filter_buf, filter_sz, BufferSpec(zero=True), force=True)
if coloc_sz > self.vid_coloc_buf.nbytes: self.vid_coloc_buf = _vid_buf(coloc_sz)
if filter_sz > self.vid_filter_buf.nbytes: self.vid_filter_buf = _vid_buf(filter_sz)
def hw_copy_queues(self): return super().hw_copy_queues() + ([("NVDEC:0", NVVideoQueue)] if hasattr(self, 'vid_gpfifo') else [])
def collect_prof(self):
# the pc samples of a whole batch come back as one stream, so they are reported against the first kernel of it
if self.pma_enabled and (ents:=list(self.prof_ents.values())) and (blob:=self._prof_readback()) is not None:
Compiled.profile_events.append(ProfilePMAEvent(self.device, str(ents[0].name), blob, next(self.pma_exec_counter), ents[0].profile_key))
super().collect_prof()
def invalidate_caches(self):
if self.is_nvd(): self.iface.rm_control(self.subdevice, nv_gpu.NV2080_CTRL_CMD_INTERNAL_BUS_FLUSH_WITH_SYSMEMBAR, None)
@@ -845,4 +845,4 @@ class NVDevice(HCQCompiled[NVSignal]):
nv_gpu.struct_NVB0CC_CTRL_PMA_STREAM_UPDATE_GET_PUT_PARAMS(bytesConsumed=params.bytesAvailable))
return pma_data
def device_props(self): return {'arch': self.arch, 'sm_version': self.sm_version}
def device_props(self) -> dict[str, Any]: return {'arch': self.arch, 'sm_version': self.sm_version}
+5 -3
View File
@@ -18,9 +18,11 @@ def _load(m, i, dtype: DType):
if (w:=m.nbytes // len(m)) >= dtype.itemsize: return from_storage_scalar(m[i], dtype)
return sum(m[i+k] << (8*w*k) for k in range(dtype.itemsize // w)) # a bitcast can read wider than the buffer, _store splits it the same way
def _step(m, dtype: DType): return max(1, dtype.itemsize // (m.nbytes // len(m))) # storage elements per lane
def load(inp, j, dtype: DType):
if len(inp) >= 3: return [_load(m, x+j if x is not None else None, dtype) if gate else default for (m,x),default,gate in zip(*inp[:3])]
return [_load(m, x+j if x is not None else None, dtype) for m,x in inp[0]]
if len(inp) >= 3: return [_load(m, x+j*_step(m, dtype) if x is not None else None, dtype) if gate else alt for (m,x),alt,gate in zip(*inp[:3])]
return [_load(m, x+j*_step(m, dtype) if x is not None else None, dtype) for m,x in inp[0]]
def _store(m, i, v, dtype: DType):
if i < 0 or i >= len(m): raise IndexError(f"store out of bounds, size is {len(m)}, access is {i}, value is {v}")
@@ -86,7 +88,7 @@ class PythonProgram(Program['PythonDevice']):
store_gate = exec_masks[-1]
for j,val in enumerate(src_values[1] if u.max_numel() > 1 else [src_values[1]]):
for (m,o),v,g in zip(src_values[0], val, store_gate):
if g: _store(m, o+j, v, src_dtypes[1])
if g: _store(m, o+j*_step(m, src_dtypes[1]), v, src_dtypes[1])
i += 1
continue
if u.op is Ops.AFTER or (u.op is Ops.BITCAST and u.addrspace in (AddrSpace.GLOBAL, AddrSpace.LOCAL)): values[u] = src_values[0]
+4 -2
View File
@@ -144,7 +144,7 @@ class AMMemoryManager(MemoryManager):
self.dev.gmc.flush_tlb(ip='MM', vmid=0)
class AMDev:
Version = 0xA0000008
Version = 0xA000000D
def _disable_aspm(self):
# L1 across retimers makes reads oscillate to 0xffffffff; power on defaults it enabled. Clearing the GPU endpoint
@@ -200,6 +200,7 @@ class AMDev:
self.smu.mode1_reset()
self.pci_dev.write_config_flush(pci.PCI_COMMAND, self.pci_dev.read_config(pci.PCI_COMMAND, 2) | pci.PCI_COMMAND_MASTER, 2)
self.init_hw(self.soc, self.gmc, self.ih, *(() if self.is_vf else (self.psp, self.smu)))
elif not self.is_vf: self.psp._tmr_init()
# Booting done
self.is_booting = False
@@ -213,6 +214,7 @@ class AMDev:
self.smu.set_clocks(level=None)
else: self.smu.set_clocks(level=-1) # last level, max perf.
for ip in [self.soc, self.gfx]: ip.set_clockgating_state()
self.reg("regSCRATCH_REG5").write(self.psp.tmr_size) # scratch registers are writable after GFX initialization
self.reg("regSCRATCH_REG7").write(AMDev.Version)
self.reg("regSCRATCH_REG6").write(1) # set initialized state.
@@ -222,7 +224,7 @@ class AMDev:
self.smi_dev, self.is_err_state = smi_dev, False
# Memory manager & firmware
self.mm = AMMemoryManager(self, self.vram_size - self.reserved_vram_size, boot_size=(32 << 20), pt_t=AMPageTableEntry, va_shifts=[12, 21, 30, 39],
self.mm = AMMemoryManager(self, self.vram_size - self.reserved_vram_size, boot_size=(3 << 20), pt_t=AMPageTableEntry, va_shifts=[12, 21, 30, 39],
va_bits=48, first_lv=am.AMDGPU_VM_PDB2, va_base=AMMemoryManager.va_allocator.base, reserve_ptable=not self.large_bar,
palloc_ranges=[(1 << (i + 12), (2 << 20) if i >= 9 else 0x1000) for i in range(9 * (3 - am.AMDGPU_VM_PDB2), -1, -1)])
self.fw = AMFirmware(self)
+8 -6
View File
@@ -603,10 +603,9 @@ class AM_PSP(AM_IP):
self.ring_size = 0x10000
self.ring_paddr = self.adev.mm.palloc(self.ring_size, zero=False, boot=True)
self.max_tmr_size, self.tmr_size = 0x1300000, 0
self.tmr_size, self.tmr_paddr = 0, 0
self.boot_time_tmr = self.adev.ip_ver[am.MP0_HWIP] in {(13,0,6), (13,0,14), (14,0,2), (14,0,3)}
self.autoload_tmr = self.adev.ip_ver[am.MP0_HWIP] not in {(13,0,6), (13,0,14)}
self.tmr_paddr = self.adev.mm.palloc(self.max_tmr_size, align=am.PSP_TMR_ALIGNMENT, zero=False, boot=True) if not self.boot_time_tmr else 0
def init_hw(self):
spl_key = am.PSP_FW_TYPE_PSP_SPL if self.adev.ip_ver[am.MP0_HWIP] >= (14,0,0) else am.PSP_FW_TYPE_PSP_KDB
@@ -655,10 +654,13 @@ class AM_PSP(AM_IP):
return self._wait_for_bootloader() if compid != am.PSP_BL__LOAD_SOSDRV else 0
def _tmr_init(self):
# Load TOC and calculate TMR size
self._prep_msg1(fwm:=self.adev.fw.sos_fw[am.PSP_FW_TYPE_PSP_TOC])
self.tmr_size = self._load_toc_cmd(len(fwm)).resp.tmr_size
assert self.tmr_size <= self.max_tmr_size
if self.adev.partial_boot: self.tmr_size = self.adev.reg("regSCRATCH_REG5").read()
else:
# Load TOC and calculate TMR size
self._prep_msg1(fwm:=self.adev.fw.sos_fw[am.PSP_FW_TYPE_PSP_TOC])
self.tmr_size = self._load_toc_cmd(len(fwm)).resp.tmr_size
# First runtime allocation on both full and partial boots, so the resident TMR keeps the same address.
if not self.boot_time_tmr: self.tmr_paddr = self.adev.mm.pa_allocator.alloc(self.tmr_size, am.PSP_TMR_ALIGNMENT)
def _ring_create(self):
# If the ring is already created, destroy it
+1 -1
View File
@@ -30,7 +30,7 @@ class AMDIP:
# https://github.com/torvalds/linux/blob/9207d47f966be9f4d52e7e0119ac2b7a7e366f3e/drivers/gpu/drm/amd/amdgpu/amdgpu_discovery.c#L3163
def import_module(name:str, target:tuple[int, int, int], submod=""):
# version overrides
target = {("smu", (13, 0, 7)): (13, 0, 0)}.get((name, target), target)
target = {("smu", (13, 0, 7)): (13, 0, 0), ("smu", (13, 0, 10)): (13, 0, 0)}.get((name, target), target)
mod = getattr(tinygrad.runtime.autogen.am, submod) if submod else tinygrad.runtime.autogen.am
if (children:=[c for c in mod.__all__ if c.startswith(name) and (v:=tuple(map(int, c.split('_')[1:])))[0] == target[0] and v <= target]):
return getattr(mod, children[-1])
+1 -1
View File
@@ -14,7 +14,7 @@ class QCOMCompiler(Compiler):
else:
# extract once into the download cache, all processes share the rootfs (extract=True)
self.arch, self.chip_id = arch, 0x6030001
fs, root = fetch('https://git.tinygrad.win/sirhcm/images/releases/download/v2/qcomcl.tar.gz', extract=True), pathlib.Path(__file__).parents[3]
fs, root = fetch('https://git.tinygrad.win/tinygrad/images/releases/download/v2/qcomcl.tar.gz', extract=True), pathlib.Path(__file__).parents[3]
self.compiler_process = self.server(f"{qemu} -cpu max,pauth=off -L {fs} {fs}/usr/bin/python3" if (qemu:=shutil.which("qemu-aarch64-static"))
else (f"docker run --rm -i --platform linux/aarch64 -v {fs}/usr:/usr -v {root}:{root} "
f"-e PYTHONPATH={root} -e QEMU_CPU=max,pauth=off gcr.io/distroless/static python3"), arch)
+106 -83
View File
@@ -1,13 +1,13 @@
from __future__ import annotations
from typing import cast, TypeVar, Generic, Any, TYPE_CHECKING
import functools, time, itertools, decimal, weakref, os, statistics, ctypes, importlib
import functools, time, itertools, decimal, weakref, statistics, ctypes, importlib
from dataclasses import replace, dataclass, field
from tinygrad.helpers import suppress_finalizing, dedup, pluralize, unwrap, PROFILE, VIZ, HCQ2, cpu_profile, mv_address
from tinygrad.helpers import to_tuple, ContextVar, Context, panic, partition, perf_counter_us
from tinygrad.device import Device, Buffer, MultiBuffer, BufferSpec, Compiled, LRUAllocator, DepsTracker
from tinygrad.helpers import to_tuple, ContextVar, Context, panic, partition, perf_counter_us, DEV
from tinygrad.device import Device, Buffer, BufferSpec, Compiled, LRUAllocator, DepsTracker
from tinygrad.device import ProfileGraphEntry, ProfileGraphEvent, ProfileDeviceEvent
from tinygrad.uop.ops import Ops, sint, UOp, UPat, PatternMatcher, KernelInfo, GroupOp, graph_rewrite, rewrite_group, exec_alu
from tinygrad.dtype import dtypes, DType, DTYPES_DICT
from tinygrad.dtype import dtypes, DType, DTYPES_DICT, AddrSpace
from tinygrad.runtime.support.memory import BumpAllocator, MMIOInterface
from tinygrad.renderer import Renderer, Estimates
from tinygrad.engine.realize import get_call_arg_uops, get_call_name, get_call_outs_ins, estimate_uop, pm_flatten_linear
@@ -20,7 +20,8 @@ if TYPE_CHECKING: from tinygrad.runtime.support.hcq import HCQBuffer # TODO: rem
HCQDeviceType = TypeVar('HCQDeviceType', bound='HCQ2Compiled')
HCQ_RUNTIME_DEV = ContextVar("HCQ_RUNTIME_DEV", "CPU")
HCQ_DEVS = frozenset(("QCOM", "CPU")) | (frozenset(("AMD",)) if HCQ2 else frozenset())
HCQ_CACHE_THRESH = ContextVar("HCQ_CACHE_THRESH", 64)
HCQ_DEVS = frozenset(("NV", "QCOM", "CPU")) | (frozenset(("AMD",)) if HCQ2 else frozenset())
@dataclass(frozen=True)
class HCQInfo:
@@ -84,19 +85,19 @@ def cstruct(struct_t, **fields:UOp|int) -> UOp:
return patch(buf, rows, bytes(ctypes.sizeof(struct_t)))
# *****************
# 0.1. prep: replace buffers with params
# 0.1. prep: eager buffers become tagged params
def replace_call_buffers(ctx:tuple[list[UOp], dict[UOp, int]], call:UOp) -> UOp|None:
bufs, slots = ctx
for s in call.src[1:]:
if s.op is not Ops.PARAM and not s.is_bound_var and slots.setdefault(s, len(bufs)) == len(bufs): bufs.append(s)
return call.replace(src=call.src[:1] + tuple(s if s.op is Ops.PARAM or s.is_bound_var else s.param_like(slots[s]) for s in call.src[1:]))
pm_replace_buffers = PatternMatcher([(UPat(Ops.CALL, name="call"), replace_call_buffers)])
def replace_buffer(ctx:tuple[bool, list[UOp], dict[UOp, int]], b:UOp) -> UOp:
use_rt, bufs, slots = ctx
if slots.setdefault(b, len(bufs)) == len(bufs): bufs.append(b)
param = UOp.param(slots[b], b.dtype, b.max_numel(), b.device)
return param if use_rt else param.replace(tag="lt_input")
pm_replace_buffers = PatternMatcher([(UPat(Ops.BUFFER, name="b"), replace_buffer)])
# *****************
# 1.1. prep: staging copies
STAGING_SIZE, STAGING_SLOTS = (4 if os.getenv("CI") else 128) << 20, 2 # the staging mlocks into the device: ci runners cap locked memory at 8mb
STAGING_SIZE, STAGING_SLOTS = (4 if DEV.interface.startswith("MOCK") else 128) << 20, 2
@functools.cache
def _staging() -> Buffer: return Buffer("CPU", STAGING_SIZE, dtypes.uint8, preallocate=True)
@@ -138,10 +139,11 @@ pm_unwrap_multi = PatternMatcher([(UPat(Ops.CALL, name="call"), unwrap_call)])
# 2. deps
class HCQDepsTracker(DepsTracker):
# TODO: optimize
@staticmethod
def _key(buf:Any) -> tuple[Any, int, int]:
return (buf, 0, buf.max_numel() * buf.dtype.itemsize) if isinstance(buf, UOp) else DepsTracker._key(buf)
def _key(a:UOp) -> tuple[Any, int, int]: # (base, lane) and the byte range: overlapping views of one base depend
lane, view = (a.arg, a.src[0]) if a.op is Ops.MSELECT else (None, a)
base, off = unwrap_view(view)
return (base, lane), off, off + view.max_numel() * view.dtype.itemsize
@dataclass
class BatchCtx:
@@ -150,37 +152,38 @@ class BatchCtx:
tracker:HCQDepsTracker = field(default_factory=HCQDepsTracker)
queues:dict[str, list[str]] = field(init=False)
first:dict[tuple[str, str], int] = field(init=False); last:dict[tuple[str, str], int] = field(init=False) # noqa: E702
prev:list[int|None] = field(init=False)
signal_tags:set[int] = field(init=False)
slots:dict[str, UOp] = field(init=False)
def __post_init__(self):
self.queues, self.first, self.last = {}, {}, {}
self.queues, self.first, self.last, self.prev = {}, {}, {}, []
for tag, (_, devs, q) in enumerate(self.batch):
if q not in self.queues.setdefault(devs[0], []): self.queues[devs[0]].append(q)
self.first.setdefault((devs[0], q), tag)
self.prev.append(self.last.get((devs[0], q)))
self.last[(devs[0], q)] = tag
self.signal_tags = {tag for (dev, q), tag in self.last.items() if q != self.epilogue_queue(dev)}
self.slots = {dev: UOp.placeholder((len(qs) + 1 + (2 * len(self.batch) if self.profile else 0),), dtypes.uint64, device=(dev,), volatile=True,
tag="slots") for dev, qs in self.queues.items()}
# a slot is [signal][timestamp], 16 bytes: the queue signals, the timeline, then two per call if profiling
self.slots = {dev: UOp.placeholder((2 * (len(qs) + 1 + (2 * len(self.batch) if self.profile else 0)),), dtypes.uint64, device=(dev,),
volatile=True, tag="slots") for dev, qs in self.queues.items()}
def epilogue_queue(self, dev:str) -> str: return "COMPUTE:0" if len(self.queues[dev]) > 1 else self.queues[dev][0] # closes the device
def slot(self, devs:tuple[str, ...], i:int) -> UOp: return self.slots[devs[0]].shrink(((i, i + 1),)) # not [i:i+1]: the slice path is 10x the cost
def slot(self, devs:tuple[str, ...], i:int) -> UOp: return self.slots[devs[0]].shrink(((2 * i, 2 * i + 2),)) # not a slice: 10x the cost
def queue_signal(self, devs:tuple[str, ...], queue:str) -> UOp: return self.slot(devs, self.queues[devs[0]].index(queue))
def sched_timeline(self, devs:tuple[str, ...]) -> UOp: return self.slot(devs, len(self.queues[devs[0]]))
def stamps(self, devs:tuple[str, ...], tag:int) -> tuple[int, ...]: return (st:=len(self.queues[devs[0]])+1+2*tag, st + 1) if self.profile else ()
def _call_bufs(call:UOp) -> list[Any]:
def dep_buf(b:UOp) -> Any:
if (base:=(b.src[0] if b.op is Ops.MSELECT else b).storage_base).op is Ops.PARAM: return b if b.op is Ops.MSELECT else base
return cast(MultiBuffer, base.buffer).bufs[b.arg] if b.op is Ops.MSELECT else base.buffer
return [dep_buf(a) for a in get_call_arg_uops(call)]
def _wait_ins(ctx:BatchCtx, call:UOp, device:str, queue:str, tag:int) -> list[UOp]:
bufs, write = _call_bufs(call), get_call_outs_ins(call)[0]
bufs, write = list(get_call_arg_uops(call)), get_call_outs_ins(call)[0]
latest:dict[tuple[str, str], int] = {} # (producer device, queue) -> the latest submit tag to wait on, same-queue submits are fifo
for d, q, t in ctx.tracker.access_resources(bufs, list(range(len(bufs)) if write is None else write), (device, queue, tag)):
if t < tag and (d, q) != (device, queue): latest[(d, q)] = max(latest.get((d, q), 0), t)
# NV waits break QMD chaining, so also wait for the previous launch
if latest and device.split(":")[0] == "NV" and queue.startswith("COMPUTE") and (p:=ctx.prev[tag]) is not None: latest[(device, queue)] = p
ctx.signal_tags |= set(latest.values())
return [UOp(Ops.INS, arg=("wait", dtypes.void), src=(ctx.queue_signal((d,), q), UOp.const(t + 1, dtypes.uint64))) for (d, q), t in latest.items()]
@@ -200,7 +203,7 @@ def _emit_submits(ctx:BatchCtx, call_waits:list[list[UOp]]) -> tuple[list[UOp],
# and make hcq call
name, est = get_call_name(call, get_call_arg_uops(call)), estimate_uop(call)
kerns.append((devices, name, est, ctx.stamps(devices, tag), getattr(call.src[0].arg, "profile_key", None)))
kerns.append((devices, name, est, tuple(2 * s + 1 for s in ctx.stamps(devices, tag)), getattr(call.src[0].arg, "profile_key", None)))
ts_ins = [UOp(Ops.INS, arg=("timestamp", dtypes.void), src=(ctx.slot(devices, i),)) for i in ctx.stamps(devices, tag)]
q += ts_ins[:1] + [call] + ts_ins[1:]
@@ -224,9 +227,13 @@ def _finalize_batch(ctx:BatchCtx) -> UOp:
submits += [_epilogue(ctx, dev) for dev in ctx.queues]
fence = UOp.custom_function("hcq_fence", *[ctx.sched_timeline((dev,)) for dev in ctx.queues],
*[ctx.queue_signal((dev,), q) for dev, qs in ctx.queues.items() for q in qs])
merged = [m.after(fence) for m in _merge_queues(submits)]
merged:list[UOp] = [] # the submits in order, after the fence
for m in _merge_queues(submits): merged.append(m.after(fence, *merged[-1:]))
estimates = sum((estimate_uop(call) for call, _, _ in ctx.batch), start=Estimates()).simplify()
return UOp.sink(*merged, arg=KernelInfo("hcq_submit", estimates=estimates), tag=1).call(aux=HCQInfo(tuple(ctx.queues), kernels=tuple(kerns)))
sink = UOp.sink(*merged, arg=KernelInfo("hcq_submit", estimates=estimates), tag=1)
for pm in [Device[d].pm_batch for d in ctx.queues if Device[d].pm_batch is not None]: # a device adds its own work to the batch
if (r:=pm.rewrite(sink)) is not None: sink = r
return sink.call(aux=HCQInfo(tuple(ctx.queues), kernels=tuple(kerns)))
@rewrite_group(new_ctx=False)
def sched_batches(l:UOp, profile:bool) -> UOp:
@@ -283,25 +290,32 @@ def hcq_fence(ctx:EncodeCtx, f:UOp) -> UOp:
for i, dev in enumerate(ctx.devs):
slots, off = unwrap_view(lasts[i])
slots = patch(slots, [], bytes(slots.max_numel() * slots.dtype.itemsize)) # zeroed at link
done = timeline((dev,)).after(*last, loop:=UOp.loop(i)).index(0).load()
waited = done.end(loop, done < slots.index(off // slots.dtype.itemsize).load())
nxt = timeline_value((dev,)) + UOp.const(1, dtypes.uint64)
last = (timeline((dev,)).after(waited).index(1).store(nxt), slots.after(waited).index(off // slots.dtype.itemsize).store(nxt))
target = slots.after(*last, tv:=timeline_value((dev,))).index(off // slots.dtype.itemsize).load()
done = timeline((dev,)).after(target, loop:=UOp.loop(i)).index(0).load()
bumped = timeline((dev,)).after(done.end(loop, done < target)).index(1).store(nxt:=tv + UOp.const(1, dtypes.uint64))
last = (slots.after(bumped).index(off // slots.dtype.itemsize).store(nxt),)
# re-arm the signals
for sig in sigs:
base, off = unwrap_view(sig)
last = (base.after(*last).index(off // sig.dtype.itemsize).store(0),)
return last[0].barrier(*last[1:])
pm_hcq_encode = PatternMatcher([(UPat(Ops.CUSTOM_FUNCTION, arg="hcq_fence", name="f"), hcq_fence)])
pm_hcq_encode = PatternMatcher([
(UPat(Ops.CUSTOM_FUNCTION, arg="hcq_fence", name="f"), hcq_fence),
# after blocks are lowered, rechain stores saving original order
(UPat(Ops.AFTER, src=(UPat(dtype=dtypes.void, name="root"),), allow_any_len=True, name="a"),
lambda root, a: root.substitute({s.buf_uop: s.buf_uop.after(*a.src[1:]) for s in root.toposort() if s.op is Ops.STORE}, walk=True)),
])
# *****************
# 3.2. split
def _is_input_addr(g:UOp) -> bool:
base = unwrap_view(g.src[0])[0]
param = base.src[0].base if base.op is Ops.MSELECT else base # unwrap mselects
return param.op is Ops.PARAM and param.tag is None
if base.op is Ops.MSELECT: base = unwrap_view(base.src[0])[0] # a lane of a view
return base.op is Ops.PARAM and base.tag is None
def addrs_to_table(ctx:EncodeCtx, g:UOp) -> UOp|None:
if not _is_input_addr(g): return None
@@ -312,6 +326,7 @@ def addrs_to_table(ctx:EncodeCtx, g:UOp) -> UOp|None:
def _is_link_patch(w:UOp) -> bool:
if w.op is Ops.GETADDR: return not _is_input_addr(w)
if w.op is Ops.PARAM: return w.tag is not None
if w.op is Ops.BUFFER: return w.addrspace is AddrSpace.GLOBAL # a register is written at runtime
if w.op in {Ops.LOAD, Ops.AFTER} or w.is_variable: return False
return all(_is_link_patch(s) for s in w.src)
@@ -324,12 +339,7 @@ def hoist_links(ctx:EncodeCtx, a:UOp) -> UOp|None:
ctx.lt_patches.setdefault(unwrap_view(a.src[0])[0], []).extend(ws.substitute(sub).src)
return a.src[0].after(*rest)
pm_lower_body = PatternMatcher([
(UPat(Ops.GETADDR, name="g"), addrs_to_table),
(UPat(Ops.AFTER, name="a"), hoist_links),
(UPat(Ops.AFTER, src=(UPat(dtype=dtypes.void, name="root"),), allow_any_len=True, name="a"),
lambda root, a: root.substitute({s.buf_uop: s.buf_uop.after(*a.src[1:]) for s in root.toposort() if s.op is Ops.STORE}, walk=True)),
])
pm_patches = PatternMatcher([(UPat(Ops.GETADDR, name="g"), addrs_to_table), (UPat(Ops.AFTER, name="a"), hoist_links)])
def patch(buf:UOp, rows:list[tuple[int, UOp]], blob:bytes|None=None) -> UOp:
groups:dict[tuple[DType, int, bool], list[tuple[int, UOp]]] = {} # split by: dtype, alignment, is_link (rt/lt can't share a store)
@@ -365,9 +375,9 @@ def lower_call(call:UOp) -> UOp|None:
# encode bodies
ctx = EncodeCtx(call.arg.aux.device)
pm = sum([Device[d].pm_encode for d in dedup([d.split(":")[0] for d in ctx.devs])], pm_hcq_encode)
body = graph_rewrite(call.src[0], pm, ctx=ctx, walk=True, name="encode body")
body = graph_rewrite(body, pm_lower_body, ctx=ctx, name="lower body")
devs = [Device[d] for d in dedup([d.split(":")[0] for d in ctx.devs])]
body = graph_rewrite(call.src[0], sum([d.pm_encode for d in devs], PatternMatcher([])) + pm_hcq_encode, ctx=ctx, bpm=pm_patches, name="encode")
body = graph_rewrite(body, sum([d.pm_lower for d in devs if d.pm_lower is not None], PatternMatcher([])), ctx=ctx, bpm=pm_patches, name="lower")
# resize table
body = body.substitute({ctx.table: (table:=UOp.placeholder((len(ctx.inputs),), dtypes.uint64, device="CPU", tag="inputs"))})
@@ -397,46 +407,47 @@ def lower_call(call:UOp) -> UOp|None:
return call.replace(src=(sink, *args), arg=replace(call.arg, aux=info))
pm_encode = PatternMatcher([(UPat(Ops.CALL, src=(UPat(Ops.SINK),), name="call", allow_any_len=True), lower_call)])
hcq_compile_cache:dict[tuple[UOp, bool, bool], UOp] = {} # uops are hash-consed: the linear itself is the key, plus whether inputs bind
hcq_compile_cache:dict[tuple[UOp, bool], UOp] = {} # eager templates: a buffer-free linear (uops are hash-consed) to its compiled form
@rewrite_group(lambda linear,input_uops,profile,ret: f"HCQ Compile {pluralize('Kernel', len(ret.src))}")
def hcq_compile(linear:UOp, input_uops:list[UOp]|None, profile:bool) -> UOp:
if input_uops is not None:
slots = {u:i for i,u in reversed(tuple(enumerate(input_uops)))}
linear = graph_rewrite(linear, pm_replace_buffers, ctx=(input_uops, slots), walk=True, name="replace buffer")
if any(isinstance(getattr(c.without_after.arg, "aux", None), HCQInfo) for c in linear.src): return linear # compiled already
# TODO: this needs a cleanup
bufmap = {s.param_like(i): s for i,s in enumerate(input_uops)} if input_uops is not None else {}
if (final_linear:=(hcq_compile_cache.get(cache_key:=(linear, profile, input_uops is None)))) is None:
lin = graph_rewrite(linear.substitute(bufmap, walk=True), pm_unwrap_multi+pm_insert_copy_staging+pm_flatten_linear, name="prep calls")
lin = sched_batches(lin, profile)
lin = graph_rewrite(lin, pm_encode, walk=True, name="encode")
with Context(EMULATED_DTYPES=""): final_linear = lower_and_compile(lin).substitute({v: k for k, v in bufmap.items()}, walk=True)
if final_linear is not linear: hcq_compile_cache[cache_key] = final_linear
return final_linear.substitute(bufmap, walk=True)
if input_uops is not None:
use_rt = len(linear.src) < HCQ_CACHE_THRESH # small schedules use runtime address patches so linked schedules can be cached without input buffers
slots = {u:i for i,u in reversed(tuple(enumerate(input_uops)))}
linear = graph_rewrite(linear, pm_replace_buffers, ctx=(use_rt, input_uops, slots), walk=True, name="replace buffers")
if (cached:=hcq_compile_cache.get(key:=(linear, profile))) is not None: return cached
lin = graph_rewrite(linear, pm_unwrap_multi+pm_insert_copy_staging+pm_flatten_linear, name="prep calls")
lin = sched_batches(lin, profile)
lin = graph_rewrite(lin, pm_encode, walk=True, name="encode")
with Context(EMULATED_DTYPES=""): final_linear = lower_and_compile(lin)
if input_uops is not None and final_linear is not linear: hcq_compile_cache[key] = final_linear
return final_linear
# *****************
# 5. bufferize placeholders
# 5. link
def bufferize_buf(ctx:bool, b:UOp) -> UOp|None: # ctx: a kept link (the jit's) owns the linear's buffers, a one-shot borrows ring slots
@dataclass
class LinkCtx: inputs:dict[UOp, UOp]; use_rt:bool; refs:list[UOp] = field(default_factory=list) # noqa: E702
def bufferize_buf(ctx:LinkCtx, b:UOp) -> UOp|None: # ctx: a kept link (the jit's) owns the linear's buffers, a one-shot borrows ring slots
if b.tag is None: return None # a param, not a placeholder
dev = cast(HCQ2Compiled, Device[to_tuple(b.device)[0]])
if b.arg.slot == 0 or b.tag == "program": r = cast(Buffer, unwrap(dev.pm_bufferize.rewrite(b, ctx=dev))) # device state and programs
elif ctx: r = Buffer(dev.device, b.max_numel(), b.dtype, options=BufferSpec(host=b.arg.volatile, uncached=True, cpu_access=True), preallocate=True)
# device owns the placeholders it names
if (r:=cast(Buffer|None, dev.pm_bufferize.rewrite(b, ctx=dev))) is not None: pass
elif not ctx.use_rt:
r = Buffer(dev.device, b.max_numel(), b.dtype, options=BufferSpec(host=b.arg.volatile, uncached=True, cpu_access=True), preallocate=True)
else: r = dev.rt_view(b.max_numel() * b.dtype.itemsize, b.dtype, host=b.arg.volatile)
return UOp.from_buffer(r, HCQ_RUNTIME_DEV.value)
pm_bufferize_placeholders = PatternMatcher([(UPat(Ops.PARAM, name="b"), bufferize_buf)])
# *****************
# 6. link
def resolve_getaddr(ctx:list[UOp], g:UOp) -> UOp|None:
def resolve_getaddr(ctx:LinkCtx, g:UOp) -> UOp|None:
buf, off = unwrap_view(g.src[0])
if buf.op not in {Ops.BUFFER, Ops.MSELECT}: return None
ctx.append(buf) # add to refs
ctx.refs.append(buf) # add to refs
return UOp.const(cast(Buffer, buf.buffer).get_buf(to_tuple(g.arg)[0]).va_addr + off, dtypes.uint64)
def fold_binary(buf:UOp, blob:UOp) -> UOp:
@@ -454,24 +465,30 @@ def fold_words(buf:UOp, offs:UOp, ws:UOp) -> UOp:
return UOp(Ops.NOOP)
pm_link = PatternMatcher([
(UPat(Ops.CAST, src=(UPat(Ops.CAST, src=(UPat.cvar(),), name="inner"),), name="c"), lambda c, inner: inner.src[0].cast(c.dtype)),
(UPat(Ops.PARAM, name="b"), lambda ctx, b: ctx.inputs[b] if b in ctx.inputs else bufferize_buf(ctx, b)),
(UPat(Ops.GETADDR, name="g"), resolve_getaddr),
(UPat(GroupOp.ALU, src=UPat.cvar().or_casted(), name="a"),
lambda a: UOp.const(exec_alu(a.op, a.dtype, [s.val for s in a.src], False), a.dtype)),
(UPat(name="buf").store(UPat.any(UPat(Ops.BINARY, name="blob"), UPat(Ops.BINARY, name="blob").bitcast())), fold_binary),
(UPat(name="buf").index(UPat(Ops.STACK, src=UPat.cvar().or_casted(), name="offs")).store(UPat(Ops.STACK, src=UPat.cvar().or_casted(), name="ws")),
fold_words),
(UPat(Ops.AFTER, name="a"), lambda a: None if a.is_bound_var else a.src[0] if all(s.op is Ops.NOOP for s in a.src[1:]) else
panic(RuntimeError, f"unresolved link words on {a.src[0].op}")),
(UPat(Ops.AFTER, name="a"), lambda a: None if a.is_bound_var or a.src[0].op is Ops.CALL else
a.src[0] if all(s.op is Ops.NOOP for s in a.src[1:]) else panic(RuntimeError, f"unresolved link words on {a.src[0].op}")),
])
link_linear_cache:weakref.WeakKeyDictionary[UOp, UOp] = weakref.WeakKeyDictionary() # a baked link lives as long as its bound linear
@rewrite_group(lambda _,cache,ret: f"HCQ Link {pluralize('Kernel', len(ret.src))}")
def hcq_link(linear:UOp, cache=True) -> UOp:
if (linked:=link_linear_cache.get(linear)) is not None: return linked
bufferized = graph_rewrite(linear, pm_bufferize_placeholders, ctx=cache, name="bufferize")
linked = graph_rewrite(bufferized, pm_link, ctx=(refs:=list[UOp]()), bottom_up=False, name="link")
if refs: linked = linked.replace(src=(linked.src[0].after(*dedup(refs)), *linked.src[1:])) # attach refs to linear
@rewrite_group(lambda _,input_uops=None,allow_cache=True,ret=None: f"HCQ Link {pluralize('Kernel', len(ret.src))}")
def hcq_link(linear:UOp, input_uops:list[UOp]|None=None, allow_cache=True) -> UOp:
if allow_cache and (linked:=link_linear_cache.get(linear)) is not None: return linked
# if we have any link time buffers, do not cache this linear
cache = allow_cache and not any(u.tag == "lt_input" for u in linear.toposort() if u.op is Ops.PARAM)
inputs = {UOp.param(i, b.dtype, b.max_numel(), b.device).replace(tag="lt_input"): b for i, b in enumerate(input_uops or ())}
linked = graph_rewrite(linear, pm_link, ctx=(ctx:=LinkCtx(inputs, use_rt=allow_cache and not cache)), walk=True, name="link")
if ctx.refs: linked = linked.replace(src=(linked.src[0].after(*dedup(ctx.refs)), *linked.src[1:])) # attach refs to linear
if cache and linked is not linear: link_linear_cache[linear] = linked
return linked
@@ -481,6 +498,7 @@ def hcq_link(linear:UOp, cache=True) -> UOp:
class HCQ2Compiled(Compiled):
timestamp_divider: float = 1000.0
wait_timeout_ms: float = 30000.0
sleep_timeout_ms: int|None = None
rt_nbytes: int = 64 << 20 # the pool every per-linear buffer is carved out of
pm_encode: PatternMatcher = PatternMatcher([]) # the backend's own encode rules, matched by its submit names
var_vals: dict[str, int] = {}
@@ -500,8 +518,10 @@ class HCQ2Compiled(Compiled):
self.prof_ents:dict[tuple[Buffer, int], ProfileGraphEntry] = {} # (a batch's timestamps, start slot) -> entry, read at synchronize
@functools.cached_property
def timeline(self) -> Buffer: # [the signal, the value the last submitted batch signals]: zeroed host memory
return Buffer(self.device, 2, dtypes.uint64, options=BufferSpec(host=True, uncached=True, cpu_access=True), preallocate=True)
def timeline(self) -> Buffer: # [the signal, the value the last submitted batch signals]
buf = Buffer(self.device, 2, dtypes.uint64, options=BufferSpec(host=True, uncached=True, cpu_access=True), preallocate=True)
buf._buf.cpu_view().view(fmt='B')[:16] = bytes(16)
return buf
def collect_prof(self):
if PROFILE:
@@ -532,7 +552,7 @@ class HCQ2Compiled(Compiled):
return Buffer(self.device, self.rt_allocator(uncached, host).size, dtypes.uint8, options=spec, preallocate=True)
def rt_view(self, nbytes:int, dtype:DType=dtypes.uint8, uncached:bool=True, host:bool=False) -> Buffer: # a slot of the ring, wraps silently
off = self.rt_allocator(uncached, host).alloc(max(nbytes, 1), alignment=128)
off = self.rt_allocator(uncached, host).alloc(max(nbytes, 1), alignment=256)
return self.rt_buffer(uncached, host).view(nbytes // dtype.itemsize, dtype, off).ensure_allocated()
def _wait_signal(self, sig:MMIOInterface|memoryview, value:int, timeout:int|None=None):
@@ -540,7 +560,8 @@ class HCQ2Compiled(Compiled):
st, done = time.perf_counter(), sig[0]
while done < value:
if done != (done:=sig[0]): st = time.perf_counter()
elif time.perf_counter() - st > (timeout or self.wait_timeout_ms) / 1000: self.on_device_hang()
elif (elapsed:=time.perf_counter() - st) > (timeout or self.wait_timeout_ms) / 1000: self.on_device_hang()
elif self.sleep_timeout_ms is not None and elapsed > self.sleep_timeout_ms / 1000: self.on_sleep()
def synchronize(self, timeout:int|None=None):
self._wait_signal(tl:=self.timeline._buf.cpu_view().view(fmt='Q'), tl[1], timeout)
@@ -548,6 +569,9 @@ class HCQ2Compiled(Compiled):
def on_device_hang(self): raise RuntimeError(f"{self.device} hang detected")
def on_sleep(self):
if (iface:=getattr(self, "iface", None)) is not None and hasattr(iface, "sleep"): iface.sleep(self.sleep_timeout_ms)
def device_props(self) -> dict[str,Any]: return {} # to be overridden if needed. dict keys are backend dependent.
def _is_cpu(self) -> bool: return hasattr(self, 'device') and self.device.split(":")[0] == "CPU"
@@ -567,8 +591,7 @@ class HCQ2Buffer:
return HCQ2Buffer(self.va_addr+offset, meta=self.meta, view=(self.view.view(offset=offset, size=size) if self.view is not None else None))
class HCQAllocator(LRUAllocator[HCQDeviceType], Generic[HCQDeviceType]):
def _as_buffer(self, buf:HCQBuffer) -> memoryview:
return unwrap(buf.view).mv
def _as_buffer(self, buf:HCQBuffer) -> memoryview|None: return buf.view.mv if buf.view is not None else None
def _copyout(self, dest:memoryview, src:HCQBuffer): # TODO: remove with memcpy on cpu worker?
self.dev.synchronize()
+5 -150
View File
@@ -1,7 +1,6 @@
from __future__ import annotations
import os, mmap, array, functools, ctypes, ctypes.util, select, contextlib, dataclasses, sys, itertools, struct, socket
import subprocess, time, enum, atexit
from tinygrad.helpers import round_up, getenv, OSX, temp, ceildiv, unwrap, fetch, system, _ensure_downloads_dir, DEBUG, flatten, pluralize
import os, mmap, array, functools, ctypes, ctypes.util, select, contextlib, dataclasses, sys, struct, socket
from tinygrad.helpers import round_up, getenv, OSX, temp, ceildiv, DEBUG, pluralize
from tinygrad.runtime.autogen import libc, pci, vfio
from tinygrad.runtime.support.hcq import FileIOInterface, MMIOInterface, HCQBuffer, hcq_filter_visible_devices
from tinygrad.runtime.support.memory import VirtMapping, AddrSpace, BumpAllocator
@@ -81,8 +80,7 @@ class _System:
@functools.cache
def list_devices(self, vendor:int, devices:tuple[tuple[int, tuple[int, ...]], ...], base_class:int|None=None):
if getenv("REMOTE", ""): return [(functools.partial(RemotePCIDevice,sock=s), x) for s,x in RemotePCIDevice.remote_list(vendor,devices,base_class)]
return [(APLRemotePCIDevice if OSX else PCIDevice, x) for x in System.pci_scan_bus(vendor, devices, base_class)]
return [(PCIDevice, x) for x in System.pci_scan_bus(vendor, devices, base_class)]
def pci_probe_device(self, device:str, dev_id:int, vendor:int, devices:tuple[tuple[int, tuple[int, ...]], ...], base_class:int|None=None):
try: cl, pcibus = (ds:=hcq_filter_visible_devices(self.list_devices(vendor, devices, base_class), device))[dev_id]
@@ -253,13 +251,12 @@ class PCIAllocationMeta: mapping:VirtMapping; has_cpu_mapping:bool; hMemory:int=
class PCIIfaceBase:
@property
def peer_group(self) -> str: return getattr(self.pci_dev, 'peer_group', type(self.pci_dev).__name__)
def is_local(self) -> bool: return not isinstance(self.pci_dev, RemotePCIDevice)
def is_bar_small(self) -> bool: return self.pci_dev.bar_info(self.vram_bar)[1] == (256 << 20)
def __init__(self, dev, dev_id, vendor, devices:tuple[tuple[int, tuple[int, ...]], ...], vram_bar, va_start, va_size,
dev_impl_t, base_class:int|None=None):
self.pci_dev = System.pci_probe_device(dn:=dev.__class__.__name__[:-6], dev_id, vendor, devices, base_class=base_class)
if self.is_local(): System.reserve_va(va_start, va_size)
System.reserve_va(va_start, va_size)
with contextlib.suppress(Exception): self.pci_dev.resize_bar(vram_bar)
self.dev_impl = dev_impl_t(self.pci_dev)
self.dev, self.vram_bar, self.count = dev, vram_bar, len(hcq_filter_visible_devices(System.list_devices(vendor, devices, base_class), dn))
@@ -283,15 +280,13 @@ class PCIIfaceBase:
def free(self, b:HCQBuffer):
if b.owner != self.dev: self.dev.iface.dev_impl.mm.unmap_range(b.va_addr, round_up(b.size, 0x1000))
if b.owner == self.dev and b.meta.mapping.aspace is AddrSpace.PHYS: self.dev_impl.mm.vfree(b.meta.mapping)
if b.owner == self.dev and self.is_local() and b.meta.has_cpu_mapping: FileIOInterface.munmap(b.va_addr, b.size)
if b.owner == self.dev and b.meta.has_cpu_mapping: FileIOInterface.munmap(b.va_addr, b.size)
def p2p_paddrs(self, paddrs:list[tuple[int,int]]) -> tuple[list[tuple[int,int]], AddrSpace]:
return [(p + self.pci_dev.bar_info(self.vram_bar)[0], sz) for p, sz in paddrs], AddrSpace.SYS
def map(self, b:HCQBuffer):
if b.owner is not None and b.owner._is_cpu():
if not self.is_local(): raise RuntimeError(f"P2P mapping not supported for remote devices: {b.owner} -> {self.dev}")
System.lock_memory(int(b.va_addr), b.size)
paddrs, aspace = [(x, 0x1000) for x in System.system_paddrs(int(b.va_addr), round_up(b.size, 0x1000))], AddrSpace.SYS
snooped, uncached = True, True
@@ -305,143 +300,3 @@ class PCIIfaceBase:
self.dev_impl.mm.map_range(int(b.va_addr), round_up(b.size, 0x1000), paddrs, aspace=aspace, snooped=snooped, uncached=uncached)
return HCQBuffer(b.va_addr, b.size, meta=b.meta, owner=b.owner)
# *** Remote PCI Devices
class RemoteCmd(enum.IntEnum):
PROBE,MAP_BAR,MAP_SYSMEM_FD,CFG_READ,CFG_WRITE,RESET,MMIO_READ,MMIO_WRITE,MAP_SYSMEM,SYSMEM_READ,SYSMEM_WRITE,RESIZE_BAR,PING = range(13)
class RemoteMMIOInterface(MMIOInterface):
def __init__(self, dev:RemotePCIDevice, residx:int, nbytes:int, fmt='B', off=0, rd_cmd=RemoteCmd.MMIO_READ, wr_cmd=RemoteCmd.MMIO_WRITE):
self.dev, self.residx, self.nbytes, self.fmt, self.off, self.el_sz = dev, residx, nbytes, fmt, off, struct.calcsize(fmt)
self.rd_cmd, self.wr_cmd = rd_cmd, wr_cmd
def __getitem__(self, index):
sl = index if isinstance(index, slice) else slice(index, index + 1)
start, stop = (sl.start or 0) * self.el_sz, (sl.stop or len(self)) * self.el_sz
data = self.dev._bulk_read(self.rd_cmd, self.residx, self.off + start, stop - start)
result = data if self.fmt == 'B' else list(struct.unpack(f'<{(stop - start) // self.el_sz}{self.fmt}', data))
return result if isinstance(index, slice) else result[0]
def __setitem__(self, index, val):
start = (index.start or 0) * self.el_sz if isinstance(index, slice) else index * self.el_sz
data = (val if self.fmt == 'B' else struct.pack(f'<{len(val)}{self.fmt}', *val)) if isinstance(index, slice) else struct.pack(f'<{self.fmt}', val)
self.dev._bulk_write(self.wr_cmd, self.residx, self.off + start, data)
def view(self, offset:int=0, size:int|None=None, fmt=None):
return RemoteMMIOInterface(self.dev, self.residx, size or (self.nbytes - offset), fmt or self.fmt, self.off + offset, self.rd_cmd, self.wr_cmd)
class RemotePCIDevice(PCIDevice):
_bulk_sent:int = 0
_bulk_recv:int = 0
_rpc_count:int = 0
_start_time:float = 0.0
@staticmethod
@functools.cache
def remote_sock(host:str, port:int) -> socket.socket:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
sock.settimeout(getenv("REMOTE_TIMEOUT", 3))
sock.connect((host, port))
sock.settimeout(None)
if DEBUG >= 1 and RemotePCIDevice._start_time == 0.0:
RemotePCIDevice._start_time = time.perf_counter()
def _print_stats():
dt = time.perf_counter() - RemotePCIDevice._start_time
sent_mb, recv_mb = RemotePCIDevice._bulk_sent / 1e6, RemotePCIDevice._bulk_recv / 1e6
print(f"remote: sent {sent_mb:,.2f} MB ({sent_mb/dt:,.2f} MB/s), recv {recv_mb:,.2f} MB ({recv_mb/dt:,.2f} MB/s), "
f"{RemotePCIDevice._rpc_count:,} roundtrips in {dt:.2f}s")
atexit.register(_print_stats)
return sock
@staticmethod
@functools.cache
def remote_list(vendor:int, devices:tuple[tuple[int, tuple[int, ...]], ...], base_class:int|None) -> list[tuple[socket.socket, str]]:
payload = array.array('I', itertools.chain.from_iterable((m, d) for m, ds in devices for d in ds)).tobytes()
def q(r:str) -> list[tuple[socket.socket, str]]:
sock = RemotePCIDevice.remote_sock((host:=r.strip().split(":")[0]), (port:=int(r.strip().split(":")[1]) if ":" in r else 6667))
data_len, _, _, _ = RemotePCIDevice._rpc(sock, 0, RemoteCmd.PROBE, base_class or 0, len(payload), vendor, payload=payload)
return [(sock, f"remote:{host}:{port}:{d}") for d in RemotePCIDevice._recvall(sock, data_len).decode().split('\n')]
return flatten([q(r) for r in getenv("REMOTE", "").split(",") if r.strip()])
@staticmethod
def _recvall(sock:socket.socket, n:int) -> bytes:
data = b''
while len(data) < n and (chunk:=sock.recv(n - len(data))): data += chunk
if len(data) < n: raise RuntimeError("Connection closed")
return data
@staticmethod
def _rpc(sock:socket.socket, dev_id:int, cmd:int, *args:int, bar:int=0, readout_size:int=0, payload:bytes=b'', has_fd=False):
sock.sendall(struct.pack('<BIIQQQ', cmd, dev_id, bar, *(*args, 0, 0, 0)[:3]) + payload)
if has_fd:
msg, anc, _, _ = sock.recvmsg(17, socket.CMSG_LEN(4))
fd = struct.unpack('<i', anc[0][2][:4])[0]
else: msg, fd = RemotePCIDevice._recvall(sock, 17), None
if (resp:=struct.unpack('<BQQ', msg))[0] != 0:
raise RuntimeError(f"RPC failed: {RemotePCIDevice._recvall(sock, resp[1]).decode('utf-8') if resp[1] > 0 else 'unknown error'}")
RemotePCIDevice._rpc_count += 1
return (resp[1], resp[2]) + ((RemotePCIDevice._recvall(sock, readout_size) if readout_size > 0 else None),) + (fd,)
def __init__(self, devpref:str, pcibus:str, sock:socket.socket):
self.sock, self.pcibus, self.dev_id = sock, pcibus, int(pcibus.split(':')[-1]) if ':' in pcibus else 0
self.peer_group = sock.getpeername()[0]
for buft in [socket.SO_SNDBUF, socket.SO_RCVBUF]: self.sock.setsockopt(socket.SOL_SOCKET, buft, 64 << 20)
self.lock_fd = System.flock_acquire(f"{devpref.lower()}_{pcibus.lower()}.lock")
def _bulk_read(self, cmd:int, idx:int, offset:int, size:int) -> bytes:
RemotePCIDevice._bulk_recv += size
return unwrap(self._rpc(self.sock, self.dev_id, cmd, offset, size, bar=idx, readout_size=size)[2])
def _bulk_write(self, cmd:int, idx:int, offset:int, data:bytes):
RemotePCIDevice._bulk_sent += len(data)
self.sock.sendall(struct.pack('<BIIQQQ', cmd, self.dev_id, idx, offset, len(data), 0) + data)
def alloc_sysmem(self, size:int, vaddr:int=0, contiguous:bool=False) -> tuple[MMIOInterface, list[int]]:
paddrs_len, handle, _, _ = self._rpc(self.sock, self.dev_id, RemoteCmd.MAP_SYSMEM, size, int(contiguous))
paddrs = list(struct.unpack(f'<{paddrs_len // 8}Q', self._recvall(self.sock, paddrs_len)))
return RemoteMMIOInterface(self, handle, size, fmt='B', rd_cmd=RemoteCmd.SYSMEM_READ, wr_cmd=RemoteCmd.SYSMEM_WRITE), paddrs
def reset(self): self._rpc(self.sock, self.dev_id, RemoteCmd.RESET)
def read_config(self, offset:int, size:int): return self._rpc(self.sock, self.dev_id, RemoteCmd.CFG_READ, offset, size)[0]
def write_config(self, offset:int, value:int, size:int): self._rpc(self.sock, self.dev_id, RemoteCmd.CFG_WRITE, offset, size, value)
@functools.cache
def bar_info(self, bar_idx:int) -> tuple[int, int]: return self._rpc(self.sock, self.dev_id, RemoteCmd.MAP_BAR, bar=bar_idx)[:2]
def map_bar(self, bar:int, off:int=0, addr:int=0, size:int|None=None, fmt='B') -> MMIOInterface:
return RemoteMMIOInterface(self, bar, size or self.bar_info(bar)[1], fmt).view(off, size, fmt)
def resize_bar(self, bar_idx:int): self._rpc(self.sock, self.dev_id, RemoteCmd.RESIZE_BAR, bar=bar_idx)
class APLRemotePCIDevice(RemotePCIDevice):
APP_PATH = "/Applications/TinyGPU.app/Contents/MacOS/TinyGPU"
@classmethod
def ensure_app(cls):
commit = "c0d024f9ff0e1dc8fdf217f255da7101d91e8323"
app_name = f"TinyGPU_{commit}.zip"
if (_ensure_downloads_dir() / app_name).is_file() and os.path.exists(cls.APP_PATH): return
print("Downloading TinyGPU.app...")
with contextlib.suppress(RuntimeError): system("pkill -f TinyGPU")
system(f"ditto -xk {fetch(f'https://github.com/tinygrad/tinygpu_releases/raw/{commit}/TinyGPU.zip', name=app_name)} /Applications")
print(system(f"{cls.APP_PATH} install"))
def __init__(self, devpref:str, pcibus:str):
self.ensure_app()
sock_path, sock = getenv("APL_REMOTE_SOCK", temp("tinygpu.sock")), socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
for i in range(100):
with contextlib.suppress(ConnectionRefusedError, FileNotFoundError):
sock.connect(sock_path)
break
if i == 0: subprocess.Popen([self.APP_PATH, "server", sock_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(0.05)
else: raise RuntimeError(f"Failed to connect to TinyGPU server at {sock_path}.")
super().__init__(devpref, "usb4", sock=sock)
def alloc_sysmem(self, size:int, vaddr:int=0, contiguous:bool=False) -> tuple[MMIOInterface, list[int]]:
mapped_size, _, _, fd = self._rpc(self.sock, self.dev_id, RemoteCmd.MAP_SYSMEM_FD, size, int(contiguous), has_fd=True)
memview = MMIOInterface(FileIOInterface(fd=fd).mmap(0, mapped_size, mmap.PROT_READ | mmap.PROT_WRITE, mmap.MAP_SHARED, 0), mapped_size, fmt='B')
# paddrs are returned as (paddr, size) pairs until a (paddr=0, size=0) terminator in the beginning of the mapping.
paddrs_raw = list(itertools.takewhile(lambda p: p[1] != 0, zip(memview.view(fmt='Q')[0::2], memview.view(fmt='Q')[1::2])))
return memview, [p + i for p, sz in paddrs_raw for i in range(0, sz, 0x1000)][:ceildiv(size, 0x1000)]
+5 -8
View File
@@ -30,14 +30,14 @@ def create_schedule(sched_sink:UOp) -> UOp:
# build kernel dependency graph: edges from producer kernel to consumer kernels
children: dict[UOp, list[UOp]] = {}
in_degree: dict[UOp, int] = {}
writes: dict[UOp, list[tuple[UOp, UOp, tuple[UOp, ...]]]] = {} # buffer -> (AFTER, prior state, new kernels)
writes: dict[UOp, list[tuple[UOp, tuple[UOp, ...]]]] = {} # superseded state -> (AFTER, new kernels)
reads: list[tuple[UOp, UOp, UOp]] = [] # (reader AFTER, reader kernel, buffer state read)
for u in sched_sink.toposort(gate_kernel_sink):
if u.op is not Ops.AFTER: continue
kernels, after_deps = _split_after(u)
prev_state = _unwrap_src(u.src[0])
prev_kernels = set(_split_after(prev_state)[0]) if prev_state.op is Ops.AFTER else set()
writes.setdefault(u.buf_uop, []).append((u, prev_state, tuple(k for k in kernels if k not in prev_kernels)))
writes.setdefault(prev_state, []).append((u, tuple(k for k in kernels if k not in prev_kernels)))
for k in kernels:
in_degree.setdefault(k, 0)
if k.op is Ops.END: assert k.src[0].op is Ops.CALL, f"END src[0] should be KERNEL, not {k.src[0].op}"
@@ -53,8 +53,8 @@ def create_schedule(sched_sink:UOp) -> UOp:
# WAR deps: a kernel reading buffer state S must run before another write that supersedes S. an AFTER only
# supersedes its immediate prior state; join members already present in that prior state are ordering deps, not writes
for u, k, s in reads:
for a, prev_state, write_kernels in writes.get(s.buf_uop, []):
if a is u or prev_state is not s: continue
for a, write_kernels in writes.get(s, []):
if a is u: continue
for t in write_kernels:
if t is not k and t not in k.backward_slice:
children.setdefault(k, []).append(t)
@@ -126,11 +126,8 @@ def lower_sink_to_linear(call:UOp) -> UOp|None:
cache_key = function.key
if not SCACHE or (sc_ret:=schedule_cache.get(cache_key, None)) is None:
if SPEC: type_verify(function, spec_tensor)
# prepare can introduce new opaque CALLs (transform_precompiled_call turns precompiled calls opaque here): lower
# nested calls first so create_schedule doesn't look through their unscheduled bodies
# support recursive CALLs
prepared = graph_rewrite(prepare_rangeify(function), pm_schedule, name="schedule nested calls", enter_calls=True)
linear = create_schedule(get_kernel_graph(prepared))
linear = create_schedule(get_kernel_graph(prepare_rangeify(function)))
if SCACHE: schedule_cache[cache_key] = linear
else:
# schedule cache hit
+1 -96
View File
@@ -8,57 +8,6 @@ from tinygrad.schedule.indexing import apply_movement_op
from tinygrad.schedule.allreduce import create_allreduce_function
from tinygrad.schedule.multi import multi_pm
# args without storage: fold movement ops that collapse to a contiguous range over a buffer into a static view of
# the base buffer (like contiguous_mops_to_view); everything else materializes with CONTIGUOUS
def arg_with_storage(a:UOp) -> UOp:
if a.has_buffer_identity(after_ok=True): return a
if all_int(a.shape) and len(a.shape) > 0 and (cv := a.contiguous_view()) is not None and (b := cv[0]).op in {Ops.BUFFER, Ops.PARAM}:
return b[cv[1]:cv[1] + a.max_numel() * a.element_size() // b.element_size()].bitcast(a.dtype).reshape(a.shape)
return a.contiguous()
def transform_precompiled_call(c:UOp) -> UOp|None:
if c.arg is None or not c.arg.precompile or not c.has_unbound_outputs: return None
assert c.src[0].op is Ops.SINK, "precompiled call bodies are SINKs of stores into the output PARAMs"
# the RETURNED srcs are the call outputs (slots are src positions)
ret_pos = [p for p,a in enumerate(c.src[1:]) if a.unsharded_base.is_unbound]
srcs = tuple(st.src[1] for st in c.src[0].src if st.op is Ops.STORE)
# add the outputs to the call
outs = tuple(c.src[1+p].empty_like() for p in ret_pos)
targets = [o.param_like(p).shrink_to(s.shape) for p,o,s in zip(ret_pos, outs, srcs)]
# how each stored value lands in its output PARAM target: a CONTIGUOUS materializes straight into the target and
# a real buffer/UNSHARD rebinds its storage to the target (once per unique value); everything else is copied into it
placed:dict[UOp, UOp] = {}
items:list[UOp] = []
for s, t in zip(srcs, targets):
deps:list[UOp] = []
while s.op is Ops.AFTER:
deps.extend(s.src[1:])
s = s.src[0]
if s not in placed:
if s.op is Ops.CONTIGUOUS: placed[s] = t.after(t.store(s.src[0]))
elif s.op in {Ops.BUFFER, Ops.UNSHARD} and s.has_buffer_identity(): placed[s] = t
if s in placed:
items.append(s.after(*deps))
continue
items.append(t.after(t.store(s.after(*deps))))
# swap every placed value for its target storage, also inside other stores' AFTER deps
fxn = UOp.sink(*(x.substitute(placed) for x in items))
# all bodies are SINKs now, the node just becomes an opaque CALL: outs take the RETURNEDs' places; afters on real
# buffers are the input storage, afters on RETURNED placeholders have no storage yet, materialize them
rmap = dict(zip(ret_pos, outs))
new_call = c.replace(src=(fxn, *[rmap.get(i, arg_with_storage(a)) for i, a in enumerate(c.src[1:])]))
rets = tuple(o.after(new_call) for o in outs)
# if the CALL has symbolic shapes, shrink the max-sized output to the actual symbolic shape
# NOTE: must use the resolved shapes of the RETURNED placeholders (which substitute PARAMs with external args), not raw body shapes
rets = tuple(r.shrink_to(rs.shape) for r,rs in zip(rets, (c.src[1+p] for p in ret_pos)))
# the AFTER outputs resolve against this: stores of each real output into its RETURNED placeholder
return UOp.sink(*[c.src[1+p].store(v) for p, v in zip(ret_pos, rets)])
def walk_mop(u:UOp):
if u.op in GroupOp.Movement or u.op in {Ops.INDEX, Ops.UNSHARD, Ops.BITCAST}: return walk_mop(u.src[0])
if u.op is Ops.AFTER and (b:=walk_mop(u.src[0])) is not u.src[0]: return b.after(*u.src[1:])
@@ -181,9 +130,6 @@ def expand_bitcast(bc:UOp) -> UOp|None:
return parts[0].stack(*parts[1:], dim=-1).flatten(-2).cast(new_uint).bitcast(bc.dtype)
earliest_rewrites = mop_cleanup+PatternMatcher([
# transform precompiled value-producing calls into opaque CALLs (outputs become real buffers)
(UPat(Ops.CALL, name="c"), transform_precompiled_call),
# resolve calls with RETURNED inputs (inline the body)
(UPat(Ops.CALL, name="c"), lambda c: resolve_function(c) if c.has_unbound_outputs else None),
@@ -265,51 +211,10 @@ pm_copy_to_store = PatternMatcher([
(UPat(Ops.COPY, name="copy"), convert_copy_to_store),
])
def bind_call_outputs(sink:UOp) -> UOp:
"""fresh outputs of opaque precompiled calls directly stored into final storage: bind the storage into the call's
output positions and drop the stores; the call then writes the storage directly, no copy kernel (like
transform_precompiled_call giving outputs real buffers). done for all consumers of a call at once (all or
nothing) so the call stays a single kernel instance.
NOTE: output positions aren't recorded on the opaque call, so this treats any arg with an AFTER(call) consumer as
an output. that's sound here: the only AFTERs on call args come from the transform's resolved outputs inputs
are read through their own states"""
parents: dict[UOp, list[UOp]] = {}
uses: dict[UOp, int] = {}
for u in sink.toposort(enter_calls=False):
for s in u.src:
parents.setdefault(s, []).append(u)
uses[s] = uses.get(s, 0)+1
subs: dict[UOp, UOp] = {}
for call in [u for u in uses if u.op is Ops.CALL and u.arg is not None and u.arg.precompile and not u.has_unbound_outputs]:
# every AFTER consumer of the call's outputs must be a single-use direct store into bindable final storage
items: list[tuple[UOp, UOp, UOp, UOp]] = []
for after in [a for a in parents.get(call, []) if a.op is Ops.AFTER and a.src[0] in call.src[1:]]:
out = after.src[0]
# dst must have buffer identity: kernel args bind via buf_uop (base buffer), which would drop a view's offset
ok = out.has_buffer_identity() and uses.get(out, 0) == 2 and len(pars:=parents.get(after, [])) == 1 and pars[0].op is Ops.STORE \
and pars[0].src[1] is after and (dst:=pars[0].src[0]).has_buffer_identity(after_ok=True) and not dst.unsharded_base.is_unbound \
and dst.dtype is out.dtype and dst.device == out.device and dst._shape is not None and dst._shape == out._shape
if not ok: break
items.append((pars[0], dst, after, out))
else:
if not items: continue
new_call = call.substitute({out: dst for _, dst, _, out in items}, enter_calls=False)
for st, dst, after, out in items: subs[st] = after.substitute({out: dst, call: new_call}, enter_calls=False)
return sink.substitute(subs, name="bind call outputs") if subs else sink
# resolve precompiled call outputs to real buffers before anything else sees the calls (like multi_pm)
pm_resolve_call_outputs = PatternMatcher([
(UPat(Ops.CALL, name="c"), transform_precompiled_call),
(UPat(Ops.AFTER, src=(UPat(name="r"), UPat(Ops.SINK, name="t")), allow_any_len=True), resolve_returned_after),
])
@rewrite_group(new_ctx=False)
def prepare_rangeify(sink:UOp) -> UOp:
# transform precompiled value-producing calls into opaque CALLs (outputs become real buffers)
tsink = bind_call_outputs(graph_rewrite(sink, pm_resolve_call_outputs, bottom_up=True, name="resolve call outputs"))
# prepare for rangeify
tsink = graph_rewrite(tsink, multi_pm, name="multi_pm")
tsink = graph_rewrite(sink, multi_pm, name="multi_pm")
if OPENPILOT_HACKS: tsink = graph_rewrite(tsink, pm_fold_moved_after, ctx={}, name="fold moved afters")
tsink = graph_rewrite(tsink, pm_mops+earliest_rewrites, bottom_up=True, name="earliest rewrites")
tsink = graph_rewrite(tsink, pm_copy_to_store, ctx=itertools.count(0), bottom_up=True, name="convert copy to store")
+57 -33
View File
@@ -8,7 +8,7 @@ from tinygrad.dtype import DType, DTypeLike, dtypes, ConstType, least_upper_dtyp
from tinygrad.helpers import all_int, getenv, fetch, Metadata, TRACEMETA, TracingKey
from tinygrad.helpers import cpu_profile, suppress_finalizing, disable_gc, VIZ, pluralize, SPEC
from tinygrad.uop.ops import UOp, Ops, sint, all_metadata, Variable, ConstLike, UPat, PatternMatcher, GroupOp, graph_rewrite, rewrite_group
from tinygrad.uop.ops import remove_all_tags
from tinygrad.uop.ops import resolve_returned_after, remove_all_tags
from tinygrad.uop.spec import type_verify, spec_tensor
from tinygrad.mixin.rand import RandMixin
from tinygrad.schedule import create_linear_with_vars
@@ -30,20 +30,19 @@ class AllocCtx:
# a tag is the tuple of original pre-rewrite UOps a node provides storage for
def tag_uop(x:UOp): return None if x.tag is not None else x.replace(tag=(x,))
# a base needs storage of its own if it can back a buffer and doesn't already have one
def needs_storage(u:UOp) -> bool: return not u.is_virtual and not u.has_buffer_identity()
def on_disk(u:UOp): return isinstance(u.device, str) and u.device.startswith("DISK")
def is_creation_device(u:UOp): return isinstance(u.device, str) and u.device.startswith(("DISK", "NPY", "PYTHON"))
def disk_copy_is_buffer(ctx:AllocCtx, u:UOp):
# copies to disk are replaced with the disk buffer
if on_disk(u) and u.tag is None:
ctx.buffer_map[u] = u.empty_like()
return u.rtag(())
def creation_copy_is_realized(u:UOp):
# all copies from disk/numpy are realized into a real buffer
if is_creation_device(u.src[0]): return tag_uop(u)
# CONTIGUOUS and AFTER + parents are the only nodes that get updated
add_tags = PatternMatcher([
(UPat(Ops.COPY, name="u"), disk_copy_is_buffer),
(UPat(Ops.COPY, name="u"), creation_copy_is_realized),
# no tag on copies that are assigned via STORE+AFTER — merge COPY tag into AFTER
(UPat(Ops.AFTER, src=(UPat(), UPat(Ops.STORE, src=(UPat(name="dest"), UPat(Ops.COPY, name="c")))), name="a"),
lambda a,c,dest: a.replace(src=(a.src[0], a.src[1].replace(src=(dest, c.rtag(())))), tag=a.tag+c.tag) if a.tag and c.tag else None),
@@ -91,8 +90,34 @@ def contiguous_mops_to_view(ctx:AllocCtx, c:UOp, src:UOp):
view = view.reshape(c.shape)
return c.replace(src=(view,)+c.src[1:]) if c.op in {Ops.COPY, Ops.STORE} else view
def transform_precompiled_call(c:UOp) -> UOp|None:
if c.arg is None or not c.arg.precompile or not c.has_unbound_outputs: return None
assert c.src[0].op is Ops.SINK, "precompiled call bodies are SINKs of stores into the output PARAMs"
# Bind output storage at the existing argument positions.
outs = {p: a.empty_like() for p,a in enumerate(c.src[1:]) if a.unsharded_base.is_unbound}
placed:dict[UOp, UOp] = {}
items = []
for st in c.src[0].src:
value = st.src[1]
while value.op is Ops.AFTER: value = value.src[0]
# A custom kernel's output buffer can be the call output directly. Rebind each buffer only once.
if value.op in {Ops.BUFFER, Ops.UNSHARD} and value.has_buffer_identity() and value not in placed:
placed[value] = st.src[0]
items.append(st.src[1])
else: items.append(st.src[0].after(st))
body = UOp.sink(*items).substitute(placed)
call = c.replace(src=(body, *(outs.get(i, a if a.has_buffer_identity(after_ok=True) else a.contiguous())
for i, a in enumerate(c.src[1:]))))
return UOp.sink(*(c.src[1+p].store(o.after(call).shrink_to(c.src[1+p].shape)) for p,o in outs.items()))
# NOTE: adding rules to here is bad. these all need to run before the schedule cache
pm_early_transform_tensor_graph = PatternMatcher([
# transform precompiled value-producing calls into opaque CALLs (outputs become real buffers)
(UPat(Ops.CALL, name="c"), transform_precompiled_call),
# resolve AFTER on RETURNED placeholders (for precompiled calls)
(UPat(Ops.AFTER, src=(UPat(name="r"), UPat(Ops.SINK, name="t")), allow_any_len=True), resolve_returned_after),
# fold MOPS+BITCAST over BUFFER into SHRINK when movement ops collapse to contiguous range
(UPat((Ops.COPY, Ops.CONTIGUOUS), src=(UPat(GroupOp.Movement|{Ops.BITCAST}, name="src"),), name="c"), contiguous_mops_to_view),
(UPat(Ops.STORE, src=(UPat(Ops.BITCAST, name="src"), UPat()), name="c", allow_any_len=True), contiguous_mops_to_view),
@@ -151,25 +176,21 @@ pm_replace_buf = pm_canonicalize_unbound+PatternMatcher([
def transform_to_call(big_sink:UOp) -> tuple[UOp, dict[UOp, UOp]]:
if VIZ: graph_rewrite(big_sink, PatternMatcher([]), name="View Tensor Graph")
if SPEC: type_verify(big_sink, spec_tensor)
# bases to realize: same predicate as Tensor.realize
ctx = AllocCtx(bases={base for x in big_sink.src if not (base:=x.base).is_virtual and not base.has_buffer_identity()
and base.op is not Ops.AFTER})
# bases to realize. an AFTER already names the storage its store writes into
ctx = AllocCtx(bases={base for x in big_sink.src if needs_storage(base:=x.base) and base.op is not Ops.AFTER})
# this rewrite is "read-only", it adds simple things to buffer_map and may sink things on big_sink, bottom_up
# this is the only one where we have to be careful to not break the tensor graph
big_sink = graph_rewrite(big_sink, add_tags, ctx=ctx, bottom_up=True, name="add tags")
# final outputs of value calls materialize with fresh storage
# precompiled calls resolve to opaque CALLs in prepare (after callify), not by inlining: their outputs need the
# storage minted now so the call survives in the call body (a CONTIGUOUS can't be minted on the device-less AFTER).
# no tag: the tensor keeps the AFTER(RETURNED, CALL) node; backward and later realizes resolve through it again
srcs:list[UOp] = []
for u in big_sink.src:
if u.op is Ops.AFTER and u.src[0].unsharded_base.is_unbound:
if (call:=u.src[1]).op is Ops.CALL and call.arg is not None and call.arg.precompile:
buf = (val:=u.rtag(None)).empty_like()
u = buf.after(buf.store(val))
else: u = u.rtag(None).contiguous(tag=u.tag)
# precompiled calls don't need this: transform_precompiled_call gives their outputs real buffers
call = u.src[1]
if not (call.op is Ops.CALL and call.arg is not None and call.arg.precompile):
u = u.rtag(None).contiguous(tag=u.tag)
srcs.append(u)
big_sink = big_sink.replace(src=tuple(srcs))
@@ -365,7 +386,7 @@ class Tensor(RandMixin):
@disable_gc()
def realize(self, *lst:Tensor, do_update_stats=True) -> Tensor:
"""Triggers the computation needed to create these Tensor(s)."""
to_realize = [x for x in (self,)+lst if not x.uop.is_virtual and not x.uop.has_buffer_identity()]
to_realize = [x for x in (self,)+lst if needs_storage(x.uop.base)]
if len(to_realize):
run_linear(*Tensor.linear_with_vars(*to_realize), update_stats=do_update_stats)
return self
@@ -397,23 +418,23 @@ class Tensor(RandMixin):
if is_disk:
(b:=self._buffer()).copy_from(Buffer("PYTHON", b.size, b.dtype, opaque=x._data()))
return self
# a STORE can only write into storage: the target must be backed by a BUFFER (possibly under views)
assigned_to = self.uop.storage_base
# assigning to a value (not storage-backed and not a CONTIGUOUS realization point) is initialization,
# not a write: a Tensor.assign always overwrites the whole tensor, so the pending value is dead
if assigned_to.op not in {Ops.BUFFER, Ops.CONTIGUOUS}:
# x is the new value: alias it if it materializes on its own (a CONTIGUOUS or a load from a creation device),
# otherwise give it a realization point so this tensor gets storage of its own
if x.uop.op is not Ops.CONTIGUOUS and not (x.uop.op is Ops.COPY and is_creation_device(x.uop.src[0])): x = x.contiguous()
self.uop = x.uop
# assigning to a value is initialization, not a write: the whole tensor is overwritten, so the pending value is dead
if not assigned_to.has_buffer_identity() and assigned_to.op is not Ops.CONTIGUOUS:
self.uop = x.uop.clone()
return self
# STORE+AFTER: STORE is the write effect (void), AFTER wraps the view for correct shape/ranging
assign = self.uop.after(self.uop.store(x.uop))
ib = self.uop
while ib.op in GroupOp.Movement|{Ops.BITCAST, Ops.DETACH} and not (ib.has_buffer_identity() and _tensor_holds(ib)): ib = ib.src[0]
if ib is not self.uop and ib.has_buffer_identity(after_ok=True):
# view assign: replace at the buffer-identity level (e.g. RESHAPE(BUFFER)) so @function's substitution catches it
_apply_map_to_tensors({ib: ib.after(assign)}, name="Embed View Assign")
if ib is not self.uop:
# view assign: replace the node under the views (e.g. RESHAPE(BUFFER)) so @function's substitution catches it
if self.uop.op is Ops.DETACH:
# Detached writes update aliases, not earlier computations that read the storage.
for ref in list(all_tensors):
if (t:=ref()) is not None and t.uop.storage_base is ib.storage_base:
t.uop = t.uop.substitute({ib: ib.after(assign)}, walk=True)
else: _apply_map_to_tensors({ib: ib.after(assign)}, name="Embed View Assign")
else:
# simple assign
self.uop = assign
@@ -502,7 +523,9 @@ class Tensor(RandMixin):
"""
if self.uop.device is None: return self
if (device:=canonicalize_device(device)) == self.device: return self
ret = Tensor(self.uop.copy_to_device(device))
# a copy to disk wants to persist, so it inserts a clone: the disk buffer is the storage of the copied value
if isinstance(device, str) and device.startswith("DISK"): ret = Tensor(self.uop.clone(device))
else: ret = Tensor(self.uop.copy_to_device(device))
if self.grad is not None: ret.grad = self.grad.to(device)
return ret.is_param_(self.is_param)
@@ -527,7 +550,9 @@ class Tensor(RandMixin):
if not isinstance(self.device, str): raise RuntimeError("can't shard a multi-device tensor")
if len(devices) == 1: return self.to(devices[0])
devices = cast(tuple[str, ...], canonicalize_device(devices))
uop = self.uop.shard(devices, None if axis is None else self._resolve_dim(axis))
# a shard of a load from a creation device (disk/npy/python) wants the copy to persist, so it inserts a clone
src = self.uop.clone(devices) if is_creation_device(self.uop) else self.uop
uop = src.shard(devices, None if axis is None else self._resolve_dim(axis))
return Tensor(uop).is_param_(self.is_param)
def shard_(self, devices:tuple[str, ...], axis:int|None=None) -> Tensor:
@@ -642,6 +667,7 @@ class Tensor(RandMixin):
if isinstance(v, Tensor):
if v.dtype in dtypes.weaks: v = v.cast(least_upper_dtype(self.dtype, v.dtype))
if v.dtype != self.dtype: raise RuntimeError(f"setitem dtype mismatch: {self.dtype=} != {v.dtype=}")
if isinstance(v, Tensor) and v.uop is self._getitem(indices).uop: return
# raise if mutation would diverge from eager (allow only pure views of a realized buffer; exclude +=/-= RHS via v_uop/v_bw)
v_uop, v_bw = (v.uop, v.uop.backward_slice) if isinstance(v, Tensor) else (None, {})
if self.uop.op_in_backward_slice_with_self(Ops.BUFFER):
@@ -655,8 +681,6 @@ class Tensor(RandMixin):
realized = is_disk or self.uop.base.op is Ops.BUFFER or self.uop._base_buffer_is_realized()
if (not self.uop.base.is_realized and self.is_floating_point()) or not (advanced or realized):
if not isinstance(v, Tensor): v = Tensor(v, device=self.device, dtype=self.dtype)
# __iadd__/__isub__ creates AFTER(view, STORE(view, computed)); unwrap to get the computed value
if v.uop.op is Ops.AFTER and any(s.op is Ops.STORE for s in v.uop.src[1:]): v = v._apply_uop(lambda x: x.src[1].src[1])
self.replace(self._getitem(indices, v))
elif advanced: # advanced setitem
if is_disk: raise RuntimeError("advanced setitem is not supported for DISK tensors")
+15 -3
View File
@@ -458,6 +458,8 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
@functools.cached_property
def ended_ranges(self) -> tuple[UOp, ...]:
if self.op is Ops.CALL and self.src[0].op is Ops.CUSTOM_FUNCTION and self.src[0].src: return ()
if self.op is Ops.END: return tuple(r for r in self.src[1:] if r.op is Ops.RANGE)
if self.op in range_start: return self.src[range_start[self.op]:]
if self.op is Ops.AFTER: return tuple(flatten([x.ended_ranges for x in self.src[1:]]))
# UNSHARD ends the DEVICE range: its src is per-device index math, the device axis is carried by the axis metadata
@@ -816,8 +818,11 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
return UOp(Ops.BUFFER, arg=ParamArg(-id(opaque), opaque.dtype, size=opaque.size, device=device or opaque.device, buffer=opaque))
def empty_like(self, dtype:DTypeLike|None=None, device:str|tuple[str, ...]|None=None) -> UOp:
device = canonicalize_device(self.device if device is None else device)
dt = self.commit_dtype() if dtype is None else dtype
if self.op is Ops.UNSHARD and isinstance(device, tuple): # mirror the sharding on the fresh storage
return UOp.empty(self.src[0].shape, dtype=dt, device=device).unshard(self.arg, self.src[1:])
axis = self.axis if isinstance(device, tuple) else None
ret = UOp.empty(self.shard_shape if axis is not None else self.shape, dtype=self.commit_dtype() if dtype is None else dtype, device=device)
ret = UOp.empty(self.shard_shape if axis is not None else self.shape, dtype=dt, device=device)
return ret.unshard(axis) if axis is not None else ret
@staticmethod
def _frompy(x:list|tuple|bytes, dtype:DType, device:str|tuple[str, ...]|None=None) -> UOp:
@@ -836,6 +841,8 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
device = device or self.device
ret = self.empty_like(device=device)
src = self if self.device is None or self.device == device else self.copy_to_device(device)
# The clone's STORE already materializes the value; a separate CONTIGUOUS is redundant.
if src.op is Ops.CONTIGUOUS: src = src.src[0]
return ret.after(ret.store(src.cast(ret.dtype)))
@recursive_property
def device(self) -> str|tuple[str, ...]|None:
@@ -1248,8 +1255,12 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
def to_elf(self) -> TinyELF:
assert self.op is Ops.PROGRAM and isinstance(self.arg, ProgramInfo), "to_elf should only be called on a PROGRAM ast"
sig = tuple((u.arg.name, u.arg.slot, u.dtype, u._shape)
for u in tuple(filter(lambda u: u.op is Ops.PARAM and u.addrspace != AddrSpace.ALU, self.src[1].src)) + self.arg.vars)
params = tuple(u for u in self.src[1].src if u.op is Ops.PARAM and u.addrspace != AddrSpace.ALU)
# sig slots are compact: buffers in globals order (runtimes launch buffers in that order), then vars. raw call-arg
# positions skip buffers for kernels using a sparse subset of the call's buffers (CL binds bufs[slot])
gmap = {s:j for j, s in enumerate(self.arg.globals)}
sig = tuple((u.arg.name, gmap[u.arg.slot], u.dtype, u._shape) for u in params) + \
tuple((v.arg.name, len(self.arg.globals)+j, v.dtype, v._shape) for j, v in enumerate(self.arg.vars))
return TinyELF(self.src[3].arg, self.arg.function_name, self.arg.target, sig, self.key)
@dataclass(frozen=True)
@@ -1407,6 +1418,7 @@ class UPat(RandMixin):
@staticmethod
def any(*src): return UPat(src=src, is_any=True)
def or_casted(self, name:str|None=None): return UPat.any(self if name is None else self.named(name), UPat(Ops.CAST, name=name, src=(self,)))
def or_bitcasted(self, name:str|None=None): return UPat.any(self if name is None else self.named(name), UPat(Ops.BITCAST, name=name, src=(self,)))
def or_after(self, name:str|None=None):
return UPat.any(self if name is None else self.named(name), UPat(Ops.AFTER, name=name, src=(self,), allow_any_len=True))
@staticmethod
+5 -7
View File
@@ -21,12 +21,10 @@ def validate_index(uidx:UOp, gate:UOp|None=None):
# We can use UOp min/max to do a faster check, but it can give false positive since its not an exact bound and doesn't consider the mask
if 0<=idx.vmin and idx.vmax<sz: return True
# TODO: validate these
# WEBGPU has a BITCAST in the index, PTX casts pointer to long
# VECTORIZE can't be properly modeled in z3 since it doesn't support vectors
# TODO: validate STACK, z3 can't model vectors
# don't descend into PARAM shape metadata; only the PARAM value participates in index arithmetic
for x in idx.toposort(gate=lambda x: x.op is not Ops.PARAM) | gate.toposort(gate=lambda x: x.op is not Ops.PARAM):
if x.op in {Ops.BITCAST, Ops.STACK}: return True
if x.op is Ops.STACK: return True
# if all is good and CHECK_OOB=1, validate with z3
from tinygrad.uop.validate import validate_index_with_z3
@@ -95,7 +93,7 @@ spec_shared = PatternMatcher([
# AFTER on Movement Op, PARAM, BUFFER, CONTIGUOUS, RETURNED, or another AFTER
(UPat(Ops.AFTER, src=(UPat(GroupOp.Movement.union({Ops.PARAM, Ops.BUFFER, Ops.CONTIGUOUS, Ops.INDEX,
Ops.AFTER, Ops.UNSHARD, Ops.BITCAST, Ops.INS})),),
Ops.AFTER, Ops.UNSHARD, Ops.BITCAST, Ops.DETACH, Ops.INS})),),
allow_any_len=True), lambda: True),
# CUSTOM (inline and non inline): the arg is the source string and the dtype it produces, void for a bare statement
@@ -198,8 +196,8 @@ spec_program = PatternMatcher([
(UPat(GroupOp.All, name="x"), lambda x: False if x.op is not Ops.CAST and any(s.op is Ops.CONST for s in x.src) else None),
(UPat(GroupOp.All-{Ops.CONST}, dtypes.weaks), lambda: False),
# allow special SHRINK
(UPat(Ops.SHRINK, src=(UPat((Ops.PARAM, Ops.BUFFER, Ops.AFTER)), UPat(), UPat(Ops.CONST).or_casted())), lambda: True),
# allow special SHRINK of a buffer or its bitcast
(UPat(Ops.SHRINK, src=(UPat((Ops.PARAM, Ops.BUFFER, Ops.AFTER)).or_bitcasted(), UPat(), UPat.cvar().or_casted())), lambda: True),
# movement ops are not allowed in programs
(UPat(GroupOp.Movement), lambda: False),
+26 -28
View File
@@ -29,36 +29,37 @@ z3_alu: dict[Ops, Callable[..., z3.ExprRef]] = python_alu | {Ops.CMOD: lambda a,
Ops.FLOORMOD: lambda a,b: a-z3_floordiv(a,b)*b,
Ops.SHR: lambda a,b: a/(2**b.as_long()), Ops.SHL: lambda a,b: a*(2**b.as_long()),
Ops.AND: z3_and, Ops.WHERE: z3.If, Ops.XOR: z3_xor, Ops.MAX: lambda a,b: z3.If(a<b, b, a),}
def create_bounded(name:str, vmin:int, vmax:int, z3ctx:z3.Context) -> tuple[z3.ArithRef, z3.BoolRef]:
return (s:=z3.Int(name, ctx=z3ctx)), (vmin <= s)&(s <= vmax)
def create_bounded(name:str, vmin:int|z3.ArithRef, vmax:int|z3.ArithRef, solver:z3.Solver) -> z3.ArithRef:
solver.add((vmin <= (s:=z3.Int(name, ctx=solver.ctx)))&(s <= vmax))
return s
def create_var(x:UOp, ctx:tuple[z3.Solver, dict[UOp, z3.ExprRef]]) -> z3.ExprRef:
name = f"{x.op.name.lower()}{len(ctx[1])}"
return z3.Bool(name, ctx=ctx[0].ctx) if x.dtype == dtypes.bool else create_bounded(name, x.dtype.min, x.dtype.max, ctx[0])
# z3 does not model widths: a cast only converts between bool and int
def z3_cast(c:UOp, x:z3.ExprRef) -> z3.ExprRef:
if (c.src[0].dtype == dtypes.bool) == (c.dtype == dtypes.bool): return x
return x != 0 if c.dtype == dtypes.bool else z3.If(x, 1, 0)
z3_renderer = PatternMatcher([
(UPat.var("cond").where(UPat.var("x"), UPat(Ops.CONST, arg=Invalid)), lambda x,cond,ctx: (ctx[1][x], ctx[1][cond])),
# the valid condition is a constraint
(UPat.var("cond").where(UPat.var("x"), UPat(Ops.CONST, arg=Invalid)), lambda x,cond,ctx: ctx[0].add(ctx[1][cond]) or ctx[1][x]),
# variables
(UPat(Ops.SPECIAL, name="x"), lambda x,ctx: create_bounded(x.arg, 0, ctx[1][x.src[0]]-1, ctx[0])),
(UPat((Ops.SPECIAL, Ops.RANGE), name="x"), lambda x,ctx: create_bounded(x.render(simplify=False), 0, ctx[1][x.src[0]]-1, ctx[0])),
(UPat(Ops.PARAM, name="x"), lambda x,ctx: create_bounded(x.arg.name, x.vmin, x.vmax, ctx[0])),
(UPat(Ops.BUFFER, name="x"), lambda x,ctx: create_bounded(x.arg.name, x.vmin, x.vmax, ctx[0]) if x.is_variable else None),
(UPat(Ops.RANGE, name="x"), lambda x,ctx: create_bounded(x.render(simplify=False), 0, ctx[1][x.src[0]]-1, ctx[0])),
# loads are variables bounded by the min/max of the dtype. non-pointer INDEX is also a LOAD
(UPat((Ops.LOAD, Ops.INDEX), dtypes.ints+(dtypes.weakint,), name="x"), lambda x,ctx:
create_bounded(f"load{len(ctx[1])}", x.dtype.min, x.dtype.max, ctx[0])),
(UPat((Ops.LOAD, Ops.INDEX), dtypes.bool), lambda ctx: (z3.Bool(f"load{len(ctx[1])}", ctx=ctx[0]), None)),
(UPat((Ops.LOAD, Ops.INDEX), name="x"), create_var),
# casts, bitcasts and comparisons from floats create new variables
(UPat((Ops.CAST, Ops.BITCAST)+tuple(GroupOp.Comparison), src=UPat(dtype=dtypes.floats), name="x"), create_var),
# a bitcast between ints wraps into the target range, z3 ints are unbounded
(UPat(Ops.BITCAST, dtypes.ints, src=(UPat.var("x", dtypes.ints),), name="c"),
lambda c,x,ctx: (ctx[1][x]-c.dtype.min) % 2**(8*c.dtype.itemsize) + c.dtype.min),
# constants
(UPat(Ops.CONST, arg=Invalid), lambda ctx: (z3.Int("Invalid", ctx=ctx[0]), None)),
(UPat(Ops.CONST, dtypes.weakint, name="x"), lambda x,ctx: (z3.IntVal(x.val, ctx=ctx[0]), None)),
(UPat(Ops.CONST, dtypes.bool, name="x"), lambda x,ctx: (z3.BoolVal(x.val, ctx=ctx[0]), None)),
# casts from floats create new variables
(UPat(Ops.CAST, dtypes.ints+(dtypes.weakint,), src=(UPat(dtype=dtypes.floats),), name="x"), lambda x,ctx:
create_bounded(f"cast{len(ctx[1])}", x.dtype.min, x.dtype.max, ctx[0])),
# A comparison between floats introduces a new bool variable
(UPat(GroupOp.Comparison, src=UPat(dtype=dtypes.floats)), lambda ctx: (z3.Bool(f"float_cmp{len(ctx[1])}", ctx=ctx[0]), None)),
# a same-dtype cast states a width, which z3 does not model: identity. must precede the rules below (bool->bool)
(UPat(Ops.CAST, name="x"), lambda x,ctx: (ctx[1][x.src[0]], None) if x.dtype == x.src[0].dtype else None),
# casts from bool/int to int/bool
(UPat(Ops.CAST, dtypes.ints+(dtypes.weakint,),src=(UPat.var("x", dtypes.bool),)), lambda x,ctx: (z3.If(ctx[1][x], 1, 0), None)),
(UPat(Ops.CAST, dtypes.ints+(dtypes.weakint,), src=(UPat.var("x", dtypes.ints+(dtypes.weakint,)),)), lambda x,ctx: (ctx[1][x], None)),
(UPat(Ops.CAST, dtypes.bool, name="x"), lambda x,ctx: (ctx[1][x.src[0]]!=0, None)),
(UPat(GroupOp.ALU, name="x"), lambda x,ctx: (z3_alu[x.op](*(ctx[1][s] for s in x.src)), None)),
(UPat(Ops.CONST, arg=Invalid), lambda ctx: z3.Int("Invalid", ctx=ctx[0].ctx)),
(UPat(Ops.CONST, name="x"), lambda x,ctx: z3.BoolVal(x.val, ctx=ctx[0].ctx) if x.dtype == dtypes.bool else z3.IntVal(x.val, ctx=ctx[0].ctx)),
(UPat(Ops.CAST, src=(UPat.var("x"),), name="c"), lambda c,x,ctx: z3_cast(c, ctx[1][x])),
(UPat(GroupOp.ALU, name="x"), lambda x,ctx: z3_alu[x.op](*(ctx[1][s] for s in x.src))),
])
def uops_to_z3(solver:z3.Solver, *uops: UOp) -> list[z3.ExprRef]:
@@ -69,11 +70,8 @@ def uops_to_z3(solver:z3.Solver, *uops: UOp) -> list[z3.ExprRef]:
for u in lst:
# NOTE: we skip STACK here, it can't actually be accessed
if u.op is Ops.STACK: continue
z3_rewritten: tuple[z3.ExprRef, z3.BoolRef|None]|None = z3_renderer.rewrite(u, ctx=(solver.ctx, z3map))
if z3_rewritten is None: raise NotImplementedError(f"{u.op} is not supported by z3")
new_u, constraint = z3_rewritten
if constraint is not None: solver.add(constraint)
z3map[u] = new_u
if (z3_rewritten:=z3_renderer.rewrite(u, ctx=(solver, z3map))) is None: raise NotImplementedError(f"{u.op} is not supported by z3")
z3map[u] = z3_rewritten
assert all(u in z3map for u in uops), "UOp failed to rewrite to z3!"
return [z3map[u] for u in uops]
+14 -12
View File
@@ -4,8 +4,8 @@ from tinygrad.dtype import dtypes, DType, AddrSpace, Invalid, least_upper_dtype,
from tinygrad.uop.ops import UOp, UPat, Ops, PatternMatcher, GroupOp, dtype_from_uop, promo_dtype
# the decomps and float emulation commit bare consts at a dtype another src already states
def commit_weak_consts(u:UOp, dt:DType|None) -> UOp|None:
return None if dt is None else u.replace(src=tuple(s.ccast(dt) if s.op is Ops.CONST and s.dtype in dtypes.weaks else s for s in u.src))
def commit_weak_consts(u:UOp, dt:DType|None) -> UOp:
return u if dt is None else u.replace(src=tuple(s.ccast(dt) if s.op is Ops.CONST and s.dtype in dtypes.weaks else s for s in u.src))
# the concrete dtypes u commits its srcs at: the operands' meet and u's own derived dtype, None if either is weak
def derived_dtypes(u:UOp, src:tuple[UOp, ...]) -> tuple[DType, DType]|None:
@@ -43,9 +43,16 @@ pm_commit_weak = PatternMatcher([
# consumers absorb the weak CAST off their srcs and default underivable consts; dtype-producing ops settle here.
# a weakfloat Unary (sin/exp2/...) must resolve before the transcendental decomposition.
_lower_weak_ops = GroupOp.Binary|GroupOp.Unary|{Ops.WHERE, Ops.RANGE, Ops.STACK, Ops.SPECIAL}
# only within the kind is a weak CAST a width statement: across kinds it converts the value, so it commits unless u recasts its srcs anyway
def absorb_weak_src(u:UOp, s:UOp) -> UOp:
if s.op is not Ops.CAST or s.dtype not in dtypes.weaks: return s
if u.op in _lower_weak_ops or u.op is Ops.CAST or weak_dtype(s.src[0].dtype) is s.dtype: return s.src[0]
return s.src[0].cast(s.commit_dtype(dtypes.int))
def lower_weak_node(u:UOp) -> UOp|None:
if u.op is Ops.CAST and u.src[0].op is Ops.CONST: return None # a committed const, not a consumer
src = tuple(s.src[0] if s.op is Ops.CAST and s.dtype in dtypes.weaks else s for s in u.src)
src = tuple(absorb_weak_src(u, s) for s in u.src)
if derived_dtypes(u, src) is None:
src = tuple(s.ccast(s.commit_dtype(dtypes.int)) if s.op is Ops.CONST and s.dtype in dtypes.weaks else s for s in src)
if src == u.src: return None
@@ -81,16 +88,11 @@ def uncast_const(u:UOp) -> UOp|None:
pm_uncast_const = PatternMatcher([(UPat(GroupOp.Broadcastable, name="u"), uncast_const)])
def cast_const(u:UOp, s:UOp) -> UOp:
if s.op is not Ops.CONST or s.is_invalid: return s # Invalid never commits
# bool is the one strong bare dtype: cconst, since .cast(bool) would fold at construction
if s.dtype is dtypes.bool: return UOp.cconst(s.val, s.dtype)
# commit at the dtype its consumer derives
return s.ccast(dts[0]) if (dts:=derived_dtypes(u, u.src)) is not None else s
# commit every remaining bare const, keyed on the consumer: "bare" is a property of the edge
def cast_consts(u:UOp) -> UOp|None:
if u.op is Ops.CAST and u.src[0].op is Ops.CONST: return None # a committed const's CONST is its value, not an edge
return None if (src:=tuple(cast_const(u, s) for s in u.src)) == u.src else u.replace(src=src)
if (dts:=derived_dtypes(u, u.src)) is not None: u = commit_weak_consts(u, dts[0])
# bool is the one strong bare dtype: .cast(bool) would fold at construction. Invalid never commits.
return u.replace(src=tuple(UOp.cconst(s.val, s.dtype) if s.op is Ops.CONST and s.dtype is dtypes.bool and not s.is_invalid else s for s in u.src))
pm_cast_const = PatternMatcher([(UPat(GroupOp.All, name="u"), cast_consts)])
pm_cast_const = PatternMatcher([(UPat(GroupOp.All, name="u", custom_early_reject={Ops.CONST}), cast_consts)])