Compare commits

..
Author SHA1 Message Date
geohot 2f85319722 Merge remote-tracking branch 'origin/master' into amd_sqtt
# Conflicts:
#	extra/assembly/amd/emu.py
#	extra/assembly/amd/sqtt.py
2026-01-12 05:42:04 +09:00
geohot d9f0e9c40c something 2026-01-12 02:26:31 +09:00
geohot 3dcffbea25 NO SLOT 2026-01-11 17:31:50 +09:00
geohot 41c5368266 close 2026-01-11 17:26:20 +09:00
geohot 4598a21f94 rdna3 timing 2026-01-11 07:14:39 +00:00
geohot 27084cd618 some 2026-01-11 16:12:16 +09:00
geohot d2616e5daf weird forward beavhior 2026-01-11 16:00:21 +09:00
geohot 3130c53f85 strange hardware behavior 2026-01-11 15:53:40 +09:00
geohot 1c66e41383 new test 2026-01-11 15:23:37 +09:00
geohot 93823b272c DEBUG=3 is pretty 2026-01-11 15:05:29 +09:00
geohot 1c6147e9bf better 2026-01-11 14:55:41 +09:00
geohot 7f5656d236 cold chain 2026-01-11 14:33:39 +09:00
geohot 5f55a61700 dumb 2026-01-11 13:22:39 +09:00
geohot ed097df864 cleaner 2026-01-11 13:14:10 +09:00
geohot a83c97f17e sqtt correct 2026-01-11 13:12:51 +09:00
geohot c793076fb6 add s_delay_alu tests 2026-01-11 11:28:21 +09:00
geohot 1f45601a97 tests with early nops 2026-01-11 11:16:25 +09:00
geohot 14c4989f65 pipeline exec 2026-01-11 11:11:52 +09:00
geohot 31b38640ac nop anomaly 2026-01-11 10:41:13 +09:00
geohot fe770e822c pats 2026-01-11 09:56:05 +09:00
geohot 768231c065 lat tests 2026-01-11 09:54:32 +09:00
geohot 4165594b30 first cycle lat 2026-01-11 09:48:04 +09:00
geohot c03b7b0da1 gap5 anomaly 2026-01-11 09:13:51 +09:00
geohot 66249836c0 good test 2026-01-11 09:11:36 +09:00
geohot a0d6ed9914 a couple more 2026-01-11 09:06:32 +09:00
geohot 99fcfc0e97 cleaner 2026-01-11 08:57:19 +09:00
geohot cf8bb15aef padding 2026-01-11 08:55:51 +09:00
geohot 32dfc9b1d0 another test 2026-01-11 08:49:20 +09:00
geohot 9803e389fe good tests 2026-01-11 08:29:39 +09:00
geohot 1f893b65cc new hw free test 2026-01-11 07:49:50 +09:00
geohot 35f5f05ad5 multiwave 2026-01-09 21:01:26 -08:00
geohot b9f08ad18a fix multiwave 2026-01-09 21:01:26 -08:00
geohot 222ae38aa4 fix multiwave 2026-01-09 21:01:26 -08:00
geohot f0bf20d7b2 structuring 2026-01-09 21:01:26 -08:00
geohot 85ef097da6 snop passes 2026-01-09 21:01:26 -08:00
George HotzandGitHub 0e240fb987 Merge branch 'master' into amd_sqtt 2026-01-02 20:30:16 -05:00
geohot d2c1712e4c more tests 2026-01-02 17:29:48 -08:00
geohot 96b0ee0966 lil 2026-01-02 16:53:31 -08:00
geohot 9b5c4bc698 shorter 2026-01-02 16:48:26 -08:00
geohot 6ea3586101 short 2026-01-02 16:45:34 -08:00
geohot 92cb8b6776 tests pass 2026-01-02 16:43:03 -08:00
geohot c416b20668 failures 2026-01-02 15:54:02 -08:00
geohot 415b83ba18 tests pass 2026-01-02 15:47:39 -08:00
geohot 8c7eacea59 getting close 2026-01-02 15:25:18 -08:00
geohot 81542699f8 work 2026-01-02 14:39:52 -08:00
geohot 79f55a5d5e test_snop is correct 2026-01-02 12:01:08 -08:00
geohot 37518fb236 start with nop 2026-01-02 11:40:08 -08:00
geohot 672008ccab framework 2026-01-02 11:31:41 -08:00
geohot 849af761a4 simpler 2026-01-02 11:10:40 -08:00
geohot ab46b3d8d3 origin/master 2026-01-02 10:47:00 -08:00
geohot df20197bfb rever emu to master 2026-01-02 10:46:46 -08:00
geohot 2b56c264d5 compare tests 2026-01-02 10:39:07 -08:00
geohot c7e5c2f996 Merge origin/master, remove deleted test_emu.py 2026-01-02 09:41:34 -08:00
geohot 659aa14043 orks 2026-01-02 05:29:48 -08:00
geohot 21ffa1a86b 64 nops 2026-01-02 00:38:27 -05:00
geohot 29f3fb7af3 still stable 2026-01-01 23:45:19 -05:00
geohot 1edc7fc519 stable 2026-01-01 23:43:43 -05:00
geohot c9a3ac988c cleanest 2026-01-01 23:18:19 -05:00
geohot 77d96acbe3 clean 2026-01-01 22:59:07 -05:00
geohot 660ecf272b work 2026-01-01 22:50:50 -05:00
geohot 267bbb163e progress 2026-01-01 21:11:29 -05:00
geohot de29a49ea3 all the ones i can find 2026-01-01 20:56:30 -05:00
geohot 742e10a572 remove fake ones 2026-01-01 20:26:53 -05:00
geohot 447fe8907b more 2026-01-01 20:22:52 -05:00
geohot b0cfcec183 good 2026-01-01 20:12:20 -05:00
geohot 1726084b2a filt 2026-01-01 19:40:43 -05:00
geohot de069a4876 many 2026-01-01 19:21:46 -05:00
geohot 4573e91e61 more 2026-01-01 18:51:31 -05:00
geohot 8d43212bc6 assembly/amd: start work on SQTT parsing/emulation 2026-01-01 18:40:58 -05:00
geohot a8bea4ec52 remove __all__ 2026-01-01 16:14:15 -05:00
geohot 388514c5b1 better 2026-01-01 16:03:29 -05:00
geohot 729bb04d8c fix test failure 2026-01-01 13:21:55 -05:00
geohot 8f4de73141 two tests 2026-01-01 13:13:01 -05:00
geohot a5959ef0f1 fix all tests 2026-01-01 13:11:51 -05:00
geohot 5ba06892c0 generic 2026-01-01 12:46:08 -05:00
geohot 469efe313d that's a hack 2026-01-01 12:40:14 -05:00
geohot e3b3cb163d fix emu test 2026-01-01 12:12:47 -05:00
geohot 3e32185faf more tests 2026-01-01 12:04:41 -05:00
geohot 5328913d2b fix flat bug 2026-01-01 11:51:10 -05:00
geohot 9c49ec1cc1 update autogen 2026-01-01 11:36:33 -05:00
geohot 000d4a125b fix ds op 2026-01-01 10:36:37 -05:00
geohot 63289902d8 refactors 2025-12-31 17:57:27 -05:00
geohot b596f77e33 assembly/amd: add pcode ds ops 2025-12-31 16:59:02 -05:00
248 changed files with 145311 additions and 139291 deletions
+16 -6
View File
@@ -43,17 +43,27 @@ jobs:
run: sudo apt-get install -y --no-install-recommends libclang-20-dev llvm-20-dev hip-dev libusb-1.0-0-dev
- name: Regenerate autogen files
run: |
find tinygrad/runtime/autogen -type f -name "*.py" -not -name "__init__.py" -not -name "comgr_3.py" -not -name "metal.py" -not -name "iokit.py" -not -name "corefoundation.py" -not -name "libclang.py" -delete
rm tinygrad/runtime/autogen/opencl.py
python3 -c "from tinygrad.runtime.autogen import opencl"
python3 -c "from tinygrad.runtime.autogen import cuda, nvrtc, nvjitlink, nv_570, nv_580, nv"
python3 -c "from tinygrad.runtime.autogen import comgr, hsa, hip, amd_gpu, sqtt, rocprof, amdgpu_kd"
python3 -c "from tinygrad.runtime.autogen.am import am, pm4_soc15, pm4_nv, sdma_4_0_0, sdma_5_0_0, sdma_6_0_0, smu_v13_0_0, smu_v13_0_6, smu_v14_0_2"
rm tinygrad/runtime/autogen/{cuda,nvrtc,nvjitlink,nv_570,nv}.py
python3 -c "from tinygrad.runtime.autogen import cuda, nvrtc, nvjitlink, nv_570, nv"
rm tinygrad/runtime/autogen/{comgr,hsa,hip,amd_gpu,sqtt,rocprof}.py
python3 -c "from tinygrad.runtime.autogen import comgr, hsa, hip, amd_gpu, sqtt, rocprof"
rm tinygrad/runtime/autogen/am/{am,pm4_soc15,pm4_nv,sdma_4_0_0,sdma_5_0_0,sdma_6_0_0,smu_v13_0_0,smu_v14_0_2}.py
python3 -c "from tinygrad.runtime.autogen.am import am, pm4_soc15, pm4_nv, sdma_4_0_0, sdma_5_0_0, sdma_6_0_0, smu_v13_0_0, smu_v14_0_2"
rm tinygrad/runtime/autogen/{libc,kfd,io_uring,ib,pci,vfio}.py
python3 -c "from tinygrad.runtime.autogen import libc, kfd, io_uring, ib, pci, vfio"
rm tinygrad/runtime/autogen/llvm.py
python3 -c "from tinygrad.runtime.autogen import llvm"
rm tinygrad/runtime/autogen/webgpu.py
python3 -c "from tinygrad.runtime.autogen import webgpu"
rm tinygrad/runtime/autogen/{kgsl,qcom_dsp}.py
python3 -c "from tinygrad.runtime.autogen import kgsl, qcom_dsp"
rm tinygrad/runtime/autogen/libusb.py
python3 -c "from tinygrad.runtime.autogen import libusb"
rm tinygrad/runtime/autogen/mesa.py
python3 -c "from tinygrad.runtime.autogen import mesa"
rm tinygrad/runtime/autogen/avcodec.py
python3 -c "from tinygrad.runtime.autogen import avcodec"
REGEN=1 python3 -c "from tinygrad.runtime.autogen import libclang"
- name: Check for differences
@@ -83,8 +93,8 @@ jobs:
llvm: 'true'
- name: Regenerate autogen files
run: |
rm tinygrad/runtime/autogen/metal.py tinygrad/runtime/autogen/iokit.py tinygrad/runtime/autogen/corefoundation.py
LIBCLANG_PATH=/opt/homebrew/opt/llvm@20/lib/libclang.dylib python3 -c "from tinygrad.runtime.autogen import metal, iokit, corefoundation"
rm tinygrad/runtime/autogen/metal.py
LIBCLANG_PATH=/opt/homebrew/opt/llvm@20/lib/libclang.dylib python3 -c "from tinygrad.runtime.autogen import metal"
- name: Check for differences
run: |
if ! git diff --quiet; then
+14 -24
View File
@@ -329,12 +329,10 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setcap to python
run: ./extra/amdpci/setup_python_cap.sh
- name: Remove amd modules
run: ./extra/hcq/hcq_smi.py amd rmmod
- name: Kill stale pids
run: ./extra/hcq/hcq_smi.py amd kill_pids
- name: Remove amdgpu
run: sudo rmmod amdgpu || true
- name: Cleanup running AM processes
run: python extra/amdpci/am_smi.py --pids --kill
#- name: Insert amdgpu
# run: sudo modprobe amdgpu
- name: Symlink models and datasets
@@ -441,12 +439,10 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setcap to python
run: ./extra/amdpci/setup_python_cap.sh
- name: Remove amd modules
run: ./extra/hcq/hcq_smi.py amd rmmod
- name: Kill stale pids
run: ./extra/hcq/hcq_smi.py amd kill_pids
- name: Remove amdgpu
run: sudo rmmod amdgpu || true
- name: Cleanup running AM processes
run: python extra/amdpci/am_smi.py --pids --kill
- name: Symlink models and datasets
run: |
mkdir -p weights
@@ -493,12 +489,10 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setcap to python
run: ./extra/amdpci/setup_python_cap.sh
- name: Remove amd modules
run: ./extra/hcq/hcq_smi.py amd rmmod
- name: Kill stale pids
run: ./extra/hcq/hcq_smi.py amd kill_pids
- name: Remove amdgpu
run: sudo rmmod amdgpu || true
- name: Cleanup running AM processes
run: python extra/amdpci/am_smi.py --pids --kill
- name: Symlink models and datasets
run: |
mkdir -p weights
@@ -547,7 +541,7 @@ jobs:
- name: reset process replay
run: test/external/process_replay/reset.py
- name: openpilot compile3 0.10.0 driving_policy
run: BENCHMARK_LOG=openpilot_0_10_0_policy PYTHONPATH="." ASSERT_MIN_STEP_TIME=3 DEV=QCOM FLOAT16=1 IMAGE=2 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/v0.10.0/selfdrive/modeld/models/driving_policy.onnx
run: BENCHMARK_LOG=openpilot_0_10_0_policy PYTHONPATH="." ASSERT_MIN_STEP_TIME=4 DEV=QCOM FLOAT16=1 IMAGE=2 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/v0.10.0/selfdrive/modeld/models/driving_policy.onnx
- name: openpilot compile3 0.10.0 dmonitoring
run: BENCHMARK_LOG=openpilot_0_10_0_dmonitoring PYTHONPATH="." ASSERT_MIN_STEP_TIME=11 DEV=QCOM FLOAT16=1 IMAGE=2 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/v0.10.0/selfdrive/modeld/models/dmonitoring_model.onnx
- name: DEBUG=2 openpilot compile3 0.10.1 driving_vision
@@ -559,7 +553,7 @@ jobs:
- name: openpilot compile3 0.10.1 driving_vision
run: BENCHMARK_LOG=openpilot_0_10_1_vision PYTHONPATH="." ASSERT_MIN_STEP_TIME=17 DEV=QCOM FLOAT16=1 IMAGE=2 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/720392c9a5b986981fdbed1bb8c47a6c5573a50e/selfdrive/modeld/models/driving_vision.onnx
- name: openpilot compile3 0.10.1 driving_policy
run: BENCHMARK_LOG=openpilot_0_10_1_policy PYTHONPATH="." ASSERT_MIN_STEP_TIME=3 DEV=QCOM FLOAT16=1 IMAGE=2 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/720392c9a5b986981fdbed1bb8c47a6c5573a50e/selfdrive/modeld/models/driving_policy.onnx
run: BENCHMARK_LOG=openpilot_0_10_1_policy PYTHONPATH="." ASSERT_MIN_STEP_TIME=4 DEV=QCOM FLOAT16=1 IMAGE=2 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/720392c9a5b986981fdbed1bb8c47a6c5573a50e/selfdrive/modeld/models/driving_policy.onnx
- name: openpilot compile3 0.10.1 dmonitoring
run: BENCHMARK_LOG=openpilot_0_10_1_dmonitoring PYTHONPATH="." ASSERT_MIN_STEP_TIME=10 DEV=QCOM FLOAT16=1 IMAGE=2 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/720392c9a5b986981fdbed1bb8c47a6c5573a50e/selfdrive/modeld/models/dmonitoring_model.onnx
- name: benchmark MobileNetV2 on DSP
@@ -584,8 +578,6 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setcap to python
run: ./extra/amdpci/setup_python_cap.sh
- name: Remove amd modules
run: ./extra/hcq/hcq_smi.py amd rmmod
- name: Kill stale pids
@@ -648,8 +640,6 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setcap to python
run: ./extra/amdpci/setup_python_cap.sh
- name: Remove nv modules
run: ./extra/hcq/hcq_smi.py nv rmmod
- name: Kill stale pids
+8 -10
View File
@@ -110,7 +110,7 @@ jobs:
- name: Test ResNet-18
run: DEBUG=2 python3 extra/torch_backend/example.py
- name: custom tests
run: python3 -m pytest -n auto extra/torch_backend/test.py --durations=20
run: python3 extra/torch_backend/test.py
- name: Test one op in torch tests
run: DEBUG=2 python3 extra/torch_backend/torch_tests.py TestTinyBackendPRIVATEUSE1.test_unary_log_tiny_float32
- name: Test Ops with TINY_BACKEND
@@ -229,14 +229,14 @@ jobs:
deps: linting
- name: Lint bad-indentation and trailing-whitespace with pylint
run: python -m pylint --disable=all -e W0311 -e C0303 --jobs=0 --indent-string=' ' --recursive=y .
- name: Run pre-commit linting hooks
run: SKIP=tiny,tests,example pre-commit run --all-files
- name: Lint additional files with ruff
- name: Lint with ruff
run: |
pip3 install --upgrade --force-reinstall ruff==0.14.10
pre-commit run ruff --all-files
python3 -m ruff check examples/mlperf/ --ignore E501
python3 -m ruff check extra/thunder/tiny/ --ignore E501 --ignore F841 --ignore E722
python3 -m ruff check extra/torch_backend/backend.py
- name: Run mypy with lineprecision report
- name: Run mypy
run: |
python -m mypy --lineprecision-report .
cat lineprecision.txt
@@ -254,12 +254,10 @@ jobs:
- name: Setup Environment
uses: ./.github/actions/setup-tinygrad
with:
key: unittest-13
pydeps: "pillow numpy ftfy regex pre-commit"
key: unittest-12
pydeps: "pillow numpy ftfy regex"
deps: testing_unit
llvm: 'true'
- name: Run pre-commit test hooks
run: SKIP=ruff,mypy pre-commit run --all-files
- name: Check Device.DEFAULT
run: python -c "from tinygrad import Device; assert Device.DEFAULT == 'CPU', Device.DEFAULT"
- name: Run unit tests
@@ -672,7 +670,7 @@ jobs:
python-version: '3.13'
- name: Verify AMD autogen is up to date
run: |
python -m extra.assembly.amd.amdxml
python -m extra.assembly.amd.pdf
git diff --exit-code extra/assembly/amd/autogen/
- name: Install LLVM 21
run: |
+1 -2
View File
@@ -58,11 +58,10 @@ weights
*.lprof
comgr_*
*.pkl
!extra/sqtt/examples/**/*.pkl
site/
profile_stats
*.log
target
.mypy_cache
mutants
.mutmut-cache
.mutmut-cache
+1 -1
View File
@@ -28,7 +28,7 @@ repos:
pass_filenames: false
- id: tests
name: comprehensive test suite
entry: env OMP_NUM_THREADS=1 SKIP_SLOW_TEST=1 PYTHONPATH="." python3 -m pytest -n=6 test/test_ops.py test/test_schedule.py test/unit/test_assign.py test/test_tensor.py test/test_jit.py test/unit/test_schedule_cache.py test/unit/test_pattern_matcher.py test/unit/test_uop_symbolic.py test/unit/test_helpers.py
entry: env OMP_NUM_THREADS=1 SKIP_SLOW_TEST=1 PYTHONPATH="." python3 -m pytest -n=6 test/test_ops.py test/test_schedule.py test/test_assign.py test/test_tensor.py test/test_jit.py test/unit/test_schedule_cache.py test/unit/test_pattern_matcher.py test/unit/test_uop_symbolic.py test/unit/test_helpers.py
language: system
always_run: true
pass_filenames: false
+1 -6
View File
@@ -6,7 +6,6 @@ Elementwise ops operate on a per element basis. They don't change the shape of t
::: tinygrad.Tensor.neg
::: tinygrad.Tensor.log
::: tinygrad.Tensor.log2
::: tinygrad.Tensor.log10
::: tinygrad.Tensor.exp
::: tinygrad.Tensor.exp2
::: tinygrad.Tensor.sqrt
@@ -88,8 +87,4 @@ Elementwise ops operate on a per element basis. They don't change the shape of t
::: tinygrad.Tensor.float
::: tinygrad.Tensor.half
::: tinygrad.Tensor.int
::: tinygrad.Tensor.bool
::: tinygrad.Tensor.bfloat16
::: tinygrad.Tensor.double
::: tinygrad.Tensor.long
::: tinygrad.Tensor.short
::: tinygrad.Tensor.bool
-1
View File
@@ -27,6 +27,5 @@
::: tinygrad.Tensor.flatten
::: tinygrad.Tensor.unflatten
::: tinygrad.Tensor.diag
::: tinygrad.Tensor.diagonal
::: tinygrad.Tensor.roll
::: tinygrad.Tensor.rearrange
-10
View File
@@ -7,7 +7,6 @@
::: tinygrad.Tensor.any
::: tinygrad.Tensor.all
::: tinygrad.Tensor.isclose
::: tinygrad.Tensor.allclose
::: tinygrad.Tensor.mean
::: tinygrad.Tensor.var
::: tinygrad.Tensor.var_mean
@@ -31,9 +30,7 @@
::: tinygrad.Tensor.matmul
::: tinygrad.Tensor.einsum
::: tinygrad.Tensor.cumsum
::: tinygrad.Tensor.cumprod
::: tinygrad.Tensor.cummax
::: tinygrad.Tensor.cummin
::: tinygrad.Tensor.triu
::: tinygrad.Tensor.tril
::: tinygrad.Tensor.interpolate
@@ -41,9 +38,7 @@
::: tinygrad.Tensor.scatter_reduce
::: tinygrad.Tensor.masked_select
::: tinygrad.Tensor.masked_fill
::: tinygrad.Tensor.nonzero
::: tinygrad.Tensor.sort
::: tinygrad.Tensor.argsort
::: tinygrad.Tensor.topk
::: tinygrad.Tensor.multinomial
@@ -61,8 +56,3 @@
::: tinygrad.Tensor.sparse_categorical_crossentropy
::: tinygrad.Tensor.cross_entropy
::: tinygrad.Tensor.nll_loss
## Linear Algebra
::: tinygrad.Tensor.qr
::: tinygrad.Tensor.svd
-79
View File
@@ -1,79 +0,0 @@
from typing import Optional
from tinygrad import Tensor
from tinygrad.dtype import DTypeLike, dtypes
import math
# rewritten from numpy
def rfftfreq(n: int, d: float = 1.0, device=None) -> Tensor:
val = 1.0 / (n * d)
N = n // 2 + 1
results = Tensor.arange(N, device=device)
return results * val
# just like in librosa
def fft_frequencies(sr: float, n_fft: int) -> Tensor:
return rfftfreq(n=n_fft, d=1.0 / sr)
def hz_to_mel(freq: Tensor) -> Tensor:
# linear part
f_min = 0.0
f_sp = 200.0 / 3
mels = (freq - f_min) / f_sp
# log-scale part
min_log_hz = 1000.0 # beginning of log region (Hz)
mask = freq >= min_log_hz
return mask.where(((min_log_hz - f_min) / f_sp) + (freq / min_log_hz).log() / (math.log(6.4) / 27.0), mels)
def mel_to_hz(mels: Tensor) -> Tensor:
# linear scale
f_min = 0.0
f_sp = 200.0 / 3
freqs = f_min + f_sp * mels
# nonlinear scale
min_log_hz = 1000.0 # beginning of log region (Hz)
min_log_mel = (min_log_hz - f_min) / f_sp # same (Mels)
logstep = math.log(6.4) / 27.0 # step size for log region
log_t = mels >= min_log_mel
freqs = log_t.where(min_log_hz * ((logstep * (mels - min_log_mel)).exp()), freqs)
return freqs
def mel_frequencies(n_mels: int = 128, *, fmin: float = 0.0, fmax: float = 11025.0) -> Tensor:
# center freqs of mel bands - uniformly spaced between limits
min_max_mel = hz_to_mel(Tensor([fmin, fmax]))
mels = Tensor.linspace(min_max_mel[0], min_max_mel[1], n_mels)
hz = mel_to_hz(mels)
return hz
def mel(
*,
sr: float,
n_fft: int,
n_mels: int = 128,
fmin: float = 0.0,
fmax: Optional[float] = None,
dtype: DTypeLike = dtypes.default_float,
) -> Tensor:
if fmax is None:
fmax = float(sr) / 2
n_mels = int(n_mels)
fftfreqs = fft_frequencies(sr=sr, n_fft=n_fft) # center freqs of each FFT bin
mel_f = mel_frequencies(n_mels + 2, fmin=fmin, fmax=fmax) # center freqs of mel bands
fdiff = mel_f[1:] - mel_f[:-1]
ramps = mel_f[None].T.expand(-1, fftfreqs.shape[-1]) - fftfreqs
lower = -ramps[:n_mels] / fdiff[:n_mels][None].T
upper = ramps[2 : n_mels + 2] / fdiff[1 : n_mels + 1][None].T
weights = lower.minimum(upper).maximum(0)
# Slaney-style mel is scaled to be approx constant energy per channel
enorm = 2.0 / (mel_f[2 : n_mels + 2] - mel_f[:n_mels])
weights *= enorm[:, None]
return weights
+3 -21
View File
@@ -1286,8 +1286,6 @@ def train_llama3():
from examples.llama3 import MODEL_PARAMS
from examples.mlperf.lr_schedulers import CosineAnnealingLRWithWarmup
BENCHMARK = getenv("BENCHMARK")
config = {}
BASEDIR = config["BASEDIR"] = Path(getenv("BASEDIR", "/raid/datasets/c4/"))
BS = config["BS"] = getenv("BS", 16)
@@ -1328,8 +1326,6 @@ def train_llama3():
# vocab_size from the mixtral tokenizer
if not SMALL: model_params |= {"vocab_size": 32000}
if (llama_layers:=getenv("LLAMA_LAYERS")) != 0: model_params['n_layers'] = llama_layers
print(f"model parameters: {model_params}")
model = Transformer(**model_params, max_context=SEQLEN, jit=False, disable_kv_cache=True)
params = get_parameters(model)
# weights are all bfloat16 for now
@@ -1436,7 +1432,7 @@ def train_llama3():
eval_dataset = None
else:
from examples.mlperf.dataloader import get_llama3_dataset
eval_dataset = get_llama3_dataset(1024 if SMALL else 5760, SEQLEN, BASEDIR, val=True, small=bool(SMALL))
eval_dataset = get_llama3_dataset(5760, SEQLEN, BASEDIR, val=True, small=bool(SMALL))
def get_eval_iter():
if eval_dataset is None:
@@ -1446,7 +1442,6 @@ def train_llama3():
iter = get_train_iter()
i, sequences_seen = resume_ckpt, 0
step_times = []
for tokens in tqdm(iter, total=SAMPLES//GBS):
GlobalCounters.reset()
if getenv("TRAIN", 1):
@@ -1459,8 +1454,6 @@ def train_llama3():
sequences_seen += tokens.shape[0]
sec = time.perf_counter()-t
if BENCHMARK: step_times.append(sec)
mem_gb = GlobalCounters.mem_used / 1e9
gflops = GlobalCounters.global_ops / 1e9 / sec
tqdm.write(
@@ -1483,14 +1476,7 @@ def train_llama3():
fn = f"{ckpt_dir}/llama3_{i}_optim.safe"
safe_save(get_state_dict(scheduler), fn)
if i == BENCHMARK:
median_step_time = sorted(step_times)[(BENCHMARK + 1) // 2]
estimated_total_minutes = int(median_step_time * (SAMPLES // GBS) / 60)
print(f"Estimated training time: {estimated_total_minutes // 60}h{estimated_total_minutes % 60}m")
print(f"epoch global_ops: {GlobalCounters.global_ops:_}, "
f"epoch global_mem: {GlobalCounters.global_mem:_}")
if (sequences_seen % EVAL_FREQ == 0 and (i != 1 or EVAL_FREQ == 1)) or (BENCHMARK and i == BENCHMARK):
if sequences_seen % EVAL_FREQ == 0 and (i != 1 or EVAL_FREQ == 1):
tqdm.write(f"evaluating after {sequences_seen} sequences")
# run eval
@@ -1498,12 +1484,8 @@ def train_llama3():
eval_iter = get_eval_iter()
tqdm.write(f"evaluating {5760//EVAL_BS} batches of {EVAL_BS} sequences")
for j,tokens in tqdm(enumerate(eval_iter), total=5760//EVAL_BS):
for tokens in tqdm(eval_iter, total=5760//EVAL_BS):
eval_losses += eval_step(model, tokens).tolist()
if BENCHMARK and (j+1) == min(BENCHMARK, 5760//EVAL_BS):
return
log_perplexity = Tensor(eval_losses).mean().float().item()
tqdm.write(f"eval log perplexity: {log_perplexity:.4f}")
@@ -1,20 +0,0 @@
#!/bin/bash
export PYTHONPATH="." AMD=1 DEBUG=0 JIT=1 FLASH_ATTENTION=1
export MODEL="bert"
export DEFAULT_FLOAT="HALF" GPUS=8 BS=1024 EVAL_BS=1024
# similar to https://github.com/mlcommons/training_results_v3.1/blob/d06288b2bd675a9d88e0e6181f5bb5626b71ec19/Quanta_Cloud_Technology/results/D54U-3U/bert/result_1.txt#L54
export OPT_BASE_LEARNING_RATE=0.0011 OPT_LAMB_BETA_1=0.60466 OPT_LAMB_BETA_2=0.85437 DECAY=0.1
export TRAIN_STEPS=3900
export IGNORE_OOB=1
export REWRITE_STACK_LIMIT=5000000
export BEAM=0 BEAM_UOPS_MAX=6000 BEAM_UPCAST_MAX=256 BEAM_LOCAL_MAX=1024 BEAM_MIN_PROGRESS=5
export IGNORE_JIT_FIRST_BEAM=1 FREE_INTERMEDIATE=0
export BASEDIR="/raid/datasets/wiki"
export WANDB=1 PARALLEL=0
RUNMLPERF=1 python3 examples/mlperf/model_train.py
@@ -1,26 +0,0 @@
#!/usr/bin/env bash
export PYTHONPATH="." AMD=1
export IGNORE_OOB=1
export REWRITE_STACK_LIMIT=5000000 HCQDEV_WAIT_TIMEOUT_MS=240000
export DEBUG=${DEBUG:-0}
export FLASH_ATTENTION=1
export DEFAULT_FLOAT="bfloat16" OPTIM_DTYPE="bfloat16"
export DP=8 BS=8 EVAL_BS=8
export MODEL="llama3"
export BASEDIR="/raid/datasets/c4-8b/"
export SMALL=1
export LLAMA3_SIZE=${LLAMA3_SIZE:-"8b"}
export EVAL_TARGET=3.3 EVAL_FREQ=12288
export LR="1e-3" END_LR="1e-4" WARMUP_STEPS=1024 MAX_STEPS=1200000
export SAMPLES=$((MAX_STEPS * BS))
export SEED=5760
export JITBEAM=3
export BEAM_UOPS_MAX=6000 BEAM_UPCAST_MAX=256 BEAM_LOCAL_MAX=1024 BEAM_MIN_PROGRESS=5
python3 examples/mlperf/model_train.py
+1 -2
View File
@@ -7,7 +7,6 @@ from tinygrad import Tensor, TinyJit, Variable, nn, dtypes
from tinygrad.nn.state import torch_load, load_state_dict
from tinygrad.helpers import getenv, fetch
from examples.audio_helpers import mel
import numpy as np
import librosa
@@ -160,7 +159,7 @@ def prep_audio(waveforms: List[np.ndarray], batch_size: int, truncate=False) ->
stft = librosa.stft(waveforms, n_fft=N_FFT, hop_length=HOP_LENGTH, window='hann', dtype=np.csingle)
magnitudes = np.absolute(stft[..., :-1]) ** 2
mel_spec = mel(sr=RATE, n_fft=N_FFT, n_mels=N_MELS).numpy() @ magnitudes
mel_spec = librosa.filters.mel(sr=RATE, n_fft=N_FFT, n_mels=N_MELS) @ magnitudes
log_spec = np.log10(np.clip(mel_spec, 1e-10, None))
log_spec = np.maximum(log_spec, log_spec.max((1,2), keepdims=True) - 8.0)
-463
View File
@@ -1,463 +0,0 @@
# AMD ISA code generator - generates enum.py, ins.py, operands.py, str_pcode.py
# Sources: XML from https://gpuopen.com/download/machine-readable-isa/latest/
# PDF manuals from AMD documentation
import re, zlib, xml.etree.ElementTree as ET, zipfile
from tinygrad.helpers import fetch
# ═══════════════════════════════════════════════════════════════════════════════
# Configuration
# ═══════════════════════════════════════════════════════════════════════════════
ARCHS = {
"rdna3": {"xml": "amdgpu_isa_rdna3_5.xml", "pdf": "https://docs.amd.com/api/khub/documents/UVVZM22UN7tMUeiW_4ShTQ/content"},
"rdna4": {"xml": "amdgpu_isa_rdna4.xml", "pdf": "https://docs.amd.com/api/khub/documents/uQpkEvk3pv~kfAb2x~j4uw/content"},
"cdna": {"xml": "amdgpu_isa_cdna4.xml", "pdf": "https://www.amd.com/content/dam/amd/en/documents/instinct-tech-docs/instruction-set-architectures/amd-instinct-cdna4-instruction-set-architecture.pdf"},
}
XML_URL = "https://gpuopen.com/download/machine-readable-isa/latest/"
# Map XML encoding names to codebase names
NAME_MAP = {"VOP3_SDST_ENC": "VOP3SD", "VOP3_SDST_ENC_LIT": "VOP3SD_LIT", "VOP3_SDST_ENC_DPP16": "VOP3SD_DPP16",
"VOP3_SDST_ENC_DPP8": "VOP3SD_DPP8", "VOPDXY": "VOPD", "VOPDXY_LIT": "VOPD_LIT", "VDS": "DS"}
# Instructions missing from XML but present in PDF
FIXES = {"rdna3": {"SOPK": {22: "S_SUBVECTOR_LOOP_BEGIN", 23: "S_SUBVECTOR_LOOP_END"}, "FLAT": {55: "FLAT_ATOMIC_CSUB_U32"}},
"rdna4": {"SOP1": {80: "S_GET_BARRIER_STATE", 81: "S_BARRIER_INIT", 82: "S_BARRIER_JOIN"}, "SOPP": {9: "S_WAITCNT", 21: "S_BARRIER_LEAVE"}},
"cdna": {"DS": {152: "DS_GWS_SEMA_RELEASE_ALL", 154: "DS_GWS_SEMA_V", 156: "DS_GWS_SEMA_P"},
"VOP3P": {44: "V_MFMA_LD_SCALE_B32", 62: "V_MFMA_F32_16X16X8_XF32", 63: "V_MFMA_F32_32X32X4_XF32"}}}
# Encoding suffixes to strip (variants we don't generate separate classes for)
_ENC_SUFFIXES = ("_NSA1",)
# Encoding suffix to class suffix mapping (for variants we DO generate)
_ENC_SUFFIX_MAP = {"_INST_LITERAL": "_LIT", "_VOP_DPP16": "_DPP16", "_VOP_DPP": "_DPP16", "_VOP_DPP8": "_DPP8",
"_VOP_SDWA": "_SDWA", "_VOP_SDWA_SDST_ENC": "_SDWA_SDST", "_MFMA": "_MFMA"}
# Field name normalization
_FIELD_RENAMES = {"opsel_hi_2": "opsel_hi2", "op_sel_hi_2": "opsel_hi2", "op_sel": "opsel", "bound_ctrl": "bc",
"tgt": "target", "row_en": "row", "unorm": "unrm", "clamp": "clmp", "wait_exp": "waitexp",
"simm32": "literal", "dpp_ctrl": "dpp", "acc_cd": "acc_cd", "acc": "acc",
"dst_sel": "dst_sel", "dst_unused": "dst_unused", "src0_sel": "src0_sel", "src1_sel": "src1_sel"}
# Encoding variants to skip entirely (NSA is for MIMG graphics instructions)
_SKIP_ENCODINGS = ("NSA",)
# ═══════════════════════════════════════════════════════════════════════════════
# XML parsing helpers
# ═══════════════════════════════════════════════════════════════════════════════
def _strip_enc(name: str) -> str:
"""Strip ENC_ prefix and normalize encoding suffixes."""
name = name.removeprefix("ENC_")
for sfx in _ENC_SUFFIXES: name = name.replace(sfx, "")
# Process longer suffixes first to avoid partial matches (e.g., _VOP_DPP8 before _VOP_DPP)
for old, new in sorted(_ENC_SUFFIX_MAP.items(), key=lambda x: -len(x[0])): name = name.replace(old, new)
return name
def _norm_field(name: str) -> str:
"""Normalize field name to match expected names."""
for old, new in _FIELD_RENAMES.items(): name = name.replace(old, new)
return name
def _map_flat(enc_name: str, instr_name: str) -> str:
"""Map FLAT/GLOBAL/SCRATCH encoding to correct enum based on instruction prefix."""
if enc_name in ("FLAT_GLBL", "FLAT_GLOBAL"): return "GLOBAL"
if enc_name == "FLAT_SCRATCH": return "SCRATCH"
if enc_name in ("FLAT", "VFLAT", "VGLOBAL", "VSCRATCH"):
v = "V" if enc_name.startswith("V") else ""
if instr_name.startswith("GLOBAL_"): return f"{v}GLOBAL"
if instr_name.startswith("SCRATCH_"): return f"{v}SCRATCH"
return f"{v}FLAT"
return enc_name
# ═══════════════════════════════════════════════════════════════════════════════
# XML parsing
# ═══════════════════════════════════════════════════════════════════════════════
def parse_xml(filename: str):
root = ET.fromstring(zipfile.ZipFile(fetch(XML_URL)).read(filename))
encodings, enums, types, fmts, op_types_set = {}, {}, {}, {}, set()
# Extract HWREG and MSG enums from OperandTypes
op_enum_map = {("OPR_HWREG", "ID"): "HWREG", ("OPR_SENDMSG_RTN", "MSG"): "MSG"}
for ot in root.findall(".//OperandTypes/OperandType"):
ot_name = ot.findtext("OperandTypeName")
for field in ot.findall(".//Field"):
if (enum_name := op_enum_map.get((ot_name, field.findtext("FieldName")))):
enums[enum_name] = {int(pv.findtext("Value")): pv.findtext("Name").upper() for pv in field.findall(".//PredefinedValue")}
# Extract DataFormats with BitCount
for df in root.findall("ISA/DataFormats/DataFormat"):
name, bits = df.findtext("DataFormatName"), df.findtext("BitCount")
if name and bits: fmts[name] = int(bits)
# Extract encoding definitions
for enc in root.findall("ISA/Encodings/Encoding"):
name = enc.findtext("EncodingName")
is_base = name.startswith("ENC_") or name in ("VOP3_SDST_ENC", "VOPDXY")
is_variant = any(sfx in name for sfx in _ENC_SUFFIX_MAP)
if not is_base and not is_variant: continue
if any(s in name for s in _SKIP_ENCODINGS): continue
fields = [(_norm_field(f.findtext("FieldName").lower()), int(f.find("BitLayout/Range").findtext("BitOffset") or 0) + int(f.find("BitLayout/Range").findtext("BitCount") or 0) - 1,
int(f.find("BitLayout/Range").findtext("BitOffset") or 0))
for f in enc.findall(".//MicrocodeFormat/BitMap/Field") if f.find("BitLayout/Range") is not None]
ident = (enc.findall("EncodingIdentifiers/EncodingIdentifier") or [None])[0]
enc_field = next((f for f in fields if f[0] == "encoding"), None)
# For multi-dword formats, encoding field may be in higher dword but identifier pattern is always in dword0; use % 32
enc_bits = "".join(ident.text[len(ident.text)-1-b] for b in range(enc_field[1] % 32, (enc_field[2] % 32)-1, -1)) if ident is not None and enc_field else None
base_name = _strip_enc(name)
encodings[NAME_MAP.get(base_name, base_name)] = (fields, enc_bits)
# Extract instruction opcodes and operand info
# Track which encodings each opcode appears in (for detecting LIT-only ops)
opcode_encs: dict[str, dict[int, set[str]]] = {} # {base_fmt: {opcode: {enc_names}}}
for instr in root.findall("ISA/Instructions/Instruction"):
name = instr.findtext("InstructionName")
for enc in instr.findall("InstructionEncodings/InstructionEncoding"):
if enc.findtext("EncodingCondition") != "default": continue
base, opcode = _map_flat(_strip_enc(enc.findtext("EncodingName")), name), int(enc.findtext("Opcode") or 0)
enc_name = NAME_MAP.get(base, base)
# Encoding variants use the same Op enum as the base format
base_enum = enc_name
for sfx in ("_SDWA_SDST", "_DPP16", "_DPP8", "_SDWA", "_LIT", "_MFMA"):
base_enum = base_enum.replace(sfx, "")
# Track which encodings this opcode appears in
opcode_encs.setdefault(base_enum, {}).setdefault(opcode, set()).add(enc_name)
# ADDTID instructions go in both FLAT and GLOBAL enums (pcode uses FLATOp for these)
if "ADDTID" in name:
if base == "GLOBAL": enums.setdefault("FLAT", {})[opcode] = name
elif base == "VGLOBAL": enums.setdefault("VFLAT", {})[opcode] = name
enums.setdefault(base_enum, {})[opcode] = name
# Extract operand info
op_info = {op.findtext("FieldName").lower(): (op.findtext("DataFormatName"), int(op.findtext("OperandSize") or 0), op.findtext("OperandType"))
for op in enc.findall("Operands/Operand") if op.findtext("FieldName")}
for fmt, _, otype in op_info.values():
if fmt and fmt not in fmts: fmts[fmt] = 0
if otype: op_types_set.add(otype)
if op_info: types[(name, base_enum)] = op_info
# Find opcodes that only exist in _LIT encoding (no base format version)
lit_only_ops: dict[str, set[int]] = {}
for base_fmt, opcodes in opcode_encs.items():
for opcode, encs in opcodes.items():
if all("_LIT" in e for e in encs):
lit_only_ops.setdefault(base_fmt, set()).add(opcode)
return encodings, enums, types, fmts, op_types_set, lit_only_ops
# ═══════════════════════════════════════════════════════════════════════════════
# PDF parsing
# ═══════════════════════════════════════════════════════════════════════════════
def extract_pdf_text(url: str) -> list[list[tuple[float, float, str, str]]]:
"""Extract positioned text from PDF. Returns list of text elements (x, y, text, font) per page."""
data = fetch(url).read_bytes()
# Parse xref table to locate objects
xref: dict[int, int] = {}
pos = int(re.search(rb'startxref\s+(\d+)', data).group(1)) + 4
while data[pos:pos+7] != b'trailer':
while data[pos:pos+1] in b' \r\n': pos += 1
line_end = data.find(b'\n', pos)
start_obj, count = map(int, data[pos:line_end].split()[:2])
pos = line_end + 1
for i in range(count):
if data[pos+17:pos+18] == b'n' and (off := int(data[pos:pos+10])) > 0: xref[start_obj + i] = off
pos += 20
def get_stream(n: int) -> bytes:
obj = data[xref[n]:data.find(b'endobj', xref[n])]
raw = obj[obj.find(b'stream\n') + 7:obj.find(b'\nendstream')]
return zlib.decompress(raw) if b'/FlateDecode' in obj else raw
pages = []
for n in sorted(xref):
if b'/Type /Page' not in data[xref[n]:xref[n]+500]: continue
if not (m := re.search(rb'/Contents (\d+) 0 R', data[xref[n]:xref[n]+500])): continue
stream = get_stream(int(m.group(1))).decode('latin-1')
elements, font = [], ''
for bt in re.finditer(r'BT(.*?)ET', stream, re.S):
x, y = 0.0, 0.0
for m in re.finditer(r'(/F[\d.]+) [\d.]+ Tf|([\d.+-]+) ([\d.+-]+) Td|[\d.+-]+ [\d.+-]+ [\d.+-]+ [\d.+-]+ ([\d.+-]+) ([\d.+-]+) Tm|<([0-9A-Fa-f]+)>.*?Tj|\[([^\]]+)\] TJ', bt.group(1)):
if m.group(1): font = m.group(1)
elif m.group(2): x, y = x + float(m.group(2)), y + float(m.group(3))
elif m.group(4): x, y = float(m.group(4)), float(m.group(5))
elif m.group(6) and (t := bytes.fromhex(m.group(6)).decode('latin-1')).strip(): elements.append((x, y, t, font))
elif m.group(7) and (t := ''.join(bytes.fromhex(h).decode('latin-1') for h in re.findall(r'<([0-9A-Fa-f]+)>', m.group(7)))).strip(): elements.append((x, y, t, font))
pages.append(sorted(elements, key=lambda e: (-e[1], e[0])))
return pages
def extract_pcode(pages: list[list[tuple[float, float, str, str]]], name_to_op: dict[str, int]) -> dict[tuple[str, int], str]:
"""Extract pseudocode for instructions. Returns {(name, opcode): pseudocode}."""
# First pass: find all instruction headers across all pages
all_instructions: list[tuple[int, float, str, int]] = [] # (page_idx, y, name, opcode)
for page_idx, page in enumerate(pages):
by_y: dict[int, list[tuple[float, str]]] = {}
for x, y, t, _ in page:
by_y.setdefault(round(y), []).append((x, t))
for y, items in sorted(by_y.items(), reverse=True):
left = [(x, t) for x, t in items if 55 < x < 65]
right = [(x, t) for x, t in items if 535 < x < 550]
if left and right and left[0][1] in name_to_op and right[0][1].isdigit():
all_instructions.append((page_idx, y, left[0][1], int(right[0][1])))
# Second pass: extract pseudocode between consecutive instructions
pcode: dict[tuple[str, int], str] = {}
for i, (page_idx, y, name, opcode) in enumerate(all_instructions):
if i + 1 < len(all_instructions):
next_page, next_y = all_instructions[i + 1][0], all_instructions[i + 1][1]
else:
next_page, next_y = page_idx, 0
# Collect F6 text from current position to next instruction (pseudocode is at x ≈ 69)
lines = []
for p in range(page_idx, next_page + 1):
start_y = y if p == page_idx else 800
end_y = next_y if p == next_page else 0
lines.extend((p, y2, t) for x, y2, t, f in pages[p] if f in ('/F6.0', '/F7.0') and end_y < y2 < start_y and 60 < x < 80)
if lines:
sorted_lines = sorted(lines, key=lambda x: (x[0], -x[1]))
# Stop at large Y gaps (>30) - indicates section break
filtered = [sorted_lines[0]]
for j in range(1, len(sorted_lines)):
prev_page, prev_y, _ = sorted_lines[j-1]
curr_page, curr_y, _ = sorted_lines[j]
if curr_page == prev_page and prev_y - curr_y > 30: break
if curr_page != prev_page and prev_y > 60 and curr_y < 730: break
filtered.append(sorted_lines[j])
pcode_lines = [t.replace('Ê', '').strip() for _, _, t in filtered]
if pcode_lines: pcode[(name, opcode)] = '\n'.join(pcode_lines)
return pcode
# ═══════════════════════════════════════════════════════════════════════════════
# Code generation
# ═══════════════════════════════════════════════════════════════════════════════
def write_common(all_fmts, all_op_types, path):
lines = ["# autogenerated from AMD ISA XML - do not edit", "from enum import Enum, auto", ""]
lines.append("class Fmt(Enum):")
for fmt in sorted(all_fmts.keys()): lines.append(f" {fmt} = auto()")
lines.append("")
lines.append("FMT_BITS = {")
for fmt, bits in sorted(all_fmts.items()): lines.append(f" Fmt.{fmt}: {bits},")
lines.append("}")
lines.append("")
lines.append("class OpType(Enum):")
for ot in sorted(all_op_types): lines.append(f" {ot} = auto()")
with open(path, "w") as f: f.write("\n".join(lines))
def write_enum(enums, path):
lines = ["# autogenerated from AMD ISA XML - do not edit", "from enum import Enum", "from extra.assembly.amd.autogen.common import Fmt, FMT_BITS, OpType # noqa: F401", ""]
for name, ops in sorted(enums.items()):
if not ops: continue
suffix = "_E32" if name in ("VOP1", "VOP2", "VOPC") else "_E64" if name == "VOP3" else ""
lines.append(f"class {name}(Enum):" if name in ("HWREG", "MSG") else f"class {name}Op(Enum):")
aliases = []
for op, mem in sorted(ops.items()):
msuf = suffix if name != "VOP3" or op < 512 else ""
lines.append(f" {mem}{msuf} = {op}")
if msuf: aliases.append((mem, msuf))
for mem, msuf in aliases: lines.append(f" {mem} = {mem}{msuf}")
lines.append("")
with open(path, "w") as f: f.write("\n".join(lines))
def write_ins(encodings, enums, lit_only_ops, types, arch, path):
_VGPR_FIELDS = {"vdst", "vdstx", "vsrc0", "vsrc1", "vsrc2", "vsrc3", "vsrcx1", "vsrcy1", "vaddr", "vdata", "data", "data0", "data1", "addr"}
_VARIANT_SUFFIXES = ("_LIT", "_DPP16", "_DPP8", "_SDWA_SDST", "_SDWA", "_MFMA")
def get_base_fmt(fmt):
for sfx in _VARIANT_SUFFIXES: fmt = fmt.replace(sfx, "")
return fmt
def field_def(name, hi, lo, fmt, enc_bits=None):
bits = hi - lo + 1
base_fmt = get_base_fmt(fmt)
if name == "encoding" and enc_bits: return f"FixedBitField({hi}, {lo}, 0b{enc_bits})"
if name == "op" and fmt not in ("DPP", "SDWA"): return f"EnumBitField({hi}, {lo}, {base_fmt}Op)"
if name in ("opx", "opy"): return f"EnumBitField({hi}, {lo}, VOPDOp)"
if name == "vdsty": return f"VDSTYField({hi}, {lo})"
if name in _VGPR_FIELDS and bits == 8: return f"VGPRField({hi}, {lo})"
if name == "sbase" and bits == 6: return f"SBaseField({hi}, {lo})"
if name in ("srsrc", "ssamp") and bits == 5: return f"SRsrcField({hi}, {lo})"
if name in ("sdst", "sdata") and bits == 7: return f"SGPRField({hi}, {lo})"
if name in ("soffset", "saddr") and bits == 7: return f"SGPRField({hi}, {lo}, default=NULL)"
if name.startswith("ssrc") and bits == 8: return f"SSrcField({hi}, {lo})"
if name in ("saddr", "soffset") and bits == 8: return f"SSrcField({hi}, {lo}, default=NULL)"
if name.startswith("src") and bits == 9: return f"SrcField({hi}, {lo})"
if base_fmt == "VOP3P" and name == "opsel_hi": return f"BitField({hi}, {lo}, default=3)"
if base_fmt == "VOP3P" and name == "opsel_hi2": return f"BitField({hi}, {lo}, default=1)"
return f"BitField({hi}, {lo})"
ORDER = ['encoding', 'op', 'opx', 'opy', 'vdst', 'vdstx', 'vdsty', 'sdst', 'vdata', 'sdata', 'addr', 'vaddr', 'data', 'data0', 'data1',
'src0', 'srcx0', 'srcy0', 'vsrc0', 'ssrc0', 'src1', 'vsrc1', 'vsrcx1', 'vsrcy1', 'ssrc1', 'src2', 'vsrc2', 'src3', 'vsrc3',
'saddr', 'sbase', 'srsrc', 'ssamp', 'soffset', 'offset', 'simm16', 'literal', 'en', 'target', 'attr', 'attr_chan',
'omod', 'neg', 'neg_hi', 'abs', 'clmp', 'opsel', 'opsel_hi', 'waitexp', 'wait_va',
'dmask', 'dim', 'seg', 'format', 'offen', 'idxen', 'glc', 'dlc', 'slc', 'tfe', 'unrm', 'done', 'row',
'dpp', 'fi', 'bc', 'row_mask', 'bank_mask', 'src0_neg', 'src0_abs', 'src1_neg', 'src1_abs',
'cbsz', 'abid', 'acc_cd', 'acc', 'blgp', 'lane_sel_0', 'lane_sel_1', 'lane_sel_2', 'lane_sel_3',
'lane_sel_4', 'lane_sel_5', 'lane_sel_6', 'lane_sel_7', 'dst_sel', 'dst_unused', 'src0_sel', 'src1_sel']
sort_fields = lambda fields: sorted(fields, key=lambda f: (ORDER.index(f[0]) if f[0] in ORDER else 999, f[2]))
# Separate base encodings from variants
base_encodings, variant_encodings = {}, {}
for enc_name, data in encodings.items():
base = get_base_fmt(enc_name)
if base == enc_name: base_encodings[enc_name] = data
else: variant_encodings[enc_name] = data
# Build sets of ops by their vdst type from operand metadata
sdst_opcodes = {} # ops where vdst is OPR_SREG (writes to SGPR)
for fmt, ops in enums.items():
for op, name in ops.items():
op_types = types.get((name, fmt), {})
vdst_type = op_types.get("vdst", (None, None, None))[2]
if vdst_type == "OPR_SREG": sdst_opcodes.setdefault(fmt, set()).add(op)
lines = ["# autogenerated from AMD ISA XML - do not edit", "# ruff: noqa: F401,F403",
"from extra.assembly.amd.dsl import *", f"from extra.assembly.amd.autogen.{arch}.enum import *", "import functools", ""]
def fmt_allowed(op_enum: str, ops: set[int]) -> str:
"""Format allowed ops as {EnumName.MEMBER, ...}."""
names = [f"{op_enum}.{enums[op_enum.removesuffix('Op')][op]}" for op in sorted(ops)]
return "{" + ", ".join(names) + "}"
# Generate base classes first
for enc_name, (fields, enc_bits) in sorted(base_encodings.items()):
# Get lit-only ops for this format (these can't be used in base class)
base_lit_ops = lit_only_ops.get(enc_name, set())
all_ops = set(enums.get(enc_name, {}).keys())
# Exclude SDST ops from base class (they need VOP1_SDST/VOP3_SDST/VOP3B)
base_allowed = all_ops - base_lit_ops - sdst_opcodes.get(enc_name, set())
if enc_name in ("FLAT", "VFLAT"):
prefix = "V" if enc_name == "VFLAT" else ""
for cls, seg, op_enum in [(f"{prefix}FLAT", 0, f"{prefix}FLATOp"), (f"{prefix}GLOBAL", 2, f"{prefix}GLOBALOp"), (f"{prefix}SCRATCH", 1, f"{prefix}SCRATCHOp")]:
cls_ops = set(enums.get(cls, {}).keys())
lines.append(f"class {cls}(Inst):")
for fn, hi, lo in sort_fields(fields):
if fn == "seg": lines.append(f" seg = FixedBitField({hi}, {lo}, {seg})")
elif fn == "op": lines.append(f" op = EnumBitField({hi}, {lo}, {op_enum}, {fmt_allowed(op_enum, cls_ops)})")
else: lines.append(f" {fn} = {field_def(fn, hi, lo, cls, enc_bits)}")
lines.append("")
elif enc_name not in ("FLAT_GLOBAL", "FLAT_SCRATCH", "FLAT_GLBL", "VGLOBAL", "VSCRATCH", "DPP", "SDWA"):
lines.append(f"class {enc_name}(Inst):")
for fn, hi, lo in sort_fields(fields):
if fn == "op":
base_fmt = get_base_fmt(enc_name)
lines.append(f" op = EnumBitField({hi}, {lo}, {base_fmt}Op, {fmt_allowed(f'{base_fmt}Op', base_allowed)})")
else:
lines.append(f" {fn} = {field_def(fn, hi, lo, enc_name, enc_bits if fn == 'encoding' else None)}")
lines.append("")
# Generate variant classes that inherit from base (only add extra fields)
for enc_name, (fields, enc_bits) in sorted(variant_encodings.items()):
base = get_base_fmt(enc_name)
if base not in base_encodings: continue # skip if no base class
base_fields = {f[0] for f in base_encodings[base][0]}
extra_fields = [(fn, hi, lo) for fn, hi, lo in fields if fn not in base_fields]
is_lit = enc_name.endswith("_LIT")
all_ops = set(enums.get(base, {}).keys())
if extra_fields or is_lit:
lines.append(f"class {enc_name}({base}):")
op_field = next((f for f in base_encodings[base][0] if f[0] == "op"), None)
# _LIT classes: override op to allow all opcodes (base excludes lit-only ops)
if op_field and is_lit:
_, hi, lo = op_field
lines.append(f" op = EnumBitField({hi}, {lo}, {base}Op, {fmt_allowed(f'{base}Op', all_ops)})")
for fn, hi, lo in sort_fields(extra_fields):
lines.append(f" {fn} = {field_def(fn, hi, lo, enc_name)}")
lines.append("")
# SDST variants (special case - redefine vdst field type, restrict to SDST ops)
for base, field_hi, field_lo in [("VOP1", 24, 17), ("VOP3", 7, 0)]:
if base not in base_encodings: continue
sdst_ops = sdst_opcodes.get(base, set())
if not sdst_ops: continue
# For VOP3, all ops < 256 (compare/cmpx ops) use SDST encoding
all_base_ops = set(enums.get(base, {}).keys())
if base == "VOP3": sdst_ops = sdst_ops | {op for op in all_base_ops if op < 256}
op_field = next((f for f in base_encodings[base][0] if f[0] == "op"), None)
lines.append(f"class {base}_SDST({base}):")
if op_field:
_, hi, lo = op_field
lines.append(f" op = EnumBitField({hi}, {lo}, {base}Op, {fmt_allowed(f'{base}Op', sdst_ops)})")
lines.append(f" vdst = SSrcField({field_hi}, {field_lo})")
lines.append("")
# SDST_LIT class (for literals with SDST destination) - same ops, just adds literal field
lit_enc = variant_encodings.get(f"{base}_LIT")
if lit_enc:
lit_field = next((f for f in lit_enc[0] if f[0] == "literal"), None)
if lit_field:
lines.append(f"class {base}_SDST_LIT({base}_SDST):")
lines.append(f" literal = BitField({lit_field[1]}, {lit_field[2]})")
lines.append("")
# Instruction helpers
lines.append("# instruction helpers")
for fmt, ops in sorted(enums.items()):
if fmt not in base_encodings and fmt not in ("GLOBAL", "SCRATCH", "VGLOBAL", "VSCRATCH"): continue
suffix = "_E32" if fmt in ("VOP1", "VOP2", "VOPC") else "_E64" if fmt == "VOP3" else ""
lit_ops = lit_only_ops.get(fmt, set())
fmt_sdst_ops = sdst_opcodes.get(fmt, set())
for op, name in sorted(ops.items()):
msuf = suffix if fmt != "VOP3" or op < 512 else ""
# Determine class: SDST variants, LIT-only instructions, or base
if fmt == "VOP1" and op in fmt_sdst_ops: cls = "VOP1_SDST"
elif fmt == "VOP3" and (op in fmt_sdst_ops or op < 256): cls = "VOP3_SDST"
elif op in lit_ops: cls = f"{fmt}_LIT"
else: cls = fmt
lines.append(f"{name.lower()}{msuf.lower()} = functools.partial({cls}, {fmt}Op.{name}{msuf})")
with open(path, "w") as f: f.write("\n".join(lines))
def write_operands(types, enums, arch, path):
valid = {(name, fmt) for fmt, ops in enums.items() for name in ops.values()}
lines = ["# autogenerated from AMD ISA XML - do not edit",
"from extra.assembly.amd.autogen.common import Fmt, OpType",
f"from extra.assembly.amd.autogen.{arch}.enum import *", ""]
lines.append("# instruction operand info: {Op: {field: (Fmt, size_bits, OpType)}}")
lines.append("OPERANDS = {")
def fmt_val(v):
fmt, size, otype = v
return f"({f'Fmt.{fmt}' if fmt else 'None'}, {size}, {f'OpType.{otype}' if otype else 'None'})"
for (name, enc_base), fields in sorted(types.items()):
if (name, enc_base) not in valid: continue
fstr = ", ".join(f'"{k}": {fmt_val(v)}' for k, v in sorted(fields.items()))
lines.append(f' {enc_base}Op.{name}: {{{fstr}}},')
lines.append("}")
with open(path, "w") as f: f.write("\n".join(lines))
def write_pcode(pcode: dict[tuple[str, int], str], enums: dict[str, dict[int, str]], arch: str, path: str):
"""Write str_pcode.py file from extracted pseudocode."""
entries: list[tuple[str, str, int, str]] = []
for fmt_name, ops in enums.items():
member_suffix = "_E32" if fmt_name in ("VOP1", "VOP2", "VOPC") else "_E64" if fmt_name == "VOP3" else ""
for opcode, name in ops.items():
if (name, opcode) in pcode:
msuf = member_suffix if fmt_name != "VOP3" or opcode < 512 else ""
entries.append((f"{fmt_name}Op", f"{name}{msuf}", opcode, pcode[(name, opcode)]))
enum_names = sorted(set(e[0] for e in entries))
lines = ["# autogenerated from AMD ISA PDF - do not edit", "# ruff: noqa: E501",
f"from extra.assembly.amd.autogen.{arch}.enum import {', '.join(enum_names)}", "", "PCODE = {"]
for enum_name, name, opcode, code in sorted(entries, key=lambda x: (x[0], x[2])):
lines.append(f" {enum_name}.{name}: {code!r},")
lines.append("}")
with open(path, "w") as f: f.write("\n".join(lines))
# ═══════════════════════════════════════════════════════════════════════════════
# Main
# ═══════════════════════════════════════════════════════════════════════════════
if __name__ == "__main__":
import pathlib
all_fmts, all_op_types, arch_data = {}, set(), {}
# First pass: parse XML for all architectures
for arch, cfg in ARCHS.items():
print(f"Parsing XML: {cfg['xml']} -> {arch}")
encodings, enums, types, fmts, op_types_set, lit_only_ops = parse_xml(cfg["xml"])
for fmt, ops in FIXES.get(arch, {}).items(): enums.setdefault(fmt, {}).update(ops)
arch_data[arch] = {"encodings": encodings, "enums": enums, "types": types, "lit_only_ops": lit_only_ops}
for fmt, bits in fmts.items():
assert fmt not in all_fmts or all_fmts[fmt] == bits, f"FMT_BITS mismatch for {fmt}: {all_fmts[fmt]} vs {bits}"
all_fmts[fmt] = bits
all_op_types.update(op_types_set)
# Write common.py
common_path = pathlib.Path(__file__).parent / "autogen" / "common.py"
write_common(all_fmts, all_op_types, common_path)
print(f"Wrote common.py: {len(all_fmts)} formats, {len(all_op_types)} op types")
# Write per-arch files from XML
for arch, data in arch_data.items():
base = pathlib.Path(__file__).parent / "autogen" / arch
write_enum(data["enums"], base / "enum.py")
write_ins(data["encodings"], data["enums"], data["lit_only_ops"], data["types"], arch, base / "ins.py")
write_operands(data["types"], data["enums"], arch, base / "operands.py")
print(f" {arch}: {len(data['encodings'])} encodings, {sum(len(v) for v in data['enums'].values())} instructions")
# Second pass: parse PDFs and write pcode
for arch, cfg in ARCHS.items():
print(f"Parsing PDF: {arch}...")
pages = extract_pdf_text(cfg["pdf"])
name_to_op = {name: op for ops in arch_data[arch]["enums"].values() for op, name in ops.items()}
pcode = extract_pcode(pages, name_to_op)
base = pathlib.Path(__file__).parent / "autogen" / arch
write_pcode(pcode, arch_data[arch]["enums"], arch, base / "str_pcode.py")
print(f" {arch}: {len(pcode)} pcode entries")
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-320
View File
@@ -1,320 +0,0 @@
# autogenerated from AMD ISA XML - do not edit
from enum import Enum, auto
class Fmt(Enum):
FMT_ANY = auto()
FMT_BUF = auto()
FMT_IMG = auto()
FMT_IMG_BVH = auto()
FMT_NUM_B1 = auto()
FMT_NUM_B1024 = auto()
FMT_NUM_B128 = auto()
FMT_NUM_B16 = auto()
FMT_NUM_B256 = auto()
FMT_NUM_B32 = auto()
FMT_NUM_B512 = auto()
FMT_NUM_B64 = auto()
FMT_NUM_B8 = auto()
FMT_NUM_B96 = auto()
FMT_NUM_BF16 = auto()
FMT_NUM_BF6 = auto()
FMT_NUM_BF8 = auto()
FMT_NUM_F16 = auto()
FMT_NUM_F32 = auto()
FMT_NUM_F64 = auto()
FMT_NUM_FP4 = auto()
FMT_NUM_FP6 = auto()
FMT_NUM_FP8 = auto()
FMT_NUM_I16 = auto()
FMT_NUM_I24 = auto()
FMT_NUM_I32 = auto()
FMT_NUM_I4 = auto()
FMT_NUM_I64 = auto()
FMT_NUM_I8 = auto()
FMT_NUM_IU4 = auto()
FMT_NUM_IU8 = auto()
FMT_NUM_M64 = auto()
FMT_NUM_PK16_BF16 = auto()
FMT_NUM_PK16_BF8 = auto()
FMT_NUM_PK16_F16 = auto()
FMT_NUM_PK16_F32 = auto()
FMT_NUM_PK16_FP8 = auto()
FMT_NUM_PK16_I32 = auto()
FMT_NUM_PK16_I8 = auto()
FMT_NUM_PK2_B16 = auto()
FMT_NUM_PK2_B32 = auto()
FMT_NUM_PK2_B64 = auto()
FMT_NUM_PK2_BF16 = auto()
FMT_NUM_PK2_BF8 = auto()
FMT_NUM_PK2_F16 = auto()
FMT_NUM_PK2_F32 = auto()
FMT_NUM_PK2_FP4 = auto()
FMT_NUM_PK2_FP8 = auto()
FMT_NUM_PK2_I16 = auto()
FMT_NUM_PK2_I8 = auto()
FMT_NUM_PK2_U16 = auto()
FMT_NUM_PK2_U8 = auto()
FMT_NUM_PK32_BF16 = auto()
FMT_NUM_PK32_BF6 = auto()
FMT_NUM_PK32_BF8 = auto()
FMT_NUM_PK32_F16 = auto()
FMT_NUM_PK32_F32 = auto()
FMT_NUM_PK32_FP6 = auto()
FMT_NUM_PK32_FP8 = auto()
FMT_NUM_PK32_I32 = auto()
FMT_NUM_PK32_I8 = auto()
FMT_NUM_PK4_B8 = auto()
FMT_NUM_PK4_BF16 = auto()
FMT_NUM_PK4_BF8 = auto()
FMT_NUM_PK4_F16 = auto()
FMT_NUM_PK4_F32 = auto()
FMT_NUM_PK4_F64 = auto()
FMT_NUM_PK4_FP8 = auto()
FMT_NUM_PK4_I32 = auto()
FMT_NUM_PK4_I8 = auto()
FMT_NUM_PK4_IU8 = auto()
FMT_NUM_PK4_U8 = auto()
FMT_NUM_PK8_B32 = auto()
FMT_NUM_PK8_BF16 = auto()
FMT_NUM_PK8_BF8 = auto()
FMT_NUM_PK8_F16 = auto()
FMT_NUM_PK8_FP8 = auto()
FMT_NUM_PK8_I4 = auto()
FMT_NUM_PK8_I8 = auto()
FMT_NUM_PK8_IU4 = auto()
FMT_NUM_PK8_U4 = auto()
FMT_NUM_PK8_U8 = auto()
FMT_NUM_PK_F16 = auto()
FMT_NUM_PK_I16 = auto()
FMT_NUM_PK_I8 = auto()
FMT_NUM_PK_U16 = auto()
FMT_NUM_PK_U8 = auto()
FMT_NUM_U16 = auto()
FMT_NUM_U24 = auto()
FMT_NUM_U32 = auto()
FMT_NUM_U4 = auto()
FMT_NUM_U64 = auto()
FMT_NUM_U8 = auto()
FMT_RSRC = auto()
FMT_RSRC_SCALAR = auto()
FMT_RSRC_SCRATCH = auto()
FMT_RSRC_SCRATCH_BYTE = auto()
FMT_RSRC_SCRATCH_STRIDE = auto()
FMT_RSRC_TYPED = auto()
FMT_RSRC_TYPED_BYTE = auto()
FMT_RSRC_TYPED_SCRATCH = auto()
FMT_RSRC_TYPED_STRIDE = auto()
FMT_RSRC_VECTOR = auto()
FMT_RSRC_VECTOR_BYTE = auto()
FMT_RSRC_VECTOR_STRIDE = auto()
FMT_SAMP = auto()
FMT_WMMA_AB_16X16_BF16 = auto()
FMT_WMMA_AB_16X16_BF8 = auto()
FMT_WMMA_AB_16X16_F16 = auto()
FMT_WMMA_AB_16X16_FP8 = auto()
FMT_WMMA_AB_16X16_IU4 = auto()
FMT_WMMA_AB_16X16_IU8 = auto()
FMT_WMMA_AB_16X32_BF16 = auto()
FMT_WMMA_AB_16X32_BF8 = auto()
FMT_WMMA_AB_16X32_F16 = auto()
FMT_WMMA_AB_16X32_FP8 = auto()
FMT_WMMA_AB_16X32_IU4 = auto()
FMT_WMMA_AB_16X32_IU8 = auto()
FMT_WMMA_AB_16X64_IU4 = auto()
FMT_WMMA_AB_BF16 = auto()
FMT_WMMA_AB_F16 = auto()
FMT_WMMA_AB_IU4 = auto()
FMT_WMMA_AB_IU8 = auto()
FMT_WMMA_DC_16X16_BF16 = auto()
FMT_WMMA_DC_16X16_F16 = auto()
FMT_WMMA_DC_16X16_F32 = auto()
FMT_WMMA_DC_16X16_I32 = auto()
FMT_WMMA_DC_BF16 = auto()
FMT_WMMA_DC_F16 = auto()
FMT_WMMA_DC_F32 = auto()
FMT_WMMA_DC_I32 = auto()
FMT_WMMA_INDEX_SET = auto()
FMT_BITS = {
Fmt.FMT_ANY: 1,
Fmt.FMT_BUF: 64,
Fmt.FMT_IMG: 256,
Fmt.FMT_IMG_BVH: 128,
Fmt.FMT_NUM_B1: 1,
Fmt.FMT_NUM_B1024: 1024,
Fmt.FMT_NUM_B128: 128,
Fmt.FMT_NUM_B16: 16,
Fmt.FMT_NUM_B256: 256,
Fmt.FMT_NUM_B32: 32,
Fmt.FMT_NUM_B512: 512,
Fmt.FMT_NUM_B64: 64,
Fmt.FMT_NUM_B8: 8,
Fmt.FMT_NUM_B96: 96,
Fmt.FMT_NUM_BF16: 16,
Fmt.FMT_NUM_BF6: 6,
Fmt.FMT_NUM_BF8: 8,
Fmt.FMT_NUM_F16: 16,
Fmt.FMT_NUM_F32: 32,
Fmt.FMT_NUM_F64: 64,
Fmt.FMT_NUM_FP4: 4,
Fmt.FMT_NUM_FP6: 6,
Fmt.FMT_NUM_FP8: 8,
Fmt.FMT_NUM_I16: 16,
Fmt.FMT_NUM_I24: 24,
Fmt.FMT_NUM_I32: 32,
Fmt.FMT_NUM_I4: 4,
Fmt.FMT_NUM_I64: 64,
Fmt.FMT_NUM_I8: 8,
Fmt.FMT_NUM_IU4: 4,
Fmt.FMT_NUM_IU8: 8,
Fmt.FMT_NUM_M64: 64,
Fmt.FMT_NUM_PK16_BF16: 256,
Fmt.FMT_NUM_PK16_BF8: 128,
Fmt.FMT_NUM_PK16_F16: 256,
Fmt.FMT_NUM_PK16_F32: 512,
Fmt.FMT_NUM_PK16_FP8: 128,
Fmt.FMT_NUM_PK16_I32: 512,
Fmt.FMT_NUM_PK16_I8: 128,
Fmt.FMT_NUM_PK2_B16: 32,
Fmt.FMT_NUM_PK2_B32: 64,
Fmt.FMT_NUM_PK2_B64: 128,
Fmt.FMT_NUM_PK2_BF16: 32,
Fmt.FMT_NUM_PK2_BF8: 16,
Fmt.FMT_NUM_PK2_F16: 32,
Fmt.FMT_NUM_PK2_F32: 64,
Fmt.FMT_NUM_PK2_FP4: 8,
Fmt.FMT_NUM_PK2_FP8: 16,
Fmt.FMT_NUM_PK2_I16: 32,
Fmt.FMT_NUM_PK2_I8: 16,
Fmt.FMT_NUM_PK2_U16: 32,
Fmt.FMT_NUM_PK2_U8: 16,
Fmt.FMT_NUM_PK32_BF16: 512,
Fmt.FMT_NUM_PK32_BF6: 192,
Fmt.FMT_NUM_PK32_BF8: 256,
Fmt.FMT_NUM_PK32_F16: 512,
Fmt.FMT_NUM_PK32_F32: 1024,
Fmt.FMT_NUM_PK32_FP6: 192,
Fmt.FMT_NUM_PK32_FP8: 256,
Fmt.FMT_NUM_PK32_I32: 1024,
Fmt.FMT_NUM_PK32_I8: 256,
Fmt.FMT_NUM_PK4_B8: 32,
Fmt.FMT_NUM_PK4_BF16: 64,
Fmt.FMT_NUM_PK4_BF8: 32,
Fmt.FMT_NUM_PK4_F16: 64,
Fmt.FMT_NUM_PK4_F32: 128,
Fmt.FMT_NUM_PK4_F64: 256,
Fmt.FMT_NUM_PK4_FP8: 32,
Fmt.FMT_NUM_PK4_I32: 128,
Fmt.FMT_NUM_PK4_I8: 32,
Fmt.FMT_NUM_PK4_IU8: 32,
Fmt.FMT_NUM_PK4_U8: 32,
Fmt.FMT_NUM_PK8_B32: 256,
Fmt.FMT_NUM_PK8_BF16: 128,
Fmt.FMT_NUM_PK8_BF8: 64,
Fmt.FMT_NUM_PK8_F16: 128,
Fmt.FMT_NUM_PK8_FP8: 64,
Fmt.FMT_NUM_PK8_I4: 32,
Fmt.FMT_NUM_PK8_I8: 64,
Fmt.FMT_NUM_PK8_IU4: 32,
Fmt.FMT_NUM_PK8_U4: 32,
Fmt.FMT_NUM_PK8_U8: 64,
Fmt.FMT_NUM_PK_F16: 32,
Fmt.FMT_NUM_PK_I16: 32,
Fmt.FMT_NUM_PK_I8: 32,
Fmt.FMT_NUM_PK_U16: 32,
Fmt.FMT_NUM_PK_U8: 32,
Fmt.FMT_NUM_U16: 16,
Fmt.FMT_NUM_U24: 24,
Fmt.FMT_NUM_U32: 32,
Fmt.FMT_NUM_U4: 4,
Fmt.FMT_NUM_U64: 64,
Fmt.FMT_NUM_U8: 8,
Fmt.FMT_RSRC: 128,
Fmt.FMT_RSRC_SCALAR: 128,
Fmt.FMT_RSRC_SCRATCH: 128,
Fmt.FMT_RSRC_SCRATCH_BYTE: 128,
Fmt.FMT_RSRC_SCRATCH_STRIDE: 128,
Fmt.FMT_RSRC_TYPED: 128,
Fmt.FMT_RSRC_TYPED_BYTE: 128,
Fmt.FMT_RSRC_TYPED_SCRATCH: 128,
Fmt.FMT_RSRC_TYPED_STRIDE: 128,
Fmt.FMT_RSRC_VECTOR: 128,
Fmt.FMT_RSRC_VECTOR_BYTE: 128,
Fmt.FMT_RSRC_VECTOR_STRIDE: 128,
Fmt.FMT_SAMP: 128,
Fmt.FMT_WMMA_AB_16X16_BF16: 128,
Fmt.FMT_WMMA_AB_16X16_BF8: 64,
Fmt.FMT_WMMA_AB_16X16_F16: 128,
Fmt.FMT_WMMA_AB_16X16_FP8: 64,
Fmt.FMT_WMMA_AB_16X16_IU4: 32,
Fmt.FMT_WMMA_AB_16X16_IU8: 64,
Fmt.FMT_WMMA_AB_16X32_BF16: 256,
Fmt.FMT_WMMA_AB_16X32_BF8: 128,
Fmt.FMT_WMMA_AB_16X32_F16: 256,
Fmt.FMT_WMMA_AB_16X32_FP8: 128,
Fmt.FMT_WMMA_AB_16X32_IU4: 64,
Fmt.FMT_WMMA_AB_16X32_IU8: 128,
Fmt.FMT_WMMA_AB_16X64_IU4: 128,
Fmt.FMT_WMMA_AB_BF16: 256,
Fmt.FMT_WMMA_AB_F16: 256,
Fmt.FMT_WMMA_AB_IU4: 64,
Fmt.FMT_WMMA_AB_IU8: 128,
Fmt.FMT_WMMA_DC_16X16_BF16: 128,
Fmt.FMT_WMMA_DC_16X16_F16: 128,
Fmt.FMT_WMMA_DC_16X16_F32: 256,
Fmt.FMT_WMMA_DC_16X16_I32: 256,
Fmt.FMT_WMMA_DC_BF16: 256,
Fmt.FMT_WMMA_DC_F16: 256,
Fmt.FMT_WMMA_DC_F32: 256,
Fmt.FMT_WMMA_DC_I32: 256,
Fmt.FMT_WMMA_INDEX_SET: 32,
}
class OpType(Enum):
OPR_ACCVGPR = auto()
OPR_ATTR = auto()
OPR_CLAUSE = auto()
OPR_DELAY = auto()
OPR_EXEC = auto()
OPR_HWREG = auto()
OPR_LABEL = auto()
OPR_SDST = auto()
OPR_SDST_NULL = auto()
OPR_SENDMSG = auto()
OPR_SENDMSG_RTN = auto()
OPR_SIMM16 = auto()
OPR_SIMM24 = auto()
OPR_SIMM4 = auto()
OPR_SIMM5 = auto()
OPR_SIMM8 = auto()
OPR_SLEEP = auto()
OPR_SMEM_OFFSET = auto()
OPR_SMEM_OFFSET_NOK = auto()
OPR_SRC = auto()
OPR_SRC_ACCVGPR = auto()
OPR_SRC_NOLDS = auto()
OPR_SRC_NOLIT = auto()
OPR_SRC_SIMPLE = auto()
OPR_SRC_VGPR = auto()
OPR_SRC_VGPR_OR_ACCVGPR = auto()
OPR_SRC_VGPR_OR_ACCVGPR_OR_CONST = auto()
OPR_SRC_VGPR_OR_INLINE = auto()
OPR_SREG = auto()
OPR_SREG_LITERAL = auto()
OPR_SREG_M0 = auto()
OPR_SREG_M0_INL = auto()
OPR_SREG_NOVCC = auto()
OPR_SSRC = auto()
OPR_SSRC_BARRIER_ID = auto()
OPR_SSRC_LANESEL = auto()
OPR_SSRC_NOLIT = auto()
OPR_TGT = auto()
OPR_VERSION = auto()
OPR_VGPR = auto()
OPR_VGPR_OR_ACCVGPR = auto()
OPR_VGPR_OR_LDS = auto()
OPR_WAITCNT = auto()
OPR_WAITCNT_DEPCTR = auto()
OPR_WAIT_ALU = auto()
OPR_WAIT_EVENT = auto()
OPR_WAIT_MEM_DS = auto()
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-65
View File
@@ -1,65 +0,0 @@
# Instruction format detection and decoding
from __future__ import annotations
from extra.assembly.amd.dsl import Inst, FixedBitField, EnumBitField
# SDWA/DPP variant detection: src0 field (bits 0-8) encodes the variant
# 0xf9 (249) = SDWA, 0xfa (250) = DPP16 for CDNA (GFX9)
_VARIANT_SRC0 = {"_SDWA_SDST": 0xf9, "_SDWA": 0xf9, "_DPP16": 0xfa}
def _matches(data: bytes, cls: type[Inst]) -> bool:
"""Check if data matches all FixedBitFields and op is in allowed."""
for _, field in cls._fields:
dword_idx = field.lo // 32
if len(data) < (dword_idx + 1) * 4: return False
word = int.from_bytes(data[dword_idx*4:(dword_idx+1)*4], 'little')
field_lo = field.lo % 32
if isinstance(field, FixedBitField):
if ((word >> field_lo) & field.mask) != field.default: return False
if isinstance(field, EnumBitField) and field.allowed is not None:
try: opcode = field.decode((word >> field_lo) & field.mask)
except ValueError: return False # opcode not in enum
if opcode not in field.allowed: return False
# Check SDWA/DPP variant based on src0 field (bits 0-8) - only for variant classes
name = cls.__name__
word = int.from_bytes(data[:4], 'little')
for suffix, expected_src0 in _VARIANT_SRC0.items():
if name.endswith(suffix): return (word & 0x1ff) == expected_src0
return True
# Import instruction classes for each architecture
from extra.assembly.amd.autogen.rdna3.ins import (VOP1, VOP1_SDST, VOP1_LIT, VOP2, VOP2_LIT, VOP3, VOP3_SDST, VOP3SD, VOP3P, VOPC, VOPD, VINTERP,
SOP1, SOP1_LIT, SOP2, SOP2_LIT, SOPC, SOPK, SOPK_LIT, SOPP, SMEM, DS, FLAT, GLOBAL, SCRATCH)
from extra.assembly.amd.autogen.rdna4.ins import (VOP1 as R4_VOP1, VOP1_SDST as R4_VOP1_SDST, VOP2 as R4_VOP2, VOP2_LIT as R4_VOP2_LIT,
VOP3 as R4_VOP3, VOP3_SDST as R4_VOP3_SDST, VOP3SD as R4_VOP3SD, VOP3P as R4_VOP3P,
VOPC as R4_VOPC, VOPD as R4_VOPD, VINTERP as R4_VINTERP, SOP1 as R4_SOP1, SOP2 as R4_SOP2, SOP2_LIT as R4_SOP2_LIT,
SOPC as R4_SOPC, SOPK as R4_SOPK, SOPK_LIT as R4_SOPK_LIT, SOPP as R4_SOPP,
SMEM as R4_SMEM, DS as R4_DS, VFLAT as R4_FLAT, VGLOBAL as R4_GLOBAL, VSCRATCH as R4_SCRATCH)
from extra.assembly.amd.autogen.cdna.ins import (VOP1 as C_VOP1, VOP1_SDWA as C_VOP1_SDWA, VOP1_DPP16 as C_VOP1_DPP16,
VOP2 as C_VOP2, VOP2_LIT as C_VOP2_LIT, VOP2_SDWA as C_VOP2_SDWA, VOP2_DPP16 as C_VOP2_DPP16,
VOPC as C_VOPC, VOPC_SDWA_SDST as C_VOPC_SDWA_SDST,
VOP3 as C_VOP3, VOP3_SDST as C_VOP3_SDST, VOP3SD as C_VOP3SD, VOP3P as C_VOP3P, VOP3PX2 as C_VOP3PX2,
SOP1 as C_SOP1, SOP2 as C_SOP2, SOPC as C_SOPC, SOPK as C_SOPK, SOPK_LIT as C_SOPK_LIT, SOPP as C_SOPP, SMEM as C_SMEM, DS as C_DS,
FLAT as C_FLAT, GLOBAL as C_GLOBAL, SCRATCH as C_SCRATCH, MUBUF as C_MUBUF)
# Order matters: more specific encodings first, catch-alls (SOP2, VOP2) last
# Order: base before _LIT (base matches regular ops, _LIT catches lit-only ops excluded from base)
_FORMATS = {
"rdna3": [VOPD, VOP3P, VINTERP, VOP3SD, VOP3_SDST, VOP3, DS, GLOBAL, SCRATCH, FLAT, SMEM,
SOP1, SOP1_LIT, SOP2, SOP2_LIT, SOPC, SOPK, SOPK_LIT, SOPP, VOPC, VOP1_SDST, VOP1, VOP1_LIT, VOP2, VOP2_LIT],
"rdna4": [R4_VOPD, R4_VOP3P, R4_VINTERP, R4_VOP3SD, R4_VOP3_SDST, R4_VOP3, R4_DS, R4_GLOBAL, R4_SCRATCH, R4_FLAT, R4_SMEM,
R4_SOP1, R4_SOPC, R4_SOPP, R4_SOPK, R4_SOPK_LIT, R4_VOPC, R4_VOP1_SDST, R4_VOP1, R4_SOP2, R4_SOP2_LIT, R4_VOP2, R4_VOP2_LIT],
"cdna": [C_VOP3PX2, C_VOP3P, C_VOP3SD, C_VOP3_SDST, C_VOP3, C_DS, C_GLOBAL, C_SCRATCH, C_FLAT, C_MUBUF, C_SMEM,
C_SOP1, C_SOPC, C_SOPP, C_SOPK, C_SOPK_LIT, C_VOPC_SDWA_SDST, C_VOPC,
C_VOP1_DPP16, C_VOP1_SDWA, C_VOP1, C_VOP2_DPP16, C_VOP2_SDWA, C_SOP2, C_VOP2, C_VOP2_LIT],
}
def detect_format(data: bytes, arch: str = "rdna3") -> type[Inst]:
"""Detect instruction format from machine code bytes."""
assert len(data) >= 4, f"need at least 4 bytes, got {len(data)}"
for cls in _FORMATS[arch]:
if _matches(data, cls): return cls
raise ValueError(f"unknown {arch} format word={int.from_bytes(data[:4], 'little'):#010x}")
def decode_inst(data: bytes, arch: str = "rdna3") -> Inst:
"""Decode machine code bytes into an instruction."""
return detect_format(data, arch).from_bytes(data)
-899
View File
@@ -1,899 +0,0 @@
# RDNA3/RDNA4/CDNA disassembler
from __future__ import annotations
import re, struct
from extra.assembly.amd.dsl import Inst, Reg
# Special register mappings for disassembly
SPECIAL_GPRS = {106: 'vcc_lo', 107: 'vcc_hi', 124: 'null', 125: 'm0', 126: 'exec_lo', 127: 'exec_hi',
128: '0', 240: '0.5', 241: '-0.5', 242: '1.0', 243: '-1.0', 244: '2.0', 245: '-2.0', 246: '4.0', 247: '-4.0', 248: '0x3e22f983', 253: 'scc'}
SPECIAL_GPRS_CDNA = {106: 'vcc_lo', 107: 'vcc_hi', 124: 'm0', 126: 'exec_lo', 127: 'exec_hi',
128: '0', 240: '0.5', 241: '-0.5', 242: '1.0', 243: '-1.0', 244: '2.0', 245: '-2.0', 246: '4.0', 247: '-4.0', 248: '0x3e22f983', 253: 'scc',
102: 'flat_scratch_lo', 103: 'flat_scratch_hi', 104: 'xnack_mask_lo', 105: 'xnack_mask_hi',
251: 'src_vccz', 252: 'src_execz'}
SPECIAL_PAIRS = {106: 'vcc', 126: 'exec'}
SPECIAL_PAIRS_CDNA = {106: 'vcc', 126: 'exec', 102: 'flat_scratch', 104: 'xnack_mask'}
def decode_src(v, cdna: bool = False) -> str:
"""Decode a source operand encoding to its string representation."""
v = _unwrap(v)
gprs = SPECIAL_GPRS_CDNA if cdna else SPECIAL_GPRS
if v in gprs: return gprs[v]
if v < 106: return f's{v}'
if 108 <= v < 124: return f'ttmp{v - 108}'
if 129 <= v <= 192: return str(v - 128) # positive integers 1-64
if 193 <= v <= 208: return str(-(v - 192)) # negative integers -1 to -16
if v >= 256: return f'v{v - 256}'
return f's{v}'
def _unwrap(v) -> int:
"""Unwrap Reg to int offset, or return int as-is."""
return v.offset if isinstance(v, Reg) else v
def _vi(v) -> int:
"""Get VGPR index from Reg or int (for v[N] fields that encode as 256+N)."""
off = _unwrap(v)
return off - 256 if off >= 256 else off
# ═══════════════════════════════════════════════════════════════════════════════
# LITERAL FORMATTING
# ═══════════════════════════════════════════════════════════════════════════════
_FLOAT_DEC = {240: 0.5, 241: -0.5, 242: 1.0, 243: -1.0, 244: 2.0, 245: -2.0, 246: 4.0, 247: -4.0}
def _lit(inst, v, neg=0, cdna=None) -> str:
"""Format literal/inline constant value."""
if cdna is None: cdna = _is_cdna(inst)
v = _unwrap(v)
if v == 255:
lit = inst._literal
if lit is None: return "0"
s = f"0x{lit:x}"
elif v in _FLOAT_DEC: s = str(_FLOAT_DEC[v])
elif 128 <= v <= 192: s = str(v - 128)
elif 193 <= v <= 208: s = str(-(v - 192))
elif v < 128: s = decode_src(v, cdna)
elif v >= 256: s = f"v{v - 256}"
else: s = decode_src(v, cdna)
return f"-{s}" if neg else s
# ═══════════════════════════════════════════════════════════════════════════════
# INSTRUCTION METADATA - fallback functions when inst.num_srcs()/inst.operands unavailable
# ═══════════════════════════════════════════════════════════════════════════════
def _num_srcs(inst) -> int:
"""Fallback: get number of source operands from instruction name."""
name = getattr(inst, 'op_name', '') or ''
n = name.upper()
# FMAC/MAC ops are 2-source (dst is implicit accumulator), but FMA/MAD ops are 3-source
if 'FMAC' in n or 'V_MAC_' in n: return 2
if any(x in n for x in ('FMA', 'MAD', 'CNDMASK', 'BFE', 'BFI', 'LERP', 'MED3', 'SAD', 'DIV_FMAS', 'DIV_FIXUP', 'DIV_SCALE', 'CUBE')): return 3
# PERMLANE_VAR ops are 2-source, but PERMLANE (non-VAR) are 3-source
if 'PERMLANE' in n and '_VAR' not in n: return 3
if any(x in n for x in ('_ADD3', '_LSHL_ADD', '_ADD_LSHL', '_LSHL_OR', '_AND_OR', 'OR3_B32', 'AND_OR_B32', 'ALIGNBIT', 'ALIGNBYTE', 'V_PERM_', 'XOR3', 'XAD', 'MULLIT', 'MINMAX', 'MAXMIN', 'MINIMUMMAXIMUM', 'MAXIMUMMINIMUM', 'MINIMUM3', 'MAXIMUM3', 'MIN3', 'MAX3', 'DOT2', 'CVT_PK_U8_F32', 'DOT4', 'DOT8', 'WMMA', 'SWMMAC')): return 3
return 2
# ═══════════════════════════════════════════════════════════════════════════════
# IMPORTS
# ═══════════════════════════════════════════════════════════════════════════════
from extra.assembly.amd.autogen.rdna3.ins import (VOP1, VOP1_SDST, VOP1_SDST_LIT, VOP1_LIT, VOP2, VOP2_LIT, VOP3, VOP3_SDST, VOP3_SDST_LIT,
VOP3_LIT, VOP3SD, VOP3SD_LIT, VOP3P, VOP3P_LIT, VOPC, VOPC_LIT, VOPD, VOPD_LIT, VINTERP, SOP1, SOP1_LIT, SOP2, SOP2_LIT, SOPC, SOPC_LIT,
SOPK, SOPK_LIT, SOPP, SMEM, DS, FLAT, GLOBAL, SCRATCH, VOP2Op, VOPDOp, SOPPOp, HWREG, MSG)
from extra.assembly.amd.autogen.rdna4.ins import (VOP1 as R4_VOP1, VOP1_SDST as R4_VOP1_SDST, VOP1_SDST_LIT as R4_VOP1_SDST_LIT, VOP1_LIT as R4_VOP1_LIT,
VOP2 as R4_VOP2, VOP2_LIT as R4_VOP2_LIT, VOP3 as R4_VOP3, VOP3_SDST as R4_VOP3_SDST, VOP3_SDST_LIT as R4_VOP3_SDST_LIT, VOP3_LIT as R4_VOP3_LIT,
VOP3SD as R4_VOP3SD, VOP3SD_LIT as R4_VOP3SD_LIT, VOP3P as R4_VOP3P, VOP3P_LIT as R4_VOP3P_LIT, VOPC as R4_VOPC, VOPC_LIT as R4_VOPC_LIT,
VOPD as R4_VOPD, VOPD_LIT as R4_VOPD_LIT, VINTERP as R4_VINTERP, SOP1 as R4_SOP1, SOP1_LIT as R4_SOP1_LIT, SOP2 as R4_SOP2, SOP2_LIT as R4_SOP2_LIT,
SOPC as R4_SOPC, SOPC_LIT as R4_SOPC_LIT, SOPK as R4_SOPK, SOPK_LIT as R4_SOPK_LIT, SOPP as R4_SOPP, SMEM as R4_SMEM, DS as R4_DS,
VOPDOp as R4_VOPDOp, HWREG as HWREG_RDNA4)
from extra.assembly.amd.autogen.cdna.ins import FLAT as C_FLAT, HWREG as HWREG_CDNA
def _is_cdna(inst: Inst) -> bool: return 'cdna' in inst.__class__.__module__
# CDNA opcode name aliases for disasm (new name -> old name expected by tests)
_CDNA_DISASM_ALIASES = {'v_fmac_f64': 'v_mul_legacy_f32', 'v_dot2c_f32_bf16': 'v_mac_f32', 'v_fmamk_f32': 'v_madmk_f32', 'v_fmaak_f32': 'v_madak_f32'}
# ═══════════════════════════════════════════════════════════════════════════════
# HELPERS
# ═══════════════════════════════════════════════════════════════════════════════
def _reg(p: str, b: int, n: int = 1) -> str: return f"{p}{_unwrap(b)}" if n == 1 else f"{p}[{_unwrap(b)}:{_unwrap(b)+n-1}]"
def _sreg(b: int, n: int = 1) -> str: return _reg("s", _unwrap(b), n)
def _vreg(b: int, n: int = 1) -> str: b = _unwrap(b); return _reg("v", b - 256 if b >= 256 else b, n)
def _areg(b: int, n: int = 1) -> str: b = _unwrap(b); return _reg("a", b - 256 if b >= 256 else b, n) # accumulator registers for GFX90a
def _ttmp(b, n: int = 1) -> str: b = _unwrap(b); return _reg("ttmp", b - 108, n) if 108 <= b <= 123 else None
def _fmt_sdst(v, n: int = 1, cdna: bool = False) -> str:
v = _unwrap(v)
if t := _ttmp(v, n): return t
pairs = SPECIAL_PAIRS_CDNA if cdna else SPECIAL_PAIRS
gprs = SPECIAL_GPRS_CDNA if cdna else SPECIAL_GPRS
if n > 1: return pairs.get(v) or gprs.get(v) or _sreg(v, n) # also check gprs for null/m0
return gprs.get(v, f"s{v}")
def _fmt_src(v, n: int = 1, cdna: bool = False) -> str:
v = _unwrap(v)
if v == 253: return "src_scc" # SCC as source operand
if n == 1: return decode_src(v, cdna)
if v >= 256: return _vreg(v, n)
if v <= 101: return _sreg(v, n) # s0-s101 can be pairs, but 102+ are special on CDNA
pairs = SPECIAL_PAIRS_CDNA if cdna else SPECIAL_PAIRS
if n == 2 and v in pairs: return pairs[v]
if v <= 105: return _sreg(v, n) # s102-s105 regular pairs for RDNA
if t := _ttmp(v, n): return t
return decode_src(v, cdna)
def _fmt_v16(v, base: int = 256, hi_thresh: int = 384) -> str:
v = _unwrap(v)
return f"v{(v - base) & 0x7f}.{'h' if v >= hi_thresh else 'l'}"
def _has(op: str, *subs) -> bool: return any(s in op for s in subs)
def _omod(v: int) -> str: return {1: " mul:2", 2: " mul:4", 3: " div:2"}.get(v, "")
def _src16(inst, v: int) -> str: v = _unwrap(v); return _fmt_v16(v) if v >= 256 else _lit(inst, v) # format 16-bit src: vgpr.h/l or literal
def _mods(*pairs) -> str: return " ".join(m for c, m in pairs if c)
def _fmt_bits(label: str, val: int, count: int) -> str: return f"{label}:[{','.join(str((val >> i) & 1) for i in range(count))}]"
def _vop3_src(inst, v: int, neg: int, abs_: int, hi: int, n: int, f16: bool) -> str:
"""Format VOP3 source operand with modifiers."""
v = _unwrap(v)
if v == 255: s = _lit(inst, v) # literal constant takes priority
elif n > 1: s = _fmt_src(v, n)
elif f16 and v >= 256: s = f"v{v - 256}.h" if hi else f"v{v - 256}.l"
elif v == 253: s = "src_scc" # VOP3 sources use src_scc not scc
else: s = _lit(inst, v)
if abs_: s = f"|{s}|"
return f"-{s}" if neg else s
def _opsel_str(opsel: int, n: int, need: bool, is16_d: bool) -> str:
"""Format op_sel modifier string."""
if not need: return ""
dst_hi = (opsel >> 3) & 1
if n == 1: return f" op_sel:[{opsel & 1},{dst_hi}]"
# Use 4-element format if bit 2 is set (src2 selection used) or if 3+ sources
if n == 2 and not ((opsel >> 2) & 1): return f" op_sel:[{opsel & 1},{(opsel >> 1) & 1},{dst_hi}]"
return f" op_sel:[{opsel & 1},{(opsel >> 1) & 1},{(opsel >> 2) & 1},{dst_hi}]"
# ═══════════════════════════════════════════════════════════════════════════════
# DISASSEMBLER
# ═══════════════════════════════════════════════════════════════════════════════
def _disasm_vop1(inst: VOP1) -> str:
name, cdna = inst.op_name.lower() or f'vop1_op_{inst.op}', _is_cdna(inst)
name = name.replace('_e32', '') # Strip _e32 suffix
if any(x in name for x in ('v_nop', 'v_pipeflush', 'v_clrexcp')): return name # no operands
if 'readfirstlane' in name:
src = inst.src0.fmt() if inst.src0.offset >= 256 else decode_src(inst.src0.offset, cdna)
vdst_off = inst.vdst.offset - 256 if inst.vdst.offset >= 256 else inst.vdst.offset
return f"{name} {_fmt_sdst(vdst_off, 1, cdna)}, {src}"
bits = inst.canonical_op_bits
is16_dst, is16_src = not cdna and bits['d'] == 16, not cdna and bits['s0'] == 16
# Format dst
if is16_dst: dst = _fmt_v16(inst.vdst)
else: dst = inst.vdst.fmt()
# Format src
if inst.src0.offset == 255: src = _lit(inst, inst.src0)
elif is16_src and inst.src0.offset >= 256: src = _fmt_v16(inst.src0)
elif inst.src0.sz > 1: src = _fmt_src(inst.src0, inst.src0.sz, cdna)
else: src = _lit(inst, inst.src0)
return f"{name} {dst}, {src}"
_VOP2_CARRY_OUT = {'v_add_co_u32', 'v_sub_co_u32', 'v_subrev_co_u32'} # carry out only
_VOP2_CARRY_INOUT = {'v_addc_co_u32', 'v_subb_co_u32', 'v_subbrev_co_u32'} # carry in and out (CDNA)
_VOP2_CARRY_INOUT_RDNA = {'v_add_co_ci_u32', 'v_sub_co_ci_u32', 'v_subrev_co_ci_u32'} # carry in and out (RDNA)
def _disasm_vop2(inst: VOP2) -> str:
name, cdna = inst.op_name.lower(), _is_cdna(inst)
if cdna: name = _CDNA_DISASM_ALIASES.get(name, name) # apply CDNA aliases
suf = "" if cdna or name.endswith('_e32') or (not cdna and inst.op == VOP2Op.V_DOT2ACC_F32_F16_E32) else "_e32"
lit = inst._literal
is16 = not cdna and inst.canonical_op_bits['d'] == 16
# fmaak/madak: dst = src0 * vsrc1 + K, fmamk/madmk: dst = src0 * K + vsrc1
if 'fmaak' in name or 'madak' in name or (not cdna and inst.op in (VOP2Op.V_FMAAK_F32_E32, VOP2Op.V_FMAAK_F16_E32)):
if lit is None: return f"op_{inst.op.value if hasattr(inst.op, 'value') else inst.op}"
if is16: return f"{name}{suf} {_fmt_v16(inst.vdst)}, {_src16(inst, inst.src0)}, {_fmt_v16(inst.vsrc1)}, 0x{lit:x}"
return f"{name}{suf} {inst.vdst.fmt()}, {_lit(inst, inst.src0)}, {inst.vsrc1.fmt()}, 0x{lit:x}"
if 'fmamk' in name or 'madmk' in name or (not cdna and inst.op in (VOP2Op.V_FMAMK_F32_E32, VOP2Op.V_FMAMK_F16_E32)):
if lit is None: return f"op_{inst.op.value if hasattr(inst.op, 'value') else inst.op}"
if is16: return f"{name}{suf} {_fmt_v16(inst.vdst)}, {_src16(inst, inst.src0)}, 0x{lit:x}, {_fmt_v16(inst.vsrc1)}"
return f"{name}{suf} {inst.vdst.fmt()}, {_lit(inst, inst.src0)}, 0x{lit:x}, {inst.vsrc1.fmt()}"
if is16: return f"{name}{suf} {_fmt_v16(inst.vdst)}, {_src16(inst, inst.src0)}, {_fmt_v16(inst.vsrc1)}"
vcc = "vcc" if cdna else "vcc_lo"
basename = name.replace('_e32', '')
if cdna and basename in _VOP2_CARRY_OUT: return f"{name}{suf} {inst.vdst.fmt()}, {vcc}, {_lit(inst, inst.src0)}, {inst.vsrc1.fmt()}"
if cdna and basename in _VOP2_CARRY_INOUT: return f"{name}{suf} {inst.vdst.fmt()}, {vcc}, {_lit(inst, inst.src0)}, {inst.vsrc1.fmt()}, {vcc}"
if not cdna and basename in _VOP2_CARRY_INOUT_RDNA: return f"{name}{suf} {inst.vdst.fmt()}, {vcc}, {_lit(inst, inst.src0)}, {inst.vsrc1.fmt()}, {vcc}"
sn0 = inst.canonical_op_regs.get('s0', 1)
if inst.vdst.sz > 1 or sn0 > 1 or inst.vsrc1.sz > 1:
src0 = _lit(inst, inst.src0) if inst.src0.offset == 255 else _fmt_src(inst.src0, sn0, cdna)
return f"{name.replace('_e32', '')} {inst.vdst.fmt()}, {src0}, {inst.vsrc1.fmt()}"
return f"{name}{suf} {inst.vdst.fmt()}, {_lit(inst, inst.src0)}, {inst.vsrc1.fmt()}" + (f", {vcc}" if name == 'v_cndmask_b32' else "")
def _disasm_vopc(inst: VOPC) -> str:
name, cdna = inst.op_name.lower(), _is_cdna(inst)
bits = inst.canonical_op_bits
is16 = bits['s0'] == 16
if cdna:
s0 = _lit(inst, inst.src0) if inst.src0.offset == 255 else _fmt_src(inst.src0, inst.src0.sz, cdna)
return f"{name} vcc, {s0}, {inst.vsrc1.fmt()}" # CDNA VOPC always outputs vcc
# RDNA: v_cmpx_* writes to exec (no vcc), v_cmp_* writes to vcc_lo
has_vcc = 'cmpx' not in name
s0 = _lit(inst, inst.src0) if inst.src0.offset == 255 else inst.src0.fmt() if inst.src0.sz > 1 else _src16(inst, inst.src0.offset) if is16 else _lit(inst, inst.src0)
s1 = inst.vsrc1.fmt() if inst.vsrc1.sz > 1 else _fmt_v16(inst.vsrc1) if is16 else inst.vsrc1.fmt()
suf = "" if name.endswith('_e32') else "_e32"
return f"{name}{suf} vcc_lo, {s0}, {s1}" if has_vcc else f"{name}{suf} {s0}, {s1}"
NO_ARG_SOPP = {SOPPOp.S_BARRIER, SOPPOp.S_WAKEUP, SOPPOp.S_ICACHE_INV,
SOPPOp.S_WAIT_IDLE, SOPPOp.S_ENDPGM_SAVED, SOPPOp.S_CODE_END, SOPPOp.S_ENDPGM_ORDERED_PS_DONE, SOPPOp.S_TTRACEDATA}
def _disasm_sopp(inst: SOPP) -> str:
name, cdna = inst.op_name.lower(), _is_cdna(inst)
is_rdna4 = 'rdna4' in inst.__class__.__module__
# Ops that have no argument when simm16 == 0
no_arg_zero = {'s_barrier', 's_wakeup', 's_icache_inv', 's_ttracedata', 's_wait_idle', 's_endpgm_saved',
's_endpgm_ordered_ps_done', 's_code_end'}
if name in no_arg_zero: return name if inst.simm16 == 0 else f"{name} {inst.simm16}"
if name == 's_endpgm': return name if inst.simm16 == 0 else f"{name} {inst.simm16}"
if cdna:
if name == 's_waitcnt':
# GFX9 format: vmcnt[3:0]=bits[3:0], vmcnt[5:4]=bits[15:14], expcnt=bits[6:4], lgkmcnt=bits[11:8] (4 bits, max 15)
vm_lo, exp, lgkm, vm_hi = inst.simm16 & 0xf, (inst.simm16 >> 4) & 0x7, (inst.simm16 >> 8) & 0xf, (inst.simm16 >> 14) & 0x3
vm = vm_lo | (vm_hi << 4)
p = [f"vmcnt({vm})" if vm != 0x3f else "", f"expcnt({exp})" if exp != 7 else "", f"lgkmcnt({lgkm})" if lgkm != 0xf else ""]
return f"s_waitcnt {' '.join(x for x in p if x) or '0'}"
if name.startswith(('s_cbranch', 's_branch')): return f"{name} {inst.simm16}"
if name == 's_set_gpr_idx_mode':
flags = [n for i, n in enumerate(['SRC0', 'SRC1', 'SRC2', 'DST']) if inst.simm16 & (1 << i)]
return f"{name} gpr_idx({','.join(flags)})"
return f"{name} 0x{inst.simm16:x}" if inst.simm16 else name
# RDNA (use name-based checks instead of enum-based for cross-arch compatibility)
if name == 's_waitcnt':
if is_rdna4:
return f"{name} {inst.simm16}" if inst.simm16 else f"{name} 0"
vm, exp, lgkm = (inst.simm16 >> 10) & 0x3f, inst.simm16 & 0xf, (inst.simm16 >> 4) & 0x3f
p = [f"vmcnt({vm})" if vm != 0x3f else "", f"expcnt({exp})" if exp != 7 else "", f"lgkmcnt({lgkm})" if lgkm != 0x3f else ""]
return f"s_waitcnt {' '.join(x for x in p if x) or '0'}"
if name == 's_delay_alu':
deps = ['VALU_DEP_1','VALU_DEP_2','VALU_DEP_3','VALU_DEP_4','TRANS32_DEP_1','TRANS32_DEP_2','TRANS32_DEP_3','FMA_ACCUM_CYCLE_1','SALU_CYCLE_1','SALU_CYCLE_2','SALU_CYCLE_3']
skips = ['SAME','NEXT','SKIP_1','SKIP_2','SKIP_3','SKIP_4']
id0, skip, id1 = inst.simm16 & 0xf, (inst.simm16 >> 4) & 0x7, (inst.simm16 >> 7) & 0xf
dep = lambda v: deps[v-1] if 0 < v <= len(deps) else str(v)
p = [f"instid0({dep(id0)})" if id0 else "", f"instskip({skips[skip]})" if skip else "", f"instid1({dep(id1)})" if id1 else ""]
return f"s_delay_alu {' | '.join(x for x in p if x) or '0'}"
if name.startswith(('s_cbranch', 's_branch')): return f"{name} 0x{inst.simm16:x}"
return f"{name} 0x{inst.simm16:x}"
def _disasm_smem(inst: SMEM) -> str:
name, cdna = inst.op_name.lower(), _is_cdna(inst)
if name in ('s_gl1_inv', 's_dcache_inv', 's_dcache_inv_vol', 's_dcache_wb', 's_dcache_wb_vol', 's_icache_inv'): return name
soe, imm = getattr(inst, 'soe', 0) or getattr(inst, 'soffset_en', 0), getattr(inst, 'imm', 1)
is_rdna4 = 'rdna4' in inst.__class__.__module__
offset = inst.ioffset if is_rdna4 else getattr(inst, 'offset', 0)
if cdna:
if soe and imm: off_s = f"{decode_src(inst.soffset, cdna)} offset:0x{offset:x}"
elif imm: off_s = f"0x{offset:x}"
elif offset < 256: off_s = decode_src(offset, cdna)
else: off_s = decode_src(inst.soffset, cdna)
elif offset and inst.soffset != 124: off_s = f"{decode_src(inst.soffset, cdna)} offset:0x{offset:x}"
elif offset: off_s = f"0x{offset:x}"
else: off_s = decode_src(inst.soffset, cdna)
is_buffer = 'buffer' in name or 's_atc_probe_buffer' == name
sbase_idx, sbase_count = _unwrap(inst.sbase), 4 if is_buffer else 2
sbase_str = _fmt_src(sbase_idx, sbase_count, cdna) if sbase_count == 2 else _sreg(sbase_idx, sbase_count) if sbase_idx <= 105 else _reg("ttmp", sbase_idx - 108, sbase_count)
if name in ('s_atc_probe', 's_atc_probe_buffer'): return f"{name} {_unwrap(inst.sdata)}, {sbase_str}, {off_s}"
if 'prefetch' in name:
off = getattr(inst, 'ioffset', getattr(inst, 'offset', 0))
if off >= 0x800000: off = off - 0x1000000
off_s = f"0x{off:x}" if off > 255 else str(off)
soff_s = decode_src(inst.soffset, cdna) if inst.soffset != 124 else ("m0" if cdna else "null")
if 'pc_rel' in name: return f"{name} {off_s}, {soff_s}, {_unwrap(inst.sdata)}"
return f"{name} {sbase_str}, {off_s}, {soff_s}, {_unwrap(inst.sdata)}"
# Use get_field_bits for register count
dst_n = inst.canonical_op_regs.get('d', 1)
th, scope = getattr(inst, 'th', 0), getattr(inst, 'scope', 0)
if is_rdna4: # RDNA4 uses th/scope instead of glc/dlc
th_names = ['TH_LOAD_RT', 'TH_LOAD_NT', 'TH_LOAD_HT', 'TH_LOAD_LU']
scope_names = ['SCOPE_CU', 'SCOPE_SE', 'SCOPE_DEV', 'SCOPE_SYS']
mods = (f" th:{th_names[th]}" if th else "") + (f" scope:{scope_names[scope]}" if scope else "")
return f"{name} {_fmt_sdst(inst.sdata, dst_n, cdna)}, {sbase_str}, {off_s}{mods}"
if th or scope:
th_names = ['TH_LOAD_RT', 'TH_LOAD_NT', 'TH_LOAD_HT', 'TH_LOAD_LU']
scope_names = ['SCOPE_CU', 'SCOPE_SE', 'SCOPE_DEV', 'SCOPE_SYS']
mods = (f" th:{th_names[th]}" if th else "") + (f" scope:{scope_names[scope]}" if scope else "")
return f"{name} {_fmt_sdst(inst.sdata, dst_n, cdna)}, {sbase_str}, {off_s}{mods}"
if 'discard' in name: return f"{name} {sbase_str}, {off_s}" + _mods((inst.glc, " glc"), (getattr(inst, 'dlc', 0), " dlc"))
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"))
def _disasm_flat(inst: FLAT) -> str:
name, cdna = inst.op_name.lower(), _is_cdna(inst)
acc = getattr(inst, 'acc', 0)
reg_fn = _areg if acc else _vreg
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
if seg != 'flat':
if cdna:
# CDNA: bit 12 is sign bit but not in offset field
raw = int.from_bytes(inst.to_bytes(), 'little')
off_val = inst.offset | ((raw >> 12) & 1) << 12 # get bit 12
else:
off_val = inst.offset
off_val = off_val if off_val < 4096 else off_val - 8192 # sign extend 13-bit
else:
off_val = inst.offset
# Use get_field_bits: data for stores/atomics, d for loads
regs = inst.canonical_op_regs
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 ''}"
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"
elif seg == 'scratch': saddr_s = f", {decode_src(inst.saddr, cdna)}"
elif _unwrap(inst.saddr) in (SPECIAL_PAIRS_CDNA if cdna else SPECIAL_PAIRS): 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}"
# load_lds_* instructions: vaddr, saddr (no vdst, data goes to LDS)
if 'load_lds' in name:
addr_w = 1 if seg == 'scratch' or (_unwrap(inst.saddr) not in (0x7F, 124)) else 2
addr_s = "off" if not inst.sve and seg == 'scratch' else _vreg(inst.addr, addr_w)
return f"{instr} {addr_s}{saddr_s}{mods}"
if seg == 'flat': addr_w = 2 # flat always uses 64-bit vaddr
elif cdna: addr_w = 1 if seg == 'scratch' or (_unwrap(inst.saddr) not in (0x7F, 124)) else 2
else: addr_w = 1 if seg == 'scratch' or (_unwrap(inst.saddr) not in (0x7F, 124)) else 2
addr_s = "off" if not inst.sve and seg == 'scratch' else _vreg(inst.addr, addr_w)
data_s, vdst_s = reg_fn(inst.data, w), reg_fn(inst.vdst, w // 2 if 'cmpswap' in name else w)
glc_or_sc0 = inst.sc0 if cdna else inst.glc
if 'atomic' in name:
return f"{instr} {vdst_s}, {addr_s}, {data_s}{saddr_s if seg != 'flat' else ''}{mods}" if glc_or_sc0 else f"{instr} {addr_s}, {data_s}{saddr_s if seg != 'flat' else ''}{mods}"
if 'store' in name: return f"{instr} {addr_s}, {data_s}{saddr_s}{mods}"
return f"{instr} {reg_fn(inst.vdst, w)}, {addr_s}{saddr_s}{mods}"
def _disasm_ds(inst: DS) -> str:
op, name = inst.op, inst.op_name.lower()
acc = getattr(inst, 'acc', 0)
reg_fn = _areg if acc else _vreg
gds = " gds" if getattr(inst, 'gds', 0) else ""
off = f" offset:{inst.offset0 | (inst.offset1 << 8)}" if inst.offset0 or inst.offset1 else ""
off2 = (" offset0:" + str(inst.offset0) if inst.offset0 else "") + (" offset1:" + str(inst.offset1) if inst.offset1 else "")
# Use get_field_bits: data for stores/writes/atomics, d for loads
regs = inst.canonical_op_regs
w = regs.get('data', regs.get('d', 1)) if 'store' in name or 'write' in name or ('load' not in name and 'read' not in name) else regs.get('d', 1)
d0, d1, dst, addr = reg_fn(inst.data0, w), reg_fn(inst.data1, w), reg_fn(inst.vdst, w), _vreg(inst.addr)
if name == 'ds_nop': return name
if name == 'ds_bvh_stack_rtn_b32': return f"{name} {_vreg(inst.vdst)}, {addr}, {_vreg(inst.data0)}, {_vreg(inst.data1, 4)}{off}{gds}"
if 'bvh_stack_push' in name:
d1_regs = 8 if 'push8' in name else 4
vdst_regs = 2 if 'pop2' in name else 1
vdst_s = _vreg(inst.vdst, vdst_regs) if vdst_regs > 1 else _vreg(inst.vdst)
return f"{name} {vdst_s}, {addr}, {_vreg(inst.data0)}, {_vreg(inst.data1, d1_regs)}{off}{gds}"
if 'gws_sema' in name and 'sema_br' not in name: return f"{name}{off}{gds}"
if 'gws_' in name: return f"{name} {addr}{off}{gds}"
if name in ('ds_consume', 'ds_append'): return f"{name} {reg_fn(inst.vdst)}{off}{gds}"
if 'gs_reg' in name: return f"{name} {reg_fn(inst.vdst, 2)}, {reg_fn(inst.data0)}{off}{gds}"
if '2addr' in name:
if 'load' in name: return f"{name} {reg_fn(inst.vdst, regs.get('d', 1))}, {addr}{off2}{gds}"
if 'store' in name and 'xchg' not in name: return f"{name} {addr}, {d0}, {d1}{off2}{gds}"
return f"{name} {reg_fn(inst.vdst, regs.get('d', 1))}, {addr}, {d0}, {d1}{off2}{gds}"
if 'write2' in name: return f"{name} {addr}, {d0}, {d1}{off2}{gds}"
if 'read2' in name: return f"{name} {reg_fn(inst.vdst, regs.get('d', 1))}, {addr}{off2}{gds}"
if 'xchg2' in name: return f"{name} {reg_fn(inst.vdst, regs.get('d', 1))}, {addr}, {d0}, {d1}{off2}{gds}"
if 'load' in name or ('read' in name and 'read2' not in name): return f"{name} {reg_fn(inst.vdst)}{off}{gds}" if 'addtid' in name else f"{name} {dst}, {addr}{off}{gds}"
if ('store' in name or 'write' in name) and not _has(name, 'cmp', 'xchg', 'write2'):
return f"{name} {reg_fn(inst.data0)}{off}{gds}" if 'addtid' in name else f"{name} {addr}, {d0}{off}{gds}"
if 'swizzle' in name or name == 'ds_ordered_count': return f"{name} {reg_fn(inst.vdst)}, {addr}{off}{gds}"
if 'permute' in name: return f"{name} {reg_fn(inst.vdst)}, {addr}, {reg_fn(inst.data0)}{off}{gds}"
if 'condxchg' in name: return f"{name} {reg_fn(inst.vdst, 2)}, {addr}, {reg_fn(inst.data0, 2)}{off}{gds}"
if _has(name, 'cmpst', 'mskor', 'wrap'):
return f"{name} {dst}, {addr}, {d0}, {d1}{off}{gds}" if '_rtn' in name else f"{name} {addr}, {d0}, {d1}{off}{gds}"
return f"{name} {dst}, {addr}, {d0}{off}{gds}" if '_rtn' in name else f"{name} {addr}, {d0}{off}{gds}"
def _disasm_vop3(inst: VOP3) -> str:
op, name = inst.op, inst.op_name.lower()
n_up = name.upper()
bits = inst.canonical_op_bits
# RDNA4 v_s_* scalar VOP3 instructions - vdst is SGPR (VGPRField adds 256)
if name.startswith('v_s_'):
src = _lit(inst, inst.src0) if _unwrap(inst.src0) == 255 else ("src_scc" if _unwrap(inst.src0) == 253 else _fmt_src(inst.src0, max(1, bits['s0'] // 32)))
if inst.neg & 1: src = f"-{src}"
if inst.abs & 1: src = f"|{src}|"
clamp = getattr(inst, 'cm', None) or getattr(inst, 'clmp', 0)
vdst_raw = _unwrap(inst.vdst)
return f"{name} s{vdst_raw - 256 if vdst_raw >= 256 else vdst_raw}, {src}" + (" clamp" if clamp else "") + _omod(inst.omod)
# Use get_field_bits for register sizes and 16-bit detection
r0, r1, r2 = max(1, bits['s0'] // 32), max(1, bits['s1'] // 32), max(1, bits['s2'] // 32)
dn = max(1, bits['d'] // 32)
is16_d, is16_s, is16_s2 = bits['d'] == 16, bits['s0'] == 16, bits['s2'] == 16
s0 = _vop3_src(inst, inst.src0, inst.neg&1, inst.abs&1, inst.opsel&1, r0, is16_s)
s1 = _vop3_src(inst, inst.src1, inst.neg&2, inst.abs&2, inst.opsel&2, r1, is16_s)
s2 = _vop3_src(inst, inst.src2, inst.neg&4, inst.abs&4, inst.opsel&4, r2, is16_s2)
# Format destination
if 'readlane' in name:
vdst_off = inst.vdst.offset - 256 if inst.vdst.offset >= 256 else inst.vdst.offset
dst = _fmt_sdst(vdst_off, 1)
elif is16_d: dst = f"{inst.vdst.fmt()}.h" if (inst.opsel & 8) else f"{inst.vdst.fmt()}.l"
else: dst = inst.vdst.fmt()
clamp = getattr(inst, 'cm', None) or getattr(inst, 'clmp', 0)
cl, om = " clamp" if clamp else "", _omod(inst.omod)
nonvgpr_opsel = (inst.src0.offset < 256 and (inst.opsel & 1)) or (inst.src1.offset < 256 and (inst.opsel & 2)) or (inst.src2.offset < 256 and (inst.opsel & 4))
need_opsel = nonvgpr_opsel or (inst.opsel and not is16_s)
op_val = inst.op.value if hasattr(inst.op, 'value') else inst.op
e64 = "" if name.endswith('_e64') else "_e64"
if op_val < 256: # VOPC
vdst_off = inst.vdst.offset - 256 if inst.vdst.offset >= 256 else inst.vdst.offset
return f"{name}{e64} {s0}, {s1}{cl}" if name.startswith('v_cmpx') else f"{name}{e64} {_fmt_sdst(vdst_off, 1)}, {s0}, {s1}{cl}"
if op_val < 384: # VOP2
n = inst.num_srcs() or 2
os = _opsel_str(inst.opsel, n, need_opsel, is16_d)
return f"{name}{e64} {dst}, {s0}, {s1}, {s2}{os}{cl}{om}" if n == 3 else f"{name}{e64} {dst}, {s0}, {s1}{os}{cl}{om}"
if op_val < 512: # VOP1
if re.match(r'v_cvt_f32_(bf|fp)8', name) and inst.opsel:
os = f" byte_sel:{((inst.opsel & 1) << 1) | ((inst.opsel >> 1) & 1)}"
else:
os = _opsel_str(inst.opsel, 1, need_opsel, is16_d)
if 'v_nop' in name or 'v_pipeflush' in name: return f"{name}{e64}"
return f"{name}{e64} {dst}, {s0}{os}{cl}{om}"
# Native VOP3
n = inst.num_srcs() or 2
os = f" byte_sel:{inst.opsel >> 2}" if 'cvt_sr' in name and inst.opsel else _opsel_str(inst.opsel, n, need_opsel, is16_d)
return f"{name} {dst}, {s0}, {s1}, {s2}{os}{cl}{om}" if n == 3 else f"{name} {dst}, {s0}, {s1}{os}{cl}{om}"
def _disasm_vop3sd(inst: VOP3SD) -> str:
name = inst.op_name.lower()
def src(reg, neg):
s = _lit(inst, reg.offset) if reg.offset == 255 else ("src_scc" if reg.offset == 253 else (reg.fmt() if reg.sz > 1 else _lit(inst, reg.offset)))
return f"neg({s})" if neg and reg.offset == 255 else (f"-{s}" if neg else s)
s0, s1, s2 = src(inst.src0, inst.neg & 1), src(inst.src1, inst.neg & 2), src(inst.src2, inst.neg & 4)
# VOP3SD: _co_ ops (add/sub) without _ci_ have only 2 sources, all others (mad, div_scale, _co_ci_) have 3 sources
has_only_two_srcs = '_co_' in name and '_ci_' not in name and 'mad' not in name
srcs = f"{s0}, {s1}" if has_only_two_srcs else f"{s0}, {s1}, {s2}"
clamp = getattr(inst, 'cm', None) or getattr(inst, 'clmp', 0)
return f"{name} {inst.vdst.fmt()}, {_fmt_sdst(inst.sdst, 1)}, {srcs}{' clamp' if clamp else ''}{_omod(inst.omod)}"
def _disasm_vopd(inst: VOPD) -> str:
lit = inst._literal
is_rdna4 = 'rdna4' in inst.__class__.__module__
op_enum = R4_VOPDOp if is_rdna4 else VOPDOp
vdst_y, nx, ny = (_unwrap(inst.vdsty) << 1) | ((_unwrap(inst.vdstx) & 1) ^ 1), op_enum(inst.opx).name.lower(), op_enum(inst.opy).name.lower()
def half(n, vd, s0, vs1):
vd, vs1 = _vi(vd), _vi(vs1)
if 'mov' in n: return f"{n} v{vd}, {_lit(inst, s0)}"
if 'fmamk' in n and lit: return f"{n} v{vd}, {_lit(inst, s0)}, 0x{lit:x}, v{vs1}"
if 'fmaak' in n and lit: return f"{n} v{vd}, {_lit(inst, s0)}, v{vs1}, 0x{lit:x}"
return f"{n} v{vd}, {_lit(inst, s0)}, v{vs1}"
return f"{half(nx, inst.vdstx, inst.srcx0, inst.vsrcx1)} :: {half(ny, vdst_y, inst.srcy0, inst.vsrcy1)}"
def _disasm_vop3p(inst: VOP3P) -> str:
name = inst.op_name.lower()
is_wmma, is_swmmac, n, is_fma_mix = 'wmma' in name, 'swmmac' in name, inst.num_srcs() or 2, 'fma_mix' in name
def get_src(reg):
return _lit(inst, reg.offset) if reg.offset == 255 else reg.fmt()
src0, src1, src2, dst = get_src(inst.src0), get_src(inst.src1), get_src(inst.src2), inst.vdst.fmt()
opsel_hi = inst.opsel_hi | (inst.opsel_hi2 << 2)
clamp = getattr(inst, 'cm', None) or getattr(inst, 'clmp', 0)
if is_fma_mix:
def m(s, neg, abs_): return f"-{f'|{s}|' if abs_ else s}" if neg else (f"|{s}|" if abs_ else s)
src0, src1, src2 = m(src0, inst.neg & 1, inst.neg_hi & 1), m(src1, inst.neg & 2, inst.neg_hi & 2), m(src2, inst.neg & 4, inst.neg_hi & 4)
mods = ([_fmt_bits("op_sel", inst.opsel, n)] if inst.opsel else []) + ([_fmt_bits("op_sel_hi", opsel_hi, n)] if opsel_hi else []) + (["clamp"] if clamp else [])
elif is_swmmac:
mods = ([f"index_key:{inst.opsel}"] if inst.opsel else []) + ([_fmt_bits("neg_lo", inst.neg, n)] if inst.neg else []) + \
([_fmt_bits("neg_hi", inst.neg_hi, n)] if inst.neg_hi else []) + (["clamp"] if clamp else [])
else:
opsel_hi_default = 7 if n == 3 else 3
mods = ([_fmt_bits("op_sel", inst.opsel, n)] if inst.opsel else []) + ([_fmt_bits("op_sel_hi", opsel_hi, n)] if opsel_hi != opsel_hi_default else []) + \
([_fmt_bits("neg_lo", inst.neg, n)] if inst.neg else []) + ([_fmt_bits("neg_hi", inst.neg_hi, n)] if inst.neg_hi else []) + (["clamp"] if clamp else [])
return f"{name} {dst}, {src0}, {src1}, {src2}{' ' + ' '.join(mods) if mods else ''}" if n == 3 else f"{name} {dst}, {src0}, {src1}{' ' + ' '.join(mods) if mods else ''}"
def _disasm_sop1(inst: SOP1) -> str:
op, name, cdna = inst.op, inst.op_name.lower(), _is_cdna(inst)
# Use get_field_bits for register sizes
regs = inst.canonical_op_regs
dst_regs, src_regs = regs.get('d', 1), regs.get('s0', 1)
src = _lit(inst, inst.ssrc0) if _unwrap(inst.ssrc0) == 255 else _fmt_src(inst.ssrc0, src_regs, cdna)
if not cdna:
if 'getpc_b64' in name: return f"{name} {_fmt_sdst(inst.sdst, 2)}"
if 'setpc_b64' in name or 'rfe_b64' in name: return f"{name} {src}"
if 'swappc_b64' in name: return f"{name} {_fmt_sdst(inst.sdst, 2)}, {src}"
if 'sendmsg_rtn' in name:
v = _unwrap(inst.ssrc0)
try: msg_str = MSG(v).name if v != 255 else None # MSG_RTN_ILLEGAL_MSG (255) not supported by LLVM
except ValueError: msg_str = None
return f"{name} {_fmt_sdst(inst.sdst, dst_regs)}, sendmsg({msg_str})" if msg_str else f"{name} {_fmt_sdst(inst.sdst, dst_regs)}, 0x{v:x}"
sop1_src_only = ('S_ALLOC_VGPR', 'S_SLEEP_VAR', 'S_BARRIER_SIGNAL', 'S_BARRIER_SIGNAL_ISFIRST', 'S_BARRIER_INIT', 'S_BARRIER_JOIN', 'S_SET_GPR_IDX_IDX',
'S_CBRANCH_JOIN')
if inst.op_name in sop1_src_only: return f"{name} {src}"
if cdna:
if 'getpc_b64' in name: return f"{name} {_fmt_sdst(inst.sdst, 2, cdna)}"
if 'setpc_b64' in name or 'rfe_b64' in name: return f"{name} {src}"
if 'swappc_b64' in name: return f"{name} {_fmt_sdst(inst.sdst, 2, cdna)}, {src}"
return f"{name} {_fmt_sdst(inst.sdst, dst_regs, cdna)}, {src}"
def _disasm_sop2(inst: SOP2) -> str:
cdna, name = _is_cdna(inst), inst.op_name.lower()
lit = inst._literal
# Use get_field_bits for register sizes
regs = inst.canonical_op_regs
dn, s0n, s1n = regs['d'], regs['s0'], regs['s1']
s0 = _lit(inst, inst.ssrc0) if _unwrap(inst.ssrc0) == 255 else _fmt_src(inst.ssrc0, s0n, cdna)
s1 = _lit(inst, inst.ssrc1) if _unwrap(inst.ssrc1) == 255 else _fmt_src(inst.ssrc1, s1n, cdna)
dst = _fmt_sdst(inst.sdst, dn, cdna)
if 'fmamk' in name and lit is not None: return f"{name} {dst}, {s0}, 0x{lit:x}, {s1}"
if 'fmaak' in name and lit is not None: return f"{name} {dst}, {s0}, {s1}, 0x{lit:x}"
if name in ('s_cbranch_g_fork', 's_rfe_restore_b64'): return f"{name} {s0}, {s1}" # no destination
return f"{name} {dst}, {s0}, {s1}"
def _disasm_sopc(inst: SOPC) -> str:
cdna, regs, name = _is_cdna(inst), inst.canonical_op_regs, inst.op_name.lower()
s0 = _lit(inst, inst.ssrc0) if _unwrap(inst.ssrc0) == 255 else _fmt_src(inst.ssrc0, regs['s0'], cdna)
if name == 's_set_gpr_idx_on':
imm = _unwrap(inst.ssrc1) & 0xf
flags = [n for i, n in enumerate(['SRC0', 'SRC1', 'SRC2', 'DST']) if imm & (1 << i)]
return f"{name} {s0}, gpr_idx({','.join(flags)})"
s1 = _lit(inst, inst.ssrc1) if _unwrap(inst.ssrc1) == 255 else _fmt_src(inst.ssrc1, regs['s1'], cdna)
return f"{name} {s0}, {s1}"
_HWREG_BLACKLIST = {'HW_REG_PC_LO', 'HW_REG_PC_HI', 'HW_REG_IB_DBG1', 'HW_REG_FLUSH_IB', 'HW_REG_SHADER_TBA_LO', 'HW_REG_SHADER_TBA_HI',
'HW_REG_SHADER_FLAT_SCRATCH_LO', 'HW_REG_SHADER_FLAT_SCRATCH_HI', 'HW_REG_SHADER_CYCLES'}
_HWREG_BLACKLIST_CDNA = {'HW_REG_PC_LO', 'HW_REG_PC_HI', 'HW_REG_IB_DBG1', 'HW_REG_FLUSH_IB', 'HW_REG_SQ_SHADER_TBA_LO', 'HW_REG_SQ_SHADER_TBA_HI',
'HW_REG_SQ_SHADER_TMA_LO', 'HW_REG_SQ_SHADER_TMA_HI', 'HW_REG_SQ_PERF_SNAPSHOT_DATA', 'HW_REG_SQ_PERF_SNAPSHOT_DATA1',
'HW_REG_SQ_PERF_SNAPSHOT_PC_LO', 'HW_REG_SQ_PERF_SNAPSHOT_PC_HI', 'HW_REG_XCC_ID'}
def _disasm_sopk(inst: SOPK) -> str:
op, name, cdna = inst.op, inst.op_name.lower(), _is_cdna(inst)
is_rdna4 = 'rdna4' in inst.__class__.__module__
hw = HWREG_CDNA if cdna else (HWREG_RDNA4 if is_rdna4 else HWREG)
blacklist = _HWREG_BLACKLIST_CDNA if cdna else _HWREG_BLACKLIST
def fmt_hwreg(hid, hoff, hsz):
try: hr_name = hw(hid).name.replace("HW_REG_WAVE_", "HW_REG_")
except ValueError: return f"0x{inst.simm16:x}"
if hr_name in blacklist: return f"0x{inst.simm16:x}"
return f"hwreg({hr_name})" if hoff == 0 and hsz == 32 else f"hwreg({hr_name}, {hoff}, {hsz})"
if name == 's_setreg_imm32_b32':
hid, hoff, hsz = inst.simm16 & 0x3f, (inst.simm16 >> 6) & 0x1f, ((inst.simm16 >> 11) & 0x1f) + 1
return f"{name} {fmt_hwreg(hid, hoff, hsz)}, 0x{inst._literal:x}"
if name == 's_version': return f"{name} 0x{inst.simm16:x}"
if name in ('s_setreg_b32', 's_getreg_b32'):
hid, hoff, hsz = inst.simm16 & 0x3f, (inst.simm16 >> 6) & 0x1f, ((inst.simm16 >> 11) & 0x1f) + 1
hs = fmt_hwreg(hid, hoff, hsz)
return f"{name} {hs}, {_fmt_sdst(inst.sdst, 1, cdna)}" if 'setreg' in name else f"{name} {_fmt_sdst(inst.sdst, 1, cdna)}, {hs}"
if name in ('s_subvector_loop_begin', 's_subvector_loop_end'):
return f"{name} {_fmt_sdst(inst.sdst, 1)}, 0x{inst.simm16:x}"
return f"{name} {_fmt_sdst(inst.sdst, inst.canonical_op_regs['d'], cdna)}, 0x{inst.simm16:x}"
def _disasm_vinterp(inst: VINTERP) -> str:
mods = _mods((inst.waitexp, f"wait_exp:{inst.waitexp}"), (inst.clmp, "clamp"))
return f"{inst.op_name.lower()} {inst.vdst.fmt()}, {_lit(inst, inst.src0, inst.neg & 1)}, {_lit(inst, inst.src1, inst.neg & 2)}, {_lit(inst, inst.src2, inst.neg & 4)}" + (" " + mods if mods else "")
DISASM_HANDLERS: dict[type, callable] = {
VOP1: _disasm_vop1, VOP1_SDST: _disasm_vop1, VOP1_SDST_LIT: _disasm_vop1, VOP1_LIT: _disasm_vop1,
VOP2: _disasm_vop2, VOP2_LIT: _disasm_vop2, VOPC: _disasm_vopc, VOPC_LIT: _disasm_vopc,
VOP3: _disasm_vop3, VOP3_SDST: _disasm_vop3, VOP3_SDST_LIT: _disasm_vop3, VOP3_LIT: _disasm_vop3, VOP3SD: _disasm_vop3sd, VOP3SD_LIT: _disasm_vop3sd,
VOPD: _disasm_vopd, VOPD_LIT: _disasm_vopd, VOP3P: _disasm_vop3p, VOP3P_LIT: _disasm_vop3p,
VINTERP: _disasm_vinterp, SOPP: _disasm_sopp, SMEM: _disasm_smem, DS: _disasm_ds, FLAT: _disasm_flat, GLOBAL: _disasm_flat, SCRATCH: _disasm_flat,
SOP1: _disasm_sop1, SOP1_LIT: _disasm_sop1, SOP2: _disasm_sop2, SOP2_LIT: _disasm_sop2,
SOPC: _disasm_sopc, SOPC_LIT: _disasm_sopc, SOPK: _disasm_sopk, SOPK_LIT: _disasm_sopk,
# RDNA4
R4_VOP1: _disasm_vop1, R4_VOP1_SDST: _disasm_vop1, R4_VOP1_SDST_LIT: _disasm_vop1, R4_VOP1_LIT: _disasm_vop1,
R4_VOP2: _disasm_vop2, R4_VOP2_LIT: _disasm_vop2, R4_VOPC: _disasm_vopc, R4_VOPC_LIT: _disasm_vopc,
R4_VOP3: _disasm_vop3, R4_VOP3_SDST: _disasm_vop3, R4_VOP3_SDST_LIT: _disasm_vop3, R4_VOP3_LIT: _disasm_vop3,
R4_VOP3SD: _disasm_vop3sd, R4_VOP3SD_LIT: _disasm_vop3sd, R4_VOP3P: _disasm_vop3p, R4_VOP3P_LIT: _disasm_vop3p,
R4_VOPD: _disasm_vopd, R4_VOPD_LIT: _disasm_vopd, R4_VINTERP: _disasm_vinterp, R4_SOPP: _disasm_sopp, R4_SMEM: _disasm_smem, R4_DS: _disasm_ds,
R4_SOP1: _disasm_sop1, R4_SOP1_LIT: _disasm_sop1, R4_SOP2: _disasm_sop2, R4_SOP2_LIT: _disasm_sop2,
R4_SOPC: _disasm_sopc, R4_SOPC_LIT: _disasm_sopc, R4_SOPK: _disasm_sopk, R4_SOPK_LIT: _disasm_sopk}
def disasm(inst: Inst) -> str: return DISASM_HANDLERS[type(inst)](inst)
# ═══════════════════════════════════════════════════════════════════════════════
# CDNA DISASSEMBLER SUPPORT
# ═══════════════════════════════════════════════════════════════════════════════
from extra.assembly.amd.autogen.cdna.ins import (VOP1 as CDNA_VOP1, VOP1_LIT as CDNA_VOP1_LIT,
VOP1_SDWA as CDNA_VOP1_SDWA, VOP1_DPP16 as CDNA_VOP1_DPP16,
VOP2 as CDNA_VOP2, VOP2_LIT as CDNA_VOP2_LIT, VOP2_SDWA as CDNA_VOP2_SDWA, VOP2_DPP16 as CDNA_VOP2_DPP16,
VOPC as CDNA_VOPC, VOPC_LIT as CDNA_VOPC_LIT, VOPC_SDWA_SDST as CDNA_VOPC_SDWA_SDST,
VOP3 as CDNA_VOP3, VOP3_SDST as CDNA_VOP3_SDST, VOP3SD as CDNA_VOP3SD, VOP3P as CDNA_VOP3P, VOP3PX2 as CDNA_VOP3PX2,
SOP1 as CDNA_SOP1, SOP1_LIT as CDNA_SOP1_LIT, SOP2 as CDNA_SOP2, SOP2_LIT as CDNA_SOP2_LIT,
SOPC as CDNA_SOPC, SOPC_LIT as CDNA_SOPC_LIT, SOPK as CDNA_SOPK, SOPK_LIT as CDNA_SOPK_LIT,
SOPP as CDNA_SOPP, SMEM as CDNA_SMEM, DS as CDNA_DS,
FLAT as CDNA_FLAT, GLOBAL as CDNA_GLOBAL, SCRATCH as CDNA_SCRATCH, MUBUF as CDNA_MUBUF)
def _cdna_src(inst, v, neg, abs_=0, n=1):
s = _lit(inst, v) if v == 255 else _fmt_src(v, n, cdna=True)
if abs_: s = f"|{s}|"
return f"neg({s})" if neg and v == 255 else (f"-{s}" if neg else s)
_CDNA_VOP3_ALIASES = {'v_fmac_f64': 'v_mul_legacy_f32', 'v_dot2c_f32_bf16': 'v_mac_f32'}
def _disasm_vop3a(inst) -> str:
op_val = inst.op.value if hasattr(inst.op, 'value') else inst.op
name = inst.op_name.lower() or f'vop3a_op_{op_val}'
n = inst.num_srcs() or _num_srcs(inst)
cl, om = " clamp" if inst.clmp else "", _omod(inst.omod)
# _sr_ instructions use 4-element op_sel (src2 for byte selection)
opsel_n = 3 if '_sr_' in name and n == 2 else n
opsel = _opsel_str(inst.opsel, opsel_n, inst.opsel != 0, False)
orig_name = name
name = _CDNA_VOP3_ALIASES.get(name, name)
if name != orig_name:
s0, s1 = _cdna_src(inst, inst.src0, inst.neg&1, inst.abs&1, 1), _cdna_src(inst, inst.src1, inst.neg&2, inst.abs&2, 1)
s2 = ""
dst = _vreg(inst.vdst)
else:
regs = inst.canonical_op_regs
dregs, r0, r1, r2 = regs['d'], regs['s0'], regs['s1'], regs['s2']
s0, s1, s2 = _cdna_src(inst, inst.src0, inst.neg&1, inst.abs&1, r0), _cdna_src(inst, inst.src1, inst.neg&2, inst.abs&2, r1), _cdna_src(inst, inst.src2, inst.neg&4, inst.abs&4, r2)
dst = _vreg(inst.vdst, dregs) if dregs > 1 else _vreg(inst.vdst)
if op_val >= 512:
return f"{name} {dst}, {s0}, {s1}, {s2}{opsel}{cl}{om}" if n == 3 else f"{name} {dst}, {s0}, {s1}{opsel}{cl}{om}"
if op_val < 256:
# VOPC: vdst is actually sdst (SGPR pair), but VGPRField adds 256 to the offset
sdst_val = _unwrap(inst.vdst)
if sdst_val >= 256: sdst_val -= 256
sdst = _fmt_sdst(sdst_val, 2, cdna=True)
return f"{name} {sdst}, {s0}, {s1}{cl}"
if 320 <= op_val < 512:
if name in ('v_nop', 'v_clrexcp', 'v_nop_e64', 'v_clrexcp_e64'): return name.replace('_e64', '')
return f"{name} {dst}, {s0}{cl}{om}"
if name == 'v_cndmask_b32':
s2 = _fmt_src(inst.src2, 2, cdna=True)
return f"{name} {dst}, {s0}, {s1}, {s2}{cl}{om}"
return f"{name} {dst}, {s0}, {s1}, {s2}{opsel}{cl}{om}" if n == 3 else f"{name} {dst}, {s0}, {s1}{opsel}{cl}{om}"
def _disasm_vop3b(inst) -> str:
op_val = inst.op.value if hasattr(inst.op, 'value') else inst.op
name, cdna = inst.op_name.lower() or f'vop3b_op_{op_val}', _is_cdna(inst)
n = inst.num_srcs() or _num_srcs(inst)
regs = inst.canonical_op_regs
dregs, r0, r1, r2 = regs['d'], regs['s0'], regs['s1'], regs['s2']
s0, s1, s2 = _cdna_src(inst, inst.src0, inst.neg&1, n=r0), _cdna_src(inst, inst.src1, inst.neg&2, n=r1), _cdna_src(inst, inst.src2, inst.neg&4, n=r2)
# CDNA VOP3_SDST uses vdst field for sdst (but vdst adds 256), RDNA uses separate sdst field
sdst_val = getattr(inst, 'sdst', None)
if sdst_val is None and hasattr(inst, 'vdst'):
sdst_val = _unwrap(inst.vdst)
if sdst_val >= 256: sdst_val -= 256 # VGPRField adds 256, remove it for SGPR
# For CDNA VOP3_SDST (VOPC->VOP3), vdst is the scalar dest (sdst), there's no vdst output
if cdna and 'v_cmp' in name:
sdst = _fmt_sdst(sdst_val, 2, cdna=True)
cl, om = " clamp" if inst.clmp else "", _omod(inst.omod)
return f"{name} {sdst}, {s0}, {s1}{cl}{om}"
dst = _vreg(inst.vdst, dregs) if dregs > 1 else _vreg(inst.vdst)
sdst = _fmt_sdst(sdst_val, 2, cdna=cdna)
cl, om = " clamp" if inst.clmp else "", _omod(inst.omod)
if name in ('v_addc_co_u32', 'v_subb_co_u32', 'v_subbrev_co_u32'):
s2 = _fmt_src(inst.src2, 2, cdna=cdna)
return f"{name} {dst}, {sdst}, {s0}, {s1}, {s2}{cl}{om}" if n == 3 else f"{name} {dst}, {sdst}, {s0}, {s1}{cl}{om}"
def _disasm_cdna_vop3p(inst) -> str:
name, n = inst.op_name.lower(), inst.num_srcs() or 2
is_mfma = 'mfma' in name or 'smfmac' in name
is_accvgpr = 'accvgpr' in name
get_src = lambda v, sc: _lit(inst, v) if v == 255 else _fmt_src(v, sc, cdna=True)
# Handle accvgpr read/write (accumulator register operations)
if is_accvgpr:
src0_off = _unwrap(inst.src0)
vdst_off = _vi(inst.vdst)
if 'read' in name:
# v_accvgpr_read_b32 vN, aM - reads from accumulator to VGPR
return f"{name}_b32 v{vdst_off}, a{src0_off - 256 if src0_off >= 256 else src0_off}"
if 'write' in name:
# v_accvgpr_write_b32 aM, src - writes to accumulator from source
src = _lit(inst, inst.src0) if src0_off == 255 else (f"v{src0_off - 256}" if src0_off >= 256 else decode_src(src0_off, cdna=True))
return f"{name}_b32 a{vdst_off}, {src}"
# Handle v_mfma_ld_scale_b32 - special 2-operand format: v_mfma_ld_scale_b32 src0, src1
if 'ld_scale' in name:
src0, src1 = get_src(inst.src0, 1), get_src(inst.src1, 1)
mods = ([_fmt_bits("op_sel", inst.opsel, 2)] if inst.opsel else []) + \
([_fmt_bits("op_sel_hi", inst.opsel_hi, 2)] if inst.opsel_hi != 3 else [])
return f"{name} {src0}, {src1}{' ' + ' '.join(mods) if mods else ''}"
# Handle MFMA instructions with accumulator destinations
if is_mfma:
regs = inst.canonical_op_regs
dregs, r0, r1, r2 = regs['d'], regs['s0'], regs['s1'], regs['s2']
# Infer register counts from instruction name if not in operands table (e.g., v_mfma_f32_32x32x4_xf32)
if dregs == 1:
if '32x32' in name: dregs, r0, r1, r2 = 16, 2, 2, 16
elif '16x16' in name: dregs, r0, r1, r2 = 4, 2, 2, 4
# MFMA reuses VOP3P fields differently: clmp -> acc_cd (dest is acc), opsel_hi -> acc (src1/src2 are acc)
# acc field (bits 60-59): bit 0 = src2 is acc (always for MFMA), bit 1 = src1 is acc
acc = inst.opsel_hi # opsel_hi field maps to acc for MFMA
acc_cd = inst.clmp # clmp field maps to acc_cd for MFMA (dest is accumulator)
is_smfmac = 'smfmac' in name # SMFMAC has different operand semantics
# Format sources: src0 is always VGPR, src1/src2 depend on acc bits
def mfma_src(v, sc, is_acc):
v = _unwrap(v)
if v == 255: return _lit(inst, v)
if 128 <= v <= 208 or 240 <= v <= 248: return _lit(inst, v)
base = v - 256 if v >= 256 else v
if is_acc: return _areg(base, sc)
return _vreg(base, sc)
src0 = get_src(inst.src0, r0) # src0 is always VGPR
src1 = mfma_src(inst.src1, r1, acc & 2) # bit 1 = src1 is acc
# For SMFMAC, src2 is always a VGPR index (1 register), not accumulator
src2 = _vreg(inst.src2) if is_smfmac else mfma_src(inst.src2, r2, acc_cd)
dst = _areg(inst.vdst, dregs) if acc_cd else _vreg(inst.vdst, dregs)
# MFMA uses neg:[...] not neg_lo:[...], and doesn't support op_sel_hi or clamp
# Only f64 MFMA instructions support neg modifier
# f8f6f4 MFMA instructions support cbsz/blgp modifiers
mods = []
if 'f8f6f4' in name:
if inst.neg_hi: mods.append(f"cbsz:{inst.neg_hi}")
if inst.neg: mods.append(f"blgp:{inst.neg}")
elif inst.neg and 'f64' in name:
mods.append(_fmt_bits("neg", inst.neg, n))
return f"{name} {dst}, {src0}, {src1}, {src2}{' ' + ' '.join(mods) if mods else ''}"
# Standard VOP3P instructions
src0, src1, src2, dst = get_src(inst.src0, 1), get_src(inst.src1, 1), get_src(inst.src2, 1), _vreg(inst.vdst)
opsel_hi = inst.opsel_hi # CDNA VOP3P only has 2 bits for opsel_hi (no opsel_hi2)
opsel_hi_default = 3 # CDNA default is 0b11 (2 bits), not 0b111 like RDNA
mods = ([_fmt_bits("op_sel", inst.opsel, n)] if inst.opsel else []) + ([_fmt_bits("op_sel_hi", opsel_hi, n)] if opsel_hi != opsel_hi_default else []) + \
([_fmt_bits("neg_lo", inst.neg, n)] if inst.neg else []) + ([_fmt_bits("neg_hi", inst.neg_hi, n)] if inst.neg_hi else []) + (["clamp"] if inst.clmp else [])
return f"{name} {dst}, {src0}, {src1}, {src2}{' ' + ' '.join(mods) if mods else ''}" if n == 3 else f"{name} {dst}, {src0}, {src1}{' ' + ' '.join(mods) if mods else ''}"
def _disasm_mubuf(inst) -> str:
name = inst.op_name.lower()
# Determine vdata register count from instruction name
nregs = 4 if 'xyzw' in name else 3 if 'xyz' in name else 2 if 'xy' in name or 'x2' in name or 'f64' in name or 'dwordx2' in name else 1
vdata = _vreg(inst.vdata, nregs)
vaddr = _vreg(inst.vaddr) if inst.offen or inst.idxen else None
srsrc = str(inst.srsrc)
soffset_val = _unwrap(inst.soffset)
soffset = f"s{soffset_val}" if soffset_val < 128 else "off"
offset = f" offset:{inst.offset}" if inst.offset else ""
offen = " offen" if inst.offen else ""
idxen = " idxen" if inst.idxen else ""
lds = " lds" if inst.lds else ""
sc0 = " sc0" if inst.sc0 else ""
sc1 = " sc1" if inst.sc1 else ""
nt = " nt" if inst.nt else ""
# Handle special cases
if name in ('buffer_wbl2', 'buffer_inv'):
return f"{name}{sc0}{sc1}"
if vaddr:
return f"{name} {vdata}, {vaddr}, {srsrc}, {soffset}{offen}{idxen}{offset}{sc0}{nt}{sc1}{lds}"
return f"{name} {vdata}, off, {srsrc}, {soffset}{offset}{sc0}{nt}{sc1}{lds}"
_SDWA_SEL = {0: 'BYTE_0', 1: 'BYTE_1', 2: 'BYTE_2', 3: 'BYTE_3', 4: 'WORD_0', 5: 'WORD_1', 6: 'DWORD'}
def _disasm_vop1_sdwa(inst) -> str:
name = inst.op_name.lower().replace('_e32', '')
regs = inst.canonical_op_regs
dst = _vreg(inst.vdst, regs['d'])
# When s0=1, vsrc0 is SGPR/constant (VGPRField adds 256, so subtract it back)
if inst.s0 == 0: src0 = _vreg(inst.vsrc0, regs['s0'])
else:
raw = _unwrap(inst.vsrc0) - 256 # VGPRField adds 256
src0 = decode_src(raw, cdna=True) # handles SGPRs, constants, specials
src0_sel = _SDWA_SEL.get(inst.src0_sel, f'SEL{inst.src0_sel}')
mods = []
if inst.clmp: mods.append("clamp")
if inst.omod == 1: mods.append("mul:2")
elif inst.omod == 2: mods.append("mul:4")
elif inst.omod == 3: mods.append("div:2")
mods.append(f"src0_sel:{src0_sel}")
return f"{name}_sdwa {dst}, {src0} {' '.join(mods)}"
def _decode_dpp(dpp: int) -> str:
"""Decode DPP control value to string."""
if dpp < 0x100: return f"quad_perm:[{dpp&3},{(dpp>>2)&3},{(dpp>>4)&3},{(dpp>>6)&3}]"
if 0x100 <= dpp <= 0x10f: return f"row_shl:{dpp & 0xf}"
if 0x110 <= dpp <= 0x11f: return f"row_shr:{dpp & 0xf}"
if 0x120 <= dpp <= 0x12f: return f"row_ror:{dpp & 0xf}"
if dpp == 0x130: return "wave_shl:1"
if dpp == 0x134: return "wave_rol:1"
if dpp == 0x138: return "wave_shr:1"
if dpp == 0x13c: return "wave_ror:1"
if dpp == 0x140: return "row_mirror"
if dpp == 0x141: return "row_half_mirror"
if dpp == 0x142: return "row_bcast:15"
if dpp == 0x143: return "row_bcast:31"
if 0x150 <= dpp <= 0x15f: return f"row_newbcast:{dpp & 0xf}"
if 0x160 <= dpp <= 0x16f: return f"row_share:{dpp & 0xf}"
if 0x170 <= dpp <= 0x17f: return f"row_xmask:{dpp & 0xf}"
return f"dpp:{dpp:#x}"
def _disasm_vop1_dpp(inst) -> str:
name = inst.op_name.lower().replace('_e32', '')
regs = inst.canonical_op_regs
dst, src0 = _vreg(inst.vdst, regs['d']), _vreg(inst.vsrc0, regs['s0'])
dpp_str = _decode_dpp(inst.dpp)
mods = [dpp_str]
if inst.row_mask != 0xf: mods.append(f"row_mask:{inst.row_mask:#x}")
if inst.bank_mask != 0xf: mods.append(f"bank_mask:{inst.bank_mask:#x}")
if inst.bc: mods.append("bound_ctrl:1")
return f"{name}_dpp {dst}, {src0} {' '.join(mods)}"
def _disasm_vop2_sdwa(inst) -> str:
name, cdna = inst.op_name.lower().replace('_e32', ''), _is_cdna(inst)
regs = inst.canonical_op_regs
dst = _vreg(inst.vdst, regs['d'])
# When s0/s1=1, vsrc is SGPR/constant (VGPRField adds 256, so subtract it back)
src0 = _vreg(inst.vsrc0, regs['s0']) if inst.s0 == 0 else decode_src(_unwrap(inst.vsrc0) - 256, cdna)
src1 = _vreg(inst.vsrc1, regs['s1']) if inst.s1 == 0 else decode_src(_unwrap(inst.vsrc1) - 256, cdna)
src0_sel = _SDWA_SEL.get(inst.src0_sel, f'SEL{inst.src0_sel}')
src1_sel = _SDWA_SEL.get(inst.src1_sel, f'SEL{inst.src1_sel}')
mods = []
if inst.clmp: mods.append("clamp")
if inst.omod == 1: mods.append("mul:2")
elif inst.omod == 2: mods.append("mul:4")
elif inst.omod == 3: mods.append("div:2")
if inst.src0_sel != 6: mods.append(f"src0_sel:{src0_sel}")
if inst.src1_sel != 6: mods.append(f"src1_sel:{src1_sel}")
mods_str = ' '.join(mods) if mods else ""
# CDNA carry instructions and cndmask need vcc operands
if cdna and name in _VOP2_CARRY_OUT: return f"{name}_sdwa {dst}, vcc, {src0}, {src1} {mods_str}".strip()
if cdna and name in _VOP2_CARRY_INOUT: return f"{name}_sdwa {dst}, vcc, {src0}, {src1}, vcc {mods_str}".strip()
if cdna and name == 'v_cndmask_b32': return f"{name}_sdwa {dst}, {src0}, {src1}, vcc {mods_str}".strip()
return f"{name}_sdwa {dst}, {src0}, {src1} {mods_str}".strip()
def _disasm_vop2_dpp(inst) -> str:
name, cdna = inst.op_name.lower().replace('_e32', ''), _is_cdna(inst)
regs = inst.canonical_op_regs
dst, src0, src1 = _vreg(inst.vdst, regs['d']), _vreg(inst.vsrc0, regs['s0']), _vreg(inst.vsrc1, regs['s1'])
dpp_str = _decode_dpp(inst.dpp)
mods = [dpp_str]
if inst.row_mask != 0xf: mods.append(f"row_mask:{inst.row_mask:#x}")
if inst.bank_mask != 0xf: mods.append(f"bank_mask:{inst.bank_mask:#x}")
if inst.bc: mods.append("bound_ctrl:1")
# CDNA carry instructions and cndmask need vcc operands
if cdna and name in _VOP2_CARRY_OUT: return f"{name}_dpp {dst}, vcc, {src0}, {src1} {' '.join(mods)}"
if cdna and name in _VOP2_CARRY_INOUT: return f"{name}_dpp {dst}, vcc, {src0}, {src1}, vcc {' '.join(mods)}"
if cdna and name == 'v_cndmask_b32': return f"{name}_dpp {dst}, {src0}, {src1}, vcc {' '.join(mods)}"
return f"{name}_dpp {dst}, {src0}, {src1} {' '.join(mods)}"
def _disasm_vopc_sdwa(inst) -> str:
name = inst.op_name.lower().replace('_e32', '')
regs = inst.canonical_op_regs
sdst = _fmt_sdst(inst.sdst, 2, cdna=True)
src0 = _vreg(inst.vsrc0, regs['s0']) if getattr(inst, 's0', 0) == 0 else decode_src(_unwrap(inst.vsrc0) - 256, cdna=True)
src1 = _vreg(inst.vsrc1, regs['s1']) if getattr(inst, 's1', 0) == 0 else decode_src(_unwrap(inst.vsrc1) - 256, cdna=True)
src0_sel = _SDWA_SEL.get(inst.src0_sel, f'SEL{inst.src0_sel}')
src1_sel = _SDWA_SEL.get(inst.src1_sel, f'SEL{inst.src1_sel}')
mods = []
if inst.src0_sel != 6: mods.append(f"src0_sel:{src0_sel}")
if inst.src1_sel != 6: mods.append(f"src1_sel:{src1_sel}")
return f"{name}_sdwa {sdst}, {src0}, {src1} {' '.join(mods)}".strip()
def _disasm_vop3px2(inst) -> str:
"""VOP3PX2 disassembler for scaled MFMA instructions."""
name = inst.op_name.lower()
regs = inst.canonical_op_regs
dregs, r2 = regs['d'], regs['s2']
# F8F6F4 MFMA: CBSZ selects matrix A format, BLGP selects matrix B format
# VGPRs: FP8/BF8(0,1)=8, FP6/BF6(2,3)=6, FP4(4)=4
vgprs = {0: 8, 1: 8, 2: 6, 3: 6, 4: 4}
r0, r1 = vgprs.get(inst.cbsz, 8), vgprs.get(inst.blgp, 8)
def mfma_src(v, sc, is_acc):
v = _unwrap(v)
if v == 255: return _lit(inst, v)
base = v - 256 if v >= 256 else v
return _areg(base, sc) if is_acc else _vreg(base, sc)
src0, src1, src2 = mfma_src(inst.src0, r0, False), mfma_src(inst.src1, r1, inst.acc & 2), mfma_src(inst.src2, r2, inst.acc_cd)
dst = _areg(inst.vdst, dregs) if inst.acc_cd else _vreg(inst.vdst, dregs)
scale_src0, scale_src1 = _vreg(inst.scale_src0), _vreg(inst.scale_src1)
mods = []
if inst.opsel: mods.append(_fmt_bits("op_sel", inst.opsel, 3))
if inst.opsel_hi != 0: mods.append(_fmt_bits("op_sel_hi", inst.opsel_hi, 3))
if inst.neg: mods.append(_fmt_bits("neg", inst.neg, 3))
if inst.cbsz: mods.append(f"cbsz:{inst.cbsz}")
if inst.blgp: mods.append(f"blgp:{inst.blgp}")
return f"{name} {dst}, {src0}, {src1}, {src2}, {scale_src0}, {scale_src1}{' ' + ' '.join(mods) if mods else ''}"
DISASM_HANDLERS.update({CDNA_VOP1: _disasm_vop1, CDNA_VOP1_LIT: _disasm_vop1,
CDNA_VOP1_SDWA: _disasm_vop1_sdwa, CDNA_VOP1_DPP16: _disasm_vop1_dpp,
CDNA_VOP2: _disasm_vop2, CDNA_VOP2_LIT: _disasm_vop2,
CDNA_VOP2_SDWA: _disasm_vop2_sdwa, CDNA_VOP2_DPP16: _disasm_vop2_dpp,
CDNA_VOPC: _disasm_vopc, CDNA_VOPC_LIT: _disasm_vopc, CDNA_VOPC_SDWA_SDST: _disasm_vopc_sdwa,
CDNA_SOP1: _disasm_sop1, CDNA_SOP1_LIT: _disasm_sop1, CDNA_SOP2: _disasm_sop2, CDNA_SOP2_LIT: _disasm_sop2,
CDNA_SOPC: _disasm_sopc, CDNA_SOPC_LIT: _disasm_sopc, CDNA_SOPK: _disasm_sopk, CDNA_SOPK_LIT: _disasm_sopk, CDNA_SOPP: _disasm_sopp,
CDNA_SMEM: _disasm_smem, CDNA_DS: _disasm_ds, CDNA_FLAT: _disasm_flat, CDNA_GLOBAL: _disasm_flat, CDNA_SCRATCH: _disasm_flat,
CDNA_VOP3: _disasm_vop3a, CDNA_VOP3_SDST: _disasm_vop3b, CDNA_VOP3SD: _disasm_vop3b, CDNA_VOP3P: _disasm_cdna_vop3p,
CDNA_MUBUF: _disasm_mubuf, CDNA_VOP3PX2: _disasm_vop3px2})
+601 -384
View File
File diff suppressed because it is too large Load Diff
+369 -128
View File
@@ -2,43 +2,17 @@
# mypy: ignore-errors
from __future__ import annotations
import ctypes, functools
from enum import IntEnum
from tinygrad.helpers import DEBUG, colored, ansilen
from tinygrad.runtime.autogen import hsa
from extra.assembly.amd.dsl import Inst, NULL, SCC, VCC_LO, VCC_HI, EXEC_LO, EXEC_HI, v, s
from extra.assembly.amd.pcode import _f32, _i32, _sext, _f16, _i16, _f64, _i64
from extra.assembly.amd.decode import decode_inst
from extra.assembly.amd.pcode import compile_pseudocode
from extra.assembly.amd.autogen.rdna3.str_pcode import PCODE
from extra.assembly.amd.autogen.rdna3.ins import (SOP1, SOP2, SOPC, SOPK, SOPP, SMEM, VOP1, VOP2, VOP3, VOP3SD, VOP3P, VOPC, DS, FLAT, GLOBAL, SCRATCH, VOPD,
from extra.assembly.amd.dsl import Inst, unwrap, FLOAT_ENC, MASK32, MASK64, _f32, _i32, _sext, _f16, _i16, _f64, _i64, SrcEnum
from extra.assembly.amd.pcode import Reg, compile_pseudocode
from extra.assembly.amd.asm import detect_format, disasm
from extra.assembly.amd.autogen.rdna3.str_pcode import PSEUDOCODE_STRINGS
from extra.assembly.amd.autogen.rdna3.ins import (SOP1, SOP2, SOPC, SOPK, SOPP, SMEM, VOP1, VOP2, VOP3, VOP3SD, VOP3P, VOPC, DS, FLAT, VOPD,
SOP1Op, SOP2Op, SOPCOp, SOPKOp, SOPPOp, SMEMOp, VOP1Op, VOP2Op, VOP3Op, VOP3SDOp, VOP3POp, VOPCOp, DSOp, FLATOp, GLOBALOp, SCRATCHOp, VOPDOp)
# Constants and helpers defined locally (not imported from dsl.py)
MASK32, MASK64 = 0xFFFFFFFF, 0xFFFFFFFFFFFFFFFF
FLOAT_ENC = {0.5: 240, -0.5: 241, 1.0: 242, -1.0: 243, 2.0: 244, -2.0: 245, 4.0: 246, -4.0: 247}
class SGPRArray:
"""SGPR array indexed by Reg or int."""
__slots__ = ('_data',)
def __init__(self, size: int): self._data = [0] * size
def __getitem__(self, key): return self._data[getattr(key, 'offset', key)]
def __setitem__(self, key, val): self._data[getattr(key, 'offset', key)] = val
def __len__(self): return len(self._data)
def __iter__(self): return iter(self._data)
class VGPRLane:
"""Single lane of VGPRs indexed by Reg (offset 256-511) or int (0-255)."""
__slots__ = ('_data',)
def __init__(self, size: int): self._data = [0] * size
def __getitem__(self, key):
i = getattr(key, 'offset', key)
return self._data[i - 256 if i >= 256 else i]
def __setitem__(self, key, val):
i = getattr(key, 'offset', key)
self._data[i - 256 if i >= 256 else i] = val
def __len__(self): return len(self._data)
def __iter__(self): return iter(self._data)
WAVE_SIZE, SGPR_COUNT, VGPR_COUNT = 32, 128, 256
VCC_LO, VCC_HI, NULL, EXEC_LO, EXEC_HI, SCC = SrcEnum.VCC_LO, SrcEnum.VCC_HI, SrcEnum.NULL, SrcEnum.EXEC_LO, SrcEnum.EXEC_HI, SrcEnum.SCC
# Inline constants for src operands 128-254. Build tables for f32, f16, and f64 formats.
_FLOAT_CONSTS = {v: k for k, v in FLOAT_ENC.items()} | {248: 0.15915494309189535} # INV_2PI
@@ -53,8 +27,8 @@ _INLINE_CONSTS_F64 = _build_inline_consts(MASK64, _i64)
# Helper: extract/write 16-bit half from/to 32-bit value
def _src16(raw: int, is_hi: bool) -> int: return ((raw >> 16) & 0xffff) if is_hi else (raw & 0xffff)
def _dst16(cur: int, val: int, is_hi: bool) -> int: return (cur & 0x0000ffff) | ((val & 0xffff) << 16) if is_hi else (cur & 0xffff0000) | (val & 0xffff)
def _vgpr_hi(src) -> bool: return src.offset >= 256 and ((src.offset - 256) & 0x80) != 0
def _vgpr_masked(src): return v[(src.offset - 256) & 0x7f] if src.offset >= 256 else src
def _vgpr_hi(src: int) -> bool: return src >= 256 and ((src - 256) & 0x80) != 0
def _vgpr_masked(src: int) -> int: return ((src - 256) & 0x7f) + 256 if src >= 256 else src
# VOP3 source modifier: apply abs/neg to value
def _mod_src(val: int, idx: int, neg: int, abs_: int, is64: bool = False) -> int:
@@ -66,10 +40,8 @@ def _mod_src(val: int, idx: int, neg: int, abs_: int, is64: bool = False) -> int
# Read source operand with VOP3 modifiers
def _read_src(st, inst, src, idx: int, lane: int, neg: int, abs_: int, opsel: int) -> int:
if src is None: return 0
src_off = src.offset
src_bits = inst.canonical_op_bits[f's{idx}']
literal, is_src_64, is_src_16 = inst._literal, src_bits == 64, src_bits == 16
if is_src_64: return _mod_src(st.rsrc64(src, lane, literal), idx, neg, abs_, is64=True)
literal, regs, is_src_16 = inst._literal, inst.src_regs(idx), inst.is_src_16(idx)
if regs == 2: return _mod_src(st.rsrc64(src, lane, literal), idx, neg, abs_, is64=True)
if isinstance(inst, VOP3P):
opsel_hi = inst.opsel_hi | (inst.opsel_hi2 << 2)
if 'FMA_MIX' in inst.op_name:
@@ -83,13 +55,13 @@ def _read_src(st, inst, src, idx: int, lane: int, neg: int, abs_: int, opsel: in
lo = _src16(raw, opsel & (1 << idx)) ^ (0x8000 if neg & (1 << idx) else 0)
return (hi << 16) | lo
if is_src_16 and isinstance(inst, VOP3):
raw = st.rsrc_f16(src, lane, literal) if 128 <= src_off < 255 else st.rsrc(src, lane, literal)
raw = st.rsrc_f16(src, lane, literal) if 128 <= src < 255 else st.rsrc(src, lane, literal)
val = _src16(raw, bool(opsel & (1 << idx)))
if abs_ & (1 << idx): val &= 0x7fff
if neg & (1 << idx): val ^= 0x8000
return val
if is_src_16 and isinstance(inst, (VOP1, VOP2, VOPC)):
if src_off >= 256: return _src16(_mod_src(st.rsrc(_vgpr_masked(src), lane, literal), idx, neg, abs_), _vgpr_hi(src))
if src >= 256: return _src16(_mod_src(st.rsrc(_vgpr_masked(src), lane, literal), idx, neg, abs_), _vgpr_hi(src))
return _mod_src(st.rsrc_f16(src, lane, literal), idx, neg, abs_) & 0xffff
return _mod_src(st.rsrc(src, lane, literal), idx, neg, abs_)
@@ -101,12 +73,11 @@ def _op_ndwords(name: str) -> int:
return 1
# Helper: build multi-dword int from consecutive VGPRs
def _vgpr_read(V: VGPRLane, reg, ndwords: int) -> int:
return sum(V[reg + i] << (32 * i) for i in range(ndwords))
def _vgpr_read(V: list, base: int, ndwords: int) -> int: return sum(V[base + i] << (32 * i) for i in range(ndwords))
# Helper: write multi-dword value to consecutive VGPRs
def _vgpr_write(V: VGPRLane, reg, val: int, ndwords: int):
for i in range(ndwords): V[reg + i] = (val >> (32 * i)) & MASK32
def _vgpr_write(V: list, base: int, val: int, ndwords: int):
for i in range(ndwords): V[base + i] = (val >> (32 * i)) & MASK32
# Memory access
_valid_mem_ranges: list[tuple[int, int]] = []
@@ -158,19 +129,19 @@ SMEM_DST_COUNT = {SMEMOp.S_LOAD_B32: 1, SMEMOp.S_LOAD_B64: 2, SMEMOp.S_LOAD_B128
# VOPD op -> VOP3 op mapping (VOPD is dual-issue of VOP1/VOP2 ops, use VOP3 enums for pseudocode lookup)
_VOPD_TO_VOP = {
VOPDOp.V_DUAL_FMAC_F32: VOP3Op.V_FMAC_F32_E64, VOPDOp.V_DUAL_FMAAK_F32: VOP2Op.V_FMAAK_F32_E32, VOPDOp.V_DUAL_FMAMK_F32: VOP2Op.V_FMAMK_F32_E32,
VOPDOp.V_DUAL_MUL_F32: VOP3Op.V_MUL_F32_E64, VOPDOp.V_DUAL_ADD_F32: VOP3Op.V_ADD_F32_E64, VOPDOp.V_DUAL_SUB_F32: VOP3Op.V_SUB_F32_E64,
VOPDOp.V_DUAL_SUBREV_F32: VOP3Op.V_SUBREV_F32_E64, VOPDOp.V_DUAL_MUL_DX9_ZERO_F32: VOP3Op.V_MUL_DX9_ZERO_F32_E64,
VOPDOp.V_DUAL_MOV_B32: VOP3Op.V_MOV_B32_E64, VOPDOp.V_DUAL_CNDMASK_B32: VOP3Op.V_CNDMASK_B32_E64,
VOPDOp.V_DUAL_MAX_F32: VOP3Op.V_MAX_F32_E64, VOPDOp.V_DUAL_MIN_F32: VOP3Op.V_MIN_F32_E64,
VOPDOp.V_DUAL_ADD_NC_U32: VOP3Op.V_ADD_NC_U32_E64, VOPDOp.V_DUAL_LSHLREV_B32: VOP3Op.V_LSHLREV_B32_E64, VOPDOp.V_DUAL_AND_B32: VOP3Op.V_AND_B32_E64,
VOPDOp.V_DUAL_FMAC_F32: VOP3Op.V_FMAC_F32, VOPDOp.V_DUAL_FMAAK_F32: VOP2Op.V_FMAAK_F32, VOPDOp.V_DUAL_FMAMK_F32: VOP2Op.V_FMAMK_F32,
VOPDOp.V_DUAL_MUL_F32: VOP3Op.V_MUL_F32, VOPDOp.V_DUAL_ADD_F32: VOP3Op.V_ADD_F32, VOPDOp.V_DUAL_SUB_F32: VOP3Op.V_SUB_F32,
VOPDOp.V_DUAL_SUBREV_F32: VOP3Op.V_SUBREV_F32, VOPDOp.V_DUAL_MUL_DX9_ZERO_F32: VOP3Op.V_MUL_DX9_ZERO_F32,
VOPDOp.V_DUAL_MOV_B32: VOP3Op.V_MOV_B32, VOPDOp.V_DUAL_CNDMASK_B32: VOP3Op.V_CNDMASK_B32,
VOPDOp.V_DUAL_MAX_F32: VOP3Op.V_MAX_F32, VOPDOp.V_DUAL_MIN_F32: VOP3Op.V_MIN_F32,
VOPDOp.V_DUAL_ADD_NC_U32: VOP3Op.V_ADD_NC_U32, VOPDOp.V_DUAL_LSHLREV_B32: VOP3Op.V_LSHLREV_B32, VOPDOp.V_DUAL_AND_B32: VOP3Op.V_AND_B32,
}
class WaveState:
__slots__ = ('sgpr', 'vgpr', 'scc', 'pc', '_pend_sgpr', 'lds', 'n_lanes')
def __init__(self, lds: LDSMem | None = None, n_lanes: int = WAVE_SIZE):
self.sgpr, self.vgpr = SGPRArray(SGPR_COUNT), [VGPRLane(VGPR_COUNT) for _ in range(WAVE_SIZE)]
self.sgpr, self.vgpr = [0] * SGPR_COUNT, [[0] * VGPR_COUNT for _ in range(WAVE_SIZE)]
self.sgpr[EXEC_LO], self.scc, self.pc, self._pend_sgpr, self.lds, self.n_lanes = 0xffffffff, 0, 0, {}, lds, n_lanes
@property
@@ -182,35 +153,26 @@ class WaveState:
@exec_mask.setter
def exec_mask(self, v: int): self.sgpr[EXEC_LO], self.sgpr[EXEC_HI] = v & MASK32, (v >> 32) & MASK32
def rsgpr(self, reg) -> int:
if reg == NULL: return 0
if reg == SCC: return self.scc
return self.sgpr[reg]
def wsgpr(self, reg, v: int):
if reg != NULL: self.sgpr[reg] = v & MASK32
def rsgpr64(self, reg) -> int:
off = reg.offset
return self.sgpr._data[off] | (self.sgpr._data[off + 1] << 32)
def wsgpr64(self, reg, v: int):
off = reg.offset
self.sgpr._data[off] = v & MASK32; self.sgpr._data[off + 1] = (v >> 32) & MASK32
def rsgpr(self, i: int) -> int: return 0 if i == NULL else self.scc if i == SCC else self.sgpr[i] if i < SGPR_COUNT else 0
def wsgpr(self, i: int, v: int):
if i < SGPR_COUNT and i != NULL: self.sgpr[i] = v & MASK32
def rsgpr64(self, i: int) -> int: return self.rsgpr(i) | (self.rsgpr(i+1) << 32)
def wsgpr64(self, i: int, v: int): self.wsgpr(i, v & MASK32); self.wsgpr(i+1, (v >> 32) & MASK32)
def _rsrc_base(self, reg, lane: int, consts, literal: int):
off = reg.offset
if off < SGPR_COUNT: return self.sgpr._data[off]
if off == SCC.offset: return self.scc
if off < 255: return consts[off - 128]
if off == 255: return literal
return self.vgpr[lane]._data[off - 256] if off <= 511 else 0
def rsrc(self, reg, lane: int, literal: int = 0) -> int: return self._rsrc_base(reg, lane, _INLINE_CONSTS, literal)
def rsrc_f16(self, reg, lane: int, literal: int = 0) -> int: return self._rsrc_base(reg, lane, _INLINE_CONSTS_F16, literal)
def rsrc64(self, reg, lane: int, literal: int = 0) -> int:
off = reg.offset
if 128 <= off < 255: return _INLINE_CONSTS_F64[off - 128]
if off == 255: return literal << 32 # 32-bit literal forms upper 32 bits of 64-bit value
return self.rsrc(reg, lane, literal) | ((self.rsrc(reg + 1, lane, literal) if off < VCC_LO.offset or 256 <= off <= 511 else 0) << 32)
def _rsrc_base(self, v: int, lane: int, consts, literal: int):
if v < SGPR_COUNT: return self.sgpr[v]
if v == SCC: return self.scc
if v < 255: return consts[v - 128]
if v == 255: return literal
return self.vgpr[lane][v - 256] if v <= 511 else 0
def rsrc(self, v: int, lane: int, literal: int = 0) -> int: return self._rsrc_base(v, lane, _INLINE_CONSTS, literal)
def rsrc_f16(self, v: int, lane: int, literal: int = 0) -> int: return self._rsrc_base(v, lane, _INLINE_CONSTS_F16, literal)
def rsrc64(self, v: int, lane: int, literal: int = 0) -> int:
if 128 <= v < 255: return _INLINE_CONSTS_F64[v - 128]
if v == 255: return literal # literal is already shifted in from_bytes for 64-bit ops
return self.rsrc(v, lane, literal) | ((self.rsrc(v+1, lane, literal) if v < VCC_LO or 256 <= v <= 511 else 0) << 32)
def pend_sgpr_lane(self, reg, lane: int, val: int):
def pend_sgpr_lane(self, reg: int, lane: int, val: int):
if reg not in self._pend_sgpr: self._pend_sgpr[reg] = 0
if val: self._pend_sgpr[reg] |= (1 << lane)
def commit_pends(self):
@@ -235,8 +197,8 @@ def exec_scalar(st: WaveState, inst: Inst):
# SMEM: memory loads
if isinstance(inst, SMEM):
addr = st.rsgpr64(inst.sbase) + _sext(inst.offset, 21)
if inst.soffset != NULL: addr += st.rsrc(inst.soffset, 0, inst._literal)
addr = st.rsgpr64(inst.sbase * 2) + _sext(inst.offset, 21)
if inst.soffset not in (NULL, 0x7f): addr += st.rsrc(inst.soffset, 0, inst._literal)
result = inst._fn(GlobalMem, addr & MASK64)
if 'SDATA' in result:
sdata = result['SDATA']
@@ -244,11 +206,11 @@ def exec_scalar(st: WaveState, inst: Inst):
st.pc += inst._words
return 0
# Build context - use canonical_op_bits to determine operand sizes
# Build context - use inst methods to determine operand sizes
literal = inst._literal
s0 = st.rsrc64(ssrc0, 0, literal) if inst.canonical_op_bits['s0'] == 64 else (st.rsrc(ssrc0, 0, literal) if not isinstance(inst, (SOPK, SOPP)) else (st.rsgpr(inst.sdst) if isinstance(inst, SOPK) else 0))
s1 = st.rsrc64(inst.ssrc1, 0, literal) if inst.canonical_op_bits['s1'] == 64 else (st.rsrc(inst.ssrc1, 0, literal) if isinstance(inst, (SOP2, SOPC)) else inst.simm16 if isinstance(inst, SOPK) else 0)
d0 = st.rsgpr64(sdst) if inst.canonical_op_bits['d'] == 64 and sdst is not None else (st.rsgpr(sdst) if sdst is not None else 0)
s0 = st.rsrc64(ssrc0, 0, literal) if inst.is_src_64(0) else (st.rsrc(ssrc0, 0, literal) if not isinstance(inst, (SOPK, SOPP)) else (st.rsgpr(inst.sdst) if isinstance(inst, SOPK) else 0))
s1 = st.rsrc64(inst.ssrc1, 0, literal) if inst.is_src_64(1) else (st.rsrc(inst.ssrc1, 0, literal) if isinstance(inst, (SOP2, SOPC)) else inst.simm16 if isinstance(inst, SOPK) else 0)
d0 = st.rsgpr64(sdst) if inst.dst_regs() == 2 and sdst is not None else (st.rsgpr(sdst) if sdst is not None else 0)
literal = inst.simm16 if isinstance(inst, (SOPK, SOPP)) else inst._literal
# Call compiled function with int parameters
@@ -256,7 +218,7 @@ def exec_scalar(st: WaveState, inst: Inst):
# Apply results (already int values)
if sdst is not None and 'D0' in result:
(st.wsgpr64 if inst.canonical_op_bits['d'] == 64 else st.wsgpr)(sdst, result['D0'])
(st.wsgpr64 if inst.dst_regs() == 2 else st.wsgpr)(sdst, result['D0'])
if 'SCC' in result: st.scc = result['SCC'] & 1
if 'EXEC' in result: st.exec_mask = result['EXEC']
if 'PC' in result:
@@ -272,25 +234,24 @@ def exec_scalar(st: WaveState, inst: Inst):
# VECTOR INSTRUCTIONS
# ═══════════════════════════════════════════════════════════════════════════════
def exec_vopd(st: WaveState, inst, V: VGPRLane, lane: int) -> None:
def exec_vopd(st: WaveState, inst, V: list, lane: int) -> None:
"""VOPD: dual-issue, execute two ops simultaneously (read all inputs before writes)."""
literal, vdstx = inst._literal, inst.vdstx
vdsty = v[(inst.vdsty << 1) | ((inst.vdstx.offset & 1) ^ 1)] # vdsty is raw int from VDSTYField.decode
literal, vdstx, vdsty = inst._literal, inst.vdstx, (inst.vdsty << 1) | ((inst.vdstx & 1) ^ 1)
sx0, sx1, dx, sy0, sy1, dy = st.rsrc(inst.srcx0, lane, literal), V[inst.vsrcx1], V[vdstx], st.rsrc(inst.srcy0, lane, literal), V[inst.vsrcy1], V[vdsty]
V[vdstx] = inst._fnx(sx0, sx1, 0, dx, st.scc, st.vcc, lane, st.exec_mask, literal, None)['D0']
V[vdsty] = inst._fny(sy0, sy1, 0, dy, st.scc, st.vcc, lane, st.exec_mask, literal, None)['D0']
def exec_flat(st: WaveState, inst, V: VGPRLane, lane: int) -> None:
def exec_flat(st: WaveState, inst, V: list, lane: int) -> None:
"""FLAT/GLOBAL/SCRATCH memory ops."""
ndwords = _op_ndwords(inst.op_name)
addr = V[inst.addr] | (V[inst.addr + 1] << 32)
ADDR = (st.rsgpr64(inst.saddr) + V[inst.addr] + _sext(inst.offset, 13)) & MASK64 if inst.saddr != NULL else (addr + _sext(inst.offset, 13)) & MASK64
ADDR = (st.rsgpr64(inst.saddr) + V[inst.addr] + _sext(inst.offset, 13)) & MASK64 if inst.saddr not in (NULL, 0x7f) else (addr + _sext(inst.offset, 13)) & MASK64
vdata_src = inst.vdst if 'LOAD' in inst.op_name else inst.data
result = inst._fn(GlobalMem, ADDR, _vgpr_read(V, vdata_src, ndwords), V[inst.vdst])
if 'VDATA' in result: _vgpr_write(V, inst.vdst, result['VDATA'], ndwords)
if 'RETURN_DATA' in result: _vgpr_write(V, inst.vdst, result['RETURN_DATA'], ndwords)
def exec_ds(st: WaveState, inst, V: VGPRLane, lane: int) -> None:
def exec_ds(st: WaveState, inst, V: list, lane: int) -> None:
"""DS (LDS) memory ops."""
ndwords = _op_ndwords(inst.op_name)
data0, data1 = _vgpr_read(V, inst.data0, ndwords), _vgpr_read(V, inst.data1, ndwords) if inst.data1 is not None else 0
@@ -298,59 +259,52 @@ def exec_ds(st: WaveState, inst, V: VGPRLane, lane: int) -> None:
if 'RETURN_DATA' in result and ('_RTN' in inst.op_name or '_LOAD' in inst.op_name):
_vgpr_write(V, inst.vdst, result['RETURN_DATA'], ndwords * 2 if '_2ADDR_' in inst.op_name else ndwords)
def exec_vop(st: WaveState, inst: Inst, V: VGPRLane, lane: int) -> None:
def exec_vop(st: WaveState, inst: Inst, V: list, lane: int) -> None:
"""VOP1/VOP2/VOP3/VOP3SD/VOP3P/VOPC: standard ALU ops."""
is_dst_16 = inst.canonical_op_bits['d'] == 16
if isinstance(inst, VOP3P):
src0, src1, src2, vdst, dst_hi = inst.src0, inst.src1, inst.src2, inst.vdst, False
neg, abs_, opsel = inst.neg, 0, inst.opsel
elif isinstance(inst, VOP1):
src0, src1, src2, vdst = inst.src0, None, None, inst.vdst
neg, abs_, opsel, dst_hi = 0, 0, 0, (inst.vdst.offset & 0x80) != 0 and is_dst_16
if is_dst_16: vdst = v[inst.vdst.offset & 0x7f]
src0, src1, src2, vdst = inst.src0, None, None, inst.vdst & 0x7f if inst.is_dst_16() else inst.vdst
neg, abs_, opsel, dst_hi = 0, 0, 0, (inst.vdst & 0x80) != 0 and inst.is_dst_16()
elif isinstance(inst, VOP2):
src0, src1, src2, vdst = inst.src0, inst.vsrc1, None, inst.vdst
neg, abs_, opsel, dst_hi = 0, 0, 0, (inst.vdst.offset & 0x80) != 0 and is_dst_16
if is_dst_16: vdst = v[inst.vdst.offset & 0x7f]
src0, src1, src2, vdst = inst.src0, inst.vsrc1 + 256, None, inst.vdst & 0x7f if inst.is_dst_16() else inst.vdst
neg, abs_, opsel, dst_hi = 0, 0, 0, (inst.vdst & 0x80) != 0 and inst.is_dst_16()
elif isinstance(inst, (VOP3, VOP3SD)):
src0, src1, src2, vdst = inst.src0, inst.src1, (None if isinstance(inst, VOP3) and inst.op.value < 256 else inst.src2), inst.vdst
neg, abs_, opsel, dst_hi = (inst.neg, inst.abs, inst.opsel, False) if isinstance(inst, VOP3) else (0, 0, 0, False)
elif isinstance(inst, VOPC):
src0, src1, src2, vdst, neg, abs_, opsel, dst_hi = inst.src0, inst.vsrc1, None, VCC_LO, 0, 0, 0, False
src0, src1, src2, vdst, neg, abs_, opsel, dst_hi = inst.src0, inst.vsrc1 + 256, None, VCC_LO, 0, 0, 0, False
else:
raise NotImplementedError(f"exec_vop: unhandled instruction type {type(inst).__name__}")
s0 = _read_src(st, inst, src0, 0, lane, neg, abs_, opsel)
s1 = _read_src(st, inst, src1, 1, lane, neg, abs_, opsel)
s2 = _read_src(st, inst, src2, 2, lane, neg, abs_, opsel)
if isinstance(inst, VOP2) and is_dst_16: d0 = _src16(V[vdst], dst_hi)
elif inst.canonical_op_bits['d'] == 64: d0 = V[vdst] | (V[vdst + 1] << 32)
if isinstance(inst, VOP2) and inst.is_16bit(): d0 = _src16(V[vdst], dst_hi)
elif inst.dst_regs() == 2: d0 = V[vdst] | (V[vdst + 1] << 32)
else: d0 = V[vdst]
if isinstance(inst, VOP3SD) and 'CO_CI' in inst.op_name: vcc_for_fn = st.rsgpr64(inst.src2)
elif isinstance(inst, VOP3) and inst.op in (VOP3Op.V_CNDMASK_B32_E64, VOP3Op.V_CNDMASK_B16) and src2 is not None and src2.offset < 256: vcc_for_fn = st.rsgpr64(src2)
elif isinstance(inst, VOP3) and inst.op in (VOP3Op.V_CNDMASK_B32, VOP3Op.V_CNDMASK_B16) and src2 is not None and src2 < 256: vcc_for_fn = st.rsgpr64(src2)
else: vcc_for_fn = st.vcc
src0_off = src0.offset if src0 is not None else 0
src0_idx = (src0_off - 256) if src0_off >= 256 else src0_off
vdst_off = vdst.offset
src0_idx = (src0 - 256) if src0 is not None and src0 >= 256 else (src0 if src0 is not None else 0)
extra_kwargs = {'opsel': opsel, 'opsel_hi': inst.opsel_hi | (inst.opsel_hi2 << 2)} if isinstance(inst, VOP3P) and 'FMA_MIX' in inst.op_name else {}
result = inst._fn(s0, s1, s2, d0, st.scc, vcc_for_fn, lane, st.exec_mask, inst._literal, st.vgpr, src0_idx, vdst_off, **extra_kwargs)
result = inst._fn(s0, s1, s2, d0, st.scc, vcc_for_fn, lane, st.exec_mask, inst._literal, st.vgpr, src0_idx, vdst, **extra_kwargs)
# Check if this is a VOPC instruction (either standalone VOPC or VOP3 with VOPC opcode)
is_vopc = isinstance(inst.op, VOPCOp) or (isinstance(inst, VOP3) and inst.op.value < 256)
if 'VCC' in result:
if isinstance(inst, VOP3SD): st.pend_sgpr_lane(inst.sdst, lane, (result['VCC'] >> lane) & 1)
elif isinstance(inst, VOP2) and 'CO_CI' in inst.op_name: st.pend_sgpr_lane(VCC_LO, lane, (result['VCC'] >> lane) & 1)
elif is_vopc: st.pend_sgpr_lane(vdst, lane, (result['VCC'] >> lane) & 1) # vdst is VCC_LO for VOPC
else: st.pend_sgpr_lane(VCC_LO, lane, (result['VCC'] >> lane) & 1)
else: st.pend_sgpr_lane(VCC_LO if isinstance(inst, VOP2) and 'CO_CI' in inst.op_name else vdst, lane, (result['VCC'] >> lane) & 1)
if 'EXEC' in result:
st.pend_sgpr_lane(EXEC_LO, lane, (result['EXEC'] >> lane) & 1)
elif is_vopc:
st.pend_sgpr_lane(vdst, lane, (result['D0'] >> lane) & 1)
if not is_vopc:
d0_val = result['D0']
if inst.canonical_op_bits['d'] == 64: V[vdst], V[vdst + 1] = d0_val & MASK32, (d0_val >> 32) & MASK32
elif not isinstance(inst, VOP3P) and is_dst_16: V[vdst] = _dst16(V[vdst], d0_val, bool(opsel & 8) if isinstance(inst, VOP3) else dst_hi)
if inst.dst_regs() == 2: V[vdst], V[vdst + 1] = d0_val & MASK32, (d0_val >> 32) & MASK32
elif not isinstance(inst, VOP3P) and inst.is_dst_16(): V[vdst] = _dst16(V[vdst], d0_val, bool(opsel & 8) if isinstance(inst, VOP3) else dst_hi)
else: V[vdst] = d0_val & MASK32
# ═══════════════════════════════════════════════════════════════════════════════
@@ -359,7 +313,7 @@ def exec_vop(st: WaveState, inst: Inst, V: VGPRLane, lane: int) -> None:
def exec_wmma(st: WaveState, inst, op: VOP3POp) -> None:
"""Execute WMMA instruction - 16x16x16 matrix multiply across the wave."""
src0, src1, src2, vdst = inst.src0.offset, inst.src1.offset, inst.src2.offset, inst.vdst.offset
src0, src1, src2, vdst = inst.src0, inst.src1, inst.src2, inst.vdst
# Read 16x16 f16 matrix from 16 lanes × 8 VGPRs (2 f16 per VGPR)
def read_f16_mat(src):
return [f for l in range(16) for r in range(8) for v in [st.vgpr[l][src-256+r] if src >= 256 else st.rsgpr(src+r)] for f in [_f16(v&0xffff), _f16((v>>16)&0xffff)]]
@@ -371,9 +325,297 @@ def exec_wmma(st: WaveState, inst, op: VOP3POp) -> None:
# Write result - f16 packed or f32
if op == VOP3POp.V_WMMA_F16_16X16X16_F16:
for i in range(0, 256, 2):
st.vgpr[(i//2) % 32][vdst - 256 + (i//2)//32] = ((_i16(mat_d[i+1]) & 0xffff) << 16) | (_i16(mat_d[i]) & 0xffff)
st.vgpr[(i//2) % 32][vdst + (i//2)//32] = ((_i16(mat_d[i+1]) & 0xffff) << 16) | (_i16(mat_d[i]) & 0xffff)
else:
for i in range(256): st.vgpr[i % 32][vdst - 256 + i//32] = _i32(mat_d[i])
for i in range(256): st.vgpr[i % 32][vdst + i//32] = _i32(mat_d[i])
# SQTT TRACING
# ═══════════════════════════════════════════════════════════════════════════════
WAVESTART_TO_INST_CYCLES = 32
SNOP_EXTRA_DELAY_MIN, SNOP_EXTRA_DELAY_MAX = 11, 22 # s_nop(11-22) has +4 penalty
SNOP_EXTRA_DELAY_CYCLES = 4
from extra.assembly.amd.sqtt import WAVESTART, WAVEEND, IMMEDIATE, VALUINST, ALUEXEC, AluSrc
def _get_src_vgprs(inst: Inst) -> list[int]:
if isinstance(inst, VOP1): return [inst.src0 - 256] if inst.src0 >= 256 else []
if isinstance(inst, VOP2): return ([inst.src0 - 256] if inst.src0 >= 256 else []) + [inst.vsrc1]
if isinstance(inst, VOP3): return [s - 256 for s in [inst.src0, inst.src1, getattr(inst, 'src2', None)] if s is not None and s >= 256]
return []
class SQTTState:
"""SQTT tracing with cycle-accurate RDNA3 VALU pipeline model.
NOTE: This is a hardware-plausible model derived from observed SQTT timing patterns.
The model should be verified by tests against real hardware traces, not by fitting
formulas to expected outputs. If tests fail, the model needs to be understood and
fixed, not hacked with magic constants.
Physical model:
- alu[4]: 4-stage ALU pipeline, each slot holds dest_vgpr or None
- in_flight: up to 12 in-flight instructions (issued but not yet completed)
- issue_queue: instructions waiting to enter ALU (sources not ready)
- fwd_slots: 4 forwarding slots, reserved at issue, freed when consumer forwards
- completed: vgprs with results ready (exited ALU)
Forwarding model (4 slots):
- Slot reserved at ISSUE time if available (len(fwd_slots) < 4)
- Slot freed when a consumer uses the result for forwarding
- Consumer can forward if: has a slot AND producer is completed
- If no slot at issue, instruction uses regfile path (+4 cycle penalty)
"""
def __init__(self, wave_id: int = 0, simd: int = 0, cu: int = 0):
self.wave_id, self.simd, self.cu = wave_id, simd, cu
self.cycle = 0
self.packets = []
# 4-stage ALU pipeline: each slot holds dest_vgpr or None
self.alu = [None, None, None, None]
# In-flight instructions: max 12 at a time, each is (dest_vgpr, srcs, has_fwd_slot)
self.in_flight: list[tuple[int, list[int], bool]] = []
# Issue queue: list of (dest_vgpr, srcs, ready_at, has_fwd_slot, was_warm) waiting for deps
# ready_at: cycle when this instruction can enter ALU (0 = no restriction)
# has_fwd_slot: True if this instruction reserved a forwarding slot at issue time
# was_warm: True if forwarding path was warm when this instruction was issued
self.issue_queue: list[tuple[int, list[int], int, bool, bool]] = []
# 4 forwarding slots: consumer adds producer at issue, freed when consumer forwards
self.fwd_slots: list[int] = [] # producer vgprs reserved for forwarding
# VGPRs that had a dependent try to add them to fwd_slots (successful or not)
self.had_dependent: set[int] = set()
# VGPRs that were issued after forwarding chain broke (can't forward)
self.fwd_chain_broken: set[int] = set()
# Set of completed vgprs (results ready, exited ALU)
self.completed: set[int] = set()
# Cold start: first forwarding use has +1 cycle penalty
self.forward_warm = False
self.cold_used = False # True if cold start penalty was applied
def emit(self, pkt_class, **kwargs):
self.packets.append(pkt_class(_time=self.cycle, **kwargs))
def _fmt_alu(self) -> str:
# Fixed width: each slot 3 chars, total ALU[xxx,xxx,xxx,xxx] = 20 chars
slots = [f'v{v}' if v is not None else '-' for v in self.alu]
return 'ALU[' + ','.join(f'{s:>3}' for s in slots) + ']'
def _fmt_fwd(self) -> str:
items = [f'v{v}' for v in self.fwd_slots]
content = 'FWD[' + ','.join(items) + ']' if items else 'FWD[]'
padded = f'{content:<24}'
return colored(padded, 'yellow') if items else padded
def _fmt_iq(self) -> str:
def fmt_item(d, r, fwd):
s = f'v{d}'
if r != 0: s += f'@{abs(r)}'
if not fwd: s += 'R'
return s
items = [fmt_item(d, r, fwd) for d, _, r, fwd, _ in self.issue_queue]
return 'IQ[' + ','.join(items) + ']' if items else 'IQ[]'
def _debug_line(self, events: list[str] | None = None):
if DEBUG < 3: return
# Skip empty cycles (nothing in ALU, no events, no IQ)
has_alu = any(s is not None for s in self.alu)
if not has_alu and not events and not self.issue_queue: return
cycle = colored(f'C{self.cycle:>3}:', 'cyan')
alu = self._fmt_alu()
fwd = self._fmt_fwd()
iq = f'{self._fmt_iq():<28}'
ev_str = ' '.join(events) if events else ''
ev_padded = f'{ev_str:<20}' if ev_str else ' ' * 20
print(f"{cycle} {alu} {fwd} {iq} {ev_padded}")
def _can_issue(self) -> bool:
return len(self.in_flight) < 12
def _has_pending_write(self, vgpr: int) -> bool:
"""Check if there's a pending write to this VGPR (in ALU, in-flight, or issue queue)."""
if any(slot == vgpr for slot in self.alu if slot is not None): return True
if any(d == vgpr for d, _, _ in self.in_flight): return True
if any(d == vgpr for d, _, _, _, _ in self.issue_queue): return True
return False
def _all_srcs_ready(self, srcs: list[int]) -> bool:
"""Returns True if all sources are ready (completed or no pending write)."""
for src in srcs:
if src in self.completed: continue
if not self._has_pending_write(src): continue # initial value
return False
return True
def tick(self):
self.cycle += 1
if self.cycle > 10000: raise RuntimeError("cycle limit exceeded")
events = []
# 1. ALU[3] exits - capture but don't add to completed yet
exiting = self.alu[3]
if exiting is not None:
self.emit(ALUEXEC, src=AluSrc.VALU)
events.append(colored(f"EXEC v{exiting}", 'red'))
# 2. Slide ALU pipeline
self.alu[3] = self.alu[2]
self.alu[2] = self.alu[1]
self.alu[1] = self.alu[0]
self.alu[0] = None
# 3. Try to promote from issue_queue to ALU[0] (before adding exiting to completed)
if self.alu[0] is None and self.issue_queue:
for i, (dest, srcs, ready_at, has_fwd_slot, was_warm) in enumerate(self.issue_queue):
# Check if instruction has a minimum ready cycle
if ready_at > 0 and self.cycle < ready_at:
continue
# Check if sources are ready
ready = self._all_srcs_ready(srcs)
has_deps = len(srcs) > 0
if not ready:
continue
# Cold start penalty: first dependent instruction has +1 cycle delay (delta=6 vs delta=5)
# Only applies if forwarding path wasn't warm when this instruction was issued
if has_deps and not was_warm and not self.cold_used:
self.cold_used = True
self.issue_queue[i] = (dest, srcs, self.cycle + 1, has_fwd_slot, was_warm)
continue
# Forwarding: consumer can forward if:
# 1. Not in fwd_chain_broken (chain must be intact), AND
# 2. Producer has a slot (source is in fwd_slots), AND
# 3. Either activated by dependent OR successfully added producer at issue
# Note: if issued cold with no slot, activation only counts if the activator also has a dependent
chain_intact = dest not in self.fwd_chain_broken
producer_has_slot = has_deps and any(src in self.fwd_slots for src in srcs)
# Check activation validity
if dest in self.had_dependent:
if was_warm or has_fwd_slot:
activated_by_dependent = True
else:
# Cold + no slot: activation only counts if activator itself has a dependent
# This handles the chain_6 vs chain_7 difference (chain_7 has v6 which activates v5)
activated_by_dependent = (dest + 1) in self.had_dependent # activator is dest+1 in a chain
else:
activated_by_dependent = False
can_forward = chain_intact and producer_has_slot and (activated_by_dependent or has_fwd_slot)
# Regfile path: has dependencies but can't forward
must_use_regfile = has_deps and not can_forward
# Regfile penalty: add +4 cycles latency (only apply once)
if must_use_regfile and ready_at == 0:
self.issue_queue[i] = (dest, srcs, self.cycle + 4, has_fwd_slot, was_warm)
continue
# Enter ALU
self.alu[0] = dest
self.issue_queue.pop(i)
# Free producer's forwarding slot when consumer dispatches (regardless of fwd/rf)
for src in srcs:
if src in self.fwd_slots:
self.fwd_slots.remove(src)
break
events.append(colored(f"v{dest}->ALU" + ("(fwd)" if can_forward else "(rf)" if must_use_regfile else ""), 'green'))
break
# 4. Now add exiting instruction to completed (after promotion decision)
if exiting is not None:
self.completed.add(exiting)
# Remove from in_flight - any VALU completing warms up the forward path
for idx, (d, _, _) in enumerate(self.in_flight):
if d == exiting:
self.forward_warm = True
self.in_flight.pop(idx)
break
self._debug_line(events)
def _pipeline_empty(self) -> bool:
if any(s is not None for s in self.alu): return False
if self.issue_queue: return False
if self.in_flight: return False
return True
def process_instruction(self, inst: Inst):
if isinstance(inst, SOPP) and inst.op == SOPPOp.S_DELAY_ALU:
# TODO: implement s_delay_alu properly
return
elif isinstance(inst, SOPP) and inst.op == SOPPOp.S_NOP:
# s_nop(N) delays N+1 cycles, plus extra penalty for s_nop(11-22)
cycles = inst.simm16 + 1
if SNOP_EXTRA_DELAY_MIN <= inst.simm16 <= SNOP_EXTRA_DELAY_MAX:
cycles += SNOP_EXTRA_DELAY_CYCLES
if DEBUG >= 3:
cycle = colored(f'C{self.cycle:>3}:', 'cyan')
# 20 (ALU) + 1 + 24 (FWD) + 1 + 28 (IQ) + 1 + 20 (events) = 95 padding after cycle
print(f"{cycle} {' ' * 95} {disasm(inst)}")
for _ in range(cycles): self.tick()
self.emit(IMMEDIATE, wave=self.wave_id)
elif isinstance(inst, SOPP) and inst.op == SOPPOp.S_ENDPGM:
# Drain pipeline before ending
while not self._pipeline_empty(): self.tick()
self.emit(WAVEEND, wave=self.wave_id, simd=self.simd, cu_lo=self.cu & 0x7, flag7=self.cu >> 3)
elif isinstance(inst, (VOP1, VOP2, VOP3)):
# Check for issue stall (no free in-flight slots)
while not self._can_issue():
self.tick()
# Issue: add to in_flight and issue_queue
srcs = _get_src_vgprs(inst)
dest = inst.vdst
# Clear stale state for this dest (WAW hazard)
self.completed.discard(dest)
if dest in self.fwd_slots: self.fwd_slots.remove(dest)
# Consumer adds producer to fwd_slots (if room and has dependency)
# If producer is in fwd_chain_broken, or we can't add, the chain breaks for this instruction too
has_fwd_slot = False
if srcs:
producer = srcs[0]
self.had_dependent.add(producer) # record that producer has a dependent
# Check if producer's forwarding chain is already broken
if producer in self.fwd_chain_broken:
# Chain is broken, this instruction also can't forward
self.fwd_chain_broken.add(dest)
elif len(self.fwd_slots) >= 4:
# Can't add producer, chain breaks
self.fwd_chain_broken.add(dest)
else:
# Can add producer
if producer not in self.fwd_slots:
self.fwd_slots.append(producer)
has_fwd_slot = len(self.fwd_slots) < 4
# Record if forwarding path was warm at issue time
was_warm = self.forward_warm
self.in_flight.append((dest, srcs, has_fwd_slot))
self.issue_queue.append((dest, srcs, 0, has_fwd_slot, was_warm))
self.emit(VALUINST, wave=self.wave_id)
if DEBUG >= 3:
cycle = colored(f'C{self.cycle:>3}:', 'cyan')
slot_info = "" if has_fwd_slot else colored(" NO_SLOT", 'red')
issue = colored(f'ISSUE v{dest}', 'magenta') + slot_info
padding = 95 - ansilen(issue)
print(f"{cycle} {issue}{' ' * padding} {disasm(inst)}")
# One cycle per instruction issued, then try to enter ALU
self.tick()
return
# One cycle per instruction issued (for non-VALU)
self.tick()
def emit_wavestart(self):
self.emit(WAVESTART, wave=self.wave_id, simd=self.simd, cu_lo=self.cu & 0x7, flag7=self.cu >> 3)
for _ in range(WAVESTART_TO_INST_CYCLES): self.tick()
# ═══════════════════════════════════════════════════════════════════════════════
# PROGRAM DECODE
@@ -384,13 +626,12 @@ def dispatch_endpgm(st, inst): return -1
def dispatch_barrier(st, inst): st.pc += inst._words; return -2
def dispatch_nop(st, inst): st.pc += inst._words; return 0
def dispatch_wmma(st, inst): exec_wmma(st, inst, inst.op); st.pc += inst._words; return 0
def dispatch_writelane(st, inst): st.vgpr[st.rsrc(inst.src1, 0, inst._literal) & 0x1f][inst.vdst.offset - 256] = st.rsrc(inst.src0, 0, inst._literal) & MASK32; st.pc += inst._words; return 0
def dispatch_writelane(st, inst): st.vgpr[st.rsrc(inst.src1, 0, inst._literal) & 0x1f][inst.vdst] = st.rsrc(inst.src0, 0, inst._literal) & MASK32; st.pc += inst._words; return 0
def dispatch_readlane(st, inst):
src0_off = inst.src0.offset
src0_idx = (src0_off - 256) if src0_off >= 256 else src0_off
src0_idx = (inst.src0 - 256) if inst.src0 >= 256 else inst.src0
s1 = st.rsrc(inst.src1, 0, inst._literal) if getattr(inst, 'src1', None) is not None else 0
result = inst._fn(0, s1, 0, 0, st.scc, st.vcc, 0, st.exec_mask, inst._literal, st.vgpr, src0_idx, inst.vdst.offset)
st.wsgpr(inst.vdst.offset, result['D0'])
result = inst._fn(0, s1, 0, 0, st.scc, st.vcc, 0, st.exec_mask, inst._literal, st.vgpr, src0_idx, inst.vdst)
st.wsgpr(inst.vdst, result['D0'])
st.pc += inst._words; return 0
# Per-lane dispatch wrapper: wraps per-lane exec functions into wave-level dispatch
@@ -409,7 +650,7 @@ def decode_program(data: bytes) -> dict[int, Inst]:
result: dict[int, Inst] = {}
i = 0
while i < len(data):
inst = decode_inst(data[i:])
inst = detect_format(data[i:]).from_bytes(data[i:])
inst._words = inst.size() // 4
# Determine dispatch function and pcode function
@@ -418,23 +659,23 @@ def decode_program(data: bytes) -> dict[int, Inst]:
elif isinstance(inst, SOPP) and inst.op == SOPPOp.S_BARRIER: inst._dispatch = dispatch_barrier
elif isinstance(inst, SOPP) and inst.op in (SOPPOp.S_CLAUSE, SOPPOp.S_WAITCNT, SOPPOp.S_WAITCNT_DEPCTR, SOPPOp.S_SENDMSG, SOPPOp.S_SET_INST_PREFETCH_DISTANCE, SOPPOp.S_DELAY_ALU): inst._dispatch = dispatch_nop
elif isinstance(inst, (SOP1, SOP2, SOPC, SOPK, SOPP, SMEM)): inst._dispatch = exec_scalar
elif isinstance(inst, VOP1) and inst.op == VOP1Op.V_NOP_E32: inst._dispatch = dispatch_nop
elif isinstance(inst, VOP1) and inst.op == VOP1Op.V_NOP: inst._dispatch = dispatch_nop
elif isinstance(inst, VOP3P) and 'WMMA' in inst.op_name: inst._dispatch = dispatch_wmma
elif isinstance(inst, VOP3) and inst.op == VOP3Op.V_WRITELANE_B32: inst._dispatch = dispatch_writelane
elif isinstance(inst, (VOP1, VOP3)) and inst.op in (VOP1Op.V_READFIRSTLANE_B32_E32, VOP3Op.V_READFIRSTLANE_B32, VOP3Op.V_READLANE_B32): inst._dispatch = dispatch_readlane
elif isinstance(inst, (VOP1, VOP3)) and inst.op in (VOP1Op.V_READFIRSTLANE_B32, VOP3Op.V_READFIRSTLANE_B32, VOP3Op.V_READLANE_B32): inst._dispatch = dispatch_readlane
elif isinstance(inst, VOPD): inst._dispatch = dispatch_lane(exec_vopd)
elif isinstance(inst, (FLAT, GLOBAL, SCRATCH)): inst._dispatch = dispatch_lane(exec_flat)
elif isinstance(inst, FLAT): inst._dispatch = dispatch_lane(exec_flat)
elif isinstance(inst, DS): inst._dispatch = dispatch_lane(exec_ds)
else: inst._dispatch = dispatch_lane(exec_vop)
# Compile pcode for instructions that use it (not VOPD which has _fnx/_fny, not special dispatches)
# VOPD needs separate functions for X and Y ops
if isinstance(inst, VOPD):
def _compile_vopd_op(op): return compile_pseudocode(type(op).__name__, op.name, PCODE[op])
def _compile_vopd_op(op): return compile_pseudocode(type(op).__name__, op.name, PSEUDOCODE_STRINGS[type(op)][op])
inst._fnx, inst._fny = _compile_vopd_op(_VOPD_TO_VOP[inst.opx]), _compile_vopd_op(_VOPD_TO_VOP[inst.opy])
elif inst._dispatch not in (dispatch_endpgm, dispatch_barrier, dispatch_nop, dispatch_wmma, dispatch_writelane):
assert type(inst.op) != int, f"inst op of {inst} is int"
inst._fn = compile_pseudocode(type(inst.op).__name__, inst.op.name, PCODE[inst.op])
inst._fn = compile_pseudocode(type(inst.op).__name__, inst.op.name, PSEUDOCODE_STRINGS[type(inst.op)][inst.op])
result[i // 4] = inst
i += inst._words * 4
return result
@@ -458,7 +699,7 @@ def exec_workgroup(program: dict[int, Inst], workgroup_id: tuple[int, int, int],
n_lanes = min(WAVE_SIZE, total_threads - wave_start)
st = WaveState(lds, n_lanes)
st.exec_mask = (1 << n_lanes) - 1
st.wsgpr64(s[0:1], args_ptr) # s[0:1] = kernel arguments pointer
st.wsgpr64(0, args_ptr) # s[0:1] = kernel arguments pointer
# COMPUTE_PGM_RSRC2: USER_SGPR_COUNT is where workgroup IDs start, ENABLE_SGPR_WORKGROUP_ID_X/Y/Z control which are passed
sgpr_idx = (rsrc2 & hsa.AMD_COMPUTE_PGM_RSRC_TWO_USER_SGPR_COUNT) >> hsa.AMD_COMPUTE_PGM_RSRC_TWO_USER_SGPR_COUNT_SHIFT
if rsrc2 & hsa.AMD_COMPUTE_PGM_RSRC_TWO_ENABLE_SGPR_WORKGROUP_ID_X: st.sgpr[sgpr_idx] = workgroup_id[0]; sgpr_idx += 1
+7 -42
View File
@@ -1,41 +1,6 @@
# DSL for RDNA3 pseudocode - makes pseudocode expressions work directly as Python
import struct, math, re, functools
MASK32, MASK64 = 0xFFFFFFFF, 0xFFFFFFFFFFFFFFFF
# Float/int bit conversion functions
_struct_f, _struct_I = struct.Struct("<f"), struct.Struct("<I")
_struct_e, _struct_H = struct.Struct("<e"), struct.Struct("<H")
_struct_d, _struct_Q = struct.Struct("<d"), struct.Struct("<Q")
def _f32(i):
i = i & MASK32
# RDNA3 default mode: flush f32 denormals to zero (FTZ)
# Denormal: exponent=0 (bits 23-30) and mantissa!=0 (bits 0-22)
if (i & 0x7f800000) == 0 and (i & 0x007fffff) != 0: return 0.0
return _struct_f.unpack(_struct_I.pack(i))[0]
def _i32(f):
if isinstance(f, int): f = float(f)
if math.isnan(f): return 0xffc00000 if math.copysign(1.0, f) < 0 else 0x7fc00000
if math.isinf(f): return 0x7f800000 if f > 0 else 0xff800000
try:
bits = _struct_I.unpack(_struct_f.pack(f))[0]
# RDNA3 default mode: flush f32 denormals to zero (FTZ)
if (bits & 0x7f800000) == 0 and (bits & 0x007fffff) != 0: return 0x80000000 if bits & 0x80000000 else 0
return bits
except (OverflowError, struct.error): return 0x7f800000 if f > 0 else 0xff800000
def _sext(v, b): return v - (1 << b) if v & (1 << (b - 1)) else v
def _f16(i): return _struct_e.unpack(_struct_H.pack(i & 0xffff))[0]
def _i16(f):
if math.isnan(f): return 0x7e00
if math.isinf(f): return 0x7c00 if f > 0 else 0xfc00
try: return _struct_H.unpack(_struct_e.pack(f))[0]
except (OverflowError, struct.error): return 0x7c00 if f > 0 else 0xfc00
def _f64(i): return _struct_d.unpack(_struct_Q.pack(i & MASK64))[0]
def _i64(f):
if math.isnan(f): return 0x7ff8000000000000
if math.isinf(f): return 0x7ff0000000000000 if f > 0 else 0xfff0000000000000
try: return _struct_Q.unpack(_struct_d.pack(f))[0]
except (OverflowError, struct.error): return 0x7ff0000000000000 if f > 0 else 0xfff0000000000000
from extra.assembly.amd.dsl import MASK32, MASK64, _f32, _i32, _sext, _f16, _i16, _f64, _i64
# ═══════════════════════════════════════════════════════════════════════════════
# INTERNAL HELPERS
@@ -653,17 +618,17 @@ def _apply_pseudocode_fixes(op_name: str, code: str) -> str:
code = code.replace('D0.f64 = 2.0 ** 64 * fma(S0.f64, S1.f64, S2.f64)',
'D0.f64 = (2.0 ** 128 if exponent(S2.f64) > 1023 else 2.0 ** -128) * fma(S0.f64, S1.f64, S2.f64)')
if op_name == 'V_DIV_SCALE_F32':
code = code.replace('D0.f32 = float("nan")', 'VCC = Reg(1 << laneId); D0.f32 = float("nan")')
code = code.replace('D0.f32 = float("nan")', 'VCC = Reg(0x1); D0.f32 = float("nan")')
code = code.replace('elif S1.f32 == DENORM.f32:\n D0.f32 = ldexp(S0.f32, 64)', 'elif False:\n pass')
code += '\nif S1.f32 == DENORM.f32:\n D0.f32 = float("nan")'
code = code.replace('elif exponent(S2.f32) <= 23:\n D0.f32 = ldexp(S0.f32, 64)', 'elif exponent(S2.f32) <= 23:\n VCC = Reg(1 << laneId); D0.f32 = ldexp(S0.f32, 64)')
code = code.replace('elif S2.f32 / S1.f32 == DENORM.f32:\n VCC = Reg(0x1)\n if S0.f32 == S2.f32:\n D0.f32 = ldexp(S0.f32, 64)', 'elif S2.f32 / S1.f32 == DENORM.f32:\n VCC = Reg(1 << laneId)')
code = code.replace('elif exponent(S2.f32) <= 23:\n D0.f32 = ldexp(S0.f32, 64)', 'elif exponent(S2.f32) <= 23:\n VCC = Reg(0x1); D0.f32 = ldexp(S0.f32, 64)')
code = code.replace('elif S2.f32 / S1.f32 == DENORM.f32:\n VCC = Reg(0x1)\n if S0.f32 == S2.f32:\n D0.f32 = ldexp(S0.f32, 64)', 'elif S2.f32 / S1.f32 == DENORM.f32:\n VCC = Reg(0x1)')
if op_name == 'V_DIV_SCALE_F64':
code = code.replace('D0.f64 = float("nan")', 'VCC = Reg(1 << laneId); D0.f64 = float("nan")')
code = code.replace('D0.f64 = float("nan")', 'VCC = Reg(0x1); D0.f64 = float("nan")')
code = code.replace('elif S1.f64 == DENORM.f64:\n D0.f64 = ldexp(S0.f64, 128)', 'elif False:\n pass')
code += '\nif S1.f64 == DENORM.f64:\n D0.f64 = float("nan")'
code = code.replace('elif exponent(S2.f64) <= 52:\n D0.f64 = ldexp(S0.f64, 128)', 'elif exponent(S2.f64) <= 52:\n VCC = Reg(1 << laneId); D0.f64 = ldexp(S0.f64, 128)')
code = code.replace('elif S2.f64 / S1.f64 == DENORM.f64:\n VCC = Reg(0x1)\n if S0.f64 == S2.f64:\n D0.f64 = ldexp(S0.f64, 128)', 'elif S2.f64 / S1.f64 == DENORM.f64:\n VCC = Reg(1 << laneId)')
code = code.replace('elif exponent(S2.f64) <= 52:\n D0.f64 = ldexp(S0.f64, 128)', 'elif exponent(S2.f64) <= 52:\n VCC = Reg(0x1); D0.f64 = ldexp(S0.f64, 128)')
code = code.replace('elif S2.f64 / S1.f64 == DENORM.f64:\n VCC = Reg(0x1)\n if S0.f64 == S2.f64:\n D0.f64 = ldexp(S0.f64, 128)', 'elif S2.f64 / S1.f64 == DENORM.f64:\n VCC = Reg(0x1)')
if op_name == 'V_DIV_FIXUP_F32':
code = code.replace('D0.f32 = ((-abs(S0.f32)) if (sign_out) else (abs(S0.f32)))',
'D0.f32 = ((-OVERFLOW_F32) if (sign_out) else (OVERFLOW_F32)) if isNAN(S0.f32) else ((-abs(S0.f32)) if (sign_out) else (abs(S0.f32)))')
+319
View File
@@ -0,0 +1,319 @@
# Generic PDF text extractor - no external dependencies
import re, zlib
from tinygrad.helpers import fetch, merge_dicts
PDF_URLS = {
"rdna3": "https://docs.amd.com/api/khub/documents/UVVZM22UN7tMUeiW_4ShTQ/content",
"rdna4": "https://docs.amd.com/api/khub/documents/uQpkEvk3pv~kfAb2x~j4uw/content",
"cdna": "https://www.amd.com/content/dam/amd/en/documents/instinct-tech-docs/instruction-set-architectures/amd-instinct-cdna4-instruction-set-architecture.pdf",
}
# ═══════════════════════════════════════════════════════════════════════════════
# Generic PDF extraction tools
# ═══════════════════════════════════════════════════════════════════════════════
def extract(url: str) -> list[list[tuple[float, float, str, str]]]:
"""Extract positioned text from PDF. Returns list of text elements (x, y, text, font) per page."""
data = fetch(url).read_bytes()
# Parse xref table to locate objects
xref: dict[int, int] = {}
pos = int(re.search(rb'startxref\s+(\d+)', data).group(1)) + 4
while data[pos:pos+7] != b'trailer':
while data[pos:pos+1] in b' \r\n': pos += 1
line_end = data.find(b'\n', pos)
start_obj, count = map(int, data[pos:line_end].split()[:2])
pos = line_end + 1
for i in range(count):
if data[pos+17:pos+18] == b'n' and (off := int(data[pos:pos+10])) > 0: xref[start_obj + i] = off
pos += 20
def get_stream(n: int) -> bytes:
obj = data[xref[n]:data.find(b'endobj', xref[n])]
raw = obj[obj.find(b'stream\n') + 7:obj.find(b'\nendstream')]
return zlib.decompress(raw) if b'/FlateDecode' in obj else raw
# Find page content streams and extract text
pages = []
for n in sorted(xref):
if b'/Type /Page' not in data[xref[n]:xref[n]+500]: continue
if not (m := re.search(rb'/Contents (\d+) 0 R', data[xref[n]:xref[n]+500])): continue
stream = get_stream(int(m.group(1))).decode('latin-1')
elements, font = [], ''
for bt in re.finditer(r'BT(.*?)ET', stream, re.S):
x, y = 0.0, 0.0
for m in re.finditer(r'(/F[\d.]+) [\d.]+ Tf|([\d.+-]+) ([\d.+-]+) Td|[\d.+-]+ [\d.+-]+ [\d.+-]+ [\d.+-]+ ([\d.+-]+) ([\d.+-]+) Tm|<([0-9A-Fa-f]+)>.*?Tj|\[([^\]]+)\] TJ', bt.group(1)):
if m.group(1): font = m.group(1)
elif m.group(2): x, y = x + float(m.group(2)), y + float(m.group(3))
elif m.group(4): x, y = float(m.group(4)), float(m.group(5))
elif m.group(6) and (t := bytes.fromhex(m.group(6)).decode('latin-1')).strip(): elements.append((x, y, t, font))
elif m.group(7) and (t := ''.join(bytes.fromhex(h).decode('latin-1') for h in re.findall(r'<([0-9A-Fa-f]+)>', m.group(7)))).strip(): elements.append((x, y, t, font))
pages.append(sorted(elements, key=lambda e: (-e[1], e[0])))
return pages
def extract_tables(pages: list[list[tuple[float, float, str, str]]]) -> dict[int, tuple[str, list[list[str]]]]:
"""Extract numbered tables from PDF pages. Returns {table_num: (title, rows)} where rows is list of cells per row."""
def group_by_y(texts, key=lambda y: round(y)):
by_y: dict[int, list[tuple[float, float, str]]] = {}
for x, y, t, _ in texts:
by_y.setdefault(key(y), []).append((x, y, t))
return by_y
# Find all table headers by merging text on same line
table_positions = []
for page_idx, texts in enumerate(pages):
for items in group_by_y(texts).values():
line = ''.join(t for _, t in sorted((x, t) for x, _, t in items))
if m := re.search(r'Table (\d+)\. (.+)', line):
table_positions.append((int(m.group(1)), m.group(2).strip(), page_idx, items[0][1]))
table_positions.sort(key=lambda t: (t[2], -t[3]))
# For each table, find rows with matching X positions
result: dict[int, tuple[str, list[list[str]]]] = {}
for num, title, start_page, header_y in table_positions:
rows, col_xs = [], None
for page_idx in range(start_page, len(pages)):
page_texts = [(x, y, t) for x, y, t, _ in pages[page_idx] if 30 < y < 760 and (page_idx > start_page or y < header_y)]
for items in sorted(group_by_y([(x, y, t, '') for x, y, t in page_texts], key=lambda y: round(y / 5)).values(), key=lambda items: -items[0][1]):
xs = tuple(sorted(round(x) for x, _, _ in items))
if col_xs is None:
if len(xs) < 2: continue # Skip single-column rows before table starts
col_xs = xs
elif len(xs) == 1 and xs[0] in col_xs: continue # Skip continuation rows at known column positions
elif not any(c in xs for c in col_xs[:2]): break # Row missing first columns = end of table
rows.append([t for _, t in sorted((x, t) for x, _, t in items)])
else: continue
break
if rows: result[num] = (title, rows)
return result
# ═══════════════════════════════════════════════════════════════════════════════
# AMD specific extraction
# ═══════════════════════════════════════════════════════════════════════════════
def extract_enums(tables: dict[int, tuple[str, list[list[str]]]]) -> dict[str, dict[int, str]]:
"""Extract all enums from tables. Returns {enum_name: {value: name}}."""
enums: dict[str, dict[int, str]] = {}
for num, (title, rows) in tables.items():
# Opcode enums from "XXX Opcodes" tables
if m := re.match(r'(\w+) (?:Y-)?Opcodes', title):
fmt_name = 'VOPD' if 'Y-Opcodes' in title else m.group(1)
ops: dict[int, str] = {}
for row in rows:
for i in range(0, len(row) - 1, 2):
if row[i].isdigit() and re.match(r'^[A-Z][A-Z0-9_]+$', row[i + 1]):
ops[int(row[i])] = row[i + 1]
if ops: enums[fmt_name] = ops
# BufFmt from "Data Format" tables
if 'Data Format' in title:
for row in rows:
for i in range(0, len(row) - 1, 2):
if row[i].isdigit() and re.match(r'^[\dA-Z_]+$', row[i + 1]) and 'INVALID' not in row[i + 1]:
enums.setdefault('BufFmt', {})[int(row[i])] = row[i + 1]
return enums
def extract_ins(tables: dict[int, tuple[str, list[list[str]]]]) -> tuple[dict[str, list[tuple[str, int, int]]], dict[str, str]]:
"""Extract formats and encodings from 'XXX Fields' tables. Returns (formats, encodings)."""
formats: dict[str, list[tuple[str, int, int]]] = {}
encodings: dict[str, str] = {}
for num, (title, rows) in tables.items():
if not (m := re.match(r'(\w+) Fields$', title)): continue
fmt_name = m.group(1)
fields = []
for row in rows:
if len(row) < 2: continue
if (bits := re.match(r'\[?(\d+):(\d+)\]?$', row[1])) or (bits := re.match(r'\[(\d+)\]$', row[1])):
field_name = row[0].lower()
hi, lo = int(bits.group(1)), int(bits.group(2)) if bits.lastindex >= 2 else int(bits.group(1))
if field_name == 'encoding' and len(row) >= 3:
enc_bits = None
if "'b" in row[2]: enc_bits = row[2].split("'b")[-1].replace('_', '')
elif (enc := re.search(r':\s*([01_]+)', row[2])): enc_bits = enc.group(1).replace('_', '')
if enc_bits:
# If encoding bits exceed field width, extend field to match (AMD docs sometimes have this)
declared_width, actual_width = hi - lo + 1, len(enc_bits)
if actual_width > declared_width: lo = hi - actual_width + 1
encodings[fmt_name] = enc_bits
fields.append((field_name, hi, lo))
if fields: formats[fmt_name] = fields
return formats, encodings
def extract_pcode(pages: list[list[tuple[float, float, str, str]]], enums: dict[str, dict[int, str]]) -> dict[tuple[str, int], str]:
"""Extract pseudocode for instructions. Returns {(name, opcode): pseudocode}."""
# Build lookup from instruction name to opcode
name_to_op = {name: op for ops in enums.values() for op, name in ops.items()}
# First pass: find all instruction headers across all pages
all_instructions: list[tuple[int, float, str, int]] = [] # (page_idx, y, name, opcode)
for page_idx, page in enumerate(pages):
by_y: dict[int, list[tuple[float, str]]] = {}
for x, y, t, _ in page:
by_y.setdefault(round(y), []).append((x, t))
for y, items in sorted(by_y.items(), reverse=True):
left = [(x, t) for x, t in items if 55 < x < 65]
right = [(x, t) for x, t in items if 535 < x < 550]
if left and right and left[0][1] in name_to_op and right[0][1].isdigit():
all_instructions.append((page_idx, y, left[0][1], int(right[0][1])))
# Second pass: extract pseudocode between consecutive instructions
pcode: dict[tuple[str, int], str] = {}
for i, (page_idx, y, name, opcode) in enumerate(all_instructions):
# Get end boundary from next instruction
if i + 1 < len(all_instructions):
next_page, next_y = all_instructions[i + 1][0], all_instructions[i + 1][1]
else:
next_page, next_y = page_idx, 0
# Collect F6 text from current position to next instruction (pseudocode is at x ≈ 69)
lines = []
for p in range(page_idx, next_page + 1):
start_y = y if p == page_idx else 800
end_y = next_y if p == next_page else 0
lines.extend((p, y2, t) for x, y2, t, f in pages[p] if f in ('/F6.0', '/F7.0') and end_y < y2 < start_y and 60 < x < 80)
if lines:
# Sort by page first, then by y descending within each page (higher y = earlier text in PDF)
sorted_lines = sorted(lines, key=lambda x: (x[0], -x[1]))
# Stop at large Y gaps (>30) - indicates section break (Notes, examples, etc)
filtered = [sorted_lines[0]]
for j in range(1, len(sorted_lines)):
prev_page, prev_y, _ = sorted_lines[j-1]
curr_page, curr_y, _ = sorted_lines[j]
if curr_page == prev_page and prev_y - curr_y > 30: break
if curr_page != prev_page and prev_y > 60 and curr_y < 730: break # examples spilled to next page (not at very top)
filtered.append(sorted_lines[j])
pcode_lines = [t.replace('Ê', '').strip() for _, _, t in filtered]
if pcode_lines: pcode[(name, opcode)] = '\n'.join(pcode_lines)
return pcode
# ═══════════════════════════════════════════════════════════════════════════════
# Write autogen files
# ═══════════════════════════════════════════════════════════════════════════════
def write_enums(enums: dict[str, dict[int, str]], arch: str, path: str):
"""Write enum.py file from extracted enums."""
lines = ["# autogenerated from AMD ISA PDF by pdf.py - do not edit", "from enum import IntEnum", ""]
for name, values in sorted(enums.items()):
suffix = "Op" if name not in ('Src', 'BufFmt') else ("Enum" if name == 'Src' else "")
prefix = "BUF_FMT_" if name == 'BufFmt' else ""
lines.append(f"class {name}{suffix}(IntEnum):")
for val, member in sorted(values.items()):
lines.append(f" {prefix}{member} = {val}")
lines.append("")
with open(path, "w") as f:
f.write("\n".join(lines))
def write_ins(formats: dict[str, list[tuple[str, int, int]]], encodings: dict[str, str], enums: dict[str, dict[int, str]], arch: str, path: str):
"""Write ins.py file from extracted formats and enums."""
# Field types and ordering
def field_type(name, fmt):
if name == 'op' and fmt in enums: return f'Annotated[BitField, {fmt}Op]'
if name in ('opx', 'opy'): return 'Annotated[BitField, VOPDOp]'
if name == 'vdsty': return 'VDSTYEnc'
if name in ('vdst', 'vsrc1', 'vaddr', 'vdata', 'data', 'data0', 'data1', 'addr', 'vsrc0', 'vsrc2', 'vsrc3'): return 'VGPRField'
if name in ('sdst', 'sbase', 'sdata', 'srsrc', 'ssamp'): return 'SGPRField'
if name.startswith('ssrc') or name in ('saddr', 'soffset'): return 'SSrc'
if name in ('src0', 'srcx0', 'srcy0') or name.startswith('src') and name[3:].isdigit(): return 'Src'
if name.startswith('simm'): return 'SImm'
if name == 'offset' or name.startswith('imm'): return 'Imm'
return None
field_priority = ['encoding', 'op', 'opx', 'opy', 'vdst', 'vdstx', 'vdsty', 'sdst', 'vdata', 'sdata', 'addr', 'vaddr', 'data', 'data0', 'data1',
'src0', 'srcx0', 'srcy0', 'vsrc0', 'ssrc0', 'src1', 'vsrc1', 'vsrcx1', 'vsrcy1', 'ssrc1', 'src2', 'vsrc2', 'src3', 'vsrc3',
'saddr', 'sbase', 'srsrc', 'ssamp', 'soffset', 'offset', 'simm16', 'en', 'target', 'attr', 'attr_chan',
'omod', 'neg', 'neg_hi', 'abs', 'clmp', 'opsel', 'opsel_hi', 'waitexp', 'wait_va',
'dmask', 'dim', 'seg', 'format', 'offen', 'idxen', 'glc', 'dlc', 'slc', 'tfe', 'unrm', 'done', 'row']
def sort_fields(fields):
order = {name: i for i, name in enumerate(field_priority)}
return sorted(fields, key=lambda f: (order.get(f[0], 1000), f[2]))
# Generate format classes
lines = ["# autogenerated from AMD ISA PDF by pdf.py - do not edit", "# ruff: noqa: F401,F403",
"from typing import Annotated",
"from extra.assembly.amd.dsl import *",
f"from extra.assembly.amd.autogen.{arch}.enum import *", "import functools", ""]
for fmt_name, fields in sorted(formats.items()):
lines.append(f"class {fmt_name}(Inst):")
for name, hi, lo in sort_fields(fields):
bits_str = f"bits[{hi}:{lo}]" if hi != lo else f"bits[{hi}]"
if name == 'encoding' and fmt_name in encodings: lines.append(f" encoding = {bits_str} == 0b{encodings[fmt_name]}")
else:
ftype = field_type(name, fmt_name)
lines.append(f" {name}{f':{ftype}' if ftype else ''} = {bits_str}")
lines.append("")
# Generate instruction helpers
lines.append("# instruction helpers")
for fmt_name, ops in sorted(enums.items()):
seg = {"GLOBAL": ", seg=2", "SCRATCH": ", seg=1"}.get(fmt_name, "")
tgt = {"GLOBAL": "FLAT, GLOBALOp", "SCRATCH": "FLAT, SCRATCHOp"}.get(fmt_name, f"{fmt_name}, {fmt_name}Op")
suffix = "_e32" if fmt_name in ("VOP1", "VOP2", "VOPC") else "_e64" if fmt_name == "VOP3" and len(ops) > 0 else ""
if fmt_name in formats or fmt_name in ("GLOBAL", "SCRATCH"):
for op_val, name in sorted(ops.items()):
fn_suffix = suffix if fmt_name != "VOP3" or op_val < 512 else ""
lines.append(f"{name.lower()}{fn_suffix} = functools.partial({tgt}.{name}{seg})")
with open(path, "w") as f:
f.write("\n".join(lines))
def write_pcode(pcode: dict[tuple[str, int], str], enums: dict[str, dict[int, str]], arch: str, path: str):
"""Write str_pcode.py file from extracted pseudocode."""
# Group pseudocode by enum class
by_enum: dict[str, list[tuple[str, int, str]]] = {}
for fmt_name, ops in enums.items():
for opcode, name in ops.items():
if (name, opcode) in pcode: by_enum.setdefault(f"{fmt_name}Op", []).append((name, opcode, pcode[(name, opcode)]))
# Generate file
enum_names = sorted(by_enum.keys())
lines = [f"# autogenerated by pdf.py - do not edit", f"# to regenerate: python -m extra.assembly.amd.pdf",
"# ruff: noqa: E501", f"from extra.assembly.amd.autogen.{arch}.enum import {', '.join(enum_names)}", ""]
for enum_name in enum_names:
lines.append(f"{enum_name}_PCODE = {{")
for name, opcode, code in sorted(by_enum[enum_name], key=lambda x: x[1]):
lines.append(f" {enum_name}.{name}: {code!r},")
lines.append("}\n")
lines.append(f"PSEUDOCODE_STRINGS = {{{', '.join(f'{e}: {e}_PCODE' for e in enum_names)}}}")
with open(path, "w") as f:
f.write("\n".join(lines))
if __name__ == "__main__":
import pathlib
for arch, url in PDF_URLS.items():
print(f"Processing {arch}...")
pages = extract(url)
tables = extract_tables(pages)
enums = extract_enums(tables)
formats, encodings = extract_ins(tables)
pcode = extract_pcode(pages, enums)
# Fix known PDF errors
if arch == 'rdna3':
fixes = {'SOPP': {8: 'S_WAITCNT_DEPCTR', 58: 'S_TTRACEDATA', 59: 'S_TTRACEDATA_IMM'},
'SOPK': {22: 'S_SUBVECTOR_LOOP_BEGIN', 23: 'S_SUBVECTOR_LOOP_END'},
'SMEM': {34: 'S_ATC_PROBE', 35: 'S_ATC_PROBE_BUFFER'},
'DS': {24: 'DS_GWS_SEMA_RELEASE_ALL', 25: 'DS_GWS_INIT', 26: 'DS_GWS_SEMA_V', 27: 'DS_GWS_SEMA_BR', 28: 'DS_GWS_SEMA_P', 29: 'DS_GWS_BARRIER'},
'FLAT': {40: 'GLOBAL_LOAD_ADDTID_B32', 41: 'GLOBAL_STORE_ADDTID_B32', 55: 'FLAT_ATOMIC_CSUB_U32'}}
for fmt, ops in fixes.items(): enums[fmt] = merge_dicts([enums[fmt], ops])
if arch == 'rdna4':
fixes = {'SMEM': {34: 'S_ATC_PROBE', 35: 'S_ATC_PROBE_BUFFER'},
'SOP1': {81: 'S_BARRIER_INIT', 82: 'S_BARRIER_JOIN'},
'SOPP': {21: 'S_BARRIER_LEAVE', 58: 'S_TTRACEDATA', 59: 'S_TTRACEDATA_IMM'}}
for fmt, ops in fixes.items(): enums[fmt] = merge_dicts([enums[fmt], ops])
if arch in ('rdna3', 'rdna4'):
# RDNA SMEM: PDF says DLC=[14], GLC=[16] but hardware uses DLC=[13], GLC=[14]
if 'SMEM' in formats:
formats['SMEM'] = [(n, 13 if n == 'dlc' else 14 if n == 'glc' else h, 13 if n == 'dlc' else 14 if n == 'glc' else l)
for n, h, l in formats['SMEM']]
if arch == 'cdna':
# CDNA DS: PDF is missing the GDS field (bit 16)
if 'DS' in formats and not any(n == 'gds' for n, _, _ in formats['DS']):
formats['DS'].append(('gds', 16, 16))
# CDNA DPP/SDWA: PDF only documents modifier fields (bits[63:32]), need to add VOP overlay fields (bits[31:0])
vop_overlay = [('encoding', 8, 0), ('vop_op', 16, 9), ('vdst', 24, 17), ('vop2_op', 31, 25)]
if 'DPP' in formats and not any(n == 'encoding' for n, _, _ in formats['DPP']):
formats['DPP'] = vop_overlay + [('bc' if n == 'bound_ctrl' else n, h, l) for n, h, l in formats['DPP']]
encodings['DPP'] = '11111010'
if 'SDWA' in formats and not any(n == 'encoding' for n, _, _ in formats['SDWA']):
formats['SDWA'] = vop_overlay + [(n, h, l) for n, h, l in formats['SDWA']]
encodings['SDWA'] = '11111001'
base = pathlib.Path(__file__).parent / "autogen" / arch
write_enums(enums, arch, base / "enum.py")
write_ins(formats, encodings, enums, arch, base / "ins.py")
write_pcode(pcode, enums, arch, base / "str_pcode.py")
print(f" {len(tables)} tables, {len(pcode)} pcode -> {base}")
+135 -71
View File
@@ -5,27 +5,27 @@ The format is nibble-based with variable-width packets determined by a state mac
Uses BitField infrastructure from dsl.py, similar to GPU instruction encoding.
"""
from __future__ import annotations
from typing import Iterator
from enum import Enum
from extra.assembly.amd.dsl import BitField, FixedBitField, bits
from enum import IntEnum
from typing import get_type_hints
from extra.assembly.amd.dsl import BitField, bits
# ═══════════════════════════════════════════════════════════════════════════════
# FIELD ENUMS
# ═══════════════════════════════════════════════════════════════════════════════
class MemSrc(Enum):
class MemSrc(IntEnum):
LDS = 0
LDS_ALT = 1
VMEM = 2
VMEM_ALT = 3
class AluSrc(Enum):
class AluSrc(IntEnum):
NONE = 0
SALU = 1
VALU = 2
VALU_SALU = 3
VALU_ALT = 3
class InstOp(Enum):
class InstOp(IntEnum):
"""SQTT instruction operation types.
Memory ops appear in two ranges depending on which SIMD executes them:
@@ -101,23 +101,60 @@ class InstOp(Enum):
class PacketType:
"""Base class for SQTT packet types."""
encoding: FixedBitField
_encoding: tuple[BitField, int] | None = None
_field_types: dict[str, type] = {}
_values: dict[str, int]
_raw: int
_time: int
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls._fields = {k: v for k, v in cls.__dict__.items() if isinstance(v, BitField)}
if 'encoding' in cls.__dict__ and isinstance(cls.__dict__['encoding'], tuple):
cls._encoding = cls.__dict__['encoding']
# Cache field type annotations for enum conversion
try: cls._field_types = {k: v for k, v in get_type_hints(cls).items() if isinstance(v, type) and issubclass(v, IntEnum)}
except Exception: cls._field_types = {}
# Cache fields and precompute extraction info: (name, lo, mask, enum_type)
cls._fields = {k: v for k, v in cls.__dict__.items() if isinstance(v, BitField) and k != 'encoding'}
cls._extract_info = [(name, bf.lo, bf.mask(), cls._field_types.get(name)) for name, bf in cls._fields.items()]
cls._size_nibbles = ((max((f.hi for f in cls._fields.values()), default=0) + 4) // 4)
def __init__(self, _time: int = 0, **kwargs):
"""Construct packet from named fields (like assembly instructions)."""
raw = 0
if self._encoding:
bf, pattern = self._encoding
raw |= pattern << bf.lo
for name, bf in self._fields.items():
val = kwargs.get(name, 0)
if isinstance(val, IntEnum): val = val.value
raw |= (val & bf.mask()) << bf.lo
self._raw, self._time, self._values = raw, _time, {}
for name, lo, mask, enum_type in self._extract_info:
val = (raw >> lo) & mask
if enum_type is not None:
try: val = enum_type(val)
except ValueError: pass
self._values[name] = val
@classmethod
def from_raw(cls, raw: int, time: int = 0):
inst = object.__new__(cls)
inst._raw, inst._time = raw, time
inst._raw, inst._time, inst._values = raw, time, {}
for name, lo, mask, enum_type in cls._extract_info:
val = (raw >> lo) & mask
if enum_type is not None:
try: val = enum_type(val)
except ValueError: pass
inst._values[name] = val
return inst
def __getattr__(self, name: str):
if name.startswith('_'): raise AttributeError(name)
return self._values.get(name, 0)
def __repr__(self) -> str:
fields_str = ", ".join(f"{k}={getattr(self, k)}" for k in self._fields if not k.startswith('_'))
fields_str = ", ".join(f"{k}={v}" for k, v in self._values.items() if not k.startswith('_'))
return f"{self.__class__.__name__}({fields_str})"
# ═══════════════════════════════════════════════════════════════════════════════
@@ -133,12 +170,12 @@ class VALUINST(PacketType): # exclude: 1 << 2
class VMEMEXEC(PacketType): # exclude: 1 << 0
encoding = bits[3:0] == 0b1111
delta = bits[5:4]
src = bits[7:6].enum(MemSrc)
src: MemSrc = bits[7:6]
class ALUEXEC(PacketType): # exclude: 1 << 1
encoding = bits[3:0] == 0b1110
delta = bits[5:4]
src = bits[7:6].enum(AluSrc)
src: AluSrc = bits[7:6]
class IMMEDIATE(PacketType): # exclude: 1 << 5
encoding = bits[3:0] == 0b1101
@@ -269,7 +306,7 @@ class INST(PacketType):
flag1 = bits[3:3]
flag2 = bits[7:7]
wave = bits[12:8]
op = bits[19:13].enum(InstOp)
op: InstOp = bits[19:13]
class UTILCTR(PacketType):
encoding = bits[6:0] == 0b0110001
@@ -286,13 +323,17 @@ PACKET_TYPES: list[type[PacketType]] = [
NOP,
]
PACKET_BY_NAME: dict[str, type[PacketType]] = {cls.__name__: cls for cls in PACKET_TYPES}
def _build_state_table() -> tuple[bytes, dict[int, type[PacketType]]]:
table = [len(PACKET_TYPES) - 1] * 256 # default to NOP
opcode_to_class: dict[int, type[PacketType]] = {i: cls for i, cls in enumerate(PACKET_TYPES)}
for byte_val in range(256):
for opcode, pkt_cls in enumerate(PACKET_TYPES):
if (byte_val & pkt_cls.encoding.mask) == pkt_cls.encoding.default:
if pkt_cls._encoding is None: continue
mask_bf, pattern = pkt_cls._encoding
if (byte_val & mask_bf.mask()) == pattern:
table[byte_val] = opcode
break
@@ -300,9 +341,16 @@ def _build_state_table() -> tuple[bytes, dict[int, type[PacketType]]]:
STATE_TO_OPCODE, OPCODE_TO_CLASS = _build_state_table()
OPCODE_TO_BYTES: dict[int, list[int]] = {}
for _byte_val, _opcode in enumerate(STATE_TO_OPCODE):
if _opcode not in OPCODE_TO_BYTES: OPCODE_TO_BYTES[_opcode] = []
OPCODE_TO_BYTES[_opcode].append(_byte_val)
# Precompute special case opcodes
_TS_DELTA_OR_MARK_OPCODE = next(op for op, cls in OPCODE_TO_CLASS.items() if cls is TS_DELTA_OR_MARK)
_TS_DELTA_SHORT_OPCODE = next(op for op, cls in OPCODE_TO_CLASS.items() if cls is TS_DELTA_SHORT)
_TS_DELTA_OR_MARK_BIT8 = (TS_DELTA_OR_MARK.bit8.lo, TS_DELTA_OR_MARK.bit8.mask())
_TS_DELTA_OR_MARK_BIT9 = (TS_DELTA_OR_MARK.bit9.lo, TS_DELTA_OR_MARK.bit9.mask())
# Combined lookup: opcode -> (pkt_cls, nib_count, delta_lo, delta_mask, special_case)
# special_case: 0=none, 1=TS_DELTA_OR_MARK, 2=TS_DELTA_SHORT
@@ -310,7 +358,7 @@ _DECODE_INFO: dict[int, tuple] = {}
for _opcode, _pkt_cls in OPCODE_TO_CLASS.items():
_delta_field = getattr(_pkt_cls, 'delta', None)
_delta_lo = _delta_field.lo if _delta_field else 0
_delta_mask = _delta_field.mask if _delta_field else 0
_delta_mask = _delta_field.mask() if _delta_field else 0
_special = 1 if _opcode == _TS_DELTA_OR_MARK_OPCODE else (2 if _opcode == _TS_DELTA_SHORT_OPCODE else 0)
_DECODE_INFO[_opcode] = (_pkt_cls, _pkt_cls._size_nibbles, _delta_lo, _delta_mask, _special)
@@ -318,69 +366,85 @@ for _opcode, _pkt_cls in OPCODE_TO_CLASS.items():
# DECODER
# ═══════════════════════════════════════════════════════════════════════════════
def decode(data: bytes) -> Iterator[PacketType]:
"""Decode raw SQTT blob, yielding packet instances."""
n, reg, pos, nib_off, nib_count, time = len(data), 0, 0, 0, 16, 0
def decode(data: bytes) -> list[PacketType]:
"""Decode raw SQTT blob into list of packet instances."""
packets: list[PacketType] = []
packets_append = packets.append
n = len(data)
reg = 0
offset = 0
nib_count = 16
time = 0
state_to_opcode = STATE_TO_OPCODE
decode_info = _DECODE_INFO
mask64 = (1 << 64) - 1
while pos + ((nib_count + nib_off + 1) >> 1) <= n:
need = nib_count - nib_off
# 1. if unaligned, read high nibble to align
if nib_off: reg, pos = (reg >> 4) | ((data[pos] >> 4) << 60), pos + 1
# 2. read all full bytes at once
if (byte_count := need >> 1):
chunk = int.from_bytes(data[pos:pos + byte_count], 'little')
reg, pos = (reg >> (byte_count * 8)) | (chunk << (64 - byte_count * 8)), pos + byte_count
# 3. if odd, read low nibble
if (nib_off := need & 1): reg = (reg >> 4) | ((data[pos] & 0xF) << 60)
while (offset >> 3) < n:
target = offset + nib_count * 4
while offset < target and (offset >> 3) < n:
byte = data[offset >> 3]
nib = (byte >> (offset & 4)) & 0xF
reg = ((reg >> 4) | (nib << 60)) & mask64
offset += 4
if offset < target: break
opcode = state_to_opcode[reg & 0xFF]
pkt_cls, nib_count, delta_lo, delta_mask, special = decode_info[opcode]
opcode = STATE_TO_OPCODE[reg & 0xFF]
pkt_cls, nib_count, delta_lo, delta_mask, special = _DECODE_INFO[opcode]
delta = (reg >> delta_lo) & delta_mask
if special == 1 and (reg >> 9) & 1 and not (reg >> 8) & 1: delta = 0 # TS_DELTA_OR_MARK marker
elif special == 2: delta += 8 # TS_DELTA_SHORT
if special == 1: # TS_DELTA_OR_MARK
bit8 = (reg >> _TS_DELTA_OR_MARK_BIT8[0]) & _TS_DELTA_OR_MARK_BIT8[1]
bit9 = (reg >> _TS_DELTA_OR_MARK_BIT9[0]) & _TS_DELTA_OR_MARK_BIT9[1]
if bit9 and not bit8: delta = 0
elif special == 2: # TS_DELTA_SHORT
delta = delta + 8
time += delta
yield pkt_cls.from_raw(reg, time)
packets_append(pkt_cls.from_raw(reg, time))
return packets
# ═══════════════════════════════════════════════════════════════════════════════
# PRINTER
# ENCODER
# ═══════════════════════════════════════════════════════════════════════════════
PACKET_COLORS = {
"INST": "WHITE", "VALUINST": "BLACK", "VMEMEXEC": "yellow", "ALUEXEC": "yellow",
"IMMEDIATE": "YELLOW", "IMMEDIATE_MASK": "YELLOW", "WAVERDY": "cyan", "WAVEALLOC": "cyan",
"WAVEEND": "blue", "WAVESTART": "blue", "PERF": "magenta", "EVENT": "red", "EVENT_BIG": "red",
"REG": "green", "LAYOUT_HEADER": "white", "SNAPSHOT": "white", "UTILCTR": "green",
}
def encode(packets: list[PacketType]) -> bytes:
"""Encode a list of packet instances into raw SQTT blob."""
if not packets: return b''
def format_packet(p) -> str:
from tinygrad.helpers import colored
name = type(p).__name__
if isinstance(p, INST):
op_name = p.op.name if isinstance(p.op, InstOp) else f"0x{p.op:02x}"
fields = f"wave={p.wave} op={op_name}" + (" flag1" if p.flag1 else "") + (" flag2" if p.flag2 else "")
elif isinstance(p, VALUINST): fields = f"wave={p.wave}" + (" flag" if p.flag else "")
elif isinstance(p, ALUEXEC): fields = f"src={p.src.name if isinstance(p.src, AluSrc) else p.src}"
elif isinstance(p, VMEMEXEC): fields = f"src={p.src.name if isinstance(p.src, MemSrc) else p.src}"
elif isinstance(p, (WAVESTART, WAVEEND)): fields = f"wave={p.wave} simd={p.simd} cu={p.cu}"
elif hasattr(p, '_fields'):
fields = " ".join(f"{k}=0x{getattr(p, k):x}" if k in {'snap', 'val32'} else f"{k}={getattr(p, k)}"
for k in p._fields if not k.startswith('_') and k not in {'delta', 'encoding'})
else: fields = ""
return f"{p._time:8}: {colored(f'{name:18}', PACKET_COLORS.get(name, 'white'))} {fields}"
read_lengths = [16]
for p in packets[:-1]:
read_lengths.append(type(p)._size_nibbles)
def print_packets(packets) -> None:
skip = {"NOP", "TS_DELTA_SHORT", "TS_WAVE_STATE", "TS_DELTA_OR_MARK", "TS_DELTA_S5_W2", "TS_DELTA_S5_W3", "TS_DELTA_S8_W3", "REG", "EVENT"}
for p in packets:
if type(p).__name__ not in skip: print(format_packet(p))
total_nibbles = sum(read_lengths)
bits_arr = [0] * (total_nibbles * 4)
if __name__ == "__main__":
import sys, pickle
if len(sys.argv) < 2:
print("Usage: python sqtt.py <pkl_file>")
sys.exit(1)
with open(sys.argv[1], "rb") as f:
data = pickle.load(f)
sqtt_events = [e for e in data if type(e).__name__ == "ProfileSQTTEvent"]
for i, event in enumerate(sqtt_events):
print(f"\n=== event {i} ===")
print_packets(decode(event.blob))
cumulative = 0
for i, p in enumerate(packets):
cumulative += read_lengths[i]
pkt_cls = type(p)
opcode = next(op for op, cls in OPCODE_TO_CLASS.items() if cls is pkt_cls)
byte_vals = OPCODE_TO_BYTES.get(opcode)
if not byte_vals: raise ValueError(f"No encoding for {pkt_cls.__name__}")
opcode_byte = byte_vals[0]
delta_field = getattr(pkt_cls, 'delta', None)
if delta_field is not None and delta_field.hi < 8:
delta = p._values.get('delta', 0)
if isinstance(delta, IntEnum): delta = delta.value
if pkt_cls is TS_DELTA_SHORT: delta = max(0, delta - 8)
delta = delta & delta_field.mask()
opcode_byte = (opcode_byte & ~(delta_field.mask() << delta_field.lo)) | (delta << delta_field.lo)
opcode_nibble_pos = max(0, cumulative - 16)
opcode_bit_pos = opcode_nibble_pos * 4
for b in range(8):
if opcode_bit_pos + b < len(bits_arr):
bits_arr[opcode_bit_pos + b] = (opcode_byte >> b) & 1
nibbles = [sum(bits_arr[i + j] << j for j in range(4) if i + j < len(bits_arr)) for i in range(0, len(bits_arr), 4)]
while len(nibbles) % 2: nibbles.append(0)
return bytes(nibbles[i] | (nibbles[i + 1] << 4) for i in range(0, len(nibbles), 2))
-132
View File
@@ -1,132 +0,0 @@
# maps SQTT trace packets to instructions.
from dataclasses import dataclass
from typing import Iterator
from tinygrad.runtime.support.elf import elf_loader
from extra.assembly.amd.sqtt import decode, print_packets, INST, VALUINST, IMMEDIATE, WAVESTART, WAVEEND, InstOp, PacketType, IMMEDIATE_MASK
from extra.assembly.amd.dsl import Inst
from extra.assembly.amd.decode import decode_inst
from extra.assembly.amd.autogen.rdna3.ins import SOPP, s_endpgm
from extra.assembly.amd.autogen.rdna3.enum import SOPPOp
@dataclass(frozen=True)
class InstructionInfo:
pc: int
wave: int
inst: Inst
def map_insts(data:bytes, lib:bytes) -> Iterator[tuple[PacketType, InstructionInfo|None]]:
"""maps SQTT packets to instructions, yields (packet, instruction_info or None)"""
# map pcs to insts
pc_map:dict[int, Inst] = {}
image, sections, _ = elf_loader(lib)
text = next((sh for sh in sections if sh.name == ".text"), None)
assert text is not None, "no .text section found"
text_off, text_size = text.header.sh_addr, text.header.sh_size
offset = text_off
while offset < text_off + text_size:
inst = decode_inst(image[offset:])
pc_map[offset-text_off] = inst
offset += inst.size()
wave_pc:dict[int, int] = {}
# only processing packets on one [CU, SIMD] unit
def simd_select(p) -> bool: return getattr(p, "cu", 0) == 0 and getattr(p, "simd", 0) == 0
for p in decode(data):
if not simd_select(p): continue
if isinstance(p, WAVESTART):
assert p.wave not in wave_pc, "only one inflight wave per unit"
wave_pc[p.wave] = 0
continue
if isinstance(p, WAVEEND):
pc = wave_pc.pop(p.wave)
yield (p, InstructionInfo(pc, p.wave, s_endpgm()))
continue
# skip OTHER_ instructions, they don't belong to this unit
if isinstance(p, INST) and p.op.name.startswith("OTHER_"): continue
if isinstance(p, IMMEDIATE_MASK):
# immediate mask may yield multiple times per packet
for wave in range(16):
if p.mask & (1 << wave):
inst = pc_map[pc:=wave_pc[wave]]
# can this assert be more strict?
assert isinstance(inst, SOPP), f"IMMEDIATE_MASK packet must map to SOPP, got {inst}"
wave_pc[wave] += inst.size()
yield (p, InstructionInfo(pc, wave, inst))
continue
if isinstance(p, (VALUINST, INST, IMMEDIATE)):
inst = pc_map[pc:=wave_pc[p.wave]]
# s_delay_alu doesn't get a packet?
if isinstance(inst, SOPP) and inst.op in {SOPPOp.S_DELAY_ALU}:
wave_pc[p.wave] += inst.size()
inst = pc_map[pc:=wave_pc[p.wave]]
# identify a branch instruction, only used for asserts
is_branch = isinstance(inst, SOPP) and "BRANCH" in inst.op_name
if is_branch: assert isinstance(p, INST) and p.op in {InstOp.JUMP_NO, InstOp.JUMP}, f"branch can only be folowed by jump packets, got {p}"
# JUMP handling
if isinstance(p, INST) and p.op is InstOp.JUMP:
assert is_branch, f"JUMP packet must map to a branch instruction, got {inst}"
x = inst.simm16 & 0xffff
wave_pc[p.wave] += inst.size() + (x - 0x10000 if x & 0x8000 else x)*4
else:
if is_branch: assert inst.op != SOPPOp.S_BRANCH, f"S_BRANCH must have a JUMP packet, got {p}"
wave_pc[p.wave] += inst.size()
yield (p, InstructionInfo(pc, p.wave, inst))
continue
# for all other packets (VMEMEXEC, ALUEXEC, etc.), yield with None
yield (p, None)
# test to compare every packet with the rocprof decoder
def test_rocprof_inst_traces_match(sqtt, prg, target):
from tinygrad.viz.serve import llvm_disasm
from extra.sqtt.roc import decode as roc_decode, InstExec
disasm = {addr+prg.base:inst_disasm for addr, inst_disasm in llvm_disasm(target, prg.lib).items()}
rctx = roc_decode([sqtt], {prg.name:disasm})
rwaves = rctx.inst_execs[(sqtt.kern, sqtt.exec_tag)]
rwaves_iter:dict[int, list[Iterator[InstExec]]] = {} # wave unit (0-15) -> list of inst trace iterators for all executions on that unit
for w in rwaves: rwaves_iter.setdefault(w.wave_id, []).append(w.unpack_insts())
rwaves_base = next(iter(disasm)) # base program counter
passed_insts = 0
for pkt, info in map_insts(sqtt.blob, prg.lib):
if DEBUG >= 2: print_packets([pkt])
if info is None: continue
if DEBUG >= 2: print(f"{' '*29}{info.inst.disasm()}")
rocprof_inst = next(rwaves_iter[info.wave][0])
ref_pc = rocprof_inst.pc-rwaves_base
# always check pc matches
assert ref_pc == info.pc, f"pc mismatch {ref_pc}:{disasm[rocprof_inst.pc][0]} != {info.pc}:{info.inst.disasm()}"
# special handling for s_endpgm, it marks the wave completion.
if info.inst == s_endpgm():
completed_wave = list(rwaves_iter[info.wave].pop(0))
assert len(completed_wave) == 0, f"incomplete instructions in wave {info.wave}"
# otherwise the packet timestamp is time + "stall"
else:
assert pkt._time == rocprof_inst.time+rocprof_inst.stall
passed_insts += 1
for k,v in rwaves_iter.items():
assert len(v) == 0, f"incomplete wave {k}"
print(f"passed for {passed_insts} instructions across {len(rwaves)} waves scheduled on {len(rwaves_iter)} wave units")
if __name__ == "__main__":
import argparse, pickle, pathlib
from tinygrad.helpers import temp, DEBUG
parser = argparse.ArgumentParser()
parser.add_argument('--profile', 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('--kernel', type=str, default=None, metavar="NAME", help='Kernel to focus on (optional name, default: all kernels)')
args = parser.parse_args()
with open(args.profile, "rb") as f:
data = pickle.load(f)
sqtt_events = [e for e in data if type(e).__name__ == "ProfileSQTTEvent"]
kern_events = {e.name:e for e in data if type(e).__name__ == "ProfileProgramEvent"}
target = next((e for e in data if type(e).__name__ == "ProfileDeviceEvent" and e.device.startswith("AMD"))).props["gfx_target_version"]
for e in sqtt_events:
if args.kernel is not None and args.kernel != e.kern: continue
if not e.itrace: continue
print(f"==== {e.kern}")
test_rocprof_inst_traces_match(e, kern_events[e.kern], target)
+855
View File
@@ -0,0 +1,855 @@
#!/usr/bin/env python3
"""SQTT InstOp discovery tool - finds instruction opcodes by running different instructions.
Requires profiling enabled:
echo 'profile_standard' | sudo tee /sys/class/drm/card1/device/power_dpm_force_performance_level
Run with: DEBUG=1 python extra/assembly/amd/test/discover_instops.py
For full traces: DEBUG=2 python extra/assembly/amd/test/discover_instops.py
"""
import os
os.environ["SQTT"] = "1"
os.environ["PROFILE"] = "1"
os.environ["SQTT_LIMIT_SE"] = "2" # Force work to traced SE only
os.environ["SQTT_TOKEN_EXCLUDE"] = "3784" # Exclude WAVERDY, REG, EVENT, UTILCTR, WAVEALLOC, PERF
from tinygrad.helpers import DEBUG, colored
from tinygrad.runtime.ops_amd import SQTT_SIMD_SEL
from extra.assembly.amd.autogen.rdna3.ins import (
# VALU - basic (these are safe, just register ops)
v_mov_b32_e32, v_add_f32_e32, v_mul_f32_e32,
v_and_b32_e32, v_or_b32_e32, v_xor_b32_e32,
v_lshlrev_b32_e32, v_lshrrev_b32_e32,
# VALU - transcendental
v_exp_f32_e32, v_log_f32_e32, v_rcp_f32_e32, v_sqrt_f32_e32,
v_sin_f32_e32, v_cos_f32_e32,
# VALU - 64-bit
v_lshlrev_b64, v_lshrrev_b64, v_ashrrev_i64,
v_add_f64, v_mul_f64, v_max_f64, v_min_f64,
v_fma_f64,
# VALU - 64-bit transcendental
v_rcp_f64_e32, v_rsq_f64_e32, v_sqrt_f64_e32,
v_trunc_f64_e32, v_ceil_f64_e32, v_floor_f64_e32, v_fract_f64_e32,
v_frexp_exp_i32_f64_e32, v_frexp_mant_f64_e32,
# VALU - div helpers
v_div_fixup_f32, v_div_fixup_f64, v_div_fmas_f32, v_div_fmas_f64, v_div_scale_f32,
# VALU - MAD64
v_mad_u64_u32, v_mad_i64_i32,
# VALU - compare (writes to VCC, safe)
v_cmp_eq_u32_e32,
# VALU - cmpx (modifies EXEC) - various types
v_cmpx_eq_u32_e32, v_cmpx_lt_u32_e32, v_cmpx_gt_u32_e32,
v_cmpx_eq_f32_e32, v_cmpx_lt_f32_e32,
v_cmpx_eq_i32_e32,
v_cmpx_class_f32_e32,
# VALU - readlane/writelane
v_readlane_b32, v_writelane_b32,
v_readfirstlane_b32_e32,
# SALU - basic (safe, just register ops)
s_mov_b32, s_add_u32, s_and_b32, s_or_b32,
s_lshl_b32, s_lshr_b32,
s_nop, s_endpgm, s_waitcnt,
# SALU - float
s_ceil_f32, s_floor_f32, s_trunc_f32,
# SALU - branch (safe if offset is 0 = next instruction)
s_branch, s_cbranch_scc0, s_cbranch_execz, s_cbranch_execnz,
# SALU - message
s_sendmsg,
# SALU - bit manipulation
s_brev_b32, s_bcnt1_i32_b32, s_ctz_i32_b32, s_clz_i32_u32,
# SALU - saveexec (modifies EXEC)
s_and_saveexec_b32, s_or_saveexec_b32, s_xor_saveexec_b32,
# SMEM - scalar memory (load from kernarg pointer in s[0:1])
s_load_b32, s_load_b64,
# GLOBAL - global memory (load/store) - various widths
global_load_u8, global_load_u16, global_load_b32, global_load_b64, global_load_b96, global_load_b128,
global_store_b8, global_store_b16, global_store_b32, global_store_b64, global_store_b96, global_store_b128,
# GLOBAL - atomics
global_atomic_add_u32, global_atomic_add_u64,
# FLAT - flat memory access
flat_load_b32, flat_load_b64, flat_load_b96, flat_load_b128,
flat_store_b8, flat_store_b16, flat_store_b32, flat_store_b64, flat_store_b96, flat_store_b128,
# LDS - local data share - various widths
ds_load_b32, ds_load_b64, ds_load_b128,
ds_store_b32, ds_store_b64, ds_store_b128,
# LDS - atomics
ds_add_u32, ds_max_u32, ds_min_u32,
# VOP3P - packed
v_pk_add_f16, v_pk_mul_f16, v_pk_fma_f16, v_pk_add_i16,
# VOP3 - misc
v_bfe_u32, v_bfi_b32, v_alignbit_b32, v_fma_f32,
v_add3_u32, v_xad_u32, v_lshl_or_b32, v_add_nc_u32_e32,
# VOP3 - carry-out
v_add_co_u32, v_add_co_ci_u32_e32,
# VOPD - dual issue
v_dual_add_f32, v_dual_mul_f32,
# VOP2 - fmac
v_fmac_f32_e32,
# DOT
v_dot2_f16_f16,
# WMMA
v_wmma_f32_16x16x16_f16, v_wmma_f16_16x16x16_f16, v_wmma_i32_16x16x16_iu8,
# Permlane ops
v_permlane64_b32_e32, v_permlane16_b32, v_permlanex16_b32,
# Interpolation
v_interp_p10_f32, v_interp_p2_f32,
# Barrier
s_barrier,
# SrcEnum for NULL soffset
SrcEnum,
)
from extra.assembly.amd.dsl import v, s
from extra.assembly.amd.sqtt import InstOp, INST, WAVESTART, WAVEEND, ALUEXEC, VMEMEXEC
from extra.assembly.amd.test.test_sqtt_hw import (
run_asm_sqtt, decode_all_blobs, get_inst_ops, print_blobs, get_wave_packets, format_packet, PACKET_COLORS, count_valuinst
)
# ═══════════════════════════════════════════════════════════════════════════════
# INSTRUCTION TEST CASES - only safe instructions that don't access memory
# ═══════════════════════════════════════════════════════════════════════════════
# Helper: load buffer address from kernarg (s[0:1] -> s[2:3])
# The runtime passes kernarg pointer in s[0:1], kernarg contains buffer address
def _load_buf_addr():
return [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL), # load buf addr from kernarg
s_waitcnt(lgkmcnt=0), # wait for SMEM load
]
INSTRUCTION_TESTS: dict[str, tuple[str, list]] = {
# SALU (0x0) - scalar ALU, just register operations
"SALU_mov": ("s_mov_b32", [s_mov_b32(s[4], 0), s_mov_b32(s[5], 1)]),
"SALU_add": ("s_add_u32", [s_mov_b32(s[4], 1), s_mov_b32(s[5], 2), s_add_u32(s[6], s[4], s[5])]),
"SALU_logic": ("s_and/or", [s_and_b32(s[6], s[4], s[5]), s_or_b32(s[7], s[4], s[5])]),
"SALU_shift": ("s_lshl/lshr", [s_lshl_b32(s[6], s[4], 1), s_lshr_b32(s[7], s[4], 1)]),
"SALU_nop": ("s_nop", [s_nop(0)]),
# JUMP (0x3) - branch taken
"JUMP_branch": ("s_branch", [s_branch(0)]),
"JUMP_cbranch_execnz": ("s_cbranch_execnz", [s_cbranch_execnz(0)]), # EXEC != 0, branch taken
# JUMP_NO (0x4) - branch not taken
"JUMP_NO_cbranch_execz": ("s_cbranch_execz", [s_cbranch_execz(0)]), # EXEC != 0, branch not taken
# VALU (0xb) - vector ALU, just register operations
"VALU_mov": ("v_mov_b32", [v_mov_b32_e32(v[0], 0), v_mov_b32_e32(v[1], 1.0)]),
"VALU_add": ("v_add_f32", [v_mov_b32_e32(v[0], 1.0), v_mov_b32_e32(v[1], 2.0), v_add_f32_e32(v[2], v[0], v[1])]),
"VALU_mul": ("v_mul_f32", [v_mul_f32_e32(v[2], v[0], v[1])]),
"VALU_logic": ("v_and/or/xor", [v_and_b32_e32(v[2], v[0], v[1]), v_or_b32_e32(v[3], v[0], v[1]), v_xor_b32_e32(v[4], v[0], v[1])]),
"VALU_shift": ("v_lshl/lshr", [v_lshlrev_b32_e32(v[2], 1, v[0]), v_lshrrev_b32_e32(v[3], 1, v[0])]),
# VALU transcendental - still just register ops
"VALU_exp": ("v_exp_f32", [v_mov_b32_e32(v[0], 1.0), v_exp_f32_e32(v[1], v[0])]),
"VALU_log": ("v_log_f32", [v_mov_b32_e32(v[0], 1.0), v_log_f32_e32(v[1], v[0])]),
"VALU_rcp": ("v_rcp_f32", [v_mov_b32_e32(v[0], 1.0), v_rcp_f32_e32(v[1], v[0])]),
"VALU_sqrt": ("v_sqrt_f32", [v_mov_b32_e32(v[0], 1.0), v_sqrt_f32_e32(v[1], v[0])]),
# VALU 64-bit shift (0xd)
"VALU64_lshl": ("v_lshlrev_b64", [v_lshlrev_b64(v[0:1], 1, v[2:3])]),
"VALU64_lshr": ("v_lshrrev_b64", [v_lshrrev_b64(v[0:1], 1, v[2:3])]),
"VALU64_ashr": ("v_ashrrev_i64", [v_ashrrev_i64(v[0:1], 1, v[2:3])]),
# VALU 64-bit arithmetic
"VALU64_add": ("v_add_f64", [v_add_f64(v[0:1], v[2:3], v[4:5])]),
"VALU64_mul": ("v_mul_f64", [v_mul_f64(v[0:1], v[2:3], v[4:5])]),
"VALU64_max": ("v_max_f64", [v_max_f64(v[0:1], v[2:3], v[4:5])]),
"VALU64_min": ("v_min_f64", [v_min_f64(v[0:1], v[2:3], v[4:5])]),
"VALU64_fma": ("v_fma_f64", [v_fma_f64(v[0:1], v[2:3], v[4:5], v[6:7])]),
# VALU 64-bit transcendental
"VALU64_rcp": ("v_rcp_f64", [v_rcp_f64_e32(v[0:1], v[2:3])]),
"VALU64_rsq": ("v_rsq_f64", [v_rsq_f64_e32(v[0:1], v[2:3])]),
"VALU64_sqrt": ("v_sqrt_f64", [v_sqrt_f64_e32(v[0:1], v[2:3])]),
# VALU 64-bit rounding
"VALU64_trunc": ("v_trunc_f64", [v_trunc_f64_e32(v[0:1], v[2:3])]),
"VALU64_ceil": ("v_ceil_f64", [v_ceil_f64_e32(v[0:1], v[2:3])]),
"VALU64_floor": ("v_floor_f64", [v_floor_f64_e32(v[0:1], v[2:3])]),
"VALU64_fract": ("v_fract_f64", [v_fract_f64_e32(v[0:1], v[2:3])]),
# VALU 64-bit frexp
"VALU64_frexp_exp": ("v_frexp_exp_i32_f64", [v_frexp_exp_i32_f64_e32(v[0], v[2:3])]),
"VALU64_frexp_mant": ("v_frexp_mant_f64", [v_frexp_mant_f64_e32(v[0:1], v[2:3])]),
# VALU 64-bit div helpers
"VALU64_div_fixup": ("v_div_fixup_f64", [v_div_fixup_f64(v[0:1], v[2:3], v[4:5], v[6:7])]),
"VALU64_div_fmas": ("v_div_fmas_f64", [v_div_fmas_f64(v[0:1], v[2:3], v[4:5], v[6:7])]),
# VALU 32-bit div helpers
"VALU_div_fixup": ("v_div_fixup_f32", [v_div_fixup_f32(v[0], v[1], v[2], v[3])]),
"VALU_div_fmas": ("v_div_fmas_f32", [v_div_fmas_f32(v[0], v[1], v[2], v[3])]),
"VALU_div_scale": ("v_div_scale_f32", [v_div_scale_f32(v[0], SrcEnum.VCC_LO, v[1], v[2], v[3])]),
# VALU MAD64 (0xe)
"VALU_mad64u": ("v_mad_u64_u32", [
v_mov_b32_e32(v[2], 2),
v_mov_b32_e32(v[3], 3),
v_mov_b32_e32(v[4], 0),
v_mov_b32_e32(v[5], 0),
v_mad_u64_u32(v[0:1], SrcEnum.NULL, v[2], v[3], v[4:5]),
]),
"VALU_mad64i": ("v_mad_i64_i32", [
v_mov_b32_e32(v[2], 2),
v_mov_b32_e32(v[3], 3),
v_mov_b32_e32(v[4], 0),
v_mov_b32_e32(v[5], 0),
v_mad_i64_i32(v[0:1], SrcEnum.NULL, v[2], v[3], v[4:5]),
]),
# VALU compare - writes to VCC
"VALU_cmp": ("v_cmp_eq_u32", [v_cmp_eq_u32_e32(v[0], v[1])]),
# VALU CMPX (0x73) - modifies EXEC
"VALU_cmpx_eq_u32": ("v_cmpx_eq_u32", [v_cmpx_eq_u32_e32(v[0], v[1])]),
# SALU saveexec (0x72) - modifies EXEC safely by ANDing with all-ones mask
"SALU_saveexec": ("s_and_saveexec_b32", [
s_mov_b32(s[5], 0xFFFFFFFF), # all lanes mask
s_and_saveexec_b32(s[4], s[5]), # EXEC = EXEC & 0xFFFFFFFF = EXEC (unchanged)
]),
# SALU float ops
"SALU_ceil": ("s_ceil_f32", [s_ceil_f32(s[4], s[5])]),
"SALU_floor": ("s_floor_f32", [s_floor_f32(s[4], s[5])]),
"SALU_trunc": ("s_trunc_f32", [s_trunc_f32(s[4], s[5])]),
# SALU bit ops
"SALU_brev": ("s_brev_b32", [s_brev_b32(s[4], s[5])]),
"SALU_bcnt1": ("s_bcnt1_i32_b32", [s_bcnt1_i32_b32(s[4], s[5])]),
"SALU_ctz": ("s_ctz_i32_b32", [s_ctz_i32_b32(s[4], s[5])]),
"SALU_clz": ("s_clz_i32_u32", [s_clz_i32_u32(s[4], s[5])]),
# VALU sin/cos
"VALU_sin": ("v_sin_f32", [v_sin_f32_e32(v[0], v[1])]),
"VALU_cos": ("v_cos_f32", [v_cos_f32_e32(v[0], v[1])]),
# VOP3P - packed operations
"VALU_pk_add_f16": ("v_pk_add_f16", [v_pk_add_f16(v[0], v[1], v[2])]),
"VALU_pk_mul_f16": ("v_pk_mul_f16", [v_pk_mul_f16(v[0], v[1], v[2])]),
"VALU_pk_fma_f16": ("v_pk_fma_f16", [v_pk_fma_f16(v[0], v[1], v[2], v[3])]),
"VALU_pk_add_i16": ("v_pk_add_i16", [v_pk_add_i16(v[0], v[1], v[2])]),
# VOP3 - misc
"VALU_bfe_u32": ("v_bfe_u32", [v_bfe_u32(v[0], v[1], 0, 8)]),
"VALU_bfi_b32": ("v_bfi_b32", [v_bfi_b32(v[0], v[1], v[2], v[3])]),
"VALU_alignbit": ("v_alignbit_b32", [v_alignbit_b32(v[0], v[1], v[2], 4)]),
"VALU_fma_f32": ("v_fma_f32", [v_fma_f32(v[0], v[1], v[2], v[3])]),
# VOP3 - integer add variants (used by tinygrad kernels)
"VALU_add3": ("v_add3_u32", [v_add3_u32(v[0], v[1], v[2], v[3])]),
"VALU_xad": ("v_xad_u32", [v_xad_u32(v[0], v[1], v[2], v[3])]),
"VALU_lshl_or": ("v_lshl_or_b32", [v_lshl_or_b32(v[0], v[1], 4, v[2])]),
"VALU_add_nc": ("v_add_nc_u32", [v_add_nc_u32_e32(v[0], v[1], v[2])]),
# VOP3 - carry-out adds (used for 64-bit address calculation)
"VALU_add_co": ("v_add_co_u32", [v_add_co_u32(v[0], SrcEnum.VCC_LO, v[1], v[2])]),
"VALU_add_co_ci": ("v_add_co_ci_u32", [v_add_co_ci_u32_e32(v[0], v[1], v[2])]),
# VOPD - dual issue (used by tinygrad kernels)
"VALU_dual_add": ("v_dual_add_f32", [v_dual_add_f32(v[0], v[1], v[2], v[3], v[4], v[5])]),
"VALU_dual_mul": ("v_dual_mul_f32", [v_dual_mul_f32(v[0], v[1], v[2], v[3], v[4], v[5])]),
# VOP2 - fmac
"VALU_fmac": ("v_fmac_f32", [v_fmac_f32_e32(v[0], v[1], v[0])]),
# DOT products
"VALU_dot2": ("v_dot2_f16_f16", [v_dot2_f16_f16(v[0], v[1], v[2], v[3])]),
# WMMA - wave matrix multiply accumulate
"VALU_wmma_f32_f16": ("v_wmma_f32_16x16x16_f16", [v_wmma_f32_16x16x16_f16(v[0:7], v[8:15], v[16:23], v[0:7])]),
"VALU_wmma_f16_f16": ("v_wmma_f16_16x16x16_f16", [v_wmma_f16_16x16x16_f16(v[0:7], v[8:15], v[16:23], v[0:7])]),
"VALU_wmma_i32_iu8": ("v_wmma_i32_16x16x16_iu8", [v_wmma_i32_16x16x16_iu8(v[0:7], v[8:11], v[12:15], v[0:7])]),
# Permlane operations - cross-lane data movement
# NOTE: permlane64 produces NO SQTT packets in wave32 mode (it's for wave64 pairs)
# NOTE: permlane16/x16 produce VALUINST packets (no specific InstOp)
"VALU_permlane16": ("v_permlane16_b32", [v_permlane16_b32(v[0], v[1], s[2], s[3])]),
"VALU_permlanex16": ("v_permlanex16_b32", [v_permlanex16_b32(v[0], v[1], s[2], s[3])]),
# Interpolation - used in graphics shaders (produces InstOp 0x12 VINTERP)
"VINTERP_p10": ("v_interp_p10_f32", [v_interp_p10_f32(v[0], v[1], v[2], v[3])]),
"VINTERP_p2": ("v_interp_p2_f32", [v_interp_p2_f32(v[0], v[1], v[2], v[3])]),
# Barrier - wave synchronization
# NOTE: s_barrier produces NO SQTT instruction packets (with 1 wave, it's essentially a no-op)
"SALU_barrier": ("s_barrier", [s_barrier()]),
# LDS atomics
"LDS_atomic_add": ("ds_add_u32", [
v_mov_b32_e32(v[0], 0), # LDS address
v_mov_b32_e32(v[1], 1), # data to add
ds_add_u32(addr=v[0], data0=v[1]),
s_waitcnt(lgkmcnt=0),
]),
# ═══════════════════════════════════════════════════════════════════════════════
# GLOBAL ATOMICS - access real buffer passed via kernarg
# ═══════════════════════════════════════════════════════════════════════════════
# GLOBAL atomic add 32-bit (0x28 GLOBAL_ATOMIC)
"GLOBAL_atomic_add": ("global_atomic_add_u32", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL), # load buf addr from kernarg
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], 0), # offset = 0
v_mov_b32_e32(v[1], 1), # data to add
global_atomic_add_u32(addr=v[0], data=v[1], saddr=s[2]),
s_waitcnt(vmcnt=0),
]),
# GLOBAL atomic add 64-bit
"GLOBAL_atomic_add64": ("global_atomic_add_u64", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], 0),
v_mov_b32_e32(v[2], 1),
v_mov_b32_e32(v[3], 0),
global_atomic_add_u64(addr=v[0], data=v[2:3], saddr=s[2]),
s_waitcnt(vmcnt=0),
]),
# ═══════════════════════════════════════════════════════════════════════════════
# MEMORY INSTRUCTIONS - access real buffer passed via kernarg
# ═══════════════════════════════════════════════════════════════════════════════
# SMEM (0x1) - scalar memory load from buffer
"SMEM_load": ("s_load_b32", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL), # load buf addr from kernarg
s_waitcnt(lgkmcnt=0),
s_load_b32(s[4], s[2], 0, soffset=SrcEnum.NULL), # load from buffer
s_waitcnt(lgkmcnt=0),
]),
# GLOBAL load (0x21 GLOBAL_LOAD) - global memory load
"GLOBAL_load": ("global_load_b32", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL), # load buf addr from kernarg
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], 0), # offset = 0
global_load_b32(v[1], addr=v[0], saddr=s[2], offset=0), # load from buffer
s_waitcnt(vmcnt=0),
]),
# GLOBAL store (0x24 GLOBAL_STORE) - global memory store
"GLOBAL_store": ("global_store_b32", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL), # load buf addr from kernarg
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], 0), # offset = 0
v_mov_b32_e32(v[1], 42), # data to store
global_store_b32(addr=v[0], data=v[1], saddr=s[2], offset=0), # store to buffer
s_waitcnt(vmcnt=0),
]),
# GLOBAL 8-bit load/store
"GLOBAL_load8": ("global_load_u8", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], 0),
global_load_u8(v[1], addr=v[0], saddr=s[2], offset=0),
s_waitcnt(vmcnt=0),
]),
"GLOBAL_store8": ("global_store_b8", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], 0),
v_mov_b32_e32(v[1], 42),
global_store_b8(addr=v[0], data=v[1], saddr=s[2], offset=0),
s_waitcnt(vmcnt=0),
]),
# GLOBAL 16-bit load/store
"GLOBAL_load16": ("global_load_u16", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], 0),
global_load_u16(v[1], addr=v[0], saddr=s[2], offset=0),
s_waitcnt(vmcnt=0),
]),
"GLOBAL_store16": ("global_store_b16", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], 0),
v_mov_b32_e32(v[1], 42),
global_store_b16(addr=v[0], data=v[1], saddr=s[2], offset=0),
s_waitcnt(vmcnt=0),
]),
# LDS load (0x29 LDS_LOAD) - local data share read
"LDS_load": ("ds_load_b32", [
v_mov_b32_e32(v[0], 0), # LDS address = 0
ds_load_b32(v[1], v[0], offset=0), # read from LDS
s_waitcnt(lgkmcnt=0),
]),
# LDS store (0x2b LDS_STORE) - local data share write
"LDS_store": ("ds_store_b32", [
v_mov_b32_e32(v[0], 0), # LDS address = 0
v_mov_b32_e32(v[1], 42), # data to store
ds_store_b32(v[0], v[1], offset=0), # write to LDS
s_waitcnt(lgkmcnt=0),
]),
# ═══════════════════════════════════════════════════════════════════════════════
# WIDER MEMORY OPERATIONS - to discover more InstOp variants
# ═══════════════════════════════════════════════════════════════════════════════
# GLOBAL 64-bit load
"GLOBAL_load64": ("global_load_b64", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], 0),
global_load_b64(v[2:3], addr=v[0], saddr=s[2], offset=0),
s_waitcnt(vmcnt=0),
]),
# GLOBAL 96-bit load
"GLOBAL_load96": ("global_load_b96", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], 0),
global_load_b96(v[4:6], addr=v[0], saddr=s[2], offset=0),
s_waitcnt(vmcnt=0),
]),
# GLOBAL 128-bit load
"GLOBAL_load128": ("global_load_b128", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], 0),
global_load_b128(v[4:7], addr=v[0], saddr=s[2], offset=0),
s_waitcnt(vmcnt=0),
]),
# GLOBAL 64-bit store
"GLOBAL_store64": ("global_store_b64", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], 0),
v_mov_b32_e32(v[2], 42),
v_mov_b32_e32(v[3], 43),
global_store_b64(addr=v[0], data=v[2:3], saddr=s[2], offset=0),
s_waitcnt(vmcnt=0),
]),
# GLOBAL 96-bit store
"GLOBAL_store96": ("global_store_b96", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], 0),
v_mov_b32_e32(v[4], 42),
v_mov_b32_e32(v[5], 43),
v_mov_b32_e32(v[6], 44),
global_store_b96(addr=v[0], data=v[4:6], saddr=s[2], offset=0),
s_waitcnt(vmcnt=0),
]),
# GLOBAL 128-bit store
"GLOBAL_store128": ("global_store_b128", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], 0),
v_mov_b32_e32(v[4], 42),
v_mov_b32_e32(v[5], 43),
v_mov_b32_e32(v[6], 44),
v_mov_b32_e32(v[7], 45),
global_store_b128(addr=v[0], data=v[4:7], saddr=s[2], offset=0),
s_waitcnt(vmcnt=0),
]),
# ═══════════════════════════════════════════════════════════════════════════════
# GLOBAL VADDR (vector-only addressing, saddr=NULL) - used by tinygrad kernels
# ═══════════════════════════════════════════════════════════════════════════════
# GLOBAL VADDR load (all sizes use same opcode 0x22)
"GLOBAL_VADDR_load": ("global_load_b32 vaddr", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
global_load_b32(v[4], addr=v[0:1], saddr=SrcEnum.NULL, offset=0),
s_waitcnt(vmcnt=0),
]),
"GLOBAL_VADDR_load128": ("global_load_b128 vaddr", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
global_load_b128(v[4:7], addr=v[0:1], saddr=SrcEnum.NULL, offset=0),
s_waitcnt(vmcnt=0),
]),
# GLOBAL VADDR stores (size encoded: 32->0x25, 64->0x26, 96->0x27, 128->0x28)
"GLOBAL_VADDR_store": ("global_store_b32 vaddr", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
v_mov_b32_e32(v[4], 42),
global_store_b32(addr=v[0:1], data=v[4], saddr=SrcEnum.NULL, offset=0),
s_waitcnt(vmcnt=0),
]),
"GLOBAL_VADDR_store64": ("global_store_b64 vaddr", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
v_mov_b32_e32(v[4], 42),
v_mov_b32_e32(v[5], 43),
global_store_b64(addr=v[0:1], data=v[4:5], saddr=SrcEnum.NULL, offset=0),
s_waitcnt(vmcnt=0),
]),
"GLOBAL_VADDR_store96": ("global_store_b96 vaddr", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
v_mov_b32_e32(v[4], 42),
v_mov_b32_e32(v[5], 43),
v_mov_b32_e32(v[6], 44),
global_store_b96(addr=v[0:1], data=v[4:6], saddr=SrcEnum.NULL, offset=0),
s_waitcnt(vmcnt=0),
]),
"GLOBAL_VADDR_store128": ("global_store_b128 vaddr", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
v_mov_b32_e32(v[4], 42),
v_mov_b32_e32(v[5], 43),
v_mov_b32_e32(v[6], 44),
v_mov_b32_e32(v[7], 45),
global_store_b128(addr=v[0:1], data=v[4:7], saddr=SrcEnum.NULL, offset=0),
s_waitcnt(vmcnt=0),
]),
# LDS 64-bit load
"LDS_load64": ("ds_load_b64", [
v_mov_b32_e32(v[0], 0),
ds_load_b64(v[2:3], v[0], offset=0),
s_waitcnt(lgkmcnt=0),
]),
# LDS 128-bit load
"LDS_load128": ("ds_load_b128", [
v_mov_b32_e32(v[0], 0),
ds_load_b128(v[4:7], v[0], offset=0),
s_waitcnt(lgkmcnt=0),
]),
# LDS 64-bit store
"LDS_store64": ("ds_store_b64", [
v_mov_b32_e32(v[0], 0),
v_mov_b32_e32(v[2], 42),
v_mov_b32_e32(v[3], 43),
ds_store_b64(v[0], v[2:3], offset=0),
s_waitcnt(lgkmcnt=0),
]),
# LDS 128-bit store
"LDS_store128": ("ds_store_b128", [
v_mov_b32_e32(v[0], 0),
v_mov_b32_e32(v[4], 42),
v_mov_b32_e32(v[5], 43),
v_mov_b32_e32(v[6], 44),
v_mov_b32_e32(v[7], 45),
ds_store_b128(v[0], v[4:7], offset=0),
s_waitcnt(lgkmcnt=0),
]),
# MESSAGE (0x9) - s_sendmsg
"MESSAGE": ("s_sendmsg", [
s_sendmsg(0), # send message 0 (NOP message)
]),
# ═══════════════════════════════════════════════════════════════════════════════
# FLAT MEMORY - uses 64-bit virtual address in VGPRs
# ═══════════════════════════════════════════════════════════════════════════════
# FLAT load - load using 64-bit address from buffer
"FLAT_load": ("flat_load_b32", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL), # load buf addr from kernarg
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]), # addr lo
v_mov_b32_e32(v[1], s[3]), # addr hi
flat_load_b32(v[2], addr=v[0:1]),
s_waitcnt(vmcnt=0, lgkmcnt=0),
]),
# FLAT store
"FLAT_store": ("flat_store_b32", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
v_mov_b32_e32(v[2], 42),
flat_store_b32(addr=v[0:1], data=v[2]),
s_waitcnt(vmcnt=0, lgkmcnt=0),
]),
# FLAT 64-bit
"FLAT_load64": ("flat_load_b64", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
flat_load_b64(v[2:3], addr=v[0:1]),
s_waitcnt(vmcnt=0, lgkmcnt=0),
]),
"FLAT_store64": ("flat_store_b64", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
v_mov_b32_e32(v[4], 42),
v_mov_b32_e32(v[5], 43),
flat_store_b64(addr=v[0:1], data=v[4:5]),
s_waitcnt(vmcnt=0, lgkmcnt=0),
]),
# FLAT 96-bit
"FLAT_load96": ("flat_load_b96", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
flat_load_b96(v[4:6], addr=v[0:1]),
s_waitcnt(vmcnt=0, lgkmcnt=0),
]),
"FLAT_store96": ("flat_store_b96", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
v_mov_b32_e32(v[4], 42),
v_mov_b32_e32(v[5], 43),
v_mov_b32_e32(v[6], 44),
flat_store_b96(addr=v[0:1], data=v[4:6]),
s_waitcnt(vmcnt=0, lgkmcnt=0),
]),
# FLAT 128-bit
"FLAT_load128": ("flat_load_b128", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
flat_load_b128(v[4:7], addr=v[0:1]),
s_waitcnt(vmcnt=0, lgkmcnt=0),
]),
"FLAT_store128": ("flat_store_b128", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
v_mov_b32_e32(v[4], 42),
v_mov_b32_e32(v[5], 43),
v_mov_b32_e32(v[6], 44),
v_mov_b32_e32(v[7], 45),
flat_store_b128(addr=v[0:1], data=v[4:7]),
s_waitcnt(vmcnt=0, lgkmcnt=0),
]),
# FLAT 8/16-bit stores
"FLAT_store8": ("flat_store_b8", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
v_mov_b32_e32(v[2], 42),
flat_store_b8(addr=v[0:1], data=v[2]),
s_waitcnt(vmcnt=0, lgkmcnt=0),
]),
"FLAT_store16": ("flat_store_b16", [
s_load_b64(s[2:3], s[0], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
v_mov_b32_e32(v[2], 42),
flat_store_b16(addr=v[0:1], data=v[2]),
s_waitcnt(vmcnt=0, lgkmcnt=0),
]),
}
def run_with_retry(instructions: list, max_attempts: int = 20) -> tuple[list[tuple[int, list[bytes]]], list[list], set, int]:
"""Run instructions multiple times to collect InstOp variants.
Memory ops produce different InstOp values (0x2x vs 0x5x) depending on which SIMD executes them:
- 0x2x range: wave ran on traced SIMD (matched)
- 0x5x range: wave ran on other SIMD (not matched)
Returns list of (traced_simd, blobs) tuples, all_packets, all_ops, max_valuinst_count.
"""
all_ops = set()
all_runs: list[tuple[int, list[bytes]]] = []
all_packets = []
max_valuinst = 0
SQTT_SIMD_SEL.value = 0 # only trace SIMD 0
for _ in range(max_attempts):
blobs = run_asm_sqtt(instructions)
packets = decode_all_blobs(blobs)
# get ops and valuinst from all SIMDs
ops = set()
valuinst_count = 0
for simd in [0, 1, 2, 3]:
ops.update(get_inst_ops(packets, traced_simd=simd))
valuinst_count = max(valuinst_count, count_valuinst(packets, traced_simd=simd))
all_runs.append((0, blobs))
all_packets.append(packets)
all_ops.update(ops)
max_valuinst = max(max_valuinst, valuinst_count)
return all_runs, all_packets, all_ops, max_valuinst
def discover_all_instops() -> tuple[dict[int, set[str]], dict[str, Exception], dict[str, int]]:
"""Run all instruction tests and collect InstOp values."""
discovered: dict[int, set[str]] = {}
failures: dict[str, Exception] = {}
valuinst_tests: dict[str, int] = {} # tests that produced VALUINST packets
for test_name, (instr_name, instructions) in INSTRUCTION_TESTS.items():
try:
all_runs, _, ops, valuinst_count = run_with_retry(instructions)
for op in ops:
if op not in discovered:
discovered[op] = set()
discovered[op].add(f"{test_name}")
if valuinst_count > 0:
valuinst_tests[test_name] = valuinst_count
if DEBUG >= 2:
print(f"\n{''*60}")
print(f"{test_name} ({instr_name}): ops={[hex(op) for op in sorted(ops)]}")
# collect wave patterns from traced SIMD runs (group by exact timing)
patterns: dict[tuple, list] = {} # pattern (types + timing) -> list of (wave_packets, t0)
for traced_simd, blobs in all_runs:
for blob in blobs:
packets = decode_all_blobs([blob])
wave_packets = get_wave_packets(packets)
# only include runs where wave ran on traced SIMD
ws = next((p for p in wave_packets if isinstance(p, WAVESTART)), None)
if ws and ws.simd == traced_simd and wave_packets:
t0 = wave_packets[0]._time
# pattern includes types AND normalized timing
pattern = tuple((type(p).__name__, p._time - t0) for p in wave_packets)
if pattern not in patterns:
patterns[pattern] = []
patterns[pattern].append((wave_packets, t0))
if patterns:
counts = {p: len(runs) for p, runs in patterns.items()}
most_common = max(counts, key=counts.get)
count = counts[most_common]
total = sum(counts.values())
print(f"\n=== most common pattern ({count}/{total} runs) ===")
wave_packets, t0 = patterns[most_common][0]
last_time = t0
for p in wave_packets:
print(format_packet(p, last_time, t0))
last_time = p._time
if len(patterns) > 1:
print(f"\n variations: {len(patterns)} unique timing patterns")
if DEBUG >= 3:
for traced_simd, blobs in all_runs:
print(f"\n=== traced simd={traced_simd} ===")
print_blobs(blobs, wave_only=False)
if DEBUG >= 1:
status = colored("", "green") if ops else (colored("V", "cyan") if valuinst_count > 0 else colored("", "yellow"))
ops_str = ", ".join(hex(op) for op in sorted(ops)) if ops else "none"
valuinst_str = f" valuinst={valuinst_count}" if valuinst_count > 0 and not ops else ""
print(f" {status} {test_name:25s} ops=[{ops_str}]{valuinst_str}")
except Exception as e:
failures[test_name] = e
if DEBUG >= 1:
print(f" {colored('', 'red')} {test_name:25s} FAILED: {e}")
return discovered, failures, valuinst_tests
def print_summary(discovered: dict[int, set[str]], failures: dict[str, Exception], valuinst_tests: dict[str, int]) -> None:
"""Print discovery summary."""
known_ops = {e.value for e in InstOp}
discovered_ops = set(discovered.keys())
print("\n" + "=" * 60)
print("DISCOVERED INSTOP VALUES")
print("=" * 60)
for op in sorted(discovered_ops):
try:
name = InstOp(op).name
status = colored("known", "green")
except ValueError:
name = f"UNKNOWN"
status = colored("NEW!", "yellow")
sources = ", ".join(sorted(discovered[op]))
print(f" 0x{op:02x} {name:20s} ({status}) <- {sources}")
# VALUINST tests (instructions that only produce VALUINST, not INST packets)
valuinst_only = {k: v for k, v in valuinst_tests.items() if not any(k in tests for tests in discovered.values())}
if valuinst_only:
print("\n" + "=" * 60)
print(colored("VALUINST-ONLY INSTRUCTIONS (no InstOp, use VALUINST packet)", "cyan"))
print("=" * 60)
for test_name, count in sorted(valuinst_only.items()):
print(f" {test_name}: {count} VALUINST packets")
# Missing from enum
missing = known_ops - discovered_ops
if missing:
print("\n" + "=" * 60)
print("ENUM VALUES NOT DISCOVERED")
print("=" * 60)
print("(need memory ops: SMEM, VMEM, LDS)")
for op in sorted(missing):
print(f" 0x{op:02x} {InstOp(op).name}")
# New values to add
new_ops = discovered_ops - known_ops
if new_ops:
print("\n" + "=" * 60)
print(colored("NEW INSTOP VALUES TO ADD TO ENUM", "yellow"))
print("=" * 60)
for op in sorted(new_ops):
sources = ", ".join(sorted(discovered[op]))
print(f" {op:#04x}: \"{sources}\",")
# Stats
print("\n" + "=" * 60)
print("STATISTICS")
print("=" * 60)
print(f" Tests run: {len(INSTRUCTION_TESTS)}")
print(f" Tests passed: {len(INSTRUCTION_TESTS) - len(failures)}")
print(f" Tests failed: {len(failures)}")
print(f" Known ops: {len(known_ops)}")
print(f" Discovered: {len(discovered_ops)}")
if known_ops:
print(f" Coverage: {len(discovered_ops & known_ops)}/{len(known_ops)} ({100*len(discovered_ops & known_ops)//len(known_ops)}%)")
print(f" New ops found: {len(new_ops)}")
print(f" VALUINST-only: {len(valuinst_only)}")
if __name__ == "__main__":
print("=" * 60)
print("SQTT InstOp Discovery Tool")
print("=" * 60)
print(f"Testing {len(INSTRUCTION_TESTS)} instruction categories...\n")
discovered, failures, valuinst_tests = discover_all_instops()
print_summary(discovered, failures, valuinst_tests)
@@ -0,0 +1,289 @@
#!/usr/bin/env python3
"""SQTT InstOp discovery from tinygrad-generated kernels.
Runs various tinygrad operations and captures SQTT traces to find new InstOp values.
Requires profiling enabled:
echo 'profile_standard' | sudo tee /sys/class/drm/card1/device/power_dpm_force_performance_level
Run with: DEBUG=1 python extra/assembly/amd/test/discover_instops_tensor.py
For full traces: DEBUG=2 python extra/assembly/amd/test/discover_instops_tensor.py
"""
import os
os.environ["SQTT"] = "1"
os.environ["PROFILE"] = "1"
os.environ["SQTT_LIMIT_SE"] = "2" # Force work to traced SE only
os.environ["SQTT_TOKEN_EXCLUDE"] = "3784" # Exclude noisy packet types
from tinygrad import Tensor, dtypes, Device
from tinygrad.helpers import DEBUG, colored
from tinygrad.runtime.ops_amd import ProfileSQTTEvent, SQTT_SIMD_SEL
from extra.assembly.amd.sqtt import InstOp, decode, INST, WAVESTART, WAVEEND
# ═══════════════════════════════════════════════════════════════════════════════
# HELPERS
# ═══════════════════════════════════════════════════════════════════════════════
def get_inst_ops_from_blobs(blobs: list[bytes]) -> set[int]:
"""Extract all InstOp values from SQTT blobs."""
ops = set()
for blob in blobs:
packets = decode(blob)
in_wave = False
for p in packets:
if isinstance(p, WAVESTART):
in_wave = True
if in_wave and isinstance(p, INST):
ops.add(p.op if isinstance(p.op, int) else p.op.value)
if isinstance(p, WAVEEND):
in_wave = False
return ops
def run_and_capture(fn, attempts: int = 5) -> tuple[set[int], list[bytes]]:
"""Run a function multiple times and collect SQTT traces."""
dev = Device["AMD"]
all_ops = set()
all_blobs = []
SQTT_SIMD_SEL.value = 0
for _ in range(attempts):
dev.profile_events.clear()
fn()
blobs = [ev.blob for ev in dev.profile_events if isinstance(ev, ProfileSQTTEvent)]
ops = get_inst_ops_from_blobs(blobs)
all_ops.update(ops)
all_blobs.extend(blobs)
return all_ops, all_blobs
# ═══════════════════════════════════════════════════════════════════════════════
# TENSOR OPERATIONS TO TEST
# ═══════════════════════════════════════════════════════════════════════════════
TENSOR_TESTS: dict[str, tuple[str, callable]] = {
# Basic arithmetic
"add_f32": ("tensor add float32", lambda: (Tensor.rand(1024) + Tensor.rand(1024)).realize()),
"mul_f32": ("tensor mul float32", lambda: (Tensor.rand(1024) * Tensor.rand(1024)).realize()),
"sub_f32": ("tensor sub float32", lambda: (Tensor.rand(1024) - Tensor.rand(1024)).realize()),
"div_f32": ("tensor div float32", lambda: (Tensor.rand(1024) / (Tensor.rand(1024) + 0.1)).realize()),
# Transcendental
"exp_f32": ("tensor exp float32", lambda: Tensor.rand(1024).exp().realize()),
"log_f32": ("tensor log float32", lambda: (Tensor.rand(1024) + 0.1).log().realize()),
"sqrt_f32": ("tensor sqrt float32", lambda: Tensor.rand(1024).sqrt().realize()),
"sin_f32": ("tensor sin float32", lambda: Tensor.rand(1024).sin().realize()),
"cos_f32": ("tensor cos float32", lambda: Tensor.rand(1024).cos().realize()),
"tanh_f32": ("tensor tanh float32", lambda: Tensor.rand(1024).tanh().realize()),
"sigmoid_f32": ("tensor sigmoid float32", lambda: Tensor.rand(1024).sigmoid().realize()),
# Reductions
"sum_f32": ("tensor sum float32", lambda: Tensor.rand(1024).sum().realize()),
"max_f32": ("tensor max float32", lambda: Tensor.rand(1024).max().realize()),
"mean_f32": ("tensor mean float32", lambda: Tensor.rand(1024).mean().realize()),
# Matmul - small
"matmul_small": ("matmul 32x32", lambda: (Tensor.rand(32, 32) @ Tensor.rand(32, 32)).realize()),
# Matmul - medium (might use WMMA)
"matmul_medium": ("matmul 128x128", lambda: (Tensor.rand(128, 128) @ Tensor.rand(128, 128)).realize()),
# Matmul - larger (more likely to use WMMA)
"matmul_large": ("matmul 256x256", lambda: (Tensor.rand(256, 256) @ Tensor.rand(256, 256)).realize()),
# Different dtypes
"add_f16": ("tensor add float16", lambda: (Tensor.rand(1024, dtype=dtypes.float16) + Tensor.rand(1024, dtype=dtypes.float16)).realize()),
"mul_f16": ("tensor mul float16", lambda: (Tensor.rand(1024, dtype=dtypes.float16) * Tensor.rand(1024, dtype=dtypes.float16)).realize()),
"matmul_f16": ("matmul float16 128x128", lambda: (Tensor.rand(128, 128, dtype=dtypes.float16) @ Tensor.rand(128, 128, dtype=dtypes.float16)).realize()),
# Integer ops
"add_i32": ("tensor add int32", lambda: (Tensor.randint(1024, high=1000) + Tensor.randint(1024, high=1000)).realize()),
"mul_i32": ("tensor mul int32", lambda: (Tensor.randint(1024, high=100) * Tensor.randint(1024, high=100)).realize()),
# Bitwise
"and_i32": ("tensor bitwise and", lambda: (Tensor.randint(1024, high=1000) & Tensor.randint(1024, high=1000)).realize()),
"or_i32": ("tensor bitwise or", lambda: (Tensor.randint(1024, high=1000) | Tensor.randint(1024, high=1000)).realize()),
"xor_i32": ("tensor bitwise xor", lambda: (Tensor.randint(1024, high=1000) ^ Tensor.randint(1024, high=1000)).realize()),
"lshift_i32": ("tensor left shift", lambda: (Tensor.randint(1024, high=1000) << 2).realize()),
"rshift_i32": ("tensor right shift", lambda: (Tensor.randint(1024, high=1000) >> 2).realize()),
# Comparisons
"cmp_eq": ("tensor compare eq", lambda: (Tensor.rand(1024) == 0.5).realize()),
"cmp_lt": ("tensor compare lt", lambda: (Tensor.rand(1024) < 0.5).realize()),
"cmp_gt": ("tensor compare gt", lambda: (Tensor.rand(1024) > 0.5).realize()),
# Where/select
"where": ("tensor where", lambda: Tensor.rand(1024).where(Tensor.rand(1024), Tensor.rand(1024)).realize()),
# Reshaping/movement (may not generate interesting ops but let's check)
"reshape": ("tensor reshape", lambda: Tensor.rand(32, 32).reshape(16, 64).realize()),
"permute": ("tensor permute", lambda: Tensor.rand(32, 32).permute(1, 0).contiguous().realize()),
"expand": ("tensor expand", lambda: Tensor.rand(1, 32).expand(32, 32).contiguous().realize()),
# Pad
"pad": ("tensor pad", lambda: Tensor.rand(30, 30).pad(((1, 1), (1, 1))).realize()),
# Conv2D - small
"conv2d_small": ("conv2d 3x3", lambda: Tensor.rand(1, 3, 32, 32).conv2d(Tensor.rand(8, 3, 3, 3)).realize()),
# Conv2D - larger
"conv2d_medium": ("conv2d 3x3 64ch", lambda: Tensor.rand(1, 64, 32, 32).conv2d(Tensor.rand(64, 64, 3, 3)).realize()),
# Pooling
"maxpool": ("max pool 2x2", lambda: Tensor.rand(1, 3, 32, 32).max_pool2d((2, 2)).realize()),
"avgpool": ("avg pool 2x2", lambda: Tensor.rand(1, 3, 32, 32).avg_pool2d((2, 2)).realize()),
# Softmax
"softmax": ("softmax", lambda: Tensor.rand(32, 128).softmax().realize()),
# LayerNorm-like
"layernorm": ("layer norm pattern", lambda: _layernorm(Tensor.rand(32, 128))),
# BatchNorm-like
"batchnorm": ("batch norm pattern", lambda: _batchnorm(Tensor.rand(1, 64, 32, 32))),
# Dropout-like (during training)
"dropout": ("dropout pattern", lambda: (Tensor.rand(1024) * (Tensor.rand(1024) > 0.5)).realize()),
# Cast operations
"cast_f32_to_f16": ("cast f32->f16", lambda: Tensor.rand(1024).cast(dtypes.float16).realize()),
"cast_f16_to_f32": ("cast f16->f32", lambda: Tensor.rand(1024, dtype=dtypes.float16).cast(dtypes.float32).realize()),
"cast_f32_to_i32": ("cast f32->i32", lambda: (Tensor.rand(1024) * 100).cast(dtypes.int32).realize()),
"cast_i32_to_f32": ("cast i32->f32", lambda: Tensor.randint(1024, high=100).cast(dtypes.float32).realize()),
# Clamp/clip
"clamp": ("tensor clamp", lambda: Tensor.rand(1024).clamp(0.2, 0.8).realize()),
# Abs/neg
"abs": ("tensor abs", lambda: (Tensor.rand(1024) - 0.5).abs().realize()),
"neg": ("tensor neg", lambda: (-Tensor.rand(1024)).realize()),
# Reciprocal
"recip": ("tensor reciprocal", lambda: (Tensor.rand(1024) + 0.1).reciprocal().realize()),
# Power
"pow2": ("tensor pow 2", lambda: (Tensor.rand(1024) ** 2).realize()),
"pow3": ("tensor pow 3", lambda: (Tensor.rand(1024) ** 3).realize()),
}
def _layernorm(x: Tensor) -> Tensor:
"""Simple layer normalization pattern."""
mean = x.mean(axis=-1, keepdim=True)
var = ((x - mean) ** 2).mean(axis=-1, keepdim=True)
return ((x - mean) / (var + 1e-5).sqrt()).realize()
def _batchnorm(x: Tensor) -> Tensor:
"""Simple batch normalization pattern."""
mean = x.mean(axis=(0, 2, 3), keepdim=True)
var = ((x - mean) ** 2).mean(axis=(0, 2, 3), keepdim=True)
return ((x - mean) / (var + 1e-5).sqrt()).realize()
# ═══════════════════════════════════════════════════════════════════════════════
# DISCOVERY
# ═══════════════════════════════════════════════════════════════════════════════
def discover_all_instops() -> tuple[dict[int, set[str]], dict[str, Exception]]:
"""Run all tensor tests and collect InstOp values."""
discovered: dict[int, set[str]] = {}
failures: dict[str, Exception] = {}
for test_name, (desc, fn) in TENSOR_TESTS.items():
try:
ops, blobs = run_and_capture(fn)
for op in ops:
if op not in discovered:
discovered[op] = set()
discovered[op].add(test_name)
if DEBUG >= 1:
status = colored("", "green") if ops else colored("", "yellow")
ops_str = ", ".join(hex(op) for op in sorted(ops)) if ops else "none"
print(f" {status} {test_name:25s} [{desc:25s}] ops=[{ops_str}]")
if DEBUG >= 2 and blobs:
# Show first wave trace
for blob in blobs[:1]:
packets = decode(blob)
print(f" First blob: {len(blob)} bytes, {len(packets)} packets")
except Exception as e:
failures[test_name] = e
if DEBUG >= 1:
print(f" {colored('', 'red')} {test_name:25s} FAILED: {e}")
return discovered, failures
def print_summary(discovered: dict[int, set[str]], failures: dict[str, Exception]) -> None:
"""Print discovery summary."""
known_ops = {e.value for e in InstOp}
discovered_ops = set(discovered.keys())
print("\n" + "=" * 70)
print("DISCOVERED INSTOP VALUES FROM TINYGRAD KERNELS")
print("=" * 70)
for op in sorted(discovered_ops):
try:
name = InstOp(op).name
status = colored("known", "green")
except ValueError:
name = "UNKNOWN"
status = colored("NEW!", "yellow")
sources = ", ".join(sorted(discovered[op]))
# Truncate sources if too long
if len(sources) > 60:
sources = sources[:57] + "..."
print(f" 0x{op:02x} {name:20s} ({status}) <- {sources}")
# New values to add
new_ops = discovered_ops - known_ops
if new_ops:
print("\n" + "=" * 70)
print(colored("NEW INSTOP VALUES TO ADD TO ENUM", "yellow"))
print("=" * 70)
for op in sorted(new_ops):
sources = ", ".join(sorted(discovered[op]))
print(f" 0x{op:02x}: discovered from [{sources}]")
# Missing from enum (not discovered)
missing = known_ops - discovered_ops
if missing:
print("\n" + "=" * 70)
print("ENUM VALUES NOT DISCOVERED (may need specific instruction patterns)")
print("=" * 70)
for op in sorted(missing):
print(f" 0x{op:02x} {InstOp(op).name}")
# Stats
print("\n" + "=" * 70)
print("STATISTICS")
print("=" * 70)
print(f" Tests run: {len(TENSOR_TESTS)}")
print(f" Tests passed: {len(TENSOR_TESTS) - len(failures)}")
print(f" Tests failed: {len(failures)}")
print(f" Known ops: {len(known_ops)}")
print(f" Discovered: {len(discovered_ops)}")
if known_ops:
coverage = len(discovered_ops & known_ops)
print(f" Coverage: {coverage}/{len(known_ops)} ({100*coverage//len(known_ops)}%)")
print(f" New ops found: {len(new_ops)}")
if failures:
print("\n" + "=" * 70)
print("FAILURES")
print("=" * 70)
for name, e in failures.items():
print(f" {name}: {e}")
if __name__ == "__main__":
print("=" * 70)
print("SQTT InstOp Discovery from Tinygrad Kernels")
print("=" * 70)
print(f"Testing {len(TENSOR_TESTS)} tensor operations...\n")
discovered, failures = discover_all_instops()
print_summary(discovered, failures)
-13
View File
@@ -23,19 +23,6 @@ def get_llvm_objdump():
if shutil.which(p): return p
raise FileNotFoundError("llvm-objdump not found")
ARCH_TO_TARGET:dict[str, list[str]] = {
"rdna3":["gfx1100"],
"rdna4":["gfx1200"],
"cdna":["gfx950", "gfx942"],
}
TARGET_TO_ARCH:dict[str, str] = {t:arch for arch,targets in ARCH_TO_TARGET.items() for t in targets}
def get_target(arch:str) -> str: return ARCH_TO_TARGET[arch][0]
def get_mattr(arch:str) -> str:
return {"rdna3":"+real-true16,+wavefrontsize32", "rdna4":"+real-true16,+wavefrontsize32", "cdna":"+wavefrontsize64"}[arch]
# ═══════════════════════════════════════════════════════════════════════════════
# EXECUTION CONTEXT (for testing compiled pseudocode)
# ═══════════════════════════════════════════════════════════════════════════════
+20 -69
View File
@@ -1,53 +1,23 @@
"""Test infrastructure for hardware-validated RDNA3 emulator tests.
Uses run_asm() with memory output, so tests can run on both emulator and real hardware.
Set USE_HW=1 to run on both emulator and hardware, comparing results.
Set USE_HW=1 to run on both emulator and real hardware, comparing results.
"""
import ctypes, math, os, struct
import ctypes, os, struct
from extra.assembly.amd.autogen.rdna3.ins import *
from extra.assembly.amd.dsl import RawImm
from extra.assembly.amd.emu import WaveState, run_asm, set_valid_mem_ranges
from extra.assembly.amd.pcode import _i32, _f32
from extra.assembly.amd.emu import run_asm
from extra.assembly.amd.dsl import NULL, SCC, VCC_LO, VCC_HI, EXEC_LO, EXEC_HI, M0
def _i32(f: float) -> int: return struct.unpack('<I', struct.pack('<f', f))[0]
def _f32(i: int) -> float: return struct.unpack('<f', struct.pack('<I', i & 0xFFFFFFFF))[0]
# f16 conversion helpers
def _f16(i: int) -> float: return struct.unpack('<e', struct.pack('<H', i & 0xFFFF))[0]
def f32_to_f16(f: float) -> int:
f = float(f)
if math.isnan(f): return 0x7e00
if math.isinf(f): return 0x7c00 if f > 0 else 0xfc00
try: return struct.unpack('<H', struct.pack('<e', f))[0]
except OverflowError: return 0x7c00 if f > 0 else 0xfc00
# For backwards compatibility with tests using SrcEnum.NULL etc.
class SrcEnum:
NULL = NULL
VCC_LO = VCC_LO
VCC_HI = VCC_HI
EXEC_LO = EXEC_LO
EXEC_HI = EXEC_HI
SCC = SCC
M0 = M0
POS_HALF = 0.5
NEG_HALF = -0.5
POS_ONE = 1.0
NEG_ONE = -1.0
POS_TWO = 2.0
NEG_TWO = -2.0
POS_FOUR = 4.0
NEG_FOUR = -4.0
VCC = VCC_LO # For VOP3SD sdst field (VCC_LO is exported from dsl)
VCC = SrcEnum.VCC_LO # For VOP3SD sdst field
USE_HW = os.environ.get("USE_HW", "0") == "1"
FLOAT_TOLERANCE = 1e-5
# Output buffer layout: vgpr[16][32], sgpr[16], vcc, scc, exec
# Output buffer layout: vgpr[16][32], sgpr[16], vcc, scc
N_VGPRS, N_SGPRS, WAVE_SIZE = 16, 16, 32
VGPR_BYTES = N_VGPRS * WAVE_SIZE * 4 # 16 regs * 32 lanes * 4 bytes = 2048
SGPR_BYTES = N_SGPRS * 4 # 16 regs * 4 bytes = 64
OUT_BYTES = VGPR_BYTES + SGPR_BYTES + 12 # + vcc + scc + exec
OUT_BYTES = VGPR_BYTES + SGPR_BYTES + 8 # + vcc + scc
# Float conversion helpers
def f2i(f: float) -> int: return _i32(f)
@@ -58,14 +28,6 @@ def i642f(i: int) -> float: return struct.unpack('<d', struct.pack('<Q', i))[0]
def assemble(instructions: list) -> bytes:
return b''.join(inst.to_bytes() for inst in instructions)
# Simple WaveState class for test output parsing (mirrors emu.py interface for tests)
class WaveState:
def __init__(self):
self.vgpr = [[0] * 256 for _ in range(32)] # vgpr[lane][reg]
self.sgpr = [0] * 128
self.vcc = 0
self.scc = 0
def get_prologue_epilogue(n_lanes: int) -> tuple[list, list]:
"""Generate prologue and epilogue instructions for state capture."""
prologue = [
@@ -77,36 +39,29 @@ def get_prologue_epilogue(n_lanes: int) -> tuple[list, list]:
prologue.append(v_mov_b32_e32(v[i], 0))
for i in range(N_SGPRS):
prologue.append(s_mov_b32(s[i], 0))
prologue.append(s_mov_b32(VCC_LO, 0))
prologue.append(s_mov_b32(s[SrcEnum.VCC_LO - 128], 0))
epilogue = [
s_mov_b32(s[90], VCC_LO),
s_mov_b32(s[90], SrcEnum.VCC_LO),
s_cselect_b32(s[91], 1, 0),
# Save EXEC early (before we modify it for VGPR stores)
s_mov_b32(s[95], EXEC_LO),
# Restore EXEC to all active lanes for VGPR stores (test may have modified EXEC)
s_mov_b32(EXEC_LO, (1 << n_lanes) - 1),
s_load_b64(s[92:93], s[80:81], 0, soffset=NULL),
s_waitcnt(0), # simm16=0 waits for all
s_load_b64(s[92:93], s[80], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_lshlrev_b32_e32(v[240], 2, v[255]),
]
for i in range(N_VGPRS):
epilogue.append(global_store_b32(addr=v[240], data=v[i], saddr=s[92:93], offset=i * WAVE_SIZE * 4))
epilogue.append(global_store_b32(addr=v[240], data=v[i], saddr=s[92], offset=i * WAVE_SIZE * 4))
epilogue.append(v_mov_b32_e32(v[241], 0))
epilogue.append(v_cmp_eq_u32_e32(v[255], v[241]))
epilogue.append(s_and_saveexec_b32(s[94], VCC_LO))
epilogue.append(s_and_saveexec_b32(s[94], SrcEnum.VCC_LO))
epilogue.append(v_mov_b32_e32(v[240], 0))
for i in range(N_SGPRS):
epilogue.append(v_mov_b32_e32(v[243], s[i]))
epilogue.append(global_store_b32(addr=v[240], data=v[243], saddr=s[92:93], offset=VGPR_BYTES + i * 4))
epilogue.append(global_store_b32(addr=v[240], data=v[243], saddr=s[92], offset=VGPR_BYTES + i * 4))
epilogue.append(v_mov_b32_e32(v[243], s[90]))
epilogue.append(global_store_b32(addr=v[240], data=v[243], saddr=s[92:93], offset=VGPR_BYTES + SGPR_BYTES))
epilogue.append(global_store_b32(addr=v[240], data=v[243], saddr=s[92], offset=VGPR_BYTES + SGPR_BYTES))
epilogue.append(v_mov_b32_e32(v[243], s[91]))
epilogue.append(global_store_b32(addr=v[240], data=v[243], saddr=s[92:93], offset=VGPR_BYTES + SGPR_BYTES + 4))
# Store EXEC (saved earlier in s[95])
epilogue.append(v_mov_b32_e32(v[243], s[95]))
epilogue.append(global_store_b32(addr=v[240], data=v[243], saddr=s[92:93], offset=VGPR_BYTES + SGPR_BYTES + 8))
epilogue.append(s_mov_b32(EXEC_LO, s[94]))
epilogue.append(global_store_b32(addr=v[240], data=v[243], saddr=s[92], offset=VGPR_BYTES + SGPR_BYTES + 4))
epilogue.append(s_mov_b32(s[SrcEnum.EXEC_LO - 128], s[94]))
epilogue.append(s_endpgm())
return prologue, epilogue
@@ -121,8 +76,6 @@ def parse_output(out_buf: bytes, n_lanes: int) -> WaveState:
st.sgpr[i] = struct.unpack_from('<I', out_buf, VGPR_BYTES + i * 4)[0]
st.vcc = struct.unpack_from('<I', out_buf, VGPR_BYTES + SGPR_BYTES)[0]
st.scc = struct.unpack_from('<I', out_buf, VGPR_BYTES + SGPR_BYTES + 4)[0]
# Store EXEC in its proper location (index 126)
st.sgpr[EXEC_LO.offset] = struct.unpack_from('<I', out_buf, VGPR_BYTES + SGPR_BYTES + 8)[0]
return st
def run_program_emu(instructions: list, n_lanes: int = 1) -> WaveState:
@@ -138,9 +91,9 @@ def run_program_emu(instructions: list, n_lanes: int = 1) -> WaveState:
kernel_buf = (ctypes.c_char * len(code)).from_buffer_copy(code)
lib_ptr = ctypes.addressof(kernel_buf)
set_valid_mem_ranges({(out_addr, OUT_BYTES), (args_ptr, 8)})
# rsrc2: USER_SGPR_COUNT=2, ENABLE_SGPR_WORKGROUP_ID_X/Y/Z=1, LDS_SIZE=128 (64KB)
rsrc2 = 0x19c | (128 << 15)
scratch_size = 0x10000 # 64KB per lane, matches .amdhsa_private_segment_fixed_size in run_program_hw
result = run_asm(lib_ptr, len(code), 1, 1, 1, n_lanes, 1, 1, args_ptr, rsrc2)
assert result == 0, f"run_asm failed with {result}"
@@ -176,8 +129,6 @@ test:
.amdhsa_user_sgpr_kernarg_segment_ptr 1
.amdhsa_kernarg_size 8
.amdhsa_group_segment_fixed_size 65536
.amdhsa_private_segment_fixed_size 65536
.amdhsa_enable_private_segment 1
.end_amdhsa_kernel
.amdgpu_metadata
@@ -190,7 +141,7 @@ amdhsa.kernels:
.symbol: test.kd
.kernarg_segment_size: 8
.group_segment_fixed_size: 65536
.private_segment_fixed_size: 65536
.private_segment_fixed_size: 0
.kernarg_segment_align: 8
.wavefront_size: 32
.sgpr_count: 96
+22 -72
View File
@@ -20,7 +20,7 @@ class TestDS2Addr(unittest.TestCase):
v_mov_b32_e32(v[1], s[0]),
DS(DSOp.DS_STORE_2ADDR_B32, addr=v[10], data0=v[0], data1=v[1], vdst=v[0], offset0=0, offset1=1),
s_waitcnt(lgkmcnt=0),
DS(DSOp.DS_LOAD_2ADDR_B32, addr=v[10], vdst=v[2:3], offset0=0, offset1=1),
DS(DSOp.DS_LOAD_2ADDR_B32, addr=v[10], vdst=v[2], offset0=0, offset1=1),
s_waitcnt(lgkmcnt=0),
]
st = run_program(instructions, n_lanes=1)
@@ -39,9 +39,9 @@ class TestDS2Addr(unittest.TestCase):
v_mov_b32_e32(v[2], s[0]),
s_mov_b32(s[0], 0x9ABCDEF0),
v_mov_b32_e32(v[3], s[0]),
DS(DSOp.DS_STORE_2ADDR_B64, addr=v[10], data0=v[0:1], data1=v[2:3], vdst=v[0], offset0=0, offset1=2),
DS(DSOp.DS_STORE_2ADDR_B64, addr=v[10], data0=v[0], data1=v[2], vdst=v[0], offset0=0, offset1=2),
s_waitcnt(lgkmcnt=0),
DS(DSOp.DS_LOAD_2ADDR_B64, addr=v[10], vdst=v[4:7], offset0=0, offset1=2),
DS(DSOp.DS_LOAD_2ADDR_B64, addr=v[10], vdst=v[4], offset0=0, offset1=2),
s_waitcnt(lgkmcnt=0),
]
st = run_program(instructions, n_lanes=1)
@@ -64,7 +64,7 @@ class TestDS2AddrMore(unittest.TestCase):
v_mov_b32_e32(v[1], s[2]),
DS(DSOp.DS_STORE_2ADDR_B32, addr=v[10], data0=v[0], data1=v[1], vdst=v[0], offset0=2, offset1=5),
s_waitcnt(lgkmcnt=0),
DS(DSOp.DS_LOAD_2ADDR_B32, addr=v[10], vdst=v[2:3], offset0=2, offset1=5),
DS(DSOp.DS_LOAD_2ADDR_B32, addr=v[10], vdst=v[2], offset0=2, offset1=5),
s_waitcnt(lgkmcnt=0),
]
st = run_program(instructions, n_lanes=1)
@@ -88,7 +88,7 @@ class TestDS2AddrMore(unittest.TestCase):
v_mov_b32_e32(v[0], s[2]),
ds_store_b32(addr=v[10], data0=v[0], offset0=12),
s_waitcnt(lgkmcnt=0),
DS(DSOp.DS_LOAD_2ADDR_B64, addr=v[10], vdst=v[4:7], offset0=0, offset1=1),
DS(DSOp.DS_LOAD_2ADDR_B64, addr=v[10], vdst=v[4], offset0=0, offset1=1),
s_waitcnt(lgkmcnt=0),
]
st = run_program(instructions, n_lanes=1)
@@ -109,7 +109,7 @@ class TestDS2AddrMore(unittest.TestCase):
s_waitcnt(lgkmcnt=0),
s_mov_b32(s[2], 0xDEADBEEF),
v_mov_b32_e32(v[4], s[2]), # Sentinel
DS(DSOp.DS_LOAD_2ADDR_B32, addr=v[10], vdst=v[2:3], offset0=0, offset1=1),
DS(DSOp.DS_LOAD_2ADDR_B32, addr=v[10], vdst=v[2], offset0=0, offset1=1),
s_waitcnt(lgkmcnt=0),
]
st = run_program(instructions, n_lanes=1)
@@ -125,11 +125,11 @@ class TestDS2AddrMore(unittest.TestCase):
v_mov_b32_e32(v[0], s[2]),
s_mov_b32(s[2], 0xCAFEBABE),
v_mov_b32_e32(v[1], s[2]),
ds_store_b64(addr=v[10], data0=v[0:1], offset0=0),
ds_store_b64(addr=v[10], data0=v[0], offset0=0),
s_waitcnt(lgkmcnt=0),
s_mov_b32(s[2], 0x12345678),
v_mov_b32_e32(v[4], s[2]), # Sentinel
ds_load_b64(addr=v[10], vdst=v[2:3], offset0=0),
ds_load_b64(addr=v[10], vdst=v[2], offset0=0),
s_waitcnt(lgkmcnt=0),
]
st = run_program(instructions, n_lanes=1)
@@ -138,56 +138,6 @@ class TestDS2AddrMore(unittest.TestCase):
self.assertEqual(st.vgpr[0][4], 0x12345678, "v4 should be untouched")
class TestDSB128(unittest.TestCase):
"""Tests for DS_STORE_B128 and DS_LOAD_B128 (128-bit / 4 dwords)."""
def test_ds_store_load_b128(self):
"""DS_STORE_B128 stores 4 VGPRs, DS_LOAD_B128 loads them back."""
instructions = [
v_mov_b32_e32(v[10], 0),
s_mov_b32(s[0], 0x11111111),
v_mov_b32_e32(v[0], s[0]),
s_mov_b32(s[0], 0x22222222),
v_mov_b32_e32(v[1], s[0]),
s_mov_b32(s[0], 0x33333333),
v_mov_b32_e32(v[2], s[0]),
s_mov_b32(s[0], 0x44444444),
v_mov_b32_e32(v[3], s[0]),
ds_store_b128(addr=v[10], data0=v[0:3]),
s_waitcnt(lgkmcnt=0),
ds_load_b128(addr=v[10], vdst=v[4:7]),
s_waitcnt(lgkmcnt=0),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][4], 0x11111111, "v4 should have first dword")
self.assertEqual(st.vgpr[0][5], 0x22222222, "v5 should have second dword")
self.assertEqual(st.vgpr[0][6], 0x33333333, "v6 should have third dword")
self.assertEqual(st.vgpr[0][7], 0x44444444, "v7 should have fourth dword")
def test_ds_store_b128_with_offset(self):
"""DS_STORE_B128 with non-zero offset."""
instructions = [
v_mov_b32_e32(v[10], 0),
s_mov_b32(s[0], 0xAAAAAAAA),
v_mov_b32_e32(v[0], s[0]),
s_mov_b32(s[0], 0xBBBBBBBB),
v_mov_b32_e32(v[1], s[0]),
s_mov_b32(s[0], 0xCCCCCCCC),
v_mov_b32_e32(v[2], s[0]),
s_mov_b32(s[0], 0xDDDDDDDD),
v_mov_b32_e32(v[3], s[0]),
DS(DSOp.DS_STORE_B128, addr=v[10], data0=v[0:3], offset0=16),
s_waitcnt(lgkmcnt=0),
DS(DSOp.DS_LOAD_B128, addr=v[10], vdst=v[4:7], offset0=16),
s_waitcnt(lgkmcnt=0),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][4], 0xAAAAAAAA)
self.assertEqual(st.vgpr[0][5], 0xBBBBBBBB)
self.assertEqual(st.vgpr[0][6], 0xCCCCCCCC)
self.assertEqual(st.vgpr[0][7], 0xDDDDDDDD)
class TestDSAtomic(unittest.TestCase):
"""Tests for DS atomic operations."""
@@ -502,7 +452,7 @@ class TestDS2AddrStride64(unittest.TestCase):
v_mov_b32_e32(v[1], s[0]),
DS(DSOp.DS_STORE_2ADDR_STRIDE64_B32, addr=v[10], data0=v[0], data1=v[1], vdst=v[0], offset0=1, offset1=2),
s_waitcnt(lgkmcnt=0),
DS(DSOp.DS_LOAD_2ADDR_STRIDE64_B32, addr=v[10], vdst=v[2:3], offset0=1, offset1=2),
DS(DSOp.DS_LOAD_2ADDR_STRIDE64_B32, addr=v[10], vdst=v[2], offset0=1, offset1=2),
s_waitcnt(lgkmcnt=0),
]
st = run_program(instructions, n_lanes=1)
@@ -521,9 +471,9 @@ class TestDS2AddrStride64(unittest.TestCase):
v_mov_b32_e32(v[2], s[0]),
s_mov_b32(s[0], 0x9ABCDEF0),
v_mov_b32_e32(v[3], s[0]),
DS(DSOp.DS_STORE_2ADDR_STRIDE64_B64, addr=v[10], data0=v[0:1], data1=v[2:3], vdst=v[0], offset0=1, offset1=2),
DS(DSOp.DS_STORE_2ADDR_STRIDE64_B64, addr=v[10], data0=v[0], data1=v[2], vdst=v[0], offset0=1, offset1=2),
s_waitcnt(lgkmcnt=0),
DS(DSOp.DS_LOAD_2ADDR_STRIDE64_B64, addr=v[10], vdst=v[4:7], offset0=1, offset1=2),
DS(DSOp.DS_LOAD_2ADDR_STRIDE64_B64, addr=v[10], vdst=v[4], offset0=1, offset1=2),
s_waitcnt(lgkmcnt=0),
]
st = run_program(instructions, n_lanes=1)
@@ -546,9 +496,9 @@ class TestDS2AddrStride64(unittest.TestCase):
v_mov_b32_e32(v[2], s[0]),
s_mov_b32(s[0], 0xBBBBBBBB),
v_mov_b32_e32(v[3], s[0]),
DS(DSOp.DS_STOREXCHG_2ADDR_RTN_B32, addr=v[10], data0=v[2], data1=v[3], vdst=v[4:5], offset0=0, offset1=1),
DS(DSOp.DS_STOREXCHG_2ADDR_RTN_B32, addr=v[10], data0=v[2], data1=v[3], vdst=v[4], offset0=0, offset1=1),
s_waitcnt(lgkmcnt=0),
DS(DSOp.DS_LOAD_2ADDR_B32, addr=v[10], vdst=v[6:7], offset0=0, offset1=1),
DS(DSOp.DS_LOAD_2ADDR_B32, addr=v[10], vdst=v[6], offset0=0, offset1=1),
s_waitcnt(lgkmcnt=0),
]
st = run_program(instructions, n_lanes=1)
@@ -566,15 +516,15 @@ class TestDS2AddrStride64(unittest.TestCase):
v_mov_b32_e32(v[0], s[0]), # initial low
s_mov_b32(s[0], 0xCAFEBABE),
v_mov_b32_e32(v[1], s[0]), # initial high
DS(DSOp.DS_STORE_B64, addr=v[10], data0=v[0:1], vdst=v[0], offset0=0),
DS(DSOp.DS_STORE_B64, addr=v[10], data0=v[0], vdst=v[0], offset0=0),
s_waitcnt(lgkmcnt=0),
s_mov_b32(s[0], 0x12345678),
v_mov_b32_e32(v[2], s[0]), # new low
s_mov_b32(s[0], 0x9ABCDEF0),
v_mov_b32_e32(v[3], s[0]), # new high
DS(DSOp.DS_STOREXCHG_RTN_B64, addr=v[10], data0=v[2:3], vdst=v[4:5], offset0=0),
DS(DSOp.DS_STOREXCHG_RTN_B64, addr=v[10], data0=v[2], vdst=v[4], offset0=0),
s_waitcnt(lgkmcnt=0),
DS(DSOp.DS_LOAD_B64, addr=v[10], vdst=v[6:7], offset0=0),
DS(DSOp.DS_LOAD_B64, addr=v[10], vdst=v[6], offset0=0),
s_waitcnt(lgkmcnt=0),
]
st = run_program(instructions, n_lanes=1)
@@ -591,9 +541,9 @@ class TestDS2AddrStride64(unittest.TestCase):
v_mov_b32_e32(v[0], s[0]),
s_mov_b32(s[0], 0x22222222),
v_mov_b32_e32(v[1], s[0]),
DS(DSOp.DS_STORE_2ADDR_STRIDE64_B64, addr=v[10], data0=v[0:1], data1=v[0:1], vdst=v[0], offset0=1, offset1=2),
DS(DSOp.DS_STORE_2ADDR_STRIDE64_B64, addr=v[10], data0=v[0], data1=v[0], vdst=v[0], offset0=1, offset1=2),
s_waitcnt(lgkmcnt=0),
DS(DSOp.DS_LOAD_2ADDR_STRIDE64_B64, addr=v[10], vdst=v[2:5], offset0=1, offset1=2),
DS(DSOp.DS_LOAD_2ADDR_STRIDE64_B64, addr=v[10], vdst=v[2], offset0=1, offset1=2),
s_waitcnt(lgkmcnt=0),
]
st = run_program(instructions, n_lanes=1)
@@ -616,9 +566,9 @@ class TestDS2AddrStride64(unittest.TestCase):
v_mov_b32_e32(v[2], s[0]),
s_mov_b32(s[0], 0xBBBBBBBB),
v_mov_b32_e32(v[3], s[0]),
DS(DSOp.DS_STOREXCHG_2ADDR_STRIDE64_RTN_B32, addr=v[10], data0=v[2], data1=v[3], vdst=v[4:5], offset0=1, offset1=2),
DS(DSOp.DS_STOREXCHG_2ADDR_STRIDE64_RTN_B32, addr=v[10], data0=v[2], data1=v[3], vdst=v[4], offset0=1, offset1=2),
s_waitcnt(lgkmcnt=0),
DS(DSOp.DS_LOAD_2ADDR_STRIDE64_B32, addr=v[10], vdst=v[6:7], offset0=1, offset1=2),
DS(DSOp.DS_LOAD_2ADDR_STRIDE64_B32, addr=v[10], vdst=v[6], offset0=1, offset1=2),
s_waitcnt(lgkmcnt=0),
]
st = run_program(instructions, n_lanes=1)
@@ -635,13 +585,13 @@ class TestDS2AddrStride64(unittest.TestCase):
v_mov_b32_e32(v[0], s[0]),
s_mov_b32(s[0], 0x22222222),
v_mov_b32_e32(v[1], s[0]),
DS(DSOp.DS_STORE_2ADDR_STRIDE64_B64, addr=v[10], data0=v[0:1], data1=v[0:1], vdst=v[0], offset0=1, offset1=2),
DS(DSOp.DS_STORE_2ADDR_STRIDE64_B64, addr=v[10], data0=v[0], data1=v[0], vdst=v[0], offset0=1, offset1=2),
s_waitcnt(lgkmcnt=0),
s_mov_b32(s[0], 0xAAAAAAAA),
v_mov_b32_e32(v[6], s[0]),
s_mov_b32(s[0], 0xBBBBBBBB),
v_mov_b32_e32(v[7], s[0]),
DS(DSOp.DS_STOREXCHG_2ADDR_STRIDE64_RTN_B64, addr=v[10], data0=v[6:7], data1=v[6:7], vdst=v[8:11], offset0=1, offset1=2),
DS(DSOp.DS_STOREXCHG_2ADDR_STRIDE64_RTN_B64, addr=v[10], data0=v[6], data1=v[6], vdst=v[8], offset0=1, offset1=2),
s_waitcnt(lgkmcnt=0),
]
st = run_program(instructions, n_lanes=1)
+39 -39
View File
@@ -11,7 +11,7 @@ class TestFlatAtomic(unittest.TestCase):
def _make_test(self, setup_instrs, atomic_instr, check_fn, test_offset=2000):
"""Helper to create atomic test instructions."""
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_load_b64(s[2:3], s[80], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
@@ -30,12 +30,12 @@ class TestFlatAtomic(unittest.TestCase):
setup = [
s_mov_b32(s[0], 100),
v_mov_b32_e32(v[2], s[0]),
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
s_mov_b32(s[0], 50),
v_mov_b32_e32(v[3], s[0]),
]
atomic = FLAT(FLATOp.FLAT_ATOMIC_ADD_U32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
atomic = FLAT(FLATOp.FLAT_ATOMIC_ADD_U32, addr=v[0], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
def check(st):
self.assertEqual(st.vgpr[0][4], 100)
self._make_test(setup, atomic, check, TEST_OFFSET)
@@ -46,12 +46,12 @@ class TestFlatAtomic(unittest.TestCase):
setup = [
s_mov_b32(s[0], 0xAAAAAAAA),
v_mov_b32_e32(v[2], s[0]),
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
s_mov_b32(s[0], 0xBBBBBBBB),
v_mov_b32_e32(v[3], s[0]),
]
atomic = FLAT(FLATOp.FLAT_ATOMIC_SWAP_B32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
atomic = FLAT(FLATOp.FLAT_ATOMIC_SWAP_B32, addr=v[0], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
def check(st):
self.assertEqual(st.vgpr[0][4], 0xAAAAAAAA)
self._make_test(setup, atomic, check, TEST_OFFSET)
@@ -62,12 +62,12 @@ class TestFlatAtomic(unittest.TestCase):
setup = [
s_mov_b32(s[0], 0xFF00FF00),
v_mov_b32_e32(v[2], s[0]),
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
s_mov_b32(s[0], 0xFFFF0000),
v_mov_b32_e32(v[3], s[0]),
]
atomic = FLAT(FLATOp.FLAT_ATOMIC_AND_B32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
atomic = FLAT(FLATOp.FLAT_ATOMIC_AND_B32, addr=v[0], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
def check(st):
self.assertEqual(st.vgpr[0][4], 0xFF00FF00)
self._make_test(setup, atomic, check, TEST_OFFSET)
@@ -78,12 +78,12 @@ class TestFlatAtomic(unittest.TestCase):
setup = [
s_mov_b32(s[0], 0x00FF0000),
v_mov_b32_e32(v[2], s[0]),
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
s_mov_b32(s[0], 0x0000FF00),
v_mov_b32_e32(v[3], s[0]),
]
atomic = FLAT(FLATOp.FLAT_ATOMIC_OR_B32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
atomic = FLAT(FLATOp.FLAT_ATOMIC_OR_B32, addr=v[0], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
def check(st):
self.assertEqual(st.vgpr[0][4], 0x00FF0000)
self._make_test(setup, atomic, check, TEST_OFFSET)
@@ -94,12 +94,12 @@ class TestFlatAtomic(unittest.TestCase):
setup = [
s_mov_b32(s[0], 10),
v_mov_b32_e32(v[2], s[0]),
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
s_mov_b32(s[0], 100), # threshold
v_mov_b32_e32(v[3], s[0]),
]
atomic = FLAT(FLATOp.FLAT_ATOMIC_INC_U32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
atomic = FLAT(FLATOp.FLAT_ATOMIC_INC_U32, addr=v[0], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
def check(st):
self.assertEqual(st.vgpr[0][4], 10)
self._make_test(setup, atomic, check, TEST_OFFSET)
@@ -110,12 +110,12 @@ class TestFlatAtomic(unittest.TestCase):
setup = [
s_mov_b32(s[0], 10),
v_mov_b32_e32(v[2], s[0]),
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
s_mov_b32(s[0], 100),
v_mov_b32_e32(v[3], s[0]),
]
atomic = FLAT(FLATOp.FLAT_ATOMIC_DEC_U32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
atomic = FLAT(FLATOp.FLAT_ATOMIC_DEC_U32, addr=v[0], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
def check(st):
self.assertEqual(st.vgpr[0][4], 10)
self._make_test(setup, atomic, check, TEST_OFFSET)
@@ -126,12 +126,12 @@ class TestFlatAtomic(unittest.TestCase):
setup = [
s_mov_b32(s[0], 100),
v_mov_b32_e32(v[2], s[0]),
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
s_mov_b32(s[0], 30),
v_mov_b32_e32(v[3], s[0]), # sub 30
]
atomic = FLAT(FLATOp.FLAT_ATOMIC_SUB_U32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
atomic = FLAT(FLATOp.FLAT_ATOMIC_SUB_U32, addr=v[0], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
def check(st):
self.assertEqual(st.vgpr[0][4], 100, "v4 should have old value (100)")
self._make_test(setup, atomic, check, TEST_OFFSET)
@@ -142,12 +142,12 @@ class TestFlatAtomic(unittest.TestCase):
setup = [
s_mov_b32(s[0], 0xAAAAAAAA),
v_mov_b32_e32(v[2], s[0]),
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
s_mov_b32(s[0], 0xFFFFFFFF),
v_mov_b32_e32(v[3], s[0]), # XOR mask
]
atomic = FLAT(FLATOp.FLAT_ATOMIC_XOR_B32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
atomic = FLAT(FLATOp.FLAT_ATOMIC_XOR_B32, addr=v[0], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
def check(st):
self.assertEqual(st.vgpr[0][4], 0xAAAAAAAA, "v4 should have old value")
self._make_test(setup, atomic, check, TEST_OFFSET)
@@ -158,12 +158,12 @@ class TestFlatAtomic(unittest.TestCase):
setup = [
s_mov_b32(s[0], 100),
v_mov_b32_e32(v[2], s[0]),
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
s_mov_b32(s[0], 50),
v_mov_b32_e32(v[3], s[0]), # compare value (smaller)
]
atomic = FLAT(FLATOp.FLAT_ATOMIC_MIN_U32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
atomic = FLAT(FLATOp.FLAT_ATOMIC_MIN_U32, addr=v[0], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
def check(st):
self.assertEqual(st.vgpr[0][4], 100, "v4 should have old value (100)")
self._make_test(setup, atomic, check, TEST_OFFSET)
@@ -174,12 +174,12 @@ class TestFlatAtomic(unittest.TestCase):
setup = [
s_mov_b32(s[0], 50),
v_mov_b32_e32(v[2], s[0]),
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
s_mov_b32(s[0], 100),
v_mov_b32_e32(v[3], s[0]), # compare value (larger)
]
atomic = FLAT(FLATOp.FLAT_ATOMIC_MAX_U32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
atomic = FLAT(FLATOp.FLAT_ATOMIC_MAX_U32, addr=v[0], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
def check(st):
self.assertEqual(st.vgpr[0][4], 50, "v4 should have old value (50)")
self._make_test(setup, atomic, check, TEST_OFFSET)
@@ -193,14 +193,14 @@ class TestFlatAtomic(unittest.TestCase):
v_mov_b32_e32(v[2], s[0]),
s_mov_b32(s[0], 0xCAFEBABE),
v_mov_b32_e32(v[3], s[0]),
global_store_b64(addr=v[0:1], data=v[2:3], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b64(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
# Threshold: 0xFFFFFFFF_FFFFFFFF
s_mov_b32(s[0], 0xFFFFFFFF),
v_mov_b32_e32(v[4], s[0]),
v_mov_b32_e32(v[5], s[0]),
]
atomic = FLAT(FLATOp.FLAT_ATOMIC_INC_U64, addr=v[0:1], data=v[4:5], vdst=v[6:7], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
atomic = FLAT(FLATOp.FLAT_ATOMIC_INC_U64, addr=v[0], data=v[4], vdst=v[6], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
def check(st):
self.assertEqual(st.vgpr[0][6], 0xDEADBEEF, "v6 should have old value low dword")
self.assertEqual(st.vgpr[0][7], 0xCAFEBABE, "v7 should have old value high dword")
@@ -214,14 +214,14 @@ class TestFlatAtomic(unittest.TestCase):
v_mov_b32_e32(v[2], s[0]),
s_mov_b32(s[0], 0x22222222),
v_mov_b32_e32(v[3], s[0]),
global_store_b64(addr=v[0:1], data=v[2:3], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b64(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
s_mov_b32(s[0], 0x00000001), # add 1
v_mov_b32_e32(v[4], s[0]),
s_mov_b32(s[0], 0x00000000),
v_mov_b32_e32(v[5], s[0]),
]
atomic = FLAT(FLATOp.FLAT_ATOMIC_ADD_U64, addr=v[0:1], data=v[4:5], vdst=v[6:7], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
atomic = FLAT(FLATOp.FLAT_ATOMIC_ADD_U64, addr=v[0], data=v[4], vdst=v[6], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
def check(st):
self.assertEqual(st.vgpr[0][6], 0x11111111, "v6 should have old value low")
self.assertEqual(st.vgpr[0][7], 0x22222222, "v7 should have old value high")
@@ -235,14 +235,14 @@ class TestFlatAtomic(unittest.TestCase):
v_mov_b32_e32(v[2], s[0]),
s_mov_b32(s[0], 0xBBBBBBBB),
v_mov_b32_e32(v[3], s[0]),
global_store_b64(addr=v[0:1], data=v[2:3], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b64(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
s_mov_b32(s[0], 0xCCCCCCCC),
v_mov_b32_e32(v[4], s[0]),
s_mov_b32(s[0], 0xDDDDDDDD),
v_mov_b32_e32(v[5], s[0]),
]
atomic = FLAT(FLATOp.FLAT_ATOMIC_SWAP_B64, addr=v[0:1], data=v[4:5], vdst=v[6:7], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
atomic = FLAT(FLATOp.FLAT_ATOMIC_SWAP_B64, addr=v[0], data=v[4], vdst=v[6], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
def check(st):
self.assertEqual(st.vgpr[0][6], 0xAAAAAAAA, "v6 should have old value low")
self.assertEqual(st.vgpr[0][7], 0xBBBBBBBB, "v7 should have old value high")
@@ -256,15 +256,15 @@ class TestFlatLoad(unittest.TestCase):
"""FLAT_LOAD_B32 loads 32-bit value correctly."""
TEST_OFFSET = 2000
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_load_b64(s[2:3], s[80], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
s_mov_b32(s[0], 0xDEADBEEF),
v_mov_b32_e32(v[2], s[0]),
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
FLAT(FLATOp.FLAT_LOAD_B32, addr=v[0:1], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
FLAT(FLATOp.FLAT_LOAD_B32, addr=v[0], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[0], 0),
v_mov_b32_e32(v[1], 0),
@@ -278,7 +278,7 @@ class TestFlatLoad(unittest.TestCase):
"""FLAT_LOAD_B64 loads 64-bit value correctly."""
TEST_OFFSET = 2000
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_load_b64(s[2:3], s[80], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
@@ -286,9 +286,9 @@ class TestFlatLoad(unittest.TestCase):
v_mov_b32_e32(v[2], s[0]),
s_mov_b32(s[0], 0xCAFEBABE),
v_mov_b32_e32(v[3], s[0]),
global_store_b64(addr=v[0:1], data=v[2:3], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b64(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
FLAT(FLATOp.FLAT_LOAD_B64, addr=v[0:1], vdst=v[4:5], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
FLAT(FLATOp.FLAT_LOAD_B64, addr=v[0], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[0], 0),
v_mov_b32_e32(v[1], 0),
@@ -303,7 +303,7 @@ class TestFlatLoad(unittest.TestCase):
"""FLAT_LOAD_B96 loads 96-bit (3 dword) value correctly."""
TEST_OFFSET = 2000
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_load_b64(s[2:3], s[80], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
@@ -313,9 +313,9 @@ class TestFlatLoad(unittest.TestCase):
v_mov_b32_e32(v[3], s[0]),
s_mov_b32(s[0], 0x33333333),
v_mov_b32_e32(v[4], s[0]),
global_store_b96(addr=v[0:1], data=v[2:4], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b96(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
FLAT(FLATOp.FLAT_LOAD_B96, addr=v[0:1], vdst=v[5:7], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
FLAT(FLATOp.FLAT_LOAD_B96, addr=v[0], vdst=v[5], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[0], 0),
v_mov_b32_e32(v[1], 0),
@@ -331,7 +331,7 @@ class TestFlatLoad(unittest.TestCase):
"""FLAT_LOAD_B128 loads 128-bit value correctly."""
TEST_OFFSET = 2000
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_load_b64(s[2:3], s[80], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
@@ -343,9 +343,9 @@ class TestFlatLoad(unittest.TestCase):
v_mov_b32_e32(v[4], s[0]),
s_mov_b32(s[0], 0x44444444),
v_mov_b32_e32(v[5], s[0]),
global_store_b128(addr=v[0:1], data=v[2:5], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b128(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
FLAT(FLATOp.FLAT_LOAD_B128, addr=v[0:1], vdst=v[6:9], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
FLAT(FLATOp.FLAT_LOAD_B128, addr=v[0], vdst=v[6], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[0], 0),
v_mov_b32_e32(v[1], 0),
+42 -205
View File
@@ -11,7 +11,7 @@ class TestGlobalAtomic(unittest.TestCase):
def _make_test(self, setup_instrs, atomic_instr, check_fn, test_offset=2000):
"""Helper to create atomic test instructions."""
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_load_b64(s[2:3], s[80], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
@@ -30,12 +30,12 @@ class TestGlobalAtomic(unittest.TestCase):
setup = [
s_mov_b32(s[0], 100),
v_mov_b32_e32(v[2], s[0]),
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
s_mov_b32(s[0], 50),
v_mov_b32_e32(v[3], s[0]),
]
atomic = GLOBAL(GLOBALOp.GLOBAL_ATOMIC_ADD_U32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
atomic = FLAT(GLOBALOp.GLOBAL_ATOMIC_ADD_U32, addr=v[0], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1, seg=2)
def check(st):
self.assertEqual(st.vgpr[0][4], 100)
self._make_test(setup, atomic, check, TEST_OFFSET)
@@ -48,14 +48,14 @@ class TestGlobalAtomic(unittest.TestCase):
v_mov_b32_e32(v[2], s[0]),
s_mov_b32(s[0], 0x00000000),
v_mov_b32_e32(v[3], s[0]),
global_store_b64(addr=v[0:1], data=v[2:3], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b64(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
s_mov_b32(s[0], 0x00000001),
v_mov_b32_e32(v[4], s[0]),
s_mov_b32(s[0], 0x00000000),
v_mov_b32_e32(v[5], s[0]),
]
atomic = GLOBAL(GLOBALOp.GLOBAL_ATOMIC_ADD_U64, addr=v[0:1], data=v[4:5], vdst=v[6:7], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
atomic = FLAT(GLOBALOp.GLOBAL_ATOMIC_ADD_U64, addr=v[0], data=v[4], vdst=v[6], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1, seg=2)
def check(st):
self.assertEqual(st.vgpr[0][6], 0xFFFFFFFF)
self.assertEqual(st.vgpr[0][7], 0x00000000)
@@ -69,7 +69,7 @@ class TestGlobalLoad(unittest.TestCase):
"""GLOBAL_LOAD_B96 loads 96-bit value correctly."""
TEST_OFFSET = 2000
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_load_b64(s[2:3], s[80], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
@@ -79,9 +79,9 @@ class TestGlobalLoad(unittest.TestCase):
v_mov_b32_e32(v[3], s[0]),
s_mov_b32(s[0], 0xCCCCCCCC),
v_mov_b32_e32(v[4], s[0]),
global_store_b96(addr=v[0:1], data=v[2:4], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b96(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
GLOBAL(GLOBALOp.GLOBAL_LOAD_B96, addr=v[0:1], vdst=v[5:7], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
FLAT(GLOBALOp.GLOBAL_LOAD_B96, addr=v[0], vdst=v[5], saddr=SrcEnum.NULL, offset=TEST_OFFSET, seg=2),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[0], 0),
v_mov_b32_e32(v[1], 0),
@@ -97,7 +97,7 @@ class TestGlobalLoad(unittest.TestCase):
"""GLOBAL_LOAD_B128 loads 128-bit value correctly."""
TEST_OFFSET = 2000
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_load_b64(s[2:3], s[80], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
@@ -109,9 +109,9 @@ class TestGlobalLoad(unittest.TestCase):
v_mov_b32_e32(v[4], s[0]),
s_mov_b32(s[0], 0x9ABCDEF0),
v_mov_b32_e32(v[5], s[0]),
global_store_b128(addr=v[0:1], data=v[2:5], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b128(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
GLOBAL(GLOBALOp.GLOBAL_LOAD_B128, addr=v[0:1], vdst=v[6:9], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
FLAT(GLOBALOp.GLOBAL_LOAD_B128, addr=v[0], vdst=v[6], saddr=SrcEnum.NULL, offset=TEST_OFFSET, seg=2),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[0], 0),
v_mov_b32_e32(v[1], 0),
@@ -128,183 +128,20 @@ class TestGlobalLoad(unittest.TestCase):
class TestGlobalStore(unittest.TestCase):
"""Tests for GLOBAL store instructions."""
def test_global_store_b8_basic(self):
"""GLOBAL_STORE_B8 stores a single byte from VDATA[7:0]."""
TEST_OFFSET = 256
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
# First store 0xDEADBEEF to memory
s_mov_b32(s[4], 0xDEADBEEF),
v_mov_b32_e32(v[2], s[4]),
v_mov_b32_e32(v[0], 0),
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
# Now store single byte 0x42 to same address (should only change byte 0)
v_mov_b32_e32(v[2], 0x42),
global_store_b8(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
# Read back and check
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[0], vdst=v[3], data=v[3], saddr=s[2:3], offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[0], v[3]),
s_mov_b32(s[2], 0),
s_mov_b32(s[3], 0),
]
st = run_program(instructions, n_lanes=1)
# Only byte 0 should change from 0xEF to 0x42
self.assertEqual(st.vgpr[0][0], 0xDEADBE42, "Only byte 0 should be modified")
def test_global_store_b8_byte1(self):
"""GLOBAL_STORE_B8 at offset+1 stores to byte 1."""
TEST_OFFSET = 256
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
s_mov_b32(s[4], 0xDEADBEEF),
v_mov_b32_e32(v[2], s[4]),
v_mov_b32_e32(v[0], 0),
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[2], 0x42),
global_store_b8(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET+1),
s_waitcnt(vmcnt=0),
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[0], vdst=v[3], data=v[3], saddr=s[2:3], offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[0], v[3]),
s_mov_b32(s[2], 0),
s_mov_b32(s[3], 0),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][0], 0xDEAD42EF, "Only byte 1 should be modified")
def test_global_store_b16_basic(self):
"""GLOBAL_STORE_B16 stores a 16-bit value from VDATA[15:0]."""
TEST_OFFSET = 256
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
s_mov_b32(s[4], 0xDEADBEEF),
v_mov_b32_e32(v[2], s[4]),
v_mov_b32_e32(v[0], 0),
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
s_mov_b32(s[4], 0xCAFE),
v_mov_b32_e32(v[2], s[4]),
global_store_b16(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[0], vdst=v[3], data=v[3], saddr=s[2:3], offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[0], v[3]),
s_mov_b32(s[2], 0),
s_mov_b32(s[3], 0),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][0], 0xDEADCAFE, "Only lower 16 bits should be modified")
def test_global_store_b16_high_half(self):
"""GLOBAL_STORE_B16 at offset+2 stores to high 16 bits."""
TEST_OFFSET = 256
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
s_mov_b32(s[4], 0xDEADBEEF),
v_mov_b32_e32(v[2], s[4]),
v_mov_b32_e32(v[0], 0),
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
s_mov_b32(s[4], 0xCAFE),
v_mov_b32_e32(v[2], s[4]),
global_store_b16(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET+2),
s_waitcnt(vmcnt=0),
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[0], vdst=v[3], data=v[3], saddr=s[2:3], offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[0], v[3]),
s_mov_b32(s[2], 0),
s_mov_b32(s[3], 0),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][0], 0xCAFEBEEF, "Only upper 16 bits should be modified")
def test_global_store_b16_byte_offset_1(self):
"""GLOBAL_STORE_B16 at byte offset 1 stores bytes 1-2 within the same word."""
TEST_OFFSET = 256
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
s_mov_b32(s[4], 0xDDCCBBAA),
v_mov_b32_e32(v[2], s[4]),
v_mov_b32_e32(v[0], 0),
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
# Store 0xBEEF at byte offset 1 (bytes 1-2)
s_mov_b32(s[4], 0xBEEF),
v_mov_b32_e32(v[2], s[4]),
global_store_b16(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET+1),
s_waitcnt(vmcnt=0),
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[0], vdst=v[3], data=v[3], saddr=s[2:3], offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[0], v[3]),
s_mov_b32(s[2], 0),
s_mov_b32(s[3], 0),
]
st = run_program(instructions, n_lanes=1)
# Bytes 1-2 should be 0xBEEF (0xEF at byte 1, 0xBE at byte 2)
# Original: 0xDDCCBBAA -> bytes [AA, BB, CC, DD]
# After: 0xDDBEEFAA -> bytes [AA, EF, BE, DD]
self.assertEqual(st.vgpr[0][0], 0xDDBEEFAA, "Bytes 1-2 should be 0xBEEF")
def test_global_store_b16_cross_word_boundary(self):
"""GLOBAL_STORE_B16 at byte offset 3 crosses word boundary (byte 3 of word N, byte 0 of word N+1)."""
TEST_OFFSET = 256
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
# Initialize two consecutive words
s_mov_b32(s[4], 0xDDCCBBAA),
v_mov_b32_e32(v[2], s[4]),
v_mov_b32_e32(v[0], 0),
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
s_mov_b32(s[4], 0x44332211),
v_mov_b32_e32(v[2], s[4]),
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET+4),
s_waitcnt(vmcnt=0),
# Store 0xBEEF at byte offset 3 (crosses word boundary)
# Low byte (0xEF) goes to byte 3 of first word
# High byte (0xBE) goes to byte 0 of second word
s_mov_b32(s[4], 0xBEEF),
v_mov_b32_e32(v[2], s[4]),
global_store_b16(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET+3),
s_waitcnt(vmcnt=0),
# Load back both words
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[0], vdst=v[3], data=v[3], saddr=s[2:3], offset=TEST_OFFSET),
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[0], vdst=v[4], data=v[4], saddr=s[2:3], offset=TEST_OFFSET+4),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[0], v[3]),
v_mov_b32_e32(v[1], v[4]),
s_mov_b32(s[2], 0),
s_mov_b32(s[3], 0),
]
st = run_program(instructions, n_lanes=1)
# First word: 0xDDCCBBAA -> 0xEFCCBBAA (byte 3 becomes 0xEF)
# Second word: 0x44332211 -> 0x443322BE (byte 0 becomes 0xBE)
self.assertEqual(st.vgpr[0][0], 0xEFCCBBAA, "Byte 3 of first word should be 0xEF")
self.assertEqual(st.vgpr[0][1], 0x443322BE, "Byte 0 of second word should be 0xBE")
def test_global_store_b64_basic(self):
"""GLOBAL_STORE_B64 stores 8 bytes from v[n:n+1] to memory."""
TEST_OFFSET = 256
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_load_b64(s[2:3], s[80], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
s_mov_b32(s[4], 0xDEADBEEF),
s_mov_b32(s[5], 0xCAFEBABE),
v_mov_b32_e32(v[2], s[4]),
v_mov_b32_e32(v[3], s[5]),
v_mov_b32_e32(v[0], 0),
global_store_b64(addr=v[0], data=v[2:3], saddr=s[2:3], offset=TEST_OFFSET),
global_store_b64(addr=v[0], data=v[2], saddr=s[2], offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
GLOBAL(GLOBALOp.GLOBAL_LOAD_B64, addr=v[0], vdst=v[4:5], data=v[4:5], saddr=s[2:3], offset=TEST_OFFSET),
FLAT(GLOBALOp.GLOBAL_LOAD_B64, addr=v[0], vdst=v[4], data=v[4], saddr=s[2], offset=TEST_OFFSET, seg=2),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[0], v[4]),
v_mov_b32_e32(v[1], v[5]),
@@ -323,17 +160,17 @@ class TestD16HiLoads(unittest.TestCase):
"""GLOBAL_LOAD_D16_HI_B16 must preserve low 16 bits of destination."""
TEST_OFFSET = 256
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_load_b64(s[2:3], s[80], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
v_mov_b32_e32(v[0], s[2]),
v_mov_b32_e32(v[1], s[3]),
s_mov_b32(s[4], 0xCAFE),
v_mov_b32_e32(v[2], s[4]),
global_store_b16(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
global_store_b16(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
s_mov_b32(s[4], 0x0000BEEF),
v_mov_b32_e32(v[3], s[4]),
GLOBAL(GLOBALOp.GLOBAL_LOAD_D16_HI_B16, addr=v[0:1], vdst=v[3], data=v[3], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
FLAT(GLOBALOp.GLOBAL_LOAD_D16_HI_B16, addr=v[0], vdst=v[3], data=v[3], saddr=SrcEnum.NULL, offset=TEST_OFFSET, seg=2),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[0], v[3]),
v_mov_b32_e32(v[1], 0),
@@ -348,17 +185,17 @@ class TestD16HiLoads(unittest.TestCase):
"""GLOBAL_LOAD_D16_HI_B16 where data field differs from vdst."""
TEST_OFFSET = 256
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_load_b64(s[2:3], s[80], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
s_mov_b32(s[4], 0xCAFE),
v_mov_b32_e32(v[2], s[4]),
v_mov_b32_e32(v[3], 0),
global_store_b16(addr=v[3], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
global_store_b16(addr=v[3], data=v[2], saddr=s[2], offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
s_mov_b32(s[4], 0x0000DEAD),
v_mov_b32_e32(v[0], s[4]), # data field - should NOT affect result
v_mov_b32_e32(v[1], 0), # vdst - low bits should be preserved
GLOBAL(GLOBALOp.GLOBAL_LOAD_D16_HI_B16, addr=v[1], vdst=v[1], data=v[0], saddr=s[2:3], offset=TEST_OFFSET),
FLAT(GLOBALOp.GLOBAL_LOAD_D16_HI_B16, addr=v[1], vdst=v[1], data=v[0], saddr=s[2], offset=TEST_OFFSET, seg=2),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[0], v[1]),
s_mov_b32(s[2], 0),
@@ -372,19 +209,19 @@ class TestD16HiLoads(unittest.TestCase):
"""GLOBAL_LOAD_D16_HI_U8 where data field differs from vdst."""
TEST_OFFSET = 256
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_load_b64(s[2:3], s[80], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
s_mov_b32(s[4], 0xAB),
v_mov_b32_e32(v[2], s[4]),
v_mov_b32_e32(v[3], 0),
global_store_b8(addr=v[3], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
global_store_b8(addr=v[3], data=v[2], saddr=s[2], offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
s_mov_b32(s[4], 0x0000DEAD),
v_mov_b32_e32(v[4], s[4]), # data field
s_mov_b32(s[4], 0x0000BEEF),
v_mov_b32_e32(v[5], s[4]), # vdst
v_mov_b32_e32(v[3], 0),
GLOBAL(GLOBALOp.GLOBAL_LOAD_D16_HI_U8, addr=v[3], vdst=v[5], data=v[4], saddr=s[2:3], offset=TEST_OFFSET),
FLAT(GLOBALOp.GLOBAL_LOAD_D16_HI_U8, addr=v[3], vdst=v[5], data=v[4], saddr=s[2], offset=TEST_OFFSET, seg=2),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[0], v[5]),
s_mov_b32(s[2], 0),
@@ -398,15 +235,15 @@ class TestD16HiLoads(unittest.TestCase):
"""GLOBAL_LOAD_D16_HI_B16 with same register for addr and vdst, addr value=0."""
TEST_OFFSET = 256
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_load_b64(s[2:3], s[80], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
s_mov_b32(s[4], 0xCAFE),
v_mov_b32_e32(v[2], s[4]),
v_mov_b32_e32(v[3], 0),
global_store_b16(addr=v[3], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
global_store_b16(addr=v[3], data=v[2], saddr=s[2], offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[1], 0),
GLOBAL(GLOBALOp.GLOBAL_LOAD_D16_HI_B16, addr=v[1], vdst=v[1], data=v[1], saddr=s[2:3], offset=TEST_OFFSET),
FLAT(GLOBALOp.GLOBAL_LOAD_D16_HI_B16, addr=v[1], vdst=v[1], data=v[1], saddr=s[2], offset=TEST_OFFSET, seg=2),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[0], v[1]),
s_mov_b32(s[2], 0),
@@ -420,13 +257,13 @@ class TestD16HiLoads(unittest.TestCase):
"""Exact pattern from tril() failure: data=v0 differs from vdst=v1."""
TEST_OFFSET = 256
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_load_b64(s[2:3], s[80], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
s_mov_b32(s[4], 0x01010101),
v_mov_b32_e32(v[10], s[4]),
v_mov_b32_e32(v[3], 0),
global_store_b32(addr=v[3], data=v[10], saddr=s[2:3], offset=TEST_OFFSET),
global_store_b32(addr=v[3], data=v[10], saddr=s[2:3], offset=TEST_OFFSET+4),
global_store_b32(addr=v[3], data=v[10], saddr=s[2], offset=TEST_OFFSET),
global_store_b32(addr=v[3], data=v[10], saddr=s[2], offset=TEST_OFFSET+4),
s_waitcnt(vmcnt=0),
# Set v[0] to 0x0101 (simulating prior u16 load result)
s_mov_b32(s[4], 0x0101),
@@ -434,7 +271,7 @@ class TestD16HiLoads(unittest.TestCase):
# Set v[1] to 0
v_mov_b32_e32(v[1], 0),
# Load using v[1] as addr AND vdst, but v[0] as data
GLOBAL(GLOBALOp.GLOBAL_LOAD_D16_HI_B16, addr=v[1], vdst=v[1], data=v[0], saddr=s[2:3], offset=TEST_OFFSET+6),
FLAT(GLOBALOp.GLOBAL_LOAD_D16_HI_B16, addr=v[1], vdst=v[1], data=v[0], saddr=s[2], offset=TEST_OFFSET+6, seg=2),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[0], v[1]),
s_mov_b32(s[2], 0),
@@ -449,19 +286,19 @@ class TestD16HiLoads(unittest.TestCase):
"""GLOBAL_LOAD_D16_HI_I8 where data field differs from vdst."""
TEST_OFFSET = 256
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_load_b64(s[2:3], s[80], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
s_mov_b32(s[4], 0x80), # negative signed byte = -128
v_mov_b32_e32(v[2], s[4]),
v_mov_b32_e32(v[3], 0),
global_store_b8(addr=v[3], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
global_store_b8(addr=v[3], data=v[2], saddr=s[2], offset=TEST_OFFSET),
s_waitcnt(vmcnt=0),
s_mov_b32(s[4], 0x0000DEAD),
v_mov_b32_e32(v[4], s[4]), # data field
s_mov_b32(s[4], 0x0000BEEF),
v_mov_b32_e32(v[5], s[4]), # vdst
v_mov_b32_e32(v[3], 0),
GLOBAL(GLOBALOp.GLOBAL_LOAD_D16_HI_I8, addr=v[3], vdst=v[5], data=v[4], saddr=s[2:3], offset=TEST_OFFSET),
FLAT(GLOBALOp.GLOBAL_LOAD_D16_HI_I8, addr=v[3], vdst=v[5], data=v[4], saddr=s[2], offset=TEST_OFFSET, seg=2),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[0], v[5]),
s_mov_b32(s[2], 0),
@@ -476,7 +313,7 @@ class TestD16HiLoads(unittest.TestCase):
"""Test the exact pattern from tril() kernel that was failing."""
TEST_OFFSET = 256
instructions = [
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
s_load_b64(s[2:3], s[80], 0, soffset=SrcEnum.NULL),
s_waitcnt(lgkmcnt=0),
s_mov_b32(s[4], 0x01010101),
v_mov_b32_e32(v[10], s[4]),
@@ -484,16 +321,16 @@ class TestD16HiLoads(unittest.TestCase):
s_mov_b32(s[4], 0x01),
v_mov_b32_e32(v[12], s[4]),
v_mov_b32_e32(v[0], 0),
global_store_b64(addr=v[0], data=v[10:11], saddr=s[2:3], offset=TEST_OFFSET),
global_store_b8(addr=v[0], data=v[12], saddr=s[2:3], offset=TEST_OFFSET+8),
global_store_b64(addr=v[0], data=v[10], saddr=s[2], offset=TEST_OFFSET),
global_store_b8(addr=v[0], data=v[12], saddr=s[2], offset=TEST_OFFSET+8),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[2], 0),
v_mov_b32_e32(v[1], 0),
GLOBAL(GLOBALOp.GLOBAL_LOAD_U16, addr=v[2], vdst=v[0], data=v[0], saddr=s[2:3], offset=TEST_OFFSET+3),
GLOBAL(GLOBALOp.GLOBAL_LOAD_D16_HI_B16, addr=v[1], vdst=v[1], data=v[1], saddr=s[2:3], offset=TEST_OFFSET+6),
GLOBAL(GLOBALOp.GLOBAL_LOAD_U8, addr=v[2], vdst=v[3], data=v[3], saddr=s[2:3], offset=TEST_OFFSET),
GLOBAL(GLOBALOp.GLOBAL_LOAD_U8, addr=v[2], vdst=v[4], data=v[4], saddr=s[2:3], offset=TEST_OFFSET+8),
FLAT(GLOBALOp.GLOBAL_LOAD_U16, addr=v[2], vdst=v[0], data=v[0], saddr=s[2], offset=TEST_OFFSET+3, seg=2),
FLAT(GLOBALOp.GLOBAL_LOAD_D16_HI_B16, addr=v[1], vdst=v[1], data=v[1], saddr=s[2], offset=TEST_OFFSET+6, seg=2),
FLAT(GLOBALOp.GLOBAL_LOAD_U8, addr=v[2], vdst=v[3], data=v[3], saddr=s[2], offset=TEST_OFFSET, seg=2),
FLAT(GLOBALOp.GLOBAL_LOAD_U8, addr=v[2], vdst=v[4], data=v[4], saddr=s[2], offset=TEST_OFFSET+8, seg=2),
s_waitcnt(vmcnt=0),
v_and_b32_e32(v[5], 0xffff, v[0]),
@@ -502,10 +339,10 @@ class TestD16HiLoads(unittest.TestCase):
v_or_b32_e32(v[0], v[3], v[0]),
v_or_b32_e32(v[1], v[5], v[1]),
global_store_b64(addr=v[2], data=v[0:1], saddr=s[2:3], offset=TEST_OFFSET+16),
global_store_b64(addr=v[2], data=v[0], saddr=s[2], offset=TEST_OFFSET+16),
s_waitcnt(vmcnt=0),
GLOBAL(GLOBALOp.GLOBAL_LOAD_B64, addr=v[2], vdst=v[6:7], data=v[6:7], saddr=s[2:3], offset=TEST_OFFSET+16),
FLAT(GLOBALOp.GLOBAL_LOAD_B64, addr=v[2], vdst=v[6], data=v[6], saddr=s[2], offset=TEST_OFFSET+16, seg=2),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[0], v[6]),
v_mov_b32_e32(v[1], v[7]),
+14 -295
View File
@@ -62,28 +62,6 @@ class TestBasicScalar(unittest.TestCase):
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.sgpr[1], 0x80000000)
def test_s_fmamk_f32(self):
"""S_FMAMK_F32: D = S0 * literal + S1."""
# 2.0 * 3.0 + 1.0 = 7.0
instructions = [
s_mov_b32(s[0], f2i(2.0)),
s_mov_b32(s[1], f2i(1.0)),
s_fmamk_f32(s[2], s[0], s[1], literal=f2i(3.0)),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.sgpr[2], f2i(7.0))
def test_s_fmamk_f32_negative(self):
"""S_FMAMK_F32 with negative values."""
# -2.0 * 4.0 + 10.0 = 2.0
instructions = [
s_mov_b32(s[0], f2i(-2.0)),
s_mov_b32(s[1], f2i(10.0)),
s_fmamk_f32(s[2], s[0], s[1], literal=f2i(4.0)),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.sgpr[2], f2i(2.0))
class TestQuadmaskWqm(unittest.TestCase):
"""Tests for S_QUADMASK_B32 and S_WQM_B32."""
@@ -164,8 +142,8 @@ class TestBranch(unittest.TestCase):
"""S_CBRANCH_VCCNZ should only check VCC_LO in wave32."""
instructions = [
# Set VCC_LO = 0, VCC_HI = 1
s_mov_b32(VCC_LO, 0),
s_mov_b32(VCC_HI, 1),
s_mov_b32(s[SrcEnum.VCC_LO - 128], 0),
s_mov_b32(s[SrcEnum.VCC_HI - 128], 1),
v_mov_b32_e32(v[0], 0),
# If VCC_HI is incorrectly used, branch will be taken
s_cbranch_vccnz(1), # Skip next instruction if VCC != 0
@@ -178,8 +156,8 @@ class TestBranch(unittest.TestCase):
"""S_CBRANCH_VCCZ should only check VCC_LO in wave32."""
instructions = [
# Set VCC_LO = 1, VCC_HI = 0
s_mov_b32(VCC_LO, 1),
s_mov_b32(VCC_HI, 0),
s_mov_b32(s[SrcEnum.VCC_LO - 128], 1),
s_mov_b32(s[SrcEnum.VCC_HI - 128], 0),
v_mov_b32_e32(v[0], 0),
# If VCC_HI is incorrectly used, branch will be taken
s_cbranch_vccz(1), # Skip next instruction if VCC == 0
@@ -191,7 +169,7 @@ class TestBranch(unittest.TestCase):
def test_cbranch_vccnz_branches_on_vcc_lo(self):
"""S_CBRANCH_VCCNZ branches when VCC_LO is non-zero."""
instructions = [
s_mov_b32(VCC_LO, 1),
s_mov_b32(s[SrcEnum.VCC_LO - 128], 1),
v_mov_b32_e32(v[0], 0),
s_cbranch_vccnz(1), # Skip next instruction if VCC != 0
v_mov_b32_e32(v[0], 42), # This should be skipped
@@ -217,6 +195,15 @@ class Test64BitLiterals(unittest.TestCase):
result = i642f(st.vgpr[0][0] | (st.vgpr[0][1] << 32))
self.assertAlmostEqual(result, -4294967296.0, places=5)
def test_64bit_literal_positive_encoding(self):
"""64-bit instruction encodes large positive literals correctly."""
large_val = 0x12345678
inst = v_add_f64(v[2], v[0], large_val)
self.assertIsNotNone(inst._literal, "Literal should be set")
actual_lit = (inst._literal >> 32) & 0xffffffff
self.assertEqual(actual_lit, large_val, f"Literal should be {large_val:#x}, got {actual_lit:#x}")
class TestSCCBehavior(unittest.TestCase):
"""Tests for SCC condition code behavior."""
@@ -320,56 +307,6 @@ class TestSignedArithmetic(unittest.TestCase):
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.sgpr[2], 2)
def test_s_mul_hi_u32_max(self):
"""S_MUL_HI_U32: 0xFFFFFFFF * 0xFFFFFFFF."""
instructions = [
s_mov_b32(s[0], 0xFFFFFFFF),
s_mov_b32(s[1], 0xFFFFFFFF),
s_mul_hi_u32(s[2], s[0], s[1]), # (0xFFFFFFFF * 0xFFFFFFFF) >> 32 = 0xFFFFFFFE
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.sgpr[2], 0xFFFFFFFE)
def test_s_mul_hi_i32_positive(self):
"""S_MUL_HI_I32: positive * positive."""
instructions = [
s_mov_b32(s[0], 0x40000000), # 2^30
s_mov_b32(s[1], 4),
s_mul_hi_i32(s[2], s[0], s[1]), # (2^30 * 4) >> 32 = 1
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.sgpr[2], 1)
def test_s_mul_hi_i32_neg_times_neg(self):
"""S_MUL_HI_I32: (-1) * (-1) = 1, high bits = 0."""
instructions = [
s_mov_b32(s[0], 0xFFFFFFFF), # -1
s_mov_b32(s[1], 0xFFFFFFFF), # -1
s_mul_hi_i32(s[2], s[0], s[1]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.sgpr[2], 0)
def test_s_mul_hi_i32_neg_times_pos(self):
"""S_MUL_HI_I32: (-1) * 2 = -2, high bits = -1 (sign extension)."""
instructions = [
s_mov_b32(s[0], 0xFFFFFFFF), # -1
s_mov_b32(s[1], 2),
s_mul_hi_i32(s[2], s[0], s[1]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.sgpr[2], 0xFFFFFFFF) # -1 sign extends
def test_s_mul_hi_i32_min_int(self):
"""S_MUL_HI_I32: MIN_INT * 2 = -2^32, high = -1."""
instructions = [
s_mov_b32(s[0], 0x80000000), # -2^31 (MIN_INT)
s_mov_b32(s[1], 2),
s_mul_hi_i32(s[2], s[0], s[1]), # (-2^31 * 2) >> 32 = -1
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.sgpr[2], 0xFFFFFFFF)
def test_s_mul_i32(self):
"""S_MUL_I32: signed multiply low 32 bits."""
instructions = [
@@ -401,223 +338,5 @@ class TestSignedArithmetic(unittest.TestCase):
self.assertEqual(st.sgpr[7], ((dividend * 2) + 1) & 0xFFFFFFFF)
class TestBitSet(unittest.TestCase):
"""Tests for S_BITSET0_B32 and S_BITSET1_B32 instructions."""
def test_s_bitset1_b32_set_bit0(self):
"""S_BITSET1_B32: set bit 0 in destination."""
instructions = [
s_mov_b32(s[0], 0), # start with 0
s_mov_b32(s[1], 0), # bit position = 0
s_bitset1_b32(s[0], s[1]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.sgpr[0], 1, "Bit 0 should be set")
def test_s_bitset1_b32_set_bit31(self):
"""S_BITSET1_B32: set bit 31 in destination."""
instructions = [
s_mov_b32(s[0], 0), # start with 0
s_mov_b32(s[1], 31), # bit position = 31
s_bitset1_b32(s[0], s[1]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.sgpr[0], 0x80000000, "Bit 31 should be set")
def test_s_bitset1_b32_preserves_other_bits(self):
"""S_BITSET1_B32: preserves bits not being set."""
instructions = [
s_mov_b32(s[0], 0xFF00FF00), # existing pattern
s_mov_b32(s[1], 0), # bit position = 0
s_bitset1_b32(s[0], s[1]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.sgpr[0], 0xFF00FF01, "Should set bit 0 while preserving others")
def test_s_bitset0_b32_clear_bit0(self):
"""S_BITSET0_B32: clear bit 0 in destination."""
instructions = [
s_mov_b32(s[0], 0xFFFFFFFF), # start with all bits set
s_mov_b32(s[1], 0), # bit position = 0
s_bitset0_b32(s[0], s[1]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.sgpr[0], 0xFFFFFFFE, "Bit 0 should be cleared")
def test_s_bitset0_b32_clear_bit31(self):
"""S_BITSET0_B32: clear bit 31 in destination."""
instructions = [
s_mov_b32(s[0], 0xFFFFFFFF), # start with all bits set
s_mov_b32(s[1], 31), # bit position = 31
s_bitset0_b32(s[0], s[1]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.sgpr[0], 0x7FFFFFFF, "Bit 31 should be cleared")
def test_s_bitset1_b32_uses_low5_bits(self):
"""S_BITSET1_B32: only uses low 5 bits of position (mod 32)."""
instructions = [
s_mov_b32(s[0], 0),
s_mov_b32(s[1], 32 + 5), # position = 37, but mod 32 = 5
s_bitset1_b32(s[0], s[1]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.sgpr[0], 0x20, "Bit 5 should be set (37 mod 32 = 5)")
class TestBfeI64(unittest.TestCase):
"""Tests for S_BFE_I64 - 64-bit bit field extract with sign extension.
Regression tests for sign extension bug where 32-bit masks were incorrectly
used for 64-bit operations, causing the high 32 bits to not be sign-extended.
"""
def test_s_bfe_i64_positive_no_sign_extend(self):
"""S_BFE_I64: positive value (1) in 16 bits should not sign extend."""
# S1 encodes: [22:16] = width, [5:0] = offset
# width=16, offset=0 -> S1 = (16 << 16) | 0 = 0x100000
instructions = [
s_mov_b32(s[0], 1), # S0 lo = 1
s_mov_b32(s[1], 0), # S0 hi = 0
s_mov_b32(s[2], 0x100000), # width=16, offset=0
s_bfe_i64(s[4:5], s[0:1], s[2]),
v_mov_b32_e32(v[0], s[4]),
v_mov_b32_e32(v[1], s[5]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][0], 1, "lo should be 1")
self.assertEqual(st.vgpr[0][1], 0, "hi should be 0 (no sign extend)")
def test_s_bfe_i64_negative_sign_extend(self):
"""S_BFE_I64: 0xFFFF (-1 in 16 bits) should sign extend to 64 bits.
This is the main regression test - before the fix, hi was 0 instead of 0xFFFFFFFF.
"""
instructions = [
s_mov_b32(s[0], 0xFFFF), # S0 lo = -1 in 16 bits
s_mov_b32(s[1], 0), # S0 hi = 0
s_mov_b32(s[2], 0x100000), # width=16, offset=0
s_bfe_i64(s[4:5], s[0:1], s[2]),
v_mov_b32_e32(v[0], s[4]),
v_mov_b32_e32(v[1], s[5]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][0], 0xFFFFFFFF, "lo should be 0xFFFFFFFF")
self.assertEqual(st.vgpr[0][1], 0xFFFFFFFF, "hi should be 0xFFFFFFFF (sign extended)")
def test_s_bfe_i64_8bit_negative_sign_extend(self):
"""S_BFE_I64: 0xFF (-1 in 8 bits) should sign extend to 64 bits."""
# width=8, offset=0 -> S1 = (8 << 16) | 0 = 0x80000
instructions = [
s_mov_b32(s[0], 0xFF), # S0 lo = -1 in 8 bits
s_mov_b32(s[1], 0), # S0 hi = 0
s_mov_b32(s[2], 0x80000), # width=8, offset=0
s_bfe_i64(s[4:5], s[0:1], s[2]),
v_mov_b32_e32(v[0], s[4]),
v_mov_b32_e32(v[1], s[5]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][0], 0xFFFFFFFF, "lo should be 0xFFFFFFFF")
self.assertEqual(st.vgpr[0][1], 0xFFFFFFFF, "hi should be 0xFFFFFFFF (sign extended)")
def test_s_bfe_i64_8bit_positive(self):
"""S_BFE_I64: 0x7F (127 in 8 bits) should not sign extend."""
# width=8, offset=0 -> S1 = (8 << 16) | 0 = 0x80000
instructions = [
s_mov_b32(s[0], 0x7F), # S0 lo = 127 in 8 bits (MSB=0)
s_mov_b32(s[1], 0), # S0 hi = 0
s_mov_b32(s[2], 0x80000), # width=8, offset=0
s_bfe_i64(s[4:5], s[0:1], s[2]),
v_mov_b32_e32(v[0], s[4]),
v_mov_b32_e32(v[1], s[5]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][0], 0x7F, "lo should be 0x7F")
self.assertEqual(st.vgpr[0][1], 0, "hi should be 0 (no sign extend)")
def test_s_bfe_i64_with_offset(self):
"""S_BFE_I64: extract from non-zero bit offset with sign extension."""
# Extract 16 bits starting at bit 8: value 0xFF00 >> 8 = 0xFF = -1 in 8 bits? No wait...
# Let's put 0x8000FF00: extract 16 bits at offset 8 = 0x00FF (positive)
# Put 0xFF00_0000: extract 16 bits at offset 16 = 0xFF00 = -256 in signed 16-bit
instructions = [
s_mov_b32(s[0], 0xFF000000), # bits [31:24] = 0xFF, [23:16] = 0x00
s_mov_b32(s[1], 0),
# width=16, offset=16 -> S1 = (16 << 16) | 16 = 0x100010
s_mov_b32(s[2], 0x100010),
s_bfe_i64(s[4:5], s[0:1], s[2]),
v_mov_b32_e32(v[0], s[4]),
v_mov_b32_e32(v[1], s[5]),
]
st = run_program(instructions, n_lanes=1)
# Extract bits [31:16] = 0xFF00, sign bit is bit 15 of extracted = bit 31 of original = 1
# So result should be sign-extended 0xFF00 -> 0xFFFFFF00 in lo, 0xFFFFFFFF in hi
self.assertEqual(st.vgpr[0][0], 0xFFFFFF00, "lo should be sign-extended 0xFF00")
self.assertEqual(st.vgpr[0][1], 0xFFFFFFFF, "hi should be 0xFFFFFFFF (sign extended)")
def test_s_bfe_i64_32bit_negative(self):
"""S_BFE_I64: extract 32 bits with sign extension."""
# width=32, offset=0 -> S1 = (32 << 16) | 0 = 0x200000
instructions = [
s_mov_b32(s[0], 0x80000000), # MIN_INT32 = -2^31
s_mov_b32(s[1], 0),
s_mov_b32(s[2], 0x200000), # width=32, offset=0
s_bfe_i64(s[4:5], s[0:1], s[2]),
v_mov_b32_e32(v[0], s[4]),
v_mov_b32_e32(v[1], s[5]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][0], 0x80000000, "lo should be 0x80000000")
self.assertEqual(st.vgpr[0][1], 0xFFFFFFFF, "hi should be 0xFFFFFFFF (sign extended)")
class Test64BitCompare(unittest.TestCase):
"""Tests for 64-bit scalar compare instructions."""
def test_s_cmp_eq_u64_equal(self):
"""S_CMP_EQ_U64: comparing equal 64-bit values sets SCC=1."""
val = 0x123456789ABCDEF0
instructions = [
s_mov_b32(s[0], val & 0xFFFFFFFF),
s_mov_b32(s[1], val >> 32),
s_mov_b32(s[2], val & 0xFFFFFFFF),
s_mov_b32(s[3], val >> 32),
s_cmp_eq_u64(s[0:1], s[2:3]),
s_cselect_b32(s[4], 1, 0),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.scc, 1)
self.assertEqual(st.sgpr[4], 1)
def test_s_cmp_eq_u64_different_upper_bits(self):
"""S_CMP_EQ_U64: values differing only in upper 32 bits are not equal."""
# This is the bug case - if only lower 32 bits are compared, these would be equal
instructions = [
s_mov_b32(s[0], 0), # lower 32 bits of value 0
s_mov_b32(s[1], 0), # upper 32 bits of value 0
s_mov_b32(s[2], 0), # lower 32 bits of 0x100000000
s_mov_b32(s[3], 1), # upper 32 bits of 0x100000000
s_cmp_eq_u64(s[0:1], s[2:3]),
s_cselect_b32(s[4], 1, 0),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.scc, 0, "0 != 0x100000000, SCC should be 0")
self.assertEqual(st.sgpr[4], 0)
def test_s_cmp_lg_u64_different(self):
"""S_CMP_LG_U64: different 64-bit values sets SCC=1."""
instructions = [
s_mov_b32(s[0], 0),
s_mov_b32(s[1], 0), # s[0:1] = 0
s_mov_b32(s[2], 0),
s_mov_b32(s[3], 1), # s[2:3] = 0x100000000
s_cmp_lg_u64(s[0:1], s[2:3]),
s_cselect_b32(s[4], 1, 0),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.scc, 1, "0 != 0x100000000, SCC should be 1")
self.assertEqual(st.sgpr[4], 1)
if __name__ == '__main__':
unittest.main()
+13 -287
View File
@@ -255,7 +255,7 @@ class TestF16Conversions(unittest.TestCase):
def test_v_cvt_f16_f32_small(self):
"""V_CVT_F16_F32 converts small f32 value."""
from extra.assembly.amd.test.hw.helpers import f32_to_f16
from extra.assembly.amd.pcode import f32_to_f16
instructions = [
v_mov_b32_e32(v[0], 0.5),
v_cvt_f16_f32_e32(v[1], v[0]),
@@ -293,7 +293,7 @@ class TestF16Conversions(unittest.TestCase):
def test_v_cvt_f16_f32_reads_full_32bit_source(self):
"""V_CVT_F16_F32 must read full 32-bit f32 source."""
from extra.assembly.amd.test.hw.helpers import _f16
from extra.assembly.amd.pcode import _f16
instructions = [
s_mov_b32(s[0], 0x3fc00000), # f32 1.5
v_mov_b32_e32(v[0], s[0]),
@@ -348,142 +348,6 @@ class TestF16Conversions(unittest.TestCase):
self.assertEqual(result, 1, f"Expected 1 from high bits, got {result}")
class TestF64Conversions(unittest.TestCase):
"""Tests for f64 conversion instructions. Regression tests for f32_to_f64/f64_to_f32."""
def test_v_cvt_f64_f32_one(self):
"""V_CVT_F64_F32 converts f32 1.0 to f64."""
instructions = [
s_mov_b32(s[0], f2i(1.0)),
v_mov_b32_e32(v[0], s[0]),
v_cvt_f64_f32_e32(v[2:3], v[0]),
]
st = run_program(instructions, n_lanes=1)
result = i642f((st.vgpr[0][3] << 32) | st.vgpr[0][2])
self.assertAlmostEqual(result, 1.0, places=10)
def test_v_cvt_f64_f32_negative(self):
"""V_CVT_F64_F32 converts f32 -2.5 to f64."""
instructions = [
s_mov_b32(s[0], f2i(-2.5)),
v_mov_b32_e32(v[0], s[0]),
v_cvt_f64_f32_e32(v[2:3], v[0]),
]
st = run_program(instructions, n_lanes=1)
result = i642f((st.vgpr[0][3] << 32) | st.vgpr[0][2])
self.assertAlmostEqual(result, -2.5, places=10)
def test_v_cvt_f64_f32_pi(self):
"""V_CVT_F64_F32 converts f32 pi to f64."""
import math
instructions = [
s_mov_b32(s[0], f2i(3.14159265)),
v_mov_b32_e32(v[0], s[0]),
v_cvt_f64_f32_e32(v[2:3], v[0]),
]
st = run_program(instructions, n_lanes=1)
result = i642f((st.vgpr[0][3] << 32) | st.vgpr[0][2])
self.assertAlmostEqual(result, 3.14159265, places=5)
def test_v_cvt_f64_f32_zero(self):
"""V_CVT_F64_F32 converts f32 0.0 to f64."""
instructions = [
v_mov_b32_e32(v[0], 0),
v_cvt_f64_f32_e32(v[2:3], v[0]),
]
st = run_program(instructions, n_lanes=1)
result = i642f((st.vgpr[0][3] << 32) | st.vgpr[0][2])
self.assertEqual(result, 0.0)
def test_v_cvt_f32_f64_one(self):
"""V_CVT_F32_F64 converts f64 1.0 to f32."""
f64_bits = f2i64(1.0)
lo, hi = f64_bits & 0xFFFFFFFF, (f64_bits >> 32) & 0xFFFFFFFF
instructions = [
s_mov_b32(s[0], lo),
s_mov_b32(s[1], hi),
v_mov_b32_e32(v[0], s[0]),
v_mov_b32_e32(v[1], s[1]),
v_cvt_f32_f64_e32(v[2], v[0:1]),
]
st = run_program(instructions, n_lanes=1)
result = i2f(st.vgpr[0][2])
self.assertAlmostEqual(result, 1.0, places=5)
def test_v_cvt_f32_f64_negative(self):
"""V_CVT_F32_F64 converts f64 -3.5 to f32."""
f64_bits = f2i64(-3.5)
lo, hi = f64_bits & 0xFFFFFFFF, (f64_bits >> 32) & 0xFFFFFFFF
instructions = [
s_mov_b32(s[0], lo),
s_mov_b32(s[1], hi),
v_mov_b32_e32(v[0], s[0]),
v_mov_b32_e32(v[1], s[1]),
v_cvt_f32_f64_e32(v[2], v[0:1]),
]
st = run_program(instructions, n_lanes=1)
result = i2f(st.vgpr[0][2])
self.assertAlmostEqual(result, -3.5, places=5)
def test_v_cvt_f32_f64_large(self):
"""V_CVT_F32_F64 converts large f64 to f32."""
f64_bits = f2i64(123456.789)
lo, hi = f64_bits & 0xFFFFFFFF, (f64_bits >> 32) & 0xFFFFFFFF
instructions = [
s_mov_b32(s[0], lo),
s_mov_b32(s[1], hi),
v_mov_b32_e32(v[0], s[0]),
v_mov_b32_e32(v[1], s[1]),
v_cvt_f32_f64_e32(v[2], v[0:1]),
]
st = run_program(instructions, n_lanes=1)
result = i2f(st.vgpr[0][2])
self.assertAlmostEqual(result, 123456.789, places=0)
def test_v_cvt_f64_i32_positive(self):
"""V_CVT_F64_I32 converts positive i32 to f64."""
instructions = [
s_mov_b32(s[0], 42),
v_mov_b32_e32(v[0], s[0]),
v_cvt_f64_i32_e32(v[2:3], v[0]),
]
st = run_program(instructions, n_lanes=1)
result = i642f((st.vgpr[0][3] << 32) | st.vgpr[0][2])
self.assertAlmostEqual(result, 42.0, places=10)
def test_v_cvt_f64_i32_negative(self):
"""V_CVT_F64_I32 converts negative i32 to f64."""
instructions = [
s_mov_b32(s[0], 0xFFFFFFFF), # -1 as i32
v_mov_b32_e32(v[0], s[0]),
v_cvt_f64_i32_e32(v[2:3], v[0]),
]
st = run_program(instructions, n_lanes=1)
result = i642f((st.vgpr[0][3] << 32) | st.vgpr[0][2])
self.assertAlmostEqual(result, -1.0, places=10)
def test_v_cvt_f64_u32_large(self):
"""V_CVT_F64_U32 converts large u32 to f64."""
instructions = [
s_mov_b32(s[0], 0xFFFFFFFF), # max u32
v_mov_b32_e32(v[0], s[0]),
v_cvt_f64_u32_e32(v[2:3], v[0]),
]
st = run_program(instructions, n_lanes=1)
result = i642f((st.vgpr[0][3] << 32) | st.vgpr[0][2])
self.assertAlmostEqual(result, 4294967295.0, places=0)
def test_v_cvt_f64_u32_zero(self):
"""V_CVT_F64_U32 converts 0 to f64."""
instructions = [
v_mov_b32_e32(v[0], 0),
v_cvt_f64_u32_e32(v[2:3], v[0]),
]
st = run_program(instructions, n_lanes=1)
result = i642f((st.vgpr[0][3] << 32) | st.vgpr[0][2])
self.assertEqual(result, 0.0)
class TestClz(unittest.TestCase):
"""Tests for V_CLZ_I32_U32 - count leading zeros."""
@@ -662,16 +526,16 @@ class TestExp(unittest.TestCase):
class TestReadFirstLane(unittest.TestCase):
"""Tests for V_READFIRSTLANE_B32."""
def _readfirstlane(self, sdst, vsrc):
def _readfirstlane(self, sdst_idx, vsrc):
"""Helper to create V_READFIRSTLANE_B32 with SGPR destination."""
return v_readfirstlane_b32_e32(sdst, vsrc)
return VOP1(VOP1Op.V_READFIRSTLANE_B32, vdst=RawImm(sdst_idx), src0=vsrc)
def test_v_readfirstlane_b32_basic(self):
"""V_READFIRSTLANE_B32 reads from the first active lane."""
instructions = [
v_lshlrev_b32_e32(v[0], 2, v[255]),
v_add_nc_u32_e32(v[0], 1000, v[0]),
self._readfirstlane(s[0], v[0]),
self._readfirstlane(0, v[0]),
v_mov_b32_e32(v[1], s[0]),
]
st = run_program(instructions, n_lanes=4)
@@ -683,7 +547,7 @@ class TestReadFirstLane(unittest.TestCase):
instructions = [
v_lshlrev_b32_e32(v[7], 5, v[255]),
v_add_nc_u32_e32(v[7], 200, v[7]),
self._readfirstlane(s[0], v[7]),
self._readfirstlane(0, v[7]),
v_mov_b32_e32(v[8], s[0]),
]
st = run_program(instructions, n_lanes=4)
@@ -696,7 +560,7 @@ class TestCvtF16Modifiers(unittest.TestCase):
def test_v_cvt_f32_f16_abs_negative(self):
"""V_CVT_F32_F16 with |abs| on negative value."""
from extra.assembly.amd.test.hw.helpers import f32_to_f16
from extra.assembly.amd.pcode import f32_to_f16
f16_neg1 = f32_to_f16(-1.0) # 0xbc00
instructions = [
s_mov_b32(s[0], f16_neg1),
@@ -709,7 +573,7 @@ class TestCvtF16Modifiers(unittest.TestCase):
def test_v_cvt_f32_f16_abs_positive(self):
"""V_CVT_F32_F16 with |abs| on positive value (should stay positive)."""
from extra.assembly.amd.test.hw.helpers import f32_to_f16
from extra.assembly.amd.pcode import f32_to_f16
f16_2 = f32_to_f16(2.0) # 0x4000
instructions = [
s_mov_b32(s[0], f16_2),
@@ -722,7 +586,7 @@ class TestCvtF16Modifiers(unittest.TestCase):
def test_v_cvt_f32_f16_neg_positive(self):
"""V_CVT_F32_F16 with neg on positive value."""
from extra.assembly.amd.test.hw.helpers import f32_to_f16
from extra.assembly.amd.pcode import f32_to_f16
f16_2 = f32_to_f16(2.0) # 0x4000
instructions = [
s_mov_b32(s[0], f16_2),
@@ -735,7 +599,7 @@ class TestCvtF16Modifiers(unittest.TestCase):
def test_v_cvt_f32_f16_neg_negative(self):
"""V_CVT_F32_F16 with neg on negative value (double negative)."""
from extra.assembly.amd.test.hw.helpers import f32_to_f16
from extra.assembly.amd.pcode import f32_to_f16
f16_neg2 = f32_to_f16(-2.0) # 0xc000
instructions = [
s_mov_b32(s[0], f16_neg2),
@@ -748,7 +612,7 @@ class TestCvtF16Modifiers(unittest.TestCase):
def test_v_cvt_f16_f32_then_pack_for_wmma(self):
"""CVT F32->F16 followed by pack (common WMMA pattern)."""
from extra.assembly.amd.test.hw.helpers import _f16
from extra.assembly.amd.pcode import _f16
f32_val = 3.5
instructions = [
s_mov_b32(s[0], f2i(f32_val)),
@@ -804,7 +668,7 @@ class TestConversionRounding(unittest.TestCase):
def test_f16_to_f32_precision(self):
"""F16 to F32 conversion precision."""
from extra.assembly.amd.test.hw.helpers import f32_to_f16
from extra.assembly.amd.pcode import f32_to_f16
f16_val = f32_to_f16(1.5)
instructions = [
s_mov_b32(s[0], f16_val),
@@ -816,7 +680,7 @@ class TestConversionRounding(unittest.TestCase):
def test_f16_denormal_to_f32(self):
"""F16 denormal converts to small positive f32."""
from extra.assembly.amd.test.hw.helpers import _f16
from extra.assembly.amd.pcode import _f16
f16_denorm = 0x0001 # Smallest positive f16 denormal
instructions = [
v_mov_b32_e32(v[0], f16_denorm),
@@ -1374,143 +1238,5 @@ class TestFloorEdgeCases(unittest.TestCase):
self.assertAlmostEqual(i2f(st.vgpr[0][1]), -1.0, places=5)
class TestVop1F16HiHalf(unittest.TestCase):
"""Regression tests for VOP1 f16 hi-half source operand handling.
For 16-bit VOP1 operations, when src0 is in the range v[128]+ (offset >= 384),
the hardware reads from the high 16 bits of v[src0-128]. The emulator must
extract bits [31:16] from the actual VGPR.
"""
def test_v_cvt_f32_f16_src_hi_half(self):
"""V_CVT_F32_F16 with source from hi-half (v[128]+).
When src0 >= v[128], it reads from the high 16 bits of v[src0-128].
This is critical for global_load_d16_hi_b16 + v_cvt_f32_f16 patterns.
Regression test for: VOP1 f16 src0 hi-half extraction bug.
"""
instructions = [
# v[0] = 0x4000_3c00: hi=f16(2.0), lo=f16(1.0)
s_mov_b32(s[0], 0x40003c00),
v_mov_b32_e32(v[0], s[0]),
# v_cvt_f32_f16 v[1], v[128] (reads hi half of v[0])
# Should convert f16(2.0) to f32(2.0)
v_cvt_f32_f16_e32(v[1], v[128]),
]
st = run_program(instructions, n_lanes=1)
result = i2f(st.vgpr[0][1])
self.assertAlmostEqual(result, 2.0, places=5, msg=f"Expected f32(2.0), got {result}")
def test_v_cvt_f32_f16_src_lo_vs_hi(self):
"""V_CVT_F32_F16 comparing lo and hi half reads.
v[0] has different values in lo and hi halves.
v_cvt_f32_f16 v[1], v[0] should read lo (1.0)
v_cvt_f32_f16 v[2], v[128] should read hi (2.0)
Regression test for: VOP1 f16 src0 hi-half extraction bug.
"""
instructions = [
# v[0] = 0x4000_3c00: hi=f16(2.0), lo=f16(1.0)
s_mov_b32(s[0], 0x40003c00),
v_mov_b32_e32(v[0], s[0]),
# Read from lo half
v_cvt_f32_f16_e32(v[1], v[0]),
# Read from hi half
v_cvt_f32_f16_e32(v[2], v[128]),
]
st = run_program(instructions, n_lanes=1)
result_lo = i2f(st.vgpr[0][1])
result_hi = i2f(st.vgpr[0][2])
self.assertAlmostEqual(result_lo, 1.0, places=5, msg=f"Expected f32(1.0) from lo, got {result_lo}")
self.assertAlmostEqual(result_hi, 2.0, places=5, msg=f"Expected f32(2.0) from hi, got {result_hi}")
def test_v_cvt_i16_f16_src_hi_half(self):
"""V_CVT_I16_F16 with source from hi-half.
Regression test for: VOP1 f16 src0 hi-half extraction bug.
"""
instructions = [
# v[0] = 0xc000_3c00: hi=f16(-2.0), lo=f16(1.0)
s_mov_b32(s[0], 0xc0003c00),
v_mov_b32_e32(v[0], s[0]),
# v_cvt_i16_f16 v[1], v[128] (reads hi half of v[0])
# Should convert f16(-2.0) to i16(-2)
v_cvt_i16_f16_e32(v[1], v[128]),
]
st = run_program(instructions, n_lanes=1)
result = st.vgpr[0][1] & 0xffff
expected = (-2) & 0xffff
self.assertEqual(result, expected, f"Expected i16(-2)=0x{expected:04x}, got 0x{result:04x}")
def test_v_mov_b16_src_hi_half(self):
"""V_MOV_B16 with source from hi-half.
Regression test for: VOP1 f16 src0 hi-half extraction bug.
"""
instructions = [
# v[0] = 0xBEEF_DEAD: hi=0xBEEF, lo=0xDEAD
s_mov_b32(s[0], 0xBEEFDEAD),
v_mov_b32_e32(v[0], s[0]),
# v[1] = 0x0000_0000 initially
v_mov_b32_e32(v[1], 0),
# v_mov_b16 v[1], v[128] (reads hi half of v[0])
# Should move 0xBEEF to v[1].lo
v_mov_b16_e32(v[1], v[128]),
]
st = run_program(instructions, n_lanes=1)
result = st.vgpr[0][1] & 0xffff
self.assertEqual(result, 0xBEEF, f"Expected 0xBEEF from hi half, got 0x{result:04x}")
class TestReciprocalF16(unittest.TestCase):
"""Tests for V_RCP_F16 - reciprocal in half precision.
The pcode uses a 16-bit float literal: D0.f16 = 16'1.0 / S0.f16
This tests that the sized float literal (16'1.0) is correctly parsed.
"""
def test_v_rcp_f16_one(self):
"""V_RCP_F16: 1/1.0 = 1.0"""
import struct
def f16_to_bits(f): return struct.unpack('<H', struct.pack('<e', f))[0]
def bits_to_f16(b): return struct.unpack('<e', struct.pack('<H', b))[0]
instructions = [
# Load f16 1.0 into low 16 bits of v[0]
v_mov_b32_e32(v[0], f16_to_bits(1.0)),
v_rcp_f16_e32(v[1], v[0]),
]
st = run_program(instructions, n_lanes=1)
result = bits_to_f16(st.vgpr[0][1] & 0xFFFF)
self.assertAlmostEqual(result, 1.0, places=2, msg="1/1.0 should be 1.0")
def test_v_rcp_f16_two(self):
"""V_RCP_F16: 1/2.0 = 0.5"""
import struct
def f16_to_bits(f): return struct.unpack('<H', struct.pack('<e', f))[0]
def bits_to_f16(b): return struct.unpack('<e', struct.pack('<H', b))[0]
instructions = [
v_mov_b32_e32(v[0], f16_to_bits(2.0)),
v_rcp_f16_e32(v[1], v[0]),
]
st = run_program(instructions, n_lanes=1)
result = bits_to_f16(st.vgpr[0][1] & 0xFFFF)
self.assertAlmostEqual(result, 0.5, places=2, msg="1/2.0 should be 0.5")
def test_v_rcp_f16_four(self):
"""V_RCP_F16: 1/4.0 = 0.25"""
import struct
def f16_to_bits(f): return struct.unpack('<H', struct.pack('<e', f))[0]
def bits_to_f16(b): return struct.unpack('<e', struct.pack('<H', b))[0]
instructions = [
v_mov_b32_e32(v[0], f16_to_bits(4.0)),
v_rcp_f16_e32(v[1], v[0]),
]
st = run_program(instructions, n_lanes=1)
result = bits_to_f16(st.vgpr[0][1] & 0xFFFF)
self.assertAlmostEqual(result, 0.25, places=2, msg="1/4.0 should be 0.25")
if __name__ == '__main__':
unittest.main()
+7 -421
View File
@@ -46,7 +46,7 @@ class TestBasicArithmetic(unittest.TestCase):
instructions = [
v_mov_b32_e32(v[0], 2.0),
v_mov_b32_e32(v[1], 4.0),
v_fmaak_f32_e32(v[2], v[0], v[1], literal=0x3f800000),
v_fmaak_f32_e32(v[2], v[0], v[1], 0x3f800000),
]
st = run_program(instructions, n_lanes=1)
self.assertAlmostEqual(i2f(st.vgpr[0][2]), 9.0, places=5)
@@ -56,7 +56,7 @@ class TestBasicArithmetic(unittest.TestCase):
instructions = [
v_mov_b32_e32(v[0], 2.0),
v_mov_b32_e32(v[1], 1.0),
v_fmamk_f32_e32(v[2], v[0], v[1], literal=0x40800000),
v_fmamk_f32_e32(v[2], v[0], 0x40800000, v[1]),
]
st = run_program(instructions, n_lanes=1)
self.assertAlmostEqual(i2f(st.vgpr[0][2]), 9.0, places=5)
@@ -66,7 +66,7 @@ class TestBasicArithmetic(unittest.TestCase):
instructions = [
v_mov_b32_e32(v[0], 4.0),
v_mov_b32_e32(v[1], 1.0),
v_fmamk_f32_e32(v[2], v[0], v[1], literal=f2i(0.5)),
v_fmamk_f32_e32(v[2], v[0], f2i(0.5), v[1]),
]
st = run_program(instructions, n_lanes=1)
self.assertAlmostEqual(i2f(st.vgpr[0][2]), 3.0, places=5)
@@ -244,7 +244,7 @@ class TestF16Ops(unittest.TestCase):
s_mov_b32(s[1], 0x4200), # f16 3.0
v_mov_b32_e32(v[0], s[0]),
v_mov_b32_e32(v[1], s[1]),
v_fmaak_f16_e32(v[2], v[0], v[1], literal=0x3c00), # + f16 1.0
v_fmaak_f16_e32(v[2], v[0], v[1], 0x3c00), # + f16 1.0
]
st = run_program(instructions, n_lanes=1)
result = st.vgpr[0][2] & 0xffff
@@ -341,300 +341,13 @@ class TestHiHalfOps(unittest.TestCase):
self.assertEqual(result, 0x4200, f"Lane {lane}: expected 0x4200, got 0x{result:04x}")
class TestVop2F16HiHalf(unittest.TestCase):
"""Regression tests for VOP2 f16 hi-half operand handling.
These test the bugs where:
1. VOP2 vsrc1 >= 384 (v[128]+) wasn't extracting hi 16 bits
2. VOP2 vdst >= 384 (v[128]+) wasn't preserving lo 16 bits
"""
def test_v_add_f16_e32_vsrc1_hi_half(self):
"""V_ADD_F16_E32 with vsrc1 from hi-half (v[128]+).
When vsrc1 >= 384 (representing v[128]+), the hardware reads from the hi 16 bits
of v[vsrc1-128]. The emulator must extract bits [31:16] from the actual VGPR.
Regression test for: VOP2 f16 vsrc1 hi-half extraction bug.
"""
instructions = [
# v[0] = 0x4000_3c00: hi=f16(2.0), lo=f16(1.0)
s_mov_b32(s[0], 0x40003c00),
v_mov_b32_e32(v[0], s[0]),
# v_add_f16_e32 v[1], v[0], v[128] (vsrc1=v[128] reads hi of v[0])
# In VOP2 encoding, vsrc1=384 means v[128], which maps to v[0].hi
# v[1] = v[0].lo + v[0].hi = 1.0 + 2.0 = 3.0
VOP2(VOP2Op.V_ADD_F16, vdst=v[1], src0=v[0], vsrc1=v[128]),
]
st = run_program(instructions, n_lanes=1)
result = st.vgpr[0][1] & 0xffff
# 1.0 + 2.0 = 3.0, f16 3.0 = 0x4200
self.assertEqual(result, 0x4200, f"Expected f16(3.0)=0x4200, got 0x{result:04x}")
def test_v_mul_f16_e32_vsrc1_hi_half(self):
"""V_MUL_F16_E32 with vsrc1 from hi-half.
Regression test for: VOP2 f16 vsrc1 hi-half extraction bug.
"""
instructions = [
# v[0] = 0x4200_4000: hi=f16(3.0), lo=f16(2.0)
s_mov_b32(s[0], 0x42004000),
v_mov_b32_e32(v[0], s[0]),
# v_mul_f16_e32 v[1], v[0], v[128] (vsrc1=v[128] reads hi of v[0])
# v[1] = v[0].lo * v[0].hi = 2.0 * 3.0 = 6.0
VOP2(VOP2Op.V_MUL_F16, vdst=v[1], src0=v[0], vsrc1=v[128]),
]
st = run_program(instructions, n_lanes=1)
result = st.vgpr[0][1] & 0xffff
# 2.0 * 3.0 = 6.0, f16 6.0 = 0x4600
self.assertEqual(result, 0x4600, f"Expected f16(6.0)=0x4600, got 0x{result:04x}")
def test_v_add_f16_e32_vdst_hi_half(self):
"""V_ADD_F16_E32 writing to hi-half destination (v[128]+).
When vdst >= 384 (representing v[128]+), the hardware writes to bits [31:16]
of v[vdst-128] while preserving bits [15:0]. The emulator must merge the result.
Regression test for: VOP2 f16 vdst hi-half write bug.
"""
instructions = [
# v[0] = 0x0000_BEEF: lo has marker value
s_mov_b32(s[0], 0x0000BEEF),
v_mov_b32_e32(v[0], s[0]),
# v[1] = f16(1.0), v[2] = f16(2.0)
s_mov_b32(s[1], 0x3c00),
s_mov_b32(s[2], 0x4000),
v_mov_b32_e32(v[1], s[1]),
v_mov_b32_e32(v[2], s[2]),
# v_add_f16_e32 v[128], v[1], v[2] (vdst=v[128] writes hi of v[0])
# v[0].hi = 1.0 + 2.0 = 3.0, v[0].lo preserved = 0xBEEF
VOP2(VOP2Op.V_ADD_F16, vdst=v[128], src0=v[1], vsrc1=v[2]),
]
st = run_program(instructions, n_lanes=1)
hi = (st.vgpr[0][0] >> 16) & 0xffff
lo = st.vgpr[0][0] & 0xffff
# hi = 3.0 = 0x4200, lo preserved = 0xBEEF
self.assertEqual(hi, 0x4200, f"Expected hi=f16(3.0)=0x4200, got 0x{hi:04x}")
self.assertEqual(lo, 0xBEEF, f"Expected lo preserved=0xBEEF, got 0x{lo:04x}")
def test_v_mul_f16_e32_vdst_hi_half(self):
"""V_MUL_F16_E32 writing to hi-half destination.
Regression test for: VOP2 f16 vdst hi-half write bug.
"""
instructions = [
# v[0] = 0x0000_DEAD: lo has marker value
s_mov_b32(s[0], 0x0000DEAD),
v_mov_b32_e32(v[0], s[0]),
# v[1] = f16(2.0), v[2] = f16(4.0)
s_mov_b32(s[1], 0x4000),
s_mov_b32(s[2], 0x4400),
v_mov_b32_e32(v[1], s[1]),
v_mov_b32_e32(v[2], s[2]),
# v_mul_f16_e32 v[128], v[1], v[2] (vdst=v[128] writes hi of v[0])
# v[0].hi = 2.0 * 4.0 = 8.0, v[0].lo preserved = 0xDEAD
VOP2(VOP2Op.V_MUL_F16, vdst=v[128], src0=v[1], vsrc1=v[2]),
]
st = run_program(instructions, n_lanes=1)
hi = (st.vgpr[0][0] >> 16) & 0xffff
lo = st.vgpr[0][0] & 0xffff
# hi = 8.0 = 0x4800, lo preserved = 0xDEAD
self.assertEqual(hi, 0x4800, f"Expected hi=f16(8.0)=0x4800, got 0x{hi:04x}")
self.assertEqual(lo, 0xDEAD, f"Expected lo preserved=0xDEAD, got 0x{lo:04x}")
def test_v_add_f16_e32_both_hi_half(self):
"""V_ADD_F16_E32 with both vsrc1 and vdst as hi-half (different underlying regs).
Tests the combination of both fixes: reading vsrc1 from hi-half AND
writing result to hi-half destination, using different underlying VGPRs.
Regression test for: VOP2 f16 hi-half bugs (combined).
"""
instructions = [
# v[0] = 0x4000_xxxx: hi=f16(2.0) for vsrc1
s_mov_b32(s[0], 0x40000000),
v_mov_b32_e32(v[0], s[0]),
# v[1] = 0x0000_3c00: lo=f16(1.0) for src0
s_mov_b32(s[1], 0x00003c00),
v_mov_b32_e32(v[1], s[1]),
# v[2] = 0x0000_CAFE: lo=marker for vdst preservation
s_mov_b32(s[2], 0x0000CAFE),
v_mov_b32_e32(v[2], s[2]),
# v_add_f16_e32 v[130], v[1], v[128]
# src0 = v[1].lo = 1.0
# vsrc1 = v[128] reads v[0].hi = 2.0
# result = 1.0 + 2.0 = 3.0
# vdst = v[130] writes to v[2].hi, preserving v[2].lo
VOP2(VOP2Op.V_ADD_F16, vdst=v[130], src0=v[1], vsrc1=v[128]),
]
st = run_program(instructions, n_lanes=1)
hi = (st.vgpr[0][2] >> 16) & 0xffff
lo = st.vgpr[0][2] & 0xffff
# hi = 3.0 = 0x4200, lo preserved = 0xCAFE
self.assertEqual(hi, 0x4200, f"Expected hi=f16(3.0)=0x4200, got 0x{hi:04x}")
self.assertEqual(lo, 0xCAFE, f"Expected lo preserved=0xCAFE, got 0x{lo:04x}")
def test_v_fmac_f16_e32_vsrc1_hi_half(self):
"""V_FMAC_F16_E32 with vsrc1 from hi-half.
V_FMAC_F16: vdst = vdst + src0 * vsrc1
Regression test for: VOP2 f16 vsrc1 hi-half extraction bug.
"""
instructions = [
# v[0] = 0x4000_3c00: hi=f16(2.0), lo=f16(1.0)
s_mov_b32(s[0], 0x40003c00),
v_mov_b32_e32(v[0], s[0]),
# v[1] = f16(3.0) = 0x4200
s_mov_b32(s[1], 0x4200),
v_mov_b32_e32(v[1], s[1]),
# v_fmac_f16_e32 v[1], v[0], v[128]
# vdst = v[1] = 3.0 + v[0].lo * v[0].hi = 3.0 + 1.0 * 2.0 = 5.0
VOP2(VOP2Op.V_FMAC_F16, vdst=v[1], src0=v[0], vsrc1=v[128]),
]
st = run_program(instructions, n_lanes=1)
result = st.vgpr[0][1] & 0xffff
# 3.0 + 1.0 * 2.0 = 5.0, f16 5.0 = 0x4500
self.assertEqual(result, 0x4500, f"Expected f16(5.0)=0x4500, got 0x{result:04x}")
def test_v_fmac_f16_e32_vdst_hi_half(self):
"""V_FMAC_F16_E32 writing to hi-half destination.
V_FMAC_F16: vdst.h = vdst.h + src0 * vsrc1
When vdst is v[128]+, the accumulator D0 must also read from the hi-half.
This tests the bug where D0 was read from lo-half instead of hi-half.
Regression test for: VOP2 FMAC hi-half D0 accumulator read bug.
"""
instructions = [
# v[0] = 0x3800_DEAD: hi=f16(0.5), lo=marker (0xDEAD)
s_mov_b32(s[0], 0x3800DEAD),
v_mov_b32_e32(v[0], s[0]),
# v[1] = f16(2.0) = 0x4000
s_mov_b32(s[1], 0x4000),
v_mov_b32_e32(v[1], s[1]),
# v[2] = f16(3.0) = 0x4200
s_mov_b32(s[2], 0x4200),
v_mov_b32_e32(v[2], s[2]),
# v_fmac_f16_e32 v[128], v[1], v[2]
# vdst = v[128] means v[0].hi
# D0 = v[0].hi = 0.5
# result = D0 + src0 * vsrc1 = 0.5 + 2.0 * 3.0 = 6.5
# v[0].hi = 6.5, v[0].lo preserved = 0xDEAD
VOP2(VOP2Op.V_FMAC_F16, vdst=v[128], src0=v[1], vsrc1=v[2]),
]
st = run_program(instructions, n_lanes=1)
hi = (st.vgpr[0][0] >> 16) & 0xffff
lo = st.vgpr[0][0] & 0xffff
# hi = 6.5 = 0x4680, lo preserved = 0xDEAD
self.assertEqual(hi, 0x4680, f"Expected hi=f16(6.5)=0x4680, got 0x{hi:04x}")
self.assertEqual(lo, 0xDEAD, f"Expected lo preserved=0xDEAD, got 0x{lo:04x}")
def test_v_mul_f16_e32_src0_hi_half(self):
"""V_MUL_F16_E32 with src0 from hi-half (src0 >= v[128]).
When src0 >= 384 (representing v[128]+), the hardware reads from the hi 16 bits
of v[src0-128]. The emulator must extract bits [31:16] from the actual VGPR.
Regression test for: VOP2 f16 src0 hi-half extraction bug.
"""
instructions = [
# v[0] = 0x4000_3c00: hi=f16(2.0), lo=f16(1.0)
s_mov_b32(s[0], 0x40003c00),
v_mov_b32_e32(v[0], s[0]),
# v[1] = f16(3.0) = 0x4200
s_mov_b32(s[1], 0x4200),
v_mov_b32_e32(v[1], s[1]),
# v_mul_f16_e32 v[2], v[128], v[1]
# src0 = v[128] reads from v[0].hi = 2.0
# result = 2.0 * 3.0 = 6.0
VOP2(VOP2Op.V_MUL_F16, vdst=v[2], src0=v[128], vsrc1=v[1]),
]
st = run_program(instructions, n_lanes=1)
result = st.vgpr[0][2] & 0xffff
# 2.0 * 3.0 = 6.0, f16 6.0 = 0x4600
self.assertEqual(result, 0x4600, f"Expected f16(6.0)=0x4600, got 0x{result:04x}")
def test_v_add_f16_e32_src0_hi_half(self):
"""V_ADD_F16_E32 with src0 from hi-half (src0 >= v[128]).
Regression test for: VOP2 f16 src0 hi-half extraction bug.
"""
instructions = [
# v[0] = 0x4000_3c00: hi=f16(2.0), lo=f16(1.0)
s_mov_b32(s[0], 0x40003c00),
v_mov_b32_e32(v[0], s[0]),
# v[1] = f16(5.0) = 0x4500
s_mov_b32(s[1], 0x4500),
v_mov_b32_e32(v[1], s[1]),
# v_add_f16_e32 v[2], v[128], v[1]
# src0 = v[128] reads from v[0].hi = 2.0
# result = 2.0 + 5.0 = 7.0
VOP2(VOP2Op.V_ADD_F16, vdst=v[2], src0=v[128], vsrc1=v[1]),
]
st = run_program(instructions, n_lanes=1)
result = st.vgpr[0][2] & 0xffff
# 2.0 + 5.0 = 7.0, f16 7.0 = 0x4700
self.assertEqual(result, 0x4700, f"Expected f16(7.0)=0x4700, got 0x{result:04x}")
class TestF16InlineConstants(unittest.TestCase):
"""Regression tests for VOP2 F16 inline float constants.
For 16-bit VOP2 operations (v_add_f16, v_mul_f16, etc.), inline float constants
like 1.0, 2.0 must use F16 encoding (0x3c00, 0x4000) not F32 encoding (0x3f800000).
The emulator's rsrc() function needs bits=16 to select F16_INLINE constants.
Regression test for: VOP2 16-bit inline constant using F32 instead of F16.
"""
def test_v_add_f16_inline_constant_1_0(self):
"""V_ADD_F16_E32 with inline constant 1.0 should use F16 encoding."""
instructions = [
s_mov_b32(s[0], 0x3c00), # f16 1.0
v_mov_b32_e32(v[0], s[0]),
# v_add_f16_e32 v[1], 1.0, v[0] -- 1.0 must be F16 0x3c00, not F32 0x3f800000
v_add_f16_e32(v[1], 1.0, v[0]),
]
st = run_program(instructions, n_lanes=1)
result = st.vgpr[0][1] & 0xFFFF
# 1.0 + 1.0 = 2.0, f16 2.0 = 0x4000
self.assertEqual(result, 0x4000, f"Expected f16(2.0)=0x4000, got 0x{result:04x}")
def test_v_add_f16_inline_constant_2_0(self):
"""V_ADD_F16_E32 with inline constant 2.0."""
instructions = [
s_mov_b32(s[0], 0x4200), # f16 3.0
v_mov_b32_e32(v[0], s[0]),
v_add_f16_e32(v[1], 2.0, v[0]),
]
st = run_program(instructions, n_lanes=1)
result = st.vgpr[0][1] & 0xFFFF
# 2.0 + 3.0 = 5.0, f16 5.0 = 0x4500
self.assertEqual(result, 0x4500, f"Expected f16(5.0)=0x4500, got 0x{result:04x}")
def test_v_mul_f16_inline_constant(self):
"""V_MUL_F16_E32 with inline constant 2.0."""
instructions = [
s_mov_b32(s[0], 0x4200), # f16 3.0
v_mov_b32_e32(v[0], s[0]),
v_mul_f16_e32(v[1], 2.0, v[0]),
]
st = run_program(instructions, n_lanes=1)
result = st.vgpr[0][1] & 0xFFFF
# 2.0 * 3.0 = 6.0, f16 6.0 = 0x4600
self.assertEqual(result, 0x4600, f"Expected f16(6.0)=0x4600, got 0x{result:04x}")
class TestCndmask(unittest.TestCase):
"""Tests for V_CNDMASK_B32 and V_CNDMASK_B16."""
def test_v_cndmask_b16_select_src0(self):
"""V_CNDMASK_B16 selects src0 when VCC bit is 0."""
instructions = [
s_mov_b32(VCC_LO, 0), # VCC = 0
s_mov_b32(s[SrcEnum.VCC_LO - 128], 0), # VCC = 0
s_mov_b32(s[0], 0x3c00), # f16 1.0
s_mov_b32(s[1], 0x4000), # f16 2.0
v_mov_b32_e32(v[0], s[0]),
@@ -648,7 +361,7 @@ class TestCndmask(unittest.TestCase):
def test_v_cndmask_b16_select_src1(self):
"""V_CNDMASK_B16 selects src1 when VCC bit is 1."""
instructions = [
s_mov_b32(VCC_LO, 1), # VCC = 1
s_mov_b32(s[SrcEnum.VCC_LO - 128], 1), # VCC = 1
s_mov_b32(s[0], 0x3c00), # f16 1.0
s_mov_b32(s[1], 0x4000), # f16 2.0
v_mov_b32_e32(v[0], s[0]),
@@ -668,7 +381,7 @@ class TestCndmask(unittest.TestCase):
v_mov_b32_e32(v[1], s[1]),
s_mov_b32(s[2], 0xDEAD0000), # v2 initial: hi=0xDEAD, lo=0
v_mov_b32_e32(v[2], s[2]),
s_mov_b32(VCC_LO, 0), # vcc = 0, select src0
s_mov_b32(s[SrcEnum.VCC_LO - 128], 0), # vcc = 0, select src0
# opsel=0b1011: bit0=src0 hi, bit1=src1 hi, bit3=dst hi
VOP3(VOP3Op.V_CNDMASK_B16, vdst=v[2], src0=v[0], src1=v[1], src2=SrcEnum.VCC_LO, opsel=0b1011),
]
@@ -734,132 +447,5 @@ class TestSpecialFloatValues(unittest.TestCase):
self.assertEqual(st.vgpr[0][1], 0x00000000)
class TestCarryOps(unittest.TestCase):
"""Tests for VOP2 carry instructions (v_add_co_ci_u32, v_sub_co_ci_u32, v_subrev_co_ci_u32)."""
def test_v_subrev_co_ci_u32_no_borrow(self):
"""V_SUBREV_CO_CI_U32: D0 = S1 - S0 - VCC_IN, when VCC_IN=0."""
instructions = [
s_mov_b32(VCC_LO, 0), # VCC = 0 (no borrow in)
v_mov_b32_e32(v[0], 5), # S0 = 5
v_mov_b32_e32(v[1], 10), # S1 = 10
v_subrev_co_ci_u32_e32(v[2], v[0], v[1]), # D0 = 10 - 5 - 0 = 5
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][2], 5)
self.assertEqual(st.vcc, 0) # No borrow out
def test_v_subrev_co_ci_u32_with_borrow(self):
"""V_SUBREV_CO_CI_U32: D0 = S1 - S0 - VCC_IN, when VCC_IN=1."""
instructions = [
s_mov_b32(VCC_LO, 1), # VCC = 1 (borrow in)
v_mov_b32_e32(v[0], 5), # S0 = 5
v_mov_b32_e32(v[1], 10), # S1 = 10
v_subrev_co_ci_u32_e32(v[2], v[0], v[1]), # D0 = 10 - 5 - 1 = 4
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][2], 4)
self.assertEqual(st.vcc, 0) # No borrow out
def test_v_subrev_co_ci_u32_generates_borrow(self):
"""V_SUBREV_CO_CI_U32: generates borrow when S0 + VCC_IN > S1."""
instructions = [
s_mov_b32(VCC_LO, 0), # VCC = 0
v_mov_b32_e32(v[0], 10), # S0 = 10
v_mov_b32_e32(v[1], 5), # S1 = 5
v_subrev_co_ci_u32_e32(v[2], v[0], v[1]), # D0 = 5 - 10 - 0 = -5 (underflow)
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][2], 0xFFFFFFFB) # -5 as unsigned
self.assertEqual(st.vcc, 1) # Borrow out
def test_v_add_co_ci_u32_no_carry(self):
"""V_ADD_CO_CI_U32: D0 = S0 + S1 + VCC_IN, when VCC_IN=0."""
instructions = [
s_mov_b32(VCC_LO, 0), # VCC = 0 (no carry in)
v_mov_b32_e32(v[0], 5), # S0 = 5
v_mov_b32_e32(v[1], 10), # S1 = 10
v_add_co_ci_u32_e32(v[2], v[0], v[1]), # D0 = 5 + 10 + 0 = 15
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][2], 15)
self.assertEqual(st.vcc, 0) # No carry out
def test_v_add_co_ci_u32_with_carry(self):
"""V_ADD_CO_CI_U32: D0 = S0 + S1 + VCC_IN, when VCC_IN=1."""
instructions = [
s_mov_b32(VCC_LO, 1), # VCC = 1 (carry in)
v_mov_b32_e32(v[0], 5), # S0 = 5
v_mov_b32_e32(v[1], 10), # S1 = 10
v_add_co_ci_u32_e32(v[2], v[0], v[1]), # D0 = 5 + 10 + 1 = 16
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][2], 16)
self.assertEqual(st.vcc, 0) # No carry out
def test_v_add_co_ci_u32_generates_carry(self):
"""V_ADD_CO_CI_U32: generates carry when overflow occurs."""
instructions = [
s_mov_b32(VCC_LO, 1), # VCC = 1 (carry in)
s_mov_b32(s[0], 0xFFFFFFFF), # max u32
v_mov_b32_e32(v[0], s[0]), # S0 = 0xFFFFFFFF
v_mov_b32_e32(v[1], 0), # S1 = 0
v_add_co_ci_u32_e32(v[2], v[0], v[1]), # D0 = 0xFFFFFFFF + 0 + 1 = 0 (overflow)
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][2], 0) # Overflowed to 0
self.assertEqual(st.vcc, 1) # Carry out
def test_v_sub_co_ci_u32_no_borrow(self):
"""V_SUB_CO_CI_U32: D0 = S0 - S1 - VCC_IN, when VCC_IN=0."""
instructions = [
s_mov_b32(VCC_LO, 0), # VCC = 0 (no borrow in)
v_mov_b32_e32(v[0], 10), # S0 = 10
v_mov_b32_e32(v[1], 5), # S1 = 5
v_sub_co_ci_u32_e32(v[2], v[0], v[1]), # D0 = 10 - 5 - 0 = 5
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][2], 5)
self.assertEqual(st.vcc, 0) # No borrow out
def test_v_sub_co_ci_u32_vop3sd_separate_carry_regs(self):
"""VOP3SD V_SUB_CO_CI_U32: carry-in from src2, carry-out to sdst (separate registers).
This tests the VOP3SD encoding where src2 specifies the carry-in register
independently from sdst (carry-out). The bug was reading carry-in from sdst
instead of src2.
Computation: D0 = S0 - S1 - carry_in = 0 - 0 - 1 = -1 = 0xFFFFFFFF
"""
instructions = [
s_mov_b32(s[6], 1), # carry-in = 1 (in s[6])
s_mov_b32(s[10], 0), # carry-out dest = 0 initially (in s[10])
# VOP3SD: v_sub_co_ci_u32(vdst, sdst, src0, src1, src2)
# src2 is carry-in (s[6]=1), sdst is carry-out (s[10])
v_sub_co_ci_u32(v[0], s[10], 0, 0, s[6]), # D0 = 0 - 0 - 1 = -1
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][0], 0xFFFFFFFF) # -1 as unsigned
self.assertEqual(st.sgpr[10], 1) # Borrow out to s[10]
def test_v_add_co_ci_u32_vop3sd_separate_carry_regs(self):
"""VOP3SD V_ADD_CO_CI_U32: carry-in from src2, carry-out to sdst (separate registers).
This tests the VOP3SD encoding where src2 specifies the carry-in register
independently from sdst (carry-out).
Computation: D0 = S0 + S1 + carry_in = 5 + 10 + 1 = 16
"""
instructions = [
s_mov_b32(s[6], 1), # carry-in = 1 (in s[6])
s_mov_b32(s[10], 0), # carry-out dest = 0 initially (in s[10])
# VOP3SD: v_add_co_ci_u32(vdst, sdst, src0, src1, src2)
v_add_co_ci_u32(v[0], s[10], 5, 10, s[6]), # D0 = 5 + 10 + 1 = 16
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][0], 16)
self.assertEqual(st.sgpr[10], 0) # No carry out
if __name__ == '__main__':
unittest.main()
+75 -591
View File
@@ -58,95 +58,6 @@ class TestFMA(unittest.TestCase):
self.assertTrue(math.isinf(result) and result > 0)
class TestFmacE64(unittest.TestCase):
"""Regression tests for V_FMAC_F32 VOP3 encoding (e64).
V_FMAC_F32: D0 = D0 + S0 * S1 (fused multiply-add with accumulator)
The VOP3 encoding needs to read D0 from the destination register as the
accumulator input, not just write to it.
Regression test for: VOP3 FMAC missing D0 accumulator bug.
"""
def test_v_fmac_f32_e64_basic(self):
"""V_FMAC_F32_E64: basic accumulate test."""
instructions = [
v_mov_b32_e32(v[0], 2.0), # S0 = 2.0
v_mov_b32_e32(v[1], 3.0), # S1 = 3.0
v_mov_b32_e32(v[2], 1.0), # D0 (accumulator) = 1.0
# v_fmac_f32_e64 v[2], v[0], v[1]
# D0 = D0 + S0 * S1 = 1.0 + 2.0 * 3.0 = 7.0
v_fmac_f32_e64(v[2], v[0], v[1]),
]
st = run_program(instructions, n_lanes=1)
self.assertAlmostEqual(i2f(st.vgpr[0][2]), 7.0, places=5)
def test_v_fmac_f32_e64_with_sgpr_sources(self):
"""V_FMAC_F32_E64 with SGPR sources (common in AMD_LLVM output).
This tests the exact pattern that was failing: v_fmac_f32_e64(v[0], s[4], 0)
where src0 is SGPR and src1 is inline constant 0.
Regression test for: VOP3 FMAC missing D0 accumulator bug.
"""
instructions = [
s_mov_b32(s[4], f2i(2.0)), # S0 = 2.0 in SGPR
v_mov_b32_e32(v[0], 5.0), # D0 (accumulator) = 5.0
# v_fmac_f32_e64 v[0], s[4], 0
# D0 = D0 + S0 * S1 = 5.0 + 2.0 * 0.0 = 5.0
v_fmac_f32_e64(v[0], s[4], 0),
]
st = run_program(instructions, n_lanes=1)
self.assertAlmostEqual(i2f(st.vgpr[0][0]), 5.0, places=5)
def test_v_fmac_f32_e64_with_two_sgprs(self):
"""V_FMAC_F32_E64 with two SGPR sources.
Tests pattern: v_fmac_f32_e64(v[0], s[a], s[b])
Regression test for: VOP3 FMAC missing D0 accumulator bug.
"""
instructions = [
s_mov_b32(s[10], f2i(3.0)), # S0 = 3.0
s_mov_b32(s[12], f2i(4.0)), # S1 = 4.0
v_mov_b32_e32(v[9], 2.0), # D0 (accumulator) = 2.0
# v_fmac_f32_e64 v[9], s[10], s[12]
# D0 = D0 + S0 * S1 = 2.0 + 3.0 * 4.0 = 14.0
v_fmac_f32_e64(v[9], s[10], s[12]),
]
st = run_program(instructions, n_lanes=1)
self.assertAlmostEqual(i2f(st.vgpr[0][9]), 14.0, places=5)
def test_v_fmac_f32_e64_accumulates_correctly(self):
"""V_FMAC_F32_E64 accumulates multiple times."""
instructions = [
v_mov_b32_e32(v[0], 0.0), # D0 = 0.0
v_mov_b32_e32(v[1], 1.0), # S0 = 1.0
v_mov_b32_e32(v[2], 2.0), # S1 = 2.0
# First: D0 = 0.0 + 1.0 * 2.0 = 2.0
v_fmac_f32_e64(v[0], v[1], v[2]),
# Second: D0 = 2.0 + 1.0 * 2.0 = 4.0
v_fmac_f32_e64(v[0], v[1], v[2]),
# Third: D0 = 4.0 + 1.0 * 2.0 = 6.0
v_fmac_f32_e64(v[0], v[1], v[2]),
]
st = run_program(instructions, n_lanes=1)
self.assertAlmostEqual(i2f(st.vgpr[0][0]), 6.0, places=5)
def test_v_fmac_f32_e64_negative_accumulator(self):
"""V_FMAC_F32_E64 with negative accumulator."""
instructions = [
v_mov_b32_e32(v[0], 2.0), # S0 = 2.0
v_mov_b32_e32(v[1], 3.0), # S1 = 3.0
v_mov_b32_e32(v[2], -10.0), # D0 (accumulator) = -10.0
# D0 = -10.0 + 2.0 * 3.0 = -4.0
v_fmac_f32_e64(v[2], v[0], v[1]),
]
st = run_program(instructions, n_lanes=1)
self.assertAlmostEqual(i2f(st.vgpr[0][2]), -4.0, places=5)
class TestDivScale(unittest.TestCase):
"""Tests for V_DIV_SCALE_F32."""
@@ -267,7 +178,7 @@ class TestDivFmas(unittest.TestCase):
def test_div_fmas_f32_no_scale(self):
"""V_DIV_FMAS_F32: VCC=0 -> normal FMA."""
instructions = [
s_mov_b32(VCC_LO, 0),
s_mov_b32(s[SrcEnum.VCC_LO - 128], 0),
v_mov_b32_e32(v[0], 2.0),
v_mov_b32_e32(v[1], 3.0),
v_mov_b32_e32(v[2], 1.0),
@@ -279,7 +190,7 @@ class TestDivFmas(unittest.TestCase):
def test_div_fmas_f32_scale_up(self):
"""V_DIV_FMAS_F32: VCC=1 with S2 >= 2.0 -> scale by 2^+64."""
instructions = [
s_mov_b32(VCC_LO, 1),
s_mov_b32(s[SrcEnum.VCC_LO - 128], 1),
v_mov_b32_e32(v[0], 1.0),
v_mov_b32_e32(v[1], 1.0),
v_mov_b32_e32(v[2], 2.0),
@@ -292,7 +203,7 @@ class TestDivFmas(unittest.TestCase):
def test_div_fmas_f32_scale_down(self):
"""V_DIV_FMAS_F32: VCC=1 with S2 < 2.0 -> scale by 2^-64."""
instructions = [
s_mov_b32(VCC_LO, 1),
s_mov_b32(s[SrcEnum.VCC_LO - 128], 1),
v_mov_b32_e32(v[0], 2.0),
v_mov_b32_e32(v[1], 3.0),
v_mov_b32_e32(v[2], 1.0),
@@ -305,7 +216,7 @@ class TestDivFmas(unittest.TestCase):
def test_div_fmas_f32_per_lane_vcc(self):
"""V_DIV_FMAS_F32: different VCC per lane with S2 < 2.0."""
instructions = [
s_mov_b32(VCC_LO, 0b0101),
s_mov_b32(s[SrcEnum.VCC_LO - 128], 0b0101),
v_mov_b32_e32(v[0], 1.0),
v_mov_b32_e32(v[1], 1.0),
v_mov_b32_e32(v[2], 1.0),
@@ -582,7 +493,7 @@ class TestMad64(unittest.TestCase):
s_mov_b32(s[1], 4),
v_mov_b32_e32(v[2], 5),
v_mov_b32_e32(v[3], 0),
v_mad_u64_u32(v[4:5], SrcEnum.NULL, s[0], s[1], v[2:3]),
v_mad_u64_u32(v[4], SrcEnum.NULL, s[0], s[1], v[2]),
]
st = run_program(instructions, n_lanes=1)
result_lo = st.vgpr[0][4]
@@ -597,7 +508,7 @@ class TestMad64(unittest.TestCase):
s_mov_b32(s[1], 2),
v_mov_b32_e32(v[2], 0),
v_mov_b32_e32(v[3], 0),
v_mad_u64_u32(v[4:5], SrcEnum.NULL, s[0], s[1], v[2:3]),
v_mad_u64_u32(v[4], SrcEnum.NULL, s[0], s[1], v[2]),
]
st = run_program(instructions, n_lanes=1)
result_lo = st.vgpr[0][4]
@@ -609,8 +520,8 @@ class TestMad64(unittest.TestCase):
class TestLaneOps(unittest.TestCase):
"""Tests for lane operations (readlane, writelane)."""
def _readlane(self, sdst, vsrc, lane_idx):
return v_readlane_b32(sdst, vsrc, lane_idx)
def _readlane(self, sdst_idx, vsrc, lane_idx):
return VOP3(VOP3Op.V_READLANE_B32, vdst=RawImm(sdst_idx), src0=vsrc, src1=lane_idx)
def test_v_readlane_b32_basic(self):
"""V_READLANE_B32 reads a value from a specific lane's VGPR."""
@@ -618,7 +529,7 @@ class TestLaneOps(unittest.TestCase):
v_lshlrev_b32_e32(v[0], 1, v[255]),
v_lshlrev_b32_e32(v[1], 3, v[255]),
v_add_nc_u32_e32(v[0], v[0], v[1]),
self._readlane(s[0], v[0], 2),
self._readlane(0, v[0], 2),
v_mov_b32_e32(v[2], s[0]),
]
st = run_program(instructions, n_lanes=4)
@@ -630,7 +541,7 @@ class TestLaneOps(unittest.TestCase):
instructions = [
v_lshlrev_b32_e32(v[0], 2, v[255]), # v0 = lane_id * 4
v_add_nc_u32_e32(v[0], 100, v[0]), # v0 = 100 + lane_id * 4
self._readlane(s[0], v[0], 0), # s0 = lane 0's v0 = 100
self._readlane(0, v[0], 0), # s0 = lane 0's v0 = 100
v_mov_b32_e32(v[1], s[0]),
]
st = run_program(instructions, n_lanes=4)
@@ -642,7 +553,7 @@ class TestLaneOps(unittest.TestCase):
instructions = [
v_lshlrev_b32_e32(v[0], 2, v[255]), # v0 = lane_id * 4
v_add_nc_u32_e32(v[0], 100, v[0]), # v0 = 100 + lane_id * 4
self._readlane(s[0], v[0], 3), # s0 = lane 3's v0 = 112
self._readlane(0, v[0], 3), # s0 = lane 3's v0 = 112
v_mov_b32_e32(v[1], s[0]),
]
st = run_program(instructions, n_lanes=4)
@@ -654,7 +565,7 @@ class TestLaneOps(unittest.TestCase):
instructions = [
v_lshlrev_b32_e32(v[5], 3, v[255]), # v5 = lane_id * 8
v_add_nc_u32_e32(v[5], 50, v[5]), # v5 = 50 + lane_id * 8
self._readlane(s[0], v[5], 1), # s0 = lane 1's v5 = 58
self._readlane(0, v[5], 1), # s0 = lane 1's v5 = 58
v_mov_b32_e32(v[6], s[0]),
]
st = run_program(instructions, n_lanes=4)
@@ -681,7 +592,7 @@ class TestLaneOps(unittest.TestCase):
v_mov_b32_e32(v[0], 0),
s_mov_b32(s[0], 0xdeadbeef),
v_writelane_b32(v[0], s[0], 1), # Write to lane 1
self._readlane(s[1], v[0], 1), # Read back from lane 1 into s1
self._readlane(1, v[0], 1), # Read back from lane 1 into s1
v_mov_b32_e32(v[1], s[1]),
]
st = run_program(instructions, n_lanes=4)
@@ -692,12 +603,12 @@ class TestLaneOps(unittest.TestCase):
"""Simulate a wave reduction using readlane - common WMMA/reduction pattern."""
instructions = [
v_add_nc_u32_e32(v[0], 1, v[255]), # v0 = lane_id + 1 (1, 2, 3, 4)
self._readlane(s[0], v[0], 0), # s0 = 1
self._readlane(s[1], v[0], 1), # s1 = 2
self._readlane(0, v[0], 0), # s0 = 1
self._readlane(1, v[0], 1), # s1 = 2
s_add_u32(s[0], s[0], s[1]), # s0 = 3
self._readlane(s[1], v[0], 2), # s1 = 3
self._readlane(1, v[0], 2), # s1 = 3
s_add_u32(s[0], s[0], s[1]), # s0 = 6
self._readlane(s[1], v[0], 3), # s1 = 4
self._readlane(1, v[0], 3), # s1 = 4
s_add_u32(s[0], s[0], s[1]), # s0 = 10
v_mov_b32_e32(v[1], s[0]), # Broadcast sum to all lanes
]
@@ -800,7 +711,7 @@ class TestLaneOps(unittest.TestCase):
v_mov_b32_e32(v[8], 0), # Initialize v8 = 0
s_mov_b32(s[0], 0xABCD1234),
v_writelane_b32(v[8], s[0], 2), # Write to lane 2's v8
self._readlane(s[1], v[8], 2), # Read back from lane 2's v8 into s1
self._readlane(1, v[8], 2), # Read back from lane 2's v8 into s1
v_mov_b32_e32(v[1], s[1]), # Broadcast to all lanes
]
st = run_program(instructions, n_lanes=4)
@@ -830,12 +741,12 @@ class TestLaneOps(unittest.TestCase):
s_mov_b32(s[0], 40),
v_writelane_b32(v[6], s[0], 3), # lane 3 gets 40
# Now read them all back and sum
self._readlane(s[0], v[6], 0), # s0 = 10
self._readlane(s[1], v[6], 1), # s1 = 20
self._readlane(0, v[6], 0), # s0 = 10
self._readlane(1, v[6], 1), # s1 = 20
s_add_u32(s[0], s[0], s[1]), # s0 = 30
self._readlane(s[1], v[6], 2), # s1 = 30
self._readlane(1, v[6], 2), # s1 = 30
s_add_u32(s[0], s[0], s[1]), # s0 = 60
self._readlane(s[1], v[6], 3), # s1 = 40
self._readlane(1, v[6], 3), # s1 = 40
s_add_u32(s[0], s[0], s[1]), # s0 = 100
v_mov_b32_e32(v[7], s[0]), # Broadcast sum to all lanes
]
@@ -857,7 +768,7 @@ class TestF16Modifiers(unittest.TestCase):
def test_v_fma_f16_inline_const_1_0(self):
"""V_FMA_F16: a*b + 1.0 should use f16 inline constant."""
from extra.assembly.amd.test.hw.helpers import f32_to_f16, _f16
from extra.assembly.amd.pcode import f32_to_f16, _f16
f16_a = f32_to_f16(0.325928) # ~0x3537
f16_b = f32_to_f16(-0.486572) # ~0xb7c9
instructions = [
@@ -874,7 +785,7 @@ class TestF16Modifiers(unittest.TestCase):
def test_v_fma_f16_inline_const_0_5(self):
"""V_FMA_F16: a*b + 0.5 should use f16 inline constant."""
from extra.assembly.amd.test.hw.helpers import f32_to_f16, _f16
from extra.assembly.amd.pcode import f32_to_f16, _f16
f16_a = f32_to_f16(2.0)
f16_b = f32_to_f16(3.0)
instructions = [
@@ -891,7 +802,7 @@ class TestF16Modifiers(unittest.TestCase):
def test_v_fma_f16_inline_const_neg_1_0(self):
"""V_FMA_F16: a*b + (-1.0) should use f16 inline constant."""
from extra.assembly.amd.test.hw.helpers import f32_to_f16, _f16
from extra.assembly.amd.pcode import f32_to_f16, _f16
f16_a = f32_to_f16(2.0)
f16_b = f32_to_f16(3.0)
instructions = [
@@ -908,7 +819,7 @@ class TestF16Modifiers(unittest.TestCase):
def test_v_add_f16_abs_both(self):
"""V_ADD_F16 with abs on both operands."""
from extra.assembly.amd.test.hw.helpers import f32_to_f16, _f16
from extra.assembly.amd.pcode import f32_to_f16, _f16
f16_neg2 = f32_to_f16(-2.0)
f16_neg3 = f32_to_f16(-3.0)
instructions = [
@@ -924,7 +835,7 @@ class TestF16Modifiers(unittest.TestCase):
def test_v_mul_f16_neg_abs(self):
"""V_MUL_F16 with neg on one operand and abs on another."""
from extra.assembly.amd.test.hw.helpers import f32_to_f16, _f16
from extra.assembly.amd.pcode import f32_to_f16, _f16
f16_2 = f32_to_f16(2.0)
f16_neg3 = f32_to_f16(-3.0)
instructions = [
@@ -943,14 +854,14 @@ class TestF16Modifiers(unittest.TestCase):
This tests the case from AMD_LLVM sin(0) where V_FMAC_F16 writes to v0.h.
"""
from extra.assembly.amd.test.hw.helpers import _f16
from extra.assembly.amd.pcode import _f16
instructions = [
s_mov_b32(s[0], 0x38003c00), # v0 = {hi=0.5, lo=1.0}
v_mov_b32_e32(v[0], s[0]),
s_mov_b32(s[1], 0x38000000), # v1 = {hi=0.5, lo=0.0}
v_mov_b32_e32(v[1], s[1]),
# v_fmac_f16 v0.h, literal(0.318...), v1.l: D.h = D.h + S0 * S1 = 0.5 + 0.318 * 0.0 = 0.5
v_fmac_f16_e32(v[0].h, 0x3518, v[1]),
VOP2(VOP2Op.V_FMAC_F16, vdst=RawImm(128), src0=RawImm(255), vsrc1=RawImm(1), literal=0x3518),
]
st = run_program(instructions, n_lanes=1)
v0 = st.vgpr[0][0]
@@ -990,7 +901,7 @@ class TestF64Ops(unittest.TestCase):
s_mov_b32(s[1], one_f64 >> 32),
v_mov_b32_e32(v[0], s[0]),
v_mov_b32_e32(v[1], s[1]),
v_add_f64(v[2:3], v[0:1], SrcEnum.POS_ONE), # 1.0 + 1.0 = 2.0
v_add_f64(v[2:4], v[0:2], SrcEnum.POS_ONE), # 1.0 + 1.0 = 2.0
]
st = run_program(instructions, n_lanes=1)
result = i642f(st.vgpr[0][2] | (st.vgpr[0][3] << 32))
@@ -1009,7 +920,7 @@ class TestF64Ops(unittest.TestCase):
v_mov_b32_e32(v[1], s[1]),
v_mov_b32_e32(v[2], s[2]),
v_mov_b32_e32(v[3], s[3]),
v_mul_f64(v[4:5], v[0:1], v[2:3]),
v_mul_f64(v[4:6], v[0:2], v[2:4]),
]
st = run_program(instructions, n_lanes=1)
result = i642f(st.vgpr[0][4] | (st.vgpr[0][5] << 32))
@@ -1025,7 +936,7 @@ class TestF64Ops(unittest.TestCase):
v_mov_b32_e32(v[1], s[1]),
s_mov_b32(s[2], 0xDEADBEEF),
v_mov_b32_e32(v[3], s[2]), # Canary in v3
v_cvt_i32_f64_e32(v[2], v[0:1]),
v_cvt_i32_f64_e32(v[2], v[0:2]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][2], 0xffffffff, "-1.0 converts to -1")
@@ -1041,7 +952,7 @@ class TestF64Ops(unittest.TestCase):
s_mov_b32(s[1], val_bits >> 32),
v_mov_b32_e32(v[0], s[0]),
v_mov_b32_e32(v[1], s[1]),
v_ldexp_f64(v[2:3], v[0:1], 0xffffffe0), # -32
v_ldexp_f64(v[2:4], v[0:2], 0xffffffe0), # -32
]
st = run_program(instructions, n_lanes=1)
result = i642f(st.vgpr[0][2] | (st.vgpr[0][3] << 32))
@@ -1053,8 +964,8 @@ class TestF64Ops(unittest.TestCase):
instructions = [
s_mov_b32(s[0], two_f64 & 0xffffffff),
s_mov_b32(s[1], two_f64 >> 32),
v_frexp_mant_f64_e32(v[0:1], s[0:1]),
v_frexp_exp_i32_f64_e32(v[2], s[0:1]),
v_frexp_mant_f64_e32(v[0:2], s[0:2]),
v_frexp_exp_i32_f64_e32(v[2], s[0:2]),
]
st = run_program(instructions, n_lanes=1)
mant = i642f(st.vgpr[0][0] | (st.vgpr[0][1] << 32))
@@ -1077,7 +988,7 @@ class TestF64Ops(unittest.TestCase):
s_mov_b32(s[3], one_f64 >> 32),
v_mov_b32_e32(v[2], s[2]),
v_mov_b32_e32(v[3], s[3]),
VOP3SD(VOP3SDOp.V_DIV_SCALE_F64, vdst=v[4:5], sdst=s[10:11], src0=v[0:1], src1=v[0:1], src2=v[2:3]),
VOP3SD(VOP3SDOp.V_DIV_SCALE_F64, vdst=v[4], sdst=s[10], src0=v[0], src1=v[0], src2=v[2]),
]
st = run_program(instructions, n_lanes=1)
result = i642f(st.vgpr[0][4] | (st.vgpr[0][5] << 32))
@@ -1092,14 +1003,14 @@ class TestF64Ops(unittest.TestCase):
instructions = [
s_mov_b32(s[0], val & 0xffffffff),
s_mov_b32(s[1], (val >> 32) & 0xffffffff),
v_trunc_f64_e32(v[0:1], s[0:1]),
v_ldexp_f64(v[2:3], v[0:1], 0xffffffe0), # -32
v_floor_f64_e32(v[2:3], v[2:3]),
v_trunc_f64_e32(v[0:2], s[0:2]),
v_ldexp_f64(v[2:4], v[0:2], 0xffffffe0), # -32
v_floor_f64_e32(v[2:4], v[2:4]),
s_mov_b32(s[2], f2i64(-4294967296.0) & 0xffffffff),
s_mov_b32(s[3], f2i64(-4294967296.0) >> 32),
v_fma_f64(v[0:1], s[2:3], v[2:3], v[0:1]),
v_cvt_u32_f64_e32(v[4], v[0:1]),
v_cvt_i32_f64_e32(v[5], v[2:3]),
v_fma_f64(v[0:2], s[2:4], v[2:4], v[0:2]),
v_cvt_u32_f64_e32(v[4], v[0:2]),
v_cvt_i32_f64_e32(v[5], v[2:4]),
]
st = run_program(instructions, n_lanes=1)
lo = st.vgpr[0][4]
@@ -1114,7 +1025,7 @@ class TestF64Ops(unittest.TestCase):
instructions = [
s_mov_b32(s[0], 0x00000000), # low bits of 1.0
s_mov_b32(s[1], 0x3ff00000), # high bits of 1.0
v_trig_preop_f64(v[0:1], abs(s[0:1]), 0),
v_trig_preop_f64(v[0], abs(s[0]), 0),
]
st = run_program(instructions, n_lanes=1)
result = i642f(st.vgpr[0][0] | (st.vgpr[0][1] << 32))
@@ -1127,9 +1038,9 @@ class TestF64Ops(unittest.TestCase):
instructions = [
s_mov_b32(s[0], 0x00000000), # low bits of 1.0
s_mov_b32(s[1], 0x3ff00000), # high bits of 1.0
v_trig_preop_f64(v[0:1], abs(s[0:1]), 0),
v_trig_preop_f64(v[2:3], abs(s[0:1]), 1),
v_trig_preop_f64(v[4:5], abs(s[0:1]), 2),
v_trig_preop_f64(v[0], abs(s[0]), 0),
v_trig_preop_f64(v[2], abs(s[0]), 1),
v_trig_preop_f64(v[4], abs(s[0]), 2),
]
st = run_program(instructions, n_lanes=1)
p0 = i642f(st.vgpr[0][0] | (st.vgpr[0][1] << 32))
@@ -1164,7 +1075,7 @@ class TestF64Ops(unittest.TestCase):
v_mov_b32_e32(v[3], s[3]),
v_mov_b32_e32(v[4], s[4]),
v_mov_b32_e32(v[5], s[5]),
v_fma_f64(v[6:7], v[0:1], v[2:3], v[4:5]),
v_fma_f64(v[6], v[0], v[2], v[4]),
]
# run_program with USE_HW=1 will verify exact bit match with hardware
st = run_program(instructions, n_lanes=1)
@@ -1182,7 +1093,7 @@ class TestMad64More(unittest.TestCase):
s_mov_b32(s[1], 1000),
v_mov_b32_e32(v[2], 0), # S2 lo
v_mov_b32_e32(v[3], 1), # S2 hi = 0x100000000
v_mad_u64_u32(v[4:5], SrcEnum.NULL, s[0], s[1], v[2:3]),
v_mad_u64_u32(v[4], SrcEnum.NULL, s[0], s[1], v[2]),
]
st = run_program(instructions, n_lanes=1)
result_lo = st.vgpr[0][4]
@@ -1198,7 +1109,7 @@ class TestMad64More(unittest.TestCase):
s_mov_b32(s[1], 0xFFFFFFFF),
v_mov_b32_e32(v[2], 0),
v_mov_b32_e32(v[3], 0),
v_mad_u64_u32(v[4:5], SrcEnum.NULL, s[0], s[1], v[2:3]),
v_mad_u64_u32(v[4], SrcEnum.NULL, s[0], s[1], v[2]),
]
st = run_program(instructions, n_lanes=1)
result_lo = st.vgpr[0][4]
@@ -1274,7 +1185,7 @@ class TestF64LiteralOps(unittest.TestCase):
s_mov_b32(s[3], (val_m1 >> 32) & 0xffffffff),
v_mov_b32_e32(v[2], s[2]),
v_mov_b32_e32(v[3], s[3]),
VOP3(VOP3Op.V_FMA_F64, vdst=v[4:5], src0=lit, src1=v[2:3], src2=v[0:1]),
VOP3(VOP3Op.V_FMA_F64, vdst=v[4], src0=RawImm(255), src1=v[2], src2=v[0], literal=lit),
]
st = run_program(instructions, n_lanes=1)
result = i642f(st.vgpr[0][4] | (st.vgpr[0][5] << 32))
@@ -1290,7 +1201,7 @@ class TestF64LiteralOps(unittest.TestCase):
s_mov_b32(s[1], (val >> 32) & 0xffffffff),
v_mov_b32_e32(v[0], s[0]),
v_mov_b32_e32(v[1], s[1]),
v_ldexp_f64(v[2:3], v[0:1], 0xFFFFFFE0), # -32
v_ldexp_f64(v[2:4], v[0:2], 0xFFFFFFE0), # -32
]
st = run_program(instructions, n_lanes=1)
result = i642f(st.vgpr[0][2] | (st.vgpr[0][3] << 32))
@@ -1309,12 +1220,12 @@ class TestF64ToI64Conversion(unittest.TestCase):
s_mov_b32(s[1], (val >> 32) & 0xffffffff),
v_mov_b32_e32(v[0], s[0]),
v_mov_b32_e32(v[1], s[1]),
v_trunc_f64_e32(v[0:1], v[0:1]),
v_ldexp_f64(v[2:3], v[0:1], 0xFFFFFFE0),
v_floor_f64_e32(v[2:3], v[2:3]),
VOP3(VOP3Op.V_FMA_F64, vdst=v[0:1], src0=lit, src1=v[2:3], src2=v[0:1]),
v_cvt_u32_f64_e32(v[4], v[0:1]),
v_cvt_i32_f64_e32(v[5], v[2:3]),
v_trunc_f64_e32(v[0:2], v[0:2]),
v_ldexp_f64(v[2:4], v[0:2], 0xFFFFFFE0),
v_floor_f64_e32(v[2:4], v[2:4]),
VOP3(VOP3Op.V_FMA_F64, vdst=v[0], src0=RawImm(255), src1=v[2], src2=v[0], literal=lit),
v_cvt_u32_f64_e32(v[4], v[0:2]),
v_cvt_i32_f64_e32(v[5], v[2:4]),
]
return instructions
@@ -1370,7 +1281,7 @@ class TestWMMAMore(unittest.TestCase):
instructions.append(v_mov_b32_e32(v[i], s[0]))
for i in range(8):
instructions.append(v_mov_b32_e32(v[i], 0))
instructions.append(v_wmma_f32_16x16x16_f16(v[0:7], v[16:23], v[24:31], v[0:7]))
instructions.append(v_wmma_f32_16x16x16_f16(v[0], v[16], v[24], v[0]))
st = run_program(instructions, n_lanes=32)
any_nonzero = any(st.vgpr[lane][0] != 0 for lane in range(32))
self.assertTrue(any_nonzero, "WMMA should produce non-zero output")
@@ -1474,7 +1385,7 @@ class TestTrigPreop(unittest.TestCase):
instructions = [
s_mov_b32(s[0], 0x00000000), # low bits of 1.0
s_mov_b32(s[1], 0x3ff00000), # high bits of 1.0
v_trig_preop_f64(v[0:1], abs(s[0:1]), 0),
v_trig_preop_f64(v[0], abs(s[0]), 0),
]
st = run_program(instructions, n_lanes=1)
result = i642f(st.vgpr[0][0] | (st.vgpr[0][1] << 32))
@@ -1485,7 +1396,7 @@ class TestTrigPreop(unittest.TestCase):
instructions = [
s_mov_b32(s[0], 0x00000000),
s_mov_b32(s[1], 0x3ff00000),
v_trig_preop_f64(v[0:1], abs(s[0:1]), 1),
v_trig_preop_f64(v[0], abs(s[0]), 1),
]
st = run_program(instructions, n_lanes=1)
result = i642f(st.vgpr[0][0] | (st.vgpr[0][1] << 32))
@@ -1497,7 +1408,7 @@ class TestTrigPreop(unittest.TestCase):
instructions = [
s_mov_b32(s[0], 0x00000000),
s_mov_b32(s[1], 0x3ff00000),
v_trig_preop_f64(v[0:1], abs(s[0:1]), 2),
v_trig_preop_f64(v[0], abs(s[0]), 2),
]
st = run_program(instructions, n_lanes=1)
result = i642f(st.vgpr[0][0] | (st.vgpr[0][1] << 32))
@@ -1510,9 +1421,9 @@ class TestTrigPreop(unittest.TestCase):
instructions = [
s_mov_b32(s[0], 0x00000000),
s_mov_b32(s[1], 0x3ff00000),
v_trig_preop_f64(v[0:1], abs(s[0:1]), 0),
v_trig_preop_f64(v[2:3], abs(s[0:1]), 1),
v_trig_preop_f64(v[4:5], abs(s[0:1]), 2),
v_trig_preop_f64(v[0], abs(s[0]), 0),
v_trig_preop_f64(v[2], abs(s[0]), 1),
v_trig_preop_f64(v[4], abs(s[0]), 2),
]
st = run_program(instructions, n_lanes=1)
p0 = i642f(st.vgpr[0][0] | (st.vgpr[0][1] << 32))
@@ -1529,7 +1440,7 @@ class TestTrigPreop(unittest.TestCase):
instructions = [
s_mov_b32(s[0], large_bits & 0xffffffff),
s_mov_b32(s[1], (large_bits >> 32) & 0xffffffff),
v_trig_preop_f64(v[0:1], abs(s[0:1]), 0),
v_trig_preop_f64(v[0], abs(s[0]), 0),
]
st = run_program(instructions, n_lanes=1)
result = i642f(st.vgpr[0][0] | (st.vgpr[0][1] << 32))
@@ -1544,7 +1455,7 @@ class TestModifierInteractions(unittest.TestCase):
"""-|x| should negate the absolute value."""
instructions = [
v_mov_b32_e32(v[0], -5.0),
VOP3(VOP3Op.V_MUL_F32, vdst=v[1], src0=1.0, src1=v[0], neg=0b10, abs=0b10),
VOP3(VOP3Op.V_MUL_F32, vdst=v[1], src0=1.0, src1=v[0], neg=0b10, abs_=0b10),
]
st = run_program(instructions, n_lanes=1)
self.assertAlmostEqual(i2f(st.vgpr[0][1]), -5.0, places=5)
@@ -1555,8 +1466,8 @@ class TestModifierInteractions(unittest.TestCase):
instructions = [
s_mov_b32(s[0], neg_zero),
v_mov_b32_e32(v[0], s[0]),
VOP3(VOP3Op.V_MUL_F32, vdst=v[1], src0=1.0, src1=v[0], abs=0b10),
VOP3(VOP3Op.V_MUL_F32, vdst=v[2], src0=1.0, src1=v[0], neg=0b10, abs=0b10),
VOP3(VOP3Op.V_MUL_F32, vdst=v[1], src0=1.0, src1=v[0], abs_=0b10),
VOP3(VOP3Op.V_MUL_F32, vdst=v[2], src0=1.0, src1=v[0], neg=0b10, abs_=0b10),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][1], 0x00000000, "|(-0.0)| = +0.0")
@@ -1710,27 +1621,6 @@ class TestCarryBorrow(unittest.TestCase):
self.assertEqual(st.vgpr[0][4], 0x00000000, "lo result")
self.assertEqual(st.vgpr[0][5], 0x00000003, "hi result")
def test_add_co_u32_same_dst_src(self):
"""V_ADD_CO_U32 where dst is same as src - VCC must use original src value."""
instructions = [
s_mov_b32(s[0], 0xFFFFFFFF),
v_mov_b32_e32(v[0], s[0]),
v_add_co_u32(v[0], VCC, v[0], 1), # v[0] = v[0] + 1, VCC should be set from overflow
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][0], 0, "0xFFFFFFFF + 1 = 0")
self.assertEqual(st.vcc & 1, 1, "Should have carry from 0xFFFFFFFF + 1")
def test_add_co_u32_same_dst_src_no_carry(self):
"""V_ADD_CO_U32 where dst is same as src - no carry case."""
instructions = [
v_mov_b32_e32(v[0], 100),
v_add_co_u32(v[0], VCC, v[0], 1), # v[0] = v[0] + 1
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][0], 101, "100 + 1 = 101")
self.assertEqual(st.vcc & 1, 0, "No carry from 100 + 1")
class TestReadlane(unittest.TestCase):
"""Tests for V_READLANE_B32 and related cross-lane operations."""
@@ -1746,12 +1636,15 @@ class TestReadlane(unittest.TestCase):
def test_reduction_pattern(self):
"""Test reduction using readlane."""
def _readlane(sdst_idx, vsrc, lane_idx):
return VOP3(VOP3Op.V_READLANE_B32, vdst=RawImm(sdst_idx), src0=vsrc, src1=lane_idx)
instructions = [
v_mov_b32_e32(v[0], v[255]),
v_readlane_b32(s[0], v[0], 0),
v_readlane_b32(s[1], v[0], 1),
v_readlane_b32(s[2], v[0], 2),
v_readlane_b32(s[3], v[0], 3),
_readlane(0, v[0], 0),
_readlane(1, v[0], 1),
_readlane(2, v[0], 2),
_readlane(3, v[0], 3),
s_add_u32(s[4], s[0], s[1]),
s_add_u32(s[4], s[4], s[2]),
s_add_u32(s[4], s[4], s[3]),
@@ -2402,414 +2295,5 @@ class TestAddF32EdgeCases(unittest.TestCase):
self.assertEqual(st.vgpr[0][2], 0x80000000) # -0
class TestDivScaleF64(unittest.TestCase):
"""Tests for V_DIV_SCALE_F64 - critical for tan() and division.
These tests verify that VCC bits are set independently per lane,
which is essential for correct multi-lane f64 division operations.
"""
def test_div_scale_f64_basic_no_scaling(self):
"""V_DIV_SCALE_F64: normal values with no scaling needed."""
sqrt2 = f2i64(1.4142135623730951)
one = f2i64(1.0)
instructions = [
s_mov_b32(s[0], sqrt2 & 0xffffffff),
s_mov_b32(s[1], sqrt2 >> 32),
s_mov_b32(s[2], one & 0xffffffff),
s_mov_b32(s[3], one >> 32),
v_mov_b32_e32(v[0], s[0]),
v_mov_b32_e32(v[1], s[1]),
v_mov_b32_e32(v[2], s[2]),
v_mov_b32_e32(v[3], s[3]),
VOP3SD(VOP3SDOp.V_DIV_SCALE_F64, vdst=v[4:5], sdst=VCC, src0=v[0:1], src1=v[0:1], src2=v[2:3]),
]
st = run_program(instructions, n_lanes=1)
result = i642f(st.vgpr[0][4] | (st.vgpr[0][5] << 32))
self.assertAlmostEqual(result, 1.4142135623730951, places=10)
self.assertEqual(st.vcc & 1, 0, "VCC should be 0 when no scaling needed")
def test_div_scale_f64_vcc_per_lane_uniform_input(self):
"""V_DIV_SCALE_F64: VCC bits should be set independently per lane (uniform input).
This is a regression test for the bug where VCC = 0x0LL was setting the whole
64-bit VCC register instead of just the current lane's bit. With uniform input
all lanes should get VCC=0.
"""
val = f2i64(2.0)
instructions = [
s_mov_b32(s[0], val & 0xffffffff),
s_mov_b32(s[1], val >> 32),
v_mov_b32_e32(v[0], s[0]),
v_mov_b32_e32(v[1], s[1]),
VOP3SD(VOP3SDOp.V_DIV_SCALE_F64, vdst=v[2:3], sdst=VCC, src0=v[0:1], src1=v[0:1], src2=v[0:1]),
]
st = run_program(instructions, n_lanes=4)
# All lanes should have VCC=0 for normal values
self.assertEqual(st.vcc & 0xf, 0, "All lanes should have VCC=0 for normal values")
# All lanes should have same result
for lane in range(4):
result = i642f(st.vgpr[lane][2] | (st.vgpr[lane][3] << 32))
self.assertAlmostEqual(result, 2.0, places=10, msg=f"Lane {lane} result mismatch")
def test_div_scale_f64_vcc_per_lane_varying_input(self):
"""V_DIV_SCALE_F64: VCC bits set per-lane with different inputs per lane.
This test uses different inputs per lane to verify that VCC is tracked
independently. This catches the bug where the emulator was setting VCC
for all lanes to the same value.
"""
import math
# Use lane-varying input: lane 0 gets 2.0, lane 1 gets 3.0, etc.
# All normal values should result in VCC=0 for each lane
instructions = [
# Set up per-lane values using lane_id
v_cvt_f64_i32_e32(v[0:1], v[255]), # v0:1 = f64(lane_id)
v_add_f64(v[0:1], v[0:1], SrcEnum.POS_TWO), # v0:1 = lane_id + 2.0
VOP3SD(VOP3SDOp.V_DIV_SCALE_F64, vdst=v[2:3], sdst=VCC, src0=v[0:1], src1=v[0:1], src2=v[0:1]),
]
st = run_program(instructions, n_lanes=4)
# All lanes should have VCC=0 (no scaling needed for 2.0, 3.0, 4.0, 5.0)
self.assertEqual(st.vcc & 0xf, 0, "All lanes should have VCC=0 for normal values")
# Verify each lane has correct result
for lane in range(4):
expected = float(lane) + 2.0
result = i642f(st.vgpr[lane][2] | (st.vgpr[lane][3] << 32))
self.assertAlmostEqual(result, expected, places=10, msg=f"Lane {lane}: expected {expected}, got {result}")
def test_div_scale_f64_zero_denom_sets_vcc(self):
"""V_DIV_SCALE_F64: zero denominator -> NaN, VCC=1."""
import math
one = f2i64(1.0)
zero = f2i64(0.0)
instructions = [
s_mov_b32(s[0], one & 0xffffffff),
s_mov_b32(s[1], one >> 32),
s_mov_b32(s[2], zero & 0xffffffff),
s_mov_b32(s[3], zero >> 32),
v_mov_b32_e32(v[0], s[0]), # numer = 1.0
v_mov_b32_e32(v[1], s[1]),
v_mov_b32_e32(v[2], s[2]), # denom = 0.0
v_mov_b32_e32(v[3], s[3]),
VOP3SD(VOP3SDOp.V_DIV_SCALE_F64, vdst=v[4:5], sdst=VCC, src0=v[0:1], src1=v[2:3], src2=v[0:1]),
]
st = run_program(instructions, n_lanes=1)
result = i642f(st.vgpr[0][4] | (st.vgpr[0][5] << 32))
self.assertTrue(math.isnan(result), "Should be NaN for zero denom")
self.assertEqual(st.vcc & 1, 1, "VCC should be 1 for zero denom")
def test_div_scale_f64_mixed_vcc_per_lane(self):
"""V_DIV_SCALE_F64: some lanes need scaling, others don't.
This is the key test for the tan() bug - it verifies that VCC is set
correctly for each lane independently when some lanes need scaling and
others don't.
"""
import math
# Lane 0: normal value (VCC=0), Lane 1: zero denom (VCC=1)
# Lane 2: normal value (VCC=0), Lane 3: zero denom (VCC=1)
normal = f2i64(2.0)
zero = f2i64(0.0)
instructions = [
# Set up numer = 2.0 for all lanes
s_mov_b32(s[0], normal & 0xffffffff),
s_mov_b32(s[1], normal >> 32),
v_mov_b32_e32(v[0], s[0]),
v_mov_b32_e32(v[1], s[1]),
# Set up denom: lane 0,2 get 2.0, lane 1,3 get 0.0
s_mov_b32(s[2], zero & 0xffffffff),
s_mov_b32(s[3], zero >> 32),
v_mov_b32_e32(v[2], s[0]), # default to 2.0
v_mov_b32_e32(v[3], s[1]),
# Override lanes 1 and 3 with 0.0 using writelane
v_writelane_b32(v[2], s[2], 1),
v_writelane_b32(v[3], s[3], 1),
v_writelane_b32(v[2], s[2], 3),
v_writelane_b32(v[3], s[3], 3),
VOP3SD(VOP3SDOp.V_DIV_SCALE_F64, vdst=v[4:5], sdst=VCC, src0=v[0:1], src1=v[2:3], src2=v[0:1]),
]
st = run_program(instructions, n_lanes=4)
# Lanes 0,2 should have VCC=0 (normal), lanes 1,3 should have VCC=1 (zero denom)
self.assertEqual(st.vcc & 0b0001, 0, "Lane 0 VCC should be 0")
self.assertEqual(st.vcc & 0b0010, 0b0010, "Lane 1 VCC should be 1")
self.assertEqual(st.vcc & 0b0100, 0, "Lane 2 VCC should be 0")
self.assertEqual(st.vcc & 0b1000, 0b1000, "Lane 3 VCC should be 1")
# Check results
for lane in [0, 2]:
result = i642f(st.vgpr[lane][4] | (st.vgpr[lane][5] << 32))
self.assertAlmostEqual(result, 2.0, places=10, msg=f"Lane {lane} should be 2.0")
for lane in [1, 3]:
result = i642f(st.vgpr[lane][4] | (st.vgpr[lane][5] << 32))
self.assertTrue(math.isnan(result), f"Lane {lane} should be NaN")
class TestDivFmasF64(unittest.TestCase):
"""Tests for V_DIV_FMAS_F64 - scaling FMA for f64 division.
These tests verify that V_DIV_FMAS applies the correct scaling
based on VCC per lane, which is essential for correct tan() results.
"""
def test_div_fmas_f64_no_scale_vcc0(self):
"""V_DIV_FMAS_F64: VCC=0 -> normal FMA, no scaling."""
a = f2i64(2.0)
b = f2i64(3.0)
c = f2i64(1.0)
instructions = [
s_mov_b32(VCC_LO, 0),
s_mov_b32(s[0], a & 0xffffffff),
s_mov_b32(s[1], a >> 32),
s_mov_b32(s[2], b & 0xffffffff),
s_mov_b32(s[3], b >> 32),
s_mov_b32(s[4], c & 0xffffffff),
s_mov_b32(s[5], c >> 32),
v_mov_b32_e32(v[0], s[0]),
v_mov_b32_e32(v[1], s[1]),
v_mov_b32_e32(v[2], s[2]),
v_mov_b32_e32(v[3], s[3]),
v_mov_b32_e32(v[4], s[4]),
v_mov_b32_e32(v[5], s[5]),
v_div_fmas_f64(v[6:7], v[0:1], v[2:3], v[4:5]),
]
st = run_program(instructions, n_lanes=1)
result = i642f(st.vgpr[0][6] | (st.vgpr[0][7] << 32))
expected = 2.0 * 3.0 + 1.0 # = 7.0
self.assertAlmostEqual(result, expected, places=10)
def test_div_fmas_f64_scale_up_vcc1_large_s2(self):
"""V_DIV_FMAS_F64: VCC=1 with S2 exponent > 1023 -> scale by 2^+128."""
a = f2i64(1.0)
b = f2i64(1.0)
c = f2i64(2.0) # exponent = 1024 > 1023, so scale UP
instructions = [
s_mov_b32(VCC_LO, 1),
s_mov_b32(s[0], a & 0xffffffff),
s_mov_b32(s[1], a >> 32),
s_mov_b32(s[2], b & 0xffffffff),
s_mov_b32(s[3], b >> 32),
s_mov_b32(s[4], c & 0xffffffff),
s_mov_b32(s[5], c >> 32),
v_mov_b32_e32(v[0], s[0]),
v_mov_b32_e32(v[1], s[1]),
v_mov_b32_e32(v[2], s[2]),
v_mov_b32_e32(v[3], s[3]),
v_mov_b32_e32(v[4], s[4]),
v_mov_b32_e32(v[5], s[5]),
v_div_fmas_f64(v[6:7], v[0:1], v[2:3], v[4:5]),
]
st = run_program(instructions, n_lanes=1)
result = i642f(st.vgpr[0][6] | (st.vgpr[0][7] << 32))
expected = (1.0 * 1.0 + 2.0) * (2.0 ** 128) # = 3.0 * 2^128
self.assertAlmostEqual(result, expected, delta=abs(expected) * 1e-10)
def test_div_fmas_f64_scale_down_vcc1_small_s2(self):
"""V_DIV_FMAS_F64: VCC=1 with S2 exponent <= 1023 -> scale by 2^-128."""
a = f2i64(2.0)
b = f2i64(3.0)
c = f2i64(1.0) # exponent = 1023, so scale DOWN
instructions = [
s_mov_b32(VCC_LO, 1),
s_mov_b32(s[0], a & 0xffffffff),
s_mov_b32(s[1], a >> 32),
s_mov_b32(s[2], b & 0xffffffff),
s_mov_b32(s[3], b >> 32),
s_mov_b32(s[4], c & 0xffffffff),
s_mov_b32(s[5], c >> 32),
v_mov_b32_e32(v[0], s[0]),
v_mov_b32_e32(v[1], s[1]),
v_mov_b32_e32(v[2], s[2]),
v_mov_b32_e32(v[3], s[3]),
v_mov_b32_e32(v[4], s[4]),
v_mov_b32_e32(v[5], s[5]),
v_div_fmas_f64(v[6:7], v[0:1], v[2:3], v[4:5]),
]
st = run_program(instructions, n_lanes=1)
result = i642f(st.vgpr[0][6] | (st.vgpr[0][7] << 32))
expected = (2.0 * 3.0 + 1.0) * (2.0 ** -128) # = 7.0 * 2^-128
self.assertAlmostEqual(result, expected, delta=abs(expected) * 1e-10)
def test_div_fmas_f64_per_lane_vcc_varying(self):
"""V_DIV_FMAS_F64: different VCC per lane applies different scaling.
This is the key test for the tan() bug - verifies that scaling is
applied per-lane based on VCC bits, not uniformly.
"""
a = f2i64(1.0)
b = f2i64(1.0)
c = f2i64(1.0) # exponent = 1023, so when VCC=1 it scales DOWN
instructions = [
# VCC = 0b0101: lanes 0,2 scale, lanes 1,3 don't
s_mov_b32(VCC_LO, 0b0101),
s_mov_b32(s[0], a & 0xffffffff),
s_mov_b32(s[1], a >> 32),
s_mov_b32(s[2], b & 0xffffffff),
s_mov_b32(s[3], b >> 32),
s_mov_b32(s[4], c & 0xffffffff),
s_mov_b32(s[5], c >> 32),
v_mov_b32_e32(v[0], s[0]),
v_mov_b32_e32(v[1], s[1]),
v_mov_b32_e32(v[2], s[2]),
v_mov_b32_e32(v[3], s[3]),
v_mov_b32_e32(v[4], s[4]),
v_mov_b32_e32(v[5], s[5]),
v_div_fmas_f64(v[6:7], v[0:1], v[2:3], v[4:5]),
]
st = run_program(instructions, n_lanes=4)
scaled = (1.0 * 1.0 + 1.0) * (2.0 ** -128) # = 2.0 * 2^-128
unscaled = 1.0 * 1.0 + 1.0 # = 2.0
# Lane 0: VCC=1, scale
result0 = i642f(st.vgpr[0][6] | (st.vgpr[0][7] << 32))
self.assertAlmostEqual(result0, scaled, delta=abs(scaled) * 1e-10, msg="Lane 0 should be scaled")
# Lane 1: VCC=0, no scale
result1 = i642f(st.vgpr[1][6] | (st.vgpr[1][7] << 32))
self.assertAlmostEqual(result1, unscaled, places=10, msg="Lane 1 should be unscaled")
# Lane 2: VCC=1, scale
result2 = i642f(st.vgpr[2][6] | (st.vgpr[2][7] << 32))
self.assertAlmostEqual(result2, scaled, delta=abs(scaled) * 1e-10, msg="Lane 2 should be scaled")
# Lane 3: VCC=0, no scale
result3 = i642f(st.vgpr[3][6] | (st.vgpr[3][7] << 32))
self.assertAlmostEqual(result3, unscaled, places=10, msg="Lane 3 should be unscaled")
class TestDivScaleFmasF64Integration(unittest.TestCase):
"""Integration tests for V_DIV_SCALE_F64 + V_DIV_FMAS_F64.
These tests verify the full division sequence used by tan() works
correctly with multiple lanes having different values.
"""
def test_div_scale_then_fmas_multi_lane_tan_pattern(self):
"""Test the pattern used by tan(): DIV_SCALE sets VCC, DIV_FMAS uses it.
This is the exact bug scenario: tan([2.0, 3.0, 4.0]) was failing because
VCC from DIV_SCALE was being set incorrectly for all lanes.
"""
import math
# Set up values like tan() would: different values per lane
instructions = [
# Create per-lane values: 2.0, 3.0, 4.0, 5.0
v_cvt_f64_i32_e32(v[0:1], v[255]), # v0:1 = f64(lane_id)
v_add_f64(v[0:1], v[0:1], SrcEnum.POS_TWO), # numer = lane_id + 2.0
# denom = 1.0 for all lanes (uniform)
v_mov_b32_e32(v[2], f2i64(1.0) & 0xffffffff),
v_mov_b32_e32(v[3], f2i64(1.0) >> 32),
# V_DIV_SCALE_F64: sets VCC per lane
VOP3SD(VOP3SDOp.V_DIV_SCALE_F64, vdst=v[4:5], sdst=VCC, src0=v[0:1], src1=v[2:3], src2=v[0:1]),
# Copy scaled numer for FMA
v_mov_b32_e32(v[6], v[4]),
v_mov_b32_e32(v[7], v[5]),
# V_DIV_FMAS_F64: uses VCC to apply scaling
v_div_fmas_f64(v[8:9], v[6:7], v[2:3], v[4:5]),
]
st = run_program(instructions, n_lanes=4)
# All lanes should have VCC=0 (no scaling needed for normal values)
self.assertEqual(st.vcc & 0xf, 0, "All lanes should have VCC=0 for normal values")
# Verify each lane has correct intermediate value
for lane in range(4):
expected_numer = float(lane) + 2.0
# With VCC=0, DIV_FMAS should just do FMA with no scaling
result = i642f(st.vgpr[lane][8] | (st.vgpr[lane][9] << 32))
# The FMA result should be: scaled_numer * denom + scaled_numer = 2*scaled_numer
expected = expected_numer * 1.0 + expected_numer # Simple FMA for this test setup
self.assertAlmostEqual(result, expected, places=8,
msg=f"Lane {lane}: expected {expected}, got {result}")
class TestVOP3VOPC(unittest.TestCase):
"""Tests for VOP3-encoded VOPC instructions (comparisons with scalar dest)."""
def test_v_cmp_ge_f32_e64_nan(self):
"""V_CMP_GE_F32_E64: |NaN| >= |0.0| should be FALSE (NaN comparisons always false)."""
from extra.assembly.amd.autogen.rdna3.ins import VOP3_SDST
instructions = [
s_mov_b32(s[0], 0xffc00000), # NaN
s_mov_b32(s[1], 0x00000000), # 0.0
v_mov_b32_e32(v[5], s[0]),
v_mov_b32_e32(v[3], s[1]),
VOP3_SDST(VOP3Op.V_CMP_GE_F32, vdst=s[5], src0=v[5], src1=v[3], abs_=3),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.sgpr[5], 0) # NaN comparison is always FALSE
class TestMin3Max3Unsigned(unittest.TestCase):
"""Regression tests for V_MIN3/V_MAX3 with unsigned integer types.
The emulator's _minmax_reduce used UOp.minimum() which implements min(a,b) as
-max(-a,-b). This is broken for unsigned types because negation (mul by -1)
doesn't preserve ordering: for uint16, -0 = 0 but -5 = 65531, so
max(-0, -5) = max(0, 65531) = 65531, and -65531 = 5, giving min(0,5) = 5 (wrong!).
Fix: use comparison-based min/max for unsigned types: min(a,b) = (a<b)?a:b
"""
def test_v_min3_u16_with_zero(self):
"""V_MIN3_U16: min3(0, 3, 5) should return 0, not a wrong value."""
instructions = [
s_mov_b32(s[0], 0), # 0
s_mov_b32(s[1], 3), # 3
s_mov_b32(s[2], 5), # 5
v_mov_b32_e32(v[0], s[0]),
v_min3_u16(v[1], v[0], s[1], s[2]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][1] & 0xFFFF, 0)
def test_v_min3_u16_all_nonzero(self):
"""V_MIN3_U16: min3(2, 5, 3) should return 2."""
instructions = [
s_mov_b32(s[0], 2),
s_mov_b32(s[1], 5),
s_mov_b32(s[2], 3),
v_mov_b32_e32(v[0], s[0]),
v_min3_u16(v[1], v[0], s[1], s[2]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][1] & 0xFFFF, 2)
def test_v_min3_u32_with_zero(self):
"""V_MIN3_U32: min3(0, 100, 50) should return 0."""
instructions = [
s_mov_b32(s[0], 0),
s_mov_b32(s[1], 100),
s_mov_b32(s[2], 50),
v_mov_b32_e32(v[0], s[0]),
v_min3_u32(v[1], v[0], s[1], s[2]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][1], 0)
def test_v_max3_u16_basic(self):
"""V_MAX3_U16: max3(0, 3, 5) should return 5."""
instructions = [
s_mov_b32(s[0], 0),
s_mov_b32(s[1], 3),
s_mov_b32(s[2], 5),
v_mov_b32_e32(v[0], s[0]),
v_max3_u16(v[1], v[0], s[1], s[2]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][1] & 0xFFFF, 5)
def test_v_min_u16_two_operand(self):
"""V_MIN_U16 (two operand): min(0, 5) should return 0."""
instructions = [
s_mov_b32(s[0], 0),
s_mov_b32(s[1], 5),
v_mov_b32_e32(v[0], s[0]),
v_min_u16(v[1], v[0], s[1]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][1] & 0xFFFF, 0)
if __name__ == '__main__':
unittest.main()
+23 -72
View File
@@ -23,12 +23,14 @@ class TestPackInstructions(unittest.TestCase):
def test_v_pack_b32_f16_opsel_hi_hi(self):
"""V_PACK_B32_F16 with opsel to read high halves."""
inst = v_pack_b32_f16(v[2], v[0], v[1])
inst._values['opsel'] = 0b0011
instructions = [
s_mov_b32(s[0], 0x40003c00), # hi=2.0, lo=1.0
s_mov_b32(s[1], 0x44004200), # hi=4.0, lo=3.0
v_mov_b32_e32(v[0], s[0]),
v_mov_b32_e32(v[1], s[1]),
v_pack_b32_f16(v[2], v[0], v[1], opsel=0b0011),
inst,
]
st = run_program(instructions, n_lanes=1)
result = st.vgpr[0][2]
@@ -81,12 +83,14 @@ class TestPackMore(unittest.TestCase):
def test_v_pack_b32_f16_opsel_lo_hi(self):
"""V_PACK_B32_F16 with opsel=0b0010 to read lo from src0, hi from src1."""
inst = v_pack_b32_f16(v[2], v[0], v[1])
inst._values['opsel'] = 0b0010
instructions = [
s_mov_b32(s[0], 0x40003c00),
s_mov_b32(s[1], 0x44004200),
v_mov_b32_e32(v[0], s[0]),
v_mov_b32_e32(v[1], s[1]),
v_pack_b32_f16(v[2], v[0], v[1], opsel=0b0010),
inst,
]
st = run_program(instructions, n_lanes=1)
result = st.vgpr[0][2]
@@ -94,12 +98,14 @@ class TestPackMore(unittest.TestCase):
def test_v_pack_b32_f16_opsel_hi_lo(self):
"""V_PACK_B32_F16 with opsel=0b0001 to read hi from src0, lo from src1."""
inst = v_pack_b32_f16(v[2], v[0], v[1])
inst._values['opsel'] = 0b0001
instructions = [
s_mov_b32(s[0], 0x40003c00),
s_mov_b32(s[1], 0x44004200),
v_mov_b32_e32(v[0], s[0]),
v_mov_b32_e32(v[1], s[1]),
v_pack_b32_f16(v[2], v[0], v[1], opsel=0b0001),
inst,
]
st = run_program(instructions, n_lanes=1)
result = st.vgpr[0][2]
@@ -149,7 +155,7 @@ class TestFmaMix(unittest.TestCase):
def test_v_fma_mix_f32_src2_f16_lo(self):
"""V_FMA_MIX_F32 with src2 as f16 from lo bits."""
from extra.assembly.amd.test.hw.helpers import f32_to_f16
from extra.assembly.amd.pcode import f32_to_f16
f16_2 = f32_to_f16(2.0)
instructions = [
s_mov_b32(s[0], f2i(1.0)),
@@ -166,7 +172,7 @@ class TestFmaMix(unittest.TestCase):
def test_v_fma_mix_f32_src2_f16_hi(self):
"""V_FMA_MIX_F32 with src2 as f16 from hi bits."""
from extra.assembly.amd.test.hw.helpers import f32_to_f16
from extra.assembly.amd.pcode import f32_to_f16
f16_2 = f32_to_f16(2.0)
val = (f16_2 << 16) | 0
instructions = [
@@ -197,64 +203,9 @@ class TestFmaMix(unittest.TestCase):
result = i2f(st.vgpr[0][3])
self.assertAlmostEqual(result, 7.0, places=5)
def test_v_fma_mix_f32_with_abs_f16_src2_lo(self):
"""V_FMA_MIX_F32 with abs modifier on f16 src2 (lo half). Regression test for sin(1.0) bug."""
from extra.assembly.amd.test.hw.helpers import f32_to_f16
f16_neg1 = f32_to_f16(-1.0) # 0xbc00
instructions = [
s_mov_b32(s[0], f2i(0.0)), # src0 = 0.0 (f32)
v_mov_b32_e32(v[0], s[0]),
s_mov_b32(s[1], f2i(1.0)), # src1 = 1.0 (f32)
v_mov_b32_e32(v[1], s[1]),
s_mov_b32(s[2], f16_neg1), # src2 = -1.0 (f16 in lo)
v_mov_b32_e32(v[2], s[2]),
# 0*1 + abs(-1.0) = 1.0; neg_hi=4 means abs on src2, opsel_hi2=1 means src2 is f16
VOP3P(VOP3POp.V_FMA_MIX_F32, vdst=v[3], src0=v[0], src1=v[1], src2=v[2], opsel=0, opsel_hi=0, opsel_hi2=1, neg_hi=4),
]
st = run_program(instructions, n_lanes=1)
result = i2f(st.vgpr[0][3])
self.assertAlmostEqual(result, 1.0, places=5)
def test_v_fma_mix_f32_with_neg_f16_src2_lo(self):
"""V_FMA_MIX_F32 with neg modifier on f16 src2 (lo half)."""
from extra.assembly.amd.test.hw.helpers import f32_to_f16
f16_1 = f32_to_f16(1.0) # 0x3c00
instructions = [
s_mov_b32(s[0], f2i(0.0)), # src0 = 0.0 (f32)
v_mov_b32_e32(v[0], s[0]),
s_mov_b32(s[1], f2i(1.0)), # src1 = 1.0 (f32)
v_mov_b32_e32(v[1], s[1]),
s_mov_b32(s[2], f16_1), # src2 = 1.0 (f16 in lo)
v_mov_b32_e32(v[2], s[2]),
# 0*1 + neg(1.0) = -1.0; neg=4 means neg on src2, opsel_hi2=1 means src2 is f16
VOP3P(VOP3POp.V_FMA_MIX_F32, vdst=v[3], src0=v[0], src1=v[1], src2=v[2], opsel=0, opsel_hi=0, opsel_hi2=1, neg=4),
]
st = run_program(instructions, n_lanes=1)
result = i2f(st.vgpr[0][3])
self.assertAlmostEqual(result, -1.0, places=5)
def test_v_fma_mix_f32_with_abs_f16_src2_hi(self):
"""V_FMA_MIX_F32 with abs modifier on f16 src2 (hi half)."""
from extra.assembly.amd.test.hw.helpers import f32_to_f16
f16_neg1 = f32_to_f16(-1.0) # 0xbc00
val = (f16_neg1 << 16) | 0 # -1.0 in hi, 0 in lo
instructions = [
s_mov_b32(s[0], f2i(0.0)),
v_mov_b32_e32(v[0], s[0]),
s_mov_b32(s[1], f2i(1.0)),
v_mov_b32_e32(v[1], s[1]),
s_mov_b32(s[2], val),
v_mov_b32_e32(v[2], s[2]),
# opsel=4 selects hi half of src2; neg_hi=4 means abs on src2
VOP3P(VOP3POp.V_FMA_MIX_F32, vdst=v[3], src0=v[0], src1=v[1], src2=v[2], opsel=4, opsel_hi=0, opsel_hi2=1, neg_hi=4),
]
st = run_program(instructions, n_lanes=1)
result = i2f(st.vgpr[0][3])
self.assertAlmostEqual(result, 1.0, places=5)
def test_v_fma_mixlo_f16(self):
"""V_FMA_MIXLO_F16 writes to low 16 bits of destination."""
from extra.assembly.amd.test.hw.helpers import _f16
from extra.assembly.amd.pcode import _f16
instructions = [
s_mov_b32(s[0], f2i(2.0)),
v_mov_b32_e32(v[0], s[0]),
@@ -274,7 +225,7 @@ class TestFmaMix(unittest.TestCase):
def test_v_fma_mixlo_f16_all_f32_sources(self):
"""V_FMA_MIXLO_F16 with all f32 sources."""
from extra.assembly.amd.test.hw.helpers import _f16
from extra.assembly.amd.pcode import _f16
instructions = [
s_mov_b32(s[0], f2i(1.0)),
v_mov_b32_e32(v[0], s[0]),
@@ -292,7 +243,7 @@ class TestFmaMix(unittest.TestCase):
def test_v_fma_mixlo_f16_sin_case(self):
"""V_FMA_MIXLO_F16 case from sin kernel."""
from extra.assembly.amd.test.hw.helpers import _f16
from extra.assembly.amd.pcode import _f16
instructions = [
s_mov_b32(s[0], 0x3f800000), # f32 1.0
v_mov_b32_e32(v[3], s[0]),
@@ -314,7 +265,7 @@ class TestVOP3P(unittest.TestCase):
def test_v_pk_add_f16_basic(self):
"""V_PK_ADD_F16 adds two packed f16 values."""
from extra.assembly.amd.test.hw.helpers import _f16
from extra.assembly.amd.pcode import _f16
instructions = [
s_mov_b32(s[0], 0x40003c00), # hi=2.0, lo=1.0
s_mov_b32(s[1], 0x44004200), # hi=4.0, lo=3.0
@@ -331,7 +282,7 @@ class TestVOP3P(unittest.TestCase):
def test_v_pk_mul_f16_basic(self):
"""V_PK_MUL_F16 multiplies two packed f16 values."""
from extra.assembly.amd.test.hw.helpers import _f16
from extra.assembly.amd.pcode import _f16
instructions = [
s_mov_b32(s[0], 0x42004000), # hi=3.0, lo=2.0
s_mov_b32(s[1], 0x45004400), # hi=5.0, lo=4.0
@@ -348,7 +299,7 @@ class TestVOP3P(unittest.TestCase):
def test_v_pk_fma_f16_basic(self):
"""V_PK_FMA_F16: D = A * B + C for packed f16."""
from extra.assembly.amd.test.hw.helpers import _f16
from extra.assembly.amd.pcode import _f16
instructions = [
s_mov_b32(s[0], 0x42004000), # A: hi=3.0, lo=2.0
s_mov_b32(s[1], 0x45004400), # B: hi=5.0, lo=4.0
@@ -370,7 +321,7 @@ class TestVOP3P(unittest.TestCase):
Inline constants for VOP3P are f16 values in the low 16 bits only.
hi half of inline constant is 0, so hi result = v0.hi + 0 = 1.0.
"""
from extra.assembly.amd.test.hw.helpers import _f16
from extra.assembly.amd.pcode import _f16
instructions = [
s_mov_b32(s[0], 0x3c003c00), # packed f16: hi=1.0, lo=1.0
v_mov_b32_e32(v[0], s[0]),
@@ -388,7 +339,7 @@ class TestVOP3P(unittest.TestCase):
"""V_PK_MUL_F16 with inline constant POS_TWO (2.0).
Inline constant has value only in low 16 bits, hi is 0.
"""
from extra.assembly.amd.test.hw.helpers import _f16
from extra.assembly.amd.pcode import _f16
# v0 = packed (3.0, 4.0), multiply by POS_TWO
# lo = 3.0 * 2.0 = 6.0, hi = 4.0 * 0.0 = 0.0 (inline const hi is 0)
instructions = [
@@ -415,7 +366,7 @@ class TestWMMA(unittest.TestCase):
instructions.append(v_mov_b32_e32(v[i], s[0]))
for i in range(8):
instructions.append(v_mov_b32_e32(v[i], 0))
instructions.append(v_wmma_f32_16x16x16_f16(v[0:7], v[16:23], v[24:31], v[0:7]))
instructions.append(v_wmma_f32_16x16x16_f16(v[0], v[16], v[24], v[0]))
st = run_program(instructions, n_lanes=32)
expected = f2i(16.0)
for lane in range(32):
@@ -432,7 +383,7 @@ class TestWMMA(unittest.TestCase):
instructions.append(v_mov_b32_e32(v[i], s[0]))
for i in range(8):
instructions.append(v_mov_b32_e32(v[i], s[1]))
instructions.append(v_wmma_f32_16x16x16_f16(v[0:7], v[16:23], v[24:31], v[0:7]))
instructions.append(v_wmma_f32_16x16x16_f16(v[0], v[16], v[24], v[0]))
st = run_program(instructions, n_lanes=32)
expected = f2i(21.0) # 16 + 5
for lane in range(32):
@@ -553,7 +504,7 @@ class TestPackedMixedSigns(unittest.TestCase):
def test_pk_add_f16_mixed_signs(self):
"""V_PK_ADD_F16 with mixed positive/negative values."""
from extra.assembly.amd.test.hw.helpers import _f16
from extra.assembly.amd.pcode import _f16
instructions = [
s_mov_b32(s[0], 0xc0003c00), # packed: hi=-2.0, lo=1.0
s_mov_b32(s[1], 0x3c003c00), # packed: hi=1.0, lo=1.0
@@ -570,7 +521,7 @@ class TestPackedMixedSigns(unittest.TestCase):
def test_pk_mul_f16_zero(self):
"""V_PK_MUL_F16 with zero."""
from extra.assembly.amd.test.hw.helpers import _f16
from extra.assembly.amd.pcode import _f16
instructions = [
s_mov_b32(s[0], 0x40004000), # packed: 2.0, 2.0
s_mov_b32(s[1], 0x00000000), # packed: 0.0, 0.0
+20 -365
View File
@@ -271,7 +271,7 @@ class TestCmpClassF16(unittest.TestCase):
instructions = [
v_mov_b32_e32(v[0], 0), # f16 +0.0
s_mov_b32(s[0], 0x1f8), # class mask
v_cmp_class_f16_e64(VCC_LO, v[0], s[0]),
VOP3(VOP3Op.V_CMP_CLASS_F16, vdst=RawImm(VCC), src0=v[0], src1=s[0]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vcc & 1, 1, "VCC should be 1 for +zero with VOP3 encoding")
@@ -282,7 +282,7 @@ class TestCmpClassF16(unittest.TestCase):
s_mov_b32(s[0], 0x3c00), # f16 +1.0
v_mov_b32_e32(v[0], s[0]),
s_mov_b32(s[1], 0x1f8), # class mask
v_cmp_class_f16_e64(VCC_LO, v[0], s[1]),
VOP3(VOP3Op.V_CMP_CLASS_F16, vdst=RawImm(VCC), src0=v[0], src1=s[1]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vcc & 1, 1, "VCC should be 1 for +1.0 (normal) with mask 0x1f8")
@@ -293,7 +293,7 @@ class TestCmpClassF16(unittest.TestCase):
s_mov_b32(s[0], 0x7e00), # f16 quiet NaN
v_mov_b32_e32(v[0], s[0]),
s_mov_b32(s[1], 0x1f8), # class mask
v_cmp_class_f16_e64(VCC_LO, v[0], s[1]),
VOP3(VOP3Op.V_CMP_CLASS_F16, vdst=RawImm(VCC), src0=v[0], src1=s[1]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vcc & 1, 0, "VCC should be 0 for NaN with mask 0x1f8 (no NaN bits)")
@@ -304,7 +304,7 @@ class TestCmpClassF16(unittest.TestCase):
s_mov_b32(s[0], 0x7c00), # f16 +inf
v_mov_b32_e32(v[0], s[0]),
s_mov_b32(s[1], 0x1f8), # class mask
v_cmp_class_f16_e64(VCC_LO, v[0], s[1]),
VOP3(VOP3Op.V_CMP_CLASS_F16, vdst=RawImm(VCC), src0=v[0], src1=s[1]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vcc & 1, 0, "VCC should be 0 for +inf with mask 0x1f8 (no inf bits)")
@@ -324,29 +324,6 @@ class TestCmpInt(unittest.TestCase):
st = run_program(instructions, n_lanes=4)
self.assertEqual(st.vcc & 0xf, 0xf, "All lanes should match")
def test_v_cmp_ne_u32_with_zero(self):
"""V_CMP_NE_U32: compare with zero, used for int->bool cast."""
instructions = [
v_mov_b32_e32(v[1], 0),
v_cmp_eq_u32_e32(1, v[255]), # vcc = (lane == 1)
v_cndmask_b32_e64(v[1], v[1], 1, VCC_LO), # v1[lane1] = 1
v_cmp_ne_u32_e32(0, v[1]), # vcc = (0 != v1)
v_cndmask_b32_e64(v[0], 0, 1, VCC_LO), # v0 = vcc ? 1 : 0
]
st = run_program(instructions, n_lanes=2)
self.assertEqual(st.vgpr[0][0], 0, "lane 0: 0 != 0 should be false")
self.assertEqual(st.vgpr[1][0], 1, "lane 1: 0 != 1 should be true")
self.assertEqual(st.vcc & 0x3, 0x2, "VCC should be 0b10")
def test_v_cmp_ne_u32_all_nonzero(self):
"""V_CMP_NE_U32: all lanes have nonzero values."""
instructions = [
v_mov_b32_e32(v[1], 5),
v_cmp_ne_u32_e32(0, v[1]),
]
st = run_program(instructions, n_lanes=4)
self.assertEqual(st.vcc & 0xf, 0xf, "All lanes should be != 0")
def test_cmp_eq_u16_opsel_lo_lo(self):
"""V_CMP_EQ_U16 comparing lo halves."""
instructions = [
@@ -360,15 +337,21 @@ class TestCmpInt(unittest.TestCase):
self.assertEqual(st.vcc & 1, 1, "Lo halves should be equal")
def test_cmp_eq_u16_opsel_hi_hi(self):
"""V_CMP_EQ_U16 comparing hi halves with VOP3 opsel."""
"""V_CMP_EQ_U16 comparing hi halves with VOP3 opsel.
VOPC doesn't have opsel, so we use VOP3 form for hi-half comparisons.
VOP3 compares write result to SGPR via vdst field.
"""
instructions = [
s_mov_b32(s[2], 0x00051234), # hi=5, lo=0x1234
v_mov_b32_e32(v[0], s[2]),
s_mov_b32(s[2], 0x0005ABCD), # hi=5, lo=0xABCD
v_mov_b32_e32(v[1], s[2]),
v_cmp_eq_u16_e64(vdst=s[0], src0=v[0], src1=v[1], opsel=3),
# opsel=3 means compare hi halves, vdst=v[0] actually writes to s[0]
VOP3(VOP3Op.V_CMP_EQ_U16, vdst=v[0], src0=v[0], src1=v[1], opsel=3),
]
st = run_program(instructions, n_lanes=1)
# Result is in sgpr[0], not vcc
self.assertEqual(st.sgpr[0] & 1, 1, "Hi halves should be equal: 5==5")
def test_cmp_eq_u16_opsel_hi_hi_equal(self):
@@ -378,7 +361,7 @@ class TestCmpInt(unittest.TestCase):
v_mov_b32_e32(v[0], s[2]),
s_mov_b32(s[2], 0x12340009), # lo=9, hi=0x1234
v_mov_b32_e32(v[1], s[2]),
v_cmp_eq_u16_e64(vdst=s[0], src0=v[0], src1=v[1], opsel=3),
VOP3(VOP3Op.V_CMP_EQ_U16, vdst=v[0], src0=v[0], src1=v[1], opsel=3),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.sgpr[0] & 1, 1, "hi==hi should be true: 0x1234==0x1234")
@@ -390,7 +373,7 @@ class TestCmpInt(unittest.TestCase):
v_mov_b32_e32(v[0], s[2]),
s_mov_b32(s[2], 0x12340005), # lo=5, hi=0x1234
v_mov_b32_e32(v[1], s[2]),
v_cmp_gt_u16_e64(vdst=s[0], src0=v[0], src1=v[1], opsel=3),
VOP3(VOP3Op.V_CMP_GT_U16, vdst=v[0], src0=v[0], src1=v[1], opsel=3),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.sgpr[0] & 1, 1, "hi>hi should be true: 0x9999>0x1234")
@@ -406,9 +389,11 @@ class TestCmpFloat(unittest.TestCase):
v_mov_b32_e32(v[0], s[2]),
s_mov_b32(s[2], 0x40000000), # hi=2.0 (f16), lo=0
v_mov_b32_e32(v[1], s[2]),
v_cmp_lt_f16_e64(vdst=s[0], src0=v[0], src1=v[1], opsel=3),
# opsel=3 means read hi halves for both src0 and src1
VOP3(VOP3Op.V_CMP_LT_F16, vdst=v[0], src0=v[0], src1=v[1], opsel=3),
]
st = run_program(instructions, n_lanes=1)
# Result is in sgpr[0]
self.assertEqual(st.sgpr[0] & 1, 1, "1.0 < 2.0 should be true")
def test_v_cmp_gt_f16_vsrc1_hi(self):
@@ -418,9 +403,11 @@ class TestCmpFloat(unittest.TestCase):
v_mov_b32_e32(v[0], s[2]),
s_mov_b32(s[2], 0x3c000000), # hi=1.0 (f16), lo=0
v_mov_b32_e32(v[1], s[2]),
v_cmp_gt_f16_e64(vdst=s[0], src0=v[0], src1=v[1], opsel=3),
# opsel=3 means read hi halves for both src0 and src1
VOP3(VOP3Op.V_CMP_GT_F16, vdst=v[0], src0=v[0], src1=v[1], opsel=3),
]
st = run_program(instructions, n_lanes=1)
# Result is in sgpr[0]
self.assertEqual(st.sgpr[0] & 1, 1, "2.0 > 1.0 should be true")
def test_v_cmp_eq_f16_vsrc1_hi_equal(self):
@@ -471,242 +458,6 @@ class TestCmpFloat(unittest.TestCase):
self.assertEqual((st.vcc >> 1) & 1, 0, "Lane 1: expected vcc=0 (2.0 < 1.0)")
class TestVOP3VOPCModifiers(unittest.TestCase):
"""Tests for VOP3 VOPC with abs/neg modifiers."""
def test_v_cmp_ge_f32_abs_both(self):
"""v_cmp_ge_f32 with abs on both sources: abs(0.0) >= abs(-1.0) = false.
Regression test: int16 mod operation uses v_cmp_ge_f32 with abs modifiers.
"""
instructions = [
v_mov_b32_e32(v[0], 0.0),
v_mov_b32_e32(v[1], -1.0),
# abs=0b11 means abs(src0) and abs(src1)
v_cmp_ge_f32_e64(VCC_LO, v[0], v[1], abs=0b11),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vcc & 1, 0, "abs(0.0) >= abs(-1.0) should be false")
def test_v_cmp_ge_f32_abs_negative_divisor(self):
"""v_cmp_ge_f32 with abs: remainder check for negative divisor.
Tests the exact comparison used in int16 mod: abs(rem_f) >= abs(div_f).
For 1 % -1: rem_f = 0.0, div_f = -1.0, so abs(0.0) >= abs(-1.0) = false.
"""
instructions = [
v_mov_b32_e32(v[0], 0.0), # remainder as float
v_mov_b32_e32(v[1], -1.0), # divisor as float
v_cmp_ge_f32_e64(VCC_LO, v[0], v[1], abs=0b11),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vcc & 1, 0, "abs(0.0) >= abs(-1.0) should be false")
def test_v_cmp_ge_f32_abs_small_remainder(self):
"""v_cmp_ge_f32 with abs: abs(-0.5) >= abs(-3.0) = false."""
instructions = [
v_mov_b32_e32(v[0], -0.5),
v_mov_b32_e32(v[1], -3.0),
v_cmp_ge_f32_e64(VCC_LO, v[0], v[1], abs=0b11),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vcc & 1, 0, "abs(-0.5) >= abs(-3.0) should be false")
def test_v_cmp_ge_f32_abs_equal(self):
"""v_cmp_ge_f32 with abs: abs(-1.0) >= abs(1.0) = true."""
instructions = [
v_mov_b32_e32(v[0], -1.0),
v_mov_b32_e32(v[1], 1.0),
v_cmp_ge_f32_e64(VCC_LO, v[0], v[1], abs=0b11),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vcc & 1, 1, "abs(-1.0) >= abs(1.0) should be true")
class TestVOP3VOPC64Bit(unittest.TestCase):
"""Tests for VOP3 VOPC with 64-bit operands."""
def test_v_cmp_lt_f64_basic(self):
"""v_cmp_lt_f64: 0.0 < 1.0 = true."""
zero_f64 = f2i64(0.0)
one_f64 = f2i64(1.0)
instructions = [
s_mov_b32(s[0], zero_f64 & 0xffffffff),
s_mov_b32(s[1], zero_f64 >> 32),
s_mov_b32(s[2], one_f64 & 0xffffffff),
s_mov_b32(s[3], one_f64 >> 32),
v_cmp_lt_f64_e64(VCC_LO, s[0:1], s[2:3]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vcc & 1, 1, "0.0 < 1.0 should be true")
def test_v_cmp_lt_f64_negative(self):
"""v_cmp_lt_f64: -1.0 < 0.0 = true."""
neg_one_f64 = f2i64(-1.0)
zero_f64 = f2i64(0.0)
instructions = [
s_mov_b32(s[0], neg_one_f64 & 0xffffffff),
s_mov_b32(s[1], neg_one_f64 >> 32),
s_mov_b32(s[2], zero_f64 & 0xffffffff),
s_mov_b32(s[3], zero_f64 >> 32),
v_cmp_lt_f64_e64(VCC_LO, s[0:1], s[2:3]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vcc & 1, 1, "-1.0 < 0.0 should be true")
def test_v_cmp_lt_i64_signed(self):
"""v_cmp_lt_i64: 0 < -1 (signed) = false."""
instructions = [
s_mov_b32(s[0], 0),
s_mov_b32(s[1], 0), # s[0:1] = 0
s_mov_b32(s[2], 0xffffffff),
s_mov_b32(s[3], 0xffffffff), # s[2:3] = -1
v_cmp_lt_i64_e64(VCC_LO, s[0:1], s[2:3]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vcc & 1, 0, "0 < -1 (signed) should be false")
def test_v_cmp_lt_u64_unsigned(self):
"""v_cmp_lt_u64: 0 < 0xFFFFFFFFFFFFFFFF (unsigned) = true."""
instructions = [
s_mov_b32(s[0], 0),
s_mov_b32(s[1], 0), # s[0:1] = 0
s_mov_b32(s[2], 0xffffffff),
s_mov_b32(s[3], 0xffffffff), # s[2:3] = max uint64
v_cmp_lt_u64_e64(VCC_LO, s[0:1], s[2:3]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vcc & 1, 1, "0 < max_uint64 should be true")
class TestVOPCF64(unittest.TestCase):
"""Tests for VOPC (E32 encoding) with 64-bit float operands. Regression test for f64 compare bug."""
def test_v_cmp_lt_f64_e32_true(self):
"""v_cmp_lt_f64_e32: 2.0 < 3.0 = true."""
lo0, hi0 = f2i64(2.0) & 0xffffffff, f2i64(2.0) >> 32
lo1, hi1 = f2i64(3.0) & 0xffffffff, f2i64(3.0) >> 32
instructions = [
s_mov_b32(s[0], lo0), s_mov_b32(s[1], hi0),
s_mov_b32(s[2], lo1), s_mov_b32(s[3], hi1),
v_mov_b32_e32(v[0], s[0]), v_mov_b32_e32(v[1], s[1]),
v_mov_b32_e32(v[2], s[2]), v_mov_b32_e32(v[3], s[3]),
v_cmp_lt_f64_e32(v[0:1], v[2:3]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vcc & 1, 1, "2.0 < 3.0 should be true")
def test_v_cmp_lt_f64_e32_false(self):
"""v_cmp_lt_f64_e32: 3.0 < 2.0 = false."""
lo0, hi0 = f2i64(3.0) & 0xffffffff, f2i64(3.0) >> 32
lo1, hi1 = f2i64(2.0) & 0xffffffff, f2i64(2.0) >> 32
instructions = [
s_mov_b32(s[0], lo0), s_mov_b32(s[1], hi0),
s_mov_b32(s[2], lo1), s_mov_b32(s[3], hi1),
v_mov_b32_e32(v[0], s[0]), v_mov_b32_e32(v[1], s[1]),
v_mov_b32_e32(v[2], s[2]), v_mov_b32_e32(v[3], s[3]),
v_cmp_lt_f64_e32(v[0:1], v[2:3]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vcc & 1, 0, "3.0 < 2.0 should be false")
def test_v_cmp_nlt_f64_e32_true(self):
"""v_cmp_nlt_f64_e32: !(3.0 < 2.0) = true."""
lo0, hi0 = f2i64(3.0) & 0xffffffff, f2i64(3.0) >> 32
lo1, hi1 = f2i64(2.0) & 0xffffffff, f2i64(2.0) >> 32
instructions = [
s_mov_b32(s[0], lo0), s_mov_b32(s[1], hi0),
s_mov_b32(s[2], lo1), s_mov_b32(s[3], hi1),
v_mov_b32_e32(v[0], s[0]), v_mov_b32_e32(v[1], s[1]),
v_mov_b32_e32(v[2], s[2]), v_mov_b32_e32(v[3], s[3]),
v_cmp_nlt_f64_e32(v[0:1], v[2:3]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vcc & 1, 1, "!(3.0 < 2.0) should be true")
def test_v_cmp_nlt_f64_e32_false(self):
"""v_cmp_nlt_f64_e32: !(2.0 < 3.0) = false."""
lo0, hi0 = f2i64(2.0) & 0xffffffff, f2i64(2.0) >> 32
lo1, hi1 = f2i64(3.0) & 0xffffffff, f2i64(3.0) >> 32
instructions = [
s_mov_b32(s[0], lo0), s_mov_b32(s[1], hi0),
s_mov_b32(s[2], lo1), s_mov_b32(s[3], hi1),
v_mov_b32_e32(v[0], s[0]), v_mov_b32_e32(v[1], s[1]),
v_mov_b32_e32(v[2], s[2]), v_mov_b32_e32(v[3], s[3]),
v_cmp_nlt_f64_e32(v[0:1], v[2:3]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vcc & 1, 0, "!(2.0 < 3.0) should be false")
class TestCmpxExec(unittest.TestCase):
"""Tests for V_CMPX instructions that modify EXEC mask."""
def test_v_cmpx_ngt_f32_e64_all_true(self):
"""V_CMPX_NGT_F32_E64: all lanes pass (literal <= all values)."""
# 131072.0 = 0x48000000
# All values > 131072, so !(131072 > val) = true for all
instructions = [
s_mov_b32(EXEC_LO, 0x7), # 3 lanes active
v_mov_b32_e32(v[0], f2i(200000.0)), # lane 0
v_cmp_eq_u32_e32(1, v[255]),
v_cndmask_b32_e64(v[1], v[0], f2i(300000.0), VCC_LO), # lane 1
v_cmp_eq_u32_e32(2, v[255]),
v_cndmask_b32_e64(v[1], v[1], f2i(400000.0), VCC_LO), # lane 2
# Now v[1] has: lane0=200000, lane1=300000, lane2=400000
# Compare: !(131072.0 > v[1]) i.e., 131072.0 <= v[1]
v_cmpx_ngt_f32_e64(EXEC_LO, f2i(131072.0), v[1]),
]
st = run_program(instructions, n_lanes=3)
# All values > 131072, so all lanes should remain active
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0x7, 0x7, "All 3 lanes should remain active")
def test_v_cmpx_ngt_f32_e64_some_false(self):
"""V_CMPX_NGT_F32_E64: some lanes fail (literal > some values)."""
instructions = [
s_mov_b32(EXEC_LO, 0x7), # 3 lanes active
v_mov_b32_e32(v[0], f2i(100000.0)), # lane 0: 131072 > 100000 = true, so !(true) = false
v_cmp_eq_u32_e32(1, v[255]),
v_cndmask_b32_e64(v[1], v[0], f2i(200000.0), VCC_LO), # lane 1: 131072 > 200000 = false, so !(false) = true
v_cmp_eq_u32_e32(2, v[255]),
v_cndmask_b32_e64(v[1], v[1], f2i(150000.0), VCC_LO), # lane 2: 131072 > 150000 = false, so !(false) = true
v_cmpx_ngt_f32_e64(EXEC_LO, f2i(131072.0), v[1]),
]
st = run_program(instructions, n_lanes=3)
# lane 0: fail (100000 < 131072), lanes 1,2: pass
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0x7, 0x6, "Lanes 1,2 should be active, lane 0 inactive")
def test_v_cmpx_ngt_f32_e64_all_false(self):
"""V_CMPX_NGT_F32_E64: all lanes fail (literal > all values)."""
instructions = [
s_mov_b32(EXEC_LO, 0x7), # 3 lanes active
v_mov_b32_e32(v[0], f2i(100.0)), # all lanes have 100.0
# 131072 > 100 = true, so !(true) = false for all
v_cmpx_ngt_f32_e64(EXEC_LO, f2i(131072.0), v[0]),
]
st = run_program(instructions, n_lanes=3)
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0x7, 0x0, "All lanes should be inactive")
def test_v_cmpx_ngt_f32_e64_large_values(self):
"""V_CMPX_NGT_F32_E64: test with values that trigger Payne-Hanek in sin().
This is a regression test for the sin(859240.0) bug.
Values 859240, 1000000, 100594688 should all pass !(131072 > val).
"""
instructions = [
s_mov_b32(EXEC_LO, 0x7), # 3 lanes active
v_mov_b32_e32(v[0], f2i(859240.0)), # lane 0
v_cmp_eq_u32_e32(1, v[255]),
v_cndmask_b32_e64(v[1], v[0], f2i(1000000.0), VCC_LO), # lane 1
v_cmp_eq_u32_e32(2, v[255]),
v_cndmask_b32_e64(v[1], v[1], f2i(100594688.0), VCC_LO), # lane 2
v_cmpx_ngt_f32_e64(EXEC_LO, f2i(131072.0), v[1]),
]
st = run_program(instructions, n_lanes=3)
# All values > 131072, so !(131072 > val) = true for all
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0x7, 0x7, "All 3 lanes should remain active")
class TestVCCBehavior(unittest.TestCase):
"""Tests for VCC condition code behavior."""
@@ -731,101 +482,5 @@ class TestVCCBehavior(unittest.TestCase):
self.assertEqual(st.vcc >> 16, 0x0000, "Lanes 16-31 should be false")
class TestCmpxPartialWavefront(unittest.TestCase):
"""Tests for V_CMPX with partial wavefronts (fewer than 32 active lanes).
Regression tests for bug where v_cmpx incorrectly set EXEC bits for inactive
lanes when the wavefront had fewer than 32 lanes. This caused garbage data
from uninitialized lanes to corrupt memory writes.
"""
def test_v_cmpx_eq_u32_partial_wave_3_lanes(self):
"""V_CMPX_EQ_U32 with 3 active lanes should only affect those 3 lanes.
With n_lanes=3, initial EXEC=0x7. After v_cmpx comparing lane_id == 1,
only lane 1 should pass, so EXEC should become 0x2 (not have bits 3-31 set).
"""
instructions = [
v_cmpx_eq_u32_e32(1, v[255]), # EXEC = lanes where lane_id == 1
]
st = run_program(instructions, n_lanes=3)
# Only lane 1 should be active (bit 1 set)
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0xFFFFFFFF, 0x2,
"Only lane 1 should be active after v_cmpx_eq_u32 with 3 lanes")
def test_v_cmpx_eq_u32_partial_wave_5_lanes(self):
"""V_CMPX_EQ_U32 with 5 active lanes."""
instructions = [
v_cmpx_eq_u32_e32(3, v[255]), # EXEC = lanes where lane_id == 3
]
st = run_program(instructions, n_lanes=5)
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0xFFFFFFFF, 0x8,
"Only lane 3 should be active after v_cmpx_eq_u32 with 5 lanes")
def test_v_cmpx_lt_u32_partial_wave(self):
"""V_CMPX_LT_U32 with partial wavefront."""
# VOPC: src0 < vsrc1, so we need v_cmpx_gt_u32 to get lane_id < 2
instructions = [
v_cmpx_gt_u32_e32(2, v[255]), # EXEC = lanes where 2 > lane_id (i.e., lane_id < 2)
]
st = run_program(instructions, n_lanes=4)
# Lanes 0,1 should be active (bits 0,1 set = 0x3)
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0xFFFFFFFF, 0x3,
"Only lanes 0,1 should be active after v_cmpx_gt_u32(2, lane_id) with 4 lanes")
def test_v_cmpx_ge_u32_partial_wave(self):
"""V_CMPX_GE_U32 with partial wavefront."""
# VOPC: src0 >= vsrc1, so v_cmpx_le_u32(1, lane_id) gives lane_id >= 2? No.
# v_cmpx_le_u32(src0, vsrc1) = src0 <= vsrc1 = 1 <= lane_id
instructions = [
v_cmpx_le_u32_e32(2, v[255]), # EXEC = lanes where 2 <= lane_id (i.e., lane_id >= 2)
]
st = run_program(instructions, n_lanes=4)
# Lanes 2,3 should be active (bits 2,3 set = 0xC)
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0xFFFFFFFF, 0xC,
"Only lanes 2,3 should be active after v_cmpx_le_u32(2, lane_id) with 4 lanes")
def test_v_cmpx_ne_u32_partial_wave_all_pass(self):
"""V_CMPX_NE_U32 where all active lanes pass."""
instructions = [
v_cmpx_ne_u32_e32(99, v[255]), # EXEC = lanes where lane_id != 99
]
st = run_program(instructions, n_lanes=3)
# All 3 lanes should remain active (bits 0,1,2 set = 0x7)
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0xFFFFFFFF, 0x7,
"All 3 lanes should remain active when all pass")
def test_v_cmpx_eq_u32_partial_wave_none_pass(self):
"""V_CMPX_EQ_U32 where no active lanes pass."""
instructions = [
v_cmpx_eq_u32_e32(99, v[255]), # EXEC = lanes where lane_id == 99
]
st = run_program(instructions, n_lanes=3)
# No lanes should be active
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0xFFFFFFFF, 0x0,
"No lanes should be active when none pass")
def test_v_cmpx_f32_partial_wave(self):
"""V_CMPX_GT_F32 with partial wavefront - float comparison."""
instructions = [
v_cvt_f32_u32_e32(v[0], v[255]), # v[0] = float(lane_id)
v_mov_b32_e32(v[1], f2i(0.5)), # v[1] = 0.5
v_cmpx_gt_f32_e32(v[0], v[1]), # EXEC = lanes where v[0] > 0.5
]
st = run_program(instructions, n_lanes=4)
# Lanes 1,2,3 have values > 0.5, lane 0 has 0.0
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0xFFFFFFFF, 0xE,
"Lanes 1,2,3 should be active (float > 0.5)")
def test_v_cmpx_e64_partial_wave(self):
"""V_CMPX_EQ_U32_E64 (VOP3 encoding) with partial wavefront."""
instructions = [
v_cmpx_eq_u32_e64(EXEC_LO, v[255], 2), # EXEC = lanes where lane_id == 2
]
st = run_program(instructions, n_lanes=4)
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0xFFFFFFFF, 0x4,
"Only lane 2 should be active after v_cmpx_eq_u32_e64")
if __name__ == '__main__':
unittest.main()
-161
View File
@@ -1,161 +0,0 @@
"""Tests for VOPD instructions - dual-issue vector operations.
VOPD executes two operations simultaneously. Key behavior:
- Both ops read their sources BEFORE either writes (dual-issue semantics)
- This means if X writes to a register that Y reads, Y sees the OLD value
- Op X can use ops 0-15 (FMAC, MUL, ADD, MOV, etc.)
- Op Y can use ops 0-18 (includes ADD_NC_U32, LSHLREV, AND)
"""
import unittest
from extra.assembly.amd.test.hw.helpers import run_program, run_program_emu, run_program_hw, compare_wave_states, \
v, s, v_mov_b32_e32, s_mov_b32
from extra.assembly.amd.autogen.rdna3.ins import VOPD, VOPD_LIT, VOPDOp
class TestVOPDBasic(unittest.TestCase):
"""Basic VOPD functionality tests."""
def test_vopd_dual_mov(self):
"""VOPD with two MOV operations to different registers."""
instructions = [
v_mov_b32_e32(v[0], 0x12345678),
v_mov_b32_e32(v[1], 0xDEADBEEF),
# X: v[2] = v[0], Y: v[3] = v[1]
VOPD(VOPDOp.V_DUAL_MOV_B32, VOPDOp.V_DUAL_MOV_B32, v[2], v[3], v[0], v[1], v[0], v[0]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][2], 0x12345678)
self.assertEqual(st.vgpr[0][3], 0xDEADBEEF)
def test_vopd_mov_and_add(self):
"""VOPD with MOV (X) and ADD_NC_U32 (Y) - ADD_NC_U32 can only be Y op."""
instructions = [
v_mov_b32_e32(v[0], 10),
v_mov_b32_e32(v[1], 5),
# X: v[2] = 100 (literal), Y: v[3] = v[0] + v[1] = 15
VOPD(VOPDOp.V_DUAL_MOV_B32, VOPDOp.V_DUAL_ADD_NC_U32, v[2], v[3], 100, v[0], v[0], v[1]),
]
st = run_program(instructions, n_lanes=1)
self.assertEqual(st.vgpr[0][2], 100)
self.assertEqual(st.vgpr[0][3], 15)
class TestVOPDReadBeforeWrite(unittest.TestCase):
"""Tests for VOPD dual-issue read-before-write semantics.
In VOPD, both X and Y operations read their sources BEFORE either writes.
This is critical when X's destination is Y's source.
"""
def test_vopd_x_writes_y_reads_same_reg(self):
"""VOPD where X writes to a register that Y reads.
X: v[2] = 0 (overwrites v[2])
Y: v[1] = v[2] + v[0] (srcy0=v[2], vsrcy1=v[0])
If reads happen before writes: v[1] = OLD_v[2] + v[0] = 0xFFFFFFFF + 1 = 0
If writes happen before reads: v[1] = 0 + v[0] = 0 + 1 = 1
Hardware does reads-before-writes, so v[1] should be 0.
"""
instructions = [
v_mov_b32_e32(v[0], 1), # v[0] = 1
v_mov_b32_e32(v[1], 0x99999999), # v[1] = placeholder (will be overwritten)
v_mov_b32_e32(v[2], 0xFFFFFFFF), # v[2] = 0xFFFFFFFF
# X: v[2] = 0 (literal), srcx0=0, vsrcx1=v[0] (unused for MOV)
# Y: v[1] = srcy0 + vsrcy1 = v[2] + v[0] (should read OLD v[2] = 0xFFFFFFFF)
# vdsty encoding: (vdsty << 1) | ((vdstx & 1) ^ 1) where vdsty field = 0, vdstx = v[2]
# So vdsty_reg = (0 << 1) | ((2 & 1) ^ 1) = 0 | 1 = 1 = v[1]
VOPD(VOPDOp.V_DUAL_MOV_B32, VOPDOp.V_DUAL_ADD_NC_U32, v[2], v[0], 0, v[2], v[0], v[0]),
]
st = run_program(instructions, n_lanes=1)
# X should have written 0 to v[2]
self.assertEqual(st.vgpr[0][2], 0, "X should write 0 to v[2]")
# Y should have read OLD v[2] (0xFFFFFFFF) and added v[0] (1)
# 0xFFFFFFFF + 1 = 0 (wrap around)
self.assertEqual(st.vgpr[0][1], 0, "Y should read OLD v[2]=0xFFFFFFFF, compute 0xFFFFFFFF+1=0")
def test_vopd_x_writes_y_reads_same_reg_v2(self):
"""VOPD where X writes to a register that Y reads - cleaner test case.
X: v[2] = 0 (MOV)
Y: v[1] = v[2] + v[2] (ADD_NC_U32 with both sources from v[2])
If reads happen before writes: v[1] = OLD_v[2] + OLD_v[2] = 100 + 100 = 200
If writes happen before reads: v[1] = 0 + 0 = 0
Hardware does reads-before-writes, so v[1] should be 200.
"""
instructions = [
v_mov_b32_e32(v[0], 0x88888888), # v[0] = unused placeholder
v_mov_b32_e32(v[1], 0x99999999), # v[1] = placeholder (will be overwritten)
v_mov_b32_e32(v[2], 100), # v[2] = 100
# X: v[2] = 0 (literal)
# Y: v[1] = srcy0 + vsrcy1 = v[2] + v[2] (should read OLD v[2] = 100)
VOPD(VOPDOp.V_DUAL_MOV_B32, VOPDOp.V_DUAL_ADD_NC_U32, v[2], v[0], 0, v[2], v[0], v[2]),
]
st = run_program(instructions, n_lanes=1)
# X should have written 0 to v[2]
self.assertEqual(st.vgpr[0][2], 0, "X should write 0 to v[2]")
# Y should have read OLD v[2] (100) twice and added them
self.assertEqual(st.vgpr[0][1], 200, "Y should read OLD v[2]=100 twice, compute 100+100=200")
class TestVOPDLiterals(unittest.TestCase):
"""Tests for VOPD instructions that use SIMM32 literals (FMAAK, FMAMK)."""
def test_vopd_fmaak_f32(self):
"""VOPD V_DUAL_FMAAK_F32: D = S0 * S1 + SIMM32 (literal addend).
Tests that the 32-bit literal (SIMM32) is correctly passed to the instruction.
fma(2.0, 3.0, 10.0) = 2*3 + 10 = 16.0
"""
from extra.assembly.amd.test.hw.helpers import f2i, i2f
instructions = [
v_mov_b32_e32(v[0], f2i(2.0)), # v[0] = 2.0
v_mov_b32_e32(v[1], f2i(3.0)), # v[1] = 3.0
# VOPD args: opx, opy, vdstx, vdsty, srcx0, srcy0, vsrcx1, vsrcy1
# X: v[2] = fma(srcx0, vsrcx1, SIMM32) = v[0]*v[1]+10.0 = 2*3+10 = 16
# Y: v[3] = srcy0 (MOV) = v[0] = 2.0
VOPD_LIT(VOPDOp.V_DUAL_FMAAK_F32, VOPDOp.V_DUAL_MOV_B32, v[2], v[3], v[0], v[0], v[1], v[0], literal=f2i(10.0)),
]
st = run_program(instructions, n_lanes=1)
self.assertAlmostEqual(i2f(st.vgpr[0][2]), 16.0, places=5, msg="fma(2.0, 3.0, 10.0) should be 16.0")
def test_vopd_fmamk_f32(self):
"""VOPD V_DUAL_FMAMK_F32: D = S0 * SIMM32 + S1 (literal multiplier).
Tests that the 32-bit literal (SIMM32) is correctly used as the multiplier.
fma(2.0, 5.0, 3.0) = 2*5 + 3 = 13.0
"""
from extra.assembly.amd.test.hw.helpers import f2i, i2f
instructions = [
v_mov_b32_e32(v[0], f2i(2.0)), # v[0] = 2.0
v_mov_b32_e32(v[1], f2i(3.0)), # v[1] = 3.0
# X: v[2] = fma(srcx0, SIMM32, vsrcx1) = v[0]*5.0+v[1] = 2*5+3 = 13
# Y: v[3] = srcy0 (MOV) = v[0] = 2.0
VOPD_LIT(VOPDOp.V_DUAL_FMAMK_F32, VOPDOp.V_DUAL_MOV_B32, v[2], v[3], v[0], v[0], v[1], v[0], literal=f2i(5.0)),
]
st = run_program(instructions, n_lanes=1)
self.assertAlmostEqual(i2f(st.vgpr[0][2]), 13.0, places=5, msg="fma(2.0, 5.0, 3.0) should be 13.0")
class TestVOPDMultilane(unittest.TestCase):
"""Tests for VOPD with multiple lanes."""
def test_vopd_multilane_mov_add(self):
"""VOPD MOV and ADD with multiple active lanes - no register conflict."""
instructions = [
v_mov_b32_e32(v[0], 5),
v_mov_b32_e32(v[1], 10),
# X: v[2] = 100 (constant), Y: v[1] = v[0] + v[1] = 5 + 10 = 15
# vdsty_reg = (vdsty << 1) | ((vdstx.offset & 1) ^ 1) = (0 << 1) | ((258 & 1) ^ 1) = 0 | 1 = 1
VOPD(VOPDOp.V_DUAL_MOV_B32, VOPDOp.V_DUAL_ADD_NC_U32, v[2], v[0], 100, v[0], v[2], v[1]),
]
st = run_program(instructions, n_lanes=4)
for lane in range(4):
self.assertEqual(st.vgpr[lane][2], 100, f"Lane {lane}: v[2] should be 100")
self.assertEqual(st.vgpr[lane][1], 15, f"Lane {lane}: v[1] should be 15 (5+10)")
if __name__ == '__main__':
unittest.main()
@@ -1,48 +0,0 @@
import unittest
import functools
from tinygrad import Tensor, Device, dtypes
from tinygrad.uop.ops import UOp, Ops, KernelInfo
from tinygrad.renderer import Estimates
from tinygrad.runtime.support.compiler_amd import HIPCompiler
from extra.assembly.amd.autogen.rdna3.ins import *
from extra.assembly.amd.dsl import s, v, Inst
def assemble_insts(insts:list[Inst], name:str, arch:str, kernarg_size:int=8) -> tuple[UOp, UOp]:
kd = {"kernarg_size":kernarg_size, "user_sgpr_kernarg_segment_ptr":1, "next_free_vgpr":8, "next_free_sgpr":8, "wavefront_size32":1}
disasm = "\n".join([inst.disasm() for inst in insts])
hsasrc = f".text\n.globl {name}\n.p2align 8\n.type fn_name,@function\n{name}:\n{disasm}\ns_code_end\n"
hsasrc += f".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"
binary = HIPCompiler(arch).compile(hsasrc)
return UOp(Ops.SOURCE, arg=disasm), UOp(Ops.BINARY, arg=binary)
def custom_add_one(A:UOp, arch:str) -> UOp:
A = A.flatten()
assert dtypes.is_float(A.dtype.base), f"buffer dtype must be float32, got {A.dtype}"
threads = UOp.special(A.size, "lidx0")
insts = [
s_load_b64(s[0:1], s[0:1], soffset=NULL),
s_waitcnt(lgkmcnt=0),
v_lshlrev_b32_e32(v[0], 2, v[0]), # element offset
global_load_b32(v[1], v[0], saddr=s[0:1]),
s_waitcnt(vmcnt=0),
v_mov_b32_e32(v[2], 1.0),
v_add_f32_e32(v[1], v[1], v[2]),
global_store_b32(addr=v[0], data=v[1], saddr=s[0:1]),
s_endpgm(),
]
sink = UOp.sink(A.base, threads, arg=KernelInfo(name:=f"custom_add_one_{A.size}", estimates=Estimates(ops=A.size, mem=A.size*4*2)))
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.DEVICE, arg="AMD"), UOp(Ops.LINEAR, src=(*sink.src, sink)), *assemble_insts(insts, name, arch)))
class TestCustomKernel(unittest.TestCase):
def test_simple(self):
a = Tensor.full((16, 16), 1.).contiguous().realize()
a = Tensor.custom_kernel(a, fxn=functools.partial(custom_add_one, arch=Device[Device.DEFAULT].arch))[0]
ei = a.schedule()[-1].lower()
self.assertEqual(ei.prg.estimates.ops, a.numel())
self.assertEqual(ei.prg.estimates.mem, a.nbytes()*2)
ei.run()
self.assertTrue((a.numpy() == 2.).all())
if __name__ == "__main__":
unittest.main()
-170
View File
@@ -1,170 +0,0 @@
import unittest
from extra.assembly.amd.dsl import *
from extra.assembly.amd.dsl import VDSTYField
from extra.assembly.amd.autogen.rdna3.enum import VOP1Op, VOP2Op
from extra.assembly.amd.autogen.rdna3.ins import VOP1
class TestRegisters(unittest.TestCase):
def test_vgpr_single(self):
self.assertEqual(repr(v[5]), "v[5]")
self.assertEqual(v[5].offset, 261) # 256 + 5
self.assertEqual(v[5].sz, 1)
def test_sgpr_single(self):
self.assertEqual(repr(s[10]), "s[10]")
self.assertEqual(s[10].offset, 10)
def test_vgpr_range(self):
self.assertEqual(repr(v[0:3]), "v[0:3]")
self.assertEqual(v[0:3].offset, 256)
self.assertEqual(v[0:3].sz, 4)
def test_sgpr_range(self):
self.assertEqual(repr(s[4:5]), "s[4:5]")
self.assertEqual(s[4:5].sz, 2)
def test_ttmp_reslice(self):
# ttmp is src[108:123], so ttmp[0] should be src[108]
self.assertEqual(ttmp[0].offset, 108)
self.assertEqual(ttmp[1].offset, 109)
# ttmp[0:1] is 2 elements (inclusive slicing)
self.assertEqual(ttmp[0:1].offset, 108)
self.assertEqual(ttmp[0:1].sz, 2)
# ttmp[0:1][0] should be src[108]
self.assertEqual(ttmp[0:1][0].offset, 108)
def test_special_regs(self):
self.assertEqual(NULL.offset, 124)
self.assertEqual(M0.offset, 125)
self.assertEqual(EXEC_LO.offset, 126)
self.assertEqual(EXEC_HI.offset, 127)
# Check repr round-trips
self.assertEqual(repr(NULL), "NULL")
self.assertEqual(repr(M0), "M0")
self.assertEqual(repr(EXEC_LO), "EXEC_LO")
self.assertEqual(repr(EXEC), "EXEC")
def test_vcc(self):
self.assertEqual(VCC.offset, 106)
self.assertEqual(VCC.sz, 2)
self.assertEqual(VCC_LO.offset, 106)
self.assertEqual(VCC_HI.offset, 107)
# Check repr round-trips
self.assertEqual(repr(VCC_LO), "VCC_LO")
self.assertEqual(repr(VCC_HI), "VCC_HI")
self.assertEqual(repr(VCC), "VCC")
def test_float_constants(self):
self.assertEqual(src[240].offset, 240)
self.assertEqual(repr(src[240]), "0.5")
self.assertEqual(repr(src[242]), "1.0")
self.assertEqual(repr(src[243]), "-1.0")
def test_int_constants(self):
self.assertEqual(repr(src[128]), "0")
self.assertEqual(repr(src[129]), "1")
self.assertEqual(repr(src[192]), "64")
self.assertEqual(repr(src[193]), "-1")
self.assertEqual(repr(src[208]), "-16")
class TestEnumBitField(unittest.TestCase):
def test_enum_name(self):
self.assertEqual(VOP1Op.V_MOV_B32_E32.name, "V_MOV_B32_E32")
def test_enum_value(self):
self.assertEqual(VOP1Op.V_MOV_B32_E32.value, 1)
def test_enum_comparison(self):
self.assertEqual(VOP1Op.V_MOV_B32_E32, VOP1Op.V_MOV_B32_E32)
self.assertNotEqual(VOP1Op.V_NOP_E32, VOP1Op.V_MOV_B32_E32)
def test_enum_different_types(self):
# VOP1Op and VOP2Op are different enums, even if same value
self.assertNotEqual(VOP1Op.V_MOV_B32_E32, VOP2Op.V_CNDMASK_B32_E32)
def test_wrong_enum_type_raises(self):
# Passing VOP2Op to VOP1 should raise
with self.assertRaises(RuntimeError):
VOP1(VOP2Op.V_CNDMASK_B32_E32, v[5], v[6])
class TestVOP1(unittest.TestCase):
def test_class_setup(self):
self.assertEqual(VOP1._size(), 4)
field_names = [n for n, _ in VOP1._fields]
self.assertIn('encoding', field_names)
self.assertIn('op', field_names)
self.assertIn('vdst', field_names)
self.assertIn('src0', field_names)
def test_encoding_vgpr_vgpr(self):
i = VOP1(VOP1Op.V_MOV_B32_E32, v[5], v[6])
raw = i._raw
# Check each field
self.assertEqual((raw >> 25) & 0x7f, 0b0111111) # encoding
self.assertEqual((raw >> 17) & 0xff, 5) # vdst (just VGPR index)
self.assertEqual((raw >> 9) & 0xff, 1) # op
self.assertEqual(raw & 0x1ff, 262) # src0 (256 + 6)
def test_encoding_vgpr_sgpr(self):
i = VOP1(VOP1Op.V_MOV_B32_E32, v[5], s[10])
raw = i._raw
self.assertEqual((raw >> 17) & 0xff, 5) # vdst (just VGPR index)
self.assertEqual(raw & 0x1ff, 10) # src0 (SGPR encoded)
def test_to_bytes(self):
i = VOP1(VOP1Op.V_MOV_B32_E32, v[5], v[6])
b = i.to_bytes()
self.assertEqual(len(b), 4)
self.assertEqual(int.from_bytes(b, 'little'), i._raw)
def test_from_bytes(self):
i1 = VOP1(VOP1Op.V_MOV_B32_E32, v[5], v[6])
i2 = VOP1.from_bytes(i1.to_bytes())
self.assertEqual(i1._raw, i2._raw)
def test_repr(self):
i = VOP1(VOP1Op.V_MOV_B32_E32, v[5], v[6])
self.assertEqual(repr(i), "v_mov_b32_e32(v[5], v[6])")
def test_repr_sgpr_src(self):
i = VOP1(VOP1Op.V_MOV_B32_E32, v[5], s[10])
self.assertEqual(repr(i), "v_mov_b32_e32(v[5], s[10])")
def test_kwargs(self):
i1 = VOP1(VOP1Op.V_MOV_B32_E32, v[5], v[6])
i2 = VOP1(op=VOP1Op.V_MOV_B32_E32, vdst=v[5], src0=v[6])
self.assertEqual(i1._raw, i2._raw)
def test_kwargs_partial(self):
i1 = VOP1(VOP1Op.V_MOV_B32_E32, v[5], v[6])
i2 = VOP1(VOP1Op.V_MOV_B32_E32, src0=v[6], vdst=v[5])
self.assertEqual(i1._raw, i2._raw)
class TestVDSTYField(unittest.TestCase):
def test_encode_even_vgpr(self):
f = VDSTYField(6, 0) # 7-bit field
self.assertEqual(f.encode(v[0]), 0)
self.assertEqual(f.encode(v[2]), 1)
self.assertEqual(f.encode(v[4]), 2)
self.assertEqual(f.encode(v[254]), 127)
def test_encode_non_vgpr_raises(self):
f = VDSTYField(6, 0)
with self.assertRaises(ValueError) as ctx:
f.encode(s[0])
self.assertIn("VGPR", str(ctx.exception))
def test_encode_non_reg_raises(self):
f = VDSTYField(6, 0)
with self.assertRaises(TypeError) as ctx:
f.encode(42)
self.assertIn("Reg", str(ctx.exception))
def test_decode_returns_raw(self):
f = VDSTYField(6, 0)
# decode returns raw value, actual vdsty computed with vdstx context
self.assertEqual(f.decode(0), 0)
self.assertEqual(f.decode(127), 127)
if __name__ == "__main__":
unittest.main()
+201 -23
View File
@@ -1,13 +1,176 @@
#!/usr/bin/env python3
"""Test DS and other compute-relevant instruction formats.
Note: Graphics-only formats (EXP, MUBUF, MTBUF, MIMG) are not supported - use GLOBAL/FLAT for memory access in compute.
"""
"""Test MUBUF, MTBUF, MIMG, EXP, DS formats against LLVM."""
import unittest
from extra.assembly.amd.autogen.rdna3.ins import *
from extra.assembly.amd.dsl import VCC_HI, EXEC_LO, NULL
OFF = NULL # OFF is alias for NULL
from extra.assembly.amd.decode import detect_format
from extra.assembly.amd.dsl import encode_src, RawImm
from extra.assembly.amd.asm import detect_format
class TestMUBUF(unittest.TestCase):
"""Test MUBUF (buffer) instructions."""
def test_buffer_load_b32_basic(self):
# buffer_load_b32 v5, off, s[8:11], s3 offset:4095
# GFX11: encoding: [0xff,0x0f,0x50,0xe0,0x00,0x05,0x02,0x03]
inst = buffer_load_b32(vdata=v[5], vaddr=v[0], srsrc=s[8:12], soffset=s[3], offset=4095)
self.assertEqual(inst.to_bytes(), bytes([0xff,0x0f,0x50,0xe0,0x00,0x05,0x02,0x03]))
def test_buffer_load_b32_idxen(self):
# buffer_load_b32 v5, v0, s[8:11], s3 idxen offset:4095
# GFX11: encoding: [0xff,0x0f,0x50,0xe0,0x00,0x05,0x82,0x03]
inst = buffer_load_b32(vdata=v[5], vaddr=v[0], srsrc=s[8:12], soffset=s[3], offset=4095, idxen=1)
self.assertEqual(inst.to_bytes(), bytes([0xff,0x0f,0x50,0xe0,0x00,0x05,0x82,0x03]))
def test_buffer_load_b32_offen(self):
# buffer_load_b32 v5, v0, s[8:11], s3 offen offset:4095
# GFX11: encoding: [0xff,0x0f,0x50,0xe0,0x00,0x05,0x42,0x03]
inst = buffer_load_b32(vdata=v[5], vaddr=v[0], srsrc=s[8:12], soffset=s[3], offset=4095, offen=1)
self.assertEqual(inst.to_bytes(), bytes([0xff,0x0f,0x50,0xe0,0x00,0x05,0x42,0x03]))
def test_buffer_load_b32_glc(self):
# buffer_load_b32 v5, off, s[8:11], s3 offset:4095 glc
# GFX11: encoding: [0xff,0x4f,0x50,0xe0,0x00,0x05,0x02,0x03]
inst = buffer_load_b32(vdata=v[5], vaddr=v[0], srsrc=s[8:12], soffset=s[3], offset=4095, glc=1)
self.assertEqual(inst.to_bytes(), bytes([0xff,0x4f,0x50,0xe0,0x00,0x05,0x02,0x03]))
def test_buffer_load_b32_slc(self):
# buffer_load_b32 v5, off, s[8:11], s3 offset:4095 slc
# GFX11: encoding: [0xff,0x1f,0x50,0xe0,0x00,0x05,0x02,0x03]
inst = buffer_load_b32(vdata=v[5], vaddr=v[0], srsrc=s[8:12], soffset=s[3], offset=4095, slc=1)
self.assertEqual(inst.to_bytes(), bytes([0xff,0x1f,0x50,0xe0,0x00,0x05,0x02,0x03]))
def test_buffer_load_b32_dlc(self):
# buffer_load_b32 v5, off, s[8:11], s3 offset:4095 dlc
# GFX11: encoding: [0xff,0x2f,0x50,0xe0,0x00,0x05,0x02,0x03]
inst = buffer_load_b32(vdata=v[5], vaddr=v[0], srsrc=s[8:12], soffset=s[3], offset=4095, dlc=1)
self.assertEqual(inst.to_bytes(), bytes([0xff,0x2f,0x50,0xe0,0x00,0x05,0x02,0x03]))
def test_buffer_load_b32_all_flags(self):
# buffer_load_b32 v5, off, s[8:11], s3 offset:4095 glc slc dlc
# GFX11: encoding: [0xff,0x7f,0x50,0xe0,0x00,0x05,0x02,0x03]
inst = buffer_load_b32(vdata=v[5], vaddr=v[0], srsrc=s[8:12], soffset=s[3], offset=4095, glc=1, slc=1, dlc=1)
self.assertEqual(inst.to_bytes(), bytes([0xff,0x7f,0x50,0xe0,0x00,0x05,0x02,0x03]))
def test_buffer_store_b32(self):
# buffer_store_b32 v1, off, s[12:15], s4 offset:4095
# GFX11: encoding: [0xff,0x0f,0x68,0xe0,0x00,0x01,0x03,0x04]
inst = buffer_store_b32(vdata=v[1], vaddr=v[0], srsrc=s[12:16], soffset=s[4], offset=4095)
self.assertEqual(inst.to_bytes(), bytes([0xff,0x0f,0x68,0xe0,0x00,0x01,0x03,0x04]))
def test_buffer_load_b64(self):
# buffer_load_b64 v[5:6], off, s[8:11], s3 offset:4095
# GFX11: encoding: [0xff,0x0f,0x54,0xe0,0x00,0x05,0x02,0x03]
inst = buffer_load_b64(vdata=v[5:7], vaddr=v[0], srsrc=s[8:12], soffset=s[3], offset=4095)
self.assertEqual(inst.to_bytes(), bytes([0xff,0x0f,0x54,0xe0,0x00,0x05,0x02,0x03]))
def test_buffer_load_soffset_m0(self):
# buffer_load_b32 v5, off, s[8:11], m0 offset:4095
# GFX11: encoding: [0xff,0x0f,0x50,0xe0,0x00,0x05,0x02,0x7d]
inst = buffer_load_b32(vdata=v[5], vaddr=v[0], srsrc=s[8:12], soffset=M0, offset=4095)
self.assertEqual(inst.to_bytes(), bytes([0xff,0x0f,0x50,0xe0,0x00,0x05,0x02,0x7d]))
def test_buffer_load_soffset_inline_const(self):
# buffer_load_b32 v5, off, s[8:11], 0 offset:4095
# GFX11: encoding: [0xff,0x0f,0x50,0xe0,0x00,0x05,0x02,0x80]
inst = buffer_load_b32(vdata=v[5], vaddr=v[0], srsrc=s[8:12], soffset=0, offset=4095)
self.assertEqual(inst.to_bytes(), bytes([0xff,0x0f,0x50,0xe0,0x00,0x05,0x02,0x80]))
def test_buffer_disasm_roundtrip(self):
inst = buffer_load_b32(vdata=v[5], vaddr=v[0], srsrc=s[8:12], soffset=s[3], offset=4095, glc=1)
decoded = MUBUF.from_bytes(inst.to_bytes())
self.assertEqual(decoded.to_bytes(), inst.to_bytes())
class TestMTBUF(unittest.TestCase):
"""Test MTBUF (typed buffer) instructions."""
def test_tbuffer_load_format_x(self):
# tbuffer_load_format_x v5, off, s[8:11], s3 format:[BUF_FMT_32_FLOAT] offset:4095
# BUF_FMT_32_FLOAT = 22
# GFX11: encoding: [0xff,0x0f,0xb0,0xe8,0x00,0x05,0x02,0x03]
inst = tbuffer_load_format_x(vdata=v[5], vaddr=v[0], srsrc=s[8:12], soffset=s[3], offset=4095, format=22)
self.assertEqual(inst.to_bytes(), bytes([0xff,0x0f,0xb0,0xe8,0x00,0x05,0x02,0x03]))
def test_tbuffer_store_format_x(self):
# tbuffer_store_format_x v5, off, s[8:11], s3 format:[BUF_FMT_32_FLOAT] offset:4095
# BUF_FMT_32_FLOAT = 22
# GFX11: encoding: [0xff,0x0f,0xb2,0xe8,0x00,0x05,0x02,0x03]
inst = tbuffer_store_format_x(vdata=v[5], vaddr=v[0], srsrc=s[8:12], soffset=s[3], offset=4095, format=22)
self.assertEqual(inst.to_bytes(), bytes([0xff,0x0f,0xb2,0xe8,0x00,0x05,0x02,0x03]))
def test_tbuffer_load_format_xy(self):
# tbuffer_load_format_xy v[5:6], off, s[8:11], s3 format:[BUF_FMT_32_32_FLOAT] offset:4095
# BUF_FMT_32_32_FLOAT = 50
# GFX11: encoding: [0xff,0x8f,0x90,0xe9,0x00,0x05,0x02,0x03]
inst = tbuffer_load_format_xy(vdata=v[5:7], vaddr=v[0], srsrc=s[8:12], soffset=s[3], offset=4095, format=50)
self.assertEqual(inst.to_bytes(), bytes([0xff,0x8f,0x90,0xe9,0x00,0x05,0x02,0x03]))
class TestMIMG(unittest.TestCase):
"""Test MIMG (image) instructions."""
def test_image_load_2d(self):
# image_load v[0:3], v[4:5], s[0:7] dmask:0xf dim:SQ_RSRC_IMG_2D
# GFX11: encoding: [0x04,0x0f,0x00,0xf0,0x04,0x00,0x00,0x00]
inst = image_load(vdata=v[0:4], vaddr=v[4:6], srsrc=s[0:8], dmask=0xf, dim=1) # dim=1 is SQ_RSRC_IMG_2D
self.assertEqual(inst.to_bytes(), bytes([0x04,0x0f,0x00,0xf0,0x04,0x00,0x00,0x00]))
def test_image_store_2d(self):
# image_store v[0:3], v[4:5], s[0:7] dmask:0xf dim:SQ_RSRC_IMG_2D
# GFX11: encoding: [0x04,0x0f,0x18,0xf0,0x04,0x00,0x00,0x00]
inst = image_store(vdata=v[0:4], vaddr=v[4:6], srsrc=s[0:8], dmask=0xf, dim=1)
self.assertEqual(inst.to_bytes(), bytes([0x04,0x0f,0x18,0xf0,0x04,0x00,0x00,0x00]))
def test_image_load_1d(self):
# image_load v[0:3], v4, s[0:7] dmask:0xf dim:SQ_RSRC_IMG_1D
# GFX11: encoding: [0x00,0x0f,0x00,0xf0,0x04,0x00,0x00,0x00]
inst = image_load(vdata=v[0:4], vaddr=v[4], srsrc=s[0:8], dmask=0xf, dim=0) # dim=0 is SQ_RSRC_IMG_1D
self.assertEqual(inst.to_bytes(), bytes([0x00,0x0f,0x00,0xf0,0x04,0x00,0x00,0x00]))
def test_image_sample(self):
# image_sample v[0:3], v[4:5], s[0:7], s[8:11] dmask:0xf dim:SQ_RSRC_IMG_2D
# GFX11: encoding: [0x04,0x0f,0x6c,0xf0,0x04,0x00,0x00,0x08]
inst = image_sample(vdata=v[0:4], vaddr=v[4:6], srsrc=s[0:8], ssamp=s[8:12], dmask=0xf, dim=1)
self.assertEqual(inst.to_bytes(), bytes([0x04,0x0f,0x6c,0xf0,0x04,0x00,0x00,0x08]))
def test_image_load_d16(self):
# image_load v[0:1], v[4:5], s[0:7] dmask:0xf dim:SQ_RSRC_IMG_2D d16
# GFX11: encoding: [0x04,0x0f,0x02,0xf0,0x04,0x00,0x00,0x00]
inst = image_load(vdata=v[0:2], vaddr=v[4:6], srsrc=s[0:8], dmask=0xf, dim=1, d16=1)
self.assertEqual(inst.to_bytes(), bytes([0x04,0x0f,0x02,0xf0,0x04,0x00,0x00,0x00]))
class TestEXP(unittest.TestCase):
"""Test EXP (export) instructions."""
def test_exp_mrt0(self):
# exp mrt0 v0, v1, v2, v3
# GFX11: encoding: [0x0f,0x00,0x00,0xf8,0x00,0x01,0x02,0x03]
inst = EXP(en=0xf, target=0, vsrc0=v[0], vsrc1=v[1], vsrc2=v[2], vsrc3=v[3])
self.assertEqual(inst.to_bytes(), bytes([0x0f,0x00,0x00,0xf8,0x00,0x01,0x02,0x03]))
def test_exp_mrtz(self):
# exp mrtz v4, v3, v2, v1
# GFX11: encoding: [0x8f,0x00,0x00,0xf8,0x04,0x03,0x02,0x01]
inst = EXP(en=0xf, target=8, vsrc0=v[4], vsrc1=v[3], vsrc2=v[2], vsrc3=v[1])
self.assertEqual(inst.to_bytes(), bytes([0x8f,0x00,0x00,0xf8,0x04,0x03,0x02,0x01]))
def test_exp_mrtz_done(self):
# exp mrtz v4, v3, v2, v1 done
# GFX11: encoding: [0x8f,0x08,0x00,0xf8,0x04,0x03,0x02,0x01]
inst = EXP(en=0xf, target=8, vsrc0=v[4], vsrc1=v[3], vsrc2=v[2], vsrc3=v[3], done=1)
self.assertEqual(inst.to_bytes(), bytes([0x8f,0x08,0x00,0xf8,0x04,0x03,0x02,0x03]))
def test_exp_partial_mask(self):
# exp mrt0 v0, v1, off, off (en=0x3, only first two components)
# GFX11: encoding: [0x03,0x00,0x00,0xf8,0x00,0x01,0x00,0x00]
inst = EXP(en=0x3, target=0, vsrc0=v[0], vsrc1=v[1], vsrc2=v[0], vsrc3=v[0])
self.assertEqual(inst.to_bytes(), bytes([0x03,0x00,0x00,0xf8,0x00,0x01,0x00,0x00]))
def test_exp_row_en(self):
# exp mrtz v4, v3, v2, v1 row_en
# GFX11: encoding: [0x8f,0x20,0x00,0xf8,0x04,0x03,0x02,0x01]
inst = EXP(en=0xf, target=8, vsrc0=v[4], vsrc1=v[3], vsrc2=v[2], vsrc3=v[1], row=1)
self.assertEqual(inst.to_bytes(), bytes([0x8f,0x20,0x00,0xf8,0x04,0x03,0x02,0x01]))
class TestDS(unittest.TestCase):
@@ -34,7 +197,7 @@ class TestDS(unittest.TestCase):
def test_ds_load_b64(self):
# ds_load_b64 v[0:1], v2
# GFX11: encoding: [0x00,0x00,0xd8,0xd9,0x02,0x00,0x00,0x00]
inst = ds_load_b64(vdst=v[0:1], addr=v[2])
inst = ds_load_b64(vdst=v[0:2], addr=v[2])
self.assertEqual(inst.to_bytes(), bytes([0x00,0x00,0xd8,0xd9,0x02,0x00,0x00,0x00]))
def test_ds_add_u32(self):
@@ -78,31 +241,31 @@ class TestFLAT(unittest.TestCase):
def test_global_load_b32(self):
# global_load_b32 v0, v[1:2], off (seg=2 for global)
# GFX11: encoding: [0x00,0x00,0x52,0xdc,0x01,0x00,0x7c,0x00]
inst = global_load_b32(vdst=v[0], addr=v[1:2], saddr=OFF)
inst = global_load_b32(vdst=v[0], addr=v[1:3], saddr=OFF)
self.assertEqual(inst.to_bytes(), bytes([0x00,0x00,0x52,0xdc,0x01,0x00,0x7c,0x00]))
def test_global_store_b32(self):
# global_store_b32 v[0:1], v2, off (seg=2 for global)
# GFX11: encoding: [0x00,0x00,0x6a,0xdc,0x00,0x02,0x7c,0x00]
inst = global_store_b32(addr=v[0:1], data=v[2], saddr=OFF)
inst = global_store_b32(addr=v[0:2], data=v[2], saddr=OFF)
self.assertEqual(inst.to_bytes(), bytes([0x00,0x00,0x6a,0xdc,0x00,0x02,0x7c,0x00]))
def test_global_load_b32_saddr(self):
# global_load_b32 v0, v1, s[0:1] (seg=2 for global)
# GFX11: encoding: [0x00,0x00,0x52,0xdc,0x01,0x00,0x00,0x00]
inst = global_load_b32(vdst=v[0], addr=v[1], saddr=s[0:1])
inst = global_load_b32(vdst=v[0], addr=v[1], saddr=s[0:2])
self.assertEqual(inst.to_bytes(), bytes([0x00,0x00,0x52,0xdc,0x01,0x00,0x00,0x00]))
def test_global_load_b32_offset(self):
# global_load_b32 v0, v[1:2], off offset:256 (seg=2 for global)
# GFX11: encoding: [0x00,0x01,0x52,0xdc,0x01,0x00,0x7c,0x00]
inst = global_load_b32(vdst=v[0], addr=v[1:2], saddr=OFF, offset=256)
inst = global_load_b32(vdst=v[0], addr=v[1:3], saddr=OFF, offset=256)
self.assertEqual(inst.to_bytes(), bytes([0x00,0x01,0x52,0xdc,0x01,0x00,0x7c,0x00]))
def test_global_load_b64(self):
# global_load_b64 v[0:1], v[2:3], off (seg=2 for global)
# GFX11: encoding: [0x00,0x00,0x56,0xdc,0x02,0x00,0x7c,0x00]
inst = global_load_b64(vdst=v[0:1], addr=v[2:3], saddr=OFF)
inst = global_load_b64(vdst=v[0:2], addr=v[2:4], saddr=OFF)
self.assertEqual(inst.to_bytes(), bytes([0x00,0x00,0x56,0xdc,0x02,0x00,0x7c,0x00]))
@@ -112,19 +275,19 @@ class TestSMEM(unittest.TestCase):
def test_smem_dlc_bit_position(self):
# s_load_b32 s5, s[2:3], s0 dlc - tests that DLC is at bit 13 (not bit 14)
# GFX11: encoding: [0x41,0x21,0x00,0xf4,0x00,0x00,0x00,0x00]
inst = s_load_b32(sdata=s[5], sbase=s[2:3], soffset=s[0], dlc=1)
inst = s_load_b32(sdata=s[5], sbase=s[2], soffset=s[0], dlc=1)
self.assertEqual(inst.to_bytes(), bytes([0x41,0x21,0x00,0xf4,0x00,0x00,0x00,0x00]))
def test_smem_glc_bit_position(self):
# s_load_b32 s5, s[2:3], s0 glc - tests that GLC is at bit 14 (not bit 16)
# GFX11: encoding: [0x41,0x41,0x00,0xf4,0x00,0x00,0x00,0x00]
inst = s_load_b32(sdata=s[5], sbase=s[2:3], soffset=s[0], glc=1)
inst = s_load_b32(sdata=s[5], sbase=s[2], soffset=s[0], glc=1)
self.assertEqual(inst.to_bytes(), bytes([0x41,0x41,0x00,0xf4,0x00,0x00,0x00,0x00]))
def test_smem_glc_dlc_combined(self):
# s_load_b32 s5, s[2:3], s0 glc dlc - tests both flags together
# GFX11: encoding: [0x41,0x61,0x00,0xf4,0x00,0x00,0x00,0x00]
inst = s_load_b32(sdata=s[5], sbase=s[2:3], soffset=s[0], glc=1, dlc=1)
inst = s_load_b32(sdata=s[5], sbase=s[2], soffset=s[0], glc=1, dlc=1)
self.assertEqual(inst.to_bytes(), bytes([0x41,0x61,0x00,0xf4,0x00,0x00,0x00,0x00]))
def test_smem_disasm_roundtrip_dlc(self):
@@ -146,20 +309,23 @@ class TestVOP3Literal(unittest.TestCase):
def test_vop3_with_literal(self):
# v_add3_u32 v5, vcc_hi, 0xaf123456, v255
# GFX11: encoding: [0x05,0x00,0x55,0xd6,0x6b,0xfe,0xfd,0x07,0x56,0x34,0x12,0xaf]
inst = VOP3(VOP3Op.V_ADD3_U32, vdst=v[5], src0=VCC_HI, src1=0xaf123456, src2=v[255])
from extra.assembly.amd.dsl import RawImm
inst = VOP3(VOP3Op.V_ADD3_U32, vdst=v[5], src0=RawImm(107), src1=0xaf123456, src2=v[255])
expected = bytes([0x05,0x00,0x55,0xd6,0x6b,0xfe,0xfd,0x07,0x56,0x34,0x12,0xaf])
self.assertEqual(inst.to_bytes(), expected)
def test_vop3_literal_null_operand(self):
# v_add3_u32 v5, null, exec_lo, 0xaf123456
# GFX11: encoding: [0x05,0x00,0x55,0xd6,0x7c,0xfc,0xfc,0x03,0x56,0x34,0x12,0xaf]
inst = VOP3(VOP3Op.V_ADD3_U32, vdst=v[5], src0=NULL, src1=EXEC_LO, src2=0xaf123456)
from extra.assembly.amd.dsl import RawImm
inst = VOP3(VOP3Op.V_ADD3_U32, vdst=v[5], src0=NULL, src1=RawImm(126), src2=0xaf123456)
expected = bytes([0x05,0x00,0x55,0xd6,0x7c,0xfc,0xfc,0x03,0x56,0x34,0x12,0xaf])
self.assertEqual(inst.to_bytes(), expected)
def test_vop3p_with_literal(self):
# Test VOP3P literal encoding (also uses Inst64)
inst = VOP3P(VOP3POp.V_PK_ADD_F16, vdst=v[5], src0=0.5, src1=0x12345678, src2=v[0])
from extra.assembly.amd.dsl import RawImm
inst = VOP3P(VOP3POp.V_PK_ADD_F16, vdst=v[5], src0=RawImm(240), src1=0x12345678, src2=v[0])
self.assertEqual(len(inst.to_bytes()), 12) # 8 bytes + 4 byte literal
@@ -205,16 +371,28 @@ class TestDetectFormat(unittest.TestCase):
self.assertEqual(detect_format(VOP3P(VOP3POp.V_PK_ADD_F16, v[0], v[1], v[2], v[3]).to_bytes()), VOP3P)
def test_detect_smem(self):
self.assertEqual(detect_format(s_load_b32(sdata=s[0], sbase=s[2:3], offset=0).to_bytes()), SMEM)
self.assertEqual(detect_format(s_load_b64(sdata=s[0:1], sbase=s[2:3], soffset=s[5]).to_bytes()), SMEM)
self.assertEqual(detect_format(s_load_b32(s[0], s[2:3], 0).to_bytes()), SMEM)
self.assertEqual(detect_format(s_load_b64(s[0:1], s[2:3], s[5]).to_bytes()), SMEM)
def test_detect_ds(self):
self.assertEqual(detect_format(ds_load_b32(v[0], v[1]).to_bytes()), DS)
self.assertEqual(detect_format(ds_store_b32(v[0], v[1]).to_bytes()), DS)
def test_detect_flat(self):
self.assertEqual(detect_format(global_load_b32(vdst=v[0], addr=v[1:2], saddr=NULL).to_bytes()), GLOBAL)
self.assertEqual(detect_format(global_store_b32(addr=v[0:1], data=v[2], saddr=NULL).to_bytes()), GLOBAL)
self.assertEqual(detect_format(global_load_b32(v[0], v[1:3], RawImm(124)).to_bytes()), FLAT)
self.assertEqual(detect_format(global_store_b32(v[0:2], v[2], RawImm(124)).to_bytes()), FLAT)
def test_detect_mubuf(self):
self.assertEqual(detect_format(buffer_load_b32(v[0], v[1], s[0:4], s[5]).to_bytes()), MUBUF)
def test_detect_mtbuf(self):
self.assertEqual(detect_format(tbuffer_load_format_x(v[0], v[1], s[0:4], s[5], format=22).to_bytes()), MTBUF)
def test_detect_mimg(self):
self.assertEqual(detect_format(image_load(v[0:4], v[4:6], s[0:8], dmask=0xf, dim=1).to_bytes()), MIMG)
def test_detect_exp(self):
self.assertEqual(detect_format(EXP(en=0xf, target=0, vsrc0=v[0], vsrc1=v[1], vsrc2=v[2], vsrc3=v[3]).to_bytes()), EXP)
def test_detect_vopd(self):
inst = VOPD(VOPDOp.V_DUAL_MOV_B32, VOPDOp.V_DUAL_MOV_B32, vdstx=v[0], vdsty=v[1], srcx0=0, srcy0=0)
+10 -6
View File
@@ -4,6 +4,7 @@
import unittest, struct
from extra.assembly.amd.autogen.rdna3.ins import *
from extra.assembly.amd.dsl import Inst
from extra.assembly.amd.asm import asm
from extra.assembly.amd.test.test_roundtrip import compile_asm
class TestIntegration(unittest.TestCase):
@@ -12,20 +13,23 @@ class TestIntegration(unittest.TestCase):
if not hasattr(self, 'inst'): return
b = self.inst.to_bytes()
st = self.inst.disasm()
# Test that the instruction can be compiled by LLVM and produces the same bytes
desc = f"{st:25s} {self.inst} {b!r}"
reasm = asm(st)
desc = f"{st:25s} {self.inst} {b!r} {reasm}"
self.assertEqual(b, compile_asm(st), desc)
# TODO: this compare should work for valid things
#self.assertEqual(self.inst, reasm)
self.assertEqual(repr(self.inst), repr(reasm))
print(desc)
def test_wmma(self):
self.inst = v_wmma_f32_16x16x16_f16(v[0:7], v[184:191], v[136:143], v[0:7])
self.inst = v_wmma_f32_16x16x16_f16(v[0:7], v[189:192], v[140:143], v[0:7])
def test_load_b128(self):
self.inst = s_load_b128(s[4:7], s[0:1], NULL, 0)
def test_load_b128_wrong_size(self):
# this should have to be 4 regs on the loaded to
with self.assertRaises(TypeError):
with self.assertRaises(Exception):
self.inst = s_load_b128(s[4:6], s[0:1], NULL, 0)
def test_mov_b32(self):
@@ -139,7 +143,7 @@ class TestRegisterSliceSyntax(unittest.TestCase):
def test_register_slice_count(self):
# s[4:7] should give 4 registers: s4, s5, s6, s7 (AMD convention, inclusive)
reg = s[4:7]
self.assertEqual(reg.sz, 4, "s[4:7] should give 4 registers (s4, s5, s6, s7)")
self.assertEqual(reg.count, 4, "s[4:7] should give 4 registers (s4, s5, s6, s7)")
def test_register_slice_roundtrip(self):
# Round-trip: DSL -> disasm -> DSL should preserve register count
@@ -150,7 +154,7 @@ class TestRegisterSliceSyntax(unittest.TestCase):
self.assertIn("s[4:7]", disasm)
# And s[4:7] in DSL should give the same 4 registers
reg_from_disasm = s[4:7]
self.assertEqual(reg_from_disasm.sz, 4, "s[4:7] from disasm should give 4 registers")
self.assertEqual(reg_from_disasm.count, 4, "s[4:7] from disasm should give 4 registers")
class TestInstructionEquality(unittest.TestCase):
"""
+81 -9
View File
@@ -1,10 +1,9 @@
#!/usr/bin/env python3
"""Integration test: round-trip RDNA3 assembly through AMD toolchain."""
import unittest, io, sys
import unittest, re, io, sys, subprocess
from extra.assembly.amd.autogen.rdna3.ins import *
def waitcnt(vmcnt: int = 0x3f, expcnt: int = 0x7, lgkmcnt: int = 0x3f) -> int:
return (expcnt & 0x7) | ((lgkmcnt & 0x3f) << 4) | ((vmcnt & 0x3f) << 10)
from extra.assembly.amd.asm import waitcnt, asm
from extra.assembly.amd.test.helpers import get_llvm_mc
def disassemble(lib: bytes, arch: str = "gfx1100") -> str:
"""Disassemble ELF binary using tinygrad's compiler, return raw output."""
@@ -41,7 +40,7 @@ def assemble_and_disassemble(instructions: list, arch: str = "gfx1100") -> list[
return parse_disassembly(disassemble(lib, arch))
class TestIntegration(unittest.TestCase):
"""Test our DSL output matches LLVM disassembly."""
"""Test our assembler output matches LLVM disassembly."""
def test_simple_sop1(self):
"""Test SOP1 instructions round-trip."""
@@ -91,9 +90,9 @@ class TestIntegration(unittest.TestCase):
def test_memory_ops(self):
"""Test memory instructions."""
instructions = [
s_load_b32(s[0], s[0:1], NULL),
s_load_b32(s[0], s[0:2], NULL),
s_waitcnt(simm16=waitcnt(lgkmcnt=0)),
global_store_b32(addr=v[0:1], data=v[2], saddr=OFF),
global_store_b32(addr=v[0:2], data=v[2], saddr=OFF),
s_endpgm(),
]
disasm = assemble_and_disassemble(instructions)
@@ -109,12 +108,12 @@ class TestIntegration(unittest.TestCase):
v_mov_b32_e32(v[0], s[0]), # base addr low
v_mov_b32_e32(v[1], s[1]), # base addr high
# Load value
global_load_b32(vdst=v[2], addr=v[0:1], saddr=OFF),
global_load_b32(vdst=v[2], addr=v[0:2], saddr=OFF),
s_waitcnt(simm16=waitcnt(vmcnt=0)),
# Add 1.0
v_add_f32_e32(v[2], 1.0, v[2]),
# Store result
global_store_b32(addr=v[0:1], data=v[2], saddr=OFF),
global_store_b32(addr=v[0:2], data=v[2], saddr=OFF),
s_endpgm(),
]
disasm = assemble_and_disassemble(instructions)
@@ -149,6 +148,79 @@ class TestIntegration(unittest.TestCase):
return
self.fail("Could not find s_mov_b32 in disassembly")
class TestAsm(unittest.TestCase):
"""Test asm() string parsing."""
def test_asm_basic(self):
"""Test basic instruction parsing."""
inst = asm('s_mov_b32 s0, s1')
self.assertEqual(inst.to_bytes(), s_mov_b32(s[0], s[1]).to_bytes())
def test_asm_with_immediates(self):
"""Test parsing with immediate values."""
inst = asm('s_add_u32 s0, s1, 10')
self.assertEqual(inst.to_bytes(), s_add_u32(s[0], s[1], 10).to_bytes())
def test_asm_float_const(self):
"""Test parsing float constants."""
inst = asm('v_mul_f32_e32 v0, 1.0, v1')
self.assertEqual(inst.to_bytes(), v_mul_f32_e32(v[0], 1.0, v[1]).to_bytes())
def test_asm_hex_immediate(self):
"""Test parsing hex immediates."""
inst = asm('s_waitcnt 0xfc07')
self.assertEqual(inst.to_bytes(), s_waitcnt(simm16=0xfc07).to_bytes())
def test_asm_special_regs(self):
"""Test parsing special registers."""
inst = asm('s_mov_b32 s0, vcc_lo')
self.assertEqual(inst.to_bytes(), s_mov_b32(s[0], VCC_LO).to_bytes())
def test_asm_register_range(self):
"""Test parsing register ranges."""
inst = asm('s_load_b128 s[4:7], s[0:1], null')
self.assertEqual(inst.to_bytes(), s_load_b128(s[4:7], s[0:1], NULL).to_bytes())
def test_asm_matches_llvm(self):
"""Test asm() output matches LLVM assembler."""
from tinygrad.runtime.support.compiler_amd import HIPCompiler
compiler = HIPCompiler('gfx1100')
def get_llvm_bytes(instr: str) -> bytes:
src = f'.text\n.globl test\n.p2align 8\n.type test,@function\ntest:\n{instr}\n'
lib = compiler.compile(src)
raw = disassemble(lib)
for line in raw.splitlines():
if instr.split()[0] in line and '//' in line:
hex_str = line.split('//')[1].strip().split(':')[1].strip()
return bytes.fromhex(hex_str)[::-1]
return b''
tests = ['s_mov_b32 s0, s1', 's_endpgm', 'v_add_f32_e32 v0, v1, v2']
for t in tests:
self.assertEqual(asm(t).to_bytes(), get_llvm_bytes(t), f"mismatch for: {t}")
def test_asm_vop3_modifiers(self):
"""Test asm() with VOP3 modifiers (neg, abs, clamp)."""
def get_llvm_encoding(instr: str) -> str:
result = subprocess.run([get_llvm_mc(), '-triple=amdgcn', '-mcpu=gfx1100', '-show-encoding'],
input=instr, capture_output=True, text=True)
if m := re.search(r'encoding:\s*\[(.*?)\]', result.stdout):
return m.group(1).replace('0x','').replace(',','').replace(' ','')
return ''
tests = [
'v_fma_f32 v0, -v1, v2, v3', # neg on src0
'v_fma_f32 v0, v1, |v2|, v3', # abs on src1
'v_fma_f32 v0, v1, v2, v3 clamp', # clamp
'v_fma_f32 v0, -v1, |v2|, v3 clamp', # all modifiers
'v_fma_f32 v0, -|v1|, v2, v3', # neg+abs on same operand
]
for t in tests:
our_hex = asm(t).to_bytes().hex()
llvm_hex = get_llvm_encoding(t)
self.assertEqual(our_hex, llvm_hex, f"mismatch for: {t}")
class TestTinygradIntegration(unittest.TestCase):
"""Test that we can parse disassembled tinygrad kernels."""
+42 -81
View File
@@ -1,47 +1,36 @@
#!/usr/bin/env python3
"""Test AMD assembler/disassembler against LLVM test vectors.
Only compute-relevant instruction formats are tested. Graphics-only formats not supported:
- MUBUF/MTBUF: buffer instructions with resource descriptors (use GLOBAL/FLAT instead)
- MIMG: image/texture instructions
- EXP/VEXPORT: export instructions for pixel/vertex output
- VIMAGE/VSAMPLE: image sampling instructions (RDNA4)
- VBUFFER: buffer instructions (RDNA4)
"""
"""Test AMD assembler/disassembler against LLVM test vectors."""
import unittest, re, subprocess, functools
from tinygrad.helpers import fetch
from extra.assembly.amd.disasm import disasm
from extra.assembly.amd.decode import decode_inst, detect_format
from extra.assembly.amd.test.helpers import get_llvm_mc, get_target, get_mattr
from extra.assembly.amd.asm import asm, disasm, detect_format
from extra.assembly.amd.test.helpers import get_llvm_mc
LLVM_BASE = "https://raw.githubusercontent.com/llvm/llvm-project/llvmorg-21.1.0/llvm/test/MC/AMDGPU"
# RDNA3 (gfx11) test files for compute instructions
# Excluded: gfx11_asm_mubuf.s, gfx11_asm_mtbuf.s, gfx11_asm_mimg.s, gfx11_asm_mubuf_alias.s, gfx11_asm_mtbuf_alias.s (graphics-only)
RDNA_FILES = ['gfx11_asm_sop1.s', 'gfx11_asm_sop2.s', 'gfx11_asm_sopp.s', 'gfx11_asm_sopk.s', 'gfx11_asm_sopc.s',
'gfx11_asm_vop1.s', 'gfx11_asm_vop2.s', 'gfx11_asm_vopc.s', 'gfx11_asm_vop3.s', 'gfx11_asm_vop3p.s', 'gfx11_asm_vinterp.s',
'gfx11_asm_vopd.s', 'gfx11_asm_vopcx.s', 'gfx11_asm_vop3_from_vop1.s', 'gfx11_asm_vop3_from_vop2.s', 'gfx11_asm_vop3_from_vopc.s',
'gfx11_asm_vop3_from_vopcx.s', 'gfx11_asm_ds.s', 'gfx11_asm_smem.s', 'gfx11_asm_flat.s',
'gfx11_asm_wmma.s', 'gfx11_asm_vop3_features.s', 'gfx11_asm_vop3p_features.s', 'gfx11_asm_vopd_features.s',
'gfx11_asm_vop3_from_vopcx.s', 'gfx11_asm_ds.s', 'gfx11_asm_smem.s', 'gfx11_asm_flat.s', 'gfx11_asm_mubuf.s', 'gfx11_asm_mtbuf.s',
'gfx11_asm_mimg.s', 'gfx11_asm_wmma.s', 'gfx11_asm_vop3_features.s', 'gfx11_asm_vop3p_features.s', 'gfx11_asm_vopd_features.s',
'gfx11_asm_vop3_alias.s', 'gfx11_asm_vop3p_alias.s', 'gfx11_asm_vopc_alias.s', 'gfx11_asm_vopcx_alias.s', 'gfx11_asm_vinterp_alias.s',
'gfx11_asm_smem_alias.s']
# CDNA (gfx9/gfx90a/gfx942/gfx950) test files for compute instructions
# Excluded: gfx9_asm_mubuf.s, gfx9_asm_mtbuf.s, gfx90a_ldst_acc.s (has MIMG mixed in)
# Exclude gfx90a: 'gfx90a_asm_features.s', 'mai-gfx90a.s',
# Exclude gfx950: 'gfx950_asm_features.s' (disasm error)
'gfx11_asm_smem_alias.s', 'gfx11_asm_mubuf_alias.s', 'gfx11_asm_mtbuf_alias.s']
# CDNA test files - includes gfx9 files for shared instructions, plus gfx90a/gfx942 specific files
# gfx90a_ldst_acc.s has MIMG mixed in, filtered via is_mimg check
CDNA_FILES = ['gfx9_asm_sop1.s', 'gfx9_asm_sop2.s', 'gfx9_asm_sopp.s', 'gfx9_asm_sopk.s', 'gfx9_asm_sopc.s',
'gfx9_asm_vop1.s', 'gfx9_asm_vop2.s', 'gfx9_asm_vopc.s', 'gfx9_asm_vop3.s', 'gfx9_asm_vop3p.s',
'gfx9_asm_ds.s', 'gfx9_asm_flat.s', 'gfx9_asm_smem.s',
'flat-scratch-gfx942.s', 'gfx942_asm_features.s', 'mai-gfx942.s',
'gfx950_asm_vop1.s', 'gfx950_asm_read_tr.s', 'mai-gfx950.s']
# RDNA4 (gfx12) test files for compute instructions
# Excluded: gfx12_asm_vbuffer_mubuf.s, gfx12_asm_vbuffer_mtbuf.s, gfx12_asm_exp.s (graphics-only)
'gfx9_asm_ds.s', 'gfx9_asm_flat.s', 'gfx9_asm_smem.s', 'gfx9_asm_mubuf.s', 'gfx9_asm_mtbuf.s',
'gfx90a_ldst_acc.s', 'gfx90a_asm_features.s', 'flat-scratch-gfx942.s', 'gfx942_asm_features.s',
'mai-gfx90a.s', 'mai-gfx942.s']
# RDNA4 (gfx12) test files - excludes alias/err/fake16/dpp files, and vimage/vsample (not supported)
# NOTE: vflat/vdsdir excluded - not implemented; features.s has mixed formats
RDNA4_FILES = ['gfx12_asm_sop1.s', 'gfx12_asm_sop2.s', 'gfx12_asm_sopp.s', 'gfx12_asm_sopk.s', 'gfx12_asm_sopc.s',
'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_wmma_w32.s']
'gfx12_asm_vbuffer_mubuf.s', 'gfx12_asm_vbuffer_mtbuf.s', 'gfx12_asm_wmma_w32.s', 'gfx12_asm_exp.s']
def _is_mimg(data: bytes) -> bool: return (int.from_bytes(data[:4], 'little') >> 26) & 0x3f == 0b111100
def _parse_llvm_tests(text: str, pattern: str) -> list[tuple[str, bytes]]:
tests = []
@@ -59,101 +48,73 @@ def _parse_llvm_tests(text: str, pattern: str) -> list[tuple[str, bytes]]:
except ValueError: pass
return tests
def _get_tests_uncached(f: str, arch: str) -> list[tuple[str, bytes]]:
@functools.cache
def _get_tests(f: str, arch: str) -> list[tuple[str, bytes]]:
text = fetch(f"{LLVM_BASE}/{f}").read_bytes().decode('utf-8', errors='ignore')
if arch == "rdna3":
# Match GFX11 and W32 only (wavefront32 mode)
tests = _parse_llvm_tests(text, r'(?:GFX11|W32)')
tests = _parse_llvm_tests(text, r'(?:GFX11|W32|W64)')
elif arch == "rdna4":
# Match GFX12 (but not GFX1250) and W32 only (wavefront32 mode)
tests = _parse_llvm_tests(text, r'(?:GFX12(?!50)|W32)')
elif 'gfx90a' in f or 'gfx942' in f or 'gfx950' in f:
tests = _parse_llvm_tests(text, r'(?:GFX90A|GFX942|GFX950)')
# Match GFX12 but not GFX1250 (which has different lit64 encoding)
tests = _parse_llvm_tests(text, r'(?:GFX12(?!50)|W32|W64)')
elif 'gfx90a' in f or 'gfx942' in f:
tests = _parse_llvm_tests(text, r'(?:GFX90A|GFX942)')
else:
tests = _parse_llvm_tests(text, r'(?:VI9|GFX9|CHECK)')
# Exclude v_interp_* (graphics-only, not on CDNA)
if arch == "cdna": tests = [(asm, data) for asm, data in tests if not asm.startswith('v_interp_')]
# Filter out tests where original ASM isn't valid on target (e.g., gfx9 tests with gfx942/gfx950 constraints)
if arch == "cdna" and not ('gfx942' in f or 'gfx950' in f or 'gfx90a' in f): tests = _filter_valid_asm(tests, arch)
return tests
return [(a, d) for a, d in tests if not _is_mimg(d)] if arch == "cdna" else tests
@functools.cache
def _get_tests(f: str, arch: str) -> list[tuple[str, bytes]]: return _get_tests_uncached(f, arch)
def _compile_asm_batch(instrs: list[str], arch: str = "rdna3", mcpu: str|None = None) -> list[bytes]:
def _compile_asm_batch(instrs: list[str], arch: str = "rdna3") -> list[bytes]:
if not instrs: return []
mcpu, mattr = mcpu or get_target(arch), get_mattr(arch)
result = subprocess.run([get_llvm_mc(), '-triple=amdgcn', f'-mcpu={mcpu}', f'-mattr={mattr}', '-show-encoding'],
mcpu = {'rdna3': 'gfx1100', 'rdna4': 'gfx1200'}.get(arch, 'gfx1100')
result = subprocess.run([get_llvm_mc(), '-triple=amdgcn', f'-mcpu={mcpu}', '-mattr=+real-true16,+wavefrontsize32', '-show-encoding'],
input=".text\n" + "\n".join(instrs) + "\n", capture_output=True, text=True, timeout=30)
if result.returncode != 0: raise RuntimeError(f"llvm-mc failed: {result.stderr.strip()}")
return [bytes.fromhex(line.split('encoding:')[1].strip()[1:-1].replace('0x', '').replace(',', '').replace(' ', ''))
for line in result.stdout.split('\n') if 'encoding:' in line]
def _filter_valid_asm(tests: list[tuple[str, bytes]], arch: str) -> list[tuple[str, bytes]]:
"""Filter out tests where the original ASM isn't valid on the target (e.g., gfx9 tests with gfx942/gfx950 constraints)."""
if not tests: return []
mcpu = get_target(arch)
# Batch assemble all instructions, parse stderr to find which lines failed
instrs = [asm for asm, _ in tests]
result = subprocess.run([get_llvm_mc(), '-triple=amdgcn', f'-mcpu={mcpu}', '-show-encoding'],
input=".text\n" + "\n".join(instrs) + "\n", capture_output=True, text=True, timeout=30)
# Parse error lines from stderr (format: "<stdin>:N:..." where N is 1-indexed, line 1 is ".text")
failed_lines = set()
for line in result.stderr.split('\n'):
if m := re.match(r'<stdin>:(\d+):', line): failed_lines.add(int(m.group(1)) - 1) # -1 for .text, so line 2 -> index 1 -> tests[0]
# Also filter out tests where LLVM roundtrip doesn't match original (reserved bits set in original)
valid = [(asm, data) for i, (asm, data) in enumerate(tests) if (i + 1) not in failed_lines]
if not valid: return []
llvm_result = subprocess.run([get_llvm_mc(), '-triple=amdgcn', f'-mcpu={mcpu}', '-show-encoding'],
input=".text\n" + "\n".join(asm for asm, _ in valid) + "\n", capture_output=True, text=True, timeout=30)
llvm_bytes = [bytes.fromhex(line.split('encoding:')[1].strip()[1:-1].replace('0x', '').replace(',', '').replace(' ', ''))
for line in llvm_result.stdout.split('\n') if 'encoding:' in line]
return [(asm, data) for (asm, data), lb in zip(valid, llvm_bytes) if lb == data]
def _make_test(f: str, arch: str, test_type: str):
def test(self):
tests = _get_tests(f, arch)
name = f"{arch}_{test_type}_{f}"
mcpu = "gfx942" if arch == "cdna" and "gfx942" in f else get_target(arch)
if test_type == "roundtrip":
passed, skipped = 0, 0
for _, data in tests:
decoded = detect_format(data, arch).from_bytes(data)
self.assertEqual(decoded.to_bytes()[:len(data)], data)
print(f"{name}: {len(tests)} passed")
elif test_type == "asm":
passed, skipped = 0, 0
for asm_text, expected in tests:
try:
decoded = detect_format(data, arch).from_bytes(data)
self.assertEqual(decoded.to_bytes()[:len(data)], data)
self.assertEqual(asm(asm_text, arch).to_bytes(), expected)
passed += 1
except ValueError: skipped += 1 # skip invalid opcodes not in enum
except: skipped += 1
print(f"{name}: {passed} passed, {skipped} skipped")
self.assertEqual(skipped, 0, f"{name}: {skipped} tests skipped, expected 0")
elif test_type == "disasm":
to_test = []
for _, data in tests:
try:
decoded = decode_inst(data, arch)
enc = decoded.to_bytes()[:len(data)]
decoded = detect_format(data, arch).from_bytes(data)
# Skip if roundtrip fails, disasm fails, or op_name is missing (disasm starts with space)
if enc == data and (d := disasm(decoded)) and not d.startswith(' '): to_test.append((enc, d))
if decoded.to_bytes()[:len(data)] == data and (d := disasm(decoded)) and not d.startswith(' '): to_test.append((data, d))
except: pass
skipped = len(tests) - len(to_test)
print(f"{name}: {len(to_test)} passed, {skipped} skipped")
self.assertEqual(skipped, 0, f"{name}: {skipped} tests skipped, expected 0")
# Compare disasm->reassemble with original encoding (filter reserved bit cases where LLVM can't reproduce)
llvm_bytes = _compile_asm_batch([t[1] for t in to_test], arch, mcpu)
valid = [(enc, d, llvm) for (enc, d), llvm in zip(to_test, llvm_bytes) if llvm == enc]
print(f"{name}: {len(valid)}/{len(to_test)} matched LLVM encoding")
for enc, _, llvm in valid: self.assertEqual(llvm, enc)
if arch in ("rdna3", "rdna4"):
self.assertEqual(skipped, 0, f"{name}: {skipped} tests skipped, expected 0")
for (data, _), llvm in zip(to_test, _compile_asm_batch([t[1] for t in to_test], arch)): self.assertEqual(llvm, data)
return test
class TestLLVM(unittest.TestCase): pass
for f in RDNA_FILES:
setattr(TestLLVM, f"test_rdna3_roundtrip_{f.replace('.s', '').replace('-', '_')}", _make_test(f, "rdna3", "roundtrip"))
setattr(TestLLVM, f"test_rdna3_asm_{f.replace('.s', '').replace('-', '_')}", _make_test(f, "rdna3", "asm"))
setattr(TestLLVM, f"test_rdna3_disasm_{f.replace('.s', '').replace('-', '_')}", _make_test(f, "rdna3", "disasm"))
for f in CDNA_FILES:
setattr(TestLLVM, f"test_cdna_roundtrip_{f.replace('.s', '').replace('-', '_')}", _make_test(f, "cdna", "roundtrip"))
setattr(TestLLVM, f"test_cdna_disasm_{f.replace('.s', '').replace('-', '_')}", _make_test(f, "cdna", "disasm"))
for f in RDNA4_FILES:
setattr(TestLLVM, f"test_rdna4_roundtrip_{f.replace('.s', '').replace('-', '_')}", _make_test(f, "rdna4", "roundtrip"))
setattr(TestLLVM, f"test_rdna4_asm_{f.replace('.s', '').replace('-', '_')}", _make_test(f, "rdna4", "asm"))
setattr(TestLLVM, f"test_rdna4_disasm_{f.replace('.s', '').replace('-', '_')}", _make_test(f, "rdna4", "disasm"))
if __name__ == "__main__":
+3 -3
View File
@@ -5,12 +5,12 @@ from extra.assembly.amd.pcode import (Reg, TypedView, TypedView, MASK32, MASK64,
_f32, _i32, _f16, _i16, f32_to_f16, isNAN, _bf16, _ibf16, bf16_to_f32, f32_to_bf16,
BYTE_PERMUTE, v_sad_u8, v_msad_u8, _compile_pseudocode, _expr, compile_pseudocode)
from extra.assembly.amd.test.helpers import ExecContext
from extra.assembly.amd.autogen.rdna3.str_pcode import PCODE
from extra.assembly.amd.autogen.rdna3.str_pcode import VOP3SDOp_PCODE, VOPCOp_PCODE
from extra.assembly.amd.autogen.rdna3.enum import VOP3SDOp, VOPCOp
# Compile pseudocode functions on demand for regression tests
_VOP3SDOp_V_DIV_SCALE_F32 = compile_pseudocode('VOP3SDOp', 'V_DIV_SCALE_F32', PCODE[VOP3SDOp.V_DIV_SCALE_F32])
_VOPCOp_V_CMP_CLASS_F32 = compile_pseudocode('VOPCOp', 'V_CMP_CLASS_F32', PCODE[VOPCOp.V_CMP_CLASS_F32_E32])
_VOP3SDOp_V_DIV_SCALE_F32 = compile_pseudocode('VOP3SDOp', 'V_DIV_SCALE_F32', VOP3SDOp_PCODE[VOP3SDOp.V_DIV_SCALE_F32])
_VOPCOp_V_CMP_CLASS_F32 = compile_pseudocode('VOPCOp', 'V_CMP_CLASS_F32', VOPCOp_PCODE[VOPCOp.V_CMP_CLASS_F32])
class TestReg(unittest.TestCase):
def test_u32_read(self):
+42 -19
View File
@@ -1,29 +1,51 @@
#!/usr/bin/env python3
"""Test PDF pseudocode extraction from amdxml.py."""
import unittest
from extra.assembly.amd.amdxml import extract_pdf_text, extract_pcode, parse_xml, ARCHS, FIXES
"""Test pdf.py PDF parser and enum generation."""
import unittest, tempfile, importlib.util
from extra.assembly.amd.pdf import extract, extract_tables, extract_enums, extract_pcode, write_enums, PDF_URLS
EXPECTED_PAGES = {"rdna3": 655, "rdna4": 711, "cdna": 610}
EXPECTED = {
"rdna3": {"pages": 655, "tables": 115, "sop2_ops": 67, "sop2_first": "S_ADD_U32"},
"rdna4": {"pages": 711, "tables": 125, "sop2_ops": 74, "sop2_first": "S_ADD_CO_U32"},
"cdna": {"pages": 610, "tables": 104, "sop2_ops": 52, "sop2_first": "S_ADD_U32"},
}
class TestPcodePDF(unittest.TestCase):
class TestPDF2(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.pages = {arch: extract_pdf_text(cfg["pdf"]) for arch, cfg in ARCHS.items()}
cls.enums = {}
for arch, cfg in ARCHS.items():
_, enums, _, _, _, _ = parse_xml(cfg["xml"])
for fmt, ops in FIXES.get(arch, {}).items(): enums.setdefault(fmt, {}).update(ops)
cls.enums[arch] = enums
cls.pcode = {arch: extract_pcode(cls.pages[arch], {n: op for ops in cls.enums[arch].values() for op, n in ops.items()}) for arch in ARCHS}
cls.data = {name: extract(url) for name, url in PDF_URLS.items()}
cls.tables = {name: extract_tables(pages) for name, pages in cls.data.items()}
cls.enums = {name: extract_enums(cls.tables[name]) for name in PDF_URLS}
cls.pcode = {name: extract_pcode(cls.data[name], cls.enums[name]) for name in PDF_URLS}
def test_page_counts(self):
for name, exp in EXPECTED_PAGES.items():
self.assertEqual(len(self.pages[name]), exp, f"{name} page count")
for name, exp in EXPECTED.items():
self.assertEqual(len(self.data[name]), exp["pages"], f"{name} page count")
def test_pcode_extracted(self):
"""Check we extracted a reasonable number of pcode entries."""
for name in ARCHS:
self.assertGreater(len(self.pcode[name]), 500, f"{name} pcode count too low")
def test_table_counts(self):
for name, exp in EXPECTED.items():
self.assertEqual(len(self.tables[name]), exp["tables"], f"{name} table count")
def test_tables_sequential(self):
for name in PDF_URLS:
nums = sorted(self.tables[name].keys())
missing = set(range(1, max(nums) + 1)) - set(nums)
self.assertEqual(missing, set(), f"{name} missing tables: {missing}")
def test_generate_enums(self):
for name, exp in EXPECTED.items():
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
write_enums(self.enums[name], name, f.name)
spec = importlib.util.spec_from_file_location("enum", f.name)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
# Check SOP2Op
self.assertTrue(hasattr(mod, 'SOP2Op'), f"{name} missing SOP2Op")
self.assertEqual(len(mod.SOP2Op), exp["sop2_ops"], f"{name} SOP2Op count")
self.assertEqual(mod.SOP2Op(0).name, exp["sop2_first"], f"{name} SOP2Op first")
# Check all enums have at least 2 ops
for attr in dir(mod):
if attr.endswith('Op'):
self.assertGreaterEqual(len(getattr(mod, attr)), 2, f"{name} {attr} has too few ops")
def test_pcode_rdna3_tricky(self):
"""Test specific pseudocode patterns that are tricky to extract correctly."""
@@ -40,8 +62,9 @@ class TestPcodePDF(unittest.TestCase):
def test_pcode_no_examples(self):
"""Pseudocode should not contain example lines with '=>'."""
for name in ARCHS:
for name in PDF_URLS:
for (op_name, opcode), code in self.pcode[name].items():
# No example lines (test vectors like "S_CTZ_I32_B32(0xaaaaaaaa) => 1")
self.assertNotIn('=>', code, f"{name} {op_name} contains example line with '=>'")
if __name__ == "__main__":
+2 -2
View File
@@ -37,10 +37,10 @@ class TestRDNA3Asm(unittest.TestCase):
v_add_co_u32(v[2], VCC_LO, s[6], v[2]),
v_add_co_ci_u32_e32(v[3], s[7], v[3]),
v_add_co_u32(v[0], VCC_LO, s[4], v[0]),
global_load_b32(vdst=v[2], addr=v[2:3], saddr=OFF),
global_load_b32(vdst=v[2], addr=v[2], saddr=OFF),
v_add_co_ci_u32_e32(v[1], s[5], v[1]),
s_waitcnt(0x03f7), # vmcnt(0)
global_store_b32(addr=v[0:1], data=v[2], saddr=OFF),
global_store_b32(addr=v[0], data=v[2], saddr=OFF),
s_endpgm(),
]
+36 -23
View File
@@ -2,8 +2,15 @@
"""Roundtrip tests: generate tinygrad kernels, decode instructions, re-encode, verify match."""
import unittest, io, sys, re, subprocess, os
from extra.assembly.amd.dsl import Inst
from extra.assembly.amd.decode import decode_inst, detect_format
from extra.assembly.amd.test.helpers import get_llvm_mc, get_llvm_objdump, get_target, get_mattr
from extra.assembly.amd.asm import asm, detect_format
from extra.assembly.amd.test.helpers import get_llvm_mc, get_llvm_objdump
# arch: (mcpu, mattr)
ARCH_CONFIG = {
'rdna3': ('gfx1100', '+real-true16,+wavefrontsize32'),
'rdna4': ('gfx1200', '+real-true16,+wavefrontsize32'),
'cdna': ('gfx942', '+wavefrontsize64'),
}
def disassemble_lib(lib: bytes, compiler) -> list[tuple[str, bytes]]:
"""Disassemble ELF binary and return list of (instruction_text, machine_code_bytes)."""
@@ -35,7 +42,8 @@ def compile_asm(instr: str, arch: str = 'rdna3') -> bytes:
def compile_asm_batch(instrs: list[str], arch: str = 'rdna3') -> list[bytes]:
"""Compile multiple instructions with a single llvm-mc call."""
if not instrs: return []
result = subprocess.run([get_llvm_mc(), '-triple=amdgcn', f'-mcpu={get_target(arch)}', f'-mattr={get_mattr(arch)}', '-show-encoding'],
mcpu, mattr = ARCH_CONFIG[arch]
result = subprocess.run([get_llvm_mc(), '-triple=amdgcn', f'-mcpu={mcpu}', f'-mattr={mattr}', '-show-encoding'],
input=".text\n" + "\n".join(instrs) + "\n", capture_output=True, text=True)
if result.returncode != 0: raise RuntimeError(f"llvm-mc batch failed: {result.stderr.strip()}")
encodings = []
@@ -51,7 +59,7 @@ def compile_and_disasm_batch(instrs: list[str], arch: str = 'rdna3') -> list[str
"""Compile instructions with LLVM and get LLVM's disassembly."""
import tempfile
if not instrs: return []
mcpu, mattr = get_target(arch), get_mattr(arch)
mcpu, mattr = ARCH_CONFIG[arch]
src = ".text\n.globl test\n.p2align 8\n.type test,@function\ntest:\n" + "\n".join(f" {instr}" for instr in instrs) + "\n"
with tempfile.NamedTemporaryFile(suffix='.o', delete=False) as f:
obj_path = f.name
@@ -77,16 +85,17 @@ class TestTinygradKernelRoundtrip(unittest.TestCase):
def _test_kernel_roundtrip(self, op_fn):
"""Generate kernel from op_fn, test:
1. decode -> reencode matches original bytes
2. disasm() -> LLVM asm -> bytes matches original (validates disasm correctness)
3. our disasm() matches LLVM's disassembly string (informational)
2. asm(disasm()) matches LLVM output
3. our disasm() matches LLVM's disassembly string exactly
"""
arch = self.arch
mcpu, mattr = ARCH_CONFIG[arch]
from extra.assembly.amd.test.test_compare_emulators import get_kernels_from_tinygrad
from tinygrad.runtime.support.compiler_amd import HIPCompiler
kernels, _, _ = get_kernels_from_tinygrad(op_fn)
compiler = HIPCompiler(get_target(arch))
compiler = HIPCompiler(mcpu)
# First pass: decode all instructions and collect info
decoded_instrs: list[tuple] = [] # list of (ki, offset, orig_bytes, decoded, our_disasm, decode_ok, decode_err)
@@ -120,19 +129,19 @@ class TestTinygradKernelRoundtrip(unittest.TestCase):
offset += size
# Collect disasm strings for batched LLVM calls - skip unknown opcodes (op_X) that LLVM can't compile
asm_test_instrs: list[tuple[int, str, bytes]] = [] # (idx, our_disasm, orig_bytes) for asm test
asm_test_instrs: list[tuple[int, str]] = [] # (idx, our_disasm) for asm test
disasm_test_instrs: list[tuple[int, str]] = [] # (idx, our_disasm) for disasm comparison test
for idx, (ki, offset, orig_bytes, decoded, our_disasm, decode_ok, decode_err) in enumerate(decoded_instrs):
if our_disasm is None: continue
# Skip unknown opcodes and malformed instructions
# Skip unknown opcodes and malformed instructions for both tests
if our_disasm.startswith('op_') or re.search(r', \d+, \d+, \d+,', our_disasm): continue
asm_test_instrs.append((idx, our_disasm, orig_bytes))
asm_test_instrs.append((idx, our_disasm))
disasm_test_instrs.append((idx, our_disasm))
# Batch compile for asm test (our disasm -> LLVM asm -> bytes)
asm_llvm_results = compile_asm_batch([d for _, d, _ in asm_test_instrs], arch)
asm_llvm_map = {idx: (result, orig) for (idx, _, orig), result in zip(asm_test_instrs, asm_llvm_results)}
# Batch compile for asm test
asm_llvm_results = compile_asm_batch([d for _, d in asm_test_instrs], arch)
asm_llvm_map = {idx: result for (idx, _), result in zip(asm_test_instrs, asm_llvm_results)}
# Batch compile+disasm for disasm comparison test
disasm_llvm_results = compile_and_disasm_batch([d for _, d in disasm_test_instrs], arch)
@@ -156,16 +165,20 @@ class TestTinygradKernelRoundtrip(unittest.TestCase):
decode_failed += 1
decode_failures.append(f"K{ki}@{offset}: {our_disasm}: {decode_err}")
# Asm test: our disasm -> LLVM asm -> compare bytes with original
# Asm test
if our_disasm is None:
asm_skipped += 1
elif idx in asm_llvm_map:
llvm_bytes, orig = asm_llvm_map[idx]
if llvm_bytes == orig[:len(llvm_bytes)]:
asm_passed += 1
else:
asm_failed += 1
asm_failures.append(f"K{ki}@{offset}: '{our_disasm}': llvm={llvm_bytes.hex()} orig={orig[:len(llvm_bytes)].hex()}")
llvm_bytes = asm_llvm_map[idx]
try:
our_bytes = asm(our_disasm).to_bytes()
if our_bytes[:len(llvm_bytes)] == llvm_bytes:
asm_passed += 1
else:
asm_failed += 1
asm_failures.append(f"K{ki}@{offset}: '{our_disasm}': ours={our_bytes[:len(llvm_bytes)].hex()} llvm={llvm_bytes.hex()}")
except Exception:
asm_skipped += 1
else:
asm_skipped += 1
@@ -183,7 +196,7 @@ class TestTinygradKernelRoundtrip(unittest.TestCase):
disasm_skipped += 1
print(f"[{arch}] decode roundtrip: {decode_passed} passed, {decode_failed} failed, {decode_skipped} skipped")
print(f"[{arch}] asm via llvm: {asm_passed} passed, {asm_failed} failed, {asm_skipped} skipped")
print(f"[{arch}] asm vs llvm: {asm_passed} passed, {asm_failed} failed, {asm_skipped} skipped")
print(f"[{arch}] disasm vs llvm: {disasm_passed} passed, {disasm_failed} failed, {disasm_skipped} skipped")
self.assertEqual(decode_failed, 0, f"Decode failures:\n" + "\n".join(decode_failures[:20]))
self.assertEqual(asm_failed, 0, f"Asm failures:\n" + "\n".join(asm_failures[:20]))
@@ -234,10 +247,10 @@ class TestTinygradKernelRoundtrip(unittest.TestCase):
# Fused ops
def test_fma(self): self._test_kernel_roundtrip(lambda T: (T([1.0, 2.0]) * T([3.0, 4.0]) + T([5.0, 6.0])))
@unittest.skip("RDNA4 decode roundtrip not yet supported")
@unittest.skip("no asm support for RDNA4")
class TestTinygradKernelRoundtripRDNA4(TestTinygradKernelRoundtrip): arch = 'rdna4'
@unittest.skip("CDNA decode roundtrip not yet supported")
@unittest.skip("no asm support for CDNA")
class TestTinygradKernelRoundtripCDNA(TestTinygradKernelRoundtrip): arch = 'cdna'
if __name__ == "__main__":
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Tests for SQTT packet codec (no hardware required)."""
import unittest
from extra.assembly.amd.sqtt import (
LAYOUT_HEADER, WAVESTART, WAVEEND, INST, NOP,
decode, encode, PACKET_TYPES, OPCODE_TO_CLASS
)
class TestSQTTCodec(unittest.TestCase):
"""Tests for SQTT encoder/decoder roundtrip."""
def test_roundtrip_simple(self):
"""Test encode/decode roundtrip for simple packets."""
test_packets = [
LAYOUT_HEADER.from_raw(0x100),
WAVESTART.from_raw(0x0),
INST.from_raw(0x10), # delta=1
INST.from_raw(0x10), # delta=1
WAVEEND.from_raw(0x40), # delta=2
]
encoded = encode(test_packets)
decoded = decode(encoded)
self.assertGreaterEqual(len(decoded), len(test_packets))
for i, (orig, dec) in enumerate(zip(test_packets, decoded)):
self.assertEqual(type(orig), type(dec), f"type mismatch at {i}")
def test_decode_empty(self):
"""Test decoding empty data."""
packets = decode(b'')
self.assertEqual(packets, [])
def test_encode_empty(self):
"""Test encoding empty list."""
data = encode([])
self.assertEqual(data, b'')
def test_all_packet_types_have_encoding(self):
"""All packet types should have an encoding defined."""
for pkt_cls in PACKET_TYPES:
self.assertIsNotNone(pkt_cls._encoding, f"{pkt_cls.__name__} missing encoding")
def test_packet_from_raw(self):
"""Test creating packets from raw values."""
# INST with wave=5, op=0x21, delta=2
raw = (0x21 << 13) | (5 << 8) | (2 << 4) | 0b010
pkt = INST.from_raw(raw)
self.assertEqual(pkt.wave, 5)
self.assertEqual(pkt.op, 0x21)
self.assertEqual(pkt.delta, 2)
class TestDecodeRealBlob(unittest.TestCase):
"""Test decoding real SQTT blobs from examples."""
def test_decode_example_file(self):
"""Test decoding a real SQTT blob from examples."""
import pickle
from pathlib import Path
example_path = Path(__file__).parent.parent.parent.parent / "sqtt/examples/profile_plus_run_0.pkl"
if not example_path.exists():
self.skipTest(f"Example file not found: {example_path}")
from tinygrad.runtime.ops_amd import ProfileSQTTEvent
with open(example_path, "rb") as f:
data = pickle.load(f)
sqtt_events = [e for e in data if isinstance(e, ProfileSQTTEvent)]
self.assertGreater(len(sqtt_events), 0, "No SQTT events in example")
packets = decode(sqtt_events[0].blob)
self.assertGreater(len(packets), 0, "No packets decoded")
# First packet should be LAYOUT_HEADER
self.assertIsInstance(packets[0], LAYOUT_HEADER)
if __name__ == "__main__":
unittest.main()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,545 @@
#!/usr/bin/env python3
"""Tests for SQTT emulator correctness against known hardware patterns.
NOTE: This file only tests NOP and VALU behavior. For WMMA/DP/trans tests,
see test_sqtt_compare.py.
Run emulator tests: PYTHONPATH="." python3 extra/assembly/amd/test/test_sqtt_correct.py
Run hardware tests: SQTT_HW=1 PYTHONPATH="." python3 extra/assembly/amd/test/test_sqtt_correct.py
"""
import os
import unittest
USE_HW = os.environ.get("SQTT_HW", "0") == "1"
if USE_HW:
os.environ["SQTT"] = "1"
os.environ["PROFILE"] = "1"
os.environ["SQTT_LIMIT_SE"] = "2"
os.environ["SQTT_TOKEN_EXCLUDE"] = "3784"
from extra.assembly.amd.emu import SQTTState, decode_program, exec_wave, WaveState, LDSMem
from extra.assembly.amd.sqtt import WAVESTART, WAVEEND
from extra.assembly.amd.autogen.rdna3.ins import v_mov_b32_e32, v_add_f32_e32, s_nop, s_endpgm, s_delay_alu
from extra.assembly.amd.dsl import v
def assemble(instructions: list) -> bytes:
return b''.join(inst.to_bytes() for inst in instructions)
def wrap_with_nops(instructions: list, nops=16) -> list:
return instructions + [s_nop(0)]*nops + [s_endpgm()]
def get_wave_packets(packets: list) -> list:
result, in_wave = [], False
for p in packets:
if isinstance(p, WAVESTART) and p.simd == 0:
in_wave, result = True, [p]
elif in_wave:
result.append(p)
if isinstance(p, WAVEEND): break
return result
def get_timing_deltas(packets: list) -> list[tuple[str, int]]:
skip_types = {"NOP", "TS_DELTA_SHORT", "TS_WAVE_STATE", "TS_DELTA_OR_MARK", "TS_DELTA_S5_W2", "TS_DELTA_S5_W3", "TS_DELTA_S8_W3", "REG"}
filtered = [p for p in packets if type(p).__name__ not in skip_types]
if not filtered: return []
result = [(type(filtered[0]).__name__, 0)]
for i in range(1, len(filtered)):
result.append((type(filtered[i]).__name__, filtered[i]._time - filtered[i-1]._time))
return result
def run_emulator(instructions: list) -> list:
code = assemble(instructions)
program = decode_program(code)
st = WaveState()
st.exec_mask = (1 << 32) - 1
lds = LDSMem(bytearray(65536))
trace = SQTTState(wave_id=0, simd=0, cu=0)
exec_wave(program, st, lds, 32, trace)
return get_wave_packets(trace.packets)
def get_all_waves(packets: list) -> list[list]:
"""Extract all WAVESTART..WAVEEND ranges on simd 0."""
waves, in_wave, current = [], False, []
for p in packets:
if isinstance(p, WAVESTART) and p.simd == 0:
in_wave, current = True, [p]
elif in_wave:
current.append(p)
if isinstance(p, WAVEEND):
waves.append(current)
in_wave, current = False, []
return waves
def run_hardware(instructions: list) -> list:
from extra.assembly.amd.test.test_sqtt_hw import compile_asm_sqtt, run_prg_sqtt_batch
from extra.assembly.amd.sqtt import decode
from collections import Counter
prg = compile_asm_sqtt(instructions, alu_only=True)
for _ in range(10):
blobs = run_prg_sqtt_batch(prg, n_runs=200)
# Extract all waves from all blobs
traces = []
for blob in blobs:
traces.extend(get_all_waves(decode(blob)))
if not traces:
continue
# Find most common pattern
delta_sets = [tuple(get_timing_deltas(t)) for t in traces]
most_common = Counter(delta_sets).most_common(1)[0][0]
for t in traces:
if tuple(get_timing_deltas(t)) == most_common:
return t
return []
def run_sqtt(instructions: list, nops: int = 16) -> list:
instructions = wrap_with_nops(instructions, nops=nops)
return run_hardware(instructions) if USE_HW else run_emulator(instructions)
def get_deltas(instructions: list) -> tuple[list[int], list[int]]:
"""Run and return (issue deltas, exec deltas).
Issue = IMMEDIATE + VALUINST, Exec = ALUEXEC.
Deltas are between consecutive packets of same stream."""
deltas = get_timing_deltas(run_sqtt(instructions))
time = 0
issue_times, exec_times = [], []
for ptype, delta in deltas:
time += delta
if ptype in ('IMMEDIATE', 'VALUINST'):
issue_times.append(time)
elif ptype == 'ALUEXEC':
exec_times.append(time)
issue = [issue_times[i] - issue_times[i-1] for i in range(1, len(issue_times))]
execd = [exec_times[i] - exec_times[i-1] for i in range(1, len(exec_times))]
return issue, execd
# ************************************ tests ************************************
class TestVALUChains(unittest.TestCase):
"""VALU dependency chains."""
def _chain(self, n, expected_issue, expected_exec):
instrs = [v_mov_b32_e32(v[0], 1.0)] + [v_add_f32_e32(v[i], v[i-1], v[i-1]) for i in range(1, n)]
issue, execd = get_deltas(instrs)
self.assertEqual(issue[:n-1], expected_issue)
if isinstance(expected_exec[0], list): self.assertIn(execd, expected_exec)
else: self.assertEqual(execd, expected_exec)
def test_chain_2(self): self._chain(2, [1], [6])
def test_chain_3(self): self._chain(3, [1, 1], [6, 5])
def test_chain_4(self): self._chain(4, [1, 1, 1], [6, 5, 5])
def test_chain_5(self): self._chain(5, [1, 1, 1, 1], [6, 5, 5, 9])
def test_chain_6(self): self._chain(6, [1, 1, 1, 1, 1], [6, 5, 5, 9, 9])
def test_chain_7(self): self._chain(7, [1, 1, 1, 1, 1, 1], [6, 5, 5, 5, 9, 9])
def test_chain_8(self): self._chain(8, [1, 1, 1, 1, 1, 1, 1], [6, 5, 5, 5, 9, 9, 9])
# NOTE: position 8 can be 5 or 9 depending on GPU variant
def test_chain_12(self): self._chain(12, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [[6, 5, 5, 5, 5, 9, 9, 9, 9, 9, 9], [6, 5, 5, 5, 5, 9, 9, 9, 5, 9, 9]])
def test_chain_14(self): self._chain(14, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [[6, 5, 5, 5, 5, 9, 9, 9, 9, 9, 9, 9, 9], [6, 5, 5, 5, 5, 9, 9, 9, 5, 9, 9, 9, 9]])
# issue stalls start here
def test_chain_15(self): self._chain(15, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], [[6, 5, 5, 5, 5, 5, 9, 9, 9, 9, 9, 9, 9, 9], [6, 5, 5, 5, 5, 5, 9, 9, 5, 9, 9, 9, 9, 9]])
def test_chain_16(self): self._chain(16, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 5], [[6, 5, 5, 5, 5, 5, 5, 9, 9, 9, 9, 9, 9, 9, 9], [6, 5, 5, 5, 5, 5, 5, 9, 5, 9, 9, 9, 9, 9, 9]])
def test_chain_18(self): self._chain(18, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 5, 5, 5], [6, 5, 5, 5, 5, 5, 5, 5, 5, 9, 9, 9, 9, 9, 9, 9, 9])
def test_chain_20(self): self._chain(20, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 5, 5, 5, 5, 5], [6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 9, 9, 9, 9, 9, 9, 9, 9])
class TestVALUChainsWithWarmup(unittest.TestCase):
"""VALU dependency chains with early VALUs to isolate warmup effects."""
# just the first stupid VALU takes 6
def _chain(self, n, warmup=True):
instrs = [v_mov_b32_e32(v[0], 1.0), s_nop(100)] if warmup else [s_nop(100)]
instrs += [v_mov_b32_e32(v[0], 1.0)] + [v_add_f32_e32(v[i], v[i-1], v[i-1]) for i in range(1, n)]
issue, execd = get_deltas(instrs)
return execd[1:] if warmup else execd
def test_warmup_chain_2(self): self.assertEqual(self._chain(2), [5])
def test_warmup_chain_3(self): self.assertEqual(self._chain(3), [5, 5])
def test_warmup_chain_4(self): self.assertEqual(self._chain(4), [5, 5, 5])
def test_warmup_chain_5(self): self.assertEqual(self._chain(5), [5, 5, 5, 9])
def test_warmup_chain_6(self): self.assertEqual(self._chain(6), [5, 5, 5, 5, 9])
def test_warmup_chain_7(self): self.assertEqual(self._chain(7), [5, 5, 5, 5, 9, 9])
def test_warmup_chain_8(self): self.assertEqual(self._chain(8), [5, 5, 5, 5, 9, 9, 9])
def test_cold_chain_2(self): self.assertEqual(self._chain(2, False), [6])
def test_cold_chain_3(self): self.assertEqual(self._chain(3, False), [6, 5])
def test_cold_chain_4(self): self.assertEqual(self._chain(4, False), [6, 5, 5])
def test_cold_chain_5(self): self.assertEqual(self._chain(5, False), [6, 5, 5, 9])
def test_cold_chain_6(self): self.assertEqual(self._chain(6, False), [6, 5, 5, 9, 9])
def test_cold_chain_7(self): self.assertEqual(self._chain(7, False), [6, 5, 5, 5, 9, 9])
def test_cold_chain_8(self): self.assertEqual(self._chain(8, False), [6, 5, 5, 5, 9, 9, 9])
class TestVALUIndependent(unittest.TestCase):
"""Independent VALU instructions."""
def _ind(self, n, expected_exec):
instrs = [v_mov_b32_e32(v[i], float(i)) for i in range(n)]
issue, execd = get_deltas(instrs)
self.assertEqual(issue[:n-1], [1]*(n-1))
self.assertEqual(execd, expected_exec)
def test_ind_2(self): self._ind(2, [1])
def test_ind_3(self): self._ind(3, [1, 1])
def test_ind_4(self): self._ind(4, [1, 1, 1])
def test_ind_5(self): self._ind(5, [1, 1, 1, 1])
def test_ind_6(self): self._ind(6, [1, 1, 1, 1, 1])
def test_ind_7(self): self._ind(7, [1, 1, 1, 1, 1, 1])
def test_ind_8(self): self._ind(8, [1, 1, 1, 1, 1, 1, 1])
class TestForwardingGap(unittest.TestCase):
"""Producer + N independent instructions + consumer - tests forwarding window."""
def _exec_deltas(self, n_gap):
instrs = [v_mov_b32_e32(v[0], 1.0)]
instrs += [v_mov_b32_e32(v[10+i], float(i)) for i in range(n_gap)]
instrs += [v_add_f32_e32(v[1], v[0], v[0])]
_, execd = get_deltas(instrs)
return execd
def test_gap0(self): self.assertEqual(self._exec_deltas(0), [6])
def test_gap1(self): self.assertEqual(self._exec_deltas(1), [1, 5])
def test_gap2(self): self.assertEqual(self._exec_deltas(2), [1, 1, 4])
def test_gap3(self): self.assertIn(self._exec_deltas(3), [[1, 1, 1, 3], [1, 1, 1, 4]])
def test_gap4(self): self.assertIn(self._exec_deltas(4), [[1, 1, 1, 1, 3], [1, 1, 1, 1, 4]])
def test_gap5(self): self.assertEqual(self._exec_deltas(5), [1, 1, 1, 1, 1, 4]) # anomaly
def test_gap6(self): self.assertEqual(self._exec_deltas(6), [1, 1, 1, 1, 1, 1, 3])
def test_gap7(self): self.assertEqual(self._exec_deltas(7), [1, 1, 1, 1, 1, 1, 1, 3])
def test_gap8(self): self.assertEqual(self._exec_deltas(8), [1, 1, 1, 1, 1, 1, 1, 1, 3])
def test_gap9(self): self.assertEqual(self._exec_deltas(9), [1, 1, 1, 1, 1, 1, 1, 1, 1, 3])
class TestChainWithIndependentGap(unittest.TestCase):
"""Chain of dependent VALUs with independent VALUs inserted before the last one.
Hardware observation: In a chain v0->v1->v2->v3->v4, if we insert N independent VALUs
before v4, the forwarding behavior changes:
- 0-1 independent VALUs: v4 cannot forward from v3 (delta=9)
- 2+ independent VALUs: v4 can forward from v3 (delta=5)
This suggests forwarding eligibility depends on whether the direct source is in the ALU
at issue time, not just at dispatch time.
"""
def _chain5_gap(self, n_ind):
"""Chain v0->v1->v2->v3->v4 with N independent VALUs before v4. Returns v3->v4 delta."""
instrs = [s_nop(100),
v_mov_b32_e32(v[0], 1.0),
v_mov_b32_e32(v[1], v[0]),
v_mov_b32_e32(v[2], v[1]),
v_mov_b32_e32(v[3], v[2])]
instrs += [v_mov_b32_e32(v[10+i], float(i)) for i in range(n_ind)]
instrs += [v_mov_b32_e32(v[4], v[3])]
_, execd = get_deltas(instrs)
# Chain execs are at indices 0,1,2,3 and last one. Independent ones are in between.
# v3->v4 delta = last exec time - 4th exec time (index 3)
# With n_ind independent VALUs, execd has 4 + n_ind entries
# We want delta between exec[3] (v3) and exec[4+n_ind-1] (v4)
# Actually execd is already deltas, so we need absolute times
time, exec_times = 0, []
packets = run_sqtt(instrs)
for ptype, delta in get_timing_deltas(packets):
time += delta
if ptype == 'ALUEXEC': exec_times.append(time)
# v0,v1,v2,v3 are first 4, v4 is last
return exec_times[-1] - exec_times[3]
def test_gap0(self): self.assertEqual(self._chain5_gap(0), 9)
def test_gap1(self): self.assertEqual(self._chain5_gap(1), 9)
def test_gap2(self): self.assertEqual(self._chain5_gap(2), 5)
def test_gap3(self): self.assertEqual(self._chain5_gap(3), 5)
def test_gap4(self): self.assertEqual(self._chain5_gap(4), 5)
class TestVALULatency(unittest.TestCase):
"""VALU latency depends on VGPR source reads.
6 cycles: no VGPR source (constant only), stays 6 regardless of warmup
8-11 cycles: VGPR source read, decreases with warmup (11->10->9->8)
s_nop(0) after VALU immediately drops VGPR read latency to 8
Anomalies:
- 7 consecutive VALUs (no s_nop) causes +1 cycle penalty
- n=0 or n=3 const VALUs + nop + vgpr = 9 cycles (not 8)
"""
def _get_latency(self, instrs):
if not isinstance(instrs, list): instrs = [instrs]
packets = run_sqtt(instrs)
deltas = get_timing_deltas(packets)
time, valu_times, exec_times = 0, [], []
for ptype, delta in deltas:
time += delta
if ptype == 'VALUINST': valu_times.append(time)
if ptype == 'ALUEXEC': exec_times.append(time)
return exec_times[-1] - valu_times[-1] if valu_times and exec_times else None
# 6-cycle latency: no VGPR source (constant), always 6
def test_const_single(self): self.assertEqual(self._get_latency(v_mov_b32_e32(v[0], 1.0)), 6)
def test_const_literal(self): self.assertEqual(self._get_latency(v_mov_b32_e32(v[0], 565.0)), 6)
def test_const_after_const(self): self.assertEqual(self._get_latency([v_mov_b32_e32(v[0], 1.0), v_mov_b32_e32(v[1], 2.0)]), 6)
def test_const_after_nop(self): self.assertEqual(self._get_latency([v_mov_b32_e32(v[0], 1.0), s_nop(0), v_mov_b32_e32(v[1], 2.0)]), 6)
# VGPR read latency: cold start = 9
def test_vgpr_cold(self): self.assertEqual(self._get_latency(v_mov_b32_e32(v[0], v[1])), 9)
# VGPR read latency: warmup decreases 11->10->9->8
def _vgpr_after_n_const(self, n):
return self._get_latency([v_mov_b32_e32(v[i], float(i)) for i in range(n)] + [v_mov_b32_e32(v[10], v[99])])
def test_vgpr_after_1_const(self): self.assertEqual(self._vgpr_after_n_const(1), 11)
def test_vgpr_after_2_const(self): self.assertEqual(self._vgpr_after_n_const(2), 10)
def test_vgpr_after_3_const(self): self.assertEqual(self._vgpr_after_n_const(3), 9)
def test_vgpr_after_4_const(self): self.assertIn(self._vgpr_after_n_const(4), [8, 9])
def test_vgpr_after_5_const(self): self.assertIn(self._vgpr_after_n_const(5), [8, 9])
def test_vgpr_after_6_const(self): self.assertEqual(self._vgpr_after_n_const(6), 9) # anomaly
def test_vgpr_after_7_const(self): self.assertEqual(self._vgpr_after_n_const(7), 8)
def test_vgpr_after_8_const(self): self.assertEqual(self._vgpr_after_n_const(8), 8)
# s_nop(0) immediately drops VGPR read latency to 8 (or 9 on some variants)
def test_vgpr_nop_warmup(self): self.assertIn(self._get_latency([v_mov_b32_e32(v[0], 1.0), s_nop(0), v_mov_b32_e32(v[1], v[99])]), [8, 9])
# s_nop + vgpr read: latency depends on # of const VALUs before nop
def _n_const_nop_vgpr(self, n):
"""N const VALUs + s_nop(0) + vgpr read."""
instrs = [v_mov_b32_e32(v[i], float(i)) for i in range(n)]
instrs += [s_nop(0)]
instrs += [v_mov_b32_e32(v[10], v[99])]
return self._get_latency(instrs)
def test_0_const_nop_vgpr(self): self.assertEqual(self._n_const_nop_vgpr(0), 9)
def test_1_const_nop_vgpr(self): self.assertIn(self._n_const_nop_vgpr(1), [8, 9])
def test_2_const_nop_vgpr(self): self.assertIn(self._n_const_nop_vgpr(2), [8, 9])
def test_3_const_nop_vgpr(self): self.assertEqual(self._n_const_nop_vgpr(3), 9) # anomaly
def test_4_const_nop_vgpr(self): self.assertEqual(self._n_const_nop_vgpr(4), 8)
def test_5_const_nop_vgpr(self): self.assertEqual(self._n_const_nop_vgpr(5), 8)
def test_6_const_nop_vgpr(self): self.assertEqual(self._n_const_nop_vgpr(6), 8)
def test_7_const_nop_vgpr(self): self.assertEqual(self._n_const_nop_vgpr(7), 8)
class TestChainWithNop(unittest.TestCase):
"""Dependency chain with s_nop between instructions."""
def _test(self, nop_val, expected_issue, expected_exec):
issue, execd = get_deltas([v_mov_b32_e32(v[0], 1.0), s_nop(nop_val), v_add_f32_e32(v[1], v[0], v[0])])
self.assertEqual(issue[:2], expected_issue)
if isinstance(expected_exec[0], list): self.assertIn(execd, expected_exec)
else: self.assertEqual(execd, expected_exec)
def test_nop0(self): self._test(0, [3, 1], [[6], [7]])
def test_nop1(self): self._test(1, [4, 1], [[7], [8]])
def test_nop2(self): self._test(2, [5, 1], [9])
def test_nop3(self): self._test(3, [6, 1], [9])
def test_nop4(self): self._test(4, [11, 1], [10])
def test_nop5(self): self._test(5, [12, 1], [11])
class TestIndWithNop(unittest.TestCase):
"""Independent instructions with s_nop between."""
def _test(self, nop_val, expected_issue, expected_exec):
issue, execd = get_deltas([v_mov_b32_e32(v[0], 1.0), s_nop(nop_val), v_mov_b32_e32(v[1], 2.0)])
self.assertEqual(issue[:2], expected_issue)
self.assertEqual(execd, expected_exec)
def test_nop0(self): self._test(0, [3, 1], [4])
def test_nop1(self): self._test(1, [4, 1], [5])
def test_nop3(self): self._test(3, [6, 1], [7])
def test_nop4(self): self._test(4, [11, 1], [8])
def test_nop5(self): self._test(5, [12, 1], [9])
class TestChain3NopMid(unittest.TestCase):
"""3-instruction chain with s_nop in middle."""
def _test(self, nop_val, expected_issue, expected_exec):
issue, execd = get_deltas([
v_mov_b32_e32(v[0], 1.0), v_add_f32_e32(v[1], v[0], v[0]),
s_nop(nop_val), v_add_f32_e32(v[2], v[1], v[1])])
self.assertEqual(issue[:3], expected_issue)
self.assertEqual(execd, expected_exec)
def test_nop0(self): self._test(0, [1, 3, 1], [6, 5])
def test_nop1(self): self._test(1, [1, 4, 1], [6, 5])
def test_nop2(self): self._test(2, [1, 5, 1], [6, 5])
def test_nop3(self): self._test(3, [1, 10, 1], [6, 5])
class TestInd3NopMid(unittest.TestCase):
"""3 independent instructions with s_nop in middle."""
def _test(self, nop_val, expected_issue, expected_exec):
issue, execd = get_deltas([
v_mov_b32_e32(v[0], 1.0), v_mov_b32_e32(v[1], 2.0),
s_nop(nop_val), v_mov_b32_e32(v[2], 3.0)])
self.assertEqual(issue[:3], expected_issue)
self.assertEqual(execd, expected_exec)
def test_nop0(self): self._test(0, [1, 3, 1], [1, 4])
def test_nop1(self): self._test(1, [1, 4, 1], [1, 5])
def test_nop2(self): self._test(2, [1, 5, 1], [1, 6])
def test_nop3(self): self._test(3, [1, 10, 1], [1, 7])
class TestSNopDelay(unittest.TestCase):
"""Single s_nop delay between two independent v_movs.
s_nop(n) delays n+1 cycles, plus +4 extra for n in [11, 22].
Exec delta = n + 4 (baseline) + 4 (if 11 <= n <= 22)."""
def _test(self, n, expected):
_, execd = get_deltas([v_mov_b32_e32(v[0], 1.0), s_nop(n), v_mov_b32_e32(v[1], 2.0)])
if isinstance(expected, list): self.assertIn(execd[0], expected)
else: self.assertEqual(execd, [expected])
def test_snop_0(self): self._test(0, 4)
def test_snop_1(self): self._test(1, 5)
def test_snop_2(self): self._test(2, 6)
def test_snop_3(self): self._test(3, 7)
def test_snop_4(self): self._test(4, 8)
def test_snop_5(self): self._test(5, 9)
def test_snop_6(self): self._test(6, 10)
def test_snop_7(self): self._test(7, 11)
def test_snop_10(self): self._test(10, 14)
def test_snop_11(self): self._test(11, 19) # +4 extra starts here
def test_snop_15(self): self._test(15, 23)
def test_snop_22(self): self._test(22, 30) # +4 extra ends here
def test_snop_23(self): self._test(23, 27)
def test_snop_31(self): self._test(31, 35)
def test_snop_32(self): self._test(32, 36)
def test_snop_63(self): self._test(63, [67, 71])
class TestVALUExecWithNop(unittest.TestCase):
"""Single VALU followed by s_nop - measures VALUINST to ALUEXEC delay."""
def _get_delay(self, instrs, nops=16):
deltas = get_timing_deltas(run_sqtt(instrs, nops=nops))
time, valu_time, exec_time = 0, None, None
for ptype, delta in deltas:
time += delta
if ptype == 'VALUINST' and valu_time is None: valu_time = time
if ptype == 'ALUEXEC' and exec_time is None: exec_time = time
return exec_time - valu_time
# Boundary: s_nop(0-3) = 6 cycles, s_nop(4+) = 10 cycles
def test_nop0(self): self.assertEqual(self._get_delay([v_mov_b32_e32(v[0], 1.0), s_nop(0)]), 6)
def test_nop1(self): self.assertEqual(self._get_delay([v_mov_b32_e32(v[0], 1.0), s_nop(1)]), 6)
def test_nop2(self): self.assertEqual(self._get_delay([v_mov_b32_e32(v[0], 1.0), s_nop(2)]), 6)
def test_nop3(self): self.assertEqual(self._get_delay([v_mov_b32_e32(v[0], 1.0), s_nop(3)]), 6)
def test_nop4(self): self.assertEqual(self._get_delay([v_mov_b32_e32(v[0], 1.0), s_nop(4)]), 10)
def test_nop5(self): self.assertEqual(self._get_delay([v_mov_b32_e32(v[0], 1.0), s_nop(5)]), 10)
def test_nop6(self): self.assertEqual(self._get_delay([v_mov_b32_e32(v[0], 1.0), s_nop(6)]), 10)
def test_nop7(self): self.assertEqual(self._get_delay([v_mov_b32_e32(v[0], 1.0), s_nop(7)]), 10)
def test_nop8(self): self.assertEqual(self._get_delay([v_mov_b32_e32(v[0], 1.0), s_nop(8)]), 10)
def test_nop9(self): self.assertEqual(self._get_delay([v_mov_b32_e32(v[0], 1.0), s_nop(9)]), 10)
def test_nop10(self): self.assertEqual(self._get_delay([v_mov_b32_e32(v[0], 1.0), s_nop(10)]), 10)
# No nop = slow path, one s_nop(0) padding = fast path
def test_no_padding(self): self.assertEqual(self._get_delay([v_mov_b32_e32(v[0], 1.0)], nops=0), 10)
def test_one_padding(self): self.assertEqual(self._get_delay([v_mov_b32_e32(v[0], 1.0)], nops=1), 6)
# Multiple s_nop(0)s don't accumulate - still fast path
def test_nop0_x2(self): self.assertEqual(self._get_delay([v_mov_b32_e32(v[0], 1.0), s_nop(0), s_nop(0)]), 6)
# First nop determines path: s_nop(0) then s_nop(4) = fast, s_nop(4) then s_nop(0) = slow
def test_nop0_nop4(self): self.assertEqual(self._get_delay([v_mov_b32_e32(v[0], 1.0), s_nop(0), s_nop(4)]), 6)
def test_nop4_nop0(self): self.assertEqual(self._get_delay([v_mov_b32_e32(v[0], 1.0), s_nop(4), s_nop(0)]), 10)
class TestDelayALU(unittest.TestCase):
"""s_delay_alu behavior - helps understand hardware pipeline latencies.
s_delay_alu(simm16) where simm16 encodes:
instid0[3:0] = dependency on VALU N instructions back (1-4), 0=none
skip[6:4] = skip count for second dependency
instid1[10:7] = second dependency
Key insight: s_delay_alu tells hardware to wait for a previous VALU to complete.
The hardware determines how many cycles to stall based on pipeline state.
"""
def _exec_delta(self, instrs):
"""Return exec delta for last instruction."""
_, execd = get_deltas(instrs)
return execd[-1] if execd else None
# Direct dependency (producer -> consumer), instid0=1 means "wait for VALU 1 back"
def test_direct_no_delay(self):
# Without s_delay_alu: 6 cycles
self.assertEqual(self._exec_delta([v_mov_b32_e32(v[0], 1.0), v_add_f32_e32(v[1], v[0], v[0])]), 6)
def test_direct_delay1(self):
# With s_delay_alu(instid0=1): 7-8 cycles (+1 from the delay instruction)
self.assertIn(self._exec_delta([v_mov_b32_e32(v[0], 1.0), s_delay_alu(simm16=1), v_add_f32_e32(v[1], v[0], v[0])]), [7, 8])
def test_direct_delay2(self):
# instid0=2 doesn't apply (only 1 VALU back), so no extra delay
self.assertEqual(self._exec_delta([v_mov_b32_e32(v[0], 1.0), s_delay_alu(simm16=2), v_add_f32_e32(v[1], v[0], v[0])]), 6)
def test_direct_delay3(self):
self.assertEqual(self._exec_delta([v_mov_b32_e32(v[0], 1.0), s_delay_alu(simm16=3), v_add_f32_e32(v[1], v[0], v[0])]), 6)
def test_direct_delay4(self):
self.assertEqual(self._exec_delta([v_mov_b32_e32(v[0], 1.0), s_delay_alu(simm16=4), v_add_f32_e32(v[1], v[0], v[0])]), 6)
# With 1 independent instruction between producer and consumer
def test_gap1_delay1(self):
# instid0=1 waits for the independent instruction (not the producer)
instrs = [v_mov_b32_e32(v[0], 1.0), v_mov_b32_e32(v[5], 5.0), s_delay_alu(simm16=1), v_add_f32_e32(v[1], v[0], v[0])]
self.assertEqual(self._exec_delta(instrs), 8)
def test_gap1_delay2(self):
# instid0=2 waits for the producer (2 VALUs back)
instrs = [v_mov_b32_e32(v[0], 1.0), v_mov_b32_e32(v[5], 5.0), s_delay_alu(simm16=2), v_add_f32_e32(v[1], v[0], v[0])]
self.assertIn(self._exec_delta(instrs), [6, 7])
def test_gap1_delay3(self):
# instid0=3 doesn't apply (only 2 VALUs back)
instrs = [v_mov_b32_e32(v[0], 1.0), v_mov_b32_e32(v[5], 5.0), s_delay_alu(simm16=3), v_add_f32_e32(v[1], v[0], v[0])]
self.assertEqual(self._exec_delta(instrs), 5)
# With 2 independent instructions between
def test_gap2_delay1(self):
instrs = [v_mov_b32_e32(v[0], 1.0), v_mov_b32_e32(v[5], 5.0), v_mov_b32_e32(v[6], 6.0),
s_delay_alu(simm16=1), v_add_f32_e32(v[1], v[0], v[0])]
self.assertEqual(self._exec_delta(instrs), 7)
def test_gap2_delay2(self):
instrs = [v_mov_b32_e32(v[0], 1.0), v_mov_b32_e32(v[5], 5.0), v_mov_b32_e32(v[6], 6.0),
s_delay_alu(simm16=2), v_add_f32_e32(v[1], v[0], v[0])]
self.assertEqual(self._exec_delta(instrs), 7)
def test_gap2_delay3(self):
# instid0=3 waits for the producer (3 VALUs back)
instrs = [v_mov_b32_e32(v[0], 1.0), v_mov_b32_e32(v[5], 5.0), v_mov_b32_e32(v[6], 6.0),
s_delay_alu(simm16=3), v_add_f32_e32(v[1], v[0], v[0])]
self.assertIn(self._exec_delta(instrs), [5, 6])
def test_gap2_delay4(self):
instrs = [v_mov_b32_e32(v[0], 1.0), v_mov_b32_e32(v[5], 5.0), v_mov_b32_e32(v[6], 6.0),
s_delay_alu(simm16=4), v_add_f32_e32(v[1], v[0], v[0])]
self.assertEqual(self._exec_delta(instrs), 4)
class TestNopTimingSensitivity(unittest.TestCase):
"""Forwarding behavior has 128-cycle periodicity.
Hardware observation: when nop_cycles % 128 is in [72, 75], chain_6 gets 5 forwards
instead of 4. This 4-cycle window repeats every 128 cycles, suggesting alignment
with some hardware scheduling period (possibly wave scheduler or cache).
Windows found: nop 72-75, 200-203, 328-331, 456-459, ...
"""
def _chain6_fwd_count(self, nop_size):
"""Count initial consecutive forwards for a 6-instruction chain after s_nop(n)."""
instrs = [s_nop(nop_size), v_mov_b32_e32(v[99], 1.0)]
instrs += [v_mov_b32_e32(v[0], 1.0)]
for i in range(1, 6):
instrs += [v_mov_b32_e32(v[i], v[i-1])]
_, execd = get_deltas(instrs)
chain_deltas = execd[1:]
fwd_count = 0
for d in chain_deltas:
if d == 5: fwd_count += 1
else: break
return fwd_count
# Normal case: 4 forwards
def test_nop71(self): self.assertEqual(self._chain6_fwd_count(71), 4)
def test_nop76(self): self.assertEqual(self._chain6_fwd_count(76), 4)
def test_nop199(self): self.assertEqual(self._chain6_fwd_count(199), 4)
def test_nop204(self): self.assertEqual(self._chain6_fwd_count(204), 4)
# Anomaly window at nop % 128 == 72-75: 5 forwards on RDNA3, 4 on other variants
def test_nop72(self): self.assertIn(self._chain6_fwd_count(72), [4, 5])
def test_nop75(self): self.assertIn(self._chain6_fwd_count(75), [4, 5])
def test_nop200(self): self.assertIn(self._chain6_fwd_count(200), [4, 5])
def test_nop203(self): self.assertIn(self._chain6_fwd_count(203), [4, 5])
def test_nop328(self): self.assertIn(self._chain6_fwd_count(328), [4, 5])
def test_nop331(self): self.assertIn(self._chain6_fwd_count(331), [4, 5])
if __name__ == "__main__":
unittest.main()
+41 -44
View File
@@ -1,16 +1,15 @@
#!/usr/bin/env python3
"""Tests for SQTT packet decoding using real captured examples."""
import pickle, unittest, ctypes, threading
import pickle, unittest, ctypes
from pathlib import Path
from tinygrad.helpers import DEBUG
from tinygrad.helpers import DEBUG, colored
from tinygrad.runtime.autogen import rocprof
from tinygrad.runtime.support.elf import elf_loader
from extra.assembly.amd.decode import decode_inst
from extra.assembly.amd.asm import detect_format
from extra.assembly.amd.autogen.rdna3.ins import SOPP
from extra.assembly.amd.autogen.rdna3.enum import SOPPOp
from extra.assembly.amd.sqtt import (decode, LAYOUT_HEADER, WAVESTART, WAVEEND, INST, VALUINST, IMMEDIATE, IMMEDIATE_MASK,
ALUEXEC, VMEMEXEC, PACKET_TYPES, InstOp, print_packets)
from extra.assembly.amd.test.helpers import TARGET_TO_ARCH
ALUEXEC, VMEMEXEC, PACKET_TYPES, InstOp, AluSrc, MemSrc)
EXAMPLES_DIR = Path(__file__).parent.parent.parent.parent / "sqtt/examples"
# INST ops for non-traced SIMDs (excluded from instruction count)
@@ -20,11 +19,39 @@ OTHER_SIMD_OPS = {InstOp.OTHER_LDS_LOAD, InstOp.OTHER_LDS_STORE, InstOp.OTHER_LD
InstOp.OTHER_GLOBAL_STORE_64, InstOp.OTHER_GLOBAL_STORE_96, InstOp.OTHER_GLOBAL_STORE_128,
InstOp.OTHER_GLOBAL_STORE_VADDR_128}
PACKET_COLORS = {
"INST": "WHITE", "VALUINST": "BLACK", "VMEMEXEC": "yellow", "ALUEXEC": "yellow",
"IMMEDIATE": "YELLOW", "IMMEDIATE_MASK": "YELLOW", "WAVERDY": "cyan", "WAVEALLOC": "cyan",
"WAVEEND": "blue", "WAVESTART": "blue", "PERF": "magenta", "EVENT": "red", "EVENT_BIG": "red",
"REG": "green", "LAYOUT_HEADER": "white", "SNAPSHOT": "white", "UTILCTR": "green",
}
def format_packet(p, time_offset: int = 0) -> str:
name, cycle = type(p).__name__, p._time - time_offset
if isinstance(p, INST):
op_name = p.op.name if isinstance(p.op, InstOp) else f"0x{p.op:02x}"
fields = f"wave={p.wave} op={op_name}" + (" flag1" if p.flag1 else "") + (" flag2" if p.flag2 else "")
elif isinstance(p, VALUINST): fields = f"wave={p.wave}" + (" flag" if p.flag else "")
elif isinstance(p, ALUEXEC): fields = f"src={p.src.name if isinstance(p.src, AluSrc) else p.src}"
elif isinstance(p, VMEMEXEC): fields = f"src={p.src.name if isinstance(p.src, MemSrc) else p.src}"
elif isinstance(p, (WAVESTART, WAVEEND)): fields = f"wave={p.wave} simd={p.simd} cu={p.cu}"
elif hasattr(p, '_values'):
fields = " ".join(f"{k}=0x{v:x}" if k in {'snap', 'val32'} else f"{k}={v}"
for k, v in p._values.items() if not k.startswith('_') and k != 'delta')
else: fields = ""
return f"{cycle:8}: {colored(f'{name:18}', PACKET_COLORS.get(name, 'white'))} {fields}"
def print_packets(packets: list) -> None:
skip = {"NOP", "TS_DELTA_SHORT", "TS_WAVE_STATE", "TS_DELTA_OR_MARK", "TS_DELTA_S5_W2", "TS_DELTA_S5_W3", "TS_DELTA_S8_W3", "REG", "EVENT"}
time_offset = packets[0]._time if packets else 0
for p in packets:
if type(p).__name__ not in skip: print(format_packet(p, time_offset))
# ═══════════════════════════════════════════════════════════════════════════════
# ROCPROF DECODER
# ═══════════════════════════════════════════════════════════════════════════════
def run_rocprof_decoder(blobs: list[bytes], lib: bytes, base: int, target: str):
def run_rocprof_decoder(blobs: list[bytes], lib: bytes, base: int):
"""Run rocprof decoder on SQTT blobs, returning raw occupancy and instruction records."""
image, sections, _ = elf_loader(lib)
text = next((sh for sh in sections if sh.name == ".text"), None)
@@ -59,7 +86,6 @@ def run_rocprof_decoder(blobs: list[bytes], lib: bytes, base: int, target: str):
wave_insts.append([(inst.time, inst.stall) for inst in insts])
return rocprof.ROCPROFILER_THREAD_TRACE_DECODER_STATUS_SUCCESS
arch = TARGET_TO_ARCH[target]
@rocprof.rocprof_trace_decoder_isa_callback_t
def isa_cb(instr_ptr, mem_size_ptr, size_ptr, pc, _):
offset = pc.address - base
@@ -67,9 +93,8 @@ def run_rocprof_decoder(blobs: list[bytes], lib: bytes, base: int, target: str):
mem_size_ptr[0] = 0
return rocprof.ROCPROFILER_THREAD_TRACE_DECODER_STATUS_SUCCESS
try:
inst = decode_inst(image[offset:], arch=arch)
inst = detect_format(data := image[offset:]).from_bytes(data)
mem_size_ptr[0] = inst._size()
# this could be an error in our decode_inst
except (ValueError, AssertionError):
mem_size_ptr[0] = 0
return rocprof.ROCPROFILER_THREAD_TRACE_DECODER_STATUS_SUCCESS
@@ -80,24 +105,14 @@ def run_rocprof_decoder(blobs: list[bytes], lib: bytes, base: int, target: str):
size_ptr[0] = min(5, max_sz - 1)
return rocprof.ROCPROFILER_THREAD_TRACE_DECODER_STATUS_SUCCESS
exc = None
def worker():
nonlocal exc
try: rocprof.rocprof_trace_decoder_parse_data(copy_cb, trace_cb, isa_cb, None)
except Exception as e: exc = e
(t:=threading.Thread(target=worker, daemon=True)).start()
t.join(timeout=1)
if exc is not None: raise exc
if t.is_alive(): raise RuntimeError("rocprof decoder timeout")
rocprof.rocprof_trace_decoder_parse_data(copy_cb, trace_cb, isa_cb, None)
return occupancy_records, wave_insts
class TestSQTTExamples(unittest.TestCase):
target = "gfx1100"
@classmethod
def setUpClass(cls):
cls.examples = {}
for pkl_path in sorted((EXAMPLES_DIR/cls.target).glob("*.pkl")):
for pkl_path in sorted(EXAMPLES_DIR.glob("*.pkl")):
with open(pkl_path, "rb") as f:
data = pickle.load(f)
sqtt_events = [e for e in data if type(e).__name__ == "ProfileSQTTEvent"]
@@ -112,7 +127,7 @@ class TestSQTTExamples(unittest.TestCase):
for name, (events, *_) in self.examples.items():
for i, event in enumerate(events):
with self.subTest(example=name, event=i):
packets = list(decode(event.blob))
packets = decode(event.blob)
if DEBUG >= 2: print(f"\n=== {name} event {i} ==="); print_packets(packets)
self.assertGreater(len(packets), 0, f"no packets decoded from {name} event {i}")
self.assertIsInstance(packets[0], LAYOUT_HEADER, f"first packet should be LAYOUT_HEADER in {name}")
@@ -146,26 +161,11 @@ class TestSQTTExamples(unittest.TestCase):
all_packets = [p for e in events for p in decode(e.blob)]
self.assertGreater(len([p for p in all_packets if isinstance(p, INST)]), 0, f"no INST packets in {name}")
expected = {
"profile_empty_run_0": [1803, 1908, 1928, 1979, 2006, 1912],
"profile_empty_run_1": [1803, 1908, 1928, 1979, 2006, 1912],
"profile_gemm_run_0": [2531, 1844, 1864, 1915, 1942, 1848, 3074, 1919, 1939, 1990, 2017, 1923, 19026, 1919, 1939, 1990, 2017, 1929],
"profile_gemm_run_1": [2554, 1844, 1864, 1915, 1942, 1848, 3084, 1919, 1939, 1990, 2017, 1923, 19010, 1919, 1939, 1990, 2017, 1923],
"profile_plus_run_0": [1900, 1908, 1928, 1979, 2006, 1912],
"profile_plus_run_1": [1856, 1908, 1928, 1979, 2006, 1912],
}
def test_packet_counts(self):
for name, (events, *_) in self.examples.items():
with self.subTest(example=name):
if not self.expected.get(name): continue
counts = [len(list(decode(e.blob))) for e in events]
self.assertEqual(counts, self.expected[name], f"packet count mismatch in {name}")
def test_rocprof_wave_times_match(self):
"""Wave start/end times must match rocprof exactly."""
for name, (events, lib, base) in self.examples.items():
with self.subTest(example=name):
occupancy, _ = run_rocprof_decoder([e.blob for e in events], lib, base, self.target)
occupancy, _ = run_rocprof_decoder([e.blob for e in events], lib, base)
# extract from rocprof occupancy records
roc_starts: dict[tuple[int, int, int], int] = {}
roc_waves: list[tuple[int, int]] = []
@@ -176,8 +176,9 @@ class TestSQTTExamples(unittest.TestCase):
# extract from our decoder
our_waves: list[tuple[int, int]] = []
for event in events:
packets = decode(event.blob)
wave_starts: dict[tuple[int, int, int], int] = {}
for p in decode(event.blob):
for p in packets:
if isinstance(p, WAVESTART): wave_starts[(p.wave, p.simd, p.cu)] = p._time
elif isinstance(p, WAVEEND) and (key := (p.wave, p.simd, p.cu)) in wave_starts:
our_waves.append((wave_starts[key], p._time))
@@ -187,7 +188,7 @@ class TestSQTTExamples(unittest.TestCase):
"""Instruction times must match rocprof exactly (excluding s_endpgm)."""
for name, (events, lib, base) in self.examples.items():
with self.subTest(example=name):
_, wave_insts = run_rocprof_decoder([e.blob for e in events], lib, base, self.target)
_, wave_insts = run_rocprof_decoder([e.blob for e in events], lib, base)
# skip last inst per wave (s_endpgm) - it needs special handling (time + duration instead of time + stall)
roc_insts = [time + stall for insts in wave_insts for time, stall in insts[:-1]]
# extract from our decoder
@@ -201,9 +202,5 @@ class TestSQTTExamples(unittest.TestCase):
for _ in range(bin(p.mask).count('1')): our_insts.append(p._time)
self.assertEqual(sorted(our_insts), sorted(roc_insts), f"instruction times mismatch in {name}")
#class TestSQTTExamplesRDNA4(TestSQTTExamples): target = "gfx1200"
#class TestSQTTExamplesCDNA(TestSQTTExamples): target = "gfx950"
if __name__ == "__main__":
unittest.main()
+463
View File
@@ -0,0 +1,463 @@
#!/usr/bin/env python3
"""Hardware tests for SQTT decoder - validates decoding of real SQTT streams.
Run with: python -m pytest extra/assembly/amd/test/test_sqtt_hw.py -v -s
Requires AMD GPU with SQTT support.
For pretty trace output: DEBUG=2 python -m pytest extra/assembly/amd/test/test_sqtt_hw.py -v -s
"""
import os
os.environ["SQTT"] = "1"
os.environ["PROFILE"] = "1"
os.environ["SQTT_ITRACE_SE_MASK"] = "1" # Enable instruction tracing on SE0
os.environ["SQTT_LIMIT_SE"] = "2" # Force work to traced SE only
import unittest
from tinygrad.helpers import DEBUG, colored
from tinygrad.device import Device
from tinygrad.runtime.ops_amd import AMDProgram, ProfileSQTTEvent
from tinygrad.runtime.support.compiler_amd import HIPCompiler
from extra.assembly.amd.autogen.rdna3.ins import v_mov_b32_e32, v_add_f32_e32, v_mul_f32_e32, s_mov_b32, s_add_u32, s_nop, s_waitcnt, s_endpgm
from extra.assembly.amd.dsl import v, s
from extra.assembly.amd.sqtt import decode, LAYOUT_HEADER, WAVESTART, WAVEEND, INST, VALUINST, ALUEXEC, VMEMEXEC, InstOp, AluSrc, MemSrc
dev = Device["AMD"]
# ═══════════════════════════════════════════════════════════════════════════════
# PRETTY PRINTING
# ═══════════════════════════════════════════════════════════════════════════════
PACKET_COLORS = {
"INST": "WHITE", "VALUINST": "BLACK",
"VMEMEXEC": "yellow", "ALUEXEC": "yellow",
"IMMEDIATE": "YELLOW", "IMMEDIATE_MASK": "YELLOW",
"WAVERDY": "cyan", "WAVEALLOC": "cyan",
"WAVEEND": "blue", "WAVESTART": "blue",
"PERF": "magenta",
"EVENT": "red", "EVENT_BIG": "red",
"REG": "green",
"LAYOUT_HEADER": "white",
"TS_DELTA_SHORT": "BLACK", "NOP": "BLACK", "TS_WAVE_STATE": "BLACK",
"SNAPSHOT": "white", "TS_DELTA_OR_MARK": "BLACK",
"TS_DELTA_S8_W3": "BLACK", "TS_DELTA_S5_W2": "BLACK", "TS_DELTA_S5_W3": "BLACK",
"UTILCTR": "green",
}
def format_packet(p, last_time: int = 0, time_offset: int = 0) -> str:
"""Format a packet for pretty printing."""
name = type(p).__name__
color = PACKET_COLORS.get(name, "white")
fields = []
if isinstance(p, INST):
op = p.op
op_name = op.name if isinstance(op, InstOp) else f"0x{op:02x}"
fields = [f"wave={p.wave}", f"op={op_name}"]
if p.flag1: fields.append("flag1")
if p.flag2: fields.append("flag2")
elif isinstance(p, VALUINST):
fields = [f"wave={p.wave}"]
if p.flag: fields.append("flag")
elif isinstance(p, ALUEXEC):
src_name = p.src.name if isinstance(p.src, AluSrc) else f"{p.src}"
fields = [f"src={src_name}"]
elif isinstance(p, VMEMEXEC):
src_name = p.src.name if isinstance(p.src, MemSrc) else f"{p.src}"
fields = [f"src={src_name}"]
elif isinstance(p, WAVESTART):
fields = [f"wave={p.wave}", f"simd={p.simd}", f"cu={p.cu}"]
elif isinstance(p, WAVEEND):
fields = [f"wave={p.wave}", f"simd={p.simd}", f"cu={p.cu}"]
elif hasattr(p, '_values'):
# Format hex fields appropriately
hex_fields = {'snap', 'val32'}
fields = [f"{k}=0x{v:x}" if k in hex_fields else f"{k}={v}" for k, v in p._values.items() if not k.startswith('_') and k != 'delta']
return colored(f"{name:18s}", color) + " " + ", ".join(fields)
def get_wave_packets(packets: list) -> list:
"""Extract packets from WAVESTART to WAVEEND, filtering pure timing packets."""
skip_types = {"NOP", "TS_DELTA_SHORT", "TS_WAVE_STATE", "TS_DELTA_OR_MARK", "TS_DELTA_S5_W2", "TS_DELTA_S5_W3", "TS_DELTA_S8_W3"}
result = []
in_wave = False
for p in packets:
name = type(p).__name__
if isinstance(p, WAVESTART):
in_wave = True
if in_wave and name not in skip_types:
result.append(p)
if isinstance(p, WAVEEND):
in_wave = False
return result
def print_wave_trace(packets: list) -> None:
"""Print packets from WAVESTART to WAVEEND with normalized time."""
wave_packets = get_wave_packets(packets)
if not wave_packets:
return
time_offset = wave_packets[0]._time
last_time = time_offset
for p in wave_packets:
print(format_packet(p, last_time, time_offset))
last_time = p._time
def print_blobs(blobs: list[bytes], wave_only: bool = True) -> None:
"""Print traces for all blobs. wave_only=True filters to WAVESTART..WAVEEND only."""
for i, blob in enumerate(blobs):
packets = decode(blob)
print(f"\n--- Blob {i}: {len(blob)} bytes, {len(packets)} packets ---")
if wave_only:
print_wave_trace(packets)
else:
print_all_packets(packets)
def print_all_packets(packets: list) -> None:
"""Print all packets, filtering out pure timing packets."""
skip_types = {"NOP", "TS_DELTA_SHORT", "TS_WAVE_STATE", "TS_DELTA_OR_MARK", "TS_DELTA_S5_W2", "TS_DELTA_S5_W3", "TS_DELTA_S8_W3"}
if not packets: return
time_offset = packets[0]._time
last_time = time_offset
for p in packets:
if type(p).__name__ not in skip_types:
print(format_packet(p, last_time, time_offset))
last_time = p._time
# ═══════════════════════════════════════════════════════════════════════════════
# ASSEMBLY HELPERS
# ═══════════════════════════════════════════════════════════════════════════════
def assemble(instructions: list) -> bytes:
return b''.join(inst.to_bytes() for inst in instructions)
def wrap_with_nops(instructions: list, nops=16) -> list:
"""Add epilogue for clean SQTT timing.
Need enough NOPs to cover long-latency ops (DP: 42 cycles, WMMA: 47 cycles).
With 64 NOPs, the IMMEDIATE phase extends to cover these completions.
"""
return instructions + [s_nop(0)]*nops + [s_endpgm()]
def compile_asm_sqtt(instructions: list, alu_only: bool = False) -> AMDProgram:
"""Compile instructions to an AMDProgram for SQTT tracing.
Args:
instructions: List of instructions to compile
alu_only: If True, use minimal kernel config with no kernargs/LDS/scratch
Returns:
Compiled AMDProgram ready to run
"""
compiler = HIPCompiler(dev.arch)
# Add NOPs before s_endpgm to flush pipeline and get clean timing
code = assemble(instructions)
byte_str = ', '.join(f'0x{b:02x}' for b in code)
if alu_only:
asm_src = f""".text
.globl test
.p2align 8
.type test,@function
test:
.byte {byte_str}
.rodata
.p2align 6
.amdhsa_kernel test
# basic memory
.amdhsa_group_segment_fixed_size 0
.amdhsa_private_segment_fixed_size 0
.amdhsa_kernarg_size 32
.amdhsa_enable_private_segment 0
# register usage
.amdhsa_next_free_vgpr 64
.amdhsa_next_free_sgpr 8
# RSRC1
.amdhsa_wavefront_size32 1
.amdhsa_memory_ordered 1
.amdhsa_forward_progress 1
# this is key
.amdhsa_workgroup_processor_mode 0
.end_amdhsa_kernel
.amdgpu_metadata
---
amdhsa.version:
- 1
- 0
amdhsa.kernels:
- .name: test
.symbol: test.kd
.kernarg_segment_size: 0
.group_segment_fixed_size: 0
.private_segment_fixed_size: 0
.kernarg_segment_align: 8
.wavefront_size: 32
.sgpr_count: 8
.vgpr_count: 64
.max_flat_workgroup_size: 1024
...
.end_amdgpu_metadata
"""
else:
asm_src = f""".text
.globl test
.p2align 8
.type test,@function
test:
.byte {byte_str}
.rodata
.p2align 6
.amdhsa_kernel test
.amdhsa_next_free_vgpr 8
.amdhsa_next_free_sgpr 16
.amdhsa_wavefront_size32 1
.amdhsa_user_sgpr_kernarg_segment_ptr 1
.amdhsa_kernarg_size 8
.amdhsa_group_segment_fixed_size 0
.amdhsa_private_segment_fixed_size 0
.end_amdhsa_kernel
.amdgpu_metadata
---
amdhsa.version:
- 1
- 0
amdhsa.kernels:
- .name: test
.symbol: test.kd
.kernarg_segment_size: 8
.group_segment_fixed_size: 0
.private_segment_fixed_size: 0
.kernarg_segment_align: 8
.wavefront_size: 32
.sgpr_count: 16
.vgpr_count: 8
.max_flat_workgroup_size: 1024
...
.end_amdgpu_metadata
"""
lib = compiler.compile(asm_src)
return AMDProgram(dev, "test", lib)
def run_asm_sqtt(instructions: list, n_lanes: int = 1, alu_only: bool = False) -> list[bytes]:
"""Compile and run instructions on AMD hardware, return SQTT blobs.
Args:
instructions: List of instructions to run
n_lanes: Number of lanes to use
alu_only: If True, use minimal kernel config with no kernargs/LDS/scratch
"""
prg = compile_asm_sqtt(instructions, alu_only=alu_only)
return run_prg_sqtt(prg, n_lanes=n_lanes, alu_only=alu_only)
def run_prg_sqtt(prg: AMDProgram, n_lanes: int = 1, alu_only: bool = False) -> list[bytes]:
"""Run a compiled AMDProgram and return SQTT blobs.
Args:
prg: Compiled AMDProgram to run
n_lanes: Number of lanes to use
alu_only: If True, don't allocate kernarg buffer
"""
dev.profile_events.clear()
if alu_only:
prg(global_size=(1, 1, 1), local_size=(n_lanes, 1, 1), wait=True)
else:
out_gpu = dev.allocator.alloc(2048)
prg(out_gpu, global_size=(1, 1, 1), local_size=(n_lanes, 1, 1), wait=True)
return [ev.blob for ev in dev.profile_events if isinstance(ev, ProfileSQTTEvent)]
def run_prg_sqtt_batch(prg: AMDProgram, n_runs: int, n_lanes: int = 1) -> list[bytes]:
"""Run a compiled AMDProgram N times in a single queue submission and return SQTT blobs.
This builds one queue with N kernel executions, submits it once, and collects SQTT.
All N runs are captured in the same SQTT trace, reducing startup jitter.
Args:
prg: Compiled AMDProgram to run
n_runs: Number of times to execute the kernel in the queue
n_lanes: Number of lanes to use
Returns:
List of SQTT blobs (one per shader engine)
"""
from typing import cast
from tinygrad.runtime.ops_amd import AMDComputeQueue, SQTT_ITRACE_SE_MASK
from tinygrad.device import Compiled
import struct
dev.profile_events.clear()
# Build queue with sqtt_start, N kernel executions, sqtt_stop
kernargs = prg.fill_kernargs([], ())
q = cast(AMDComputeQueue, dev.hw_compute_queue_t())
q.wait(dev.timeline_signal, dev.timeline_value - 1).memory_barrier()
q.sqtt_start(dev.sqtt_buffers)
# Execute kernel N times
for _ in range(n_runs):
q.exec(prg, kernargs, (1, 1, 1), (n_lanes, 1, 1))
q.sqtt_stop(dev.sqtt_wptrs)
q.signal(dev.timeline_signal, dev.next_timeline())
q.submit(dev)
dev.synchronize()
# Collect SQTT blobs
blobs = []
for se, buf in enumerate(dev.sqtt_buffers):
wptr = (dev.sqtt_wptrs.cpu_view().view(fmt='I')[se] & 0x1FFFFFFF) * 32
if dev.target[:2] == (11, 0): wptr -= ((buf.va_addr // 32) & 0x1FFFFFFF) * 32
if wptr > 0 and wptr <= buf.size:
dev.allocator._copyout(sqtt_mv:=memoryview(bytearray(wptr)), buf)
resbuf = (struct.pack('<Q', 0x11 | (4 << 13) | (0xf << 16) | (se << 24)) + bytes(sqtt_mv)) if dev.target[0] == 9 else bytes(sqtt_mv)
blobs.append(resbuf)
return blobs
def decode_all_blobs(blobs: list[bytes]) -> list:
"""Decode all blobs and combine packets."""
all_packets = []
for blob in blobs:
all_packets.extend(decode(blob))
return all_packets
def get_inst_ops(packets: list, traced_simd: int | None = None) -> set:
"""Extract all InstOp values from INST packets within WAVESTART..WAVEEND on traced SIMD."""
ops = set()
in_wave = False
for p in packets:
if isinstance(p, WAVESTART):
in_wave = traced_simd is None or p.simd == traced_simd
if in_wave and isinstance(p, INST):
ops.add(p.op if isinstance(p.op, int) else p.op.value)
if isinstance(p, WAVEEND):
in_wave = False
return ops
def count_valuinst(packets: list, traced_simd: int | None = None) -> int:
"""Count VALUINST packets within WAVESTART..WAVEEND on traced SIMD."""
count = 0
in_wave = False
for p in packets:
if isinstance(p, WAVESTART):
in_wave = traced_simd is None or p.simd == traced_simd
if in_wave and isinstance(p, VALUINST):
count += 1
if isinstance(p, WAVEEND):
in_wave = False
return count
# ═══════════════════════════════════════════════════════════════════════════════
# TESTS
# ═══════════════════════════════════════════════════════════════════════════════
@unittest.skipIf(not hasattr(dev, 'profile_events'), "AMD device required")
class TestSQTTDecode(unittest.TestCase):
"""Test SQTT decoder with real hardware traces."""
def test_basic_structure(self):
"""Verify basic SQTT stream structure: LAYOUT_HEADER, WAVESTART, instructions, WAVEEND."""
blobs = run_asm_sqtt([v_mov_b32_e32(v[0], 0)])
self.assertGreater(len(blobs), 0, "No SQTT data captured")
packets = decode_all_blobs(blobs)
self.assertGreater(len(packets), 0, "No packets decoded")
self.assertGreater(len([p for p in packets if isinstance(p, LAYOUT_HEADER)]), 0, "No LAYOUT_HEADER packets")
self.assertGreater(len([p for p in packets if isinstance(p, WAVESTART)]), 0, "No WAVESTART packets")
self.assertGreater(len([p for p in packets if isinstance(p, WAVEEND)]), 0, "No WAVEEND packets")
if DEBUG >= 2:
print("\n=== Basic structure trace ===")
print_trace(packets)
def test_valu_instructions(self):
"""Verify VALU instructions produce INST or VALUINST packets."""
instructions = [
v_mov_b32_e32(v[0], 1.0),
v_mov_b32_e32(v[1], 2.0),
v_add_f32_e32(v[2], v[0], v[1]),
v_add_f32_e32(v[3], v[2], v[1]),
v_mul_f32_e32(v[4], v[2], v[3]),
]
blobs = run_asm_sqtt(instructions)
self.assertGreater(len(blobs), 0, "No SQTT data captured")
packets = decode_all_blobs(blobs)
inst_packets = [p for p in packets if isinstance(p, (INST, VALUINST))]
self.assertGreater(len(inst_packets), 0, "No INST/VALUINST packets for VALU instructions")
if DEBUG >= 2:
print("\n=== VALU instructions trace ===")
print_trace(packets)
def test_salu_instructions(self):
"""Verify SALU instructions produce appropriate packets."""
instructions = [
s_mov_b32(s[0], 0),
s_mov_b32(s[1], 1),
s_add_u32(s[2], s[0], s[1]),
s_add_u32(s[3], s[2], s[1]),
s_nop(0),
]
blobs = run_asm_sqtt(instructions)
self.assertGreater(len(blobs), 0, "No SQTT data captured")
packets = decode_all_blobs(blobs)
if DEBUG >= 2:
print("\n=== SALU instructions trace ===")
print_trace(packets)
def test_timing_increases(self):
"""Verify time increases monotonically through packets within each blob."""
instructions = [
v_mov_b32_e32(v[0], 1.0),
v_mov_b32_e32(v[1], 2.0),
v_add_f32_e32(v[2], v[0], v[1]),
v_mul_f32_e32(v[3], v[2], v[1]),
]
blobs = run_asm_sqtt(instructions)
self.assertGreater(len(blobs), 0, "No SQTT data captured")
for blob in blobs:
packets = decode(blob)
prev_time = 0
for p in packets:
self.assertGreaterEqual(p._time, prev_time, f"Time decreased: {prev_time} -> {p._time}")
prev_time = p._time
def test_wave_id_consistency(self):
"""Verify wave IDs are consistent between WAVESTART/WAVEEND."""
blobs = run_asm_sqtt([v_mov_b32_e32(v[0], 0)])
self.assertGreater(len(blobs), 0, "No SQTT data captured")
packets = decode_all_blobs(blobs)
wavestarts = [p for p in packets if isinstance(p, WAVESTART)]
waveends = [p for p in packets if isinstance(p, WAVEEND)]
if wavestarts and waveends:
start_waves = {p.wave for p in wavestarts}
end_waves = {p.wave for p in waveends}
self.assertTrue(start_waves & end_waves, "No matching wave IDs between WAVESTART and WAVEEND")
def test_nop_sequence(self):
"""Test a sequence of NOP instructions."""
blobs = run_asm_sqtt([s_nop(0), s_nop(0), s_nop(0)])
self.assertGreater(len(blobs), 0, "No SQTT data captured")
packets = decode_all_blobs(blobs)
self.assertGreater(len(packets), 0, "No packets decoded")
if DEBUG >= 2:
print("\n=== NOP sequence trace ===")
print_trace(packets, filter_timing=False)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,35 @@
import os
os.environ["SQTT"] = "1"
os.environ["PROFILE"] = "1"
os.environ["SQTT_LIMIT_SE"] = "2"
os.environ["SQTT_SIMD_SEL"] = "0"
os.environ["SQTT_TOKEN_EXCLUDE"] = "3784" # Exclude WAVERDY, REG, EVENT, UTILCTR, WAVEALLOC, PERF
import unittest
from extra.assembly.amd.autogen.rdna3.ins import *
from extra.assembly.amd.sqtt import decode
from extra.assembly.amd.test.test_sqtt_hw import compile_asm_sqtt, run_prg_sqtt_batch, format_packet
from extra.assembly.amd.test.test_sqtt_compare import filter_noise_packets
from tinygrad.uop.ops import UOp
from tinygrad.engine.realize import get_runner
class SQTTMultiwave(unittest.TestCase):
def test_simple_multiwave(self):
ins = [
s_barrier(),
v_mov_b32_e32(v[0], v[1]),
s_nop(0),
s_nop(100),
s_endpgm(),
]
#prg = get_runner("AMD", UOp.sink())._prg
prg = compile_asm_sqtt(ins, alu_only=True)
print(prg)
blobs = run_prg_sqtt_batch(prg, n_runs=1, n_lanes=32*16)
for blob in blobs:
packets = decode(blob)
for p in filter_noise_packets(packets):
print(f" {p._time:8d}: {format_packet(p)}")
if __name__ == "__main__":
unittest.main()
+204
View File
@@ -0,0 +1,204 @@
#!/usr/bin/env python3
"""Tests validating SQTT packet definitions against the reference implementation.
Verifies that:
1. Encoding patterns produce the correct STATE_TO_OPCODE table
2. Packet sizes (derived from fields) match expected budget values
3. Field extractions match attempt_sqtt_parse.py
"""
import unittest
from extra.assembly.amd.sqtt import (
VALUINST, VMEMEXEC, ALUEXEC, IMMEDIATE, IMMEDIATE_MASK, WAVERDY,
WAVEEND, WAVESTART, PERF, TS_WAVE_STATE, EVENT, EVENT_BIG, REG, SNAPSHOT,
TS_DELTA_OR_MARK, LAYOUT_HEADER, INST, UTILCTR, TS_DELTA_SHORT, NOP,
TS_DELTA_S8_W3, TS_DELTA_S5_W2, TS_DELTA_S5_W3, WAVEALLOC,
decode, encode, OPCODE_TO_CLASS, STATE_TO_OPCODE, PACKET_TYPES, BUDGET,
AluSrc, MemSrc, InstOp
)
# Reference table from rocprof trace decoder (attempt_sqtt_parse.py)
REFERENCE_STATE_TABLE = 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,
])
# Reference opcode -> name mapping (old opcode values from rocprof)
OLD_OPCODE_TO_NAME = {
0x01: 'VALUINST', 0x02: 'VMEMEXEC', 0x03: 'ALUEXEC', 0x04: 'IMMEDIATE',
0x05: 'IMMEDIATE_MASK', 0x06: 'WAVERDY', 0x07: 'TS_DELTA_S8_W3',
0x08: 'WAVEEND', 0x09: 'WAVESTART', 0x0A: 'TS_DELTA_S5_W2',
0x0B: 'WAVEALLOC', 0x0C: 'TS_DELTA_S5_W3', 0x0D: 'PERF',
0x0F: 'TS_DELTA_SHORT', 0x10: 'NOP', 0x11: 'TS_WAVE_STATE',
0x12: 'EVENT', 0x13: 'EVENT_BIG', 0x14: 'REG', 0x15: 'SNAPSHOT',
0x16: 'TS_DELTA_OR_MARK', 0x17: 'LAYOUT_HEADER', 0x18: 'INST',
0x19: 'UTILCTR', 0x00: 'NOP',
}
# Reference budget values (nibbles for NEXT packet) from rocprof
REFERENCE_BUDGET_NIBBLES = {
'VALUINST': 3, 'VMEMEXEC': 2, 'ALUEXEC': 2, 'IMMEDIATE': 3,
'IMMEDIATE_MASK': 6, 'WAVERDY': 6, 'TS_DELTA_S8_W3': 16,
'WAVEEND': 5, 'WAVESTART': 8, 'TS_DELTA_S5_W2': 12,
'WAVEALLOC': 5, 'TS_DELTA_S5_W3': 13, 'PERF': 7,
'TS_DELTA_SHORT': 2, 'NOP': 1, 'TS_WAVE_STATE': 6,
'EVENT': 6, 'EVENT_BIG': 8, 'REG': 16, 'SNAPSHOT': 16,
'TS_DELTA_OR_MARK': 12, 'LAYOUT_HEADER': 16, 'INST': 5,
'UTILCTR': 12,
}
class TestEncodingsMatchStateTable(unittest.TestCase):
"""Verify encoding patterns produce the correct state decode table."""
def test_all_256_bytes_decode_correctly(self):
"""Each byte value should decode to the same packet type as reference."""
mismatches = []
for byte_val in range(256):
ref_opcode = REFERENCE_STATE_TABLE[byte_val]
ref_name = OLD_OPCODE_TO_NAME.get(ref_opcode, f"UNK_{ref_opcode:02x}")
our_opcode = STATE_TO_OPCODE[byte_val]
our_name = OPCODE_TO_CLASS[our_opcode].__name__
if ref_name != our_name:
mismatches.append((byte_val, ref_name, our_name))
if mismatches:
msg = "\n".join(f" 0x{b:02x}: expected {r}, got {o}" for b, r, o in mismatches[:10])
self.fail(f"State table mismatches ({len(mismatches)} total):\n{msg}")
class TestPacketSizesMatchBudget(unittest.TestCase):
"""Verify packet sizes (from field definitions) match expected budget values."""
def test_all_packet_sizes(self):
"""Each packet type's size should match the reference budget."""
for pkt_cls in PACKET_TYPES:
name = pkt_cls.__name__
expected = REFERENCE_BUDGET_NIBBLES.get(name)
if expected is None:
continue
actual = pkt_cls.size_nibbles()
self.assertEqual(expected, actual,
f"{name}: expected {expected} nibbles, got {actual} (size_bits={pkt_cls.size_bits()})")
class TestFieldExtraction(unittest.TestCase):
"""Test that field values are extracted correctly."""
def test_valuinst(self):
reg = 0b11110_1_001_011 # wave=0x1E, flag=1, delta=1
pkt = VALUINST.from_raw(reg)
self.assertEqual(pkt.delta, 1)
self.assertEqual(pkt.flag, 1)
self.assertEqual(pkt.wave, 0x1E)
def test_vmemexec_enum(self):
reg = 0b11_00_1111 # src=3 (VMEM_ALT), delta=0
pkt = VMEMEXEC.from_raw(reg)
self.assertEqual(pkt.src, MemSrc.VMEM_ALT)
def test_aluexec_enum(self):
reg = 0b10_01_1110 # src=2 (VALU), delta=1
pkt = ALUEXEC.from_raw(reg)
self.assertEqual(pkt.src, AluSrc.VALU)
def test_waveend(self):
reg = (0x15 << 15) | (0x7 << 11) | (0x3 << 9) | (1 << 8) | 0b10101
pkt = WAVEEND.from_raw(reg)
self.assertEqual(pkt.flag7, 1)
self.assertEqual(pkt.simd, 3)
self.assertEqual(pkt.cu_lo, 7)
self.assertEqual(pkt.wave, 0x15)
self.assertEqual(pkt.cu, 0xF) # cu_lo | (flag7 << 3) = 7 | 8 = 15
def test_wavestart(self):
reg = (0x7F << 18) | (0x15 << 13) | (0x7 << 10) | (0x3 << 8) | (1 << 7) | 0b01100
pkt = WAVESTART.from_raw(reg)
self.assertEqual(pkt.flag7, 1)
self.assertEqual(pkt.simd, 3)
self.assertEqual(pkt.cu_lo, 7)
self.assertEqual(pkt.wave, 0x15)
self.assertEqual(pkt.id7, 0x7F)
self.assertEqual(pkt.cu, 0xF)
def test_inst_enum(self):
reg = (0x21 << 13) | (0x15 << 8) | (1 << 7) | (1 << 3) | 0b010
pkt = INST.from_raw(reg)
self.assertEqual(pkt.flag1, 1)
self.assertEqual(pkt.flag2, 1)
self.assertEqual(pkt.wave, 0x15)
self.assertEqual(pkt.op, InstOp.VMEM_LOAD)
def test_layout_header(self):
reg = (0b101 << 33) | (0b1010 << 28) | (0b111 << 15) | (0b11 << 13) | (0b101010 << 7) | 0b0010001
pkt = LAYOUT_HEADER.from_raw(reg)
self.assertEqual(pkt.layout, 0b101010)
self.assertEqual(pkt.simd, 0b11)
self.assertEqual(pkt.group, 0b111)
self.assertEqual(pkt.sel_a, 0b1010)
self.assertEqual(pkt.sel_b, 0b101)
def test_ts_delta_or_mark_modes(self):
# delta mode: bit9=0, bit8=0
pkt_delta = TS_DELTA_OR_MARK.from_raw(0b0000001) # just the encoding pattern
self.assertFalse(pkt_delta.is_marker)
# marker mode: bit9=1, bit8=0
pkt_marker = TS_DELTA_OR_MARK.from_raw(0b0000001 | (1 << 9)) # bit9=1, bit8=0
self.assertTrue(pkt_marker.is_marker)
# other mode: bit9=1, bit8=1 (not marker)
pkt_other = TS_DELTA_OR_MARK.from_raw(0b0000001 | (1 << 8) | (1 << 9))
self.assertFalse(pkt_other.is_marker)
def test_reg(self):
# REG fields: slot=bits[9:7], hi_byte=bits[15:8], subop=bits[31:16], val32=bits[63:32]
# Note: slot[2:1] overlaps with hi_byte[1:0], so we need to set them consistently
# hi_byte=0x55 means bits 8-15 = 0b01010101, so slot bits 8-9 = 0b01
# slot bit 7 = 1, so slot = 0b011 = 3
reg = (0xDEADBEEF << 32) | (0xCAFE << 16) | (0x55 << 8) | (1 << 7) | 0b1001
pkt = REG.from_raw(reg)
self.assertEqual(pkt.slot, 0b011) # bit7=1, bits 8-9 from hi_byte low 2 bits = 01
self.assertEqual(pkt.hi_byte, 0x55)
self.assertEqual(pkt.subop, 0xCAFE)
self.assertEqual(pkt.val32, 0xDEADBEEF)
class TestRoundtrip(unittest.TestCase):
"""Test encode/decode roundtrip."""
def test_simple_roundtrip(self):
"""Test encode/decode roundtrip preserves packet types."""
test_packets = [
LAYOUT_HEADER.from_raw(0x100),
WAVESTART.from_raw(0x0),
INST.from_raw(0x10),
INST.from_raw(0x10),
WAVEEND.from_raw(0x40),
]
encoded = encode(test_packets)
decoded = decode(encoded)
self.assertGreaterEqual(len(decoded), len(test_packets))
for i, (orig, dec) in enumerate(zip(test_packets, decoded)):
self.assertEqual(type(orig), type(dec), f"type mismatch at {i}")
if __name__ == "__main__":
unittest.main()
+269 -159
View File
@@ -6,15 +6,14 @@
# Workgroup: 128 threads (arranged as 32x4 for coalesced memory access)
# Inner loop: 8 iterations per K-block, processing 8 columns of A and 8 rows of B
#
# Accumulators: 128 vgprs (v[2-129])
# Accumulators: 128 vgprs (v[2-117], v[120-124], v[126-129], v[131-133])
import numpy as np
from pathlib import Path
from tinygrad import Tensor, Device, Context, GlobalCounters
from tinygrad.uop.ops import UOp, Ops, KernelInfo
from tinygrad.helpers import getenv, colored
from tinygrad.engine.realize import Estimates
from extra.assembly.amd.dsl import s, v, VCC_LO, NULL
from tinygrad.engine.realize import Runner, Estimates, ExecItem
from extra.assembly.amd.dsl import s, v, VCC_LO, RawImm, EXEC_LO
from extra.assembly.amd.autogen.rdna3.ins import *
# =============================================================================
@@ -27,25 +26,30 @@ LDS_B_STRIDE = 0x200 # LDS stride for B tile (512 bytes)
LDS_BASE_OFFSET = 0x1080 # Base LDS offset for tiles
ADDR_MASK = 0x3fffff80 # Address alignment mask
# s_waitcnt encodings: wait for memory operations to complete
WAIT_LGKM = 64519 # wait for LDS/GDS/KMEM (lgkm_cnt=0)
WAIT_ALL = 0 # wait for everything
WAIT_VMEM = 1015 # wait for VMEM only (vm_cnt=0, lgkm_cnt=63)
# =============================================================================
# Named register assignments (VGPRs)
# Named register assignments (VGPRs) - COMPACT LAYOUT
# =============================================================================
V_LANE_ID = 0 # lane_id set on startup
# Use tile gaps (v146-159) for named regs to minimize max VGPR
V_LANE_ID_MOD8 = 146 # lane_id & 7
V_LANE_MOD8_X4 = 147 # (lane_id & 7) << 2
V_LANE_DIV8_X4 = 150 # ((lane_id >> 3) & 3) << 2
V_LDS_B_BASE = 151 # LDS B-tile base address for inner loop
V_LDS_A_BASE = 154 # LDS A-tile base address for inner loop
V_GLOBAL_A_ADDR = 155 # global memory A prefetch address
V_GLOBAL_B_ADDR = 158 # global memory B prefetch address
V_LDS_A_ADDR = 159 # single base register for A stores
V_LDS_B_ADDR = 162 # single base register for B stores
V_LANE_ID_MOD8 = 214 # lane_id & 7 (column within 8-wide tile chunk)
V_OUTPUT_ROW = 131 # output row coordinate
V_LANE_MOD8_X4 = 134 # V_LANE_ID_MOD8 << 2 (byte offset)
V_LANE_DIV8_X4 = 135 # (lane_id >> 3) << 2
V_ADDR_HI_ZERO = 136 # always 0 (for 64-bit address high bits)
V_LDS_A_BASE = 133 # LDS A-tile base address for inner loop (in ACC_RESERVED gap)
V_LDS_B_BASE = 130 # LDS B-tile base address for inner loop (in ACC_RESERVED gap)
V_GLOBAL_A_ADDR = 131 # global memory A prefetch address (reuses V_OUTPUT_ROW slot during main loop)
V_GLOBAL_B_ADDR = 154 # global memory B prefetch address
# LDS tile register destinations - SEPARATE from DATA to avoid overlap
# DATA regs (v155-170) receive global prefetch
# A on banks 2-3, B on banks 0-1 to avoid bank conflicts in VOPD
V_A_TILE_REGS = [130, 134, 138, 142] # A tile: banks 2,2,2,2 (130%4=2, etc.)
V_B_TILE_REGS = [132, 136, 140, 144, 148, 152, 156, 160] # B tile: banks 0,0,0,0,0,0,0,0
# This layout matches kernel8's optimization for VGPR cache utilization
V_A_TILE_REGS = [186, 190, 194, 198] # A tile: banks 2,2,2,2 (186%4=2, 190%4=2, etc.)
V_B_TILE_REGS = [184, 188, 192, 196, 200, 204, 208, 212] # B tile: banks 0,0,0,0,0,0,0,0
# =============================================================================
# Named register assignments (SGPRs)
@@ -55,11 +59,14 @@ S_TILE_X = 2 # workgroup_x << 7
S_TILE_Y = 3 # workgroup_y << 7
S_DIM_N = 4 # matrix dimension N
S_LOOP_BOUND = 7 # K-8 (loop termination bound)
S_A_PTR = (8, 9) # A matrix base pointer
S_B_PTR = (10, 11) # B matrix base pointer
S_LOOP_CTR = 12 # loop counter (increments by 8)
S_PREFETCH_FLAG = 13 # prefetch condition flag / row stride in epilogue
S_WORKGROUP_X = 14 # workgroup_id_x
S_WORKGROUP_Y = 15 # workgroup_id_y
# Kernarg load destinations
# Kernarg load destinations (before copy to working regs)
S_KERNARG_OUT = (16, 17) # output pointer from kernarg
S_KERNARG_A = (20, 21) # A pointer from kernarg
S_KERNARG_B = (22, 23) # B pointer from kernarg
# Prefetch base pointers (8 pairs each, 16KB/256KB apart)
@@ -94,6 +101,8 @@ FMAC_PAIR_ORDER = [
def derive_fmac_pattern(acc_grid, a_tile_regs=None, b_tile_regs=None):
"""Generate 64 dual FMAC ops from accumulator grid with optimized iteration order."""
if a_tile_regs is None: a_tile_regs = V_A_TILE_REGS
if b_tile_regs is None: b_tile_regs = V_B_TILE_REGS
pattern = []
for idx, (a_pair, b_pair) in enumerate(FMAC_PAIR_ORDER):
a_even, a_odd = a_pair * 2, a_pair * 2 + 1
@@ -112,14 +121,14 @@ def derive_fmac_pattern(acc_grid, a_tile_regs=None, b_tile_regs=None):
return pattern
# Derived: 64 dual FMAC operations
FMAC_PATTERN = derive_fmac_pattern(ACC_GRID, V_A_TILE_REGS, V_B_TILE_REGS)
FMAC_PATTERN = derive_fmac_pattern(ACC_GRID)
def derive_permute_swaps(acc_grid, out_regs):
"""Derive swap sequence to permute accumulators from FMAC layout to output order.
After FMAC loop: acc_grid[a][b] holds C[a,b]
Output order: for row_half in 0,1; col_group in 0-3; row_in_group in 0-3; b_off in 0-3
-> need C[row_half*4 + row_in_group, col_group*4 + b_off] in specified reg order
-> need C[row_half*4 + row_in_group, col_group*4 + b_off] in descending reg order
"""
def target_ab(i):
row_half, col_group = i // 64, (i // 16) % 4
@@ -142,27 +151,34 @@ def derive_permute_swaps(acc_grid, out_regs):
return swaps
# Derived: swap sequence to arrange accumulators for output
# Each group of 4 registers is ascending for direct global_store_b128
OUT_REGS = [r for i in range(32) for r in range(126 - i*4, 130 - i*4)]
OUT_REGS = list(range(129, 1, -1))
PERMUTE_SWAPS = derive_permute_swaps(ACC_GRID, OUT_REGS)
# =============================================================================
# LDS tile staging registers
# LDS tile staging registers - COMPACT LAYOUT
# =============================================================================
# DATA regs receive contiguous global prefetch, then write to LDS
# TILE regs receive scattered LDS loads (ds_load_b64 pairs), then feed FMACs
# Contiguous layout with mod4=[3,0,1,2,3,0,1,2] for bank conflict avoidance
V_LDS_A_DATA = [163, 164, 165, 166, 167, 168, 169, 170]
V_LDS_B_DATA = [171, 172, 173, 174, 175, 176, 177, 178]
# Initial tile prefetch: (vdst, saddr_lo) - load into A data regs using B prefetch pointers (s[24:31])
INIT_PREFETCH = [(V_LDS_A_DATA[i], S_PREFETCH_B+2*i) for i in range(4)]
# These are SEPARATE - DATA lives during prefetch/store, TILE lives during inner loop
V_LDS_A_ADDR = 153 # single base register for A stores (use +512 offsets)
V_LDS_A_DATA = list(range(155, 163)) # 8 data registers for A prefetch (v155-162)
V_LDS_B_ADDR = 145 # single base register for B stores (use 16-bit offsets)
V_LDS_B_DATA = list(range(163, 171)) # 8 data registers for B prefetch (v163-170)
# Global memory prefetch schedule: (vdst1, vdst2, addr_vreg, saddr_lo1, saddr_lo2)
# First 2 pairs from B prefetch pointers (s[32:39]), next 4 pairs from A prefetch pointers (s[40:55])
PREFETCH_LOADS = [(V_LDS_A_DATA[4+2*i], V_LDS_A_DATA[4+2*i+1], V_GLOBAL_B_ADDR, S_PREFETCH_B+8+4*i, S_PREFETCH_B+10+4*i) for i in range(2)] + \
[(V_LDS_B_DATA[2*(i-2)], V_LDS_B_DATA[2*(i-2)+1], V_GLOBAL_A_ADDR, S_PREFETCH_A+4*(i-2), S_PREFETCH_A+2+4*(i-2)) for i in range(2, 6)]
# Initial tile prefetch: (vdst, saddr_lo) - load into A data regs using B prefetch pointers (s[24:31])
INIT_PREFETCH = [(V_LDS_A_DATA[i], S_PREFETCH_B+2*i) for i in range(4)]
# Initial tile loads: (vdst, addr_lo) pairs - use temp regs in accumulator gaps
INIT_TILE_LOADS = [(23,5),(24,9),(25,7),(26,2),(27,11),(28,13),(29,6),(30,8),(31,10),(12,12),(13,14),(3,2),(4,4),(5,8),(6,6),(7,10)]
# A matrix row offset registers (scattered to avoid accumulator conflicts)
ROW_REGS = list(range(137, 145)) # v137-v144 (8 regs)
# =============================================================================
# Kernel class
# =============================================================================
@@ -175,11 +191,29 @@ class Kernel:
def label(self, name): self.labels[name] = len(self.instructions)
def branch_to(self, label): self.branch_targets[len(self.instructions) - 1] = label
def add64(self, dst_lo, dst_hi, src_lo, src_hi, off):
"""s[dst_lo:dst_hi] = s[src_lo:src_hi] + off"""
if off: self.emit(s_add_u32(s[dst_lo], s[src_lo], off)); self.emit(s_addc_u32(s[dst_hi], s[src_hi], 0))
elif dst_lo != src_lo: self.emit(s_mov_b64(s[dst_lo:dst_hi], s[src_lo:src_hi]))
def global_load(self, vdst, addr, saddr=None):
"""Global load b32"""
self.emit(global_load_b32(vdst=v[vdst], addr=v[addr:addr+1],
saddr=s[saddr:saddr+2] if saddr else RawImm(124)))
def waitcnt(self, lgkm=None, vm=None):
"""Wait for memory operations. lgkm=N waits until N lgkm ops remain, vm=N waits until N vmem ops remain."""
vmcnt, lgkmcnt, expcnt = vm if vm is not None else 63, lgkm if lgkm is not None else 63, 7
waitcnt = (expcnt & 0x7) | ((lgkmcnt & 0x3f) << 4) | ((vmcnt & 0x3f) << 10)
self.emit(s_waitcnt(simm16=waitcnt))
from extra.assembly.amd.asm import waitcnt as encode_waitcnt
if lgkm == 0 and vm is None: self.emit(s_waitcnt(simm16=WAIT_LGKM))
elif vm == 0 and lgkm is None: self.emit(s_waitcnt(simm16=WAIT_VMEM))
elif lgkm == 0 and vm == 0: self.emit(s_waitcnt(simm16=WAIT_ALL))
elif vm is not None and lgkm is None:
self.emit(s_waitcnt(simm16=encode_waitcnt(vmcnt=vm, expcnt=7, lgkmcnt=63)))
elif lgkm is not None and vm is None:
self.emit(s_waitcnt(simm16=encode_waitcnt(vmcnt=63, expcnt=7, lgkmcnt=lgkm)))
else: raise ValueError(f"unsupported waitcnt: lgkm={lgkm}, vm={vm}")
def barrier(self): self.emit(s_barrier())
def to_asm(self):
import re
@@ -193,17 +227,14 @@ class Kernel:
asm = re.sub(r'(s_cbranch_\w+|s_branch)\s+\S+', rf'\1 .{self.branch_targets[i]}', asm)
body.append('\t' + asm)
# limit wave occupancy by using more LDS
lds_size = max(LDS_SIZE, 65536//getenv("LIMIT_OCC", 65536))
# HSA kernel descriptor attributes (zeros included for compatibility)
hsa = [
('group_segment_fixed_size', lds_size), ('private_segment_fixed_size', 0), ('kernarg_size', 36),
('group_segment_fixed_size', LDS_SIZE), ('private_segment_fixed_size', 0), ('kernarg_size', 36),
('user_sgpr_count', 14), ('user_sgpr_dispatch_ptr', 0), ('user_sgpr_queue_ptr', 0),
('user_sgpr_kernarg_segment_ptr', 1), ('user_sgpr_dispatch_id', 0), ('user_sgpr_private_segment_size', 0),
('wavefront_size32', 1), ('uses_dynamic_stack', 0), ('enable_private_segment', 0),
('system_sgpr_workgroup_id_x', 1), ('system_sgpr_workgroup_id_y', 1), ('system_sgpr_workgroup_id_z', 0),
('system_sgpr_workgroup_info', 0), ('system_vgpr_workitem_id', 0), ('next_free_vgpr', 179),
('system_sgpr_workgroup_info', 0), ('system_vgpr_workitem_id', 0), ('next_free_vgpr', 214),
('next_free_sgpr', 16), ('float_round_mode_32', 0), ('float_round_mode_16_64', 0),
('float_denorm_mode_32', 3), ('float_denorm_mode_16_64', 3), ('dx10_clamp', 1), ('ieee_mode', 1),
('fp16_overflow', 0), ('workgroup_processor_mode', 0), ('memory_ordered', 1), ('forward_progress', 0),
@@ -218,10 +249,10 @@ class Kernel:
'\t.end_amdhsa_kernel', '\t.text', '.Lfunc_end0:', '\t.size\tkernel, .Lfunc_end0-kernel',
'\t.amdgpu_metadata', '---', 'amdhsa.kernels:', ' - .args:',
*[f' - .address_space: global\n .offset: {i*8}\n .size: 8\n .value_kind: global_buffer' for i in range(3)],
f' .group_segment_fixed_size: {lds_size}', ' .kernarg_segment_align: 8',
f' .group_segment_fixed_size: {LDS_SIZE}', ' .kernarg_segment_align: 8',
' .kernarg_segment_size: 24', ' .max_flat_workgroup_size: 128', ' .name: kernel',
' .private_segment_fixed_size: 0', ' .sgpr_count: 60', ' .symbol: kernel.kd',
' .vgpr_count: 179', ' .wavefront_size: 32', f'amdhsa.target: amdgcn-amd-amdhsa--{self.arch}',
' .vgpr_count: 214', ' .wavefront_size: 32', f'amdhsa.target: amdgcn-amd-amdhsa--{self.arch}',
'amdhsa.version:', ' - 1', ' - 2', '...', '\t.end_amdgpu_metadata'])
@@ -235,85 +266,171 @@ def build_kernel(arch='gfx1100'):
# ===========================================================================
# PROLOGUE: Load kernel arguments, compute tile coordinates and addresses
# ===========================================================================
k.emit(s_load_b128(sdata=s[S_KERNARG_A[0]:S_KERNARG_B[1]], sbase=s[0:1], offset=0x0, soffset=NULL))
k.emit(s_load_b64(sdata=s[S_OUT_PTR[0]:S_OUT_PTR[1]], sbase=s[0:1], offset=0x10, soffset=NULL))
k.emit(s_load_b128(sdata=s[S_KERNARG_A[0]:S_KERNARG_B[1]], sbase=s[0:1], offset=0x0, soffset=RawImm(124)))
k.emit(s_load_b64(sdata=s[S_KERNARG_OUT[0]:S_KERNARG_OUT[1]], sbase=s[0:1], offset=0x10, soffset=RawImm(124)))
k.emit(s_mov_b32(s[S_DIM_N], MATRIX_DIM))
k.emit(s_mov_b32(s[S_LOOP_CTR], 0)) # used by LDS swizzle, always 0 for valid workgroups
k.emit(s_lshl_b32(s[S_TILE_X], s[S_WORKGROUP_X], 7))
k.emit(s_lshl_b32(s[S_TILE_Y], s[S_WORKGROUP_Y], 7))
# Lane-derived values
k.emit(v_and_b32_e32(v[V_LANE_ID_MOD8], 7, v[V_LANE_ID]))
k.emit(v_lshrrev_b32_e32(v[4], 3, v[V_LANE_ID]))
k.emit(v_or_b32_e32(v[1], s[S_TILE_X], v[V_LANE_ID]))
k.emit(v_and_b32_e32(v[V_LANE_ID_MOD8], 7, v[0]))
k.emit(v_lshrrev_b32_e32(v[4], 3, v[0]))
k.emit(v_or_b32_e32(v[1], s[S_TILE_X], v[0]))
k.emit(v_or_b32_e32(v[22], s[S_TILE_Y], v[4]))
k.emit(v_lshlrev_b32_e32(v[V_LANE_MOD8_X4], 2, v[V_LANE_ID_MOD8]))
k.emit(v_mov_b32_e32(v[2], 0)) # v[1] always positive, sign extension is 0
k.emit(v_lshlrev_b64(v[5:6], 2, v[1:2]))
k.waitcnt(lgkm=0)
# Copy pointers to working registers
k.emit(s_mov_b64(s[S_OUT_PTR[0]:S_OUT_PTR[1]], s[S_KERNARG_OUT[0]:S_KERNARG_OUT[1]]))
k.emit(s_mov_b64(s[S_A_PTR[0]:S_A_PTR[1]], s[S_KERNARG_A[0]:S_KERNARG_A[1]]))
k.emit(s_mov_b64(s[S_B_PTR[0]:S_B_PTR[1]], s[S_KERNARG_B[0]:S_KERNARG_B[1]]))
# Compute 8 A and B matrix tile base pointers for prefetch
k.emit(s_mov_b64(s[S_PREFETCH_B:S_PREFETCH_B+1], s[S_KERNARG_B[0]:S_KERNARG_B[1]])) # B[0]: no offset
for i in range(1, 8): # B: 16KB apart
k.emit(s_add_u32(s[S_PREFETCH_B+i*2], s[S_KERNARG_B[0]], i * 0x4000))
k.emit(s_addc_u32(s[S_PREFETCH_B+i*2+1], s[S_KERNARG_B[1]], 0))
k.emit(s_mov_b64(s[S_PREFETCH_A:S_PREFETCH_A+1], s[S_KERNARG_A[0]:S_KERNARG_A[1]])) # A[0]: no offset
for i in range(1, 8): # A: 256KB apart
k.emit(s_add_u32(s[S_PREFETCH_A+i*2], s[S_KERNARG_A[0]], i * 0x40000))
k.emit(s_addc_u32(s[S_PREFETCH_A+i*2+1], s[S_KERNARG_A[1]], 0))
for i in range(8): k.add64(S_PREFETCH_B + i*2, S_PREFETCH_B + i*2 + 1, S_KERNARG_B[0], S_KERNARG_B[1], i * 0x4000) # B: 16KB apart
for i in range(8): k.add64(S_PREFETCH_A + i*2, S_PREFETCH_A + i*2 + 1, S_KERNARG_A[0], S_KERNARG_A[1], i * 0x40000) # A: 256KB apart
# Global prefetch addresses: B = (tile_x + lane_id) * 4, A = ((tile_y << 12) + (lane_id/8)*4K + lane_id%8) * 4
k.emit(v_add_nc_u32_e32(v[V_GLOBAL_B_ADDR], s[S_TILE_X], v[V_LANE_ID]))
k.emit(v_add_nc_u32_e32(v[V_GLOBAL_B_ADDR], s[S_TILE_X], v[0]))
k.emit(v_lshlrev_b32_e32(v[V_GLOBAL_B_ADDR], 2, v[V_GLOBAL_B_ADDR]))
k.emit(s_lshl_b32(s[19], s[S_TILE_Y], 12))
k.emit(v_lshl_add_u32(v[V_GLOBAL_A_ADDR], v[4], 12, v[V_LANE_ID_MOD8])) # (lane_id/8)*4K + lane_id%8
k.emit(v_add_nc_u32_e32(v[V_GLOBAL_A_ADDR], s[19], v[V_GLOBAL_A_ADDR]))
k.emit(v_lshlrev_b32_e32(v[V_GLOBAL_A_ADDR], 2, v[V_GLOBAL_A_ADDR]))
# Do initial loads
for vdst, saddr_lo in INIT_PREFETCH:
k.emit(global_load_b32(vdst=v[vdst], addr=v[V_GLOBAL_B_ADDR], saddr=s[saddr_lo:saddr_lo+1]))
for iter in range(6):
vdst1, vdst2, addr, slo1, slo2 = PREFETCH_LOADS[iter]
k.emit(global_load_b32(vdst=v[vdst1], addr=v[addr], saddr=s[slo1:slo1+1]))
k.emit(global_load_b32(vdst=v[vdst2], addr=v[addr], saddr=s[slo2:slo2+1]))
# ===========================================================================
# Tile address computation for initial A/B matrix loads
# ===========================================================================
k.emit(s_lshl_b32(s[S_LOOP_BOUND], s[S_DIM_N], 4)) # row stride = 16*N
k.emit(v_mul_lo_u32(v[ROW_REGS[0]], v[22], s[S_DIM_N])) # A matrix row offsets
for i in range(1, 8): k.emit(v_add_nc_u32_e32(v[ROW_REGS[i]], s[S_LOOP_BOUND], v[ROW_REGS[i-1]]))
def addr64(dst, base_s): # 64-bit address: v[dst:dst+1] = s[base_s:base_s+1] + v[dst]*4
k.emit(v_mov_b32_e32(v[dst+1], 0)) # offset always positive, sign ext = 0
k.emit(v_lshlrev_b64(v[dst:dst+1], 2, v[dst:dst+1]))
k.emit(v_add_co_u32(v[dst], VCC_LO, s[base_s], v[dst]))
k.emit(v_add_co_ci_u32_e32(v[dst+1], s[base_s+1], v[dst+1]))
def b_addr(dst, mult, tmp=None): # B address for col + mult*N
tmp = tmp if tmp is not None else dst
k.emit(v_mad_u32_u24(v[tmp], s[S_DIM_N], mult, v[1]))
if tmp != dst:
k.emit(v_mov_b32_e32(v[tmp+1], 0)) # offset always positive
k.emit(v_lshlrev_b64(v[dst:dst+1], 2, v[tmp:tmp+1]))
k.emit(v_add_co_u32(v[dst], VCC_LO, s[S_B_PTR[0]], v[dst]))
k.emit(v_add_co_ci_u32_e32(v[dst+1], s[S_B_PTR[1]], v[dst+1]))
else: addr64(dst, S_B_PTR[0])
def a_addr(dst, row_reg, tmp): # A address for row_reg + lane_id_mod8
k.emit(v_add_nc_u32_e32(v[tmp], v[row_reg], v[V_LANE_ID_MOD8]))
k.emit(v_mov_b32_e32(v[tmp+1], 0)) # offset always positive
k.emit(v_lshlrev_b64(v[dst:dst+1], 2, v[tmp:tmp+1]))
k.emit(v_add_co_u32(v[dst], VCC_LO, s[S_A_PTR[0]], v[dst]))
k.emit(v_add_co_ci_u32_e32(v[dst+1], s[S_A_PTR[1]], v[dst+1]))
# Batch 1: B addresses (cols 0-5) and loads
k.emit(v_add_co_u32(v[5], VCC_LO, s[S_B_PTR[0]], v[5]))
k.emit(v_add_co_ci_u32_e32(v[6], s[S_B_PTR[1]], v[6]))
for dst, mult in [(9,1), (7,2), (2,3), (11,4), (13,5)]: b_addr(dst, mult)
k.emit(s_clause(simm16=5)) # 6 consecutive global loads
for vdst, addr in INIT_TILE_LOADS[:6]: k.global_load(vdst, addr)
# Batch 2: A addresses (rows 0-4) and loads
for dst, ri in [(6,0), (8,1), (10,2), (12,3), (14,4)]:
k.emit(v_add_nc_u32_e32(v[dst], v[ROW_REGS[ri]], v[V_LANE_ID_MOD8]))
addr64(dst, S_A_PTR[0])
k.emit(s_clause(simm16=4)) # 5 consecutive global loads
for vdst, addr in INIT_TILE_LOADS[6:11]: k.global_load(vdst, addr)
# Batch 3: B cols 6-7, A rows 5-7, and loads
for dst, mult, tmp in [(2,6,15), (4,7,4)]: b_addr(dst, mult, tmp)
for dst, ri, tmp in [(8,5,16), (6,6,18), (10,7,20)]: a_addr(dst, ROW_REGS[ri], tmp)
k.emit(s_clause(simm16=4)) # 5 consecutive global loads
for vdst, addr in INIT_TILE_LOADS[11:]: k.global_load(vdst, addr)
# ===========================================================================
# LDS store address computation (bank-conflict-avoiding swizzle)
# ===========================================================================
# This section computes LDS store addresses with a swizzle pattern to avoid bank conflicts.
# Key outputs:
# v[8]: A-tile initial store base (used only for initial stores with stride64)
# V_LDS_B_ADDR (v145): B-tile store base (used for both initial and main loop)
# V_LANE_DIV8_X4 (v135): (lane_id >> 3) << 2 for epilogue
#
# The swizzle ensures that threads in the same wavefront write to different LDS banks.
# Formula: swizzled_addr = base + (lane_id & 7) * LDS_A_STRIDE + swizzle_offset
# where swizzle_offset depends on (lane_id >> 3) to distribute across banks.
k.emit(v_add_nc_u32_e32(v[9], s[S_LOOP_CTR], v[22])) # row 0 base
# v[22] = tile_y | (lane_id >> 3) from prologue, used as base for row offsets
# Compute 7 row offsets for B-tile rows 1-7 (row 0 computed separately in v[9])
k.emit(v_add_nc_u32_e32(v[9], s[S_LOOP_CTR], v[22])) # row 0 base (S_LOOP_CTR=0)
for i in range(7): k.emit(v_or_b32_e32(v[10 + i if i < 2 else 12 + i], 16 * (i + 1), v[22])) # rows 1-7
# Extract sign bit of workgroup_x (always 0 for valid workgroups, used for masking)
k.emit(s_bfe_i32(s[S_LOOP_BOUND], s[S_WORKGROUP_X], 0x10018))
k.emit(v_and_b32_e32(v[9], ADDR_MASK, v[9]))
k.emit(s_lshr_b32(s[S_LOOP_BOUND], s[S_LOOP_BOUND], 25))
# Compute masked row offsets for bank conflict avoidance pattern
# Pattern: v[row] = row_val - (row_val & ADDR_MASK) extracts lower bits
k.emit(v_add_nc_u32_e32(v[19], s[S_LOOP_CTR], v[10]))
k.emit(v_add_nc_u32_e32(v[8], s[S_LOOP_BOUND], v[1])) # A-tile base computation
for d, r in zip([20, 21, 32, 33, 34, 35], [11, 14, 15, 16, 17, 18]):
k.emit(v_add_nc_u32_e32(v[d], s[S_LOOP_CTR], v[r]))
k.emit(v_and_b32_e32(v[8], ADDR_MASK, v[8]))
k.emit(v_sub_nc_u32_e32(v[9], v[22], v[9])) # row 0 swizzle offset
k.emit(v_lshlrev_b32_e32(v[9], 2, v[9])) # * 4
for d, s_ in zip([19, 20, 21, 22, 32, 33, 34], [20, 21, 22, 32, 33, 34, 35]):
k.emit(v_and_b32_e32(v[d], ADDR_MASK, v[s_]))
k.emit(v_sub_nc_u32_e32(v[8], v[1], v[8])) # A-tile swizzle
# Apply swizzle offsets and scale to byte offsets
k.emit(v_lshlrev_b32_e32(v[9], 2, v[9])) # row 0 offset * 4
for r, t in zip([10, 11, 14, 15, 16, 17, 18], [19, 20, 21, 22, 32, 33, 34]):
k.emit(v_sub_nc_u32_e32(v[r], v[r], v[t])) # rows 1-7 swizzle
k.emit(v_bfe_u32(v[2], v[0], 3, 2)) # v[2] = (lane_id >> 3) & 3
k.emit(v_lshlrev_b32_e32(v[8], 2, v[8])) # A-tile base * 4
# Compute B-tile base address: LDS_A_STRIDE * (lane_id % 8) + row0_offset
k.emit(v_mad_u32_u24(v[V_LDS_B_ADDR], LDS_A_STRIDE, v[V_LANE_ID_MOD8], v[9]))
# For V_LDS_A_BASE and epilogue
k.emit(v_bfe_u32(v[2], v[V_LANE_ID], 3, 2)) # v[2] = (lane_id >> 3) & 3
# Scale row offsets 1-7 to byte offsets (row 0 already in v[9])
for d, r in zip([9, 10, 11, 14, 15, 16, 17], [10, 11, 14, 15, 16, 17, 18]):
k.emit(v_lshlrev_b32_e32(v[d], 2, v[r]))
k.emit(v_lshlrev_b32_e32(v[V_LANE_DIV8_X4], 2, v[2]))
k.emit(v_add_nc_u32_e32(v[8], 0x80, v[8])) # A-tile initial store base + 128
# Compute LDS load/store base addresses for inner loop
# Store initial tile data to LDS
k.waitcnt(vm=0)
for i, (d0, d1) in enumerate([(0,1), (2,3), (4,5), (11,12)]):
k.emit(ds_store_2addr_stride64_b32(addr=v[8], data0=v[INIT_TILE_LOADS[d0][0]], data1=v[INIT_TILE_LOADS[d1][0]], offset0=16+i*4, offset1=18+i*4))
# B stores: single base with offsets 0,64,128,192,256,320,384,448
for i, idx in enumerate([6,7,8,9,10,13,14,15]):
offset = i * 64
k.emit(ds_store_b32(addr=v[V_LDS_B_ADDR], data0=v[INIT_TILE_LOADS[idx][0]], offset0=offset & 0xFF, offset1=offset >> 8))
k.waitcnt(lgkm=0)
k.barrier()
# ===========================================================================
# INIT: Compute LDS base addresses, then zero accumulators
# ===========================================================================
# v[3] = v[1] & 0x7F (lower 7 bits) since S_LOOP_BOUND=0 for valid workgroups
k.emit(v_lshlrev_b32_e32(v[2], 4, v[2]))
k.emit(v_and_b32_e32(v[3], 0x7F, v[1])) # simplified from 3 lines
k.emit(v_add_nc_u32_e32(v[3], s[S_LOOP_BOUND], v[1]))
k.emit(v_and_b32_e32(v[3], ADDR_MASK, v[3]))
k.emit(v_sub_nc_u32_e32(v[3], v[1], v[3]))
k.emit(v_lshl_or_b32(v[V_LDS_B_BASE], v[V_LANE_ID_MOD8], 4, LDS_BASE_OFFSET))
k.emit(v_lshl_add_u32(v[V_LDS_A_ADDR], v[3], 2, LDS_BASE_OFFSET))
k.emit(v_lshlrev_b32_e32(v[3], 2, v[V_LANE_ID]))
k.emit(v_lshlrev_b32_e32(v[3], 2, v[0]))
k.emit(v_and_or_b32(v[V_LDS_A_BASE], 0x180, v[3], v[2]))
# Do initial stores
k.waitcnt(vm=0)
for i in range(4): # A tile: 8 values via 4 stride64 stores
k.emit(ds_store_2addr_stride64_b32(addr=v[V_LDS_A_ADDR], data0=v[V_LDS_A_DATA[i*2]], data1=v[V_LDS_A_DATA[i*2+1]], offset0=i*4, offset1=i*4+2))
for i in range(8): # B tile: 8 values via 8 scalar stores with 64-byte spacing
offset = i * 64
k.emit(ds_store_b32(addr=v[V_LDS_B_ADDR], data0=v[V_LDS_B_DATA[i]], offset0=offset & 0xFF, offset1=offset >> 8))
# Zero all 128 accumulators using VOPD dual moves (64 instructions instead of 128)
for i in range(0, len(OUT_REGS), 2):
k.emit(VOPD(VOPDOp.V_DUAL_MOV_B32, VOPDOp.V_DUAL_MOV_B32, vdstx=v[OUT_REGS[i]], vdsty=v[OUT_REGS[i+1]], srcx0=0, srcy0=0))
k.emit(s_add_i32(s[S_LOOP_BOUND], s[S_DIM_N], -8))
k.emit(s_add_i32(s[S_LOOP_BOUND], s[S_DIM_N], -8))
k.emit(s_add_u32(s[S_A_PTR[0]], s[S_A_PTR[0]], 32))
k.emit(s_addc_u32(s[S_A_PTR[1]], s[S_A_PTR[1]], 0))
# S_LOOP_CTR is already 0 from prologue initialization
k.emit(s_branch(simm16=0)); k.branch_to('LOOP_ENTRY')
@@ -333,36 +450,21 @@ def build_kernel(arch='gfx1100'):
k.emit(s_cselect_b32(s[S_PREFETCH_FLAG], -1, 0)) # s_cselect doesn't modify SCC
k.emit(s_cbranch_scc0(simm16=0)); k.branch_to('SKIP_PREFETCH') # branch if loop_ctr >= loop_bound
# Advance prefetch pointers
k.emit(v_add_nc_u32_e32(v[V_GLOBAL_B_ADDR], 0x20000, v[V_GLOBAL_B_ADDR]))
k.emit(v_add_nc_u32_e32(v[V_GLOBAL_A_ADDR], 0x20, v[V_GLOBAL_A_ADDR]))
if not NO_GLOBAL:
# Advance prefetch pointers (VGPR)
#k.emit(v_add_nc_u32_e32(v[V_GLOBAL_B_ADDR], 0x20000, v[V_GLOBAL_B_ADDR]))
#k.emit(v_add_nc_u32_e32(v[V_GLOBAL_A_ADDR], 0x20, v[V_GLOBAL_A_ADDR]))
# Advance prefetch pointers (64-bit adds)
k.emit(s_clause(simm16=31))
for i in range(8):
k.emit(s_add_u32(s[S_PREFETCH_B+i*2], s[S_PREFETCH_B+i*2], 0x20000))
k.emit(s_addc_u32(s[S_PREFETCH_B+i*2+1], s[S_PREFETCH_B+i*2+1], 0))
for i in range(8):
k.emit(s_add_u32(s[S_PREFETCH_A+i*2], s[S_PREFETCH_A+i*2], 0x20))
k.emit(s_addc_u32(s[S_PREFETCH_A+i*2+1], s[S_PREFETCH_A+i*2+1], 0))
# do the fetch
for vdst, saddr_lo in INIT_PREFETCH:
k.emit(global_load_b32(vdst=v[vdst], addr=v[V_GLOBAL_B_ADDR], saddr=s[saddr_lo:saddr_lo+1]))
k.global_load(vdst, V_GLOBAL_B_ADDR, saddr_lo)
k.label('SKIP_PREFETCH')
# wait for local stores to finish (either initial or loop)
# then sync the warp so it's safe to load local
k.waitcnt(lgkm=0)
k.emit(s_barrier())
# 8 inner loop iterations
for iter in range(8):
# Load A tile (4 pairs) and B tile (8 pairs) from LDS
if not NO_DS:
k.emit(s_clause(simm16=len(V_A_TILE_REGS) + len(V_B_TILE_REGS) - 1)) # 12 loads total: 4 A + 8 B
k.emit(s_clause(simm16=11)) # 12 loads total: 4 A + 8 B
# A tile: 4 ds_load_b64
for i, vdst in enumerate(V_A_TILE_REGS):
a_off = (i & 1) * 8 + (i >> 1) * 64 + iter * LDS_A_STRIDE
@@ -371,37 +473,38 @@ def build_kernel(arch='gfx1100'):
for i, vdst in enumerate(V_B_TILE_REGS):
b_off = (i & 1) * 8 + (i & 2) * 64 + (i >> 2) * 256 + iter * LDS_B_STRIDE
k.emit(ds_load_b64(vdst=v[vdst:vdst+1], addr=v[V_LDS_B_BASE], offset0=b_off & 0xFF, offset1=b_off >> 8))
# Issue global prefetch (first 6 iterations only)
if iter < 6 and not NO_GLOBAL:
vdst1, vdst2, addr, slo1, slo2 = PREFETCH_LOADS[iter]
k.emit(global_load_b32(vdst=v[vdst1], addr=v[addr], saddr=s[slo1:slo1+1]))
k.emit(global_load_b32(vdst=v[vdst2], addr=v[addr], saddr=s[slo2:slo2+1]))
k.waitcnt(lgkm=0)
# 64 dual FMACs
k.waitcnt(lgkm=0)
k.emit(s_clause(simm16=len(FMAC_PATTERN)-1))
k.emit(s_clause(simm16=63))
for i, (vdst_x, vdst_y, ax, bx, ay, by) in enumerate(FMAC_PATTERN):
k.emit(VOPD(VOPDOp.V_DUAL_FMAC_F32, VOPDOp.V_DUAL_FMAC_F32,
vdstx=v[vdst_x], vdsty=v[vdst_y], srcx0=v[ax], vsrcx1=v[bx], srcy0=v[ay], vsrcy1=v[by]))
# wait for all global loads to finish
# then sync the warp so it's safe to store local
# Issue global prefetch AFTER FMACs (first 6 iterations only)
if iter < 6 and not NO_GLOBAL:
vdst1, vdst2, addr, slo1, slo2 = PREFETCH_LOADS[iter]
k.global_load(vdst1, addr, slo1)
k.global_load(vdst2, addr, slo2)
k.emit(s_and_not1_b32(VCC_LO, EXEC_LO, s[S_PREFETCH_FLAG]))
k.waitcnt(vm=0)
k.emit(s_barrier())
k.barrier()
k.emit(s_cbranch_vccnz(simm16=0)); k.branch_to('LOOP_INC')
# Store prefetched data to LDS
# NOTE: Register naming reflects LDS tile organization, not source matrix:
# V_LDS_A_DATA (v155-162) holds data that goes to LDS A-tile region
# V_LDS_B_DATA (v163-170) holds data that goes to LDS B-tile region
# The data sources are swapped: A-tile receives B matrix rows, B-tile receives A matrix columns
if not NO_DS:
for i in range(4): # A tile: 8 values via 4 stride64 stores
k.emit(ds_store_2addr_stride64_b32(addr=v[V_LDS_A_ADDR], data0=v[V_LDS_A_DATA[i*2]], data1=v[V_LDS_A_DATA[i*2+1]], offset0=i*4, offset1=i*4+2))
for i in range(8): # B tile: 8 values via 8 scalar stores with 64-byte spacing
offset = i * 64
k.emit(ds_store_b32(addr=v[V_LDS_B_ADDR], data0=v[V_LDS_B_DATA[i]], offset0=offset & 0xFF, offset1=offset >> 8))
for i in range(4): # A tile: 8 values via 4 stride64 stores
k.emit(ds_store_2addr_stride64_b32(addr=v[V_LDS_A_ADDR], data0=v[V_LDS_A_DATA[i*2]], data1=v[V_LDS_A_DATA[i*2+1]], offset0=i*4, offset1=i*4+2))
for i in range(8): # B tile: 8 values via 8 scalar stores with 64-byte spacing
offset = i * 64
k.emit(ds_store_b32(addr=v[V_LDS_B_ADDR], data0=v[V_LDS_B_DATA[i]], offset0=offset & 0xFF, offset1=offset >> 8))
k.waitcnt(lgkm=0)
k.barrier()
k.emit(s_branch(simm16=0)); k.branch_to('LOOP_INC')
# ===========================================================================
@@ -413,49 +516,56 @@ def build_kernel(arch='gfx1100'):
for a, b in PERMUTE_SWAPS:
k.emit(v_swap_b32_e32(v[a], v[b]))
# Compute output base coordinates
# v[130] = col_base = tile_x + (lane_id & 7) * 4
# v[131] = row_base = tile_y + (lane_id & 0x60) + ((lane_id >> 3) & 3) * 4
# v[132] = 0 (for 64-bit address high part)
k.emit(v_add_nc_u32_e32(v[130], s[S_TILE_X], v[V_LANE_MOD8_X4]))
k.emit(v_and_b32_e32(v[131], 0x60, v[V_LANE_ID]))
k.emit(v_add_nc_u32_e32(v[131], s[S_TILE_Y], v[131]))
k.emit(v_add_nc_u32_e32(v[131], v[V_LANE_DIV8_X4], v[131]))
k.emit(v_mov_b32_e32(v[132], 0))
# Compute output coordinates: v[V_LANE_ID_MOD8] = col, v[V_OUTPUT_ROW] = row
k.emit(VOPD(VOPDOp.V_DUAL_MOV_B32, VOPDOp.V_DUAL_MOV_B32,
vdstx=v[149], vdsty=v[150], srcx0=v[V_LANE_MOD8_X4], vsrcx1=v[0], srcy0=v[V_LANE_DIV8_X4], vsrcy1=v[0]))
k.emit(v_and_b32_e32(v[0], 0x60, v[0]))
k.emit(v_or_b32_e32(v[V_LANE_ID_MOD8], s[S_TILE_X], v[149]))
k.emit(v_add_nc_u32_e32(v[0], s[S_TILE_Y], v[0]))
k.emit(v_or_b32_e32(v[V_OUTPUT_ROW], v[0], v[150]))
# Precompute row offsets: v[133-136] for rows 0-3, v[137-140] for rows 16-19
for base, row_off in [(133, 0), (137, 16)]:
if row_off: k.emit(v_add_nc_u32_e32(v[141], row_off, v[131]))
k.emit(v_mul_lo_u32(v[base], v[141] if row_off else v[131], s[S_DIM_N]))
for j in range(3): k.emit(v_add_nc_u32_e32(v[base + 1 + j], s[S_DIM_N], v[base + j]))
# Precompute row offsets: v[144-147] for rows 0-3, v[148-151] for rows 16-19
for base, row_off in [(144, 0), (148, 16)]:
if row_off: k.emit(v_or_b32_e32(v[1], row_off, v[V_OUTPUT_ROW]))
k.emit(v_mul_lo_u32(v[base], v[1] if row_off else v[V_OUTPUT_ROW], s[S_DIM_N]))
for i in range(3): k.emit(v_add_nc_u32_e32(v[base + 1 + i], s[S_DIM_N], v[base + i]))
# s[S_PREFETCH_FLAG] = row stride in bytes (N * 4)
k.emit(s_lshl_b32(s[S_PREFETCH_FLAG], s[S_DIM_N], 2))
k.emit(v_mov_b32_e32(v[V_ADDR_HI_ZERO], 0))
k.emit(s_lshl_b32(s[S_PREFETCH_FLAG], s[S_DIM_N], 2)) # row stride in bytes
# Store 128 output values as 32 groups of 4 (128-bit stores)
# Layout: 2 row halves (0-3, 16-19) x 4 col groups x 4 rows = 32 stores of 4 floats
epilogue_reserved = {V_LANE_ID_MOD8, V_OUTPUT_ROW, V_LANE_MOD8_X4, V_LANE_DIV8_X4, V_ADDR_HI_ZERO}
for i, (row_half, col_off, row_in_group) in enumerate([(rh, co, ri)
for rh in range(2) for co in [0, 32, 64, 96] for ri in range(4)]):
row = row_half * 16 + row_in_group
src = OUT_REGS[i*4] # first reg of ascending group of 4
srcs = OUT_REGS[i*4:(i+1)*4]
if row_in_group == 0:
# First row of group: compute full address
if col_off == 0: k.emit(v_mov_b32_e32(v[141], v[130]))
else: k.emit(v_add_nc_u32_e32(v[141], col_off, v[130]))
row_base = 133 + row if row < 4 else 137 + row - 16
k.emit(v_add_nc_u32_e32(v[141], v[row_base], v[141]))
k.emit(v_lshlrev_b32_e32(v[141], 2, v[141]))
k.emit(v_add_co_u32(v[141], VCC_LO, s[S_OUT_PTR[0]], v[141]))
k.emit(v_add_co_ci_u32_e32(v[142], s[S_OUT_PTR[1]], v[132]))
else:
# Subsequent rows: add stride
k.emit(v_add_co_u32(v[141], VCC_LO, s[S_PREFETCH_FLAG], v[141]))
k.emit(v_add_co_ci_u32_e32(v[142], v[142], v[132]))
# Find temp register for scaled values (must not conflict with reserved regs)
tmp = max(srcs) + 5
while any(r in epilogue_reserved for r in range(tmp, tmp + 4)): tmp += 1
k.emit(global_store_b128(addr=v[141:142], data=v[src:src+3], saddr=NULL))
# Copy values to temp regs for output (alpha=1.0 hardcoded, so just move)
for j, src in enumerate(srcs):
k.emit(v_mov_b32_e32(v[tmp + j], v[src]))
k.emit(s_sendmsg(simm16=3)) # DEALLOC_VGPRS
# Compute output address
if row_in_group == 0: # first row: compute base address for this column group
if col_off == 0: k.emit(v_mov_b32_e32(v[0], v[V_LANE_ID_MOD8]))
else: k.emit(v_add_nc_u32_e32(v[0], col_off, v[V_LANE_ID_MOD8]))
row_base = 144 + row if row < 4 else 148 + row - 16
k.emit(v_add_nc_u32_e32(v[0], v[row_base], v[0]))
k.emit(v_lshlrev_b32_e32(v[0], 2, v[0]))
k.emit(v_add_co_u32(v[0], VCC_LO, s[S_OUT_PTR[0]], v[0]))
k.emit(v_add_co_ci_u32_e32(v[1], s[S_OUT_PTR[1]], v[V_ADDR_HI_ZERO]))
else: # subsequent rows: just add stride
k.emit(v_add_co_u32(v[0], VCC_LO, s[S_PREFETCH_FLAG], v[0]))
k.emit(v_add_co_ci_u32_e32(v[1], v[1], v[V_ADDR_HI_ZERO]))
k.emit(global_store_b128(addr=v[0:1], data=v[tmp:tmp+3], saddr=RawImm(124)))
k.emit(s_sendmsg(simm16=3))
k.emit(s_endpgm())
return k.to_asm()
@@ -479,6 +589,7 @@ def test_matmul():
print(f"Loaded stock kernel from {stock_path}")
else:
asm = build_kernel(dev.arch)
if getenv("PRINT_ASM", 0): print(asm)
binary = dev.compiler.compile(asm)
print(f"Compiled! Binary size: {len(binary)} bytes")
@@ -492,16 +603,15 @@ def test_matmul():
grid, local = (N // BLOCK_N, N // BLOCK_M, 1), (THREADS, 1, 1)
print(f"Grid: {grid}, Local: {local}")
dname:str = Device.DEFAULT
def asm_kernel(A:UOp, B:UOp, C:UOp) -> UOp:
gidxs = [UOp.special(n, f"gidx{i}") for i,n in enumerate(grid)]
lidxs = [UOp.special(n, f"lidx{i}") for i,n in enumerate(local)]
sink = UOp.sink(A.base, B.base, C.base, *gidxs, *lidxs, arg=KernelInfo(name=colored("kernel", "cyan"),
estimates=Estimates(ops=N*N*N*2, mem=N*N*4*3)))
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.DEVICE, arg=dname), UOp(Ops.LINEAR, src=(*sink.src, sink)), UOp(Ops.SOURCE, arg=asm),
UOp(Ops.BINARY, arg=binary)))
c = Tensor.custom_kernel(a, b, c, fxn=asm_kernel)[2]
ei = c.schedule()[0].lower()
_prg = dev.runtime("kernel", binary)
class AsmRunner(Runner):
def __init__(self):
super().__init__(colored("kernel", "cyan"), Device.DEFAULT, Estimates(ops=N*N*N*2, mem=N*N*4*3))
def __call__(self, rawbufs, var_vals, wait=False):
c_buf, a_buf, b_buf = [x.ensure_allocated()._buf for x in rawbufs]
return _prg(a_buf, b_buf, c_buf, global_size=grid, local_size=local, wait=wait)
ei = ExecItem(None, [c.uop.buffer, a.uop.buffer, b.uop.buffer], prg=AsmRunner())
ets = []
with Context(DEBUG=2):
@@ -513,7 +623,7 @@ def test_matmul():
with Context(DEBUG=2): tc = (a @ b).realize()
with Context(DEBUG=0): err = (c - tc).square().mean().item()
print(f"mean squared error {err}")
if err != err or err > 1e-06: raise RuntimeError("matmul is wrong!")
if err > 1e-06: raise RuntimeError("matmul is wrong!")
def run_sqtt():
"""Run with SQTT profiling and write trace files."""
+1 -1
View File
@@ -45,7 +45,7 @@ def custom_asm_gemm(C:UOp, A:UOp, B:UOp) -> UOp:
sz = UOp.variable("SZ", 256, 8192)
sink = UOp.sink(C.base, A.base, B.base, sz, lidx, gidx, arg=KernelInfo(name="gemm"))
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.DEVICE, arg=Device.DEFAULT), UOp(Ops.LINEAR, src=(*sink.src, sink)), UOp(Ops.SOURCE, arg=src)))
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.DEVICE, arg=Device.DEFAULT), UOp(Ops.LINEAR, src=(*sink.src, sink)), UOp(Ops.SOURCE, arg=src)), arg=())
C_asm = Tensor.custom_kernel(C_asm, from_torch(A), from_torch(Bt), fxn=custom_asm_gemm)[0]
+1 -1
View File
@@ -24,7 +24,7 @@ def asm_kernel() -> UOp:
src = template.replace("INSTRUCTIONS", (pathlib.Path(__file__).parent/"gemm.s").read_text())
sink = UOp.sink(a, b, c, lidx, gidx, arg=KernelInfo(name="gemm"))
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.DEVICE, arg=dname), UOp(Ops.LINEAR, src=(*sink.src, sink)), UOp(Ops.SOURCE, arg=src)))
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.DEVICE, arg=dname), UOp(Ops.LINEAR, src=(*sink.src, sink)), UOp(Ops.SOURCE, arg=src)), arg=())
if __name__ == "__main__":
test_matmul(asm_kernel(), dtype=dtypes.half, N=N)
+1 -18
View File
@@ -13,7 +13,7 @@ def get_struct(argp, stype):
def format_struct(s):
sdats = []
for field_name, *_ in s._real_fields_:
for field_name, field_type in s._fields_:
dat = getattr(s, field_name)
if isinstance(dat, int): sdats.append(f"{field_name}:0x{dat:X}")
else: sdats.append(f"{field_name}:{dat}")
@@ -46,22 +46,6 @@ def install_hook(c_function, python_function):
# *** ioctl lib end ***
import tinygrad.runtime.autogen.kfd as kfd_ioctl
import tinygrad.runtime.autogen.hsa as hsa
def print_aql_queue(read_pointer_address):
rptr_offset = getattr(hsa.amd_queue_v2_t, 'read_dispatch_id').offset
queue_base = read_pointer_address - rptr_offset
queue = hsa.amd_queue_v2_t.from_address(queue_base)
print(f" AQL Queue @ 0x{queue_base:X}:")
for field_name, *_ in hsa.amd_queue_v2_t._real_fields_:
val = getattr(queue, field_name)
if isinstance(val, int): print(f" {field_name}: 0x{val:X}")
elif hasattr(val, '_length_'):
arr_vals = [f"{format_struct(v)}" if hasattr(v, '_real_fields_') else f"{v:#X}" for v in val]
print(f" {field_name}: [{', '.join(arr_vals)}]")
elif hasattr(val, '_real_fields_'): print(f" {field_name}: {format_struct(val)}")
else: print(f" {field_name}: {val}")
def ioctls_from_header():
hdr = (pathlib.Path(__file__).parent / "kfd_ioctl.h").read_text().replace("\\\n", "")
pattern = r'#define\s+(AMDKFD_IOC_[A-Z0-9_]+)\s+AMDKFD_IOW?R?\((0x[0-9a-fA-F]+),\s+struct\s([A-Za-z0-9_]+)\)'
@@ -83,7 +67,6 @@ def ioctl(fd, request, argp):
if name == "AMDKFD_IOC_SVM":
out = ctypes.cast(s.attrs, ctypes.POINTER(kfd_ioctl.struct_kfd_ioctl_svm_attribute))
for i in range(s.nattr): print(f"{i}: {kfd_ioctl.enum_kfd_ioctl_svm_attr_type.get(out[i].type):40s}: {out[i].value:#x}")
if name == "AMDKFD_IOC_CREATE_QUEUE" and s.queue_type == kfd_ioctl.KFD_IOC_QUEUE_TYPE_COMPUTE_AQL: print_aql_queue(s.read_pointer_address)
else:
print(f"{(st-start)*1000:7.2f} ms +{et*1000.:7.2f} ms : ioctl",
f"{idir=} {size=} {itype=} {nr=} {fd=} {ret=}", os.readlink(f"/proc/self/fd/{fd}") if fd >= 0 else "")
+1 -1
View File
@@ -145,7 +145,7 @@ OPNAME = {
ALUSRC = {
1: "SALU",
2: "VALU",
3: "VALU_SALU",
3: "VALU_ALT",
}
MEMSRC = {
-23
View File
@@ -1,23 +0,0 @@
import os, subprocess
from pathlib import Path
from tinygrad.helpers import temp
EXAMPLES_DIR = Path(__file__).parent
PROFILE_PATH = Path(temp("profile.pkl", append_user=True))
EXAMPLES = [
"test.test_custom_kernel.TestCustomKernel.test_empty",
"test.test_tiny.TestTiny.test_plus",
"test.test_tiny.TestTiny.test_gemm",
]
if __name__ == "__main__":
arch = subprocess.check_output(["python", "-c", "from tinygrad import Device; print(Device['AMD'].arch)"], text=True,
env={**os.environ, "DEBUG":"0"}).rstrip()
(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)
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More