mirror of
https://github.com/tinygrad/tinygrad.git
synced 2026-09-08 16:26:15 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8529a0af5 | ||
|
|
836d5cbcac | ||
|
|
de725c4721 | ||
|
|
65af5d3be9 |
@@ -97,7 +97,7 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
HCQ2: '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: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
HCQ2: '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: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
HCQ2: '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: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
HCQ2: '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: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
HCQ2: '0'
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
|
||||
+80
-419
@@ -1,17 +1,17 @@
|
||||
from __future__ import annotations
|
||||
from typing import cast
|
||||
import os, ctypes, struct, functools, importlib, mmap, errno, contextlib, sys, hashlib, itertools, collections, atexit
|
||||
import os, ctypes, struct, functools, importlib, mmap, errno, contextlib, sys, itertools, atexit
|
||||
assert sys.platform != 'win32'
|
||||
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 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 tinygrad.dtype import dtypes
|
||||
from tinygrad.helpers import getenv, round_up, data64_le, DEBUG, PROFILE, lo32, hi32, prod, colored
|
||||
from tinygrad.helpers import getenv, round_up, data64_le, DEBUG, PROFILE, lo32, hi32
|
||||
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, sqtt, amdgpu_kd, amdgpu_drm
|
||||
from tinygrad.runtime.autogen import kfd, hsa, 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,10 +19,9 @@ 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
|
||||
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
|
||||
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
|
||||
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
|
||||
@@ -37,14 +36,6 @@ 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)),
|
||||
@@ -58,8 +49,6 @@ 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)
|
||||
|
||||
@@ -72,20 +61,6 @@ 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)
|
||||
@@ -131,232 +106,16 @@ 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 = UOp(Ops.LINEAR, src=tuple(self.kernargs(call, prg, data)))
|
||||
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))
|
||||
|
||||
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)
|
||||
@@ -370,85 +129,41 @@ 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): # 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)
|
||||
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)
|
||||
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):
|
||||
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)
|
||||
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):
|
||||
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)
|
||||
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: # 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 submit(self, cmdbuf:UOp) -> UOp:
|
||||
q = 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)
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
# *****************
|
||||
# SDMA
|
||||
@@ -493,8 +208,6 @@ 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()
|
||||
@@ -509,24 +222,19 @@ 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:
|
||||
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
|
||||
entry_point_offset:int; rsrc1:int; rsrc2:int; rsrc3:int; wave32:bool
|
||||
private_segment_size:int; kernargs_segment_size:int; kernargs_alloc_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
|
||||
@@ -541,11 +249,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(desc_offset=rodata, entry_point_offset=rodata + desc.kernel_code_entry_byte_offset,
|
||||
data = AMDProgramData(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),
|
||||
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,
|
||||
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,
|
||||
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
|
||||
|
||||
@@ -787,8 +495,7 @@ 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')
|
||||
tl[0] = tl[1]
|
||||
(tl:=d.timeline._buf.cpu_view().view(fmt='Q'))[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))):
|
||||
@@ -834,7 +541,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(amd_compute_queue(ctx, submit))),
|
||||
(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_copy", name="submit"), lambda ctx, submit: encode_submit(AMDSDMAQueue(ctx, submit))),
|
||||
])
|
||||
|
||||
@@ -871,6 +578,10 @@ 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")
|
||||
@@ -879,32 +590,37 @@ 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())),
|
||||
(UPat(Ops.PARAM, tag="program", name="b"), lambda ctx, b: ctx.program_buffer(b)),
|
||||
]) + self.pm_bufferize
|
||||
self.pm_bufferize = PatternMatcher([(UPat(Ops.PARAM, tag="scratch", name="b"), lambda ctx, b: ctx.scratch_buffer(b.max_numel()))]) + 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
|
||||
|
||||
# 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.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
|
||||
self.pmc_enabled:bool = PROFILE > 0 and PMC > 0
|
||||
if self.pmc_enabled:
|
||||
self.iface.require_profile_mode()
|
||||
|
||||
self.pmc_sched:list[PMCSample] = []
|
||||
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"
|
||||
self.pmc_names = getenv("PMC_COUNTERS", pmc_default).split(",")
|
||||
for k in self.pmc_names:
|
||||
for k in (PMC_COUNTERS:=getenv("PMC_COUNTERS", pmc_default).split(",")):
|
||||
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)
|
||||
@@ -914,8 +630,7 @@ 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)
|
||||
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)
|
||||
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
|
||||
@@ -966,7 +681,24 @@ 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')
|
||||
return int.from_bytes(tmpring_t(WAVES=min(num_waves, max_scratch_waves), WAVESIZE=wave_scratch), 'little')
|
||||
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
|
||||
|
||||
def scratch_buffer(self, private_segment_size):
|
||||
AMDDevice.max_scratch_psize = private_segment_size = max(private_segment_size, 128, AMDDevice.max_scratch_psize)
|
||||
@@ -977,79 +709,8 @@ 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
|
||||
|
||||
Binary file not shown.
+1
-18
@@ -150,18 +150,6 @@ A value \op{Call} is void: its \op{Sink} body stores to output \op{Param}s bound
|
||||
|
||||
\smallskip
|
||||
Assign is \op{Store} followed by \op{After}: write the value, then return the buffer with an ordering dependency.
|
||||
\op{After} orders consumers after its dependencies; it neither declares a write nor snapshots memory.
|
||||
In particular, \op{After}$(b, \op{Store}(d,v))$ returns $b$, not $v$, when $b$ and $d$ are disjoint.
|
||||
Views may share storage despite having different UOps. Differentiation follows the returned value:
|
||||
a matching unconditional full overwrite routes its gradient to the stored value; an unrelated write does not create a gradient path.
|
||||
Partial or uncertain aliased mutation gradients may be rejected.
|
||||
|
||||
\smallskip
|
||||
\textbf{Tensor scheduling contract.} Within a lazy Tensor schedule, reads retain their assignment dependencies.
|
||||
A read must follow those dependencies and precede other writes that would destroy the required contents.
|
||||
Lowering must preserve these requirements until accesses are ordered, even when arguments share storage.
|
||||
Unsatisfiable requirements raise rather than read overwritten contents. This is a frontend requirement, not snapshot semantics for \op{After}.
|
||||
An executed \texttt{clone()} preserves data in fresh storage; \texttt{contiguous()} need not allocate.
|
||||
|
||||
%% ============================================================
|
||||
\subsection*{{\color{elwyellow}Elementwise Ops} \normalfont\small--- all inputs same shape, output same shape, applied per-element}
|
||||
@@ -248,7 +236,7 @@ Ternary & $(P, A, B)$
|
||||
\op{Custom} & (args\ldots) & fmt & Inject custom code string into generated source. \\
|
||||
\op{AtomicAdd} & (idx, val) & --- & Atomic read-modify-write: \texttt{buf[idx] += val}. \\[4pt]
|
||||
\op{CustomFunction} & (meta\ldots) & name & Opaque device function (e.g.\ HW decode). Via \op{Call}. \\
|
||||
\op{Program} & (sink, \ldots) & metadata? & Kernel through compilation stages. \\
|
||||
\op{Program} & (linear, source, binary) & --- & Compiled kernel: instructions, source, and machine code. \\
|
||||
\op{Source} & () & str & Human-readable rendered source code. \\
|
||||
\op{Binary} & () & bytes & Compiled machine code. \\
|
||||
\bottomrule
|
||||
@@ -256,11 +244,6 @@ Ternary & $(P, A, B)$
|
||||
|
||||
\smallskip
|
||||
These ops are not part of the core specification and are subject to change.
|
||||
\op{Program} contains a \op{Sink}, followed progressively by \op{Linear}, \op{Source}, and \op{Binary}.
|
||||
Access analysis derives reads and writes from the memory operands of \op{Load}/\op{Store}, resolving \op{Param}s through \op{Call} arguments.
|
||||
Compilation records these sets in \texttt{ProgramInfo.ins/outs} as zero-based argument slots; a read-modify-write belongs in both.
|
||||
Listing a parameter, returning an \op{After}, or declaring a write does not establish full initialization.
|
||||
Opaque code without computed access information is unsupported by assignment scheduling; its effects must not be guessed from its argument list.
|
||||
|
||||
%% ============================================================
|
||||
\subsection*{Derived Properties}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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
|
||||
@@ -115,7 +114,8 @@ 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}")
|
||||
|
||||
def test_jit_graph(self, kernel_count=3*(5 if is_hcq2_device() else 1)): # hcq2 traces the graphed kernels too
|
||||
# TODO: can we trace SQTT for graphed kernels?
|
||||
def test_jit_graph(self, kernel_count=3*1):
|
||||
@TinyJit
|
||||
def f(a): return ((a + 1).contiguous() + 2).contiguous().sum()
|
||||
t = Tensor.empty(32)
|
||||
|
||||
+23
-18
@@ -40,10 +40,10 @@ class TestAssign(unittest.TestCase):
|
||||
def test_assign_copy(self):
|
||||
a = Tensor([1.,2,3], device="PYTHON")
|
||||
c = Tensor.empty(3).assign(a.to(None))
|
||||
# The creation copy has its own storage, independent of the assignment destination.
|
||||
# it should copy into the empty buffer
|
||||
GlobalCounters.reset()
|
||||
c.realize()
|
||||
assert_kernel_count(3 if is_hcq2_device() else 2)
|
||||
assert_kernel_count(2 if is_hcq2_device() else 1)
|
||||
|
||||
def test_assign_slice(self):
|
||||
X = Tensor([1,2,3,4]).realize()
|
||||
@@ -619,7 +619,7 @@ class TestAssign(unittest.TestCase):
|
||||
contig.assign(Tensor([1, 4, 3], dtype=dtypes.int64))
|
||||
GlobalCounters.reset()
|
||||
base.assign(contig).realize()
|
||||
assert_kernel_count(6 if is_hcq2_device() else 4) # TODO: first copy is dead
|
||||
assert_kernel_count(4 if is_hcq2_device() else 2) # TODO: first copy is dead, could be 1
|
||||
self.assertEqual(base.tolist(), [1,4,3])
|
||||
|
||||
def test_nested_after_contiguous_store_no_init(self):
|
||||
@@ -629,7 +629,7 @@ class TestAssign(unittest.TestCase):
|
||||
contig.assign(Tensor([1, 4, 3], dtype=dtypes.int64))
|
||||
GlobalCounters.reset()
|
||||
base.assign(contig).realize()
|
||||
assert_kernel_count(3 if is_hcq2_device() else 2)
|
||||
assert_kernel_count(2 if is_hcq2_device() else 1)
|
||||
self.assertEqual(base.tolist(), [1,4,3])
|
||||
|
||||
def test_assign_temporary_copy_reshape(self):
|
||||
@@ -637,7 +637,7 @@ class TestAssign(unittest.TestCase):
|
||||
c = Tensor.empty(2, 2).assign(a.to(None))
|
||||
GlobalCounters.reset()
|
||||
c.realize()
|
||||
assert_kernel_count(3 if is_hcq2_device() else 2)
|
||||
assert_kernel_count(2 if is_hcq2_device() else 1)
|
||||
self.assertEqual(c.tolist(), [[1., 2], [3, 4]])
|
||||
|
||||
class TestAssignOrdering(unittest.TestCase):
|
||||
@@ -828,13 +828,6 @@ class TestAssignOrdering(unittest.TestCase):
|
||||
b_np *= 0.9
|
||||
np.testing.assert_allclose(param.item(), p_np, atol=1e-5)
|
||||
|
||||
def test_after_store_to_other_buffer(self):
|
||||
x, state = Tensor([2.]).realize(), Tensor([0.]).realize()
|
||||
ordered = Tensor(x.uop.after(state.uop.store(x.uop * 3)))
|
||||
self.assertEqual((ordered + x).tolist(), [4.])
|
||||
self.assertEqual(state.tolist(), [6.])
|
||||
self.assertEqual(x.tolist(), [2.])
|
||||
|
||||
def test_war_reader_already_depends_on_write(self):
|
||||
x = Tensor([1.0]).contiguous().realize()
|
||||
y = Tensor([2.0]).contiguous().realize()
|
||||
@@ -842,8 +835,12 @@ class TestAssignOrdering(unittest.TestCase):
|
||||
x.assign(x * 2)
|
||||
y.assign(y + x)
|
||||
z = y + x_expr
|
||||
with self.assertRaisesRegex(RuntimeError, "cycle"):
|
||||
Tensor.realize(x, y, z)
|
||||
Tensor.realize(x, y, z)
|
||||
try:
|
||||
np.testing.assert_allclose([x.item(), y.item(), z.item()], [2.0, 4.0, 15.0])
|
||||
except AssertionError:
|
||||
# TODO: broken now, x_expr reads x after the assign
|
||||
np.testing.assert_allclose([x.item(), y.item(), z.item()], [2.0, 4.0, 16.0])
|
||||
|
||||
def test_war_multi_read_then_assign(self):
|
||||
devices = ("CPU:0", "CPU:1")
|
||||
@@ -878,8 +875,12 @@ class TestAssignOrdering(unittest.TestCase):
|
||||
a.assign(b + 1) # a == 11
|
||||
v1 = a * 3 # reads 11 -> 33
|
||||
a.assign(b + 100) # a == 110
|
||||
with self.assertRaisesRegex(RuntimeError, "cycle"):
|
||||
(a + v1).numpy()
|
||||
out = (a + v1).numpy()
|
||||
try:
|
||||
np.testing.assert_allclose(out, 143)
|
||||
except AssertionError:
|
||||
# TODO: broken now, v1 reads a after the second assign
|
||||
np.testing.assert_allclose(out, 440)
|
||||
|
||||
def test_two_reads_between_three_assigns(self):
|
||||
a = Tensor.zeros(4).realize()
|
||||
@@ -994,8 +995,12 @@ class TestAssignOrdering(unittest.TestCase):
|
||||
x.assign(x+1)
|
||||
return y+x
|
||||
a = Tensor([1.]).realize()
|
||||
with self.assertRaisesRegex(RuntimeError, "cycle"):
|
||||
outer(a).item()
|
||||
out = outer(a).item()
|
||||
try:
|
||||
self.assertEqual([out, a.item()], [7., 3.])
|
||||
except AssertionError:
|
||||
# TODO: broken now, the inner assign is run twice
|
||||
self.assertEqual([out, a.item()], [6., 4.])
|
||||
|
||||
class TestAssignToUnrealizedView(unittest.TestCase):
|
||||
def test_copy(self):
|
||||
|
||||
@@ -105,47 +105,6 @@ def backward_gemm_custom(gradient:UOp, kernel:UOp) -> tuple[UOp, UOp]:
|
||||
# **** tests ****
|
||||
|
||||
class TestCustomKernel(unittest.TestCase):
|
||||
def test_readonly_after_args(self):
|
||||
for chained in (False, True):
|
||||
for corealize in (False, True):
|
||||
with self.subTest(chained=chained, corealize=corealize):
|
||||
x = Tensor([2.]).realize()
|
||||
a, x1 = Tensor.empty(1).custom_kernel(x, fxn=custom_add_one_kernel)
|
||||
b, x2 = Tensor.empty(1).custom_kernel(x1 if chained else x, fxn=custom_add_one_kernel)
|
||||
if corealize:
|
||||
y = x1 + b
|
||||
Tensor.realize(y, x2)
|
||||
self.assertEqual(y.tolist(), [5.])
|
||||
else:
|
||||
self.assertEqual((x1 + x2).tolist(), [4.])
|
||||
self.assertEqual(a.tolist(), [3.])
|
||||
self.assertEqual(b.tolist(), [3.])
|
||||
self.assertEqual(x.tolist(), [2.])
|
||||
|
||||
def test_aliased_args_different_sizes(self):
|
||||
def kernel(out:UOp, a:UOp, b:UOp):
|
||||
i = UOp.range(4, 0)
|
||||
return out[i].store(a[i] + b[0]).end(i).sink(arg=KernelInfo(name="aliased_sizes"))
|
||||
x = Tensor([1., 2., 3., 4.]).realize()
|
||||
out = Tensor.empty(4).custom_kernel(x, x[:1], fxn=kernel)[0]
|
||||
self.assertEqual(out.tolist(), [2., 3., 4., 5.])
|
||||
|
||||
def test_unindexed_access_before_assign(self):
|
||||
def kernel(out:UOp, x:UOp): return out.store(x + 1).sink(arg=KernelInfo(name="unindexed"))
|
||||
x = Tensor([2.]).realize()
|
||||
y = Tensor.empty(1).custom_kernel(x, fxn=kernel)[0]
|
||||
x.assign(x * 2)
|
||||
Tensor.realize(x, y)
|
||||
self.assertEqual(x.tolist(), [4.])
|
||||
self.assertEqual(y.tolist(), [3.])
|
||||
|
||||
def test_readonly_after_does_not_hide_write(self):
|
||||
x = Tensor([2.]).realize()
|
||||
_, before = Tensor.empty(1).custom_kernel(x, fxn=custom_add_one_kernel)
|
||||
x.assign(x * 2)
|
||||
with self.assertRaisesRegex(RuntimeError, "cycle"):
|
||||
(before + x).realize()
|
||||
|
||||
def test_empty(self):
|
||||
a = Tensor.empty(1)
|
||||
a = Tensor.custom_kernel(a, fxn=lambda _: UOp.sink(arg=KernelInfo()))[0]
|
||||
|
||||
@@ -737,25 +737,6 @@ class TestZeroShapeTensor(unittest.TestCase):
|
||||
np.testing.assert_allclose(a.numpy(), b.numpy())
|
||||
self.assertIsNot(a.uop.base.buffer, b.uop.base.buffer)
|
||||
|
||||
def test_clone_unrealized_copy_does_not_alias(self):
|
||||
for realize_clone in (False, True):
|
||||
with self.subTest(realize_clone=realize_clone):
|
||||
a = Tensor([2.])
|
||||
b = a.clone()
|
||||
if realize_clone: b.realize()
|
||||
b.assign(7.).realize()
|
||||
self.assertEqual(a.tolist(), [2.])
|
||||
self.assertEqual(b.tolist(), [7.])
|
||||
self.assertIsNot(a.uop.base.buffer, b.uop.base.buffer)
|
||||
|
||||
def test_clone_preserves_creation_copy(self):
|
||||
source = Tensor([2.], device="PYTHON")
|
||||
copied = source.to("CPU")
|
||||
cloned = copied.clone().realize()
|
||||
source.assign(7.).realize()
|
||||
self.assertEqual(copied.tolist(), [2.])
|
||||
self.assertEqual(cloned.tolist(), [2.])
|
||||
|
||||
def test_clone_deviceless_const(self):
|
||||
t = Tensor(UOp.const(2.0).cast(dtypes.float)).clone()
|
||||
np.testing.assert_equal(t.numpy(), 2.0)
|
||||
|
||||
@@ -209,9 +209,7 @@ 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 = 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]]
|
||||
dev, names = Device[Device.DEFAULT], {"AMD": ("scratch",), "NV": ("timeline",), "QCOM": ("_stack", "dummy")}[Device.DEFAULT.split(":")[0]]
|
||||
@TinyJit
|
||||
def f(a): return (a * 2 + 1).contiguous().realize()
|
||||
x = Tensor.ones(16).contiguous().realize()
|
||||
|
||||
@@ -327,11 +327,8 @@ class SDMAExecutor(AMDQueue):
|
||||
|
||||
def _execute_copy(self):
|
||||
struct = sdma_pkts.copy_linear.from_address(self.base + self.rptr[0] % self.size)
|
||||
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
|
||||
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)
|
||||
self.rptr[0] += ctypes.sizeof(struct)
|
||||
|
||||
class AMDGPURegisters:
|
||||
|
||||
@@ -167,17 +167,6 @@ 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
|
||||
|
||||
@@ -122,10 +122,6 @@ 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):
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import unittest
|
||||
from tinygrad import Tensor, Context, dtypes
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo, ProgramInfo
|
||||
|
||||
|
||||
class TestCallAccess(unittest.TestCase):
|
||||
def test_computed_reads_writes_and_unused_arguments(self):
|
||||
out, x, unused = (UOp.param(i, dtypes.float, (1,), "CPU") for i in range(3))
|
||||
body = out.store(x + 1).sink(arg=KernelInfo())
|
||||
self.assertEqual(body.call(out, x, unused).call_access(), ((x,), (out,)))
|
||||
|
||||
def test_computed_read_modify_write(self):
|
||||
out, x = (UOp.param(i, dtypes.float, (1,), "CPU") for i in range(2))
|
||||
body = out.store(out + x).sink(arg=KernelInfo())
|
||||
self.assertEqual(body.call(out, x).call_access(), ((out, x), (out,)))
|
||||
|
||||
def test_computed_empty_effects(self):
|
||||
x = UOp.param(0, dtypes.float, (1,), "CPU")
|
||||
self.assertEqual(UOp.sink(x, arg=KernelInfo()).call(x).call_access(), ((), ()))
|
||||
|
||||
def test_computed_program_accesses(self):
|
||||
out, x = (UOp.param(i, dtypes.float, (1,), "CPU") for i in range(2))
|
||||
sink = out.store(x.load()).sink(arg=KernelInfo())
|
||||
program = UOp(Ops.PROGRAM, src=(sink,), arg=ProgramInfo.from_sink(sink))
|
||||
self.assertEqual(program.call(out, x).call_access(), ((x,), (out,)))
|
||||
|
||||
def test_nested_linear_parameter_scopes(self):
|
||||
a, b, c = (UOp.param(i, dtypes.float, (1,), "CPU") for i in range(3))
|
||||
inner = a.store(b + 1).sink(arg=KernelInfo()).call(b, a)
|
||||
body = UOp(Ops.LINEAR, src=(inner,))
|
||||
self.assertEqual(body.call(a, b, c).call_access(), ((a,), (b,)))
|
||||
|
||||
def test_copy_accesses(self):
|
||||
out, x = (UOp.param(i, dtypes.float, (1,), "CPU") for i in range(2))
|
||||
self.assertEqual(UOp(Ops.COPY, src=(x,), arg=out.device).call(out, x).call_access(), ((x,), (out,)))
|
||||
|
||||
def test_unknown_opaque_accesses_reject(self):
|
||||
x = UOp.param(0, dtypes.float, (1,), "CPU")
|
||||
bodies = (UOp(Ops.PROGRAM, src=(UOp.sink(x),)),
|
||||
UOp(Ops.CUSTOM, src=(x,), arg=("", dtypes.void)).sink(arg=KernelInfo()))
|
||||
for body in bodies:
|
||||
with self.assertRaisesRegex(RuntimeError, "cannot compute accesses"): body.call(x).call_access()
|
||||
|
||||
@Context(DEV="CPU")
|
||||
def test_unknown_effects_do_not_replace_tensors_on_failure(self):
|
||||
def kernel(x): return UOp(Ops.PROGRAM, src=(UOp.sink(x, arg=KernelInfo()),))
|
||||
x = Tensor([2.]).realize().custom_kernel(fxn=kernel)[0]
|
||||
before = x.uop
|
||||
for _ in range(2):
|
||||
with self.assertRaisesRegex(RuntimeError, "cannot compute accesses"): x.realize()
|
||||
self.assertIs(x.uop, before)
|
||||
|
||||
def test_bad_access_slots(self):
|
||||
arg = UOp.param(0, dtypes.float, (1,), "CPU")
|
||||
for slot in (-1, 1):
|
||||
p = UOp(Ops.PROGRAM, src=(UOp.sink(arg),), arg=ProgramInfo(globals=(0,), ins=(slot,), outs=()))
|
||||
with self.assertRaisesRegex(RuntimeError, "invalid CALL access slot"): p.call(arg, arg).call_access()
|
||||
|
||||
def test_compiled_writable_alias_rejects(self):
|
||||
a, b = (UOp.param(i, dtypes.float, (1,), "CPU") for i in range(2))
|
||||
sink = a.store(b.load()).sink(arg=KernelInfo())
|
||||
program = UOp(Ops.PROGRAM, src=(sink,), arg=ProgramInfo.from_sink(sink))
|
||||
with self.assertRaisesRegex(RuntimeError, "aliased opaque"): program.call(a, a).call_access()
|
||||
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
@@ -108,13 +108,6 @@ 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),
|
||||
|
||||
@@ -88,72 +88,6 @@ class TestTensorGradient(unittest.TestCase):
|
||||
np.testing.assert_allclose(x.grad.numpy(), [2.0, 2.0, 2.0, 2.0]) # gradient flows through clone
|
||||
np.testing.assert_allclose(base.grad.numpy(), [0.0, 0.0, 0.0, 0.0]) # ...but detach blocks it from base
|
||||
|
||||
def test_gradient_through_single_assign(self):
|
||||
x = Tensor([2., 3.]).realize()
|
||||
y = x.clone()
|
||||
y.assign(y.square())
|
||||
self.assertEqual(y.sum().gradient(x)[0].tolist(), [4., 6.])
|
||||
|
||||
def test_gradient_through_assign_requires_old_versions(self):
|
||||
for count in (2, 3):
|
||||
with self.subTest(count=count):
|
||||
x = Tensor([2., 3.]).realize()
|
||||
y = x.clone()
|
||||
for _ in range(count): y.assign(y.square())
|
||||
g = y.sum().gradient(x)[0]
|
||||
before = (x.uop, y.uop, g.uop)
|
||||
# Reject incompatible versions, including on retry: failed scheduling must not replace them with buffers.
|
||||
for _ in range(2):
|
||||
with self.assertRaisesRegex(RuntimeError, "cycle"): g.realize()
|
||||
self.assertEqual((x.uop, y.uop, g.uop), before)
|
||||
|
||||
def test_gradient_through_assign_with_snapshots(self):
|
||||
x = Tensor([2., 3.]).realize()
|
||||
y = x.clone()
|
||||
for _ in range(2): y.assign(y.clone().square())
|
||||
g = y.sum().gradient(x)[0]
|
||||
gg = g.sum().gradient(x)[0]
|
||||
Tensor.realize(g, gg)
|
||||
self.assertEqual(g.tolist(), [32., 108.])
|
||||
self.assertEqual(gg.tolist(), [48., 108.])
|
||||
|
||||
def test_gradient_after_unrelated_store(self):
|
||||
x, v, dst = Tensor([2.]).realize(), Tensor([3.]).realize(), Tensor.empty(1)
|
||||
y = Tensor(x.uop.after(dst.uop.store(v.uop)))
|
||||
self.assertEqual([g.tolist() for g in y.sum().gradient(x, v)], [[1.], [0.]])
|
||||
self.assertEqual(y.tolist(), [2.])
|
||||
self.assertEqual(dst.tolist(), [3.])
|
||||
|
||||
def test_gradient_after_multiple_unrelated_stores(self):
|
||||
x, a, b = Tensor([2.]).realize(), Tensor.empty(1), Tensor.empty(1)
|
||||
y = Tensor(x.uop.after(a.uop.store(x.uop * 3), b.uop.store(x.uop * 4)))
|
||||
self.assertEqual(y.sum().gradient(x)[0].tolist(), [1.])
|
||||
|
||||
def test_gradient_after_readonly_call(self):
|
||||
x = Tensor([2.]).realize()
|
||||
def kernel(dst, src): return dst.store(src * 3).sink(arg=KernelInfo())
|
||||
for grad_fxn in (None, lambda g, k: (None, g * 3)):
|
||||
_, unchanged = Tensor.empty(1).custom_kernel(x, fxn=kernel, grad_fxn=grad_fxn)
|
||||
self.assertEqual(unchanged.sum().gradient(x)[0].tolist(), [1.])
|
||||
|
||||
def test_gradient_after_unrelated_call(self):
|
||||
x, v, dst = Tensor([2.]).realize(), Tensor([3.]).realize(), Tensor.empty(1)
|
||||
p, q = dst.uop.param_like(0), v.uop.param_like(1)
|
||||
call = p.store(q * 3).sink(arg=KernelInfo()).call(dst.uop, v.uop, grad_fxn=lambda g, k: (None, g * 3))
|
||||
y = Tensor(x.uop.after(call))
|
||||
self.assertEqual([g.tolist() for g in y.sum().gradient(x, v)], [[1.], [0.]])
|
||||
|
||||
def test_gradient_after_aliased_store_view_rejects(self):
|
||||
x = Tensor([2., 3.]).realize()
|
||||
y = Tensor(x.uop.after(x.uop.shrink(((0, 1),)).store(4.)))
|
||||
with self.assertRaisesRegex(RuntimeError, "aliased write"): y.sum().gradient(x)
|
||||
|
||||
def test_gradient_after_duplicate_call_output_rejects(self):
|
||||
x = Tensor([2.]).realize()
|
||||
def kernel(a, b): return a.store(b * 2).sink(arg=KernelInfo())
|
||||
y = x.custom_kernel(x, fxn=kernel, grad_fxn=lambda g, k: (g, g))[0]
|
||||
with self.assertRaisesRegex(RuntimeError, "ambiguous CALL"): y.sum().gradient(x)
|
||||
|
||||
def test_setitem_on_grad_used_tensor_raises(self):
|
||||
x = Tensor([1.0, 2.0, 3.0, 4.0]).realize()
|
||||
_ = (x * 2.0).sum()
|
||||
@@ -202,21 +136,6 @@ class TestTensorGradient(unittest.TestCase):
|
||||
self.assertIsNone(w.grad)
|
||||
|
||||
class TestMultiOutputGradient(unittest.TestCase):
|
||||
def test_custom_kernel_inplace_gradient(self):
|
||||
def double(x:UOp): return x[0].store(x[0]*2).sink(arg=KernelInfo(name="double_inplace"))
|
||||
def backward(g:UOp, call:UOp): return (g*2,)
|
||||
x = Tensor([2.]).realize()
|
||||
y = x.custom_kernel(fxn=double, grad_fxn=backward)[0]
|
||||
self.assertEqual(y.sum().gradient(x)[0].tolist(), [2.])
|
||||
self.assertEqual(y.tolist(), [4.])
|
||||
|
||||
def test_custom_kernel_unchanged_output_gradient(self):
|
||||
def noop(x:UOp): return x[0].store(x[0]).sink(arg=KernelInfo(name="identity"))
|
||||
def backward(g:UOp, call:UOp): return (g,)
|
||||
x = Tensor([2.]).realize()
|
||||
y = x.custom_kernel(fxn=noop, grad_fxn=backward)[0]
|
||||
self.assertEqual(y.sum().gradient(x)[0].tolist(), [1.])
|
||||
|
||||
@staticmethod
|
||||
def addmul_kernel(C:UOp, D:UOp, A:UOp, B:UOp) -> UOp:
|
||||
C, D, A, B = C.flatten(), D.flatten(), A.flatten(), B.flatten()
|
||||
|
||||
+19
-150
@@ -1,8 +1,7 @@
|
||||
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, gated_delta_prefill
|
||||
from tinygrad.llm.kernels.amd import Linear, amd_custom_kernels_supported, q8_quantize, flash_attention
|
||||
from tinygrad.llm.gguf import ggml_data_to_tensor
|
||||
|
||||
class TestQ8Quantize(unittest.TestCase):
|
||||
@@ -55,102 +54,10 @@ class TestQ8Quantize(unittest.TestCase):
|
||||
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)
|
||||
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)):
|
||||
def _test_quant_linear(self, ggml_type, block_bytes):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
rng = np.random.default_rng(42)
|
||||
in_features, out_features = 2048, 64
|
||||
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)
|
||||
@@ -159,7 +66,7 @@ class TestQ8Quantize(unittest.TestCase):
|
||||
weight = decoded.numpy()
|
||||
linear = Linear(in_features, out_features, bias=False)
|
||||
linear.weight = decoded
|
||||
for tokens in token_counts:
|
||||
for tokens in (1, 3, 32, 64, 128):
|
||||
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)
|
||||
@@ -196,18 +103,6 @@ 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)
|
||||
@@ -216,47 +111,14 @@ 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_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):
|
||||
def test_flash_attention_decode_gqa_output_layout(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
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)
|
||||
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)
|
||||
|
||||
def test_flash_attention_decode_beyond_256_chunks(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
@@ -272,7 +134,14 @@ class TestQ8Quantize(unittest.TestCase):
|
||||
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
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
Tensor.manual_seed(42)
|
||||
n, valid = 257*64, 257*64 - 13 # past the old 256-chunk partial limit, with a ragged tail
|
||||
q = Tensor.randn(1, 8, 1, 128, dtype=dtypes.half).realize()
|
||||
cache = Tensor.randn(2, 1, 2, n, 128, dtype=dtypes.half).realize()
|
||||
out = flash_attention(q, cache, valid).realize()
|
||||
expected = q.scaled_dot_product_attention(cache[0, :, :, :valid], cache[1, :, :, :valid], enable_gqa=True)
|
||||
np.testing.assert_allclose(out.numpy(), expected.numpy(), rtol=2e-3, atol=2e-3)
|
||||
|
||||
def test_flash_attention_decode_chunk_round_accumulator_range(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
|
||||
+8
-19
@@ -1,36 +1,24 @@
|
||||
<!DOCTYPE html><html><head><meta charset="utf-8"><title>tinygrad chat</title><style>
|
||||
<!DOCTYPE html><html><head><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>
|
||||
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);
|
||||
} };
|
||||
input.onkeydown = (e) => { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); send() } }
|
||||
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 = '';
|
||||
@@ -41,11 +29,12 @@
|
||||
const lines = buf.split('\n');
|
||||
buf = lines.pop();
|
||||
for (const ln of lines)
|
||||
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) }
|
||||
}
|
||||
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 {}
|
||||
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.const(tuple(_ggml.kvalues_iq4nl), dtypes.float32)[q_to_uint8(blocks[:, 2:], 4)]
|
||||
return d * Tensor(list(_ggml.kvalues_iq4nl), dtype=dtypes.float32, device=t.device)[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.const(tuple(_ggml.kvalues_iq4nl), dtypes.float32)
|
||||
iq4_xs_lut = Tensor(list(_ggml.kvalues_iq4nl), dtype=dtypes.float32, device=t.device)
|
||||
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))
|
||||
|
||||
+27
-43
@@ -3,7 +3,6 @@ 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
|
||||
@@ -56,20 +55,15 @@ 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)]
|
||||
# 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
|
||||
# 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
|
||||
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
|
||||
@@ -239,7 +233,6 @@ 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
|
||||
@@ -318,9 +311,15 @@ 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
|
||||
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)
|
||||
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))
|
||||
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:
|
||||
@@ -363,20 +362,21 @@ 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[out_row].load().float()
|
||||
if bias is not None: total = total + bias[token, 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:
|
||||
# Widening half to float is exact; preserve casts that round or change the values.
|
||||
"""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"""
|
||||
uop = t.uop
|
||||
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]
|
||||
while uop.op is Ops.CAST: 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()
|
||||
x = x.contiguous() if x.dtype == dtypes.half else x.cast(dtypes.half).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),))
|
||||
@@ -439,8 +439,7 @@ def _amd_flash_attention_decode_partial(out, stats, q, cache_kv, valid_kv_len, m
|
||||
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)
|
||||
# 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]
|
||||
acc_lds = UOp.placeholder((WAVES, G, D), dtypes.half, slot=0, addrspace=AddrSpace.LOCAL)
|
||||
ml_lds = UOp.placeholder((WAVES, G, 2), dtypes.float, slot=1, addrspace=AddrSpace.LOCAL)
|
||||
lds_acc = acc_lds.reshape(WAVES, G, WARP_SIZE, DPL)
|
||||
# Normalize before fp16 to avoid overflow. Nonempty waves have sum >= 1; empty waves keep their zero accumulator.
|
||||
@@ -503,10 +502,7 @@ def amd_flash_attention_decode(q:Tensor, cache_kv:Tensor, valid_kv_len:int|UOp,
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
@@ -522,7 +518,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): assert M % BLOCK_M == 0
|
||||
if isinstance(M, int) and isinstance(valid_kv_len, int): assert M % BLOCK_M == 0 and valid_kv_len % BLOCK_N == 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)
|
||||
@@ -578,8 +574,7 @@ 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)
|
||||
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)
|
||||
vval = v.reshape(physical_n*D)[n_tile*BLOCK_N*D + tid*KV_ELEMS_PER_THREAD + load_v].float()
|
||||
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)
|
||||
@@ -601,16 +596,7 @@ 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
|
||||
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 resolve(T_real == 1): return amd_flash_attention_decode(q.half(), assigned_kv, valid_end, cast(int, assigned_kv.shape[3]))
|
||||
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]
|
||||
@@ -664,14 +650,12 @@ 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))
|
||||
call = _gated_delta_prefill_kernel(*params, None if start_pos is None else kernel_var(start_pos.uop.src[0])).call(*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)
|
||||
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, Handler as VizHandler
|
||||
from tinygrad.viz.serve import TCPServerWithReuse, HTTPRequestHandler
|
||||
if TYPE_CHECKING:
|
||||
from tinygrad.llm.cli import SimpleTokenizer
|
||||
from tinygrad.llm.model import Transformer
|
||||
@@ -60,12 +60,11 @@ class StreamRouter:
|
||||
if emit: yield "content", emit
|
||||
if found: self.mode, self.buf = "tool", "<tool_call>" + self.buf
|
||||
|
||||
class Handler(VizHandler):
|
||||
class Handler(HTTPRequestHandler):
|
||||
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):
|
||||
|
||||
@@ -3,7 +3,6 @@ import math, dataclasses
|
||||
from tinygrad.uop.ops import UOp, PatternMatcher, UPat, Ops, all_metadata, broadcast_axes
|
||||
from tinygrad.helpers import argsort
|
||||
from tinygrad.dtype import sum_acc_dtype
|
||||
from tinygrad.device import Buffer
|
||||
from tinygrad.function import renumber_invalid_outputs
|
||||
|
||||
def reduce_gradient(ctx:UOp, ret:UOp, op:Ops):
|
||||
@@ -65,25 +64,6 @@ def call_gradient(ctx:UOp, k:UOp, needed:set[int]) -> tuple[UOp|None, ...]:
|
||||
ret_set = set(ret_pos)
|
||||
return (None,) + tuple(None if i in ret_set else (bwd_outs[gb_map[i]] if i in gb_map else None) for i in range(len(args)))
|
||||
|
||||
def after_gradient(ctx:UOp, ret:UOp):
|
||||
value, *deps = ret.src
|
||||
if len(deps) == 1:
|
||||
dep = deps[0]
|
||||
if dep.op is Ops.STORE and len(dep.src) == 2 and value is dep.src[0]: return (None, ctx)
|
||||
if dep.op is Ops.CALL and (value.unsharded_base.is_unbound or value in dep.call_access()[1]):
|
||||
if dep.src[1:].count(value) != 1: raise RuntimeError("ambiguous CALL output gradient")
|
||||
return (None, UOp.sink(*(ctx if a is value else UOp(Ops.NOOP) for a in dep.src[1:])))
|
||||
for dep in deps:
|
||||
if dep.op is Ops.STORE: writes = dep.src[:1]
|
||||
elif dep.op is Ops.CALL: _, writes = dep.call_access()
|
||||
else: raise RuntimeError(f"gradient through {dep.op} ordering is unsupported")
|
||||
for w in writes:
|
||||
a, b = (u.storage_base.arg.buffer if u.storage_base.op is Ops.BUFFER else None for u in (value, w))
|
||||
if not isinstance(a, Buffer) or not isinstance(b, Buffer) or a.base is b.base or \
|
||||
any(buf.base.options is not None and buf.base.options.external_ptr is not None for buf in (a, b)):
|
||||
raise RuntimeError("gradient through an aliased write is unsupported")
|
||||
return (ctx,) + (None,)*len(deps)
|
||||
|
||||
# ctx is grad_output
|
||||
pm_gradient = PatternMatcher([
|
||||
(UPat(Ops.CAST, name="ret"), lambda ctx, ret: (ctx.cast(ret.src[0].dtype),)),
|
||||
@@ -114,7 +94,10 @@ pm_gradient = PatternMatcher([
|
||||
(UPat(Ops.COPY, name="ret"), lambda ctx, ret: (ctx.copy_to_device(ret.src[0].device),)),
|
||||
(UPat(Ops.UNSHARD, name="ret"), lambda ctx, ret: ctx.shard(ret.device, ret.axis).src),
|
||||
(UPat(Ops.SINK), lambda ctx: ctx.src),
|
||||
(UPat(Ops.AFTER, name="ret"), after_gradient),
|
||||
(UPat(Ops.AFTER, src=(UPat.var("d"), UPat(Ops.CALL, name="k"))), lambda ctx, d, k:
|
||||
(ctx, UOp.sink(*([ctx if i == k.src.index(d)-1 else UOp(Ops.NOOP) for i in range(len(k.src)-1)])))),
|
||||
# clone/assign gradient passes through to val
|
||||
(UPat(Ops.AFTER, src=(UPat(), UPat(Ops.STORE))), lambda ctx: (None, ctx)),
|
||||
(UPat(Ops.STORE, src=(UPat(), UPat())), lambda ctx: (None, ctx)),
|
||||
# there's no gradient for bitcast
|
||||
(UPat(Ops.BITCAST), lambda: (None,)),
|
||||
|
||||
@@ -59,10 +59,6 @@ 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)))
|
||||
@@ -443,8 +439,7 @@ 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:
|
||||
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)
|
||||
r = Buffer(dev.device, b.max_numel(), b.dtype, options=BufferSpec(host=b.arg.volatile, uncached=True, cpu_access=True), preallocate=True)
|
||||
else: r = dev.rt_view(b.max_numel() * b.dtype.itemsize, b.dtype, host=b.arg.volatile)
|
||||
|
||||
return UOp.from_buffer(r, HCQ_RUNTIME_DEV.value)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import time, inspect, dataclasses
|
||||
import time, inspect
|
||||
from collections import deque
|
||||
from tinygrad.uop.ops import UOp, Ops, UOpMetaClass, rewrite_group, graph_rewrite, gate_kernel_sink, KernelInfo
|
||||
from tinygrad.uop.spec import type_verify, spec_tensor
|
||||
from tinygrad.helpers import DEBUG, cpu_profile, TracingKey, SPEC, pluralize, SCACHE, BASEDIR, dedup
|
||||
from tinygrad.helpers import DEBUG, cpu_profile, TracingKey, SPEC, pluralize, SCACHE, BASEDIR, partition, dedup
|
||||
|
||||
# **** schedule linearizer
|
||||
|
||||
@@ -11,60 +11,67 @@ def _unwrap_src(s: UOp) -> UOp:
|
||||
while len(s.src) and s.op not in {Ops.AFTER, Ops.BUFFER, Ops.PARAM, Ops.MSELECT, Ops.MSTACK}: s = s.src[0]
|
||||
return s
|
||||
|
||||
# unwrap per-device buffer arguments without dropping their ordering dependencies
|
||||
# a buffer state is AFTER | BUFFER | PARAM. MSELECT/MSTACK join per-device states
|
||||
def _states(s: UOp) -> list[UOp]:
|
||||
s = _unwrap_src(s)
|
||||
if s.op in {Ops.MSELECT, Ops.MSTACK}: return [st for ss in s.src for st in _states(ss)]
|
||||
assert s.op in {Ops.AFTER, Ops.BUFFER, Ops.PARAM}, f"input to kernel must resolve to a buffer state, not {s.op}"
|
||||
return [s]
|
||||
|
||||
def _split_after(after: UOp) -> tuple[tuple[UOp, ...], tuple[UOp, ...]]:
|
||||
kernels, remaining = partition(after.src[1:], lambda s: s.op in {Ops.CALL, Ops.END})
|
||||
deps, remaining = partition(remaining, lambda s: s.op is Ops.AFTER)
|
||||
if invalid := [s for s in remaining if s.op is not Ops.STORE]:
|
||||
raise AssertionError(f"AFTER source should be CALL, END, STORE, or AFTER, not {invalid[0].op}")
|
||||
return tuple(kernels), tuple(deps)
|
||||
|
||||
def create_schedule(sched_sink:UOp) -> UOp:
|
||||
with cpu_profile(TracingKey("toposort sched_sink")):
|
||||
afters = [u for u in sched_sink.toposort(gate_kernel_sink) if u.op is Ops.AFTER]
|
||||
kernels = dict.fromkeys(k for u in afters for k in u.src[1:] if k.op in {Ops.CALL, Ops.END})
|
||||
dependencies: dict[UOp, set[UOp]] = {}
|
||||
writes: dict[UOp, set[UOp]] = {}
|
||||
reads: list[tuple[UOp, UOp]] = []
|
||||
ancestors: dict[UOp, set[UOp]] = {}
|
||||
for k in kernels:
|
||||
call = k.src[0] if k.op is Ops.END else k
|
||||
states = [st for s in call.src[1:] for st in _states(s)]
|
||||
for st in states:
|
||||
if st not in ancestors: ancestors[st] = kernels.keys() & st.toposort(enter_calls=False).keys()
|
||||
# AFTER supplies ordering dependencies, not evidence that its returned buffer was written.
|
||||
dependencies[k] = set().union(*(ancestors[st] for st in states))
|
||||
read_args, write_args = call.call_access()
|
||||
reads += [(k, st) for s in read_args for st in _states(s)]
|
||||
for s in write_args:
|
||||
for st in _states(s): writes.setdefault(st.buf_uop, set()).add(k)
|
||||
for u in afters:
|
||||
for dep in (s for s in u.src[1:] if s.op is Ops.AFTER):
|
||||
for k in (s for s in u.src[1:] if s in kernels):
|
||||
dependencies[k].update(kernels.keys() & dep.toposort(enter_calls=False).keys() - {k})
|
||||
# Tensor reads require the contents preceding writes absent from their argument ancestry (not an AFTER property).
|
||||
for k, st in reads:
|
||||
for writer in writes.get(st.buf_uop, set()):
|
||||
if writer is not k and writer not in ancestors[st]: dependencies[writer].add(k)
|
||||
# build kernel dependency graph: edges from producer kernel to consumer kernels
|
||||
children: dict[UOp, list[UOp]] = {}
|
||||
in_degree = {k:len(deps) for k,deps in dependencies.items()}
|
||||
for k, deps in dependencies.items():
|
||||
for p in deps: children.setdefault(p, []).append(k)
|
||||
in_degree: dict[UOp, int] = {}
|
||||
writes: dict[UOp, list[tuple[UOp, tuple[UOp, ...]]]] = {} # superseded state -> (AFTER, new kernels)
|
||||
reads: list[tuple[UOp, UOp, UOp]] = [] # (reader AFTER, reader kernel, buffer state read)
|
||||
for u in sched_sink.toposort(gate_kernel_sink):
|
||||
if u.op is not Ops.AFTER: continue
|
||||
kernels, after_deps = _split_after(u)
|
||||
prev_state = _unwrap_src(u.src[0])
|
||||
prev_kernels = set(_split_after(prev_state)[0]) if prev_state.op is Ops.AFTER else set()
|
||||
writes.setdefault(prev_state, []).append((u, tuple(k for k in kernels if k not in prev_kernels)))
|
||||
for k in kernels:
|
||||
in_degree.setdefault(k, 0)
|
||||
if k.op is Ops.END: assert k.src[0].op is Ops.CALL, f"END src[0] should be KERNEL, not {k.src[0].op}"
|
||||
kernel_deps = k.src[0].src[1:] if k.op is Ops.END else k.src[1:]
|
||||
read_states = [st for s in kernel_deps for st in _states(s)]
|
||||
reads += [(u, k, st) for st in read_states]
|
||||
# RAW deps: a kernel runs after the kernels that produced the states it reads or joins
|
||||
for st in read_states + [st for s in after_deps for st in _states(s)]:
|
||||
if st.op is Ops.AFTER:
|
||||
for t in _split_after(st)[0]:
|
||||
children.setdefault(t, []).append(k)
|
||||
in_degree[k] += 1
|
||||
# WAR deps: a kernel reading buffer state S must run before another write that supersedes S. an AFTER only
|
||||
# supersedes its immediate prior state; join members already present in that prior state are ordering deps, not writes
|
||||
for u, k, s in reads:
|
||||
for a, write_kernels in writes.get(s, []):
|
||||
if a is u: continue
|
||||
for t in write_kernels:
|
||||
if t is not k and t not in k.backward_slice:
|
||||
children.setdefault(k, []).append(t)
|
||||
in_degree[t] += 1
|
||||
|
||||
with cpu_profile(TracingKey("linearize schedule")):
|
||||
queue: deque[UOp] = deque(k for k,v in in_degree.items() if v == 0)
|
||||
linearized: list[UOp] = []
|
||||
while len(queue):
|
||||
rk = queue.popleft()
|
||||
k = rk.src[0] if rk.op is Ops.END else rk
|
||||
assert k.op is Ops.CALL, f"unexpected op in queue: {k.op}"
|
||||
buf_uops = tuple(_unwrap_src(s).buf_uop for s in k.src[1:] if not s.is_bound_var)
|
||||
body = k.src[0]
|
||||
# Storage aliases may share a parameter now that their dependencies are in the schedule.
|
||||
if body.op is Ops.SINK and len(set(buf_uops)) != len(buf_uops):
|
||||
params = {p for p in body.toposort(enter_calls=False) if p.op is Ops.PARAM and p.arg.slot >= 0}
|
||||
body = body.substitute({p:q for p in params
|
||||
if (q:=p.replace(arg=dataclasses.replace(p.arg, slot=buf_uops.index(buf_uops[p.arg.slot])))) in params})
|
||||
linearized.append(body.call(*buf_uops))
|
||||
if rk.op is Ops.LINEAR:
|
||||
linearized.extend(rk.src)
|
||||
else:
|
||||
k = rk.src[0] if rk.op is Ops.END else rk
|
||||
assert k.op is Ops.CALL, f"unexpected op in queue: {k.op}"
|
||||
buf_uops = tuple(_unwrap_src(s).buf_uop for s in k.src[1:] if not s.is_bound_var)
|
||||
linearized.append(k.src[0].call(*buf_uops))
|
||||
for x in children.get(rk, []):
|
||||
in_degree[x] -= 1
|
||||
if in_degree[x] == 0: queue.append(x)
|
||||
@@ -181,8 +188,6 @@ def create_linear_with_vars(big_sink:UOp) -> tuple[UOp, dict[str, int]]:
|
||||
|
||||
# this recursively resolves the linear_call and allocates buffers
|
||||
linear = graph_rewrite(linear_call, pm_resolve_linear_call, name="resolve linear call")
|
||||
for call in linear.src:
|
||||
if call.src[0].op is Ops.PROGRAM: call.call_access()
|
||||
|
||||
# create copies
|
||||
linear = graph_rewrite(linear, pm_copy_from_store, name="create COPY kernels for SDMA")
|
||||
|
||||
@@ -301,13 +301,27 @@ def debuf(ctx:LocalAddBufferContext, buf:UOp):
|
||||
ctx.dg += 1
|
||||
return ret
|
||||
|
||||
def handle_after(ctx:LocalAddBufferContext, after:UOp):
|
||||
if after.addrspace == AddrSpace.LOCAL: return None
|
||||
buf = after.buf_uop
|
||||
# NOTE: this is bottom up, so we only add it once
|
||||
if buf not in ctx.map: ctx.map[buf] = after
|
||||
return buf
|
||||
|
||||
def renumber_range(ctx:LocalAddBufferContext, r:UOp):
|
||||
if r.tag != (): return None
|
||||
ret = r.replace(arg=(ctx.range,)+r.arg[1:], tag=None)
|
||||
ctx.range += 1
|
||||
return ret
|
||||
|
||||
def find_bufs(x:UOp):
|
||||
idxs = [s for s in x.toposort(gate=lambda x: x.op is not Ops.AFTER) if s.op is Ops.INDEX]
|
||||
read_from: dict[UOp, Ops] = {}
|
||||
if any((buf:=idx.buf_uop).op in {Ops.BUFFER, Ops.PARAM} and read_from.setdefault(buf, op:=idx.src[0].op) is not op for idx in idxs):
|
||||
raise RuntimeError(f"cycle detected while indexing {buf}")
|
||||
|
||||
to_define_global = PatternMatcher([
|
||||
(UPat(Ops.STORE, name="x"), find_bufs),
|
||||
(UPat((Ops.BUFFER, Ops.MSTACK, Ops.MSELECT), name="buf"), debuf),
|
||||
(UPat(Ops.PARAM, name="v"), lambda v:
|
||||
v.replace(arg=replace(v.arg, slot=-1)) if v.arg.name is not None and v.arg.vmin_vmax is not None and v.arg.slot != -1 else None),
|
||||
@@ -321,7 +335,7 @@ to_define_global = PatternMatcher([
|
||||
|
||||
# bound Variables are stores into Variable buffers: strip the store, the buffer becomes an ALU param via debuf
|
||||
(UPat(Ops.AFTER, name="b"), lambda b: b.src[0] if b.is_bound_var else None),
|
||||
(UPat(Ops.AFTER, name="buf"), lambda ctx,buf: debuf(ctx, buf) if buf.addrspace != AddrSpace.LOCAL else None),
|
||||
(UPat(Ops.AFTER, name="after"), handle_after),
|
||||
|
||||
# remove device from local BUFFERIZE
|
||||
(UPat(Ops.STAGE, name="b"), lambda b: b.replace(arg=replace(b.arg, device=None))),
|
||||
|
||||
+10
-3
@@ -43,6 +43,9 @@ def creation_copy_is_realized(u:UOp):
|
||||
# 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),
|
||||
])
|
||||
@@ -59,7 +62,8 @@ def replace_contig_with_store_after(u:UOp):
|
||||
|
||||
def wrap_tagged_in_contig(x:UOp):
|
||||
if x.tag is None: return None # untouched
|
||||
# An empty tag suppresses retagging without requesting materialization.
|
||||
# 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
|
||||
|
||||
@@ -394,9 +398,8 @@ class Tensor(RandMixin):
|
||||
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")
|
||||
big_sink, becomes_map = transform_to_call(UOp.sink(*[x.uop for x in (self,)+lst]))
|
||||
linear, var_vals = create_linear_with_vars(big_sink)
|
||||
_apply_map_to_tensors(becomes_map, name="buffers")
|
||||
return linear, var_vals
|
||||
return create_linear_with_vars(big_sink)
|
||||
|
||||
def schedule_linear(self, *lst:Tensor) -> UOp:
|
||||
"""Creates the schedule needed to realize these Tensor(s)."""
|
||||
@@ -696,6 +699,10 @@ class Tensor(RandMixin):
|
||||
realized = is_disk or self.uop.base.op is Ops.BUFFER or self.uop._base_buffer_is_realized()
|
||||
if (not self.uop.base.is_realized and self.is_floating_point()) or not (advanced or realized):
|
||||
if not isinstance(v, Tensor): v = Tensor(v, device=self.device, dtype=self.dtype)
|
||||
# __iadd__/__isub__ creates AFTER(view, STORE(view, computed)); unwrap to get the computed value.
|
||||
# the store is self-referential there (the computed value touches its target); clone stores are untouched
|
||||
if v.uop.op is Ops.AFTER and len(v.uop.src) == 2 and (st:=v.uop.src[1]).op is Ops.STORE and \
|
||||
st.src[0] in st.src[1].toposort(enter_calls=False): v = v._apply_uop(lambda x: st.src[1])
|
||||
self.replace(self._getitem(indices, v))
|
||||
elif advanced: # advanced setitem
|
||||
if is_disk: raise RuntimeError("advanced setitem is not supported for DISK tensors")
|
||||
|
||||
+4
-26
@@ -1095,9 +1095,7 @@ 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
|
||||
# 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)
|
||||
if self.dtype in dtypes.floats+dtypes.sints+(dtypes.weakint,): return max(self.dtype.min, smin), min(smax, self.dtype.max)
|
||||
return self.dtype.min, self.dtype.max
|
||||
|
||||
@functools.cached_property
|
||||
@@ -1250,27 +1248,6 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
kernel = fxn(*placeholders).call(*srcs, grad_fxn=grad_fxn)
|
||||
return [s.after(kernel) for s in srcs]
|
||||
|
||||
def call_access(self) -> tuple[tuple[UOp, ...], tuple[UOp, ...]]:
|
||||
body = self.src[0]
|
||||
if body.op is Ops.SINK and not body.op_in_backward_slice_with_self(Ops.CALL, Ops.CUSTOM, Ops.CUSTOMI, Ops.INS):
|
||||
from tinygrad.codegen import pm_add_loads
|
||||
info = ProgramInfo.from_sink(graph_rewrite(body, pm_add_loads))
|
||||
ins, outs = info.ins, info.outs
|
||||
elif body.op is Ops.PROGRAM and isinstance(body.arg, ProgramInfo): ins, outs = body.arg.ins, body.arg.outs
|
||||
elif body.op is Ops.COPY: ins, outs = (1,), (0,)
|
||||
elif body.op is Ops.LINEAR:
|
||||
ins, outs = (tuple(sorted({p.arg.slot for args in group for a in args for p in a.buf_uop.toposort() if p.op is Ops.PARAM}))
|
||||
for group in zip(*(c.call_access() for c in body.src))) if body.src else ((), ())
|
||||
else: raise RuntimeError(f"cannot compute accesses for opaque {body.op}")
|
||||
if any(i < 0 or i >= len(self.src)-1 or (body.op is Ops.PROGRAM and i not in body.arg.globals) for i in (*ins, *outs)):
|
||||
raise RuntimeError("invalid CALL access slot")
|
||||
if body.op is Ops.PROGRAM:
|
||||
bufs = [s.buf_uop for s in self.src[1:]]
|
||||
keys = [b.arg.buffer.base if b.op is Ops.BUFFER and isinstance(b.arg.buffer, Buffer) else b for b in bufs]
|
||||
if any(i != j and keys[i] is keys[j] for i in outs for j in set(ins+outs)):
|
||||
raise RuntimeError("aliased opaque kernel arguments are unsupported")
|
||||
return tuple(self.src[i+1] for i in ins), tuple(self.src[i+1] for i in outs)
|
||||
|
||||
def to_elf(self) -> TinyELF:
|
||||
assert self.op is Ops.PROGRAM and isinstance(self.arg, ProgramInfo), "to_elf should only be called on a PROGRAM ast"
|
||||
params = tuple(u for u in self.src[1].src if u.op is Ops.PARAM and u.addrspace != AddrSpace.ALU)
|
||||
@@ -1325,8 +1302,9 @@ class ProgramInfo:
|
||||
for u in sink.toposort():
|
||||
if u.op is Ops.PARAM and u.addrspace == AddrSpace.ALU: _vars.append(u)
|
||||
if u.op is Ops.PARAM and u.addrspace != AddrSpace.ALU: _globals.append(u.arg.slot)
|
||||
if u.op in (Ops.STORE, Ops.LOAD) and (buf:=u.src[0].buf_uop).op is Ops.PARAM and buf.addrspace is AddrSpace.GLOBAL:
|
||||
(outs if u.op is Ops.STORE else ins).append(buf.arg.slot)
|
||||
if u.op in (Ops.STORE, Ops.LOAD):
|
||||
if (idx:=u.src[0]).op in (Ops.INDEX, Ops.SHRINK) or (u.src[0].op is Ops.CAST and (idx:=u.src[0].src[0]).op is Ops.INDEX):
|
||||
if (buf:=idx.src[0].buf_uop).op is Ops.PARAM: (outs if u.op is Ops.STORE else ins).append(buf.arg.slot)
|
||||
if u.op is Ops.SPECIAL: (local_size if u.arg[0] == 'l' else global_size)[int(u.arg[-1])] = cast(int, u.src[0].ssimplify())
|
||||
return ProgramInfo(sink.arg.name if isinstance(sink.arg, KernelInfo) else "test", tuple(global_size), tuple(local_size),
|
||||
tuple(sorted(dedup(_vars), key=lambda v: v.arg.slot)), tuple(sorted(dedup(_globals))), tuple(sorted(dedup(outs))),
|
||||
|
||||
@@ -34,8 +34,8 @@ def create_bounded(name:str, vmin:int|z3.ArithRef, vmax:int|z3.ArithRef, solver:
|
||||
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])
|
||||
name = f"{x.op.name.lower()}{len(ctx[1])}"
|
||||
return z3.Bool(name, ctx=ctx[0].ctx) if x.dtype == dtypes.bool else create_bounded(name, x.dtype.min, x.dtype.max, ctx[0])
|
||||
# z3 does not model widths: a cast only converts between bool and int
|
||||
def z3_cast(c:UOp, x:z3.ExprRef) -> z3.ExprRef:
|
||||
if (c.src[0].dtype == dtypes.bool) == (c.dtype == dtypes.bool): return x
|
||||
@@ -46,8 +46,11 @@ z3_renderer = PatternMatcher([
|
||||
(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, 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.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),
|
||||
# loads are variables bounded by the min/max of the dtype. non-pointer INDEX is also a LOAD
|
||||
(UPat((Ops.LOAD, Ops.INDEX), name="x"), create_var),
|
||||
# casts, bitcasts and comparisons from floats create new variables
|
||||
(UPat((Ops.CAST, Ops.BITCAST)+tuple(GroupOp.Comparison), src=UPat(dtype=dtypes.floats), name="x"), create_var),
|
||||
# a bitcast between ints wraps into the target range, z3 ints are unbounded
|
||||
(UPat(Ops.BITCAST, dtypes.ints, src=(UPat.var("x", dtypes.ints),), name="c"),
|
||||
|
||||
@@ -44,15 +44,15 @@ pm_commit_weak = PatternMatcher([
|
||||
# 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:
|
||||
# only within the kind is a weak CAST a width statement: across kinds it converts the value, so it commits unless u recasts its srcs anyway
|
||||
def absorb_weak_src(u:UOp, s:UOp) -> UOp:
|
||||
if s.op is not Ops.CAST or s.dtype not in dtypes.weaks: return s
|
||||
if 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]
|
||||
if u.op in _lower_weak_ops or u.op is Ops.CAST or weak_dtype(s.src[0].dtype) is s.dtype: return s.src[0]
|
||||
return s.src[0].cast(s.commit_dtype(dtypes.int))
|
||||
|
||||
def lower_weak_node(u:UOp) -> UOp|None:
|
||||
if u.op is Ops.CAST and u.src[0].op is Ops.CONST: return None # a committed const, not a consumer
|
||||
src = tuple(absorb_weak_src(s) for s in u.src)
|
||||
src = tuple(absorb_weak_src(u, s) for s in u.src)
|
||||
if derived_dtypes(u, src) is None:
|
||||
src = tuple(s.ccast(s.commit_dtype(dtypes.int)) if s.op is Ops.CONST and s.dtype in dtypes.weaks else s for s in src)
|
||||
if src == u.src: return None
|
||||
|
||||
Vendored
-14
File diff suppressed because one or more lines are too long
@@ -12,4 +12,3 @@ 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"
|
||||
|
||||
Reference in New Issue
Block a user