forked from tinygrad/tinygrad
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a164ec328c | ||
|
|
0c31a8f63b |
@@ -66,14 +66,14 @@ runs:
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: ${{ github.workspace }}/.venv
|
||||
key: venv-${{ runner.os }}-${{ runner.arch }}-python-${{ steps.setup-python.outputs.python-version }}-${{ inputs.deps }}-${{ inputs.pydeps }}-${{ env.CACHE_VERSION }}
|
||||
key: venv-${{ runner.os }}-python-${{ steps.setup-python.outputs.python-version }}-${{ inputs.deps }}-${{ inputs.pydeps }}-${{ env.CACHE_VERSION }}
|
||||
- name: Cache Python packages
|
||||
if: github.event_name != 'pull_request'
|
||||
id: restore-venv
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ github.workspace }}/.venv
|
||||
key: venv-${{ runner.os }}-${{ runner.arch }}-python-${{ steps.setup-python.outputs.python-version }}-${{ inputs.deps }}-${{ inputs.pydeps }}-${{ env.CACHE_VERSION }}
|
||||
key: venv-${{ runner.os }}-python-${{ steps.setup-python.outputs.python-version }}-${{ inputs.deps }}-${{ inputs.pydeps }}-${{ env.CACHE_VERSION }}
|
||||
|
||||
# **** Caching downloads ****
|
||||
|
||||
@@ -199,13 +199,13 @@ runs:
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: /var/cache/apt/archives/
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-apt-${{ steps.apt-pkgs.outputs.hash }}-${{ env.CACHE_VERSION }}
|
||||
key: ${{ runner.os }}-apt-${{ steps.apt-pkgs.outputs.hash }}-${{ env.CACHE_VERSION }}
|
||||
- name: Cache apt
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.cuda == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true') && github.event_name != 'pull_request'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /var/cache/apt/archives/
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-apt-${{ steps.apt-pkgs.outputs.hash }}-${{ env.CACHE_VERSION }}
|
||||
key: ${{ runner.os }}-apt-${{ steps.apt-pkgs.outputs.hash }}-${{ env.CACHE_VERSION }}
|
||||
|
||||
- name: Run apt Update + Install
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.cuda == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true')
|
||||
|
||||
@@ -1282,7 +1282,7 @@ def train_bert():
|
||||
previous_step = i
|
||||
|
||||
def train_llama3():
|
||||
from examples.mlperf.models.llama import Transformer
|
||||
from extra.models.llama import Transformer
|
||||
from examples.llama3 import MODEL_PARAMS
|
||||
from examples.mlperf.lr_schedulers import CosineAnnealingLRWithWarmup
|
||||
from examples.mlperf.optim import GradAccClipAdamW
|
||||
@@ -1343,7 +1343,7 @@ def train_llama3():
|
||||
if (MP := getenv("MP", 1)) > 1: model_params['vocab_size'] = round_up(model_params['vocab_size'], 256 * MP)
|
||||
vocab_mask:Tensor = Tensor.arange(model_params['vocab_size']).reshape(1, 1, -1) >= real_vocab_size
|
||||
|
||||
model = Transformer(**model_params, max_context=SEQLEN)
|
||||
model = Transformer(**model_params, max_context=SEQLEN, jit=False, disable_kv_cache=True)
|
||||
|
||||
params = get_parameters(model)
|
||||
# weights are all bfloat16 for now
|
||||
@@ -1417,7 +1417,7 @@ def train_llama3():
|
||||
device = tuple(f"{Device.DEFAULT}:{i}" for i in range(MP))
|
||||
tokens = tokens.shard(device)
|
||||
if DP == 1 and MP == 1: tokens = tokens.to(None)
|
||||
logits:Tensor = model(tokens[:, :-1])
|
||||
logits:Tensor = model(tokens[:, :-1], start_pos=0, temperature=math.nan)
|
||||
loss = vocab_mask.where(-1e9, logits).sparse_categorical_crossentropy(tokens[:, 1:])
|
||||
loss.backward()
|
||||
assert all(p.grad is g for p,g in zip(optim.params, grads))
|
||||
@@ -1449,7 +1449,7 @@ def train_llama3():
|
||||
device = tuple(f"{Device.DEFAULT}:{i}" for i in range(MP))
|
||||
tokens = tokens.shard(device)
|
||||
if DP == 1 and MP == 1: tokens = tokens.to(None)
|
||||
logits:Tensor = model(tokens[:, :-1])
|
||||
logits:Tensor = model(tokens[:, :-1], start_pos=0, temperature=math.nan)
|
||||
loss = vocab_mask.where(-1e9, logits).sparse_categorical_crossentropy(tokens[:, 1:])
|
||||
return loss.flatten().float().to("CPU")
|
||||
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
from tinygrad import Tensor, nn
|
||||
from tinygrad.helpers import getenv
|
||||
from extra.models.llama import apply_rotary_emb, precompute_freqs_cis
|
||||
|
||||
class Attention:
|
||||
def __init__(self, dim:int, n_heads:int, n_kv_heads:int|None=None, linear=nn.Linear):
|
||||
self.n_heads = n_heads
|
||||
self.n_kv_heads = n_kv_heads if n_kv_heads is not None else n_heads # n_kv_heads != n_heads implies MQA [arxiv/2307.09288, A.2.1]
|
||||
self.head_dim = dim // n_heads
|
||||
self.n_rep = self.n_heads // self.n_kv_heads
|
||||
|
||||
if getenv("WQKV"):
|
||||
self.wqkv = linear(dim, self.n_heads * self.head_dim + self.n_kv_heads * self.head_dim * 2, bias=False)
|
||||
else:
|
||||
self.wq = linear(dim, self.n_heads * self.head_dim, bias=False)
|
||||
self.wk = linear(dim, self.n_kv_heads * self.head_dim, bias=False)
|
||||
self.wv = linear(dim, self.n_kv_heads * self.head_dim, bias=False)
|
||||
|
||||
self.wo = linear(self.n_heads * self.head_dim, dim, bias=False)
|
||||
|
||||
def __call__(self, x:Tensor, freqs_cis:Tensor) -> Tensor:
|
||||
if getenv("WQKV"):
|
||||
xqkv = self.wqkv(x)
|
||||
xqkv = xqkv.reshape(xqkv.shape[0], xqkv.shape[1], self.n_kv_heads, self.n_rep + 2, self.head_dim)
|
||||
xq = xqkv[:, :, :, :self.n_rep].reshape(xqkv.shape[0], xqkv.shape[1], -1)
|
||||
xk = xqkv[:, :, :, self.n_rep:self.n_rep+1].reshape(xqkv.shape[0], xqkv.shape[1], -1)
|
||||
xv = xqkv[:, :, :, self.n_rep+1:self.n_rep+2].reshape(xqkv.shape[0], xqkv.shape[1], -1)
|
||||
else:
|
||||
xq, xk, xv = self.wq(x), self.wk(x), self.wv(x)
|
||||
|
||||
xq = xq.reshape(xq.shape[0], xq.shape[1], self.n_heads, self.head_dim)
|
||||
xk = xk.reshape(xk.shape[0], xk.shape[1], self.n_kv_heads, self.head_dim)
|
||||
xv = xv.reshape(xv.shape[0], xv.shape[1], self.n_kv_heads, self.head_dim)
|
||||
|
||||
xq, xk = apply_rotary_emb(xq, xk, freqs_cis)
|
||||
bsz, seqlen, _, _ = xq.shape
|
||||
|
||||
xq, xk, xv = xq.transpose(1, 2), xk.transpose(1, 2), xv.transpose(1, 2)
|
||||
attn = xq.scaled_dot_product_attention(xk, xv, is_causal=True, enable_gqa=True).transpose(1, 2)
|
||||
|
||||
attn = attn.reshape(bsz, seqlen, -1)
|
||||
return self.wo(attn)
|
||||
|
||||
class FeedForward:
|
||||
def __init__(self, dim:int, hidden_dim:int, linear=nn.Linear):
|
||||
self.w1 = linear(dim, hidden_dim, bias=False)
|
||||
self.w2 = linear(hidden_dim, dim, bias=False)
|
||||
self.w3 = linear(dim, hidden_dim, bias=False) # the gate in Gated Linear Unit
|
||||
|
||||
def __call__(self, x:Tensor) -> Tensor:
|
||||
w1 = self.w1(x).silu()
|
||||
w3 = self.w3(x)
|
||||
return self.w2(w1 * w3)
|
||||
|
||||
class TransformerBlock:
|
||||
def __init__(self, dim:int, hidden_dim:int, n_heads:int, n_kv_heads:int|None, norm_eps:float, linear=nn.Linear):
|
||||
self.attention = Attention(dim, n_heads, n_kv_heads, linear)
|
||||
self.feed_forward = FeedForward(dim, hidden_dim, linear)
|
||||
self.attention_norm = nn.RMSNorm(dim, norm_eps)
|
||||
self.ffn_norm = nn.RMSNorm(dim, norm_eps)
|
||||
|
||||
def __call__(self, x:Tensor, freqs_cis:Tensor):
|
||||
h = x + self.attention(self.attention_norm(x), freqs_cis)
|
||||
return h + self.feed_forward(self.ffn_norm(h))
|
||||
|
||||
class Transformer:
|
||||
def __init__(self, dim:int, hidden_dim:int, n_heads:int, n_layers:int, norm_eps:float, vocab_size:int, n_kv_heads:int|None=None,
|
||||
rope_theta:int=10000, max_context:int=1024, linear=nn.Linear, embedding=nn.Embedding):
|
||||
self.layers = [TransformerBlock(dim, hidden_dim, n_heads, n_kv_heads, norm_eps, linear) for _ in range(n_layers)]
|
||||
self.norm = nn.RMSNorm(dim, norm_eps)
|
||||
self.tok_embeddings = embedding(vocab_size, dim)
|
||||
self.output = nn.Linear(dim, vocab_size, bias=False) if embedding == nn.Embedding else linear(dim, vocab_size, bias=False)
|
||||
self.freqs_cis = precompute_freqs_cis(dim // n_heads, max_context * 2, rope_theta).contiguous().requires_grad_(False)
|
||||
|
||||
def __call__(self, tokens:Tensor):
|
||||
h = self.tok_embeddings(tokens)
|
||||
freqs_cis = self.freqs_cis.cast(h.dtype)[:, :tokens.shape[1], :, :, :]
|
||||
for layer in self.layers: h = layer(h, freqs_cis)
|
||||
logits = self.output(self.norm(h))
|
||||
return logits
|
||||
@@ -65,7 +65,7 @@ def get_bar0_size(pcibus):
|
||||
class AMSMI(AMDev):
|
||||
def __init__(self, pcibus, vram_bar:MMIOInterface, doorbell_bar:MMIOInterface, mmio_bar:MMIOInterface):
|
||||
self.pcibus = pcibus
|
||||
self.vram, self.doorbell64, self.mmio = vram_bar, doorbell_bar, mmio_bar
|
||||
self.vram, self.doorbell64, self.mmio, self.dma_regions = vram_bar, doorbell_bar, mmio_bar, None
|
||||
self.pci_state = self.read_pci_state()
|
||||
if self.pci_state == "D0": self._init_from_d0()
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ class GFXFake:
|
||||
def __init__(self): self.xccs = 8
|
||||
|
||||
class AMDFake(AMDev):
|
||||
def __init__(self, pci_dev):
|
||||
self.pci_dev, self.devfmt = pci_dev, pci_dev.pcibus
|
||||
def __init__(self, pci_dev, dma_regions=None):
|
||||
self.pci_dev, self.devfmt, self.dma_regions = pci_dev, pci_dev.pcibus, dma_regions
|
||||
self.vram, self.doorbell64, self.mmio = self.pci_dev.map_bar(0), self.pci_dev.map_bar(2, fmt='Q'), self.pci_dev.map_bar(5, fmt='I')
|
||||
self._run_discovery()
|
||||
self._build_regs()
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
from tinygrad import UOp, getenv
|
||||
from tinygrad.uop.ops import AxisType, KernelInfo, Ops
|
||||
from tinygrad.dtype import AddrSpace, dtypes
|
||||
|
||||
N = getenv("N", 4096)
|
||||
M = getenv("M", N)
|
||||
K = getenv("K", N)
|
||||
|
||||
WARP_SIZE = 32
|
||||
BLOCK_M, BLOCK_N, BLOCK_K = 128, 128, 8
|
||||
TM, TN = 4, 4
|
||||
LANES_PER_WAVE_M, LANES_PER_WAVE_N = 4, 8
|
||||
assert N % BLOCK_N == 0 and M % BLOCK_M == 0 and K % BLOCK_K == 0
|
||||
|
||||
is_kernel5 = getenv("K5", 0)
|
||||
THREADS_PER_BLOCK = 128 if is_kernel5 else 256
|
||||
WAVES_PER_BLOCK_N = 1 if is_kernel5 else 2
|
||||
WAVES_PER_BLOCK_M = THREADS_PER_BLOCK // WARP_SIZE // WAVES_PER_BLOCK_N
|
||||
REG_TILES_PER_WAVE_N = BLOCK_N // (WAVES_PER_BLOCK_N * LANES_PER_WAVE_N * TN)
|
||||
REG_TILES_PER_WAVE_M = BLOCK_M // (WAVES_PER_BLOCK_M * LANES_PER_WAVE_M * TM)
|
||||
|
||||
assert WAVES_PER_BLOCK_M*REG_TILES_PER_WAVE_M*LANES_PER_WAVE_M*TM == BLOCK_M, "M reshape is wrong"
|
||||
assert WAVES_PER_BLOCK_N*REG_TILES_PER_WAVE_N*LANES_PER_WAVE_N*TN == BLOCK_N, "N reshape is wrong"
|
||||
|
||||
consts = {"wpb_m":WAVES_PER_BLOCK_M, "lpw_m":LANES_PER_WAVE_M, "rt_m":REG_TILES_PER_WAVE_M, "t_m": TM,
|
||||
"wpb_n":WAVES_PER_BLOCK_N, "lpw_n":LANES_PER_WAVE_N, "rt_n":REG_TILES_PER_WAVE_N, "t_n": TN}
|
||||
|
||||
# 128x128 out, kx128, kx128 in
|
||||
def block_128x128_gemm(c:UOp, a:UOp, b:UOp) -> UOp:
|
||||
tid = UOp.range(THREADS_PER_BLOCK, 2, AxisType.LOCAL)
|
||||
|
||||
#tid = UOp.special(THREADS_PER_BLOCK, "lidx0")
|
||||
warp, lane = tid // WARP_SIZE, tid % WARP_SIZE
|
||||
wave_n, wave_m = warp % WAVES_PER_BLOCK_N, warp // WAVES_PER_BLOCK_N
|
||||
lane_n, lane_m = lane % LANES_PER_WAVE_N, lane // LANES_PER_WAVE_N
|
||||
|
||||
# define locals
|
||||
A_local = UOp.placeholder((BLOCK_K, BLOCK_M), dtypes.float, slot=0, addrspace=AddrSpace.LOCAL)
|
||||
B_local = UOp.placeholder((BLOCK_K, BLOCK_N), dtypes.float, slot=1, addrspace=AddrSpace.LOCAL)
|
||||
|
||||
# open the main reduction range and copy in GLOBAL -> LOCAL
|
||||
a = a.reshape(K // BLOCK_K, BLOCK_K, BLOCK_M)
|
||||
b = b.reshape(K // BLOCK_K, BLOCK_K, BLOCK_N)
|
||||
k_tile_range = UOp.range(K // BLOCK_K, 3, AxisType.REDUCE)
|
||||
A_store = A_local.reshape(-1, THREADS_PER_BLOCK)[:, tid].store(a[k_tile_range].reshape(-1, THREADS_PER_BLOCK)[:, tid])
|
||||
B_store = B_local.reshape(-1, THREADS_PER_BLOCK)[:, tid].store(b[k_tile_range].reshape(-1, THREADS_PER_BLOCK)[:, tid])
|
||||
barrier = UOp.barrier(A_store, B_store)
|
||||
A_local, B_local = A_local.after(barrier), B_local.after(barrier)
|
||||
|
||||
# define accumulator (128x128), but broadcast across tid
|
||||
c_regs = UOp.placeholder((REG_TILES_PER_WAVE_M*TM, REG_TILES_PER_WAVE_N*TN), dtypes.float, slot=2, addrspace=AddrSpace.REG)
|
||||
c_regs = c_regs.after(c_regs.store(UOp.const(dtypes.float, 0).reshape((1,)*len(c_regs.shape)).expand(c_regs.shape)))
|
||||
|
||||
# define registers (NOTE: the thread count is the device count for this multi, it's sharded across the THREADS_PER_BLOCK)
|
||||
A_col = UOp.placeholder((REG_TILES_PER_WAVE_M, TM), dtypes.float, slot=0, addrspace=AddrSpace.REG)
|
||||
B_row = UOp.placeholder((REG_TILES_PER_WAVE_N, TN), dtypes.float, slot=1, addrspace=AddrSpace.REG)
|
||||
|
||||
# LOCAL -> REGS
|
||||
k = UOp.range(BLOCK_K, 4, AxisType.REDUCE)
|
||||
A_col = A_col.after(A_col.store(A_local[k].reshape(WAVES_PER_BLOCK_M, REG_TILES_PER_WAVE_M, LANES_PER_WAVE_M, TM)[wave_m, :, lane_m, :]))
|
||||
B_row = B_row.after(B_row.store(B_local[k].reshape(WAVES_PER_BLOCK_N, REG_TILES_PER_WAVE_N, LANES_PER_WAVE_N, TN)[wave_n, :, lane_n, :]))
|
||||
|
||||
# do FMA
|
||||
A_col = A_col.reshape(REG_TILES_PER_WAVE_M*TM, 1).expand(REG_TILES_PER_WAVE_M*TM, REG_TILES_PER_WAVE_N*TN)
|
||||
B_row = B_row.reshape(1, REG_TILES_PER_WAVE_N*TN).expand(REG_TILES_PER_WAVE_M*TM, REG_TILES_PER_WAVE_N*TN)
|
||||
c_regs = c_regs.after(c_regs.store(c_regs.after(k) + (A_col * B_row)).end(k).barrier().end(k_tile_range))
|
||||
|
||||
# store back to c
|
||||
c_store = c.rearrange("(wpb_m rt_m lpw_m t_m) (wpb_n rt_n lpw_n t_n) -> (wpb_m wpb_n lpw_m lpw_n) (rt_m t_m) (rt_n t_n)", **consts)
|
||||
return c_store[tid].store(c_regs).end(tid)
|
||||
|
||||
def amd_copy_matmul(c:UOp, a:UOp, b:UOp) -> UOp:
|
||||
block_id_n = UOp.range(N // BLOCK_N, 0, AxisType.GLOBAL)
|
||||
block_id_m = UOp.range(M // BLOCK_M, 1, AxisType.GLOBAL)
|
||||
|
||||
# index the output with the globals
|
||||
c = c.reshape(M // BLOCK_M, BLOCK_M, N // BLOCK_N, BLOCK_N)[block_id_m, :, block_id_n, :]
|
||||
a = a.T.reshape(K, M // BLOCK_M, BLOCK_M)[:, block_id_m, :]
|
||||
b = b.reshape(K, N // BLOCK_N, BLOCK_N)[:, block_id_n, :]
|
||||
return block_128x128_gemm(c, a, b).end(block_id_n, block_id_m).sink(arg=KernelInfo(opts_to_apply=()))
|
||||
|
||||
if __name__ == "__main__":
|
||||
from amd_uop_matmul import eval_custom_matmul
|
||||
eval_custom_matmul(amd_copy_matmul)
|
||||
@@ -115,7 +115,7 @@ def hand_spec_kernel3(c:UOp, a:UOp, b:UOp) -> UOp:
|
||||
|
||||
return sink.sink(arg=KernelInfo(opts_to_apply=())).simplify()
|
||||
|
||||
def eval_custom_matmul(fxn):
|
||||
if __name__ == "__main__":
|
||||
a = Tensor.randn(M, K, dtype=dtypes.float)
|
||||
b = Tensor.randn(K, N, dtype=dtypes.float)
|
||||
c = Tensor.empty(M, N, dtype=dtypes.float)
|
||||
@@ -125,7 +125,7 @@ def eval_custom_matmul(fxn):
|
||||
with Context(DEBUG=max(2, DEBUG.value)):
|
||||
for _ in range(NUM_RUNS):
|
||||
GlobalCounters.reset()
|
||||
tst = Tensor.custom_kernel(c, a, b, fxn=fxn)[0].realize()
|
||||
tst = Tensor.custom_kernel(c, a, b, fxn=hand_spec_kernel3)[0].realize()
|
||||
ets.append(GlobalCounters.time_sum_s)
|
||||
print(f"REAL TFLOPS {M * N * K * 2 / min(ets) * 1e-12:.2f}")
|
||||
|
||||
@@ -138,6 +138,3 @@ def eval_custom_matmul(fxn):
|
||||
print(f"mean squared error {err}")
|
||||
if err > 1e-06:
|
||||
raise RuntimeError("matmul is wrong!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
eval_custom_matmul(hand_spec_kernel3)
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
import atexit, functools
|
||||
from tinygrad import Tensor, Device, dtypes
|
||||
from tinygrad.dtype import AddrSpace
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo, AxisType
|
||||
from tinygrad.renderer import Estimates
|
||||
from tinygrad.helpers import getenv, all_same, DEBUG
|
||||
from tinygrad.runtime.autogen.amd.cdna.ins import *
|
||||
|
||||
# ** CDNA4 assembly gemm
|
||||
|
||||
WORKGROUP_SIZE = 256
|
||||
from tinygrad.dtype import dtypes
|
||||
|
||||
# M0 is encoded with 124 (NULL in RDNA) in CDNA
|
||||
M0 = NULL
|
||||
@@ -2606,117 +2597,3 @@ def build_kernel(batch, M, N, K, dtype):
|
||||
k.label('KernelEnd')
|
||||
k.emit(s_endpgm())
|
||||
return k.finalize()
|
||||
|
||||
# ** ASM_GEMM custom kernel
|
||||
|
||||
@functools.cache
|
||||
def custom_asm_gemm(C:UOp, A:UOp, B:UOp, dname:str) -> UOp:
|
||||
batch, M, K = A.shape
|
||||
K2, N = B.shape[(1 if B.ndim == 3 else 0):]
|
||||
assert K == K2
|
||||
lidx = UOp.special(WORKGROUP_SIZE, "lidx0")
|
||||
gidx = UOp.special(NUM_WG, "gidx0")
|
||||
insts = build_kernel(batch, M, N, K, A.dtype.base)
|
||||
lds = UOp(Ops.DEFINE_LOCAL, dtypes.uint8.ptr(size=133_120, addrspace=AddrSpace.LOCAL), (), 'lds')
|
||||
sink = UOp.sink(C.base, A.base, B.base, lds, lidx, gidx,
|
||||
arg=KernelInfo(name=f"gemm_{batch}_{M}_{N}_{K}", estimates=Estimates(ops=2*batch*M*N*K, mem=(batch*M*K + K*N + batch*M*N)*2)))
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.DEVICE, arg=dname),
|
||||
UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=x) for x in insts]))))
|
||||
|
||||
counters = {"used":0, "todos":[]}
|
||||
def todo(msg:str) -> bool: counters["todos"].append(msg); return False
|
||||
def _asm_gemm_report():
|
||||
print(f'asm_gemm: {counters["used"]} used, {len(counters["todos"])} not used')
|
||||
if DEBUG >= 2 and counters["todos"]:
|
||||
from collections import Counter
|
||||
for msg, cnt in Counter(counters["todos"]).most_common(): print(f' {cnt:3d}x {msg}')
|
||||
atexit.register(_asm_gemm_report)
|
||||
|
||||
def can_use_asm_gemm(a:Tensor, b:Tensor) -> bool:
|
||||
if a.dtype != b.dtype: return todo(f"dtypes must match {a.dtype} != {b.dtype}")
|
||||
if a.dtype not in {dtypes.bfloat16, dtypes.float16}: return todo(f"only bfloat16/float16, got {a.dtype}")
|
||||
batch, M, K = (1, *a.shape) if a.ndim == 2 else a.shape
|
||||
N = b.shape[1]
|
||||
if isinstance(a.device, tuple):
|
||||
if a.ndim == 2 and a.uop.axis == 0 and b.uop.axis is None: M //= len(a.device)
|
||||
elif a.ndim == 2 and a.uop.axis == 1 and b.uop.axis == 0: K //= len(a.device)
|
||||
elif a.ndim == 2 and a.uop.axis is None and b.uop.axis == 1: N //= len(a.device)
|
||||
elif a.ndim == 3 and a.uop.axis == 0 and b.uop.axis is None: batch //= len(a.device)
|
||||
elif a.ndim == 3 and a.uop.axis is None and b.uop.axis == 1: N //= len(a.device)
|
||||
elif a.ndim == 3 and a.uop.axis == 2 and b.uop.axis == 0: K //= len(a.device)
|
||||
else: return todo(f"sharding mismatch a.ndim={a.ndim} a.uop.axis={a.uop.axis} b.uop.axis={b.uop.axis}")
|
||||
dname = a.device[0]
|
||||
else: dname = a.device
|
||||
arch = getattr(Device[dname].renderer, "arch", "")
|
||||
if batch not in {1, 2}: return todo(f"GEMM batch size {batch}")
|
||||
if (M % TILE_M != 0 or N % TILE_N != 0 or K % TILE_K != 0) and arch == "gfx950":
|
||||
return todo(f"GEMM shape ({M},{N},{K}) not a multiple of ({TILE_M},{TILE_N},{TILE_K})")
|
||||
return True
|
||||
|
||||
# ** UOp gemm to test Tensor.custom_kernel multi and backward correctness on non cdna4
|
||||
# note: this can be removed after we have GEMM on mixins
|
||||
|
||||
def custom_uop_gemm(C:UOp, A:UOp, B:UOp) -> UOp:
|
||||
M, K = A.shape[0]*A.shape[1], A.shape[2]
|
||||
K2, N = B.shape[(1 if B.ndim == 3 else 0):]
|
||||
assert K == K2
|
||||
m = UOp.range(M, 1, AxisType.LOOP)
|
||||
n = UOp.range(N, 2, AxisType.LOOP)
|
||||
k = UOp.range(K, 0, AxisType.REDUCE)
|
||||
mul = (A.flatten().index((m*UOp.const(dtypes.index, K)+k))*
|
||||
B.flatten().index((k*UOp.const(dtypes.index, N)+n))).cast(dtypes.float32)
|
||||
red = mul.reduce(k, arg=Ops.ADD, dtype=dtypes.float32).cast(C.dtype.base)
|
||||
store = C.flatten().index((m*UOp.const(dtypes.index, N)+n), ptr=True).store(red).end(m, n)
|
||||
return store.sink(arg=KernelInfo(name=f'uop_gemm_{M}_{N}_{K}'))
|
||||
|
||||
# ** backward gemm, might use the asm gemm
|
||||
|
||||
def custom_gemm_bw(gradient:UOp, kernel:UOp):
|
||||
out, a, b = kernel.src[1:]
|
||||
assert all_same([gradient.device, a.device, b.device, out.device])
|
||||
a_t, b_t, g_t = Tensor(a, device=a.device), Tensor(b, device=a.device), Tensor(gradient, device=a.device)
|
||||
# TODO: this needs to be cleaned up and done properly, the batch dim of grad and a multi need to align
|
||||
g_t = g_t[:a.shape[0]]
|
||||
grad_a = (g_t @ b_t.T).uop
|
||||
grad_b = (a_t.permute(2, 0, 1).reshape(a_t.shape[2], -1) @ g_t.reshape(-1, g_t.shape[-1])).uop
|
||||
return (None, grad_a, grad_b)
|
||||
|
||||
# ** main gemm function
|
||||
|
||||
def asm_gemm(a:Tensor, b:Tensor) -> Tensor:
|
||||
assert can_use_asm_gemm(a, b), f"{counters['todos'][-1]}"
|
||||
counters["used"] += 1
|
||||
unfold_batch = a.ndim == 3 and isinstance(a.device, tuple) and a.uop.axis == 2 and b.uop.axis == 0
|
||||
if unfold_batch:
|
||||
orig_batch = a.shape[0]
|
||||
a = a.reshape(a.shape[0]*a.shape[1], a.shape[2])
|
||||
squeeze = a.ndim == 2
|
||||
if squeeze: a = a.unsqueeze(0)
|
||||
|
||||
batch, M, K = a.shape
|
||||
N = b.shape[1]
|
||||
is_multi = isinstance(a.device, tuple)
|
||||
if (k_sharded:=is_multi and a.uop.axis == 2): K //= len(a.device)
|
||||
if (m_sharded:=is_multi and a.uop.axis == 1): M //= len(a.device)
|
||||
n_sharded = is_multi and b.uop.axis == 1
|
||||
|
||||
if is_multi:
|
||||
if n_sharded:
|
||||
out = Tensor(Tensor.empty(batch, M, N//len(a.device), dtype=a.dtype, device=a.device).uop.multi(2), device=a.device)
|
||||
elif m_sharded:
|
||||
out = Tensor(Tensor.empty(batch, M, N, dtype=a.dtype, device=a.device).uop.multi(1), device=a.device)
|
||||
else:
|
||||
out = Tensor(Tensor.empty(batch//len(a.device) if a.uop.axis==0 else batch, M, N, dtype=a.dtype, device=a.device).uop.multi(0), device=a.device)
|
||||
else:
|
||||
out = Tensor.empty(batch, M, N, dtype=a.dtype, device=a.device)
|
||||
|
||||
renderer = Device[a.device[0] if is_multi else a.device].renderer
|
||||
dname, arch = renderer.device, getattr(renderer, "arch", "")
|
||||
if arch.startswith("gfx950") and getenv("USE_ASM", 1):
|
||||
out = Tensor.custom_kernel(out, a, b, fxn=functools.partial(custom_asm_gemm, dname=dname), grad_fxn=custom_gemm_bw)[0]
|
||||
else:
|
||||
out = Tensor.custom_kernel(out, a, b, fxn=custom_uop_gemm, grad_fxn=custom_gemm_bw)[0]
|
||||
if k_sharded: out = out.sum(0)
|
||||
out = out.squeeze(0) if squeeze else out
|
||||
if unfold_batch: out = out.reshape(orig_batch, -1, out.shape[-1])
|
||||
return out
|
||||
@@ -0,0 +1,122 @@
|
||||
import atexit, functools
|
||||
from tinygrad import Tensor, Device, dtypes
|
||||
from tinygrad.dtype import AddrSpace
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo, AxisType
|
||||
from tinygrad.renderer import Estimates
|
||||
from tinygrad.helpers import getenv, all_same, DEBUG
|
||||
from extra.gemm.asm.cdna.asm import build_kernel, TILE_M, TILE_N, TILE_K, NUM_WG
|
||||
|
||||
# ** CDNA4 assembly gemm
|
||||
|
||||
WORKGROUP_SIZE = 256
|
||||
|
||||
@functools.cache
|
||||
def custom_asm_gemm(C:UOp, A:UOp, B:UOp, dname:str) -> UOp:
|
||||
batch, M, K = A.shape
|
||||
K2, N = B.shape[(1 if B.ndim == 3 else 0):]
|
||||
assert K == K2
|
||||
lidx = UOp.special(WORKGROUP_SIZE, "lidx0")
|
||||
gidx = UOp.special(NUM_WG, "gidx0")
|
||||
insts = build_kernel(batch, M, N, K, A.dtype.base)
|
||||
lds = UOp(Ops.DEFINE_LOCAL, dtypes.uint8.ptr(size=133_120, addrspace=AddrSpace.LOCAL), (), 'lds')
|
||||
sink = UOp.sink(C.base, A.base, B.base, lds, lidx, gidx,
|
||||
arg=KernelInfo(name=f"gemm_{batch}_{M}_{N}_{K}", estimates=Estimates(ops=2*batch*M*N*K, mem=(batch*M*K + K*N + batch*M*N)*2)))
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.DEVICE, arg=dname),
|
||||
UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=x) for x in insts]))))
|
||||
|
||||
counters = {"used":0, "todos":[]}
|
||||
def todo(msg:str) -> bool: counters["todos"].append(msg); return False
|
||||
def _asm_gemm_report():
|
||||
print(f'asm_gemm: {counters["used"]} used, {len(counters["todos"])} not used')
|
||||
if DEBUG >= 2 and counters["todos"]:
|
||||
from collections import Counter
|
||||
for msg, cnt in Counter(counters["todos"]).most_common(): print(f' {cnt:3d}x {msg}')
|
||||
atexit.register(_asm_gemm_report)
|
||||
|
||||
def can_use_asm_gemm(a:Tensor, b:Tensor) -> bool:
|
||||
if a.dtype != b.dtype: return todo(f"dtypes must match {a.dtype} != {b.dtype}")
|
||||
if a.dtype not in {dtypes.bfloat16, dtypes.float16}: return todo(f"only bfloat16/float16, got {a.dtype}")
|
||||
batch, M, K = (1, *a.shape) if a.ndim == 2 else a.shape
|
||||
N = b.shape[1]
|
||||
if isinstance(a.device, tuple):
|
||||
if a.ndim == 2 and a.uop.axis == 0 and b.uop.axis is None: M //= len(a.device)
|
||||
elif a.ndim == 2 and a.uop.axis == 1 and b.uop.axis == 0: K //= len(a.device)
|
||||
elif a.ndim == 2 and a.uop.axis is None and b.uop.axis == 1: N //= len(a.device)
|
||||
elif a.ndim == 3 and a.uop.axis == 0 and b.uop.axis is None: batch //= len(a.device)
|
||||
elif a.ndim == 3 and a.uop.axis is None and b.uop.axis == 1: N //= len(a.device)
|
||||
elif a.ndim == 3 and a.uop.axis == 2 and b.uop.axis == 0: K //= len(a.device)
|
||||
else: return todo(f"sharding mismatch a.ndim={a.ndim} a.uop.axis={a.uop.axis} b.uop.axis={b.uop.axis}")
|
||||
dname = a.device[0]
|
||||
else: dname = a.device
|
||||
arch = getattr(Device[dname].renderer, "arch", "")
|
||||
if batch not in {1, 2}: return todo(f"GEMM batch size {batch}")
|
||||
if (M % TILE_M != 0 or N % TILE_N != 0 or K % TILE_K != 0) and arch == "gfx950":
|
||||
return todo(f"GEMM shape ({M},{N},{K}) not a multiple of ({TILE_M},{TILE_N},{TILE_K})")
|
||||
return True
|
||||
|
||||
# ** UOp gemm to test Tensor.custom_kernel multi and backward correctness on non cdna4
|
||||
# note: this can be removed after we have GEMM on mixins
|
||||
|
||||
def custom_uop_gemm(C:UOp, A:UOp, B:UOp) -> UOp:
|
||||
M, K = A.shape[0]*A.shape[1], A.shape[2]
|
||||
K2, N = B.shape[(1 if B.ndim == 3 else 0):]
|
||||
assert K == K2
|
||||
m = UOp.range(M, 1, AxisType.LOOP)
|
||||
n = UOp.range(N, 2, AxisType.LOOP)
|
||||
k = UOp.range(K, 0, AxisType.REDUCE)
|
||||
mul = (A.index((m*UOp.const(dtypes.index, K)+k))*B.index((k*UOp.const(dtypes.index, N)+n))).cast(dtypes.float32)
|
||||
red = mul.reduce(k, arg=Ops.ADD, dtype=dtypes.float32).cast(C.dtype.base)
|
||||
store = C.index((m*UOp.const(dtypes.index, N)+n), ptr=True).store(red).end(m, n)
|
||||
return store.sink(arg=KernelInfo(name=f'uop_gemm_{M}_{N}_{K}'))
|
||||
|
||||
# ** backward gemm, might use the asm gemm
|
||||
|
||||
def custom_gemm_bw(gradient:UOp, kernel:UOp):
|
||||
out, a, b = kernel.src[1:]
|
||||
assert all_same([gradient.device, a.device, b.device, out.device])
|
||||
a_t, b_t, g_t = Tensor(a, device=a.device), Tensor(b, device=a.device), Tensor(gradient, device=a.device)
|
||||
# TODO: this needs to be cleaned up and done properly, the batch dim of grad and a multi need to align
|
||||
g_t = g_t[:a.shape[0]]
|
||||
grad_a = (g_t @ b_t.T).uop
|
||||
grad_b = (a_t.permute(2, 0, 1).reshape(a_t.shape[2], -1) @ g_t.reshape(-1, g_t.shape[-1])).uop
|
||||
return (None, grad_a, grad_b)
|
||||
|
||||
# ** main gemm function
|
||||
|
||||
def asm_gemm(a:Tensor, b:Tensor) -> Tensor:
|
||||
assert can_use_asm_gemm(a, b), f"{counters['todos'][-1]}"
|
||||
counters["used"] += 1
|
||||
unfold_batch = a.ndim == 3 and isinstance(a.device, tuple) and a.uop.axis == 2 and b.uop.axis == 0
|
||||
if unfold_batch:
|
||||
orig_batch = a.shape[0]
|
||||
a = a.reshape(a.shape[0]*a.shape[1], a.shape[2])
|
||||
squeeze = a.ndim == 2
|
||||
if squeeze: a = a.unsqueeze(0)
|
||||
|
||||
batch, M, K = a.shape
|
||||
N = b.shape[1]
|
||||
is_multi = isinstance(a.device, tuple)
|
||||
if (k_sharded:=is_multi and a.uop.axis == 2): K //= len(a.device)
|
||||
if (m_sharded:=is_multi and a.uop.axis == 1): M //= len(a.device)
|
||||
n_sharded = is_multi and b.uop.axis == 1
|
||||
|
||||
if is_multi:
|
||||
if n_sharded:
|
||||
out = Tensor(Tensor.empty(batch, M, N//len(a.device), dtype=a.dtype, device=a.device).uop.multi(2), device=a.device)
|
||||
elif m_sharded:
|
||||
out = Tensor(Tensor.empty(batch, M, N, dtype=a.dtype, device=a.device).uop.multi(1), device=a.device)
|
||||
else:
|
||||
out = Tensor(Tensor.empty(batch//len(a.device) if a.uop.axis==0 else batch, M, N, dtype=a.dtype, device=a.device).uop.multi(0), device=a.device)
|
||||
else:
|
||||
out = Tensor.empty(batch, M, N, dtype=a.dtype, device=a.device)
|
||||
|
||||
renderer = Device[a.device[0] if is_multi else a.device].renderer
|
||||
dname, arch = renderer.device, getattr(renderer, "arch", "")
|
||||
if arch.startswith("gfx950") and getenv("USE_ASM", 1):
|
||||
out = Tensor.custom_kernel(out, a, b, fxn=functools.partial(custom_asm_gemm, dname=dname), grad_fxn=custom_gemm_bw)[0]
|
||||
else:
|
||||
out = Tensor.custom_kernel(out, a, b, fxn=custom_uop_gemm, grad_fxn=custom_gemm_bw)[0]
|
||||
if k_sharded: out = out.sum(0)
|
||||
out = out.squeeze(0) if squeeze else out
|
||||
if unfold_batch: out = out.reshape(orig_batch, -1, out.shape[-1])
|
||||
return out
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,76 @@
|
||||
.text
|
||||
.section .text.
|
||||
.global gemm
|
||||
.p2align 8
|
||||
.type gemm,@function
|
||||
|
||||
gemm:
|
||||
INSTRUCTIONS
|
||||
|
||||
.section .rodata,"a",@progbits
|
||||
.p2align 6, 0x0
|
||||
.amdhsa_kernel gemm
|
||||
# basic memory requirements
|
||||
.amdhsa_group_segment_fixed_size 30336
|
||||
.amdhsa_private_segment_fixed_size 0
|
||||
.amdhsa_kernarg_size 32
|
||||
# register usage (RSRC1)
|
||||
.amdhsa_next_free_vgpr 256
|
||||
.amdhsa_next_free_sgpr 100
|
||||
# workgroup / workitem IDs (RSRC2)
|
||||
.amdhsa_system_sgpr_workgroup_id_x 1
|
||||
.amdhsa_system_sgpr_workgroup_id_y 1
|
||||
.amdhsa_system_sgpr_workgroup_id_z 1
|
||||
# user SGPRs: kernarg ptr in s[0:1]
|
||||
.amdhsa_user_sgpr_kernarg_segment_ptr 1
|
||||
.amdhsa_user_sgpr_count 2
|
||||
# gfx10+ / gfx11 specifics (RSRC1[29..31])
|
||||
.amdhsa_wavefront_size32 1
|
||||
.amdhsa_workgroup_processor_mode 1
|
||||
.amdhsa_memory_ordered 1
|
||||
.amdhsa_forward_progress 1
|
||||
# misc for gfx11
|
||||
.amdhsa_dx10_clamp 1
|
||||
.amdhsa_ieee_mode 1
|
||||
.amdhsa_uses_dynamic_stack 0
|
||||
.end_amdhsa_kernel
|
||||
|
||||
.amdgpu_metadata
|
||||
---
|
||||
amdhsa.kernels:
|
||||
- .args:
|
||||
- .address_space: generic
|
||||
.name: C
|
||||
.offset: 0
|
||||
.size: 8
|
||||
.value_kind: global_buffer
|
||||
.value_type: f16
|
||||
- .address_space: generic
|
||||
.name: A
|
||||
.offset: 8
|
||||
.size: 8
|
||||
.value_kind: global_buffer
|
||||
.value_type: f16
|
||||
- .address_space: generic
|
||||
.name: B
|
||||
.offset: 16
|
||||
.size: 8
|
||||
.value_kind: global_buffer
|
||||
.value_type: f16
|
||||
.group_segment_fixed_size: 30336
|
||||
.kernarg_segment_align: 8
|
||||
.kernarg_segment_size: 32
|
||||
.max_flat_workgroup_size: 128
|
||||
.name: gemm
|
||||
.private_segment_fixed_size: 0
|
||||
.sgpr_count: 70
|
||||
.sgpr_spill_count: 0
|
||||
.symbol: gemm.kd
|
||||
.vgpr_count: 256
|
||||
.vgpr_spill_count: 0
|
||||
.wavefront_size: 32
|
||||
amdhsa.version:
|
||||
- 1
|
||||
- 1
|
||||
...
|
||||
.end_amdgpu_metadata
|
||||
@@ -0,0 +1,30 @@
|
||||
import math, pathlib
|
||||
|
||||
from tinygrad import Device, dtypes
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo
|
||||
|
||||
from extra.gemm.amd_uop_matmul import test_matmul
|
||||
|
||||
N = 4096
|
||||
TN = 96
|
||||
THREADS_PER_WG = 128
|
||||
NUM_WG = math.ceil(N / TN) * math.ceil(N / TN)
|
||||
|
||||
dname:str = Device.DEFAULT
|
||||
template:str = (pathlib.Path(__file__).parent/"template.s").read_text()
|
||||
|
||||
def asm_kernel() -> UOp:
|
||||
lidx = UOp.special(THREADS_PER_WG, "lidx0")
|
||||
gidx = UOp.special(NUM_WG, "gidx0")
|
||||
|
||||
a = UOp.placeholder((N*N,), dtypes.half, slot=1)
|
||||
b = UOp.placeholder((N*N,), dtypes.half, slot=2)
|
||||
c = UOp.placeholder((N*N,), dtypes.half, slot=0)
|
||||
|
||||
src = template.replace("INSTRUCTIONS", (pathlib.Path(__file__).parent/"gemm.s").read_text())
|
||||
|
||||
sink = UOp.sink(a, b, c, lidx, gidx, arg=KernelInfo(name="gemm"))
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.DEVICE, arg=dname), UOp(Ops.LINEAR, src=(*sink.src, sink)), UOp(Ops.SOURCE, arg=src)))
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_matmul(asm_kernel(), dtype=dtypes.half, N=N)
|
||||
@@ -0,0 +1,179 @@
|
||||
# unpack the complete kernel descriptor of an amdgpu ELF
|
||||
# https://rocm.docs.amd.com/projects/llvm-project/en/latest/LLVM/llvm/html/AMDGPUUsage.html#code-object-v3-kernel-descriptor
|
||||
import struct, pathlib, sys
|
||||
from tinygrad.runtime.support.elf import elf_loader
|
||||
|
||||
def bits(x, lo, hi): return (x >> lo) & ((1 << (hi - lo + 1)) - 1)
|
||||
def assert_zero(x, lo, hi): assert bits(x, lo, hi) == 0
|
||||
|
||||
with open(sys.argv[1], "rb") as f:
|
||||
lib = f.read()
|
||||
|
||||
image, sections, relocs = elf_loader(lib)
|
||||
rodata_entry = next((sh.header.sh_addr for sh in sections if sh.name == ".rodata"))
|
||||
|
||||
# rodata is exactly 64 bytes
|
||||
kd = image[rodata_entry:rodata_entry+64]
|
||||
desc = int.from_bytes(kd, byteorder="little")
|
||||
|
||||
group_segment_fixed_size = bits(desc, 0, 31)
|
||||
private_segment_fixed_size = bits(desc, 32, 63)
|
||||
kernarg_size = bits(desc, 64, 95)
|
||||
reserved_127_96 = bits(desc, 96, 127)
|
||||
assert reserved_127_96 == 0
|
||||
|
||||
print("GROUP_SEGMENT_FIXED_SIZE:", group_segment_fixed_size)
|
||||
print("PRIVATE_SEGMENT_FIXED_SIZE:", private_segment_fixed_size)
|
||||
print("KERNARG_SIZE:", kernarg_size)
|
||||
print("RESERVED 127:96:", reserved_127_96)
|
||||
|
||||
entry_off = bits(desc, 128, 191)
|
||||
|
||||
# sign-extend manually if needed
|
||||
if entry_off & (1 << 63):
|
||||
entry_off -= 1 << 64
|
||||
|
||||
print("KERNEL_CODE_ENTRY_BYTE_OFFSET:", entry_off)
|
||||
|
||||
kd_addr = 0x1840
|
||||
entry_addr = kd_addr + entry_off
|
||||
|
||||
print("Computed entry address: 0x%016x" % entry_addr)
|
||||
print("256B aligned:", entry_addr % 256 == 0)
|
||||
|
||||
pgm_rsrc3 = bits(desc, 352, 383)
|
||||
pgm_rsrc1 = bits(desc, 384, 415)
|
||||
pgm_rsrc2 = bits(desc, 416, 447)
|
||||
|
||||
print("COMPUTE_PGM_RSRC3: 0x%08x" % pgm_rsrc3)
|
||||
print("COMPUTE_PGM_RSRC1: 0x%08x" % pgm_rsrc1)
|
||||
print("COMPUTE_PGM_RSRC2: 0x%08x" % pgm_rsrc2)
|
||||
|
||||
# rsrc 3 (gfx950)
|
||||
|
||||
accum_offset_raw = bits(pgm_rsrc3, 0, 5)
|
||||
assert_zero(pgm_rsrc3, 6, 15)
|
||||
tg_split = bits(pgm_rsrc3, 16, 16)
|
||||
accum_offset_vgprs = (accum_offset_raw + 1) * 4
|
||||
print("RSRC3.ACCUM_OFFSET (AccVGPR index):", accum_offset_vgprs)
|
||||
print("RSRC3.TG_SPLIT:", tg_split)
|
||||
|
||||
# rsrc 1
|
||||
|
||||
vgpr_gran = bits(pgm_rsrc1, 0, 5)
|
||||
sgpr_gran = bits(pgm_rsrc1, 6, 9)
|
||||
assert_zero(pgm_rsrc1, 27, 28)
|
||||
|
||||
# NOTE: this is vgprs + agprs
|
||||
vgprs_used = (vgpr_gran + 1) * 8
|
||||
assert 0 <= vgprs_used <= 512
|
||||
|
||||
k = sgpr_gran // 2
|
||||
sgprs_used = (k + 1) * 16
|
||||
|
||||
print("RSRC1.VGPRS:", vgprs_used)
|
||||
print("RSRC1.SGPRS:", sgprs_used)
|
||||
|
||||
assert_zero(pgm_rsrc1, 10, 11)
|
||||
|
||||
float_round_mode_32 = bits(pgm_rsrc1, 12, 13)
|
||||
float_round_mode_16_64 = bits(pgm_rsrc1, 15, 14)
|
||||
float_denorm_mode_32 = bits(pgm_rsrc1, 16, 17)
|
||||
float_denorm_mode_16_64 = bits(pgm_rsrc1, 18, 19)
|
||||
|
||||
priv = bits(pgm_rsrc1, 20, 20)
|
||||
assert priv == 0
|
||||
enable_dx10_clamp_wg_rr_en = bits(pgm_rsrc1, 21, 21)
|
||||
debug_mode = bits(pgm_rsrc1, 22, 22)
|
||||
enable_ieee_mode = bits(pgm_rsrc1, 23, 23)
|
||||
bulky = bits(pgm_rsrc1, 24, 24)
|
||||
assert bulky == 0
|
||||
cdbg_user = bits(pgm_rsrc1, 25, 25)
|
||||
assert cdbg_user == 0
|
||||
fp16_ovfl = bits(pgm_rsrc1, 26, 26)
|
||||
assert_zero(pgm_rsrc1, 27, 28) # reserved
|
||||
assert_zero(pgm_rsrc1, 29, 29) # WGP_MODE (reserved on gfx9)
|
||||
assert_zero(pgm_rsrc1, 30, 30) # MEM_ORDERED (reserved on gfx9)
|
||||
assert_zero(pgm_rsrc1, 31, 31) # FWD_PROGRESS (reserved on gfx9)
|
||||
|
||||
# rsrc 2
|
||||
|
||||
enable_private_segment = bits(pgm_rsrc2, 0, 0) # SCRATCH_EN
|
||||
user_sgpr_count = bits(pgm_rsrc2, 1, 5) # USER_SGPR
|
||||
enable_trap_handler = bits(pgm_rsrc2, 6, 6) # TRAP_PRESENT (must be 0 here)
|
||||
assert enable_trap_handler == 0
|
||||
|
||||
enable_sgpr_workgroup_id_x = bits(pgm_rsrc2, 7, 7)
|
||||
enable_sgpr_workgroup_id_y = bits(pgm_rsrc2, 8, 8)
|
||||
enable_sgpr_workgroup_id_z = bits(pgm_rsrc2, 9, 9)
|
||||
enable_sgpr_workgroup_info = bits(pgm_rsrc2, 10, 10)
|
||||
|
||||
enable_vgpr_workitem_id = bits(pgm_rsrc2, 11, 12) # TIDIG_CMP_CNT enum (0..3)
|
||||
|
||||
enable_exception_address_watch = bits(pgm_rsrc2, 13, 13)
|
||||
assert enable_exception_address_watch == 0
|
||||
enable_exception_memory = bits(pgm_rsrc2, 14, 14)
|
||||
assert enable_exception_memory == 0
|
||||
|
||||
granulated_lds_size = bits(pgm_rsrc2, 15, 23)
|
||||
assert granulated_lds_size == 0 # spec: must be 0; CP uses dispatch packet rounding
|
||||
|
||||
enable_exception_fp_invalid = bits(pgm_rsrc2, 24, 24)
|
||||
enable_exception_fp_denorm_src = bits(pgm_rsrc2, 25, 25)
|
||||
enable_exception_fp_div0 = bits(pgm_rsrc2, 26, 26)
|
||||
enable_exception_fp_overflow = bits(pgm_rsrc2, 27, 27)
|
||||
enable_exception_fp_underflow = bits(pgm_rsrc2, 28, 28)
|
||||
enable_exception_fp_inexact = bits(pgm_rsrc2, 29, 29)
|
||||
enable_exception_int_div0 = bits(pgm_rsrc2, 30, 30)
|
||||
|
||||
assert_zero(pgm_rsrc2, 31, 31)
|
||||
|
||||
print("RSRC2.ENABLE_PRIVATE_SEGMENT:", enable_private_segment)
|
||||
print("RSRC2.USER_SGPR_COUNT:", user_sgpr_count)
|
||||
print("RSRC2.ENABLE_SGPR_WORKGROUP_ID_X:", enable_sgpr_workgroup_id_x)
|
||||
print("RSRC2.ENABLE_SGPR_WORKGROUP_ID_Y:", enable_sgpr_workgroup_id_y)
|
||||
print("RSRC2.ENABLE_SGPR_WORKGROUP_ID_Z:", enable_sgpr_workgroup_id_z)
|
||||
print("RSRC2.ENABLE_SGPR_WORKGROUP_INFO:", enable_sgpr_workgroup_info)
|
||||
print("RSRC2.ENABLE_VGPR_WORKITEM_ID (enum):", enable_vgpr_workitem_id)
|
||||
|
||||
print("RSRC2.EXC_FP_INVALID:", enable_exception_fp_invalid)
|
||||
print("RSRC2.EXC_FP_DENORM_SRC:", enable_exception_fp_denorm_src)
|
||||
print("RSRC2.EXC_FP_DIV0:", enable_exception_fp_div0)
|
||||
print("RSRC2.EXC_FP_OVERFLOW:", enable_exception_fp_overflow)
|
||||
print("RSRC2.EXC_FP_UNDERFLOW:", enable_exception_fp_underflow)
|
||||
print("RSRC2.EXC_FP_INEXACT:", enable_exception_fp_inexact)
|
||||
print("RSRC2.EXC_INT_DIV0:", enable_exception_int_div0)
|
||||
|
||||
# user sgprs
|
||||
|
||||
enable_sgpr_private_segment_buffer = bits(desc, 448, 448)
|
||||
enable_sgpr_dispatch_ptr = bits(desc, 449, 449)
|
||||
enable_sgpr_queue_ptr = bits(desc, 450, 450)
|
||||
enable_sgpr_kernarg_segment_ptr = bits(desc, 451, 451)
|
||||
enable_sgpr_dispatch_id = bits(desc, 452, 452)
|
||||
enable_sgpr_flat_scratch_init = bits(desc, 453, 453)
|
||||
enable_sgpr_private_segment_size = bits(desc, 454, 454)
|
||||
|
||||
assert_zero(desc, 455, 457)
|
||||
|
||||
print("DESC.ENABLE_SGPR_PRIVATE_SEGMENT_BUFFER:", enable_sgpr_private_segment_buffer)
|
||||
print("DESC.ENABLE_SGPR_DISPATCH_PTR:", enable_sgpr_dispatch_ptr)
|
||||
print("DESC.ENABLE_SGPR_QUEUE_PTR:", enable_sgpr_queue_ptr)
|
||||
print("DESC.ENABLE_SGPR_KERNARG_SEGMENT_PTR:", enable_sgpr_kernarg_segment_ptr)
|
||||
print("DESC.ENABLE_SGPR_DISPATCH_ID:", enable_sgpr_dispatch_id)
|
||||
print("DESC.ENABLE_SGPR_FLAT_SCRATCH_INIT:", enable_sgpr_flat_scratch_init)
|
||||
print("DESC.ENABLE_SGPR_PRIVATE_SEGMENT_SIZE:", enable_sgpr_private_segment_size)
|
||||
|
||||
assert_zero(desc, 458, 459)
|
||||
|
||||
uses_dynamic_stack = bits(desc, 459, 460)
|
||||
print("DESC.USES_DYNAMIC_STACK:", uses_dynamic_stack)
|
||||
|
||||
# gfx950 only
|
||||
assert_zero(desc, 460, 463)
|
||||
kernarg_preload_spec_length = bits(desc, 464, 470)
|
||||
print("DESC.KERNARG_PRELOAD_SPEC_LENGTH:", kernarg_preload_spec_length)
|
||||
kernarg_preload_spec_offset = bits(desc, 471, 479)
|
||||
print("DESC.KERNARG_PRELOAD_SPEC_OFFSET:", kernarg_preload_spec_offset)
|
||||
|
||||
assert_zero(desc, 480, 511)
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
# Run all ALU and memory instructions in the ISA
|
||||
import functools, inspect
|
||||
from enum import Enum
|
||||
from tinygrad import Tensor, Device, dtypes
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo, AddrSpace
|
||||
from tinygrad.renderer.amd.dsl import Inst, Reg, OPERANDS, SrcField, VGPRField, SGPRField, SSrcField, SBaseField, AlignedSGPRField, BitField
|
||||
from tinygrad.renderer.amd.dsl import FixedBitField, EnumBitField, s, v, NULL, VCC_LO
|
||||
from extra.gemm.amd_asm_matmul import Kernel
|
||||
|
||||
# skip instructions that mutate wave state (PC, EXEC, allocations, signals)
|
||||
SKIP = {"S_SETPC_B64", "S_SWAPPC_B64", "S_RFE_B64", "S_BARRIER_SIGNAL_ISFIRST", "S_GET_BARRIER_STATE", "S_ALLOC_VGPR", "S_SLEEP_VAR", "S_GETPC_B64",
|
||||
"S_SENDMSG_RTN_B32", "S_SENDMSG_RTN_B64"}
|
||||
# skip barriers, s_waits, wrap level atomics, and ray tracing (bvh)
|
||||
SKIP_SUBSTR = ["SAVEEXEC", "CMPX", "WREXEC", "MOVREL", "ATOMIC", "S_BUFFER_", "S_ATC_PROBE", "BARRIER", "S_WAITCNT", "BVH",
|
||||
"DS_CMPSTORE_RTN", "DS_WRAP_RTN_B32", "DS_ORDERED_COUNT", "DS_GWS", "GS_REG", "GLOBAL_LOAD_LDS", "GLOBAL_STORE_BLOCK"]
|
||||
|
||||
ALU_FORMATS = {"VOP1", "VOP1_LIT", "VOP1_SDST", "VOP2", "VOP2_LIT", "VOP3", "VOP3_SDST", "VOP3SD", "VOP3P", "VOP3P_MFMA", "VOP3PX2",
|
||||
"VOPC", "SOP1", "SOP1_LIT", "SOP2", "SOP2_LIT", "SOPC", "SOPC_LIT", "SOPK", "SOPK_LIT", "VINTERP"}
|
||||
# intentionally not testing scratch memory ops
|
||||
MEM_FORMATS = {"VGLOBAL", "GLOBAL", "SMEM", "DS"}
|
||||
|
||||
def should_skip(op:Enum) -> bool: return (name:=op.name) in SKIP or any(sub in name for sub in SKIP_SUBSTR)
|
||||
|
||||
# ** named register assignments
|
||||
|
||||
# ALU operands
|
||||
ALU_VGPR_STRIDE = 16 # v[0], v[16], v[32], ... per ALU operand slot
|
||||
ALU_SGPR_STRIDE = 4 # s[0], s[4], s[8], ... per ALU operand slot
|
||||
|
||||
# memory address registers
|
||||
S_KERNARG_PTR = (0, 1)
|
||||
S_BUF_PTR = (2, 3)
|
||||
V_VADDR = (0, 1)
|
||||
V_DS_ADDR = 0
|
||||
|
||||
# memory data registers
|
||||
MEM_VGPR_BASE = 32 # v[32], v[48], ... for vdst/vdata/vsrc
|
||||
MEM_VGPR_STRIDE = 16 # spacing between memory data vgpr slots
|
||||
MEM_SGPR_BASE = 8 # s[8], s[10], ... for SMEM sdata
|
||||
MEM_SGPR_STRIDE = 2 # spacing between memory data sgpr slots
|
||||
|
||||
# ** create an ALU instruction based on the operands
|
||||
|
||||
def create_alu_inst(op:Enum, builder:functools.partial[Inst]) -> Inst:
|
||||
inst_cls, operands, slot = builder.func, OPERANDS[op], 0
|
||||
kwargs:dict[str, Reg|int] = {}
|
||||
for name, field in inst_cls._fields:
|
||||
if isinstance(field, (FixedBitField, EnumBitField)): continue
|
||||
nregs = max(1, operands[name][1] // 32) if name in operands else 1
|
||||
is_sreg = name in operands and "SREG" in str(operands[name][2])
|
||||
base_v, base_s = slot * ALU_VGPR_STRIDE, slot * ALU_SGPR_STRIDE
|
||||
if name == "sdst" and isinstance(field, SGPRField): reg = VCC_LO
|
||||
elif is_sreg and not isinstance(field, VGPRField): reg = VCC_LO
|
||||
elif isinstance(field, VGPRField): reg = v[base_v:base_v+nregs-1] if nregs > 1 else v[base_v]
|
||||
elif isinstance(field, SSrcField): reg = VCC_LO if nregs <= 2 else s[base_s:base_s+nregs-1] if nregs > 1 else s[base_s]
|
||||
elif isinstance(field, SGPRField): reg = s[base_s:base_s+nregs-1] if nregs > 1 else s[base_s]
|
||||
elif isinstance(field, SrcField): reg = v[base_v:base_v+nregs-1] if nregs > 1 else v[base_v]
|
||||
else: reg = None
|
||||
if reg is not None: kwargs[name] = reg; slot += 1
|
||||
elif isinstance(field, BitField): kwargs[name] = field.default
|
||||
return builder(**kwargs)
|
||||
|
||||
# ** create a memory instruction with pre set address registers
|
||||
|
||||
MEM_PRESET_REGS:dict[str, dict[str, Reg]] = {
|
||||
"VGLOBAL":{"saddr":s[S_BUF_PTR[0]:S_BUF_PTR[1]], "vaddr":v[V_VADDR[0]:V_VADDR[1]]},
|
||||
"GLOBAL":{"saddr":s[S_BUF_PTR[0]:S_BUF_PTR[1]], "addr":v[V_DS_ADDR]}, # addr is 32-bit offset when saddr is valid SGPR
|
||||
"DS":{"addr":v[V_DS_ADDR]},
|
||||
"SMEM":{"sbase":s[S_KERNARG_PTR[0]:S_KERNARG_PTR[1]], "soffset":NULL},
|
||||
}
|
||||
|
||||
def create_mem_inst(op:Enum, builder:functools.partial[Inst]) -> Inst:
|
||||
inst_cls, operands, field_map = builder.func, OPERANDS.get(op, {}), MEM_PRESET_REGS.get(builder.func.__name__, {})
|
||||
kwargs:dict[str, Reg|int] = {}
|
||||
vslot, sslot = 0, 0
|
||||
for name, field in inst_cls._fields:
|
||||
if isinstance(field, (FixedBitField, EnumBitField)): continue
|
||||
if name in field_map:
|
||||
kwargs[name] = field_map[name]
|
||||
continue
|
||||
nregs = max(1, operands[name][1] // 32) if name in operands else 1
|
||||
if isinstance(field, VGPRField):
|
||||
vi = MEM_VGPR_BASE + vslot * MEM_VGPR_STRIDE
|
||||
kwargs[name] = v[vi:vi+nregs-1] if nregs > 1 else v[vi]
|
||||
vslot += 1
|
||||
elif isinstance(field, (SGPRField, AlignedSGPRField, SBaseField)):
|
||||
si = MEM_SGPR_BASE + sslot * MEM_SGPR_STRIDE
|
||||
kwargs[name] = s[si:si+nregs-1] if nregs > 1 else s[si]
|
||||
sslot += 1
|
||||
elif isinstance(field, BitField): kwargs[name] = field.default
|
||||
return builder(**kwargs)
|
||||
|
||||
# ** collect all memory and ALU instructions from the ISA autogen
|
||||
|
||||
def collect_instructions() -> tuple[list[Inst], list[Inst], list[str]]:
|
||||
op_map:dict[Enum, functools.partial[Inst]] = {}
|
||||
for name, obj in inspect.getmembers(all_insts):
|
||||
if isinstance(obj, functools.partial) and len(obj.args) == 1: op_map[obj.args[0]] = obj
|
||||
alu_insts:list[Inst] = []
|
||||
mem_insts:list[Inst] = []
|
||||
skipped:list[str] = []
|
||||
for op_enum, builder in op_map.items():
|
||||
if should_skip(op_enum) or op_enum not in OPERANDS: skipped.append(op_enum.name); continue
|
||||
fmt = builder.func.__name__
|
||||
if fmt in ALU_FORMATS: alu_insts.append(create_alu_inst(op_enum, builder))
|
||||
elif fmt in MEM_FORMATS: mem_insts.append(create_mem_inst(op_enum, builder))
|
||||
return alu_insts, mem_insts, skipped
|
||||
|
||||
def exec_insts(insts:list):
|
||||
k = Kernel(arch)
|
||||
# ** prologue for global memory
|
||||
k.emit(s_load_b64(sdata=s[S_BUF_PTR[0]:S_BUF_PTR[1]], sbase=s[S_KERNARG_PTR[0]:S_KERNARG_PTR[1]], soffset=NULL))
|
||||
k.waitcnt(lgkm=0)
|
||||
k.emit(v_mov_b32_e32(v[V_VADDR[0]], 0))
|
||||
k.emit(v_mov_b32_e32(v[V_VADDR[1]], 0))
|
||||
# ** emit
|
||||
for inst in insts: k.emit(inst)
|
||||
k.emit(s_endpgm())
|
||||
# ** run
|
||||
NUM_THREADS, NUM_GRIDS, BUF_SIZE = 32, 1, 1024*1024
|
||||
def fxn(A:UOp, B:UOp, C:UOp) -> UOp:
|
||||
lidx, gidx = UOp.special(NUM_THREADS, "lidx0"), UOp.special(NUM_GRIDS, "gidx0")
|
||||
lds = UOp(Ops.DEFINE_LOCAL, dtypes.uint8.ptr(size=BUF_SIZE, addrspace=AddrSpace.LOCAL), (), "lds")
|
||||
sink = UOp.sink(A.base, B.base, C.base, lds, lidx, gidx, arg=KernelInfo(name="discover_ops"))
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.DEVICE, arg="AMD"), UOp(Ops.LINEAR, src=tuple(UOp(Ops.INS, arg=x) for x in k.finalize()))))
|
||||
A = Tensor.empty(BUF_SIZE, dtype=dtypes.uint8)
|
||||
B = Tensor.empty(1, dtype=dtypes.uint8)
|
||||
C = Tensor.empty(1, dtype=dtypes.uint8)
|
||||
Tensor.custom_kernel(A, B, C, fxn=fxn)[0].realize()
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
arch = Device[Device.DEFAULT].renderer.arch
|
||||
if arch.startswith("gfx12"):
|
||||
from tinygrad.runtime.autogen.amd.rdna4.ins import *
|
||||
import tinygrad.runtime.autogen.amd.rdna4.ins as all_insts
|
||||
elif arch.startswith("gfx11"):
|
||||
from tinygrad.runtime.autogen.amd.rdna3.ins import *
|
||||
import tinygrad.runtime.autogen.amd.rdna3.ins as all_insts
|
||||
# these don"t exist in RDNA3, only RDNA3.5 and above
|
||||
SKIP.update(["S_FMAAK_F32", "S_FMAMK_F32"])
|
||||
else:
|
||||
print(f"{arch} not supported yet")
|
||||
sys.exit(0)
|
||||
alu_insts, mem_insts, skipped = collect_instructions()
|
||||
print(f"collected {len(alu_insts)} ALU + {len(mem_insts)} memory instructions ({len(skipped)} skipped)")
|
||||
exec_insts(mem_insts+alu_insts)
|
||||
@@ -8,8 +8,8 @@ PROFILE_PATH = Path(temp("profile.pkl", append_user=True))
|
||||
EXAMPLES = {
|
||||
"empty":"test/backend/test_custom_kernel.py TestCustomKernel.test_empty",
|
||||
"plus":"test/test_tiny.py TestTiny.test_plus",
|
||||
"gemm":"-c \"from tinygrad import Tensor; (Tensor.empty(N:=32, N)@Tensor.empty(N, N)).realize()\"",
|
||||
"sync":"test/amd/test_custom_kernel.py TestCustomKernel.test_wave_sync",
|
||||
"gemm":"-c \"from tinygrad import Tensor; (Tensor.empty(N:=64, N)@Tensor.empty(N, N)).realize()\"",
|
||||
"ops":"extra/sqtt/examples/discover_ops.py"
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
-1
@@ -6,7 +6,7 @@ from tinygrad.runtime.support.elf import elf_loader
|
||||
|
||||
ARCH_TO_TARGET:dict[str, list[str]] = {
|
||||
"rdna3":["gfx1100"],
|
||||
"rdna4":["gfx1200", "gfx1201"],
|
||||
"rdna4":["gfx1200"],
|
||||
"cdna":["gfx950", "gfx942"],
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import unittest
|
||||
import functools
|
||||
from tinygrad import Tensor, Device, dtypes
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo
|
||||
from tinygrad.renderer import Estimates
|
||||
|
||||
from tinygrad.runtime.autogen.amd.rdna3.ins import *
|
||||
from tinygrad.runtime.autogen.amd.rdna4.ins import s_barrier_wait, s_barrier_signal
|
||||
from tinygrad.renderer.amd.dsl import s, v
|
||||
from test.amd.helpers import TARGET_TO_ARCH
|
||||
|
||||
def custom_add_one(A:UOp) -> UOp:
|
||||
A = A.flatten()
|
||||
@@ -45,26 +43,9 @@ def custom_add_var(A:UOp, B:UOp) -> UOp:
|
||||
sink = UOp.sink(A.base, B.base, var, threads, arg=KernelInfo(f"custom_add_var_{A.size}"))
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.DEVICE, arg="AMD"), UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=x) for x in insts]))))
|
||||
|
||||
def custom_wave_sync(A:UOp, arch:str) -> UOp:
|
||||
# 4 waves across 1024 WG — enough to saturate a SIMD with many concurrent WGs
|
||||
# s_sleep yields the SIMD so waves from different WGs interleave, causing barrier packet reordering
|
||||
threads = UOp.special(128, "lidx0")
|
||||
wg = UOp.special(1024, "gidx0")
|
||||
insts = []
|
||||
for _ in range(4):
|
||||
insts.append(s_sleep(4))
|
||||
insts += [s_barrier()] if arch == "rdna3" else [s_barrier_signal(), s_barrier_wait()]
|
||||
insts += [s_nop(0)]*4
|
||||
insts.append(s_endpgm())
|
||||
sink = UOp.sink(A.base, threads, wg, arg=KernelInfo("custom_wave_sync"))
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.DEVICE, arg="AMD"), UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=x) for x in insts]))))
|
||||
|
||||
@unittest.skipUnless(Device.DEFAULT == "AMD", "requires AMD device")
|
||||
class TestCustomKernel(unittest.TestCase):
|
||||
def setUp(self): self.arch = TARGET_TO_ARCH[Device["AMD"].arch]
|
||||
|
||||
def test_simple(self):
|
||||
if self.arch != "rdna3": self.skipTest("only rdna3")
|
||||
a = Tensor.full((16, 16), 1.).contiguous().realize()
|
||||
a = Tensor.custom_kernel(a, fxn=custom_add_one)[0]
|
||||
ei = a.schedule()[-1].lower()
|
||||
@@ -74,7 +55,6 @@ class TestCustomKernel(unittest.TestCase):
|
||||
self.assertTrue((a.numpy() == 2.).all())
|
||||
|
||||
def test_variable(self):
|
||||
if self.arch != "rdna3": self.skipTest("only rdna3")
|
||||
b = Tensor.full((16, 16), 1, dtype=dtypes.uint32).contiguous().realize()
|
||||
a = Tensor.zeros_like(b).contiguous().realize()
|
||||
a = Tensor.custom_kernel(a, b, fxn=custom_add_var)[0]
|
||||
@@ -83,9 +63,5 @@ class TestCustomKernel(unittest.TestCase):
|
||||
ei.run({"var":i})
|
||||
self.assertTrue((a.numpy() == 1+i).all())
|
||||
|
||||
def test_wave_sync(self):
|
||||
if self.arch not in {"rdna3", "rdna4"}: self.skipTest("only rdna3 or rdna4")
|
||||
Tensor.empty(1).custom_kernel(fxn=functools.partial(custom_wave_sync, arch=self.arch))[0].realize()
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -10,7 +10,7 @@ from tinygrad.runtime.autogen.amd.rdna3.ins import SOPP
|
||||
from tinygrad.runtime.autogen.amd.rdna3.enum import SOPPOp
|
||||
from tinygrad.renderer.amd.sqtt import (decode, LAYOUT_HEADER, WAVESTART, WAVESTART_RDNA4, WAVEEND, INST, INST_RDNA4, VALUINST,
|
||||
IMMEDIATE, IMMEDIATE_MASK, PACKET_TYPES_RDNA3, PACKET_TYPES_RDNA4, PACKET_TYPES_CDNA, CDNA_WAVESTART,
|
||||
InstOp, InstOpRDNA4, print_packets, CDNA_WAVEEND, CDNA_INST)
|
||||
InstOp, InstOpRDNA4, print_packets, CDNA_WAVEEND, CDNA_TIMESTAMP)
|
||||
from test.amd.helpers import TARGET_TO_ARCH
|
||||
|
||||
import tinygrad
|
||||
@@ -21,7 +21,7 @@ OTHER_SIMD_OPS = {InstOp.OTHER_LDS_LOAD, InstOp.OTHER_LDS_STORE, InstOp.OTHER_LD
|
||||
InstOp.OTHER_FLAT_STORE_128, InstOp.OTHER_GLOBAL_LOAD, InstOp.OTHER_GLOBAL_LOAD_VADDR,
|
||||
InstOp.OTHER_GLOBAL_STORE_64, InstOp.OTHER_GLOBAL_STORE_96, InstOp.OTHER_GLOBAL_STORE_128,
|
||||
InstOp.OTHER_GLOBAL_STORE_VADDR_128}
|
||||
OTHER_SIMD_OPS_RDNA4 = {InstOpRDNA4.OTHER_VMEM, InstOpRDNA4.OTHER_VMEM_5, InstOpRDNA4.OTHER_LDS_1, InstOpRDNA4.OTHER_LDS_2}
|
||||
OTHER_SIMD_OPS_RDNA4 = {InstOpRDNA4.OTHER_VMEM, InstOpRDNA4.OTHER_VMEM_5}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# ROCPROF DECODER
|
||||
@@ -122,7 +122,8 @@ class SQTTExamplesTestBase(unittest.TestCase):
|
||||
print(f"\n=== {name} event {i} ===")
|
||||
print_packets(packets)
|
||||
self.assertGreater(len(packets), 0, f"no packets decoded from {name} event {i}")
|
||||
self.assertIsInstance(packets[0], LAYOUT_HEADER, f"first packet should be LAYOUT_HEADER in {name}")
|
||||
first_pkt = CDNA_TIMESTAMP if self.target.startswith("gfx9") else LAYOUT_HEADER
|
||||
self.assertIsInstance(packets[0], first_pkt, f"first packet should be {first_pkt.__name__} in {name}")
|
||||
|
||||
def test_packet_types_valid(self):
|
||||
all_classes = set(PACKET_TYPES_RDNA3.values()) | set(PACKET_TYPES_RDNA4.values()) | set(PACKET_TYPES_CDNA.values())
|
||||
@@ -153,10 +154,9 @@ class SQTTExamplesTestBase(unittest.TestCase):
|
||||
if "gemm" not in name: continue
|
||||
with self.subTest(example=name):
|
||||
all_packets = [p for e in events for p in decode(e.blob)]
|
||||
inst_packets = [p for p in all_packets if isinstance(p, (INST, INST_RDNA4, CDNA_INST))]
|
||||
self.assertGreater(len(inst_packets), 0, f"no INST packets in {name}")
|
||||
if isinstance(inst_packets[0], (INST, INST_RDNA4)):
|
||||
self.assertGreater(len([p for p in inst_packets if p.op.name.startswith("JUMP")]), 0, f"no JUMP packets in {name}")
|
||||
inst_names = [p.op.name for p in all_packets if isinstance(p, (INST, INST_RDNA4))]
|
||||
self.assertGreater(len(inst_names), 0, f"no INST packets in {name}")
|
||||
self.assertGreater(len([n for n in inst_names if n.startswith("JUMP")]), 0, f"no JUMP packets in {name}")
|
||||
|
||||
expected: dict[str, list[int]] = {} # override in subclasses
|
||||
def test_packet_counts(self):
|
||||
@@ -211,22 +211,22 @@ class SQTTExamplesTestBase(unittest.TestCase):
|
||||
class TestSQTTExamplesRDNA3(SQTTExamplesTestBase):
|
||||
target = "gfx1100"
|
||||
expected = {
|
||||
"profile_empty_run_0": [1880, 1867, 1920, 1971, 1998, 1904],
|
||||
"profile_empty_run_1": [1880, 1867, 1920, 1971, 1998, 1904],
|
||||
"profile_gemm_run_0": [3275, 3278, 2426, 2475, 2511, 2431],
|
||||
"profile_gemm_run_1": [3264, 3268, 2420, 2469, 2504, 2401],
|
||||
"profile_ops_run_0": [1944, 4903, 1984, 2035, 2062, 1968],
|
||||
"profile_ops_run_1": [1944, 4918, 1984, 2035, 2062, 1968],
|
||||
"profile_plus_run_0": [1938, 1932, 1978, 2029, 2056, 1962],
|
||||
"profile_plus_run_1": [1891, 1874, 1931, 1982, 2009, 1915],
|
||||
"profile_empty_run_0": [1974, 1961, 2014, 2065, 2092, 1998],
|
||||
"profile_empty_run_1": [1979, 1972, 2019, 2070, 2097, 2003],
|
||||
"profile_gemm_run_0": [2038, 11076, 2324, 2129, 2156, 2062],
|
||||
"profile_gemm_run_1": [2038, 11037, 2318, 2129, 2156, 2062],
|
||||
"profile_ops_run_0": [2038, 5070, 2078, 2129, 2156, 2062],
|
||||
"profile_ops_run_1": [2038, 5007, 2078, 2129, 2156, 2062],
|
||||
"profile_plus_run_0": [1979, 1979, 2030, 2070, 2097, 2003],
|
||||
"profile_plus_run_1": [1979, 2043, 2030, 2070, 2097, 2003],
|
||||
}
|
||||
|
||||
class TestSQTTExamplesRDNA4(SQTTExamplesTestBase): target = "gfx1200"
|
||||
|
||||
class TestSQTTExamplesCDNA(SQTTExamplesTestBase):
|
||||
target = "gfx950"
|
||||
def test_gemm_has_instructions(self): self.skipTest("TODO: decode CDNA inst packets")
|
||||
def test_rocprof_wave_times_match(self): self.skipTest("TODO: requires timestamp patching")
|
||||
def test_rocprof_inst_times_match(self): self.skipTest("TODO: requires timestamp patching")
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import unittest, pickle
|
||||
from typing import Iterator
|
||||
from pathlib import Path
|
||||
from tinygrad.helpers import DEBUG, getenv, temp
|
||||
from tinygrad.helpers import DEBUG, OSX, getenv, temp
|
||||
from tinygrad.renderer.amd.sqtt import print_packets, map_insts
|
||||
from tinygrad.runtime.autogen.amd.rdna3.ins import s_endpgm
|
||||
from tinygrad.viz.serve import sqtt_timeline
|
||||
@@ -11,7 +11,7 @@ from test.amd.disasm import disasm
|
||||
import tinygrad
|
||||
EXAMPLES_DIR = Path(tinygrad.__file__).parent.parent / "extra/sqtt/examples"
|
||||
|
||||
def rocprof_inst_traces_match(sqtt, prg, target):
|
||||
def rocprof_inst_traces_match(sqtt, prg, target, pass_rocprof_err=False):
|
||||
from tinygrad.viz.serve import amd_decode
|
||||
from extra.sqtt.roc import decode as roc_decode, InstExec
|
||||
addr_table = amd_decode(prg.lib, target)
|
||||
@@ -31,7 +31,7 @@ def rocprof_inst_traces_match(sqtt, prg, target):
|
||||
rocprof_inst = next(rwaves_iter[info.wave][0])
|
||||
ref_pc = rocprof_inst.pc-prg.base
|
||||
# always check pc matches
|
||||
assert ref_pc == info.pc, f"pc mismatch {ref_pc}:{disasm_map[rocprof_inst.pc]} != {info.pc}:{disasm(info.inst)}"
|
||||
assert ref_pc == info.pc or pass_rocprof_err, f"pc mismatch {ref_pc}:{disasm_map[rocprof_inst.pc]} != {info.pc}:{disasm(info.inst)}"
|
||||
# special handling for s_endpgm, it marks the wave completion.
|
||||
if info.inst == s_endpgm():
|
||||
completed_wave = list(rwaves_iter[info.wave].pop(0))
|
||||
@@ -64,13 +64,13 @@ class TestSQTTMapBase(unittest.TestCase):
|
||||
|
||||
def test_rocprof_inst_traces_match(self):
|
||||
for name, (events, kern_events, target) in self.examples.items():
|
||||
if "sync" in name and self.target.startswith("gfx12"):
|
||||
self.skipTest("our timestamps are off by a few cycles because rocprof patches timestamps for rdna4 barriers")
|
||||
for event in events:
|
||||
if not event.itrace: continue
|
||||
if event.kern not in kern_events: continue
|
||||
with self.subTest(example=name, kern=event.kern):
|
||||
passed_insts, n_waves, n_units = rocprof_inst_traces_match(event, kern_events[event.kern], target)
|
||||
# rocprof OSX has a bug for sopk decoding, linux rocprof works
|
||||
pass_rocprof_err = OSX and target == "gfx1200" and name.startswith("profile_ops")
|
||||
passed_insts, n_waves, n_units = rocprof_inst_traces_match(event, kern_events[event.kern], target, pass_rocprof_err)
|
||||
if n_waves: print(f"{name}: passed for {passed_insts} instructions across {n_waves} waves scheduled on {n_units} wave units")
|
||||
|
||||
def test_sqtt_timeline(self):
|
||||
@@ -83,6 +83,7 @@ class TestSQTTMapBase(unittest.TestCase):
|
||||
mean = sum(frequency) / len(frequency)
|
||||
variance = sum((v - mean) ** 2 for v in frequency) / len(frequency)
|
||||
self.assertGreater(mean, 0)
|
||||
self.assertGreater(variance, 0)
|
||||
if DEBUG >= 2: print(f"{name:20s} SE:{event.se} {mean/1e9:.2f} GHz mean, {variance/1e18:.2f} GHz^2 variance")
|
||||
events = [e for e in timeline if type(e).__name__ == "ProfileRangeEvent"]
|
||||
insts, execs = 0, 0
|
||||
@@ -91,21 +92,10 @@ class TestSQTTMapBase(unittest.TestCase):
|
||||
if "ALT" not in e.name.display_name: execs += 1
|
||||
elif "WAVE" in e.device:
|
||||
# sopk/immediates don't get ALU/MEM EXEC
|
||||
if e.name.display_name not in {"IMMEDIATE", "IMMEDIATE_MASK", "JUMP", "JUMP_NO", "MESSAGE", "BARRIER", "BARRIER_SIGNAL"}: insts += 1
|
||||
if e.name.display_name not in {"IMMEDIATE", "IMMEDIATE_MASK", "JUMP", "JUMP_NO", "MESSAGE"}: insts += 1
|
||||
else: raise Exception(f"timeline row must be INST or EXEC, got {e.device}")
|
||||
self.assertEqual(execs, insts)
|
||||
|
||||
def test_wave_sync(self):
|
||||
for name, (events, kern_events, target) in self.examples.items():
|
||||
for event in events:
|
||||
wave_barriers = {}
|
||||
for e in sqtt_timeline(event.blob, kern_events[event.kern].lib, target):
|
||||
if type(e).__name__ == "ProfileRangeEvent" and e.name.display_name == "BARRIER": wave_barriers.setdefault(e.device, []).append(e)
|
||||
if not wave_barriers: continue
|
||||
for row, events in wave_barriers.items():
|
||||
for e in events:
|
||||
assert e.en-e.st > 1, f"all barriers must have a duration greater than 1, got {e}"
|
||||
|
||||
class TestSQTTMapRDNA3(TestSQTTMapBase): target = "gfx1100"
|
||||
|
||||
class TestSQTTMapRDNA4(TestSQTTMapBase): target = "gfx1200"
|
||||
|
||||
@@ -2,7 +2,7 @@ import unittest
|
||||
from tinygrad import Tensor, Device, dtypes, Context
|
||||
from tinygrad.device import is_dtype_supported
|
||||
from tinygrad.helpers import getenv
|
||||
from extra.gemm.cdna_asm_gemm import asm_gemm
|
||||
from extra.gemm.asm.cdna.gemm import asm_gemm
|
||||
from test.helpers import needs_second_gpu
|
||||
|
||||
# On non CDNA4 it will only validate the Tensor.custom_kernel integration
|
||||
@@ -157,7 +157,7 @@ class TestGemmLarge(unittest.TestCase):
|
||||
|
||||
class TestMagicGu(unittest.TestCase):
|
||||
def test_magicgu_matches_old(self):
|
||||
from extra.gemm.cdna_asm_gemm import _magicgu_mulhi, TILE_M, TILE_N, TILE_K
|
||||
from extra.gemm.asm.cdna.asm import _magicgu_mulhi, TILE_M, TILE_N, TILE_K
|
||||
old_iters_args = {64: (67108864, 0), 128: (33554432, 0), 224: (613566757, 2147483656)}
|
||||
old_gemm_shapes = [
|
||||
(8192, 4096, 4096), (8192, 14336, 4096), (8192, 4096, 14336),
|
||||
|
||||
@@ -46,9 +46,9 @@ class TestHelpers(unittest.TestCase):
|
||||
self.assertTrue((rng+2).is_increasing())
|
||||
|
||||
class TestValidIdxSimplification(unittest.TestCase):
|
||||
def check(self, load, sidx, svalid, extra=()):
|
||||
def check(self, load, sidx, svalid):
|
||||
with Context(NOOPT=1, SPEC=0):
|
||||
load = full_rewrite_to_sink(UOp.sink(load, *extra)).src[0]
|
||||
load = full_rewrite_to_sink(load.sink()).src[0]
|
||||
idx, valid = load.src[0].src[1], load.src[0].src[2]
|
||||
check_uop_against_string(self, idx, sidx)
|
||||
check_uop_against_string(self, valid, svalid)
|
||||
@@ -156,12 +156,9 @@ class TestValidIdxSimplification(unittest.TestCase):
|
||||
idx = (alu15*-31)+(((((alu11+218)//224)+ridx0)%30)*1568)
|
||||
valid = (ridx2<1)&(ridx1<6)
|
||||
load = get_gated_load_uop(valid, idx)
|
||||
# prevent ridx1 and ridx2 from being shrunk
|
||||
red = UOp(Ops.REDUCE, dtypes.float, (load, ridx1, ridx2), Ops.ADD)
|
||||
self.check(load,
|
||||
"(r0*1568)",
|
||||
"((r2<1)&(r1<6))",
|
||||
extra=(red,))
|
||||
"((r2<1)&(r1<6))")
|
||||
|
||||
def test_valid_becomes_const1_z3(self):
|
||||
from z3 import Ints, Solver, And, If, Not, unsat
|
||||
@@ -486,79 +483,5 @@ class TestDropTrueGate(unittest.TestCase):
|
||||
# the True gate should be dropped (INDEX should only have 2 sources)
|
||||
self.assertEqual(len(result.src), 2, "True gate should be dropped from INDEX")
|
||||
|
||||
class TestRangeShrink(unittest.TestCase):
|
||||
def get_ranges(self, sink):
|
||||
with Context(NOOPT=1, SPEC=0):
|
||||
result = full_rewrite_to_sink(sink)
|
||||
return [u for u in result.toposort() if u.op is Ops.RANGE]
|
||||
|
||||
def test_range_shrink_single_guard(self):
|
||||
# range 0..203 guarded by r < 4 everywhere -> shrink to 0..3
|
||||
r = Range(0, 204)
|
||||
load = get_gated_load_uop(r < UOp.const(dtypes.index, 4), r)
|
||||
ranges = self.get_ranges(load.sink())
|
||||
self.assertEqual(len(ranges), 1)
|
||||
self.assertEqual(ranges[0].src[0].arg, 4)
|
||||
|
||||
def test_range_shrink_picks_max_guard(self):
|
||||
# two loads guard the same range with r < 4 and r < 8 -> shrink to max(4, 8) = 8
|
||||
r = Range(0, 204)
|
||||
load1 = get_gated_load_uop(r < UOp.const(dtypes.index, 4), r)
|
||||
load2 = get_gated_load_uop(r < UOp.const(dtypes.index, 8), r)
|
||||
ranges = self.get_ranges(UOp.sink(load1, load2))
|
||||
self.assertEqual(len(ranges), 1)
|
||||
self.assertEqual(ranges[0].src[0].arg, 8)
|
||||
|
||||
def test_range_no_shrink_guard_ge_max(self):
|
||||
# guard r < 300 with range max 204 -> no shrink (guard doesn't constrain)
|
||||
r = Range(0, 204)
|
||||
load = get_gated_load_uop(r < UOp.const(dtypes.index, 300), r)
|
||||
ranges = self.get_ranges(load.sink())
|
||||
self.assertEqual(len(ranges), 1)
|
||||
self.assertEqual(ranges[0].src[0].arg, 204)
|
||||
|
||||
def test_range_no_shrink_when_unguarded_elsewhere(self):
|
||||
# one load guards r < 4, but another load uses r without a gate -> no shrink
|
||||
r = Range(0, 204)
|
||||
load1 = get_gated_load_uop(r < UOp.const(dtypes.index, 4), r)
|
||||
load2 = UOp(Ops.LOAD, dtypes.float, (UOp(Ops.PARAM, dtypes.float.ptr(), arg=1).index(r, ptr=True),))
|
||||
ranges = self.get_ranges(UOp.sink(load1, load2))
|
||||
self.assertEqual(len(ranges), 1)
|
||||
self.assertEqual(ranges[0].src[0].arg, 204)
|
||||
|
||||
def test_range_no_shrink_when_used_in_reduce(self):
|
||||
# range used in both a gated load AND directly in the reduce expression -> no shrink
|
||||
r = Range(0, 204)
|
||||
gated_load = get_gated_load_uop(r < UOp.const(dtypes.index, 4), r)
|
||||
red = UOp(Ops.REDUCE, dtypes.float, (r.cast(dtypes.float) + gated_load, r), Ops.ADD)
|
||||
ranges = self.get_ranges(red.sink())
|
||||
self.assertEqual(len(ranges), 1)
|
||||
self.assertEqual(ranges[0].src[0].arg, 204)
|
||||
|
||||
def test_range_shrink_to_single_iteration(self):
|
||||
# guard r < 1 shrinks range to 1 -> single iteration, range eliminated entirely
|
||||
r = Range(0, 204)
|
||||
load = get_gated_load_uop(r < UOp.const(dtypes.index, 1), r)
|
||||
ranges = self.get_ranges(load.sink())
|
||||
self.assertEqual(len(ranges), 0)
|
||||
|
||||
def test_range_shrink_store_where_invalid(self):
|
||||
# emulates mask.where(x.pad_to(mask.shape), Invalid): range should shrink accordingly
|
||||
from tinygrad.dtype import Invalid
|
||||
r = Range(0, 204)
|
||||
x = (r < 4).where(UOp.const(dtypes.float, 1), Invalid)
|
||||
ranges = self.get_ranges(UOp(Ops.PARAM, dtypes.float.ptr(), arg=0).index(r).store((r < 4).where(x, 0)).sink())
|
||||
self.assertEqual(len(ranges), 1)
|
||||
self.assertEqual(ranges[0].src[0].arg, 4)
|
||||
|
||||
def test_range_shrink_store_where_invalid_flipped(self):
|
||||
# above, but flipped
|
||||
from tinygrad.dtype import Invalid
|
||||
r = Range(0, 204)
|
||||
x = (r < 4).where(UOp.const(dtypes.float, 1), Invalid)
|
||||
ranges = self.get_ranges(UOp(Ops.PARAM, dtypes.float.ptr(), arg=0).index(r).store((r < 4).where(0, x)).sink())
|
||||
self.assertEqual(len(ranges), 1)
|
||||
self.assertEqual(ranges[0].src[0].arg, 4)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -423,12 +423,10 @@ class TestUOpGraph(unittest.TestCase):
|
||||
d0 = UOp(Ops.PARAM, dtypes.long.ptr(), (), 0)
|
||||
ld = d0.index(ridx0.valid(ridx0<50))
|
||||
w = (ridx0<50).where(ld, 5)
|
||||
# prevent ridx0 from being shrunk
|
||||
red = UOp(Ops.REDUCE, dtypes.long, (ridx0.cast(dtypes.long), ridx0), Ops.ADD)
|
||||
uops = to_uops_list([w, red])
|
||||
uops = to_uops_list([w])
|
||||
for u in uops:
|
||||
assert u.op is not Ops.WHERE
|
||||
if u.op is Ops.LOAD and u.src[0].src[0].op is Ops.PARAM: assert u.src[1].arg==5
|
||||
if u.op is Ops.LOAD: assert u.src[1].arg==5
|
||||
|
||||
def test_where_on_gated_load_folds_swapped_branches(self):
|
||||
ridx0 = UOp.range(100, 0)
|
||||
@@ -446,12 +444,10 @@ class TestUOpGraph(unittest.TestCase):
|
||||
gate_idx = ridx0.valid((ridx0<50))
|
||||
ld = d0.index(gate_idx).cast(dtypes.float)
|
||||
w = (ridx0<50).where(ld, 5.0)
|
||||
# prevent ridx0 from being shrunk
|
||||
red = UOp(Ops.REDUCE, dtypes.long, (ridx0.cast(dtypes.long), ridx0), Ops.ADD)
|
||||
uops = to_uops_list([w, red])
|
||||
uops = to_uops_list([w])
|
||||
for u in uops:
|
||||
assert u.op is not Ops.WHERE
|
||||
if u.op is Ops.LOAD and u.src[0].src[0].op is Ops.PARAM: assert u.src[1].arg == 5
|
||||
if u.op is Ops.LOAD: assert u.src[1].arg == 5
|
||||
|
||||
def test_where_in_store_becomes_gate(self):
|
||||
ridx0 = UOp.range(100, 0)
|
||||
|
||||
@@ -58,8 +58,7 @@ class TestCfg(unittest.TestCase):
|
||||
k.emit(s_endpgm())
|
||||
k.emit(s_code_end())
|
||||
ei = run_asm("diamond", k)
|
||||
ret = amdgpu_cfg(ei.prg.p.lib, self.arch)
|
||||
cfg = ret["data"]
|
||||
cfg = amdgpu_cfg(ei.prg.p.lib, self.arch)["data"]
|
||||
self.assertEqual(len(cfg["blocks"]), 5)
|
||||
edge_count = sum(len(v) for v in cfg["paths"].values())
|
||||
self.assertEqual(edge_count, 5)
|
||||
@@ -70,11 +69,8 @@ class TestCfg(unittest.TestCase):
|
||||
self.assertEqual(len(references["r0"]), 2)
|
||||
insts = [cfg["pc_tokens"][pc][0]["st"] for pc in references["r0"]]
|
||||
self.assertEqual(insts, ['s_mov_b32', 's_cmp_eq_u64'])
|
||||
end_block = [" ".join(t["st"] for t in cfg["pc_tokens"][pc]) for pc in list(cfg["blocks"].values())[-1]]
|
||||
code_line = ret["src"].splitlines()[-1]
|
||||
self.assertEqual(len(end_block), 2)
|
||||
for st in [end_block[-1], code_line]:
|
||||
assert st.startswith("s_code_end") and st.endswith("x)"), st
|
||||
end_block_content = "\n".join(" ".join(t["st"] for t in cfg["pc_tokens"][pc]) for pc in list(cfg["blocks"].values())[-1])
|
||||
self.assertEqual(end_block_content, "s_endpgm\ns_code_end (217x)")
|
||||
|
||||
def test_loop(self):
|
||||
k = Kernel(arch=Device["AMD"].arch)
|
||||
|
||||
@@ -280,11 +280,6 @@ class TestAssign(unittest.TestCase):
|
||||
t.uop = t.uop.after(t[:5].uop.assign(Tensor.ones(5).uop))
|
||||
np.testing.assert_allclose(t.numpy(), [1.,1.,1.,1.,1.,0.,0.,0.,0.,0.])
|
||||
|
||||
def test_assign_after_target_chain(self):
|
||||
t = Tensor.arange(16).reshape(4, 4).permute(1, 0).contiguous()
|
||||
t.assign(t + 100)
|
||||
np.testing.assert_equal(t.numpy(), [[100, 104, 108, 112], [101, 105, 109, 113], [102, 106, 110, 114], [103, 107, 111, 115]])
|
||||
|
||||
def test_assign_contiguous(self):
|
||||
b = Tensor.arange(16).reshape(4,4).contiguous().realize()
|
||||
a = (Tensor.arange(16).reshape(4,4).contiguous().realize() + 1)
|
||||
|
||||
@@ -237,13 +237,5 @@ class TestCallSchedule(unittest.TestCase):
|
||||
self.assertIsInstance(out.shape[0], UOp)
|
||||
np.testing.assert_allclose(out[:5].numpy(), (np.arange(16*4).reshape(16, 4)[:5] * 2 + 1).astype(np.float32))
|
||||
|
||||
def test_precompile_multi_sharded(self):
|
||||
@function(precompile=True)
|
||||
def f(x:Tensor) -> Tensor: return x + 1
|
||||
devs = ("CPU:0", "CPU:1")
|
||||
a = Tensor.arange(8).reshape(4, 2).float().shard(devs, axis=0)
|
||||
out = f(a) + 2
|
||||
np.testing.assert_allclose(out.numpy(), np.arange(8, dtype=np.float32).reshape(4, 2) + 3)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -3,7 +3,6 @@ import unittest
|
||||
from tinygrad.function import function
|
||||
from tinygrad import Tensor
|
||||
from tinygrad.uop.ops import UOp
|
||||
from tinygrad.helpers import GlobalCounters
|
||||
|
||||
class TestFunction(unittest.TestCase):
|
||||
def test_simple(self):
|
||||
@@ -336,100 +335,5 @@ class TestFunctionMulti(unittest.TestCase):
|
||||
f(x).sum().backward()
|
||||
np.testing.assert_allclose(x.grad.numpy(), expected)
|
||||
|
||||
class TestFunctionTuple(unittest.TestCase):
|
||||
def test_tuple(self, precompile=False):
|
||||
x = Tensor.ones(3).contiguous()
|
||||
@function(precompile=precompile)
|
||||
def f(t:Tensor): return (t+1, t+2)
|
||||
t1, t2 = f(x)
|
||||
t1.realize(t2)
|
||||
print(t1.tolist(), t2.tolist())
|
||||
assert t1.tolist() == [2,2,2]
|
||||
assert t2.tolist() == [3,3,3]
|
||||
def test_tuple_precompile(self): self.test_tuple(True)
|
||||
|
||||
class TestFunctionBackward(unittest.TestCase):
|
||||
def test_backward_has_grad(self):
|
||||
N = 16
|
||||
x = Tensor.empty(N, N)
|
||||
w1 = Tensor.empty(N, N, requires_grad=True)
|
||||
@function
|
||||
def f(t:Tensor, w1:Tensor): return (t@w1)
|
||||
f(x, w1).sum().backward()
|
||||
assert w1.grad is not None
|
||||
|
||||
def test_double_matmul_backward(self):
|
||||
N = 16
|
||||
x = Tensor.empty(N, N)
|
||||
w1 = Tensor.empty(N, N, requires_grad=True)
|
||||
w2 = Tensor.empty(N, N, requires_grad=True)
|
||||
ref = Tensor.empty(N)
|
||||
|
||||
@function(precompile=True)
|
||||
def f(t:Tensor, w1:Tensor, w2:Tensor): return (t@w1)@w2
|
||||
loss = (f(x, w1, w2)-ref).square().mean().backward()
|
||||
loss.realize(w1.grad, w2.grad)
|
||||
|
||||
def test_backward_single_call(self):
|
||||
N = 4
|
||||
x = Tensor.arange(N*N).reshape(N, N).float()
|
||||
w1 = Tensor.arange(N*N).reshape(N, N).float().requires_grad_()
|
||||
w2 = Tensor.arange(N*N).reshape(N, N).float().requires_grad_()
|
||||
xn, w1n, w2n = x.numpy(), w1.numpy(), w2.numpy()
|
||||
@function
|
||||
def f(t:Tensor, w1:Tensor, w2:Tensor): return (t@w1)@w2
|
||||
f(x, w1, w2).sum().backward()
|
||||
assert w1.grad is not None and w2.grad is not None
|
||||
np.testing.assert_allclose(w1.grad.numpy(), xn.T @ np.ones((N,N)) @ w2n.T, atol=1e-3)
|
||||
np.testing.assert_allclose(w2.grad.numpy(), (xn @ w1n).T @ np.ones((N,N)), atol=1e-3)
|
||||
|
||||
def test_backward_precompile_backward(self):
|
||||
N = 4
|
||||
x = Tensor.arange(N*N).reshape(N, N).float().contiguous()
|
||||
w1 = Tensor.arange(N*N).reshape(N, N).float().requires_grad_().contiguous()
|
||||
w2 = Tensor.arange(N*N).reshape(N, N).float().requires_grad_().contiguous()
|
||||
Tensor.realize(x, w1, w2)
|
||||
xn, w1n, w2n = x.numpy(), w1.numpy(), w2.numpy()
|
||||
@function(precompile=True, precompile_backward=True)
|
||||
def f(t:Tensor, w1:Tensor, w2:Tensor): return (t@w1)@w2
|
||||
loss = f(x, w1, w2).sum().backward()
|
||||
assert w1.grad is not None and w2.grad is not None
|
||||
GlobalCounters.reset()
|
||||
Tensor.realize(loss, w1.grad, w2.grad)
|
||||
np.testing.assert_allclose(w1.grad.numpy(), xn.T @ np.ones((N,N)) @ w2n.T, atol=1e-3)
|
||||
np.testing.assert_allclose(w2.grad.numpy(), (xn @ w1n).T @ np.ones((N,N)), atol=1e-3)
|
||||
|
||||
def test_backward_precompile_backward_tuple(self):
|
||||
N = 4
|
||||
x = Tensor.arange(N*N).reshape(N, N).float().contiguous()
|
||||
w1 = Tensor.arange(N*N).reshape(N, N).float().requires_grad_().contiguous()
|
||||
w2 = Tensor.arange(N*N).reshape(N, N).float().requires_grad_().contiguous()
|
||||
Tensor.realize(x, w1, w2)
|
||||
xn, w1n, w2n = x.numpy(), w1.numpy(), w2.numpy()
|
||||
# non-tuple reference
|
||||
ref_w1 = Tensor.arange(N*N).reshape(N, N).float().requires_grad_().contiguous()
|
||||
ref_w2 = Tensor.arange(N*N).reshape(N, N).float().requires_grad_().contiguous()
|
||||
Tensor.realize(ref_w1, ref_w2)
|
||||
@function(precompile=True, precompile_backward=True)
|
||||
def g(t:Tensor, w1:Tensor, w2:Tensor): return (t@w1)@w2
|
||||
g(x, ref_w1, ref_w2).sum().backward()
|
||||
GlobalCounters.reset()
|
||||
Tensor.realize(ref_w1.grad, ref_w2.grad)
|
||||
ref_ops = GlobalCounters.global_ops
|
||||
# tuple version with intermediate — should not redo forward compute
|
||||
@function(precompile=True, precompile_backward=True)
|
||||
def f(t:Tensor, w1:Tensor, w2:Tensor):
|
||||
h = t@w1
|
||||
return (h, h@w2)
|
||||
h, out = f(x, w1, w2)
|
||||
loss = out.sum().backward()
|
||||
assert w1.grad is not None and w2.grad is not None
|
||||
GlobalCounters.reset()
|
||||
Tensor.realize(loss, w1.grad, w2.grad)
|
||||
np.testing.assert_allclose(w1.grad.numpy(), xn.T @ np.ones((N,N)) @ w2n.T, atol=1e-3)
|
||||
np.testing.assert_allclose(w2.grad.numpy(), (xn @ w1n).T @ np.ones((N,N)), atol=1e-3)
|
||||
# tuple version should have fewer ops than non-tuple (saves recomputing h=t@w1 in backward)
|
||||
self.assertLessEqual(GlobalCounters.global_ops, ref_ops)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -36,7 +36,8 @@ class TestSetitemInto(unittest.TestCase):
|
||||
self.assertEqual(GlobalCounters.kernel_count, 0)
|
||||
t.realize()
|
||||
self.assertEqual(GlobalCounters.kernel_count, 1)
|
||||
self.assertEqual(GlobalCounters.global_mem, 4)
|
||||
# TODO: this can be just 4 if empty goes through is_realized setitem path
|
||||
self.assertEqual(GlobalCounters.global_mem, 4*(3*2+1)) # 3 elements had +1, 1 is assigned directly
|
||||
t[1].realize()
|
||||
t.realize()
|
||||
self.assertEqual(GlobalCounters.kernel_count, 1)
|
||||
|
||||
@@ -19,7 +19,7 @@ from tinygrad.codegen.late.devectorizer import load_store_folding, load_store_in
|
||||
ReduceContext, correct_load_store, pm_render, pm_add_loads
|
||||
from tinygrad.codegen.opt.postrange import apply_opts, pm_make_images
|
||||
from tinygrad.codegen.simplify import pm_simplify_ranges, pm_flatten_range, pm_split_ranges, pm_load_collapse
|
||||
from tinygrad.schedule.rangeify import pm_add_buffers_local, rangeify_codegen, pm_mops, pm_syntactic_sugar, pm_store_ranges
|
||||
from tinygrad.schedule.rangeify import pm_add_buffers_local, rangeify_codegen, pm_mops, pm_syntactic_sugar
|
||||
from tinygrad.codegen.late.linearizer import CFGContext, pm_split_ends, pm_add_control_flow, linearize
|
||||
from tinygrad.renderer.amd.elf import do_assemble_amd
|
||||
|
||||
@@ -31,7 +31,7 @@ def full_rewrite_to_sink(sink:UOp, ren:Renderer|None=None, optimize:bool=True) -
|
||||
if SPEC: type_verify(sink, kernel_spec)
|
||||
|
||||
# preprocess
|
||||
sink = graph_rewrite(sink, pm_mops+pm_syntactic_sugar+pm_store_ranges, ctx=itertools.count(1000), name="early movement ops", bottom_up=True)
|
||||
sink = graph_rewrite(sink, pm_mops+pm_syntactic_sugar, name="early movement ops", bottom_up=True)
|
||||
|
||||
# first we optimize
|
||||
if optimize:
|
||||
@@ -48,7 +48,7 @@ def full_rewrite_to_sink(sink:UOp, ren:Renderer|None=None, optimize:bool=True) -
|
||||
sink = graph_rewrite(sink, sym+pm_flatten_range, name="initial symbolic")
|
||||
|
||||
# optimize (schedule) the AST
|
||||
sink = graph_rewrite(sink, pm_flatten_range+pm_simplify_ranges, ctx={}, name="simplify ranges")
|
||||
sink = graph_rewrite(sink, pm_simplify_ranges, name="simplify ranges")
|
||||
|
||||
# do postrange optimization, BEAM or hand_coded_optimizations
|
||||
sink = apply_opts(sink, ren)
|
||||
|
||||
@@ -190,9 +190,9 @@ def _do_image_fixup(dt:ImageDType, idx:UOp) -> tuple[UOp, UOp, int, int]:
|
||||
if IMAGE == 1 and valid is not None:
|
||||
h, w = max(ImageDType.valid_dims(dt), key=lambda hw:
|
||||
# maximize number of valids removed
|
||||
(len(_drop_valid_stmts(valid, idx:=uop_given_valid(valid, UOp.vectorize((x//4)%hw[1], x//(4*hw[1])).simplify()), *hw)),
|
||||
(len(_drop_valid_stmts(valid, idx:=uop_given_valid(valid, UOp.vectorize((x//4)%hw[1], x//(4*hw[1]))), *hw)),
|
||||
# and minimize idx complexity (number of nodes)
|
||||
-len(idx.gep(1).backward_slice)))
|
||||
-len(idx.backward_slice)))
|
||||
buf = buf.replace(dtype=(dtypes.imageh if dt.itemsize == 2 else dtypes.imagef)((h, w, 4), w * 4 * dt.itemsize))
|
||||
oidx = UOp(Ops.VECTORIZE, dtypes.index.vec(2), ((x // 4) % w, (x // (4*w))))
|
||||
return x, idx.replace(src=(buf, oidx.valid(valid))), w, h
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import itertools
|
||||
from typing import Callable
|
||||
from tinygrad.uop.ops import UOp, PatternMatcher, UPat, Ops, graph_rewrite, _substitute, range_start
|
||||
from tinygrad.uop.symbolic import symbolic
|
||||
from tinygrad.helpers import partition
|
||||
@@ -37,32 +36,22 @@ def simplify_merge_adjacent(u:UOp) -> UOp|None:
|
||||
u = nidx
|
||||
return u
|
||||
|
||||
def mark_gated(ctx, idx):
|
||||
if idx.src[1].op is Ops.WHERE:
|
||||
x, cond = idx.src[1].get_idx(), idx.src[1].get_valid()
|
||||
# get all ranges r with guards "r < c" for some const c
|
||||
guards = {r:c for v in cond.split_uop(Ops.AND) if v.op is Ops.CMPLT and (r:=v.src[0]).op is Ops.RANGE and (c:=v.src[1]).op is Ops.CONST}
|
||||
else: x, guards = idx, {}
|
||||
# ensure that we choose max(c_i) for all i where r < c_i
|
||||
ctx |= {r:c for r,c in guards.items() if (r not in ctx or ctx[r].arg < c.arg)}
|
||||
# but if a range is ever ungated, we cannot shrink it
|
||||
ctx |= {r:r.src[0] for r in x.ranges if r not in guards}
|
||||
|
||||
pm_simplify_ranges = PatternMatcher([
|
||||
(UPat((Ops.END, Ops.REDUCE), name="u"), simplify_merge_adjacent),
|
||||
(UPat(Ops.INDEX, name="idx"), mark_gated),
|
||||
# reduce ranges can't be shrunk
|
||||
(UPat(Ops.REDUCE, name="red"), lambda ctx, red: ctx.update({r:r.src[0] for r in red.src[1:]})),
|
||||
(UPat(Ops.SINK, name="x"), lambda ctx, x: do_substitute(ctx, x, lambda r,c: r.replace(src=(c,)))),
|
||||
])
|
||||
|
||||
def mark_range_mod(ctx:dict[UOp, UOp|None], r:UOp, c:UOp) -> None:
|
||||
if r not in ctx and r.src[0].op is Ops.CONST and r.src[0].divides(c.arg) is not None: ctx[r] = c
|
||||
|
||||
def do_substitute(ctx:dict, x: UOp, sub_fxn:Callable[[UOp, UOp], UOp]) -> UOp|None:
|
||||
ret = x.substitute({k:sub_fxn(k,v) for k,v in ctx.items() if v is not None})
|
||||
def do_substitute(ctx:dict[UOp, UOp|None], x: UOp) -> UOp|None:
|
||||
subs = {}
|
||||
for k,v in ctx.items():
|
||||
if v is not None:
|
||||
subs[k] = k.replace(src=(k.src[0]//v,), arg=k.arg[0:-1]+(0,k.arg[-1]))*v + k.replace(src=(v,), arg=k.arg[0:-1]+(1,k.arg[-1]))
|
||||
if not len(subs): return None
|
||||
ret = x.substitute(subs).simplify()
|
||||
ctx.clear()
|
||||
return None if ret is x else ret.simplify()
|
||||
return ret
|
||||
|
||||
def dont_sub_ranges_for_image(ctx:dict[UOp, UOp|None], x:UOp) -> None:
|
||||
if isinstance(x.src[0].src[0].dtype, ImageDType):
|
||||
@@ -71,8 +60,7 @@ def dont_sub_ranges_for_image(ctx:dict[UOp, UOp|None], x:UOp) -> None:
|
||||
pm_split_ranges = PatternMatcher([
|
||||
(UPat(Ops.RANGE, name="r")%UPat.cvar("c"), mark_range_mod),
|
||||
(UPat(Ops.STORE, name="x"), dont_sub_ranges_for_image),
|
||||
(UPat(Ops.SINK, name="x"), lambda ctx, x: do_substitute(ctx, x,
|
||||
lambda k,v: k.replace(src=(k.src[0]//v,), arg=k.arg[0:-1]+(0,k.arg[-1]))*v + k.replace(src=(v,), arg=k.arg[0:-1]+(1,k.arg[-1])))),
|
||||
(UPat(Ops.SINK, name="x"), do_substitute),
|
||||
])
|
||||
|
||||
# **** reduce simplification ****
|
||||
|
||||
@@ -46,19 +46,18 @@ def _buffer_like(u:UOp) -> UOp:
|
||||
if prod(dtype.shape) != prod(u.max_shard_shape) or ([x for x in u.max_shard_shape if x != 1] or [1])[-1] % 4 != 0:
|
||||
if DEBUG >= 1: print(f"demoting Image {dtype} with shape {u.max_shard_shape}")
|
||||
dtype = dtype.base
|
||||
buffer = UOp.new_buffer(u.device, u.shard_size, dtype).reshape(u.max_shard_shape).shrink_to(u.shard_shape)
|
||||
buffer = UOp.new_buffer(u.device, u.shard_size, dtype).reshape(u.max_shard_shape)
|
||||
if isinstance(u.device, tuple) and u.axis is not None: buffer = buffer.multi(u.axis)
|
||||
return buffer
|
||||
|
||||
def replace_contig_with_store_after(u:UOp):
|
||||
def replace_contig_with_assign(u:UOp):
|
||||
# can't allocate a buffer without a device (e.g., inside a CALL function body with only PARAMs)
|
||||
if u._device is None: return None
|
||||
# if size is 0, remove the contig
|
||||
if u.size == 0: return u.src[0]
|
||||
# no real contig for DISK/TINYFS tensors, they are left alone
|
||||
if isinstance(u._device, str) and u._device.startswith(("DISK", "TINYFS")): return u.rtag(None)
|
||||
buf = _buffer_like(u)
|
||||
return buf.after(buf.store(u.src[0])).rtag(u.tag)
|
||||
return _buffer_like(u).assign(u.src[0]).rtag(u.tag)
|
||||
|
||||
def replace_assign_with_contig(u:UOp):
|
||||
assigned_to = u
|
||||
@@ -94,23 +93,9 @@ def contiguous_mops_to_view(c:UOp):
|
||||
def transform_precompiled_call(c:UOp) -> UOp|None:
|
||||
if not c.arg.precompile: return None
|
||||
if c.src[0].op is Ops.SINK: return None
|
||||
input_buffers = tuple(x.contiguous() if x.op not in {Ops.AFTER, Ops.BIND} else x for x in c.src[1:])
|
||||
# multi-output (TUPLE) precompiled calls: allocate a buffer per output and return a TUPLE of buffers
|
||||
if c.src[0].op is Ops.TUPLE:
|
||||
tuple_body = c.src[0]
|
||||
out_bufs = []
|
||||
sink_srcs = []
|
||||
for i, elem in enumerate(tuple_body.src):
|
||||
buf = UOp.new_buffer(c.device, prod(elem.max_shape), elem.dtype).reshape(elem.max_shape).shrink_to(elem.shape)
|
||||
out_bufs.append(buf)
|
||||
target = buf.param_like(len(c.src) - 1 + i).shrink_to(elem.shape)
|
||||
sink_srcs.append(target.after(target.store(elem)))
|
||||
fxn = UOp.sink(*sink_srcs)
|
||||
new_call = c.replace(src=(fxn, *input_buffers, *out_bufs), dtype=dtypes.void, tag=None)
|
||||
return UOp.maketuple(*[buf.after(new_call) for buf in out_bufs])
|
||||
out = _buffer_like(c)
|
||||
target = out.param_like(len(c.src)-1).shrink_to(c.shape)
|
||||
fxn = target.after(target.store(c.src[0])).sink()
|
||||
input_buffers = tuple(x.contiguous() if x.op not in {Ops.AFTER, Ops.BIND} else x for x in c.src[1:])
|
||||
fxn = out.param_like(len(c.src)-1).assign(c.src[0]).sink()
|
||||
ret = out.after(c.replace(src=(fxn, *input_buffers, out), dtype=dtypes.void, tag=None))
|
||||
# if the CALL has symbolic shapes, shrink the max-sized output to the actual symbolic shape
|
||||
if any(isinstance(s, UOp) for s in c.shape): ret = ret.shrink(tuple((0, s) for s in c.shape))
|
||||
@@ -125,15 +110,14 @@ pm_early_transform_tensor_graph = PatternMatcher([
|
||||
(UPat(Ops.CONTIGUOUS, src=(UPat(GroupOp.Movement),), name="c"), contiguous_mops_to_view),
|
||||
|
||||
# add CONTIGUOUS to tagged UOps
|
||||
(UPat(GroupOp.All-{Ops.CONTIGUOUS, Ops.ASSIGN, Ops.AFTER, Ops.STORE}, name="x"),
|
||||
lambda x: x.rtag(None).contiguous(tag=x.tag) if x.tag else x.replace(tag=None)),
|
||||
# remove extra CONTIGUOUS on ASSIGN/AFTER (only when target is contiguous)
|
||||
(UPat(Ops.CONTIGUOUS, src=(UPat({Ops.ASSIGN, Ops.AFTER}, name="a"),), name="c"),
|
||||
(UPat(GroupOp.All-{Ops.CONTIGUOUS, Ops.ASSIGN}, name="x"), lambda x: x.rtag(None).contiguous(tag=x.tag) if x.tag else x.replace(tag=None)),
|
||||
# remove extra CONTIGUOUS on ASSIGN (only when assign target is contiguous)
|
||||
(UPat(Ops.CONTIGUOUS, src=(UPat(Ops.ASSIGN, name="a"),), name="c"),
|
||||
lambda a,c: a.replace(tag=(a.tag or ())+(c.tag or ())) if a.src[0].has_buffer_identity() else None),
|
||||
# replace ASSIGN with CONTIGUOUS
|
||||
(UPat(Ops.ASSIGN, name="u"), replace_assign_with_contig),
|
||||
# replace CONTIGUOUS with STORE+AFTER
|
||||
(UPat(Ops.CONTIGUOUS, name="u"), replace_contig_with_store_after),
|
||||
# replace CONTIGUOUS with ASSIGNs
|
||||
(UPat(Ops.CONTIGUOUS, name="u"), replace_contig_with_assign),
|
||||
# remove DETACH/CONTIGUOUS_BACKWARD (allows more contiguous removal)
|
||||
(UPat((Ops.DETACH, Ops.CONTIGUOUS_BACKWARD), name="x"), lambda x: x.src[0]),
|
||||
])
|
||||
@@ -144,9 +128,9 @@ def untag_and_append(ctx:AllocCtx, x:UOp):
|
||||
for t in x.tag:
|
||||
original_uop: UOp = ctx.uop_list[t]
|
||||
replace_uop = ret
|
||||
while replace_uop.op in {Ops.ASSIGN, Ops.AFTER}: replace_uop = replace_uop.src[0]
|
||||
while replace_uop.op is Ops.ASSIGN: replace_uop = replace_uop.src[0]
|
||||
ctx.buffer_map[original_uop] = replace_uop.shrink_to(original_uop.shape)
|
||||
if ret.op is not Ops.AFTER: ctx.assigns.append(ret) # AFTER gets appended by append_after
|
||||
ctx.assigns.append(ret)
|
||||
return ret
|
||||
|
||||
def append_after(ctx:AllocCtx, x:UOp):
|
||||
@@ -158,7 +142,7 @@ def replace_input_buffer(ctx:AllocCtx, b:UOp):
|
||||
b._min_max if b.op is Ops.BIND else None, b.src[0].arg[0] if b.op is Ops.BIND else None)
|
||||
|
||||
pm_finalize_call = PatternMatcher([
|
||||
(UPat({Ops.ASSIGN, Ops.AFTER}, name="x"), untag_and_append),
|
||||
(UPat(Ops.ASSIGN, name="x"), untag_and_append),
|
||||
(UPat(Ops.AFTER, name="x"), append_after),
|
||||
(UPat(Ops.COPY, name="x"), lambda ctx,x: append_after(ctx,x) if isinstance(x.device, str) and x.device.startswith(("DISK", "TINYFS")) else None),
|
||||
# remove unique from const. TODO: this is copied in function.py
|
||||
|
||||
+10
-80
@@ -2,7 +2,6 @@ import functools
|
||||
from typing import Generic, TypeVar, Callable, cast, overload
|
||||
from tinygrad.helpers import Context, dedup, getenv
|
||||
from tinygrad.uop.ops import UOp, Ops, graph_rewrite, PatternMatcher, UPat
|
||||
from tinygrad.gradient import compute_gradient
|
||||
from tinygrad.tensor import Tensor
|
||||
|
||||
def add_to_ctx(ctx, x:UOp):
|
||||
@@ -20,10 +19,9 @@ pm_ctx = PatternMatcher([
|
||||
|
||||
ReturnType = TypeVar('ReturnType')
|
||||
class _function(Generic[ReturnType]):
|
||||
def __init__(self, fxn:Callable[..., ReturnType], *, precompile:bool=False, precompile_backward:bool=False):
|
||||
def __init__(self, fxn:Callable[..., ReturnType], *, precompile:bool=False):
|
||||
self.fxn = fxn
|
||||
self.precompile = precompile
|
||||
self.precompile_backward = precompile_backward
|
||||
|
||||
def __get__(self, obj, objtype=None): return functools.partial(self.__call__, obj) if obj is not None else self
|
||||
|
||||
@@ -41,17 +39,12 @@ class _function(Generic[ReturnType]):
|
||||
# run it and do surgery later
|
||||
with Context(ALLOW_DEVICE_USAGE=getenv("DEVICE_IN_FUNCTION_BUG", 0)):
|
||||
ret = self.fxn(*args, **kwargs)
|
||||
if isinstance(ret, Tensor):
|
||||
uret = ret.uop
|
||||
elif isinstance(ret, tuple) and all(isinstance(x, Tensor) for x in ret):
|
||||
uret = UOp.maketuple(*[x.uop for x in ret])
|
||||
else:
|
||||
raise RuntimeError(f"function return type {type(ret)} not supported")
|
||||
assert isinstance(ret, Tensor), "only supports one tensor return for now"
|
||||
|
||||
# replace the known inputs with params (using deduplicated slots)
|
||||
subs = {}
|
||||
for i,x in enumerate(call_uops): subs[x] = x.param_like(i)
|
||||
uret = uret.substitute(subs)
|
||||
uret = ret.uop.substitute(subs)
|
||||
|
||||
# add contiguous to call_uops
|
||||
#call_uops = [x.contiguous() for x in call_uops]
|
||||
@@ -67,77 +60,14 @@ class _function(Generic[ReturnType]):
|
||||
#call = assigned.call(*call_uops, buffer, name=name)
|
||||
#ret = buffer.after(call)
|
||||
|
||||
# precompute the backward: determine which inputs need gradients and build the backward CALL body
|
||||
grad_fxn = None
|
||||
inputs = [t for t in list(args)+[kwargs[k] for k in sorted(kwargs)] if isinstance(t, (Tensor, UOp))]
|
||||
grad_params = {x.arg:x for x in uret.toposort(enter_calls=False) if x.op == Ops.PARAM}
|
||||
# find which param slots correspond to requires_grad inputs
|
||||
need_grad = {i for i, t in enumerate(inputs) if isinstance(t, Tensor) and t.requires_grad}
|
||||
target_params = {grad_params[i] for i in need_grad if i in grad_params}
|
||||
if target_params:
|
||||
grad_fxn = self._make_grad_fxn(uret, len(call_uops), target_params, need_grad, name, self.precompile_backward)
|
||||
|
||||
fret = uret.call(*call_uops, name=name, precompile=self.precompile, precompile_backward=self.precompile_backward, grad_fxn=grad_fxn)
|
||||
if isinstance(ret, tuple):
|
||||
return cast(ReturnType, tuple(Tensor(fret.gettuple(i), device=fret.device) for i in range(len(ret))))
|
||||
else:
|
||||
return cast(ReturnType, Tensor(fret, device=fret.device))
|
||||
|
||||
@staticmethod
|
||||
def _make_grad_fxn(uret:UOp, num_args:int, target_params:set[UOp], need_grad:set[int], name:str, precompile_backward:bool):
|
||||
def grad_fxn(ctx, k):
|
||||
fxn, args = k.src[0], k.src[1:]
|
||||
params = {x.arg:x for x in fxn.toposort(enter_calls=False) if x.op == Ops.PARAM}
|
||||
# compute gradients only for needed params
|
||||
if isinstance(ctx, dict):
|
||||
all_grads: dict[UOp, UOp] = {}
|
||||
for idx, grad_out in ctx.items():
|
||||
elem_grads = compute_gradient(fxn.src[idx], grad_out.param_like(len(args) + idx), target_params)
|
||||
for p, g in elem_grads.items():
|
||||
if p in all_grads: all_grads[p] = all_grads[p] + g
|
||||
else: all_grads[p] = g
|
||||
grads = all_grads
|
||||
grad_ctx_inputs = tuple(ctx.get(i, fxn.src[i].const_like(0)) for i in range(len(fxn.src)))
|
||||
else:
|
||||
grads = compute_gradient(fxn, ctx.param_like(len(args)), target_params)
|
||||
grad_ctx_inputs = (ctx,)
|
||||
# collect gradients for needed params only
|
||||
grad_indices: list[int] = []
|
||||
grad_uops: list[UOp] = []
|
||||
for i in range(len(args)):
|
||||
if i in need_grad and (p:=params.get(i)) is not None and p in grads:
|
||||
grad_indices.append(i)
|
||||
grad_uops.append(grads[p])
|
||||
if len(grad_uops) == 0: return (None,) * len(args)
|
||||
# replace forward output references with PARAMs to avoid recomputation
|
||||
if fxn.op is Ops.TUPLE:
|
||||
fwd_subs = {elem: elem.param_like(len(args) + len(grad_ctx_inputs) + i) for i, elem in enumerate(fxn.src)}
|
||||
fwd_inputs = tuple(k.gettuple(i) for i in range(len(fxn.src)))
|
||||
else:
|
||||
fwd_subs = {fxn: fxn.param_like(len(args) + 1)}
|
||||
fwd_inputs = (k,)
|
||||
grad_uops = [g.substitute(fwd_subs) for g in grad_uops]
|
||||
# build a single backward CALL returning a TUPLE of all gradients
|
||||
bwd_body = UOp.maketuple(*grad_uops)
|
||||
bwd_call = bwd_body.call(*args, *grad_ctx_inputs, *fwd_inputs, name=name+"_backward", precompile=precompile_backward)
|
||||
# extract each gradient via GETTUPLE
|
||||
ret: list[UOp|None] = []
|
||||
gi = 0
|
||||
for i in range(len(args)):
|
||||
if gi < len(grad_indices) and grad_indices[gi] == i:
|
||||
ret.append(bwd_call.gettuple(gi))
|
||||
gi += 1
|
||||
else:
|
||||
ret.append(None)
|
||||
return tuple(ret)
|
||||
return grad_fxn
|
||||
ret = uret.call(*call_uops, name=name, precompile=self.precompile)
|
||||
return cast(ReturnType, Tensor(ret, device=ret.device))
|
||||
|
||||
# overload signatures support both @function and @function(precompile=True) syntax
|
||||
@overload
|
||||
def function(fxn:Callable[..., ReturnType], *, precompile:bool=False, precompile_backward:bool=False) -> _function[ReturnType]: ...
|
||||
def function(fxn:Callable[..., ReturnType], *, precompile:bool=False) -> _function[ReturnType]: ...
|
||||
@overload
|
||||
def function(fxn:None=None, *, precompile:bool=False, precompile_backward:bool=False) -> \
|
||||
Callable[[Callable[..., ReturnType]], _function[ReturnType]]: ...
|
||||
def function(fxn=None, *, precompile:bool=False, precompile_backward:bool=False):
|
||||
if fxn is None: return lambda f: _function(f, precompile=precompile, precompile_backward=precompile_backward)
|
||||
return _function(fxn, precompile=precompile, precompile_backward=precompile_backward)
|
||||
def function(fxn:None=None, *, precompile:bool=False) -> Callable[[Callable[..., ReturnType]], _function[ReturnType]]: ...
|
||||
def function(fxn=None, *, precompile:bool=False):
|
||||
if fxn is None: return lambda f: _function(f, precompile=precompile)
|
||||
return _function(fxn, precompile=precompile)
|
||||
|
||||
+10
-60
@@ -13,53 +13,18 @@ def reduce_gradient(ctx:UOp, ret:UOp, op:Ops):
|
||||
return ((mask/broadcast_to_input(count)) * broadcast_to_input(ctx),)
|
||||
if op == Ops.MUL: return (broadcast_to_input(ctx * ret) / ret.src[0],)
|
||||
|
||||
def call_gradient(ctx:UOp|dict[int, UOp], k:UOp) -> tuple[UOp|None, ...]:
|
||||
if k.arg.grad_fxn is not None:
|
||||
if isinstance(ctx, dict): return (None,) + k.arg.grad_fxn(ctx, k)
|
||||
return (None,) + k.arg.grad_fxn(ctx, k)
|
||||
def call_gradient(ctx:UOp, k:UOp) -> tuple[UOp|None, ...]:
|
||||
if k.arg.grad_fxn is not None: return (None,) + k.arg.grad_fxn(ctx, k)
|
||||
# auto-differentiate the function
|
||||
fxn, args = k.src[0], k.src[1:]
|
||||
params = {x.arg:x for x in fxn.toposort(enter_calls=False) if x.op == Ops.PARAM}
|
||||
# for tuple-returning CALLs, ctx is a dict mapping output index to gradient; differentiate each output separately and sum
|
||||
if isinstance(ctx, dict):
|
||||
all_grads: dict[UOp, UOp] = {}
|
||||
for idx, grad_out in ctx.items():
|
||||
elem_grads = compute_gradient(fxn.src[idx], grad_out.param_like(len(args) + idx), set(params.values()))
|
||||
for p, g in elem_grads.items():
|
||||
if p in all_grads: all_grads[p] = all_grads[p] + g
|
||||
else: all_grads[p] = g
|
||||
grads = all_grads
|
||||
grad_ctx_inputs = tuple(ctx.get(i, fxn.src[i].const_like(0)) for i in range(len(fxn.src)))
|
||||
else:
|
||||
grads = compute_gradient(fxn, ctx.param_like(len(args)), set(params.values()))
|
||||
grad_ctx_inputs = (ctx,)
|
||||
# collect which args have gradients
|
||||
grad_indices: list[int] = []
|
||||
grad_uops: list[UOp] = []
|
||||
grads = compute_gradient(fxn, ctx.param_like(len(args)), set(params.values()))
|
||||
ret: list[UOp|None] = [None]
|
||||
for i in range(len(args)):
|
||||
if (p:=params.get(i, None)) is not None and p in grads:
|
||||
# TODO: compact the args and remove unused ones
|
||||
assert not grads[p].op_in_backward_slice_with_self(Ops.BUFFER), "BUG: BUFFER in backward slice of grad"
|
||||
grad_indices.append(i)
|
||||
grad_uops.append(grads[p])
|
||||
if len(grad_uops) == 0: return (None,) * (len(args) + 1)
|
||||
# replace forward output references with PARAMs, passing the forward CALL output(s) as inputs to avoid recomputation
|
||||
if fxn.op is Ops.TUPLE:
|
||||
fwd_subs = {elem: elem.param_like(len(args) + len(grad_ctx_inputs) + i) for i, elem in enumerate(fxn.src)}
|
||||
fwd_inputs = tuple(k.gettuple(i) for i in range(len(fxn.src)))
|
||||
else:
|
||||
fwd_subs = {fxn: fxn.param_like(len(args) + 1)}
|
||||
fwd_inputs = (k,)
|
||||
grad_uops = [g.substitute(fwd_subs) for g in grad_uops]
|
||||
# build a single backward CALL returning a TUPLE of all gradients
|
||||
bwd_body = UOp.maketuple(*grad_uops)
|
||||
bwd_call = bwd_body.call(*args, *grad_ctx_inputs, *fwd_inputs, name=(k.arg.name or "")+"_backward", precompile=k.arg.precompile_backward)
|
||||
# extract each gradient via GETTUPLE
|
||||
ret: list[UOp|None] = [None]
|
||||
gi = 0
|
||||
for i in range(len(args)):
|
||||
if gi < len(grad_indices) and grad_indices[gi] == i:
|
||||
ret.append(bwd_call.gettuple(gi))
|
||||
gi += 1
|
||||
ret.append(grads[p].call(*args, ctx, name=(k.arg.name or "")+f"_backward_{i}"))
|
||||
else:
|
||||
ret.append(None)
|
||||
return tuple(ret)
|
||||
@@ -107,26 +72,11 @@ def _deepwalk(root:UOp, targets:set[UOp]) -> list[UOp]:
|
||||
return list(root.toposort(lambda node: node.op not in {Ops.DETACH, Ops.ASSIGN} and in_target_path[node]))
|
||||
|
||||
def compute_gradient(root:UOp, root_grad:UOp, targets:set[UOp]) -> dict[UOp, UOp]:
|
||||
grads: dict[UOp, UOp] = {root: root_grad}
|
||||
# for GETTUPLE nodes on tuple-returning CALLs, collect per-output gradients
|
||||
tuple_call_grads: dict[UOp, dict[int, UOp]] = {}
|
||||
grads = {root: root_grad}
|
||||
for t0 in reversed(_deepwalk(root, targets)):
|
||||
if t0 not in grads and t0 not in tuple_call_grads: continue
|
||||
# for tuple-returning CALLs, use accumulated per-output gradients
|
||||
if t0.op is Ops.CALL and t0 in tuple_call_grads:
|
||||
lgrads = cast(tuple[UOp|None, ...], call_gradient(tuple_call_grads[t0], t0))
|
||||
elif t0 not in grads:
|
||||
continue
|
||||
# for GETTUPLE on a CALL, accumulate gradient per output index instead of propagating to CALL directly
|
||||
elif t0.op is Ops.GETTUPLE and t0.src[0].op is Ops.CALL:
|
||||
call = t0.src[0]
|
||||
if call not in tuple_call_grads: tuple_call_grads[call] = {}
|
||||
if t0.arg in tuple_call_grads[call]: tuple_call_grads[call][t0.arg] = tuple_call_grads[call][t0.arg] + grads[t0]
|
||||
else: tuple_call_grads[call][t0.arg] = grads[t0]
|
||||
continue
|
||||
else:
|
||||
lgrads = cast(tuple[UOp|None, ...]|None, pm_gradient.rewrite(t0, ctx=grads[t0]))
|
||||
if lgrads is None: raise RuntimeError(f"failed to compute gradient for {t0.op}\n\nin {str(t0)[0:1000]}...")
|
||||
if t0 not in grads: continue
|
||||
lgrads: tuple[UOp|None, ...]|None = cast(tuple[UOp|None, ...]|None, pm_gradient.rewrite(t0, ctx=grads[t0]))
|
||||
if lgrads is None: raise RuntimeError(f"failed to compute gradient for {t0.op}\n\nin {str(t0)[0:1000]}...")
|
||||
assert len(lgrads) == len(t0.src), f"got {len(lgrads)} gradient, expected {len(t0.src)}"
|
||||
for k,v in zip(t0.src, lgrads):
|
||||
if v is None: continue
|
||||
|
||||
+2
-16
@@ -303,7 +303,7 @@ class RMSNorm:
|
||||
x = self._norm(x.float()).cast(x.dtype)
|
||||
return x if self.weight is None else x * self.weight
|
||||
|
||||
from tinygrad.uop.ops import UOp, KernelInfo, Ops, AxisType
|
||||
from tinygrad.uop.ops import UOp, KernelInfo, Ops
|
||||
def _embedding_bwd(grad_emb:UOp, call:UOp) -> tuple:
|
||||
weight, idx = call.src[1:]
|
||||
# for multi-device: unshard inputs to one device
|
||||
@@ -326,29 +326,15 @@ def _embedding_bwd(grad_emb:UOp, call:UOp) -> tuple:
|
||||
# this is the real atomic kernel
|
||||
def _embedding_bwd_kernel(grad_weight:UOp, grad_emb:UOp, idx:UOp) -> UOp:
|
||||
idx_flat, grad_emb_flat = idx.flatten(), grad_emb.reshape((idx.size, grad_weight.shape[-1]))
|
||||
|
||||
i = UOp.range(grad_emb_flat.shape[0], 0) # batch_size * sequence_length
|
||||
j = UOp.range(grad_emb_flat.shape[1], 1) # embed_size
|
||||
|
||||
embed_size = grad_weight.shape[-1]
|
||||
BLOCK_J = min(256, embed_size)
|
||||
assert embed_size % BLOCK_J == 0, f"embed_size {embed_size} must be divisible by {BLOCK_J}"
|
||||
|
||||
n_j_blocks = embed_size // BLOCK_J
|
||||
i = UOp.range(grad_emb_flat.shape[0], 0) # batch_size * sequence_length -> GLOBAL
|
||||
j_inner = UOp.range(BLOCK_J, 2, AxisType.LOOP if device in ("CPU", "NULL") else AxisType.LOCAL) # BLOCK_J threads per workgroup
|
||||
j_outer = UOp.range(n_j_blocks, 1)
|
||||
j = j_outer * BLOCK_J + j_inner
|
||||
|
||||
token_id = idx_flat[i].clip(0, grad_weight.shape[0]-1).cast(dtypes.index)
|
||||
|
||||
# atomic scatter-add: grad_weight[token_id, j] += grad_emb_flat[i, j]
|
||||
if device in ("CPU", "NULL"): atomic_arg = "__atomic_fetch_add({0}, {1}, __ATOMIC_RELAXED);"
|
||||
elif device == "AMD": atomic_arg = "__hip_atomic_fetch_add({0}, {1}, __ATOMIC_RELAXED, __HIP_MEMORY_SCOPE_AGENT);"
|
||||
else: raise NotImplementedError(f"no atomics for device {device}")
|
||||
atomic = UOp(Ops.CUSTOM, dtypes.void, (grad_weight.index(token_id, j, ptr=True), grad_emb_flat[i, j].cast(dtypes.float)), arg = atomic_arg)
|
||||
return atomic.end(i, j_outer, j_inner).sink(arg=KernelInfo(name="embedding_bwd", opts_to_apply=()))
|
||||
|
||||
return atomic.end(i, j).sink(arg=KernelInfo(name="embedding_bwd", opts_to_apply=()))
|
||||
grad_weight_uop = grad_weight_uop.custom_kernel(grad_emb, idx, fxn=_embedding_bwd_kernel)[0]
|
||||
|
||||
return (grad_weight_uop.cast(weight.dtype), None)
|
||||
|
||||
@@ -126,9 +126,6 @@ class InstOpRDNA4(Enum):
|
||||
LDS_WR_3 = 0x2c
|
||||
LDS_WR_4 = 0x2d
|
||||
LDS_WR_5 = 0x2e
|
||||
OTHER_LDS_1 = 0x50
|
||||
OTHER_LDS_2 = 0x51
|
||||
BARRIER_SIGNAL = 0x7a
|
||||
WMMA_8 = 0x8c
|
||||
WMMA_16 = 0x8d
|
||||
VALU_DPFP = 0x92
|
||||
@@ -522,7 +519,7 @@ class CDNA_REG_CS_PRIV(PacketType):
|
||||
PACKET_TYPES_CDNA: dict[int, type[PacketType]] = {
|
||||
0: CDNA_MISC, 1: CDNA_TIMESTAMP, 2: CDNA_REG, 3: CDNA_WAVESTART, 4: CDNA_WAVEALLOC, 5: CDNA_REG_CS, 6: CDNA_WAVEEND,
|
||||
7: CDNA_EVENT, 8: CDNA_EVENT_CS, 9: CDNA_EVENT_GFX1, 10: CDNA_INST, 11: CDNA_INST_PC, 12: CDNA_USERDATA,
|
||||
13: CDNA_ISSUE, 14: CDNA_PERF, 15: CDNA_REG_CS_PRIV, 16: LAYOUT_HEADER,
|
||||
13: CDNA_ISSUE, 14: CDNA_PERF, 15: CDNA_REG_CS_PRIV,
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from __future__ import annotations
|
||||
from typing import cast
|
||||
from typing import cast, ClassVar
|
||||
import os, ctypes, struct, hashlib, functools, importlib, mmap, errno, array, contextlib, sys, weakref, itertools, collections, atexit
|
||||
assert sys.platform != 'win32'
|
||||
from dataclasses import dataclass
|
||||
@@ -11,7 +11,7 @@ from tinygrad.helpers import getenv, round_up, data64_le, DEBUG, PROFILE, Profil
|
||||
from tinygrad.helpers import VIZ, AMD_CC, AMD_LLVM, AMD_HIPCC, ceildiv, unwrap
|
||||
from tinygrad.renderer.cstyle import AMDHIPRenderer, AMDHIPCCRenderer
|
||||
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, pci, sqtt, amdgpu_kd, amdgpu_drm
|
||||
from tinygrad.runtime.autogen.am import am
|
||||
from tinygrad.runtime.support.elf import elf_loader
|
||||
from tinygrad.runtime.support.am.amdev import AMDev, AMMemoryManager
|
||||
@@ -824,16 +824,19 @@ class KFDIface:
|
||||
def is_wgp_active(self, xcc, se, sa, wgp) -> bool: return ((self.drm_dev_info.cu_bitmap[se % 4][sa + (se // 4) * 2] >> (2 * wgp)) & 0x3) == 0x3
|
||||
|
||||
class PCIIface(PCIIfaceBase):
|
||||
gpus:ClassVar[list[str]] = []
|
||||
|
||||
def __init__(self, dev, dev_id):
|
||||
super().__init__(dev, dev_id, vendor=0x1002, devices=[(0xffff, [0x74a1, 0x744c, 0x7480, 0x7550, 0x7590, 0x75a0])], bars=[0, 2, 5], vram_bar=0,
|
||||
va_start=AMMemoryManager.va_allocator.base, va_size=AMMemoryManager.va_allocator.size)
|
||||
self._setup_adev(self.pci_dev)
|
||||
self.pci_dev.write_config(pci.PCI_COMMAND, self.pci_dev.read_config(pci.PCI_COMMAND, 2) | pci.PCI_COMMAND_MASTER, 2)
|
||||
|
||||
def require_profile_mode(self): return True
|
||||
def is_wgp_active(self, xcc, se, sa, wgp) -> bool: return True # TODO: account for WGP disablement on some asics.
|
||||
|
||||
def _setup_adev(self, pci_dev:PCIDevice):
|
||||
self.dev_impl:AMDev = AMDev(pci_dev)
|
||||
def _setup_adev(self, pci_dev:PCIDevice, dma_regions:list[tuple[int, MMIOInterface]]|None=None):
|
||||
self.dev_impl:AMDev = AMDev(pci_dev, dma_regions)
|
||||
self.ip_versions = self.dev_impl.ip_ver
|
||||
|
||||
gfxver = int(f"{self.dev_impl.ip_ver[am.GC_HWIP][0]:02d}{self.dev_impl.ip_ver[am.GC_HWIP][1]:02d}{self.dev_impl.ip_ver[am.GC_HWIP][2]:02d}")
|
||||
@@ -891,7 +894,7 @@ class PCIIface(PCIIfaceBase):
|
||||
class USBIface(PCIIface):
|
||||
def __init__(self, dev, dev_id): # pylint: disable=super-init-not-called
|
||||
self.dev, self.pci_dev = dev, USBPCIDevice(dev.__class__.__name__[:2], f"usb:{dev_id}", bars=[0, 2, 5])
|
||||
self._setup_adev(self.pci_dev)
|
||||
self._setup_adev(self.pci_dev, dma_regions=[(0x200000, self.pci_dev.dma_view(0xf000, 0x80000))])
|
||||
self.pci_dev.usb._pci_cacheable += [(self.pci_dev.bar_info[2].addr, self.pci_dev.bar_info[2].size)] # doorbell region is cacheable
|
||||
|
||||
# special regions
|
||||
@@ -902,7 +905,7 @@ class USBIface(PCIIface):
|
||||
region = self.dev_impl.mm.map_range(vaddr:=self.dev_impl.mm.alloc_vaddr(size=size), size, [(sys_addr, size)], aspace=AddrSpace.SYS, uncached=True)
|
||||
return HCQBuffer(vaddr, size, meta=PCIAllocationMeta(region, has_cpu_mapping=False), view=self.pci_dev.dma_view(ctrl_addr, size), owner=self.dev)
|
||||
|
||||
def alloc(self, size:int, host=False, uncached=False, cpu_access=False, contiguous=False, force_devmem=False, **kwargs) -> HCQBuffer:
|
||||
def alloc(self, size:int, host=False, uncached=False, cpu_access=False, contiguous=False, **kwargs) -> HCQBuffer:
|
||||
if (host or (uncached and cpu_access)) and self.sys_next_off + size < self.sys_buf.size:
|
||||
self.sys_next_off += size
|
||||
return self.sys_buf.offset(self.sys_next_off - size, size)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
import os, ctypes, contextlib, re, functools, mmap, struct, array, sys, weakref
|
||||
assert sys.platform != 'win32'
|
||||
from typing import cast
|
||||
from typing import cast, ClassVar
|
||||
from dataclasses import dataclass
|
||||
from tinygrad.runtime.support.hcq import HCQCompiled, HCQAllocator, HCQBuffer, HWQueue, CLikeArgsState, HCQProgram, HCQSignal, BumpAllocator
|
||||
from tinygrad.runtime.support.hcq import MMIOInterface, FileIOInterface, MOCKGPU, hcq_filter_visible_devices, hcq_profile
|
||||
@@ -11,7 +11,7 @@ from tinygrad.helpers import getenv, mv_address, round_up, data64, data64_le, pr
|
||||
from tinygrad.helpers import ContextVar, VIZ, ProfileEvent
|
||||
from tinygrad.renderer.ptx import PTXRenderer
|
||||
from tinygrad.renderer.cstyle import CUDARenderer
|
||||
from tinygrad.runtime.autogen import nv_570, nv_580, mesa
|
||||
from tinygrad.runtime.autogen import nv_570, nv_580, pci, mesa
|
||||
from tinygrad.runtime.support.elf import elf_loader
|
||||
from tinygrad.runtime.support.nv.nvdev import NVDev, NVMemoryManager
|
||||
from tinygrad.runtime.support.system import System, PCIIfaceBase, MAP_FIXED
|
||||
@@ -535,6 +535,8 @@ class NVKIface:
|
||||
def sleep(self, tm:int): pass
|
||||
|
||||
class PCIIface(PCIIfaceBase):
|
||||
gpus:ClassVar[list[str]] = []
|
||||
|
||||
def __init__(self, dev, dev_id):
|
||||
# PCIIface's MAP_FIXED mmap will overwrite UVM allocations made by NVKIface, so don't try PCIIface if kernel driver was already used.
|
||||
if NVKIface.root is not None: raise RuntimeError("Cannot use PCIIface after NVKIface has been initialized (would corrupt UVM memory)")
|
||||
@@ -542,6 +544,7 @@ class PCIIface(PCIIfaceBase):
|
||||
base_class=0x03, bars=[0, 1, 3], vram_bar=1, va_start=NVMemoryManager.va_allocator.base, va_size=NVMemoryManager.va_allocator.size)
|
||||
if not OSX: System.reserve_hugepages(64)
|
||||
|
||||
self.pci_dev.write_config(pci.PCI_COMMAND, self.pci_dev.read_config(pci.PCI_COMMAND, 2) | pci.PCI_COMMAND_MASTER, 2)
|
||||
self.dev_impl:NVDev = NVDev(self.pci_dev)
|
||||
self.root, self.gpu_instance = 0xc1000000, 0
|
||||
self.rm_alloc(0, nv_gpu.NV01_ROOT, nv_gpu.NV0000_ALLOC_PARAMETERS())
|
||||
@@ -550,11 +553,11 @@ class PCIIface(PCIIfaceBase):
|
||||
self.gpfifo_class, self.compute_class, self.dma_class = (gsp:=self.dev_impl.gsp).gpfifo_class, gsp.compute_class, gsp.dma_class
|
||||
self.viddec_class = None
|
||||
|
||||
def alloc(self, size:int, host=False, uncached=False, cpu_access=False, contiguous=False, force_devmem=False, **kwargs) -> HCQBuffer:
|
||||
def alloc(self, size:int, host=False, uncached=False, cpu_access=False, contiguous=False, **kwargs) -> HCQBuffer:
|
||||
# Force use of huge pages for large allocations. NVDev will attempt to use huge pages in any case,
|
||||
# but if the size is not aligned, the tail will be allocated with 4KB pages, increasing TLB pressure.
|
||||
return super().alloc(round_up(size, mmap.PAGESIZE if uncached or host else ((2 << 20) if size >= (8 << 20) else (4 << 10))),
|
||||
host=host, uncached=uncached, cpu_access=cpu_access, contiguous=contiguous, force_devmem=force_devmem, **kwargs)
|
||||
page_size = mmap.PAGESIZE if uncached or host else ((2 << 20) if size >= (8 << 20) else (4 << 10))
|
||||
return super().alloc(round_up(size, page_size), host=host, uncached=uncached, cpu_access=cpu_access, contiguous=contiguous, **kwargs)
|
||||
|
||||
def setup_usermode(self): return 0xce000000, self.pci_dev.map_bar(bar=0, fmt='I', off=0xbb0000, size=0x10000)
|
||||
def setup_vm(self, vaspace): pass
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from __future__ import annotations
|
||||
import ctypes, collections, dataclasses, functools, hashlib, array
|
||||
from tinygrad.helpers import mv_address, getenv, DEBUG, fetch, lo32, hi32
|
||||
from tinygrad.runtime.autogen import pci
|
||||
from tinygrad.runtime.autogen.am import am
|
||||
from tinygrad.runtime.support.hcq import MMIOInterface
|
||||
from tinygrad.runtime.support.amd import AMDReg, import_module, import_asic_regs
|
||||
from tinygrad.runtime.support.memory import TLSFAllocator, MemoryManager, AddrSpace
|
||||
from tinygrad.runtime.support.system import PCIDevice, PCIDevImplBase
|
||||
@@ -146,8 +146,8 @@ class AMMemoryManager(MemoryManager):
|
||||
class AMDev(PCIDevImplBase):
|
||||
Version = 0xA0000008
|
||||
|
||||
def __init__(self, pci_dev:PCIDevice, reset_mode=False):
|
||||
self.pci_dev, self.devfmt = pci_dev, pci_dev.pcibus
|
||||
def __init__(self, pci_dev:PCIDevice, dma_regions:list[tuple[int, MMIOInterface]]|None=None, reset_mode=False):
|
||||
self.pci_dev, self.devfmt, self.dma_regions = pci_dev, pci_dev.pcibus, dma_regions
|
||||
self.vram, self.doorbell64, self.mmio = self.pci_dev.map_bar(0), self.pci_dev.map_bar(2, fmt='Q'), self.pci_dev.map_bar(5, fmt='I')
|
||||
|
||||
self._run_discovery()
|
||||
@@ -185,7 +185,6 @@ class AMDev(PCIDevImplBase):
|
||||
|
||||
# Re-initialize main blocks
|
||||
self.init_hw(self.gfx, self.sdma)
|
||||
self.pci_dev.write_config(pci.PCI_COMMAND, self.pci_dev.read_config(pci.PCI_COMMAND, 2) | pci.PCI_COMMAND_MASTER, 2)
|
||||
|
||||
self.smu.set_clocks(level=-1) # last level, max perf.
|
||||
for ip in [self.soc, self.gfx]: ip.set_clockgating_state()
|
||||
|
||||
@@ -547,10 +547,10 @@ class AM_PSP(AM_IP):
|
||||
def init_sw(self):
|
||||
self.reg_pref = "regMP0_SMN_C2PMSG" if self.adev.ip_ver[am.MP0_HWIP] < (14,0,0) else "regMPASP_SMN_C2PMSG"
|
||||
|
||||
if self.adev.devfmt.startswith("usb:"):
|
||||
self.msg1_view, paddrs = self.adev.pci_dev.alloc_sysmem(512 << 10)
|
||||
self.msg1_addr = self.adev.mm.alloc_vaddr(size=self.msg1_view.nbytes, align=am.PSP_1_MEG)
|
||||
self.adev.mm.map_range(self.msg1_addr, self.msg1_view.nbytes, [(paddrs[0], self.msg1_view.nbytes)], AddrSpace.SYS, uncached=True, boot=True)
|
||||
msg1_region = next((reg for reg in self.adev.dma_regions or [] if reg[1].nbytes >= (512 << 10)), None)
|
||||
if msg1_region is not None:
|
||||
self.msg1_addr, self.msg1_view = self.adev.mm.alloc_vaddr(size=msg1_region[1].nbytes, align=am.PSP_1_MEG), msg1_region[1]
|
||||
self.adev.mm.map_range(self.msg1_addr, msg1_region[1].nbytes, [(msg1_region[0],msg1_region[1].nbytes)], AddrSpace.SYS, uncached=True, boot=True)
|
||||
else:
|
||||
self.msg1_paddr = self.adev.mm.palloc(am.PSP_1_MEG, align=am.PSP_1_MEG, zero=False, boot=True)
|
||||
self.msg1_addr, self.msg1_view = self.adev.paddr2mc(self.msg1_paddr), self.adev.vram.view(self.msg1_paddr, am.PSP_1_MEG, 'B')
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
import ctypes, time, functools, re, gzip, struct
|
||||
from tinygrad.helpers import getenv, DEBUG, fetch, getbits
|
||||
from tinygrad.runtime.autogen import pci
|
||||
from tinygrad.runtime.support.memory import TLSFAllocator, MemoryManager, AddrSpace
|
||||
from tinygrad.runtime.support.nv.ip import NV_FLCN, NV_FLCN_COT, NV_GSP
|
||||
from tinygrad.runtime.support.system import PCIDevice, PCIDevImplBase, MMIOInterface
|
||||
@@ -73,7 +72,6 @@ class NVMemoryManager(MemoryManager):
|
||||
class NVDev(PCIDevImplBase):
|
||||
def __init__(self, pci_dev:PCIDevice):
|
||||
self.pci_dev, self.devfmt, self.mmio = pci_dev, pci_dev.pcibus, pci_dev.map_bar(0, fmt='I')
|
||||
self.pci_dev.write_config(pci.PCI_COMMAND, self.pci_dev.read_config(pci.PCI_COMMAND, 2) | pci.PCI_COMMAND_MASTER, 2)
|
||||
|
||||
self.smi_dev, self.is_booting, self.is_err_state = False, True, False
|
||||
self._early_ip_init()
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
import os, mmap, array, functools, ctypes, select, contextlib, dataclasses, sys, itertools, struct, socket, subprocess, time, enum
|
||||
from tinygrad.helpers import round_up, getenv, OSX, temp, ceildiv, unwrap, fetch, system, _ensure_downloads_dir
|
||||
from typing import ClassVar
|
||||
from tinygrad.helpers import round_up, getenv, OSX, temp, ceildiv, unwrap, fetch, system
|
||||
from tinygrad.runtime.autogen import libc, pci, vfio, iokit, corefoundation
|
||||
from tinygrad.runtime.support.hcq import FileIOInterface, MMIOInterface, HCQBuffer, hcq_filter_visible_devices
|
||||
from tinygrad.runtime.support.memory import MemoryManager, VirtMapping, AddrSpace, BumpAllocator
|
||||
from tinygrad.runtime.support.memory import MemoryManager, VirtMapping, AddrSpace
|
||||
from tinygrad.runtime.support.usb import ASM24Controller, USBMMIOInterface
|
||||
|
||||
MAP_FIXED, MAP_FIXED_NOREPLACE = 0x10, 0x100000
|
||||
@@ -43,11 +44,6 @@ class _System:
|
||||
|
||||
def reserve_hugepages(self, cnt): os.system(f"sudo sh -c 'echo {cnt} > /proc/sys/vm/nr_hugepages'")
|
||||
|
||||
@functools.cache
|
||||
def reserve_va(self, va_start, va_size):
|
||||
# cached, runs only once per range. used to not collide with other mappings.
|
||||
FileIOInterface.anon_mmap(va_start, va_size, 0, mmap.MAP_PRIVATE | mmap.MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED_NOREPLACE, 0)
|
||||
|
||||
def memory_barrier(self): lib.atomic_thread_fence(__ATOMIC_SEQ_CST:=5) if (lib:=self.libsys if OSX else self.atomic_lib) is not None else None
|
||||
|
||||
def lock_memory(self, addr:int, size:int):
|
||||
@@ -76,13 +72,6 @@ class _System:
|
||||
|
||||
return sorted([val for vndr, device, val in all_devs if vndr == vendor and any((device & mask) in devlist for mask, devlist in devices)])
|
||||
|
||||
def pci_probe_device(self, devpref:str, dev_id:int, vendor:int, devices:list[tuple[int, list[int]]], bars:list[int],
|
||||
resize_bars:list[int]|None=None, base_class:int|None=None):
|
||||
gpus = hcq_filter_visible_devices(System.pci_scan_bus(vendor, devices, base_class))
|
||||
if not gpus: raise RuntimeError("No supported GPUs found")
|
||||
if OSX: return APLRemotePCIDevice(devpref, f'usb4:{dev_id}', bars)
|
||||
return PCIDevice(devpref, gpus[dev_id], bars=bars, resize_bars=resize_bars)
|
||||
|
||||
def pci_setup_usb_bars(self, usb:ASM24Controller, gpu_bus:int, mem_base:int, pref_mem_base:int) -> dict[int, PCIBarInfo]:
|
||||
for bus in range(gpu_bus):
|
||||
# All 3 values must be written at the same time.
|
||||
@@ -212,14 +201,9 @@ class USBPCIDevice(PCIDevice):
|
||||
self.lock_fd = System.flock_acquire(f"{devpref.lower()}_{pcibus.lower()}.lock")
|
||||
self.usb = ASM24Controller()
|
||||
self.pcibus, self.bar_info = pcibus, System.pci_setup_usb_bars(self.usb, gpu_bus=4, mem_base=0x10000000, pref_mem_base=(32 << 30))
|
||||
self.sram = BumpAllocator(size=0x80000, wrap=False) # asm24 controller sram
|
||||
def read_config(self, offset:int, size:int): return self.usb.pcie_cfg_req(offset, bus=4, dev=0, fn=0, size=size)
|
||||
def write_config(self, offset:int, value:int, size:int): self.usb.pcie_cfg_req(offset, bus=4, dev=0, fn=0, value=value, size=size)
|
||||
def map_bar(self, bar, off=0, addr=0, size=None, fmt='B'):
|
||||
return USBMMIOInterface(self.usb, self.bar_info[bar].addr + off, size or self.bar_info[bar].size, fmt)
|
||||
def dma_view(self, ctrl_addr, size): return USBMMIOInterface(self.usb, ctrl_addr, size, fmt='B', pcimem=False)
|
||||
def alloc_sysmem(self, size:int, vaddr:int=0, contiguous:bool=False) -> tuple[MMIOInterface, list[int]]:
|
||||
return self.dma_view(0xf000 + (off:=self.sram.alloc(size)), size), [0x200000 + off]
|
||||
|
||||
class PCIDevImplBase:
|
||||
mm: MemoryManager
|
||||
@@ -227,20 +211,22 @@ class PCIDevImplBase:
|
||||
@dataclasses.dataclass
|
||||
class PCIAllocationMeta: mapping:VirtMapping; has_cpu_mapping:bool; hMemory:int=0 # noqa: E702
|
||||
|
||||
class PCIIfaceBase:
|
||||
class LNXPCIIfaceBase:
|
||||
dev_impl:PCIDevImplBase
|
||||
|
||||
def is_local(self) -> bool: return not isinstance(self.pci_dev, RemotePCIDevice)
|
||||
def is_bar_small(self) -> bool: return self.pci_dev.bar_info[self.vram_bar].size == (256 << 20)
|
||||
gpus:ClassVar[list[str]] = []
|
||||
|
||||
def __init__(self, dev, dev_id, vendor, devices:list[tuple[int, list[int]]], bars, vram_bar, va_start, va_size, base_class:int|None=None):
|
||||
self.pci_dev = System.pci_probe_device(dev.__class__.__name__[:2], dev_id, vendor, devices, bars, resize_bars=[vram_bar], base_class=base_class)
|
||||
if self.is_local(): System.reserve_va(va_start, va_size)
|
||||
self.dev, self.vram_bar = dev, vram_bar
|
||||
if len((cls:=type(self)).gpus) == 0:
|
||||
cls.gpus = hcq_filter_visible_devices(System.pci_scan_bus(vendor, devices, base_class))
|
||||
|
||||
# Acquire va range to avoid collisions.
|
||||
FileIOInterface.anon_mmap(va_start, va_size, 0, mmap.MAP_PRIVATE | mmap.MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED_NOREPLACE, 0)
|
||||
self.pci_dev, self.dev, self.vram_bar = PCIDevice(dev.__class__.__name__[:2], cls.gpus[dev_id], bars=bars, resize_bars=[vram_bar]), dev, vram_bar
|
||||
self.p2p_base_addr = self.pci_dev.bar_info[vram_bar].addr
|
||||
|
||||
def alloc(self, size:int, host=False, uncached=False, cpu_access=False, contiguous=False, force_devmem=False, **kwargs) -> HCQBuffer:
|
||||
should_use_sysmem = host or ((cpu_access if self.is_bar_small() else (uncached and cpu_access)) and not force_devmem)
|
||||
# NOTE: logic on macos is different, since bar is small
|
||||
should_use_sysmem = host or ((cpu_access if OSX else (uncached and cpu_access)) and not force_devmem)
|
||||
if should_use_sysmem:
|
||||
vaddr = self.dev_impl.mm.alloc_vaddr(size:=round_up(size, mmap.PAGESIZE), align=mmap.PAGESIZE)
|
||||
memview, paddrs = self.pci_dev.alloc_sysmem(size, vaddr=vaddr, contiguous=contiguous)
|
||||
@@ -254,16 +240,14 @@ class PCIIfaceBase:
|
||||
def free(self, b:HCQBuffer):
|
||||
for dev in b.mapped_devs[1:]: dev.iface.dev_impl.mm.unmap_range(b.va_addr, b.size)
|
||||
if b.meta.mapping.aspace is AddrSpace.PHYS: self.dev_impl.mm.vfree(b.meta.mapping)
|
||||
if self.is_local() and b.owner == self.dev and b.meta.has_cpu_mapping: FileIOInterface.munmap(b.va_addr, b.size)
|
||||
if b.owner == self.dev and b.meta.has_cpu_mapping and not OSX: FileIOInterface.munmap(b.va_addr, b.size)
|
||||
|
||||
def map(self, b:HCQBuffer):
|
||||
if not self.is_local(): raise RuntimeError(f"P2P mapping not supported for remote devices: {b.owner} -> {self.dev}")
|
||||
|
||||
if b.owner is not None and b.owner._is_cpu():
|
||||
System.lock_memory(int(b.va_addr), b.size)
|
||||
paddrs, aspace = [(x, 0x1000) for x in System.system_paddrs(int(b.va_addr), round_up(b.size, 0x1000))], AddrSpace.SYS
|
||||
snooped, uncached = True, True
|
||||
elif (ifa:=getattr(b.owner, "iface", None)) is not None and isinstance(ifa, PCIIfaceBase):
|
||||
elif (ifa:=getattr(b.owner, "iface", None)) is not None and isinstance(ifa, LNXPCIIfaceBase):
|
||||
snooped, uncached = True, b.meta.mapping.uncached
|
||||
if b.meta.mapping.aspace is AddrSpace.SYS: paddrs, aspace = b.meta.mapping.paddrs, AddrSpace.SYS
|
||||
elif hasattr(ifa.dev_impl, 'paddr2xgmi') and ifa.dev_impl.gmc.xgmi_seg_sz > 0:
|
||||
@@ -339,16 +323,13 @@ class RemotePCIDevice(PCIDevice):
|
||||
class APLRemotePCIDevice(RemotePCIDevice):
|
||||
APP_PATH = "/Applications/TinyGPU.app/Contents/MacOS/TinyGPU"
|
||||
|
||||
@classmethod
|
||||
def ensure_app(cls):
|
||||
commit = "8120b5508b43149d27bf22f9a4e6d7c5a4b401e9"
|
||||
if os.path.exists(cls.APP_PATH) and (_ensure_downloads_dir() / (app_name:=f"TinyGPU_{commit}.zip")).is_file(): return
|
||||
@staticmethod
|
||||
def install_tinygpu():
|
||||
print("Downloading TinyGPU.app...")
|
||||
system(f"ditto -xk {fetch(f'https://github.com/nimlgen/tinygpu_releases/raw/{commit}/TinyGPU.zip', name=app_name)} /Applications")
|
||||
print(system(f"{cls.APP_PATH} install"))
|
||||
system(f"ditto -xk {fetch('https://github.com/nimlgen/tinygpu_releases/raw/8120b5508b43149d27bf22f9a4e6d7c5a4b401e9/TinyGPU.zip')} /Applications")
|
||||
print(system(f"{APLRemotePCIDevice.APP_PATH} install"))
|
||||
|
||||
def __init__(self, devpref:str, pcibus:str, bars:list[int], resize_bars:list[int]|None=None):
|
||||
self.ensure_app()
|
||||
sock_path, sock = getenv("APL_REMOTE_SOCK", temp("tinygpu.sock")), socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
for i in range(100):
|
||||
with contextlib.suppress(ConnectionRefusedError, FileNotFoundError):
|
||||
@@ -358,3 +339,20 @@ class APLRemotePCIDevice(RemotePCIDevice):
|
||||
time.sleep(0.05)
|
||||
else: raise RuntimeError(f"Failed to connect to TinyGPU server at {sock_path}.")
|
||||
super().__init__(devpref, pcibus, bars, sock)
|
||||
|
||||
class APLRemoteIfaceBase(LNXPCIIfaceBase):
|
||||
def __init__(self, dev, dev_id, vendor, devices:list[tuple[int, list[int]]], bars, vram_bar, va_start, va_size, base_class:int|None=None):
|
||||
if not (cls:=type(self)).gpus:
|
||||
cls.gpus = System.pci_scan_bus(vendor, devices, base_class)
|
||||
if not cls.gpus: raise RuntimeError("No supported GPUs found")
|
||||
if not os.path.exists(APLRemotePCIDevice.APP_PATH): APLRemotePCIDevice.install_tinygpu()
|
||||
if dev_id >= len(cls.gpus): raise RuntimeError(f"No device found for {dev_id}. Requesting more devices than the system has ({cls.gpus})?")
|
||||
self.pci_dev = APLRemotePCIDevice(dev.__class__.__name__[:2], f'remote:{dev_id}', bars)
|
||||
self.dev, self.vram_bar = dev, vram_bar
|
||||
|
||||
def free(self, b:HCQBuffer):
|
||||
for dev in b.mapped_devs[1:]: dev.iface.dev_impl.mm.unmap_range(b.va_addr, b.size)
|
||||
|
||||
def map(self, b:HCQBuffer): raise RuntimeError(f"P2P mapping not supported for remote devices: {b.owner} -> {self.dev}")
|
||||
|
||||
PCIIfaceBase:type = APLRemoteIfaceBase if OSX else LNXPCIIfaceBase
|
||||
|
||||
@@ -107,7 +107,7 @@ class USB3:
|
||||
if self.use_bot:
|
||||
dir_in = rlen > 0
|
||||
data_len = rlen if dir_in else (len(send_data) if send_data is not None else 0)
|
||||
assert not (rlen > 0 and send_data is not None), "BOT mode only supports either read or write per command"
|
||||
assert (data_len == 0) if dir_in else (rlen == 0), "BOT mode only supports either read or write per command"
|
||||
|
||||
# CBW
|
||||
self._tag += 1
|
||||
|
||||
@@ -7,7 +7,7 @@ from tinygrad.uop.ops import consumer_map_from_toposort, gate_kernel_sink
|
||||
from tinygrad.uop.symbolic import symbolic, pm_simplify_valid, pm_drop_and_clauses
|
||||
from tinygrad.helpers import argsort, all_same, cpu_profile, PCONTIG, colored
|
||||
|
||||
ALWAYS_CONTIGUOUS: set[Ops] = {Ops.CONTIGUOUS, Ops.ASSIGN, Ops.AFTER, Ops.COPY, Ops.BUFFER, Ops.BUFFER_VIEW,
|
||||
ALWAYS_CONTIGUOUS: set[Ops] = {Ops.CONTIGUOUS, Ops.ASSIGN, Ops.COPY, Ops.BUFFER, Ops.BUFFER_VIEW,
|
||||
Ops.CONST, Ops.BIND, Ops.DEVICE, Ops.MSELECT, Ops.MSTACK, Ops.PARAM,
|
||||
Ops.DEFINE_LOCAL, Ops.DEFINE_REG, Ops.LOAD, Ops.CALL}
|
||||
|
||||
@@ -25,21 +25,13 @@ def realize_assign_src(ctx:dict[UOp, None], buf:UOp, x:UOp):
|
||||
# you don't usually have to do this for assign unless there's a WAR hazard like TestAssign.test_assign_double_diamond_reduce
|
||||
if buf.base in x.backward_slice_with_self: ctx[x] = None
|
||||
|
||||
def unrealize_store_src(ctx:dict[UOp, None], x:UOp):
|
||||
"""Don't realize COPY/BUFFER_VIEW consumed by STORE inside AFTER — bufferize_to_store handles them."""
|
||||
if x in ctx: del ctx[x]
|
||||
|
||||
pm_generate_realize_map = PatternMatcher([
|
||||
# always realize
|
||||
(UPat({Ops.COPY, Ops.CONTIGUOUS, Ops.ASSIGN}, name="tr"), realize),
|
||||
# realize AFTER of STORE+AFTER
|
||||
(UPat(Ops.AFTER, src=(UPat(), UPat(Ops.STORE)), allow_any_len=True, name="tr"), realize),
|
||||
(UPat({Ops.COPY, Ops.CONTIGUOUS, Ops.STORE, Ops.ASSIGN}, name="tr"), realize),
|
||||
# realize srcs of these
|
||||
(UPat((Ops.COPY, Ops.MSELECT, Ops.MSTACK), name="rb"), realize_srcs),
|
||||
# sometimes realize src of assign
|
||||
(UPat(Ops.ASSIGN, src=(UPat.var("buf"), UPat.var("x"))), realize_assign_src),
|
||||
# don't realize COPY/BUFFER_VIEW consumed by STORE inside AFTER (like realize_assign_src for ASSIGN)
|
||||
(UPat(Ops.AFTER, src=(UPat(), UPat(Ops.STORE, src=(UPat(), UPat({Ops.COPY, Ops.BUFFER_VIEW}, name="x"))))), unrealize_store_src),
|
||||
])
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -66,8 +58,7 @@ def create_bufferize_and_index_based_on_ranges(ctx:IndexingContext, x:UOp):
|
||||
new_srcs = []
|
||||
for s in x.src:
|
||||
new_src = s
|
||||
if s.op in {Ops.PARAM, Ops.BUFFER_VIEW, Ops.MSTACK, Ops.MSELECT} or \
|
||||
(s.op is Ops.AFTER and not any(c.op in {Ops.STORE, Ops.END} for c in s.src[1:])):
|
||||
if s.op in {Ops.PARAM, Ops.BUFFER_VIEW, Ops.MSTACK, Ops.MSELECT, Ops.AFTER}:
|
||||
if x in ctx.range_map: new_src = new_src.index(*ctx.range_map[x][0])
|
||||
elif s in ctx.realize_map:
|
||||
realized_ranges = ctx.realize_map[s]
|
||||
@@ -106,11 +97,25 @@ def convert_reduce_axis_to_reduce_with_ranges(ctx:IndexingContext, x:UOp):
|
||||
def remove_movement_op_after_rangeify(ctx:IndexingContext, x:UOp):
|
||||
if x in ctx.range_map or x.src[0].op is Ops.INDEX: return x.src[0]
|
||||
|
||||
def handle_assign_mops(ctx:IndexingContext, assign:UOp, target:UOp, src:UOp):
|
||||
if target.op in GroupOp.Movement and src.op is not Ops.CALL:
|
||||
mops = []
|
||||
while target.op in GroupOp.Movement:
|
||||
mops.append((target.op, target.marg))
|
||||
target = target.src[0]
|
||||
if mops and assign in ctx.range_map:
|
||||
ret = assign.replace(arg=tuple(mops))
|
||||
ctx.range_map[ret] = ctx.range_map[assign]
|
||||
return ret
|
||||
return None
|
||||
|
||||
pm_apply_rangeify = PatternMatcher([
|
||||
# REDUCE_AXIS -> REDUCE
|
||||
(UPat(Ops.REDUCE_AXIS, name="x"), convert_reduce_axis_to_reduce_with_ranges),
|
||||
# PAD -> WHERE
|
||||
(UPat(Ops.PAD, name="x"), convert_pad_to_where_to_keep_behavior_local),
|
||||
# store movement ops in ASSIGN arg
|
||||
(UPat(Ops.ASSIGN, src=(UPat(name="target"), UPat(name="src")), name="assign"), handle_assign_mops),
|
||||
# finally, apply_rangeify
|
||||
(UPat(GroupOp.All, name="x"), create_bufferize_and_index_based_on_ranges),
|
||||
# remove movement op
|
||||
@@ -172,8 +177,8 @@ def run_rangeify(tsink:UOp, debug:bool=False) -> tuple[UOp, IndexingContext]:
|
||||
# no ranges on kernels, they are internal
|
||||
if x.op in {Ops.CALL, Ops.LINEAR}: continue
|
||||
|
||||
# only STORE+AFTER has range
|
||||
if x.op is Ops.AFTER and all(s.op is not Ops.STORE for s in x.src[1:]): continue
|
||||
# no range on after
|
||||
if x.op is Ops.AFTER: continue
|
||||
|
||||
# treat MSTACK/MSELECT like SINK
|
||||
if x.op in {Ops.MSTACK, Ops.MSELECT}: continue
|
||||
|
||||
@@ -141,10 +141,13 @@ multi_pm = PatternMatcher([
|
||||
lambda multi,device,red: multi.src[0].allreduce(red.arg, device).multi(axis=multi.axis)),
|
||||
# rewrite into calls explicitly for MULTI
|
||||
(UPat(Ops.CALL, name="call"), rewrite_into_call),
|
||||
(UPat((Ops.CALL, Ops.AFTER, Ops.STORE), src=(UPat(Ops.MULTI, name="multi"), ), name="root", allow_any_len=True), passthrough_multi),
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.MULTI, name="multi"), ), name="root", allow_any_len=True), passthrough_multi),
|
||||
# we just remove the MULTI from CALLs with dtypes.void and assume they are handled by the user for custom kernels
|
||||
(UPat(Ops.CALL, dtype=dtypes.void, name="root", custom_early_reject=set([Ops.MULTI])), lambda root:
|
||||
UOp(root.op, root.dtype, tuple(x.src[0] if x.op is Ops.MULTI else x for x in root.src), root.arg)),
|
||||
(UPat((Ops.CAST, Ops.BITCAST, Ops.CONTIGUOUS, Ops.DETACH, Ops.CONTIGUOUS_BACKWARD),
|
||||
src=(UPat(Ops.MULTI, name="multi"), ), name="root"), passthrough_multi),
|
||||
# after CALL
|
||||
(UPat(Ops.AFTER, src=(UPat(Ops.MULTI, name="multi"), UPat(Ops.CALL)), name="a"),
|
||||
lambda multi,a: a.replace(src=(multi.src[0],)+a.src[1:]).multi(multi.axis)),
|
||||
])+replace_allreduce
|
||||
|
||||
@@ -16,39 +16,24 @@ from tinygrad.schedule.allreduce import create_allreduce_function
|
||||
import sys
|
||||
sys.setrecursionlimit(10000)
|
||||
|
||||
def add_ranges_to_store(ctx, x):
|
||||
if x.src[0]._shape is None or x.src[1]._shape is None or x.src[0].shape == (): return None
|
||||
assert x.src[0].shape == x.src[1].shape, "bad store shape"
|
||||
idxs = [UOp.range(r, next(ctx), AxisType.LOOP) for r in x.src[0].shape]
|
||||
return UOp.store(x.src[0].index(*idxs), x.src[1].index(*idxs)).end(*idxs)
|
||||
|
||||
pm_store_ranges = PatternMatcher([
|
||||
(UPat(Ops.STORE, name="x"), add_ranges_to_store),
|
||||
])
|
||||
|
||||
pm_syntactic_sugar = PatternMatcher([
|
||||
# INDEX on ptr INDEX concats them
|
||||
(UPat(Ops.INDEX, name="i1").f(Ops.INDEX, name="i2", allow_any_len=True),
|
||||
lambda i1,i2: i2.replace(src=i1.src+i2.src[1:]) if isinstance(i1.dtype, PtrDType) and not isinstance(i2.dtype, PtrDType) else None),
|
||||
# early rangeify
|
||||
(UPat(Ops.INDEX, src=(UPat(GroupOp.Elementwise | {Ops.CONST}, name="x"),), allow_any_len=True, name="idx"),
|
||||
lambda idx,x: x.replace(src=tuple([s.index(*idx.src[1:]) for s in x.src]))),
|
||||
])
|
||||
|
||||
def found_assign(ctx:dict[UOp, UOp], assign:UOp, src:UOp):
|
||||
if (x:=src).op is Ops.CAST and x.dtype == dtypes.half and FLOAT16: x, assign = x.src[0], assign.cast(dtypes.float)
|
||||
while True:
|
||||
if x.op is Ops.PERMUTE: x, assign = x.src[0], assign.permute(argsort(x.marg))
|
||||
elif x.op is Ops.RESHAPE: x, assign = x.src[0], assign.reshape(x.src[0].shape)
|
||||
elif x.op is Ops.WHERE and x.src[2].base.arg == Invalid and x.src[1].op is Ops.PAD:
|
||||
x, assign = x.src[1].src[0], assign.shrink(tuple((l, s-r) for (l,r),s in zip(x.src[1].marg, x.shape)))
|
||||
else: break
|
||||
while x is not x.base:
|
||||
if x.op is Ops.PERMUTE: assign = assign.permute(argsort(x.marg))
|
||||
elif x.op is Ops.RESHAPE: assign = assign.reshape(x.src[0].shape)
|
||||
else: return None
|
||||
x = x.src[0]
|
||||
ctx[x] = assign
|
||||
|
||||
# *** fold moved ASSIGNs/AFTERs (hack for openpilot) ***
|
||||
# *** fold moved ASSIGNs (hack for openpilot) ***
|
||||
pm_fold_moved_assign = PatternMatcher([
|
||||
(UPat(Ops.ASSIGN, src=(UPat(), UPat((*GroupOp.Movement, Ops.CAST), name="src")), name="assign"), found_assign),
|
||||
(UPat(Ops.AFTER, src=(UPat(), UPat(Ops.STORE, src=(UPat(), UPat((*GroupOp.Movement, Ops.CAST), name="src")))), name="assign"), found_assign),
|
||||
# replace ALU sources with assign versions found above
|
||||
(UPat(GroupOp.ALU, name="alu"), lambda ctx,alu: alu.replace(src=new_src) if (new_src:=tuple(ctx.get(s, s) for s in alu.src)) != alu.src else None),
|
||||
])
|
||||
@@ -56,12 +41,10 @@ pm_fold_moved_assign = PatternMatcher([
|
||||
# movement op on INDEX as a PatternMatcher
|
||||
pm_mops = PatternMatcher([
|
||||
(UPat(GroupOp.Movement, name="r").f(Ops.INDEX, allow_any_len=True, name="idx"),
|
||||
lambda r,idx: r.src[0].index(*apply_movement_op(r.op, r.src[0].shape, r.marg, idx.src[1:]), dtype=idx.dtype, arg=idx.arg)
|
||||
if len(idx.src[1:]) == len(r.shape) else None),
|
||||
# move movement ops after AFTER (but not when AFTER has a raw STORE with shaped children — from replace_contig_with_store_after)
|
||||
lambda r,idx: r.src[0].index(*apply_movement_op(r.op, r.src[0].shape, r.marg, idx.src[1:]), dtype=idx.dtype, arg=idx.arg)),
|
||||
# move movement ops after AFTER
|
||||
(UPat(GroupOp.Movement, name="r").after(name="a", allow_any_len=True),
|
||||
lambda r,a: UOp(r.op, r.dtype, (a.replace(src=(r.src[0],)+a.src[1:]),)+r.src[1:], r.arg)
|
||||
if not any(s.op is Ops.STORE and s.src[0]._shape is not None for s in a.src[1:]) else None),
|
||||
lambda r,a: UOp(r.op, r.dtype, (a.replace(src=(r.src[0],)+a.src[1:]),)+r.src[1:], r.arg)),
|
||||
(UPat(GroupOp.Movement, name="r").end(name="a", allow_any_len=True), lambda r,a: a.replace(src=(r.src[0],)+a.src[1:])),
|
||||
])
|
||||
|
||||
@@ -71,12 +54,12 @@ pm_mops = PatternMatcher([
|
||||
def fix_assign_hazard(assign:UOp, target:UOp, src:UOp):
|
||||
# PERMUTE and FLIP reorder indices, SHRINK can have overlapping regions when dest is also shrunk
|
||||
unsafe = {Ops.PERMUTE, Ops.FLIP} | ({Ops.SHRINK} if target.op_in_backward_slice_with_self(Ops.SHRINK) else set())
|
||||
if any(s.op in unsafe and target.base in s.backward_slice for s in src.toposort(gate=lambda s:s.op not in ALWAYS_CONTIGUOUS or s.op is Ops.AFTER)):
|
||||
if any(s.op in unsafe and target.base in s.backward_slice for s in src.toposort(gate=lambda s:s.op not in ALWAYS_CONTIGUOUS)):
|
||||
return assign.replace(src=(target, src.contiguous()))
|
||||
|
||||
def normalize_assign_target_chain(assign:UOp, target:UOp, src:UOp):
|
||||
root_target = target
|
||||
while root_target.op in {Ops.ASSIGN, Ops.AFTER}: root_target = root_target.src[0]
|
||||
while root_target.op is Ops.ASSIGN: root_target = root_target.src[0]
|
||||
# when RHS depends on the previous assign result, break with contiguous
|
||||
if target in src.toposort(): src = src.contiguous()
|
||||
return assign.replace(src=(root_target, src))
|
||||
@@ -137,10 +120,8 @@ earliest_rewrites = mop_cleanup+PatternMatcher([
|
||||
# resolve calls
|
||||
(UPat(Ops.CALL, name="c"), resolve_call),
|
||||
|
||||
# resolve TUPLE+GETTUPLE
|
||||
(UPat(Ops.GETTUPLE, src=(UPat(Ops.TUPLE, name="t"),), name="g"), lambda g,t: t.src[g.arg]),
|
||||
|
||||
# resolve allreduce (must be bottom up)
|
||||
(UPat(Ops.ASSIGN, src=(UPat.var("output"), UPat(Ops.ALLREDUCE, src=(UPat.var("buf"), UPat()), name="red"))), create_allreduce_function),
|
||||
(UPat(Ops.ALLREDUCE, src=(UPat.var("buf"), UPat()), name="red"), create_allreduce_function),
|
||||
|
||||
# split_reduceop
|
||||
@@ -175,9 +156,8 @@ earliest_rewrites = mop_cleanup+PatternMatcher([
|
||||
(UPat(Ops.ASSIGN, src=(UPat(Ops.BITCAST, src=(UPat(name="target"),)), UPat(name="src"))),
|
||||
lambda target, src: target.assign(src.bitcast(target.dtype))),
|
||||
|
||||
# if assign target is itself an ASSIGN/AFTER chain, canonicalize to the original buffer target
|
||||
(UPat(Ops.ASSIGN, src=(UPat({Ops.ASSIGN, Ops.AFTER}, name="target"), UPat(name="src")), allow_any_len=True, name="assign"),
|
||||
normalize_assign_target_chain),
|
||||
# if assign target is itself an ASSIGN chain, canonicalize to the original buffer target
|
||||
(UPat(Ops.ASSIGN, src=(UPat(Ops.ASSIGN, name="target"), UPat(name="src")), allow_any_len=True, name="assign"), normalize_assign_target_chain),
|
||||
|
||||
# make source contiguous if it has hazardous movement ops on the dest buffer
|
||||
(UPat(Ops.ASSIGN, src=(UPat.var("target"), UPat.var("src")), name="assign"), fix_assign_hazard),
|
||||
@@ -198,8 +178,8 @@ ALWAYS_RUN_OPS = {Ops.CONTIGUOUS, Ops.COPY, Ops.ASSIGN, Ops.NOOP}
|
||||
|
||||
# you don't know in the first pass if axes are going to die, this happens if there's an EXPAND to the left
|
||||
def cleanup_dead_axes(b:UOp):
|
||||
# don't optimize ALWAYS_RUN_OPS or AFTER (AFTER is a buffer identity — ranges define consumer access, not computation)
|
||||
if b.src[0].op in ALWAYS_RUN_OPS or b.src[0].op is Ops.AFTER: return None
|
||||
# don't optimize ALWAYS_RUN_OPS
|
||||
if b.src[0].op in ALWAYS_RUN_OPS: return None
|
||||
|
||||
new_rng = []
|
||||
hit = False
|
||||
@@ -373,24 +353,20 @@ def bufferize_to_store(ctx:itertools.count, x:UOp, idx:UOp, allow_locals=True):
|
||||
assert size > 0 and isinstance(size, int), f"no zero sized or symbolic sized buffers {size}"
|
||||
|
||||
sdtype = x.dtype.ptr(size=size, addrspace=x.arg.addrspace)
|
||||
# AFTER: add END to the existing STORE, return buffer with kernel dependency
|
||||
if x.src[0].op is Ops.AFTER:
|
||||
buf = x.src[0].src[0].buf_uop.base
|
||||
stores = [s for s in x.src[0].src[1:] if s.op is Ops.STORE]
|
||||
return buf.after(*[s.end(*rngs) for s in stores]) if stores else buf
|
||||
if (assign := x.src[0]).op is Ops.ASSIGN:
|
||||
assign_target, assign_src = assign.src[0], assign.src[1]
|
||||
assert assign_target.op is Ops.INDEX, f"{assign_target.op} is not index"
|
||||
while assign_src.op is Ops.NOOP: assign_src = assign_src.src[0]
|
||||
|
||||
store_target = assign_target
|
||||
if assign_target.src[0].op is Ops.BUFFERIZE and assign_target.src[0].src[0].op is Ops.INDEX:
|
||||
if assign.arg and assign_target.src[0].op is Ops.BUFFERIZE and assign_target.src[0].src[0].op is Ops.INDEX:
|
||||
# BUFFERIZE(INDEX(...)); store through the underlying global index instead.
|
||||
store_target = assign_target.src[0].src[0]
|
||||
|
||||
end_rngs = sorted(dedup(tuple(store_target.ranges) + tuple(rngs)), key=lambda x: x.arg)
|
||||
ret = store_target.buf_uop.base
|
||||
if assign_src is not assign_target: ret = ret.after(store_target.replace(dtype=sdtype).store(assign_src).end(*end_rngs))
|
||||
if assign_src is not store_target: ret = ret.after(store_target.replace(dtype=sdtype).store(assign_src).end(*end_rngs))
|
||||
for op, marg in reversed(assign.arg or ()): ret = ret._mop(op, marg)
|
||||
return ret
|
||||
|
||||
# NOTE: the DEFINE_LOCAL needs to be disambiguated here
|
||||
@@ -540,8 +516,6 @@ pm_add_range_tags = PatternMatcher([
|
||||
def split_store(x:UOp) -> UOp|None:
|
||||
# if we have any open ranges here, we don't split
|
||||
if x.ranges: return None
|
||||
# raw STORE (not from bufferize_to_store) should be processed through its END wrapper, not independently
|
||||
if x.op is Ops.STORE and x.src[0]._shape is not None: return None
|
||||
|
||||
# local kernel rewrite
|
||||
lctx = LocalAddBufferContext()
|
||||
|
||||
+13
-26
@@ -313,14 +313,7 @@ class Tensor(OpMixin):
|
||||
assign_uop = self.uop.assign(x.uop)
|
||||
base = self.uop.base
|
||||
if base.op in {Ops.BUFFER, Ops.AFTER} and not self.uop.has_buffer_identity():
|
||||
original_uop = self.uop
|
||||
assigned_base = base.after(assign_uop)
|
||||
_apply_map_to_tensors({base: assigned_base}, name="Embed View Assign", walk=True)
|
||||
def replace_view_base(u:UOp) -> UOp:
|
||||
return u.replace(src=((assigned_base if u.src[0] is base else replace_view_base(u.src[0])),)+u.src[1:])
|
||||
ret = Tensor(replace_view_base(original_uop), device=self.device, requires_grad=self.requires_grad)
|
||||
self.replace(self._apply_uop(lambda *_: assign_uop, x))
|
||||
return ret
|
||||
_apply_map_to_tensors({base: base.after(assign_uop)}, name="Embed View Assign", walk=True)
|
||||
return self.replace(self._apply_uop(lambda *_: assign_uop, x))
|
||||
|
||||
def detach(self) -> Tensor:
|
||||
@@ -1347,7 +1340,7 @@ class Tensor(OpMixin):
|
||||
if is_disk: raise RuntimeError("advanced setitem is not supported for DISK tensors")
|
||||
if not isinstance(v, Tensor): v = Tensor(v, device=self.device, dtype=self.dtype)
|
||||
self.assign(self._getitem(indices, v))
|
||||
elif is_disk or self.uop.is_realized or self.uop.base.op in (Ops.AFTER, Ops.BUFFER): # basic setitem, self is realized
|
||||
elif is_disk or self.uop.is_realized or self.uop.base.op is Ops.AFTER: # basic setitem, self is realized
|
||||
view = self[indices]
|
||||
if isinstance(v, Tensor) and v.uop.op is Ops.ASSIGN and v.uop in view.uop.base.src: return
|
||||
view.assign(v)
|
||||
@@ -2491,7 +2484,7 @@ class Tensor(OpMixin):
|
||||
"""
|
||||
if IMAGE: return self.image_dot(w, dtype)
|
||||
if ASM_GEMM:
|
||||
from extra.gemm.cdna_asm_gemm import can_use_asm_gemm, asm_gemm
|
||||
from extra.gemm.asm.cdna.gemm import can_use_asm_gemm, asm_gemm
|
||||
if can_use_asm_gemm(self, w): return asm_gemm(self, w)
|
||||
x, dx, dw = self, self.ndim, w.ndim
|
||||
if not (dx > 0 and dw > 0): raise RuntimeError(f"both tensors need to be at least 1D, got {dx}D and {dw}D")
|
||||
@@ -3662,27 +3655,21 @@ class Tensor(OpMixin):
|
||||
|
||||
# contiguous creates the image, and early realize static weights (TODO: test for the static weight)
|
||||
if IMAGE == 1:
|
||||
def is_pow2(v): return v > 0 and v & (v - 1) == 0
|
||||
# pad dimension i to amt with invalids
|
||||
def ipad(t, i, amt):
|
||||
shape = (None,)*i + (amt,) + (None,)*(t.ndim-i-1)
|
||||
return Tensor(True, device=t.device).expand(t.shape).pad_to(shape).where(t.pad_to(shape), Invalid) if amt != t.shape[i] else t
|
||||
# align a dimension to 64 bytes
|
||||
def pad_align(t, dim):
|
||||
return ipad(t, dim, round_up(t.shape[dim], (64 // dtsz) // math.gcd(prod(t.shape) // t.shape[dim], (64 // dtsz))))
|
||||
|
||||
# bank conflicts
|
||||
if cin >= 8 and is_pow2(cin // 4): x, w = ipad(x.reshape(bs, iy, ix, groups, cin // 4, 4), 4, cin // 4 + 1), ipad(w, 2, cin // 4 + 1)
|
||||
|
||||
# 64-byte pitch alignment
|
||||
x, w = pad_align(x, 2), pad_align(w, 1)
|
||||
# pad with Invalid
|
||||
def _invalid_pad_to(t, shape):
|
||||
if all(p is None or p == s for p,s in zip(shape, t.shape)): return t
|
||||
return Tensor(True, device=t.device).expand(t.shape).pad_to(shape).where(t.pad_to(shape), Invalid)
|
||||
# hacks for pitch alignment
|
||||
assert isinstance(ix, int) and isinstance(H, int)
|
||||
ALIGN = 64 // dtsz
|
||||
x = _invalid_pad_to(x, (None, None, round_up(ix, ALIGN // math.gcd(groups * cin, ALIGN)), None))
|
||||
w = _invalid_pad_to(w, (None, round_up(H, ALIGN // math.gcd(W * cin * 4, ALIGN))) + (None,) * (w.ndim - 2))
|
||||
|
||||
if FLOAT16: x, w = x.cast(dtypes.half).contiguous().cast(dtypes.float), w.cast(dtypes.half).contiguous().cast(dtypes.float)
|
||||
else: x, w = x.contiguous(), w.contiguous()
|
||||
|
||||
# undo alignment hacks
|
||||
if cin >= 8 and is_pow2(cin // 4): x, w = x[:, :, :ix, :, :cin // 4, :], w[:, :H, :cin // 4, ...]
|
||||
else: x, w = x[:, :, :ix, :], w[:, :H, ...]
|
||||
x, w = x[:, :, :ix, :], w[:, :H, ...]
|
||||
|
||||
elif IMAGE: x, w = x.cast(base_image_type((bs*iy, ix*groups*cin//4, 4))).contiguous(), w.cast(base_image_type((cout//4, H*W*cin, 4))).contiguous()
|
||||
else: x, w = x.contiguous(), w.contiguous()
|
||||
|
||||
@@ -39,9 +39,6 @@ class Ops(FastEnum):
|
||||
# vector creation / item selection
|
||||
GEP = auto(); VECTORIZE = auto()
|
||||
|
||||
# tuple/gettuple for function with multiple returns
|
||||
TUPLE = auto(); GETTUPLE = auto()
|
||||
|
||||
# ** 3 -- load/store **
|
||||
|
||||
# INDEX is a BinaryOp similar to ADD, but it operates on pointers
|
||||
|
||||
+5
-23
@@ -209,15 +209,9 @@ class UOp(OpMixin, metaclass=UOpMetaClass):
|
||||
# late ops don't have shape
|
||||
case Ops.UNIQUE | Ops.LUNIQUE | Ops.DEVICE | Ops.RANGE | Ops.LOAD | Ops.STORE | Ops.IF | Ops.BARRIER | Ops.CUSTOM | Ops.CUSTOMI | \
|
||||
Ops.VECTORIZE | Ops.GEP | Ops.SPECIAL | Ops.UNROLL | Ops.CONTRACT | Ops.SINK | \
|
||||
Ops.LINEAR | Ops.PROGRAM | Ops.SOURCE | Ops.BINARY | Ops.INS | Ops.TUPLE:
|
||||
Ops.LINEAR | Ops.PROGRAM | Ops.SOURCE | Ops.BINARY | Ops.INS:
|
||||
return None
|
||||
|
||||
case Ops.GETTUPLE:
|
||||
# GETTUPLE extracts from a TUPLE
|
||||
in_tuple = self.src[0].src[0] if self.src[0].op is Ops.CALL else self.src[0]
|
||||
assert in_tuple.op is Ops.TUPLE
|
||||
return in_tuple.src[self.arg]._shape
|
||||
|
||||
case Ops.CAST:
|
||||
# when PTX casts from ptr to non ptr, remove the shape
|
||||
if isinstance(self.src[0].dtype, PtrDType) and not isinstance(self.src[0].dtype, ImageDType) and not isinstance(self.dtype, PtrDType):
|
||||
@@ -410,12 +404,6 @@ class UOp(OpMixin, metaclass=UOpMetaClass):
|
||||
|
||||
def sink(*srcs:UOp|None, **kwargs): # pylint: disable=no-self-argument
|
||||
return UOp(Ops.SINK, dtypes.void, tuple([x for x in srcs if x is not None]), **kwargs)
|
||||
def maketuple(*srcs:UOp): # pylint: disable=no-self-argument
|
||||
return UOp(Ops.TUPLE, dtypes.void, srcs)
|
||||
def gettuple(self, idx:int) -> UOp:
|
||||
in_tuple = self.src[0] if self.op is Ops.CALL else self
|
||||
assert in_tuple.op is Ops.TUPLE, f"gettuple requires CALL or TUPLE source, got {self.op}"
|
||||
return UOp(Ops.GETTUPLE, in_tuple.src[idx].dtype, (self,), idx)
|
||||
def group(*srcs:UOp|None): # pylint: disable=no-self-argument
|
||||
if len(srcs) == 1 and isinstance(srcs[0], UOp): return srcs[0]
|
||||
return UOp(Ops.GROUP, dtypes.void, tuple([x for x in srcs if x is not None]))
|
||||
@@ -426,8 +414,6 @@ class UOp(OpMixin, metaclass=UOpMetaClass):
|
||||
return UOp(Ops.INDEX, kwargs.pop("dtype", self.dtype if ptr else self.dtype.base), (self,)+tuple([x for x in srcs if x is not None]), **kwargs)
|
||||
def __getitem__(self, idx):
|
||||
idx = argfix(idx)
|
||||
# add : to the end
|
||||
if len(idx) < len(self.shape): idx += tuple([slice(None)]*(len(self.shape)-len(idx)))
|
||||
assert len(idx) == len(self.shape), f"__getitem__ shape mismatch, indexing {self.shape} with {len(idx)} args"
|
||||
if len(slice_idx:=[i for i,x in enumerate(idx) if isinstance(x, slice)]):
|
||||
perm = self.permute(tuple([i for i in range(self.ndim) if i not in slice_idx] + slice_idx))
|
||||
@@ -915,10 +901,9 @@ class UOp(OpMixin, metaclass=UOpMetaClass):
|
||||
if self.axis is not None: p = p.replace(src=p.src + (UOp(Ops.MULTI, arg=self.axis),))
|
||||
return p
|
||||
|
||||
def call(self, *srcs:UOp, grad_fxn:Callable|None=None, metadata:tuple[Metadata, ...]=(),
|
||||
name:str|None=None, precompile:bool=False, precompile_backward:bool=False) -> UOp:
|
||||
def call(self, *srcs:UOp, grad_fxn:Callable|None=None, metadata:tuple[Metadata, ...]=(), name:str|None=None, precompile:bool=False) -> UOp:
|
||||
assert len(self.ranges) == 0, f"ranges {self.ranges} are leaking out of the call in {self.pyrender()}"
|
||||
return UOp(Ops.CALL, self.dtype, (self,)+srcs, CallInfo(grad_fxn, metadata, name, precompile, precompile_backward))
|
||||
return UOp(Ops.CALL, self.dtype, (self,)+srcs, CallInfo(grad_fxn, metadata, name, precompile))
|
||||
def custom_kernel(*srcs:UOp, fxn:Callable, grad_fxn:Callable|None=None) -> list[UOp]:
|
||||
contig_srcs = tuple(x.contiguous() if x.op is not Ops.AFTER else x for x in srcs)
|
||||
placeholders = [UOp.placeholder_like(s, slot=i) for i,s in enumerate(contig_srcs)]
|
||||
@@ -942,12 +927,9 @@ class CallInfo:
|
||||
metadata: tuple[Metadata, ...] = ()
|
||||
name: str|None = None
|
||||
precompile: bool = False
|
||||
precompile_backward: bool = False
|
||||
# grad_fxn can't be pickled, but metadata can
|
||||
def __reduce__(self): return (CallInfo, (None, self.metadata, self.name, self.precompile, self.precompile_backward))
|
||||
def __repr__(self):
|
||||
gf = id(self.grad_fxn) if self.grad_fxn else None
|
||||
return f"CallInfo({gf}, {self.metadata}, {repr(self.name)}, {self.precompile}, {self.precompile_backward})"
|
||||
def __reduce__(self): return (CallInfo, (None, self.metadata, self.name, self.precompile))
|
||||
def __repr__(self): return f"CallInfo({id(self.grad_fxn) if self.grad_fxn else None}, {self.metadata}, {repr(self.name)}, {self.precompile})"
|
||||
|
||||
def should_resolve_call(c:UOp) -> bool:
|
||||
# don't resolve real kernel calls, sink or program
|
||||
|
||||
+3
-10
@@ -59,9 +59,6 @@ shared_spec = PatternMatcher([
|
||||
# RANGE/SPECIAL define loops, END closes them
|
||||
(UPat(Ops.END, src=(UPat(), UPat(Ops.RANGE))), lambda: True),
|
||||
|
||||
# STORE in tensor graph: store a value into a target
|
||||
(UPat(Ops.STORE, dtypes.void, (UPat(), UPat())), lambda: True),
|
||||
|
||||
# NOOP
|
||||
(UPat(Ops.NOOP), lambda: True)
|
||||
])
|
||||
@@ -78,7 +75,7 @@ movement_ops = PatternMatcher([
|
||||
(UPat({Ops.ADD, Ops.MUL, Ops.IDIV}, dtype=dtypes.index), lambda: True),
|
||||
|
||||
# AFTER on Movement Op or ASSIGN
|
||||
(UPat(Ops.AFTER, src=(UPat(GroupOp.Movement.union({Ops.MULTI, Ops.CONTIGUOUS, Ops.ASSIGN, Ops.BUFFER})),), allow_any_len=True), lambda: True),
|
||||
(UPat(Ops.AFTER, src=(UPat(GroupOp.Movement.union({Ops.MULTI, Ops.CONTIGUOUS, Ops.ASSIGN})),), allow_any_len=True), lambda: True),
|
||||
])
|
||||
|
||||
_tensor_spec = PatternMatcher([
|
||||
@@ -139,10 +136,6 @@ _tensor_spec = PatternMatcher([
|
||||
(UPat(Ops.PARAM), lambda: True),
|
||||
(UPat(Ops.CUSTOM_FUNCTION, name="x"), lambda x: isinstance(x.arg, str)),
|
||||
|
||||
# TUPLE must have void dtype, GETTUPLE can only appear on CALL or TUPLE
|
||||
(UPat(Ops.TUPLE, dtypes.void), lambda: True),
|
||||
(UPat(Ops.GETTUPLE, src=(UPat((Ops.CALL, Ops.TUPLE)),), name="g"), lambda g: isinstance(g.arg, int)),
|
||||
|
||||
# ** for custom kernels **
|
||||
|
||||
# codegen: PROGRAM with progressive sources through the pipeline (SINK, DEVICE, LINEAR?, SOURCE?, BINARY?)
|
||||
@@ -237,8 +230,8 @@ program_spec = PatternMatcher([
|
||||
# END closes ranges
|
||||
(UPat(Ops.END, src=(UPat(), UPat(Ops.RANGE)), dtype=dtypes.void), lambda: True),
|
||||
|
||||
# make sure all index dtypes have been lowered (except CONST/RANGE/DEFINE_VAR which are valid index-typed)
|
||||
(UPat(GroupOp.All-{Ops.CONST, Ops.RANGE, Ops.DEFINE_VAR, Ops.VCONST, Ops.VECTORIZE}, dtype=dtypes.index), lambda: False),
|
||||
# make sure all index dtypes have been lowered
|
||||
(UPat(GroupOp.All, dtype=dtypes.index), lambda: False),
|
||||
(UPat(Ops.CONST, arg=Invalid), lambda: False),
|
||||
(UPat(Ops.VCONST, name="x"), lambda x: all(v is not Invalid for v in x.arg) and len(x.arg)==x.dtype.vcount>1 and
|
||||
type(x.arg) is type(dtypes.as_const(x.arg, x.dtype))),
|
||||
|
||||
@@ -65,8 +65,6 @@ propagate_invalid = PatternMatcher([
|
||||
(UPat(GroupOp.Binary-GroupOp.Comparison, src=[invalid_pat, UPat()]), lambda i: i),
|
||||
# normalize where(cond, Invalid, val) -> where(~cond, val, Invalid)
|
||||
(UPat.var("cond").where(invalid_pat, UPat.var("val")), lambda cond, i, val: cond.logical_not().where(val, i) if val.arg != Invalid else i),
|
||||
(UPat.var("a").where(invalid_gate, UPat.var("c")), lambda cond,i,x,a,c: cond.where(a.where(x, c), i) if c.arg != Invalid else None),
|
||||
(UPat.var("a").where(UPat.var("b"), invalid_gate), lambda cond,i,x,a,b: cond.where(a.where(b, x), i) if b.arg != Invalid else None),
|
||||
(UPat(Ops.BITCAST, src=(invalid_pat,), name="bc"), lambda bc,i: i.cast(bc.dtype)),
|
||||
(UPat(Ops.BITCAST, src=(invalid_gate,), name="bc"), lambda bc,cond,x,i: cond.where(x.bitcast(bc.dtype), i.bitcast(bc.dtype))),
|
||||
])
|
||||
|
||||
@@ -157,6 +157,10 @@
|
||||
#insts .left.highlight {
|
||||
background-color: rgba(0, 199, 47, 0.2);
|
||||
}
|
||||
#insts .n {
|
||||
color: #787fa1;
|
||||
min-width: 5ch;
|
||||
}
|
||||
#insts .wave {
|
||||
color: #7aa2f7;
|
||||
min-width: 2ch;
|
||||
|
||||
@@ -323,11 +323,10 @@ function setFocus(key) {
|
||||
let instList = document.getElementById("insts");
|
||||
if (data.pcToShape.size == 0) return d3.select(instList?.parentElement).html("");
|
||||
if (instList == null) {
|
||||
let contents = "";
|
||||
let contents = "", i = 0;
|
||||
for (const [k, v] of data.pcToShape) {
|
||||
const pcHex = v.pc.toString(16);
|
||||
contents += `<div class="line" data-k="${k}"><span class="left" id="inst-${k}"><span class="wave">${v.wave}</span>
|
||||
<span class="pc">${"0x"+pcHex.padStart(Math.max(4, Math.ceil(pcHex.length/4)*4), 0)}</span><span class="label">${data.pcMap[v.pc]}</span></div>`;
|
||||
contents += `<div class="line" data-k="${k}"><span class="left" id="inst-${k}"><span class="n">${i++}</span><span class="wave">${v.wave}</span>
|
||||
<span class="pc">${"0x"+v.pc.toString(16).padStart(12, "0")}</span></span><span class="label">${data.pcMap[v.pc]}</span></div>`;
|
||||
}
|
||||
instList = d3.create("pre").append("code").classed("hljs", true).style("margin-top", "20px").attr("id", "insts").html(contents)
|
||||
.on("click", e => { const line = e.target.closest(".line"); line && setFocus(line.dataset.k); }).node();
|
||||
|
||||
+8
-12
@@ -342,17 +342,13 @@ def sqtt_timeline(data:bytes, lib:bytes, target:str) -> list[ProfileEvent]:
|
||||
from tinygrad.renderer.amd.sqtt import INST_RDNA4, InstOpRDNA4, TS_DELTA_OR_MARK, TS_DELTA_OR_MARK_RDNA4
|
||||
ret:list[ProfileEvent] = []
|
||||
row_ends:dict[str, Decimal] = {}
|
||||
curr_barrier:dict[str, ProfileRangeEvent] = {}
|
||||
NS_PER_TICK = 10 # 100MHz
|
||||
prev_pair:tuple[int, int]|None = None # (shader, realtime)
|
||||
def add(name:str, p:PacketType, op:str|None=None, wave:int|None=None, info:InstructionInfo|None=None) -> None:
|
||||
def add(name:str, p:PacketType, width=1, op:str|None=None, wave:int|None=None, info:InstructionInfo|None=None) -> None:
|
||||
row = f"WAVE:{wave}" if (wave:=getattr(p, "wave", wave)) is not None else f"{p.__class__.__name__}:0 {name}"
|
||||
# barrier on this row extends to fill the time our wave was waiting
|
||||
if (barrier:=curr_barrier.pop(row, None)) is not None: barrier.en = Decimal(p._time)
|
||||
ret.append(e:=ProfileRangeEvent(row, TracingKey(op or name, ret=f"PC:{info.pc}" if info else None), Decimal(p._time), Decimal(p._time+1)))
|
||||
ret.append(e:=ProfileRangeEvent(row, TracingKey(op or name, ret=f"PC:{info.pc}" if info else None), Decimal(p._time), Decimal(p._time+width)))
|
||||
if (et:=row_ends.get(row)) is not None and e.st < et: raise RuntimeError(f"packet {p} overlaps another packet in {row}.")
|
||||
row_ends[row] = unwrap(e.en)
|
||||
if name == "BARRIER": curr_barrier[row] = e
|
||||
for p, info in map_insts(data, lib, target):
|
||||
if len(ret) > getenv("MAX_SQTT_PKTS", 50_000): break
|
||||
if isinstance(p, (TS_DELTA_OR_MARK, TS_DELTA_OR_MARK_RDNA4)) and p.is_marker:
|
||||
@@ -365,7 +361,7 @@ def sqtt_timeline(data:bytes, lib:bytes, target:str) -> list[ProfileEvent]:
|
||||
prev_pair = pair
|
||||
if isinstance(p, (INST, INST_RDNA4)):
|
||||
name = p.op.name if isinstance(p.op, (InstOp, InstOpRDNA4)) else f"0x{p.op:02x}"
|
||||
add(name, p, info=info)
|
||||
add(name, p, width=10 if "BARRIER" in name else 1, info=info)
|
||||
if isinstance(p, (VALUINST, IMMEDIATE)): add(p.__class__.__name__, p, info=info)
|
||||
if isinstance(p, IMMEDIATE_MASK): add("IMMEDIATE", p, wave=unwrap(info).wave, info=info)
|
||||
if isinstance(p, (VMEMEXEC, ALUEXEC)):
|
||||
@@ -529,7 +525,11 @@ def amdgpu_cfg(lib:bytes, target:str) -> dict:
|
||||
curr:int|None = None
|
||||
blocks:dict[int, list[int]] = {}
|
||||
paths:dict[int, dict[int, int]] = {}
|
||||
lines:list[str] = []
|
||||
disasm = {pc:str(inst) for pc,inst in pc_table.items()}
|
||||
asm_width = max(len(asm) for asm in disasm.values())
|
||||
for pc, inst in pc_table.items():
|
||||
lines.append(f" {disasm[pc]:<{asm_width}} // {pc:012X}")
|
||||
if pc in leaders:
|
||||
paths[curr:=pc] = {}
|
||||
blocks[pc] = []
|
||||
@@ -550,9 +550,6 @@ def amdgpu_cfg(lib:bytes, target:str) -> dict:
|
||||
elif name in {"op","opx","opy"}: tokens.append({"st":(op_name:=val.name.lower()), "keys":[op_name], "kind":0})
|
||||
elif name != "encoding" and val != field.default: tokens.append({"st":(s:=repr(val)), "keys":[s], "kind":1})
|
||||
# show a smaller view for repeated instructions in the graph
|
||||
lines:list[str] = []
|
||||
disasm = {pc:str(inst) for pc,inst in pc_table.items()}
|
||||
asm_width = max(len(asm) for asm in disasm.values())
|
||||
for pcs in blocks.values():
|
||||
new_pcs:list[int] = []
|
||||
i, n = 0, len(pcs)
|
||||
@@ -563,13 +560,12 @@ def amdgpu_cfg(lib:bytes, target:str) -> dict:
|
||||
if j-i>1:
|
||||
pc_tokens[pcs[i]].append({"st":f"({j-i}x)", "keys":[], "kind":0})
|
||||
for k in range(i+1, j): del pc_tokens[pcs[k]]
|
||||
lines.append(f"{disasm[pcs[i]]:<{asm_width}} # {pcs[i]:012X}"+(f"...{pcs[j-1]:012X} ({j-i}x)" if j-i>1 else ""))
|
||||
i = j
|
||||
pcs[:] = new_pcs
|
||||
from tinygrad.runtime.autogen import amdgpu_kd
|
||||
kd = amdgpu_kd.llvm_amdhsa_kernel_descriptor_t.from_buffer_copy(bytearray(get_elf_section(lib, ".rodata").content))
|
||||
vgpr_gran = kd.compute_pgm_rsrc1 & amdgpu_kd.COMPUTE_PGM_RSRC1_GRANULATED_WORKITEM_VGPR_COUNT
|
||||
return {"data":{"blocks":blocks, "paths":paths, "pc_tokens":pc_tokens}, "src":"\n".join(lines), "lang":"python",
|
||||
return {"data":{"blocks":blocks, "paths":paths, "pc_tokens":pc_tokens}, "src":"\n".join(lines),
|
||||
"metadata":[[{"label":f"{r} Alloc", "value":v} for r,v in [("VGPR", (vgpr_gran+1)*8-7), ("LDS", kd.group_segment_fixed_size),
|
||||
("Scratch", kd.private_segment_fixed_size)] if v>0]]}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user