Compare commits

...
5 Commits
16 changed files with 62 additions and 51 deletions
+14 -6
View File
@@ -42,7 +42,11 @@ inputs:
required: false
default: 'false'
qemu:
description: "Install qemu"
description: "Install qemu?"
required: false
default: 'false'
ninja:
description: "Install ninja?"
required: false
default: 'false'
runs:
@@ -130,7 +134,7 @@ runs:
# ******************* apt *******************
- name: Setup apt
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true')
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true' || inputs.ninja == 'true')
shell: bash
run: |
sudo mkdir -p /var/cache/apt/archives
@@ -158,7 +162,7 @@ runs:
echo "deb http://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-20 main" | sudo tee /etc/apt/sources.list.d/llvm.list
- name: Compute Package List + Hash
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true')
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true' || inputs.ninja == 'true')
id: apt-pkgs
shell: bash
run: |
@@ -183,25 +187,29 @@ runs:
if [[ "${{ inputs.qemu }}" == "true" ]]; then
pkgs+=" qemu-user-static"
fi
# **** ninja ****
if [[ "${{ inputs.ninja }}" == "true" ]]; then
pkgs+=" ninja-build"
fi
echo "pkgs=$pkgs" >> "$GITHUB_OUTPUT"
echo "hash=$(echo -n "$pkgs" | sha256sum | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
- name: Cache apt (PR)
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true') && github.event_name == 'pull_request'
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true' || inputs.ninja == 'true') && github.event_name == 'pull_request'
uses: actions/cache/restore@v5
with:
path: /var/cache/apt/archives/
key: ${{ runner.os }}-${{ runner.arch }}-apt-${{ steps.apt-pkgs.outputs.hash }}-${{ env.CACHE_VERSION }}
- name: Cache apt
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true') && github.event_name != 'pull_request'
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true' || inputs.ninja == 'true') && github.event_name != 'pull_request'
uses: actions/cache@v5
with:
path: /var/cache/apt/archives/
key: ${{ runner.os }}-${{ runner.arch }}-apt-${{ steps.apt-pkgs.outputs.hash }}-${{ env.CACHE_VERSION }}
- name: Run apt Update + Install
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true')
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true' || inputs.ninja == 'true')
shell: bash
run: |
sudo apt -qq update || true
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
key: 'autogen'
amd: 'true'
llvm: 'true'
pydeps: 'pyyaml mako'
deps: 'autogen'
- name: Install autogen support packages
run: sudo apt-get install -y --no-install-recommends libclang-20-dev llvm-20-dev hip-dev libusb-1.0-0-dev libdrm-dev liburing-dev
- name: Regenerate autogen files
+1 -1
View File
@@ -8,7 +8,7 @@ permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Configure Git Credentials
+1 -1
View File
@@ -10,7 +10,7 @@ on:
jobs:
deploy:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Set up Python
+3 -3
View File
@@ -10,7 +10,7 @@ concurrency:
jobs:
checkbranch:
name: Check PR Branch status
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
outputs:
branchstat: ${{ steps.brstat.outputs.stat}}
steps:
@@ -44,7 +44,7 @@ jobs:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
needs: checkbranch
if: needs.checkbranch.outputs.branchstat == 'false'
steps:
@@ -87,7 +87,7 @@ jobs:
name: Core Library Line Difference
permissions:
pull-requests: write
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
needs: checkbranch
if: needs.checkbranch.outputs.branchstat == 'true'
steps:
+3 -11
View File
@@ -31,8 +31,7 @@ jobs:
- name: Setup Environment
uses: ./.github/actions/setup-tinygrad
with:
deps: docs
pydeps: "capstone torch"
deps: "docs testing_minimal"
- name: Build wheel and show size
run: |
uv build --wheel
@@ -73,10 +72,7 @@ jobs:
deps: testing_unit
pydeps: "pillow torchvision expecttest"
llvm: 'true'
- name: Install ninja
run: |
sudo apt update || true
sudo apt install -y --no-install-recommends ninja-build
ninja: 'true'
- name: Test ResNet-18
run: DEBUG=2 python3 extra/torch_backend/example.py
- name: Test one op in torch tests
@@ -98,12 +94,8 @@ jobs:
with:
key: torch-backend-pillow-torchvision-et-pt
deps: testing_unit
pydeps: "pillow torchvision expecttest"
llvm: 'true'
- name: Install ninja
run: |
sudo apt update || true
sudo apt install -y --no-install-recommends ninja-build
ninja: 'true'
- name: Test beautiful_mnist in torch with TINY_BACKEND
run: STEPS=20 DEV=CPU TARGET_EVAL_ACC_PCT=90.0 MAX_BUFFER_SIZE=0 TINY_BACKEND=1 python3 examples/other_mnist/beautiful_mnist_torch.py
+4
View File
@@ -111,6 +111,10 @@ docs = [
"numpy",
]
mesa = ["tinymesa==25.2.7.2"]
autogen = [
"pyyaml",
"mako",
]
[tool.mutmut]
+3 -3
View File
@@ -4,7 +4,7 @@ import numpy as np
from tinygrad.dtype import AddrSpace, dtypes, Invalid
from tinygrad.uop.ops import KernelInfo, AxisType, Ops
from tinygrad.renderer.ptx import PTXRenderer
from test.helpers import assert_kernel_count
from test.helpers import assert_kernel_count, KernelCountException
# **** kernels ****
@@ -474,7 +474,7 @@ class TestCustomKernelInput(unittest.TestCase):
y.realize()
kernel_count = GlobalCounters.kernel_count
self.assertEqual(y.tolist(), x.add(1).tolist())
self.assertLessEqual(kernel_count, max_kernels)
if kernel_count > max_kernels: raise KernelCountException(max_kernels, kernel_count)
# same test with @function, input is PARAM
from tinygrad import function
x0 = Tensor.arange(32).clone("CPU").realize()
@@ -487,7 +487,7 @@ class TestCustomKernelInput(unittest.TestCase):
y = run(x0).realize()
kernel_count = GlobalCounters.kernel_count
self.assertEqual(y.tolist(), mop_fxn(x0).add(1).tolist())
self.assertLessEqual(kernel_count, max_kernels)
if kernel_count > max_kernels: raise KernelCountException(max_kernels, kernel_count)
def test_reshape(self): self._test_mop(lambda x: x.reshape(16, 2), max_kernels=2)
def test_permute(self): self._test_mop(lambda x: x.reshape(4, 8).T, max_kernels=3)
+2 -2
View File
@@ -6,7 +6,7 @@ from tinygrad.nn.state import get_parameters
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
from test.helpers import not_support_multi_device, needs_second_gpu, slow, call_is_graph, check_schedule, assert_kernel_count, KernelCountException
settings.register_profile("my_profile", max_examples=200, deadline=None, derandomize=getenv("DERANDOMIZE_CI", False))
settings.load_profile("my_profile")
@@ -395,7 +395,7 @@ class TestMultiBufferView(unittest.TestCase):
linear, var_vals = b_multi.linear_with_vars()
if all(not d.startswith(("WEBGPU", "CL")) for d in b_multi.device):
compiled = [call for call in linear.src if call.src[0].op is Ops.SINK]
self.assertEqual(len(compiled), 0, f"expected zero compiled kernels, got {len(compiled)}")
if len(compiled) != 0: raise KernelCountException(0, len(compiled))
run_linear(linear, var_vals)
np.testing.assert_equal(b_multi.numpy(), b_ref.numpy())
+2 -1
View File
@@ -3,6 +3,7 @@ import numpy as np
from tinygrad import Tensor, Device, TinyJit, Variable, dtypes
from tinygrad.helpers import GlobalCounters, ContextVar, Context, DEV
from tinygrad.uop.ops import PatternMatcher, UPat, UOp, deconstruct_function
from test.helpers import KernelCountException
class TestPickle(unittest.TestCase):
def test_pickle_code_object(self):
@@ -41,7 +42,7 @@ class TestPickle(unittest.TestCase):
t2:Tensor = pickle.loads(st)
np.testing.assert_equal(t_values, t2.numpy())
# expect at most one COPY kernel
self.assertLessEqual(GlobalCounters.kernel_count, 1)
if GlobalCounters.kernel_count > 1: raise KernelCountException(1, GlobalCounters.kernel_count)
def test_pickle_realized_tensor_alt(self):
print("** init")
+2 -1
View File
@@ -1,6 +1,7 @@
import unittest
from tinygrad.helpers import GlobalCounters
from tinygrad.nn.datasets import mnist
from test.helpers import KernelCountException
class TestDataset(unittest.TestCase):
def test_dataset_is_realized(self):
@@ -8,7 +9,7 @@ class TestDataset(unittest.TestCase):
X_train[0].contiguous().realize()
GlobalCounters.reset()
X_train[0].contiguous().realize()
self.assertLessEqual(GlobalCounters.kernel_count, 1) # 0 if SLICE (zero-copy), 1 otherwise
if GlobalCounters.kernel_count > 1: raise KernelCountException(1, GlobalCounters.kernel_count) # 0 if SLICE (zero-copy), 1 otherwise
if __name__ == '__main__':
unittest.main()
+6 -5
View File
@@ -2,6 +2,7 @@ import unittest
from tinygrad import Tensor, UOp, dtypes
from tinygrad.helpers import Context
from tinygrad.uop.ops import Ops
from test.helpers import KernelCountException
class TestRingAllReduce(unittest.TestCase):
def test_schedule_ring(self):
@@ -13,7 +14,7 @@ class TestRingAllReduce(unittest.TestCase):
copies = [si for si in linear.src if si.src[0].op is Ops.COPY]
pairs = [(c.src[1].buffer.device, c.src[2].buffer.device) for c in copies]
# N*(N-1) scatter reduce, and N*(N-1) allgather
self.assertEqual(len(pairs), N*(N-1)*2)
if len(pairs) != N*(N-1)*2: raise KernelCountException(N*(N-1)*2, len(pairs))
# copy topology forms a ring
self.assertEqual(len(set(pairs)), N)
@@ -25,8 +26,8 @@ class TestRingAllReduce(unittest.TestCase):
linear = t.sum(0).mul(2.0).contiguous().linear_with_vars()[0]
copies = [si for si in linear.src if si.src[0].op is Ops.COPY]
sinks = [si for si in linear.src if si.src[0].op is Ops.SINK]
self.assertEqual(len(copies), 24)
self.assertEqual(len(sinks), 26)
if len(copies) != 24: raise KernelCountException(24, len(copies))
if len(sinks) != 26: raise KernelCountException(26, len(sinks))
@Context(RING=0, ALL2ALL=0)
def test_schedule_naive(self):
@@ -39,8 +40,8 @@ class TestRingAllReduce(unittest.TestCase):
sinks = [si for si in linear.src if si.src[0].op is Ops.SINK]
pairs = [(c.src[1].buffer.device, c.src[2].buffer.device) for c in copies]
self.assertEqual(len(pairs), N*(N-1))
self.assertEqual(len(sinks), 2)
if len(pairs) != N*(N-1): raise KernelCountException(N*(N-1), len(pairs))
if len(sinks) != 2: raise KernelCountException(2, len(sinks))
self.assertTrue(all(dst != src for dst, src in pairs))
def test_symbolic_shape(self):
+2 -2
View File
@@ -4,7 +4,7 @@ from tinygrad.function import function
from tinygrad import Tensor, GlobalCounters, Device
from tinygrad.dtype import Invalid
from tinygrad.uop.ops import UOp, Ops, KernelInfo, ProgramInfo
from test.helpers import assert_kernel_count
from test.helpers import assert_kernel_count, KernelCountException
class TestFunction(unittest.TestCase):
def test_simple(self):
@@ -516,7 +516,7 @@ class TestFunctionTuple(unittest.TestCase):
Tensor.realize(a)
c = f(a)
self.assertEqual(count_kernels(c), 1)
if count_kernels(c) != 1: raise KernelCountException(1, count_kernels(c))
c.sum().backward()
Tensor.realize(a.grad)
+5 -4
View File
@@ -81,8 +81,8 @@ base_rewrite = PatternMatcher([
(UPat((Ops.INDEX, Ops.SHRINK), src=(UPat((Ops.BUFFER, Ops.PARAM, Ops.AFTER)),), allow_any_len=True, name="x"), lambda ctx,x:
f" {ctx[x]} = getelementptr inbounds {ldt(x.dtype)}, {ldt(x.dtype, ptr=True)} {ctx[x.src[0]]}, {ldt(x.src[1].dtype)} {ctx[x.src[1]]}"),
# register index
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.cvar("idx")), name="x"), lambda ctx,buf,idx,x:
f" {ctx[x]} = extractelement {ldt(buf.dtype, buf.max_numel())} {ctx[buf]}, i32 {idx.val}" if buf.addrspace == AddrSpace.ALU else None),
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.cvar("c").cast()), name="x"), lambda ctx,buf,c,x:
f" {ctx[x]} = extractelement {ldt(buf.dtype, buf.max_numel())} {ctx[buf]}, i32 {c.val}" if buf.addrspace == AddrSpace.ALU else None),
# load/store
(UPat(Ops.LOAD, src=(UPat.var("idx"), UPat.var("alt"), UPat.var("mask")), name="x"),
@@ -146,6 +146,7 @@ base_rewrite = PatternMatcher([
])
class LLVMRenderer(Renderer):
casted_consts = True
abi: str | None
string_rewrite: PatternMatcher
code_for_op = {k:lambda:None for v in lop.values() for k in v.keys()}
@@ -165,7 +166,7 @@ class LLVMRenderer(Renderer):
local_args: list[str] = []
name = "test"
for u in uops:
if u.op in {Ops.NOOP, Ops.GROUP}: continue
if u.op in {Ops.NOOP, Ops.GROUP, Ops.CONST}: continue
if u.op is Ops.AFTER:
r[u] = r[u.src[0]]
continue
@@ -185,7 +186,7 @@ class LLVMRenderer(Renderer):
kernel.append(f" {r[u]} = addrspacecast [{size} x {ldt(u.dtype)}] addrspace(3)* @{r[u][1:]} to [{size} x {ldt(u.dtype)}]*")
else:
kernel.append(f" {r[u]} = alloca [{size} x {ldt(u.dtype)}], align 16")
elif u.op is Ops.CONST: r[u] = lconst(u.val, u.dtype)
elif u.op is Ops.CAST and u.src[0].op is Ops.CONST: r[u] = lconst(u.src[0].val, u.dtype)
elif u.op is Ops.CAST and ldt(u.dtype) == ldt(u.src[0].dtype):
r[u] = r[u.src[0]] # cast from signed to unsigned of the same size is a noop, or pointer cast
else:
+6 -4
View File
@@ -116,6 +116,7 @@ def nidx(b:mesa.nir_builder, buf, off, space, itemsize, gate=None) -> mesa.nir_d
class NIRRenderer(Renderer):
suffix = "NIR"
casted_consts = True
nir_options: bytes
global_max, local_max, shared_max = CUDARenderer.global_max, CUDARenderer.local_max, CUDARenderer.shared_max
code_for_op = {**{k:lambda:None for k in u_aop.keys()}, **{k:lambda:None for k in s_aop.keys()}, **{k:lambda:None for k in f_aop.keys()}}
@@ -145,7 +146,7 @@ class NIRRenderer(Renderer):
])
def_rewrite = PatternMatcher([
(UPat(Ops.CONST, name="x"), lambda ctx,x: nimm(ctx.b, x.val, x.dtype)),
(UPat.cvar("c").cast(name="x"), lambda ctx,x,c: nimm(ctx.b, c.val, x.dtype)),
(UPat(Ops.PARAM, name="x"), lambda ctx,x: ctx.param(ctx.b, x, x.dtype.itemsize if x.addrspace is AddrSpace.ALU else 8)),
(UPat(Ops.SPECIAL, name="x"), lambda ctx,x: nchannel(ctx.b, {'g':ngid, 'l':nlid, 'i': nid}[x.arg[0]](ctx.b), int(x.arg[-1]))),
(UPat(Ops.STORE, src=(UPat((Ops.INDEX, Ops.SHRINK), src=(UPat.var("buf"),UPat.var("off")), allow_any_len=True), UPat.var("val"))),
@@ -186,16 +187,17 @@ class NIRRenderer(Renderer):
def render(self, uops:list[UOp]):
self.prerender(uops)
for u in [u for u in uops if u.op is Ops.SPECIAL and u.arg[0] == "l"]: self.b.shader.contents.info.workgroup_size[int(u.arg[-1])] = u.src[0].val
for u in [u for u in uops if u.op is Ops.SPECIAL and u.arg[0] == "l"]:
self.b.shader.contents.info.workgroup_size[int(u.arg[-1])] = u.src[0].src[0].val
self.r: dict[UOp, Any] = {}
self.param_idx = 0
ranges: list[mesa.nir_def|None] = []
for u in uops:
if u.op in {Ops.NOOP, Ops.GROUP} or (u.op is Ops.STACK and len(u.src) == 0): pass
if u.op in {Ops.NOOP, Ops.GROUP, Ops.CONST} or (u.op is Ops.STACK and len(u.src) == 0): pass
elif u.op in {Ops.INDEX, Ops.SHRINK}:
# INDEX on a register value picks the element, memory INDEX is handled in the LOAD/STORE patterns
if u.src[0].op not in {Ops.PARAM, Ops.BUFFER, Ops.AFTER}: self.r[u] = nchannel(self.b, self.r[u.src[0]], u.src[1].val)
if u.src[0].op not in {Ops.PARAM, Ops.BUFFER, Ops.AFTER}: self.r[u] = nchannel(self.b, self.r[u.src[0]], u.src[1].src[0].val)
elif u.op is Ops.AFTER:
self.r[u] = self.r[u.src[0]]
elif u.op == Ops.SINK:
+7 -6
View File
@@ -79,8 +79,8 @@ def modifier(a: DType, b: DType): return '.rzi' if dtypes.is_int(a) and dtypes.i
(a.itemsize < b.itemsize or dtypes.is_int(b) or b == dtypes.bool) else ''
string_rewrite = PatternMatcher([
(UPat.cvar("x", dtypes.bool), lambda ctx, x: f"setp.ne.s16 {ctx.r[x]}, {render_val(x.val, x.dtype)}, 0;"),
(UPat.cvar("x"), lambda ctx, x: f"mov.b{ctx.types[x.dtype][1:]} {ctx.r[x]}, {render_val(x.val, x.dtype)};"),
(UPat.cvar("c").cast(dtypes.bool, name="x"), lambda ctx, x, c: f"setp.ne.s16 {ctx.r[x]}, {render_val(c.val, x.dtype)}, 0;"),
(UPat.cvar("c").cast(name="x"), lambda ctx, x, c: f"mov.b{ctx.types[x.dtype][1:]} {ctx.r[x]}, {render_val(c.val, x.dtype)};"),
(UPat(Ops.SPECIAL, name="x"), lambda ctx,x: f"mov.u32 %{x.arg}, %{'ctaid' if x.arg[0] == 'g' else 'tid'}.{chr(120+int(x.arg[-1]))};"),
(UPat(Ops.PARAM, name="x"), lambda ctx, x:
f"ld.param.{ctx.types[dtypes.ulong] if x.addrspace is AddrSpace.GLOBAL else ctx.mem_types[x.dtype]} {ctx.r[x]}, [data{x.arg.slot}+0];"),
@@ -136,6 +136,7 @@ string_rewrite = PatternMatcher([
class PTXRenderer(Renderer):
suffix = "PTX"
casted_consts = True
global_max, local_max, shared_max = CUDARenderer.global_max, CUDARenderer.local_max, CUDARenderer.shared_max
tc_sm80 = [x for x in tc.cuda_sm80 if x.dtype_in in [dtypes.half, dtypes.float]]
code_for_op = asm_for_op
@@ -186,7 +187,7 @@ class PTXRenderer(Renderer):
name = "test"
for u in uops:
if u.op in {Ops.NOOP, Ops.GROUP}: continue
if u.op in {Ops.NOOP, Ops.GROUP, Ops.CONST}: continue
if u.op is Ops.AFTER:
self.r[u] = self.r[u.src[0]]
continue
@@ -201,9 +202,9 @@ class PTXRenderer(Renderer):
continue
if u.op in {Ops.INDEX, Ops.SHRINK, Ops.LOAD} and u.src[0].addrspace in (AddrSpace.REG, AddrSpace.ALU):
# on REG, INDEX/SHRINK pick the register (must be CONST) and LOAD is a noop
if u.op is not Ops.LOAD and u.src[1].op is not Ops.CONST:
if u.op is not Ops.LOAD and not (u.src[1].op is Ops.CAST and u.src[1].src[0].op is Ops.CONST):
raise RuntimeError(f"PTX does not support dynamic register indexing: {u}")
r[u] = r[u.src[0]] if u.op is Ops.LOAD else r[u.src[0]][u.src[1].val]
r[u] = r[u.src[0]] if u.op is Ops.LOAD else r[u.src[0]][u.src[1].src[0].val]
continue
if u.op is Ops.SPECIAL: r[u] = "%" + u.arg
elif u.op is Ops.LOAD:
@@ -216,7 +217,7 @@ class PTXRenderer(Renderer):
[ssa("wmma_acc", dtype="b32") for _ in range(0, len(r[u.src[2]]), 4 // u.dtype.itemsize)]]
r[u] = [ssa("wmma", dtype=self.types[u.dtype]) for _ in range(u.max_numel())]
prefix, dtype = {Ops.CAST: ("cast", None), Ops.BITCAST: ("cast", None), Ops.END: ("pred", "pred"), Ops.RANGE: ("ridx", None),
Ops.CONST: ("const", None), Ops.BUFFER: ("local", "u64"), Ops.INDEX: ("bidx", "u64"), Ops.SHRINK: ("bidx", "u64"),
Ops.BUFFER: ("local", "u64"), Ops.INDEX: ("bidx", "u64"), Ops.SHRINK: ("bidx", "u64"),
Ops.PARAM: ("dat", "u64" if u.addrspace is AddrSpace.GLOBAL else None), **{op: ("alu", None) for op in GroupOp.ALU}}.get(u.op, (None, None))
if u.op is Ops.RANGE and u.dtype == dtypes.void: prefix = None # loop headers don't have a register
if prefix: r[u] = ssa(prefix, u, dtype)