mirror of
https://github.com/tinygrad/tinygrad.git
synced 2026-08-22 01:06:08 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
369b9d0c27 |
@@ -61,7 +61,6 @@ runs:
|
||||
echo "MAX_BUFFER_SIZE=300000000" >> "$GITHUB_ENV"
|
||||
if [[ "$RUNNER_OS" == "Linux" ]]; then
|
||||
echo "VIRTUAL_ENV=/opt/venv/${{ inputs.python-version }}" >> "$GITHUB_ENV"
|
||||
echo "UV_PYTHON_INSTALL_DIR=/opt/python" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "VIRTUAL_ENV=${{ github.workspace }}/.venv" >> "$GITHUB_ENV"
|
||||
fi
|
||||
@@ -71,6 +70,11 @@ runs:
|
||||
with:
|
||||
enable-cache: 'false' # see below for manual caching
|
||||
|
||||
- name: Set up Python ${{ inputs.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
|
||||
# **** Caching packages ****
|
||||
|
||||
- name: Cache Python packages (PR)
|
||||
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
HCQ2: "0"
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -141,7 +141,7 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
HCQ2: "0"
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -190,7 +190,7 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
HCQ2: "0"
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -233,7 +233,7 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
HCQ2: "0"
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -279,7 +279,7 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
HCQ2: "0"
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -634,9 +634,6 @@ jobs:
|
||||
run: |
|
||||
GRAPH_ONE_KERNEL=1 NSZ=8192 python3 test/speed/external_test_copy_speed.py TestCopySpeed.testCopyDefaulttoCPUJit
|
||||
GRAPH_ONE_KERNEL=1 NSZ=8192 python3 test/speed/external_test_copy_speed.py TestCopySpeed.testCopyCPUtoDefaultJit
|
||||
- name: HEVC Decode Benchmark
|
||||
if: ${{ matrix.dev == 'NV' }}
|
||||
run: VALIDATE=1 MAX_FRAMES=100 ASSERT_FPS=1400 JITBEAM=1 PYTHONPATH=. python3 extra/hevc/decode.py
|
||||
- name: Run 10 MLPerf ResNet50 training steps (1 gpu)
|
||||
if: ${{ matrix.dev == 'NV' }}
|
||||
run: BENCHMARK_LOG=resnet_10steps MNISTMOCK=1 DEFAULT_FLOAT=HALF BENCHMARK=10 BS=256 GPUS=1 MODEL=resnet python3 examples/mlperf/model_train.py
|
||||
|
||||
@@ -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: VIZ=-2 python3 -m pytest -n=auto test/amd/test_sqtt_profiler.py
|
||||
run: PROFILE=1 SQTT=1 python3 -m pytest -n=auto test/amd/test_sqtt_profiler.py
|
||||
- name: Run AMD emulated tests on NULL backend
|
||||
env:
|
||||
AMD: 0
|
||||
@@ -679,5 +679,4 @@ jobs:
|
||||
run: |
|
||||
python -c "from tinygrad import Device; assert Device.DEFAULT == 'NULL'"
|
||||
DEBUG=4 python3 test/backend/test_ops.py TestOps.test_add
|
||||
# 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
|
||||
python -m pytest -n=auto test/backend/test_ops.py --durations=20
|
||||
|
||||
@@ -69,4 +69,3 @@ mutants
|
||||
dagre/
|
||||
graphlib/
|
||||
uv.lock
|
||||
pi_session_window0.jsonl
|
||||
|
||||
@@ -107,21 +107,14 @@ def compile(onnx_file):
|
||||
return inputs, test_val
|
||||
|
||||
def test_vs_compile(run, inputs, test_val=None):
|
||||
if (log:=bool(getenv("BENCHMARK_LOG", ""))): from extra.bench_log import WallTimeEvent, BenchEvent
|
||||
|
||||
# run 20 times
|
||||
step_times = []
|
||||
for _ in range(20):
|
||||
st = time.perf_counter()
|
||||
if log:
|
||||
with WallTimeEvent(BenchEvent.STEP):
|
||||
out = run(**inputs)
|
||||
mt = time.perf_counter()
|
||||
val = out.numpy()
|
||||
else:
|
||||
out = run(**inputs)
|
||||
mt = time.perf_counter()
|
||||
val = out.numpy()
|
||||
out = run(**inputs)
|
||||
mt = time.perf_counter()
|
||||
val = out.numpy()
|
||||
et = time.perf_counter()
|
||||
step_times.append((et-st)*1e3)
|
||||
print(f"enqueue {(mt-st)*1e3:6.2f} ms -- total run {step_times[-1]:6.2f} ms")
|
||||
@@ -167,6 +160,12 @@ def test_vs_onnx(new_inputs, test_val, onnx_file, tol):
|
||||
print("test vs onnx passed")
|
||||
return timings
|
||||
|
||||
def bench(run, inputs):
|
||||
from extra.bench_log import WallTimeEvent, BenchEvent
|
||||
for _ in range(10):
|
||||
with WallTimeEvent(BenchEvent.STEP):
|
||||
run(**inputs).numpy()
|
||||
|
||||
if __name__ == "__main__":
|
||||
if getenv("RUN_PICKLE"):
|
||||
with open(OUTPUT, "rb") as f: pickle_loaded = load_pickle(f)
|
||||
@@ -182,3 +181,6 @@ if __name__ == "__main__":
|
||||
test_vs_compile(pickle_loaded, inputs, outputs)
|
||||
if getenv("SELFTEST"):
|
||||
test_vs_onnx(inputs, outputs, onnx_file, 1e-4)
|
||||
|
||||
if getenv("BENCHMARK_LOG", ""):
|
||||
bench(pickle_loaded, inputs)
|
||||
|
||||
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"].profile_key)}
|
||||
return {"rows":[tuple(v.values()) for v in rows.values()], "cols":columns, "metadata":[summary], "ref":viz_data.ref_map.get(data["prg"].name)}
|
||||
|
||||
def print_data(data:dict) -> None:
|
||||
from tabulate import tabulate
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
import unittest, contextlib
|
||||
from tinygrad import Device, Tensor, Context, TinyJit
|
||||
from tinygrad.device import Compiled, ProfileProgramEvent
|
||||
from tinygrad.device import Compiled, ProfileProgramEvent, ProfileDeviceEvent
|
||||
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, [e for e in Compiled.profile_events[:profile_start] if isinstance(e, ProfileProgramEvent)] +
|
||||
Compiled.profile_events[profile_start:])
|
||||
load_amd_counters(data, Compiled.profile_events)
|
||||
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, is_cdna4
|
||||
from test.backend.test_asm_gemm import has_hipcc
|
||||
|
||||
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() and is_cdna4(), "backward kernel requires hipcc to compile")
|
||||
@unittest.skipUnless(has_hipcc(), "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, lower_and_compile, pm_beam
|
||||
from tinygrad.engine.realize import run_linear, compile_linear, pm_beam, pm_compile
|
||||
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 = lower_and_compile(graph_rewrite(lin, pm_beam, ctx=1, walk=True)).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]
|
||||
self.assertNotEqual(call.src[0].src[0].arg.applied_opts, ())
|
||||
|
||||
def test_shard_same_device(self):
|
||||
|
||||
@@ -77,14 +77,6 @@ class TestCStyleFailures(unittest.TestCase):
|
||||
|
||||
@unittest.skipUnless(isinstance(Device[Device.DEFAULT].renderer, WGSLRenderer), "tests for wgsl renderer")
|
||||
class TestWGSLFailures(unittest.TestCase):
|
||||
def test_folded_packed_store(self):
|
||||
b = UOp.param(0, dtypes.char, (4,))
|
||||
idx = b.index(UOp.const(0).cast(dtypes.int))
|
||||
store = UOp.store(idx, UOp.load(idx, dtype=dtypes.uint32) & UOp.const(0xffffff00).cast(dtypes.uint32))
|
||||
src = Device[Device.DEFAULT].renderer.render(UOp.sink(store, arg=KernelInfo()).toposort())
|
||||
self.assertIn("atomicAnd(&data0_4[0],4294967040u);", src)
|
||||
self.assertNotIn("atomicAdd", src)
|
||||
|
||||
def test_multiply_infinity(self):
|
||||
# multiplying a positive constant by infinity should return infinity
|
||||
# WGSL pipelines do not handle this reliably, some of which return zero, unless infinity always comes from a read on a dynamic buffer
|
||||
|
||||
+10
-40
@@ -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, ceildiv
|
||||
from tinygrad.helpers import Context
|
||||
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
|
||||
@@ -57,35 +57,6 @@ def _test_uops_result(output_dtype, uops, res):
|
||||
run_uops([out], [buf])
|
||||
return np.frombuffer(buf.as_memoryview(), _to_np_dtype(output_dtype))[0]
|
||||
|
||||
@unittest.skipUnless(isinstance(Device[Device.DEFAULT].renderer, CStyleLanguage) and
|
||||
dtypes.uint64 in Device[Device.DEFAULT].renderer.supported_dtypes(), "requires C-style pointer bitcast and 64-bit ints")
|
||||
class TestBitcastBufferView(unittest.TestCase):
|
||||
@Context(SPEC=2)
|
||||
def test_render(self):
|
||||
buf = UOp.param(0, dtypes.uint32, (4,))
|
||||
uops = to_uops_list([buf.shrink(((1, 3),)).bitcast(dtypes.uint64).index(0).store(1)], ren=Device[Device.DEFAULT].renderer)
|
||||
idx = next(u for u in uops if u.op is Ops.INDEX and u.src[0].op is Ops.BITCAST)
|
||||
self.assertEqual(idx.src[0].src[0].op, Ops.SHRINK)
|
||||
Device[Device.DEFAULT].renderer.render(uops)
|
||||
|
||||
@Context(SPEC=2)
|
||||
def test_load(self):
|
||||
val = 0x1122334455667788
|
||||
src, out = UOp.param(0, dtypes.uint32, (4,)), UOp.param(1, dtypes.uint64, (1,))
|
||||
ibuf = Buffer(Device.DEFAULT, 4, dtypes.uint32, initial_value=np.array([0, 0x55667788, 0x11223344, 0], dtype=np.uint32).tobytes())
|
||||
obuf = Buffer(Device.DEFAULT, 1, dtypes.uint64).allocate()
|
||||
run_uops([out.index(0).store(src.shrink(((1, 3),)).bitcast(dtypes.uint64).index(0))], [ibuf, obuf])
|
||||
self.assertEqual(np.frombuffer(obuf.as_memoryview(), dtype=np.uint64)[0], val)
|
||||
|
||||
@Context(SPEC=2)
|
||||
def test_store(self):
|
||||
val = 0x1122334455667788
|
||||
dst = UOp.param(0, dtypes.uint32, (6,))
|
||||
buf = Buffer(Device.DEFAULT, 6, dtypes.uint32, initial_value=bytes(24))
|
||||
view = dst.shrink(((1, 5),)).bitcast(dtypes.uint64) # two stores through one view: it must inline, not get a declared vector-pointer
|
||||
run_uops([view.index(0).store(val ^ 0xff), view.index(1).store(val)], [buf])
|
||||
self.assertEqual(np.frombuffer(buf.as_memoryview(), dtype=np.uint64, count=2, offset=4).tolist(), [val ^ 0xff, val])
|
||||
|
||||
class TestUOps(unittest.TestCase):
|
||||
def _equal(self, v1, v2):
|
||||
assert isinstance(v2, (float, int, bool))
|
||||
@@ -222,16 +193,15 @@ 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]
|
||||
# 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)
|
||||
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)
|
||||
|
||||
@unittest.skipUnless(Device[Device.DEFAULT].renderer.has_shared, "test requires shared memory")
|
||||
@unittest.skip("tinygrad doesn't support this behavior")
|
||||
|
||||
+5
-19
@@ -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 + (index & 0xFF) * 0x4000, (value & 0x7FFF) * 512)
|
||||
op = ("sram_read" if value & 0x8000 else "sram_write", 0xF000, (value & 0x7FFF) * 512)
|
||||
if value & 0x8000: self._bulk_read_op = op
|
||||
else: self._bulk_write_op = op
|
||||
elif request == 0xF0:
|
||||
@@ -193,33 +193,19 @@ class MockUSB3:
|
||||
op, address, size = self._bulk_write_op
|
||||
assert len(data) == size
|
||||
if op == "sram_write":
|
||||
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)
|
||||
host_addr, region_size = self.state._dma_regions[address]
|
||||
ctypes.memmove(host_addr, data, min(len(data), region_size))
|
||||
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 control_write_async(self, request:int, value:int=0, index:int=0, data:bytes=b"", timeout:int=1000) -> int:
|
||||
self.control_write(request, value, index, data, timeout)
|
||||
return 0
|
||||
|
||||
def control_read_async(self, request:int, length:int, value:int=0, index:int=0, timeout:int=1000) -> tuple[int, memoryview]:
|
||||
return 0, self.control_read(request, length, value, index, timeout)
|
||||
|
||||
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":
|
||||
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)))
|
||||
host_addr, region_size = self.state._dma_regions[address]
|
||||
data = bytes((ctypes.c_ubyte * min(length, region_size)).from_address(host_addr))
|
||||
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
|
||||
|
||||
@@ -149,13 +149,6 @@ class TestSymbolic(unittest.TestCase):
|
||||
def test_xor_0(self):
|
||||
self.helper_test_variable(Variable("a", 0, 8, dtypes.int) ^ 0, 0, 8, "a", test_z3=False)
|
||||
|
||||
def test_or_0(self):
|
||||
self.helper_test_variable(Variable("a", 0, 8, dtypes.int) | 0, 0, 8, "a", test_z3=False)
|
||||
|
||||
def test_shift_0(self):
|
||||
self.helper_test_variable(Variable("a", 0, 8, dtypes.int) << 0, 0, 8, "a")
|
||||
self.helper_test_variable(Variable("a", 0, 8, dtypes.int) >> 0, 0, 8, "a")
|
||||
|
||||
def test_xor_self_inverse(self):
|
||||
self.helper_test_variable((Variable("a", 0, 8, dtypes.int) ^ 5) ^ 5, 0, 8, "a", test_z3=False)
|
||||
|
||||
|
||||
+4
-13
@@ -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, 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.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.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
|
||||
@@ -301,9 +301,9 @@ class TestFastIdiv(unittest.TestCase):
|
||||
self.assertNotIn(Ops.CMOD, ops, f"For dtype={dt} FLOORMOD by pow2 left a MOD")
|
||||
self.assertNotIn(Ops.FLOORMOD, ops, f"For dtype={dt} FLOORMOD survived past late rewrite")
|
||||
|
||||
def test_floordiv_power_of_two(self):
|
||||
# FLOORDIV by a power of two lowers to a shift, with no round toward zero correction (a shift is exactly floor division)
|
||||
for dt in (dtypes.int32, dtypes.uint32, dtypes.int64, dtypes.uint64):
|
||||
def test_floordiv_power_of_two_uint(self):
|
||||
# uint FLOORDIV by a power of two lowers to a shift, leaving no IDIV/FLOORDIV in the kernel
|
||||
for dt in (dtypes.uint32, dtypes.uint64):
|
||||
g = UOp.param(0, dt, (3,))
|
||||
c = UOp.const(2).cast(dt)
|
||||
a = UOp(Ops.FLOORDIV, dt, (g.index(c), c))
|
||||
@@ -311,7 +311,6 @@ class TestFastIdiv(unittest.TestCase):
|
||||
ops = [x.op for x in uops]
|
||||
self.assertIn(Ops.SHR, ops, f"For dtype={dt} FLOORDIV by power of two did not simplify to shift")
|
||||
self.assertNotIn(Ops.CDIV, ops, f"For dtype={dt} FLOORDIV by power of two did not simplify to shift")
|
||||
self.assertNotIn(Ops.CMOD, ops, f"For dtype={dt} FLOORDIV by pow2 kept the round toward zero correction")
|
||||
self.assertNotIn(Ops.FLOORDIV, ops, f"For dtype={dt} FLOORDIV survived past late rewrite")
|
||||
|
||||
@Context(DISABLE_FAST_IDIV=0)
|
||||
@@ -458,14 +457,6 @@ 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), "{}")
|
||||
|
||||
+7
-21
@@ -1,5 +1,5 @@
|
||||
import unittest
|
||||
import decimal, sys, json, contextlib, tempfile, pickle, io, math, pathlib
|
||||
import unittest, decimal, sys, json, contextlib, tempfile, pickle, io, math
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
from typing import Generator
|
||||
|
||||
@@ -516,22 +516,6 @@ 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
|
||||
@@ -835,6 +819,8 @@ 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:
|
||||
@@ -843,7 +829,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="NULL::gfx1100"):
|
||||
with Context(DEV=f"NULL::{self.arch}"):
|
||||
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)
|
||||
@@ -1025,8 +1011,8 @@ def run_cli(*cli_args) -> list[dict]:
|
||||
@contextlib.contextmanager
|
||||
def write_files(viz) -> list[str]:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
(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))
|
||||
(r:=Path(tmpdir)/"rewrites.pkl").write_bytes(pickle.dumps(viz.data.trace))
|
||||
(p:=Path(tmpdir)/"profile.pkl").write_bytes(pickle.dumps(cpu_events))
|
||||
yield ["--rewrites-path", str(r), "--profile-path", str(p)]
|
||||
|
||||
class TestCLI(unittest.TestCase):
|
||||
|
||||
@@ -224,21 +224,6 @@ 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,8 +1,6 @@
|
||||
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
|
||||
@@ -154,22 +152,6 @@ 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, NUM_CPU_THREADS, TC_SELECT, TC_OPT, TracingKey, Context, panic
|
||||
from tinygrad.helpers import ALLOW_TF32, DEFAULT_FLOAT, DEFAULT_INT, 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
|
||||
@@ -233,11 +233,10 @@ pm_reduce_local = pm_wmma_add+PatternMatcher([
|
||||
(UPat(Ops.SINK, name="sink"), merge_reduce_ends),
|
||||
])+pm_clean_up_group_sink
|
||||
|
||||
def is_shape_changing_bitcast(u:UOp): return u.op is Ops.BITCAST and u.shape != u.src[0].shape
|
||||
def maybe_load(u:UOp): return u.load() if u.addrspace in (AddrSpace.GLOBAL, AddrSpace.LOCAL, AddrSpace.REG) else u
|
||||
pm_add_loads = PatternMatcher([
|
||||
(UPat(GroupOp.Elementwise|{Ops.REDUCE,Ops.WMMA,Ops.STACK}, name="x"),
|
||||
lambda x: None if is_shape_changing_bitcast(x) else x.replace(src=tuple(map(maybe_load, x.src)))),
|
||||
# BITCAST?
|
||||
(UPat(GroupOp.Elementwise|{Ops.REDUCE,Ops.WMMA,Ops.STACK}, name="x"), lambda x: x.replace(src=tuple([maybe_load(u) for u in x.src]))),
|
||||
(UPat(Ops.STORE, name="x"), lambda x: x.replace(src=(x.src[0], maybe_load(x.src[1]))+x.src[2:])),
|
||||
])
|
||||
|
||||
@@ -378,10 +377,6 @@ 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")
|
||||
|
||||
@@ -392,6 +387,10 @@ 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)
|
||||
|
||||
@@ -460,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,**_: TracingKey((k:=ret.src[0].arg).name,(k.function_name, ast, ret.key),ret=renderer), replay=True)
|
||||
@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)
|
||||
@Context(ALLOW_DEVICE_USAGE=0)
|
||||
def do_to_program(ast:UOp, renderer:Renderer) -> UOp:
|
||||
"""
|
||||
@@ -489,14 +488,9 @@ 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:
|
||||
if (prg:=to_program_cache.get(key:=to_program_key(ast, renderer))) is None: to_program_cache[key] = prg = do_to_program(ast, renderer)
|
||||
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)
|
||||
return prg
|
||||
|
||||
@@ -25,10 +25,10 @@ def l2i(op: Ops, dt: DType, *uops:UOp):
|
||||
match op:
|
||||
case Ops.NEG: return l2i(Ops.SUB, dt, zero, zero, *uops)
|
||||
case Ops.CAST if dt in (dtypes.long, dtypes.ulong) and uops[0].dtype not in dtypes.floats:
|
||||
# the high word is the sign extension, and unsigned and bool sources zero extend
|
||||
# the high word is the sign extension; bool has no sign, test the already-cast low word instead (bool < 0 would promote to weakint)
|
||||
x, lo = uops[0], uops[0].cast(l2i_dt[dt])
|
||||
if x.dtype is dtypes.bool or x.dtype in dtypes.uints: return lo, lo.const_like(0)
|
||||
return lo, (x < x.const_like(0)).where(lo.const_like(-1), lo.const_like(0))
|
||||
sign = lo if x.dtype is dtypes.bool else x
|
||||
return lo, (sign < sign.const_like(0)).where(lo.const_like(-1), lo.const_like(0))
|
||||
case Ops.CAST if dt in (dtypes.long, dtypes.ulong):
|
||||
return (lo:=uops[0].cast(l2i_dt[dt])), (uops[0] / 2**32).cast(l2i_dt[dt]) - ((uops[0] < 0) & lo.ne(0))
|
||||
case Ops.CAST if dt in dtypes.floats:
|
||||
|
||||
@@ -75,11 +75,7 @@ powers_of_two: dict[int, int] = {2**i:i for i in range(64)}
|
||||
@functools.cache
|
||||
def get_simplifying_rewrite_patterns(ops:tuple[Ops, ...]) -> PatternMatcher:
|
||||
# these are rewrites that make things simpler
|
||||
pat: list[tuple[UPat, Callable]] = []
|
||||
# FLOORDIV by 2**y -> x >> y (an arithmetic shift is exactly floor division for any sign); fires before floordiv_to_idiv
|
||||
if Ops.SHR in ops: pat.append((UPat.var("x", dtypes.ints)//UPat.cvar("c"),
|
||||
lambda x,c: x >> v if (v:=powers_of_two.get(c.val, 0)) else None))
|
||||
pat.append((UPat.var("a")//UPat.var("b"), floordiv_to_idiv))
|
||||
pat: list[tuple[UPat, Callable]] = [(UPat.var("a")//UPat.var("b"), floordiv_to_idiv)]
|
||||
# FLOORMOD by 2**y -> x & (2**y-1) (correct floor mod for any sign in two's complement); fires before floormod_to_mod
|
||||
if Ops.AND in ops: pat.append((UPat.var("x", dtypes.ints)%UPat.cvar("c"), lambda x,c: x & (c.val-1) if c.val in powers_of_two else None))
|
||||
pat.append((UPat.var("a")%UPat.var("b"), floormod_to_mod))
|
||||
@@ -132,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: 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))]
|
||||
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))]
|
||||
return PatternMatcher(pat)
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import math, time, traceback, signal
|
||||
import math, time, multiprocessing, traceback, signal, atexit
|
||||
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, colored, time_to_str
|
||||
from tinygrad.helpers import prod, flatten, DEBUG, CACHELEVEL, diskcache_get, diskcache_put, getenv, Context, 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
|
||||
|
||||
@@ -79,6 +78,11 @@ 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 ***
|
||||
@@ -107,8 +111,9 @@ def get_kernel_actions(s:Scheduler, include_0=True, max_up:int|None=None) -> dic
|
||||
except KernelOptError: pass
|
||||
return acted
|
||||
|
||||
BEAM_DEBUG = getenv("BEAM_DEBUG")
|
||||
beam_pool, BEAM_DEBUG = None, 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()
|
||||
@@ -118,7 +123,11 @@ 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()
|
||||
|
||||
pool = get_worker_pool()
|
||||
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()
|
||||
|
||||
min_progress = getenv("BEAM_MIN_PROGRESS", 0.01)/1e6
|
||||
if BEAM_DEBUG:
|
||||
@@ -134,7 +143,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 pool is None else pool.imap_unordered)(_try_compile, enumerate(candidates))):
|
||||
for i, proc in ((map if beam_pool is None else beam_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
|
||||
@@ -170,7 +179,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:
|
||||
terminate_worker_pool()
|
||||
if beam_pool is not None: beam_pool.terminate()
|
||||
raise e
|
||||
|
||||
if CACHELEVEL >= 1: diskcache_put("beam_search", key, beam[0][0].applied_opts)
|
||||
|
||||
+3
-5
@@ -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; profile_key:bytes|None=None # noqa: E702
|
||||
class ProfileProgramEvent(ProfileEvent): device:str; name:str; lib:bytes|None; base:int|None; tag:int|None=None # noqa: E702
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProfileGraphEntry: device:str; name:str|TracingKey; st_id:int; en_id:int; profile_key:bytes|None=None # noqa: E702
|
||||
class ProfileGraphEntry: device:str; name:str|TracingKey; st_id:int; en_id:int # noqa: E702
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProfileGraphEvent(ProfileEvent): ents:list[ProfileGraphEntry]; deps:list[list[int]]; sigs:list[decimal.Decimal] # noqa: E702
|
||||
@@ -83,7 +83,6 @@ class BufferSpec:
|
||||
cpu_access: bool = False
|
||||
host: bool = False
|
||||
nolru: bool = False
|
||||
zero: bool = False
|
||||
external_ptr: int|None = None
|
||||
|
||||
class MultiBuffer:
|
||||
@@ -266,7 +265,7 @@ class LRUAllocator(Allocator, Generic[DeviceType]):
|
||||
for opaque in opaques: super().free(opaque, sz, options)
|
||||
opaques.clear()
|
||||
def free(self, opaque:Any, size:int, options:BufferSpec|None=None):
|
||||
if LRU and (options is None or (not (options.nolru or options.zero) and options.external_ptr is None)): self.cache[(size, options)].append(opaque)
|
||||
if LRU and (options is None or (not options.nolru and options.external_ptr is None)): self.cache[(size, options)].append(opaque)
|
||||
else: super().free(opaque, size, options)
|
||||
|
||||
class DepsTracker:
|
||||
@@ -327,7 +326,6 @@ 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]:
|
||||
|
||||
+10
-45
@@ -2,15 +2,14 @@ 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, ansipad, all_int, prod, flatten, Context, getenv, to_tuple, tqdm
|
||||
from tinygrad.helpers import colored, DEBUG, GlobalCounters, ansipad, all_int, prod, flatten, Context, getenv, to_tuple
|
||||
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, ProgramInfo
|
||||
from tinygrad.uop.ops import Ops, PatternMatcher, UOp, UPat, AxisType, sym_infer, graph_rewrite
|
||||
from tinygrad.device import Device, Buffer, MultiBuffer, ProfileGraphEntry
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.renderer import Estimates, Renderer
|
||||
from tinygrad.codegen import to_program, to_program_cache, to_program_key, to_program_context
|
||||
from tinygrad.renderer import Estimates
|
||||
from tinygrad.codegen import to_program
|
||||
from tinygrad.codegen.opt.postrange import args_from_ast
|
||||
from tinygrad.engine.worker import get_worker_pool, terminate_worker_pool
|
||||
|
||||
# **************** Helpers ****************
|
||||
|
||||
@@ -222,7 +221,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[0], prof[1], stat_call.key)
|
||||
(d:=cast(Any, Device[device])).prof_ents[prof[0]] = ProfileGraphEntry(device, stat_call.arg.name, *prof)
|
||||
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)
|
||||
@@ -248,44 +247,10 @@ 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),
|
||||
])
|
||||
|
||||
# **************** 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 _get_call_to_compile(c:UOp) -> tuple[UOp, Renderer]|None:
|
||||
ast = a0.src[0] if (a0:=c.src[0]).op is Ops.CUSTOM_FUNCTION and a0.arg == "hcq" else a0
|
||||
# a PROGRAM with a ProgramInfo and a BINARY is already compiled
|
||||
if ast.op is Ops.SINK or (ast.op is Ops.PROGRAM and not (isinstance(ast.arg, ProgramInfo) and ast.src[-1].op is Ops.BINARY)):
|
||||
return ast, Device[c.device if isinstance(c.device, str) else c.device[0]].renderer
|
||||
return None
|
||||
|
||||
def lower_and_compile(linear:UOp) -> UOp:
|
||||
# collect the kernels to lower and compile, deduped by their compile cache key
|
||||
if not len(ar:={c: a for c in linear.toposort() if c.op is Ops.CALL and (a:=_get_call_to_compile(c)) is not None}): return linear
|
||||
|
||||
# lower and compile what's not cached, in parallel if there's a worker pool
|
||||
keys = {c: to_program_key(*a) for c, a in ar.items()}
|
||||
todo = list({keys[c]: a for c, a in ar.items() 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 ar) 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=(c.src[0].substitute({a[0]: to_program_cache[keys[c]]}), *c.src[1:])) for c, a in ar.items()},
|
||||
name="precompile kernels")
|
||||
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:]))),
|
||||
])
|
||||
|
||||
pm_optimize_local_size = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.PROGRAM, name="prg"),), name="call", allow_any_len=True), optimize_local_size),
|
||||
@@ -305,7 +270,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 = lower_and_compile(linear)
|
||||
linear = graph_rewrite(linear, pm_compile, name="precompile kernels", walk=True)
|
||||
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
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
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
|
||||
+4
-18
@@ -364,8 +364,7 @@ class TracingKey:
|
||||
class ProfileEvent: pass
|
||||
|
||||
@dataclass
|
||||
class ProfileRangeEvent(ProfileEvent):
|
||||
device:str; name:str|TracingKey; st:decimal.Decimal; en:decimal.Decimal|None=None; profile_key:bytes|None=None # noqa: E702
|
||||
class ProfileRangeEvent(ProfileEvent): device:str; name:str|TracingKey; st:decimal.Decimal; en:decimal.Decimal|None=None # noqa: E702
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProfilePointEvent(ProfileEvent):
|
||||
@@ -373,8 +372,8 @@ class ProfilePointEvent(ProfileEvent):
|
||||
|
||||
cpu_events:list[ProfileEvent] = []
|
||||
@contextlib.contextmanager
|
||||
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)
|
||||
def cpu_profile(name:str|TracingKey, device="TINY", display=True) -> Generator[ProfileRangeEvent, None, None]:
|
||||
res = ProfileRangeEvent(device, name, perf_counter_us())
|
||||
try: yield res
|
||||
finally:
|
||||
res.en = perf_counter_us()
|
||||
@@ -465,16 +464,14 @@ 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, extract:bool=False) -> pathlib.Path:
|
||||
headers:dict[str, str]={}, sha256:str|None=None) -> 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
|
||||
@@ -491,17 +488,6 @@ 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:
|
||||
|
||||
@@ -38,8 +38,7 @@ base_rewrite = PatternMatcher([
|
||||
(UPat(Ops.CAST, name="x"), lambda ctx,x: f"__builtin_convertvector({ctx[x.src[0]]}, {ctx.render_type(x)})" \
|
||||
if x.max_numel() > 1 and x.addrspace is AddrSpace.REG else None),
|
||||
(UPat(Ops.CAST, name="x"), lambda ctx,x: f"({ctx.render_cast(x, ctx[x.src[0]])})"),
|
||||
(UPat(Ops.BITCAST, name="x"), lambda ctx,x: f"(({ctx._render_dtype(x.dtype, addrspace=x.addrspace)})({ctx[x.src[0]]}))"
|
||||
if x.addrspace in (AddrSpace.GLOBAL, AddrSpace.LOCAL) else None),
|
||||
(UPat(Ops.BITCAST, name="x"), lambda ctx,x: ctx[x.src[0]] if x.addrspace in (AddrSpace.GLOBAL, AddrSpace.LOCAL) else None),
|
||||
(UPat(Ops.BITCAST, name="x"), lambda ctx,x: f"__builtin_bit_cast({ctx.render_type(x)}, ({ctx.render_type(x.src[0])})({ctx[x.src[0]]}))"),
|
||||
|
||||
# GPU stuff
|
||||
@@ -238,7 +237,7 @@ class CStyleLanguage(Renderer):
|
||||
if (u.op is not Ops.CAST or u.max_numel() == 1) and ((u.op is Ops.CAST and u.src[0].op is Ops.CONST) or \
|
||||
u.op in {Ops.INDEX, Ops.SHRINK, Ops.CUSTOMI} or \
|
||||
(u.op is Ops.LOAD and u.src[0].addrspace == AddrSpace.REG and child_count[u] == 1) or \
|
||||
(u.op in {Ops.CAST, Ops.BITCAST} and u.addrspace in (AddrSpace.GLOBAL, AddrSpace.LOCAL)) or \
|
||||
(u.op is Ops.CAST and u.addrspace in (AddrSpace.GLOBAL, AddrSpace.LOCAL)) or \
|
||||
(u.op in {Ops.STACK, *(GroupOp.ALU-{Ops.WHERE}), Ops.CAST, Ops.BITCAST} and child_count[u] == 1 and not getenv("EXPAND_SSA"))):
|
||||
r[u] = l
|
||||
else:
|
||||
@@ -319,8 +318,7 @@ class OpenCLRenderer(CStyleLanguage):
|
||||
extra_matcher = create_non_native_float_pats((dtypes.bfloat16,)) + pm_manual_bf16_cast
|
||||
|
||||
string_rewrite = PatternMatcher([
|
||||
(UPat(Ops.BITCAST, name="x"), lambda ctx,x: f"as_{ctx.render_dtype(x.dtype)}(({ctx.render_dtype(x.src[0].dtype)})({ctx[x.src[0]]}))"
|
||||
if x.addrspace not in (AddrSpace.GLOBAL, AddrSpace.LOCAL) else None),
|
||||
(UPat(Ops.BITCAST, name="x"), lambda ctx,x: f"as_{ctx.render_dtype(x.dtype)}(({ctx.render_dtype(x.src[0].dtype)})({ctx[x.src[0]]}))"),
|
||||
# bfloat16 constants need to be rendered as their bit pattern since bf16 is stored as ushort
|
||||
(UPat.cvar("c").cast(dtypes.bfloat16), lambda ctx,c: f"{(struct.unpack('I', struct.pack('f', float_to_bf16(c.val)))[0] >> 16)}u"),
|
||||
# load/store image (OpenCL)
|
||||
@@ -371,8 +369,7 @@ class MetalRenderer(CStyleLanguage):
|
||||
]) + pm_manual_bf16_cast
|
||||
|
||||
string_rewrite = PatternMatcher([
|
||||
(UPat(Ops.BITCAST, name="x"), lambda ctx,x: f"as_type<{ctx.render_dtype(x.dtype)}>(({ctx.render_dtype(x.src[0].dtype)})({ctx[x.src[0]]}))"
|
||||
if x.addrspace not in (AddrSpace.GLOBAL, AddrSpace.LOCAL) else None),
|
||||
(UPat(Ops.BITCAST, name="x"), lambda ctx,x: f"as_type<{ctx.render_dtype(x.dtype)}>(({ctx.render_dtype(x.src[0].dtype)})({ctx[x.src[0]]}))"),
|
||||
]) + base_rewrite
|
||||
|
||||
def render_kernel(self, function_name, kernel, bufs, uops, prefix=None):
|
||||
@@ -428,8 +425,7 @@ class CUDARenderer(CStyleLanguage):
|
||||
(UPat(Ops.CAST, dtypes.fp8s, UPat.var("x", dtypes.fp8s), name='y'), lambda x,y: x.cast(dtypes.float).cast(y.dtype) if x.dtype!=y.dtype else None),
|
||||
])
|
||||
string_rewrite = PatternMatcher([
|
||||
(UPat(Ops.BITCAST, name="x"), lambda ctx,x: f"tg_bitcast<{ctx.render_dtype(x.dtype)}>(({ctx.render_dtype(x.src[0].dtype)})({ctx[x.src[0]]}))"
|
||||
if x.addrspace not in (AddrSpace.GLOBAL, AddrSpace.LOCAL) else None),
|
||||
(UPat(Ops.BITCAST, name="x"), lambda ctx,x: f"tg_bitcast<{ctx.render_dtype(x.dtype)}>(({ctx.render_dtype(x.src[0].dtype)})({ctx[x.src[0]]}))"),
|
||||
]) + base_rewrite
|
||||
|
||||
def render_vector_prefix(self, dt:DType, count:int) -> str:
|
||||
|
||||
+32
-29
@@ -1,50 +1,53 @@
|
||||
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, ceildiv
|
||||
from tinygrad.helpers import strip_parens
|
||||
|
||||
# a field of `width` bits sitting in the low bits of val: shift it up to the sign bit, then let the arithmetic shift fill
|
||||
def sign_extend(val:UOp, width:int): return (val << (32-width)).bitcast(dtypes.int) >> (32-width)
|
||||
def _mask(dt:DType): return 0xFF if dt.itemsize == 1 else 0xFFFF
|
||||
|
||||
# a packed field of dt: the word it lives in, its offset in that word, and its mask. width is 8*itemsize, bool is one bit in a byte
|
||||
def packed_field(bidx:UOp, dt:DType) -> tuple[UOp, UOp, int]:
|
||||
elems, width = 4//dt.itemsize, 8*dt.itemsize
|
||||
return bidx.src[0].index(bidx.src[1] // elems), (bidx.src[1].cast(dtypes.uint32) % elems) * width, (1 << width)-1
|
||||
def sign_extend(val:UOp, sext_am:int):
|
||||
return (UOp.where((val >> (sext_am - 1)) > 0, UOp.const(0xffffffff << sext_am, dtypes.uint32), UOp.const(0, dtypes.uint32)) \
|
||||
| val.bitcast(dtypes.uint32)).bitcast(dtypes.int)
|
||||
|
||||
# store for char: buf[idx/4] <- (var << (idx%4)*8))
|
||||
def packed_store(s:UOp):
|
||||
bidx, var, *gate = s.src
|
||||
idx, shift_am, mask = packed_field(bidx, var.dtype)
|
||||
def packed_store(bidx:UOp, var:UOp, gate:UOp|None=None):
|
||||
elems, mask = 4//var.dtype.itemsize, _mask(var.dtype)
|
||||
shift_am, div_idx = (bidx.src[1].cast(dtypes.uint32) % elems) * (8*var.dtype.itemsize), bidx.src[1] // elems
|
||||
# bool does its mask math at int32: renderer rewrites run after weak dtypes are lowered, and bool & 0xFF would create a weakint const
|
||||
if var.dtype == dtypes.bool: var = var.cast(dtypes.int32)
|
||||
new_v, wmask = (var & mask).cast(dtypes.uint32) << shift_am, ((mask << shift_am) ^ 0xFFFFFFFF).cast(dtypes.uint32)
|
||||
buf = idx.load(*((UOp.const(0, dtypes.uint32), *gate) if gate else ()), dtype=dtypes.uint32)
|
||||
return idx.store((buf & wmask) | new_v, *gate)
|
||||
idx = UOp(Ops.INDEX, src=(bidx.src[0], div_idx))
|
||||
buf = UOp.load(idx, *((UOp.const(0, dtypes.uint32), gate) if gate is not None else ()), dtype=dtypes.uint32)
|
||||
return UOp.store(idx, (buf & wmask) | new_v, *((gate,) if gate is not None else ()))
|
||||
|
||||
# load for char: sign_extend(buf[idx/4] >> ((idx%4)*8))
|
||||
def packed_load(root:UOp):
|
||||
bidx, *alt = root.src
|
||||
idx, shift_am, mask = packed_field(bidx, dtype:=root.dtype)
|
||||
load = idx.load(*((alt[0].cast(dtypes.uint32), *alt[1:]) if alt else ()), dtype=dtypes.uint32, arg=root.arg)
|
||||
val = (load >> shift_am) & mask
|
||||
def packed_load(root:UOp, bidx:UOp, dtype:DType, var:UOp|None=None, gate:UOp|None=None):
|
||||
elems, mask = 4//dtype.itemsize, _mask(dtype)
|
||||
shift_am, div_idx = (bidx.src[1].cast(dtypes.uint32) % elems) * (8*dtype.itemsize), bidx.src[1] // elems
|
||||
idx = UOp(Ops.INDEX, src=(bidx.src[0], div_idx))
|
||||
load = UOp.load(idx, *((var, gate) if var is not None and gate is not None else root.src[1:]), dtype=dtypes.uint32, arg=root.arg)
|
||||
val = (load.cast(dtypes.uint32) >> shift_am) & mask
|
||||
return sign_extend(val, 8*dtype.itemsize).cast(dtype) if dtype in [dtypes.char, dtypes.short] else val.cast(dtype)
|
||||
|
||||
def is_packed(x:UOp):
|
||||
dt = x.src[1].dtype if x.op is Ops.STORE else x.dtype
|
||||
return dt.itemsize < 4 and dt != dtypes.half and x.buf_uop.addrspace != AddrSpace.REG
|
||||
def _packed_size(u:UOp): return ceildiv(u.max_numel(), 4//u.dtype.itemsize) if is_packed(u) else u.max_numel()
|
||||
if x.op is Ops.LOAD: dt, addrspace = x.dtype, x.src[0].addrspace
|
||||
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 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)
|
||||
|
||||
# the read-modify-write packed_store emits: a load of the very index being stored to, masked (a gated store loads with 3 srcs)
|
||||
packed_rmw = UPat(Ops.LOAD, src=(UPat.var("b"),), allow_any_len=True) & UPat.var("wmask")
|
||||
|
||||
wgsl_matcher = PatternMatcher([
|
||||
(UPat((Ops.CMPLT, Ops.XOR), src=(UPat(name="a", dtype=dtypes.bool), UPat.var("b")), name="c"),
|
||||
lambda a,b,c: a.cast(dtypes.int).alu(c.op, b.cast(dtypes.int)).cast(dtypes.bool)),
|
||||
(UPat(Ops.LOAD, name="l"), lambda l: packed_load(l) if is_packed(l) else None),
|
||||
(UPat(Ops.STORE, name="s"), lambda s: packed_store(s) if is_packed(s) else None),
|
||||
(UPat.load(UPat.var("b"), UPat.var("c"), UPat.var("gate"), name="l"),
|
||||
lambda l,b,c,gate: packed_load(l,b,l.dtype,c.cast(dtypes.uint32),gate) if is_packed(l) else None),
|
||||
(UPat.load(UPat.var("b"), name='l'), lambda l,b: packed_load(l,b,l.dtype) if is_packed(l) else None),
|
||||
(UPat.store(UPat.var("b"), UPat.var("var"), UPat.var("gate"), name="s"),
|
||||
lambda b,var,gate,s: packed_store(b,var,gate) if is_packed(s) else None),
|
||||
(UPat.store(UPat.var("b"), UPat.var("var"), name="s"), lambda b,var,s: packed_store(b,var) if is_packed(s) else None),
|
||||
(UPat.var("a") << UPat.var("b"),lambda a,b:(a.bitcast(dtypes.uint32)<<b.cast(dtypes.uint32)).bitcast(a.dtype) if b.dtype!=dtypes.uint32 else None),
|
||||
(UPat.var("x") >> UPat.var("y"), lambda x,y: UOp(Ops.SHR, x.dtype, (x,y.cast(dtypes.uint))) if y.dtype != dtypes.uint else None),
|
||||
# fix nan check: 'a != a -> is_nan()'. the decomp rewrites (a != a).logical_not() to CMPEQ, so match both forms
|
||||
@@ -84,10 +87,10 @@ class WGSLRenderer(CStyleLanguage):
|
||||
(UPat.load(UPat.var("b"), UPat.var("v"), UPat.var("gate")),
|
||||
lambda ctx,b,v,gate: f"select({ctx[v]}, {ctx.render_load(ctx[b], b.src[0])}, {ctx[gate]})"),
|
||||
(UPat.load(UPat.var("b")), lambda ctx, b: ctx.render_load(ctx[b], b)),
|
||||
# packed_store writes (load & wmask) | new_v: atomicAnd clears the field, atomicAdd sets it. new_v is gone when it is 0
|
||||
(UPat.store(UPat.var("b"), UPat.any(packed_rmw, packed_rmw | UPat.var("nv"))), lambda ctx,b,wmask,nv=None:
|
||||
f"atomicAnd(&{ctx[b]},{ctx[wmask]});"+(f"\n atomicAdd(&{ctx[b]},{ctx[nv]});" if nv is not None else "") if is_packed(b) else None),
|
||||
(UPat.store(UPat.var("b"), UPat.var("v")), lambda ctx,b,v: f"{ctx[b]} = {ctx[v]};"),
|
||||
(UPat.store(UPat.var("b"), UPat.var("v")), lambda ctx,b,v:\
|
||||
# (load & mask) | var -> mask = v.src[0].src[1], var = v.src[1]
|
||||
f"atomicAnd(&{ctx[b]},{ctx[v.src[0].src[1]]});\n atomicAdd(&{ctx[b]},{ctx[v.src[1]]});" if is_packed(b) \
|
||||
else f"{ctx[b]} = {ctx[v]};"),
|
||||
(UPat(Ops.INDEX, src=(UPat.var("b"), UPat.var("idx"))),
|
||||
lambda ctx,b,idx: f"{ctx[b]}[{strip_parens(ctx[idx]) if idx.arg is Ops.ADD else ctx[idx]}]"),
|
||||
]) + base_rewrite
|
||||
|
||||
@@ -139,8 +139,7 @@ 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,
|
||||
runtime.profile_key if runtime is not None else None))
|
||||
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])
|
||||
|
||||
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, rt.profile_key) for i, rt in enumerate(self.runtimes) if rt is not None]
|
||||
ents = [ProfileGraphEntry(self.device, rt.name, i, i+1) 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, time
|
||||
import os, ctypes, struct, hashlib, functools, importlib, mmap, errno, array, contextlib, sys, weakref, itertools, collections, atexit
|
||||
assert sys.platform != 'win32'
|
||||
from dataclasses import dataclass
|
||||
from tinygrad.runtime.support.hcq import HCQCompiled, HCQAllocator, HCQBuffer, HWQueue, CLikeArgsState, HCQSignal, HCQProgram, FileIOInterface
|
||||
@@ -649,59 +649,6 @@ 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. ~256KB chunks stream into two alternating 256KB SRAM bounce windows; the
|
||||
# engine can't signal data landing, so each chunk's wire image ends in a 4B sentinel 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 = 0x40000 - 4, src.cast('B') # payload per chunk: the 256KB window minus the 4B trailing sentinel
|
||||
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.perf_counter()
|
||||
while int.from_bytes(usb.read(FENCE, 8), 'little') < count:
|
||||
if time.perf_counter() - 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 + 4, 512) - 4), 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]
|
||||
wire = round_up(size + 4, 512) # payload plus the sentinel, padded to 512B sectors (full window for max chunks)
|
||||
struct.pack_into('<I', buf, wire - 4, 0x51000000 | (seq & 0xFFFFFF)) # the sentinel is the last dword of the wire
|
||||
arm_tag = usb.usb.control_write_async(0xF2, wire // 512, (seq & 1) * 16 | (ceildiv(wire, 0x4000) << 8)) # wValue=sectors, wIndex=slot|count
|
||||
rd_tag, rd_mv = usb.usb.control_read_async(0xE4, 8, value=FENCE) # arm and fence read fly in one round-trip window
|
||||
usb.usb.bulk_wait(arm_tag)
|
||||
usb.usb.bulk_wait(rd_tag)
|
||||
if int.from_bytes(rd_mv, 'little') < seq - 1: wait_drain(seq - 1) # rare: the drain lagged; spin on fresh reads
|
||||
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()
|
||||
@@ -977,13 +924,14 @@ class USBIface(PCIIface):
|
||||
region = self.dev_impl.mm.map_range(vaddr:=self.dev_impl.mm.alloc_vaddr(size=size), size, [(sys_addr, size)], aspace=AddrSpace.SYS, uncached=True)
|
||||
return HCQBuffer(vaddr, size, meta=PCIAllocationMeta(region, has_cpu_mapping=False), view=self.pci_dev.dma_view(ctrl_addr, size), owner=self.dev)
|
||||
|
||||
def alloc(self, size:int, host=False, uncached=False, cpu_access=False, contiguous=False, force_devmem=False, zero=False, **kwargs) -> HCQBuffer:
|
||||
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
|
||||
# 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.
|
||||
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)
|
||||
|
||||
# force devmem
|
||||
return super().alloc(size, host=False, uncached=uncached, cpu_access=cpu_access, contiguous=contiguous, force_devmem=True, zero=zero, **kwargs)
|
||||
return super().alloc(size, host=False, uncached=uncached, cpu_access=cpu_access, contiguous=contiguous, force_devmem=True, **kwargs)
|
||||
|
||||
def sleep(self, timeout): pass
|
||||
|
||||
@@ -1100,8 +1048,7 @@ 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):
|
||||
# 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)
|
||||
self.sdma_queues[idx] = self.create_queue(kfd.KFD_IOC_QUEUE_TYPE_SDMA, 0x200 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,7 +34,6 @@ 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
|
||||
|
||||
@@ -56,7 +55,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, self.mtl_profile_keys.pop(id(cbuf), None))]
|
||||
Compiled.profile_events += [ProfileRangeEvent(self.device, lb, st, en)]
|
||||
self.mtl_buffers_in_flight.clear()
|
||||
|
||||
class MetalCompiler(Compiler):
|
||||
@@ -114,7 +113,7 @@ class MetalCompiler(Compiler):
|
||||
|
||||
class MetalProgram(Program[MetalDevice]):
|
||||
def __init__(self, dev:MetalDevice, obj:TinyELF):
|
||||
self.dev, self.name, self.lib, self.signature, self.profile_key = dev, obj.name, obj.lib, obj.signature, obj.profile_key
|
||||
self.dev, self.name, self.lib, self.signature = dev, obj.name, obj.lib, obj.signature
|
||||
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)
|
||||
@@ -146,7 +145,6 @@ 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, self.profile_key = dev.device, obj.name, obj.profile_key
|
||||
def __init__(self, dev:'NullDevice', obj:TinyELF): self.device, self.name = dev.device, obj.name
|
||||
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, profile_key=self.profile_key): return 1e-3
|
||||
with cpu_profile(self.name, self.device): return 1e-3
|
||||
|
||||
class NullAllocator(Allocator['NullDevice']):
|
||||
def _alloc(self, size, options): pass
|
||||
@@ -38,14 +38,13 @@ 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}",
|
||||
runtime.profile_key if runtime is not None else None, count:=event_count.get(device, 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)))
|
||||
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,profile_key,count) in enumerate(descs):
|
||||
for i,(device,name,count) in enumerate(descs):
|
||||
sigs += [st+count*dur, st+(count+1)*dur]
|
||||
ents.append(ProfileGraphEntry(device, name, 2*i, 2*i+1, profile_key))
|
||||
ents.append(ProfileGraphEntry(device, name, 2*i, 2*i+1))
|
||||
cpu_events.append(ProfileGraphEvent(ents, [], sigs))
|
||||
return 1e-1
|
||||
|
||||
|
||||
+12
-13
@@ -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; profile_key:bytes|None=None # noqa: E702
|
||||
class ProfilePMAEvent(ProfileEvent): device:str; kern:str; blob:bytes; exec_tag:int # noqa: E702
|
||||
|
||||
class NVSignal(HCQSignal):
|
||||
def _sleep(self, time_spent_since_last_sleep_ms:int):
|
||||
@@ -335,12 +335,12 @@ 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, self.profile_key)]
|
||||
Compiled.profile_events += [ProfilePMAEvent(self.dev.device, self.name, pma_blob, self.dev.prof_exec_counter)]
|
||||
return res
|
||||
|
||||
class NVAllocator(HCQAllocator['NVDevice']):
|
||||
def _alloc(self, size:int, options:BufferSpec) -> HCQBuffer:
|
||||
return self.dev.iface.alloc(size, cpu_access=options.cpu_access, host=options.host, zero=options.zero)
|
||||
return self.dev.iface.alloc(size, cpu_access=options.cpu_access, host=options.host)
|
||||
|
||||
def _do_free(self, opaque:HCQBuffer, options:BufferSpec): self.dev.iface.free(opaque)
|
||||
|
||||
@@ -565,7 +565,7 @@ class PCIIface(PCIIfaceBase):
|
||||
|
||||
# Setup classes for the GPU
|
||||
self.gpfifo_class, self.compute_class, self.dma_class = (gsp:=self.dev_impl.gsp).gpfifo_class, gsp.compute_class, gsp.dma_class
|
||||
self.viddec_class = gsp.viddec_class
|
||||
self.viddec_class = None
|
||||
|
||||
def setup_usermode(self): return 0xce000000, self.pci_dev.map_bar(bar=0, fmt='I', off=0xbb0000, size=0x10000)
|
||||
def setup_vm(self, vaspace): pass
|
||||
@@ -603,7 +603,7 @@ class NVDevice(HCQCompiled[NVSignal]):
|
||||
|
||||
vaspace_params = nv_gpu.NV_VASPACE_ALLOCATION_PARAMETERS(vaBase=0x1000, vaSize=0x1fffffb000000,
|
||||
flags=nv_gpu.NV_VASPACE_ALLOCATION_FLAGS_ENABLE_PAGE_FAULTING | nv_gpu.NV_VASPACE_ALLOCATION_FLAGS_IS_EXTERNALLY_OWNED)
|
||||
self.vaspace = vaspace = self.iface.rm_alloc(self.nvdevice, nv_gpu.FERMI_VASPACE_A, vaspace_params)
|
||||
vaspace = self.iface.rm_alloc(self.nvdevice, nv_gpu.FERMI_VASPACE_A, vaspace_params)
|
||||
|
||||
self.iface.setup_vm(vaspace)
|
||||
|
||||
@@ -643,8 +643,7 @@ class NVDevice(HCQCompiled[NVSignal]):
|
||||
notifier = self.iface.alloc(48 << 20, uncached=True)
|
||||
params = nv_gpu.NV_CHANNELGPFIFO_ALLOCATION_PARAMETERS(gpFifoOffset=gpfifo_area.va_addr+offset, gpFifoEntries=entries, hContextShare=ctxshare,
|
||||
hObjectError=notifier.meta.hMemory, hObjectBuffer=self.virtmem if video else gpfifo_area.meta.hMemory,
|
||||
hUserdMemory=(ctypes.c_uint32*8)(gpfifo_area.meta.hMemory), userdOffset=(ctypes.c_uint64*8)(entries*8+offset), engineType=19 if video else 0,
|
||||
hVASpace=self.vaspace if video and self.is_nvd() else 0) # gsp has no default vaspace, rm maps the decoder ctx into its own
|
||||
hUserdMemory=(ctypes.c_uint32*8)(gpfifo_area.meta.hMemory), userdOffset=(ctypes.c_uint64*8)(entries*8+offset), engineType=19 if video else 0)
|
||||
gpfifo = self.iface.rm_alloc(channel_group, self.iface.gpfifo_class, params)
|
||||
|
||||
if compute:
|
||||
@@ -710,22 +709,22 @@ class NVDevice(HCQCompiled[NVSignal]):
|
||||
def _ensure_has_vid_hw(self, w, h):
|
||||
if self.iface.viddec_class is None: raise RuntimeError(f"{self.device} Video decoder class not available.")
|
||||
|
||||
coloc_sz = round_up((round_up(h, 64) * round_up(h, 64)) + (round_up(w, 64) * round_up(h, 64) // 16), 2 << 20)
|
||||
coloc_size = round_up((round_up(h, 64) * round_up(h, 64)) + (round_up(w, 64) * round_up(h, 64) // 16), 2 << 20)
|
||||
self.intra_top_off = round_up(h, 64) * (608 + 4864 + 152 + 2000)
|
||||
intra_unk_size = ((2 << 20) if self.iface.viddec_class >= nv_gpu.NVCFB0_VIDEO_DECODER else 0)
|
||||
self.intra_unk_off = (round_up(self.intra_top_off, 0x10000) + (64 << 10)) if intra_unk_size > 0 else None
|
||||
filter_sz = round_up(round_up(self.intra_top_off, 0x10000) + (64 << 10) + intra_unk_size, 2 << 20)
|
||||
filter_size = round_up(round_up(self.intra_top_off, 0x10000) + (64 << 10) + intra_unk_size, 2 << 20)
|
||||
|
||||
if not hasattr(self, 'vid_gpfifo'):
|
||||
self.vid_gpfifo = self._new_gpu_fifo(self.gpfifo_area, 0, self.nvdevice, offset=0x200000, entries=2048, compute=False, video=True)
|
||||
self.vid_coloc_buf, self.vid_filter_buf = (self.allocator.alloc(sz, BufferSpec(zero=True)) for sz in [coloc_sz, filter_sz])
|
||||
self.vid_stat_buf = self.allocator.alloc(0x1000, BufferSpec(zero=True))
|
||||
self.vid_coloc_buf, self.vid_filter_buf = self.allocator.alloc(coloc_size), self.allocator.alloc(filter_size)
|
||||
self.vid_stat_buf = self.allocator.alloc(0x1000)
|
||||
NVVideoQueue().wait(self.timeline_signal, self.timeline_value - 1) \
|
||||
.setup(copy_class=self.iface.viddec_class) \
|
||||
.signal(self.timeline_signal, self.next_timeline()).submit(self)
|
||||
else:
|
||||
if coloc_sz > self.vid_coloc_buf.size: self.vid_coloc_buf,_= self._realloc(self.vid_coloc_buf, coloc_sz, BufferSpec(zero=True), force=True)
|
||||
if filter_sz > self.vid_filter_buf.size: self.vid_filter_buf,_= self._realloc(self.vid_filter_buf, filter_sz, BufferSpec(zero=True), force=True)
|
||||
if coloc_size > self.vid_coloc_buf.size: self.vid_coloc_buf, _ = self._realloc(self.vid_coloc_buf, coloc_size, force=True)
|
||||
if filter_size > self.vid_filter_buf.size: self.vid_filter_buf, _ = self._realloc(self.vid_filter_buf, filter_size, force=True)
|
||||
|
||||
def hw_copy_queues(self): return super().hw_copy_queues() + ([("NVDEC:0", NVVideoQueue)] if hasattr(self, 'vid_gpfifo') else [])
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ class PythonProgram(Program['PythonDevice']):
|
||||
if g: _store(m, o+j, v, src_dtypes[1])
|
||||
i += 1
|
||||
continue
|
||||
if u.op is Ops.AFTER or (u.op is Ops.BITCAST and u.addrspace in (AddrSpace.GLOBAL, AddrSpace.LOCAL)): values[u] = src_values[0]
|
||||
if u.op is Ops.AFTER: values[u] = src_values[0]
|
||||
elif u.op is Ops.PARAM and u.addrspace is AddrSpace.ALU: values[u] = [pvals.pop(0)] * warp_size
|
||||
elif u.op in {Ops.PARAM, Ops.BUFFER}:
|
||||
storage_fmt = storage_fmt_for_dtype(u.dtype)
|
||||
@@ -114,8 +114,7 @@ class PythonProgram(Program['PythonDevice']):
|
||||
if ox < 0 or ox >= u.src[0]._shape[1] or oy < 0 or oy >= u.src[0]._shape[0]: ret.append((m, None))
|
||||
else: ret.append((m, ox*4 + oy*u.src[0]._shape[1]*4))
|
||||
else:
|
||||
scale = u.src[0].dtype.itemsize // u.src[0].src[0].dtype.itemsize if u.src[0].op is Ops.BITCAST else 1
|
||||
for m,o in zip(src_values[0], src_values[1]): ret.append((m[0], m[1]+o*scale) if isinstance(m, tuple) else (m, o*scale))
|
||||
for m,o in zip(src_values[0], src_values[1]): ret.append((m,o))
|
||||
values[u] = ret
|
||||
elif u.op is Ops.RANGE:
|
||||
if u not in values: values[u] = [0] * warp_size
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ctypes, struct, platform, pathlib, shutil
|
||||
import ctypes, struct, platform, pathlib, shutil, tarfile, tempfile
|
||||
from tinygrad.device import Compiler
|
||||
from tinygrad.helpers import DEBUG, system, fetch
|
||||
from tinygrad.runtime.support.compiler_mesa import disas_adreno
|
||||
@@ -12,9 +12,8 @@ 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:
|
||||
# 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.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)
|
||||
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,8 +295,7 @@ 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,
|
||||
profile_key:bytes|None=None):
|
||||
def hcq_profile(dev:HCQCompiled, enabled, desc, queue_type:Callable[[], HWQueue]|None=None, queue:HWQueue|None=None, dev_suff:str|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"
|
||||
|
||||
@@ -310,8 +309,7 @@ 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,
|
||||
profile_key))
|
||||
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))
|
||||
|
||||
class HCQArgsState(Generic[ProgramType]):
|
||||
def __init__(self, buf:HCQBuffer, prg:ProgramType, bufs:tuple[HCQBuffer, ...], vals:tuple[sint|None, ...]=()):
|
||||
@@ -334,9 +332,8 @@ 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, self.profile_key)]
|
||||
if PROFILE: Compiled.profile_events += [ProfileProgramEvent(dev.device, obj.name, obj.lib, base, self.prof_prg_counter)]
|
||||
|
||||
@staticmethod
|
||||
def _fini(dev, buf, spec): dev.allocator.free(buf, buf.size, spec)
|
||||
@@ -375,7 +372,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, profile_key=self.profile_key) as (sig_st, sig_en):
|
||||
with hcq_profile(self.dev, queue=q, desc=self.name, enabled=wait or PROFILE) 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)
|
||||
@@ -404,7 +401,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, bytes|None]] = []
|
||||
self.sig_prof_records:list[tuple[HCQSignal, HCQSignal, str|TracingKey, str]] = []
|
||||
self.prof_exec_counter:int = 0
|
||||
self.prof_prg_counter = itertools.count(0)
|
||||
|
||||
@@ -440,7 +437,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, pk) for st,en,name,dev,pk in self.sig_prof_records]
|
||||
Compiled.profile_events += [ProfileRangeEvent(dev, name, st.timestamp, en.timestamp) for st,en,name,dev in self.sig_prof_records]
|
||||
self.sig_prof_records = []
|
||||
|
||||
def next_timeline(self):
|
||||
|
||||
@@ -8,12 +8,12 @@ from tinygrad.device import Device, Buffer, BufferSpec, Compiled, LRUAllocator,
|
||||
from tinygrad.device import ProfileDeviceEvent, ProfileGraphEntry, ProfileGraphEvent
|
||||
from tinygrad.uop.ops import Ops, sint, UOp, UPat, PatternMatcher, KernelInfo, graph_rewrite, rewrite_group, GroupOp
|
||||
from tinygrad.uop.symbolic import symbolic
|
||||
from tinygrad.dtype import dtypes, truncate, DType
|
||||
from tinygrad.dtype import dtypes, truncate
|
||||
from tinygrad.runtime.support.hcq import MMIOInterface, HCQBuffer
|
||||
from tinygrad.runtime.support.memory import BumpAllocator
|
||||
from tinygrad.renderer import Renderer, Estimates
|
||||
from tinygrad.engine.realize import to_program, get_call_arg_uops, get_call_name, get_call_outs_ins, estimate_uop
|
||||
from tinygrad.engine.realize import pm_flatten_linear, lower_and_compile
|
||||
from tinygrad.engine.realize import pm_flatten_linear
|
||||
|
||||
# *****************
|
||||
# 0. helpers
|
||||
@@ -40,9 +40,6 @@ def unwrap_mstack(u):
|
||||
if u.op is Ops.MSTACK: return tuple(x for s in u.src for x in unwrap_mstack(s))
|
||||
return unwrap_mstack(u.src[0]) if u.op is Ops.MSELECT else (u,)
|
||||
|
||||
def unwrap_view(v:UOp) -> tuple[UOp, int]:
|
||||
return unwrap_view(v.src[0]) if v.op is Ops.BITCAST else (v.src[0], v.src[1].val) if v.op is Ops.SHRINK else (v, 0)
|
||||
|
||||
def is_value_known_at_link(val:UOp) -> bool:
|
||||
runtime_reads = [u for u in val.toposort() if u.op in (Ops.LOAD, Ops.INDEX)]
|
||||
addressed_bufs = [b for g in val.toposort() if g.op is Ops.GETADDR for b in unwrap_mstack(g.buf_uop)]
|
||||
@@ -51,17 +48,16 @@ def is_value_known_at_link(val:UOp) -> bool:
|
||||
return not val.variables() and not runtime_reads and all(b.op is not Ops.PARAM or b.tag is not None for b in addressed_bufs)
|
||||
|
||||
def make_patches(buf:UOp, patches:Sequence[tuple[sint, UOp]]) -> tuple[UOp, ...]:
|
||||
groups:dict[tuple[str|None, DType, sint], list[tuple[sint, UOp]]] = collections.defaultdict(list)
|
||||
for off, val in patches:
|
||||
tag = "link" if is_value_known_at_link(val) else "inputs" if val.op is Ops.GETADDR else None
|
||||
groups[(tag, (v:=(val.bitcast(buf.dtype) if val.dtype.itemsize == buf.dtype.itemsize else val)).dtype, off % v.dtype.itemsize)].append((off, v))
|
||||
def _mk_store(ps:list[tuple[sint, UOp]], tag:str|None) -> UOp:
|
||||
offs = UOp(Ops.STACK, dtypes.int, tuple(UOp.const(off // buf.dtype.itemsize, dtypes.int) for off,_ in ps))
|
||||
vals = UOp(Ops.STACK, ps[0][1].dtype, tuple(val for _,val in ps))
|
||||
return buf.index(offs, dtype=vals.dtype).store(vals).rtag(tag)
|
||||
|
||||
ret, bit = [], buf.dtype.itemsize
|
||||
for (tag, dt, r), ps in groups.items():
|
||||
view = buf.shrink(((r // bit, (max(off for off,_ in ps) + dt.itemsize) // bit),)).bitcast(dt)
|
||||
offs = UOp(Ops.STACK, dtypes.int, tuple(UOp.const((off - r) // dt.itemsize, dtypes.int) for off,_ in ps))
|
||||
ret.append(view.index(offs).store(UOp(Ops.STACK, dt, tuple(val for _,val in ps))).rtag(tag))
|
||||
return tuple(ret)
|
||||
patches = [(off, val.cast(buf.dtype) if val.dtype.itemsize == buf.dtype.itemsize else val) for off, val in patches]
|
||||
link, runtime = partition(patches, lambda p: is_value_known_at_link(p[1]))
|
||||
inputs, runtime = partition(runtime, lambda p: p[1].op is Ops.GETADDR)
|
||||
return tuple(_mk_store(list(ps), tag) for cls, tag in ((link, "link"), (inputs, "inputs"), (runtime, None))
|
||||
for _, ps in itertools.groupby(sorted(cls, key=lambda p: p[1].dtype), key=lambda p: p[1].dtype))
|
||||
|
||||
def make_binary_patch(buf:UOp, blob:bytes) -> UOp:
|
||||
data = UOp(Ops.BINARY, src=(), arg=blob).bitcast(buf.dtype)
|
||||
@@ -332,16 +328,14 @@ def make_addr_table(call:UOp, gaddrs:list[UOp], name:str) -> tuple[UOp, dict[UOp
|
||||
return table, reads, fills, {g:slots[bare[g]] for g in gaddrs}
|
||||
|
||||
def make_gather_loop(patches:list[UOp], table:UOp, slots:dict[UOp, int], lt_patches:list[UOp]) -> dict[UOp, UOp]:
|
||||
(dst,), words = dedup(p.buf_uop for p in patches), [(unwrap_view(p.src[0].src[0])[1] + off.val*(val.dtype.itemsize//p.buf_uop.dtype.itemsize),
|
||||
slots[val]) for p in patches for off,val in zip(p.src[0].src[1].src, p.src[1].src)]
|
||||
(dst,), words = dedup(p.buf_uop for p in patches), [(off.val, slots[val]) for p in patches for off, val in zip(p.src[0].src[1].src, p.src[1].src)]
|
||||
|
||||
# build a runtime loop that writes every input address
|
||||
pairs = UOp.placeholder((2*len(words),), dtypes.uint32, next(UOp.unique_num), device=dst.device).rtag("systems")
|
||||
lt_patches.append(make_binary_patch(pairs, struct.pack(f'<{2*len(words)}I', *itertools.chain(*words))))
|
||||
r = UOp.range(len(words), next(UOp.unique_num), dtype=dtypes.int, src=(pairs, dst))
|
||||
off, slot = ((pairs.index(2*r+i).load() % bound).cast(dtypes.int) for i, bound in ((0, dst.max_numel()-1), (1, table.max_numel())))
|
||||
patch = dst.shrink(((off, off+table.dtype.itemsize//dst.dtype.itemsize),)).bitcast(table.dtype).index(0).store(table.index(slot).load()).end(r)
|
||||
return {p: UOp(Ops.NOOP) for p in patches} | {patches[0]: patch}
|
||||
return {p: UOp(Ops.NOOP) for p in patches} | {patches[0]: dst.index(off, dtype=table.dtype).store(table.index(slot).load()).end(r)}
|
||||
|
||||
def is_input_addr(g:UOp) -> bool: return all(x.op is Ops.PARAM and x.tag is None for x in unwrap_mstack(g.buf_uop))
|
||||
|
||||
@@ -389,22 +383,22 @@ def replace_params(call:UOp) -> UOp|None:
|
||||
sub = {(b:=u.without_after): UOp.param(i, u.dtype, shape=b.shape, device=HCQ_RUNTIME_DEV.value, volatile=b.op is Ops.PARAM and b.arg.volatile)
|
||||
for i,u in enumerate(c_args)} | {v: v.replace(arg=replace(v.arg, slot=-1)) for v in variables if v.op is Ops.PARAM} | _rank_ranges(tops)
|
||||
info = replace(call.arg.aux, inputs=next((i for i,u in enumerate(c_args + refhold) if u.without_after.tag == "inputs"), None))
|
||||
prg_sink = body.src[0].substitute(sub).replace(arg=KernelInfo("hcq_submit"), tag=1)
|
||||
return call.replace(src=(body.replace(src=(prg_sink,)), *c_args, *refhold), arg=replace(call.arg, aux=info))
|
||||
return call.replace(src=(body.substitute(sub).replace(arg="hcq_args"), *c_args, *refhold), arg=replace(call.arg, aux=info))
|
||||
pm_replace_params = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="hcq", src=(UPat(Ops.SINK),)),), name="call", allow_any_len=True), replace_params)])
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="hcq"),), name="call", allow_any_len=True), replace_params)])
|
||||
|
||||
# *****************
|
||||
|
||||
def resolve_getaddr_view(bv:UOp, g:UOp) -> UOp:
|
||||
base = bv.src[0].after(*g.src[0].src[1:] if g.src[0].op is Ops.AFTER else ())
|
||||
addr = UOp(Ops.GETADDR, src=(base,), arg=g.arg)
|
||||
return addr if bv.op is Ops.BITCAST else addr + UOp.const(bv.src[1].val * bv.dtype.itemsize, dtypes.uint64)
|
||||
if bv.op is Ops.BITCAST: return UOp(Ops.GETADDR, src=(base,), arg=g.arg)
|
||||
itemsize = bv.src[0].dtype.itemsize if bv.src[0].without_after.op in (Ops.BUFFER, Ops.MSTACK, Ops.MSELECT) else bv.dtype.itemsize
|
||||
return UOp(Ops.GETADDR, src=(base,), arg=g.arg) + UOp.const(bv.src[1].val * itemsize, dtypes.uint64)
|
||||
|
||||
pm_early_simplify = PatternMatcher([
|
||||
(UPat(Ops.GETADDR, src=(UPat((Ops.SHRINK, Ops.BITCAST), name="bv").or_after(),), name="g"), resolve_getaddr_view),
|
||||
(UPat(Ops.SHRINK, src=(UPat(Ops.SHRINK, name="bv"), UPat(), UPat()), name="x"),
|
||||
lambda bv,x: bv.src[0].shrink(((start:=bv.src[1]+x.src[1], start+x.src[2]),))),
|
||||
(UPat(Ops.INDEX, src=(UPat(Ops.SHRINK, name="bv"),), allow_any_len=True, name="x"),
|
||||
lambda bv,x: x.replace(src=(bv.src[0], x.src[1] + bv.src[1].cast(x.src[1].dtype), *x.src[2:]))),
|
||||
])
|
||||
|
||||
# *****************
|
||||
@@ -426,6 +420,15 @@ def pack_hcq_placeholders(call:UOp) -> UOp|None:
|
||||
pm_pack_placeholders = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="hcq"),), name="call", allow_any_len=True), pack_hcq_placeholders)])
|
||||
|
||||
# *****************
|
||||
# 8. callify hcq programs
|
||||
|
||||
def callify_hcq(call:UOp, cf:UOp) -> UOp:
|
||||
prg = to_program(cf.src[0].replace(arg=KernelInfo("hcq_submit"), tag=1), Device[HCQ_RUNTIME_DEV.value].renderer)
|
||||
return call.replace(src=(cf.replace(src=(prg,), arg="hcq"), *call.src[1:]))
|
||||
pm_callify_hcq = PatternMatcher([(UPat(Ops.CALL, src=(
|
||||
UPat(Ops.CUSTOM_FUNCTION, arg="hcq_args", src=(UPat(Ops.SINK),), name="cf"),), name="call", allow_any_len=True), callify_hcq)])
|
||||
|
||||
# *****************
|
||||
# 9. merge submitters
|
||||
|
||||
@@ -461,7 +464,8 @@ def hcq_lower(linear:UOp, pm_encode:PatternMatcher) -> UOp:
|
||||
linear = graph_rewrite(linear, pm_split_patches, walk=True, name="split patches")
|
||||
|
||||
# and compile it
|
||||
return lower_and_compile(graph_rewrite(linear, pm_replace_params, walk=True, name="replace params"))
|
||||
linear = graph_rewrite(linear, pm_replace_params, name="replace params")
|
||||
return graph_rewrite(linear, pm_callify_hcq, name="callify hcq", enter_calls=True)
|
||||
|
||||
@rewrite_group(lambda linear,input_uops,profile,ret: f"HCQ Compile {pluralize('Kernel', len(ret.src))}")
|
||||
def hcq_compile(linear:UOp, input_uops:list[UOp]|None, profile:bool) -> UOp:
|
||||
@@ -503,13 +507,11 @@ def fold_binary(buf:UOp, blob:UOp) -> UOp:
|
||||
b.ensure_allocated().as_memoryview(force_zero_copy=True, no_sync=True).cast('B')[:len(blob.arg)] = blob.arg
|
||||
return UOp(Ops.NOOP)
|
||||
|
||||
def fold_const_store(view:UOp, off:UOp, val:UOp) -> UOp:
|
||||
buf, start = unwrap_view(view)
|
||||
def fold_const_store(buf:UOp, off:UOp, val:UOp) -> UOp:
|
||||
for off,val in zip(off.src, val.src):
|
||||
for b,v in zip((bs:=mb.bufs if isinstance((mb:=buf.buffer), MultiBuffer) else (mb,)), val.src if val.op is Ops.STACK else (val,)*len(bs)):
|
||||
data = struct.pack(f'<{v.dtype.fmt}', truncate[v.dtype]((v.src[0] if v.op is Ops.CAST else v).val))
|
||||
bo = start*buf.dtype.itemsize + off.val*val.dtype.itemsize
|
||||
b.ensure_allocated().as_memoryview(force_zero_copy=True, no_sync=True).cast('B')[bo:bo+len(data)] = data
|
||||
b.ensure_allocated().as_memoryview(force_zero_copy=True, no_sync=True).cast('B')[(bo:=off.val*buf.dtype.itemsize):bo+len(data)] = data
|
||||
return UOp(Ops.NOOP)
|
||||
|
||||
def resolve_getaddr(buf:UOp, g:UOp) -> UOp:
|
||||
@@ -530,10 +532,10 @@ pm_resolve_patches = PatternMatcher([
|
||||
(UPat(Ops.GETADDR, src=(UPat(name="buf"),), name="g"), resolve_getaddr),
|
||||
|
||||
# folders
|
||||
(UPat(name="buf").index(UPat(Ops.RANGE), allow_any_len=True).store(UPat.any(UPat(Ops.BINARY, name="blob"), UPat(Ops.BINARY, name="blob").bitcast())
|
||||
.index(UPat(Ops.RANGE), allow_any_len=True).load()).end(UPat(Ops.RANGE)), fold_binary),
|
||||
(UPat((Ops.BITCAST, Ops.SHRINK, Ops.BUFFER, Ops.MSTACK), name="view")
|
||||
.index(UPat(Ops.STACK, name="off")).store(UPat(Ops.STACK, name="val")), fold_const_store),
|
||||
(UPat(name="buf").index(UPat(Ops.RANGE), allow_any_len=True)
|
||||
.store(UPat.any(UPat(Ops.BINARY, name="blob"), UPat(Ops.BINARY, name="blob").bitcast()).index(UPat(Ops.RANGE), allow_any_len=True).load())
|
||||
.end(UPat(Ops.RANGE)), fold_binary),
|
||||
(UPat({Ops.BUFFER, Ops.MSTACK}, name="buf").index(UPat(Ops.STACK, name="off")).store(UPat(Ops.STACK, name="val")), fold_const_store),
|
||||
])
|
||||
|
||||
pm_assert_no_afters = PatternMatcher([(UPat(Ops.AFTER, name="a"), lambda a: panic(RuntimeError, f"AFTER left at hcq_link: {a.src[0].op}"))])
|
||||
|
||||
@@ -236,7 +236,7 @@ class MemoryManager:
|
||||
self.map_range(va:=self.alloc_vaddr(self.vram_size, self.vram_size), self.vram_size, [(0, self.vram_size)], AddrSpace.PHYS, uncached=uncached)
|
||||
return va
|
||||
|
||||
def valloc(self, size:int, align=0x1000, uncached=False, contiguous=False, zero=False) -> VirtMapping:
|
||||
def valloc(self, size:int, align=0x1000, uncached=False, contiguous=False) -> VirtMapping:
|
||||
if not getenv("GMMU", 1):
|
||||
paddr = self.palloc(size:=round_up(size, 0x1000), align, zero=False)
|
||||
return VirtMapping(self.identity_va(uncached) + paddr, size, [(paddr, size)], aspace=AddrSpace.PHYS, uncached=uncached)
|
||||
@@ -251,7 +251,7 @@ class MemoryManager:
|
||||
while rem_size > 0:
|
||||
while self.palloc_ranges[nxt_range][0] > rem_size: nxt_range += 1
|
||||
|
||||
try: paddrs += [(self.palloc(try_sz:=self.palloc_ranges[nxt_range][0], self.palloc_ranges[nxt_range][1], zero=zero), try_sz)]
|
||||
try: paddrs += [(self.palloc(try_sz:=self.palloc_ranges[nxt_range][0], self.palloc_ranges[nxt_range][1], zero=False), try_sz)]
|
||||
except MemoryError:
|
||||
# Move to a smaller size and try again.
|
||||
nxt_range += 1
|
||||
|
||||
@@ -345,7 +345,7 @@ class NV_FLCN_COT(NV_IP):
|
||||
|
||||
class NV_GSP(NV_IP):
|
||||
def init_sw(self):
|
||||
self.handle_gen, self.chan_runlists = itertools.count(0xcf000000), {}
|
||||
self.handle_gen = itertools.count(0xcf000000)
|
||||
self.init_rm_args()
|
||||
self.init_libos_args()
|
||||
self.init_wpr_meta()
|
||||
@@ -355,7 +355,6 @@ class NV_GSP(NV_IP):
|
||||
self.rpc_set_registry_table()
|
||||
|
||||
self.gpfifo_class, self.compute_class, self.dma_class = nv_gpu.AMPERE_CHANNEL_GPFIFO_A, nv_gpu.AMPERE_COMPUTE_B, nv_gpu.AMPERE_DMA_COPY_B
|
||||
self.viddec_class = {"AD":nv_gpu.NVC9B0_VIDEO_DECODER, "GB":nv_gpu.NVCFB0_VIDEO_DECODER}.get(self.nvdev.chip_name[:2]) # nvdec: ada and blackwell
|
||||
match self.nvdev.chip_name[:2]:
|
||||
case "AD": self.compute_class = nv_gpu.ADA_COMPUTE_A
|
||||
case "GB":
|
||||
@@ -454,8 +453,8 @@ class NV_GSP(NV_IP):
|
||||
self.wpr_meta, _, wpr_meta_addrs = self.nvdev._alloc_boot_mem(ctypes.sizeof(type(m)), data=bytes(m))
|
||||
self.wpr_meta_sysmem = wpr_meta_addrs[0]
|
||||
|
||||
def promote_ctx(self, client:int, subdevice:int, obj:int, ctxbufs:dict[int, GRBufDesc], bufs=None, virt=None, phys=None, engine=0x1):
|
||||
res, prom = {}, nv_gpu.NV2080_CTRL_GPU_PROMOTE_CTX_PARAMS(entryCount=len(ctxbufs), engineType=engine, hChanClient=client, hObject=obj)
|
||||
def promote_ctx(self, client:int, subdevice:int, obj:int, ctxbufs:dict[int, GRBufDesc], bufs=None, virt=None, phys=None):
|
||||
res, prom = {}, nv_gpu.NV2080_CTRL_GPU_PROMOTE_CTX_PARAMS(entryCount=len(ctxbufs), engineType=0x1, hChanClient=client, hObject=obj)
|
||||
for i,(buf,desc) in enumerate(ctxbufs.items()):
|
||||
use_v, use_p = (desc.virt if virt is None else virt), (desc.phys if phys is None else phys)
|
||||
x = (bufs or {}).get(buf, self.nvdev.mm.valloc(desc.size, contiguous=True)) # allocate buffers
|
||||
@@ -471,9 +470,6 @@ class NV_GSP(NV_IP):
|
||||
subdev = self.rpc_rm_alloc(hParent=dev, hClass=nv_gpu.NV20_SUBDEVICE_0, params=nv_gpu.NV2080_ALLOC_PARAMETERS())
|
||||
vaspace = self.rpc_rm_alloc(hParent=dev, hClass=nv_gpu.FERMI_VASPACE_A, params=nv_gpu.NV_VASPACE_ALLOCATION_PARAMETERS())
|
||||
|
||||
di = self.rpc_rm_control(subdev, nv_gpu.NV2080_CTRL_CMD_FIFO_GET_DEVICE_INFO_TABLE, nv_gpu.NV2080_CTRL_FIFO_GET_DEVICE_INFO_TABLE_PARAMS())
|
||||
self.runlists = {di.entries[i].engineData[2]: di.entries[i].engineData[3] for i in range(di.numEntries)}
|
||||
|
||||
# reserve 512MB for the reserved PDES
|
||||
res_va = self.nvdev.mm.alloc_vaddr(res_sz:=(512 << 20))
|
||||
|
||||
@@ -553,16 +549,10 @@ class NV_GSP(NV_IP):
|
||||
self.cmd_q.send_rpc(nv.NV_VGPU_MSG_FUNCTION_GSP_RM_ALLOC, bytes(alloc_args) + (bytes(params) if params is not None else b''))
|
||||
self.stat_q.wait_resp(nv.NV_VGPU_MSG_FUNCTION_GSP_RM_ALLOC)
|
||||
|
||||
if hClass == self.gpfifo_class:
|
||||
self.chan_runlists[obj] = self.runlists.get((e:=params.engineType) + 10*(e >= nv_gpu.NV2080_ENGINE_TYPE_NVDEC0), 0)
|
||||
if hClass == nv_gpu.FERMI_VASPACE_A and client != self.priv_root:
|
||||
self.rpc_set_page_directory(device=hParent, hVASpace=obj, pdir_paddr=self.nvdev.mm.root_page_table.paddr, client=client)
|
||||
if hClass == nv_gpu.NV01_DEVICE_0 and client != self.priv_root: self.device = obj # save user device handle
|
||||
if hClass == nv_gpu.NV20_SUBDEVICE_0: self.subdevice = obj # save subdevice handle
|
||||
if hClass == self.viddec_class and client != self.priv_root:
|
||||
ctx, eng = {0: GRBufDesc(0x1000, phys=True, virt=True)}, nv_gpu.NV2080_ENGINE_TYPE_NVDEC0
|
||||
bufs = self.promote_ctx(client, self.subdevice, hParent, ctx, virt=False, engine=eng)
|
||||
self.promote_ctx(client, self.subdevice, hParent, ctx, bufs, phys=False, engine=eng)
|
||||
if hClass == self.compute_class and client != self.priv_root:
|
||||
phys_gr_ctx = self.promote_ctx(client, self.subdevice, hParent, {k:v for k,v in self.grctx_bufs.items() if k in [0, 1, 2]}, virt=False)
|
||||
self.promote_ctx(client, self.subdevice, hParent, {k:v for k,v in self.grctx_bufs.items() if k in [0, 1, 2]}, phys_gr_ctx, phys=False)
|
||||
@@ -585,10 +575,9 @@ class NV_GSP(NV_IP):
|
||||
res = self.stat_q.wait_resp(nv.NV_VGPU_MSG_FUNCTION_GSP_RM_CONTROL)
|
||||
st = type(params).from_buffer_copy(res[len(bytes(control_args)):]) if params is not None else None
|
||||
|
||||
# NOTE: gsp only fills in the channel id, the runlist id (and, on gb20x, the doorbell enable bit) are added by the driver.
|
||||
if cmd == nv_gpu.NVC36F_CTRL_CMD_GPFIFO_GET_WORK_SUBMIT_TOKEN:
|
||||
cast(nv_gpu.NVC36F_CTRL_CMD_GPFIFO_GET_WORK_SUBMIT_TOKEN_PARAMS, st).workSubmitToken |= (self.chan_runlists[hObject] << 16) | \
|
||||
((1 << 30) if self.nvdev.chip_name.startswith("GB2") else 0)
|
||||
# NOTE: gb20x requires the enable bit for token submission. Patch workSubmitToken here to maintain userspace compatibility.
|
||||
if self.nvdev.chip_name.startswith("GB2") and cmd == nv_gpu.NVC36F_CTRL_CMD_GPFIFO_GET_WORK_SUBMIT_TOKEN:
|
||||
cast(nv_gpu.NVC36F_CTRL_CMD_GPFIFO_GET_WORK_SUBMIT_TOKEN_PARAMS, st).workSubmitToken |= (1 << 30)
|
||||
return st
|
||||
|
||||
def rpc_set_page_directory(self, device:int, hVASpace:int, pdir_paddr:int, client=None, pasid=0xffffffff):
|
||||
|
||||
@@ -262,7 +262,7 @@ class PCIIfaceBase:
|
||||
self.dev_impl = dev_impl_t(self.pci_dev)
|
||||
self.dev, self.vram_bar, self.count = dev, vram_bar, len(hcq_filter_visible_devices(System.list_devices(vendor, devices, base_class), dn))
|
||||
|
||||
def alloc(self, size:int, host=False, uncached=False, cpu_access=False, contiguous=False, force_devmem=False, zero=False, **kwargs) -> HCQBuffer:
|
||||
def alloc(self, size:int, host=False, uncached=False, cpu_access=False, contiguous=False, force_devmem=False, **kwargs) -> HCQBuffer:
|
||||
should_use_sysmem = host or ((cpu_access if self.is_bar_small() else (uncached and cpu_access)) and not force_devmem)
|
||||
|
||||
# Align size to huge pages for large allocations, otherwise the unaligned tail falls back to 4KB pages, increasing TLB pressure.
|
||||
@@ -274,7 +274,7 @@ class PCIIfaceBase:
|
||||
mapping = self.dev_impl.mm.map_range(vaddr, size, [(paddr, 0x1000) for paddr in paddrs], aspace=AddrSpace.SYS, snooped=True, uncached=True)
|
||||
return HCQBuffer(vaddr, size, meta=PCIAllocationMeta(mapping, has_cpu_mapping=True, hMemory=paddrs[0]), view=memview, owner=self.dev)
|
||||
|
||||
mapping = self.dev_impl.mm.valloc(size:=round_up(size, 0x1000), uncached=uncached, contiguous=cpu_access, zero=zero)
|
||||
mapping = self.dev_impl.mm.valloc(size:=round_up(size, 0x1000), uncached=uncached, contiguous=cpu_access)
|
||||
barview = self.pci_dev.map_bar(bar=self.vram_bar, off=mapping.paddrs[0][0], size=mapping.size) if cpu_access else None
|
||||
return HCQBuffer(mapping.va_addr, size, view=barview, meta=PCIAllocationMeta(mapping, cpu_access, hMemory=mapping.paddrs[0][0]), owner=self.dev)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import ctypes, struct, time, functools, itertools
|
||||
from tinygrad.runtime.autogen import libusb
|
||||
from tinygrad.helpers import DEBUG, DEV, to_mv, from_mv, round_up, ceildiv
|
||||
from tinygrad.helpers import DEBUG, DEV, to_mv, round_up, ceildiv
|
||||
from tinygrad.runtime.support.hcq import MMIOInterface
|
||||
from tinygrad.runtime.support import c
|
||||
|
||||
@@ -35,11 +35,6 @@ 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))
|
||||
|
||||
@@ -78,40 +73,6 @@ 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)
|
||||
exp = xfer.contents.length - 8 if xfer.contents.type == libusb.LIBUSB_TRANSFER_TYPE_CONTROL else xfer.contents.length
|
||||
if xfer.contents.status != 0 or xfer.contents.actual_length != exp: 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 _submit_async(self, endpoint:int, xtype:int, payload:bytes|bytearray|memoryview, timeout:int) -> int: # payload kept alive till bulk_wait
|
||||
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, endpoint, xtype
|
||||
tr.contents.timeout, tr.contents.length = timeout, len(payload)
|
||||
tr.contents.buffer = ctypes.cast(from_mv(memoryview(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 submit failed")(tr)
|
||||
return tag
|
||||
|
||||
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)."""
|
||||
return self._submit_async(0x02, libusb.LIBUSB_TRANSFER_TYPE_BULK, payload, timeout)
|
||||
|
||||
def control_write_async(self, request:int, value:int=0, index:int=0, data:bytes=b"", timeout:int=1000) -> int:
|
||||
"""Queue a vendor control OUT without blocking; completes via bulk_wait(tag) like bulk_write_async."""
|
||||
setup = bytearray(struct.pack('<BBHHH', 0x40, request, value, index, len(data)) + data)
|
||||
return self._submit_async(0, libusb.LIBUSB_TRANSFER_TYPE_CONTROL, setup, timeout)
|
||||
|
||||
def control_read_async(self, request:int, length:int, value:int=0, index:int=0, timeout:int=1000) -> tuple[int, memoryview]:
|
||||
"""Queue a vendor control IN without blocking; the data lands in the returned buffer by bulk_wait(tag)."""
|
||||
buf = bytearray(struct.pack('<BBHHH', 0xC0, request, value, index, length)) + bytearray(length)
|
||||
return self._submit_async(0, libusb.LIBUSB_TRANSFER_TYPE_CONTROL, buf, timeout), memoryview(buf)[8:]
|
||||
|
||||
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)
|
||||
@@ -199,10 +160,13 @@ 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, slot_start:int=0):
|
||||
def scsi_write(self, buf:bytes):
|
||||
"""Write to SRAM via 0xF2 vendor command + bulk OUT."""
|
||||
buf_padded = buf + b'\x00' * (round_up(len(buf), 512) - len(buf))
|
||||
self.usb.control_write(0xF2, value=len(buf_padded) // 512, index=(slot_start & 0xFF) | (ceildiv(len(buf_padded), 0x4000) << 8))
|
||||
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.bulk_write(buf_padded)
|
||||
|
||||
def scsi_read_arm(self, size:int):
|
||||
@@ -225,7 +189,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 data if isinstance(index, slice) else int.from_bytes(data, "little")
|
||||
return int.from_bytes(data, "little") if sz == self.el_sz else data
|
||||
|
||||
def __setitem__(self, index, data):
|
||||
off, _ = self._off_from_index(index)
|
||||
|
||||
@@ -97,17 +97,11 @@ 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, outer_binds:dict[str, UOp]|None=None):
|
||||
def resolve_linear_call(linear_call:UOp):
|
||||
linear = graph_rewrite(linear_call.src[0], pm_post_sched_cache, ctx=({}, linear_call.src[1:]), walk=True, name="params to buffers")
|
||||
# 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))
|
||||
# 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")
|
||||
|
||||
pm_resolve_linear_call = PatternMatcher([
|
||||
# call LINEAR is resolved here
|
||||
|
||||
@@ -12,6 +12,8 @@ class IndexingContext:
|
||||
realize_map: dict[UOp, None|list[int]] = field(default_factory=dict)
|
||||
non_removable: dict[UOp, None] = field(default_factory=dict)
|
||||
range_map: dict[UOp, tuple[tuple[UOp, ...], tuple[UOp, ...]]] = field(default_factory=dict)
|
||||
# loads reachable from each UOp memoized across matches
|
||||
buf_cache: dict[UOp, frozenset[UOp]] = field(default_factory=dict)
|
||||
|
||||
# create ranges
|
||||
range_idx: Iterator[int] = field(default_factory=itertools.count)
|
||||
@@ -185,7 +187,7 @@ def apply_movement_op(op:Ops, in_shape:tuple[sint,...], arg:tuple, rngs:tuple[UO
|
||||
return rngs
|
||||
|
||||
@rewrite_group(new_ctx=False)
|
||||
def run_rangeify(tsink:UOp, debug:bool=False) -> UOp:
|
||||
def run_rangeify(tsink:UOp, debug:bool=False) -> tuple[UOp, IndexingContext]:
|
||||
if debug: print("**************************")
|
||||
rctx = IndexingContext()
|
||||
|
||||
@@ -320,7 +322,7 @@ def run_rangeify(tsink:UOp, debug:bool=False) -> UOp:
|
||||
tsink = graph_rewrite(tsink, pm_apply_rangeify, ctx=rctx, bottom_up=True, name="apply rangeify")
|
||||
# if a deviceless value must materialize, place it on the sink device
|
||||
tsink = graph_rewrite(tsink, pm_fix_deviceless, ctx=tsink.device, name="add device to deviceless")
|
||||
return tsink
|
||||
return tsink, rctx
|
||||
|
||||
def render_ranges(*rngs_list, realized) -> str:
|
||||
disp = []
|
||||
|
||||
@@ -10,7 +10,7 @@ from tinygrad.helpers import prod, getenv, dedup, all_int, DEBUG, SPLIT_REDUCEOP
|
||||
from tinygrad.helpers import PCONTIG, FLOAT16, OPENPILOT_HACKS, argsort, partition, get_single_element
|
||||
from tinygrad.codegen.simplify import pm_flatten_range, pm_reduce_simplify
|
||||
from tinygrad.codegen.opt import Opt
|
||||
from tinygrad.schedule.indexing import run_rangeify, BufferizeOpts, apply_movement_op
|
||||
from tinygrad.schedule.indexing import run_rangeify, BufferizeOpts, IndexingContext, apply_movement_op
|
||||
from tinygrad.schedule.multi import multi_pm
|
||||
from tinygrad.schedule.allreduce import create_allreduce_function
|
||||
|
||||
@@ -352,12 +352,7 @@ pm_no_indexing_calls = PatternMatcher([
|
||||
])
|
||||
|
||||
DEVICE_MAX_BUFS = {"METAL": 31, "WEBGPU": 8, "CPU": 31} # TODO: get from device?
|
||||
@dataclass
|
||||
class LimitBufsContext:
|
||||
buf_cache: dict[UOp, frozenset[UOp]] = field(default_factory=dict)
|
||||
range_idx: itertools.count = field(default_factory=itertools.count)
|
||||
|
||||
def _limit_bufs(ctx:LimitBufsContext, root:UOp):
|
||||
def limit_bufs(ctx:IndexingContext, root:UOp):
|
||||
if (device:=root.device) is None: return None # no device, index related calculations
|
||||
device = device if isinstance(device, str) else device[0].split(":")[0]
|
||||
if not (MAX_BUFS:=MAX_KERNEL_BUFFERS.value or DEVICE_MAX_BUFS.get(device, 0)): return None
|
||||
@@ -379,7 +374,7 @@ def _limit_bufs(ctx:LimitBufsContext, root:UOp):
|
||||
s = s.substitute(dict(zip(orig_ranges, end_ranges))).bufferize(*end_ranges, arg=BufferizeOpts(device=s.device)).index(*orig_ranges)
|
||||
srcs.append(s)
|
||||
return root.replace(src=tuple(srcs))
|
||||
pm_limit_bufs = PatternMatcher([(UPat(set.union(GroupOp.Binary, GroupOp.Ternary), name="root"), _limit_bufs)])
|
||||
pm_limit_bufs = PatternMatcher([(UPat(set.union(GroupOp.Binary, GroupOp.Ternary), name="root"), limit_bufs)])
|
||||
|
||||
# *****************
|
||||
# 4. put in buffers for bufferize
|
||||
@@ -583,21 +578,20 @@ pm_copy_to_store = PatternMatcher([
|
||||
|
||||
@rewrite_group(new_ctx=False)
|
||||
def get_kernel_graph(sink:UOp) -> UOp:
|
||||
# prepare for rangeify
|
||||
tsink = graph_rewrite(sink, multi_pm, name="multi_pm")
|
||||
if OPENPILOT_HACKS: tsink = graph_rewrite(tsink, pm_fold_moved_after, ctx={}, name="fold moved afters")
|
||||
tsink = graph_rewrite(tsink, pm_mops+earliest_rewrites, bottom_up=True, name="earliest rewrites")
|
||||
|
||||
tsink = graph_rewrite(tsink, pm_copy_to_store, ctx=itertools.count(0), bottom_up=True, name="convert copy to store")
|
||||
|
||||
# convert movement ops to ranges
|
||||
tsink = run_rangeify(tsink, bool(DEBUG_RANGEIFY))
|
||||
tsink, rctx = run_rangeify(tsink, bool(DEBUG_RANGEIFY))
|
||||
|
||||
# cleanups for speed and runability
|
||||
tsink = graph_rewrite(tsink,
|
||||
symbolic+pm_reduce_simplify+pm_const_buffer_folding+pm_remove_bufferize,
|
||||
name="symbolic+reduce_collapse+debuf")
|
||||
next_range = max((x.arg[0] for x in tsink.toposort() if x.op is Ops.RANGE), default=-1) + 1
|
||||
tsink = graph_rewrite(tsink, pm_limit_bufs, ctx=LimitBufsContext(range_idx=itertools.count(next_range)), name="limit buffers")
|
||||
tsink = graph_rewrite(tsink, pm_limit_bufs, ctx=rctx, name="limit buffers")
|
||||
|
||||
if VIZ: graph_rewrite(tsink, PatternMatcher([]), name="View Rangeify")
|
||||
|
||||
# bufferize -> store
|
||||
|
||||
+1
-1
@@ -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, self.key)
|
||||
return TinyELF(self.src[3].arg, self.arg.function_name, self.arg.target, sig)
|
||||
|
||||
@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, sint
|
||||
from tinygrad.uop.ops import ParamArg, UOp, PatternMatcher, UPat, multirange_str, range_str, consumer_map_from_toposort
|
||||
from tinygrad.helpers import strip_parens
|
||||
|
||||
def pretty_print(x:UOp, cache=None, d=0)->str:
|
||||
@@ -69,15 +69,14 @@ 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 = [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]
|
||||
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]
|
||||
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 and STACK
|
||||
const_folding_pat = UPat((Ops.CONST, Ops.STACK))
|
||||
# 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),)))
|
||||
|
||||
def const_arg(u:UOp) -> ConstType|tuple[ConstType, ...]|None:
|
||||
if u.op is Ops.CONST: return u.val
|
||||
@@ -108,9 +108,9 @@ def fold_const_where(gate:UOp, c0:UOp, c1:UOp, w:UOp) -> UOp:
|
||||
|
||||
symbolic_simple = pm_data_invalid + PatternMatcher([
|
||||
# ** self folding **
|
||||
(UPat({Ops.ADD, Ops.XOR, Ops.OR}, src=[UPat.var("x"), UPat.const(0)]), lambda x: x), # x+0 / x^0 / x|0 -> x
|
||||
(UPat({Ops.SHL, Ops.SHR}, src=(UPat.var("x"), UPat.const(0))), lambda x: x), # x<<0 / x>>0 -> x
|
||||
(UPat.var("x") + 0, lambda x: x), # x+0 -> x
|
||||
(UPat.var("x") * 1, lambda x: x), # x*1 -> x
|
||||
(UPat.var("x", dtype=dtypes.ints+(dtypes.bool, dtypes.weakint)) ^ 0, lambda x: x), # x^0 -> x
|
||||
(UPat.var("x") // UPat.var("x"), lambda x: x.const_like(1)), # x//x -> 1
|
||||
(UPat.var("x") // 1, lambda x: x), # x//1 -> x
|
||||
(UPat.var("x") // -1, lambda x: -x), # x//-1 -> -x
|
||||
@@ -142,9 +142,6 @@ 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,24 +11,12 @@ 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_srcs_at(u:UOp, dt:DType) -> UOp:
|
||||
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
|
||||
# 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),
|
||||
@@ -37,13 +25,20 @@ 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 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}
|
||||
# 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)),
|
||||
])
|
||||
|
||||
def lower_weak_node(u:UOp) -> UOp|None:
|
||||
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
|
||||
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)
|
||||
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)
|
||||
@@ -54,9 +49,11 @@ 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:
|
||||
@@ -72,6 +69,9 @@ 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)
|
||||
|
||||
+9
-10
@@ -231,11 +231,10 @@ 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
|
||||
# only visualize range events with an end timestamp
|
||||
if dur == 0 or isinstance(e, ProfilePointEvent): continue
|
||||
if dur == 0: continue
|
||||
name, key = e.name, None
|
||||
fmt:dict = {}
|
||||
if (ref:=data.ref_map.get(e.profile_key)) is not None and ref < len(data.ctxs):
|
||||
if (ref:=data.ref_map.get(name)) 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)):
|
||||
@@ -334,14 +333,14 @@ def unpack_pmc(e) -> dict:
|
||||
|
||||
def load_amd_counters(data:VizData, profile:list) -> None:
|
||||
counter_events:dict[tuple[int, int], dict] = {}
|
||||
durations:dict[bytes|str, list[float]] = {}
|
||||
durations:dict[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 and e.profile_key is not None:
|
||||
durations.setdefault(e.profile_key, []).append(float(e.en-e.st))
|
||||
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, 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
|
||||
@@ -349,12 +348,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(unwrap(prg_events[k].profile_key))) is not None else prg_events[k].name
|
||||
name = data.ctxs[r]["ki"].name if (r:=data.ref_map.get(pname:=prg_events[k].name)) is not None else pname
|
||||
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], unwrap(prg_events[k].profile_key))] = pmc[0]
|
||||
all_counters[(name, run_number[k], pname)] = 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:
|
||||
@@ -497,10 +496,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[bytes, int] = {}
|
||||
run_number:dict[str, int] = {}
|
||||
for e in profile:
|
||||
if type(e).__name__ == "ProfilePMAEvent":
|
||||
run_number[profile_key] = run_num = run_number.get(profile_key:=unwrap(e.profile_key), 0)+1
|
||||
run_number[e.kern] = run_num = run_number.get(e.kern, 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