mirror of
https://github.com/tinygrad/tinygrad.git
synced 2026-08-27 01:26:06 +00:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12aa7a22a2 | ||
|
|
3919ce8427 | ||
|
|
756e82e055 | ||
|
|
cc32aa18db | ||
|
|
77f698e55b | ||
|
|
554d078ac4 | ||
|
|
176377ff6e | ||
|
|
1c3c9e96f6 | ||
|
|
e8a8d99b99 | ||
|
|
dcc2d021e7 | ||
|
|
80bf60d782 | ||
|
|
1cb0600086 | ||
|
|
1bcb6bdc62 | ||
|
|
d716d0d927 | ||
|
|
9216aa494c | ||
|
|
9aa9e11301 | ||
|
|
3fdbb82bfe | ||
|
|
0ccef542e0 | ||
|
|
3b6abbd84b | ||
|
|
dfe08dfcf7 |
@@ -504,7 +504,7 @@ jobs:
|
||||
- name: Run AMD renderer tests (AMD:LLVM)
|
||||
run: DEV=MOCKKFD+AMD:LLVM python -m pytest -n=auto test/amd/ --durations 20
|
||||
- name: Run SQTT profiling tests
|
||||
run: PROFILE=1 SQTT=1 python3 -m pytest -n=auto test/amd/test_sqtt_profiler.py
|
||||
run: VIZ=-2 python3 -m pytest -n=auto test/amd/test_sqtt_profiler.py
|
||||
- name: Run AMD emulated tests on NULL backend
|
||||
env:
|
||||
AMD: 0
|
||||
@@ -679,4 +679,5 @@ jobs:
|
||||
run: |
|
||||
python -c "from tinygrad import Device; assert Device.DEFAULT == 'NULL'"
|
||||
DEBUG=4 python3 test/backend/test_ops.py TestOps.test_add
|
||||
python -m pytest -n=auto test/backend/test_ops.py --durations=20
|
||||
# QCOMCL compiles in qemu, too slow for parallel workers
|
||||
${{ contains(matrix.dev, 'QCOMCL') && 'PARALLEL=0' || '' }} python -m pytest -n=auto test/backend/test_ops.py --durations=20
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -192,7 +192,7 @@ def unpack_insts(viz_data, i:int, j:int, data:dict) -> dict:
|
||||
prev_instr = max(prev_instr, e.time + e.dur)
|
||||
summary = [{"label":"Total Cycles", "value":w.end_time-w.begin_time}, {"label":"SE", "value":w.se}, {"label":"CU", "value":w.cu},
|
||||
{"label":"SIMD", "value":w.simd}, {"label":"Wave ID", "value":w.wave_id}, {"label":"Run number", "value":data["run_number"]}]
|
||||
return {"rows":[tuple(v.values()) for v in rows.values()], "cols":columns, "metadata":[summary], "ref":viz_data.ref_map.get(data["prg"].name)}
|
||||
return {"rows":[tuple(v.values()) for v in rows.values()], "cols":columns, "metadata":[summary],"ref":viz_data.ref_map.get(data["prg"].profile_key)}
|
||||
|
||||
def print_data(data:dict) -> None:
|
||||
from tabulate import tabulate
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
import unittest, contextlib
|
||||
from tinygrad import Device, Tensor, Context, TinyJit
|
||||
from tinygrad.device import Compiled, ProfileProgramEvent, ProfileDeviceEvent
|
||||
from tinygrad.device import Compiled, ProfileProgramEvent
|
||||
from tinygrad.engine.realize import run_linear
|
||||
from tinygrad.codegen import to_program
|
||||
from tinygrad.viz.serve import load_amd_counters, VizData
|
||||
|
||||
@contextlib.contextmanager
|
||||
def save_sqtt():
|
||||
Device[Device.DEFAULT].synchronize()
|
||||
profile_start = len(Compiled.profile_events)
|
||||
data = VizData()
|
||||
yield data.ctxs
|
||||
Device[Device.DEFAULT].synchronize()
|
||||
Device[Device.DEFAULT]._at_profile_finalize()
|
||||
load_amd_counters(data, Compiled.profile_events)
|
||||
load_amd_counters(data, [e for e in Compiled.profile_events[:profile_start] if isinstance(e, ProfileProgramEvent)] +
|
||||
Compiled.profile_events[profile_start:])
|
||||
data.ctxs[:] = [r for r in data.ctxs if r["name"].startswith("SQTT")]
|
||||
|
||||
@unittest.skipUnless(Device.DEFAULT == "AMD", "only runs on AMD")
|
||||
class TestSQTTProfiler(unittest.TestCase):
|
||||
# TODO: can we enable SQTT profiling in context?
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if not Device[Device.DEFAULT].sqtt_enabled: raise unittest.SkipTest("device must be in SQTT profiling mode")
|
||||
|
||||
def setUp(self):
|
||||
Device[Device.DEFAULT].synchronize()
|
||||
Compiled.profile_events[:] = [e for e in Compiled.profile_events if isinstance(e, (ProfileProgramEvent, ProfileDeviceEvent))]
|
||||
|
||||
def test_simple(self):
|
||||
t = Tensor.empty(1) + 1
|
||||
with save_sqtt() as sqtt:
|
||||
|
||||
@@ -9,7 +9,7 @@ from extra.llama_kernels.swiglu import swiglu
|
||||
from extra.models.llama import apply_rotary_emb, precompute_freqs_cis
|
||||
from extra.thunder.amd.fa import custom_fused_qkv_rope_backward, fused_qkv_rope
|
||||
from test.helpers import needs_second_gpu, assert_kernel_count
|
||||
from test.backend.test_asm_gemm import has_hipcc
|
||||
from test.backend.test_asm_gemm import has_hipcc, is_cdna4
|
||||
|
||||
def run_fused_ce(bs:int, seqlen:int, vocab:int, label_smoothing:float=0.0) -> None:
|
||||
Tensor.manual_seed(0)
|
||||
@@ -129,7 +129,7 @@ class TestFusedQKVRoPE(unittest.TestCase):
|
||||
self.assertTrue(k.allclose(k_ref, atol=2e-2, rtol=0).item(), "K forward mismatch")
|
||||
self.assertTrue(v.allclose(v_ref, atol=0, rtol=0).item(), "V forward mismatch")
|
||||
|
||||
@unittest.skipUnless(has_hipcc(), "backward kernel requires hipcc to compile")
|
||||
@unittest.skipUnless(has_hipcc() and is_cdna4(), "backward kernel requires hipcc to compile")
|
||||
def test_llama31_8b(self):
|
||||
Tensor.manual_seed(1)
|
||||
B, N, H, H_KV, D = self.SHAPE
|
||||
|
||||
@@ -3,7 +3,7 @@ from tinygrad import Tensor, Device, nn, GlobalCounters, TinyJit, dtypes, Variab
|
||||
from tinygrad.uop.ops import Ops, UOp, AxisType, graph_rewrite
|
||||
from tinygrad.helpers import getenv, prod, Context
|
||||
from tinygrad.nn.state import get_parameters
|
||||
from tinygrad.engine.realize import run_linear, compile_linear, pm_beam, pm_compile
|
||||
from tinygrad.engine.realize import run_linear, compile_linear, lower_and_compile, pm_beam
|
||||
import numpy as np
|
||||
from hypothesis import given, strategies as strat, settings
|
||||
from test.helpers import not_support_multi_device, needs_second_gpu, slow, call_is_graph, check_schedule, assert_kernel_count, KernelCountException
|
||||
@@ -80,7 +80,7 @@ class TestMultiTensor(unittest.TestCase):
|
||||
cpu_2 = ("CPU:1", "CPU:2")
|
||||
src = Tensor.ones(16).shard(cpu_2, 0).realize()
|
||||
lin = UOp(Ops.LINEAR, src=(src.to(cpu_2[::-1]).schedule_linear().src[0],))
|
||||
with Context(BEAM=1, IGNORE_BEAM_CACHE=1): call = graph_rewrite(graph_rewrite(lin, pm_beam, ctx=1, walk=True), pm_compile, walk=True).src[0]
|
||||
with Context(BEAM=1, IGNORE_BEAM_CACHE=1): call = lower_and_compile(graph_rewrite(lin, pm_beam, ctx=1, walk=True)).src[0]
|
||||
self.assertNotEqual(call.src[0].src[0].arg.applied_opts, ())
|
||||
|
||||
def test_shard_same_device(self):
|
||||
|
||||
@@ -176,7 +176,7 @@ class TestLimitBufs(unittest.TestCase):
|
||||
|
||||
def test_limit_bufs_linear_scaling(self):
|
||||
def sched_time(n):
|
||||
with Context(TRACK_MATCH_STATS=0, DEBUG=0):
|
||||
with Context(TRACK_MATCH_STATS=0, DEBUG=0, PARALLEL=0):
|
||||
bufs = [Tensor.ones(16).contiguous().realize() for _ in range(4)]
|
||||
root = bufs[0]
|
||||
for i in range(n): root = root + bufs[i % 4]
|
||||
|
||||
+11
-10
@@ -2,7 +2,7 @@ from typing import Optional, Any
|
||||
import unittest, math
|
||||
import numpy as np
|
||||
from tinygrad.tensor import Tensor, _to_np_dtype
|
||||
from tinygrad.helpers import Context
|
||||
from tinygrad.helpers import Context, ceildiv
|
||||
from tinygrad.dtype import dtypes, DType, AddrSpace, ConstFloat # noqa: F401
|
||||
from tinygrad.device import Buffer, Device
|
||||
from tinygrad.uop.ops import Ops, UOp, KernelInfo, AxisType, buffers
|
||||
@@ -193,15 +193,16 @@ class TestLocalAccess(unittest.TestCase):
|
||||
@unittest.skipUnless(Device.DEFAULT == "WEBGPU", "Test local memory size for packed data types")
|
||||
def test_packed_smem_size(self):
|
||||
_dtypes = [dtypes.char, dtypes.uchar, dtypes.short, dtypes.ushort, dtypes.half]
|
||||
size = 16
|
||||
for dtype in _dtypes:
|
||||
temp = UOp.placeholder((size,), dtype, slot=0, addrspace=AddrSpace.LOCAL)
|
||||
uops = to_uops_list([temp], ren=Device[Device.DEFAULT].renderer)
|
||||
out = Device[Device.DEFAULT].renderer.render(uops)
|
||||
# half is supported in wgsl, so it doesn't have to be packed
|
||||
corrected_size = size//(4//dtype.itemsize) if dtype != dtypes.half else size
|
||||
# temp0: array<{Device[Device.DEFAULT].renderer.buf_map(dtype)},{corrected_size}>;
|
||||
self.assertIn(f",{corrected_size}>;", out)
|
||||
# a partial word still needs a whole word, so sizes that don't fill one must round up
|
||||
for size in (16, 5):
|
||||
for dtype in _dtypes:
|
||||
temp = UOp.placeholder((size,), dtype, slot=0, addrspace=AddrSpace.LOCAL)
|
||||
uops = to_uops_list([temp], ren=Device[Device.DEFAULT].renderer)
|
||||
out = Device[Device.DEFAULT].renderer.render(uops)
|
||||
# half is supported in wgsl, so it doesn't have to be packed
|
||||
corrected_size = ceildiv(size, 4//dtype.itemsize) if dtype != dtypes.half else size
|
||||
# temp0: array<{Device[Device.DEFAULT].renderer.buf_map(dtype)},{corrected_size}>;
|
||||
self.assertIn(f",{corrected_size}>;", out)
|
||||
|
||||
@unittest.skipUnless(Device[Device.DEFAULT].renderer.has_shared, "test requires shared memory")
|
||||
@unittest.skip("tinygrad doesn't support this behavior")
|
||||
|
||||
+12
-5
@@ -160,7 +160,7 @@ class MockUSB3:
|
||||
elif request == 0xE5:
|
||||
self.state._xram_write_byte(value, index)
|
||||
elif request == 0xF2:
|
||||
op = ("sram_read" if value & 0x8000 else "sram_write", 0xF000, (value & 0x7FFF) * 512)
|
||||
op = ("sram_read" if value & 0x8000 else "sram_write", 0xF000 + (index & 0xFF) * 0x4000, (value & 0x7FFF) * 512)
|
||||
if value & 0x8000: self._bulk_read_op = op
|
||||
else: self._bulk_write_op = op
|
||||
elif request == 0xF0:
|
||||
@@ -193,19 +193,26 @@ class MockUSB3:
|
||||
op, address, size = self._bulk_write_op
|
||||
assert len(data) == size
|
||||
if op == "sram_write":
|
||||
host_addr, region_size = self.state._dma_regions[address]
|
||||
ctypes.memmove(host_addr, data, min(len(data), region_size))
|
||||
ctrl, (host_addr, region_size) = next((ca, r) for ca, r in self.state._dma_regions.items() if ca <= address < ca + r[1])
|
||||
ctypes.memmove(host_addr + (address - ctrl), data, min(len(data), region_size - (address - ctrl)))
|
||||
self.state.driver._emulate_execute() # landed data may un-stall a ring polling on it (e.g. copyin sentinels)
|
||||
elif op == "pcie_write": self.state._pcie_write(address, data)
|
||||
else: raise RuntimeError(f"cannot bulk write for {op}")
|
||||
self._bulk_write_op = None
|
||||
|
||||
def bulk_write_async(self, payload:memoryview, timeout:int=10000) -> int: # the mock completes transfers synchronously
|
||||
self.bulk_write(bytes(payload), timeout)
|
||||
return 0
|
||||
|
||||
def bulk_wait(self, tag:int): pass
|
||||
|
||||
def bulk_read(self, length:int, timeout:int=1000) -> memoryview:
|
||||
assert self._bulk_read_op is not None
|
||||
op, address, size = self._bulk_read_op
|
||||
assert length == size
|
||||
if op == "sram_read":
|
||||
host_addr, region_size = self.state._dma_regions[address]
|
||||
data = bytes((ctypes.c_ubyte * min(length, region_size)).from_address(host_addr))
|
||||
ctrl, (host_addr, region_size) = next((ca, r) for ca, r in self.state._dma_regions.items() if ca <= address < ca + r[1])
|
||||
data = bytes((ctypes.c_ubyte * min(length, region_size - (address - ctrl))).from_address(host_addr + (address - ctrl)))
|
||||
elif op == "pcie_read": data = self.state._pcie_read(address, length)
|
||||
else: raise RuntimeError(f"cannot bulk read for {op}")
|
||||
self._bulk_read_op = None
|
||||
|
||||
@@ -5,7 +5,7 @@ from tinygrad.tensor import Tensor
|
||||
from tinygrad.helpers import Timing, Context, cdiv
|
||||
from tinygrad.dtype import dtypes, AddrSpace, ConstFloat, Invalid # noqa: F401
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.uop.ops import Ops, ParamArg, PatternMatcher, UOp, UPat, dtype_from_uop, exec_alu, graph_rewrite # noqa: F401 # ParamArg used by eval(str(uop)) roundtrip tests
|
||||
from tinygrad.uop.ops import Ops, AxisType, ParamArg, PatternMatcher, UOp, UPat, dtype_from_uop, exec_alu, graph_rewrite # noqa: F401 # ParamArg used by eval(str(uop)) roundtrip tests
|
||||
from tinygrad.uop.weak import pm_lower_index_dtype
|
||||
from tinygrad.uop.spec import spec_program, spec_shared, type_verify
|
||||
from tinygrad.uop.symbolic import sym, pm_remove_invalid
|
||||
@@ -457,6 +457,14 @@ class TestUopsObject(unittest.TestCase):
|
||||
self.assertEqual(a.device, Device.DEFAULT)
|
||||
|
||||
class TestUOpRender(unittest.TestCase):
|
||||
def test_render_ssimplified_marg_outside_toposort(self):
|
||||
r = UOp.range(UOp.const(16, dtypes.int), 2, AxisType.WEAK, dtype=dtypes.int)
|
||||
offset = (r * 2) + (r * 2)
|
||||
shrink = UOp(Ops.SHRINK, src=(UOp.param(0, dtypes.uint, (32,)), offset, UOp.const(2, dtypes.int)))
|
||||
self.assertIsNot(shrink.src[1], shrink.marg[0][0])
|
||||
self.assertEqual(shrink.render(simplify=False), "p0.shrink((((r2*4), 2),))")
|
||||
self.assertEqual(UOp.range(1, 0, src=(shrink,), dtype=dtypes.int).render(simplify=False), "r0")
|
||||
|
||||
def test_render_vectorize_empty(self):
|
||||
u = UOp(Ops.STACK, dtype=dtypes.void, src=())
|
||||
self.assertEqual(u.render(simplify=False), "{}")
|
||||
|
||||
+22
-8
@@ -1,5 +1,5 @@
|
||||
import unittest, decimal, sys, json, contextlib, tempfile, pickle, io, math
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
import decimal, sys, json, contextlib, tempfile, pickle, io, math, pathlib
|
||||
from dataclasses import dataclass
|
||||
from typing import Generator
|
||||
|
||||
@@ -43,7 +43,7 @@ def save_viz():
|
||||
Buffer.profile_events.clear()
|
||||
cpu_events.clear()
|
||||
viz = VizTrace()
|
||||
with Context(VIZ=-1, TRACK_MATCH_STATS=2, PROFILE=1):
|
||||
with Context(VIZ=-1, TRACK_MATCH_STATS=2, PROFILE=1, PARALLEL=0):
|
||||
yield viz
|
||||
viz.set_data()
|
||||
|
||||
@@ -516,6 +516,22 @@ class TestVizIntegration(unittest.TestCase):
|
||||
src_render = get_render(viz.data, steps[src_idx]["query"])["src"]
|
||||
self.assertEqual(src, src_render)
|
||||
|
||||
def test_profiler_duplicate_name(self):
|
||||
kernel_name = "duplicate_name"
|
||||
def one(A:UOp): return A[0].store(UOp.const(1.0, dtypes.float)).sink(arg=KernelInfo(kernel_name))
|
||||
def zero(A:UOp): return A[0].store(UOp.const(0.0, dtypes.float)).sink(arg=KernelInfo(kernel_name))
|
||||
with save_viz() as viz:
|
||||
@TinyJit
|
||||
def f(a:Tensor, b:Tensor): return Tensor.custom_kernel(a, fxn=one)[0], Tensor.custom_kernel(b, fxn=zero)[0]
|
||||
a, b = Tensor.empty(4, device="NULL"), Tensor.empty(4, device="NULL")
|
||||
# warmup
|
||||
for _ in range(2): Tensor.realize(*f(a, b))
|
||||
Tensor.realize(*f(a, b))
|
||||
kernels = {i for i,c in enumerate(viz.list_items()) if c["name"] == kernel_name}
|
||||
profile = decode_profile(unwrap(get_profile(viz.data, cpu_events)))
|
||||
events = [e for e in profile["layout"]["NULL"]["events"] if e["name"] == kernel_name]
|
||||
self.assertEqual({e["ref"] for e in events}, kernels)
|
||||
|
||||
from tinygrad.device import ProfileDeviceEvent, ProfileGraphEvent, ProfileGraphEntry
|
||||
from tinygrad.viz.serve import get_profile
|
||||
from tinygrad.viz.cli import decode_profile
|
||||
@@ -819,8 +835,6 @@ from extra.gemm.amd_asm_matmul import Kernel
|
||||
|
||||
@needs_tracked_pm
|
||||
class TestCfg(unittest.TestCase):
|
||||
def setUp(self): self.arch = "gfx1100"
|
||||
|
||||
def get_cfg(self, name:str, k:Kernel):
|
||||
insts = k.finalize()
|
||||
def fxn(out:UOp) -> UOp:
|
||||
@@ -829,7 +843,7 @@ class TestCfg(unittest.TestCase):
|
||||
sink = UOp.sink(out.base, lidx, gidx, arg=KernelInfo(name=name))
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=x) for x in insts]))))
|
||||
with save_viz() as viz:
|
||||
with Context(DEV=f"NULL::{self.arch}"):
|
||||
with Context(DEV="NULL::gfx1100"):
|
||||
out = Tensor.custom_kernel(Tensor.empty(1), fxn=fxn)[0]
|
||||
_ = do_to_program(out.schedule_linear().src[-1].src[0], Device[out.device].renderer)
|
||||
codegen_rewrites = next(s for s in viz.list_items() if s["name"] == name)
|
||||
@@ -1011,8 +1025,8 @@ def run_cli(*cli_args) -> list[dict]:
|
||||
@contextlib.contextmanager
|
||||
def write_files(viz) -> list[str]:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
(r:=Path(tmpdir)/"rewrites.pkl").write_bytes(pickle.dumps(viz.data.trace))
|
||||
(p:=Path(tmpdir)/"profile.pkl").write_bytes(pickle.dumps(cpu_events))
|
||||
(r:=pathlib.Path(tmpdir)/"rewrites.pkl").write_bytes(pickle.dumps(viz.data.trace))
|
||||
(p:=pathlib.Path(tmpdir)/"profile.pkl").write_bytes(pickle.dumps(cpu_events))
|
||||
yield ["--rewrites-path", str(r), "--profile-path", str(p)]
|
||||
|
||||
class TestCLI(unittest.TestCase):
|
||||
|
||||
@@ -224,6 +224,21 @@ class TestCallSchedule(unittest.TestCase):
|
||||
np.testing.assert_equal(x.numpy(), [2, 2, 2])
|
||||
np.testing.assert_equal(y.numpy(), [3, 3, 3])
|
||||
|
||||
def test_precompile_nested_scope_collision(self):
|
||||
# a precompiled function body gets its own positional p{slot} params; they must not be renumbered when the call is
|
||||
# scheduled inside an enclosing realize with a different slot ordering. the store must use this call's Variable
|
||||
cache = Tensor.zeros(16)
|
||||
@function(precompile=True, allow_implicit=True)
|
||||
def store(x:Tensor, sp:UOp) -> Tensor:
|
||||
# update a cache at a symbolic offset, like an attention KV cache update
|
||||
return Tensor(cache.uop.after(cache[sp:sp+x.shape[0]].uop.store(x.uop)))[:sp+x.shape[0]].sum()
|
||||
sp_v, nt_v = UOp.variable("sp", 0, 8), UOp.variable("nt", 1, 8)
|
||||
t = Tensor.arange(16).float().realize()
|
||||
sp, nt = sp_v.bind(0), nt_v.bind(8)
|
||||
store(t[sp:sp+nt].clone().realize(), sp).realize()
|
||||
np.testing.assert_equal(cache.numpy()[:8], t[:8].numpy())
|
||||
np.testing.assert_equal(cache.numpy()[8:], np.zeros(8))
|
||||
|
||||
def test_precompile_schedule_cache_hit(self):
|
||||
"""two instances of the same @function should produce identical function body keys (schedule cache hit)"""
|
||||
@function(precompile=True)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import unittest
|
||||
import numpy as np
|
||||
from unittest.mock import patch
|
||||
from tinygrad import Tensor, UOp
|
||||
from tinygrad.nn.state import get_state_dict
|
||||
from tinygrad.schedule import schedule_cache
|
||||
from tinygrad.llm.model import Transformer, TransformerConfig
|
||||
from tinygrad.llm.serve import StreamRouter
|
||||
@@ -152,6 +154,22 @@ class TestTransformerGenerate(unittest.TestCase):
|
||||
# 4 tokens, chunk_size=4 -> 1 prefill chunk
|
||||
self.assertEqual(get_prefill_flags(list(range(4)), 4), [True, False, False])
|
||||
|
||||
def test_chunked_prefill_kv_cache_matches_single_chunk(self):
|
||||
config = TransformerConfig(num_blocks=1, dim=8, hidden_dim=16, n_heads=1, n_kv_heads=1, norm_eps=1e-5,
|
||||
vocab_size=32, head_dim=4, rope_theta=1000000, rope_dim=4, qk_norm=4, v_head_dim=4, max_context=16)
|
||||
def model():
|
||||
m = Transformer(config)
|
||||
rng = np.random.RandomState(1234)
|
||||
for t in get_state_dict(m).values():
|
||||
t.assign(Tensor(rng.uniform(-1, 1, t.shape).astype(np.float32))).realize()
|
||||
return m
|
||||
def prefill(m, chunk_size):
|
||||
gen = m.generate(list(range(1, 9)), chunk_size=chunk_size, temperature=0.0)
|
||||
next(gen)
|
||||
return [b.cache_kv.numpy() for b in m.blk]
|
||||
for g, r in zip(prefill(model(), 4), prefill(model(), 8)):
|
||||
np.testing.assert_allclose(g[:, :, :, :8, :], r[:, :, :, :8, :], atol=1e-5)
|
||||
|
||||
def test_kv_cache_resume_matches_fresh(self):
|
||||
model = Transformer(TEST_CONFIG)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from dataclasses import replace, dataclass
|
||||
import itertools, functools
|
||||
from tinygrad.helpers import DISABLE_FAST_IDIV, TRANSCENDENTAL, SPEC, DEBUG, VIZ, IMAGE, NOOPT, EMULATED_DTYPES, NOLOCALS, USE_TC
|
||||
from tinygrad.helpers import ALLOW_TF32, DEFAULT_FLOAT, DEFAULT_INT, TracingKey, Context, panic
|
||||
from tinygrad.helpers import ALLOW_TF32, DEFAULT_FLOAT, DEFAULT_INT, NUM_CPU_THREADS, TC_SELECT, TC_OPT, TracingKey, Context, panic
|
||||
from tinygrad.uop.ops import PatternMatcher, graph_rewrite, UOp, Ops, UPat, rewrite_group, KernelInfo, ProgramInfo, GroupOp, AxisType
|
||||
from tinygrad.uop.weak import pm_lower_index_dtype, pm_commit_weak, pm_cast_weak
|
||||
from tinygrad.uop.render import pyrender
|
||||
@@ -377,6 +377,10 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
|
||||
pm_final_rewrite = pm_commit_weak+pm_cast_weak+pm_decomp+extra_matcher+pm_split_ends
|
||||
sink = graph_rewrite(sink, pm_final_rewrite+pm_remove_invalid, ctx=ren, name="final rewrite")
|
||||
|
||||
# spell every literal as a casted const CAST(dt, CONST(value))
|
||||
# TODO: remove once consts are always weak
|
||||
sink = graph_rewrite(sink, pm_casted_consts, name="casted consts", walk=True)
|
||||
|
||||
# add implicit barriers (stores/loads through LOCAL memory ordered by AFTER or across loop iterations need workgroup barriers)
|
||||
sink = graph_rewrite(sink, pm_implicit_barriers, name="add implicit barriers")
|
||||
|
||||
@@ -387,10 +391,6 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
|
||||
num_params = len([x for x in sink.toposort() if x.op is Ops.PARAM and x.arg.slot != -1])
|
||||
sink = graph_rewrite(sink, pm_number_params, ctx=[num_params], name="number params with -1", walk=True)
|
||||
|
||||
# spell every literal as a casted const CAST(dt, CONST(value))
|
||||
# TODO: remove once consts are always weak
|
||||
sink = graph_rewrite(sink, pm_casted_consts, name="casted consts", walk=True)
|
||||
|
||||
if VIZ: graph_rewrite(sink, PatternMatcher([]), name="View Output AST")
|
||||
if SPEC: type_verify(sink, spec_program)
|
||||
|
||||
@@ -459,7 +459,7 @@ pm_to_program = PatternMatcher([
|
||||
(UPat(Ops.PROGRAM, src=(UPat(), UPat(Ops.LINEAR), UPat(Ops.SOURCE, name="source")), name="prg"), do_compile),
|
||||
])
|
||||
|
||||
@rewrite_group(name=lambda ast,renderer,ret,**kwargs: TracingKey(ret.src[0].arg.name,(ret.src[0].arg.function_name, ast), ret=renderer), replay=True)
|
||||
@rewrite_group(name=lambda ast,renderer,ret,**_: TracingKey((k:=ret.src[0].arg).name,(k.function_name, ast, ret.key),ret=renderer), replay=True)
|
||||
@Context(ALLOW_DEVICE_USAGE=0)
|
||||
def do_to_program(ast:UOp, renderer:Renderer) -> UOp:
|
||||
"""
|
||||
@@ -488,9 +488,14 @@ def do_to_program(ast:UOp, renderer:Renderer) -> UOp:
|
||||
if VIZ: graph_rewrite(prg, PatternMatcher([]), name="View Program")
|
||||
return prg
|
||||
|
||||
# config affects generated programs and cache keys; context also carries compile-only behavior to workers
|
||||
to_program_config = (NOOPT, EMULATED_DTYPES, NOLOCALS, USE_TC, IMAGE, DISABLE_FAST_IDIV, TRANSCENDENTAL, ALLOW_TF32,
|
||||
DEFAULT_FLOAT, DEFAULT_INT, NUM_CPU_THREADS, TC_SELECT, TC_OPT)
|
||||
to_program_context = (*to_program_config, SPEC, DEBUG)
|
||||
def to_program_key(ast:UOp, renderer:Renderer) -> tuple:
|
||||
return (ast.key, type(renderer), renderer.target, *[x.value for x in to_program_config])
|
||||
|
||||
to_program_cache: dict[tuple, UOp] = {}
|
||||
def to_program(ast:UOp, renderer:Renderer) -> UOp:
|
||||
config = (NOOPT, EMULATED_DTYPES, NOLOCALS, USE_TC, IMAGE, DISABLE_FAST_IDIV, TRANSCENDENTAL, ALLOW_TF32, DEFAULT_FLOAT, DEFAULT_INT)
|
||||
key = (ast.key, type(renderer), renderer.target, *[x.value for x in config])
|
||||
if (prg:=to_program_cache.get(key)) is None: to_program_cache[key] = prg = do_to_program(ast, renderer)
|
||||
if (prg:=to_program_cache.get(key:=to_program_key(ast, renderer))) is None: to_program_cache[key] = prg = do_to_program(ast, renderer)
|
||||
return prg
|
||||
|
||||
@@ -128,6 +128,6 @@ def get_late_rewrite_patterns(ops:tuple[Ops, ...], disable_fast_idiv:bool) -> Pa
|
||||
if Ops.SHL in ops: pat += [(UPat.var('x').alu(Ops.SHL, UPat.cvar('n'))+UPat.var('c'), lambda x,n,c: x.alu(Ops.MULACC, x.const_like(1<<n.val), c))]
|
||||
# some backends emit FDIV for RECIP, in that case: a*(1/b) -> a/b
|
||||
if Ops.FDIV in ops:
|
||||
pat += [(UPat.var("x").reciprocal(), lambda x: x.const_like(1).alu(Ops.FDIV, x))]
|
||||
pat += [(UPat.var("a", dtypes.floats) * UPat(Ops.FDIV, dtypes.floats, src=(UPat.const(1), UPat.var("b"))), lambda a,b: a.alu(Ops.FDIV, b))]
|
||||
pat += [(UPat.var("x").reciprocal(), lambda x: UOp.const(1.0).alu(Ops.FDIV, x))]
|
||||
pat += [(UPat.var("a") * UPat(Ops.FDIV, dtypes.floats, src=(UPat.const(1), UPat.var("b"))), lambda a,b: a.alu(Ops.FDIV, b))]
|
||||
return PatternMatcher(pat)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import math, time, multiprocessing, traceback, signal, atexit
|
||||
import math, time, traceback, signal
|
||||
from dataclasses import replace
|
||||
from tinygrad.uop.ops import sym_infer, AxisType, UOp, Ops
|
||||
from tinygrad.uop.render import pyrender
|
||||
from tinygrad.device import Device, Buffer
|
||||
from tinygrad.helpers import prod, flatten, DEBUG, CACHELEVEL, diskcache_get, diskcache_put, getenv, Context, colored, time_to_str
|
||||
from tinygrad.helpers import prod, flatten, DEBUG, CACHELEVEL, diskcache_get, diskcache_put, getenv, colored, time_to_str
|
||||
from tinygrad.helpers import IGNORE_BEAM_CACHE
|
||||
from tinygrad.codegen.opt import Opt, OptOps, KernelOptError
|
||||
from tinygrad.engine.realize import time_call
|
||||
from tinygrad.engine.worker import get_worker_pool, terminate_worker_pool
|
||||
from tinygrad.codegen import to_program
|
||||
from tinygrad.codegen.opt.postrange import Scheduler
|
||||
|
||||
@@ -78,11 +79,6 @@ def _try_compile(x:tuple[int,Scheduler]) -> tuple[int, tuple[UOp, float]|None]:
|
||||
if hasattr(signal, "alarm"): signal.alarm(0)
|
||||
return x[0], ret
|
||||
|
||||
# workers should not open devices and should ignore ctrl c and should not launch VIZ
|
||||
def _init_worker():
|
||||
Context(ALLOW_DEVICE_USAGE=0, VIZ=0, TRACK_MATCH_STATS=0).__enter__()
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
def _ensure_buffer_alloc(bufs:list[Buffer]) -> list[Buffer]: return [buf.ensure_allocated() if buf is not None else buf for buf in bufs]
|
||||
|
||||
# *** external API ***
|
||||
@@ -111,9 +107,8 @@ def get_kernel_actions(s:Scheduler, include_0=True, max_up:int|None=None) -> dic
|
||||
except KernelOptError: pass
|
||||
return acted
|
||||
|
||||
beam_pool, BEAM_DEBUG = None, getenv("BEAM_DEBUG")
|
||||
BEAM_DEBUG = getenv("BEAM_DEBUG")
|
||||
def beam_search(s:Scheduler, rawbufs:list[Buffer], var_vals:dict[str,int], amt:int, allow_test_size=True, disable_cache=IGNORE_BEAM_CACHE.value):
|
||||
global beam_pool
|
||||
key = {"ast": s.ast.key, "amt": amt, "allow_test_size": allow_test_size, "device": s.ren.target.device, "suffix": s.ren.suffix}
|
||||
if not disable_cache and CACHELEVEL >= 1 and (val:=diskcache_get("beam_search", key)) is not None:
|
||||
ret = s.copy()
|
||||
@@ -123,11 +118,7 @@ def beam_search(s:Scheduler, rawbufs:list[Buffer], var_vals:dict[str,int], amt:i
|
||||
beam: list[tuple[Scheduler, float]] = [(s, float("inf"))]
|
||||
seen_libs = set()
|
||||
|
||||
default_parallel = multiprocessing.cpu_count() if s.ren.target.device in {"CUDA", "AMD", "NV", "METAL", "HIP"} else 0
|
||||
if beam_pool is None and (workers := getenv("PARALLEL", default_parallel)):
|
||||
beam_pool = multiprocessing.get_context("spawn").Pool(workers, _init_worker, (), getenv("BEAM_MAX_TASKS_PER_CHILD", 16))
|
||||
@atexit.register
|
||||
def close_pool(): beam_pool.close()
|
||||
pool = get_worker_pool()
|
||||
|
||||
min_progress = getenv("BEAM_MIN_PROGRESS", 0.01)/1e6
|
||||
if BEAM_DEBUG:
|
||||
@@ -143,7 +134,7 @@ def beam_search(s:Scheduler, rawbufs:list[Buffer], var_vals:dict[str,int], amt:i
|
||||
candidates: list[Scheduler] = flatten([get_kernel_actions(si, include_0=False).values() for si,_ in beam])
|
||||
timed: list[tuple[Scheduler, float]] = []
|
||||
least_compute_ops = math.inf
|
||||
for i, proc in ((map if beam_pool is None else beam_pool.imap_unordered)(_try_compile, enumerate(candidates))):
|
||||
for i, proc in ((map if pool is None else pool.imap_unordered)(_try_compile, enumerate(candidates))):
|
||||
if proc is None: continue
|
||||
prg, compile_et = proc
|
||||
if (lib:=prg.src[3].arg) in seen_libs: continue
|
||||
@@ -179,7 +170,7 @@ def beam_search(s:Scheduler, rawbufs:list[Buffer], var_vals:dict[str,int], amt:i
|
||||
print(f"\r{time.perf_counter() - st:7.2f}s:", colored(time_to_str(beam[0][1], w=12), "green" if exiting else None),
|
||||
f"from {len(candidates):3d} -> {len(opts):3d} actions\033[K", beam[0][0].colored_shape())
|
||||
except KeyboardInterrupt as e:
|
||||
if beam_pool is not None: beam_pool.terminate()
|
||||
terminate_worker_pool()
|
||||
raise e
|
||||
|
||||
if CACHELEVEL >= 1: diskcache_put("beam_search", key, beam[0][0].applied_opts)
|
||||
|
||||
+3
-2
@@ -66,10 +66,10 @@ def canonicalize_device(device:str|tuple|list|None) -> str|tuple[str, ...]:
|
||||
class ProfileDeviceEvent(ProfileEvent): device:str; tdiff:decimal.Decimal=decimal.Decimal(0); props:dict[str,Any]|None=None # noqa: E702
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProfileProgramEvent(ProfileEvent): device:str; name:str; lib:bytes|None; base:int|None; tag:int|None=None # noqa: E702
|
||||
class ProfileProgramEvent(ProfileEvent): device:str; name:str; lib:bytes|None; base:int|None; tag:int|None=None; profile_key:bytes|None=None # noqa: E702
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProfileGraphEntry: device:str; name:str|TracingKey; st_id:int; en_id:int # noqa: E702
|
||||
class ProfileGraphEntry: device:str; name:str|TracingKey; st_id:int; en_id:int; profile_key:bytes|None=None # noqa: E702
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProfileGraphEvent(ProfileEvent): ents:list[ProfileGraphEntry]; deps:list[list[int]]; sigs:list[decimal.Decimal] # noqa: E702
|
||||
@@ -326,6 +326,7 @@ class TinyELF:
|
||||
target: Target
|
||||
# tuple of (name, slot, dtype, shape)
|
||||
signature: tuple[tuple[str|None, int, DType, tuple], ...]
|
||||
profile_key: bytes|None = None
|
||||
|
||||
@staticmethod
|
||||
def iter_sig(signature:tuple[tuple[str|None, int, DType, tuple], ...], offset:int=0) -> Generator[tuple[int, DType], None, None]:
|
||||
|
||||
+46
-11
@@ -2,14 +2,15 @@ from __future__ import annotations
|
||||
from typing import cast, Iterator, Any, Sequence
|
||||
import random, itertools, math, weakref, array, decimal
|
||||
from dataclasses import dataclass, replace, field
|
||||
from tinygrad.helpers import colored, DEBUG, GlobalCounters, ansilen, all_int, prod, flatten, Context, getenv, to_tuple
|
||||
from tinygrad.helpers import colored, DEBUG, GlobalCounters, ansipad, all_int, prod, flatten, Context, getenv, to_tuple, tqdm
|
||||
from tinygrad.helpers import BEAM, size_to_str, time_to_str, VALIDATE_WITH_CPU, PROFILE, ProfilePointEvent, cpu_events, perf_counter_us
|
||||
from tinygrad.uop.ops import Ops, PatternMatcher, UOp, UPat, AxisType, sym_infer, graph_rewrite
|
||||
from tinygrad.uop.ops import Ops, PatternMatcher, UOp, UPat, AxisType, sym_infer, graph_rewrite, ProgramInfo
|
||||
from tinygrad.device import Device, Buffer, MultiBuffer, ProfileGraphEntry
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.renderer import Estimates
|
||||
from tinygrad.codegen import to_program
|
||||
from tinygrad.renderer import Estimates, Renderer
|
||||
from tinygrad.codegen import to_program, to_program_cache, to_program_key, to_program_context
|
||||
from tinygrad.codegen.opt.postrange import args_from_ast
|
||||
from tinygrad.engine.worker import get_worker_pool, terminate_worker_pool
|
||||
|
||||
# **************** Helpers ****************
|
||||
|
||||
@@ -89,7 +90,7 @@ def track_stats(ctx:ExecContext, call:UOp, st:decimal.Decimal, ets:list[float|No
|
||||
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 {len(bufs):2d} mem {GlobalCounters.mem_used/1e9:6.2f} GB"+
|
||||
f" {ansipad(display_name, 46)} 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})"))
|
||||
first_run_cache.add(kcall.src[0].key)
|
||||
|
||||
@@ -221,7 +222,7 @@ def exec_hcq(ctx:ExecContext, call:UOp, ast:UOp) -> list[float|None]:
|
||||
exec_kernel(replace(ctx, var_vals={**ctx.var_vals, "hcq_inputs_ptr": dev.rt_buffer()._buf.va_addr + base}), call, ast)
|
||||
|
||||
def _prof_tm(device:str, stat_call:UOp, prof:tuple[int, ...]) -> float|None:
|
||||
(d:=cast(Any, Device[device])).prof_ents[prof[0]] = ProfileGraphEntry(device, stat_call.arg.name, *prof)
|
||||
(d:=cast(Any, Device[device])).prof_ents[prof[0]] = ProfileGraphEntry(device, stat_call.arg.name, prof[0], prof[1], stat_call.key)
|
||||
if not ctx.wait: return None
|
||||
d.synchronize(timeout=ctx.timeout)
|
||||
st, en = (d.signal(x)._buf.cpu_view().view(fmt='Q')[0] for x in prof)
|
||||
@@ -247,10 +248,44 @@ pm_beam = PatternMatcher([
|
||||
lambda ctx,call,sink: call.replace(src=(sink.replace(arg=replace(sink.arg, beam=ctx)), *call.src[1:])) if sink.arg.beam == 0 else None),
|
||||
])
|
||||
|
||||
pm_compile = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat((Ops.SINK, Ops.PROGRAM), name="ast"),), name="call", allow_any_len=True), lambda call,ast:
|
||||
call.replace(src=(to_program(ast, Device[call.device if isinstance(call.device, str) else call.device[0]].renderer), *call.src[1:]))),
|
||||
])
|
||||
# **************** parallel lowering + compilation ****************
|
||||
|
||||
def _compile_kernel(x:tuple[int, tuple[UOp, Renderer], dict]) -> tuple[int, UOp]:
|
||||
with Context(**x[2]): return x[0], to_program(*x[1])
|
||||
|
||||
def _needs_compile(c:UOp) -> bool:
|
||||
if c.op is not Ops.CALL: return False
|
||||
if c.src[0].op is Ops.SINK: return True
|
||||
# a PROGRAM with a ProgramInfo and a BINARY is already compiled
|
||||
return c.src[0].op is Ops.PROGRAM and not (isinstance(c.src[0].arg, ProgramInfo) and c.src[0].src[-1].op is Ops.BINARY)
|
||||
|
||||
def lower_and_compile(linear:UOp) -> UOp:
|
||||
# collect the kernels to lower and compile, deduped by their compile cache key
|
||||
calls = [c for c in linear.toposort() if _needs_compile(c)]
|
||||
rens = {c: Device[c.device if isinstance(c.device, str) else c.device[0]].renderer for c in calls}
|
||||
keys = {c: to_program_key(c.src[0], rens[c]) for c in calls}
|
||||
if not len(calls): return linear
|
||||
|
||||
# lower and compile what's not cached, in parallel if there's a worker pool
|
||||
todo = list({keys[c]: (c.src[0], rens[c]) for c in calls if keys[c] not in to_program_cache}.items())
|
||||
if len(todo):
|
||||
# kernels that beam search must compile in the parent, beam needs device access to time candidates
|
||||
|
||||
pool = None if len(todo) == 1 or any(getattr(c.src[0].arg, "beam", 0) for c in calls) else get_worker_pool()
|
||||
ctx = {v.key: v.value for v in to_program_context}
|
||||
tasks = ((i, ast_ren, ctx) for i, (_, ast_ren) in enumerate(todo))
|
||||
try:
|
||||
with tqdm(total=len(todo), desc="compiling", disable=DEBUG<1) as pbar:
|
||||
for i, prg in (map if pool is None else pool.imap_unordered)(_compile_kernel, tasks):
|
||||
pbar.set_description(f"compiling {ansipad(prg.src[0].arg.name, 40)}")
|
||||
to_program_cache[todo[i][0]] = prg
|
||||
pbar.update(1)
|
||||
except KeyboardInterrupt:
|
||||
if pool is not None: terminate_worker_pool()
|
||||
raise
|
||||
|
||||
# swap the compiled PROGRAMs into the calls
|
||||
return linear.substitute({c: c.replace(src=(to_program_cache[keys[c]], *c.src[1:])) for c in calls}, name="precompile kernels")
|
||||
|
||||
pm_optimize_local_size = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.PROGRAM, name="prg"),), name="call", allow_any_len=True), optimize_local_size),
|
||||
@@ -270,7 +305,7 @@ if getenv("HCQ2"): from tinygrad.runtime.support.hcq2 import hcq_compile, hcq_li
|
||||
def compile_linear(linear:UOp, beam:int|None=None, validate=False, input_uops:list[UOp]|None=None, profile:bool|None=None) -> UOp:
|
||||
if validate: linear = graph_rewrite(linear, pm_validate, name="validate", walk=True)
|
||||
if (beam_val:=BEAM.value if beam is None else beam) >= 1: linear = graph_rewrite(linear, pm_beam, ctx=beam_val, walk=True)
|
||||
linear = graph_rewrite(linear, pm_compile, name="precompile kernels", walk=True)
|
||||
linear = lower_and_compile(linear)
|
||||
linear = graph_rewrite(linear, pm_optimize_local_size, name="optimize local size", walk=True)
|
||||
if getenv("HCQ2"): linear = hcq_compile(linear, input_uops, bool(PROFILE or DEBUG >= 2) if profile is None else profile)
|
||||
return linear
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import multiprocessing, atexit, signal, sys, threading, contextlib
|
||||
from multiprocessing.context import SpawnContext, SpawnProcess
|
||||
from tinygrad.helpers import Context, getenv, PARALLEL
|
||||
|
||||
# generic pool of worker processes for parallel compilation, shared by kernel lowering and BEAM search
|
||||
|
||||
# workers should not open devices and should ignore ctrl c and should not launch VIZ
|
||||
def _init_worker():
|
||||
Context(ALLOW_DEVICE_USAGE=0, VIZ=0, TRACK_MATCH_STATS=0).__enter__()
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
# spawn normally reimports the user's __main__ before _init_worker. This replays top-level code and can recursively create pools. There is no public
|
||||
# multiprocessing switch to skip that import, so hide the two attributes used to locate __main__ while each worker (including replacements) starts.
|
||||
_spawn_lock, _missing = threading.Lock(), object()
|
||||
@contextlib.contextmanager
|
||||
def _without_main():
|
||||
main = sys.modules.get("__main__")
|
||||
if main is None:
|
||||
yield
|
||||
return
|
||||
with _spawn_lock:
|
||||
saved = {name:getattr(main, name, _missing) for name in ("__file__", "__spec__")}
|
||||
try:
|
||||
for name in saved: setattr(main, name, None)
|
||||
yield
|
||||
finally:
|
||||
for name,value in saved.items(): delattr(main, name) if value is _missing else setattr(main, name, value)
|
||||
|
||||
class _WorkerProcess(SpawnProcess):
|
||||
@staticmethod
|
||||
def _Popen(process_obj):
|
||||
with _without_main(): return SpawnProcess._Popen(process_obj)
|
||||
|
||||
class _WorkerContext(SpawnContext): Process = _WorkerProcess
|
||||
|
||||
worker_pool = None
|
||||
def get_worker_pool():
|
||||
global worker_pool
|
||||
if multiprocessing.current_process().daemon or PARALLEL == 0: return None
|
||||
if worker_pool is None:
|
||||
worker_pool = _WorkerContext().Pool(PARALLEL.value, _init_worker, (), getenv("BEAM_MAX_TASKS_PER_CHILD", 16))
|
||||
@atexit.register
|
||||
def close_pool(pool=worker_pool): pool.close()
|
||||
return worker_pool
|
||||
|
||||
def terminate_worker_pool():
|
||||
global worker_pool
|
||||
if worker_pool is not None: worker_pool.terminate()
|
||||
worker_pool = None
|
||||
+24
-6
@@ -44,6 +44,7 @@ def time_to_str(t:float, w=8) -> str: return next((f"{t * d:{w}.2f}{pr}" for d,p
|
||||
def size_to_str(s:int) -> str: return next((f"{s / d:.2f} {pr}" for d,pr in [(1<<30, "GB"),(1<<20, "MB"),(1<<10, "KB")] if s >= d), f"{s} B")
|
||||
def ansistrip(s:str): return re.sub('\x1b\\[(K|.*?m)', '', s)
|
||||
def ansilen(s:str): return len(ansistrip(s))
|
||||
def ansipad(s:str, w:int): return s+' '*max(w-ansilen(s), 0)
|
||||
def make_tuple(x:int|Sequence[int], cnt:int) -> tuple[int, ...]: return (x,)*cnt if isinstance(x, int) else tuple(x)
|
||||
def to_tuple(x:T|tuple[T, ...]) -> tuple[T, ...]: return x if isinstance(x, tuple) else (x,)
|
||||
def flatten(l:Iterable[Iterable[T]]): return [item for sublist in l for item in sublist]
|
||||
@@ -263,6 +264,9 @@ NUM_CPU_THREADS = ContextVar("NUM_CPU_THREADS", _get_cpu_count())
|
||||
NULL_ALLOW_COPYOUT = ContextVar("NULL_ALLOW_COPYOUT", 0)
|
||||
# VIZ implies PROFILE, but you can run PROFILE without VIZ
|
||||
VIZ = ContextVar("VIZ", 0)
|
||||
# this PARALLEL is for BEAM and compilation, it's currently disabled if you are using VIZ
|
||||
# pytest-xdist workers share the CPU budget, explicit PARALLEL still overrides this default
|
||||
PARALLEL = ContextVar("PARALLEL", NUM_CPU_THREADS.value // max(1, getenv("PYTEST_XDIST_WORKER_COUNT", 1)) if VIZ == 0 else 0)
|
||||
PROFILE = ContextVar("PROFILE", abs(VIZ.value))
|
||||
SPEC = ContextVar("SPEC", 1)
|
||||
# TODO: disable by default due to speed
|
||||
@@ -360,7 +364,8 @@ class TracingKey:
|
||||
class ProfileEvent: pass
|
||||
|
||||
@dataclass
|
||||
class ProfileRangeEvent(ProfileEvent): device:str; name:str|TracingKey; st:decimal.Decimal; en:decimal.Decimal|None=None # noqa: E702
|
||||
class ProfileRangeEvent(ProfileEvent):
|
||||
device:str; name:str|TracingKey; st:decimal.Decimal; en:decimal.Decimal|None=None; profile_key:bytes|None=None # noqa: E702
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProfilePointEvent(ProfileEvent):
|
||||
@@ -368,8 +373,8 @@ class ProfilePointEvent(ProfileEvent):
|
||||
|
||||
cpu_events:list[ProfileEvent] = []
|
||||
@contextlib.contextmanager
|
||||
def cpu_profile(name:str|TracingKey, device="TINY", display=True) -> Generator[ProfileRangeEvent, None, None]:
|
||||
res = ProfileRangeEvent(device, name, perf_counter_us())
|
||||
def cpu_profile(name:str|TracingKey, device="TINY", display=True, profile_key:bytes|None=None) -> Generator[ProfileRangeEvent, None, None]:
|
||||
res = ProfileRangeEvent(device, name, perf_counter_us(), profile_key=profile_key)
|
||||
try: yield res
|
||||
finally:
|
||||
res.en = perf_counter_us()
|
||||
@@ -460,14 +465,16 @@ def _ensure_downloads_dir() -> pathlib.Path:
|
||||
return pathlib.Path(cache_dir) / "downloads"
|
||||
|
||||
def fetch(url:str, name:pathlib.Path|str|None=None, subdir:str|None=None, gunzip:bool=False, allow_caching=not getenv("DISABLE_HTTP_CACHE"),
|
||||
headers:dict[str, str]={}, sha256:str|None=None) -> pathlib.Path:
|
||||
headers:dict[str, str]={}, sha256:str|None=None, extract:bool=False) -> pathlib.Path:
|
||||
import urllib.request
|
||||
if url.startswith(("/", ".")): return pathlib.Path(url)
|
||||
if name is not None and (isinstance(name, pathlib.Path) or '/' in name): fp = pathlib.Path(name)
|
||||
else:
|
||||
hh = "_"+hashlib.md5(("\n".join(f"{k.strip()}:{v.strip()}" for k,v in sorted(headers.items()))).encode("utf-8")).hexdigest() if headers else ""
|
||||
fp = _ensure_downloads_dir() / (subdir or "") / ((name or hashlib.md5(url.encode('utf-8')).hexdigest()) + hh + (".gunzip" if gunzip else ""))
|
||||
extract_dir = fp.parent / f"{fp.name}.extract"
|
||||
if not fp.is_file() or not allow_caching or (sha256 and hashlib.sha256(fp.read_bytes()).hexdigest() != sha256):
|
||||
if extract: shutil.rmtree(extract_dir, ignore_errors=True)
|
||||
(_dir := fp.parent).mkdir(parents=True, exist_ok=True)
|
||||
with urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "tinygrad 0.13.0", **headers}), timeout=10) as r:
|
||||
assert r.status in {200, 206}, r.status
|
||||
@@ -484,6 +491,17 @@ def fetch(url:str, name:pathlib.Path|str|None=None, subdir:str|None=None, gunzip
|
||||
pathlib.Path(f.name).rename(fp)
|
||||
progress_bar.update(close=True)
|
||||
if length and (file_size:=os.stat(fp).st_size) < length: raise RuntimeError(f"fetch size incomplete, {file_size} < {length}")
|
||||
if extract:
|
||||
if not extract_dir.is_dir():
|
||||
import tarfile
|
||||
tmpdir = tempfile.mkdtemp(dir=fp.parent)
|
||||
try:
|
||||
with tarfile.open(fp) as t: t.extractall(tmpdir, filter="data")
|
||||
try: os.rename(tmpdir, extract_dir) # rename is atomic, so concurrent fetches can't see a partial extraction
|
||||
except OSError:
|
||||
if not extract_dir.is_dir(): raise
|
||||
finally: shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
return extract_dir
|
||||
return fp
|
||||
|
||||
def fetch_fw(path:str, name:str, sha256:str) -> bytes:
|
||||
@@ -585,9 +603,9 @@ class tqdm(Generic[T]):
|
||||
est_text = f'<{HMS(elapsed/prog-elapsed) if self.n else "?"}' if self.t else ''
|
||||
it_text = (SI(self.n/elapsed) if self.unit_scale else f"{self.n/elapsed:5.2f}") if self.n else "?"
|
||||
suf = f'{prog_text} [{HMS(elapsed)}{est_text}, {it_text}{self.unit}/s]'
|
||||
sz = max(ncols-len(self.desc)-3-2-2-len(suf), 1)
|
||||
sz = max(ncols-ansilen(self.desc)-3-2-2-len(suf), 1)
|
||||
bar = '\r' + self.desc + (f'{100*prog:3.0f}%|{("█"*int(num:=sz*prog)+" ▏▎▍▌▋▊▉"[int(8*num)%8].strip()).ljust(sz," ")}| ' if self.t else '') + suf
|
||||
print(bar[:ncols+1], flush=True, end='\n'*close, file=sys.stderr)
|
||||
print(bar, flush=True, end='\n'*close, file=sys.stderr)
|
||||
@classmethod
|
||||
def write(cls, s:str): print(f"\r\033[K{s}", flush=True, file=sys.stderr)
|
||||
|
||||
|
||||
@@ -258,7 +258,8 @@ class ClangRenderer(CStyleLanguage):
|
||||
gep_arr_threshold = 0
|
||||
has_local = False
|
||||
has_threads = bool(getenv("THREADS", 1))
|
||||
global_max = (NUM_CPU_THREADS.value, 0, 0)
|
||||
@property
|
||||
def global_max(self): return (NUM_CPU_THREADS.value, 0, 0) # type: ignore[override]
|
||||
infinity = "__builtin_inff()"
|
||||
nan = '__builtin_nanf("")'
|
||||
|
||||
|
||||
@@ -810,7 +810,8 @@ class X86Renderer(ISARenderer):
|
||||
device = "CPU"
|
||||
has_local = False
|
||||
has_threads = bool(getenv("THREADS", 1))
|
||||
global_max = (NUM_CPU_THREADS.value, 0, 0)
|
||||
@property
|
||||
def global_max(self): return (NUM_CPU_THREADS.value, 0, 0) # type: ignore[override]
|
||||
extra_matcher = extra_matcher
|
||||
pre_isel_matcher = pre_isel_matcher
|
||||
isel_matcher = isel_matcher
|
||||
|
||||
@@ -204,7 +204,8 @@ class LLVMRenderer(Renderer):
|
||||
class CPULLVMRenderer(LLVMRenderer):
|
||||
has_local = False
|
||||
has_threads = bool(getenv("THREADS", 1))
|
||||
global_max = (NUM_CPU_THREADS.value, 0, 0)
|
||||
@property
|
||||
def global_max(self): return (NUM_CPU_THREADS.value, 0, 0) # type: ignore[override]
|
||||
abi = 'win64cc' if sys.platform == 'win32' else None
|
||||
string_rewrite = base_rewrite
|
||||
def render(self, uops: list[UOp]) -> str: return "\n".join((k:=self._render_kernel(uops))[0] + (k[1], self._render_footer(uops)))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from tinygrad.dtype import DType, dtypes, truncate, AddrSpace
|
||||
from tinygrad.uop.ops import UOp, Ops, PatternMatcher, UPat
|
||||
from tinygrad.renderer.cstyle import CStyleLanguage, base_rewrite
|
||||
from tinygrad.helpers import strip_parens
|
||||
from tinygrad.helpers import strip_parens, ceildiv
|
||||
|
||||
def _mask(dt:DType): return 0xFF if dt.itemsize == 1 else 0xFFFF
|
||||
|
||||
@@ -34,7 +34,7 @@ def is_packed(x:UOp):
|
||||
elif x.op is Ops.STORE: dt, addrspace = x.src[1].dtype, x.src[0].addrspace
|
||||
else: dt, addrspace = x.dtype, x.addrspace
|
||||
return dt.itemsize < 4 and dt != dtypes.half and addrspace != AddrSpace.REG
|
||||
def _packed_size(u:UOp): return u.max_numel() // (4//u.dtype.itemsize) if is_packed(u) else u.max_numel()
|
||||
def _packed_size(u:UOp): return ceildiv(u.max_numel(), 4//u.dtype.itemsize) if is_packed(u) else u.max_numel()
|
||||
def is_nan(a):
|
||||
bs, (exp, mant) = a.dtype.bitsize, dtypes.finfo(a.dtype)
|
||||
return (a.bitcast(getattr(dtypes, f"uint{bs}")) & ((1 << (bs - 1)) - 1)) > (((1 << exp) - 1) << mant)
|
||||
|
||||
@@ -139,7 +139,8 @@ class HCQGraph(MultiGraphRunner):
|
||||
prof_ji_desc = runtime.name if runtime is not None else TracingKey(f"{bufs[1].device} -> {bufs[0].device}", ret=bufs[0].nbytes)
|
||||
|
||||
prof_name = enqueue_dev.device if runtime is not None else f"{enqueue_dev.device}:SDMA:{queue_idx}"
|
||||
self.prof_graph_entries.append(ProfileGraphEntry(prof_name, prof_ji_desc, sig_st, j * 2 + 1))
|
||||
self.prof_graph_entries.append(ProfileGraphEntry(prof_name, prof_ji_desc, sig_st, j * 2 + 1,
|
||||
runtime.profile_key if runtime is not None else None))
|
||||
self.prof_graph_deps.append([d - 1 for _, d in rdeps])
|
||||
|
||||
self.last_j[enqueue_queue] = j
|
||||
|
||||
@@ -102,7 +102,7 @@ class MetalGraph(GraphRunner):
|
||||
def collect_timestamps(self):
|
||||
# create a graph event and evenly space each program
|
||||
st, en = decimal.Decimal(self.command_buffer.GPUStartTime()) * 1000000, decimal.Decimal(self.command_buffer.GPUEndTime()) * 1000000
|
||||
ents = [ProfileGraphEntry(self.device, rt.name, i, i+1) for i, rt in enumerate(self.runtimes) if rt is not None]
|
||||
ents = [ProfileGraphEntry(self.device, rt.name, i, i+1, rt.profile_key) for i, rt in enumerate(self.runtimes) if rt is not None]
|
||||
self.dev.profile_events += [ProfileGraphEvent(ents, [], [st + (en-st)/len(ents)*i for i in range(len(ents)+1)])]
|
||||
|
||||
def __del__(self):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
from typing import cast
|
||||
import os, ctypes, struct, hashlib, functools, importlib, mmap, errno, array, contextlib, sys, weakref, itertools, collections, atexit
|
||||
import os, ctypes, struct, hashlib, functools, importlib, mmap, errno, array, contextlib, sys, weakref, itertools, collections, atexit, time
|
||||
assert sys.platform != 'win32'
|
||||
from dataclasses import dataclass
|
||||
from tinygrad.runtime.support.hcq import HCQCompiled, HCQAllocator, HCQBuffer, HWQueue, CLikeArgsState, HCQSignal, HCQProgram, FileIOInterface
|
||||
@@ -649,6 +649,56 @@ class AMDAllocator(HCQAllocator['AMDDevice']):
|
||||
|
||||
def _do_map(self, buf:HCQBuffer): return self.dev.iface.map(buf._base if buf._base is not None else buf)
|
||||
|
||||
def _copyin(self, dest:HCQBuffer, src:memoryview):
|
||||
if not self.dev.is_usb(): return super()._copyin(dest, src)
|
||||
from tinygrad.runtime.support.usb import alloc_cbuffer
|
||||
# Pipelined copyin over the 0xF2 engine. 240KB chunks stream into two alternating 256KB SRAM bounce windows; the
|
||||
# engine can't signal data landing, so each chunk ends in a 512B sentinel sector tagged with its sequence number.
|
||||
# A prebuilt SDMA ring polls each chunk's sentinel before copying it to VRAM, then bumps a drain fence; the host
|
||||
# waits on that fence before re-arming a window. No timing is assumed in either direction.
|
||||
dev, usb, ts, sdma = self.dev, self.dev.iface.pci_dev.usb, self.dev.timeline_signal, self.dev.sdma
|
||||
CHUNK, src_mv = 0x3C000, src.cast('B') # 15 16KB slots: the wire image must end mid-window (full windows corrupt)
|
||||
nchunks = ceildiv(src.nbytes, CHUNK)
|
||||
FENCE = 0xA800 # drain fence: the GPU writes it via sys_buf (PCIe 0x820800), the host reads it here (xdata)
|
||||
if not hasattr(self, '_usb_seq'): # one-time: clear the fence and zero both windows so garbage can't match a sentinel
|
||||
self._usb_seq, self._usb_stage = 0, [alloc_cbuffer(0x40000) for _ in range(2)] # (backing array, memoryview) pairs
|
||||
self._usb_wins = (self.b[0].offset(0, 0x40000), self.b[0].offset(0x40000, 0x40000)) # two windows, engine slots 0/16
|
||||
usb.write(FENCE, bytes(8))
|
||||
for bi in range(2): usb.scsi_write(bytes(0x40000), slot_start=bi * 16)
|
||||
|
||||
def wait_drain(count): # spin until the drain fence reaches count, i.e. chunks 0..count-1 are fully in VRAM
|
||||
t0 = time.monotonic()
|
||||
while int.from_bytes(usb.read(FENCE, 8), 'little') < count:
|
||||
if time.monotonic() - t0 > 10: raise RuntimeError(f"GPU failed to drain USB copyin chunk {count - 1} (10s, hung GPU?)")
|
||||
|
||||
# build the whole ring upfront: per chunk, poll the sentinel, copy SRAM->VRAM, bump the fence; then one doorbell
|
||||
POLL_EQ = sdma.SDMA_OP_POLL_REGMEM | sdma.SDMA_PKT_POLL_REGMEM_HEADER_FUNC(3) | sdma.SDMA_PKT_POLL_REGMEM_HEADER_MEM_POLL(1)
|
||||
POLL_DW5 = sdma.SDMA_PKT_POLL_REGMEM_DW5_INTERVAL(0x04) | sdma.SDMA_PKT_POLL_REGMEM_DW5_RETRY_COUNT(0xfff)
|
||||
q = dev.hw_copy_queue_t().wait(ts, dev.timeline_value - 1)
|
||||
for c in range(nchunks):
|
||||
seq, size = self._usb_seq + c, min(CHUNK, src.nbytes - c * CHUNK)
|
||||
q.q(POLL_EQ, *data64_le(self._usb_wins[seq & 1].va_addr + round_up(size, 512)), 0x51000000 | (seq & 0xFFFFFF), 0xFFFFFFFF, POLL_DW5)
|
||||
q.copy(dest.offset(c * CHUNK), self._usb_wins[seq & 1], size)
|
||||
q.write(dev.iface.sys_buf.offset(0x800, 8), seq + 1, b64=True)
|
||||
q.signal(ts, dev.next_timeline()).submit(dev)
|
||||
|
||||
# stream the chunks: stage the wire image [payload][sentinel], arm the window, send. A window is reusable once
|
||||
# its previous occupant (seq-2) is both fully sent (tag reaped) and fully drained to VRAM (the fence).
|
||||
inflight = [None, None]
|
||||
for c in range(nchunks):
|
||||
seq, size = self._usb_seq + c, min(CHUNK, src.nbytes - c * CHUNK)
|
||||
if inflight[seq & 1] is not None: usb.usb.bulk_wait(inflight[seq & 1])
|
||||
buf = self._usb_stage[seq & 1][1]
|
||||
buf[:size] = src_mv[c * CHUNK : c * CHUNK + size]
|
||||
struct.pack_into('<I', buf, round_up(size, 512), 0x51000000 | (seq & 0xFFFFFF)) # the sentinel sector
|
||||
wait_drain(seq - 1)
|
||||
wire = round_up(size, 512) + 512 # payload padded to 512B sectors, plus the sentinel sector
|
||||
usb.usb.control_write(0xF2, wire // 512, (seq & 1) * 16 | (ceildiv(wire, 0x4000) << 8)) # wValue=sectors, wIndex=slot|count
|
||||
inflight[seq & 1] = usb.usb.bulk_write_async(buf[:wire])
|
||||
for tag in inflight: usb.usb.bulk_wait(tag)
|
||||
self._usb_seq += nchunks
|
||||
wait_drain(self._usb_seq) # copyin is synchronous: everything must be in VRAM before returning
|
||||
|
||||
def _copyout(self, dest:memoryview, src:HCQBuffer):
|
||||
if not self.dev.is_usb(): return super()._copyout(dest, src)
|
||||
self.dev.synchronize()
|
||||
@@ -926,9 +976,8 @@ class USBIface(PCIIface):
|
||||
|
||||
def alloc(self, size:int, host=False, uncached=False, cpu_access=False, contiguous=False, force_devmem=False, **kwargs) -> HCQBuffer:
|
||||
# usb allocates uncached and cpu_access in vram. vram writes are faster than sram writes
|
||||
if host and self.sys_next_off + size < self.sys_buf.size:
|
||||
self.sys_next_off += size
|
||||
return self.sys_buf.offset(self.sys_next_off - size, size)
|
||||
# NOTE: host allocs deliberately do NOT use sys_buf (the 0x820000 NVMe SQ region): the GPU's signal writes there
|
||||
# collide with the 0xF2 engine mid-stream. Signals in VRAM are read back via 0xF0 streaming reads instead.
|
||||
|
||||
# force devmem
|
||||
return super().alloc(size, host=False, uncached=uncached, cpu_access=cpu_access, contiguous=contiguous, force_devmem=True, **kwargs)
|
||||
@@ -1048,7 +1097,8 @@ class AMDDevice(HCQCompiled):
|
||||
if getenv("AMD_DISABLE_SDMA"): return None
|
||||
if idx in self.sdma_queues: return self.sdma_queues[idx]
|
||||
with contextlib.suppress(OSError):
|
||||
self.sdma_queues[idx] = self.create_queue(kfd.KFD_IOC_QUEUE_TYPE_SDMA, 0x200 if self.is_usb() else (16 << 20), idx=idx)
|
||||
# USB: a copyin submits its whole ring at once (3 packets per 240KB chunk), so it needs more than the 0x200 default
|
||||
self.sdma_queues[idx] = self.create_queue(kfd.KFD_IOC_QUEUE_TYPE_SDMA, (1 << 20) if self.is_usb() else (16 << 20), idx=idx)
|
||||
return self.sdma_queues.get(idx, None)
|
||||
|
||||
def _ensure_has_local_memory(self, private_segment_size):
|
||||
|
||||
@@ -34,6 +34,7 @@ class MetalDevice(Compiled):
|
||||
self.mtl_queue = self.sysdevice.newCommandQueueWithMaxCommandBufferCount(1024)
|
||||
if self.mtl_queue is None: raise RuntimeError("Cannot allocate a new command queue")
|
||||
self.mtl_buffers_in_flight: list[metal.MTLCommandBuffer] = []
|
||||
self.mtl_profile_keys: dict[int, bytes] = {}
|
||||
self.timeline_signal = self.sysdevice.newSharedEvent()
|
||||
self.timeline_value = 0
|
||||
|
||||
@@ -55,7 +56,7 @@ class MetalDevice(Compiled):
|
||||
st, en = decimal.Decimal(cbuf.GPUStartTime()) * 1000000, decimal.Decimal(cbuf.GPUEndTime()) * 1000000
|
||||
# NOTE: command buffers from MetalGraph are not profiled here
|
||||
if PROFILE and (lb:=cmdbuf_label(cbuf)) is not None and not lb.startswith("batched"):
|
||||
Compiled.profile_events += [ProfileRangeEvent(self.device, lb, st, en)]
|
||||
Compiled.profile_events += [ProfileRangeEvent(self.device, lb, st, en, self.mtl_profile_keys.pop(id(cbuf), None))]
|
||||
self.mtl_buffers_in_flight.clear()
|
||||
|
||||
class MetalCompiler(Compiler):
|
||||
@@ -113,7 +114,7 @@ class MetalCompiler(Compiler):
|
||||
|
||||
class MetalProgram(Program[MetalDevice]):
|
||||
def __init__(self, dev:MetalDevice, obj:TinyELF):
|
||||
self.dev, self.name, self.lib, self.signature = dev, obj.name, obj.lib, obj.signature
|
||||
self.dev, self.name, self.lib, self.signature, self.profile_key = dev, obj.name, obj.lib, obj.signature, obj.profile_key
|
||||
data = objc.dispatch_data_create(obj.lib, len(obj.lib), None, None)
|
||||
self.library = self.dev.sysdevice.newLibraryWithData_error(data, ctypes.byref(error_lib:=metal.NSError().retained())).retained()
|
||||
error_check(error_lib)
|
||||
@@ -145,6 +146,7 @@ class MetalProgram(Program[MetalDevice]):
|
||||
command_buffer.setLabel(to_ns_str(self.name)) # TODO: is this always needed?
|
||||
command_buffer.commit()
|
||||
self.dev.mtl_buffers_in_flight.append(command_buffer)
|
||||
if PROFILE and self.profile_key is not None: self.dev.mtl_profile_keys[id(command_buffer)] = self.profile_key
|
||||
if wait:
|
||||
wait_check(command_buffer)
|
||||
return command_buffer.GPUEndTime() - command_buffer.GPUStartTime()
|
||||
|
||||
@@ -17,9 +17,9 @@ class NullRenderer(CStyleLanguage):
|
||||
return assemble_linear(prg, lin, self.target.arch)
|
||||
|
||||
class NullProgram(Program['NullDevice']):
|
||||
def __init__(self, dev:'NullDevice', obj:TinyELF): self.device, self.name = dev.device, obj.name
|
||||
def __init__(self, dev:'NullDevice', obj:TinyELF): self.device, self.name, self.profile_key = dev.device, obj.name, obj.profile_key
|
||||
def __call__(self, *bufs, global_size:tuple[int,int,int]=(1,1,1), local_size:tuple[int,int,int]=(1,1,1), vals:tuple[int, ...]=(), wait=False, **kw):
|
||||
with cpu_profile(self.name, self.device): return 1e-3
|
||||
with cpu_profile(self.name, self.device, profile_key=self.profile_key): return 1e-3
|
||||
|
||||
class NullAllocator(Allocator['NullDevice']):
|
||||
def _alloc(self, size, options): pass
|
||||
@@ -38,13 +38,14 @@ class NullGraph(MultiGraphRunner):
|
||||
for (_,_,bufs,_),runtime in zip(self.calls, self.runtimes):
|
||||
# description based on command, copied from HCQ graph
|
||||
device = runtime.device if runtime is not None else f"{bufs[1].device}:SDMA:0"
|
||||
descs.append((device, runtime.name if runtime is not None else f"{bufs[1].device} -> {bufs[0].device}", count:=event_count.get(device, 0)))
|
||||
descs.append((device, runtime.name if runtime is not None else f"{bufs[1].device} -> {bufs[0].device}",
|
||||
runtime.profile_key if runtime is not None else None, count:=event_count.get(device, 0)))
|
||||
event_count[device] = count+1
|
||||
# pack events evenly per device
|
||||
dur, sigs, ents = max(1, math.ceil((perf_counter_us()-st)/max(event_count.values()))), [], []
|
||||
for i,(device,name,count) in enumerate(descs):
|
||||
for i,(device,name,profile_key,count) in enumerate(descs):
|
||||
sigs += [st+count*dur, st+(count+1)*dur]
|
||||
ents.append(ProfileGraphEntry(device, name, 2*i, 2*i+1))
|
||||
ents.append(ProfileGraphEntry(device, name, 2*i, 2*i+1, profile_key))
|
||||
cpu_events.append(ProfileGraphEvent(ents, [], sigs))
|
||||
return 1e-1
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ nv_gpu = nv_570 # default to 570
|
||||
PMA = ContextVar("PMA", abs(VIZ.value)>=2)
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProfilePMAEvent(ProfileEvent): device:str; kern:str; blob:bytes; exec_tag:int # noqa: E702
|
||||
class ProfilePMAEvent(ProfileEvent): device:str; kern:str; blob:bytes; exec_tag:int; profile_key:bytes|None=None # noqa: E702
|
||||
|
||||
class NVSignal(HCQSignal):
|
||||
def _sleep(self, time_spent_since_last_sleep_ms:int):
|
||||
@@ -335,7 +335,7 @@ class NVProgram(HCQProgram['NVDevice']):
|
||||
if self.dev.pma_enabled:
|
||||
self.dev.synchronize()
|
||||
if pma_blob:=self.dev._prof_readback():
|
||||
Compiled.profile_events += [ProfilePMAEvent(self.dev.device, self.name, pma_blob, self.dev.prof_exec_counter)]
|
||||
Compiled.profile_events += [ProfilePMAEvent(self.dev.device, self.name, pma_blob, self.dev.prof_exec_counter, self.profile_key)]
|
||||
return res
|
||||
|
||||
class NVAllocator(HCQAllocator['NVDevice']):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ctypes, struct, platform, pathlib, shutil, tarfile, tempfile
|
||||
import ctypes, struct, platform, pathlib, shutil
|
||||
from tinygrad.device import Compiler
|
||||
from tinygrad.helpers import DEBUG, system, fetch
|
||||
from tinygrad.runtime.support.compiler_mesa import disas_adreno
|
||||
@@ -12,8 +12,9 @@ class QCOMCompiler(Compiler):
|
||||
assert arch.split(',')[0] == "a630", "only a630 supported"
|
||||
if platform.machine() == "aarch64": self.arch, self.chip_id, self.llvm_inst = arch, 0x6030001, llvm_qcom.cl_compiler_create_llvm_instance()
|
||||
else:
|
||||
self.arch, self.chip_id, self.fs, root = arch, 0x6030001, tempfile.TemporaryDirectory(), pathlib.Path(__file__).parents[3]
|
||||
with tarfile.open(fetch('https://git.tinygrad.win/sirhcm/images/releases/download/v2/qcomcl.tar.gz')) as t: t.extractall(fs:=self.fs.name)
|
||||
# extract once into the download cache, all processes share the rootfs (extract=True)
|
||||
self.arch, self.chip_id = arch, 0x6030001
|
||||
fs, root = fetch('https://git.tinygrad.win/sirhcm/images/releases/download/v2/qcomcl.tar.gz', extract=True), pathlib.Path(__file__).parents[3]
|
||||
self.compiler_process = self.server(f"{qemu} -cpu max,pauth=off -L {fs} {fs}/usr/bin/python3" if (qemu:=shutil.which("qemu-aarch64-static"))
|
||||
else (f"docker run --rm -i --platform linux/aarch64 -v {fs}/usr:/usr -v {root}:{root} "
|
||||
f"-e PYTHONPATH={root} -e QEMU_CPU=max,pauth=off gcr.io/distroless/static python3"), arch)
|
||||
|
||||
@@ -295,7 +295,8 @@ class HCQSignal(Generic[HCQDeviceType]):
|
||||
if not_passed and self.value < value: raise RuntimeError(f"Wait timeout: {timeout} ms! (the signal is not set to {value}, but {self.value})")
|
||||
|
||||
@contextlib.contextmanager
|
||||
def hcq_profile(dev:HCQCompiled, enabled, desc, queue_type:Callable[[], HWQueue]|None=None, queue:HWQueue|None=None, dev_suff:str|None=None):
|
||||
def hcq_profile(dev:HCQCompiled, enabled, desc, queue_type:Callable[[], HWQueue]|None=None, queue:HWQueue|None=None, dev_suff:str|None=None,
|
||||
profile_key:bytes|None=None):
|
||||
st, en = (dev.new_signal(), dev.new_signal()) if enabled else (None, None)
|
||||
assert queue is not None or queue_type is not None, "Either queue or queue_type must be provided"
|
||||
|
||||
@@ -309,7 +310,8 @@ def hcq_profile(dev:HCQCompiled, enabled, desc, queue_type:Callable[[], HWQueue]
|
||||
elif enabled and queue_type is not None:
|
||||
queue_type().wait(dev.timeline_signal, dev.timeline_value - 1).timestamp(en).signal(dev.timeline_signal, dev.next_timeline()).submit(dev)
|
||||
|
||||
if enabled and PROFILE: dev.sig_prof_records.append((unwrap(st), unwrap(en), desc, f"{dev.device}:{dev_suff}" if dev_suff else dev.device))
|
||||
if enabled and PROFILE: dev.sig_prof_records.append((unwrap(st), unwrap(en), desc, f"{dev.device}:{dev_suff}" if dev_suff else dev.device,
|
||||
profile_key))
|
||||
|
||||
class HCQArgsState(Generic[ProgramType]):
|
||||
def __init__(self, buf:HCQBuffer, prg:ProgramType, bufs:tuple[HCQBuffer, ...], vals:tuple[sint|None, ...]=()):
|
||||
@@ -332,8 +334,9 @@ class CLikeArgsState(HCQArgsState[ProgramType]):
|
||||
class HCQProgram(Program[HCQDeviceType]):
|
||||
def __init__(self, args_state_t:Type[HCQArgsState], dev:HCQDeviceType, obj:TinyELF, kernargs_alloc_size:int, base:int|None=None):
|
||||
self.args_state_t, self.dev, self.name, self.signature, self.kernargs_alloc_size = args_state_t, dev, obj.name, obj.signature, kernargs_alloc_size
|
||||
self.profile_key = obj.profile_key
|
||||
self.prof_prg_counter = next(self.dev.prof_prg_counter)
|
||||
if PROFILE: Compiled.profile_events += [ProfileProgramEvent(dev.device, obj.name, obj.lib, base, self.prof_prg_counter)]
|
||||
if PROFILE: Compiled.profile_events += [ProfileProgramEvent(dev.device, obj.name, obj.lib, base, self.prof_prg_counter, self.profile_key)]
|
||||
|
||||
@staticmethod
|
||||
def _fini(dev, buf, spec): dev.allocator.free(buf, buf.size, spec)
|
||||
@@ -372,7 +375,7 @@ class HCQProgram(Program[HCQDeviceType]):
|
||||
q = unwrap(self.dev.hw_compute_queue_t)().wait(self.dev.timeline_signal, self.dev.timeline_value - 1).memory_barrier()
|
||||
|
||||
self.dev.prof_exec_counter += 1
|
||||
with hcq_profile(self.dev, queue=q, desc=self.name, enabled=wait or PROFILE) as (sig_st, sig_en):
|
||||
with hcq_profile(self.dev, queue=q, desc=self.name, enabled=wait or PROFILE, profile_key=self.profile_key) as (sig_st, sig_en):
|
||||
q.exec(self, kernargs, global_size, local_size)
|
||||
|
||||
q.signal(self.dev.timeline_signal, self.dev.next_timeline()).submit(self.dev)
|
||||
@@ -401,7 +404,7 @@ class HCQCompiled(Compiled, Generic[SignalType]):
|
||||
self.signal_t, self.hw_compute_queue_t, self.hw_copy_queue_t = signal_t, comp_queue_t, copy_queue_t
|
||||
|
||||
self.timeline_value:int = 1
|
||||
self.sig_prof_records:list[tuple[HCQSignal, HCQSignal, str|TracingKey, str]] = []
|
||||
self.sig_prof_records:list[tuple[HCQSignal, HCQSignal, str|TracingKey, str, bytes|None]] = []
|
||||
self.prof_exec_counter:int = 0
|
||||
self.prof_prg_counter = itertools.count(0)
|
||||
|
||||
@@ -437,7 +440,7 @@ class HCQCompiled(Compiled, Generic[SignalType]):
|
||||
|
||||
if self.timeline_value > (1 << 31): self._wrap_timeline_signal()
|
||||
if PROFILE:
|
||||
Compiled.profile_events += [ProfileRangeEvent(dev, name, st.timestamp, en.timestamp) for st,en,name,dev in self.sig_prof_records]
|
||||
Compiled.profile_events += [ProfileRangeEvent(dev, name, st.timestamp, en.timestamp, pk) for st,en,name,dev,pk in self.sig_prof_records]
|
||||
self.sig_prof_records = []
|
||||
|
||||
def next_timeline(self):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import ctypes, struct, time, functools, itertools
|
||||
from tinygrad.runtime.autogen import libusb
|
||||
from tinygrad.helpers import DEBUG, DEV, to_mv, round_up, ceildiv
|
||||
from tinygrad.helpers import DEBUG, DEV, to_mv, from_mv, round_up, ceildiv
|
||||
from tinygrad.runtime.support.hcq import MMIOInterface
|
||||
from tinygrad.runtime.support import c
|
||||
|
||||
@@ -35,6 +35,11 @@ class USB3:
|
||||
self._tags, self._transferred = itertools.count(1), ctypes.c_int(0)
|
||||
self._bulk_buf, self._bulk_mv = alloc_cbuffer(4 << 20)
|
||||
self._ctrl_buf, self._ctrl_mv = alloc_cbuffer(0x1000)
|
||||
# async bulk OUT state: tag -> (pooled transfer, keepalive payload mv); transfer errors latch into _async_err
|
||||
self._async_seq, self._async_err = itertools.count(1), 0
|
||||
self._async_pending: dict = {}
|
||||
self._async_pool: list = []
|
||||
self._async_cb = libusb.libusb_transfer_cb_fn(self._on_bulk_done)
|
||||
|
||||
self.handle = c.init_c_var(c.POINTER[libusb.struct_libusb_device_handle], lambda x: checked(libusb.libusb_open)(dev, x))
|
||||
|
||||
@@ -73,6 +78,26 @@ class USB3:
|
||||
(self.handle, 0x02, self._bulk_buf, len(payload), self._transferred, timeout)
|
||||
assert self._transferred.value == len(payload), f"bulk OUT short write: {self._transferred.value}/{len(payload)} bytes"
|
||||
|
||||
def _on_bulk_done(self, xfer): # runs in libusb event handling; latch errors (exceptions here are unraisable)
|
||||
if xfer.contents.status != 0 or xfer.contents.actual_length != xfer.contents.length: self._async_err = xfer.contents.status or -1
|
||||
self._async_pool.append(self._async_pending.pop(int(xfer.contents.user_data or 0))[0])
|
||||
|
||||
def bulk_write_async(self, payload:memoryview, timeout:int=10000) -> int:
|
||||
"""Queue a bulk OUT transfer without blocking; payload is kept alive until bulk_wait(tag)."""
|
||||
tr = self._async_pool.pop() if self._async_pool else libusb.libusb_alloc_transfer(0)
|
||||
tr.contents.dev_handle, tr.contents.endpoint, tr.contents.type = self.handle, 0x02, libusb.LIBUSB_TRANSFER_TYPE_BULK
|
||||
tr.contents.timeout, tr.contents.length = timeout, len(payload)
|
||||
tr.contents.buffer = ctypes.cast(from_mv(payload, ctypes.c_ubyte), ctypes.POINTER(ctypes.c_ubyte))
|
||||
tr.contents.callback, tr.contents.user_data = self._async_cb, (tag := next(self._async_seq))
|
||||
self._async_pending[tag] = (tr, payload)
|
||||
checked(libusb.libusb_submit_transfer, "async bulk OUT submit failed")(tr)
|
||||
return tag
|
||||
|
||||
def bulk_wait(self, tag:int):
|
||||
"""Block until the tagged transfer completes; raises if any async transfer failed."""
|
||||
while tag in self._async_pending: checked(libusb.libusb_handle_events)(None)
|
||||
if self._async_err: raise RuntimeError(f"async bulk OUT failed: status={self._async_err}")
|
||||
|
||||
def bulk_read(self, length:int, timeout:int=1000) -> memoryview:
|
||||
if length > len(self._bulk_mv): self._bulk_buf, self._bulk_mv = alloc_cbuffer(length)
|
||||
checked(libusb.libusb_bulk_transfer, "bulk IN 0x81 failed")(self.handle, 0x81, self._bulk_buf, length, self._transferred, timeout)
|
||||
@@ -160,13 +185,10 @@ class CustomASM24Controller:
|
||||
"""Write to chip XDATA via vendor control OUT (bRequest=0xE5). wValue=addr, wIndex=val."""
|
||||
for off, val in enumerate(data): self.usb.control_write(0xE5, value=base_addr + off, index=val)
|
||||
|
||||
def scsi_write(self, buf:bytes):
|
||||
def scsi_write(self, buf:bytes, slot_start:int=0):
|
||||
"""Write to SRAM via 0xF2 vendor command + bulk OUT."""
|
||||
buf_padded = buf + b'\x00' * (round_up(len(buf), 512) - len(buf))
|
||||
sectors = len(buf_padded) // 512
|
||||
num_slots = ceildiv(len(buf_padded), 0x4000) # 16KB per slot
|
||||
windex = (num_slots & 0xFF) << 8
|
||||
self.usb.control_write(0xF2, value=sectors, index=windex)
|
||||
self.usb.control_write(0xF2, value=len(buf_padded) // 512, index=(slot_start & 0xFF) | (ceildiv(len(buf_padded), 0x4000) << 8))
|
||||
self.usb.bulk_write(buf_padded)
|
||||
|
||||
def scsi_read_arm(self, size:int):
|
||||
@@ -189,7 +211,7 @@ class USBMMIOInterface(MMIOInterface):
|
||||
assert sz % 4 == 0 and off % 4 == 0, f"pcie_mem_read requires 4-byte aligned access, got off={off}, sz={sz}"
|
||||
data = self.usb.pcie_mem_read(self.addr + off, sz)
|
||||
else: data = self.usb.scsi_read(sz) if self.addr == 0xf000 else self.usb.read(self.addr + off, sz)
|
||||
return int.from_bytes(data, "little") if sz == self.el_sz else data
|
||||
return data if isinstance(index, slice) else int.from_bytes(data, "little")
|
||||
|
||||
def __setitem__(self, index, data):
|
||||
off, _ = self._off_from_index(index)
|
||||
|
||||
@@ -97,11 +97,17 @@ pm_post_sched_cache = PatternMatcher([
|
||||
create_new_buffer(ctx, b) if isinstance(b.arg, ParamArg) and b.addrspace is AddrSpace.GLOBAL else None),
|
||||
])
|
||||
|
||||
def resolve_linear_call(linear_call:UOp):
|
||||
def resolve_linear_call(linear_call:UOp, outer_binds:dict[str, UOp]|None=None):
|
||||
linear = graph_rewrite(linear_call.src[0], pm_post_sched_cache, ctx=({}, linear_call.src[1:]), walk=True, name="params to buffers")
|
||||
# map the call body params back to the original Variables stored in the call args
|
||||
binds = {f"p{i}":x.src[0].replace(op=Ops.PARAM) for i,x in enumerate(linear_call.src[1:]) if x.is_bound_var}
|
||||
return linear.substitute({v:binds[v.expr] for v in linear.variables() if v.expr in binds}, enter_calls=True, name="resolve scalar params")
|
||||
# nested LINEAR calls are lexical scopes: their positional params shadow the enclosing scope, while calls without
|
||||
# scalar args (e.g. precompiled allreduce) inherit it
|
||||
binds = {**(outer_binds or {}),
|
||||
**{f"p{i}":x.src[0].replace(op=Ops.PARAM) for i,x in enumerate(linear_call.src[1:]) if x.is_bound_var}}
|
||||
def apply_binds(si:UOp) -> UOp:
|
||||
if si.op is Ops.CALL and si.src[0].op is Ops.LINEAR: return resolve_linear_call(si, binds)
|
||||
subs = {v:binds[v.expr] for v in si.variables() if v.expr in binds}
|
||||
return si.replace(src=tuple(s.substitute(subs, name="resolve scalar params") for s in si.src))
|
||||
return linear.replace(src=tuple(apply_binds(si) for si in linear.src))
|
||||
|
||||
pm_resolve_linear_call = PatternMatcher([
|
||||
# call LINEAR is resolved here
|
||||
|
||||
+2
-2
@@ -969,7 +969,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
# *** uop Variable stuff ***
|
||||
|
||||
@staticmethod
|
||||
def variable(name:str, min_val:PyConst, max_val:PyConst, dtype:DType=dtypes.weakint, multiple_of:int=1, param:bool=False) -> UOp:
|
||||
def variable(name:str, min_val:PyConst, max_val:PyConst, dtype:DType=dtypes.int, multiple_of:int=1, param:bool=False) -> UOp:
|
||||
# a Variable is a 0-d BUFFER in the ALU addrspace; binding it is storing a CONST into it
|
||||
# param=True creates the kernel-side form directly: an ALU PARAM (what the BUFFER becomes inside kernels)
|
||||
arg = ParamArg(-1, dtype, name=name, vmin_vmax=(min_val, max_val), multiple_of=multiple_of, addrspace=AddrSpace.ALU)
|
||||
@@ -1198,7 +1198,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
assert self.op is Ops.PROGRAM and isinstance(self.arg, ProgramInfo), "to_elf should only be called on a PROGRAM ast"
|
||||
sig = tuple((u.arg.name, u.arg.slot, u.dtype, u._shape)
|
||||
for u in tuple(filter(lambda u: u.op is Ops.PARAM and u.addrspace != AddrSpace.ALU, self.src[1].src)) + self.arg.vars)
|
||||
return TinyELF(self.src[3].arg, self.arg.function_name, self.arg.target, sig)
|
||||
return TinyELF(self.src[3].arg, self.arg.function_name, self.arg.target, sig, self.key)
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class KernelInfo:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from tinygrad.dtype import AddrSpace, dtypes
|
||||
from tinygrad.uop import Ops, GroupOp
|
||||
from tinygrad.uop.ops import ParamArg, UOp, PatternMatcher, UPat, multirange_str, range_str, consumer_map_from_toposort
|
||||
from tinygrad.uop.ops import ParamArg, UOp, PatternMatcher, UPat, multirange_str, range_str, consumer_map_from_toposort, sint
|
||||
from tinygrad.helpers import strip_parens
|
||||
|
||||
def pretty_print(x:UOp, cache=None, d=0)->str:
|
||||
@@ -69,14 +69,15 @@ renderer_infer = PatternMatcher([
|
||||
# *** pyrender ***
|
||||
|
||||
def srcs(ctx, src): return f"({ctx[src[0]]},)" if len(src) == 1 else f"({', '.join([ctx[x] for x in src])})"
|
||||
# marg is ssimplify'd, so a bound can be a node this graph never contained
|
||||
def marg_str(ctx, a:sint) -> str: return str(a) if not isinstance(a, UOp) else ctx[a] if a in ctx else a.render()
|
||||
|
||||
def render_marg(ctx,x:UOp):
|
||||
if x.op is Ops.PERMUTE: return str(x.marg)
|
||||
if x.op is Ops.FLIP: return str(tuple([i for i,x in enumerate(x.marg) if x]))
|
||||
pieces = []
|
||||
if x.op in {Ops.RESHAPE, Ops.EXPAND}:
|
||||
pieces = [f"{ctx[a] if isinstance(a, UOp) else str(a)}" for a in x.marg]
|
||||
if x.op in {Ops.PAD, Ops.SHRINK}:
|
||||
pieces = [f"({ctx[a[0]] if isinstance(a[0], UOp) else str(a[0])}, {ctx[a[1]] if isinstance(a[1], UOp) else str(a[1])})" for a in x.marg]
|
||||
if x.op in {Ops.RESHAPE, Ops.EXPAND}: pieces = [marg_str(ctx, a) for a in x.marg]
|
||||
if x.op in {Ops.PAD, Ops.SHRINK}: pieces = [f"({marg_str(ctx, a[0])}, {marg_str(ctx, a[1])})" for a in x.marg]
|
||||
return f"({','.join(pieces)})" if len(pieces) != 1 else f"({pieces[0]},)"
|
||||
|
||||
sugar = {Ops.SINK, Ops.END, Ops.STORE, Ops.LOAD, Ops.SQRT, Ops.INDEX, Ops.REDUCE, Ops.AFTER, Ops.THREEFRY,
|
||||
|
||||
@@ -24,8 +24,8 @@ def fold_bitcast(root:UOp, c:UOp) -> UOp|None:
|
||||
if c.dtype.fmt is None or root.dtype.fmt is None or c.dtype.itemsize != root.dtype.itemsize: return None
|
||||
return root.const_like(bitcast(truncate[c.dtype](c.val), c.dtype, root.dtype))
|
||||
|
||||
# const folding works for CONST, STACK, and casted CONST
|
||||
const_folding_pat = UPat.any(UPat((Ops.CONST, Ops.STACK)), UPat(Ops.CAST, src=(UPat(Ops.CONST),)))
|
||||
# const folding works for CONST and STACK
|
||||
const_folding_pat = UPat((Ops.CONST, Ops.STACK))
|
||||
|
||||
def const_arg(u:UOp) -> ConstType|tuple[ConstType, ...]|None:
|
||||
if u.op is Ops.CONST: return u.val
|
||||
@@ -142,6 +142,9 @@ symbolic_simple = pm_data_invalid + PatternMatcher([
|
||||
(UPat.var("x", dtype=dtypes.ints+(dtypes.bool, dtypes.weakint)) != UPat.var("x"),
|
||||
lambda x: x.const_like(False, dtypes.bool)), # x != x -> False (only ints)
|
||||
# ** constant folding **
|
||||
# a CAST to a concrete dtype over a CONST is a value conversion: evaluate it once, at the width the CAST states
|
||||
# TODO: delete this once CONST has no dtype
|
||||
(UPat(Ops.CAST, dtypes.all, name="root", src=(UPat.cvar("c"),)), lambda root, c: root.const_like(c.val)),
|
||||
(UPat(GroupOp.Unary, src=(const_folding_pat,), name="a"), fold_const_alu),
|
||||
# NOTE: THREEFRY(const,const) folds via its decomposition
|
||||
(UPat(GroupOp.Binary-{Ops.THREEFRY}, src=(const_folding_pat,)*2, name="a"), fold_const_alu),
|
||||
|
||||
+21
-21
@@ -11,12 +11,24 @@ def commit_weak(s:UOp, dt:DType) -> UOp:
|
||||
# a CONST commits directly at dt (the value stays mathematical, emission truncates), a non-const src takes the cast
|
||||
return UOp.const(s.val, dt) if s.op is Ops.CONST else s.cast(dt)
|
||||
|
||||
def commit_weak_srcs(u:UOp) -> UOp|None:
|
||||
if not any(s.dtype in dtypes.weaks for s in u.src): return None
|
||||
if (dt:=least_upper_dtype(*(s.dtype for s in u.src))) in dtypes.weaks: return None
|
||||
def commit_srcs_at(u:UOp, dt:DType) -> UOp:
|
||||
# the root re-derives: a shift's dtype is its lhs's, so committing the lhs commits the node too
|
||||
return u.replace(dtype=None, src=tuple(commit_weak(s, dt) if s.dtype in dtypes.weaks else s for s in u.src))
|
||||
|
||||
def commit_weak_srcs(u:UOp) -> UOp|None:
|
||||
if not any(s.dtype in dtypes.weaks for s in u.src) or (dt:=least_upper_dtype(*(s.dtype for s in u.src))) in dtypes.weaks: return None
|
||||
return commit_srcs_at(u, dt)
|
||||
|
||||
# a concrete CAST over a weak node states the width the value will live at. that width is a floor, never a narrowing
|
||||
def cast_weak_srcs(c:UOp, u:UOp) -> UOp|None:
|
||||
if c.dtype in dtypes.weaks or weak_dtype(c.dtype) is not u.dtype: return None
|
||||
return commit_srcs_at(u, least_upper_dtype(c.dtype, default_dtype(u))).cast(c.dtype)
|
||||
|
||||
pm_cast_weak = PatternMatcher([
|
||||
(UPat(Ops.CAST, name="c", src=(UPat(GroupOp.ALU, dtype=dtypes.weaks, name="u"),)), cast_weak_srcs),
|
||||
(UPat(Ops.CAST, name="c", src=(UPat(Ops.CONST, dtype=dtypes.weaks, name="u"),)), lambda c,u: commit_weak(u, c.dtype)),
|
||||
])
|
||||
|
||||
# runs in index lowering and in the decomps: a rule that mints a weak const commits it in the same rewrite, so none reaches the renderer
|
||||
pm_commit_weak = PatternMatcher([
|
||||
(UPat(GroupOp.Broadcastable, name="u"), commit_weak_srcs),
|
||||
@@ -25,20 +37,13 @@ pm_commit_weak = PatternMatcher([
|
||||
lambda u: u.replace(src=(u.src[0], commit_weak(u.src[1], u.src[0].dtype), *u.src[2:]))),
|
||||
])
|
||||
|
||||
# a concrete CAST over a weak node states the width the value will live at. that width is a floor, never a narrowing
|
||||
def cast_weak_srcs(c:UOp, u:UOp) -> UOp|None:
|
||||
if c.dtype in dtypes.weaks or weak_dtype(c.dtype) is not u.dtype: return None
|
||||
dt = least_upper_dtype(c.dtype, default_dtype(u))
|
||||
return u.replace(dtype=None, src=tuple(commit_weak(s, dt) if s.dtype in dtypes.weaks else s for s in u.src)).cast(c.dtype)
|
||||
|
||||
pm_cast_weak = PatternMatcher([
|
||||
(UPat(Ops.CAST, name="c", src=(UPat(GroupOp.ALU, dtype=dtypes.weaks, name="u"),)), cast_weak_srcs),
|
||||
(UPat(Ops.CAST, name="c", src=(UPat(Ops.CONST, dtype=dtypes.weaks, name="u"),)), lambda c,u: commit_weak(u, c.dtype)),
|
||||
])
|
||||
|
||||
# A weakfloat Unary (sin/exp2/...) must resolve here, before the transcendental decomposition.
|
||||
_lower_weak_ops = GroupOp.Binary|GroupOp.Unary|{Ops.WHERE, Ops.RANGE, Ops.STACK, Ops.SPECIAL}
|
||||
def lower_weak_node(u:UOp) -> UOp|None:
|
||||
start, src = (1 if u.op is Ops.WHERE else 0), tuple(s.src[0] if s.op is Ops.CAST and s.dtype in dtypes.weaks else s for s in u.src)
|
||||
src = tuple(s.src[0] if s.op is Ops.CAST and s.dtype in dtypes.weaks else s for s in u.src)
|
||||
start = 1 if u.op is Ops.WHERE else 0 # WHERE's cond is bool, never part of the width unification
|
||||
if src == u.src or any(s.dtype in dtypes.weaks for s in src[start:]): return None
|
||||
# Binary can widen from the bounds, all other nodes derive from the lowered sources.
|
||||
dt = strong_dtype(least_upper_dtype(default_dtype(u), *(s.dtype for s in src)) if u.op in GroupOp.Binary
|
||||
else unwrap(dtype_from_uop(u.op, src, u.arg)))
|
||||
return u.replace(dtype=None, src=src[:start]+tuple(s if s.base.is_invalid else commit_weak(s, dt) for s in src[start:])).cast(u.dtype)
|
||||
@@ -49,11 +54,9 @@ pm_lower_weak = PatternMatcher([
|
||||
# a SINGLE weak cast is never rewritten here, each consumer absorbs it on its own edge (see lower_weak_srcs)
|
||||
(UPat(Ops.CAST, dtype=dtypes.weaks, src=(UPat(Ops.CAST, dtype=dtypes.weaks, src=(UPat.var("x"),)),), name="u"),
|
||||
lambda u,x: x.cast(default_dtype(u.src[0])).cast(default_dtype(u)).cast(u.dtype) if x.dtype not in dtypes.weaks else None),
|
||||
# Binary can widen from the bounds, all other nodes derive from the lowered sources.
|
||||
# a weakfloat Unary (sin/exp2/...) must resolve here, before the transcendental decomposition
|
||||
(UPat(GroupOp.Binary|GroupOp.Unary|{Ops.WHERE, Ops.RANGE, Ops.STACK, Ops.SPECIAL}, name="u"), lower_weak_node),
|
||||
(UPat((Ops.PARAM, Ops.BUFFER), dtype=dtypes.weakint, name="u"),
|
||||
lambda u: u.replace(dtype=None, arg=replace(u.arg, dtype=default_dtype(u))).cast(dtypes.weakint) if u.addrspace == AddrSpace.ALU else None),
|
||||
(UPat(_lower_weak_ops, name="u"), lower_weak_node),
|
||||
])
|
||||
|
||||
def lower_weak_srcs(ctx:dict[UOp, UOp]|None, u:UOp) -> UOp|None:
|
||||
@@ -69,9 +72,6 @@ def lower_weak_srcs(ctx:dict[UOp, UOp]|None, u:UOp) -> UOp|None:
|
||||
return None if ret is u else ret
|
||||
|
||||
pm_lower_index_dtype = pm_commit_weak+pm_cast_weak+PatternMatcher([
|
||||
# a CAST between two concrete dtypes over a CONST is a value conversion: evaluate it once, at the width the CAST states
|
||||
# TODO: delete this once CONST has no dtype
|
||||
(UPat(Ops.CAST, dtypes.all, name="root", src=(UPat.cvar("c", dtypes.all),)), lambda root, c: root.const_like(c.val)),
|
||||
(UPat(GroupOp.All, name="u"),
|
||||
lambda ctx,u: lower_weak_srcs(ctx, u) if u.dtype not in dtypes.weaks and any(s.dtype in dtypes.weaks for s in u.src) else None),
|
||||
# a valid index into an n-element buffer lives in [0,n): a gated long index narrows when n-1 fits int32 (out-of-gate wraps, discarded)
|
||||
|
||||
+10
-9
@@ -231,10 +231,11 @@ def timeline_layout(data:VizData, dev_events:list[tuple[int, int, float, DevEven
|
||||
ei:ProfilePointEvent|None = None
|
||||
for st,et,dur,e in dev_events:
|
||||
if isinstance(e, ProfilePointEvent) and e.name == "exec": ei = e
|
||||
if dur == 0: continue
|
||||
# only visualize range events with an end timestamp
|
||||
if dur == 0 or isinstance(e, ProfilePointEvent): continue
|
||||
name, key = e.name, None
|
||||
fmt:dict = {}
|
||||
if (ref:=data.ref_map.get(name)) is not None and ref < len(data.ctxs):
|
||||
if (ref:=data.ref_map.get(e.profile_key)) is not None and ref < len(data.ctxs):
|
||||
name = data.ctxs[ref]["name"]
|
||||
if (ki:=data.ctxs[ref].get("ki")) is not None and ki.estimates is not None and ei is not None:
|
||||
for est_key,est_val in (("FLOPS", ki.estimates.ops), ("B/s mem", ki.estimates.mem), ("B/s lds", ki.estimates.lds)):
|
||||
@@ -333,14 +334,14 @@ def unpack_pmc(e) -> dict:
|
||||
|
||||
def load_amd_counters(data:VizData, profile:list) -> None:
|
||||
counter_events:dict[tuple[int, int], dict] = {}
|
||||
durations:dict[str, list[float]] = {}
|
||||
durations:dict[bytes|str, list[float]] = {}
|
||||
prg_events:dict[int, ProfileProgramEvent] = {}
|
||||
arch = ""
|
||||
for e in profile:
|
||||
if type(e).__name__ in {"ProfilePMCEvent", "ProfileSQTTEvent"}:
|
||||
counter_events.setdefault((e.kern, e.exec_tag), {}).setdefault(type(e).__name__, []).append(e)
|
||||
if isinstance(e, ProfileRangeEvent) and e.device.startswith("AMD") and e.en is not None:
|
||||
durations.setdefault(str(e.name), []).append(float(e.en-e.st))
|
||||
if isinstance(e, ProfileRangeEvent) and e.device.startswith("AMD") and e.en is not None and e.profile_key is not None:
|
||||
durations.setdefault(e.profile_key, []).append(float(e.en-e.st))
|
||||
if isinstance(e, ProfileProgramEvent) and e.device.startswith("AMD") and e.tag is not None: prg_events[e.tag] = e
|
||||
if isinstance(e, ProfileDeviceEvent) and e.device.startswith("AMD"): arch = f"gfx{unwrap(e.props)['gfx_target_version']//1000}"
|
||||
if len(counter_events) == 0: return None
|
||||
@@ -348,12 +349,12 @@ def load_amd_counters(data:VizData, profile:list) -> None:
|
||||
run_number = {n:0 for n,_ in counter_events}
|
||||
for (k, tag),v in counter_events.items():
|
||||
# use the colored name if it exists
|
||||
name = data.ctxs[r]["ki"].name if (r:=data.ref_map.get(pname:=prg_events[k].name)) is not None else pname
|
||||
name = data.ctxs[r]["ki"].name if (r:=data.ref_map.get(unwrap(prg_events[k].profile_key))) is not None else prg_events[k].name
|
||||
run_number[k] += 1
|
||||
steps:list[dict] = []
|
||||
if (pmc:=v.get("ProfilePMCEvent")):
|
||||
steps.append(create_step("PMC", ("/prg-pmc", len(data.ctxs), len(steps)), pmc[0]))
|
||||
all_counters[(name, run_number[k], pname)] = pmc[0]
|
||||
all_counters[(name, run_number[k], unwrap(prg_events[k].profile_key))] = pmc[0]
|
||||
# to decode a SQTT trace, we need the raw stream, program binary and device properties
|
||||
if (sqtt:=v.get("ProfileSQTTEvent")):
|
||||
for e in sqtt:
|
||||
@@ -496,10 +497,10 @@ def get_profile(data:VizData, profile:list[ProfileEvent], sort_fn:Callable[[str]
|
||||
def load_nv_counters(data:VizData, profile:list) -> None:
|
||||
steps:list[dict] = []
|
||||
sm_version = {e.device:e.props.get("sm_version", 0x800) for e in profile if isinstance(e, ProfileDeviceEvent) and e.props is not None}
|
||||
run_number:dict[str, int] = {}
|
||||
run_number:dict[bytes, int] = {}
|
||||
for e in profile:
|
||||
if type(e).__name__ == "ProfilePMAEvent":
|
||||
run_number[e.kern] = run_num = run_number.get(e.kern, 0)+1
|
||||
run_number[profile_key] = run_num = run_number.get(profile_key:=unwrap(e.profile_key), 0)+1
|
||||
steps.append(create_step(f"PMA {e.kern}"+(f"n{run_num}" if run_num>1 else ""), ("/prg-pma-pkts", len(data.ctxs), len(steps)),
|
||||
data=(e.blob, sm_version[e.device])))
|
||||
if steps: data.ctxs.append({"name":"All Counters", "steps":steps})
|
||||
|
||||
Reference in New Issue
Block a user