forked from tinygrad/tinygrad
Merge branch 'master' into new_x86_backend
This commit is contained in:
@@ -246,7 +246,7 @@ def train_resnet():
|
||||
|
||||
if i == BENCHMARK:
|
||||
assert not math.isnan(loss)
|
||||
median_step_time = sorted(step_times)[(BENCHMARK + 1) // 2] # in seconds
|
||||
median_step_time = sorted(step_times)[BENCHMARK // 2] # in seconds
|
||||
estimated_total_minutes = int(median_step_time * steps_in_train_epoch * epochs / 60)
|
||||
print(f"Estimated training time: {estimated_total_minutes // 60}h{estimated_total_minutes % 60}m")
|
||||
print(f"epoch global_ops: {steps_in_train_epoch * GlobalCounters.global_ops:_}, "
|
||||
@@ -593,7 +593,7 @@ def train_retinanet():
|
||||
|
||||
if i == BENCHMARK:
|
||||
assert not math.isnan(loss)
|
||||
median_step_time = sorted(step_times)[(BENCHMARK + 1) // 2] # in seconds
|
||||
median_step_time = sorted(step_times)[BENCHMARK // 2] # in seconds
|
||||
estimated_total_minutes = int(median_step_time * steps_in_train_epoch * EPOCHS / 60)
|
||||
print(f"Estimated training time: {estimated_total_minutes // 60}h{estimated_total_minutes % 60}m")
|
||||
print(f"epoch global_ops: {steps_in_train_epoch * GlobalCounters.global_ops:_}, "
|
||||
@@ -868,7 +868,7 @@ def train_unet3d():
|
||||
i += 1
|
||||
|
||||
if i == BENCHMARK:
|
||||
median_step_time = sorted(step_times)[(BENCHMARK + 1) // 2] # in seconds
|
||||
median_step_time = sorted(step_times)[BENCHMARK // 2] # in seconds
|
||||
estimated_total_minutes = int(median_step_time * SAMPLES_PER_EPOCH * NUM_EPOCHS / 60)
|
||||
print(f"Estimated training time: {estimated_total_minutes // 60}h{estimated_total_minutes % 60}m")
|
||||
if (TRAIN_BEAM or EVAL_BEAM) and epoch == start_epoch: break
|
||||
@@ -1167,7 +1167,7 @@ def train_bert():
|
||||
i += 1
|
||||
|
||||
if i == BENCHMARK:
|
||||
median_step_time = sorted(step_times)[(BENCHMARK + 1) // 2] # in seconds
|
||||
median_step_time = sorted(step_times)[BENCHMARK // 2] # in seconds
|
||||
estimated_total_minutes = int(median_step_time * train_steps / 60)
|
||||
print(f"Estimated training time: {estimated_total_minutes // 60}h{estimated_total_minutes % 60}m")
|
||||
print(f"epoch global_ops: {train_steps * GlobalCounters.global_ops:_}, "
|
||||
@@ -1544,7 +1544,7 @@ def train_llama3():
|
||||
|
||||
mem_gb = GlobalCounters.mem_used / 1e9
|
||||
gflops = GlobalCounters.global_ops / 1e9 / dev_time
|
||||
mfu = ((6 * num_params * SEQLEN * GBS) / (dev_time * device_count * 2.3e15)) * 100
|
||||
mfu = ((6 * num_params * SEQLEN * GBS) / (dev_time * device_count * (4.6e15 if FP8 else 2.3e15))) * 100
|
||||
tqdm.write(
|
||||
f"{i:5} {step_time:.3f} s step, {gbs_time:.3f} s gbs, {optim_time:.3f} s optim, {data_time:.3f} s data, {loss:.4f} loss, " \
|
||||
f"{lr:.12f} LR, {grad_norm:.6f} grad_norm, {mem_gb:.2f} GB used, {gflops:9.2f} GFLOPS, {mfu:5.2f}% MFU")
|
||||
@@ -1577,8 +1577,9 @@ def train_llama3():
|
||||
safe_save(get_state_dict(scheduler), fn)
|
||||
|
||||
if i == BENCHMARK:
|
||||
median_step_time = sorted(step_times)[(BENCHMARK + 1) // 2]
|
||||
estimated_total_minutes = int(median_step_time * (SAMPLES // GBS) / 60)
|
||||
median_step_time = sorted(step_times)[BENCHMARK // 2]
|
||||
estimated_steps = 200_000 // GBS if getenv("LLAMA3_SIZE", "8B") == "8B" else MAX_STEPS
|
||||
estimated_total_minutes = int(median_step_time * estimated_steps / 60)
|
||||
print(f"Estimated training time: {estimated_total_minutes // 60}h{estimated_total_minutes % 60}m")
|
||||
print(f"epoch global_ops: {GlobalCounters.global_ops:_}, "
|
||||
f"epoch global_mem: {GlobalCounters.global_mem:_}")
|
||||
|
||||
@@ -35,12 +35,15 @@ def quantize_fp8(x:Tensor, amax_state:Tensor|None=None):
|
||||
return x_clamped.cast(FP8_DTYPE), scale.float().reciprocal()
|
||||
|
||||
def matmul(x:Tensor, w:Tensor, fp8=FP8, amax_x:Tensor|None=None, amax_w:Tensor|None=None) -> Tensor:
|
||||
if not fp8: return x @ w.T
|
||||
from tinygrad.helpers import ASM_GEMM
|
||||
if not fp8:
|
||||
if getenv("ASM_GEMM"):
|
||||
from extra.gemm.cdna_asm_gemm import can_use_asm_gemm, asm_gemm
|
||||
if can_use_asm_gemm(x, w.T): return asm_gemm(x, w.T)
|
||||
return x @ w.T
|
||||
x_fp8, x_scale = quantize_fp8(x, amax_state=amax_x)
|
||||
w_fp8, w_scale = quantize_fp8(w, amax_state=amax_w)
|
||||
combined_scale = x_scale * w_scale
|
||||
if ASM_GEMM:
|
||||
if getenv("ASM_GEMM"):
|
||||
from extra.gemm.cdna_asm_gemm import can_use_asm_gemm, asm_gemm
|
||||
if can_use_asm_gemm(x_fp8, w_fp8.T): return asm_gemm(x_fp8, w_fp8.T, combined_scale=combined_scale)
|
||||
return x_fp8.dot(w_fp8.T, dtype=dtypes.float) * combined_scale
|
||||
@@ -122,7 +125,11 @@ class FlatTransformer:
|
||||
xq, xk = apply_rotary_emb(xq, xk, freqs_cis)
|
||||
if FP8: xq, xk, xv = xq.cast(dtypes.bfloat16), xk.cast(dtypes.bfloat16), xv.cast(dtypes.bfloat16)
|
||||
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)
|
||||
if getenv("HK_FLASH_ATTENTION"):
|
||||
from extra.thunder.amd.fa import flash_attention
|
||||
attn = flash_attention(xq, xk, xv, is_causal=True)
|
||||
else:
|
||||
attn = xq.scaled_dot_product_attention(xk, xv, is_causal=True, enable_gqa=True).transpose(1, 2)
|
||||
attn = attn.reshape(bsz, seqlen, -1)
|
||||
return matmul(attn, wo, amax_x=amax_xo, amax_w=amax_wo)
|
||||
|
||||
@@ -193,7 +200,7 @@ class FlatTransformer:
|
||||
self.attention_norm[i], self.wo[i],
|
||||
self.ffn_norm[i], self.w1[i], self.w2[i], self.w3[i],
|
||||
**attn_kwargs, **amax_attn, **amax_layer)
|
||||
logits = (self.norm(h).contiguous().contiguous_backward() @ self.output[0].T).contiguous_backward()
|
||||
logits = matmul(self.norm(h).contiguous().contiguous_backward(), self.output[0], fp8=False).contiguous_backward()
|
||||
return logits
|
||||
|
||||
def _get_pads(uop:UOp) -> list[UOp]:
|
||||
|
||||
@@ -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
|
||||
+1
-1
@@ -36,7 +36,7 @@ export DATA_SEED=${DATA_SEED:-5760}
|
||||
export JITBEAM=${JITBEAM:-3}
|
||||
export BEAM_UOPS_MAX=6000 BEAM_UPCAST_MAX=256 BEAM_LOCAL_MAX=1024 BEAM_MIN_PROGRESS=5 BEAM_PADTO=1
|
||||
|
||||
export FAKEDATA=1 BENCHMARK=10
|
||||
export FAKEDATA=1 BENCHMARK=${BENCHMARK:-10}
|
||||
if [ -z "$FULL_LAYERS" ]; then
|
||||
export LLAMA_LAYERS=2
|
||||
fi
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
export BENCHMARK=5
|
||||
export EVAL_BS=0
|
||||
VIZ=${VIZ:--1} examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama8b/implementations/tinybox_8xMI350X/dev_run.sh
|
||||
VIZ=${VIZ:--1} FULL_LAYERS=1 DEBUG=0 examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama8b/implementations/tinybox_8xMI350X/dev_beam.sh
|
||||
extra/viz/cli.py --profile -s "${DEV:-AMD}"
|
||||
|
||||
@@ -2716,8 +2716,11 @@ def custom_gemm_bw(gradient:UOp, kernel:UOp):
|
||||
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)
|
||||
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
|
||||
if can_use_asm_gemm(g_t, b_t.T): grad_a = asm_gemm(g_t, b_t.T).uop
|
||||
else: grad_a = (g_t @ b_t.T).uop
|
||||
a_t_flat, g_t_flat = a_t.permute(2, 0, 1).reshape(a_t.shape[2], -1), g_t.reshape(-1, g_t.shape[-1])
|
||||
if can_use_asm_gemm(a_t_flat, g_t_flat): grad_b = asm_gemm(a_t_flat, g_t_flat).uop
|
||||
else: grad_b = (a_t_flat @ g_t_flat).uop
|
||||
return (None, grad_a, grad_b)
|
||||
|
||||
# ** main gemm function
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
# RDNA4 128x128 GEMM using WMMA — optimized DS scheduling
|
||||
import numpy as np
|
||||
from tinygrad import Tensor, Device, Context, GlobalCounters
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo
|
||||
from tinygrad.helpers import getenv, colored
|
||||
from tinygrad.dtype import dtypes, AddrSpace
|
||||
from tinygrad.engine.realize import Estimates
|
||||
from tinygrad.renderer.amd.dsl import s, v, VCC_LO, NULL, src, ttmp
|
||||
from tinygrad.runtime.autogen.amd.rdna4.ins import *
|
||||
|
||||
BLOCK_M, BLOCK_N, BLOCK_K = 128, 128, 16
|
||||
TILES_M, TILES_N = 4, 4
|
||||
THREADS, ELEM = 128, 2
|
||||
LDS_A_ROW = BLOCK_K*ELEM # 32
|
||||
LDS_B_ROW = BLOCK_N*ELEM # 256
|
||||
LDS_A_SIZE = BLOCK_M * LDS_A_ROW # 4096
|
||||
LDS_B_SIZE = BLOCK_K * LDS_B_ROW # 4096
|
||||
LDS_SIZE = LDS_A_SIZE + LDS_B_SIZE # 8192
|
||||
LDS_B_OFF = LDS_A_SIZE
|
||||
ACC, DA, DB, FA, FB, ET = 60, 188, 196, 204, 44, 10
|
||||
|
||||
def build_kernel(N, arch='gfx1200'):
|
||||
assert N % BLOCK_M == 0 and N >= 256
|
||||
NO_ALU, NO_DS, NO_GLOBAL = getenv("NO_ALU", 0), getenv("NO_DS", 0), getenv("NO_GLOBAL", 0)
|
||||
I, L, B = [], {}, []
|
||||
def e(i): I.append(i); return i
|
||||
def label(n): L[n] = sum(i.size() for i in I)
|
||||
def br(i, t): B.append((len(I)-1, t))
|
||||
|
||||
e(s_load_b128(sdata=s[4:7], sbase=s[0:1], ioffset=0, soffset=NULL))
|
||||
e(s_load_b64(sdata=s[8:9], sbase=s[0:1], ioffset=0x10, soffset=NULL))
|
||||
e(s_wait_kmcnt(simm16=0))
|
||||
e(s_mov_b32(s[10], ttmp[9])); e(s_and_b32(s[11], ttmp[7], 0xFFFF))
|
||||
e(s_lshl_b32(s[10], s[10], 7)); e(s_lshl_b32(s[11], s[11], 7))
|
||||
e(s_mov_b32(s[12], N)); e(s_lshl_b32(s[13], s[12], 1))
|
||||
e(s_mul_i32(s[14], s[12], BLOCK_K*ELEM))
|
||||
e(s_add_co_i32(s[17], s[12], -2*BLOCK_K)) # loop bound
|
||||
|
||||
e(v_and_b32_e32(v[1], 31, v[0])); e(v_lshrrev_b32_e32(v[2], 5, v[0]))
|
||||
e(v_and_b32_e32(v[3], 1, v[2])); e(v_lshrrev_b32_e32(v[2], 1, v[2]))
|
||||
|
||||
e(v_lshlrev_b32_e32(v[4], 5, v[0]))
|
||||
# B store: transposed layout for stride-32 reads. addr = LDS_B_OFF + (tid%8)*512 + (tid/8)*32
|
||||
e(v_and_b32_e32(v[48], 7, v[0])); e(v_lshlrev_b32_e32(v[5], 9, v[48])) # (tid%8)*512
|
||||
e(v_lshrrev_b32_e32(v[48], 3, v[0])); e(v_lshlrev_b32_e32(v[48], 5, v[48])) # (tid/8)*32
|
||||
e(v_add_nc_u32_e32(v[5], v[5], v[48])); e(v_add_nc_u32_e32(v[5], LDS_B_OFF, v[5]))
|
||||
|
||||
e(v_add_nc_u32_e32(v[48], s[11], v[0]))
|
||||
e(v_mul_lo_u32(v[6], v[48], N*ELEM)); e(v_mov_b32_e32(v[7], 0))
|
||||
e(v_lshrrev_b32_e32(v[48], 3, v[0])); e(v_mul_lo_u32(v[8], v[48], N*ELEM))
|
||||
e(v_and_b32_e32(v[48], 7, v[0])); e(v_lshlrev_b32_e32(v[48], 5, v[48]))
|
||||
e(v_add_nc_u32_e32(v[8], v[8], v[48]))
|
||||
e(s_mul_i32(s[15], s[10], ELEM)); e(v_add_nc_u32_e32(v[8], s[15], v[8]))
|
||||
e(v_mov_b32_e32(v[9], 0))
|
||||
|
||||
# LDS read addrs with padded strides (eliminates bank conflicts)
|
||||
# A: (lane%16)*LDS_A_ROW + (lane/16)*16 + wave_m*64*LDS_A_ROW
|
||||
# B: (lane%16)*LDS_B_ROW + (lane/16)*16 + wave_n*64*ELEM + LDS_B_OFF
|
||||
LLA, LLB = 40, 43
|
||||
e(v_and_b32_e32(v[50], 15, v[1])); e(v_lshrrev_b32_e32(v[51], 4, v[1]))
|
||||
e(v_lshlrev_b32_e32(v[LLA], 5, v[50])) # (lane%16) * 32
|
||||
e(v_lshlrev_b32_e32(v[51], 4, v[51])) # (lane/16) * 16
|
||||
e(v_add_nc_u32_e32(v[LLA], v[LLA], v[51]))
|
||||
e(v_lshlrev_b32_e32(v[52], 11, v[2])) # wave_m * 2048
|
||||
e(v_add_nc_u32_e32(v[LLA], v[LLA], v[52]))
|
||||
# B read: transposed layout. addr = LDS_B_OFF + (lane%16)*32 + (lane/16)*16 + wave_n*2*512
|
||||
# wave_n selects column panels: wave_n*2 panels (each panel=16 cols, wave_n covers 64 cols = 4 panels)
|
||||
# But wave_n*2*512 = wave_n*1024. Hmm, wave_n covers cols [wave_n*64 : (wave_n+1)*64].
|
||||
# Each panel = 16 cols = 512 bytes. wave_n*64/16 = wave_n*4 panels. Offset = wave_n*4*512 = wave_n*2048.
|
||||
e(v_lshlrev_b32_e32(v[LLB], 5, v[50])) # (lane%16) * 32 (stride 32!)
|
||||
e(v_add_nc_u32_e32(v[LLB], v[LLB], v[51])) # + (lane/16)*16
|
||||
e(v_lshlrev_b32_e32(v[52], 11, v[3])) # wave_n * 2048
|
||||
e(v_add_nc_u32_e32(v[LLB], v[LLB], v[52]))
|
||||
e(v_add_nc_u32_e32(v[LLB], LDS_B_OFF, v[LLB]))
|
||||
|
||||
for i in range(0, 128, 2):
|
||||
e(VOPD(VOPDOp.V_DUAL_MOV_B32, VOPDOp.V_DUAL_MOV_B32, vdstx=v[ACC+i], vdsty=v[ACC+i+1], srcx0=0, srcy0=0))
|
||||
e(s_mov_b32(s[16], 0))
|
||||
|
||||
if not NO_GLOBAL:
|
||||
for i in range(2): e(global_load_b128(vdst=v[DA+i*4:DA+i*4+3], vaddr=v[6:7], saddr=s[4:5], ioffset=i*16))
|
||||
for i in range(2): e(global_load_b128(vdst=v[DB+i*4:DB+i*4+3], vaddr=v[8:9], saddr=s[6:7], ioffset=i*16))
|
||||
e(s_wait_loadcnt(simm16=0))
|
||||
if not NO_DS:
|
||||
for i in range(2): e(ds_store_b128(addr=v[4], data0=v[DA+i*4:DA+i*4+3], offset0=(i*16)&0xFF, offset1=(i*16)>>8))
|
||||
for i in range(2): e(ds_store_b128(addr=v[5], data0=v[DB+i*4:DB+i*4+3], offset0=(i*16)&0xFF, offset1=(i*16)>>8))
|
||||
if not NO_GLOBAL:
|
||||
e(v_add_nc_u32_e32(v[6], BLOCK_K*ELEM, v[6]))
|
||||
e(v_add_nc_u32_e32(v[8], s[14], v[8]))
|
||||
|
||||
# =============================================================================
|
||||
def emit_iter_body(load_set='AB'):
|
||||
if not NO_DS:
|
||||
e(s_wait_dscnt(simm16=0))
|
||||
e(s_barrier_signal(ssrc0=src[193])); e(s_barrier_wait(simm16=0xFFFF))
|
||||
if not NO_GLOBAL:
|
||||
if 'A' in load_set:
|
||||
for i in range(2): e(global_load_b128(vdst=v[DA+i*4:DA+i*4+3], vaddr=v[6:7], saddr=s[4:5], ioffset=i*16))
|
||||
e(v_add_nc_u32_e32(v[6], BLOCK_K*ELEM, v[6]))
|
||||
if 'B' in load_set:
|
||||
for i in range(2): e(global_load_b128(vdst=v[DB+i*4:DB+i*4+3], vaddr=v[8:9], saddr=s[6:7], ioffset=i*16))
|
||||
e(v_add_nc_u32_e32(v[8], s[14], v[8]))
|
||||
if not NO_DS:
|
||||
# Issue 6 loads: A[0:3] + B[0] + B[1]. B[2:3] interleaved with WMMAs.
|
||||
for tm in range(TILES_M):
|
||||
aoff = tm * 16 * LDS_A_ROW
|
||||
e(ds_load_b128(vdst=v[FA+tm*4:FA+tm*4+3], addr=v[LLA], offset0=aoff&0xFF, offset1=aoff>>8))
|
||||
e(ds_load_b128(vdst=v[FB:FB+3], addr=v[LLB], offset0=0, offset1=0))
|
||||
e(ds_load_b128(vdst=v[FB+4:FB+7], addr=v[LLB], offset0=0, offset1=2))
|
||||
e(s_wait_dscnt(simm16=0)) # wait for 6 loads (no stall!)
|
||||
if not NO_ALU:
|
||||
# B[0] WMMAs — issue B[2] during compute
|
||||
if not NO_DS: e(ds_load_b128(vdst=v[FB+8:FB+11], addr=v[LLB], offset0=0, offset1=4))
|
||||
for tm in range(TILES_M):
|
||||
ac = ACC + (tm*TILES_N+0)*8
|
||||
e(v_wmma_f32_16x16x16_f16(vdst=v[ac:ac+7], src0=v[FA+tm*4:FA+tm*4+3], src1=v[FB:FB+3], src2=v[ac:ac+7]))
|
||||
# B[1] WMMAs — issue B[3] during compute
|
||||
if not NO_DS:
|
||||
e(ds_load_b128(vdst=v[FB+12:FB+15], addr=v[LLB], offset0=0, offset1=6))
|
||||
for tm in range(TILES_M):
|
||||
ac = ACC + (tm*TILES_N+1)*8
|
||||
e(v_wmma_f32_16x16x16_f16(vdst=v[ac:ac+7], src0=v[FA+tm*4:FA+tm*4+3], src1=v[FB+4:FB+7], src2=v[ac:ac+7]))
|
||||
# B[2] WMMAs — B[2] loaded during B[0] WMMAs (~100 cycles ago)
|
||||
if not NO_DS: e(s_wait_dscnt(simm16=1)) # B[2] done, B[3] may still be loading
|
||||
for tm in range(TILES_M):
|
||||
ac = ACC + (tm*TILES_N+2)*8
|
||||
e(v_wmma_f32_16x16x16_f16(vdst=v[ac:ac+7], src0=v[FA+tm*4:FA+tm*4+3], src1=v[FB+8:FB+11], src2=v[ac:ac+7]))
|
||||
# B[3] WMMAs
|
||||
if not NO_DS: e(s_wait_dscnt(simm16=0))
|
||||
for tm in range(TILES_M):
|
||||
ac = ACC + (tm*TILES_N+3)*8
|
||||
e(v_wmma_f32_16x16x16_f16(vdst=v[ac:ac+7], src0=v[FA+tm*4:FA+tm*4+3], src1=v[FB+12:FB+15], src2=v[ac:ac+7]))
|
||||
if not NO_GLOBAL and not NO_DS: e(s_wait_loadcnt(simm16=0))
|
||||
if not NO_DS:
|
||||
for i in range(2): e(ds_store_b128(addr=v[4], data0=v[DA+i*4:DA+i*4+3], offset0=(i*16)&0xFF, offset1=(i*16)>>8))
|
||||
for i in range(2): e(ds_store_b128(addr=v[5], data0=v[DB+i*4:DB+i*4+3], offset0=(i*16)&0xFF, offset1=(i*16)>>8))
|
||||
e(s_add_co_i32(s[16], s[16], BLOCK_K))
|
||||
|
||||
label('LOOP')
|
||||
emit_iter_body(load_set='A')
|
||||
emit_iter_body(load_set='B')
|
||||
e(s_cmp_lt_i32(s[16], s[17])); e(s_cbranch_scc1(simm16=0)); br(I[-1], 'LOOP')
|
||||
|
||||
emit_iter_body(load_set='AB') # tail with prefetch
|
||||
|
||||
# Final iteration: no prefetch, no ds_store needed
|
||||
if not NO_DS:
|
||||
e(s_wait_dscnt(simm16=0))
|
||||
e(s_barrier_signal(ssrc0=src[193])); e(s_barrier_wait(simm16=0xFFFF))
|
||||
if not NO_DS:
|
||||
for tm in range(TILES_M):
|
||||
aoff = tm * 16 * LDS_A_ROW
|
||||
e(ds_load_b128(vdst=v[FA+tm*4:FA+tm*4+3], addr=v[LLA], offset0=aoff&0xFF, offset1=aoff>>8))
|
||||
e(ds_load_b128(vdst=v[FB:FB+3], addr=v[LLB], offset0=0, offset1=0))
|
||||
e(ds_load_b128(vdst=v[FB+4:FB+7], addr=v[LLB], offset0=0, offset1=2))
|
||||
e(s_wait_dscnt(simm16=0))
|
||||
if not NO_ALU:
|
||||
if not NO_DS: e(ds_load_b128(vdst=v[FB+8:FB+11], addr=v[LLB], offset0=0, offset1=4))
|
||||
for tm in range(TILES_M):
|
||||
ac = ACC + (tm*TILES_N+0)*8
|
||||
e(v_wmma_f32_16x16x16_f16(vdst=v[ac:ac+7], src0=v[FA+tm*4:FA+tm*4+3], src1=v[FB:FB+3], src2=v[ac:ac+7]))
|
||||
if not NO_DS: e(ds_load_b128(vdst=v[FB+12:FB+15], addr=v[LLB], offset0=0, offset1=6))
|
||||
for tm in range(TILES_M):
|
||||
ac = ACC + (tm*TILES_N+1)*8
|
||||
e(v_wmma_f32_16x16x16_f16(vdst=v[ac:ac+7], src0=v[FA+tm*4:FA+tm*4+3], src1=v[FB+4:FB+7], src2=v[ac:ac+7]))
|
||||
if not NO_DS: e(s_wait_dscnt(simm16=1))
|
||||
for tm in range(TILES_M):
|
||||
ac = ACC + (tm*TILES_N+2)*8
|
||||
e(v_wmma_f32_16x16x16_f16(vdst=v[ac:ac+7], src0=v[FA+tm*4:FA+tm*4+3], src1=v[FB+8:FB+11], src2=v[ac:ac+7]))
|
||||
if not NO_DS: e(s_wait_dscnt(simm16=0))
|
||||
for tm in range(TILES_M):
|
||||
ac = ACC + (tm*TILES_N+3)*8
|
||||
e(v_wmma_f32_16x16x16_f16(vdst=v[ac:ac+7], src0=v[FA+tm*4:FA+tm*4+3], src1=v[FB+12:FB+15], src2=v[ac:ac+7]))
|
||||
|
||||
label('EPILOGUE')
|
||||
e(v_and_b32_e32(v[ET], 15, v[1]))
|
||||
e(v_lshrrev_b32_e32(v[ET+1], 4, v[1])); e(v_lshlrev_b32_e32(v[ET+1], 3, v[ET+1]))
|
||||
e(v_lshlrev_b32_e32(v[ET+2], 6, v[2])); e(v_add_nc_u32_e32(v[ET+2], s[11], v[ET+2]))
|
||||
e(v_lshlrev_b32_e32(v[ET+3], 6, v[3])); e(v_add_nc_u32_e32(v[ET+3], s[10], v[ET+3]))
|
||||
e(v_add_nc_u32_e32(v[ET+3], v[ET+3], v[ET])); e(v_mov_b32_e32(v[ET+5], 0))
|
||||
|
||||
for tm in range(TILES_M):
|
||||
for tn in range(TILES_N):
|
||||
ac = ACC + (tm*TILES_N+tn)*8; r_off, c_off = tm*16, tn*16
|
||||
e(v_add_nc_u32_e32(v[ET+6], r_off, v[ET+2])); e(v_add_nc_u32_e32(v[ET+6], v[ET+1], v[ET+6]))
|
||||
e(v_mul_lo_u32(v[ET+4], v[ET+6], s[12])); e(v_add_nc_u32_e32(v[ET+4], v[ET+4], v[ET+3]))
|
||||
if c_off: e(v_add_nc_u32_e32(v[ET+4], c_off, v[ET+4]))
|
||||
e(v_lshlrev_b32_e32(v[ET+4], 1, v[ET+4]))
|
||||
for elem in range(8):
|
||||
e(v_cvt_f16_f32_e32(v[ET+7], v[ac+elem]))
|
||||
e(global_store_b16(vaddr=v[ET+4:ET+5], vsrc=v[ET+7], saddr=s[8:9]))
|
||||
if elem < 7: e(v_add_nc_u32_e32(v[ET+4], s[13], v[ET+4]))
|
||||
|
||||
e(s_wait_storecnt(simm16=0)); e(s_sendmsg(simm16=3)); e(s_endpgm())
|
||||
|
||||
for idx, target in B:
|
||||
off = (L[target] - sum(i.size() for i in I[:idx+1])) // 4
|
||||
assert -32768 <= off <= 32767; I[idx].simm16 = off
|
||||
return I
|
||||
|
||||
N = getenv("N", 4096)
|
||||
|
||||
def test_matmul():
|
||||
dev = Device[Device.DEFAULT]
|
||||
arch = getattr(dev.renderer, 'arch', 'gfx1200')
|
||||
print(f"Device arch: {arch}")
|
||||
insts = build_kernel(N, arch)
|
||||
|
||||
rng = np.random.default_rng(42)
|
||||
a = Tensor(rng.random((N, N), dtype=np.float32).astype(np.float16))
|
||||
b = Tensor(rng.random((N, N), dtype=np.float32).astype(np.float16))
|
||||
c = Tensor.empty(N, N, dtype=dtypes.half)
|
||||
Tensor.realize(a, b, c)
|
||||
|
||||
grid, local = (N//BLOCK_N, N//BLOCK_M, 1), (THREADS, 1, 1)
|
||||
print(f"Grid: {grid}, Local: {local}")
|
||||
|
||||
dname = Device.DEFAULT
|
||||
def asm_kernel(A, B, C):
|
||||
gidxs = [UOp.special(n, f"gidx{i}") for i,n in enumerate(grid)]
|
||||
lidxs = [UOp.special(THREADS, "lidx0")]
|
||||
lds = UOp(Ops.DEFINE_LOCAL, dtypes.uint8.ptr(size=max(LDS_SIZE, 65536//getenv("LIMIT_OCC",2)), addrspace=AddrSpace.LOCAL), (), 'lds')
|
||||
sink = UOp.sink(A.base, B.base, C.base, lds, *gidxs, *lidxs,
|
||||
arg=KernelInfo(name=colored("kernel","cyan"), estimates=Estimates(ops=N*N*N*2, mem=N*N*2*3)))
|
||||
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]))))
|
||||
|
||||
c = Tensor.custom_kernel(a, b, c, fxn=asm_kernel)[2]
|
||||
ei = c.schedule()[0].lower()
|
||||
|
||||
ets = []
|
||||
with Context(DEBUG=2):
|
||||
for _ in range(getenv("CNT", 5)): ets.append(ei.run(wait=True))
|
||||
print(f"REAL TFLOPS {N*N*N*2 / min(ets) * 1e-12:.2f}")
|
||||
|
||||
if getenv("VERIFY", 1):
|
||||
GlobalCounters.reset()
|
||||
c_np = c.float().numpy()
|
||||
a_np, b_np = a.float().numpy(), b.float().numpy()
|
||||
ref = a_np @ b_np
|
||||
err = np.sqrt(np.mean((c_np - ref)**2)) / np.sqrt(np.mean(ref**2))
|
||||
print(f"relative RMSE {err:.6f}")
|
||||
if err != err or err > 0.05: raise RuntimeError(f"matmul is wrong! RMSE={err}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_matmul()
|
||||
@@ -6,7 +6,8 @@ from tinygrad.device import Device, BufferSpec
|
||||
from tinygrad.runtime.support.system import PCIDevice
|
||||
from tinygrad.runtime.support.memory import AddrSpace
|
||||
from tinygrad.runtime.ops_amd import AMDComputeQueue
|
||||
from extra.mlx_driver.mlxdev import MLXDev, MLXQP, to_be
|
||||
from tinygrad.helpers import to_be32, to_be64
|
||||
from extra.mlx_driver.mlxdev import MLXDev, MLXQP
|
||||
|
||||
BUF_SIZE = 0x1000
|
||||
MLX_PCI = getenv("MLX_PCI", "0000:41:00.0")
|
||||
@@ -49,7 +50,7 @@ rq_wqe = qp.qp_buf.view((qp.rq_head & rq_mask) * 16, 16)
|
||||
rq_wqe[:] = struct.pack('>IIQ', len(test_msg), dev.mkey, dst_paddr)
|
||||
qp.rq_head += 1
|
||||
# ring recv doorbell from CPU (DBR offset 0 = recv counter)
|
||||
dev.dbr[qp.qp_dbr // 4] = to_be('I', qp.rq_head)
|
||||
dev.dbr[qp.qp_dbr // 4] = to_be32(qp.rq_head)
|
||||
|
||||
# build send WQE in SQ from CPU (opcode 0x0a = SEND, ds_count=2)
|
||||
sq_head = qp.sq_head
|
||||
@@ -60,7 +61,7 @@ wqe[0:8] = struct.pack('>II', (sq_head << 8) | 0x0a, (qp.qp_info['qpn'] << 8) |
|
||||
wqe[11] = 0x08 # CE: signal completion
|
||||
wqe[16:32] = struct.pack('>IIQ', len(test_msg), dev.mkey, src_paddr)
|
||||
qp.sq_head += 1
|
||||
doorbell_val = to_be('Q', int.from_bytes(bytes(wqe[0:8]), 'big'))
|
||||
doorbell_val = to_be64(int.from_bytes(bytes(wqe[0:8]), 'big'))
|
||||
|
||||
# map MLX5 UAR and DBR into GPU VA
|
||||
uar_paddr = dev.pci_dev.bar_info(0)[0] + dev.uar * 0x1000
|
||||
@@ -72,7 +73,7 @@ print(f"UAR gpu_va=0x{uar_gpu_va:x} DBR gpu_va=0x{dbr_gpu_va:x}")
|
||||
q = AMDComputeQueue(gpu)
|
||||
q.wait(gpu.timeline_signal, gpu.timeline_value - 1)
|
||||
# write DBR (32-bit sq_head) - send doorbell at qp_dbr + 4
|
||||
q.release_mem(dbr_gpu_va + qp.qp_dbr + 4, to_be('I', qp.sq_head), q.pm4.data_sel__mec_release_mem__send_32_bit_low,
|
||||
q.release_mem(dbr_gpu_va + qp.qp_dbr + 4, to_be32(qp.sq_head), q.pm4.data_sel__mec_release_mem__send_32_bit_low,
|
||||
q.pm4.int_sel__mec_release_mem__none)
|
||||
# write UAR doorbell (64-bit)
|
||||
q.release_mem(uar_gpu_va + 0x800, doorbell_val, q.pm4.data_sel__mec_release_mem__send_64_bit_data,
|
||||
|
||||
@@ -10,6 +10,7 @@ EXAMPLES = {
|
||||
"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_lds_sync",
|
||||
"handwritten":"test/amd/test_custom_kernel.py TestCustomKernel.test_handwritten",
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -21,6 +22,6 @@ if __name__ == "__main__":
|
||||
for i in range(2):
|
||||
# AM_RESET=1 gets a clear trace, does not work on mi300 machines
|
||||
subprocess.run([sys.executable, *shlex.split(test)], cwd=EXAMPLES_DIR.parent.parent.parent,
|
||||
env={**os.environ, "AMD":"1", "AM_RESET":"1" if not arch.startswith("gfx9") else "0", "VIZ":"-2", "PYTHONPATH":"."})
|
||||
env={**os.environ, "DEV":"AMD", "AM_RESET":"1" if not arch.startswith("gfx9") else "0", "VIZ":"-2", "PYTHONPATH":"."})
|
||||
PROFILE_PATH.rename(dest:=EXAMPLES_DIR/arch/f"profile_{name}_run_{i}.pkl")
|
||||
print(f"saved SQTT trace to {dest}")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+1
-2
@@ -59,7 +59,6 @@ def main(args) -> None:
|
||||
events:list = viz.load_pickle(args.profile_path, default=[])
|
||||
if (profile_bytes:=viz.get_profile(events)) is None: raise RuntimeError(f"empty profile in {args.profile_path}")
|
||||
profile = decode_profile(profile_bytes)
|
||||
viz.load_amd_counters(viz.ctxs, events)
|
||||
profile["layout"].update([(f'{c["name"]} {s["name"]}', s["data"]) for c in viz.ctxs if c["name"].startswith("SQTT") for s in c["steps"]
|
||||
if "PKTS" in s["name"]])
|
||||
if args.src is None:
|
||||
@@ -88,7 +87,7 @@ def main(args) -> None:
|
||||
op_str = hex_colored(op_name, color) if color and not args.no_color else op_name
|
||||
phase, delay = None, 0
|
||||
idx = next(pkt_idxs.setdefault(e.device, itertools.count()))
|
||||
if e.device.startswith("WAVE") or e.device == "OTHER_SIMD":
|
||||
if e.device.startswith("WAVE"):
|
||||
inst = f"0x{(pc:=int(info.replace('PC:', ''))):05x} {pc_map[pc]}" if info else f"{'':7} {op_name}"
|
||||
dispatch_to_inst[f"{e.device}-{idx}"] = (inst, int(e.st))
|
||||
phase = "DISPATCH"
|
||||
|
||||
@@ -104,20 +104,21 @@ def custom_handwritten(A:UOp, arch:str) -> UOp:
|
||||
lds = UOp(Ops.DEFINE_LOCAL, dtypes.uint8.ptr(size=512, addrspace=AddrSpace.LOCAL), (), 'lds') # 128 * 4 bytes
|
||||
k = Kernel(arch)
|
||||
k.emit(r4.s_nop(0))
|
||||
k.emit(r4.v_mov_b32_e32(v[1], 10))
|
||||
k.emit(r4.v_mov_b32_e32(v[1], 4))
|
||||
def emit_alt():
|
||||
for i in range(4):
|
||||
for i in range(2):
|
||||
k.emit(r4.v_mov_b32_e32(v[20+i], 4.0))
|
||||
k.emit(r4.v_rcp_f32_e32(v[22+i], v[20+i]))
|
||||
k.emit(r4.s_mov_b32(s[20+i], i))
|
||||
k.emit(r4.s_mul_i32(s[14+i], s[12+i], 32))
|
||||
def emit_wmma():
|
||||
for _ in range(4):
|
||||
for _ in range(2):
|
||||
k.emit(r4.v_wmma_f32_16x16x16_f16(v[0:7], v[8:11], v[8:11], 1))
|
||||
k.label("start")
|
||||
k.emit(s_mov_b32(s[1], 10))
|
||||
k.label("loop")
|
||||
# wmma should've overlapped here if it was a different unit?
|
||||
for _ in range(4):
|
||||
for _ in range(2):
|
||||
emit_wmma()
|
||||
emit_alt()
|
||||
for _ in range(8): k.emit(s_nop(1))
|
||||
|
||||
@@ -100,9 +100,7 @@ class TestSQTTMapBase(unittest.TestCase):
|
||||
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",
|
||||
"WAVEEND", "WAVERDY"}: insts += 1
|
||||
# OTHER_ is its own stream, it's the INST from other SIMDs that share the same EXEC.
|
||||
elif e.device.startswith("OTHER"): continue
|
||||
"WAVEEND", "WAVERDY"} and not e.name.display_name.startswith("OTHER_"): insts += 1
|
||||
else: raise Exception(f"timeline row must be INST or EXEC, got {e.device}")
|
||||
self.assertEqual(execs, insts)
|
||||
|
||||
@@ -131,7 +129,18 @@ class TestSQTTMapBase(unittest.TestCase):
|
||||
|
||||
class TestSQTTMapRDNA3(TestSQTTMapBase): target = "gfx1100"
|
||||
|
||||
class TestSQTTMapRDNA4(TestSQTTMapBase): target = "gfx1200"
|
||||
class TestSQTTMapRDNA4(TestSQTTMapBase):
|
||||
target = "gfx1200"
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_rdna4_wmma(self):
|
||||
events, kernels, target = self.examples["profile_handwritten_run_0"]
|
||||
row_ends = {}
|
||||
for e in sqtt_timeline(events[0].blob, list(kernels.values())[0].lib, target):
|
||||
if type(e).__name__ != "ProfileRangeEvent" or e.device != "ALUEXEC:0 WMMA": continue
|
||||
if (et:=row_ends.get(e.device)) is not None and e.st < et:
|
||||
raise RuntimeError(f"WMMA exec overlaps in {e.device}: {e.st} {et}.")
|
||||
row_ends[e.device] = e.en
|
||||
|
||||
class TestSQTTMapCDNA(TestSQTTMapBase):
|
||||
target = "gfx950"
|
||||
|
||||
@@ -21,9 +21,8 @@ def run_asm_gemm(a_shape, b_shape, dtype=dtypes.float16, a_shard=None, b_shard=N
|
||||
|
||||
a, b = a_rand.clone().requires_grad_(), b_rand.clone().requires_grad_()
|
||||
if multi: a, b = a.shard(devs, axis=a_shard), b.shard(devs, axis=b_shard)
|
||||
with Context(ASM_GEMM=1):
|
||||
tst = asm_gemm(a, b)
|
||||
tst.sum().backward()
|
||||
tst = asm_gemm(a, b)
|
||||
tst.sum().backward()
|
||||
Tensor.realize(tst, a.grad, b.grad)
|
||||
|
||||
a_ref, b_ref = a_rand.clone().requires_grad_(), b_rand.clone().requires_grad_()
|
||||
@@ -32,9 +31,8 @@ def run_asm_gemm(a_shape, b_shape, dtype=dtypes.float16, a_shard=None, b_shard=N
|
||||
a_ref = a_ref.cast(dtypes.bfloat16)
|
||||
b_ref = b_ref.cast(dtypes.bfloat16)
|
||||
if multi: a_ref, b_ref = a_ref.shard(devs, axis=a_shard), b_ref.shard(devs, axis=b_shard)
|
||||
with Context(ASM_GEMM=0):
|
||||
ref = asm_gemm(a_ref, b_ref)
|
||||
ref.sum().backward()
|
||||
ref = a_ref @ b_ref
|
||||
ref.sum().backward()
|
||||
Tensor.realize(ref, a_ref.grad, b_ref.grad)
|
||||
|
||||
# no validation on the NULL device
|
||||
@@ -136,14 +134,12 @@ class TestGemmLlama(unittest.TestCase):
|
||||
if not is_cdna4() or getenv("MOCKGPU"):
|
||||
self.skipTest("very slow on non mi350x")
|
||||
|
||||
@Context(ASM_GEMM=1)
|
||||
def test_empty(self): (Tensor.empty(N:=getenv("N", 4096), N, dtype=self.dtype)@Tensor.empty(N, N, dtype=self.dtype)).realize()
|
||||
def test_empty(self): asm_gemm(Tensor.empty(N:=getenv("N", 4096), N, dtype=self.dtype), Tensor.empty(N, N, dtype=self.dtype)).realize()
|
||||
|
||||
@Context(ASM_GEMM=1)
|
||||
def test_empty_bw(self):
|
||||
x = Tensor.empty(1, N:=getenv("N", 4096), N, dtype=self.dtype, requires_grad=True)
|
||||
y = Tensor.empty((N, N), dtype=self.dtype, requires_grad=True)
|
||||
z = x @ y
|
||||
z = asm_gemm(x, y)
|
||||
z.sum().backward()
|
||||
Tensor.realize(z, x.grad, y.grad)
|
||||
# FP8 forward output is bf16, gradients use fp8e5m2 (aka bf8)
|
||||
|
||||
Vendored
+1
-1
@@ -8,7 +8,7 @@ LABEL = ["A", "B", "C", "D"]
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--port", "-p", type=int, default=11434)
|
||||
parser.add_argument("--port", "-p", type=int, default=8000)
|
||||
parser.add_argument("--limit", "-L", type=int, default=None)
|
||||
parser.add_argument("--max_tokens", "-T", type=int, default=4096)
|
||||
parser.add_argument("--offset", "-O", type=int, default=0)
|
||||
|
||||
@@ -470,7 +470,7 @@ class TestVizProfiler(unittest.TestCase):
|
||||
j = load_profile(prof)
|
||||
event = j['layout']['NV:SDMA:0']['events'][0]
|
||||
gbs = sz/(dur*1e-6)*1e-9
|
||||
self.assertEqual(event['fmt'], f"{gbs:.0f} GB/s")
|
||||
self.assertEqual(event['fmt'], f"{gbs:.0f} GB/s\n{sz/1e6:.0f} MB")
|
||||
|
||||
def test_graph(self):
|
||||
prof = [ProfileDeviceEvent(device='NV', tdiff=decimal.Decimal(-1000)),
|
||||
@@ -512,7 +512,7 @@ class TestVizProfiler(unittest.TestCase):
|
||||
j = load_profile(prof)
|
||||
sdma_events = j['layout']['NV:1:SDMA:0']['events']
|
||||
gbs = sz/(dur*1e-6)*1e-9
|
||||
self.assertEqual(sdma_events[0]['fmt'], f"{gbs:.0f} GB/s")
|
||||
self.assertEqual(sdma_events[0]["fmt"], f"{gbs:.0f} GB/s\n{sz/1e6:.0f} MB")
|
||||
|
||||
def test_block_ordering(self):
|
||||
prof = [ProfileDeviceEvent(device='NV', tdiff=decimal.Decimal(-1000)),
|
||||
|
||||
+22
-11
@@ -30,11 +30,15 @@ class TestGGUF(unittest.TestCase):
|
||||
|
||||
def test_dequantization_q4_0(self): self._test_dequantization(GGMLQuantizationType.Q4_0)
|
||||
def test_dequantization_q4_1(self): self._test_dequantization(GGMLQuantizationType.Q4_1)
|
||||
def test_dequantization_q5_0(self): self._test_dequantization(GGMLQuantizationType.Q5_0)
|
||||
def test_dequantization_q5_1(self): self._test_dequantization(GGMLQuantizationType.Q5_1)
|
||||
def test_dequantization_q8_0(self): self._test_dequantization(GGMLQuantizationType.Q8_0)
|
||||
def test_dequantization_q4_k(self): self._test_dequantization(GGMLQuantizationType.Q4_K)
|
||||
def test_dequantization_q5_k(self): self._test_dequantization(GGMLQuantizationType.Q5_K)
|
||||
def test_dequantization_q6_k(self): self._test_dequantization(GGMLQuantizationType.Q6_K)
|
||||
def test_dequantization_mxfp4(self): self._test_dequantization(GGMLQuantizationType.MXFP4)
|
||||
@unittest.skipUnless(is_dtype_supported(dtypes.bfloat16), "Backend must support bfloat16")
|
||||
def test_dequantization_bf16(self): self._test_dequantization(GGMLQuantizationType.BF16)
|
||||
def test_dequantization_mxfp4_old(self):
|
||||
def encode(nibbles, E):
|
||||
packed = [(low & 0xF) | ((high & 0xF) << 4) for low, high in zip(nibbles[:16], nibbles[16:])]
|
||||
@@ -120,17 +124,21 @@ class TestGGUF(unittest.TestCase):
|
||||
class TestGGUFGEMV(unittest.TestCase):
|
||||
def _test_gguf_gemv(self, qtype: GGMLQuantizationType):
|
||||
block_size, type_size = GGML_QUANT_SIZES[qtype]
|
||||
rows, cols = 8192, 2048
|
||||
rows, cols = (1024, 512) if qtype == GGMLQuantizationType.BF16 else (8192, 2048)
|
||||
n_blocks = rows * cols // block_size
|
||||
rng = np.random.default_rng(42)
|
||||
# generate random quantized blocks with valid fp16 scale fields (random bytes can produce NaN scales)
|
||||
q_data = rng.integers(0, 256, size=n_blocks * type_size, dtype=np.uint8).reshape(n_blocks, type_size)
|
||||
scales = np.float16(rng.standard_normal(n_blocks * 4)).view(np.uint8).reshape(n_blocks, -1)
|
||||
if qtype == GGMLQuantizationType.Q8_0: q_data[:, :2] = scales[:, :2] # d at offset 0
|
||||
elif qtype in (GGMLQuantizationType.Q4_K, GGMLQuantizationType.Q5_K): q_data[:, :4] = scales[:, :4] # d, dmin at offset 0
|
||||
elif qtype == GGMLQuantizationType.Q6_K: q_data[:, -2:] = scales[:, :2] # d at end
|
||||
elif qtype == GGMLQuantizationType.MXFP4: q_data[:, 0] = rng.integers(120, 136, size=n_blocks, dtype=np.uint8) # constrain byte0
|
||||
q_data = q_data.flatten()
|
||||
if qtype == GGMLQuantizationType.BF16:
|
||||
q_data = (rng.standard_normal(rows * cols).astype(np.float32).view(np.uint32) >> 16).astype(np.uint16).view(np.uint8)
|
||||
else:
|
||||
# generate random quantized blocks with valid fp16 scale fields (random bytes can produce NaN scales)
|
||||
q_data = rng.integers(0, 256, size=n_blocks * type_size, dtype=np.uint8).reshape(n_blocks, type_size)
|
||||
scales = np.float16(rng.standard_normal(n_blocks * 4)).view(np.uint8).reshape(n_blocks, -1)
|
||||
if qtype in (GGMLQuantizationType.Q5_0, GGMLQuantizationType.Q8_0): q_data[:, :2] = scales[:, :2] # d at offset 0
|
||||
elif qtype in (GGMLQuantizationType.Q5_1, GGMLQuantizationType.Q4_K, GGMLQuantizationType.Q5_K):
|
||||
q_data[:, :4] = scales[:, :4] # d, m/dmin at offset 0
|
||||
elif qtype == GGMLQuantizationType.Q6_K: q_data[:, -2:] = scales[:, :2] # d at end
|
||||
elif qtype == GGMLQuantizationType.MXFP4: q_data[:, 0] = rng.integers(120, 136, size=n_blocks, dtype=np.uint8) # constrain byte0
|
||||
q_data = q_data.flatten()
|
||||
ref = dequantize(q_data, qtype).reshape(rows, cols)
|
||||
|
||||
# build a minimal gguf in memory: header + 1 tensor info + aligned data
|
||||
@@ -148,15 +156,18 @@ class TestGGUFGEMV(unittest.TestCase):
|
||||
|
||||
x = rng.standard_normal(cols).astype(np.float32)
|
||||
np.testing.assert_allclose((tensors["weight"] @ Tensor(x)).numpy(), ref @ x, atol=1e-2, rtol=1e-2)
|
||||
# can only expect the weights to be identical if we really support float16 (ie. not decompositions)
|
||||
if is_dtype_supported(dtypes.half): np.testing.assert_equal(tensors["weight"].numpy(), ref)
|
||||
if qtype == GGMLQuantizationType.BF16 or is_dtype_supported(dtypes.half): np.testing.assert_equal(tensors["weight"].numpy(), ref)
|
||||
assert np.isfinite(ref).all() and np.isfinite(tensors["weight"].numpy()).all(), f"{qtype.name} has NaN/Inf"
|
||||
|
||||
def test_gguf_gemv_q8_0(self): self._test_gguf_gemv(GGMLQuantizationType.Q8_0)
|
||||
def test_gguf_gemv_q5_0(self): self._test_gguf_gemv(GGMLQuantizationType.Q5_0)
|
||||
def test_gguf_gemv_q5_1(self): self._test_gguf_gemv(GGMLQuantizationType.Q5_1)
|
||||
def test_gguf_gemv_q4_k(self): self._test_gguf_gemv(GGMLQuantizationType.Q4_K)
|
||||
def test_gguf_gemv_q5_k(self): self._test_gguf_gemv(GGMLQuantizationType.Q5_K)
|
||||
def test_gguf_gemv_q6_k(self): self._test_gguf_gemv(GGMLQuantizationType.Q6_K)
|
||||
def test_gguf_gemv_mxfp4(self): self._test_gguf_gemv(GGMLQuantizationType.MXFP4)
|
||||
@unittest.skipUnless(is_dtype_supported(dtypes.bfloat16), "Backend must support bfloat16")
|
||||
def test_gguf_gemv_bf16(self): self._test_gguf_gemv(GGMLQuantizationType.BF16)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -287,8 +287,7 @@ models = {
|
||||
"olmoe": "https://huggingface.co/allenai/OLMoE-1B-7B-0924-Instruct-GGUF/resolve/main/olmoe-1b-7b-0924-instruct-q4_k_m.gguf",
|
||||
}
|
||||
|
||||
# *** simple OpenAI compatible server on 11434 to match ollama ***
|
||||
# OPENAI_BASE_URL=http://localhost:11434/v1 OPENAI_API_KEY=ollama uvx --from gpt-command-line gpt
|
||||
# *** simple OpenAI API compatible server with web interface on http://localhost:8000/ ***
|
||||
|
||||
CHAT_HTML = b'''<!DOCTYPE html><html><head><title>tinygrad chat</title><style>
|
||||
* { margin: 0 }
|
||||
@@ -313,10 +312,14 @@ CHAT_HTML = b'''<!DOCTYPE html><html><head><title>tinygrad chat</title><style>
|
||||
const d = document.createElement('div'); d.className = 'msg'; chat.appendChild(d);
|
||||
const r = await fetch('/v1/chat/completions', {method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({model: 'llama', messages: msgs, stream: true, temperature: 0.7})});
|
||||
let buf = '';
|
||||
for (const rd = r.body.getReader(), dec = new TextDecoder();;) {
|
||||
const {done, value} = await rd.read();
|
||||
if (done) break;
|
||||
for (const ln of dec.decode(value).split('\\n'))
|
||||
buf += dec.decode(value, {stream: true});
|
||||
const lines = buf.split('\\n');
|
||||
buf = lines.pop();
|
||||
for (const ln of lines)
|
||||
if (ln.startsWith('data: ') && !ln.includes('[DONE]'))
|
||||
try { d.textContent += JSON.parse(ln.slice(6)).choices[0]?.delta?.content || '' } catch {}
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
@@ -393,7 +396,7 @@ if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--model", "-m", default=list(models.keys())[0], help=f"Model choice ({', '.join(models.keys())}) or path to a local GGUF file")
|
||||
parser.add_argument("--max_context", type=int, default=4096, help="Max Context Length")
|
||||
parser.add_argument("--serve", nargs='?', type=int, const=11434, metavar="PORT", help="Run OpenAI compatible API (optional port, default 11434)")
|
||||
parser.add_argument("--serve", nargs='?', type=int, const=8000, metavar="PORT", help="Run OpenAI compatible API (optional port, default 8000)")
|
||||
parser.add_argument("--warmup", action="store_true", help="warmup the JIT")
|
||||
parser.add_argument("--benchmark", nargs='?', type=int, const=20, metavar="COUNT", help="Benchmark tok/s (optional count, default 20)")
|
||||
args = parser.parse_args()
|
||||
|
||||
+22
-16
@@ -9,6 +9,27 @@ from tinygrad.device import Device, Buffer
|
||||
from tinygrad.renderer import ProgramSpec, Estimates
|
||||
from tinygrad.codegen import get_program
|
||||
|
||||
# **************** Stat ****************
|
||||
|
||||
def update_stats(display_name:str, device:str, estimates:Estimates, var_vals:dict[str, int], et:float|None, buf_count:int,
|
||||
jit=False, metadata:tuple[Metadata, ...]=(), first_run=False):
|
||||
GlobalCounters.kernel_count += 1
|
||||
GlobalCounters.global_ops += (op_est:=sym_infer(estimates.ops, var_vals))
|
||||
GlobalCounters.global_mem += (mem_est:=sym_infer(estimates.mem, var_vals))
|
||||
if et is not None: GlobalCounters.time_sum_s += et
|
||||
if DEBUG >= 2:
|
||||
lds_est = sym_infer(estimates.lds, var_vals)
|
||||
header_color = 'magenta' if jit else ('green' if first_run else None)
|
||||
ptm = colored(time_to_str(et, w=9), "yellow" if et > 0.01 else None) if et is not None else ""
|
||||
flops, membw, ldsbw = op_est/(et or 1e-20), mem_est/(et or 1e-20), lds_est/(et or 1e-20)
|
||||
flops_str = f"{flops*1e-9:7.0f} GFLOPS" if flops < 1e14 else colored(f"{flops*1e-12:7.0f} TFLOPS", 'green')
|
||||
mem_str = f"{membw*1e-9:4.0f}|{ldsbw*1e-9:<6.0f} GB/s" if membw < 1e13 and ldsbw < 1e15 else \
|
||||
colored(f"{membw*1e-12:4.0f}|{ldsbw*1e-12:<6.0f} TB/s", 'green')
|
||||
print(f"{colored(f'*** {device[:7]:7s} {GlobalCounters.kernel_count:4d}', header_color)}"+
|
||||
f" {display_name+' '*(46-ansilen(display_name))} arg {buf_count:2d} mem {GlobalCounters.mem_used/1e9:6.2f} GB"+
|
||||
("" if et is None else f" tm {ptm}/{GlobalCounters.time_sum_s*1e3:9.2f}ms ({flops_str} {mem_str})")+
|
||||
f" {[repr(m) if TRACEMETA >= 2 else str(m) for m in metadata] if metadata else ''}")
|
||||
|
||||
# **************** Runners ****************
|
||||
|
||||
class Runner:
|
||||
@@ -166,22 +187,7 @@ class ExecItem:
|
||||
cpu_events.append(ProfilePointEvent(self.prg.device, "exec", len(cpu_events), payload))
|
||||
et = self.prg(bufs, var_vals, wait=wait or DEBUG >= 2)
|
||||
if do_update_stats:
|
||||
GlobalCounters.kernel_count += 1
|
||||
GlobalCounters.global_ops += (op_est:=sym_infer(self.prg.estimates.ops, var_vals))
|
||||
GlobalCounters.global_mem += (mem_est:=sym_infer(self.prg.estimates.mem, var_vals))
|
||||
if et is not None: GlobalCounters.time_sum_s += et
|
||||
if DEBUG >= 2:
|
||||
lds_est = sym_infer(self.prg.estimates.lds, var_vals)
|
||||
header_color = 'magenta' if jit else ('green' if self.prg.first_run else None)
|
||||
ptm = colored(time_to_str(et, w=9), "yellow" if et > 0.01 else None) if et is not None else ""
|
||||
flops, membw, ldsbw = op_est/(et or 1e-20), mem_est/(et or 1e-20), lds_est/(et or 1e-20)
|
||||
flops_str = f"{flops*1e-9:7.0f} GFLOPS" if flops < 1e14 else colored(f"{flops*1e-12:7.0f} TFLOPS", 'green')
|
||||
mem_str = f"{membw*1e-9:4.0f}|{ldsbw*1e-9:<6.0f} GB/s" if membw < 1e13 and ldsbw < 1e15 else \
|
||||
colored(f"{membw*1e-12:4.0f}|{ldsbw*1e-12:<6.0f} TB/s", 'green')
|
||||
print(f"{colored(f'*** {self.prg.device[:7]:7s} {GlobalCounters.kernel_count:4d}', header_color)}"+
|
||||
f" {self.prg.display_name+' '*(46-ansilen(self.prg.display_name))} arg {len(bufs):2d} mem {GlobalCounters.mem_used/1e9:6.2f} GB"+
|
||||
("" if et is None else f" tm {ptm}/{GlobalCounters.time_sum_s*1e3:9.2f}ms ({flops_str} {mem_str})")+
|
||||
f" {[repr(m) if TRACEMETA >= 2 else str(m) for m in self.metadata] if self.metadata else ''}")
|
||||
update_stats(self.prg.display_name, self.prg.device, self.prg.estimates, var_vals, et, len(bufs), jit, self.metadata, self.prg.first_run)
|
||||
self.prg.first_run = False
|
||||
return et
|
||||
|
||||
|
||||
+2
-2
@@ -61,6 +61,8 @@ def lo32(x:Any) -> Any: return x & 0xFFFFFFFF # Any is sint
|
||||
def hi32(x:Any) -> Any: return x >> 32 # Any is sint
|
||||
def data64(data:Any) -> tuple[Any, Any]: return (data >> 32, data & 0xFFFFFFFF) # Any is sint
|
||||
def data64_le(data:Any) -> tuple[Any, Any]: return (data & 0xFFFFFFFF, data >> 32) # Any is sint
|
||||
def to_be32(val:Any) -> Any: return ((val & 0xFF) << 24) | (((val >> 8) & 0xFF) << 16) | (((val >> 16) & 0xFF) << 8) | ((val >> 24) & 0xFF)
|
||||
def to_be64(val:Any) -> Any: return to_be32(val >> 32) | (to_be32(val & 0xFFFFFFFF) << 32)
|
||||
def getbits(value: int, start: int, end: int): return (value >> start) & ((1 << (end - start + 1)) - 1)
|
||||
def i2u(bits: int, value: int): return value if value >= 0 else (1<<bits)+value
|
||||
def is_numpy_ndarray(x) -> bool: return str(type(x)) == "<class 'numpy.ndarray'>"
|
||||
@@ -249,8 +251,6 @@ ALLOW_TF32 = ContextVar("ALLOW_TF32", 0)
|
||||
SCACHE = ContextVar("SCACHE", 1)
|
||||
# allow use of atomics for embedding backward
|
||||
USE_ATOMICS = ContextVar("USE_ATOMICS", 0)
|
||||
# allow use of assembly for gemm
|
||||
ASM_GEMM = ContextVar("ASM_GEMM", 0)
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Metadata:
|
||||
|
||||
@@ -974,3 +974,16 @@ class ElementwiseMixin(DTypeMixin, CreationMixin):
|
||||
"""
|
||||
if self.dtype != dtypes.bool and not dtypes.is_int(self.dtype): raise RuntimeError(f"{self.dtype} is not supported")
|
||||
return self.logical_not() if self.dtype == dtypes.bool else self ^ -1
|
||||
|
||||
def lerp(self, end: Self, weight: Self | ConstType) -> Self:
|
||||
"""
|
||||
Linearly interpolates between `self` and `end` by `weight`.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor([1., 2., 3.]).lerp(Tensor([4., 5., 6.]), 0.5).numpy())
|
||||
```
|
||||
"""
|
||||
if self.dtype == dtypes.uint8 and isinstance(weight, ElementwiseMixin):
|
||||
w_i = (weight * (1<<(W_PREC:=7)) + 0.5).cast(dtypes.int16)
|
||||
return (self+(((end - self).cast(dtypes.int8) * w_i + (1<<W_PREC-1)).cast(dtypes.uint16) >> W_PREC)).cast(dtypes.uint8)
|
||||
return self + (end - self) * weight
|
||||
|
||||
@@ -107,6 +107,12 @@ class MovementMixin:
|
||||
ret = self._mop(Ops.RESHAPE, arg=new_shape)
|
||||
return self if ret.shape == self.shape else ret
|
||||
|
||||
def pad(self, arg:tuple[tuple[sint, sint] | None, ...]) -> Self:
|
||||
if self.ndim != len(arg):
|
||||
raise ValueError(f"{self.ndim=} != {len(arg)=}")
|
||||
ret = self._mop(Ops.PAD, tuple(x if x is not None else (0, 0) for x in arg))
|
||||
return self if ret.shape == self.shape else ret
|
||||
|
||||
def shrink(self, arg: tuple[tuple[sint, sint] | None, ...]) -> Self:
|
||||
"""
|
||||
Returns a tensor that shrinks the each axis based on input arg.
|
||||
|
||||
+19
-4
@@ -297,13 +297,19 @@ def ggml_data_to_tensor(t: Tensor, n: int, ggml_type: int) -> Tensor:
|
||||
"""
|
||||
Converts ggml tensor data to a tinygrad tensor.
|
||||
|
||||
Supported native types: float32 (id: 0), float16 (id: 1), int8 (id: 16), int16 (id: 17), int32 (id: 18)
|
||||
Supported quantized types: Q4_0 (id: 2), Q4_1 (id: 3), Q8_0 (id: 8), Q4_K (id: 12), Q5_K (id: 13), Q6_K (id: 14), MXFP4 (id: 39)
|
||||
Supported native types: float32 (id: 0), float16 (id: 1), bfloat16 (id: 30),
|
||||
int8 (id: 16), int16 (id: 17), int32 (id: 18)
|
||||
Supported quantized types: Q4_0 (id: 2), Q4_1 (id: 3), Q5_0 (id: 6),
|
||||
Q5_1 (id: 7), Q8_0 (id: 8), Q4_K (id: 12), Q5_K (id: 13),
|
||||
Q6_K (id: 14), MXFP4 (id: 39)
|
||||
"""
|
||||
# https://github.com/ggerganov/ggml/blob/323951f1bdcdfbd5b5ff3a9a7c3770e63b1a560e/include/ggml.h#L356
|
||||
|
||||
# native types
|
||||
if (dtype := { 0: dtypes.float32, 1: dtypes.float16, 16: dtypes.int8, 17: dtypes.int16, 18: dtypes.int32 }.get(ggml_type)) is not None:
|
||||
if (dtype := {
|
||||
0: dtypes.float32, 1: dtypes.float16, 16: dtypes.int8,
|
||||
17: dtypes.int16, 18: dtypes.int32, 30: dtypes.bfloat16,
|
||||
}.get(ggml_type)) is not None:
|
||||
return t[:dtype.itemsize * n].contiguous().bitcast(dtype)
|
||||
|
||||
def q_to_uint8(t: Tensor, b: int) -> Tensor:
|
||||
@@ -312,12 +318,21 @@ def ggml_data_to_tensor(t: Tensor, n: int, ggml_type: int) -> Tensor:
|
||||
return t.unsqueeze(-1).expand((*t.shape,8//b)).idiv(shift_tensor).bitwise_and(bitmask).transpose(-1, -2).flatten(-2)
|
||||
|
||||
# map to (number of elements, number of bytes)
|
||||
if (nelements_nbytes := { 2:(32,18), 3:(32,20), 8:(32,34), 12:(256,144), 13:(256,176), 14:(256,210), 39:(32,17) }.get(ggml_type)) is not None:
|
||||
if (nelements_nbytes := {
|
||||
2:(32,18), 3:(32,20), 6:(32,22), 7:(32,24), 8:(32,34),
|
||||
12:(256,144), 13:(256,176), 14:(256,210), 39:(32,17),
|
||||
}.get(ggml_type)) is not None:
|
||||
blocks = t[:(n//nelements_nbytes[0])*nelements_nbytes[1]].reshape((-1, nelements_nbytes[1])).contiguous()
|
||||
if ggml_type == 2: return (q_to_uint8(blocks[:,2:], 4).bitcast(dtypes.int8) - 8) * blocks[:,:2].bitcast(dtypes.float16).cast(dtypes.float32)
|
||||
if ggml_type == 3:
|
||||
d, m = (blocks[:,s:s+2].bitcast(dtypes.float16).cast(dtypes.float32) for s in [ 0, 2 ])
|
||||
return q_to_uint8(blocks[:,4:], 4).bitcast(dtypes.int8) * d + m
|
||||
if ggml_type in (6, 7):
|
||||
d = blocks[:,:2].bitcast(dtypes.float16).cast(dtypes.float32)
|
||||
qh_off = 2 if ggml_type == 6 else 4
|
||||
qh = q_to_uint8(blocks[:,qh_off:qh_off+4], 1).reshape((-1, 8, 4)).transpose(-1, -2).flatten(-2).bitcast(dtypes.int8)
|
||||
q = q_to_uint8(blocks[:,qh_off+4:], 4).bitcast(dtypes.int8) + qh * 16
|
||||
return q * d + (blocks[:,2:4].bitcast(dtypes.float16).cast(dtypes.float32) if ggml_type == 7 else -16 * d)
|
||||
if ggml_type == 8: return blocks[:,:2].bitcast(dtypes.float16).cast(dtypes.float32) * blocks[:,2:].bitcast(dtypes.int8)
|
||||
# Q4_K: 256 elements per 144-byte block (d:2, dmin:2, scales:12, qs:128)
|
||||
# Q5_K: 256 elements per 176-byte block (d:2, dmin:2, scales:12, qh:32, qs:128)
|
||||
|
||||
+115
-43
@@ -7,6 +7,7 @@ from tinygrad.dtype import dtypes
|
||||
from tinygrad.uop.ops import UOp, Ops, Variable
|
||||
from tinygrad.engine.realize import BufferXfer, CompiledRunner, BufferCopy
|
||||
from tinygrad.engine.jit import GraphRunner, MultiGraphRunner
|
||||
from tinygrad.runtime.ops_rdma import RDMACopyQueue
|
||||
|
||||
class HCQGraph(MultiGraphRunner):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -50,10 +51,21 @@ class HCQGraph(MultiGraphRunner):
|
||||
|
||||
self.comp_queues: dict[HCQCompiled, HWQueue] = {dev: unwrap(dev.hw_compute_queue_t)() for dev in self.devices}
|
||||
self.copy_queues: dict[tuple[HCQCompiled, int], HWQueue] = {} # lazy allocation, keyed by (device, queue_idx)
|
||||
self.rdma_queues: dict[tuple[HCQCompiled, HCQCompiled], RDMACopyQueue] = {} # lazy allocation, keyed by device pair
|
||||
self.num_copy_queues: int = getenv("HCQ_NUM_SDMA", min(len(self.devices), 8) if ALL2ALL >= 1 else 1)
|
||||
self.num_rdma_ops: dict[tuple[HCQCompiled, HCQCompiled], int] = collections.defaultdict(int)
|
||||
|
||||
self.rdma_vars: dict[tuple[HCQCompiled, HCQCompiled], tuple[Variable, Any]] = {} # value is variable and src_qp
|
||||
self.rdma_deps: dict[int, tuple[HWQueue, list[tuple[HCQSignal, int]], HCQSignal, int]] = {}
|
||||
self.rdma_last_dest: dict[int, tuple[HWQueue, int]] = {} # per QP id: last (queue, signal_value) for dbell ordering
|
||||
|
||||
# Per-peer-group representative device for signal allocation. For cpu, use devices[0].
|
||||
self.pg_dev: dict[Any, HCQCompiled] = {dev.peer_group: self.devices[0] for dev in self.devices if dev._is_cpu()} \
|
||||
| {dev.peer_group: dev for dev in self.devices if not dev._is_cpu()}
|
||||
|
||||
self.kick_signals: dict[Any, HCQSignal] = {pg: pg_dev.new_signal(value=0) for pg, pg_dev in self.pg_dev.items()}
|
||||
self.signals: dict[Any, HCQSignal] = {**{dev: dev.new_signal(value=0) for dev in self.devices if not dev._is_cpu()},
|
||||
**{"KICK": self.devices[0].new_signal(value=0)}, **{dev: self.devices[0].new_signal(value=0) for dev in self.devices if dev._is_cpu()}}
|
||||
**{dev: self.pg_dev[dev.peer_group].new_signal(value=0) for dev in self.devices if dev._is_cpu()}}
|
||||
self.kickoff_value: int = 0
|
||||
self.kickoff_var = UOp.variable("kickoff_var", 0, 0xffffffff, dtype=dtypes.uint32)
|
||||
|
||||
@@ -63,19 +75,22 @@ class HCQGraph(MultiGraphRunner):
|
||||
self.prof_graph_deps: list[list[int]] = []
|
||||
self.prof_graph_entries: list[ProfileGraphEntry] = []
|
||||
|
||||
last_j: dict[HWQueue, int|None] = collections.defaultdict(lambda: None)
|
||||
queue_access: dict[HWQueue, dict[HWQueue, int|None]] = collections.defaultdict(lambda: collections.defaultdict(lambda: None))
|
||||
dev_access: dict[HWQueue, set[HCQCompiled]] = collections.defaultdict(set)
|
||||
self.last_j: dict[HWQueue, int|None] = collections.defaultdict(lambda: None)
|
||||
self.queue_access: dict[HWQueue, dict[HWQueue, int|None]] = collections.defaultdict(lambda: collections.defaultdict(lambda: None))
|
||||
self.dev_access: dict[HWQueue, set[HCQCompiled]] = collections.defaultdict(set)
|
||||
|
||||
for dev, queue in self.comp_queues.items(): dev_access[queue].add(dev)
|
||||
for dev, queue in self.comp_queues.items(): self.dev_access[queue].add(dev)
|
||||
|
||||
self.input_replace_map: dict[HCQCompiled, set[int]] = collections.defaultdict(set)
|
||||
self.device_vars: dict[HCQCompiled, dict[str, int]] = {}
|
||||
|
||||
for j,ji in enumerate(self.jit_cache):
|
||||
ji_devs = [cast(HCQCompiled, Device[cast(Buffer, b).device]) for b in ji.bufs] if isinstance(ji.prg, BufferXfer) else []
|
||||
is_rdma = len(ji_devs) > 0 and not any(d._is_cpu() for d in ji_devs) and len(set(d.peer_group for d in ji_devs)) > 1
|
||||
|
||||
if is_exec_prg:=isinstance(ji.prg, CompiledRunner): enqueue_dev: HCQCompiled = ji.prg.dev
|
||||
else:
|
||||
# For copy ops prioritize enqeueuing on the dest device, so reverse the buffers.
|
||||
# For copy ops prioritize enqeueuing on the src device, so reverse the buffers.
|
||||
for b in cast(list[Buffer], ji.bufs[::-1]):
|
||||
if (enqueue_dev:=cast(HCQCompiled, Device[b.device])).hw_copy_queue_t is not None: break
|
||||
|
||||
@@ -85,47 +100,39 @@ class HCQGraph(MultiGraphRunner):
|
||||
|
||||
if is_exec_prg:
|
||||
enqueue_queue = self.comp_queues[enqueue_dev]
|
||||
elif is_rdma:
|
||||
enqueue_queue = self.comp_queues[enqueue_dev]
|
||||
rdma_key = (cast(HCQCompiled, Device[cast(Buffer, ji.bufs[0]).device]).rdma_dev(), enqueue_dev.rdma_dev())
|
||||
self.rdma_queues.setdefault(rdma_key, RDMACopyQueue(enqueue_dev.rdma_dev()))
|
||||
else:
|
||||
assert (enqueue_dev.hw_copy_queue_t is not None), "device must implement a copy queue"
|
||||
queue_idx = self.devices.index(cast(HCQCompiled, Device[cast(Buffer, ji.bufs[0]).device])) % self.num_copy_queues
|
||||
enqueue_queue = self.copy_queues.setdefault((enqueue_dev, queue_idx),
|
||||
enqueue_dev.hw_copy_queue_t(queue_idx=queue_idx).wait(self.signals['KICK'], self.kickoff_var))
|
||||
enqueue_dev.hw_copy_queue_t(queue_idx=queue_idx).wait(self.kick_signals[enqueue_dev.peer_group], self.kickoff_var))
|
||||
|
||||
out_signal = self.signals.setdefault(enqueue_queue, self.devices[0].new_signal(value=0))
|
||||
out_signal = self.signals.setdefault(enqueue_queue, self.pg_dev[enqueue_dev.peer_group].new_signal(value=0))
|
||||
|
||||
# Get dependencies based on input and output buffers.
|
||||
rdeps = self._access_resources(ji.bufs, ji.prg.p.outs if is_exec_prg else [0], (enqueue_queue, j + 1)) #type:ignore
|
||||
if is_rdma:
|
||||
src_qp, dest_qp = rdma_key[1].iface.connect(rdma_key[0])[:2]
|
||||
sync_signals, opt_deps, rdeps = self._resolve_deps(ji.bufs[1:], [], enqueue_queue, enqueue_dev, out_signal, j,
|
||||
is_copy=isinstance(ji.prg, BufferXfer), rdma_qp=src_qp)
|
||||
peer_queue = self.comp_queues[peer_dev:=cast(HCQCompiled, Device[cast(Buffer, ji.bufs[0]).device])]
|
||||
peer_out_signal = self.signals.setdefault(peer_queue, self.pg_dev[peer_dev.peer_group].new_signal(value=0))
|
||||
peer_sync_signals, peer_opt_deps, peer_rdeps = self._resolve_deps(ji.bufs[:1], [0], peer_queue, peer_dev, peer_out_signal, j,
|
||||
is_copy=isinstance(ji.prg, BufferXfer), rdma_qp=dest_qp)
|
||||
self.rdma_deps[j] = (peer_queue, peer_sync_signals + peer_opt_deps, peer_out_signal, j + 1)
|
||||
self.last_j[peer_queue] = j
|
||||
else:
|
||||
sync_signals, opt_deps, rdeps = self._resolve_deps(ji.bufs, cast(CompiledRunner, ji.prg).p.outs if is_exec_prg else [0], enqueue_queue,
|
||||
enqueue_dev, out_signal, j, is_copy=isinstance(ji.prg, BufferXfer))
|
||||
|
||||
# Update dependencies to include previous kernel in queue. This is required for timeline signals.
|
||||
opt_deps, deps = [], rdeps + ([(enqueue_queue, prev_ji + 1)] if (prev_ji:=last_j[enqueue_queue]) is not None else [])
|
||||
|
||||
# Optimize dependencies by removing redundant ones. Remove waiting for the value of the queue which is known to be already
|
||||
# synced with the current queue.
|
||||
for dep_queue, dep_val in sorted(deps, key=lambda x: x[1], reverse=True):
|
||||
if (qa:=queue_access[enqueue_queue][dep_queue]) is None or qa < dep_val:
|
||||
opt_deps.append((self.signals[dep_queue], dep_val))
|
||||
queue_access[enqueue_queue][dep_queue] = dep_val
|
||||
dev_access[enqueue_queue].update(dev_access[dep_queue])
|
||||
|
||||
# Ensure device is ready for use in current context: the graph has initialized the device and it's safe to operate on it within this graph.
|
||||
sync_signals = [(self.signals[d], self.kickoff_var) for b in ji.bufs if (d:=Device[cast(Buffer, b).device]) not in dev_access[enqueue_queue]]
|
||||
dev_access[enqueue_queue].update(cast(HCQCompiled, Device[cast(Buffer, b).device]) for b in ji.bufs)
|
||||
|
||||
# Remove self-dependency for compute and copy queues.
|
||||
# For compute, in case of NV, optimize when only 1 same-queue dependency exists, since NV chains 2+ executions in this case,
|
||||
# eliminating dependency need.
|
||||
dname = enqueue_dev.device.split(":", 1)[0]
|
||||
can_opt = dname in {"AMD", "QCOM"} or (dname == "NV" and len(sync_signals) == 0 and len(opt_deps) == 1 and id(opt_deps[0][0]) == id(out_signal))
|
||||
if can_opt or isinstance(ji.prg, BufferXfer): opt_deps = [x for x in opt_deps if id(x[0]) != id(out_signal)]
|
||||
|
||||
# Enable necessary signals in the schedule by setting the signal value.
|
||||
for sig, val in opt_deps: self.ji_schedule[val - 1] = self.ji_schedule[val - 1][:5] + (val,)
|
||||
self.ji_schedule[j] = (enqueue_dev, enqueue_queue, sync_signals, opt_deps[::-1], out_signal, None if is_exec_prg else (j + 1))
|
||||
|
||||
# Collect profile information if profiling is enabled.
|
||||
if PROFILE:
|
||||
# When execution are chained, we can reuse the end timestamp from the previous command as the start timestamp for the current command.
|
||||
sig_st = prev_ji * 2 + 1 if len(opt_deps) == 0 and (prev_ji:=last_j[enqueue_queue]) is not None else j * 2
|
||||
sig_st = prev_ji * 2 + 1 if len(opt_deps) == 0 and (prev_ji:=self.last_j[enqueue_queue]) is not None else j * 2
|
||||
|
||||
# Description based on the command.
|
||||
prof_ji_desc = ji.prg._prg.name if is_exec_prg else TracingKey(f"{ji.bufs[1].device} -> {ji.bufs[0].device}", ret=ji.bufs[0].nbytes) # type: ignore
|
||||
@@ -134,10 +141,10 @@ class HCQGraph(MultiGraphRunner):
|
||||
self.prof_graph_entries.append(ProfileGraphEntry(prof_name, prof_ji_desc, sig_st, j * 2 + 1))
|
||||
self.prof_graph_deps.append([d - 1 for _, d in rdeps])
|
||||
|
||||
last_j[enqueue_queue] = j
|
||||
self.last_j[enqueue_queue] = j
|
||||
|
||||
# Check which signals are used in the profile graph.
|
||||
self.prof_signal_is_used = [any(ent.st_id == j or ent.en_id == j for ent in self.prof_graph_entries) for j in range(len(self.jit_cache) * 2)]
|
||||
self.prof_signal_is_used: set[int] = {sid for ent in self.prof_graph_entries for sid in (ent.st_id, ent.en_id)}
|
||||
|
||||
# Build hardware queues.
|
||||
self.copy_to_devs: dict[HCQCompiled, set[HCQCompiled]] = {dev: set() for dev in self.devices}
|
||||
@@ -149,7 +156,7 @@ class HCQGraph(MultiGraphRunner):
|
||||
|
||||
for dev in self.devices:
|
||||
self.comp_queues[dev].memory_barrier().wait(self.virt_timeline_signals[dev], self.virt_timeline_vals[dev]) \
|
||||
.wait(self.signals['KICK'], self.kickoff_var).signal(self.signals[dev], self.kickoff_var)
|
||||
.wait(self.kick_signals[dev.peer_group], self.kickoff_var).signal(self.signals[dev], self.kickoff_var)
|
||||
|
||||
for j,ji in enumerate(self.jit_cache):
|
||||
enqueue_dev, enqueue_queue, sync_signals, deps, signal, signal_val = self.ji_schedule[j]
|
||||
@@ -160,11 +167,33 @@ class HCQGraph(MultiGraphRunner):
|
||||
for sig, val in sync_signals + deps: enqueue_queue.wait(sig, val)
|
||||
|
||||
# Encode waits and start profile timestamp (if needed).
|
||||
if PROFILE and self.prof_signal_is_used[j * 2]: enqueue_queue.timestamp(self.prof_signals[j * 2])
|
||||
if PROFILE and j * 2 in self.prof_signal_is_used: enqueue_queue.timestamp(self.prof_signals[j * 2])
|
||||
|
||||
# Encode main commands based on ji type.
|
||||
if isinstance(ji.prg, CompiledRunner):
|
||||
enqueue_queue.exec(ji.prg._prg, self.ji_args[j], tuple(ji.prg.p.global_size or (1,1,1)), tuple(ji.prg.p.local_size or (1,1,1)))
|
||||
elif isinstance(ji.prg, BufferXfer) and len(set(cast(HCQCompiled, Device[cast(Buffer, b).device]).peer_group for b in ji.bufs)) > 1:
|
||||
dest_queue, dest_deps, dest_out_signal, dest_out_val = self.rdma_deps[j]
|
||||
for sig, val in dest_deps: dest_queue.wait(sig, val)
|
||||
|
||||
dest, src = [cast(Buffer, x) for x in ji.bufs[0:2]]
|
||||
dest_dev, src_dev = cast(HCQCompiled, Device[dest.device]), cast(HCQCompiled, Device[src.device])
|
||||
dest_rdma, src_rdma = dest_dev.rdma_dev(), src_dev.rdma_dev()
|
||||
|
||||
# get qp info
|
||||
src_qp, dest_qp, src_cq_buf, dest_cq_buf = src_rdma.iface.connect(dest_rdma)
|
||||
|
||||
# use var for head
|
||||
head_var = self.rdma_vars.setdefault((dest_rdma, src_rdma), (UOp.variable(f"rdma_var_{j}", 0, 0xffffffff, dtype=dtypes.uint32), src_qp))[0]
|
||||
next_head = self.num_rdma_ops[(dest_rdma, src_rdma)]
|
||||
|
||||
rdma_queue = self.rdma_queues[(dest_rdma, src_rdma)]
|
||||
rdma_queue.copy(self.hcq_bufs[j][0], self.hcq_bufs[j][1], dest.nbytes) \
|
||||
.encode_ring(enqueue_queue, src_dev, src_rdma.iface, src_qp, src_cq_buf, head_var + next_head, ring_uar=True) \
|
||||
.encode_ring(self.comp_queues[dest_dev], dest_dev, dest_rdma.iface, dest_qp, dest_cq_buf, head_var + next_head)
|
||||
|
||||
dest_queue.signal(dest_out_signal, dest_out_val)
|
||||
self.num_rdma_ops[(dest_rdma, src_rdma)] += 1
|
||||
elif isinstance(ji.prg, (BufferXfer, BufferCopy)):
|
||||
dest, src = [cast(Buffer, x) for x in ji.bufs[0:2]]
|
||||
for bufid, src in enumerate(cast(list[Buffer], ji.bufs)):
|
||||
@@ -174,14 +203,14 @@ class HCQGraph(MultiGraphRunner):
|
||||
self.copy_to_devs[cast(HCQCompiled, Device[dest.device])].add(cast(HCQCompiled, Device[src.device]))
|
||||
|
||||
# Encode finish profile timestamp (if needed).
|
||||
if PROFILE and self.prof_signal_is_used[j * 2 + 1]: enqueue_queue.timestamp(self.prof_signals[j * 2 + 1])
|
||||
if PROFILE and j * 2 + 1 in self.prof_signal_is_used: enqueue_queue.timestamp(self.prof_signals[j * 2 + 1])
|
||||
|
||||
if signal_val is not None: enqueue_queue.signal(signal, signal_val)
|
||||
|
||||
for dev in self.devices:
|
||||
for dep_dev in list(self.copy_to_devs[dev]) + [dev]:
|
||||
for copy_q in self._dev_copy_queues(dep_dev):
|
||||
if copy_q in self.signals: self.comp_queues[dev].wait(self.signals[copy_q], cast(int, last_j[copy_q]) + 1)
|
||||
if copy_q in self.signals: self.comp_queues[dev].wait(self.signals[copy_q], cast(int, self.last_j[copy_q]) + 1)
|
||||
|
||||
self.comp_queues[dev].signal(self.virt_timeline_signals[dev], self.virt_timeline_vals[dev] + 1).bind(dev)
|
||||
for copy_q in self._dev_copy_queues(dev): copy_q.bind(dev)
|
||||
@@ -189,6 +218,44 @@ class HCQGraph(MultiGraphRunner):
|
||||
self.last_timeline: dict[HCQCompiled, tuple[HCQSignal, int]] = {dev: (dev.timeline_signal, 0) for dev in self.devices}
|
||||
self.queue_signals_to_reset = [self.signals[q] for q in list(self.comp_queues.values()) + list(self.copy_queues.values()) if q in self.signals]
|
||||
|
||||
def _resolve_deps(self, bufs, outs, enqueue_queue, enqueue_dev, out_signal, j, is_copy, rdma_qp=None):
|
||||
rdeps = self._access_resources(bufs, outs, (enqueue_queue, j + 1)) #type:ignore
|
||||
|
||||
# Order shared QP doorbell record writes across different compute queues (head+1 must complete before head+2).
|
||||
if rdma_qp is not None and (prev:=self.rdma_last_dest.get(id(rdma_qp))) is not None and prev[0] is not enqueue_queue:
|
||||
rdeps = rdeps + [(prev[0], prev[1])]
|
||||
if rdma_qp is not None: self.rdma_last_dest[id(rdma_qp)] = (enqueue_queue, j + 1)
|
||||
|
||||
# Update dependencies to include previous kernel in queue. This is required for timeline signals.
|
||||
opt_deps, deps = [], rdeps + ([(enqueue_queue, prev_ji + 1)] if (prev_ji:=self.last_j[enqueue_queue]) is not None else [])
|
||||
|
||||
# Optimize dependencies by removing redundant ones. Remove waiting for the value of the queue which is known to be already
|
||||
# synced with the current queue.
|
||||
for dep_queue, dep_val in sorted(deps, key=lambda x: x[1], reverse=True):
|
||||
if (qa:=self.queue_access[enqueue_queue][dep_queue]) is None or qa < dep_val:
|
||||
opt_deps.append((self.signals[dep_queue], dep_val))
|
||||
self.queue_access[enqueue_queue][dep_queue] = dep_val
|
||||
self.dev_access[enqueue_queue].update(self.dev_access[dep_queue])
|
||||
|
||||
# Ensure device is ready for use in current context: the graph has initialized the device and it's safe to operate on it within this graph.
|
||||
# Only sync with same-peer-group devices; cross-peer-group sync is handled by RDMA.
|
||||
sync_signals = [(self.signals[d], self.kickoff_var) for b in bufs
|
||||
if (d:=cast(HCQCompiled, Device[cast(Buffer, b).device])) not in self.dev_access[enqueue_queue]
|
||||
and (d.peer_group == enqueue_dev.peer_group or rdma_qp is None)]
|
||||
self.dev_access[enqueue_queue].update(cast(HCQCompiled, Device[cast(Buffer, b).device]) for b in bufs)
|
||||
|
||||
# Remove self-dependency for compute and copy queues.
|
||||
# For compute, in case of NV, optimize when only 1 same-queue dependency exists, since NV chains 2+ executions in this case,
|
||||
# eliminating dependency need. For RDMA, keep self-dependency to flush cache.
|
||||
dname = enqueue_dev.device.split(":", 1)[0]
|
||||
can_opt = dname in {"AMD", "QCOM"} or (dname == "NV" and len(sync_signals) == 0 and len(opt_deps) == 1 and id(opt_deps[0][0]) == id(out_signal))
|
||||
if (can_opt or is_copy) and rdma_qp is None: opt_deps = [x for x in opt_deps if id(x[0]) != id(out_signal)]
|
||||
|
||||
# Enable necessary signals in the schedule by setting the signal value.
|
||||
for sig, val in opt_deps: self.ji_schedule[val - 1] = self.ji_schedule[val - 1][:5] + (val,)
|
||||
|
||||
return sync_signals, opt_deps, rdeps
|
||||
|
||||
def _dev_copy_queues(self, dev): return [q for (d, _), q in self.copy_queues.items() if d == dev]
|
||||
|
||||
def __call__(self, input_buffers: list[Buffer], var_vals: dict[str, int], wait=False) -> float|None:
|
||||
@@ -209,6 +276,9 @@ class HCQGraph(MultiGraphRunner):
|
||||
for (j,i),input_idx in self.input_replace.items():
|
||||
hcq_var_vals[self.input_replace_to_var[(j,i)].expr] = input_buffers[input_idx]._buf.va_addr
|
||||
|
||||
for (var, qp) in self.rdma_vars.values(): hcq_var_vals[var.expr] = qp.head
|
||||
for q in self.rdma_queues.values(): q.submit(q.dev, hcq_var_vals)
|
||||
|
||||
for dev in self.devices:
|
||||
self.comp_queues[dev].submit(dev, hcq_var_vals_local:=hcq_var_vals|self.device_vars.get(dev, {}))
|
||||
for copy_queue in self._dev_copy_queues(dev): copy_queue.submit(dev, hcq_var_vals_local)
|
||||
@@ -216,7 +286,7 @@ class HCQGraph(MultiGraphRunner):
|
||||
|
||||
# Launch graph
|
||||
for sig in self.queue_signals_to_reset: sig.value = 0
|
||||
self.signals['KICK'].value = self.kickoff_value
|
||||
for sig in self.kick_signals.values(): sig.value = self.kickoff_value
|
||||
|
||||
if wait:
|
||||
st = time.perf_counter()
|
||||
@@ -247,8 +317,10 @@ class HCQGraph(MultiGraphRunner):
|
||||
# If all of devices are mapped into CPU address space, can use CPU inside the peer group.
|
||||
cpu_support = all(type(d.timeline_signal.base_buf.view) is MMIOInterface for d in all_devs)
|
||||
|
||||
# Check if all devices are within the same peer group. If CPU is supported, don't count it as a separate peer group.
|
||||
if len(set(d.peer_group for d in all_devs if not (cpu_support and d._is_cpu()))) > 1: return False
|
||||
# Check if all devices are within the same peer group. Allow cross-peer-group if all peer groups have RDMA devices.
|
||||
if len(set(d.peer_group for d in all_devs if not (cpu_support and d._is_cpu()))) > 1:
|
||||
try: [d.rdma_dev() for d in all_devs if not d._is_cpu()]
|
||||
except RuntimeError: return False
|
||||
|
||||
if new_call.src[0].op is Ops.COPY:
|
||||
# MOCKGPU is not supported, since it can't execute commands in parallel
|
||||
|
||||
@@ -5,8 +5,8 @@ from tinygrad.uop.ops import sint
|
||||
from tinygrad.runtime.support.hcq import HCQCompiled, HCQAllocatorBase, HCQAllocator, HWQueue, HCQBuffer, FileIOInterface
|
||||
from tinygrad.runtime.support.system import System, PCIIfaceBase, PCIAllocationMeta
|
||||
from tinygrad.runtime.support.memory import VirtMapping, AddrSpace
|
||||
from tinygrad.runtime.support.mlx.mlxdev import MLXDev, MLXQP, to_be
|
||||
from tinygrad.helpers import unwrap
|
||||
from tinygrad.runtime.support.mlx.mlxdev import MLXDev, MLXQP
|
||||
from tinygrad.helpers import unwrap, to_be32, to_be64
|
||||
|
||||
class RDMACopyQueue(HWQueue):
|
||||
def __init__(self, dev:RDMADevice):
|
||||
@@ -19,10 +19,10 @@ class RDMACopyQueue(HWQueue):
|
||||
|
||||
def encode_ring(self, hwq:HWQueue, dev:HCQCompiled, iface:MLXIface, qp:MLXQP, cq_buf:HCQBuffer, head:sint, ring_uar:bool=False):
|
||||
for buf in [iface.dbr_buf, cq_buf] + ([iface.uar_buf] if ring_uar else []): cast(HCQAllocator, dev.allocator).map(buf)
|
||||
hwq.write(iface.dbr_buf.offset(qp.qp_dbr + (4 if ring_uar else 0)), to_be('I', head + 1))
|
||||
if ring_uar: hwq.write(iface.uar_buf.offset(0x800), to_be('Q', ((head << 8) | 0x0a) << 32 | ((qp.qp_info['qpn'] << 8) | 2)), b64=True)
|
||||
hwq.poll_bit(cq_buf.offset((head & (qp.cq_size - 1)) * 64 + 60, 4), ((head >> 7) & 1) << 24, mask=0x01000000)
|
||||
hwq.write(iface.dbr_buf.offset(qp.cq_dbr), to_be('I', (head + 1) & 0xFFFFFF))
|
||||
hwq.write(iface.dbr_buf.offset(qp.qp_dbr + (4 if ring_uar else 0)), to_be32(head + 1))
|
||||
if ring_uar: hwq.write(iface.uar_buf.offset(0x800), to_be64(((head << 8) | 0x0a) << 32 | ((qp.qp_info['qpn'] << 8) | 2)), b64=True)
|
||||
hwq.poll_bit(cq_buf.offset((head & (qp.cq_size - 1)) * 64 + 60, 4), ((head >> (qp.cq_size.bit_length() - 1)) & 1) << 24, mask=0x01000000)
|
||||
hwq.write(iface.dbr_buf.offset(qp.cq_dbr), to_be32((head + 1) & 0xFFFFFF))
|
||||
return self
|
||||
|
||||
def copy(self, dest:HCQBuffer, src:HCQBuffer, sz:int):
|
||||
@@ -39,8 +39,8 @@ class RDMACopyQueue(HWQueue):
|
||||
def _submit(self, dev:RDMADevice):
|
||||
for remote_nic, sq_wqe, rq_wqe in zip(self._q[0::3], self._q[1::3], self._q[2::3]):
|
||||
src_qp, dest_qp, _, _ = dev.iface.connect(remote_nic)
|
||||
assert src_qp.head + 1 - to_be('I', src_qp.dev.dbr[src_qp.qp_dbr // 4 + 1]) <= (1 << src_qp.log_sq_size), "SQ ring full"
|
||||
assert src_qp.head + 1 - to_be('I', dest_qp.dev.dbr[dest_qp.qp_dbr // 4]) <= (1 << dest_qp.log_rq_size), "RQ ring full"
|
||||
assert src_qp.head + 1 - to_be32(src_qp.dev.dbr[src_qp.qp_dbr // 4 + 1]) <= (1 << src_qp.log_sq_size), "SQ ring full"
|
||||
assert src_qp.head + 1 - to_be32(dest_qp.dev.dbr[dest_qp.qp_dbr // 4]) <= (1 << dest_qp.log_rq_size), "RQ ring full"
|
||||
dest_qp.qp_buf.view((src_qp.head & ((1 << dest_qp.log_rq_size) - 1)) * 16, 16)[:] = rq_wqe
|
||||
sq_view = src_qp.qp_buf.view(src_qp.sq_offset + (src_qp.head & ((1 << src_qp.log_sq_size) - 1)) * 64, 64)
|
||||
sq_view[:] = struct.pack('>I', (src_qp.head << 8) | 0x0a) + sq_wqe[4:]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
import struct, random, socket, ctypes, functools, itertools
|
||||
from tinygrad.helpers import getenv, wait_cond, round_up, next_power2, ceildiv, DEBUG, hi32, lo32
|
||||
from tinygrad.helpers import getenv, wait_cond, round_up, next_power2, ceildiv, DEBUG, hi32, lo32, to_be32, to_be64
|
||||
from tinygrad.runtime.support.memory import BumpAllocator
|
||||
from tinygrad.runtime.support.system import PCIDevice
|
||||
from tinygrad.runtime.autogen import mlx5, pci
|
||||
@@ -11,7 +11,6 @@ MLX5_CMD_STRUCTS = {v: (getattr(mlx5, f"struct_mlx5_ifc_{n[12:].lower()}_in_bits
|
||||
getattr(mlx5, f"struct_mlx5_ifc_{n[12:].lower()}_out_bits", None)) for n, v in mlx5.__dict__.items() if n.startswith("MLX5_CMD_OP_")}
|
||||
MLX5_CMD_STRUCTS[mlx5.MLX5_CMD_OP_ACCESS_REG] = (mlx5.struct_mlx5_ifc_access_register_in_bits, mlx5.struct_mlx5_ifc_access_register_out_bits)
|
||||
|
||||
def to_be(fmt, val): return struct.unpack('<'+fmt, struct.pack('>'+fmt, val))[0]
|
||||
def ipv4_to_gid(ip): return bytes(10) + b'\xff\xff' + socket.inet_aton(ip)
|
||||
|
||||
def udp_sport(lqpn, rqpn):
|
||||
@@ -64,7 +63,7 @@ class MLXCmdQueue:
|
||||
for i in range(n):
|
||||
off, _ = self.mboxes[base + i]
|
||||
blk = mlx5.struct_mlx5_cmd_prot_block(data=list(data[i*chunk_sz:(i+1)*chunk_sz].ljust(chunk_sz, b'\x00')),
|
||||
next=to_be('Q', self.mboxes[base+i+1][1]) if i < n-1 else 0, block_num=to_be('I', i), token=tok)
|
||||
next=to_be64(self.mboxes[base+i+1][1]) if i < n-1 else 0, block_num=to_be32(i), token=tok)
|
||||
self.queue[off:off + ctypes.sizeof(mlx5.struct_mlx5_cmd_prot_block)] = bytes(blk)
|
||||
return (self.mboxes[base][0], self.mboxes[base][1], n)
|
||||
|
||||
@@ -81,9 +80,9 @@ class MLXCmdQueue:
|
||||
# prepare mailboxes and build command layout
|
||||
_, in_ptr, n_in = self.create_mbox_chain(0, tok, inp[16:])
|
||||
_, out_ptr, n_out = self.create_mbox_chain(n_in, tok, bytes(out_sz))
|
||||
cmd = mlx5.struct_mlx5_cmd_layout(type=mlx5.MLX5_PCI_CMD_XPORT, inlen=to_be('I', len(inp)), in_ptr=to_be('Q', in_ptr),
|
||||
cmd = mlx5.struct_mlx5_cmd_layout(type=mlx5.MLX5_PCI_CMD_XPORT, inlen=to_be32(len(inp)), in_ptr=to_be64(in_ptr),
|
||||
_in=[int.from_bytes(inp[i:i+4], 'little') for i in range(0, 16, 4)],
|
||||
out_ptr=to_be('Q', out_ptr), outlen=to_be('I', 16 + out_sz), token=tok, status_own=mlx5.CMD_OWNER_HW)
|
||||
out_ptr=to_be64(out_ptr), outlen=to_be32(16 + out_sz), token=tok, status_own=mlx5.CMD_OWNER_HW)
|
||||
cmd_bytes = bytearray(bytes(cmd))
|
||||
cmd_bytes[mlx5.struct_mlx5_cmd_layout.sig.offset] = (~functools.reduce(lambda a, b: a ^ b, cmd_bytes)) & 0xFF # type: ignore[attr-defined]
|
||||
|
||||
@@ -115,8 +114,8 @@ class MLXDev:
|
||||
|
||||
self.init_hw(ip)
|
||||
|
||||
def rreg(self, off): return to_be('I',self.bar[off // 4])
|
||||
def wreg(self, off, val): self.bar[off // 4] = to_be('I',val)
|
||||
def rreg(self, off): return to_be32(self.bar[off // 4])
|
||||
def wreg(self, off, val): self.bar[off // 4] = to_be32(val)
|
||||
def iseg_r(self, field): return self.rreg(getattr(mlx5.struct_mlx5_init_seg, field).offset)
|
||||
def iseg_w(self, field, val): self.wreg(getattr(mlx5.struct_mlx5_init_seg, field).offset, val)
|
||||
|
||||
|
||||
@@ -66,7 +66,9 @@ class _System:
|
||||
iokit.IOServiceGetMatchingServices(0, iokit.IOServiceMatching(b"IOPCIDevice"), ctypes.byref(iterator:=ctypes.c_uint()))
|
||||
while svc:=iokit.IOIteratorNext(iterator): all_devs.append((v:=read_prop(svc, "vendor-id"), d:=read_prop(svc, "device-id"), f"{v:x}:{d:x}"))
|
||||
else:
|
||||
for pcibus in FileIOInterface("/sys/bus/pci/devices").listdir():
|
||||
try: devs = FileIOInterface("/sys/bus/pci/devices")
|
||||
except FileNotFoundError: raise RuntimeError("no pcie")
|
||||
for pcibus in devs.listdir():
|
||||
if base_class is not None and int(FileIOInterface(f"/sys/bus/pci/devices/{pcibus}/class").read(), 16) >> 16 != base_class: continue
|
||||
all_devs.append((int(FileIOInterface(f"/sys/bus/pci/devices/{pcibus}/vendor").read(), 16),
|
||||
int(FileIOInterface(f"/sys/bus/pci/devices/{pcibus}/device").read(), 16), pcibus))
|
||||
|
||||
+10
-41
@@ -7,7 +7,7 @@ if TYPE_CHECKING: import numpy
|
||||
from tinygrad.dtype import DType, DTypeLike, dtypes, ConstType, least_upper_float, least_upper_dtype, to_dtype, truncate
|
||||
from tinygrad.dtype import _from_np_dtype, _to_np_dtype, PyConst, Invalid, InvalidType
|
||||
from tinygrad.helpers import argfix, make_tuple, flatten, prod, all_int, round_up, merge_dicts, argsort, getenv, all_same, fully_flatten
|
||||
from tinygrad.helpers import IMAGE, FLOAT16, WINO, Metadata, TRACEMETA, ASM_GEMM, ceildiv, fetch, is_numpy_ndarray, TracingKey, cpu_profile
|
||||
from tinygrad.helpers import IMAGE, FLOAT16, WINO, Metadata, TRACEMETA, ceildiv, fetch, is_numpy_ndarray, TracingKey, cpu_profile
|
||||
from tinygrad.helpers import suppress_finalizing, disable_gc
|
||||
from tinygrad.gradient import compute_gradient
|
||||
from tinygrad.mixin import OpMixin
|
||||
@@ -832,7 +832,7 @@ class Tensor(OpMixin):
|
||||
if isinstance(self.device, tuple): return self._multi_like(Tensor.rand, **kwargs)
|
||||
return Tensor.rand(*self.shape, device=kwargs.pop("device", self.device), dtype=kwargs.pop("dtype", self.dtype), **kwargs)
|
||||
|
||||
# ***** rng hlops *****
|
||||
# ***** random functions *****
|
||||
|
||||
def randn_like(self, dtype:DTypeLike|None=None, requires_grad:bool|None=None, **kwargs) -> Tensor:
|
||||
"""
|
||||
@@ -880,9 +880,8 @@ class Tensor(OpMixin):
|
||||
print(Tensor.randint(2, 3, low=5, high=10).numpy())
|
||||
```
|
||||
"""
|
||||
if not isinstance(low, int) or not isinstance(high, int): raise TypeError(f"{low=} and {high=} must be integers")
|
||||
dtype = to_dtype(dtype)
|
||||
if not dtypes.is_int(dtype): raise TypeError(f"{dtype=} must be int")
|
||||
if not all_int([low, high]): raise TypeError(f"{low=} and {high=} must be integers")
|
||||
if not dtypes.is_int(dtype := to_dtype(dtype)): raise TypeError(f"{dtype=} must be int")
|
||||
return Tensor.uniform(*shape, low=low, high=high, dtype=dtype, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
@@ -898,7 +897,7 @@ class Tensor(OpMixin):
|
||||
print(Tensor.normal(2, 3, mean=10, std=2).numpy())
|
||||
```
|
||||
"""
|
||||
return ((std * Tensor.randn(*shape, **kwargs)) + mean).requires_grad_(requires_grad)
|
||||
return (std * Tensor.randn(*shape, **kwargs) + mean).requires_grad_(requires_grad)
|
||||
|
||||
@staticmethod
|
||||
def uniform(*shape, low=0.0, high=1.0, dtype:DTypeLike|None=None, requires_grad:bool|None=None, **kwargs) -> Tensor:
|
||||
@@ -931,7 +930,6 @@ class Tensor(OpMixin):
|
||||
"""
|
||||
return Tensor.uniform(*shape, low=-1.0, high=1.0, **kwargs).mul(prod(argfix(*shape))**-0.5)
|
||||
|
||||
# https://www.tensorflow.org/api_docs/python/tf/keras/initializers/GlorotUniform
|
||||
@staticmethod
|
||||
def glorot_uniform(*shape, **kwargs) -> Tensor:
|
||||
"""
|
||||
@@ -945,9 +943,9 @@ class Tensor(OpMixin):
|
||||
print(Tensor.glorot_uniform(2, 3).numpy())
|
||||
```
|
||||
"""
|
||||
return Tensor.uniform(*shape, low=-1.0, high=1.0, **kwargs).mul((6/(argfix(*shape)[0]+prod(argfix(*shape)[1:])))**0.5)
|
||||
bound = (6 / (argfix(*shape)[0]+prod(argfix(*shape)[1:]))) ** 0.5
|
||||
return Tensor.uniform(*shape, low=-bound, high=bound, **kwargs)
|
||||
|
||||
# https://pytorch.org/docs/stable/_modules/torch/nn/init.html#kaiming_uniform_
|
||||
@staticmethod
|
||||
def kaiming_uniform(*shape, a:float = 0.01, **kwargs) -> Tensor:
|
||||
"""
|
||||
@@ -961,10 +959,9 @@ class Tensor(OpMixin):
|
||||
print(Tensor.kaiming_uniform(2, 3).numpy())
|
||||
```
|
||||
"""
|
||||
bound = math.sqrt(3.0) * math.sqrt(2.0 / (1 + a ** 2)) / math.sqrt(prod(argfix(*shape)[1:]))
|
||||
bound = (6 / (1 + a ** 2) / prod(argfix(*shape)[1:])) ** 0.5
|
||||
return Tensor.uniform(*shape, low=-bound, high=bound, **kwargs)
|
||||
|
||||
# https://pytorch.org/docs/stable/_modules/torch/nn/init.html#kaiming_normal_
|
||||
@staticmethod
|
||||
def kaiming_normal(*shape, a:float = 0.01, **kwargs) -> Tensor:
|
||||
"""
|
||||
@@ -978,7 +975,7 @@ class Tensor(OpMixin):
|
||||
print(Tensor.kaiming_normal(2, 3).numpy())
|
||||
```
|
||||
"""
|
||||
std = math.sqrt(2.0 / (1 + a ** 2)) / math.sqrt(prod(argfix(*shape)[1:]))
|
||||
std = (2 / (1 + a ** 2) / prod(argfix(*shape)[1:])) ** 0.5
|
||||
return Tensor.normal(*shape, mean=0.0, std=std, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
@@ -1059,7 +1056,7 @@ class Tensor(OpMixin):
|
||||
else: t.grad.assign(t.grad + g.to(t.grad.device))
|
||||
return self
|
||||
|
||||
# ***** movement low level ops *****
|
||||
# ***** movement ops *****
|
||||
|
||||
def _mop(self, op:Ops, arg) -> Tensor: return self._apply_uop(UOp._mop, extra_args=(op,), arg=arg)
|
||||
def _rop(self, op:Ops, axis:tuple[int, ...]) -> Tensor: return self._apply_uop(UOp._rop, op=op, axis=axis)
|
||||
@@ -1137,8 +1134,6 @@ class Tensor(OpMixin):
|
||||
if mode in {"reflect", "replicate"}: return self._pad_reflect_replicate(pX, mode)
|
||||
raise NotImplementedError(f"{mode=} is not supported")
|
||||
|
||||
# ***** movement high level ops *****
|
||||
|
||||
def _getitem(self, indices, v: Tensor|None = None) -> Tensor:
|
||||
# wrap single index into a list
|
||||
if (isinstance(indices, list) and all_int(indices)) or not isinstance(indices, (tuple, list)): indices = [indices]
|
||||
@@ -2032,9 +2027,6 @@ class Tensor(OpMixin):
|
||||
|
||||
def dot(self, w:Tensor, dtype:DTypeLike|None=None) -> Tensor:
|
||||
if IMAGE: return self.image_dot(w, dtype)
|
||||
if ASM_GEMM:
|
||||
from extra.gemm.cdna_asm_gemm import can_use_asm_gemm, asm_gemm
|
||||
if can_use_asm_gemm(self, w): return asm_gemm(self, w)
|
||||
return super().dot(w, dtype=dtype)
|
||||
|
||||
def _cumalu(self, axis:int, op:Ops, _include_initial=False) -> Tensor:
|
||||
@@ -2385,21 +2377,6 @@ class Tensor(OpMixin):
|
||||
"""
|
||||
return self._apply_uop(UOp.contiguous_backward)
|
||||
|
||||
# ***** math functions *****
|
||||
|
||||
def lerp(self, end:Tensor, weight:Tensor|float) -> Tensor:
|
||||
"""
|
||||
Linearly interpolates between `self` and `end` by `weight`.
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(Tensor([1., 2., 3.]).lerp(Tensor([4., 5., 6.]), 0.5).numpy())
|
||||
```
|
||||
"""
|
||||
if self.dtype == dtypes.uint8 and isinstance(weight, Tensor):
|
||||
w_i = (weight * (1<<(W_PREC:=7)) + 0.5).cast(dtypes.int16)
|
||||
return (self+(((end - self).cast(dtypes.int8) * w_i + (1<<W_PREC-1)).cast(dtypes.uint16) >> W_PREC)).cast(dtypes.uint8)
|
||||
return self + (end - self) * weight
|
||||
|
||||
# ***** broadcasted elementwise ops *****
|
||||
|
||||
def ufix(self, x) -> Tensor:
|
||||
@@ -2644,14 +2621,6 @@ class Tensor(OpMixin):
|
||||
print(q.scaled_dot_product_attention(k, v).numpy())
|
||||
```
|
||||
"""
|
||||
if getenv("FLASH_ATTENTION"):
|
||||
from extra.thunder.tiny.fa import flash_attention
|
||||
return flash_attention(self, key, value, attn_mask=attn_mask, is_causal=is_causal)
|
||||
|
||||
if getenv("HK_FLASH_ATTENTION"):
|
||||
from extra.thunder.amd.fa import flash_attention
|
||||
return flash_attention(self, key, value, attn_mask=attn_mask, is_causal=is_causal)
|
||||
|
||||
# GQA: https://docs.pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html
|
||||
if enable_gqa:
|
||||
key = key.repeat_interleave(int(self.shape[-3] // key.shape[-3]), dim=-3)
|
||||
|
||||
@@ -658,16 +658,6 @@ class UOp(OpMixin, metaclass=UOpMetaClass):
|
||||
if ret.shape == self.shape and same_shape_noop: return self
|
||||
return ret
|
||||
|
||||
# in these four, if the shape doesn't change we can return self
|
||||
#def reshape(self, arg:tuple[sint, ...]): return self._mop(Ops.RESHAPE, arg, same_shape_noop=True)
|
||||
#def expand(self, arg:tuple[sint, ...]): return self._mop(Ops.EXPAND, arg, same_shape_noop=True)
|
||||
#def shrink(self, arg:tuple[tuple[sint, sint], ...]): return self._mop(Ops.SHRINK, arg, same_shape_noop=True)
|
||||
def pad(self, arg:tuple[tuple[sint, sint], ...]): return self._mop(Ops.PAD, arg, same_shape_noop=True)
|
||||
|
||||
# in these two, we have custom logic to check if they are a no-op
|
||||
#def permute(self, arg:tuple[int, ...]): return self._mop(Ops.PERMUTE, arg, same_shape_noop=False) if arg != tuple(range(len(self.shape))) else self
|
||||
#def flip(self, arg:tuple[bool, ...]): return self._mop(Ops.FLIP, arg, same_shape_noop=False) if any(arg) and len(arg) == len(self.shape) else self
|
||||
|
||||
# *** uop UNIQUE ***
|
||||
|
||||
# TODO: use this in Buffer
|
||||
|
||||
+17
-18
@@ -16,7 +16,6 @@ const darkenHex = (h, p = 0) =>
|
||||
|
||||
const ANSI_COLORS = ["#b3b3b3", "#ff6666", "#66b366", "#ffff66", "#6666ff", "#ff66ff", "#66ffff", "#ffffff"];
|
||||
const ANSI_COLORS_LIGHT = ["#d9d9d9","#ff9999","#99cc99","#ffff99","#9999ff","#ff99ff","#ccffff","#ffffff"];
|
||||
const colorsCache = new Map();
|
||||
const parseColors = (name, defaultColor="#ffffff") => Array.from(name.matchAll(/(?:\u001b\[(\d+)m([\s\S]*?)\u001b\[0m)|([^\u001b]+)/g),
|
||||
([_, code, colored_st, st]) => ({ st: colored_st ?? st, color: code != null ? (code>=90 ? ANSI_COLORS_LIGHT : ANSI_COLORS)[(parseInt(code)-30+60)%60] : defaultColor }));
|
||||
|
||||
@@ -375,7 +374,6 @@ function setFocus(key) {
|
||||
}
|
||||
|
||||
const EventTypes = { EXEC:0, BUF:1 };
|
||||
const GraphConfig = [{ pcolor:"#c9a8ff", unit:"B", fillColor:"#2B1B72"}, { pcolor:"#4fa3cc", unit:"Hz", fillColor:"#4fa3cc"}];
|
||||
|
||||
async function renderProfiler(path, opts) {
|
||||
displaySelection("#profiler");
|
||||
@@ -408,8 +406,8 @@ async function renderProfiler(path, opts) {
|
||||
canvas.addEventListener("wheel", e => (e.stopPropagation(), e.preventDefault()), { passive:false });
|
||||
const ctx = canvas.getContext("2d");
|
||||
const canvasTop = rect(canvas).top;
|
||||
// color by key (name/device)
|
||||
const colorMap = new Map();
|
||||
// map event name to shape and label colors
|
||||
const colorMap = new Map(), coloredNames = new Map();
|
||||
// map shapes by event key
|
||||
const shapeMap = new Map();
|
||||
const heightScale = d3.scaleLinear().domain([0, tracePeak]).range([4,maxheight=100]);
|
||||
@@ -448,11 +446,14 @@ async function renderProfiler(path, opts) {
|
||||
colorMap.set(colorKey, d3.rgb(color));
|
||||
}
|
||||
const fillColor = colorMap.get(colorKey).brighter(0.3*depth).toString();
|
||||
const label = parseColors(e.name).flatMap(({ color, st }) => {
|
||||
const parts = [];
|
||||
for (let i=0; i<st.length; i+=4) { const part = st.slice(i, i+4); parts.push({ color, st:part, width:ctx.measureText(part).width }); }
|
||||
return parts;
|
||||
});
|
||||
let label = coloredNames.get(e.name);
|
||||
if (label == null) {
|
||||
label = parseColors(e.name).flatMap(({ color, st }) => {
|
||||
const parts = [];
|
||||
for (let i=0; i<st.length; i+=4) { const part = st.slice(i, i+4); parts.push({ color, st:part, width:ctx.measureText(part).width }); }
|
||||
return parts;
|
||||
}); coloredNames.set(e.name, label);
|
||||
}
|
||||
let shapeRef = e.ref;
|
||||
if (shapeRef != null) { ref = {ctx:e.ref, step:0}; shapeRef = ref; }
|
||||
else if (ref != null) {
|
||||
@@ -484,7 +485,6 @@ async function renderProfiler(path, opts) {
|
||||
div.style("height", levelHeight*levels.length+padding+"px").style("pointerEvents", "none");
|
||||
} else {
|
||||
const linear = u8(), peak = u64();
|
||||
const config = GraphConfig[linear];
|
||||
const timestamps = [], valueMap = new Map();
|
||||
// start by unpacking the raw events
|
||||
const memEvents = [];
|
||||
@@ -513,7 +513,7 @@ async function renderProfiler(path, opts) {
|
||||
const yscale = d3.scaleLinear().domain([0, peak]).range([height, 0]);
|
||||
// generic polygon merger
|
||||
const base0 = yscale(0);
|
||||
const sum = {x:[], y0:[], y1:[], fillColor:config.fillColor};
|
||||
const sum = {x:[], y0:[], y1:[], fillColor:linear ? null : "#2b1b72"};
|
||||
for (let i=0; i<timestamps.length-1; i++) {
|
||||
const yv = yscale(valueMap.get(timestamps[i]));
|
||||
sum.x.push(timestamps[i], timestamps[i+1]); sum.y1.push(yv, yv); sum.y0.push(base0, base0);
|
||||
@@ -552,8 +552,8 @@ async function renderProfiler(path, opts) {
|
||||
return bufShapes;
|
||||
};
|
||||
if (timestamps.length > 0) data.first = data.first == null ? timestamps[0] : Math.min(data.first, timestamps[0]);
|
||||
data.tracks.set(k, { shapes:[sum], eventType, linear, visible, offsetY, pcolor:config.pcolor, height, peak, scaleFactor:maxheight*4/height,
|
||||
get views() { return [[sum], linear ? null : buildBufShapes()]; }, valueMap, rowBorderColor });
|
||||
data.tracks.set(k, { shapes:[sum], eventType, linear, visible, offsetY, pcolor:linear ? "#4fa3cc" : "#c9a8ff", height, peak, scaleFactor:maxheight*4/height,
|
||||
get views() { return [[sum], linear ? null : buildBufShapes()]; }, valueMap, rowBorderColor, unit:linear ? "Hz" : "B" });
|
||||
div.style("height", height+padding+"px").style("cursor", "pointer").on("click", (e) => {
|
||||
if (linear) return;
|
||||
const newFocus = e.currentTarget.id === focusedDevice ? null : e.currentTarget.id;
|
||||
@@ -563,7 +563,7 @@ async function renderProfiler(path, opts) {
|
||||
if (tid === newFocus) { track.shapes = track.views[1]; offset += rescaleTrack(track, tid, track.scaleFactor); }
|
||||
else if (tid === focusedDevice) { track.shapes = track.views[0]; offset += rescaleTrack(track, tid, 1/track.scaleFactor); }
|
||||
}
|
||||
data.axes.y = newFocus != null ? { domain:[0, (t=data.tracks.get(newFocus)).peak], range:[t.offsetY+t.height, t.offsetY], fmt:config.unit } : null;
|
||||
data.axes.y = newFocus != null ? { domain:[0, (t=data.tracks.get(newFocus)).peak], range:[t.offsetY+t.height, t.offsetY], fmt:t.unit } : null;
|
||||
toggleCls(document.getElementById(focusedDevice), document.getElementById(newFocus), "expanded");
|
||||
focusedDevice = newFocus;
|
||||
return resize();
|
||||
@@ -608,12 +608,11 @@ async function renderProfiler(path, opts) {
|
||||
const visibleYStart = profilerEl.scrollTop-canvasTop + rect(profilerEl).top, visibleYEnd = visibleYStart+profilerEl.clientHeight;
|
||||
ctx.textBaseline = "middle";
|
||||
// draw shapes
|
||||
for (const [k, { shapes, eventType, linear, visible, offsetY, valueMap, pcolor, scolor, rowBorderColor }] of data.tracks) {
|
||||
for (const [k, { shapes, eventType, linear, visible, offsetY, valueMap, pcolor, scolor, unit, rowBorderColor }] of data.tracks) {
|
||||
visible.length = 0;
|
||||
const trackHeight = rect(document.getElementById(k)).height;
|
||||
if (offsetY+trackHeight < visibleYStart || offsetY > visibleYEnd) continue;
|
||||
const addBorder = scolor != null ? (w) => { if (w > 10) { ctx.strokeStyle = scolor; ctx.stroke(); } } : null;
|
||||
const config = GraphConfig[linear];
|
||||
for (const e of shapes) {
|
||||
if (eventType === EventTypes.BUF) { // generic polygon
|
||||
if (e.x[0]>et || e.x.at(-1)<st) continue;
|
||||
@@ -623,10 +622,10 @@ async function renderProfiler(path, opts) {
|
||||
for (let i=1; i<x.length; i++) {
|
||||
ctx.lineTo(x[i], offsetY+e.y1[i]);
|
||||
let arg = e.arg;
|
||||
if (arg == null && valueMap != null) arg = {tooltipText: formatUnit(valueMap.get(e.x[i-1]), config.unit)}
|
||||
if (arg == null && valueMap != null) arg = {tooltipText: formatUnit(valueMap.get(e.x[i-1]), unit)}
|
||||
visible.push({ x0:x[i-1], x1:x[i], y0:offsetY+e.y1[i-1], y1:offsetY+e.y0[i], arg });
|
||||
}
|
||||
if (linear) { ctx.strokeStyle = e.fillColor; ctx.lineWidth = 2; ctx.stroke(); ctx.lineWidth = 1; }
|
||||
if (linear) { ctx.strokeStyle = pcolor; ctx.lineWidth = 2; ctx.stroke(); ctx.lineWidth = 1; }
|
||||
// walk the path back and fill the complete shape
|
||||
else { for (let i=x.length-1; i>=0; i--) ctx.lineTo(x[i], offsetY+e.y0[i]); ctx.closePath(); ctx.fillStyle = e.fillColor; ctx.fill(); }
|
||||
} else { // contiguous rect
|
||||
|
||||
+7
-11
@@ -225,8 +225,9 @@ def timeline_layout(dev_events:list[tuple[int, int, float, DevEvent]], start_ts:
|
||||
ref = next((v for k in e.name.keys if (v:=ref_map.get(k)) is not None), None)
|
||||
if isinstance(e.name.ret, str): fmt.append(e.name.ret)
|
||||
elif isinstance(e.name.ret, int):
|
||||
membw = e.name.ret / (dur * 1e-6)
|
||||
membw = (nbytes:=e.name.ret) / (dur * 1e-6)
|
||||
fmt.append(f"{membw*1e-9:.0f} GB/s" if membw < 1e13 else f"{membw*1e-12:.0f} TB/s")
|
||||
fmt.append(next((f"{nbytes/s:.0f} {u}" for s,u in ((1e9,"GB"),(1e6,"MB"),(1e3,"KB")) if nbytes>=s), f"{nbytes:.0f} B"))
|
||||
elif e.name.tb: fmt.append("TB:"+json.dumps(e.name.tb))
|
||||
events.append(struct.pack("<IIIIfI", enum_str(name, scache), option(ref), option(key), rel_ts(st,start_ts, f"'{name}' on {e.device}"),
|
||||
dur, enum_str("\n".join(fmt),scache)))
|
||||
@@ -351,12 +352,10 @@ def sqtt_timeline(data:bytes, lib:bytes, target:str) -> Generator[ProfileEvent,
|
||||
row_counts:dict[str, itertools.count] = {}
|
||||
curr_barrier:dict[int, ProfileRangeEvent] = {}
|
||||
exec_pending:dict[str, list[tuple[str, str]]] = {}
|
||||
is_cdna = target.startswith("gfx9")
|
||||
dispatch_to_exec = {"WMMA":"VALU", "VALU":"VALU", "VALU1":"VALU", "VALUT":"VALU", "VALUB":"VALU", "VALUINST":"VALU", "VINTERP":"VALU",
|
||||
"SGMEM":"VMEM", "FLAT":"VMEM", "LDS":"LDS", "SALU":"SALU", "SMEM":"SALU", "VMEM":"VMEM"}
|
||||
def add(name:str, p:PacketType, wave:int|None=None, info:InstructionInfo|None=None) -> Generator[ProfileEvent, None, None]:
|
||||
row = "OTHER_SIMD" if name.startswith("OTHER_") else f"WAVE:{wave}" if (wave:=getattr(p, "wave", wave)) is not None \
|
||||
else f"{p.__class__.__name__}:0 {name.replace('_ALT', '')}"
|
||||
row = f"WAVE:{wave}" if (wave:=getattr(p, "wave", wave)) is not None else f"{p.__class__.__name__}:0 {name.replace('_ALT', '')}"
|
||||
# by default we extend the packet to one cycle after timestamp
|
||||
start_time, end_time = p._time, p._time+1
|
||||
# exec links to dispatch, dispatch links to PC
|
||||
@@ -365,11 +364,11 @@ def sqtt_timeline(data:bytes, lib:bytes, target:str) -> Generator[ProfileEvent,
|
||||
dispatch_id, op_type = exec_pending[name].pop(0)
|
||||
# get the number of cycles from the op type
|
||||
duration = int(dur_match.group(1)) if (dur_match:=re.match(r".*_(\d+)$", op_type)) else 1
|
||||
# for execs, timestamp is the completion time
|
||||
start_time, end_time = p._time-duration, p._time
|
||||
# for execs, extend end time by the duration
|
||||
start_time, end_time = p._time, p._time+duration
|
||||
link = f"LINK:{dispatch_id}"
|
||||
# add wmma in the exec name for coloring
|
||||
if op_type.startswith("WMMA"): name += "_WMMA"
|
||||
# wmma exec gets its own row and color
|
||||
if op_type.startswith("WMMA"): name, row = name+"_WMMA", "ALUEXEC:0 WMMA"
|
||||
# queue inst dispatches
|
||||
idx = next(row_counts.setdefault(row, itertools.count(0)))
|
||||
if isinstance(p, (VALUINST, INST, INST_RDNA4)) and (exec_type:=dispatch_to_exec.get(name.replace("OTHER_", "").split("_")[0])) is not None:
|
||||
@@ -381,9 +380,6 @@ def sqtt_timeline(data:bytes, lib:bytes, target:str) -> Generator[ProfileEvent,
|
||||
# construct and yield the event for this packet
|
||||
if row not in row_ends: yield ProfilePointEvent(row, "JSON", "pcMap", pc_map, ts=Decimal(0))
|
||||
yield (e:=ProfileRangeEvent(row, TracingKey(name, ret=link), Decimal(start_time), Decimal(end_time)))
|
||||
# allow CDNA packets to overlap, NOT allowed on RDNA.
|
||||
if (et:=row_ends.get(row)) is not None and e.st < et and not is_cdna and not isinstance(p, (ALUEXEC, VMEMEXEC)):
|
||||
RuntimeError(f"packet {row}-{idx} overlaps: {e.st} {et}.")
|
||||
row_ends[row] = unwrap(e.en)
|
||||
# barrier on this wave extends to fill the time it was waiting
|
||||
if wave is not None:
|
||||
|
||||
Reference in New Issue
Block a user