mirror of
https://github.com/tinygrad/tinygrad.git
synced 2026-08-16 14:58:27 +00:00
Compare commits
170
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73331f4e55 | ||
|
|
fe25f98f49 | ||
|
|
4f91db06d7 | ||
|
|
1a4826f802 | ||
|
|
df23057984 | ||
|
|
5623cea7b1 | ||
|
|
759c7fc81c | ||
|
|
5ecfe549e7 | ||
|
|
e7e70a3c95 | ||
|
|
2d72a4a90c | ||
|
|
b5ebb4d06d | ||
|
|
abd830b260 | ||
|
|
4b42bb54aa | ||
|
|
01ddb4c267 | ||
|
|
c7f908b788 | ||
|
|
8dd691761d | ||
|
|
de043226ba | ||
|
|
5f6b610da1 | ||
|
|
529318259c | ||
|
|
7d025089e3 | ||
|
|
92c16810ac | ||
|
|
e3a0598d0b | ||
|
|
a9ea36de79 | ||
|
|
c35de9bd68 | ||
|
|
824ba4386a | ||
|
|
5dcf29b1a0 | ||
|
|
c70e8af068 | ||
|
|
d483e4153a | ||
|
|
62ee976c1b | ||
|
|
848f5cea96 | ||
|
|
14d1c5fdfd | ||
|
|
dfa180413d | ||
|
|
71f228f80f | ||
|
|
f80b1033c5 | ||
|
|
4008f7d4e8 | ||
|
|
dafbe9733a | ||
|
|
f7aeff6061 | ||
|
|
5ff278446c | ||
|
|
977c270774 | ||
|
|
3539693555 | ||
|
|
a4f6365929 | ||
|
|
8e8e9f6ff6 | ||
|
|
ccbbca05ef | ||
|
|
8cb4368967 | ||
|
|
efce99adc9 | ||
|
|
103ea16ec0 | ||
|
|
fe0fa8333b | ||
|
|
e3003631f2 | ||
|
|
cfc5cf65ad | ||
|
|
76170d035a | ||
|
|
cfb8e6922d | ||
|
|
9b3450c9da | ||
|
|
6bbf813dd3 | ||
|
|
77846300b2 | ||
|
|
dc54441e1f | ||
|
|
bb84e389cf | ||
|
|
9b4ba3f838 | ||
|
|
151608aa90 | ||
|
|
5fd06f4f02 | ||
|
|
db6b3e1edc | ||
|
|
c9f6d8751b | ||
|
|
b8a55d5f68 | ||
|
|
4e12fc3fe6 | ||
|
|
81a35cef38 | ||
|
|
1406d49eef | ||
|
|
ef1017f7ed | ||
|
|
ad99b77f6d | ||
|
|
010d2790ce | ||
|
|
3e1e12528c | ||
|
|
d23b79530e | ||
|
|
d345f7f5dc | ||
|
|
37e31e7da4 | ||
|
|
af94bfc401 | ||
|
|
2bbf8bbefa | ||
|
|
0f94a4bb73 | ||
|
|
3a4db53b43 | ||
|
|
d65db32395 | ||
|
|
c61fe57cfd | ||
|
|
88d650d606 | ||
|
|
1c09890f66 | ||
|
|
fe3ee8c27e | ||
|
|
12d179f5f4 | ||
|
|
2655655a0c | ||
|
|
94acd85285 | ||
|
|
e5c0db66d1 | ||
|
|
3244131f59 | ||
|
|
ed9d475a12 | ||
|
|
faa66e0a61 | ||
|
|
8983830aa8 | ||
|
|
0d35b67f2c | ||
|
|
35f85c393f | ||
|
|
421b1d4a56 | ||
|
|
448e997be4 | ||
|
|
c58e91942c | ||
|
|
68831cd852 | ||
|
|
d941dd5aeb | ||
|
|
e1c9985715 | ||
|
|
4a2fc7ecbb | ||
|
|
e3fa9896b7 | ||
|
|
fde7a40bb0 | ||
|
|
46d9a9a74f | ||
|
|
8dae9be573 | ||
|
|
9d9151a21e | ||
|
|
f68a472244 | ||
|
|
e5d27a3773 | ||
|
|
5fd4fc0c6d | ||
|
|
8a6dffc87e | ||
|
|
6f1cb6be86 | ||
|
|
b643fca51e | ||
|
|
8d9545e09e | ||
|
|
a36a26d4ed | ||
|
|
e2b1f2620d | ||
|
|
57ade7608a | ||
|
|
0bda5585c7 | ||
|
|
405d37423e | ||
|
|
77db8e1c07 | ||
|
|
127136421d | ||
|
|
0366474089 | ||
|
|
806581f807 | ||
|
|
d86f1d66b5 | ||
|
|
b824490e3f | ||
|
|
dd8302a6d0 | ||
|
|
25565b2410 | ||
|
|
d6145736c7 | ||
|
|
3acd763684 | ||
|
|
f45199269b | ||
|
|
677145b393 | ||
|
|
1538960002 | ||
|
|
226d4a2440 | ||
|
|
4424757b9a | ||
|
|
f9b7493e7a | ||
|
|
60f90dd97c | ||
|
|
ccfd878e0f | ||
|
|
24e8919438 | ||
|
|
acf8f6b287 | ||
|
|
9764e2561c | ||
|
|
6de15dc480 | ||
|
|
0dbcd764ad | ||
|
|
3cda781876 | ||
|
|
0255a64a27 | ||
|
|
8ef5544e4a | ||
|
|
cf23c2eee7 | ||
|
|
639224e6e1 | ||
|
|
d3b829a189 | ||
|
|
8278886cf9 | ||
|
|
06fb35a1e5 | ||
|
|
c5029fa460 | ||
|
|
6533250246 | ||
|
|
0c0d07d330 | ||
|
|
5b6fcd1cda | ||
|
|
ad3d821d63 | ||
|
|
df7774661a | ||
|
|
c9b706125d | ||
|
|
5ee654b0d9 | ||
|
|
815780f72f | ||
|
|
24286c5593 | ||
|
|
1fc1508f67 | ||
|
|
a4634b253a | ||
|
|
86e7804d60 | ||
|
|
aa905db7f7 | ||
|
|
f9536f3cd4 | ||
|
|
697d0b06c2 | ||
|
|
07d145debd | ||
|
|
d895713116 | ||
|
|
2611907afb | ||
|
|
1b3b94a72a | ||
|
|
55d3a5def9 | ||
|
|
dbf894215a | ||
|
|
4b9825c829 | ||
|
|
6610255654 |
@@ -233,7 +233,7 @@ runs:
|
||||
shell: bash
|
||||
run: |
|
||||
sudo mkdir -p /usr/local/lib
|
||||
curl -s -H "Authorization: token $GH_TOKEN" curl -s https://api.github.com/repos/nimlgen/amdcomgr_dylib/releases/latest | \
|
||||
curl -s -H "Authorization: token $GH_TOKEN" curl -s https://api.github.com/repos/tinygrad/amdcomgr_dylib/releases/latest | \
|
||||
jq -r '.assets[] | select(.name == "libamd_comgr.dylib").browser_download_url' | \
|
||||
sudo xargs curl -fL -o /usr/local/lib/libamd_comgr.dylib
|
||||
cargo build --release --manifest-path ./extra/remu/Cargo.toml
|
||||
|
||||
@@ -32,6 +32,7 @@ jobs:
|
||||
- name: Setup Environment
|
||||
uses: ./.github/actions/setup-tinygrad
|
||||
with:
|
||||
key: 'autogen'
|
||||
opencl: 'true'
|
||||
amd: 'true'
|
||||
cuda: 'true'
|
||||
@@ -81,6 +82,7 @@ jobs:
|
||||
- name: Setup Environment
|
||||
uses: ./.github/actions/setup-tinygrad
|
||||
with:
|
||||
key: 'autogen-mac'
|
||||
llvm: 'true'
|
||||
- name: Regenerate autogen files
|
||||
run: |
|
||||
@@ -110,6 +112,8 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Environment
|
||||
uses: ./.github/actions/setup-tinygrad
|
||||
with:
|
||||
key: 'autogen-comgr'
|
||||
- name: Install autogen support packages
|
||||
run: |
|
||||
wget https://repo.radeon.com/rocm/rocm.gpg.key -O - | gpg --dearmor | sudo tee /etc/apt/keyrings/rocm.gpg > /dev/null
|
||||
|
||||
@@ -21,6 +21,9 @@ jobs:
|
||||
# the 3 minute timeout should not be raised
|
||||
testmacpytest:
|
||||
name: Mac pytest
|
||||
env:
|
||||
CI: ""
|
||||
CAPTURE_PROCESS_REPLAY: "0"
|
||||
runs-on: [self-hosted, macOS]
|
||||
timeout-minutes: 3
|
||||
defaults:
|
||||
@@ -41,22 +44,14 @@ jobs:
|
||||
run: |
|
||||
echo "CACHEDB=/tmp/pytest-db-ci.db" >> $GITHUB_ENV
|
||||
rm -f /tmp/pytest-db-ci*
|
||||
# TODO: remove this step once all old caches are migrated
|
||||
- name: Migrate old huggingface cache (symlinks break onnxruntime 1.24+)
|
||||
run: |
|
||||
cd ~/Library/Caches/tinygrad/downloads/models 2>/dev/null || exit 0
|
||||
for old_dir in models--*; do
|
||||
[ -d "$old_dir" ] || continue
|
||||
repo_id=$(echo "$old_dir" | sed 's/models--//; s/--/\//g')
|
||||
snapshot=$(ls -1 "$old_dir/snapshots" 2>/dev/null | head -1)
|
||||
[ -n "$snapshot" ] || continue
|
||||
mkdir -p "$repo_id"
|
||||
cp -RLn "$old_dir/snapshots/$snapshot/"* "$repo_id/" 2>/dev/null || true
|
||||
done
|
||||
- name: Run pytest -nauto
|
||||
run: |
|
||||
source /tmp/tinygrad_pytest_ci/bin/activate
|
||||
pytest -nauto --durations=20
|
||||
- name: openpilot compile3 0.10.1 driving_vision
|
||||
run: FLOAT16=1 CL=1 IMAGE=2 python3.11 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/720392c9a5b986981fdbed1bb8c47a6c5573a50e/selfdrive/modeld/models/driving_vision.onnx
|
||||
- name: IMAGE=1 openpilot compile3 0.10.1 driving_vision
|
||||
run: FLOAT16=1 CL=1 IMAGE=1 python3.11 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/720392c9a5b986981fdbed1bb8c47a6c5573a50e/selfdrive/modeld/models/driving_vision.onnx
|
||||
|
||||
testmacbenchmark:
|
||||
name: Mac Benchmark
|
||||
@@ -343,7 +338,7 @@ jobs:
|
||||
- name: Run 10 CIFAR training steps
|
||||
run: BENCHMARK_LOG=cifar_10steps ASSERT_MIN_STEP_TIME=120 NV=1 STEPS=10 python3 examples/hlb_cifar10.py
|
||||
- name: Run 10 CIFAR training steps w HALF
|
||||
run: BENCHMARK_LOG=cifar_10steps_half ASSERT_MIN_STEP_TIME=110 NV=1 STEPS=10 DEFAULT_FLOAT=HALF python3 examples/hlb_cifar10.py
|
||||
run: BENCHMARK_LOG=cifar_10steps_half ASSERT_MIN_STEP_TIME=120 NV=1 STEPS=10 DEFAULT_FLOAT=HALF python3 examples/hlb_cifar10.py
|
||||
- name: Run 10 CIFAR training steps w BF16
|
||||
run: BENCHMARK_LOG=cifar_10steps_bf16 ASSERT_MIN_STEP_TIME=120 NV=1 STEPS=10 DEFAULT_FLOAT=BFLOAT16 python3 examples/hlb_cifar10.py
|
||||
# - name: Run 10 CIFAR training steps w winograd
|
||||
@@ -515,7 +510,7 @@ jobs:
|
||||
- name: Run 10 CIFAR training steps
|
||||
run: BENCHMARK_LOG=cifar_10steps ASSERT_MIN_STEP_TIME=200 AMD=1 STEPS=10 python3 examples/hlb_cifar10.py
|
||||
- name: Run 10 CIFAR training steps w HALF
|
||||
run: BENCHMARK_LOG=cifar_10steps_half ASSERT_MIN_STEP_TIME=200 AMD=1 STEPS=10 DEFAULT_FLOAT=HALF python3 examples/hlb_cifar10.py
|
||||
run: BENCHMARK_LOG=cifar_10steps_half ASSERT_MIN_STEP_TIME=230 AMD=1 STEPS=10 DEFAULT_FLOAT=HALF python3 examples/hlb_cifar10.py
|
||||
# - name: Run 10 CIFAR training steps w BF16
|
||||
# run: BENCHMARK_LOG=cifar_10steps_bf16 ASSERT_MIN_STEP_TIME=288 AMD=1 STEPS=10 DEFAULT_FLOAT=BFLOAT16 python3 examples/hlb_cifar10.py
|
||||
# TODO: too slow
|
||||
@@ -525,8 +520,9 @@ jobs:
|
||||
run: time BENCHMARK_LOG=cifar AMD=1 DEFAULT_FLOAT=HALF STEPS=1000 TARGET_EVAL_ACC_PCT=93.0 python3 examples/hlb_cifar10.py
|
||||
- name: Run full CIFAR training steps w 6 GPUS
|
||||
run: time BENCHMARK_LOG=cifar_6gpu AMD=1 DEFAULT_FLOAT=HALF STEPS=350 BS=1536 GPUS=6 TARGET_EVAL_ACC_PCT=93.0 python3 examples/hlb_cifar10.py
|
||||
- name: Test full tinyfs load
|
||||
run: TINYFS_ENDPOINT=10.0.52.11:6767 PYTHONPATH=. python extra/tinyfs/fetch_file.py --hash d734f5e3be9f1e9d863bfaa4fc6c1ef2 --len 175866113 --dest mapping.json --check
|
||||
# TODO: broken on some of the machines
|
||||
#- name: Test full tinyfs load
|
||||
# run: TINYFS_ENDPOINT=10.0.52.11:6767 PYTHONPATH=. python extra/tinyfs/fetch_file.py --hash d734f5e3be9f1e9d863bfaa4fc6c1ef2 --len 175866113 --dest mapping.json --check
|
||||
- name: Run process replay tests
|
||||
run: cp test/external/process_replay/process_replay.py ./process_replay.py && git fetch origin master && git -c advice.detachedHead=false checkout origin/master && PYTHONPATH=. python3 process_replay.py
|
||||
|
||||
@@ -621,6 +617,27 @@ jobs:
|
||||
- name: Run process replay tests
|
||||
run: cp test/external/process_replay/process_replay.py ./process_replay.py && git fetch origin master && git -c advice.detachedHead=false checkout origin/master && PYTHONPATH=. python3 process_replay.py
|
||||
|
||||
testcommausbgpubenchmark:
|
||||
name: UsbGPU Benchmark (comma)
|
||||
runs-on: [self-hosted, Linux, comma4]
|
||||
timeout-minutes: 20
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
- 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: openpilot compile3 0.10.1 driving_vision
|
||||
run: BENCHMARK_LOG=usbgpu_openpilot_0_10_1_vision PYTHONPATH="." DEV=AMD AMD_LLVM=1 AMD_IFACE=USB 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
|
||||
run: BENCHMARK_LOG=usbgpu_openpilot_0_10_1_vision_load_pickle PYTHONPATH="." DEV=AMD AMD_IFACE=USB ASSERT_MIN_LOAD_TIME=15 python3 examples/openpilot/load_pickle.py
|
||||
|
||||
testreddriverbenchmark:
|
||||
name: AM Benchmark
|
||||
runs-on: [self-hosted, Linux, tinyboxrandom]
|
||||
|
||||
+60
-19
@@ -244,6 +244,37 @@ jobs:
|
||||
- name: Run TYPED=1
|
||||
run: CHECK_OOB=0 DEV=CPU TYPED=1 python test/test_tiny.py
|
||||
|
||||
nulltest:
|
||||
name: Null Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Environment
|
||||
uses: ./.github/actions/setup-tinygrad
|
||||
with:
|
||||
key: unittest-13
|
||||
pydeps: "pillow ftfy regex pre-commit"
|
||||
deps: testing_unit
|
||||
llvm: 'true'
|
||||
amd: 'true'
|
||||
- name: Run NULL backend tests
|
||||
run: NULL=1 python -m pytest -n=auto test/null/ --durations=20
|
||||
- name: Run targetted tests on NULL backend
|
||||
run: NULL=1 python3 -m unittest test.backend.test_multitensor.TestMultiTensor.test_data_parallel_resnet_train_step
|
||||
# TODO: too slow
|
||||
# - name: Run SDXL on NULL backend
|
||||
# run: NULL=1 DEBUG=1 python3 examples/sdxl.py --seed 0 --noshow --timing --fakeweights
|
||||
- name: Run Clip tests for SD MLPerf on NULL backend
|
||||
run: NULL=1 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: EMULATE=AMD_RDNA4 NULL=1 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: NULL=1 python3 examples/llama.py --gen 1 --size 7B --shard 4 --prompt "Hello." --count 3 --temperature 0 --timing
|
||||
|
||||
unittest:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
@@ -268,20 +299,6 @@ jobs:
|
||||
run: |
|
||||
CPU=1 python test/null/test_device.py TestRunAsModule.test_module_runs
|
||||
CPU=1 python -m pytest -n=auto test/unit/ --durations=20
|
||||
- name: Run NULL backend tests
|
||||
run: NULL=1 python -m pytest -n=auto test/null/ --durations=20
|
||||
- name: Run targetted tests on NULL backend
|
||||
run: NULL=1 python3 -m unittest test.backend.test_multitensor.TestMultiTensor.test_data_parallel_resnet_train_step
|
||||
# TODO: too slow
|
||||
# - name: Run SDXL on NULL backend
|
||||
# run: NULL=1 DEBUG=1 python3 examples/sdxl.py --seed 0 --noshow --timing --fakeweights
|
||||
- name: Run Clip tests for SD MLPerf on NULL backend
|
||||
run: NULL=1 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: EMULATE=AMD_RDNA4 NULL=1 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: NULL=1 python3 examples/llama.py --gen 1 --size 7B --shard 4 --prompt "Hello." --count 3 --temperature 0 --timing
|
||||
- name: Run GC tests
|
||||
run: python test/external/external_uop_gc.py
|
||||
- name: External Benchmark Schedule
|
||||
@@ -644,15 +661,13 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install llvm-21 llvm-21-tools cloc
|
||||
- name: Install rocprof-trace-decoder
|
||||
run: sudo PYTHONPATH="." ./extra/sqtt/install_sqtt_decoder.py
|
||||
run: sudo PYTHONPATH="." ./extra/sqtt/install_rocprof_decoder.py
|
||||
- name: Run AMD renderer tests
|
||||
run: AMD_LLVM=0 python -m pytest -n=auto test/amd/ --durations 20
|
||||
- name: Run AMD renderer tests (AMD_LLVM=1)
|
||||
run: AMD_LLVM=1 python -m pytest -n=auto test/amd/ --durations 20
|
||||
- name: Run TestOps.test_add with SQTT
|
||||
run: |
|
||||
VIZ=-2 DEBUG=5 python3 test/backend/test_ops.py TestOps.test_add
|
||||
extra/sqtt/rgptool.py create "/tmp/profile.pkl.$USER" -o /tmp/gpu0.rgp
|
||||
- name: Run SQTT profiling tests
|
||||
run: PROFILE=1 SQTT=1 python3 -m pytest -n=auto test/amd/test_sqtt_profiler.py
|
||||
- name: Run AMD emulated tests on NULL backend
|
||||
env:
|
||||
AMD: 0
|
||||
@@ -664,6 +679,30 @@ jobs:
|
||||
- name: Run LLVM test
|
||||
run: AMD_LLVM=1 python test/device/test_amd_llvm.py
|
||||
|
||||
testmockam:
|
||||
name: Linux (am)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
AMD: 1
|
||||
MOCKGPU: 1
|
||||
AMD_IFACE: PCI
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Environment
|
||||
uses: ./.github/actions/setup-tinygrad
|
||||
with:
|
||||
key: mockam
|
||||
deps: testing_unit
|
||||
amd: 'true'
|
||||
- name: Run test_tiny on MOCKAM
|
||||
run: python test/test_tiny.py
|
||||
- name: Run test_tiny on MOCKAM USB
|
||||
run: AMD_IFACE=USB python test/test_tiny.py
|
||||
- name: Run test_hcq on MOCKAM
|
||||
run: python -m pytest test/device/test_hcq.py
|
||||
|
||||
testamd:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -802,6 +841,8 @@ jobs:
|
||||
run: METAL=1 DEBUG=3 python test/backend/test_ops.py TestOps.test_big_gemm
|
||||
- name: Test Beam Search
|
||||
run: METAL=1 IGNORE_BEAM_CACHE=1 python3 -m pytest extra/optimization/test_beam_search.py
|
||||
- name: Test Device Specific
|
||||
run: METAL=1 python3 -m pytest test/device/test_metal.py
|
||||
#- name: Fuzz Test linearizer
|
||||
# run: METAL=1 DEPTH=4 FUZZ_N=50 FUZZ_MAX_SIZE=1000000 python test/external/fuzz_linearizer.py
|
||||
- name: Run TRANSCENDENTAL math
|
||||
|
||||
@@ -66,3 +66,5 @@ target
|
||||
.mypy_cache
|
||||
mutants
|
||||
.mutmut-cache
|
||||
dagre/
|
||||
graphlib/
|
||||
|
||||
@@ -10,7 +10,7 @@ Directories are listed in order of how they are processed.
|
||||
|
||||
Group UOps into kernels.
|
||||
|
||||
::: tinygrad.schedule.rangeify.get_rangeify_map
|
||||
::: tinygrad.schedule.rangeify.get_kernel_graph
|
||||
options:
|
||||
members: false
|
||||
show_labels: false
|
||||
|
||||
@@ -254,8 +254,8 @@ def load_unet3d_data(preprocessed_dataset_dir, seed, queue_in, queue_out, X:Tens
|
||||
x = random_brightness_augmentation(x)
|
||||
x = gaussian_noise(x)
|
||||
|
||||
X[idx].contiguous().realize().uop.base.realized.as_memoryview(force_zero_copy=True)[:] = x.tobytes()
|
||||
Y[idx].contiguous().realize().uop.base.realized.as_memoryview(force_zero_copy=True)[:] = y.tobytes()
|
||||
X[idx].flatten().assign(x.tobytes())
|
||||
Y[idx].flatten().assign(y.tobytes())
|
||||
|
||||
queue_out.put(idx)
|
||||
queue_out.put(None)
|
||||
@@ -369,12 +369,12 @@ def load_retinanet_data(base_dir:Path, val:bool, queue_in:Queue, queue_out:Queue
|
||||
clipped_match_idxs = np.clip(match_idxs, 0, None)
|
||||
clipped_boxes, clipped_labels = tgt["boxes"][clipped_match_idxs], tgt["labels"][clipped_match_idxs]
|
||||
|
||||
boxes[idx].contiguous().realize().uop.base.realized.as_memoryview(force_zero_copy=True)[:] = clipped_boxes.tobytes()
|
||||
labels[idx].contiguous().realize().uop.base.realized.as_memoryview(force_zero_copy=True)[:] = clipped_labels.tobytes()
|
||||
matches[idx].contiguous().realize().uop.base.realized.as_memoryview(force_zero_copy=True)[:] = match_idxs.tobytes()
|
||||
anchors[idx].contiguous().realize().uop.base.realized.as_memoryview(force_zero_copy=True)[:] = anchor.tobytes()
|
||||
boxes[idx].flatten().assign(clipped_boxes.tobytes())
|
||||
labels[idx].flatten().assign(clipped_labels.tobytes())
|
||||
matches[idx].flatten().assign(match_idxs.tobytes())
|
||||
anchors[idx].flatten().assign(anchor.tobytes())
|
||||
|
||||
imgs[idx].contiguous().realize().uop.base.realized.as_memoryview(force_zero_copy=True)[:] = img.tobytes()
|
||||
imgs[idx].flatten().assign(img.tobytes())
|
||||
|
||||
queue_out.put(idx)
|
||||
queue_out.put(None)
|
||||
@@ -396,6 +396,7 @@ def batch_load_retinanet(dataset, val:bool, base_dir:Path, batch_size:int=32, sh
|
||||
queue_in.put((idx, img, tgt))
|
||||
|
||||
def _setup_shared_mem(shm_name:str, size:tuple[int, ...], dtype:dtypes) -> tuple[shared_memory.SharedMemory, Tensor]:
|
||||
shm_name = f"{shm_name}_{os.getpid()}"
|
||||
if os.path.exists(f"/dev/shm/{shm_name}"): os.unlink(f"/dev/shm/{shm_name}")
|
||||
shm = shared_memory.SharedMemory(name=shm_name, create=True, size=prod(size))
|
||||
shm_tensor = Tensor.empty(*size, dtype=dtype, device=f"disk:/dev/shm/{shm_name}")
|
||||
|
||||
@@ -3,7 +3,7 @@ from pathlib import Path
|
||||
import multiprocessing
|
||||
|
||||
from tinygrad import Device, GlobalCounters, Tensor, TinyJit, dtypes
|
||||
from tinygrad.helpers import getenv, BEAM, WINO, round_up, diskcache_clear, Profiling, profile_marker
|
||||
from tinygrad.helpers import getenv, BEAM, WINO, round_up, diskcache_clear, Profiling, profile_marker, DEBUG
|
||||
from tinygrad.nn.state import get_parameters, get_state_dict, load_state_dict, safe_load, safe_save
|
||||
from tinygrad.nn.optim import LAMB, LARS, SGD, OptimizerGroup, Adam, AdamW
|
||||
|
||||
@@ -1335,9 +1335,14 @@ def train_llama3():
|
||||
model_params = MODEL_PARAMS[getenv("LLAMA3_SIZE", "8B")]["args"]
|
||||
# vocab_size from the mixtral tokenizer
|
||||
if not SMALL: model_params |= {"vocab_size": 32000}
|
||||
real_vocab_size = model_params['vocab_size']
|
||||
if (llama_layers:=getenv("LLAMA_LAYERS")) != 0: model_params['n_layers'] = llama_layers
|
||||
print(f"model parameters: {model_params}")
|
||||
|
||||
# pad vocab
|
||||
if (MP := getenv("MP", 1)) > 1: model_params['vocab_size'] = round_up(model_params['vocab_size'], 256 * MP)
|
||||
vocab_mask:Tensor = Tensor.arange(model_params['vocab_size']).reshape(1, 1, -1) >= real_vocab_size
|
||||
|
||||
model = Transformer(**model_params, max_context=SEQLEN, jit=False, disable_kv_cache=True)
|
||||
params = get_parameters(model)
|
||||
# weights are all bfloat16 for now
|
||||
@@ -1352,6 +1357,8 @@ def train_llama3():
|
||||
for v in get_parameters(model):
|
||||
v.shard_(device, axis=None)
|
||||
|
||||
vocab_mask.shard_(device, axis=None)
|
||||
|
||||
if (MP := getenv("MP", 1)) > 1:
|
||||
device = tuple(f"{Device.DEFAULT}:{i}" for i in range(MP))
|
||||
for k,v in get_state_dict(model).items():
|
||||
@@ -1359,6 +1366,7 @@ def train_llama3():
|
||||
elif '.attention.wq' in k: v.shard_(device, axis=0)
|
||||
elif '.attention.wk' in k: v.shard_(device, axis=0)
|
||||
elif '.attention.wv' in k: v.shard_(device, axis=0)
|
||||
elif '.attention.wqkv' in k: v.shard_(device, axis=0)
|
||||
elif '.attention.wo' in k: v.shard_(device, axis=1)
|
||||
elif '.feed_forward.w1.' in k: v.shard_(device, axis=0)
|
||||
elif '.feed_forward.w2.' in k: v.shard_(device, axis=1)
|
||||
@@ -1371,13 +1379,18 @@ def train_llama3():
|
||||
# prevents memory spike on device 0
|
||||
v.realize()
|
||||
|
||||
optim = GradAccClipAdamW(get_parameters(model), 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)
|
||||
vocab_mask.shard_(device, axis=2).realize()
|
||||
|
||||
optim_device = "CPU" if getenv("OFFLOAD_OPTIM") else None
|
||||
optim = GradAccClipAdamW(get_parameters(model), 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)
|
||||
|
||||
# init grads
|
||||
for p in optim.params:
|
||||
p.grad = p.zeros_like().contiguous().realize()
|
||||
p.grad = p.empty_like().realize()
|
||||
grads: list[Tensor] = [p.grad for p in optim.params]
|
||||
for p in optim.params:
|
||||
p.grad.assign(p.grad.zeros_like()).realize()
|
||||
|
||||
scheduler = CosineAnnealingLRWithWarmup(optim, opt_base_learning_rate, opt_end_learning_rate, opt_learning_rate_warmup_steps, opt_learning_rate_decay_steps)
|
||||
|
||||
@@ -1392,15 +1405,15 @@ def train_llama3():
|
||||
|
||||
@TinyJit
|
||||
def minibatch(tokens:Tensor):
|
||||
tokens = tokens.to(None)
|
||||
if (DP := getenv("DP", 1)) > 1:
|
||||
device = tuple(f"{Device.DEFAULT}:{i}" for i in range(DP))
|
||||
tokens = tokens.shard(device, 0)
|
||||
tokens = tokens.to(None).shard(device, 0)
|
||||
if (MP := getenv("MP", 1)) > 1:
|
||||
device = tuple(f"{Device.DEFAULT}:{i}" for i in range(MP))
|
||||
tokens = tokens.shard(device)
|
||||
if DP == 1 and MP == 1: tokens = tokens.to(None)
|
||||
logits:Tensor = model(tokens[:, :-1], start_pos=0, temperature=math.nan)
|
||||
loss = logits.sparse_categorical_crossentropy(tokens[:, 1:])
|
||||
loss = vocab_mask.where(-1e9, logits).sparse_categorical_crossentropy(tokens[:, 1:])
|
||||
loss.backward()
|
||||
assert all(p.grad is g for p,g in zip(optim.params, grads))
|
||||
Tensor.realize(loss, *grads)
|
||||
@@ -1408,35 +1421,37 @@ def train_llama3():
|
||||
|
||||
@TinyJit
|
||||
def optim_step():
|
||||
optim.step()
|
||||
grad_norm = optim.fstep(grads)
|
||||
scheduler.step()
|
||||
|
||||
for g in grads:
|
||||
g.assign(g.zeros_like())
|
||||
g.assign(g.zeros_like()).realize()
|
||||
|
||||
lr = optim.lr
|
||||
Tensor.realize(lr, *grads)
|
||||
|
||||
return lr.float().to("CPU")
|
||||
return lr.float().to("CPU"), grad_norm.float().to("CPU")
|
||||
|
||||
@TinyJit
|
||||
@Tensor.train(False)
|
||||
def eval_step(tokens:Tensor):
|
||||
tokens = tokens.to(None)
|
||||
if (DP := getenv("DP", 1)) > 1:
|
||||
device = tuple(f"{Device.DEFAULT}:{i}" for i in range(DP))
|
||||
tokens = tokens.shard(device, 0)
|
||||
tokens = tokens.to(None).shard(device, 0)
|
||||
if (MP := getenv("MP", 1)) > 1:
|
||||
device = tuple(f"{Device.DEFAULT}:{i}" for i in range(MP))
|
||||
tokens = tokens.shard(device)
|
||||
if DP == 1 and MP == 1: tokens = tokens.to(None)
|
||||
logits:Tensor = model(tokens[:, :-1], start_pos=0, temperature=math.nan)
|
||||
loss = logits.sparse_categorical_crossentropy(tokens[:, 1:])
|
||||
loss = vocab_mask.where(-1e9, logits).sparse_categorical_crossentropy(tokens[:, 1:])
|
||||
return loss.flatten().float().to("CPU")
|
||||
|
||||
# ** data iters **
|
||||
def fake_data(bs, samples):
|
||||
import numpy as np
|
||||
for _ in range(samples // bs):
|
||||
yield Tensor.randint(bs, SEQLEN + 1, low=0, high=model_params["vocab_size"], dtype=dtypes.int32, device=Device.DEFAULT)
|
||||
fake_data_np = np.random.randint(0, model_params["vocab_size"], size=(bs, SEQLEN + 1), dtype=np.int32)
|
||||
yield Tensor(fake_data_np, device="NPY")
|
||||
|
||||
def get_train_iter():
|
||||
if getenv("FAKEDATA", 0):
|
||||
@@ -1468,29 +1483,29 @@ def train_llama3():
|
||||
st = time.perf_counter()
|
||||
|
||||
stopped = False
|
||||
for _ in range(grad_acc):
|
||||
losses, data_time, dev_time = [], 0, 0
|
||||
for _ in range(grad_acc if i >= 3 else 1):
|
||||
ist = time.perf_counter()
|
||||
try: tokens = next(train_iter)
|
||||
except StopIteration:
|
||||
stopped = True
|
||||
break
|
||||
dt = time.perf_counter()
|
||||
loss = minibatch(tokens)
|
||||
mst = time.perf_counter()
|
||||
data_time += mst - ist
|
||||
losses.append(minibatch(tokens).item())
|
||||
dev_time += time.perf_counter() - mst
|
||||
if stopped: break
|
||||
|
||||
gt = time.perf_counter()
|
||||
lr = optim_step()
|
||||
ot = time.perf_counter()
|
||||
|
||||
loss = loss.float().item()
|
||||
lr = lr.item()
|
||||
|
||||
ret = optim_step()
|
||||
lr, grad_norm = ret[0].item(), ret[1].item()
|
||||
et = time.perf_counter()
|
||||
|
||||
loss = sum(losses) / len(losses)
|
||||
optim_time = et - gt
|
||||
dev_time += optim_time
|
||||
step_time = et - st
|
||||
gbs_time = gt - st
|
||||
optim_time = ot - gt
|
||||
data_time = dt - ist
|
||||
dev_time = step_time - data_time * grad_acc
|
||||
if BENCHMARK: step_times.append(step_time)
|
||||
|
||||
i += 1
|
||||
@@ -1501,11 +1516,14 @@ def train_llama3():
|
||||
mfu = ((6 * num_params * SEQLEN * GBS) / (dev_time * max(getenv("DP", 1), getenv("MP", 1)) * 2.3e15)) * 100
|
||||
tqdm.write(
|
||||
f"{i:5} {step_time:.3f} s step, {gbs_time:.3f} s gbs, {optim_time:.3f} s optim, {data_time:.3f} s data, {loss:.4f} loss, " \
|
||||
f"{lr:.12f} LR, {mem_gb:.2f} GB used, {gflops:9.2f} GFLOPS, {mfu:5.2f}% MFU")
|
||||
f"{lr:.12f} LR, {grad_norm:.6f} grad_norm, {mem_gb:.2f} GB used, {gflops:9.2f} GFLOPS, {mfu:5.2f}% MFU")
|
||||
if DEBUG >= 1: tqdm.write(" mem per device: " + ', '.join(f"{dev}: {mem/1e9:.2f} GB" for dev, mem in sorted(GlobalCounters.mem_used_per_device.items())))
|
||||
|
||||
if WANDB:
|
||||
wandb.log({
|
||||
"lr": lr, "train/loss": loss,
|
||||
"train/loss": loss,
|
||||
"train/lr": lr,
|
||||
"train/grad_norm": grad_norm,
|
||||
"train/step_time": step_time,
|
||||
"train/gbs_time": gbs_time,
|
||||
"train/optim_time": optim_time,
|
||||
@@ -1542,7 +1560,7 @@ def train_llama3():
|
||||
# run eval
|
||||
eval_losses = []
|
||||
eval_iter = get_eval_iter()
|
||||
tqdm.write(f"evaluating {5760//EVAL_BS} batches of {EVAL_BS} sequences")
|
||||
tqdm.write(f"evaluating {EVAL_SAMPLES//EVAL_BS} batches of {EVAL_BS} sequences")
|
||||
|
||||
for j,tokens in tqdm(enumerate(eval_iter), total=EVAL_SAMPLES//EVAL_BS):
|
||||
eval_losses += eval_step(tokens).tolist()
|
||||
|
||||
+46
-14
@@ -1,24 +1,56 @@
|
||||
from tinygrad.tensor import Tensor
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.nn.optim import LAMB
|
||||
from tinygrad.nn.optim import Optimizer
|
||||
from tinygrad.helpers import FUSE_OPTIM
|
||||
|
||||
class GradAccClipAdamW(LAMB):
|
||||
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, fused=FUSE_OPTIM):
|
||||
super().__init__(params, lr, b1, b2, eps, weight_decay, adam=True, fused=FUSE_OPTIM)
|
||||
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)
|
||||
self.b1, self.b2, self.eps, self.wd = b1, b2, eps, weight_decay
|
||||
self.b1_t, self.b2_t = (Tensor.ones((1,), dtype=dtypes.float32, device=self.device, requires_grad=False) for _ in [b1, b2])
|
||||
self.m = self._new_optim_param()
|
||||
self.v = self._new_optim_param()
|
||||
self.grad_acc, self.clip_norm = grad_acc, clip_norm
|
||||
|
||||
def _step(self, params:list[Tensor], grads:list[Tensor]) -> tuple[list[Tensor], list[Tensor]]:
|
||||
def fstep(self, grads:list[Tensor]):
|
||||
if self.fused:
|
||||
grads[0] = grads[0] / self.grad_acc
|
||||
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]))
|
||||
to_realize = extra+self.params+self.buffers
|
||||
|
||||
Tensor.realize(*to_realize)
|
||||
return extra[-1]
|
||||
|
||||
def _step(self, params:list[Tensor], grads:list[Tensor]) -> tuple[list[Tensor], list[Tensor]]:
|
||||
for i in range(len(grads)):
|
||||
if grads[i].device != self.m[i].device: grads[i].assign(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] = (grads[0] * (self.clip_norm / (total_norm + 1e-6)).clamp(max_=1.0)).cast(grads[0].dtype)
|
||||
grads[0].assign((grads[0] * (self.clip_norm / (total_norm + 1e-6)).clamp(max_=1.0)).cast(grads[0].dtype))
|
||||
else:
|
||||
total_norm = Tensor.zeros((), dtype=dtypes.float32, device=self.device)
|
||||
for g in grads:
|
||||
total_norm += g.float().square().sum()
|
||||
total_norm = total_norm.sqrt()
|
||||
for i in range(len(grads)):
|
||||
grads[i] = grads[i] / self.grad_acc
|
||||
grads[i] = (grads[i] * (self.clip_norm / (total_norm + 1e-6)).clamp(max_=1.0)).cast(grads[i].dtype)
|
||||
return super()._step(params, grads)
|
||||
grads[i].assign(grads[i] / self.grad_acc).realize()
|
||||
total_norm = Tensor.stack(*[g.float().square().sum() for g in grads]).sum().sqrt().contiguous().realize()
|
||||
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)).realize()
|
||||
|
||||
ret = []
|
||||
self.b1_t *= self.b1
|
||||
self.b2_t *= self.b2
|
||||
for i, g in enumerate(grads):
|
||||
self.m[i].assign((self.b1 * self.m[i] + (1.0 - self.b1) * g).cast(self.m[i].dtype))
|
||||
self.v[i].assign((self.b2 * self.v[i] + (1.0 - self.b2) * (g * g)).cast(self.v[i].dtype))
|
||||
m_hat = self.m[i] / (1.0 - self.b1_t)
|
||||
v_hat = self.v[i] / (1.0 - self.b2_t)
|
||||
up = m_hat / (v_hat.sqrt() + self.eps)
|
||||
ret.append((self.lr * up).cast(g.dtype))
|
||||
return ret, [self.b1_t, self.b2_t] + self.m + self.v + [total_norm]
|
||||
|
||||
def _apply_update(self, t:Tensor, up:Tensor) -> Tensor:
|
||||
up = up.shard_like(t) + self.lr.to(t.device) * self.wd * t.detach()
|
||||
return t.detach() - up.cast(t.dtype)
|
||||
|
||||
+1
@@ -5,6 +5,7 @@ export DEV=${DEV:-AMD}
|
||||
export EMULATE="AMD_CDNA4"
|
||||
export CHECK_OOB=0
|
||||
export REWRITE_STACK_LIMIT=5000000 HCQDEV_WAIT_TIMEOUT_MS=240000
|
||||
export DEVICE_IN_FUNCTION_BUG=1
|
||||
|
||||
export DEBUG=${DEBUG:-0}
|
||||
export HK_FLASH_ATTENTION=${HK_FLASH_ATTENTION:-1}
|
||||
|
||||
+1
-1
@@ -3,4 +3,4 @@ export BENCHMARK=5
|
||||
export EVAL_BS=0
|
||||
export VIZ=${VIZ:--1}
|
||||
examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama8b/implementations/tinybox_8xMI350X/dev_run.sh
|
||||
PYTHONPATH="." extra/viz/cli.py --profile --device "AMD" --top 20
|
||||
extra/viz/cli.py --profile --device "AMD" --top 20
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import sys, pickle
|
||||
from extra.bench_log import WallTimeEvent, BenchEvent
|
||||
from tinygrad.helpers import getenv
|
||||
|
||||
PKL = sys.argv[1] if len(sys.argv) > 1 else "/tmp/openpilot.pkl"
|
||||
|
||||
load_times = []
|
||||
|
||||
for _ in range(10):
|
||||
with WallTimeEvent(BenchEvent.STEP) as wte: pickle.load(open(PKL, 'rb'))
|
||||
load_times.append(wte.time)
|
||||
print(f"pickle load: {wte.time:6.2f} s")
|
||||
|
||||
if (assert_time:=getenv("ASSERT_MIN_LOAD_TIME")):
|
||||
min_time = min(load_times)
|
||||
assert min_time < assert_time, f"Speed regression, expected min load time of < {assert_time} s but took: {min_time} s"
|
||||
+2
-1
@@ -34,7 +34,8 @@ class WallTimeEvent:
|
||||
self.start = time.monotonic()
|
||||
return self
|
||||
def __exit__(self, *_):
|
||||
_events[self.event]["wall"].append(time.monotonic() - self.start)
|
||||
self.time = time.monotonic() - self.start
|
||||
_events[self.event]["wall"].append(self.time)
|
||||
return False
|
||||
|
||||
class KernelTimeEvent:
|
||||
|
||||
+674
-9576
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ from tinygrad import Tensor, Device, dtypes
|
||||
from tinygrad.dtype import AddrSpace
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo, AxisType
|
||||
from tinygrad.renderer import Estimates
|
||||
from tinygrad.helpers import getenv, all_same, dedup
|
||||
from tinygrad.helpers import getenv, all_same, DEBUG
|
||||
from extra.gemm.asm.cdna.asm import build_kernel, TILE_M, TILE_N, TILE_K, NUM_WG
|
||||
|
||||
# ** CDNA4 assembly gemm
|
||||
@@ -11,12 +11,12 @@ from extra.gemm.asm.cdna.asm import build_kernel, TILE_M, TILE_N, TILE_K, NUM_WG
|
||||
WORKGROUP_SIZE = 256
|
||||
|
||||
@functools.cache
|
||||
def custom_asm_gemm(C:UOp, A:UOp, B:UOp, dname:str, arch:str, wg:int) -> UOp:
|
||||
def custom_asm_gemm(C:UOp, A:UOp, B:UOp, dname:str) -> UOp:
|
||||
batch, M, K = A.shape
|
||||
K2, N = B.shape[(1 if B.ndim == 3 else 0):]
|
||||
assert K == K2
|
||||
lidx = UOp.special(WORKGROUP_SIZE, "lidx0")
|
||||
gidx = UOp.special(wg, "gidx0")
|
||||
gidx = UOp.special(NUM_WG, "gidx0")
|
||||
insts = build_kernel(batch, M, N, K, A.dtype.base)
|
||||
lds = UOp(Ops.DEFINE_LOCAL, dtypes.uint8.ptr(size=133_120, addrspace=AddrSpace.LOCAL), (), 'lds')
|
||||
sink = UOp.sink(C.base, A.base, B.base, lds, lidx, gidx,
|
||||
@@ -26,17 +26,25 @@ def custom_asm_gemm(C:UOp, A:UOp, B:UOp, dname:str, arch:str, wg:int) -> UOp:
|
||||
|
||||
counters = {"used":0, "todos":[]}
|
||||
def todo(msg:str) -> bool: counters["todos"].append(msg); return False
|
||||
atexit.register(lambda: print(f'asm_gemm: {counters["used"]} used, {len(counters["todos"])} not used'))
|
||||
def _asm_gemm_report():
|
||||
print(f'asm_gemm: {counters["used"]} used, {len(counters["todos"])} not used')
|
||||
if DEBUG >= 2 and counters["todos"]:
|
||||
from collections import Counter
|
||||
for msg, cnt in Counter(counters["todos"]).most_common(): print(f' {cnt:3d}x {msg}')
|
||||
atexit.register(_asm_gemm_report)
|
||||
|
||||
def can_use_asm_gemm(a:Tensor, b:Tensor) -> bool:
|
||||
if a.dtype != b.dtype: return todo(f"dtypes must match {a.dtype} != {b.dtype}")
|
||||
if a.dtype not in {dtypes.bfloat16, dtypes.float16}: return todo(f"only bfloat16/float16, got {a.dtype}")
|
||||
batch, M, K = (1, *a.shape) if a.ndim == 2 else a.shape
|
||||
N = b.shape[1]
|
||||
# only sharding on the batch or K is tested, others might work too
|
||||
if isinstance(a.device, tuple):
|
||||
if a.ndim == 2 and a.uop.axis == 1 and b.uop.axis == 0: K //= len(a.device)
|
||||
if a.ndim == 2 and a.uop.axis == 0 and b.uop.axis is None: M //= len(a.device)
|
||||
elif a.ndim == 2 and a.uop.axis == 1 and b.uop.axis == 0: K //= len(a.device)
|
||||
elif a.ndim == 2 and a.uop.axis is None and b.uop.axis == 1: N //= len(a.device)
|
||||
elif a.ndim == 3 and a.uop.axis == 0 and b.uop.axis is None: batch //= len(a.device)
|
||||
elif a.ndim == 3 and a.uop.axis is None and b.uop.axis == 1: N //= len(a.device)
|
||||
elif a.ndim == 3 and a.uop.axis == 2 and b.uop.axis == 0: K //= len(a.device)
|
||||
else: return todo(f"sharding mismatch a.ndim={a.ndim} a.uop.axis={a.uop.axis} b.uop.axis={b.uop.axis}")
|
||||
dname = a.device[0]
|
||||
else: dname = a.device
|
||||
@@ -78,6 +86,10 @@ def custom_gemm_bw(gradient:UOp, kernel:UOp):
|
||||
def asm_gemm(a:Tensor, b:Tensor) -> Tensor:
|
||||
assert can_use_asm_gemm(a, b), f"{counters['todos'][-1]}"
|
||||
counters["used"] += 1
|
||||
unfold_batch = a.ndim == 3 and isinstance(a.device, tuple) and a.uop.axis == 2 and b.uop.axis == 0
|
||||
if unfold_batch:
|
||||
orig_batch = a.shape[0]
|
||||
a = a.reshape(a.shape[0]*a.shape[1], a.shape[2])
|
||||
squeeze = a.ndim == 2
|
||||
if squeeze: a = a.unsqueeze(0)
|
||||
|
||||
@@ -85,17 +97,26 @@ def asm_gemm(a:Tensor, b:Tensor) -> Tensor:
|
||||
N = b.shape[1]
|
||||
is_multi = isinstance(a.device, tuple)
|
||||
if (k_sharded:=is_multi and a.uop.axis == 2): K //= len(a.device)
|
||||
if (m_sharded:=is_multi and a.uop.axis == 1): M //= len(a.device)
|
||||
n_sharded = is_multi and b.uop.axis == 1
|
||||
|
||||
if is_multi:
|
||||
out = Tensor(Tensor.empty(batch//len(a.device) if a.uop.axis==0 else batch, M, N, dtype=a.dtype, device=a.device).uop.multi(0), device=a.device)
|
||||
if n_sharded:
|
||||
out = Tensor(Tensor.empty(batch, M, N//len(a.device), dtype=a.dtype, device=a.device).uop.multi(2), device=a.device)
|
||||
elif m_sharded:
|
||||
out = Tensor(Tensor.empty(batch, M, N, dtype=a.dtype, device=a.device).uop.multi(1), device=a.device)
|
||||
else:
|
||||
out = Tensor(Tensor.empty(batch//len(a.device) if a.uop.axis==0 else batch, M, N, dtype=a.dtype, device=a.device).uop.multi(0), device=a.device)
|
||||
else:
|
||||
out = Tensor.empty(batch, M, N, dtype=a.dtype, device=a.device)
|
||||
|
||||
renderer = Device[a.device[0] if is_multi else a.device].renderer
|
||||
dname, arch = renderer.device, getattr(renderer, "arch", "")
|
||||
if arch.startswith("gfx950") and getenv("USE_ASM", 1):
|
||||
out = Tensor.custom_kernel(out, a, b, fxn=functools.partial(custom_asm_gemm, dname=dname, wg=NUM_WG, arch=arch), grad_fxn=custom_gemm_bw)[0]
|
||||
out = Tensor.custom_kernel(out, a, b, fxn=functools.partial(custom_asm_gemm, dname=dname), grad_fxn=custom_gemm_bw)[0]
|
||||
else:
|
||||
out = Tensor.custom_kernel(out, a, b, fxn=custom_uop_gemm, grad_fxn=custom_gemm_bw)[0]
|
||||
if k_sharded: out = out.sum(0)
|
||||
return out.squeeze(0) if squeeze else out
|
||||
out = out.squeeze(0) if squeeze else out
|
||||
if unfold_batch: out = out.reshape(orig_batch, -1, out.shape[-1])
|
||||
return out
|
||||
|
||||
@@ -56,7 +56,10 @@ class Attention:
|
||||
def __call__(self, x:Tensor, start_pos:Union[Variable,int], freqs_cis:Tensor, mask:Optional[Tensor]=None) -> Tensor:
|
||||
if getenv("WQKV"):
|
||||
xqkv = self.wqkv(x)
|
||||
xq, xk, xv = xqkv.split([self.n_heads * self.head_dim, self.n_kv_heads * self.head_dim, self.n_kv_heads * self.head_dim], dim=2)
|
||||
xqkv = xqkv.reshape(xqkv.shape[0], xqkv.shape[1], self.n_kv_heads, self.n_rep + 2, self.head_dim)
|
||||
xq = xqkv[:, :, :, :self.n_rep].reshape(xqkv.shape[0], xqkv.shape[1], -1)
|
||||
xk = xqkv[:, :, :, self.n_rep:self.n_rep+1].reshape(xqkv.shape[0], xqkv.shape[1], -1)
|
||||
xv = xqkv[:, :, :, self.n_rep+1:self.n_rep+2].reshape(xqkv.shape[0], xqkv.shape[1], -1)
|
||||
else:
|
||||
xq, xk, xv = self.wq(x), self.wk(x.contiguous_backward()), self.wv(x)
|
||||
|
||||
|
||||
+6
-23
@@ -2,30 +2,13 @@
|
||||
|
||||
## Getting SQ Thread Trace
|
||||
|
||||
SQTT is implemented on top of normal tinygrad profiling, `VIZ=1 SQTT=1` to get profile pickle with sqtt data embedded in it.
|
||||
`VIZ=2` to enable SQTT profiling.
|
||||
|
||||
`SQTT_ITRACE_SE_MASK=X` to select shader engines for instruction tracing, -1 = all, 0 = disabled, >0 = SE bitmask, default 0b11.
|
||||
|
||||
`SQTT_BUFFER_SIZE=X` to change size of SQTT buffer (per shader engine, 6 SEs on 7900xtx) in megabytes, default 256.
|
||||
|
||||
`SQTT_ITRACE_SE_MASK=X` to select for which shader engines instruction tracing will be enabled, -1 is all, 0 is none (instruction tracing disabled), >0 is
|
||||
bitfield/mask for SEs to enable instruction tracing on. Masking shader engines will give smaller file sizes at a cost of less hits and kernels that
|
||||
don't have any wavefront on first simd of shader engine with instruction tracing enabled will not have instruction timings.
|
||||
The default is 2 (second shader engine only), only one for file size reasons, second instead of first because dispatch starts from it so there is
|
||||
greater chance that kernels with small global size will have instruction tracing data.
|
||||
|
||||
Note that instruction tracing might not be available for kernels with small global dims, this is not a bug, but it can be improved with various hacks
|
||||
to the point where it can reliably trace a kernel consisting of a single wavefront (am only, not quite reliable under amdgpu due to waves sometimes
|
||||
being dispatched starting from different simds). More info in comments in ops_amd.py
|
||||
## Viewing the traces
|
||||
|
||||
## Converting pickled profile with SQTT data into RGP file
|
||||
|
||||
```bash
|
||||
extra/sqtt/rgptool.py create "/tmp/profile.pkl.$USER" -o /tmp/gpu0.rgp
|
||||
```
|
||||
|
||||
Then load gpu0.rgp into Radeon GPU Profiler. It works just fine both in wine (macos, native version available for linux) and via ssh X forwarding
|
||||
|
||||
If multiple gpus are used you can select which one to export with `-d` like this:
|
||||
|
||||
```bash
|
||||
extra/sqtt/rgptool.py create "/tmp/profile.pkl.$USER" -d 'AMD:5' -o /tmp/gpu5.rgp
|
||||
```
|
||||
- Web UI: `tinygrad/viz/serve.py`
|
||||
- Command line: `python -m tinygrad.renderer.amd.sqtt`
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
import os
|
||||
os.environ["PYTHONPATH"] = "."
|
||||
os.environ["SQTT"] = "1"
|
||||
if "DEV" not in os.environ: os.environ["DEV"] = "AMD"
|
||||
os.environ["PROFILE"] = "1"
|
||||
os.environ["AMD_LLVM"] = "0"
|
||||
|
||||
from dataclasses import replace
|
||||
import atexit, contextlib
|
||||
from tinygrad import Tensor
|
||||
from tinygrad.helpers import system, OSX
|
||||
from tinygrad.runtime.ops_amd import AMDProgram
|
||||
from extra.sqtt.roc import decode, WaveExec, ProfileSQTTEvent
|
||||
from tinygrad.device import Device
|
||||
|
||||
from extra.sqtt.attempt_sqtt_parse import parse_sqtt_print_packets
|
||||
|
||||
dev = Device["AMD"]
|
||||
|
||||
@contextlib.contextmanager
|
||||
def save_sqtt():
|
||||
# clear the old traces
|
||||
dev.profile_events.clear()
|
||||
sqtt:dict[str, list[WaveExec]] = {}
|
||||
yield sqtt
|
||||
events = dev.profile_events
|
||||
|
||||
#rctx = decode(events)
|
||||
#assert len(rctx.inst_execs) > 0, "empty sqtt output"
|
||||
#sqtt.update(rctx.inst_execs)
|
||||
|
||||
for e in events:
|
||||
if isinstance(e, ProfileSQTTEvent):
|
||||
print(replace(e, blob=b''))
|
||||
if e.se == 0:
|
||||
parse_sqtt_print_packets(e.blob)
|
||||
|
||||
template = """.text
|
||||
.globl matmul
|
||||
.p2align 8
|
||||
.type matmul,@function
|
||||
matmul:
|
||||
INSTRUCTION
|
||||
|
||||
.rodata
|
||||
.p2align 6
|
||||
.amdhsa_kernel matmul
|
||||
.amdhsa_kernarg_size 8
|
||||
.amdhsa_user_sgpr_kernarg_segment_ptr 1
|
||||
.amdhsa_next_free_vgpr .amdgcn.next_free_vgpr
|
||||
.amdhsa_next_free_sgpr .amdgcn.next_free_sgpr
|
||||
.amdhsa_wavefront_size32 1
|
||||
.end_amdhsa_kernel
|
||||
|
||||
.amdgpu_metadata
|
||||
---
|
||||
amdhsa.version:
|
||||
- 1
|
||||
- 0
|
||||
amdhsa.kernels:
|
||||
- .name: matmul
|
||||
.symbol: matmul.kd
|
||||
.group_segment_fixed_size: 0
|
||||
.private_segment_fixed_size: 0
|
||||
.wavefront_size: 32
|
||||
.sgpr_count: 8
|
||||
.vgpr_count: 8
|
||||
.max_flat_workgroup_size: 1024
|
||||
.kernarg_segment_align: 8
|
||||
.kernarg_segment_size: 8
|
||||
.args:
|
||||
- .address_space: global
|
||||
.name: a
|
||||
.offset: 0
|
||||
.size: 8
|
||||
.type_name: 'float*'
|
||||
.value_kind: global_buffer
|
||||
...
|
||||
.end_amdgpu_metadata
|
||||
"""
|
||||
|
||||
def run_asm(src, num_workgroups=1, num_waves=1):
|
||||
WAVE_SIZE = 32
|
||||
t = Tensor.empty(0x1000).realize()
|
||||
buf = t.uop.buffer.ensure_allocated()
|
||||
lib = dev.compiler.compile(template.replace("INSTRUCTION", '\n'.join(src)))
|
||||
dev.compiler.disassemble(lib)
|
||||
fxn = AMDProgram(dev, "matmul", lib)
|
||||
fxn(buf._buf, global_size=(num_workgroups,1,1), local_size=(WAVE_SIZE*num_waves,1,1), wait=True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
with save_sqtt() as sqtt:
|
||||
run_asm([
|
||||
"s_nop 100",
|
||||
"s_nop 100",
|
||||
"s_load_b64 s[0:1], s[0:1], null",
|
||||
"s_waitcnt lgkmcnt(0)",
|
||||
"s_nop 100",
|
||||
"s_nop 100",
|
||||
"s_add_i32 s2, s2, 10",
|
||||
"s_add_i32 s2, s2, 10",
|
||||
"s_nop 100",
|
||||
"s_nop 100",
|
||||
"v_mov_b32_e32 v0, 0",
|
||||
"v_mov_b32_e32 v0, 0",
|
||||
"s_nop 100",
|
||||
"s_nop 100",
|
||||
"v_dual_fmac_f32 v2, v48, v24 :: v_dual_fmac_f32 v9, v37, v51",
|
||||
"v_dual_fmac_f32 v2, v48, v24 :: v_dual_fmac_f32 v9, v37, v51",
|
||||
"s_nop 100",
|
||||
"s_nop 100",
|
||||
"global_load_b128 v[2:5], v0, s[0:1]",
|
||||
"global_load_b128 v[2:5], v0, s[0:1]",
|
||||
"s_nop 100",
|
||||
"s_nop 100",
|
||||
"s_sendmsg sendmsg(MSG_DEALLOC_VGPRS)",
|
||||
"s_endpgm",
|
||||
], num_workgroups=1, num_waves=1)
|
||||
exit(0)
|
||||
|
||||
with save_sqtt() as sqtt:
|
||||
#(Tensor.empty(16,16) @ Tensor.empty(16,16)).elu().realize()
|
||||
#Tensor.empty(1, 64).sum(axis=1).realize()
|
||||
Tensor.empty(1).log2().realize()
|
||||
exit(0)
|
||||
|
||||
with save_sqtt() as sqtt:
|
||||
# what's in v0?
|
||||
run_asm([
|
||||
"v_mov_b32_e32 v0, 0",
|
||||
"v_mov_b32_e32 v1, 0",
|
||||
"s_clause 0x1",
|
||||
"s_load_b64 s[0:1], s[0:1], null",
|
||||
"s_waitcnt lgkmcnt(0)",
|
||||
]+[
|
||||
"global_load_b32 v1, v0, s[0:1]",
|
||||
]*10+[
|
||||
"global_load_b32 v10, v1, s[0:1]",
|
||||
"s_waitcnt vmcnt(0)",
|
||||
|
||||
#"v_rcp_f32 v1, v0"
|
||||
#"v_add_f32_e32 v1 v0 v0",
|
||||
#"v_add_f32_e32 v5 v4 v4",
|
||||
#"v_add_f32_e32 v7 v6 v6",
|
||||
#"v_add_f32_e32 v1 v0 v0",
|
||||
#"v_add_f32_e32 v2 v1 v1",
|
||||
#"s_nop 1"
|
||||
]*5+[
|
||||
"v_add_f32_e32 v3 v2 v2",
|
||||
]*5+[
|
||||
"v_mul_f32_e32 v3 v2 v2",
|
||||
]*7)
|
||||
@@ -1,548 +0,0 @@
|
||||
import pickle, sys
|
||||
from tinygrad.helpers import getenv, Timing, colored
|
||||
from extra.sqtt.roc import decode, ProfileSQTTEvent
|
||||
|
||||
# do these enums match fields in the packets?
|
||||
#from tinygrad.runtime.support.amd import import_soc
|
||||
#soc = import_soc([11])
|
||||
#perf_sel = {getattr(soc, k):k for k in dir(soc) if k.startswith("SQ_PERF_")}
|
||||
|
||||
# Instruction packets (one per ISA op)
|
||||
# NOTE: these are bad guesses and may be wrong! feel free to update if you know better
|
||||
# some names were taken from SQ_TT_TOKEN_MASK_TOKEN_EXCLUDE_SHIFT
|
||||
|
||||
# we see 18 opcodes
|
||||
# opcodes(18): 1 2 3 4 5 6 8 9 F 10 11 12 14 15 16 17 18 19
|
||||
# if you exclude everything, you are left with 6
|
||||
# opcodes( 6): 10 11 14 15 16 17
|
||||
# sometimes we see a lot of B, but not repeatable
|
||||
|
||||
# not seen
|
||||
# 7 A C
|
||||
|
||||
# NOTE: INST runs before EXEC
|
||||
|
||||
OPCODE_COLORS = {
|
||||
# dispatches are BLACK
|
||||
0x1: "BLACK",
|
||||
0x18: "BLACK",
|
||||
|
||||
# execs are yellow
|
||||
0x2: "yellow",
|
||||
0x3: "yellow",
|
||||
0x4: "YELLOW",
|
||||
0x5: "YELLOW",
|
||||
|
||||
# waves are blue
|
||||
0x8: "blue",
|
||||
0x9: "blue",
|
||||
0x6: "cyan",
|
||||
0xb: "cyan",
|
||||
}
|
||||
|
||||
OPCODE_NAMES = {
|
||||
# gated by SQ_TT_TOKEN_EXCLUDE_VALUINST_SHIFT (but others must be enabled for it to show)
|
||||
0x01: "VALUINST",
|
||||
# gated by SQ_TT_TOKEN_EXCLUDE_VMEMEXEC_SHIFT
|
||||
0x02: "VMEMEXEC",
|
||||
# gated by SQ_TT_TOKEN_EXCLUDE_ALUEXEC_SHIFT
|
||||
0x03: "ALUEXEC",
|
||||
# gated by SQ_TT_TOKEN_EXCLUDE_IMMEDIATE_SHIFT
|
||||
0x04: "IMMEDIATE",
|
||||
0x05: "IMMEDIATE_MASK",
|
||||
|
||||
# gated by SQ_TT_TOKEN_EXCLUDE_WAVERDY_SHIFT
|
||||
0x06: "WAVERDY",
|
||||
# gated by SQ_TT_TOKEN_EXCLUDE_WAVESTARTEND_SHIFT
|
||||
0x08: "WAVEEND",
|
||||
0x09: "WAVESTART",
|
||||
# gated by SQ_TT_TOKEN_EXCLUDE_WAVEALLOC_SHIFT
|
||||
0x0B: "WAVEALLOC", # FFF00
|
||||
|
||||
# gated by NOT SQ_TT_TOKEN_EXCLUDE_PERF_SHIFT
|
||||
0x0D: "PERF",
|
||||
# gated by SQ_TT_TOKEN_EXCLUDE_EVENT_SHIFT
|
||||
0x12: "EVENT",
|
||||
0x13: "EVENT_BIG", # FFFFF800
|
||||
# some gated by SQ_TT_TOKEN_EXCLUDE_REG_SHIFT, some always there. something is broken with the timing on this
|
||||
0x14: "REG",
|
||||
# gated by SQ_TT_TOKEN_EXCLUDE_INST_SHIFT
|
||||
0x18: "INST",
|
||||
# gated by SQ_TT_TOKEN_EXCLUDE_UTILCTR_SHIFT
|
||||
0x19: "UTILCTR",
|
||||
|
||||
# this is the first (8 byte) packet in the bitstream
|
||||
0x17: "LAYOUT_HEADER", # layout/mode/group + selectors A/B (reversed)
|
||||
|
||||
# pure time (no extra bits)
|
||||
0x0F: "TS_DELTA_SHORT",
|
||||
0x10: "NOP",
|
||||
0x11: "TS_WAVE_STATE", # almost pure time, has a small flag
|
||||
|
||||
# not a good name, but seen and understood mostly
|
||||
0x15: "SNAPSHOT", # small delta + 50-ish bits of snapshot
|
||||
0x16: "TS_DELTA_OR_MARK", # 36-bit long delta or 36-bit marker
|
||||
|
||||
# packets we haven't seen / rarely see 0x0b
|
||||
0x07: "TS_DELTA_S8_W3_7", # shift=8, width=3 (small delta)
|
||||
0x0A: "TS_DELTA_S5_W2_A", # shift=5, width=2
|
||||
0x0C: "TS_DELTA_S5_W3_B", # shift=5, width=3 (different consumer)
|
||||
}
|
||||
|
||||
# SALU = 0x0 / s_mov_b32
|
||||
# SMEM = 0x1 / s_load_b*
|
||||
# JUMP = 0x3 / s_cbranch_scc0
|
||||
# NEXT = 0x4 / s_cbranch_execz
|
||||
# MESSAGE = 0x9 / s_sendmsg
|
||||
# VALU = 0xb / v_(exp,log)_f32_e32
|
||||
# VALU = 0xd / v_lshlrev_b64
|
||||
# VALU = 0xe / v_mad_u64_u32
|
||||
# VMEM = 0x21 / global_load_b32
|
||||
# VMEM = 0x22 / global_load_b32
|
||||
# VMEM = 0x24 / global_store_b32
|
||||
# VMEM = 0x25 / global_store_b64
|
||||
# VMEM = 0x27 / global_store
|
||||
# VMEM = 0x28 / global_store_b64
|
||||
# LDS = 0x29 / ds_load_b128
|
||||
# LDS = 0x2b / ds_store_b32
|
||||
# LDS = 0x2e / ds_store_b128
|
||||
# ???? = 0x5a / hidden global_load instruction
|
||||
# ???? = 0x5b / hidden global_load instruction
|
||||
# ???? = 0x5c / hidden global_store instruction
|
||||
# VALU = 0x73 / v_cmpx_eq_u32_e32 (not normal VALUINST)
|
||||
OPNAME = {
|
||||
0x0: "SALU",
|
||||
0x1: "SMEM",
|
||||
0x3: "JUMP",
|
||||
0x4: "NEXT",
|
||||
0x9: "MESSAGE",
|
||||
0xb: "VALU",
|
||||
0xd: "VALU",
|
||||
0xe: "VALU",
|
||||
0x21: "VMEM_LOAD",
|
||||
0x22: "VMEM_LOAD",
|
||||
0x24: "VMEM_STORE",
|
||||
0x25: "VMEM_STORE",
|
||||
0x26: "VMEM_STORE",
|
||||
0x27: "VMEM_STORE",
|
||||
0x28: "VMEM_STORE",
|
||||
0x29: "LDS_LOAD",
|
||||
0x2b: "LDS_STORE",
|
||||
0x2e: "LDS_STORE",
|
||||
0x50: "__SIMD_LDS_LOAD",
|
||||
0x51: "__SIMD_LDS_LOAD",
|
||||
0x54: "__SIMD_LDS_STORE",
|
||||
0x5a: "__SIMD_VMEM_LOAD",
|
||||
0x5b: "__SIMD_VMEM_LOAD",
|
||||
0x5c: "__SIMD_VMEM_STORE",
|
||||
0x5d: "__SIMD_VMEM_STORE",
|
||||
0x5e: "__SIMD_VMEM_STORE",
|
||||
0x5f: "__SIMD_VMEM_STORE",
|
||||
0x72: "SALU_OR",
|
||||
0x73: "VALU_CMPX",
|
||||
}
|
||||
|
||||
ALUSRC = {
|
||||
1: "SALU",
|
||||
2: "VALU",
|
||||
3: "VALU_SALU",
|
||||
}
|
||||
|
||||
MEMSRC = {
|
||||
0: "LDS",
|
||||
1: "__LDS",
|
||||
2: "VMEM",
|
||||
3: "__VMEM",
|
||||
}
|
||||
|
||||
|
||||
# these tables are from rocprof trace decoder
|
||||
# rocprof_trace_decoder_parse_data-0x11c6a0
|
||||
# parse_sqtt_180 = b *rocprof_trace_decoder_parse_data-0x11c6a0+0x110040
|
||||
|
||||
# ---------- 1. local_138: 256-byte state->opcode table ----------
|
||||
|
||||
STATE_TO_OPCODE: bytes = bytes([
|
||||
0x10, 0x16, 0x18, 0x01, 0x05, 0x0b, 0x0c, 0x00, 0x0f, 0x14, 0x18, 0x01, 0x09, 0x04, 0x03, 0x02,
|
||||
0x10, 0x17, 0x18, 0x01, 0x06, 0x08, 0x0d, 0x00, 0x0f, 0x14, 0x18, 0x01, 0x0a, 0x04, 0x03, 0x02,
|
||||
0x10, 0x07, 0x18, 0x01, 0x05, 0x0b, 0x0c, 0x00, 0x0f, 0x14, 0x18, 0x01, 0x09, 0x04, 0x03, 0x02,
|
||||
0x10, 0x19, 0x18, 0x01, 0x06, 0x08, 0x0d, 0x00, 0x0f, 0x14, 0x18, 0x01, 0x0a, 0x04, 0x03, 0x02,
|
||||
0x10, 0x00, 0x18, 0x01, 0x05, 0x0b, 0x0c, 0x00, 0x0f, 0x14, 0x18, 0x01, 0x09, 0x04, 0x03, 0x02,
|
||||
0x10, 0x11, 0x18, 0x01, 0x06, 0x08, 0x0d, 0x00, 0x0f, 0x14, 0x18, 0x01, 0x0a, 0x04, 0x03, 0x02,
|
||||
0x10, 0x12, 0x18, 0x01, 0x05, 0x0b, 0x0c, 0x00, 0x0f, 0x14, 0x18, 0x01, 0x09, 0x04, 0x03, 0x02,
|
||||
0x10, 0x15, 0x18, 0x01, 0x06, 0x08, 0x0d, 0x00, 0x0f, 0x14, 0x18, 0x01, 0x0a, 0x04, 0x03, 0x02,
|
||||
0x10, 0x16, 0x18, 0x01, 0x05, 0x0b, 0x0c, 0x00, 0x0f, 0x14, 0x18, 0x01, 0x09, 0x04, 0x03, 0x02,
|
||||
0x10, 0x17, 0x18, 0x01, 0x06, 0x08, 0x0d, 0x00, 0x0f, 0x14, 0x18, 0x01, 0x0a, 0x04, 0x03, 0x02,
|
||||
0x10, 0x07, 0x18, 0x01, 0x05, 0x0b, 0x0c, 0x00, 0x0f, 0x14, 0x18, 0x01, 0x09, 0x04, 0x03, 0x02,
|
||||
0x10, 0x19, 0x18, 0x01, 0x06, 0x08, 0x0d, 0x00, 0x0f, 0x14, 0x18, 0x01, 0x0a, 0x04, 0x03, 0x02,
|
||||
0x10, 0x00, 0x18, 0x01, 0x05, 0x0b, 0x0c, 0x00, 0x0f, 0x14, 0x18, 0x01, 0x09, 0x04, 0x03, 0x02,
|
||||
0x10, 0x11, 0x18, 0x01, 0x06, 0x08, 0x0d, 0x00, 0x0f, 0x14, 0x18, 0x01, 0x0a, 0x04, 0x03, 0x02,
|
||||
0x10, 0x13, 0x18, 0x01, 0x05, 0x0b, 0x0c, 0x00, 0x0f, 0x14, 0x18, 0x01, 0x09, 0x04, 0x03, 0x02,
|
||||
0x10, 0x15, 0x18, 0x01, 0x06, 0x08, 0x0d, 0x00, 0x0f, 0x14, 0x18, 0x01, 0x0a, 0x04, 0x03, 0x02,
|
||||
])
|
||||
|
||||
# opcode mask (the bits used to determine the opcode, worked out by looking at the repeats in STATE_TO_OPCODE)
|
||||
|
||||
opcode_mask = {
|
||||
0x10: 0b1111,
|
||||
|
||||
0x16: 0b1111111,
|
||||
0x17: 0b1111111,
|
||||
0x07: 0b1111111,
|
||||
0x19: 0b1111111,
|
||||
0x11: 0b1111111,
|
||||
0x12: 0b11111111,
|
||||
0x13: 0b11111111,
|
||||
0x15: 0b1111111,
|
||||
|
||||
0x18: 0b111,
|
||||
0x1: 0b111,
|
||||
|
||||
0x5: 0b11111,
|
||||
0x6: 0b11111,
|
||||
0xb: 0b11111,
|
||||
0x8: 0b11111,
|
||||
0xc: 0b11111,
|
||||
0xd: 0b11111,
|
||||
|
||||
0xf: 0b1111,
|
||||
0x14: 0b1111,
|
||||
|
||||
0x9: 0b11111,
|
||||
0xa: 0b11111,
|
||||
|
||||
0x4: 0b1111,
|
||||
0x3: 0b1111,
|
||||
0x2: 0b1111,
|
||||
}
|
||||
|
||||
# ---------- 2. DAT_0012e280: nibble budget per opcode&0x1F ----------
|
||||
|
||||
NIBBLE_BUDGET = [
|
||||
0x08, 0x0C, 0x08, 0x08, 0x0C, 0x18, 0x18, 0x40, 0x14, 0x20, 0x30, 0x14, 0x34, 0x1C, 0x30, 0x08,
|
||||
0x04, 0x18, 0x18, 0x20, 0x40, 0x40, 0x30, 0x40, 0x14, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
]
|
||||
|
||||
# ---------- 3. delta_map from your hash nodes ----------
|
||||
|
||||
# opcode -> (shift, width)
|
||||
DELTA_MAP_DEFAULT = {
|
||||
0x01: (3, 3), # shift=3, end=6
|
||||
0x02: (4, 2), # shift=4, end=6
|
||||
0x03: (4, 2), # shift=4, end=6
|
||||
0x04: (4, 3), # shift=4, end=7
|
||||
0x05: (5, 3), # shift=5, end=8
|
||||
0x06: (5, 3), # shift=5, end=8
|
||||
0x07: (8, 3), # shift=8, end=11
|
||||
0x08: (5, 3), # shift=5, end=8
|
||||
0x09: (5, 2), # shift=5, end=7
|
||||
0x0A: (5, 2), # shift=5, end=7
|
||||
0x0B: (5, 3), # shift=5, end=8
|
||||
0x0C: (5, 3), # shift=5, end=8
|
||||
0x0D: (5, 3), # shift=5, end=8
|
||||
# NOTE: 0x0e can never be decoded, it's not in the STATE_TO_OPCODE table
|
||||
#0x0E: (7, 2), # shift=7, end=9
|
||||
0x0F: (4, 4), # shift=4, end=8
|
||||
0x10: (0, 0), # shift=0, end=0 (no delta)
|
||||
0x11: (7, 9), # shift=7, end=16
|
||||
0x12: (8, 3), # shift=8, end=11
|
||||
0x13: (8, 3), # shift=8, end=11
|
||||
0x14: (4, 3), # shift=4, end=7
|
||||
0x15: (7, 3), # shift=7, end=10
|
||||
0x16: (12, 36), # shift=12, end=48 (36-bit field, matches the 0x16 special-case)
|
||||
0x17: (0, 0), # shift=0, end=0 (no delta)
|
||||
0x18: (4, 3), # shift=4, end=7
|
||||
0x19: (7, 2), # shift=7, end=9
|
||||
}
|
||||
|
||||
# ---------- 4. One-line-per-packet parser ----------
|
||||
|
||||
def reg_mask(opcode):
|
||||
nb_bits = NIBBLE_BUDGET[opcode & 0x1F]
|
||||
shift, width = DELTA_MAP_DEFAULT[opcode]
|
||||
delta_mask = ((1 << width) - 1) << shift
|
||||
assert delta_mask & opcode_mask[opcode] == 0, "masks shouldn't overlap"
|
||||
return ((1 << nb_bits) - 1) & ~(delta_mask | opcode_mask[opcode])
|
||||
|
||||
def decode_packet_fields(opcode: int, reg: int) -> str:
|
||||
"""
|
||||
Decode packet payloads conservatively, using:
|
||||
- NIBBLE_BUDGET[opcode & 0x1F] to mask reg down to true width.
|
||||
- DELTA_MAP_DEFAULT[opcode] to expose the "primary" field (often delta).
|
||||
- Per-opcode layouts derived from rocprof's decompiled consumers.
|
||||
"""
|
||||
# --- 0. Restrict to real packet bits not used in delta ---------------------------------
|
||||
pkt = reg & reg_mask(opcode)
|
||||
fields: list[str] = []
|
||||
|
||||
match opcode:
|
||||
case 0x01: # VALUINST
|
||||
# 6 bit field
|
||||
flag = (pkt >> 6) & 1
|
||||
wave = pkt >> 7
|
||||
fields.append(f"wave={wave:x}")
|
||||
if flag: fields.append("flag")
|
||||
case 0x02: # VMEMEXEC
|
||||
# 2 bit field (pipe is a guess)
|
||||
src = pkt>>6
|
||||
fields.append(f"src={src} [{MEMSRC.get(src, '')}]")
|
||||
case 0x03: # ALUEXEC
|
||||
# 2 bit field
|
||||
src = pkt>>6
|
||||
fields.append(f"src={src} [{ALUSRC.get(src, '')}]")
|
||||
case 0x04: # IMMEDIATE_4
|
||||
# 5 bit field (actually 4)
|
||||
wave = pkt >> 7
|
||||
fields.append(f"wave={wave:x}")
|
||||
case 0x05: # IMMEDIATE_5
|
||||
# 16 bit field
|
||||
# 1 bit per wave
|
||||
fields.append(f"mask={pkt>>8:016b}")
|
||||
case 0x6:
|
||||
# wave ready FFFF00
|
||||
# 16 bit field
|
||||
# 1 bit per wave
|
||||
fields.append(f"mask={pkt>>8:016b}")
|
||||
case 0x0d:
|
||||
# 20 bit field
|
||||
fields.append(f"arg = {pkt>>8:X}")
|
||||
case 0x12:
|
||||
fields.append(f"event = {pkt>>11:X}")
|
||||
case 0x15:
|
||||
fields.append(f"snap = {pkt>>10:X}")
|
||||
case 0x19:
|
||||
# wave end
|
||||
fields.append(f"ctr = {pkt>>9:X}")
|
||||
case 0xf:
|
||||
extracted_delta = (reg >> 4) & 0xF
|
||||
fields.append(f"strange_delta=0x{extracted_delta:x}")
|
||||
case 0x11:
|
||||
# DELTA_MAP_DEFAULT: shift=7, width=9 -> small delta.
|
||||
# FF0000 is the mask
|
||||
coarse = pkt >> 16
|
||||
fields.append(f"coarse=0x{coarse:02x}")
|
||||
# From decomp:
|
||||
# - when layout<3 and coarse&1, it sets a "has interesting wave" flag
|
||||
# - when coarse&8, it marks all live waves as "terminated"
|
||||
if coarse & 0x01:
|
||||
fields.append("flag_wave_interest=1")
|
||||
if coarse & 0x08:
|
||||
fields.append("flag_terminate_all=1")
|
||||
case 0x8:
|
||||
# wave end, this is 20 bits (FFF00)
|
||||
flag7 = (pkt >> 8) & 1
|
||||
simd = (pkt >> 9) & 3
|
||||
cu = ((pkt >> 11) & 0x7) | (flag7 << 3)
|
||||
wave = (pkt >> 15) & 0x1f
|
||||
fields.append(f"wave={wave:x}")
|
||||
fields.append(f"simd={simd}")
|
||||
fields.append(f"cu={cu}")
|
||||
case 0x9:
|
||||
# From case 9 (WAVESTART) in multiple consumers:
|
||||
# flag7 = (w >> 7) & 1 (low bit of uVar41)
|
||||
# cls2 = (w >> 8) & 3 (class / group)
|
||||
# slot4 = (w >> 10) & 0xf (slot / group index)
|
||||
# idx_lo = (w >> 0xd) & 0x1f (low index, layout<4 path)
|
||||
# idx_hi = (w >> 0xf) & 0x1f (high index, layout>=4 path)
|
||||
# id7 = (w >> 0x19) & 0x7f (7-bit id)
|
||||
flag7 = (pkt >> 7) & 1
|
||||
simd = (pkt >> 8) & 3
|
||||
cu = ((pkt >> 10) & 0x7) | (flag7 << 3)
|
||||
wave = (pkt >> 13) & 0x1F
|
||||
id7 = (pkt >> 17)
|
||||
fields.append(f"wave={wave:x}")
|
||||
fields.append(f"simd={simd}")
|
||||
fields.append(f"cu={cu}")
|
||||
fields.append(f"id7=0x{id7:x}")
|
||||
case 0x18:
|
||||
# FFF88 is the mask
|
||||
# From case 0x18:
|
||||
# low3 = w & 7
|
||||
# grp3 = (w >> 3) or (w >> 4) & 7 (layout-dependent)
|
||||
# flags = bits 6 (B6) and 7 (B7)
|
||||
# hi8 = (w >> 0xc) & 0xff (layout 4 path)
|
||||
# hi7 = (w >> 0xd) & 0x7f (other layouts)
|
||||
# idx5 = (w >> 7) or (w >> 8) & 0x1f, used as wave index
|
||||
flag1 = (pkt >> 3) & 1
|
||||
flag2 = (pkt >> 7) & 1
|
||||
wave = (pkt >> 8) & 0x1F
|
||||
op = (pkt >> 13)
|
||||
fields.append(f"wave={wave:x}")
|
||||
fields.append(f"op=0x{op:02x} [{OPNAME.get(op, '')}]")
|
||||
if flag1: fields.append("flag1")
|
||||
if flag2: fields.append("flag2")
|
||||
case 0x14:
|
||||
subop = (pkt >> 16) & 0xFFFF # (short)(w >> 0x10)
|
||||
val32 = (pkt >> 32) & 0xFFFFFFFF # (uint)(w >> 0x20)
|
||||
slot = (pkt >> 7) & 0x7 # index in local_168[...] tables
|
||||
hi_byte = (pkt >> 8) & 0xFF # determines config vs marker
|
||||
|
||||
fields.append(f"subop=0x{subop:04x}")
|
||||
fields.append(f"slot={slot}")
|
||||
fields.append(f"val32=0x{val32:08x}")
|
||||
|
||||
if hi_byte & 0x80:
|
||||
# Config flavour: writes config words into per-slot state arrays.
|
||||
fields.append("kind=config")
|
||||
if subop == 0x000C:
|
||||
fields.append("slot=lo")
|
||||
elif subop == 0x000D:
|
||||
fields.append("slot=hi")
|
||||
else:
|
||||
# COR marker: subop 0xC342, payload "COR\0" → start of a COR region.
|
||||
if subop == 0xC342:
|
||||
fields.append("kind=cor_stream")
|
||||
if val32 == 0x434F5200:
|
||||
fields.append("cor_magic='COR\\0'")
|
||||
case 0x16:
|
||||
# Bits:
|
||||
# bit8 -> 0x100
|
||||
# bit9 -> 0x200
|
||||
# bits 12..47 -> 36-bit field used as delta or marker
|
||||
bit8 = bool(pkt & 0x100)
|
||||
bit9 = bool(pkt & 0x200)
|
||||
if not bit9:
|
||||
mode = "delta"
|
||||
elif not bit8:
|
||||
mode = "marker"
|
||||
else:
|
||||
mode = "other"
|
||||
# need to use reg here
|
||||
val36 = (reg >> 12) & ((1 << 36) - 1)
|
||||
fields.append(f"mode={mode}")
|
||||
if mode != "delta":
|
||||
fields.append(f"val36=0x{val36:x}")
|
||||
case 0x17:
|
||||
# From decomp (two sites with identical logic):
|
||||
# layout = (w >> 7) & 0x3f
|
||||
# mode = (w >> 0xd) & 3
|
||||
# group = (w >> 0xf) & 7
|
||||
# sel_a = (w >> 0x1c) & 0xf
|
||||
# sel_b = (w >> 0x21) & 7
|
||||
# flag4 = (w >> 0x3b) & 1 (only meaningful when layout == 4)
|
||||
layout = (pkt >> 7) & 0x3F
|
||||
simd = (pkt >> 13) & 0x3 # you can change this by changing traced simd
|
||||
group = (pkt >> 15) & 0x7
|
||||
sel_a = (pkt >> 0x1C) & 0xF
|
||||
sel_b = (pkt >> 0x21) & 0x7
|
||||
flag4 = (pkt >> 0x3B) & 0x1
|
||||
|
||||
fields.append(f"layout={layout}")
|
||||
fields.append(f"group={group}")
|
||||
fields.append(f"simd={simd}")
|
||||
fields.append(f"sel_a={sel_a}")
|
||||
fields.append(f"sel_b={sel_b}")
|
||||
if layout == 4:
|
||||
fields.append(f"layout4_flag={flag4}")
|
||||
case _:
|
||||
fields.append(f"{pkt:X} & {reg_mask(opcode):X}")
|
||||
return ",".join(fields)
|
||||
|
||||
FILTER_LEVEL = getenv("FILTER", 1)
|
||||
|
||||
DEFAULT_FILTER: tuple[int, ...] = tuple()
|
||||
# NOP + pure time + "sample"
|
||||
if FILTER_LEVEL >= 0: DEFAULT_FILTER += (0x10, 0xf, 0x11)
|
||||
# reg + event + sample + marker
|
||||
# TODO: events are probably good
|
||||
if FILTER_LEVEL >= 1: DEFAULT_FILTER += (0x14, 0x12, 0x16)
|
||||
# instruction runs + valuinst
|
||||
if FILTER_LEVEL >= 2: DEFAULT_FILTER += (0x01, 0x02, 0x03)
|
||||
# instructions dispatch (inst, immed)
|
||||
if FILTER_LEVEL >= 3: DEFAULT_FILTER += (0x4, 0x5, 0x18)
|
||||
# waves
|
||||
if FILTER_LEVEL >= 4: DEFAULT_FILTER += (0x6, 0x8, 0x9)
|
||||
|
||||
def parse_sqtt_print_packets(data: bytes, filter=DEFAULT_FILTER, verbose=True) -> None:
|
||||
"""
|
||||
Minimal debug: print ONE LINE per decoded token (packet).
|
||||
|
||||
Now prints only the actual nibbles that belong to each packet, instead of
|
||||
the full 64-bit shift register.
|
||||
"""
|
||||
n = len(data)
|
||||
time = 0
|
||||
last_printed_time = 0
|
||||
reg = 0 # shift register
|
||||
offset = 0 # bit offset, in steps of 4 (one nibble)
|
||||
nib_budget = 0x40
|
||||
flags = 0
|
||||
token_index = 0
|
||||
opcodes_seen = set()
|
||||
|
||||
while (offset >> 3) < n:
|
||||
# 1) Fill register with nibbles according to nib_budget
|
||||
if nib_budget != 0:
|
||||
target = offset + 4 + ((nib_budget - 1) & ~3)
|
||||
while offset != target and (offset >> 3) < n:
|
||||
byte = data[offset >> 3]
|
||||
nib = (byte >> (offset & 4)) & 0xF
|
||||
reg = ((reg >> 4) | (nib << 60)) & ((1 << 64) - 1)
|
||||
offset += 4
|
||||
if offset != target: break # don't parse past the end
|
||||
|
||||
# 2) Decode token from low 8 bits
|
||||
opcode = STATE_TO_OPCODE[reg & 0xFF]
|
||||
opcodes_seen.add(opcode)
|
||||
|
||||
# 4) Set next nibble budget based on opcode
|
||||
nib_budget = NIBBLE_BUDGET[opcode & 0x1F]
|
||||
|
||||
# 5) Get delta
|
||||
shift, width = DELTA_MAP_DEFAULT[opcode]
|
||||
delta = (reg >> shift) & ((1 << width) - 1)
|
||||
|
||||
# 6) Update time and handle special opcodes 0xF/0x16
|
||||
if opcode == 0x16:
|
||||
two_bits = (reg >> 8) & 0x3
|
||||
if two_bits == 1:
|
||||
flags |= 0x01
|
||||
|
||||
# Common 36-bit field at bits [12..47]
|
||||
if (reg & 0x200) == 0:
|
||||
# delta mode: add 36-bit delta to time
|
||||
pass
|
||||
elif (reg & 0x100) == 0:
|
||||
# marker / other modes: no time advance
|
||||
# real marker: bit9=1, bit8=0, non-zero payload
|
||||
# "other" 0x16 variants, ignored for timing
|
||||
delta = 0
|
||||
else:
|
||||
raise RuntimeError("unknown 0x16 delta")
|
||||
elif opcode == 0x0F:
|
||||
# opcode 0x0F has an offset of 4 to the delta
|
||||
# update: it's actually computed to be 8 to match WAVESTART
|
||||
delta = delta + 8
|
||||
|
||||
# Append extra decoded fields into the note string
|
||||
note = decode_packet_fields(opcode, reg)
|
||||
|
||||
# this delta happens before the instruction
|
||||
time += delta
|
||||
token_index += 1
|
||||
|
||||
if verbose and (filter is None or opcode not in filter):
|
||||
print(f"{time:8d} +{time-last_printed_time:8d} : "+colored(f"{OPCODE_NAMES[opcode]:18s} ", OPCODE_COLORS.get(opcode, "white"))+f"{note}")
|
||||
last_printed_time = time
|
||||
|
||||
# Optional summary at the end
|
||||
print(f"# done: tokens={token_index:_}, final_time={time}, flags=0x{flags:02x}")
|
||||
if verbose:
|
||||
print(f"opcodes({len(opcodes_seen):2d}):",
|
||||
' '.join([colored(f"{op:2X}", "WHITE" if op in opcodes_seen else "BLACK") for op in sorted(opcode_mask)]))
|
||||
|
||||
|
||||
def parse(fn:str):
|
||||
with Timing(f"unpickle {fn}: "): dat = pickle.load(open(fn, "rb"))
|
||||
#if getenv("ROCM", 0):
|
||||
# with Timing(f"decode {fn}: "): ctx = decode(dat)
|
||||
dat_sqtt = [x for x in dat if isinstance(x, ProfileSQTTEvent)]
|
||||
print(f"got {len(dat_sqtt)} SQTT events in {fn}")
|
||||
return dat_sqtt
|
||||
|
||||
if __name__ == "__main__":
|
||||
fn = "extra/sqtt/examples/profile_gemm_run_0.pkl"
|
||||
dat_sqtt = parse(sys.argv[1] if len(sys.argv) > 1 else fn)
|
||||
for i,dat in enumerate(dat_sqtt):
|
||||
with Timing(f"decode pkt {i} with len {len(dat.blob):_}: "):
|
||||
parse_sqtt_print_packets(dat.blob, verbose=getenv("V", 1))
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
# Run all ALU and memory instructions in the ISA
|
||||
import functools, inspect
|
||||
from enum import Enum
|
||||
from tinygrad import Tensor, Device, dtypes
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo, AddrSpace
|
||||
from tinygrad.renderer.amd.dsl import Inst, Reg, OPERANDS, SrcField, VGPRField, SGPRField, SSrcField, SBaseField, AlignedSGPRField, BitField
|
||||
from tinygrad.renderer.amd.dsl import FixedBitField, EnumBitField, s, v, NULL, VCC_LO
|
||||
from extra.gemm.amd_asm_matmul import Kernel
|
||||
|
||||
# skip instructions that mutate wave state (PC, EXEC, allocations, signals)
|
||||
SKIP = {"S_SETPC_B64", "S_SWAPPC_B64", "S_RFE_B64", "S_BARRIER_SIGNAL_ISFIRST", "S_GET_BARRIER_STATE", "S_ALLOC_VGPR", "S_SLEEP_VAR", "S_GETPC_B64",
|
||||
"S_SENDMSG_RTN_B32", "S_SENDMSG_RTN_B64"}
|
||||
# skip barriers, s_waits, wrap level atomics, and ray tracing (bvh)
|
||||
SKIP_SUBSTR = ["SAVEEXEC", "CMPX", "WREXEC", "MOVREL", "ATOMIC", "S_BUFFER_", "S_ATC_PROBE", "BARRIER", "S_WAITCNT", "BVH",
|
||||
"DS_CMPSTORE_RTN", "DS_WRAP_RTN_B32", "DS_ORDERED_COUNT", "DS_GWS", "GS_REG", "GLOBAL_LOAD_LDS", "GLOBAL_STORE_BLOCK"]
|
||||
|
||||
ALU_FORMATS = {"VOP1", "VOP1_LIT", "VOP1_SDST", "VOP2", "VOP2_LIT", "VOP3", "VOP3_SDST", "VOP3SD", "VOP3P", "VOP3P_MFMA", "VOP3PX2",
|
||||
"VOPC", "SOP1", "SOP1_LIT", "SOP2", "SOP2_LIT", "SOPC", "SOPC_LIT", "SOPK", "SOPK_LIT", "VINTERP"}
|
||||
# intentionally not testing scratch memory ops
|
||||
MEM_FORMATS = {"VGLOBAL", "GLOBAL", "SMEM", "DS"}
|
||||
|
||||
def should_skip(op:Enum) -> bool: return (name:=op.name) in SKIP or any(sub in name for sub in SKIP_SUBSTR)
|
||||
|
||||
# ** named register assignments
|
||||
|
||||
# ALU operands
|
||||
ALU_VGPR_STRIDE = 16 # v[0], v[16], v[32], ... per ALU operand slot
|
||||
ALU_SGPR_STRIDE = 4 # s[0], s[4], s[8], ... per ALU operand slot
|
||||
|
||||
# memory address registers
|
||||
S_KERNARG_PTR = (0, 1)
|
||||
S_BUF_PTR = (2, 3)
|
||||
V_VADDR = (0, 1)
|
||||
V_DS_ADDR = 0
|
||||
|
||||
# memory data registers
|
||||
MEM_VGPR_BASE = 32 # v[32], v[48], ... for vdst/vdata/vsrc
|
||||
MEM_VGPR_STRIDE = 16 # spacing between memory data vgpr slots
|
||||
MEM_SGPR_BASE = 8 # s[8], s[10], ... for SMEM sdata
|
||||
MEM_SGPR_STRIDE = 2 # spacing between memory data sgpr slots
|
||||
|
||||
# ** create an ALU instruction based on the operands
|
||||
|
||||
def create_alu_inst(op:Enum, builder:functools.partial[Inst]) -> Inst:
|
||||
inst_cls, operands, slot = builder.func, OPERANDS[op], 0
|
||||
kwargs:dict[str, Reg|int] = {}
|
||||
for name, field in inst_cls._fields:
|
||||
if isinstance(field, (FixedBitField, EnumBitField)): continue
|
||||
nregs = max(1, operands[name][1] // 32) if name in operands else 1
|
||||
is_sreg = name in operands and "SREG" in str(operands[name][2])
|
||||
base_v, base_s = slot * ALU_VGPR_STRIDE, slot * ALU_SGPR_STRIDE
|
||||
if name == "sdst" and isinstance(field, SGPRField): reg = VCC_LO
|
||||
elif is_sreg and not isinstance(field, VGPRField): reg = VCC_LO
|
||||
elif isinstance(field, VGPRField): reg = v[base_v:base_v+nregs-1] if nregs > 1 else v[base_v]
|
||||
elif isinstance(field, SSrcField): reg = VCC_LO if nregs <= 2 else s[base_s:base_s+nregs-1] if nregs > 1 else s[base_s]
|
||||
elif isinstance(field, SGPRField): reg = s[base_s:base_s+nregs-1] if nregs > 1 else s[base_s]
|
||||
elif isinstance(field, SrcField): reg = v[base_v:base_v+nregs-1] if nregs > 1 else v[base_v]
|
||||
else: reg = None
|
||||
if reg is not None: kwargs[name] = reg; slot += 1
|
||||
elif isinstance(field, BitField): kwargs[name] = field.default
|
||||
return builder(**kwargs)
|
||||
|
||||
# ** create a memory instruction with pre set address registers
|
||||
|
||||
MEM_PRESET_REGS:dict[str, dict[str, Reg]] = {
|
||||
"VGLOBAL":{"saddr":s[S_BUF_PTR[0]:S_BUF_PTR[1]], "vaddr":v[V_VADDR[0]:V_VADDR[1]]},
|
||||
"GLOBAL":{"saddr":s[S_BUF_PTR[0]:S_BUF_PTR[1]], "addr":v[V_DS_ADDR]}, # addr is 32-bit offset when saddr is valid SGPR
|
||||
"DS":{"addr":v[V_DS_ADDR]},
|
||||
"SMEM":{"sbase":s[S_KERNARG_PTR[0]:S_KERNARG_PTR[1]], "soffset":NULL},
|
||||
}
|
||||
|
||||
def create_mem_inst(op:Enum, builder:functools.partial[Inst]) -> Inst:
|
||||
inst_cls, operands, field_map = builder.func, OPERANDS.get(op, {}), MEM_PRESET_REGS.get(builder.func.__name__, {})
|
||||
kwargs:dict[str, Reg|int] = {}
|
||||
vslot, sslot = 0, 0
|
||||
for name, field in inst_cls._fields:
|
||||
if isinstance(field, (FixedBitField, EnumBitField)): continue
|
||||
if name in field_map:
|
||||
kwargs[name] = field_map[name]
|
||||
continue
|
||||
nregs = max(1, operands[name][1] // 32) if name in operands else 1
|
||||
if isinstance(field, VGPRField):
|
||||
vi = MEM_VGPR_BASE + vslot * MEM_VGPR_STRIDE
|
||||
kwargs[name] = v[vi:vi+nregs-1] if nregs > 1 else v[vi]
|
||||
vslot += 1
|
||||
elif isinstance(field, (SGPRField, AlignedSGPRField, SBaseField)):
|
||||
si = MEM_SGPR_BASE + sslot * MEM_SGPR_STRIDE
|
||||
kwargs[name] = s[si:si+nregs-1] if nregs > 1 else s[si]
|
||||
sslot += 1
|
||||
elif isinstance(field, BitField): kwargs[name] = field.default
|
||||
return builder(**kwargs)
|
||||
|
||||
# ** collect all memory and ALU instructions from the ISA autogen
|
||||
|
||||
def collect_instructions() -> tuple[list[Inst], list[Inst], list[str]]:
|
||||
op_map:dict[Enum, functools.partial[Inst]] = {}
|
||||
for name, obj in inspect.getmembers(all_insts):
|
||||
if isinstance(obj, functools.partial) and len(obj.args) == 1: op_map[obj.args[0]] = obj
|
||||
alu_insts:list[Inst] = []
|
||||
mem_insts:list[Inst] = []
|
||||
skipped:list[str] = []
|
||||
for op_enum, builder in op_map.items():
|
||||
if should_skip(op_enum) or op_enum not in OPERANDS: skipped.append(op_enum.name); continue
|
||||
fmt = builder.func.__name__
|
||||
if fmt in ALU_FORMATS: alu_insts.append(create_alu_inst(op_enum, builder))
|
||||
elif fmt in MEM_FORMATS: mem_insts.append(create_mem_inst(op_enum, builder))
|
||||
return alu_insts, mem_insts, skipped
|
||||
|
||||
def exec_insts(insts:list):
|
||||
k = Kernel(arch)
|
||||
# ** prologue for global memory
|
||||
k.emit(s_load_b64(sdata=s[S_BUF_PTR[0]:S_BUF_PTR[1]], sbase=s[S_KERNARG_PTR[0]:S_KERNARG_PTR[1]], soffset=NULL))
|
||||
k.waitcnt(lgkm=0)
|
||||
k.emit(v_mov_b32_e32(v[V_VADDR[0]], 0))
|
||||
k.emit(v_mov_b32_e32(v[V_VADDR[1]], 0))
|
||||
# ** emit
|
||||
for inst in insts: k.emit(inst)
|
||||
k.emit(s_endpgm())
|
||||
# ** run
|
||||
NUM_THREADS, NUM_GRIDS, BUF_SIZE = 32, 1, 1024*1024
|
||||
def fxn(A:UOp, B:UOp, C:UOp) -> UOp:
|
||||
lidx, gidx = UOp.special(NUM_THREADS, "lidx0"), UOp.special(NUM_GRIDS, "gidx0")
|
||||
lds = UOp(Ops.DEFINE_LOCAL, dtypes.uint8.ptr(size=BUF_SIZE, addrspace=AddrSpace.LOCAL), (), "lds")
|
||||
sink = UOp.sink(A.base, B.base, C.base, lds, lidx, gidx, arg=KernelInfo(name="discover_ops"))
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.DEVICE, arg="AMD"), UOp(Ops.LINEAR, src=tuple(UOp(Ops.INS, arg=x) for x in k.finalize()))))
|
||||
A = Tensor.empty(BUF_SIZE, dtype=dtypes.uint8)
|
||||
B = Tensor.empty(1, dtype=dtypes.uint8)
|
||||
C = Tensor.empty(1, dtype=dtypes.uint8)
|
||||
Tensor.custom_kernel(A, B, C, fxn=fxn)[0].realize()
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
arch = Device[Device.DEFAULT].renderer.arch
|
||||
if arch.startswith("gfx12"):
|
||||
from tinygrad.runtime.autogen.amd.rdna4.ins import *
|
||||
import tinygrad.runtime.autogen.amd.rdna4.ins as all_insts
|
||||
elif arch.startswith("gfx11"):
|
||||
from tinygrad.runtime.autogen.amd.rdna3.ins import *
|
||||
import tinygrad.runtime.autogen.amd.rdna3.ins as all_insts
|
||||
# these don"t exist in RDNA3, only RDNA3.5 and above
|
||||
SKIP.update(["S_FMAAK_F32", "S_FMAMK_F32"])
|
||||
else:
|
||||
print(f"{arch} not supported yet")
|
||||
sys.exit(0)
|
||||
alu_insts, mem_insts, skipped = collect_instructions()
|
||||
print(f"collected {len(alu_insts)} ALU + {len(mem_insts)} memory instructions ({len(skipped)} skipped)")
|
||||
exec_insts(mem_insts+alu_insts)
|
||||
@@ -1,4 +1,4 @@
|
||||
import os, subprocess
|
||||
import os, subprocess, sys
|
||||
from pathlib import Path
|
||||
from tinygrad.helpers import temp
|
||||
|
||||
@@ -6,9 +6,10 @@ EXAMPLES_DIR = Path(__file__).parent
|
||||
PROFILE_PATH = Path(temp("profile.pkl", append_user=True))
|
||||
|
||||
EXAMPLES = [
|
||||
"test.backend.test_custom_kernel.TestCustomKernel.test_empty",
|
||||
"test.test_tiny.TestTiny.test_plus",
|
||||
"test.test_tiny.TestTiny.test_gemm",
|
||||
"test/backend/test_custom_kernel.py TestCustomKernel.test_empty",
|
||||
"test/test_tiny.py TestTiny.test_plus",
|
||||
"test/test_tiny.py TestTiny.test_gemm",
|
||||
"extra/sqtt/examples/discover_ops.py"
|
||||
]
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -17,7 +18,8 @@ if __name__ == "__main__":
|
||||
(EXAMPLES_DIR/arch).mkdir(exist_ok=True)
|
||||
for test in EXAMPLES:
|
||||
for i in range(2):
|
||||
subprocess.run(["python", "-m", "unittest", test], cwd=EXAMPLES_DIR.parent.parent.parent,
|
||||
env={**os.environ, "AMD":"1", "SQTT_LIMIT_SE":"-1", "VIZ":"-2"}, check=True)
|
||||
# AM_RESET=1 gets a clear trace, does not work on mi300 machines
|
||||
subprocess.run([sys.executable, *test.split()], cwd=EXAMPLES_DIR.parent.parent.parent,
|
||||
env={**os.environ, "AMD":"1", "AM_RESET":"1" if not arch.startswith("gfx9") else "0", "VIZ":"-2", "PYTHONPATH":"."})
|
||||
PROFILE_PATH.rename(dest:=EXAMPLES_DIR/arch/f"profile_{test.split('.')[-1].replace('test_', '')}_run_{i}.pkl")
|
||||
print(f"saved SQTT trace to {dest}")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
-1
@@ -118,7 +118,7 @@ def decode(sqtt_evs:list[ProfileSQTTEvent], disasms:dict[str, dict[int, Inst]])
|
||||
nonlocal exc
|
||||
try: rocprof.rocprof_trace_decoder_parse_data(copy_cb, trace_cb, isa_cb, None)
|
||||
except AttributeError as e:
|
||||
exc = RuntimeError("Failed to find rocprof-trace-decoder. Run sudo ./extra/sqtt/install_sqtt_decoder.py to install")
|
||||
exc = RuntimeError("Failed to find rocprof-trace-decoder. Run sudo ./extra/sqtt/install_rocprof_decoder.py to install")
|
||||
exc.__cause__ = e
|
||||
(t:=threading.Thread(target=worker, daemon=True)).start()
|
||||
t.join()
|
||||
|
||||
+28
-20
@@ -11,7 +11,8 @@ from tinygrad.uop.ops import UOp, Ops, KernelInfo
|
||||
def _sharded_empty(shape:Tensor, ref:Tensor, axis:int|None, dtype:DTypeLike|None=None) -> Tensor:
|
||||
dtype = dtype or ref.dtype
|
||||
if not isinstance(ref.device, tuple): return Tensor.empty(*shape, dtype=dtype, device=ref.device)
|
||||
shape = tuple(s // len(ref.device) if i == ref.uop.axis else s for i, s in enumerate(shape))
|
||||
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.empty(*shape, dtype=dtype, device=ref.device).uop.multi(axis), dtype=dtype, device=ref.device)
|
||||
|
||||
@@ -29,34 +30,45 @@ def flash_attention(xq, xk, xv, attn_mask:Tensor|None=None, is_causal:bool=False
|
||||
assert D == 128, "only D=128 supported"
|
||||
|
||||
num_devices = len(xq.device) if isinstance(xq.device, tuple) else 1
|
||||
B_local = B // num_devices
|
||||
if DEBUG >= 2: print(f"Flash Attention {B=} {B_local=} {N=} {H=} {H_KV=} {D=}")
|
||||
is_dp = xq.uop.axis == 0
|
||||
is_mp = xq.uop.axis == 2
|
||||
B_local = B // num_devices if is_dp else B
|
||||
H_local = H // num_devices if is_mp else H
|
||||
H_KV_local = H_KV // num_devices if is_mp else H_KV
|
||||
shard_axis = 0 if is_dp else 2 if is_mp else None
|
||||
shard_axis_t = 0 if is_dp else 1 if is_mp else None
|
||||
if DEBUG >= 2: print(f"Flash Attention {B=} {B_local=} {N=} {H=} {H_local=} {H_KV=} {H_KV_local=} {D=} on {num_devices} devices, {'DP' if is_dp else 'MP' if is_mp else 'no sharding'}")
|
||||
|
||||
single_device = xq.device[0] if isinstance(xq.device, tuple) else xq.device
|
||||
arch = Device[single_device].renderer.arch
|
||||
|
||||
attn = _sharded_empty_like(xq, axis=0)
|
||||
l_vec = _sharded_empty((B, H, 1, N), xq, axis=0, dtype=dtypes.float32)
|
||||
attn = _sharded_empty_like(xq, axis=shard_axis)
|
||||
l_vec = _sharded_empty((B, H, 1, N), xq, dtype=dtypes.float32, axis=shard_axis_t)
|
||||
|
||||
def grad(dou:UOp, _) -> tuple[None, None, UOp, UOp, UOp]:
|
||||
do = Tensor(dou, device=dou.device)
|
||||
dq_in = _sharded_empty((B, H, N, D), xq, axis=0)
|
||||
dq = _sharded_empty_like(xq, axis=0)
|
||||
dk = _sharded_empty_like(xk, axis=0)
|
||||
dv = _sharded_empty_like(xv, axis=0)
|
||||
dq = _sharded_empty((B, H, N, D), xq, axis=shard_axis_t)
|
||||
GROUP_SIZE = H_local // H_KV_local
|
||||
dk_partial = _sharded_empty((B * GROUP_SIZE, N, H_KV, D), xk, axis=shard_axis)
|
||||
dv_partial = _sharded_empty((B * GROUP_SIZE, N, H_KV, D), xv, axis=shard_axis)
|
||||
|
||||
# delta_vec = (do * attn).sum(-1, dtype=dtypes.float32).transpose(1, 2).unsqueeze(-2).detach()
|
||||
delta_vec = _sharded_empty((B, H, 1, N), xq, axis=0, dtype=dtypes.float32)
|
||||
delta_vec, dq_in = Tensor.custom_kernel(delta_vec, dq_in, attn, do, fxn=functools.partial(custom_fa_backward_pre, device=single_device, arch=arch, B=B_local, N=N, H=H, H_KV=H_KV, D=D))[:2]
|
||||
delta_vec = _sharded_empty((B, H, 1, N), xq, dtype=dtypes.float32, axis=shard_axis_t)
|
||||
delta_vec, dq = Tensor.custom_kernel(delta_vec, dq, attn, do, fxn=functools.partial(custom_fa_backward_pre, device=single_device, arch=arch, B=B_local, N=N, H=H_local, H_KV=H_KV_local, D=D))[:2]
|
||||
|
||||
dq_in, dk, dv = Tensor.custom_kernel(dq_in, dk, dv, do, xq, xk, xv, l_vec, delta_vec, fxn=functools.partial(custom_fa_backward, device=single_device, arch=arch, B=B_local, N=N, H=H, H_KV=H_KV, D=D))[:3]
|
||||
dq, dk_partial, dv_partial = Tensor.custom_kernel(dq, dk_partial, dv_partial, do, xq, xk, xv, l_vec, delta_vec, fxn=functools.partial(custom_fa_backward, device=single_device, arch=arch, B=B_local, N=N, H=H_local, H_KV=H_KV_local, D=D))[:3]
|
||||
|
||||
# unshuffle dq
|
||||
dq = Tensor.custom_kernel(dq, dq_in, fxn=functools.partial(custom_fa_backward_post, device=single_device, arch=arch, B=B_local, N=N, H=H, H_KV=H_KV, D=D))[0]
|
||||
# unshuffle dq: atomic_pk_add_bf16_with_warpid creates a shuffled layout within each 16x128 tile
|
||||
# decompose each tile into (j=4, a=2, b=2, d=4, e=4, k=4, c=2) and permute to (e, k, j, a, d, b, c) = standard row-major
|
||||
dq = dq.reshape(B, H, N//16, 4, 2, 2, 4, 4, 4, 2).permute(0, 1, 2, 7, 8, 3, 4, 6, 5, 9).reshape(B, H, N, D).transpose(1, 2)
|
||||
|
||||
# reduce partial dK/dV across GROUP_SIZE query heads
|
||||
dk = dk_partial.reshape(B, GROUP_SIZE, N, H_KV, D).sum(1)
|
||||
dv = dv_partial.reshape(B, GROUP_SIZE, N, H_KV, D).sum(1)
|
||||
|
||||
return None, None, dq.uop, dk.uop, dv.uop
|
||||
|
||||
attn, l_vec = Tensor.custom_kernel(attn, l_vec, xq, xk, xv, fxn=functools.partial(custom_fa_forward, device=single_device, arch=arch, B=B_local, N=N, H=H, H_KV=H_KV, D=D), grad_fxn=grad)[:2]
|
||||
attn, l_vec = Tensor.custom_kernel(attn, l_vec, xq, xk, xv, fxn=functools.partial(custom_fa_forward, device=single_device, arch=arch, B=B_local, N=N, H=H_local, H_KV=H_KV_local, D=D), grad_fxn=grad)[:2]
|
||||
|
||||
return attn.transpose(1, 2)
|
||||
|
||||
@@ -82,7 +94,6 @@ def custom_fa_forward(o:UOp, l_vec:UOp, q:UOp, k:UOp, v:UOp, device:str, arch:st
|
||||
arg=KernelInfo(name="custom_fa_forward", estimates=estimates))
|
||||
|
||||
lib = HIPCCCompiler(arch, compile_args).compile_cached(code)
|
||||
|
||||
lib = bytearray(lib)
|
||||
rodata_off = next(sh.header.sh_offset for sh in elf_loader(bytes(lib))[1] if sh.name == ".rodata")
|
||||
struct.pack_into('<I', lib, rodata_off, 160000)
|
||||
@@ -113,7 +124,6 @@ def custom_fa_backward_pre(delta_vec:UOp, dq:UOp, o:UOp, do:UOp, device:str, arc
|
||||
arg=KernelInfo(name="custom_fa_backward_pre", estimates=estimates))
|
||||
|
||||
lib = HIPCCCompiler(arch, compile_args).compile_cached(code)
|
||||
|
||||
lib = bytearray(lib)
|
||||
rodata_off = next(sh.header.sh_offset for sh in elf_loader(bytes(lib))[1] if sh.name == ".rodata")
|
||||
struct.pack_into('<I', lib, rodata_off, 160000)
|
||||
@@ -131,7 +141,7 @@ def custom_fa_backward(dq:UOp, dk:UOp, dv:UOp, do:UOp, q:UOp, k:UOp, v:UOp, l_ve
|
||||
BLOCK_SIZE_KV = 256
|
||||
NUM_WARPS = 4
|
||||
NUM_THREADS = 64 * NUM_WARPS
|
||||
gsz = (H_KV, N // BLOCK_SIZE_KV, B)
|
||||
gsz = (H, N // BLOCK_SIZE_KV, B)
|
||||
lsz = (NUM_THREADS, 1, 1)
|
||||
threadIdx_x = UOp.special(lsz[0], "lidx0")
|
||||
blockIdx_x, blockIdx_y, blockIdx_z = UOp.special(gsz[0], "gidx0"), UOp.special(gsz[1], "gidx1"), UOp.special(gsz[2], "gidx2")
|
||||
@@ -144,7 +154,6 @@ def custom_fa_backward(dq:UOp, dk:UOp, dv:UOp, do:UOp, q:UOp, k:UOp, v:UOp, l_ve
|
||||
arg=KernelInfo(name="custom_fa_backward", estimates=estimates))
|
||||
|
||||
lib = HIPCCCompiler(arch, compile_args).compile_cached(code)
|
||||
|
||||
lib = bytearray(lib)
|
||||
rodata_off = next(sh.header.sh_offset for sh in elf_loader(bytes(lib))[1] if sh.name == ".rodata")
|
||||
struct.pack_into('<I', lib, rodata_off, 160000)
|
||||
@@ -175,7 +184,6 @@ def custom_fa_backward_post(dq_out:UOp, dq_in:UOp, device:str, arch:str, B:int,
|
||||
arg=KernelInfo(name="custom_fa_backward_post", estimates=estimates))
|
||||
|
||||
lib = HIPCCCompiler(arch, compile_args).compile_cached(code)
|
||||
|
||||
lib = bytearray(lib)
|
||||
rodata_off = next(sh.header.sh_offset for sh in elf_loader(bytes(lib))[1] if sh.name == ".rodata")
|
||||
struct.pack_into('<I', lib, rodata_off, 160000)
|
||||
|
||||
@@ -37,7 +37,7 @@ using namespace kittens;
|
||||
using _gl_QdO = gl<bf16, ATTN_B, ATTN_N, ATTN_H, ATTN_D>;
|
||||
using _gl_KV = gl<bf16, ATTN_B, ATTN_N, ATTN_H_KV, ATTN_D>;
|
||||
using _gl_dQ = gl<bf16, ATTN_B, ATTN_H, ATTN_N, ATTN_D>;
|
||||
using _gl_dKV = gl<bf16, ATTN_B, ATTN_N, ATTN_H_KV, ATTN_D>;
|
||||
using _gl_dKV = gl<bf16, ATTN_B * GROUP_SIZE, ATTN_N, ATTN_H_KV, ATTN_D>;
|
||||
using _gl_Lvec = gl<float, ATTN_B, ATTN_H, 1, ATTN_N>;
|
||||
|
||||
template<int D> struct attn_bwd_combined_globals {
|
||||
@@ -47,7 +47,7 @@ template<int D> struct attn_bwd_combined_globals {
|
||||
_gl_dQ dQg;
|
||||
_gl_dKV dKg, dVg;
|
||||
_gl_Lvec L_vec, delta_vec;
|
||||
dim3 grid() { return dim3(ATTN_H_KV, (ATTN_N / BLOCK_SIZE_KV), ATTN_B); }
|
||||
dim3 grid() { return dim3(ATTN_H, (ATTN_N / BLOCK_SIZE_KV), ATTN_B); }
|
||||
dim3 block() { return dim3(NUM_THREADS); }
|
||||
size_t dynamic_shared_memory() { return MAX_SHARED_MEMORY; }
|
||||
};
|
||||
@@ -55,10 +55,12 @@ template<int D> struct attn_bwd_combined_globals {
|
||||
template<int D> __launch_bounds__(NUM_THREADS, 1)
|
||||
__global__ void attend_bwd_combined_ker(bf16 *dQ_ptr, bf16 *dK_ptr, bf16 *dV_ptr, bf16 *dO_ptr, bf16 *Q_ptr, bf16 *K_ptr, bf16 *V_ptr, float *L_vec_ptr, float *delta_vec_ptr) {
|
||||
|
||||
const int kv_head_idx = blockIdx.x; // This is the KV head index
|
||||
const int q_head_idx_fixed = blockIdx.x; // This is the query head index [0, ATTN_H)
|
||||
const int kv_head_idx = q_head_idx_fixed / GROUP_SIZE;
|
||||
const int q_head_in_group = q_head_idx_fixed % GROUP_SIZE;
|
||||
const int seq_idx = blockIdx.y;
|
||||
const int batch_idx = blockIdx.z;
|
||||
const int first_q_head = kv_head_idx * GROUP_SIZE;
|
||||
const int first_q_head = q_head_idx_fixed;
|
||||
|
||||
const int warpid = kittens::warpid();
|
||||
const int j = seq_idx * NUM_WARPS + warpid;
|
||||
@@ -70,7 +72,7 @@ __global__ void attend_bwd_combined_ker(bf16 *dQ_ptr, bf16 *dK_ptr, bf16 *dV_ptr
|
||||
// first Q step that can overlap this K_span:
|
||||
const int first_step = max(0, k_start_min / STEP_QO);
|
||||
const int num_steps_per_head = total_steps_per_head - first_step;
|
||||
const int num_steps = num_steps_per_head * GROUP_SIZE;
|
||||
const int num_steps = num_steps_per_head;
|
||||
const int k_pos = j * WARP_SIZE_KV;
|
||||
|
||||
constexpr float L_SCALE_FACTOR = 1.44269504089f;
|
||||
@@ -3355,14 +3357,14 @@ __global__ void attend_bwd_combined_ker(bf16 *dQ_ptr, bf16 *dK_ptr, bf16 *dV_ptr
|
||||
}
|
||||
}
|
||||
|
||||
store<1>(g.dVg, dV_j, {batch_idx, 0, kv_head_idx, 0}, {0, j, 0, 0});
|
||||
store<1>(g.dVg, dV_j, {batch_idx * GROUP_SIZE + q_head_in_group, 0, kv_head_idx, 0}, {0, j, 0, 0});
|
||||
__builtin_amdgcn_s_waitcnt(0);
|
||||
__builtin_amdgcn_s_barrier();
|
||||
|
||||
// We first copy dV_j_T from accumulator GPRs to vector GPRs and then perform the store
|
||||
accvgpr_read(dV_j_T, dK_j_T);
|
||||
mul(dV_j_T, dV_j_T, dP_SCALE_FACTOR);
|
||||
store<1>(g.dKg, dV_j, {batch_idx, 0, kv_head_idx, 0}, {0, j, 0, 0});
|
||||
store<1>(g.dKg, dV_j, {batch_idx * GROUP_SIZE + q_head_in_group, 0, kv_head_idx, 0}, {0, j, 0, 0});
|
||||
|
||||
// Write out final dQ_i slice
|
||||
mul(dQ_i_T, dQ_i_T, dP_SCALE_FACTOR);
|
||||
|
||||
@@ -23,7 +23,8 @@ if __name__ == "__main__":
|
||||
|
||||
kernel_count = GlobalCounters.kernel_count
|
||||
assert kernel_count > 0, "No kernels, test failed"
|
||||
expected_kernels = 228
|
||||
# NOTE: this is 124 on torch 2.10.0
|
||||
expected_kernels = 332
|
||||
expectation = f"ResNet18 kernels are {kernel_count} vs {expected_kernels} expected."
|
||||
if kernel_count < expected_kernels: warnings.warn(f"{expectation} Expectation can be lowered.", UserWarning)
|
||||
assert kernel_count <= expected_kernels, f"{expectation}"
|
||||
@@ -1,7 +1,6 @@
|
||||
# simple tests
|
||||
import unittest
|
||||
import torch
|
||||
import warnings
|
||||
from tinygrad.helpers import getenv, GlobalCounters
|
||||
if getenv("TINY_BACKEND2"):
|
||||
import extra.torch_backend.backend2
|
||||
@@ -18,15 +17,13 @@ class TestKernelFusionRegression(unittest.TestCase):
|
||||
torch.manual_seed(42)
|
||||
GlobalCounters.reset()
|
||||
fn().detach().cpu().numpy()
|
||||
expectation = f"{GlobalCounters.kernel_count} vs {expected_kernels} expected."
|
||||
if GlobalCounters.kernel_count < expected_kernels: warnings.warn(f"{expectation} Expectation can be lowered.", UserWarning)
|
||||
self.assertLessEqual(GlobalCounters.kernel_count, expected_kernels, f"{expectation}")
|
||||
self.assertEqual(GlobalCounters.kernel_count, expected_kernels)
|
||||
|
||||
def test_elementwise_fusion(self):
|
||||
def fn():
|
||||
x = torch.randn(128, 128, device=device)
|
||||
return (x + 1.0) * 2.0 - 0.5
|
||||
self._check_kernel_count(fn, 6)
|
||||
self._check_kernel_count(fn, 5)
|
||||
|
||||
def test_relu_fusion(self):
|
||||
def fn():
|
||||
@@ -34,7 +31,7 @@ class TestKernelFusionRegression(unittest.TestCase):
|
||||
conv = torch.nn.Conv2d(3, 16, 3, padding=1).to(device)
|
||||
with torch.no_grad():
|
||||
return torch.nn.functional.relu(conv(x))
|
||||
self._check_kernel_count(fn, 8)
|
||||
self._check_kernel_count(fn, 6)
|
||||
|
||||
def test_batchnorm_fusion(self):
|
||||
def fn():
|
||||
@@ -44,20 +41,20 @@ class TestKernelFusionRegression(unittest.TestCase):
|
||||
bn.eval()
|
||||
with torch.no_grad():
|
||||
return torch.nn.functional.relu(bn(conv(x)))
|
||||
self._check_kernel_count(fn, 16)
|
||||
self._check_kernel_count(fn, 10)
|
||||
|
||||
def test_reduce_fusion(self):
|
||||
def fn():
|
||||
x = torch.randn(64, 64, device=device)
|
||||
return (x * 2.0).sum()
|
||||
self._check_kernel_count(fn, 7)
|
||||
self._check_kernel_count(fn, 5)
|
||||
|
||||
def test_matmul_elementwise_fusion(self):
|
||||
def fn():
|
||||
x = torch.randn(32, 32, device=device)
|
||||
w = torch.randn(32, 32, device=device)
|
||||
return torch.nn.functional.relu(x @ w + 1.0)
|
||||
self._check_kernel_count(fn, 6)
|
||||
self._check_kernel_count(fn, 7)
|
||||
|
||||
def test_pooling_fusion(self):
|
||||
def fn():
|
||||
@@ -71,7 +68,7 @@ class TestKernelFusionRegression(unittest.TestCase):
|
||||
identity = torch.randn(1, 8, 16, 16, device=device)
|
||||
out = x + identity
|
||||
return torch.nn.functional.relu(out)
|
||||
self._check_kernel_count(fn, 6)
|
||||
self._check_kernel_count(fn, 7)
|
||||
|
||||
def test_inplace_add_relu_fusion(self):
|
||||
def fn():
|
||||
@@ -79,7 +76,7 @@ class TestKernelFusionRegression(unittest.TestCase):
|
||||
y = torch.randn(1, 16, 32, 32, device=device)
|
||||
x += y
|
||||
return torch.nn.functional.relu(x)
|
||||
self._check_kernel_count(fn, 6)
|
||||
self._check_kernel_count(fn, 7)
|
||||
|
||||
def test_conv_bn_add_relu_fusion(self):
|
||||
def fn():
|
||||
@@ -92,7 +89,7 @@ class TestKernelFusionRegression(unittest.TestCase):
|
||||
out = bn(conv(x))
|
||||
out += identity
|
||||
return torch.nn.functional.relu(out)
|
||||
self._check_kernel_count(fn, 16)
|
||||
self._check_kernel_count(fn, 12)
|
||||
|
||||
def test_multiple_inplace_ops_fusion(self):
|
||||
def fn():
|
||||
@@ -117,7 +114,7 @@ class TestKernelFusionRegression(unittest.TestCase):
|
||||
bn.train()
|
||||
with torch.no_grad():
|
||||
return bn(x)
|
||||
self._check_kernel_count(fn, 10)
|
||||
self._check_kernel_count(fn, 8)
|
||||
|
||||
# this is a minimal extra/other_mnist/beautiful_mnist_torch.py to cover fusion for training with optimizer
|
||||
def test_mnist_training_fusion(self):
|
||||
@@ -138,7 +135,7 @@ class TestKernelFusionRegression(unittest.TestCase):
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
return loss
|
||||
self._check_kernel_count(fn, 33)
|
||||
self._check_kernel_count(fn, 24)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.application-identifier</key>
|
||||
<string>9YG3G8543N.org.tinygrad.tinygpu.edriver</string>
|
||||
<key>com.apple.developer.driverkit</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.driverkit.transport.pci</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>IOPCIPrimaryMatch</key>
|
||||
<string>0x000010de&0x0000FFFF</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
xcodebuild clean build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO -alltargets -configuration Release build
|
||||
|
||||
cp "../profiles/edriver_rel_2.provisionprofile" "./build/Release/TinyGPU.app/Contents/Library/SystemExtensions/org.tinygrad.tinygpu.edriver.dext/embedded.provisionprofile"
|
||||
cp "../profiles/installer_provisioning.provisionprofile" "./build/Release/TinyGPU.app/Contents/embedded.provisionprofile"
|
||||
|
||||
codesign \
|
||||
--sign "Developer ID Application: tinygrad, Corp. (9YG3G8543N)" \
|
||||
--entitlements ./TinyGPUDriverExtension/TinyGPUDriver.NV.Release.entitlements \
|
||||
--verbose \
|
||||
--options runtime \
|
||||
--timestamp \
|
||||
--force \
|
||||
./build/Release/TinyGPU.app/Contents/Library/SystemExtensions/org.tinygrad.tinygpu.edriver.dext
|
||||
|
||||
codesign \
|
||||
--sign "Developer ID Application: tinygrad, Corp. (9YG3G8543N)" \
|
||||
--entitlements ./macOS/macOS.entitlements \
|
||||
--options runtime \
|
||||
--verbose \
|
||||
--timestamp \
|
||||
--force \
|
||||
./build/Release/TinyGPU.app
|
||||
|
||||
codesign --verify --deep --strict --verbose=4 ./build/Release/TinyGPU.app/Contents/Library/SystemExtensions/org.tinygrad.tinygpu.edriver.dext
|
||||
|
||||
codesign --verify --deep --strict --verbose=4 ./build/Release/TinyGPU.app
|
||||
|
||||
spctl -a -vv ./build/Release/TinyGPU.app
|
||||
|
||||
spctl -a -vv ./build/Release/TinyGPU.app/Contents/Library/SystemExtensions/org.tinygrad.tinygpu.edriver.dext
|
||||
+3
-3
@@ -1,17 +1,17 @@
|
||||
A command line tool for exploring the VIZ trace.
|
||||
|
||||
After running with VIZ=-1, use `PYTHONPATH=. extra/viz/cli.py` to explore the saved trace files.
|
||||
After running with VIZ=-1, use `extra/viz/cli.py` to explore the saved trace files.
|
||||
|
||||
## Inspect runtime profiling
|
||||
|
||||
Use `PYTHONPATH=. extra/viz/cli.py --profile` to list all traced devices.
|
||||
Use `extra/viz/cli.py --profile` to list all traced devices.
|
||||
|
||||
List top slowest kernels on a device: `--profile --device "AMD"`
|
||||
List samples of a kernel on a device: `--profile --device "AMD" --kernel E_3`
|
||||
|
||||
## Inspect codegen and PatternMatcher
|
||||
|
||||
Use `PYTHONPATH=. extra/viz/cli.py --rewrites` to list all traced kernels.
|
||||
Use `extra/viz/cli.py --rewrites` to list all traced kernels.
|
||||
|
||||
List all codegen steps for a kernel: `--rewrites --kernel E_3`
|
||||
Get source code: `--rewrites --kernel E_3 --select "View Source"`
|
||||
|
||||
+48
-19
@@ -1,44 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
os.environ["VIZ"] = "0"
|
||||
import argparse, pathlib
|
||||
import argparse, pathlib, sys, struct, json
|
||||
from typing import Iterator
|
||||
from tinygrad.viz import serve as viz
|
||||
from tinygrad.uop.ops import RewriteTrace
|
||||
from tinygrad.helpers import temp, ansistrip, colored, time_to_str, ansilen
|
||||
from test.null.test_viz import load_profile
|
||||
|
||||
# ** generic helpers
|
||||
|
||||
def optional_eq(val:dict, arg:str|None) -> bool: return arg is None or ansistrip(val["name"]) == arg
|
||||
|
||||
def print_data(data:dict) -> None:
|
||||
if isinstance(data.get("value"), Iterator):
|
||||
for m in data["value"]:
|
||||
if m.get("uop"):
|
||||
print("Input UOp:")
|
||||
print(m["uop"])
|
||||
if not m["diff"]: continue
|
||||
print("Rewrites:")
|
||||
fp = pathlib.Path(m["upat"][0][0])
|
||||
print(f"{fp.parent.name}/{fp.name}:{m['upat'][0][1]}")
|
||||
print(m["upat"][1])
|
||||
for line in m["diff"]:
|
||||
color = "red" if line.startswith("-") else "green" if line.startswith("+") else None
|
||||
print(colored(line, color))
|
||||
if m.get("uop"): print(f"Input UOp:\n{m['uop']}")
|
||||
if m.get("diff"):
|
||||
loc = pathlib.Path(m["upat"][0][0])
|
||||
print(f"Rewrite at {loc.parent.name}/{loc.name}:{m['upat'][0][1]}\n{m['upat'][1]}")
|
||||
for line in m["diff"]: print(colored(line, "red" if line.startswith("-") else "green" if line.startswith("+") else None))
|
||||
if data.get("src") is not None: print(data["src"])
|
||||
|
||||
# ** Profiler trace decoder
|
||||
|
||||
# 0 means None, otherwise it's an enum value
|
||||
def option(i:int) -> int|None: return None if i == 0 else i-1
|
||||
|
||||
def decode_profile(data:bytes) -> dict:
|
||||
ret, off = data, 0
|
||||
def u(fmt:str) -> tuple:
|
||||
nonlocal off
|
||||
vals = struct.unpack_from(fmt, ret, off)
|
||||
off += struct.calcsize(fmt)
|
||||
return vals
|
||||
total_dur, global_peak, index_len, layout_len = u("<IQII")
|
||||
strings, dtypes, markers = json.loads(ret[off:off+index_len]).values()
|
||||
off += index_len
|
||||
layout:dict[str, dict] = {}
|
||||
for _ in range(layout_len):
|
||||
klen = u("<B")[0]
|
||||
k = ret[off:off+klen].decode()
|
||||
off += klen
|
||||
layout[k] = v = {"events":[]}
|
||||
event_type, event_count = u("<BI")
|
||||
if event_type == 0:
|
||||
for _ in range(event_count):
|
||||
name, ref, key, st, dur, fmt = u("<IIIIfI")
|
||||
v["events"].append({"name":strings[name], "ref":option(ref), "key":option(key), "st":st, "dur":dur, "fmt":strings[fmt]})
|
||||
else:
|
||||
v["peak"] = u("<Q")[0]
|
||||
for _ in range(event_count):
|
||||
alloc, ts, key = u("<BII")
|
||||
if alloc: v["events"].append({"event":"alloc", "ts":ts, "key":key, "arg": {"dtype":strings[u("<I")[0]], "sz":u("<Q")[0]}})
|
||||
else: v["events"].append({"event":"free", "ts":ts, "key":key, "arg": {"users":[u("<IIIB") for _ in range(u("<I")[0])]}})
|
||||
return {"dur":total_dur, "peak":global_peak, "layout":layout, "markers":markers}
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
g_mode = parser.add_argument_group("mode")
|
||||
g_mode.add_argument("--profile", action="store_true", help="View profile trace")
|
||||
g_mode.add_argument("--rewrites", action="store_true", help="View rewrites trace")
|
||||
g_common = parser.add_argument_group("common options")
|
||||
g_common.add_argument("--kernel", type=str, default=None, metavar="NAME", help="Select a kernel by name (optional name, default: only list names)")
|
||||
g_profile = parser.add_argument_group("profile options")
|
||||
g_profile.add_argument("--device", type=str, default=None, metavar="NAME", help="Select a device (optional name, default: only list names)")
|
||||
g_profile.add_argument("--top", type=int, default=10, metavar="N", help="Number of top kernels to show (-1 for all, default: 10)")
|
||||
g_rewrites = parser.add_argument_group("rewrites options")
|
||||
g_rewrites.add_argument("--select", type=str, default=None, metavar="NAME",
|
||||
help="Select an item within the chosen kernel (optional name, default: only list names)")
|
||||
g_common = parser.add_argument_group("common options")
|
||||
g_common.add_argument("--kernel", type=str, default=None, metavar="NAME", help="Select a kernel by name (optional name, default: only list names)")
|
||||
parser.add_argument("--profile-path", type=pathlib.Path, metavar="PATH", help="Path to profile (optional file, default: latest profile)",
|
||||
default=pathlib.Path(temp("profile.pkl", append_user=True)))
|
||||
parser.add_argument("--rewrites-path", type=pathlib.Path, metavar="PATH", help="Path to rewrites (optional file, default: latest rewrites)",
|
||||
@@ -46,14 +75,14 @@ if __name__ == "__main__":
|
||||
args = parser.parse_args()
|
||||
if not args.profile and not args.rewrites:
|
||||
parser.print_help()
|
||||
exit(0)
|
||||
sys.exit(0)
|
||||
|
||||
viz.trace = viz.load_pickle(args.rewrites_path, default=RewriteTrace([], [], {}))
|
||||
viz.ctxs = viz.get_rewrites(viz.trace)
|
||||
|
||||
if args.profile:
|
||||
from tabulate import tabulate
|
||||
profile = load_profile(viz.load_pickle(args.profile_path, default=[]))
|
||||
profile = decode_profile(viz.get_profile(viz.load_pickle(args.profile_path, default=[])))
|
||||
agg, total, n = {}, 0, 0
|
||||
if args.device is None: print("Select a device:")
|
||||
for k,v in profile["layout"].items():
|
||||
@@ -63,7 +92,7 @@ if __name__ == "__main__":
|
||||
for e in v.get("events", []):
|
||||
et = e["dur"]*1e-6
|
||||
if args.kernel is not None:
|
||||
if ansistrip(e["name"]) == args.kernel and n < 10:
|
||||
if optional_eq(e, args.kernel) and n < 10:
|
||||
ptm = colored(time_to_str(et, w=9), "yellow" if et > 0.01 else None) if et is not None else ""
|
||||
name = e["name"]+(" " * (46 - ansilen(e["name"])))
|
||||
print(f"{name} {ptm}/{(et or 0)*1e3:9.2f}ms "+e['fmt'].replace('\n', ' | ')+" ")
|
||||
@@ -81,7 +110,7 @@ if __name__ == "__main__":
|
||||
other_t = total-sum(t for _, (t, _) in sel)
|
||||
table.append([f"Other ({len(other)} unique)", time_to_str(other_t, w=9), sum(c for _,(_,c) in other), f"{other_t/total*100.0:.2f}%"])
|
||||
print(tabulate(table, headers=["name", "total", "count", "pct"], tablefmt="github"))
|
||||
exit(0)
|
||||
sys.exit(0)
|
||||
|
||||
for k in viz.ctxs:
|
||||
if not optional_eq(k, args.kernel): continue
|
||||
|
||||
+14
-5
@@ -324,6 +324,12 @@ def _disasm_smem(inst: SMEM) -> str:
|
||||
if name in ('s_memrealtime', 's_memtime'): return f"{name} {_fmt_sdst(inst.sdata, dst_n, cdna)}"
|
||||
return f"{name} {_fmt_sdst(inst.sdata, dst_n, cdna)}, {sbase_str}, {off_s}" + _mods((inst.glc, " glc"), (getattr(inst, 'dlc', 0), " dlc"))
|
||||
|
||||
R4_TH_LOAD = {1: 'TH_LOAD_NT', 2: 'TH_LOAD_HT', 3: 'TH_LOAD_LU', 4: 'TH_LOAD_RT_WB', 5: 'TH_LOAD_NT_WB'}
|
||||
R4_TH_STORE = {1: 'TH_STORE_NT', 2: 'TH_STORE_HT', 3: 'TH_STORE_ST', 4: 'TH_STORE_RT_WB', 5: 'TH_STORE_NT_WB'}
|
||||
R4_TH_ATOMIC = {1: 'TH_ATOMIC_RETURN', 2: 'TH_ATOMIC_NT', 3: 'TH_ATOMIC_RETURN_NT',
|
||||
4: 'TH_ATOMIC_CASCADE_RT', 5: 'TH_ATOMIC_CASCADE_RETURN', 6: 'TH_ATOMIC_CASCADE_NT', 7: 'TH_ATOMIC_CASCADE_RETURN_NT'}
|
||||
R4_SCOPE = {1: 'SCOPE_SE', 2: 'SCOPE_DEV', 3: 'SCOPE_SYS'}
|
||||
|
||||
def _disasm_flat(inst: FLAT) -> str:
|
||||
name, cdna, r4 = inst.op_name.lower(), _is_cdna(inst), _is_r4(inst)
|
||||
acc = getattr(inst, 'acc', 0)
|
||||
@@ -331,9 +337,10 @@ def _disasm_flat(inst: FLAT) -> str:
|
||||
if r4: seg = 'flat' if (cls_name:=inst.__class__.__name__) == 'VFLAT' else ('global' if cls_name == 'VGLOBAL' else 'scratch')
|
||||
else: seg = ['flat', 'scratch', 'global'][inst.seg] if inst.seg < 3 else 'flat'
|
||||
instr = f"{seg}_{name.split('_', 1)[1] if '_' in name else name}"
|
||||
# Global/scratch uses 13-bit signed offset
|
||||
# Global/scratch uses 13-bit signed offset (RDNA3/CDNA), 24-bit signed offset (RDNA4)
|
||||
offset = inst.ioffset if r4 else inst.offset # type: ignore[attr-defined]
|
||||
if seg != 'flat':
|
||||
if r4: off_val = offset if offset < (1 << 23) else offset - (1 << 24) # sign extend 24-bit
|
||||
elif seg != 'flat':
|
||||
if cdna:
|
||||
# CDNA: bit 12 is sign bit but not in offset field
|
||||
raw = int.from_bytes(inst.to_bytes(), 'little')
|
||||
@@ -348,7 +355,9 @@ def _disasm_flat(inst: FLAT) -> str:
|
||||
w = regs.get('data', regs.get('d', 1)) if 'store' in name or 'atomic' in name else regs.get('d', 1)
|
||||
off_s = f" offset:{off_val}" if off_val else ""
|
||||
if cdna: mods = f"{off_s}{' sc0' if inst.sc0 else ''}{' nt' if inst.nt else ''}{' sc1' if getattr(inst, 'sc1', 0) else ''}" # type: ignore[attr-defined]
|
||||
elif r4: mods = f"{off_s}{' scope' if inst.scope else ''}{' th' if inst.th else ''}" # type: ignore[attr-defined]
|
||||
elif r4:
|
||||
th_names = R4_TH_ATOMIC if 'atomic' in name else (R4_TH_STORE if 'store' in name else R4_TH_LOAD)
|
||||
mods = off_s + (f" th:{th_names[inst.th]}" if inst.th in th_names else "") + (f" scope:{R4_SCOPE[inst.scope]}" if inst.scope in R4_SCOPE else "")
|
||||
else: mods = f"{off_s}{' glc' if inst.glc else ''}{' slc' if inst.slc else ''}{' dlc' if inst.dlc else ''}"
|
||||
if seg == 'flat': saddr_s = ""
|
||||
elif _unwrap(inst.saddr) in (0x7F, 124): saddr_s = ", off"
|
||||
@@ -357,7 +366,7 @@ def _disasm_flat(inst: FLAT) -> str:
|
||||
saddr_s = f", {(SPECIAL_PAIRS_CDNA if cdna else SPECIAL_PAIRS)[_unwrap(inst.saddr)]}"
|
||||
elif t := _ttmp(inst.saddr, 2): saddr_s = f", {t}"
|
||||
else: saddr_s = f", {_sreg(inst.saddr, 2) if _unwrap(inst.saddr) < 106 else decode_src(_unwrap(inst.saddr), cdna)}"
|
||||
if 'addtid' in name: return f"{instr} {reg_fn(inst.data if 'store' in name else inst.vdst)}{saddr_s}{mods}"
|
||||
if 'addtid' in name: return f"{instr} {reg_fn((inst.vsrc if r4 else inst.data) if 'store' in name else inst.vdst)}{saddr_s}{mods}"
|
||||
# RDNA4: vaddr instead of addr, vsrc instead of data
|
||||
addr = inst.vaddr if r4 else inst.addr # type: ignore[attr-defined]
|
||||
data = inst.vsrc if r4 else inst.data # type: ignore[attr-defined]
|
||||
@@ -372,7 +381,7 @@ def _disasm_flat(inst: FLAT) -> str:
|
||||
addr_s = "off" if not inst.sve and seg == 'scratch' else _vreg(addr, addr_w)
|
||||
data_s, vdst_s = reg_fn(data, w), reg_fn(inst.vdst, w // 2 if 'cmpswap' in name else w)
|
||||
if 'atomic' in name:
|
||||
glc_or_sc0 = inst.sc0 if cdna else inst.glc # type: ignore[attr-defined]
|
||||
glc_or_sc0 = inst.sc0 if cdna else (inst.th & 1 if r4 else inst.glc) # type: ignore[attr-defined]
|
||||
sfx = f"{saddr_s if seg != 'flat' else ''}{mods}"
|
||||
return f"{instr} {vdst_s}, {addr_s}, {data_s}{sfx}" if glc_or_sc0 else f"{instr} {addr_s}, {data_s}{sfx}"
|
||||
if 'store' in name: return f"{instr} {addr_s}, {data_s}{saddr_s}{mods}"
|
||||
|
||||
@@ -104,6 +104,34 @@ class TestCmpClass(unittest.TestCase):
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 0, "Signaling NaN should not match quiet mask")
|
||||
|
||||
def test_v_cmp_lg_f32_nan(self):
|
||||
"""v_cmp_lg_f32 is ordered not-equal (<>): NaN <> x should be False per IEEE 754."""
|
||||
quiet_nan = 0x7fc00000
|
||||
one_f32 = 0x3f800000 # 1.0f
|
||||
instructions = [
|
||||
s_mov_b32(s[0], quiet_nan),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[1], one_f32),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_cmp_lg_f32_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 0, "v_cmp_lg_f32(NaN, 1.0) should be 0")
|
||||
|
||||
def test_v_cmp_neq_f32_nan(self):
|
||||
"""v_cmp_neq_f32 is unordered not-equal (!=): NaN != x should be True per IEEE 754."""
|
||||
quiet_nan = 0x7fc00000
|
||||
one_f32 = 0x3f800000 # 1.0f
|
||||
instructions = [
|
||||
s_mov_b32(s[0], quiet_nan),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[1], one_f32),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_cmp_neq_f32_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "v_cmp_neq_f32(NaN, 1.0) should be 1")
|
||||
|
||||
def test_v_cmp_sets_vcc_bits(self):
|
||||
"""V_CMP_EQ sets VCC bits based on per-lane comparison."""
|
||||
instructions = [
|
||||
|
||||
@@ -40,7 +40,7 @@ RDNA4_FILES = ['gfx12_asm_sop1.s', 'gfx12_asm_sop2.s', 'gfx12_asm_sopp.s', 'gfx1
|
||||
'gfx12_asm_vop1.s', 'gfx12_asm_vop2.s', 'gfx12_asm_vopc.s', 'gfx12_asm_vopcx.s', 'gfx12_asm_vop3.s', 'gfx12_asm_vop3c.s',
|
||||
'gfx12_asm_vop3cx.s', 'gfx12_asm_vop3p.s', 'gfx12_asm_vop3_from_vop1.s', 'gfx12_asm_vop3_from_vop2.s',
|
||||
'gfx12_asm_vop3p_features.s', 'gfx12_asm_vopd.s', 'gfx12_asm_vopd_features.s',
|
||||
'gfx12_asm_ds.s', 'gfx12_asm_smem.s',
|
||||
'gfx12_asm_ds.s', 'gfx12_asm_smem.s', 'gfx12_asm_vflat.s',
|
||||
'gfx12_asm_wmma_w32.s']
|
||||
|
||||
def _parse_llvm_tests(text: str, pattern: str) -> list[tuple[str, bytes]]:
|
||||
|
||||
@@ -21,7 +21,7 @@ OTHER_SIMD_OPS = {InstOp.OTHER_LDS_LOAD, InstOp.OTHER_LDS_STORE, InstOp.OTHER_LD
|
||||
InstOp.OTHER_FLAT_STORE_128, InstOp.OTHER_GLOBAL_LOAD, InstOp.OTHER_GLOBAL_LOAD_VADDR,
|
||||
InstOp.OTHER_GLOBAL_STORE_64, InstOp.OTHER_GLOBAL_STORE_96, InstOp.OTHER_GLOBAL_STORE_128,
|
||||
InstOp.OTHER_GLOBAL_STORE_VADDR_128}
|
||||
OTHER_SIMD_OPS_RDNA4 = {InstOpRDNA4.OTHER_VMEM, InstOpRDNA4.OTHER_VMEM_STORE}
|
||||
OTHER_SIMD_OPS_RDNA4 = {InstOpRDNA4.OTHER_VMEM}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# ROCPROF DECODER
|
||||
@@ -208,12 +208,12 @@ class SQTTExamplesTestBase(unittest.TestCase):
|
||||
class TestSQTTExamplesRDNA3(SQTTExamplesTestBase):
|
||||
target = "gfx1100"
|
||||
expected = {
|
||||
"profile_empty_run_0": [1844, 1885, 1905, 1956, 1983, 1889],
|
||||
"profile_empty_run_1": [1780, 1885, 1905, 1956, 1983, 1889],
|
||||
"profile_gemm_run_0": [2656, 2025, 2045, 2096, 2123, 2029, 3183, 2019, 2039, 2090, 2117, 2023, 19119, 2013, 2033, 2084, 2111, 2017],
|
||||
"profile_gemm_run_1": [2662, 2025, 2045, 2096, 2123, 2029, 3179, 2019, 2039, 2090, 2117, 2023, 19113, 2071, 2091, 2142, 2169, 2075],
|
||||
"profile_plus_run_0": [1886, 2013, 2033, 2084, 2111, 2017],
|
||||
"profile_plus_run_1": [1988, 2071, 2091, 2142, 2169, 2075],
|
||||
"profile_empty_run_0": [1744, 1801, 1854, 1890, 1917, 1822],
|
||||
"profile_empty_run_1": [1744, 1801, 1854, 1886, 1921, 1906],
|
||||
"profile_gemm_run_0": [1800, 1867, 1899, 1898, 1914, 1895, 1694, 1779, 1819, 1872, 1877, 1858, 1750, 1834, 1866, 1834, 1911, 1796],
|
||||
"profile_gemm_run_1": [1806, 1874, 1837, 1885, 1907, 1906, 1694, 1778, 1810, 1873, 1885, 1867, 1750, 1834, 1866, 1856, 1903, 1897],
|
||||
"profile_plus_run_0": [1744, 1878, 1854, 1890, 1878, 1910],
|
||||
"profile_plus_run_1": [1744, 1878, 1854, 1886, 1921, 1909],
|
||||
}
|
||||
|
||||
class TestSQTTExamplesRDNA4(SQTTExamplesTestBase): target = "gfx1200"
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import unittest, contextlib
|
||||
from tinygrad import Device, Tensor, Context, TinyJit
|
||||
from tinygrad.device import Compiled, ProfileProgramEvent, ProfileDeviceEvent
|
||||
from tinygrad.viz.serve import load_amd_counters
|
||||
|
||||
@contextlib.contextmanager
|
||||
def save_sqtt():
|
||||
yield (ret:=[])
|
||||
Device[Device.DEFAULT].synchronize()
|
||||
Device[Device.DEFAULT]._at_profile_finalize()
|
||||
load_amd_counters(ret, Compiled.profile_events)
|
||||
ret[:] = [r for r in ret if r["name"].startswith("Exec")]
|
||||
|
||||
@unittest.skipUnless(Device.DEFAULT == "AMD", "only runs on AMD")
|
||||
class TestSQTTProfiler(unittest.TestCase):
|
||||
# TODO: can we enable SQTT profiling in context?
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if not Device[Device.DEFAULT].sqtt_enabled: raise unittest.SkipTest("device must be in SQTT profiling mode")
|
||||
|
||||
def setUp(self):
|
||||
Device[Device.DEFAULT].synchronize()
|
||||
Compiled.profile_events[:] = [e for e in Compiled.profile_events if isinstance(e, (ProfileProgramEvent, ProfileDeviceEvent))]
|
||||
|
||||
def test_simple(self):
|
||||
t = Tensor.empty(1) + 1
|
||||
with save_sqtt() as sqtt:
|
||||
ei = t.schedule()[0].lower()
|
||||
ei.run()
|
||||
self.assertEqual(len(sqtt), 1)
|
||||
self.assertEqual(sqtt[0]["name"], f"Exec {ei.prg.p.function_name}")
|
||||
|
||||
def test_multiple_runs(self):
|
||||
t = Tensor.empty(1) + 1
|
||||
with save_sqtt() as sqtt:
|
||||
ei = t.schedule()[0].lower()
|
||||
for _ in range(N:=3):
|
||||
ei.run()
|
||||
self.assertEqual(len(sqtt), N)
|
||||
for i in range(1, N):
|
||||
self.assertEqual(sqtt[i]["name"], f"Exec {ei.prg.p.function_name} n{i+1}")
|
||||
|
||||
def test_multiple_kernels(self):
|
||||
t = ((Tensor.empty(1) + 1).contiguous() + 2)
|
||||
sched = t.schedule()
|
||||
with save_sqtt() as sqtt:
|
||||
for si in sched: si.lower().run()
|
||||
self.assertEqual(len(sqtt), len(sched))
|
||||
for i,k in enumerate(sched):
|
||||
self.assertEqual(sqtt[i]["name"], f"Exec {k.lower().prg.p.function_name}")
|
||||
|
||||
def test_multiple_kernels_lower(self):
|
||||
t = ((Tensor.empty(1) + 1).contiguous() + 2)
|
||||
sched = t.schedule()
|
||||
with save_sqtt() as sqtt:
|
||||
prgs = [si.lower() for si in sched]
|
||||
for p in prgs: p.run()
|
||||
self.assertEqual(len(sqtt), len(sched))
|
||||
for i,ei in enumerate(prgs):
|
||||
self.assertEqual(sqtt[i]["name"], f"Exec {ei.prg.p.function_name}")
|
||||
|
||||
def test_jit(self):
|
||||
@TinyJit
|
||||
def f(a): return a + 1
|
||||
t = Tensor.empty(1)
|
||||
with save_sqtt() as sqtt:
|
||||
for _ in range(N:=5):
|
||||
f(t).realize()
|
||||
self.assertEqual(len(sqtt), N)
|
||||
kernel_name = sqtt[0]["name"]
|
||||
for i,s in enumerate(sqtt[1:], start=1): self.assertEqual(s["name"], f"{kernel_name} n{i+1}")
|
||||
|
||||
# TODO: can we trace SQTT for graphed kernels?
|
||||
def test_jit_graph(self, kernel_count=3*2):
|
||||
@TinyJit
|
||||
def f(a): return ((a + 1).contiguous() + 2).contiguous().sum()
|
||||
t = Tensor.empty(32)
|
||||
with save_sqtt() as sqtt:
|
||||
for _ in range(5):
|
||||
f(t).realize()
|
||||
names = [s["name"] for s in sqtt]
|
||||
k0, k1, k2 = names[:3]
|
||||
for i in range(3, len(sqtt), 3):
|
||||
n = (i // 3)+1
|
||||
self.assertEqual(names[i], f"{k0} n{n}")
|
||||
self.assertEqual(names[i+1], f"{k1} n{n}")
|
||||
self.assertEqual(names[i+2], f"{k2} n{n}")
|
||||
self.assertEqual(len(sqtt), kernel_count)
|
||||
|
||||
@Context(JIT=2)
|
||||
def test_jit_multiple_kernels(self): self.test_jit_graph(kernel_count=3*5)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,183 +0,0 @@
|
||||
"""Tests comparing sqtt.py PACKET_TYPES_RDNA3/RDNA4 against AMD's rocprof-trace-decoder binary."""
|
||||
import unittest, struct, ctypes, pickle
|
||||
from pathlib import Path
|
||||
|
||||
ROCPROF_LIB = Path("/usr/lib/librocprof-trace-decoder.so")
|
||||
import tinygrad
|
||||
EXAMPLES_DIR = Path(tinygrad.__file__).parent.parent / "extra/sqtt/examples"
|
||||
|
||||
# CDNA pkt_fmt -> size in bytes (extracted from rocprof hash table)
|
||||
CDNA_PKT_SIZES = {0: 2, 1: 8, 2: 8, 3: 4, 4: 2, 5: 6, 6: 2, 7: 2, 8: 2, 9: 2, 10: 2, 11: 8, 12: 6, 13: 4, 14: 8, 15: 6}
|
||||
|
||||
def _find_segment(perms: str):
|
||||
"""Find a segment of the loaded library with given permissions (e.g. 'rw-p', 'r--p')."""
|
||||
with open('/proc/self/maps', 'r') as f:
|
||||
for line in f:
|
||||
if 'librocprof-trace-decoder.so' in line and f' {perms} ' in line:
|
||||
parts = line.split()
|
||||
return int(parts[0].split('-')[0], 16), int(parts[2], 16)
|
||||
return None, None
|
||||
|
||||
def _read_array(file_offset: int, count: int):
|
||||
"""Read an array of uint8 at file_offset from the loaded library."""
|
||||
base, seg_offset = _find_segment('rw-p')
|
||||
if base is None: return None
|
||||
return list((ctypes.c_uint8 * count).from_address(base + (file_offset - seg_offset)))
|
||||
|
||||
def _load_lib():
|
||||
if not ROCPROF_LIB.exists(): return False
|
||||
ctypes.CDLL(str(ROCPROF_LIB))
|
||||
return True
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# RDNA EXTRACTION (nibble-based format)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def extract_bit_tables():
|
||||
"""Extract bit budget tables. Returns (layout2, layout3, layout4) or None."""
|
||||
if not _load_lib(): return None
|
||||
return _read_array(0x2d220, 32), _read_array(0x2d280, 32), _read_array(0x2d2c0, 32)
|
||||
|
||||
def extract_delta_fields():
|
||||
"""Extract delta bitfield tables. Returns (layout2, layout3, layout4) dicts mapping type_id -> (lo, hi)."""
|
||||
if not _load_lib(): return None
|
||||
ro_base, ro_offset = _find_segment('r--p')
|
||||
if ro_base is None: return None
|
||||
|
||||
def read_table(file_offset, num_entries):
|
||||
addr = ro_base + (file_offset - ro_offset)
|
||||
data = bytes((ctypes.c_uint8 * (num_entries * 12)).from_address(addr))
|
||||
return {type_id: (lo, hi) for j in range(0, len(data), 12)
|
||||
for type_id, lo, hi in [struct.unpack('<III', data[j:j+12])] if type_id < 32}
|
||||
|
||||
return read_table(0x26800, 24), read_table(0x26dc0, 25), read_table(0x27300, 27)
|
||||
|
||||
def extract_packet_encodings():
|
||||
"""Extract packet encodings. Returns (L2, L3, L4) dicts mapping type_id -> (mask, value)."""
|
||||
if not _load_lib(): return None
|
||||
rw_base, rw_offset = _find_segment('rw-p')
|
||||
if rw_base is None: return None
|
||||
|
||||
# Read base encodings from registration vector at 0x2d340
|
||||
vec_start = ctypes.c_void_p.from_address(rw_base + (0x2d340 - rw_offset)).value
|
||||
vec_end = ctypes.c_void_p.from_address(rw_base + (0x2d348 - rw_offset)).value
|
||||
base = {}
|
||||
if vec_start and vec_end:
|
||||
for i in range((vec_end - vec_start) // 32):
|
||||
addr = vec_start + i * 32
|
||||
type_id = ctypes.c_uint8.from_address(addr).value
|
||||
pat_start = ctypes.c_void_p.from_address(addr + 8).value
|
||||
pat_end = ctypes.c_void_p.from_address(addr + 16).value
|
||||
if pat_start and pat_end and 0 < (n := pat_end - pat_start) <= 8:
|
||||
pat = list((ctypes.c_uint8 * n).from_address(pat_start))
|
||||
base[type_id] = (sum(1 << j for j in range(n)), sum(b << j for j, b in enumerate(pat)))
|
||||
|
||||
return {**base, 17: (0x7f, 0x51), 25: (0x7f, 0x31)}, base, {**base} # L2 has overrides
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CDNA EXTRACTION (16-bit header format)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def extract_cdna_packet_sizes():
|
||||
"""Extract CDNA pkt_fmt -> size mapping by running rocprof decoder to populate its hash table."""
|
||||
if not _load_lib(): return None
|
||||
from test.amd.test_sqtt_examples import run_rocprof_decoder
|
||||
|
||||
if not (pkl_path := next((EXAMPLES_DIR / "gfx950").glob("*.pkl"), None)): return None
|
||||
with open(pkl_path, "rb") as f: data = pickle.load(f)
|
||||
sqtt_events = [e for e in data if type(e).__name__ == "ProfileSQTTEvent"]
|
||||
prg = next((e for e in data if type(e).__name__ == "ProfileProgramEvent"), None)
|
||||
if not sqtt_events or not prg: return None
|
||||
|
||||
# Run decoder to trigger hash table initialization
|
||||
run_rocprof_decoder([e.blob for e in sqtt_events], prg.lib, prg.base, "gfx950")
|
||||
|
||||
# Extract hash table: head at 0x2d4f0, nodes are 16 bytes (next[8], key[4], value[4])
|
||||
rw_base, rw_offset = _find_segment('rw-p')
|
||||
if not (head := ctypes.c_void_p.from_address(rw_base + (0x2d4f0 - rw_offset)).value if rw_base else None): return None
|
||||
|
||||
pkt_sizes: dict[int, int] = {}
|
||||
node, seen = head, set()
|
||||
while node and node not in seen and len(pkt_sizes) < 20:
|
||||
seen.add(node)
|
||||
key, val = ctypes.c_uint32.from_address(node + 8).value, ctypes.c_uint32.from_address(node + 12).value
|
||||
if key < 16 and val in (0x10, 0x20, 0x30, 0x40): pkt_sizes[key] = {0x10: 2, 0x20: 4, 0x30: 6, 0x40: 8}[val]
|
||||
node = ctypes.c_void_p.from_address(node).value # type: ignore[assignment]
|
||||
return pkt_sizes if len(pkt_sizes) == 16 else None
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# TESTS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
class TestSQTTMatchesBinary(unittest.TestCase):
|
||||
def test_bit_counts_match_layout3(self): self._test_bit_counts(3)
|
||||
def test_bit_counts_match_layout4(self): self._test_bit_counts(4)
|
||||
def test_encodings_match_layout3(self): self._test_encodings(3)
|
||||
def test_encodings_match_layout4(self): self._test_encodings(4)
|
||||
def test_delta_fields_match_layout3(self): self._test_delta_fields(3)
|
||||
def test_delta_fields_match_layout4(self): self._test_delta_fields(4)
|
||||
|
||||
def test_cdna_packet_sizes(self):
|
||||
"""Extract and verify CDNA pkt_fmt -> size mapping from rocprof's hash table."""
|
||||
if not (EXAMPLES_DIR / "gfx950").exists(): self.skipTest("no CDNA examples")
|
||||
if not (pkt_sizes := extract_cdna_packet_sizes()): self.skipTest("rocprof-trace-decoder not installed")
|
||||
for pkt_fmt, size in CDNA_PKT_SIZES.items():
|
||||
with self.subTest(pkt_fmt=pkt_fmt): self.assertEqual(pkt_sizes.get(pkt_fmt), size)
|
||||
|
||||
def test_cdna_packet_definitions(self):
|
||||
from tinygrad.renderer.amd.sqtt import PACKET_TYPES_CDNA
|
||||
for pkt_fmt, pkt_cls in PACKET_TYPES_CDNA.items():
|
||||
with self.subTest(packet=pkt_cls.__name__):
|
||||
self.assertEqual(pkt_cls.encoding.default, pkt_fmt)
|
||||
self.assertEqual(CDNA_PKT_SIZES[pkt_fmt] * 2, pkt_cls._size_nibbles) # type: ignore[attr-defined]
|
||||
|
||||
def _test_bit_counts(self, layout: int):
|
||||
if not (tables := extract_bit_tables()): self.skipTest("rocprof-trace-decoder not installed")
|
||||
from tinygrad.renderer.amd.sqtt import PACKET_TYPES_RDNA3, PACKET_TYPES_RDNA4
|
||||
for type_id, pkt_cls in {3: PACKET_TYPES_RDNA3, 4: PACKET_TYPES_RDNA4}[layout].items():
|
||||
with self.subTest(packet=pkt_cls.__name__):
|
||||
self.assertEqual(pkt_cls._size_nibbles * 4, tables[layout - 2][type_id]) # type: ignore[attr-defined]
|
||||
|
||||
def _test_encodings(self, layout: int):
|
||||
if not (encodings := extract_packet_encodings()): self.skipTest("rocprof-trace-decoder not installed")
|
||||
from tinygrad.renderer.amd.sqtt import PACKET_TYPES_RDNA3, PACKET_TYPES_RDNA4
|
||||
for type_id, pkt_cls in {3: PACKET_TYPES_RDNA3, 4: PACKET_TYPES_RDNA4}[layout].items():
|
||||
with self.subTest(packet=pkt_cls.__name__):
|
||||
self.assertEqual((pkt_cls.encoding.mask, pkt_cls.encoding.default), encodings[layout - 2][type_id])
|
||||
|
||||
def _test_delta_fields(self, layout: int):
|
||||
if not (deltas := extract_delta_fields()): self.skipTest("rocprof-trace-decoder not installed")
|
||||
from tinygrad.renderer.amd.sqtt import PACKET_TYPES_RDNA3, PACKET_TYPES_RDNA4
|
||||
for type_id, pkt_cls in {3: PACKET_TYPES_RDNA3, 4: PACKET_TYPES_RDNA4}[layout].items():
|
||||
if type_id not in deltas[layout - 2]: continue
|
||||
delta = getattr(pkt_cls, 'delta', None)
|
||||
actual = (0, 0) if delta is None else (delta.lo, delta.hi + 1)
|
||||
with self.subTest(packet=pkt_cls.__name__): self.assertEqual(actual, deltas[layout - 2][type_id])
|
||||
|
||||
if __name__ == "__main__":
|
||||
tables = extract_bit_tables()
|
||||
encodings = extract_packet_encodings()
|
||||
deltas = extract_delta_fields()
|
||||
|
||||
TYPE_NAMES = {1: 'VALUINST', 2: 'VMEMEXEC', 3: 'ALUEXEC', 4: 'IMMEDIATE', 5: 'IMMEDIATE_MASK', 6: 'WAVERDY',
|
||||
7: 'TS_DELTA_S8_W3', 8: 'WAVEEND', 9: 'WAVESTART', 10: 'TS_DELTA_S5_W2', 11: 'WAVEALLOC', 12: 'TS_DELTA_S5_W3',
|
||||
13: 'PERF', 14: 'UTILCTR', 15: 'TS_DELTA_SHORT', 16: 'NOP', 17: 'TS_WAVE_STATE', 18: 'EVENT', 19: 'EVENT_BIG',
|
||||
20: 'REG', 21: 'SNAPSHOT', 22: 'TS_DELTA_OR_MARK', 23: 'LAYOUT_HEADER', 24: 'INST', 25: 'UNK_25'}
|
||||
|
||||
print("L2:", tables[0], "\nL3:", tables[1], "\nL4:", tables[2])
|
||||
if encodings and tables:
|
||||
print(f"\n{'TypeID':>6} {'Name':>18} {'L2 enc':>12} {'L3 enc':>12} {'L4 enc':>12}"
|
||||
f" {'L2':>4} {'L3':>4} {'L4':>4} {'L2 delta':>12} {'L3 delta':>12} {'L4 delta':>12}")
|
||||
print("-" * 140)
|
||||
for type_id in sorted(set(encodings[0]) | set(encodings[1]) | set(encodings[2])):
|
||||
name = TYPE_NAMES.get(type_id, f'UNK_{type_id}')
|
||||
bits = [tables[i][type_id] if type_id < len(tables[i]) else 0 for i in range(3)]
|
||||
enc_strs = [f"0x{encodings[i][type_id][0]:02x}/0x{encodings[i][type_id][1]:02x}" if type_id in encodings[i] else "-" for i in range(3)]
|
||||
delta_strs = [f"[{d[1]-1}:{d[0]}]" if (d := deltas[i].get(type_id, (0, 0)))[1] > d[0] else "-" for i in range(3)]
|
||||
print(f"{type_id:6d} {name:>18} {enc_strs[0]:>12} {enc_strs[1]:>12} {enc_strs[2]:>12}"
|
||||
f" {bits[0]:4d} {bits[1]:4d} {bits[2]:4d} {delta_strs[0]:>12} {delta_strs[1]:>12} {delta_strs[2]:>12}")
|
||||
|
||||
cdna = extract_cdna_packet_sizes()
|
||||
if cdna: print(f"\nCDNA packet sizes: {cdna}")
|
||||
|
||||
unittest.main()
|
||||
@@ -2,7 +2,7 @@
|
||||
import unittest, pickle
|
||||
from typing import Iterator
|
||||
from pathlib import Path
|
||||
from tinygrad.helpers import DEBUG
|
||||
from tinygrad.helpers import DEBUG, OSX
|
||||
from tinygrad.renderer.amd.sqtt import print_packets, map_insts
|
||||
from tinygrad.runtime.autogen.amd.rdna3.ins import s_endpgm
|
||||
from test.amd.disasm import disasm
|
||||
@@ -10,7 +10,7 @@ from test.amd.disasm import disasm
|
||||
import tinygrad
|
||||
EXAMPLES_DIR = Path(tinygrad.__file__).parent.parent / "extra/sqtt/examples"
|
||||
|
||||
def rocprof_inst_traces_match(sqtt, prg, target):
|
||||
def rocprof_inst_traces_match(sqtt, prg, target, pass_rocprof_err=False):
|
||||
from tinygrad.viz.serve import amd_decode
|
||||
from extra.sqtt.roc import decode as roc_decode, InstExec
|
||||
addr_table = amd_decode(prg.lib, target)
|
||||
@@ -24,13 +24,13 @@ def rocprof_inst_traces_match(sqtt, prg, target):
|
||||
|
||||
passed_insts = 0
|
||||
for pkt, info in map_insts(sqtt.blob, prg.lib, target):
|
||||
if DEBUG >= 2: print_packets([pkt])
|
||||
if DEBUG >= 2: print_packets([(pkt, info)])
|
||||
if info is None: continue
|
||||
if DEBUG >= 2: print(f"{' '*29}{disasm(info.inst)}")
|
||||
rocprof_inst = next(rwaves_iter[info.wave][0])
|
||||
ref_pc = rocprof_inst.pc-prg.base
|
||||
# always check pc matches
|
||||
assert ref_pc == info.pc, f"pc mismatch {ref_pc}:{disasm_map[rocprof_inst.pc]} != {info.pc}:{disasm(info.inst)}"
|
||||
assert ref_pc == info.pc or pass_rocprof_err, f"pc mismatch {ref_pc}:{disasm_map[rocprof_inst.pc]} != {info.pc}:{disasm(info.inst)}"
|
||||
# special handling for s_endpgm, it marks the wave completion.
|
||||
if info.inst == s_endpgm():
|
||||
completed_wave = list(rwaves_iter[info.wave].pop(0))
|
||||
@@ -67,7 +67,9 @@ class TestSQTTMapBase(unittest.TestCase):
|
||||
if not event.itrace: continue
|
||||
if event.kern not in kern_events: continue
|
||||
with self.subTest(example=name, kern=event.kern):
|
||||
passed_insts, n_waves, n_units = rocprof_inst_traces_match(event, kern_events[event.kern], target)
|
||||
# rocprof OSX has a bug for sopk decoding, linux rocprof works
|
||||
pass_rocprof_err = OSX and target == "gfx1200" and name.startswith("profile_py")
|
||||
passed_insts, n_waves, n_units = rocprof_inst_traces_match(event, kern_events[event.kern], target, pass_rocprof_err)
|
||||
if n_waves: print(f"{name}: passed for {passed_insts} instructions across {n_waves} waves scheduled on {n_units} wave units")
|
||||
|
||||
class TestSQTTMapRDNA3(TestSQTTMapBase): target = "gfx1100"
|
||||
|
||||
@@ -47,6 +47,18 @@ def verify_asm_gemm(batch:int, M:int, N:int, K:int, dtype=dtypes.float16, gpus:i
|
||||
def verify_asm_gemm_k_sharded(M:int, N:int, K:int, dtype=dtypes.float16, gpus:int=8) -> None:
|
||||
run_asm_gemm((M, K), (K, N), dtype=dtype, a_shard=1, b_shard=0, gpus=gpus)
|
||||
|
||||
def verify_asm_gemm_n_sharded(batch:int, M:int, N:int, K:int, dtype=dtypes.float16, gpus:int=2) -> None:
|
||||
run_asm_gemm((batch, M, K), (K, N), dtype=dtype, a_shard=None, b_shard=1, gpus=gpus)
|
||||
|
||||
def verify_asm_gemm_m_sharded(M:int, N:int, K:int, dtype=dtypes.float16, gpus:int=2) -> None:
|
||||
run_asm_gemm((M, K), (K, N), dtype=dtype, a_shard=0, b_shard=None, gpus=gpus)
|
||||
|
||||
def verify_asm_gemm_n_sharded_2d(M:int, N:int, K:int, dtype=dtypes.float16, gpus:int=2) -> None:
|
||||
run_asm_gemm((M, K), (K, N), dtype=dtype, a_shard=None, b_shard=1, gpus=gpus)
|
||||
|
||||
def verify_asm_gemm_k_sharded_3d(batch:int, M:int, N:int, K:int, dtype=dtypes.float16, gpus:int=2) -> None:
|
||||
run_asm_gemm((batch, M, K), (K, N), dtype=dtype, a_shard=2, b_shard=0, gpus=gpus)
|
||||
|
||||
# 128x smaller than usual
|
||||
# uses the UOp GEMM, runs on non CDNA4 and CI
|
||||
@unittest.skipUnless(is_dtype_supported(dtypes.half), "need half")
|
||||
@@ -60,6 +72,14 @@ class TestGemm(unittest.TestCase):
|
||||
def test_gemm_multi(self): verify_asm_gemm(2, 64, 32, 32, gpus=2)
|
||||
@needs_second_gpu
|
||||
def test_gemm_k_sharded(self): verify_asm_gemm_k_sharded(64, 64, 2*64, gpus=2)
|
||||
@needs_second_gpu
|
||||
def test_gemm_m_sharded(self): verify_asm_gemm_m_sharded(2*64, 64, 32, gpus=2)
|
||||
@needs_second_gpu
|
||||
def test_gemm_n_sharded(self): verify_asm_gemm_n_sharded(1, 64, 64, 32, gpus=2)
|
||||
@needs_second_gpu
|
||||
def test_gemm_n_sharded_2d(self): verify_asm_gemm_n_sharded_2d(64, 2*64, 32, gpus=2)
|
||||
@needs_second_gpu
|
||||
def test_gemm_k_sharded_3d(self): verify_asm_gemm_k_sharded_3d(1, 64, 32, 2*64, gpus=2)
|
||||
|
||||
# uses the Asm GEMM on CDNA4 only for speed reasons
|
||||
class TestGemmLarge(unittest.TestCase):
|
||||
@@ -67,6 +87,7 @@ class TestGemmLarge(unittest.TestCase):
|
||||
if not is_cdna4():
|
||||
self.skipTest("very slow on non mi350x")
|
||||
|
||||
def test_tiny(self): verify_asm_gemm(1, 256, 256, 64)
|
||||
def test_simple(self): verify_asm_gemm(1, N:=getenv("N", 4096), N, N, dtype=dtypes.half)
|
||||
def test_gemm(self): verify_asm_gemm(1, 8192, 4096, 14336)
|
||||
def test_gemm_batched(self): verify_asm_gemm(2, 8192, 4096, 4096)
|
||||
@@ -100,6 +121,20 @@ class TestGemmLarge(unittest.TestCase):
|
||||
verify_asm_gemm(3, 256, 256, 256)
|
||||
def test_gemm_previously_unsupported(self): verify_asm_gemm(8, 1024, 1024, 4096, gpus=8)
|
||||
|
||||
# M-sharded 2D
|
||||
def test_m_sharded_1(self): verify_asm_gemm_m_sharded(8*8192, 4096, 4096, dtype=dtypes.bfloat16, gpus=8)
|
||||
def test_m_sharded_2(self): verify_asm_gemm_m_sharded(8*4096, 14336, 4096, dtype=dtypes.bfloat16, gpus=8)
|
||||
|
||||
# N-sharded 2D
|
||||
def test_n_sharded_2d_1(self): verify_asm_gemm_n_sharded_2d(8192, 8*4096, 4096, dtype=dtypes.bfloat16, gpus=8)
|
||||
def test_n_sharded_2d_2(self): verify_asm_gemm_n_sharded_2d(4096, 8*14336, 4096, dtype=dtypes.bfloat16, gpus=8)
|
||||
|
||||
# tensor parallel shapes (Llama 8B, MP=8)
|
||||
def test_tp_n_sharded_wq(self): verify_asm_gemm_n_sharded(1, 8192, 4096, 4096, dtype=dtypes.bfloat16, gpus=8)
|
||||
def test_tp_n_sharded_w1(self): verify_asm_gemm_n_sharded(1, 8192, 14336, 4096, dtype=dtypes.bfloat16, gpus=8)
|
||||
def test_tp_k_sharded_wo(self): verify_asm_gemm_k_sharded_3d(1, 8192, 4096, 4096, dtype=dtypes.bfloat16, gpus=8)
|
||||
def test_tp_k_sharded_w2(self): verify_asm_gemm_k_sharded_3d(1, 8192, 4096, 14336, dtype=dtypes.bfloat16, gpus=8)
|
||||
|
||||
# more shapes: vary M, N, K independently
|
||||
def test_shape_small_square(self): verify_asm_gemm(1, 256, 256, 256)
|
||||
def test_shape_small_rect_m(self): verify_asm_gemm(1, 512, 256, 256)
|
||||
|
||||
@@ -27,6 +27,11 @@ class TestMovedConstFolding(unittest.TestCase):
|
||||
def test_add_padded_one(self):
|
||||
_check_ast_count(1, Tensor([1.0, 2, 3, 4]) * Tensor.ones(2).pad(((1, 1),)))
|
||||
|
||||
def test_copy_padded_const(self):
|
||||
schedule = Tensor.ones(4, device="CPU:0").pad(((1, 1),)).to("CPU:1").schedule()
|
||||
assert not any(si.ast.op is Ops.COPY for si in schedule), "const copy should be folded"
|
||||
np.testing.assert_equal(Tensor.ones(4, device="CPU:0").pad(((1, 1),)).to("CPU:1").numpy(), [0, 1, 1, 1, 1, 0])
|
||||
|
||||
def test_cast_padded(self):
|
||||
# NOTE: it's always 1 kernel when calling .numpy, limitation of _check_ast_count
|
||||
if is_dtype_supported(dtypes.int16):
|
||||
|
||||
@@ -265,8 +265,6 @@ class TestCustomKernel(unittest.TestCase):
|
||||
Expected schedule order: [A2, B2, E, custom_addmul, final_sum]
|
||||
The custom_addmul kernel should be at index 3.
|
||||
"""
|
||||
from tinygrad.engine.schedule import create_schedule
|
||||
from tinygrad.schedule.rangeify import get_rangeify_map
|
||||
|
||||
A, B = Tensor.empty(4, 4), Tensor.empty(4, 4)
|
||||
A2 = (A + 1).contiguous() # kernel 0: depends on A
|
||||
@@ -275,11 +273,7 @@ class TestCustomKernel(unittest.TestCase):
|
||||
C, D, _, _ = Tensor.custom_kernel(C, D, A2, B2, fxn=custom_elementwise_addmul_kernel) # depends on A2 AND B2
|
||||
E = (A2 * 3).contiguous() # kernel 2: depends only on A2
|
||||
result = (C + D + E).sum() # kernel 3: custom_addmul, then kernel 4: sum
|
||||
|
||||
big_sink = result.uop.sink()
|
||||
tensor_map = get_rangeify_map(big_sink)
|
||||
sched_sink = big_sink.substitute(tensor_map)
|
||||
schedule, _ = create_schedule(sched_sink)
|
||||
schedule = result.schedule()
|
||||
|
||||
# Find the custom_addmul kernel position
|
||||
custom_idx = next((i for i, item in enumerate(schedule)
|
||||
|
||||
@@ -150,28 +150,16 @@ class TestFp8sConversions(unittest.TestCase):
|
||||
np.testing.assert_equal(float_to_fp8(x, dtypes.fp8e4m3), torch.tensor(x, dtype=torch.float8_e4m3fn).view(torch.uint8).item())
|
||||
|
||||
def test_float_to_fp8e4m3_extreme_values(self):
|
||||
np.testing.assert_equal(float_to_fp8(FP8E4M3_MAX, dtypes.fp8e4m3), 126)
|
||||
np.testing.assert_equal(float_to_fp8(FP8E4M3_MAX*1.01, dtypes.fp8e4m3), 126)
|
||||
np.testing.assert_equal(float_to_fp8(math.inf, dtypes.fp8e4m3), 127)
|
||||
np.testing.assert_equal(float_to_fp8(-FP8E4M3_MAX, dtypes.fp8e4m3), 254)
|
||||
np.testing.assert_equal(float_to_fp8(-FP8E4M3_MAX*1.01, dtypes.fp8e4m3), 254)
|
||||
np.testing.assert_equal(float_to_fp8(-math.inf, dtypes.fp8e4m3), 255)
|
||||
np.testing.assert_equal(float_to_fp8(math.nan, dtypes.fp8e4m3), 127)
|
||||
np.testing.assert_equal(float_to_fp8(-math.nan, dtypes.fp8e4m3), 255)
|
||||
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())
|
||||
|
||||
@given(strat.floats(width=32, allow_subnormal=True, allow_nan=False, allow_infinity=False, min_value=-FP8E5M2_MAX, max_value=FP8E5M2_MAX))
|
||||
def test_float_to_fp8e5m2(self, x):
|
||||
np.testing.assert_equal(float_to_fp8(x, dtypes.fp8e5m2), torch.tensor(x, dtype=torch.float8_e5m2).view(torch.uint8).item())
|
||||
|
||||
def test_float_to_fp8e5m2_extreme_values(self):
|
||||
np.testing.assert_equal(float_to_fp8(FP8E5M2_MAX, dtypes.fp8e5m2), 123)
|
||||
np.testing.assert_equal(float_to_fp8(FP8E5M2_MAX*1.01, dtypes.fp8e5m2), 123)
|
||||
np.testing.assert_equal(float_to_fp8(math.inf, dtypes.fp8e5m2), 124)
|
||||
np.testing.assert_equal(float_to_fp8(-FP8E5M2_MAX, dtypes.fp8e5m2), 251)
|
||||
np.testing.assert_equal(float_to_fp8(-FP8E5M2_MAX*1.01, dtypes.fp8e5m2), 251)
|
||||
np.testing.assert_equal(float_to_fp8(-math.inf, dtypes.fp8e5m2), 252)
|
||||
np.testing.assert_equal(float_to_fp8(math.nan, dtypes.fp8e5m2), 126)
|
||||
np.testing.assert_equal(float_to_fp8(-math.nan, dtypes.fp8e5m2), 254)
|
||||
for x in [FP8E5M2_MAX, FP8E5M2_MAX*1.01, -FP8E5M2_MAX, -FP8E5M2_MAX*1.01, math.inf, -math.inf, math.nan, -math.nan]:
|
||||
np.testing.assert_equal(float_to_fp8(x, dtypes.fp8e5m2), torch.tensor(x, dtype=torch.float8_e5m2).view(torch.uint8).item())
|
||||
|
||||
@given(strat.integers(min_value=0, max_value=255))
|
||||
def test_fp8e4m3_to_float(self, x):
|
||||
|
||||
@@ -115,7 +115,7 @@ class TestImageDType(unittest.TestCase):
|
||||
tst = data.numpy()
|
||||
it = data.cast(dtypes.imagef((9,27,4))).realize()
|
||||
# the underlying UOp is identical
|
||||
self.assertIs(it.uop.base.realized, data.uop.base.realized)
|
||||
#self.assertIs(it.uop.base.realized, data.uop.base.realized)
|
||||
np.testing.assert_equal(tst, it.numpy())
|
||||
|
||||
def test_image_and_back_wrong_shape(self):
|
||||
|
||||
@@ -332,7 +332,6 @@ class TestJit(unittest.TestCase):
|
||||
assert len(res3) == 10, "All values should be different, rand works in jit."
|
||||
assert res3 != res2, "Jit rand is diff with diff seeds"
|
||||
|
||||
#@unittest.expectedFailure # requires contiguous folding
|
||||
def test_jit_random_after_unrealized_random(self):
|
||||
@TinyJit
|
||||
def f(): return Tensor.rand()
|
||||
@@ -476,7 +475,7 @@ class TestJit(unittest.TestCase):
|
||||
b = f(Tensor([2.0]))
|
||||
assert abs((a - b).item()) > 0.5
|
||||
|
||||
def test_jit_init_with_empty_different_size(self):
|
||||
def test_jit_init_empty(self):
|
||||
@TinyJit
|
||||
def f(x:Tensor) -> Tensor: return (x + 1).realize()
|
||||
|
||||
@@ -485,10 +484,16 @@ class TestJit(unittest.TestCase):
|
||||
# scalar const input is not allowed
|
||||
with self.assertRaises(JitError):
|
||||
f(Tensor(2.0)).item()
|
||||
# list input has different view structure than empty(1)
|
||||
# but okay if it's realized
|
||||
#with self.assertRaises(JitError):
|
||||
# f(Tensor([2.0])).item()
|
||||
# self.assertEqual(f(Tensor([2.0])).item(), 1.0) # TODO: wrong output, should be 3.0. currently depends on empty value
|
||||
|
||||
def test_jit_init_empty_alt(self):
|
||||
@TinyJit
|
||||
def f(a:Tensor, b:Tensor) -> Tensor: return b.assign(a+1)
|
||||
for i in range(4):
|
||||
a = Tensor([i])
|
||||
b = Tensor.empty_like(a)
|
||||
c = f(a, b)
|
||||
self.assertEqual(c.item(), i+1)
|
||||
|
||||
@unittest.skip("Pending multioutput implementation #3607")
|
||||
class TestMultioutputJit(unittest.TestCase):
|
||||
|
||||
@@ -135,34 +135,6 @@ class TestMultiTensor(unittest.TestCase):
|
||||
si.run()
|
||||
self.assertEqual(len(set(names)), 1, "function was relinearized")
|
||||
|
||||
@unittest.skip("this doesn't fold because shard_ calls contiguous on all lbs")
|
||||
def test_sharded_memory(self):
|
||||
# Buffer may be stuck in track_cross_buffer
|
||||
for x in (d0, d1, d2, d3, d4): Device[x].synchronize()
|
||||
mem_base = GlobalCounters.mem_used
|
||||
|
||||
X = Tensor.ones(256).contiguous().realize()
|
||||
assert GlobalCounters.mem_used-mem_base== X.dtype.itemsize * 256, GlobalCounters.mem_used-mem_base
|
||||
X.shard_(devices_4).realize()
|
||||
for x in (d0, d1, d2, d3, d4): Device[x].synchronize()
|
||||
assert GlobalCounters.mem_used-mem_base == X.dtype.itemsize * 256 * 4, GlobalCounters.mem_used-mem_base
|
||||
|
||||
X = Tensor.ones(256).contiguous().realize()
|
||||
assert GlobalCounters.mem_used-mem_base == X.dtype.itemsize * 256, GlobalCounters.mem_used-mem_base
|
||||
X.shard_(devices_4, axis=0).realize()
|
||||
for x in (d0, d1, d2, d3, d4): Device[x].synchronize()
|
||||
assert GlobalCounters.mem_used-mem_base == X.dtype.itemsize * 256, GlobalCounters.mem_used-mem_base
|
||||
|
||||
X = Tensor.ones(256).realize()
|
||||
assert GlobalCounters.mem_used-mem_base == 0
|
||||
X.shard_(devices_4).realize()
|
||||
assert GlobalCounters.mem_used-mem_base == 0
|
||||
|
||||
X = Tensor.ones(256).realize()
|
||||
assert GlobalCounters.mem_used-mem_base == 0
|
||||
X.shard_(devices_4, axis=0).realize()
|
||||
assert GlobalCounters.mem_used-mem_base == 0
|
||||
|
||||
def test_shard_same_device(self):
|
||||
X = Tensor.ones(256).contiguous().realize()
|
||||
X.shard_((d1, X.device), 0)
|
||||
@@ -256,17 +228,17 @@ class TestMultiTensor(unittest.TestCase):
|
||||
a,b = _test_allreduce(Tensor.rand(256, 256))
|
||||
np.testing.assert_almost_equal(a.numpy(), b.numpy(), decimal=5)
|
||||
|
||||
def test_multiple_to_single_device_naive(self):
|
||||
with Context(RING=0):
|
||||
t = Tensor.arange(32).shard(devices_4, 0).to(Device.DEFAULT).realize()
|
||||
self.assertEqual(t.device, Device.DEFAULT)
|
||||
np.testing.assert_equal(t.numpy(), np.arange(32))
|
||||
|
||||
def test_multiple_to_single_device_ring(self):
|
||||
with Context(RING=2):
|
||||
t = Tensor.arange(32).shard(devices_4, 0).to(Device.DEFAULT).realize()
|
||||
self.assertEqual(t.device, Device.DEFAULT)
|
||||
np.testing.assert_equal(t.numpy(), np.arange(32))
|
||||
def test_multiple_to_single_device(self):
|
||||
kernel_counts = {}
|
||||
for ring in (0, 2):
|
||||
GlobalCounters.reset()
|
||||
with Context(RING=ring, SCACHE=0):
|
||||
t = Tensor.arange(32).contiguous().shard(devices_4, 0).to(Device.DEFAULT)
|
||||
t.realize()
|
||||
kernel_counts[ring] = GlobalCounters.kernel_count
|
||||
self.assertEqual(t.device, Device.DEFAULT)
|
||||
np.testing.assert_equal(t.numpy(), np.arange(32))
|
||||
self.assertNotEqual(kernel_counts[0], kernel_counts[2])
|
||||
|
||||
def test_allreduce_all2all(self):
|
||||
with Context(ALL2ALL=2):
|
||||
@@ -704,7 +676,7 @@ class TestMultiTensor(unittest.TestCase):
|
||||
|
||||
# test no left join
|
||||
with self.assertRaises((AssertionError, ValueError)):
|
||||
t0.reshape((26*15,7)).schedule()
|
||||
t0.reshape((26*15,7)).contiguous().schedule()
|
||||
|
||||
# it doesn't work like this anymore
|
||||
# NOTE: this never failed in assign_multi, it failed tensor spec because MULTI was never pushed in the graph
|
||||
@@ -840,13 +812,15 @@ class TestMultiTensor(unittest.TestCase):
|
||||
t.shard_(devices, axis=0).realize()
|
||||
assert all([lb is lb.base and lb.realized.base.size == 4 * 16 for lb in t.uop.src])
|
||||
|
||||
@unittest.skip("this is unreliable on OSX")
|
||||
def test_clone(self):
|
||||
t = Tensor.rand(16, 16).shard(devices_2, axis=None)
|
||||
np.testing.assert_allclose(t.numpy(), t.clone().numpy())
|
||||
|
||||
t = Tensor.rand(16, 16).shard(devices_2, axis=0)
|
||||
np.testing.assert_allclose(t.numpy(), t.clone().numpy())
|
||||
for axis in (None, 0):
|
||||
t = Tensor.arange(16).reshape(4, 4).shard(devices_2, axis=axis).contiguous().realize()
|
||||
t_clone = t.clone().realize()
|
||||
self.assertEqual(t_clone.device, t.device)
|
||||
self.assertEqual(t_clone.uop.axis, axis)
|
||||
self.assertEqual(t_clone.tolist(), t.tolist())
|
||||
t_clone += 1
|
||||
self.assertNotEqual(t_clone.tolist(), t.tolist())
|
||||
|
||||
@unittest.skip("RANGEIFY doesn't support multi const folding")
|
||||
def test_multi_const_folding(self):
|
||||
@@ -895,18 +869,18 @@ class TestShrinkMultiTensorShardedAxis(unittest.TestCase):
|
||||
|
||||
with self.assertRaises(AssertionError):
|
||||
# sharded axis shrink on non-device boundry is not allowed
|
||||
a = t.shrink(((0, 3), (0, 8)))
|
||||
a = t.shrink(((0, 3), (0, 8))).contiguous()
|
||||
a.schedule()
|
||||
a = t.shrink(((0, 2), (2, 4)))
|
||||
assert a.shape == (2, 2)
|
||||
ref = Tensor.arange(64).reshape(8, 8).shrink(((0, 2), (2, 4)))
|
||||
np.testing.assert_equal(a.numpy(), ref.numpy())
|
||||
|
||||
a = t.shrink(((0, 2), (0, 8)))
|
||||
a = t.shrink(((0, 2), (0, 8))).contiguous()
|
||||
a.schedule()
|
||||
assert a.shape == (2, 8)
|
||||
|
||||
p = a.pad(((0, 6), (0, 0)))
|
||||
p = a.pad(((0, 6), (0, 0))).contiguous()
|
||||
p.schedule()
|
||||
assert p.shape == (8, 8)
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ from tinygrad.tensor import _to_np_dtype
|
||||
from tinygrad.device import is_dtype_supported
|
||||
from tinygrad.renderer.nir import NIRRenderer
|
||||
|
||||
if getenv("TINY_BACKEND"):
|
||||
TINY_BACKEND = getenv("TINY_BACKEND")
|
||||
if TINY_BACKEND:
|
||||
import tinygrad.nn.torch # noqa: F401 # pylint: disable=unused-import
|
||||
torch.set_default_device("tiny")
|
||||
|
||||
@@ -418,7 +419,6 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op(None, lambda x: x.round(), vals=[[1.499, 1.5, 1.501, 1.0, 2.1, 0.0, -5.0, -2.499, -2.5, -2.501]], forward_only=True)
|
||||
helper_test_op(None, lambda x: x.round(), vals=[[2.5, -1.5]], forward_only=True)
|
||||
|
||||
@unittest.skipIf(Device.DEFAULT == "WEBGPU" and CI, "isinf check of 'nan' fails on CI software-based vulkan")
|
||||
def test_isinf(self):
|
||||
val = [float('-inf'), 0., float('inf'), float('nan'), 1.1]
|
||||
helper_test_op(None, torch.isinf, Tensor.isinf, vals=[val], forward_only=True)
|
||||
@@ -640,8 +640,6 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op([(45,65), (45,65)], lambda x,y: x**y)
|
||||
helper_test_op([(45,65), (45,65)], lambda x,y: x.pow(y))
|
||||
|
||||
# TODO: WEBGPU NaN handling in pow operations
|
||||
@unittest.skipIf(Device.DEFAULT == "WEBGPU", "WEBGPU NaN handling differs")
|
||||
def test_pow(self):
|
||||
helper_test_op([(45,65)], lambda x: x**0)
|
||||
helper_test_op([(45,65)], lambda x: x**1)
|
||||
@@ -760,6 +758,7 @@ class TestOps(unittest.TestCase):
|
||||
data = [[1,-8,1],[32,1,6]]
|
||||
tor = torch.tensor(data, dtype=torch.int)
|
||||
ten = Tensor(data, dtype=dtypes.int32)
|
||||
# NOTE: this breaks assigns because it's folded to 0!
|
||||
helper_test_op([], lambda: tor^tor, lambda: ten^ten, forward_only=True)
|
||||
helper_test_op([], lambda: tor^0x1337, lambda: ten^0x1337, forward_only=True)
|
||||
helper_test_op([], lambda: 0x1337^tor, lambda: 0x1337^ten, forward_only=True)
|
||||
@@ -1543,7 +1542,6 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op([(3, 4, 5, 6)], lambda x: x.isclose(x + 1e-9, rtol=0.01), forward_only=True)
|
||||
helper_test_op(None, lambda x,y: x.isclose(y), vals=[[1e-7, 1e-8, 1e-9], [0.0, 0.0, 0.0]], forward_only=True)
|
||||
|
||||
@unittest.skipIf(Device.DEFAULT == "WEBGPU" and CI, "isinf check of 'nan' fails on CI software-based vulkan")
|
||||
def test_isclose_edge_cases(self):
|
||||
for a in [math.inf, -math.inf, math.nan, 0.0]:
|
||||
for b in [math.inf, -math.inf, math.nan, 0.0]:
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import unittest
|
||||
from tinygrad import Tensor
|
||||
|
||||
class TestOuterCall(unittest.TestCase):
|
||||
def test_outer_call_assign(self):
|
||||
a = Tensor.zeros(10,10).contiguous()
|
||||
b = Tensor.ones(10,10).contiguous()
|
||||
Tensor.realize(a,b)
|
||||
|
||||
pa = a.as_param(0)
|
||||
pb = b.as_param(1)
|
||||
out = Tensor.call(a, b, fxn=pa.assign(pa+pb))
|
||||
out.realize()
|
||||
|
||||
print(a.numpy())
|
||||
assert (a == 1).all().item()
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,4 +1,4 @@
|
||||
import unittest, struct, contextlib, statistics, time, gc
|
||||
import unittest, struct, contextlib, statistics, gc
|
||||
from tinygrad import Device, Tensor, dtypes, TinyJit
|
||||
from tinygrad.helpers import CI, getenv, Context, ProfileRangeEvent, cpu_profile, cpu_events, ProfilePointEvent, dedup
|
||||
from tinygrad.device import Buffer, BufferSpec, Compiled, ProfileDeviceEvent, ProfileGraphEvent
|
||||
@@ -20,7 +20,7 @@ def helper_collect_profile(*devs):
|
||||
cpu_events.clear()
|
||||
|
||||
profile_list = []
|
||||
with Context(VIZ=1, PROFILE=1):
|
||||
with Context(PROFILE=1):
|
||||
yield profile_list
|
||||
for dev in devs: dev.synchronize()
|
||||
for dev in devs: dev._at_profile_finalize()
|
||||
@@ -170,30 +170,19 @@ class TestProfiler(unittest.TestCase):
|
||||
for (i1, d1), (i2, d2) in pairs:
|
||||
assert abs(jitter_matrix[i1][i2]) < 0.5, "jitter should be less than 0.5us"
|
||||
|
||||
@unittest.skip("this test is flaky")
|
||||
def test_cpu_profile(self):
|
||||
def test_fxn(err=False):
|
||||
time.sleep(0.1)
|
||||
if err: raise Exception()
|
||||
time.sleep(0.1)
|
||||
|
||||
with helper_collect_profile(dev:=TestProfiler.d0) as profile:
|
||||
with cpu_profile("test_1", dev.device):
|
||||
with cpu_profile("test_1", dev):
|
||||
test_fxn(err=False)
|
||||
with self.assertRaises(Exception):
|
||||
with cpu_profile("test_2", dev.device):
|
||||
with cpu_profile("test_2", dev):
|
||||
test_fxn(err=True)
|
||||
|
||||
range_events = [p for p in profile if isinstance(p, ProfileRangeEvent)]
|
||||
range_events = [p for p in profile if isinstance(p, ProfileRangeEvent) and p.device == dev]
|
||||
self.assertEqual(len(range_events), 2)
|
||||
# record start/end time up to exit (error or success)
|
||||
for e in range_events:
|
||||
self.assertGreater(e.en, e.st)
|
||||
e1, e2 = range_events
|
||||
self.assertEqual([e1.name, e2.name], ["test_1", "test_2"])
|
||||
# TODO: this is flaky
|
||||
#self.assertLess(e1.st, e2.st)
|
||||
#self.assertGreater(e1.en-e1.st, e2.en-e2.st)
|
||||
|
||||
@unittest.skip("this test is flaky")
|
||||
@unittest.skipUnless(Device[Device.DEFAULT].graph is not None, "graph support required")
|
||||
|
||||
@@ -78,7 +78,9 @@ class TestCStyleFailures(unittest.TestCase):
|
||||
def test_repeat_add(self): self._test_src_strip_paren(Ops.ADD)
|
||||
def test_repeat_mul(self): self._test_src_strip_paren(Ops.MUL)
|
||||
def test_repeat_xor(self): self._test_src_strip_paren(Ops.XOR)
|
||||
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, WGSLRenderer), "wgsl ends up with '(' * 5")
|
||||
def test_repeat_or(self): self._test_src_strip_paren(Ops.OR)
|
||||
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, WGSLRenderer), "wgsl ends up with '(' * 5")
|
||||
def test_repeat_and(self): self._test_src_strip_paren(Ops.AND)
|
||||
def test_repeat_sub(self): self._test_src_strip_paren(Ops.SUB, should_strip_paren=False)
|
||||
|
||||
|
||||
@@ -168,13 +168,13 @@ class TestSchedule(unittest.TestCase):
|
||||
a = Tensor.full((4,), 4.0).contiguous().realize()
|
||||
b = Tensor.full((4,), 2.0).contiguous().realize()
|
||||
expr = (a*b)/b
|
||||
run_schedule(check_schedule(expr, 0))
|
||||
run_schedule(check_schedule(expr, 1))
|
||||
np.testing.assert_allclose(expr.numpy(), np.full((4,), 4.0))
|
||||
|
||||
def test_div_collapse_const(self):
|
||||
a = Tensor.full((4,), 4.0).contiguous().realize()
|
||||
expr = a/a
|
||||
run_schedule(check_schedule(expr, 0))
|
||||
run_schedule(check_schedule(expr, 1))
|
||||
np.testing.assert_allclose(expr.numpy(), np.full((4,), 1.0))
|
||||
|
||||
def test_div_collapse(self):
|
||||
@@ -747,7 +747,7 @@ class TestSchedule(unittest.TestCase):
|
||||
p = P[0]
|
||||
p = p.pad(((1, 0), ))
|
||||
p = p.repeat([2])
|
||||
run_schedule(check_schedule(p, 3))
|
||||
run_schedule(check_schedule(p, 4)) # TODO: this is high
|
||||
tiny_ret = p.numpy()
|
||||
|
||||
P = np.ones((3, 3), dtype=np.float32)
|
||||
@@ -795,6 +795,38 @@ class TestSchedule(unittest.TestCase):
|
||||
self.assertIsNotNone(out.uop.base.realized)
|
||||
self.assertIsInstance(out.uop.base.realized.dtype, ImageDType)
|
||||
|
||||
@unittest.skipIf(Device.DEFAULT != "CL", "image only supported on CL")
|
||||
def test_image_dot_f16_fusion(self):
|
||||
with Context(FLOAT16=1):
|
||||
def cnt():
|
||||
x, y, z = Tensor.empty((64, 64), dtype='float'), Tensor.empty((64, 64), dtype='float'), Tensor.empty((64, 64), dtype='float')
|
||||
a = (x @ y).relu()
|
||||
sched = ((a @ z).relu() + a).schedule()
|
||||
for si in sched: si.lower()
|
||||
return len([si for si in sched if isinstance(si.prg, CompiledRunner)])
|
||||
|
||||
with Context(IMAGE=1): cnt1 = cnt()
|
||||
with Context(IMAGE=2): cnt2 = cnt()
|
||||
|
||||
self.assertEqual(cnt1, 5)
|
||||
self.assertEqual(cnt2, 5)
|
||||
|
||||
@unittest.skipIf(Device.DEFAULT != "CL", "image only supported on CL")
|
||||
@unittest.expectedFailure
|
||||
def test_image_conv_fusion(self):
|
||||
def cnt():
|
||||
x, y, z = Tensor.empty((1, 4, 3, 3)), Tensor.empty((4, 1, 3, 3)), Tensor.empty((4, 1, 7, 7))
|
||||
a = x.conv2d(y, Tensor.empty(4), groups=4, padding=1)
|
||||
b = a.conv2d(z, groups=4, padding=3)
|
||||
sched = (a + b).schedule()
|
||||
for si in sched: si.lower()
|
||||
return len([si for si in sched if isinstance(si.prg, CompiledRunner)])
|
||||
|
||||
with Context(IMAGE=1): cnt1 = cnt()
|
||||
with Context(IMAGE=2): cnt2 = cnt()
|
||||
|
||||
self.assertEqual(cnt1, cnt2)
|
||||
|
||||
def _test_fusion(self, shapes, f, cnt):
|
||||
with Context(DEBUG=0, TRACK_MATCH_STATS=0): args = [Tensor.randn(s).realize() for s in shapes]
|
||||
run_schedule(check_schedule(compare:=f(*args), cnt))
|
||||
@@ -841,10 +873,9 @@ class TestSchedule(unittest.TestCase):
|
||||
def test_cast_const_view(self):
|
||||
a = Tensor.ones((4, 4), dtype=dtypes.float32)
|
||||
casted_view = a.cast(dtypes.int32)
|
||||
run_schedule(check_schedule(casted_view, 0))
|
||||
self.assertIsNone(casted_view.uop.base.realized)
|
||||
run_schedule(check_schedule(casted_view, 1))
|
||||
realized_const_view = casted_view.contiguous()
|
||||
run_schedule(check_schedule(realized_const_view, 1))
|
||||
run_schedule(check_schedule(realized_const_view, 0))
|
||||
self.assertListEqual(realized_const_view.tolist(), [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]])
|
||||
|
||||
@given(strat.sampled_from(dtypes.all), strat.sampled_from(dtypes.all))
|
||||
@@ -1037,7 +1068,7 @@ class TestSchedule(unittest.TestCase):
|
||||
idx = Tensor([1,2,5,6], dtype=dtypes.int32)
|
||||
flat_base[idx] = Tensor([99,99,99,99])
|
||||
base.assign(flat_base.reshape(4, 4))
|
||||
sched = check_schedule(base, 2)
|
||||
sched = check_schedule(base, 6) # TODO: this is high
|
||||
run_schedule(sched)
|
||||
expected = list(range(16))
|
||||
for i, v in zip([1,2,5,6], [99,99,99,99]): expected[i] = v
|
||||
@@ -1236,8 +1267,7 @@ class TestView(unittest.TestCase):
|
||||
bv = b.pad(((0, 2),))[-2:]
|
||||
# this becomes a late a*0
|
||||
late_mul = a*bv
|
||||
run_schedule(check_schedule(late_mul, 0))
|
||||
# NOTE: no longer checked
|
||||
run_schedule(check_schedule(late_mul, 2))
|
||||
# the arange doesn't realize
|
||||
#self.assertIsNone(b.uop.base.realized)
|
||||
# mul doesn't realize
|
||||
@@ -1254,7 +1284,7 @@ class TestView(unittest.TestCase):
|
||||
bv = b.pad(((0, 2),))[-2:]
|
||||
late_mul = a*bv
|
||||
other_child = b+2
|
||||
s = check_schedule([late_mul, other_child], 2)
|
||||
s = check_schedule([late_mul, other_child], 3)
|
||||
# the arange becomes a BUFFER
|
||||
self.assertIs(b.uop.base.op, Ops.BUFFER)
|
||||
# NOTE: no longer checked
|
||||
@@ -1267,7 +1297,7 @@ class TestView(unittest.TestCase):
|
||||
class TestCopyFolding(unittest.TestCase):
|
||||
def test_const_copy_is_free(self):
|
||||
b = Tensor(1).to("CPU") * 4
|
||||
run_schedule(check_schedule(b, 0, filter_sink=False))
|
||||
run_schedule(check_schedule(b, 1, filter_sink=False))
|
||||
assert b.item() == 4
|
||||
|
||||
def test_one_hot_with_copy(self):
|
||||
@@ -1277,14 +1307,14 @@ class TestCopyFolding(unittest.TestCase):
|
||||
|
||||
def test_const_copy_multi(self):
|
||||
x = Tensor.ones(1, device="CPU").to_(["CPU", "CPU:1"]) * 2
|
||||
run_schedule(check_schedule(x, 0, filter_sink=False))
|
||||
run_schedule(check_schedule(x, 2, filter_sink=False))
|
||||
self.assertEqual(x.item(), 2.0)
|
||||
|
||||
def test_late_const_copy_folding(self):
|
||||
a = Tensor.arange(3).realize()
|
||||
zeros = Tensor.zeros(3).realize()
|
||||
b = (a*zeros).to("CPU") + 1
|
||||
run_schedule(check_schedule(b, 0, filter_sink=False))
|
||||
run_schedule(check_schedule(b, 1, filter_sink=False))
|
||||
self.assertListEqual(b.tolist(), [1, 1, 1])
|
||||
self.assertEqual(b.device, "CPU")
|
||||
|
||||
@@ -1324,7 +1354,7 @@ class TestCopyFolding(unittest.TestCase):
|
||||
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
|
||||
a.assign(Tensor(a.uop.copy_to_device(a.device), a.device))
|
||||
run_schedule(check_schedule(a, 0, filter_sink=False))
|
||||
run_schedule(check_schedule(a, 2, filter_sink=False))
|
||||
self.assertListEqual(a.tolist(), [[1.]*4]*4)
|
||||
|
||||
def test_clone(self):
|
||||
|
||||
@@ -205,6 +205,20 @@ class TestSetitem(unittest.TestCase):
|
||||
n[:, ind_1.numpy(), :, ind_2.numpy(), :] = v.numpy()
|
||||
np.testing.assert_equal(t.numpy(), n)
|
||||
|
||||
def test_setitem_tensor_int_indexing(self):
|
||||
t = Tensor.zeros(4, 3, dtype=dtypes.int).contiguous()
|
||||
t[Tensor([0, 2]), 0] = Tensor([99, 88], dtype=dtypes.int)
|
||||
n = np.zeros((4, 3), dtype=np.int32)
|
||||
n[[0, 2], 0] = [99, 88]
|
||||
np.testing.assert_equal(t.numpy(), n)
|
||||
|
||||
def test_setitem_tensor_slice_indexing(self):
|
||||
t = Tensor.zeros(4, 3, dtype=dtypes.int).contiguous()
|
||||
t[Tensor([0, 2]), :2] = Tensor([[10, 20], [30, 40]], dtype=dtypes.int)
|
||||
n = np.zeros((4, 3), dtype=np.int32)
|
||||
n[[0, 2], :2] = [[10, 20], [30, 40]]
|
||||
np.testing.assert_equal(t.numpy(), n)
|
||||
|
||||
def test_setitem_2d_tensor_indexing(self):
|
||||
t = Tensor.zeros(2, dtype=dtypes.int).contiguous()
|
||||
index = Tensor([[0, 1], [1,0]])
|
||||
@@ -279,17 +293,43 @@ class TestWithGrad(unittest.TestCase):
|
||||
x = Tensor.rand(8)
|
||||
z[:3] = x
|
||||
|
||||
def test_set_into_requires_grad(self):
|
||||
z = Tensor.rand(8, 8, requires_grad=True)
|
||||
x = Tensor.rand(8)
|
||||
with self.assertRaises(NotImplementedError):
|
||||
z[:3] = x
|
||||
|
||||
def test_set_with_requires_grad(self):
|
||||
z = Tensor.rand(8, 8)
|
||||
x = Tensor.rand(8, requires_grad=True)
|
||||
with self.assertRaises(NotImplementedError):
|
||||
z[:3] = x
|
||||
z = Tensor.ones(8, 8)
|
||||
x = Tensor.rand(8, 8, requires_grad=True)
|
||||
z[:] = x
|
||||
z.sum().backward()
|
||||
np.testing.assert_allclose(x.grad.numpy(), np.ones((8, 8)))
|
||||
|
||||
def test_set_nonleaf_requires_grad(self):
|
||||
x = Tensor([1.0, 2.0, 3.0, 4.0], requires_grad=True)
|
||||
z = x * 2
|
||||
z[:2] = Tensor([10.0, 20.0])
|
||||
z.sum().backward()
|
||||
np.testing.assert_allclose(x.grad.numpy(), [0, 0, 2, 2])
|
||||
|
||||
def test_set_overlapping_requires_grad(self):
|
||||
z = Tensor.zeros(6, requires_grad=True)
|
||||
x = Tensor.ones(4, requires_grad=True)
|
||||
y = Tensor.ones(4, requires_grad=True) * 2
|
||||
z[:4] = x
|
||||
z[2:] = y
|
||||
z.sum().backward()
|
||||
np.testing.assert_allclose(x.grad.numpy(), [1, 1, 0, 0])
|
||||
np.testing.assert_allclose(y.grad.numpy(), np.ones(4))
|
||||
|
||||
def test_set_iadd_requires_grad(self):
|
||||
z = Tensor([1.0, 2.0, 3.0, 4.0], requires_grad=True)
|
||||
x = Tensor([10.0, 20.0], requires_grad=True)
|
||||
z[:2] += x
|
||||
z.sum().backward()
|
||||
np.testing.assert_allclose(z.grad.numpy(), np.ones(4))
|
||||
np.testing.assert_allclose(x.grad.numpy(), np.ones(2))
|
||||
|
||||
def test_set_used_before_setitem(self):
|
||||
z = Tensor([1.0, 2.0, 3.0, 4.0], requires_grad=True)
|
||||
_ = z.sum()
|
||||
with self.assertRaises(RuntimeError):
|
||||
z[:2] = Tensor([0.0, 0.0])
|
||||
|
||||
class TestSetitemLoop(unittest.TestCase):
|
||||
def test_arange(self):
|
||||
|
||||
@@ -25,7 +25,7 @@ class TestStunning(unittest.TestCase):
|
||||
nv = a[12].cat(a[76]).tolist()
|
||||
|
||||
vi = Variable('i', 0, a.shape[0]-1)
|
||||
with self.assertRaisesRegex(AssertionError, "bind mismatch on"):
|
||||
with self.assertRaisesRegex(RuntimeError, "bind mismatch on"):
|
||||
wv = a[vi.bind(12)].cat(a[vi.bind(76)]).tolist()
|
||||
self.assertListEqual(nv, wv)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import unittest
|
||||
from tinygrad import Device, dtypes, Tensor
|
||||
from tinygrad.device import Buffer
|
||||
from tinygrad.helpers import Context
|
||||
from tinygrad.helpers import Context, getenv
|
||||
from test.helpers import needs_second_gpu
|
||||
|
||||
@unittest.skipUnless(hasattr(Device[Device.DEFAULT].allocator, "_offset"), "subbuffer not supported")
|
||||
@@ -42,7 +42,7 @@ class TestSubBuffer(unittest.TestCase):
|
||||
assert out == [102, 103]
|
||||
|
||||
@needs_second_gpu
|
||||
@unittest.skipIf(Device.DEFAULT not in {"CUDA", "NV", "AMD"}, "only NV, AMD, CUDA")
|
||||
@unittest.skipIf(Device.DEFAULT not in {"CUDA", "NV", "AMD"} or getenv("MOCKGPU"), "only NV, AMD, CUDA")
|
||||
def test_subbuffer_transfer(self):
|
||||
t = Tensor.arange(0, 10, dtype=dtypes.uint8).realize()
|
||||
vt = t[2:5].contiguous().realize()
|
||||
|
||||
@@ -80,7 +80,7 @@ class TestSymbolicJit(unittest.TestCase):
|
||||
symbolic = jf(q, k[:, :vi], v[:, :vi])[:2, :4, :1, :8].numpy()
|
||||
expected = f(q, k[:, :i], v[:, :i]).numpy()
|
||||
np.testing.assert_allclose(symbolic, expected, atol=1e-6, rtol=1e-6)
|
||||
assert_jit_cache_len(jf, 4)
|
||||
assert_jit_cache_len(jf, 5)
|
||||
|
||||
def test_cat_dim0(self):
|
||||
def f(a, b): return a.cat(b, dim=0).realize()
|
||||
|
||||
@@ -69,6 +69,58 @@ class TestSymbolicOps(unittest.TestCase):
|
||||
# symbolic shape dropout is not supported
|
||||
self.test_attention(dropout_p=0.5)
|
||||
|
||||
def test_sdpa_symbolic_seq_len(self):
|
||||
# symbolic seq_len on all of q/k/v (dim -2 after transpose)
|
||||
q = Tensor.rand(2, 10, 4, 8)
|
||||
k = Tensor.rand(2, 10, 4, 8)
|
||||
v = Tensor.rand(2, 10, 4, 8)
|
||||
for i in range(1, 5):
|
||||
vi = Variable("i", 1, 10).bind(i)
|
||||
Tensor.realize(q, k, v)
|
||||
symbolic = q[:, :vi].transpose(1, 2).scaled_dot_product_attention(
|
||||
k[:, :vi].transpose(1, 2), v[:, :vi].transpose(1, 2)).realize()[:2, :4, :i, :8].numpy()
|
||||
expected = q[:, :i].transpose(1, 2).scaled_dot_product_attention(
|
||||
k[:, :i].transpose(1, 2), v[:, :i].transpose(1, 2)).realize().numpy()
|
||||
np.testing.assert_allclose(symbolic, expected, atol=1e-6, rtol=1e-6)
|
||||
|
||||
def test_sdpa_symbolic_seq_len_query_only(self):
|
||||
# symbolic seq_len on query only (dim -2 after transpose)
|
||||
q = Tensor.rand(2, 10, 4, 8)
|
||||
k = Tensor.rand(2, 5, 4, 8)
|
||||
v = Tensor.rand(2, 5, 4, 8)
|
||||
for i in range(1, 5):
|
||||
vi = Variable("i", 1, 10).bind(i)
|
||||
Tensor.realize(q, k, v)
|
||||
symbolic = q[:, :vi].transpose(1, 2).scaled_dot_product_attention(
|
||||
k.transpose(1, 2), v.transpose(1, 2)).realize()[:2, :4, :i, :8].numpy()
|
||||
expected = q[:, :i].transpose(1, 2).scaled_dot_product_attention(
|
||||
k.transpose(1, 2), v.transpose(1, 2)).realize().numpy()
|
||||
np.testing.assert_allclose(symbolic, expected, atol=1e-6, rtol=1e-6)
|
||||
|
||||
def test_sdpa_symbolic_batch(self):
|
||||
# symbolic batch dim (dim 0)
|
||||
q = Tensor.rand(10, 4, 3, 8)
|
||||
k = Tensor.rand(10, 4, 3, 8)
|
||||
v = Tensor.rand(10, 4, 3, 8)
|
||||
for i in range(1, 5):
|
||||
vi = Variable("i", 1, 10).bind(i)
|
||||
Tensor.realize(q, k, v)
|
||||
symbolic = q[:vi].scaled_dot_product_attention(k[:vi], v[:vi]).realize()[:i, :4, :3, :8].numpy()
|
||||
expected = q[:i].scaled_dot_product_attention(k[:i], v[:i]).realize().numpy()
|
||||
np.testing.assert_allclose(symbolic, expected, atol=1e-6, rtol=1e-6)
|
||||
|
||||
def test_sdpa_symbolic_heads(self):
|
||||
# symbolic heads dim (dim -3)
|
||||
q = Tensor.rand(2, 10, 3, 8)
|
||||
k = Tensor.rand(2, 10, 3, 8)
|
||||
v = Tensor.rand(2, 10, 3, 8)
|
||||
for i in range(1, 5):
|
||||
vi = Variable("i", 1, 10).bind(i)
|
||||
Tensor.realize(q, k, v)
|
||||
symbolic = q[:, :vi].scaled_dot_product_attention(k[:, :vi], v[:, :vi]).realize()[:2, :i, :3, :8].numpy()
|
||||
expected = q[:, :i].scaled_dot_product_attention(k[:, :i], v[:, :i]).realize().numpy()
|
||||
np.testing.assert_allclose(symbolic, expected, atol=1e-6, rtol=1e-6)
|
||||
|
||||
def test_attention_pos_0_sz_0(self):
|
||||
Attention(128, 8)(Tensor.ones(1, 0, 128), Variable("start_pos", 0, 128).bind(0), None)
|
||||
|
||||
|
||||
@@ -136,6 +136,30 @@ class TestTensorVariable(unittest.TestCase):
|
||||
with self.assertRaises(AssertionError):
|
||||
t.chunk(2, dim=0) # can't split along symbolic dim
|
||||
|
||||
def test_symbolic_var_sum(self, var_name="u"):
|
||||
t = Variable("t", 1, 10).bind(4)
|
||||
v = Variable(var_name, 1, 5).bind(1)
|
||||
mask = (Tensor.full((1, 1, t, v+t), 1) + 1).contiguous()
|
||||
mask.shrink(((0, 1), (0, 1), (0, 4), (0, 4))).numpy()
|
||||
def test_symbolic_var_sum_alt_name(self): self.test_symbolic_var_sum("s")
|
||||
|
||||
def test_symbolic_triu(self):
|
||||
t = Variable("t", 1, 10).bind(4)
|
||||
for start_pos in (0, 1, 3):
|
||||
var_start_pos = Variable("start_pos", 0, 5).bind(start_pos)
|
||||
mask = Tensor.full((1, 1, t, var_start_pos+t), float("-inf")).triu(var_start_pos+1)
|
||||
out = mask.shrink(((0, 1), (0, 1), (0, 4), (0, start_pos+4))).numpy()
|
||||
expected = np.triu(np.full((1, 1, 4, start_pos+4), float("-inf")), k=start_pos+1)
|
||||
np.testing.assert_equal(out, expected)
|
||||
|
||||
def test_symbolic_tril(self):
|
||||
t = Variable("t", 1, 10).bind(4)
|
||||
for start_pos in (0, 1, 3):
|
||||
var_start_pos = Variable("start_pos", 0, 5).bind(start_pos)
|
||||
mask = Tensor.full((1, 1, t, var_start_pos+t), float("-inf")).tril(var_start_pos+1)
|
||||
out = mask.shrink(((0, 1), (0, 1), (0, 4), (0, start_pos+4))).numpy()
|
||||
expected = np.tril(np.full((1, 1, 4, start_pos+4), float("-inf")), k=start_pos+1)
|
||||
np.testing.assert_equal(out, expected)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -84,7 +84,6 @@ class TestFromFuzzer(unittest.TestCase):
|
||||
_test_value(np.pi * 2, unit=1.5)
|
||||
|
||||
@given(strat.sampled_from(dtypes_float))
|
||||
@unittest.skipIf(Device.DEFAULT == "WEBGPU" and CI, "Nan location mismatch on Vulkan, Metal works")
|
||||
def test_log2(self, dtype):
|
||||
if not is_dtype_supported(dtype): return
|
||||
if dtype == dtypes.float64:
|
||||
|
||||
@@ -113,6 +113,12 @@ class TestFloatUOps(TestUOps):
|
||||
def test_max(self): self._test_bop_fxn(Ops.MAX, lambda a,b: max(a,b))
|
||||
def test_cmplt(self): self._test_bop_fxn(Ops.CMPLT, lambda a,b: a<b)
|
||||
def test_cmpne(self): self._test_bop_fxn(Ops.CMPNE, lambda a,b: a!=b)
|
||||
@unittest.skipIf(Device.DEFAULT == "WEBGPU", "WEBGPU doesn't support NaN comparison correctly")
|
||||
def test_cmpne_nan(self): # NaN != x for any x (IEEE 754)
|
||||
for a, b in [(math.nan, 1.0), (1.0, math.nan), (math.nan, math.nan)]:
|
||||
self.assertTrue(_test_single_value(
|
||||
[dtypes.as_const(a, dtypes.float32), dtypes.as_const(b, dtypes.float32)],
|
||||
Ops.CMPNE, (dtypes.float32, dtypes.float32)))
|
||||
# MOD isn't tested on floats
|
||||
|
||||
def test_where(self):
|
||||
|
||||
@@ -76,7 +76,7 @@ class TestHCQ(unittest.TestCase):
|
||||
TestHCQ.d0.timeline_signal.wait(TestHCQ.d0.timeline_value)
|
||||
TestHCQ.d0.timeline_value += 1
|
||||
|
||||
@unittest.skipIf(Device.DEFAULT in {"CPU"}, "Can't handle async update on CPU device")
|
||||
@unittest.skipIf(Device.DEFAULT in {"CPU"} or getenv("AMD_IFACE", "") == "PCI", "Can't handle async update on CPU/MOCKAM device")
|
||||
def test_wait_late_set(self):
|
||||
for queue_type in [TestHCQ.d0.hw_compute_queue_t, TestHCQ.d0.hw_copy_queue_t]:
|
||||
if queue_type is None: continue
|
||||
@@ -538,7 +538,7 @@ class TestHCQ(unittest.TestCase):
|
||||
|
||||
np.testing.assert_equal(cpu_buffer.numpy(), local_buf.numpy(), "failed")
|
||||
|
||||
@unittest.skipUnless(MOCKGPU, "Emulate this on MOCKGPU to check the path in CI")
|
||||
@unittest.skipUnless(MOCKGPU and getenv("AMD_IFACE", "") != "PCI", "Emulate this on MOCKGPU to check the path in CI")
|
||||
def test_on_device_hang(self):
|
||||
if not hasattr(self.d0, 'on_device_hang'): self.skipTest("device does not have on_device_hang")
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import unittest
|
||||
from tinygrad.device import CompileError, Device
|
||||
from tinygrad.device import CompileError, Device, BufferSpec
|
||||
if Device.DEFAULT=="METAL":
|
||||
from tinygrad.runtime.ops_metal import MetalDevice, MetalCompiler, MetalProgram
|
||||
@unittest.skipIf(Device.DEFAULT!="METAL", "Metal support required")
|
||||
@@ -48,4 +48,14 @@ 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
|
||||
MetalProgram(device, "r_5", compiled)
|
||||
MetalProgram(device, "r_5", compiled)
|
||||
|
||||
def test_free(self):
|
||||
size = 2**16
|
||||
device = Device['METAL']
|
||||
before = device.sysdevice.currentAllocatedSize()
|
||||
|
||||
buf = device.allocator.alloc(size, BufferSpec(nolru=True))
|
||||
self.assertEqual(curr:=device.sysdevice.currentAllocatedSize(), before+size, msg=f"{curr=} - {before=}")
|
||||
device.allocator.free(buf, buf.size, BufferSpec(nolru=True))
|
||||
self.assertEqual(curr:=device.sysdevice.currentAllocatedSize(), before, msg=f"{curr=} - {before=}")
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Stress test for beam timeout + device recovery on AM devices.
|
||||
|
||||
Usage:
|
||||
AMD=1 python test/external/external_test_beam_timeout_recovery.py
|
||||
"""
|
||||
from tinygrad import Tensor, Device
|
||||
from tinygrad.helpers import Context
|
||||
from tinygrad.runtime.ops_amd import AMDDevice
|
||||
|
||||
if __name__ == "__main__":
|
||||
dev = Device["AMD"]
|
||||
assert isinstance(dev, AMDDevice) and dev.is_am(), "not am"
|
||||
|
||||
N = 10000
|
||||
for i in range(N):
|
||||
with Context(DEBUG=0, BEAM=0):
|
||||
a = Tensor.rand(4096, 4096, device="AMD").contiguous().realize()
|
||||
b = Tensor.rand(4096, 4096, device="AMD").contiguous().realize()
|
||||
c = a.matmul(b)
|
||||
c.realize()
|
||||
try: dev.synchronize(timeout=1)
|
||||
except RuntimeError as e: print(e)
|
||||
with Context(DEBUG=0, BEAM=0):
|
||||
a = Tensor.ones(512, 512, device="AMD").contiguous().realize()
|
||||
b = Tensor.ones(512, 512, device="AMD").contiguous().realize()
|
||||
result = a.matmul(b).realize()[0, 0].item()
|
||||
assert result == 512.0, f"iter {i}: got {result}"
|
||||
print(f" iter {i+1}/{N}: ok")
|
||||
print(f"=== All {N} iterations passed ===")
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import subprocess, sys, os, random
|
||||
|
||||
CHILD_SCRIPT = """
|
||||
import os, random
|
||||
import numpy as np
|
||||
from tinygrad import Tensor, Device
|
||||
from tinygrad.runtime.ops_amd import AMDDevice
|
||||
|
||||
dev = Device["AMD"]
|
||||
for i in range({N}):
|
||||
sz = random.randint(1, {MAX_SZ})
|
||||
data = np.random.randint(0, 256, sz, dtype=np.uint8)
|
||||
t = Tensor(data, device="AMD").contiguous().realize()
|
||||
dev.synchronize()
|
||||
result = t.numpy()
|
||||
assert (result == data).all(), f"Data mismatch at iter {{i}}"
|
||||
""".strip()
|
||||
|
||||
def run_child(n_ops, max_sz, timeout):
|
||||
env = os.environ.copy()
|
||||
env.setdefault("SDMA_RING_SIZE", "4096")
|
||||
|
||||
script = CHILD_SCRIPT.format(N=n_ops, MAX_SZ=max_sz)
|
||||
p = subprocess.Popen([sys.executable, "-c", script], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
|
||||
|
||||
try:
|
||||
_, stderr = p.communicate(timeout=timeout)
|
||||
return ("ok" if p.returncode == 0 else "fail"), stderr.decode(errors='replace')
|
||||
except subprocess.TimeoutExpired:
|
||||
p.kill()
|
||||
p.communicate()
|
||||
return "timeout", "TIMEOUT: SDMA ring likely stuck"
|
||||
|
||||
if __name__ == "__main__":
|
||||
n_iters = int(os.environ.get("FUZZ_ITERS", "10000"))
|
||||
timeout = int(os.environ.get("FUZZ_TIMEOUT", "10"))
|
||||
max_sz = int(os.environ.get("FUZZ_MAX_SZ", "65536"))
|
||||
|
||||
timeouts = 0
|
||||
failures = 0
|
||||
|
||||
for i in range(n_iters):
|
||||
# Run child with many ops to stress the small sdma ring buffer across warm starts
|
||||
n_ops = random.randint(20, 100)
|
||||
status, stderr = run_child(n_ops=n_ops, max_sz=max_sz, timeout=timeout)
|
||||
if status == "timeout":
|
||||
timeouts += 1
|
||||
print(f"\tstderr: {stderr[:500]}")
|
||||
elif status == "fail":
|
||||
failures += 1
|
||||
print(f"\tstderr: {stderr[:500]}")
|
||||
else:
|
||||
print(f"iter {i}: ok (n_ops={n_ops})")
|
||||
|
||||
print(f"\n=== Results: {n_iters} iterations, {timeouts} timeouts, {failures} failures ===")
|
||||
+35
-25
@@ -4,13 +4,19 @@
|
||||
These tests intentionally cause GPU faults to verify error handling.
|
||||
Run with: AMD=1 python -m pytest test/external/external_test_gpu_crash.py -v
|
||||
"""
|
||||
import unittest, re
|
||||
import unittest, re, importlib
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.runtime.autogen.amd.rdna3.ins import * # noqa: F403
|
||||
from tinygrad.renderer.amd.dsl import s, v, Inst, NULL
|
||||
|
||||
def assemble(code:str, name:str="test") -> str:
|
||||
kd = {"next_free_vgpr": 8, "next_free_sgpr": 8, "wavefront_size32": 1, "user_sgpr_kernarg_segment_ptr": 1, "kernarg_size": 8}
|
||||
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",
|
||||
"global_load_b32": "global_load_dword", "global_store_b32": "global_store_dword",
|
||||
"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"
|
||||
|
||||
@@ -21,6 +27,10 @@ class TestGPUCrash(unittest.TestCase):
|
||||
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():
|
||||
setattr(cls, rdna3_name, getattr(ins, cdna3_name if cls.is_cdna else rdna3_name))
|
||||
|
||||
def setUp(self):
|
||||
# Verify device works before each test
|
||||
@@ -33,7 +43,7 @@ class TestGPUCrash(unittest.TestCase):
|
||||
|
||||
def _run(self, code: str):
|
||||
from tinygrad.runtime.ops_amd import AMDProgram
|
||||
prg = AMDProgram(self.dev, "test", self.compiler.compile(assemble(code)))
|
||||
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]):
|
||||
@@ -57,32 +67,32 @@ class TestOutOfBoundsMemoryAccess(TestGPUCrash):
|
||||
|
||||
def test_global_load_null_ptr(self):
|
||||
"""Global load from NULL pointer."""
|
||||
insts = [v_mov_b32_e32(v[0], 0), v_mov_b32_e32(v[1], 0),
|
||||
global_load_b32(v[2], addr=v[0:1], saddr=NULL, offset=0), s_waitcnt(0), s_endpgm()]
|
||||
insts = [self.v_mov_b32_e32(v[0], 0), self.v_mov_b32_e32(v[1], 0),
|
||||
self.global_load_b32(v[2], addr=v[0:1], saddr=NULL, offset=0), self.s_waitcnt(0), self.s_endpgm()]
|
||||
self._assert_gpu_fault(lambda: self._run_insts(insts))
|
||||
|
||||
def test_global_store_null_ptr(self):
|
||||
"""Global store to NULL pointer."""
|
||||
insts = [v_mov_b32_e32(v[0], 0), v_mov_b32_e32(v[1], 0), v_mov_b32_e32(v[2], 0xDEADBEEF),
|
||||
global_store_b32(addr=v[0:1], data=v[2], saddr=NULL, offset=0), s_waitcnt(0), s_endpgm()]
|
||||
insts = [self.v_mov_b32_e32(v[0], 0), self.v_mov_b32_e32(v[1], 0), self.v_mov_b32_e32(v[2], 0xDEADBEEF),
|
||||
self.global_store_b32(addr=v[0:1], data=v[2], saddr=NULL, offset=0), self.s_waitcnt(0), self.s_endpgm()]
|
||||
self._assert_gpu_fault(lambda: self._run_insts(insts))
|
||||
|
||||
def test_global_load_unmapped_high_address(self):
|
||||
"""Global load from high unmapped address (0xDEAD00000000)."""
|
||||
insts = [v_mov_b32_e32(v[0], 0x00000000), v_mov_b32_e32(v[1], 0xDEAD),
|
||||
global_load_b32(v[2], addr=v[0:1], saddr=NULL, offset=0), s_waitcnt(0), s_endpgm()]
|
||||
insts = [self.v_mov_b32_e32(v[0], 0x00000000), self.v_mov_b32_e32(v[1], 0xDEAD),
|
||||
self.global_load_b32(v[2], addr=v[0:1], saddr=NULL, offset=0), self.s_waitcnt(0), self.s_endpgm()]
|
||||
self._assert_gpu_fault(lambda: self._run_insts(insts))
|
||||
|
||||
def test_global_store_unmapped_high_address(self):
|
||||
"""Global store to high unmapped address."""
|
||||
insts = [v_mov_b32_e32(v[0], 0x00000000), v_mov_b32_e32(v[1], 0xDEAD), v_mov_b32_e32(v[2], 0x12345678),
|
||||
global_store_b32(addr=v[0:1], data=v[2], saddr=NULL, offset=0), s_waitcnt(0), s_endpgm()]
|
||||
insts = [self.v_mov_b32_e32(v[0], 0x00000000), self.v_mov_b32_e32(v[1], 0xDEAD), self.v_mov_b32_e32(v[2], 0x12345678),
|
||||
self.global_store_b32(addr=v[0:1], data=v[2], saddr=NULL, offset=0), self.s_waitcnt(0), self.s_endpgm()]
|
||||
self._assert_gpu_fault(lambda: self._run_insts(insts))
|
||||
|
||||
def test_global_atomic_unmapped(self):
|
||||
"""Atomic operation on unmapped memory."""
|
||||
insts = [v_mov_b32_e32(v[0], 0xBEEF0000), v_mov_b32_e32(v[1], 0xDEAD), v_mov_b32_e32(v[2], 1),
|
||||
global_atomic_add_u32(addr=v[0:1], data=v[2], saddr=NULL, offset=0), s_waitcnt(0), s_endpgm()]
|
||||
insts = [self.v_mov_b32_e32(v[0], 0xBEEF0000), self.v_mov_b32_e32(v[1], 0xDEAD), self.v_mov_b32_e32(v[2], 1),
|
||||
self.global_atomic_add_u32(addr=v[0:1], data=v[2], saddr=NULL, offset=0), self.s_waitcnt(0), self.s_endpgm()]
|
||||
self._assert_gpu_fault(lambda: self._run_insts(insts))
|
||||
|
||||
|
||||
@@ -91,14 +101,14 @@ class TestSMEMFaults(TestGPUCrash):
|
||||
|
||||
def test_smem_load_null(self):
|
||||
"""SMEM load from NULL base."""
|
||||
insts = [s_mov_b32(s[2], 0), s_mov_b32(s[3], 0),
|
||||
s_load_b32(s[4], s[2:3], 0, soffset=NULL), s_waitcnt(0), s_endpgm()]
|
||||
insts = [self.s_mov_b32(s[2], 0), self.s_mov_b32(s[3], 0),
|
||||
self.s_load_b32(s[4], s[2:3], 0, soffset=NULL), self.s_waitcnt(0), self.s_endpgm()]
|
||||
self._assert_gpu_fault(lambda: self._run_insts(insts))
|
||||
|
||||
def test_smem_load_unmapped(self):
|
||||
"""SMEM load from unmapped address."""
|
||||
insts = [s_mov_b32(s[2], 0xBEEF0000), s_mov_b32(s[3], 0xDEAD),
|
||||
s_load_b32(s[4], s[2:3], 0, soffset=NULL), s_waitcnt(0), s_endpgm()]
|
||||
insts = [self.s_mov_b32(s[2], 0xBEEF0000), self.s_mov_b32(s[3], 0xDEAD),
|
||||
self.s_load_b32(s[4], s[2:3], 0, soffset=NULL), self.s_waitcnt(0), self.s_endpgm()]
|
||||
self._assert_gpu_fault(lambda: self._run_insts(insts))
|
||||
|
||||
|
||||
@@ -107,20 +117,20 @@ class TestFlatMemoryFaults(TestGPUCrash):
|
||||
|
||||
def test_flat_load_null(self):
|
||||
"""FLAT load from NULL address."""
|
||||
insts = [v_mov_b32_e32(v[0], 0), v_mov_b32_e32(v[1], 0),
|
||||
flat_load_b32(v[2], addr=v[0:1], saddr=NULL, offset=0), s_waitcnt(0), s_endpgm()]
|
||||
insts = [self.v_mov_b32_e32(v[0], 0), self.v_mov_b32_e32(v[1], 0),
|
||||
self.flat_load_b32(v[2], addr=v[0:1], saddr=NULL, offset=0), self.s_waitcnt(0), self.s_endpgm()]
|
||||
self._assert_gpu_fault(lambda: self._run_insts(insts))
|
||||
|
||||
def test_flat_store_null(self):
|
||||
"""FLAT store to NULL address."""
|
||||
insts = [v_mov_b32_e32(v[0], 0), v_mov_b32_e32(v[1], 0), v_mov_b32_e32(v[2], 0xDEADBEEF),
|
||||
flat_store_b32(addr=v[0:1], data=v[2], saddr=NULL, offset=0), s_waitcnt(0), s_endpgm()]
|
||||
insts = [self.v_mov_b32_e32(v[0], 0), self.v_mov_b32_e32(v[1], 0), self.v_mov_b32_e32(v[2], 0xDEADBEEF),
|
||||
self.flat_store_b32(addr=v[0:1], data=v[2], saddr=NULL, offset=0), self.s_waitcnt(0), self.s_endpgm()]
|
||||
self._assert_gpu_fault(lambda: self._run_insts(insts))
|
||||
|
||||
def test_flat_atomic_null(self):
|
||||
"""FLAT atomic on NULL address."""
|
||||
insts = [v_mov_b32_e32(v[0], 0), v_mov_b32_e32(v[1], 0), v_mov_b32_e32(v[2], 1),
|
||||
flat_atomic_add_u32(addr=v[0:1], data=v[2], saddr=NULL, offset=0), s_waitcnt(0), s_endpgm()]
|
||||
insts = [self.v_mov_b32_e32(v[0], 0), self.v_mov_b32_e32(v[1], 0), self.v_mov_b32_e32(v[2], 1),
|
||||
self.flat_atomic_add_u32(addr=v[0:1], data=v[2], saddr=NULL, offset=0), self.s_waitcnt(0), self.s_endpgm()]
|
||||
self._assert_gpu_fault(lambda: self._run_insts(insts))
|
||||
|
||||
|
||||
|
||||
+6
-5
@@ -2,8 +2,9 @@
|
||||
import subprocess, sys
|
||||
from tinygrad.helpers import getenv
|
||||
|
||||
LOOPS = getenv("LOOPS", 10)
|
||||
LOOPS = getenv("LOOPS", 50)
|
||||
BROKEN = getenv("BROKEN", 0)
|
||||
ONLY_RESET = getenv("ONLY_RESET", 0)
|
||||
|
||||
BROKEN_KERNEL_SCRIPT = """
|
||||
from tinygrad.device import Device
|
||||
@@ -36,7 +37,7 @@ for i in range(LOOPS):
|
||||
print(f"=== Running broken kernel ({i+1}/{LOOPS}) ===")
|
||||
ret = subprocess.run([sys.executable, "-c", BROKEN_KERNEL_SCRIPT])
|
||||
print(f"=== broken kernel exited with code {ret.returncode} ===")
|
||||
|
||||
print(f"=== Running test_tiny.py ({i+1}/{LOOPS}) ===")
|
||||
ret = subprocess.run([sys.executable, "test/test_tiny.py", "TestTiny.test_plus"])
|
||||
print(f"=== test_tiny.py exited with code {ret.returncode} ===")
|
||||
elif not ONLY_RESET:
|
||||
print(f"=== Running test_tiny.py ({i+1}/{LOOPS}) ===")
|
||||
ret = subprocess.run([sys.executable, "test/test_tiny.py", "TestTiny.test_plus"])
|
||||
print(f"=== test_tiny.py exited with code {ret.returncode} ===")
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ from tinygrad.helpers import Profiling
|
||||
|
||||
class FakeProgram:
|
||||
def __init__(self, name:str, prg:bytes, **kwargs): pass
|
||||
def __call__(self, *bufs, global_size, local_size, vals=(), wait=False): pass
|
||||
def __call__(self, *bufs, global_size, local_size, vals=(), wait=False, **kw): pass
|
||||
|
||||
class FakeAllocator(Allocator[Compiled]):
|
||||
def _alloc(self, sz, options): return None
|
||||
|
||||
+1
-12
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# compare kernels created by HEAD against master
|
||||
import os, multiprocessing, logging, pickle, sqlite3, difflib, warnings, itertools, functools, base64, codecs
|
||||
import os, multiprocessing, logging, pickle, sqlite3, difflib, warnings, functools, base64, codecs
|
||||
from dataclasses import replace
|
||||
from typing import Callable, Any
|
||||
|
||||
@@ -8,7 +8,6 @@ ASSERT_DIFF = int((flag:="[pr]") in os.getenv("COMMIT_MESSAGE", flag) or flag in
|
||||
if not int(os.getenv("ASSERT_PROCESS_REPLAY", "1")): ASSERT_DIFF = 0
|
||||
|
||||
try:
|
||||
from tinygrad.schedule.rangeify import get_rangeify_map
|
||||
from tinygrad.renderer import Renderer, ProgramSpec
|
||||
from tinygrad.engine.realize import get_program
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo
|
||||
@@ -43,14 +42,6 @@ class ProcessReplayWarning(Warning): pass
|
||||
|
||||
# *** replay the function and convert return values to string
|
||||
|
||||
def replay_get_rangeify_map(ret:dict[UOp, UOp], big_sink:UOp) -> tuple[str, str, tuple[Any, ...]]:
|
||||
UOp.unique_num = itertools.count(max([u.arg for u in big_sink.toposort() if u.op is Ops.UNIQUE], default=0)+1)
|
||||
new_sink = big_sink.substitute(get_rangeify_map(big_sink))
|
||||
def to_str(ret:UOp) -> str:
|
||||
asts = [repr(u.arg.ast) for u in ret.toposort() if u.op is Ops.CALL]
|
||||
return "\n".join([f"{len(asts)} kernels", *asts])
|
||||
return to_str(new_sink), to_str(big_sink.substitute(ret)), (big_sink,)
|
||||
|
||||
def replay_get_program(p:ProgramSpec, ast:UOp, renderer:Renderer, opts:list[Opt]|None=None) -> tuple[str, str, tuple[Any, ...]]:
|
||||
# the ast.arg is non None if we are inside of search.py
|
||||
sink_arg = ast.arg or KernelInfo()
|
||||
@@ -68,8 +59,6 @@ def replay_get_program(p:ProgramSpec, ast:UOp, renderer:Renderer, opts:list[Opt]
|
||||
|
||||
replayers: dict[str, Callable[..., tuple[str, str, tuple[Any, ...]]]] = {}
|
||||
replayers["get_program"] = replay_get_program
|
||||
# disable this for speed, does it ever find things?
|
||||
#replayers["get_rangeify_map"] = replay_get_rangeify_map
|
||||
|
||||
# *** run replayers on captured rows and print diffs
|
||||
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ def assert_jit_cache_len(fxn, expected_len):
|
||||
assert len(fxn.jit_cache) == 1, len(fxn.jit_cache)
|
||||
# until we have a better way of typing the prg in ExecItem
|
||||
assert type(fxn.jit_cache[0].prg).__name__.endswith('Graph')
|
||||
assert len(fxn.jit_cache[0].prg.jit_cache) == expected_len
|
||||
assert len(fxn.jit_cache[0].prg.jit_cache) == expected_len, f"expected {expected_len}, got {len(fxn.jit_cache[0].prg.jit_cache)}"
|
||||
|
||||
def rand_for_dtype(dt:DType, size:int, allow_subnormal=True):
|
||||
if dtypes.is_unsigned(dt):
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
from __future__ import annotations
|
||||
import mmap, functools
|
||||
from tinygrad.runtime.autogen import libc
|
||||
from test.mockgpu.driver import VirtDriver, VirtFileDesc, TextFileDesc, DirFileDesc, VirtFile
|
||||
from test.mockgpu.am.amgpu import MockAMGPU, VRAM_SIZE
|
||||
|
||||
DOORBELL_SIZE = 0x2000
|
||||
MMIO_SIZE = 2 << 20
|
||||
PCIBUS = "mock:am:0"
|
||||
|
||||
_empty_bar = "0x0000000000000000 0x0000000000000000 0x0000000000000000"
|
||||
_resource_lines = [
|
||||
f"0x0000000000000000 0x{VRAM_SIZE-1:016x} 0x0000000000000000", _empty_bar,
|
||||
f"0x0000000000000000 0x{DOORBELL_SIZE-1:016x} 0x0000000000000000", _empty_bar, _empty_bar,
|
||||
f"0x0000000000000000 0x{MMIO_SIZE-1:016x} 0x0000000000000000", _empty_bar,
|
||||
]
|
||||
|
||||
class PagemapFileDesc(VirtFileDesc):
|
||||
def __init__(self, fd, gpu):
|
||||
super().__init__(fd)
|
||||
self.gpu = gpu
|
||||
def seek(self, offset): self.off = offset
|
||||
def read_contents(self, size=None):
|
||||
entries = bytearray()
|
||||
for i in range((size or 8) // 8):
|
||||
vaddr = ((self.off // 8) + i) * 0x1000
|
||||
paddr = self.gpu._next_sysmem_paddr
|
||||
self.gpu._next_sysmem_paddr += 0x1000
|
||||
self.gpu._sysmem_map[paddr] = vaddr
|
||||
entries += ((1 << 63) | (paddr // 0x1000)).to_bytes(8, 'little')
|
||||
self.off += len(entries)
|
||||
return bytes(entries)
|
||||
|
||||
class PCIBarFileDesc(VirtFileDesc):
|
||||
def __init__(self, fd, memfd, driver=None):
|
||||
super().__init__(fd)
|
||||
self.memfd, self.driver = memfd, driver
|
||||
def mmap(self, start, sz, prot, flags, fd, off):
|
||||
addr = libc.mmap(start, sz, prot, flags, self.memfd, off)
|
||||
if self.driver is not None:
|
||||
self.driver.track_address(addr, addr + sz, lambda mv, idx: None, lambda mv, idx: self.driver._emulate_execute())
|
||||
return addr
|
||||
|
||||
class PCIMMIOBarFileDesc(VirtFileDesc):
|
||||
def __init__(self, fd, bar5_addr):
|
||||
super().__init__(fd)
|
||||
self.bar5_addr = bar5_addr
|
||||
def mmap(self, start, sz, prot, flags, fd, off): return self.bar5_addr + off
|
||||
|
||||
class PCIConfigFileDesc(VirtFileDesc):
|
||||
def __init__(self, fd):
|
||||
super().__init__(fd)
|
||||
self.data = bytearray(256)
|
||||
def read_contents(self, size=None): return bytes(self.data[self.off:self.off + (size or len(self.data) - self.off)])
|
||||
def write_contents(self, content): self.data[self.off:self.off + len(content)] = content
|
||||
def seek(self, offset): self.off = offset
|
||||
|
||||
class PCIEnableFileDesc(VirtFileDesc):
|
||||
def __init__(self, fd): super().__init__(fd)
|
||||
def read_contents(self, size=None): return "1\n"
|
||||
def write_contents(self, content): pass
|
||||
|
||||
class AMDriver(VirtDriver):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.gpus:dict[int, MockAMGPU] = {}
|
||||
self._executing = False
|
||||
self.gpu = MockAMGPU(0)
|
||||
self.gpus[0] = self.gpu
|
||||
self.next_fd = 1 << 30
|
||||
|
||||
self._bar5_addr = libc.mmap(0, MMIO_SIZE, mmap.PROT_READ | mmap.PROT_WRITE, mmap.MAP_SHARED | mmap.MAP_ANONYMOUS, -1, 0)
|
||||
mmio = self.gpu.mmio
|
||||
self.track_address(self._bar5_addr, self._bar5_addr + MMIO_SIZE,
|
||||
lambda mv, idx: _bar5_sync_read(mv, idx, mmio), lambda mv, idx: _bar5_sync_write(mv, idx, mmio))
|
||||
|
||||
p = f"/sys/bus/pci/devices/{PCIBUS}"
|
||||
self.tracked_files += [
|
||||
VirtFile("/proc/sys/vm/compact_unevictable_allowed", functools.partial(TextFileDesc, text="0\n")),
|
||||
VirtFile("/proc/self/pagemap", functools.partial(PagemapFileDesc, gpu=self.gpu)),
|
||||
VirtFile("/sys/bus/pci/devices", functools.partial(DirFileDesc, child_names=[PCIBUS])),
|
||||
VirtFile(f"{p}/vendor", functools.partial(TextFileDesc, text="0x1002\n")),
|
||||
VirtFile(f"{p}/device", functools.partial(TextFileDesc, text="0x74a1\n")),
|
||||
VirtFile(f"{p}/enable", PCIEnableFileDesc),
|
||||
VirtFile(f"{p}/config", PCIConfigFileDesc),
|
||||
VirtFile(f"{p}/resource", functools.partial(TextFileDesc, text="\n".join(_resource_lines) + "\n")),
|
||||
VirtFile(f"{p}/resource0", functools.partial(PCIBarFileDesc, memfd=self.gpu.vram_fd)),
|
||||
VirtFile(f"{p}/resource2", functools.partial(PCIBarFileDesc, memfd=self.gpu.doorbell_fd, driver=self)),
|
||||
VirtFile(f"{p}/resource5", functools.partial(PCIMMIOBarFileDesc, bar5_addr=self._bar5_addr)),
|
||||
]
|
||||
|
||||
def _alloc_fd(self):
|
||||
fd = self.next_fd
|
||||
self.next_fd += 1
|
||||
return fd
|
||||
|
||||
def open(self, name, flags, mode, virtfile): return virtfile.fdcls(self._alloc_fd())
|
||||
|
||||
def _emulate_execute(self):
|
||||
if self._executing: return
|
||||
self._executing = True
|
||||
try:
|
||||
any_progress = True
|
||||
while any_progress:
|
||||
any_progress = False
|
||||
for gpu in self.gpus.values():
|
||||
for q in gpu.queues:
|
||||
if q.executing: any_progress |= q.execute() > 0
|
||||
finally:
|
||||
self._executing = False
|
||||
|
||||
def _bar5_sync_read(mv, idx, mmio):
|
||||
if isinstance(idx, slice):
|
||||
for i in range(idx.start or 0, idx.stop or len(mv), idx.step or 1): mv[i] = mmio[i]
|
||||
else: mv[idx] = mmio[idx]
|
||||
|
||||
def _bar5_sync_write(mv, idx, mmio):
|
||||
if isinstance(idx, slice):
|
||||
for i in range(idx.start or 0, idx.stop or len(mv), idx.step or 1): mmio[i] = mv[i]
|
||||
else: mmio[idx] = mv[idx]
|
||||
|
||||
class AMUSBDriver(AMDriver):
|
||||
def __init__(self):
|
||||
import test.mockgpu.usb as _musb
|
||||
super().__init__()
|
||||
self.state = _musb.MockASM24State(self.gpu, self, VRAM_SIZE, DOORBELL_SIZE, MMIO_SIZE)
|
||||
_musb._mock_usb_state = self.state
|
||||
@@ -0,0 +1,314 @@
|
||||
# mypy: ignore-errors
|
||||
from __future__ import annotations
|
||||
import ctypes, ctypes.util, struct, functools, os, mmap
|
||||
from tinygrad.runtime.autogen.am import am
|
||||
from tinygrad.runtime.support.amd import AMDReg, import_asic_regs
|
||||
from test.mockgpu.amd.amdgpu import AMDGPU
|
||||
|
||||
libc = ctypes.CDLL(ctypes.util.find_library("c"))
|
||||
libc.mmap.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_long]
|
||||
libc.mmap.restype = ctypes.c_void_p
|
||||
|
||||
VRAM_SIZE = 512 << 20
|
||||
|
||||
IP_VERSIONS = {
|
||||
am.GC_HWIP: (12, 0, 0), am.SDMA0_HWIP: (7, 0, 0), am.MMHUB_HWIP: (4, 1, 0), am.NBIO_HWIP: (6, 3, 1),
|
||||
am.MP0_HWIP: (14, 0, 2), am.MP1_HWIP: (14, 0, 2), am.HDP_HWIP: (7, 0, 0), am.OSSSYS_HWIP: (7, 0, 0),
|
||||
}
|
||||
|
||||
def _pad(t, n=10): return t + (0,) * (n - len(t))
|
||||
IP_BASES = {
|
||||
am.GC_HWIP: _pad((0x00001260, 0x0000A000, 0x0001C000, 0x02402C00)),
|
||||
am.SDMA0_HWIP: _pad((0x00001260, 0x0000A000, 0x0001C000, 0x02402C00)),
|
||||
am.MMHUB_HWIP: _pad((0x0001A000, 0x02408800)),
|
||||
am.NBIO_HWIP: _pad((0x00000000, 0x00000014, 0x00000D20, 0x00010400, 0x0241B000, 0x04040000)),
|
||||
am.MP0_HWIP: _pad((0x00016000, 0x00DC0000, 0x00E00000, 0x00E40000, 0x0243FC00)),
|
||||
am.MP1_HWIP: _pad((0x00016000, 0x00DC0000, 0x00E00000, 0x00E40000, 0x0243FC00)),
|
||||
am.HDP_HWIP: _pad((0x00000F20, 0x0240A400)),
|
||||
am.OSSSYS_HWIP: _pad((0x000010A0, 0x0240A000)),
|
||||
}
|
||||
|
||||
IP_HWIDS = {hwip: am.hw_id_map[hwip] for hwip in IP_VERSIONS}
|
||||
|
||||
GC_INFO = dict(gc_num_se=2, gc_num_cu_per_sh=8, gc_num_sh_per_se=2, gc_num_rb_per_se=4,
|
||||
gc_num_tccs=8, gc_wave_size=32, gc_max_waves_per_simd=16, gc_max_scratch_slots_per_cu=32, gc_lds_size=64)
|
||||
|
||||
def _build_ip_regs(prefix, hwip) -> dict[str, AMDReg]:
|
||||
try: return import_asic_regs(prefix, IP_VERSIONS[hwip], cls=functools.partial(AMDReg, bases={0: IP_BASES[hwip]}))
|
||||
except Exception: return {}
|
||||
|
||||
class MockMMU:
|
||||
def __init__(self, gpu:MockAMGPU):
|
||||
self.gpu = gpu
|
||||
self.tlb: dict[int, tuple[int, int, bool]] = {}
|
||||
|
||||
def invalidate(self, pt_base:int, va_base:int):
|
||||
new_tlb: dict[int, tuple[int, int, bool]] = {}
|
||||
self._walk(pt_base, 0, 0, new_tlb, va_base)
|
||||
for va, (pa, sz, is_sys) in new_tlb.items():
|
||||
old = self.tlb.get(va)
|
||||
if not is_sys and (old is None or old[0] != pa): self.gpu.map_vram_at(va, pa, sz)
|
||||
if old is None: self.gpu.map_range(va, sz)
|
||||
self.tlb = new_tlb
|
||||
|
||||
def _walk(self, pt_paddr:int, level:int, va_acc:int, out:dict, va_base:int):
|
||||
shift = [39, 30, 21, 12][level]
|
||||
for i in range(512):
|
||||
pte = struct.unpack_from('<Q', self.gpu.vram, pt_paddr + i * 8)[0]
|
||||
if not (pte & am.AMDGPU_PTE_VALID): continue
|
||||
va, pa = va_acc | (i << shift), pte & 0x0000FFFFFFFFF000
|
||||
if level == 3 or (pte & am.AMDGPU_PDE_PTE_GFX12):
|
||||
out[va_base + va] = (pa, 1 << shift, bool(pte & am.AMDGPU_PTE_SYSTEM))
|
||||
else:
|
||||
self._walk(pa, level + 1, va, out, va_base)
|
||||
|
||||
def paddr_to_host(self, paddr:int) -> int:
|
||||
page, off = paddr & ~0xFFF, paddr & 0xFFF
|
||||
if page in self.gpu._sysmem_map: return self.gpu._sysmem_map[page] + off
|
||||
if paddr < VRAM_SIZE: return self.gpu.vram_addr + paddr
|
||||
raise ValueError(f"paddr {paddr:#x} not found in sysmem_map or VRAM")
|
||||
|
||||
def addr_to_host(self, addr:int) -> int:
|
||||
gmc = self.gpu.mmio.gmc
|
||||
sys_lo = self.gpu.mmio.regs.get(gmc.reg('regMMMC_VM_SYSTEM_APERTURE_LOW_ADDR') or 0, 0) << 18
|
||||
sys_hi = self.gpu.mmio.regs.get(gmc.reg('regMMMC_VM_SYSTEM_APERTURE_HIGH_ADDR') or 0, 0) << 18
|
||||
if sys_lo <= addr < sys_hi: return self.paddr_to_host(addr - self.gpu.mc_base)
|
||||
for tva, (pa, sz, is_sys) in self.tlb.items():
|
||||
if tva <= addr < tva + sz:
|
||||
paddr = pa + (addr - tva)
|
||||
if not is_sys: return self.gpu.vram_addr + paddr
|
||||
return self.paddr_to_host(paddr)
|
||||
raise ValueError(f"addr {addr:#x} not mapped (sys_aperture=[{sys_lo:#x}, {sys_hi:#x}])")
|
||||
|
||||
class MockIPBlock:
|
||||
def __init__(self, gpu:MockAMGPU, mmio:MockMMIOInterface, regs:dict[str, AMDReg]):
|
||||
self.gpu, self.mmio, self._regs = gpu, mmio, regs
|
||||
self._n2a = {n: r.addr[0] for n, r in regs.items()}
|
||||
self._a2n = {a: n for n, a in self._n2a.items()}
|
||||
self.addrs = set(self._n2a.values())
|
||||
def reg(self, name) -> int|None: return self._n2a.get(name)
|
||||
def decode(self, name) -> dict: return self._regs[name].decode(self.mmio.regs.get(self._n2a[name], 0))
|
||||
def read(self, reg:int) -> int: return self.mmio.regs.get(reg, 0)
|
||||
def write(self, reg:int, val:int): self.mmio.regs[reg] = val
|
||||
def _read_pair(self, pair) -> int:
|
||||
if pair[0] is None: return 0
|
||||
return self.mmio.regs.get(pair[0], 0) | (self.mmio.regs.get(pair[1], 0) << 32)
|
||||
|
||||
class MockPSP(MockIPBlock):
|
||||
def __init__(self, gpu, mmio):
|
||||
super().__init__(gpu, mmio, _build_ip_regs('mp', am.MP0_HWIP))
|
||||
self._sos_alive, self._ring_wptr = False, 0
|
||||
pref = "regMPASP_SMN_C2PMSG" if IP_VERSIONS[am.MP0_HWIP] >= (14,0,0) else "regMP0_SMN_C2PMSG"
|
||||
def r(n): return self.reg(f"{pref}_{n}")
|
||||
self._c2pmsg_35, self._c2pmsg_64, self._c2pmsg_67 = r(35), r(64), r(67)
|
||||
self._c2pmsg_69, self._c2pmsg_70, self._c2pmsg_81 = r(69), r(70), r(81)
|
||||
|
||||
def read(self, reg:int) -> int:
|
||||
if reg == self._c2pmsg_35: return 0x80000000
|
||||
if reg == self._c2pmsg_81: return 0x1 if self._sos_alive else 0x0
|
||||
if reg == self._c2pmsg_64: return 0x80000000 if self._sos_alive else 0x0
|
||||
if reg == self._c2pmsg_67: return self._ring_wptr
|
||||
return super().read(reg)
|
||||
|
||||
def write(self, reg:int, val:int):
|
||||
super().write(reg, val)
|
||||
if reg == self._c2pmsg_35 and val == am.PSP_BL__LOAD_SOSDRV: self._sos_alive = True
|
||||
if reg == self._c2pmsg_67: self._ring_submit(val)
|
||||
|
||||
def _ring_submit(self, new_wptr:int):
|
||||
old_wptr = self._ring_wptr
|
||||
self._ring_wptr = new_wptr
|
||||
lo, hi = self._c2pmsg_69, self._c2pmsg_70
|
||||
if lo is None or hi is None: return
|
||||
ring_mc = self.mmio.regs.get(lo, 0) | (self.mmio.regs.get(hi, 0) << 32)
|
||||
ring_paddr = ring_mc - self.gpu.mc_base
|
||||
frame_off = ring_paddr + old_wptr * 4
|
||||
frame = am.struct_psp_gfx_rb_frame.from_buffer_copy(bytes(self.gpu.vram[frame_off:frame_off + ctypes.sizeof(am.struct_psp_gfx_rb_frame)]))
|
||||
fence_paddr = ((frame.fence_addr_hi << 32) | frame.fence_addr_lo) - self.gpu.mc_base
|
||||
if 0 <= fence_paddr < len(self.gpu.vram):
|
||||
struct.pack_into('<I', self.gpu.vram, fence_paddr, frame.fence_value)
|
||||
cmd_paddr = ((frame.cmd_buf_addr_hi << 32) | frame.cmd_buf_addr_lo) - self.gpu.mc_base
|
||||
if 0 <= cmd_paddr < len(self.gpu.vram):
|
||||
struct.pack_into('<I', self.gpu.vram, cmd_paddr + 864, 0)
|
||||
|
||||
class MockSMU(MockIPBlock):
|
||||
def __init__(self, gpu, mmio):
|
||||
try: regs = import_asic_regs('mp', (11, 0), cls=functools.partial(AMDReg, bases={0: IP_BASES[am.MP1_HWIP]}))
|
||||
except Exception: regs = {}
|
||||
super().__init__(gpu, mmio, regs)
|
||||
self._msg_pending = False
|
||||
def r(n): return self.reg(f"mmMP1_SMN_C2PMSG_{n}")
|
||||
self._c2pmsg_53, self._c2pmsg_54, self._c2pmsg_66 = r(53), r(54), r(66)
|
||||
self._c2pmsg_75, self._c2pmsg_82, self._c2pmsg_90 = r(75), r(82), r(90)
|
||||
|
||||
def read(self, reg:int) -> int:
|
||||
if reg == self._c2pmsg_90 or reg == self._c2pmsg_54: return 0x1 if self._msg_pending else super().read(reg)
|
||||
if reg == self._c2pmsg_82: return self.mmio.regs.get(reg, 3)
|
||||
return super().read(reg)
|
||||
|
||||
def write(self, reg:int, val:int):
|
||||
super().write(reg, val)
|
||||
if reg == self._c2pmsg_66 or reg == self._c2pmsg_75: self._msg_pending = True
|
||||
if (reg == self._c2pmsg_90 or reg == self._c2pmsg_54) and val == 0: self._msg_pending = False
|
||||
|
||||
class MockSDMA(MockIPBlock):
|
||||
def __init__(self, gpu, mmio):
|
||||
all_gc = _build_ip_regs('gc', am.GC_HWIP)
|
||||
super().__init__(gpu, mmio, {n: r for n, r in all_gc.items() if 'SDMA' in n})
|
||||
|
||||
def write(self, reg:int, val:int):
|
||||
super().write(reg, val)
|
||||
name = self._a2n.get(reg, '')
|
||||
if name.endswith('_RB_CNTL') and self._regs[name].decode(val).get('rb_enable', 0):
|
||||
self._activate_queue(name.rsplit('_RB_CNTL', 1)[0])
|
||||
|
||||
def _activate_queue(self, prefix:str):
|
||||
ring_addr = self._read_pair((self.reg(f'{prefix}_RB_BASE'), self.reg(f'{prefix}_RB_BASE_HI'))) << 8
|
||||
rptr_addr = self._read_pair((self.reg(f'{prefix}_RB_RPTR_ADDR_LO'), self.reg(f'{prefix}_RB_RPTR_ADDR_HI')))
|
||||
wptr_addr = self._read_pair((self.reg(f'{prefix}_RB_WPTR_POLL_ADDR_LO'), self.reg(f'{prefix}_RB_WPTR_POLL_ADDR_HI')))
|
||||
rb_size = self.decode(f'{prefix}_RB_CNTL')['rb_size']
|
||||
self.gpu.add_sdma_queue(self.gpu.mmu.addr_to_host(ring_addr), 4 << rb_size,
|
||||
self.gpu.mmu.addr_to_host(rptr_addr), self.gpu.mmu.addr_to_host(wptr_addr))
|
||||
|
||||
class MockGFX(MockIPBlock):
|
||||
def __init__(self, gpu, mmio):
|
||||
super().__init__(gpu, mmio, _build_ip_regs('gc', am.GC_HWIP))
|
||||
self._pt_base = (self.reg('regGCVM_CONTEXT0_PAGE_TABLE_BASE_ADDR_LO32'), self.reg('regGCVM_CONTEXT0_PAGE_TABLE_BASE_ADDR_HI32'))
|
||||
self._pt_start = (self.reg('regGCVM_CONTEXT0_PAGE_TABLE_START_ADDR_LO32'), self.reg('regGCVM_CONTEXT0_PAGE_TABLE_START_ADDR_HI32'))
|
||||
self._gc_inv_ack = self.reg('regGCVM_INVALIDATE_ENG17_ACK')
|
||||
self._gc_inv_req = self.reg('regGCVM_INVALIDATE_ENG17_REQ')
|
||||
self._hqd_active = self.reg('regCP_HQD_ACTIVE')
|
||||
|
||||
def read(self, reg:int) -> int:
|
||||
if reg == self.reg('regCP_STAT') or reg == self.reg('regRLC_SAFE_MODE'): return 0
|
||||
if reg == self.reg('regRLC_RLCS_BOOTLOAD_STATUS'): return 0x2
|
||||
if reg == self._gc_inv_ack: return 0x1
|
||||
return super().read(reg)
|
||||
|
||||
def write(self, reg:int, val:int):
|
||||
super().write(reg, val)
|
||||
if reg == self.reg('regCP_HQD_DEQUEUE_REQUEST'):
|
||||
if self._hqd_active is not None: self.mmio.regs[self._hqd_active] = 0
|
||||
if reg == self._hqd_active and val == 1: self._activate_pm4_queue()
|
||||
if reg == self._gc_inv_req: self.gpu.mmu.invalidate(self.get_pt_base(), self.get_va_base())
|
||||
|
||||
def _activate_pm4_queue(self):
|
||||
ring_addr = self._read_pair((self.reg('regCP_HQD_PQ_BASE'), self.reg('regCP_HQD_PQ_BASE_HI'))) << 8
|
||||
rptr_addr = self._read_pair((self.reg('regCP_HQD_PQ_RPTR_REPORT_ADDR'), self.reg('regCP_HQD_PQ_RPTR_REPORT_ADDR_HI')))
|
||||
wptr_addr = self._read_pair((self.reg('regCP_HQD_PQ_WPTR_POLL_ADDR'), self.reg('regCP_HQD_PQ_WPTR_POLL_ADDR_HI')))
|
||||
queue_size = self.decode('regCP_HQD_PQ_CONTROL')['queue_size']
|
||||
self.gpu.add_pm4_queue(self.gpu.mmu.addr_to_host(ring_addr), 4 << (queue_size + 1),
|
||||
self.gpu.mmu.addr_to_host(rptr_addr), self.gpu.mmu.addr_to_host(wptr_addr))
|
||||
|
||||
def get_pt_base(self) -> int: return self._read_pair(self._pt_base) & 0x0000FFFFFFFFF000
|
||||
def get_va_base(self) -> int: return self._read_pair(self._pt_start) << 12
|
||||
|
||||
class MockGMC(MockIPBlock):
|
||||
def __init__(self, gpu, mmio, gfx:MockGFX):
|
||||
super().__init__(gpu, mmio, _build_ip_regs('mmhub', am.MMHUB_HWIP))
|
||||
self._gfx = gfx
|
||||
self._inv_ack = self.reg('regMMVM_INVALIDATE_ENG17_ACK')
|
||||
self._inv_sem = self.reg('regMMVM_INVALIDATE_ENG17_SEM')
|
||||
self._inv_req = self.reg('regMMVM_INVALIDATE_ENG17_REQ')
|
||||
self._fb_loc_top = self.reg('regMMMC_VM_FB_LOCATION_TOP')
|
||||
|
||||
def read(self, reg:int) -> int:
|
||||
if reg == self._inv_ack or reg == self._inv_sem: return 0x1
|
||||
if reg == self._fb_loc_top: return VRAM_SIZE >> 24
|
||||
return super().read(reg)
|
||||
|
||||
def write(self, reg:int, val:int):
|
||||
super().write(reg, val)
|
||||
if reg == self._inv_req: self.gpu.mmu.invalidate(self._gfx.get_pt_base(), self._gfx.get_va_base())
|
||||
|
||||
class MockNBIO(MockIPBlock):
|
||||
def __init__(self, gpu, mmio):
|
||||
regs = _build_ip_regs('nbif', am.NBIO_HWIP)
|
||||
regs.update(_build_ip_regs('hdp', am.HDP_HWIP))
|
||||
super().__init__(gpu, mmio, regs)
|
||||
self._remap_hdp = self.reg('regBIF_BX0_REMAP_HDP_MEM_FLUSH_CNTL')
|
||||
self._hdp_flush = self.reg('regHDP_MEM_FLUSH_CNTL')
|
||||
|
||||
def read(self, reg:int) -> int:
|
||||
if reg == self._remap_hdp and self._hdp_flush is not None: return self._hdp_flush * 4
|
||||
return super().read(reg)
|
||||
|
||||
class MockMMIOInterface:
|
||||
def __init__(self, gpu:MockAMGPU):
|
||||
self.gpu = gpu
|
||||
self.regs: dict[int, int] = {}
|
||||
gfx = MockGFX(gpu, self)
|
||||
self.gmc = MockGMC(gpu, self, gfx)
|
||||
self.blocks = [MockPSP(gpu, self), MockSMU(gpu, self), MockSDMA(gpu, self), gfx, self.gmc, MockNBIO(gpu, self)]
|
||||
self._addr_block: dict[int, MockIPBlock] = {}
|
||||
for block in self.blocks:
|
||||
for addr in block.addrs: self._addr_block.setdefault(addr, block)
|
||||
|
||||
def __getitem__(self, index:int|slice) -> int|list[int]:
|
||||
if isinstance(index, slice): return [self[i] for i in range(index.start or 0, index.stop or 0, index.step or 1)] # type: ignore[misc]
|
||||
if index == 0xde3: return VRAM_SIZE >> 20
|
||||
if block := self._addr_block.get(index): return block.read(index)
|
||||
return self.regs.get(index, 0)
|
||||
|
||||
def __setitem__(self, index:int|slice, val:int|list[int]|tuple[int, ...]):
|
||||
if isinstance(index, slice):
|
||||
vals = val if isinstance(val, (list, tuple)) else [val] * ((index.stop - index.start) // (index.step or 1)) # type: ignore[operator]
|
||||
for i, v in zip(range(index.start or 0, index.stop or 0, index.step or 1), vals): self[i] = v
|
||||
return
|
||||
assert isinstance(val, int)
|
||||
self.regs[index] = val
|
||||
if block := self._addr_block.get(index): block.write(index, val)
|
||||
|
||||
def __len__(self): return 0x10000000
|
||||
|
||||
class MockAMGPU(AMDGPU):
|
||||
def __init__(self, gpuid:int=0):
|
||||
super().__init__(gpuid)
|
||||
self.vram_fd = os.memfd_create("vram")
|
||||
os.ftruncate(self.vram_fd, VRAM_SIZE)
|
||||
self.vram_addr = libc.mmap(0, VRAM_SIZE, mmap.PROT_READ | mmap.PROT_WRITE, mmap.MAP_SHARED, self.vram_fd, 0)
|
||||
self.vram = (ctypes.c_ubyte * VRAM_SIZE).from_address(self.vram_addr)
|
||||
self.doorbell_fd = os.memfd_create("doorbell")
|
||||
os.ftruncate(self.doorbell_fd, 0x2000)
|
||||
self.arch = "rdna4"
|
||||
self._sysmem_map:dict[int,int] = {}
|
||||
self._next_sysmem_paddr = 0x100000000
|
||||
self.mmu = MockMMU(self)
|
||||
self.mmio = MockMMIOInterface(self)
|
||||
self._preboot()
|
||||
|
||||
def translate_addr(self, addr:int) -> int: return self.mmu.addr_to_host(addr)
|
||||
|
||||
def map_vram_at(self, va:int, paddr:int, size:int):
|
||||
libc.mmap(va, size, mmap.PROT_READ | mmap.PROT_WRITE, mmap.MAP_SHARED | 0x10, self.vram_fd, paddr)
|
||||
|
||||
def _preboot(self):
|
||||
ip_data = bytearray()
|
||||
for hwip, (major, minor, rev) in IP_VERSIONS.items():
|
||||
ip = am.struct_ip_v4(hw_id=IP_HWIDS[hwip], num_base_address=len(IP_BASES[hwip]), major=major, minor=minor, revision=rev)
|
||||
ip_data += bytes(ip) + b'\x00'
|
||||
for b in IP_BASES[hwip]: ip_data += struct.pack('<I', b)
|
||||
|
||||
dhdr = am.struct_die_header(num_ips=len(IP_VERSIONS))
|
||||
ihdr = am.struct_ip_discovery_header(signature=am.DISCOVERY_TABLE_SIGNATURE, version=4, num_dies=1)
|
||||
ip_disc_off = ctypes.sizeof(am.struct_binary_header)
|
||||
ihdr.die_info[0].die_offset = ip_disc_off + ctypes.sizeof(am.struct_ip_discovery_header)
|
||||
|
||||
gc = am.struct_gc_info_v2_1()
|
||||
gc.header.table_id, gc.header.version_major, gc.header.version_minor = am.GC, 2, 1
|
||||
gc.header.size = ctypes.sizeof(am.struct_gc_info_v2_1)
|
||||
for field, val in GC_INFO.items(): setattr(gc, field, val)
|
||||
|
||||
gc_off = ip_disc_off + ctypes.sizeof(am.struct_ip_discovery_header) + ctypes.sizeof(am.struct_die_header) + len(ip_data)
|
||||
bhdr = am.struct_binary_header(binary_signature=am.BINARY_SIGNATURE)
|
||||
bhdr.table_list[am.IP_DISCOVERY].offset = ip_disc_off
|
||||
bhdr.table_list[am.GC].offset = gc_off
|
||||
|
||||
tbl = bytes(bhdr) + bytes(ihdr) + bytes(dhdr) + ip_data + bytes(gc)
|
||||
tbl_offset = VRAM_SIZE - (64 << 10)
|
||||
self.vram[tbl_offset:tbl_offset + len(tbl)] = list(tbl)
|
||||
|
||||
@property
|
||||
def mc_base(self) -> int:
|
||||
fb_loc_base = self.mmio.gmc.reg('regMMMC_VM_FB_LOCATION_BASE') or 0
|
||||
return (self.mmio.regs.get(fb_loc_base, 0) & 0xFFFFFF) << 24
|
||||
@@ -127,7 +127,7 @@ class PM4Executor(AMDQueue):
|
||||
val = val_lo + (val_hi << 32)
|
||||
_ = self._next_dword() # ev
|
||||
|
||||
ptr = to_mv(addr_lo + (addr_hi << 32), 8)
|
||||
ptr = to_mv(self.gpu.translate_addr(addr_lo + (addr_hi << 32)), 8)
|
||||
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)
|
||||
@@ -143,7 +143,7 @@ class PM4Executor(AMDQueue):
|
||||
dst_addr_lo = self._next_dword()
|
||||
dst_addr_hi = self._next_dword()
|
||||
assert copy_data_flags in {0x100204, 0x000204}, hex(copy_data_flags) # better fail than silently do the wrong thing
|
||||
to_mv(dst_addr_hi<<32|dst_addr_lo, 4).cast('I')[0] = self.gpu.regs[src_addr_lo]
|
||||
to_mv(self.gpu.translate_addr(dst_addr_hi<<32|dst_addr_lo), 4).cast('I')[0] = self.gpu.regs[src_addr_lo]
|
||||
|
||||
def _exec_wait_reg_mem(self, n):
|
||||
assert n == 5
|
||||
@@ -161,7 +161,7 @@ class PM4Executor(AMDQueue):
|
||||
|
||||
if mem_space == 0 and mem_op == 1: mval = val # hack for memory barrier, should properly handle (req_req, reg_done)
|
||||
elif mem_space == 0: mval = self.gpu.regs[addr_hi<<32|addr_lo]
|
||||
elif mem_space == 1: mval = to_mv(addr_lo + (addr_hi << 32), 4).cast('I')[0]
|
||||
elif mem_space == 1: mval = to_mv(self.gpu.translate_addr(addr_lo + (addr_hi << 32)), 4).cast('I')[0]
|
||||
|
||||
mval &= mask
|
||||
|
||||
@@ -225,7 +225,7 @@ class PM4Executor(AMDQueue):
|
||||
wptr = memoryview(bytearray(8)).cast('Q')
|
||||
rptr[0] = 0
|
||||
wptr[0] = buf_sz
|
||||
self.ib_executor = PM4Executor(self.gpu, (addr_hi << 32) | addr_lo, buf_sz * 4, rptr, wptr)
|
||||
self.ib_executor = PM4Executor(self.gpu, self.gpu.translate_addr((addr_hi << 32) | addr_lo), buf_sz * 4, rptr, wptr)
|
||||
|
||||
def _exec_event_write(self, n):
|
||||
assert n == 0
|
||||
@@ -276,7 +276,7 @@ class SDMAExecutor(AMDQueue):
|
||||
|
||||
def _execute_fence(self):
|
||||
struct = sdma_pkts.fence.from_address(self.base + self.rptr[0] % self.size)
|
||||
to_mv(struct.addr, 8).cast('Q')[0] = struct.data
|
||||
to_mv(self.gpu.translate_addr(struct.addr), 8).cast('Q')[0] = struct.data
|
||||
self.rptr[0] += ctypes.sizeof(struct)
|
||||
|
||||
def _execute_trap(self):
|
||||
@@ -287,7 +287,7 @@ class SDMAExecutor(AMDQueue):
|
||||
struct = sdma_pkts.poll_regmem.from_address(self.base + self.rptr[0] % self.size)
|
||||
|
||||
if struct.mem_poll == 0: mval = struct.value & struct.mask
|
||||
elif struct.mem_poll == 1: mval = to_mv(struct.addr, 4).cast('I')[0] & struct.mask
|
||||
elif struct.mem_poll == 1: mval = to_mv(self.gpu.translate_addr(struct.addr), 4).cast('I')[0] & struct.mask
|
||||
|
||||
if struct.func == WAIT_REG_MEM_FUNCTION_GEQ: can_cont = bool(mval >= struct.value)
|
||||
elif struct.func == WAIT_REG_MEM_FUNCTION_EQ: can_cont = bool(mval == struct.value)
|
||||
@@ -302,7 +302,7 @@ class SDMAExecutor(AMDQueue):
|
||||
def _execute_timestamp(self):
|
||||
struct = sdma_pkts.timestamp.from_address(self.base + self.rptr[0] % self.size)
|
||||
|
||||
mem = to_mv(struct.addr, 8).cast('Q')
|
||||
mem = to_mv(self.gpu.translate_addr(struct.addr), 8).cast('Q')
|
||||
mem[0] = int(time.perf_counter() * 1e8)
|
||||
|
||||
self.rptr[0] += ctypes.sizeof(struct)
|
||||
@@ -313,8 +313,8 @@ class SDMAExecutor(AMDQueue):
|
||||
|
||||
def _execute_copy(self):
|
||||
struct = sdma_pkts.copy_linear.from_address(self.base + self.rptr[0] % self.size)
|
||||
count_cnt = to_mv(self.base + self.rptr[0] + 4, 4).cast('I')[0] & 0x3FFFFFFF
|
||||
ctypes.memmove(struct.dst_addr, struct.src_addr, count_cnt + 1)
|
||||
count_cnt = to_mv(self.base + self.rptr[0] % self.size + 4, 4).cast('I')[0] & 0x3FFFFFFF
|
||||
ctypes.memmove(self.gpu.translate_addr(struct.dst_addr), self.gpu.translate_addr(struct.src_addr), count_cnt + 1)
|
||||
self.rptr[0] += ctypes.sizeof(struct)
|
||||
|
||||
class AMDGPURegisters:
|
||||
@@ -343,6 +343,7 @@ class AMDGPU(VirtGPU):
|
||||
self.queues = []
|
||||
self.arch = "cdna" if MOCKGPU_ARCH == "cdna4" else MOCKGPU_ARCH
|
||||
|
||||
def translate_addr(self, addr:int) -> int: return addr
|
||||
def map_range(self, vaddr, size): self.mapped_ranges.add((vaddr, size))
|
||||
def unmap_range(self, vaddr, size): self.mapped_ranges.remove((vaddr, size))
|
||||
def add_pm4_queue(self, base, size, rptr, wptr):
|
||||
|
||||
@@ -416,10 +416,10 @@ class Parser:
|
||||
case '||' | '|': return left | right
|
||||
case '&&' | '&': return left & right
|
||||
case '^': return left ^ right
|
||||
case '==' | '<>': return left.eq(right) if op == '==' else left.ne(right)
|
||||
case '==': return left.eq(right)
|
||||
case '!=': return left.ne(right)
|
||||
case '>=' | '<=' | '>' | '<':
|
||||
ops = {'>=':(lambda a,b:a>=b),'<=':(lambda a,b:a<=b),'>':(lambda a,b:a>b),'<':(lambda a,b:a<b)}
|
||||
case '>=' | '<=' | '>' | '<' | '<>':
|
||||
ops = {'>=':(lambda a,b:a>=b),'<=':(lambda a,b:a<=b),'>':(lambda a,b:a>b),'<':(lambda a,b:a<b),'<>':(lambda a,b:a.ne(b))}
|
||||
return self._cmp_nan(left, right, ops[op])
|
||||
case '>>' | '<<': return (left >> right) if op == '>>' else (left << right)
|
||||
case '+' | '-':
|
||||
|
||||
+14
-3
@@ -1,7 +1,9 @@
|
||||
import ctypes, ctypes.util, time, os, builtins, fcntl
|
||||
from tinygrad.helpers import getenv
|
||||
from tinygrad.runtime.support.hcq import FileIOInterface
|
||||
from test.mockgpu.nv.nvdriver import NVDriver
|
||||
from test.mockgpu.amd.amddriver import AMDDriver
|
||||
from test.mockgpu.am.amdriver import AMDriver, AMUSBDriver
|
||||
start = time.perf_counter()
|
||||
|
||||
# *** ioctl lib ***
|
||||
@@ -9,7 +11,8 @@ libc = ctypes.CDLL(ctypes.util.find_library("c"))
|
||||
libc.mmap.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_long]
|
||||
libc.mmap.restype = ctypes.c_void_p
|
||||
|
||||
drivers = [AMDDriver(), NVDriver()]
|
||||
_amd_iface = getenv("AMD_IFACE", "")
|
||||
drivers = [NVDriver(), AMDriver() if _amd_iface == "PCI" else (AMUSBDriver() if _amd_iface == "USB" else AMDDriver())]
|
||||
tracked_fds = {}
|
||||
|
||||
original_memoryview = builtins.memoryview
|
||||
@@ -77,9 +80,10 @@ class MockFileIOInterface(FileIOInterface):
|
||||
return libc.mmap(start, sz, prot, flags, self.fd, offset)
|
||||
|
||||
def read(self, size=None, binary=False, offset=None):
|
||||
if binary: raise NotImplementedError()
|
||||
if self.fd in tracked_fds:
|
||||
if offset is not None: tracked_fds[self.fd].seek(offset)
|
||||
return tracked_fds[self.fd].read_contents(size)
|
||||
if binary: raise NotImplementedError()
|
||||
with open(self.fd, "rb" if binary else "r", closefd=False) as file:
|
||||
if file.tell() >= os.fstat(self.fd).st_size: file.seek(0)
|
||||
return file.read(size)
|
||||
@@ -89,13 +93,20 @@ class MockFileIOInterface(FileIOInterface):
|
||||
return tracked_fds[self.fd].list_contents()
|
||||
return os.listdir(self.path)
|
||||
|
||||
def write(self, content, binary=False, offset=None): raise NotImplementedError()
|
||||
def write(self, content, binary=False, offset=None):
|
||||
if self.fd in tracked_fds:
|
||||
if offset is not None: tracked_fds[self.fd].seek(offset)
|
||||
return tracked_fds[self.fd].write_contents(content)
|
||||
raise NotImplementedError()
|
||||
def seek(self, offset):
|
||||
if self.fd in tracked_fds:
|
||||
tracked_fds[self.fd].seek(offset)
|
||||
else:
|
||||
os.lseek(self.fd, offset, os.SEEK_CUR)
|
||||
@staticmethod
|
||||
def anon_mmap(start, sz, prot, flags, offset):
|
||||
return FileIOInterface._mmap(start, sz, prot, flags & ~0x4a000, -1, offset) # strip MAP_LOCKED|MAP_POPULATE|MAP_HUGETLB
|
||||
@staticmethod
|
||||
def exists(path): return _open(path, os.O_RDONLY) is not None
|
||||
@staticmethod
|
||||
def readlink(path): raise NotImplementedError()
|
||||
|
||||
+205
-8
@@ -1,16 +1,213 @@
|
||||
from __future__ import annotations
|
||||
import ctypes, mmap, struct, sys
|
||||
if sys.platform != "win32": from tinygrad.runtime.autogen import libc
|
||||
|
||||
class MockUSB:
|
||||
def __init__(self, mem):
|
||||
self.mem = mem
|
||||
|
||||
def read(self, address, size):
|
||||
return bytes(self.mem[address:address+size])
|
||||
|
||||
def write(self, address, data, ignore_cache=False):
|
||||
self.mem[address:address+len(data)] = data
|
||||
|
||||
def read(self, address, size): return bytes(self.mem[address:address+size])
|
||||
def write(self, address, data, ignore_cache=False): self.mem[address:address+len(data)] = data
|
||||
def pcie_mem_req(self, address, value=None, size=1):
|
||||
if value is None: return int.from_bytes(self.mem[address:address+size], "little")
|
||||
else: self.mem[address:address+size] = value.to_bytes(size, "little")
|
||||
|
||||
def pcie_mem_write(self, address, values, size):
|
||||
for i, value in enumerate(values): self.pcie_mem_req(address + i * size, value, size)
|
||||
|
||||
# *** ASM24 Controller Mock ***
|
||||
|
||||
_mock_usb_state: MockASM24State|None = None
|
||||
|
||||
class MockASM24State:
|
||||
"""Mock ASM24 controller: XRAM memory map, DMA windows, TLP engine, PCI config space.
|
||||
|
||||
Memory map (64KB XRAM):
|
||||
0xA000-0xAFFF: DMA window -> sys 0x820000
|
||||
0xB000-0xB1FF: DMA window -> sys 0x800000
|
||||
0xB200-0xB7FF: PCI MMIO (TLP engine)
|
||||
0xF000-0xFFFF: DMA window -> sys 0x200000 (512KB)
|
||||
"""
|
||||
XRAM_SIZE = 0x10000
|
||||
|
||||
TLP_FMT_TYPE = 0xB210
|
||||
TLP_BYTE_EN = 0xB217
|
||||
TLP_ADDR_LO = 0xB218
|
||||
TLP_ADDR_HI = 0xB21C
|
||||
TLP_DATA = 0xB220
|
||||
TLP_COMPL = 0xB22A
|
||||
TLP_TRIGGER = 0xB254
|
||||
TLP_LINK_STATUS = 0xB284
|
||||
TLP_STATUS = 0xB296
|
||||
|
||||
def __init__(self, gpu, driver, vram_size:int, doorbell_size:int, mmio_size:int):
|
||||
self.gpu, self.driver = gpu, driver
|
||||
self._xram = bytearray(self.XRAM_SIZE)
|
||||
|
||||
self._doorbell_addr = libc.mmap(0, doorbell_size, mmap.PROT_READ | mmap.PROT_WRITE, mmap.MAP_SHARED, gpu.doorbell_fd, 0)
|
||||
self._doorbell = (ctypes.c_ubyte * doorbell_size).from_address(self._doorbell_addr)
|
||||
|
||||
# DMA windows: ctrl_addr -> (host_addr, size)
|
||||
self._dma_regions: dict[int, tuple[int, int]] = {}
|
||||
self._add_dma_window(0xF000, 0x200000, 0x80000)
|
||||
self._add_dma_window(0xA000, 0x820000, 0x1000)
|
||||
self._add_dma_window(0xB000, 0x800000, 0x200)
|
||||
|
||||
# PCI config space: (bus,dev,fn) -> bytearray(4096)
|
||||
self._pci_cfg: dict[tuple[int,int,int], bytearray] = {}
|
||||
|
||||
# GPU BAR definitions: reg_offset -> (size, type_bits, is_64bit)
|
||||
self._gpu_bars: dict[int, tuple[int, int, bool]] = {
|
||||
0x10: (vram_size, 0x0C, True), # BAR0: VRAM, 64-bit prefetchable
|
||||
0x18: (doorbell_size, 0x00, False), # BAR2: doorbell, 32-bit
|
||||
0x1C: (0, 0x00, False), # BAR3: unused
|
||||
0x20: (0, 0x00, False), # BAR4: unused
|
||||
0x24: (mmio_size, 0x00, False), # BAR5: MMIO, 32-bit
|
||||
}
|
||||
self._bar_addrs: dict[int, tuple[int, int]] = {} # reg_offset -> (addr, size)
|
||||
|
||||
# Initialize GPU config space (bus=4, dev=0, fn=0) with BAR type bits and REBAR capability
|
||||
gpu_cfg = self._get_cfg(4, 0, 0)
|
||||
for reg_off, (sz, type_bits, _) in self._gpu_bars.items():
|
||||
if sz > 0: struct.pack_into('<I', gpu_cfg, reg_off, type_bits)
|
||||
struct.pack_into('<I', gpu_cfg, 0x100, 0x15 | (1 << 16)) # REBAR cap header: id=0x15, version=1, next=0
|
||||
struct.pack_into('<I', gpu_cfg, 0x104, sum(1 << (i + 4) for i in range(10))) # supported sizes up to 512MB
|
||||
|
||||
def _get_cfg(self, bus:int, dev:int, fn:int) -> bytearray:
|
||||
if (key:=(bus, dev, fn)) not in self._pci_cfg: self._pci_cfg[key] = bytearray(4096)
|
||||
return self._pci_cfg[key]
|
||||
|
||||
def _add_dma_window(self, ctrl_addr:int, sys_addr:int, size:int):
|
||||
host_addr = libc.mmap(0, size, mmap.PROT_READ | mmap.PROT_WRITE, mmap.MAP_SHARED | mmap.MAP_ANONYMOUS, -1, 0)
|
||||
self._dma_regions[ctrl_addr] = (host_addr, size)
|
||||
for off in range(0, size, 0x1000): self.gpu._sysmem_map[sys_addr + off] = host_addr + off
|
||||
|
||||
# --- XRAM access ---
|
||||
|
||||
def _xram_read(self, addr:int, length:int) -> bytes:
|
||||
for ctrl_addr, (host_addr, dma_size) in self._dma_regions.items():
|
||||
if ctrl_addr <= addr < ctrl_addr + dma_size:
|
||||
return bytes((ctypes.c_ubyte * length).from_address(host_addr + (addr - ctrl_addr)))
|
||||
return bytes(self._xram[addr:addr+length])
|
||||
|
||||
def _xram_write_byte(self, addr:int, value:int):
|
||||
for ctrl_addr, (host_addr, dma_size) in self._dma_regions.items():
|
||||
if ctrl_addr <= addr < ctrl_addr + dma_size:
|
||||
(ctypes.c_ubyte * 1).from_address(host_addr + (addr - ctrl_addr))[0] = value
|
||||
return
|
||||
if addr == self.TLP_STATUS:
|
||||
self._xram[addr] &= ~value & 0xFF
|
||||
return
|
||||
self._xram[addr] = value
|
||||
if addr == self.TLP_TRIGGER and value == 0x0F: self._process_tlp()
|
||||
|
||||
# --- TLP engine ---
|
||||
|
||||
def _process_tlp(self):
|
||||
fmt_type, byte_en = self._xram[self.TLP_FMT_TYPE], self._xram[self.TLP_BYTE_EN]
|
||||
addr_lo = int.from_bytes(self._xram[self.TLP_ADDR_LO:self.TLP_ADDR_LO+4], 'big')
|
||||
addr_hi = int.from_bytes(self._xram[self.TLP_ADDR_HI:self.TLP_ADDR_HI+4], 'big')
|
||||
address = addr_lo | (addr_hi << 32)
|
||||
|
||||
size, offset, tmp = 0, 0, byte_en
|
||||
while tmp and not (tmp & 1):
|
||||
offset += 1
|
||||
tmp >>= 1
|
||||
while tmp:
|
||||
size += tmp & 1
|
||||
tmp >>= 1
|
||||
|
||||
is_write, is_cfg = bool(fmt_type & 0x40), (fmt_type & 0xbe) == 0x04
|
||||
|
||||
if is_cfg:
|
||||
bus, dev, fn, byte_addr = (address >> 24) & 0xFF, (address >> 19) & 0x1F, (address >> 16) & 0x7, address & 0xFFC
|
||||
if is_write:
|
||||
data = int.from_bytes(self._xram[self.TLP_DATA:self.TLP_DATA+4], 'big')
|
||||
self._cfg_write(bus, dev, fn, byte_addr + offset, (data >> (8 * offset)) & ((1 << (8 * size)) - 1), size)
|
||||
else:
|
||||
self._xram[self.TLP_DATA:self.TLP_DATA+4] = int.from_bytes(self._get_cfg(bus, dev, fn)[byte_addr:byte_addr+4], 'little').to_bytes(4, 'big')
|
||||
self._xram[self.TLP_COMPL:self.TLP_COMPL+2] = (4).to_bytes(2, 'big')
|
||||
self._xram[self.TLP_LINK_STATUS] = 0x01 if not is_write else 0x00
|
||||
self._xram[self.TLP_STATUS] = 0x02
|
||||
return
|
||||
|
||||
if is_write:
|
||||
data = int.from_bytes(self._xram[self.TLP_DATA:self.TLP_DATA+4], 'big')
|
||||
self._pcie_dispatch(address + offset, (data >> (8 * offset)) & ((1 << (8 * size)) - 1), size)
|
||||
else:
|
||||
result = self._pcie_dispatch(address + offset, None, size)
|
||||
if result is not None:
|
||||
self._xram[self.TLP_DATA:self.TLP_DATA+4] = ((result << (8 * offset)) & 0xFFFFFFFF).to_bytes(4, 'big')
|
||||
|
||||
self._xram[self.TLP_COMPL:self.TLP_COMPL+2] = (size & 0xFFF).to_bytes(2, 'big')
|
||||
self._xram[self.TLP_LINK_STATUS] = 0x01 if not is_write else 0x00
|
||||
self._xram[self.TLP_STATUS] = 0x02
|
||||
|
||||
def _cfg_write(self, bus:int, dev:int, fn:int, byte_addr:int, val:int, size:int):
|
||||
cfg = self._get_cfg(bus, dev, fn)
|
||||
|
||||
# Handle BAR register writes for GPU device (bus=4, dev=0, fn=0)
|
||||
if (bus, dev, fn) == (4, 0, 0) and 0x10 <= byte_addr < 0x28 and size == 4:
|
||||
reg_off = byte_addr & ~0x3
|
||||
if (bar_def:=self._gpu_bars.get(reg_off)) is not None:
|
||||
bar_size, type_bits, is_64 = bar_def
|
||||
if bar_size == 0: return # unused BAR
|
||||
if val == 0xFFFFFFFF: # size probe
|
||||
struct.pack_into('<I', cfg, reg_off, (~(bar_size - 1)) & 0xFFFFFFF0 | type_bits)
|
||||
else:
|
||||
struct.pack_into('<I', cfg, reg_off, val)
|
||||
hi = struct.unpack_from('<I', cfg, reg_off + 4)[0] if is_64 else 0
|
||||
self._bar_addrs[reg_off] = ((hi << 32) | (val & ~0xF), bar_size)
|
||||
return
|
||||
# Check if upper 32 bits of a 64-bit BAR
|
||||
for breg, (bsz, _, b64) in self._gpu_bars.items():
|
||||
if b64 and reg_off == breg + 4:
|
||||
struct.pack_into('<I', cfg, reg_off, 0xFFFFFFFF if val == 0xFFFFFFFF else val)
|
||||
if val != 0xFFFFFFFF:
|
||||
self._bar_addrs[breg] = ((val << 32) | (struct.unpack_from('<I', cfg, breg)[0] & ~0xF), bsz)
|
||||
return
|
||||
|
||||
# Generic config write
|
||||
for i in range(size): cfg[byte_addr + i] = (val >> (8 * i)) & 0xFF
|
||||
|
||||
def _pcie_dispatch(self, address:int, value:int|None, size:int) -> int|None:
|
||||
for reg_off, (bar_addr, bar_size) in self._bar_addrs.items():
|
||||
if bar_addr <= address < bar_addr + bar_size:
|
||||
offset = address - bar_addr
|
||||
if reg_off == 0x10: # BAR0 - VRAM
|
||||
if value is None: return int.from_bytes(bytes(self.gpu.vram[offset:offset+size]), "little")
|
||||
self.gpu.vram[offset:offset+size] = list(value.to_bytes(size, "little"))
|
||||
return None
|
||||
if reg_off == 0x18: # BAR2 - Doorbell
|
||||
if value is None: return int.from_bytes(bytes(self._doorbell[offset:offset+size]), "little")
|
||||
for i, b in enumerate(value.to_bytes(size, "little")): self._doorbell[offset + i] = b
|
||||
self.driver._emulate_execute()
|
||||
return None
|
||||
if reg_off == 0x24: # BAR5 - MMIO
|
||||
if value is None: return self.gpu.mmio[offset // 4]
|
||||
self.gpu.mmio[offset // 4] = value
|
||||
return None
|
||||
raise ValueError(f"PCIe address {address:#x} not mapped to any BAR")
|
||||
|
||||
# --- CDB processing (called by MockUSB3.send_batch) ---
|
||||
|
||||
def process_cdb(self, cdb:bytes, rlen:int, send_data:bytes|None) -> bytes|None:
|
||||
op = cdb[0]
|
||||
if op == 0xE5: # write byte
|
||||
self._xram_write_byte(((cdb[2] << 16) | (cdb[3] << 8) | cdb[4]) & 0xFFFF, cdb[1])
|
||||
return None
|
||||
if op == 0xE4: # read
|
||||
return self._xram_read(((cdb[2] << 16) | (cdb[3] << 8) | cdb[4]) & 0xFFFF, cdb[1])
|
||||
if op == 0x8A and send_data is not None and 0xF000 in self._dma_regions: # SCSI write
|
||||
host_addr, dma_size = self._dma_regions[0xF000]
|
||||
ctypes.memmove(host_addr, send_data, min(len(send_data), dma_size))
|
||||
return None
|
||||
|
||||
class MockUSB3:
|
||||
def __init__(self, *args, **kwargs): pass
|
||||
def send_batch(self, cdbs:list[bytes], idata:list[int]|None=None, odata:list[bytes|None]|None=None) -> list[bytes|None]:
|
||||
assert _mock_usb_state is not None
|
||||
idata, odata = idata or [0] * len(cdbs), odata or [None] * len(cdbs)
|
||||
results: list[bytes|None] = []
|
||||
for cdb, rlen, sdata in zip(cdbs, idata, odata):
|
||||
result = _mock_usb_state.process_cdb(cdb, rlen, sdata)
|
||||
results.append(result if rlen > 0 else None)
|
||||
return results
|
||||
|
||||
@@ -87,7 +87,7 @@ class TestHuggingFaceOnnxModels(unittest.TestCase):
|
||||
"input_ids": np.random.randint(0, 250002, (1, 11), dtype=np.int64),
|
||||
"attention_mask": np.ones((1, 11), dtype=np.int64),
|
||||
}
|
||||
self._validate(repo_id, model_file, custom_inputs)
|
||||
self._validate(repo_id, model_file, custom_inputs, atol=1e-3)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -349,5 +349,184 @@ class TestStopEarly(unittest.TestCase):
|
||||
ret = (c+d).substitute({c:cn}, extra_pm=pm_cvisit)
|
||||
assert ret == cn+d
|
||||
|
||||
class TestWalkRewrite(unittest.TestCase):
|
||||
"""Tests for graph_rewrite with walk=True (MLIR Walk Pattern Rewrite Driver semantics).
|
||||
walk=True gives a single-pass traversal that does NOT revisit or re-traverse into rewritten subtrees.
|
||||
Supports both top-down (default) and bottom-up (bottom_up=True) modes."""
|
||||
|
||||
# *** top-down walk (default): process children first, then try pm on rebuilt node ***
|
||||
|
||||
def test_walk_topdown_simple_substitute(self):
|
||||
a = UOp.variable('a', 0, 10)
|
||||
b = UOp.variable('b', 0, 10)
|
||||
ret = graph_rewrite(a + 4, _substitute, {a:b}, walk=True)
|
||||
self.assertIs(ret, b+4)
|
||||
|
||||
def test_walk_topdown_does_not_traverse_into_replacement(self):
|
||||
"""Top-down walk: replacement subtrees are NOT re-entered."""
|
||||
a = UOp.variable('a', 0, 10)
|
||||
b = UOp.variable('b', 0, 10)
|
||||
c = UOp.variable('c', 0, 10)
|
||||
d = UOp.variable('d', 0, 10)
|
||||
# a is replaced by b+c, but b inside the replacement is NOT further substituted to d
|
||||
ret_walk = graph_rewrite(a + 4, _substitute, {a:b+c, b:d}, walk=True)
|
||||
self.assertIs(ret_walk, (b+c)+4)
|
||||
# contrast: greedy bottom_up WOULD replace b inside the replacement
|
||||
ret_greedy = graph_rewrite(a + 4, _substitute, {a:b+c, b:d}, bottom_up=True)
|
||||
self.assertIs(ret_greedy, (d+c)+4)
|
||||
|
||||
def test_walk_topdown_no_fixed_point(self):
|
||||
"""A bouncing pattern applies once and stops instead of looping."""
|
||||
a = UOp.const(dtypes.int, 3)
|
||||
pm = PatternMatcher([
|
||||
(UPat(Ops.CONST, arg=3, name="x"), lambda x: x.replace(arg=4)),
|
||||
(UPat(Ops.CONST, arg=4, name="x"), lambda x: x.replace(arg=3)),
|
||||
])
|
||||
with self.assertRaises(RuntimeError):
|
||||
graph_rewrite(a, pm, bottom_up=True)
|
||||
ret = graph_rewrite(a, pm, walk=True)
|
||||
self.assertIs(ret, UOp.const(dtypes.int, 4))
|
||||
|
||||
def test_walk_topdown_rewrites_children(self):
|
||||
a = UOp.variable('a', 0, 10)
|
||||
b = UOp.variable('b', 0, 10)
|
||||
c = UOp.variable('c', 0, 10)
|
||||
ret = graph_rewrite((a + 4) + (b + 5), _substitute, {a:c, b:c}, walk=True)
|
||||
self.assertIs(ret, (c + 4) + (c + 5))
|
||||
|
||||
def test_walk_topdown_diamond(self):
|
||||
a = UOp.variable('a', 0, 10)
|
||||
b = UOp.variable('b', 0, 10)
|
||||
ret = graph_rewrite((a + 4) + (a + 5), _substitute, {a:b}, walk=True)
|
||||
self.assertIs(ret, (b + 4) + (b + 5))
|
||||
|
||||
def test_walk_topdown_children_rewritten_before_parent(self):
|
||||
"""Top-down walk processes children first: child substitution changes the rebuilt parent."""
|
||||
a = UOp.variable('a', 0, 10, dtype=dtypes.float)
|
||||
n1 = a.sin() # sin(a)
|
||||
ret = n1.sin() # sin(sin(a))
|
||||
# sin(a)->sqrt(a) fires first (child), parent rebuilds to sin(sqrt(a)), which doesn't match sin(sin(a)) in dvars
|
||||
ret_walk = graph_rewrite(ret, _substitute, {a.sin():a.sqrt(), n1.sin():n1.sqrt()}, walk=True)
|
||||
self.assertIs(ret_walk, a.sqrt().sin())
|
||||
|
||||
def test_walk_topdown_self_referential_replacement(self):
|
||||
"""Replacement containing the replaced node works without infinite recursion."""
|
||||
a = UOp.variable('a', 0, 10, dtype=dtypes.float)
|
||||
ret = graph_rewrite(a.sin() + 4, _substitute, {a.sin(): a.sin().sqrt()}, walk=True)
|
||||
self.assertIs(ret, a.sin().sqrt() + 4)
|
||||
|
||||
def test_walk_topdown_visit_order(self):
|
||||
"""Top-down walk fires pm after children are processed (post-order)."""
|
||||
visited = []
|
||||
def track_visit(ctx, x):
|
||||
ctx.append(x.arg if x.op is Ops.CONST else x.op)
|
||||
return None
|
||||
pm = PatternMatcher([(UPat(GroupOp.All, name="x"), track_visit)])
|
||||
a = UOp.const(dtypes.int, 1)
|
||||
b = UOp.const(dtypes.int, 2)
|
||||
graph_rewrite(a + b, pm, ctx=visited, walk=True)
|
||||
self.assertEqual(visited, [1, 2, Ops.ADD])
|
||||
|
||||
# *** bottom-up walk: try bpm on node first, skip children if it matches ***
|
||||
|
||||
def test_walk_bottomup_simple_substitute(self):
|
||||
a = UOp.variable('a', 0, 10)
|
||||
b = UOp.variable('b', 0, 10)
|
||||
ret = graph_rewrite(a + 4, _substitute, {a:b}, bottom_up=True, walk=True)
|
||||
self.assertIs(ret, b+4)
|
||||
|
||||
def test_walk_bottomup_does_not_traverse_into_replacement(self):
|
||||
"""Bottom-up walk: replacement subtrees are NOT entered."""
|
||||
a = UOp.variable('a', 0, 10)
|
||||
b = UOp.variable('b', 0, 10)
|
||||
c = UOp.variable('c', 0, 10)
|
||||
d = UOp.variable('d', 0, 10)
|
||||
ret = graph_rewrite(a + 4, _substitute, {a:b+c, b:d}, bottom_up=True, walk=True)
|
||||
self.assertIs(ret, (b+c)+4)
|
||||
|
||||
def test_walk_bottomup_parent_match_skips_children(self):
|
||||
"""Bottom-up walk matches parent first: if it matches, children are never visited."""
|
||||
a = UOp.variable('a', 0, 10, dtype=dtypes.float)
|
||||
n1 = a.sin()
|
||||
ret = n1.sin() # sin(sin(a))
|
||||
# sin(sin(a)) matches n1.sin()->n1.sqrt() immediately, children never visited, sin(a) inside replacement untouched
|
||||
ret_walk = graph_rewrite(ret, _substitute, {a.sin():a.sqrt(), n1.sin():n1.sqrt()}, bottom_up=True, walk=True)
|
||||
self.assertIs(ret_walk, a.sin().sqrt())
|
||||
|
||||
def test_walk_bottomup_no_fixed_point(self):
|
||||
"""Bottom-up walk also applies once per node, no fixed-point iteration."""
|
||||
a = UOp.const(dtypes.int, 3)
|
||||
pm = PatternMatcher([
|
||||
(UPat(Ops.CONST, arg=3, name="x"), lambda x: x.replace(arg=4)),
|
||||
(UPat(Ops.CONST, arg=4, name="x"), lambda x: x.replace(arg=3)),
|
||||
])
|
||||
ret = graph_rewrite(a, pm, bottom_up=True, walk=True)
|
||||
self.assertIs(ret, UOp.const(dtypes.int, 4))
|
||||
|
||||
def test_walk_bottomup_visit_order(self):
|
||||
"""Bottom-up walk fires bpm before descending (pre-order)."""
|
||||
visited = []
|
||||
def track_visit(ctx, x):
|
||||
ctx.append(x.arg if x.op is Ops.CONST else x.op)
|
||||
return None
|
||||
pm = PatternMatcher([(UPat(GroupOp.All, name="x"), track_visit)])
|
||||
a = UOp.const(dtypes.int, 1)
|
||||
b = UOp.const(dtypes.int, 2)
|
||||
graph_rewrite(a + b, pm, ctx=visited, bottom_up=True, walk=True)
|
||||
# bpm fires on each node before children: +, 1, 2
|
||||
self.assertEqual(visited, [Ops.ADD, 1, 2])
|
||||
|
||||
def test_walk_bottomup_unmatched_falls_through_to_children(self):
|
||||
"""Bottom-up walk: if bpm doesn't match a node, its children are still processed."""
|
||||
a = UOp.variable('a', 0, 10)
|
||||
b = UOp.variable('b', 0, 10)
|
||||
c = UOp.variable('c', 0, 10)
|
||||
# only a is in dvars, not a+4. bpm won't match a+4, so it descends and finds a.
|
||||
ret = graph_rewrite((a + 4) + (b + 5), _substitute, {a:c, b:c}, bottom_up=True, walk=True)
|
||||
self.assertIs(ret, (c + 4) + (c + 5))
|
||||
|
||||
# *** bidirectional walk: bpm fires before children, pm fires after rebuild ***
|
||||
|
||||
def test_walk_bidirectional_visit_order(self):
|
||||
"""Bidirectional walk: bpm fires pre-order, pm fires post-order."""
|
||||
visited = []
|
||||
def bpm_visit(ctx, x):
|
||||
ctx.append((x.arg if x.op is Ops.CONST else x.op, "bpm"))
|
||||
return None
|
||||
def pm_visit(ctx, x):
|
||||
ctx.append((x.arg if x.op is Ops.CONST else x.op, "pm"))
|
||||
return None
|
||||
bpm = PatternMatcher([(UPat(GroupOp.All, name="x"), bpm_visit)])
|
||||
pm = PatternMatcher([(UPat(GroupOp.All, name="x"), pm_visit)])
|
||||
a = UOp.const(dtypes.int, 1)
|
||||
b = UOp.const(dtypes.int, 2)
|
||||
graph_rewrite(a + b, pm, ctx=visited, bpm=bpm, walk=True)
|
||||
# bpm fires pre-order, pm fires post-order
|
||||
self.assertEqual(visited, [
|
||||
(Ops.ADD, "bpm"), (1, "bpm"), (1, "pm"), (2, "bpm"), (2, "pm"), (Ops.ADD, "pm"),
|
||||
])
|
||||
|
||||
def test_walk_bidirectional_bpm_short_circuits(self):
|
||||
"""If bpm matches, children are skipped and pm never fires on that node."""
|
||||
visited = []
|
||||
def bpm_match(ctx, x):
|
||||
ctx.append((x.arg if x.op is Ops.CONST else x.op, "bpm"))
|
||||
# rewrite const(1) -> const(10), short-circuiting its subtree
|
||||
if x.op is Ops.CONST and x.arg == 1: return x.replace(arg=10)
|
||||
return None
|
||||
def pm_match(ctx, x):
|
||||
ctx.append((x.arg if x.op is Ops.CONST else x.op, "pm"))
|
||||
return None
|
||||
bpm = PatternMatcher([(UPat(GroupOp.All, name="x"), bpm_match)])
|
||||
pm = PatternMatcher([(UPat(GroupOp.All, name="x"), pm_match)])
|
||||
a = UOp.const(dtypes.int, 1)
|
||||
b = UOp.const(dtypes.int, 2)
|
||||
ret = graph_rewrite(a + b, pm, ctx=visited, bpm=bpm, walk=True)
|
||||
# bpm matches const(1) and short-circuits it, so pm never fires on const(1)
|
||||
self.assertNotIn((1, "pm"), visited)
|
||||
# but pm still fires on const(2) and the rebuilt ADD
|
||||
self.assertIn((2, "pm"), visited)
|
||||
self.assertIs(ret, UOp.const(dtypes.int, 10) + b)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -8,7 +8,7 @@ class TestDataset(unittest.TestCase):
|
||||
X_train[0].contiguous().realize()
|
||||
GlobalCounters.reset()
|
||||
X_train[0].contiguous().realize()
|
||||
self.assertEqual(GlobalCounters.kernel_count, 1)
|
||||
self.assertLessEqual(GlobalCounters.kernel_count, 1) # 0 if BUFFER_VIEW (zero-copy), 1 otherwise
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import gc, unittest
|
||||
from tinygrad import Tensor, GlobalCounters, dtypes
|
||||
from tinygrad.engine.jit import TinyJit
|
||||
|
||||
class TestMultiRamUsage(unittest.TestCase):
|
||||
def setUp(self):
|
||||
gc.collect()
|
||||
self.baseline = GlobalCounters.mem_used
|
||||
self.baseline_per_device = dict(GlobalCounters.mem_used_per_device)
|
||||
self.N = 100
|
||||
def assertUsed(self, amt, strict=True):
|
||||
gc.collect()
|
||||
@@ -12,6 +14,11 @@ class TestMultiRamUsage(unittest.TestCase):
|
||||
print(f"used {used} bytes")
|
||||
if strict: self.assertEqual(used, amt)
|
||||
else: self.assertLessEqual(used, amt)
|
||||
def assertDeviceUsed(self, expected:dict[str, int]):
|
||||
gc.collect()
|
||||
for dev, amt in expected.items():
|
||||
used = GlobalCounters.mem_used_per_device[dev] - self.baseline_per_device.get(dev, 0)
|
||||
self.assertEqual(used, amt, f"device {dev}: expected {amt} bytes used, got {used}")
|
||||
|
||||
def test_zeros(self):
|
||||
_ = Tensor.zeros(self.N, self.N).contiguous().realize()
|
||||
@@ -38,6 +45,54 @@ class TestMultiRamUsage(unittest.TestCase):
|
||||
_ = Tensor.zeros(self.N, self.N).contiguous().shard(devices_2, axis=0).contiguous().realize()
|
||||
self.assertUsed(self.N*self.N*4) # sharding should not increase total ram usage
|
||||
|
||||
def test_sharded_memory_replicated(self):
|
||||
devices_4 = tuple(f"NULL:{i+1}" for i in range(4))
|
||||
X = Tensor.ones(256).contiguous().realize()
|
||||
self.assertUsed(256 * 4)
|
||||
X.shard_(devices_4).realize()
|
||||
self.assertUsed(256 * 4 * 4)
|
||||
|
||||
def test_sharded_memory_replicated_const(self):
|
||||
devices_4 = tuple(f"NULL:{i+1}" for i in range(4))
|
||||
X = Tensor.ones(256).realize()
|
||||
self.assertUsed(0)
|
||||
X.shard_(devices_4).realize()
|
||||
self.assertUsed(256 * 4 * 4) # TODO: can be zero
|
||||
|
||||
def test_sharded_memory_axis_const(self):
|
||||
devices_4 = tuple(f"NULL:{i+1}" for i in range(4))
|
||||
X = Tensor.ones(256).realize()
|
||||
self.assertUsed(0)
|
||||
X.shard_(devices_4, axis=0).realize()
|
||||
self.assertUsed(256 * 4) # TODO: can be zero
|
||||
|
||||
def test_zeros_per_device(self):
|
||||
_ = Tensor.zeros(self.N, self.N, device="NULL").contiguous().realize()
|
||||
self.assertDeviceUsed({"NULL": self.N*self.N*4})
|
||||
|
||||
def test_zeros_del_per_device(self):
|
||||
_ = Tensor.zeros(self.N, self.N, device="NULL").contiguous().realize()
|
||||
del _
|
||||
self.assertDeviceUsed({"NULL": 0})
|
||||
|
||||
def test_zeros_copy_per_device(self):
|
||||
devices_2 = ("NULL:1", "NULL:2")
|
||||
_ = Tensor.zeros(self.N, self.N).contiguous().to(devices_2).realize()
|
||||
self.assertDeviceUsed({"NULL:1": self.N*self.N*4, "NULL:2": self.N*self.N*4})
|
||||
|
||||
def test_zeros_shard_per_device(self):
|
||||
devices_2 = ("NULL:1", "NULL:2")
|
||||
_ = Tensor.zeros(self.N, self.N).contiguous().shard(devices_2, axis=0).realize()
|
||||
self.assertDeviceUsed({"NULL:1": self.N*(self.N//2)*4, "NULL:2": self.N*(self.N//2)*4})
|
||||
|
||||
def test_sharded_memory_replicated_per_device(self):
|
||||
devices_4 = tuple(f"NULL:{i+1}" for i in range(4))
|
||||
X = Tensor.ones(256, device="NULL").contiguous().realize()
|
||||
self.assertDeviceUsed({"NULL": 256*4})
|
||||
X.shard_(devices_4).realize()
|
||||
for d in devices_4:
|
||||
self.assertDeviceUsed({d: 256*4})
|
||||
|
||||
def _test_matmul_half(self, dev_count:int):
|
||||
N = 32
|
||||
total_mem = {}
|
||||
@@ -53,6 +108,38 @@ class TestMultiRamUsage(unittest.TestCase):
|
||||
def test_matmul_half(self): self._test_matmul_half(dev_count=2)
|
||||
def test_matmul_half_alt(self): self._test_matmul_half(dev_count=4)
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_multi_layer_allreduce(self):
|
||||
N = 32
|
||||
devices_2 = ("NULL:1", "NULL:2")
|
||||
|
||||
def make_inp():
|
||||
x = Tensor.zeros(N, N).contiguous().shard(devices_2, axis=None).realize()
|
||||
w1 = Tensor.zeros(N, N).contiguous().shard(devices_2, axis=1).realize()
|
||||
w2 = Tensor.zeros(N, N).contiguous().shard(devices_2, axis=0).realize()
|
||||
return x, w1, w2
|
||||
|
||||
def run_layers(n_layers):
|
||||
GlobalCounters.reset()
|
||||
|
||||
@TinyJit
|
||||
def f(x, w1, w2):
|
||||
for _ in range(n_layers):
|
||||
x = (x @ w1 @ w2)
|
||||
return x.contiguous()
|
||||
|
||||
for _ in range(3):
|
||||
a = make_inp()
|
||||
r = f(*a)
|
||||
del a, r
|
||||
|
||||
gc.collect()
|
||||
return GlobalCounters.mem_used
|
||||
|
||||
mem_2 = run_layers(2)
|
||||
mem_4 = run_layers(4)
|
||||
self.assertEqual(mem_2, mem_4, f"graph memory should not grow with layers: 2 layers={mem_2}, 4 layers={mem_4}")
|
||||
|
||||
class TestMultiAxis(unittest.TestCase):
|
||||
def test_reshape_shard_invalid(self):
|
||||
devices = ("NULL:0", "NULL:1")
|
||||
@@ -66,5 +153,13 @@ class TestMultiAxis(unittest.TestCase):
|
||||
self.assertEqual(t.reshape(2, 16).uop.axis, 0)
|
||||
self.assertEqual(t.reshape(2, 2, 8).uop.axis, 0)
|
||||
|
||||
def test_empty_like_sharded(self):
|
||||
t = Tensor.ones(4, 8).shard(("NULL:0", "NULL:1"), axis=0)
|
||||
e = t.empty_like()
|
||||
self.assertEqual(e.shape, t.shape)
|
||||
self.assertEqual(e.device, t.device)
|
||||
self.assertEqual(e.uop.axis, 0)
|
||||
self.assertTrue(e.uop.has_buffer_identity())
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user