Compare commits

..
Author SHA1 Message Date
sirhcmandGitHub 0820c7a424 benchmarks: openpilot matrix (#17714) 2026-08-26 18:53:55 -04:00
wozeparrotandGitHub 22ffff3258 gptoss: single jit (#17525) 2026-08-26 15:32:21 -07:00
George HotzandGitHub a0a901c8e4 faster qwen 3.8 (#17720)
* faster qwen

* test fix

* dead code

* fix gguf issue

* pretty nt loads

* lil

* use warp
2026-08-26 15:23:25 -07:00
sirhcmandGitHub c015351ac5 fix _get_cpu_count for docker --cpus=N in python 3.13+ (#17760) 2026-08-26 16:53:30 -04:00
11 changed files with 380 additions and 244 deletions
+48 -42
View File
@@ -446,14 +446,55 @@ jobs:
- name: UsbGPU (USB4/TB) tiny tests
run: DEV=PCI+NV:NAK python3.11 test/test_tiny.py
testcommalatest:
name: comma Benchmark (0.11.2)
testcomma:
strategy:
matrix:
dev: ['QCOM', 'QCOM:IR3']
version: ['0.11.0', '0.11.2']
model: ['vision', 'policy', 'supercombo', 'dmonitoring']
# exclude non-existent models
exclude: [{ version: '0.11.0', model: supercombo }, { version: '0.11.2', model: vision }, { version: '0.11.2', model: policy }]
include:
- version: '0.11.0'
model: vision
url: https://github.com/commaai/openpilot/raw/v0.11.0/selfdrive/modeld/models/driving_vision.onnx
timing: 17
- version: '0.11.0'
model: policy
url: https://github.com/commaai/openpilot/raw/v0.11.0/selfdrive/modeld/models/driving_policy.onnx
timing: 3.2
- version: '0.11.0'
model: dmonitoring
url: https://github.com/commaai/openpilot/raw/v0.11.0/selfdrive/modeld/models/dmonitoring_model.onnx
timing: 11
- version: '0.11.2'
model: supercombo
url: https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/433f85f956837606ad1f1cbee4aa7e2158ad23c768dea914b20436c97232741b
timing: 26
- dev: QCOM:IR3
version: '0.11.2'
model: supercombo
timing: 41
- version: '0.11.2'
model: dmonitoring
url: https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/3e7b31dfbc0a5234f1baf196513b77fc6af12204b8a8ffe8ee0417e48352f316
timing: 11
# IR3 dmonitoring is slightly slower
- dev: QCOM:IR3
model: dmonitoring
timing: 12
fail-fast: false
name: openpilot ${{ matrix.version }} compile3 ${{ matrix.model }} (DEV=${{ matrix.dev }})
runs-on: [self-hosted, Linux, comma]
timeout-minutes: 12
timeout-minutes: 5
defaults:
run:
shell: bash -e -o pipefail {0}
if: github.repository_owner == 'tinygrad'
env:
DEV: ${{ matrix.dev }}
ASSERT_MIN_STEP_TIME: ${{ matrix.timing }}
BENCHMARK_LOG: ${{ matrix.dev == 'QCOM:IR3' && 'ir3_' || '' }}openpilot_${{ matrix.version }}_${{ matrix.model }}
steps:
- name: Checkout Code
uses: actions/checkout@v6
@@ -464,45 +505,10 @@ jobs:
rm -f /tmp/staging.db /tmp/staging.db-shm /tmp/staging.db-wal
- name: reset process replay
run: test/external/process_replay/reset.py
- name: openpilot compile3 0.11.2 supercombo
run: BENCHMARK_LOG=openpilot_0_11_2_supercombo PYTHONPATH="." ASSERT_MIN_STEP_TIME=26 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/433f85f956837606ad1f1cbee4aa7e2158ad23c768dea914b20436c97232741b
- name: openpilot compile3 0.11.2 supercombo (from pickle)
run: BENCHMARK_LOG=openpilot_0_11_2_supercombo_run_pickle RUN_PICKLE=1 PYTHONPATH="." ASSERT_MIN_STEP_TIME=26 DEV=QCOM taskset -c 4-7 python3 examples/openpilot/compile3.py
- name: IR3 openpilot compile3 0.11.2 supercombo
run: BENCHMARK_LOG=ir3_openpilot_0_11_2_supercombo PYTHONPATH="." ASSERT_MIN_STEP_TIME=41 DEV=QCOM:IR3 FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/433f85f956837606ad1f1cbee4aa7e2158ad23c768dea914b20436c97232741b
- name: openpilot compile3 0.11.2 dmonitoring
run: BENCHMARK_LOG=openpilot_0_11_2_dmonitoring PYTHONPATH="." ASSERT_MIN_STEP_TIME=11 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/3e7b31dfbc0a5234f1baf196513b77fc6af12204b8a8ffe8ee0417e48352f316
- name: Run process replay tests
uses: ./.github/actions/process-replay
testcommaold:
name: comma Benchmark (0.11.0)
runs-on: [self-hosted, Linux, comma]
timeout-minutes: 12
defaults:
run:
shell: bash -e -o pipefail {0}
if: github.repository_owner == 'tinygrad'
steps:
- name: Checkout Code
uses: actions/checkout@v6
- name: setup staging db
if: github.ref == 'refs/heads/update_benchmark_staging'
run: |
echo "CACHEDB=/tmp/staging.db" >> $GITHUB_ENV
rm -f /tmp/staging.db /tmp/staging.db-shm /tmp/staging.db-wal
- name: reset process replay
run: test/external/process_replay/reset.py
- name: openpilot compile3 0.11.0 driving_vision
run: BENCHMARK_LOG=openpilot_0_11_0_vision PYTHONPATH="." ASSERT_MIN_STEP_TIME=17 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/v0.11.0/selfdrive/modeld/models/driving_vision.onnx
- name: openpilot compile3 0.11.0 driving_vision (from pickle)
run: BENCHMARK_LOG=openpilot_0_11_0_vision_run_pickle RUN_PICKLE=1 PYTHONPATH="." ASSERT_MIN_STEP_TIME=17 DEV=QCOM taskset -c 4-7 python3 examples/openpilot/compile3.py
- name: IR3 openpilot compile3 0.11.0 driving_vision
run: BENCHMARK_LOG=ir3_openpilot_0_11_0_vision PYTHONPATH="." ASSERT_MIN_STEP_TIME=18 DEV=QCOM:IR3 FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/v0.11.0/selfdrive/modeld/models/driving_vision.onnx
- name: openpilot compile3 0.11.0 driving_policy
run: BENCHMARK_LOG=openpilot_0_11_0_policy PYTHONPATH="." ASSERT_MIN_STEP_TIME=3.2 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/v0.11.0/selfdrive/modeld/models/driving_policy.onnx
- name: openpilot compile3 0.11.0 dmonitoring
run: BENCHMARK_LOG=openpilot_0_11_0_dmonitoring PYTHONPATH="." ASSERT_MIN_STEP_TIME=11 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/v0.11.0/selfdrive/modeld/models/dmonitoring_model.onnx
- name: compile
run: FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py ${{ matrix.url }}
- name: run pickle
run: BENCHMARK_LOG="${BENCHMARK_LOG}_run_pickle" RUN_PICKLE=1 taskset -c 4-7 python3 examples/openpilot/compile3.py
- name: Run process replay tests
uses: ./.github/actions/process-replay
+20 -35
View File
@@ -1674,8 +1674,7 @@ def train_gptoss():
config = {}
BASEDIR = config["BASEDIR"] = Path(getenv("BASEDIR", "/raid/datasets/c4-8b/"))
BS = config["BS"] = getenv("BS", 16)
grad_acc = config["GRADIENT_ACC_STEPS"] = getenv("GRADIENT_ACC_STEPS", 1)
GBS = config["GLOBAL_BATCH_SIZE"] = BS * grad_acc
GBS = config["GLOBAL_BATCH_SIZE"] = BS
SEED = config["SEED"] = getenv("SEED", 5760)
DATA_SEED = config["DATA_SEED"] = getenv("DATA_SEED", SEED)
SEQLEN = config["SEQLEN"] = getenv("SEQLEN", 8192)
@@ -1737,8 +1736,8 @@ def train_gptoss():
params_wd = [p for p in params if p.ndim >= 3]
params_no_wd = [p for p in params if p.ndim < 3]
optim = GradAccClipAdamWGroup(
GradAccClipAdamW(params_wd, lr=0.0, b1=opt_adamw_beta_1, b2=opt_adamw_beta_2, eps=opt_adamw_epsilon, weight_decay=opt_adamw_weight_decay, grad_acc=grad_acc, device=optim_device),
GradAccClipAdamW(params_no_wd, lr=0.0, b1=opt_adamw_beta_1, b2=opt_adamw_beta_2, eps=opt_adamw_epsilon, weight_decay=0.0, grad_acc=grad_acc, device=optim_device),
GradAccClipAdamW(params_wd, lr=0.0, b1=opt_adamw_beta_1, b2=opt_adamw_beta_2, eps=opt_adamw_epsilon, weight_decay=opt_adamw_weight_decay, grad_acc=1, device=optim_device),
GradAccClipAdamW(params_no_wd, lr=0.0, b1=opt_adamw_beta_1, b2=opt_adamw_beta_2, eps=opt_adamw_epsilon, weight_decay=0.0, grad_acc=1, device=optim_device),
)
for p in optim.params:
@@ -1770,7 +1769,7 @@ def train_gptoss():
@TinyJit
@Context(TRAINING=1)
def minibatch(tokens:Tensor):
def step(tokens:Tensor):
if is_dp: tokens = tokens.to(None).shard(device, 0)
if not is_sharding: tokens = tokens.to(None)
@@ -1784,22 +1783,20 @@ def train_gptoss():
for g, new_g in zip(grads, loss.gradient(*optim.params)):
apply_grad(g, new_g.uop)
loss_cpu = loss.flatten().float().to("CPU")
return loss_cpu.realize(*grads)
Tensor.realize(loss, *grads)
@TinyJit
def optim_step():
grad_norm = clip_grads(grads, grad_acc, 1.0)
grad_norm = clip_grads(grads, 1, 1.0)
optim.fstep(grads, grad_norm)
scheduler.step()
for g in grads: g.assign(0)
loss_cpu = loss.flatten().float().to("CPU")
lr_cpu = optim.lr.float().to("CPU")
grad_norm_cpu = grad_norm.float().to("CPU")
Tensor.realize(lr_cpu, grad_norm_cpu, *grads, *fp8_inv_scales)
Tensor.realize(loss_cpu, lr_cpu, grad_norm_cpu, *grads, *fp8_inv_scales)
return lr_cpu, grad_norm_cpu
return loss_cpu, lr_cpu, grad_norm_cpu
@TinyJit
@Context(TRAINING=0)
@@ -1848,30 +1845,20 @@ def train_gptoss():
profile_marker(f"train @ {i}")
st = time.perf_counter()
stopped = False
losses, data_time, dev_time = [], 0, 0
for _ in range(grad_acc if i >= 2 else 1):
ist = time.perf_counter()
try: tokens = next(train_iter)
except StopIteration:
stopped = True
break
mst = time.perf_counter()
data_time += mst - ist
losses.append(minibatch(tokens).item())
dev_time += time.perf_counter() - mst
if stopped: break
ist = time.perf_counter()
gt = time.perf_counter()
ret = optim_step()
lr, grad_norm = ret[0].item(), ret[1].item()
try: tokens = next(train_iter)
except StopIteration: break
mst = time.perf_counter()
data_time = mst - ist
ret = step(tokens)
dev_time = time.perf_counter() - mst
loss, lr, grad_norm = ret[0].item(), ret[1].item(), ret[2].item()
et = time.perf_counter()
loss = sum(losses) / len(losses)
optim_time = et - gt
dev_time += optim_time
step_time = et - st
gbs_time = gt - st
if BENCHMARK: step_times.append(step_time)
i += 1
@@ -1881,7 +1868,7 @@ def train_gptoss():
gflops = GlobalCounters.global_ops / 1e9 / dev_time
mfu = ((6 * num_params * SEQLEN * GBS) / (dev_time * device_count * 4.6e15)) * 100
tqdm.write(
f"{i:5} {step_time:.3f} s step, {gbs_time:.3f} s gbs, {optim_time:.3f} s optim, {data_time:.3f} s data, {loss:.4f} loss, " \
f"{i:5} {step_time:.3f} s step, {dev_time:.3f} s dev, {data_time:.3f} s data, {loss:.4f} loss, " \
f"{lr:.12f} LR, {grad_norm:.6f} grad_norm, {mem_gb:.2f} GB used, {gflops:9.2f} GFLOPS, {mfu:5.2f}% MFU")
if DEBUG >= 1: tqdm.write(" mem per device: " + ', '.join(f"{dev}: {mem/1e9:.2f} GB" for dev, mem in sorted(GlobalCounters.mem_used_per_device.items())))
@@ -1891,8 +1878,6 @@ def train_gptoss():
"train/lr": lr,
"train/grad_norm": grad_norm,
"train/step_time": step_time,
"train/gbs_time": gbs_time,
"train/optim_time": optim_time,
"train/dev_time": dev_time,
"train/data_time": data_time,
"train/mem": mem_gb,
+1 -1
View File
@@ -16,7 +16,7 @@ def apply_rope(x:Tensor, start_pos:int):
class TestLinear(unittest.TestCase):
def test_recovers_packed_ggml_weight(self):
for ggml_type,packed_size,words in ((13, 176, 44), (14, 210, 210), (23, 136, 34)):
for ggml_type,packed_size,words in ((13, 176, 44), (14, 210, 53), (23, 136, 34)):
packed = Tensor.empty(packed_size+4, dtype=dtypes.uint8, device="CPU")[4:]
decoded = ggml_data_to_tensor(packed, 256, ggml_type).reshape(1, 256)
linear = Linear(256, 1, bias=False)
+6 -2
View File
@@ -20,11 +20,13 @@ class TestQ8Quantize(unittest.TestCase):
def test_values_and_scales(self):
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
x = np.linspace(-3.1, 2.7, 64, dtype=np.float32).reshape(2, 32)
quant, scale = q8_quantize(Tensor(x), 2, 32)
quant, scale, gsum = q8_quantize(Tensor(x), 2, 32)
scale_np = np.maximum(np.max(np.abs(x), axis=-1, keepdims=True) / 127, 1e-8)
expected = np.clip(np.rint(x / scale_np), -127, 127).astype(np.int8)
np.testing.assert_array_equal(quant.bitcast(dtypes.int8).reshape(2, 32).numpy(), expected)
np.testing.assert_allclose(scale.numpy(), scale_np, rtol=1e-6)
# xsum holds the two per-16 sums per 32-wide group
np.testing.assert_array_equal(gsum.numpy().reshape(2, 2), expected.reshape(2, 2, 16).sum(-1).astype(np.float32))
def test_q6_linear_compiles(self):
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
@@ -36,7 +38,9 @@ class TestQ8Quantize(unittest.TestCase):
linear = Linear(256, 1, bias=False)
nn.state.load_state_dict(linear, {"weight":decoded}, verbose=False, realize=False)
self.assertTrue(np.isfinite(linear(Tensor.randn(1, 256)).realize().item()))
self.assertEqual(linear.weight.uop.buf_uop.buffer.offset, 4)
# the Q6 weight is repacked: 210-byte blocks padded to 212 (one block = 53 words)
self.assertEqual(linear.weight.uop.buf_uop.buffer.nbytes, 53*4)
self.assertEqual(linear.weight.dtype, dtypes.uint32)
def test_q4_k_linear(self):
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
+5 -4
View File
@@ -103,7 +103,7 @@ def memory_coalescing(sink:UOp, ctx:Renderer) -> UOp:
if getenv("DMC"): return sink
# collect
memory: defaultdict[tuple[Ops, UOp, UOp|str, UOp], dict[int, list[UOp]]] = defaultdict(dict)
memory: defaultdict[tuple[Ops, UOp, UOp|str, UOp, object], dict[int, list[UOp]]] = defaultdict(dict)
for u in sink.toposort():
# TODO: this should handle images too, it's just memory coalescing
if u.op in {Ops.LOAD, Ops.STORE}:
@@ -118,11 +118,12 @@ def memory_coalescing(sink:UOp, ctx:Renderer) -> UOp:
elif idx.op is Ops.CONST and idx.val is Invalid: root_src, arg = "INVALID", 0
elif idx.op is Ops.CONST: root_src, arg = "CONST", idx.val
else: root_src, arg = idx, 0
memory[(u.op, buf, root_src, valid)].setdefault(arg, []).append(u)
# loads/stores only coalesce with others carrying the same arg (e.g. the nontemporal flag)
memory[(u.op, buf, root_src, valid, u.arg)].setdefault(arg, []).append(u)
# build replacements
replacements = {}
for (op,buf,base,valid),offsets in memory.items():
for (op,buf,base,valid,ld_arg),offsets in memory.items():
# allowed lengths (copied in)
lengths = []
must_divide = True
@@ -157,7 +158,7 @@ def memory_coalescing(sink:UOp, ctx:Renderer) -> UOp:
store = idx.store(UOp.stack(*datas) if len(datas) > 1 else datas[0])
for i,g in enumerate(grp): replacements[offsets[g][0]] = store
else:
ld = idx.load()
ld = idx.load(arg=ld_arg)
for i,g in enumerate(grp):
for oo in offsets[g]:
replacements[oo] = ld.index(i) if len(grp) > 1 else ld
+6 -6
View File
@@ -250,16 +250,16 @@ EMULATED_DTYPES = ContextVar("EMULATED_DTYPES", "")
DEFAULT_FLOAT, DEFAULT_INT = ContextVar("DEFAULT_FLOAT", "float32"), ContextVar("DEFAULT_INT", "int32")
CAPTURE_PROCESS_REPLAY = ContextVar("CAPTURE_PROCESS_REPLAY", 0)
def _get_cpu_count() -> int:
# os.process_cpu_count (3.13+) respects cgroup limits
if hasattr(os, "process_cpu_count"): return max(1, os.process_cpu_count() or 1)
# cgroup v2 (containers with --cpus=N)
# os.process_cpu_count is available in 3.13+, then try affinity, then fallback to cpu_count
count = (os.process_cpu_count() if hasattr(os, "process_cpu_count") else
len(os.sched_getaffinity(0)) if hasattr(os, "sched_getaffinity") else os.cpu_count()) or 1
# limit with cgroup v2 (containers with --cpus=N)
try:
with open("/sys/fs/cgroup/cpu.max") as f:
quota, period = f.read().strip().split()
if quota != "max": return max(1, int(quota) // int(period))
if quota != "max": count = min(count, max(1, int(quota) // int(period)))
except (FileNotFoundError, ValueError, ZeroDivisionError): pass
# fall back to affinity (respects taskset but not cgroup quota)
return max(1, len(os.sched_getaffinity(0)) if hasattr(os, "sched_getaffinity") else (os.cpu_count() or 1))
return count
NUM_CPU_THREADS = ContextVar("NUM_CPU_THREADS", _get_cpu_count())
NULL_ALLOW_COPYOUT = ContextVar("NULL_ALLOW_COPYOUT", 0)
# VIZ implies PROFILE, but you can run PROFILE without VIZ
+259 -104
View File
@@ -7,12 +7,13 @@ from tinygrad.dtype import AddrSpace, dtypes
from tinygrad.helpers import prod
from tinygrad.uop.ops import AxisType, KernelInfo, Ops, resolve
BLOCK_M, BLOCK_N, DECODE_HEAD_TILE, WARP_SIZE = 32, 32, 8, 32
BLOCK_M, BLOCK_N, WARP_SIZE = 32, 32, 32
WMMA_M, WMMA_N, WMMA_K = 16, 16, 16
WAVES_M, WAVES_N, LANES_PER_WAVE_M, LANES_PER_WAVE_N = 2, 2, 2, 16
WMMA_ACC, THREADS_PER_BLOCK = WMMA_M // LANES_PER_WAVE_M, WARP_SIZE * WAVES_M * WAVES_N
LDS_PAD, WMMA_ARG, LOG2E = 4, ((WMMA_M, WMMA_N, WMMA_K), 'AMD', 32), math.log2(math.e)
Q4_K, Q5_K, Q6_K, IQ4_XS, GGML_BLOCK_SIZE, Q8_GROUP_SIZE, Q4_WORDS, Q5_WORDS, Q6_BYTES, IQ4_WORDS = 12, 13, 14, 23, 256, 32, 36, 44, 210, 34
Q6_PADDED, Q6_WORDS = 212, 53 # the 210-byte Q6 blocks are padded to 212 bytes so they are word-addressable
QUANT_SIZES = {Q4_K: Q4_WORDS*4, Q5_K: Q5_WORDS*4, Q6_K: Q6_BYTES, IQ4_XS: IQ4_WORDS*4} # bytes per 256-weight block
def kernel_var(x:UOp) -> UOp:
@@ -54,21 +55,43 @@ class Linear(nn.Linear):
self.in_features, self.out_features = in_features, out_features
def set_quantized(self, decoded:Tensor):
packed_sizes = {decoded.numel() // 256 * type_size:typ for typ,type_size in QUANT_SIZES.items()}
raw = next((u for u in decoded.uop.toposort() if u.op is Ops.SHRINK and u.dtype == dtypes.uint8 and prod(u.shape) in packed_sizes), None)
graph = decoded.uop.toposort()
raw = next((u for u in graph if u.op is Ops.SHRINK and u.dtype == dtypes.uint8 and prod(u.shape) in packed_sizes), None)
if raw is None: return
ggml_type = packed_sizes[prod(raw.shape)]
# the packed byte rate alone can't distinguish same-rate formats (Q4_0 vs Q4_K, Q5_0 vs Q5_K, MXFP4 vs IQ4_XS).
# the supported formats are 256-wide superblocks: their decode views the packed bytes at the superblock width
# (ggml_data_to_tensor reshapes to (-1, QUANT_SIZES[type])), while same-rate 32-wide formats reshape to 17-22
if not any(u.op is Ops.RESHAPE and u.shape[-1:] == (QUANT_SIZES[ggml_type],) for u in graph): return
raw_offset = raw.contiguous_view_offset()
assert raw_offset is not None and raw_offset % 4 == 0 and raw.buf_uop.dtype == dtypes.uint8
self.ggml_type = packed_sizes[prod(raw.shape)]
self.ggml_type = ggml_type
# store a typed buffer view: a lazy BITCAST is decomposed into byte-combining ALU before custom-kernel
# scheduling and would copy the entire packed weight on every JIT graph
packed_dtype = dtypes.uint8 if self.ggml_type == Q6_K else dtypes.uint32
self.weight = Tensor(UOp.from_buffer(cast(Buffer, raw.buf_uop.buffer)
.view(raw.max_numel() * raw.dtype.itemsize // packed_dtype.itemsize, packed_dtype, raw_offset)))
if self.ggml_type == Q6_K:
# Q6 blocks are 210 bytes, so consecutive blocks are only 2-byte aligned. pad each block to 212 bytes
# (a one-time copy at load) so the kernel can do all its reads as aligned u32 words
nbytes, nblocks = raw.max_numel(), raw.max_numel() // Q6_BYTES
byte_view = Tensor(UOp.from_buffer(cast(Buffer, raw.buf_uop.buffer).view(nbytes, dtypes.uint8, raw_offset)))
padded = byte_view.reshape((nblocks, Q6_BYTES)).pad_to((nblocks, Q6_PADDED)).contiguous().realize()
self.weight = Tensor(UOp.from_buffer(cast(Buffer, padded.uop.buf_uop.buffer).view(nblocks * Q6_WORDS, dtypes.uint32, 0)))
else:
self.weight = Tensor(UOp.from_buffer(cast(Buffer, raw.buf_uop.buffer)
.view(raw.max_numel() * raw.dtype.itemsize // dtypes.uint32.itemsize, dtypes.uint32, raw_offset)))
def __call__(self, x:Tensor) -> Tensor:
supported = self.use_custom_quant and amd_custom_kernels_supported(self.weight.device)
if self.ggml_type is None and supported:
self.set_quantized(self.weight)
if self.ggml_type is None: self.use_custom_quant = supported = False # not a supported quant format
if self.ggml_type is None:
# tiny dense fp16 matmul (e.g. the ssm beta/alpha head rows): single fp16 gemv kernel instead of a
# generic matmul schedule, and realize the densely packed weight once if it is still a lazy ggml view
if self.weight.dtype in (dtypes.half, dtypes.float, dtypes.bfloat16) and self.out_features <= 2048 \
and self.in_features % (WARP_SIZE*4) == 0:
numel, max_shape = x.numel(), x.max_shape
if isinstance(numel, int) or prod(max_shape) // self.in_features <= 32:
out = f16_gemv(self, x if isinstance(numel, int) else x.pad_to(max_shape))
return out if isinstance(numel, int) else out.shrink(tuple((0, s) for s in (*x.shape[:-1], self.out_features)))
self.use_custom_quant = supported = False # not a supported quant format
if self.ggml_type in (Q4_K, Q5_K, Q6_K, IQ4_XS) and supported:
if isinstance(x.numel(), int): return q8_linear(self, x)
# symbolic token count: pad to the max chunk size so the kernels see static shapes, garbage rows are sliced off
@@ -77,14 +100,20 @@ class Linear(nn.Linear):
return super().__call__(x)
def _amd_dp4a(a:UOp, b:UOp, c:UOp) -> UOp:
return UOp(Ops.CUSTOMI, src=(a.int(), b.int(), c), arg=("__builtin_amdgcn_sudot4(true, {}, true, {}, {}, false)", dtypes.int32))
# int8 4-wide dot, widened to scalar multiply-adds (2% decode slower than the sudot4 builtin, but portable)
for i in range(4):
av = ((a >> (8*i)) & 255).cast(dtypes.uint8).bitcast(dtypes.int8).int()
bv = ((b >> (8*i)) & 255).cast(dtypes.uint8).bitcast(dtypes.int8).int()
c = c + av*bv
return c
def _amd_byte_perm(a:UOp, b:UOp, selectors:UOp) -> UOp:
return UOp(Ops.CUSTOMI, src=tuple(x.cast(dtypes.uint32) for x in (a, b, selectors)), arg=("__builtin_amdgcn_perm({}, {}, {})", dtypes.uint32))
def _amd_load(ptr:UOp, lanes:int|None=None) -> UOp:
assert ptr.op is Ops.INDEX
if lanes is None: return UOp(Ops.CUSTOMI, src=(ptr,), arg=("__builtin_nontemporal_load({0})", ptr.dtype))
# nontemporal scalar load: streamed weights must not evict the activations/KV cache from L2
if lanes is None: return ptr.load(arg="nontemporal")
buf, coords = ptr.src[0], ptr.src[1:]
idx = sum((coord*math.prod(buf.shape[i+1:]) for i,coord in enumerate(coords)), UOp.const(0))
return UOp(Ops.SHRINK, src=(buf.flatten(), idx, UOp.const(lanes))).load(dtype=ptr.dtype)
@@ -93,16 +122,19 @@ def _load_byte(raw:UOp, base:UOp, offset:UOp) -> UOp: return (raw[base + offset/
def _half(value:UOp) -> UOp: return value.cast(dtypes.uint16).bitcast(dtypes.float16).float()
def _iq4_bytes(packed:UOp, shift:int) -> UOp:
# the non-linear iq4nl table as a byte lookup: 3 byte_perms beat any arithmetic/select-tree form (~60% decode)
selectors = (packed >> shift) & 0x0f0f0f0f
low = _amd_byte_perm(UOp.const(0xf6eaddcf, dtypes.uint32), UOp.const(0xbfad9881, dtypes.uint32), selectors)
high = _amd_byte_perm(UOp.const(0x71594535, dtypes.uint32), UOp.const(0x26190d01, dtypes.uint32), selectors & 0x07070707)
return _amd_byte_perm(high, low, 0x03020100 | ((selectors & 0x08080808) >> 1))
def _q5_scales(raw:UOp, base:UOp, subgroup:UOp) -> tuple[UOp, UOp, UOp, UOp]:
scale = (subgroup < 4).where(_load_byte(raw, base, 4 + subgroup) & 63,
(_load_byte(raw, base, 8 + subgroup) & 15) | ((_load_byte(raw, base, subgroup) >> 6) << 4))
minimum = (subgroup < 4).where(_load_byte(raw, base, 8 + subgroup) & 63,
(_load_byte(raw, base, 8 + subgroup) >> 4) | ((_load_byte(raw, base, 4 + subgroup) >> 6) << 4))
# scales/mins (6-bit each) live in block bytes 4-15: three words total, same for the whole super-block's lanes
w1, w2, w3 = _amd_load(raw[base+1]), _amd_load(raw[base+2]), _amd_load(raw[base+3])
sb = (subgroup & 3) * 8 # byte within word
byte1, byte2, byte3 = (w1 >> sb) & 255, (w2 >> sb) & 255, (w3 >> sb) & 255
scale = (subgroup < 4).where(byte1 & 63, (byte3 & 15) | ((byte1 >> 6) << 4))
minimum = (subgroup < 4).where(byte2 & 63, (byte3 >> 4) | ((byte2 >> 6) << 4))
d, dmin = (raw[base] & 0xffff).cast(dtypes.uint16), (raw[base] >> 16).cast(dtypes.uint16)
return _half(d), _half(dmin), scale.float(), minimum.float()
@@ -118,52 +150,66 @@ def iq4_half_lut(device:str) -> Tensor:
dtype=dtypes.float16, device=device).bitcast(dtypes.uint32).contiguous()
@functools.cache
def _q8_quantize_kernel(q:UOp, scale:UOp, x:UOp, tokens:int, in_features:int) -> UOp:
def _q8_quantize_kernel(q:UOp, scale:UOp, xsum:UOp, x:UOp, tokens:int, in_features:int) -> UOp:
groups = in_features//Q8_GROUP_SIZE
token_group, lane = UOp.range(tokens*groups, 0), UOp.range(32, 1, axis_type=AxisType.LOCAL)
token_group, lane = UOp.range(tokens*groups, 0, axis_type=AxisType.GLOBAL), UOp.range(32, 1, axis_type=AxisType.LOCAL)
token, group = token_group//groups, token_group%groups
x = x.reshape(tokens, groups, 32)
group_scale = (warp_reduce(x[token, group, lane].float().abs(), maximum=True, full_wave=True) / 127).maximum(1e-8)
word_lane = lane.minimum(7)
xs = tuple(x[token, group, word_lane*4+i].float() for i in range(4))
word = sum(((v/group_scale).round().clip(-127, 127).cast(dtypes.int8).cast(dtypes.uint8).cast(dtypes.uint32) << (i*8)
for i,v in enumerate(xs)), UOp.const(0, dtypes.uint32))
stores = (q[token, group, lane.valid(lane < 8)].store(word), scale[token, group.valid(lane.eq(0))].store(group_scale))
qs = tuple((v/group_scale).round().clip(-127, 127).cast(dtypes.int8) for v in xs)
word = sum((v.cast(dtypes.uint8).cast(dtypes.uint32) << (i*8) for i, v in enumerate(qs)), UOp.const(0, dtypes.uint32))
# per-16 sums of the quantized values (lanes 0-3 / 4-7): Q4_K/Q5_K need the 32-sum, Q6_K the 16-sums
part = (lane < 8).where(sum((v.cast(dtypes.int32) for v in qs), UOp.const(0, dtypes.int32)), UOp.const(0, dtypes.int32))
gsum = [warp_reduce(((lane & 4).eq(h*4)).where(part, UOp.const(0, dtypes.int32)), full_wave=True) for h in range(2)]
store_half = (lane & 4) >> 2
stores = (q[token, group, lane.valid(lane < 8)].store(word),
UOp.group(scale[token, group.valid(lane.eq(0))].store(group_scale),
xsum[token, group, store_half.valid(lane.eq(0) | lane.eq(4))].store(
store_half.eq(0).where(gsum[0].float(), gsum[1].float()))))
return UOp.group(*stores).end(token_group, lane).sink(arg=KernelInfo(name="q8_quantize", opts_to_apply=()))
def q8_quantize(x:Tensor, tokens:int, in_features:int) -> tuple[Tensor, Tensor]:
def q8_quantize(x:Tensor, tokens:int, in_features:int) -> tuple[Tensor, Tensor, Tensor]:
groups = in_features//Q8_GROUP_SIZE
q = Tensor.empty(tokens, groups, 8, dtype=dtypes.uint32, device=x.device)
scale = Tensor.empty(tokens, groups, dtype=dtypes.float32, device=x.device)
q, scale = Tensor.custom_kernel(q, scale, x, fxn=functools.partial(_q8_quantize_kernel, tokens=tokens, in_features=in_features))[:2]
return q, scale
xsum = Tensor.empty(tokens, groups, 2, dtype=dtypes.float32, device=x.device)
q, scale, xsum = Tensor.custom_kernel(q, scale, xsum, x, fxn=functools.partial(_q8_quantize_kernel, tokens=tokens, in_features=in_features))[:3]
return q, scale, xsum
def _decode_linear(out:UOp, out_features:int, group_count:int, group_dot, name:str) -> UOp:
chunks = (group_count+31)//32
token_output_chunk, lane = UOp.range(out.shape[0]*out_features*chunks, 0), UOp.range(32, 1, axis_type=AxisType.LOCAL)
token, output, chunk = token_output_chunk // (out_features*chunks), (token_output_chunk//chunks) % out_features, token_output_chunk % chunks
group = lane+chunk*32
value = group_dot(token, output, group) if group_count % 32 == 0 else \
(group < group_count).where(group_dot(token, output, group.minimum(group_count-1)), UOp.const(0, dtypes.float32))
chunks = out.shape[2]
# two-dim global grid instead of one flat grid: no div/mods needed to decompose the gid
token_output = UOp.range(out.shape[0]*out_features, 0, axis_type=AxisType.GLOBAL)
chunk, lane = UOp.range(chunks, 1, axis_type=AxisType.GLOBAL), UOp.range(32, 2, axis_type=AxisType.LOCAL)
token, output = token_output // out_features, token_output % out_features
group = (lane+chunk*32).minimum(group_count-1)
value = group_dot(token, output, group) if chunks*32 == group_count else \
(lane+chunk*32 < group_count).where(group_dot(token, output, group), UOp.const(0, dtypes.float32))
total = warp_reduce(value, full_wave=True)
return out[token, output, chunk.valid(lane.eq(0))].store(total.cast(out.dtype)).end(token_output_chunk, lane).sink(
return out[token, output, chunk.valid(lane.eq(0))].store(total.cast(out.dtype)).end(token_output, chunk, lane).sink(
arg=KernelInfo(name=name, opts_to_apply=()))
@functools.cache
def _quant_decode_kernel(out:UOp, raw:UOp, xq:UOp, xd:UOp, out_features:int, in_features:int, ggml_type:int) -> UOp:
def _quant_decode_kernel(out:UOp, raw:UOp, xq:UOp, xd:UOp, xs:UOp, out_features:int, in_features:int, ggml_type:int) -> UOp:
group_count = in_features // Q8_GROUP_SIZE
def group_dot(token:UOp, output:UOp, group:UOp) -> UOp:
block, subgroup = group // 8, group % 8
xwords = _amd_load(xq[token, group, 0], 8)
if ggml_type in (Q4_K, Q5_K):
base = (output * in_features//GGML_BLOCK_SIZE + block) * (Q4_WORDS if ggml_type == Q4_K else Q5_WORDS)
qs_base, dot, qsum = base + (4 if ggml_type == Q4_K else 12) + (subgroup//2)*8, UOp.const(0, dtypes.int32), UOp.const(0, dtypes.int32)
qs_base, dot = base + (4 if ggml_type == Q4_K else 12) + (subgroup//2)*8, UOp.const(0, dtypes.int32)
# vectorize the 8 packed-weight words and (for Q5_K) the 32-byte high-bit bitmap
qs_pair = (_amd_load(raw[qs_base], 4), _amd_load(raw[qs_base+4], 4))
if ggml_type == Q5_K: qh_pair = (_amd_load(raw[base+4], 4), _amd_load(raw[base+8], 4))
for word_idx in range(8):
word = (raw[qs_base+word_idx] >> ((subgroup&1)*4).cast(dtypes.uint32)) & 0x0f0f0f0f
if ggml_type == Q5_K: word |= ((raw[base+4+word_idx] >> subgroup.cast(dtypes.uint32)) & 0x01010101) << 4
dot, qsum = _amd_dp4a(word, xwords[word_idx], dot), _amd_dp4a(UOp.const(0x01010101, dtypes.uint32), xwords[word_idx], qsum)
word = (qs_pair[word_idx//4][word_idx%4] >> ((subgroup&1)*4).cast(dtypes.uint32)) & 0x0f0f0f0f
if ggml_type == Q5_K: word |= ((qh_pair[word_idx//4][word_idx%4] >> subgroup.cast(dtypes.uint32)) & 0x01010101) << 4
dot = _amd_dp4a(word, xwords[word_idx], dot)
d, dmin, scale, minimum = _q5_scales(raw, base, subgroup)
return (dot.float()*d*scale - qsum.float()*dmin*minimum) * xd[token, group]
gsum = xs[token, group, 0].load() + xs[token, group, 1].load()
return (dot.float()*d*scale - gsum*dmin*minimum) * xd[token, group]
if ggml_type == IQ4_XS:
base = (output * in_features//GGML_BLOCK_SIZE + block) * IQ4_WORDS
dot = UOp.const(0, dtypes.int32)
@@ -172,45 +218,57 @@ def _quant_decode_kernel(out:UOp, raw:UOp, xq:UOp, xd:UOp, out_features:int, in_
dot = _amd_dp4a(_iq4_bytes(packed, 4*(word_idx//4)), xwords[word_idx], dot)
d, scale = _iq4_scales(raw, base, subgroup)
return dot.float() * xd[token, group] * d * scale
base = (output*in_features//GGML_BLOCK_SIZE+block)*Q6_BYTES
# the packed rows were padded to 212 bytes (53 words) per 256-block in set_quantized: everything is word-aligned
base = (output*in_features//GGML_BLOCK_SIZE+block)*Q6_WORDS
# the subgroup's 8 ql words and 8 qh words are contiguous: two 16-byte vector loads each
lows = tuple(_amd_load(raw[base + (subgroup//4)*16 + (subgroup%2)*8 + half*4], 4) for half in range(2))
highs = tuple(_amd_load(raw[base + 32 + (subgroup//4)*8 + half*4], 4) for half in range(2))
dots = [UOp.const(0, dtypes.int32)] * 2
for word_idx in range(8):
pos, within = subgroup*32 + word_idx*4, (subgroup*32 + word_idx*4)%128
low = _amd_load(raw[base + (pos//128)*64 + within%64], 4) >> ((within//64)*4).cast(dtypes.uint8)
high = _amd_load(raw[base + 128 + (pos//128)*32 + within%32], 4) >> ((within//32)*2).cast(dtypes.uint8)
quant = ((low & 15) | ((high & 3) << 4)).bitcast(dtypes.int8) - 32
word = sum((quant[i].cast(dtypes.uint8).cast(dtypes.uint32) << (i*8) for i in range(4)), UOp.const(0, dtypes.uint32))
within = (subgroup*32 + word_idx*4)%128
low = lows[word_idx//4][word_idx%4] >> ((within//64)*4).cast(dtypes.uint32)
high = highs[word_idx//4][word_idx%4] >> ((within//32)*2).cast(dtypes.uint32)
# 4 values per word: (low nibble) | (2 high bits << 4). values stay positive, so the int8-bitcast/-32 of the
# naive dequant is skipped and the -32 offset is applied later via the per-16 sums of the quantized inputs
word = (low & 0x0f0f0f0f) | ((high & 0x03030303) << 4)
dots[word_idx//4] = _amd_dp4a(word, xwords[word_idx], dots[word_idx//4])
scales = [raw[base + 192 + subgroup*2+i].cast(dtypes.uint8).bitcast(dtypes.int8).float() for i in range(2)]
dbits = raw[base+208].cast(dtypes.uint16) | (raw[base+209].cast(dtypes.uint16) << 8)
return (dots[0].float()*scales[0] + dots[1].float()*scales[1]) * xd[token, group] * _half(dbits)
scales = [((raw[base + 48 + (subgroup*2+i)//4] >> (((subgroup*2+i)%4)*8).cast(dtypes.uint32)) & 255)
.cast(dtypes.uint8).bitcast(dtypes.int8).float() for i in range(2)]
gsum = [xs[token, group, i].load() * 32 for i in range(2)]
return ((dots[0].float() - gsum[0])*scales[0] + (dots[1].float() - gsum[1])*scales[1]) * xd[token, group] * _half(raw[base+52] & 0xffff)
names = {Q4_K: "linear_q4_k", Q5_K: "linear_q5_k", IQ4_XS: "linear_iq4_xs", Q6_K: "linear_q6"}
return _decode_linear(out, out_features, group_count, group_dot, names[ggml_type])
def _wmma_layout(out:UOp, out_features:int, token_tile:int, output_tiles:int):
output_waves = 2 if out_features % (32*output_tiles) == 0 else 1
token_block, output_block = UOp.range(out.shape[0]//token_tile, 0), UOp.range(out_features//(16*output_tiles*output_waves), 1)
lane, wave = UOp.range(WARP_SIZE, 2, axis_type=AxisType.LOCAL), UOp.range(output_waves, 3, axis_type=AxisType.LOCAL)
hw_lane = UOp(Ops.CUSTOM, src=(lane.int(),), arg=("__builtin_amdgcn_mbcnt_lo(-1, 0)", dtypes.int32)).cast(dtypes.weakint)
col, half = hw_lane % 16, hw_lane // 16
# lane is a hardware WARP range (like the flash kernel): the fragment math stays visible without being
# range-split into nested loops, which would scramble the WMMA fragment layout
lane, wave = UOp.range(WARP_SIZE, -1, axis_type=AxisType.WARP), UOp.range(output_waves, 3, axis_type=AxisType.LOCAL)
col, half = lane % 16, lane // 16
outputs = tuple((output_block*output_waves+wave)*(16*output_tiles) + tile*16 + col for tile in range(output_tiles))
inputs = tuple(token_block*token_tile + tile*16 + col for tile in range(token_tile//16))
tokens = tuple(tuple(token_block*token_tile + tile*16 + half*8 + i for i in range(8)) for tile in range(token_tile//16))
return output_waves, token_block, output_block, lane, wave, half, outputs, inputs, tokens
def _wmma_stores(out, outputs, tokens, accs, update, half):
def values(acc:UOp) -> tuple[UOp, ...]:
vals = tuple(acc.after(update)[i].load() for i in range(8))
swapped = tuple(UOp(Ops.CUSTOM, src=(value,),
arg=("__builtin_bit_cast(float, __builtin_amdgcn_ds_swizzle(__builtin_bit_cast(int, {0}), 50688))", dtypes.float32)) for value in vals)
def _wmma_stores(out, outputs, tokens, accs, update, half, lane, wave, output_waves):
# the accumulator fragment halves are exchanged between lane pairs (l, l^16) through LDS (a ds_swizzle without CUSTOM)
flat_accs = [acc for output_accs in accs for acc in output_accs]
lds = UOp.placeholder((output_waves, 32, len(flat_accs)*8), dtypes.float32, slot=33, addrspace=AddrSpace.LOCAL)
stores = [lds[wave, lane, a*8+i].store(acc.after(update)[i].load()) for a,acc in enumerate(flat_accs) for i in range(8)]
lds = lds.after(UOp.barrier(UOp.group(*stores)))
def values(ai:int) -> tuple[UOp, ...]:
own = tuple(lds[wave, lane, ai*8+i].load() for i in range(8))
peer = tuple(lds[wave, lane ^ 16, ai*8+i].load() for i in range(8))
low = half.eq(0)
return tuple(low.where(vals[i], swapped[i+4]) if j == 0 else low.where(swapped[i], vals[i+4]) for i in range(4) for j in range(2))
return [out[token, output].store(value) for output,output_accs in zip(outputs, accs)
for tile_tokens,acc in zip(tokens, output_accs) for token,value in zip(tile_tokens, values(acc))]
return tuple(low.where(own[i], peer[i+4]) if j == 0 else low.where(peer[i], own[i+4]) for i in range(4) for j in range(2))
tt = len(tokens)
return [out[token, output].store(value) for ot,(output,output_accs) in enumerate(zip(outputs, accs))
for tile,(tile_tokens,_acc) in enumerate(zip(tokens, output_accs)) for token,value in zip(tile_tokens, values(ot*tt+tile))]
def _quant_linear_wmma(out, x, out_features, in_features, type_words, layout, dequant, name):
x = x.reshape(out.shape[0], in_features)
_, token_block, output_block, lane, wave, physical_half, outputs, input_tokens, tokens = layout
output_waves, token_block, output_block, lane, wave, physical_half, outputs, input_tokens, tokens = layout
token_tile, output_tiles = len(tokens)*16, len(outputs)
output_words = in_features // GGML_BLOCK_SIZE * type_words
accs = tuple(tuple(UOp.placeholder((8,), dtypes.float32, slot=ot*(token_tile//16)+tile, addrspace=AddrSpace.REG)
@@ -229,8 +287,8 @@ def _quant_linear_wmma(out, x, out_features, in_features, type_words, layout, de
wmma_accs[output_tile][tile] = UOp.wmma(afrag, bfrag, previous, *WMMA_ARG)
update = UOp.group(*(acc.store(value) for output_accs,output_values in zip(accs, wmma_accs)
for acc,value in zip(output_accs, output_values))).end(group)
return UOp.group(*_wmma_stores(out, outputs, tokens, accs, update, physical_half)).end(token_block, output_block, lane, wave).sink(
arg=KernelInfo(name=name, opts_to_apply=()))
stores = _wmma_stores(out, outputs, tokens, accs, update, physical_half, lane, wave, output_waves)
return UOp.group(*stores).end(token_block, output_block, lane, wave).sink(arg=KernelInfo(name=name, opts_to_apply=()))
@functools.cache
def _q5_linear_f16_wmma_kernel(out:UOp, raw:UOp, x:UOp, out_features:int, in_features:int, ggml_type:int) -> UOp:
@@ -264,9 +322,10 @@ def _iq4_linear_f16_wmma_kernel(out:UOp, raw:UOp, x:UOp, lut:UOp, out_features:i
pairs = tuple(lut[((raw[base + 2 + subgroup*4 + word] >> (byte*8)) & 255).cast(dtypes.weakint)]
for word in range(4) for byte in range(4))
return tuple((_half((pair >> (half*16)) & 0xffff)*scale).cast(dtypes.float16) for pair in pairs)
def nibble(packed:UOp, index:int): return (packed >> (8*index+4*half)) & 15
lut_pairs = (lut[(nibble(packed, i) | nibble(packed, i+1)<<4).cast(dtypes.weakint)]
for packed in (raw[base+2+subgroup*4+i] for i in range(4)) for i in (0, 2))
# a subgroup-half gathers the lo (half=0) or hi (half=1) nibbles of byte pairs of each packed word
lut_pairs = (lut[(((raw[base+2+subgroup*4+i] >> (8*j+4*half)) & 15) |
(((raw[base+2+subgroup*4+i] >> (8*j+8+4*half)) & 15) << 4)).cast(dtypes.weakint)]
for i in range(4) for j in (0, 2))
return tuple((_half((pair >> (i*16)) & 0xffff)*scale).cast(dtypes.float16) for pair in lut_pairs for i in range(2))
return _quant_linear_wmma(out, x, out_features, in_features, IQ4_WORDS, layout, dequant, "linear_iq4_xs_f16_wmma")
@@ -287,67 +346,163 @@ def q8_linear(layer:Linear, x:Tensor) -> Tensor:
fxn = _iq4_linear_f16_wmma_kernel if layer.ggml_type == IQ4_XS else functools.partial(_q5_linear_f16_wmma_kernel, ggml_type=layer.ggml_type)
extra = (iq4_half_lut(str(x.device)).uop,) if layer.ggml_type == IQ4_XS else ()
return run(fxn, out, raw, x.cast(dtypes.float16).contiguous().uop, *extra)
xq, xd = q8_quantize(x, tokens, in_features)
xq_, xd, xs = q8_quantize(x, tokens, in_features)
decode = functools.partial(_quant_decode_kernel, ggml_type=layer.ggml_type)
out = Tensor.empty(tokens, out_features, (in_features+1023)//1024, dtype=dtypes.float32, device=x.device).uop
return run(decode, out, raw, xq.uop, xd.uop)
return run(decode, out, raw, xq_.uop, xd.uop, xs.uop)
# ******** tiny dense fp16 gemv ********
@functools.cache
def _amd_f16_gemv_kernel(out:UOp, w:UOp, x:UOp, *rest:UOp, in_features:int, out_features:int, tokens:int) -> UOp:
bias: UOp|None = rest[0] if rest else None
# one block per (token, output row), 32 lanes accumulate 4-wide chunks of the row
lanes, val_chunk = WARP_SIZE, 4
token, out_row = UOp.range(tokens, 0, AxisType.GLOBAL), UOp.range(out_features, 1, AxisType.GLOBAL)
lane = UOp.range(lanes, 2, axis_type=AxisType.LOCAL)
per = in_features // (lanes * val_chunk)
assert per * lanes * val_chunk == in_features
w = w.reshape((out_features, per, lanes*val_chunk))
x = x.reshape((tokens, per, lanes*val_chunk))
acc = UOp.const(0, dtypes.float32)
for i in range(per):
for j in range(val_chunk):
acc = acc + w[out_row, i, lane*val_chunk + j].load().float() * x[token, i, lane*val_chunk + j].load().float()
total = warp_reduce(acc, full_wave=True)
if bias is not None: total = total + bias[token, out_row].load().float()
return out[token, out_row.valid(lane.eq(0))].store(total).end(token, out_row, lane).sink(arg=KernelInfo(name="linear_f16_gemv", opts_to_apply=()))
def _view_back(t:Tensor) -> Tensor:
"""strip top-of-chain CAST(s) from a lazy weight: reading the raw file bytes in the kernel instead of
materializing the cast into a fresh buffer every step"""
uop = t.uop
while uop.op is Ops.CAST: uop = uop.src[0]
return Tensor(uop).reshape(t.shape)
def f16_gemv(layer:Linear, x:Tensor) -> Tensor:
tokens = prod(x.shape[:-1])
assert isinstance(tokens, int)
weight = _view_back(layer.weight)
x = x.contiguous() if x.dtype == dtypes.half else x.cast(dtypes.half).contiguous()
out = Tensor.empty(tokens, layer.out_features, dtype=dtypes.float32, device=x.device)
fxn = functools.partial(_amd_f16_gemv_kernel, in_features=layer.in_features, out_features=layer.out_features, tokens=tokens)
srcs = (out, weight.reshape(-1), x.reshape(tokens, layer.in_features)) + (() if layer.bias is None else (_view_back(layer.bias),))
return Tensor.custom_kernel(*srcs, fxn=fxn)[0].reshape(*x.shape[:-1], layer.out_features)
# ******** flash attention on the KV cache ********
def _vec_load(ptr:UOp, lanes:int) -> tuple[UOp, ...]:
if lanes == 1: return (ptr.load().float(),)
vec = _amd_load(ptr, lanes)
return tuple(vec[i].float() for i in range(lanes))
@functools.cache
def _amd_flash_attention_decode_partial(out, stats, q, cache_kv, valid_kv_len, max_kv_len, block_n):
def _amd_flash_attention_decode_partial(out, stats, q, cache_kv, valid_kv_len, max_kv_len, block_n, waves=4):
valid_kv_len = _unbind(valid_kv_len)
_, B, H_KV, N, D = cast(tuple[int, int, int, int, int], cache_kv.shape)
_, H, M, _ = cast(tuple[int, int, int, int], q.shape)
assert M == 1 and H % H_KV == 0 and D % WARP_SIZE == 0 and max_kv_len <= N and max_kv_len % block_n == 0
G, CHUNK, DV, heads_per_wave = H // H_KV, block_n, D // WARP_SIZE, 2
head_tile = min(DECODE_HEAD_TILE, G) # share each KV stream across two GQA heads per wave
assert G % head_tile == 0 and head_tile % heads_per_wave == 0
decode_waves, decode_group = head_tile // heads_per_wave, 4
block_bhkv = UOp.range(B*H_KV*(G//head_tile), 0, AxisType.GLOBAL)
valid_chunks = (valid_kv_len+CHUNK-1)//CHUNK
group_count = min(valid_chunks, out.shape[2]) if isinstance(valid_chunks, int) else valid_chunks.minimum(out.shape[2])
block_n, lane = UOp.range(group_count, 1, AxisType.GLOBAL), UOp.range(WARP_SIZE, 2, axis_type=AxisType.LOCAL)
wave = UOp.range(decode_waves, 3, axis_type=AxisType.LOCAL)
head_group, bhkv = block_bhkv % (G//head_tile), block_bhkv // (G//head_tile)
b, kv_head = bhkv // H_KV, bhkv % H_KV
dims = tuple(lane + i*WARP_SIZE for i in range(DV))
acc, row_max, row_sum = _reg((heads_per_wave, DV), 0, 0), _reg((heads_per_wave,), 1, -math.inf), _reg((heads_per_wave,), 2, 0)
groups_per_chunk, offset = CHUNK // decode_group, UOp.range(((valid_chunks+group_count-1)//group_count)*(CHUNK//decode_group), 100, AxisType.REDUCE)
chunk = block_n + (offset // groups_per_chunk) * group_count
keys = tuple(chunk*CHUNK + (offset % groups_per_chunk)*decode_group + i for i in range(decode_group))
valid = tuple(key < valid_kv_len for key in keys)
kvals, vvals = (tuple(tuple(is_valid.where(cache_kv[kv, b, kv_head, key, d].float(), UOp.const(0, dtypes.float)) for d in dims)
for key,is_valid in zip(keys, valid)) for kv in range(2))
q_heads = tuple(kv_head*G + head_group*head_tile + wave*heads_per_wave + head for head in range(heads_per_wave))
updates:list[UOp] = []
for head,q_head in enumerate(q_heads):
scores = tuple(warp_reduce(sum((q[b, q_head, 0, d].float()*k for d,k in zip(dims, key_kvals)),
UOp.const(0, dtypes.float)), full_wave=True) / math.sqrt(D) for key_kvals in kvals)
prev_acc, prev_max, prev_sum = acc.after(offset)[head], row_max.after(offset)[head], row_sum.after(offset)[head]
new_max = functools.reduce(lambda a,vs:a.maximum(vs[0].where(vs[1], UOp.const(-math.inf, dtypes.float))), zip(valid, scores), prev_max)
alpha = ((prev_max-new_max)*LOG2E).exp2()
betas = tuple(is_valid.where(((score-new_max)*LOG2E).exp2(), UOp.const(0, dtypes.float)) for is_valid,score in zip(valid, scores))
updates += [acc[head].store(prev_acc*alpha + sum((UOp.stack(*value)*beta for value,beta in zip(vvals, betas)), acc[head].const_like(0))),
row_sum[head].store(prev_sum*alpha + sum(betas, UOp.const(0, dtypes.float))), row_max[head].store(new_max)]
update = UOp.group(*updates).end(offset)
acc, row_max, row_sum = acc.after(update), row_max.after(update), row_sum.after(update)
stores = [out[b, q_head, block_n, d].store(acc[head, i]) for head,q_head in enumerate(q_heads) for i,d in enumerate(dims)] + \
[stats[b, q_head.valid(lane.eq(0)), block_n, i].store(x[head]) for head,q_head in enumerate(q_heads) for i,x in enumerate((row_max, row_sum))]
return UOp.group(*stores).end(lane, wave, block_n, block_bhkv).sink(arg=KernelInfo(name="flash_decode_partial", opts_to_apply=()))
G, CHUNK, DPL, WAVES = H // H_KV, block_n, D // WARP_SIZE, waves
assert CHUNK % WAVES == 0
SEC = CHUNK // WAVES # keys each wave scans independently
live_chunks = (valid_kv_len+CHUNK-1)//CHUNK
live_chunks = min(live_chunks, out.shape[2]) if isinstance(live_chunks, int) else live_chunks.minimum(out.shape[2])
block_bhkv, block_chunk = UOp.range(B*H_KV, 0, AxisType.GLOBAL), UOp.range(live_chunks, 1, AxisType.GLOBAL)
lane, wave = UOp.range(WARP_SIZE, 2, axis_type=AxisType.LOCAL), UOp.range(WAVES, 3, axis_type=AxisType.LOCAL)
b, kv_head = block_bhkv // H_KV, block_bhkv % H_KV
# per-lane query fragments for every GQA head, kept packed in registers; unpacked at use
qf = tuple(_vec_load(q[b, kv_head*G+h, 0, lane*DPL], DPL) for h in range(G))
zerof = UOp.const(0, dtypes.float)
valids: list[UOp] = []
scores: list[list[UOp]] = [[zerof]*G for _ in range(SEC)]
vfrags: list[tuple[UOp, ...]] = [()]*SEC
for j in range(SEC):
key = block_chunk*CHUNK + wave*SEC + j
valid = key < valid_kv_len
valids.append(valid)
kfrag = _vec_load(cache_kv[0, b, kv_head, key, lane*DPL], DPL)
# V is prefetched in the score pass so both streams are in flight together
vfrags[j] = _vec_load(cache_kv[1, b, kv_head, key, lane*DPL], DPL)
for h in range(G):
s = warp_reduce(sum((qf[h][i]*kfrag[i] for i in range(DPL)), UOp.const(0, dtypes.float)), full_wave=True) * (1/math.sqrt(D))
scores[j][h] = valid.where(s, UOp.const(-math.inf, dtypes.float))
ninf = UOp.const(-math.inf, dtypes.float)
row_max = [functools.reduce(UOp.maximum, (scores[j][h] for j in range(SEC)), ninf) for h in range(G)]
accs:list[list[UOp]] = [[UOp.const(0, dtypes.float)] * DPL for _ in range(G)]
row_sums:list[UOp] = [UOp.const(0, dtypes.float) for _ in range(G)]
for j in range(SEC):
for h in range(G):
beta = valids[j].where(((scores[j][h]-row_max[h])*LOG2E).exp2(), UOp.const(0, dtypes.float))
accs[h] = [a + beta*v for a, v in zip(accs[h], vfrags[j])]
row_sums[h] = row_sums[h] + beta
# exchange across the block's waves through LDS (fp16 halves LDS so more blocks fit per CU)
acc_lds = UOp.placeholder((WAVES, G, D), dtypes.half, slot=0, addrspace=AddrSpace.LOCAL)
ml_lds = UOp.placeholder((WAVES, G, 2), dtypes.float, slot=1, addrspace=AddrSpace.LOCAL)
lds_acc = acc_lds.reshape(WAVES, G, WARP_SIZE, DPL)
stores = [lds_acc[wave, h, lane].store(UOp.stack(*accs[h]).cast(dtypes.half)) for h in range(G)]
# NOTE: duplicate stores of the same value from every lane are harmless here
stores += [ml_lds[wave, h, i].store(x) for h in range(G) for i, x in enumerate((row_max[h], row_sums[h]))]
barrier = UOp.barrier(UOp.group(*stores))
acc_lds, ml_lds = acc_lds.after(barrier), ml_lds.after(barrier)
tid = wave*WARP_SIZE + lane
final_stores:list[UOp] = []
for i in range(-(-G*D//(WAVES*WARP_SIZE))):
flat = tid + i*WAVES*WARP_SIZE
h, d = flat // D, flat % D
M = functools.reduce(UOp.maximum, (ml_lds[w, h, 0].load() for w in range(WAVES)), ninf)
val = sum((((ml_lds[w, h, 0].load()-M)*LOG2E).exp2() * acc_lds[w, h, d].load().float() for w in range(WAVES)), UOp.const(0, dtypes.float))
oidx = out[b, kv_head*G + h, block_chunk, d]
if G*D % (WAVES*WARP_SIZE): oidx = out[b, (kv_head*G + h).valid(flat < G*D), block_chunk, d]
final_stores.append(oidx.store(val))
hstat = tid
M = functools.reduce(UOp.maximum, (ml_lds[w, hstat, 0].load() for w in range(WAVES)), ninf)
L = sum((((ml_lds[w, hstat, 0].load()-M)*LOG2E).exp2() * ml_lds[w, hstat, 1].load() for w in range(WAVES)), UOp.const(0, dtypes.float))
q_head = (kv_head*G + hstat).valid(hstat < G) if WAVES*WARP_SIZE > G else kv_head*G + hstat
final_stores += [stats[b, q_head, block_chunk, 0].store(M), stats[b, q_head, block_chunk, 1].store(L)]
return UOp.group(*final_stores).end(lane, wave, block_chunk, block_bhkv).sink(arg=KernelInfo(name="flash_decode_partial", opts_to_apply=()))
@functools.cache
def _amd_flash_decode_combine(o:UOp, partial:UOp, stats:UOp, live:int|UOp) -> UOp:
# one wave per (batch, head, 64-dim tile): every lane redundantly weights its chunks; no cross-lane traffic
live = _unbind(live)
B, H, C, D = cast(tuple[int, int, int, int], partial.shape)
DT = 64 if D % 64 == 0 else WARP_SIZE # dims per block
assert D % DT == 0
block_bh, block_dt = UOp.range(B*H, 0, AxisType.GLOBAL), UOp.range(D//DT, 1, AxisType.GLOBAL)
lane = UOp.range(WARP_SIZE, 2, axis_type=AxisType.LOCAL)
b, h = block_bh // H, block_bh % H
NPD = DT // WARP_SIZE # output dims per lane
dims = tuple(block_dt*DT + lane*NPD + i for i in range(NPD))
chunk = UOp.range(live, 100, AxisType.REDUCE)
def iloop(ph, val): return ph.store(ph.const_like(val))
chunk_max = UOp.placeholder((1,), dtypes.float, slot=0, addrspace=AddrSpace.REG)
chunk_max_i = chunk_max.after(iloop(chunk_max, -math.inf))
update0 = chunk_max_i.store(chunk_max_i.after(chunk).maximum(stats[b, h, chunk, 0].load())).end(chunk)
chunk_max = chunk_max_i.after(update0)
chunk2 = UOp.range(live, 101, AxisType.REDUCE)
acc = UOp.placeholder((NPD,), dtypes.float, slot=1, addrspace=AddrSpace.REG)
weight_sum = UOp.placeholder((1,), dtypes.float, slot=2, addrspace=AddrSpace.REG)
acc_i, weight_sum_i = acc.after(iloop(acc, 0)), weight_sum.after(iloop(weight_sum, 0))
w = ((stats[b, h, chunk2, 0].load()-chunk_max)*LOG2E).exp2()
update1 = UOp.group(*[acc_i[i].store(acc_i.after(chunk2)[i].load() + w*partial[b, h, chunk2, d].load()) for i, d in enumerate(dims)],
weight_sum_i[0].store(weight_sum_i.after(chunk2)[0].load() + w*stats[b, h, chunk2, 1].load())).end(chunk2)
acc, weight_sum = acc_i.after(update1), weight_sum_i.after(update1)
inv = 1 / weight_sum[0].load()
return UOp.group(*[o[b, h, 0, d].store(acc[i].load() * inv) for i, d in enumerate(dims)]) \
.end(lane, block_dt, block_bh).sink(arg=KernelInfo(name="flash_decode_combine", opts_to_apply=()))
def amd_flash_attention_decode(q:Tensor, cache_kv:Tensor, valid_kv_len:int|UOp, max_kv_len:int) -> Tensor:
B, H, D = cache_kv.shape[1], q.shape[1], cache_kv.shape[4]
chunks = min(64, max_kv_len // 128)
chunks = min(256, max_kv_len // 64)
partial = Tensor.empty(B, H, chunks, D, dtype="float32", device=q.device)
stats = Tensor.empty(B, H, chunks, 2, dtype="float32", device=q.device)
fxn = functools.partial(_amd_flash_attention_decode_partial, valid_kv_len=valid_kv_len, max_kv_len=max_kv_len, block_n=128)
fxn = functools.partial(_amd_flash_attention_decode_partial, valid_kv_len=valid_kv_len, max_kv_len=max_kv_len, block_n=64, waves=16)
partial, stats = Tensor.custom_kernel(partial, stats, q, cache_kv, fxn=fxn)[:2]
live = (valid_kv_len+127)//128
live = (valid_kv_len+63)//64
live = min(live, chunks) if isinstance(live, int) else live.minimum(chunks)
partial, stats = partial[:, :, :live], stats[:, :, :live]
weights = ((stats[..., 0]-stats[..., 0].max(2, keepdim=True))*LOG2E).exp2()
return ((partial*weights.unsqueeze(-1)).sum(2) / (stats[..., 1]*weights).sum(2, keepdim=True)).unsqueeze(2)
out = Tensor.empty(B, H, 1, D, dtype="float32", device=q.device)
fxn = functools.partial(_amd_flash_decode_combine, live=live)
return Tensor.custom_kernel(out, partial, stats, fxn=fxn)[0]
@functools.cache
def _amd_flash_attention(o:UOp, q:UOp, cache:UOp, valid_kv_len:int|UOp, q_start:int|UOp|None=None) -> UOp:
+8 -3
View File
@@ -188,10 +188,12 @@ class CStyleLanguage(Renderer):
return prefix + self.type_map.get(dtype, dtype.name) + suffix
def render_type(self, u:UOp): return self._render_dtype(u.dtype, u.max_numel(), u.addrspace, shape=u._shape)
def render_access(self, u:UOp):
def render_ptr(self, u:UOp):
# the address of an access, vector-cast if the access reads/writes more lanes than the pointer's scalar type
if u.max_numel() > 1 or u.dtype != u.src[0].dtype:
return f"*(({self._render_dtype(u.dtype, u.max_numel(), u.addrspace, override_ptr=True, shape=u._shape)})({self[u]}))"
else: return f"*{self[u]}"
return f"(({self._render_dtype(u.dtype, u.max_numel(), u.addrspace, override_ptr=True, shape=u._shape)})({self[u]}))"
else: return f"{self[u]}"
def render_access(self, u:UOp): return f"*{self.render_ptr(u)}"
def render_cast(self, u:UOp, val:str) -> str: return f"({self.render_type(u)})({val})"
# LEGACY
@@ -509,6 +511,9 @@ class HIPRenderer(CStyleLanguage):
(UPat(Ops.CAST, dtypes.float, (UPat.var("y", dtypes.fp8s),), name="x",),
lambda ctx,x,y: f"__builtin_amdgcn_cvt_f32_{('fp8', 'bf8')[fp8_index(y.dtype)]}((unsigned int){ctx[x.src[0]]}, 0)"),
]) + base_rewrite
# a LOAD flagged nontemporal renders as the cache-bypassing builtin (only used on global loads)
self.string_rewrite = PatternMatcher([(UPat(Ops.LOAD, arg="nontemporal", src=(UPat.var("bidx"),)),
lambda ctx,bidx: f"__builtin_nontemporal_load({ctx.render_ptr(bidx)})")]) + self.string_rewrite
# https://clang.llvm.org/docs/AttributeReference.html#amdgpu-flat-work-group-size
# NOTE: this makes hlb_cifar10 twice as fast, there may be more gains in tweaking these parameters
+23 -29
View File
@@ -2,10 +2,10 @@ from typing import Iterator
import functools, itertools
from dataclasses import dataclass, field, replace
from tinygrad.dtype import dtypes, AddrSpace
from tinygrad.uop.ops import PatternMatcher, UPat, Ops, UOp, resolve, GroupOp, graph_rewrite, sint, AxisType, rewrite_group
from tinygrad.uop.ops import PatternMatcher, UPat, Ops, UOp, resolve, GroupOp, graph_rewrite, sint, AxisType, rewrite_group, broadcast_axes
from tinygrad.uop.ops import gate_kernel_sink
from tinygrad.uop.symbolic import symbolic, pm_simplify_valid, pm_drop_and_clauses
from tinygrad.helpers import argsort, all_same, cpu_profile, colored, Context, SPEC, prod
from tinygrad.helpers import argsort, all_same, cpu_profile, colored, Context, SPEC
@dataclass
class IndexingContext:
@@ -60,6 +60,11 @@ class BufferizeOpts:
addrspace: AddrSpace = AddrSpace.GLOBAL
removable: bool = True
def broadcast_rngs(x:UOp, src:UOp, rngs:tuple[UOp, ...]) -> tuple[UOp, ...]:
if x.op not in GroupOp.Broadcastable: return rngs
baxes, nleft = broadcast_axes(src.shape, x.shape), len(x.shape)-len(src.shape)
return tuple(r.const_like(0) if j in baxes else r for j,r in enumerate(rngs) if j >= nleft)
# TODO: srcs contain (real data srcs, something else, ranges) and the boundary is confusing. see range_start
def data_srcs(op:Ops, src:tuple[UOp, ...]) -> tuple[UOp, ...]:
if op in {Ops.PARAM, Ops.BUFFER, Ops.RANGE, Ops.SPECIAL}: return ()
@@ -68,17 +73,13 @@ def data_srcs(op:Ops, src:tuple[UOp, ...]) -> tuple[UOp, ...]:
if op in GroupOp.Movement|{Ops.INDEX, Ops.STAGE, Ops.REDUCE, Ops.AFTER, Ops.END}: return src[:1]
return src
def truncate_src_rngs(rngs:tuple[UOp, ...], s:UOp) -> tuple[UOp, ...]:
# smaller rank srcs (like bare scalar CONSTs) don't iterate the leading ranges
return rngs[len(rngs)-len(s_shape):] if (s_shape:=s._shape) is not None else rngs
def create_bufferize_and_index_srcs(ctx:IndexingContext, x:UOp) -> list[UOp]:
new_srcs = []
# shape/bound/index args that are not data src should not be indexed
data_src_count = len(data_srcs(x.op, x.src))
for i, s in enumerate(x.src):
new_src = s
src_rngs = truncate_src_rngs(ctx.range_map[x][0], s) if x in ctx.range_map else ()
src_rngs = broadcast_rngs(x, s, ctx.range_map[x][0]) if x in ctx.range_map else ()
if s.op in {Ops.PARAM, Ops.BUFFER, Ops.MSTACK, Ops.MSELECT, Ops.AFTER}:
if x in ctx.range_map and i < data_src_count: new_src = new_src.index(*src_rngs)
elif s in ctx.realize_map:
@@ -201,9 +202,6 @@ def run_rangeify(tsink:UOp, debug:bool=False) -> UOp:
# explicit rangeify
ending_ranges: dict[UOp, list[UOp]] = {}
# ranges ended by an EXPAND don't fire at the first elementwise op below it: that eltwise op is a single-consumer
# wrapper, realizing there materializes the wrapper instead of the shared value below it. movement ops forward the deferral.
deferred_ending: dict[UOp, list[UOp]] = {}
for x in reversed(tsink_toposort):
# no ranges on kernels, they are internal
if x.op in {Ops.CALL, Ops.FUNCTION, Ops.LINEAR}: continue
@@ -215,13 +213,19 @@ def run_rangeify(tsink:UOp, debug:bool=False) -> UOp:
if x.op in {Ops.MSTACK, Ops.MSELECT}: continue
ending_ranges[x] = sum([ending_ranges.get(u, []) for u in consumer_map[x]], [])
# ranges the consumers iterate that this node broadcasts over
ended = [rctx.range_map[c][0][i] for c in consumer_map[x] if c in rctx.range_map and c.op in GroupOp.Broadcastable
for i in broadcast_axes(x.shape, c.shape)]
broadcast_ending_ranges = list(UOp.sink(*ended).ranges)
# fusion decision: REDUCE before the broadcast
if x.op is Ops.REDUCE: ending_ranges[x] += broadcast_ending_ranges
# *** the ranges on the output are
# 1. new if this op is realized
# 2. from the single consumer if this op only has one consumer
# 3. potentially new if this op has 2+ consumers
consumer_rngs = [truncate_src_rngs(rctx.range_map[c][0], x) for c in consumer_map[x] if c in rctx.range_map]
consumer_rngs = [broadcast_rngs(c, x, rctx.range_map[c][0]) for c in consumer_map[x] if c in rctx.range_map]
if x in rctx.realize_map:
# if this is in the realize_map, we create new ranges (at the output)
out_rngs = tuple(rctx.new_range(s) for s in x.shape)
@@ -261,23 +265,15 @@ def run_rangeify(tsink:UOp, debug:bool=False) -> UOp:
# we have to (partially) realize here if there's new ranges
if len(_realize_axis): rctx.realize_map[x] = _realize_axis
defer = set()
if x in deferred_ending:
if x.op in GroupOp.Movement: deferred_ending.setdefault(x.src[0], []).extend(deferred_ending[x])
elif x.op in GroupOp.Elementwise and len(consumer_map[x]) == 1 and resolve(prod(x.shape) == 1):
# scalar single-consumer wrappers below the EXPAND chain (like broadcasting (x * -1)) can't materialize
# anything useful: defer the ended ranges to the first node below that can (the shared value anchor)
defer = set(deferred_ending[x])
# if this element is a reduce and there's ended ranges, we might have to end some other ranges
if len(ending_ranges[x]) and x.op in GroupOp.Elementwise.union({Ops.REDUCE}):
firing = set(ending_ranges[x]) - defer
if len(firing):
_realize_axis = list(range(len(out_rngs)))
ending_ranges[x] = [r for r in ending_ranges[x] if r in defer]
if len(_realize_axis):
rctx.realize_map[x] = _realize_axis
out_rngs = tuple(rctx.new_range(x.shape[i]) for i in range(len(out_rngs)))
_realize_axis = list(range(len(out_rngs)))
ending_ranges[x] = []
if len(_realize_axis):
rctx.realize_map[x] = _realize_axis
out_rngs = tuple(rctx.new_range(x.shape[i]) for i in range(len(out_rngs)))
ending_ranges[x] += broadcast_ending_ranges
# TODO: some ops don't have shape, enable this after the `.st` property is removed
#assert len(out_rngs) == len(x.shape), \
# f"shape len mismatch {len(out_rngs)} != {len(x.shape)} on {x.op} with {len(consumer_map[x])} consumers and realize {x in realize_map}"
@@ -296,9 +292,7 @@ def run_rangeify(tsink:UOp, debug:bool=False) -> UOp:
# if the EXPAND is used to inject a range, we don't mark it as ending_ranges. otherwise we do.
# NOTE: this doesn't actually always end a range, but this is why convs are realized, so for now we need it
if x.op is Ops.EXPAND and all(isinstance(y, int) or y.op is not Ops.RANGE for y in x.shape):
ended_here = list(UOp.sink(*out_rngs[:len(x.marg)]).ranges.keys())
ending_ranges[x] += ended_here
deferred_ending.setdefault(x.src[0], []).extend(ended_here)
ending_ranges[x] += list(UOp.sink(*out_rngs[:len(x.marg)]).ranges.keys())
# REDUCE creates ranges for the axes it is reducing
if x.op is Ops.REDUCE and x.arg[1]:
+2 -17
View File
@@ -1,9 +1,9 @@
import itertools
from tinygrad.dtype import dtypes, to_dtype
from tinygrad.uop.ops import PatternMatcher, UPat, Ops, UOp, resolve, GroupOp
from tinygrad.uop.ops import graph_rewrite, rewrite_group, shape_to_shape_arg, ParamArg, identity_element, _broadcast_shape
from tinygrad.uop.ops import graph_rewrite, rewrite_group, shape_to_shape_arg, ParamArg, identity_element
from tinygrad.uop.movement import mop_cleanup
from tinygrad.helpers import prod, getenv, all_int, DEBUG, SPLIT_REDUCEOP, OPENPILOT_HACKS, FLOAT16, argsort, all_same
from tinygrad.helpers import prod, getenv, all_int, DEBUG, SPLIT_REDUCEOP, OPENPILOT_HACKS, FLOAT16, argsort
from tinygrad.schedule.indexing import apply_movement_op
from tinygrad.schedule.allreduce import create_allreduce_function
from tinygrad.schedule.multi import multi_pm
@@ -118,15 +118,6 @@ def expand_bitcast(bc:UOp) -> UOp|None:
parts = [tmp>>8*i*ns for i in range(os//ns)]
return parts[0].stack(*parts[1:], dim=-1).flatten(-2).cast(new_uint).bitcast(bc.dtype)
def expand_broadcast(x:UOp):
shapes = [u._shape for u in x.src]
if any(s is None for s in shapes) or all_same(shapes): return None
shape = _broadcast_shape(*shapes)
# don't expand CONSTs (bare or casted): scalar consts pass through rangeify as-is,
# and EXPAND of an Invalid const must stay a bare scalar
def expanded(u:UOp): return u if u.op is Ops.CONST or (u.op is Ops.CAST and u.src[0].op is Ops.CONST) else u.expand(shape)
return x.replace(src=tuple([expanded(u) for u in x.src]))
earliest_rewrites = mop_cleanup+PatternMatcher([
# resolve FUNCTION calls (inline the body)
(UPat(Ops.FUNCTION, name="c"), resolve_function),
@@ -178,13 +169,7 @@ earliest_rewrites = mop_cleanup+PatternMatcher([
(UPat(Ops.STORE, src=(UPat(Ops.BITCAST, src=(UPat(name="target"),)), UPat(name="src"))),
lambda target, src: target.store(src.bitcast(target.dtype))),
# expand bitcasts and broadcasts
(UPat(Ops.BITCAST, name="bc"), expand_bitcast),
(UPat(GroupOp.Binary|GroupOp.Ternary|{Ops.STORE}, name="x"), expand_broadcast),
# move RESHAPEs through MSELECT/MSTACK
(UPat((Ops.MSELECT, Ops.MSTACK), src=UPat(Ops.RESHAPE), name="m"),
lambda m: m.replace(src=tuple([x.src[0].base for x in m.src])).reshape(m.shape)),
# ** size 0 **
+2 -1
View File
@@ -325,7 +325,8 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
@functools.cached_property
def tuplize(self:UOp) -> tuple:
return (self.op.value, self.arg, self.dtype,)+tuple([x.tuplize for x in self.src])
# arg goes through repr: args of different types (None, str, tuple) must stay mutually comparable for the sort
return (self.op.value, repr(self.arg), self.dtype,)+tuple([x.tuplize for x in self.src])
# *** uop shape stuff ***