mirror of
https://github.com/tinygrad/tinygrad.git
synced 2026-09-07 05:26:13 +00:00
Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4ac2605fb | ||
|
|
5f06e19fbd | ||
|
|
48c8736dc2 | ||
|
|
00a5b14216 | ||
|
|
af598b33bb | ||
|
|
dabcec6691 | ||
|
|
86baa8d125 | ||
|
|
eb6bca255d | ||
|
|
1f114dc961 | ||
|
|
5a4831bca0 | ||
|
|
e0413ba189 | ||
|
|
c1560cb44b | ||
|
|
b6deae1e9c | ||
|
|
9fca24ffb7 | ||
|
|
f5528f3eb5 | ||
|
|
2b787196b3 | ||
|
|
f34f308b61 | ||
|
|
6a6c3042f4 | ||
|
|
5ae6526d47 | ||
|
|
020c7a14fd | ||
|
|
f9ae840f91 | ||
|
|
371ac77173 |
@@ -97,7 +97,7 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: '0'
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -137,7 +137,7 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: '0'
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -185,7 +185,7 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: '0'
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -227,7 +227,7 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: '0'
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -272,7 +272,7 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: '0'
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
|
||||
@@ -253,7 +253,7 @@ jobs:
|
||||
deps: testing_unit
|
||||
llvm: 'true'
|
||||
- name: Test SPEC=2
|
||||
run: SPEC=2 pytest --maxfail=10 -n auto --durations=30 test/unit test/backend test/opt --ignore test/backend/test_custom_kernel.py --ignore test/unit/test_hashing.py -k "not test_setitem_big" -k "not test_conv2d_ceildiv_edge_case" --splits 2 --group ${{ matrix.group }}
|
||||
run: SPEC=2 pytest --maxfail=10 -n auto --durations=30 test/unit test/backend test/opt --ignore test/backend/test_custom_kernel.py --ignore test/unit/test_hashing.py --splits 2 --group ${{ matrix.group }}
|
||||
|
||||
fuzzing:
|
||||
name: Fuzzing
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import os, pytest, signal, threading
|
||||
|
||||
@pytest.hookimpl(wrapper=True)
|
||||
def pytest_runtest_call(item):
|
||||
t = threading.Timer(int(os.getenv("TEST_TIMEOUT", 90)), os.kill, args=(os.getpid(), signal.SIGABRT))
|
||||
t = threading.Timer(int(os.getenv("TEST_TIMEOUT", 120)), os.kill, args=(os.getpid(), signal.SIGABRT))
|
||||
t.start()
|
||||
try: yield
|
||||
finally:
|
||||
|
||||
+1
-1
@@ -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))).clone()
|
||||
return (h + self.mlp(self.ln_2(h))).contiguous()
|
||||
|
||||
class Transformer:
|
||||
def __init__(self, dim, n_heads, n_layers, norm_eps, vocab_size, max_seq_len=1024):
|
||||
|
||||
+419
-80
@@ -1,17 +1,17 @@
|
||||
from __future__ import annotations
|
||||
from typing import cast
|
||||
import os, ctypes, struct, functools, importlib, mmap, errno, contextlib, sys, itertools, atexit
|
||||
import os, ctypes, struct, functools, importlib, mmap, errno, contextlib, sys, hashlib, itertools, collections, atexit
|
||||
assert sys.platform != 'win32'
|
||||
from dataclasses import dataclass
|
||||
from tinygrad.runtime.support.hcq2 import HCQ2Compiled, HCQAllocator, HWQueue, encode_submit, to_name
|
||||
from tinygrad.uop.ops import sint, UOp
|
||||
from tinygrad.device import BufferSpec, Buffer
|
||||
from dataclasses import dataclass, replace
|
||||
from tinygrad.runtime.support.hcq2 import HCQ2Compiled, HCQAllocator, HWQueue, encode_submit, to_name, patch, unwrap_view, rt_addr
|
||||
from tinygrad.uop.ops import sint, UOp, ProgramInfo
|
||||
from tinygrad.device import BufferSpec, Buffer, Device, Compiled, ProfileProgramEvent
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.helpers import getenv, round_up, data64_le, DEBUG, PROFILE, lo32, hi32
|
||||
from tinygrad.helpers import getenv, round_up, data64_le, DEBUG, PROFILE, lo32, hi32, prod, colored
|
||||
from tinygrad.helpers import ceildiv, unwrap, pluralize
|
||||
from tinygrad.renderer.cstyle import HIPRenderer, HIPCCRenderer
|
||||
from tinygrad.renderer.llvmir import AMDLLVMRenderer
|
||||
from tinygrad.runtime.autogen import kfd, hsa, amdgpu_kd, amdgpu_drm
|
||||
from tinygrad.runtime.autogen import kfd, hsa, sqtt, amdgpu_kd, amdgpu_drm
|
||||
from tinygrad.runtime.autogen.am import am
|
||||
from tinygrad.runtime.support.elf import elf_loader
|
||||
from tinygrad.runtime.support.hcq import FileIOInterface, HCQBuffer, MMIOInterface, hcq_filter_visible_devices
|
||||
@@ -19,9 +19,10 @@ from tinygrad.runtime.support.am.amdev import AMDev, AMMemoryManager
|
||||
from tinygrad.runtime.support.amd import AMDReg, AMDIP, import_module, import_soc, import_pmc
|
||||
from tinygrad.runtime.support.system import PCIIfaceBase, PCIAllocationMeta, USBPCIDevice, MAP_FIXED, MAP_NORESERVE
|
||||
from tinygrad.runtime.support.usb import USB3, pm_usb_bufferize
|
||||
from tinygrad.runtime.support.memory import AddrSpace, BumpAllocator
|
||||
from tinygrad.runtime.ops_amd import SQTT, PMC
|
||||
from tinygrad.runtime.ops_amd import EVENT_INDEX_PARTIAL_FLUSH, WAIT_REG_MEM_FUNCTION_GEQ
|
||||
from tinygrad.runtime.support.memory import AddrSpace
|
||||
from tinygrad.runtime.ops_amd import SQTT, PMC, SQTT_ITRACE_SE_MASK, SQTT_LIMIT_SE, SQTT_SIMD_SEL, SQTT_TOKEN_EXCLUDE, AQL_HDR
|
||||
from tinygrad.runtime.ops_amd import ProfileSQTTEvent, ProfilePMCEvent, PMCSample
|
||||
from tinygrad.runtime.ops_amd import EVENT_INDEX_PARTIAL_FLUSH, WAIT_REG_MEM_FUNCTION_GEQ, WAIT_REG_MEM_FUNCTION_EQ
|
||||
if getenv("IOCTL"): import extra.hip_gpu_driver.hip_ioctl # noqa: F401 # pylint: disable=unused-import
|
||||
|
||||
from tinygrad.engine.realize import get_call_arg_uops, get_call_var_uops
|
||||
@@ -36,6 +37,14 @@ def _queue_args(hq:HWQueue, q) -> list[UOp]: # the ring and its pointers, tagged
|
||||
|
||||
def _dw(vals) -> int: return sum(2 if isinstance(x, UOp) and x.dtype.itemsize == 8 else 1 for x in vals)
|
||||
|
||||
def dispatch_packet(data:AMDProgramData, info:ProgramInfo, kernel_object:UOp=UOp.const(0, dtypes.uint64),
|
||||
kernarg_address:UOp=UOp.const(0, dtypes.uint64)) -> list: # as words: the grid may be symbolic
|
||||
pkt = bytes(hsa.hsa_kernel_dispatch_packet_t(header=AQL_HDR | (hsa.HSA_PACKET_TYPE_KERNEL_DISPATCH << hsa.HSA_PACKET_HEADER_TYPE),
|
||||
setup=3 << hsa.HSA_KERNEL_DISPATCH_PACKET_SETUP_DIMENSIONS, private_segment_size=data.private_segment_size,
|
||||
group_segment_size=data.group_segment_size, **{f"workgroup_size_{d}": l for d, l in zip("xyz", info.local_size)}))
|
||||
grid = [(g * l).cast(dtypes.uint32) if isinstance(g, UOp) else g * l for g, l in zip(info.global_size, info.local_size)]
|
||||
return [UOp(Ops.BINARY, arg=pkt[:12]), *grid, UOp(Ops.BINARY, arg=pkt[24:32]), kernel_object, kernarg_address, UOp(Ops.BINARY, arg=pkt[48:])]
|
||||
|
||||
class AMDComputeQueue(HWQueue):
|
||||
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)),
|
||||
@@ -49,6 +58,8 @@ class AMDComputeQueue(HWQueue):
|
||||
def __init__(self, ctx, submit):
|
||||
super().__init__(ctx, submit)
|
||||
self.pm4, self.gc, self.soc, self.nbio, self.target = self.dev.pm4, self.dev.gc, self.dev.soc, self.dev.nbio, self.dev.target
|
||||
self.profiled:list[UOp] = []
|
||||
if self.dev.pmc_enabled: self.pmc_start()
|
||||
|
||||
def pkt3(self, cmd, *vals): self.q(self.pm4.PACKET3(cmd, _dw(vals) - 1), *vals)
|
||||
|
||||
@@ -61,6 +72,20 @@ class AMDComputeQueue(HWQueue):
|
||||
else: raise RuntimeError(f'Cannot set {reg.name} ({reg.addr[0]}) via pm4 packet')
|
||||
self.pkt3(set_packet, reg.addr[0] - set_packet_start, *(args or (reg.encode(**kwargs),)))
|
||||
|
||||
@contextlib.contextmanager
|
||||
def pred_exec(self, xcc_mask:int): # the count fills in when the block closes
|
||||
if self.dev.xccs > 1: self.pkt3(self.pm4.PACKET3_PRED_EXEC, xcc_mask << 24)
|
||||
start = len(self.blob)
|
||||
yield
|
||||
if self.dev.xccs > 1:
|
||||
cnt, = struct.unpack("I", self.blob[start-4:start])
|
||||
self.blob[start-4:start] = struct.pack("I", cnt | (len(self.blob) - start) // 4)
|
||||
|
||||
def set_grbm(self, instance=None, se=None, sh=None, wgp=None):
|
||||
instance_val = (wgp << 2 | (instance or 0)) if wgp is not None else instance
|
||||
self.wreg(self.gc.regGRBM_GFX_INDEX, **{(f'{key}_broadcast_writes' if val is None else f'{key}_index'): (1 if val is None else val)
|
||||
for key, val in [('instance', instance_val), ('se', se), ('sh' if self.target[0] == 9 else 'sa', sh)]})
|
||||
|
||||
def wait_reg_mem(self, value, mask=0xffffffff, mem=None, reg=None, reg_done=0, op=WAIT_REG_MEM_FUNCTION_GEQ):
|
||||
wrm_info_dw = self.pm4.WAIT_REG_MEM_MEM_SPACE(int(mem is not None)) | self.pm4.WAIT_REG_MEM_OPERATION(int(mem is None and reg_done > 0)) \
|
||||
| self.pm4.WAIT_REG_MEM_FUNCTION(op) | self.pm4.WAIT_REG_MEM_ENGINE(0)
|
||||
@@ -106,16 +131,232 @@ class AMDComputeQueue(HWQueue):
|
||||
reg_done=getattr(self.nbio, f'regBIF_BX_PF{pf}_GPU_HDP_FLUSH_DONE').addr[0], value=0xffffffff)
|
||||
self.acquire_mem()
|
||||
|
||||
def spi_config(self, tracing:bool):
|
||||
self.wreg(self.gc.regSPI_CONFIG_CNTL, ps_pkr_priority_cntl=3, exp_priority_order=3, gpr_write_priority=0x2c688,
|
||||
enable_sqg_bop_events=int(tracing), enable_sqg_top_events=int(tracing))
|
||||
|
||||
### profiling: a kernel's slot holds its counters and trace until a synchronize reads them back
|
||||
|
||||
def prof_buf(self, name:str) -> UOp:
|
||||
return UOp.placeholder((getattr(self.dev, name).size,), getattr(self.dev, name).dtype, 0, device=self.devs, tag=name)
|
||||
|
||||
def prof_start(self, data:AMDProgramData, info:ProgramInfo, lib:UOp) -> UOp|None:
|
||||
if not (self.dev.pmc_enabled or self.dev.sqtt_enabled): return None
|
||||
slot = (self.prof_buf("prof_log").index(0).load() + len(self.profiled)) % self.dev.prof_slots
|
||||
tag = UOp.const(unwrap_view(lib)[0].arg.slot, dtypes.uint64)
|
||||
self.profiled.append(self.prof_buf("prof_log").index(1 + slot.cast(dtypes.int)).store(tag))
|
||||
if self.dev.sqtt_enabled:
|
||||
self.sqtt_start(slot)
|
||||
self.sqtt_setup_exec(data, info)
|
||||
return slot
|
||||
|
||||
def prof_stop(self, slot:UOp|None):
|
||||
if slot is None: return
|
||||
if self.dev.pmc_enabled: self.pmc_read(slot)
|
||||
if self.dev.sqtt_enabled: self.sqtt_stop(slot)
|
||||
|
||||
def prof_bump(self, cmdbuf:UOp) -> UOp:
|
||||
if not self.profiled: return cmdbuf
|
||||
log = self.prof_buf("prof_log")
|
||||
return cmdbuf.after(log.after(cmdbuf, *self.profiled).index(0).store(log.index(0).load() + len(self.profiled)))
|
||||
|
||||
### PMC
|
||||
|
||||
def pmc_reset_counters(self, en=True):
|
||||
self.set_grbm()
|
||||
self.wreg(self.gc.regCP_PERFMON_CNTL if self.target[0] <= 11 else self.gc.regCP_PERFMON_CNTL_1, perfmon_state=0)
|
||||
if en: self.wreg(self.gc.regCP_PERFMON_CNTL if self.target[0] <= 11 else self.gc.regCP_PERFMON_CNTL_1, perfmon_state=1)
|
||||
|
||||
def pmc_start(self): # every submit
|
||||
self.pmc_reset_counters(en=False)
|
||||
self.wreg(self.gc.regSQ_PERFCOUNTER_CTRL, cs_en=1, ps_en=1, gs_en=1, hs_en=1, **({'vmid_mask':0xffff} if (gfx9:=self.target[0] == 9) else {}))
|
||||
if not gfx9: self.wreg(self.gc.regSQ_PERFCOUNTER_CTRL2, force_en=1, vmid_en=0xffff)
|
||||
|
||||
end_off, sched = 0, []
|
||||
block2pid:dict[str, itertools.count] = collections.defaultdict(lambda: itertools.count())
|
||||
for name in self.dev.pmc_names:
|
||||
block, idx = self.dev.pmc_counters[name]
|
||||
# sq block on gfx11+ goes down to wgps
|
||||
inst_cnt, se_cnt, sa_cnt, wgp_cnt = {"GRBM": (1, 1, 1, 1), "GL2C": (32, 1, 1, 1), "TCC": (16, 1, 1, 1),
|
||||
"SQ": (1, self.dev.se_cnt) + ((1, 1) if gfx9 else (2, self.dev.iface.props['cu_per_simd_array'] // 2))}[block]
|
||||
end_off += (rec_size:=prod((self.dev.xccs, inst_cnt, se_cnt, sa_cnt, wgp_cnt)) * 8)
|
||||
|
||||
# gfx11+ and later require even-numbered SQ *_SELECT registers
|
||||
regsample = f'reg{block}_PERFCOUNTER{(pcid:=next(block2pid[block]))}'
|
||||
if (regsel:=getattr(self.gc, (f'reg{block}_PERFCOUNTER{(pcid*2) if not gfx9 and block=="SQ" else pcid}_SELECT'), None)) is None:
|
||||
raise RuntimeError(f'{block} is out of perfcounter registers: ({regsample} is not found)')
|
||||
|
||||
self.wreg(regsel, perf_sel=idx, **({'simd_mask':0xf, 'sqc_bank_mask':0xf, 'sqc_client_mask':0xf} if gfx9 and block == "SQ" else {}))
|
||||
sched.append(PMCSample(name, block, self.dev.xccs, inst_cnt, se_cnt, sa_cnt, wgp_cnt, end_off-rec_size, rec_size, regsample))
|
||||
self.dev.pmc_sched = sched
|
||||
|
||||
if gfx9: self.wreg(self.gc.regSQ_PERFCOUNTER_MASK, sh0_mask=0xffff, sh1_mask=0xffff)
|
||||
self.wreg(self.gc.regCOMPUTE_PERFCOUNT_ENABLE, 1)
|
||||
self.pmc_reset_counters(en=True)
|
||||
|
||||
def pmc_read(self, slot:UOp):
|
||||
buf = rt_addr(self.prof_buf("pmc_buf"), self.devs) + slot * self.dev.pmc_size
|
||||
self.set_grbm()
|
||||
self.wreg(self.gc.regCP_PERFMON_CNTL if self.target[0] <= 11 else self.gc.regCP_PERFMON_CNTL_1, perfmon_state=1, perfmon_sample_enable=1)
|
||||
|
||||
for smp in self.dev.pmc_sched:
|
||||
offset = itertools.count(smp.off, step=8)
|
||||
|
||||
for xcc in range(smp.xcc):
|
||||
with self.pred_exec(xcc_mask=1 << xcc):
|
||||
for inst, se_idx, sa_idx, wgp_idx in itertools.product(range(smp.inst), range(smp.se), range(smp.sa), range(smp.wgp)):
|
||||
loff = next(offset)
|
||||
if smp.wgp > 1 and not self.dev.iface.is_wgp_active(xcc, se_idx, sa_idx, wgp_idx): continue
|
||||
self.set_grbm(**({'instance':inst} if smp.inst > 1 else ({'se':se_idx}|({'sh':sa_idx, 'wgp':wgp_idx} if self.target[0] != 9 else {}))))
|
||||
|
||||
# Copy counter to memory (src_sel = perf, dst_sel = tc_l2)
|
||||
lo, hi = getattr(self.gc, f'{smp.regsample}_LO'), getattr(self.gc, f'{smp.regsample}_HI', None)
|
||||
self.pkt3(self.pm4.PACKET3_COPY_DATA, (2 << 8) | 4, lo.addr[0], 0, buf + loff)
|
||||
if hi is not None: self.pkt3(self.pm4.PACKET3_COPY_DATA, (2 << 8) | 4, hi.addr[0], 0, buf + (loff + 4))
|
||||
|
||||
self.pmc_reset_counters(en=True)
|
||||
|
||||
### SQTT
|
||||
|
||||
def sqtt_userdata(self, data, *extra_dwords):
|
||||
data_ints = [x[0] for x in struct.iter_unpack('<I', bytes(data))] + list(extra_dwords)
|
||||
for i in range(0, len(data_ints), 2):
|
||||
self.wreg(self.gc.regSQ_THREAD_TRACE_USERDATA_2, *data_ints[i:i+2])
|
||||
|
||||
def sqtt_config(self, tracing:bool):
|
||||
trace_ctrl = {'rt_freq': self.soc.SQ_TT_RT_FREQ_4096_CLK} if self.target < (12,0,0) else {}
|
||||
self.wreg(self.gc.regSQ_THREAD_TRACE_CTRL, draw_event_en=1, spi_stall_en=1, sq_stall_en=1, reg_at_hwm=2, hiwater=1, util_timer=1,
|
||||
mode=int(tracing), **trace_ctrl)
|
||||
|
||||
def sqtt_setup_exec(self, data:AMDProgramData, info:ProgramInfo):
|
||||
self.sqtt_userdata(sqtt.struct_rgp_sqtt_marker_pipeline_bind(identifier=sqtt.RGP_SQTT_MARKER_IDENTIFIER_BIND_PIPELINE,
|
||||
bind_point=(__BIND_POINT_COMPUTE:=1), api_pso_hash=data64_le(data.libhash)))
|
||||
self.sqtt_userdata(sqtt.struct_rgp_sqtt_marker_event(has_thread_dims=1, cmd_id=next(self.dev.sqtt_next_cmd_id)), *info.global_size)
|
||||
|
||||
if SQTT_LIMIT_SE:
|
||||
# Calculate number of CUs per SE to enable based on blocks count. 4 is maximum simd per CU, but on rdna we can trace only 1.
|
||||
cu_per_se = prod([x if isinstance(x, int) else 1 for x in info.global_size]) // ((self.dev.cu_cnt // self.dev.se_cnt) * 4)
|
||||
for xcc in range(self.dev.xccs):
|
||||
with self.pred_exec(xcc_mask=1 << xcc):
|
||||
for i in range(8 if self.target[0] != 9 else 4):
|
||||
if SQTT_LIMIT_SE > 1: mask = 1 if SQTT_ITRACE_SE_MASK.value & (1 << i) else 0 # only run unmasked shader engines
|
||||
else:
|
||||
sa_mask = (1 << (self.dev.iface.props['cu_per_simd_array'] // 2)) - 1
|
||||
cu_mask = (1 << (cu_per_se + (1 if i == 0 else 0))) - 1
|
||||
mask = lo32((cu_mask & sa_mask) | (cu_mask & (sa_mask << 16)) << 16)
|
||||
self.wreg(getattr(self.gc, f'regCOMPUTE_STATIC_THREAD_MGMT_SE{i}'), mask)
|
||||
|
||||
def sqtt_start(self, slot:UOp):
|
||||
self.memory_barrier()
|
||||
win, ses = self.dev.sqtt_win, self.dev.sqtt_ses
|
||||
base = rt_addr(self.prof_buf("sqtt_buf"), self.devs) + slot * win
|
||||
if self.target[0] == 9:
|
||||
self.set_grbm()
|
||||
self.wreg(self.gc.regSQ_THREAD_TRACE_MASK, simd_en=0xf, cu_sel=0, sq_stall_en=1, spi_stall_en=1, reg_stall_en=1, vm_id_mask=0)
|
||||
for se in range(ses):
|
||||
mask = (__SQTT_MISC:=1<<0) | (__SQTT_TIME:=1<<1) | (__SQTT_REG:=1<<2) | (__SQTT_WAVE_START:=1<<3) | (__SQTT_WAVE_END:=1<<6) \
|
||||
| (__SQTT_USERDATA:=1<<12) | (__SQTT_REG_CS:=1<<5) | (__SQTT_REG_CS_PRIV:=1<<15)
|
||||
if (SQTT_ITRACE_SE_MASK.value >> se) & 0b1: mask |= (__SQTTINST:=1<<10) | (__SQTT_INST_PC:=1<<11) | (__SQTT_ISSUE:=1<<13)
|
||||
|
||||
buf0_lo, buf0_hi = [((base + se * self.dev.prof_slots * win) >> sh).cast(dtypes.uint32) for sh in (12, 44)]
|
||||
with self.pred_exec(xcc_mask=1<<(se // self.dev.se_cnt)):
|
||||
self.set_grbm(se=se % self.dev.se_cnt, sh=0)
|
||||
self.wreg(self.gc.regSQ_THREAD_TRACE_TOKEN_MASK, reg_mask=0xf, token_mask=mask)
|
||||
self.wreg(self.gc.regSQ_THREAD_TRACE_TOKEN_MASK2, inst_mask=0xffffffff)
|
||||
self.wreg(self.gc.regSQ_THREAD_TRACE_BASE, buf0_lo)
|
||||
self.wreg(self.gc.regSQ_THREAD_TRACE_BASE2, buf0_hi)
|
||||
self.wreg(self.gc.regSQ_THREAD_TRACE_SIZE, size=win >> 12)
|
||||
self.wreg(self.gc.regSQ_THREAD_TRACE_CTRL, reset_buffer=1)
|
||||
self.wreg(self.gc.regSQ_THREAD_TRACE_MODE, mask_cs=1, autoflush_en=1, mode=1)
|
||||
else:
|
||||
self.spi_config(tracing=True)
|
||||
# One buffer for one SE, mesa does it with a single buffer and ac_sqtt_get_data_offset, but this is simpler and should work just as well
|
||||
for se in range(ses):
|
||||
self.set_grbm(se=se, sh=0)
|
||||
|
||||
buf0_lo, buf0_hi = [((base + se * self.dev.prof_slots * win) >> sh).cast(dtypes.uint32) for sh in (12, 44)]
|
||||
if self.target >= (12,0,0):
|
||||
self.wreg(self.gc.regSQ_THREAD_TRACE_BUF0_SIZE, size=win >> 12)
|
||||
self.wreg(self.gc.regSQ_THREAD_TRACE_BUF0_BASE_LO, buf0_lo)
|
||||
self.wreg(self.gc.regSQ_THREAD_TRACE_BUF0_BASE_HI, buf0_hi)
|
||||
else:
|
||||
self.wreg(self.gc.regSQ_THREAD_TRACE_BUF0_SIZE, self.gc.regSQ_THREAD_TRACE_BUF0_SIZE.encode(size=win >> 12) | buf0_hi)
|
||||
self.wreg(self.gc.regSQ_THREAD_TRACE_BUF0_BASE, buf0_lo)
|
||||
# NOTE: SQTT can only trace instructions on one simd per se, this selects the simd in first wgp in first sa.
|
||||
# For RGP to display instruction trace it has to see it on first SE. Howerver ACE/MEC/whatever does the dispatching starting with second se,
|
||||
# and on amdgpu/non-AM it also does weird things with dispatch order inside se: around 7 times out of 10 it starts from the last cu, but
|
||||
# sometimes not, especially if the kernel has more than one wavefront which means that kernels with small global size might get unlucky and
|
||||
# be dispatched on something else and not be seen in instruction tracing tab. You can force the wavefronts of a kernel to be dispatched on the
|
||||
# CUs you want to by disabling other CUs via bits in regCOMPUTE_STATIC_THREAD_MGMT_SE<x> and trace even kernels that only have one wavefront.
|
||||
# Use SQTT_SIMD_SEL to select which SIMD to trace (0-3). Memory ops show different InstOp values (0x2x vs 0x5x) based on SIMD.
|
||||
cs_wtype = (1 << 6) if self.target >= (12,0,0) else self.soc.SQ_TT_WTYPE_INCLUDE_CS_BIT
|
||||
self.wreg(self.gc.regSQ_THREAD_TRACE_MASK, wtype_include=cs_wtype, simd_sel=SQTT_SIMD_SEL.value, wgp_sel=0, sa_sel=0)
|
||||
reg_include = self.soc.SQ_TT_TOKEN_MASK_SQDEC_BIT | self.soc.SQ_TT_TOKEN_MASK_SHDEC_BIT | self.soc.SQ_TT_TOKEN_MASK_GFXUDEC_BIT | \
|
||||
self.soc.SQ_TT_TOKEN_MASK_COMP_BIT | self.soc.SQ_TT_TOKEN_MASK_CONTEXT_BIT
|
||||
token_exclude = SQTT_TOKEN_EXCLUDE.value | ((1 << self.soc.SQ_TT_TOKEN_EXCLUDE_PERF_SHIFT) if self.target < (12,0,0) else 0)
|
||||
|
||||
# disable instr tracing
|
||||
if not (SQTT_ITRACE_SE_MASK.value >> se) & 0b1:
|
||||
# gfx12 doesn't have enums with all fields, so it's hardcoded, but it's the same as gfx11.
|
||||
token_exclude |= (1 << self.soc.SQ_TT_TOKEN_EXCLUDE_VMEMEXEC_SHIFT | 1 << self.soc.SQ_TT_TOKEN_EXCLUDE_ALUEXEC_SHIFT | \
|
||||
1 << self.soc.SQ_TT_TOKEN_EXCLUDE_VALUINST_SHIFT | 1 << self.soc.SQ_TT_TOKEN_EXCLUDE_IMMEDIATE_SHIFT | \
|
||||
1 << self.soc.SQ_TT_TOKEN_EXCLUDE_INST_SHIFT) if self.target < (12,0,0) else 0x927
|
||||
|
||||
self.wreg(self.gc.regSQ_THREAD_TRACE_TOKEN_MASK, reg_include=reg_include, token_exclude=token_exclude, bop_events_token_include=1,
|
||||
**({} if self.target < (12,0,0) else {'exclude_barrier_wait': 1}))
|
||||
self.sqtt_config(tracing=True)
|
||||
|
||||
self.set_grbm()
|
||||
if self.target[0] != 9: self.wreg(self.gc.regCOMPUTE_THREAD_TRACE_ENABLE, 1)
|
||||
self.memory_barrier()
|
||||
|
||||
# Magic values from src/amd/common/ac_sqtt.c:ac_sqtt_emit_stop and src/amd/common/ac_sqtt.c:ac_sqtt_emit_wait
|
||||
def sqtt_stop(self, slot:UOp):
|
||||
self.memory_barrier()
|
||||
self.set_grbm()
|
||||
ses = self.dev.sqtt_ses
|
||||
wptrs = rt_addr(self.prof_buf("sqtt_wptrs"), self.devs) + slot * (ses * 4)
|
||||
|
||||
# Start shutting everything down
|
||||
if self.target[0] == 9: self.wreg(self.gc.regSQ_THREAD_TRACE_MODE, mask_cs=1, autoflush_en=1, mode=0)
|
||||
else:
|
||||
self.wreg(self.gc.regCOMPUTE_THREAD_TRACE_ENABLE, 0)
|
||||
self.pkt3(self.pm4.PACKET3_EVENT_WRITE, self.pm4.EVENT_TYPE(self.soc.THREAD_TRACE_FINISH) | self.pm4.EVENT_INDEX(0))
|
||||
|
||||
# For each SE wait for finish to complete and copy regSQ_THREAD_TRACE_WPTR to know where in the buffer trace data ends
|
||||
for se in range(ses):
|
||||
with self.pred_exec(xcc_mask=1<<(se // self.dev.se_cnt)):
|
||||
self.set_grbm(se=se % self.dev.se_cnt, sh=0)
|
||||
|
||||
regstatus = self.gc.regSQ_THREAD_TRACE_STATUS.addr[0] - (self.pm4.PACKET3_SET_UCONFIG_REG_START if self.target[0] == 9 else 0)
|
||||
if self.target[0] != 9:
|
||||
self.wait_reg_mem(reg=regstatus, mask=self.gc.regSQ_THREAD_TRACE_STATUS.fields_mask('finish_pending'), op=WAIT_REG_MEM_FUNCTION_EQ, value=0)
|
||||
self.sqtt_config(tracing=False)
|
||||
self.wait_reg_mem(reg=regstatus, mask=self.gc.regSQ_THREAD_TRACE_STATUS.fields_mask('busy'), op=WAIT_REG_MEM_FUNCTION_EQ, value=0)
|
||||
self.pkt3(self.pm4.PACKET3_EVENT_WRITE, self.pm4.EVENT_TYPE(self.soc.CS_PARTIAL_FLUSH) | self.pm4.EVENT_INDEX(EVENT_INDEX_PARTIAL_FLUSH))
|
||||
|
||||
# Copy WPTR to memory (src_sel = perf, dst_sel = tc_l2, wr_confirm = True)
|
||||
self.pkt3(self.pm4.PACKET3_COPY_DATA, 1 << 20 | 2 << 8 | 4, self.gc.regSQ_THREAD_TRACE_WPTR.addr[0], 0, wptrs + se * 4)
|
||||
|
||||
self.set_grbm()
|
||||
if self.target[0] != 9: self.spi_config(tracing=False)
|
||||
self.memory_barrier()
|
||||
|
||||
### exec
|
||||
|
||||
def kernargs(self, call:UOp, prg:UOp, data:AMDProgramData) -> list[UOp]:
|
||||
words = [get_call_arg_uops(call)[gi].getaddr(self.devs) for gi in prg.arg.globals] + \
|
||||
[b.ccast(v.dtype) for v, b in zip(prg.arg.vars, get_call_var_uops(call, prg))] # a bound value is a bare const, the var has the width
|
||||
pad = data.kernargs_segment_size - sum(w.dtype.itemsize for w in words)
|
||||
assert pad >= 0 and pad % 4 == 0, f"bad kernargs padding {pad}"
|
||||
return words + [UOp.const(0, dtypes.uint32)] * (pad // 4) + (dispatch_packet(data, prg.arg) if data.enable_dispatch_ptr else [])
|
||||
|
||||
def exec(self, call:UOp, prg:UOp):
|
||||
data, lib = amd_build_program(self.dev, prg, self.devs)
|
||||
info = prg.arg
|
||||
|
||||
# kernargs: a nested blob linear inside a getaddr, packed into the tail of the cmdbuf
|
||||
ka_words = [get_call_arg_uops(call)[gi].getaddr(self.devs) for gi in info.globals] + \
|
||||
[b.ccast(v.dtype) for v, b in zip(info.vars, get_call_var_uops(call, prg))] # a bound value is a bare const, the var has the width
|
||||
pad = data.kernargs_alloc_size - sum(w.dtype.itemsize for w in ka_words)
|
||||
assert pad >= 0 and pad % 4 == 0, f"bad kernargs padding {pad}"
|
||||
ka = UOp(Ops.LINEAR, src=tuple(ka_words) + (UOp.const(0, dtypes.uint32),) * (pad // 4))
|
||||
ka = UOp(Ops.LINEAR, src=tuple(self.kernargs(call, prg, data)))
|
||||
|
||||
prog_addr = lib.getaddr(self.devs) + data.entry_point_offset
|
||||
scratch_addr = UOp.placeholder((data.private_segment_size,), dtypes.uint8, 0, device=self.devs).rtag("scratch").getaddr(self.devs)
|
||||
@@ -129,41 +370,85 @@ class AMDComputeQueue(HWQueue):
|
||||
dispatch_init = self.gc.regCOMPUTE_DISPATCH_INITIATOR.encode(
|
||||
**({'cs_w32_en': int(data.wave32)} if self.target[0] != 9 else {}), force_start_at_000=1, compute_shader_en=1)
|
||||
self.acquire_mem(gli=0, gl2=0)
|
||||
slot = self.prof_start(data, info, lib)
|
||||
self.wreg(self.gc.regCOMPUTE_PGM_LO, prog_addr >> 8)
|
||||
self.wreg(self.gc.regCOMPUTE_PGM_RSRC1, data.rsrc1, data.rsrc2)
|
||||
self.wreg(self.gc.regCOMPUTE_PGM_RSRC3, data.rsrc3)
|
||||
self.wreg(self.gc.regCOMPUTE_TMPRING_SIZE, self.dev.tmpring_size(data.private_segment_size))
|
||||
for xcc_id in range(self.dev.xccs):
|
||||
self.wreg(self.gc.regCOMPUTE_DISPATCH_SCRATCH_BASE_LO, (scratch_addr + data.private_segment_size // self.dev.xccs * xcc_id) >> 8)
|
||||
for xcc_id in range(self.dev.xccs): # architected flat scratch: each xcc gets its part
|
||||
with self.pred_exec(xcc_mask=1 << xcc_id):
|
||||
self.wreg(self.gc.regCOMPUTE_DISPATCH_SCRATCH_BASE_LO, (scratch_addr + data.private_segment_size // self.dev.xccs * xcc_id) >> 8)
|
||||
self.wreg(self.gc.regCOMPUTE_RESTART_X, 0, 0, 0)
|
||||
self.wreg(self.gc.regCOMPUTE_USER_DATA_0, *user_regs)
|
||||
self.wreg(self.gc.regCOMPUTE_RESOURCE_LIMITS, self.gc.regCOMPUTE_RESOURCE_LIMITS.encode(waves_per_sh=getenv("WAVES_PER_SH")))
|
||||
self.wreg(self.gc.regCOMPUTE_START_X, 0, 0, 0, *info.local_size, 0, 0)
|
||||
self.pkt3(self.pm4.PACKET3_DISPATCH_DIRECT, *info.global_size, dispatch_init)
|
||||
if self.dev.sqtt_enabled: self.pkt3(self.pm4.PACKET3_EVENT_WRITE, self.pm4.EVENT_TYPE(self.soc.THREAD_TRACE_MARKER) | self.pm4.EVENT_INDEX(0))
|
||||
self.pkt3(self.pm4.PACKET3_EVENT_WRITE, self.pm4.EVENT_TYPE(self.soc.CS_PARTIAL_FLUSH) | self.pm4.EVENT_INDEX(EVENT_INDEX_PARTIAL_FLUSH))
|
||||
self.prof_stop(slot)
|
||||
|
||||
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) + 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)
|
||||
with self.pred_exec(xcc_mask=0b1):
|
||||
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):
|
||||
self.release_mem(signal.getaddr(self.devs), value, self.pm4.data_sel__mec_release_mem__send_32_bit_low,
|
||||
self.pm4.int_sel__mec_release_mem__send_interrupt_after_write_confirm, cache_flush=True)
|
||||
with self.pred_exec(xcc_mask=0b1):
|
||||
self.release_mem(signal.getaddr(self.devs), value, self.pm4.data_sel__mec_release_mem__send_32_bit_low,
|
||||
self.pm4.int_sel__mec_release_mem__send_interrupt_after_write_confirm, cache_flush=True)
|
||||
|
||||
def submit(self, cmdbuf:UOp) -> UOp:
|
||||
q = self.dev.compute_queue
|
||||
def submit(self, cmdbuf:UOp) -> UOp: # the ring gets an indirect buffer packet: 4 dwords, put stays aligned so it never wraps mid packet
|
||||
base, off = unwrap_view(cmdbuf)
|
||||
blob = struct.pack("IIII", self.pm4.PACKET3(self.pm4.PACKET3_INDIRECT_BUFFER, 2), 0, 0, cmdbuf.max_numel() // 4 | self.pm4.INDIRECT_BUFFER_VALID)
|
||||
ib = patch(UOp.placeholder((16,), dtypes.uint8, device="CPU", tag=to_name("ib", self.queue)), [(4, base.getaddr(self.devs) + off)], blob)
|
||||
return self.push(self.prof_bump(cmdbuf), ib, self.dev.compute_queue)
|
||||
|
||||
def push(self, cmdbuf:UOp, words:UOp, q, unit:int=4, doorbell_lag:int=0) -> UOp:
|
||||
ring, wptr, doorbell, put = _queue_args(self, q)
|
||||
n, p = words.max_numel() // unit, put.index(0).load() # put counts units
|
||||
i = UOp.range(words.max_numel() // 4, 10, dtype=dtypes.int, src=(cmdbuf,))
|
||||
at = ((p * (unit // 4) + i.cast(p.dtype)) % q.ring.size).cast(dtypes.int)
|
||||
written = ring.index(at).store(words.bitcast(dtypes.uint32).index(i).load()).end(i)
|
||||
w = wptr.after(written).index(0).store(p + n)
|
||||
return doorbell.after(put.after(w).index(0).store(p + n)).index(0).store(p + n - doorbell_lag)
|
||||
|
||||
size_dw = cmdbuf.max_numel() // 4
|
||||
p = put.index(0).load()
|
||||
i = UOp.range(size_dw, 10, dtype=dtypes.int, src=(cmdbuf,))
|
||||
copy = ring.index(((p + i.cast(p.dtype)) % q.ring.size).cast(dtypes.int)).store(cmdbuf.bitcast(dtypes.uint32).index(i).load()).end(i)
|
||||
next_put = p + size_dw
|
||||
flush = UOp.barrier(copy, put.index(0).store(next_put), wptr.index(0).store(next_put))
|
||||
return doorbell.after(flush).index(0).store(next_put)
|
||||
class AMDComputeAQLQueue(AMDComputeQueue): # the ring holds 64 byte aql packets: a dispatch per kernel, the pm4 between them wrapped as an ib
|
||||
def __init__(self, ctx, submit):
|
||||
super().__init__(ctx, submit)
|
||||
self.cmd_addr = UOp.variable("cmdbuf", 0, 2**48, dtypes.uint64) # the packets point into the cmdbuf, its address binds at submit
|
||||
self.pkts:list[UOp] = []
|
||||
self.run_start = 0
|
||||
|
||||
def close_run(self, end:int):
|
||||
if end > self.run_start:
|
||||
hdr = AQL_HDR | (hsa.HSA_PACKET_TYPE_VENDOR_SPECIFIC << hsa.HSA_PACKET_HEADER_TYPE) | (1 << 16)
|
||||
ib = [self.pm4.PACKET3(self.pm4.PACKET3_INDIRECT_BUFFER, 2), self.cmd_addr + self.run_start,
|
||||
(end - self.run_start) // 4 | self.pm4.INDIRECT_BUFFER_VALID]
|
||||
self.pkts += [UOp.const(w, dtypes.uint32) if isinstance(w, int) else w for w in [hdr, *ib, 10, *[0] * 10]]
|
||||
self.run_start = end
|
||||
|
||||
def exec(self, call:UOp, prg:UOp):
|
||||
data, lib = amd_build_program(self.dev, prg, self.devs)
|
||||
self.dev.scratch_buffer(data.private_segment_size) # the queue descriptor holds the scratch
|
||||
slot = self.prof_start(data, prg.arg, lib)
|
||||
self.close_run(len(self.blob))
|
||||
self.blob += bytes(-len(self.blob) % 16)
|
||||
kernarg_address = self.cmd_addr + len(self.blob) # the kernargs go inline in the cmdbuf: the runs skip them
|
||||
self.q(*self.kernargs(call, prg, data))
|
||||
self.pkts += [UOp.const(w, dtypes.uint32) if isinstance(w, int) else w
|
||||
for w in dispatch_packet(data, prg.arg, lib.getaddr(self.devs) + data.desc_offset, kernarg_address)]
|
||||
self.run_start = len(self.blob)
|
||||
self.prof_stop(slot)
|
||||
|
||||
def submit(self, cmdbuf:UOp) -> UOp: # the doorbell is the last packet's index
|
||||
self.close_run(cmdbuf.max_numel())
|
||||
base, off = unwrap_view(cmdbuf)
|
||||
self.blob, self.patches = bytearray(), [] # q again, for the aql stream
|
||||
self.q(*UOp.sink(*self.pkts).substitute({self.cmd_addr: base.getaddr(self.devs) + off}).src)
|
||||
aql = UOp.placeholder((len(self.blob),), dtypes.uint8, device="CPU", tag=to_name("aql", self.queue))
|
||||
return self.push(self.prof_bump(cmdbuf), patch(aql, self.patches, bytes(self.blob)), self.dev.compute_queue, unit=64, doorbell_lag=1)
|
||||
|
||||
# *****************
|
||||
# SDMA
|
||||
@@ -208,6 +493,8 @@ class AMDSDMAQueue(HWQueue):
|
||||
q = unwrap(self.dev.sdma_queue(int(self.queue.split(":")[1])))
|
||||
|
||||
ring, wptr, doorbell, put = _queue_args(self, q)
|
||||
base = unwrap_view(cmdbuf)[0] # in host memory: streamed into the ring, the device never reads it
|
||||
cmdbuf = cmdbuf.substitute({base: base.replace(arg=replace(base.arg, device="CPU"))})
|
||||
|
||||
rs, size_dw = q.ring.size, cmdbuf.max_numel() // 4
|
||||
put_b = put.index(0).load()
|
||||
@@ -222,19 +509,24 @@ class AMDSDMAQueue(HWQueue):
|
||||
flush = UOp.barrier(zero_tail, copy, put.index(0).store(next_put), wptr.index(0).store(next_put))
|
||||
return doorbell.after(flush).index(0).store(next_put)
|
||||
|
||||
def amd_compute_queue(ctx, submit:UOp) -> HWQueue:
|
||||
return (AMDComputeAQLQueue if Device[submit.src[0].arg[0][0]].is_aql else AMDComputeQueue)(ctx, submit)
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AMDProgramData:
|
||||
entry_point_offset:int; rsrc1:int; rsrc2:int; rsrc3:int; wave32:bool
|
||||
private_segment_size:int; kernargs_segment_size:int; kernargs_alloc_size:int
|
||||
desc_offset:int; entry_point_offset:int; rsrc1:int; rsrc2:int; rsrc3:int; wave32:bool; libhash:int
|
||||
private_segment_size:int; group_segment_size:int; kernargs_segment_size:int
|
||||
enable_dispatch_ptr:int; enable_private_segment_sgpr:int
|
||||
|
||||
_amd_program_cache:dict[tuple[bytes, tuple[str, ...]], tuple[AMDProgramData, UOp]] = {}
|
||||
_amd_program_prof:dict[UOp, tuple[str, bytes, bytes]] = {} # placeholder -> (name, lib, key) for its profile event
|
||||
def amd_build_program(dev, prg:UOp, devs:tuple[str, ...]) -> tuple[AMDProgramData, UOp]:
|
||||
# the image parses once per lib, each device set gets its own program buffer of it
|
||||
if (cached:=_amd_program_cache.get(key:=(lib:=prg.src[3].arg, devs))) is None:
|
||||
data, image = _amd_program_image(dev, lib)
|
||||
buf = UOp.placeholder((len(image),), dtypes.uint8, next(UOp.unique_num), device=devs).rtag("program")
|
||||
cached = _amd_program_cache[key] = (data, buf.after(buf.store(UOp(Ops.BINARY, src=(), arg=image).bitcast(buf.dtype))))
|
||||
if PROFILE: _amd_program_prof[buf] = (prg.arg.function_name, lib, prg.key)
|
||||
return cached
|
||||
|
||||
@functools.cache
|
||||
@@ -249,11 +541,11 @@ def _amd_program_image(dev, lib:bytes) -> tuple[AMDProgramData, bytes]:
|
||||
raise RuntimeError("Too many resources requested: group_segment_size")
|
||||
edp = desc.kernel_code_properties & hsa.AMD_KERNEL_CODE_PROPERTIES_ENABLE_SGPR_DISPATCH_PTR
|
||||
|
||||
data = AMDProgramData(entry_point_offset=rodata + desc.kernel_code_entry_byte_offset,
|
||||
data = AMDProgramData(desc_offset=rodata, entry_point_offset=rodata + desc.kernel_code_entry_byte_offset,
|
||||
rsrc1=desc.compute_pgm_rsrc1 | ((1<<20) if dev.target[0]==11 else 0), # priv=1 on gfx11 for cwsr
|
||||
rsrc2=desc.compute_pgm_rsrc2 | (lds<<15), rsrc3=desc.compute_pgm_rsrc3,
|
||||
wave32=bool(desc.kernel_code_properties & 0x400), private_segment_size=desc.private_segment_fixed_size, kernargs_segment_size=desc.kernarg_size,
|
||||
kernargs_alloc_size=desc.kernarg_size + (ctypes.sizeof(hsa.hsa_kernel_dispatch_packet_t) if edp else 0), enable_dispatch_ptr=edp,
|
||||
rsrc2=desc.compute_pgm_rsrc2 | (lds<<15), rsrc3=desc.compute_pgm_rsrc3, wave32=bool(desc.kernel_code_properties & 0x400),
|
||||
libhash=struct.unpack('<Q', hashlib.md5(lib).digest()[:8])[0], private_segment_size=desc.private_segment_fixed_size,
|
||||
group_segment_size=desc.group_segment_fixed_size, kernargs_segment_size=desc.kernarg_size, enable_dispatch_ptr=edp,
|
||||
enable_private_segment_sgpr=desc.kernel_code_properties & hsa.AMD_KERNEL_CODE_PROPERTIES_ENABLE_SGPR_PRIVATE_SEGMENT_BUFFER)
|
||||
return data, bytes(image).ljust(round_up(len(image), 4), b"\x00") # the program is uploaded as whole dwords
|
||||
|
||||
@@ -495,7 +787,8 @@ class PCIIface(PCIIfaceBase):
|
||||
cq = d.compute_queue
|
||||
for b in (cq.put_value, cq.read_ptr, cq.write_ptr): b._buf.view.view(fmt='Q')[0] = 0
|
||||
d.iface.dev_impl.gfx.setup_ring(*cq.params)
|
||||
(tl:=d.timeline._buf.cpu_view().view(fmt='Q'))[0] = tl[1]
|
||||
tl = d.timeline._buf.cpu_view().view(fmt='Q')
|
||||
tl[0] = tl[1]
|
||||
|
||||
def sleep(self, timeout):
|
||||
if hasattr(self.pci_dev, 'irq_poller') and self.pci_dev.irq_poller is not None and (events_cnt:=len(self.pci_dev.irq_poller.poll(timeout))):
|
||||
@@ -541,7 +834,7 @@ class AMDDevice(HCQ2Compiled):
|
||||
timestamp_divider = 100.0 # AMD GPU clock: ticks/us
|
||||
max_scratch_psize = 0
|
||||
pm_encode = PatternMatcher([
|
||||
(UPat(Ops.CUSTOM_FUNCTION, arg="submit_amd_compute", name="submit"), lambda ctx, submit: encode_submit(AMDComputeQueue(ctx, submit))),
|
||||
(UPat(Ops.CUSTOM_FUNCTION, arg="submit_amd_compute", name="submit"), lambda ctx, submit: encode_submit(amd_compute_queue(ctx, submit))),
|
||||
(UPat(Ops.CUSTOM_FUNCTION, arg="submit_amd_copy", name="submit"), lambda ctx, submit: encode_submit(AMDSDMAQueue(ctx, submit))),
|
||||
])
|
||||
|
||||
@@ -578,10 +871,6 @@ class AMDDevice(HCQ2Compiled):
|
||||
bases={i: tuple(getattr(self.ip_off, f'NBIO_BASE__INST{i}_SEG{s}', 0) for s in range(9)) for i in range(6)})
|
||||
|
||||
self.is_aql = getenv("AMD_AQL", int(self.xccs > 1))
|
||||
if self.is_aql:
|
||||
self.pm4_ibs = self.iface.alloc(0x2000 if self.is_usb else (16 << 20), uncached=True, cpu_access=True)
|
||||
self.pm4_ib_alloc = BumpAllocator(self.pm4_ibs.size, wrap=True)
|
||||
|
||||
self.max_copy_size = 0x40000000 if self.iface.ip_versions[am.SDMA0_HWIP][0] >= 5 else 0x400000
|
||||
self.sdma_queues:dict = {}
|
||||
self.has_copy_queue = not getenv("AMD_DISABLE_SDMA")
|
||||
@@ -590,37 +879,32 @@ class AMDDevice(HCQ2Compiled):
|
||||
|
||||
# Scratch setup
|
||||
self.max_private_segment_size = 0
|
||||
self.pm_bufferize = PatternMatcher([(UPat(Ops.PARAM, tag="scratch", name="b"), lambda ctx, b: ctx.scratch_buffer(b.max_numel()))]) + self.pm_bufferize
|
||||
self.pm_bufferize = PatternMatcher([
|
||||
(UPat(Ops.PARAM, tag="scratch", name="b"), lambda ctx, b: ctx.scratch_buffer(b.max_numel())),
|
||||
(UPat(Ops.PARAM, tag="program", name="b"), lambda ctx, b: ctx.program_buffer(b)),
|
||||
]) + self.pm_bufferize
|
||||
|
||||
if self.is_usb:
|
||||
self.pm_bufferize = pm_usb_bufferize + self.pm_bufferize
|
||||
raise NotImplementedError("usb amd is not migrated to sealed submits yet") # a usb pm_lower can override the whole submit graph
|
||||
|
||||
self.pmc_enabled:bool = PROFILE > 0 and PMC > 0
|
||||
if self.pmc_enabled:
|
||||
# SQTT is disabled by default because of runtime overhead and big file sizes (~200mb to Tensor.full() two 4096x4096 tensors and matmul them)
|
||||
self.pmc_enabled, self.sqtt_enabled = PROFILE > 0 and PMC > 0, PROFILE > 0 and SQTT > 0
|
||||
if self.pmc_enabled or self.sqtt_enabled:
|
||||
self.iface.require_profile_mode()
|
||||
|
||||
self.pmc_sched:list[PMCSample] = []
|
||||
self.prof_slots, self.prof_read, self.pmc_sched, self.sqtt_next_cmd_id = getenv("PROF_SLOTS", 32), 0, [], itertools.count(0)
|
||||
self.sqtt_ses, self.sqtt_win = self.se_cnt * self.xccs, (getenv("SQTT_BUFFER_SIZE", 256) << 20) // self.prof_slots # mb, per shader engine
|
||||
self.pm_bufferize = PatternMatcher([(UPat(Ops.PARAM, tag=n), lambda ctx, n=n: getattr(ctx, n))
|
||||
for n in ("prof_log", "pmc_buf", "sqtt_buf", "sqtt_wptrs")]) + self.pm_bufferize
|
||||
if self.pmc_enabled:
|
||||
self.pmc_counters = import_pmc(self.target)
|
||||
|
||||
# validate counters: SQ for SIMD busy/instruction counts, LDS stats, GRBM for GPU cycles, L2 cache hits/misses
|
||||
l2, lds = ("TCC", "SQ") if self.target[0] == 9 else ("GL2C", "SQC")
|
||||
pmc_default = f"SQ_BUSY_CYCLES,SQ_INSTS_VALU,SQ_INSTS_SALU,{lds}_LDS_IDX_ACTIVE,{lds}_LDS_BANK_CONFLICT,GRBM_GUI_ACTIVE,{l2}_HIT,{l2}_MISS"
|
||||
for k in (PMC_COUNTERS:=getenv("PMC_COUNTERS", pmc_default).split(",")):
|
||||
self.pmc_names = getenv("PMC_COUNTERS", pmc_default).split(",")
|
||||
for k in self.pmc_names:
|
||||
if k not in self.pmc_counters: raise RuntimeError(f"PMC counter {k} is not supported. Available: {','.join(self.pmc_counters.keys())}")
|
||||
|
||||
raise NotImplementedError("PMC start not migrated to hcq2 yet")
|
||||
|
||||
# SQTT is disabled by default because of runtime overhead and big file sizes (~200mb to Tensor.full() two 4096x4096 tensors and matmul them)
|
||||
self.sqtt_enabled:bool = PROFILE > 0 and SQTT > 0
|
||||
if self.sqtt_enabled:
|
||||
self.iface.require_profile_mode()
|
||||
|
||||
SQTT_BUFFER_SIZE = getenv("SQTT_BUFFER_SIZE", 256) # in mb, per shader engine
|
||||
self.sqtt_buffers = [self.allocator.alloc(SQTT_BUFFER_SIZE<<20, BufferSpec(nolru=True, uncached=True)) for _ in range(self.se_cnt * self.xccs)]
|
||||
self.sqtt_wptrs = self.allocator.alloc(round_up(self.se_cnt * self.xccs * 4, 0x1000), BufferSpec(cpu_access=True, nolru=True))
|
||||
self.sqtt_next_cmd_id = itertools.count(0)
|
||||
|
||||
def create_queue(self, queue_type, ring_size, ctx_save_restore_size=0, eop_buffer_size=0, ctl_stack_size=0, debug_memory_size=0, idx=0):
|
||||
ring = Buffer(self.device, ring_size // 4, dtypes.uint32, options=BufferSpec(uncached=True, cpu_access=True), preallocate=True)
|
||||
gart = Buffer(self.device, 0x100, dtypes.uint8, options=BufferSpec(uncached=True, cpu_access=True), preallocate=True)
|
||||
@@ -630,7 +914,8 @@ class AMDDevice(HCQ2Compiled):
|
||||
self.aql_desc = hsa.amd_queue_t(queue_properties=hsa.AMD_QUEUE_PROPERTIES_IS_PTR64 | hsa.AMD_QUEUE_PROPERTIES_ENABLE_PROFILING,
|
||||
read_dispatch_id_field_base_byte_offset=getattr(hsa.amd_queue_t, 'read_dispatch_id').offset,
|
||||
max_cu_id=(self.cu_cnt * self.xccs) - 1, max_wave_id=self.waves_per_cu - 1)
|
||||
self.aql_gart._buf.cpu_view().view(fmt='B')[:ctypes.sizeof(self.aql_desc)] = bytes(self.aql_desc)
|
||||
if hasattr(self, 'scratch'): self.aql_scratch()
|
||||
else: self.aql_gart._buf.cpu_view().view(fmt='B')[:ctypes.sizeof(self.aql_desc)] = bytes(self.aql_desc)
|
||||
|
||||
cwsr_buffer_size = round_up((ctx_save_restore_size + debug_memory_size) * self.xccs, mmap.PAGESIZE)
|
||||
cwsr_buffer = Buffer(self.device, cwsr_buffer_size, dtypes.uint8, preallocate=True) if ctx_save_restore_size else None
|
||||
@@ -681,24 +966,7 @@ class AMDDevice(HCQ2Compiled):
|
||||
num_waves = (size_per_xcc // (wave_scratch * mem_alignment_size)) // (self.se_cnt if self.target[0] != 9 else 1)
|
||||
|
||||
tmpring_t = getattr(hsa, f'union_COMPUTE_TMPRING_SIZE{"_GFX"+str(self.target[0]) if self.target[0] != 9 else ""}_bitfields')
|
||||
tmpring = int.from_bytes(tmpring_t(WAVES=min(num_waves, max_scratch_waves), WAVESIZE=wave_scratch), 'little')
|
||||
|
||||
if hasattr(self, 'aql_desc'):
|
||||
gfx9_rsrc = {'NUM_FORMAT':hsa.BUF_NUM_FORMAT_UINT, 'DATA_FORMAT':hsa.BUF_DATA_FORMAT_32, 'ELEMENT_SIZE':1, 'INDEX_STRIDE':3}
|
||||
rsrc = {'DST_SEL_X':hsa.SQ_SEL_X, 'DST_SEL_Y':hsa.SQ_SEL_Y, 'DST_SEL_Z':hsa.SQ_SEL_Z, 'DST_SEL_W':hsa.SQ_SEL_W, 'ADD_TID_ENABLE':1,
|
||||
'TYPE':hsa.SQ_RSRC_BUF, **(gfx9_rsrc if self.target[0] == 9 else {'FORMAT':hsa.BUF_FORMAT_32_UINT, 'OOB_SELECT':2})}
|
||||
rsrc1_t = getattr(hsa, f'union_SQ_BUF_RSRC_WORD1{"_GFX11" if self.target[0] != 9 else ""}_bitfields')
|
||||
rsrc3_t = getattr(hsa, f'union_SQ_BUF_RSRC_WORD3{"_GFX"+str(self.target[0]) if self.target[0] != 9 else ""}_bitfields')
|
||||
|
||||
self.aql_desc.scratch_backing_memory_location = int(self.scratch.get_buf().va_addr)
|
||||
self.aql_desc.scratch_wave64_lane_byte_size = self.max_private_segment_size * lanes_per_wave // 64
|
||||
self.aql_desc.scratch_resource_descriptor[:] = [lo32(self.scratch.get_buf().va_addr),
|
||||
int.from_bytes(rsrc1_t(BASE_ADDRESS_HI=hi32(self.scratch.get_buf().va_addr), SWIZZLE_ENABLE=1), 'little'),
|
||||
lo32(size_per_xcc), int.from_bytes(bytes(rsrc3_t(**rsrc)), 'little')]
|
||||
self.aql_desc.compute_tmpring_size = tmpring
|
||||
self.aql_gart._buf.cpu_view()[:ctypes.sizeof(self.aql_desc)] = bytes(self.aql_desc)
|
||||
|
||||
return tmpring
|
||||
return int.from_bytes(tmpring_t(WAVES=min(num_waves, max_scratch_waves), WAVESIZE=wave_scratch), 'little')
|
||||
|
||||
def scratch_buffer(self, private_segment_size):
|
||||
AMDDevice.max_scratch_psize = private_segment_size = max(private_segment_size, 128, AMDDevice.max_scratch_psize)
|
||||
@@ -709,8 +977,79 @@ class AMDDevice(HCQ2Compiled):
|
||||
size_per_xcc = size_per_thread * lanes_per_wave * self.iface.props['max_slots_scratch_cu'] * self.cu_cnt
|
||||
self.scratch = Buffer(self.device, size_per_xcc * self.xccs, dtypes.uint8, options=BufferSpec(nolru=True), preallocate=True)
|
||||
self.max_private_segment_size = private_segment_size
|
||||
if hasattr(self, 'aql_desc'): self.aql_scratch()
|
||||
return self.scratch
|
||||
|
||||
def aql_scratch(self):
|
||||
gfx9_rsrc = {'NUM_FORMAT':hsa.BUF_NUM_FORMAT_UINT, 'DATA_FORMAT':hsa.BUF_DATA_FORMAT_32, 'ELEMENT_SIZE':1, 'INDEX_STRIDE':3}
|
||||
rsrc = {'DST_SEL_X':hsa.SQ_SEL_X, 'DST_SEL_Y':hsa.SQ_SEL_Y, 'DST_SEL_Z':hsa.SQ_SEL_Z, 'DST_SEL_W':hsa.SQ_SEL_W, 'ADD_TID_ENABLE':1,
|
||||
'TYPE':hsa.SQ_RSRC_BUF, **(gfx9_rsrc if self.target[0] == 9 else {'FORMAT':hsa.BUF_FORMAT_32_UINT, 'OOB_SELECT':2})}
|
||||
rsrc1_t = getattr(hsa, f'union_SQ_BUF_RSRC_WORD1{"_GFX11" if self.target[0] != 9 else ""}_bitfields')
|
||||
rsrc3_t = getattr(hsa, f'union_SQ_BUF_RSRC_WORD3{"_GFX"+str(self.target[0]) if self.target[0] != 9 else ""}_bitfields')
|
||||
|
||||
base = self.scratch._buf.va_addr
|
||||
self.aql_desc.scratch_backing_memory_location = base
|
||||
self.aql_desc.scratch_wave64_lane_byte_size = self.max_private_segment_size
|
||||
self.aql_desc.scratch_resource_descriptor[:] = [lo32(base), int.from_bytes(rsrc1_t(BASE_ADDRESS_HI=hi32(base), SWIZZLE_ENABLE=1), 'little'),
|
||||
lo32(self.scratch.nbytes // self.xccs), int.from_bytes(bytes(rsrc3_t(**rsrc)), 'little')]
|
||||
self.aql_desc.compute_tmpring_size = self.tmpring_size(self.max_private_segment_size)
|
||||
self.aql_gart._buf.cpu_view()[:ctypes.sizeof(self.aql_desc)] = bytes(self.aql_desc)
|
||||
|
||||
def _prof_buffer(self, size:int, dtype, host:bool=False) -> Buffer:
|
||||
buf = Buffer(self.device, size, dtype, options=BufferSpec(host=host, nolru=True, uncached=True, cpu_access=True), preallocate=True)
|
||||
buf._buf.cpu_view().view(fmt='B')[:buf.nbytes] = bytes(buf.nbytes)
|
||||
return buf
|
||||
|
||||
@functools.cached_property
|
||||
def prof_log(self) -> Buffer: return self._prof_buffer(1 + self.prof_slots, dtypes.uint64, host=True)
|
||||
@property
|
||||
def pmc_size(self) -> int: return self.pmc_sched[-1].off + self.pmc_sched[-1].size
|
||||
@functools.cached_property
|
||||
def pmc_buf(self) -> Buffer: return self._prof_buffer(self.pmc_size * self.prof_slots, dtypes.uint8)
|
||||
@functools.cached_property
|
||||
def sqtt_buf(self) -> Buffer: return self._prof_buffer(self.sqtt_win * self.prof_slots * self.sqtt_ses, dtypes.uint8)
|
||||
@functools.cached_property
|
||||
def sqtt_wptrs(self) -> Buffer: return self._prof_buffer(self.prof_slots * self.sqtt_ses, dtypes.uint32)
|
||||
|
||||
def program_buffer(self, b:UOp) -> Buffer:
|
||||
if b not in self.prog_bufs:
|
||||
buf = self.prog_bufs[b] = Buffer(self.device, b.max_numel(), b.dtype, options=BufferSpec(cpu_access=True, nolru=True)).ensure_allocated()
|
||||
if PROFILE:
|
||||
name, lib, key = _amd_program_prof[b]
|
||||
Compiled.profile_events.append(ProfileProgramEvent(self.device, name, lib, buf._buf.va_addr, b.arg.slot, key))
|
||||
return self.prog_bufs[b]
|
||||
|
||||
def sqtt_trace(self, slot:int, se:int) -> bytes:
|
||||
off = (se * self.prof_slots + slot) * self.sqtt_win
|
||||
wptr = (self.sqtt_wptrs._buf.cpu_view().view(fmt='I')[slot * self.sqtt_ses + se] & 0x1FFFFFFF) * 32
|
||||
if self.target[:2] == (11, 0): wptr -= (((self.sqtt_buf._buf.va_addr + off) // 32) & 0x1FFFFFFF) * 32
|
||||
assert 0 <= wptr <= self.sqtt_win, f"{wptr} > {self.sqtt_win}, should never happen"
|
||||
if wptr >= self.sqtt_win - 32: # the wptr stops at the last dword when the window overflows
|
||||
print(colored(f"{self.device}: Warning: SQTT buffer is full (SE {se})! Increase SQTT buffer with SQTT_BUFFER_SIZE=X (in MB)", "yellow"))
|
||||
blob = bytes(self.sqtt_buf._buf.cpu_view()[off:off + wptr])
|
||||
return (struct.pack('<Q', 0x11 | (4 << 13) | (0xf << 16) | (se << 24)) + blob) if self.target[0] == 9 else blob
|
||||
|
||||
def _at_profile_finalize(self): # the calibration kernels aren't profiles
|
||||
self.synchronize()
|
||||
super()._at_profile_finalize()
|
||||
if self.pmc_enabled or self.sqtt_enabled: self.prof_read = self.prof_log._buf.cpu_view().view(fmt='Q')[0]
|
||||
|
||||
def collect_prof(self):
|
||||
if self.pmc_enabled or self.sqtt_enabled:
|
||||
log = self.prof_log._buf.cpu_view().view(fmt='Q')
|
||||
if (lost:=log[0] - self.prof_read - self.prof_slots) > 0:
|
||||
print(colored(f"{self.device}: Warning: {lost} kernel profiles were overwritten: synchronize more often or raise PROF_SLOTS", "yellow"))
|
||||
for k in range(max(self.prof_read, log[0] - self.prof_slots), log[0]):
|
||||
slot, tag = k % self.prof_slots, log[1 + k % self.prof_slots]
|
||||
if self.pmc_enabled:
|
||||
blob = bytes(self.pmc_buf._buf.cpu_view()[slot * self.pmc_size:(slot + 1) * self.pmc_size])
|
||||
Compiled.profile_events.append(ProfilePMCEvent(self.device, tag, self.pmc_sched, blob, k))
|
||||
for se in range(self.sqtt_ses if self.sqtt_enabled else 0):
|
||||
itrace = bool((SQTT_ITRACE_SE_MASK.value >> se) & 1)
|
||||
Compiled.profile_events.append(ProfileSQTTEvent(self.device, tag, se, self.sqtt_trace(slot, se), itrace, k))
|
||||
self.prof_read = log[0]
|
||||
super().collect_prof()
|
||||
|
||||
def on_device_hang(self): self.iface.on_device_hang()
|
||||
|
||||
def device_props(self): return self.iface.props
|
||||
|
||||
@@ -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))).clone().contiguous_backward()
|
||||
return (h + self.feed_forward(self.ffn_norm(h))).contiguous().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).clone().is_param_(False)
|
||||
self.freqs_cis = precompute_freqs_cis(dim // n_heads, self.max_context * 2, rope_theta).contiguous().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):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import unittest, contextlib
|
||||
from tinygrad import Device, Tensor, Context, TinyJit, dtypes
|
||||
from test.helpers import is_hcq2_device
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo
|
||||
from tinygrad.device import Compiled, ProfileProgramEvent
|
||||
from tinygrad.runtime.ops_amd import ProfileSQTTEvent
|
||||
@@ -114,8 +115,7 @@ class TestSQTTProfiler(unittest.TestCase):
|
||||
kernel_name = sqtt[0]["name"]
|
||||
for i,e in enumerate(sqtt[1:], start=1): self.assertEqual(e["name"], f"{kernel_name} n{i+1}")
|
||||
|
||||
# TODO: can we trace SQTT for graphed kernels?
|
||||
def test_jit_graph(self, kernel_count=3*1):
|
||||
def test_jit_graph(self, kernel_count=3*(5 if is_hcq2_device() else 1)): # hcq2 traces the graphed kernels too
|
||||
@TinyJit
|
||||
def f(a): return ((a + 1).contiguous() + 2).contiguous().sum()
|
||||
t = Tensor.empty(32)
|
||||
|
||||
@@ -45,16 +45,6 @@ class TestAssign(unittest.TestCase):
|
||||
c.realize()
|
||||
assert_kernel_count(2 if is_hcq2_device() else 1)
|
||||
|
||||
def test_assign_copy_retained_uses(self):
|
||||
for use in (lambda x: x.reshape(1, 3), lambda x: x + 1):
|
||||
with self.subTest(use=use):
|
||||
x = Tensor([1., 2, 3], device="PYTHON").to(None)
|
||||
retained = use(x)
|
||||
dest = Tensor.empty(3).assign(x)
|
||||
del x
|
||||
dest.realize().assign(0).realize()
|
||||
self.assertEqual(retained.tolist(), [[1., 2, 3]] if retained.ndim == 2 else [2., 3, 4])
|
||||
|
||||
def test_assign_slice(self):
|
||||
X = Tensor([1,2,3,4]).realize()
|
||||
xs = X[2:4]
|
||||
@@ -1024,10 +1014,10 @@ class TestAssignToUnrealizedView(unittest.TestCase):
|
||||
# TODO: broken now
|
||||
self.assertEqual(c.tolist(), [[0,0],[0,0]])
|
||||
|
||||
def test_clone(self):
|
||||
def test_contiguous(self):
|
||||
t = Tensor([[1,2],[3,4]]).contiguous().realize()
|
||||
c = t.permute(1,0).clone()
|
||||
self.assertIs(c.uop.base.op, Ops.AFTER)
|
||||
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())
|
||||
self.assertEqual(c.tolist(), [[1,1],[2,1]])
|
||||
|
||||
@@ -1042,16 +1032,6 @@ class TestAssignToUnrealizedView(unittest.TestCase):
|
||||
# TODO: broken now
|
||||
self.assertEqual(cb.tolist(), [[1,2],[3,4]])
|
||||
|
||||
def test_detach_buffer_assignment(self):
|
||||
for realized in (False, True):
|
||||
with self.subTest(realized=realized):
|
||||
base = Tensor([1., 2., 3.])
|
||||
if realized: base.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)
|
||||
@@ -1063,10 +1043,10 @@ class TestAssignToUnrealizedView(unittest.TestCase):
|
||||
# TODO: broken now
|
||||
self.assertEqual(d.tolist(), [[0,0],[0,0]])
|
||||
|
||||
def test_detach_clone(self):
|
||||
def test_detach_contiguous(self):
|
||||
t = Tensor([[1,2],[3,4]]).contiguous().realize()
|
||||
d = t.permute(1,0).clone().detach()
|
||||
self.assertIs(d.uop.base.op, Ops.AFTER)
|
||||
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())
|
||||
self.assertEqual(d.tolist(), [[1,1],[2,1]])
|
||||
|
||||
|
||||
@@ -86,8 +86,8 @@ class TestReduceOpsConstFolding(unittest.TestCase):
|
||||
def test_zero_size_realize_folded(self):
|
||||
# non contiguous folded output doesn't realize
|
||||
_check_ast_count(0, Tensor.empty(1, 0).sum())
|
||||
# An explicitly cloned folded constant still owns persistent storage.
|
||||
a = Tensor.empty(1, 0).sum().clone()
|
||||
# contiguous folded const can still schedule
|
||||
a = Tensor.empty(1, 0).sum().contiguous()
|
||||
_check_ast_count(2, a+2)
|
||||
self.assertIs(a.uop.base.op, Ops.BUFFER)
|
||||
np.testing.assert_equal((Tensor.empty(1, 0).sum().contiguous()+2).numpy(), 2)
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import unittest, random
|
||||
from tinygrad import Tensor, Device, nn, GlobalCounters, TinyJit, dtypes, Variable
|
||||
from tinygrad.uop.ops import Ops, UOp, AxisType, graph_rewrite
|
||||
from tinygrad.helpers import getenv, prod, Context
|
||||
from tinygrad.helpers import prod, Context
|
||||
from tinygrad.nn.state import get_parameters
|
||||
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
|
||||
|
||||
settings.register_profile("my_profile", max_examples=200, deadline=None, derandomize=getenv("DERANDOMIZE_CI", False))
|
||||
settings.load_profile("my_profile")
|
||||
|
||||
d0 = f"{Device.DEFAULT}:0"
|
||||
d1 = f"{Device.DEFAULT}:1"
|
||||
d2 = f"{Device.DEFAULT}:2"
|
||||
@@ -129,17 +125,21 @@ class TestMultiTensor(unittest.TestCase):
|
||||
run_linear(linear, var_vals)
|
||||
np.testing.assert_equal(xt.numpy(), X_np[i*2:i*2+2])
|
||||
|
||||
@given(strat.sampled_from((devices_2, devices_3)),
|
||||
strat.sampled_from((Ops.ADD, Ops.MUL, Ops.MAX)),
|
||||
strat.sampled_from((None, 0, 1)), strat.sampled_from((None, 0, 1)))
|
||||
def test_simple_reduce(self, devices, rop, shard_axis, reduce_axis):
|
||||
N = 4 * len(devices)
|
||||
X = (Tensor.rand(N*N)-1).reshape(N, N).shard_(devices, shard_axis)
|
||||
n = X.numpy()
|
||||
f = {Ops.ADD: lambda x: x.sum(reduce_axis), Ops.MUL: lambda x: x.prod(reduce_axis), Ops.MAX: lambda x: x.max(reduce_axis)}[rop]
|
||||
fX = f(X)
|
||||
fn = f(n)
|
||||
np.testing.assert_allclose(fX.numpy(), fn, rtol=1e-6, atol=1e-6)
|
||||
def test_simple_reduce(self):
|
||||
for devices, rop, shard_axis, reduce_axis in [
|
||||
(devices_2, Ops.ADD, None, None), (devices_2, Ops.ADD, 0, 0), (devices_2, Ops.ADD, 0, 1),
|
||||
(devices_2, Ops.ADD, 1, 0), (devices_2, Ops.ADD, 1, 1),
|
||||
(devices_3, Ops.ADD, 0, 0), (devices_3, Ops.ADD, 1, 0),
|
||||
(devices_2, Ops.MUL, 0, 1), (devices_2, Ops.MUL, 1, 1), (devices_3, Ops.MUL, 0, 0),
|
||||
(devices_2, Ops.MAX, 0, 1), (devices_3, Ops.MAX, 1, 0)]:
|
||||
with self.subTest(devices=len(devices), op=rop.name, shard_axis=shard_axis, reduce_axis=reduce_axis):
|
||||
N = 4 * len(devices)
|
||||
X = (Tensor.rand(N*N)-1).reshape(N, N).shard_(devices, shard_axis)
|
||||
n = X.numpy()
|
||||
f = {Ops.ADD: lambda x: x.sum(reduce_axis), Ops.MUL: lambda x: x.prod(reduce_axis), Ops.MAX: lambda x: x.max(reduce_axis)}[rop]
|
||||
fX = f(X)
|
||||
fn = f(n)
|
||||
np.testing.assert_allclose(fX.numpy(), fn, rtol=1e-6, atol=1e-6)
|
||||
|
||||
def test_stack(self):
|
||||
X = Tensor.rand(4, 4).shard_(devices_2, 0)
|
||||
@@ -176,21 +176,21 @@ class TestMultiTensor(unittest.TestCase):
|
||||
def test_allreduce_naive_jit(self):
|
||||
with Context(RING=0):
|
||||
jit_allreduce = TinyJit(_test_allreduce)
|
||||
for _ in range(5):
|
||||
for _ in range(3):
|
||||
a,b = jit_allreduce(Tensor.rand(256, 256))
|
||||
np.testing.assert_almost_equal(a.numpy(), b.numpy(), decimal=5)
|
||||
|
||||
def test_allreduce_ring_jit(self):
|
||||
with Context(RING=2):
|
||||
jit_allreduce = TinyJit(_test_allreduce)
|
||||
for _ in range(5):
|
||||
for _ in range(3):
|
||||
a,b = jit_allreduce(Tensor.rand(256, 256))
|
||||
np.testing.assert_almost_equal(a.numpy(), b.numpy(), decimal=5)
|
||||
|
||||
def test_allreduce_all2all_jit(self):
|
||||
with Context(ALL2ALL=2):
|
||||
jit_allreduce = TinyJit(_test_allreduce)
|
||||
for _ in range(5):
|
||||
for _ in range(3):
|
||||
a,b = jit_allreduce(Tensor.rand(256, 256))
|
||||
np.testing.assert_almost_equal(a.numpy(), b.numpy(), decimal=5)
|
||||
|
||||
@@ -212,7 +212,7 @@ class TestMultiTensor(unittest.TestCase):
|
||||
|
||||
def test_fuzz_allreduce(self):
|
||||
random.seed(41)
|
||||
for it in range(2):
|
||||
for it in range(1):
|
||||
for n in range(2, 4+1):
|
||||
shape = tuple([(n if i == 0 else 1) * random.randint(1, 10) for i in range(random.randint(1, 4))])
|
||||
t = Tensor.rand(shape).shard_(tuple([d0, d1, d2, d3][:n]), 0)
|
||||
@@ -445,6 +445,7 @@ class TestMultiBufferView(unittest.TestCase):
|
||||
|
||||
@unittest.skipIf(not_support_multi_device(), "need multi")
|
||||
class Test2DShard(unittest.TestCase):
|
||||
@needs_second_gpu
|
||||
def setUp(self):
|
||||
self.devices_4 = tuple(f"{Device.DEFAULT}:{i}" for i in range(4))
|
||||
self.rng = UOp.range(4, -1, AxisType.DEVICE)
|
||||
@@ -460,6 +461,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)
|
||||
@@ -513,7 +523,8 @@ class TestMultiTransformer(unittest.TestCase):
|
||||
else: v.shard_(device, axis=None)
|
||||
|
||||
last_tok = 0
|
||||
for i in range(5):
|
||||
# i=0: bypasses jit, i=1: jit warmup, i=2: capture and run, i>=3: re-execute jit with new start_pos (catches stale bindings)
|
||||
for i in range(4):
|
||||
real_tok = real_model(Tensor([[last_tok]], device=Device.DEFAULT), i).item()
|
||||
shard_tok = shard_model(Tensor([[last_tok]], device=device), i).item()
|
||||
|
||||
|
||||
@@ -3107,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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -115,8 +115,7 @@ class TestSchedule(unittest.TestCase):
|
||||
idx = Tensor([1,2,5,6], dtype=dtypes.int32)
|
||||
flat_base[idx] = Tensor([99,99,99,99])
|
||||
base.assign(flat_base.reshape(4, 4))
|
||||
# The pending clone is already contiguous, so assign-back needs no separate contiguous buffer.
|
||||
sched = check_schedule(base, 2)
|
||||
sched = check_schedule(base, 4)
|
||||
run_linear(*sched)
|
||||
expected = list(range(16))
|
||||
for i, v in zip([1,2,5,6], [99,99,99,99]): expected[i] = v
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import unittest, operator
|
||||
from tinygrad import Tensor, TinyJit, Variable, dtypes, Device
|
||||
from tinygrad.helpers import Context
|
||||
import numpy as np
|
||||
|
||||
class TestSetitem(unittest.TestCase):
|
||||
@@ -75,11 +74,6 @@ class TestSetitem(unittest.TestCase):
|
||||
t.detach()[1, 2] = 5
|
||||
self.assertEqual(t[1, 2].item(), 5.0)
|
||||
|
||||
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()
|
||||
@@ -168,21 +162,20 @@ class TestSetitem(unittest.TestCase):
|
||||
np.testing.assert_allclose(t.numpy(), n)
|
||||
|
||||
def test_jit_setitem_variable_offset(self):
|
||||
with Context(CHECK_OOB=0):
|
||||
@TinyJit
|
||||
def f(t:Tensor, a:Tensor, v:Variable):
|
||||
t.shrink(((v,v+1), None)).assign(a).realize()
|
||||
@TinyJit
|
||||
def f(t:Tensor, a:Tensor, v:Variable):
|
||||
t.shrink(((v,v+1), None)).assign(a).realize()
|
||||
|
||||
t = Tensor.zeros(6, 6).contiguous().realize()
|
||||
n = np.zeros((6, 6))
|
||||
t = Tensor.zeros(6, 6).contiguous().realize()
|
||||
n = np.zeros((6, 6))
|
||||
|
||||
for i in range(6):
|
||||
v = Variable("v", 0, 6).bind(i)
|
||||
a = Tensor.full((1, 6), fill_value=i+1, dtype=dtypes.float).contiguous()
|
||||
n[i, :] = i+1
|
||||
f(t, a, v)
|
||||
np.testing.assert_allclose(t.numpy(), n)
|
||||
np.testing.assert_allclose(t.numpy(), [[1,1,1,1,1,1],[2,2,2,2,2,2],[3,3,3,3,3,3],[4,4,4,4,4,4],[5,5,5,5,5,5],[6,6,6,6,6,6]])
|
||||
for i in range(6):
|
||||
v = Variable("v", 0, 6).bind(i)
|
||||
a = Tensor.full((1, 6), fill_value=i+1, dtype=dtypes.float).contiguous()
|
||||
n[i, :] = i+1
|
||||
f(t, a, v)
|
||||
np.testing.assert_allclose(t.numpy(), n)
|
||||
np.testing.assert_allclose(t.numpy(), [[1,1,1,1,1,1],[2,2,2,2,2,2],[3,3,3,3,3,3],[4,4,4,4,4,4],[5,5,5,5,5,5],[6,6,6,6,6,6]])
|
||||
|
||||
def test_setitem_overlapping_inplace1(self):
|
||||
t = Tensor([[3.0], [2.0], [1.0]]).contiguous()
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import unittest, contextlib, ctypes, gc, numpy as np
|
||||
from unittest.mock import patch
|
||||
from tinygrad import Device, Tensor, TinyJit, Variable, dtypes, GlobalCounters
|
||||
from tinygrad.device import Buffer
|
||||
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.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
|
||||
@@ -42,6 +42,25 @@ 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
|
||||
@@ -190,7 +209,9 @@ class TestHCQ2Core(unittest.TestCase):
|
||||
|
||||
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",), "NV": ("timeline",), "QCOM": ("_stack", "dummy")}[Device.DEFAULT.split(":")[0]]
|
||||
dev = Device[Device.DEFAULT]
|
||||
names = {"AMD": () if getattr(dev, "is_aql", False) else ("scratch",), # the aql descriptor holds the scratch, nothing addresses it
|
||||
"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()
|
||||
@@ -226,6 +247,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()
|
||||
|
||||
@@ -327,8 +327,11 @@ class SDMAExecutor(AMDQueue):
|
||||
|
||||
def _execute_copy(self):
|
||||
struct = sdma_pkts.copy_linear.from_address(self.base + self.rptr[0] % self.size)
|
||||
count_cnt = to_mv(self.base + self.rptr[0] % self.size + 4, 4).cast('I')[0] & 0x3FFFFFFF
|
||||
ctypes.memmove(self.gpu.translate_addr(struct.dst_addr), self.gpu.translate_addr(struct.src_addr), count_cnt + 1)
|
||||
count, off = (to_mv(self.base + self.rptr[0] % self.size + 4, 4).cast('I')[0] & 0x3FFFFFFF) + 1, 0
|
||||
while off < count: # a page at a time: the physical pages of a range needn't be contiguous
|
||||
n = min(count - off, 0x1000 - ((struct.src_addr + off) & 0xfff), 0x1000 - ((struct.dst_addr + off) & 0xfff))
|
||||
ctypes.memmove(self.gpu.translate_addr(struct.dst_addr + off), self.gpu.translate_addr(struct.src_addr + off), n)
|
||||
off += n
|
||||
self.rptr[0] += ctypes.sizeof(struct)
|
||||
|
||||
class AMDGPURegisters:
|
||||
|
||||
@@ -89,7 +89,7 @@ class TestDevice(unittest.TestCase):
|
||||
except Exception as e: self.skipTest(f"skipping compiler test: not all compilers: {e}")
|
||||
|
||||
imports = ("from tinygrad import Device; from tinygrad.runtime.support.compiler_amd import HIPCompiler; "
|
||||
"from tinygrad.runtime.support.compiler_amd import AMDLLVMCompiler")
|
||||
"from tinygrad.runtime.support.compiler_llvm import AMDLLVMCompiler")
|
||||
subprocess.run([f'python3 -c "{imports}; assert isinstance(Device[Device.DEFAULT].compiler, AMDLLVMCompiler)"'],
|
||||
shell=True, check=True, env={**os.environ, "DEV": "AMD:LLVM"})
|
||||
subprocess.run([f'python3 -c "{imports}; assert isinstance(Device[Device.DEFAULT].compiler, HIPCompiler)"'],
|
||||
|
||||
@@ -78,6 +78,13 @@ class TestContextVars(unittest.TestCase):
|
||||
test()
|
||||
self.assertEqual(VARIABLE.value, 0)
|
||||
|
||||
def test_decorator_recursive(self):
|
||||
@Context(VARIABLE=1)
|
||||
def test(n):
|
||||
if n: test(n-1)
|
||||
test(2)
|
||||
self.assertEqual(VARIABLE.value, 0)
|
||||
|
||||
def test_context_exit_reverts_updated_values(self):
|
||||
D = ContextVar("D", 1)
|
||||
D.value = 2
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -167,6 +167,17 @@ class TestVminVmaxProperties(unittest.TestCase):
|
||||
self.assertEqual(UOp.const(4.5).cast(dtypes.float).cast(dtypes.int)._min_max, (4, 4))
|
||||
x = UOp.const(4.5).cast(dtypes.float)
|
||||
self.assertIs(x.ne(x.cast(dtypes.int).cast(dtypes.float)).simplify().arg, True)
|
||||
# a source reaching past the destination clamps to its edge
|
||||
self.assertEqual(UOp.variable('x', 2e9, 3e9, dtypes.float).cast(dtypes.int)._min_max, (2000000000, dtypes.int.max))
|
||||
# a source entirely past the destination has no value in it
|
||||
self.assertEqual(UOp.variable('x', 3e9, 4e9, dtypes.float).cast(dtypes.int)._min_max, (dtypes.int.min, dtypes.int.max))
|
||||
self.assertEqual(UOp.variable('x', -4e9, -3e9, dtypes.float).cast(dtypes.int)._min_max, (dtypes.int.min, dtypes.int.max))
|
||||
self.assertEqual(UOp.variable('x', 200, 300, dtypes.int).cast(dtypes.char)._min_max, (dtypes.char.min, dtypes.char.max))
|
||||
self.assertEqual(UOp.const(300, dtypes.char)._min_max, (dtypes.char.min, dtypes.char.max))
|
||||
self.assertEqual(UOp.const(math.inf).cast(dtypes.int)._min_max, (dtypes.int.min, dtypes.int.max))
|
||||
self.assertEqual(UOp.const(math.nan, dtypes.float)._min_max, (-math.inf, math.inf))
|
||||
# a weak destination has no width to clamp to
|
||||
self.assertEqual(UOp.variable('x', 5, 7, dtypes.int).cast(dtypes.weakfloat)._min_max, (5, 7))
|
||||
|
||||
def test_vmin_vmax_cast_int_to_float_grid(self):
|
||||
# a cast to float only takes values on the float grid, so its bounds are the source bounds rounded at the destination
|
||||
|
||||
+1
-19
@@ -7,28 +7,10 @@ from tinygrad.dtype import dtypes, AddrSpace, ConstFloat, Invalid # noqa: F401
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.uop.ops import Ops, AxisType, ParamArg, PatternMatcher, UOp, UPat, dtype_from_uop, exec_alu, graph_rewrite # noqa: F401 # ParamArg used by eval(str(uop)) roundtrip tests
|
||||
from tinygrad.uop.weak import pm_lower_weak
|
||||
from tinygrad.uop.spec import spec_program, spec_shared, spec_tensor, type_verify
|
||||
from tinygrad.uop.spec import spec_program, spec_shared, type_verify
|
||||
from tinygrad.uop.symbolic import sym, pm_remove_invalid
|
||||
from test.helpers import eval_uop, to_uops_list
|
||||
|
||||
class TestStorageSpec(unittest.TestCase):
|
||||
def test_contiguous_is_not_store_target(self):
|
||||
value = (Tensor.empty(4).uop + 1).contiguous()
|
||||
for target in (value, value.reshape(2, 2), value.detach()):
|
||||
with self.subTest(op=target.op), self.assertRaises(RuntimeError):
|
||||
type_verify(target.store(target), spec_tensor)
|
||||
|
||||
def test_contiguous_can_depend_on_other_storage_writes(self):
|
||||
buf = Tensor.empty(4).uop
|
||||
type_verify((buf + 1).contiguous().after(buf.store(buf + 1)), spec_tensor)
|
||||
|
||||
def test_detached_storage_can_carry_writes(self):
|
||||
buf = Tensor.empty(4).uop
|
||||
detached = buf.detach()
|
||||
type_verify(detached.after(detached.store(buf + 1)), spec_tensor)
|
||||
with self.assertRaises(RuntimeError):
|
||||
type_verify((buf + 1).detach().after(buf.store(buf + 1)), spec_tensor)
|
||||
|
||||
class TestDTypeFromUOp(unittest.TestCase):
|
||||
def test_broadcastable_promotion(self):
|
||||
self.assertEqual(dtype_from_uop(Ops.ADD, (UOp.const(1.0).cast(dtypes.float32), UOp.const(1.0).cast(dtypes.float16)), None), dtypes.float32)
|
||||
|
||||
@@ -122,6 +122,35 @@ class TestValidateOOB(unittest.TestCase):
|
||||
r = UOp.range(20, 0)
|
||||
i = (r.cast(dtypes.float) * 0.68).trunc().cast(dtypes.int)
|
||||
to_uops_list([buf.index(i.valid((i >= 0) & (i < 16))).load()])
|
||||
# a float entirely out of the int range has no value, not an empty one
|
||||
f = UOp.variable("f", 3e9, 4e9, dtypes.float32, param=True).cast(dtypes.int)
|
||||
with self.assertRaises(RuntimeError):
|
||||
to_uops_list([buf.index(f).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):
|
||||
@@ -157,40 +186,20 @@ class TestValidateOOB(unittest.TestCase):
|
||||
with self.assertRaises(RuntimeError):
|
||||
to_uops_list([buf_int.index(gidx.valid(ld_bool)).load()]) # gidx 0..15, buf_int size 8
|
||||
|
||||
# skipped tests (moved from test_uop_graph.py)
|
||||
@unittest.skip("if not allowed in graph")
|
||||
def test_in_bounds_access_gated_local(self):
|
||||
with Context(CHECK_OOB=1):
|
||||
# Define buffers
|
||||
# local memory
|
||||
def test_gated_local(self):
|
||||
with Context(CHECK_OOB=1, SPEC=2):
|
||||
gbuf = UOp.param(0, dtypes.uint, 400)
|
||||
sbuf = UOp.placeholder((8,), dtypes.uint, slot=0, addrspace=AddrSpace.LOCAL)
|
||||
|
||||
# Define indices, valids and barrier
|
||||
gidx = UOp(Ops.SPECIAL, src=(UOp.const(416),), arg="gidx0")
|
||||
lidx = UOp(Ops.SPECIAL, src=(UOp.const(10),), arg="lidx0")
|
||||
|
||||
gate = (gidx<400) & (lidx<8)
|
||||
|
||||
local_store = sbuf.index(lidx.valid(lidx<8)).store(UOp.const(1))
|
||||
|
||||
barrier = UOp(Ops.BARRIER, src=(local_store,))
|
||||
if_barrier = UOp(Ops.IF, src=(gate, barrier))
|
||||
|
||||
# Load from local memory (after the IF/barrier)
|
||||
local_load = UOp(Ops.LOAD, src=(sbuf.index(lidx), if_barrier))
|
||||
|
||||
# Store to global memory
|
||||
global_store = UOp(Ops.STORE, src=(gbuf.index(gidx), local_load))
|
||||
to_uops_list([global_store])
|
||||
|
||||
@unittest.skip("Bool load is not supported yet")
|
||||
def test_load_mask(self):
|
||||
with Context(CHECK_OOB=1):
|
||||
glbl0 = UOp.param(0, dtypes.int, 16)
|
||||
mask = UOp.param(0, dtypes.bool, 16)
|
||||
ridx = UOp.range(20, 0)
|
||||
ld0 = UOp(Ops.LOAD, src=(glbl0.index(UOp.const(ridx<16&mask, ridx))))
|
||||
to_uops_list([ld0])
|
||||
store = sbuf.index(lidx.valid(lidx < 8)).store(UOp.const(1))
|
||||
load = sbuf.after(store).index(lidx.valid(lidx < 8)).load()
|
||||
to_uops_list([gbuf.index(gidx.valid(gidx < 400)).store(load)]) # valid: local store and load gated to 8, global store gated to 400
|
||||
with self.assertRaises(RuntimeError):
|
||||
to_uops_list([gbuf.index(gidx.valid(gidx < 400)).store(sbuf.after(store).index(lidx).load())]) # lidx 0..9 into 8
|
||||
with self.assertRaises(RuntimeError):
|
||||
to_uops_list([gbuf.index(gidx).store(load)]) # gidx 0..415 into 400
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -454,7 +454,7 @@ class TestVizIntegration(unittest.TestCase):
|
||||
def test_jit(self):
|
||||
with save_viz():
|
||||
@TinyJit
|
||||
def f(a, b, c): return (a+b).contiguous().mul(3), c.add(1).clone().assign(a.to(c.device)), b.assign(c.to(b.device))
|
||||
def f(a, b, c): return (a+b).contiguous().mul(3), c.add(1).contiguous().assign(a.to(c.device)), b.assign(c.to(b.device))
|
||||
a, b, c = Tensor.empty(16, device="NULL"), Tensor.empty(16, device="NULL"), Tensor.empty(16, device="NULL:1")
|
||||
for _ in range(3): Tensor.realize(*f(a, b, c))
|
||||
out = load_profile(cpu_events)
|
||||
|
||||
@@ -5,7 +5,7 @@ from tinygrad.dtype import dtypes
|
||||
from tinygrad.uop.ops import UOp, Ops
|
||||
from tinygrad.tensor import transform_to_call
|
||||
|
||||
def sched_key(t:Tensor): return transform_to_call(UOp.sink(t.uop)).src[0].key
|
||||
def sched_key(t:Tensor): return transform_to_call(UOp.sink(t.uop))[0].src[0].key
|
||||
|
||||
class TestCall(unittest.TestCase):
|
||||
def test_call_plus(self):
|
||||
@@ -370,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)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import unittest
|
||||
from tinygrad import Tensor, dtypes
|
||||
from tinygrad.uop.ops import UOp, Ops
|
||||
from tinygrad.tensor import transform_to_call
|
||||
|
||||
class TestCallify(unittest.TestCase):
|
||||
def test_basic(self):
|
||||
@@ -109,75 +107,6 @@ 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_only_replace_inputs(self):
|
||||
x = Tensor.empty(4)
|
||||
body = UOp.sink((x.uop + 1).contiguous().copy_to_device("CPU:1"))
|
||||
call = transform_to_call(body)
|
||||
self.assertEqual(call.src[1:], (x.uop,))
|
||||
self.assertIs(call.src[0], body.substitute({x.uop: x.uop.param_like(0)}))
|
||||
|
||||
def test_existing_params_do_not_alias_buffers(self):
|
||||
x = Tensor.empty(4)
|
||||
param = UOp.param(0, x.dtype, x.shape, device=x.device)
|
||||
body = UOp.sink(x.uop + param)
|
||||
call = transform_to_call(body)
|
||||
self.assertEqual(set(call.src[1:]), {x.uop, param})
|
||||
params = [u for u in call.src[0].toposort() if u.op is Ops.PARAM]
|
||||
self.assertEqual({u.arg.slot for u in params}, {0, 1})
|
||||
self.assertIs(call.src[0].substitute({u: call.src[1+u.arg.slot] for u in params}, walk=True), body)
|
||||
|
||||
def test_scalar_param_binding_survives_renumbering(self):
|
||||
from tinygrad.schedule import create_linear_with_vars
|
||||
from tinygrad.engine.realize import run_linear
|
||||
x = Tensor([1, 2, 3]).realize()
|
||||
out = Tensor.empty_like(x)
|
||||
binding = UOp.variable("amount", 1, 10, dtypes.int).bind(4)
|
||||
param = binding.param_like(7)
|
||||
call = transform_to_call(UOp.sink(out.uop.after(out.uop.store(x.uop + param))))
|
||||
call = call.replace(src=(call.src[0], *(binding if arg is param else arg for arg in call.src[1:])))
|
||||
run_linear(*create_linear_with_vars(call))
|
||||
self.assertEqual(out.tolist(), [5, 6, 7])
|
||||
|
||||
def test_nested_params_keep_their_scope(self):
|
||||
x = Tensor.empty(4)
|
||||
param = UOp.param(7, x.dtype, x.shape, device=x.device)
|
||||
nested_body = UOp.sink(param + 1)
|
||||
nested = nested_body.call(*([x.uop] * 8))
|
||||
call = transform_to_call(UOp.sink(x.uop + param, nested))
|
||||
self.assertIs(call.src[0].src[1].src[0], nested_body)
|
||||
self.assertEqual(set(call.src[1:]), {x.uop, param})
|
||||
|
||||
def test_fresh_slots_are_negative_and_canonical_slots_are_dense(self):
|
||||
x = Tensor.empty(4)
|
||||
param = UOp.placeholder((4,), x.dtype, device=x.device)
|
||||
inner = x.uop.param_like(0)
|
||||
outputs = UOp.call_with_outputs((inner + 1, inner + 2), x.uop)
|
||||
fresh = [x.uop.arg.slot, param.arg.slot, *(out.src[0].arg.slot for out in outputs)]
|
||||
self.assertLess(fresh[0], 0)
|
||||
self.assertTrue(all(a > b for a, b in zip(fresh, fresh[1:])))
|
||||
call = transform_to_call(UOp.sink(*outputs, param))
|
||||
unbound = [u.arg.slot for u in call.src[0].toposort() if u.is_unbound]
|
||||
self.assertEqual(unbound, list(range(len(outputs))))
|
||||
params = [u.arg.slot for u in call.src[0].toposort(enter_calls=False) if u.op is Ops.PARAM]
|
||||
self.assertEqual(params, list(range(len(call.src)-1)))
|
||||
self.assertIn(param, call.src[1:])
|
||||
|
||||
def test_unbound_renumbering_preserves_distinct_outputs(self):
|
||||
def output(): return UOp.call_with_outputs((Tensor(1., dtype=dtypes.float, device="CPU").uop,))[0]
|
||||
canonical = transform_to_call(UOp.sink(output())).src[0].src[0]
|
||||
body = UOp.sink(canonical, output())
|
||||
call = transform_to_call(body)
|
||||
self.assertEqual(len([u for u in call.src[0].toposort() if u.is_unbound]), 2)
|
||||
self.assertIs(transform_to_call(call.src[0]).src[0], call.src[0])
|
||||
|
||||
def test_intermediate_contiguous_stays_a_value(self):
|
||||
x = (Tensor([1, 2, 3]).realize() + 1).contiguous()
|
||||
original = x.uop
|
||||
y = (x * 2).realize()
|
||||
self.assertIs(x.uop, original)
|
||||
self.assertIs(x.uop.op, Ops.CONTIGUOUS)
|
||||
self.assertEqual(y.tolist(), [4, 6, 8])
|
||||
|
||||
def test_intermediate_clone_persists(self):
|
||||
x = (Tensor([1, 2, 3]).realize() + 1).clone()
|
||||
y = (x * 2).realize()
|
||||
@@ -185,13 +114,6 @@ class TestCallify(unittest.TestCase):
|
||||
self.assertEqual(x.tolist(), [2, 3, 4])
|
||||
self.assertEqual(y.tolist(), [4, 6, 8])
|
||||
|
||||
def test_creation_copy_has_storage(self):
|
||||
x = Tensor([1, 2, 3], device="PYTHON").to("CPU")
|
||||
self.assertTrue(x.uop.has_buffer_identity(after_ok=True))
|
||||
y = Tensor.empty(3, dtype=dtypes.int, device=x.device).assign(x).realize()
|
||||
y.assign(0).realize()
|
||||
self.assertEqual(x.tolist(), [1, 2, 3])
|
||||
|
||||
def test_zero_size_cat_with_rng(self):
|
||||
# Empty outputs must not replay a pending RNG counter update.
|
||||
a = Tensor.rand(2, 2)
|
||||
|
||||
@@ -108,6 +108,13 @@ class TestWeakPromotion(unittest.TestCase):
|
||||
self.assertIs(stacked.dtype, dtypes.weakfloat)
|
||||
self.assertEqual(stacked.tolist(), [2.0, -3.0])
|
||||
|
||||
def test_weakint_cast_truncates_for_every_consumer(self):
|
||||
# a weakint cast of a float is a truncation whether a cast, a compare or an arithmetic op consumes it
|
||||
x = Tensor([2.5, -3.5], dtype=dtypes.float32, device="CPU")
|
||||
self.assertEqual(x.cast(dtypes.weakint).cast(dtypes.float32).tolist(), [2.0, -3.0])
|
||||
self.assertEqual((x.cast(dtypes.weakint) * x).tolist(), [5.0, 10.5])
|
||||
self.assertEqual(Tensor([0.5, -0.5], dtype=dtypes.float32, device="CPU").cast(dtypes.weakint).cast(dtypes.bool).tolist(), [False, False])
|
||||
|
||||
def test_uop_scalar_const_lifts_kind(self):
|
||||
for dtype, value, out_dtype, const_dtype in ((dtypes.weakint, 1, dtypes.weakint, dtypes.weakint),
|
||||
(dtypes.int32, 1, dtypes.int32, dtypes.weakint),
|
||||
|
||||
+200
-21
@@ -1,7 +1,8 @@
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
import numpy as np
|
||||
from tinygrad import Tensor, UOp, dtypes, nn, function
|
||||
from tinygrad.llm.kernels.amd import Linear, amd_custom_kernels_supported, q8_quantize, flash_attention
|
||||
from tinygrad.llm.kernels.amd import Linear, amd_custom_kernels_supported, q8_quantize, flash_attention, gated_delta_prefill
|
||||
from tinygrad.llm.gguf import ggml_data_to_tensor
|
||||
|
||||
class TestQ8Quantize(unittest.TestCase):
|
||||
@@ -28,6 +29,12 @@ class TestQ8Quantize(unittest.TestCase):
|
||||
# xsum holds the two per-16 sums per 32-wide group
|
||||
np.testing.assert_array_equal(gsum.numpy().reshape(2, 2), expected.reshape(2, 2, 16).sum(-1).astype(np.float32))
|
||||
|
||||
def test_quantize_rounding_ties(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
values = np.array([-127,127]+[i+0.5 for i in range(-15,15)],dtype=np.float32)
|
||||
quant,_,_ = q8_quantize(Tensor(values),1,32)
|
||||
np.testing.assert_array_equal(quant.bitcast(dtypes.int8).reshape(32).numpy(),np.rint(values).astype(np.int8))
|
||||
|
||||
def test_q6_linear_compiles_in_function(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
rng = np.random.default_rng(42)
|
||||
@@ -44,22 +51,125 @@ class TestQ8Quantize(unittest.TestCase):
|
||||
self.assertEqual(linear.weight.uop.buf_uop.buffer.nbytes, 53*4)
|
||||
self.assertEqual(linear.weight.dtype, dtypes.uint32)
|
||||
|
||||
def test_q4_k_linear(self):
|
||||
def test_q4_k_linear(self): self._test_quant_linear(12, 144)
|
||||
def test_iq4_linear(self): self._test_quant_linear(23, 136)
|
||||
def test_q5_linear(self): self._test_quant_linear(13, 176)
|
||||
|
||||
def test_quant_linear_partial_output_tile(self):
|
||||
# Cover a sub-tile output, a trailing tile, and IQ4's larger-output tile selection.
|
||||
for typ, size, outputs, tokens in ((12, 144, 16, 16), (12, 144, 48, 32), (13, 176, 48, 16), (23, 136, 4112, 32)):
|
||||
with self.subTest(ggml_type=typ, out_features=outputs):
|
||||
self._test_quant_linear(typ, size, in_features=256, out_features=outputs, token_counts=(tokens,))
|
||||
|
||||
def test_quant_linear_preserves_rope_permutation(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
rng = np.random.default_rng(42)
|
||||
in_features, blocks = 2048, 16*2048//256
|
||||
packed = rng.integers(0, 256, blocks*144, dtype=np.uint8)
|
||||
for i in range(blocks): packed[i*144:i*144+4] = np.array([0.01, 0.002], dtype=np.float16).view(np.uint8)
|
||||
raw = Tensor(np.pad(packed, (4, 0))).contiguous().realize()[4:]
|
||||
decoded = ggml_data_to_tensor(raw, 16*in_features, 12).reshape(16, in_features)
|
||||
for typ, size in ((12, 144), (13, 176), (14, 210), (23, 136)):
|
||||
with self.subTest(ggml_type=typ):
|
||||
packed = rng.integers(0, 256, (16, size), dtype=np.uint8)
|
||||
packed[:, -2:] = np.array([0.001], dtype=np.float16).view(np.uint8)
|
||||
if typ != 14: packed[:, :2] = np.array([0.001], dtype=np.float16).view(np.uint8)
|
||||
if typ in (12, 13): packed[:, 2:4] = np.array([0.0002], dtype=np.float16).view(np.uint8)
|
||||
raw = Tensor(np.pad(packed.flatten(), (4, 0))).contiguous().realize()[4:]
|
||||
decoded = ggml_data_to_tensor(raw, 16*256, typ).reshape(16, 256).half()
|
||||
original = decoded.numpy()
|
||||
x = rng.normal(size=(3, 256)).astype(np.float16)
|
||||
for prefix in (None, 0, 4):
|
||||
with self.subTest(prefix=prefix):
|
||||
w = decoded.reshape(2, 8, 256)
|
||||
if prefix is None:
|
||||
weight = w.rearrange("n (h two) d -> n (two h) d", two=2)
|
||||
else:
|
||||
weight = w[:, :prefix].cat(w[:, prefix:].rearrange("n (h two) d -> n (two h) d", two=2), dim=1)
|
||||
start = prefix or 0
|
||||
rows = np.arange(16).reshape(2, 8)
|
||||
order = np.concatenate((rows[:, :start], rows[:, start:].reshape(2, -1, 2).transpose(0, 2, 1).reshape(2, -1)), axis=1)
|
||||
linear = Linear(256, 16, bias=False)
|
||||
linear.weight = weight.reshape(16, 256)
|
||||
np.testing.assert_allclose(linear(Tensor(x)).numpy(), x.astype(np.float32) @ original[order.flatten()].astype(np.float32).T,
|
||||
rtol=3e-3, atol=2e-2)
|
||||
self.assertIsNone(linear.ggml_type)
|
||||
|
||||
def test_quant_linear_rejects_unaligned_rows_and_integer_casts(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
for width in (128, 256):
|
||||
with self.subTest(width=width):
|
||||
packed = np.zeros((2*width//256, 136), dtype=np.uint8)
|
||||
packed[:, :2] = np.array([0.001], dtype=np.float16).view(np.uint8)
|
||||
packed[:, 8:] = np.arange(128, dtype=np.uint8)
|
||||
raw = Tensor(np.pad(packed.flatten(), (4, 0))).realize()[4:]
|
||||
weight = ggml_data_to_tensor(raw, 2*width, 23).reshape(2, width)
|
||||
if width == 256: weight = weight.int().float()
|
||||
expected = weight.numpy().sum(-1)[None]
|
||||
linear = Linear(width, 2, bias=False)
|
||||
linear.weight = weight
|
||||
np.testing.assert_allclose(linear(Tensor.ones(1, width)).numpy(), expected, rtol=1e-3, atol=1e-3)
|
||||
self.assertIsNone(linear.ggml_type)
|
||||
|
||||
def test_dense_gemv_preserves_integer_casts(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
linear = Linear(128, 1)
|
||||
linear.weight = Tensor.full((1, 128), 0.75).contiguous().realize().int().float()
|
||||
linear.bias = Tensor.full((1,), 0.75).contiguous().realize().int().float()
|
||||
np.testing.assert_array_equal(linear(Tensor.ones(1, 128)).numpy(), 0)
|
||||
|
||||
def test_dense_gemv_float32_range(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
linear = Linear(128, 1, bias=False)
|
||||
linear.weight = Tensor.full((1, 128), 1/128, dtype=dtypes.float32).realize()
|
||||
np.testing.assert_array_equal(linear(Tensor.full((1, 128), 65536, dtype=dtypes.float32)).numpy(), 65536)
|
||||
|
||||
def test_gated_delta_state_and_precision(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
for case in ("view", "reset", "half"):
|
||||
with self.subTest(case=case):
|
||||
q = Tensor.full((1, 1, 1, 32), 256 if case == "half" else 1, dtype=dtypes.half if case == "half" else dtypes.float32)
|
||||
state = Tensor.full((1, 1, 32, 4), int(case == "reset"), dtype=dtypes.float32).contiguous().realize().transpose(-1, -2)
|
||||
if case != "view": state = state.contiguous().realize()
|
||||
start = Tensor(UOp.variable("start_pos", 0, 10).bind(0)) if case == "reset" else None
|
||||
beta = Tensor.full((1, 1, 1), 1/2097152 if case == "half" else 1, dtype=dtypes.float32)
|
||||
if case != "reset":
|
||||
message = "recurrent state must be contiguous" if case == "view" else "recurrent Q/K must be float32"
|
||||
with self.assertRaisesRegex(AssertionError, message):
|
||||
gated_delta_prefill(q, q, Tensor.ones(1, 1, 1, 4), beta, Tensor.ones(1, 1, 1), state, start)
|
||||
continue
|
||||
out = gated_delta_prefill(q, q, Tensor.ones(1, 1, 1, 4), beta, Tensor.ones(1, 1, 1), state, start)
|
||||
np.testing.assert_array_equal(out.numpy(), 32)
|
||||
np.testing.assert_array_equal(state.numpy(), 1)
|
||||
|
||||
def test_dense_gemv_bias(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
rng = np.random.default_rng(42)
|
||||
w, bias = rng.normal(size=(32, 128)).astype(np.float16), rng.normal(size=32).astype(np.float16)
|
||||
linear = Linear(128, 32)
|
||||
linear.weight, linear.bias = Tensor(w), Tensor(bias)
|
||||
for tokens in (1, 3):
|
||||
with self.subTest(tokens=tokens):
|
||||
x = rng.normal(size=(tokens, 128)).astype(np.float16)
|
||||
np.testing.assert_allclose(linear(Tensor(x)).numpy(), x.astype(np.float32) @ w.astype(np.float32).T + bias, rtol=2e-3, atol=2e-3)
|
||||
|
||||
def _test_quant_linear(self, ggml_type, block_bytes, in_features=2048, out_features=64, token_counts=(1, 3, 32, 64, 128)):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
rng = np.random.default_rng(42)
|
||||
packed = rng.integers(0, 256, (out_features*in_features//256, block_bytes), dtype=np.uint8)
|
||||
packed[:, :2] = np.array([0.001], dtype=np.float16).view(np.uint8)
|
||||
if ggml_type in (12, 13): packed[:, 2:4] = np.array([0.0002], dtype=np.float16).view(np.uint8)
|
||||
raw = Tensor(np.pad(packed.flatten(), (4, 0))).contiguous().realize()[4:]
|
||||
decoded = ggml_data_to_tensor(raw, out_features*in_features, ggml_type).reshape(out_features, in_features)
|
||||
weight = decoded.numpy()
|
||||
linear = Linear(in_features, 16, bias=False)
|
||||
nn.state.load_state_dict(linear, {"weight":decoded}, verbose=False, realize=False)
|
||||
x = rng.normal(size=(3, in_features)).astype(np.float32)
|
||||
scale = np.maximum(np.abs(x).reshape(3, in_features//32, 32).max(-1, keepdims=True) / 127, 1e-8)
|
||||
xq = np.clip(np.rint(x.reshape(3, in_features//32, 32) / scale), -127, 127) * scale
|
||||
np.testing.assert_allclose(linear(Tensor(x)).numpy(), xq.reshape(3, in_features) @ weight.T, rtol=2e-3, atol=2e-2)
|
||||
self.assertEqual(linear.ggml_type, 12)
|
||||
linear = Linear(in_features, out_features, bias=False)
|
||||
linear.weight = decoded
|
||||
for tokens in token_counts:
|
||||
with self.subTest(tokens=tokens):
|
||||
x = rng.normal(size=(tokens, in_features)).astype(np.float32 if tokens == 3 else np.float16)
|
||||
reference_x = x.astype(np.float32)
|
||||
if tokens < 16:
|
||||
grouped = reference_x.reshape(tokens, -1, 32)
|
||||
scale = np.maximum(np.abs(grouped).max(-1, keepdims=True) / 127, 1e-8)
|
||||
reference_x = (np.clip(np.rint(grouped/scale), -127, 127)*scale).reshape(tokens, in_features)
|
||||
reference_w = weight if tokens < 16 else weight.astype(np.float16).astype(np.float32)
|
||||
np.testing.assert_allclose(linear(Tensor(x)).numpy(), reference_x @ reference_w.T, rtol=3e-3, atol=2e-2)
|
||||
self.assertEqual(linear.ggml_type, ggml_type)
|
||||
|
||||
def test_q6_linear_multiple_tokens(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
@@ -86,6 +196,18 @@ class TestQ8Quantize(unittest.TestCase):
|
||||
self.assertTrue(generic.use_custom_quant)
|
||||
self.assertEqual(generic.ggml_type, 14)
|
||||
|
||||
def test_attention_fallback_shapes(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
for tokens, capacity, dim in ((1, 65, 64), (32, 64, 32), (32, 64, 384), (32, 64, 512)):
|
||||
with self.subTest(tokens=tokens, capacity=capacity, dim=dim):
|
||||
valid = 33
|
||||
cache = np.full((2, 1, 1, capacity, dim), np.nan, dtype=np.float16)
|
||||
cache[0, :, :, :valid] = 0
|
||||
cache[1, :, :, :valid] = np.arange(valid)[:, None]
|
||||
q = Tensor.zeros(1, 2, tokens, dim, dtype=dtypes.half)
|
||||
expected = np.broadcast_to(np.arange(valid-tokens, valid)[None, None, :, None]/2, q.shape)
|
||||
np.testing.assert_allclose(flash_attention(q, Tensor(cache), valid).numpy(), expected, rtol=1e-3, atol=1e-3)
|
||||
|
||||
def test_attention_uses_physical_cache_length(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
q, k, v = Tensor.zeros(1, 2, 1, 32), Tensor.randn(1, 1, 1, 32), Tensor.randn(1, 1, 1, 32)
|
||||
@@ -94,14 +216,71 @@ class TestQ8Quantize(unittest.TestCase):
|
||||
out = flash_attention(q, assigned, 1).realize()
|
||||
np.testing.assert_allclose(out.numpy(), v.expand(1, 2, 1, 32).numpy(), rtol=2e-2, atol=2e-2)
|
||||
|
||||
def test_flash_attention_decode_gqa_output_layout(self):
|
||||
def test_flash_attention_decode_symbolic_gqa(self):
|
||||
with patch.object(Tensor, "scaled_dot_product_attention", side_effect=AssertionError("expected custom decode")):
|
||||
self._test_flash_decode(8, 2, 256, 128, 37, symbolic=True)
|
||||
|
||||
def test_flash_attention_decode_gqa_tail(self): self._test_flash_decode(3, 1, 192, 64, 37)
|
||||
|
||||
def test_flash_attention_decode_gqa_output_layout(self): self._test_flash_decode(4, 1, 128, 256, 3)
|
||||
def test_flash_attention_decode_large_gqa_group(self): self._test_flash_decode(8, 1, 256, 256, 73)
|
||||
|
||||
def _test_flash_decode(self, heads, kv_heads, dim, n, valid, symbolic=False):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
Tensor.manual_seed(42)
|
||||
q = Tensor.randn(1, 4, 1, 128, dtype=dtypes.half).realize()
|
||||
cache = Tensor.randn(2, 1, 1, 256, 128, dtype=dtypes.half).realize()
|
||||
out = flash_attention(q, cache, 3).realize()
|
||||
expected = q.scaled_dot_product_attention(cache[0, :, :, :3], cache[1, :, :, :3], enable_gqa=True)
|
||||
np.testing.assert_allclose(out.numpy(), expected.numpy(), rtol=2e-3, atol=2e-3)
|
||||
rng = np.random.default_rng(42)
|
||||
q = rng.normal(size=(1, heads, 1, dim)).astype(np.float16)
|
||||
cache = rng.normal(size=(2, 1, kv_heads, n, dim)).astype(np.float16)
|
||||
k, v = (np.repeat(c[0, :, :valid].astype(np.float32), heads//kv_heads, axis=0) for c in cache)
|
||||
scores = q[0].astype(np.float32) @ k.transpose(0, 2, 1) / np.sqrt(dim)
|
||||
probs = np.exp(scores - scores.max(-1, keepdims=True))
|
||||
expected = (probs / probs.sum(-1, keepdims=True)) @ v
|
||||
cache_tensor = Tensor(cache)
|
||||
if symbolic:
|
||||
start_pos = UOp.variable("start_pos", 0, n-1).bind(valid-1)
|
||||
valid = start_pos + 1
|
||||
cache_tensor = Tensor(cache_tensor.realize().uop.after(Tensor(start_pos).uop))
|
||||
np.testing.assert_allclose(flash_attention(Tensor(q), cache_tensor, valid).numpy(), expected[None], rtol=2e-3, atol=2e-3)
|
||||
|
||||
def test_prefill_attention_nonfinite_cache_tail(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
rng = np.random.default_rng(42)
|
||||
q = Tensor.zeros(1, 2, 32, 128, dtype=dtypes.half)
|
||||
values = rng.normal(size=(33, 128)).astype(np.float16)
|
||||
expected = np.stack([values[:i+2].astype(np.float32).mean(0) for i in range(32)])[None, None].repeat(2, axis=1)
|
||||
for tail in (np.nan, np.inf, -np.inf):
|
||||
with self.subTest(tail=tail):
|
||||
cache = np.full((2, 1, 1, 64, 128), tail, dtype=np.float16)
|
||||
cache[0, :, :, :33] = 0
|
||||
cache[1, :, :, :33] = values
|
||||
valid = UOp.variable("valid_end", 32, 64).bind(33)
|
||||
cache_tensor = Tensor(cache).realize()
|
||||
assigned = Tensor(cache_tensor.uop.after(Tensor(valid).uop))
|
||||
out = flash_attention(q, assigned, valid)
|
||||
np.testing.assert_allclose(out.numpy(), expected, rtol=2e-3, atol=2e-3)
|
||||
|
||||
def test_flash_attention_decode_beyond_256_chunks(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
n = 257 * 64
|
||||
q = Tensor.zeros(1, 1, 1, 32, dtype=dtypes.half).realize()
|
||||
k = Tensor.zeros(1, 1, n, 32, dtype=dtypes.half)
|
||||
v = Tensor.zeros(1, 1, n-64, 32, dtype=dtypes.half).cat(Tensor.ones(1, 1, 64, 32, dtype=dtypes.half), dim=2)
|
||||
cache = Tensor.stack(k, v).contiguous().realize()
|
||||
for valid, expected in ((1, 0), (n, 1/257)):
|
||||
with self.subTest(valid=valid):
|
||||
valid_kv_len = UOp.variable("valid_kv_len", 1, n).bind(valid)
|
||||
assigned = Tensor(cache.uop.after(Tensor(valid_kv_len).uop))
|
||||
np.testing.assert_allclose(flash_attention(q, assigned, valid_kv_len).numpy(), expected, rtol=2e-3, atol=2e-4)
|
||||
|
||||
def test_flash_attention_decode_long_context_random(self):
|
||||
self._test_flash_decode(8, 2, 128, 257*64, 257*64-13) # past 256 chunks, with a ragged tail
|
||||
|
||||
def test_flash_attention_decode_chunk_round_accumulator_range(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
valid_kv_len, max_kv_len = 6749, 6784 # three chunk rounds, with a ragged tail
|
||||
q = Tensor.zeros(1, 8, 1, 32, dtype=dtypes.half).realize()
|
||||
cache = Tensor.stack(Tensor.zeros(1, 1, max_kv_len, 32, dtype=dtypes.half),
|
||||
Tensor.full((1, 1, max_kv_len, 32), 5500, dtype=dtypes.half)).contiguous().realize()
|
||||
np.testing.assert_allclose(flash_attention(q, cache, valid_kv_len).numpy(), 5500, rtol=2e-3, atol=2e-3)
|
||||
|
||||
def test_prefill_attention_unaligned_start(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
|
||||
@@ -7,7 +7,7 @@ from tinygrad.uop.weak import pm_lower_weak, pm_commit_weak, pm_cast_const
|
||||
from tinygrad.uop.render import pyrender
|
||||
from tinygrad.uop.spec import type_verify, spec_tensor, spec_program
|
||||
from tinygrad.renderer import Renderer, Estimates
|
||||
from tinygrad.renderer.isa import ISARenderer, IselContext, PreRegAllocContext
|
||||
from tinygrad.renderer.isa import ISARenderer, IselContext
|
||||
from tinygrad.dtype import dtypes, AddrSpace
|
||||
|
||||
# import all pattern matchers here
|
||||
@@ -439,12 +439,13 @@ 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):
|
||||
lst = line_rewrite(lst, ctx.pre_regalloc_matcher, PreRegAllocContext())
|
||||
lin_ctx = ctx.linear_ctx_type(ctx)
|
||||
lst = line_rewrite(lst, ctx.pre_regalloc_matcher, lin_ctx)
|
||||
# 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)
|
||||
regalloc_ctx = LinearScanRegallocContext(lin_ctx, lst, ctx)
|
||||
lst = line_rewrite(lst, pm_regalloc_rewrite, regalloc_ctx)
|
||||
lst = line_rewrite(lst, ctx.post_regalloc_matcher, regalloc_ctx)
|
||||
lst = line_rewrite(lst, ctx.post_regalloc_matcher, lin_ctx)
|
||||
if DEBUG >= 4: print(ctx.asm_str(lst, sink.arg.function_name))
|
||||
return prg.replace(src=prg.src + (UOp(Ops.LINEAR, src=tuple(lst)),))
|
||||
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import itertools
|
||||
from tinygrad.helpers import dedup
|
||||
from tinygrad.uop.ops import UOp, Ops, PatternMatcher, UPat
|
||||
from tinygrad.renderer.isa import ISARenderer, Register, greg
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.renderer.isa import ISARenderer, Register, rdef, LinearContext
|
||||
from typing import Any
|
||||
|
||||
PSEUDO_OPS = {Ops.CONST, Ops.CAST, Ops.BITCAST, Ops.NOOP, Ops.AFTER, Ops.BARRIER, Ops.GROUP, Ops.STACK}
|
||||
|
||||
class LinearScanRegallocContext:
|
||||
# returns the uop that defines the virtual register
|
||||
def vdef(self, v:Register) -> UOp: return self.uops[self.live_range[v][0]]
|
||||
def __init__(self, uops:list[UOp], ren:ISARenderer):
|
||||
def __init__(self, ctx:LinearContext, uops:list[UOp], ren:ISARenderer):
|
||||
self.uops = uops
|
||||
self.ren = ren
|
||||
self.idx = itertools.count()
|
||||
# the label associated with each loop NOTE: this is only used post regalloc and should be removed
|
||||
self.loop_label: dict[UOp, str] = {}
|
||||
|
||||
# compute live ranges
|
||||
self.live_range: dict[Register, list[int]] = {}
|
||||
@@ -23,16 +21,15 @@ class LinearScanRegallocContext:
|
||||
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)):
|
||||
for v in defs + tuple(rdef(s) for s in dedup(u.src)):
|
||||
if isinstance(v, Register): lr.setdefault(v, []).insert(0, idx)
|
||||
for v in defs:
|
||||
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
|
||||
self.locals: dict[UOp, UOp] = {}
|
||||
self.spills: dict[Register, UOp] = {} # mapping from virtual to stack slot
|
||||
self.spills: dict[Register, Any] = {} # mapping from virtual to arbitrary spill slot
|
||||
self.reals: dict[int, dict[Register, Register]] = {} # mapping from virtual to real at each program point
|
||||
self.insert_before: dict[int, list[tuple[Register, Register]]] = {} # fills to be inserted at each program point
|
||||
live: dict[Register, Register] = {} # mapping from virtual to real that's currently assigned to it
|
||||
@@ -49,11 +46,7 @@ class LinearScanRegallocContext:
|
||||
# assign register to spilled virtual and record load to be emitted before current uop, also assign it a stack slot
|
||||
def fill(v:Register, i:int, cons:tuple[Register, ...]|None=None) -> Register:
|
||||
if v not in self.spills:
|
||||
# the value of a BUFFER is its 64bit address, XMM registers need 16 bytes
|
||||
sz = 16 if v.cons[0].size == 16 else (8 if self.vdef(v).op is Ops.BUFFER else self.vdef(v).dtype.itemsize)
|
||||
offset = self.stack_size + (sz - self.stack_size % sz) % sz
|
||||
self.spills[v] = UOp.cconst(offset, dtypes.int32)
|
||||
self.stack_size = offset + sz
|
||||
self.spills[v] = ctx.assign_spill_slot(v, self.vdef(v))
|
||||
r = alloc(cons if cons is not None else v.cons, i)
|
||||
self.insert_before.setdefault(i, []).append((v, r))
|
||||
return r
|
||||
@@ -64,7 +57,7 @@ class LinearScanRegallocContext:
|
||||
for s in u.src:
|
||||
# HACK: cause of later hacks to lower range
|
||||
if u.op is Ops.END: continue
|
||||
if not isinstance(v:=greg(s), Register): continue
|
||||
if not isinstance(v:=rdef(s), Register): continue
|
||||
if v not in live: live[v] = fill(v, i)
|
||||
self.reals.setdefault(i, {})[v] = live[v]
|
||||
|
||||
@@ -76,17 +69,12 @@ class LinearScanRegallocContext:
|
||||
cons = v.cons
|
||||
# two address instructions (src is reused by def) can only coalesce reused src. reused src goes first to get priority in case of a tiebreak
|
||||
if ren.is_two_address(u) and j == 0:
|
||||
uses = tuple(live.get(greg(s)) for s in u.src)
|
||||
uses = tuple(live.get(rdef(s)) for s in u.src)
|
||||
cons = ((uses[0],) if uses[0] in cons else ()) + tuple(r for r in cons if r not in uses)
|
||||
# HACK: cause the range is missing the comparison
|
||||
live[v] = alloc(cons, i+1 if u.op is not Ops.RANGE else i)
|
||||
self.reals.setdefault(i, {})[v] = live[v]
|
||||
|
||||
# allocate stack array
|
||||
if u.op is Ops.BUFFER:
|
||||
self.locals[u] = UOp.cconst(self.stack_size, dtypes.int32)
|
||||
self.stack_size += u.max_numel() * u.dtype.itemsize
|
||||
|
||||
# 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
|
||||
@@ -113,22 +101,14 @@ def regalloc_rewrite(ctx:LinearScanRegallocContext, x:UOp):
|
||||
nsrc = []
|
||||
for j,s in enumerate(x.src):
|
||||
# v here is the virtual defined by the original s as s is the rewritten version
|
||||
if i in ctx.reals and (v:=greg(ctx.uops[i].src[j])) in ctx.spills: nsrc.append(ctx.ren.fill(ctx.spills[v], ctx.vdef(v), ctx.reals[i][v]))
|
||||
if i in ctx.reals and (v:=rdef(ctx.uops[i].src[j])) in ctx.spills: nsrc.append(ctx.ren.fill(ctx.spills[v], ctx.vdef(v), ctx.reals[i][v]))
|
||||
else: nsrc.append(s)
|
||||
ndefs = tuple(ctx.reals[i][v] for v in x.tag) if isinstance(x.tag, tuple) else x.tag
|
||||
if x.op is Ops.BUFFER: nx = ctx.ren.isel_matcher.rewrite(ctx.ren.stack_pointer().index(ctx.locals[x], tag=ndefs))
|
||||
else: nx = x.replace(src=tuple(nsrc), tag=ndefs)
|
||||
nx = x.replace(src=tuple(nsrc), tag=ndefs)
|
||||
|
||||
before = [ctx.ren.fill(ctx.spills[v], ctx.vdef(v), r) for v,r in ctx.insert_before.get(i, [])]
|
||||
after = [ctx.ren.spill(ctx.spills[v], nx) for v in x.tag if v in ctx.spills] if isinstance(x.tag, tuple) else []
|
||||
|
||||
# alloc/dealloc stack
|
||||
if ctx.stack_size > 0:
|
||||
sp = ctx.ren.stack_pointer()
|
||||
offset = UOp.cconst(ctx.stack_size, sp.dtype)
|
||||
if i == 0: before = [ctx.ren.isel_matcher.rewrite(UOp(Ops.SUB, src=(sp, offset), tag=sp.tag))] + before
|
||||
elif i == len(ctx.uops) - 2: before += [ctx.ren.isel_matcher.rewrite(UOp(Ops.ADD, src=(sp, offset), tag=sp.tag))]
|
||||
|
||||
return nx, before + [nx] + after
|
||||
|
||||
pm_regalloc_rewrite = PatternMatcher([
|
||||
|
||||
+10
-5
@@ -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):
|
||||
|
||||
@@ -26,8 +26,9 @@ def invalid_outputs(uret:UOp) -> set[UOp]:
|
||||
if u.op is Ops.STORE and u.src[1].base.is_invalid and not u.src[0].buf_uop.is_realized}
|
||||
|
||||
def renumber_invalid_outputs(uret:UOp) -> UOp:
|
||||
invalid = invalid_outputs(uret)
|
||||
return uret.substitute({b:b.replace(arg=replace(b.arg, slot=i))
|
||||
for i,b in enumerate(x for x in uret.toposort(enter_calls=False) if x in invalid_outputs(uret))})
|
||||
for i,b in enumerate(x for x in uret.toposort(enter_calls=False) if x in invalid)})
|
||||
|
||||
ReturnType = TypeVar('ReturnType')
|
||||
class _function(Generic[ReturnType]):
|
||||
|
||||
@@ -166,6 +166,8 @@ def stderr_log(msg:str): print(msg, end='', file=sys.stderr, flush=True)
|
||||
|
||||
class Context(contextlib.ContextDecorator):
|
||||
def __init__(self, **kwargs): self.kwargs = kwargs
|
||||
# ContextDecorator otherwise reuses self, so recursive calls overwrite old_context.
|
||||
def _recreate_cm(self): return Context(**self.kwargs)
|
||||
def __enter__(self):
|
||||
self.old_context:dict[str, Any] = {k: ContextVar._cache[k].value for k in self.kwargs}
|
||||
for k,v in self.kwargs.items(): ContextVar._cache[k].value = v
|
||||
|
||||
+19
-8
@@ -1,24 +1,36 @@
|
||||
<!DOCTYPE html><html><head><title>tinygrad chat</title><style>
|
||||
<!DOCTYPE html><html><head><meta charset="utf-8"><title>tinygrad chat</title><style>
|
||||
* { margin: 0 }
|
||||
body { background: #212121; color: #e3e3e3; font-family: system-ui;
|
||||
height: 100vh; display: flex; flex-direction: column }
|
||||
#chat { flex: 1; overflow-y: auto; padding: 20px }
|
||||
.msg { padding: 10px 16px; margin: 8px 0; white-space: pre-wrap; border-radius: 18px }
|
||||
table { border-collapse: collapse; table-layout: fixed; width: 100%; overflow-wrap: anywhere }
|
||||
th, td { border: 1px solid #555; padding: 6px 10px; text-align: left }
|
||||
a { color: #8ab4f8 } hr { border: 0; border-top: 1px solid #555 }
|
||||
.answer { white-space: normal; line-height: 1.65 } .answer > * { margin: 12px 0 }
|
||||
pre, blockquote { background: #2f2f2f; padding: 12px 16px; border-radius: 8px } pre { white-space: pre-wrap }
|
||||
.user { background: #2f2f2f; margin-left: auto; width: fit-content; max-width: 70% }
|
||||
#input { max-width: 768px; width: 100%; margin: 20px auto; padding: 14px 20px;
|
||||
background: #2f2f2f; color: inherit; font: inherit;
|
||||
border: none; outline: none; resize: none; border-radius: 24px; field-sizing: content }
|
||||
</style></head><body><div id="chat"></div>
|
||||
<textarea id="input" rows="1" placeholder="Ask anything" autofocus></textarea>
|
||||
<script src="/assets/cdn.jsdelivr.net/npm/[email protected]/dist/browser/markdown-it.umd.min.js"></script>
|
||||
<script>
|
||||
input.onkeydown = (e) => { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); send() } }
|
||||
let generating = false;
|
||||
input.onkeydown = (e) => { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
|
||||
e.preventDefault(); if (generating) return;
|
||||
generating = true; send().finally(() => generating = false);
|
||||
} };
|
||||
const msgs = [];
|
||||
const md = markdownit();
|
||||
async function send() {
|
||||
if (!input.value.trim()) return;
|
||||
msgs.push({role: 'user', content: input.value.trim()});
|
||||
chat.innerHTML += '<div class="msg user">' + input.value.trim().replace(/</g, '<') + '</div>';
|
||||
input.value = '';
|
||||
const d = document.createElement('div'); d.className = 'msg'; chat.appendChild(d);
|
||||
d.innerHTML = '<span style="color:#888"></span><div class="answer"></div>'; const [thinking, answer] = d.children;
|
||||
const r = await fetch('/v1/chat/completions', {method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({model: 'llama', messages: msgs, stream: true, temperature: 0.7})});
|
||||
let buf = '', txt = '', rsn = '';
|
||||
@@ -29,12 +41,11 @@
|
||||
const lines = buf.split('\n');
|
||||
buf = lines.pop();
|
||||
for (const ln of lines)
|
||||
if (ln.startsWith('data: ') && !ln.includes('[DONE]'))
|
||||
try { const dl = JSON.parse(ln.slice(6)).choices[0]?.delta;
|
||||
if (dl?.reasoning_content) { const s = document.createElement('span'); s.style.color = '#888';
|
||||
s.textContent = dl.reasoning_content; rsn += dl.reasoning_content; d.appendChild(s) }
|
||||
if (dl?.content) { const s = document.createElement('span');
|
||||
s.textContent = dl.content; txt += dl.content; d.appendChild(s) } } catch {}
|
||||
if (ln.startsWith('data: ') && !ln.includes('[DONE]')) {
|
||||
const dl = JSON.parse(ln.slice(6)).choices[0]?.delta;
|
||||
if (dl?.reasoning_content) { rsn += dl.reasoning_content; thinking.textContent = rsn }
|
||||
if (dl?.content) { txt += dl.content; answer.innerHTML = md.render(txt) }
|
||||
}
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
}
|
||||
const m = {role:'assistant', content:txt}; if (rsn) m.reasoning_content = rsn; msgs.push(m);
|
||||
|
||||
@@ -129,7 +129,7 @@ def ggml_data_to_tensor(t: Tensor, n: int, ggml_type: int) -> Tensor:
|
||||
return (dl * (grid + delta)).flatten(-3)
|
||||
if ggml_type == 20:
|
||||
d = blocks[:, :2].bitcast(dtypes.float16).cast(dtypes.float32)
|
||||
return d * Tensor(list(_ggml.kvalues_iq4nl), dtype=dtypes.float32, device=t.device)[q_to_uint8(blocks[:, 2:], 4)]
|
||||
return d * Tensor.const(tuple(_ggml.kvalues_iq4nl), dtypes.float32)[q_to_uint8(blocks[:, 2:], 4)]
|
||||
if ggml_type == 21:
|
||||
d = blocks[:, :2].bitcast(dtypes.float16).cast(dtypes.float32).reshape((-1, 1, 1, 1))
|
||||
scales = (1 + 2 * q_to_uint8(blocks[:, 106:110].reshape((-1, 4, 1)), 4).reshape((-1, 8))).cast(dtypes.float32).reshape((-1, 8, 1, 1))
|
||||
@@ -147,7 +147,7 @@ def ggml_data_to_tensor(t: Tensor, n: int, ggml_type: int) -> Tensor:
|
||||
if ggml_type == 23:
|
||||
d = blocks[:, :2].bitcast(dtypes.float16).cast(dtypes.float32).reshape((-1, 1, 1))
|
||||
scale_shifts = Tensor.const((0, 2, 4, 6, 8, 10, 12, 14), dtypes.uint16)
|
||||
iq4_xs_lut = Tensor(list(_ggml.kvalues_iq4nl), dtype=dtypes.float32, device=t.device)
|
||||
iq4_xs_lut = Tensor.const(tuple(_ggml.kvalues_iq4nl), dtypes.float32)
|
||||
scales_l = Tensor.stack((sl:=blocks[:, 4:8]).bitwise_and(0xF), sl.rshift(4), dim=2).reshape((-1, 8))
|
||||
scales_h = blocks[:, 2:4].bitcast(dtypes.uint16).unsqueeze(-1).rshift(scale_shifts).bitwise_and(0x03).reshape((-1, 8)).cast(dtypes.uint8)
|
||||
scales = (scales_l.bitwise_or(scales_h.lshift(4)).bitcast(dtypes.int8) - 32).cast(dtypes.float32).reshape((-1, 8, 1))
|
||||
|
||||
+91
-70
@@ -3,6 +3,7 @@ import functools, math
|
||||
from typing import Callable, cast
|
||||
from tinygrad import Tensor, UOp, nn, Device, Context
|
||||
from tinygrad.device import Buffer
|
||||
from tinygrad.llm.gguf import ggml_data_to_tensor
|
||||
from tinygrad.dtype import AddrSpace, dtypes
|
||||
from tinygrad.helpers import prod
|
||||
from tinygrad.uop.ops import AxisType, KernelInfo, Ops, resolve
|
||||
@@ -55,15 +56,20 @@ class Linear(nn.Linear):
|
||||
super().__init__(in_features, out_features, bias)
|
||||
self.in_features, self.out_features = in_features, out_features
|
||||
def set_quantized(self, decoded:Tensor):
|
||||
if self.in_features % GGML_BLOCK_SIZE: return
|
||||
packed_sizes = {decoded.numel() // 256 * type_size:typ for typ,type_size in QUANT_SIZES.items()}
|
||||
graph = decoded.uop.toposort()
|
||||
raw = next((u for u in graph if u.op is Ops.SHRINK and u.dtype == dtypes.uint8 and prod(u.shape) in packed_sizes), None)
|
||||
if raw is None: return
|
||||
ggml_type = packed_sizes[prod(raw.shape)]
|
||||
# the packed byte rate alone can't distinguish same-rate formats (Q4_0 vs Q4_K, Q5_0 vs Q5_K, MXFP4 vs IQ4_XS).
|
||||
# the supported formats are 256-wide superblocks: their decode views the packed bytes at the superblock width
|
||||
# (ggml_data_to_tensor reshapes to (-1, QUANT_SIZES[type])), while same-rate 32-wide formats reshape to 17-22
|
||||
if not any(u.op is Ops.RESHAPE and u.shape[-1:] == (QUANT_SIZES[ggml_type],) for u in graph): return
|
||||
# Only unwrap storage/order-preserving views, then require the exact dequantization expression.
|
||||
# This rejects subsequent arithmetic and permutations, including RoPE's concatenated query weights.
|
||||
def unwrapped(u:UOp) -> UOp:
|
||||
while u.op in (Ops.RESHAPE, Ops.CONTIGUOUS) or (u.op is Ops.CAST and dtypes.is_float(u.dtype) and dtypes.is_float(u.src[0].dtype)):
|
||||
u = u.src[0]
|
||||
return u
|
||||
expected = ggml_data_to_tensor(Tensor(raw), self.in_features * self.out_features, ggml_type)
|
||||
if unwrapped(decoded.uop).key != unwrapped(expected.uop).key: return
|
||||
raw_offset = raw.contiguous_view_offset()
|
||||
assert raw_offset is not None and raw_offset % 4 == 0 and raw.buf_uop.dtype == dtypes.uint8
|
||||
self.ggml_type = ggml_type
|
||||
@@ -75,7 +81,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.clone().reshape(nblocks * Q6_WORDS)
|
||||
self.weight = padded.contiguous().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)))
|
||||
@@ -101,23 +107,18 @@ class Linear(nn.Linear):
|
||||
return super().__call__(x)
|
||||
|
||||
def _amd_dp4a(a:UOp, b:UOp, c:UOp) -> UOp:
|
||||
# int8 4-wide dot, widened to scalar multiply-adds (2% decode slower than the sudot4 builtin, but portable)
|
||||
for i in range(4):
|
||||
av = ((a >> (8*i)) & 255).cast(dtypes.uint8).bitcast(dtypes.int8).int()
|
||||
bv = ((b >> (8*i)) & 255).cast(dtypes.uint8).bitcast(dtypes.int8).int()
|
||||
c = c + av*bv
|
||||
return c
|
||||
return UOp(Ops.CUSTOMI, src=(a, b, c), arg=("__builtin_amdgcn_sudot4(true, {}, true, {}, {}, false)", dtypes.int32))
|
||||
|
||||
def _amd_byte_perm(a:UOp, b:UOp, selectors:UOp) -> UOp:
|
||||
return UOp(Ops.CUSTOMI, src=tuple(x.cast(dtypes.uint32) for x in (a, b, selectors)), arg=("__builtin_amdgcn_perm({}, {}, {})", dtypes.uint32))
|
||||
|
||||
def _amd_load(ptr:UOp, lanes:int|None=None) -> UOp:
|
||||
def _amd_load(ptr:UOp, lanes:int|None=None, stream:bool=False) -> UOp:
|
||||
assert ptr.op is Ops.INDEX
|
||||
# nontemporal scalar load: streamed weights must not evict the activations/KV cache from L2
|
||||
if lanes is None: return ptr.load(arg="nontemporal")
|
||||
buf, coords = ptr.src[0], ptr.src[1:]
|
||||
idx = sum((coord*math.prod(buf.shape[i+1:]) for i,coord in enumerate(coords)), UOp.const(0))
|
||||
return UOp(Ops.SHRINK, src=(buf.flatten(), idx, UOp.const(lanes))).load()
|
||||
return UOp(Ops.SHRINK, src=(buf.flatten(), idx, UOp.const(lanes))).load(arg="nontemporal" if stream else None)
|
||||
|
||||
def _load_byte(raw:UOp, base:UOp, offset:UOp) -> UOp: return (raw[base + offset//4] >> ((offset&3)*8).cast(dtypes.uint32)) & 255
|
||||
def _half(value:UOp) -> UOp: return value.cast(dtypes.uint16).bitcast(dtypes.float16).float()
|
||||
@@ -153,22 +154,19 @@ def iq4_half_lut(device:str) -> Tensor:
|
||||
@functools.cache
|
||||
def _q8_quantize_kernel(q:UOp, scale:UOp, xsum:UOp, x:UOp, tokens:int, in_features:int) -> UOp:
|
||||
groups = in_features//Q8_GROUP_SIZE
|
||||
token_group, lane = UOp.range(tokens*groups, 0, axis_type=AxisType.GLOBAL), UOp.range(32, 1, axis_type=AxisType.LOCAL)
|
||||
token_group, lane = UOp.range(tokens*groups, 0, AxisType.GLOBAL), UOp.range(32, -1, AxisType.WARP)
|
||||
token, group = token_group//groups, token_group%groups
|
||||
x = x.reshape(tokens, groups, 32)
|
||||
group_scale = (warp_reduce(x[token, group, lane].float().abs(), maximum=True, full_wave=True) / 127).maximum(1e-8)
|
||||
word_lane = lane.minimum(7)
|
||||
xs = tuple(x[token, group, word_lane*4+i].float() for i in range(4))
|
||||
qs = tuple((v/group_scale).round().clip(-127, 127).cast(dtypes.int8) for v in xs)
|
||||
word = sum((v.cast(dtypes.uint8).cast(dtypes.uint32) << (i*8) for i, v in enumerate(qs)), UOp.const(0, dtypes.uint32))
|
||||
# per-16 sums of the quantized values (lanes 0-3 / 4-7): Q4_K/Q5_K need the 32-sum, Q6_K the 16-sums
|
||||
part = (lane < 8).where(sum((v.cast(dtypes.int32) for v in qs), UOp.const(0, dtypes.int32)), UOp.const(0, dtypes.int32))
|
||||
gsum = [warp_reduce(((lane & 4).eq(h*4)).where(part, UOp.const(0, dtypes.int32)), full_wave=True) for h in range(2)]
|
||||
store_half = (lane & 4) >> 2
|
||||
stores = (q[token, group, lane.valid(lane < 8)].store(word),
|
||||
UOp.group(scale[token, group.valid(lane.eq(0))].store(group_scale),
|
||||
xsum[token, group, store_half.valid(lane.eq(0) | lane.eq(4))].store(
|
||||
store_half.eq(0).where(gsum[0].float(), gsum[1].float()))))
|
||||
value = x.reshape(tokens, groups, 32)[token, group, lane].float()
|
||||
# Quantize each input once, then pack four neighboring lanes into one word.
|
||||
d = (warp_reduce(value.abs(), maximum=True, full_wave=True)/127).maximum(1e-8)
|
||||
rounded = UOp(Ops.CUSTOM, src=(value/d,), arg=("__builtin_nearbyintf({0})", dtypes.float))
|
||||
quant = rounded.clip(-127, 127).cast(dtypes.int8)
|
||||
word = quant.cast(dtypes.uint8).cast(dtypes.uint32) << ((lane%4)*8).cast(dtypes.uint32)
|
||||
for offset in (1, 2):
|
||||
word |= UOp(Ops.CUSTOM, src=(word,), arg=(f"__builtin_amdgcn_ds_swizzle({{0}}, {0x1f | offset<<10})", dtypes.uint32))
|
||||
stores = (q[token, group, (lane//4).valid((lane%4).eq(0))].store(word),
|
||||
scale[token, group.valid(lane.eq(0))].store(d),
|
||||
xsum[token, group, (lane//16).valid((lane%16).eq(0))].store(warp_reduce(quant.float())))
|
||||
return UOp.group(*stores).end(token_group, lane).sink(arg=KernelInfo(name="q8_quantize", opts_to_apply=()))
|
||||
|
||||
def q8_quantize(x:Tensor, tokens:int, in_features:int) -> tuple[Tensor, Tensor, Tensor]:
|
||||
@@ -222,8 +220,8 @@ def _quant_decode_kernel(out:UOp, raw:UOp, xq:UOp, xd:UOp, xs:UOp, out_features:
|
||||
# the packed rows were padded to 212 bytes (53 words) per 256-block in set_quantized: everything is word-aligned
|
||||
base = (output*in_features//GGML_BLOCK_SIZE+block)*Q6_WORDS
|
||||
# the subgroup's 8 ql words and 8 qh words are contiguous: two 16-byte vector loads each
|
||||
lows = tuple(_amd_load(raw[base + (subgroup//4)*16 + (subgroup%2)*8 + half*4], 4) for half in range(2))
|
||||
highs = tuple(_amd_load(raw[base + 32 + (subgroup//4)*8 + half*4], 4) for half in range(2))
|
||||
lows = tuple(_amd_load(raw[base + (subgroup//4)*16 + (subgroup%2)*8 + half*4], 4, stream=True) for half in range(2))
|
||||
highs = tuple(_amd_load(raw[base + 32 + (subgroup//4)*8 + half*4], 4, stream=True) for half in range(2))
|
||||
dots = [UOp.const(0, dtypes.int32)] * 2
|
||||
for word_idx in range(8):
|
||||
within = (subgroup*32 + word_idx*4)%128
|
||||
@@ -241,6 +239,7 @@ def _quant_decode_kernel(out:UOp, raw:UOp, xq:UOp, xd:UOp, xs:UOp, out_features:
|
||||
return _decode_linear(out, out_features, group_count, group_dot, names[ggml_type])
|
||||
|
||||
def _wmma_layout(out:UOp, out_features:int, token_tile:int, output_tiles:int):
|
||||
if out_features % (16*output_tiles): output_tiles = 1
|
||||
output_waves = 2 if out_features % (32*output_tiles) == 0 else 1
|
||||
token_block, output_block = UOp.range(out.shape[0]//token_tile, 0), UOp.range(out_features//(16*output_tiles*output_waves), 1)
|
||||
# lane is a hardware WARP range (like the flash kernel): the fragment math stays visible without being
|
||||
@@ -319,15 +318,9 @@ def _iq4_linear_f16_wmma_kernel(out:UOp, raw:UOp, x:UOp, lut:UOp, out_features:i
|
||||
def dequant(base:UOp, subgroup:UOp, half:int) -> tuple[UOp, ...]:
|
||||
d, scale = _iq4_scales(raw, base, subgroup)
|
||||
scale = scale * d
|
||||
if out_features <= 6144:
|
||||
pairs = tuple(lut[((raw[base + 2 + subgroup*4 + word] >> (byte*8)) & 255).cast(dtypes.weakint)]
|
||||
for word in range(4) for byte in range(4))
|
||||
return tuple((_half((pair >> (half*16)) & 0xffff)*scale).cast(dtypes.float16) for pair in pairs)
|
||||
# a subgroup-half gathers the lo (half=0) or hi (half=1) nibbles of byte pairs of each packed word
|
||||
lut_pairs = (lut[(((raw[base+2+subgroup*4+i] >> (8*j+4*half)) & 15) |
|
||||
(((raw[base+2+subgroup*4+i] >> (8*j+8+4*half)) & 15) << 4)).cast(dtypes.weakint)]
|
||||
for i in range(4) for j in (0, 2))
|
||||
return tuple((_half((pair >> (i*16)) & 0xffff)*scale).cast(dtypes.float16) for pair in lut_pairs for i in range(2))
|
||||
pairs = tuple(lut[((raw[base + 2 + subgroup*4 + word] >> (byte*8)) & 255).cast(dtypes.weakint)]
|
||||
for word in range(4) for byte in range(4))
|
||||
return tuple((_half((pair >> (half*16)) & 0xffff)*scale).cast(dtypes.float16) for pair in pairs)
|
||||
return _quant_linear_wmma(out, x, out_features, in_features, IQ4_WORDS, layout, dequant, "linear_iq4_xs_f16_wmma")
|
||||
|
||||
def q8_linear(layer:Linear, x:Tensor) -> Tensor:
|
||||
@@ -370,21 +363,20 @@ def _amd_f16_gemv_kernel(out:UOp, w:UOp, x:UOp, *rest:UOp, in_features:int, out_
|
||||
for j in range(val_chunk):
|
||||
acc = acc + w[out_row, i, lane*val_chunk + j].load().float() * x[token, i, lane*val_chunk + j].load().float()
|
||||
total = warp_reduce(acc, full_wave=True)
|
||||
if bias is not None: total = total + bias[token, out_row].load().float()
|
||||
if bias is not None: total = total + bias[out_row].load().float()
|
||||
return out[token, out_row.valid(lane.eq(0))].store(total).end(token, out_row, lane).sink(arg=KernelInfo(name="linear_f16_gemv", opts_to_apply=()))
|
||||
|
||||
def _view_back(t:Tensor) -> Tensor:
|
||||
"""strip top-of-chain CAST(s) from a lazy weight: reading the raw file bytes in the kernel instead of
|
||||
materializing the cast into a fresh buffer every step"""
|
||||
# Widening half to float is exact; preserve casts that round or change the values.
|
||||
uop = t.uop
|
||||
while uop.op is Ops.CAST: uop = uop.src[0]
|
||||
while uop.op is Ops.CAST and uop.dtype == dtypes.float32 and uop.src[0].dtype in (dtypes.half, dtypes.bfloat16): uop = uop.src[0]
|
||||
return Tensor(uop).reshape(t.shape)
|
||||
|
||||
def f16_gemv(layer:Linear, x:Tensor) -> Tensor:
|
||||
tokens = prod(x.shape[:-1])
|
||||
assert isinstance(tokens, int)
|
||||
weight = _view_back(layer.weight)
|
||||
x = x.contiguous() if x.dtype == dtypes.half else x.cast(dtypes.half).contiguous()
|
||||
x = x.contiguous()
|
||||
out = Tensor.empty(tokens, layer.out_features, dtype=dtypes.float32, device=x.device)
|
||||
fxn = functools.partial(_amd_f16_gemv_kernel, in_features=layer.in_features, out_features=layer.out_features, tokens=tokens)
|
||||
srcs = (out, weight.reshape(-1), x.reshape(tokens, layer.in_features)) + (() if layer.bias is None else (_view_back(layer.bias),))
|
||||
@@ -403,22 +395,25 @@ def _amd_flash_attention_decode_partial(out, stats, q, cache_kv, valid_kv_len, m
|
||||
_, B, H_KV, N, D = cast(tuple[int, int, int, int, int], cache_kv.shape)
|
||||
_, H, M, _ = cast(tuple[int, int, int, int], q.shape)
|
||||
assert M == 1 and H % H_KV == 0 and D % WARP_SIZE == 0 and max_kv_len <= N and max_kv_len % block_n == 0
|
||||
G, CHUNK, DPL, WAVES = H // H_KV, block_n, D // WARP_SIZE, waves
|
||||
G, CHUNK, DPL, WAVES, PARTIALS = H // H_KV, block_n, D // WARP_SIZE, waves, out.shape[2]
|
||||
assert CHUNK % WAVES == 0
|
||||
SEC = CHUNK // WAVES # keys each wave scans independently
|
||||
live_chunks = (valid_kv_len+CHUNK-1)//CHUNK
|
||||
live_chunks = min(live_chunks, out.shape[2]) if isinstance(live_chunks, int) else live_chunks.minimum(out.shape[2])
|
||||
total_chunks = (valid_kv_len+CHUNK-1)//CHUNK
|
||||
live_chunks = min(total_chunks, PARTIALS) if isinstance(total_chunks, int) else total_chunks.minimum(PARTIALS)
|
||||
block_bhkv, block_chunk = UOp.range(B*H_KV, 0, AxisType.GLOBAL), UOp.range(live_chunks, 1, AxisType.GLOBAL)
|
||||
lane, wave = UOp.range(WARP_SIZE, -1, axis_type=AxisType.WARP), UOp.range(WAVES, 3, axis_type=AxisType.LOCAL)
|
||||
b, kv_head = block_bhkv // H_KV, block_bhkv % H_KV
|
||||
# per-lane query fragments for every GQA head, kept packed in registers; unpacked at use
|
||||
qf = tuple(_vec_load(q[b, kv_head*G+h, 0, lane*DPL], DPL) for h in range(G))
|
||||
zerof = UOp.const(0, dtypes.float)
|
||||
# Each block scans every PARTIALS-th chunk, keeping an online softmax across rounds.
|
||||
chunk_round = UOp.range((total_chunks-1-block_chunk)//PARTIALS+1, 4, AxisType.REDUCE)
|
||||
chunk_id = block_chunk + chunk_round*PARTIALS
|
||||
valids: list[UOp] = []
|
||||
scores: list[list[UOp]] = [[zerof]*G for _ in range(SEC)]
|
||||
vfrags: list[tuple[UOp, ...]] = [()]*SEC
|
||||
for j in range(SEC):
|
||||
key = block_chunk*CHUNK + wave*SEC + j
|
||||
key = chunk_id*CHUNK + wave*SEC + j
|
||||
valid = key < valid_kv_len
|
||||
valids.append(valid)
|
||||
kfrag = _vec_load(cache_kv[0, b, kv_head, key, lane*DPL], DPL)
|
||||
@@ -426,23 +421,32 @@ def _amd_flash_attention_decode_partial(out, stats, q, cache_kv, valid_kv_len, m
|
||||
vfrags[j] = tuple(valid.where(v, zerof) for v in _vec_load(cache_kv[1, b, kv_head, key, lane*DPL], DPL))
|
||||
for h in range(G):
|
||||
s = warp_reduce(sum((qf[h][i]*kfrag[i] for i in range(DPL)), UOp.const(0, dtypes.float)), full_wave=True) * (1/math.sqrt(D))
|
||||
scores[j][h] = valid.where(s, UOp.const(-math.inf, dtypes.float))
|
||||
ninf = UOp.const(-math.inf, dtypes.float)
|
||||
row_max = [functools.reduce(UOp.maximum, (scores[j][h] for j in range(SEC)), ninf) for h in range(G)]
|
||||
accs:list[list[UOp]] = [[UOp.const(0, dtypes.float)] * DPL for _ in range(G)]
|
||||
row_sums:list[UOp] = [UOp.const(0, dtypes.float) for _ in range(G)]
|
||||
scores[j][h] = valid.where(s, UOp.const(-1e30, dtypes.float))
|
||||
# A finite initial max keeps fully masked waves from computing exp(-inf - -inf).
|
||||
acc_reg, max_reg, sum_reg = _reg((G, DPL), 2, 0), _reg((G,), 3, -1e30), _reg((G,), 4, 0)
|
||||
prev_acc, prev_max, prev_sum = acc_reg.after(chunk_round), max_reg.after(chunk_round), sum_reg.after(chunk_round)
|
||||
row_max = [functools.reduce(UOp.maximum, (scores[j][h] for j in range(SEC)), prev_max[h].load()) for h in range(G)]
|
||||
# Rescale the previous rounds to the new max, then accumulate this round's keys.
|
||||
alpha = [((prev_max[h].load()-row_max[h])*LOG2E).exp2() for h in range(G)]
|
||||
accs = [[alpha[h]*prev_acc[h, i].load() for i in range(DPL)] for h in range(G)]
|
||||
row_sums = [alpha[h]*prev_sum[h].load() for h in range(G)]
|
||||
for j in range(SEC):
|
||||
for h in range(G):
|
||||
beta = valids[j].where(((scores[j][h]-row_max[h])*LOG2E).exp2(), UOp.const(0, dtypes.float))
|
||||
beta = valids[j].where(((scores[j][h]-row_max[h])*LOG2E).exp2(), zerof)
|
||||
accs[h] = [a + beta*v for a, v in zip(accs[h], vfrags[j])]
|
||||
row_sums[h] = row_sums[h] + beta
|
||||
update = UOp.group(acc_reg.store(UOp.stack(*(x for acc in accs for x in acc)).reshape(G, DPL)),
|
||||
max_reg.store(UOp.stack(*row_max)), sum_reg.store(UOp.stack(*row_sums))).end(chunk_round)
|
||||
acc_reg, max_reg, sum_reg = acc_reg.after(update), max_reg.after(update), sum_reg.after(update)
|
||||
# exchange across the block's waves through LDS (fp16 halves LDS so more blocks fit per CU)
|
||||
acc_lds = UOp.placeholder((WAVES, G, D), dtypes.half, slot=0, addrspace=AddrSpace.LOCAL)
|
||||
# Matching cache/LDS strides can reuse a loop-local cache index outside the loop. Pad that layout.
|
||||
acc_lds = UOp.placeholder((WAVES, G, D + (LDS_PAD if G == SEC else 0)), dtypes.half, slot=0, addrspace=AddrSpace.LOCAL)[:, :, :D]
|
||||
ml_lds = UOp.placeholder((WAVES, G, 2), dtypes.float, slot=1, addrspace=AddrSpace.LOCAL)
|
||||
lds_acc = acc_lds.reshape(WAVES, G, WARP_SIZE, DPL)
|
||||
stores = [lds_acc[wave, h, lane].store(UOp.stack(*accs[h]).cast(dtypes.half)) for h in range(G)]
|
||||
# Normalize before fp16 to avoid overflow. Nonempty waves have sum >= 1; empty waves keep their zero accumulator.
|
||||
stores = [lds_acc[wave, h, lane].store((acc_reg[h].load() / sum_reg[h].load().maximum(1)).cast(dtypes.half)) for h in range(G)]
|
||||
# NOTE: duplicate stores of the same value from every lane are harmless here
|
||||
stores += [ml_lds[wave, h, i].store(x) for h in range(G) for i, x in enumerate((row_max[h], row_sums[h]))]
|
||||
stores += [ml_lds[wave, h, i].store(x) for h in range(G) for i, x in enumerate((max_reg[h].load(), sum_reg[h].load()))]
|
||||
barrier = UOp.barrier(UOp.group(*stores))
|
||||
acc_lds, ml_lds = acc_lds.after(barrier), ml_lds.after(barrier)
|
||||
tid = wave*WARP_SIZE + lane
|
||||
@@ -450,14 +454,16 @@ def _amd_flash_attention_decode_partial(out, stats, q, cache_kv, valid_kv_len, m
|
||||
for i in range(-(-G*D//(WAVES*WARP_SIZE))):
|
||||
flat = tid + i*WAVES*WARP_SIZE
|
||||
h, d = flat // D, flat % D
|
||||
M = functools.reduce(UOp.maximum, (ml_lds[w, h, 0].load() for w in range(WAVES)), ninf)
|
||||
val = sum((((ml_lds[w, h, 0].load()-M)*LOG2E).exp2() * acc_lds[w, h, d].load().float() for w in range(WAVES)), UOp.const(0, dtypes.float))
|
||||
M = functools.reduce(UOp.maximum, (ml_lds[w, h, 0].load() for w in range(WAVES)))
|
||||
# LDS holds normalized values; restore each wave's sum before combining.
|
||||
val = sum((((ml_lds[w, h, 0].load()-M)*LOG2E).exp2() * ml_lds[w, h, 1].load() * acc_lds[w, h, d].load().float()
|
||||
for w in range(WAVES)), zerof)
|
||||
oidx = out[b, kv_head*G + h, block_chunk, d]
|
||||
if G*D % (WAVES*WARP_SIZE): oidx = out[b, (kv_head*G + h).valid(flat < G*D), block_chunk, d]
|
||||
final_stores.append(oidx.store(val))
|
||||
hstat = tid
|
||||
M = functools.reduce(UOp.maximum, (ml_lds[w, hstat, 0].load() for w in range(WAVES)), ninf)
|
||||
L = sum((((ml_lds[w, hstat, 0].load()-M)*LOG2E).exp2() * ml_lds[w, hstat, 1].load() for w in range(WAVES)), UOp.const(0, dtypes.float))
|
||||
M = functools.reduce(UOp.maximum, (ml_lds[w, hstat, 0].load() for w in range(WAVES)))
|
||||
L = sum((((ml_lds[w, hstat, 0].load()-M)*LOG2E).exp2() * ml_lds[w, hstat, 1].load() for w in range(WAVES)), zerof)
|
||||
q_head = (kv_head*G + hstat).valid(hstat < G) if WAVES*WARP_SIZE > G else kv_head*G + hstat
|
||||
final_stores += [stats[b, q_head, block_chunk, 0].store(M), stats[b, q_head, block_chunk, 1].store(L)]
|
||||
return UOp.group(*final_stores).end(lane, wave, block_chunk, block_bhkv).sink(arg=KernelInfo(name="flash_decode_partial", opts_to_apply=()))
|
||||
@@ -494,10 +500,13 @@ def _amd_flash_decode_combine(o:UOp, partial:UOp, stats:UOp, live:int|UOp) -> UO
|
||||
|
||||
def amd_flash_attention_decode(q:Tensor, cache_kv:Tensor, valid_kv_len:int|UOp, max_kv_len:int) -> Tensor:
|
||||
B, H, D = cache_kv.shape[1], q.shape[1], cache_kv.shape[4]
|
||||
chunks = min(256, max_kv_len // 64)
|
||||
chunks = min(48, max_kv_len // 64)
|
||||
partial = Tensor.empty(B, H, chunks, D, dtype="float32", device=q.device)
|
||||
stats = Tensor.empty(B, H, chunks, 2, dtype="float32", device=q.device)
|
||||
fxn = functools.partial(_amd_flash_attention_decode_partial, valid_kv_len=valid_kv_len, max_kv_len=max_kv_len, block_n=64, waves=16)
|
||||
waves, group = 16, H // cache_kv.shape[2]
|
||||
while waves * group * ((D+LDS_PAD)*2 + 8) > 65536: waves //= 2
|
||||
assert waves > 0, "attention head group exceeds shared memory capacity"
|
||||
fxn = functools.partial(_amd_flash_attention_decode_partial, valid_kv_len=valid_kv_len, max_kv_len=max_kv_len, block_n=64, waves=waves)
|
||||
partial, stats = Tensor.custom_kernel(partial, stats, q, cache_kv, fxn=fxn)[:2]
|
||||
live = (valid_kv_len+63)//64
|
||||
live = min(live, chunks) if isinstance(live, int) else live.minimum(chunks)
|
||||
@@ -513,7 +522,7 @@ def _amd_flash_attention(o:UOp, q:UOp, cache:UOp, valid_kv_len:int|UOp, q_start:
|
||||
k, v = cache[0].reshape(B*H_KV, physical_n, cache_dim), cache[1].reshape(B*H_KV, physical_n, cache_dim)
|
||||
assert k.shape == v.shape and BH % k.shape[0] == 0 and k.shape[2] == D
|
||||
gqa_group = BH // k.shape[0]
|
||||
if isinstance(M, int) and isinstance(valid_kv_len, int): assert M % BLOCK_M == 0 and valid_kv_len % BLOCK_N == 0
|
||||
if isinstance(M, int): assert M % BLOCK_M == 0
|
||||
assert isinstance(D, int) and D % WMMA_K == 0 and D % LANES_PER_WAVE_N == 0
|
||||
TM, TN, TD, SCALE = BLOCK_M//(WAVES_M*LANES_PER_WAVE_M), BLOCK_N//LANES_PER_WAVE_N, D//(WAVES_N*LANES_PER_WAVE_N), 1/math.sqrt(D)
|
||||
# query row 0 sits at sequence position q_base (the queries may be padded beyond valid_kv_len - q_base rows)
|
||||
@@ -569,7 +578,8 @@ def _amd_flash_attention(o:UOp, q:UOp, cache:UOp, valid_kv_len:int|UOp, q_start:
|
||||
acc, l_i, m_i, beta_i = acc.after(correction), l_i.after(correction), m_i.after(correction), beta_i.after(correction)
|
||||
V_lds = UOp.placeholder((D, BLOCK_N + LDS_PAD), dtypes.half, slot=1, addrspace=AddrSpace.LOCAL)[:, :BLOCK_N]
|
||||
V_copy, load_v = V_lds.after(qk_done).permute(1, 0), UOp.range(KV_ELEMS_PER_THREAD, 390)
|
||||
vval = v.reshape(physical_n*D)[n_tile*BLOCK_N*D + tid*KV_ELEMS_PER_THREAD + load_v].float()
|
||||
v_pos = n_tile*BLOCK_N + (tid*KV_ELEMS_PER_THREAD + load_v)//D
|
||||
vval = (v_pos < valid_kv_len).where(v.reshape(physical_n*D)[n_tile*BLOCK_N*D + tid*KV_ELEMS_PER_THREAD + load_v].float(), 0)
|
||||
V_store = V_copy.reshape(THREADS_PER_BLOCK, KV_ELEMS_PER_THREAD)[tid, load_v].store(vval).end(load_v)
|
||||
pv_barrier = UOp.barrier(UOp.group(P_store, V_store))
|
||||
P_lds, V_lds = P_lds.after(pv_barrier), V_lds.after(pv_barrier)
|
||||
@@ -591,7 +601,16 @@ def _amd_flash_attention(o:UOp, q:UOp, cache:UOp, valid_kv_len:int|UOp, q_start:
|
||||
def flash_attention(q:Tensor, assigned_kv:Tensor, valid_end:int|UOp) -> Tensor:
|
||||
# cached flash attention on the half KV cache (already written through assigned_kv); valid_end stays bound at the graph level
|
||||
T_real, q_start = q.shape[2], None
|
||||
if resolve(T_real == 1): return amd_flash_attention_decode(q.half(), assigned_kv, valid_end, cast(int, assigned_kv.shape[3]))
|
||||
D, N, group = q.shape[3], assigned_kv.shape[3], q.shape[1] // assigned_kv.shape[2]
|
||||
decode = resolve(T_real == 1, False)
|
||||
# Non-power-of-two decode dimensions can lose tail-store masks. Q/P, K, and V use separate LDS allocations.
|
||||
supported = D % 32 == 0 and (D & (D-1) == 0 and N % 64 == 0 and group*((D+LDS_PAD)*2+8) <= 65536 if decode else
|
||||
D >= 64 and 2*(2*BLOCK_M*(D+LDS_PAD) + D*(BLOCK_N+LDS_PAD)) <= 65536 and N % BLOCK_N == 0 and q.max_shape[2] % BLOCK_M == 0)
|
||||
if not supported:
|
||||
k, v = (assigned_kv[i, :, :, :valid_end].float() for i in range(2))
|
||||
mask = None if decode else Tensor.full((T_real, valid_end), -math.inf, dtype=dtypes.float32, device=q.device).triu(valid_end-T_real+1)
|
||||
return q.float().scaled_dot_product_attention(k, v, attn_mask=mask, enable_gqa=True)
|
||||
if decode: return amd_flash_attention_decode(q.half(), assigned_kv, valid_end, cast(int, N))
|
||||
if isinstance(T_real, UOp):
|
||||
# symbolic chunk: pad the queries to the static tile; garbage rows are sliced off
|
||||
T_pad = q.max_shape[2]
|
||||
@@ -645,12 +664,14 @@ def gated_delta_prefill(q:Tensor, k:Tensor, v:Tensor, beta:Tensor, alpha:Tensor,
|
||||
assert q.shape == k.shape and v.shape[:3] == beta.shape == (batch, heads, tokens) and state.shape == (batch, heads, value_dim, key_dim)
|
||||
assert alpha.shape[:3] == (batch, heads, tokens) and (len(alpha.shape) == 3 or alpha.shape[-1] in (1, value_dim))
|
||||
assert key_dim % 32 == 0 and value_dim % 4 == 0
|
||||
assert q.dtype == k.dtype == dtypes.float32, "recurrent Q/K must be float32"
|
||||
assert state.uop.contiguous_view_offset() is not None, "recurrent state must be contiguous"
|
||||
if start_pos is not None:
|
||||
assert start_pos.uop.is_bound_var
|
||||
state = Tensor(state.uop.after(start_pos.uop))
|
||||
core, kq = Tensor.empty_like(v), (q*k).sum(-1).contiguous()
|
||||
srcs = (core, q.contiguous(), k.contiguous(), v.contiguous(), beta.contiguous(), alpha.contiguous(), state, kq)
|
||||
if start_pos is None: return Tensor.custom_kernel(*srcs, fxn=_gated_delta_prefill_kernel)[0]
|
||||
contig = tuple(x.uop if x.uop.op is Ops.AFTER else x.uop.contiguous() for x in srcs)
|
||||
params = tuple(UOp.placeholder_like(x, slot=i) for i,x in enumerate(contig))
|
||||
assert start_pos.uop.is_bound_var
|
||||
# the bound start_pos reaches the graph through the state AFTER chain, like the flash kernels' valid_end
|
||||
call = _gated_delta_prefill_kernel(*params, kernel_var(start_pos.uop.src[0])).call(*contig)
|
||||
call = _gated_delta_prefill_kernel(*params, None if start_pos is None else kernel_var(start_pos.uop.src[0])).call(*contig)
|
||||
return Tensor(contig[0].after(call))
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
import json, pathlib, re, time, typing, uuid
|
||||
from typing import TYPE_CHECKING
|
||||
from tinygrad.helpers import DEBUG, colored, stderr_log
|
||||
from tinygrad.viz.serve import TCPServerWithReuse, HTTPRequestHandler
|
||||
from tinygrad.viz.serve import TCPServerWithReuse, Handler as VizHandler
|
||||
if TYPE_CHECKING:
|
||||
from tinygrad.llm.cli import SimpleTokenizer
|
||||
from tinygrad.llm.model import Transformer
|
||||
@@ -60,11 +60,12 @@ class StreamRouter:
|
||||
if emit: yield "content", emit
|
||||
if found: self.mode, self.buf = "tool", "<tool_call>" + self.buf
|
||||
|
||||
class Handler(HTTPRequestHandler):
|
||||
class Handler(VizHandler):
|
||||
server: LLMServer
|
||||
def log_request(self, code='-', size='-'): pass
|
||||
def do_GET(self):
|
||||
if self.path == "/v1/models": self.send_data(json.dumps({"object":"list","data":[{"id":self.server.model_name,"object":"model"}]}).encode())
|
||||
elif self.path.startswith("/assets/"): super().do_GET()
|
||||
else: self.send_data((pathlib.Path(__file__).parent / "chat.html").read_bytes(), content_type="text/html")
|
||||
def run_model(self, ids:list[int], model_name:str, include_usage=False, max_tokens:int|None=None, temperature:float=0.0,
|
||||
reasoning:bool=False):
|
||||
|
||||
@@ -58,14 +58,11 @@ class ElementwiseMixin(CreationMixin):
|
||||
|
||||
def contiguous(self, **kwargs) -> Self:
|
||||
"""
|
||||
Requests a contiguous layout for this value when it is computed.
|
||||
This does not reserve independent storage or retain an intermediate result across realizations; use `clone()` for that.
|
||||
Returns a contiguous tensor.
|
||||
"""
|
||||
if self.dtype in dtypes.weaks: return self
|
||||
uop = self._uop
|
||||
src = uop
|
||||
while src.op in {Ops.DETACH, Ops.CONTIGUOUS_BACKWARD}: src = src.src[0]
|
||||
if uop.op is Ops.CONTIGUOUS or self.device is None or src.has_buffer_identity(after_ok=True): return self._wrap_uop(uop)
|
||||
if uop.op is Ops.CONTIGUOUS or self.device is None or uop.has_buffer_identity(): return self._wrap_uop(uop)
|
||||
return self._wrap_uop(uop.alu(Ops.CONTIGUOUS, **kwargs))
|
||||
|
||||
def contiguous_backward(self) -> Self:
|
||||
|
||||
@@ -52,7 +52,6 @@ class RandMixin(OpMixin):
|
||||
Creates a tensor with the given shape, filled with random values from a uniform distribution over the interval `[0, 1)`.
|
||||
|
||||
You can pass in `dtype` and `device` keyword arguments to control the data type and device of the tensor.
|
||||
By default, the random values get persistent storage when computed. `contiguous=False` leaves them as an expression.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
Tensor.manual_seed(42)
|
||||
@@ -66,8 +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))
|
||||
out = cls._rand(key, counter, shape, dt, contiguous=False)
|
||||
return cls._wrap_uop(out._uop.clone()) if contiguous else out
|
||||
return cls._rand(key, counter, shape, dt, contiguous=contiguous)
|
||||
|
||||
def rand_like(self, **kwargs) -> Self:
|
||||
"""
|
||||
@@ -295,8 +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)
|
||||
mask = self.rand_like(dtype=dtypes.default_float, contiguous=False) >= p
|
||||
return self._wrap_uop(mask._uop.clone()).where(self, 0) / (1.0 - p)
|
||||
return (self.rand_like(dtype=dtypes.default_float, contiguous=False) >= p).contiguous().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:
|
||||
|
||||
@@ -3,6 +3,7 @@ import itertools
|
||||
from dataclasses import dataclass, field
|
||||
from tinygrad.renderer import Renderer
|
||||
from tinygrad.uop.ops import PatternMatcher, UOp, Ops
|
||||
from typing import Any
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Register:
|
||||
@@ -23,23 +24,24 @@ class IselContext:
|
||||
def vreg(self, cons:tuple[Register, ...]|Register):
|
||||
return Register(f"v{next(self.reg_n)}", 0, _cons=cons if isinstance(cons, tuple) else (cons,))
|
||||
|
||||
def greg(u:UOp):
|
||||
if u.op in {Ops.NOOP, Ops.AFTER, Ops.BITCAST} and u.src: return greg(u.src[0])
|
||||
if isinstance(u.tag, tuple): return u.tag[0]
|
||||
return u.tag
|
||||
def rdef(u:UOp):
|
||||
if u.op in {Ops.NOOP, Ops.AFTER, Ops.BITCAST} and u.src: return rdef(u.src[0])
|
||||
return u.tag[0] if isinstance(u.tag, tuple) else u.tag
|
||||
|
||||
@dataclass
|
||||
class PreRegAllocContext:
|
||||
lock: UOp|None = None
|
||||
class LinearContext:
|
||||
def __init__(self, ren:ISARenderer):
|
||||
self.ren, self.stack_size = ren, 0
|
||||
self.loop_label: dict[UOp, str] = {}
|
||||
def assign_spill_slot(self, r:Register, u:UOp) -> Any: raise NotImplementedError("arch specific")
|
||||
|
||||
class ISARenderer(Renderer):
|
||||
pre_isel_matcher: PatternMatcher
|
||||
isel_matcher: PatternMatcher
|
||||
pre_regalloc_matcher: PatternMatcher
|
||||
post_regalloc_matcher: PatternMatcher
|
||||
linear_ctx_type: type = LinearContext
|
||||
|
||||
def is_two_address(self, x:UOp) -> bool: return False
|
||||
def stack_pointer(self) -> 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 spill(self, spill_slot:Any, x:UOp) -> UOp: raise NotImplementedError("arch specific")
|
||||
def fill(self, spill_slot:Any, x:UOp, reg:Register) -> UOp: raise NotImplementedError("arch specific")
|
||||
def asm_str(self, uops:list[UOp], function_name:str) -> str: raise NotImplementedError("arch specific")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from __future__ import annotations
|
||||
# flake8: noqa: E702
|
||||
# allow semicolons to put multiple ops on one line
|
||||
import sys, struct, functools
|
||||
@@ -6,7 +7,7 @@ from dataclasses import replace
|
||||
from tinygrad.dtype import dtypes, DType, truncate, AddrSpace
|
||||
from tinygrad.uop import FastEnum, auto, Ops, GroupOp
|
||||
from tinygrad.uop.ops import UOp, UPat, PatternMatcher, promo_dtype
|
||||
from tinygrad.renderer.isa import ISARenderer, IselContext, Register, PreRegAllocContext, greg
|
||||
from tinygrad.renderer.isa import ISARenderer, IselContext, Register, LinearContext, rdef
|
||||
from tinygrad.helpers import unwrap, Target
|
||||
|
||||
# ***** X86 Ops *****
|
||||
@@ -158,6 +159,9 @@ pre_isel_matcher = PatternMatcher([
|
||||
])
|
||||
|
||||
# ***** X86 registers *****
|
||||
def def_reg(dt:DType, reg:Register) -> UOp: return UOp(Ops.INS, arg=(X86Ops.DEFINE, dt), tag=(reg,))
|
||||
# undefined operand, used for VEX instructions
|
||||
def undef(): return UOp(Ops.NOOP)
|
||||
|
||||
RAX = Register("rax", 0)
|
||||
RCX = Register("rcx", 1)
|
||||
@@ -178,11 +182,12 @@ reg_strs = {"rax": {4:"eax", 2:"ax", 1:"al"}, "rcx": {4:"ecx", 2:"cx", 1:"cl"},
|
||||
"rsp": {4:"esp", 2:"sp", 1:"spl"}, "rbp": {4:"ebp", 2:"bp", 1:"bpl"}, "rsi": {4:"esi", 2:"si", 1:"sil"}, "rdi": {4:"edi", 2:"di", 1:"dil"},
|
||||
**{f"r{i}": {4:f"r{i}d", 2:f"r{i}w", 1:f"r{i}b"} for i in range(8, 16)}}
|
||||
|
||||
stack_pointer = def_reg(dtypes.uint64, RSP)
|
||||
|
||||
# ***** X86 instruction selection *****
|
||||
def base(x:UOp, i:int) -> UOp: return s.src[0] if (s:=x.src[i]).op is Ops.INDEX else s
|
||||
def lane(x:UOp, i:int) -> int: return s.src[1].src[0].val if (s:=x.src[i]).op is Ops.INDEX else 0
|
||||
def to_int(dt:DType): return {dtypes.float16: dtypes.int16, dtypes.float32: dtypes.int32, dtypes.float64: dtypes.int64}[dt]
|
||||
def def_reg(dt:DType, reg:Register|None=None) -> UOp: return UOp(Ops.INS, arg=(X86Ops.DEFINE, dt), tag=None if reg is None else (reg,))
|
||||
def imm(dt:DType, v:int) -> UOp: return UOp.cconst(truncate[dt](v), dt).rtag()
|
||||
def to_imm(c:UOp) -> UOp|None:
|
||||
if not (c.op is Ops.CAST and (v:=c.src[0]).op is Ops.CONST): return None
|
||||
@@ -206,13 +211,13 @@ def vinsertps(x:UOp) -> UOp:
|
||||
def _insert(ret:UOp, i:int) -> UOp:
|
||||
s, v = base(x, i), lane(x, i)
|
||||
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))
|
||||
return functools.reduce(_insert, range(len(x.src)), undef())
|
||||
|
||||
# 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 = {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))
|
||||
return functools.reduce(lambda ret,i: x.ins(op, src=(ret, srcs[i], imm(dtypes.uint8, i))), range(len(srcs)), undef())
|
||||
|
||||
# we don't call ctx.vreg on the srcs to avoid duplicates, a rewrite will assign the tuple of valid registers to a vreg
|
||||
def idiv(ctx:IselContext, x:UOp) -> UOp:
|
||||
@@ -265,7 +270,7 @@ def abi(ctx:IselContext, x:UOp) -> UOp|None:
|
||||
# the shape srcs of a PARAM are not values, tag them so they aren't materialized into registers
|
||||
def _reg_arg(r:Register) -> tuple[UOp, ...]: return (x.replace(arg=arg, src=tuple(s.rtag() for s in x.src), tag=(r,)),)
|
||||
def _stack_arg(disp:int):
|
||||
return (def_reg(dtypes.uint64, RSP), UOp(Ops.NOOP), UOp(Ops.INS, arg=(X86Ops.FRAME_INDEX, dtypes.int32), tag=disp), imm(dtypes.uint8, 8))
|
||||
return (stack_pointer, UOp(Ops.NOOP), UOp(Ops.INS, arg=(X86Ops.FRAME_INDEX, dtypes.int32), src=(imm(dtypes.int32, disp),)), imm(dtypes.uint8, 8))
|
||||
if sys.platform == "win32": src = _reg_arg((RCX, RDX, GPR[8], GPR[9])[i]) if i < 4 else _stack_arg((i-3)*8+32)
|
||||
else: src = _reg_arg((RDI, RSI, RDX, RCX, GPR[8], GPR[9])[i]) if i < 6 else _stack_arg((i-5)*8)
|
||||
# this move "cleanses" the abi register constraint
|
||||
@@ -320,9 +325,9 @@ isel_matcher = PatternMatcher([
|
||||
lambda x,cond: cond.ins(X86Ops.LOOP_CMP, tag=cond.op, src=cond.src + x.src[:2])),
|
||||
# **** Op -> X86Op ****
|
||||
# add callee saved registers to the RET, these will be scheduled at the top of the kernel and will be saved/restored if they are used in regalloc
|
||||
# so regalloc builds the prologue/epilogue naturally
|
||||
# so regalloc builds the prologue/epilogue naturally. they all share the stack pointer define's dtype so the the stack pointer define is first
|
||||
(UPat(Ops.SINK, name="x"), lambda x:
|
||||
x.replace(src=(x.ins(X86Ops.RET, src=x.src + tuple(def_reg(dtypes.uint64 if r in GPR else dtypes.float64, r) for r in CALLEE_SAVED)),)) \
|
||||
x.replace(src=(x.ins(X86Ops.RET, src=x.src + (stack_pointer,) + tuple(def_reg(dtypes.uint64, r) for r in CALLEE_SAVED)),))
|
||||
if not x.src or x.src[0].op is not Ops.INS or x.src[0].arg[0] is not X86Ops.RET else None),
|
||||
# function abi constraints
|
||||
(UPat((Ops.PARAM, Ops.SPECIAL), name="x"), abi),
|
||||
@@ -417,8 +422,8 @@ isel_matcher = PatternMatcher([
|
||||
(UPat(dtype=dtypes.float64).cast(dtypes.int32s+dtypes.int64s, name="x"), lambda x: x.ins(X86Ops.VCVTTSD2SI)),
|
||||
(UPat.var("y", dtypes.float32).cast(dtypes.float64, name="x"), lambda y,x: x.ins(X86Ops.VCVTSS2SD, src=(y, y))),
|
||||
(UPat.var("y", dtypes.float64).cast(dtypes.float32, name="x"), lambda y,x: x.ins(X86Ops.VCVTSD2SS, src=(y, y))),
|
||||
(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.var("y", (dtypes.int32, dtypes.int64)).cast(dtypes.float32, name="x"), lambda y,x: x.ins(X86Ops.VCVTSI2SS, src=(undef(), y))),
|
||||
(UPat.var("y", (dtypes.int32, dtypes.int64)).cast(dtypes.float64, name="x"), lambda y,x: x.ins(X86Ops.VCVTSI2SD, src=(undef(), y))),
|
||||
(UPat(dtype=(dtypes.uint8, dtypes.uint16, dtypes.bool)).cast(dtypes.ints, name="x"), lambda x:
|
||||
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)),
|
||||
@@ -436,7 +441,7 @@ isel_matcher = PatternMatcher([
|
||||
# TODO: fuse stores, very few cases -- store cmp becomes setcc, store gep int becomes vpextr, store bitcast to int becomes vmovd/q
|
||||
# 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(X86Ops.VPINSRW, src=(undef(),) + 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(_xmm_sz(x), src=fold_address(a))),
|
||||
@@ -454,14 +459,21 @@ isel_matcher = PatternMatcher([
|
||||
# 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):
|
||||
def flag_rematerialize(ctx:X86LinearContext, x:UOp):
|
||||
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
|
||||
|
||||
# TODO: dont use rewrite
|
||||
def alloc_buffer(ctx:X86LinearContext, x:UOp):
|
||||
nx = isel_matcher.rewrite(stack_pointer.index(UOp.cconst(ctx.stack_size, dtypes.uint32), tag=x.tag))
|
||||
ctx.stack_size += x.max_numel() * x.dtype.itemsize
|
||||
return nx, [nx]
|
||||
|
||||
pre_regalloc_matcher = PatternMatcher([
|
||||
(UPat(Ops.BUFFER, name="x"), alloc_buffer),
|
||||
(UPat((Ops.INS, Ops.RANGE, Ops.END), name="x"), flag_rematerialize),
|
||||
])
|
||||
|
||||
@@ -492,8 +504,14 @@ def lower_loop(ctx, x:UOp) -> tuple[UOp, list[UOp]]:
|
||||
|
||||
# final rewrite to match the isa spec
|
||||
post_regalloc_matcher = PatternMatcher([
|
||||
# the frame is allocated after the stack pointer define at the top of the program and freed before RET
|
||||
(UPat(Ops.INS, name="x"), lambda ctx,x: (x, [x, x.ins(X86Ops.SUBi, src=(imm(dtypes.int32, ctx.stack_size),))])
|
||||
if ctx.stack_size and x.arg[0] is X86Ops.DEFINE and rdef(x) == RSP else None),
|
||||
(UPat(Ops.INS, name="x"), lambda ctx,x: (x, [stack_pointer.ins(X86Ops.ADDi, src=(imm(dtypes.int32, ctx.stack_size),)), x])
|
||||
if ctx.stack_size and x.arg[0] is X86Ops.RET else None),
|
||||
# rewrite FRAME_INDEX to IMM now that the stack size is known
|
||||
(UPat(Ops.INS, name="x"), lambda ctx,x: (nx:=UOp.cconst(ctx.stack_size + x.tag, x.dtype), [nx]) if x.arg[0] is X86Ops.FRAME_INDEX else None),
|
||||
(UPat(Ops.INS, src=(UPat.cvar("disp").cast(),), name="x"), lambda ctx,disp,x:
|
||||
(nx:=UOp.cconst(ctx.stack_size + disp.val, x.dtype), [nx]) if x.arg[0] is X86Ops.FRAME_INDEX else None),
|
||||
# expand the cmp here so we can preserve rng src edge to get label from ctx
|
||||
(UPat(Ops.INS, name="x"), lambda ctx,x: lower_loop(ctx, x) if x.arg[0] is X86Ops.LOOP_CMP else None),
|
||||
# rewrite RANGE to ACC = 0 -> LABEL -> JUMP if ACC >= loop bound
|
||||
@@ -502,7 +520,7 @@ post_regalloc_matcher = PatternMatcher([
|
||||
(UPat(Ops.END, name="x"), lower_end),
|
||||
# rewrite two address instructions to two address form, if reused src wasn't coalesced insert a move
|
||||
(UPat(Ops.INS, name="x"), lambda ctx,x: (nx:=x.replace(src=x.src[1:]),
|
||||
[ctx.ren.copy(x.src[0], greg(x)), nx] if greg(x) != greg(x.src[0]) else [nx]) if x.arg[0] in X86GroupOp.TwoAddress else None),
|
||||
[ctx.ren.copy(x.src[0], rdef(x)), nx] if rdef(x) != rdef(x.src[0]) else [nx]) if x.arg[0] in X86GroupOp.TwoAddress else None),
|
||||
])
|
||||
|
||||
# ***** X86 instruction encoding *****
|
||||
@@ -512,9 +530,9 @@ def encode(x:UOp, opc:int, reg:int|None=None, pp:int=0, sel:int=0, we:int=0) ->
|
||||
vvvv_uop:UOp|None=None, imm_uop:UOp|None=None) -> bytes:
|
||||
nonlocal reg, opc
|
||||
# get the encoding values of the different fields
|
||||
reg = cast(int, cast(Register, greg(reg_uop)).index if reg_uop is not None else reg)
|
||||
rm = cast(Register, greg(rm_uop)).index
|
||||
idx = cast(Register, greg(idx_uop)).index if idx_uop is not None and greg(idx_uop) is not None else 4
|
||||
reg = cast(int, cast(Register, rdef(reg_uop)).index if reg_uop is not None else reg)
|
||||
rm = cast(Register, rdef(rm_uop)).index
|
||||
idx = cast(Register, rdef(idx_uop)).index if idx_uop is not None and rdef(idx_uop) is not None else 4
|
||||
# for a memory operand the rm size is the element size from the address, otherwise it's the size of the value in the register
|
||||
rm_sz = sz_uop.src[0].val if sz_uop is not None else rm_uop.dtype.itemsize
|
||||
reg_sz = reg_uop.dtype.itemsize if reg_uop is not None else 0
|
||||
@@ -526,7 +544,7 @@ def encode(x:UOp, opc:int, reg:int|None=None, pp:int=0, sel:int=0, we:int=0) ->
|
||||
# r extends reg field, x extends index field, b extends rm or base field
|
||||
r, _x, b = reg >> 3, idx >> 3, rm >> 3
|
||||
if sel: # VEX bytes
|
||||
vvvv = cast(Register, greg(vvvv_uop)).index if vvvv_uop is not None else 0
|
||||
vvvv = (vd.index if isinstance(vd := rdef(vvvv_uop), Register) else reg) if vvvv_uop is not None else 0
|
||||
if sel == 1 and _x == b == we == 0: inst += bytes([0xC5, (~r & 0b1) << 7 | (~vvvv & 0b1111) << 3 | pp])
|
||||
else: inst += bytes([0xC4, (~r & 0b1) << 7 | (~_x & 0b1) << 6 | (~b & 0b1) << 5 | sel, we << 7 | (~vvvv & 0b1111) << 3 | pp])
|
||||
else: # optional PREFIX and REX bytes
|
||||
@@ -571,7 +589,7 @@ def encode(x:UOp, opc:int, reg:int|None=None, pp:int=0, sel:int=0, we:int=0) ->
|
||||
# IMM byte
|
||||
if imm_uop is not None:
|
||||
if imm_uop.op is Ops.CAST: inst += struct.pack(unwrap(imm_uop.dtype.fmt), imm_uop.src[0].val)
|
||||
elif isinstance(greg(imm_uop), Register): inst += bytes([(greg(imm_uop).index & 0b1111) << 4 | 0b0000])
|
||||
elif isinstance(rdef(imm_uop), Register): inst += bytes([(rdef(imm_uop).index & 0b1111) << 4 | 0b0000])
|
||||
return inst
|
||||
|
||||
# get the encoding structure of the uop
|
||||
@@ -604,7 +622,7 @@ def encode(x:UOp, opc:int, reg:int|None=None, pp:int=0, sel:int=0, we:int=0) ->
|
||||
encodings = {
|
||||
# moves
|
||||
X86Ops.MOVABS: lambda x:
|
||||
bytes([0b0100 << 4 | 0b1 << 3 | 0b00 << 2 | greg(x).index >> 3, 0xB8 + (greg(x).index & 0b111)]) + struct.pack(x.dtype.fmt, x.src[0].src[0].val),
|
||||
bytes([0b0100 << 4 | 0b1 << 3 | 0b00 << 2 | rdef(x).index >> 3, 0xB8 + (rdef(x).index & 0b111)]) + struct.pack(x.dtype.fmt, x.src[0].src[0].val),
|
||||
X86Ops.MOV: lambda x: encode(x, 0x8B), X86Ops.MOVi: lambda x: encode(x, 0xC7, reg=0),
|
||||
X86Ops.MOVm: lambda x: encode(x, 0x89), X86Ops.LEA: lambda x: encode(x, 0x8D),
|
||||
X86Ops.VMOVSS: lambda x: encode(x, 0x10, pp=2, sel=1), X86Ops.VMOVSSm: lambda x: encode(x, 0x11, pp=2, sel=1),
|
||||
@@ -666,6 +684,16 @@ encodings = {
|
||||
X86Ops.RET: lambda x: bytes([0xC3]),
|
||||
}
|
||||
|
||||
class X86LinearContext(LinearContext):
|
||||
def __init__(self, ren:X86Renderer):
|
||||
super().__init__(ren)
|
||||
self.lock: UOp|None = None
|
||||
def assign_spill_slot(self, r:Register, u:UOp) -> int:
|
||||
sz = r.cons[0].size
|
||||
offset = self.stack_size + (sz - self.stack_size % sz) %sz
|
||||
self.stack_size = offset + sz
|
||||
return offset
|
||||
|
||||
class X86Renderer(ISARenderer):
|
||||
device = "CPU"
|
||||
has_local = False
|
||||
@@ -676,34 +704,36 @@ class X86Renderer(ISARenderer):
|
||||
pre_regalloc_matcher = pre_regalloc_matcher
|
||||
post_regalloc_matcher = post_regalloc_matcher
|
||||
code_for_op = {x: lambda: None for x in (Ops.SQRT, Ops.AND, Ops.OR, Ops.SHL, Ops.SHR, Ops.NEG, Ops.SUB, Ops.FDIV, Ops.CMPLT, Ops.CMPEQ)}
|
||||
linear_ctx_type = X86LinearContext
|
||||
def __init__(self, target:Target):
|
||||
if target.arch.split(",")[0] != "x86_64": raise RuntimeError(f"X86Renderer only supports x86_64, got {target.arch}")
|
||||
super().__init__(target)
|
||||
from tinygrad.runtime.support.compiler_cpu import X86Compiler
|
||||
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)
|
||||
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:
|
||||
def spill(self, spill_slot:int, x:UOp) -> UOp:
|
||||
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)
|
||||
disp = UOp.cconst(spill_slot, dtypes.int32)
|
||||
return UOp(Ops.INS, src=fold_address(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:
|
||||
def fill(self, spill_slot:int, x:UOp, reg:Register) -> UOp:
|
||||
is_xmm = reg.cons[0].size == 16
|
||||
dt = dtypes.uint64 if x.op is Ops.BUFFER else x.dtype
|
||||
return UOp(Ops.INS, src=fold_address(self.stack_pointer().index(disp)), arg=(X86Ops.VMOVUPS if is_xmm else X86Ops.MOV, dt), tag=(reg,))
|
||||
disp = UOp.cconst(spill_slot, dtypes.int32)
|
||||
return UOp(Ops.INS, src=fold_address(stack_pointer.index(disp)), arg=(X86Ops.VMOVUPS if is_xmm else X86Ops.MOV, dt), tag=(reg,))
|
||||
|
||||
def asm_str(self, uops:list[UOp], function_name:str) -> str:
|
||||
def _format_op(x:UOp) -> str: return f" {(o[7:-1] if (o:=str(x.arg[0]))[-1] in ('i', 'm') else o[7:]).lower():7s}"
|
||||
def _format_operands(x:UOp) -> str:
|
||||
def _format(src:tuple[UOp, ...]) -> list[str]:
|
||||
return [str(s.src[0].val) if s.op is Ops.CAST else reg_strs[o].get(s.dtype.itemsize, o) if \
|
||||
(o:=str(greg(s))) in reg_strs else o for s in src if greg(s) is not None]
|
||||
(o:=str(rdef(s))) in reg_strs else o for s in src if rdef(s) is not None]
|
||||
def _mem_adress(base:UOp, idx:UOp, disp:UOp, sz:UOp) -> list[str]:
|
||||
return [f"[{greg(base)}" + (f" + {greg(idx)}*{sz.src[0].val}" if greg(idx) else "") + (f" + {d}" if (d:=disp.src[0].val) else "") + "]"]
|
||||
return [f"[{rdef(base)}" + (f" + {rdef(idx)}*{sz.src[0].val}" if rdef(idx) else "") + (f" + {d}" if (d:=disp.src[0].val) else "") + "]"]
|
||||
|
||||
if len(x.src) > 4 and x.arg[0] in X86GroupOp.WriteMem: ret = _mem_adress(*x.src[:4]) + _format(x.src[4:])
|
||||
elif len(x.src) > 3 and x.arg[0] in X86GroupOp.Rm1st: ret = _format((x,)) + _mem_adress(*x.src[:4]) + _format(x.src[4:])
|
||||
|
||||
@@ -7,7 +7,7 @@ from tinygrad.helpers import to_tuple, ContextVar, Context, panic, partition, pe
|
||||
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
|
||||
@@ -59,6 +59,10 @@ def to_name(*parts:str) -> str: return "_".join(parts).replace(":", "_").lower()
|
||||
def timeline(devs:tuple[str, ...]) -> UOp: return UOp.placeholder((2,), dtypes.uint64, 0, device=devs, volatile=True, tag="timeline")
|
||||
def timeline_value(devs:tuple[str, ...]) -> UOp: return timeline(devs).index(1).load()
|
||||
|
||||
def rt_addr(b:UOp, dev) -> UOp:
|
||||
base, off = unwrap_view(b)
|
||||
return patch(UOp.placeholder((1,), dtypes.uint64, device=base.device, tag="addr"), [(0, base.getaddr(dev))]).index(0).load() + off
|
||||
|
||||
def make_submit(*cmds, devs:str|tuple[str, ...], queue:str) -> UOp:
|
||||
fn = to_name("submit", (devs:=to_tuple(devs))[0].split(":")[0], queue.split(":")[0])
|
||||
return UOp.custom_function(fn, UOp(Ops.LINEAR, src=tuple(cmds), arg=(devs, queue)))
|
||||
@@ -227,9 +231,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:
|
||||
@@ -286,17 +294,24 @@ 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
|
||||
@@ -315,6 +330,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)
|
||||
|
||||
@@ -327,12 +343,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)
|
||||
@@ -368,9 +379,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"))})
|
||||
@@ -432,7 +443,8 @@ def bufferize_buf(ctx:LinkCtx, b:UOp) -> UOp|None: # ctx: a kept link (the jit's
|
||||
# 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)
|
||||
spec = BufferSpec(host=b.arg.volatile, uncached=b.arg.volatile, cpu_access=True)
|
||||
r = Buffer(dev.device, b.max_numel(), b.dtype, options=spec, 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)
|
||||
@@ -511,8 +523,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:
|
||||
|
||||
@@ -80,7 +80,7 @@ def create_schedule(sched_sink:UOp) -> UOp:
|
||||
|
||||
from tinygrad.schedule.memory import memory_plan_rewrite
|
||||
from tinygrad.engine.realize import capturing, pm_flatten_linear
|
||||
from tinygrad.schedule.prepare import prepare_rangeify, prepare_call_views
|
||||
from tinygrad.schedule.prepare import prepare_rangeify
|
||||
from tinygrad.schedule.rangeify import get_kernel_graph
|
||||
from tinygrad.helpers import CAPTURING
|
||||
from tinygrad.uop.ops import PatternMatcher, UPat, ParamArg
|
||||
@@ -122,8 +122,6 @@ def lower_sink_to_linear(call:UOp) -> UOp|None:
|
||||
if function.op is not Ops.SINK or isinstance(function.arg, KernelInfo): return None
|
||||
# value calls (with unbound outputs) are inlined positionally during prepare: their bodies are not programs to schedule
|
||||
if call.has_unbound_outputs: return None
|
||||
call = prepare_call_views(call)
|
||||
function = call.src[0]
|
||||
st = time.perf_counter()
|
||||
cache_key = function.key
|
||||
if not SCACHE or (sc_ret:=schedule_cache.get(cache_key, None)) is None:
|
||||
|
||||
@@ -8,109 +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
|
||||
|
||||
def on_disk(u:UOp): return isinstance(u.device, str) and u.device.startswith("DISK")
|
||||
|
||||
def contiguous_mops_to_view(ctx:list[UOp]|None, c:UOp, src:UOp):
|
||||
"""MOPS(BUFFER) → SHRINK when movement ops collapse to a contiguous range."""
|
||||
# A list holds CALL arguments; None rewrites views in the live Tensor graph.
|
||||
# Ordinary copies keep their source graph so JIT can substitute its input buffer.
|
||||
if ctx is None and c.op is Ops.COPY and not on_disk(src): return None
|
||||
buf = src.base
|
||||
while buf.op is Ops.BITCAST: buf = buf.src[0].base
|
||||
# no symbolic shape
|
||||
if buf.op not in {Ops.BUFFER, Ops.PARAM, Ops.UNSHARD} or not all_int(c.shape): return None
|
||||
|
||||
# for UNSHARD tensors, use multi_pm to resolve per-shard movement ops, then view the resolved shard
|
||||
unshard = None
|
||||
if buf.op is Ops.UNSHARD:
|
||||
if isinstance(c.device, str): return None
|
||||
if (unshard := graph_rewrite(src, multi_pm, name="multi_buffer_view")).op is not Ops.UNSHARD: return None
|
||||
src = unshard.src[0]
|
||||
|
||||
# offset the base buffer by the collapsed movement ops and view it
|
||||
if (cv := src.contiguous_view()) is None or (buf := cv[0]).op not in {Ops.BUFFER, Ops.PARAM}: return None
|
||||
view = buf[cv[1]:cv[1] + src.max_numel() * src.element_size() // buf.element_size()].bitcast(src.dtype)
|
||||
if ctx is not None and view.op in {Ops.SHRINK, Ops.BITCAST}:
|
||||
arg = view.substitute({u: ctx[u.arg.slot] for u in view.toposort() if u.op is Ops.PARAM and u.arg.slot >= 0})
|
||||
if arg not in ctx: ctx.append(arg)
|
||||
view = view.param_like(ctx.index(arg))
|
||||
elif on_disk(buf) and buf.op is Ops.BUFFER and not buf.is_unbound: view = UOp.from_buffer(view.buffer, device=buf.device)
|
||||
view = view.reshape(src.shape).unshard(unshard.arg, unshard.src[1:]) if unshard is not None else view.reshape(c.shape)
|
||||
return c.replace(src=(view,)+c.src[1:]) if c.op in {Ops.COPY, Ops.STORE} else view
|
||||
|
||||
# Fold contiguous movement operations into buffer views.
|
||||
pm_mops_to_view = PatternMatcher([
|
||||
(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),
|
||||
|
||||
# remove contiguous on movement ops before a copy on disk
|
||||
(UPat(GroupOp.Movement-{Ops.SHRINK, Ops.RESHAPE}, name="x").f(Ops.CONTIGUOUS).f(Ops.COPY, name="copy"), lambda x,copy:
|
||||
copy.replace(src=(x,), tag=None) if on_disk(x) else None),
|
||||
# push copy past movement ops on disk
|
||||
(UPat(GroupOp.Movement-{Ops.SHRINK, Ops.RESHAPE}, name="x").f(Ops.COPY, name="copy"), lambda x,copy:
|
||||
x.replace(src=(copy.replace(src=(x.src[0],), tag=None),)+x.src[1:]) if on_disk(x) else None),
|
||||
])
|
||||
|
||||
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()))
|
||||
|
||||
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),
|
||||
])
|
||||
|
||||
def buffer_view_subs(sink:UOp) -> dict[UOp, UOp]:
|
||||
# Include intermediate nodes so every Tensor sharing a pending write receives the same view rewrite.
|
||||
nodes = list(sink.toposort(enter_calls=False))
|
||||
rewritten = graph_rewrite(UOp.sink(*nodes), pm_mops_to_view, bottom_up=True, name="fold buffer views")
|
||||
return {u: v for u, v in zip(nodes, rewritten.src) if u is not v}
|
||||
|
||||
def prepare_call_views(call:UOp) -> UOp:
|
||||
# Lift contiguous views into call arguments, preserving their buffer/offset graph for JIT input substitution.
|
||||
args = list(call.src[1:])
|
||||
body = graph_rewrite(call.src[0], pm_mops_to_view, ctx=args, bottom_up=True, name="prepare call views")
|
||||
return call.replace(src=(body, *args))
|
||||
|
||||
def prepare_to_call(sink:UOp, tensor_roots:tuple[UOp, ...]) -> UOp:
|
||||
# A copy used only to initialize another buffer can write directly into that destination.
|
||||
# Include live Tensor graphs so retained copies and aliases keep their independent storage.
|
||||
users:dict[UOp, set[UOp]] = {}
|
||||
for u in UOp.sink(sink, *tensor_roots).toposort(enter_calls=False):
|
||||
for src in u.src: users.setdefault(src, set()).add(u)
|
||||
subs = {}
|
||||
for store in sink.toposort(enter_calls=False):
|
||||
if store.op is not Ops.STORE: continue
|
||||
value = store.src[1]
|
||||
if value.op is not Ops.AFTER or len(value.src) != 2: continue
|
||||
buf, init = value.src
|
||||
if init.op is not Ops.STORE or len(init.src) != 2 or init.src[0] is not buf or init.src[1].op is not Ops.COPY: continue
|
||||
# Only this assignment may consume the copy, and only the initialization may use its storage.
|
||||
if users.get(value) != {store} or users.get(buf) != {value, init}: continue
|
||||
while buf.op is Ops.RESHAPE and users.get(buf.src[0]) == {buf}: buf = buf.src[0]
|
||||
if buf.op is not Ops.BUFFER or buf.is_unbound or buf.buffer.is_allocated(): continue
|
||||
subs[value] = init.src[1]
|
||||
sink = sink.substitute(subs, walk=True)
|
||||
sink = graph_rewrite(sink, pm_resolve_call_outputs, bottom_up=True, name="resolve call outputs")
|
||||
return UOp.sink(*[u for u in sink.toposort(enter_calls=False)
|
||||
if u.op is Ops.AFTER and not u.is_bound_var and not u.src[0].unsharded_base.is_unbound])
|
||||
|
||||
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:])
|
||||
@@ -127,8 +24,6 @@ def found_after(ctx:dict[UOp, UOp], after:UOp, src:UOp):
|
||||
ctx[x] = after
|
||||
|
||||
# *** fold moved AFTERs (hack for openpilot) ***
|
||||
# These temporary stores exist only in the schedule; they do not persist Tensor intermediates.
|
||||
pm_contiguous_to_store = PatternMatcher([(UPat(Ops.CONTIGUOUS, name="c"), lambda c: c.clone())])
|
||||
pm_fold_moved_after = PatternMatcher([
|
||||
(UPat(Ops.AFTER, src=(UPat(), UPat(Ops.STORE, src=(UPat(), UPat((*GroupOp.Movement,Ops.CAST,Ops.WHERE), name="src")))), name="after"), found_after),
|
||||
# replace ALU sources with AFTER versions found above
|
||||
@@ -234,10 +129,13 @@ def expand_bitcast(bc:UOp) -> UOp|None:
|
||||
parts = [tmp>>8*i*ns for i in range(os//ns)]
|
||||
return parts[0].stack(*parts[1:], dim=-1).flatten(-2).cast(new_uint).bitcast(bc.dtype)
|
||||
|
||||
earliest_rewrites = mop_cleanup+pm_resolve_call_outputs+PatternMatcher([
|
||||
# Inline calls with unbound outputs.
|
||||
earliest_rewrites = mop_cleanup+PatternMatcher([
|
||||
# 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),
|
||||
|
||||
# resolve AFTER on RETURNED (call outputs)
|
||||
(UPat(Ops.AFTER, src=(UPat(name="r"), UPat(Ops.SINK, name="t")), allow_any_len=True), resolve_returned_after),
|
||||
|
||||
# resolve allreduce (must be bottom up)
|
||||
(UPat(Ops.ALLREDUCE, src=(UPat.var("buf"),), name="red"), create_allreduce_function),
|
||||
|
||||
@@ -317,9 +215,7 @@ pm_copy_to_store = PatternMatcher([
|
||||
def prepare_rangeify(sink:UOp) -> UOp:
|
||||
# prepare for rangeify
|
||||
tsink = graph_rewrite(sink, multi_pm, name="multi_pm")
|
||||
if OPENPILOT_HACKS:
|
||||
tsink = graph_rewrite(tsink, pm_contiguous_to_store, bottom_up=True, name="materialize contiguous")
|
||||
tsink = graph_rewrite(tsink, pm_fold_moved_after, ctx={}, name="fold moved afters")
|
||||
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")
|
||||
return tsink
|
||||
|
||||
+240
-101
@@ -1,55 +1,246 @@
|
||||
# inspired by https://github.com/karpathy/micrograd/blob/master/micrograd/engine.py
|
||||
from __future__ import annotations
|
||||
import time, functools, sys, inspect, pathlib, hashlib, weakref
|
||||
from dataclasses import replace
|
||||
from dataclasses import dataclass, field, replace
|
||||
from typing import Any, Callable, cast, get_args, ParamSpec, TypeGuard, TypeVar, Generic, TYPE_CHECKING
|
||||
if TYPE_CHECKING: import numpy
|
||||
from tinygrad.dtype import DType, DTypeLike, dtypes, ConstType, least_upper_dtype, to_dtype, _from_np_dtype, _to_np_dtype, PyConst, AddrSpace
|
||||
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 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
|
||||
from tinygrad.schedule.prepare import buffer_view_subs, prepare_to_call, on_disk
|
||||
from tinygrad.schedule.multi import multi_pm
|
||||
from tinygrad.device import Buffer, canonicalize_device
|
||||
from tinygrad.engine.realize import run_linear
|
||||
|
||||
# *** callify: transform a tensor graph into a CALL UOp such that all state is properly scoped ***
|
||||
|
||||
@rewrite_group(lambda _,ret: f"Callify {pluralize('Buffer', len(ret.src)-1)}")
|
||||
def transform_to_call(big_sink:UOp) -> UOp:
|
||||
if VIZ: graph_rewrite(big_sink, PatternMatcher([]), name="View Tensor Graph")
|
||||
if SPEC: type_verify(big_sink, spec_tensor)
|
||||
# Storage declarations have unique global IDs; canonicalize them, including declarations inside nested calls.
|
||||
unbound = [u for u in big_sink.toposort() if u.is_unbound]
|
||||
body = big_sink.substitute({u: u.replace(arg=replace(u.arg, slot=i)) for i,u in enumerate(unbound)},
|
||||
enter_calls=True, walk=True, name="renumber buffers")
|
||||
# PARAMs belong to the enclosing scope. Nested call bodies keep their own positional PARAMs.
|
||||
inputs = [u for u in body.toposort(enter_calls=False)
|
||||
if (u.op is Ops.PARAM and (u.addrspace is not AddrSpace.ALU or u.arg.slot >= 0)) or u.is_bound_var or
|
||||
(u.op is Ops.BUFFER and u.addrspace is AddrSpace.GLOBAL and not u.is_unbound)]
|
||||
params = {u: u.replace(arg=replace(u.arg, slot=i, name=f"p{i}" if u.addrspace is AddrSpace.ALU else u.arg.name))
|
||||
if u.op is Ops.PARAM else u.param_like(i) for i,u in enumerate(inputs)}
|
||||
ret = body.substitute(params, walk=True, name="replace inputs").call(*inputs)
|
||||
if VIZ: graph_rewrite(ret, PatternMatcher([]), name="View Call")
|
||||
return ret
|
||||
@dataclass
|
||||
class AllocCtx:
|
||||
buffer_map: dict[UOp, UOp] = field(default_factory=dict)
|
||||
bases: set[UOp] = field(default_factory=set)
|
||||
stores: list[UOp] = field(default_factory=list)
|
||||
replacements: list[UOp] = field(default_factory=list)
|
||||
unbound: dict[UOp, UOp] = field(default_factory=dict)
|
||||
views: set[UOp] = field(default_factory=set)
|
||||
|
||||
# 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 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"), 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),
|
||||
(UPat((Ops.CONTIGUOUS, Ops.AFTER), name="x"), tag_uop),
|
||||
(UPat(GroupOp.All, name="x"), lambda ctx,x: tag_uop(x) if x in ctx.bases else None),
|
||||
])
|
||||
|
||||
def replace_contig_with_store_after(u:UOp):
|
||||
# can't allocate a buffer for a virtual value
|
||||
if u.is_virtual: return None
|
||||
# if size is 0, remove the contig
|
||||
if 0 in u.shape: return u.src[0]
|
||||
# no real contig for DISK tensors, they are left alone
|
||||
if on_disk(u): return u.rtag(None)
|
||||
buf = u.empty_like()
|
||||
return buf.after(buf.store(u.src[0])).rtag(u.tag)
|
||||
|
||||
def wrap_tagged_in_contig(x:UOp):
|
||||
if x.tag is None: return None # untouched
|
||||
# empty tag from rtag(()): a COPY already handled via buffer_map or merged into a parent AFTER.
|
||||
# () is falsy but not None, so it isn't re-tagged like a bare (tag=None) node would be; just strip it here
|
||||
if not x.tag: return x.rtag(None)
|
||||
return x.rtag(None).contiguous(tag=x.tag) # the tag moves onto the wrapping CONTIGUOUS
|
||||
|
||||
def contiguous_mops_to_view(ctx:AllocCtx, c:UOp, src:UOp):
|
||||
"""MOPS(BUFFER) → SHRINK when movement ops collapse to a contiguous range."""
|
||||
buf = src.base
|
||||
while buf.op is Ops.BITCAST: buf = buf.src[0].base
|
||||
# no symbolic shape
|
||||
if buf.op not in {Ops.BUFFER, Ops.UNSHARD} or not all_int(c.shape): return None
|
||||
|
||||
# for UNSHARD tensors, use multi_pm to resolve per-shard movement ops, then view the resolved shard
|
||||
unshard = None
|
||||
if buf.op is Ops.UNSHARD:
|
||||
if isinstance(c.device, str): return None
|
||||
if (unshard := graph_rewrite(src, multi_pm, name="multi_buffer_view")).op is not Ops.UNSHARD: return None
|
||||
src = unshard.src[0]
|
||||
|
||||
# offset the base buffer by the collapsed movement ops and view it
|
||||
if (cv := src.contiguous_view()) is None or (buf := cv[0]).op is not Ops.BUFFER: return None
|
||||
# NB: make offset a UOp.variable here to do the offset computation in the kernels
|
||||
view = buf[cv[1]:cv[1] + src.max_numel() * src.element_size() // buf.element_size()].bitcast(src.dtype)
|
||||
ctx.views.add(view)
|
||||
if unshard is not None: return view.reshape(src.shape).unshard(unshard.arg, unshard.src[1:])
|
||||
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"
|
||||
# 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, a if a.has_buffer_identity(after_ok=True) else a.contiguous())
|
||||
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)])
|
||||
|
||||
# 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),
|
||||
|
||||
# remove contiguous on movement ops before a copy on disk
|
||||
(UPat(GroupOp.Movement-{Ops.SHRINK, Ops.RESHAPE}, name="x").f(Ops.CONTIGUOUS).f(Ops.COPY, name="copy"), lambda x,copy:
|
||||
copy.replace(src=(x,), tag=None) if on_disk(x) else None),
|
||||
# push copy past movement ops to disk
|
||||
(UPat(GroupOp.Movement-{Ops.SHRINK, Ops.RESHAPE}, name="x").f(Ops.COPY, name="copy"), lambda x,copy:
|
||||
x.replace(src=(copy.replace(src=(x.src[0],), tag=None),)+x.src[1:]) if on_disk(x) else None),
|
||||
|
||||
# add CONTIGUOUS to tagged UOps
|
||||
(UPat(GroupOp.All-{Ops.CONTIGUOUS, Ops.AFTER, Ops.STORE}, name="x"), wrap_tagged_in_contig),
|
||||
# remove extra CONTIGUOUS on AFTER (only when target is contiguous)
|
||||
(UPat(Ops.CONTIGUOUS, src=(UPat(Ops.AFTER, name="a"),), name="c"),
|
||||
lambda a,c: a.replace(tag=(a.tag or ())+(c.tag or ())) if a.src[0].has_buffer_identity() else None),
|
||||
# replace CONTIGUOUS with STORE+AFTER
|
||||
(UPat(Ops.CONTIGUOUS, name="u"), replace_contig_with_store_after),
|
||||
# remove DETACH/CONTIGUOUS_BACKWARD (allows more contiguous removal)
|
||||
(UPat((Ops.DETACH, Ops.CONTIGUOUS_BACKWARD), name="x"), lambda x: x.src[0]),
|
||||
])
|
||||
|
||||
# a store's storage keeps the views and drops AFTERs (they only sequence stores)
|
||||
pm_drop_after = PatternMatcher([(UPat(Ops.AFTER, name="a"), lambda a: a.src[0])])
|
||||
|
||||
def replace_input_buffer(ctx:AllocCtx, b:UOp):
|
||||
ctx.replacements.append(b)
|
||||
return b.param_like(len(ctx.replacements)-1)
|
||||
|
||||
# unbound BUFFERs get canonical scope-local id slots here so structurally identical calls hash identically for the
|
||||
# schedule cache (fresh slots are all positive from the global counter; negative slots are already canonical)
|
||||
def canonicalize_unbound_buffer(ctx:AllocCtx, b:UOp):
|
||||
if b.arg.slot >= 0 and b not in ctx.unbound: ctx.unbound[b] = b.replace(arg=replace(b.arg, slot=-1-len(ctx.unbound)))
|
||||
return ctx.unbound.get(b)
|
||||
|
||||
def canonicalize_call_body(ctx:AllocCtx, c:UOp):
|
||||
body = graph_rewrite(c.src[0], pm_canonicalize_unbound, ctx=ctx, bottom_up=True)
|
||||
return c.replace(src=(body,)+c.src[1:]) if body is not c.src[0] else None
|
||||
|
||||
pm_canonicalize_unbound = PatternMatcher([
|
||||
(UPat(Ops.CALL, name="c"), canonicalize_call_body),
|
||||
(UPat(Ops.BUFFER, src=(), name="b"), lambda ctx,b: canonicalize_unbound_buffer(ctx, b) if b.is_unbound else None),
|
||||
])
|
||||
|
||||
pm_replace_buf = pm_canonicalize_unbound+PatternMatcher([
|
||||
# replace BUFFER with PARAM for cache key normalization (ALU addrspace buffers are Variables, they stay, and unbound BUFFERs too)
|
||||
(UPat(Ops.BUFFER, src=(), name="b"), lambda ctx,b:
|
||||
replace_input_buffer(ctx, b) if b.addrspace is AddrSpace.GLOBAL and not b.is_unbound else None),
|
||||
# replace buffer views (SHRINK/BITCAST) with PARAM (only the views created by contiguous_mops_to_view)
|
||||
(UPat((Ops.SHRINK, Ops.BITCAST), name="b"), lambda ctx,b: replace_input_buffer(ctx, b) if b in ctx.views else None),
|
||||
# strip the stored value from bound Variables for cache key normalization, so different values hit same cache
|
||||
(UPat(Ops.AFTER, name="b"), lambda ctx,b: replace_input_buffer(ctx, b) if b.is_bound_var else None),
|
||||
])
|
||||
|
||||
@rewrite_group(lambda _,ret: f"Callify {pluralize('Buffer', len(ret[1]))}")
|
||||
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. 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
|
||||
srcs:list[UOp] = []
|
||||
for u in big_sink.src:
|
||||
if u.op is Ops.AFTER and u.src[0].unsharded_base.is_unbound:
|
||||
# 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))
|
||||
|
||||
# here we can break the tensor graph. tags propagate through replaces so we can still find the original UOps
|
||||
big_sink = graph_rewrite(big_sink, pm_early_transform_tensor_graph, ctx=ctx, name="early transform tensor graph")
|
||||
|
||||
# collect the stores (never entering call bodies) and map tagged AFTERs to their storage; tags are stripped at the end
|
||||
# copies to disk are stores to the disk buffer; bound Variables are call inputs and RETURNEDs are call outputs
|
||||
for u in big_sink.toposort(enter_calls=False):
|
||||
if (u.op is Ops.COPY and on_disk(u)) or (u.op is Ops.AFTER and not u.is_bound_var and not u.src[0].unsharded_base.is_unbound):
|
||||
ctx.stores.append(u)
|
||||
if u.tag: ctx.buffer_map.update({t:graph_rewrite(u.src[0], pm_drop_after).shrink_to(t.shape) for t in u.tag})
|
||||
ret = graph_rewrite(UOp.sink(*ctx.stores), pm_replace_buf+remove_all_tags, ctx=ctx, bottom_up=True, name="replace bufs").call(*ctx.replacements)
|
||||
assert not any(x in ctx.buffer_map for x in ctx.buffer_map.values())
|
||||
if VIZ: graph_rewrite(ret, PatternMatcher([]), name="View Call")
|
||||
return ret, ctx.buffer_map
|
||||
|
||||
# *** all in scope Tensors are here. this gets relevant UOps ***
|
||||
|
||||
all_tensors: dict[weakref.ref[Tensor], None] = {}
|
||||
def _apply_map_to_tensors(applied_map:dict[UOp, UOp], name:str, *, tensors:list[Tensor]|None=None) -> None:
|
||||
def _apply_map_to_tensors(applied_map:dict[UOp, UOp], name:str) -> None:
|
||||
with cpu_profile(TracingKey(name), "TINY"):
|
||||
# get tensors in scope
|
||||
in_scope: dict[UOp, bool] = {}
|
||||
def visitor(node: UOp) -> bool: return True if node in applied_map else any(in_scope.get(s, False) for s in node.src)
|
||||
if tensors is None: tensors = [t for tref in list(all_tensors) if (t:=tref()) is not None]
|
||||
scope_tensors = [t for t in tensors if t.uop.topovisit(visitor, in_scope)]
|
||||
scope_tensors: list[Tensor] = [t for tref in list(all_tensors) if (t:=tref()) is not None and t.uop.topovisit(visitor, in_scope)]
|
||||
|
||||
# get all Tensors and apply the map. always walk: replace exactly the nodes the map names, values are final
|
||||
sink = UOp.sink(*[t.uop for t in scope_tensors])
|
||||
@@ -60,14 +251,9 @@ def _apply_map_to_tensors(applied_map:dict[UOp, UOp], name:str, *, tensors:list[
|
||||
if s is ns: continue
|
||||
t.uop = ns
|
||||
|
||||
# **** Tensor helper functions ****
|
||||
def _tensor_holds(u:UOp) -> bool: return any((t:=tref()) is not None and t.uop is u for tref in list(all_tensors))
|
||||
|
||||
def _inplace_rhs(update:UOp) -> UOp|None:
|
||||
# Recover the computed value of a read-modify-write; ordinary clone stores are not self-referential.
|
||||
if update.op is not Ops.AFTER or len(update.src) != 2: return None
|
||||
store = update.src[1]
|
||||
if store.op is not Ops.STORE or store.src[0] not in store.src[1].toposort(enter_calls=False): return None
|
||||
return store.src[1]
|
||||
# **** Tensor helper functions ****
|
||||
|
||||
def is_numpy_ndarray(x) -> "TypeGuard[numpy.ndarray]": return str(type(x)) == "<class 'numpy.ndarray'>"
|
||||
|
||||
@@ -127,9 +313,7 @@ class Tensor(RandMixin):
|
||||
if not isinstance(data, UOp): raise RuntimeError(f"can't create Tensor from {data!r} with type {type(data)}")
|
||||
|
||||
# data might be on a different device
|
||||
self.uop:UOp = data
|
||||
if data.device is not None and data.device != _device:
|
||||
self.uop = data.clone(_device) if is_creation_device(data) else data.copy_to_device(_device)
|
||||
self.uop:UOp = data if data.device is None or data.device == _device else data.copy_to_device(_device)
|
||||
# cast on the target device, the source may not hold the dtype (numpy has no fp8/bfloat16) or be able to compute it (DISK)
|
||||
if _dtype is not None: self.uop = self.uop.cast(_dtype)
|
||||
|
||||
@@ -202,33 +386,10 @@ class Tensor(RandMixin):
|
||||
"""
|
||||
return [Tensor(u) for u in UOp.custom_kernel(*[t.uop for t in (self,)+lst], fxn=fxn, grad_fxn=grad_fxn)]
|
||||
|
||||
def _prepare_call(self, *lst:Tensor) -> tuple[UOp, dict[UOp, UOp]]:
|
||||
outs = (self,)+lst
|
||||
_apply_map_to_tensors(buffer_view_subs(UOp.sink(*[x.uop for x in outs])), name="fold buffer views")
|
||||
# Only requested outputs acquire storage. Intermediate values persist only when explicitly cloned.
|
||||
bases = set()
|
||||
for x in outs:
|
||||
base = x.uop.base
|
||||
while base.op is Ops.CONTIGUOUS_BACKWARD: base = base.src[0].base
|
||||
bases.add(base)
|
||||
subs:dict[UOp, UOp] = {}
|
||||
for u in UOp.sink(*bases).toposort(enter_calls=False):
|
||||
if u not in bases or u.is_virtual or on_disk(u): continue
|
||||
if u.has_buffer_identity(after_ok=True) or u.storage_base.has_buffer_identity(): continue
|
||||
if u.op is Ops.AFTER and u.src[1].op is Ops.CALL and u.src[1].arg.precompile: continue
|
||||
subs[u] = u.substitute(subs, walk=True).clone()
|
||||
_apply_map_to_tensors(subs, name="materialize")
|
||||
sink = UOp.sink(*[x.uop for x in outs])
|
||||
becomes_map = {u: graph_rewrite(u.src[0], pm_drop_after).shrink_to(u.shape)
|
||||
for u in sink.toposort(enter_calls=False)
|
||||
if u.op is Ops.AFTER and not u.is_bound_var and not u.src[0].unsharded_base.is_unbound}
|
||||
tensor_roots = tuple(t.uop for ref in list(all_tensors) if (t:=ref()) is not None)
|
||||
return transform_to_call(prepare_to_call(sink, tensor_roots)), becomes_map
|
||||
|
||||
def callify(self, *lst:Tensor) -> Tensor:
|
||||
"""Groups the computation for these tensors into a deferred call. Returns `self` without executing the call."""
|
||||
call, becomes_map = self._prepare_call(*lst)
|
||||
_apply_map_to_tensors({x:y.after(call) for x,y in becomes_map.items()}, name="callify")
|
||||
big_sink = UOp.sink(*[x.uop for x in (self,)+lst])
|
||||
big_sink, buffer_map = transform_to_call(big_sink)
|
||||
_apply_map_to_tensors({x:y.after(big_sink) for x,y in buffer_map.items()}, name="callify")
|
||||
return self
|
||||
|
||||
def linear_with_vars(self, *lst:Tensor) -> tuple[UOp, dict[str, int]]:
|
||||
@@ -236,9 +397,9 @@ class Tensor(RandMixin):
|
||||
# weakness ends where storage begins
|
||||
if any(t.dtype in dtypes.weaks and t.uop.device is not None for t in (self,)+lst):
|
||||
raise RuntimeError("cannot realize a weak dtype; cast to a concrete dtype first")
|
||||
call, becomes_map = self._prepare_call(*lst)
|
||||
big_sink, becomes_map = transform_to_call(UOp.sink(*[x.uop for x in (self,)+lst]))
|
||||
_apply_map_to_tensors(becomes_map, name="buffers")
|
||||
return create_linear_with_vars(call)
|
||||
return create_linear_with_vars(big_sink)
|
||||
|
||||
def schedule_linear(self, *lst:Tensor) -> UOp:
|
||||
"""Creates the schedule needed to realize these Tensor(s)."""
|
||||
@@ -249,7 +410,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 (b:=x.uop.base).is_virtual and not b.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
|
||||
@@ -264,12 +425,6 @@ class Tensor(RandMixin):
|
||||
return self
|
||||
|
||||
def assign(self, x:Tensor|PyConst|list|tuple) -> Tensor:
|
||||
"""
|
||||
Assigns `x` to this tensor and returns `self`. `x` must broadcast to this tensor's shape.
|
||||
Tensor inputs must match its dtype and device, except that disk tensors accept inputs from any device.
|
||||
Updates existing storage, or creates storage if this tensor is a computed value.
|
||||
The write is deferred until realization, except for disk tensors.
|
||||
"""
|
||||
if self.dtype in dtypes.weaks: self.uop = self.uop.clone()
|
||||
is_disk = on_disk(self.uop)
|
||||
if not isinstance(x, Tensor): x = Tensor(x, device="CPU" if is_disk else self.device, dtype=self.dtype)
|
||||
@@ -287,26 +442,21 @@ class Tensor(RandMixin):
|
||||
if is_disk:
|
||||
(b:=self._buffer()).copy_from(Buffer("PYTHON", b.size, b.dtype, opaque=x._data()))
|
||||
return self
|
||||
# Assigning to a value initializes new storage; assigning to a buffer updates its storage.
|
||||
if not self.uop.storage_base.has_buffer_identity():
|
||||
self.uop = x.uop.clone()
|
||||
assigned_to = self.uop.storage_base
|
||||
# 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.src[0] if x.uop.op is Ops.CONTIGUOUS else x.uop).clone()
|
||||
return self
|
||||
update = self.uop.after(self.uop.store(x.uop))
|
||||
base = self.uop
|
||||
# Direct assignments need no alias search. A held reshape of a buffer also owns its update.
|
||||
if not base.has_buffer_identity() and base.op in GroupOp.Movement|{Ops.BITCAST, Ops.DETACH}:
|
||||
tensors = [t for ref in list(all_tensors) if (t:=ref()) is not None]
|
||||
held = {t.uop for t in tensors}
|
||||
# Find the owning Tensor's buffer or pending write, preserving its shape for function argument substitution.
|
||||
while base.op in GroupOp.Movement|{Ops.BITCAST, Ops.DETACH}:
|
||||
if base.has_buffer_identity() and base in held: break
|
||||
base = base.src[0]
|
||||
if base.has_buffer_identity(after_ok=True):
|
||||
# Detach shares storage, but an assignment through it must not rewrite earlier computations using that storage.
|
||||
if self.uop.op is Ops.DETACH: tensors = [t for t in tensors if t.uop.storage_base is base.storage_base]
|
||||
_apply_map_to_tensors({base: base.after(update)}, name="Embed View Assign", tensors=tensors)
|
||||
return self
|
||||
self.uop = update
|
||||
# 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:
|
||||
# view assign: replace the node under the views (e.g. RESHAPE(BUFFER)) so @function's substitution catches it
|
||||
_apply_map_to_tensors({ib: ib.after(assign)}, name="Embed View Assign")
|
||||
else:
|
||||
# simple assign
|
||||
self.uop = assign
|
||||
return self
|
||||
|
||||
def _buffer(self) -> Buffer:
|
||||
@@ -379,8 +529,7 @@ class Tensor(RandMixin):
|
||||
|
||||
def clone(self, device:str|tuple[str, ...]|None=None) -> Tensor:
|
||||
"""
|
||||
Creates a tensor with independent storage, populated lazily when its value is needed.
|
||||
Use this to retain an intermediate result across realizations or to modify it independently.
|
||||
Creates a clone of this tensor allocating a separate buffer for the data.
|
||||
If `device` is specified, the clone is placed on that device.
|
||||
"""
|
||||
ret = Tensor(self.uop.clone(device=device))
|
||||
@@ -389,13 +538,12 @@ class Tensor(RandMixin):
|
||||
|
||||
def to(self, device:str|tuple[str, ...]|None) -> Tensor:
|
||||
"""
|
||||
Returns this tensor on the given device, transferring its data lazily. Returns `self` if the device already matches.
|
||||
Use `clone(device)` when the result needs independent, persistent storage.
|
||||
Moves the tensor to the given device.
|
||||
"""
|
||||
if self.uop.device is None: return self
|
||||
if (device:=canonicalize_device(device)) == self.device: return self
|
||||
# Copies from creation devices and copies to disk own persistent storage.
|
||||
if is_creation_device(self.uop) or (isinstance(device, str) and device.startswith("DISK")): ret = Tensor(self.uop.clone(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)
|
||||
@@ -538,20 +686,12 @@ 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=}")
|
||||
# Augmented view assignment may already have embedded its STORE in the parent. Undo that dependency
|
||||
# before the functional setitem below, while retaining the computed RHS for autograd.
|
||||
if isinstance(v, Tensor) and self.is_floating_point() and not self.uop._base_buffer_is_realized():
|
||||
a = self.uop
|
||||
if a.op is Ops.AFTER and len(a.src) == 2 and a.src[1] in v.uop.backward_slice and (view_rhs:=_inplace_rhs(a.src[1])) is not None:
|
||||
_apply_map_to_tensors({a: a.src[0]}, name="functional setitem")
|
||||
v = v._apply_uop(lambda _: view_rhs)
|
||||
# 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):
|
||||
shared = self.uop.base if self.uop.base.is_realized else None
|
||||
if any(self.uop in t.uop.backward_slice_with_self and t.uop.base is not shared for tref in all_tensors
|
||||
if (t:=tref()) is not None and t is not self and t.uop is not v_uop and t.uop not in v_bw):
|
||||
self._getitem(indices) # invalid indices take precedence over the mutation restriction
|
||||
raise RuntimeError("can't setitem on a tensor with other uses")
|
||||
idx = [indices] if (isinstance(indices, list) and all_int(indices)) or not isinstance(indices, (tuple, list)) else list(indices)
|
||||
is_disk = on_disk(self.uop)
|
||||
@@ -559,7 +699,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)
|
||||
if (rhs:=_inplace_rhs(v.uop)) is not None: v = v._apply_uop(lambda _, rhs=rhs: rhs)
|
||||
self.replace(self._getitem(indices, v))
|
||||
elif advanced: # advanced setitem
|
||||
if is_disk: raise RuntimeError("advanced setitem is not supported for DISK tensors")
|
||||
|
||||
+8
-10
@@ -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
|
||||
@@ -798,8 +800,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
|
||||
# *** uop Buffer stuff ***
|
||||
|
||||
# Fresh storage IDs decrease from -1; canonical slots are numbered from 0 within their scope.
|
||||
unique_num = itertools.count(-1, -1)
|
||||
unique_num = itertools.count(0)
|
||||
|
||||
def getaddr(self, device=None) -> UOp:
|
||||
if self.without_after.op not in {Ops.BUFFER, Ops.SHRINK, Ops.BITCAST, Ops.BINARY, Ops.MSTACK, Ops.MSELECT, Ops.PARAM, Ops.LINEAR}: return self
|
||||
@@ -817,11 +818,8 @@ 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=dt, device=device)
|
||||
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)
|
||||
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:
|
||||
@@ -835,13 +833,11 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
data = struct.pack(f"{prod(shape)}{bdtype.fmt}", *[truncate[bdtype](bdtype.const(xi)) for xi in fully_flatten(x)])
|
||||
ret.buffer.allocate(memoryview(bytearray(data))) # fake realize. buffer storage must be writable, and bytes isn't
|
||||
if ret.dtype != dtype: ret = ret.cast(dtype)
|
||||
return ret if ret.device == device else ret.clone(device)
|
||||
return ret if ret.device == device else ret.copy_to_device(device)
|
||||
def clone(self, device=None) -> UOp:
|
||||
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:
|
||||
@@ -1099,7 +1095,9 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
trunc = truncate.get(self.dtype) if dtypes.is_float(self.dtype) else math.trunc if dtypes.is_int(self.dtype) else None
|
||||
if trunc is not None and all(math.isfinite(v) for v in (smin, smax)): smin, smax = trunc(smin), trunc(smax)
|
||||
if dtypes.is_unsigned(self.dtype) and 0 <= smin and smax <= self.dtype.max: return smin, smax
|
||||
if self.dtype in dtypes.floats+dtypes.sints+(dtypes.weakint,): return max(self.dtype.min, smin), min(smax, self.dtype.max)
|
||||
# a signed or float destination holds the part of the source that overlaps it: overflow is undefined, a nan bound overlaps nothing
|
||||
if self.dtype in dtypes.floats+dtypes.sints+dtypes.weaks and smin <= self.dtype.max and self.dtype.min <= smax:
|
||||
return max(self.dtype.min, smin), min(smax, self.dtype.max)
|
||||
return self.dtype.min, self.dtype.max
|
||||
|
||||
@functools.cached_property
|
||||
|
||||
+5
-14
@@ -21,13 +21,6 @@ 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
|
||||
# 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 all is good and CHECK_OOB=1, validate with z3
|
||||
from tinygrad.uop.validate import validate_index_with_z3
|
||||
return validate_index_with_z3(sz, idx, gate)
|
||||
@@ -93,7 +86,7 @@ spec_shared = PatternMatcher([
|
||||
# GROUP of stores (or groups, or NOOPs)
|
||||
(UPat(Ops.GROUP, dtypes.void, src=UPat((Ops.GROUP, Ops.STORE, Ops.NOOP, Ops.INS, Ops.END))), lambda: True),
|
||||
|
||||
# AFTER preserves its target view.
|
||||
# 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})),),
|
||||
allow_any_len=True), lambda: True),
|
||||
@@ -124,9 +117,10 @@ spec_shared = PatternMatcher([
|
||||
(UPat((Ops.INDEX, Ops.SHRINK), name="uidx").or_casted().store(UPat()), validate_index),
|
||||
(UPat((Ops.INDEX, Ops.SHRINK), name="uidx").or_casted().store(UPat(), UPat.var("gate", dtype=dtypes.bool)), validate_index),
|
||||
|
||||
# STORE targets storage (or an AFTER/BITCAST/view of it). INDEX stores are checked above.
|
||||
# STORE: the target must be storage or a CONTIGUOUS realization point (or an AFTER/BITCAST/view of one);
|
||||
# CONTIGUOUS targets are written into the buffer the CONTIGUOUS creates. INDEX stores are checked above
|
||||
(UPat(Ops.STORE, dtypes.void, (UPat(name="x"), UPat())), lambda x:
|
||||
True if (b:=x.storage_base).op in {Ops.BUFFER, Ops.PARAM} else None if b.op is Ops.INDEX else False),
|
||||
True if (b:=x.storage_base).op in {Ops.BUFFER, Ops.PARAM, Ops.CONTIGUOUS} else None if b.op is Ops.INDEX else False),
|
||||
|
||||
# WMMA has a <a, b, acc>
|
||||
(UPat(Ops.WMMA, src=(UPat(), UPat(), UPat()), name="x"), lambda x: isinstance(x.arg, tuple) and len(x.arg) == 5),
|
||||
@@ -175,10 +169,7 @@ spec_tensor = PatternMatcher([
|
||||
(UPat(Ops.MSELECT, name="x"), lambda x: isinstance(x.src[0].device, tuple) and x.arg < len(x.src[0].device)),
|
||||
(UPat(Ops.MSTACK, name="x"), lambda x: all(isinstance(s.device, str) for s in x.src) or (all_same(x.src) and x.src[0].device is None)),
|
||||
|
||||
# Detached storage may carry pending writes in the Tensor graph.
|
||||
(UPat(Ops.AFTER, src=(UPat(Ops.DETACH, name="x"),), allow_any_len=True), lambda x: x.storage_base.op in {Ops.BUFFER, Ops.PARAM}),
|
||||
|
||||
# Layout and autograd markers preserve the source value.
|
||||
# CONTIGUOUS ensures the source UOp realizes
|
||||
(UPat((Ops.DETACH, Ops.CONTIGUOUS, Ops.CONTIGUOUS_BACKWARD), src=(UPat(),), arg=None), lambda: True),
|
||||
|
||||
# TODO: this should not be here. STAGE is transformed to BUFFER later
|
||||
|
||||
+26
-33
@@ -29,36 +29,34 @@ 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 = x.arg.name if x.op in {Ops.PARAM, Ops.BUFFER} else 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.vmin, x.vmax, 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.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.SPECIAL, Ops.RANGE), name="x"), lambda x,ctx: create_bounded(x.render(simplify=False), 0, ctx[1][x.src[0]]-1, ctx[0])),
|
||||
# unknown values are variables bounded by their vmin/vmax: params, loads (non-pointer INDEX is a LOAD) and anything from floats
|
||||
(UPat((Ops.PARAM, Ops.BUFFER, Ops.LOAD, Ops.INDEX), name="x"), create_var),
|
||||
(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]:
|
||||
@@ -67,13 +65,8 @@ def uops_to_z3(solver:z3.Solver, *uops: UOp) -> list[z3.ExprRef]:
|
||||
(x.dtype in dtypes.ints+(dtypes.bool, dtypes.weakint) or x.op is Ops.SINK)))[:-1]
|
||||
z3map: dict[UOp, 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]
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
# a weak CAST states a width, which the consumer restates. a weakint over a bool or float is a conversion, it commits here
|
||||
def absorb_weak_src(s:UOp) -> UOp:
|
||||
if s.op is not Ops.CAST or s.dtype not in dtypes.weaks: return s
|
||||
if s.dtype is dtypes.weakint and not dtypes.is_int(s.src[0].dtype): return s.src[0].cast(s.commit_dtype(dtypes.int))
|
||||
return s.src[0]
|
||||
|
||||
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(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
|
||||
|
||||
Vendored
+14
File diff suppressed because one or more lines are too long
@@ -12,3 +12,4 @@ fetch "cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/highlight.min.js"
|
||||
fetch "cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/languages/python.min.js"
|
||||
fetch "cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/languages/cpp.min.js"
|
||||
fetch "unpkg.com/@highlightjs/[email protected]/styles/tokyo-night-dark.min.css"
|
||||
fetch "cdn.jsdelivr.net/npm/[email protected]/dist/browser/markdown-it.umd.min.js"
|
||||
|
||||
@@ -479,10 +479,11 @@ def get_profile(data:VizData, profile:list[ProfileEvent], sort_fn:Callable[[str]
|
||||
scache:dict[str, int] = {}
|
||||
peaks:list[int] = []
|
||||
dtype_size:dict[str, int] = {}
|
||||
for k,v in dev_events.items():
|
||||
v.sort(key=lambda e:e[0])
|
||||
layout[k] = timeline_layout(data, v, start_ts, scache)
|
||||
layout.update([graph_layout(k, v, start_ts, unwrap(end_ts), peaks, dtype_size, scache)])
|
||||
with soft_err():
|
||||
for k,v in dev_events.items():
|
||||
v.sort(key=lambda e:e[0])
|
||||
layout[k] = timeline_layout(data, v, start_ts, scache)
|
||||
layout.update([graph_layout(k, v, start_ts, unwrap(end_ts), peaks, dtype_size, scache)])
|
||||
sorted_layout = sorted([k for k,v in layout.items() if v is not None], key=sort_fn)
|
||||
ret = [b"".join([struct.pack("<B", len(k)), k.encode(), unwrap(layout[k])]) for k in sorted_layout]
|
||||
index = json.dumps({"strings":list(scache), "dtypeSize":dtype_size,
|
||||
|
||||
Reference in New Issue
Block a user