forked from tinygrad/tinygrad
Compare commits
1
Commits
master
..
ansip_para
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
369b9d0c27 |
@@ -61,7 +61,6 @@ runs:
|
||||
echo "MAX_BUFFER_SIZE=300000000" >> "$GITHUB_ENV"
|
||||
if [[ "$RUNNER_OS" == "Linux" ]]; then
|
||||
echo "VIRTUAL_ENV=/opt/venv/${{ inputs.python-version }}" >> "$GITHUB_ENV"
|
||||
echo "UV_PYTHON_INSTALL_DIR=/opt/python" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "VIRTUAL_ENV=${{ github.workspace }}/.venv" >> "$GITHUB_ENV"
|
||||
fi
|
||||
@@ -71,6 +70,11 @@ runs:
|
||||
with:
|
||||
enable-cache: 'false' # see below for manual caching
|
||||
|
||||
- name: Set up Python ${{ inputs.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
|
||||
# **** Caching packages ****
|
||||
|
||||
- name: Cache Python packages (PR)
|
||||
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
python3 -c "from tinygrad.runtime.autogen import mesa"
|
||||
python3 -c "from tinygrad.runtime.autogen import avcodec"
|
||||
python3 -c "from tinygrad.runtime.autogen import llvm_qcom"
|
||||
python3 -c "from tinygrad.runtime.autogen import mlx5, bnxt"
|
||||
python3 -c "from tinygrad.runtime.autogen import mlx5"
|
||||
python3 -c "from tinygrad.runtime.autogen import ggml_common"
|
||||
REGEN=1 python3 -c "from tinygrad.runtime.autogen import libclang"
|
||||
- name: Check for differences
|
||||
|
||||
+107
-97
@@ -88,13 +88,13 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
dev: ['METAL', 'AMD', 'NV']
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 60
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
HCQ2: "0"
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -102,11 +102,12 @@ jobs:
|
||||
- name: Setup (AMD)
|
||||
if: ${{ matrix.dev == 'AMD' }}
|
||||
run: |
|
||||
./extra/hcq/hcq_smi.py amd rmmod --expect
|
||||
./extra/hcq/hcq_smi.py amd kill_pids --sudoless
|
||||
./extra/amdpci/setup_python_cap.sh
|
||||
./extra/hcq/hcq_smi.py amd rmmod
|
||||
./extra/hcq/hcq_smi.py amd kill_pids
|
||||
- name: Setup (NV)
|
||||
if: ${{ matrix.dev == 'NV' }}
|
||||
run: lsof -tQ /dev/nvidia* | { xargs -r kill -9 || true; }
|
||||
run: sudo lsof -tQ /dev/nvidia* | { xargs -r sudo kill -9 || true; }
|
||||
- name: setup staging db
|
||||
if: github.ref == 'refs/heads/update_benchmark_staging'
|
||||
run: |
|
||||
@@ -116,10 +117,10 @@ jobs:
|
||||
run: python3 test/external/process_replay/reset.py
|
||||
- name: Run llama3.2
|
||||
run: BENCHMARK_LOG=llama32_3b-f16 JITBEAM=2 IGNORE_BEAM_CACHE=1 python3 -m tinygrad.llm -m llama3.2:3b-f16 --benchmark --warmup
|
||||
- name: Run qwen3.8
|
||||
# qwen3.8:27b doesn't fit on mac
|
||||
- name: Run qwen3.6
|
||||
# qwen3.6:35b-a3b doesn't fit on mac
|
||||
if: ${{ matrix.dev != 'METAL' }}
|
||||
run: BENCHMARK_LOG=qwen38_27b JITBEAM=2 IGNORE_BEAM_CACHE=1 python3 -m tinygrad.llm -m qwen3.8:27b --benchmark --warmup
|
||||
run: BENCHMARK_LOG=qwen36_35b-a3b JITBEAM=2 IGNORE_BEAM_CACHE=1 python3 -m tinygrad.llm -m qwen3.6:35b-a3b --benchmark --warmup
|
||||
- name: Run olmoe
|
||||
# just metal for now
|
||||
if: ${{ matrix.dev == 'METAL' }}
|
||||
@@ -134,13 +135,13 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
dev: ['METAL', 'AMD', 'NV']
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 60
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
HCQ2: "0"
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -148,11 +149,12 @@ jobs:
|
||||
- name: Setup (AMD)
|
||||
if: ${{ matrix.dev == 'AMD' }}
|
||||
run: |
|
||||
./extra/hcq/hcq_smi.py amd rmmod --expect
|
||||
./extra/hcq/hcq_smi.py amd kill_pids --sudoless
|
||||
./extra/amdpci/setup_python_cap.sh
|
||||
./extra/hcq/hcq_smi.py amd rmmod
|
||||
./extra/hcq/hcq_smi.py amd kill_pids
|
||||
- name: Setup (NV)
|
||||
if: ${{ matrix.dev == 'NV' }}
|
||||
run: lsof -tQ /dev/nvidia* | { xargs -r kill -9 || true; }
|
||||
run: sudo lsof -tQ /dev/nvidia* | { xargs -r sudo kill -9 || true; }
|
||||
- name: setup staging db
|
||||
if: github.ref == 'refs/heads/update_benchmark_staging'
|
||||
run: |
|
||||
@@ -182,13 +184,13 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
dev: ['AMD', 'NV']
|
||||
timeout-minutes: 5
|
||||
timeout-minutes: 60
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
HCQ2: "0"
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -196,11 +198,12 @@ jobs:
|
||||
- name: Setup (AMD)
|
||||
if: ${{ matrix.dev == 'AMD' }}
|
||||
run: |
|
||||
./extra/hcq/hcq_smi.py amd rmmod --expect
|
||||
./extra/hcq/hcq_smi.py amd kill_pids --sudoless
|
||||
./extra/amdpci/setup_python_cap.sh
|
||||
./extra/hcq/hcq_smi.py amd rmmod
|
||||
./extra/hcq/hcq_smi.py amd kill_pids
|
||||
- name: Setup (NV)
|
||||
if: ${{ matrix.dev == 'NV' }}
|
||||
run: lsof -tQ /dev/nvidia* | { xargs -r kill -9 || true; }
|
||||
run: sudo lsof -tQ /dev/nvidia* | { xargs -r sudo kill -9 || true; }
|
||||
- name: Symlink models and datasets
|
||||
run: |
|
||||
mkdir -p extra/datasets
|
||||
@@ -224,13 +227,13 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
dev: ['METAL', 'AMD', 'NV']
|
||||
timeout-minutes: 15
|
||||
timeout-minutes: 60
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
HCQ2: "0"
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -238,11 +241,12 @@ jobs:
|
||||
- name: Setup (AMD)
|
||||
if: ${{ matrix.dev == 'AMD' }}
|
||||
run: |
|
||||
./extra/hcq/hcq_smi.py amd rmmod --expect
|
||||
./extra/hcq/hcq_smi.py amd kill_pids --sudoless
|
||||
./extra/amdpci/setup_python_cap.sh
|
||||
./extra/hcq/hcq_smi.py amd rmmod
|
||||
./extra/hcq/hcq_smi.py amd kill_pids
|
||||
- name: Setup (NV)
|
||||
if: ${{ matrix.dev == 'NV' }}
|
||||
run: lsof -tQ /dev/nvidia* | { xargs -r kill -9 || true; }
|
||||
run: sudo lsof -tQ /dev/nvidia* | { xargs -r sudo kill -9 || true; }
|
||||
- name: setup staging db
|
||||
if: github.ref == 'refs/heads/update_benchmark_staging'
|
||||
run: |
|
||||
@@ -269,13 +273,13 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
dev: ['AMD', 'NV']
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 60
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
HCQ2: "0"
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -283,11 +287,12 @@ jobs:
|
||||
- name: Setup (AMD)
|
||||
if: ${{ matrix.dev == 'AMD' }}
|
||||
run: |
|
||||
./extra/hcq/hcq_smi.py amd rmmod --expect
|
||||
./extra/hcq/hcq_smi.py amd kill_pids --sudoless
|
||||
./extra/amdpci/setup_python_cap.sh
|
||||
./extra/hcq/hcq_smi.py amd rmmod
|
||||
./extra/hcq/hcq_smi.py amd kill_pids
|
||||
- name: Setup (NV)
|
||||
if: ${{ matrix.dev == 'NV' }}
|
||||
run: lsof -tQ /dev/nvidia* | { xargs -r kill -9 || true; }
|
||||
run: sudo lsof -tQ /dev/nvidia* | { xargs -r sudo kill -9 || true; }
|
||||
- name: Symlink models and datasets
|
||||
run: |
|
||||
mkdir -p weights
|
||||
@@ -322,7 +327,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
dev: ['METAL', 'AMD', 'NV']
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 60
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
@@ -335,11 +340,12 @@ jobs:
|
||||
- name: Setup (AMD)
|
||||
if: ${{ matrix.dev == 'AMD' }}
|
||||
run: |
|
||||
./extra/hcq/hcq_smi.py amd rmmod --expect
|
||||
./extra/hcq/hcq_smi.py amd kill_pids --sudoless
|
||||
./extra/amdpci/setup_python_cap.sh
|
||||
./extra/hcq/hcq_smi.py amd rmmod
|
||||
./extra/hcq/hcq_smi.py amd kill_pids
|
||||
- name: Setup (NV)
|
||||
if: ${{ matrix.dev == 'NV' }}
|
||||
run: lsof -tQ /dev/nvidia* | { xargs -r kill -9 || true; }
|
||||
run: sudo lsof -tQ /dev/nvidia* | { xargs -r sudo kill -9 || true; }
|
||||
- name: setup staging db
|
||||
if: github.ref == 'refs/heads/update_benchmark_staging'
|
||||
run: |
|
||||
@@ -416,7 +422,7 @@ jobs:
|
||||
testusbgpu:
|
||||
name: UsbGPU Benchmark
|
||||
runs-on: [self-hosted, macOS]
|
||||
timeout-minutes: 3
|
||||
timeout-minutes: 10
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
@@ -431,70 +437,32 @@ jobs:
|
||||
rm -f /tmp/staging.db /tmp/staging.db-shm /tmp/staging.db-wal
|
||||
- name: Kill stale pids
|
||||
run: |
|
||||
./extra/hcq/hcq_smi.py amd kill_pids --sudoless
|
||||
./extra/hcq/hcq_smi.py nv kill_pids --sudoless
|
||||
PYTHONPATH=. ./extra/hcq/hcq_smi.py amd kill_pids
|
||||
PYTHONPATH=. ./extra/hcq/hcq_smi.py nv kill_pids
|
||||
# since sudo is required for usbgpu on macos, do not write bytecode, as some of the files are owned by root
|
||||
- name: UsbGPU boot time
|
||||
run: GMMU=0 DEBUG=2 AM_RESET=1 DEV=USB+AMD time python3.11 test/test_tiny.py TestTiny.test_plus
|
||||
run: sudo -E PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=. GMMU=0 DEBUG=2 AM_RESET=1 DEV=USB+AMD time python3.11 test/test_tiny.py TestTiny.test_plus
|
||||
- name: UsbGPU tiny tests
|
||||
run: GMMU=0 DEV=USB+AMD python3.11 test/test_tiny.py
|
||||
run: sudo -E PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=. GMMU=0 DEV=USB+AMD python3.11 test/test_tiny.py
|
||||
- name: UsbGPU copy speeds
|
||||
run: SIZE=64000000 PYTHONPATH=. GMMU=0 DEV=USB+AMD python3.11 test/external/external_test_usb_asm24.py TestDevCopySpeeds
|
||||
run: sudo -E PYTHONDONTWRITEBYTECODE=1 SIZE=64000000 PYTHONPATH=. GMMU=0 DEV=USB+AMD python3.11 test/external/external_test_usb_asm24.py TestDevCopySpeeds
|
||||
#- name: UsbGPU openpilot test
|
||||
# run: sudo -E PYTHONPATH=. GMMU=0 DEV=USB+AMD GRAPH_ONE_KERNEL=1 python3.11 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/9118973ed03c1ae1d40cf69a29507ec2cc78efd7/selfdrive/modeld/models/supercombo.onnx
|
||||
- name: UsbGPU (USB4/TB) install script
|
||||
run: sh extra/setup_tinygpu_osx.sh
|
||||
run: PYTHONPATH=. sh extra/setup_tinygpu_osx.sh
|
||||
- name: UsbGPU (USB4/TB) boot time
|
||||
run: DEBUG=3 DEV=PCI+NV:NAK time python3.11 test/test_tiny.py TestTiny.test_plus
|
||||
run: PYTHONPATH=. DEBUG=3 DEV=PCI+NV:NAK time python3.11 test/test_tiny.py TestTiny.test_plus
|
||||
- name: UsbGPU (USB4/TB) tiny tests
|
||||
run: DEV=PCI+NV:NAK python3.11 test/test_tiny.py
|
||||
run: PYTHONPATH=. DEV=PCI+NV:NAK python3.11 test/test_tiny.py
|
||||
|
||||
testcomma:
|
||||
strategy:
|
||||
matrix:
|
||||
dev: ['QCOM', 'QCOM:IR3']
|
||||
version: ['0.11.0', '0.11.2']
|
||||
model: ['vision', 'policy', 'supercombo', 'dmonitoring']
|
||||
# exclude non-existent models
|
||||
exclude: [{ version: '0.11.0', model: supercombo }, { version: '0.11.2', model: vision }, { version: '0.11.2', model: policy }]
|
||||
include:
|
||||
- version: '0.11.0'
|
||||
model: vision
|
||||
url: https://github.com/commaai/openpilot/raw/v0.11.0/selfdrive/modeld/models/driving_vision.onnx
|
||||
timing: 17
|
||||
- version: '0.11.0'
|
||||
model: policy
|
||||
url: https://github.com/commaai/openpilot/raw/v0.11.0/selfdrive/modeld/models/driving_policy.onnx
|
||||
timing: 3.2
|
||||
- version: '0.11.0'
|
||||
model: dmonitoring
|
||||
url: https://github.com/commaai/openpilot/raw/v0.11.0/selfdrive/modeld/models/dmonitoring_model.onnx
|
||||
timing: 11
|
||||
- version: '0.11.2'
|
||||
model: supercombo
|
||||
url: https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/433f85f956837606ad1f1cbee4aa7e2158ad23c768dea914b20436c97232741b
|
||||
timing: 26
|
||||
- dev: QCOM:IR3
|
||||
version: '0.11.2'
|
||||
model: supercombo
|
||||
timing: 41
|
||||
- version: '0.11.2'
|
||||
model: dmonitoring
|
||||
url: https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/3e7b31dfbc0a5234f1baf196513b77fc6af12204b8a8ffe8ee0417e48352f316
|
||||
timing: 11
|
||||
# IR3 dmonitoring is slightly slower
|
||||
- dev: QCOM:IR3
|
||||
model: dmonitoring
|
||||
timing: 12
|
||||
fail-fast: false
|
||||
name: openpilot ${{ matrix.version }} compile3 ${{ matrix.model }} (DEV=${{ matrix.dev }})
|
||||
testcommalatest:
|
||||
name: comma Benchmark (0.11.2)
|
||||
runs-on: [self-hosted, Linux, comma]
|
||||
timeout-minutes: 5
|
||||
timeout-minutes: 12
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
ASSERT_MIN_STEP_TIME: ${{ matrix.timing }}
|
||||
BENCHMARK_LOG: ${{ matrix.dev == 'QCOM:IR3' && 'ir3_' || '' }}openpilot_${{ matrix.version }}_${{ matrix.model }}
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
@@ -505,10 +473,45 @@ jobs:
|
||||
rm -f /tmp/staging.db /tmp/staging.db-shm /tmp/staging.db-wal
|
||||
- name: reset process replay
|
||||
run: test/external/process_replay/reset.py
|
||||
- name: compile
|
||||
run: FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py ${{ matrix.url }}
|
||||
- name: run pickle
|
||||
run: BENCHMARK_LOG="${BENCHMARK_LOG}_run_pickle" RUN_PICKLE=1 taskset -c 4-7 python3 examples/openpilot/compile3.py
|
||||
- name: openpilot compile3 0.11.2 supercombo
|
||||
run: BENCHMARK_LOG=openpilot_0_11_2_supercombo PYTHONPATH="." ASSERT_MIN_STEP_TIME=26 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/433f85f956837606ad1f1cbee4aa7e2158ad23c768dea914b20436c97232741b
|
||||
- name: openpilot compile3 0.11.2 supercombo (from pickle)
|
||||
run: BENCHMARK_LOG=openpilot_0_11_2_supercombo_run_pickle RUN_PICKLE=1 PYTHONPATH="." ASSERT_MIN_STEP_TIME=26 DEV=QCOM taskset -c 4-7 python3 examples/openpilot/compile3.py
|
||||
- name: IR3 openpilot compile3 0.11.2 supercombo
|
||||
run: BENCHMARK_LOG=ir3_openpilot_0_11_2_supercombo PYTHONPATH="." ASSERT_MIN_STEP_TIME=41 DEV=QCOM:IR3 FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/433f85f956837606ad1f1cbee4aa7e2158ad23c768dea914b20436c97232741b
|
||||
- name: openpilot compile3 0.11.2 dmonitoring
|
||||
run: BENCHMARK_LOG=openpilot_0_11_2_dmonitoring PYTHONPATH="." ASSERT_MIN_STEP_TIME=11 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/3e7b31dfbc0a5234f1baf196513b77fc6af12204b8a8ffe8ee0417e48352f316
|
||||
- name: Run process replay tests
|
||||
uses: ./.github/actions/process-replay
|
||||
|
||||
testcommaold:
|
||||
name: comma Benchmark (0.11.0)
|
||||
runs-on: [self-hosted, Linux, comma]
|
||||
timeout-minutes: 12
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
- name: setup staging db
|
||||
if: github.ref == 'refs/heads/update_benchmark_staging'
|
||||
run: |
|
||||
echo "CACHEDB=/tmp/staging.db" >> $GITHUB_ENV
|
||||
rm -f /tmp/staging.db /tmp/staging.db-shm /tmp/staging.db-wal
|
||||
- name: reset process replay
|
||||
run: test/external/process_replay/reset.py
|
||||
- name: openpilot compile3 0.11.0 driving_vision
|
||||
run: BENCHMARK_LOG=openpilot_0_11_0_vision PYTHONPATH="." ASSERT_MIN_STEP_TIME=17 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/v0.11.0/selfdrive/modeld/models/driving_vision.onnx
|
||||
- name: openpilot compile3 0.11.0 driving_vision (from pickle)
|
||||
run: BENCHMARK_LOG=openpilot_0_11_0_vision_run_pickle RUN_PICKLE=1 PYTHONPATH="." ASSERT_MIN_STEP_TIME=17 DEV=QCOM taskset -c 4-7 python3 examples/openpilot/compile3.py
|
||||
- name: IR3 openpilot compile3 0.11.0 driving_vision
|
||||
run: BENCHMARK_LOG=ir3_openpilot_0_11_0_vision PYTHONPATH="." ASSERT_MIN_STEP_TIME=18 DEV=QCOM:IR3 FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/v0.11.0/selfdrive/modeld/models/driving_vision.onnx
|
||||
- name: openpilot compile3 0.11.0 driving_policy
|
||||
run: BENCHMARK_LOG=openpilot_0_11_0_policy PYTHONPATH="." ASSERT_MIN_STEP_TIME=3.2 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/v0.11.0/selfdrive/modeld/models/driving_policy.onnx
|
||||
- name: openpilot compile3 0.11.0 dmonitoring
|
||||
run: BENCHMARK_LOG=openpilot_0_11_0_dmonitoring PYTHONPATH="." ASSERT_MIN_STEP_TIME=11 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/v0.11.0/selfdrive/modeld/models/dmonitoring_model.onnx
|
||||
- name: Run process replay tests
|
||||
uses: ./.github/actions/process-replay
|
||||
|
||||
@@ -521,6 +524,15 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
- name: setup staging db
|
||||
if: github.ref == 'refs/heads/update_benchmark_staging'
|
||||
run: |
|
||||
echo "CACHEDB=/tmp/staging.db" >> $GITHUB_ENV
|
||||
rm -f /tmp/staging.db /tmp/staging.db-shm /tmp/staging.db-wal
|
||||
- name: reset process replay
|
||||
run: test/external/process_replay/reset.py
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
- name: setup staging db
|
||||
@@ -544,7 +556,7 @@ jobs:
|
||||
testcommausbgpubenchmark:
|
||||
name: UsbGPU Benchmark (comma)
|
||||
runs-on: [self-hosted, Linux, comma4]
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 20
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
@@ -573,7 +585,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
dev: ['AMD', 'NV']
|
||||
timeout-minutes: 5
|
||||
timeout-minutes: 20
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
@@ -585,8 +597,9 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup
|
||||
run: |
|
||||
./extra/hcq/hcq_smi.py ${{ matrix.dev }} rmmod --expect
|
||||
./extra/hcq/hcq_smi.py ${{ matrix.dev }} kill_pids --sudoless
|
||||
./extra/amdpci/setup_python_cap.sh
|
||||
./extra/hcq/hcq_smi.py ${{ matrix.dev }} rmmod
|
||||
./extra/hcq/hcq_smi.py ${{ matrix.dev }} kill_pids
|
||||
mkdir -p extra/datasets
|
||||
ln -s /raid/datasets/imagenet extra/datasets/imagenet
|
||||
- name: setup staging db
|
||||
@@ -621,9 +634,6 @@ jobs:
|
||||
run: |
|
||||
GRAPH_ONE_KERNEL=1 NSZ=8192 python3 test/speed/external_test_copy_speed.py TestCopySpeed.testCopyDefaulttoCPUJit
|
||||
GRAPH_ONE_KERNEL=1 NSZ=8192 python3 test/speed/external_test_copy_speed.py TestCopySpeed.testCopyCPUtoDefaultJit
|
||||
- name: HEVC Decode Benchmark
|
||||
if: ${{ matrix.dev == 'NV' }}
|
||||
run: VALIDATE=1 MAX_FRAMES=100 ASSERT_FPS=1400 JITBEAM=1 PYTHONPATH=. python3 extra/hevc/decode.py
|
||||
- name: Run 10 MLPerf ResNet50 training steps (1 gpu)
|
||||
if: ${{ matrix.dev == 'NV' }}
|
||||
run: BENCHMARK_LOG=resnet_10steps MNISTMOCK=1 DEFAULT_FLOAT=HALF BENCHMARK=10 BS=256 GPUS=1 MODEL=resnet python3 examples/mlperf/model_train.py
|
||||
@@ -646,7 +656,7 @@ jobs:
|
||||
llvmspeed:
|
||||
name: LLVM Speed
|
||||
runs-on: [self-hosted, Linux, tinyboxrandom]
|
||||
timeout-minutes: 5
|
||||
timeout-minutes: 20
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
|
||||
@@ -233,7 +233,7 @@ jobs:
|
||||
- name: Run process replay tests
|
||||
uses: ./.github/actions/process-replay
|
||||
- name: Repo line count <= 26000 lines
|
||||
run: MAX_LINE_COUNT=26500 python sz.py
|
||||
run: MAX_LINE_COUNT=26000 python sz.py
|
||||
|
||||
spec:
|
||||
strategy:
|
||||
@@ -504,7 +504,7 @@ jobs:
|
||||
- name: Run AMD renderer tests (AMD:LLVM)
|
||||
run: DEV=MOCKKFD+AMD:LLVM python -m pytest -n=auto test/amd/ --durations 20
|
||||
- name: Run SQTT profiling tests
|
||||
run: VIZ=-2 python3 -m pytest -n=auto test/amd/test_sqtt_profiler.py
|
||||
run: PROFILE=1 SQTT=1 python3 -m pytest -n=auto test/amd/test_sqtt_profiler.py
|
||||
- name: Run AMD emulated tests on NULL backend
|
||||
env:
|
||||
AMD: 0
|
||||
@@ -679,5 +679,4 @@ jobs:
|
||||
run: |
|
||||
python -c "from tinygrad import Device; assert Device.DEFAULT == 'NULL'"
|
||||
DEBUG=4 python3 test/backend/test_ops.py TestOps.test_add
|
||||
# QCOMCL compiles in qemu, too slow for parallel workers
|
||||
${{ contains(matrix.dev, 'QCOMCL') && 'PARALLEL=0' || '' }} python -m pytest -n=auto test/backend/test_ops.py --durations=20
|
||||
python -m pytest -n=auto test/backend/test_ops.py --durations=20
|
||||
|
||||
@@ -69,4 +69,3 @@ mutants
|
||||
dagre/
|
||||
graphlib/
|
||||
uv.lock
|
||||
pi_session_window0.jsonl
|
||||
|
||||
@@ -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/backend/test_ops.py test/backend/test_schedule.py test/backend/test_assign.py test/backend/test_tensor.py test/backend/test_jit.py test/unit/test_schedule_cache.py test/null/test_pattern_matcher.py test/null/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/backend/test_ops.py test/backend/test_schedule.py test/unit/test_assign.py test/backend/test_tensor.py test/backend/test_jit.py test/unit/test_schedule_cache.py test/null/test_pattern_matcher.py test/null/test_uop_symbolic.py test/unit/test_helpers.py
|
||||
language: system
|
||||
always_run: true
|
||||
pass_filenames: false
|
||||
|
||||
@@ -4,4 +4,3 @@
|
||||
- Run `python -m mypy tinygrad/` to typecheck
|
||||
- Run `python -m ruff check .` to lint
|
||||
- Read `./tinygrad/viz/README.md` for profiling and debugging rewrite rules
|
||||
- Do not do amend commits. Always do a new commit if a force push to origin would be required.
|
||||
|
||||
@@ -1674,7 +1674,8 @@ def train_gptoss():
|
||||
config = {}
|
||||
BASEDIR = config["BASEDIR"] = Path(getenv("BASEDIR", "/raid/datasets/c4-8b/"))
|
||||
BS = config["BS"] = getenv("BS", 16)
|
||||
GBS = config["GLOBAL_BATCH_SIZE"] = BS
|
||||
grad_acc = config["GRADIENT_ACC_STEPS"] = getenv("GRADIENT_ACC_STEPS", 1)
|
||||
GBS = config["GLOBAL_BATCH_SIZE"] = BS * grad_acc
|
||||
SEED = config["SEED"] = getenv("SEED", 5760)
|
||||
DATA_SEED = config["DATA_SEED"] = getenv("DATA_SEED", SEED)
|
||||
SEQLEN = config["SEQLEN"] = getenv("SEQLEN", 8192)
|
||||
@@ -1736,8 +1737,8 @@ def train_gptoss():
|
||||
params_wd = [p for p in params if p.ndim >= 3]
|
||||
params_no_wd = [p for p in params if p.ndim < 3]
|
||||
optim = GradAccClipAdamWGroup(
|
||||
GradAccClipAdamW(params_wd, lr=0.0, b1=opt_adamw_beta_1, b2=opt_adamw_beta_2, eps=opt_adamw_epsilon, weight_decay=opt_adamw_weight_decay, grad_acc=1, device=optim_device),
|
||||
GradAccClipAdamW(params_no_wd, lr=0.0, b1=opt_adamw_beta_1, b2=opt_adamw_beta_2, eps=opt_adamw_epsilon, weight_decay=0.0, grad_acc=1, device=optim_device),
|
||||
GradAccClipAdamW(params_wd, lr=0.0, b1=opt_adamw_beta_1, b2=opt_adamw_beta_2, eps=opt_adamw_epsilon, weight_decay=opt_adamw_weight_decay, grad_acc=grad_acc, device=optim_device),
|
||||
GradAccClipAdamW(params_no_wd, lr=0.0, b1=opt_adamw_beta_1, b2=opt_adamw_beta_2, eps=opt_adamw_epsilon, weight_decay=0.0, grad_acc=grad_acc, device=optim_device),
|
||||
)
|
||||
|
||||
for p in optim.params:
|
||||
@@ -1769,34 +1770,31 @@ def train_gptoss():
|
||||
|
||||
@TinyJit
|
||||
@Context(TRAINING=1)
|
||||
def step(tokens:Tensor):
|
||||
def minibatch(tokens:Tensor):
|
||||
if is_dp: tokens = tokens.to(None).shard(device, 0)
|
||||
if not is_sharding: tokens = tokens.to(None)
|
||||
|
||||
logits:Tensor = model(tokens[:, :-1], save=True)
|
||||
if getenv("FUSED_CE", 0):
|
||||
from extra.llama_kernels.fused_ce import fused_ce_loss
|
||||
loss = fused_ce_loss(logits.cast(dtypes.bfloat16), tokens[:, 1:], label_smoothing=0.0)
|
||||
else:
|
||||
loss = logits.sparse_categorical_crossentropy(tokens[:, 1:])
|
||||
loss = logits.sparse_categorical_crossentropy(tokens[:, 1:])
|
||||
|
||||
for g, new_g in zip(grads, loss.gradient(*optim.params)):
|
||||
apply_grad(g, new_g.uop)
|
||||
|
||||
Tensor.realize(loss, *grads)
|
||||
loss_cpu = loss.flatten().float().to("CPU")
|
||||
return loss_cpu.realize(*grads)
|
||||
|
||||
grad_norm = clip_grads(grads, 1, 1.0)
|
||||
@TinyJit
|
||||
def optim_step():
|
||||
grad_norm = clip_grads(grads, grad_acc, 1.0)
|
||||
optim.fstep(grads, grad_norm)
|
||||
scheduler.step()
|
||||
|
||||
for g in grads: g.assign(0)
|
||||
|
||||
loss_cpu = loss.flatten().float().to("CPU")
|
||||
lr_cpu = optim.lr.float().to("CPU")
|
||||
grad_norm_cpu = grad_norm.float().to("CPU")
|
||||
Tensor.realize(loss_cpu, lr_cpu, grad_norm_cpu, *grads, *fp8_inv_scales)
|
||||
Tensor.realize(lr_cpu, grad_norm_cpu, *grads, *fp8_inv_scales)
|
||||
|
||||
return loss_cpu, lr_cpu, grad_norm_cpu
|
||||
return lr_cpu, grad_norm_cpu
|
||||
|
||||
@TinyJit
|
||||
@Context(TRAINING=0)
|
||||
@@ -1845,20 +1843,30 @@ def train_gptoss():
|
||||
profile_marker(f"train @ {i}")
|
||||
st = time.perf_counter()
|
||||
|
||||
ist = time.perf_counter()
|
||||
stopped = False
|
||||
losses, data_time, dev_time = [], 0, 0
|
||||
for _ in range(grad_acc if i >= 2 else 1):
|
||||
ist = time.perf_counter()
|
||||
try: tokens = next(train_iter)
|
||||
except StopIteration:
|
||||
stopped = True
|
||||
break
|
||||
mst = time.perf_counter()
|
||||
data_time += mst - ist
|
||||
losses.append(minibatch(tokens).item())
|
||||
dev_time += time.perf_counter() - mst
|
||||
if stopped: break
|
||||
|
||||
try: tokens = next(train_iter)
|
||||
except StopIteration: break
|
||||
mst = time.perf_counter()
|
||||
data_time = mst - ist
|
||||
|
||||
ret = step(tokens)
|
||||
dev_time = time.perf_counter() - mst
|
||||
|
||||
loss, lr, grad_norm = ret[0].item(), ret[1].item(), ret[2].item()
|
||||
gt = time.perf_counter()
|
||||
ret = optim_step()
|
||||
lr, grad_norm = ret[0].item(), ret[1].item()
|
||||
et = time.perf_counter()
|
||||
|
||||
loss = sum(losses) / len(losses)
|
||||
optim_time = et - gt
|
||||
dev_time += optim_time
|
||||
step_time = et - st
|
||||
gbs_time = gt - st
|
||||
if BENCHMARK: step_times.append(step_time)
|
||||
|
||||
i += 1
|
||||
@@ -1868,7 +1876,7 @@ def train_gptoss():
|
||||
gflops = GlobalCounters.global_ops / 1e9 / dev_time
|
||||
mfu = ((6 * num_params * SEQLEN * GBS) / (dev_time * device_count * 4.6e15)) * 100
|
||||
tqdm.write(
|
||||
f"{i:5} {step_time:.3f} s step, {dev_time:.3f} s dev, {data_time:.3f} s data, {loss:.4f} loss, " \
|
||||
f"{i:5} {step_time:.3f} s step, {gbs_time:.3f} s gbs, {optim_time:.3f} s optim, {data_time:.3f} s data, {loss:.4f} loss, " \
|
||||
f"{lr:.12f} LR, {grad_norm:.6f} grad_norm, {mem_gb:.2f} GB used, {gflops:9.2f} GFLOPS, {mfu:5.2f}% MFU")
|
||||
if DEBUG >= 1: tqdm.write(" mem per device: " + ', '.join(f"{dev}: {mem/1e9:.2f} GB" for dev, mem in sorted(GlobalCounters.mem_used_per_device.items())))
|
||||
|
||||
@@ -1878,6 +1886,8 @@ def train_gptoss():
|
||||
"train/lr": lr,
|
||||
"train/grad_norm": grad_norm,
|
||||
"train/step_time": step_time,
|
||||
"train/gbs_time": gbs_time,
|
||||
"train/optim_time": optim_time,
|
||||
"train/dev_time": dev_time,
|
||||
"train/data_time": data_time,
|
||||
"train/mem": mem_gb,
|
||||
|
||||
@@ -15,7 +15,7 @@ def stochastic_round_bf16(x:Tensor) -> Tensor:
|
||||
bits = x.bitcast(dtypes.uint32)
|
||||
if isinstance(x.device, tuple):
|
||||
shape = x.uop.shard_shape if x.uop.axis is not None else x.shape
|
||||
noise = Tensor(UOp(Ops.MSTACK, src=tuple(Tensor.rand(*shape, device=d).uop for d in x.device)))
|
||||
noise = Tensor(UOp(Ops.MSTACK, dtypes.default_float, tuple(Tensor.rand(*shape, device=d).uop for d in x.device)))
|
||||
else:
|
||||
noise = x.rand_like()
|
||||
noise = (noise * 0xFFFF).cast(dtypes.uint32)
|
||||
|
||||
+3
-3
@@ -35,7 +35,7 @@ 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_SAMPLES=2048 MAX_STEPS=1200000
|
||||
export LR="1e-3" END_LR="1e-4" WARMUP_SAMPLES=4096 MAX_STEPS=1200000
|
||||
export WARMUP_STEPS=$((WARMUP_SAMPLES / GBS))
|
||||
export SAMPLES=$((MAX_STEPS * GBS))
|
||||
export SEQLEN=${SEQLEN:-8192}
|
||||
@@ -44,9 +44,9 @@ export SEED=${SEED:-5760}
|
||||
export DATA_SEED=${DATA_SEED:-5760}
|
||||
|
||||
export JITBEAM=${JITBEAM:-3}
|
||||
export BEAM_UOPS_MAX=6000 BEAM_UPCAST_MAX=256 BEAM_LOCAL_MAX=1024 BEAM_MIN_PROGRESS=5 BEAM_PADTO=0
|
||||
export BEAM_UOPS_MAX=6000 BEAM_UPCAST_MAX=256 BEAM_LOCAL_MAX=1024 BEAM_MIN_PROGRESS=5 BEAM_PADTO=1
|
||||
|
||||
export FAKEDATA=${FAKEDATA:-1} BENCHMARK=${BENCHMARK:-10}
|
||||
export FAKEDATA=${FAKEDATA:-$([[ "$DEV" == NULL:* ]] && echo 1 || echo 0)} BENCHMARK=${BENCHMARK:-10}
|
||||
if [ -z "$FULL_LAYERS" ]; then
|
||||
export LLAMA_LAYERS=${LLAMA_LAYERS:-2}
|
||||
fi
|
||||
|
||||
+2
-2
@@ -35,7 +35,7 @@ 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_SAMPLES=2048 MAX_STEPS=1200000
|
||||
export LR="1e-3" END_LR="1e-4" WARMUP_SAMPLES=4096 MAX_STEPS=1200000
|
||||
export WARMUP_STEPS=$((WARMUP_SAMPLES / GBS))
|
||||
export SAMPLES=$((MAX_STEPS * GBS))
|
||||
export SEQLEN=${SEQLEN:-8192}
|
||||
@@ -44,6 +44,6 @@ export SEED=${SEED:-$RANDOM}
|
||||
export DATA_SEED=${DATA_SEED:-5760}
|
||||
|
||||
export JITBEAM=${JITBEAM:-3}
|
||||
export BEAM_UOPS_MAX=6000 BEAM_UPCAST_MAX=256 BEAM_LOCAL_MAX=1024 BEAM_MIN_PROGRESS=5 BEAM_PADTO=0
|
||||
export BEAM_UOPS_MAX=6000 BEAM_UPCAST_MAX=256 BEAM_LOCAL_MAX=1024 BEAM_MIN_PROGRESS=5 BEAM_PADTO=1
|
||||
|
||||
python3 examples/mlperf/model_train.py
|
||||
|
||||
+3
-3
@@ -17,7 +17,7 @@ export USE_ATOMICS=1
|
||||
export ASM_GEMM=1
|
||||
export WQKV=1
|
||||
export MASTER_WEIGHTS=1
|
||||
export MXFP4=1
|
||||
export FP8=1
|
||||
export ALLREDUCE_CAST=1
|
||||
export FAST_CE=1
|
||||
export FUSED_INPUT_QUANTIZE=1
|
||||
@@ -26,7 +26,7 @@ export FUSED_ADD_NORM_MUL_QUANTIZE=1
|
||||
export FUSED_SILU_W13=1
|
||||
export SPLIT_W13=0
|
||||
|
||||
export DEFAULT_FLOAT="bfloat16" OPTIM_DTYPE="float32"
|
||||
export DEFAULT_FLOAT="bfloat16" OPTIM_DTYPE="bfloat16"
|
||||
export DP=8 MP=1 BS=16 EVAL_BS=8 GRADIENT_ACC_STEPS=2
|
||||
export GBS=$((BS * GRADIENT_ACC_STEPS))
|
||||
|
||||
@@ -44,7 +44,7 @@ export SEED=$RANDOM
|
||||
export DATA_SEED=$SEED
|
||||
|
||||
export JITBEAM=3
|
||||
export BEAM_UOPS_MAX=6000 BEAM_UPCAST_MAX=256 BEAM_LOCAL_MAX=1024 BEAM_MIN_PROGRESS=5 BEAM_PADTO=0
|
||||
export BEAM_UOPS_MAX=6000 BEAM_UPCAST_MAX=256 BEAM_LOCAL_MAX=1024 BEAM_MIN_PROGRESS=5 BEAM_PADTO=1
|
||||
|
||||
export LOGMLPERF=1
|
||||
|
||||
|
||||
@@ -107,21 +107,14 @@ def compile(onnx_file):
|
||||
return inputs, test_val
|
||||
|
||||
def test_vs_compile(run, inputs, test_val=None):
|
||||
if (log:=bool(getenv("BENCHMARK_LOG", ""))): from extra.bench_log import WallTimeEvent, BenchEvent
|
||||
|
||||
# run 20 times
|
||||
step_times = []
|
||||
for _ in range(20):
|
||||
st = time.perf_counter()
|
||||
if log:
|
||||
with WallTimeEvent(BenchEvent.STEP):
|
||||
out = run(**inputs)
|
||||
mt = time.perf_counter()
|
||||
val = out.numpy()
|
||||
else:
|
||||
out = run(**inputs)
|
||||
mt = time.perf_counter()
|
||||
val = out.numpy()
|
||||
out = run(**inputs)
|
||||
mt = time.perf_counter()
|
||||
val = out.numpy()
|
||||
et = time.perf_counter()
|
||||
step_times.append((et-st)*1e3)
|
||||
print(f"enqueue {(mt-st)*1e3:6.2f} ms -- total run {step_times[-1]:6.2f} ms")
|
||||
@@ -167,6 +160,12 @@ def test_vs_onnx(new_inputs, test_val, onnx_file, tol):
|
||||
print("test vs onnx passed")
|
||||
return timings
|
||||
|
||||
def bench(run, inputs):
|
||||
from extra.bench_log import WallTimeEvent, BenchEvent
|
||||
for _ in range(10):
|
||||
with WallTimeEvent(BenchEvent.STEP):
|
||||
run(**inputs).numpy()
|
||||
|
||||
if __name__ == "__main__":
|
||||
if getenv("RUN_PICKLE"):
|
||||
with open(OUTPUT, "rb") as f: pickle_loaded = load_pickle(f)
|
||||
@@ -182,3 +181,6 @@ if __name__ == "__main__":
|
||||
test_vs_compile(pickle_loaded, inputs, outputs)
|
||||
if getenv("SELFTEST"):
|
||||
test_vs_onnx(inputs, outputs, onnx_file, 1e-4)
|
||||
|
||||
if getenv("BENCHMARK_LOG", ""):
|
||||
bench(pickle_loaded, inputs)
|
||||
|
||||
@@ -84,8 +84,7 @@ class AMSMI(AMDev):
|
||||
with open(f"/sys/bus/pci/devices/{self.pcibus}/power_state", "r") as f: return f.read().strip().rstrip()
|
||||
|
||||
class SMICtx:
|
||||
def __init__(self, dev_filter=None):
|
||||
self.dev_filter = dev_filter
|
||||
def __init__(self):
|
||||
self.devs = []
|
||||
self.opened_pcidevs = []
|
||||
self.opened_pci_resources = {}
|
||||
@@ -136,7 +135,6 @@ class SMICtx:
|
||||
pattern = os.path.join('/tmp', 'am_*.lock')
|
||||
for d in [f[8:-5] for f in glob.glob(pattern)]:
|
||||
if d.startswith("usb"): continue
|
||||
if self.dev_filter is not None and d != self.dev_filter: continue
|
||||
if d not in self.opened_pcidevs:
|
||||
self._open_am_device(d)
|
||||
|
||||
@@ -408,7 +406,7 @@ if __name__ == "__main__":
|
||||
|
||||
try:
|
||||
if not args.list: os.system('clear')
|
||||
smi_ctx = SMICtx(args.dev)
|
||||
smi_ctx = SMICtx()
|
||||
while True:
|
||||
smi_ctx.rescan_devs()
|
||||
smi_ctx.draw(args.list)
|
||||
|
||||
+9
-9
@@ -35,7 +35,7 @@ class WallTimeEvent:
|
||||
return self
|
||||
def __exit__(self, *_):
|
||||
self.time = time.monotonic() - self.start
|
||||
_events[self.event]["wall"].append((self.time, BENCHMARK_LOG.value))
|
||||
_events[self.event]["wall"].append(self.time)
|
||||
return False
|
||||
|
||||
class KernelTimeEvent:
|
||||
@@ -47,19 +47,19 @@ class KernelTimeEvent:
|
||||
self.start = GlobalCounters.time_sum_s
|
||||
return self
|
||||
def __exit__(self, *_):
|
||||
_events[self.event]["kernel"].append((GlobalCounters.time_sum_s - self.start, BENCHMARK_LOG.value))
|
||||
_events[self.event]["kernel"].append(GlobalCounters.time_sum_s - self.start)
|
||||
return False
|
||||
|
||||
def log_event_instant(event:InstantBenchEvent, value:float):
|
||||
_events[event].append((value, BENCHMARK_LOG.value))
|
||||
_events[event].append(value)
|
||||
|
||||
if BENCHMARK_LOG:
|
||||
INFLUXDB_HOST = getenv("INFLUXDB_HOST", "")
|
||||
INFLUXDB_ORG = getenv("INFLUXDB_ORG", "tiny")
|
||||
INFLUXDB_TOKEN = getenv("INFLUXDB_TOKEN", "")
|
||||
|
||||
def _create_point(run_id, i, attempt, ref, commit, name, value, log_name, run):
|
||||
point = Point(log_name.replace(':', '_').replace('.', '_')).tag("id", run_id).tag("index", i)
|
||||
def _create_point(run_id, i, attempt, ref, commit, name, value, run):
|
||||
point = Point(BENCHMARK_LOG.value).tag("id", run_id).tag("index", i)
|
||||
point = point.tag("device", Device.DEFAULT)
|
||||
point = point.tag("attempt", attempt).tag("ref", ref).tag("commit", commit)
|
||||
point = point.field(name, value).field("x", run)
|
||||
@@ -91,12 +91,12 @@ if BENCHMARK_LOG:
|
||||
run_id = str(uuid.uuid4())
|
||||
if isinstance(event, BenchEvent):
|
||||
for event_type, values in _events[event].items():
|
||||
for i, (value, log_name) in enumerate(values):
|
||||
point = _create_point(run_id, i, attempt, ref, commit, f"{event.value}_{event_type}", value, log_name, run)
|
||||
for i, value in enumerate(values):
|
||||
point = _create_point(run_id, i, attempt, ref, commit, f"{event.value}_{event_type}", value, run)
|
||||
points.append(point)
|
||||
else:
|
||||
for i, (value, log_name) in enumerate(_events[event]):
|
||||
point = _create_point(run_id, i, attempt, ref, commit, event.value, value, log_name, run)
|
||||
for i, value in enumerate(_events[event]):
|
||||
point = _create_point(run_id, i, attempt, ref, commit, event.value, value, run)
|
||||
points.append(point)
|
||||
|
||||
write_options = WriteOptions(write_type=WriteType.synchronous, retry_interval=5000, max_retries=5, max_retry_delay=30000, exponential_base=2)
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
import ctypes, struct
|
||||
from tinygrad.helpers import ceildiv, getenv, wait_cond, DEBUG
|
||||
from tinygrad.runtime.autogen import bnxt, pci
|
||||
from tinygrad.runtime.support.system import PCIDevice, System, ipv4_to_gid
|
||||
|
||||
BNXT_DEBUG = getenv("BNXT_DEBUG", 0)
|
||||
BNXT_ACCESS, BNXT_INIT_MASK, BNXT_RTR_MASK, BNXT_RTS_MASK = 3, 0xd, 0x41515ad, 0xae005
|
||||
BNXT_CHIMP_COMM, BNXT_CHIMP_COMM_TRIGGER = 0x0, 0x100
|
||||
BNXT_BACKING_STORE = ((0, 2), (1, 0), (2, 2), (3, 0), (4, 2), (5, 0), (6, 0), (14, 2), (15, 0))
|
||||
|
||||
def db_value(xid, typ, index, epoch):
|
||||
return (xid & bnxt.DBC_DBC_XID_MASK | bnxt.DBC_DBC_PATH_ROCE | typ | bnxt.BNXT_QPLIB_DBR_VALID) << 32 | \
|
||||
index & bnxt.DBC_DBC_INDEX_MASK | epoch << bnxt.BNXT_QPLIB_DBR_EPOCH_SHIFT
|
||||
|
||||
def _pbl(dev, paddrs, queue=False):
|
||||
if len(paddrs) == 1: return 0, paddrs[0]
|
||||
values = [p | bnxt.PTU_PTE_VALID for p in paddrs]
|
||||
if queue:
|
||||
values[-1] |= bnxt.PTU_PTE_LAST
|
||||
if len(values) > 1: values[-2] |= bnxt.PTU_PTE_NEXT_TO_LAST
|
||||
table, table_paddrs = dev.pci_dev.alloc_sysmem(ceildiv(len(values), 512) * 0x1000)
|
||||
table[:len(values) * 8] = struct.pack(f"<{len(values)}Q", *values)
|
||||
if len(table_paddrs) == 1: return 1, table_paddrs[0]
|
||||
top, top_paddrs = dev.pci_dev.alloc_sysmem(0x1000)
|
||||
top[:len(table_paddrs) * 8] = struct.pack(f"<{len(table_paddrs)}Q", *(p | bnxt.PTU_PTE_VALID for p in table_paddrs))
|
||||
return 2, top_paddrs[0]
|
||||
|
||||
def _queue(dev, stride:int=16, aux=False):
|
||||
mem, paddrs = dev.pci_dev.alloc_sysmem(0x1000 + aux * 0x400)
|
||||
level, base = _pbl(dev, paddrs, queue=True)
|
||||
return {"mem":mem, "paddrs":paddrs, "stride":stride, "prod":0, "cons":0, "level":level, "base":base}
|
||||
|
||||
def _qread(q, i):
|
||||
off = (i & 15) * q["stride"]
|
||||
return q["mem"][off:off + q["stride"]]
|
||||
|
||||
def _qwrite(q, i, data, aux=False):
|
||||
off = 0x1000 + i % 128 * 8 if aux else (i & 15) * q["stride"]
|
||||
q["mem"][off:off + len(data)] = data
|
||||
|
||||
class BNXTDev:
|
||||
def __init__(self, pci_dev:PCIDevice, ip:str=getenv("BNXT_IP", "10.0.0.1")):
|
||||
self.pci_dev, self.devfmt = pci_dev, pci_dev.pcibus
|
||||
self.bar0, self.db = pci_dev.map_bar(0, fmt='I'), pci_dev.map_bar(2, fmt='Q')
|
||||
pci_dev.write_config(pci.PCI_COMMAND, pci_dev.read_config(pci.PCI_COMMAND, 2) | pci.PCI_COMMAND_MASTER, 2)
|
||||
self.resp, self.resp_pa = pci_dev.alloc_sysmem(0x1000)
|
||||
self.seq = 0
|
||||
|
||||
ver = self.hwrm("ver_get")
|
||||
if DEBUG >= 2: print(f"bnxt {self.devfmt}: firmware {ver.hwrm_fw_maj_8b}.{ver.hwrm_fw_min_8b}.{ver.hwrm_fw_bld_8b}")
|
||||
self.hwrm("func_reset", timeout_ms=40000)
|
||||
caps = self.hwrm("func_qcaps", fid=0xffff)
|
||||
self.mac, self.port_id = int.from_bytes(bytes(caps.mac_address), 'big'), caps.port_id
|
||||
self.hwrm("func_drv_rgtr")
|
||||
self.db_off = self.hwrm("func_qcfg", fid=0xffff).legacy_l2_db_size_kb * 1024
|
||||
|
||||
self.setup_backing_store()
|
||||
self._open_rcfw()
|
||||
self._open_l2()
|
||||
self.local_gid = ipv4_to_gid(ip)
|
||||
gids, mac = (ctypes.c_uint32 * 4)(*(int.from_bytes(self.local_gid[i:i + 4], 'big') for i in (12, 8, 4, 0))), self.mac.to_bytes(6, 'big')
|
||||
smac = (ctypes.c_uint16 * 3)(*(int.from_bytes(mac[i:i + 2], 'big') for i in (0, 2, 4)))
|
||||
self.gid_id = self.rcfw("add_gid", gid=gids, src_mac=smac).xid
|
||||
|
||||
if DEBUG >= 2: print(f"bnxt {self.devfmt}: booted mac={self.mac.to_bytes(6, 'big').hex(':')} gid={self.local_gid.hex()}")
|
||||
|
||||
def hwrm(self, name, timeout_ms=10000, **fields):
|
||||
inp, out = getattr(bnxt, f"struct_hwrm_{name}_input"), getattr(bnxt, f"struct_hwrm_{name}_output")
|
||||
opcode = getattr(bnxt, f"HWRM_{name.upper()}")
|
||||
self.seq = (self.seq + 1) & 0xffff
|
||||
data = bytes(inp(req_type=opcode, cmpl_ring=bnxt.BNXT_HWRM_NO_CMPL_RING, seq_id=self.seq, target_id=bnxt.BNXT_HWRM_TARGET,
|
||||
resp_addr=self.resp_pa[0], **fields))
|
||||
self.resp[:] = bytes(len(self.resp))
|
||||
System.memory_barrier()
|
||||
for i, w in enumerate(memoryview(bytearray(data.ljust(bnxt.HWRM_MAX_REQ_LEN, b'\0'))).cast('I')):
|
||||
self.bar0[BNXT_CHIMP_COMM // 4 + i] = w
|
||||
self.bar0[BNXT_CHIMP_COMM_TRIGGER // 4] = 1
|
||||
def hdr(): return bnxt.struct_hwrm_resp_hdr.from_buffer_copy(bytes(self.resp[:8]))
|
||||
wait_cond(lambda: (n := hdr().resp_len) and hdr().seq_id == self.seq and self.resp[n - 1], timeout_ms=timeout_ms, msg=f"HWRM {name}")
|
||||
ret = out.from_buffer_copy(bytes(self.resp[:ctypes.sizeof(out)]))
|
||||
assert ret.error_code == 0, f"HWRM {name}: {ret.error_code}"
|
||||
return ret
|
||||
|
||||
def setup_backing_store(self):
|
||||
counts: dict[int, int] = {}
|
||||
for typ, extra in BNXT_BACKING_STORE:
|
||||
caps = self.hwrm("func_backing_store_qcaps_v2", type=typ)
|
||||
size, splits = caps.entry_size, tuple(getattr(caps, f"split_entry_{j}") for j in range(caps.subtype_valid_cnt))
|
||||
counts[typ] = n = counts[0] if typ == 15 else max(caps.min_num_entries, sum(splits) + extra)
|
||||
# a zero bitmap means the type has a single instance 0
|
||||
for instance in [i for i in range(8) if caps.instance_bit_map >> i & 1] or [0]:
|
||||
mem, paddrs = self.pci_dev.alloc_sysmem(ceildiv(n * size, 0x1000) * 0x1000)
|
||||
if caps.ctx_init_value:
|
||||
for off in range(caps.ctx_init_offset, len(mem), size): mem[off] = caps.ctx_init_value
|
||||
lvl, base = _pbl(self, paddrs)
|
||||
self.hwrm("func_backing_store_cfg_v2", type=typ, instance=instance, entry_size=size, num_entries=n, page_dir=base,
|
||||
page_size_pbl_level=lvl, subtype_valid_cnt=len(splits),
|
||||
flags=bnxt.FUNC_BACKING_STORE_CFG_V2_REQ_FLAGS_BS_CFG_ALL_DONE if typ == 15 else 0,
|
||||
**{f"split_entry_{j}": v for j, v in enumerate(splits)})
|
||||
|
||||
def _open_rcfw(self):
|
||||
self.rcfw_first = True
|
||||
|
||||
self.creq = _queue(self)
|
||||
self.creq_id = self.hwrm("ring_alloc", ring_type=bnxt.RING_ALLOC_REQ_RING_TYPE_NQ, page_tbl_addr=self.creq["base"],
|
||||
page_size=12, page_tbl_depth=self.creq["level"], length=16, int_mode=bnxt.RING_ALLOC_REQ_INT_MODE_MSIX).ring_id
|
||||
|
||||
self.cmdq = _queue(self)
|
||||
self.doorbell(self.creq_id, bnxt.DBC_DBC_TYPE_NQ_ARM, 0, 0)
|
||||
init = bnxt.struct_cmdq_init(cmdq_pbl=self.cmdq["base"], creq_ring_id=self.creq_id,
|
||||
cmdq_size_cmdq_lvl=16 << bnxt.CMDQ_INIT_CMDQ_SIZE_SFT)
|
||||
|
||||
System.memory_barrier()
|
||||
for i, w in enumerate(memoryview(bytearray(bytes(init))).cast('I')): self.bar0[bnxt.RCFW_COMM_BASE_OFFSET // 4 + i] = w
|
||||
|
||||
_, p = self.pci_dev.alloc_sysmem(0x1000)
|
||||
self.rcfw("initialize_fw", stat_ctx_id=self.hwrm("stat_ctx_alloc", stats_dma_addr=p[0], stats_dma_length=176).stat_ctx_id,
|
||||
flags=bnxt.CMDQ_INITIALIZE_FW_FLAGS_HW_REQUESTER_RETX_SUPPORTED)
|
||||
|
||||
# RoCE notification ring: never armed or serviced, but CQ and L2 ring allocation require one
|
||||
nq = _queue(self)
|
||||
self.nq_id = self.hwrm("ring_alloc", ring_type=bnxt.RING_ALLOC_REQ_RING_TYPE_NQ, page_tbl_addr=nq["base"],
|
||||
page_size=12, page_tbl_depth=nq["level"], length=16, logical_id=1, int_mode=bnxt.RING_ALLOC_REQ_INT_MODE_MSIX).ring_id
|
||||
|
||||
def rcfw(self, name, timeout_ms=20000, **fields):
|
||||
req_t, resp_t = getattr(bnxt, f"struct_cmdq_{name}"), getattr(bnxt, f"struct_creq_{name}_resp")
|
||||
op = getattr(bnxt, f"CMDQ_BASE_OPCODE_{name.upper()}")
|
||||
data = bytes(req_t(opcode=op, cmd_size=(slots := ceildiv(ctypes.sizeof(req_t), 16)), **fields)).ljust(slots * 16, b'\0')
|
||||
for i in range(slots): _qwrite(self.cmdq, self.cmdq["prod"] + i, data[i * 16:(i + 1) * 16])
|
||||
|
||||
self.cmdq["prod"] += slots
|
||||
prod = self.cmdq["prod"] & 0xffff
|
||||
if self.rcfw_first: prod, self.rcfw_first = prod | 1 << bnxt.FIRMWARE_FIRST_FLAG, False
|
||||
|
||||
System.memory_barrier()
|
||||
|
||||
self.bar0[(bnxt.RCFW_COMM_BASE_OFFSET + bnxt.RCFW_PF_VF_COMM_PROD_OFFSET) // 4] = prod
|
||||
self.bar0[(bnxt.RCFW_COMM_BASE_OFFSET + bnxt.RCFW_COMM_TRIG_OFFSET) // 4] = bnxt.RCFW_CMDQ_TRIG_VAL
|
||||
|
||||
def poll():
|
||||
h = bnxt.struct_creq_base.from_buffer_copy(bytes(_qread(self.creq, self.creq["cons"])))
|
||||
return bool(h.v & bnxt.CREQ_BASE_V) != bool((self.creq["cons"] // 16) & 1)
|
||||
wait_cond(poll, timeout_ms=timeout_ms, msg=f"RCFW {name}")
|
||||
|
||||
ret = resp_t.from_buffer_copy(bytes(_qread(self.creq, self.creq["cons"])))
|
||||
self.creq["cons"] += 1
|
||||
|
||||
# NQ_ARM also publishes the CREQ consumer index, which is what frees ring space for the next command
|
||||
self.doorbell(self.creq_id, bnxt.DBC_DBC_TYPE_NQ_ARM, self.creq["cons"] & 15, (self.creq["cons"] // 16) & 1)
|
||||
assert ret.status == 0, f"RCFW {name}: {ret.status}"
|
||||
|
||||
if BNXT_DEBUG >= 1: print(f"bnxt {self.devfmt}: rcfw {name} xid={getattr(ret, 'xid', 0):#x}")
|
||||
return ret
|
||||
|
||||
def doorbell(self, xid, typ, index, epoch):
|
||||
System.memory_barrier()
|
||||
self.db[self.db_off // 8] = db_value(xid, typ, index, epoch)
|
||||
|
||||
# L2 receive path, required for RoCE ingress even though no ethernet receive buffers are posted
|
||||
def _open_l2(self):
|
||||
cq = _queue(self)
|
||||
ci = self.hwrm("ring_alloc", enables=bnxt.RING_ALLOC_REQ_ENABLES_NQ_RING_ID_VALID, ring_type=bnxt.RING_ALLOC_REQ_RING_TYPE_L2_CMPL,
|
||||
page_tbl_addr=cq["base"], page_size=12, page_tbl_depth=cq["level"], length=16, nq_ring_id=self.nq_id).ring_id
|
||||
rx = _queue(self)
|
||||
ri = self.hwrm("ring_alloc", enables=bnxt.RING_ALLOC_REQ_ENABLES_NQ_RING_ID_VALID |
|
||||
bnxt.RING_ALLOC_REQ_ENABLES_RX_BUF_SIZE_VALID, ring_type=bnxt.RING_ALLOC_REQ_RING_TYPE_RX, page_tbl_addr=rx["base"],
|
||||
page_size=12, page_tbl_depth=rx["level"], length=16, rx_buf_size=640, nq_ring_id=self.nq_id).ring_id
|
||||
vi = self.hwrm("vnic_alloc").vnic_id
|
||||
self.hwrm("vnic_cfg", enables=bnxt.VNIC_CFG_REQ_ENABLES_MRU | bnxt.VNIC_CFG_REQ_ENABLES_DEFAULT_RX_RING_ID |
|
||||
bnxt.VNIC_CFG_REQ_ENABLES_DEFAULT_CMPL_RING_ID, vnic_id=vi, mru=9018,
|
||||
default_rx_ring_id=ri, default_cmpl_ring_id=ci)
|
||||
self.hwrm("cfa_l2_filter_alloc", flags=bnxt.CFA_L2_FILTER_ALLOC_REQ_FLAGS_PATH_RX,
|
||||
enables=bnxt.CFA_L2_FILTER_ALLOC_REQ_ENABLES_L2_ADDR | bnxt.CFA_L2_FILTER_ALLOC_REQ_ENABLES_L2_ADDR_MASK |
|
||||
bnxt.CFA_L2_FILTER_ALLOC_REQ_ENABLES_DST_ID, l2_addr=tuple(self.mac.to_bytes(6, 'big')), l2_addr_mask=(0xff,) * 6, dst_id=vi)
|
||||
|
||||
def register_mem(self, paddrs:list[int], size:int, log_page_size:int=12) -> int:
|
||||
level, base = _pbl(self, paddrs[:ceildiv(size, 1 << log_page_size)])
|
||||
return self.rcfw("register_mr", flags=bnxt.CMDQ_REGISTER_MR_FLAGS_ALLOC_MR,
|
||||
log2_pg_size_lvl=level << bnxt.CMDQ_REGISTER_MR_LVL_SFT | log_page_size << bnxt.CMDQ_REGISTER_MR_LOG2_PG_SIZE_SFT,
|
||||
access=bnxt.CMDQ_REGISTER_MR_ACCESS_LOCAL_WRITE | bnxt.CMDQ_REGISTER_MR_ACCESS_REMOTE_WRITE,
|
||||
log2_pbl_pg_size=12, pbl=base, va=paddrs[0], mr_size=size).xid
|
||||
|
||||
class BNXTQP:
|
||||
def __init__(self, dev:BNXTDev):
|
||||
self.dev, self.sq_psn, self.msn = dev, 0, 0
|
||||
|
||||
self.cqq = _queue(dev, ctypes.sizeof(bnxt.struct_cq_base))
|
||||
self.cq_id = dev.rcfw("create_cq", cq_size=16, pbl=self.cqq["base"],
|
||||
pg_size_lvl=self.cqq["level"], cq_fco_cnq_id=dev.nq_id).xid
|
||||
|
||||
self.sq = _queue(dev, aux=True)
|
||||
self.qpn = dev.rcfw("create_qp", type=bnxt.CMDQ_CREATE_QP_TYPE_RC,
|
||||
sq_size=16, sq_fwo_sq_sge=1, scq_cid=self.cq_id, rcq_cid=self.cq_id,
|
||||
sq_pbl=self.sq["base"], sq_pg_size_sq_lvl=self.sq["level"]).xid
|
||||
self.qp_op(1, BNXT_INIT_MASK, access=BNXT_ACCESS, pkey=0xffff)
|
||||
|
||||
def qp_op(self, state, mask, network_type=0, **fields):
|
||||
self.dev.rcfw("modify_qp", qp_cid=self.qpn, modify_mask=mask,
|
||||
network_type_en_sqd_async_notify_new_state=state | network_type, **fields)
|
||||
|
||||
def connect(self, qpn:int, gid:bytes, mac:int):
|
||||
network_type = bnxt.CMDQ_MODIFY_QP_NETWORK_TYPE_ROCEV2_IPV4
|
||||
dgid = (ctypes.c_uint32 * 4)(*(int.from_bytes(gid[i:i + 4], 'little') for i in (0, 4, 8, 12)))
|
||||
dmac = (ctypes.c_uint16 * 3)(*(int.from_bytes(mac.to_bytes(6, 'big')[i:i + 2], 'little') for i in (0, 2, 4)))
|
||||
|
||||
self.qp_op(2, BNXT_RTR_MASK, network_type=network_type, qp_type=bnxt.CMDQ_MODIFY_QP_QP_TYPE_RC, access=BNXT_ACCESS,
|
||||
pkey=0xffff, dgid=dgid, sgid_index=self.dev.gid_id, hop_limit=64, dest_mac=dmac,
|
||||
path_mtu_pingpong_push_enable=bnxt.CMDQ_MODIFY_QP_PATH_MTU_MTU_1024, max_dest_rd_atomic=4,
|
||||
dest_qp_id=qpn)
|
||||
self.qp_op(3, BNXT_RTS_MASK, network_type=network_type, qp_type=bnxt.CMDQ_MODIFY_QP_QP_TYPE_RC, access=BNXT_ACCESS,
|
||||
max_rd_atomic=1)
|
||||
|
||||
if BNXT_DEBUG >= 1: print(f"bnxt: QP {self.qpn:#x} connected (remote={qpn:#x})")
|
||||
|
||||
def _poll(self, timeout):
|
||||
def poll():
|
||||
base = bnxt.struct_cq_base.from_buffer_copy(bytes(_qread(self.cqq, self.cqq["cons"])))
|
||||
return bool(base.cqe_type_toggle & bnxt.CQ_BASE_TOGGLE) == (not bool((self.cqq["cons"] // 16) & 1))
|
||||
wait_cond(poll, timeout_ms=timeout, msg="BNXT CQ")
|
||||
raw = bytes(_qread(self.cqq, self.cqq["cons"]))
|
||||
self.cqq["cons"] += 1
|
||||
self.dev.doorbell(self.cq_id, bnxt.DBC_DBC_TYPE_CQ, self.cqq["cons"] & 15, (self.cqq["cons"] // 16) & 1)
|
||||
return raw
|
||||
|
||||
def rdma_write(self, rva, rkey, lva, lkey, size, timeout_ms=20000):
|
||||
start = self.sq["prod"] & 15
|
||||
hdr = bytes(bnxt.struct_sq_rdma_hdr(wqe_type=bnxt.SQ_RDMA_HDR_WQE_TYPE_WRITE_WQE,
|
||||
flags=bnxt.SQ_SEND_FLAGS_SIGNAL_COMP, wqe_size=3, length=size, remote_va=rva, remote_key=rkey))
|
||||
for i, data in enumerate((hdr[:16], hdr[16:32], bytes(bnxt.struct_sq_sge(va_or_pa=lva, l_key=lkey, size=size)))):
|
||||
_qwrite(self.sq, start + i, data)
|
||||
nxt = (self.sq_psn + max(1, ceildiv(size, 1024))) & 0xffffff
|
||||
value = start << bnxt.SQ_MSN_SEARCH_START_IDX_SFT | nxt << bnxt.SQ_MSN_SEARCH_NEXT_PSN_SFT | self.sq_psn
|
||||
_qwrite(self.sq, self.msn, struct.pack("<Q", value), aux=True)
|
||||
|
||||
self.msn, self.sq_psn, self.sq["prod"] = (self.msn + 1) % 128, nxt, self.sq["prod"] + 3
|
||||
self.dev.doorbell(self.qpn, bnxt.DBC_DBC_TYPE_SQ, self.sq["prod"] & 15, (self.sq["prod"] // 16) & 1)
|
||||
cqe = bnxt.struct_cq_req.from_buffer_copy(self._poll(timeout_ms))
|
||||
assert cqe.status == 0
|
||||
@@ -1,118 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Send and validate one RDMA WRITE between two Broadcom BNXT hosts.
|
||||
|
||||
This follows ``extra/mlx_driver/connect.py``: sync the driver, start the remote
|
||||
endpoint over SSH, exchange QP/GID/MAC/MR metadata, move both RC QPs to RTS,
|
||||
write bytes into the remote MR, and verify the bytes on the remote host.
|
||||
|
||||
Both PCI functions must be unbound from bnxt_en/bnxt_re first.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any, IO
|
||||
|
||||
TINYGRAD = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../.."))
|
||||
sys.path.insert(0, TINYGRAD)
|
||||
|
||||
from extra.bnxt_driver.bnxtdev import BNXTDev, BNXTQP
|
||||
from tinygrad.runtime.support.system import PCIDevice
|
||||
|
||||
REMOTE_HOST = os.getenv("REMOTE_HOST", "192.168.52.213")
|
||||
REMOTE_USER = os.getenv("REMOTE_USER", "nimlgen")
|
||||
LOCAL_PCI = os.getenv("BNXT_PCI", "0000:41:00.0")
|
||||
REMOTE_PCI = os.getenv("REMOTE_PCI", "0000:41:00.0")
|
||||
LOCAL_IP = os.getenv("LOCAL_IP", "10.0.200.5")
|
||||
REMOTE_IP = os.getenv("REMOTE_IP", "10.0.200.6")
|
||||
MESSAGE = os.getenv("RDMA_MESSAGE", "Test message, rdma works!").encode()
|
||||
REMOTE = f"{REMOTE_USER}@{REMOTE_HOST}"
|
||||
SSH = ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "-o", "StrictHostKeyChecking=accept-new", REMOTE]
|
||||
SYNC_FILES = ("tinygrad/runtime/autogen/bnxt.py", "tinygrad/runtime/support/system.py",
|
||||
"extra/bnxt_driver/bnxtdev.py", "extra/bnxt_driver/connect.py")
|
||||
|
||||
def read_json(stream:IO[str], what:str) -> dict[str, Any]:
|
||||
for line in iter(stream.readline, ""):
|
||||
print(f" [remote] {line}", end="")
|
||||
try: value = json.loads(line)
|
||||
except json.JSONDecodeError: continue
|
||||
if isinstance(value, dict): return value
|
||||
raise RuntimeError(f"remote exited before publishing {what}")
|
||||
|
||||
def wait_line(stream:IO[str], text:str) -> str:
|
||||
for line in iter(stream.readline, ""):
|
||||
print(f" [remote] {line}", end="")
|
||||
if text in line: return line
|
||||
raise RuntimeError(f"remote exited before reporting {text!r}")
|
||||
|
||||
def send_line(stream:IO[str], value:str|dict[str, Any]):
|
||||
stream.write((json.dumps(value) if isinstance(value, dict) else value) + "\n")
|
||||
stream.flush()
|
||||
|
||||
def qp_info(dev:BNXTDev, qp:BNXTQP) -> dict[str, Any]:
|
||||
return {"qpn":qp.qpn, "mac":dev.mac.to_bytes(6, "big").hex(), "gid":dev.local_gid.hex()}
|
||||
|
||||
def server():
|
||||
dev = BNXTDev(PCIDevice("bnxt", os.getenv("BNXT_PCI", "0000:41:00.0")), ip=os.getenv("BNXT_IP", REMOTE_IP))
|
||||
qp = BNXTQP(dev)
|
||||
print(json.dumps(qp_info(dev, qp)), flush=True)
|
||||
|
||||
peer = json.loads(sys.stdin.readline())
|
||||
qp.connect(peer["qpn"], bytes.fromhex(peer["gid"]), int(peer["mac"], 16))
|
||||
print("connected", flush=True)
|
||||
|
||||
target, target_paddrs = dev.pci_dev.alloc_sysmem(0x1000)
|
||||
target[:0x1000] = bytes(0x1000)
|
||||
rkey = dev.register_mem(target_paddrs, 0x1000)
|
||||
print(json.dumps({"target_addr":target_paddrs[0], "rkey":rkey}), flush=True)
|
||||
|
||||
assert sys.stdin.readline().strip() == "done"
|
||||
received = bytes(target).rstrip(b"\0")
|
||||
print(f"AS TEXT: {received.decode(errors='replace')!r}", flush=True)
|
||||
print(json.dumps({"data":received.hex()}), flush=True)
|
||||
|
||||
def sync_remote():
|
||||
if os.getenv("SYNC", "1") == "0": return
|
||||
print("syncing BNXT driver to remote")
|
||||
subprocess.run(["rsync", "-azR", *SYNC_FILES, f"{REMOTE}:~/tinygrad/"], cwd=TINYGRAD, check=True)
|
||||
|
||||
def start_remote() -> subprocess.Popen[str]:
|
||||
print("booting remote")
|
||||
command = (f"cd ~/tinygrad && sudo env PYTHONPATH=. PYTHONUNBUFFERED=1 BNXT_DEBUG={os.getenv('BNXT_DEBUG', '0')} "
|
||||
f"BNXT_PCI={REMOTE_PCI} BNXT_IP={REMOTE_IP} python3 extra/bnxt_driver/connect.py --server")
|
||||
return subprocess.Popen(SSH + [command], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=sys.stderr, text=True)
|
||||
|
||||
def client():
|
||||
assert 0 < len(MESSAGE) <= 0x1000
|
||||
sync_remote()
|
||||
remote = start_remote()
|
||||
assert remote.stdin is not None and remote.stdout is not None
|
||||
remote_info = read_json(remote.stdout, "QP information")
|
||||
print("booting local")
|
||||
dev = BNXTDev(PCIDevice("bnxt", LOCAL_PCI), ip=LOCAL_IP)
|
||||
qp = BNXTQP(dev)
|
||||
|
||||
send_line(remote.stdin, qp_info(dev, qp))
|
||||
wait_line(remote.stdout, "connected")
|
||||
qp.connect(remote_info["qpn"], bytes.fromhex(remote_info["gid"]), int(remote_info["mac"], 16))
|
||||
print("both QPs in RTS")
|
||||
|
||||
remote_target = read_json(remote.stdout, "MR information")
|
||||
source, source_paddrs = dev.pci_dev.alloc_sysmem(0x1000)
|
||||
source[:len(MESSAGE)] = MESSAGE
|
||||
lkey = dev.register_mem(source_paddrs, 0x1000)
|
||||
print(f"RDMA WRITE {len(MESSAGE)}B to remote phys 0x{remote_target['target_addr']:x}")
|
||||
qp.rdma_write(remote_target["target_addr"], remote_target["rkey"], source_paddrs[0], lkey, len(MESSAGE))
|
||||
|
||||
send_line(remote.stdin, "done")
|
||||
wait_line(remote.stdout, "AS TEXT")
|
||||
result = read_json(remote.stdout, "RDMA result")
|
||||
assert bytes.fromhex(result["data"]) == MESSAGE
|
||||
print("RDMA WRITE data verified")
|
||||
|
||||
remote.stdin.close()
|
||||
assert remote.wait() == 0
|
||||
print("RDMA WRITE test complete")
|
||||
|
||||
if __name__ == "__main__":
|
||||
server() if "--server" in sys.argv else client()
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Local BNXT RoCEv2 RDMA WRITE loopback using the firmware's PHY loopback mode.
|
||||
|
||||
The kernel bnxt_en/bnxt_re modules must be unloaded first.
|
||||
|
||||
sudo PYTHONPATH=. BNXT_PCI=0000:41:00.0 BNXT_IP=10.0.200.5 python3 extra/bnxt_driver/loopback.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "../.."))
|
||||
|
||||
from extra.bnxt_driver.bnxtdev import BNXTDev, BNXTQP
|
||||
from tinygrad.runtime.autogen import bnxt
|
||||
from tinygrad.runtime.support.system import PCIDevice
|
||||
|
||||
BUF_SIZE = 0x1000
|
||||
BNXT_PCI = os.getenv("BNXT_PCI", "0000:41:00.0")
|
||||
BNXT_IP = os.getenv("BNXT_IP", "10.0.200.5")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"[init] BNXT at {BNXT_PCI}")
|
||||
dev = BNXTDev(PCIDevice("bnxt", BNXT_PCI), ip=BNXT_IP)
|
||||
tx_qp, rx_qp = BNXTQP(dev), BNXTQP(dev)
|
||||
print(f"[init] loopback-connect TX QP 0x{tx_qp.qpn:x} <-> RX QP 0x{rx_qp.qpn:x}")
|
||||
tx_qp.connect(rx_qp.qpn, dev.local_gid, dev.mac)
|
||||
rx_qp.connect(tx_qp.qpn, dev.local_gid, dev.mac)
|
||||
|
||||
src, src_paddrs = dev.pci_dev.alloc_sysmem(BUF_SIZE)
|
||||
dst, dst_paddrs = dev.pci_dev.alloc_sysmem(BUF_SIZE)
|
||||
message = b"Hello from BNXT RoCE PHY loopback!"
|
||||
src[:BUF_SIZE], dst[:BUF_SIZE] = bytes(BUF_SIZE), bytes(BUF_SIZE)
|
||||
src[:len(message)] = message
|
||||
lkey = dev.register_mem(src_paddrs, BUF_SIZE)
|
||||
rkey = dev.register_mem(dst_paddrs, BUF_SIZE)
|
||||
|
||||
print("[loopback] enabling local PHY loopback")
|
||||
dev.hwrm("port_phy_cfg", port_id=dev.port_id, enables=bnxt.PORT_PHY_CFG_REQ_ENABLES_LPBK, lpbk=bnxt.PORT_PHY_CFG_REQ_LPBK_LOCAL)
|
||||
time.sleep(1)
|
||||
tx_qp.rdma_write(dst_paddrs[0], rkey, src_paddrs[0], lkey, len(message))
|
||||
got = bytes(dst[:len(message)])
|
||||
print(f"[result] {got!r}")
|
||||
assert got == message
|
||||
print("BNXT RoCE PHY loopback RDMA WRITE passed")
|
||||
dev.hwrm("port_phy_cfg", port_id=dev.port_id, enables=bnxt.PORT_PHY_CFG_REQ_ENABLES_LPBK, lpbk=bnxt.PORT_PHY_CFG_REQ_LPBK_NONE)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -53,7 +53,7 @@ def _ggather_bwd(gradient:UOp, kernel:UOp) -> tuple:
|
||||
g, m, j, jo, ji = _kv_ranges(Gk, M, Dk, _blk_for(Dk))
|
||||
row = idx.index(g, m).cast(dtypes.weakint)
|
||||
val = gout.index(g, m, j).load().cast(dtypes.float32)
|
||||
atomic = UOp(Ops.CUSTOM, src=(gtab.index(g, row, j), val), arg=(atomic_str, dtypes.void))
|
||||
atomic = UOp(Ops.CUSTOM, dtypes.void, (gtab.index(g, row, j), val), arg=atomic_str)
|
||||
return atomic.end(g, m, jo, ji).sink(arg=KernelInfo(name=f"ggather_bwd_{M}_{Dk}", opts_to_apply=()))
|
||||
grad_table = Tensor.custom_kernel(gt, go, Tensor(idx_u, device=dev), fxn=_bwd_kernel)[0]
|
||||
return (None, grad_table.cast(table_u.dtype).uop, None)
|
||||
|
||||
+3
-10
@@ -16,12 +16,9 @@ def _do_reset_device(pci_bus): os.system(f"sudo sh -c 'echo 1 > /sys/bus/pci/dev
|
||||
def _is_module_loaded(name: str) -> bool: return os.path.isdir(f"/sys/module/{name}")
|
||||
|
||||
def cmd_remove_module(args):
|
||||
modules = ["nvidia_drm", "nvidia_modeset", "nvidia_uvm", "nvidia"] if args.backend == "nv" else ["amdgpu"]
|
||||
modules = ["nvidia_drm", "nvidia_modeset", "nvidia_uvm", "nvidia", "ast"] if args.backend == "nv" else ["amdgpu"]
|
||||
to_unload = [m for m in modules if _is_module_loaded(m)]
|
||||
if not to_unload: print("Kernel modules are not loaded")
|
||||
elif getattr(args, "expect", False):
|
||||
print(f"Kernel modules are loaded: {to_unload}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("Removing kernel modules:", ", ".join(to_unload))
|
||||
try: subprocess.run(["sudo", "modprobe", "-r", *to_unload], check=True)
|
||||
@@ -63,19 +60,17 @@ def cmd_show_pids(args):
|
||||
|
||||
def cmd_kill_pids(args):
|
||||
devs = scan_devs_based_on_lock(prefix:={"amd":"am", "nv":"nv"}[args.backend], args)
|
||||
use_sudo = not getattr(args, "sudoless", False)
|
||||
|
||||
for dev in devs:
|
||||
for i in range(128):
|
||||
if i > 0: time.sleep(0.2)
|
||||
|
||||
try:
|
||||
try: pid = subprocess.check_output((['sudo'] if use_sudo else []) +
|
||||
['lsof', temp(f'{prefix}_{dev}.lock')]).decode('utf-8').strip().split('\n')[1].split()[1]
|
||||
try: pid = subprocess.check_output(['sudo', 'lsof', temp(f'{prefix}_{dev}.lock')]).decode('utf-8').strip().split('\n')[1].split()[1]
|
||||
except subprocess.CalledProcessError: break
|
||||
|
||||
print(f"Killing process {pid} (which uses {dev})")
|
||||
subprocess.run((['sudo'] if use_sudo else []) + ['kill', '-9', pid], check=True)
|
||||
subprocess.run(['sudo', 'kill', '-9', pid], check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Failed to kill process for device {dev}: {e}", file=sys.stderr)
|
||||
|
||||
@@ -84,7 +79,6 @@ def add_common_commands(parent_subparsers):
|
||||
p_insmod.set_defaults(func=cmd_insert_module)
|
||||
|
||||
p_rmmod = parent_subparsers.add_parser("rmmod", help="Remove a kernel module")
|
||||
p_rmmod.add_argument("--expect", action="store_true", help="Just assert that module is already unloaded")
|
||||
p_rmmod.set_defaults(func=cmd_remove_module)
|
||||
|
||||
p_reset = parent_subparsers.add_parser("reset", help="Reset a device")
|
||||
@@ -97,7 +91,6 @@ def add_common_commands(parent_subparsers):
|
||||
|
||||
p_reset = parent_subparsers.add_parser("kill_pids", help="Kill pids of processes using the device")
|
||||
p_reset.add_argument("--pci_bus", default="", help="PCI bus ID of the device")
|
||||
p_reset.add_argument("--sudoless", action="store_true", help="Do not use sudo when detecting or killing pids")
|
||||
p_reset.set_defaults(func=cmd_kill_pids)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+20
-71
@@ -19,7 +19,7 @@ from tinygrad.runtime.support.hcq import FileIOInterface, HCQBuffer, MMIOInterfa
|
||||
from tinygrad.runtime.support.am.amdev import AMDev, AMMemoryManager
|
||||
from tinygrad.runtime.support.amd import AMDReg, AMDIP, import_module, import_soc, import_pmc
|
||||
from tinygrad.runtime.support.system import PCIIfaceBase, PCIAllocationMeta, USBPCIDevice, MAP_FIXED, MAP_NORESERVE
|
||||
from tinygrad.runtime.support.usb import USB3, usb_ib, usb_push, usb_arm_bytes, pm_usb_stage, pm_usb_hostio, pm_usb_bufferize
|
||||
from tinygrad.runtime.support.usb import USB3
|
||||
from tinygrad.runtime.support.memory import AddrSpace, BumpAllocator
|
||||
from tinygrad.runtime.ops_amd import SQTT, SQTT_ITRACE_SE_MASK, SQTT_LIMIT_SE, SQTT_SIMD_SEL, SQTT_TOKEN_EXCLUDE, PMC
|
||||
from tinygrad.runtime.ops_amd import EVENT_INDEX_PARTIAL_FLUSH, WAIT_REG_MEM_FUNCTION_EQ, WAIT_REG_MEM_FUNCTION_NEQ, WAIT_REG_MEM_FUNCTION_GEQ
|
||||
@@ -87,7 +87,7 @@ def release_mem(ctx, address=0x0, value=0, data_sel=0, int_sel=2, ctxid=0, cache
|
||||
|
||||
def memory_barrier(ctx):
|
||||
pf = '' if ctx.nbio.version[0] == 2 else '0' if ctx.nbio.version[:2] != (7, 11) else '1'
|
||||
return UOp(Ops.LINEAR, src=(
|
||||
return UOp(Ops.LINEAR, dtypes.void, (
|
||||
wait_reg_mem(ctx, reg=getattr(ctx.nbio, f'regBIF_BX_PF{pf}_GPU_HDP_FLUSH_REQ').addr[0],
|
||||
reg_done=getattr(ctx.nbio, f'regBIF_BX_PF{pf}_GPU_HDP_FLUSH_DONE').addr[0], value=0xffffffff),
|
||||
acquire_mem(ctx)))
|
||||
@@ -135,7 +135,7 @@ def pm4_program(ctx, call, prg):
|
||||
wreg(ctx, ctx.gc.regCOMPUTE_START_X, 0, 0, 0, *(info.local_size or (1, 1, 1)), 0, 0),
|
||||
pkt3(ctx, PM4Ops.DISPATCH_DIRECT, *info.global_size, dispatch_init),
|
||||
pkt3(ctx, PM4Ops.EVENT_WRITE, ctx.pm4.EVENT_TYPE(ctx.soc.CS_PARTIAL_FLUSH) | ctx.pm4.EVENT_INDEX(EVENT_INDEX_PARTIAL_FLUSH))]
|
||||
return UOp(Ops.LINEAR, src=tuple(ins))
|
||||
return UOp(Ops.LINEAR, dtypes.void, tuple(ins))
|
||||
|
||||
pm_pm4_opsel = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.PROGRAM, name="prg"),), name="call", allow_any_len=True), pm4_program),
|
||||
@@ -146,14 +146,11 @@ pm_pm4_opsel = PatternMatcher([
|
||||
(UPat(Ops.INS, arg="store", src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val"))), pm4_store),
|
||||
])
|
||||
|
||||
def queue_ptrs(devs, qname:str, q:AMDQueueDesc) -> tuple[UOp, ...]:
|
||||
return tuple(UOp.placeholder((b.size,), b.dtype, 0, device=devs).rtag(f"{qname}_{n}")
|
||||
for n, b in (("ring", q.ring), ("write_ptr", q.write_ptr), ("doorbell", q.doorbell), ("put_value", q.put_value)))
|
||||
|
||||
def pm4_submit(ctx, lin):
|
||||
# ensure compute queues are allocated
|
||||
for d in (devs:=ctx.devs): q = Device[d].compute_queue
|
||||
ring, wptr, doorbell, put_ptr = queue_ptrs(devs, "COMPUTE:0", q)
|
||||
ring, wptr, doorbell, put_ptr = (UOp.placeholder((b.size,), b.dtype, 0, device=devs).rtag(f"COMPUTE:0_{name}")
|
||||
for name, b in (("ring", q.ring), ("write_ptr", q.write_ptr), ("doorbell", q.doorbell), ("put_value", q.put_value)))
|
||||
|
||||
# the host fence at the start of the batch guarantees the ib is free to reuse
|
||||
size_dw = sum(len(ins.src) for ins in lin.src)
|
||||
@@ -207,7 +204,7 @@ def sdma_timestamp(ctx, ins, dst):
|
||||
pm_sdma_opsel = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.COPY),), name="call", allow_any_len=True), sdma_copy),
|
||||
|
||||
(UPat(Ops.INS, arg="barrier"), lambda: UOp(Ops.NOOP)),
|
||||
(UPat(Ops.INS, arg="barrier"), lambda: UOp(Ops.NOOP, dtypes.void, ())),
|
||||
(UPat(Ops.INS, arg="wait", src=(UPat(name="dst"), UPat(name="val")), name="ins"), sdma_wait),
|
||||
(UPat(Ops.INS, arg="timestamp", src=(UPat(name="dst"),), name="ins"), sdma_timestamp),
|
||||
(UPat(Ops.INS, arg="store", src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val")), name="ins"), sdma_store),
|
||||
@@ -219,7 +216,8 @@ def sdma_submit(cmdbuf, devs):
|
||||
|
||||
# the sdma queue's ring and its host-side ring/write/put pointers
|
||||
for d in devs: q = Device[d].sdma_queue(0)
|
||||
ring, wptr, doorbell, put_ptr = queue_ptrs(devs, "COPY:0", q)
|
||||
ring, wptr, doorbell, put_ptr = (UOp.placeholder((b.size,), b.dtype, 0, device=devs).rtag(f"COPY:0_{name}")
|
||||
for name, b in (("ring", q.ring), ("write_ptr", q.write_ptr), ("doorbell", q.doorbell), ("put_value", q.put_value)))
|
||||
|
||||
# sdma needs the cmdbuf contiguous: if it won't fit before the ring end, restart at 0 and zero the tail
|
||||
put_b = put_ptr.index(zero)
|
||||
@@ -246,32 +244,15 @@ def sdma_submit(cmdbuf, devs):
|
||||
pm_sdma_submit = PatternMatcher([(UPat(Ops.LINEAR, name="lin"),
|
||||
lambda ctx, lin: sdma_submit(make_cmdbuf(lin, ctx.devs), ctx.devs))])
|
||||
|
||||
# *****************
|
||||
# USB submit
|
||||
|
||||
def amd_usb_submit(ctx, lin):
|
||||
for d in ctx.devs: q = Device[d].compute_queue if (comp:=ctx.qname.startswith("COMPUTE")) else Device[d].sdma_queue(0)
|
||||
|
||||
if nb:=usb_arm_bytes(ctx.pre, Device[ctx.devs[0]].iface.usb_sram):
|
||||
poke = (ctx.sdma.SDMA_OP_WRITE, *data64_le(Device[ctx.devs[0]].iface.cq_buf.va_addr + 12), 0, 0)
|
||||
lin = lin.replace(src=lin.src + (UOp(Ops.INS, arg="poke", src=tuple(UOp.const(x, dtypes.uint32) for x in poke)),))
|
||||
|
||||
ib_host, ib_gpu, pkt_dw = usb_ib(ctx.devs, lin, 32 if comp else 0x100, nb)
|
||||
pkt = (ctx.pm4.PACKET3(ctx.pm4.PACKET3_INDIRECT_BUFFER,2),*data64_le(ib_gpu.getaddr(ctx.devs)),pkt_dw|ctx.pm4.INDIRECT_BUFFER_VALID) if comp else ()
|
||||
return usb_push(ctx.devs, *queue_ptrs(ctx.devs, ctx.qname, q), ib_host, ib_gpu, pkt, 4 if comp else 1)
|
||||
|
||||
pm_usb_submit = PatternMatcher([(UPat(Ops.LINEAR, name="lin"), amd_usb_submit)])
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AMDEncodeCtx: # encode-time constants for one queue: devs (every cmdbuf address resolves into these) + gfx version + packet/ip modules
|
||||
devs: tuple[str, ...]; target: tuple[int, ...]; pm4: Any; sdma: Any; soc: Any # noqa: E702
|
||||
gc: AMDIP; nbio: AMDIP; xccs: int; max_copy_size: int; tmpring_size: Callable; qname: str; pre: UOp # pre: the queue before opsel
|
||||
gc: AMDIP; nbio: AMDIP; xccs: int; max_copy_size: int; tmpring_size: Callable # noqa: E702
|
||||
|
||||
def encode_queue(q:UOp) -> UOp|None:
|
||||
d = Device[(devs:=to_tuple(q.arg[0]))[0]]
|
||||
ctx = AMDEncodeCtx(devs, d.target, d.pm4, d.sdma, d.soc, d.gc, d.nbio, d.xccs, d.max_copy_size, d.tmpring_size, q.arg[1], q)
|
||||
opsel = pm_pm4_opsel if (comp:=q.arg[1].startswith("COMPUTE")) else pm_sdma_opsel
|
||||
submit = d.pm_submit if d.pm_submit is not None else (pm_pm4_submit if comp else pm_sdma_submit)
|
||||
ctx = AMDEncodeCtx(devs, d.target, d.pm4, d.sdma, d.soc, d.gc, d.nbio, d.xccs, d.max_copy_size, d.tmpring_size)
|
||||
opsel, submit = (pm_pm4_opsel, pm_pm4_submit) if q.arg[1].startswith("COMPUTE") else (pm_sdma_opsel, pm_sdma_submit)
|
||||
return submit.rewrite(graph_rewrite(q, opsel + pm_flatten_linear, walk=True, ctx=ctx, name=f"{q.arg[1]} opsel"), ctx)
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -301,14 +282,13 @@ def amd_build_program(prg:UOp) -> UOp:
|
||||
wave32=bool(desc.kernel_code_properties & 0x400), private_segment_size=desc.private_segment_fixed_size, kernargs_segment_size=desc.kernarg_size,
|
||||
kernargs_alloc_size=desc.kernarg_size + (ctypes.sizeof(hsa.hsa_kernel_dispatch_packet_t) if edp else 0), enable_dispatch_ptr=edp,
|
||||
enable_private_segment_sgpr=desc.kernel_code_properties & hsa.AMD_KERNEL_CODE_PROPERTIES_ENABLE_SGPR_PRIVATE_SEGMENT_BUFFER)
|
||||
image = bytes(image).ljust(round_up(len(image), 4), b"\x00") # the program is uploaded as whole dwords
|
||||
buf = UOp.placeholder((len(image),), dtypes.uint8, next(UOp.unique_num), device=prg.device).rtag("program")
|
||||
cached = _amd_program_cache[key] = prg.replace(src=(buf.after(make_binary_patch(buf, image)),), arg=(data, prg.arg))
|
||||
cached = _amd_program_cache[key] = prg.replace(src=(buf.after(make_binary_patch(buf, bytes(image))),), arg=(data, prg.arg))
|
||||
return cached
|
||||
|
||||
class AMDAllocator(HCQAllocator['AMDDevice']):
|
||||
def __init__(self, dev:AMDDevice):
|
||||
super().__init__(dev, supports_copy_from_disk=dev.has_copy_queue, supports_transfer=dev.has_copy_queue and not dev.is_usb)
|
||||
super().__init__(dev, supports_copy_from_disk=dev.has_copy_queue, supports_transfer=dev.has_copy_queue and not dev.is_usb())
|
||||
|
||||
def _alloc(self, size:int, options:BufferSpec) -> HCQBuffer:
|
||||
return self.dev.iface.alloc(size, host=options.host, uncached=options.uncached, cpu_access=options.cpu_access or not self.dev.has_copy_queue)
|
||||
@@ -544,7 +524,8 @@ class PCIIface(PCIIfaceBase):
|
||||
cq = d.compute_queue
|
||||
for b in (cq.put_value, cq.read_ptr, cq.write_ptr): b._buf.view.view(fmt='Q')[0] = 0
|
||||
d.iface.dev_impl.gfx.setup_ring(*cq.params)
|
||||
d.signal('timeline')._buf.cpu_view().view(fmt='Q')[0] = d.signal('value', 1, device="CPU")._buf.cpu_view().view(fmt='Q')[0] - 1
|
||||
d.signal('timeline')._buf.cpu_view().mv.cast('Q')[0] = \
|
||||
d.signal('value', 1).as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')[0] - 1
|
||||
|
||||
def sleep(self, timeout):
|
||||
if hasattr(self.pci_dev, 'irq_poller') and self.pci_dev.irq_poller is not None and (events_cnt:=len(self.pci_dev.irq_poller.poll(timeout))):
|
||||
@@ -558,32 +539,6 @@ class PCIIface(PCIIfaceBase):
|
||||
|
||||
def device_fini(self): self.dev_impl.fini()
|
||||
|
||||
class USBIface(PCIIface):
|
||||
def __init__(self, dev, dev_id): # pylint: disable=super-init-not-called
|
||||
if dev_id >= len(visible:=hcq_filter_visible_devices(USB3.list_devices(0xADD1, 0x0001) + USB3.list_devices(0x3801, 0x0001), "AMD")):
|
||||
raise RuntimeError(f"AMD:{dev_id} does not exist ({pluralize('device', len(visible))} available)")
|
||||
self.dev, self.pci_dev, self.vram_bar, self.count = dev, USBPCIDevice("AM", *visible[dev_id]), 0, len(visible)
|
||||
self.dev_impl = AMDev(self.pci_dev)
|
||||
self._compute_props()
|
||||
self.sram = self._dma_region(ctrl_addr=0xf000, sys_addr=0x200000, size=0x80000)
|
||||
self.cq_buf = self._dma_region(ctrl_addr=0xb800, sys_addr=0x822000, size=0x1000) # +12 is the dword that releases an armed read
|
||||
self.usb_handle = unwrap(ctypes.cast(self.pci_dev.usb.usb.handle, ctypes.c_void_p).value)
|
||||
|
||||
def _dma_region(self, ctrl_addr, sys_addr, size):
|
||||
region = self.dev_impl.mm.map_range(vaddr:=self.dev_impl.mm.alloc_vaddr(size=size), size, [(sys_addr, size)], aspace=AddrSpace.SYS, uncached=True)
|
||||
return HCQBuffer(vaddr, size, meta=PCIAllocationMeta(region, has_cpu_mapping=False), view=self.pci_dev.dma_view(ctrl_addr, size), owner=self.dev)
|
||||
|
||||
def alloc(self, size:int, host=False, uncached=False, cpu_access=False, contiguous=False, force_devmem=False, **kwargs) -> HCQBuffer:
|
||||
# everything, even host-style signals, lives in vram: gpu writes into the bridge's own memory collide with an armed 0xF2 read stream
|
||||
return super().alloc(size, host=False, uncached=uncached, cpu_access=cpu_access or host, contiguous=contiguous, force_devmem=True, **kwargs)
|
||||
|
||||
def sleep(self, timeout): pass
|
||||
|
||||
# we don't own the sram region, so the buffer never frees it
|
||||
@functools.cached_property
|
||||
def usb_sram(self) -> Buffer:
|
||||
return Buffer(self.dev.device, (b:=self.sram).size, dtypes.uint8, options=BufferSpec(external_ptr=b.va_addr, nolru=True)).allocate(opaque=b)
|
||||
|
||||
def _mock(iface, name=None): return type(name or f"MOCK{iface.__name__}", (iface,), {})
|
||||
|
||||
class AMDDevice(HCQ2Compiled):
|
||||
@@ -594,21 +549,19 @@ class AMDDevice(HCQ2Compiled):
|
||||
# encoding of cmdbuf
|
||||
(UPat(Ops.CUSTOM_FUNCTION, arg="submit_cmdbuf", src=(UPat(Ops.LINEAR, name="q"),)), encode_queue),
|
||||
])
|
||||
pm_submit: PatternMatcher|None = None
|
||||
|
||||
timestamp_divider = 100.0 # AMD GPU clock: ticks/us
|
||||
max_scratch_psize = 0
|
||||
|
||||
ifaces = [KFDIface, PCIIface, USBIface, _mock(KFDIface, "MOCKIface"), _mock(KFDIface), _mock(PCIIface), _mock(USBIface)]
|
||||
ifaces = [KFDIface, PCIIface, _mock(KFDIface, "MOCKIface"), _mock(KFDIface), _mock(PCIIface)]
|
||||
|
||||
def device_props(self): return self.iface.props
|
||||
|
||||
def is_am(self) -> bool: return isinstance(self.iface, (PCIIface,))
|
||||
def is_usb(self) -> bool: return False
|
||||
|
||||
def __init__(self, device:str=""):
|
||||
self.iface = self._select_iface(device)
|
||||
self.is_usb = isinstance(self.iface, USBIface)
|
||||
if self.is_usb: self.rt_nbytes = 4 << 20
|
||||
|
||||
self.target:tuple[int, ...] = ((trgt:=self.iface.props['gfx_target_version']) // 10000, (trgt // 100) % 100, trgt % 100)
|
||||
self.arch = "gfx%d%x%x" % self.target
|
||||
@@ -633,7 +586,7 @@ class AMDDevice(HCQ2Compiled):
|
||||
|
||||
self.is_aql = getenv("AMD_AQL", int(self.xccs > 1))
|
||||
if self.is_aql:
|
||||
self.pm4_ibs = self.iface.alloc(0x2000 if self.is_usb else (16 << 20), uncached=True, cpu_access=True)
|
||||
self.pm4_ibs = self.iface.alloc(0x2000 if self.is_usb() else (16 << 20), uncached=True, cpu_access=True)
|
||||
self.pm4_ib_alloc = BumpAllocator(self.pm4_ibs.size, wrap=True)
|
||||
|
||||
self.max_copy_size = 0x40000000 if self.iface.ip_versions[am.SDMA0_HWIP][0] >= 5 else 0x400000
|
||||
@@ -646,10 +599,6 @@ class AMDDevice(HCQ2Compiled):
|
||||
self.max_private_segment_size = 0
|
||||
self.pm_bufferize = PatternMatcher([(UPat(Ops.PARAM, tag="scratch", name="b"), lambda ctx, b: ctx[0].scratch_buffer(b.max_numel()))]) + self.pm_bufferize
|
||||
|
||||
if self.is_usb:
|
||||
self.pm_bufferize = pm_usb_bufferize + self.pm_bufferize
|
||||
self.pm_stage_copy, self.pm_host_lower, self.pm_submit = pm_usb_stage, pm_usb_hostio, pm_usb_submit
|
||||
|
||||
self.pmc_enabled:bool = PROFILE > 0 and PMC > 0
|
||||
if self.pmc_enabled:
|
||||
self.iface.require_profile_mode()
|
||||
@@ -710,7 +659,7 @@ class AMDDevice(HCQ2Compiled):
|
||||
wg_data_size = round_up((vgpr_size_per_cu + sgrp_size_per_cu + lds_size_per_cu + hwreg_size_per_cu) * self.cu_cnt, mmap.PAGESIZE)
|
||||
ctl_stack_size = round_up((12 if self.target[0] != 9 else 8) * self.wave_cnt + 8 + 40, mmap.PAGESIZE)
|
||||
return self.create_queue(kfd.KFD_IOC_QUEUE_TYPE_COMPUTE_AQL if self.is_aql else kfd.KFD_IOC_QUEUE_TYPE_COMPUTE,
|
||||
0x2000 if self.is_usb else (16 << 20), eop_buffer_size=0x1000,
|
||||
0x2000 if self.is_usb() else (16 << 20), eop_buffer_size=0x1000,
|
||||
ctx_save_restore_size=0 if self.is_am() else wg_data_size + ctl_stack_size, ctl_stack_size=ctl_stack_size,
|
||||
debug_memory_size=round_up(self.wave_cnt * 32, 64))
|
||||
|
||||
@@ -718,7 +667,7 @@ class AMDDevice(HCQ2Compiled):
|
||||
if getenv("AMD_DISABLE_SDMA"): return None
|
||||
if idx in self.sdma_queues: return self.sdma_queues[idx]
|
||||
with contextlib.suppress(OSError):
|
||||
self.sdma_queues[idx] = self.create_queue(kfd.KFD_IOC_QUEUE_TYPE_SDMA, 0x2000 if self.is_usb else (16 << 20), idx=idx)
|
||||
self.sdma_queues[idx] = self.create_queue(kfd.KFD_IOC_QUEUE_TYPE_SDMA, 0x200 if self.is_usb() else (16 << 20), idx=idx)
|
||||
return self.sdma_queues.get(idx, None)
|
||||
|
||||
def tmpring_size(self, private_segment_size):
|
||||
|
||||
@@ -50,7 +50,7 @@ def _custom_quantize_fp8_with_amax(fp8_out:UOp, amax_out:UOp, x:UOp, amax_state:
|
||||
else: raise NotImplementedError(f"no atomic max for device {device}")
|
||||
amax_idx = amax_out.reshape((1,)).index(UOp.const(0))
|
||||
max_val = lds[0].load()
|
||||
atomic = UOp(Ops.CUSTOM, src=(amax_idx, max_val.bitcast(dtypes.int32), max_val, amax_idx.load()), arg=(atomic_arg, dtypes.void))
|
||||
atomic = UOp(Ops.CUSTOM, dtypes.void, (amax_idx, max_val.bitcast(dtypes.int32), max_val, amax_idx.load()), arg=atomic_arg)
|
||||
return atomic.end(tid, wg).sink(arg=KernelInfo(f"quantize_fp8_with_amax_{n_elems}", opts_to_apply=()))
|
||||
|
||||
@functools.cache
|
||||
|
||||
@@ -12,7 +12,7 @@ def _custom_quantize_mxfp4(row_fp4:UOp, row_scale:UOp, col_fp4:UOp, col_scale:UO
|
||||
mem = M*N*2 + M*N + M*N//16 # read bf16, write row+col fp4 + e8m0
|
||||
outputs = (row_fp4, row_scale, col_fp4, col_scale)
|
||||
sink = UOp.sink(*(o.base for o in outputs), x.base,
|
||||
*(UOp(Ops.CUSTOM, src=(o.base.index(0),), arg=("", dtypes.void)) for o in outputs),
|
||||
*(UOp(Ops.CUSTOM, dtypes.void, (o.base.index(0),), arg="") for o in outputs),
|
||||
UOp.special(256, "lidx0"), UOp.special(M//128, "gidx0"), UOp.special(N//64, "gidx1"),
|
||||
arg=KernelInfo(name, estimates=Estimates(ops=12*M*N, mem=mem)))
|
||||
src = (pathlib.Path(__file__).parent/"quantize_mxfp4.cpp").read_text()
|
||||
|
||||
@@ -5,9 +5,9 @@ from tinygrad.helpers import getenv, DEBUG
|
||||
|
||||
# https://github.com/facebookresearch/llama/blob/1076b9c51c77ad06e9d7ba8a4c6df775741732bd/llama/model.py#L47
|
||||
def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0) -> Tensor:
|
||||
freqs = 1.0 / (theta ** (Tensor.arange(0, dim, 2, dtype=dtypes.float32)[:(dim // 2)] / dim))
|
||||
freqs = Tensor.arange(end, dtype=dtypes.float32).unsqueeze(dim=1) * freqs.unsqueeze(dim=0)
|
||||
return Tensor.stack(freqs.cos(), freqs.sin(), dim=-1).cast(dtypes.default_float).reshape(1, end, 1, dim//2, 2)
|
||||
freqs = 1.0 / (theta ** (Tensor.arange(0, dim, 2)[:(dim // 2)] / dim))
|
||||
freqs = Tensor.arange(end).unsqueeze(dim=1) * freqs.unsqueeze(dim=0)
|
||||
return Tensor.stack(freqs.cos(), freqs.sin(), dim=-1).reshape(1, end, 1, dim//2, 2)
|
||||
|
||||
# matches meta, non hugging face weights
|
||||
# (a+i*b) * (c+i*d) = (ac-bd) + i*(ad+bc)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import os, subprocess, sys, shlex, pickle
|
||||
import os, subprocess, sys, shlex
|
||||
from pathlib import Path
|
||||
from tinygrad.helpers import temp, getenv
|
||||
|
||||
@@ -23,8 +23,5 @@ if __name__ == "__main__":
|
||||
# AM_RESET=1 gets a clear trace, does not work on mi300 machines
|
||||
subprocess.run([sys.executable, *shlex.split(test)], cwd=EXAMPLES_DIR.parent.parent.parent,
|
||||
env={**os.environ, "DEV":"AMD", "AM_RESET":"1" if not arch.startswith("gfx9") else "0", "VIZ":"-2", "PYTHONPATH":"."})
|
||||
with open(PROFILE_PATH, "rb") as f: events = pickle.load(f)
|
||||
with open(PROFILE_PATH, "wb") as f:
|
||||
pickle.dump([e for e in events if type(e).__name__ in {"ProfilePMCEvent", "ProfileSQTTEvent", "ProfileProgramEvent"}], f)
|
||||
PROFILE_PATH.rename(dest:=EXAMPLES_DIR/arch/f"profile_{name}_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.
+1
-1
@@ -192,7 +192,7 @@ def unpack_insts(viz_data, i:int, j:int, data:dict) -> dict:
|
||||
prev_instr = max(prev_instr, e.time + e.dur)
|
||||
summary = [{"label":"Total Cycles", "value":w.end_time-w.begin_time}, {"label":"SE", "value":w.se}, {"label":"CU", "value":w.cu},
|
||||
{"label":"SIMD", "value":w.simd}, {"label":"Wave ID", "value":w.wave_id}, {"label":"Run number", "value":data["run_number"]}]
|
||||
return {"rows":[tuple(v.values()) for v in rows.values()], "cols":columns, "metadata":[summary],"ref":viz_data.ref_map.get(data["prg"].profile_key)}
|
||||
return {"rows":[tuple(v.values()) for v in rows.values()], "cols":columns, "metadata":[summary], "ref":viz_data.ref_map.get(data["prg"].name)}
|
||||
|
||||
def print_data(data:dict) -> None:
|
||||
from tabulate import tabulate
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "tinygrad"
|
||||
version = "0.14.0"
|
||||
version = "0.13.0"
|
||||
description = "You like pytorch? You like micrograd? You love tinygrad! <3"
|
||||
authors = [{ name = "George Hotz" }]
|
||||
|
||||
@@ -84,7 +84,7 @@ testing = [
|
||||
"pillow",
|
||||
"onnx==1.19.0",
|
||||
"onnx2torch",
|
||||
"onnxruntime==1.24.1",
|
||||
"onnxruntime",
|
||||
"opencv-python",
|
||||
"transformers",
|
||||
"sentencepiece",
|
||||
|
||||
@@ -1002,39 +1002,6 @@ class TestBarrier(unittest.TestCase):
|
||||
for tid in range(64):
|
||||
self.assertEqual(st.vgpr[tid][0], tid + 100 + 1000, f"tid={tid}")
|
||||
|
||||
class TestSMaxMinSCCRegressions(unittest.TestCase):
|
||||
"""Regression test: S_MAX sets SCC only on strict inequality (equal operands -> SCC=0)."""
|
||||
|
||||
def test_s_max_i32_equal_scc(self):
|
||||
st = run_program([s_mov_b32(s[4], 64), s_mov_b32(s[5], 64), s_max_i32(s[6], s[4], s[5])], n_lanes=1)
|
||||
self.assertEqual(st.scc, 0)
|
||||
self.assertEqual(st.sgpr[6], 64)
|
||||
st = run_program([s_mov_b32(s[4], 65), s_mov_b32(s[5], 64), s_max_i32(s[6], s[4], s[5])], n_lanes=1)
|
||||
self.assertEqual(st.scc, 1) # still set when strictly greater
|
||||
|
||||
def test_s_max_u32_equal_scc(self):
|
||||
st = run_program([s_mov_b32(s[4], 64), s_mov_b32(s[5], 64), s_max_u32(s[6], s[4], s[5])], n_lanes=1)
|
||||
self.assertEqual(st.scc, 0)
|
||||
|
||||
class TestAbsdiffOverflowRegressions(unittest.TestCase):
|
||||
"""Regression test: S_ABSDIFF_I32 computes abs on the WRAPPED 32-bit difference (found by random difftest vs hardware)."""
|
||||
|
||||
def test_s_absdiff_wrapped(self):
|
||||
# |45 - (-2147483647)| overflows int32; hardware takes abs of the wrapped 32-bit difference
|
||||
instructions = [s_mov_b32(s[4], 45), s_mov_b32(s[5], 0x80000001), s_absdiff_i32(s[6], s[4], s[5])]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.sgpr[6], 0x7FFFFFD4)
|
||||
self.assertEqual(st.scc, 1)
|
||||
# INT_MIN - 1 wraps to +2147483647, already positive
|
||||
instructions = [s_mov_b32(s[4], 0x80000000), s_mov_b32(s[5], 1), s_absdiff_i32(s[6], s[4], s[5])]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.sgpr[6], 0x7FFFFFFF)
|
||||
# equality -> 0 and SCC=0
|
||||
instructions = [s_mov_b32(s[4], 7), s_mov_b32(s[5], 7), s_absdiff_i32(s[6], s[4], s[5])]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.sgpr[6], 0)
|
||||
self.assertEqual(st.scc, 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -1629,66 +1629,5 @@ class TestSwap(unittest.TestCase):
|
||||
self.assertEqual(st.vgpr[0][1], 0x55555555)
|
||||
|
||||
|
||||
class TestCvtFrexpRegressions(unittest.TestCase):
|
||||
"""Regression tests for float<->int conversion and FREXP corner cases (found by random difftest vs hardware)."""
|
||||
|
||||
def test_cvt_i32_f32_nan_is_zero(self):
|
||||
"""v_cvt_i32_f32 of NaN is 0, not INT_MIN (x86 cvttss2si returns INT_MIN)."""
|
||||
for nan in (0x7FC00000, 0xFFC00000, 0x7F800001):
|
||||
st = run_program([v_mov_b32_e32(v[0], nan), v_cvt_i32_f32_e32(v[1], v[0])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1], 0, f"nan=0x{nan:08x}")
|
||||
|
||||
def test_cvt_i32_f32_positive_overflow(self):
|
||||
"""v_cvt_i32_f32 saturates positive overflow/inf to INT_MAX, not INT_MIN."""
|
||||
for bits in (0x7F800000, 0x4F000000, 0x4F800000): # +inf, 2^31, ~2^32
|
||||
st = run_program([v_mov_b32_e32(v[0], bits), v_cvt_i32_f32_e32(v[1], v[0])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1], 0x7FFFFFFF, f"bits=0x{bits:08x}")
|
||||
|
||||
def test_cvt_i32_f32_negative_overflow(self):
|
||||
"""v_cvt_i32_f32 saturates negative overflow/-inf to INT_MIN."""
|
||||
for bits in (0xFF800000, 0xCF000001): # -inf, below -2^31
|
||||
st = run_program([v_mov_b32_e32(v[0], bits), v_cvt_i32_f32_e32(v[1], v[0])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1], 0x80000000, f"bits=0x{bits:08x}")
|
||||
|
||||
def test_cvt_u32_f32_nan_is_zero(self):
|
||||
"""v_cvt_u32_f32 of NaN is 0, not UINT_MAX."""
|
||||
for nan in (0x7FC00000, 0xFFC00000, 0x7F800001):
|
||||
st = run_program([v_mov_b32_e32(v[0], nan), v_cvt_u32_f32_e32(v[1], v[0])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1], 0, f"nan=0x{nan:08x}")
|
||||
|
||||
def test_cvt_i32_f64_nan_and_overflow(self):
|
||||
"""v_cvt_i32_f64: NaN -> 0, positive overflow/+inf -> INT_MAX."""
|
||||
st = run_program([v_mov_b32_e32(v[0], 0), v_mov_b32_e32(v[1], 0x7FF80000), v_cvt_i32_f64_e32(v[2], v[0:1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0)
|
||||
st = run_program([v_mov_b32_e32(v[0], 0), v_mov_b32_e32(v[1], 0x41F00000), v_cvt_i32_f64_e32(v[2], v[0:1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x7FFFFFFF) # 2^32 -> INT_MAX
|
||||
|
||||
def test_frexp_f32_denormal(self):
|
||||
"""v_frexp_exp/mant_f32 of denormal/zero inputs is (0, signed zero) on hardware."""
|
||||
for bits in (0x00000001, 0x007FFFFF, 0x00000000):
|
||||
st = run_program([v_mov_b32_e32(v[0], bits), v_frexp_exp_i32_f32_e32(v[1], v[0]), v_frexp_mant_f32_e32(v[2], v[0])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1] & 0xFFFFFFFF, 0, f"exp bits=0x{bits:08x}")
|
||||
self.assertEqual(st.vgpr[0][2], bits & 0x80000000, f"mant bits=0x{bits:08x}")
|
||||
# negative denormal: mant is -0.0
|
||||
st = run_program([v_mov_b32_e32(v[0], 0x80000001), v_frexp_mant_f32_e32(v[2], v[0])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x80000000)
|
||||
|
||||
def test_frexp_f64_denormal(self):
|
||||
"""v_frexp_exp_f64 of a denormal returns the normalized exponent (-1073 for min-denormal); zero -> 0."""
|
||||
st = run_program([v_mov_b32_e32(v[0], 1), v_mov_b32_e32(v[1], 0), v_frexp_exp_i32_f64_e32(v[2], v[0:1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2] & 0xFFFFFFFF, 0xFFFFFBCF) # -1073
|
||||
st = run_program([v_mov_b32_e32(v[0], 0), v_mov_b32_e32(v[1], 0), v_frexp_exp_i32_f64_e32(v[2], v[0:1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0)
|
||||
|
||||
def test_frexp_exp_inf_nan(self):
|
||||
"""v_frexp_exp of +/-inf and NaN is 0 on hardware (host frexp gives 129/1024), for both f32 and f64."""
|
||||
for bits in (0x7F800000, 0xFF800000, 0x7FC00000):
|
||||
st = run_program([v_mov_b32_e32(v[0], bits), v_frexp_exp_i32_f32_e32(v[1], v[0])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1] & 0xFFFFFFFF, 0, f"f32 bits=0x{bits:08x}")
|
||||
for lo, hi in ((0, 0x7FF00000), (0, 0xFFF00000), (0, 0x7FF80000), (1, 0x7FF00000)):
|
||||
st = run_program([v_mov_b32_e32(v[0], lo), v_mov_b32_e32(v[1], hi), v_frexp_exp_i32_f64_e32(v[2], v[0:1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2] & 0xFFFFFFFF, 0, f"f64 bits=0x{hi:08x}{lo:08x}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -989,53 +989,6 @@ class TestCarryOps(unittest.TestCase):
|
||||
self.assertEqual(st.vgpr[0][0], 0) # 0xFFFFFFFF + 1 + 0 = 0 (overflow)
|
||||
self.assertEqual(st.vcc, 0xDEADBEEF) # VCC unchanged - carry was discarded
|
||||
|
||||
class TestSelectFlushRegressions(unittest.TestCase):
|
||||
"""Regression tests: f32 MIN/MAX flush denormal inputs to signed zero (select-style ops propagate inputs bitwise)."""
|
||||
|
||||
def test_v_min_f32_denormal_flush(self):
|
||||
"""min(denormal, 1.0) is +0, min(-denormal, -1.0) is -0."""
|
||||
st = run_program([v_mov_b32_e32(v[0], 0x00000001), v_mov_b32_e32(v[1], 0x3F800000), v_min_f32_e32(v[2], v[0], v[1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x00000000)
|
||||
# flush(-denormal) = -0.0 > -1.0, so the result is -1.0 (both operand orders)
|
||||
st = run_program([v_mov_b32_e32(v[0], 0x80000001), v_mov_b32_e32(v[1], 0xBF800000), v_min_f32_e32(v[2], v[0], v[1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0xBF800000)
|
||||
st = run_program([v_mov_b32_e32(v[1], 0xBF800000), v_mov_b32_e32(v[2], 0x80000001), v_min_f32_e32(v[3], v[1], v[2])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][3], 0xBF800000)
|
||||
|
||||
def test_v_max_f32_denormal_flush(self):
|
||||
"""max(-denormal, -1.0) is -0; max(+denormal, -0) is +0."""
|
||||
st = run_program([v_mov_b32_e32(v[0], 0x80000001), v_mov_b32_e32(v[1], 0xBF800000), v_max_f32_e32(v[2], v[0], v[1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x80000000)
|
||||
st = run_program([v_mov_b32_e32(v[0], 0x00000001), v_mov_b32_e32(v[1], 0x80000000), v_max_f32_e32(v[2], v[0], v[1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x00000000)
|
||||
|
||||
|
||||
class TestCarryExecRegressions(unittest.TestCase):
|
||||
"""Regression tests: per-lane VCC writes (carry ops) zero inactive lane bits - VCC = mask & EXEC, never preserved."""
|
||||
|
||||
def test_co_ci_e32_vcc_masked_by_exec(self):
|
||||
"""v_sub_co_ci_u32_e32 with EXEC=0xFFFF0000: hw clears inactive VCC bits instead of preserving them."""
|
||||
instructions = [
|
||||
s_mov_b32(EXEC_LO, 0xFFFF0000),
|
||||
s_mov_b32(VCC_LO, 0xFFFFFFFF), # preset all bits
|
||||
v_mov_b32_e32(v[0], 0xFFFFFFFE), v_mov_b32_e32(v[1], 0x80000000),
|
||||
v_sub_co_ci_u32_e32(v[2], v[0], v[1]), # active lanes: no borrow
|
||||
]
|
||||
st = run_program(instructions, n_lanes=32)
|
||||
self.assertEqual(st.vcc, 0x00000000)
|
||||
|
||||
def test_co_ci_e32_vcc_masked_by_exec_ones(self):
|
||||
"""Same with all-ones carry: VCC = borrow_mask & EXEC."""
|
||||
instructions = [
|
||||
s_mov_b32(EXEC_LO, 0x0F0F0F0F),
|
||||
s_mov_b32(VCC_LO, 0),
|
||||
v_mov_b32_e32(v[0], 0xFFFFFFFF), v_mov_b32_e32(v[1], 1),
|
||||
v_add_co_ci_u32_e32(v[2], v[0], v[1]), # all lanes would carry if active
|
||||
]
|
||||
st = run_program(instructions, n_lanes=32)
|
||||
self.assertEqual(st.vcc, 0x0F0F0F0F)
|
||||
self.assertEqual(st.vgpr[31][2], 0) # 0xFFFFFFFF + 1 wraps to 0 in active lanes
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -4,7 +4,6 @@ Includes: v_fma_f32, v_div_scale_f32, v_div_fmas_f32, v_div_fixup_f32,
|
||||
v_alignbit_b32, v_bfe_i32, v_mad_u64_u32, v_readlane_b32, v_writelane_b32
|
||||
"""
|
||||
import unittest
|
||||
from tinygrad.helpers import OSX
|
||||
from test.amd.hw.helpers import *
|
||||
|
||||
class TestFMA(unittest.TestCase):
|
||||
@@ -3265,23 +3264,6 @@ class TestVOP3ClampMAD(unittest.TestCase):
|
||||
# 0xFFFF * 2 = 0x1FFFE, low 16 bits = 0xFFFE
|
||||
self.assertEqual(st.vgpr[0][3] & 0xFFFF, 0xFFFE, f"expected 0xFFFE, got 0x{st.vgpr[0][3] & 0xFFFF:04x}")
|
||||
|
||||
class TestMadNarrowClampRegressions(unittest.TestCase):
|
||||
"""Regression tests: mad i16/i24 with clamp saturate to narrow output range (found by random difftest vs hardware)."""
|
||||
|
||||
def test_mad_i16_clamp_sat_max(self):
|
||||
# neg/src-floggled 16-bit mul operands are sign-extended after toggling bit15; sum > INT_MAX saturates
|
||||
instructions = [s_mov_b32(s[4], 1232348160), v_mov_b32_e32(v[3], 0x80000000),
|
||||
v_mov_b32_e32(v[1], 0x7F7FFFFF), v_mad_i32_i16(v[0], s[4], v[3], v[1], 0, 3, 5, 1)]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0x7FFFFFFF)
|
||||
|
||||
def test_mad_i24_clamp_sat_min(self):
|
||||
# sext24(-6344704) * sext24(+4210688) << -2^31 saturates to INT_MIN
|
||||
instructions = [s_mov_b32(s[7], 4290772992), v_mov_b32_e32(v[1], 1077936128),
|
||||
v_mad_i32_i24(v[0], s[7], v[1], v[1], 1, 0, 0, 1)]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0x80000000)
|
||||
|
||||
|
||||
class TestCvtPkF16(unittest.TestCase):
|
||||
"""Tests for V_CVT_PK_RTZ_F16_F32 - pack two f32 to f16 with round toward zero."""
|
||||
@@ -3669,80 +3651,6 @@ class TestPermlane(unittest.TestCase):
|
||||
self.assertEqual(st.vgpr[21][1], 5)
|
||||
self.assertEqual(st.vgpr[31][1], 15)
|
||||
|
||||
class TestClampLdExpRegressions(unittest.TestCase):
|
||||
"""Regression tests for f32 clamp (-0 -> +0) and ldexp input passthrough."""
|
||||
|
||||
def test_clamp_negative_zero(self):
|
||||
"""clmp=1 maps -0.0 to +0.0 (found by random difftest vs hardware)."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 0x80000000), v_mov_b32_e32(v[1], 0x80000000),
|
||||
v_add_f32_e64(v[2], v[0], v[1], clmp=1), # -0 + -0 = -0, clamp -> +0
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x00000000)
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 0x3F800000), v_mov_b32_e32(v[1], 0x80000000),
|
||||
v_min_f32_e64(v[2], v[0], v[1], clmp=1), # min(1.0, -0) = -0, clamp -> +0
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x00000000)
|
||||
|
||||
def test_ldexp_special_inputs(self):
|
||||
"""v_ldexp_f32 of 0/-0/inf/NaN propagates the input instead of computing val * 2**exp (0*inf = NaN on host)."""
|
||||
# -0.0 * 2^INT_MIN = -0.0 (src1 as integer exponent; huge negative)
|
||||
instructions = [v_mov_b32_e32(v[0], 0x80000000), v_mov_b32_e32(v[1], 0x80000000), v_ldexp_f32(v[2], v[0], v[1])]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x80000000)
|
||||
# inf stays inf even with negative exponent
|
||||
instructions = [v_mov_b32_e32(v[0], 0x7F800000), v_mov_b32_e32(v[1], 0xFFFFFF80), v_ldexp_f32(v[2], v[0], v[1])]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x7F800000)
|
||||
|
||||
def test_ldexp_denormal_flush(self):
|
||||
"""v_ldexp_f32/f64 flush denormal inputs to signed zero (found by random difftest vs hardware)."""
|
||||
# ldexp(+denorm, 1) = +0, ldexp(-denorm, 250) = -0
|
||||
for src, exp_val, want in [(0x00000001, 1, 0x00000000), (0x80000001, 250, 0x80000000)]:
|
||||
st = run_program([v_mov_b32_e32(v[0], src), v_mov_b32_e32(v[1], exp_val), v_ldexp_f32(v[2], v[0], v[1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], want)
|
||||
|
||||
def test_v_mul_neg_modifier_nan_sign(self):
|
||||
"""neg modifier is a pure sign-bit toggle on a NaN operand; result keeps that sign (found by random difftest)."""
|
||||
# mul(normal, NEG(ABS(qNaN))): NaN payload negated in the operand stays negative qNaN
|
||||
instructions = [v_mov_b32_e32(v[0], 0xC96CF47F), v_mov_b32_e32(v[1], 0x7FC00000),
|
||||
v_mul_f32_e64(v[2], v[0], v[1], s[0], 0, 7, 6)]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0xFFC00000)
|
||||
# plain neg modifier still applies to non-NaN values: mul(-1.0, NEG(2.0)) = +2.0
|
||||
st = run_program([v_mov_b32_e32(v[0], 0xBF800000), v_mov_b32_e32(v[1], 0x40000000),
|
||||
v_mul_f32_e64(v[2], v[0], v[1], s[0], 0, 2, 0)], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x40000000)
|
||||
|
||||
|
||||
class TestNaNPropagationRegressions(unittest.TestCase):
|
||||
"""Regression tests: float arithmetic propagates a NaN from the FIRST NaN operand, quieted with its own sign/payload."""
|
||||
|
||||
@unittest.skipIf(OSX, "broken on mac, TODO: why?")
|
||||
def test_mul_nan_priority(self):
|
||||
# first NaN operand wins (sign+payload), not x86's second-source propagation
|
||||
for a, b, want in [(0x7FC00001, 0x7F800003, 0x7FC00001), (0xFFC00005, 0x7F800003, 0xFFC00005),
|
||||
(0x7F800001, 0xFFC00005, 0x7FC00001), (0xFF9F1800, 0x7F800001, 0xFFDF1800)]:
|
||||
st = run_program([v_mov_b32_e32(v[0], a), v_mov_b32_e32(v[1], b),
|
||||
v_mul_f32_e32(v[2], v[0], v[1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], want, f"mul({a:#x}, {b:#x})")
|
||||
|
||||
class TestMinMaxFlushE64Regressions(unittest.TestCase):
|
||||
"""Regression tests: f32 min/max/median flush denormal inputs to signed zero (e64 forms)."""
|
||||
|
||||
def test_v_min3_f32_denormal_flush(self):
|
||||
st = run_program([v_mov_b32_e32(v[0], 0x00000001), v_mov_b32_e32(v[1], 0x3F800000), v_mov_b32_e32(v[2], 0x40000000),
|
||||
v_min3_f32(v[3], v[0], v[1], v[2])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][3], 0x00000000) # min(+denorm, 1, 2) = +0
|
||||
|
||||
def test_v_med3_f32_denormal_flush(self):
|
||||
st = run_program([v_mov_b32_e32(v[0], 0x80000001), v_mov_b32_e32(v[1], 0x3F800000), v_mov_b32_e32(v[2], 0x40000000),
|
||||
v_med3_f32(v[3], v[0], v[1], v[2])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][3], 0x3F800000) # med(-0, 1, 2) = 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -973,71 +973,6 @@ class TestCmpxPartialWavefront(unittest.TestCase):
|
||||
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0xFFFFFFFF, 0x4,
|
||||
"Only lane 2 should be active after v_cmpx_eq_u32_e64")
|
||||
|
||||
class TestClassDenormalRegressions(unittest.TestCase):
|
||||
"""Regression tests: V_CMP_CLASS classifies denormals as DENORMAL (raw bits), not as zero class."""
|
||||
|
||||
def test_class_pos_denormal(self):
|
||||
for bits in (0x00000001, 0x007FFFFF):
|
||||
instructions = [v_mov_b32_e32(v[0], bits), v_mov_b32_e32(v[1], 0x80), v_cmp_class_f32_e64(VCC_LO, v[0], v[1])]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc, 1, f"bits=0x{bits:08x}") # n_lanes=1
|
||||
# ...and it is not the zero class
|
||||
instructions = [v_mov_b32_e32(v[0], bits), v_mov_b32_e32(v[1], 0x40), v_cmp_class_f32_e64(VCC_LO, v[0], v[1])]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc, 0, f"bits=0x{bits:08x}")
|
||||
|
||||
def test_class_neg_denormal(self):
|
||||
instructions = [v_mov_b32_e32(v[0], 0x80000001), v_mov_b32_e32(v[1], 0x10), v_cmp_class_f32_e64(VCC_LO, v[0], v[1])]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc, 1) # n_lanes=1
|
||||
instructions = [v_mov_b32_e32(v[0], 0x80000001), v_mov_b32_e32(v[1], 0x20), v_cmp_class_f32_e64(VCC_LO, v[0], v[1])]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc, 0) # not the negative-zero class
|
||||
|
||||
|
||||
class TestIntCmpModRegressions(unittest.TestCase):
|
||||
"""Regression tests: int compares (i32/u32) honor abs/neg as bit-level sign clear/flip (not integer abs/negate)."""
|
||||
|
||||
def test_cmp_i32_abs_neg_bit_level(self):
|
||||
# abs(0x80000001) = 1 -> 1 > 1 is false (integer abs would give 2147483647 > 1)
|
||||
instructions = [v_mov_b32_e32(v[0], 0x80000001), v_mov_b32_e32(v[1], 1), v_cmp_gt_i32_e64(VCC_LO, v[0], v[1], abs=1)]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc, 0)
|
||||
# neg(0x80000001) flips the sign bit -> 1 > 2 is false (integer negate would give 2147483647 > 2)
|
||||
instructions = [v_mov_b32_e32(v[0], 0x80000001), v_mov_b32_e32(v[1], 2), v_cmp_gt_i32_e64(VCC_LO, v[0], v[1], neg=1)]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc, 0)
|
||||
|
||||
def test_cmp_u32_abs_bit_level(self):
|
||||
# abs(0x80000000) = 0 -> 0 < 1 is true
|
||||
instructions = [v_mov_b32_e32(v[0], 0x80000000), v_mov_b32_e32(v[1], 1), v_cmp_lt_u32_e64(VCC_LO, v[0], v[1], abs=1)]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc, 1) # n_lanes=1
|
||||
|
||||
|
||||
class TestCmpxSdstRegressions(unittest.TestCase):
|
||||
"""Regression tests: V_CMPX_*_E64 writes EXEC only, never SDST (hardware verified)."""
|
||||
|
||||
def test_cmpx_e64_no_sdst(self):
|
||||
instructions = [
|
||||
s_mov_b32(VCC_LO, 0), # preset VCC to 0
|
||||
v_mov_b32_e32(v[0], 0x3F800000), v_mov_b32_e32(v[1], 0x40000000),
|
||||
v_cmpx_lt_f32_e64(VCC_LO, v[0], v[1]), # 1.0 < 2.0
|
||||
]
|
||||
st = run_program(instructions, n_lanes=32)
|
||||
self.assertEqual(st.sgpr[EXEC_LO.offset], 0xFFFFFFFF) # EXEC updated
|
||||
self.assertEqual(st.vcc, 0) # but VCC untouched
|
||||
|
||||
def test_cmpx_e64_partial_exec(self):
|
||||
instructions = [
|
||||
s_mov_b32(EXEC_LO, 0x0F0F0F0F),
|
||||
s_mov_b32(VCC_LO, 0xFFFFFFFF),
|
||||
v_mov_b32_e32(v[0], 0), v_mov_b32_e32(v[1], 0x3F800000),
|
||||
v_cmpx_lt_f32_e64(VCC_LO, v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=32)
|
||||
self.assertEqual(st.sgpr[EXEC_LO.offset], 0x0F0F0F0F) # EXEC = computed & old EXEC
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -88,6 +88,7 @@ def run_rocprof_decoder(blobs: list[bytes], lib: bytes, base: int, target: str):
|
||||
if t.is_alive(): raise RuntimeError("rocprof decoder timeout")
|
||||
return occupancy_records, wave_insts
|
||||
|
||||
@unittest.skip("TODO: fix to not require unpickling UOps.")
|
||||
class SQTTExamplesTestBase(unittest.TestCase):
|
||||
target: str
|
||||
examples: dict
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
import unittest, contextlib
|
||||
from tinygrad import Device, Tensor, Context, TinyJit
|
||||
from tinygrad.device import Compiled, ProfileProgramEvent
|
||||
from tinygrad.device import Compiled, ProfileProgramEvent, ProfileDeviceEvent
|
||||
from tinygrad.engine.realize import run_linear
|
||||
from tinygrad.codegen import to_program
|
||||
from tinygrad.viz.serve import load_amd_counters, VizData
|
||||
|
||||
@contextlib.contextmanager
|
||||
def save_sqtt():
|
||||
Device[Device.DEFAULT].synchronize()
|
||||
profile_start = len(Compiled.profile_events)
|
||||
data = VizData()
|
||||
yield data.ctxs
|
||||
Device[Device.DEFAULT].synchronize()
|
||||
Device[Device.DEFAULT]._at_profile_finalize()
|
||||
load_amd_counters(data, [e for e in Compiled.profile_events[:profile_start] if isinstance(e, ProfileProgramEvent)] +
|
||||
Compiled.profile_events[profile_start:])
|
||||
load_amd_counters(data, Compiled.profile_events)
|
||||
data.ctxs[:] = [r for r in data.ctxs if r["name"].startswith("SQTT")]
|
||||
|
||||
@unittest.skipUnless(Device.DEFAULT == "AMD", "only runs on AMD")
|
||||
class TestSQTTProfiler(unittest.TestCase):
|
||||
# TODO: can we enable SQTT profiling in context?
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if not Device[Device.DEFAULT].sqtt_enabled: raise unittest.SkipTest("device must be in SQTT profiling mode")
|
||||
|
||||
def setUp(self):
|
||||
Device[Device.DEFAULT].synchronize()
|
||||
Compiled.profile_events[:] = [e for e in Compiled.profile_events if isinstance(e, (ProfileProgramEvent, ProfileDeviceEvent))]
|
||||
|
||||
def test_simple(self):
|
||||
t = Tensor.empty(1) + 1
|
||||
with save_sqtt() as sqtt:
|
||||
|
||||
@@ -188,7 +188,7 @@ class TestMXFP4(unittest.TestCase):
|
||||
M, N, K = getenv("M", 16384), getenv("N", 4096), getenv("K", 14336)
|
||||
a = Tensor.empty(M, K, dtype=dtypes.bfloat16)
|
||||
b = Tensor.empty(N, K, dtype=dtypes.bfloat16)
|
||||
for _ in range(getenv("CNT", 1)): asm_gemm(a, b.T, mxfp4=True).realize()
|
||||
asm_gemm(a, b.T, mxfp4=True).realize()
|
||||
|
||||
# test the Asm GEMM with Llama shapes, only run on the real machine for speed
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from tinygrad.renderer.isa.x86 import X86Renderer, X86Ops
|
||||
from tinygrad.renderer.isa import IselContext
|
||||
|
||||
# INDEX on a register value with a constant index extracts a single element (the old GEP)
|
||||
def lane(y:UOp, i:int) -> UOp: return y.index(UOp.cconst(i, dtypes.int))
|
||||
def lane(y:UOp, i:int) -> UOp: return y.index(UOp.cconst(i, dtypes.int), dtype=y.dtype)
|
||||
|
||||
@unittest.skipUnless(isinstance(Device[Device.DEFAULT].renderer, X86Renderer), "only x86")
|
||||
class TestIselX86(unittest.TestCase):
|
||||
|
||||
@@ -9,7 +9,7 @@ from extra.llama_kernels.swiglu import swiglu
|
||||
from extra.models.llama import apply_rotary_emb, precompute_freqs_cis
|
||||
from extra.thunder.amd.fa import custom_fused_qkv_rope_backward, fused_qkv_rope
|
||||
from test.helpers import needs_second_gpu, assert_kernel_count
|
||||
from test.backend.test_asm_gemm import has_hipcc, is_cdna4
|
||||
from test.backend.test_asm_gemm import has_hipcc
|
||||
|
||||
def run_fused_ce(bs:int, seqlen:int, vocab:int, label_smoothing:float=0.0) -> None:
|
||||
Tensor.manual_seed(0)
|
||||
@@ -129,7 +129,7 @@ class TestFusedQKVRoPE(unittest.TestCase):
|
||||
self.assertTrue(k.allclose(k_ref, atol=2e-2, rtol=0).item(), "K forward mismatch")
|
||||
self.assertTrue(v.allclose(v_ref, atol=0, rtol=0).item(), "V forward mismatch")
|
||||
|
||||
@unittest.skipUnless(has_hipcc() and is_cdna4(), "backward kernel requires hipcc to compile")
|
||||
@unittest.skipUnless(has_hipcc(), "backward kernel requires hipcc to compile")
|
||||
def test_llama31_8b(self):
|
||||
Tensor.manual_seed(1)
|
||||
B, N, H, H_KV, D = self.SHAPE
|
||||
|
||||
@@ -3,7 +3,7 @@ from tinygrad import Tensor, Device, nn, GlobalCounters, TinyJit, dtypes, Variab
|
||||
from tinygrad.uop.ops import Ops, UOp, AxisType, graph_rewrite
|
||||
from tinygrad.helpers import getenv, prod, Context
|
||||
from tinygrad.nn.state import get_parameters
|
||||
from tinygrad.engine.realize import run_linear, compile_linear, lower_and_compile, pm_beam
|
||||
from tinygrad.engine.realize import run_linear, compile_linear, pm_beam, pm_compile
|
||||
import numpy as np
|
||||
from hypothesis import given, strategies as strat, settings
|
||||
from test.helpers import not_support_multi_device, needs_second_gpu, slow, call_is_graph, check_schedule, assert_kernel_count, KernelCountException
|
||||
@@ -58,11 +58,6 @@ class TestMultiTensor(unittest.TestCase):
|
||||
assert X.uop.ended_ranges == X.uop.src[1:]
|
||||
(X + X).realize()
|
||||
|
||||
def test_shard_invalids_contiguous(self):
|
||||
# every store is Invalid, so none of them should become a (empty) kernel
|
||||
t = Tensor.invalids(8).shard(devices_2, axis=0).contiguous()
|
||||
self.assertEqual(len([c for c in t.schedule_linear().src if c.src[0].op is Ops.SINK]), 1)
|
||||
|
||||
@unittest.expectedFailure # TODO: fix
|
||||
def test_shard_empty(self):
|
||||
GlobalCounters.reset()
|
||||
@@ -85,7 +80,7 @@ class TestMultiTensor(unittest.TestCase):
|
||||
cpu_2 = ("CPU:1", "CPU:2")
|
||||
src = Tensor.ones(16).shard(cpu_2, 0).realize()
|
||||
lin = UOp(Ops.LINEAR, src=(src.to(cpu_2[::-1]).schedule_linear().src[0],))
|
||||
with Context(BEAM=1, IGNORE_BEAM_CACHE=1): call = lower_and_compile(graph_rewrite(lin, pm_beam, ctx=1, walk=True)).src[0]
|
||||
with Context(BEAM=1, IGNORE_BEAM_CACHE=1): call = graph_rewrite(graph_rewrite(lin, pm_beam, ctx=1, walk=True), pm_compile, walk=True).src[0]
|
||||
self.assertNotEqual(call.src[0].src[0].arg.applied_opts, ())
|
||||
|
||||
def test_shard_same_device(self):
|
||||
|
||||
@@ -6,7 +6,6 @@ from tinygrad.helpers import getenv, DEBUG, DEV, IMAGE, Context
|
||||
from tinygrad import Tensor, Device, dtypes
|
||||
from tinygrad.tensor import _to_np_dtype
|
||||
from tinygrad.renderer.nir import NIRRenderer
|
||||
from tinygrad.renderer.isa.x86 import X86Renderer
|
||||
|
||||
TINY_BACKEND = getenv("TINY_BACKEND")
|
||||
if TINY_BACKEND:
|
||||
@@ -809,8 +808,6 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op([], lambda: tor^0x1337, lambda: ten^0x1337, forward_only=True)
|
||||
helper_test_op([], lambda: 0x1337^tor, lambda: 0x1337^ten, forward_only=True)
|
||||
|
||||
# TODO: x86 PARAM dtype fails SPEC=2
|
||||
@Context(SPEC=1 if isinstance(Device[Device.DEFAULT].renderer, X86Renderer) else 2)
|
||||
def test_and(self):
|
||||
data = [[1,-8,1],[32,1,6]]
|
||||
tor = torch.tensor(data, dtype=torch.int)
|
||||
@@ -868,9 +865,9 @@ class TestOps(unittest.TestCase):
|
||||
lambda: (ten << Tensor([0,2,4], dtype=dtypes.uint32)).cast(dtypes.int32), forward_only=True)
|
||||
helper_test_op([], lambda: tor.__lshift__(2), lambda: ten.__lshift__(2).cast(dtypes.int32), forward_only=True)
|
||||
helper_test_op([], lambda: tor.bitwise_left_shift(2), lambda: ten.lshift(2).cast(dtypes.int32), forward_only=True)
|
||||
self.helper_test_exception([], lambda: torch.tensor([1.0]) << 2, lambda: (Tensor([1.0]) << 2).realize(), expected=RuntimeError)
|
||||
self.helper_test_exception([], lambda: tor << torch.tensor([1.0]), lambda: (ten << Tensor([1.0])).realize(), expected=RuntimeError)
|
||||
self.helper_test_exception([], lambda: tor << 1.0, lambda: (ten << 1.0).realize(), expected=RuntimeError)
|
||||
self.helper_test_exception([], lambda: torch.tensor([1.0]) << 2, lambda: Tensor([1.0]) << 2, expected=RuntimeError)
|
||||
self.helper_test_exception([], lambda: tor << torch.tensor([1.0]), lambda: ten << Tensor([1.0]), expected=RuntimeError)
|
||||
self.helper_test_exception([], lambda: tor << 1.0, lambda: ten << 1.0, expected=RuntimeError)
|
||||
|
||||
def test_rshift(self):
|
||||
data = [[0,1,2],[1<<8,1<<16,1<<31-1]]
|
||||
@@ -884,8 +881,8 @@ class TestOps(unittest.TestCase):
|
||||
lambda: (ten >> Tensor([0,2,4], dtype=dtypes.uint32)).cast(dtypes.int32), forward_only=True)
|
||||
helper_test_op([], lambda: tor.__rshift__(2), lambda: ten.__rshift__(2).cast(dtypes.int32), forward_only=True)
|
||||
helper_test_op([], lambda: tor.bitwise_right_shift(2), lambda: ten.rshift(2).cast(dtypes.int32), forward_only=True)
|
||||
self.helper_test_exception([], lambda: torch.tensor([4.0]) >> 1, lambda: (Tensor([4.0]) >> 1).realize(), expected=RuntimeError)
|
||||
self.helper_test_exception([], lambda: tor >> torch.tensor([1.0]), lambda: (ten >> Tensor([1.0])).realize(), expected=RuntimeError)
|
||||
self.helper_test_exception([], lambda: torch.tensor([4.0]) >> 1, lambda: Tensor([4.0]) >> 1, expected=RuntimeError)
|
||||
self.helper_test_exception([], lambda: tor >> torch.tensor([1.0]), lambda: ten >> Tensor([1.0]), expected=RuntimeError)
|
||||
|
||||
def test_lshift_signed(self):
|
||||
data = [[-1, -3, 1, 7], [0, -2147483648, 2147483647, -1]]
|
||||
@@ -1810,10 +1807,9 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op([()], lambda x: torch.nn.functional.hardtanh(x, -val, val), lambda x: x.hardtanh(-val, val), grad_atol=1e-6)
|
||||
def test_asinh(self):
|
||||
helper_test_op([(45,65)], lambda x: x.asinh(), grad_atol=1e-6)
|
||||
helper_test_op([(45,65)], lambda x: x.asinh(), grad_atol=1e-6, low=-300, high=-297)
|
||||
# TODO: this one has larger tol?
|
||||
helper_test_op([(45,65)], lambda x: x.asinh(), atol=1e-2, rtol=2e-2, grad_rtol=2e-2, low=-300, high=-297)
|
||||
helper_test_op([(45,65)], lambda x: x.asinh(), grad_atol=1e-6, low=300, high=303)
|
||||
helper_test_op([(45,65)], lambda x: x.asinh(), grad_atol=1e-6, low=-1e10, high=-1e9)
|
||||
helper_test_op(None, lambda x: x.asinh(), grad_atol=1e-6, vals=[[-1.0, 0.0, 1.0]])
|
||||
def test_acosh(self):
|
||||
helper_test_op([(45,65)], lambda x: x.acosh(), grad_atol=1e-6)
|
||||
helper_test_op([(45,65)], lambda x: x.acosh(), grad_atol=1e-3, grad_rtol=1e-2, low=-300, high=-297)
|
||||
|
||||
@@ -87,8 +87,7 @@ class TestOptim(unittest.TestCase):
|
||||
def test_muon(self): self._test_muon(1, {'lr': 0.001}, 1e-3, 0)
|
||||
# TODO: disabled due to big atol
|
||||
# def test_muon_high_lr(self): self._test_muon(1, {'lr': 10}, 1e-6, 3e-4)
|
||||
# NOTE: big weight_decay so a missing wd would be way over atol
|
||||
def test_muon_wd(self): self._test_muon(1, {'lr': 0.001, 'weight_decay': 10}, 1e-3, 3e-4)
|
||||
def test_muon_wd(self): self._test_muon(1, {'lr': 0.001, 'weight_decay': 0.01}, 1e-3, 3e-4)
|
||||
# TODO: disabled due to big atol
|
||||
# def test_muon_high_lr_wd(self): self._test_muon(1, {'lr': 10, 'weight_decay': 0.01}, 1e-6, 5e-4)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import unittest
|
||||
from tinygrad import Tensor, Device, dtypes, Variable
|
||||
from tinygrad.helpers import Context, GlobalCounters, getenv, DEBUG
|
||||
from tinygrad import Tensor, nn, Device, dtypes, Variable
|
||||
from tinygrad.helpers import Context, GlobalCounters, getenv, PCONTIG, DEBUG
|
||||
from tinygrad.uop.ops import graph_rewrite, PatternMatcher, UPat, Ops, UOp
|
||||
from tinygrad.codegen.opt import OptOps, Opt
|
||||
from tinygrad.renderer.ptx import PTXRenderer
|
||||
@@ -14,7 +14,7 @@ class TestDoubleMatmul(unittest.TestCase):
|
||||
self.ref = (self.a @ self.b @ self.c).realize()
|
||||
|
||||
def _test(self, opts):
|
||||
with Context(DEBUG=max(2, DEBUG.value)):
|
||||
with Context(PCONTIG=2, DEBUG=max(2, DEBUG.value)):
|
||||
out = (self.a @ self.b @ self.c).contiguous(arg=opts).realize()
|
||||
|
||||
with Context(DEBUG=0):
|
||||
@@ -88,15 +88,16 @@ class TestRangeifyEdgeCase(unittest.TestCase):
|
||||
res = Tensor.cat(a, c, dim=0)
|
||||
self.assertEqual(res.numpy()[-1, :16].tolist(), [512] * 16)
|
||||
|
||||
def test_multi_gather(self):
|
||||
def test_pcontig_multi_gather(self):
|
||||
# regression test: local bufferize must have device set for const_like to work
|
||||
# NOTE: with uint type, this will become a long and fail on WEBGPU
|
||||
forest = Tensor(list(range(8)), dtype='int')
|
||||
idx = Tensor([0, 0], dtype='int')
|
||||
node_val = forest.gather(0, idx)
|
||||
idx2 = idx * 2 + 1
|
||||
node_val2 = forest.gather(0, idx2)
|
||||
result = (node_val + node_val2).numpy()
|
||||
with Context(PCONTIG=2):
|
||||
# NOTE: with uint type, this will become a long and fail on WEBGPU
|
||||
forest = Tensor(list(range(8)), dtype='int')
|
||||
idx = Tensor([0, 0], dtype='int')
|
||||
node_val = forest.gather(0, idx)
|
||||
idx2 = idx * 2 + 1
|
||||
node_val2 = forest.gather(0, idx2)
|
||||
result = (node_val + node_val2).numpy()
|
||||
self.assertEqual(result.tolist(), [1, 1])
|
||||
|
||||
if getenv("BIG") > 2:
|
||||
@@ -117,6 +118,65 @@ def fa():
|
||||
GlobalCounters.reset()
|
||||
return q.scaled_dot_product_attention(k, v)
|
||||
|
||||
def fa_bw():
|
||||
Tensor.manual_seed(1337)
|
||||
with Context(DEBUG=0):
|
||||
q,k,v = [Tensor.rand(BS, HEADS, SEQLEN, EMB).contiguous().realize() for _ in range(3)]
|
||||
attn_output = nn.Linear(HEADS*EMB, HEADS*EMB, bias=False)
|
||||
attn_output.weight.realize()
|
||||
target = Tensor.rand(BS, SEQLEN, HEADS*EMB).contiguous().realize()
|
||||
|
||||
GlobalCounters.reset()
|
||||
attn = q.scaled_dot_product_attention(k, v).contiguous().contiguous_backward()
|
||||
attn = attn.transpose(1, 2).reshape(BS, SEQLEN, -1)
|
||||
out = attn_output(attn)
|
||||
loss = (out - target).square().mean()
|
||||
loss.backward()
|
||||
#ret = [out, Tensor.stack(q.grad, k.grad, v.grad, dim=-1)]
|
||||
#ret = [out, Tensor.stack(q.grad, k.grad, dim=-1), v.grad]
|
||||
ret = [out, q.grad, k.grad, v.grad]
|
||||
Tensor.realize(*ret)
|
||||
return ret
|
||||
|
||||
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, (NIRRenderer, PTXRenderer)), "broken in LVP and PTX")
|
||||
class TestPcontig(unittest.TestCase):
|
||||
def test_flash_attention_bw(self):
|
||||
with Context(PCONTIG=max(2, PCONTIG.value), DEBUG=2):
|
||||
grads = fa_bw()
|
||||
print(f"{GlobalCounters.global_ops/1e9:.2f} GFLOPS")
|
||||
|
||||
with Context(PCONTIG=0, DEBUG=2):
|
||||
cmp_grads = fa_bw()
|
||||
print(f"{GlobalCounters.global_ops/1e9:.2f} GFLOPS")
|
||||
|
||||
with Context(DEBUG=0):
|
||||
mses = [((x-y)**2).sum().item() for x,y in zip(grads, cmp_grads)]
|
||||
mse = sum(mses)
|
||||
print(f"mse: {mse}")
|
||||
self.assertLessEqual(mse, 1e-6)
|
||||
|
||||
def test_flash_attention(self, opts=None):
|
||||
with Context(PCONTIG=2, DEBUG=max(2, DEBUG.value)):
|
||||
ret = fa().realize() if opts is None else fa().contiguous(arg=opts).realize()
|
||||
print(f"{GlobalCounters.global_ops/1e9:.2f} GFLOPS")
|
||||
with Context(DEBUG=2):
|
||||
cmp = fa().realize()
|
||||
print(f"{GlobalCounters.global_ops/1e9:.2f} GFLOPS")
|
||||
with Context(DEBUG=0):
|
||||
mse = ((cmp-ret)**2).sum().item()
|
||||
print(f"mse: {mse}")
|
||||
self.assertLessEqual(mse, 1e-6)
|
||||
|
||||
def test_flash_attention_opt(self):
|
||||
opts = ()
|
||||
# columns in top matrix
|
||||
opts += (Opt(OptOps.UPCAST, 0, 4),)
|
||||
# columns in bottom matrix
|
||||
opts += (Opt(OptOps.UPCAST, 3, 4),)
|
||||
# rows in all the matrix
|
||||
opts += (Opt(OptOps.UPCAST, 4, 4),)
|
||||
self.test_flash_attention(opts)
|
||||
|
||||
# contiguous + reduce can support ranges?
|
||||
|
||||
@unittest.skip("pm_rangeify no longer exists. test this in a different way")
|
||||
|
||||
@@ -77,14 +77,6 @@ class TestCStyleFailures(unittest.TestCase):
|
||||
|
||||
@unittest.skipUnless(isinstance(Device[Device.DEFAULT].renderer, WGSLRenderer), "tests for wgsl renderer")
|
||||
class TestWGSLFailures(unittest.TestCase):
|
||||
def test_folded_packed_store(self):
|
||||
b = UOp.param(0, dtypes.char, (4,))
|
||||
idx = b.index(UOp.const(0).cast(dtypes.int))
|
||||
store = UOp.store(idx, idx.cast(dtypes.uint32).load() & UOp.const(0xffffff00).cast(dtypes.uint32))
|
||||
src = Device[Device.DEFAULT].renderer.render(UOp.sink(store, arg=KernelInfo()).toposort())
|
||||
self.assertIn("atomicAnd(&data0_4[0],4294967040u);", src)
|
||||
self.assertNotIn("atomicAdd", src)
|
||||
|
||||
def test_multiply_infinity(self):
|
||||
# multiplying a positive constant by infinity should return infinity
|
||||
# WGSL pipelines do not handle this reliably, some of which return zero, unless infinity always comes from a read on a dynamic buffer
|
||||
|
||||
@@ -147,7 +147,7 @@ class TestSchedule(unittest.TestCase):
|
||||
devs = ("CPU:0", "CPU:1")
|
||||
x = Tensor.ones(2, device="CPU").shard(devs, axis=0).realize()
|
||||
out = (x.sum()*2).reshape(1).to("CPU")
|
||||
run_linear(*check_schedule(out, 3))
|
||||
run_linear(*check_schedule(out, 5))
|
||||
np.testing.assert_equal(out.numpy(), [4.])
|
||||
|
||||
class TestLimitBufs(unittest.TestCase):
|
||||
@@ -365,6 +365,16 @@ class TestCopyFolding(unittest.TestCase):
|
||||
b = a.to("CPU")
|
||||
self.assertListEqual(b.tolist(), [2.])
|
||||
|
||||
def test_copy_to_same_device(self):
|
||||
a = Tensor.empty(4).uop
|
||||
b = a.copy_to_device(a.device)
|
||||
check_schedule(b, 1, filter_sink=False) # TODO: 0?
|
||||
|
||||
def test_copy_to_same_device_alt(self):
|
||||
a = Tensor.empty(4, 4).uop
|
||||
b = a.copy_to_device(a.device)
|
||||
check_schedule(b, 1, filter_sink=False) # TODO: 0?
|
||||
|
||||
def test_copy_to_same_device_sched(self):
|
||||
a = Tensor.ones(4).contiguous().realize().uop.buf_uop
|
||||
t = Tensor(a.copy_to_device(a.device))
|
||||
@@ -374,6 +384,14 @@ class TestCopyFolding(unittest.TestCase):
|
||||
assert t.uop.is_realized, f"didn't realize Tensor {t}"
|
||||
self.assertListEqual(t.tolist(), [1.,1.,1.,1.])
|
||||
|
||||
@unittest.skip("same-device copies are no-ops")
|
||||
def test_self_assign_same_device_copy(self):
|
||||
a = Tensor.ones(4, 4).contiguous().realize()
|
||||
# use copy_to_device to bypass Tensor.to() shortcircuit and force a real same-device COPY in the graph
|
||||
a.assign(Tensor(a.uop.copy_to_device(a.device), a.device))
|
||||
run_linear(*check_schedule(a, 2, filter_sink=False))
|
||||
self.assertListEqual(a.tolist(), [[1.]*4]*4)
|
||||
|
||||
def test_clone(self):
|
||||
a = Tensor.empty(4)
|
||||
check_schedule(a.clone(), 1, filter_sink=False)
|
||||
|
||||
@@ -301,14 +301,6 @@ class TestSetitem(unittest.TestCase):
|
||||
self.assertListEqual(z[2:5].tolist(), [2, 2, 2])
|
||||
self.assertListEqual(z[6:7].tolist(), [3])
|
||||
|
||||
class TestAssignBitcast(unittest.TestCase):
|
||||
def test_assign_through_bitcast(self):
|
||||
# the dest is unrealized, so callify cannot fold the BITCAST into a buffer view and the STORE keeps a
|
||||
# BITCAST dest; the bitcast has to move to the value side or the store never reaches the buffer
|
||||
a = Tensor.full((4,), 1.0, dtype=dtypes.float32).contiguous()
|
||||
a.bitcast(dtypes.uint32).assign(Tensor([0x40800000, 0x40400000, 0x40000000, 0x3f800000], dtype=dtypes.uint32)).realize()
|
||||
np.testing.assert_allclose(a.numpy(), [4.0, 3.0, 2.0, 1.0])
|
||||
|
||||
class TestWithGrad(unittest.TestCase):
|
||||
def test_basic_setitem_works(self):
|
||||
z = Tensor.rand(8, 8)
|
||||
|
||||
+10
-40
@@ -2,7 +2,7 @@ from typing import Optional, Any
|
||||
import unittest, math
|
||||
import numpy as np
|
||||
from tinygrad.tensor import Tensor, _to_np_dtype
|
||||
from tinygrad.helpers import Context, ceildiv
|
||||
from tinygrad.helpers import Context
|
||||
from tinygrad.dtype import dtypes, DType, AddrSpace, ConstFloat # noqa: F401
|
||||
from tinygrad.device import Buffer, Device
|
||||
from tinygrad.uop.ops import Ops, UOp, KernelInfo, AxisType, buffers
|
||||
@@ -57,35 +57,6 @@ def _test_uops_result(output_dtype, uops, res):
|
||||
run_uops([out], [buf])
|
||||
return np.frombuffer(buf.as_memoryview(), _to_np_dtype(output_dtype))[0]
|
||||
|
||||
@unittest.skipUnless(isinstance(Device[Device.DEFAULT].renderer, CStyleLanguage) and
|
||||
dtypes.uint64 in Device[Device.DEFAULT].renderer.supported_dtypes(), "requires C-style pointer bitcast and 64-bit ints")
|
||||
class TestBitcastBufferView(unittest.TestCase):
|
||||
@Context(SPEC=2)
|
||||
def test_render(self):
|
||||
buf = UOp.param(0, dtypes.uint32, (4,))
|
||||
uops = to_uops_list([buf.shrink(((1, 3),)).bitcast(dtypes.uint64).index(0).store(1)], ren=Device[Device.DEFAULT].renderer)
|
||||
idx = next(u for u in uops if u.op is Ops.INDEX and u.src[0].op is Ops.BITCAST)
|
||||
self.assertEqual(idx.src[0].src[0].op, Ops.SHRINK)
|
||||
Device[Device.DEFAULT].renderer.render(uops)
|
||||
|
||||
@Context(SPEC=2)
|
||||
def test_load(self):
|
||||
val = 0x1122334455667788
|
||||
src, out = UOp.param(0, dtypes.uint32, (4,)), UOp.param(1, dtypes.uint64, (1,))
|
||||
ibuf = Buffer(Device.DEFAULT, 4, dtypes.uint32, initial_value=np.array([0, 0x55667788, 0x11223344, 0], dtype=np.uint32).tobytes())
|
||||
obuf = Buffer(Device.DEFAULT, 1, dtypes.uint64).allocate()
|
||||
run_uops([out.index(0).store(src.shrink(((1, 3),)).bitcast(dtypes.uint64).index(0))], [ibuf, obuf])
|
||||
self.assertEqual(np.frombuffer(obuf.as_memoryview(), dtype=np.uint64)[0], val)
|
||||
|
||||
@Context(SPEC=2)
|
||||
def test_store(self):
|
||||
val = 0x1122334455667788
|
||||
dst = UOp.param(0, dtypes.uint32, (6,))
|
||||
buf = Buffer(Device.DEFAULT, 6, dtypes.uint32, initial_value=bytes(24))
|
||||
view = dst.shrink(((1, 5),)).bitcast(dtypes.uint64) # two stores through one view: it must inline, not get a declared vector-pointer
|
||||
run_uops([view.index(0).store(val ^ 0xff), view.index(1).store(val)], [buf])
|
||||
self.assertEqual(np.frombuffer(buf.as_memoryview(), dtype=np.uint64, count=2, offset=4).tolist(), [val ^ 0xff, val])
|
||||
|
||||
class TestUOps(unittest.TestCase):
|
||||
def _equal(self, v1, v2):
|
||||
assert isinstance(v2, (float, int, bool))
|
||||
@@ -222,16 +193,15 @@ class TestLocalAccess(unittest.TestCase):
|
||||
@unittest.skipUnless(Device.DEFAULT == "WEBGPU", "Test local memory size for packed data types")
|
||||
def test_packed_smem_size(self):
|
||||
_dtypes = [dtypes.char, dtypes.uchar, dtypes.short, dtypes.ushort, dtypes.half]
|
||||
# a partial word still needs a whole word, so sizes that don't fill one must round up
|
||||
for size in (16, 5):
|
||||
for dtype in _dtypes:
|
||||
temp = UOp.placeholder((size,), dtype, slot=0, addrspace=AddrSpace.LOCAL)
|
||||
uops = to_uops_list([temp], ren=Device[Device.DEFAULT].renderer)
|
||||
out = Device[Device.DEFAULT].renderer.render(uops)
|
||||
# half is supported in wgsl, so it doesn't have to be packed
|
||||
corrected_size = ceildiv(size, 4//dtype.itemsize) if dtype != dtypes.half else size
|
||||
# temp0: array<{Device[Device.DEFAULT].renderer.buf_map(dtype)},{corrected_size}>;
|
||||
self.assertIn(f",{corrected_size}>;", out)
|
||||
size = 16
|
||||
for dtype in _dtypes:
|
||||
temp = UOp.placeholder((size,), dtype, slot=0, addrspace=AddrSpace.LOCAL)
|
||||
uops = to_uops_list([temp], ren=Device[Device.DEFAULT].renderer)
|
||||
out = Device[Device.DEFAULT].renderer.render(uops)
|
||||
# half is supported in wgsl, so it doesn't have to be packed
|
||||
corrected_size = size//(4//dtype.itemsize) if dtype != dtypes.half else size
|
||||
# temp0: array<{Device[Device.DEFAULT].renderer.buf_map(dtype)},{corrected_size}>;
|
||||
self.assertIn(f",{corrected_size}>;", out)
|
||||
|
||||
@unittest.skipUnless(Device[Device.DEFAULT].renderer.has_shared, "test requires shared memory")
|
||||
@unittest.skip("tinygrad doesn't support this behavior")
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ class TestGPUCrash(unittest.TestCase):
|
||||
cls.is_cdna = cls.dev.target[0] < 10
|
||||
ins = importlib.import_module('tinygrad.runtime.autogen.amd.' + ('cdna' if cls.is_cdna else 'rdna3') + '.ins')
|
||||
for rdna3_name, cdna3_name in RDNA3_CDNA3_MAP.items():
|
||||
setattr(cls, rdna3_name, staticmethod(getattr(ins, cdna3_name if cls.is_cdna else rdna3_name)))
|
||||
setattr(cls, rdna3_name, getattr(ins, cdna3_name if cls.is_cdna else rdna3_name))
|
||||
|
||||
def setUp(self):
|
||||
# Verify device works before each test
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import ctypes, mmap, collections, functools, copy, os
|
||||
import pathlib, re, ctypes, mmap, collections, functools, copy, os
|
||||
from tinygrad.runtime.autogen import kfd, amdgpu_drm, libc
|
||||
import tinygrad.runtime.autogen.am.am as am
|
||||
from tinygrad.helpers import from_mv
|
||||
from test.mockgpu.driver import VirtDriver, VirtFileDesc, TextFileDesc, DirFileDesc, VirtFile
|
||||
from test.mockgpu.amd.amdgpu import AMDGPU, gpu_props, GFX_TARGET_VERSION, MOCKGPU_ARCH
|
||||
|
||||
def _ioctl_nr(ioctl: functools.partial) -> int: return ioctl.args[2]
|
||||
|
||||
kfd_ioctl_info = {
|
||||
_ioctl_nr(ioctl): (name, ioctl.args[3]) for name, ioctl in vars(kfd).items()
|
||||
if name.startswith("AMDKFD_IOC_") and isinstance(ioctl, functools.partial)}
|
||||
def ioctls_from_header():
|
||||
# hdrpy = (pathlib.Path(__file__).parent.parent.parent.parent / "tinygrad" / "runtime" / "autogen" / "kfd.py").read_text()
|
||||
# pattern = r'# (AMDKFD_IOC_[A-Z0-9_]+)\s=\s_(IOW?R?).*\(( 0x[0-9a-fA-F]+) ,\s+struct\s([A-Za-z0-9_]+)\s+\)'
|
||||
# matches = re.findall(pattern, hdrpy, re.MULTILINE)
|
||||
hdr = (pathlib.Path(__file__).parent.parent.parent.parent / "extra" / "hip_gpu_driver" / "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_]+)\)'
|
||||
matches = re.findall(pattern, hdr, re.MULTILINE)
|
||||
return type("KFD_IOCTLS", (object, ), {name: int(nr, 0x10) for name, _, nr, _ in matches}), \
|
||||
{int(nr, 0x10): getattr(kfd, "struct_"+sname, None) for name, idir, nr, sname in matches}
|
||||
kfd_ioctls, kfd_headers = ioctls_from_header()
|
||||
|
||||
class KFDFileDesc(VirtFileDesc):
|
||||
def __init__(self, fd, driver):
|
||||
@@ -111,44 +116,42 @@ class AMDDriver(VirtDriver):
|
||||
|
||||
def kfd_ioctl(self, req, argp):
|
||||
nr = req & 0xFF
|
||||
if nr not in kfd_ioctl_info: raise RuntimeError(f"unknown kfd ioctl, {nr} unknown")
|
||||
name, struct_type = kfd_ioctl_info[nr]
|
||||
struct = struct_type.from_address(argp)
|
||||
struct = kfd_headers[nr].from_address(argp)
|
||||
|
||||
if nr == _ioctl_nr(kfd.AMDKFD_IOC_ACQUIRE_VM): pass
|
||||
elif nr == _ioctl_nr(kfd.AMDKFD_IOC_RUNTIME_ENABLE): pass
|
||||
elif nr == _ioctl_nr(kfd.AMDKFD_IOC_GET_VERSION):
|
||||
if nr == kfd_ioctls.AMDKFD_IOC_ACQUIRE_VM: pass
|
||||
elif nr == kfd_ioctls.AMDKFD_IOC_RUNTIME_ENABLE: pass
|
||||
elif nr == kfd_ioctls.AMDKFD_IOC_GET_VERSION:
|
||||
struct.major_version = 1
|
||||
struct.minor_version = 14
|
||||
elif nr == _ioctl_nr(kfd.AMDKFD_IOC_ALLOC_MEMORY_OF_GPU):
|
||||
elif nr == kfd_ioctls.AMDKFD_IOC_ALLOC_MEMORY_OF_GPU:
|
||||
if struct.gpu_id not in self.gpus: return -1
|
||||
struct.handle = self._alloc_handle()
|
||||
self.object_by_handle[struct.handle] = copy.deepcopy(struct) # save memory struct to know what mem it is
|
||||
# Track signal memory (uncached + coherent) - progress queues when written to
|
||||
if struct.flags & kfd.KFD_IOC_ALLOC_MEM_FLAGS_UNCACHED:
|
||||
self.track_address(struct.va_addr, struct.va_addr + struct.size, lambda mv,off: None, lambda mv, off: self._emulate_execute())
|
||||
elif nr == _ioctl_nr(kfd.AMDKFD_IOC_FREE_MEMORY_OF_GPU):
|
||||
elif nr == kfd_ioctls.AMDKFD_IOC_FREE_MEMORY_OF_GPU:
|
||||
self.object_by_handle.pop(struct.handle)
|
||||
elif nr == _ioctl_nr(kfd.AMDKFD_IOC_MAP_MEMORY_TO_GPU):
|
||||
elif nr == kfd_ioctls.AMDKFD_IOC_MAP_MEMORY_TO_GPU:
|
||||
dev_ids = (ctypes.c_int32 * struct.n_devices).from_address(struct.device_ids_array_ptr)
|
||||
for i in range(struct.n_devices):
|
||||
gpu = self.gpus[dev_ids[i]]
|
||||
mem_obj = self.object_by_handle[struct.handle]
|
||||
gpu.map_range(mem_obj.va_addr, mem_obj.size)
|
||||
struct.n_success = i + 1
|
||||
elif nr == _ioctl_nr(kfd.AMDKFD_IOC_UNMAP_MEMORY_FROM_GPU):
|
||||
elif nr == kfd_ioctls.AMDKFD_IOC_UNMAP_MEMORY_FROM_GPU:
|
||||
dev_ids = (ctypes.c_int32 * struct.n_devices).from_address(struct.device_ids_array_ptr)
|
||||
for i in range(struct.n_devices):
|
||||
gpu = self.gpus[dev_ids[i]]
|
||||
mem_obj = self.object_by_handle[struct.handle]
|
||||
gpu.unmap_range(mem_obj.va_addr, mem_obj.size)
|
||||
struct.n_success = i + 1
|
||||
elif nr == _ioctl_nr(kfd.AMDKFD_IOC_CREATE_EVENT):
|
||||
elif nr == kfd_ioctls.AMDKFD_IOC_CREATE_EVENT:
|
||||
struct.event_slot_index = self._alloc_next_event_slot()
|
||||
struct.event_id = struct.event_slot_index
|
||||
|
||||
if struct.event_type == kfd.KFD_IOC_EVENT_MEMORY: self.mmu_event_ids.append(struct.event_id)
|
||||
elif nr == _ioctl_nr(kfd.AMDKFD_IOC_CREATE_QUEUE):
|
||||
elif nr == kfd_ioctls.AMDKFD_IOC_CREATE_QUEUE:
|
||||
gpu = self.gpus[struct.gpu_id]
|
||||
if struct.queue_type == kfd.KFD_IOC_QUEUE_TYPE_SDMA:
|
||||
gpu.add_sdma_queue(struct.ring_base_address, struct.ring_size, struct.read_pointer_address, struct.write_pointer_address)
|
||||
@@ -159,7 +162,7 @@ class AMDDriver(VirtDriver):
|
||||
# Track writes to doorbell, calling callback
|
||||
struct.doorbell_offset = self._alloc_doorbell(struct.gpu_id)
|
||||
self.track_address(struct.doorbell_offset, struct.doorbell_offset + 8, lambda mv,off: None, lambda mv, off: self._emulate_execute())
|
||||
elif nr == _ioctl_nr(kfd.AMDKFD_IOC_WAIT_EVENTS):
|
||||
elif nr == kfd_ioctls.AMDKFD_IOC_WAIT_EVENTS:
|
||||
evs = (kfd.struct_kfd_event_data * struct.num_events).from_address(struct.events_ptr)
|
||||
for ev in evs:
|
||||
if ev.event_id in self.mmu_event_ids and "MOCKGPU_EMU_FAULTADDR" in os.environ:
|
||||
@@ -167,7 +170,11 @@ class AMDDriver(VirtDriver):
|
||||
ev.memory_exception_data.va = int(os.environ["MOCKGPU_EMU_FAULTADDR"], 16)
|
||||
ev.memory_exception_data.failure.NotPresent = 1
|
||||
else:
|
||||
raise RuntimeError(f"unsupported kfd ioctl, {nr} {name}")
|
||||
name = "unknown"
|
||||
for k,v in kfd_ioctls.__dict__.items():
|
||||
if nr == v: name = k
|
||||
assert False, f"unknown kfd ioctl, {nr} {name}"
|
||||
exit(1)
|
||||
return 0
|
||||
|
||||
def _emulate_execute(self):
|
||||
|
||||
+745
-432
File diff suppressed because it is too large
Load Diff
+48
-120
@@ -1,20 +1,5 @@
|
||||
# Tokenizer-based expression parser for AMD pcode
|
||||
import ast, itertools, operator, re
|
||||
from typing import Any, Callable
|
||||
|
||||
_BINOPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.FloorDiv: operator.floordiv,
|
||||
ast.Mod: operator.mod, ast.LShift: operator.lshift, ast.RShift: operator.rshift,
|
||||
ast.BitAnd: operator.and_, ast.BitOr: operator.or_, ast.BitXor: operator.xor}
|
||||
def _const_int(expr: str) -> int:
|
||||
"""Evaluate a compile-time integer expression (integer literals and basic arithmetic only)."""
|
||||
def ev(node: ast.AST) -> int:
|
||||
if isinstance(node, ast.Expression): return ev(node.body)
|
||||
if isinstance(node, ast.Constant) and isinstance(node.value, int): return node.value
|
||||
if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.USub, ast.UAdd)):
|
||||
return (-1 if isinstance(node.op, ast.USub) else 1) * ev(node.operand)
|
||||
if isinstance(node, ast.BinOp) and type(node.op) in _BINOPS: return _BINOPS[type(node.op)](ev(node.left), ev(node.right))
|
||||
raise ValueError(f"not a constant integer expression: {expr!r}")
|
||||
return ev(ast.parse(expr.strip(), mode='eval'))
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.uop.ops import Ops, UOp
|
||||
from tinygrad.codegen.decomp.dtype import f2f
|
||||
@@ -23,7 +8,6 @@ from tinygrad.codegen.decomp.dtype import f2f
|
||||
VarVal = UOp | tuple[str, list[str], str]
|
||||
|
||||
def _const(dt, v): return UOp.const(v, dt)
|
||||
def _single_value(v: UOp): return v.vmin if v.vmin == v.vmax else None
|
||||
def _u32(v): return _const(dtypes.uint32, v)
|
||||
def _u64(v): return _const(dtypes.uint64, v)
|
||||
def _to_u32(v): return v if v.dtype == dtypes.uint32 else v.bitcast(dtypes.uint32) if v.dtype.itemsize == 4 else v.cast(dtypes.uint32)
|
||||
@@ -71,8 +55,8 @@ def _expr_bits(v: UOp) -> int:
|
||||
if v.op in (Ops.AND, Ops.XOR):
|
||||
widths: list[int] = []
|
||||
for src in v.src:
|
||||
if isinstance(sv:=_single_value(src), int) and sv > 0 and (sv & (sv + 1)) == 0:
|
||||
widths.append(sv.bit_length())
|
||||
if src.op == Ops.CONST and isinstance(src.val, int) and src.val > 0 and (src.val & (src.val + 1)) == 0:
|
||||
widths.append(src.val.bit_length())
|
||||
if widths: return max(widths)
|
||||
return v.dtype.bitsize
|
||||
|
||||
@@ -160,9 +144,9 @@ def _minmax_reduce(is_max: bool, dt, *args: UOp) -> UOp:
|
||||
def _find_two_pi_mul(x):
|
||||
if x.op != Ops.MUL or len(x.src) != 2: return None
|
||||
for i, s in enumerate(x.src):
|
||||
if (sv:=_single_value(s)) is not None and abs(sv - 6.283185307179586) < 1e-5: return (x.src[1-i], 6.283185307179586)
|
||||
if s.op == Ops.CONST and abs(s.val - 6.283185307179586) < 1e-5: return (x.src[1-i], 6.283185307179586)
|
||||
if s.op == Ops.MUL and len(s.src) == 2:
|
||||
vals = [sv for ss in s.src if (sv:=_single_value(ss)) is not None]
|
||||
vals = [ss.val for ss in s.src if ss.op == Ops.CONST] + [ss.src[0].val for ss in s.src if ss.op == Ops.CAST and ss.src[0].op == Ops.CONST]
|
||||
if len(vals) == 2 and abs(vals[0] * vals[1] - 6.283185307179586) < 1e-5: return (x.src[1-i], vals[0] * vals[1])
|
||||
return None
|
||||
|
||||
@@ -179,7 +163,7 @@ def _trig_reduce(x, phase=0.0):
|
||||
|
||||
def _signext(val: UOp) -> UOp:
|
||||
for bits, mask, ext in [(4, 0xF, 0xFFFFFFF0), (8, 0xFF, 0xFFFFFF00), (16, 0xFFFF, 0xFFFF0000)]:
|
||||
if (val.op == Ops.AND and len(val.src) == 2 and _single_value(val.src[1]) == mask) or val.dtype.itemsize == bits // 8:
|
||||
if (val.op == Ops.AND and len(val.src) == 2 and val.src[1].op == Ops.CONST and val.src[1].val == mask) or val.dtype.itemsize == bits // 8:
|
||||
v32 = val.cast(dtypes.uint32) if val.dtype != dtypes.uint32 else val
|
||||
sb = (v32 >> _u32(bits - 1)) & _u32(1)
|
||||
return sb.ne(_u32(0)).where(v32 | _u32(ext), v32).cast(dtypes.int)
|
||||
@@ -201,20 +185,7 @@ def _abs(val: UOp) -> UOp:
|
||||
def _f_to_u(f, dt):
|
||||
clamped = (f < _const(f.dtype, 0.0)).where(_const(f.dtype, 0.0), f)
|
||||
truncated = UOp(Ops.TRUNC, src=(clamped,))
|
||||
res = (truncated >= _const(f.dtype, 2**(dt.itemsize*8))).where(_const(dt, dt.max), truncated.cast(dt))
|
||||
return _isnan(f).where(_const(dt, 0), res) # float->uint conversion of NaN is 0 on hardware
|
||||
|
||||
def _f_to_i32(a: UOp) -> UOp:
|
||||
"""v_cvt_i32_f32/f64: truncate toward zero, saturate to [INT_MIN, INT_MAX], NaN -> 0.
|
||||
(x86 cvttss2si returns 0x80000000 for all of these, which matches hardware only for negative overflow.)"""
|
||||
res = (a >= _const(a.dtype, 2147483648.0)).where(_const(dtypes.int, 0x7FFFFFFF), UOp(Ops.TRUNC, src=(a,)).cast(dtypes.int))
|
||||
return _isnan(a).where(_const(dtypes.int, 0), res)
|
||||
|
||||
def _ftz_f32(v: UOp) -> UOp:
|
||||
"""Flush f32 denormals to signed zero (RDNA default float mode flushes denormal f32 inputs on select-style ops)."""
|
||||
bits = v.bitcast(dtypes.uint32) if v.dtype == dtypes.float32 else v
|
||||
return ((bits & _u32(0x7FFFFFFF)) < _u32(0x00800000)).where((bits & _u32(0x80000000)).bitcast(dtypes.float32),
|
||||
v if v.dtype == dtypes.float32 else v.bitcast(dtypes.float32))
|
||||
return (truncated >= _const(f.dtype, 2**(dt.itemsize*8))).where(_const(dt, dt.max), truncated.cast(dt))
|
||||
|
||||
def _cvt_quiet(val: UOp) -> UOp:
|
||||
bits, _, _, qb, _ = _float_info(val)
|
||||
@@ -259,51 +230,18 @@ def _ldexp(val: UOp, exp: UOp) -> UOp:
|
||||
if val.dtype == dtypes.uint32: val = val.bitcast(dtypes.float32)
|
||||
elif val.dtype == dtypes.uint64: val = val.bitcast(dtypes.float64)
|
||||
if exp.dtype in (dtypes.uint32, dtypes.uint64): exp = exp.cast(dtypes.int if exp.dtype == dtypes.uint32 else dtypes.int64)
|
||||
bits = val.bitcast(dtypes.uint32) if val.dtype == dtypes.float32 else val.bitcast(dtypes.uint64)
|
||||
abs_max = _const(bits.dtype, 0x7F800000 if val.dtype == dtypes.float32 else 0x7FF0000000000000)
|
||||
sign_mask = _const(bits.dtype, 0x80000000 if val.dtype == dtypes.float32 else 0x8000000000000000)
|
||||
# hardware flushes denormal inputs to signed zero
|
||||
magn_mask = _const(bits.dtype, 0x7FFFFFFF if val.dtype == dtypes.float32 else 0x7FFFFFFFFFFFFFFF)
|
||||
is_denorm = ((bits & abs_max).eq(_const(bits.dtype, 0))) & ((bits & magn_mask).ne(_const(bits.dtype, 0)))
|
||||
val = is_denorm.where((bits & sign_mask).bitcast(val.dtype), val)
|
||||
# hardware propagates 0/+-inf/NaN unchanged (avoids 0*inf = NaN on the host)
|
||||
res = val * UOp(Ops.EXP2, src=(exp.cast(val.dtype),))
|
||||
is_special = (bits & abs_max).eq(_const(bits.dtype, 0)) | ((bits & abs_max) >= abs_max)
|
||||
return is_special.where(val, res)
|
||||
return val * UOp(Ops.EXP2, src=(exp.cast(val.dtype),))
|
||||
|
||||
def _frexp_mant(val: UOp) -> UOp:
|
||||
val = val.bitcast(dtypes.float32) if val.dtype == dtypes.uint32 else val.bitcast(dtypes.float64) if val.dtype == dtypes.uint64 else val
|
||||
if val.dtype == dtypes.float32:
|
||||
bits = val.bitcast(dtypes.uint32)
|
||||
# denormal/zero inputs (exponent field == 0) return signed zero on hardware
|
||||
return ((bits & _u32(0x7F800000)).ne(_u32(0))).where(((bits & _u32(0x807FFFFF)) | _u32(0x3F000000)).bitcast(dtypes.float32),
|
||||
(bits & _u32(0x80000000)).bitcast(dtypes.float32))
|
||||
bits = val.bitcast(dtypes.uint64)
|
||||
return ((bits & _const(dtypes.uint64, 0x7FF0000000000000)).ne(_const(dtypes.uint64, 0))).where(
|
||||
((bits & _const(dtypes.uint64, 0x800FFFFFFFFFFFFF)) | _const(dtypes.uint64, 0x3fe0000000000000)).bitcast(dtypes.float64),
|
||||
(bits & _const(dtypes.uint64, 0x8000000000000000)).bitcast(dtypes.float64))
|
||||
|
||||
def _msb(val: UOp, bits: int) -> UOp:
|
||||
"""Index of the highest set bit, or -1 if val == 0."""
|
||||
dt = dtypes.uint64 if bits > 32 else dtypes.uint32
|
||||
val = val.cast(dt) if val.dtype != dt else val
|
||||
result = _const(dtypes.int, -1)
|
||||
for i in range(bits - 1, -1, -1):
|
||||
cond = ((val >> _const(dt, i)) & _const(dt, 1)).ne(_const(dt, 0)) & result.eq(_const(dtypes.int, -1))
|
||||
result = cond.where(_const(dtypes.int, i), result)
|
||||
return result
|
||||
if val.dtype == dtypes.float32: return ((val.bitcast(dtypes.uint32) & _u32(0x807FFFFF)) | _u32(0x3f000000)).bitcast(dtypes.float32)
|
||||
return ((val.bitcast(dtypes.uint64) & _const(dtypes.uint64, 0x800FFFFFFFFFFFFF)) |
|
||||
_const(dtypes.uint64, 0x3fe0000000000000)).bitcast(dtypes.float64)
|
||||
|
||||
def _frexp_exp(val: UOp) -> UOp:
|
||||
val = val.bitcast(dtypes.float32) if val.dtype == dtypes.uint32 else val.bitcast(dtypes.float64) if val.dtype == dtypes.uint64 else val
|
||||
if val.dtype == dtypes.float32:
|
||||
e = (val.bitcast(dtypes.uint32) >> _u32(23)) & _u32(0xFF)
|
||||
return e.ne(_u32(0)).where(e.cast(dtypes.int) - _const(dtypes.int, 126), _const(dtypes.int, 0)) # f32 denormals -> 0 (hardware verified)
|
||||
bits = val.bitcast(dtypes.uint64)
|
||||
e = (bits >> _const(dtypes.uint64, 52)) & _const(dtypes.uint64, 0x7FF)
|
||||
mant = bits & _const(dtypes.uint64, 0xFFFFFFFFFFFFF)
|
||||
# f64 denormals: normalized exponent = highest set mantissa bit - 1073, zero -> 0 (hardware verified)
|
||||
denorm = mant.ne(_const(dtypes.uint64, 0)).where(_msb(mant, 52) - _const(dtypes.int, 1073), _const(dtypes.int, 0))
|
||||
return e.ne(_const(dtypes.uint64, 0)).where(e.cast(dtypes.int) - _const(dtypes.int, 1022), denorm)
|
||||
if val.dtype == dtypes.float32: return ((val.bitcast(dtypes.uint32) >> _u32(23)) & _u32(0xFF)).cast(dtypes.int) - _const(dtypes.int, 126)
|
||||
return ((val.bitcast(dtypes.uint64) >> _const(dtypes.uint64, 52)) & _const(dtypes.uint64, 0x7FF)).cast(dtypes.int) - _const(dtypes.int, 1022)
|
||||
|
||||
TWO_OVER_PI = int(
|
||||
"0145f306dc9c882a53f84eafa3ea69bb81b6c52b3278872083fca2c757bd778ac36e48dc74849ba5c00c925dd413a32439fc3bd"
|
||||
@@ -361,9 +299,9 @@ _FUNCS: dict[str, Callable[..., UOp]] = {
|
||||
'fma': lambda a, b, c: a * b + c,
|
||||
'i32_to_f32': lambda a: a.cast(dtypes.int).cast(dtypes.float32),
|
||||
'u32_to_f32': lambda a: a.cast(dtypes.uint32).cast(dtypes.float32),
|
||||
'f32_to_i32': lambda a: _f_to_i32(a.bitcast(dtypes.float32)),
|
||||
'f32_to_i32': lambda a: UOp(Ops.TRUNC, src=(a.bitcast(dtypes.float32),)).cast(dtypes.int),
|
||||
'f32_to_u32': lambda a: _f_to_u(a.bitcast(dtypes.float32), dtypes.uint32),
|
||||
'f64_to_i32': lambda a: _f_to_i32(a.bitcast(dtypes.float64)),
|
||||
'f64_to_i32': lambda a: UOp(Ops.TRUNC, src=(a.bitcast(dtypes.float64),)).cast(dtypes.int),
|
||||
'f64_to_u32': lambda a: _f_to_u(a.bitcast(dtypes.float64), dtypes.uint32),
|
||||
'f16_to_f32': lambda a: _f16_extract(a).cast(dtypes.float32),
|
||||
'f32_to_f16': lambda a: a.cast(dtypes.half),
|
||||
@@ -422,13 +360,22 @@ _FUNCS: dict[str, Callable[..., UOp]] = {
|
||||
'fp8_to_f32': _fp8_to_f32, 'bf8_to_f32': _bf8_to_f32, 'f32_to_fp8': _f32_to_fp8, 'f32_to_bf8': _f32_to_bf8,
|
||||
'f32_to_bf16': _f32_to_bf16, 'f32_to_bf16_SR': _f32_to_bf16_sr, 'f32_to_bf16_sr': _f32_to_bf16_sr,
|
||||
}
|
||||
# min/max family: min/max + 3-input (x3), IEEE num variants (f16/f32 only), and long names minimum/maximum (f16/f32 only)
|
||||
for is_max, name, full in [(False, 'min', 'minimum'), (True, 'max', 'maximum')]:
|
||||
for dt, sfx, pre in [(dtypes.float32, 'f32', None), (dtypes.int, 'i32', None), (dtypes.uint32, 'u32', None),
|
||||
(dtypes.int16, 'i16', None), (dtypes.uint16, 'u16', None), (dtypes.half, 'f16', _f16_extract)]:
|
||||
def mm(*a, im=is_max, d=dt, p=pre): return _minmax_reduce(im, d, *(a if p is None else [p(x) for x in a]))
|
||||
extra = (f'v_{name}_num_{sfx}', f'v_{name}3_num_{sfx}', f'v_{full}_{sfx}', f'v_{full}3_{sfx}') if dt in (dtypes.float32, dtypes.half) else ()
|
||||
for fn in (f'v_{name}_{sfx}', f'v_{name}3_{sfx}', *extra): _FUNCS[fn] = mm
|
||||
for is_max, name in [(False, 'min'), (True, 'max')]:
|
||||
for dt, sfx in [(dtypes.float32, 'f32'), (dtypes.int, 'i32'), (dtypes.uint32, 'u32'), (dtypes.int16, 'i16'), (dtypes.uint16, 'u16')]:
|
||||
_FUNCS[f'v_{name}_{sfx}'] = lambda *a, im=is_max, d=dt: _minmax_reduce(im, d, *a)
|
||||
_FUNCS[f'v_{name}3_{sfx}'] = lambda *a, im=is_max, d=dt: _minmax_reduce(im, d, *a)
|
||||
# f16 min/max/min3/max3/med3
|
||||
for is_max, name in [(False, 'min'), (True, 'max')]:
|
||||
_FUNCS[f'v_{name}_f16'] = lambda *a, im=is_max: _minmax_reduce(im, dtypes.half, *[_f16_extract(x) for x in a])
|
||||
_FUNCS[f'v_{name}3_f16'] = lambda *a, im=is_max: _minmax_reduce(im, dtypes.half, *[_f16_extract(x) for x in a])
|
||||
_FUNCS[f'v_{name}_num_f16'] = lambda *a, im=is_max: _minmax_reduce(im, dtypes.half, *[_f16_extract(x) for x in a])
|
||||
_FUNCS[f'v_{name}_num_f32'] = lambda *a, im=is_max: _minmax_reduce(im, dtypes.float32, *a)
|
||||
_FUNCS[f'v_{name}3_num_f16'] = lambda *a, im=is_max: _minmax_reduce(im, dtypes.half, *[_f16_extract(x) for x in a])
|
||||
_FUNCS[f'v_{name}3_num_f32'] = lambda *a, im=is_max: _minmax_reduce(im, dtypes.float32, *a)
|
||||
_FUNCS[f'v_{name}imum_f16'] = lambda *a, im=is_max: _minmax_reduce(im, dtypes.half, *[_f16_extract(x) for x in a])
|
||||
_FUNCS[f'v_{name}imum_f32'] = lambda *a, im=is_max: _minmax_reduce(im, dtypes.float32, *a)
|
||||
_FUNCS[f'v_{name}imum3_f16'] = lambda *a, im=is_max: _minmax_reduce(im, dtypes.half, *[_f16_extract(x) for x in a])
|
||||
_FUNCS[f'v_{name}imum3_f32'] = lambda *a, im=is_max: _minmax_reduce(im, dtypes.float32, *a)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# TOKENIZER/PARSER
|
||||
@@ -550,7 +497,7 @@ class Parser:
|
||||
if not dtypes.is_int(right.dtype): right = right.cast(dtypes.uint32)
|
||||
return (left >> right) if op == '>>' else (left << right)
|
||||
case '+' | '-':
|
||||
if op == '-' and (lv:=_single_value(left)) is not None and (rv:=_single_value(right)) is not None: return _const(left.dtype, lv - rv)
|
||||
if op == '-' and left.op == Ops.CONST and right.op == Ops.CONST: return _const(left.dtype, left.val - right.val)
|
||||
return (left + right) if op == '+' else (left - right)
|
||||
case '*' | '/':
|
||||
# Integer promotion: promote 16-bit integers to 32-bit before multiply to avoid overflow
|
||||
@@ -560,7 +507,7 @@ class Parser:
|
||||
left, right = left.cast(pdt), right.cast(pdt)
|
||||
if op == '*': return left * right
|
||||
return (left // right) if dtypes.is_int(left.dtype) else (left / right)
|
||||
case '**': return UOp(Ops.EXP2, src=(right.cast(left.dtype),)) if _single_value(left) == 2.0 else left
|
||||
case '**': return UOp(Ops.EXP2, src=(right.cast(left.dtype),)) if left.op == Ops.CONST and left.val == 2.0 else left
|
||||
|
||||
_PREC = [('||',), ('&&',), ('|',), ('^',), ('&',), ('==', '!=', '<>'), ('>=', '<=', '>', '<'), ('>>', '<<'), ('+', '-'), ('*', '/'), ('**',)]
|
||||
|
||||
@@ -582,8 +529,8 @@ class Parser:
|
||||
return inner.eq(_const(inner.dtype, 0))
|
||||
if self.try_eat_val('-', 'OP'):
|
||||
inner = self.unary()
|
||||
if (v:=_single_value(inner)) is not None:
|
||||
return _const(dtypes.int if inner.dtype == dtypes.uint32 else inner.dtype, -v)
|
||||
if inner.op == Ops.CONST:
|
||||
return _const(dtypes.int if inner.dtype == dtypes.uint32 else inner.dtype, -inner.val)
|
||||
return inner.neg()
|
||||
if self.try_eat_val('+', 'OP'): return self.unary()
|
||||
return self.postfix()
|
||||
@@ -722,13 +669,15 @@ class Parser:
|
||||
self.eat('OP')
|
||||
width = self.parse()
|
||||
self.eat('RBRACKET')
|
||||
if isinstance(w:=_single_value(width), int):
|
||||
if width.op == Ops.CONST:
|
||||
w = int(width.val)
|
||||
return (base >> _to_u32(first)) & _const(base.dtype, (1 << w) - 1)
|
||||
return base
|
||||
if self.try_eat('COLON'):
|
||||
second = self.parse()
|
||||
self.eat('RBRACKET')
|
||||
if isinstance(a:=_single_value(first), int) and isinstance(b:=_single_value(second), int):
|
||||
if first.op == Ops.CONST and second.op == Ops.CONST:
|
||||
a, b = int(first.val), int(second.val)
|
||||
if a < b: return _bitreverse(base, b - a + 1)
|
||||
hi, lo = a, b
|
||||
if lo >= base.dtype.itemsize * 8:
|
||||
@@ -749,7 +698,8 @@ class Parser:
|
||||
dt_suffix = DTYPES.get(self.eat('IDENT').val, dtypes.uint32)
|
||||
if var_name is None:
|
||||
var_name = self._find_var_name(base)
|
||||
if isinstance(idx:=_single_value(first), int):
|
||||
if first.op == Ops.CONST:
|
||||
idx = int(first.val)
|
||||
# Check for array element (var@idx)
|
||||
if var_name and f'{var_name}@{idx}' in self.vars:
|
||||
v = self.vars[f'{var_name}@{idx}']
|
||||
@@ -922,7 +872,7 @@ class Parser:
|
||||
|
||||
def _coerce_cmp(self, l: UOp, r: UOp) -> tuple[UOp, UOp]:
|
||||
if l.dtype != r.dtype:
|
||||
if r.dtype == dtypes.int and isinstance(rv:=_single_value(r), int) and rv < 0: l = l.cast(dtypes.int)
|
||||
if r.dtype == dtypes.int and r.op == Ops.CONST and r.val < 0: l = l.cast(dtypes.int)
|
||||
else: r = r.cast(l.dtype)
|
||||
return l, r
|
||||
|
||||
@@ -940,8 +890,6 @@ class Parser:
|
||||
return result & _isnan(l).logical_not() & _isnan(r).logical_not()
|
||||
return result
|
||||
|
||||
_break_var_ids = itertools.count() # unique names for per-loop break-tracking variables
|
||||
|
||||
def _match_bracket(toks: list[Token], start: int) -> tuple[int, list[Token]]:
|
||||
"""Match brackets from start, return (end_idx, inner_tokens)."""
|
||||
j, depth = start + 1, 1
|
||||
@@ -1020,7 +968,9 @@ def parse_block(lines: list[str], start: int, env: dict[str, VarVal], funcs: dic
|
||||
p.eat('NUM')
|
||||
p.eat('QUOTE')
|
||||
if p.at('NUM'): return int(p.eat('NUM').val.rstrip('UuLl'))
|
||||
return int(p.parse())
|
||||
expr = p.parse().simplify()
|
||||
assert expr.op == Ops.CONST, f"loop bound must be constant, got {expr}"
|
||||
return int(expr.val)
|
||||
start_val = parse_bound()
|
||||
p.eat('COLON')
|
||||
end_val = parse_bound()
|
||||
@@ -1037,7 +987,7 @@ def parse_block(lines: list[str], start: int, env: dict[str, VarVal], funcs: dic
|
||||
i += 1
|
||||
# Execute loop with break support
|
||||
has_break = any('break' in bl.lower() for bl in body_lines)
|
||||
found_var = f'_found_{next(_break_var_ids)}' if has_break else None
|
||||
found_var = f'_found_{id(body_lines)}' if has_break else None
|
||||
if found_var: env[found_var] = block_assigns[found_var] = _const(dtypes.bool, False)
|
||||
for loop_i in range(start_val, end_val + 1):
|
||||
subst_lines = [_subst_loop_var(bl, loop_var, loop_i) for bl in body_lines if not (has_break and bl.strip().lower() == 'break')]
|
||||
@@ -1137,7 +1087,7 @@ def parse_block(lines: list[str], start: int, env: dict[str, VarVal], funcs: dic
|
||||
j, slice_toks = _match_bracket(toks, j)
|
||||
slice_str = _tok_str(slice_toks)
|
||||
hi_str, lo_str = slice_str.split(':')
|
||||
hi_val, lo_val = _const_int(hi_str), _const_int(lo_str)
|
||||
hi_val, lo_val = int(eval(hi_str.strip())), int(eval(lo_str.strip()))
|
||||
if j < len(toks) and toks[j].type == 'DOT': j += 2 # skip .type suffix
|
||||
if j < len(toks) and toks[j].type == 'EQUALS': j += 1
|
||||
ln = parse_tokens(lane_toks, env, funcs)
|
||||
@@ -1195,7 +1145,7 @@ def parse_block(lines: list[str], start: int, env: dict[str, VarVal], funcs: dic
|
||||
hi_str = ' '.join(t.val for t in toks[bracket_start:colon_pos] if t.type != 'EOF')
|
||||
lo_str = ' '.join(t.val for t in toks[colon_pos+1:j] if t.type != 'EOF')
|
||||
try:
|
||||
hi_val, lo_val = _const_int(hi_str), _const_int(lo_str)
|
||||
hi_val, lo_val = int(eval(hi_str)), int(eval(lo_str))
|
||||
hi, lo = max(hi_val, lo_val), min(hi_val, lo_val)
|
||||
j += 1
|
||||
if j < len(toks) and toks[j].type == 'DOT': j += 2
|
||||
@@ -1209,7 +1159,7 @@ def parse_block(lines: list[str], start: int, env: dict[str, VarVal], funcs: dic
|
||||
block_assigns[var] = env[var] = _set_bits(old, _val_to_bits(val), hi - lo + 1, lo)
|
||||
i += 1
|
||||
continue
|
||||
except (ValueError, SyntaxError): pass # non-constant slice bounds - fall through to other statement forms
|
||||
except Exception: pass
|
||||
elif toks[1].type == 'LBRACKET': # bit index: var[expr] (only for var[...], not var.type[...])
|
||||
existing = block_assigns.get(var, env.get(var))
|
||||
if existing is not None and isinstance(existing, UOp) and \
|
||||
@@ -1410,25 +1360,3 @@ def parse_block(lines: list[str], start: int, env: dict[str, VarVal], funcs: dic
|
||||
|
||||
def parse_expr(expr: str, env: dict[str, VarVal], funcs: dict | None = None) -> UOp:
|
||||
return parse_tokens(tokenize(expr.strip().rstrip(';')), env, funcs)
|
||||
|
||||
def parse_pcode(pcode: str, srcs: dict[str, UOp | int] | None = None) -> tuple[dict, list]:
|
||||
env: dict = srcs.copy() if srcs else {}
|
||||
assigns: list[tuple[str, UOp]] = []
|
||||
raw_lines = [l.strip().rstrip(';') for l in pcode.split('\n') if l.strip() and not l.strip().startswith('//')]
|
||||
# TODO: pcode.py should tokenize full pcode string instead of line-by-line, then this hack can be removed
|
||||
lines: list[str] = []
|
||||
for l in raw_lines:
|
||||
if lines and re.search(r'(&&|\|\||[&|+\-*/^])\s*$', lines[-1]): lines[-1] = lines[-1] + ' ' + l
|
||||
else: lines.append(l)
|
||||
_, final, _ = parse_block(lines, 0, env, assigns=assigns)
|
||||
sliced = set(d.split('[')[0] for d, _ in assigns if '[' in d)
|
||||
for var, val in final.items():
|
||||
if var in ['D0', 'S0', 'SCC', 'VCC', 'EXEC', 'PC', 'RETURN_DATA', 'VDATA'] and isinstance(val, UOp):
|
||||
if var in sliced and not any(re.match(rf'{var}\.\w+\s*=', l) for l in lines): continue
|
||||
for l in lines:
|
||||
if (m := re.match(rf'{var}\.(\w+(?:\[\w+\])?)', l)):
|
||||
assigns.append((f'{var}.{m.group(1)}', val))
|
||||
break
|
||||
else: assigns.append((var, val))
|
||||
return env, assigns
|
||||
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
# SQTT trace encoder for the emulator (the decoder lives in tinygrad/renderer/amd/sqtt.py).
|
||||
# run_asm emits packets inline as instructions execute; finished traces end up in emu.sqtt_traces.
|
||||
from __future__ import annotations
|
||||
from tinygrad.renderer.amd.dsl import Inst
|
||||
from tinygrad.renderer.amd.sqtt import (_build_decode_tables, PACKET_TYPES_RDNA3, PacketType, InstOp,
|
||||
LAYOUT_HEADER, WAVESTART, WAVEEND, INST, IMMEDIATE, VALUINST)
|
||||
|
||||
_NIB_COUNTS = {cls: nc for _, (cls, nc, *_) in _build_decode_tables(PACKET_TYPES_RDNA3)[0].items()}
|
||||
|
||||
def _emit_nibbles(nibbles: list[int], pkt_cls: type[PacketType], **kwargs):
|
||||
raw = pkt_cls.encoding.default
|
||||
for k, v in kwargs.items(): raw = pkt_cls.__dict__[k].set(raw, v)
|
||||
nibbles.extend((raw >> (i * 4)) & 0xF for i in range(_NIB_COUNTS[pkt_cls]))
|
||||
|
||||
def make_encoder():
|
||||
"""Build an SQTT trace encoder for the emulator. Returns (emit, finish, finalize)."""
|
||||
from tinygrad.runtime.autogen.amd.rdna3.enum import SOPPOp as SOPPOp3
|
||||
from tinygrad.runtime.autogen.amd.rdna4.enum import SOPPOp as SOPPOp4
|
||||
from tinygrad.runtime.autogen.amd.rdna3 import ins as ir3
|
||||
from tinygrad.runtime.autogen.amd.rdna4 import ins as ir4
|
||||
from tinygrad.runtime.autogen.amd.cdna import ins as irc
|
||||
import re
|
||||
|
||||
def _kinds(*names: str) -> tuple[type[Inst], ...]:
|
||||
return tuple(getattr(m, n) for m in (ir3, ir4, irc) for n in names if hasattr(m, n))
|
||||
_SOPP, _SMEM, _DS = _kinds('SOPP'), _kinds('SMEM'), _kinds('DS')
|
||||
_GLOBAL, _FLAT, _SCRATCH = _kinds('GLOBAL', 'VGLOBAL'), _kinds('FLAT', 'VFLAT'), _kinds('SCRATCH', 'VSCRATCH')
|
||||
_VALU = _kinds('VOP1', 'VOP2', 'VOP3', 'VOP3P', 'VOP3PX2', 'VOPC', 'VOPD', 'VOP3SD', 'VOP3_SDST', 'VOP1_SDST')
|
||||
|
||||
# SOPP classification sets
|
||||
_SOPP_SKIP = {SOPPOp3.S_ENDPGM.value, SOPPOp3.S_ENDPGM_SAVED.value, SOPPOp3.S_ENDPGM_ORDERED_PS_DONE.value, SOPPOp3.S_DELAY_ALU.value}
|
||||
_SOPP_IMMEDIATE = {SOPPOp3.S_NOP.value, SOPPOp3.S_CLAUSE.value, SOPPOp3.S_WAITCNT.value, SOPPOp3.S_WAITCNT_DEPCTR.value,
|
||||
SOPPOp3.S_WAIT_IDLE.value, SOPPOp3.S_WAIT_EVENT.value, SOPPOp3.S_SLEEP.value, SOPPOp3.S_SET_INST_PREFETCH_DISTANCE.value}
|
||||
for _op in (SOPPOp4.S_WAIT_ALU, SOPPOp4.S_WAIT_LOADCNT, SOPPOp4.S_WAIT_STORECNT, SOPPOp4.S_WAIT_SAMPLECNT,
|
||||
SOPPOp4.S_WAIT_BVHCNT, SOPPOp4.S_WAIT_EXPCNT, SOPPOp4.S_WAIT_DSCNT, SOPPOp4.S_WAIT_KMCNT,
|
||||
SOPPOp4.S_WAIT_LOADCNT_DSCNT, SOPPOp4.S_WAIT_STORECNT_DSCNT):
|
||||
_SOPP_IMMEDIATE.add(_op.value)
|
||||
_SOPP_BARRIER = {SOPPOp3.S_BARRIER.value}
|
||||
if hasattr(SOPPOp4, 'S_BARRIER_WAIT'): _SOPP_BARRIER.add(SOPPOp4.S_BARRIER_WAIT.value)
|
||||
if hasattr(SOPPOp4, 'S_BARRIER_LEAVE'): _SOPP_BARRIER.add(SOPPOp4.S_BARRIER_LEAVE.value)
|
||||
_SOPP_BRANCH = {SOPPOp3.S_BRANCH.value, SOPPOp3.S_CBRANCH_SCC0.value, SOPPOp3.S_CBRANCH_SCC1.value,
|
||||
SOPPOp3.S_CBRANCH_VCCZ.value, SOPPOp3.S_CBRANCH_VCCNZ.value,
|
||||
SOPPOp3.S_CBRANCH_EXECZ.value, SOPPOp3.S_CBRANCH_EXECNZ.value}
|
||||
|
||||
# VALU sub-classification patterns
|
||||
_VALUT_4_RE = re.compile(r'V_(EXP|LOG|RCP|RSQ|SQRT|SIN|COS|CEIL|FLOOR|TRUNC|RNDNE|FRACT|FREXP)_')
|
||||
_VALUB_2_RE = re.compile(r'V_(LSHLREV|LSHRREV|ASHRREV)_(B|I)64')
|
||||
_VALUB_4_RE = re.compile(r'V_MAD_(U|I)64')
|
||||
_VALUB_16_RE = re.compile(r'V_\w+_F64')
|
||||
|
||||
def _valu_op(op_name: str) -> InstOp|None:
|
||||
if 'CMPX' in op_name: return InstOp.VALU1_WR_EXEC
|
||||
if _VALUB_2_RE.search(op_name): return InstOp.VALUB_2
|
||||
if _VALUB_4_RE.search(op_name): return InstOp.VALUB_4
|
||||
if _VALUB_16_RE.search(op_name): return InstOp.VALUB_16
|
||||
if _VALUT_4_RE.search(op_name): return InstOp.VALUT_4
|
||||
return None
|
||||
|
||||
def _mem_op(t: type[Inst], op_name: str) -> InstOp:
|
||||
is_store = "STORE" in op_name
|
||||
if issubclass(t, _DS): return InstOp.LDS_WR_2 if is_store else InstOp.LDS_RD
|
||||
if issubclass(t, _GLOBAL): return InstOp.SGMEM_WR_2 if is_store else InstOp.SGMEM_RD_1
|
||||
if issubclass(t, _FLAT) or issubclass(t, _SCRATCH): return InstOp.FLAT_WR_3 if is_store else InstOp.FLAT_RD_2
|
||||
return InstOp.SALU
|
||||
|
||||
nibbles: list[int] = []
|
||||
started: set[int] = set()
|
||||
_emit_nibbles(nibbles, LAYOUT_HEADER, layout=3, sel_a=6)
|
||||
|
||||
def emit(wave_id: int, inst: Inst, branch_taken: bool|None):
|
||||
"""Emit an SQTT packet for one executed instruction."""
|
||||
w = wave_id & 0x1F
|
||||
if wave_id not in started:
|
||||
_emit_nibbles(nibbles, WAVESTART, delta=1, simd=0, wgp=0, wave=w, id7=wave_id)
|
||||
started.add(wave_id)
|
||||
inst_type, inst_op, op_name = type(inst), inst.op.value if hasattr(inst, 'op') else 0, inst.op.name if hasattr(inst, 'op') else ""
|
||||
if issubclass(inst_type, _SOPP):
|
||||
if inst_op in _SOPP_SKIP: return
|
||||
if inst_op in _SOPP_IMMEDIATE: _emit_nibbles(nibbles, IMMEDIATE, delta=1, wave=w)
|
||||
elif inst_op in _SOPP_BARRIER: _emit_nibbles(nibbles, INST, delta=1, wave=w, op=InstOp.BARRIER)
|
||||
elif inst_op in _SOPP_BRANCH: _emit_nibbles(nibbles, INST, delta=1, wave=w, op=InstOp.JUMP if branch_taken else InstOp.JUMP_NO)
|
||||
else: _emit_nibbles(nibbles, INST, delta=1, wave=w, op=InstOp.SALU)
|
||||
elif issubclass(inst_type, _VALU):
|
||||
if (op := _valu_op(op_name)) is None: _emit_nibbles(nibbles, VALUINST, delta=1, wave=w)
|
||||
else: _emit_nibbles(nibbles, INST, delta=1, wave=w, op=op)
|
||||
elif issubclass(inst_type, _SMEM): _emit_nibbles(nibbles, INST, delta=1, wave=w, op=InstOp.SMEM_RD)
|
||||
else: _emit_nibbles(nibbles, INST, delta=1, wave=w, op=_mem_op(inst_type, op_name))
|
||||
|
||||
def finish(wave_id: int):
|
||||
"""Emit WAVEEND for a completed wave."""
|
||||
if wave_id in started: _emit_nibbles(nibbles, WAVEEND, delta=1, simd=0, wgp=0, wave=wave_id & 0x1F)
|
||||
|
||||
def finalize() -> bytes:
|
||||
"""Pad and return the encoded SQTT blob."""
|
||||
while len(nibbles) % 2 != 0: nibbles.append(0)
|
||||
nibbles.extend([0] * 32)
|
||||
while len(nibbles) % 64 != 0: nibbles.append(0)
|
||||
return bytes(nibbles[i] | ((nibbles[i + 1] if i + 1 < len(nibbles) else 0) << 4) for i in range(0, len(nibbles), 2))
|
||||
|
||||
return emit, finish, finalize
|
||||
+5
-19
@@ -160,7 +160,7 @@ class MockUSB3:
|
||||
elif request == 0xE5:
|
||||
self.state._xram_write_byte(value, index)
|
||||
elif request == 0xF2:
|
||||
op = ("sram_read" if value & 0x8000 else "sram_write", 0xF000 + (index & 0xFF) * 0x4000, (value & 0x7FFF) * 512)
|
||||
op = ("sram_read" if value & 0x8000 else "sram_write", 0xF000, (value & 0x7FFF) * 512)
|
||||
if value & 0x8000: self._bulk_read_op = op
|
||||
else: self._bulk_write_op = op
|
||||
elif request == 0xF0:
|
||||
@@ -193,33 +193,19 @@ class MockUSB3:
|
||||
op, address, size = self._bulk_write_op
|
||||
assert len(data) == size
|
||||
if op == "sram_write":
|
||||
ctrl, (host_addr, region_size) = next((ca, r) for ca, r in self.state._dma_regions.items() if ca <= address < ca + r[1])
|
||||
ctypes.memmove(host_addr + (address - ctrl), data, min(len(data), region_size - (address - ctrl)))
|
||||
self.state.driver._emulate_execute() # landed data may un-stall a ring polling on it (e.g. copyin sentinels)
|
||||
host_addr, region_size = self.state._dma_regions[address]
|
||||
ctypes.memmove(host_addr, data, min(len(data), region_size))
|
||||
elif op == "pcie_write": self.state._pcie_write(address, data)
|
||||
else: raise RuntimeError(f"cannot bulk write for {op}")
|
||||
self._bulk_write_op = None
|
||||
|
||||
def bulk_write_async(self, payload:memoryview, timeout:int=10000) -> int: # the mock completes transfers synchronously
|
||||
self.bulk_write(bytes(payload), timeout)
|
||||
return 0
|
||||
|
||||
def control_write_async(self, request:int, value:int=0, index:int=0, data:bytes=b"", timeout:int=1000) -> int:
|
||||
self.control_write(request, value, index, data, timeout)
|
||||
return 0
|
||||
|
||||
def control_read_async(self, request:int, length:int, value:int=0, index:int=0, timeout:int=1000) -> tuple[int, memoryview]:
|
||||
return 0, self.control_read(request, length, value, index, timeout)
|
||||
|
||||
def bulk_wait(self, tag:int): pass
|
||||
|
||||
def bulk_read(self, length:int, timeout:int=1000) -> memoryview:
|
||||
assert self._bulk_read_op is not None
|
||||
op, address, size = self._bulk_read_op
|
||||
assert length == size
|
||||
if op == "sram_read":
|
||||
ctrl, (host_addr, region_size) = next((ca, r) for ca, r in self.state._dma_regions.items() if ca <= address < ca + r[1])
|
||||
data = bytes((ctypes.c_ubyte * min(length, region_size - (address - ctrl))).from_address(host_addr + (address - ctrl)))
|
||||
host_addr, region_size = self.state._dma_regions[address]
|
||||
data = bytes((ctypes.c_ubyte * min(length, region_size)).from_address(host_addr))
|
||||
elif op == "pcie_read": data = self.state._pcie_read(address, length)
|
||||
else: raise RuntimeError(f"cannot bulk read for {op}")
|
||||
self._bulk_read_op = None
|
||||
|
||||
@@ -3,6 +3,7 @@ from tinygrad import dtypes, Context
|
||||
from tinygrad.dtype import DType, ConstType
|
||||
from tinygrad.uop.ops import Ops, UOp
|
||||
from test.helpers import full_rewrite
|
||||
import numpy as np
|
||||
|
||||
class TestWeakConstFolding(unittest.TestCase):
|
||||
def test_weakint_math(self):
|
||||
@@ -26,14 +27,16 @@ class TestBitcastConstFolding(unittest.TestCase):
|
||||
for val, src_dt, dst_dt, bits in ((3000000000, dtypes.int32, dtypes.uint32, 3000000000),
|
||||
(70000, dtypes.int16, dtypes.uint16, 4464),
|
||||
(-5, dtypes.uint32, dtypes.int32, -5)):
|
||||
self.assertIs(UOp.const(val, src_dt).bitcast(dst_dt).simplify(), UOp.const(bits, dst_dt))
|
||||
self.assertEqual(UOp.const(val, src_dt).bitcast(dst_dt).simplify().val, bits)
|
||||
|
||||
def test_scalar_bitcast(self):
|
||||
def t(cases: dict[DType, ConstType]):
|
||||
for (from_dt, from_v), (to_dt, to_v) in itertools.product(cases.items(), cases.items()):
|
||||
if not math.isnan(from_v):
|
||||
r = UOp.const(from_v, from_dt).bitcast(to_dt).simplify()
|
||||
self.assertIs(r, UOp.const(to_v, to_dt), f"{from_dt} -> {to_dt} ({from_v} -> {to_v})")
|
||||
self.assertEqual(r.op, Ops.CONST, msg:=f"{from_dt} -> {to_dt} ({from_v} -> {to_v})")
|
||||
self.assertEqual(r.dtype, to_dt, msg)
|
||||
np.testing.assert_equal(r.val, to_v, msg)
|
||||
|
||||
t({dtypes.int8: 0, dtypes.uint8: 0, dtypes.bool: False})
|
||||
t({dtypes.int8: 1, dtypes.uint8: 1, dtypes.bool: True})
|
||||
|
||||
@@ -419,7 +419,7 @@ class TestAutoCastType(unittest.TestCase):
|
||||
self.check_where_alternate_input_other(3, True, dtypes.weakint)
|
||||
|
||||
def test_where_non_bool_cond_raises(self):
|
||||
with self.assertRaises(RuntimeError): Tensor([1, 0, 2]).where(1, 0).dtype
|
||||
with self.assertRaises(RuntimeError): Tensor([1, 0, 2]).where(1, 0)
|
||||
self.check_where_alternate_input_other(False, True, dtypes.bool)
|
||||
|
||||
@given(strat.sampled_from(core_dtypes), strat.sampled_from(core_dtypes))
|
||||
|
||||
@@ -95,26 +95,6 @@ class TestLLMTokenizer(unittest.TestCase):
|
||||
self.assertEqual(template.end_turn(), "[/INST]")
|
||||
self.assertEqual(template.role("assistant"), "")
|
||||
|
||||
def test_tekken_gpt4o_split(self):
|
||||
split = {p: SimpleTokenizer({}, {}, p)._split_to_word.findall for p in ("tekken", "gpt-4o")}
|
||||
shared = {
|
||||
"HelloWorld": ["Hello", "World"],
|
||||
" ÜNICODE": [" ÜNICODE"], # Ü: non-ascii upper joins the run
|
||||
"é café": ["é", " café"], # first é is e + U+0301 combining acute (NFD)
|
||||
"เพื่อน วิ": ["เพื่อน", " วิ"], # thai vowel marks stay in the word
|
||||
"a/b\r\n x": ["a", "/b", "\r\n", " x"], # punct tail eats /
|
||||
}
|
||||
for s, want in shared.items():
|
||||
self.assertEqual(split["tekken"](s), want, f"tekken {s!r}")
|
||||
self.assertEqual(split["gpt-4o"](s), want, f"gpt-4o {s!r}")
|
||||
differ = [
|
||||
("12345", list("12345"), ["123", "45"]), # digits: tekken single, o200k groups {1,3}
|
||||
("it's I'M don'T", ["it", "'s", " I", "'M", " don", "'T"], ["it's", " I'M", " don'T"]), # contraction: o200k inline suffix
|
||||
]
|
||||
for s, tk, go in differ:
|
||||
self.assertEqual(split["tekken"](s), tk, f"tekken {s!r}")
|
||||
self.assertEqual(split["gpt-4o"](s), go, f"gpt-4o {s!r}")
|
||||
|
||||
def test_stream_decoder(self):
|
||||
"""stream_decoder buffers incomplete UTF-8: token 25677 has 3/4 of emoji, token 138 completes it."""
|
||||
bs = [*range(33, 127), *range(161, 173), *range(174, 256)]
|
||||
|
||||
@@ -3,7 +3,7 @@ import unittest, itertools
|
||||
from tinygrad.codegen.late.coalesce import indexing_simplify
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.uop.ops import UOp, Ops, graph_rewrite
|
||||
from tinygrad.uop.weak import pm_commit_weak
|
||||
from tinygrad.uop.weak import pm_lower_index_dtype
|
||||
from tinygrad.uop.symbolic import simplify_valid, sym, pm_move_where_on_load
|
||||
from tinygrad.helpers import Context
|
||||
from test.helpers import full_rewrite
|
||||
@@ -496,7 +496,7 @@ class TestImageSimplification(unittest.TestCase):
|
||||
idx_y = (f + UOp.const(1.0)).cast(dtypes.int)
|
||||
load = get_load_image_uop((10, 10, 4), (UOp.const(-1) < idx_y) & (idx_y < UOp.const(10)),
|
||||
(Special("gidx0", 10), idx_y))
|
||||
off = graph_rewrite(load.sink(), pm_commit_weak+indexing_simplify).src[0].src[0]
|
||||
off = graph_rewrite(load.sink(), pm_lower_index_dtype+indexing_simplify, ctx={}).src[0].src[0]
|
||||
self.assertEqual(off.src[1].get_valid(), UOp.const(True))
|
||||
|
||||
class TestDropTrueGate(unittest.TestCase):
|
||||
|
||||
@@ -202,11 +202,6 @@ class TestUOpGraph(unittest.TestCase):
|
||||
invalid_lane_mul = next(u for u in out.src[0].toposort() if u.op is Ops.MUL)
|
||||
self.assertIs(invalid_lane_mul.dtype, dtypes.bool)
|
||||
|
||||
def test_devectorize_zero_sized_scalar_expand(self):
|
||||
from tinygrad.codegen import devectorizer2
|
||||
expanded = UOp.const(1.0).reshape(1, 1).expand(0, 3)
|
||||
self.assertEqual(graph_rewrite(expanded, devectorizer2).shape, (0, 3))
|
||||
|
||||
def test_gep_vec_const_fold(self):
|
||||
for vec_size in [2, 4, 8]:
|
||||
consts = [UOp.const(float(i), dtypes.float) for i in range(vec_size)]
|
||||
@@ -235,11 +230,11 @@ class TestUOpGraph(unittest.TestCase):
|
||||
|
||||
def test_depth_2_const_fold(self):
|
||||
v = UOp.variable("tmp", 0, 1, dtypes.int, param=True)
|
||||
c2 = UOp.const(2)
|
||||
c4 = UOp.const(4)
|
||||
c2 = UOp.const(2, dtypes.int)
|
||||
c4 = UOp.const(4, dtypes.int)
|
||||
vc = v+c2
|
||||
out = vc+c4
|
||||
self.assertIs(out.simplify(), (v+UOp.const(6)).simplify())
|
||||
self.assertIs(out.simplify(), (v+UOp.const(6, dtypes.int)).simplify())
|
||||
|
||||
def test_bitcast_to_same_dtype_fold(self):
|
||||
for dt in dtypes.ints + dtypes.floats + (dtypes.bool,):
|
||||
@@ -250,7 +245,7 @@ class TestUOpGraph(unittest.TestCase):
|
||||
|
||||
def test_sub_with_cast_folds(self):
|
||||
a = Variable("a", 0, 5)
|
||||
out = a+(-a)
|
||||
out = a.cast(dtypes.int)+(-a).cast(dtypes.int)
|
||||
self.assertIs(full_rewrite(out.sink()).src[0], full_rewrite(UOp.const(0, dtypes.int).sink()).src[0])
|
||||
|
||||
def test_where_on_gated_load_fold(self):
|
||||
@@ -434,7 +429,7 @@ class TestReduceCollapse(unittest.TestCase):
|
||||
|
||||
class TestMovementOps(unittest.TestCase):
|
||||
def test_pm_mops_partial_reshape_index_removes_reshape(self):
|
||||
from tinygrad.schedule.prepare import pm_mops
|
||||
from tinygrad.schedule.rangeify import pm_mops
|
||||
src = UOp.param(0, dtypes.float, shape=(32, 4))
|
||||
r0, r1 = UOp.range(4, 0), UOp.range(8, 1)
|
||||
result = graph_rewrite(src.reshape((4, 8, 4)).index(r0, r1), pm_mops, name="test")
|
||||
@@ -444,7 +439,7 @@ class TestMovementOps(unittest.TestCase):
|
||||
self.assertNotIn(Ops.RESHAPE, [u.op for u in result.toposort()])
|
||||
|
||||
def test_pm_mops_partial_reshape_index_suffix_mismatch_does_nothing(self):
|
||||
from tinygrad.schedule.prepare import pm_mops
|
||||
from tinygrad.schedule.rangeify import pm_mops
|
||||
src = UOp.param(0, dtypes.float, shape=(2, 6))
|
||||
result = graph_rewrite(src.reshape((2, 3, 2)).index(UOp.range(2, 0)), pm_mops, name="test")
|
||||
self.assertEqual(result.op, Ops.INDEX)
|
||||
|
||||
@@ -5,7 +5,8 @@ import z3
|
||||
from tinygrad.dtype import dtypes, ConstType, DType, Invalid
|
||||
from tinygrad.uop.ops import UOp, Ops, graph_rewrite, sym_infer
|
||||
from tinygrad.uop.spec import spec_shared, type_verify
|
||||
from tinygrad.uop.symbolic import sym, commutative, pm_simplify_valid, pm_move_where_on_load, symbolic_simple
|
||||
from tinygrad.uop.symbolic import sym, commutative, pm_simplify_valid, pm_move_where_on_load
|
||||
from tinygrad.uop.weak import pm_cast_weak
|
||||
from tinygrad.uop.validate import uops_to_z3
|
||||
|
||||
def check_uop_against_string(self, v:UOp, s:str):
|
||||
@@ -35,7 +36,7 @@ class TestSymbolic(unittest.TestCase):
|
||||
self.assertEqual(solver.check(expr1 != expr2), z3.unsat, "simplified expression not equal to original")
|
||||
|
||||
def helper_test_variable(self, v, n, m, s, test_z3:bool=True):
|
||||
v_simplified = graph_rewrite(v, sym, name="simplify symbolic uop")
|
||||
v_simplified = graph_rewrite(v, sym+pm_cast_weak, name="simplify symbolic uop")
|
||||
if test_z3: self.check_equal_z3(v, v_simplified)
|
||||
nmin, nmax = v_simplified.vmin, v_simplified.vmax
|
||||
check_uop_against_string(self, v_simplified, s)
|
||||
@@ -148,13 +149,6 @@ class TestSymbolic(unittest.TestCase):
|
||||
def test_xor_0(self):
|
||||
self.helper_test_variable(Variable("a", 0, 8, dtypes.int) ^ 0, 0, 8, "a", test_z3=False)
|
||||
|
||||
def test_or_0(self):
|
||||
self.helper_test_variable(Variable("a", 0, 8, dtypes.int) | 0, 0, 8, "a", test_z3=False)
|
||||
|
||||
def test_shift_0(self):
|
||||
self.helper_test_variable(Variable("a", 0, 8, dtypes.int) << 0, 0, 8, "a")
|
||||
self.helper_test_variable(Variable("a", 0, 8, dtypes.int) >> 0, 0, 8, "a")
|
||||
|
||||
def test_xor_self_inverse(self):
|
||||
self.helper_test_variable((Variable("a", 0, 8, dtypes.int) ^ 5) ^ 5, 0, 8, "a", test_z3=False)
|
||||
|
||||
@@ -448,20 +442,10 @@ class TestSymbolic(unittest.TestCase):
|
||||
def test_and_remove(self):
|
||||
self.helper_test_variable(uand([uconst(1), Variable("a", 0, 1)]), 0, 1, "a")
|
||||
|
||||
def test_zero_div_zero_bottom_up(self):
|
||||
# codegen runs symbolic_simple bottom_up, so the 0/0 is rewritten before its consts fold.
|
||||
# without the guard the unsound x/x -> 1 below it claims this one.
|
||||
z = UOp.const(0.0)
|
||||
self.assertTrue(math.isnan(graph_rewrite(z/z, symbolic_simple, bottom_up=True).arg))
|
||||
|
||||
def test_masked_shr_fold(self):
|
||||
x = UOp.variable('x', 0, 255, dtype=dtypes.uint32, param=True)
|
||||
self.helper_test_variable((x & -4) >> 2, 0, 63, "(x>>2)")
|
||||
|
||||
def test_masked_idiv_fold(self):
|
||||
x = UOp.variable('x', 0, 255, dtype=dtypes.uint32, param=True)
|
||||
self.helper_test_variable((x & -4) // 4, 0, 63, "(x//4)")
|
||||
|
||||
def test_bool_or_not_tautology(self):
|
||||
a = Variable("a", 0, 10)
|
||||
c = a<10
|
||||
@@ -1033,7 +1017,7 @@ class TestSymbolic(unittest.TestCase):
|
||||
cond = Variable("s", 0, 3, dtypes.int) < 2
|
||||
a = Variable("a", 0, 3, dtypes.int)
|
||||
self.assertIs(graph_rewrite(cond.where(a, a+1).cast(dtypes.half), sym), cond.where(a.cast(dtypes.half), (a+1).cast(dtypes.half)))
|
||||
self.assertIs(graph_rewrite(cond.where(a, uconst(2)).cast(dtypes.half), sym), cond.where(a.cast(dtypes.half), uconst(2.0)))
|
||||
self.assertIs(graph_rewrite(cond.where(a, uconst(2)).cast(dtypes.half), sym), cond.where(a.cast(dtypes.half), UOp.const(2, dtypes.half)))
|
||||
self.assertIs(graph_rewrite(cond.where(a, UOp.invalid()).cast(dtypes.half), sym), cond.where(a.cast(dtypes.half), UOp.invalid()))
|
||||
|
||||
def test_where_const_gate_keeps_stated_width(self):
|
||||
@@ -1465,11 +1449,6 @@ class TestGatedUopGivenValid(unittest.TestCase):
|
||||
self.assertEqual(idx, (r0 < 3).where(expected_vec, UOp.invalid()))
|
||||
|
||||
class TestRangeSplitting(unittest.TestCase):
|
||||
def test_end_preserves_constant_backedge(self):
|
||||
loop, backedge = UOp.loop(0), UOp.const(False)
|
||||
end = graph_rewrite(UOp(Ops.NOOP).end(loop, backedge), sym)
|
||||
self.assertEqual(end.src, (UOp(Ops.NOOP), loop, backedge))
|
||||
|
||||
def test_range_split_on_mod(self):
|
||||
# test that mark_range_mod splits RANGE(8) into RANGE(4)*2 + RANGE(2) when used with %2
|
||||
from tinygrad.codegen.simplify import pm_split_ranges, pm_flatten_range
|
||||
|
||||
@@ -82,7 +82,7 @@ class TestVminVmaxProperties(unittest.TestCase):
|
||||
def test_vmin_vmax_multiplication_0_inf(self):
|
||||
# vmin and vmax for multiplication with a variable
|
||||
x = UOp.const(0.0)
|
||||
y = UOp.load(UOp.param(0, dtypes.float, (1,)), UOp.const(0))
|
||||
y = UOp.load(UOp.param(0, dtypes.float, (1,)), UOp.const(0), dtype=dtypes.float)
|
||||
uop = x * y
|
||||
# TODO: these should be 0, but definitely should not be nan
|
||||
self.assertEqual(uop.vmin, -math.inf)
|
||||
|
||||
+22
-33
@@ -5,8 +5,8 @@ from tinygrad.tensor import Tensor
|
||||
from tinygrad.helpers import Timing, Context, cdiv
|
||||
from tinygrad.dtype import dtypes, AddrSpace, ConstFloat, Invalid # noqa: F401
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.uop.ops import Ops, AxisType, ParamArg, PatternMatcher, UOp, UPat, dtype_from_uop, exec_alu, graph_rewrite # noqa: F401 # ParamArg used by eval(str(uop)) roundtrip tests
|
||||
from tinygrad.uop.weak import pm_lower_weak
|
||||
from tinygrad.uop.ops import Ops, ParamArg, PatternMatcher, UOp, UPat, dtype_from_uop, exec_alu, graph_rewrite # noqa: F401 # ParamArg used by eval(str(uop)) roundtrip tests
|
||||
from tinygrad.uop.weak import pm_lower_index_dtype
|
||||
from tinygrad.uop.spec import spec_program, spec_shared, type_verify
|
||||
from tinygrad.uop.symbolic import sym, pm_remove_invalid
|
||||
from test.helpers import eval_uop, to_uops_list
|
||||
@@ -56,7 +56,7 @@ class TestDTypeFromUOp(unittest.TestCase):
|
||||
if u.is_invalid)), (dtypes.float32, dtypes.float32, dtypes.bool))
|
||||
invalid, value = UOp.invalid(), UOp.const(1, dtypes.float32)
|
||||
for u in (UOp.param(0, dtypes.bool, ()).where(value, invalid), value+invalid, UOp.stack(value, invalid)): self.assertIs(u.src[-1], invalid)
|
||||
for u in (UOp(Ops.STACK, src=(value, invalid)), UOp(Ops.ADD, src=(value, invalid)),
|
||||
for u in (UOp(Ops.STACK, dtypes.float32, src=(value, invalid)), UOp(Ops.ADD, dtypes.float32, src=(value, invalid)),
|
||||
UOp.const(True).where(value, invalid), UOp(Ops.CMPLT, src=(invalid, value)), UOp(Ops.CMPLT, src=(value, invalid)),
|
||||
UOp.param(0, dtypes.float32, (4,)).index(invalid)): type_verify(u, spec_shared)
|
||||
gate, value = UOp.param(0, dtypes.bool, ()), UOp.param(1, dtypes.float, ())
|
||||
@@ -64,7 +64,7 @@ class TestDTypeFromUOp(unittest.TestCase):
|
||||
type_verify(out.sink(), spec_program)
|
||||
|
||||
def test_remove_invalid_stack_lanes(self):
|
||||
stack = UOp(Ops.STACK, src=(UOp.const(1, dtypes.half), UOp.invalid()))
|
||||
stack = UOp(Ops.STACK, dtypes.half, (UOp.const(1, dtypes.half), UOp.invalid()))
|
||||
out = graph_rewrite(stack, pm_remove_invalid)
|
||||
self.assertEqual(out.src, (UOp.const(1, dtypes.half), UOp.const(0, dtypes.half)))
|
||||
type_verify(out.sink(), spec_program)
|
||||
@@ -76,18 +76,16 @@ class TestLowerIndexDtype(unittest.TestCase):
|
||||
buf = UOp.param(0, dtypes.float, (2**31+64,))
|
||||
i = UOp.variable("i", 0, 2**28)
|
||||
shrink = UOp(Ops.SHRINK, src=(buf, (i*24).valid(i < 2**28), UOp.const(4)))
|
||||
lowered = graph_rewrite(shrink.sink(), pm_lower_weak)
|
||||
self.assertTrue(all(u.op is Ops.CONST for u in lowered.backward_slice_with_self if u.dtype in dtypes.weaks),
|
||||
"lowering must resolve every weak width, except a typed literal's value half")
|
||||
lowered = graph_rewrite(shrink.sink(), pm_lower_index_dtype)
|
||||
self.assertTrue(all(u.dtype != dtypes.weakint for u in lowered.backward_slice_with_self), "lowering must resolve all weakint")
|
||||
sh = next(u for u in lowered.backward_slice_with_self if u.op is Ops.SHRINK)
|
||||
self.assertEqual(sh.src[1].dtype, dtypes.long)
|
||||
|
||||
def test_reg_buffer_size_lowers(self):
|
||||
reg = UOp.placeholder((4,), dtypes.float, 0, addrspace=AddrSpace.REG)
|
||||
self.assertEqual(reg.src[0].dtype, dtypes.weakint)
|
||||
lowered = graph_rewrite(reg.sink(), pm_lower_weak)
|
||||
self.assertTrue(all(u.op is Ops.CONST for u in lowered.backward_slice_with_self if u.dtype in dtypes.weaks),
|
||||
"lowering must resolve every weak width, except a typed literal's value half")
|
||||
lowered = graph_rewrite(reg.sink(), pm_lower_index_dtype)
|
||||
self.assertTrue(all(u.dtype != dtypes.weakint for u in lowered.backward_slice_with_self), "lowering must resolve all weakint")
|
||||
self.assertEqual(next(u for u in lowered.backward_slice_with_self if u.op is Ops.BUFFER).src[0].dtype, dtypes.int)
|
||||
|
||||
class TestSafeCast(unittest.TestCase):
|
||||
@@ -282,9 +280,9 @@ class TestFastIdiv(unittest.TestCase):
|
||||
def test_division_power_of_two(self):
|
||||
for dt in (dtypes.int32, dtypes.uint32):
|
||||
g = UOp.param(0, dt, (3,))
|
||||
c = UOp.const(2)
|
||||
c = UOp.const(2).cast(dt)
|
||||
l = g.index(c)
|
||||
a = UOp(Ops.CDIV, src=(l, c))
|
||||
a = UOp(Ops.CDIV, dt, (l, c))
|
||||
uops = to_uops_list([a], ren=Device[Device.DEFAULT].renderer)
|
||||
Device[Device.DEFAULT].renderer.render(uops)
|
||||
ops = [x.op for x in uops]
|
||||
@@ -295,32 +293,31 @@ class TestFastIdiv(unittest.TestCase):
|
||||
# FLOORMOD by a power of two lowers to AND (correct floor mod for any sign in two's complement)
|
||||
for dt in (dtypes.int32, dtypes.uint32):
|
||||
g = UOp.param(0, dt, (9,))
|
||||
c = UOp.const(8)
|
||||
a = UOp(Ops.FLOORMOD, src=(g.index(c), c))
|
||||
c = UOp.const(8).cast(dt)
|
||||
a = UOp(Ops.FLOORMOD, dt, (g.index(c), c))
|
||||
uops = to_uops_list([a], ren=Device[Device.DEFAULT].renderer)
|
||||
ops = [x.op for x in uops]
|
||||
self.assertIn(Ops.AND, ops, f"For dtype={dt} FLOORMOD by pow2 did not simplify to AND")
|
||||
self.assertNotIn(Ops.CMOD, ops, f"For dtype={dt} FLOORMOD by pow2 left a MOD")
|
||||
self.assertNotIn(Ops.FLOORMOD, ops, f"For dtype={dt} FLOORMOD survived past late rewrite")
|
||||
|
||||
def test_floordiv_power_of_two(self):
|
||||
# FLOORDIV by a power of two lowers to a shift, with no round toward zero correction (a shift is exactly floor division)
|
||||
for dt in (dtypes.int32, dtypes.uint32, dtypes.int64, dtypes.uint64):
|
||||
def test_floordiv_power_of_two_uint(self):
|
||||
# uint FLOORDIV by a power of two lowers to a shift, leaving no IDIV/FLOORDIV in the kernel
|
||||
for dt in (dtypes.uint32, dtypes.uint64):
|
||||
g = UOp.param(0, dt, (3,))
|
||||
c = UOp.const(2)
|
||||
a = UOp(Ops.FLOORDIV, src=(g.index(c), c))
|
||||
c = UOp.const(2).cast(dt)
|
||||
a = UOp(Ops.FLOORDIV, dt, (g.index(c), c))
|
||||
uops = to_uops_list([a], ren=Device[Device.DEFAULT].renderer)
|
||||
ops = [x.op for x in uops]
|
||||
self.assertIn(Ops.SHR, ops, f"For dtype={dt} FLOORDIV by power of two did not simplify to shift")
|
||||
self.assertNotIn(Ops.CDIV, ops, f"For dtype={dt} FLOORDIV by power of two did not simplify to shift")
|
||||
self.assertNotIn(Ops.CMOD, ops, f"For dtype={dt} FLOORDIV by pow2 kept the round toward zero correction")
|
||||
self.assertNotIn(Ops.FLOORDIV, ops, f"For dtype={dt} FLOORDIV survived past late rewrite")
|
||||
|
||||
@Context(DISABLE_FAST_IDIV=0)
|
||||
@unittest.skipIf(Device.DEFAULT == "WEBGPU", "WEBGPU doesn't support long")
|
||||
def test_fast_idiv_and_mod(self):
|
||||
g = UOp.param(0, dtypes.uint32, (4,))
|
||||
c = UOp.const(3)
|
||||
c = UOp.const(3).cast(dtypes.uint)
|
||||
l = g.index(c)
|
||||
a = UOp(Ops.CDIV, src=(l, c))
|
||||
uops = to_uops_list([a], ren=Device[Device.DEFAULT].renderer)
|
||||
@@ -340,7 +337,7 @@ class TestFastIdiv(unittest.TestCase):
|
||||
def test_fast_idiv_bounded_numerator_zero(self):
|
||||
x = UOp.variable("x", 0, 1, dtype=dtypes.int32)
|
||||
for val in range(2):
|
||||
self.assertEqual(eval_uop(x.alu(Ops.CDIV, UOp.const(3)), vals=(val,)), cdiv(val, 3))
|
||||
self.assertEqual(eval_uop(x.alu(Ops.CDIV, UOp.const(3).cast(x.dtype)), vals=(val,)), cdiv(val, 3))
|
||||
|
||||
@Context(DISABLE_FAST_IDIV=0)
|
||||
def test_fast_idiv_remove_powers_of_two(self):
|
||||
@@ -365,7 +362,7 @@ class TestFastIdiv(unittest.TestCase):
|
||||
|
||||
def test_disable_fast_idiv(self):
|
||||
g = UOp.param(0, dtypes.uint32, (4,))
|
||||
c = UOp.const(3)
|
||||
c = UOp.const(3).cast(dtypes.uint)
|
||||
l = g.index(c)
|
||||
a = UOp(Ops.CDIV, src=(l, c))
|
||||
with Context(DISABLE_FAST_IDIV=1):
|
||||
@@ -460,19 +457,11 @@ class TestUopsObject(unittest.TestCase):
|
||||
self.assertEqual(a.device, Device.DEFAULT)
|
||||
|
||||
class TestUOpRender(unittest.TestCase):
|
||||
def test_render_ssimplified_marg_outside_toposort(self):
|
||||
r = UOp.range(UOp.const(16, dtypes.int), 2, AxisType.WEAK, dtype=dtypes.int)
|
||||
offset = (r * 2) + (r * 2)
|
||||
shrink = UOp(Ops.SHRINK, src=(UOp.param(0, dtypes.uint, (32,)), offset, UOp.const(2, dtypes.int)))
|
||||
self.assertIsNot(shrink.src[1], shrink.marg[0][0])
|
||||
self.assertEqual(shrink.render(simplify=False), "p0.shrink((((r2*4), 2),))")
|
||||
self.assertEqual(UOp.range(1, 0, src=(shrink,), dtype=dtypes.int).render(simplify=False), "r0")
|
||||
|
||||
def test_render_vectorize_empty(self):
|
||||
u = UOp(Ops.STACK, src=())
|
||||
u = UOp(Ops.STACK, dtype=dtypes.void, src=())
|
||||
self.assertEqual(u.render(simplify=False), "{}")
|
||||
def test_render_vectorize_empty_simplified(self):
|
||||
u = UOp(Ops.STACK, src=())
|
||||
u = UOp(Ops.STACK, dtype=dtypes.void, src=())
|
||||
self.assertEqual(u.render(), "{}")
|
||||
def test_render_vectorize_same(self):
|
||||
u = UOp(Ops.STACK, src=(UOp.const(0),)*3)
|
||||
|
||||
@@ -14,37 +14,37 @@ class TestValidateOOB(unittest.TestCase):
|
||||
def test_const_index(self):
|
||||
with Context(CHECK_OOB=1, SPEC=2):
|
||||
buf = UOp.param(0, dtypes.int, (16,))
|
||||
to_uops_list([buf.index(UOp.const(0)).load()]) # valid
|
||||
to_uops_list([buf.index(UOp.const(15)).load()]) # valid (last element)
|
||||
to_uops_list([buf.index(UOp.const(0)).load(dtype=dtypes.int)]) # valid
|
||||
to_uops_list([buf.index(UOp.const(15)).load(dtype=dtypes.int)]) # valid (last element)
|
||||
with self.assertRaises(RuntimeError):
|
||||
to_uops_list([buf.index(UOp.const(16)).load()]) # off by one
|
||||
to_uops_list([buf.index(UOp.const(16)).load(dtype=dtypes.int)]) # off by one
|
||||
with self.assertRaises(RuntimeError):
|
||||
to_uops_list([buf.index(UOp.const(42)).load()]) # way out
|
||||
to_uops_list([buf.index(UOp.const(42)).load(dtype=dtypes.int)]) # way out
|
||||
|
||||
def test_variable_index(self):
|
||||
with Context(CHECK_OOB=1, SPEC=2):
|
||||
buf = UOp.param(0, dtypes.int, (16,))
|
||||
to_uops_list([buf.index(Variable("i", 0, 15)).load()]) # valid
|
||||
to_uops_list([buf.index(Variable("i", 0, 15)).load(dtype=dtypes.int)]) # valid
|
||||
with self.assertRaises(RuntimeError):
|
||||
to_uops_list([buf.index(Variable("i", 0, 20)).load()]) # oob
|
||||
to_uops_list([buf.index(Variable("i", 0, 20)).load(dtype=dtypes.int)]) # oob
|
||||
with self.assertRaises(RuntimeError):
|
||||
to_uops_list([buf.index(Variable("i", -5, 10)).load()]) # negative
|
||||
to_uops_list([buf.index(Variable("i", -5, 10)).load(dtype=dtypes.int)]) # negative
|
||||
|
||||
def test_range_with_mask(self):
|
||||
with Context(CHECK_OOB=1, SPEC=2):
|
||||
buf = UOp.param(0, dtypes.int, (16,))
|
||||
r = UOp.range(42, 0, AxisType.GLOBAL)
|
||||
to_uops_list([buf.index(r.valid(r < 16)).load()]) # valid
|
||||
to_uops_list([buf.index(r.valid(r < 16)).load(dtype=dtypes.int)]) # valid
|
||||
with self.assertRaises(RuntimeError):
|
||||
to_uops_list([buf.index(r.valid(r < 17)).load()]) # oob
|
||||
to_uops_list([buf.index(r.valid(r < 17)).load(dtype=dtypes.int)]) # oob
|
||||
|
||||
def test_variable_with_mask(self):
|
||||
with Context(CHECK_OOB=1, SPEC=2):
|
||||
buf = UOp.param(0, dtypes.int, (16,))
|
||||
v = Variable("v", -5, 80)
|
||||
to_uops_list([buf.index(v.valid((v >= 0) & (v < 16))).load()]) # valid
|
||||
to_uops_list([buf.index(v.valid((v >= 0) & (v < 16))).load(dtype=dtypes.int)]) # valid
|
||||
with self.assertRaises(RuntimeError):
|
||||
to_uops_list([buf.index(v.valid(v < 20)).load()]) # negative not masked
|
||||
to_uops_list([buf.index(v.valid(v < 20)).load(dtype=dtypes.int)]) # negative not masked
|
||||
|
||||
def test_gated_store(self):
|
||||
with Context(CHECK_OOB=1, SPEC=2):
|
||||
@@ -58,62 +58,62 @@ class TestValidateOOB(unittest.TestCase):
|
||||
def test_floordiv(self):
|
||||
with Context(CHECK_OOB=1, SPEC=2):
|
||||
buf = UOp.param(0, dtypes.int, (16,))
|
||||
to_uops_list([buf.index(UOp.range(32, 0, AxisType.GLOBAL) // 2).load()]) # 0..15 valid
|
||||
to_uops_list([buf.index(UOp.range(32, 0, AxisType.GLOBAL) // 2).load(dtype=dtypes.int)]) # 0..15 valid
|
||||
with self.assertRaises(RuntimeError):
|
||||
to_uops_list([buf.index(UOp.range(34, 0, AxisType.GLOBAL) // 2).load()]) # 0..16 oob
|
||||
to_uops_list([buf.index(UOp.range(34, 0, AxisType.GLOBAL) // 2).load(dtype=dtypes.int)]) # 0..16 oob
|
||||
|
||||
def test_mod(self):
|
||||
with Context(CHECK_OOB=1, SPEC=2):
|
||||
buf = UOp.param(0, dtypes.int, (16,))
|
||||
r = UOp.range(100, 0, AxisType.GLOBAL)
|
||||
to_uops_list([buf.index(r % 16).load()]) # 0..15 valid
|
||||
to_uops_list([buf.index(r % 16).load(dtype=dtypes.int)]) # 0..15 valid
|
||||
with self.assertRaises(RuntimeError):
|
||||
to_uops_list([buf.index(r % 20).load()]) # 0..19 oob
|
||||
to_uops_list([buf.index(r % 20).load(dtype=dtypes.int)]) # 0..19 oob
|
||||
|
||||
def test_shr(self):
|
||||
with Context(CHECK_OOB=1, SPEC=2):
|
||||
buf = UOp.param(0, dtypes.int, (16,))
|
||||
to_uops_list([buf.index(UOp.range(64, 0, AxisType.GLOBAL) >> 2).load()]) # 0..15 valid
|
||||
to_uops_list([buf.index(UOp.range(64, 0, AxisType.GLOBAL) >> 2).load(dtype=dtypes.int)]) # 0..15 valid
|
||||
with self.assertRaises(RuntimeError):
|
||||
to_uops_list([buf.index(UOp.range(128, 0, AxisType.GLOBAL) >> 2).load()]) # 0..31 oob
|
||||
to_uops_list([buf.index(UOp.range(128, 0, AxisType.GLOBAL) >> 2).load(dtype=dtypes.int)]) # 0..31 oob
|
||||
|
||||
def test_shl(self):
|
||||
with Context(CHECK_OOB=1, SPEC=2):
|
||||
buf = UOp.param(0, dtypes.int, (64,))
|
||||
r = UOp.range(8, 0, AxisType.GLOBAL)
|
||||
to_uops_list([buf.index(r << 2).load()]) # 0..28 valid
|
||||
to_uops_list([buf.index(r << 2).load(dtype=dtypes.int)]) # 0..28 valid
|
||||
with self.assertRaises(RuntimeError):
|
||||
to_uops_list([buf.index(r << 4).load()]) # 0..112 oob
|
||||
to_uops_list([buf.index(r << 4).load(dtype=dtypes.int)]) # 0..112 oob
|
||||
|
||||
def test_and(self):
|
||||
with Context(CHECK_OOB=1, SPEC=2):
|
||||
buf = UOp.param(0, dtypes.int, (16,))
|
||||
r = UOp.range(100, 0, AxisType.GLOBAL)
|
||||
to_uops_list([buf.index(r & 15).load()]) # 0..15 valid
|
||||
to_uops_list([buf.index(r & 15).load(dtype=dtypes.int)]) # 0..15 valid
|
||||
with self.assertRaises(RuntimeError):
|
||||
to_uops_list([buf.index(r & 31).load()]) # 0..31 oob
|
||||
to_uops_list([buf.index(r & 31).load(dtype=dtypes.int)]) # 0..31 oob
|
||||
# align masks round down to a multiple of 2^k
|
||||
to_uops_list([buf.index((r & -4).valid(r < 16)).load()]) # 0..12 valid
|
||||
to_uops_list([buf.index((r & -4).valid(r < 16)).load(dtype=dtypes.int)]) # 0..12 valid
|
||||
with self.assertRaises(RuntimeError):
|
||||
to_uops_list([buf.index(r & -2).load()]) # 0..100 oob
|
||||
to_uops_list([buf.index(r & -2).load(dtype=dtypes.int)]) # 0..100 oob
|
||||
# other masks can't be modeled as mod
|
||||
with self.assertRaisesRegex(RuntimeError, "z3 int AND only supports"):
|
||||
to_uops_list([buf.index(r & 21).load()])
|
||||
to_uops_list([buf.index(r & 21).load(dtype=dtypes.int)])
|
||||
|
||||
def test_max(self):
|
||||
with Context(CHECK_OOB=1, SPEC=2):
|
||||
buf = UOp.param(0, dtypes.int, (16,))
|
||||
to_uops_list([buf.index(Variable("v", -10, 15).maximum(0)).load()]) # 0..15 valid
|
||||
to_uops_list([buf.index(Variable("v", -10, 15).maximum(0)).load(dtype=dtypes.int)]) # 0..15 valid
|
||||
with self.assertRaises(RuntimeError):
|
||||
to_uops_list([buf.index(Variable("v2", -10, 20).maximum(0)).load()]) # 0..20 oob
|
||||
to_uops_list([buf.index(Variable("v2", -10, 20).maximum(0)).load(dtype=dtypes.int)]) # 0..20 oob
|
||||
|
||||
def test_xor_in_mask(self):
|
||||
with Context(CHECK_OOB=1, SPEC=2):
|
||||
buf = UOp.param(0, dtypes.int, (16,))
|
||||
r = UOp.range(32, 0, AxisType.GLOBAL)
|
||||
to_uops_list([buf.index(r.valid((r < 8) ^ ((r >= 8) & (r < 16)))).load()]) # 0..15 valid
|
||||
to_uops_list([buf.index(r.valid((r < 8) ^ ((r >= 8) & (r < 16)))).load(dtype=dtypes.int)]) # 0..15 valid
|
||||
with self.assertRaises(RuntimeError):
|
||||
to_uops_list([buf.index(r.valid((r < 10) ^ (r >= 20))).load()]) # 0..9,20..31 oob
|
||||
to_uops_list([buf.index(r.valid((r < 10) ^ (r >= 20))).load(dtype=dtypes.int)]) # 0..9,20..31 oob
|
||||
|
||||
# cast patterns
|
||||
def test_float_cast_in_index(self):
|
||||
@@ -121,13 +121,13 @@ class TestValidateOOB(unittest.TestCase):
|
||||
buf = UOp.param(0, dtypes.int, (16,))
|
||||
r = UOp.range(20, 0)
|
||||
i = (r.cast(dtypes.float) * 0.68).trunc().cast(dtypes.int)
|
||||
to_uops_list([buf.index(i.valid((i >= 0) & (i < 16))).load()])
|
||||
to_uops_list([buf.index(i.valid((i >= 0) & (i < 16))).load(dtype=dtypes.int)])
|
||||
|
||||
def test_bool_cast_in_mask(self):
|
||||
with Context(CHECK_OOB=1, SPEC=2):
|
||||
buf = UOp.param(0, dtypes.int, (1,))
|
||||
r = UOp.range(20, 0)
|
||||
to_uops_list([buf.index(r.valid(r.cast(dtypes.bool).logical_not())).load()]) # only r=0 valid
|
||||
to_uops_list([buf.index(r.valid(r.cast(dtypes.bool).logical_not())).load(dtype=dtypes.int)]) # only r=0 valid
|
||||
|
||||
# load result as index/mask
|
||||
def test_load_as_index(self):
|
||||
@@ -135,18 +135,18 @@ class TestValidateOOB(unittest.TestCase):
|
||||
buf0 = UOp.param(0, dtypes.int, (16,))
|
||||
buf1 = UOp.param(1, dtypes.int, (64,))
|
||||
r = UOp.range(42, 0, AxisType.GLOBAL)
|
||||
ld0 = buf0.index(r.valid(r < 8)).load().cast(dtypes.weakint)
|
||||
to_uops_list([buf1.index((ld0 * 2).valid((ld0 >= 0) & (ld0 < 32))).load()]) # valid
|
||||
ld0 = buf0.index(r.valid(r < 8)).load(dtype=dtypes.int).cast(dtypes.weakint)
|
||||
to_uops_list([buf1.index((ld0 * 2).valid((ld0 >= 0) & (ld0 < 32))).load(dtype=dtypes.int)]) # valid
|
||||
with self.assertRaises(RuntimeError):
|
||||
to_uops_list([buf1.index((ld0 * 2).valid((ld0 >= 0) & (ld0 < 64))).load()]) # oob
|
||||
to_uops_list([buf1.index((ld0 * 2).valid((ld0 >= 0) & (ld0 < 64))).load(dtype=dtypes.int)]) # oob
|
||||
|
||||
def test_load_from_shrink_as_index(self):
|
||||
with Context(CHECK_OOB=1, SPEC=2):
|
||||
buf0 = UOp.param(0, dtypes.int, (16,))
|
||||
buf1 = UOp.param(1, dtypes.int, (64,))
|
||||
shrink = UOp(Ops.SHRINK, src=(buf0, UOp.const(0, dtypes.int), UOp.const(4)))
|
||||
ld0 = shrink.load().index(0)
|
||||
to_uops_list([buf1.index(ld0.valid((ld0 >= 0) & (ld0 < 64))).load()])
|
||||
ld0 = shrink.load(dtype=dtypes.int).index(0)
|
||||
to_uops_list([buf1.index(ld0.valid((ld0 >= 0) & (ld0 < 64))).load(dtype=dtypes.int)])
|
||||
|
||||
def test_load_bool_as_mask(self):
|
||||
with Context(CHECK_OOB=1, SPEC=2):
|
||||
|
||||
+13
-27
@@ -1,5 +1,5 @@
|
||||
import unittest
|
||||
import decimal, sys, json, contextlib, tempfile, pickle, io, math, pathlib
|
||||
import unittest, decimal, sys, json, contextlib, tempfile, pickle, io, math
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
from typing import Generator
|
||||
|
||||
@@ -185,18 +185,18 @@ class TestViz(unittest.TestCase):
|
||||
@dataclass(frozen=True)
|
||||
class TestStruct:
|
||||
colored_field: str
|
||||
a = UOp(Ops.PYLITERAL, arg=TestStruct(colored("xyz", "magenta")+colored("12345", "blue")))
|
||||
a = UOp(Ops.CUSTOM, arg=TestStruct(colored("xyz", "magenta")+colored("12345", "blue")))
|
||||
a2 = uop_to_json(VizData(), a)[id(a)]
|
||||
self.assertEqual(ansistrip(a2["label"]), f"PYLITERAL\n{TestStruct.__qualname__}(colored_field='xyz12345')")
|
||||
self.assertEqual(ansistrip(a2["label"]), f"CUSTOM\n{TestStruct.__qualname__}(colored_field='xyz12345')")
|
||||
|
||||
def test_colored_label_multiline(self):
|
||||
with save_viz() as viz:
|
||||
arg = colored("x", "green")+"\n"+colored("y", "red")+colored("z", "yellow")+colored("ww\nw", "magenta")
|
||||
src = [Tensor.empty(1).uop for _ in range(10)]
|
||||
a = UOp(Ops.PYLITERAL, src=tuple(src), arg=arg)
|
||||
a = UOp(Ops.CUSTOM, src=tuple(src), arg=arg)
|
||||
exec_rewrite(a, [PatternMatcher([])])
|
||||
a2 = next(viz.get_details(0, 0))["graph"][id(a)]
|
||||
self.assertEqual(ansistrip(a2["label"]), "PYLITERAL\nx\nyzww\nw")
|
||||
self.assertEqual(ansistrip(a2["label"]), "CUSTOM\nx\nyzww\nw")
|
||||
|
||||
def test_inf_loop(self):
|
||||
a = UOp.const(3)
|
||||
@@ -347,7 +347,7 @@ class TestVizGC(unittest.TestCase):
|
||||
init = bufs_allocated()
|
||||
a = UOp.new_buffer("NULL", 10, dtypes.char)
|
||||
a.buffer.allocate()
|
||||
exec_rewrite(UOp(Ops.PYLITERAL, src=(a,), arg=a), [PatternMatcher([])])
|
||||
exec_rewrite(UOp(Ops.CUSTOM, src=(a,), arg=a), [PatternMatcher([])])
|
||||
del a
|
||||
self.assertEqual(bufs_allocated()-init, 0)
|
||||
lst = viz.list_items()
|
||||
@@ -474,7 +474,7 @@ class TestVizIntegration(unittest.TestCase):
|
||||
def custom_fn(X:UOp):
|
||||
X = X.flatten()
|
||||
i = UOp.range(X.numel(), 0)
|
||||
custom_op = UOp(Ops.CUSTOMI, src=(X[i],), arg=("{} + undeclared_name", X.dtype))
|
||||
custom_op = UOp(Ops.CUSTOMI, src=(X[i],), arg="{} + undeclared_name")
|
||||
return X[i].store(custom_op).end(i).sink(arg=KernelInfo(name=f"custom_fn_{X.numel()}"))
|
||||
x = Tensor.custom_kernel(Tensor.empty(1, device="CPU"), fxn=custom_fn)[0]
|
||||
with save_viz() as viz:
|
||||
@@ -516,22 +516,6 @@ class TestVizIntegration(unittest.TestCase):
|
||||
src_render = get_render(viz.data, steps[src_idx]["query"])["src"]
|
||||
self.assertEqual(src, src_render)
|
||||
|
||||
def test_profiler_duplicate_name(self):
|
||||
kernel_name = "duplicate_name"
|
||||
def one(A:UOp): return A[0].store(UOp.const(1.0, dtypes.float)).sink(arg=KernelInfo(kernel_name))
|
||||
def zero(A:UOp): return A[0].store(UOp.const(0.0, dtypes.float)).sink(arg=KernelInfo(kernel_name))
|
||||
with save_viz() as viz:
|
||||
@TinyJit
|
||||
def f(a:Tensor, b:Tensor): return Tensor.custom_kernel(a, fxn=one)[0], Tensor.custom_kernel(b, fxn=zero)[0]
|
||||
a, b = Tensor.empty(4, device="NULL"), Tensor.empty(4, device="NULL")
|
||||
# warmup
|
||||
for _ in range(2): Tensor.realize(*f(a, b))
|
||||
Tensor.realize(*f(a, b))
|
||||
kernels = {i for i,c in enumerate(viz.list_items()) if c["name"] == kernel_name}
|
||||
profile = decode_profile(unwrap(get_profile(viz.data, cpu_events)))
|
||||
events = [e for e in profile["layout"]["NULL"]["events"] if e["name"] == kernel_name]
|
||||
self.assertEqual({e["ref"] for e in events}, kernels)
|
||||
|
||||
from tinygrad.device import ProfileDeviceEvent, ProfileGraphEvent, ProfileGraphEntry
|
||||
from tinygrad.viz.serve import get_profile
|
||||
from tinygrad.viz.cli import decode_profile
|
||||
@@ -835,6 +819,8 @@ from extra.gemm.amd_asm_matmul import Kernel
|
||||
|
||||
@needs_tracked_pm
|
||||
class TestCfg(unittest.TestCase):
|
||||
def setUp(self): self.arch = "gfx1100"
|
||||
|
||||
def get_cfg(self, name:str, k:Kernel):
|
||||
insts = k.finalize()
|
||||
def fxn(out:UOp) -> UOp:
|
||||
@@ -843,7 +829,7 @@ class TestCfg(unittest.TestCase):
|
||||
sink = UOp.sink(out.base, lidx, gidx, arg=KernelInfo(name=name))
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=x) for x in insts]))))
|
||||
with save_viz() as viz:
|
||||
with Context(DEV="NULL::gfx1100"):
|
||||
with Context(DEV=f"NULL::{self.arch}"):
|
||||
out = Tensor.custom_kernel(Tensor.empty(1), fxn=fxn)[0]
|
||||
_ = do_to_program(out.schedule_linear().src[-1].src[0], Device[out.device].renderer)
|
||||
codegen_rewrites = next(s for s in viz.list_items() if s["name"] == name)
|
||||
@@ -1025,8 +1011,8 @@ def run_cli(*cli_args) -> list[dict]:
|
||||
@contextlib.contextmanager
|
||||
def write_files(viz) -> list[str]:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
(r:=pathlib.Path(tmpdir)/"rewrites.pkl").write_bytes(pickle.dumps(viz.data.trace))
|
||||
(p:=pathlib.Path(tmpdir)/"profile.pkl").write_bytes(pickle.dumps(cpu_events))
|
||||
(r:=Path(tmpdir)/"rewrites.pkl").write_bytes(pickle.dumps(viz.data.trace))
|
||||
(p:=Path(tmpdir)/"profile.pkl").write_bytes(pickle.dumps(cpu_events))
|
||||
yield ["--rewrites-path", str(r), "--profile-path", str(p)]
|
||||
|
||||
class TestCLI(unittest.TestCase):
|
||||
|
||||
@@ -21,7 +21,7 @@ class TestBenchLog(unittest.TestCase):
|
||||
# check event list
|
||||
for event in BenchEvent:
|
||||
self.assertEqual(len(_events[event]["wall"]), 1)
|
||||
self.assertGreater(_events[event]["wall"][0][0], 0)
|
||||
self.assertGreater(_events[event]["wall"][0], 0)
|
||||
|
||||
def test_log_double_wall_time(self):
|
||||
for event in BenchEvent:
|
||||
@@ -35,8 +35,8 @@ class TestBenchLog(unittest.TestCase):
|
||||
# check event list
|
||||
for event in BenchEvent:
|
||||
self.assertEqual(len(_events[event]["wall"]), 2)
|
||||
self.assertGreater(_events[event]["wall"][0][0], 0)
|
||||
self.assertGreater(_events[event]["wall"][1][0], 0)
|
||||
self.assertGreater(_events[event]["wall"][0], 0)
|
||||
self.assertGreater(_events[event]["wall"][1], 0)
|
||||
|
||||
@skipIf(_SKIP_KERNEL_TIMING, "ci timing is not accurate")
|
||||
def test_log_single_kernel_time(self):
|
||||
@@ -52,8 +52,8 @@ class TestBenchLog(unittest.TestCase):
|
||||
# check event list
|
||||
for event in BenchEvent:
|
||||
self.assertEqual(len(_events[event]["kernel"]), 1)
|
||||
self.assertLess(_events[event]["kernel"][0][0], wall_times[0])
|
||||
self.assertGreater(_events[event]["kernel"][0][0], 0)
|
||||
self.assertLess(_events[event]["kernel"][0], wall_times[0])
|
||||
self.assertGreater(_events[event]["kernel"][0], 0)
|
||||
|
||||
@skipIf(_SKIP_KERNEL_TIMING, "ci cuda timing is not accurate")
|
||||
def test_interleaved_wall_kernel_time(self):
|
||||
@@ -74,8 +74,8 @@ class TestBenchLog(unittest.TestCase):
|
||||
for event in BenchEvent:
|
||||
self.assertEqual(len(_events[event]["wall"]), 1)
|
||||
self.assertEqual(len(_events[event]["kernel"]), 1)
|
||||
self.assertLess(_events[event]["kernel"][0][0], wall_times[0])
|
||||
self.assertGreater(_events[event]["kernel"][0][0], 0)
|
||||
self.assertLess(_events[event]["kernel"][0], wall_times[0])
|
||||
self.assertGreater(_events[event]["kernel"][0], 0)
|
||||
|
||||
@skipIf(_SKIP_KERNEL_TIMING, "ci cuda timing is not accurate")
|
||||
def test_stacked_wall_kernel_time(self):
|
||||
@@ -93,10 +93,10 @@ class TestBenchLog(unittest.TestCase):
|
||||
for event in BenchEvent:
|
||||
self.assertEqual(len(_events[event]["wall"]), 2)
|
||||
self.assertEqual(len(_events[event]["kernel"]), 2)
|
||||
self.assertLess(_events[event]["kernel"][0][0], _events[event]["wall"][0][0])
|
||||
self.assertGreater(_events[event]["kernel"][0][0], 0)
|
||||
self.assertLess(_events[event]["kernel"][1][0], _events[event]["wall"][1][0])
|
||||
self.assertGreater(_events[event]["kernel"][1][0], 0)
|
||||
self.assertLess(_events[event]["kernel"][0], _events[event]["wall"][0])
|
||||
self.assertGreater(_events[event]["kernel"][0], 0)
|
||||
self.assertLess(_events[event]["kernel"][1], _events[event]["wall"][1])
|
||||
self.assertGreater(_events[event]["kernel"][1], 0)
|
||||
|
||||
def test_log_instant_event(self):
|
||||
for event in InstantBenchEvent:
|
||||
@@ -105,7 +105,7 @@ class TestBenchLog(unittest.TestCase):
|
||||
# check event list
|
||||
for event in InstantBenchEvent:
|
||||
self.assertEqual(len(_events[event]), 1)
|
||||
self.assertEqual(_events[event][0][0], 1000)
|
||||
self.assertEqual(_events[event][0], 1000)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -3,7 +3,6 @@ from tinygrad import Tensor, UOp, dtypes
|
||||
from tinygrad.helpers import Context
|
||||
from tinygrad.uop.ops import Ops
|
||||
from test.helpers import KernelCountException
|
||||
from tinygrad.engine.realize import run_linear
|
||||
|
||||
class TestRingAllReduce(unittest.TestCase):
|
||||
def test_schedule_ring(self):
|
||||
@@ -22,26 +21,13 @@ class TestRingAllReduce(unittest.TestCase):
|
||||
def test_schedule_all2all(self):
|
||||
with Context(ALL2ALL=2):
|
||||
N = 4
|
||||
M = N*100
|
||||
ds = tuple(f"CPU:{i}" for i in range(N))
|
||||
x = Tensor.arange(N*M, dtype=dtypes.float).reshape(N, M)
|
||||
t = (x*x).clone().shard(ds, axis=0).realize()
|
||||
out = t.sum(0).mul(2.).contiguous()
|
||||
linear, var_vals = out.linear_with_vars()
|
||||
t = Tensor.empty(N, N*100).shard(ds, axis=0).realize()
|
||||
linear = t.sum(0).mul(2.0).contiguous().linear_with_vars()[0]
|
||||
copies = [si for si in linear.src if si.src[0].op is Ops.COPY]
|
||||
sinks = [si for si in linear.src if si.src[0].op is Ops.SINK]
|
||||
# N*(N-1) copies for input and output
|
||||
copy_count = N*(N-1)*2
|
||||
if len(copies) != copy_count: raise KernelCountException(copy_count, len(copies))
|
||||
# N*(N-1) shrinks from other devices becoming contigs, N ALU, N extra contig, reassembly (cat), and mul
|
||||
sink_count = (N*(N-1))+(N)+(N)+(1)+(1)
|
||||
if len(sinks) != sink_count: raise KernelCountException(sink_count, len(sinks))
|
||||
# correctness
|
||||
run_linear(linear, var_vals)
|
||||
expected = [2*sum((d*M+i)**2 for d in range(N)) for i in range(M)]
|
||||
dev_nums = Tensor.arange(1, N+1, dtype=dtypes.float).reshape(N, 1).expand(N, M).shard(ds, axis=0)
|
||||
shards = out.reshape(1, M).expand(N, M)+dev_nums
|
||||
self.assertListEqual(shards.tolist(), [[x+d+1 for x in expected] for d in range(N)])
|
||||
if len(copies) != 24: raise KernelCountException(24, len(copies))
|
||||
if len(sinks) != 26: raise KernelCountException(26, len(sinks))
|
||||
|
||||
@Context(RING=0, ALL2ALL=0)
|
||||
def test_schedule_naive(self):
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
import unittest
|
||||
import numpy as np
|
||||
from tinygrad import Device, dtypes, Tensor, TinyJit, GlobalCounters, Variable
|
||||
from tinygrad import dtypes, Tensor, TinyJit, GlobalCounters, Variable
|
||||
from tinygrad.uop.ops import Ops, UOp
|
||||
from tinygrad.helpers import temp, DEV, Context
|
||||
from test.helpers import assert_kernel_count, needs_second_gpu
|
||||
from test.helpers import assert_kernel_count
|
||||
|
||||
N = 200 # has to be bigger than the cache to fail
|
||||
|
||||
@@ -1079,80 +1079,5 @@ class TestBatchNormRunningStats(unittest.TestCase):
|
||||
with Context(TRAINING=1): bn(x).realize()
|
||||
self.assertTrue(bn.running_mean.uop.base.is_realized)
|
||||
|
||||
class TestMultiAssign(unittest.TestCase):
|
||||
device = tuple(f"{Device.DEFAULT}:{i}" for i in range(2))
|
||||
|
||||
@needs_second_gpu
|
||||
def setUp(self): pass
|
||||
|
||||
def test_multi_assign_realized(self):
|
||||
out = Tensor.zeros(4).shard(self.device, 0).contiguous().realize()
|
||||
ones = Tensor.ones(4).shard(self.device, 0).contiguous().realize()
|
||||
out.assign(ones).realize()
|
||||
self.assertListEqual(out.tolist(), [1,1,1,1])
|
||||
|
||||
def test_multi_assign_unrealized(self):
|
||||
out = Tensor.zeros(4).contiguous().realize().shard(self.device, 0)
|
||||
ones = Tensor.ones(4).shard(self.device, 0).contiguous().realize()
|
||||
out.assign(ones).realize()
|
||||
self.assertListEqual(out.tolist(), [1,1,1,1])
|
||||
|
||||
def test_multi_assign_both_unrealized(self):
|
||||
out = Tensor.zeros(4).contiguous().realize().shard(self.device, 0)
|
||||
ones = Tensor.ones(4).contiguous().realize().shard(self.device, 0)
|
||||
out.assign(ones).realize()
|
||||
self.assertListEqual(out.tolist(), [1,1,1,1])
|
||||
|
||||
def test_multi_assign_scalar(self):
|
||||
out = Tensor.ones(4).shard(self.device, 0).contiguous().realize()
|
||||
out.assign(0).realize()
|
||||
self.assertListEqual(out.tolist(), [0,0,0,0])
|
||||
|
||||
def test_multi_assign_const_like(self):
|
||||
out = Tensor.ones(4).shard(self.device, 0).contiguous().realize()
|
||||
out.assign(out.const_like(7)).realize()
|
||||
self.assertListEqual(out.tolist(), [7,7,7,7])
|
||||
|
||||
def test_multi_assign_piece(self):
|
||||
out = Tensor.zeros(4,4).shard(self.device, 0).contiguous().realize()
|
||||
ones = Tensor.ones(4,1).shard(self.device, 0).contiguous().realize()
|
||||
out[:, 2:3].assign(ones).realize()
|
||||
self.assertListEqual(out.tolist(), [[0,0,1,0], [0,0,1,0], [0,0,1,0], [0,0,1,0]])
|
||||
|
||||
def test_multi_assign_piece_noncontig(self):
|
||||
out = Tensor.zeros(4,4).contiguous().realize().shard(self.device, 0).realize()
|
||||
ones = Tensor.ones(4,1).shard(self.device, 0).contiguous().realize()
|
||||
out[:, 2:3].assign(ones).realize()
|
||||
self.assertListEqual(out.tolist(), [[0,0,1,0], [0,0,1,0], [0,0,1,0], [0,0,1,0]])
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_multi_assign_piece_unrealized(self):
|
||||
out = Tensor.zeros(4,4).contiguous().realize().shard(self.device, 0)
|
||||
ones = Tensor.ones(4,1).shard(self.device, 0).contiguous().realize()
|
||||
out[:, 2:3].assign(ones).realize()
|
||||
self.assertListEqual(out.tolist(), [[0,0,1,0], [0,0,1,0], [0,0,1,0], [0,0,1,0]])
|
||||
|
||||
def test_multi_assign_var_offset(self):
|
||||
out = Tensor.zeros(4,4).contiguous().realize().shard(self.device, 0).realize()
|
||||
ones = Tensor.ones(4,1).shard(self.device, 0).contiguous().realize()
|
||||
vi = Variable("i", 0, 3).bind(2)
|
||||
out[:, vi:vi+1].assign(ones).realize()
|
||||
self.assertListEqual(out.tolist(), [[0,0,1,0], [0,0,1,0], [0,0,1,0], [0,0,1,0]])
|
||||
|
||||
def test_multi_assign_var_offset_jit_none(self): self.test_multi_assign_var_offset_jit(None)
|
||||
def test_multi_assign_var_offset_jit(self, shard_axis=0):
|
||||
out = Tensor.zeros(4,6).contiguous().realize().shard(self.device, shard_axis).realize()
|
||||
ones = Tensor.ones(4,1).shard(self.device, shard_axis).contiguous().realize()
|
||||
|
||||
@TinyJit
|
||||
def f(out:Tensor, vi):
|
||||
out[:, vi:vi+1].assign(ones).realize()
|
||||
ones.assign(ones+1).realize()
|
||||
|
||||
vi = Variable("i", 0, 5)
|
||||
for i in range(1,5):
|
||||
GlobalCounters.reset()
|
||||
f(out, vi.bind(i))
|
||||
self.assertListEqual(out.tolist(), [[0,1,2,3,4,0]]*4)
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -5,8 +5,6 @@ from tinygrad.llm.model import (
|
||||
GatedDeltaNetBlock, SSMConfig, TransformerBlock, TransformerConfig,
|
||||
apply_rope as apply_rope_new, precompute_freqs_cis, pairwise_topk,
|
||||
)
|
||||
from tinygrad.llm.kernels.amd import Linear, gated_delta_prefill, amd_custom_kernels_supported
|
||||
from tinygrad.llm.gguf import ggml_data_to_tensor
|
||||
|
||||
def apply_rope(x:Tensor, start_pos:int):
|
||||
B, H, T, Hd = x.shape
|
||||
@@ -14,15 +12,6 @@ def apply_rope(x:Tensor, start_pos:int):
|
||||
freqs_cis = precompute_freqs_cis(Hd, start_pos+T)[start_pos:start_pos+T]
|
||||
return apply_rope_new(x, freqs_cis)
|
||||
|
||||
class TestLinear(unittest.TestCase):
|
||||
def test_recovers_packed_ggml_weight(self):
|
||||
for ggml_type,packed_size,words in ((13, 176, 44), (14, 210, 53), (23, 136, 34)):
|
||||
packed = Tensor.empty(packed_size+4, dtype=dtypes.uint8, device="CPU")[4:]
|
||||
decoded = ggml_data_to_tensor(packed, 256, ggml_type).reshape(1, 256)
|
||||
linear = Linear(256, 1, bias=False)
|
||||
linear.set_quantized(decoded)
|
||||
self.assertEqual((linear.ggml_type, linear.weight.numel()), (ggml_type, words))
|
||||
|
||||
class TestAttention(unittest.TestCase):
|
||||
def test_apply_rope(self):
|
||||
x = Tensor.randn(1, 2, 4, 8, dtype=dtypes.float32)
|
||||
@@ -52,23 +41,6 @@ class TestAttention(unittest.TestCase):
|
||||
np.testing.assert_allclose(block.cache_kv[0, :, :, :seqlen, :].numpy(), expected.numpy(), rtol=1e-5, atol=1e-5)
|
||||
|
||||
class TestGatedDeltaNetBlock(unittest.TestCase):
|
||||
def test_gated_delta_rectangular_state_and_row_decay(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
rng = np.random.default_rng(42)
|
||||
q, k = (rng.normal(size=(1, 1, 3, 32)).astype(np.float32) for _ in range(2))
|
||||
v, beta = rng.normal(size=(1, 1, 3, 4)).astype(np.float32), rng.uniform(size=(1, 1, 3)).astype(np.float32)
|
||||
alpha, initial = rng.uniform(0.8, 1, size=(1, 1, 3, 4)).astype(np.float32), rng.normal(size=(1, 1, 4, 32)).astype(np.float32)
|
||||
expected_state, expected_out = initial.copy(), np.empty_like(v)
|
||||
for t in range(3):
|
||||
previous, av = expected_state.copy(), alpha[:, :, t, :, None]
|
||||
delta = (v[:, :, t] - (previous*k[:, :, t, None]).sum(-1)*alpha[:, :, t]) * beta[:, :, t, None]
|
||||
expected_state = previous*av + delta[..., None]*k[:, :, t, None, :]
|
||||
expected_out[:, :, t] = (previous*q[:, :, t, None]).sum(-1)*alpha[:, :, t] + delta*(q[:, :, t]*k[:, :, t]).sum(-1)
|
||||
state = Tensor(initial).contiguous().realize()
|
||||
out = gated_delta_prefill(Tensor(q), Tensor(k), Tensor(v), Tensor(beta), Tensor(alpha), state).realize()
|
||||
np.testing.assert_allclose(out.numpy(), expected_out, rtol=1e-4, atol=1e-4)
|
||||
np.testing.assert_allclose(state.numpy(), expected_state, rtol=1e-4, atol=1e-4)
|
||||
|
||||
def _tensor_linspace(self, start:float, stop:float, shape:tuple[int, ...]) -> Tensor:
|
||||
return Tensor.linspace(start, stop, int(np.prod(shape)), dtype=dtypes.float32).reshape(*shape)
|
||||
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import struct, unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from tinygrad.runtime.autogen import bnxt
|
||||
from extra.bnxt_driver.bnxtdev import BNXT_BACKING_STORE, BNXTDev, BNXTQP, _queue, _qwrite, ipv4_to_gid
|
||||
|
||||
class FakePCI:
|
||||
def __init__(self): self.next_addr, self.allocations = 0x100000, []
|
||||
def alloc_sysmem(self, size, contiguous=False):
|
||||
pages = [self.next_addr+i*0x1000 for i in range((size+0xfff)//0x1000)]
|
||||
self.next_addr += len(pages)*0x1000
|
||||
self.allocations.append(mem := bytearray(size))
|
||||
return mem, pages
|
||||
|
||||
class FakeDev:
|
||||
def __init__(self): self.pci_dev, self.calls = FakePCI(), []
|
||||
def hwrm(self, name, **fields):
|
||||
self.calls.append((name, fields))
|
||||
typ = fields.get("type", 0)
|
||||
return SimpleNamespace(ctx_init_value=0x5a, ctx_init_offset=4, entry_size=16 if typ == 0 else 4,
|
||||
subtype_valid_cnt=typ == 0, split_entry_0=2, instance_bit_map=5 if typ == 0 else 1, min_num_entries=0)
|
||||
|
||||
class FakeRCFW:
|
||||
def __init__(self): self.calls, self.doorbells = [], []
|
||||
def exec(self, name, **fields):
|
||||
self.calls.append((name, fields))
|
||||
return SimpleNamespace(xid={"create_cq":77, "create_qp":88, "register_mr":0x5678}.get(name, 0))
|
||||
def doorbell(self, *args, **kwargs): self.doorbells.append((args, kwargs))
|
||||
|
||||
class FakeQPDev:
|
||||
def __init__(self): self.pci_dev, self.fw, self.gid_id, self.nq_id = FakePCI(), FakeRCFW(), 9, 41
|
||||
def rcfw(self, *args, **kwargs): return self.fw.exec(*args, **kwargs)
|
||||
def doorbell(self, *args, **kwargs): self.fw.doorbell(*args, **kwargs)
|
||||
|
||||
class TestMemory(unittest.TestCase):
|
||||
def test_cmdq_and_sq_aux(self):
|
||||
dev = FakeDev()
|
||||
cmdq, sq = _queue(dev), _queue(dev, aux=True)
|
||||
self.assertEqual((cmdq["level"], cmdq["base"]), (0, 0x100000))
|
||||
_qwrite(sq, 3, b"ABCDEFGH", aux=True)
|
||||
self.assertEqual(bytes(sq["mem"][0x1018:0x1020]), b"ABCDEFGH")
|
||||
|
||||
def test_f320_backing_layout_and_final_marker(self):
|
||||
self.assertEqual(len(BNXT_BACKING_STORE), 9)
|
||||
dev = FakeDev()
|
||||
small = ((0, 6), (15, 0))
|
||||
with patch("extra.bnxt_driver.bnxtdev.BNXT_BACKING_STORE", small): BNXTDev.setup_backing_store(dev)
|
||||
cfg = [fields for name, fields in dev.calls if name == "func_backing_store_cfg_v2"]
|
||||
self.assertEqual([(x["type"], x["instance"]) for x in cfg], [(0, 0), (0, 2), (15, 0)])
|
||||
self.assertTrue(all(not x["flags"] for x in cfg[:-1]))
|
||||
self.assertEqual(cfg[-1]["flags"], bnxt.FUNC_BACKING_STORE_CFG_V2_REQ_FLAGS_BS_CFG_ALL_DONE)
|
||||
self.assertEqual((dev.pci_dev.allocations[0][4], dev.pci_dev.allocations[0][20]), (0x5a, 0x5a))
|
||||
|
||||
class TestRCFW(unittest.TestCase):
|
||||
def setUp(self):
|
||||
patch("extra.bnxt_driver.bnxtdev.System.memory_barrier").start()
|
||||
self.addCleanup(patch.stopall)
|
||||
|
||||
def test_doorbell_encodes_xid_type_and_index(self):
|
||||
dev = BNXTDev.__new__(BNXTDev)
|
||||
dev.db, dev.db_off = [0]*1024, 0x1000
|
||||
dev.doorbell(0x123456, bnxt.DBC_DBC_TYPE_CQ_ARMALL, 0x456, epoch=1)
|
||||
key = dev.db[0x1000//8]
|
||||
self.assertEqual(key >> 32,
|
||||
0x123456 & bnxt.DBC_DBC_XID_MASK | bnxt.DBC_DBC_PATH_ROCE | bnxt.DBC_DBC_TYPE_CQ_ARMALL | bnxt.BNXT_QPLIB_DBR_VALID)
|
||||
self.assertEqual(key & 0xffffffff, 0x456 | 1<<bnxt.BNXT_QPLIB_DBR_EPOCH_SHIFT)
|
||||
|
||||
def test_command_uses_first_flag(self):
|
||||
dev = BNXTDev.__new__(BNXTDev)
|
||||
dev.bar0, dev.cmdq, dev.creq = [0]*1024, _queue(FakeDev()), _queue(FakeDev())
|
||||
dev.rcfw_first, dev.creq_id = True, 23
|
||||
dev.doorbell = lambda *args: None
|
||||
_qwrite(dev.creq, 0, bytes(bnxt.struct_creq_query_version_resp(type=bnxt.CREQ_BASE_TYPE_QP_EVENT, cookie=0, v=1)))
|
||||
ret = dev.rcfw("query_version")
|
||||
req = bnxt.struct_cmdq_query_version.from_buffer_copy(bytes(dev.cmdq["mem"][:16]))
|
||||
prod = dev.bar0[(bnxt.RCFW_COMM_BASE_OFFSET+bnxt.RCFW_PF_VF_COMM_PROD_OFFSET)//4]
|
||||
self.assertEqual((req.cookie, ret.cookie, prod), (0, 0, 1 | 1<<bnxt.FIRMWARE_FIRST_FLAG))
|
||||
|
||||
class TestFastPath(unittest.TestCase):
|
||||
def test_unified_mr(self):
|
||||
dev = BNXTDev.__new__(BNXTDev)
|
||||
fw = FakeRCFW()
|
||||
dev.pci_dev, dev.rcfw = FakePCI(), fw.exec
|
||||
self.assertEqual(dev.register_mem([0x800000, 0x900000], 0x2000), 0x5678)
|
||||
mr = fw.calls[-1][1]
|
||||
self.assertEqual((mr["flags"], mr["va"], mr["mr_size"], mr["log2_pg_size_lvl"]),
|
||||
(bnxt.CMDQ_REGISTER_MR_FLAGS_ALLOC_MR, 0x800000, 0x2000,
|
||||
1<<bnxt.CMDQ_REGISTER_MR_LVL_SFT | 12<<bnxt.CMDQ_REGISTER_MR_LOG2_PG_SIZE_SFT))
|
||||
|
||||
def test_qp_creation_and_connect_use_f320_layout(self):
|
||||
dev = FakeQPDev()
|
||||
qp = BNXTQP(dev)
|
||||
create = next(fields for name, fields in dev.fw.calls if name == "create_qp")
|
||||
self.assertEqual((create["sq_size"], "rq_size" in create, qp.qpn), (16, False, 88))
|
||||
qp.connect(0x123, ipv4_to_gid("10.0.0.2"), 0x001122334455)
|
||||
rtr, rts = dev.fw.calls[-2][1], dev.fw.calls[-1][1]
|
||||
self.assertEqual((bytes(rtr["dgid"]), bytes(rtr["dest_mac"])),
|
||||
(ipv4_to_gid("10.0.0.2"), bytes.fromhex("001122334455")))
|
||||
self.assertEqual((rtr["modify_mask"], rts["modify_mask"]), (0x41515ad, 0xae005))
|
||||
|
||||
def test_rdma_write_builds_three_slots_and_host_msn(self):
|
||||
qp = BNXTQP.__new__(BNXTQP)
|
||||
qp.dev, qp.qpn = FakeQPDev(), 88
|
||||
qp.sq, qp.sq_psn, qp.msn = _queue(FakeDev(), aux=True), 5, 0
|
||||
qp._poll = lambda timeout: bytes(bnxt.struct_cq_req())
|
||||
qp.rdma_write(0x1122334455667788, 0x99aa, 0x12345000, 0x55aa, 100)
|
||||
hdr = bnxt.struct_sq_rdma_hdr.from_buffer_copy(bytes(qp.sq["mem"][:32]))
|
||||
sge = bnxt.struct_sq_sge.from_buffer_copy(bytes(qp.sq["mem"][32:48]))
|
||||
self.assertEqual((hdr.remote_va, hdr.remote_key, hdr.length, sge.va_or_pa, sge.l_key, sge.size),
|
||||
(0x1122334455667788, 0x99aa, 100, 0x12345000, 0x55aa, 100))
|
||||
self.assertEqual(struct.unpack_from("<Q", qp.sq["mem"], 0x1000)[0], 6<<24 | 5)
|
||||
self.assertEqual(qp.dev.fw.doorbells, [((88, bnxt.DBC_DBC_TYPE_SQ, 3, 0), {})])
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
@@ -1,40 +0,0 @@
|
||||
import ctypes, unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from tinygrad.runtime.autogen import bnxt
|
||||
from extra.bnxt_driver.bnxtdev import BNXT_CHIMP_COMM, BNXT_CHIMP_COMM_TRIGGER, BNXTDev
|
||||
|
||||
class Mailbox:
|
||||
def __init__(self, trigger): self.words, self.trigger = {}, trigger
|
||||
def __setitem__(self, idx, val):
|
||||
self.words[idx] = val
|
||||
if idx == BNXT_CHIMP_COMM_TRIGGER//4: self.trigger()
|
||||
def request(self):
|
||||
base = BNXT_CHIMP_COMM//4
|
||||
return b"".join(self.words.get(base+i, 0).to_bytes(4, "little") for i in range(bnxt.HWRM_MAX_REQ_LEN//4))
|
||||
|
||||
def fake_dev():
|
||||
dev = BNXTDev.__new__(BNXTDev)
|
||||
dev.resp, dev.resp_pa, dev.seq = bytearray(0x1000), [0x6789a000], 0
|
||||
return dev
|
||||
|
||||
def reply(dev, out_type):
|
||||
req = bnxt.struct_hwrm_cmd_hdr.from_buffer_copy(dev.bar0.request())
|
||||
out = out_type(req_type=req.req_type, seq_id=req.seq_id, resp_len=ctypes.sizeof(out_type), valid=1)
|
||||
dev.resp[:ctypes.sizeof(out_type)] = bytes(out)
|
||||
|
||||
class TestHWRM(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.barrier = patch("extra.bnxt_driver.bnxtdev.System.memory_barrier").start()
|
||||
self.addCleanup(patch.stopall)
|
||||
|
||||
def test_request(self):
|
||||
dev = fake_dev()
|
||||
dev.bar0 = Mailbox(lambda: reply(dev, bnxt.struct_hwrm_func_qcaps_output))
|
||||
dev.hwrm("func_qcaps", fid=0xffff)
|
||||
req = bnxt.struct_hwrm_func_qcaps_input.from_buffer_copy(dev.bar0.request())
|
||||
self.assertEqual((req.req_type, req.seq_id, req.resp_addr, req.fid),
|
||||
(bnxt.HWRM_FUNC_QCAPS, 1, dev.resp_pa[0], 0xffff))
|
||||
self.barrier.assert_called_once_with()
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
@@ -224,21 +224,6 @@ class TestCallSchedule(unittest.TestCase):
|
||||
np.testing.assert_equal(x.numpy(), [2, 2, 2])
|
||||
np.testing.assert_equal(y.numpy(), [3, 3, 3])
|
||||
|
||||
def test_precompile_nested_scope_collision(self):
|
||||
# a precompiled function body gets its own positional p{slot} params; they must not be renumbered when the call is
|
||||
# scheduled inside an enclosing realize with a different slot ordering. the store must use this call's Variable
|
||||
cache = Tensor.zeros(16)
|
||||
@function(precompile=True, allow_implicit=True)
|
||||
def store(x:Tensor, sp:UOp) -> Tensor:
|
||||
# update a cache at a symbolic offset, like an attention KV cache update
|
||||
return Tensor(cache.uop.after(cache[sp:sp+x.shape[0]].uop.store(x.uop)))[:sp+x.shape[0]].sum()
|
||||
sp_v, nt_v = UOp.variable("sp", 0, 8), UOp.variable("nt", 1, 8)
|
||||
t = Tensor.arange(16).float().realize()
|
||||
sp, nt = sp_v.bind(0), nt_v.bind(8)
|
||||
store(t[sp:sp+nt].clone().realize(), sp).realize()
|
||||
np.testing.assert_equal(cache.numpy()[:8], t[:8].numpy())
|
||||
np.testing.assert_equal(cache.numpy()[8:], np.zeros(8))
|
||||
|
||||
def test_precompile_schedule_cache_hit(self):
|
||||
"""two instances of the same @function should produce identical function body keys (schedule cache hit)"""
|
||||
@function(precompile=True)
|
||||
|
||||
@@ -4,7 +4,7 @@ from tinygrad import Tensor, dtypes, TinyJit
|
||||
from tinygrad.helpers import Context
|
||||
from tinygrad.dtype import least_upper_float
|
||||
from tinygrad.uop.ops import UOp, Ops, GroupOp, dtype_from_uop, graph_rewrite
|
||||
from tinygrad.uop.weak import pm_commit_weak
|
||||
from tinygrad.uop.weak import pm_lower_index_dtype, pm_commit_weak
|
||||
from tinygrad.uop.symbolic import symbolic_simple
|
||||
from tinygrad.uop.spec import spec_shared, type_verify
|
||||
from tinygrad.engine.jit import JitError
|
||||
@@ -74,7 +74,7 @@ class TestWeakPromotion(unittest.TestCase):
|
||||
recips = [u for u in (x / y)._uop.toposort() if u.op is Ops.RECIPROCAL]
|
||||
self.assertEqual([(u.dtype, u.src[0].dtype) for u in recips], [(dtypes.float32, dtypes.float32)])
|
||||
with Context(DEFAULT_FLOAT=dtypes.float16):
|
||||
committed = graph_rewrite((UOp.const(1).cast(dtypes.int32) + UOp.const(1.0)).cast(dtypes.float32), pm_commit_weak)
|
||||
committed = graph_rewrite((UOp.const(1).cast(dtypes.int32) + UOp.const(1.0)).cast(dtypes.float32), pm_lower_index_dtype, ctx={})
|
||||
self.assertEqual([u.dtype for u in committed.toposort() if u.op is Ops.ADD], [dtypes.float32])
|
||||
|
||||
def test_div_sub_operand_kept_weak(self):
|
||||
@@ -85,7 +85,7 @@ class TestWeakPromotion(unittest.TestCase):
|
||||
def test_cast_weak_expression_commits_at_cast_floor(self):
|
||||
# the floor never narrows: a cast BELOW the default does not pull the compute width down with it
|
||||
with Context(DEFAULT_FLOAT=dtypes.float32):
|
||||
narrowed = graph_rewrite((UOp.const(1.0) + UOp.const(2.0)).cast(dtypes.float16), pm_commit_weak)
|
||||
narrowed = graph_rewrite((UOp.const(1.0) + UOp.const(2.0)).cast(dtypes.float16), pm_lower_index_dtype, ctx={})
|
||||
self.assertEqual((narrowed.dtype, narrowed.src[0].dtype), (dtypes.float16, dtypes.float32))
|
||||
|
||||
def test_cast_weak_expression_value_uses_cast_floor(self):
|
||||
@@ -114,35 +114,27 @@ class TestWeakPromotion(unittest.TestCase):
|
||||
self.assertIsInstance((x + 2).src[1].val, float)
|
||||
self.assertIs(x + UOp.const(2), x + 2)
|
||||
|
||||
def test_index_dtype_ignores_weakness(self):
|
||||
with Context(SPEC=2):
|
||||
idx = UOp.const(0).cast(dtypes.int32)
|
||||
weak = UOp.const(1.0).expand((1,))
|
||||
self.assertEqual(UOp(Ops.INDEX, dtypes.float32, (weak, idx)).dtype, dtypes.float32)
|
||||
with self.assertRaisesRegex(RuntimeError, "bad dtype"): UOp(Ops.INDEX, dtypes.int32, (weak, idx))
|
||||
|
||||
def test_store_weak_value_uses_destination_dtype(self):
|
||||
with Context(DEFAULT_FLOAT=dtypes.float16):
|
||||
dst = UOp.param(0, dtypes.bfloat16, (1,)).index(UOp.const(0).cast(dtypes.int32))
|
||||
gate = UOp.const(True)
|
||||
out = graph_rewrite(dst.store(UOp.const(5.0), gate), pm_commit_weak)
|
||||
out = graph_rewrite(dst.store(UOp.const(5.0), gate), pm_lower_index_dtype, ctx={})
|
||||
# a bare weak CONST commits directly: the pass runs without symbolic, so a CAST here would survive it
|
||||
self.assertEqual((out.src[1], out.src[2]), (UOp.const(5.0, dtypes.bfloat16), gate))
|
||||
|
||||
def test_weak_srcs_commit_only_at_a_concrete_lub(self):
|
||||
weak_lub = UOp(Ops.ADD, src=(UOp.const(1), UOp.const(1.0)))
|
||||
self.assertIs(graph_rewrite(weak_lub, pm_commit_weak), weak_lub)
|
||||
self.assertIs(graph_rewrite(weak_lub, pm_lower_index_dtype, ctx={}), weak_lub)
|
||||
concrete = UOp.const(2.0).cast(dtypes.float16)
|
||||
# the weak arm stays bare: its sibling states the width, so the WHERE already derives float16 for it
|
||||
where = graph_rewrite(UOp(Ops.WHERE, src=(UOp.const(True), concrete, UOp.const(1.0))), pm_commit_weak)
|
||||
self.assertEqual((where.dtype, tuple(x.dtype for x in where.src)), (dtypes.float16, (dtypes.bool, dtypes.float16, dtypes.weakfloat)))
|
||||
|
||||
def test_derivable_const_rounds_at_the_derived_width(self):
|
||||
# re-rounds a derivable const in place (still bare) so value-keyed folds (x*1 -> x, x*-1 -> NEG) still fire
|
||||
x = UOp.param(0, dtypes.float32, (1,)).index(UOp.const(0).cast(dtypes.int32)).load()
|
||||
mul = graph_rewrite(x * UOp.const(-0.9999999893980771), symbolic_simple+pm_commit_weak)
|
||||
self.assertIs(mul.src[1], UOp.const(-1.0))
|
||||
self.assertIs(graph_rewrite(x * UOp.const(1.0000000106), symbolic_simple+pm_commit_weak), x)
|
||||
|
||||
def test_committed_const_conversion_folds_for_native_format(self):
|
||||
folded = graph_rewrite(UOp.const(16256, dtypes.ushort).cast(dtypes.uint), symbolic_simple)
|
||||
self.assertIs(folded, UOp.const(16256, dtypes.uint))
|
||||
# fmt-less targets are lowered by renderer rewrites, where collapsing this pair would cycle with float-intermediate insertion.
|
||||
emulated = UOp.const(1.0, dtypes.float).cast(dtypes.bfloat16)
|
||||
self.assertIs(graph_rewrite(emulated, symbolic_simple), emulated)
|
||||
where = graph_rewrite(UOp(Ops.WHERE, src=(UOp.const(True), concrete, UOp.const(1.0))), pm_lower_index_dtype, ctx={})
|
||||
self.assertEqual(tuple(x.dtype for x in where.src), (dtypes.bool, dtypes.float16, dtypes.float16))
|
||||
|
||||
def test_weak_shift_lhs_commits_the_node(self):
|
||||
# a shift derives its lhs's dtype, so committing the lhs restates the root (WGSL's packed store writes `mask << shift_am`)
|
||||
@@ -183,11 +175,11 @@ class TestWeakPromotion(unittest.TestCase):
|
||||
self.assertEqual(dtype_from_uop(Ops.SHL, (UOp.const(1, dtypes.int8), UOp.const(1, dtypes.uint32)), None), dtypes.int8)
|
||||
self.assertEqual(UOp.const(1).alu(Ops.SHL, UOp.const(1, dtypes.uint)).dtype, dtypes.weakint)
|
||||
self.assertEqual((v & 3).dtype, dtypes.weakint)
|
||||
with self.assertRaises(RuntimeError): (Tensor.const(1.0) << Tensor.const(1.0)).dtype
|
||||
with self.assertRaises(RuntimeError): UOp.const(1, dtypes.int32).alu(Ops.SHL, UOp.const(1, dtypes.float64)).dtype
|
||||
with self.assertRaises(RuntimeError): Tensor.const(1.0) << Tensor.const(1.0)
|
||||
with self.assertRaises(RuntimeError): UOp.const(1, dtypes.int32).alu(Ops.SHL, UOp.const(1, dtypes.float64))
|
||||
for op in (Ops.SHL, Ops.SHR):
|
||||
with self.assertRaises(RuntimeError):
|
||||
UOp.const(1, dtypes.float32).alu(op, UOp.const(1, dtypes.int32)).dtype
|
||||
UOp.const(1, dtypes.float32).alu(op, UOp.const(1, dtypes.int32))
|
||||
# float bitwise builds, the spec rejects it
|
||||
with Context(SPEC=1):
|
||||
f32, wf = UOp.const(1.0, dtypes.float32), UOp.const(1.0)
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import unittest
|
||||
import numpy as np
|
||||
from tinygrad import Tensor, UOp, dtypes, nn
|
||||
from tinygrad.llm.kernels.amd import Linear, amd_custom_kernels_supported, q8_quantize, flash_attention
|
||||
from tinygrad.llm.gguf import ggml_data_to_tensor
|
||||
|
||||
class TestQ8Quantize(unittest.TestCase):
|
||||
def test_word_quant_weights_use_typed_buffer_view(self):
|
||||
for ggml_type, type_size in ((13, 176), (23, 136)):
|
||||
with self.subTest(ggml_type=ggml_type):
|
||||
raw = Tensor(np.zeros(type_size + 4, dtype=np.uint8), device="CPU").contiguous().realize()[4:]
|
||||
decoded = ggml_data_to_tensor(raw, 256, ggml_type).reshape(1, 256)
|
||||
linear = Linear(256, 1, bias=False)
|
||||
linear.set_quantized(decoded)
|
||||
self.assertEqual(linear.ggml_type, ggml_type)
|
||||
self.assertEqual(linear.weight.dtype, dtypes.uint32)
|
||||
self.assertEqual(linear.weight.nbytes(), type_size)
|
||||
self.assertEqual(linear.weight.uop.buf_uop.buffer.offset, 4)
|
||||
|
||||
def test_values_and_scales(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
x = np.linspace(-3.1, 2.7, 64, dtype=np.float32).reshape(2, 32)
|
||||
quant, scale, gsum = q8_quantize(Tensor(x), 2, 32)
|
||||
scale_np = np.maximum(np.max(np.abs(x), axis=-1, keepdims=True) / 127, 1e-8)
|
||||
expected = np.clip(np.rint(x / scale_np), -127, 127).astype(np.int8)
|
||||
np.testing.assert_array_equal(quant.bitcast(dtypes.int8).reshape(2, 32).numpy(), expected)
|
||||
np.testing.assert_allclose(scale.numpy(), scale_np, rtol=1e-6)
|
||||
# xsum holds the two per-16 sums per 32-wide group
|
||||
np.testing.assert_array_equal(gsum.numpy().reshape(2, 2), expected.reshape(2, 2, 16).sum(-1).astype(np.float32))
|
||||
|
||||
def test_q6_linear_compiles(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
rng = np.random.default_rng(42)
|
||||
packed = rng.integers(0, 256, 210, dtype=np.uint8)
|
||||
packed[-2:] = np.array([0.01], dtype=np.float16).view(np.uint8)
|
||||
raw = Tensor(np.pad(packed, (4, 0))).contiguous().realize()[4:]
|
||||
decoded = ggml_data_to_tensor(raw, 256, 14).reshape(1, 256)
|
||||
linear = Linear(256, 1, bias=False)
|
||||
nn.state.load_state_dict(linear, {"weight":decoded}, verbose=False, realize=False)
|
||||
self.assertTrue(np.isfinite(linear(Tensor.randn(1, 256)).realize().item()))
|
||||
# the Q6 weight is repacked: 210-byte blocks padded to 212 (one block = 53 words)
|
||||
self.assertEqual(linear.weight.uop.buf_uop.buffer.nbytes, 53*4)
|
||||
self.assertEqual(linear.weight.dtype, dtypes.uint32)
|
||||
|
||||
def test_q4_k_linear(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
rng = np.random.default_rng(42)
|
||||
in_features, blocks = 2048, 16*2048//256
|
||||
packed = rng.integers(0, 256, blocks*144, dtype=np.uint8)
|
||||
for i in range(blocks): packed[i*144:i*144+4] = np.array([0.01, 0.002], dtype=np.float16).view(np.uint8)
|
||||
raw = Tensor(np.pad(packed, (4, 0))).contiguous().realize()[4:]
|
||||
decoded = ggml_data_to_tensor(raw, 16*in_features, 12).reshape(16, in_features)
|
||||
weight = decoded.numpy()
|
||||
linear = Linear(in_features, 16, bias=False)
|
||||
nn.state.load_state_dict(linear, {"weight":decoded}, verbose=False, realize=False)
|
||||
x = rng.normal(size=(3, in_features)).astype(np.float32)
|
||||
scale = np.maximum(np.abs(x).reshape(3, in_features//32, 32).max(-1, keepdims=True) / 127, 1e-8)
|
||||
xq = np.clip(np.rint(x.reshape(3, in_features//32, 32) / scale), -127, 127) * scale
|
||||
np.testing.assert_allclose(linear(Tensor(x)).numpy(), xq.reshape(3, in_features) @ weight.T, rtol=2e-3, atol=2e-2)
|
||||
self.assertEqual(linear.ggml_type, 12)
|
||||
|
||||
def test_q6_linear_multiple_tokens(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
rng = np.random.default_rng(42)
|
||||
in_features, blocks = 2048, 16*2048//256
|
||||
packed = rng.integers(0, 256, blocks*210, dtype=np.uint8)
|
||||
for i in range(blocks): packed[i*210+208:i*210+210] = np.array([0.01], dtype=np.float16).view(np.uint8)
|
||||
raw = Tensor(np.pad(packed, (4, 0))).contiguous().realize()[4:]
|
||||
decoded = ggml_data_to_tensor(raw, 16*in_features, 14).reshape(16, in_features)
|
||||
weight = decoded.numpy()
|
||||
linear = Linear(in_features, 16, bias=False)
|
||||
nn.state.load_state_dict(linear, {"weight":decoded}, verbose=False, realize=False)
|
||||
x = rng.normal(size=(3, in_features)).astype(np.float32)
|
||||
scale = np.maximum(np.abs(x).reshape(3, in_features//32, 32).max(-1, keepdims=True) / 127, 1e-8)
|
||||
xq = np.clip(np.rint(x.reshape(3, in_features//32, 32) / scale), -127, 127) * scale
|
||||
np.testing.assert_allclose(linear(Tensor(x)).numpy(), xq.reshape(3, in_features) @ weight.T, rtol=2e-3, atol=2e-2)
|
||||
self.assertEqual(linear.ggml_type, 14)
|
||||
|
||||
# symbolic token counts take the padded kernel path and give the same results
|
||||
generic = Linear(in_features, 16, bias=False)
|
||||
nn.state.load_state_dict(generic, {"weight":decoded}, verbose=False, realize=False)
|
||||
sym = Tensor(np.concatenate([x, np.zeros((1, in_features), np.float32)])).contiguous()[:UOp.variable("tokens", 1, 4).bind(3)]
|
||||
np.testing.assert_allclose(generic(sym)[:3].numpy(), xq.reshape(3, in_features) @ weight.T, rtol=2e-3, atol=2e-2)
|
||||
self.assertTrue(generic.use_custom_quant)
|
||||
self.assertEqual(generic.ggml_type, 14)
|
||||
|
||||
def test_attention_uses_physical_cache_length(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
q, k, v = Tensor.zeros(1, 2, 1, 32), Tensor.randn(1, 1, 1, 32), Tensor.randn(1, 1, 1, 32)
|
||||
cache = Tensor.empty(2, 1, 1, 256, 32, dtype=dtypes.half).contiguous()
|
||||
assigned = Tensor(cache.uop.after(cache[:, :, :, 0:1, :].uop.store(Tensor.stack(k, v).cast(dtypes.half).uop)))
|
||||
out = flash_attention(q, assigned, 1).realize()
|
||||
np.testing.assert_allclose(out.numpy(), v.expand(1, 2, 1, 32).numpy(), rtol=2e-2, atol=2e-2)
|
||||
|
||||
def test_prefill_attention_unaligned_start(self):
|
||||
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
||||
rng = np.random.default_rng(42)
|
||||
start_pos = 1718
|
||||
q = Tensor.zeros(1, 8, 32, 128)
|
||||
old_kv = rng.normal(size=(2, 1, 1, start_pos, 128)).astype(np.float32)
|
||||
new_kv = rng.normal(size=(2, 1, 1, 32, 128)).astype(np.float32)
|
||||
cache = Tensor.zeros(2, 1, 1, 2048, 128, dtype=dtypes.half).contiguous()
|
||||
Tensor.realize(cache[:, :, :, :start_pos].assign(Tensor(old_kv).cast(dtypes.half)))
|
||||
sp = UOp.variable("start_pos", 0, 2047).bind(start_pos)
|
||||
assigned = Tensor(cache.uop.after(cache[:, :, :, sp:sp+32, :].uop.store(Tensor(new_kv).cast(dtypes.half).uop)))
|
||||
out = flash_attention(q, assigned, sp+32).realize()
|
||||
values = np.concatenate([old_kv[1, 0, 0], new_kv[1, 0, 0]]).astype(np.float16).astype(np.float32)
|
||||
expected = np.stack([values[:start_pos+i+1].mean(0) for i in range(32)])[None, None].repeat(8, axis=1)
|
||||
np.testing.assert_allclose(out.numpy(), expected, rtol=2e-3, atol=2e-3)
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
@@ -1,8 +1,6 @@
|
||||
import unittest
|
||||
import numpy as np
|
||||
from unittest.mock import patch
|
||||
from tinygrad import Tensor, UOp
|
||||
from tinygrad.nn.state import get_state_dict
|
||||
from tinygrad.schedule import schedule_cache
|
||||
from tinygrad.llm.model import Transformer, TransformerConfig
|
||||
from tinygrad.llm.serve import StreamRouter
|
||||
@@ -44,10 +42,7 @@ class TestTransformerGenerate(unittest.TestCase):
|
||||
return Tensor([[42]])
|
||||
with patch.object(Transformer, '__call__', mock_call):
|
||||
next(model.generate([1, 2, 3, 4, 5, 42, 10]))
|
||||
# resumes from the reused state at position 5 and consumes the 2 new tokens (one chunk or two decode steps)
|
||||
self.assertEqual(calls[0][1], V_START_POS.bind(5))
|
||||
def ntok(shape): return shape[1] if isinstance(shape[1], int) else shape[1].unbind()[1]
|
||||
self.assertEqual(sum(ntok(c[0]) for c in calls), 2)
|
||||
self.assertEqual(calls, [((1, 1), V_START_POS.bind(5)), ((1, 1), V_START_POS.bind(6))])
|
||||
|
||||
def test_recurrent_divergent_prompt_restarts(self):
|
||||
model, calls = Transformer(TEST_CONFIG), []
|
||||
@@ -157,22 +152,6 @@ class TestTransformerGenerate(unittest.TestCase):
|
||||
# 4 tokens, chunk_size=4 -> 1 prefill chunk
|
||||
self.assertEqual(get_prefill_flags(list(range(4)), 4), [True, False, False])
|
||||
|
||||
def test_chunked_prefill_kv_cache_matches_single_chunk(self):
|
||||
config = TransformerConfig(num_blocks=1, dim=8, hidden_dim=16, n_heads=1, n_kv_heads=1, norm_eps=1e-5,
|
||||
vocab_size=32, head_dim=4, rope_theta=1000000, rope_dim=4, qk_norm=4, v_head_dim=4, max_context=16)
|
||||
def model():
|
||||
m = Transformer(config)
|
||||
rng = np.random.RandomState(1234)
|
||||
for t in get_state_dict(m).values():
|
||||
t.assign(Tensor(rng.uniform(-1, 1, t.shape).astype(np.float32))).realize()
|
||||
return m
|
||||
def prefill(m, chunk_size):
|
||||
gen = m.generate(list(range(1, 9)), chunk_size=chunk_size, temperature=0.0)
|
||||
next(gen)
|
||||
return [b.cache_kv.numpy() for b in m.blk]
|
||||
for g, r in zip(prefill(model(), 4), prefill(model(), 8)):
|
||||
np.testing.assert_allclose(g[:, :, :, :8, :], r[:, :, :, :8, :], atol=1e-5)
|
||||
|
||||
def test_kv_cache_resume_matches_fresh(self):
|
||||
model = Transformer(TEST_CONFIG)
|
||||
|
||||
|
||||
@@ -852,6 +852,82 @@ class TestMultiFromUnrenderable(unittest.TestCase):
|
||||
np.testing.assert_equal(ll.numpy(), np.arange(100)+1)
|
||||
|
||||
@unittest.skipIf(not_support_multi_device(), "need multi")
|
||||
class TestMultiAssign(unittest.TestCase):
|
||||
device = tuple(f"{Device.DEFAULT}:{i}" for i in range(2))
|
||||
|
||||
@needs_second_gpu
|
||||
def setUp(self): pass
|
||||
|
||||
def test_multi_assign_realized(self):
|
||||
out = Tensor.zeros(4).shard(self.device, 0).contiguous().realize()
|
||||
ones = Tensor.ones(4).shard(self.device, 0).contiguous().realize()
|
||||
out.assign(ones).realize()
|
||||
self.assertListEqual(out.tolist(), [1,1,1,1])
|
||||
|
||||
def test_multi_assign_unrealized(self):
|
||||
out = Tensor.zeros(4).contiguous().realize().shard(self.device, 0)
|
||||
ones = Tensor.ones(4).shard(self.device, 0).contiguous().realize()
|
||||
out.assign(ones).realize()
|
||||
self.assertListEqual(out.tolist(), [1,1,1,1])
|
||||
|
||||
def test_multi_assign_both_unrealized(self):
|
||||
out = Tensor.zeros(4).contiguous().realize().shard(self.device, 0)
|
||||
ones = Tensor.ones(4).contiguous().realize().shard(self.device, 0)
|
||||
out.assign(ones).realize()
|
||||
self.assertListEqual(out.tolist(), [1,1,1,1])
|
||||
|
||||
def test_multi_assign_scalar(self):
|
||||
out = Tensor.ones(4).shard(self.device, 0).contiguous().realize()
|
||||
out.assign(0).realize()
|
||||
self.assertListEqual(out.tolist(), [0,0,0,0])
|
||||
|
||||
def test_multi_assign_const_like(self):
|
||||
out = Tensor.ones(4).shard(self.device, 0).contiguous().realize()
|
||||
out.assign(out.const_like(7)).realize()
|
||||
self.assertListEqual(out.tolist(), [7,7,7,7])
|
||||
|
||||
def test_multi_assign_piece(self):
|
||||
out = Tensor.zeros(4,4).shard(self.device, 0).contiguous().realize()
|
||||
ones = Tensor.ones(4,1).shard(self.device, 0).contiguous().realize()
|
||||
out[:, 2:3].assign(ones).realize()
|
||||
self.assertListEqual(out.tolist(), [[0,0,1,0], [0,0,1,0], [0,0,1,0], [0,0,1,0]])
|
||||
|
||||
def test_multi_assign_piece_noncontig(self):
|
||||
out = Tensor.zeros(4,4).contiguous().realize().shard(self.device, 0).realize()
|
||||
ones = Tensor.ones(4,1).shard(self.device, 0).contiguous().realize()
|
||||
out[:, 2:3].assign(ones).realize()
|
||||
self.assertListEqual(out.tolist(), [[0,0,1,0], [0,0,1,0], [0,0,1,0], [0,0,1,0]])
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_multi_assign_piece_unrealized(self):
|
||||
out = Tensor.zeros(4,4).contiguous().realize().shard(self.device, 0)
|
||||
ones = Tensor.ones(4,1).shard(self.device, 0).contiguous().realize()
|
||||
out[:, 2:3].assign(ones).realize()
|
||||
self.assertListEqual(out.tolist(), [[0,0,1,0], [0,0,1,0], [0,0,1,0], [0,0,1,0]])
|
||||
|
||||
def test_multi_assign_var_offset(self):
|
||||
out = Tensor.zeros(4,4).contiguous().realize().shard(self.device, 0).realize()
|
||||
ones = Tensor.ones(4,1).shard(self.device, 0).contiguous().realize()
|
||||
vi = Variable("i", 0, 3).bind(2)
|
||||
out[:, vi:vi+1].assign(ones).realize()
|
||||
self.assertListEqual(out.tolist(), [[0,0,1,0], [0,0,1,0], [0,0,1,0], [0,0,1,0]])
|
||||
|
||||
def test_multi_assign_var_offset_jit_none(self): self.test_multi_assign_var_offset_jit(None)
|
||||
def test_multi_assign_var_offset_jit(self, shard_axis=0):
|
||||
out = Tensor.zeros(4,6).contiguous().realize().shard(self.device, shard_axis).realize()
|
||||
ones = Tensor.ones(4,1).shard(self.device, shard_axis).contiguous().realize()
|
||||
|
||||
@TinyJit
|
||||
def f(out:Tensor, vi):
|
||||
out[:, vi:vi+1].assign(ones).realize()
|
||||
ones.assign(ones+1).realize()
|
||||
|
||||
vi = Variable("i", 0, 5)
|
||||
for i in range(1,5):
|
||||
GlobalCounters.reset()
|
||||
f(out, vi.bind(i))
|
||||
self.assertListEqual(out.tolist(), [[0,1,2,3,4,0]]*4)
|
||||
|
||||
@unittest.skipIf(not_support_multi_device(), "need multi")
|
||||
class TestMultiSetitem(unittest.TestCase):
|
||||
device = tuple(f"{Device.DEFAULT}:{i}" for i in range(4))
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from dataclasses import replace, dataclass
|
||||
import itertools, functools
|
||||
from tinygrad.helpers import DISABLE_FAST_IDIV, TRANSCENDENTAL, SPEC, DEBUG, VIZ, IMAGE, NOOPT, EMULATED_DTYPES, NOLOCALS, USE_TC
|
||||
from tinygrad.helpers import ALLOW_TF32, DEFAULT_FLOAT, DEFAULT_INT, NUM_CPU_THREADS, TC_SELECT, TC_OPT, TracingKey, Context, panic
|
||||
from tinygrad.helpers import ALLOW_TF32, DEFAULT_FLOAT, DEFAULT_INT, TracingKey, Context, panic
|
||||
from tinygrad.uop.ops import PatternMatcher, graph_rewrite, UOp, Ops, UPat, rewrite_group, KernelInfo, ProgramInfo, GroupOp, AxisType
|
||||
from tinygrad.uop.weak import pm_lower_weak, pm_commit_weak, pm_cast_const
|
||||
from tinygrad.uop.weak import pm_lower_index_dtype, pm_commit_weak, pm_cast_weak
|
||||
from tinygrad.uop.render import pyrender
|
||||
from tinygrad.uop.spec import type_verify, spec_tensor, spec_program
|
||||
from tinygrad.renderer import Renderer, Estimates
|
||||
@@ -22,11 +22,11 @@ from tinygrad.codegen.opt.postrange import apply_opts
|
||||
from tinygrad.codegen.late.gater import pm_move_gates_from_index
|
||||
from tinygrad.codegen.simplify import pm_simplify_ranges, pm_flatten_range, pm_split_ranges, pm_load_collapse, pm_reduce_unparented
|
||||
from tinygrad.schedule.multi import multi_pm
|
||||
from tinygrad.schedule.prepare import pm_mops
|
||||
from tinygrad.schedule.rangeify import pm_mops
|
||||
from tinygrad.codegen.late.linearizer import CFGContext, pm_split_ends, pm_add_control_flow, linearize
|
||||
from tinygrad.codegen.late.regalloc import LinearScanRegallocContext, pm_regalloc_rewrite
|
||||
from tinygrad.codegen.late.coalesce import memory_coalescing, pm_simplify_add_image
|
||||
from tinygrad.helpers import all_same, all_int, flatten, argsort, partition
|
||||
from tinygrad.helpers import all_same, flatten, argsort, partition
|
||||
from tinygrad.uop.ops import _broadcast_shape, identity_element
|
||||
from tinygrad.schedule.rangeify import BufferizeOpts
|
||||
|
||||
@@ -162,10 +162,9 @@ devectorizer2 = mop_cleanup+pm_mops+PatternMatcher([
|
||||
(UPat(Ops.RESHAPE, dtype=dtypes.void, name="x"), lambda x: x.src[0]),
|
||||
# reshape of a single element shaped value to scalar is an index
|
||||
(UPat(Ops.RESHAPE, name="x"), lambda x: x.src[0].index(0) if x.marg == () and x.src[0].shape == (1,) else None),
|
||||
# EXPAND on scalar -> nested STACKs with the same shape
|
||||
# EXPAND on scalar -> STACK
|
||||
(UPat(Ops.EXPAND, src=(UPat.var("x"), UPat()), name="out"),
|
||||
lambda x,out: functools.reduce(lambda x,s: UOp.stack(*([x]*s)), reversed(out.shape), x)
|
||||
if x.shape == () and all_int(out.shape) and 0 not in out.shape else None),
|
||||
lambda x,out: UOp.stack(*([x]*out.max_numel())) if x.shape == () and out.shape == (out.max_numel(),) else None),
|
||||
])
|
||||
|
||||
def fix_group_for_reduce(x:UOp):
|
||||
@@ -234,11 +233,10 @@ pm_reduce_local = pm_wmma_add+PatternMatcher([
|
||||
(UPat(Ops.SINK, name="sink"), merge_reduce_ends),
|
||||
])+pm_clean_up_group_sink
|
||||
|
||||
def is_shape_changing_bitcast(u:UOp): return u.op is Ops.BITCAST and u.shape != u.src[0].shape
|
||||
def maybe_load(u:UOp): return u.load() if u.addrspace in (AddrSpace.GLOBAL, AddrSpace.LOCAL, AddrSpace.REG) else u
|
||||
pm_add_loads = PatternMatcher([
|
||||
(UPat(GroupOp.Elementwise|{Ops.REDUCE,Ops.WMMA,Ops.STACK}, name="x"),
|
||||
lambda x: None if is_shape_changing_bitcast(x) else x.replace(src=tuple(map(maybe_load, x.src)))),
|
||||
# BITCAST?
|
||||
(UPat(GroupOp.Elementwise|{Ops.REDUCE,Ops.WMMA,Ops.STACK}, name="x"), lambda x: x.replace(src=tuple([maybe_load(u) for u in x.src]))),
|
||||
(UPat(Ops.STORE, name="x"), lambda x: x.replace(src=(x.src[0], maybe_load(x.src[1]))+x.src[2:])),
|
||||
])
|
||||
|
||||
@@ -283,6 +281,10 @@ pm_implicit_barriers = PatternMatcher([
|
||||
(UPat(Ops.END, name="end"), add_war_barrier),
|
||||
])
|
||||
|
||||
pm_casted_consts = PatternMatcher([
|
||||
(UPat(Ops.CONST, dtypes.all, name="c"), lambda c: UOp.cconst(c.val, c.dtype)),
|
||||
])
|
||||
|
||||
def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
|
||||
if VIZ: graph_rewrite(ast, PatternMatcher([]), name="View Base AST")
|
||||
if DEBUG >= 5: print(pyrender(ast))
|
||||
@@ -344,13 +346,11 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
|
||||
|
||||
# extra symbolic before decomp. crashes without this?
|
||||
# NOTE: also run indexing_simplify here, while the index is still weakint and (x+y)*c -> x*c+y*c applies
|
||||
# commit widths minted in this fixpoint before lowering inspects INDEX shapes
|
||||
sink = graph_rewrite(sink, sym+indexing_simplify+pm_commit_weak, name="extra symbolic")
|
||||
sink = graph_rewrite(sink, sym+indexing_simplify, name="extra symbolic")
|
||||
|
||||
# the boundary: required compute dtypes settle here; derivable const edges may stay bare
|
||||
# lower index dtype
|
||||
# NOTE: we need indexing_simplify to remove the cast to long using the Invalid
|
||||
# NOTE: symbolic must NOT be composed here -- pm_data_invalid pushes the weak result CAST into a gated WHERE, remaking the weak node, and it cycles
|
||||
sink = graph_rewrite(sink, pm_lower_weak+indexing_simplify, name="lower all index dtypes")
|
||||
sink = graph_rewrite(sink, symbolic_simple+pm_lower_index_dtype+indexing_simplify, ctx={}, name="lower all index dtypes")
|
||||
|
||||
# final symbolic before decomp
|
||||
sink = graph_rewrite(sink, symbolic, name="final symbolic")
|
||||
@@ -374,12 +374,9 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
|
||||
|
||||
# final rules for the renderer (without sym)
|
||||
extra_matcher = ren.extra_matcher if ren.extra_matcher is not None else PatternMatcher([])
|
||||
pm_final_rewrite = pm_commit_weak+pm_decomp+extra_matcher+pm_split_ends
|
||||
pm_final_rewrite = pm_commit_weak+pm_cast_weak+pm_decomp+extra_matcher+pm_split_ends
|
||||
sink = graph_rewrite(sink, pm_final_rewrite+pm_remove_invalid, ctx=ren, name="final rewrite")
|
||||
|
||||
# commit every const still bare so no renderer reads one
|
||||
sink = graph_rewrite(sink, pm_cast_const, name="cast consts")
|
||||
|
||||
# add implicit barriers (stores/loads through LOCAL memory ordered by AFTER or across loop iterations need workgroup barriers)
|
||||
sink = graph_rewrite(sink, pm_implicit_barriers, name="add implicit barriers")
|
||||
|
||||
@@ -390,6 +387,10 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
|
||||
num_params = len([x for x in sink.toposort() if x.op is Ops.PARAM and x.arg.slot != -1])
|
||||
sink = graph_rewrite(sink, pm_number_params, ctx=[num_params], name="number params with -1", walk=True)
|
||||
|
||||
# spell every literal as a casted const CAST(dt, CONST(value))
|
||||
# TODO: remove once consts are always weak
|
||||
sink = graph_rewrite(sink, pm_casted_consts, name="casted consts", walk=True)
|
||||
|
||||
if VIZ: graph_rewrite(sink, PatternMatcher([]), name="View Output AST")
|
||||
if SPEC: type_verify(sink, spec_program)
|
||||
|
||||
@@ -458,7 +459,7 @@ pm_to_program = PatternMatcher([
|
||||
(UPat(Ops.PROGRAM, src=(UPat(), UPat(Ops.LINEAR), UPat(Ops.SOURCE, name="source")), name="prg"), do_compile),
|
||||
])
|
||||
|
||||
@rewrite_group(name=lambda ast,renderer,ret,**_: TracingKey((k:=ret.src[0].arg).name,(k.function_name, ast, ret.key),ret=renderer), replay=True)
|
||||
@rewrite_group(name=lambda ast,renderer,ret,**kwargs: TracingKey(ret.src[0].arg.name,(ret.src[0].arg.function_name, ast), ret=renderer), replay=True)
|
||||
@Context(ALLOW_DEVICE_USAGE=0)
|
||||
def do_to_program(ast:UOp, renderer:Renderer) -> UOp:
|
||||
"""
|
||||
@@ -487,14 +488,9 @@ def do_to_program(ast:UOp, renderer:Renderer) -> UOp:
|
||||
if VIZ: graph_rewrite(prg, PatternMatcher([]), name="View Program")
|
||||
return prg
|
||||
|
||||
# config affects generated programs and cache keys; context also carries compile-only behavior to workers
|
||||
to_program_config = (NOOPT, EMULATED_DTYPES, NOLOCALS, USE_TC, IMAGE, DISABLE_FAST_IDIV, TRANSCENDENTAL, ALLOW_TF32,
|
||||
DEFAULT_FLOAT, DEFAULT_INT, NUM_CPU_THREADS, TC_SELECT, TC_OPT)
|
||||
to_program_context = (*to_program_config, SPEC, DEBUG)
|
||||
def to_program_key(ast:UOp, renderer:Renderer) -> tuple:
|
||||
return (ast.key, type(renderer), renderer.target, *[x.value for x in to_program_config])
|
||||
|
||||
to_program_cache: dict[tuple, UOp] = {}
|
||||
def to_program(ast:UOp, renderer:Renderer) -> UOp:
|
||||
if (prg:=to_program_cache.get(key:=to_program_key(ast, renderer))) is None: to_program_cache[key] = prg = do_to_program(ast, renderer)
|
||||
config = (NOOPT, EMULATED_DTYPES, NOLOCALS, USE_TC, IMAGE, DISABLE_FAST_IDIV, TRANSCENDENTAL, ALLOW_TF32, DEFAULT_FLOAT, DEFAULT_INT)
|
||||
key = (ast.key, type(renderer), renderer.target, *[x.value for x in config])
|
||||
if (prg:=to_program_cache.get(key)) is None: to_program_cache[key] = prg = do_to_program(ast, renderer)
|
||||
return prg
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from dataclasses import replace
|
||||
from tinygrad.dtype import dtypes, DType, truncate
|
||||
from tinygrad.helpers import flatten, DEBUG, EMULATED_DTYPES
|
||||
from tinygrad.helpers import flatten, DEBUG, EMULATED_DTYPES, Context, SPEC
|
||||
from tinygrad.uop import GroupOp
|
||||
from tinygrad.uop.ops import UOp, UPat, Ops, PatternMatcher, graph_rewrite
|
||||
from tinygrad.uop.weak import commit_weak_consts
|
||||
from tinygrad.uop.ops import UOp, UPat, Ops, PatternMatcher, graph_rewrite, ParamArg
|
||||
from tinygrad.renderer import Renderer
|
||||
from tinygrad.codegen.decomp.transcendental import exponent_bias, shl, shr
|
||||
|
||||
@@ -26,10 +25,10 @@ def l2i(op: Ops, dt: DType, *uops:UOp):
|
||||
match op:
|
||||
case Ops.NEG: return l2i(Ops.SUB, dt, zero, zero, *uops)
|
||||
case Ops.CAST if dt in (dtypes.long, dtypes.ulong) and uops[0].dtype not in dtypes.floats:
|
||||
# the high word is the sign extension, and unsigned and bool sources zero extend
|
||||
# the high word is the sign extension; bool has no sign, test the already-cast low word instead (bool < 0 would promote to weakint)
|
||||
x, lo = uops[0], uops[0].cast(l2i_dt[dt])
|
||||
if x.dtype is dtypes.bool or x.dtype in dtypes.uints: return lo, lo.const_like(0)
|
||||
return lo, (x < x.const_like(0)).where(lo.const_like(-1), lo.const_like(0))
|
||||
sign = lo if x.dtype is dtypes.bool else x
|
||||
return lo, (sign < sign.const_like(0)).where(lo.const_like(-1), lo.const_like(0))
|
||||
case Ops.CAST if dt in (dtypes.long, dtypes.ulong):
|
||||
return (lo:=uops[0].cast(l2i_dt[dt])), (uops[0] / 2**32).cast(l2i_dt[dt]) - ((uops[0] < 0) & lo.ne(0))
|
||||
case Ops.CAST if dt in dtypes.floats:
|
||||
@@ -128,22 +127,19 @@ def f2f_clamp(val:UOp, dt:DType, sat=True) -> UOp:
|
||||
return val.ne(val).where(val, (val < -mx).where(-sat, (mx < val).where(sat, val)))
|
||||
|
||||
def f2f_load(x: UOp, fr:DType, to:DType) -> UOp:
|
||||
storage_idx = graph_rewrite(x.src[0], pm_float_decomp, ctx=(fr, to), bottom_up=True)
|
||||
if (n:=x.max_numel()) == 1: return f2f(storage_idx.load(*x.src[1:]), fr, to)
|
||||
return UOp(Ops.STACK, src=tuple(f2f(reindex(storage_idx, i, 1).load(*x.src[1:]), fr, to) for i in range(n)))
|
||||
if (n:=x.max_numel()) == 1: return f2f(x.replace(dtype=f2f_dt[fr]), fr, to)
|
||||
return UOp(Ops.STACK, src=tuple(f2f(x.replace(dtype=f2f_dt[fr], src=(reindex(x.src[0], i, 1),)), fr, to) for i in range(n)))
|
||||
|
||||
def f2f_store(st, idx, val, fr:DType, to:DType):
|
||||
if (n:=val.max_numel()) == 1: return st.replace(src=(idx, f2f(val.bitcast(f2f_dt[to]), to, fr)))
|
||||
return UOp.group(*(st.replace(src=(reindex(idx, i, 1), f2f(val.index(i).bitcast(f2f_dt[to]), to, fr))) for i in range(n)))
|
||||
|
||||
# tag is the 32-bit word this node becomes - (0 for the low word, 1 for the high, the dtype the consumer wants)
|
||||
pm_long_decomp: PatternMatcher = PatternMatcher([
|
||||
# the decomp's own bottom-up rewrite can mint bare consts mid-flight: word splitting commits them at the long sibling's dtype
|
||||
(UPat(GroupOp.All, name='x'), lambda x: commit_weak_consts(x, next((s.dtype for s in x.src if s.dtype in l2i_dt), None))),
|
||||
(UPat(GroupOp.Defines, tuple(l2i_dt.keys()), src=(UPat.var("sz"),), name="x"), lambda x,sz:
|
||||
UOp(x.op, src=(sz*2,), arg=replace(x.arg, dtype=l2i_dt[x.dtype]), tag=x.tag)),
|
||||
pm_long_decomp = PatternMatcher([
|
||||
(UPat(GroupOp.Defines, src=(UPat.var("sz"),), name="x"), lambda x,sz:
|
||||
x.replace(dtype=l2i_dt[x.dtype], arg=replace(x.arg, dtype=l2i_dt[x.dtype]), src=(sz*2,)) if x.dtype in l2i_dt else None),
|
||||
(UPat(Ops.INDEX, tuple(l2i_dt.keys()), name='x'), lambda x:
|
||||
reindex(x, x.tag[0]).replace(tag=None) if x.tag is not None else None),
|
||||
reindex(x, x.tag[0]).replace(dtype=x.tag[1], tag=None) if x.tag is not None else None),
|
||||
(UPat(Ops.STORE, src=(UPat.var('idx', tuple(l2i_dt.keys())), UPat.var('val')), name='st'), lambda st,idx,val:
|
||||
st.replace(src=(idx.rtag((0, dt:=l2i_dt[idx.dtype])), val.rtag((0, dt)))).group(
|
||||
st.replace(src=(idx.rtag((1, dt)), val.rtag((1, dt))))) if val.tag is None else None),
|
||||
@@ -151,9 +147,6 @@ pm_long_decomp: PatternMatcher = PatternMatcher([
|
||||
split_l2i(ctx, x.op, dt:=l2i_dt[a.dtype], *flatten((s.rtag((0, dt)), s.rtag((1, dt))) for s in x.src))),
|
||||
(UPat(Ops.CAST, tuple(l2i_dt.keys()), src=(UPat.var('a', tuple(l2i_dt.keys())),), name="x"), lambda ctx,a,x:
|
||||
split_l2i(ctx, Ops.BITCAST, l2i_dt[x.dtype], a.rtag((0, dt:=l2i_dt[a.dtype])), a.rtag((1, dt)))[x.tag[0]]),
|
||||
# a const splits by value; the general CAST arm below would drop its high word
|
||||
(UPat(Ops.CAST, src=(UPat(Ops.CONST, name='c'),), tag={(w, dt) for w in (0, 1) for dt in l2i_dt.values()}, name='x'),
|
||||
lambda x,c: UOp.const(truncate[x.tag[1]](c.val >> (32*x.tag[0])), x.tag[1])),
|
||||
(UPat(Ops.CAST, tuple(l2i_dt.keys()), src=(UPat.var('a'),), name="x"), lambda ctx,a,x:
|
||||
split_l2i(ctx, x.op, x.dtype, a)[x.tag[0]] if x.tag is not None else None),
|
||||
(UPat(Ops.CAST, src=(UPat.var('a', tuple(l2i_dt.keys())),), name="x"), lambda ctx,a,x:
|
||||
@@ -166,22 +159,21 @@ pm_long_decomp: PatternMatcher = PatternMatcher([
|
||||
(UPat((*(GroupOp.ALU - GroupOp.Comparison - {Ops.SHL, Ops.SHR, Ops.WHERE}), Ops.BITCAST), tuple(l2i_dt.keys()), name="x"), lambda ctx,x:
|
||||
split_l2i(ctx, x.op, l2i_dt[x.dtype], *flatten((a.rtag((0, l2i_dt[x.dtype])), a.rtag((1, l2i_dt[x.dtype]))) for a in x.src))[x.tag[0]]
|
||||
if x.tag is not None else None),
|
||||
(UPat(Ops.LOAD, tuple(l2i_dt.keys()), src=(UPat.var('idx'),), name='x'), lambda ctx,x,idx:
|
||||
reindex(graph_rewrite(idx, pm_long_decomp, ctx=ctx, bottom_up=True), x.tag[0]).replace(tag=None).load() if x.tag is not None else None)
|
||||
(UPat(Ops.LOAD, tuple(l2i_dt.keys()), src=(UPat.var('idx'),), name='x'), lambda x,idx:
|
||||
x.replace(dtype=l2i_dt[x.dtype], src=(reindex(idx, x.tag[0]).replace(dtype=l2i_dt[x.dtype], tag=None),), tag=None) if x.tag is not None else None),
|
||||
(UPat(Ops.CONST, tag={(w, dt) for w in (0, 1) for dt in l2i_dt.values()}, name='x'), lambda x:
|
||||
UOp.const(truncate[x.tag[1]]((x.val >> 32) if x.tag[0] == 1 else (x.val & 0xFFFFFFFF)), x.tag[1]))
|
||||
])
|
||||
|
||||
# float decomposition patterns - ctx is (fr, to) tuple
|
||||
pm_float_decomp: PatternMatcher = PatternMatcher([
|
||||
(UPat(GroupOp.Defines, name="x"), lambda ctx,x:
|
||||
UOp(x.op, src=x.src, arg=replace(x.arg, dtype=f2f_dt[ctx[0]]), tag=ctx[0]) if x.dtype == ctx[0] else None),
|
||||
# INDEX into a LOAD/STACK selects a lane of an already converted value, the load rules below own those
|
||||
(UPat((Ops.INDEX, Ops.SHRINK), src=(UPat(GroupOp.All-{Ops.LOAD, Ops.STACK}),), allow_any_len=True, name="x"), lambda ctx,x:
|
||||
UOp(x.op, src=(graph_rewrite(x.src[0], pm_float_decomp, ctx=ctx, bottom_up=True), *x.src[1:]), arg=x.arg, tag=ctx[0])
|
||||
if x.dtype == ctx[0] else None),
|
||||
pm_float_decomp = PatternMatcher([
|
||||
(UPat((*GroupOp.Defines, Ops.INDEX, Ops.SHRINK), name="x"), lambda ctx,x:
|
||||
x.replace(dtype=f2f_dt[ctx[0]], arg=replace(x.arg, dtype=f2f_dt[ctx[0]]) if isinstance(x.arg, ParamArg) else x.arg, tag=ctx[0])
|
||||
if x.dtype == ctx[0] and (x.op is not Ops.INDEX or x.src[0].op not in {Ops.LOAD, Ops.STACK}) else None),
|
||||
(UPat(Ops.LOAD, dtypes.floats, name="x"), lambda ctx,x: f2f_load(x, *ctx) if x.dtype == ctx[0] else None),
|
||||
# bitcasted load should just replace load
|
||||
(UPat(Ops.BITCAST, src=(UPat(Ops.LOAD, name="ld"),), name="bc"), lambda ctx,bc,ld:
|
||||
graph_rewrite(ld.src[0], pm_float_decomp, ctx=ctx, bottom_up=True).load(*ld.src[1:]).bitcast(bc.dtype) if ld.dtype == ctx[0] else None),
|
||||
ld.replace(dtype=f2f_dt[ctx[0]]).bitcast(bc.dtype) if ld.dtype == ctx[0] else None),
|
||||
# bitcast from
|
||||
(UPat(Ops.BITCAST, src=(UPat.var("x", dtypes.floats),), name="bc"), lambda ctx,bc,x:
|
||||
bc.replace(src=(f2f(x.bitcast(f2f_dt[ctx[1]]), ctx[1], ctx[0]),)) if x.dtype == ctx[1] and bc.dtype.bitsize == ctx[0].bitsize else None),
|
||||
@@ -190,21 +182,26 @@ pm_float_decomp: PatternMatcher = PatternMatcher([
|
||||
f2f(x.bitcast(f2f_dt[ctx[0]]), ctx[0], ctx[1]) if bc.dtype == ctx[0] else None),
|
||||
(UPat(Ops.CAST, dtypes.floats, src=(UPat.var("val"),), name="x"), lambda ctx,x,val:
|
||||
f2f_clamp(val.cast(ctx[1]), ctx[0]) if x.dtype == ctx[0] else None),
|
||||
# a CONST has no srcs to cast, it restates its value at the emulating dtype
|
||||
(UPat(Ops.CONST, dtypes.floats, name="x"), lambda ctx,x: UOp.const(x.val, ctx[1]) if x.dtype == ctx[0] else None),
|
||||
(UPat(GroupOp.All-GroupOp.Defines-{Ops.CAST, Ops.BITCAST, Ops.CONST}, dtypes.floats, name="x"), lambda ctx,x:
|
||||
UOp(x.op, src=tuple(s.cast(ctx[1]) if s.dtype == ctx[0] else s for s in x.src), arg=x.arg, tag=x.tag) if x.dtype == ctx[0] else None),
|
||||
x.replace(dtype=ctx[1], src=tuple(s.cast(ctx[1]) if s.dtype == ctx[0] else s for s in x.src))
|
||||
if x.dtype == ctx[0] else None),
|
||||
(UPat(Ops.STORE, src=(UPat.var("idx"), UPat(Ops.BITCAST, dtypes.floats, name="val")), name='st'), lambda ctx,st,idx,val:
|
||||
st.replace(src=(idx, val.src[0].bitcast(f2f_dt[ctx[0]]))) if val.dtype == ctx[0] and idx.tag == ctx[0] else None),
|
||||
(UPat(Ops.STORE, src=(UPat.var("idx").or_casted(), UPat.var("val", dtypes.floats)), name='st'), lambda ctx,st,idx,val:
|
||||
f2f_store(st, idx, val, *ctx) if val.dtype == ctx[1] and idx.tag == ctx[0] else None),
|
||||
st.replace(src=(idx, val.replace(dtype=f2f_dt[ctx[0]]))) if val.dtype == ctx[0] and idx.tag == ctx[0] else None),
|
||||
(UPat(Ops.STORE, src=(UPat.var("idx"), UPat.var("val", dtypes.floats)), name='st'), lambda ctx,st,idx,val:
|
||||
f2f_store(st, idx, val, *ctx) if val.dtype == ctx[1] and (idx:=idx.src[0] if idx.op == Ops.CAST else idx).tag == ctx[0] else None),
|
||||
])
|
||||
|
||||
def do_dtype_decomps(sink:UOp, ctx:tuple[set[DType], Renderer]) -> UOp:
|
||||
def _should_emulate(dt): return dt in EMULATED_DTYPES.tolist(dtypes) or dt not in ctx[1].supported_dtypes()
|
||||
for fr in sorted(filter(_should_emulate, ctx[0])):
|
||||
to = dtypes.int if fr == dtypes.long else dtypes.half if not _should_emulate(dtypes.half) and fr in dtypes.fp8s else dtypes.float
|
||||
if DEBUG >= 2: print(f"emulating {fr} as {to}")
|
||||
pm = pm_float_decomp if fr in dtypes.floats else pm_long_decomp
|
||||
sink = graph_rewrite(sink, pm, name=f"decomp {fr} -> {to}", ctx={} if pm is pm_long_decomp else (fr, to), bottom_up=True)
|
||||
# NOTE: dtype decomp creates intermediate UOps that don't follow the spec (e.g. half LOAD on ushort BUFFER)
|
||||
with Context(SPEC=min(SPEC.value, 1)):
|
||||
for fr in sorted(filter(_should_emulate, ctx[0])):
|
||||
to = dtypes.int if fr == dtypes.long else dtypes.half if not _should_emulate(dtypes.half) and fr in dtypes.fp8s else dtypes.float
|
||||
if DEBUG >= 2: print(f"emulating {fr} as {to}")
|
||||
pm = pm_float_decomp if fr in dtypes.floats else pm_long_decomp
|
||||
sink = graph_rewrite(sink, pm, name=f"decomp {fr} -> {to}", ctx={} if pm is pm_long_decomp else (fr, to), bottom_up=True)
|
||||
ctx[0].clear()
|
||||
return sink
|
||||
|
||||
|
||||
@@ -75,11 +75,7 @@ powers_of_two: dict[int, int] = {2**i:i for i in range(64)}
|
||||
@functools.cache
|
||||
def get_simplifying_rewrite_patterns(ops:tuple[Ops, ...]) -> PatternMatcher:
|
||||
# these are rewrites that make things simpler
|
||||
pat: list[tuple[UPat, Callable]] = []
|
||||
# FLOORDIV by 2**y -> x >> y (an arithmetic shift is exactly floor division for any sign); fires before floordiv_to_idiv
|
||||
if Ops.SHR in ops: pat.append((UPat.var("x", dtypes.ints)//UPat.cvar("c"),
|
||||
lambda x,c: x >> v if (v:=powers_of_two.get(c.val, 0)) else None))
|
||||
pat.append((UPat.var("a")//UPat.var("b"), floordiv_to_idiv))
|
||||
pat: list[tuple[UPat, Callable]] = [(UPat.var("a")//UPat.var("b"), floordiv_to_idiv)]
|
||||
# FLOORMOD by 2**y -> x & (2**y-1) (correct floor mod for any sign in two's complement); fires before floormod_to_mod
|
||||
if Ops.AND in ops: pat.append((UPat.var("x", dtypes.ints)%UPat.cvar("c"), lambda x,c: x & (c.val-1) if c.val in powers_of_two else None))
|
||||
pat.append((UPat.var("a")%UPat.var("b"), floormod_to_mod))
|
||||
@@ -132,6 +128,6 @@ def get_late_rewrite_patterns(ops:tuple[Ops, ...], disable_fast_idiv:bool) -> Pa
|
||||
if Ops.SHL in ops: pat += [(UPat.var('x').alu(Ops.SHL, UPat.cvar('n'))+UPat.var('c'), lambda x,n,c: x.alu(Ops.MULACC, x.const_like(1<<n.val), c))]
|
||||
# some backends emit FDIV for RECIP, in that case: a*(1/b) -> a/b
|
||||
if Ops.FDIV in ops:
|
||||
pat += [(UPat.var("x").reciprocal(), lambda x: UOp.const(1.0).alu(Ops.FDIV, x))]
|
||||
pat += [(UPat.var("a") * UPat(Ops.FDIV, dtypes.floats, src=(UPat.const(1), UPat.var("b"))), lambda a,b: a.alu(Ops.FDIV, b))]
|
||||
pat += [(UPat.var("x").reciprocal(), lambda x: x.const_like(1).alu(Ops.FDIV, x))]
|
||||
pat += [(UPat.var("a", dtypes.floats) * UPat(Ops.FDIV, dtypes.floats, src=(UPat.const(1), UPat.var("b"))), lambda a,b: a.alu(Ops.FDIV, b))]
|
||||
return PatternMatcher(pat)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user