mirror of
https://github.com/tinygrad/tinygrad.git
synced 2026-08-16 02:38:26 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f7226da30 | ||
|
|
ab3a217c0b |
@@ -137,7 +137,6 @@ runs:
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true')
|
||||
shell: bash
|
||||
run: |
|
||||
sudo mkdir -p /var/cache/apt/archives
|
||||
sudo chown -R $USER:$USER /var/cache/apt/archives
|
||||
|
||||
echo 'Acquire::GzipIndexes "true";' | sudo tee /etc/apt/apt.conf.d/gzip
|
||||
@@ -215,7 +214,6 @@ runs:
|
||||
sudo apt-get -y --allow-unauthenticated --no-install-recommends install ${{ steps.apt-pkgs.outputs.pkgs }}
|
||||
fi
|
||||
|
||||
sudo mkdir -p /var/cache/apt/archives
|
||||
sudo chown -R $USER:$USER /var/cache/apt/archives/
|
||||
|
||||
- name: Add clang to PATH (Linux)
|
||||
|
||||
@@ -117,10 +117,10 @@ jobs:
|
||||
run: python3 test/external/process_replay/reset.py
|
||||
- name: Run llama3.2
|
||||
run: BENCHMARK_LOG=llama32_3b-f16 JITBEAM=2 IGNORE_BEAM_CACHE=1 python3 -m tinygrad.llm -m llama3.2:3b-f16 --benchmark --warmup
|
||||
- name: Run qwen3.6
|
||||
# qwen3.6:35b-a3b doesn't fit on mac
|
||||
- name: Run qwen3.5
|
||||
# qwen3.5:35b-a3b doesn't fit on mac
|
||||
if: ${{ matrix.dev != 'METAL' }}
|
||||
run: BENCHMARK_LOG=qwen36_35b-a3b JITBEAM=2 IGNORE_BEAM_CACHE=1 python3 -m tinygrad.llm -m qwen3.6:35b-a3b --benchmark --warmup
|
||||
run: BENCHMARK_LOG=qwen35_35b-a3b JITBEAM=2 IGNORE_BEAM_CACHE=1 python3 -m tinygrad.llm -m qwen3.5:35b-a3b --benchmark --warmup
|
||||
- name: Run olmoe
|
||||
# just metal for now
|
||||
if: ${{ matrix.dev == 'METAL' }}
|
||||
@@ -402,35 +402,6 @@ jobs:
|
||||
run: PYTHONPATH=. DEV=PCI+NV:NAK python3.11 test/test_tiny.py
|
||||
|
||||
testcommalatest:
|
||||
name: comma Benchmark (0.11.2)
|
||||
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.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
|
||||
@@ -461,6 +432,35 @@ jobs:
|
||||
- name: Run process replay tests
|
||||
uses: ./.github/actions/process-replay
|
||||
|
||||
testcommaold:
|
||||
name: comma Benchmark (0.10.1)
|
||||
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: DEBUG=2 openpilot compile3 0.10.1 driving_vision
|
||||
run: PYTHONPATH="." DEBUG=2 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/720392c9a5b986981fdbed1bb8c47a6c5573a50e/selfdrive/modeld/models/driving_vision.onnx
|
||||
- name: openpilot compile3 0.10.1 driving_vision
|
||||
run: BENCHMARK_LOG=openpilot_0_10_1_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/720392c9a5b986981fdbed1bb8c47a6c5573a50e/selfdrive/modeld/models/driving_vision.onnx
|
||||
- name: openpilot compile3 0.10.1 driving_policy
|
||||
run: BENCHMARK_LOG=openpilot_0_10_1_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/720392c9a5b986981fdbed1bb8c47a6c5573a50e/selfdrive/modeld/models/driving_policy.onnx
|
||||
- name: openpilot compile3 0.10.1 dmonitoring
|
||||
run: BENCHMARK_LOG=openpilot_0_10_1_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/720392c9a5b986981fdbed1bb8c47a6c5573a50e/selfdrive/modeld/models/dmonitoring_model.onnx
|
||||
- name: Run process replay tests
|
||||
uses: ./.github/actions/process-replay
|
||||
|
||||
testqualcommdsp:
|
||||
name: DSP Benchmark
|
||||
runs-on: [self-hosted, Linux, comma4]
|
||||
@@ -515,8 +515,6 @@ jobs:
|
||||
run: |
|
||||
echo "CACHEDB=/tmp/staging.db" >> $GITHUB_ENV
|
||||
rm -f /tmp/staging.db /tmp/staging.db-shm /tmp/staging.db-wal
|
||||
- name: reset chestnut
|
||||
run: python3 extra/usbgpu/debug.py -rn
|
||||
- name: openpilot compile3 0.10.1 driving_vision
|
||||
run: BENCHMARK_LOG=usbgpu_openpilot_0_10_1_vision PYTHONPATH="." GMMU=0 DEV=USB+AMD:LLVM ASSERT_MIN_STEP_TIME=50 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/720392c9a5b986981fdbed1bb8c47a6c5573a50e/selfdrive/modeld/models/driving_vision.onnx
|
||||
- name: openpilot load_pickle 0.10.1 driving_vision
|
||||
|
||||
@@ -167,7 +167,6 @@ jobs:
|
||||
uses: ./.github/actions/setup-tinygrad
|
||||
with:
|
||||
key: unittest-13
|
||||
python-version: '3.11'
|
||||
deps: testing_unit
|
||||
llvm: 'true'
|
||||
amd: 'true'
|
||||
@@ -177,14 +176,13 @@ jobs:
|
||||
run: |
|
||||
DEV=NULL python3 -m unittest test.backend.test_multitensor.TestMultiTensor.test_data_parallel_resnet_train_step
|
||||
DEV=NULL VIZ=1 python3 -m pytest -n=auto test/null/test_viz.py
|
||||
DEBUG=7 python -m tinygrad.viz.cli --json | jq empty
|
||||
# TODO: too slow
|
||||
# - name: Run SDXL on NULL backend
|
||||
# run: DEV=NULL DEBUG=1 python3 examples/sdxl.py --seed 0 --noshow --timing --fakeweights
|
||||
- name: Run Clip tests for SD MLPerf on NULL backend
|
||||
run: DEV=NULL python -m pytest -n=auto test/external/mlperf_stable_diffusion/external_test_models.py::TestOpenClip --durations=20
|
||||
- name: Run AMD emulated BERT training on NULL backend
|
||||
run: DEV=NULL::gfx1201 NULL_ALLOW_COPYOUT=1 DEFAULT_FLOAT=HALF BENCHMARK=10 BS=66 GPUS=1 BERT_LAYERS=2 MODEL=bert python3 examples/mlperf/model_train.py
|
||||
run: DEV=NULL::gfx1201 NULL_ALLOW_COPYOUT=1 CAPTURE_PROCESS_REPLAY=0 DEFAULT_FLOAT=HALF BENCHMARK=10 BS=66 GPUS=1 BERT_LAYERS=2 MODEL=bert python3 examples/mlperf/model_train.py
|
||||
# TODO: support fake weights
|
||||
#- name: Run LLaMA 7B on 4 fake devices
|
||||
# run: DEV=NULL python3 examples/llama.py --gen 1 --size 7B --shard 4 --prompt "Hello." --count 3 --temperature 0 --timing
|
||||
@@ -201,7 +199,6 @@ jobs:
|
||||
uses: ./.github/actions/setup-tinygrad
|
||||
with:
|
||||
key: unittest-13
|
||||
python-version: '3.11'
|
||||
pydeps: "pre-commit"
|
||||
deps: testing_unit
|
||||
llvm: 'true'
|
||||
@@ -481,7 +478,7 @@ jobs:
|
||||
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
|
||||
echo "deb http://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-21 main" | sudo tee /etc/apt/sources.list.d/llvm.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y llvm-21 llvm-21-tools cloc
|
||||
sudo apt-get install llvm-21 llvm-21-tools cloc
|
||||
- name: Install rocprof-trace-decoder
|
||||
run: sudo PYTHONPATH="." ./extra/sqtt/install_rocprof_decoder.py
|
||||
- name: Run AMD renderer tests
|
||||
@@ -631,7 +628,7 @@ jobs:
|
||||
|
||||
unittestmacos:
|
||||
name: MacOS (unit)
|
||||
runs-on: macos-26
|
||||
runs-on: &macos macos-26
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -680,7 +677,7 @@ jobs:
|
||||
matrix:
|
||||
group: [1, 2]
|
||||
name: MacOS (DEV=METAL) (${{ matrix.group }})
|
||||
runs-on: macos-26
|
||||
runs-on: *macos
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
DEV: METAL
|
||||
@@ -712,7 +709,7 @@ jobs:
|
||||
- 'WEBGPU'
|
||||
|
||||
name: MacOS (DEV=${{ matrix.dev }})
|
||||
runs-on: macos-26
|
||||
runs-on: *macos
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
|
||||
@@ -3,4 +3,3 @@
|
||||
- Run tests with `-n12` for speed (e.g. `python -m pytest test/null/test_dtype.py -x -q -n12`)
|
||||
- Run `python -m mypy tinygrad/` to typecheck
|
||||
- Run `python -m ruff check .` to lint
|
||||
- Read `./tinygrad/viz/README.md` for profiling and debugging rewrite rules
|
||||
|
||||
@@ -88,7 +88,7 @@ def example_3_custom_uop(a:Tensor, correct):
|
||||
|
||||
# store all the per lane accumulators to LOCAL
|
||||
local_accs = UOp.placeholder((LCLS,), dtypes.float, slot=0, addrspace=AddrSpace.LOCAL)
|
||||
local_accs = local_accs.after(local_accs[lane].store(acc[0]))
|
||||
local_accs = local_accs.after(local_accs[lane].store(acc[0]).barrier())
|
||||
|
||||
# accumulate LOCALs into a single per CU accumulator
|
||||
late_reduce_loop = UOp.range(LCLS, 3, AxisType.REDUCE)
|
||||
|
||||
+1
-2
@@ -1,8 +1,7 @@
|
||||
::: tinygrad.dtype.DType
|
||||
|
||||
::: tinygrad.dtype.DTypes
|
||||
::: tinygrad.dtype.dtypes
|
||||
options:
|
||||
heading: dtypes
|
||||
members: true
|
||||
members_order: source
|
||||
show_labels: false
|
||||
|
||||
@@ -9,7 +9,8 @@ from extra.lr_scheduler import OneCycleLR
|
||||
GPUS = [f'{Device.DEFAULT}:{i}' for i in range(getenv("GPUS", 1))]
|
||||
|
||||
# override tinygrad defaults
|
||||
Context(DEFAULT_FLOAT=dtypes.half, FUSE_OPTIM=1).__enter__()
|
||||
dtypes.default_float = dtypes.half
|
||||
Context(FUSE_OPTIM=1).__enter__()
|
||||
|
||||
# from https://github.com/tysam-code/hlb-CIFAR10/blob/main/main.py
|
||||
batchsize = getenv("BS", 1024)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import os, random, pickle, queue, struct, math, functools, hashlib, time
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
from multiprocessing import Queue, Process, shared_memory, connection, Lock
|
||||
from multiprocessing import Queue, Process, shared_memory, connection, Lock, cpu_count
|
||||
|
||||
import numpy as np
|
||||
from tinygrad import dtypes, Tensor
|
||||
from tinygrad.helpers import getenv, prod, Context, round_up, tqdm, OSX, NUM_CPU_THREADS
|
||||
from tinygrad.helpers import getenv, prod, Context, round_up, tqdm, OSX
|
||||
from tinygrad.nn.state import TensorIO
|
||||
|
||||
### ResNet
|
||||
@@ -131,7 +131,7 @@ def batch_load_resnet(batch_size=64, val=False, shuffle=True, seed=None, pad_fir
|
||||
else: X = Tensor.empty(*sz, dtype=dtypes.uint8, device=f"disk:/dev/shm/{shm_name}")
|
||||
Y = [None] * (batch_size*BATCH_COUNT)
|
||||
|
||||
for _ in range(NUM_CPU_THREADS.value):
|
||||
for _ in range(cpu_count()):
|
||||
p = Process(target=loader_process, args=(q_in, q_out, X, seed))
|
||||
p.daemon = True
|
||||
p.start()
|
||||
@@ -212,7 +212,7 @@ def batch_load_train_bert(BS:int, seed:int|None=None):
|
||||
rng.shuffle(fs)
|
||||
train_files.append(fs.pop(0))
|
||||
|
||||
cycle_length = min(NUM_CPU_THREADS.value, len(train_files))
|
||||
cycle_length = min(getenv("NUM_CPU_THREADS", min(os.cpu_count(), 8)), len(train_files))
|
||||
assert cycle_length > 0, "cycle_length must be greater than 0"
|
||||
|
||||
dataset = InterleavedDataset(train_files, cycle_length)
|
||||
@@ -301,7 +301,7 @@ def batch_load_unet3d(preprocessed_dataset_dir:Path, batch_size:int=6, val:bool=
|
||||
X = Tensor.empty(*sz, dtype=dtypes.float32, device=f"disk:/dev/shm/{shm_name_x}")
|
||||
Y = Tensor.empty(*sz, dtype=dtypes.uint8, device=f"disk:/dev/shm/{shm_name_y}")
|
||||
|
||||
for _ in range(NUM_CPU_THREADS.value):
|
||||
for _ in range(cpu_count()):
|
||||
proc = Process(target=load_unet3d_data, args=(preprocessed_dataset_dir, seed, queue_in, queue_out, X, Y))
|
||||
proc.daemon = True
|
||||
proc.start()
|
||||
@@ -437,7 +437,7 @@ def batch_load_retinanet(dataset, val:bool, base_dir:Path, batch_size:int=32, sh
|
||||
dataset_iter = iter(image_ids)
|
||||
|
||||
try:
|
||||
for _ in range(NUM_CPU_THREADS.value):
|
||||
for _ in range(cpu_count()):
|
||||
proc = Process(
|
||||
target=load_retinanet_data,
|
||||
args=(base_dir, val, queue_in, queue_out, imgs, boxes, labels),
|
||||
|
||||
@@ -1285,7 +1285,7 @@ def train_llama3():
|
||||
from examples.mlperf.models.flat_llama import FlatTransformer, apply_grad, FP8_DTYPE, MXFP8
|
||||
from examples.llama3 import MODEL_PARAMS
|
||||
from examples.mlperf.lr_schedulers import CosineAnnealingLRWithWarmup
|
||||
from examples.mlperf.optim import GradAccClipAdamW, clip_grads
|
||||
from examples.mlperf.optim import GradAccClipAdamW
|
||||
|
||||
INITMLPERF = getenv("INITMLPERF")
|
||||
RUNMLPERF = getenv("RUNMLPERF")
|
||||
@@ -1482,8 +1482,7 @@ def train_llama3():
|
||||
|
||||
@TinyJit
|
||||
def optim_step():
|
||||
grad_norm = clip_grads(grads, grad_acc, 1.0)
|
||||
optim.fstep(grads, grad_norm)
|
||||
grad_norm = optim.fstep(grads)
|
||||
scheduler.step()
|
||||
|
||||
for g in grads: g.assign(0)
|
||||
@@ -1668,7 +1667,7 @@ def train_llama3():
|
||||
def train_gptoss():
|
||||
from examples.mlperf.models.gpt_oss import GPTOSS, GPT_OSS_20B, apply_grad, FP8_DTYPE
|
||||
from examples.mlperf.lr_schedulers import CosineAnnealingLRWithWarmup
|
||||
from examples.mlperf.optim import GradAccClipAdamW, GradAccClipAdamWGroup, clip_grads
|
||||
from examples.mlperf.optim import GradAccClipAdamW
|
||||
|
||||
BENCHMARK = getenv("BENCHMARK")
|
||||
|
||||
@@ -1734,12 +1733,7 @@ def train_gptoss():
|
||||
is_offload_optim = bool(getenv("OFFLOAD_OPTIM"))
|
||||
is_fake_offload = Device.DEFAULT == "NULL"
|
||||
optim_device = ("CPU" if not is_fake_offload else "NULL:99") if is_offload_optim else None
|
||||
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),
|
||||
)
|
||||
optim = GradAccClipAdamW(params, 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)
|
||||
|
||||
for p in optim.params:
|
||||
grad_dtype = dtypes.bfloat16 if p.dtype == FP8_DTYPE else p.dtype
|
||||
@@ -1781,8 +1775,7 @@ def train_gptoss():
|
||||
|
||||
@TinyJit
|
||||
def optim_step():
|
||||
grad_norm = clip_grads(grads, grad_acc, 1.0)
|
||||
optim.fstep(grads, grad_norm)
|
||||
grad_norm = optim.fstep(grads)
|
||||
scheduler.step()
|
||||
|
||||
for g in grads: g.assign(0)
|
||||
|
||||
@@ -50,7 +50,8 @@ def _dequant_fwd_fxn(wq_p, ws_p, device):
|
||||
return _dequant_fwd(Tensor(wq_p, device=device), Tensor(ws_p, device=device))
|
||||
|
||||
def _dequant_bwd(grad:UOp, call:UOp) -> tuple:
|
||||
return (Tensor(grad).cast(dtypes.bfloat16).uop, None)
|
||||
w_scale = Tensor(call.src[2])
|
||||
return ((Tensor(grad).cast(dtypes.bfloat16) * _mx_scale(w_scale).cast(dtypes.bfloat16)).uop, None)
|
||||
|
||||
def dequant_weight(w_q:Tensor, w_scale:Tensor) -> Tensor:
|
||||
fxn = _dequant_fwd_fxn(w_q.as_param(0).uop, w_scale.as_param(1).uop, w_q.device)
|
||||
|
||||
+26
-33
@@ -1,6 +1,6 @@
|
||||
from tinygrad.tensor import Tensor
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.nn.optim import Optimizer, OptimizerGroup
|
||||
from tinygrad.nn.optim import Optimizer
|
||||
from tinygrad.helpers import FUSE_OPTIM, getenv
|
||||
from tinygrad.uop.ops import UOp, Ops
|
||||
|
||||
@@ -21,12 +21,6 @@ def stochastic_round_bf16(x:Tensor) -> Tensor:
|
||||
noise = (noise * 0xFFFF).cast(dtypes.uint32)
|
||||
return ((bits + noise) & 0xFFFF0000).bitcast(dtypes.float32).cast(dtypes.bfloat16)
|
||||
|
||||
def clip_grads(grads:list[Tensor], grad_acc, clip_norm) -> Tensor:
|
||||
for g in grads: g.assign(g / grad_acc)
|
||||
total_norm = Tensor.stack(*[g.float().square().sum() for g in grads]).sum().sqrt().contiguous()
|
||||
for g in grads: g.assign((g * (clip_norm / (total_norm + 1e-6)).clamp(max_=1.0)).cast(g.dtype))
|
||||
return total_norm
|
||||
|
||||
class GradAccClipAdamW(Optimizer):
|
||||
def __init__(self, params:list[Tensor], lr=0.001, b1=0.9, b2=0.999, eps=1e-6, weight_decay=0.0, grad_acc=1, clip_norm=1.0, device=None, fused=FUSE_OPTIM):
|
||||
super().__init__(params, lr, device, fused)
|
||||
@@ -43,28 +37,45 @@ class GradAccClipAdamW(Optimizer):
|
||||
|
||||
def _zero_shard(self, t:Tensor) -> Tensor:
|
||||
if not self.zero or (t.shape[0] % len(self.device)) != 0: return t
|
||||
return Tensor(t.uop._shard(0, len(self.device)).unshard(0)).clone()
|
||||
return Tensor(t.uop._shard(0, len(self.device)).multi(0)).clone()
|
||||
|
||||
def _zero_gather(self, t:Tensor) -> Tensor:
|
||||
if not isinstance(t.device, tuple) or t.uop.axis != 0: return t
|
||||
n, sz = len(t.device), t.shape[0] // len(t.device)
|
||||
return Tensor.cat(*[t[p*sz:(p+1)*sz] for p in range(n)], dim=0)
|
||||
|
||||
def fschedule_step(self, grads:list[Tensor]) -> list[Tensor]:
|
||||
updates, extra = self._step([], grads)
|
||||
def fstep(self, grads:list[Tensor]):
|
||||
if self.fused:
|
||||
out, extra = self._step([], grads)
|
||||
updates = [out[0][self.pos_params[i]:self.pos_params[i+1]].reshape(tt.shape) for i, tt in enumerate(self.params)]
|
||||
else:
|
||||
updates, extra = self._step([], grads)
|
||||
for i, tt in enumerate(self.params): tt.assign(self._apply_update(tt, updates[i], self.master_params[i] if self.master_params else None))
|
||||
# collect inv_scale tensors attached to fp8 params (set by _apply_update)
|
||||
fp8_inv_scales = [tt._inv_scale for tt in self.params if hasattr(tt, '_inv_scale')]
|
||||
fp8_next_inv_scales = [tt._next_inv_scale for tt in self.params if hasattr(tt, '_next_inv_scale')]
|
||||
return extra + self.params + self.buffers + (self.master_params or []) + fp8_inv_scales + fp8_next_inv_scales
|
||||
to_realize = extra+self.params+self.buffers+(self.master_params or [])+fp8_inv_scales+fp8_next_inv_scales
|
||||
|
||||
def fstep(self, grads:list[Tensor], grad_norm:Tensor|None=None):
|
||||
Tensor.realize(*([grad_norm] if grad_norm is not None else []), *self.fschedule_step(grads))
|
||||
Tensor.realize(*to_realize)
|
||||
return extra[-1]
|
||||
|
||||
def _step(self, params:list[Tensor], grads:list[Tensor]) -> tuple[list[Tensor], list[Tensor]]:
|
||||
grads = list(grads)
|
||||
|
||||
for i in range(len(grads)):
|
||||
if grads[i].device != self.m[i].device: grads[i] = grads[i].to(self.m[i].device)
|
||||
|
||||
if self.fused:
|
||||
grads[0].assign(grads[0] / self.grad_acc)
|
||||
total_norm = grads[0].float().square().sum().sqrt()
|
||||
grads[0].assign((grads[0] * (self.clip_norm / (total_norm + 1e-6)).clamp(max_=1.0)).cast(grads[0].dtype))
|
||||
else:
|
||||
for i in range(len(grads)):
|
||||
grads[i].assign(grads[i] / self.grad_acc)
|
||||
total_norm = Tensor.stack(*[g.float().square().sum() for g in grads]).sum().sqrt().contiguous()
|
||||
for i in range(len(grads)):
|
||||
grads[i].assign((grads[i] * (self.clip_norm / (total_norm + 1e-6)).clamp(max_=1.0)).cast(grads[i].dtype))
|
||||
|
||||
ret = []
|
||||
self.b1_t *= self.b1
|
||||
self.b2_t *= self.b2
|
||||
@@ -77,7 +88,7 @@ class GradAccClipAdamW(Optimizer):
|
||||
v_hat = v_new / (1.0 - self.b2_t)
|
||||
up = m_hat / (v_hat.sqrt() + self.eps)
|
||||
ret.append(self.lr * up)
|
||||
return ret, [self.b1_t, self.b2_t] + self.m + self.v
|
||||
return ret, [self.b1_t, self.b2_t] + self.m + self.v + [total_norm]
|
||||
|
||||
def _apply_update(self, t:Tensor, up:Tensor, master:Tensor|None=None) -> Tensor:
|
||||
w = master if master is not None else t
|
||||
@@ -98,7 +109,7 @@ class GradAccClipAdamW(Optimizer):
|
||||
if self.zero: w_q, w_e8 = self._zero_gather(w_q), self._zero_gather(w_e8)
|
||||
new_e8 = w_e8.reshape(t._inv_scale.shape)
|
||||
t._inv_scale.assign(new_e8.shard_like(t._inv_scale) if offloaded else new_e8)
|
||||
ret = w_q.reshape(t.shape)
|
||||
ret = w_q.reshape(new_w.shape)
|
||||
return ret.shard_like(t) if offloaded else ret
|
||||
from examples.mlperf.models.flat_llama import FP8_MAX
|
||||
if IMMEDIATE_SCALE:
|
||||
@@ -121,21 +132,3 @@ class GradAccClipAdamW(Optimizer):
|
||||
return ret.shard_like(t) if offloaded else ret
|
||||
out = new_w.cast(t.dtype)
|
||||
return out.shard_like(t) if offloaded else out
|
||||
|
||||
class GradAccClipAdamWGroup(OptimizerGroup):
|
||||
def fstep(self, grads:list[Tensor], grad_norm:Tensor|None=None):
|
||||
offset = 0
|
||||
to_realize = []
|
||||
for o in self.optimizers:
|
||||
n = len(o.params)
|
||||
to_realize += o.fschedule_step(grads[offset:offset+n])
|
||||
offset += n
|
||||
Tensor.realize(*to_realize, *([grad_norm] if grad_norm is not None else []))
|
||||
@property
|
||||
def lr(self): return self.optimizers[0].lr
|
||||
@property
|
||||
def device(self): return self.optimizers[0].device
|
||||
@property
|
||||
def master_params(self):
|
||||
mp = [mp for o in self.optimizers for mp in (o.master_params or [])]
|
||||
return mp if mp else None
|
||||
|
||||
@@ -28,8 +28,8 @@ def compile(onnx_file):
|
||||
inputs = {k:Tensor(v.numpy(), device=Device.DEFAULT).realize() if 'img' in k else v for k,v in inputs.items()}
|
||||
print("created tensors")
|
||||
|
||||
@TinyJit(prune=True)
|
||||
def run_onnx_jit(**kwargs): return next(iter(run_onnx({k:v.to(Device.DEFAULT) for k,v in kwargs.items()}).values())).cast('float32')
|
||||
run_onnx_jit = TinyJit(lambda **kwargs:
|
||||
next(iter(run_onnx({k:v.to(Device.DEFAULT) for k,v in kwargs.items()}).values())).cast('float32'), prune=True)
|
||||
for i in range(3):
|
||||
GlobalCounters.reset()
|
||||
print(f"run {i}")
|
||||
|
||||
@@ -241,8 +241,8 @@ export default {model_name};
|
||||
def export_model(model, target:str, *inputs, model_name: Optional[str] = "model", stream_weights=False):
|
||||
assert Device.DEFAULT in EXPORT_SUPPORTED_DEVICE, f"only {', '.join(EXPORT_SUPPORTED_DEVICE)} are supported"
|
||||
|
||||
# NOTE: NUM_CPU_THREADS=1, since export does not support threading
|
||||
with Context(JIT=2, NUM_CPU_THREADS=1): linear, output_bufs = jit_model(model, *inputs)
|
||||
# NOTE: CPU_COUNT=1, since export does not support threading
|
||||
with Context(JIT=2, CPU_COUNT=1): linear, output_bufs = jit_model(model, *inputs)
|
||||
functions, statements, bufs, bufs_to_save = compile_net(linear, output_bufs)
|
||||
state = get_state_dict(model)
|
||||
weight_names = {(id(b), b.offset, b.size, b.dtype): name for name, x in state.items() if (b:=x.uop.base.realized) is not None}
|
||||
|
||||
@@ -18,9 +18,9 @@ def custom_matmul(output: UOp, inp: UOp, weight: UOp) -> UOp:
|
||||
SEQ = inp.shape[1]
|
||||
OUT = weight.shape[0]
|
||||
IN = weight.shape[-1]
|
||||
seq_idx = UOp.range(SEQ, 2)
|
||||
out_idx = UOp.range(OUT, 3)
|
||||
batch_idx = UOp.range(output.size//SEQ//OUT, 1)
|
||||
seq_idx = UOp.range(SEQ, 2, AxisType.LOOP)
|
||||
out_idx = UOp.range(OUT, 3, AxisType.LOOP)
|
||||
batch_idx = UOp.range(output.size//SEQ//OUT, 1, AxisType.LOOP)
|
||||
reduce_idx = UOp.range(IN, 0, AxisType.REDUCE)
|
||||
product = (inp.index((seq_idx*IN+reduce_idx+batch_idx*IN*SEQ)) * weight.index((out_idx*IN+reduce_idx))).cast(dtypes.float)
|
||||
reduced = product.reduce(reduce_idx, arg=Ops.ADD)
|
||||
@@ -53,7 +53,7 @@ class FP8Linear:
|
||||
x_fp8, x_scale = quantize_to_fp8(x)
|
||||
GPUS = self.weight.device
|
||||
if isinstance(GPUS, tuple) and len(GPUS) > 1:
|
||||
y = Tensor(Tensor.empty((batch//len(GPUS), seq, self.weight.shape[0]), dtype=dtypes.float, device=GPUS).uop.unshard(0), device=GPUS)
|
||||
y = Tensor(Tensor.empty((batch//len(GPUS), seq, self.weight.shape[0]), dtype=dtypes.float, device=GPUS).uop.multi(0), device=GPUS)
|
||||
else:
|
||||
y = Tensor.empty((batch, seq, self.weight.shape[0]), dtype=dtypes.float)
|
||||
y = Tensor.custom_kernel(y, x_fp8, w_fp8, fxn=custom_matmul, grad_fxn=custom_matmul_backward)[0]
|
||||
|
||||
@@ -58,8 +58,8 @@ def block_128x128_gemm(c:UOp, a:UOp, b:UOp) -> UOp:
|
||||
B_copy = B_local.permute((1,0)) if use_wmma else B_local
|
||||
A_store = A_copy.reshape(-1, THREADS_PER_BLOCK)[:, tid].store(a[k_tile].reshape(-1, THREADS_PER_BLOCK)[:, tid])
|
||||
B_store = B_copy.reshape(-1, THREADS_PER_BLOCK)[:, tid].store(b[k_tile].reshape(-1, THREADS_PER_BLOCK)[:, tid])
|
||||
# NOTE: no explicit barrier needed, the AFTER on the LOCAL buffers implies it in late codegen
|
||||
A_local, B_local = A_local.after(A_store, B_store), B_local.after(A_store, B_store)
|
||||
barrier = UOp.barrier(A_store, B_store)
|
||||
A_local, B_local = A_local.after(barrier), B_local.after(barrier)
|
||||
|
||||
# -- COMPUTE --
|
||||
lane_m, lane_n = lane // LANES_PER_WAVE_N, lane % LANES_PER_WAVE_N
|
||||
@@ -70,8 +70,8 @@ def block_128x128_gemm(c:UOp, a:UOp, b:UOp) -> UOp:
|
||||
|
||||
if use_wmma:
|
||||
k = UOp.range(BLOCK_K // WMMA_K, 101, AxisType.REDUCE)
|
||||
tile_m = UOp.range(TM // WMMA_ACC, 200)
|
||||
tile_n = UOp.range(TN, 201)
|
||||
tile_m = UOp.range(TM // WMMA_ACC, 200, AxisType.LOOP)
|
||||
tile_n = UOp.range(TN, 201, AxisType.LOOP)
|
||||
|
||||
acc_frag = acc.reshape(TM // WMMA_ACC, WMMA_ACC, TN).permute(0,2,1)[tile_m, tile_n]
|
||||
a_frag = A_local.reshape(WAVES_M, TM // WMMA_ACC, WMMA_M, BLOCK_K // WMMA_K, WMMA_K)[wave_m, tile_m, lane_n, k]
|
||||
@@ -96,8 +96,8 @@ def block_128x128_gemm(c:UOp, a:UOp, b:UOp) -> UOp:
|
||||
b_frag = b_frag.reshape(1, TN).expand(TM, TN)
|
||||
acc_store = acc.store(acc.after(k) + (a_frag * b_frag))
|
||||
|
||||
# store accumulator and loop (the barrier at the end of the loop is implied by the LOCAL buffers stored and loaded in the loop)
|
||||
acc = acc.after(acc_store.end(k).end(k_tile))
|
||||
# store accumulator and loop
|
||||
acc = acc.after(acc_store.end(k).barrier().end(k_tile))
|
||||
|
||||
# store accumulator to output (unified)
|
||||
c = c.reshape(WAVES_M, TM//UNROLL_M, LANES_PER_WAVE_M, UNROLL_M,
|
||||
|
||||
@@ -84,16 +84,16 @@ def amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp) -> UOp:
|
||||
q.reshape(THREADS_PER_BLOCK, ELEMS_PER_THREAD)[tid])
|
||||
K_store = KV_lds.reshape(THREADS_PER_BLOCK, ELEMS_PER_THREAD)[tid].store(
|
||||
k[n_tile].reshape(THREADS_PER_BLOCK, ELEMS_PER_THREAD)[tid])
|
||||
# NOTE: no explicit barrier needed, the AFTER on the LOCAL buffers implies it in late codegen
|
||||
Q_lds = Q_lds.after(UOp.group(Q_store, K_store))
|
||||
KV_lds_k = KV_lds.after(UOp.group(Q_store, K_store))
|
||||
qk_load_barrier = UOp.barrier(UOp.group(Q_store, K_store))
|
||||
Q_lds = Q_lds.after(qk_load_barrier)
|
||||
KV_lds_k = KV_lds.after(qk_load_barrier)
|
||||
|
||||
# -- S = Q @ K^T via WMMA (re-init each n_tile) --
|
||||
S_reg = UOp.placeholder((TM, TN), dtypes.float, slot=6, addrspace=AddrSpace.REG)
|
||||
S_reg = S_reg.after(S_reg.after(n_tile).store(S_reg.const_like(0)))
|
||||
k_qk = UOp.range(D // WMMA_K, 101, AxisType.REDUCE)
|
||||
tm1 = UOp.range(TM // WMMA_ACC, 200)
|
||||
tn1 = UOp.range(TN, 201)
|
||||
tm1 = UOp.range(TM // WMMA_ACC, 200, AxisType.LOOP)
|
||||
tn1 = UOp.range(TN, 201, AxisType.LOOP)
|
||||
S_frag = S_reg.reshape(TM // WMMA_ACC, WMMA_ACC, TN).permute(0, 2, 1)[tm1, tn1]
|
||||
q_frag = Q_lds.reshape(WAVES_M, TM // WMMA_ACC, WMMA_M, D // WMMA_K, WMMA_K)[wave_m, tm1, lane_n, k_qk]
|
||||
k_frag = KV_lds_k.reshape(WAVES_N, TN, WMMA_N, D // WMMA_K, WMMA_K)[wave_n, tn1, lane_n, k_qk]
|
||||
@@ -110,7 +110,7 @@ def amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp) -> UOp:
|
||||
rm2 = UOp.range(TN, 261, AxisType.REDUCE)
|
||||
m_ij = m_ij.after(m_ij.store(m_ij.after(rm2).maximum(S_reg[:, rm2])).end(rm2))
|
||||
# warp reduce max (in-place)
|
||||
ri_w = UOp.range(TM, 270)
|
||||
ri_w = UOp.range(TM, 270, AxisType.LOOP)
|
||||
m_ij = m_ij.after(m_ij[ri_w].store(warp_reduce_max(m_ij[ri_w], lane)).end(ri_w))
|
||||
|
||||
# compute P = exp(S - m_ij) in S_reg
|
||||
@@ -120,7 +120,7 @@ def amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp) -> UOp:
|
||||
p_local = p_local.after(p_local.after(n_tile).store(p_local.const_like(0)))
|
||||
rp2 = UOp.range(TN, 291, AxisType.REDUCE)
|
||||
p_local = p_local.after(p_local.store(p_local.after(rp2) + S_reg[:, rp2]).end(rp2))
|
||||
ri_ws = UOp.range(TM, 295)
|
||||
ri_ws = UOp.range(TM, 295, AxisType.LOOP)
|
||||
p_sum = p_local.after(p_local[ri_ws].store(warp_reduce_sum(p_local[ri_ws], lane)).end(ri_ws))
|
||||
|
||||
# write P = exp(S - m_ij) to P_lds (reuses slot 0, Q no longer needed)
|
||||
@@ -130,11 +130,11 @@ def amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp) -> UOp:
|
||||
P_store = P_write[tid].store(S_reg.cast(dtypes.half))
|
||||
|
||||
# -- online softmax correction --
|
||||
ri4 = UOp.range(TM, 330)
|
||||
ri4 = UOp.range(TM, 330, AxisType.LOOP)
|
||||
m_new_val = m_i[ri4].maximum(m_ij[ri4])
|
||||
alpha_val = ((m_i[ri4] - m_new_val) * LOG2E).exp2()
|
||||
beta_val = ((m_ij[ri4] - m_new_val) * LOG2E).exp2()
|
||||
rj4 = UOp.range(TD, 331)
|
||||
rj4 = UOp.range(TD, 331, AxisType.LOOP)
|
||||
correction = UOp.group(
|
||||
acc[ri4, rj4].store(alpha_val * acc[ri4, rj4]).end(rj4),
|
||||
l_i[ri4].store(alpha_val * l_i[ri4] + beta_val * p_sum[ri4]),
|
||||
@@ -147,21 +147,21 @@ def amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp) -> UOp:
|
||||
# load V into KV_lds (must wait for QK WMMA to finish reading K from KV_lds)
|
||||
V_store = KV_lds.after(qk_done).reshape(THREADS_PER_BLOCK, ELEMS_PER_THREAD)[tid].store(
|
||||
v[n_tile].reshape(THREADS_PER_BLOCK, ELEMS_PER_THREAD)[tid])
|
||||
# NOTE: no explicit barrier needed, the AFTER on the LOCAL buffers implies it in late codegen
|
||||
P_lds = P_lds.after(UOp.group(P_store, V_store))
|
||||
KV_lds_v = KV_lds.after(UOp.group(P_store, V_store))
|
||||
pv_barrier = UOp.barrier(UOp.group(P_store, V_store))
|
||||
P_lds = P_lds.after(pv_barrier)
|
||||
KV_lds_v = KV_lds.after(pv_barrier)
|
||||
|
||||
# -- acc += P @ V via WMMA --
|
||||
k_pv = UOp.range(BLOCK_N // WMMA_K, 400, AxisType.REDUCE)
|
||||
tm2 = UOp.range(TM // WMMA_ACC, 401)
|
||||
tn2 = UOp.range(TD, 402)
|
||||
tm2 = UOp.range(TM // WMMA_ACC, 401, AxisType.LOOP)
|
||||
tn2 = UOp.range(TD, 402, AxisType.LOOP)
|
||||
acc_frag = acc.reshape(TM // WMMA_ACC, WMMA_ACC, TD).permute(0, 2, 1)[tm2, tn2]
|
||||
p_frag = P_lds.reshape(WAVES_M, TM // WMMA_ACC, WMMA_M, BLOCK_N // WMMA_K, WMMA_K)[wave_m, tm2, lane_n, k_pv]
|
||||
v_frag = KV_lds_v.reshape(WAVES_N, TD, WMMA_N, BLOCK_N // WMMA_K, WMMA_K)[wave_n, tn2, lane_n, k_pv]
|
||||
pv = UOp.wmma(p_frag, v_frag, acc_frag.after(k_pv), *WMMA_ARG)
|
||||
|
||||
# end KV tile loop
|
||||
n_tile_end = acc_frag.store(pv).end(tm2, tn2).end(k_pv).end(n_tile)
|
||||
n_tile_end = acc_frag.store(pv).end(tm2, tn2).end(k_pv).barrier().end(n_tile)
|
||||
acc = acc.after(n_tile_end)
|
||||
l_i = l_i.after(n_tile_end)
|
||||
m_i = m_i.after(n_tile_end)
|
||||
|
||||
@@ -28,10 +28,10 @@ REG_TILES_PER_WAVE_M = BLOCK_M // (WAVES_PER_BLOCK_M * LANES_PER_WAVE_M * TM)
|
||||
assert WAVES_PER_BLOCK_M*REG_TILES_PER_WAVE_M*LANES_PER_WAVE_M*TM == BLOCK_M, "M reshape is wrong"
|
||||
assert WAVES_PER_BLOCK_N*REG_TILES_PER_WAVE_N*LANES_PER_WAVE_N*TN == BLOCK_N, "N reshape is wrong"
|
||||
|
||||
def rngs_for_shape(shape:tuple[sint, ...], rng:int, axis_type=AxisType.WEAK): return [UOp.range(s, rng+i, axis_type) for i,s in enumerate(shape)]
|
||||
def rngs_for_shape(shape:tuple[sint, ...], rng:int, axis_type=AxisType.LOOP): return [UOp.range(s, rng+i, axis_type) for i,s in enumerate(shape)]
|
||||
def copy(dest:UOp, src:UOp, rng:int, upcast=False):
|
||||
assert dest.shape == src.shape
|
||||
rngs = rngs_for_shape(src.shape, rng, AxisType.UPCAST if upcast else AxisType.WEAK)
|
||||
rngs = rngs_for_shape(src.shape, rng, AxisType.UPCAST if upcast else AxisType.LOOP)
|
||||
return dest[*rngs].store(src[*rngs]).end(*rngs)
|
||||
|
||||
def hand_spec_kernel3(c:UOp, a:UOp, b:UOp) -> UOp:
|
||||
@@ -66,8 +66,9 @@ def hand_spec_kernel3(c:UOp, a:UOp, b:UOp) -> UOp:
|
||||
B_local = UOp.placeholder((BLOCK_K, BLOCK_N), dtypes.float, slot=1, addrspace=AddrSpace.LOCAL)
|
||||
B_local_store = copy(B_local.reshape(-1, THREADS_PER_BLOCK)[:, tid], b.reshape(-1, THREADS_PER_BLOCK)[:, tid], rng=200)
|
||||
|
||||
# NOTE: no explicit barrier needed, the AFTER on the LOCAL buffers implies it in late codegen
|
||||
A_local, B_local = A_local.after(A_local_store, B_local_store), B_local.after(A_local_store, B_local_store)
|
||||
# TODO: can we automate barrier?
|
||||
barrier = UOp.barrier(A_local_store, B_local_store)
|
||||
A_local, B_local = A_local.after(barrier), B_local.after(barrier)
|
||||
|
||||
# open inner k range
|
||||
k = UOp.range(BLOCK_K, 3, AxisType.REDUCE)
|
||||
@@ -101,7 +102,7 @@ def hand_spec_kernel3(c:UOp, a:UOp, b:UOp) -> UOp:
|
||||
sink = c_regs[*rngs].store(c_regs.after(k)[*rngs] + A_col[iter_m, t_m] * B_row[iter_n, t_n]).end(iter_m, iter_n, t_m, t_n)
|
||||
|
||||
# Close k, sync, and close K tiles
|
||||
sink = sink.end(k).end(k_tile_range)
|
||||
sink = sink.end(k).barrier().end(k_tile_range)
|
||||
|
||||
# ---------------------------
|
||||
# REG -> GLOBAL (epilogue)
|
||||
|
||||
@@ -72,7 +72,7 @@ def hk_fp8_atb_gemm(a:Tensor, b:Tensor, x_scale:Tensor|None=None, g_amax:Tensor|
|
||||
elif b.uop.axis == 2: inv, out_axis = Tensor.invalids(1, M, N // ndev, dtype=dtypes.bfloat16, device=a.device), 2
|
||||
elif a.uop.axis == 2: inv, out_axis = Tensor.invalids(1, M // ndev, N, dtype=dtypes.bfloat16, device=a.device), 1
|
||||
else: inv, out_axis, reduce_out = Tensor.invalids(1, M, N, dtype=dtypes.bfloat16, device=a.device), 0, True
|
||||
out = Tensor(inv.uop.unshard(out_axis), device=a.device)
|
||||
out = Tensor(inv.uop.multi(out_axis), device=a.device)
|
||||
dname = a.device[0]
|
||||
else:
|
||||
out = Tensor.invalids(1, M, N, dtype=dtypes.bfloat16, device=a.device)
|
||||
@@ -171,8 +171,8 @@ def custom_uop_gemm(C:UOp, A:UOp, B:UOp) -> UOp:
|
||||
M, K = A.shape[0]*A.shape[1], A.shape[2]
|
||||
K2, N = B.shape[(1 if B.ndim == 3 else 0):]
|
||||
assert K == K2
|
||||
m = UOp.range(M, 1)
|
||||
n = UOp.range(N, 2)
|
||||
m = UOp.range(M, 1, AxisType.LOOP)
|
||||
n = UOp.range(N, 2, AxisType.LOOP)
|
||||
k = UOp.range(K, 0, AxisType.REDUCE)
|
||||
mul = (A.flatten().index((m*UOp.const(dtypes.weakint, K)+k))*
|
||||
B.flatten().index((k*UOp.const(dtypes.weakint, N)+n))).cast(dtypes.float32)
|
||||
@@ -234,7 +234,7 @@ def hk_bf16_atb_gemm(a:Tensor, b:Tensor) -> Tensor:
|
||||
elif b.uop.axis == 2: inv, out_axis = Tensor.invalids(1, M, N // ndev, dtype=a.dtype, device=a.device), 2
|
||||
elif a.uop.axis == 2: inv, out_axis = Tensor.invalids(1, M // ndev, N, dtype=a.dtype, device=a.device), 1
|
||||
else: inv, out_axis, reduce_out = Tensor.invalids(1, M, N, dtype=a.dtype, device=a.device), 0, True
|
||||
out = Tensor(inv.uop.unshard(out_axis), device=a.device)
|
||||
out = Tensor(inv.uop.multi(out_axis), device=a.device)
|
||||
dname = a.device[0]
|
||||
else:
|
||||
out = Tensor.invalids(1, M, N, dtype=a.dtype, device=a.device)
|
||||
@@ -366,11 +366,11 @@ def asm_gemm(a:Tensor, b:Tensor, x_scale:Tensor|None=None, w_scale:Tensor|None=N
|
||||
|
||||
if is_multi:
|
||||
if n_sharded:
|
||||
out = Tensor(Tensor.invalids(batch, M, N//len(a.device), dtype=out_dtype, device=a.device).uop.unshard(2), device=a.device)
|
||||
out = Tensor(Tensor.invalids(batch, M, N//len(a.device), dtype=out_dtype, device=a.device).uop.multi(2), device=a.device)
|
||||
elif m_sharded:
|
||||
out = Tensor(Tensor.invalids(batch, M, N, dtype=out_dtype, device=a.device).uop.unshard(1), device=a.device)
|
||||
out = Tensor(Tensor.invalids(batch, M, N, dtype=out_dtype, device=a.device).uop.multi(1), device=a.device)
|
||||
else:
|
||||
out = Tensor(Tensor.invalids(batch//len(a.device) if a.uop.axis==0 else batch, M, N, dtype=out_dtype, device=a.device).uop.unshard(0),
|
||||
out = Tensor(Tensor.invalids(batch//len(a.device) if a.uop.axis==0 else batch, M, N, dtype=out_dtype, device=a.device).uop.multi(0),
|
||||
device=a.device)
|
||||
else:
|
||||
out = Tensor.invalids(batch, M, N, dtype=out_dtype, device=a.device)
|
||||
|
||||
@@ -1,395 +0,0 @@
|
||||
"""
|
||||
HipKittens hk_bf16_gemm (extra/thunder/amd/gemm_bf16.cpp) reimplemented with tinygrad UOps.
|
||||
|
||||
C[M, N] (bf16) = A[M, K] @ B[N, K]^T, fp32 accumulation, exactly the kittens kernel shape:
|
||||
- 256x256 output tile per workgroup, K_STEP=64
|
||||
- 8 warps in a 2x4 grid, each warp owns a 128x64 warp-tile
|
||||
- v_mfma_f32_16x16x32_bf16 on CDNA4 (gfx950) / v_wmma_f32_16x16x16_bf16 (wave32, gfx12) on RDNA4,
|
||||
fp32 accumulators
|
||||
- shared tiles As/Bs with the kittens st_16x32_s swizzle (16x32 subtiles of 1024B)
|
||||
- K stages (STAGES=1: synchronous single buffer)
|
||||
|
||||
Validated on gfx1201 hardware (exact for identity-B, rounding-level noise otherwise), rendered
|
||||
and compiled to gfx950 with comgr for assembly comparison against gemm_bf16.cpp.
|
||||
|
||||
What is NOT expressible vs the kittens C++:
|
||||
- explicit s_waitcnt vmcnt()/lgkmcnt() pipelining and s_setprio: tinygrad models async copy
|
||||
overlap with slot dependencies and emits full workgroup barriers; instruction scheduling
|
||||
is left to clang/LLVM
|
||||
- direct-to-LDS global loads (buffer_load_lds): tinygrad goes global->reg->LDS
|
||||
|
||||
Pipelining status: STAGES=2 gives the kittens-shaped double-buffered pipeline (2 x 64KB LDS
|
||||
like gemm_bf16.cpp, copies overlap the previous pair's mma's), written with FA/gemm_fragment
|
||||
conventions: LDS buffers are (2, tile) placeholders indexed by symbolic parity (ko % 2),
|
||||
which sidesteps static slot choice, fill iterations, predication and duplicate static stores.
|
||||
Validated on the CDNA4 emulator for all tile counts (amt = K//64 in {1..32}, odd/even),
|
||||
single- AND multi-workgroup (bit-close to stages=1 / to hippkittens at rounding level).
|
||||
|
||||
Bug hunt notes (all fixed on this branch; they were entangled for a long time):
|
||||
1. The double-buffered pipeline REGISTER-SPILLS (255+ VGPRs vs 166 for stages=1), and the
|
||||
mock emulator aliased the spill (scratch) segment of ALL waves of a workgroup onto one
|
||||
64-lane region. On real HW each wavefront owns a per-lane segment of the scratch ring
|
||||
(indexed by (wave_id, lane)); waves trampled each other's spilled accumulators, giving
|
||||
the "only the last wave's output survives" signature. emu.py now allocates per-wave
|
||||
scratch buffers.
|
||||
2. The remaining "shape-dependent" corruption (NaNs, mispositioned values in contiguous
|
||||
copies feeding the GEMM) came from tinygrad's devectorizer fusing adjacent bf16 stores
|
||||
into 32-bit stores with UNALIGNED (2-byte) granularity: legal on AMD FLAT/GLOBAL (the
|
||||
hardware splits them), but the emulator floored misaligned addresses to the word below.
|
||||
_mem_store now handles unaligned 32-bit (and wider) accesses byte-exactly.
|
||||
3. memory_coalescing (late/coalesce.py) assumed a single static store per (buffer, index)
|
||||
("attempting multiple stores"); aliased stores (a double-buffered LDS slot written in a
|
||||
prologue AND a loop body) are now simply kept scalar instead of asserting/merging.
|
||||
4. pm_split_ranges may only split ranges WITHOUT hardware meaning (WEAK/REDUCE/LOOP);
|
||||
splitting LOCAL/WARP/THREAD/GLOBAL/GROUP_REDUCE/UPCAST ranges scrambles the
|
||||
logical<->hardware mapping of hand-written kernels such as this one.
|
||||
|
||||
RDNA4 (gfx12) uses 8-element accumulator fragments, so the 8x4 tile grid needs
|
||||
256 fp32 acc registers per thread -> guaranteed spills (0.85 TF vs 96 TF default on
|
||||
gfx1201). The kernel is right-sized for CDNA4 (fragsz 4 -> 128 acc regs).
|
||||
|
||||
Lane layouts (RDNA4 verified with probing on gfx1201 hardware; CDNA from the mfma docs):
|
||||
CDNA (64 thr/warp, 16x16x32): A/B frag: tile-row = l%16, k = (l//16)*8+i (i in 0..7)
|
||||
RDNA4 (32 thr/warp, 16x16x16): A/B frag: tile-row = l%16, k = (l//16)*4+(i%4)+8*(i//4) (i in 0..7)
|
||||
both: acc frag: CDNA m=(l//16)*4+i (i<4) / RDNA4 m=(l//16)*8+i (i<8), n=l%16
|
||||
|
||||
The RDNA4 fragment k-set {k0..3, k0+8..11} is not contiguous, so on RDNA4 the LDS column layout
|
||||
is block-permuted (4-element blocks within each 16-col group are stored as [0,2,1,3]) making
|
||||
every fragment 8 contiguous halves (one 16B chunk) on both archs; the copy path applies the
|
||||
same permutation.
|
||||
|
||||
NOTE: thread ids come from UOp.special (like mi350x_uop_matmul.py), not an AxisType.LOCAL
|
||||
RANGE. (pm_split_ranges now only splits WEAK/REDUCE/LOOP ranges, so LOCAL ranges would
|
||||
survive too, but UOp.special is the sanctioned way to tag hardware lane ids.)
|
||||
NOTE 2: WMMA operand/accumulator fragments must carry the fragment length in their UOp shape.
|
||||
NOTE 3: swizzled addresses are written in provably-contiguous "base + vector-offset" form,
|
||||
otherwise the devectorizer emits scalar ds_read_u16/ds_write_b16.
|
||||
"""
|
||||
from tinygrad import Tensor, Device, dtypes
|
||||
from tinygrad.uop.ops import UOp, Ops, AxisType, KernelInfo
|
||||
from tinygrad.dtype import AddrSpace
|
||||
from tinygrad.renderer import Estimates
|
||||
from tinygrad.helpers import getenv, cdiv
|
||||
|
||||
# ---- tile shape (identical to gemm_bf16.cpp; HK_TILE=128 overrides for small-LDS devices) ----
|
||||
BLOCK_M = BLOCK_N = getenv("HK_TILE", 256)
|
||||
K_STEP = 64
|
||||
WARPS_M, WARPS_N = 2, 4
|
||||
NUM_WARPS = WARPS_M * WARPS_N # 8
|
||||
WARP_TILE_M, WARP_TILE_N = BLOCK_M // WARPS_M, BLOCK_N // WARPS_N # 128 x 64 (64 x 32 at HK_TILE=128)
|
||||
|
||||
def arch_params(arch:str):
|
||||
is_cdna = arch.startswith("gfx9")
|
||||
if is_cdna: # CDNA mfma 16x16x32 bf16: in-frag 8, acc 4 (16,16,16 on the acc side)
|
||||
return dict(warp_threads=64, dims=(16,16,32), frag_in=8, frag_out=4,
|
||||
acc_m=lambda l, i: (l//16)*4 + i, kperm=None, copy_vec=8)
|
||||
# RDNA4 wmma 16x16x16 (wave32, gfx12 layout): in-frag 8 (permuted in LDS), acc 8
|
||||
return dict(warp_threads=32, dims=(16,16,16), frag_in=8, frag_out=8,
|
||||
acc_m=lambda l, i: (l//16)*8 + i, kperm=(0,2,1,3), copy_vec=4)
|
||||
|
||||
# ---- kittens st_16x32_s swizzle (byte offset: off ^ (((off % 1024) >> 9) << 5)) ----
|
||||
# halves-index form: within a 1024B (16x32) subtile, halves-index bit4 ^= row bit3,
|
||||
# written in "base + vector-offset" form so the devectorizer can prove contiguity.
|
||||
def st_half_base(r, c, tile_cols:int):
|
||||
"""swizzled halves-index pre-vector-offset; c is the logical (permuted) column, 4-aligned."""
|
||||
subtile_id = (r//16) * (tile_cols//32) + (c//32)
|
||||
r16 = r % 16
|
||||
flip = (r16 >> 3) & 1
|
||||
return subtile_id*512 + r16*32 + (((c % 32) >> 2) ^ (flip << 2)) * 4
|
||||
|
||||
def hk_bf16_gemm_kernel(C:UOp, A:UOp, B:UOp, *, arch:str, stages:int=1) -> UOp:
|
||||
"""C = A @ B^T ; A is (M,K), B is (N,K), C is (M,N). HipKittens tile shape."""
|
||||
M, K = A.shape
|
||||
N, K2 = B.shape
|
||||
assert K == K2 and A.dtype == B.dtype == dtypes.bfloat16 and C.dtype == dtypes.bfloat16
|
||||
assert not (M % BLOCK_M or N % BLOCK_N or K % K_STEP), f"dims must be multiples of {(BLOCK_M, BLOCK_N, K_STEP)}"
|
||||
|
||||
ap = arch_params(arch)
|
||||
warp_threads, dims = ap["warp_threads"], ap["dims"]
|
||||
FRAG_IN, FRAG_OUT, kperm, acc_m, CPV = ap["frag_in"], ap["frag_out"], ap["kperm"], ap["acc_m"], ap["copy_vec"]
|
||||
NUM_THREADS = NUM_WARPS * warp_threads
|
||||
TC_M, TC_N, TC_K = dims
|
||||
MT, NT = WARP_TILE_M // TC_M, WARP_TILE_N // TC_N # 8, 4 tiles per warp
|
||||
# permute 4-half blocks within each 16-col group (RDNA4: [0,2,1,3] = swap middle blocks)
|
||||
def perm_col(c):
|
||||
if kperm is None: return c
|
||||
return (c & ~15) | ((((c>>2) & 1) << 1 | ((c>>3) & 1)) << 2) | (c & 3)
|
||||
|
||||
bx, by = UOp.special(N//BLOCK_N, "gidx0"), UOp.special(M//BLOCK_M, "gidx1")
|
||||
lane = UOp.special(warp_threads, "lidx0")
|
||||
warp = UOp.special(NUM_WARPS, "lidx1")
|
||||
warp_row, warp_col = warp // WARPS_N, warp % WARPS_N
|
||||
tid = warp*warp_threads + lane
|
||||
|
||||
def smem(slot) -> UOp: return UOp.placeholder((BLOCK_M*K_STEP,), dtypes.bfloat16, slot, AddrSpace.LOCAL)
|
||||
As = [smem(2*i) for i in range(stages)]
|
||||
Bs = [smem(2*i+1) for i in range(stages)]
|
||||
|
||||
# per-warp accumulator: (MT x NT) 16x16 tiles of FRAG_OUT fp32 per thread
|
||||
acc = UOp.placeholder((MT, NT, FRAG_OUT), dtypes.float32, 12, AddrSpace.REG)
|
||||
acc = acc.after(acc.store(acc.const_like(0.0))) # FA-style init: self-store, keeps the value flow loop-carried
|
||||
|
||||
# global -> LDS copy: CPV halves per op (16B on CDNA, 8B on RDNA4), thread-major coalescing
|
||||
OPS_PER_TILE = BLOCK_M*K_STEP//CPV
|
||||
OPR = K_STEP//CPV
|
||||
def copy_tile(dst:UOp, src:UOp, base_row:UOp, base_col:UOp, slot:int) -> UOp:
|
||||
ir = UOp.range(cdiv(OPS_PER_TILE, NUM_THREADS), slot, AxisType.LOOP)
|
||||
j = UOp.range(CPV, slot+1, AxisType.UPCAST)
|
||||
chunk = ir*NUM_THREADS + tid
|
||||
r, cb = chunk // OPR, chunk % OPR # row, 4/8-col block
|
||||
return dst[st_half_base(r, perm_col(cb*CPV), K_STEP) + j].store(src[base_row + r, base_col + cb*CPV + j]).end(ir, j)
|
||||
|
||||
def load_stage(sidx:int, ko, slot:int, barrier:bool) -> tuple[UOp, UOp]:
|
||||
A_r = copy_tile(As[sidx], A, by*BLOCK_M, ko*K_STEP, slot)
|
||||
B_r = copy_tile(Bs[sidx], B, bx*BLOCK_N, ko*K_STEP, slot+10)
|
||||
bar = UOp.barrier(A_r, B_r) if barrier else UOp.group(A_r, B_r)
|
||||
return As[sidx].after(bar), Bs[sidx].after(bar)
|
||||
|
||||
# ---- pipelined path (stages=2) ----
|
||||
NIT = cdiv(OPS_PER_TILE, NUM_THREADS) # copy ops per thread per tile
|
||||
def setprio(n:int, slot:int) -> UOp:
|
||||
"""__builtin_amdgcn_s_setprio(n), like gemm_bf16.cpp: raise warp priority for the mma phase
|
||||
so global/LDS traffic of the other waves doesn't starve issue slots."""
|
||||
# distinct src slot per call site so identical-priority instructions at different k-tiles
|
||||
# don't get UOp-hash-deduped into one placement (s_setprio is position-sensitive)
|
||||
return UOp(Ops.CUSTOMI, dtypes.void, src=(UOp.const(dtypes.weakint, slot), UOp.const(dtypes.weakint, n)),
|
||||
arg="__builtin_amdgcn_s_setprio({1}); // {0}")
|
||||
|
||||
def gload_write_tile(dst:UOp, src:UOp, base_row:UOp, kt, slot:int) -> UOp:
|
||||
"""store one global tile into an LDS slot (loads and stores share the vec range j)."""
|
||||
j = UOp.range(CPV, slot, AxisType.UPCAST)
|
||||
def one(ir:int) -> UOp:
|
||||
chunk = ir*NUM_THREADS + tid
|
||||
r, cb = chunk // OPR, chunk % OPR
|
||||
return dst[st_half_base(r, perm_col(cb*CPV), K_STEP) + j].store(src[base_row + r, kt*K_STEP + cb*CPV + j])
|
||||
return UOp.group(*[one(ir) for ir in range(NIT)]).end(j)
|
||||
|
||||
def compute(acc:UOp, A_l:UOp, B_l:UOp, afters:tuple[UOp, ...], pred:UOp|None=None, aoff:UOp=None, boff:UOp=None) -> UOp:
|
||||
"""One K_STEP=64 iteration: (K_STEP//TC_K) k-chunks unrolled, (MT x NT) mma each, like the kittens main loop.
|
||||
|
||||
pred (optional): a loop-range condition; accumulator stores are predicated on it so the
|
||||
first (fill) iteration of a software pipeline can run the body with garbage LDS contents
|
||||
without contaminating the accumulator."""
|
||||
arow = warp_row*WARP_TILE_M + lane % 16 # fragment tile row in the LDS tile (m)
|
||||
brow = warp_col*WARP_TILE_N + lane % 16 # (n)
|
||||
ja = UOp.range(FRAG_IN, 701, AxisType.UPCAST)
|
||||
jb = UOp.range(FRAG_IN, 702, AxisType.UPCAST)
|
||||
acc_k = acc.after(*afters) if afters else acc
|
||||
last_store = None
|
||||
# in the permuted layout every fragment is 8 contiguous halves starting at an 8-aligned col
|
||||
for kk in range(K_STEP//TC_K):
|
||||
cc = kk*(TC_K//FRAG_IN) + (lane // 16) # fragment chunk col (8 halves)
|
||||
oa, ob = (aoff, boff) if aoff is not None else (None, None)
|
||||
a_frags = [A_l[st_half_base(arow + mt*16, cc*8, K_STEP) + ja].contract(ja) if oa is None else
|
||||
A_l[oa + st_half_base(arow + mt*16, cc*8, K_STEP) + ja].contract(ja) for mt in range(MT)]
|
||||
b_frags = [B_l[st_half_base(brow + nt*16, cc*8, K_STEP) + jb].contract(jb) if ob is None else
|
||||
B_l[ob + st_half_base(brow + nt*16, cc*8, K_STEP) + jb].contract(jb) for nt in range(NT)]
|
||||
for mt in range(MT):
|
||||
for nt in range(NT):
|
||||
cur = acc_k[mt, nt]
|
||||
out = UOp.wmma(a_frags[mt], b_frags[nt], cur, dims, 'AMD', warp_threads)
|
||||
if pred is not None: out = pred.where(cur, out)
|
||||
last_store = acc_k[mt, nt].store(out)
|
||||
acc_k = acc_k.after(last_store)
|
||||
return last_store
|
||||
|
||||
# ---- K loop ----
|
||||
amt = cdiv(K, K_STEP)
|
||||
_stages = stages
|
||||
if _stages == 1:
|
||||
ko = UOp.range(amt, 600, AxisType.LOOP)
|
||||
A_l, B_l = load_stage(0, ko, 100, barrier=True)
|
||||
last = compute(acc, A_l, B_l, afters=(ko,))
|
||||
acc = acc.after(last.barrier().end(ko))
|
||||
else:
|
||||
# Double-buffered pipeline on FA/gemm_fragment conventions: each LDS buffer is a
|
||||
# (2, tile) placeholder indexed by symbolic parity (ko % 2) -- no static slot choice,
|
||||
# no duplicate static stores (memory_coalescing-safe), no fill iteration, no predication.
|
||||
def smem2(slot) -> UOp: return UOp.placeholder((2*BLOCK_M*K_STEP,), dtypes.bfloat16, slot, AddrSpace.LOCAL)
|
||||
A_l, B_l = smem2(0), smem2(1)
|
||||
|
||||
TILE_ELEMS = BLOCK_M * K_STEP
|
||||
def copy_stage(dst:UOp, slot_off:UOp, src:UOp, base_row:UOp, kt, slot:int) -> UOp:
|
||||
"""store one global tile into dst + slot_off (flat element offset -- slot_off = parity*TILE_ELEMS).
|
||||
|
||||
Each thread's 8-element chunk is a single buffer_load_lds direct-to-LDS instruction
|
||||
(the kittens '... offen lds' fill path), emitted via Ops.CUSTOMI so it bypasses the
|
||||
devectorizer (a SHRINK store of a SHRINK load gets expanded to scalars before render)."""
|
||||
ir = UOp.range(cdiv(OPS_PER_TILE, NUM_THREADS), slot+1, AxisType.LOOP)
|
||||
chunk = ir*NUM_THREADS + tid
|
||||
r, cc = chunk // OPR, chunk % OPR
|
||||
if getenv("HK_G2L", 0) == 3:
|
||||
# direct-to-LDS fill (kittens '... offen lds' path): the hardware writes each lane's
|
||||
# chunk to the lane-linear LDS address (M0 + lane*size), so the swizzle is moved to
|
||||
# the GLOBAL side: lane q's 16B chunk fetches the matrix element that st_half_base
|
||||
# maps to the tile-linear position q. Verified bijective; the fragment-read layout
|
||||
# (and therefore the read swizzle) is unchanged.
|
||||
chunk = ir*NUM_THREADS + tid
|
||||
p_ = chunk * CPV # tile-linear halves position of this lane's chunk
|
||||
sub = p_ >> 9 # 16x32 subtile id (512 halves)
|
||||
r16 = (p_ & 511) >> 5
|
||||
flip = (r16 >> 3) & 1
|
||||
cb = ((p_ & 31) >> 2) ^ (flip << 2)
|
||||
r_ = (sub >> 1) * 16 + r16
|
||||
c_ = cb*4 + (sub & 1) * 32 # global column (8-aligned)
|
||||
off_g = (base_row + r_) * K + kt*K_STEP + c_
|
||||
lds_el = slot_off + ir*NUM_THREADS*CPV # elements; &buf[el*8] = chunk base byte addr
|
||||
# feed the raw PARAM (unwrapping the scheduler's RESHAPE view, which would otherwise
|
||||
# live unfused into the program and fail spec: 'movement ops not allowed in programs').
|
||||
prm = src
|
||||
while prm.op is not Ops.PARAM and len(prm.src): prm = prm.src[0]
|
||||
nbytes = prm.max_numel() * prm.dtype.itemsize
|
||||
gname = f"data{prm.arg.slot}_{prm.max_numel()}"
|
||||
return UOp(Ops.CUSTOMI, dtypes.void, src=(prm, dst, lds_el, off_g),
|
||||
arg=(f"llvm_amdgcn_raw_buffer_load_lds(make_srsrc_((void*){gname}, {nbytes}), "
|
||||
f"(as3_uint32_ptr)(&({{1}}[({{2}})])), {CPV*2}, ((unsigned)({{3}}))*2U, 0, 0, 0);")).end(ir)
|
||||
# default: elementwise global->LDS stores
|
||||
off_l = slot_off + st_half_base(r, perm_col(cc*CPV), K_STEP)
|
||||
off_g = (base_row + r) * K + kt*K_STEP + cc*CPV
|
||||
j = UOp.range(CPV, slot, AxisType.UPCAST)
|
||||
return dst[off_l + j].store(src[base_row + r, kt*K_STEP + cc*CPV + j]).end(ir, j)
|
||||
|
||||
ZERO = UOp.const(dtypes.weakint, 0)
|
||||
# prologue: tile 0 into slot 0 of both buffers, barrier before first read
|
||||
g0 = UOp.group(copy_stage(A_l, ZERO, A, by*BLOCK_M, ZERO, 100),
|
||||
copy_stage(B_l, ZERO, B, bx*BLOCK_N, ZERO, 110))
|
||||
bar0 = UOp.barrier(g0)
|
||||
# Double-buffered pipeline: slot ko%2 holds k-tile ko; the prefetch copy of tile ko+1
|
||||
# (into the other slot) rides IN FRONT of the wmma's and overlaps them; one barrier per
|
||||
# k-tile hand-off covers write(ko)->read(ko+1) [and read(ko)->write(ko+1) is closed by
|
||||
# the ko-1 barrier already]. The parities/offsets are static python constants when
|
||||
# HK_UNROLL (default on): straight-line like the kittens main loop; the rolled variant
|
||||
# uses pm_split_ranges to split the ko LOOP range at the (ko % 2) boundary.
|
||||
if getenv("HK_UNROLL", 1) and amt % (UN := getenv("HK_UNROLL_U", 8)) == 0:
|
||||
# outer rolled loop of amt//U iterations, U python-unrolled k-tiles inside: nearly the
|
||||
# kittens straight-line node shape (one barrier per k-tile) at a fraction of the
|
||||
# full-unroll uop count (full unroll of amt=64 needs ~12 min of schedule time; U=8
|
||||
# keeps every tile's prefetch + compute + hand-off barrier but stays seconds).
|
||||
ko_o = UOp.range(amt // UN, 600, AxisType.LOOP)
|
||||
pa, pb = A_l.after(bar0, ko_o), B_l.after(bar0, ko_o)
|
||||
for i in range(UN):
|
||||
kt = ko_o * UN + i
|
||||
pr, pn = (i % 2) * TILE_ELEMS, ((i + 1) % 2) * TILE_ELEMS
|
||||
kt_next = UOp.minimum(kt + 1, amt - 1)
|
||||
ga0 = UOp.group(copy_stage(pa, UOp.const(dtypes.weakint, pn), A, by*BLOCK_M, kt_next, 300 + 4*i),
|
||||
copy_stage(pb, UOp.const(dtypes.weakint, pn), B, bx*BLOCK_N, kt_next, 302 + 4*i))
|
||||
sp_hi = setprio(1, 300 + 4*i) # kittens: raised prio for the mma phase
|
||||
last = compute(acc, pa, pb, afters=(ko_o, sp_hi), aoff=UOp.const(dtypes.weakint, pr), boff=UOp.const(dtypes.weakint, pr))
|
||||
sp_lo = setprio(0, 301 + 4*i)
|
||||
handoff = UOp.group(last, sp_lo, ga0).barrier()
|
||||
acc = acc.after(handoff)
|
||||
pa, pb = A_l.after(handoff, ga0), B_l.after(handoff, ga0)
|
||||
acc = acc.after(UOp.group(handoff).end(ko_o))
|
||||
else:
|
||||
ko = UOp.range(amt, 600, AxisType.LOOP)
|
||||
pr, pn = ko % 2, (ko+1) % 2 # slot of the tile being computed / being prefetched
|
||||
kt_next = UOp.minimum(ko+1, amt-1) # clamped tail prefetch (its data is unused)
|
||||
pa, pb = A_l.after(bar0, ko), B_l.after(bar0, ko)
|
||||
ga = UOp.group(copy_stage(pa, pn*TILE_ELEMS, A, by*BLOCK_M, kt_next, 130),
|
||||
copy_stage(pb, pn*TILE_ELEMS, B, bx*BLOCK_N, kt_next, 140))
|
||||
sp_hi = setprio(1, 150)
|
||||
last = compute(acc, pa, pb, afters=(ko, sp_hi), aoff=pr*TILE_ELEMS, boff=pr*TILE_ELEMS)
|
||||
acc = acc.after(UOp.group(last, setprio(0, 151), ga).barrier().end(ko))
|
||||
|
||||
# ---- epilogue: per-thread fragment stores, cast to bf16 (scalar per fragment element) ----
|
||||
mt, nt = UOp.range(MT, 801, AxisType.LOOP), UOp.range(NT, 802, AxisType.LOOP)
|
||||
def store_i(i:int) -> UOp:
|
||||
crow = by*BLOCK_M + warp_row*WARP_TILE_M + mt*16 + acc_m(lane, i)
|
||||
ccol = bx*BLOCK_N + warp_col*WARP_TILE_N + nt*16 + lane % 16
|
||||
return C[crow, ccol].store(acc[mt, nt, i].cast(dtypes.bfloat16))
|
||||
out_st = UOp.group(*[store_i(i) for i in range(FRAG_OUT)])
|
||||
return out_st.end(mt, nt).sink(arg=KernelInfo(name="hk_bf16_gemm",
|
||||
estimates=Estimates(ops=2*M*N*K, mem=(M*K+N*K+M*N)*2)))
|
||||
|
||||
def hk_bf16_gemm_tiny(a:Tensor, b:Tensor, stages:int=1) -> Tensor:
|
||||
"""C = a @ b.T for bf16 a (M,K), b (N,K) with the HipKittens-shaped tinygrad kernel."""
|
||||
arch = Device[a.device].renderer.target.arch
|
||||
c = Tensor.empty(a.shape[0], b.shape[0], dtype=dtypes.bfloat16, device=a.device)
|
||||
return c.custom_kernel(a, b, fxn=lambda C, A, B: hk_bf16_gemm_kernel(C, A, B, arch=arch, stages=stages))[0]
|
||||
|
||||
if __name__ == "__main__":
|
||||
import numpy as np
|
||||
from tinygrad import Device
|
||||
M = N = K = 512
|
||||
# exact test: B = identity -> C must equal A bit-exactly
|
||||
a = Tensor.randn(M, K, dtype=dtypes.bfloat16).contiguous()
|
||||
bid = Tensor(np.eye(K, N, dtype=np.float32), dtype=dtypes.bfloat16).contiguous()
|
||||
cid = hk_bf16_gemm_tiny(a, bid, stages=getenv("STAGES", 1)).realize()
|
||||
assert np.array_equal(cid.float().numpy(), a.float().numpy()), "identity test failed"
|
||||
# real test: bf16 gemm vs fp32 reference, rounding-level noise
|
||||
b = Tensor.randn(N, K, dtype=dtypes.bfloat16).contiguous()
|
||||
c = hk_bf16_gemm_tiny(a, b, stages=getenv("STAGES", 1)).realize()
|
||||
ref = (a @ b.T).float().realize()
|
||||
err = (c.float() - ref).abs().max().item()
|
||||
print(f"identity exact, random max err: {err:.5f}")
|
||||
|
||||
# ---- benchmark mode: kittens hk_bf16_gemm vs tinygrad stages={1,2} vs the default scheduled gemm ----
|
||||
# run on real hardware with: DEV=AMD:HIP:gfx950 DEBUG=2 HK_BENCH=1 python extra/gemm/hk_gemm_frag.py
|
||||
# sizes via HK_SIZES="2048x2048x2048,4096x4096x4096" (default 2048 cubed), iteration count via ITERS=20.
|
||||
# timings come from GlobalCounters.time_sum_s (sum of kernel times; same source as the DEBUG=2 'tm' column).
|
||||
if getenv("HK_BENCH"):
|
||||
from tinygrad.helpers import GlobalCounters
|
||||
from extra.gemm.cdna_asm_gemm import asm_gemm
|
||||
from tinygrad import Device
|
||||
dev, iters, warm = Device.DEFAULT, getenv("ITERS", 20), 3
|
||||
arch = Device[dev].renderer.target.arch
|
||||
assert arch.startswith("gfx9"), "CDNA only"
|
||||
def bench(label:str, fn, M:int, N:int, K:int) -> float:
|
||||
try:
|
||||
for _ in range(warm): fn()
|
||||
Device[dev].synchronize()
|
||||
GlobalCounters.reset()
|
||||
import time
|
||||
t0 = time.perf_counter()
|
||||
for _ in range(iters): fn()
|
||||
Device[dev].synchronize()
|
||||
wall = time.perf_counter() - t0
|
||||
except Exception as e:
|
||||
print(f" {label:32s} unsupported/failed: {type(e).__name__}: {e}")
|
||||
return float('nan')
|
||||
# prefer kernel-side time (GlobalCounters matches the DEBUG=2 'tm' column); fall back to wall clock
|
||||
ms = (GlobalCounters.time_sum_s if GlobalCounters.time_sum_s > 0 else wall) * 1e3 / iters
|
||||
tf = 2*M*N*K / (ms * 1e-3) / 1e12
|
||||
print(f" {label:32s} {ms:9.3f} ms {tf:8.1f} TFLOPS")
|
||||
return tf
|
||||
for (M, N, K) in [tuple(map(int, s.split("x"))) for s in getenv("HK_SIZES", "2048x2048x2048").split(",")]:
|
||||
print(f" size ({M},{N},{K}), grid {M//BLOCK_M}x{N//BLOCK_N} WGs, amt={K//K_STEP} k-tiles/WG")
|
||||
np.random.seed(0)
|
||||
An, Bn = np.random.randn(M, K), np.random.randn(K, N)
|
||||
A = Tensor(An, dtype=dtypes.bfloat16).contiguous().realize() # (M,K)
|
||||
Bk = Tensor(Bn, dtype=dtypes.bfloat16).contiguous().realize() # (K,N) for kittens
|
||||
Bt = Bk.T.contiguous().realize() # (N,K) for ours
|
||||
tf_kc = bench("kittens hk_bf16_gemm (asm_gemm)", lambda: asm_gemm(A, Bk).realize(), M, N, K)
|
||||
tf_s1 = bench("tiny stages=1", lambda: hk_bf16_gemm_tiny(A, Bt, stages=1).realize(), M, N, K)
|
||||
tf_s2 = bench("tiny stages=2", lambda: hk_bf16_gemm_tiny(A, Bt, stages=2).realize(), M, N, K)
|
||||
tf_df = bench("tinygrad default (a @ Bt.T)", lambda: (A @ Bt.T).realize(), M, N, K)
|
||||
err = (hk_bf16_gemm_tiny(A, Bt, stages=2).float() - asm_gemm(A, Bk).float()).abs().max().item()
|
||||
print(f" correctness tiny-s2 vs kittens max diff: {err:.5f}")
|
||||
for nm, tf in [("s1", tf_s1), ("s2", tf_s2), ("default", tf_df)]:
|
||||
if tf == tf and tf_kc == tf_kc: print(f" tiny {nm:8s}/kittens: {tf/tf_kc:6.2%}")
|
||||
# match the real HipKittens hk_bf16_gemm on the (mock) CDNA4 emulator at small sizes.
|
||||
# run from the repo root with: DEV=MOCK+AMD:HIP:gfx950 HK_COMPARE=1 python extra/gemm/hk_gemm_frag.py
|
||||
if getenv("HK_COMPARE"):
|
||||
from extra.gemm.cdna_asm_gemm import asm_gemm
|
||||
assert Device[Device.DEFAULT].renderer.target.arch.startswith("gfx950"), "needs CDNA4 (mock emulator or hardware)"
|
||||
def compare(M:int, N:int, K:int, seed:int=0, identity:bool=False):
|
||||
np.random.seed(seed)
|
||||
An = np.random.randn(M, K)
|
||||
Bn = np.eye(K, N) if identity else np.random.randn(K, N) # (K,N) as expected by asm_gemm
|
||||
A = Tensor(An, dtype=dtypes.bfloat16).contiguous()
|
||||
B = Tensor(Bn, dtype=dtypes.bfloat16).contiguous() # (K,N) for asm_gemm
|
||||
c_hkc = asm_gemm(A, B).realize().float().numpy() # real HipKittens hk_bf16_gemm
|
||||
c_hkt = hk_bf16_gemm_tiny(A, B.T.contiguous(), stages=getenv("STAGES", 2)).realize().float().numpy()
|
||||
ref64 = A.float().numpy().astype(np.float64) @ B.float().numpy()
|
||||
tag = "ident" if identity else "rand "
|
||||
print(f"({M},{N},{K}) {tag}: tiny-vs-kittens {np.abs(c_hkt-c_hkc).max():9.6f} "
|
||||
f"tiny-vs-fp64 {np.abs(c_hkt-ref64).max():9.6f} kittens-vs-fp64 {np.abs(c_hkc-ref64).max():9.6f}")
|
||||
assert np.abs(c_hkt - ref64).max() < 0.26, "tiny kernel must match fp64 at rounding level"
|
||||
assert np.abs(c_hkt - c_hkc).max() < 0.51, "tiny kernel must match hipkittens"
|
||||
# NOTE: hk_bf16_gemm requires K % 128 == 0 (its prologue+epilogue unconditionally touch
|
||||
# k-tiles num_tiles-1 and num_tiles-2); at other K it reads wrong-but-in-bounds global
|
||||
# memory on the emulator and on real hardware, so only K%128==0 sizes are checked here.
|
||||
compare(256, 256, 128, seed=1) # single workgroup
|
||||
compare(256, 256, 256, seed=2)
|
||||
compare(512, 512, 128, seed=3) # multi workgroup
|
||||
compare(256, 256, 128, identity=True) # bit-exact check
|
||||
@@ -29,7 +29,7 @@ TID_SIZE = WARPGROUP_SIZE*WARP_SIZE
|
||||
|
||||
def copy(dest:UOp, src:UOp, rng:int, set=False, upcast=()):
|
||||
assert dest.shape == src.shape
|
||||
rngs = [UOp.range(s, rng+i, AxisType.UPCAST if i in upcast else AxisType.WEAK) for i,s in enumerate(src.shape)]
|
||||
rngs = [UOp.range(s, rng+i, AxisType.UPCAST if i in upcast else AxisType.LOOP) for i,s in enumerate(src.shape)]
|
||||
copy = dest[*rngs].store(src[*rngs]).end(*rngs)
|
||||
return dest.after(copy) if set else copy
|
||||
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import functools, pathlib
|
||||
from tinygrad import Tensor, dtypes
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo
|
||||
from tinygrad.renderer import Estimates
|
||||
from tinygrad.runtime.support.compiler_amd import HIPCCCompiler
|
||||
from extra.gemm.cdna_asm_gemm import quantize_mxfp8, _mx_block_scale, _mx_block_scale_3d
|
||||
|
||||
@functools.cache
|
||||
def custom_hk_grouped_mxfp8_gemm(C:UOp, A:UOp, B:UOp, scale_A:UOp, scale_B:UOp, *extra:UOp, dname:str, n_experts:int) -> UOp:
|
||||
M, K = A.shape
|
||||
E, N, K2 = B.shape
|
||||
assert K == K2, f"{A.shape} {B.shape}"
|
||||
assert E == n_experts, f"{E} != {n_experts}"
|
||||
threads = UOp.special(64 * 8, "lidx0")
|
||||
workgroups = UOp.special((M // 256) * (N // 256), "gidx0")
|
||||
sink_inputs = (C.base, A.base, B.base, scale_A.base, scale_B.base, extra[0].base, extra[1].base, extra[2].base, threads, workgroups)
|
||||
sink = UOp.sink(*sink_inputs,
|
||||
arg=KernelInfo(f"hk_grouped_mxfp8_gemm_{E}_{M}_{N}_{K}",
|
||||
estimates=Estimates(ops=2*M*N*K, mem=(M*K+E*N*K)*A.dtype.itemsize+M*N*C.dtype.itemsize)))
|
||||
kittens_path = pathlib.Path(__file__).parent.parent/"thunder"/"amd"
|
||||
src = (kittens_path/"grouped_mxfp8_gemm.cpp").read_text()
|
||||
lib = HIPCCCompiler("gfx950", [f"-I{(kittens_path/'include').as_posix()}", "-std=c++20", "-DKITTENS_CDNA4", "-ffast-math",
|
||||
"-DHIP_ENABLE_WARP_SYNC_BUILTINS", f"-DGEMM_M={M}", f"-DGEMM_N={N}", f"-DGEMM_K={K}",
|
||||
f"-DGEMM_E={E}"]).compile_cached(src)
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=(*sink.src, sink)), UOp(Ops.SOURCE, arg=src),
|
||||
UOp(Ops.BINARY, arg=lib)))
|
||||
|
||||
@functools.cache
|
||||
def custom_hk_grouped_mxfp8_wgrad(C:UOp, A:UOp, B:UOp, scale_A:UOp, scale_B:UOp, expert_off:UOp, *, dname:str, n_experts:int) -> UOp:
|
||||
N, M = A.shape
|
||||
K, M2 = B.shape
|
||||
assert M == M2, f"{A.shape} {B.shape}"
|
||||
E = n_experts
|
||||
threads = UOp.special(64 * 8, "lidx0")
|
||||
workgroups = UOp.special(E * (N // 256) * (K // 256), "gidx0")
|
||||
sink = UOp.sink(C.base, A.base, B.base, scale_A.base, scale_B.base, expert_off.base, threads, workgroups,
|
||||
arg=KernelInfo(f"hk_grouped_mxfp8_wgrad_{E}_{M}_{N}_{K}",
|
||||
estimates=Estimates(ops=2*M*N*K, mem=(N*M+K*M)*A.dtype.itemsize+E*N*K*C.dtype.itemsize)))
|
||||
kittens_path = pathlib.Path(__file__).parent.parent/"thunder"/"amd"
|
||||
src = (kittens_path/"grouped_mxfp8_wgrad.cpp").read_text()
|
||||
lib = HIPCCCompiler("gfx950", [f"-I{(kittens_path/'include').as_posix()}", "-std=c++20", "-DKITTENS_CDNA4", "-ffast-math",
|
||||
"-DHIP_ENABLE_WARP_SYNC_BUILTINS", f"-DWGRAD_M={M}", f"-DWGRAD_N={N}", f"-DWGRAD_K={K}",
|
||||
f"-DWGRAD_E={E}"]).compile_cached(src)
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=(*sink.src, sink)), UOp(Ops.SOURCE, arg=src),
|
||||
UOp(Ops.BINARY, arg=lib)))
|
||||
|
||||
def grouped_mx_wgrad(g:Tensor, xg:Tensor, expert_off:Tensor, n_experts:int) -> Tensor:
|
||||
from extra.llama_kernels.transpose_quantize_mxfp8 import transpose_quantize_mxfp8
|
||||
M, N = g.shape
|
||||
M2, K = xg.shape
|
||||
assert M == M2, f"{g.shape} {xg.shape}"
|
||||
assert M % 128 == 0 and N % 256 == 0 and K % 256 == 0, f"wgrad needs M%128,N%256,K%256, got {g.shape} {xg.shape}"
|
||||
gT, _, g_si = transpose_quantize_mxfp8(g.contiguous())
|
||||
xT, _, x_si = transpose_quantize_mxfp8(xg.contiguous())
|
||||
dname = (g.device[0] if isinstance(g.device, tuple) else g.device).split(":")[0]
|
||||
is_multi = isinstance(g.device, tuple)
|
||||
inv = Tensor.invalids(1, n_experts * N, K, dtype=dtypes.bfloat16, device=g.device)
|
||||
out = Tensor(inv.uop.unshard(0), device=g.device) if is_multi else inv
|
||||
out = Tensor.custom_kernel(out, gT, xT, g_si, x_si, expert_off,
|
||||
fxn=functools.partial(custom_hk_grouped_mxfp8_wgrad, dname=dname, n_experts=n_experts))[0]
|
||||
out = out.sum(0) if is_multi else out.squeeze(0)
|
||||
return out.reshape(n_experts, N, K)
|
||||
|
||||
def mx_pack_3d(e8:Tensor) -> Tensor:
|
||||
E, rows, scale_K = e8.shape
|
||||
return e8.reshape(E, rows, scale_K // 4, 4).bitcast(dtypes.uint32).reshape(E, rows, scale_K // 4).permute(0, 2, 1).contiguous()
|
||||
|
||||
@functools.cache
|
||||
def custom_grouped_mx_gemm_bw(gradient:UOp, kernel:UOp, w_stored:bool=False) -> tuple:
|
||||
inputs = kernel.src[1:]
|
||||
aq = Tensor(inputs[1], device=inputs[1].device)
|
||||
bq = Tensor(inputs[2], device=inputs[2].device)
|
||||
ae8 = Tensor(inputs[5], device=inputs[5].device)
|
||||
be8 = Tensor(inputs[6], device=inputs[6].device)
|
||||
E, N = bq.shape[0], bq.shape[1]
|
||||
M, K = aq.shape
|
||||
g = Tensor(gradient, device=aq.device).reshape(M, N).cast(dtypes.bfloat16)
|
||||
x_phys = (aq.cast(dtypes.bfloat16) * _mx_block_scale(ae8).cast(dtypes.bfloat16))
|
||||
w_phys = (bq.cast(dtypes.bfloat16) * _mx_block_scale_3d(be8).cast(dtypes.bfloat16))
|
||||
expert_off = Tensor(inputs[7], device=inputs[7].device)
|
||||
grad_x = grouped_mx_gemm(g, w_phys.transpose(1, 2), expert_off)
|
||||
grad_w = grouped_mx_wgrad(g, x_phys, expert_off, E)
|
||||
grad_xq = grad_x * _mx_block_scale(ae8).cast(dtypes.bfloat16)
|
||||
grad_wq = grad_w.contiguous() if w_stored else (grad_w * _mx_block_scale_3d(be8).cast(dtypes.bfloat16)).contiguous()
|
||||
return (None, grad_xq.uop, grad_wq.uop) + tuple(None for _ in inputs[3:])
|
||||
|
||||
_grouped_bw_stored = functools.partial(custom_grouped_mx_gemm_bw, w_stored=True)
|
||||
|
||||
def grouped_mx_gemm(x:Tensor, w:Tensor|tuple[Tensor, Tensor], expert_off:Tensor) -> Tensor:
|
||||
if (pre_quantized := isinstance(w, tuple)):
|
||||
w_q, w_e8 = w
|
||||
E, N, K2 = w_q.shape
|
||||
else:
|
||||
E, N, K2 = w.shape
|
||||
M, K = x.shape
|
||||
assert K == K2, f"shape mismatch {x.shape} {w.shape}"
|
||||
assert M % 256 == 0 and N % 256 == 0 and K % 128 == 0, f"grouped mxfp8 needs M%256,N%256,K%128, got {x.shape} {w.shape}"
|
||||
dname = (x.device[0] if isinstance(x.device, tuple) else x.device).split(":")[0]
|
||||
x_q, x_e8, x_si = quantize_mxfp8(x)
|
||||
if not pre_quantized: w_q, w_e8, _ = quantize_mxfp8(w)
|
||||
w_si = mx_pack_3d(w_e8)
|
||||
xe_in, out_shape = x_e8.reshape(M, K // 32), (M, N)
|
||||
if isinstance(x.device, tuple) and (row_axis := x.uop.axis) is not None:
|
||||
ndev = len(x.device)
|
||||
out = Tensor(Tensor.invalids(*(s // ndev if i == row_axis else s for i, s in enumerate(out_shape)),
|
||||
dtype=dtypes.bfloat16, device=x.device).uop.unshard(row_axis), device=x.device)
|
||||
else:
|
||||
out = Tensor.invalids(*out_shape, dtype=dtypes.bfloat16, device=x.device)
|
||||
return Tensor.custom_kernel(out, x_q, w_q, x_si, w_si, xe_in, w_e8, expert_off,
|
||||
fxn=functools.partial(custom_hk_grouped_mxfp8_gemm, dname=dname, n_experts=E),
|
||||
grad_fxn=(_grouped_bw_stored if pre_quantized else custom_grouped_mx_gemm_bw))[0]
|
||||
@@ -1,130 +0,0 @@
|
||||
from tinygrad import Tensor, dtypes
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo, AxisType
|
||||
|
||||
BLOCK_ROW = 256
|
||||
|
||||
def _sharded_invalids(shape:tuple[int, ...], dtype, device) -> Tensor:
|
||||
if isinstance(device, tuple):
|
||||
return Tensor(Tensor.invalids(shape[0] // len(device), *shape[1:], dtype=dtype, device=device).uop.multi(0), device=device)
|
||||
return Tensor.invalids(*shape, dtype=dtype, device=device)
|
||||
|
||||
def _atomic_add(device:str) -> str:
|
||||
return "__hip_atomic_fetch_add({0}, {1}, __ATOMIC_RELAXED, __HIP_MEMORY_SCOPE_AGENT);" if device == "AMD" \
|
||||
else "__atomic_fetch_add({0}, {1}, __ATOMIC_RELAXED);"
|
||||
|
||||
def _blk_for(D:int) -> int:
|
||||
blk = 64
|
||||
while D % blk: blk //= 2
|
||||
return blk
|
||||
|
||||
def _kv_ranges(G, N, D, BLK):
|
||||
g = UOp.range(G, 0)
|
||||
m = UOp.range(N, 1)
|
||||
jo = UOp.range(D // BLK, 2)
|
||||
ji = UOp.range(BLK, 3, AxisType.LOCAL)
|
||||
return g, m, jo * BLK + ji, jo, ji
|
||||
|
||||
def _ggather_fwd_kernel(out:UOp, table:UOp, idx:UOp) -> UOp:
|
||||
G, M, D = out.shape
|
||||
g, m, j, jo, ji = _kv_ranges(G, M, D, _blk_for(D))
|
||||
row = idx.index(g, m).cast(dtypes.weakint)
|
||||
val = table.index(g, row, j).load()
|
||||
return out.index(g, m, j).store(val).end(g, m, jo, ji).sink(
|
||||
arg=KernelInfo(name=f"ggather_fwd_{M}_{D}", opts_to_apply=()))
|
||||
|
||||
def _ggather_zero_kernel(out:UOp) -> UOp:
|
||||
i = UOp.range(out.numel(), 0)
|
||||
return out.flatten().index(i).store(UOp.const(out.dtype, 0.0)).end(i).sink(arg=KernelInfo(name="ggather_zero"))
|
||||
|
||||
def _sharded_zeros(shape:tuple[int, ...], dtype, device) -> Tensor:
|
||||
return Tensor.custom_kernel(_sharded_invalids(shape, dtype, device), fxn=_ggather_zero_kernel)[0]
|
||||
|
||||
def _ggather_bwd(gradient:UOp, kernel:UOp) -> tuple:
|
||||
_, table_u, idx_u = kernel.src[1:4]
|
||||
dev = table_u.device
|
||||
device = (dev[0] if isinstance(dev, tuple) else dev).split(":")[0]
|
||||
G, R, D = table_u.shape
|
||||
gt = _sharded_zeros((G, R, D), dtypes.float32, dev)
|
||||
go = Tensor(gradient, device=dev)
|
||||
atomic_str = _atomic_add(device)
|
||||
def _bwd_kernel(gtab:UOp, gout:UOp, idx:UOp) -> UOp:
|
||||
Gk, M, Dk = gout.shape
|
||||
g, m, j, jo, ji = _kv_ranges(Gk, M, Dk, _blk_for(Dk))
|
||||
row = idx.index(g, m).cast(dtypes.weakint)
|
||||
val = gout.index(g, m, j).load().cast(dtypes.float32)
|
||||
atomic = UOp(Ops.CUSTOM, dtypes.void, (gtab.index(g, row, j), val), arg=atomic_str)
|
||||
return atomic.end(g, m, jo, ji).sink(arg=KernelInfo(name=f"ggather_bwd_{M}_{Dk}", opts_to_apply=()))
|
||||
grad_table = Tensor.custom_kernel(gt, go, Tensor(idx_u, device=dev), fxn=_bwd_kernel)[0]
|
||||
return (None, grad_table.cast(table_u.dtype).uop, None)
|
||||
|
||||
def grouped_gather_rows(table:Tensor, idx:Tensor, n_groups:int) -> Tensor:
|
||||
G, R, D = table.shape
|
||||
M = idx.shape[1]
|
||||
out = _sharded_invalids((G, M, D), table.dtype, table.device)
|
||||
return Tensor.custom_kernel(out, table, idx, fxn=_ggather_fwd_kernel, grad_fxn=_ggather_bwd)[0]
|
||||
|
||||
def _gscatter_fwd_kernel(out:UOp, src:UOp, idx:UOp) -> UOp:
|
||||
G, M, D = out.shape
|
||||
k = idx.shape[1] // src.shape[1]
|
||||
g, m, j, jo, ji = _kv_ranges(G, idx.shape[1], D, _blk_for(D))
|
||||
row = idx.index(g, m).cast(dtypes.weakint)
|
||||
val = src.index(g, (m // k).cast(dtypes.weakint), j).load()
|
||||
return out.index(g, row, j).store(val).end(g, m, jo, ji).sink(
|
||||
arg=KernelInfo(name=f"gscatter_fwd_{idx.shape[1]}_{D}", opts_to_apply=()))
|
||||
|
||||
def _gscatter_bwd(gradient:UOp, kernel:UOp) -> tuple:
|
||||
_, src_u, idx_u = kernel.src[1:4]
|
||||
dev = src_u.device
|
||||
G, T_l, D = src_u.shape
|
||||
k = idx_u.shape[1] // T_l
|
||||
sel = grouped_gather_rows(Tensor(gradient, device=dev), Tensor(idx_u, device=dev), G)
|
||||
return (None, sel.reshape(G, T_l, k, D).sum(2).cast(src_u.dtype).uop, None)
|
||||
|
||||
def grouped_scatter_rows(src:Tensor, idx:Tensor, m_l:int) -> Tensor:
|
||||
G, T_l, D = src.shape
|
||||
zero = _sharded_zeros((G, m_l, D), src.dtype, src.device)
|
||||
return Tensor.custom_kernel(zero, src, idx, fxn=_gscatter_fwd_kernel, grad_fxn=_gscatter_bwd)[0]
|
||||
|
||||
def m_max_for(t_local:int, experts_per_tok:int, n_experts:int) -> int:
|
||||
return (-(-t_local * experts_per_tok // BLOCK_ROW) + n_experts) * BLOCK_ROW
|
||||
|
||||
class Routing:
|
||||
def __init__(self, weights:Tensor, dest_row:Tensor, off:Tensor, m_l:int, n_groups:int, t_local:int):
|
||||
self.weights, self.dest_row = weights, dest_row
|
||||
self.off = off
|
||||
self.m_l, self.n_groups, self.t_local = m_l, n_groups, t_local
|
||||
|
||||
@property
|
||||
def rows_e(self) -> Tensor:
|
||||
G, E = self.off.shape[0], self.off.shape[1] - 1
|
||||
tr = Tensor.arange(self.m_l // BLOCK_ROW, dtype=dtypes.int32).reshape(1, -1, 1) * BLOCK_ROW
|
||||
tr = tr.shard(self.off.device) if isinstance(self.off.device, tuple) else tr.to(self.off.device)
|
||||
tile_e = ((tr >= self.off[:, :E].reshape(G, 1, E)).sum(-1) - 1).cast(dtypes.int32)
|
||||
return tile_e.reshape(-1, 1).expand(-1, BLOCK_ROW).reshape(-1)
|
||||
|
||||
def n_groups_of(t:Tensor) -> int:
|
||||
return len(t.device) if isinstance(t.device, tuple) else 1
|
||||
|
||||
def route(logits:Tensor, experts_per_tok:int, n_experts:int) -> Routing:
|
||||
T, E = logits.shape
|
||||
k, G = experts_per_tok, n_groups_of(logits)
|
||||
assert T % G == 0, f"tokens {T} must split across {G} devices"
|
||||
T_l, m_l = T // G, m_max_for(T // G, k, n_experts)
|
||||
|
||||
topv, topi = logits.reshape(G, T_l, E).topk(k)
|
||||
weights = topv.softmax(-1)
|
||||
m = topi.reshape(G, T_l * k).cast(dtypes.int32).one_hot(E).cast(dtypes.int32)
|
||||
|
||||
pad = ((m.sum(1) + (BLOCK_ROW - 1)) // BLOCK_ROW) * BLOCK_ROW
|
||||
off = pad.cumsum(1).pad(((0, 0), (1, 0)))
|
||||
dest_row = ((m.cumsum(1) + off[:, :E].reshape(G, 1, E)) * m).sum(-1).sub(1).cast(dtypes.int32)
|
||||
return Routing(weights, dest_row, off, m_l, G, T_l)
|
||||
|
||||
def dispatch(x:Tensor, r:Routing) -> Tensor:
|
||||
G, D = r.n_groups, x.shape[-1]
|
||||
return grouped_scatter_rows(x.reshape(G, r.t_local, D), r.dest_row, r.m_l).reshape(G * r.m_l, D)
|
||||
|
||||
def combine(y:Tensor, r:Routing, n_tokens:int, experts_per_tok:int) -> Tensor:
|
||||
G, D, k = r.n_groups, y.shape[-1], experts_per_tok
|
||||
sel = grouped_gather_rows(y.reshape(G, r.m_l, D), r.dest_row, G).reshape(G, r.t_local, k, D)
|
||||
return (sel * r.weights.reshape(G, r.t_local, k, 1).cast(sel.dtype)).sum(2).reshape(n_tokens, D).cast(y.dtype)
|
||||
@@ -1,195 +0,0 @@
|
||||
"""
|
||||
tilelang-style matmul_relu written with tinygrad UOp APIs.
|
||||
|
||||
Demonstrates that tilelang's T.alloc_fragment is expressible with existing
|
||||
tinygrad primitives: a per-thread REG buffer, wrapped in one Ops.UNSHARD per
|
||||
sharded axis over the LOCAL thread-grid ranges to form the full logical tile.
|
||||
Here the 64 threads are an 8x8 grid and each thread owns an 8x8 sub-tile --
|
||||
the 2-D fragment layout tilelang infers. The kernel is written against the
|
||||
full-tile UNSHARD view, and multi_pm (the same pass that lowers multi-device
|
||||
UNSHARDs) resolves it into per-thread shard code.
|
||||
|
||||
Reference tilelang kernel:
|
||||
|
||||
@tilelang.jit
|
||||
def matmul_relu(A, B, block_M=64, block_N=64, block_K=64,
|
||||
dtype=T.float16, accum_dtype=T.float32):
|
||||
M, N, K = T.const('M, N, K')
|
||||
C = T.empty([M, N], dtype)
|
||||
with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), threads=128) as (bx, by):
|
||||
A_shared = T.alloc_shared((block_M, block_K), dtype)
|
||||
B_shared = T.alloc_shared((block_K, block_N), dtype)
|
||||
C_local = T.alloc_fragment((block_M, block_N), accum_dtype)
|
||||
T.clear(C_local)
|
||||
for ko in T.Pipelined(T.ceildiv(K, block_K), num_stages=3):
|
||||
T.copy(A[by * block_M, ko * block_K], A_shared)
|
||||
T.copy(B[ko * block_K, bx * block_N], B_shared)
|
||||
T.gemm(A_shared, B_shared, C_local)
|
||||
for i, j in T.Parallel(block_M, block_N):
|
||||
C_local[i, j] = T.max(C_local[i, j], 0)
|
||||
T.copy(C_local, C[by * block_M, bx * block_N])
|
||||
return C
|
||||
|
||||
API mapping (tilelang -> tinygrad UOps, idioms from test/backend/test_custom_kernel.py):
|
||||
|
||||
T.Kernel(gx, gy, threads=T) -> AxisType.GLOBAL ranges (blocks) + AxisType.LOCAL ranges (thread grid)
|
||||
T.alloc_shared(shape, dtype) -> UOp.placeholder(shape, dtype, slot, AddrSpace.LOCAL)
|
||||
T.alloc_fragment(shape, dt) -> per-thread REG placeholder, wrapped in one Ops.UNSHARD per sharded axis over
|
||||
the AxisType.LOCAL ranges: fragment.unshard((axis_y, axis_x), (ty, tx)).
|
||||
The full logical tile is the shard with each sharded axis multiplied by its
|
||||
range size, exactly like device sharding, but the sharding axes are thread
|
||||
axes carried by the RANGE metadata instead of a device tuple. C_local[i, j]
|
||||
with [i, j] in this thread's shard is INDEX on the UNSHARD, which multi_pm
|
||||
resolves into INDEX on the per-thread REG shard, axis by axis.
|
||||
T.copy(gmem_slice, smem) -> smem[thread_idx].set(gmem_slice[thread_idx], end=copy_rng). set returns the
|
||||
smem tile AFTER the copy; the implicit-barrier pass turns the store->load
|
||||
dependency of the loop that consumes it into a workgroup barrier
|
||||
T.gemm (no WMMA) -> C_local[..].set(C_local.after(k)[..] + a_shared[..] * b_shared[..], end=k)
|
||||
with k a loop-carried LOOP range (codegen builds the register accumulator
|
||||
from this self-referential store automatically)
|
||||
T.copy(fragment, gmem) -> gmem.index(gidx).store(C_local[..]).end(all_ranges)
|
||||
UNSHARD lowering -> multi_pm in codegen (full_rewrite_to_sink): INDEX/AFTER/STORE ops on the
|
||||
full-tile view become per-thread shard ops, no UNSHARD survives into the program.
|
||||
"""
|
||||
|
||||
from tinygrad.dtype import dtypes, AddrSpace, DType
|
||||
from tinygrad.uop.ops import UOp, Ops, AxisType, KernelInfo
|
||||
from tinygrad.helpers import cdiv, getenv
|
||||
from tinygrad.tensor import Tensor
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# tilelang builtins, expressed with tinygrad UOp APIs
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def alloc_shared(shape:tuple[int, ...], dtype:DType) -> UOp:
|
||||
"""T.alloc_shared: one LOCAL buffer shared by all threads in the block."""
|
||||
return UOp.placeholder(tuple(shape), dtype, next(UOp.unique_num), AddrSpace.LOCAL)
|
||||
|
||||
def alloc_fragment(shape:tuple[int, ...], dtype:DType, axes:tuple[int, ...], rngs:tuple[UOp, ...]) -> UOp:
|
||||
"""T.alloc_fragment: per-thread REG fragment + UNSHARD over the LOCAL thread grid.
|
||||
|
||||
Each thread privately owns shape[axis]//threads elements along every sharded
|
||||
axis in a REG buffer. The UNSHARDs over the LOCAL thread ranges present the
|
||||
full logical tile: full_shape = shard_shape with each sharded axis multiplied
|
||||
by its range size. This is exactly how UNSHARD carries a DEVICE axis today,
|
||||
except the sharding axes are thread axes carried by the RANGE metadata.
|
||||
"""
|
||||
assert len(axes) == len(rngs)
|
||||
assert all(tnum.op is Ops.RANGE and tnum.arg[-1] is AxisType.LOCAL for tnum in rngs), "fragments shard over LOCAL ranges"
|
||||
assert all(shape[a] % (int(rng.vmax)+1) == 0 for a, rng in zip(axes, rngs))
|
||||
by_axis = dict(zip(axes, rngs))
|
||||
shard_shape = tuple(s // (int(by_axis[i].vmax)+1) if i in by_axis else s for i, s in enumerate(shape))
|
||||
fragment = UOp.placeholder(shard_shape, dtype, next(UOp.unique_num), AddrSpace.REG)
|
||||
return fragment.unshard(axes, rngs)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GEMM kernel: C = relu(A @ B), float inputs (fp16 or fp32), fp32 fragment accumulator, no WMMA
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# 64x64 output tile per block, 128 threads as an 8x16 grid; each thread owns an 8x4 fragment sub-tile
|
||||
# (the 2-D per-thread layout tilelang infers for this GEMM). The 4 contiguous columns (TN=4) are what
|
||||
# let codegen vectorize loads/stores to float4, matching tilelang's lowering exactly.
|
||||
BLOCK_M = BLOCK_N = BLOCK_K = 64
|
||||
TY = 8
|
||||
TX = 16
|
||||
THREADS = TY * TX
|
||||
TM = BLOCK_M // TY # fragment rows per thread (8)
|
||||
TN = BLOCK_N // TX # fragment columns per thread (4)
|
||||
|
||||
def matmul_relu_kernel(c:UOp, a:UOp, b:UOp) -> UOp:
|
||||
"""C[M, N] = relu(A[M, K] @ B[K, N]) -- one 64x64 tile per block, locals + a 2-D fragment."""
|
||||
M, K = a.shape
|
||||
K2, N = b.shape
|
||||
assert K == K2 and a.dtype == b.dtype == c.dtype and not dtypes.is_int(a.dtype)
|
||||
assert not (K % BLOCK_K or M % BLOCK_M or N % BLOCK_N), "test sizes must be multiples of the block sizes"
|
||||
|
||||
# with T.Kernel(T.ceildiv(N, BLOCK_N), T.ceildiv(M, BLOCK_M), threads=128) as (bx, by):
|
||||
bx = UOp.range(cdiv(N, BLOCK_N), 0, AxisType.GLOBAL)
|
||||
by = UOp.range(cdiv(M, BLOCK_M), 1, AxisType.GLOBAL)
|
||||
# tx (N, 16) is the fast/inner LOCAL axis so a warp covers 16 cols x 2 rows --
|
||||
# matching tilelang's (tidx>>4, tidx&15) warp composition. This keeps the 8 A_shared
|
||||
# reads in a warp on only 2 row-groups (broadcast across 16 cols) instead of 8 rows
|
||||
# (8-way bank conflict), since A_shared[row*512 + ...] all map to the same bank when 8
|
||||
# distinct rows land in one warp.
|
||||
tx = UOp.range(TX, 2, AxisType.LOCAL)
|
||||
ty = UOp.range(TY, 3, AxisType.LOCAL)
|
||||
|
||||
# A_shared = T.alloc_shared((BLOCK_M, BLOCK_K), dtype)
|
||||
# B_shared = T.alloc_shared((BLOCK_K, BLOCK_N), dtype)
|
||||
A_shared = alloc_shared((BLOCK_M, BLOCK_K), a.dtype)
|
||||
B_shared = alloc_shared((BLOCK_K, BLOCK_N), b.dtype)
|
||||
|
||||
# C_local = T.alloc_fragment((BLOCK_M, BLOCK_N), accum_dtype) -- an 8x4 REG tile per thread of the 8x16 grid
|
||||
C_local = alloc_fragment((BLOCK_M, BLOCK_N), dtypes.float32, (0, 1), (ty, tx))
|
||||
|
||||
# T.clear(C_local) -- each thread zeroes its own fragment sub-tile
|
||||
ic, jc = UOp.range(TM, 4, AxisType.LOOP), UOp.range(TN, 5, AxisType.UPCAST)
|
||||
C_loc = C_local[ic*TM + ty, tx*TN + jc].set(0.0, end=(ic, jc))
|
||||
|
||||
# for ko in T.Pipelined(T.ceildiv(K, BLOCK_K), num_stages=3):
|
||||
# (num_stages pipelining is async copy + multi-buffering; this is the synchronous single-buffer version)
|
||||
ko = UOp.range(cdiv(K, BLOCK_K), 6, AxisType.LOOP)
|
||||
|
||||
# T.copy(A[by * BLOCK_M, ko * BLOCK_K], A_shared) -- each thread copies its own 8x4 sub-tile.
|
||||
# Row index is iar*TM + ty (strided by TM across ty), matching tilelang's layout: thread ty owns
|
||||
# rows {ty, ty+8, ..., ty+56} not {ty*8, ..., ty*8+7}.
|
||||
iar, ka = UOp.range(TM, 7, AxisType.LOOP), UOp.range(TN, 8, AxisType.UPCAST)
|
||||
A_store = A_shared[iar*TM + ty, tx*TN + ka].store(a[by*BLOCK_M + iar*TM + ty, ko*BLOCK_K + tx*TN + ka]).end(iar, ka)
|
||||
|
||||
# T.copy(B[ko * BLOCK_K, bx * BLOCK_N], B_shared)
|
||||
kb, ibr = UOp.range(TM, 9, AxisType.LOOP), UOp.range(TN, 10, AxisType.UPCAST)
|
||||
B_store = B_shared[kb*TM + ty, tx*TN + ibr].store(b[ko*BLOCK_K + kb*TM + ty, bx*BLOCK_N + tx*TN + ibr]).end(kb, ibr)
|
||||
|
||||
# get the shared after the stores (single barrier)
|
||||
A_shared = A_shared.after(A_store, B_store)
|
||||
B_shared = B_shared.after(A_store, B_store)
|
||||
|
||||
# T.gemm(A_shared, B_shared, C_local), no WMMA -- per-thread accumulate over its fragment sub-tile.
|
||||
# identical to custom_gemm: a self-referential store over the loop-carried kk range,
|
||||
# which codegen turns into a register accumulator
|
||||
# kk is the outer compute loop (axis 11) so that for each kk we read all 8 A rows and reuse
|
||||
# the B[kk] read across them -- matching tilelang's ko > kk > row > col access order exactly.
|
||||
kk, ir = UOp.range(BLOCK_K, 11, AxisType.LOOP), UOp.range(TM, 12, AxisType.LOOP)
|
||||
jj = UOp.range(TN, 13, AxisType.UPCAST)
|
||||
acc = C_loc.after(kk)[ir*TM + ty, tx*TN + jj] + A_shared[ir*TM + ty, kk].cast(dtypes.float32) * B_shared[kk, tx*TN + jj].cast(dtypes.float32)
|
||||
# closing the ko loop here too; codegen adds the barrier so no thread overwrites the tiles while others still read them
|
||||
C_loc = C_loc[ir*TM + ty, tx*TN + jj].set(acc, end=(kk, ir, jj, ko))
|
||||
|
||||
# for i, j in T.Parallel(BLOCK_M, BLOCK_N): C_local[i, j] = T.max(C_local[i, j], 0)
|
||||
# T.copy(C_local, C[by * BLOCK_M, bx * BLOCK_N]) -- per-thread store of the fragment shard (relu fused into it)
|
||||
# LOOP: these loops are the per-thread output layout; convert_loop_to_global must not globalize them
|
||||
ie, je = UOp.range(TM, 14, AxisType.LOOP), UOp.range(TN, 15, AxisType.UPCAST)
|
||||
c_st = c[by*BLOCK_M + ie*TM + ty, bx*BLOCK_N + tx*TN + je].store(C_loc[ie*TM + ty, tx*TN + je].relu().cast(c.dtype))
|
||||
|
||||
# all open ranges are closed at the final store (ko was closed above).
|
||||
# the fragment UNSHARDs go to codegen as is: multi_pm there resolves the full-tile view into per-thread shard code
|
||||
return c_st.end(je, ie, tx, ty, bx, by).sink(arg=KernelInfo(name="matmul_relu", opts_to_apply=()))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# python wrapper: same signature as the tilelang function
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def matmul_relu(a:Tensor, b:Tensor) -> Tensor:
|
||||
"""C = relu(A @ B), fp16 in/out with an fp32 fragment accumulator."""
|
||||
c = Tensor.empty(a.shape[0], b.shape[1], dtype=a.dtype, device=a.device)
|
||||
return c.custom_kernel(a, b, fxn=matmul_relu_kernel)[0]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# test
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
from tinygrad import Device
|
||||
assert Device[Device.DEFAULT].renderer.has_local, "this GPU-style kernel needs a backend with local memory (LOCAL ranges + barriers)"
|
||||
M = K = N = getenv("N", 256) # 4x4 grid of 64x64 tiles, 4 K chunks
|
||||
dtype_in = dtypes.half if getenv("HALF") else dtypes.float
|
||||
|
||||
a = Tensor.randn(M, K, dtype=dtype_in).contiguous()
|
||||
b = Tensor.randn(K, N, dtype=dtype_in).contiguous()
|
||||
ref = (a @ b).relu().realize()
|
||||
|
||||
out = matmul_relu(a, b).realize()
|
||||
|
||||
import numpy as np
|
||||
np.testing.assert_allclose(out.numpy(), ref.numpy(), atol=1e-1, rtol=1e-2)
|
||||
print("matmul_relu passed!")
|
||||
@@ -1,24 +1,24 @@
|
||||
from __future__ import annotations
|
||||
from typing import cast, Callable, TypeVar, Generic, Any, Sequence
|
||||
from typing import cast, Callable, TypeVar, Generic, Any
|
||||
import struct, functools, time, collections, itertools
|
||||
from dataclasses import replace, dataclass
|
||||
from tinygrad.helpers import DEV, getenv, select_first_inited, select_by_name, suppress_finalizing, dedup, pluralize, JIT_BATCH_SIZE, unwrap
|
||||
from tinygrad.helpers import to_tuple, round_up, partition, data64_le, panic, ContextVar
|
||||
from tinygrad.device import Device, Buffer, BufferSpec, Compiled, LRUAllocator, MultiBuffer, DepsTracker
|
||||
from tinygrad.device import Device, Buffer, BufferSpec, Compiled, LRUAllocator, MultiBuffer
|
||||
from tinygrad.uop.ops import Ops, sint, UOp, UPat, PatternMatcher, KernelInfo, graph_rewrite, track_rewrites, GroupOp
|
||||
from tinygrad.uop.symbolic import symbolic
|
||||
from tinygrad.dtype import dtypes, truncate
|
||||
from tinygrad.runtime.support.hcq import MMIOInterface
|
||||
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
|
||||
from tinygrad.engine.realize import to_program, get_call_arg_uops, get_call_name, get_call_outs_ins, estimate_uop, pm_flatten_linear
|
||||
from tinygrad.engine.jit import DepsTracker
|
||||
|
||||
HCQDeviceType = TypeVar('HCQDeviceType', bound='HCQ2Compiled')
|
||||
|
||||
# *****************
|
||||
# 0. helpers
|
||||
|
||||
HCQDeviceType = TypeVar('HCQDeviceType', bound='HCQ2Compiled')
|
||||
|
||||
HCQ_RUNTIME_DEV = ContextVar("HCQ_RUNTIME_DEV", "CPU")
|
||||
|
||||
HCQ_DEVS = frozenset(("AMD",))
|
||||
@@ -37,42 +37,50 @@ class HCQInfo:
|
||||
|
||||
def all_devices_in(d:Any, c:frozenset[str]) -> bool: return {x.split(":")[0] for x in to_tuple(d)} <= c
|
||||
|
||||
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 in {Ops.MSELECT, Ops.SLICE} else (u,)
|
||||
def unwrap_after(uop):
|
||||
while uop.op is Ops.AFTER: uop = uop.src[0]
|
||||
return uop
|
||||
|
||||
def make_patches(buf:UOp, patches:Sequence[tuple[sint, UOp]]) -> UOp:
|
||||
return buf.index(UOp.stack(*(UOp.const(dtypes.int, off // buf.dtype.itemsize) for off,_ in patches))) \
|
||||
.store(UOp.stack(*(val.simplify().cast(buf.dtype) for _,val in patches)))
|
||||
def unwrap_mstack(u):
|
||||
return tuple(x for s in u.src for x in unwrap_mstack(s)) if u.op is Ops.MSTACK else (unwrap_mstack(u.src[0]) if u.op in {Ops.MSELECT, Ops.SLICE} else (u,))
|
||||
|
||||
def make_getaddr(u, device=None):
|
||||
if unwrap_after(u).op not in (Ops.BUFFER, Ops.SLICE, Ops.BINARY, Ops.MSTACK, Ops.MSELECT, Ops.PARAM): return u
|
||||
return UOp(Ops.GETADDR, dtypes.uint64, src=(u,), arg=device or to_tuple(u.device)[0])
|
||||
|
||||
def make_ins(op, *srcs):
|
||||
return UOp(Ops.INS, arg=op, src=tuple(UOp.const(dtypes.uint32, s) if isinstance(s, int) else s.cast(dtypes.uint32) for s in srcs))
|
||||
|
||||
def make_placeholder(devs, size:int, dtype, name=None, unique=True) -> UOp:
|
||||
return UOp.param(next(UOp.unique_num) if unique else 0, dtype, shape=(size,), device=devs).rtag(name or "temp")
|
||||
|
||||
def make_patch(buf:UOp, off:sint, val:UOp, dtype=None) -> UOp:
|
||||
return buf.index(UOp.const(dtypes.int, off // buf.dtype.itemsize)).store(val.simplify().cast(dtype or buf.dtype))
|
||||
|
||||
def make_binary_patch(buf:UOp, blob:bytes) -> UOp:
|
||||
data = UOp(Ops.BINARY, src=(), arg=blob).bitcast(buf.dtype)
|
||||
r = UOp.range(len(blob) // buf.dtype.itemsize, 0, dtype=dtypes.int, src=(buf, data))
|
||||
return buf.index(r).store(data.index(r).load()).end(r)
|
||||
|
||||
def make_cmdbuf(lin, devs, buf:UOp|None=None, dep:UOp|None=None):
|
||||
def make_cmdbuf(lin, devs):
|
||||
blob, patches = b'', []
|
||||
for s in (s for ins in lin.src for s in ins.src):
|
||||
if (ssimp:=s.simplify()).op is not Ops.CONST: patches.append((len(blob), ssimp))
|
||||
blob += struct.pack(f'<{ssimp.dtype.fmt}', ssimp.arg if ssimp.op is Ops.CONST else 0x0)
|
||||
cmdbuf = buf if buf is not None else UOp.placeholder((len(blob) // 4,), dtypes.uint32, next(UOp.unique_num), device=devs).rtag("cmdbuf")
|
||||
writable = cmdbuf.after(dep) if dep is not None else cmdbuf
|
||||
return cmdbuf.after(make_binary_patch(writable, blob), *((make_patches(writable, patches),) if patches else ()))
|
||||
cmdbuf = make_placeholder(devs, len(blob) // 4, dtypes.uint32, name="cmdbuf")
|
||||
return cmdbuf.after(make_binary_patch(cmdbuf, blob), *[make_patch(cmdbuf, off, s) for off, s in patches])
|
||||
|
||||
def make_signal(devs, queue="COMPUTE:0", sentinel=False):
|
||||
return UOp.placeholder((1,), dtypes.uint64, 0, device=devs, volatile=True).rtag("sentinel_signal" if sentinel else f"{queue}_timeline_signal")
|
||||
def make_signal_value(devs, queue="COMPUTE:0"): return UOp.placeholder((1,), dtypes.uint64, 0, device=devs).rtag(f"{queue}_timeline_value")
|
||||
def make_mstack(uops): return uops[0] if len(uops) == 1 else UOp(Ops.MSTACK, uops[0].dtype, tuple(uops))
|
||||
|
||||
def make_signal(devs, queue=None, sentinel=False):
|
||||
return make_placeholder(devs, 1, dtypes.uint64, "sentinel_signal" if sentinel else (queue, "timeline_signal") if queue else "timeline_signal", unique=False)
|
||||
def make_signal_value(devs, queue=None):
|
||||
return make_placeholder(devs, 1, dtypes.uint64, (queue, "timeline_value") if queue else "timeline_value", unique=False)
|
||||
|
||||
def make_submit(*cmds, devs:str|tuple[str, ...], queue:str) -> UOp:
|
||||
return UOp.custom_function("submit_cmdbuf", UOp(Ops.LINEAR, src=tuple(cmds), arg=(to_tuple(devs), queue)))
|
||||
def get_submit(ast:UOp) -> UOp: return next(u for u in ast.toposort() if u.op is Ops.CUSTOM_FUNCTION and u.arg == "submit_cmdbuf")
|
||||
|
||||
def encode_kernargs_clike(call:UOp, prg:UOp, devs:str|tuple[str, ...]) -> UOp:
|
||||
data, info = prg.arg
|
||||
buf = UOp.placeholder((data.kernargs_alloc_size // 4,), dtypes.uint32, next(UOp.unique_num), device=devs).rtag("kernargs")
|
||||
words = [w for gi in info.globals for w in data64_le(get_call_arg_uops(call)[gi].getaddr(devs))] + list(info.vars)
|
||||
return buf.after(*((make_patches(buf, [(i * 4, w) for i, w in enumerate(words)]),) if words else ()))
|
||||
|
||||
# *****************
|
||||
# 0.1. prep: replace buffers with params
|
||||
|
||||
@@ -108,8 +116,7 @@ def _get_call_bufs_by_lane(call:UOp, devices:tuple[str, ...]) -> list[list[Any]]
|
||||
def _get_deps(ctx:DepsTracker, bufs_by_lane:list[list[Any]], write, key:tuple[tuple[str, ...], str, int]) -> list[tuple[tuple, int, int]]:
|
||||
dep_lanes:list[tuple[tuple, int, int]] = []
|
||||
for lane, bufs in enumerate(bufs_by_lane):
|
||||
written = write if write is not None else list(range(len(bufs)))
|
||||
dep_lanes += [(dep, dlane, lane) for dep, dlane in ctx.access_resources(bufs, written, (key, lane))]
|
||||
dep_lanes += [(dep, dlane, lane) for dep, dlane in ctx.access_resources(bufs, write if write is not None else range(len(bufs)), (key, lane))]
|
||||
return dep_lanes
|
||||
|
||||
def _build_wait_cmds(dep_lanes:list[tuple[tuple, int, int]], devices:tuple[str, ...], queue:str) -> tuple[list[UOp], set[int]]:
|
||||
@@ -124,8 +131,8 @@ def _build_wait_cmds(dep_lanes:list[tuple[tuple, int, int]], devices:tuple[str,
|
||||
|
||||
waits = []
|
||||
for (ddevs, dqueue, dtag), lanes in deps.items():
|
||||
sig = UOp.mstack(*[make_signal(d if dl is None else ddevs[dl], queue=dqueue, sentinel=dl is None) for dl, d in zip(lanes, devices)])
|
||||
val = UOp.mstack(*[make_signal_value(d if dl is None else ddevs[dl], queue=dqueue) for dl, d in zip(lanes, devices)])
|
||||
sig = make_mstack([make_signal(d if dl is None else ddevs[dl], queue=dqueue, sentinel=dl is None) for dl, d in zip(lanes, devices)])
|
||||
val = make_mstack([make_signal_value(d if dl is None else ddevs[dl], queue=dqueue) for dl, d in zip(lanes, devices)])
|
||||
waits.append(UOp(Ops.INS, arg="wait", src=(sig, val.index(UOp.const(dtypes.int, 0)) + dtag)))
|
||||
return waits, {dtag for _, _, dtag in deps}
|
||||
|
||||
@@ -137,8 +144,8 @@ def _build_finalizers(batch:list[tuple[UOp, tuple[str, ...]]], batch_info:list[t
|
||||
for b in itertools.chain.from_iterable(_get_call_bufs_by_lane(call, devices)):
|
||||
for bd in to_tuple(b.device): dev_bufs[bd][id(b)] = b
|
||||
|
||||
zero, n, submits, bumps, waited = UOp.const(dtypes.int, 0), len(batch_info), [], [], set()
|
||||
for _, devgroup in itertools.groupby(sorted(dev_bufs), key=lambda d: d.split(":")[0]):
|
||||
zero, n, finalizers, waited = UOp.const(dtypes.int, 0), len(batch_info), [], set()
|
||||
for _, devgroup in itertools.groupby(sorted(dedup([d for devs, _ in batch_info for d in devs])), key=lambda d: d.split(":")[0]):
|
||||
devs = tuple(devgroup)
|
||||
|
||||
# to finalize the batch, sync all accesses from other devices to buffers that belong to this device
|
||||
@@ -146,16 +153,13 @@ def _build_finalizers(batch:list[tuple[UOp, tuple[str, ...]]], batch_info:list[t
|
||||
waits, cur_waited = _build_wait_cmds(fin_deps, devs, "COMPUTE:0")
|
||||
waited |= cur_waited
|
||||
|
||||
# wait the syncs, store the device epoch
|
||||
store = UOp(Ops.INS, arg="store", src=(make_signal(devs), (tl:=make_signal_value(devs)).index(zero) + n))
|
||||
submits.append((devs, make_submit(*waits, store, devs=devs, queue="COMPUTE:0")))
|
||||
upd = [(tl, n + 1)] + [(make_signal_value(devs, queue=qn), n)
|
||||
for qn in dedup([qn for bdevs, qn in batch_info if set(bdevs) & set(devs)]) if qn != "COMPUTE:0"]
|
||||
bumps.append((devs, UOp.barrier(*[s.index(zero, dtype=s.dtype).store(s.index(zero) + inc) for s, inc in upd])))
|
||||
|
||||
# NOTE: submit before bumps
|
||||
fins = [UOp.custom_function("hcq", b.sink()).call(aux=HCQInfo("hcq_finalizer", Estimates(), devs, "COMPUTE:0")) for devs, b in submits + bumps]
|
||||
return fins, waited
|
||||
# wait the syncs, store the device epoch; value bumps are a separate call: no lane may bump until every lane has patched its waits
|
||||
store = UOp(Ops.INS, arg="store", src=(make_signal(devs), (tl:=make_signal_value(devs)).index(zero)))
|
||||
submit = make_submit(*waits, store, devs=devs, queue="COMPUTE:0")
|
||||
upd = [(tl, 1)] + [(make_signal_value(devs, queue=qn), n) for qn in dedup([qn for bdevs, qn in batch_info if set(bdevs) & set(devs)])]
|
||||
bump = UOp.barrier(*[s.index(zero, dtype=s.dtype).store(s.index(zero) + inc) for s, inc in upd])
|
||||
finalizers += [UOp.custom_function("hcq", b.sink()).call(aux=HCQInfo("hcq_finalizer", Estimates(), devs, "COMPUTE:0")) for b in (submit, bump)]
|
||||
return finalizers, waited
|
||||
|
||||
def _finalize_batch(batch:list[tuple[UOp, tuple[str, ...]]]) -> list[UOp]:
|
||||
batch_info = [(devices, "COMPUTE:0" if call.src[0].op is Ops.PROGRAM else "COPY:0") for call, devices in batch]
|
||||
@@ -175,18 +179,19 @@ def _finalize_batch(batch:list[tuple[UOp, tuple[str, ...]]]) -> list[UOp]:
|
||||
waited |= finalizer_waited
|
||||
|
||||
src = []
|
||||
for tag, ((call, _), (devices, queue), q) in enumerate(zip(batch, batch_info, call_waits)):
|
||||
for tag, ((call, _), (devices, queue), cmds) in enumerate(zip(batch, batch_info, call_waits)):
|
||||
# first queue use, sync prior device work with main signal
|
||||
if batch_info.index((devices, queue)) == tag:
|
||||
q = [UOp(Ops.INS, arg="barrier", src=()), UOp(Ops.INS, arg="wait", src=(make_signal(devices), make_signal_value(devices).index(0) - 1))] + q
|
||||
epoch = UOp(Ops.INS, arg="wait", src=(make_signal(devices), make_signal_value(devices).index(0) - 1))
|
||||
cmds = [UOp(Ops.INS, arg="barrier", src=()), epoch] + cmds
|
||||
|
||||
# and make hcq call
|
||||
info = HCQInfo(get_call_name(call, get_call_arg_uops(call)), estimate_uop(call), devices, queue)
|
||||
q += [call.replace(arg=replace(call.arg, aux=info))]
|
||||
cmds = [*cmds, call.replace(arg=replace(call.arg, aux=info))]
|
||||
|
||||
# signal queue timeline if someone waits for us
|
||||
if tag in waited: q += [UOp(Ops.INS, arg="store", src=(make_signal(devices, queue), make_signal_value(devices, queue).index(0) + tag))]
|
||||
src.append(UOp.custom_function("hcq", make_submit(*q, devs=devices, queue=queue).sink()).call(name="hcq", aux=info))
|
||||
if tag in waited: cmds += [UOp(Ops.INS, arg="store", src=(make_signal(devices, queue), make_signal_value(devices, queue).index(0) + tag))]
|
||||
src.append(UOp.custom_function("hcq", make_submit(*cmds, devs=devices, queue=queue).sink()).call(name="hcq", aux=info))
|
||||
return src + finalizers
|
||||
|
||||
def sched_hcq_batches(l:UOp) -> UOp:
|
||||
@@ -196,6 +201,7 @@ def sched_hcq_batches(l:UOp) -> UOp:
|
||||
if (devs:=next((b.device for b in call.src[1:] if all_devices_in(b.device, HCQ_DEVS)), None)) is not None: batch.append((call, to_tuple(devs)))
|
||||
else: srcs, batch = srcs + _finalize_batch(batch) + [call], []
|
||||
return l.replace(src=tuple(srcs + _finalize_batch(batch)))
|
||||
pm_sched_hcq_batches = PatternMatcher([(UPat(Ops.LINEAR, name="l"), sched_hcq_batches)])
|
||||
|
||||
# *****************
|
||||
# 3. merge into queues
|
||||
@@ -210,7 +216,7 @@ def _merged_hcq_call(calls:list[UOp]) -> UOp: # TODO: simplify?
|
||||
def merge_queues(linear:UOp) -> UOp:
|
||||
new_src:list[UOp] = []
|
||||
opened_qs:dict[tuple[tuple[str, ...], str], list[UOp]] = {} # (devs, queue) -> list of hcq calls, kept in submit order
|
||||
limits:dict[tuple[tuple[str, ...], str], int] = collections.defaultdict(lambda: JIT_BATCH_SIZE.value)
|
||||
limits = collections.defaultdict(lambda: JIT_BATCH_SIZE.value)
|
||||
|
||||
for call in linear.src:
|
||||
if not isinstance(info:=call.arg.aux, HCQInfo) or info.name == "hcq_finalizer": # non-hcq call or finalizer: close all open queues
|
||||
@@ -227,10 +233,16 @@ def merge_queues(linear:UOp) -> UOp:
|
||||
new_rec = [call]
|
||||
opened_qs[(info.device, info.queue)] = new_rec
|
||||
return linear.replace(src=tuple(new_src + [_merged_hcq_call(c) for c in opened_qs.values()]))
|
||||
pm_merge_queues = PatternMatcher([(UPat(Ops.LINEAR, name="linear"), merge_queues)])
|
||||
|
||||
def schedule_and_merge(ctx:dict[UOp, UOp], linear:UOp) -> UOp:
|
||||
return merge_queues(sched_hcq_batches(linear).substitute(ctx, walk=True, enter_calls=True))
|
||||
pm_schedule_and_merge = PatternMatcher([(UPat(Ops.LINEAR, name="linear"), schedule_and_merge)])
|
||||
# *****************
|
||||
# 4.1. hcq lowering: programs
|
||||
|
||||
def encode_kernargs_clike(call:UOp, prg:UOp, devs:str|tuple[str, ...]) -> UOp:
|
||||
data, info = prg.arg
|
||||
buf = make_placeholder(devs, data.kernargs_alloc_size // 4, dtypes.uint32, name="kernargs")
|
||||
words = [w for gi in info.globals for w in data64_le(make_getaddr(get_call_arg_uops(call)[gi], devs))] + list(info.vars)
|
||||
return buf.after(*[make_patch(buf, i * 4, w) for i, w in enumerate(words)])
|
||||
|
||||
# *****************
|
||||
# 4.2. hcq lowering: ops to ir
|
||||
@@ -238,8 +250,7 @@ pm_schedule_and_merge = PatternMatcher([(UPat(Ops.LINEAR, name="linear"), schedu
|
||||
def encode_cmdbuf(submit:UOp, lin:UOp) -> UOp|None:
|
||||
if (pm:=Device.get_class(lin.arg[0][0]).pm_lower) is None: return None
|
||||
return graph_rewrite(submit, pm, name=f"encode {lin.arg[0]}", enter_calls=True)
|
||||
pm_encode_cmdbufs = PatternMatcher([
|
||||
(UPat(Ops.CUSTOM_FUNCTION, arg="submit_cmdbuf", src=(UPat(Ops.LINEAR, name="lin"),), name="submit"), encode_cmdbuf)])
|
||||
pm_encode_cmdbufs = PatternMatcher([(UPat(Ops.CUSTOM_FUNCTION, arg="submit_cmdbuf", src=(UPat(Ops.LINEAR, name="lin"),), name="submit"), encode_cmdbuf)])
|
||||
|
||||
# *****************
|
||||
|
||||
@@ -251,7 +262,7 @@ 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 is_link_patch(p:UOp, jit:bool) -> bool:
|
||||
store = p.src[0] if (is_binary_patch:=(p.op is Ops.END and p.src[0].op is Ops.STORE)) else p
|
||||
store = p.src[0] if (is_binary_patch:=p.op is Ops.END) else p
|
||||
if not jit: return store.buf_uop.tag == "program"
|
||||
return is_binary_patch or (store.op is Ops.STORE and is_value_known_at_link(store.src[1]))
|
||||
|
||||
@@ -265,8 +276,7 @@ def trim_link_patches(ctx:tuple[bool, list[UOp]], a:UOp) -> UOp|None:
|
||||
pm_trim_link_patches = PatternMatcher([(UPat(Ops.AFTER, src=(UPat((Ops.PARAM, Ops.MSTACK)),), allow_any_len=True, name="a"), trim_link_patches)])
|
||||
|
||||
def split_patches(ctx:bool, call:UOp) -> UOp|None:
|
||||
lt_patches:list[UOp] = []
|
||||
body = graph_rewrite(call.src[0], pm_trim_link_patches, ctx=(ctx, lt_patches), name=f"trim link-time patches ({call.arg.aux.name})")
|
||||
body = graph_rewrite(call.src[0], pm_trim_link_patches, ctx=(ctx, lt_patches:=[]), name=f"trim link-time patches ({call.arg.aux.name})")
|
||||
|
||||
lt_srcs = collections.defaultdict(list)
|
||||
for p in lt_patches: lt_srcs[p.buf_uop].append(p)
|
||||
@@ -276,17 +286,16 @@ pm_split_patches = PatternMatcher([(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION
|
||||
# *****************
|
||||
|
||||
def make_addr_table(call:UOp, gaddrs:list[UOp], name:str) -> tuple[dict[UOp, UOp], tuple[UOp, ...]]:
|
||||
bare = {g: g.replace(src=(g.src[0].without_after,)) for g in gaddrs}
|
||||
bare = {g: g.replace(src=(unwrap_after(g.src[0]),)) for g in gaddrs}
|
||||
|
||||
order = sorted(dedup(bare.values()), key=lambda g: ((b:=unwrap_mstack(g.buf_uop)[0]).arg.slot, repr(b.tag)))
|
||||
slots = {g:i for i,g in enumerate(order)}
|
||||
table = UOp.placeholder((len(order),), dtypes.uint64, next(UOp.unique_num), device=call.arg.aux.device).rtag(name)
|
||||
slots, table = {g:i for i,g in enumerate(order)}, make_placeholder(call.arg.aux.device, len(order), dtypes.uint64, name)
|
||||
|
||||
reads = {g: table.after(*g.src[0].src[1:] if g.src[0].op is Ops.AFTER else ()).index(UOp.const(dtypes.int, slots[bare[g]])).load() for g in gaddrs}
|
||||
return reads, (table.after(make_patches(table, [(i * table.dtype.itemsize, addr) for addr, i in slots.items()])),) if slots else ()
|
||||
return reads, (table.after(*[make_patch(table, i * table.dtype.itemsize, addr) for addr, i in slots.items()]),) if slots else ()
|
||||
|
||||
def make_blob_bufs(call:UOp, blobs:list[UOp]) -> tuple[dict[UOp, UOp], tuple[UOp, ...]]:
|
||||
bufs = {b: UOp.placeholder((b.max_numel(),), b.dtype, next(UOp.unique_num), device=call.arg.aux.device).rtag("template") for b in blobs}
|
||||
bufs = {b: make_placeholder(call.arg.aux.device, b.max_numel(), b.dtype, "template") for b in blobs}
|
||||
return bufs, tuple(buf.after(make_binary_patch(buf, b.src[0].arg)) for b,buf in bufs.items())
|
||||
|
||||
def rm_rt_uops(call:UOp) -> UOp|None:
|
||||
@@ -301,8 +310,7 @@ def rm_rt_uops(call:UOp) -> UOp|None:
|
||||
reads, fills = reads | {k:v for r,_ in tables for k,v in r.items()}, [f for _,fs in tables for f in fs]
|
||||
return call.replace(src=(call.src[0].substitute(reads), *call.src[1:], *fills),
|
||||
arg=replace(call.arg, aux=replace(call.arg.aux, input_idxs=tuple(sorted(dedup(g.buf_uop.arg.slot for g in inputs))))))
|
||||
pm_rm_rt_uops = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="hcq"),), name="call", allow_any_len=True), rm_rt_uops)])
|
||||
pm_rm_rt_uops = PatternMatcher([(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="hcq"),), name="call", allow_any_len=True), rm_rt_uops)])
|
||||
|
||||
# *****************
|
||||
|
||||
@@ -314,25 +322,18 @@ def replace_params(call:UOp) -> UOp|None:
|
||||
by_root = {p.src[0]: p for p in patched}
|
||||
c_args = [by_root.get(a, a) for a in args]
|
||||
|
||||
# keep buffers whose addresses become link-time constants alive and mapped
|
||||
held = args + [r.without_after for r in refhold]
|
||||
addrs = dedup([g.src[0].without_after for x in call.src for g in x.toposort() if g.op is Ops.GETADDR])
|
||||
refhold += [a for a in addrs if a not in held and all(b.op is not Ops.PARAM or b.tag is not None for b in unwrap_mstack(a))]
|
||||
|
||||
sub = {(b:=u.without_after): UOp.param(i, u.dtype, shape=b.shape, device=u.device, 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}
|
||||
sub = {unwrap_after(u): UOp.param(i, u.dtype, shape=unwrap_after(u).shape, device=u.device) 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}
|
||||
info = replace(call.arg.aux, inputs=next((i for i,u in enumerate(c_args) if u.tag == "inputs"), None))
|
||||
return call.replace(src=(body.substitute(sub).replace(arg="hcq_args"), *c_args, *refhold),
|
||||
arg=replace(call.arg, aux=info)) # TODO: call.after(*refhold)?
|
||||
pm_replace_params = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="hcq"),), name="call", allow_any_len=True), replace_params)])
|
||||
return call.replace(src=(body.substitute(sub), *c_args, *refhold), arg=replace(call.arg, aux=info)) # TODO: call.after(*refhold)?
|
||||
pm_replace_params = PatternMatcher([(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="hcq"),), name="call", allow_any_len=True), replace_params)])
|
||||
|
||||
# *****************
|
||||
|
||||
def resolve_getaddr_slice(bv:UOp, g:UOp) -> UOp:
|
||||
base = bv.src[0].after(*g.src[0].src[1:] if g.src[0].op is Ops.AFTER else ())
|
||||
itemsize = bv.src[0].dtype.itemsize if bv.src[0].without_after.op in (Ops.BUFFER, Ops.SLICE, Ops.MSTACK, Ops.MSELECT) else bv.dtype.itemsize
|
||||
return UOp(Ops.GETADDR, src=(base,), arg=g.arg) + UOp.const(dtypes.uint64, bv.src[1].arg * itemsize)
|
||||
itemsize = bv.src[0].dtype.itemsize if unwrap_after(bv.src[0]).op in (Ops.BUFFER, Ops.SLICE, Ops.MSTACK, Ops.MSELECT) else bv.dtype.itemsize
|
||||
return UOp(Ops.GETADDR, dtypes.uint64, src=(base,), arg=g.arg) + UOp.const(dtypes.uint64, bv.src[1].arg * itemsize)
|
||||
|
||||
pm_early_simplify = PatternMatcher([
|
||||
(UPat(Ops.GETADDR, src=(UPat.any(sl:=UPat(Ops.SLICE, name="bv"), sl.after(allow_any_len=True)),), name="g"), resolve_getaddr_slice),
|
||||
@@ -345,51 +346,51 @@ pm_early_simplify = PatternMatcher([
|
||||
|
||||
def pack_hcq_placeholders(call:UOp) -> UOp|None:
|
||||
bufs = [b for b in call.src[0].toposort() if b.op is Ops.PARAM and b.tag in {"scratch", "kernargs"}]
|
||||
offs:dict[UOp, int] = {}
|
||||
sizes:dict[Any, int] = {}
|
||||
offs, sizes = {}, {}
|
||||
for b in bufs:
|
||||
if b.tag == "scratch": sizes[b.tag] = max(sizes.get(b.tag, 0), b.max_numel())
|
||||
else:
|
||||
offs[b] = round_up(sizes.get(b.tag, 0), 128 // b.dtype.itemsize)
|
||||
sizes[b.tag] = offs[b] + b.max_numel()
|
||||
counts = collections.Counter(b.tag for b in bufs)
|
||||
bases = {b.tag:UOp.placeholder((sizes[b.tag],), b.dtype, next(UOp.unique_num), device=b.device).rtag(b.tag) for b in bufs if counts[b.tag] > 1}
|
||||
subs = {b:UOp(Ops.SLICE, b.dtype, (bases[b.tag], UOp.const(None, offs.get(b, 0))), b.max_numel()) for b in bufs if b.tag in bases}
|
||||
bases = {b.tag:make_placeholder(b.device, sizes[b.tag], b.dtype, b.tag) for b in bufs if counts[b.tag] > 1}
|
||||
subs = {b:UOp(Ops.SLICE, b.dtype, (bases[b.tag], UOp.const(dtypes.weakint, offs.get(b, 0))), b.max_numel()) for b in bufs if b.tag in bases}
|
||||
return call.replace(src=(call.src[0].substitute(subs, walk=True), *call.src[1:])) if subs else None
|
||||
pm_pack_placeholders = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="hcq"),), name="call", allow_any_len=True), pack_hcq_placeholders)])
|
||||
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)])
|
||||
pm_callify_hcq = PatternMatcher([(UPat(Ops.CUSTOM_FUNCTION, arg="hcq", src=(UPat(Ops.SINK),), name="cf"),
|
||||
lambda cf: cf.replace(src=(to_program(cf.src[0].replace(arg=KernelInfo("hcq_submit"), tag=1), Device[HCQ_RUNTIME_DEV.value].renderer),)))])
|
||||
|
||||
hcq_compile_cache:dict[tuple[bytes, bool], UOp] = {}
|
||||
|
||||
@track_rewrites(lambda linear,input_uops,jit,ret: f"HCQ Compile {pluralize('Kernel', len(ret.src))}")
|
||||
def hcq_compile(linear:UOp, input_uops:list[UOp]|None=None, jit=False) -> UOp:
|
||||
if input_uops is not None: linear = graph_rewrite(linear, pm_replace_buffers, ctx=input_uops, walk=True, name="replace buffer")
|
||||
if input_uops is not None: linear = graph_rewrite(linear, pm_replace_buffers, ctx=input_uops, walk=True, enter_calls=True, name="replace buffer")
|
||||
|
||||
if (final_linear:=(hcq_compile_cache.get(cache_key:=(linear.key, jit)))) is None:
|
||||
# prep
|
||||
linear = linear.substitute(back_map:={s.param_like(i): s for i,s in enumerate(input_uops)} if input_uops is not None else {}, walk=True)
|
||||
linear = graph_rewrite(linear, pm_insert_copy_staging+pm_flatten_linear, name="insert copy staging")
|
||||
linear = graph_rewrite(linear, pm_insert_copy_staging + pm_flatten_linear, name="insert copy staging")
|
||||
|
||||
# schedule
|
||||
linear = graph_rewrite(linear, pm_schedule_and_merge, ctx={s:p for p,s in back_map.items()}, walk=True, name="schedule and merge hcq")
|
||||
linear = graph_rewrite(linear, pm_sched_hcq_batches, walk=True, name="schedule hcq batches")
|
||||
linear = linear.substitute({s: p for p, s in back_map.items()}, walk=True, enter_calls=True)
|
||||
linear = graph_rewrite(linear, pm_merge_queues, walk=True, name="merge queues")
|
||||
|
||||
# lowering to hcq ir
|
||||
linear = graph_rewrite(linear, pm_encode_cmdbufs+pm_pack_placeholders, walk=True, name="encode and pack", enter_calls=True)
|
||||
linear = graph_rewrite(linear, pm_encode_cmdbufs, walk=True, name="encode cmdbufs", enter_calls=True)
|
||||
linear = graph_rewrite(linear, pm_pack_placeholders, walk=True, name="pack placeholders")
|
||||
|
||||
# patches
|
||||
linear = graph_rewrite(linear, pm_split_patches+pm_early_simplify+symbolic, ctx=jit, bottom_up=False, name="simplify patches", enter_calls=True)
|
||||
# pie
|
||||
linear = graph_rewrite(linear, pm_split_patches, ctx=jit, walk=True, name="split rt/lt patches")
|
||||
linear = graph_rewrite(linear, pm_early_simplify + symbolic, bottom_up=False, name="simplify packed placeholders", enter_calls=True)
|
||||
linear = graph_rewrite(linear, pm_rm_rt_uops, walk=True, name="replace rt uops")
|
||||
linear = graph_rewrite(linear, pm_replace_params, walk=True, name="replace with args")
|
||||
|
||||
# and compile it
|
||||
linear = graph_rewrite(linear, pm_replace_params, bpm=pm_rm_rt_uops, name="replace rt uops and params")
|
||||
final_linear = hcq_compile_cache[cache_key] = graph_rewrite(linear, pm_callify_hcq, name="callify hcq", enter_calls=True)
|
||||
|
||||
return final_linear
|
||||
@@ -399,15 +400,14 @@ def hcq_compile(linear:UOp, input_uops:list[UOp]|None=None, jit=False) -> UOp:
|
||||
|
||||
def bufferize_buf(ctx:bool, buf:UOp) -> UOp|None:
|
||||
if buf.tag is None: return None
|
||||
return UOp.mstack(*(UOp.from_buffer((dv:=Device[dev]).pm_bufferize.rewrite(buf, ctx=(dv, ctx)), HCQ_RUNTIME_DEV.value)
|
||||
for dev in to_tuple(buf.device)))
|
||||
return make_mstack(tuple(UOp.from_buffer((dv:=Device[dev]).pm_bufferize.rewrite(buf, ctx=(dv, ctx)), HCQ_RUNTIME_DEV.value) for dev in to_tuple(buf.device)))
|
||||
pm_bufferize = PatternMatcher([(UPat(Ops.PARAM, name="buf"), bufferize_buf)])
|
||||
|
||||
# *****************
|
||||
# 7. resolve patches
|
||||
|
||||
def push_stack(op, s): return UOp(Ops.STACK,
|
||||
src=tuple(op.replace(dtype=op.dtype.scalar(), src=tuple(x if y is s else y for y in op.src)) for x in s.src))
|
||||
def push_stack(op, s): return UOp(Ops.STACK, op.dtype.scalar(),
|
||||
tuple(op.replace(dtype=op.dtype.scalar(), src=tuple(x if y is s else y for y in op.src)) for x in s.src))
|
||||
|
||||
def fold_binary(buf:UOp, blob:UOp) -> UOp:
|
||||
for b in (m.bufs if isinstance(m:=buf.buffer, MultiBuffer) else (m,)):
|
||||
@@ -415,10 +415,9 @@ def fold_binary(buf:UOp, blob:UOp) -> UOp:
|
||||
return UOp(Ops.NOOP)
|
||||
|
||||
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.arg))
|
||||
b.ensure_allocated().as_memoryview(force_zero_copy=True, no_sync=True).cast('B')[(bo:=off.arg*buf.dtype.itemsize):bo+len(data)] = data
|
||||
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.arg))
|
||||
b.ensure_allocated().as_memoryview(force_zero_copy=True, no_sync=True).cast('B')[(byte_off:=off.arg*buf.dtype.itemsize):byte_off+len(data)] = data
|
||||
return UOp(Ops.NOOP)
|
||||
|
||||
def resolve_getaddr(buf:UOp, g:UOp) -> UOp:
|
||||
@@ -428,7 +427,7 @@ def resolve_getaddr(buf:UOp, g:UOp) -> UOp:
|
||||
bufs = tuple(cast(Buffer, x.buffer) for x in buf.src) if buf.op is Ops.MSTACK else tuple(b.bufs if isinstance(b, MultiBuffer) else (b,)*len(devs))
|
||||
assert len(bufs) == len(devs), f"can't resolve {len(bufs)} buffers on {len(devs)} devices"
|
||||
addrs = tuple(UOp.const(dtypes.uint64, x.get_buf(d).va_addr) for x, d in zip(bufs, devs))
|
||||
return addrs[0] if len(addrs) == 1 else UOp(Ops.STACK, src=addrs)
|
||||
return addrs[0] if len(addrs) == 1 else UOp(Ops.STACK, dtypes.uint64, addrs)
|
||||
|
||||
pm_resolve_patches = PatternMatcher([
|
||||
# multi
|
||||
@@ -442,26 +441,27 @@ pm_resolve_patches = PatternMatcher([
|
||||
(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.SLICE, Ops.MSTACK}, name="buf").index(UPat(Ops.STACK, name="off")).store(UPat(Ops.STACK, name="val")), fold_const_store),
|
||||
(UPat({Ops.BUFFER, Ops.SLICE, Ops.MSTACK}, name="buf").index(UPat.cvar("off"))
|
||||
.store(UPat.any(UPat.cvar("val"), 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}"))])
|
||||
|
||||
def link_buf_key(a:UOp): return a.key, to_tuple(a.device)
|
||||
link_buf_cache:dict[tuple[bytes, tuple[str, ...]], UOp] = {}
|
||||
link_linear_cache:dict[tuple[bytes, bool], UOp] = {}
|
||||
hcq_link_cache:dict[tuple[bytes, tuple[str, ...]], UOp] = {}
|
||||
|
||||
@track_rewrites(lambda _,jit,cache,ret: f"HCQ Link {pluralize('Kernel', len(ret.src))}")
|
||||
def hcq_link(linear:UOp, jit=False, cache=True) -> UOp:
|
||||
if (linked:=link_linear_cache.get(linear_key:=(linear.key, jit))) is not None: return linked
|
||||
def link_cache_key(a:UOp): return a.key, to_tuple(a.device)
|
||||
pm_link_cache = PatternMatcher([(UPat(Ops.AFTER, name="a"), lambda a: hcq_link_cache.get(link_cache_key(a)))])
|
||||
|
||||
bufs = {(j,i):a for j,c in enumerate(linear.src) for i,a in enumerate(c.src[1:], 1)
|
||||
if a.op is Ops.AFTER and unwrap_mstack(a.src[0])[0].tag in HCQ_CACHE_TAGS}
|
||||
linear = linear.substitute({x:link_buf_cache[k] for a in bufs.values() if (k:=link_buf_key(a)) in link_buf_cache for x in (a, a.src[0])}, walk=True)
|
||||
linear = graph_rewrite(linear, pm_resolve_patches+symbolic+pm_assert_no_afters, bpm=pm_bufferize, ctx=cache, bottom_up=False,
|
||||
name="resolve patches")
|
||||
for (j,i),a in bufs.items(): link_buf_cache.setdefault(link_buf_key(a), linear.src[j].src[i])
|
||||
if cache: link_linear_cache[linear_key] = linear
|
||||
@track_rewrites(lambda _,jit,ret: f"HCQ Link {pluralize('Kernel', len(ret.src))}")
|
||||
def hcq_link(linear:UOp, jit=False) -> UOp:
|
||||
cacheable = {(j,i):a for j,c in enumerate(linear.src) for i,a in enumerate(c.src[1:], 1)
|
||||
if a.op is Ops.AFTER and unwrap_mstack(a.src[0])[0].tag in HCQ_CACHE_TAGS}
|
||||
hits = {a.src[0]:hcq_link_cache[key] for a in cacheable.values() if (key:=link_cache_key(a)) in hcq_link_cache}
|
||||
linear = graph_rewrite(linear, pm_link_cache, name="apply link cache").substitute(hits, walk=True)
|
||||
linear = graph_rewrite(linear, pm_bufferize, ctx=jit, bottom_up=True, walk=True, name="bufferize placeholders")
|
||||
linear = graph_rewrite(linear, pm_resolve_patches + symbolic, bottom_up=False, name="simplify patches")
|
||||
linear = graph_rewrite(linear, pm_assert_no_afters, name="assert no afters")
|
||||
for (j,i),a in cacheable.items(): hcq_link_cache.setdefault(link_cache_key(a), linear.src[j].src[i])
|
||||
return linear
|
||||
|
||||
# *****************
|
||||
@@ -474,42 +474,42 @@ class HCQ2Compiled(Compiled):
|
||||
self.device_id:int = int(device.split(":")[1]) if ":" in device else 0
|
||||
|
||||
self.pm_bufferize = PatternMatcher([
|
||||
(UPat(Ops.PARAM, tag="timeline_signal"), lambda ctx: ctx[0].timeline_signal()),
|
||||
(UPat(Ops.PARAM, tag="timeline_value"), lambda ctx: ctx[0].timeline_value()),
|
||||
(UPat(Ops.PARAM, tag="sentinel_signal"), lambda ctx: ctx[0].timeline_signal("sentinel", (1 << 64) - 1)),
|
||||
(UPat(Ops.PARAM, name="b"), lambda ctx, b: None if b.tag is None else ctx[0].new_buffer(b, cache=ctx[1]))
|
||||
(UPat(Ops.PARAM, name="b"), lambda ctx, b: None if b.tag is None else ctx[0].new_buffer(b, jit=ctx[1]))
|
||||
])
|
||||
|
||||
super().__init__(device, allocator, compilers, runtime, None, arch=arch)
|
||||
super().__init__(device, allocator, compilers, lambda *a, **kw: None, None, arch=arch)
|
||||
|
||||
self.rt_buffer = Buffer(self.device, 64 << 20, dtypes.uint8, options=BufferSpec(uncached=True, cpu_access=True))
|
||||
self.rt_allocator = BumpAllocator(64 << 20)
|
||||
self.rt_allocator = BumpAllocator(64 << 20, wrap=False)
|
||||
|
||||
def new_buffer(self, b:UOp, cache:bool) -> Buffer:
|
||||
if cache or b.tag in HCQ_CACHE_TAGS:
|
||||
def new_buffer(self, b:UOp, jit:bool) -> Buffer:
|
||||
if jit or b.tag in HCQ_CACHE_TAGS:
|
||||
return Buffer(self.device, b.max_numel(), b.dtype, options=BufferSpec(uncached=True, cpu_access=True, nolru=True))
|
||||
return self.rt_buffer.view(b.max_numel(), b.dtype, self.rt_allocator.alloc(b.max_numel() * b.dtype.itemsize, alignment=128))
|
||||
|
||||
@functools.cache
|
||||
def timeline_signal(self, queue:str, init_value:int=0) -> Buffer:
|
||||
def timeline_signal(self, queue:str|None=None, init_value:int=0) -> Buffer:
|
||||
buf = Buffer(self.device, 1, dtypes.uint64, options=BufferSpec(host=True, uncached=True, cpu_access=True), preallocate=True)
|
||||
buf.as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')[0] = init_value
|
||||
return buf
|
||||
|
||||
@functools.cache
|
||||
def timeline_value(self, queue:str, init_value:int=1) -> Buffer:
|
||||
def timeline_value(self, queue:str|None=None, init_value:int=1) -> Buffer:
|
||||
buf = Buffer("CPU", 1, dtypes.uint64, preallocate=True)
|
||||
buf.as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')[0] = init_value
|
||||
return buf
|
||||
|
||||
def synchronize(self, timeout:int|None=None):
|
||||
if not hasattr(self, 'iface'): return
|
||||
sig = self.timeline_signal("COMPUTE:0").as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')
|
||||
tl = self.timeline_value("COMPUTE:0").as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')
|
||||
sig = self.timeline_signal().as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')
|
||||
tl = self.timeline_value().as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')
|
||||
st = time.perf_counter()
|
||||
while sig[0] < tl[0] - 1:
|
||||
if time.perf_counter() - st > (timeout or 3000) / 1000: self.on_device_hang()
|
||||
|
||||
def on_device_hang(self): raise RuntimeError(f"{self.device} hang detected")
|
||||
|
||||
def device_props(self) -> dict[str,Any]: return {} # to be overridden if needed. dict keys are backend dependent.
|
||||
|
||||
def count(self) -> int: return self.iface.count if hasattr(self, 'iface') else 1
|
||||
+55
-64
@@ -3,8 +3,8 @@ from typing import cast, Any, Callable
|
||||
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.hcq2 import HCQ2Compiled, HCQAllocator, HCQ2Buffer, encode_kernargs_clike, make_cmdbuf
|
||||
from tinygrad.runtime.support.hcq2 import make_binary_patch, make_patches
|
||||
from extra.hcq2.hcq2 import HCQ2Compiled, HCQAllocator, HCQ2Buffer, encode_kernargs_clike, make_getaddr, make_ins, make_cmdbuf, make_placeholder
|
||||
from extra.hcq2.hcq2 import make_binary_patch
|
||||
from tinygrad.uop.ops import sint, UOp
|
||||
from tinygrad.device import Compiled, BufferSpec, Buffer, Device
|
||||
from tinygrad.dtype import dtypes
|
||||
@@ -36,9 +36,7 @@ class PM4Ops(FastEnum):
|
||||
SET_SH_REG = auto(); SET_UCONFIG_REG = auto(); WAIT_REG_MEM = auto(); ACQUIRE_MEM = auto() # noqa: E702
|
||||
RELEASE_MEM = auto(); DISPATCH_DIRECT = auto(); EVENT_WRITE = auto() # noqa: E702
|
||||
|
||||
def pkt3(ctx, op:PM4Ops, *vals):
|
||||
return UOp(Ops.INS, arg=op, src=tuple(UOp.const(dtypes.uint32, x)
|
||||
for x in (ctx.pm4.PACKET3(getattr(ctx.pm4, f"PACKET3_{op.name}"), len(vals) - 1), *vals)))
|
||||
def pkt3(ctx, op:PM4Ops, *vals): return make_ins(op, ctx.pm4.PACKET3(getattr(ctx.pm4, f"PACKET3_{op.name}"), len(vals) - 1), *vals)
|
||||
|
||||
def wreg(ctx, reg:AMDReg, *args:sint, **kwargs:int):
|
||||
if bool(args) == bool(kwargs): raise RuntimeError('One (and only one) of *args or **kwargs must be specified')
|
||||
@@ -92,26 +90,26 @@ def memory_barrier(ctx):
|
||||
reg_done=getattr(ctx.nbio, f'regBIF_BX_PF{pf}_GPU_HDP_FLUSH_DONE').addr[0], value=0xffffffff),
|
||||
acquire_mem(ctx)))
|
||||
|
||||
def pm4_wait(ctx, dst, val): return wait_reg_mem(ctx, val, mem=dst.getaddr(ctx.devs))
|
||||
def pm4_wait(ctx, dst, val): return wait_reg_mem(ctx, val, mem=make_getaddr(dst, ctx.devs))
|
||||
|
||||
def pm4_barrier(ctx): return memory_barrier(ctx)
|
||||
|
||||
def pm4_store(ctx, dst, val):
|
||||
if val.op is Ops.BINARY: return None
|
||||
return release_mem(ctx, dst.getaddr(ctx.devs), val, ctx.pm4.data_sel__mec_release_mem__send_32_bit_low,
|
||||
return release_mem(ctx, make_getaddr(dst, ctx.devs), val, ctx.pm4.data_sel__mec_release_mem__send_32_bit_low,
|
||||
ctx.pm4.int_sel__mec_release_mem__send_interrupt_after_write_confirm, cache_flush=True)
|
||||
|
||||
def pm4_timestamp(ctx, dst):
|
||||
return release_mem(ctx, dst.getaddr(ctx.devs), 0, ctx.pm4.data_sel__mec_release_mem__send_gpu_clock_counter,
|
||||
return release_mem(ctx, make_getaddr(dst, ctx.devs), 0, ctx.pm4.data_sel__mec_release_mem__send_gpu_clock_counter,
|
||||
ctx.pm4.int_sel__mec_release_mem__none)
|
||||
|
||||
def pm4_program(ctx, call, prg):
|
||||
data, info = prg.arg
|
||||
lib_gpu = prg.src[0]
|
||||
args = encode_kernargs_clike(call, prg, ctx.devs)
|
||||
prog_addr = lib_gpu.getaddr(ctx.devs) + data.entry_point_offset
|
||||
scratch_addr = UOp.placeholder((data.private_segment_size,), dtypes.uint8, 0, device=ctx.devs).rtag("scratch").getaddr(ctx.devs)
|
||||
args_addr = args.getaddr(ctx.devs)
|
||||
prog_addr = make_getaddr(lib_gpu, ctx.devs) + data.entry_point_offset
|
||||
scratch_addr = make_getaddr(make_placeholder(ctx.devs, data.private_segment_size, dtypes.uint8, "scratch", unique=False), ctx.devs)
|
||||
args_addr = make_getaddr(args, ctx.devs)
|
||||
|
||||
user_regs = []
|
||||
if data.enable_private_segment_sgpr:
|
||||
@@ -146,37 +144,31 @@ pm_pm4_opsel = PatternMatcher([
|
||||
(UPat(Ops.INS, arg="store", src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val"))), pm4_store),
|
||||
])
|
||||
|
||||
def pm4_submit(ctx, lin):
|
||||
# ensure compute queues are allocated
|
||||
for d in (devs:=ctx.devs): q = Device[d].compute_queue
|
||||
ring, wptr, doorbell, put_ptr = (UOp.placeholder((b.size,), b.dtype, 0, device=devs).rtag(f"COMPUTE:0_{name}")
|
||||
def pm4_submit(cmdbuf, devs):
|
||||
size, zero = UOp.const(dtypes.uint32, cmdbuf.nbytes() // dtypes.uint32.itemsize), UOp.const(dtypes.int, 0)
|
||||
|
||||
# the compute queue's ring and its host-side ring/write/put pointers (placeholders, resolved in pm_bufferize)
|
||||
for d in devs: q = Device[d].compute_queue
|
||||
ring, wptr, doorbell, put_ptr = (make_placeholder(devs, b.size, b.dtype, ("COMPUTE:0", name), unique=False)
|
||||
for name, b in (("ring", q.ring), ("write_ptr", q.write_ptr), ("doorbell", q.doorbell), ("put_value", q.put_value)))
|
||||
|
||||
# two tail dwords coordinate safe IB reuse: GPU completions and host submits
|
||||
size_dw = sum(len(ins.src) for ins in lin.src) + len(release_mem(ctx, 0, 0).src)
|
||||
assert size_dw < (1 << 20), f"indirect buffer of {size_dw} dwords doesn't fit one packet"
|
||||
# place the cmdbuf at the ring's write offset, wrapping the ring
|
||||
put = put_ptr.index(zero)
|
||||
next_put = put + size.cast(put.dtype)
|
||||
i = UOp.range(size, 0, dtype=dtypes.int, src=(cmdbuf,))
|
||||
ring_idx = ((put + i.cast(put.dtype)) % q.ring.size).cast(dtypes.int)
|
||||
|
||||
ib = UOp.placeholder((size_dw + 2,), dtypes.uint32, next(UOp.unique_num), device=devs, volatile=True).rtag("cmdbuf")
|
||||
done_idx, submit_idx = UOp.const(dtypes.int, size_dw + 0), UOp.const(dtypes.int, size_dw + 1)
|
||||
submitted = (counter:=ib.after(make_patches(ib, [((size_dw + i) * 4, UOp.const(dtypes.uint32, 0)) for i in range(2)])).index(submit_idx)).load()
|
||||
completed = ib.after(loop:=UOp.loop(0)).index(done_idx).load()
|
||||
ib_free = completed.end(loop, completed != submitted)
|
||||
# copy the cmdbuf into the ring and advance the put/write pointers
|
||||
copy_to_ring = ring.index(ring_idx).store(cmdbuf.index(i).load()).end(i)
|
||||
bump_put_ptr = put_ptr.index(zero).store(next_put)
|
||||
bump_wptr = wptr.index(zero).store(next_put)
|
||||
|
||||
bump_fence = pm4_store(ctx, UOp(Ops.SLICE, dtypes.uint32, (ib, UOp.const(dtypes.weakint, size_dw)), 2), (submitted + 1).cast(dtypes.uint64))
|
||||
cmdbuf = make_cmdbuf(lin.replace(src=lin.src + (bump_fence,)), devs, buf=ib, dep=ib_free)
|
||||
# ring the doorbell once the copy and pointer bumps have landed
|
||||
flush = UOp.barrier(copy_to_ring, bump_put_ptr, bump_wptr)
|
||||
return doorbell.after(flush).index(zero).store(next_put)
|
||||
|
||||
# the ring itself only carries a packet pointing at the ib, wrapping the ring
|
||||
put = put_ptr.index(zero:=UOp.const(dtypes.int, 0))
|
||||
pkt = (ctx.pm4.PACKET3(ctx.pm4.PACKET3_INDIRECT_BUFFER, 2), *data64_le(cmdbuf.getaddr(devs)), size_dw | ctx.pm4.INDIRECT_BUFFER_VALID)
|
||||
write_pkt = UOp.barrier(*[ring.index(((put + off) % q.ring.size).cast(dtypes.int)).store(UOp.const(dtypes.uint32, x)) for off,x in enumerate(pkt)])
|
||||
|
||||
# advance the put/write pointers past the packet
|
||||
bump_put_ptr = put_ptr.index(zero).store(put + len(pkt))
|
||||
bump_wptr = wptr.index(zero).store(put + len(pkt))
|
||||
flush = UOp.barrier(write_pkt, bump_put_ptr, bump_wptr, counter.store(submitted + 1))
|
||||
return doorbell.after(flush).index(zero).store(put + len(pkt))
|
||||
|
||||
pm_pm4_submit = PatternMatcher([(UPat(Ops.LINEAR, name="lin"), pm4_submit)])
|
||||
pm_pm4_submit = PatternMatcher([(UPat(Ops.LINEAR, name="lin"),
|
||||
lambda lin: pm4_submit(make_cmdbuf(lin, to_tuple(lin.arg[0])), to_tuple(lin.arg[0])))])
|
||||
|
||||
# *****************
|
||||
# SDMA
|
||||
@@ -184,36 +176,36 @@ pm_pm4_submit = PatternMatcher([(UPat(Ops.LINEAR, name="lin"), pm4_submit)])
|
||||
class SDMAOps(FastEnum): COPY = auto(); POLL_REGMEM = auto(); FENCE = auto(); TRAP = auto(); TIMESTAMP = auto() # noqa: E702
|
||||
|
||||
def sdma_copy(ctx, call):
|
||||
sz = call.src[2].max_numel() * call.src[2].dtype.itemsize
|
||||
src_addr, dst_addr = call.src[2].getaddr(ctx.devs), call.src[1].getaddr(ctx.devs)
|
||||
return call.ins(SDMAOps.COPY, src=tuple(UOp.const(dtypes.uint32, x) for off in range(0, sz, ctx.max_copy_size) for x in (
|
||||
ctx.sdma.SDMA_OP_COPY | ctx.sdma.SDMA_PKT_COPY_LINEAR_HEADER_SUB_OP(ctx.sdma.SDMA_SUBOP_COPY_LINEAR),
|
||||
ctx.sdma.SDMA_PKT_COPY_LINEAR_COUNT_COUNT(min(sz-off, ctx.max_copy_size)-1), 0, *data64_le(src_addr+off), *data64_le(dst_addr+off))))
|
||||
dst, src = call.src[1], call.src[2]
|
||||
sz = src.max_numel() * src.dtype.itemsize
|
||||
src_addr, dst_addr = make_getaddr(src, ctx.devs), make_getaddr(dst, ctx.devs)
|
||||
return UOp(Ops.LINEAR, dtypes.void, tuple([make_ins(SDMAOps.COPY,
|
||||
ctx.sdma.SDMA_OP_COPY | ctx.sdma.SDMA_PKT_COPY_LINEAR_HEADER_SUB_OP(ctx.sdma.SDMA_SUBOP_COPY_LINEAR),
|
||||
ctx.sdma.SDMA_PKT_COPY_LINEAR_COUNT_COUNT(min(sz - off, ctx.max_copy_size) - 1), 0,
|
||||
*data64_le(src_addr + off), *data64_le(dst_addr + off)) for off in range(0, sz, ctx.max_copy_size)]))
|
||||
|
||||
def sdma_wait(ctx, ins, dst, val):
|
||||
def sdma_wait(ctx, dst, val):
|
||||
op = ctx.sdma.SDMA_OP_POLL_REGMEM | ctx.sdma.SDMA_PKT_POLL_REGMEM_HEADER_FUNC(WAIT_REG_MEM_FUNCTION_GEQ) \
|
||||
| ctx.sdma.SDMA_PKT_POLL_REGMEM_HEADER_MEM_POLL(1)
|
||||
return ins.ins(SDMAOps.POLL_REGMEM, src=tuple(UOp.const(dtypes.uint32, x) for x in (
|
||||
op, *data64_le(dst.getaddr(ctx.devs)), val, 0xffffffff,
|
||||
ctx.sdma.SDMA_PKT_POLL_REGMEM_DW5_INTERVAL(0x04) | ctx.sdma.SDMA_PKT_POLL_REGMEM_DW5_RETRY_COUNT(0xfff))))
|
||||
return make_ins(SDMAOps.POLL_REGMEM, op, *data64_le(make_getaddr(dst, ctx.devs)), val, 0xffffffff,
|
||||
ctx.sdma.SDMA_PKT_POLL_REGMEM_DW5_INTERVAL(0x04) | ctx.sdma.SDMA_PKT_POLL_REGMEM_DW5_RETRY_COUNT(0xfff))
|
||||
|
||||
def sdma_store(ctx, ins, dst, val):
|
||||
def sdma_store(ctx, dst, val):
|
||||
op = ctx.sdma.SDMA_OP_FENCE | (ctx.sdma.SDMA_PKT_FENCE_HEADER_MTYPE(3) if ctx.target[0] != 9 else 0)
|
||||
return UOp(Ops.LINEAR, src=(
|
||||
ins.ins(SDMAOps.FENCE, src=tuple(UOp.const(dtypes.uint32, x) for x in (op, *data64_le(dst.getaddr(ctx.devs)), val))),
|
||||
ins.ins(SDMAOps.TRAP, src=tuple(UOp.const(dtypes.uint32, x) for x in (ctx.sdma.SDMA_OP_TRAP, 0)))))
|
||||
return UOp(Ops.LINEAR, dtypes.void, (
|
||||
make_ins(SDMAOps.FENCE, op, *data64_le(make_getaddr(dst, ctx.devs)), val), make_ins(SDMAOps.TRAP, ctx.sdma.SDMA_OP_TRAP, 0)))
|
||||
|
||||
def sdma_timestamp(ctx, ins, dst):
|
||||
def sdma_timestamp(ctx, dst):
|
||||
op = ctx.sdma.SDMA_OP_TIMESTAMP | ctx.sdma.SDMA_PKT_TIMESTAMP_GET_HEADER_SUB_OP(ctx.sdma.SDMA_SUBOP_TIMESTAMP_GET_GLOBAL)
|
||||
return ins.ins(SDMAOps.TIMESTAMP, src=tuple(UOp.const(dtypes.uint32, x) for x in (op, *data64_le(dst.getaddr(ctx.devs)))))
|
||||
return make_ins(SDMAOps.TIMESTAMP, op, *data64_le(make_getaddr(dst, ctx.devs)))
|
||||
|
||||
pm_sdma_opsel = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.COPY),), name="call", allow_any_len=True), sdma_copy),
|
||||
|
||||
(UPat(Ops.INS, arg="barrier"), lambda: UOp(Ops.NOOP, dtypes.void, ())),
|
||||
(UPat(Ops.INS, arg="wait", src=(UPat(name="dst"), UPat(name="val")), name="ins"), sdma_wait),
|
||||
(UPat(Ops.INS, arg="timestamp", src=(UPat(name="dst"),), name="ins"), sdma_timestamp),
|
||||
(UPat(Ops.INS, arg="store", src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val")), name="ins"), sdma_store),
|
||||
(UPat(Ops.INS, arg="wait", src=(UPat(name="dst"), UPat(name="val"))), sdma_wait),
|
||||
(UPat(Ops.INS, arg="timestamp", src=(UPat(name="dst"),)), sdma_timestamp),
|
||||
(UPat(Ops.INS, arg="store", src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val"))), sdma_store),
|
||||
])
|
||||
|
||||
def sdma_submit(cmdbuf, devs):
|
||||
@@ -222,7 +214,7 @@ def sdma_submit(cmdbuf, devs):
|
||||
|
||||
# the sdma queue's ring and its host-side ring/write/put pointers
|
||||
for d in devs: q = Device[d].sdma_queue(0)
|
||||
ring, wptr, doorbell, put_ptr = (UOp.placeholder((b.size,), b.dtype, 0, device=devs).rtag(f"COPY:0_{name}")
|
||||
ring, wptr, doorbell, put_ptr = (make_placeholder(devs, b.size, b.dtype, ("COPY:0", name), unique=False)
|
||||
for name, b in (("ring", q.ring), ("write_ptr", q.write_ptr), ("doorbell", q.doorbell), ("put_value", q.put_value)))
|
||||
|
||||
# sdma needs the cmdbuf contiguous: if it won't fit before the ring end, restart at 0 and zero the tail
|
||||
@@ -248,7 +240,7 @@ def sdma_submit(cmdbuf, devs):
|
||||
return doorbell.after(flush).index(zero).store(next_put_b)
|
||||
|
||||
pm_sdma_submit = PatternMatcher([(UPat(Ops.LINEAR, name="lin"),
|
||||
lambda ctx, lin: sdma_submit(make_cmdbuf(lin, ctx.devs), ctx.devs))])
|
||||
lambda lin: sdma_submit(make_cmdbuf(lin, to_tuple(lin.arg[0])), to_tuple(lin.arg[0])))])
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AMDEncodeCtx: # encode-time constants for one queue: devs (every cmdbuf address resolves into these) + gfx version + packet/ip modules
|
||||
@@ -259,7 +251,7 @@ def encode_queue(q:UOp) -> UOp|None:
|
||||
d = Device[(devs:=to_tuple(q.arg[0]))[0]]
|
||||
ctx = AMDEncodeCtx(devs, d.target, d.pm4, d.sdma, d.soc, d.gc, d.nbio, d.xccs, d.max_copy_size, d.tmpring_size)
|
||||
opsel, submit = (pm_pm4_opsel, pm_pm4_submit) if q.arg[1].startswith("COMPUTE") else (pm_sdma_opsel, pm_sdma_submit)
|
||||
return submit.rewrite(graph_rewrite(q, opsel + pm_flatten_linear, walk=True, ctx=ctx, name=f"{q.arg[1]} opsel"), ctx)
|
||||
return submit.rewrite(graph_rewrite(q, opsel + pm_flatten_linear, walk=True, ctx=ctx, name=f"{q.arg[1]} opsel"))
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AMDProgramData:
|
||||
@@ -287,7 +279,7 @@ def amd_build_program(prg:UOp) -> UOp:
|
||||
wave32=bool(desc.kernel_code_properties & 0x400), private_segment_size=desc.private_segment_fixed_size, kernargs_segment_size=desc.kernarg_size,
|
||||
kernargs_alloc_size=desc.kernarg_size + (ctypes.sizeof(hsa.hsa_kernel_dispatch_packet_t) if edp else 0), enable_dispatch_ptr=edp,
|
||||
enable_private_segment_sgpr=desc.kernel_code_properties & hsa.AMD_KERNEL_CODE_PROPERTIES_ENABLE_SGPR_PRIVATE_SEGMENT_BUFFER)
|
||||
buf = UOp.placeholder((len(image),), dtypes.uint8, next(UOp.unique_num), device=prg.device).rtag("program")
|
||||
buf = make_placeholder(prg.device, len(image), dtypes.uint8, "program")
|
||||
cached = _amd_program_cache[key] = prg.replace(src=(buf.after(make_binary_patch(buf, bytes(image))),), arg=(data, prg.arg))
|
||||
return cached
|
||||
|
||||
@@ -517,8 +509,7 @@ class PCIIface(PCIIfaceBase):
|
||||
cq = d.compute_queue
|
||||
for b in (cq.put_value, cq.read_ptr, cq.write_ptr): b._buf.view.view(fmt='Q')[0] = 0
|
||||
d.iface.dev_impl.gfx.setup_ring(*cq.params)
|
||||
d.timeline_signal('COMPUTE:0')._buf.cpu_view().mv.cast('Q')[0] = \
|
||||
d.timeline_value('COMPUTE:0').as_memoryview(force_zero_copy=True).cast('Q')[0] - 1
|
||||
d.timeline_signal()._buf.cpu_view().mv.cast('Q')[0] = d.timeline_value().as_memoryview(force_zero_copy=True).cast('Q')[0] - 1
|
||||
|
||||
def sleep(self, timeout):
|
||||
if hasattr(self.pci_dev, 'irq_poller') and self.pci_dev.irq_poller is not None and (events_cnt:=len(self.pci_dev.irq_poller.poll(timeout))):
|
||||
@@ -637,10 +628,10 @@ class AMDDevice(HCQ2Compiled):
|
||||
|
||||
qname = f"{'COPY' if queue_type == kfd.KFD_IOC_QUEUE_TYPE_SDMA else 'COMPUTE'}:{idx}"
|
||||
self.pm_bufferize = PatternMatcher([
|
||||
(UPat(Ops.PARAM, tag=f"{qname}_{name}"), lambda ctx, b=getattr(queue, name): b) for name in ["ring", "write_ptr", "doorbell", "put_value"]
|
||||
(UPat(Ops.PARAM, tag={(qname, name)}), lambda ctx, b=getattr(queue, name): b) for name in ["ring", "write_ptr", "doorbell", "put_value"]
|
||||
] + [
|
||||
(UPat(Ops.PARAM, tag=f"{qname}_timeline_signal"), lambda ctx, q=qname: ctx[0].timeline_signal(q)),
|
||||
(UPat(Ops.PARAM, tag=f"{qname}_timeline_value"), lambda ctx, q=qname: ctx[0].timeline_value(q)),
|
||||
(UPat(Ops.PARAM, tag={(qname, "timeline_signal")}), lambda ctx, q=qname: ctx[0].timeline_signal(q)),
|
||||
(UPat(Ops.PARAM, tag={(qname, "timeline_value")}), lambda ctx, q=qname: ctx[0].timeline_value(q)),
|
||||
]) + self.pm_bufferize
|
||||
|
||||
return queue
|
||||
|
||||
@@ -31,12 +31,12 @@ def dname_of(device) -> str:
|
||||
|
||||
def alloc_like(shape, dtype, device, axis=None) -> Tensor:
|
||||
if isinstance(device, tuple) and axis is not None:
|
||||
return Tensor(Tensor.invalids(*shard_shape(shape, axis, len(device)), dtype=dtype, device=device).uop.unshard(axis), device=device)
|
||||
return Tensor(Tensor.invalids(*shard_shape(shape, axis, len(device)), dtype=dtype, device=device).uop.multi(axis), device=device)
|
||||
return Tensor.invalids(*shape, dtype=dtype, device=device)
|
||||
|
||||
def alloc_local(shape, dtype, device, axis=None) -> Tensor:
|
||||
if isinstance(device, tuple) and axis is not None:
|
||||
return Tensor(Tensor.invalids(*shape, dtype=dtype, device=device).uop.unshard(0), device=device)
|
||||
return Tensor(Tensor.invalids(*shape, dtype=dtype, device=device).uop.multi(0), device=device)
|
||||
return Tensor.invalids(*shape, dtype=dtype, device=device)
|
||||
|
||||
def compile_hip(src:str, defines:list[str]):
|
||||
|
||||
@@ -48,7 +48,7 @@ def _fused_ce_loss_bwd(gradient:UOp, kernel:UOp, label_smoothing:float):
|
||||
axis = logits_u.axis
|
||||
ndev = len(device)
|
||||
local_shape = tuple(s//ndev if i == axis else s for i,s in enumerate((MBS, SEQ, VOCAB)))
|
||||
d_logits = Tensor(Tensor.invalids(*local_shape, dtype=dtypes.bfloat16, device=device).uop.unshard(axis), device=device)
|
||||
d_logits = Tensor(Tensor.invalids(*local_shape, dtype=dtypes.bfloat16, device=device).uop.multi(axis), device=device)
|
||||
rows_per_dev = local_shape[0] * local_shape[1]
|
||||
seq_per_dev = local_shape[1]
|
||||
else:
|
||||
@@ -74,11 +74,11 @@ def fused_ce_loss(logits:Tensor, targets:Tensor, label_smoothing:float=0.1) -> T
|
||||
axis = logits.uop.axis
|
||||
assert axis in (0, 1), f"unsupported sharding axis={axis} for CE loss"
|
||||
ndev = len(logits.device)
|
||||
loss_out = Tensor(Tensor.invalids(rows // ndev, dtype=dtypes.float32, device=logits.device).uop.unshard(0),
|
||||
loss_out = Tensor(Tensor.invalids(rows // ndev, dtype=dtypes.float32, device=logits.device).uop.multi(0),
|
||||
device=logits.device)
|
||||
max_out = Tensor(Tensor.invalids(rows // ndev, dtype=dtypes.float32, device=logits.device).uop.unshard(0),
|
||||
max_out = Tensor(Tensor.invalids(rows // ndev, dtype=dtypes.float32, device=logits.device).uop.multi(0),
|
||||
device=logits.device)
|
||||
lse_out = Tensor(Tensor.invalids(rows // ndev, dtype=dtypes.float32, device=logits.device).uop.unshard(0),
|
||||
lse_out = Tensor(Tensor.invalids(rows // ndev, dtype=dtypes.float32, device=logits.device).uop.multi(0),
|
||||
device=logits.device)
|
||||
local_shape = tuple(s//ndev if i == axis else s for i,s in enumerate(logits.shape))
|
||||
rows_per_dev = local_shape[0] * local_shape[1]
|
||||
|
||||
@@ -16,7 +16,7 @@ def _custom_quantize_fp8_with_amax(fp8_out:UOp, amax_out:UOp, x:UOp, amax_state:
|
||||
|
||||
wg = UOp.range(NUM_WG, 0, AxisType.GLOBAL)
|
||||
tid = UOp.range(THREADS_PER_WG, 1, AxisType.LOCAL)
|
||||
it = UOp.range((n_elems // VEC) // (NUM_WG * THREADS_PER_WG), 2, AxisType.WEAK)
|
||||
it = UOp.range((n_elems // VEC) // (NUM_WG * THREADS_PER_WG), 2, AxisType.LOOP)
|
||||
lane = UOp.range(VEC, 3, AxisType.UNROLL)
|
||||
|
||||
idx = (((it * NUM_WG + wg) * THREADS_PER_WG + tid) * VEC) + lane
|
||||
@@ -36,13 +36,13 @@ def _custom_quantize_fp8_with_amax(fp8_out:UOp, amax_out:UOp, x:UOp, amax_state:
|
||||
lmax_val = lmax.after(lmax_store.end(it))[0]
|
||||
|
||||
lds = UOp.placeholder((THREADS_PER_WG,), dtypes.float, slot=0, addrspace=AddrSpace.LOCAL)
|
||||
lds = lds.after(lds[tid].store(lmax_val))
|
||||
lds = lds.after(lds[tid].store(lmax_val).barrier())
|
||||
|
||||
step = THREADS_PER_WG // 2
|
||||
while step:
|
||||
active = tid < step
|
||||
other = lds[(tid + step).valid(active)].load()
|
||||
lds = lds.after(lds[tid.valid(active)].store(lds[tid].maximum(other)))
|
||||
lds = lds.after(lds[tid.valid(active)].store(lds[tid].maximum(other)).barrier())
|
||||
step //= 2
|
||||
|
||||
device = device[0].split(":")[0] if isinstance(device, tuple) else device.split(":")[0]
|
||||
|
||||
@@ -4,7 +4,7 @@ from hexdump import hexdump
|
||||
from copy import deepcopy
|
||||
import pathlib, sys
|
||||
from tinygrad.helpers import to_mv, getenv
|
||||
from tinygrad.runtime.autogen import mesa
|
||||
from tinygrad.runtime.autogen import adreno
|
||||
sys.path.append(pathlib.Path(__file__).parent.parent.parent.as_posix())
|
||||
|
||||
IOCTL = getenv("IOCTL", 0)
|
||||
@@ -23,7 +23,7 @@ for child in xml.getroot():
|
||||
CAPTURED_STATE = {}
|
||||
|
||||
REGS = {}
|
||||
for k, v in mesa.__dict__.items():
|
||||
for k, v in adreno.__dict__.items():
|
||||
if k.startswith("REG_") and isinstance(v, int) and v > 1024: REGS[v] = k
|
||||
|
||||
from extra.qcom_gpu_driver import msm_kgsl
|
||||
@@ -42,7 +42,7 @@ def get_struct(argp, stype):
|
||||
|
||||
def format_struct(s):
|
||||
sdats = []
|
||||
for field_name, *_ in s._fields_:
|
||||
for field_name, *_ in s._real_fields_:
|
||||
if field_name in {"__pad", "PADDING_0"}: continue
|
||||
dat = getattr(s, field_name)
|
||||
if isinstance(dat, int): sdats.append(f"{field_name}:0x{dat:X}")
|
||||
@@ -96,9 +96,9 @@ def parse_cmd_buf(dat):
|
||||
CAPTURED_STATE['LOAD_FRAGS'].append((state_block, state_type, num_unit, dst_off))
|
||||
|
||||
if state_block == SB6_CS_SHADER:
|
||||
from tinygrad.runtime.support.compiler_mesa import disas_adreno
|
||||
from extra.disassemblers.adreno import disasm_raw
|
||||
if state_type == ST6_SHADER and IOCTL > 3:
|
||||
disas_adreno(get_mem(((vals[2] << 32) | vals[1]), num_unit * 128))
|
||||
disasm_raw(get_mem(((vals[2] << 32) | vals[1]), num_unit * 128))
|
||||
if state_type == ST6_CONSTANTS:
|
||||
x = get_mem(((vals[2] << 32) | vals[1]), num_unit*4)
|
||||
CAPTURED_STATE['constants'] = x[:]
|
||||
@@ -142,7 +142,7 @@ def parse_cmd_buf(dat):
|
||||
vals = struct.unpack("I"*size, dat[ptr+4:ptr+4+4*size])
|
||||
if IOCTL > 0: print(f"{ptr:3X} -- typ 4: {size=:3d}, {reg_name}", hprint(vals))
|
||||
for vi,v in enumerate(vals): CAPTURED_STATE[offset+vi] = v
|
||||
if offset == mesa.REG_A6XX_SP_CS_CONFIG:
|
||||
if offset == adreno.REG_A6XX_SP_CS_CONFIG:
|
||||
val = vals[0]
|
||||
if IOCTL > 0:
|
||||
print(f"\tBINDLESS_TEX={(val >> 0) & 0b1}")
|
||||
@@ -215,3 +215,79 @@ def install_hook(c_function, python_function):
|
||||
|
||||
libc = ctypes.CDLL(ctypes.util.find_library("libc"))
|
||||
install_hook(libc.ioctl, ioctl)
|
||||
|
||||
def before_launch():
|
||||
global CAPTURED_STATE
|
||||
CAPTURED_STATE.clear()
|
||||
def collect_last_launch_state():
|
||||
global CAPTURED_STATE
|
||||
return deepcopy(CAPTURED_STATE)
|
||||
def compare_launch_state(state, good_state):
|
||||
cmp = [
|
||||
(adreno.REG_A6XX_SP_CS_CONFIG, adreno.A6XX_SP_CS_CONFIG_NTEX__MASK),
|
||||
(adreno.REG_A6XX_SP_CS_CONFIG, adreno.A6XX_SP_CS_CONFIG_NSAMP__MASK),
|
||||
(adreno.REG_A6XX_SP_CS_CONFIG, adreno.A6XX_SP_CS_CONFIG_NIBO__MASK),
|
||||
(adreno.REG_A6XX_SP_CS_CONFIG, adreno.A6XX_SP_CS_CONFIG_ENABLED),
|
||||
(adreno.REG_A6XX_SP_CS_CONFIG, adreno.A6XX_SP_CS_CONFIG_BINDLESS_TEX),
|
||||
(adreno.REG_A6XX_SP_CS_CONFIG, adreno.A6XX_SP_CS_CONFIG_BINDLESS_SAMP),
|
||||
(adreno.REG_A6XX_SP_CS_CONFIG, adreno.A6XX_SP_CS_CONFIG_BINDLESS_IBO),
|
||||
(adreno.REG_A6XX_SP_CS_CONFIG, adreno.A6XX_SP_CS_CONFIG_BINDLESS_UBO),
|
||||
|
||||
(adreno.REG_A6XX_SP_CS_CTRL_REG0, adreno.A6XX_SP_CS_CTRL_REG0_HALFREGFOOTPRINT__MASK),
|
||||
(adreno.REG_A6XX_SP_CS_CTRL_REG0, adreno.A6XX_SP_CS_CTRL_REG0_FULLREGFOOTPRINT__MASK),
|
||||
(adreno.REG_A6XX_SP_CS_CTRL_REG0, adreno.A6XX_SP_CS_CTRL_REG0_BRANCHSTACK__MASK),
|
||||
(adreno.REG_A6XX_SP_CS_CTRL_REG0, adreno.A6XX_SP_CS_CTRL_REG0_FULLREGFOOTPRINT__MASK),
|
||||
(adreno.REG_A6XX_SP_CS_CTRL_REG0, adreno.A6XX_SP_CS_CTRL_REG0_THREADMODE__MASK),
|
||||
(adreno.REG_A6XX_SP_CS_CTRL_REG0, adreno.A6XX_SP_CS_CTRL_REG0_EARLYPREAMBLE),
|
||||
(adreno.REG_A6XX_SP_CS_CTRL_REG0, adreno.A6XX_SP_CS_CTRL_REG0_MERGEDREGS),
|
||||
|
||||
(adreno.REG_A6XX_SP_CS_PVT_MEM_PARAM, adreno.A6XX_SP_CS_PVT_MEM_PARAM_MEMSIZEPERITEM__MASK),
|
||||
(adreno.REG_A6XX_SP_CS_PVT_MEM_PARAM, adreno.A6XX_SP_CS_PVT_MEM_PARAM_HWSTACKSIZEPERTHREAD__MASK),
|
||||
|
||||
(adreno.REG_A6XX_SP_CS_UNKNOWN_A9B1, adreno.A6XX_SP_CS_UNKNOWN_A9B1_UNK5),
|
||||
(adreno.REG_A6XX_SP_CS_UNKNOWN_A9B1, adreno.A6XX_SP_CS_UNKNOWN_A9B1_UNK6),
|
||||
|
||||
(adreno.REG_A6XX_SP_CS_BRANCH_COND, 0xffffffff),
|
||||
|
||||
(adreno.REG_A6XX_HLSQ_CS_NDRANGE_0, adreno.A6XX_HLSQ_CS_NDRANGE_0_KERNELDIM__MASK),
|
||||
(adreno.REG_A6XX_HLSQ_CS_NDRANGE_0, adreno.A6XX_HLSQ_CS_NDRANGE_0_LOCALSIZEX__MASK),
|
||||
(adreno.REG_A6XX_HLSQ_CS_NDRANGE_0, adreno.A6XX_HLSQ_CS_NDRANGE_0_LOCALSIZEY__MASK),
|
||||
(adreno.REG_A6XX_HLSQ_CS_NDRANGE_0, adreno.A6XX_HLSQ_CS_NDRANGE_0_LOCALSIZEZ__MASK),
|
||||
|
||||
(adreno.REG_A6XX_HLSQ_CS_NDRANGE_1, 0xffffffff),
|
||||
(adreno.REG_A6XX_HLSQ_CS_NDRANGE_2, 0xffffffff),
|
||||
(adreno.REG_A6XX_HLSQ_CS_NDRANGE_3, 0xffffffff),
|
||||
(adreno.REG_A6XX_HLSQ_CS_NDRANGE_4, 0xffffffff),
|
||||
(adreno.REG_A6XX_HLSQ_CS_NDRANGE_5, 0xffffffff),
|
||||
(adreno.REG_A6XX_HLSQ_CS_NDRANGE_6, 0xffffffff),
|
||||
|
||||
(adreno.REG_A6XX_HLSQ_CS_CNTL_0, 0xffffffff),
|
||||
(adreno.REG_A6XX_HLSQ_CS_CNTL_1, 0xffffffff),
|
||||
(adreno.REG_A6XX_HLSQ_CS_KERNEL_GROUP_X, 0xffffffff),
|
||||
(adreno.REG_A6XX_HLSQ_CS_KERNEL_GROUP_Y, 0xffffffff),
|
||||
(adreno.REG_A6XX_HLSQ_CS_KERNEL_GROUP_Z, 0xffffffff),
|
||||
]
|
||||
|
||||
for x,m in cmp:
|
||||
print(f"Field {REGS[x]}, mask: 0x{m:X} cmp: {state.get(x, 0) & m} vs {good_state.get(x, 0) & m}")
|
||||
if state.get(x, 0) & m != good_state.get(x, 0) & m:
|
||||
return False, f"Field {REGS[x]}, mask: 0x{m:X} mismatch: {state.get(x, 0) & m} vs {good_state.get(x, 0) & m}"
|
||||
|
||||
for n in ['descriptors', 'ibos']:
|
||||
if n not in good_state: continue
|
||||
mv1, mv2 = state.get(n), good_state.get(n)
|
||||
|
||||
if len(mv1) != len(mv2): return False, f"{n}: len mismatch {len(mv1)} != {len(mv2)}"
|
||||
mv1 = memoryview(bytearray(mv1)).cast('I')
|
||||
mv2 = memoryview(bytearray(mv2)).cast('I')
|
||||
for i in range(len(mv2)):
|
||||
if i % 8 == 5 or i % 8 == 4: continue # addresses
|
||||
if mv1[i]!=mv2[i]: return False, f"{n}: content mismatch {i} {mv1[i]} {mv2[i]}"
|
||||
|
||||
for n in ['samplers']:
|
||||
if n not in good_state: continue
|
||||
mv1, mv2 = state.get(n), good_state.get(n)
|
||||
if len(mv1) != len(mv2): return False, f"{n}: len mismatch {len(mv1)} != {len(mv2)}"
|
||||
if any(mv1[i]!=mv2[i] for i in range(len(mv1))): return False, f"{n}: content mismatch"
|
||||
|
||||
return True, "PASS"
|
||||
|
||||
@@ -14,7 +14,7 @@ def _sharded_empty(shape:Tensor, ref:Tensor, axis:int|None, dtype:DTypeLike|None
|
||||
shard_axis = ref.uop.axis if axis is None else axis
|
||||
shape = tuple(s // len(ref.device) if i == shard_axis else s for i, s in enumerate(shape))
|
||||
axis = ref.uop.axis if axis is None else axis
|
||||
return Tensor(Tensor.invalids(*shape, dtype=dtype, device=ref.device).uop.unshard(axis), dtype=dtype, device=ref.device)
|
||||
return Tensor(Tensor.invalids(*shape, dtype=dtype, device=ref.device).uop.multi(axis), dtype=dtype, device=ref.device)
|
||||
|
||||
@functools.cache
|
||||
def custom_fused_qkv_rope_forward(q:UOp, k:UOp, v:UOp, xqkv:UOp, freqs_cis:UOp,
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
#include "kittens.cuh"
|
||||
|
||||
using namespace kittens;
|
||||
|
||||
#ifndef GEMM_M
|
||||
constexpr int GEMM_M = 8192;
|
||||
#endif
|
||||
#ifndef GEMM_N
|
||||
constexpr int GEMM_N = 8192;
|
||||
#endif
|
||||
#ifndef GEMM_K
|
||||
constexpr int GEMM_K = 8192;
|
||||
#endif
|
||||
#ifndef GEMM_E
|
||||
constexpr int GEMM_E = 8;
|
||||
#endif
|
||||
|
||||
// Kernel
|
||||
constexpr int NUM_WARPS = 8;
|
||||
constexpr int WARPS_ROW = 2;
|
||||
constexpr int WARPS_COL = 4;
|
||||
constexpr int BLOCK_ROW = 256;
|
||||
constexpr int BLOCK_COL = 256;
|
||||
constexpr int BLOCK_K = 128;
|
||||
constexpr int HALF_ROW = BLOCK_ROW / 2;
|
||||
constexpr int HALF_COL = BLOCK_COL / 2;
|
||||
constexpr int REG_M = BLOCK_ROW / WARPS_ROW / 2;
|
||||
constexpr int REG_N = BLOCK_COL / WARPS_COL / 2;
|
||||
|
||||
using G = kittens::group<NUM_WARPS>;
|
||||
|
||||
__global__ __launch_bounds__(512, 2) void grouped_mxfp8_gemm_kernel(bf16 *C_ptr, fp8e4m3 *A_ptr, fp8e4m3 *B_ptr, fp8e8m0 *scale_A_ptr, fp8e8m0 *scale_B_ptr,
|
||||
const uint8_t *__restrict__ a_e8_unused,
|
||||
const uint8_t *__restrict__ b_e8_unused,
|
||||
const int *__restrict__ expert_off) {
|
||||
constexpr int M = GEMM_M, N = GEMM_N, K = GEMM_K, E = GEMM_E;
|
||||
|
||||
kittens::gl<fp8e4m3, 1, 1, M, K> A{A_ptr, nullptr, nullptr, nullptr, nullptr};
|
||||
kittens::gl<fp8e4m3, 1, 1, E * N, K> B{B_ptr, nullptr, nullptr, nullptr, nullptr}; // all experts stacked on rows
|
||||
kittens::gl<bf16, 1, 1, M, N> C{C_ptr, nullptr, nullptr, nullptr, nullptr};
|
||||
|
||||
constexpr int k_iters = K / BLOCK_K;
|
||||
constexpr int NUM_THREADS = NUM_WARPS * WARP_THREADS;
|
||||
|
||||
kittens::gl<fp8e8m0, k_iters * (M / BLOCK_ROW), 1, 16, 64> scale_A_gl{scale_A_ptr, nullptr, nullptr, nullptr, nullptr};
|
||||
kittens::gl<fp8e8m0, E * k_iters * (N / BLOCK_COL), 1, 16, 64> scale_B_gl{scale_B_ptr, nullptr, nullptr, nullptr, nullptr};
|
||||
|
||||
using ST_A = st_fp8e4m3<HALF_ROW, BLOCK_K, st_16x128_s>;
|
||||
using ST_B = st_fp8e4m3<HALF_COL, BLOCK_K, st_16x128_s>;
|
||||
using ST_Scale = st<fp8e8m0, 16, 64, st_16x64_s>;
|
||||
using RT_A = rt_fp8e4m3<REG_M, BLOCK_K>;
|
||||
using RT_B = rt_fp8e4m3<REG_N, BLOCK_K>;
|
||||
using RT_C = rt_fl<REG_M, REG_N, col_l, rt_16x16_s>;
|
||||
|
||||
__shared__ ST_A As[2][2];
|
||||
__shared__ ST_B Bs[2][2];
|
||||
__shared__ ST_Scale scale_A_smem[2], scale_B_smem[2];
|
||||
|
||||
RT_A a;
|
||||
RT_B b0, b1;
|
||||
RT_C cA, cB, cC, cD;
|
||||
zero(cA); zero(cB); zero(cC); zero(cD);
|
||||
|
||||
constexpr int tiles_M = M / BLOCK_ROW;
|
||||
constexpr int tiles_N = N / BLOCK_COL;
|
||||
const int NUM_XCDS = 8;
|
||||
const int WGM = 8;
|
||||
int wgid = chiplet_transform_chunked(blockIdx.x, gridDim.x, NUM_XCDS, WGM * WGM);
|
||||
int num_wgid_in_group = WGM * tiles_N;
|
||||
int group_id = wgid / num_wgid_in_group;
|
||||
int first_pid_m = group_id * WGM;
|
||||
int group_size_m = min(tiles_M - first_pid_m, WGM);
|
||||
int block_row = first_pid_m + ((wgid % num_wgid_in_group) % group_size_m);
|
||||
int block_col = (wgid % num_wgid_in_group) / group_size_m;
|
||||
int block_m = block_row * BLOCK_ROW;
|
||||
int block_n = block_col * BLOCK_COL;
|
||||
|
||||
int e = 0;
|
||||
#pragma unroll
|
||||
for (int i = 1; i < E; i++) e += (expert_off[i] <= block_row * BLOCK_ROW);
|
||||
e = __builtin_amdgcn_readfirstlane(e);
|
||||
const int bcol_base = e * (N / HALF_COL); // expert base in B row-tile (128-row) units
|
||||
const int sb_base = e * (k_iters * tiles_N); // expert base into scale_B batches
|
||||
|
||||
int warp_m = warpid() / WARPS_COL;
|
||||
int warp_n = warpid() % WARPS_COL;
|
||||
|
||||
using T = fp8e4m3;
|
||||
constexpr int bpt = ST_A::underlying_subtile_bytes_per_thread;
|
||||
constexpr int bpm = bpt * NUM_THREADS;
|
||||
constexpr int copies_A = HALF_ROW * BLOCK_K * sizeof(T) / bpm;
|
||||
constexpr int copies_B = HALF_COL * BLOCK_K * sizeof(T) / bpm;
|
||||
uint32_t sw_A[copies_A], sw_B[copies_B];
|
||||
G::prefill_swizzled_offsets(As[0][0], A, sw_A);
|
||||
G::prefill_swizzled_offsets(Bs[0][0], B, sw_B);
|
||||
|
||||
const T *a_base = (const T *)&A[{0, 0, 0, 0}];
|
||||
const T *b_base = (const T *)&B[{0, 0, 0, 0}];
|
||||
const int a_row_stride = A.template stride<2>() * sizeof(T);
|
||||
const int b_row_stride = B.template stride<2>() * sizeof(T);
|
||||
i32x4 a_srd = make_srsrc(a_base, (uint32_t)((uint64_t)M * a_row_stride), a_row_stride);
|
||||
i32x4 b_srd = make_srsrc(b_base, (uint32_t)((uint64_t)E * N * b_row_stride), b_row_stride);
|
||||
|
||||
const int wid = warpid() % NUM_WARPS;
|
||||
constexpr int elem_per_warp = (16 / sizeof(T)) * kittens::WARP_THREADS;
|
||||
uint32_t a_lds_00 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&As[0][0].data[0]) + wid * elem_per_warp * sizeof(T)));
|
||||
uint32_t a_lds_01 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&As[0][1].data[0]) + wid * elem_per_warp * sizeof(T)));
|
||||
uint32_t a_lds_10 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&As[1][0].data[0]) + wid * elem_per_warp * sizeof(T)));
|
||||
uint32_t a_lds_11 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&As[1][1].data[0]) + wid * elem_per_warp * sizeof(T)));
|
||||
uint32_t b_lds_00 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&Bs[0][0].data[0]) + wid * elem_per_warp * sizeof(T)));
|
||||
uint32_t b_lds_01 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&Bs[0][1].data[0]) + wid * elem_per_warp * sizeof(T)));
|
||||
uint32_t b_lds_10 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&Bs[1][0].data[0]) + wid * elem_per_warp * sizeof(T)));
|
||||
uint32_t b_lds_11 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&Bs[1][1].data[0]) + wid * elem_per_warp * sizeof(T)));
|
||||
|
||||
int a_row_h0 = warp_m * REG_M;
|
||||
int a_row_h1 = HALF_ROW + warp_m * REG_M;
|
||||
int b_row_h0 = warp_n * REG_N;
|
||||
int b_row_h1 = HALF_COL + warp_n * REG_N;
|
||||
|
||||
|
||||
uint32_t a_lds[2][2] = {{a_lds_00, a_lds_01}, {a_lds_10, a_lds_11}};
|
||||
uint32_t b_lds[2][2] = {{b_lds_00, b_lds_01}, {b_lds_10, b_lds_11}};
|
||||
|
||||
#pragma unroll 1
|
||||
for (int kk = 0; kk < k_iters; kk++) {
|
||||
G::load(As[0][0], A, {0, 0, block_row * 2, kk}, sw_A, a_srd, a_base, __builtin_amdgcn_readfirstlane(a_lds[0][0]));
|
||||
G::load(As[0][1], A, {0, 0, block_row * 2 + 1, kk}, sw_A, a_srd, a_base, __builtin_amdgcn_readfirstlane(a_lds[0][1]));
|
||||
G::load(Bs[0][0], B, {0, 0, bcol_base + block_col * 2, kk}, sw_B, b_srd, b_base, __builtin_amdgcn_readfirstlane(b_lds[0][0]));
|
||||
G::load(Bs[0][1], B, {0, 0, bcol_base + block_col * 2 + 1, kk}, sw_B, b_srd, b_base, __builtin_amdgcn_readfirstlane(b_lds[0][1]));
|
||||
G::load(scale_A_smem[0], scale_A_gl, {kk * tiles_M + block_row, 0, 0, 0});
|
||||
G::load(scale_B_smem[0], scale_B_gl, {sb_base + kk * tiles_N + block_col, 0, 0, 0});
|
||||
asm volatile("s_waitcnt vmcnt(0)");
|
||||
asm volatile("s_waitcnt lgkmcnt(0)");
|
||||
__builtin_amdgcn_s_barrier();
|
||||
|
||||
fp8e8m0_4 sa_h0 = pack_scales(scale_A_smem[0].data, a_row_h0);
|
||||
fp8e8m0_4 sa_h1 = pack_scales(scale_A_smem[0].data, a_row_h1);
|
||||
fp8e8m0_4 sb_h0 = pack_scales(scale_B_smem[0].data, b_row_h0);
|
||||
fp8e8m0_4 sb_h1 = pack_scales(scale_B_smem[0].data, b_row_h1);
|
||||
|
||||
auto bs0 = subtile_inplace<REG_N, BLOCK_K>(Bs[0][0], {warp_n, 0}); load(b0, bs0);
|
||||
auto bs1 = subtile_inplace<REG_N, BLOCK_K>(Bs[0][1], {warp_n, 0}); load(b1, bs1);
|
||||
auto as0 = subtile_inplace<REG_M, BLOCK_K>(As[0][0], {warp_m, 0}); load(a, as0);
|
||||
asm volatile("s_waitcnt lgkmcnt(0)");
|
||||
mma_ABt_scaled(cA, a, b0, cA, &sa_h0, &sb_h0);
|
||||
mma_ABt_scaled(cB, a, b1, cB, &sa_h0, &sb_h1);
|
||||
auto as1 = subtile_inplace<REG_M, BLOCK_K>(As[0][1], {warp_m, 0}); load(a, as1);
|
||||
asm volatile("s_waitcnt lgkmcnt(0)");
|
||||
mma_ABt_scaled(cC, a, b0, cC, &sa_h1, &sb_h0);
|
||||
mma_ABt_scaled(cD, a, b1, cD, &sa_h1, &sb_h1);
|
||||
__builtin_amdgcn_s_barrier();
|
||||
}
|
||||
|
||||
store(C, cA, {0, 0, block_row * WARPS_ROW * 2 + warp_m, block_col * WARPS_COL * 2 + warp_n});
|
||||
store(C, cB, {0, 0, block_row * WARPS_ROW * 2 + warp_m, block_col * WARPS_COL * 2 + WARPS_COL + warp_n});
|
||||
store(C, cC, {0, 0, block_row * WARPS_ROW * 2 + WARPS_ROW + warp_m, block_col * WARPS_COL * 2 + warp_n});
|
||||
store(C, cD, {0, 0, block_row * WARPS_ROW * 2 + WARPS_ROW + warp_m, block_col * WARPS_COL * 2 + WARPS_COL + warp_n});
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
#include "kittens.cuh"
|
||||
|
||||
using namespace kittens;
|
||||
|
||||
#ifndef WGRAD_M
|
||||
constexpr int WGRAD_M = 8192;
|
||||
#endif
|
||||
#ifndef WGRAD_N
|
||||
constexpr int WGRAD_N = 8192;
|
||||
#endif
|
||||
#ifndef WGRAD_K
|
||||
constexpr int WGRAD_K = 8192;
|
||||
#endif
|
||||
#ifndef WGRAD_E
|
||||
constexpr int WGRAD_E = 8;
|
||||
#endif
|
||||
|
||||
constexpr int NUM_WARPS = 8;
|
||||
constexpr int WARPS_ROW = 2;
|
||||
constexpr int WARPS_COL = 4;
|
||||
constexpr int BLOCK_ROW = 256;
|
||||
constexpr int BLOCK_COL = 256;
|
||||
constexpr int BLOCK_K = 128;
|
||||
constexpr int HALF_ROW = BLOCK_ROW / 2;
|
||||
constexpr int HALF_COL = BLOCK_COL / 2;
|
||||
constexpr int REG_M = BLOCK_ROW / WARPS_ROW / 2;
|
||||
constexpr int REG_N = BLOCK_COL / WARPS_COL / 2;
|
||||
|
||||
using G = kittens::group<NUM_WARPS>;
|
||||
|
||||
__global__ __launch_bounds__(512, 2) void grouped_mxfp8_wgrad_kernel(bf16 *C_ptr, fp8e4m3 *A_ptr, fp8e4m3 *B_ptr,
|
||||
fp8e8m0 *scale_A_ptr, fp8e8m0 *scale_B_ptr,
|
||||
const int *__restrict__ expert_off) {
|
||||
constexpr int M = WGRAD_M, N = WGRAD_N, K = WGRAD_K, E = WGRAD_E;
|
||||
|
||||
kittens::gl<fp8e4m3, 1, 1, N, M> A{A_ptr, nullptr, nullptr, nullptr, nullptr}; // g^T
|
||||
kittens::gl<fp8e4m3, 1, 1, K, M> B{B_ptr, nullptr, nullptr, nullptr, nullptr}; // x^T
|
||||
kittens::gl<bf16, 1, 1, E * N, K> C{C_ptr, nullptr, nullptr, nullptr, nullptr}; // grad_w, experts stacked
|
||||
|
||||
constexpr int m_blocks = M / BLOCK_K; // 128-wide blocks along the contraction (token) axis
|
||||
constexpr int tiles_N = N / BLOCK_ROW;
|
||||
constexpr int tiles_K = K / BLOCK_COL;
|
||||
constexpr int NUM_THREADS = NUM_WARPS * WARP_THREADS;
|
||||
|
||||
kittens::gl<fp8e8m0, m_blocks * tiles_N, 1, 16, 64> scale_A_gl{scale_A_ptr, nullptr, nullptr, nullptr, nullptr};
|
||||
kittens::gl<fp8e8m0, m_blocks * tiles_K, 1, 16, 64> scale_B_gl{scale_B_ptr, nullptr, nullptr, nullptr, nullptr};
|
||||
|
||||
using ST_A = st_fp8e4m3<HALF_ROW, BLOCK_K, st_16x128_s>;
|
||||
using ST_B = st_fp8e4m3<HALF_COL, BLOCK_K, st_16x128_s>;
|
||||
using ST_Scale = st<fp8e8m0, 16, 64, st_16x64_s>;
|
||||
using RT_A = rt_fp8e4m3<REG_M, BLOCK_K>;
|
||||
using RT_B = rt_fp8e4m3<REG_N, BLOCK_K>;
|
||||
using RT_C = rt_fl<REG_M, REG_N, col_l, rt_16x16_s>;
|
||||
|
||||
__shared__ ST_A As[2];
|
||||
__shared__ ST_B Bs[2];
|
||||
__shared__ ST_Scale scale_A_smem, scale_B_smem;
|
||||
|
||||
RT_A a;
|
||||
RT_B b0, b1;
|
||||
RT_C cA, cB, cC, cD;
|
||||
zero(cA); zero(cB); zero(cC); zero(cD);
|
||||
|
||||
const int wg = blockIdx.x;
|
||||
const int e = wg / (tiles_N * tiles_K);
|
||||
const int rem = wg % (tiles_N * tiles_K);
|
||||
const int block_row = rem / tiles_K; // over grad_w rows (N)
|
||||
const int block_col = rem % tiles_K; // over grad_w cols (K)
|
||||
|
||||
const int o0 = __builtin_amdgcn_readfirstlane(expert_off[e]);
|
||||
const int kk0 = o0 / BLOCK_K;
|
||||
const int nk = (__builtin_amdgcn_readfirstlane(expert_off[e + 1]) - o0) / BLOCK_K;
|
||||
|
||||
const int warp_m = warpid() / WARPS_COL;
|
||||
const int warp_n = warpid() % WARPS_COL;
|
||||
|
||||
using T = fp8e4m3;
|
||||
constexpr int bpt = ST_A::underlying_subtile_bytes_per_thread;
|
||||
constexpr int bpm = bpt * NUM_THREADS;
|
||||
constexpr int copies_A = HALF_ROW * BLOCK_K * sizeof(T) / bpm;
|
||||
constexpr int copies_B = HALF_COL * BLOCK_K * sizeof(T) / bpm;
|
||||
uint32_t sw_A[copies_A], sw_B[copies_B];
|
||||
G::prefill_swizzled_offsets(As[0], A, sw_A);
|
||||
G::prefill_swizzled_offsets(Bs[0], B, sw_B);
|
||||
|
||||
const T *a_base = (const T *)&A[{0, 0, 0, 0}];
|
||||
const T *b_base = (const T *)&B[{0, 0, 0, 0}];
|
||||
const int a_row_stride = A.template stride<2>() * sizeof(T);
|
||||
const int b_row_stride = B.template stride<2>() * sizeof(T);
|
||||
i32x4 a_srd = make_srsrc(a_base, (uint32_t)((uint64_t)N * a_row_stride), a_row_stride);
|
||||
i32x4 b_srd = make_srsrc(b_base, (uint32_t)((uint64_t)K * b_row_stride), b_row_stride);
|
||||
|
||||
const int wid = warpid() % NUM_WARPS;
|
||||
constexpr int elem_per_warp = (16 / sizeof(T)) * kittens::WARP_THREADS;
|
||||
uint32_t a_lds_0 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&As[0].data[0]) + wid * elem_per_warp * sizeof(T)));
|
||||
uint32_t a_lds_1 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&As[1].data[0]) + wid * elem_per_warp * sizeof(T)));
|
||||
uint32_t b_lds_0 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&Bs[0].data[0]) + wid * elem_per_warp * sizeof(T)));
|
||||
uint32_t b_lds_1 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&Bs[1].data[0]) + wid * elem_per_warp * sizeof(T)));
|
||||
|
||||
const int a_row_h0 = warp_m * REG_M;
|
||||
const int a_row_h1 = HALF_ROW + warp_m * REG_M;
|
||||
const int b_row_h0 = warp_n * REG_N;
|
||||
const int b_row_h1 = HALF_COL + warp_n * REG_N;
|
||||
|
||||
#pragma unroll 1
|
||||
for (int t = 0; t < nk; t++) {
|
||||
const int kk = kk0 + t;
|
||||
G::load(As[0], A, {0, 0, block_row * 2, kk}, sw_A, a_srd, a_base, __builtin_amdgcn_readfirstlane(a_lds_0));
|
||||
G::load(As[1], A, {0, 0, block_row * 2 + 1, kk}, sw_A, a_srd, a_base, __builtin_amdgcn_readfirstlane(a_lds_1));
|
||||
G::load(Bs[0], B, {0, 0, block_col * 2, kk}, sw_B, b_srd, b_base, __builtin_amdgcn_readfirstlane(b_lds_0));
|
||||
G::load(Bs[1], B, {0, 0, block_col * 2 + 1, kk}, sw_B, b_srd, b_base, __builtin_amdgcn_readfirstlane(b_lds_1));
|
||||
G::load(scale_A_smem, scale_A_gl, {kk * tiles_N + block_row, 0, 0, 0});
|
||||
G::load(scale_B_smem, scale_B_gl, {kk * tiles_K + block_col, 0, 0, 0});
|
||||
asm volatile("s_waitcnt vmcnt(0)");
|
||||
asm volatile("s_waitcnt lgkmcnt(0)");
|
||||
__builtin_amdgcn_s_barrier();
|
||||
|
||||
fp8e8m0_4 sa_h0 = pack_scales(scale_A_smem.data, a_row_h0);
|
||||
fp8e8m0_4 sa_h1 = pack_scales(scale_A_smem.data, a_row_h1);
|
||||
fp8e8m0_4 sb_h0 = pack_scales(scale_B_smem.data, b_row_h0);
|
||||
fp8e8m0_4 sb_h1 = pack_scales(scale_B_smem.data, b_row_h1);
|
||||
|
||||
auto bs0 = subtile_inplace<REG_N, BLOCK_K>(Bs[0], {warp_n, 0}); load(b0, bs0);
|
||||
auto bs1 = subtile_inplace<REG_N, BLOCK_K>(Bs[1], {warp_n, 0}); load(b1, bs1);
|
||||
auto as0 = subtile_inplace<REG_M, BLOCK_K>(As[0], {warp_m, 0}); load(a, as0);
|
||||
asm volatile("s_waitcnt lgkmcnt(0)");
|
||||
mma_ABt_scaled(cA, a, b0, cA, &sa_h0, &sb_h0);
|
||||
mma_ABt_scaled(cB, a, b1, cB, &sa_h0, &sb_h1);
|
||||
auto as1 = subtile_inplace<REG_M, BLOCK_K>(As[1], {warp_m, 0}); load(a, as1);
|
||||
asm volatile("s_waitcnt lgkmcnt(0)");
|
||||
mma_ABt_scaled(cC, a, b0, cC, &sa_h1, &sb_h0);
|
||||
mma_ABt_scaled(cD, a, b1, cD, &sa_h1, &sb_h1);
|
||||
__builtin_amdgcn_s_barrier();
|
||||
}
|
||||
|
||||
const int crow_base = e * (N / REG_M); // grad_w rows are experts stacked; store coord is in REG_M units
|
||||
store(C, cA, {0, 0, crow_base + block_row * WARPS_ROW * 2 + warp_m, block_col * WARPS_COL * 2 + warp_n});
|
||||
store(C, cB, {0, 0, crow_base + block_row * WARPS_ROW * 2 + warp_m, block_col * WARPS_COL * 2 + WARPS_COL + warp_n});
|
||||
store(C, cC, {0, 0, crow_base + block_row * WARPS_ROW * 2 + WARPS_ROW + warp_m, block_col * WARPS_COL * 2 + warp_n});
|
||||
store(C, cD, {0, 0, crow_base + block_row * WARPS_ROW * 2 + WARPS_ROW + warp_m, block_col * WARPS_COL * 2 + WARPS_COL + warp_n});
|
||||
}
|
||||
@@ -16,7 +16,7 @@ def _sharded_empty(shape:Tensor, ref:Tensor, axis:int|None) -> Tensor:
|
||||
if not isinstance(ref.device, tuple): return Tensor.empty(*shape, dtype=ref.dtype, device=ref.device)
|
||||
shape = tuple(s // len(ref.device) if i == ref.uop.axis else s for i, s in enumerate(shape))
|
||||
axis = ref.uop.axis if axis is None else axis
|
||||
return Tensor(Tensor.empty(*shape, dtype=ref.dtype, device=ref.device).uop.unshard(axis), dtype=ref.dtype, device=ref.device)
|
||||
return Tensor(Tensor.empty(*shape, dtype=ref.dtype, device=ref.device).uop.multi(axis), dtype=ref.dtype, device=ref.device)
|
||||
|
||||
def _sharded_empty_like(ref:Tensor, axis:int|None=None) -> Tensor:
|
||||
return _sharded_empty(ref.shape, ref, axis)
|
||||
|
||||
@@ -224,7 +224,7 @@ class Group:
|
||||
|
||||
# store to shared memory
|
||||
red_local_store = red_local[self.laneid].store(red_reg[0])
|
||||
red_local = red_local.after(red_local_store).reshape(red_local.shape)
|
||||
red_local = red_local.after(red_local_store.barrier()).reshape(red_local.shape)
|
||||
|
||||
# reduce from shared memory
|
||||
for inner in self.ker.range(3, axis_type=AxisType.REDUCE, track=False):
|
||||
@@ -258,7 +258,7 @@ class Group:
|
||||
|
||||
# store to shared memory
|
||||
red_local_store = red_local[self.laneid].store(red_reg[0])
|
||||
red_local = red_local.after(red_local_store).reshape(red_local.shape)
|
||||
red_local = red_local.after(red_local_store.barrier()).reshape(red_local.shape)
|
||||
|
||||
# reduce from shared memory
|
||||
for inner in self.ker.range(3, axis_type=AxisType.REDUCE, track=False):
|
||||
@@ -342,7 +342,7 @@ class Group:
|
||||
if src.dtype != dst.dtype:
|
||||
src_load = src_load.cast(dst.dtype)
|
||||
dst_store = dst[*dst_idxs, height, width, srow, scol].store(src_load)
|
||||
dst_store = dst_store.end(height, width, outer, inner)
|
||||
dst_store = dst_store.end(height, width, outer, inner).barrier()
|
||||
elif dst.addrspace == AddrSpace.REG and src.addrspace == AddrSpace.GLOBAL and isinstance(dst, RT):
|
||||
srcf = src.flatten()
|
||||
row_stride = prod(src.shape[axis+1:])
|
||||
|
||||
@@ -48,14 +48,14 @@ class Kernel(AbstractContextManager):
|
||||
@property
|
||||
def warpgroup(self): return self.group(4)
|
||||
|
||||
def range(self, start:int, end:int=0, step:int=1, axis_type:AxisType=AxisType.WEAK, track:bool=True):
|
||||
def range(self, start:int, end:int=0, step:int=1, axis_type:AxisType=AxisType.LOOP, track:bool=True):
|
||||
if end == 0: start, end = 0, start
|
||||
rng = _tk_range(start, end, step, axis_type, self.range_id)
|
||||
self.range_id += 1
|
||||
if track: self.range_stack.append(rng)
|
||||
return rng
|
||||
|
||||
def raw_range(self, end:int=0, axis_type:AxisType=AxisType.WEAK):
|
||||
def raw_range(self, end:int=0, axis_type:AxisType=AxisType.LOOP):
|
||||
rng = UOp.range(end, self.range_id, axis_type=axis_type)
|
||||
self.range_id += 1
|
||||
return rng
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# A002 Function argument `input` is shadowing a Python builtin
|
||||
# A006 Lambda argument `input` is shadowing a Python builtin
|
||||
from tinygrad import Tensor, dtypes, Device
|
||||
from tinygrad.uop.ops import Ops, GroupOp
|
||||
from tinygrad.uop.ops import Ops
|
||||
from tinygrad.helpers import getenv, prod, strides_for_shape, argfix
|
||||
import torch.lib
|
||||
TORCH_DEBUG = getenv("TORCH_DEBUG")
|
||||
@@ -18,16 +18,12 @@ def _to_torch_device(device: str): return torch.device("tiny", int(device.partit
|
||||
|
||||
import torch.utils.cpp_extension
|
||||
mod = torch.utils.cpp_extension.load(name="custom_device_extension", sources=[str(pathlib.Path(__file__).parent / "wrapped_tensor.cpp")])
|
||||
# TODO: this assumes a contiguous source, so PERMUTE/EXPAND/PAD/FLIP are wrong. UOp.contiguous_view_offset does it
|
||||
# properly, but it needs a device (these are deviceless)
|
||||
alias_ops = GroupOp.Movement | {Ops.BITCAST, Ops.DETACH, Ops.AFTER}
|
||||
def calculate_storage_offset(x: Tensor) -> int:
|
||||
offset, u = 0, x.uop
|
||||
while u.op in alias_ops:
|
||||
if u.op is Ops.SHRINK:
|
||||
offset = 0
|
||||
for u in x.uop.toposort():
|
||||
if u.op == Ops.SHRINK:
|
||||
u_strides = strides_for_shape(u.src[0].shape)
|
||||
for i, (start, _) in enumerate(u.marg): offset += start * u_strides[i]
|
||||
u = u.src[0]
|
||||
return offset
|
||||
def wrap(x: Tensor, dev: torch.device|None=None) -> torch.Tensor:
|
||||
x._strides = strides_for_shape(x.shape) # always recalculate
|
||||
@@ -224,7 +220,7 @@ def _as_strided(tensor:Tensor, size, stride, storage_offset=0):
|
||||
|
||||
@torch.library.impl("aten::as_strided", "privateuseone")
|
||||
def as_strided(tensor:torch.Tensor, size, stride, storage_offset=None):
|
||||
if storage_offset is None: storage_offset = tensor.storage_offset()
|
||||
storage_offset = storage_offset or tensor.storage_offset()
|
||||
return _as_strided(tensor, size, stride, storage_offset)
|
||||
|
||||
@torch.library.impl("aten::_reshape_alias", "privateuseone")
|
||||
@@ -232,16 +228,16 @@ def _reshape_alias(tensor:torch.Tensor, size, stride):
|
||||
return _as_strided(tensor, size, stride)
|
||||
|
||||
@torch.library.impl("aten::empty_strided", "privateuseone")
|
||||
def empty_strided(size, stride, dtype=None, layout=None, device=None, pin_memory=False):
|
||||
def empty_strided(size, stride, dtype, layout=None, device=None, pin_memory=False):
|
||||
if TORCH_DEBUG: print(f"empty_strided {size=} {stride=} {dtype=} {layout=} {device=} {pin_memory=}")
|
||||
ret = Tensor.empty(*size, dtype=_from_torch_dtype(dtype or torch.get_default_dtype()), device=_from_torch_device(device))
|
||||
ret = Tensor.empty(*size, dtype=_from_torch_dtype(dtype), device=_from_torch_device(device)).contiguous()
|
||||
# TODO: should return with requested strides
|
||||
return wrap(ret)
|
||||
|
||||
@torch.library.impl("aten::empty.memory_format", "privateuseone")
|
||||
def empty_memory_format(size, dtype=None, layout=None, device=None, pin_memory=False, memory_format=None):
|
||||
if TORCH_DEBUG: print(f"empty.memory_format {size=} {dtype=} {layout=} {device=} {pin_memory=} {memory_format=}")
|
||||
ret = Tensor.empty(*size, dtype=_from_torch_dtype(dtype or torch.get_default_dtype()), device=_from_torch_device(device))
|
||||
ret = Tensor.empty(*size, dtype=_from_torch_dtype(dtype or torch.get_default_dtype()), device=_from_torch_device(device)).contiguous()
|
||||
return wrap(ret)
|
||||
|
||||
@torch.library.impl("aten::max_pool2d_with_indices", "privateuseone")
|
||||
@@ -555,8 +551,6 @@ def wrap_out(f):
|
||||
assert out.shape == assigned.shape, f"shape mismatch: {assigned.shape} -> {out.shape}"
|
||||
assert out.device == assigned.device or out.device is None or assigned.device is None, f"device mismatch: {assigned.device} -> {out.device}"
|
||||
assert out.dtype == assigned.dtype, f"dtype mismatch: {assigned.dtype} -> {out.dtype}"
|
||||
# an out= that is a view has to be written through its base, and _apply_inplace gives a deviceless base its buffer first
|
||||
if canonical_base(out) is not out: return _apply_inplace(out, assigned) or out
|
||||
if out.device is None and assigned.device is not None: out.replace(out.empty_like(device=assigned.device))
|
||||
return out.assign(assigned)
|
||||
return _wrap_out
|
||||
@@ -658,6 +652,35 @@ tiny_backend = {**{k:wrap_out(v) for k,v in tiny_backend_out.items()}, **{
|
||||
"aten.unfold": Tensor.unfold,
|
||||
}}
|
||||
|
||||
# operations that need inplace treatment (use _inplace_op instead of wrap_fxn) AKA return original tensor
|
||||
inplace_ops = {
|
||||
"aten.zero_",
|
||||
"aten.fill_.Scalar",
|
||||
"aten.fill_.Tensor",
|
||||
"aten.add_.Tensor",
|
||||
"aten.add_.Scalar",
|
||||
"aten.mul_.Tensor",
|
||||
"aten.mul_.Scalar",
|
||||
"aten.floor_divide_.Tensor",
|
||||
"aten.__ilshift__.Scalar",
|
||||
"aten.__irshift__.Scalar",
|
||||
"aten.relu_",
|
||||
"aten.random_",
|
||||
"aten.random_.from",
|
||||
"aten.uniform_",
|
||||
"aten.normal_",
|
||||
"aten.logical_or_",
|
||||
"aten.masked_fill_.Scalar",
|
||||
"aten.masked_fill_.Tensor",
|
||||
}
|
||||
|
||||
inplace_view_ops = {
|
||||
"aten.squeeze_.dim",
|
||||
"aten.unsqueeze_",
|
||||
"aten.transpose_",
|
||||
"aten.t_",
|
||||
}
|
||||
|
||||
def wrap_fxn(k,f):
|
||||
def nf(*args, **kwargs):
|
||||
if TORCH_DEBUG:
|
||||
@@ -671,7 +694,7 @@ def wrap_fxn(k,f):
|
||||
else: raise RuntimeError(f"unknown output type {type(out)}")
|
||||
return nf
|
||||
|
||||
def wrap_inplace(f):
|
||||
def wrap_inplace(k,f):
|
||||
def nf(*args, **kwargs):
|
||||
orig = args[0]
|
||||
args, kwargs = unwrap_args(args, kwargs)
|
||||
@@ -679,7 +702,7 @@ def wrap_inplace(f):
|
||||
return orig
|
||||
return nf
|
||||
|
||||
def wrap_inplace_view_op(f):
|
||||
def wrap_inplace_view_op(k,f):
|
||||
def nf(*args, **kwargs):
|
||||
orig = args[0]
|
||||
args, kwargs = unwrap_args(args, kwargs)
|
||||
@@ -712,17 +735,11 @@ def wrap_inplace_view_op(f):
|
||||
return orig
|
||||
return nf
|
||||
|
||||
# the aten schema says how an op is called: an inplace view retargets the view, a writable first arg is inplace,
|
||||
# and a writable out arg must have come from tiny_backend_out so that wrap_out was applied
|
||||
for k,v in tiny_backend.items():
|
||||
name, _, overload = k.removeprefix("aten.").partition(".")
|
||||
op = getattr(getattr(aten, name), overload or "default")
|
||||
writes = [a.name for a in op._schema.arguments if a.alias_info is not None and a.alias_info.is_write]
|
||||
if torch.Tag.inplace_view in op.tags: fxn = wrap_inplace_view_op(v)
|
||||
elif writes == [op._schema.arguments[0].name] and op._schema.returns: fxn = wrap_inplace(v)
|
||||
elif not writes or (writes == ["out"] and k in tiny_backend_out): fxn = wrap_fxn(k, v)
|
||||
else: raise RuntimeError(f"{k} writes {writes}: expected an inplace first arg, or an out arg with {k} in tiny_backend_out")
|
||||
torch.library.impl(k.replace("aten.", "aten::"), "privateuseone")(fxn)
|
||||
if k in inplace_view_ops: wrapper = wrap_inplace_view_op
|
||||
elif k in inplace_ops: wrapper = wrap_inplace
|
||||
else: wrapper = wrap_fxn
|
||||
torch.library.impl(k.replace("aten.", "aten::"), "privateuseone")(wrapper(k,v))
|
||||
|
||||
@torch.library.impl("aten::equal", "privateuseone")
|
||||
def equal(x: torch.Tensor, y: torch.Tensor): return (x==y).all().item()
|
||||
@@ -758,17 +775,21 @@ def native_batch_norm(input, weight, bias, running_mean, running_var, training,
|
||||
@torch.library.impl("aten::native_batch_norm_backward", "privateuseone")
|
||||
def native_batch_norm_backward(grad_out, input, weight, running_mean, running_var, save_mean, save_invstd, train, eps, output_mask):
|
||||
grad_out_t, input_t = unwrap(grad_out), unwrap(input)
|
||||
dims, shape = tuple(x for x in range(input_t.ndim) if x != 1), (1, -1) + (1,)*(input_t.ndim-2)
|
||||
# training differentiates the batch stats it was given, eval treats the running stats as constants
|
||||
if train: mean, invstd = unwrap(save_mean), unwrap(save_invstd)
|
||||
else: mean, invstd = unwrap(running_mean), unwrap(running_var).add(eps).rsqrt()
|
||||
xhat = (input_t - mean.reshape(shape)) * invstd.reshape(shape)
|
||||
grad_bias, grad_weight = grad_out_t.sum(axis=dims), (grad_out_t * xhat).sum(axis=dims)
|
||||
grad_input = grad_out_t if not train else \
|
||||
grad_out_t - (grad_bias.reshape(shape) + xhat * grad_weight.reshape(shape)) / (input_t.numel() // input_t.shape[1])
|
||||
grad_input = grad_input * invstd.reshape(shape) * (unwrap(weight).reshape(shape) if weight is not None else 1)
|
||||
return (wrap(grad_input) if output_mask[0] else None, wrap(grad_weight) if output_mask[1] else None,
|
||||
wrap(grad_bias) if output_mask[2] else None)
|
||||
weight_t = unwrap(weight) if weight is not None else None
|
||||
save_mean_t = unwrap(save_mean)
|
||||
save_invstd_t = unwrap(save_invstd)
|
||||
out = input_t.batchnorm(weight_t, None, save_mean_t, save_invstd_t)
|
||||
targets = [t for t, m in zip([input_t, weight_t], output_mask[:2]) if t is not None and m]
|
||||
if targets:
|
||||
grads = out.gradient(*targets, gradient=grad_out_t)
|
||||
grad_input = grads.pop(0) if output_mask[0] else None
|
||||
grad_weight = grads.pop(0) if output_mask[1] and weight_t is not None else None
|
||||
else:
|
||||
grad_input, grad_weight = None, None
|
||||
grad_bias = grad_out_t.sum(axis=tuple(x for x in range(grad_out_t.ndim) if x != 1)) if output_mask[2] else None
|
||||
return (wrap(grad_input) if grad_input is not None else None,
|
||||
wrap(grad_weight) if grad_weight is not None else None,
|
||||
wrap(grad_bias) if grad_bias is not None else None)
|
||||
|
||||
# _pad_circular is not CompositeImplicitAutograd (unlike reflect/replicate pad)
|
||||
# we need torch.autograd.Function with explicit AutogradPrivateUse1 registration
|
||||
|
||||
@@ -71,48 +71,6 @@ class TestTorchBackend(unittest.TestCase):
|
||||
a = a.as_strided((1,1,5,5), (50,50,7,1), storage_offset=21)
|
||||
np.testing.assert_equal(a.cpu().numpy().sum(-1), [[[115,150,185,220,255]]])
|
||||
|
||||
def test_storage_offset_of_computed_tensor(self):
|
||||
# a computed result owns its storage, so a slice anywhere in its history must not shift the offset
|
||||
a = torch.arange(8., device=device)
|
||||
self.assertEqual((a[3:]+1).storage_offset(), 0)
|
||||
|
||||
def test_storage_offset_through_aliases(self):
|
||||
a = torch.arange(8., device=device)[3:]
|
||||
self.assertEqual(a.detach().storage_offset(), 3)
|
||||
self.assertEqual(a.view(torch.int32).storage_offset(), 3)
|
||||
torch.add(torch.ones(5, device=device), torch.ones(5, device=device), out=a)
|
||||
self.assertEqual(a.detach().storage_offset(), 3)
|
||||
|
||||
@unittest.expectedFailure # TODO: storage offset assumes a contiguous source, use UOp.contiguous_view_offset
|
||||
def test_storage_offset_non_contiguous_source(self):
|
||||
a = torch.arange(12., device=device).reshape(3,4)
|
||||
self.assertEqual(a.permute(1,0)[1:].storage_offset(), 1)
|
||||
self.assertEqual(a.flatten()[3:].flip(0).storage_offset(), 0)
|
||||
|
||||
def test_as_strided_explicit_zero_offset(self):
|
||||
# storage_offset=0 is a real offset, not "unspecified": it must not fall back to the input's own offset
|
||||
a = torch.arange(6., device=device)
|
||||
np.testing.assert_equal(a[3:].as_strided((2,), (1,), 0).cpu().numpy(), [0,1])
|
||||
np.testing.assert_equal(a[3:].as_strided((2,), (1,)).cpu().numpy(), [3,4])
|
||||
|
||||
def test_empty_strided_default_dtype(self):
|
||||
self.assertEqual(torch.empty_strided((2,3), (1,2), device=device).dtype, torch.get_default_dtype())
|
||||
|
||||
@unittest.expectedFailure # TODO: empty_strided ignores the requested strides, the backend treats everything as contiguous
|
||||
def test_empty_strided_honors_strides(self):
|
||||
self.assertEqual(tuple(torch.empty_strided((2,3), (1,2), device=device).stride()), (1,2))
|
||||
|
||||
@unittest.expectedFailure # TODO: torch refuses an out= that overlaps an input, we compute silently
|
||||
def test_out_overlapping_input_is_rejected(self):
|
||||
x = torch.arange(6., device=device)
|
||||
with self.assertRaises(RuntimeError): torch.add(x[:-1], 10, out=x[1:])
|
||||
|
||||
def test_out_disjoint_input_is_allowed(self):
|
||||
# torch permits an out= that shares a base with an input as long as they do not overlap
|
||||
x, xc = torch.arange(6., device=device), torch.arange(6.)
|
||||
torch.add(x[:3], 10, out=x[3:]); torch.add(xc[:3], 10, out=xc[3:])
|
||||
np.testing.assert_equal(x.cpu().numpy(), xc.numpy())
|
||||
|
||||
def test_plus_inplace(self):
|
||||
a = torch.ones(4, device=device)
|
||||
b = torch.ones(4, device=device)
|
||||
@@ -358,21 +316,6 @@ class TestTorchBackend(unittest.TestCase):
|
||||
assert b.shape == (4, 2, 3)
|
||||
np.testing.assert_equal(b.cpu().numpy(), a.cpu().numpy().transpose(2, 0, 1))
|
||||
|
||||
def test_batchnorm_backward_realized_stats(self):
|
||||
# the saved stats are a function of input in training, so grad_input must flow through them even when handed in realized.
|
||||
# the backward eps is unused in training: torch differentiates the save_invstd it was given
|
||||
x0, g0 = torch.randn(8, 4, 3, 3), torch.randn(8, 4, 3, 3)
|
||||
def run(dev, bwd_eps):
|
||||
x, go = x0.to(dev), g0.to(dev)
|
||||
w, b = torch.linspace(0.5, 2.0, 4).to(dev), torch.zeros(4, device=dev)
|
||||
rm, rv = torch.zeros(4, device=dev), torch.ones(4, device=dev)
|
||||
out, sm, si = torch.ops.aten.native_batch_norm(x, w, b, rm, rv, True, 0.1, 1e-5)
|
||||
grads = torch.ops.aten.native_batch_norm_backward(go, x, w, rm, rv, sm.clone().detach(), si.clone().detach(),
|
||||
True, bwd_eps, [True,True,True])
|
||||
return [t.cpu().numpy() for t in grads]
|
||||
for bwd_eps in [1e-5, 0.3]:
|
||||
for got, want in zip(run(device, bwd_eps), run("cpu", bwd_eps)): np.testing.assert_allclose(got, want, atol=1e-4, rtol=1e-3)
|
||||
|
||||
def test_batchnorm_unsqueeze(self):
|
||||
bn = torch.nn.BatchNorm2d(4).to(device)
|
||||
x = torch.randn(8, 4, 3, 3, device=device)
|
||||
@@ -799,20 +742,6 @@ class TestTorchBackend(unittest.TestCase):
|
||||
|
||||
from tinygrad import Tensor
|
||||
class TestBackendHelpers(unittest.TestCase):
|
||||
def test_unwrap_rejects_foreign_tensor(self):
|
||||
# unwrap casts to the tiny impl, so a tensor from another backend must be refused rather than reinterpreted
|
||||
with self.assertRaises(RuntimeError): extra.torch_backend.backend.unwrap(torch.ones(4))
|
||||
|
||||
def test_update_metadata_rejects_foreign_tensor(self):
|
||||
# resizing a tensor we don't own would expose memory past its allocation
|
||||
t = torch.ones(4)
|
||||
with self.assertRaises(RuntimeError): extra.torch_backend.backend.mod.update_metadata(t, [8], [1], 0)
|
||||
self.assertEqual(t.shape, (4,))
|
||||
|
||||
def test_unwrap_parameter_and_detached(self):
|
||||
# nn.Parameter and detach rebuild the base OpaqueTensorImpl, which unwrap still has to accept
|
||||
extra.torch_backend.backend.unwrap(torch.nn.Parameter(torch.ones(4, device="tiny")))
|
||||
extra.torch_backend.backend.unwrap(torch.ones(4, device="tiny").detach())
|
||||
|
||||
def test_calculate_storage_offset_no_shrink(self):
|
||||
t = Tensor.ones(3, 4)
|
||||
|
||||
@@ -124,21 +124,16 @@ at::Tensor wrap_tensor(py::object &py_obj, c10::ScalarType dtype, c10::DeviceInd
|
||||
sizes, strides, storage_offset);
|
||||
}
|
||||
|
||||
// shallow_copy_and_detach (nn.Parameter, aten.detach) rebuilds the base OpaqueTensorImpl, so that is the type every tiny tensor has
|
||||
at::OpaqueTensorImpl<std::shared_ptr<c10::SafePyObject>> *tiny_impl(const at::Tensor &tensor) {
|
||||
auto* impl = dynamic_cast<at::OpaqueTensorImpl<std::shared_ptr<c10::SafePyObject>>*>(tensor.unsafeGetTensorImpl());
|
||||
TORCH_CHECK(impl != nullptr, "expected a tiny tensor, got a ", tensor.device().str(), " one. move it with .to(\"tiny\") first");
|
||||
return impl;
|
||||
}
|
||||
|
||||
py::object unwrap_tensor(const at::Tensor &tensor) {
|
||||
std::shared_ptr<c10::SafePyObject> tiny = tiny_impl(tensor)->opaque_handle();
|
||||
auto* impl = tensor.unsafeGetTensorImpl();
|
||||
auto* opaque_impl = static_cast<at::TinyOpaqueTensorImpl<std::shared_ptr<c10::SafePyObject>>*>(impl);
|
||||
std::shared_ptr<c10::SafePyObject> tiny = opaque_impl->opaque_handle();
|
||||
return py::reinterpret_borrow<py::object>(tiny->ptr(getPyInterpreter()));
|
||||
}
|
||||
|
||||
void update_metadata(const at::Tensor &tensor, const std::vector<int64_t> &sizes,
|
||||
const std::vector<int64_t> &strides, int64_t storage_offset) {
|
||||
auto* impl = tiny_impl(tensor);
|
||||
auto* impl = tensor.unsafeGetTensorImpl();
|
||||
impl->set_allow_tensor_metadata_change(true);
|
||||
impl->set_sizes_and_strides(sizes, strides, storage_offset);
|
||||
}
|
||||
|
||||
Binary file not shown.
+3
-9
@@ -29,7 +29,6 @@
|
||||
\definecolor{axbrred}{HTML}{E53935} % GROUP_REDUCE
|
||||
\definecolor{axyellow}{HTML}{F9A825} % UPCAST
|
||||
\definecolor{axmagenta}{HTML}{7B1FA2} % UNROLL
|
||||
\definecolor{axgreen}{HTML}{2E7D32} % DEVICE
|
||||
|
||||
\title{tinygrad: a single dialect from Tensor programs to Command Buffers}
|
||||
\author{tinygrad, Corp. \\ \texttt{[email protected]}}
|
||||
@@ -80,7 +79,6 @@ All nodes in the tinygrad graph are \textbf{UOps}. A UOp is a tuple $(\mathrm{op
|
||||
\op{Index} & $(T, i_0, i_1, \ldots)$ & --- & Index from left. $()$-shaped $i$ removes dim; $(k,)$-shaped makes it $k$. \\
|
||||
\op{Stack} & $(T_0, T_1, \ldots)$ & --- & Join along a newly created leading axis. All shapes must match. \\
|
||||
\op{Bitcast} & $(T,)$ & dtype & Reinterpret storage as target dtype; preserve total bytes. \\
|
||||
\op{Unshard} & $(T, R_0, R_1, \ldots)$ & axes $(a_0, a_1, \ldots)$ & Concatenate shards of \op{Range} $R_k$ along axis $a_k$; $R_k$ is outer. \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
|
||||
@@ -260,7 +258,6 @@ Every UOp has a \textbf{dtype}, \textbf{shape}, \textbf{device}, \textbf{addrspa
|
||||
\op{Const} & from arg & $()$ & \textsc{null} & $[v, v]$ \\
|
||||
\op{Param} & from arg & from $\mathrm{src}[0]$ & from arg & from src or dtype range \\[3pt]
|
||||
Movement ops & $\mathrm{src}[0].\mathrm{dtype}$ & (see op) & $\mathrm{src}[0].\mathrm{device}$ & $\mathrm{src}[0]$ \\
|
||||
\op{Unshard} & $\mathrm{src}[0].\mathrm{dtype}$ & $\mathrm{src}[0]$, each $a_k \times n_k$ & $\mathrm{src}[0].\mathrm{device}$ & $\mathrm{src}[0]$ \\
|
||||
\op{Reduce} & $\mathrm{src}[0].\mathrm{dtype}$ & remove first $n$ axes & $\mathrm{src}[0].\mathrm{device}$ & dtype range \\[3pt]
|
||||
\op{Cast} & from arg & $\mathrm{src}[0].\mathrm{shape}$ & $\mathrm{src}[0].\mathrm{device}$ & clamped to dtype \\
|
||||
\op{Bitcast} & from arg & $\mathrm{src}[0].\mathrm{shape}$ & $\mathrm{src}[0].\mathrm{device}$ & dtype range \\
|
||||
@@ -286,9 +283,9 @@ $[a,A]$, $[b,B]$, $[c,C]$ denote min\_max of $\mathrm{src}[0]$, $\mathrm{src}[1]
|
||||
Default \emph{dtype range}: $[\mathrm{dtype\_min},\, \mathrm{dtype\_max}]$.
|
||||
|
||||
\medskip
|
||||
\textbf{sharding} tracks multi-device sharding as a set of (axis, \op{Range}) pairs. \op{Unshard} defines it: arg is the tuple of sharded axes, one \op{Range} in src per axis (positional: the $k$-th \op{Range} shards the $k$-th axis). \op{Buffer} with $n$-tuple device: sharded on axis $0$ (device dim). The single-axis convenience \textbf{axis} is \textsc{null} unless exactly one axis is sharded.
|
||||
\op{Reshape} remaps each sharded axis to preserve its shard boundary. \op{Permute} follows the permutation. \op{Expand} shifts all sharded axes right by $|\mathbf{n}|$.
|
||||
\op{Reduce} on a sharded axis drops it. \op{Replicated} on the shard axis $\to$ \textsc{null}. \op{Copy} $\to$ \textsc{null}. ALU ops inherit from sources. Default: \textsc{null}.
|
||||
\textbf{axis} tracks the multi-device sharding dimension. \op{Buffer} with $n$-tuple device: axis $= 0$ (device dim).
|
||||
\op{Reshape} remaps axis to preserve the shard boundary. \op{Permute} follows the permutation. \op{Expand} shifts axis right by $|\mathbf{n}|$.
|
||||
\op{Reduce} on the shard axis $\to$ \textsc{null} (shard axis is among the first $n$ axes). \op{Replicated} on the shard axis $\to$ \textsc{null}. \op{Copy} $\to$ \textsc{null}. ALU ops inherit from sources. Default: \textsc{null}.
|
||||
|
||||
%% ============================================================
|
||||
\subsection*{Kernel Optimizations (OptOps) \normalfont\small--- schedule-level transforms on kernel ranges}
|
||||
@@ -300,7 +297,6 @@ Each kernel's iteration space is a set of \op{Range} axes. Every range has an \t
|
||||
\toprule
|
||||
\textbf{AxisType} & \textbf{Letter} & \textbf{Split from} & \textbf{Direction} & \textbf{Semantics} \\
|
||||
\midrule
|
||||
{\color{axgreen}\texttt{DEVICE}} & \texttt{d} & --- & --- & Multi-device sharding dimension. \\
|
||||
{\color{axblue}\texttt{GLOBAL}} & \texttt{g} & --- & --- & GPU global workgroup dimension. \\
|
||||
{\color{axcyan}\texttt{LOCAL}} & \texttt{l} & g, L & inner & Workgroup local dimension (shared memory). \\
|
||||
{\color{axbrcyan}\texttt{WARP}} & \texttt{w} & \multicolumn{2}{l}{(created by \op{TC})} & Warp-level lanes for tensor cores. \\
|
||||
@@ -382,8 +378,6 @@ def scatter_add(T, idx, val):
|
||||
Let $D = (d_0, \ldots, d_{n-1})$ be an $n$-tuple device.
|
||||
\op{Copy} to an $n$-tuple device reshards with axis $= 0$. \op{Copy} never changes shape.
|
||||
|
||||
\textbf{Sharding} splits a tensor along an axis across $n$ devices. It opens a \op{Range} of type \texttt{DEVICE} (a symbolic per-device index $d$), shrinks each device's view to its piece, then closes the range with \op{Unshard}$(T, R, a)$. The result is a logical tensor whose shape along axis $a$ is the full size; each device holds $1/n$ of it. \op{Unshard} is the inverse of sharding --- it marks the boundary between per-device computation and the logical multi-device tensor. The range need not be \texttt{DEVICE}; e.g.\ a \texttt{WARP} range closes the same way, concatenating per-lane shards along $a$ with the range as the outer factor. A tensor may be sharded along several axes at once: \op{Unshard}$(T, R_0, R_1, \ldots;\; a_0, a_1, \ldots)$ carries one \op{Range} per sharded axis, and every movement op maps each sharded axis independently.
|
||||
|
||||
\begin{lstlisting}
|
||||
# T has shape (s,) on a single device.
|
||||
|
||||
|
||||
@@ -169,10 +169,11 @@ def run_program_emu(instructions: list, n_lanes: int = 1) -> WaveState:
|
||||
return parse_output(bytes(out_buf), n_lanes)
|
||||
|
||||
def run_program_hw(instructions: list, n_lanes: int = 1) -> WaveState:
|
||||
"""Run instructions on real AMD hardware via HIPCompiler and the AMD runtime."""
|
||||
from tinygrad.device import Device, TinyELF
|
||||
"""Run instructions on real AMD hardware via HIPCompiler and AMDProgram."""
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.runtime.ops_amd import AMDProgram
|
||||
from tinygrad.runtime.support.compiler_amd import HIPCompiler
|
||||
from tinygrad.helpers import Target, flat_mv
|
||||
from tinygrad.helpers import flat_mv
|
||||
|
||||
dev = Device["AMD"]
|
||||
compiler = HIPCompiler(dev.arch) # type: ignore[attr-defined]
|
||||
@@ -222,7 +223,7 @@ amdhsa.kernels:
|
||||
"""
|
||||
|
||||
lib = compiler.compile(asm_src)
|
||||
prg = dev.runtime(TinyELF(lib, "test", Target("AMD", arch=dev.arch), ()))
|
||||
prg = AMDProgram(dev, "test", lib) # type: ignore[arg-type]
|
||||
|
||||
buf_sz = _out_bytes(n_lanes)
|
||||
out_gpu = dev.allocator.alloc(buf_sz)
|
||||
|
||||
@@ -5,7 +5,7 @@ gfx950 hardware when USE_HW=1.
|
||||
"""
|
||||
import ctypes, struct, unittest
|
||||
import tinygrad.runtime.autogen.amd.cdna.ins as cdna
|
||||
from tinygrad.helpers import Target, flat_mv
|
||||
from tinygrad.helpers import flat_mv
|
||||
from tinygrad.renderer.amd.dsl import NULL
|
||||
from test.amd.hw.helpers import USE_HW, assemble
|
||||
from test.mockgpu.amd.emu import run_asm
|
||||
@@ -42,7 +42,8 @@ def _run_emu(instructions: list, out_reg: int = 2) -> int:
|
||||
return out_buf[0]
|
||||
|
||||
def _run_hw(instructions: list, out_reg: int = 2) -> int:
|
||||
from tinygrad.device import Device, TinyELF
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.runtime.ops_amd import AMDProgram
|
||||
from tinygrad.runtime.support.compiler_amd import HIPCompiler
|
||||
|
||||
dev = Device["AMD"]
|
||||
@@ -85,7 +86,7 @@ amdhsa.kernels:
|
||||
...
|
||||
.end_amdgpu_metadata
|
||||
"""
|
||||
prg = dev.runtime(TinyELF(HIPCompiler(dev.arch).compile(asm_src), "test", Target("AMD", arch=dev.arch), ()))
|
||||
prg = AMDProgram(dev, "test", HIPCompiler(dev.arch).compile(asm_src))
|
||||
prg(global_size=(1, 1, 1), local_size=(LANES, 1, 1), wait=True)
|
||||
out = bytearray(LANES * 4)
|
||||
dev.allocator._copyout(flat_mv(memoryview(out)), out_gpu)
|
||||
|
||||
@@ -6,7 +6,7 @@ when USE_HW=1.
|
||||
"""
|
||||
import ctypes, unittest
|
||||
from tinygrad.runtime.autogen.amd.rdna3.ins import *
|
||||
from tinygrad.helpers import Target, flat_mv
|
||||
from tinygrad.helpers import flat_mv
|
||||
from test.amd.hw.helpers import USE_HW, assemble
|
||||
from test.mockgpu.amd.emu import run_asm
|
||||
|
||||
@@ -37,7 +37,8 @@ def _run_wave64_emu(instructions: list, out_reg: int = 1) -> list[int]:
|
||||
return list(out_buf)
|
||||
|
||||
def _run_wave64_hw(instructions: list, out_reg: int = 1) -> list[int]:
|
||||
from tinygrad.device import Device, TinyELF
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.runtime.ops_amd import AMDProgram
|
||||
from tinygrad.runtime.support.compiler_amd import HIPCompiler
|
||||
|
||||
dev = Device["AMD"]
|
||||
@@ -83,7 +84,7 @@ amdhsa.kernels:
|
||||
.end_amdgpu_metadata
|
||||
"""
|
||||
lib = compiler.compile(asm_src)
|
||||
prg = dev.runtime(TinyELF(lib, "test", Target("AMD", arch=dev.arch), ()))
|
||||
prg = AMDProgram(dev, "test", lib) # type: ignore[arg-type]
|
||||
out_gpu = dev.allocator.alloc(WAVE64 * 4)
|
||||
prg(out_gpu, global_size=(1, 1, 1), local_size=(WAVE64, 1, 1), wait=True)
|
||||
out = bytearray(WAVE64 * 4)
|
||||
|
||||
@@ -5,7 +5,7 @@ real RDNA4 hardware when USE_HW=1.
|
||||
"""
|
||||
import ctypes, unittest
|
||||
import tinygrad.runtime.autogen.amd.rdna4.ins as r4
|
||||
from tinygrad.helpers import Target, flat_mv
|
||||
from tinygrad.helpers import flat_mv
|
||||
from tinygrad.renderer.amd.dsl import NULL
|
||||
from test.amd.hw.helpers import USE_HW, assemble
|
||||
from test.mockgpu.amd.emu import run_asm
|
||||
@@ -36,7 +36,8 @@ def _run_emu(instructions: list, out_reg: int = 2) -> list[int]:
|
||||
return list(out_buf)
|
||||
|
||||
def _run_hw(instructions: list, out_reg: int = 2) -> list[int]:
|
||||
from tinygrad.device import Device, TinyELF
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.runtime.ops_amd import AMDProgram
|
||||
from tinygrad.runtime.support.compiler_amd import HIPCompiler
|
||||
|
||||
dev = Device['AMD']
|
||||
@@ -84,7 +85,7 @@ amdhsa.kernels:
|
||||
.end_amdgpu_metadata
|
||||
"""
|
||||
lib = compiler.compile(asm_src)
|
||||
prg = dev.runtime(TinyELF(lib, "test", Target("AMD", arch=dev.arch), ()))
|
||||
prg = AMDProgram(dev, 'test', lib)
|
||||
out_gpu = dev.allocator.alloc(LANES * 4)
|
||||
prg(out_gpu, global_size=(1, 1, 1), local_size=(LANES, 1, 1), wait=True)
|
||||
out = bytearray(LANES * 4)
|
||||
|
||||
@@ -36,7 +36,7 @@ def custom_add_var(A:UOp, B:UOp) -> UOp:
|
||||
A,B = A.flatten(), B.flatten()
|
||||
assert A.dtype == dtypes.uint32, f"buffer dtype must be uint32, got {A.dtype}"
|
||||
threads = UOp.special(A.numel(), "lidx0")
|
||||
var = UOp.param(2, dtypes.int, vmin_vmax=(0, 10), name="var", addrspace=AddrSpace.ALU)
|
||||
var = UOp.param(2, dtypes.weakint, vmin_vmax=(0, 10), name="var", addrspace=AddrSpace.ALU)
|
||||
insts = [
|
||||
s_load_b128(s[4:7], s[0:1]),
|
||||
s_load_b32(s[8], s[0:1], offset=0x10), # all threads load the same variable
|
||||
|
||||
@@ -7,16 +7,15 @@ class TestMockGPUInvalidInstruction(unittest.TestCase):
|
||||
"""Test that unsupported instructions raise immediately through the full MOCKGPU stack."""
|
||||
test_code = '''
|
||||
import struct
|
||||
from dataclasses import replace
|
||||
from tinygrad import Device, Tensor
|
||||
from tinygrad.engine.realize import compile_linear
|
||||
from tinygrad.runtime.ops_amd import AMDProgram
|
||||
|
||||
dev = Device["AMD"]
|
||||
a = Tensor([1.0]).realize()
|
||||
b = a + 1
|
||||
linear = compile_linear(b.schedule_linear())
|
||||
compiled_prg = linear.src[-1].src[0]
|
||||
lib = bytearray(compiled_prg.src[3].arg)
|
||||
lib = bytearray(linear.src[-1].src[0].src[3].arg)
|
||||
|
||||
# Find s_endpgm (0xBFB00000) and replace with V_MOVRELD_B32 (op=66) which has no pcode
|
||||
# VOP1 encoding: bits[31:25]=0x7E, op=bits[16:9], so op=66 -> 66<<9 = 0x8400
|
||||
@@ -28,7 +27,7 @@ for i in range(0, len(lib) - 4, 4):
|
||||
break
|
||||
assert found, "s_endpgm not found"
|
||||
|
||||
patched_prg = dev.runtime(replace(compiled_prg.to_elf(), name="patched", lib=bytes(lib)))
|
||||
patched_prg = AMDProgram(dev, "patched", bytes(lib))
|
||||
b.uop.buffer.allocate()
|
||||
patched_prg(b.uop.buffer._buf, a.uop.buffer._buf, global_size=(1,1,1), local_size=(1,1,1))
|
||||
dev.synchronize()
|
||||
|
||||
@@ -19,12 +19,6 @@ class TestArange(unittest.TestCase):
|
||||
self.assertLess(self._get_flops(Tensor.arange(256).clone(), np.arange(256)), 256*4)
|
||||
self.assertLess(self._get_flops(Tensor.arange(2560).clone(), np.arange(2560)), 2560*4)
|
||||
|
||||
def test_cat_complexity(self):
|
||||
x = Tensor.arange(2**10) + Tensor.empty((), dtype=dtypes.uint32)
|
||||
out = x.cat(x).cat(Tensor.empty(1, dtype=dtypes.uint32))
|
||||
linear = compile_linear(out.schedule_linear())
|
||||
self.assertLessEqual(estimate_uop(linear.src[-1]).ops, out.numel()*20)
|
||||
|
||||
@unittest.skipIf(Device.DEFAULT == "CL", "flaky in CI")
|
||||
def test_arange_cumsum(self):
|
||||
np.testing.assert_equal(Tensor.arange(513).cumsum(0).numpy(), np.arange(513).cumsum())
|
||||
|
||||
@@ -2,7 +2,6 @@ import unittest
|
||||
from tinygrad import Tensor, UOp, GlobalCounters, Context, Device
|
||||
from tinygrad.dtype import AddrSpace, dtypes, Invalid
|
||||
from tinygrad.uop.ops import KernelInfo, AxisType, Ops
|
||||
from tinygrad.renderer.ptx import PTXRenderer
|
||||
|
||||
# **** kernels ****
|
||||
|
||||
@@ -56,7 +55,7 @@ def flip_contract_kernel(dest:UOp, src:UOp):
|
||||
return store.end(i, j).sink(arg=KernelInfo(name=f"flip_contract_{dest.numel()}", opts_to_apply=()))
|
||||
|
||||
def slice_sum_kernel(dest:UOp, src:UOp):
|
||||
G = UOp.range(src.shape[0], 0, dtype=dtypes.int)
|
||||
G = UOp.range(src.shape[0], 0)
|
||||
slice_src = src[G, :]
|
||||
reg = UOp.placeholder((1,), dest.dtype, 0, addrspace=AddrSpace.REG)
|
||||
reg = reg.after(G)[0].set(0)
|
||||
@@ -118,20 +117,13 @@ class TestCustomKernel(unittest.TestCase):
|
||||
out = c.flatten().tolist()
|
||||
assert all(x == 2 for x in out), "all 2"
|
||||
|
||||
def test_duplicate_call_arg(self):
|
||||
x = Tensor.arange(4).clone().realize()
|
||||
x = Tensor.custom_kernel(x, x, fxn=custom_add_one_kernel)[0]
|
||||
# webgpu silently errors when a kernel has duplicate buffer args, so the list stays the same.
|
||||
# https://gpuweb.github.io/gpuweb/#abstract-opdef-encoder-bind-groups-alias-a-writable-resource
|
||||
self.assertEqual(x.tolist(), [1, 2, 3, 4] if Device.DEFAULT != "WEBGPU" else [0, 1, 2, 3])
|
||||
|
||||
def test_simple_sharded(self):
|
||||
devs = ("CPU:0", "CPU:1")
|
||||
|
||||
a = Tensor.ones(16, 16).contiguous().shard(devs, axis=0)
|
||||
b = Tensor.ones(16, 16).contiguous().shard(devs, axis=0)
|
||||
# ugly construction to get a sharded empty tensor
|
||||
c = Tensor(Tensor.empty(8, 16, device=devs).uop.unshard(0), device=devs)
|
||||
c = Tensor(Tensor.empty(8, 16, device=devs).uop.multi(0), device=devs)
|
||||
c = Tensor.custom_kernel(c,a,b, fxn=custom_elementwise_add_kernel)[0]
|
||||
out = c.flatten().tolist()
|
||||
assert all(x == 2 for x in out), "all 2"
|
||||
@@ -140,7 +132,7 @@ class TestCustomKernel(unittest.TestCase):
|
||||
# PYTHON backend explicitly checks for OOB access for wrong multi shape regression
|
||||
devs = ("PYTHON:0", "PYTHON:1")
|
||||
a = Tensor.ones(4, 4).contiguous().shard(devs, axis=0)
|
||||
c = Tensor(Tensor.empty(2, 4, device=devs).uop.unshard(0), device=devs)
|
||||
c = Tensor(Tensor.empty(2, 4, device=devs).uop.multi(0), device=devs)
|
||||
c = Tensor.custom_kernel(c, a, fxn=custom_add_one_kernel)[0]
|
||||
assert (c == 2).all().item()
|
||||
|
||||
@@ -214,7 +206,7 @@ class TestCustomKernel(unittest.TestCase):
|
||||
N = 16
|
||||
a = Tensor.randn(N, N).shard_(devs, axis=0)
|
||||
b = Tensor.randn(N, N).to(devs)
|
||||
c = Tensor(Tensor.empty(N//2, N, device=devs).uop.unshard(0), device=devs)
|
||||
c = Tensor(Tensor.empty(N//2, N, device=devs).uop.multi(0), device=devs)
|
||||
tst = Tensor.custom_kernel(c, a, b, fxn=custom_gemm)[0]
|
||||
self.assertTrue(tst.allclose(a@b, atol=1e-3).item())
|
||||
|
||||
@@ -331,7 +323,7 @@ class TestCustomKernel(unittest.TestCase):
|
||||
def test_multi_invalids_custom_kernel_no_copy(self):
|
||||
devs = ("CPU:0", "CPU:1")
|
||||
a = Tensor.ones(4, 4).shard(devs, axis=0).realize()
|
||||
c = Tensor(Tensor.invalids(2, 4, dtype=dtypes.float, device=devs).uop.unshard(0), device=devs)
|
||||
c = Tensor(UOp.const(dtypes.float, Invalid, shape=(2, 4)).clone(device=devs).multi(0), device=devs)
|
||||
c = Tensor.custom_kernel(c, a, fxn=custom_add_one_kernel)[0]
|
||||
GlobalCounters.reset()
|
||||
c.realize()
|
||||
@@ -396,7 +388,7 @@ class TestCustomKernel(unittest.TestCase):
|
||||
y = Tensor.custom_kernel(y, x, fxn=custom_add_one_kernel)[0]
|
||||
if use_custom:
|
||||
z = Tensor.empty_like(x)
|
||||
z = Tensor.custom_kernel(z, y.T.T, fxn=custom_add_one_kernel)[0]
|
||||
z = Tensor.custom_kernel(y, y.T.T, fxn=custom_add_one_kernel)[0]
|
||||
else: z = y.T.T+1
|
||||
GlobalCounters.reset()
|
||||
z.realize()
|
||||
@@ -422,110 +414,19 @@ class TestCustomKernel(unittest.TestCase):
|
||||
|
||||
@Context(DEV="CPU")
|
||||
def test_simple_from_source(self):
|
||||
a = Tensor.arange(4).clone().realize()
|
||||
src = "void test_src(int* restrict a) { a[0] = 1; }"
|
||||
a = Tensor([0., 1., 2.]).realize()
|
||||
|
||||
src = "void test_src(float* restrict a) { a[0] = 1.0; }"
|
||||
# TODO: it currently requires a compiler for Ops.BINARY
|
||||
from tinygrad.device import Device
|
||||
binary = Device[a.device].renderer.compiler.compile(src)
|
||||
def custom_src_kernel(A:UOp) -> UOp:
|
||||
sink = UOp.sink(A, arg=KernelInfo(name="test_src"))
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple(sink.toposort())), UOp(Ops.SOURCE, arg=src), UOp(Ops.BINARY, arg=binary)))
|
||||
a = Tensor.custom_kernel(a.reshape(2, 2).T, fxn=custom_src_kernel)[0]
|
||||
self.assertEqual(a.tolist(), [[1, 2], [1, 3]])
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple(sink.toposort())),
|
||||
UOp(Ops.SOURCE, arg=src), UOp(Ops.BINARY, arg=binary)))
|
||||
|
||||
class TestCustomKernelInput(unittest.TestCase):
|
||||
def _test_mop(self, mop_fxn, max_kernels):
|
||||
# default: input is BUFFER
|
||||
x = mop_fxn(Tensor.arange(32).clone("CPU").realize())
|
||||
y = Tensor.custom_kernel(Tensor.empty_like(x), x, fxn=custom_add_one_kernel)[0]
|
||||
GlobalCounters.reset()
|
||||
y.realize()
|
||||
kernel_count = GlobalCounters.kernel_count
|
||||
self.assertEqual(y.tolist(), x.add(1).tolist())
|
||||
self.assertLessEqual(kernel_count, max_kernels)
|
||||
# same test with @function, input is PARAM
|
||||
from tinygrad import function
|
||||
x0 = Tensor.arange(32).clone("CPU").realize()
|
||||
@function(precompile=True)
|
||||
def run(a:Tensor) -> Tensor:
|
||||
xv = mop_fxn(a)
|
||||
y = Tensor.invalids(*xv.shape, dtype=xv.dtype, device=a.device)
|
||||
return Tensor.custom_kernel(y, xv, fxn=custom_add_one_kernel)[0]
|
||||
GlobalCounters.reset()
|
||||
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)
|
||||
|
||||
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)
|
||||
def test_double_permute(self): self._test_mop(lambda x: x.reshape(4, 8).T.T, max_kernels=3)
|
||||
def test_shrink(self): self._test_mop(lambda x: x[:4], max_kernels=2)
|
||||
def test_pad(self): self._test_mop(lambda x: x[:4].pad(((0, 4),)), max_kernels=2)
|
||||
def test_flip(self): self._test_mop(lambda x: x.flip(0), max_kernels=2)
|
||||
def test_offset_shrink(self): self._test_mop(lambda x: x[4:8], max_kernels=2)
|
||||
def test_2d_shrink(self): self._test_mop(lambda x: x.reshape(4, 8)[:, 2:6], max_kernels=3)
|
||||
def test_expand(self): self._test_mop(lambda x: x.reshape(16, 2)[:, :1].expand(16, 2), max_kernels=3)
|
||||
|
||||
class TestUnshardIndex(unittest.TestCase):
|
||||
"""Regression tests for INDEX on UNSHARD (fragment) resolution in schedule/multi.py.
|
||||
|
||||
A fragment is a per-thread REG buffer wrapped in UNSHARD over LOCAL thread ranges.
|
||||
index_multi must resolve an INDEX on the UNSHARD view into an INDEX on the per-thread
|
||||
shard. Two ownership patterns must work:
|
||||
contiguous: idx = rng*shard_sz + local (thread rng owns [rng*shard_sz, ...))
|
||||
strided: idx = rng + ir*shard_sz (thread rng owns {rng, rng+shard_sz, ...})
|
||||
"""
|
||||
def _run(self, kernel, shape=(8, 8)):
|
||||
c = Tensor.empty(*shape)
|
||||
out = Tensor.custom_kernel(c, fxn=kernel)[0]
|
||||
try: return out.numpy()
|
||||
except RuntimeError as e:
|
||||
if isinstance(Device[Device.DEFAULT].renderer, PTXRenderer) and "dynamic register indexing" in str(e):
|
||||
self.skipTest("PTX does not support dynamic register indexing")
|
||||
raise
|
||||
|
||||
@unittest.skipIf(not Device[Device.DEFAULT].renderer.has_local, "fragment tests need LOCAL ranges")
|
||||
def test_contiguous_fragment_index(self):
|
||||
# thread ty owns rows [ty*8, ty*8+8) of a 64-row fragment -- contiguous ownership.
|
||||
# This is the pre-existing case that index_multi always handled.
|
||||
def kernel(C:UOp) -> UOp:
|
||||
ty = UOp.range(8, 0, AxisType.LOCAL)
|
||||
ir = UOp.range(8, 1, AxisType.LOOP)
|
||||
j = UOp.range(8, 2, AxisType.LOOP)
|
||||
# 8x8 fragment, 8 threads -> 64x8 full tile. thread ty owns rows [ty*8, ty*8+8).
|
||||
frag = UOp.placeholder((8, 8), dtypes.float32, 0, AddrSpace.REG).unshard((0,), (ty,))
|
||||
return C[ty*8 + ir, j].store(frag[ty*8 + ir, j]).end(j, ir, ty).sink(arg=KernelInfo(name="contig_frag"))
|
||||
out = self._run(kernel, (64, 8))
|
||||
assert out.shape == (64, 8)
|
||||
|
||||
@unittest.skipIf(not Device[Device.DEFAULT].renderer.has_local, "fragment tests need LOCAL ranges")
|
||||
def test_strided_fragment_index(self):
|
||||
# thread ty owns rows {ty, ty+8, ty+16, ty+24, ..., ty+56} of a 64-row fragment --
|
||||
# strided ownership. idx = ty + ir*8 where shard_sz=8 (8 threads, shard rows=8).
|
||||
# The contiguous check (idx - rng*shard_sz) fails; the strided check
|
||||
# (idx-rng) % shard_sz == 0 must succeed. This is the pattern the index_multi fix adds.
|
||||
def kernel(C:UOp) -> UOp:
|
||||
ty = UOp.range(8, 0, AxisType.LOCAL)
|
||||
ir = UOp.range(8, 1, AxisType.LOOP)
|
||||
j = UOp.range(8, 2, AxisType.LOOP)
|
||||
# 8x8 fragment, 8 threads -> 64x8 full tile. thread ty owns rows {ty, ty+8, ..., ty+56}.
|
||||
frag = UOp.placeholder((8, 8), dtypes.float32, 0, AddrSpace.REG).unshard((0,), (ty,))
|
||||
return C[ty + ir*8, j].store(frag[ty + ir*8, j]).end(j, ir, ty).sink(arg=KernelInfo(name="strided_frag"))
|
||||
out = self._run(kernel, (64, 8))
|
||||
assert out.shape == (64, 8)
|
||||
|
||||
def test_fragment_index_cannot_shard(self):
|
||||
# thread ty indexing rows [ty, ty+8) overlaps with other threads' rows -- this matches neither
|
||||
# the contiguous nor the strided ownership pattern, so index_multi must raise.
|
||||
def kernel(C:UOp) -> UOp:
|
||||
ty = UOp.range(8, 0, AxisType.LOCAL)
|
||||
ir = UOp.range(8, 1, AxisType.LOOP)
|
||||
j = UOp.range(8, 2, AxisType.LOOP)
|
||||
frag = UOp.placeholder((8, 8), dtypes.float32, 0, AddrSpace.REG).unshard((0,), (ty,))
|
||||
return C[ty + ir, j].store(frag[ty + ir, j]).end(j, ir, ty).sink(arg=KernelInfo(name="bad_frag"))
|
||||
with self.assertRaisesRegex(RuntimeError, "cannot shard index"):
|
||||
self._run(kernel, (64, 8))
|
||||
a = Tensor.custom_kernel(a, fxn=custom_src_kernel)[0]
|
||||
self.assertEqual(a.tolist(), [1., 1., 2.])
|
||||
|
||||
class TestUOpReduce(unittest.TestCase):
|
||||
def test_uop_sum(self):
|
||||
|
||||
@@ -124,7 +124,6 @@ class TestFp8sConversions(unittest.TestCase):
|
||||
def test_float_to_fp8e4m3(self, x):
|
||||
np.testing.assert_equal(float_to_fp8(x, dtypes.fp8e4m3), torch.tensor(x, dtype=torch.float8_e4m3fn).view(torch.uint8).item())
|
||||
|
||||
@unittest.skip("fp8 overflow semantics are inconsistent")
|
||||
def test_float_to_fp8e4m3_extreme_values(self):
|
||||
for x in [FP8E4M3_MAX, FP8E4M3_MAX*1.01, -FP8E4M3_MAX, -FP8E4M3_MAX*1.01, math.inf, -math.inf, math.nan, -math.nan]:
|
||||
np.testing.assert_equal(float_to_fp8(x, dtypes.fp8e4m3), torch.tensor(x, dtype=torch.float8_e4m3fn).view(torch.uint8).item())
|
||||
|
||||
@@ -13,7 +13,6 @@ from tinygrad.renderer.ptx import PTXRenderer
|
||||
from tinygrad.renderer.cstyle import CUDARenderer
|
||||
from tinygrad.renderer.isa import ISARenderer
|
||||
from test.helpers import replace_opts
|
||||
from test.backend.test_softmax_fusion import single_kernel_softmax
|
||||
MOCKGPU = DEV.interface.startswith("MOCK")
|
||||
|
||||
from tinygrad.uop.render import print_uops # noqa: F401 # pylint: disable=unused-import
|
||||
@@ -60,6 +59,7 @@ class TestLinearizer(unittest.TestCase):
|
||||
self.assertEqual(len([x for x in uops if x.op is Ops.CAST]), 0)
|
||||
|
||||
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, PTXRenderer), "broken on ptx")
|
||||
@unittest.skipUnless(Device[Device.DEFAULT].renderer.supports_ranges, "test inspects ranges, which are rewritten to loops on this renderer")
|
||||
def test_late_bias_load(self):
|
||||
img = Tensor.empty(1, 3, 16, 16)
|
||||
w = Tensor.empty(16, 3, 3, 3)
|
||||
@@ -239,6 +239,7 @@ class TestLinearizer(unittest.TestCase):
|
||||
helper_arg_acc_dtype(d.conv2d(w, dtype=acc_dtype), expected_dtype)
|
||||
|
||||
@unittest.skipUnless(Device[Device.DEFAULT].renderer.supports_float4, "test requires float4")
|
||||
@unittest.skipUnless(Device[Device.DEFAULT].renderer.supports_ranges, "test inspects ranges, which are rewritten to loops on this renderer")
|
||||
def test_simple_unroll_no_between_phi_dependencies(self):
|
||||
x, y = Tensor.empty(64, 64), Tensor.empty(64, 64)
|
||||
r = (x@y).relu()
|
||||
@@ -300,6 +301,7 @@ class TestLinearizer(unittest.TestCase):
|
||||
program = to_program(replace_opts(linear.src[-1].src[0], []), renderer=Device[Device.DEFAULT].renderer)
|
||||
assert not any(u.op == Ops.WHERE for u in tuple(program.src[1].src)), "found where where where should be folded"
|
||||
|
||||
@unittest.skipUnless(Device[Device.DEFAULT].renderer.supports_ranges, "test inspects ranges, which are rewritten to loops on this renderer")
|
||||
def test_phi_simplification(self):
|
||||
def helper(t, max_ops=0):
|
||||
ast = helper_linearizer_opt(t)
|
||||
@@ -393,16 +395,6 @@ class TestLinearizer(unittest.TestCase):
|
||||
# the global store doesn't change
|
||||
assert stores[1].src[1].dtype == dtypes.float
|
||||
|
||||
@unittest.skipUnless(Device[Device.DEFAULT].renderer.has_local, "test requires locals")
|
||||
@unittest.skipUnless(Device[Device.DEFAULT].renderer.has_shared, "test requires shared")
|
||||
def test_two_grouped_stores_local(self):
|
||||
# GROUP on both reduces puts two LOCAL buffers in one kernel, and the store to each needs its own barrier
|
||||
a = Tensor.rand(32, 32).realize()
|
||||
opts = [Opt(OptOps.GROUP, 1, 4), Opt(OptOps.GROUP, 2, 4)]
|
||||
ast = helper_linearizer_opt(single_kernel_softmax(a), [opts])
|
||||
uops = to_program(replace_opts(ast, opts), renderer=Device[Device.DEFAULT].renderer).src[1].src
|
||||
self.assertEqual(len([u for u in uops if u.op is Ops.BARRIER]), 2)
|
||||
|
||||
# *** helpers ***
|
||||
|
||||
def helper_realized_ast(r:Tensor|list[Tensor]) -> tuple[UOp, list[Buffer]]:
|
||||
|
||||
@@ -80,7 +80,7 @@ class TestQuantizeFP8(unittest.TestCase):
|
||||
@needs_second_gpu
|
||||
def test_multi(self):
|
||||
devs = tuple(f"{Device.DEFAULT}:{i}" for i in range(8))
|
||||
x = Tensor.empty(2048*8, 1024, dtype=dtypes.bfloat16, device=devs).uop.unshard(0)
|
||||
x = Tensor.empty(2048*8, 1024, dtype=dtypes.bfloat16, device=devs).uop.multi(0)
|
||||
x = Tensor(x, device=devs)
|
||||
amax_state = Tensor.full((), 2.0, dtype=dtypes.float32, device=devs).contiguous()
|
||||
amax_out = Tensor.zeros((), dtype=dtypes.float32, device=devs).realize()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import unittest, random
|
||||
from tinygrad import Tensor, Device, nn, GlobalCounters, TinyJit, dtypes, Variable
|
||||
from tinygrad.uop.ops import Ops, UOp, AxisType
|
||||
from tinygrad.uop.ops import Ops, UOp
|
||||
from tinygrad.helpers import getenv, prod, Context
|
||||
from tinygrad.nn.state import get_parameters
|
||||
from tinygrad.engine.realize import run_linear, compile_linear
|
||||
@@ -52,10 +52,8 @@ class TestMultiTensor(unittest.TestCase):
|
||||
def test_shard(self):
|
||||
X = Tensor.ones(256).contiguous().realize()
|
||||
X.shard_(devices_2, 0)
|
||||
assert X.uop.src[0].shape == (128,)
|
||||
# the MULTI carries and ends the DEVICE range as its second src
|
||||
assert X.uop.src[1].op is Ops.RANGE and X.uop.src[1].arg[-1] is AxisType.DEVICE
|
||||
assert X.uop.ended_ranges == X.uop.src[1:]
|
||||
for lb in X.uop.src:
|
||||
assert lb.shape == (128,)
|
||||
(X + X).realize()
|
||||
|
||||
@unittest.expectedFailure # TODO: fix
|
||||
@@ -425,49 +423,6 @@ class TestMultiBufferView(unittest.TestCase):
|
||||
run_linear(linear, var_vals)
|
||||
np.testing.assert_equal(out.numpy(), ref[5].numpy())
|
||||
|
||||
@unittest.skipIf(not_support_multi_device(), "need multi")
|
||||
class Test2DShard(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.devices_4 = tuple(f"{Device.DEFAULT}:{i}" for i in range(4))
|
||||
self.rng = UOp.range(4, -1, AxisType.DEVICE)
|
||||
self.rng0, self.rng1 = self.rng // 2, self.rng % 2
|
||||
|
||||
def _shard_2d(self, t:Tensor) -> Tensor:
|
||||
u = t.uop.copy_to_device(self.devices_4)._shard(0, self.rng0)._shard(1, self.rng1).unshard((0, 1), (self.rng0, self.rng1))
|
||||
return Tensor(u)
|
||||
|
||||
def test_2d_shard_basic(self):
|
||||
ref = Tensor.arange(16).reshape(4, 4).contiguous().realize()
|
||||
t = self._shard_2d(ref)
|
||||
out = t.contiguous().realize()
|
||||
np.testing.assert_equal(out.numpy(), ref.numpy())
|
||||
|
||||
def test_2d_shard_elementwise(self):
|
||||
ref = Tensor.arange(16).reshape(4, 4).contiguous().realize()
|
||||
t = self._shard_2d(ref)
|
||||
out = (t + 1).contiguous().realize()
|
||||
np.testing.assert_equal(out.numpy(), ref.numpy() + 1)
|
||||
|
||||
def test_2d_shard_sum_all(self):
|
||||
ref = Tensor.arange(16).reshape(4, 4).contiguous().realize()
|
||||
t = self._shard_2d(ref)
|
||||
out = t.sum().contiguous().realize()
|
||||
np.testing.assert_equal(out.numpy(), np.array(ref.numpy().sum()))
|
||||
|
||||
def test_2d_shard_sum_non_sharded_axis(self):
|
||||
ref = Tensor.arange(4*4*2).reshape(4, 4, 2).contiguous().realize()
|
||||
t = self._shard_2d(ref)
|
||||
out = t.sum(axis=2).contiguous().realize()
|
||||
np.testing.assert_equal(out.numpy(), ref.numpy().sum(axis=2))
|
||||
|
||||
def test_2d_shard_matmul(self):
|
||||
a = Tensor.arange(16).reshape(4, 4).contiguous().realize()
|
||||
b = Tensor.arange(16).reshape(4, 4).contiguous().realize()
|
||||
a_s = self._shard_2d(a)
|
||||
b_s = self._shard_2d(b)
|
||||
out = (a_s @ b_s).contiguous().realize()
|
||||
np.testing.assert_equal(out.numpy(), a.numpy() @ b.numpy())
|
||||
|
||||
@unittest.skipIf(not_support_multi_device(), "need multi")
|
||||
class TestMultiTransformer(unittest.TestCase):
|
||||
@needs_second_gpu
|
||||
|
||||
@@ -339,12 +339,10 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op([], lambda: torch.ones(256,256).max(1)[0], lambda: Tensor.ones(256,256).max(1), forward_only=True)
|
||||
|
||||
def test_where(self):
|
||||
helper_test_op([], lambda: torch.where(torch.tensor([True, False]), 1, 3).type(torch.int32),
|
||||
lambda: Tensor([True, False]).where(1, 3).clone(), forward_only=True)
|
||||
helper_test_op(
|
||||
[(100,)],
|
||||
lambda x: torch.where(x > 0.5, 4, 2).type(torch.int32),
|
||||
lambda x: (x > 0.5).where(4, 2).clone(), forward_only=True)
|
||||
lambda x: (x > 0.5).where(4, 2), forward_only=True)
|
||||
|
||||
for shps in [[(8,),(1,),(1,)], [(10,10),(10,),(10,)], [(100,)]*3, [(10,10)]*3]:
|
||||
helper_test_op(
|
||||
@@ -356,7 +354,7 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op(
|
||||
[(5, 5)],
|
||||
lambda x: torch.where(x > 0.5, 4, 2).type(torch.int32).permute((1, 0)),
|
||||
lambda x: (x > 0.5).where(4, 2).clone().permute((1, 0)), forward_only=True)
|
||||
lambda x: (x > 0.5).where(4, 2).permute((1, 0)), forward_only=True)
|
||||
|
||||
def _test_cmp(self, fxn, reverse=True):
|
||||
# test different dtypes
|
||||
@@ -527,7 +525,6 @@ class TestOps(unittest.TestCase):
|
||||
|
||||
def test_add(self):
|
||||
helper_test_op([(45,68), (45,68)], lambda x,y: x+y, Tensor.add)
|
||||
helper_test_op([], lambda: torch.tensor(1)+0.5, lambda: Tensor(1)+0.5, forward_only=True)
|
||||
helper_test_op([(45,68), (45,68)], lambda x,y: x+y)
|
||||
helper_test_op([(), ()], lambda x,y: x+y)
|
||||
def test_add3(self):
|
||||
@@ -636,9 +633,9 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op(None, lambda x,y: x%y, forward_only=True, vals=[va, vb])
|
||||
helper_test_op(None, lambda x: x%2, forward_only=True, vals=[va])
|
||||
helper_test_op(None, lambda x: x%3, forward_only=True, vals=[va])
|
||||
helper_test_op(None, lambda x: x%3.5, lambda x: (x%3.5).clone(), forward_only=True, vals=[va])
|
||||
helper_test_op(None, lambda x: x%3.5, forward_only=True, vals=[va])
|
||||
helper_test_op(None, lambda x: 100%x, forward_only=True, vals=[va])
|
||||
helper_test_op(None, lambda x: 100.5%x, lambda x: (100.5%x).clone(), forward_only=True, vals=[va])
|
||||
helper_test_op(None, lambda x: 100.5%x, forward_only=True, vals=[va])
|
||||
|
||||
def test_fmod(self):
|
||||
a = [-4, 7, 5, 4, -7, 8, -9]
|
||||
@@ -649,7 +646,7 @@ class TestOps(unittest.TestCase):
|
||||
vb = [float(bi) for bi in b] if float_b else b
|
||||
helper_test_op(None, lambda x,y: x.fmod(y), forward_only=True, vals=[va, vb])
|
||||
helper_test_op(None, lambda x: x.fmod(2), forward_only=True, vals=[va])
|
||||
helper_test_op(None, lambda x: x.fmod(3.5), lambda x: x.fmod(3.5).clone(), forward_only=True, vals=[va])
|
||||
helper_test_op(None, lambda x: x.fmod(3.5), forward_only=True, vals=[va])
|
||||
|
||||
def test_mul_naninf(self):
|
||||
helper_test_op([(45,65)], lambda x: x*math.inf)
|
||||
@@ -706,7 +703,7 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op(None, lambda x: 0.7**x, vals=[[-2.,-1,0,1,2,3]])
|
||||
helper_test_op(None, lambda x: (-2)**x, vals=[[-2.,-1,0,1,2,3]])
|
||||
# float to power of int
|
||||
helper_test_op(None, lambda x: 0.7**x, lambda x: (0.7**x).clone(), vals=[[-2,-1,0,1,2,3]], forward_only=True)
|
||||
helper_test_op(None, lambda x: 0.7**x, vals=[[-2,-1,0,1,2,3]], forward_only=True)
|
||||
|
||||
@unittest.skipIf(COMPILE_ONLY, "test requires runtime")
|
||||
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, NIRRenderer), "TODO: broken in LVP")
|
||||
@@ -775,7 +772,7 @@ class TestOps(unittest.TestCase):
|
||||
def test_pow_int_base_float_exponent(self):
|
||||
for exponent in (0.5, 1.5, 2.0, -1.0, 0.0):
|
||||
helper_test_op([], lambda: torch.tensor([1, 2, 3, 4], dtype=torch.int) ** exponent,
|
||||
lambda: (Tensor([1, 2, 3, 4], dtype=dtypes.int32) ** exponent).clone(), forward_only=True)
|
||||
lambda: Tensor([1, 2, 3, 4], dtype=dtypes.int32) ** exponent, forward_only=True)
|
||||
|
||||
def test_sqrt(self):
|
||||
helper_test_op([(45,65)], lambda x: x.sqrt())
|
||||
@@ -906,7 +903,6 @@ class TestOps(unittest.TestCase):
|
||||
@unittest.skipIf(DEV.renderer == "NAK", "MUFU.SIN is not accurate enough")
|
||||
def test_cos(self):
|
||||
helper_test_op([(45,65)], lambda x: x.cos())
|
||||
helper_test_op([], lambda: torch.tensor(2.0).cos(), lambda: Tensor(2).cos(), forward_only=True)
|
||||
helper_test_op([()], lambda x: x.cos())
|
||||
if not ((DEV.interface.startswith("MOCK") and Device.DEFAULT == "NV") or Device.DEFAULT == "WEBGPU"):
|
||||
helper_test_op(None, lambda x: x.cos(), vals=[[math.nan, math.inf, -math.inf, 0.0]])
|
||||
@@ -985,8 +981,6 @@ class TestOps(unittest.TestCase):
|
||||
|
||||
def test_exp(self):
|
||||
helper_test_op([(45,65)], torch.exp, Tensor.exp)
|
||||
# int scalar input: the python coefficients in the decomposition must stay float
|
||||
helper_test_op([], lambda: torch.tensor(2.0).exp(), lambda: Tensor(2).exp(), forward_only=True)
|
||||
helper_test_op(None, torch.exp, Tensor.exp, vals=[[math.inf, -math.inf, math.nan]])
|
||||
helper_test_op([()], torch.exp, Tensor.exp)
|
||||
def test_exp2(self):
|
||||
|
||||
@@ -143,11 +143,12 @@ class TestOptim(unittest.TestCase):
|
||||
|
||||
@unittest.skipUnless(dtypes.half in Device[Device.DEFAULT].renderer.supported_dtypes(), "need half")
|
||||
def test_mixed_precision(self):
|
||||
self.enterContext(Context(DEFAULT_FLOAT=dtypes.half))
|
||||
old_default_float, dtypes.default_float = dtypes.default_float, dtypes.half
|
||||
# weight update would overflow without upcasting
|
||||
self._test_sgd(10, {'lr': 1e10}, 1e-6, 3e-4)
|
||||
self._test_adam(1, {'lr': 1e10}, 1e-4, 1e-4)
|
||||
self._test_adamw(1, {'lr': 1e10}, 1e-4, 1e-4)
|
||||
dtypes.default_float = old_default_float
|
||||
|
||||
def test_assert_tensor_train(self):
|
||||
t = Tensor.ones((1,1))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import unittest, math
|
||||
|
||||
from tinygrad import dtypes, Tensor, Device
|
||||
from tinygrad.helpers import getenv, DEV, Context
|
||||
from tinygrad.helpers import getenv, DEV
|
||||
from tinygrad.codegen import to_program
|
||||
|
||||
from tinygrad.uop.ops import Ops
|
||||
@@ -232,14 +232,16 @@ class TestRandomness(unittest.TestCase):
|
||||
@given(strat.sampled_from([dtypes.float, dtypes.float16, dtypes.bfloat16]))
|
||||
def test_randn_finite(self, default_float):
|
||||
if default_float not in Device[Device.DEFAULT].renderer.supported_dtypes(): return
|
||||
old_default_float = dtypes.default_float
|
||||
# low precision can result in inf from randn
|
||||
self.enterContext(Context(DEFAULT_FLOAT=default_float))
|
||||
dtypes.default_float = default_float
|
||||
t = Tensor.randn(64, 64)
|
||||
mx = t.max().numpy().item()
|
||||
mn = t.min().numpy().item()
|
||||
print(f"testing with {default_float=}")
|
||||
assert math.isfinite(mx), mx
|
||||
assert math.isfinite(mn), mn
|
||||
dtypes.default_float = old_default_float
|
||||
|
||||
def test_random_counter_overflow(self):
|
||||
device = Device.DEFAULT
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import unittest
|
||||
from tinygrad import Tensor, nn, Device, dtypes, Variable
|
||||
from tinygrad import Tensor, nn, Device
|
||||
from tinygrad.helpers import Context, GlobalCounters, getenv, PCONTIG, DEBUG
|
||||
from tinygrad.uop.ops import graph_rewrite, PatternMatcher, UPat, Ops, UOp
|
||||
from tinygrad.uop.ops import graph_rewrite, PatternMatcher, UPat, Ops
|
||||
from tinygrad.codegen.opt import OptOps, Opt
|
||||
from tinygrad.renderer.ptx import PTXRenderer
|
||||
from tinygrad.renderer.nir import NIRRenderer
|
||||
@@ -66,18 +66,6 @@ class TestRangeifyAssign(unittest.TestCase):
|
||||
self.assertListEqual(lst2, B.permute(1, 0).tolist())
|
||||
|
||||
class TestRangeifyEdgeCase(unittest.TestCase):
|
||||
def test_variable_stack_data(self):
|
||||
# a bound-Variable STACK used as data gets ranges from its graph position
|
||||
v = Variable("v", 0, 10).bind(3)
|
||||
t = Tensor(UOp.stack(v, v+1).cast(dtypes.int)) + Tensor.arange(2)
|
||||
self.assertListEqual(t.tolist(), [3, 5])
|
||||
|
||||
def test_variable_data_and_shape(self):
|
||||
# the same Variable has a data edge through CAST and a structural edge through SHRINK
|
||||
v = Variable("shared_v", 1, 10).bind(3)
|
||||
t = Tensor.ones(10)[:v] * Tensor(v.cast(dtypes.float))
|
||||
self.assertEqual(t.sum().item(), 9)
|
||||
|
||||
def test_matmul_relu_cat(self):
|
||||
a = Tensor.ones(100, 512).contiguous().realize()
|
||||
c = Tensor.ones(1, 512).contiguous().realize()
|
||||
|
||||
@@ -363,10 +363,9 @@ class TestCopyFolding(unittest.TestCase):
|
||||
|
||||
def test_one_hot_with_copy(self):
|
||||
y = Tensor([1, 2, 3]).to("CPU")
|
||||
x = y.one_hot(10).int()
|
||||
x = y.one_hot(10)
|
||||
check_schedule(x, 3, filter_sink=False)
|
||||
|
||||
@unittest.skip("no longer supported")
|
||||
def test_late_const_copy_folding(self):
|
||||
a = Tensor.arange(3).clone().realize()
|
||||
zeros = Tensor.zeros(3, buffer=False).realize()
|
||||
@@ -407,7 +406,6 @@ class TestCopyFolding(unittest.TestCase):
|
||||
assert t.uop.is_realized, f"didn't realize Tensor {t}"
|
||||
self.assertListEqual(t.tolist(), [1.,1.,1.,1.])
|
||||
|
||||
@unittest.skip("same-device copies are no-ops")
|
||||
def test_self_assign_same_device_copy(self):
|
||||
a = Tensor.ones(4, 4).contiguous().realize()
|
||||
# use copy_to_device to bypass Tensor.to() shortcircuit and force a real same-device COPY in the graph
|
||||
|
||||
@@ -464,7 +464,6 @@ class TestTinygrad(unittest.TestCase):
|
||||
data = data + [-x for x in data]
|
||||
np.testing.assert_allclose(Tensor(data, dtype=dtypes.int32).numpy(), np.array(data).astype(np.int32))
|
||||
|
||||
@unittest.skip("list elements are python scalars: numpy values in a list have no inferred dtype, use Tensor(np.array(data))")
|
||||
def test_tensor_list_ndarray(self):
|
||||
data = [np.array([1, 2, 3]), np.array([1, 2, 3]), np.array([1, 2, 3])]
|
||||
np.testing.assert_equal(Tensor(data).numpy(), np.array(data))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import unittest
|
||||
import numpy as np
|
||||
from tinygrad import Device, Tensor, Variable, TinyJit, dtypes
|
||||
from tinygrad import Tensor, Variable, dtypes
|
||||
from tinygrad.helpers import CHECK_OOB
|
||||
|
||||
class TestTensorVariable(unittest.TestCase):
|
||||
@@ -18,24 +18,13 @@ class TestTensorVariable(unittest.TestCase):
|
||||
self.assertListEqual((vv * t).tolist(), [2, 2, 2])
|
||||
except RuntimeError: pass
|
||||
|
||||
@unittest.skipUnless(dtypes.long in Device[Device.DEFAULT].renderer.supported_dtypes(), "requires long support")
|
||||
def test_large_range_variable(self):
|
||||
self.assertEqual(Tensor(Variable("b", 0, 2**40, dtype=dtypes.long).bind(2**35)).clone(Device.DEFAULT).item(), 2**35)
|
||||
|
||||
@unittest.skipUnless(dtypes.long in Device[Device.DEFAULT].renderer.supported_dtypes(), "requires long support")
|
||||
def test_large_range_variable_jit(self):
|
||||
@TinyJit
|
||||
def f(a,b): return (Tensor(a+b).clone(Device.DEFAULT) * 2).realize()
|
||||
for i in range(3):
|
||||
a = Variable("a", 0, 2**10, dtype=dtypes.int).bind(i)
|
||||
b = Variable("b", 0, 2**40, dtype=dtypes.long).bind(2**35)
|
||||
self.assertEqual(f(a,b).item(), (2**35 + i) * 2)
|
||||
|
||||
def test_variable_defers_like_a_literal(self):
|
||||
vv = Variable("a", 1, 10).bind(2)
|
||||
self.assertEqual(Tensor(vv).dtype, dtypes.weakint)
|
||||
self.assertEqual((Tensor(vv) + Tensor([1], dtype=dtypes.int8)).dtype, dtypes.int8) # takes the concrete side, no widening
|
||||
self.assertEqual(Tensor(vv).item(), 2) # a read commits at default_int
|
||||
vv = Variable("b", 0, 2**40).bind(2**35)
|
||||
# TODO: pm_lower_index_dtype lowers ALU PARAM to int32 unconditionally
|
||||
try:
|
||||
self.assertEqual(Tensor(vv).item(), 2**35)
|
||||
except AssertionError:
|
||||
pass
|
||||
|
||||
def test_variable_tensor_dtype_arg(self):
|
||||
vv = Variable("a", 1, 10).bind(2)
|
||||
|
||||
@@ -3,6 +3,7 @@ from tinygrad import Tensor, UOp
|
||||
from tinygrad.device import Device, Buffer, BufferSpec
|
||||
from tinygrad.dtype import AddrSpace, dtypes
|
||||
from tinygrad.engine.realize import run_linear
|
||||
from tinygrad.renderer.isa.x86 import X86Renderer
|
||||
from tinygrad.uop.ops import Ops, KernelInfo
|
||||
|
||||
def wait_loop_kernel(C:UOp) -> UOp:
|
||||
@@ -81,6 +82,7 @@ def loop_in_loop_kernel(C:UOp) -> UOp:
|
||||
|
||||
return C[0].store(i[0].load()).sink(arg=KernelInfo(name="loop_in_loop", opts_to_apply=()))
|
||||
|
||||
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, X86Renderer), "loops are not supported in X86")
|
||||
class TestWaitLoop(unittest.TestCase):
|
||||
def test_wait_loop(self):
|
||||
c = Tensor.empty(1, dtype=dtypes.int)
|
||||
@@ -107,6 +109,7 @@ class TestWaitLoop(unittest.TestCase):
|
||||
self.assertEqual(c.item(), 12)
|
||||
|
||||
@unittest.skipUnless(Device.DEFAULT in ("CPU", "AMD", "NV"), "need proper uncached=True handling")
|
||||
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, X86Renderer), "loops are not supported in X86")
|
||||
class TestVolatileLoops(unittest.TestCase):
|
||||
def test_async_wait_ext(self):
|
||||
sig_buf = Buffer(Device.DEFAULT, 1, dtypes.int, options=BufferSpec(host=True, uncached=True, cpu_access=True), preallocate=True)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import unittest
|
||||
from tinygrad.device import CompileError, Device, BufferSpec, TinyELF
|
||||
from tinygrad.helpers import Target
|
||||
from tinygrad.device import CompileError, Device, BufferSpec
|
||||
if Device.DEFAULT=="METAL":
|
||||
from tinygrad.runtime.ops_metal import MetalDevice, MetalCompiler
|
||||
from tinygrad.runtime.ops_metal import MetalDevice, MetalCompiler, MetalProgram
|
||||
@unittest.skipIf(Device.DEFAULT!="METAL", "Metal support required")
|
||||
class TestMetal(unittest.TestCase):
|
||||
def test_alloc_oom(self):
|
||||
@@ -49,7 +48,7 @@ kernel void r_5(device int* data0, const device int* data1, uint3 gid [[threadgr
|
||||
""")
|
||||
with self.assertRaises(RuntimeError):
|
||||
compiled = compiled[:40] # corrupt the compiled program
|
||||
device.runtime(TinyELF(compiled, "r_5", Target("METAL"), ()))
|
||||
MetalProgram(device, "r_5", compiled)
|
||||
|
||||
def test_free(self):
|
||||
size = 2**16
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from tinygrad import Device
|
||||
from tinygrad.device import Buffer, TinyELF
|
||||
from tinygrad.device import Buffer
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.helpers import Target
|
||||
from tinygrad.runtime.ops_cl import CLDevice, CLAllocator, CLCompiler
|
||||
from tinygrad.runtime.ops_cl import CLDevice, CLAllocator, CLCompiler, CLProgram
|
||||
|
||||
@unittest.skipUnless(Device.DEFAULT == "CL", "Runs only on OpenCL")
|
||||
class TestCLCompileCache(unittest.TestCase):
|
||||
def test_compile_cached(self):
|
||||
device = Device[Device.DEFAULT]
|
||||
src = "__kernel void cached_test(__global int* a) { a[0] = 1; }"
|
||||
obj = TinyELF(src.encode(), "cached_test", Target("CL"), ())
|
||||
device.runtime(obj)
|
||||
CLProgram(device, name="cached_test", lib=src.encode())
|
||||
with patch.object(CLCompiler, 'compile', side_effect=RuntimeError("compile should not be called on cache hit")):
|
||||
device.runtime(obj)
|
||||
CLProgram(device, name="cached_test", lib=src.encode())
|
||||
|
||||
@unittest.skipUnless(Device.DEFAULT == "CL", "Runs only on OpenCL")
|
||||
class TestCLError(unittest.TestCase):
|
||||
@@ -29,7 +27,7 @@ class TestCLError(unittest.TestCase):
|
||||
def test_invalid_kernel_name(self):
|
||||
device = Device[Device.DEFAULT]
|
||||
with self.assertRaises(RuntimeError) as err:
|
||||
device.runtime(TinyELF(b"__kernel void test(__global int* a) { a[0] = 1; }", "", Target("CL"), ()))
|
||||
CLProgram(device, name="", lib="__kernel void test(__global int* a) { a[0] = 1; }".encode())
|
||||
assert str(err.exception) == "OpenCL Error -46: CL_INVALID_KERNEL_NAME"
|
||||
|
||||
def test_unaligned_copy(self):
|
||||
|
||||
+2
-3
@@ -1,8 +1,7 @@
|
||||
from tinygrad import Tensor, dtypes
|
||||
dtypes.default_float = dtypes.float16
|
||||
from tinygrad.dtype import to_dtype
|
||||
from tinygrad.helpers import getenv, Context
|
||||
|
||||
Context(DEFAULT_FLOAT=dtypes.float16).__enter__()
|
||||
from tinygrad.helpers import getenv
|
||||
|
||||
if __name__ == "__main__":
|
||||
# matmuls in bert layers
|
||||
|
||||
+2
-3
@@ -1,10 +1,9 @@
|
||||
from tinygrad import Tensor, dtypes, GlobalCounters
|
||||
dtypes.default_float = dtypes.float16
|
||||
from tinygrad.dtype import to_dtype
|
||||
from tinygrad.helpers import getenv, Context
|
||||
from tinygrad.helpers import getenv
|
||||
from test.backend.test_softmax_fusion import single_kernel_softmax
|
||||
|
||||
Context(DEFAULT_FLOAT=dtypes.float16).__enter__()
|
||||
|
||||
if __name__ == "__main__":
|
||||
# softmax in bert layers
|
||||
BS = getenv("BS", 96//6)
|
||||
|
||||
+7
-7
@@ -13,9 +13,9 @@ from tinygrad.dtype import Invalid
|
||||
|
||||
def vision_conv_143():
|
||||
c0 = UOp.param(0, dtypes.half, shape=(16, 1024, 4))
|
||||
c2 = UOp.range(32, 3)
|
||||
c5 = UOp.range(128, 4)
|
||||
c8 = UOp.range(16, 2)
|
||||
c2 = UOp.range(32, 3, AxisType.LOOP)
|
||||
c5 = UOp.range(128, 4, AxisType.LOOP)
|
||||
c8 = UOp.range(16, 2, AxisType.LOOP)
|
||||
c16 = UOp.range(7, 0, AxisType.REDUCE)
|
||||
c17 = c8*2+c16
|
||||
c24 = ((c17<3)!=True)&(c17<35)
|
||||
@@ -39,9 +39,9 @@ def vision_conv_143():
|
||||
|
||||
def vision_conv_153():
|
||||
c0 = UOp.param(0, dtypes.half, shape=(8, 1024, 4))
|
||||
c2 = UOp.range(16, 3)
|
||||
c5 = UOp.range(256, 4)
|
||||
c8 = UOp.range(8, 2)
|
||||
c2 = UOp.range(16, 3, AxisType.LOOP)
|
||||
c5 = UOp.range(256, 4, AxisType.LOOP)
|
||||
c8 = UOp.range(8, 2, AxisType.LOOP)
|
||||
c16 = UOp.range(7, 0, AxisType.REDUCE)
|
||||
c17 = c8*2+c16
|
||||
c24 = ((c17<3)!=True)&(c17<19)
|
||||
@@ -65,7 +65,7 @@ def vision_conv_153():
|
||||
|
||||
def dm_conv_172():
|
||||
c0 = UOp.param(0, dtypes.half, shape=(1, 240, 4))
|
||||
c2 = UOp.range(960, 4)
|
||||
c2 = UOp.range(960, 4, AxisType.LOOP)
|
||||
c5 = UOp.param(1, dtypes.half, shape=(8, 384, 4))
|
||||
c7 = UOp.range(32, 0, AxisType.REDUCE)
|
||||
c10 = UOp.range(4, 1, AxisType.REDUCE)
|
||||
|
||||
+2
-2
@@ -16,9 +16,9 @@ def _run(code:str, timeout:float=15.0) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(["python", "-c", code], env={**os.environ, "AMD": "1"}, capture_output=True, text=True, timeout=timeout)
|
||||
|
||||
def _run_asm(asm_src:str) -> subprocess.CompletedProcess:
|
||||
return _run('from tinygrad.device import Device, TinyELF; from tinygrad.helpers import Target; '
|
||||
return _run('from tinygrad.device import Device; from tinygrad.runtime.ops_amd import AMDProgram; '
|
||||
'from tinygrad.runtime.support.compiler_amd import HIPCompiler; dev = Device["AMD"]; '
|
||||
f'dev.runtime(TinyELF(HIPCompiler(dev.arch).compile("""{asm_src}"""), "test", Target("AMD", arch=dev.arch), ()))('
|
||||
f'AMDProgram(dev, "test", HIPCompiler(dev.arch).compile("""{asm_src}"""))('
|
||||
'dev.allocator.alloc(64), global_size=(1,1,1), local_size=(1,1,1), wait=True)')
|
||||
|
||||
def _verify_recovery() -> subprocess.CompletedProcess:
|
||||
|
||||
+17
-7
@@ -5,9 +5,7 @@ These tests intentionally cause GPU faults to verify error handling.
|
||||
Run with: DEV=AMD python -m pytest test/external/external_test_gpu_crash.py -v
|
||||
"""
|
||||
import unittest, re, importlib
|
||||
from tinygrad import Device, UOp, dtypes
|
||||
from tinygrad.engine.realize import run_linear
|
||||
from tinygrad.uop.ops import Ops, KernelInfo
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.renderer.amd.dsl import s, v, Inst, NULL
|
||||
|
||||
RDNA3_CDNA3_MAP = {"v_mov_b32_e32": "v_mov_b32_e32", "s_mov_b32": "s_mov_b32", "s_waitcnt": "s_waitcnt", "s_endpgm": "s_endpgm",
|
||||
@@ -15,11 +13,20 @@ RDNA3_CDNA3_MAP = {"v_mov_b32_e32": "v_mov_b32_e32", "s_mov_b32": "s_mov_b32", "
|
||||
"global_atomic_add_u32": "global_atomic_add", "flat_load_b32": "flat_load_dword",
|
||||
"flat_store_b32": "flat_store_dword", "flat_atomic_add_u32": "flat_atomic_add", "s_load_b32": "s_load_dword"}
|
||||
|
||||
def assemble(code:str, name:str="test", is_cdna:bool=False) -> str:
|
||||
kd = {"next_free_vgpr": 8, "next_free_sgpr": 8, "user_sgpr_kernarg_segment_ptr": 1, "kernarg_size": 8}
|
||||
if is_cdna: kd["accum_offset"] = 8
|
||||
else: kd["wavefront_size32"] = 1
|
||||
return f".text\n.globl {name}\n.p2align 8\n.type {name},@function\n{name}:\n{code}\n.rodata\n.p2align 6\n.amdhsa_kernel {name}\n" + \
|
||||
"\n".join(f".amdhsa_{k} {v}" for k,v in kd.items()) + "\n.end_amdhsa_kernel"
|
||||
|
||||
@unittest.skipIf(Device.DEFAULT != "AMD", "AMD required")
|
||||
class TestGPUCrash(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
from tinygrad.runtime.support.compiler_amd import HIPCompiler
|
||||
cls.dev = Device["AMD"]
|
||||
cls.compiler = HIPCompiler(cls.dev.arch)
|
||||
cls.is_cdna = cls.dev.target[0] < 10
|
||||
ins = importlib.import_module('tinygrad.runtime.autogen.amd.' + ('cdna' if cls.is_cdna else 'rdna3') + '.ins')
|
||||
for rdna3_name, cdna3_name in RDNA3_CDNA3_MAP.items():
|
||||
@@ -34,11 +41,14 @@ class TestGPUCrash(unittest.TestCase):
|
||||
except Exception:
|
||||
self.fail("Device not working before test")
|
||||
|
||||
def _run(self, code: str):
|
||||
from tinygrad.runtime.ops_amd import AMDProgram
|
||||
prg = AMDProgram(self.dev, "test", self.compiler.compile(assemble(code, is_cdna=self.is_cdna)))
|
||||
prg(self.dev.allocator.alloc(64), global_size=(1,1,1), local_size=(1,1,1), wait=True)
|
||||
|
||||
def _run_insts(self, insts: list[Inst]):
|
||||
buf = UOp.new_buffer("AMD", 64, dtypes.uint8)
|
||||
sink = UOp.sink(UOp.param(0, dtypes.uint8, (64,), device="AMD"), UOp.special(1, "lidx0"), arg=KernelInfo("test"))
|
||||
prg = UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple(UOp(Ops.INS, arg=i) for i in insts))))
|
||||
run_linear(UOp(Ops.LINEAR, src=(prg.call(buf),)), wait=True)
|
||||
from test.amd.disasm import disasm
|
||||
self._run("\n".join(disasm(i) for i in insts))
|
||||
|
||||
def _assert_gpu_fault(self, func):
|
||||
"""Assert that func raises a RuntimeError indicating a GPU fault (not a setup error)."""
|
||||
|
||||
+3
-2
@@ -3,7 +3,6 @@ import unittest
|
||||
import numpy as np
|
||||
from tinygrad import Tensor, dtypes
|
||||
from tinygrad.engine.jit import TinyJit
|
||||
from tinygrad.helpers import Context
|
||||
from test.helpers import derandomize_model
|
||||
|
||||
from examples.llama import Transformer
|
||||
@@ -15,7 +14,8 @@ def helper_test_jitted_correctness(gen, train, train_jit):
|
||||
|
||||
class TestJittedModels(unittest.TestCase):
|
||||
def test_jitted_tiny_llama(self):
|
||||
self.enterContext(Context(DEFAULT_FLOAT=dtypes.float16))
|
||||
old_float = dtypes.default_float
|
||||
dtypes.default_float = dtypes.float16
|
||||
|
||||
args_tiny = {"dim": 1024, "hidden_dim": 1024, "n_heads": 8, "n_layers": 8, "norm_eps": 1e-05, "vocab_size": 1000}
|
||||
model = Transformer(**args_tiny)
|
||||
@@ -25,6 +25,7 @@ class TestJittedModels(unittest.TestCase):
|
||||
@TinyJit
|
||||
def test_jit(t): return model(t, 0).realize()
|
||||
helper_test_jitted_correctness(lambda: (Tensor([[1,]]),), test, test_jit)
|
||||
dtypes.default_float = old_float
|
||||
|
||||
def test_jitted_stable_diffusion(self):
|
||||
from examples.stable_diffusion import UNetModel, unet_params
|
||||
|
||||
@@ -51,7 +51,7 @@ class TestEval(unittest.TestCase):
|
||||
vae = AutoencoderKL()
|
||||
for p in get_parameters(vae): p.to_(GPUS).realize()
|
||||
x = Tensor.zeros(48,4,64,64).contiguous().to(GPUS).realize()
|
||||
x.uop = x.uop.unshard(0)
|
||||
x.uop = x.uop.multi(0)
|
||||
for _ in range(2): vae_decode(x, vae)
|
||||
|
||||
if __name__=="__main__":
|
||||
|
||||
+3
-4
@@ -15,7 +15,7 @@ from tinygrad.codegen.late.linearizer import linearize
|
||||
|
||||
# decorator to skip slow tests by default, run with RUN_SLOW=1 to include them
|
||||
slow = unittest.skipUnless(os.getenv("RUN_SLOW"), "slow test, set RUN_SLOW=1 to run")
|
||||
from tinygrad.runtime.ops_python import PythonRenderer
|
||||
from tinygrad.runtime.ops_python import PythonProgram, PythonRenderer, PythonCompiler
|
||||
|
||||
def full_rewrite(sink:UOp, ren:Renderer|None=None) -> UOp:
|
||||
if ren is None: ren = Renderer(Target())
|
||||
@@ -83,15 +83,14 @@ def timeit(fxn:Callable[..., T], *args, **kwargs) -> tuple[T, float]:
|
||||
return ret, (time.perf_counter_ns()-st)*1e-6
|
||||
|
||||
def eval_uop(uop:UOp, inputs:list[tuple[DType, list[Any]]]|None=None, vals:tuple[int, ...]=()):
|
||||
dev = Device['PYTHON']
|
||||
allocator = dev.allocator
|
||||
allocator = Device['PYTHON'].allocator
|
||||
bufs = []
|
||||
for buf_dt, data in inputs or []:
|
||||
bufs.append(buf:=allocator.alloc(len(data) * buf_dt.itemsize))
|
||||
allocator._copyin(buf, memoryview(struct.pack(str(len(data)) + (buf_dt.fmt or ""), *data)))
|
||||
g = UOp.param(0, uop.dtype, (1,))
|
||||
prg = to_program(UOp.store(g.index(UOp.const(dtypes.int, 0)), uop).sink(arg=KernelInfo()), PythonRenderer(Target("PYTHON")))
|
||||
prog = dev.runtime(prg.to_elf())
|
||||
prog = PythonProgram("run", PythonCompiler().compile(prg.src[2].arg))
|
||||
prog(out_buf:=allocator.alloc(uop.dtype.itemsize), *bufs, vals=vals)
|
||||
return out_buf.cast(uop.dtype.fmt or "").tolist()[0]
|
||||
|
||||
|
||||
@@ -131,8 +131,7 @@ class PM4Executor(AMDQueue):
|
||||
_ = self._next_dword() # ev
|
||||
|
||||
ptr = to_mv(self.gpu.translate_addr(addr_lo + (addr_hi << 32)), 8)
|
||||
if mem_data_sel == 1: ptr.cast('I')[0] = val & 0xffffffff
|
||||
elif mem_data_sel == 2: ptr.cast('Q')[0] = val
|
||||
if mem_data_sel == 1 or mem_data_sel == 2: ptr.cast('Q')[0] = val
|
||||
elif mem_data_sel == 3:
|
||||
if mem_event_type == CACHE_FLUSH_AND_INV_TS_EVENT: ptr.cast('Q')[0] = int(time.perf_counter() * 1e8)
|
||||
else: raise RuntimeError(f"Unknown {mem_data_sel=} {mem_event_type=}")
|
||||
|
||||
+8
-77
@@ -6,7 +6,7 @@
|
||||
# arg=3: lds - local data share
|
||||
# arg=4: scratch - per-lane scratch memory
|
||||
from __future__ import annotations
|
||||
import ctypes, functools, re, platform, subprocess, tempfile, os
|
||||
import ctypes, functools, re, platform, subprocess, tempfile
|
||||
from typing import Callable
|
||||
|
||||
# Set/restore DAZ+FTZ (denormals-are-zero + flush-to-zero) to match RDNA3 default float mode
|
||||
@@ -51,7 +51,7 @@ class _MXCSRContext:
|
||||
if lib is None or not hasattr(self, '_saved'): return
|
||||
lib.set_fpcr(self._saved)
|
||||
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo, AxisType
|
||||
from tinygrad.dtype import dtypes, AddrSpace
|
||||
from tinygrad.device import Buffer, BufferSpec, Device
|
||||
from tinygrad.runtime.autogen import hsa
|
||||
@@ -372,33 +372,11 @@ def _write_val(bits: int, val: UOp, wfn, reg_or_addr, *args, is_mem: bool = Fals
|
||||
return _write_64bit(val, wfn, reg_or_addr, is_mem, *args) if bits == 64 else [wfn(reg_or_addr, _to_u32(val), *args)]
|
||||
|
||||
def _mem_store(mem: UOp, addr: UOp, val: UOp, active: UOp, addr_bits: int = 32, data_bits: int = 32) -> list[UOp]:
|
||||
"""Conditional memory store with sub-word and unaligned support. Returns list of store UOps.
|
||||
|
||||
FLAT/GLOBAL accesses on AMD hardware are allowed to be unaligned, so a 32-bit store at a
|
||||
byte offset of 1-3 spans two words (handled in 64-bit domain to keep shifts in range)."""
|
||||
"""Conditional memory store with sub-word support. Returns list of store UOps."""
|
||||
adt = dtypes.uint64 if addr_bits == 64 else dtypes.uint32
|
||||
if data_bits > 32: # wider stores decompose into dwords; each dword handles its own alignment
|
||||
ws = val.cast(dtypes.uint64) if data_bits > 64 else val
|
||||
return [s for i in range(data_bits // 32)
|
||||
for s in _mem_store(mem, addr + UOp.const(adt, i * 4), ws >> UOp.const(ws.dtype, 32 * i) if i else ws, active, addr_bits, 32)]
|
||||
word_addr = addr >> UOp.const(adt, 2)
|
||||
idx = mem.index(word_addr.valid(active))
|
||||
if data_bits == 32:
|
||||
byte_off = (addr & UOp.const(adt, 3)).cast(dtypes.uint32)
|
||||
is_unaligned = byte_off.ne(UOp.const(dtypes.uint32, 0))
|
||||
if addr.divides(4) is not None: return [idx.store(active.where(_to_u32(val), idx))]
|
||||
shift = byte_off * UOp.const(dtypes.uint32, 8)
|
||||
val64 = _to_u32(val).cast(dtypes.uint64)
|
||||
# word0 keeps its low byte_off*8 bits, gets val's low bits shifted in; word1 gets the rest
|
||||
low_keep = (UOp.const(dtypes.uint32, 1) << shift) - UOp.const(dtypes.uint32, 1)
|
||||
lo_bits = ((val64 << shift.cast(dtypes.uint64)) & UOp.const(dtypes.uint64, 0xFFFFFFFF)).cast(dtypes.uint32)
|
||||
new_word0 = (idx & low_keep) | lo_bits
|
||||
store0 = idx.store(active.where(is_unaligned.where(new_word0, _to_u32(val)), idx))
|
||||
idx1 = mem.index((word_addr + UOp.const(adt, 1)).cast(dtypes.int64).valid(active & is_unaligned))
|
||||
spill = (val64 >> (UOp.const(dtypes.uint64, 32) - shift.cast(dtypes.uint64))).cast(dtypes.uint32)
|
||||
keep = UOp.const(dtypes.uint32, 0xFFFFFFFF) << shift
|
||||
new_word1 = (idx1 & keep) | spill
|
||||
return [store0, idx1.store((active & is_unaligned).where(new_word1, idx1))]
|
||||
if data_bits == 32: return [idx.store(active.where(_to_u32(val), idx))]
|
||||
# Sub-word store: read-modify-write with mask
|
||||
byte_pos = addr.cast(dtypes.uint32) & _c(3)
|
||||
byte_shift = byte_pos * _c(8)
|
||||
@@ -468,7 +446,7 @@ class _Ctx:
|
||||
"""Create a lane range UOp with unique axis ID."""
|
||||
if n is None: n = self.wave_size
|
||||
self._axis_id += 1
|
||||
return UOp.range(n, self._axis_id, dtype=dtypes.int)
|
||||
return UOp.range(n, self._axis_id, AxisType.LOOP, dtype=dtypes.int)
|
||||
|
||||
def unroll_lanes(self, get_lane_bit, exec_mask: UOp, apply_exec: bool = True) -> UOp:
|
||||
"""Combine lane bits into a mask using RANGE+REDUCE (32-bit for RDNA, 64-bit for CDNA)."""
|
||||
@@ -2027,9 +2005,7 @@ def _compile_mubuf(inst: irc.MUBUF, ctx: _Ctx) -> UOp:
|
||||
|
||||
stores: list[UOp] = []
|
||||
if is_lds and not is_store:
|
||||
# LDS load: buffer -> LDS (bypass VGPRs), LDS addr = M0[17:0] + lane * elem_size.
|
||||
# HW never takes a per-lane LDS address: kittens' direct fill sets M0 (s_mov_b32 m0, sN)
|
||||
# before every lds instruction, giving lane-linear chunks with the swizzle on the GLOBAL side.
|
||||
# LDS load: buffer -> LDS (bypass VGPRs), LDS addr = M0[17:0] + lane * elem_size
|
||||
lds_base = ctx.rsgpr_dyn(_c(124)) & _c(0x3FFFF)
|
||||
lds_addr = lds_base + lane.cast(dtypes.uint32) * _c(n_dwords * 4)
|
||||
for i in range(n_dwords):
|
||||
@@ -2138,8 +2114,6 @@ def _decode_at(pc: int, arch: str):
|
||||
F32_INLINE = {240: 0x3f000000, 241: 0xbf000000, 242: 0x3f800000, 243: 0xbf800000, # 0.5, -0.5, 1.0, -1.0
|
||||
244: 0x40000000, 245: 0xc0000000, 246: 0x40800000, 247: 0xc0800000, 248: 0x3e22f983} # 2.0, -2.0, 4.0, -4.0, 1/(2*pi)
|
||||
|
||||
_inst_hist: dict = {}
|
||||
|
||||
class WaveState:
|
||||
__slots__ = ('vgpr_buf', 'sgpr_buf', 'accvgpr_buf', '_vgpr_mv', '_sgpr_mv', 'n_lanes', 'wave_size')
|
||||
|
||||
@@ -2235,8 +2209,7 @@ def run_asm(lib: int, lib_sz: int, gx: int, gy: int, gz: int, lx: int, ly: int,
|
||||
# Use Buffer objects with external_ptr=0 for vmem
|
||||
vmem_buf = Buffer('CPU', 1 << 40, dtypes.uint32, options=BufferSpec(external_ptr=0)).ensure_allocated()
|
||||
lds_buf = Buffer('CPU', max(lds_size // 4, 1), dtypes.uint32).ensure_allocated()
|
||||
ctypes.memset(lds_buf._buf.va_addr, 0, max(lds_size, 4))
|
||||
# NOTE: scratch (private/spill) memory is per-wavefront; buffers are allocated in the wave loop below
|
||||
scratch_buf = Buffer('CPU', scratch_size * wave_size, dtypes.uint8).ensure_allocated() if scratch_size else None
|
||||
|
||||
# Initialize SQTT encoder — emits packets inline as instructions execute (only when profiling)
|
||||
if PROFILE:
|
||||
@@ -2254,8 +2227,6 @@ def run_asm(lib: int, lib_sz: int, gx: int, gy: int, gz: int, lx: int, ly: int,
|
||||
print(colored(msg, 'green') if len(_canonical_runner_cache) > prev_len else msg)
|
||||
return program[pc]
|
||||
|
||||
if os.getenv("EMU_TRACE_INST") or os.getenv("EMU_WATCH_PC") is not None:
|
||||
print(f"[emu-dispatch] gx={gx} gy={gy} gz={gz} lx={lx} ly={ly} lz={lz} scratch={scratch_size}", flush=True)
|
||||
# Set DAZ+FTZ during emulator execution, restore afterward to avoid breaking hypothesis tests
|
||||
# Only trace the first workgroup (like real HW traces one CU/SIMD), subsequent workgroups run but don't add to trace
|
||||
tracing = bool(PROFILE)
|
||||
@@ -2266,27 +2237,18 @@ def run_asm(lib: int, lib_sz: int, gx: int, gy: int, gz: int, lx: int, ly: int,
|
||||
for gidx in range(gx):
|
||||
# Initialize all wavefronts for this workgroup
|
||||
waves: list[tuple[WaveState, list]] = []
|
||||
wave_scratch_bufs: list[Buffer] = [] # keep alive for the dispatch
|
||||
for wave_start in range(0, total_threads, wave_size):
|
||||
st = _init_wave(lib, wave_start, total_threads, lx, ly, lz, args_ptr, rsrc2, scratch_size, arch, gidx, gidy, gidz, user_data,
|
||||
wave_size)
|
||||
# each wavefront owns its private (spill) scratch segment: on real HW the ring is indexed by
|
||||
# (wave_id, lane), and scratch addresses here are lane*stride within the wave's segment.
|
||||
if scratch_size:
|
||||
wave_scratch_bufs.append(Buffer('CPU', scratch_size * wave_size, dtypes.uint8).ensure_allocated())
|
||||
ctypes.memset(wave_scratch_bufs[-1]._buf.va_addr, 0, scratch_size * wave_size)
|
||||
c_bufs = [ctypes.c_uint64(st.sgpr_buf._buf.va_addr), ctypes.c_uint64(st.vgpr_buf._buf.va_addr),
|
||||
ctypes.c_uint64(vmem_buf._buf.va_addr), ctypes.c_uint64(lds_buf._buf.va_addr),
|
||||
ctypes.c_uint64(wave_scratch_bufs[-1]._buf.va_addr if scratch_size else 0),
|
||||
ctypes.c_uint64(scratch_buf._buf.va_addr if scratch_buf else 0),
|
||||
ctypes.c_uint64(st.accvgpr_buf._buf.va_addr)]
|
||||
waves.append((st, c_bufs))
|
||||
|
||||
# Execute wavefronts with barrier synchronization
|
||||
# Each wave runs until it hits s_barrier or s_endpgm. When all waves have stopped, release barrier waves.
|
||||
done = [False] * len(waves)
|
||||
_exec_dumped: set = set()
|
||||
_trace_on = bool(os.getenv("EMU_TRACE_INST")) and total_threads >= int(os.getenv("EMU_TRACE_MIN_THREADS", "1"))
|
||||
if _trace_on: _inst_hist.clear()
|
||||
for total_inst in range(10_000_000):
|
||||
if all(done): break
|
||||
for wi, (st, c_bufs) in enumerate(waves):
|
||||
@@ -2297,23 +2259,9 @@ def run_asm(lib: int, lib_sz: int, gx: int, gy: int, gz: int, lx: int, ly: int,
|
||||
if pc == ENDPGM_PC:
|
||||
done[wi] = True
|
||||
if tracing: sqtt_finish(wi)
|
||||
if os.getenv("EMU_EXEC_DUMP") and wi not in _exec_dumped:
|
||||
_exec_dumped.add(wi)
|
||||
print(f"[exec-dump] gid=({gidx},{gidy},{gidz}) wave{wi} "
|
||||
f"exec_lo={st._read_sgpr(EXEC_LO.offset):08x} exec_hi={st._read_sgpr(EXEC_LO.offset+1):08x}")
|
||||
break
|
||||
fxn, globals_list, is_barrier, inst = _ensure_compiled(pc)
|
||||
if DEBUG >= 5: print(f" exec gid=({gidx},{gidy},{gidz}) w={wi} PC={pc - lib}: {inst!r}", flush=True)
|
||||
if _trace_on:
|
||||
key = (gidx, gidy, gidz, wi)
|
||||
_inst_hist.setdefault(key, []).append((pc - lib, type(inst).__name__, getattr(inst, 'op', None) and inst.op.name))
|
||||
wpc, wwave = os.getenv("EMU_WATCH_PC"), int(os.getenv("EMU_WATCH_WAVE", "0"))
|
||||
if wpc is not None and (int(wpc) < 0 or (pc - lib) == int(wpc)) and wi == wwave and gidx == gidy == gidz == 0:
|
||||
if int(wpc) < 0 and total_inst < 800: print(f"[watch-stream] pc={pc-lib} {inst!r}", flush=True)
|
||||
for rv in os.getenv("EMU_WATCH_VGPR", "").split(","):
|
||||
if rv: print(f"[watch pc={pc-lib}] wave{wi} {rv}:", [st._read_vgpr(int(rv[1:]), l) for l in range(8)], flush=True)
|
||||
for rv in os.getenv("EMU_WATCH_SGPR", "").split(","):
|
||||
if rv: print(f"[watch pc={pc-lib}] wave{wi} {rv}:", [st._read_sgpr(int(rv[1:]))], flush=True)
|
||||
fxn(*[c_bufs[g] for g in globals_list])
|
||||
if tracing:
|
||||
inst_op = inst.op.value if hasattr(inst, 'op') else 0
|
||||
@@ -2321,26 +2269,9 @@ def run_asm(lib: int, lib_sz: int, gx: int, gy: int, gz: int, lx: int, ly: int,
|
||||
if is_barrier: break # s_barrier hit: PC already advanced past it, pause this wave
|
||||
else: raise RuntimeError("exceeded 1M instructions in single wave, likely infinite loop")
|
||||
# All waves have either hit barrier or endpgm — release barrier waves for next round
|
||||
if os.getenv("EMU_DUMP_ROUNDS") and lds_size > 0 and total_inst < int(os.getenv("EMU_DUMP_ROUNDS")):
|
||||
import numpy as _np
|
||||
lds_words = _np.frombuffer((ctypes.c_uint32 * (lds_size//4)).from_address(lds_buf._buf.va_addr), dtype=_np.uint32)
|
||||
nz = _np.argwhere(lds_words != 0)
|
||||
print(f"[emu-dump] round={total_inst} lds nonzero words={len(nz)}",
|
||||
(f"first16={[f'w{w}:0x{lds_words[w]:08x}' for w in nz[:16].flatten()]}" if len(nz) else ""), flush=True)
|
||||
for wi2, (st2, _) in enumerate(waves):
|
||||
v = _np.frombuffer((ctypes.c_uint32 * (256*st2.wave_size)).from_address(st2.vgpr_buf._buf.va_addr), dtype=_np.uint32)
|
||||
if st2.wave_size == 64:
|
||||
av = _np.frombuffer((ctypes.c_uint32 * (256*st2.wave_size)).from_address(st2.accvgpr_buf._buf.va_addr), dtype=_np.uint32)
|
||||
else: av = _np.zeros(1, dtype=_np.uint32)
|
||||
print(f"[emu-dump] wave{wi2} vgpr nonzero={int((v!=0).sum())} accvgpr nonzero={int((av!=0).sum())}", flush=True)
|
||||
else: raise RuntimeError("exceeded 10M total scheduling rounds")
|
||||
tracing = False # only trace the first workgroup
|
||||
|
||||
if _trace_on:
|
||||
import pickle
|
||||
tag = os.environ["EMU_TRACE_INST"]
|
||||
with open(f"/tmp/emu_trace_{tag}.pkl", "wb") as f: pickle.dump(dict(_inst_hist), f)
|
||||
os.environ.pop("EMU_TRACE_INST", None) # only dump for the first matching dispatch
|
||||
# Reset LDS for next workgroup
|
||||
if lds_size > 0: ctypes.memset(lds_buf._buf.va_addr, 0, max(lds_size, 4))
|
||||
|
||||
|
||||
@@ -837,16 +837,16 @@ class Parser:
|
||||
idx = addr
|
||||
if dt in (dtypes.uint64, dtypes.int64, dtypes.float64):
|
||||
val = _u32(0).cast(dtypes.uint64)
|
||||
for i in range(8): val = val | (mindex(idx + _const(adt, i)).load().cast(dtypes.uint64) << _u64(i * 8))
|
||||
for i in range(8): val = val | (mindex(idx + _const(dtypes.int, i)).load().cast(dtypes.uint64) << _u64(i * 8))
|
||||
elif dt in (dtypes.uint8, dtypes.int8):
|
||||
val = mindex(idx).load().cast(dt)
|
||||
elif dt in (dtypes.uint16, dtypes.int16, dtypes.short):
|
||||
lo = mindex(idx).load().cast(dtypes.uint32)
|
||||
hi = mindex(idx + _const(adt, 1)).load().cast(dtypes.uint32)
|
||||
hi = mindex(idx + _const(dtypes.int, 1)).load().cast(dtypes.uint32)
|
||||
val = (lo | (hi << _u32(8))).cast(dt)
|
||||
else:
|
||||
val = _u32(0)
|
||||
for i in range(4): val = val | (mindex(idx + _const(adt, i)).load().cast(dtypes.uint32) << _u32(i * 8))
|
||||
for i in range(4): val = val | (mindex(idx + _const(dtypes.int, i)).load().cast(dtypes.uint32) << _u32(i * 8))
|
||||
else:
|
||||
idx = addr >> _const(addr.dtype, 2)
|
||||
val = mindex(idx)
|
||||
|
||||
@@ -91,7 +91,7 @@ class GPFIFO:
|
||||
args_cnt, vals_cnt = const0[80], const0[81]
|
||||
args_addr = qmd.constant_buffer_addr_lower_0 + (qmd.constant_buffer_addr_upper_0 << 32) + 0x160
|
||||
args = to_mv(args_addr, args_cnt*8).cast('Q')
|
||||
vals = to_mv(args_addr + args_cnt*8, vals_cnt*8).cast('Q')
|
||||
vals = to_mv(args_addr + args_cnt*8, vals_cnt*4).cast('I')
|
||||
cargs = [ctypes.cast(args[i], ctypes.c_void_p) for i in range(args_cnt)] + [ctypes.cast(vals[i], ctypes.c_void_p) for i in range(vals_cnt)]
|
||||
gx, gy, gz = qmd.cta_raster_width, qmd.cta_raster_height, qmd.cta_raster_depth
|
||||
lx, ly, lz = qmd.cta_thread_dimension0, qmd.cta_thread_dimension1, qmd.cta_thread_dimension2
|
||||
|
||||
@@ -2,7 +2,7 @@ import unittest, math, struct, operator
|
||||
from tinygrad import Tensor, Device
|
||||
from tinygrad.dtype import DTYPES_DICT, dtypes, Invalid, truncate, float_to_fp16, float_to_bf16, _to_np_dtype, least_upper_dtype, least_upper_float
|
||||
|
||||
from tinygrad.helpers import getenv, Context
|
||||
from tinygrad.helpers import getenv
|
||||
from hypothesis import given, settings, strategies as strat
|
||||
import numpy as np
|
||||
import torch
|
||||
@@ -58,8 +58,8 @@ class TestHelpers(unittest.TestCase):
|
||||
def test_from_py(self):
|
||||
assert dtypes.from_py(True) == dtypes.bool
|
||||
assert dtypes.from_py(Invalid) == dtypes.bool
|
||||
assert dtypes.from_py(2) == dtypes.weakint
|
||||
assert dtypes.from_py(3.0) == dtypes.weakfloat
|
||||
assert dtypes.from_py(2) == dtypes.default_int
|
||||
assert dtypes.from_py(3.0) == dtypes.default_float
|
||||
assert dtypes.from_py([]) == dtypes.default_float
|
||||
assert dtypes.from_py(()) == dtypes.default_float
|
||||
assert dtypes.from_py([True]) == dtypes.bool
|
||||
@@ -205,8 +205,7 @@ class TestTypePromotion(unittest.TestCase):
|
||||
assert least_upper_dtype(dtypes.uint16, dtypes.int32) == dtypes.int32
|
||||
assert least_upper_dtype(dtypes.int32, dtypes.uint32) == dtypes.int64
|
||||
assert least_upper_dtype(dtypes.uint32, dtypes.int64) == dtypes.int64
|
||||
# uint64 has no common integer supertype with any signed int (JAX JEP), they all defer up to weakfloat
|
||||
for st in dtypes.sints: assert least_upper_dtype(st, dtypes.uint64) == dtypes.weakfloat
|
||||
assert least_upper_dtype(dtypes.int64, dtypes.uint64) == dtypes.uint64
|
||||
assert least_upper_dtype(dtypes.float16, dtypes.float32) == dtypes.float32
|
||||
assert least_upper_dtype(dtypes.float32, dtypes.float64) == dtypes.float64
|
||||
|
||||
@@ -225,9 +224,8 @@ class TestTypePromotion(unittest.TestCase):
|
||||
assert least_upper_dtype(dtypes.fp8e5m2, dtypes.uint64) == dtypes.fp8e5m2
|
||||
|
||||
def test_weakint_promo(self):
|
||||
assert least_upper_dtype(dtypes.weakint, dtypes.weakint) == dtypes.weakint
|
||||
assert least_upper_dtype(dtypes.bool, dtypes.weakint) == dtypes.weakint
|
||||
assert least_upper_dtype(dtypes.weakint, dtypes.int8) == dtypes.int8
|
||||
with self.assertRaises(KeyError): least_upper_dtype(dtypes.weakint, dtypes.weakint)
|
||||
with self.assertRaises(KeyError): least_upper_dtype(dtypes.weakint, dtypes.int8)
|
||||
|
||||
def test_weakfloat_promo(self):
|
||||
# weakfloat is a float, but is not one of dtypes.floats
|
||||
@@ -245,14 +243,19 @@ class TestTypePromotion(unittest.TestCase):
|
||||
assert least_upper_dtype(dtypes.weakfloat, dtypes.float64) == dtypes.float64
|
||||
|
||||
class TestTypeSpec(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.old_default_int, self.old_default_float = dtypes.default_int, dtypes.default_float
|
||||
def tearDown(self):
|
||||
dtypes.default_int, dtypes.default_float = self.old_default_int, self.old_default_float
|
||||
|
||||
def test_set_dtype_default(self):
|
||||
for default_int in [dtypes.int8, dtypes.int16, dtypes.int32, dtypes.int64]:
|
||||
with Context(DEFAULT_INT=default_int):
|
||||
assert dtypes.default_int == default_int
|
||||
dtypes.default_int = default_int
|
||||
assert dtypes.default_int == default_int
|
||||
|
||||
for default_float in [*dtypes.fp8s, dtypes.float16, dtypes.bfloat16, dtypes.float32, dtypes.float64]:
|
||||
with Context(DEFAULT_FLOAT=default_float):
|
||||
assert dtypes.default_float == default_float
|
||||
dtypes.default_float = default_float
|
||||
assert dtypes.default_float == default_float
|
||||
|
||||
@given(strat.sampled_from(core_dtypes), strat.sampled_from([operator.gt, operator.ge, operator.le, operator.lt, operator.eq, operator.ne]))
|
||||
def test_bool_ops(self, dtype, op):
|
||||
@@ -260,7 +263,7 @@ class TestTypeSpec(unittest.TestCase):
|
||||
|
||||
@given(strat.sampled_from(core_dtypes), strat.sampled_from(dtype_ints), strat.sampled_from(dtype_floats))
|
||||
def test_functions_return_index(self, dtype, default_int, default_float):
|
||||
self.enterContext(Context(DEFAULT_INT=default_int, DEFAULT_FLOAT=default_float))
|
||||
dtypes.default_int, dtypes.default_float = default_int, default_float
|
||||
assert Tensor([0, 1], dtype=dtype).argmax().dtype == dtypes.int32
|
||||
assert Tensor([0, 1], dtype=dtype).argmin().dtype == dtypes.int32
|
||||
assert Tensor([0, 1], dtype=dtype).multinomial().dtype == dtypes.int32
|
||||
@@ -280,7 +283,7 @@ class TestTypeSpec(unittest.TestCase):
|
||||
|
||||
@given(strat.sampled_from(dtype_floats), strat.sampled_from(dtype_floats))
|
||||
def test_attention_returns_same_dtype(self, data_dtype, default_float):
|
||||
self.enterContext(Context(DEFAULT_FLOAT=default_float))
|
||||
dtypes.default_float = default_float
|
||||
query = Tensor.rand(32, 8, 128, 64, dtype=data_dtype)
|
||||
key = Tensor.rand(32, 8, 128, 64, dtype=data_dtype)
|
||||
value = Tensor.rand(32, 8, 128, 64, dtype=data_dtype)
|
||||
@@ -291,27 +294,32 @@ class TestTypeSpec(unittest.TestCase):
|
||||
assert query.scaled_dot_product_attention(key, value, attn_mask=mask).dtype == data_dtype
|
||||
|
||||
class TestAutoCastType(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.old_default_int, self.old_default_float = dtypes.default_int, dtypes.default_float
|
||||
def tearDown(self):
|
||||
dtypes.default_int, dtypes.default_float = self.old_default_int, self.old_default_float
|
||||
|
||||
@given(strat.sampled_from(dtype_floats), strat.sampled_from(dtype_floats))
|
||||
def test_least_upper_float_input_is_float(self, input_dtype, default_float):
|
||||
self.enterContext(Context(DEFAULT_FLOAT=default_float))
|
||||
dtypes.default_float = default_float
|
||||
self.assertEqual(least_upper_float(input_dtype), input_dtype)
|
||||
|
||||
@given(strat.sampled_from(dtype_ints), strat.sampled_from(dtype_floats))
|
||||
def test_least_upper_float_input_is_int(self, input_dtype, default_float):
|
||||
self.enterContext(Context(DEFAULT_FLOAT=default_float))
|
||||
dtypes.default_float = default_float
|
||||
self.assertEqual(least_upper_float(input_dtype), default_float)
|
||||
|
||||
@given(strat.sampled_from(core_dtypes))
|
||||
def test_broadcast_scalar(self, dt):
|
||||
assert (Tensor.ones(4, 4, dtype=dt) + 2.3).dtype == (dt if dtypes.is_float(dt) else dtypes.weakfloat)
|
||||
assert (Tensor.ones(4, 4, dtype=dt) + 2).dtype == (dt if dtypes.is_float(dt) or dtypes.is_int(dt) else dtypes.weakint)
|
||||
assert (Tensor.ones(4, 4, dtype=dt) + 2.3).dtype == (dt if dtypes.is_float(dt) else dtypes.default_float)
|
||||
assert (Tensor.ones(4, 4, dtype=dt) + 2).dtype == (dt if dtypes.is_float(dt) or dtypes.is_int(dt) else dtypes.default_int)
|
||||
assert (Tensor.ones(4, 4, dtype=dt) + True).dtype == dt
|
||||
|
||||
@given(strat.sampled_from(core_dtypes))
|
||||
def test_pad_scalar(self, dt):
|
||||
t = Tensor.ones(4, dtype=dt)
|
||||
assert t.pad(((1, 1),), value=2.3).dtype == (dt if dtypes.is_float(dt) else dtypes.weakfloat)
|
||||
assert t.pad(((1, 1),), value=2).dtype == (dt if dtypes.is_float(dt) or dtypes.is_int(dt) else dtypes.weakint)
|
||||
assert t.pad(((1, 1),), value=2.3).dtype == (dt if dtypes.is_float(dt) else dtypes.default_float)
|
||||
assert t.pad(((1, 1),), value=2).dtype == (dt if dtypes.is_float(dt) or dtypes.is_int(dt) else dtypes.default_int)
|
||||
assert t.pad(((1, 1),), value=True).dtype == dt
|
||||
|
||||
@given(strat.sampled_from(core_dtypes))
|
||||
@@ -322,7 +330,7 @@ class TestAutoCastType(unittest.TestCase):
|
||||
|
||||
@given(strat.sampled_from(dtype_floats))
|
||||
def test_int_div_int(self, default_float):
|
||||
self.enterContext(Context(DEFAULT_FLOAT=default_float))
|
||||
dtypes.default_float = default_float
|
||||
self.assertEqual(Tensor([1]).div(Tensor([2])).dtype, default_float)
|
||||
|
||||
def test_sum(self):
|
||||
@@ -410,16 +418,16 @@ class TestAutoCastType(unittest.TestCase):
|
||||
@given(strat.sampled_from(core_dtypes))
|
||||
def test_where_one_scalar(self, dt):
|
||||
t = Tensor(2, dtype=dt)
|
||||
self.check_where_alternate_input_other(t, 3.2, (dt if dtypes.is_float(dt) else dtypes.weakfloat))
|
||||
self.check_where_alternate_input_other(t, 3, (dt if dtypes.is_float(dt) or dtypes.is_int(dt) else dtypes.weakint))
|
||||
self.check_where_alternate_input_other(t, 3.2, (dt if dtypes.is_float(dt) else dtypes.default_float))
|
||||
self.check_where_alternate_input_other(t, 3, (dt if dtypes.is_float(dt) or dtypes.is_int(dt) else dtypes.default_int))
|
||||
self.check_where_alternate_input_other(t, True, dt)
|
||||
|
||||
def test_where_two_scalars(self):
|
||||
self.check_where_alternate_input_other(3.1, 3.2, dtypes.weakfloat)
|
||||
self.check_where_alternate_input_other(3.1, 3, dtypes.weakfloat)
|
||||
self.check_where_alternate_input_other(3.1, True, dtypes.weakfloat)
|
||||
self.check_where_alternate_input_other(3, 2, dtypes.weakint)
|
||||
self.check_where_alternate_input_other(3, True, dtypes.weakint)
|
||||
self.check_where_alternate_input_other(3.1, 3.2, dtypes.default_float)
|
||||
self.check_where_alternate_input_other(3.1, 3, dtypes.default_float)
|
||||
self.check_where_alternate_input_other(3.1, True, dtypes.default_float)
|
||||
self.check_where_alternate_input_other(3, 2, dtypes.default_int)
|
||||
self.check_where_alternate_input_other(3, True, dtypes.default_int)
|
||||
|
||||
def test_where_non_bool_cond_raises(self):
|
||||
with self.assertRaises(RuntimeError): Tensor([1, 0, 2]).where(1, 0)
|
||||
@@ -431,8 +439,8 @@ class TestAutoCastType(unittest.TestCase):
|
||||
|
||||
@given(strat.sampled_from(core_dtypes))
|
||||
def test_maximum_const(self, dt):
|
||||
assert Tensor([1, 2], dtype=dt).maximum(3.1).dtype == (dt if dtypes.is_float(dt) else dtypes.weakfloat)
|
||||
assert Tensor([1, 2], dtype=dt).maximum(3).dtype == (dt if dtypes.is_float(dt) or dtypes.is_int(dt) else dtypes.weakint)
|
||||
assert Tensor([1, 2], dtype=dt).maximum(3.1).dtype == (dt if dtypes.is_float(dt) else dtypes.default_float)
|
||||
assert Tensor([1, 2], dtype=dt).maximum(3).dtype == (dt if dtypes.is_float(dt) or dtypes.is_int(dt) else dtypes.default_int)
|
||||
assert Tensor([1, 2], dtype=dt).maximum(True).dtype == dt
|
||||
|
||||
def test_div(self):
|
||||
@@ -443,7 +451,7 @@ class TestAutoCastType(unittest.TestCase):
|
||||
|
||||
def test_div_const(self):
|
||||
assert (Tensor([1, 2], dtype=dtypes.int32) / 2).dtype == dtypes.default_float
|
||||
assert (Tensor([1, 2], dtype=dtypes.int32) / 2.0).dtype == dtypes.weakfloat
|
||||
assert (Tensor([1, 2], dtype=dtypes.int32) / 2.0).dtype == dtypes.default_float
|
||||
assert (Tensor([1, 2], dtype=dtypes.float16) / 2).dtype == dtypes.float16
|
||||
assert (Tensor([1, 2], dtype=dtypes.float16) / 2.0).dtype == dtypes.float16
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ from tinygrad.codegen import to_program
|
||||
class TestLinearizerFailures(unittest.TestCase):
|
||||
def test_fail_1(self):
|
||||
c0 = UOp.param(0, dtypes.float, (64,))
|
||||
c1 = UOp.range(UOp.const(dtypes.weakint, 2), 1, AxisType.WEAK)
|
||||
c2 = UOp.range(UOp.const(dtypes.weakint, 32), 2, AxisType.WEAK)
|
||||
c1 = UOp.range(UOp.const(dtypes.weakint, 2), 1, AxisType.LOOP)
|
||||
c2 = UOp.range(UOp.const(dtypes.weakint, 32), 2, AxisType.LOOP)
|
||||
c3 = ((c1*UOp.const(dtypes.weakint, 32))+c2)
|
||||
c4 = UOp.param(1, dtypes.float, (163840,))
|
||||
c5 = UOp.range(UOp.const(dtypes.weakint, 2560), 0, AxisType.REDUCE)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import unittest, threading, time, json
|
||||
from unittest.mock import Mock, patch
|
||||
from unittest.mock import Mock
|
||||
|
||||
class TestLLMServer(unittest.TestCase):
|
||||
"""Integration tests using the real OpenAI client."""
|
||||
@@ -66,18 +66,20 @@ class TestLLMServer(unittest.TestCase):
|
||||
self.assertEqual(chunk.model, "test-model")
|
||||
|
||||
def test_stream_with_usage(self):
|
||||
def generate(ids, **kwargs):
|
||||
for token in (300, 301, 999):
|
||||
ids.append(token)
|
||||
yield token
|
||||
with patch.object(self.mock_model, "generate", side_effect=generate):
|
||||
chunks = list(self.client.chat.completions.create(
|
||||
model="test", messages=[{"role": "user", "content": "Hello"}], stream=True, stream_options={"include_usage": True}))
|
||||
stream = self.client.chat.completions.create(
|
||||
model="test",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
stream=True,
|
||||
stream_options={"include_usage": True}
|
||||
)
|
||||
|
||||
chunks = list(stream)
|
||||
last_chunk = chunks[-1]
|
||||
|
||||
self.assertEqual(last_chunk.usage.prompt_tokens, 3)
|
||||
self.assertEqual(last_chunk.usage.completion_tokens, 2)
|
||||
self.assertEqual(last_chunk.usage.total_tokens, 5)
|
||||
self.assertIsNotNone(last_chunk.usage)
|
||||
self.assertIsNotNone(last_chunk.usage.prompt_tokens)
|
||||
self.assertIsNotNone(last_chunk.usage.completion_tokens)
|
||||
self.assertIsNotNone(last_chunk.usage.total_tokens)
|
||||
|
||||
def test_multi_turn_conversation(self):
|
||||
stream = self.client.chat.completions.create(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import gc, unittest
|
||||
from tinygrad import Tensor, UOp, GlobalCounters, dtypes
|
||||
from tinygrad import Tensor, GlobalCounters, dtypes
|
||||
from tinygrad.engine.jit import TinyJit
|
||||
from tinygrad.helpers import Context
|
||||
|
||||
@@ -181,7 +181,7 @@ class TestMultiScalarALU(unittest.TestCase):
|
||||
@functools.cache
|
||||
def _fxn(x_p, device):
|
||||
t = Tensor(x_p, device=device)
|
||||
inner = Tensor(t.uop.src[0]) if t.uop.op is Ops.UNSHARD else t
|
||||
inner = Tensor(t.uop.src[0]) if t.uop.op is Ops.MULTI else t
|
||||
return (inner.sum(),)
|
||||
param = x.as_param(0)
|
||||
fxn = _fxn(param.uop, x.device)
|
||||
@@ -218,10 +218,5 @@ class TestMultiAxis(unittest.TestCase):
|
||||
self.assertEqual(e.uop.axis, 0)
|
||||
self.assertTrue(e.uop.has_buffer_identity())
|
||||
|
||||
def test_symbolic_reshape_shard_axis(self):
|
||||
rows = UOp.variable("rows", 1, 4).bind(3)
|
||||
x = Tensor.empty(4, 2).shard(("NULL:1", "NULL:2"), axis=1)[:rows]
|
||||
self.assertEqual(x.reshape(rows, 1, 2).uop.axis, 2)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -47,8 +47,12 @@ class TestRealWorld(unittest.TestCase):
|
||||
gc.collect()
|
||||
global global_mem_used
|
||||
global_mem_used = GlobalCounters.mem_used
|
||||
self.old_float = dtypes.default_float
|
||||
np.random.seed(2002)
|
||||
|
||||
def tearDown(self):
|
||||
dtypes.default_float = self.old_float
|
||||
|
||||
@slow
|
||||
@unittest.skipUnless(dtypes.float16 in supported_dtypes, "need dtypes.float16")
|
||||
def test_stable_diffusion(self):
|
||||
@@ -77,7 +81,7 @@ class TestRealWorld(unittest.TestCase):
|
||||
|
||||
@unittest.skipUnless(dtypes.float16 in supported_dtypes, "need dtypes.float16")
|
||||
def test_llama(self):
|
||||
self.enterContext(Context(DEFAULT_FLOAT=dtypes.float16))
|
||||
dtypes.default_float = dtypes.float16
|
||||
|
||||
args_tiny = {"dim": 1024, "hidden_dim": 2048, "n_heads": 8, "n_layers": 8, "norm_eps": 1e-05, "vocab_size": 1000}
|
||||
model = LLaMaTransformer(**args_tiny)
|
||||
@@ -89,7 +93,7 @@ class TestRealWorld(unittest.TestCase):
|
||||
|
||||
@unittest.skipUnless(dtypes.float16 in supported_dtypes, "need dtypes.float16")
|
||||
def test_gpt2(self):
|
||||
self.enterContext(Context(DEFAULT_FLOAT=dtypes.float16))
|
||||
dtypes.default_float = dtypes.float16
|
||||
|
||||
args_tiny = {"dim": 1024, "n_heads": 8, "n_layers": 8, "norm_eps": 1e-5, "vocab_size": 1000}
|
||||
model = GPT2Transformer(**args_tiny)
|
||||
@@ -146,7 +150,7 @@ class TestRealWorld(unittest.TestCase):
|
||||
|
||||
@unittest.skipUnless(dtypes.float16 in supported_dtypes, "need dtypes.float16")
|
||||
def test_train_cifar_hyp(self):
|
||||
self.enterContext(Context(DEFAULT_FLOAT=dtypes.float16))
|
||||
dtypes.default_float = dtypes.float16
|
||||
with Context(TRAINING=1):
|
||||
model = SpeedyResNet(Tensor.ones((12,3,2,2)))
|
||||
optimizer = optim.SGD(get_parameters(model), lr=0.01, momentum=hyp['opt']['momentum'], nesterov=True, weight_decay=hyp['opt']['bias_decay'])
|
||||
|
||||
@@ -603,12 +603,14 @@ class TestSchedule(unittest.TestCase):
|
||||
check_schedule(p, 4)
|
||||
|
||||
def test_conv2d(self, allowed=4, dtype=dtypes.float):
|
||||
self.enterContext(Context(DEFAULT_FLOAT=dtype))
|
||||
old_default_float, dtypes.default_float = dtypes.default_float, dtype
|
||||
dtypes.default_float = dtype
|
||||
Tensor.manual_seed(0)
|
||||
BS, CIN = 2, 3
|
||||
img = Tensor.randn(BS, CIN, 64, 64).realize()
|
||||
w = Tensor.uniform(16, CIN, 3, 3).realize()
|
||||
ret = Tensor.conv2d(img, w).relu().mean().backward()
|
||||
dtypes.default_float = old_default_float
|
||||
linear, var_vals = Tensor.linear_with_vars(ret, img.grad, w.grad)
|
||||
cnt = len([call for call in linear.src if call.src[0].op is Ops.SINK])
|
||||
assert cnt == allowed, f"expected {allowed} kernels, got {cnt}"
|
||||
|
||||
@@ -181,7 +181,7 @@ class TestTensorPad(unittest.TestCase):
|
||||
t = Tensor.arange(9).reshape(1, 1, 3, 3)
|
||||
self.assertEqual(t.dtype, dtypes.int)
|
||||
r = t.pad((1, 2, 0, -1), value=-float('inf'))
|
||||
self.assertEqual(r.dtype, dtypes.weakfloat)
|
||||
self.assertEqual(r.dtype, dtypes.float)
|
||||
self.assertEqual(r.shape, (1, 1, 2, 6))
|
||||
|
||||
class TestTensorDeviceMismatch(unittest.TestCase):
|
||||
|
||||
@@ -248,32 +248,6 @@ class TestUOpGraph(unittest.TestCase):
|
||||
uops = to_uops_list([out])
|
||||
self.assertEqual(len(uops), 2) # +1 for SINK
|
||||
|
||||
def test_coalesce_aliased_stores(self):
|
||||
from tinygrad.codegen.late.coalesce import memory_coalescing
|
||||
from tinygrad import Device
|
||||
# two distinct stores to the same INDEX (e.g. a double-buffered LDS slot written in a
|
||||
# prologue and a loop body) can't be merged: merging would drop one write
|
||||
lbuf = UOp.placeholder((64,), dtypes.half, 0, AddrSpace.LOCAL)
|
||||
r = UOp.range(4, 0, AxisType.LOOP)
|
||||
gbuf = UOp.placeholder((64,), dtypes.half, 1, AddrSpace.GLOBAL)
|
||||
s_a = lbuf.index(r*2).store(gbuf.index(r*2).load())
|
||||
s_b = lbuf.index(r*2).store(gbuf.index(r*2+64).load()) # aliases s_a
|
||||
s_c = lbuf.index(r*2+1).store(gbuf.index(r*2+32).load()) # adjacent, unique -> coalesceable with nothing
|
||||
out = memory_coalescing(UOp.sink(s_a, s_b, s_c).end(r), Device["NULL"].renderer)
|
||||
stores = [u for u in out.toposort() if u.op is Ops.STORE]
|
||||
# both aliased stores survive (both datas preserved), nothing merged across them
|
||||
self.assertEqual(len([u for u in stores if u.src[0].src[1] is not None and u.src[0].op is Ops.INDEX]), 3)
|
||||
datas = sorted([u.src[1] for u in stores], key=str)
|
||||
self.assertEqual(len(datas), 3)
|
||||
|
||||
def test_devectorize_derives_lane_dtype(self):
|
||||
from tinygrad.codegen import do_devectorize
|
||||
# an Invalid lane derives bool while the value lane derives float: the lane rebuild must derive, not inherit
|
||||
lhs = UOp.stack(UOp.invalid(), UOp.const(None, 1.0).cast(dtypes.float))
|
||||
out = do_devectorize(lhs * lhs)
|
||||
invalid_lane_mul = next(u for u in out.src[0].toposort() if u.op is Ops.MUL)
|
||||
self.assertIs(invalid_lane_mul.dtype, dtypes.bool)
|
||||
|
||||
@unittest.skip("this test isn't valid uops")
|
||||
def test_noop_vectorize_fold(self):
|
||||
d0 = UOp.param(0, dtypes.float, (1,))
|
||||
@@ -450,8 +424,8 @@ class TestUOpGraph(unittest.TestCase):
|
||||
# mnist indexing with split reduceop
|
||||
# Make sure we are not doign math on the loaded index, which would promote it to long
|
||||
c0 = UOp.param(0, dtypes.uchar, (128000,))
|
||||
c1 = UOp.range(UOp.const(dtypes.weakint, 512), 1, AxisType.WEAK)
|
||||
c2 = UOp.range(UOp.const(dtypes.weakint, 250), 2, AxisType.WEAK)
|
||||
c1 = UOp.range(UOp.const(dtypes.weakint, 512), 1, AxisType.LOOP)
|
||||
c2 = UOp.range(UOp.const(dtypes.weakint, 250), 2, AxisType.LOOP)
|
||||
c3 = UOp.param(1, dtypes.int, (512,))
|
||||
c4 = c3.index(c1)
|
||||
c5 = UOp.range(UOp.const(dtypes.weakint, 240), 0, AxisType.REDUCE)
|
||||
@@ -467,8 +441,8 @@ class TestUOpGraph(unittest.TestCase):
|
||||
def test_load_idx_no_math_on_loaded(self):
|
||||
# test the (x+y)<c pattern where x has loads - we shouldn't do math on loaded indices
|
||||
c0 = UOp.param(0, dtypes.uchar, (128000,))
|
||||
c1 = UOp.range(UOp.const(dtypes.weakint, 512), 1, AxisType.WEAK)
|
||||
c2 = UOp.range(UOp.const(dtypes.weakint, 250), 2, AxisType.WEAK)
|
||||
c1 = UOp.range(UOp.const(dtypes.weakint, 512), 1, AxisType.LOOP)
|
||||
c2 = UOp.range(UOp.const(dtypes.weakint, 250), 2, AxisType.LOOP)
|
||||
c3 = UOp.param(1, dtypes.int, (512,))
|
||||
c4 = c3.index(c1) # c4 is a load
|
||||
c5 = UOp.range(UOp.const(dtypes.weakint, 240), 0, AxisType.REDUCE)
|
||||
|
||||
@@ -5,7 +5,6 @@ import z3
|
||||
from tinygrad.dtype import dtypes, ConstType, DType, Invalid
|
||||
from test.helpers import get_uops
|
||||
from tinygrad.uop.ops import UOp, Ops, graph_rewrite, sym_infer
|
||||
from tinygrad.uop.spec import spec_shared, type_verify
|
||||
from tinygrad.uop.symbolic import sym, commutative, pm_simplify_valid, pm_move_where_on_load
|
||||
from tinygrad.uop.validate import uops_to_z3
|
||||
|
||||
@@ -347,12 +346,6 @@ class TestSymbolic(unittest.TestCase):
|
||||
def test_mul_lt(self):
|
||||
self.helper_test_variable(Variable("a", 0, 5)*4 < 13, 0, 1, "(a<4)")
|
||||
self.helper_test_variable(Variable("a", 0, 5)*4 < 16, 0, 1, "(a<4)")
|
||||
self.helper_test_variable(Variable("a", -5, 5)*4 < -13, 0, 1, "(a<-3)")
|
||||
self.helper_test_variable(Variable("a", -5, 5)*-4 < 13, 0, 1, "((a*-1)<4)")
|
||||
c0, c1 = 2, 2**54+1
|
||||
self.helper_test_variable(Variable("a", 0, c1)*c0 < c1, 0, 1, f"(a<{2**53+1})")
|
||||
c0, c1 = -2, -(2**54-1)
|
||||
self.helper_test_variable(Variable("a", 0, -c1)*c0 < c1, 0, 1, f"((a*-1)<{-(2**53-1)})")
|
||||
self.helper_test_variable(Variable("a", 0, 5)*(-2) < 0, 0, 1, "((a*-1)<0)")
|
||||
self.helper_test_variable(Variable("a", 0, 5)*4 >= 12, 0, 1, "((a<3)!=True)")
|
||||
self.helper_test_variable(Variable("a", 0, 5)*4 >= 13, 0, 1, "((a<4)!=True)")
|
||||
@@ -993,7 +986,7 @@ class TestSymbolic(unittest.TestCase):
|
||||
self.helper_test_variable(cond.ne(False), 0, 1, "(x<2)")
|
||||
|
||||
def test_bitcast_chain(self):
|
||||
a = UOp.variable("a", 0, 3, dtype=dtypes.int32)
|
||||
a = Variable("a", 0, 3)
|
||||
self.assertIs(graph_rewrite(a.bitcast(dtypes.float32).bitcast(a.dtype), sym), a)
|
||||
|
||||
def test_negation_in_where(self):
|
||||
@@ -1337,8 +1330,7 @@ class TestInvalidIndex(unittest.TestCase):
|
||||
def test_invalid_times_0(self):
|
||||
ridx = Variable("ridx", 0, 10)
|
||||
idx = (ridx<5).where(ridx, UOp.invalid())*0
|
||||
self.assertIs(idx.simplify(), (ridx<5).where(UOp.const(dtypes.weakint, 0), UOp.invalid()),
|
||||
"multiplying an index by 0 should preserve the invalid")
|
||||
self.assertIs(idx.simplify(), (ridx<5).where(0, UOp.invalid()), "multiplying an index by 0 should preserve the invalid")
|
||||
|
||||
def test_alu_moves_inside_invalid(self):
|
||||
ridx = Variable("ridx", 0, 10)
|
||||
@@ -1392,7 +1384,11 @@ class TestMoveWhereOnLoad(unittest.TestCase):
|
||||
idx = buf.index(a.valid(valid))
|
||||
expr = cond.where(idx, idx.const_like(0))
|
||||
out = graph_rewrite(expr, pm_move_where_on_load)
|
||||
type_verify(out, spec_shared) # Invalid matches any dtype
|
||||
# any WHERE in the rewritten graph must have matched-dtype branches
|
||||
for u in out.toposort():
|
||||
if u.op is Ops.WHERE:
|
||||
self.assertEqual(u.dtype, u.src[1].dtype, f"WHERE branch 1 dtype mismatch: {u}")
|
||||
self.assertEqual(u.dtype, u.src[2].dtype, f"WHERE branch 2 dtype mismatch: {u}")
|
||||
|
||||
class TestSymbolicRealWorld(unittest.TestCase):
|
||||
def test_resnet_half(self):
|
||||
|
||||
@@ -168,7 +168,8 @@ class TestVminVmaxProperties(unittest.TestCase):
|
||||
|
||||
def test_vmin_vmax_invalid_vconst(self):
|
||||
x = UOp.const(dtypes.weakint, (0, 4, Invalid, Invalid))
|
||||
self.assertEqual((x.vmin, x.vmax), (0, 4))
|
||||
self.assertLess(x.vmin, 0)
|
||||
self.assertGreater(x.vmax, 4)
|
||||
|
||||
class TestVminVmaxDivMod(unittest.TestCase):
|
||||
def test_vmin_vmax_division_positive(self):
|
||||
|
||||
+3
-29
@@ -1,5 +1,5 @@
|
||||
# uops tests that pass on NULL backend (no copyout needed)
|
||||
import math, unittest
|
||||
import unittest
|
||||
import numpy as np
|
||||
from tinygrad.tensor import Tensor
|
||||
from tinygrad.helpers import Timing, Context, cdiv
|
||||
@@ -7,13 +7,14 @@ from tinygrad.dtype import dtypes, AddrSpace, ConstFloat, Invalid # noqa: F401
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.uop.ops import Ops, ParamArg, UOp, UPat, dtype_from_uop, exec_alu, graph_rewrite, pm_lower_index_dtype # noqa: F401 # ParamArg used by eval(str(uop)) roundtrip tests
|
||||
from tinygrad.uop.spec import spec_program, spec_shared, type_verify
|
||||
from tinygrad.uop.symbolic import sym, pm_remove_invalid
|
||||
from tinygrad.uop.symbolic import sym
|
||||
from test.helpers import eval_uop, to_uops_list
|
||||
|
||||
class TestDTypeFromUOp(unittest.TestCase):
|
||||
def test_broadcastable_promotion(self):
|
||||
self.assertEqual(dtype_from_uop(Ops.ADD, (UOp.const(dtypes.float32, 1.0), UOp.const(dtypes.float16, 1.0)), None), dtypes.float32)
|
||||
self.assertEqual(dtype_from_uop(Ops.MUL, (UOp.const(dtypes.int8, 1), UOp.const(dtypes.int32, 1)), None), dtypes.int32)
|
||||
with self.assertRaises(KeyError): dtype_from_uop(Ops.ADD, (UOp.const(dtypes.weakint, 1), UOp.const(dtypes.int8, 1)), None)
|
||||
|
||||
def test_same_dtype_fast_path(self):
|
||||
src = (UOp.const(dtypes.weakint, 1), UOp.const(dtypes.weakint, 2))
|
||||
@@ -44,28 +45,6 @@ class TestDTypeFromUOp(unittest.TestCase):
|
||||
with self.assertRaises(RuntimeError): type_verify(UOp.const(weak, value).sink(), spec_program)
|
||||
type_verify(UOp.const(concrete, value).sink(), spec_program)
|
||||
|
||||
def test_invalid_dtype_and_consumers(self):
|
||||
invalid = UOp.invalid()
|
||||
self.assertIs(invalid.dtype, dtypes.bool)
|
||||
self.assertIs(UOp.const(dtypes.float32, Invalid), invalid)
|
||||
self.assertIs((moved:=invalid.reshape((1,))).cast(dtypes.float32), moved)
|
||||
scratch = Tensor.invalids(4, dtype=dtypes.float32)
|
||||
self.assertEqual((scratch.dtype, next(u.dtype for u in scratch.uop.toposort() if u.op is Ops.BUFFER), next(u.dtype for u in scratch.uop.toposort()
|
||||
if u.arg is Invalid)), (dtypes.float32, dtypes.float32, dtypes.bool))
|
||||
invalid, value = UOp.invalid(), UOp.const(dtypes.float32, 1)
|
||||
for u in (UOp(Ops.STACK, dtypes.float32, src=(value, invalid)), UOp(Ops.ADD, dtypes.float32, src=(value, invalid)),
|
||||
UOp.const(dtypes.bool, True).where(value, invalid), UOp(Ops.CMPLT, src=(invalid, value)), UOp(Ops.CMPLT, src=(value, invalid)),
|
||||
UOp.param(0, dtypes.float32, (4,)).index(invalid)): type_verify(u, spec_shared)
|
||||
gate, value = UOp.param(0, dtypes.bool, ()), UOp.param(1, dtypes.float, ())
|
||||
self.assertIs((out:=graph_rewrite(gate.where(value, UOp.invalid()), pm_remove_invalid)).src[2], UOp.const(dtypes.float, 0))
|
||||
type_verify(out.sink(), spec_program)
|
||||
|
||||
def test_remove_invalid_stack_lanes(self):
|
||||
stack = UOp(Ops.STACK, dtypes.half, (UOp.const(dtypes.half, 1), UOp.invalid()))
|
||||
out = graph_rewrite(stack, pm_remove_invalid)
|
||||
self.assertEqual(out.src, (UOp.const(dtypes.half, 1), UOp.const(dtypes.half, 0)))
|
||||
type_verify(out.sink(), spec_program)
|
||||
|
||||
class TestLowerIndexDtype(unittest.TestCase):
|
||||
def test_gated_shrink_lowers_to_selected_width(self):
|
||||
# coalesce builds gated SHRINKs for masked vectorized loads; lowering must resolve them at the
|
||||
@@ -115,11 +94,6 @@ class TestExecALU(unittest.TestCase):
|
||||
def test_sqrt(self):
|
||||
self.assertEqual(exec_alu(Ops.SQRT, dtypes.float, (0.0,)), 0.0)
|
||||
|
||||
def test_trunc_nonfinite(self):
|
||||
self.assertEqual(exec_alu(Ops.TRUNC, dtypes.float, (math.inf,)), math.inf)
|
||||
self.assertEqual(exec_alu(Ops.TRUNC, dtypes.float, (-math.inf,)), -math.inf)
|
||||
self.assertTrue(math.isnan(exec_alu(Ops.TRUNC, dtypes.float, (math.nan,))))
|
||||
|
||||
def test_invalid_poison(self):
|
||||
# Invalid poisons any binary op regardless of result dtype: a comparison must not fold to a boolean
|
||||
self.assertIs(exec_alu(Ops.CMPLT, dtypes.bool, (Invalid, 1)), Invalid)
|
||||
|
||||
@@ -270,7 +270,7 @@ class TestKernelOpts(unittest.TestCase):
|
||||
def test_padto_where(self):
|
||||
Tensor.manual_seed(0)
|
||||
N = 17
|
||||
a = (Tensor.randn(N, N).realize().max(axis=0, keepdim=True) > 1).where(1, 0).int()
|
||||
a = (Tensor.randn(N, N).realize().max(axis=0, keepdim=True) > 1).where(1, 0)
|
||||
helper_linearizer_opt(a.max(0), [
|
||||
[Opt(OptOps.PADTO, 0, 32)],
|
||||
[Opt(OptOps.PADTO, 0, 32), Opt(OptOps.UPCAST, 0, 8),],
|
||||
@@ -280,8 +280,8 @@ class TestKernelOpts(unittest.TestCase):
|
||||
Tensor.manual_seed(0)
|
||||
N = 17
|
||||
r = Tensor.randn(N, N).realize().max(axis=0, keepdim=True) > 1
|
||||
a0 = r.where(1, 0).int()
|
||||
a1 = r.where(2, 0).int()
|
||||
a0 = r.where(1, 0)
|
||||
a1 = r.where(2, 0)
|
||||
helper_linearizer_opt([a0.max(0), a1.max(0)], [
|
||||
[Opt(OptOps.PADTO, 0, 32)],
|
||||
[Opt(OptOps.PADTO, 0, 32), Opt(OptOps.UPCAST, 0, 8),],
|
||||
|
||||
@@ -26,14 +26,8 @@ def run_program(prg:UOp, bufs:list[Buffer]):
|
||||
for u,b in zip(buf_uops, bufs): buffers[u] = b
|
||||
run_linear(UOp(Ops.LINEAR, src=(prg.call(*buf_uops),)))
|
||||
|
||||
def _skip_unsupported_tc_dtypes(dtype_in:DType, dtype_out:DType):
|
||||
supported_dtypes = Device[Device.DEFAULT].renderer.supported_dtypes()
|
||||
if unsupported := [f"{name}={dtype}" for name,dtype in (("dtype_in", dtype_in), ("dtype_out", dtype_out)) if dtype not in supported_dtypes]:
|
||||
raise unittest.SkipTest(f"tensor core requires unsupported renderer dtype: {', '.join(unsupported)}")
|
||||
|
||||
def helper_tc_ensure_uops_and_opts_count(N: int, M:int, K:int, dtype_in:DType, dtype_out:DType, axis:int=0, tc_select:int=-1, tc_opt:int=0,
|
||||
ensure_triggered:bool=True):
|
||||
_skip_unsupported_tc_dtypes(dtype_in, dtype_out)
|
||||
a, b = _tc_rand(M, K, dtype=dtype_in), _tc_rand(K, N, dtype=dtype_in)
|
||||
r = a.matmul(b, dtype=dtype_out)
|
||||
sched = r.schedule_linear()
|
||||
@@ -53,7 +47,6 @@ def helper_tc_ensure_uops_and_opts_count(N: int, M:int, K:int, dtype_in:DType, d
|
||||
except KernelOptError: pass
|
||||
|
||||
def helper_tc_allclose(N:int, M:int, K:int, dtype_in:DType, dtype_out:DType, axis:int=0, tc_select:int=-1, tc_opt:int=0, use_tensor_cores:int=1):
|
||||
_skip_unsupported_tc_dtypes(dtype_in, dtype_out)
|
||||
a, b = _tc_rand(M, K, dtype=dtype_in), _tc_rand(K, N, dtype=dtype_in)
|
||||
np_a, np_b = a.numpy(), b.numpy()
|
||||
r = a.matmul(b, dtype=dtype_out)
|
||||
|
||||
@@ -1066,13 +1066,5 @@ class TestAfterCachePatterns(unittest.TestCase):
|
||||
np.testing.assert_array_equal(head.numpy(), [3])
|
||||
np.testing.assert_array_equal(full.numpy(), [1, 2])
|
||||
|
||||
class TestBatchNormRunningStats(unittest.TestCase):
|
||||
@unittest.expectedFailure # TODO: nothing reads the stat update so it is never scheduled, and the chain grows every step
|
||||
def test_running_stats_are_realized(self):
|
||||
from tinygrad import nn
|
||||
bn, x = nn.BatchNorm(4), Tensor.randn(2, 4, 3, 3).contiguous().realize()
|
||||
with Context(TRAINING=1): bn(x).realize()
|
||||
self.assertTrue(bn.running_mean.uop.base.is_realized)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -47,7 +47,7 @@ class TestGatedDeltaNetBlock(unittest.TestCase):
|
||||
def _make_config(self, **kwargs):
|
||||
return TransformerConfig(**({"num_blocks":1, "dim":4, "hidden_dim":8, "n_heads":1, "n_kv_heads":1,
|
||||
"norm_eps":1e-5, "vocab_size":32, "head_dim":4, "rope_theta":10000.0,
|
||||
"rope_dim":4, "v_head_dim":4, "max_context":4, "ssm_layers":(True,),
|
||||
"rope_dim":4, "v_head_dim":4, "max_context":4, "full_attention_interval":2,
|
||||
"ssm":SSMConfig(conv_kernel=2, state_size=2, group_count=1, time_step_rank=1, inner_size=2)} | kwargs))
|
||||
|
||||
def _make_block(self, config:TransformerConfig) -> GatedDeltaNetBlock:
|
||||
@@ -176,20 +176,6 @@ class TestGatedDeltaNetBlock(unittest.TestCase):
|
||||
np.testing.assert_allclose(recurrent_state, expected_recurrent[step], rtol=1e-3, atol=1e-3,
|
||||
err_msg=f"GatedDeltaNet reset recurrent cache mismatch at step {step}")
|
||||
|
||||
def test_kda_channel_decay(self):
|
||||
config = self._make_config(n_heads=2, ssm=SSMConfig(conv_kernel=2, state_size=2, group_count=2, time_step_rank=2, inner_size=4, kda=True))
|
||||
block, x = GatedDeltaNetBlock(config, config.ssm), Tensor([[[1., 2., 0., 0.]]])
|
||||
# f_b(f_a(x)) = [1, 2, 3, 4]
|
||||
block.ssm_f_a.weight = Tensor([[1., 0., 0., 0.], [0., 1., 0., 0.]])
|
||||
block.ssm_f_b.weight = Tensor([[1., 0.], [0., 1.], [1., 1.], [2., 1.]])
|
||||
block._init_state(x)
|
||||
initial_state = Tensor.arange(8, dtype=dtypes.float32).reshape(1, 2, 2, 2)
|
||||
block.recurrent_state.assign(initial_state).realize()
|
||||
block.ssm_a = Tensor([[-1.], [-1.]])
|
||||
block._attention(x, 0).realize()
|
||||
alpha = np.exp(-self._softplus_np(np.arange(1, 5)).reshape(1, 2, 1, 2))
|
||||
np.testing.assert_allclose(block.recurrent_state.numpy(), initial_state.numpy() * alpha, rtol=1e-5, atol=1e-5)
|
||||
|
||||
class TestPairwiseTopk(unittest.TestCase):
|
||||
def test_basic_topk(self):
|
||||
x = Tensor([[[1.0, 3.0, 2.0, 5.0, 4.0]]])
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import unittest, math, subprocess
|
||||
from tinygrad.tensor import Tensor
|
||||
from tinygrad.dtype import dtypes, DType, DTYPES_DICT, strong_dtype
|
||||
from tinygrad.dtype import dtypes, DType, DTYPES_DICT
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.helpers import getenv, DEBUG, EMULATED_DTYPES, Context
|
||||
from tinygrad.helpers import getenv, DEBUG, EMULATED_DTYPES
|
||||
from test.helpers import slow
|
||||
from hypothesis import given, settings, strategies as strat
|
||||
import numpy as np
|
||||
@@ -25,8 +25,6 @@ def _assert_eq(tensor:Tensor, target_dtype:DType, target, tol_target_dtype:float
|
||||
if DEBUG >= 2: print(tensor.numpy())
|
||||
try:
|
||||
assert tensor.dtype == target_dtype
|
||||
# weak values read back at their default.
|
||||
target_dtype = strong_dtype(target_dtype)
|
||||
# denormals are zero
|
||||
if target_dtype in dtypes.floats and (target_dtype not in supported_dtypes or target_dtype in EMULATED_DTYPES.tolist(dtypes)):
|
||||
fe, fm = dtypes.finfo(target_dtype)
|
||||
@@ -39,13 +37,10 @@ def _assert_eq(tensor:Tensor, target_dtype:DType, target, tol_target_dtype:float
|
||||
raise AssertionError(f"\ntensor {tensor.numpy()} dtype {tensor.dtype} does not match target {target} with dtype {target_dtype}") from e
|
||||
|
||||
class TestTypeSpec(unittest.TestCase):
|
||||
def test_default_dtype_context(self):
|
||||
default_float, default_int = dtypes.default_float, dtypes.default_int
|
||||
with Context(DEFAULT_FLOAT=dtypes.half, DEFAULT_INT=dtypes.int16):
|
||||
assert dtypes.default_float is dtypes.half
|
||||
assert dtypes.default_int is dtypes.int16
|
||||
assert dtypes.default_float is default_float
|
||||
assert dtypes.default_int is default_int
|
||||
def setUp(self):
|
||||
self.old_default_int, self.old_default_float = dtypes.default_int, dtypes.default_float
|
||||
def tearDown(self):
|
||||
dtypes.default_int, dtypes.default_float = self.old_default_int, self.old_default_float
|
||||
|
||||
@unittest.skip("this test is slow and spawning whole pythons")
|
||||
def test_env_set_default_float(self):
|
||||
@@ -84,15 +79,13 @@ class TestTypeSpec(unittest.TestCase):
|
||||
|
||||
@given(strat.sampled_from(dtype_ints), strat.sampled_from(dtype_floats))
|
||||
def test_creation(self, default_int, default_float):
|
||||
self.enterContext(Context(DEFAULT_INT=default_int, DEFAULT_FLOAT=default_float))
|
||||
dtypes.default_int, dtypes.default_float = default_int, default_float
|
||||
_assert_eq(Tensor(True), dtypes.bool, True)
|
||||
_assert_eq(Tensor(None), dtypes.weakfloat, [])
|
||||
_assert_eq(Tensor(2), dtypes.weakint, 2)
|
||||
_assert_eq(Tensor(2.34), dtypes.weakfloat, 2.34)
|
||||
_assert_eq(Tensor(None), dtypes.default_float, [])
|
||||
_assert_eq(Tensor(2), dtypes.default_int, 2)
|
||||
_assert_eq(Tensor(2.34), dtypes.default_float, 2.34)
|
||||
_assert_eq(Tensor([]), dtypes.default_float, [])
|
||||
_assert_eq(Tensor([1]), dtypes.default_int, [1])
|
||||
# list elements are python scalars; a numpy scalar in a list has no inferred dtype (use np.array or state a dtype)
|
||||
with self.assertRaises(RuntimeError): Tensor([np.int32(1)])
|
||||
_assert_eq(Tensor([1.1]), dtypes.default_float, [1.1])
|
||||
|
||||
_assert_eq(Tensor.eye(0), dtypes.default_float, np.eye(0))
|
||||
@@ -103,7 +96,7 @@ class TestTypeSpec(unittest.TestCase):
|
||||
|
||||
@given(strat.sampled_from(dtype_ints), strat.sampled_from(dtype_floats))
|
||||
def test_full(self, default_int, default_float):
|
||||
self.enterContext(Context(DEFAULT_INT=default_int, DEFAULT_FLOAT=default_float))
|
||||
dtypes.default_int, dtypes.default_float = default_int, default_float
|
||||
|
||||
_assert_eq(Tensor.zeros((2, 3)), dtypes.default_float, np.zeros((2, 3)))
|
||||
_assert_eq(Tensor.zeros((2, 3), dtype=dtypes.int64), dtypes.int64, np.zeros((2, 3)))
|
||||
@@ -126,7 +119,7 @@ class TestTypeSpec(unittest.TestCase):
|
||||
|
||||
@given(strat.sampled_from(dtype_ints), strat.sampled_from(dtype_floats))
|
||||
def test_reduce_0d_default(self, default_int, default_float):
|
||||
self.enterContext(Context(DEFAULT_INT=default_int, DEFAULT_FLOAT=default_float))
|
||||
dtypes.default_int, dtypes.default_float = default_int, default_float
|
||||
_assert_eq(Tensor.ones((2,3,0)).sum(2), dtypes.default_float, np.zeros((2, 3)))
|
||||
# TODO: what should this one be?
|
||||
# _assert_eq(Tensor.ones((2,3,0), dtype=dtypes.default_int).sum(2), dtypes.default_int, np.zeros((2, 3)))
|
||||
@@ -134,7 +127,7 @@ class TestTypeSpec(unittest.TestCase):
|
||||
|
||||
@given(strat.sampled_from(dtype_ints), strat.sampled_from(dtype_floats))
|
||||
def test_arange(self, default_int, default_float):
|
||||
self.enterContext(Context(DEFAULT_INT=default_int, DEFAULT_FLOAT=default_float))
|
||||
dtypes.default_int, dtypes.default_float = default_int, default_float
|
||||
|
||||
_assert_eq(Tensor.arange(5), dtypes.default_int, np.arange(5))
|
||||
_assert_eq(Tensor.arange(120), dtypes.default_int, np.arange(120))
|
||||
@@ -151,6 +144,11 @@ class TestTypeSpec(unittest.TestCase):
|
||||
_assert_eq(Tensor.arange(5.0, 3.0), dtypes.default_float, np.arange(5.0, 3.0))
|
||||
|
||||
class TestAutoCastType(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.old_default_int, self.old_default_float = dtypes.default_int, dtypes.default_float
|
||||
def tearDown(self):
|
||||
dtypes.default_int, dtypes.default_float = self.old_default_int, self.old_default_float
|
||||
|
||||
def test_int_sqrt(self):
|
||||
_assert_eq(Tensor([1, 4, 9, 16]).sqrt(), dtypes.default_float, [1, 2, 3, 4])
|
||||
|
||||
@@ -190,18 +188,22 @@ class TestAutoCastType(unittest.TestCase):
|
||||
np.testing.assert_allclose(t.prod(dtype=dtypes.float32).numpy(), 20000)
|
||||
|
||||
def test_gradient_dtype(self):
|
||||
old_default_float = dtypes.default_float
|
||||
|
||||
for default_dtype in dtypes.floats:
|
||||
if default_dtype not in supported_dtypes: continue
|
||||
with Context(DEFAULT_FLOAT=default_dtype):
|
||||
for dtype in dtypes.floats:
|
||||
if dtype not in supported_dtypes: continue
|
||||
if DEBUG >= 2:
|
||||
print(f"testing {default_dtype=}, {dtype=}")
|
||||
a = Tensor([1, 2, 3], dtype=dtype)
|
||||
b = (a * 5).sum()
|
||||
b.backward() # if there is dtype mismatch, lazy should assert
|
||||
assert a.grad.dtype == a.dtype
|
||||
np.testing.assert_allclose(a.grad.numpy(), [5, 5, 5])
|
||||
dtypes.default_float = default_dtype
|
||||
for dtype in dtypes.floats:
|
||||
if dtype not in supported_dtypes: continue
|
||||
if DEBUG >= 2:
|
||||
print(f"testing {default_dtype=}, {dtype=}")
|
||||
a = Tensor([1, 2, 3], dtype=dtype)
|
||||
b = (a * 5).sum()
|
||||
b.backward() # if there is dtype mismatch, lazy should assert
|
||||
assert a.grad.dtype == a.dtype
|
||||
np.testing.assert_allclose(a.grad.numpy(), [5, 5, 5])
|
||||
|
||||
dtypes.default_float = old_default_float
|
||||
|
||||
@unittest.skipIf(Device.DEFAULT == "PYTHON", "very slow")
|
||||
@slow
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import tempfile, unittest, math
|
||||
import tempfile, unittest
|
||||
|
||||
from tinygrad import Tensor, dtypes, TinyJit
|
||||
from tinygrad import Tensor, dtypes
|
||||
from tinygrad.helpers import Context
|
||||
from tinygrad.dtype import least_upper_float
|
||||
from tinygrad.uop.ops import UOp, Ops, dtype_from_uop
|
||||
from tinygrad.uop.ops import UOp, Ops
|
||||
from tinygrad.uop.spec import spec_shared, type_verify
|
||||
from tinygrad.engine.jit import JitError
|
||||
|
||||
|
||||
class TestWeakPromotion(unittest.TestCase):
|
||||
@@ -14,11 +12,10 @@ class TestWeakPromotion(unittest.TestCase):
|
||||
with self.assertRaises(ValueError): Tensor.const(dtypes.weakfloat, 1.0).rand_like()
|
||||
with self.assertRaises(ValueError): Tensor.const(dtypes.weakfloat, 1.0).randn_like()
|
||||
|
||||
def test_reduce_strips_weakness(self):
|
||||
for weak, value, strong in ((dtypes.weakint, 1, dtypes.default_int), (dtypes.weakfloat, 1.0, dtypes.default_float)):
|
||||
t = Tensor.const(weak, value).expand(3)
|
||||
for out in (t.sum(), t.max(), t.prod(), t.cumsum(0), t.cummax(0)[0]): self.assertEqual(out.dtype, strong)
|
||||
self.assertEqual((Tensor.const(dtypes.weakfloat, 1.0).expand(3).sum() + Tensor([1], dtype=dtypes.float16)).dtype, dtypes.float32)
|
||||
def test_sum_stays_weak(self):
|
||||
for weak, value in ((dtypes.weakfloat, 1.0),):
|
||||
self.assertEqual(Tensor.const(weak, value).expand(3).sum().dtype, weak)
|
||||
self.assertEqual((Tensor.const(dtypes.weakfloat, 1.0).expand(3).sum() + Tensor([1], dtype=dtypes.float16)).dtype, dtypes.float16)
|
||||
|
||||
def test_materialize_at_default_dtype(self):
|
||||
for weak, value, strong in ((dtypes.weakfloat, 0.5, dtypes.default_float),):
|
||||
@@ -26,22 +23,7 @@ class TestWeakPromotion(unittest.TestCase):
|
||||
self.assertEqual(t.dtype, weak)
|
||||
self.assertEqual(t.data().itemsize, strong.itemsize)
|
||||
self.assertEqual(t.numpy().dtype.itemsize, strong.itemsize)
|
||||
# materializing commits at the kind default; contiguous has no layout to fix so it stays weak
|
||||
self.assertEqual((c := t.clone("CPU")).dtype, strong)
|
||||
self.assertEqual(c.item(), value)
|
||||
self.assertEqual(t.contiguous().dtype, weak)
|
||||
|
||||
def test_assign_into_weak_commits(self):
|
||||
t = Tensor.const(dtypes.weakfloat, 0.5)
|
||||
t.assign(Tensor(1.0, dtype=dtypes.default_float))
|
||||
self.assertEqual((t.dtype, t.item()), (dtypes.default_float, 1.0))
|
||||
|
||||
def test_float_unary_on_weakint_stays_weak(self):
|
||||
self.assertIs(least_upper_float(dtypes.weakint), dtypes.weakfloat)
|
||||
|
||||
def test_copysign_meets_operands(self):
|
||||
r = Tensor([2], dtype=dtypes.uint8, device="CPU").copysign(Tensor([1], dtype=dtypes.uint32, device="CPU"))
|
||||
self.assertEqual((r.dtype, r.tolist()), (dtypes.uint32, [2]))
|
||||
with self.assertRaises(RuntimeError): t.clone("CPU")
|
||||
|
||||
def test_uop_scalar_const_unchanged(self):
|
||||
for dtype, value in ((dtypes.weakint, 1), (dtypes.int32, 1), (dtypes.float32, 0.5)):
|
||||
@@ -68,10 +50,7 @@ class TestWeakPromotion(unittest.TestCase):
|
||||
self.assertEqual((t_f32 + t_f16).dtype, dtypes.float32)
|
||||
self.assertEqual(Tensor([2], dtype=dtypes.uint8).pad(((1, 1),), value=1).dtype, dtypes.uint8)
|
||||
|
||||
def test_concrete_pair_promotes_weak(self):
|
||||
out = Tensor([-1], dtype=dtypes.int64, device="CPU") + Tensor([3], dtype=dtypes.uint64, device="CPU") + Tensor(0.5)
|
||||
self.assertEqual((out.dtype, out.tolist()), (dtypes.weakfloat, [2.5]))
|
||||
|
||||
@unittest.expectedFailure # TODO: dot of a weak const tensor defers to the other operand once python scalars are weak consts
|
||||
def test_dot_defers_weak(self):
|
||||
weak = Tensor([True, False]).where(Tensor(1), 2)
|
||||
self.assertEqual(weak.dot(Tensor([1, 1], dtype=dtypes.int8)).dtype, dtypes.int8)
|
||||
@@ -79,18 +58,14 @@ class TestWeakPromotion(unittest.TestCase):
|
||||
def test_weak_int_binop(self):
|
||||
v = UOp.variable("i", 0, 10, dtypes.weakint)
|
||||
self.assertEqual((v << 1).dtype, dtypes.weakint)
|
||||
self.assertEqual(dtype_from_uop(Ops.SHL, (UOp.const(dtypes.int8, 1), UOp.const(dtypes.uint32, 1)), None), dtypes.int8)
|
||||
self.assertEqual(UOp.const(dtypes.weakint, 1).alu(Ops.SHL, UOp.const(dtypes.uint, 1)).dtype, dtypes.weakint)
|
||||
self.assertEqual((v & 3).dtype, dtypes.weakint)
|
||||
with self.assertRaises(RuntimeError): Tensor.const(dtypes.weakfloat, 1.0) << Tensor.const(dtypes.weakfloat, 1.0)
|
||||
with self.assertRaises(RuntimeError): UOp.const(dtypes.int32, 1).alu(Ops.SHL, UOp.const(dtypes.float64, 1))
|
||||
for op in (Ops.SHL, Ops.SHR):
|
||||
with self.assertRaises(RuntimeError):
|
||||
UOp.const(dtypes.float32, 1).alu(op, UOp.const(dtypes.int32, 1))
|
||||
# float bitwise builds, the spec rejects it
|
||||
# float bitwise/shift builds, the spec rejects it
|
||||
with Context(SPEC=1):
|
||||
f32, wf = UOp.const(dtypes.float32, 1.0), UOp.const(dtypes.weakfloat, 1.0)
|
||||
for bad in (f32.alu(Ops.AND, f32), UOp(Ops.AND, dtypes.float32, (f32, f32)), UOp(Ops.AND, dtypes.int32, (wf, wf))):
|
||||
for bad in (f32.alu(Ops.AND, f32), f32.alu(Ops.SHL, UOp.const(dtypes.int32, 1)),
|
||||
UOp(Ops.AND, dtypes.float32, (f32, f32)), UOp(Ops.AND, dtypes.int32, (wf, wf))):
|
||||
with self.assertRaises(RuntimeError): type_verify([bad], spec_shared)
|
||||
|
||||
def test_integer_values(self):
|
||||
@@ -106,6 +81,7 @@ class TestWeakPromotion(unittest.TestCase):
|
||||
x32 = Tensor.full((1,), 0.0, dtype=dtypes.float32, device="CPU")
|
||||
self.assertEqual((x32 + value).item(), 1.0)
|
||||
|
||||
@unittest.expectedFailure # TODO: exp/cos/sigmoid of a weak const stay weak instead of casting to a concrete float
|
||||
def test_weak_transcendentals(self):
|
||||
t_f16 = Tensor([1], dtype=dtypes.float16)
|
||||
for out in (Tensor(2).exp(), Tensor(2).cos(), Tensor(2).sigmoid()):
|
||||
@@ -117,13 +93,6 @@ class TestWeakPromotion(unittest.TestCase):
|
||||
t.realize()
|
||||
self.assertNotIn(t.uop.buffer.dtype, dtypes.weaks)
|
||||
|
||||
def test_computed_float_index_lowers(self):
|
||||
# a half-pixel nearest index resolves its float-scaled range before the gather
|
||||
idx = (Tensor.arange(8) + 0.5) / 4 - 0.5
|
||||
idx = (idx.clip(0, 1) - 0.5).ceil().int()
|
||||
out = Tensor([0, 1], device="NULL")[idx].contiguous().realize()
|
||||
self.assertNotIn(out.uop.buffer.dtype, dtypes.weaks)
|
||||
|
||||
|
||||
class TestWeakStorageBoundary(unittest.TestCase):
|
||||
# weak has no storage: a weak assignment source casts when it defers to the destination, everything else raises
|
||||
@@ -156,24 +125,10 @@ class TestWeakMaterializationEntries(unittest.TestCase):
|
||||
self.assertEqual(weak_val().numpy().dtype.itemsize, strong.itemsize)
|
||||
self.assertEqual(weak_val().tolist(), [value])
|
||||
self.assertEqual(weak_val().cast(strong).realize().uop.buffer.dtype, strong)
|
||||
self.assertEqual(weak_val().contiguous().dtype, weak) # no layout to fix, stays weak
|
||||
self.assertEqual(weak_val().realize().dtype, weak) # no width to store, stays weak
|
||||
self.assertEqual(weak_val().clone().dtype, strong) # storage commits at the default
|
||||
for entry in (lambda t: t.to("CPU:1").realize(), lambda t: t.as_param(0)):
|
||||
for entry in (lambda t: t.contiguous(), lambda t: t.realize(), lambda t: t.clone(),
|
||||
lambda t: t.to("CPU:1").realize(), lambda t: t.as_param(0)):
|
||||
with self.assertRaises(RuntimeError): entry(weak_val())
|
||||
|
||||
def test_weak_is_virtual(self):
|
||||
# NOTE: int64 lub uint64 is weakfloat, so this is device-ful weak from promotion, never from a cast to weak
|
||||
devful = Tensor([1], dtype=dtypes.int64, device="CPU") + Tensor([1], dtype=dtypes.uint64, device="CPU")
|
||||
for t in (Tensor.const(dtypes.weakfloat, 0.5), devful):
|
||||
self.assertTrue(t.uop.is_virtual)
|
||||
# realize is a no-op, so a weak input can never become the real buffer TinyJit needs
|
||||
with self.assertRaises(JitError): TinyJit(lambda x: (x+1).realize())(t)
|
||||
# callify must not silently commit a weak CONTIGUOUS to storage
|
||||
c = devful.alu(Ops.CONTIGUOUS)
|
||||
c.callify()
|
||||
self.assertIs(c.dtype, dtypes.weakfloat)
|
||||
|
||||
def test_empty_reads_commit(self):
|
||||
for weak, strong in ((dtypes.weakfloat, dtypes.default_float),):
|
||||
empty = Tensor.const(weak, 0).reshape(1).shrink(((0, 0),))
|
||||
@@ -182,19 +137,5 @@ class TestWeakMaterializationEntries(unittest.TestCase):
|
||||
self.assertEqual(empty.tolist(), [])
|
||||
|
||||
|
||||
class TestSignedUint64Weakfloat(unittest.TestCase):
|
||||
# int64 and uint64 have no common integer supertype (JAX JEP), so the join defers to weakfloat instead of wrapping
|
||||
def test_no_wrap(self):
|
||||
r = Tensor([-1], dtype=dtypes.int64, device="CPU") + Tensor([1], dtype=dtypes.uint64, device="CPU")
|
||||
self.assertEqual((r.dtype, r.item()), (dtypes.weakfloat, 0.0))
|
||||
|
||||
def test_weakfloat_lowers(self):
|
||||
i64, u64 = Tensor([-1], dtype=dtypes.int64, device="CPU"), Tensor([3], dtype=dtypes.uint64, device="CPU")
|
||||
r = i64 + u64 + Tensor([2], dtype=dtypes.float16, device="CPU") # a concrete consumer takes the join
|
||||
self.assertEqual((r.dtype, r.cast(dtypes.float32).item()), (dtypes.half, 4.0))
|
||||
self.assertEqual((i64 < u64).item(), True) # comparison meets at float
|
||||
self.assertAlmostEqual((i64 + u64).sin().item(), math.sin(2), places=5) # Unary lowers before transcendental
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -535,7 +535,7 @@ class TestFunctionTuple(unittest.TestCase):
|
||||
|
||||
@function(precompile=True, precompile_backward=True)
|
||||
def f(a:Tensor):
|
||||
c = Tensor(Tensor.invalids(a.shape[0]//len(devs), a.shape[1], dtype=a.dtype, device=devs).uop.unshard(0), device=devs)
|
||||
c = Tensor(Tensor.invalids(a.shape[0]//len(devs), a.shape[1], dtype=a.dtype, device=devs).uop.multi(0), device=devs)
|
||||
return Tensor.custom_kernel(c, a, fxn=double_kernel, grad_fxn=double_grad)[0]
|
||||
|
||||
np.testing.assert_allclose(f(a).numpy(), 14.0)
|
||||
@@ -543,7 +543,7 @@ class TestFunctionTuple(unittest.TestCase):
|
||||
# g is f with empty output instead of invalids
|
||||
@function(precompile=True, allow_implicit=True)
|
||||
def g(a:Tensor):
|
||||
c = Tensor(Tensor.empty(a.shape[0]//len(devs), a.shape[1], dtype=a.dtype, device=devs).uop.unshard(0), device=devs)
|
||||
c = Tensor(Tensor.empty(a.shape[0]//len(devs), a.shape[1], dtype=a.dtype, device=devs).uop.multi(0), device=devs)
|
||||
return Tensor.custom_kernel(c, a, fxn=double_kernel, grad_fxn=double_grad)[0]
|
||||
|
||||
np.testing.assert_allclose(g(a).numpy(), 14.0)
|
||||
@@ -608,7 +608,7 @@ class TestFunctionTuple(unittest.TestCase):
|
||||
@function(precompile=True)
|
||||
def f(a:Tensor):
|
||||
c = Tensor.invalids(*a.uop.shard_shape, dtype=a.dtype, device=a.device)
|
||||
if multi: c = Tensor(c.uop.unshard(a.uop.axis), device=a.device)
|
||||
if multi: c = Tensor(c.uop.multi(a.uop.axis), device=a.device)
|
||||
c = Tensor.custom_kernel(c, a, fxn=my_kernel)[0]
|
||||
return c + 1
|
||||
|
||||
|
||||
@@ -79,10 +79,6 @@ class TestMultiTensor(unittest.TestCase):
|
||||
with self.assertRaises(RuntimeError):
|
||||
X.shard_(devices_3, 0)
|
||||
|
||||
def test_shard_reshape_cross_boundary(self):
|
||||
X = Tensor.ones(5, 4).contiguous().realize().shard(devices_2, 1)
|
||||
with self.assertRaises(RuntimeError): X.reshape(10, 2).uop.axis
|
||||
|
||||
def test_tensor_from_multi(self):
|
||||
X = Tensor([1, 2], dtype=dtypes.int).shard_(devices_2, 0)
|
||||
Y = Tensor(X.uop)
|
||||
@@ -127,28 +123,6 @@ class TestMultiTensor(unittest.TestCase):
|
||||
with Context(RING=use_ring):
|
||||
np.testing.assert_equal(t.shard(devices_2, axis=axis).sum().item(), 10)
|
||||
|
||||
def test_allreduce_cast_half(self, assign=False, kernel_count=8):
|
||||
devices = tuple(f"{Device.DEFAULT}:{i}" for i in range(2))
|
||||
a_src = Tensor.arange(2*3, dtype=dtypes.half).reshape(2, 3).clone().realize()
|
||||
b_src = Tensor.arange(2*3, dtype=dtypes.half).reshape(2, 3).clone().realize()
|
||||
a = a_src.shard(devices, axis=0).realize()
|
||||
b = b_src.shard(devices, axis=0).realize()
|
||||
# assigning creates a copy of the output before allreduce
|
||||
if assign:
|
||||
tst = Tensor.empty_like(b)
|
||||
tst.assign(a + b)
|
||||
else:
|
||||
tst = a + b
|
||||
tst = tst.float().sum(0)
|
||||
GlobalCounters.reset()
|
||||
with Context(ALLREDUCE_CAST=1, RING=0, ALL2ALL=0):
|
||||
tst.realize()
|
||||
kernel_count = GlobalCounters.kernel_count
|
||||
np.testing.assert_allclose(tst.numpy(), (a_src.numpy()+b_src.numpy()).sum(0))
|
||||
self.assertEqual(kernel_count, kernel_count)
|
||||
|
||||
def test_allreduce_cast_half_assign(self): self.test_allreduce_cast_half(assign=True, kernel_count=10)
|
||||
|
||||
def test_multiple_to_single_device(self):
|
||||
kernel_counts = {}
|
||||
for ring in (0, 2):
|
||||
@@ -597,7 +571,7 @@ class TestShrinkMultiTensorShardedAxis(unittest.TestCase):
|
||||
t = Tensor.arange(64).reshape(8, 8).clone().realize()
|
||||
t.shard_([f"{Device.DEFAULT}:{i}" for i in range(4)], axis=0)
|
||||
|
||||
with self.assertRaises(RuntimeError):
|
||||
with self.assertRaises(AssertionError):
|
||||
# sharded axis shrink on non-device boundry is not allowed
|
||||
a = t.shrink(((0, 3), (0, 8))).contiguous()
|
||||
a.schedule_linear()
|
||||
|
||||
@@ -30,7 +30,7 @@ class TestRealizeIsRealized(unittest.TestCase):
|
||||
def test_multi(self):
|
||||
d = Device.DEFAULT
|
||||
t = Tensor.ones(8).contiguous().shard((d, d), axis=0).realize()
|
||||
assert t.uop.src[0].is_realized
|
||||
assert all(u.is_realized for u in t.uop.src)
|
||||
|
||||
def test_empty(self):
|
||||
t = Tensor.empty(4, 4).realize()
|
||||
|
||||
+17
-16
@@ -38,8 +38,8 @@ add_tags = PatternMatcher([
|
||||
])
|
||||
|
||||
def replace_contig_with_store_after(u:UOp):
|
||||
# can't allocate a buffer for a virtual value
|
||||
if u.is_virtual: return None
|
||||
# can't allocate a buffer without a device (e.g., inside a CALL function body with only PARAMs)
|
||||
if u.device is None: return None
|
||||
# if size is 0, remove the contig
|
||||
if 0 in u.shape: return u.src[0]
|
||||
# no real contig for DISK/TINYFS tensors, they are left alone
|
||||
@@ -49,7 +49,7 @@ def replace_contig_with_store_after(u:UOp):
|
||||
|
||||
def replace_store_after_with_contig(u:UOp, src:UOp):
|
||||
assigned_to = u
|
||||
while assigned_to.op in {Ops.BITCAST, Ops.AFTER, Ops.UNSHARD}: assigned_to = assigned_to.src[0].base
|
||||
while assigned_to.op in {Ops.BITCAST, Ops.AFTER, Ops.MULTI}: assigned_to = assigned_to.src[0].base
|
||||
if assigned_to.op not in {Ops.BUFFER, Ops.SLICE}: return src.contiguous(tag=u.tag)
|
||||
|
||||
def _make_buffer_view(src:UOp) -> UOp|None:
|
||||
@@ -61,29 +61,29 @@ def _make_buffer_view(src:UOp) -> UOp|None:
|
||||
buf = buf.src[0]
|
||||
if byte_offset % buf.dtype.itemsize != 0: return None
|
||||
offset = byte_offset // buf.dtype.itemsize
|
||||
return UOp(Ops.SLICE, src.dtype, (buf, UOp.const(None, offset)), src.numel())
|
||||
return UOp(Ops.SLICE, src.dtype, (buf, UOp.const(dtypes.weakint, offset)), src.numel())
|
||||
|
||||
def contiguous_mops_to_view(c:UOp, src:UOp):
|
||||
"""MOPS(BUFFER) → SLICE when movement ops collapse to a contiguous range."""
|
||||
buf = src.base
|
||||
if buf.op not in {Ops.BUFFER, Ops.SLICE, Ops.UNSHARD}: return None
|
||||
if buf.op not in {Ops.BUFFER, Ops.SLICE, Ops.MULTI}: return None
|
||||
if src.op is Ops.RESHAPE and src.src[0].op in {Ops.BUFFER, Ops.SLICE} and c.op is not Ops.BITCAST: return None
|
||||
if c.op is not Ops.BITCAST and src.op is Ops.BUFFER: return None
|
||||
|
||||
# no symbolic shape
|
||||
if not all_int(c.shape): return None
|
||||
|
||||
if buf.op is not Ops.UNSHARD and (view := _make_buffer_view(src)) is not None:
|
||||
if buf.op is not Ops.MULTI and (view := _make_buffer_view(src)) is not None:
|
||||
view = (view.replace(dtype=c.dtype, arg=c.numel()) if c.op is Ops.BITCAST else view).reshape(c.shape)
|
||||
return c.replace(src=(view,)) if c.op is Ops.COPY else view
|
||||
|
||||
# for UNSHARD tensors, use multi_pm to resolve per-shard movement ops, then create SLICE on the resolved result
|
||||
# for MULTI tensors, use multi_pm to resolve per-shard movement ops, then create SLICE on the resolved result
|
||||
if not isinstance(c.device, str):
|
||||
from tinygrad.schedule.multi import multi_pm
|
||||
resolved = graph_rewrite(src, multi_pm, name="multi_buffer_view")
|
||||
if resolved.op is not Ops.UNSHARD: return None
|
||||
if resolved.op is not Ops.MULTI: return None
|
||||
if (view := _make_buffer_view(resolved.src[0])) is None: return None
|
||||
return view.reshape(resolved.src[0].shape).unshard(resolved.arg, resolved.src[1:]).contiguous(tag=c.tag)
|
||||
return view.reshape(resolved.src[0].shape).multi(resolved.arg).contiguous(tag=c.tag)
|
||||
|
||||
return None
|
||||
|
||||
@@ -92,7 +92,7 @@ def _precompiled_output_redirect(s:UOp, t:UOp) -> UOp|None:
|
||||
# materialize straight into t
|
||||
if s.op is Ops.CONTIGUOUS: return t.after(t.store(s.src[0]))
|
||||
# rebind output storage to t
|
||||
if s.op in {Ops.BUFFER, Ops.UNSHARD} and s.has_buffer_identity(): return t
|
||||
if s.op in {Ops.BUFFER, Ops.MULTI} and s.has_buffer_identity(): return t
|
||||
return None
|
||||
|
||||
def transform_precompiled_call(c:UOp) -> UOp|None:
|
||||
@@ -142,11 +142,11 @@ pm_early_transform_tensor_graph = PatternMatcher([
|
||||
(UPat((Ops.BITCAST, Ops.COPY, Ops.CONTIGUOUS), src=(UPat(GroupOp.Movement|{Ops.BUFFER}, name="src"),), name="c"), contiguous_mops_to_view),
|
||||
|
||||
# remove contiguous on movement ops before a copy on disk
|
||||
(UPat(GroupOp.Movement-{Ops.SHRINK, Ops.RESHAPE}, name="x").f(Ops.CONTIGUOUS).f(Ops.COPY, name="copy"), lambda x,copy:
|
||||
copy.replace(src=(x,), tag=None) if isinstance(x.device, str) and x.device.startswith("DISK") else None),
|
||||
(UPat(GroupOp.Movement-{Ops.SHRINK, Ops.RESHAPE}, name="x").f(Ops.CONTIGUOUS).f(Ops.COPY, allow_any_len=True, name="copy"), lambda x,copy:
|
||||
copy.replace(src=(x,)+copy.src[1:], tag=None) if isinstance(x.device, str) and x.device.startswith("DISK") else None),
|
||||
# push copy past movement ops to disk
|
||||
(UPat(GroupOp.Movement-{Ops.SHRINK, Ops.RESHAPE}, name="x").f(Ops.COPY, name="copy"), lambda x,copy:
|
||||
x.replace(src=(copy.replace(src=(x.src[0],), tag=None),)+x.src[1:]) \
|
||||
x.replace(src=(copy.replace(src=(x.src[0],)+copy.src[1:], tag=None),)+x.src[1:]) \
|
||||
if isinstance(x.device, str) and x.device.startswith("DISK") else None),
|
||||
|
||||
# add CONTIGUOUS to tagged UOps
|
||||
@@ -179,9 +179,10 @@ def finalize_after(ctx:AllocCtx, x:UOp):
|
||||
|
||||
def replace_input_buffer(ctx:AllocCtx, b:UOp):
|
||||
ctx.replacements.append(b)
|
||||
if b.op is Ops.BIND: return b.param_like(len(ctx.replacements)-1)
|
||||
return UOp.param(len(ctx.replacements)-1, b.dtype, b.shape, b.device,
|
||||
addrspace=b.addrspace if b.addrspace is not None else AddrSpace.GLOBAL)
|
||||
b._min_max if b.op is Ops.BIND else None, name=b.src[0].expr if b.op is Ops.BIND else None,
|
||||
addrspace=b.addrspace if b.addrspace is not None else AddrSpace.GLOBAL,
|
||||
multiple_of=b.src[0].arg.multiple_of if b.op is Ops.BIND else None)
|
||||
|
||||
pm_finalize_call = PatternMatcher([
|
||||
(UPat(Ops.AFTER, name="x"), finalize_after),
|
||||
@@ -203,7 +204,7 @@ def transform_to_call(big_sink:UOp) -> tuple[UOp, dict[UOp, UOp]]:
|
||||
if VIZ: graph_rewrite(big_sink, PatternMatcher([]), name="View Tensor Graph")
|
||||
# uop list is a list in the original_sink graph and we can map to the tags later
|
||||
# same predicate as Tensor.realize
|
||||
ctx = AllocCtx(bases={base for x in big_sink.src if not (base:=x.base).is_virtual and not base.has_buffer_identity()
|
||||
ctx = AllocCtx(bases={base for x in big_sink.src if (base:=x.base).device is not None and not base.has_buffer_identity()
|
||||
and base.op is not Ops.AFTER and base.addrspace is not AddrSpace.ALU})
|
||||
|
||||
# this rewrite is "read-only", it adds simple things to buffer_map and may sink things on big_sink, bottom_up
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from dataclasses import replace, dataclass
|
||||
import itertools, functools
|
||||
from tinygrad.helpers import DISABLE_FAST_IDIV, TRANSCENDENTAL, SPEC, DEBUG, VIZ, IMAGE, NOOPT, EMULATED_DTYPES, NOLOCALS, USE_TC
|
||||
from tinygrad.helpers import ALLOW_TF32, DEFAULT_FLOAT, DEFAULT_INT, TracingKey, Context, panic
|
||||
from tinygrad.helpers import ALLOW_TF32, TracingKey, Context, panic
|
||||
from tinygrad.uop.ops import PatternMatcher, graph_rewrite, UOp, pm_lower_index_dtype, Ops, UPat, track_rewrites, KernelInfo, ProgramInfo, GroupOp
|
||||
from tinygrad.uop.ops import AxisType
|
||||
from tinygrad.uop.render import pyrender
|
||||
from tinygrad.uop.spec import type_verify, spec_tensor, spec_program
|
||||
from tinygrad.renderer import Renderer, Estimates
|
||||
from tinygrad.renderer.isa import ISARenderer, IselContext, PreRegAllocContext
|
||||
from tinygrad.dtype import dtypes, AddrSpace, Invalid
|
||||
from tinygrad.dtype import dtypes, AddrSpace
|
||||
|
||||
# import all pattern matchers here
|
||||
from tinygrad.codegen.gpudims import pm_add_gpudims
|
||||
@@ -21,13 +21,12 @@ from tinygrad.codegen.late.coalesce import indexing_simplify
|
||||
from tinygrad.codegen.opt.postrange import apply_opts
|
||||
from tinygrad.codegen.late.gater import pm_move_gates_from_index
|
||||
from tinygrad.codegen.simplify import pm_simplify_ranges, pm_flatten_range, pm_split_ranges, pm_load_collapse
|
||||
from tinygrad.schedule.multi import multi_pm
|
||||
from tinygrad.schedule.rangeify import pm_mops
|
||||
from tinygrad.codegen.late.linearizer import CFGContext, pm_split_ends, pm_add_control_flow, linearize
|
||||
from tinygrad.codegen.late.linearizer import CFGContext, pm_split_ends, pm_add_control_flow, linearize, ranges_to_loops
|
||||
from tinygrad.codegen.late.regalloc import LinearScanRegallocContext, pm_regalloc_rewrite
|
||||
from tinygrad.codegen.late.coalesce import memory_coalescing, pm_simplify_add_image
|
||||
from tinygrad.helpers import all_same, flatten, argsort, partition
|
||||
from tinygrad.uop.ops import _broadcast_shape, identity_element
|
||||
from tinygrad.uop.ops import _align_left, _broadcast_shape, identity_element
|
||||
from tinygrad.schedule.rangeify import BufferizeOpts
|
||||
|
||||
def do_number_param(ctx:list[int], x:UOp):
|
||||
@@ -89,20 +88,25 @@ expander2 = PatternMatcher([
|
||||
(UPat(Ops.WMMA, name="u"), expand_wmma),
|
||||
])+pm_flatten_range+mop_cleanup
|
||||
|
||||
def expand_broadcast(x:UOp):
|
||||
def broadcast_binary(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)
|
||||
return x.replace(src=tuple([u.expand(shape) for u in x.src]))
|
||||
shaped_aligned = _align_left(*shapes)
|
||||
broadcasted = _broadcast_shape(*shapes)
|
||||
src_reshaped = [u.reshape(shp).expand(broadcasted) for u,shp in zip(x.src, shaped_aligned)]
|
||||
return x.replace(src=tuple(src_reshaped))
|
||||
|
||||
def broadcast_and_devec_wmma(b:UOp):
|
||||
shapes = [u.shape[:-1] for u in b.src]
|
||||
if all_same(shapes): return None
|
||||
shape = _broadcast_shape(*shapes)
|
||||
src_expanded = tuple([u.expand(shape+(u.shape[-1],)) for u in b.src])
|
||||
shaped_aligned = _align_left(*shapes)
|
||||
broadcasted = _broadcast_shape(*shapes)
|
||||
src_reshaped = [u.reshape(shp+(u.shape[-1],)).expand(broadcasted+(u.shape[-1],))
|
||||
for u,shp in zip(b.src, shaped_aligned)]
|
||||
src = []
|
||||
for idx in itertools.product(*[range(i) for i in b.shape[:-1]]):
|
||||
src.append(b.replace(src=tuple([x.index(*idx) for x in src_expanded])))
|
||||
idx_c = [UOp.const(dtypes.weakint, i) for i in idx]
|
||||
src.append(b.replace(src=tuple([x.index(*idx_c) for x in src_reshaped])))
|
||||
return UOp.stack(*src).reshape(b.shape)
|
||||
|
||||
pm_wmma_add = PatternMatcher([
|
||||
@@ -115,19 +119,19 @@ pm_wmma_add = PatternMatcher([
|
||||
lambda wmma,reshape,permute,add: (wmma + add.permute(argsort(permute.arg)).reshape(wmma.shape)).reshape(reshape.shape).permute(permute.arg)),
|
||||
])
|
||||
|
||||
pm_expand_broadcast = pm_wmma_add+PatternMatcher([
|
||||
(UPat(GroupOp.Binary|GroupOp.Ternary|{Ops.STORE}, name="x"), expand_broadcast),
|
||||
unbroadcast = pm_wmma_add+PatternMatcher([
|
||||
(UPat(GroupOp.Binary|GroupOp.Ternary|{Ops.STORE}, name="x"), broadcast_binary),
|
||||
(UPat(Ops.WMMA, name="b"), broadcast_and_devec_wmma),
|
||||
])
|
||||
|
||||
def do_devectorize(b:UOp):
|
||||
if b.shape == (): return None
|
||||
# broadcasting needs to be already unpacked, Invalid matches any dtype and shape
|
||||
if not all(x.shape == b.shape or x.base.arg is Invalid for x in b.src): return None
|
||||
# broadcasting needs to be already unpacked
|
||||
if not all_same([x.shape for x in b.src]): return None
|
||||
src = []
|
||||
for idx in itertools.product(*[range(x) for x in b.shape]):
|
||||
idx_c = [UOp.const(None, i) for i in idx]
|
||||
src.append(b.replace(dtype=None, src=tuple(x.base if x.base.arg is Invalid else x.index(*idx_c) for x in b.src)))
|
||||
idx_c = [UOp.const(dtypes.weakint, i) for i in idx]
|
||||
src.append(b.replace(src=tuple([x.index(*idx_c) for x in b.src])))
|
||||
return UOp.stack(*src).reshape(b.shape) if b.op is not Ops.STORE else UOp.group(*src)
|
||||
|
||||
def do_stack_wmma(u:UOp):
|
||||
@@ -136,7 +140,7 @@ def do_stack_wmma(u:UOp):
|
||||
src = []
|
||||
for b in u.src:
|
||||
if b.op != Ops.STACK:
|
||||
src.append(UOp.stack(*[b.index(i) for i in range(b.max_numel())]))
|
||||
src.append(UOp.stack(*[b.index(UOp.const(dtypes.weakint, i)) for i in range(b.max_numel())]))
|
||||
else:
|
||||
src.append(b)
|
||||
return u.replace(src=tuple(src))
|
||||
@@ -162,7 +166,7 @@ devectorizer2 = mop_cleanup+pm_mops+PatternMatcher([
|
||||
# RESHAPE a void is removed (hack for AFTER)
|
||||
(UPat(Ops.RESHAPE, dtype=dtypes.void, name="x"), lambda x: x.src[0]),
|
||||
# reshape of a single element shaped value to scalar is an index
|
||||
(UPat(Ops.RESHAPE, name="x"), lambda x: x.src[0].index(0) if x.marg == () and x.src[0].shape == (1,) else None),
|
||||
(UPat(Ops.RESHAPE, name="x"), lambda x: x.src[0].index(UOp.const(dtypes.weakint, 0)) if x.marg == () and x.src[0].shape == (1,) else None),
|
||||
# EXPAND on scalar -> STACK
|
||||
(UPat(Ops.EXPAND, src=(UPat.var("x"), UPat()), name="out"),
|
||||
lambda x,out: UOp.stack(*([x]*out.max_numel())) if x.shape == () and out.shape == (out.max_numel(),) else None),
|
||||
@@ -243,7 +247,7 @@ pm_add_loads = PatternMatcher([
|
||||
|
||||
def add_local_buffer(ctx, x:UOp):
|
||||
buf = UOp.placeholder(x.max_shape, x.dtype, slot=next(ctx), addrspace=x.arg.addrspace)
|
||||
return buf.after(buf.index(*x.src[1:]).store(x.src[0]).end(*x.src[1:]))
|
||||
return buf.after(buf.index(*x.src[1:]).store(x.src[0]).end(*x.src[1:]).barrier())
|
||||
|
||||
pm_add_local_buffers = PatternMatcher([
|
||||
(UPat(Ops.STAGE, name="x"), add_local_buffer),
|
||||
@@ -256,42 +260,13 @@ pm_cast_float_alu = PatternMatcher([
|
||||
lambda u,x: u.replace(src=(x.cast(u.dtype),)) if x.dtype != u.dtype else None),
|
||||
])
|
||||
|
||||
def _is_local_store(x:UOp): return x.op is Ops.STORE and x.addrspace is AddrSpace.LOCAL
|
||||
|
||||
def add_raw_barrier(after:UOp):
|
||||
# loads from a LOCAL buffer that depend (via AFTER) on stores to LOCAL memory need a workgroup barrier
|
||||
if after.addrspace is not AddrSpace.LOCAL: return None
|
||||
# one toposort over all the deps
|
||||
deps = UOp.sink(*after.src[1:]).toposort(gate=lambda x: x.op is not Ops.BARRIER)
|
||||
if not any(_is_local_store(x) for x in deps): return None
|
||||
return after.src[0].after(UOp(Ops.BARRIER, src=after.src[1:]))
|
||||
|
||||
def add_war_barrier(end:UOp):
|
||||
# a LOCAL buffer stored and loaded in the same loop needs a barrier at the end of the loop body
|
||||
rngs = [r for r in end.src[1:] if r.op is Ops.RANGE and r.arg[1] in (AxisType.REDUCE, AxisType.WEAK, AxisType.LOOP) and r.vmax > 0]
|
||||
if not rngs or end.src[0].op is Ops.BARRIER: return None
|
||||
sl = end.src[0].backward_slice_with_self
|
||||
# only stores that are inside this loop body (not in the backward slice through AFTER chains from other loops)
|
||||
store_bufs = {x.buf_uop for x in sl if _is_local_store(x) and any(r in x.ranges for r in rngs)}
|
||||
# a load whose buffer matches a local store's buffer is necessarily a local load
|
||||
if not (loads:=[x for x in sl if x.op is Ops.LOAD and x.src[0].buf_uop in store_bufs]): return None
|
||||
return end.replace(src=(UOp(Ops.BARRIER, src=(end.src[0], *loads)),)+end.src[1:])
|
||||
|
||||
pm_implicit_barriers = PatternMatcher([
|
||||
(UPat(Ops.AFTER, name="after"), add_raw_barrier),
|
||||
(UPat(Ops.END, name="end"), add_war_barrier),
|
||||
])
|
||||
|
||||
def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
|
||||
if VIZ: graph_rewrite(ast, PatternMatcher([]), name="View Base AST")
|
||||
if DEBUG >= 5: print(pyrender(ast))
|
||||
if SPEC: type_verify(ast, spec_tensor)
|
||||
|
||||
# resolve UNSHARDs (multi-device UNSHARDs are already resolved by the scheduler; this handles in-kernel shards, e.g. fragments)
|
||||
sink = graph_rewrite(ast, multi_pm, name="multi_pm")
|
||||
|
||||
# preprocess
|
||||
sink = graph_rewrite(sink, pm_mops, name="early movement ops", bottom_up=True)
|
||||
sink = graph_rewrite(ast, pm_mops, name="early movement ops", bottom_up=True)
|
||||
|
||||
# first we optimize
|
||||
if optimize:
|
||||
@@ -327,7 +302,7 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
|
||||
|
||||
# **** optimizations are done, now we lower to actual code ****
|
||||
|
||||
sink = graph_rewrite(sink, symbolic_simple+pm_expand_broadcast+pm_add_loads, name="*** expand broadcast / add loads")
|
||||
sink = graph_rewrite(sink, symbolic_simple+unbroadcast+pm_add_loads, name="*** unbroadcast / add loads")
|
||||
|
||||
# devectorize
|
||||
sink = graph_rewrite(sink, symbolic_simple+devectorizer2, ctx=ren, name="devectorize2")
|
||||
@@ -374,8 +349,11 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
|
||||
pm_final_rewrite = pm_decomp+extra_matcher+pm_split_ends
|
||||
sink = graph_rewrite(sink, pm_final_rewrite+pm_remove_invalid, ctx=ren, name="final rewrite")
|
||||
|
||||
# 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")
|
||||
if VIZ: graph_rewrite(sink, PatternMatcher([]), name="View Output AST")
|
||||
if SPEC: type_verify(sink, spec_program)
|
||||
|
||||
# rewrite bounded ranges to loops for renderers without range support, after validation like instruction selection
|
||||
if not ren.supports_ranges: sink = ranges_to_loops(sink)
|
||||
|
||||
# this was the linearizer
|
||||
sink = graph_rewrite(sink, pm_add_control_flow, ctx=CFGContext(sink), name="add control flow", bottom_up=True)
|
||||
@@ -384,9 +362,6 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
|
||||
num_params = len([x for x in sink.toposort() if x.op is Ops.PARAM and x.arg.slot != -1])
|
||||
sink = graph_rewrite(sink, pm_number_params, ctx=[num_params], name="number params with -1", walk=True)
|
||||
|
||||
if VIZ: graph_rewrite(sink, PatternMatcher([]), name="View Output AST")
|
||||
if SPEC: type_verify(sink, spec_program)
|
||||
|
||||
# return the rewritten sink
|
||||
return sink
|
||||
|
||||
@@ -436,7 +411,8 @@ def do_assemble(ctx:Renderer, prg:UOp, lin:UOp) -> UOp:
|
||||
|
||||
def do_render(ctx:Renderer, prg:UOp, lin:UOp) -> UOp:
|
||||
src = ctx.render(list(lin.src))
|
||||
return prg.replace(src=prg.src + (UOp(Ops.SOURCE, arg=src),))
|
||||
new_arg = replace(prg.arg, aux=tuple(ctx.aux(list(lin.src)))) if ctx.has_aux else prg.arg
|
||||
return prg.replace(src=prg.src + (UOp(Ops.SOURCE, arg=src),), arg=new_arg)
|
||||
|
||||
def do_compile(ctx:Renderer, prg:UOp, source:UOp) -> UOp|None:
|
||||
if DEBUG >= 4: print(source.arg)
|
||||
@@ -469,21 +445,21 @@ def do_to_program(ast:UOp, renderer:Renderer) -> UOp:
|
||||
elif ast.op is Ops.SINK:
|
||||
assert isinstance(ast.arg, KernelInfo), "requires KernelInfo on arg to to_program"
|
||||
full_sink = full_rewrite_to_sink(ast, renderer, optimize=ast.tag is None)
|
||||
prog_info = ProgramInfo.from_sink(full_sink, renderer.target)
|
||||
prog_info = ProgramInfo.from_sink(full_sink)
|
||||
# instruction selection
|
||||
if isinstance(renderer, ISARenderer):
|
||||
full_sink = graph_rewrite(full_sink, renderer.pre_isel_matcher, ctx=itertools.count(-1, -1), name="pre instruction selection", bottom_up=True)
|
||||
full_sink = graph_rewrite(full_sink, renderer.isel_matcher, ctx=IselContext(full_sink), name="instruction selection", bottom_up=True)
|
||||
prg = UOp(Ops.PROGRAM, src=(full_sink,), arg=prog_info)
|
||||
else: raise RuntimeError(f"can't call to_program on {ast.op}")
|
||||
if not isinstance(prg.arg, ProgramInfo): prg = prg.replace(arg=ProgramInfo.from_sink(prg.src[0], renderer.target))
|
||||
if not isinstance(prg.arg, ProgramInfo): prg = prg.replace(arg=ProgramInfo.from_sink(prg.src[0]))
|
||||
prg = graph_rewrite(prg, pm_to_program, ctx=renderer, name="linearize/render")
|
||||
if VIZ: graph_rewrite(prg, PatternMatcher([]), name="View Program")
|
||||
return prg
|
||||
|
||||
to_program_cache: dict[tuple, UOp] = {}
|
||||
def to_program(ast:UOp, renderer:Renderer) -> UOp:
|
||||
config = (NOOPT, EMULATED_DTYPES, NOLOCALS, USE_TC, IMAGE, DISABLE_FAST_IDIV, TRANSCENDENTAL, ALLOW_TF32, DEFAULT_FLOAT, DEFAULT_INT)
|
||||
config = (NOOPT, EMULATED_DTYPES, NOLOCALS, USE_TC, IMAGE, DISABLE_FAST_IDIV, TRANSCENDENTAL, ALLOW_TF32)
|
||||
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
|
||||
|
||||
@@ -20,17 +20,13 @@ def reindex(idx:UOp, off:int, mul=2) -> UOp:
|
||||
def l2i(op: Ops, dt: DType, *uops:UOp):
|
||||
zero = UOp.const(dt, 0)
|
||||
if len(uops) == 2: a0, a1 = uops
|
||||
elif len(uops) == 3: a0, a1, b0 = uops # a shift's count is a single word
|
||||
elif len(uops) == 4: a0, a1, b0, b1 = uops
|
||||
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; 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])
|
||||
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))
|
||||
return uops[0].cast(l2i_dt[dt]), (uops[0] < 0).where(UOp.const(l2i_dt[dt], -1), UOp.const(l2i_dt[dt], 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))
|
||||
return (lo:=uops[0].cast(l2i_dt[dt])), (uops[0] / 2**32).cast(l2i_dt[dt]) - ((uops[0] < 0) & lo.ne(0)).cast(l2i_dt[dt])
|
||||
case Ops.CAST if dt in dtypes.floats:
|
||||
small = (a1.eq(0) & (a0 >= 0)) | (a1.eq(-1) & (a0 < 0))
|
||||
return small.where(a0.cast(dt), ((a1.cast(dtypes.float32) * (2**32)) + a0.bitcast(dtypes.uint).cast(dtypes.float32)).cast(dt))
|
||||
@@ -45,8 +41,8 @@ def l2i(op: Ops, dt: DType, *uops:UOp):
|
||||
lo, hi = ((a0u >> n) | ((a1u << 1) << (31 - n))).bitcast(dt), a1 >> (b0 & 31)
|
||||
fill = a1 >> 31 if dt == dtypes.int else zero # vacated high word: sign bits when signed, else 0
|
||||
return (b0 >= 32).where(hi, lo), (b0 >= 32).where(fill, hi)
|
||||
case Ops.ADD: return (low:=a0+b0), a1 + b1 + (low.bitcast(dtypes.uint) < a0.bitcast(dtypes.uint))
|
||||
case Ops.SUB: return a0 - b0, a1 - b1 - (a0.bitcast(dtypes.uint) < b0.bitcast(dtypes.uint))
|
||||
case Ops.ADD: return (low:=a0+b0), (a1 + b1).replace(dtype=dt) + (low.bitcast(dtypes.uint) < a0.bitcast(dtypes.uint)).cast(dt)
|
||||
case Ops.SUB: return a0 - b0, a1 - b1 - (a0.bitcast(dtypes.uint) < b0.bitcast(dtypes.uint)).cast(dt)
|
||||
case Ops.MUL:
|
||||
(a00, a01), (b00, b01) = unpack32(a0), unpack32(b0)
|
||||
mid = l2i(Ops.ADD, dt, shl(a00*b01, 16).bitcast(dt), shr(a00*b01, 16).bitcast(dt), shl(a01*b00, 16).bitcast(dt), shr(a01*b00, 16).bitcast(dt))
|
||||
@@ -69,23 +65,19 @@ def l2i(op: Ops, dt: DType, *uops:UOp):
|
||||
(nq0, nq1), (nr0, nr1) = l2i(Ops.BITCAST, dt, *l2i(Ops.NEG, dtypes.uint, *q)), l2i(Ops.BITCAST, dt, *l2i(Ops.NEG, dtypes.uint, *r))
|
||||
(q0, q1), (r0, r1) = l2i(Ops.BITCAST, dt, *q), l2i(Ops.BITCAST, dt, *r)
|
||||
return (a_neg.where(nr0, r0), a_neg.where(nr1, r1)) if op == Ops.CMOD else ((a_neg^b_neg).where(nq0, q0), (a_neg^b_neg).where(nq1, q1))
|
||||
return r if op == Ops.CMOD else q
|
||||
return (r[0].bitcast(dt), r[1].bitcast(dt)) if op == Ops.CMOD else (q[0].bitcast(dt), q[1].bitcast(dt))
|
||||
case Ops.CMPLT: return (a1 < b1) | ((a1.eq(b1)) & (a0.bitcast(dtypes.uint) < b0.bitcast(dtypes.uint)))
|
||||
case Ops.CMPEQ: return a0.eq(b0) & a1.eq(b1)
|
||||
case Ops.CMPNE: return a0.ne(b0) | a1.ne(b1)
|
||||
case Ops.XOR | Ops.OR | Ops.AND: return UOp(op, src=(a0, b0)), UOp(op, src=(a1, b1))
|
||||
case Ops.XOR | Ops.OR | Ops.AND: return UOp(op, dt, src=(a0, b0)), UOp(op, dt, src=(a1, b1))
|
||||
case Ops.WHERE: return uops[0].where(uops[1], uops[3]), uops[0].where(uops[2], uops[4])
|
||||
case Ops.MAX: return l2i(Ops.WHERE, dt, l2i(Ops.CMPLT, dt, *uops), b0, b1, a0, a1)
|
||||
case _: raise NotImplementedError(f"long decomposition of {op} unsupported")
|
||||
|
||||
def split_l2i(op: Ops, dt: DType, *uops:UOp):
|
||||
# l2i does arithmetic on its inputs; rules enter here to split them to 32-bit words first, l2i recurses on itself
|
||||
return l2i(op, dt, *graph_rewrite(UOp.sink(*uops), pm_long_decomp, bottom_up=True).src)
|
||||
|
||||
# ***** floats *****
|
||||
f2f_dt = { f:getattr(dtypes, f"uint{f.bitsize}") for f in dtypes.floats }
|
||||
|
||||
def rne(v: UOp, s) -> UOp: return shr(v, s) + ((shr(v, s - 1) & 1) & ((v & ((1 << (s - 1)) - 1)).ne(0) | (shr(v, s) & 1)))
|
||||
def rne(v: UOp, s) -> UOp: return shr(v, s) + ((shr(v, s - 1) & 1) & ((v & ((1 << (s - 1)) - 1)).ne(0).cast(v.dtype) | (shr(v, s) & 1)))
|
||||
|
||||
def f2f(v, fr:DType, to:DType, sat=True):
|
||||
fs, fb, (fe, fm), ts, tb, (te, tm) = fr.bitsize, exponent_bias(fr), dtypes.finfo(fr), to.bitsize, exponent_bias(to), dtypes.finfo(to)
|
||||
@@ -130,35 +122,27 @@ def f2f_store(st, idx, val, fr:DType, to:DType):
|
||||
if (n:=val.max_numel()) == 1: return st.replace(src=(idx, f2f(val.bitcast(f2f_dt[to]), to, fr)))
|
||||
return UOp.group(*(st.replace(src=(reindex(idx, i, 1), f2f(val.index(i).bitcast(f2f_dt[to]), to, fr))) for i in range(n)))
|
||||
|
||||
# tag is the 32-bit word this node becomes - (0 for the low word, 1 for the high, the dtype the consumer wants)
|
||||
pm_long_decomp = PatternMatcher([
|
||||
(UPat(GroupOp.Defines, src=(UPat.var("sz"),), name="x"), lambda x,sz:
|
||||
x.replace(dtype=l2i_dt[x.dtype], arg=replace(x.arg, dtype=l2i_dt[x.dtype]), src=(sz*2,)) if x.dtype in l2i_dt else None),
|
||||
(UPat(Ops.INDEX, tuple(l2i_dt.keys()), name='x'), lambda x:
|
||||
reindex(x, x.tag[0]).replace(dtype=x.tag[1], tag=None) if x.tag is not None else None),
|
||||
(UPat(Ops.STORE, src=(UPat.var('idx', tuple(l2i_dt.keys())), UPat.var('val')), name='st'), lambda st,idx,val:
|
||||
st.replace(src=(idx.rtag((0, dt:=l2i_dt[idx.dtype])), val.rtag((0, dt)))).group(
|
||||
st.replace(src=(idx.rtag((1, dt)), val.rtag((1, dt))))) if val.tag is None else None),
|
||||
(UPat(GroupOp.Comparison, src=[UPat.var('a', tuple(l2i_dt.keys())), UPat()], name="x"), lambda a,x:
|
||||
split_l2i(x.op, dt:=l2i_dt[a.dtype], *flatten((s.rtag((0, dt)), s.rtag((1, dt))) for s in x.src))),
|
||||
(UPat(Ops.CAST, tuple(l2i_dt.keys()), src=(UPat.var('a', tuple(l2i_dt.keys())),), name="x"), lambda a,x:
|
||||
split_l2i(Ops.BITCAST, l2i_dt[x.dtype], a.rtag((0, dt:=l2i_dt[a.dtype])), a.rtag((1, dt)))[x.tag[0]]),
|
||||
(UPat(Ops.INDEX, tuple(l2i_dt.keys()), name='x'), lambda x: reindex(x, x.tag).replace(dtype=l2i_dt[x.dtype]) if x.tag is not None else None),
|
||||
(UPat(Ops.STORE, src=(UPat.var('idx'), UPat.var('val', tuple(l2i_dt.keys()))), name='st'), lambda st,idx,val:
|
||||
st.replace(src=(idx.rtag(0), val.rtag(0))).group(st.replace(src=(idx.rtag(1), val.rtag(1)))) if val.tag is None else None),
|
||||
(UPat(GroupOp.Comparison, src=(UPat.var('a', tuple(l2i_dt.keys())), UPat.var('b', tuple(l2i_dt.keys()))), name="x"), lambda a,b,x:
|
||||
l2i(x.op, dt:=l2i_dt[a.dtype], a.rtag(0).cast(dt), a.rtag(1).cast(dt), b.rtag(0).cast(dt), b.rtag(1).cast(dt))),
|
||||
(UPat(Ops.CAST, tuple(l2i_dt.keys()), src=(UPat.var('a'),), name="x"), lambda a,x:
|
||||
split_l2i(x.op, x.dtype, a)[x.tag[0]] if x.tag is not None else None),
|
||||
l2i(x.op, x.dtype, a)[x.tag] if x.tag is not None and a.dtype not in l2i_dt else None),
|
||||
(UPat(Ops.CAST, tuple(l2i_dt.keys()), src=(UPat.var('a', tuple(l2i_dt.keys())),), name="x"), lambda a,x:
|
||||
(a.rtag(0).cast(dt:=l2i_dt[a.dtype]).bitcast(xdt:=l2i_dt[x.dtype]), a.rtag(1).cast(dt).bitcast(xdt))[x.tag]),
|
||||
(UPat(Ops.CAST, src=(UPat.var('a', tuple(l2i_dt.keys())),), name="x"), lambda a,x:
|
||||
split_l2i(x.op, x.dtype, a.rtag((0, dt:=l2i_dt[a.dtype])), a.rtag((1, dt))) if x.dtype not in l2i_dt and a.tag is None else None),
|
||||
(UPat((Ops.SHL, Ops.SHR), tuple(l2i_dt.keys()), src=(UPat.var('a'), UPat.var('b')), name="x"), lambda a,b,x:
|
||||
split_l2i(x.op, dt:=l2i_dt[x.dtype], a.rtag((0, dt)), a.rtag((1, dt)), b.rtag((0, dt)))[x.tag[0]] if x.tag is not None else None),
|
||||
(UPat(Ops.WHERE, tuple(l2i_dt.keys()), src=(UPat.var('c'), UPat.var('a'), UPat.var('b')), name="x"), lambda a,b,c,x:
|
||||
split_l2i(x.op, dt:=l2i_dt[x.dtype], c, a.rtag((0, dt)), a.rtag((1, dt)), b.rtag((0, dt)), b.rtag((1, dt)))[x.tag[0]]
|
||||
if x.tag is not None else None),
|
||||
(UPat((*(GroupOp.ALU - GroupOp.Comparison - {Ops.SHL, Ops.SHR, Ops.WHERE}), Ops.BITCAST), tuple(l2i_dt.keys()), name="x"), lambda x:
|
||||
split_l2i(x.op, l2i_dt[x.dtype], *flatten((a.rtag((0, l2i_dt[x.dtype])), a.rtag((1, l2i_dt[x.dtype]))) for a in x.src))[x.tag[0]]
|
||||
if x.tag is not None else None),
|
||||
l2i(x.op, x.dtype, a.rtag(0).cast(dt:=l2i_dt[a.dtype]), a.rtag(1).cast(dt)) if x.dtype not in l2i_dt and a.tag is None else None),
|
||||
(UPat((*(GroupOp.ALU - GroupOp.Comparison), Ops.BITCAST), tuple(l2i_dt.keys()), name="x"), lambda x:
|
||||
l2i(x.op, l2i_dt[x.dtype], *flatten((a.rtag(0).cast(dt:=l2i_dt[x.src[-1].dtype]), a.rtag(1).cast(dt))
|
||||
if a.dtype in l2i_dt else (a,) for a in x.src))[x.tag] if x.tag is not None else None),
|
||||
(UPat(Ops.LOAD, tuple(l2i_dt.keys()), src=(UPat.var('idx'),), name='x'), lambda x,idx:
|
||||
x.replace(dtype=l2i_dt[x.dtype], src=(reindex(idx, x.tag[0]).replace(dtype=l2i_dt[x.dtype], tag=None),), tag=None) if x.tag is not None else None),
|
||||
(UPat(Ops.CONST, tag={(w, dt) for w in (0, 1) for dt in l2i_dt.values()}, name='x'), lambda x:
|
||||
UOp.const(x.tag[1], truncate[x.tag[1]]((x.arg >> 32) if x.tag[0] == 1 else (x.arg & 0xFFFFFFFF))))
|
||||
x.replace(dtype=l2i_dt[x.dtype], src=(reindex(idx, x.tag).replace(dtype=l2i_dt[x.dtype]),))),
|
||||
(UPat(Ops.CONST, tuple(l2i_dt.keys()), name='x'), lambda x:
|
||||
UOp.const(dt:=l2i_dt[x.dtype], truncate[dt]((x.arg >> 32) if x.tag == 1 else (x.arg & 0xFFFFFFFF))))
|
||||
])
|
||||
|
||||
# float decomposition patterns - ctx is (fr, to) tuple
|
||||
|
||||
@@ -63,7 +63,7 @@ def threefry2x32(x: UOp, key: UOp):
|
||||
|
||||
def floordiv_to_idiv(a:UOp, b:UOp) -> UOp:
|
||||
if (a.vmin >= 0 and b.vmin > 0) or (a.vmax <= 0 and b.vmax < 0): return a.alu(Ops.CDIV, b)
|
||||
return a.alu(Ops.CDIV, b) - (a.alu(Ops.CMOD, b).ne(0) & (a<0).ne(b<0))
|
||||
return a.alu(Ops.CDIV, b) - (a.alu(Ops.CMOD, b).ne(0) & (a<0).ne(b<0)).cast(a.dtype)
|
||||
|
||||
def floormod_to_mod(a:UOp, b:UOp) -> UOp:
|
||||
if (a.vmin >= 0 and b.vmin > 0) or (a.vmax <= 0 and b.vmax < 0): return a.alu(Ops.CMOD, b)
|
||||
@@ -114,8 +114,8 @@ def get_late_rewrite_patterns(ops:tuple[Ops, ...], disable_fast_idiv:bool) -> Pa
|
||||
if Ops.CMPLT in ops:
|
||||
# These are late rewrites because simplex expects equalities to be a certain format
|
||||
pat += [
|
||||
((UPat.var("x", dtypes.sints) < UPat.cvar("c")).logical_not(), lambda x,c: c-1<x),
|
||||
((UPat.cvar("c") < UPat.var("x", dtypes.sints)).logical_not(), lambda x,c: x<c+1),
|
||||
((UPat.var("x", dtypes.sints) < UPat.cvar("c", dtypes.sints)).logical_not(), lambda x,c: c-1<x),
|
||||
((UPat.cvar("c", dtypes.sints) < UPat.var("x", dtypes.sints)).logical_not(), lambda x,c: x<c+1),
|
||||
(UPat.var("x", dtypes.sints)*-1 < UPat.var("y", dtypes.sints)*UPat.cvar("c"), lambda x,y,c: y*(-c)<x),
|
||||
(UPat.var("x", dtypes.sints)*-1 < UPat.cvar("c"), lambda x,c:-c<x),
|
||||
((UPat.cvar("c1")<UPat.var("x", dtypes.sints)) & (UPat.var("x", dtypes.sints)<UPat.cvar("c2")),
|
||||
@@ -129,5 +129,5 @@ def get_late_rewrite_patterns(ops:tuple[Ops, ...], disable_fast_idiv:bool) -> Pa
|
||||
# some backends emit FDIV for RECIP, in that case: a*(1/b) -> a/b
|
||||
if Ops.FDIV in ops:
|
||||
pat += [(UPat.var("x").reciprocal(), lambda x: x.const_like(1).alu(Ops.FDIV, x))]
|
||||
pat += [(UPat.var("a", dtypes.floats) * UPat(Ops.FDIV, dtypes.floats, src=(UPat.const(None, 1), UPat.var("b"))), lambda a,b: a.alu(Ops.FDIV, b))]
|
||||
pat += [(UPat.var("a", dtypes.floats) * UPat.const(dtypes.floats, 1).alu(Ops.FDIV, UPat.var("b")), lambda a,b: a.alu(Ops.FDIV, b))]
|
||||
return PatternMatcher(pat)
|
||||
|
||||
@@ -200,7 +200,7 @@ def xexp2(d:UOp) -> UOp:
|
||||
x = _lazy_map_numbers(d, d.const_like(0.0), d.const_like(0.0), d.const_like(0.0), d)
|
||||
q = rintk(x)
|
||||
# s = d - round(d)
|
||||
s = x - q
|
||||
s = x - q.cast(x.dtype)
|
||||
# a polynomial approximation with 13 non-zero terms in the range of [−(log 2)/2,(log 2)/2].
|
||||
if d.dtype == dtypes.float64:
|
||||
u = polyN(s, [0.4434359082926529454e-9, 0.7073164598085707425e-8, 0.1017819260921760451e-6, 0.1321543872511327615e-5, 0.1525273353517584730e-4,
|
||||
@@ -226,7 +226,7 @@ def xlog2(d:UOp) -> UOp:
|
||||
denormal_exp = 10 if d.dtype == dtypes.float16 else 64
|
||||
FLT_MIN = d.const_like({dtypes.float16: 6.1e-5, dtypes.float32: 1e-4, dtypes.float64: 1e-4}[d.dtype])
|
||||
is_denormal = d<FLT_MIN
|
||||
a = is_denormal.where(d * (2.0 ** denormal_exp), d)
|
||||
a = is_denormal.where(d * (2 ** denormal_exp), d)
|
||||
|
||||
e = ilogb2k(a * (1.0 / 0.75)).cast(a.dtype)
|
||||
m = ldexp3k(a, -e)
|
||||
|
||||
@@ -87,15 +87,7 @@ def add_gpudims(ctx:Renderer, s:UOp):
|
||||
except ValueError: continue
|
||||
return s.substitute(subs)
|
||||
|
||||
pm_device_to_var = PatternMatcher([
|
||||
# the DEVICE axis is not a program axis, it's bound per device at launch. lower it to the _device_num variable (like SPECIAL for devices)
|
||||
(UPat(Ops.RANGE, name="r"), lambda r: UOp.variable("_device_num", 0, r.vmax, dtype=r.dtype) if r.arg[-1] is AxisType.DEVICE else None),
|
||||
# ENDs that closed a DEVICE range no longer close it
|
||||
(UPat(Ops.END, name="e"), lambda e: e.replace(src=(e.src[0],)+tuple(s for s in e.src[1:] if s.op is not Ops.PARAM))
|
||||
if any(s.op is Ops.PARAM and s.arg.name == '_device_num' for s in e.src[1:]) else None),
|
||||
])
|
||||
|
||||
pm_add_gpudims = PatternMatcher([
|
||||
# add gpudims must be last
|
||||
(UPat(Ops.SINK, name="s"), add_gpudims),
|
||||
])+pm_device_to_var
|
||||
])
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user