forked from tinygrad/tinygrad
Compare commits
87
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12aa7a22a2 | ||
|
|
3919ce8427 | ||
|
|
756e82e055 | ||
|
|
cc32aa18db | ||
|
|
77f698e55b | ||
|
|
554d078ac4 | ||
|
|
176377ff6e | ||
|
|
1c3c9e96f6 | ||
|
|
e8a8d99b99 | ||
|
|
dcc2d021e7 | ||
|
|
80bf60d782 | ||
|
|
1cb0600086 | ||
|
|
1bcb6bdc62 | ||
|
|
d716d0d927 | ||
|
|
9216aa494c | ||
|
|
9aa9e11301 | ||
|
|
3fdbb82bfe | ||
|
|
0ccef542e0 | ||
|
|
c655aaf3a2 | ||
|
|
592e3f8363 | ||
|
|
3715006a21 | ||
|
|
d80254c1d9 | ||
|
|
c773891e3f | ||
|
|
0e7ab863a0 | ||
|
|
a57188ea6d | ||
|
|
1707dca3b4 | ||
|
|
eba5b7e750 | ||
|
|
a8ecb73363 | ||
|
|
f55c1a37d2 | ||
|
|
6732d05157 | ||
|
|
e68aa16e3f | ||
|
|
8c3cb00d36 | ||
|
|
c117da9850 | ||
|
|
57d1104a92 | ||
|
|
a1263fadf3 | ||
|
|
e6324d1e1c | ||
|
|
3b6abbd84b | ||
|
|
c63d94e059 | ||
|
|
c89ae6c083 | ||
|
|
2067133732 | ||
|
|
ab68c58759 | ||
|
|
fc214da417 | ||
|
|
0a0b6cb596 | ||
|
|
b8cc74ecf8 | ||
|
|
7064e76bc8 | ||
|
|
0c5307b4f3 | ||
|
|
a4fadcf606 | ||
|
|
c218b4842d | ||
|
|
bd6e70ac15 | ||
|
|
9550378704 | ||
|
|
b3e2f17b24 | ||
|
|
e8ba214b56 | ||
|
|
68b4407fe3 | ||
|
|
d539aaf752 | ||
|
|
8c2bf02d17 | ||
|
|
ca86a42703 | ||
|
|
df3b114fbc | ||
|
|
e37b44d048 | ||
|
|
2cfb421a81 | ||
|
|
c31038ff37 | ||
|
|
49778d9a48 | ||
|
|
72280bb218 | ||
|
|
af2a43c850 | ||
|
|
a1366e2f6c | ||
|
|
0b757bb9bc | ||
|
|
7cbe8e0d15 | ||
|
|
a746861ac0 | ||
|
|
8d2cc64b69 | ||
|
|
cb892e1b92 | ||
|
|
d4a1f39038 | ||
|
|
34c9b9d434 | ||
|
|
901d257a26 | ||
|
|
b757437f64 | ||
|
|
00d6eed43c | ||
|
|
2776c5b369 | ||
|
|
58edff61d9 | ||
|
|
954d4f7797 | ||
|
|
7fe8e350c5 | ||
|
|
42714e1399 | ||
|
|
821e80ff9a | ||
|
|
37a54dc7cf | ||
|
|
e25f86721d | ||
|
|
138fb4a783 | ||
|
|
bfd4048abf | ||
|
|
057a18a07c | ||
|
|
c30bf116b7 | ||
|
|
dfe08dfcf7 |
@@ -4,7 +4,7 @@ inputs:
|
||||
python-version:
|
||||
description: 'Python version to use'
|
||||
required: false
|
||||
default: '' # if you don't set a version, the native python version will be used
|
||||
default: '3.14'
|
||||
key:
|
||||
description: 'Key for the python cache'
|
||||
required: false
|
||||
@@ -42,7 +42,11 @@ inputs:
|
||||
required: false
|
||||
default: 'false'
|
||||
qemu:
|
||||
description: "Install qemu"
|
||||
description: "Install qemu?"
|
||||
required: false
|
||||
default: 'false'
|
||||
ninja:
|
||||
description: "Install ninja?"
|
||||
required: false
|
||||
default: 'false'
|
||||
runs:
|
||||
@@ -55,6 +59,11 @@ runs:
|
||||
echo "OMP_NUM_THREADS=1" >> "$GITHUB_ENV"
|
||||
# no buffers should be over 300MB in CI
|
||||
echo "MAX_BUFFER_SIZE=300000000" >> "$GITHUB_ENV"
|
||||
if [[ "$RUNNER_OS" == "Linux" ]]; then
|
||||
echo "VIRTUAL_ENV=/opt/venv/${{ inputs.python-version }}" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "VIRTUAL_ENV=${{ github.workspace }}/.venv" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
|
||||
@@ -63,7 +72,6 @@ runs:
|
||||
|
||||
- name: Set up Python ${{ inputs.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
if: inputs.python-version != ''
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
|
||||
@@ -105,15 +113,15 @@ runs:
|
||||
if: inputs.deps != ''
|
||||
shell: bash
|
||||
run: |
|
||||
uv venv .venv
|
||||
uv venv --allow-existing --python ${{ inputs.python-version }} "$VIRTUAL_ENV"
|
||||
DEPS="${{ inputs.deps }}"
|
||||
uv pip install --python .venv -e ".[${DEPS// /,}]" ${{ inputs.pydeps }} --torch-backend cpu --extra-index-url https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/Triton-Nightly/pypi/simple/
|
||||
uv pip install --python "$VIRTUAL_ENV" -e ".[${DEPS// /,}]" ${{ inputs.pydeps }} --torch-backend cpu --extra-index-url https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/Triton-Nightly/pypi/simple/
|
||||
- name: Install dependencies in venv (without extra)
|
||||
if: inputs.deps == ''
|
||||
shell: bash
|
||||
run: |
|
||||
uv venv .venv
|
||||
uv pip install --python .venv -e . ${{ inputs.pydeps }}
|
||||
uv venv --allow-existing --python ${{ inputs.python-version }} "$VIRTUAL_ENV"
|
||||
uv pip install --python "$VIRTUAL_ENV" -e . ${{ inputs.pydeps }}
|
||||
- name: Prune uv cache
|
||||
if: github.event_name != 'pull_request'
|
||||
shell: bash
|
||||
@@ -121,16 +129,15 @@ runs:
|
||||
- name: Configure venv
|
||||
shell: bash
|
||||
run: |
|
||||
echo "VIRTUAL_ENV=${{ github.workspace }}/.venv" >> "$GITHUB_ENV"
|
||||
if [[ "$RUNNER_OS" == "Windows" ]]; then
|
||||
echo "${{ github.workspace }}/.venv/Scripts" >> "$GITHUB_PATH"
|
||||
echo "$VIRTUAL_ENV/Scripts" >> "$GITHUB_PATH"
|
||||
else
|
||||
echo "${{ github.workspace }}/.venv/bin" >> "$GITHUB_PATH"
|
||||
echo "$VIRTUAL_ENV/bin" >> "$GITHUB_PATH"
|
||||
fi
|
||||
|
||||
# ******************* apt *******************
|
||||
- name: Setup apt
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true')
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true' || inputs.ninja == 'true')
|
||||
shell: bash
|
||||
run: |
|
||||
sudo mkdir -p /var/cache/apt/archives
|
||||
@@ -158,7 +165,7 @@ runs:
|
||||
echo "deb http://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-20 main" | sudo tee /etc/apt/sources.list.d/llvm.list
|
||||
|
||||
- name: Compute Package List + Hash
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true')
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true' || inputs.ninja == 'true')
|
||||
id: apt-pkgs
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -183,25 +190,29 @@ runs:
|
||||
if [[ "${{ inputs.qemu }}" == "true" ]]; then
|
||||
pkgs+=" qemu-user-static"
|
||||
fi
|
||||
# **** ninja ****
|
||||
if [[ "${{ inputs.ninja }}" == "true" ]]; then
|
||||
pkgs+=" ninja-build"
|
||||
fi
|
||||
|
||||
echo "pkgs=$pkgs" >> "$GITHUB_OUTPUT"
|
||||
echo "hash=$(echo -n "$pkgs" | sha256sum | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cache apt (PR)
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true') && github.event_name == 'pull_request'
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true' || inputs.ninja == 'true') && github.event_name == 'pull_request'
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: /var/cache/apt/archives/
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-apt-${{ steps.apt-pkgs.outputs.hash }}-${{ env.CACHE_VERSION }}
|
||||
- name: Cache apt
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true') && github.event_name != 'pull_request'
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true' || inputs.ninja == 'true') && github.event_name != 'pull_request'
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: /var/cache/apt/archives/
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-apt-${{ steps.apt-pkgs.outputs.hash }}-${{ env.CACHE_VERSION }}
|
||||
|
||||
- name: Run apt Update + Install
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true')
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true' || inputs.ninja == 'true')
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt -qq update || true
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
key: 'autogen'
|
||||
amd: 'true'
|
||||
llvm: 'true'
|
||||
pydeps: 'pyyaml mako'
|
||||
deps: 'autogen'
|
||||
- name: Install autogen support packages
|
||||
run: sudo apt-get install -y --no-install-recommends libclang-20-dev llvm-20-dev hip-dev libusb-1.0-0-dev libdrm-dev liburing-dev
|
||||
- name: Regenerate autogen files
|
||||
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
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
|
||||
@@ -108,10 +108,6 @@ jobs:
|
||||
- name: Setup (NV)
|
||||
if: ${{ matrix.dev == 'NV' }}
|
||||
run: sudo lsof -tQ /dev/nvidia* | { xargs -r sudo kill -9 || true; }
|
||||
- name: Symlink models and datasets
|
||||
run: |
|
||||
mkdir -p weights
|
||||
ln -s /raid/weights/LLaMA-3 weights/LLaMA-3
|
||||
- name: setup staging db
|
||||
if: github.ref == 'refs/heads/update_benchmark_staging'
|
||||
run: |
|
||||
@@ -129,10 +125,6 @@ jobs:
|
||||
# just metal for now
|
||||
if: ${{ matrix.dev == 'METAL' }}
|
||||
run: BENCHMARK_LOG=olmoe JITBEAM=2 IGNORE_BEAM_CACHE=1 python3 -m tinygrad.llm -m olmoe --benchmark --warmup
|
||||
- name: Run LLaMA-3 8B on 4 GPUs with BEAM
|
||||
# only run on machines with multiple gpus
|
||||
if: ${{ matrix.dev != 'METAL' }}
|
||||
run: BENCHMARK_LOG=llama3_beam_4gpu JITBEAM=2 IGNORE_BEAM_CACHE=1 CAPTURE_PROCESS_REPLAY=0 python3 examples/llama3.py --size 8B --shard 4 --model weights/LLaMA-3/8B-SF-DPO/ --benchmark --temperature 0
|
||||
- name: Run process replay tests
|
||||
uses: ./.github/actions/process-replay
|
||||
|
||||
@@ -149,7 +141,7 @@ jobs:
|
||||
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
|
||||
@@ -182,10 +174,6 @@ jobs:
|
||||
# slow on metal
|
||||
if: ${{ matrix.dev != 'METAL' }}
|
||||
run: time BENCHMARK_LOG=cifar DEFAULT_FLOAT=HALF STEPS=1000 TARGET_EVAL_ACC_PCT=93.0 python3 examples/hlb_cifar10.py
|
||||
- name: Run full CIFAR training steps w 6 GPUS
|
||||
# only run on machines with multiple gpus
|
||||
if: ${{ matrix.dev != 'METAL' }}
|
||||
run: time BENCHMARK_LOG=cifar_6gpu CAPTURE_PROCESS_REPLAY=0 DEFAULT_FLOAT=HALF STEPS=350 BS=1536 GPUS=6 TARGET_EVAL_ACC_PCT=93.0 python3 examples/hlb_cifar10.py
|
||||
- name: Run process replay tests
|
||||
uses: ./.github/actions/process-replay
|
||||
|
||||
@@ -202,7 +190,7 @@ jobs:
|
||||
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
|
||||
@@ -227,15 +215,8 @@ 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: Run MLPerf resnet eval on training data
|
||||
run: time BENCHMARK_LOG=resnet_eval MODEL=resnet python3 examples/mlperf/model_eval.py
|
||||
- name: Run 10 MLPerf ResNet50 training steps (1 gpu)
|
||||
run: BENCHMARK_LOG=resnet_10steps DEFAULT_FLOAT=HALF BENCHMARK=10 BS=256 GPUS=1 MODEL=resnet python3 examples/mlperf/model_train.py
|
||||
- name: Run 10 MLPerf ResNet50 training steps (6 gpu)
|
||||
run: BENCHMARK_LOG=resnet_10steps_6gpu CAPTURE_PROCESS_REPLAY=0 DEFAULT_FLOAT=HALF BENCHMARK=10 BS=1536 GPUS=6 MODEL=resnet python3 examples/mlperf/model_train.py
|
||||
- name: Run 10 MLPerf Bert training steps (6 gpu)
|
||||
# TODO: remove BERT_LAYERS once scheduler is fast
|
||||
run: BENCHMARK_LOG=bert_10steps_6gpu CAPTURE_PROCESS_REPLAY=0 DEFAULT_FLOAT=HALF BENCHMARK=10 BS=72 GPUS=6 BERT_LAYERS=2 MODEL=bert python3 examples/mlperf/model_train.py
|
||||
- name: Run process replay tests
|
||||
uses: ./.github/actions/process-replay
|
||||
|
||||
@@ -252,7 +233,7 @@ jobs:
|
||||
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
|
||||
@@ -285,6 +266,60 @@ jobs:
|
||||
- name: Run process replay tests
|
||||
uses: ./.github/actions/process-replay
|
||||
|
||||
multigpubenchmark:
|
||||
name: Multi-GPU Benchmarks (DEV=${{ matrix.dev }})
|
||||
runs-on: [self-hosted, "${{ matrix.dev == 'AMD' && 'tinybox' || 'tinyboxgreen' }}"]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
dev: ['AMD', 'NV']
|
||||
timeout-minutes: 60
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: "0"
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup (AMD)
|
||||
if: ${{ matrix.dev == 'AMD' }}
|
||||
run: |
|
||||
./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: sudo lsof -tQ /dev/nvidia* | { xargs -r sudo kill -9 || true; }
|
||||
- name: Symlink models and datasets
|
||||
run: |
|
||||
mkdir -p weights
|
||||
mkdir -p extra/datasets
|
||||
ln -s /raid/weights/LLaMA-3 weights/LLaMA-3
|
||||
ln -s /raid/datasets/imagenet extra/datasets/imagenet
|
||||
- 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: python3 test/external/process_replay/reset.py
|
||||
- name: Run LLaMA-3 8B on 4 GPUs with BEAM
|
||||
run: BENCHMARK_LOG=llama3_beam_4gpu JITBEAM=2 IGNORE_BEAM_CACHE=1 CAPTURE_PROCESS_REPLAY=0 python3 examples/llama3.py --size 8B --shard 4 --model weights/LLaMA-3/8B-SF-DPO/ --benchmark --temperature 0
|
||||
- name: Run full CIFAR training steps w 6 GPUS
|
||||
run: time BENCHMARK_LOG=cifar_6gpu CAPTURE_PROCESS_REPLAY=0 DEFAULT_FLOAT=HALF STEPS=350 BS=1536 GPUS=6 TARGET_EVAL_ACC_PCT=93.0 python3 examples/hlb_cifar10.py
|
||||
- name: Run MLPerf resnet eval on training data
|
||||
run: time BENCHMARK_LOG=resnet_eval MODEL=resnet python3 examples/mlperf/model_eval.py
|
||||
- name: Run 10 MLPerf ResNet50 training steps (6 gpu)
|
||||
run: BENCHMARK_LOG=resnet_10steps_6gpu CAPTURE_PROCESS_REPLAY=0 DEFAULT_FLOAT=HALF BENCHMARK=10 BS=1536 GPUS=6 MODEL=resnet python3 examples/mlperf/model_train.py
|
||||
- name: Run 10 MLPerf Bert training steps (6 gpu)
|
||||
# TODO: remove BERT_LAYERS once scheduler is fast
|
||||
run: BENCHMARK_LOG=bert_10steps_6gpu CAPTURE_PROCESS_REPLAY=0 DEFAULT_FLOAT=HALF BENCHMARK=10 BS=72 GPUS=6 BERT_LAYERS=2 MODEL=bert python3 examples/mlperf/model_train.py
|
||||
- name: Run process replay tests
|
||||
uses: ./.github/actions/process-replay
|
||||
|
||||
tests:
|
||||
name: Tests (DEV=${{ matrix.dev }})
|
||||
runs-on: [self-hosted, "${{ matrix.dev == 'METAL' && 'macOS' || matrix.dev == 'AMD' && 'tinybox' || 'tinyboxgreen' }}"]
|
||||
@@ -410,7 +445,7 @@ jobs:
|
||||
- name: UsbGPU tiny tests
|
||||
run: sudo -E PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=. GMMU=0 DEV=USB+AMD python3.11 test/test_tiny.py
|
||||
- name: UsbGPU copy speeds
|
||||
run: sudo -E PYTHONDONTWRITEBYTECODE=1 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
|
||||
@@ -541,7 +576,7 @@ jobs:
|
||||
- name: openpilot run_pickle big_driving_supercombo
|
||||
run: BENCHMARK_LOG=usbgpu_openpilot_big_driving_supercombo_run_pickle RUN_PICKLE=1 PICKLE_OOB=1 PYTHONPATH="." GMMU=0 DEV=USB+AMD ASSERT_MIN_STEP_TIME=50 python3 examples/openpilot/compile3.py - openpilot.pkl
|
||||
- name: Test copy speeds
|
||||
run: SIZE=64e6 PYTHONPATH=. GMMU=0 DEV=USB+AMD python3 test/external/external_test_usb_asm24.py TestDevCopySpeeds
|
||||
run: SIZE=64000000 PYTHONPATH=. GMMU=0 DEV=USB+AMD python3 test/external/external_test_usb_asm24.py TestDevCopySpeeds
|
||||
|
||||
driverbenchmarks:
|
||||
name: PCI Driver Benchmark (DEV=${{ matrix.dev }})
|
||||
@@ -563,8 +598,8 @@ jobs:
|
||||
- name: Setup
|
||||
run: |
|
||||
./extra/amdpci/setup_python_cap.sh
|
||||
./extra/hcq/hcq_smi.py ${{ matrix.dev == 'AMD' && 'amd' || 'nv' }} rmmod
|
||||
./extra/hcq/hcq_smi.py ${{ matrix.dev == 'AMD' && 'amd' || 'nv' }} kill_pids
|
||||
./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
|
||||
|
||||
@@ -8,7 +8,7 @@ permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Configure Git Credentials
|
||||
|
||||
@@ -166,7 +166,7 @@ jobs:
|
||||
uses: ./.github/actions/setup-tinygrad
|
||||
with:
|
||||
key: windows-${{ matrix.dev }}-minimal
|
||||
deps: testing_unit
|
||||
deps: testing_minimal
|
||||
pydeps: ${{ matrix.dev == 'WEBGPU' && 'dawn-python' || '' }}
|
||||
- name: Set env
|
||||
shell: bash
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
|
||||
@@ -10,7 +10,7 @@ concurrency:
|
||||
jobs:
|
||||
checkbranch:
|
||||
name: Check PR Branch status
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
branchstat: ${{ steps.brstat.outputs.stat}}
|
||||
steps:
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
needs: checkbranch
|
||||
if: needs.checkbranch.outputs.branchstat == 'false'
|
||||
steps:
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
name: Core Library Line Difference
|
||||
permissions:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
needs: checkbranch
|
||||
if: needs.checkbranch.outputs.branchstat == 'true'
|
||||
steps:
|
||||
|
||||
@@ -31,8 +31,7 @@ jobs:
|
||||
- name: Setup Environment
|
||||
uses: ./.github/actions/setup-tinygrad
|
||||
with:
|
||||
deps: docs
|
||||
pydeps: "capstone torch"
|
||||
deps: "docs testing_minimal"
|
||||
- name: Build wheel and show size
|
||||
run: |
|
||||
uv build --wheel
|
||||
@@ -73,10 +72,7 @@ jobs:
|
||||
deps: testing_unit
|
||||
pydeps: "pillow torchvision expecttest"
|
||||
llvm: 'true'
|
||||
- name: Install ninja
|
||||
run: |
|
||||
sudo apt update || true
|
||||
sudo apt install -y --no-install-recommends ninja-build
|
||||
ninja: 'true'
|
||||
- name: Test ResNet-18
|
||||
run: DEBUG=2 python3 extra/torch_backend/example.py
|
||||
- name: Test one op in torch tests
|
||||
@@ -86,6 +82,23 @@ jobs:
|
||||
- name: Custom tests
|
||||
run: DEV=CPU:LLVM GPUS=4 TINY_BACKEND=1 python3 -m pytest -nauto extra/torch_backend/test.py extra/torch_backend/test_inplace.py extra/torch_backend/test_multigpu.py extra/torch_backend/test_kernel_fusion.py --durations=20
|
||||
|
||||
torchbackendtrain:
|
||||
name: Torch Backend Training
|
||||
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup Environment
|
||||
uses: ./.github/actions/setup-tinygrad
|
||||
with:
|
||||
key: torch-backend-pillow-torchvision-et-pt
|
||||
deps: testing_unit
|
||||
llvm: 'true'
|
||||
ninja: 'true'
|
||||
- name: Test beautiful_mnist in torch with TINY_BACKEND
|
||||
run: STEPS=20 DEV=CPU TARGET_EVAL_ACC_PCT=90.0 MAX_BUFFER_SIZE=0 TINY_BACKEND=1 python3 examples/other_mnist/beautiful_mnist_torch.py
|
||||
|
||||
bepython:
|
||||
name: Python Backend
|
||||
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
|
||||
@@ -491,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: PROFILE=1 SQTT=1 python3 -m pytest -n=auto test/amd/test_sqtt_profiler.py
|
||||
run: VIZ=-2 python3 -m pytest -n=auto test/amd/test_sqtt_profiler.py
|
||||
- name: Run AMD emulated tests on NULL backend
|
||||
env:
|
||||
AMD: 0
|
||||
@@ -666,4 +679,5 @@ jobs:
|
||||
run: |
|
||||
python -c "from tinygrad import Device; assert Device.DEFAULT == 'NULL'"
|
||||
DEBUG=4 python3 test/backend/test_ops.py TestOps.test_add
|
||||
python -m pytest -n=auto test/backend/test_ops.py --durations=20
|
||||
# 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
|
||||
|
||||
@@ -140,7 +140,7 @@ Documentation along with a quick start guide can be found on the [docs website](
|
||||
```python
|
||||
from tinygrad import Tensor
|
||||
|
||||
x = Tensor.eye(3)
|
||||
x = Tensor.eye(3).clone() # clone to make it a buffer
|
||||
y = Tensor([[2.0,0,-2.0]])
|
||||
z = y.matmul(x).sum()
|
||||
z.backward()
|
||||
|
||||
@@ -1742,8 +1742,8 @@ def train_gptoss():
|
||||
)
|
||||
|
||||
for p in optim.params:
|
||||
grad_dtype = dtypes.bfloat16 if p.dtype == FP8_DTYPE else p.dtype
|
||||
p.grad = p.zeros_like(dtype=grad_dtype).contiguous()
|
||||
p.grad = p.zeros_like(dtype=dtypes.bfloat16 if p.dtype == FP8_DTYPE else p.dtype).contiguous()
|
||||
if getattr(p, "_zero2", False): p.grad = optim.optimizers[0]._zero_shard(p.grad)
|
||||
grads = [p.grad for p in optim.params]
|
||||
|
||||
from extra.gemm.cdna_asm_gemm import _mx_block_scale
|
||||
|
||||
@@ -146,6 +146,7 @@ class GPTOSS:
|
||||
return w_q, w_e8.is_param_(False)
|
||||
if moe:
|
||||
qs = [_one(*shape[1:]) for _ in range(shape[0])]
|
||||
for q in qs: q[0]._zero2 = True # grad arrives sharded on the expert axis under ZeRO-2 (moe_gemm)
|
||||
return [q[0] for q in qs], [q[1] for q in qs]
|
||||
return _one(*shape)
|
||||
|
||||
@@ -182,10 +183,12 @@ class GPTOSS:
|
||||
xq, xk = apply_rotary_emb(xq, xk, freqs_cis)
|
||||
xq, xk, xv = xq.cast(dtypes.bfloat16), xk.cast(dtypes.bfloat16), xv.cast(dtypes.bfloat16) # (B,N,H,D)/(B,N,KV,D)
|
||||
|
||||
fa_saves = []
|
||||
if getenv("HK_FLASH_ATTENTION"):
|
||||
from extra.thunder.amd.fa import flash_attention
|
||||
attn, *_ = flash_attention(xq, xk, xv, is_causal=True, write_flat=True, sinks=sinks, window=self.sliding_window if sliding else 0)
|
||||
attn, _, l_vec = flash_attention(xq, xk, xv, is_causal=True, write_flat=True, sinks=sinks, window=self.sliding_window if sliding else 0)
|
||||
attn = attn.reshape(bsz, seqlen, self.n_heads * self.head_dim)
|
||||
fa_saves = [xq, xk, xv, l_vec]
|
||||
elif sliding:
|
||||
attn = self._sliding_attention(xq, xk, xv, sinks)
|
||||
else:
|
||||
@@ -199,7 +202,7 @@ class GPTOSS:
|
||||
attn = (w @ xvm).permute(0, 3, 1, 2, 4).reshape(bsz, seqlen, self.n_heads * self.head_dim)
|
||||
|
||||
out = matmul_mx(attn, wo, wo_scale) + wo_bias
|
||||
return out, [x_normed, rrms, attn]
|
||||
return out, [x_normed, rrms, attn] + fa_saves
|
||||
|
||||
def feed_forward(self, x:Tensor, *, ffn_norm:Tensor, gate:Tensor, gate_bias:Tensor,
|
||||
w_gate_up:Tensor, w_gate_up_scale:Tensor, w_gate_up_bias:Tensor,
|
||||
@@ -220,6 +223,7 @@ class GPTOSS:
|
||||
z = grouped_mx_gemm(_pad_cols(y.cast(dtypes.bfloat16)), (w_down, w_down_scale), r.off)[:, :dim] \
|
||||
+ (onehot @ w_down_bias.float()).cast(dtypes.bfloat16)
|
||||
out = combine(z, r, inp.shape[0], self.experts_per_tok).reshape(bsz, seqlen, dim)
|
||||
return out, [x_normed, rrms, xg, h, y, z, r.weights, r.dest_row, r.off]
|
||||
else:
|
||||
thresh = logits.topk(self.experts_per_tok)[0][..., -1:]
|
||||
weights = (logits >= thresh).where(logits, -float("inf")).softmax(-1)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+25
-2
@@ -1,10 +1,32 @@
|
||||
import functools, pathlib
|
||||
from tinygrad import Tensor, dtypes
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo, AxisType
|
||||
from tinygrad.helpers import getenv
|
||||
from tinygrad.renderer import Estimates
|
||||
from tinygrad.runtime.support.compiler_amd import HIPCCCompiler
|
||||
from extra.gemm.cdna_asm_gemm import quantize_mxfp8, _mx_block_scale, _mx_block_scale_3d
|
||||
|
||||
ZERO_OPTIM = getenv("ZERO_OPTIM", 0)
|
||||
|
||||
def reduce_scatter_devaxis(out:Tensor, shard_axis:int=0) -> Tensor:
|
||||
# out: sharded on the device axis, shape (ndev, *rest); return the device-axis sum left sharded on shard_axis.
|
||||
u = out.uop
|
||||
devs, rest = u.device, u.shape[1:]
|
||||
assert rest[shard_axis] % len(devs) == 0, f"reduce_scatter needs even shards: {rest[shard_axis]} % {len(devs)}"
|
||||
# reach the raw per-device buffer below the UNSHARD, keeping the AFTERs so reads stay ordered after the kernel writes
|
||||
node, barriers = u, []
|
||||
while node.op is not Ops.UNSHARD:
|
||||
if node.op is Ops.AFTER: barriers += node.src[1:]
|
||||
node = node.src[0]
|
||||
mbuf = node.src[0].after(*barriers) if barriers else node.src[0]
|
||||
sz = rest[shard_axis] // len(devs)
|
||||
shards = []
|
||||
for i in range(len(devs)):
|
||||
bounds = tuple((0,s) if a != shard_axis else (i*sz,(i+1)*sz) for a,s in enumerate(rest))
|
||||
contribs = [mbuf.mselect(j).reshape(rest).shrink(bounds).copy_to_device(devs[i]) for j in range(len(devs))]
|
||||
shards.append(functools.reduce(lambda a,b: a.alu(Ops.ADD, b), contribs))
|
||||
return Tensor(UOp.mstack(*shards).unshard(shard_axis, UOp.range(len(devs), -1, AxisType.DEVICE)), device=devs)
|
||||
|
||||
@functools.cache
|
||||
def custom_hk_grouped_mxfp8_gemm(C:UOp, A:UOp, B:UOp, scale_A:UOp, scale_B:UOp, *extra:UOp, dname:str, n_experts:int) -> UOp:
|
||||
M, K = A.shape
|
||||
@@ -58,7 +80,8 @@ def grouped_mx_wgrad(g:Tensor, xg:Tensor, expert_off:Tensor, n_experts:int) -> T
|
||||
out = Tensor(inv.uop.unshard(0), device=g.device) if is_multi else inv
|
||||
out = Tensor.custom_kernel(out, gT, xT, g_si, x_si, expert_off,
|
||||
fxn=functools.partial(custom_hk_grouped_mxfp8_wgrad, dname=dname, n_experts=n_experts))[0]
|
||||
out = out.sum(0) if is_multi else out.squeeze(0)
|
||||
if is_multi and ZERO_OPTIM: out = reduce_scatter_devaxis(out, 0)
|
||||
else: out = out.sum(0) if is_multi else out.squeeze(0)
|
||||
return out.reshape(n_experts, N, K)
|
||||
|
||||
def mx_pack_3d(e8:Tensor) -> Tensor:
|
||||
|
||||
@@ -97,11 +97,13 @@ if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
backend_subparsers = parser.add_subparsers(dest="backend", required=True, metavar="{nv,amd}", help="Hardware backend to target")
|
||||
|
||||
nv_parser = backend_subparsers.add_parser("nv", help="NVIDIA GPUs")
|
||||
nv_parser = backend_subparsers.add_parser("nv", aliases=["NV"], help="NVIDIA GPUs")
|
||||
nv_parser.set_defaults(backend="nv")
|
||||
nv_commands = nv_parser.add_subparsers(dest="command", required=True)
|
||||
add_common_commands(nv_commands)
|
||||
|
||||
amd_parser = backend_subparsers.add_parser("amd", help="AMD GPUs")
|
||||
amd_parser = backend_subparsers.add_parser("amd", aliases=["AMD"], help="AMD GPUs")
|
||||
amd_parser.set_defaults(backend="amd")
|
||||
amd_commands = amd_parser.add_subparsers(dest="command", required=True)
|
||||
add_common_commands(amd_commands)
|
||||
|
||||
|
||||
@@ -179,11 +179,10 @@ class SDMAOps(FastEnum): COPY = auto(); POLL_REGMEM = auto(); FENCE = auto(); TR
|
||||
|
||||
def sdma_copy(ctx, call):
|
||||
sz = call.src[2].max_numel() * call.src[2].dtype.itemsize
|
||||
src_addr, dst_addr = call.src[2].getaddr(ctx.devs), call.src[1].getaddr(ctx.devs)
|
||||
return call.ins(SDMAOps.COPY, src=tuple(UOp.const(x, dtypes.uint32) for off in range(0, sz, ctx.max_copy_size) for x in (
|
||||
ctx.sdma.SDMA_OP_COPY | ctx.sdma.SDMA_PKT_COPY_LINEAR_HEADER_SUB_OP(ctx.sdma.SDMA_SUBOP_COPY_LINEAR),
|
||||
ctx.sdma.SDMA_PKT_COPY_LINEAR_COUNT_COUNT(min(sz-off, ctx.max_copy_size)-1), 0,
|
||||
*data64_le(src_addr+UOp.const(off, dtypes.uint64)), *data64_le(dst_addr+UOp.const(off, dtypes.uint64)))))
|
||||
hdr = ctx.sdma.SDMA_OP_COPY | ctx.sdma.SDMA_PKT_COPY_LINEAR_HEADER_SUB_OP(ctx.sdma.SDMA_SUBOP_COPY_LINEAR)
|
||||
return call.ins(SDMAOps.COPY, src=tuple(x for off in range(0, sz, ctx.max_copy_size) for x in (
|
||||
*(UOp.const(v, dtypes.uint32) for v in (hdr, ctx.sdma.SDMA_PKT_COPY_LINEAR_COUNT_COUNT(min(sz-off, ctx.max_copy_size)-1), 0)),
|
||||
*(a + UOp.const(off, dtypes.uint64) if off else a for a in (call.src[2].getaddr(ctx.devs), call.src[1].getaddr(ctx.devs))))))
|
||||
|
||||
def sdma_wait(ctx, ins, dst, val):
|
||||
op = ctx.sdma.SDMA_OP_POLL_REGMEM | ctx.sdma.SDMA_PKT_POLL_REGMEM_HEADER_FUNC(WAIT_REG_MEM_FUNCTION_GEQ) \
|
||||
@@ -262,10 +261,11 @@ class AMDProgramData:
|
||||
private_segment_size:int; kernargs_segment_size:int; kernargs_alloc_size:int
|
||||
enable_dispatch_ptr:int; enable_private_segment_sgpr:int
|
||||
|
||||
_amd_program_cache:dict[tuple[bytes,str], tuple[AMDProgramData,bytes]] = {}
|
||||
_amd_program_cache:dict[tuple[bytes, tuple[str, ...]], UOp] = {}
|
||||
def amd_build_program(prg:UOp) -> UOp:
|
||||
dev = Device[to_tuple(prg.device)[0]] # TODO: rm this
|
||||
if (cached:=_amd_program_cache.get(key:=(lib:=prg.src[3].arg, dev.device))) is None:
|
||||
# key on the full device tuple: the same lib can be built for different device sets, each needs its own program buffer
|
||||
if (cached:=_amd_program_cache.get(key:=(lib:=prg.src[3].arg, to_tuple(prg.device)))) is None:
|
||||
image, sections, relocs = elf_loader(lib)
|
||||
rodata = next(sh.header.sh_addr for sh in sections if sh.name == ".rodata")
|
||||
for off, sym, typ, addent in relocs:
|
||||
|
||||
+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"].name)}
|
||||
return {"rows":[tuple(v.values()) for v in rows.values()], "cols":columns, "metadata":[summary],"ref":viz_data.ref_map.get(data["prg"].profile_key)}
|
||||
|
||||
def print_data(data:dict) -> None:
|
||||
from tabulate import tabulate
|
||||
|
||||
+27
-57
@@ -19,16 +19,33 @@ def _sharded_empty(shape:Tensor, ref:Tensor, axis:int|None, dtype:DTypeLike|None
|
||||
@functools.cache
|
||||
def custom_fused_qkv_rope_forward(q:UOp, k:UOp, v:UOp, xqkv:UOp, freqs_cis:UOp,
|
||||
device:str, arch:str, B:int, N:int, H:int, H_KV:int, D:int):
|
||||
code = (pathlib.Path(__file__).parent / "fused_qkv_rope.cpp").read_text()
|
||||
threads = 256
|
||||
thread_idx = UOp.special(threads, "lidx0")
|
||||
block_idx_x, block_idx_y = UOp.special(B, "gidx0"), UOp.special(N, "gidx1")
|
||||
sink = UOp.sink(q.base, k.base, v.base, xqkv.base, freqs_cis.base, thread_idx, block_idx_x, block_idx_y,
|
||||
arg=KernelInfo(name="fused_qkv_rope_forward"))
|
||||
compile_args = ["-std=c++20", "-ffast-math", f"-DATTN_B={B}", f"-DATTN_N={N}", f"-DATTN_H={H}",
|
||||
f"-DATTN_H_KV={H_KV}", f"-DATTN_D={D}", f"-DTHREADS_PER_BLOCK={threads}"]
|
||||
lib = HIPCCCompiler(arch, compile_args).compile_cached(code)
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=(*sink.src, sink)), UOp(Ops.SOURCE, arg=code), UOp(Ops.BINARY, arg=lib)))
|
||||
group_size = H // H_KV
|
||||
q, k, v = q.reshape(B, N, H, D), k.reshape(B, N, H_KV, D), v.reshape(B, N, H_KV, D)
|
||||
xqkv = xqkv.reshape(B, N, H_KV, group_size + 2, D)
|
||||
b, n = UOp.range(B, 0), UOp.range(N, 1)
|
||||
pair = UOp.range(D // 2, 2)
|
||||
even = pair * 2
|
||||
c = freqs_cis[0, n, 0, pair, 0].cast(dtypes.float)
|
||||
s = freqs_cis[0, n, 0, pair, 1].cast(dtypes.float)
|
||||
ordered:UOp|None = None
|
||||
for kvh in range(H_KV):
|
||||
q_out, k_out, v_out = (x.after(ordered) if ordered is not None else x for x in (q, k, v))
|
||||
x_in = xqkv.after(ordered) if ordered is not None else xqkv
|
||||
stores:list[UOp] = []
|
||||
for rep in range(group_size):
|
||||
a = x_in[b, n, kvh, rep, even].cast(dtypes.float)
|
||||
bb = x_in[b, n, kvh, rep, even + 1].cast(dtypes.float)
|
||||
h = kvh * group_size + rep
|
||||
stores += [q_out[b, n, h, even].store((a * c - bb * s).cast(q.dtype)), q_out[b, n, h, even + 1].store((a * s + bb * c).cast(q.dtype))]
|
||||
a = x_in[b, n, kvh, group_size, even].cast(dtypes.float)
|
||||
bb = x_in[b, n, kvh, group_size, even + 1].cast(dtypes.float)
|
||||
stores += [k_out[b, n, kvh, even].store((a * c - bb * s).cast(k.dtype)),
|
||||
k_out[b, n, kvh, even + 1].store((a * s + bb * c).cast(k.dtype)),
|
||||
v_out[b, n, kvh, even].store(x_in[b, n, kvh, group_size + 1, even]),
|
||||
v_out[b, n, kvh, even + 1].store(x_in[b, n, kvh, group_size + 1, even + 1])]
|
||||
ordered = UOp.group(*stores)
|
||||
assert ordered is not None
|
||||
return ordered.end(pair, n, b).sink(arg=KernelInfo(name="fused_qkv_rope_forward"))
|
||||
|
||||
@functools.cache
|
||||
def custom_fused_qkv_rope_backward(dxqkv:UOp, dq:UOp, dk:UOp, dv:UOp, freqs_cis:UOp,
|
||||
@@ -109,49 +126,6 @@ def fused_qkv_rope(xqkv:Tensor, freqs_cis:Tensor, n_heads:int, n_kv_heads:int, h
|
||||
def _sharded_empty_like(ref:Tensor, axis:int|None=None) -> Tensor:
|
||||
return _sharded_empty(ref.shape, ref, axis)
|
||||
|
||||
@functools.cache
|
||||
def _windowed_lse(xq:Tensor, xk:Tensor, sinks, W:int) -> Tensor:
|
||||
B, N, H, hd = xq.shape
|
||||
H_KV = xk.shape[2]; R = H // H_KV; nb = N // W; sm = hd ** -0.5
|
||||
q = xq.reshape(B, N, H_KV, R, hd).permute(0, 2, 3, 1, 4).reshape(B, H_KV, R, nb, W, hd).float()
|
||||
k = xk.permute(0, 2, 1, 3).reshape(B, H_KV, 1, nb, W, hd).float()
|
||||
k_prev = k.pad((None, None, None, (1, 0), None, None))[:, :, :, :nb]
|
||||
sc_d = (q @ k.transpose(-1, -2)) * sm
|
||||
sc_p = (q @ k_prev.transpose(-1, -2)) * sm
|
||||
li, lj = Tensor.arange(W).reshape(W, 1), Tensor.arange(W).reshape(1, W)
|
||||
pv = (Tensor.arange(nb).reshape(nb, 1, 1) >= 1)
|
||||
sc_d = (lj <= li).where(sc_d, -float("inf"))
|
||||
sc_p = ((li < lj) & pv).where(sc_p, -float("inf"))
|
||||
m = sc_d.max(-1, keepdim=True).maximum(sc_p.max(-1, keepdim=True))
|
||||
if sinks is not None: m = m.maximum(sinks.reshape(1, H_KV, R, 1, 1, 1).float())
|
||||
denom = (sc_d - m).exp().sum(-1, keepdim=True) + (sc_p - m).exp().sum(-1, keepdim=True)
|
||||
if sinks is not None: denom = denom + (sinks.reshape(1, H_KV, R, 1, 1, 1).float() - m).exp()
|
||||
return (m + denom.log()).reshape(B, H, N).unsqueeze(2) # (B, H, 1, N), matches saved l_vec
|
||||
|
||||
def _windowed_delta(xq:Tensor, xk:Tensor, xv:Tensor, do:Tensor, sinks, W:int) -> Tensor:
|
||||
B, N, H, hd = xq.shape
|
||||
H_KV = xk.shape[2]; R = H // H_KV; nb = N // W; sm = hd ** -0.5
|
||||
q = xq.reshape(B, N, H_KV, R, hd).permute(0, 2, 3, 1, 4).reshape(B, H_KV, R, nb, W, hd).float()
|
||||
k = xk.permute(0, 2, 1, 3).reshape(B, H_KV, 1, nb, W, hd).float()
|
||||
v = xv.permute(0, 2, 1, 3).reshape(B, H_KV, 1, nb, W, hd).float()
|
||||
dob = do.reshape(B, N, H_KV, R, hd).permute(0, 2, 3, 1, 4).reshape(B, H_KV, R, nb, W, hd).float()
|
||||
k_prev = k.pad((None, None, None, (1, 0), None, None))[:, :, :, :nb]
|
||||
v_prev = v.pad((None, None, None, (1, 0), None, None))[:, :, :, :nb]
|
||||
sc_d = (q @ k.transpose(-1, -2)) * sm
|
||||
sc_p = (q @ k_prev.transpose(-1, -2)) * sm
|
||||
li, lj = Tensor.arange(W).reshape(W, 1), Tensor.arange(W).reshape(1, W)
|
||||
pv = (Tensor.arange(nb).reshape(nb, 1, 1) >= 1)
|
||||
sc_d = (lj <= li).where(sc_d, -float("inf"))
|
||||
sc_p = ((li < lj) & pv).where(sc_p, -float("inf"))
|
||||
m = sc_d.max(-1, keepdim=True).maximum(sc_p.max(-1, keepdim=True))
|
||||
if sinks is not None: m = m.maximum(sinks.reshape(1, H_KV, R, 1, 1, 1).float())
|
||||
e_d, e_p = (sc_d - m).exp(), (sc_p - m).exp()
|
||||
denom = e_d.sum(-1, keepdim=True) + e_p.sum(-1, keepdim=True)
|
||||
if sinks is not None: denom = denom + (sinks.reshape(1, H_KV, R, 1, 1, 1).float() - m).exp()
|
||||
o = ((e_d / denom) @ v) + ((e_p / denom) @ v_prev)
|
||||
delta = (dob * o).sum(-1)
|
||||
return delta.reshape(B, H, N).unsqueeze(2)
|
||||
|
||||
def _fa_grad_fxn(B, H, N, D, H_local, H_KV_local, H_KV, B_local, shard_axis, shard_axis_t, single_device, arch, has_sink, window=0):
|
||||
def grad(dou:UOp, ker:UOp) -> tuple:
|
||||
do = Tensor(dou, device=dou.device)
|
||||
@@ -160,8 +134,6 @@ def _fa_grad_fxn(B, H, N, D, H_local, H_KV_local, H_KV, B_local, shard_axis, sha
|
||||
xq = Tensor(ker.src[3], device=ker.src[3].device)
|
||||
xk = Tensor(ker.src[4], device=ker.src[4].device)
|
||||
xv = Tensor(ker.src[5], device=ker.src[5].device)
|
||||
if window:
|
||||
l_vec = _windowed_lse(xq, xk, Tensor(ker.src[6], device=ker.src[6].device) if has_sink else None, window)
|
||||
|
||||
dq = _sharded_empty((B, H, N, D), xq, axis=shard_axis_t)
|
||||
GROUP_SIZE = H_local // H_KV_local
|
||||
@@ -172,8 +144,6 @@ def _fa_grad_fxn(B, H, N, D, H_local, H_KV_local, H_KV, B_local, shard_axis, sha
|
||||
# delta_vec = (do * attn).sum(-1, dtype=dtypes.float32).transpose(1, 2).unsqueeze(-2).detach()
|
||||
delta_vec = _sharded_empty((B, H, 1, N), xq, dtype=dtypes.float32, axis=shard_axis_t)
|
||||
delta_vec, dq = Tensor.custom_kernel(delta_vec, dq, attn, do, fxn=functools.partial(custom_fa_backward_pre, device=single_device, arch=arch, B=B_local, N=N, H=H_local, H_KV=H_KV_local, D=D))[:2]
|
||||
if window:
|
||||
delta_vec = _windowed_delta(xq, xk, xv, do, Tensor(ker.src[6], device=ker.src[6].device) if has_sink else None, window)
|
||||
|
||||
dq, dk_partial, dv_partial = Tensor.custom_kernel(dq, dk_partial, dv_partial, do, xq, xk, xv, l_vec, delta_vec, fxn=functools.partial(custom_fa_backward, device=single_device, arch=arch, B=B_local, N=N, H=H_local, H_KV=H_KV_local, D=D, window=window))[:3]
|
||||
|
||||
|
||||
@@ -269,7 +269,9 @@ __global__ void attend_ker(bf16 *O_ptr, float *L_vec_ptr, bf16 *Q_ptr, bf16 *K_p
|
||||
|
||||
qo_tile<D, float> q_reg_fl;
|
||||
load<1, qo_tile<D, float>, _gl_QKVO>(q_reg_fl, g.Qg, {batch_idx, tile_idx, head_idx, 0});
|
||||
#if !WINDOW
|
||||
mul(q_reg_fl, q_reg_fl, TEMPERATURE_SCALE); // Use sqrtf for clarity
|
||||
#endif
|
||||
copy(q_reg, q_reg_fl);
|
||||
transpose(q_reg_transposed, q_reg);
|
||||
|
||||
@@ -288,6 +290,9 @@ __global__ void attend_ker(bf16 *O_ptr, float *L_vec_ptr, bf16 *Q_ptr, bf16 *K_p
|
||||
zero(att_block[0]);
|
||||
transpose(k_reg_transposed, k_reg);
|
||||
mma_AtB(att_block[0], k_reg_transposed, q_reg_transposed, att_block[0]);
|
||||
#if WINDOW
|
||||
mul(att_block[0], att_block[0], TEMPERATURE_SCALE);
|
||||
#endif
|
||||
__builtin_amdgcn_sched_barrier(0);
|
||||
if constexpr (causal) {
|
||||
const int kv_end_pos = (min_tile + 1) * KV_BLOCK_SIZE;
|
||||
@@ -337,6 +342,9 @@ __global__ void attend_ker(bf16 *O_ptr, float *L_vec_ptr, bf16 *Q_ptr, bf16 *K_p
|
||||
zero(att_block[1]);
|
||||
transpose(k_reg_transposed, k_reg);
|
||||
mma_AtB(att_block[1], k_reg_transposed, q_reg_transposed, att_block[1]);
|
||||
#if WINDOW
|
||||
mul(att_block[1], att_block[1], TEMPERATURE_SCALE);
|
||||
#endif
|
||||
#if WINDOW
|
||||
// window masks interior tiles that causal skips
|
||||
mask_kv_tile(att_block[1], tile_idx, j - 2, neg_inf_v, lane);
|
||||
@@ -401,6 +409,9 @@ __global__ void attend_ker(bf16 *O_ptr, float *L_vec_ptr, bf16 *Q_ptr, bf16 *K_p
|
||||
zero(att_block[0]);
|
||||
transpose(k_reg_transposed, k_reg);
|
||||
mma_AtB(att_block[0], k_reg_transposed, q_reg_transposed, att_block[0]);
|
||||
#if WINDOW
|
||||
mul(att_block[0], att_block[0], TEMPERATURE_SCALE);
|
||||
#endif
|
||||
// Finish softmax for QK1
|
||||
exp2(att_block[1].tiles[1][0], att_block[1].tiles[1][0]);
|
||||
mul(norm_vec, norm_vec, scale_vec);
|
||||
@@ -469,6 +480,9 @@ __global__ void attend_ker(bf16 *O_ptr, float *L_vec_ptr, bf16 *Q_ptr, bf16 *K_p
|
||||
zero(att_block[1]);
|
||||
transpose(k_reg_transposed, k_reg);
|
||||
mma_AtB(att_block[1], k_reg_transposed, q_reg_transposed, att_block[1]);
|
||||
#if WINDOW
|
||||
mul(att_block[1], att_block[1], TEMPERATURE_SCALE);
|
||||
#endif
|
||||
// Finish softmax for QK2
|
||||
exp2(att_block[0].tiles[1][0], att_block[0].tiles[1][0]);
|
||||
mul(norm_vec, norm_vec, scale_vec);
|
||||
@@ -535,6 +549,9 @@ __global__ void attend_ker(bf16 *O_ptr, float *L_vec_ptr, bf16 *Q_ptr, bf16 *K_p
|
||||
zero(att_block[0]);
|
||||
transpose(k_reg_transposed, k_reg);
|
||||
mma_AtB(att_block[0], k_reg_transposed, q_reg_transposed, att_block[0]);
|
||||
#if WINDOW
|
||||
mul(att_block[0], att_block[0], TEMPERATURE_SCALE);
|
||||
#endif
|
||||
// Finish softmax for QK3
|
||||
exp2(att_block[1].tiles[1][0], att_block[1].tiles[1][0]);
|
||||
mul(norm_vec, norm_vec, scale_vec);
|
||||
@@ -597,6 +614,9 @@ __global__ void attend_ker(bf16 *O_ptr, float *L_vec_ptr, bf16 *Q_ptr, bf16 *K_p
|
||||
zero(att_block[1]);
|
||||
transpose(k_reg_transposed, k_reg);
|
||||
mma_AtB(att_block[1], k_reg_transposed, q_reg_transposed, att_block[1]);
|
||||
#if WINDOW
|
||||
mul(att_block[1], att_block[1], TEMPERATURE_SCALE);
|
||||
#endif
|
||||
// Finish softmax for QK4
|
||||
exp2(att_block[0].tiles[1][0], att_block[0].tiles[1][0]);
|
||||
mul(norm_vec, norm_vec, scale_vec);
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
#include <hip/hip_runtime.h>
|
||||
#include <hip/hip_bf16.h>
|
||||
|
||||
#ifndef ATTN_B
|
||||
#define ATTN_B 2
|
||||
#endif
|
||||
#ifndef ATTN_N
|
||||
#define ATTN_N 8192
|
||||
#endif
|
||||
#ifndef ATTN_H
|
||||
#define ATTN_H 32
|
||||
#endif
|
||||
#ifndef ATTN_H_KV
|
||||
#define ATTN_H_KV 8
|
||||
#endif
|
||||
#ifndef ATTN_D
|
||||
#define ATTN_D 128
|
||||
#endif
|
||||
#ifndef THREADS_PER_BLOCK
|
||||
#define THREADS_PER_BLOCK 256
|
||||
#endif
|
||||
|
||||
constexpr int GROUP_SIZE = ATTN_H / ATTN_H_KV;
|
||||
constexpr int HALF_D = ATTN_D / 2;
|
||||
constexpr int PACKED_D = (GROUP_SIZE + 2) * ATTN_D;
|
||||
|
||||
extern "C" __global__ __launch_bounds__(THREADS_PER_BLOCK) void
|
||||
fused_qkv_rope_forward(
|
||||
__hip_bfloat16* __restrict__ q,
|
||||
__hip_bfloat16* __restrict__ k,
|
||||
__hip_bfloat16* __restrict__ v,
|
||||
const __hip_bfloat16* __restrict__ xqkv,
|
||||
const __hip_bfloat16* __restrict__ freqs_cis) {
|
||||
const int b = blockIdx.x;
|
||||
const int n = blockIdx.y;
|
||||
const int bn = b * ATTN_N + n;
|
||||
const int packed_bn = bn * ATTN_H_KV * PACKED_D;
|
||||
const int q_bn = bn * ATTN_H * ATTN_D;
|
||||
const int kv_bn = bn * ATTN_H_KV * ATTN_D;
|
||||
|
||||
if (threadIdx.x < HALF_D) {
|
||||
const int pair = threadIdx.x;
|
||||
const int even = pair << 1;
|
||||
const float c = static_cast<float>(freqs_cis[((n * HALF_D + pair) * 2) + 0]);
|
||||
const float s = static_cast<float>(freqs_cis[((n * HALF_D + pair) * 2) + 1]);
|
||||
|
||||
for (int kvh = 0; kvh < ATTN_H_KV; kvh++) {
|
||||
const int base = packed_bn + kvh * PACKED_D;
|
||||
|
||||
for (int rep = 0; rep < GROUP_SIZE; rep++) {
|
||||
const int qbase = base + rep * ATTN_D;
|
||||
const int h = kvh * GROUP_SIZE + rep;
|
||||
const float a = static_cast<float>(xqkv[qbase + even]);
|
||||
const float bb = static_cast<float>(xqkv[qbase + even + 1]);
|
||||
const int out = q_bn + h * ATTN_D + even;
|
||||
q[out] = static_cast<__hip_bfloat16>(a * c - bb * s);
|
||||
q[out + 1] = static_cast<__hip_bfloat16>(a * s + bb * c);
|
||||
}
|
||||
|
||||
const float a = static_cast<float>(xqkv[base + GROUP_SIZE * ATTN_D + even]);
|
||||
const float bb = static_cast<float>(xqkv[base + GROUP_SIZE * ATTN_D + even + 1]);
|
||||
const int out = kv_bn + kvh * ATTN_D + even;
|
||||
k[out] = static_cast<__hip_bfloat16>(a * c - bb * s);
|
||||
k[out + 1] = static_cast<__hip_bfloat16>(a * s + bb * c);
|
||||
v[out] = xqkv[base + (GROUP_SIZE + 1) * ATTN_D + even];
|
||||
v[out + 1] = xqkv[base + (GROUP_SIZE + 1) * ATTN_D + even + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,7 +209,7 @@ class ST:
|
||||
return cls(uop, rows, cols, layout, base_shape, ker)
|
||||
|
||||
def swizzle(self, row, col):
|
||||
swizzled_offset = self.base_shape.swizzle(row, col, self._uop.dtype.scalar())
|
||||
swizzled_offset = self.base_shape.swizzle(row, col, self._uop.dtype)
|
||||
|
||||
row = swizzled_offset // self.base_shape.cols
|
||||
col = swizzled_offset % self.base_shape.cols
|
||||
|
||||
+87
-125
@@ -4,7 +4,7 @@
|
||||
# A006 Lambda argument `input` is shadowing a Python builtin
|
||||
from tinygrad import Tensor, dtypes, Device
|
||||
from tinygrad.uop.ops import Ops, GroupOp
|
||||
from tinygrad.helpers import getenv, prod, strides_for_shape, argfix
|
||||
from tinygrad.helpers import getenv, prod, strides_for_shape
|
||||
import torch.lib
|
||||
TORCH_DEBUG = getenv("TORCH_DEBUG")
|
||||
import torch, pathlib, operator, functools, weakref
|
||||
@@ -73,6 +73,12 @@ def wrap_view_op(fn):
|
||||
return wrap(ret)
|
||||
return _wrap
|
||||
|
||||
# NOTE: list assignment raises IndexError on an out of range dim, and the index must be a tuple: a list of all ints is one advanced index
|
||||
def _index_dim(self, dim, idx):
|
||||
idxs = [slice(None)] * self.ndim
|
||||
idxs[dim] = idx
|
||||
return self[tuple(idxs)]
|
||||
|
||||
view_ops = {
|
||||
"aten.view": Tensor.reshape,
|
||||
"aten._unsafe_view": Tensor.reshape, # when are views unsafe, and do we care?
|
||||
@@ -82,15 +88,13 @@ view_ops = {
|
||||
"aten.transpose.int": Tensor.transpose,
|
||||
"aten.squeeze.dim": Tensor.squeeze,
|
||||
"aten.unsqueeze": Tensor.unsqueeze,
|
||||
"aten.select.int": lambda self, dim, idx: self[(slice(None),) * (dim%self.ndim) + (idx,)],
|
||||
"aten.select.int": _index_dim,
|
||||
"aten.permute": Tensor.permute,
|
||||
"aten.alias": lambda self: self,
|
||||
"aten.diagonal": Tensor.diagonal,
|
||||
"aten.slice.Tensor": lambda self, dim=0, start=None, end=None, step=1: _index_dim(self, dim, slice(start, end, step)),
|
||||
}
|
||||
|
||||
# torch 2.10 handles this natively
|
||||
if tuple(map(int, torch.__version__.split('.')[:2])) < (2, 10): view_ops.update({"aten.detach": Tensor.detach})
|
||||
|
||||
for k,v in view_ops.items(): torch.library.impl(k.replace("aten.", "aten::"), "privateuseone")(wrap_view_op(v))
|
||||
|
||||
def _get_view_ops(view): return getattr(view, "_view_ops", [])
|
||||
@@ -99,46 +103,21 @@ def _apply_view_ops(target, ops):
|
||||
for fn, args, kwargs in ops: target = fn(target, *args, **kwargs)
|
||||
return target
|
||||
|
||||
# similar to https://github.com/pytorch/pytorch/blob/main/aten/src/ATen/InferSize.h
|
||||
def _reshape_target_shape(shape:tuple[int, ...], args) -> tuple[int, ...]|None:
|
||||
if not (req := argfix(*args)): return None
|
||||
new_shape, infer_idx = [], -1
|
||||
for i, s in enumerate(req):
|
||||
if s is None: s = shape[i] if i < len(shape) else None
|
||||
if not isinstance(s, int): return None
|
||||
if s == -1:
|
||||
if infer_idx != -1: return None
|
||||
infer_idx = len(new_shape)
|
||||
new_shape.append(s)
|
||||
total = prod(shape)
|
||||
if infer_idx != -1:
|
||||
known = prod(x for x in new_shape if x != -1)
|
||||
if known == 0:
|
||||
if total != 0: return None
|
||||
new_shape[infer_idx] = 0
|
||||
else: new_shape[infer_idx] = total // known
|
||||
return tuple(new_shape) if prod(new_shape) == total else None
|
||||
|
||||
# TODO: can we get rid of this? only for test_flatten_reshape_add
|
||||
# a chain of reshapes is undone by reshaping the value back to the base
|
||||
def _try_simple_reshape_view_write(base: Tensor, view: Tensor, val: Tensor) -> bool:
|
||||
if not (ops := _get_view_ops(view)): return False
|
||||
shapes = [base.shape]
|
||||
for fn, args, _ in ops:
|
||||
if fn is Tensor.reshape:
|
||||
if not (next_shape := _reshape_target_shape(shapes[-1], args)): return False
|
||||
shapes.append(next_shape)
|
||||
if shapes[-1] != view.shape: return False
|
||||
for s in reversed(shapes[:-1]): val = val.reshape(s)
|
||||
base.assign(val)
|
||||
if any(fn is not Tensor.reshape for fn, _, _ in ops): return False
|
||||
base.assign(val.reshape(base.shape))
|
||||
return True
|
||||
|
||||
def _view_write(base: Tensor, view: Tensor, value: Tensor) -> None:
|
||||
val = value if value.dtype == base.dtype else value.cast(base.dtype)
|
||||
if view.shape == base.shape: return base.assign(val)
|
||||
if _try_simple_reshape_view_write(base, view, val): return
|
||||
idx_base = Tensor.arange(base.numel(), dtype=dtypes.int32).reshape(base.shape)
|
||||
idx_view = _apply_view_ops(idx_base, _get_view_ops(view)).reshape(-1)
|
||||
flat_base = base.reshape(base.numel()).contiguous()
|
||||
# clone, not contiguous: contiguous() on a base that already owns its buffer returns the base itself, and scattering
|
||||
# into that is an in-place write to a buffer other tensors still hold, which setitem refuses
|
||||
flat_base = base.reshape(base.numel()).clone()
|
||||
flat_base[idx_view] = val.reshape(-1)
|
||||
base.assign(flat_base.reshape(base.shape))
|
||||
|
||||
@@ -166,11 +145,6 @@ def _index_put_impl_(self, indices, values, accumulate=False, unsafe=False):
|
||||
def index_put(self, indices, values, accumulate=False):
|
||||
return aten.index_put(self.cpu(), [z.cpu() if isinstance(z, torch.Tensor) else None for z in indices], values.clone().cpu(), accumulate).tiny()
|
||||
|
||||
@torch.library.impl("aten::isin.Tensor_Tensor_out", "privateuseone")
|
||||
def isin_tensor_tensor_out(x, y, *, assume_unique=False, invert=False, out=None):
|
||||
result = (unwrap(x).unsqueeze(-1) == unwrap(y).flatten()).any(-1)
|
||||
return out.copy_(wrap(~result if invert else result))
|
||||
|
||||
@torch.library.impl("aten::randperm.generator_out", "privateuseone")
|
||||
def randperm_generator(n, generator=None, out=None):
|
||||
if generator is not None: raise NotImplementedError("tinygrad torch backend does not support torch.Generator for randperm")
|
||||
@@ -231,49 +205,6 @@ def as_strided(tensor:torch.Tensor, size, stride, storage_offset=None):
|
||||
def _reshape_alias(tensor:torch.Tensor, size, stride):
|
||||
return _as_strided(tensor, size, stride)
|
||||
|
||||
@torch.library.impl("aten::empty_strided", "privateuseone")
|
||||
def empty_strided(size, stride, dtype=None, layout=None, device=None, pin_memory=False):
|
||||
if TORCH_DEBUG: print(f"empty_strided {size=} {stride=} {dtype=} {layout=} {device=} {pin_memory=}")
|
||||
ret = Tensor.empty(*size, dtype=_from_torch_dtype(dtype or torch.get_default_dtype()), device=_from_torch_device(device))
|
||||
# TODO: should return with requested strides
|
||||
return wrap(ret)
|
||||
|
||||
@torch.library.impl("aten::empty.memory_format", "privateuseone")
|
||||
def empty_memory_format(size, dtype=None, layout=None, device=None, pin_memory=False, memory_format=None):
|
||||
if TORCH_DEBUG: print(f"empty.memory_format {size=} {dtype=} {layout=} {device=} {pin_memory=} {memory_format=}")
|
||||
ret = Tensor.empty(*size, dtype=_from_torch_dtype(dtype or torch.get_default_dtype()), device=_from_torch_device(device))
|
||||
return wrap(ret)
|
||||
|
||||
@torch.library.impl("aten::max_pool2d_with_indices", "privateuseone")
|
||||
def max_pool2d_with_indices(self:torch.Tensor, kernel_size:tuple[int, ...], stride=None, padding=0, dilation=1, ceil_mode=False):
|
||||
# TODO: supprt stride [] in tinygrad?
|
||||
if stride is not None and len(stride) == 0: stride = None
|
||||
ret, idx = unwrap(self).max_pool2d(kernel_size, stride, dilation, padding, ceil_mode, return_indices=True)
|
||||
return (wrap(ret), wrap(idx.cast(dtypes.int64)))
|
||||
|
||||
@torch.library.impl("aten::max_pool2d_with_indices_backward", "privateuseone")
|
||||
def max_pool2d_with_indices_backward(grad_out:torch.Tensor, self:torch.Tensor, kernel_size:tuple[int, ...], stride=None, padding=0, dilation=1, ceil_mode=False, indices=None):
|
||||
return wrap(Tensor.max_unpool2d(unwrap(grad_out), unwrap(indices), output_size=unwrap(self).shape))
|
||||
|
||||
@torch.library.impl("aten::max_unpool2d", "privateuseone")
|
||||
def max_unpool2d(self:torch.Tensor, indices:torch.Tensor, output_size):
|
||||
return wrap(unwrap(self).max_unpool2d(unwrap(indices), output_size=output_size))
|
||||
|
||||
@torch.library.impl("aten::arange", "privateuseone")
|
||||
def arange(end, dtype=None, device=None, pin_memory=None):
|
||||
has_float = isinstance(end, float)
|
||||
return wrap(Tensor.arange(0, end, dtype=_from_torch_dtype(dtype or (torch.get_default_dtype() if has_float else torch.int64))))
|
||||
|
||||
@torch.library.impl("aten::arange.start", "privateuseone")
|
||||
def arange_start(start, end, dtype=None, device=None, pin_memory=None):
|
||||
has_float = any(isinstance(x, float) for x in (start, end))
|
||||
return wrap(Tensor.arange(start, end, dtype=_from_torch_dtype(dtype or (torch.get_default_dtype() if has_float else torch.int64))))
|
||||
|
||||
@torch.library.impl("aten::arange.start_step", "privateuseone")
|
||||
def arange_start_step(start, end, step, dtype=None, device=None, pin_memory=None):
|
||||
has_float = any(isinstance(x, float) for x in (start, end, step))
|
||||
return wrap(Tensor.arange(start, end, step, dtype=_from_torch_dtype(dtype or (torch.get_default_dtype() if has_float else torch.int64))))
|
||||
|
||||
@torch.library.impl("aten::convolution_overrideable", "privateuseone")
|
||||
def convolution_overrideable(input, weight, bias, stride, padding, dilation, transposed, output_padding, groups):
|
||||
if TORCH_DEBUG >= 1:
|
||||
@@ -294,12 +225,27 @@ def convolution_backward_overrideable(grad_out, input, weight, stride, padding,
|
||||
grads = out.gradient(*[t for t,m in zip([input, weight, bias], output_mask) if m], gradient=grad_out)
|
||||
return tuple([wrap(grads.pop(0)) if m else None for m in output_mask])
|
||||
|
||||
@torch.library.impl("aten::slice.Tensor", "privateuseone")
|
||||
@wrap_view_op
|
||||
def slice_tensor(self, dim=0, start=None, end=None, step=1):
|
||||
slices = [slice(None)] * self.ndim
|
||||
slices[dim] = slice(start, end, step)
|
||||
return self[slices]
|
||||
# the functional scatters. without an impl aten falls back to a path that assumes a real storage: "self.has_storage() INTERNAL ASSERT FAILED"
|
||||
def _scatter_into(self, src, dim, index):
|
||||
out = unwrap(self).clone()
|
||||
slices = [slice(None)] * out.ndim
|
||||
slices[dim] = index
|
||||
out[slices] = unwrap(src).cast(out.dtype) # torch casts src to self's dtype, tinygrad setitem demands they already match
|
||||
return wrap(out)
|
||||
|
||||
@torch.library.impl("aten::slice_scatter", "privateuseone")
|
||||
def slice_scatter(self, src, dim=0, start=None, end=None, step=1): return _scatter_into(self, src, dim, slice(start, end, step))
|
||||
|
||||
@torch.library.impl("aten::select_scatter", "privateuseone")
|
||||
def select_scatter(self, src, dim, index): return _scatter_into(self, src, dim, index)
|
||||
|
||||
@torch.library.impl("aten::diagonal_scatter", "privateuseone")
|
||||
def diagonal_scatter(self, src, offset=0, dim1=0, dim2=1):
|
||||
# a diagonal is not one axis, so scatter through the flat indices it picks out
|
||||
base, out = unwrap(self), unwrap(self).clone().reshape(-1)
|
||||
idx = Tensor.arange(base.numel(), dtype=dtypes.int32).reshape(base.shape).diagonal(offset, dim1, dim2).reshape(-1)
|
||||
out[idx] = unwrap(src).cast(base.dtype).reshape(-1)
|
||||
return wrap(out.reshape(base.shape))
|
||||
|
||||
@torch.library.impl("aten::slice_backward", "privateuseone")
|
||||
def slice_backward(grad_out, input_sizes, dim, start, end, step):
|
||||
@@ -341,19 +287,14 @@ for dim in [1, 2, 3]:
|
||||
torch.library.impl(f"aten::{pad_type}_pad{dim}d", "privateuseone")(functools.partial(pad_forward, mode=mode))
|
||||
torch.library.impl(f"aten::{pad_type}_pad{dim}d_backward", "privateuseone")(functools.partial(pad_backward, mode=mode))
|
||||
|
||||
def upsample(self, size, align_corners=False, mode=None): return wrap(Tensor.interpolate(unwrap(self), size, mode=mode, align_corners=align_corners))
|
||||
# the schemas are all positional: (self, output_size, align_corners, *scales) for linear, (self, output_size, *scales) for nearest.
|
||||
def upsample(self, size, *args, mode=None):
|
||||
return wrap(Tensor.interpolate(unwrap(self), size, mode=mode, align_corners=args[0] if mode == "linear" else False))
|
||||
for i,pre in enumerate(["", "bi", "tri"]):
|
||||
torch.library.impl(f"aten::upsample_{pre}linear{i+1}d", "privateuseone")(functools.partial(upsample, mode="linear"))
|
||||
torch.library.impl(f"aten::upsample_nearest{i+1}d", "privateuseone")(functools.partial(upsample, mode="nearest"))
|
||||
torch.library.impl(f"aten::_upsample_nearest_exact{i+1}d", "privateuseone")(functools.partial(upsample, mode="nearest-exact"))
|
||||
|
||||
@torch.library.impl("aten::scatter_add.out", "privateuseone")
|
||||
def scatter_add(self, dim, index, src, out):
|
||||
self, index, src, out_unwrapped = unwrap(self), unwrap(index), unwrap(src), unwrap(out)
|
||||
if self.shape == (): _apply_inplace(out_unwrapped, src)
|
||||
else: _apply_inplace(out_unwrapped, Tensor.scatter_reduce(self, dim, index, src, reduce='sum'))
|
||||
return out
|
||||
|
||||
def _copy_between_devices(src, dest, cast_dtype, to_device, non_blocking=False):
|
||||
if src.is_tiny and dest.is_tiny:
|
||||
src_t, dest_t = unwrap(src), unwrap(dest)
|
||||
@@ -404,15 +345,11 @@ def sort_values(input, dim=-1, descending=False, stable=True, values=None, indic
|
||||
_apply_inplace(unwrap(indices), out_indices.cast(dtypes.int64))
|
||||
return values, indices
|
||||
|
||||
@torch.library.impl("aten::_linalg_svd", "privateuseone")
|
||||
def _linalg_svd(self, full_matrices=False):
|
||||
U, S, Vh = unwrap(self).svd(full_matrices)
|
||||
return wrap(U), wrap(S), wrap(Vh)
|
||||
|
||||
# register some decompositions
|
||||
from torch._decomp import get_decompositions
|
||||
decomps = [
|
||||
aten.native_layer_norm_backward,
|
||||
aten.native_group_norm_backward,
|
||||
aten.linalg_cross,
|
||||
aten.addmm,
|
||||
aten.addcmul,
|
||||
@@ -447,12 +384,20 @@ decomps = [
|
||||
aten._softmax_backward_data, aten.embedding_dense_backward,
|
||||
aten.linalg_vector_norm,
|
||||
aten.binary_cross_entropy, aten.binary_cross_entropy_backward,
|
||||
# the C++ mse/smooth_l1 kernels resize their out tensor, and a tiny tensor has no storage to resize
|
||||
aten.mse_loss, aten.mse_loss_backward,
|
||||
aten.smooth_l1_loss, aten.smooth_l1_loss_backward,
|
||||
aten.upsample_nearest2d.out,
|
||||
# NOTE: only the "out" overload, the "vec" one is CompositeImplicitAutograd and overriding it loses the autograd kernel
|
||||
aten.upsample_bicubic2d.out,
|
||||
aten._adaptive_avg_pool2d,
|
||||
# activations
|
||||
aten.hardswish, aten.hardswish_backward,
|
||||
aten.hardtanh, aten.hardtanh_backward,
|
||||
aten.gelu, aten.gelu_backward,
|
||||
aten.logical_and,
|
||||
# NOTE: no aten.logical_or here, its decomposition reaches aten.bitwise_or through a path that checks aliasing by
|
||||
# reading storage, which a tiny tensor has none of. it gets a direct impl below instead
|
||||
aten.logical_and, aten.logical_xor,
|
||||
aten.randint,
|
||||
aten.eye,
|
||||
aten.hardsigmoid_backward,
|
||||
@@ -495,7 +440,7 @@ simple_tensor_methods = [
|
||||
# reduce
|
||||
"all", "any", "argmax", "argmin", "cumsum", "cumprod",
|
||||
# complex
|
||||
"avg_pool2d", "linspace"]
|
||||
"linspace"]
|
||||
|
||||
tiny_backend_out = {**{f"aten.{x}.out":getattr(Tensor,x) for x in simple_tensor_methods}, **{
|
||||
"aten.add.out": lambda input,other,alpha=1: input+alpha*other,
|
||||
@@ -540,6 +485,8 @@ tiny_backend_out = {**{f"aten.{x}.out":getattr(Tensor,x) for x in simple_tensor_
|
||||
"aten.where.self_out": Tensor.where,
|
||||
"aten.prod.int_out": Tensor.prod,
|
||||
"aten.scatter.src_out": Tensor.scatter,
|
||||
"aten.scatter_add.out": lambda self,dim,index,src: src if self.shape == () else Tensor.scatter_reduce(self, dim, index, src, reduce="sum"),
|
||||
"aten.isin.Tensor_Tensor_out": lambda x,y,assume_unique=False,invert=False: (x.unsqueeze(-1)==y.flatten()).any(-1) != invert,
|
||||
# NOTE: axis=[] in torch means all, change tinygrad?
|
||||
"aten.sum.IntList_out": lambda self,axis,keepdim=False,dtype=None:
|
||||
self.sum(axis if axis is None or len(axis) else None, keepdim,
|
||||
@@ -555,10 +502,9 @@ def wrap_out(f):
|
||||
assert out.shape == assigned.shape, f"shape mismatch: {assigned.shape} -> {out.shape}"
|
||||
assert out.device == assigned.device or out.device is None or assigned.device is None, f"device mismatch: {assigned.device} -> {out.device}"
|
||||
assert out.dtype == assigned.dtype, f"dtype mismatch: {assigned.dtype} -> {out.dtype}"
|
||||
# an out= that is a view has to be written through its base, and _apply_inplace gives a deviceless base its buffer first
|
||||
if canonical_base(out) is not out: return _apply_inplace(out, assigned) or out
|
||||
if out.device is None and assigned.device is not None: out.replace(out.empty_like(device=assigned.device))
|
||||
return out.assign(assigned)
|
||||
# writing out= is an in-place write like any other: through the base if it is a view, refreshing any derived views
|
||||
_apply_inplace(out, assigned)
|
||||
return out
|
||||
return _wrap_out
|
||||
|
||||
def _inplace_op(t, new_value):
|
||||
@@ -566,7 +512,14 @@ def _inplace_op(t, new_value):
|
||||
else: _apply_inplace(t, new_value)
|
||||
return t
|
||||
|
||||
tiny_backend = {**{k:wrap_out(v) for k,v in tiny_backend_out.items()}, **{
|
||||
# the three arange overloads are one function at different arity, and dtype/layout/device/pin_memory are keyword only in all of them
|
||||
def _arange(*args, dtype=None, **_):
|
||||
return Tensor.arange(*args, dtype=_from_torch_dtype(dtype or (torch.get_default_dtype() if any(isinstance(x, float) for x in args) else torch.int64)))
|
||||
|
||||
def _empty(size, dtype=None, device=None, **_):
|
||||
return Tensor.empty(*size, dtype=_from_torch_dtype(dtype or torch.get_default_dtype()), device=_from_torch_device(device))
|
||||
|
||||
tiny_backend = {**tiny_backend_out, **{
|
||||
"aten.remainder.Scalar_Tensor": lambda x,y: x%y,
|
||||
"aten.floor_divide": lambda x,y: x//y,
|
||||
"aten.floor_divide_.Tensor": lambda x,y: x//y,
|
||||
@@ -579,8 +532,8 @@ tiny_backend = {**{k:wrap_out(v) for k,v in tiny_backend_out.items()}, **{
|
||||
# inplace ops using replace for fusion
|
||||
"aten.zero_": lambda x: x.const_like(0),
|
||||
"aten.fill_.Scalar": lambda x, y: x.const_like(y),
|
||||
"aten.add_.Tensor": lambda self, other, alpha=1.0: self + other * alpha,
|
||||
"aten.add_.Scalar": lambda self, other, alpha=1.0: self + other * alpha,
|
||||
"aten.add_.Tensor": lambda self, other, alpha=1: self + other * alpha,
|
||||
"aten.add_.Scalar": lambda self, other, alpha=1: self + other * alpha,
|
||||
"aten.mul_.Tensor": lambda self, other: self * other,
|
||||
"aten.mul_.Scalar": lambda self, other: self * other,
|
||||
# relu doesn't have an out form?
|
||||
@@ -613,7 +566,9 @@ tiny_backend = {**{k:wrap_out(v) for k,v in tiny_backend_out.items()}, **{
|
||||
# these don't work in out form, they have size 0
|
||||
"aten.abs": Tensor.abs,
|
||||
"aten.logical_not": Tensor.logical_not,
|
||||
"aten.logical_or_": lambda x, y: x | y,
|
||||
# compare against zero first: logical_* is bool-valued for any input dtype, while | is bitwise
|
||||
"aten.logical_or": lambda x, y: (x != 0) | (y != 0),
|
||||
"aten.logical_or_": lambda x, y: (x != 0) | (y != 0),
|
||||
"aten.multinomial": Tensor.multinomial,
|
||||
"aten.masked_fill_.Scalar": lambda self, mask, value: self.masked_fill(mask, value),
|
||||
"aten.masked_fill_.Tensor": lambda self, mask, value: self.masked_fill(mask, value),
|
||||
@@ -622,14 +577,7 @@ tiny_backend = {**{k:wrap_out(v) for k,v in tiny_backend_out.items()}, **{
|
||||
"aten.masked_select": Tensor.masked_select,
|
||||
"aten.all": Tensor.all,
|
||||
"aten.sgn": Tensor.sign,
|
||||
"aten.acos": Tensor.acos,
|
||||
"aten.any": Tensor.any,
|
||||
"aten.bitwise_not": Tensor.bitwise_not,
|
||||
"aten.argmax": Tensor.argmax,
|
||||
"aten.argmin": Tensor.argmin,
|
||||
"aten.asinh": Tensor.asinh,
|
||||
"aten.mul": Tensor.mul,
|
||||
"aten.atanh": Tensor.atanh,
|
||||
"aten.fill_.Tensor": lambda self, value: self.const_like(value.reshape(()).item()),
|
||||
"aten.flip": Tensor.flip,
|
||||
"aten.scatter_reduce.two": Tensor.scatter_reduce,
|
||||
@@ -640,10 +588,22 @@ tiny_backend = {**{k:wrap_out(v) for k,v in tiny_backend_out.items()}, **{
|
||||
"aten.add.Tensor": lambda input,other,alpha=1: input+alpha*other,
|
||||
"aten.linspace": lambda start, stop, steps, dtype=None, **kwargs:
|
||||
Tensor.linspace(start, stop, steps, **({"dtype": _from_torch_dtype(dtype)} if dtype is not None else {})),
|
||||
# the functional copy_. without an impl the fallback segfaults on a tensor with no storage
|
||||
"aten.copy": lambda self,src,non_blocking=False: src.cast(self.dtype).to(self.device).expand(self.shape),
|
||||
"aten.arange": lambda end, **kwargs: _arange(0, end, **kwargs),
|
||||
"aten.arange.start": _arange,
|
||||
"aten.arange.start_step": _arange,
|
||||
# empty_strided takes the strides and drops them: we always allocate contiguous
|
||||
"aten.empty_strided": lambda size, stride, **kwargs: _empty(size, **kwargs),
|
||||
"aten.empty.memory_format": _empty,
|
||||
# TODO: supprt stride [] in tinygrad?
|
||||
"aten.max_pool2d_with_indices": lambda self,kernel_size,stride=None,padding=0,dilation=1,ceil_mode=False: ((r:=Tensor.max_pool2d(self, kernel_size, stride or None, dilation, padding, ceil_mode, return_indices=True))[0], r[1].cast(dtypes.int64)),
|
||||
"aten.max_pool2d_with_indices_backward": lambda grad_out,self,kernel_size,stride=None,padding=0,dilation=1,ceil_mode=False,indices=None: Tensor.max_unpool2d(grad_out, indices, output_size=self.shape),
|
||||
"aten.max_unpool2d": lambda self,indices,output_size: Tensor.max_unpool2d(self, indices, output_size=output_size),
|
||||
"aten._linalg_svd": lambda self,full_matrices=False: Tensor.svd(self, full_matrices),
|
||||
"aten.topk": Tensor.topk,
|
||||
"aten.constant_pad_nd": lambda self, padding, value=0.0: self.pad(padding, mode="constant", value=value).contiguous(),
|
||||
# TODO: input contiguous is needed to prevent CFGContext circular dependency assertion for shapes >512 (see test_cumsum_arange_large)
|
||||
"aten.cumsum": lambda self, dim: self.contiguous().cumsum(dim),
|
||||
"aten.cumsum": lambda self, dim: self.cumsum(dim),
|
||||
"aten.logsumexp": lambda self, axis, keepdim=False: self.logsumexp(axis[0], keepdim=keepdim),
|
||||
"aten.roll": Tensor.roll,
|
||||
"aten.logcumsumexp": Tensor.logcumsumexp,
|
||||
@@ -652,6 +612,7 @@ tiny_backend = {**{k:wrap_out(v) for k,v in tiny_backend_out.items()}, **{
|
||||
self.ones_like(**{k: v for k, v in {"dtype": _from_torch_dtype(dtype) if dtype else None,
|
||||
"device": _from_torch_device(device) if device else None}.items() if v is not None}),
|
||||
"aten.max.dim": lambda self, dim, keepdim=False: (self.max(dim, keepdim), self.argmax(dim, keepdim).cast(dtype=dtypes.int64)),
|
||||
"aten.min.dim": lambda self, dim, keepdim=False: (self.min(dim, keepdim), self.argmin(dim, keepdim).cast(dtype=dtypes.int64)),
|
||||
"aten.cummax": lambda self, dim: ((r := self.cummax(dim))[0], r[1].cast(dtypes.int64)),
|
||||
"aten.cummin": lambda self, dim: ((r := self.cummin(dim))[0], r[1].cast(dtypes.int64)),
|
||||
"aten.nonzero": Tensor.nonzero,
|
||||
@@ -713,15 +674,16 @@ def wrap_inplace_view_op(f):
|
||||
return nf
|
||||
|
||||
# the aten schema says how an op is called: an inplace view retargets the view, a writable first arg is inplace,
|
||||
# and a writable out arg must have come from tiny_backend_out so that wrap_out was applied
|
||||
# and a writable out arg gets wrap_out's dtype cast, shape assert, and view write-through
|
||||
for k,v in tiny_backend.items():
|
||||
name, _, overload = k.removeprefix("aten.").partition(".")
|
||||
op = getattr(getattr(aten, name), overload or "default")
|
||||
writes = [a.name for a in op._schema.arguments if a.alias_info is not None and a.alias_info.is_write]
|
||||
if torch.Tag.inplace_view in op.tags: fxn = wrap_inplace_view_op(v)
|
||||
elif writes == [op._schema.arguments[0].name] and op._schema.returns: fxn = wrap_inplace(v)
|
||||
elif not writes or (writes == ["out"] and k in tiny_backend_out): fxn = wrap_fxn(k, v)
|
||||
else: raise RuntimeError(f"{k} writes {writes}: expected an inplace first arg, or an out arg with {k} in tiny_backend_out")
|
||||
elif not writes: fxn = wrap_fxn(k, v)
|
||||
elif writes == ["out"]: fxn = wrap_fxn(k, wrap_out(v))
|
||||
else: raise RuntimeError(f"{k} writes {writes}: unhandled writable arg in schema")
|
||||
torch.library.impl(k.replace("aten.", "aten::"), "privateuseone")(fxn)
|
||||
|
||||
@torch.library.impl("aten::equal", "privateuseone")
|
||||
|
||||
@@ -83,6 +83,12 @@ class TestTorchBackend(unittest.TestCase):
|
||||
torch.add(torch.ones(5, device=device), torch.ones(5, device=device), out=a)
|
||||
self.assertEqual(a.detach().storage_offset(), 3)
|
||||
|
||||
def test_out_refreshes_views_of_base(self):
|
||||
a = torch.zeros(4, device=device)
|
||||
v = a[2:]
|
||||
torch.add(torch.ones(4, device=device), torch.ones(4, device=device), out=a)
|
||||
np.testing.assert_equal(v.cpu().numpy(), [2., 2.])
|
||||
|
||||
@unittest.expectedFailure # TODO: storage offset assumes a contiguous source, use UOp.contiguous_view_offset
|
||||
def test_storage_offset_non_contiguous_source(self):
|
||||
a = torch.arange(12., device=device).reshape(3,4)
|
||||
@@ -166,6 +172,15 @@ class TestTorchBackend(unittest.TestCase):
|
||||
expected = np.array([[1.5, 5.2, 9.0], [13.2, 17.1, 18.4]], dtype=np.float32)
|
||||
np.testing.assert_equal(y3.cpu().numpy(), expected)
|
||||
|
||||
def test_argmax_argmin(self):
|
||||
a = torch.arange(12, dtype=torch.float32, device=device).reshape(3, 4)
|
||||
c = a.cpu()
|
||||
for got, want in [(a.argmax(), c.argmax()), (a.argmin(0), c.argmin(0)), (a.argmax(1, keepdim=True), c.argmax(1, keepdim=True)),
|
||||
(torch.min(a, 1).indices, torch.min(c, 1).indices), (torch.max(a, 1).indices, torch.max(c, 1).indices),
|
||||
(torch.min(a, 1).values, torch.min(c, 1).values), (torch.min(a, 1, keepdim=True).indices, torch.min(c, 1, keepdim=True).indices)]:
|
||||
self.assertEqual(got.dtype, want.dtype) # torch's arg reduces are int64, tinygrad's are int32
|
||||
np.testing.assert_equal(got.cpu().numpy(), want.numpy())
|
||||
|
||||
def test_isfinite(self):
|
||||
a = torch.ones(4, device=device)
|
||||
np.testing.assert_equal(torch.isfinite(a).cpu().numpy(), [True, True, True, True])
|
||||
@@ -373,6 +388,22 @@ class TestTorchBackend(unittest.TestCase):
|
||||
for bwd_eps in [1e-5, 0.3]:
|
||||
for got, want in zip(run(device, bwd_eps), run("cpu", bwd_eps)): np.testing.assert_allclose(got, want, atol=1e-4, rtol=1e-3)
|
||||
|
||||
def test_groupnorm_backward(self):
|
||||
def run(dev):
|
||||
x = torch.arange(24., device=dev).reshape(2, 4, 3).requires_grad_()
|
||||
w = torch.linspace(0.5, 2.0, 4).to(dev).requires_grad_()
|
||||
torch.nn.functional.group_norm(x, 2, w, torch.zeros(4, device=dev)).square().sum().backward()
|
||||
return x.grad.cpu().numpy(), w.grad.cpu().numpy()
|
||||
for got, want in zip(run(device), run("cpu")): np.testing.assert_allclose(got, want, atol=1e-4, rtol=1e-3)
|
||||
|
||||
def test_mse_smooth_l1_loss_backward(self):
|
||||
def run(dev, loss):
|
||||
x = torch.arange(4., device=dev).requires_grad_()
|
||||
loss(x, torch.ones(4, device=dev)).backward()
|
||||
return x.grad.cpu().numpy()
|
||||
for loss in [torch.nn.functional.mse_loss, torch.nn.functional.smooth_l1_loss]:
|
||||
np.testing.assert_allclose(run(device, loss), run("cpu", loss), atol=1e-6)
|
||||
|
||||
def test_batchnorm_unsqueeze(self):
|
||||
bn = torch.nn.BatchNorm2d(4).to(device)
|
||||
x = torch.randn(8, 4, 3, 3, device=device)
|
||||
@@ -516,6 +547,15 @@ class TestTorchBackend(unittest.TestCase):
|
||||
cpu_res = torch.arange(20, dtype=torch.float32)[::2][1:4].numpy()
|
||||
np.testing.assert_equal(torch_res, cpu_res)
|
||||
|
||||
def test_select_out_of_range_dim(self):
|
||||
a = torch.arange(12, dtype=torch.int32, device=device).reshape(3, 4)
|
||||
with self.assertRaises(IndexError): a.select(5, 0)
|
||||
|
||||
def test_select_collapses_the_only_dim(self):
|
||||
a = torch.arange(3, dtype=torch.int32, device=device)
|
||||
self.assertEqual(a.select(0, 1).shape, ())
|
||||
np.testing.assert_equal(a.select(0, 1).cpu().numpy(), 1)
|
||||
|
||||
def test_slice_negative_dim(self):
|
||||
a = torch.arange(13, dtype=torch.int32, device=device).repeat(8, 1)
|
||||
torch_chunks = a.chunk(3, -1)
|
||||
@@ -796,6 +836,86 @@ class TestTorchBackend(unittest.TestCase):
|
||||
np.testing.assert_allclose(w_tiny.grad.cpu().numpy(), w_cpu.grad.numpy(), atol=1e-4, rtol=1e-3)
|
||||
np.testing.assert_allclose(b_tiny.grad.cpu().numpy(), b_cpu.grad.numpy(), atol=1e-4, rtol=1e-3)
|
||||
|
||||
def test_write_through_detach_of_unrealized(self):
|
||||
a = torch.empty(4, device=device)
|
||||
a.detach().fill_(3)
|
||||
np.testing.assert_equal(a.cpu().numpy(), [3, 3, 3, 3])
|
||||
|
||||
def test_square_transpose_inplace(self):
|
||||
# a same-shape transpose is not a reshape: writing the transposed values straight back would scramble the base
|
||||
a = torch.tensor([[0., 1., 2.], [3., 4., 5.], [6., 7., 8.]], device=device)
|
||||
a.transpose(0, 1).add_(100)
|
||||
np.testing.assert_equal(a.cpu().numpy(), [[100., 101., 102.], [103., 104., 105.], [106., 107., 108.]])
|
||||
|
||||
def test_interpolate(self):
|
||||
a = torch.arange(4, dtype=torch.float32, device=device).reshape(1, 1, 2, 2)
|
||||
nearest = torch.nn.functional.interpolate(a, scale_factor=2.0)
|
||||
np.testing.assert_equal(nearest.cpu().numpy()[0, 0], [[0, 0, 1, 1], [0, 0, 1, 1], [2, 2, 3, 3], [2, 2, 3, 3]])
|
||||
linear = torch.nn.functional.interpolate(a, size=(4, 4), mode="bilinear", align_corners=False)
|
||||
ref = torch.nn.functional.interpolate(a.cpu(), size=(4, 4), mode="bilinear", align_corners=False)
|
||||
np.testing.assert_allclose(linear.cpu().numpy(), ref.numpy(), rtol=1e-5)
|
||||
|
||||
def test_interpolate_bicubic_area(self):
|
||||
a = torch.arange(32, dtype=torch.float32, device=device).reshape(1, 2, 4, 4)
|
||||
for mode, scale in [("bicubic", 2.0), ("area", 0.5)]:
|
||||
ref = torch.nn.functional.interpolate(a.cpu(), scale_factor=scale, mode=mode)
|
||||
np.testing.assert_allclose(torch.nn.functional.interpolate(a, scale_factor=scale, mode=mode).cpu().numpy(), ref.numpy(), atol=1e-4)
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_interpolate_bicubic_backward(self):
|
||||
# the forward comes from a decomposition, but aten::upsample_bicubic2d_backward has none (nor does
|
||||
# aten::_adaptive_avg_pool2d_backward, for area), so training through these modes needs a real kernel
|
||||
x = torch.arange(32., dtype=torch.float32, device=device).reshape(1, 2, 4, 4).requires_grad_()
|
||||
torch.nn.functional.interpolate(x, scale_factor=2.0, mode="bicubic").sum().backward()
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_interpolate_inexact_scale(self):
|
||||
# torch forwards the raw scale_factor, Tensor.interpolate recomputes it from output_size, and they disagree here
|
||||
a = torch.arange(6, dtype=torch.float32, device=device).reshape(1, 1, 2, 3)
|
||||
tiny = torch.nn.functional.interpolate(a, scale_factor=2.5, mode="bilinear")
|
||||
ref = torch.nn.functional.interpolate(a.cpu(), scale_factor=2.5, mode="bilinear")
|
||||
np.testing.assert_allclose(tiny.cpu().numpy(), ref.numpy(), rtol=1e-5)
|
||||
|
||||
def test_logical_or_xor(self):
|
||||
a = torch.tensor([True, True, False, False], device=device)
|
||||
b = torch.tensor([True, False, True, False], device=device)
|
||||
np.testing.assert_equal(torch.logical_or(a, b).cpu().numpy(), [True, True, True, False])
|
||||
np.testing.assert_equal(torch.logical_xor(a, b).cpu().numpy(), [False, True, True, False])
|
||||
# bool-valued whatever the input dtype, so this is not | and ^
|
||||
i, j = torch.tensor([2, 0, 5, 0], device=device), torch.tensor([0, 0, 1, 1], device=device)
|
||||
np.testing.assert_equal(torch.logical_or(i, j).cpu().numpy(), [True, False, True, True])
|
||||
np.testing.assert_equal(torch.logical_xor(i, j).cpu().numpy(), [True, False, False, True])
|
||||
|
||||
def test_slice_scatter(self):
|
||||
# the scatters are functional: they return a new tensor and must leave the one they were given alone
|
||||
a = torch.arange(12, dtype=torch.float32, device=device).reshape(3, 4)
|
||||
out = torch.slice_scatter(a, torch.ones(1, 4, device=device), 0, 0, 1)
|
||||
np.testing.assert_equal(out.cpu().numpy(), [[1, 1, 1, 1], [4, 5, 6, 7], [8, 9, 10, 11]])
|
||||
np.testing.assert_equal(a.cpu().numpy(), np.arange(12, dtype=np.float32).reshape(3, 4))
|
||||
|
||||
def test_slice_scatter_casts_src(self):
|
||||
a = torch.zeros(3, 4, device=device)
|
||||
out = torch.slice_scatter(a, torch.ones(1, 4, dtype=torch.int32, device=device), 0, 0, 1)
|
||||
self.assertEqual(out.dtype, torch.float32)
|
||||
np.testing.assert_equal(out.cpu().numpy()[0], np.ones(4, dtype=np.float32))
|
||||
|
||||
def test_select_scatter(self):
|
||||
a = torch.arange(12, dtype=torch.float32, device=device).reshape(3, 4)
|
||||
out = torch.select_scatter(a, torch.ones(4, device=device), 0, 1)
|
||||
np.testing.assert_equal(out.cpu().numpy(), [[0, 1, 2, 3], [1, 1, 1, 1], [8, 9, 10, 11]])
|
||||
|
||||
def test_diagonal_scatter(self):
|
||||
a = torch.zeros(3, 3, device=device)
|
||||
out = torch.diagonal_scatter(a, torch.arange(3, dtype=torch.float32, device=device))
|
||||
np.testing.assert_equal(out.cpu().numpy(), np.diag([0., 1., 2.]))
|
||||
np.testing.assert_equal(a.cpu().numpy(), np.zeros((3, 3), dtype=np.float32))
|
||||
|
||||
def test_copy_functional(self):
|
||||
# without an impl this segfaults rather than fails: a regression here takes the whole run down
|
||||
a = torch.arange(4, dtype=torch.float32, device=device)
|
||||
out = torch.ops.aten.copy(a, torch.zeros(4, device=device))
|
||||
np.testing.assert_equal(out.cpu().numpy(), [0., 0., 0., 0.])
|
||||
np.testing.assert_equal(a.cpu().numpy(), [0., 1., 2., 3.])
|
||||
|
||||
from tinygrad import Tensor
|
||||
class TestBackendHelpers(unittest.TestCase):
|
||||
|
||||
@@ -111,6 +111,10 @@ docs = [
|
||||
"numpy",
|
||||
]
|
||||
mesa = ["tinymesa==25.2.7.2"]
|
||||
autogen = [
|
||||
"pyyaml",
|
||||
"mako",
|
||||
]
|
||||
|
||||
|
||||
[tool.mutmut]
|
||||
|
||||
+20
-37
@@ -67,32 +67,26 @@ class TestParseExpr(unittest.TestCase):
|
||||
|
||||
def test_integer_literals(self):
|
||||
"""Test parsing integer literals."""
|
||||
self.assertEqual(parse_expr('0', {}).val, 0)
|
||||
self.assertEqual(parse_expr('42', {}).val, 42)
|
||||
self.assertEqual(parse_expr('42U', {}).val, 42)
|
||||
self.assertIs(parse_expr('0', {}), UOp.const(0, dtypes.uint32))
|
||||
self.assertIs(parse_expr('42', {}), UOp.const(42, dtypes.uint32))
|
||||
self.assertIs(parse_expr('42U', {}), UOp.const(42, dtypes.uint32))
|
||||
|
||||
def test_negative_integers(self):
|
||||
"""Test parsing negative integer literals."""
|
||||
result = parse_expr('-1', {})
|
||||
self.assertEqual(result.val, -1)
|
||||
self.assertEqual(result.dtype, dtypes.int)
|
||||
self.assertIs(parse_expr('-1', {}), UOp.const(-1, dtypes.int))
|
||||
|
||||
def test_float_literals(self):
|
||||
"""Test parsing float literals."""
|
||||
result = parse_expr('1.0F', {})
|
||||
self.assertEqual(result.val, 1.0)
|
||||
self.assertEqual(result.dtype, dtypes.float32)
|
||||
self.assertIs(parse_expr('1.0F', {}), UOp.const(1.0, dtypes.float32))
|
||||
|
||||
def test_hex_literals(self):
|
||||
"""Test parsing hex literals."""
|
||||
result = parse_expr('0xFF', {})
|
||||
self.assertEqual(result.val, 255)
|
||||
self.assertIs(parse_expr('0xFF', {}), UOp.const(255, dtypes.uint32))
|
||||
|
||||
def test_variable_lookup(self):
|
||||
"""Test variable lookup in parse_expr."""
|
||||
vrs = {'x': UOp.const(42, dtypes.uint32)}
|
||||
result = parse_expr('x', vrs)
|
||||
self.assertEqual(result.val, 42)
|
||||
self.assertIs(parse_expr('x', vrs), vrs['x'])
|
||||
|
||||
def test_binary_ops(self):
|
||||
"""Test parsing binary operations."""
|
||||
@@ -103,9 +97,7 @@ class TestParseExpr(unittest.TestCase):
|
||||
self.assertEqual(result.op, Ops.ADD)
|
||||
|
||||
# Subtraction with constant folding
|
||||
result = parse_expr('10 - 5', {})
|
||||
self.assertEqual(result.op, Ops.CONST)
|
||||
self.assertEqual(result.val, 5)
|
||||
self.assertIs(parse_expr('10 - 5', {}), UOp.const(5, dtypes.uint32))
|
||||
|
||||
def test_ternary(self):
|
||||
"""Test parsing ternary expressions."""
|
||||
@@ -142,15 +134,8 @@ class TestForLoopParsing(unittest.TestCase):
|
||||
S0 = UOp.const(0, dtypes.uint32)
|
||||
_vrs, assigns = parse_pcode(pcode, {'S0': S0})
|
||||
|
||||
# Check that the innermost value (default) is -1 (may be wrapped in CAST)
|
||||
val = assigns[0][1]
|
||||
# Traverse to innermost WHERE
|
||||
while val.op == Ops.WHERE:
|
||||
val = val.src[2] # false branch
|
||||
# Unwrap CAST if present
|
||||
while val.op == Ops.CAST:
|
||||
val = val.src[0]
|
||||
self.assertEqual(val.val, -1)
|
||||
# every cond folds (S0 is a const), leaving the default branch: -1 in the destination dtype
|
||||
self.assertIs(assigns[0][1].simplify(), UOp.const(-1, dtypes.uint32))
|
||||
|
||||
def test_ctz_parsing(self):
|
||||
"""Test CTZ pcode parsing."""
|
||||
@@ -262,8 +247,8 @@ class TestDSPcodePatterns(unittest.TestCase):
|
||||
_, assigns = parse_pcode(pcode, srcs)
|
||||
# Check addresses: 100 + 2*4 = 108, 100 + 5*4 = 120
|
||||
# assigns[i][1] is (addr, val) tuple for MEM writes; mypy sees UOp
|
||||
self.assertEqual(assigns[0][1][0].simplify().val, 108) # type: ignore[index]
|
||||
self.assertEqual(assigns[1][1][0].simplify().val, 120) # type: ignore[index]
|
||||
self.assertIs(assigns[0][1][0].simplify(), UOp.const(108, dtypes.uint32)) # type: ignore[index]
|
||||
self.assertIs(assigns[1][1][0].simplify(), UOp.const(120, dtypes.uint32)) # type: ignore[index]
|
||||
|
||||
def test_ds_store_data_values(self):
|
||||
"""Test DS_STORE_2ADDR_B32 uses correct data values."""
|
||||
@@ -280,8 +265,8 @@ class TestDSPcodePatterns(unittest.TestCase):
|
||||
_, assigns = parse_pcode(pcode, srcs)
|
||||
# assigns[i][1] is (addr, val) tuple for MEM writes; mypy sees UOp
|
||||
# DATA[31:0] should preserve the value
|
||||
self.assertEqual(assigns[0][1][1].simplify().val, 0xAAAAAAAA) # type: ignore[index]
|
||||
self.assertEqual(assigns[1][1][1].simplify().val, 0xBBBBBBBB) # type: ignore[index]
|
||||
self.assertIs(assigns[0][1][1].simplify(), UOp.const(0xAAAAAAAA, dtypes.uint32)) # type: ignore[index]
|
||||
self.assertIs(assigns[1][1][1].simplify(), UOp.const(0xBBBBBBBB, dtypes.uint32)) # type: ignore[index]
|
||||
|
||||
class TestConditionalParsing(unittest.TestCase):
|
||||
"""Test conditional (if/elsif/else) pcode parsing."""
|
||||
@@ -306,12 +291,12 @@ class TestConcatWidthParsing(unittest.TestCase):
|
||||
def test_permlanex16_altrow_concat(self):
|
||||
for row, expected in [(0, 1), (1, 0), (2, 3), (3, 2)]:
|
||||
parsed = parse_expr('{ row[1], ~row[0] }', {'row': UOp.const(row, dtypes.uint32)})
|
||||
self.assertEqual(parsed.simplify().val, expected)
|
||||
self.assertIs(parsed.simplify(), UOp.const(expected, dtypes.uint32))
|
||||
|
||||
def test_permlane64_altlane_concat(self):
|
||||
for lane, expected in [(0, 32), (1, 33), (31, 63), (32, 0), (63, 31)]:
|
||||
parsed = parse_expr('{ ~lane[5], lane[4:0] }', {'lane': UOp.const(lane, dtypes.uint32)})
|
||||
self.assertEqual(parsed.simplify().val, expected)
|
||||
self.assertIs(parsed.simplify(), UOp.const(expected, dtypes.uint32))
|
||||
|
||||
def test_permlane64_wave64_pcode_indices(self):
|
||||
vgpr = UOp.param(0, dtypes.uint32, (256,))
|
||||
@@ -327,19 +312,17 @@ class TestConcatWidthParsing(unittest.TestCase):
|
||||
'S2': UOp.const(0, dtypes.uint32),
|
||||
}
|
||||
|
||||
def load_idx(v: UOp) -> int:
|
||||
def check_load_idx(v: UOp, expected: int):
|
||||
simp = v.simplify()
|
||||
self.assertEqual(simp.op, Ops.LOAD)
|
||||
self.assertEqual(simp.src[0].op, Ops.INDEX)
|
||||
idx = simp.src[0].src[1].simplify()
|
||||
self.assertEqual(idx.op, Ops.CONST)
|
||||
return idx.val
|
||||
self.assertIs(simp.src[0].src[1].simplify(), UOp.const(expected, dtypes.uint32))
|
||||
|
||||
_, assigns = parse_pcode(PCODE[VOP1Op.V_PERMLANE64_B32_E32], srcs)
|
||||
self.assertEqual(len(assigns), 64)
|
||||
for lane, (dst_idx, src_idx) in {0: (64, 32), 31: (95, 63), 32: (96, 0), 63: (127, 31)}.items():
|
||||
self.assertEqual(assigns[lane][1][0].simplify().val, dst_idx) # type: ignore[index]
|
||||
self.assertEqual(load_idx(assigns[lane][1][1]), src_idx) # type: ignore[index]
|
||||
self.assertIs(assigns[lane][1][0].simplify(), UOp.const(dst_idx, dtypes.uint32)) # type: ignore[index]
|
||||
check_load_idx(assigns[lane][1][1], src_idx) # type: ignore[index]
|
||||
|
||||
class TestAllPcode(unittest.TestCase):
|
||||
"""Test that all pcode from all architectures can be parsed."""
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
import unittest, contextlib
|
||||
from tinygrad import Device, Tensor, Context, TinyJit
|
||||
from tinygrad.device import Compiled, ProfileProgramEvent, ProfileDeviceEvent
|
||||
from tinygrad.device import Compiled, ProfileProgramEvent
|
||||
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, Compiled.profile_events)
|
||||
load_amd_counters(data, [e for e in Compiled.profile_events[:profile_start] if isinstance(e, ProfileProgramEvent)] +
|
||||
Compiled.profile_events[profile_start:])
|
||||
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:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import unittest
|
||||
import functools
|
||||
from tinygrad import Tensor, Device, dtypes, Context
|
||||
from tinygrad.helpers import getenv, system, DEV
|
||||
from extra.gemm.cdna_asm_gemm import asm_gemm, hk_bf16_atb_gemm
|
||||
@@ -9,6 +10,7 @@ from examples.mlperf.models.flat_llama import FP8_DTYPE, quantize_fp8, FP8_MAX
|
||||
# Use DEV=NULL:HIP:gfx950 to also test the assembly
|
||||
def is_cdna4(): return Device[Device.DEFAULT].renderer.target.arch.startswith("gfx950")
|
||||
|
||||
@functools.cache
|
||||
def has_hipcc():
|
||||
try: system("hipcc --version")
|
||||
except Exception: return False
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import unittest, math
|
||||
from tinygrad import Tensor, Device, dtypes
|
||||
from tinygrad.dtype import DTYPES_DICT
|
||||
from tinygrad.uop.ops import Ops, UOp
|
||||
from tinygrad.uop.ops import Ops, UOp, GroupOp
|
||||
from tinygrad.codegen.decomp.op import threefry2x32
|
||||
import numpy as np
|
||||
from test.helpers import not_support_multi_device
|
||||
@@ -17,7 +17,7 @@ def _check_ast_count(desired_count:int, t:Tensor):
|
||||
class TestMovedConstFolding(unittest.TestCase):
|
||||
def test_contiguous_deviceless_const(self):
|
||||
t = Tensor(UOp.const(2.0, dtypes.float)).contiguous()
|
||||
self.assertIs(t.uop.op, Ops.CONST)
|
||||
self.assertIs(t.uop, UOp.const(2.0, dtypes.float))
|
||||
self.assertIsNone(t.uop.device)
|
||||
|
||||
def test_add_shrunk_zero(self):
|
||||
@@ -169,8 +169,8 @@ class TestMultiConstFolding(unittest.TestCase):
|
||||
class TestThreefryConstFolding(unittest.TestCase):
|
||||
def test_threefry(self):
|
||||
# THREEFRY(const,const) folds to a const once decomposed
|
||||
x = threefry2x32(UOp.const(5, dtypes.uint64), UOp.const(10, dtypes.uint64))
|
||||
self.assertIs(x.simplify().op, Ops.CONST)
|
||||
x = threefry2x32(UOp.const(5, dtypes.uint64), UOp.const(10, dtypes.uint64)).simplify()
|
||||
self.assertEqual([u.op for u in x.toposort() if u.op in GroupOp.ALU], [])
|
||||
|
||||
class TestTautologicalCompare(unittest.TestCase):
|
||||
# without const folding, these would have triggered -Wtautological-compare in clang
|
||||
|
||||
@@ -4,7 +4,7 @@ import numpy as np
|
||||
from tinygrad.dtype import AddrSpace, dtypes, Invalid
|
||||
from tinygrad.uop.ops import KernelInfo, AxisType, Ops
|
||||
from tinygrad.renderer.ptx import PTXRenderer
|
||||
from test.helpers import assert_kernel_count
|
||||
from test.helpers import assert_kernel_count, KernelCountException
|
||||
|
||||
# **** kernels ****
|
||||
|
||||
@@ -474,7 +474,7 @@ class TestCustomKernelInput(unittest.TestCase):
|
||||
y.realize()
|
||||
kernel_count = GlobalCounters.kernel_count
|
||||
self.assertEqual(y.tolist(), x.add(1).tolist())
|
||||
self.assertLessEqual(kernel_count, max_kernels)
|
||||
if kernel_count > max_kernels: raise KernelCountException(max_kernels, kernel_count)
|
||||
# same test with @function, input is PARAM
|
||||
from tinygrad import function
|
||||
x0 = Tensor.arange(32).clone("CPU").realize()
|
||||
@@ -487,7 +487,7 @@ class TestCustomKernelInput(unittest.TestCase):
|
||||
y = run(x0).realize()
|
||||
kernel_count = GlobalCounters.kernel_count
|
||||
self.assertEqual(y.tolist(), mop_fxn(x0).add(1).tolist())
|
||||
self.assertLessEqual(kernel_count, max_kernels)
|
||||
if kernel_count > max_kernels: raise KernelCountException(max_kernels, kernel_count)
|
||||
|
||||
def test_reshape(self): self._test_mop(lambda x: x.reshape(16, 2), max_kernels=2)
|
||||
def test_permute(self): self._test_mop(lambda x: x.reshape(4, 8).T, max_kernels=3)
|
||||
|
||||
@@ -340,6 +340,9 @@ class TestUint64DType(TestDType):
|
||||
DTYPE = dtypes.uint64
|
||||
def test_uint64_load(self):
|
||||
assert Tensor(2**64 - 1, dtype=dtypes.uint64).numpy() == 2**64 - 1
|
||||
@unittest.skipIf(dtypes.double not in supported_dtypes, "needs float64")
|
||||
def test_uint64_cast_double(self):
|
||||
assert Tensor([2**32 + 1], dtype=dtypes.uint64).cast(dtypes.double).numpy() == 2**32 + 1
|
||||
|
||||
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, PTXRenderer), "PTX does indexing math with longs")
|
||||
class TestEmulatedUInt64DType(TestUint64DType):
|
||||
|
||||
@@ -6,6 +6,8 @@ from tinygrad.tensor import _to_np_dtype
|
||||
from tinygrad.runtime.ops_python import from_storage_scalar
|
||||
from tinygrad.renderer.ptx import PTXRenderer
|
||||
from tinygrad.renderer.nir import NIRRenderer
|
||||
from tinygrad.renderer.llvmir import CPULLVMRenderer
|
||||
from tinygrad.renderer.isa.x86 import X86Renderer
|
||||
from tinygrad.uop import Ops
|
||||
import numpy as np
|
||||
import pytest
|
||||
@@ -64,6 +66,8 @@ ht.fp8e5m2fnuz = ht.uint8
|
||||
def universal_test(a, b, dtype, op):
|
||||
if not isinstance(op, tuple): op = (op, op)
|
||||
if op[0] == operator.mod and b == 0: return
|
||||
# TODO: throws floating point exception
|
||||
if isinstance(Device[Device.DEFAULT].renderer, (X86Renderer, CPULLVMRenderer)) and op[0] == operator.mod and a == dtype.min and b == -1: return
|
||||
# lt and max with nan is undefined in tinygrad
|
||||
if op[0] in (operator.lt, Tensor.maximum) and (math.isnan(a) or math.isnan(b)): return
|
||||
ta, tb = Tensor([a], dtype=dtype), Tensor([b], dtype=dtype)
|
||||
|
||||
@@ -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.const(i, dtypes.int), dtype=y.dtype.scalar())
|
||||
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):
|
||||
@@ -46,10 +46,10 @@ class TestIselX86(unittest.TestCase):
|
||||
# complex address is [base + index*scale + displacement]
|
||||
def test_complex_address(self):
|
||||
a = UOp.variable("a", 0, 0, dtypes.int32)
|
||||
load = UOp.param(0, dtypes.int32, (16,)).index(a + 1).load()
|
||||
load = UOp.param(0, dtypes.int32, (16,)).index(a + UOp.cconst(1, dtypes.int32)).load()
|
||||
n = self.isel_rewrite(load)
|
||||
# displacement is the constant in "a" scaled to the buffer element size, dtype is int8 when the value fits otherwise int32
|
||||
self.assertTrue(n.src[2].op is Ops.CONST and n.src[2].dtype is dtypes.int8 and n.src[2].val == 4)
|
||||
self.assertTrue(n.src[2].dtype is dtypes.int8 and n.src[2].src[0].op is Ops.CONST and n.src[2].src[0].val == 4)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -16,8 +16,6 @@ from test.helpers import replace_opts, check_schedule
|
||||
from test.backend.test_softmax_fusion import single_kernel_softmax
|
||||
MOCKGPU = DEV.interface.startswith("MOCK")
|
||||
|
||||
from tinygrad.uop.render import print_uops # noqa: F401 # pylint: disable=unused-import
|
||||
|
||||
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, ISARenderer), "isa backends don't preserve the op spec when lowering")
|
||||
class TestLinearizer(unittest.TestCase):
|
||||
def test_arg_dedup(self):
|
||||
@@ -248,11 +246,10 @@ class TestLinearizer(unittest.TestCase):
|
||||
uops = tuple(to_program(replace_opts(ast, opt), renderer=Device[Device.DEFAULT].renderer).src[1].src)
|
||||
begin_range = [i for i, x in enumerate(uops) if x.op is Ops.RANGE][-1]
|
||||
end_range = [i for i, x in enumerate(uops) if x.op is Ops.END][0]
|
||||
for i,u in enumerate(uops): print(i, u.op, [uops.index(s) for s in u.src], u.arg, u.dtype)
|
||||
for u in uops:
|
||||
if u.op is Ops.STORE and u.src[0].addrspace is AddrSpace.REG:
|
||||
if uops.index(u) < begin_range:
|
||||
assert u.src[1].op is Ops.CONST
|
||||
assert u.src[1].op not in GroupOp.ALU
|
||||
else:
|
||||
assert u.src[1].op in GroupOp.ALU
|
||||
assert begin_range < uops.index(u) < end_range
|
||||
@@ -268,9 +265,9 @@ class TestLinearizer(unittest.TestCase):
|
||||
uops = tuple(to_program(replace_opts(ast, []), renderer=Device[Device.DEFAULT].renderer).src[1].src)
|
||||
idxs = dedup([uop for uop in uops if uop.op is Ops.SPECIAL])
|
||||
idxs = sorted(idxs, key=lambda uop: uop.arg)
|
||||
assert (idxs[0].arg, idxs[0].src[0].val) == ('gidx0', 6), idxs[0]
|
||||
assert (idxs[1].arg, idxs[1].src[0].val) == ('gidx1', 5), idxs[1].arg
|
||||
assert (idxs[2].arg, idxs[2].src[0].val) == ('gidx2', 4), idxs[2].arg
|
||||
assert (idxs[0].arg, idxs[0].src[0].src[0].val) == ('gidx0', 6), idxs[0]
|
||||
assert (idxs[1].arg, idxs[1].src[0].src[0].val) == ('gidx1', 5), idxs[1].arg
|
||||
assert (idxs[2].arg, idxs[2].src[0].src[0].val) == ('gidx2', 4), idxs[2].arg
|
||||
|
||||
def test_sum_collapse(self):
|
||||
t = Tensor([2]).reshape(1, 1).expand(256, 256).sum()
|
||||
|
||||
@@ -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
|
||||
from test.backend.test_asm_gemm import has_hipcc, is_cdna4
|
||||
|
||||
def run_fused_ce(bs:int, seqlen:int, vocab:int, label_smoothing:float=0.0) -> None:
|
||||
Tensor.manual_seed(0)
|
||||
@@ -99,22 +99,20 @@ class TestLocalAmax(unittest.TestCase):
|
||||
assert_kernel_count(2)
|
||||
self.assertEqual(out.tolist(), [[0., 7., 14., 21.], [28., 35., 42., 49.], [120., 135., 150., 165.], [180., 195., 210., 225.]])
|
||||
|
||||
@unittest.skipUnless(has_hipcc() and Device.DEFAULT == "AMD", "requires hipcc to compile and amd device to run")
|
||||
class TestFusedQKVRoPE(unittest.TestCase):
|
||||
SHAPE = (2, 8192, 32, 8, 128)
|
||||
|
||||
def setUp(self):
|
||||
if dtypes.bfloat16 not in Device[Device.DEFAULT].renderer.supported_dtypes(): self.skipTest("test uses bf16 inputs")
|
||||
|
||||
def rand_bf16(self, *shape:int) -> Tensor:
|
||||
return (Tensor.randn(*shape) * 0.1).cast(dtypes.bfloat16).contiguous().realize()
|
||||
|
||||
def freqs_cis(self) -> Tensor:
|
||||
_, N, _, _, D = self.SHAPE
|
||||
return precompute_freqs_cis(D, N * 2).cast(dtypes.bfloat16).clone().realize()
|
||||
|
||||
def test_llama31_8b_forward(self):
|
||||
def test_forward(self):
|
||||
Tensor.manual_seed(0)
|
||||
B, N, H, H_KV, D = self.SHAPE
|
||||
B, N, H, H_KV, D = 1, 32, 8, 2, 16
|
||||
GROUP = H // H_KV
|
||||
freqs_cis = self.freqs_cis()
|
||||
freqs_cis = (Tensor.randn(1, N * 2, 1, D // 2, 2) * 0.1).cast(dtypes.bfloat16).contiguous().realize()
|
||||
|
||||
x = self.rand_bf16(B, N, H_KV * (GROUP + 2) * D)
|
||||
q, k, v = fused_qkv_rope(x, freqs_cis, H, H_KV, D)
|
||||
@@ -131,12 +129,13 @@ 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")
|
||||
|
||||
def test_llama31_8b_backward(self):
|
||||
@unittest.skipUnless(has_hipcc() and is_cdna4(), "backward kernel requires hipcc to compile")
|
||||
def test_llama31_8b(self):
|
||||
Tensor.manual_seed(1)
|
||||
B, N, H, H_KV, D = self.SHAPE
|
||||
PARTIALS = 2
|
||||
GROUP = H // H_KV
|
||||
freqs_cis = self.freqs_cis()
|
||||
freqs_cis = precompute_freqs_cis(D, N * 2).cast(dtypes.bfloat16).clone().realize()
|
||||
dq = self.rand_bf16(B, N, H, D)
|
||||
dk_partial = self.rand_bf16(B * PARTIALS, N, H_KV, D)
|
||||
dv_partial = self.rand_bf16(B * PARTIALS, N, H_KV, D)
|
||||
|
||||
@@ -3,10 +3,10 @@ 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, pm_beam, pm_compile
|
||||
from tinygrad.engine.realize import run_linear, compile_linear, lower_and_compile, pm_beam
|
||||
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
|
||||
from test.helpers import not_support_multi_device, needs_second_gpu, slow, call_is_graph, check_schedule, assert_kernel_count, KernelCountException
|
||||
|
||||
settings.register_profile("my_profile", max_examples=200, deadline=None, derandomize=getenv("DERANDOMIZE_CI", False))
|
||||
settings.load_profile("my_profile")
|
||||
@@ -72,15 +72,15 @@ class TestMultiTensor(unittest.TestCase):
|
||||
X.shard_(devices_2, 0)
|
||||
out = (X + X)
|
||||
linear = compile_linear(out.schedule_linear())
|
||||
names = [call.src[0].src[0].arg.name for call in linear.src if call.src[0].op is Ops.PROGRAM]
|
||||
uops = [call.src[0].src[0] for call in linear.src if call.src[0].op is Ops.PROGRAM]
|
||||
run_linear(linear)
|
||||
self.assertEqual(len(set(names)), 1, "function was relinearized")
|
||||
self.assertEqual(len(set(uops)), 1, "function was relinearized")
|
||||
|
||||
def test_shard_beam(self):
|
||||
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 = graph_rewrite(graph_rewrite(lin, pm_beam, ctx=1, walk=True), pm_compile, walk=True).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]
|
||||
self.assertNotEqual(call.src[0].src[0].arg.applied_opts, ())
|
||||
|
||||
def test_shard_same_device(self):
|
||||
@@ -395,7 +395,7 @@ class TestMultiBufferView(unittest.TestCase):
|
||||
linear, var_vals = b_multi.linear_with_vars()
|
||||
if all(not d.startswith(("WEBGPU", "CL")) for d in b_multi.device):
|
||||
compiled = [call for call in linear.src if call.src[0].op is Ops.SINK]
|
||||
self.assertEqual(len(compiled), 0, f"expected zero compiled kernels, got {len(compiled)}")
|
||||
if len(compiled) != 0: raise KernelCountException(0, len(compiled))
|
||||
run_linear(linear, var_vals)
|
||||
np.testing.assert_equal(b_multi.numpy(), b_ref.numpy())
|
||||
|
||||
|
||||
@@ -822,6 +822,10 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op([], lambda: tor0&tor1, lambda: ten0&ten1, forward_only=True)
|
||||
|
||||
helper_test_op(None, lambda x: (1 < x) & (x < 2), forward_only=True, vals=[[1.2, 1.2, 1.2, 3.2]])
|
||||
helper_test_op([(3000,)]*10, lambda *xs: (sum(xs[1:], xs[0]) > 5) & (xs[0] < 0.9), forward_only=True)
|
||||
|
||||
if not COMPILE_ONLY:
|
||||
np.testing.assert_equal((Tensor(2**64-1, dtype=dtypes.uint64) & 0xFFFFFFFF).numpy(), 0xFFFFFFFF)
|
||||
|
||||
def test_or(self):
|
||||
data = [[1,-8,1],[32,1,6]]
|
||||
@@ -2164,6 +2168,10 @@ class TestOps(unittest.TestCase):
|
||||
def test_roll(self):
|
||||
helper_test_op([(2, 4)], lambda x: x.roll(1))
|
||||
helper_test_op([(2, 4)], lambda x: x.roll((1,)))
|
||||
helper_test_op([(0,)], lambda x: x.roll(1, 0))
|
||||
helper_test_op([(2, 0, 3)], lambda x: x.roll(1, 0))
|
||||
helper_test_op([(2, 0, 3)], lambda x: x.roll(1, 1))
|
||||
helper_test_op([(2, 0, 3)], lambda x: x.roll(1))
|
||||
self.helper_test_exception([(2, 4)], lambda x: x.roll((1, 2)), expected=RuntimeError)
|
||||
helper_test_op([(2, 4)], lambda x: x.roll(1, 0))
|
||||
helper_test_op([(2, 4)], lambda x: x.roll(-1, 0))
|
||||
|
||||
@@ -3,6 +3,7 @@ import numpy as np
|
||||
from tinygrad import Tensor, Device, TinyJit, Variable, dtypes
|
||||
from tinygrad.helpers import GlobalCounters, ContextVar, Context, DEV
|
||||
from tinygrad.uop.ops import PatternMatcher, UPat, UOp, deconstruct_function
|
||||
from test.helpers import KernelCountException
|
||||
|
||||
class TestPickle(unittest.TestCase):
|
||||
def test_pickle_code_object(self):
|
||||
@@ -41,7 +42,7 @@ class TestPickle(unittest.TestCase):
|
||||
t2:Tensor = pickle.loads(st)
|
||||
np.testing.assert_equal(t_values, t2.numpy())
|
||||
# expect at most one COPY kernel
|
||||
self.assertLessEqual(GlobalCounters.kernel_count, 1)
|
||||
if GlobalCounters.kernel_count > 1: raise KernelCountException(1, GlobalCounters.kernel_count)
|
||||
|
||||
def test_pickle_realized_tensor_alt(self):
|
||||
print("** init")
|
||||
|
||||
@@ -82,7 +82,7 @@ class TestQuantizeOnnxCPU(unittest.TestCase):
|
||||
linear = run_onnx({"input":inp})["output"].schedule_linear()
|
||||
prg = to_program(linear.src[-2].src[0], renderer=Device[Device.DEFAULT].renderer)
|
||||
daccs = [u for u in tuple(prg.src[1].src) if u.op is Ops.BUFFER and u.addrspace is AddrSpace.REG]
|
||||
assert all(u.dtype.scalar() is dtypes.int for u in daccs)
|
||||
assert all(u.dtype is dtypes.int for u in daccs)
|
||||
|
||||
@unittest.skipIf(Device.DEFAULT != "DSP", "only tests for DSP")
|
||||
class TestQuantizeOnnx(unittest.TestCase):
|
||||
|
||||
@@ -176,7 +176,7 @@ class TestLimitBufs(unittest.TestCase):
|
||||
|
||||
def test_limit_bufs_linear_scaling(self):
|
||||
def sched_time(n):
|
||||
with Context(TRACK_MATCH_STATS=0, DEBUG=0):
|
||||
with Context(TRACK_MATCH_STATS=0, DEBUG=0, PARALLEL=0):
|
||||
bufs = [Tensor.ones(16).contiguous().realize() for _ in range(4)]
|
||||
root = bufs[0]
|
||||
for i in range(n): root = root + bufs[i % 4]
|
||||
|
||||
+11
-10
@@ -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
|
||||
from tinygrad.helpers import Context, ceildiv
|
||||
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
|
||||
@@ -193,15 +193,16 @@ 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]
|
||||
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)
|
||||
# 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)
|
||||
|
||||
@unittest.skipUnless(Device[Device.DEFAULT].renderer.has_shared, "test requires shared memory")
|
||||
@unittest.skip("tinygrad doesn't support this behavior")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import unittest, numpy as np
|
||||
from unittest.mock import patch
|
||||
from tinygrad import Device, Tensor
|
||||
from tinygrad.device import Buffer
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.helpers import getenv
|
||||
from tinygrad.runtime.support.hcq2 import HCQ_DEVS, all_devices_in
|
||||
|
||||
@@ -10,6 +12,15 @@ class TestHCQ2(unittest.TestCase):
|
||||
with patch.object(Device[Device.DEFAULT], "has_copy_queue", False):
|
||||
np.testing.assert_equal(Tensor(np.arange(61, dtype=np.float32)).to(Device.DEFAULT).contiguous().realize().numpy(), np.arange(61))
|
||||
|
||||
@unittest.skipIf(Device.DEFAULT == "CPU", "staged copies need a non-CPU hcq2 device")
|
||||
def test_staged_copy_slot_reuse(self):
|
||||
# chunks of a staged copy rotate through the staging buffer slots, many rotations must stay bit-exact in both directions
|
||||
import tinygrad.runtime.support.hcq2 as hcq2
|
||||
buf = Buffer("CPU", 1 << 20, dtypes.uint8, preallocate=True)
|
||||
data = np.random.default_rng(42).integers(0, 256, (5 << 20) + 123, dtype=np.uint8)
|
||||
with patch.object(hcq2, "STAGING_SIZE", 1 << 20), patch.object(hcq2, "STAGING_SLOTS", 4), patch.object(hcq2, "_staging", lambda: buf):
|
||||
np.testing.assert_equal(Tensor(data).to(Device.DEFAULT).realize().numpy(), data)
|
||||
|
||||
def test_overlapping_device_tuples(self):
|
||||
# an op on a wide device tuple followed by an op on an overlapping smaller tuple used to MMU-fault the smaller one
|
||||
d4, d2 = tuple(f"{Device.DEFAULT}:{i}" for i in range(4)), tuple(f"{Device.DEFAULT}:{i}" for i in range(2))
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import numpy as np
|
||||
class TestDevCopySpeeds(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.sz = getenv("SIZE", 2e6)
|
||||
cls.sz = getenv("SIZE", 2000000)
|
||||
cls.dev = Device["AMD"]
|
||||
if not cls.dev.is_usb(): raise unittest.SkipTest("only test this on USB devices")
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -44,7 +44,7 @@ def realized_matmul():
|
||||
z = y.matmul(x)
|
||||
Tensor.realize(z)
|
||||
def realized_gradient():
|
||||
x = Tensor.eye(3)
|
||||
x = Tensor.eye(3).clone()
|
||||
y = Tensor([[2.0,0,-2.0]])
|
||||
z = y.matmul(x).sum()
|
||||
z.backward()
|
||||
|
||||
+3
-1
@@ -86,7 +86,9 @@ def assert_jit_cache_len(fxn, expected_len):
|
||||
if linear is None or not linear.src:
|
||||
if expected_len != 0: raise KernelCountException(expected_len, 0)
|
||||
return
|
||||
if expected_len and all(call_is_hcq(call) for call in linear.src): expected_len = 4 # HCQ2: fence + reset + merged same-queue calls + finalizer
|
||||
if expected_len and all(call_is_hcq(call) for call in linear.src): # HCQ2: one batch submitter, or fence + reset + merged calls + finalizer
|
||||
from tinygrad.runtime.support.hcq2 import HCQ_RUNTIME_DEV
|
||||
expected_len = 1 if HCQ_RUNTIME_DEV.value == "CPU" else 4
|
||||
if call_is_graph(linear.src[0]):
|
||||
if len(linear.src) != 1: raise KernelCountException(1, len(linear.src))
|
||||
inner = linear.src[0].src[0].src[0] # LINEAR UOp inside CUSTOM_FUNCTION
|
||||
|
||||
+12
-5
@@ -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, (value & 0x7FFF) * 512)
|
||||
op = ("sram_read" if value & 0x8000 else "sram_write", 0xF000 + (index & 0xFF) * 0x4000, (value & 0x7FFF) * 512)
|
||||
if value & 0x8000: self._bulk_read_op = op
|
||||
else: self._bulk_write_op = op
|
||||
elif request == 0xF0:
|
||||
@@ -193,19 +193,26 @@ class MockUSB3:
|
||||
op, address, size = self._bulk_write_op
|
||||
assert len(data) == size
|
||||
if op == "sram_write":
|
||||
host_addr, region_size = self.state._dma_regions[address]
|
||||
ctypes.memmove(host_addr, data, min(len(data), region_size))
|
||||
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)
|
||||
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 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":
|
||||
host_addr, region_size = self.state._dma_regions[address]
|
||||
data = bytes((ctypes.c_ubyte * min(length, region_size)).from_address(host_addr))
|
||||
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)))
|
||||
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
|
||||
|
||||
+11
-121
@@ -1,39 +1,10 @@
|
||||
import unittest, itertools, math
|
||||
from tinygrad import Tensor, dtypes, Context
|
||||
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
|
||||
|
||||
def _check_ast_count(desired_count:int, t:Tensor):
|
||||
# NOTE: this has side effect because everything can be scheduled only once
|
||||
linear = t.schedule_linear()
|
||||
asts = [s for s in linear.src if s.src[0].op is Ops.SINK]
|
||||
len(asts)
|
||||
# NOT SUPPORTED ANYMORE
|
||||
#assert len(asts) == desired_count, f"{len(asts)} != {desired_count}"
|
||||
|
||||
class TestUnaryOpsConstFolding(unittest.TestCase):
|
||||
def test_all_consts_ops(self):
|
||||
_check_ast_count(0, Tensor.ones(4).exp())
|
||||
_check_ast_count(0, Tensor.ones(4).sqrt())
|
||||
_check_ast_count(0, Tensor.ones(4) + Tensor.ones(4))
|
||||
_check_ast_count(0, Tensor.ones(4) / Tensor.ones(4))
|
||||
|
||||
def test_cast(self):
|
||||
_check_ast_count(0, Tensor.ones(4).cast(dtypes.int16))
|
||||
_check_ast_count(0, Tensor.full(4, fill_value=-1).cast(dtypes.uint16))
|
||||
|
||||
def test_neg_folding(self):
|
||||
_check_ast_count(0, Tensor([1, 2, 3]).mul(-1).neg())
|
||||
_check_ast_count(0, Tensor([1, 2, 3]).neg().mul(-1))
|
||||
_check_ast_count(0, Tensor([1, 2, 3]).neg().neg())
|
||||
|
||||
def test_neg_realized_no_fold(self):
|
||||
x = Tensor.randn(32, 32)
|
||||
x = x.clip(0, 1).realize()
|
||||
_check_ast_count(1, x.neg())
|
||||
|
||||
class TestWeakConstFolding(unittest.TestCase):
|
||||
def test_weakint_math(self):
|
||||
out = (UOp.const(2**40) + UOp.const(2**40)).simplify()
|
||||
@@ -51,84 +22,18 @@ class TestWeakConstFolding(unittest.TestCase):
|
||||
def test_invalid_poison(self):
|
||||
self.assertTrue(UOp.invalid().alu(Ops.CDIV, UOp.const(0)).simplify().is_invalid)
|
||||
|
||||
def test_single_rounding_log10_backward(self):
|
||||
# log10 backward folds log10(2)/log(2) = 1/log(10) in one rounding, not the double-rounded 1/float32(log(10))
|
||||
x = Tensor([1.0, 2.0, 3.0])
|
||||
ast = next(s.src[0] for s in x.log10().sum().gradient(x)[0].schedule_linear().src if s.src[0].op is Ops.SINK)
|
||||
const = next(u.arg for u in full_rewrite(ast).toposort() if u.op is Ops.CONST and u.dtype is dtypes.float32)
|
||||
# correctly rounded: within half a float32 ulp of the exact value (folding at float32 lands 0.66 ulp off)
|
||||
self.assertLess(abs(const - 1/math.log(10)), 2**-26)
|
||||
|
||||
class TestBinaryOpsConstFolding(unittest.TestCase):
|
||||
def test_add_literal_zero(self):
|
||||
_check_ast_count(0, Tensor([1.0, 2, 3, 4]) + 0)
|
||||
def test_add_tensor_zero(self):
|
||||
_check_ast_count(0, Tensor([1.0, 2, 3, 4]) + Tensor.zeros(4))
|
||||
def test_literal_zero_add(self):
|
||||
_check_ast_count(0, 0 + Tensor([1.0, 2, 3, 4]))
|
||||
def test_tensor_zero_add(self):
|
||||
_check_ast_count(0, Tensor.zeros(4) + Tensor([1.0, 2, 3, 4]))
|
||||
|
||||
def test_sub_literal_zero(self):
|
||||
_check_ast_count(0, Tensor([1.0, 2, 3, 4]) - 0)
|
||||
def test_sub_tensor_zero(self):
|
||||
_check_ast_count(0, Tensor([1.0, 2, 3, 4]) - Tensor.zeros(4))
|
||||
|
||||
def test_mul_literal_zero(self):
|
||||
_check_ast_count(0, Tensor([1.0, 2, 3, 4]) * 0)
|
||||
def test_mul_tensor_zero(self):
|
||||
_check_ast_count(0, Tensor([1.0, 2, 3, 4]) * Tensor.zeros(4))
|
||||
def test_literal_zero_mul(self):
|
||||
_check_ast_count(0, 0 * Tensor([1.0, 2, 3, 4]) * 0)
|
||||
def test_tensor_zero_mul(self):
|
||||
_check_ast_count(0, Tensor.zeros(4) * Tensor([1.0, 2, 3, 4]))
|
||||
|
||||
def test_mul_literal_one(self):
|
||||
_check_ast_count(0, Tensor([1.0, 2, 3, 4]) * 1)
|
||||
def test_mul_tensor_one(self):
|
||||
_check_ast_count(0, Tensor([1.0, 2, 3, 4]) * Tensor.ones(4))
|
||||
def test_literal_one_mul(self):
|
||||
_check_ast_count(0, 1 * Tensor([1.0, 2, 3, 4]))
|
||||
def test_tensor_one_mul(self):
|
||||
_check_ast_count(0, Tensor.ones(4) * Tensor([1.0, 2, 3, 4]))
|
||||
|
||||
def test_bool_tensor_mul_bool(self):
|
||||
_check_ast_count(0, Tensor([True, False]) * True)
|
||||
_check_ast_count(0, Tensor([True, False]) * False)
|
||||
def test_bool_mul_bool_tensor(self):
|
||||
_check_ast_count(0, True * Tensor([True, False]))
|
||||
_check_ast_count(0, False * Tensor([True, False]))
|
||||
|
||||
def test_div_literal_one(self):
|
||||
_check_ast_count(0, Tensor([1.0, 2, 3, 4]) / 1)
|
||||
def test_div_tensor_one(self):
|
||||
_check_ast_count(0, Tensor([1.0, 2, 3, 4]) / Tensor.ones(4))
|
||||
|
||||
def test_floordiv_literal_one(self):
|
||||
_check_ast_count(0, Tensor([1, 2, 3, 4]) // 1)
|
||||
def test_floordiv_tensor_one(self):
|
||||
_check_ast_count(0, Tensor([1, 2, 3, 4]) // Tensor.ones(4, dtype=dtypes.int32))
|
||||
|
||||
def test_pow_literal_zero(self):
|
||||
_check_ast_count(0, Tensor([1.0, 2, 3, 4]) ** 0)
|
||||
def test_pow_tensor_zero(self):
|
||||
_check_ast_count(0, Tensor([1.0, 2, 3, 4]) ** Tensor.zeros(4))
|
||||
|
||||
def test_pow_literal_one(self):
|
||||
_check_ast_count(0, Tensor([1.0, 2, 3, 4]) ** 1)
|
||||
def test_pow_tensor_one(self):
|
||||
_check_ast_count(0, Tensor([1.0, 2, 3, 4]) ** Tensor.ones(4))
|
||||
def test_literal_one_pow(self):
|
||||
_check_ast_count(0, 1 ** Tensor([1.0, 2, 3, 4]))
|
||||
def test_tensor_one_pow(self):
|
||||
_check_ast_count(0, Tensor.ones(4) ** Tensor([1.0, 2, 3, 4]))
|
||||
|
||||
class TestBitcastConstFolding(unittest.TestCase):
|
||||
def test_out_of_range_source_value(self):
|
||||
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.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 = full_rewrite(UOp.const(from_v, from_dt).bitcast(to_dt).sink()).src[0]
|
||||
r = UOp.const(from_v, from_dt).bitcast(to_dt).simplify()
|
||||
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)
|
||||
@@ -152,24 +57,9 @@ class TestBitcastConstFolding(unittest.TestCase):
|
||||
|
||||
def test_vec_bitcast(self):
|
||||
with Context(SPEC=0):
|
||||
srcs = full_rewrite(UOp.const((-1, -2**31, 75), dtypes.int32).bitcast(dtypes.uint32).sink()).src
|
||||
self.assertTrue(all(r.op is Ops.CONST and r.dtype == dtypes.uint32 for r in srcs))
|
||||
self.assertEqual(tuple(x.val for x in srcs), (2**32-1, 2**31, 75))
|
||||
|
||||
# folds advance indexing into basic indexing
|
||||
class TestIndexingConstFolding(unittest.TestCase):
|
||||
def test_scalar_index(self):
|
||||
t = Tensor.arange(16).float().reshape(1,1,4,4).clone().realize()
|
||||
_check_ast_count(1, t[:,:,Tensor(1),:])
|
||||
_check_ast_count(1, t[:,:,Tensor(1)+2,:])
|
||||
_check_ast_count(1, t[:,:,Tensor(1),Tensor(0)])
|
||||
|
||||
def test_const_tensor_index(self):
|
||||
# TODO: these can be 0, implement const tensor folded indexing
|
||||
t = Tensor.arange(16).float().reshape(1,1,4,4).clone().realize()
|
||||
_check_ast_count(1, t[:,:,Tensor.ones(2,1,dtype=dtypes.int),:])
|
||||
_check_ast_count(1, t[:,:,Tensor.ones(1,2,dtype=dtypes.int)+2,:])
|
||||
_check_ast_count(1, t[:,:,Tensor.ones(1,1,dtype=dtypes.int),Tensor.zeros(2,1,2,dtype=dtypes.int)])
|
||||
result = full_rewrite(UOp.const((-1, -2**31, 75), dtypes.int32).bitcast(dtypes.uint32).sink())
|
||||
expected = full_rewrite(UOp.const((2**32-1, 2**31, 75), dtypes.uint32).sink())
|
||||
self.assertEqual(result.src, expected.src)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -51,10 +51,6 @@ class TestHelpers(unittest.TestCase):
|
||||
assert dtypes.is_float(dtypes.fp8e4m3)
|
||||
assert dtypes.is_float(dtypes.fp8e5m2)
|
||||
|
||||
@given(strat.sampled_from([d for d in DTYPES_DICT.values() if dtypes.is_float(d) or dtypes.is_int(d)]))
|
||||
def test_scalar(self, dtype):
|
||||
assert dtype.scalar() == dtype
|
||||
|
||||
def test_from_py(self):
|
||||
assert dtypes.from_py(True) == dtypes.bool
|
||||
assert dtypes.from_py(Invalid) == dtypes.bool
|
||||
@@ -110,7 +106,8 @@ class TestHelpers(unittest.TestCase):
|
||||
|
||||
def test_float_to_bf16(self):
|
||||
max_bf16 = torch.finfo(torch.bfloat16).max
|
||||
for a in [1, 1.1, 1234, 23456, -777.777, max_bf16, max_bf16 * 1.00001, -max_bf16, -max_bf16 * 1.00001, math.inf, -math.inf]:
|
||||
for a in [1, 1.1, 1234, 23456, -777.777, max_bf16, max_bf16 * 1.00001, -max_bf16, -max_bf16 * 1.00001,
|
||||
max_bf16 * 2, -max_bf16 * 2, math.inf, -math.inf]:
|
||||
self.assertEqual(float_to_bf16(a), torch.tensor([a], dtype=torch.bfloat16).item())
|
||||
self.assertTrue(math.isnan(float_to_bf16(math.nan)))
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import unittest, subprocess, platform
|
||||
from tinygrad.runtime.support.compiler_cpu import ClangCompiler
|
||||
from tinygrad.runtime.support.elf import elf_loader
|
||||
from tinygrad.runtime.support.c import DLL
|
||||
|
||||
class TestElfLoader(unittest.TestCase):
|
||||
def test_load_clang_jit_strtab(self):
|
||||
@@ -23,7 +24,7 @@ class TestElfLoader(unittest.TestCase):
|
||||
}
|
||||
'''
|
||||
with self.assertRaisesRegex(RuntimeError, 'evil_external_function'):
|
||||
ClangCompiler([{'AMD64':'x86_64', 'aarch64':'arm64'}.get(m:=platform.machine(), m), "native"]).compile(src)
|
||||
elf_loader(ClangCompiler([{'AMD64':'x86_64', 'aarch64':'arm64'}.get(m:=platform.machine(), m), "native"]).compile(src))
|
||||
def test_link(self):
|
||||
src = '''
|
||||
float powf(float, float); // from libm
|
||||
@@ -32,7 +33,7 @@ class TestElfLoader(unittest.TestCase):
|
||||
args = ('-x', 'c', '-c', '-target', f'{platform.machine()}-none-unknown-elf', '-march=native', '-fPIC', '-O2', '-ffreestanding', '-nostdlib')
|
||||
obj = subprocess.check_output(('clang',) + args + ('-', '-o', '-'), input=src.encode())
|
||||
with self.assertRaisesRegex(RuntimeError, 'powf'): elf_loader(obj)
|
||||
elf_loader(obj, link_libs=['m'])
|
||||
elf_loader(obj, link_libs=[DLL('m', 'm')])
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
+14
-179
@@ -1,8 +1,7 @@
|
||||
import unittest, math
|
||||
from tinygrad import dtypes
|
||||
from tinygrad.dtype import AddrSpace
|
||||
from tinygrad.helpers import all_same, Context
|
||||
from tinygrad.uop.ops import GroupOp, UOp, Ops, exec_alu, PatternMatcher, TrackedPatternMatcher, UPat
|
||||
from tinygrad.uop.ops import GroupOp, UOp, Ops, PatternMatcher, TrackedPatternMatcher, UPat
|
||||
from test.helpers import full_rewrite
|
||||
from hypothesis import given, strategies as strat
|
||||
|
||||
@@ -11,125 +10,14 @@ from hypothesis import given, strategies as strat
|
||||
def apply_rewrite(expr):
|
||||
return full_rewrite(expr.sink()).src[0]
|
||||
|
||||
@Context(SPEC=0)
|
||||
def apply_rewrite_values(expr):
|
||||
srcs = full_rewrite(expr.sink()).src
|
||||
if len(srcs) == 1:
|
||||
if srcs[0].op is Ops.CONST: return (srcs[0].val,)
|
||||
if srcs[0].op is Ops.STACK: return tuple(s.val for s in srcs[0].src)
|
||||
return tuple(s.val for s in srcs)
|
||||
|
||||
def evaluate_uop(uop, variables):
|
||||
if uop.op == Ops.CONST:
|
||||
return uop.val
|
||||
elif uop.op == Ops.PARAM and uop.arg.addrspace is AddrSpace.ALU:
|
||||
return variables[uop.expr]
|
||||
elif uop.op in GroupOp.ALU:
|
||||
src_values = [evaluate_uop(src, variables) for src in uop.src]
|
||||
return exec_alu(uop.op, uop.dtype, src_values)
|
||||
else:
|
||||
raise NotImplementedError(f"Unsupported UOp {uop.op}")
|
||||
|
||||
class TestArithmeticSimplifications(unittest.TestCase):
|
||||
def test_full_graph_rewrite_division_by_zero(self):
|
||||
optimized_div_uop = apply_rewrite(UOp.const(10.0) / UOp.const(0.0))
|
||||
self.assertEqual(optimized_div_uop.op, Ops.CONST)
|
||||
self.assertTrue(math.isinf(optimized_div_uop.val) or math.isnan(optimized_div_uop.val))
|
||||
|
||||
def test_full_graph_rewrite_redundant_operations(self):
|
||||
optimized_uop = apply_rewrite((UOp.const(10.0) + UOp.const(0.0)) * UOp.const(1.0))
|
||||
self.assertEqual(optimized_uop.op, Ops.CONST)
|
||||
self.assertEqual(optimized_uop.val, 10.0)
|
||||
|
||||
def test_full_graph_rewrite_large_graph(self):
|
||||
prev_uop = UOp.const(0)
|
||||
for i in range(1, 101):
|
||||
prev_uop += UOp.const(i)
|
||||
optimized_uop = apply_rewrite(prev_uop)
|
||||
self.assertEqual(optimized_uop.op, Ops.CONST)
|
||||
self.assertEqual(optimized_uop.val, sum(range(1, 101)))
|
||||
|
||||
def test_full_graph_rewrite_division_by_one(self):
|
||||
optimized_uop = apply_rewrite(UOp.const(42.0) / UOp.const(1.0))
|
||||
self.assertEqual(optimized_uop.op, Ops.CONST)
|
||||
self.assertEqual(optimized_uop.val, 42.0)
|
||||
|
||||
def test_full_graph_rewrite_modulo_by_one(self):
|
||||
optimized_uop = apply_rewrite(UOp.const(42) % UOp.const(1))
|
||||
self.assertEqual(optimized_uop.op, Ops.CONST)
|
||||
self.assertEqual(optimized_uop.val, 0)
|
||||
|
||||
|
||||
class TestFoldingAndReduction(unittest.TestCase):
|
||||
@unittest.skip("reduce is removed now")
|
||||
def test_full_graph_rewrite_constant_reduction_folding(self):
|
||||
const1 = UOp.const(5)
|
||||
const2 = UOp.const(10)
|
||||
const3 = UOp.const(20)
|
||||
optimized_sink = apply_rewrite((const1 + const2 + const3).reduce(Ops.ADD))
|
||||
expected_sum = 5 + 10 + 20
|
||||
self.assertEqual(optimized_sink.val, expected_sum)
|
||||
|
||||
@unittest.skip("reduce is removed now")
|
||||
def test_full_graph_rewrite_reduction_with_unused_range(self):
|
||||
const1 = UOp.const(15)
|
||||
const2 = UOp.const(25)
|
||||
rng = UOp.range(10, idx=0)
|
||||
optimized_sink = apply_rewrite((const1 + const2).reduce(Ops.ADD, rng))
|
||||
expected_sum = 10 * (15 + 25)
|
||||
self.assertEqual(optimized_sink.val, expected_sum)
|
||||
|
||||
@unittest.skip("currently failing")
|
||||
def test_full_graph_rewrite_range_reduction(self):
|
||||
simple_range = UOp.range(5, idx=0)
|
||||
optimized_sink = apply_rewrite(simple_range.reduce(Ops.ADD, simple_range))
|
||||
expected_sum = sum(range(5))
|
||||
self.assertEqual(optimized_sink.val, expected_sum)
|
||||
|
||||
@unittest.skip("currently failing")
|
||||
def test_full_graph_rewrite_simple_reduction_folding(self):
|
||||
simple_range = UOp.range(4, idx=0)
|
||||
add_uop = simple_range + UOp.const(1)
|
||||
optimized_sink = apply_rewrite(add_uop.reduce(Ops.ADD, simple_range))
|
||||
expected_sum = sum(i + 1 for i in range(4))
|
||||
self.assertEqual(optimized_sink.val, expected_sum)
|
||||
|
||||
@unittest.skip("currently failing")
|
||||
def test_full_graph_rewrite_nested_loop_collapse(self):
|
||||
outer_range = UOp.range(8, 0)
|
||||
inner_range = UOp.range(4, 1)
|
||||
expr = (outer_range * 10) + inner_range
|
||||
optimized_reduce_uop = apply_rewrite(expr.reduce(Ops.ADD, outer_range, inner_range))
|
||||
self.assertEqual(optimized_reduce_uop.op, Ops.CONST)
|
||||
self.assertEqual(optimized_reduce_uop.val, sum((i * 10) + j for i in range(8) for j in range(4)))
|
||||
|
||||
def const_value(uop:UOp):
|
||||
if uop.op is Ops.CAST: uop = uop.src[0]
|
||||
assert uop.op is Ops.CONST
|
||||
return uop.val
|
||||
|
||||
class TestModuloAndDivisionFolding(unittest.TestCase):
|
||||
def test_full_graph_rewrite_modulo_folding_with_define_var(self):
|
||||
# index dtype because div-mod rules only work on index
|
||||
x_var_uop = UOp.variable('x', 0, 100).cast(dtypes.weakint)
|
||||
optimized_mod_uop = apply_rewrite(((x_var_uop * 4) + 2) % 4)
|
||||
self.assertEqual(optimized_mod_uop.op, Ops.CONST)
|
||||
self.assertEqual(optimized_mod_uop.val, 2)
|
||||
|
||||
def test_full_graph_rewrite_division_folding_with_define_var(self):
|
||||
# index dtype because div-mod rules only work on index
|
||||
n_var_uop = UOp.variable('n', 1, 1000).cast(dtypes.weakint)
|
||||
optimized_div_uop = apply_rewrite((n_var_uop * 6) // 3)
|
||||
self.assertEqual(optimized_div_uop.op, Ops.MUL)
|
||||
self.assertEqual(optimized_div_uop.src[1].val, 2)
|
||||
|
||||
def test_full_graph_rewrite_complex_mod_div_folding(self):
|
||||
# index dtype because div-mod rules only work on index
|
||||
k_var_uop = UOp.variable('k', 0, 50).cast(dtypes.weakint)
|
||||
optimized_div_uop = apply_rewrite(((k_var_uop * 12 + 8) % 6) // 2)
|
||||
self.assertEqual(optimized_div_uop.op, Ops.CONST)
|
||||
self.assertEqual(optimized_div_uop.val, 1)
|
||||
|
||||
def test_graph_rewrite_div_folding_bug(self):
|
||||
lhs = UOp(Ops.ADD, src=(
|
||||
UOp(Ops.STACK, arg=None, src=(UOp(Ops.SPECIAL, src=(UOp.const(32),), arg='lidx0'),)*4),
|
||||
UOp.const((0, 256, 512, 768))))
|
||||
lhs = UOp.stack(*(UOp.special(32, 'lidx0'),)*4) + UOp.const((0, 256, 512, 768))
|
||||
rhs = UOp.const((2,)*4)
|
||||
unopt = lhs<rhs
|
||||
opt = apply_rewrite(unopt)
|
||||
@@ -137,74 +25,31 @@ class TestModuloAndDivisionFolding(unittest.TestCase):
|
||||
print(opt)
|
||||
if opt.op is Ops.STACK: self.assertFalse(all_same(opt.src))
|
||||
|
||||
def test_full_graph_rewrite_modulo_large_divisor(self):
|
||||
# index dtype because div-mod rules only work on index
|
||||
x_var_uop = UOp.variable('x', 1, 5)
|
||||
self.assertIs(apply_rewrite(x_var_uop.cast(dtypes.weakint) % 10).render(simplify=False), x_var_uop.render(simplify=False))
|
||||
|
||||
def test_full_graph_rewrite_division_with_remainder(self):
|
||||
x_var_uop = UOp.variable('x', 7, 9, param=True)
|
||||
optimized_sink = apply_rewrite(x_var_uop // 2)
|
||||
for x_value in range(7, 10):
|
||||
self.assertEqual(x_value // 2, evaluate_uop(optimized_sink, {'x': x_value}))
|
||||
|
||||
def test_full_graph_rewrite_complex_mod_div_expression(self):
|
||||
x_var_uop = UOp.variable('x', 1, 10, param=True)
|
||||
optimized_sink = apply_rewrite(((x_var_uop * 5) % 3) // 2)
|
||||
for x_value in range(1, 11):
|
||||
original_result = ((x_value * 5) % 3) // 2
|
||||
optimized_result = evaluate_uop(optimized_sink, {'x': x_value})
|
||||
self.assertEqual(original_result, optimized_result)
|
||||
|
||||
|
||||
class TestEdgeCasesAndSpecialOperations(unittest.TestCase):
|
||||
def test_full_graph_rewrite_transcendental_edge_cases(self):
|
||||
optimized_sink = full_rewrite(UOp.const(-1.0).log2().sink(UOp.const(0.0).reciprocal()))
|
||||
optimized_log2_neg, optimized_recip_zero = optimized_sink.src
|
||||
self.assertTrue(math.isnan(optimized_log2_neg.val), f"Expected NaN for log2(-1.0), got {optimized_log2_neg.val}")
|
||||
self.assertTrue(math.isinf(optimized_recip_zero.val) and optimized_recip_zero.val > 0,
|
||||
f"Expected +inf for reciprocal(0.0), got {optimized_recip_zero.val}")
|
||||
|
||||
@unittest.skip("broken")
|
||||
def test_full_graph_rewrite_modulo_negative_dividend(self):
|
||||
x_var_uop = UOp.variable('x', -5, -1)
|
||||
optimized_sink = full_rewrite((x_var_uop % 3).sink())
|
||||
for x_value in range(-5, 0):
|
||||
self.assertEqual(x_value % 3, evaluate_uop(optimized_sink.src[0], {'x': x_value}))
|
||||
|
||||
@unittest.skip("broken")
|
||||
def test_full_graph_rewrite_division_negative_divisor(self):
|
||||
x_var_uop = UOp.variable('x', 1, 5)
|
||||
optimized_sink = full_rewrite((x_var_uop // -2).sink())
|
||||
for x_value in range(1, 6):
|
||||
self.assertEqual(x_value // -2, evaluate_uop(optimized_sink.src[0], {'x': x_value}))
|
||||
log2_neg, recip_zero = const_value(optimized_log2_neg), const_value(optimized_recip_zero)
|
||||
self.assertTrue(math.isnan(log2_neg), f"Expected NaN for log2(-1.0), got {log2_neg}")
|
||||
self.assertTrue(math.isinf(recip_zero) and recip_zero > 0, f"Expected +inf for reciprocal(0.0), got {recip_zero}")
|
||||
|
||||
class TestGEPAndVectorizeRewrite(unittest.TestCase):
|
||||
def test_gep_single_element_extraction(self):
|
||||
# GEP on a vector dtype to extract a single element
|
||||
base_vector = UOp.const((1.0, 2.0, 3.0, 4.0))
|
||||
self.assertEqual(apply_rewrite(base_vector.index(2)).val, 3.0)
|
||||
self.assertIs(apply_rewrite(base_vector.index(2)), apply_rewrite(base_vector.src[2]))
|
||||
|
||||
def test_gep_tuple_extraction(self):
|
||||
# GEP on a vector dtype to extract multiple elements as a vector
|
||||
base_vector = UOp.const((1.0, 2.0, 3.0, 4.0))
|
||||
self.assertEqual(list(apply_rewrite_values(UOp.stack(*[base_vector.index(i) for i in (2, 3)]))), [3.0, 4.0])
|
||||
|
||||
def test_gep_on_const_stack(self):
|
||||
# GEP on a const STACK to extract a single element
|
||||
const_stack = UOp.const((1.0, 2.0, 3.0, 4.0))
|
||||
self.assertEqual(apply_rewrite(const_stack.index(2)).val, 3.0)
|
||||
|
||||
def test_gep_tuple_on_const_stack(self):
|
||||
# GEP on a const STACK using a tuple to extract multiple elements
|
||||
const_stack = UOp.const((7.0, 8.0, 9.0, 10.0))
|
||||
self.assertEqual(list(apply_rewrite_values(UOp.stack(*[const_stack.index(i) for i in (1, 3)]))), [8.0, 10.0])
|
||||
self.assertIs(apply_rewrite(UOp.stack(*[base_vector.index(i) for i in (2, 3)])),
|
||||
apply_rewrite(UOp.stack(base_vector.src[2], base_vector.src[3])))
|
||||
|
||||
def test_vectorize_multiple_elements(self):
|
||||
# Vectorizing multiple elements using GEP
|
||||
base_vector = UOp.const((5.0, 10.0, 15.0, 20.0))
|
||||
vectorized_uop = UOp(Ops.STACK, src=tuple(base_vector.index(i) for i in range(4)))
|
||||
self.assertEqual(list(apply_rewrite_values(vectorized_uop)), [5.0, 10.0, 15.0, 20.0])
|
||||
vectorized_uop = UOp.stack(*(base_vector.index(i) for i in range(4)))
|
||||
self.assertIs(apply_rewrite(vectorized_uop), apply_rewrite(base_vector))
|
||||
|
||||
|
||||
import inspect
|
||||
@@ -256,16 +101,6 @@ class TestSubstitute(unittest.TestCase):
|
||||
ret = substitute(ret, {a.sin():b})
|
||||
self.assertIs(ret, b.sin())
|
||||
|
||||
# broken due to infinite recursion
|
||||
# NOTE: VIZ hangs and doesn't recover if you click this one
|
||||
@unittest.skip("recursion error no longer raised")
|
||||
def test_assert_inf_recurse(self):
|
||||
a = UOp.variable('a', 0, 10)
|
||||
n1 = a.sin()
|
||||
ret = n1
|
||||
with self.assertRaises(RecursionError):
|
||||
ret = substitute(ret, {n1:n1.sqrt()})
|
||||
|
||||
def test_sin_to_sqrt(self):
|
||||
a = UOp.variable('a', 0, 10, dtype=dtypes.float)
|
||||
n1 = a.sin()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import unittest
|
||||
from tinygrad.helpers import GlobalCounters
|
||||
from tinygrad.nn.datasets import mnist
|
||||
from test.helpers import KernelCountException
|
||||
|
||||
class TestDataset(unittest.TestCase):
|
||||
def test_dataset_is_realized(self):
|
||||
@@ -8,7 +9,7 @@ class TestDataset(unittest.TestCase):
|
||||
X_train[0].contiguous().realize()
|
||||
GlobalCounters.reset()
|
||||
X_train[0].contiguous().realize()
|
||||
self.assertLessEqual(GlobalCounters.kernel_count, 1) # 0 if SLICE (zero-copy), 1 otherwise
|
||||
if GlobalCounters.kernel_count > 1: raise KernelCountException(1, GlobalCounters.kernel_count) # 0 if SLICE (zero-copy), 1 otherwise
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -15,16 +15,12 @@ def simplify_valid_idx(sink: UOp) -> UOp: return graph_rewrite(sink, sym+pm_move
|
||||
def simplify_image_idx(sink: UOp) -> UOp: return graph_rewrite(sink, sym+pm_move_where_on_load+indexing_simplify, name="simplify_image_idx")
|
||||
|
||||
def get_gated_load_uop(valid:UOp, idx:UOp):
|
||||
return UOp(Ops.LOAD, src=(
|
||||
UOp.param(0, dtypes.float, (1024,)).index(idx.valid(valid)),
|
||||
))
|
||||
return UOp.param(0, dtypes.float, (1024,)).index(idx.valid(valid)).load()
|
||||
|
||||
def get_load_image_uop(image_shape:tuple[int, ...], valid:UOp, idx:tuple[UOp, UOp]):
|
||||
return UOp(Ops.LOAD, src=(
|
||||
UOp.param(0, dtypes.float, image_shape).index(idx[1].valid(valid), idx[0].valid(valid)),
|
||||
))
|
||||
return UOp.param(0, dtypes.float, image_shape).index(idx[1].valid(valid), idx[0].valid(valid)).load()
|
||||
|
||||
def Special(expr, nmax): return UOp(Ops.SPECIAL, src=(UOp.const(nmax),), arg=expr)
|
||||
def Special(expr, nmax): return UOp.special(nmax, expr)
|
||||
def Variable(expr, nmin, nmax): return UOp.variable(expr, nmin, nmax, param=True)
|
||||
def Range(n, nmax): return UOp.range(nmax, n)
|
||||
|
||||
@@ -512,7 +508,7 @@ class TestDropTrueGate(unittest.TestCase):
|
||||
buf = UOp.param(0, dtypes.int, (1,))
|
||||
idx = UOp.const(0)
|
||||
true_gate = UOp.const(True)
|
||||
index_with_gate = UOp(Ops.INDEX, src=(buf, idx.valid(true_gate)))
|
||||
index_with_gate = buf.index(idx.valid(true_gate))
|
||||
# apply the optimization
|
||||
result = graph_rewrite(index_with_gate, sym+indexing_simplify)
|
||||
# the True valid should be dropped (INDEX should only have 2 sources)
|
||||
@@ -524,13 +520,17 @@ class TestRangeShrink(unittest.TestCase):
|
||||
result = full_rewrite(sink)
|
||||
return [u for u in result.toposort() if u.op is Ops.RANGE]
|
||||
|
||||
def assert_range_end(self, ranges:list[UOp], end:int):
|
||||
self.assertEqual(len(ranges), 1)
|
||||
with Context(NOOPT=1, SPEC=0): expected = full_rewrite(UOp.const(end, dtypes.int).sink()).src[0]
|
||||
self.assertIs(ranges[0].src[0], expected)
|
||||
|
||||
def test_range_shrink_single_guard(self):
|
||||
# range 0..203 guarded by r < 4 everywhere -> shrink to 0..3
|
||||
r = Range(0, 204)
|
||||
load = get_gated_load_uop(r < UOp.const(4), r)
|
||||
ranges = self.get_ranges(load.sink())
|
||||
self.assertEqual(len(ranges), 1)
|
||||
self.assertEqual(ranges[0].src[0].val, 4)
|
||||
self.assert_range_end(ranges, 4)
|
||||
|
||||
def test_range_shrink_picks_max_guard(self):
|
||||
# two loads guard the same range with r < 4 and r < 8 -> shrink to max(4, 8) = 8
|
||||
@@ -538,25 +538,22 @@ class TestRangeShrink(unittest.TestCase):
|
||||
load1 = get_gated_load_uop(r < UOp.const(4), r)
|
||||
load2 = get_gated_load_uop(r < UOp.const(8), r)
|
||||
ranges = self.get_ranges(UOp.sink(load1, load2))
|
||||
self.assertEqual(len(ranges), 1)
|
||||
self.assertEqual(ranges[0].src[0].val, 8)
|
||||
self.assert_range_end(ranges, 8)
|
||||
|
||||
def test_range_no_shrink_guard_ge_max(self):
|
||||
# guard r < 300 with range max 204 -> no shrink (guard doesn't constrain)
|
||||
r = Range(0, 204)
|
||||
load = get_gated_load_uop(r < UOp.const(300), r)
|
||||
ranges = self.get_ranges(load.sink())
|
||||
self.assertEqual(len(ranges), 1)
|
||||
self.assertEqual(ranges[0].src[0].val, 204)
|
||||
self.assert_range_end(ranges, 204)
|
||||
|
||||
def test_range_no_shrink_when_unguarded_elsewhere(self):
|
||||
# one load guards r < 4, but another load uses r without a gate -> no shrink
|
||||
r = Range(0, 204)
|
||||
load1 = get_gated_load_uop(r < UOp.const(4), r)
|
||||
load2 = UOp(Ops.LOAD, src=(UOp.param(1, dtypes.float, (204,)).index(r),))
|
||||
load2 = UOp.param(1, dtypes.float, (204,)).index(r).load()
|
||||
ranges = self.get_ranges(UOp.sink(load1, load2))
|
||||
self.assertEqual(len(ranges), 1)
|
||||
self.assertEqual(ranges[0].src[0].val, 204)
|
||||
self.assert_range_end(ranges, 204)
|
||||
|
||||
def test_range_no_shrink_when_used_in_reduce(self):
|
||||
# range used in both a gated load AND directly in the reduce expression -> no shrink
|
||||
@@ -564,8 +561,7 @@ class TestRangeShrink(unittest.TestCase):
|
||||
gated_load = get_gated_load_uop(r < UOp.const(4), r)
|
||||
red = (r.cast(dtypes.float) + gated_load).reduce(r, arg=Ops.ADD)
|
||||
ranges = self.get_ranges(red.sink())
|
||||
self.assertEqual(len(ranges), 1)
|
||||
self.assertEqual(ranges[0].src[0].val, 204)
|
||||
self.assert_range_end(ranges, 204)
|
||||
|
||||
def test_range_shrink_to_single_iteration(self):
|
||||
# guard r < 1 shrinks range to 1 -> single iteration, range eliminated entirely
|
||||
@@ -580,8 +576,7 @@ class TestRangeShrink(unittest.TestCase):
|
||||
r = Range(0, 204)
|
||||
x = (r < 4).where(UOp.const(1.0), Invalid)
|
||||
ranges = self.get_ranges(UOp.param(0, dtypes.float, (204,)).index(r).store((r < 4).where(x, Invalid)).sink())
|
||||
self.assertEqual(len(ranges), 1)
|
||||
self.assertEqual(ranges[0].src[0].val, 4)
|
||||
self.assert_range_end(ranges, 4)
|
||||
|
||||
def test_range_shrink_store_where_invalid_flipped(self):
|
||||
# above, but flipped
|
||||
@@ -589,8 +584,7 @@ class TestRangeShrink(unittest.TestCase):
|
||||
r = Range(0, 204)
|
||||
x = (r < 4).where(UOp.const(1.0), Invalid)
|
||||
ranges = self.get_ranges(UOp.param(0, dtypes.float, (204,)).index(r).store((r >= 4).where(Invalid, x)).sink())
|
||||
self.assertEqual(len(ranges), 1)
|
||||
self.assertEqual(ranges[0].src[0].val, 4)
|
||||
self.assert_range_end(ranges, 4)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -69,7 +69,7 @@ class TestIdxUpcast(unittest.TestCase):
|
||||
if not isinstance(Device[Device.DEFAULT].renderer, (PTXRenderer, NIRRenderer)):
|
||||
assert idx.op is Ops.INDEX
|
||||
idx_val = idx.src[1]
|
||||
self.assertFalse(idx_val.overflows(idx_val.dtype.scalar()))
|
||||
self.assertFalse(idx_val.overflows(idx_val.dtype))
|
||||
|
||||
# use expand to generate kernel that uses large idx
|
||||
def do_op_then_assert(self, dtype: DType, dim1, dim2, dim3):
|
||||
@@ -171,6 +171,11 @@ class TestTensorConstLike(unittest.TestCase):
|
||||
t = Tensor.ones(8, 4).shard(("NULL:0", "NULL:1"), axis=0)
|
||||
with self.assertRaises(RuntimeError): t.full_like(5, device="NULL")
|
||||
|
||||
class TestTensorShape(unittest.TestCase):
|
||||
def test_float_shape_raises(self):
|
||||
for dim in (2.0, 2.5):
|
||||
with self.subTest(dim=dim), self.assertRaisesRegex(RuntimeError, "shape must be int"): Tensor.ones(dim)
|
||||
|
||||
class TestTensorDevice(unittest.TestCase):
|
||||
def test_create_from_single_device_tuple(self):
|
||||
(Tensor([1.0], device=(Device.DEFAULT,)) + Tensor([2.0])).realize()
|
||||
|
||||
+27
-146
@@ -1,10 +1,9 @@
|
||||
import unittest, pytest
|
||||
from tinygrad import dtypes, Variable, Device
|
||||
from tinygrad.dtype import AddrSpace
|
||||
from tinygrad.helpers import DEBUG, Context
|
||||
from tinygrad.uop.ops import Ops, UOp, UPat, PatternMatcher, graph_rewrite, GroupOp, AxisType, broadcast_axes, KernelInfo
|
||||
from tinygrad.uop.symbolic import sym
|
||||
from test.helpers import to_uops_list
|
||||
from test.helpers import full_rewrite, to_uops_list
|
||||
from tinygrad.codegen import full_rewrite_to_sink
|
||||
|
||||
simple_pm = PatternMatcher([
|
||||
@@ -14,43 +13,27 @@ simple_pm = PatternMatcher([
|
||||
((UPat.var('x') + UPat.cvar('c1')) + UPat.cvar('c2'), lambda x,c1,c2: x + (c1.val+c2.val)),
|
||||
])
|
||||
|
||||
def const_values(u:UOp):
|
||||
if u.op is Ops.CONST: return (u.val,)
|
||||
if u.op is Ops.STACK: return tuple(x.val for x in u.src)
|
||||
raise AssertionError(f"expected const-like UOp, got {u.op}")
|
||||
|
||||
class TestGraphRewriteConst(unittest.TestCase):
|
||||
def test_gep_const(self):
|
||||
v1 = UOp.const((0,1,2), dtypes.int)
|
||||
v2 = v1.index(1)
|
||||
ret = graph_rewrite(v2, sym)
|
||||
self.assertEqual(ret.dtype, dtypes.int)
|
||||
self.assertEqual(ret.val, 1)
|
||||
self.assertIs(ret, UOp.const(1, dtypes.int))
|
||||
|
||||
def test_add_const(self):
|
||||
v1 = UOp.const((0,1,2))
|
||||
v2 = UOp.const((5,6,7))
|
||||
ret = graph_rewrite(v1+v2, sym)
|
||||
self.assertEqual(ret.op, Ops.STACK)
|
||||
self.assertEqual(const_values(ret), (5,7,9))
|
||||
|
||||
def test_add_const_lose_v(self):
|
||||
v1 = UOp.const((0,1,2))
|
||||
v2 = UOp.const((2,1,0))
|
||||
ret = graph_rewrite(v1+v2, sym)
|
||||
self.assertEqual(ret.op, Ops.STACK)
|
||||
self.assertEqual(const_values(ret), (2,2,2))
|
||||
self.assertIs(graph_rewrite(v1+v2, sym), UOp.const((5,7,9)))
|
||||
|
||||
def xfail_broken_const_wraparound(fn):
|
||||
fn = pytest.mark.xfail(reason="const folding does not properly implement modular arithmetic")(fn)
|
||||
return unittest.expectedFailure(fn)
|
||||
class TestModularWraparound(unittest.TestCase):
|
||||
def _test(self, uop:UOp, expected:int):
|
||||
results = to_uops_list([uop])
|
||||
self.assertEqual(len(results), 2) # +1 for SINK
|
||||
self.assertEqual(results[0].op, Ops.CONST)
|
||||
self.assertEqual(results[0].dtype, uop.dtype)
|
||||
self.assertEqual(results[0].val, expected)
|
||||
result = uop.simplify()
|
||||
self.assertEqual(result.op, Ops.CONST)
|
||||
self.assertEqual(result.dtype, uop.dtype)
|
||||
self.assertEqual(result.val, expected)
|
||||
|
||||
@xfail_broken_const_wraparound
|
||||
def test_cast(self):
|
||||
@@ -191,63 +174,25 @@ class TestGraphRewrite(unittest.TestCase):
|
||||
self.assertEqual(len([x for x in sink.toposort() if x.op is Ops.CONST]), 1)
|
||||
|
||||
class TestUOpGraph(unittest.TestCase):
|
||||
def test_add_constant_fold(self):
|
||||
c1 = UOp.const(1.0, dtypes.float)
|
||||
c2 = UOp.const(2.0, dtypes.float)
|
||||
out = c1+c2
|
||||
uops = to_uops_list([out])
|
||||
self.assertEqual(len(uops), 2) # +1 for SINK
|
||||
out = uops[-2]
|
||||
self.assertEqual(out.op, Ops.CONST)
|
||||
self.assertEqual(out.val, 3.0)
|
||||
|
||||
def test_where_same_fold(self):
|
||||
v = UOp.variable('tmp', 0, 1)
|
||||
c0 = UOp.const(0)
|
||||
vc = v != c0
|
||||
c1 = UOp.const(1.0, dtypes.float)
|
||||
out = vc.where(c1, c1)
|
||||
uops = to_uops_list([out])
|
||||
self.assertEqual(len(uops), 2) # +1 for SINK
|
||||
out = uops[-2]
|
||||
self.assertEqual(out.op, Ops.CONST)
|
||||
self.assertEqual(out.val, 1.0)
|
||||
self.assertIs(out.simplify(), c1)
|
||||
|
||||
def test_where_const_fold(self):
|
||||
bf = UOp.const(False)
|
||||
c1 = UOp.const(1.0, dtypes.float)
|
||||
c2 = UOp.const(2.0, dtypes.float)
|
||||
out = bf.where(c1, c2)
|
||||
uops = to_uops_list([out])
|
||||
self.assertEqual(len(uops), 2) # +1 for SINK
|
||||
out = uops[-2]
|
||||
self.assertEqual(out.op, Ops.CONST)
|
||||
self.assertEqual(out.val, 2.0)
|
||||
self.assertIs(out.simplify(), c2)
|
||||
|
||||
def test_const_cast(self):
|
||||
bf = UOp.const(False)
|
||||
out = bf.cast(dtypes.int)
|
||||
uops = to_uops_list([out])
|
||||
self.assertEqual(len(uops), 2) # +1 for SINK
|
||||
out = uops[-2]
|
||||
self.assertEqual(out.op, Ops.CONST)
|
||||
self.assertEqual(out.val, 0)
|
||||
|
||||
def test_const_bitcast(self):
|
||||
bf = UOp.const(1.0, dtypes.float)
|
||||
out = bf.bitcast(dtypes.uint32)
|
||||
uops = to_uops_list([out])
|
||||
self.assertEqual(len(uops), 2) # +1 for SINK
|
||||
out = uops[-2]
|
||||
self.assertEqual(out.op, Ops.CONST)
|
||||
self.assertEqual(out.val, 0x3F800000)
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_const_shape_change_bitcast(self):
|
||||
bf = UOp.const(0x3F).cast(dtypes.uint8)
|
||||
out = bf.bitcast(dtypes.half)
|
||||
uops = to_uops_list([out])
|
||||
self.assertEqual(len(uops), 2) # +1 for SINK
|
||||
self.assertIs(full_rewrite(out.sink()).src[0], full_rewrite(UOp.const(0, dtypes.int).sink()).src[0])
|
||||
|
||||
def test_devectorize_derives_lane_dtype(self):
|
||||
from tinygrad.codegen import do_devectorize
|
||||
@@ -257,66 +202,11 @@ 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)
|
||||
|
||||
@unittest.skip("this test isn't valid uops")
|
||||
def test_noop_vectorize_fold(self):
|
||||
d0 = UOp.param(0, dtypes.float, (1,))
|
||||
idx = UOp.const(0)
|
||||
ld = d0.load(idx, dtype=dtypes.float)
|
||||
vec = UOp(Ops.STACK, dtypes.float, (ld,))
|
||||
x = vec.index(0)
|
||||
alu = UOp(Ops.SQRT, src=(x, ))
|
||||
out = UOp(Ops.STORE, src=(d0, idx, alu))
|
||||
uops = to_uops_list([out])
|
||||
self.assertEqual(len([x for x in uops if x.op is Ops.STACK]), 0)
|
||||
|
||||
@unittest.skip("this test isn't valid uops")
|
||||
def test_gep_vec_fold(self):
|
||||
d0 = UOp.param(0, dtypes.float, (1,))
|
||||
d1 = UOp.param(1, dtypes.float, (1,))
|
||||
d2 = UOp.param(2, dtypes.float, (1,))
|
||||
idx = UOp.const(0)
|
||||
def _test_vec(geps, count=4):
|
||||
vec = UOp(Ops.STACK, dtypes.float, geps)
|
||||
out = d0.index(idx).store(vec)
|
||||
uops = to_uops_list([out])
|
||||
if DEBUG >= 4:
|
||||
from tinygrad import Device
|
||||
print(Device[Device.DEFAULT].renderer.render(uops))
|
||||
return uops[-2].src[-1] # -2 to skip SINK
|
||||
|
||||
# possible
|
||||
val = d1.index(idx).load(dtype=dtypes.float)
|
||||
xyzw = tuple(val.index(i) for i in range(4))
|
||||
self.assertIs(_test_vec(xyzw).op, Ops.LOAD)
|
||||
|
||||
# unaligned
|
||||
val = d1.index(idx).load(dtype=dtypes.float)
|
||||
wzyx = tuple(val.index(i) for i in reversed(range(4)))
|
||||
self.assertIs(_test_vec(wzyx).op, Ops.STACK)
|
||||
|
||||
# different_size
|
||||
val = d1.index(idx).load(dtype=dtypes.float)
|
||||
xy = tuple(val.index(i) for i in range(2))
|
||||
self.assertIs(_test_vec(xy+xy).op, Ops.STACK)
|
||||
val = d1.index(idx).load(dtype=dtypes.float)
|
||||
xy = tuple(val.index(i) for i in range(2))
|
||||
self.assertIs(_test_vec(xy, count=2).op, Ops.STACK)
|
||||
|
||||
# different vals
|
||||
val1 = d1.index(idx).load(dtype=dtypes.float)
|
||||
val2 = d2.index(idx).load(dtype=dtypes.float)
|
||||
xy1 = tuple(val1.index(i) for i in range(2))
|
||||
xy2 = tuple(val2.index(i) for i in range(2))
|
||||
self.assertIs(_test_vec(xy1+xy2).op, Ops.STACK)
|
||||
|
||||
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)]
|
||||
vec = UOp(Ops.STACK, src=tuple(consts))
|
||||
with Context(SPEC=0):
|
||||
uops = to_uops_list([vec.index(i) for i in range(vec_size)])
|
||||
for uop, const in zip(uops, consts):
|
||||
self.assertEqual(uop, const)
|
||||
vec = UOp.stack(*consts)
|
||||
for i, const in enumerate(consts): self.assertIs(vec.index(i), const)
|
||||
|
||||
def test_cast_alu_fold(self):
|
||||
d0 = UOp.param(0, dtypes.bool, (1,))
|
||||
@@ -326,7 +216,7 @@ class TestUOpGraph(unittest.TestCase):
|
||||
alu = (ld<1).cast(dtypes.bool)
|
||||
out = d0.index(idx).store(alu)
|
||||
uops = to_uops_list([out])
|
||||
self.assertEqual(len([x for x in uops if x.op is Ops.CAST]), 0)
|
||||
self.assertEqual(len([x for x in uops if x.op is Ops.CAST and x.src[0].op is not Ops.CONST]), 0)
|
||||
|
||||
def test_double_cast_fold(self):
|
||||
d0 = UOp.param(0, dtypes.float, (1,))
|
||||
@@ -336,7 +226,7 @@ class TestUOpGraph(unittest.TestCase):
|
||||
alu = ld.cast(dtypes.float).cast(dtypes.float)
|
||||
out = d0.index(idx).store(alu)
|
||||
uops = to_uops_list([out])
|
||||
self.assertEqual(len([x for x in uops if x.op is Ops.CAST]), 1)
|
||||
self.assertEqual(len([x for x in uops if x.op is Ops.CAST and x.src[0].op is not Ops.CONST]), 1)
|
||||
|
||||
def test_depth_2_const_fold(self):
|
||||
v = UOp.variable("tmp", 0, 1, dtypes.int, param=True)
|
||||
@@ -344,12 +234,7 @@ class TestUOpGraph(unittest.TestCase):
|
||||
c4 = UOp.const(4, dtypes.int)
|
||||
vc = v+c2
|
||||
out = vc+c4
|
||||
uops = to_uops_list([out])
|
||||
self.assertEqual(len(uops), 5) # +1 for SINK, +1 for the PARAM shape STACK
|
||||
out = uops[-2] # -2 to skip SINK
|
||||
self.assertEqual(out.op, Ops.ADD)
|
||||
self.assertEqual(out.src[1].op, Ops.CONST)
|
||||
self.assertEqual(out.src[1].val, 6)
|
||||
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,):
|
||||
@@ -360,9 +245,8 @@ class TestUOpGraph(unittest.TestCase):
|
||||
|
||||
def test_sub_with_cast_folds(self):
|
||||
a = Variable("a", 0, 5)
|
||||
uops = to_uops_list([a.cast(dtypes.int)+(-a).cast(dtypes.int)])
|
||||
assert uops[0] == UOp.const(0, dtypes.int)
|
||||
assert uops[-1].op == Ops.SINK
|
||||
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):
|
||||
ridx0 = UOp.range(100, 0)
|
||||
@@ -371,9 +255,10 @@ class TestUOpGraph(unittest.TestCase):
|
||||
w = (ridx0<50).where(ld, 5)
|
||||
out = UOp.param(1, dtypes.long, (100,))
|
||||
uops = to_uops_list([out.index(ridx0).store(w)])
|
||||
expected = full_rewrite(UOp.const(5, dtypes.long).sink()).src[0]
|
||||
for u in uops:
|
||||
assert u.op is not Ops.WHERE
|
||||
if u.op is Ops.LOAD and u.src[0].src[0].op is Ops.PARAM: assert u.src[1].val==5
|
||||
if u.op is Ops.LOAD and u.src[0].src[0].op is Ops.PARAM: self.assertIs(u.src[1], expected)
|
||||
|
||||
def test_where_on_gated_load_folds_swapped_branches(self):
|
||||
ridx0 = UOp.range(100, 0)
|
||||
@@ -381,9 +266,10 @@ class TestUOpGraph(unittest.TestCase):
|
||||
ld = d0.index(ridx0.valid((ridx0<50).logical_not()))
|
||||
w = (ridx0<50).where(5, ld)
|
||||
uops = to_uops_list([w])
|
||||
expected = full_rewrite(UOp.const(5, dtypes.long).sink()).src[0]
|
||||
for u in uops:
|
||||
assert u.op is not Ops.WHERE
|
||||
if u.op is Ops.LOAD: assert u.src[1].val==5
|
||||
if u.op is Ops.LOAD: self.assertIs(u.src[1], expected)
|
||||
|
||||
def test_where_on_gated_load_with_cast(self):
|
||||
ridx0 = UOp.range(100, 0)
|
||||
@@ -393,9 +279,10 @@ class TestUOpGraph(unittest.TestCase):
|
||||
w = (ridx0<50).where(ld, 5.0)
|
||||
out = UOp.param(1, dtypes.float, (100,))
|
||||
uops = to_uops_list([out.index(ridx0).store(w)])
|
||||
expected = full_rewrite(UOp.const(5, dtypes.int).sink()).src[0]
|
||||
for u in uops:
|
||||
assert u.op is not Ops.WHERE
|
||||
if u.op is Ops.LOAD and u.src[0].src[0].op is Ops.PARAM: assert u.src[1].val == 5
|
||||
if u.op is Ops.LOAD and u.src[0].src[0].op is Ops.PARAM: self.assertIs(u.src[1], expected)
|
||||
|
||||
def test_where_on_casted_gated_load_extra_cond(self):
|
||||
ridx0 = UOp.range(100, 0)
|
||||
@@ -425,9 +312,10 @@ class TestUOpGraph(unittest.TestCase):
|
||||
val = (ridx0<50).where(5, ld)
|
||||
st = idx.store(val).end(ridx0)
|
||||
uops = to_uops_list([st])
|
||||
expected = full_rewrite(UOp.const(5, dtypes.long).sink()).src[0]
|
||||
for u in uops:
|
||||
assert u.op is not Ops.WHERE
|
||||
if u.op is Ops.STORE: assert u.src[1].val==5
|
||||
if u.op is Ops.STORE: self.assertIs(u.src[1], expected)
|
||||
|
||||
def test_load_idx_becomes_int(self):
|
||||
# mnist indexing with split reduceop
|
||||
@@ -501,13 +389,6 @@ class TestUOpGraph(unittest.TestCase):
|
||||
# only the second store happens
|
||||
self.assertEqual(len([u for u in uops if u.op is Ops.STORE]), 1)
|
||||
|
||||
@unittest.skip("this is a uop type error")
|
||||
def test_asserts_bad_gate(self):
|
||||
glbl0 = UOp.param(0, dtypes.int, (1,))
|
||||
idx = UOp.const(0)
|
||||
bad_gate = UOp.const(1)
|
||||
with self.assertRaises(AssertionError): to_uops_list([UOp(Ops.STORE, src=(glbl0, idx, UOp.const(42), bad_gate))])
|
||||
|
||||
def test_after_end(self):
|
||||
r = UOp.range(10, 0)
|
||||
|
||||
@@ -575,7 +456,7 @@ class TestConstBufferize(unittest.TestCase):
|
||||
from tinygrad.schedule.rangeify import pm_const_buffer_folding, BufferizeOpts
|
||||
c = UOp.const(42.0)
|
||||
r1 = UOp.range(3, 0)
|
||||
bufferize_with_range = UOp(Ops.STAGE, src=(c, r1), arg=BufferizeOpts(device="CPU"))
|
||||
bufferize_with_range = c.bufferize(r1, arg=BufferizeOpts(device="CPU"))
|
||||
self.assertEqual(len(bufferize_with_range.src), 2) # const + 1 range
|
||||
|
||||
result = graph_rewrite(bufferize_with_range, pm_const_buffer_folding, name='test')
|
||||
@@ -590,7 +471,7 @@ class TestConstBufferize(unittest.TestCase):
|
||||
c = UOp.const(3.14)
|
||||
r1 = UOp.range(3, 0)
|
||||
r2 = UOp.range(4, 1)
|
||||
bufferize_with_ranges = UOp(Ops.STAGE, src=(c, r1, r2), arg=BufferizeOpts(device="CPU"))
|
||||
bufferize_with_ranges = c.bufferize(r1, r2, arg=BufferizeOpts(device="CPU"))
|
||||
self.assertEqual(len(bufferize_with_ranges.src), 3) # const + 2 ranges
|
||||
|
||||
result = graph_rewrite(bufferize_with_ranges, pm_const_buffer_folding, name='test')
|
||||
|
||||
@@ -1013,6 +1013,21 @@ class TestSymbolic(unittest.TestCase):
|
||||
b = Variable("b", 0, 3)
|
||||
self.helper_test_variable(-a<-b, False, True, "(b<a)")
|
||||
|
||||
def test_where_cast(self):
|
||||
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), 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):
|
||||
a = Variable("a", 0, 3, dtypes.half)
|
||||
self.assertIs(graph_rewrite(UOp.const(True, dtypes.bool).where(uconst(0.0), a), sym), UOp.const(0.0, dtypes.half))
|
||||
self.assertIs(graph_rewrite(UOp.const(True, dtypes.bool).where(uconst(0), Variable("i", 0, 3, dtypes.int)), sym), UOp.const(0, dtypes.int))
|
||||
self.assertIs(graph_rewrite(UOp.const(False, dtypes.bool).where(uconst(0.0), a), sym), a)
|
||||
self.assertIs(graph_rewrite(UOp.const(False, dtypes.bool).where(uconst(0.0), UOp.invalid()), sym), UOp.invalid())
|
||||
self.assertIs(graph_rewrite(UOp.const(True, dtypes.bool).where(uconst(0.0), uconst(1)), sym), uconst(0.0))
|
||||
|
||||
def test_where_merge_branches(self):
|
||||
cond1 = Variable("s", 0, 10) < 6
|
||||
cond2 = Variable("s", 0, 10) > 2
|
||||
|
||||
+11
-9
@@ -5,7 +5,7 @@ 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, ParamArg, PatternMatcher, UOp, UPat, dtype_from_uop, exec_alu, graph_rewrite # noqa: F401 # ParamArg used by eval(str(uop)) roundtrip tests
|
||||
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_index_dtype
|
||||
from tinygrad.uop.spec import spec_program, spec_shared, type_verify
|
||||
from tinygrad.uop.symbolic import sym, pm_remove_invalid
|
||||
@@ -41,11 +41,6 @@ class TestDTypeFromUOp(unittest.TestCase):
|
||||
# an explicit (strong) const dtype is legal until the field is removed
|
||||
self.assertEqual(UOp.const(3, dtypes.int32).dtype, dtypes.int32)
|
||||
|
||||
def test_weak_dtype_rejected_by_program_spec(self):
|
||||
for weak, concrete, value in ((dtypes.weakint, dtypes.int32, 1), (dtypes.weakfloat, dtypes.float32, 1.0)):
|
||||
with self.assertRaises(RuntimeError): type_verify(UOp.const(value, weak).sink(), spec_program)
|
||||
type_verify(UOp.const(value, concrete).sink(), spec_program)
|
||||
|
||||
def test_invalid_stated_dtype(self):
|
||||
# UOp.const normalizes a stated dtype away (const_like/full pass their position's); the core constructor does not,
|
||||
# and the spec is what rejects a non-bool Invalid
|
||||
@@ -134,7 +129,7 @@ class TestConstFloatEq(unittest.TestCase):
|
||||
self.assertFalse(Invalid != HoldsInvalid())
|
||||
|
||||
def test_matchers_agree_on_nan(self):
|
||||
n = UOp.const(math.nan, dtypes.float32)
|
||||
n = UOp.const(math.nan)
|
||||
for compiled in (False, True):
|
||||
pm = PatternMatcher([(UPat(Ops.CONST, arg=math.nan), lambda: True)], compiled=compiled)
|
||||
self.assertTrue(pm.rewrite(n), f"{compiled=}")
|
||||
@@ -348,10 +343,9 @@ class TestFastIdiv(unittest.TestCase):
|
||||
def test_fast_idiv_remove_powers_of_two(self):
|
||||
ridx = UOp.range(2**20, 0)
|
||||
uops = to_uops_list([ridx//(7*64)], ren=Device[Device.DEFAULT].renderer)
|
||||
ops = [x.op for x in uops]
|
||||
# this requires shifting out the powers of two before doing fast_idiv
|
||||
# (((ridx0>>6)*18725)>>17) instead of (int)((((long)(ridx0)*1198373)>>29))
|
||||
self.assertNotIn(Ops.CAST, ops)
|
||||
self.assertNotIn(dtypes.long, [x.dtype for x in uops])
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_fast_idiv_overflow(self):
|
||||
@@ -463,6 +457,14 @@ 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, dtype=dtypes.void, src=())
|
||||
self.assertEqual(u.render(simplify=False), "{}")
|
||||
|
||||
+25
-11
@@ -1,5 +1,5 @@
|
||||
import unittest, decimal, sys, json, contextlib, tempfile, pickle, io, math
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
import decimal, sys, json, contextlib, tempfile, pickle, io, math, pathlib
|
||||
from dataclasses import dataclass
|
||||
from typing import Generator
|
||||
|
||||
@@ -43,7 +43,7 @@ def save_viz():
|
||||
Buffer.profile_events.clear()
|
||||
cpu_events.clear()
|
||||
viz = VizTrace()
|
||||
with Context(VIZ=-1, TRACK_MATCH_STATS=2, PROFILE=1):
|
||||
with Context(VIZ=-1, TRACK_MATCH_STATS=2, PROFILE=1, PARALLEL=0):
|
||||
yield viz
|
||||
viz.set_data()
|
||||
|
||||
@@ -236,8 +236,8 @@ class TestViz(unittest.TestCase):
|
||||
def test_const_node_visibility(self):
|
||||
with save_viz() as viz:
|
||||
a = UOp.variable("a", 0, 10, dtype=dtypes.int)
|
||||
z = UOp.const(0, a.dtype)
|
||||
y = UOp.const(math.pi, dtypes.float)
|
||||
z = UOp.const(0)
|
||||
y = UOp.const(math.pi)
|
||||
alu = a*z
|
||||
ret = exec_rewrite(sink:=UOp.sink(alu, y), [sym])
|
||||
lst = viz.list_items()
|
||||
@@ -249,7 +249,7 @@ class TestViz(unittest.TestCase):
|
||||
self.assertTrue(graphs[0][id(y)]["exclude"])
|
||||
self.assertFalse(graphs[0][id(alu)]["exclude"])
|
||||
self.assertEqual(graphs[0][id(y)]["label"].split("\n")[:2], ["CONST", "3.14159"])
|
||||
self.assertEqual(list(graphs[1]), [id(z), id(y), id(ret)])
|
||||
self.assertEqual(list(graphs[1]), [id(u) for u in ret.toposort()]) # rewrite graph keys follow the rewritten sink's toposort
|
||||
|
||||
def test_const_reshape_expand_folded(self):
|
||||
# CONST->EXPAND should be folded into the ALU node, not shown as separate EXPAND nodes
|
||||
@@ -516,6 +516,22 @@ 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
|
||||
@@ -819,8 +835,6 @@ 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:
|
||||
@@ -829,7 +843,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=f"NULL::{self.arch}"):
|
||||
with Context(DEV="NULL::gfx1100"):
|
||||
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)
|
||||
@@ -1011,8 +1025,8 @@ def run_cli(*cli_args) -> list[dict]:
|
||||
@contextlib.contextmanager
|
||||
def write_files(viz) -> list[str]:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
(r:=Path(tmpdir)/"rewrites.pkl").write_bytes(pickle.dumps(viz.data.trace))
|
||||
(p:=Path(tmpdir)/"profile.pkl").write_bytes(pickle.dumps(cpu_events))
|
||||
(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))
|
||||
yield ["--rewrites-path", str(r), "--profile-path", str(p)]
|
||||
|
||||
class TestCLI(unittest.TestCase):
|
||||
|
||||
@@ -10,12 +10,12 @@ from test.helpers import replace_opts
|
||||
class TestFloat4(unittest.TestCase):
|
||||
@staticmethod
|
||||
def count_float4(uops: list[UOp], n=4):
|
||||
return (len([uop for uop in uops if uop.op is Ops.LOAD and uop.dtype.scalar() == dtypes.float and uop.shape == (4,)]),
|
||||
len([uop for uop in uops if uop.op is Ops.STORE and uop.src[1].dtype.scalar() == dtypes.float and uop.shape == (4,)]))
|
||||
return (len([uop for uop in uops if uop.op is Ops.LOAD and uop.dtype == dtypes.float and uop.shape == (4,)]),
|
||||
len([uop for uop in uops if uop.op is Ops.STORE and uop.src[1].dtype == dtypes.float and uop.shape == (4,)]))
|
||||
@staticmethod
|
||||
def count_half4(uops: list[UOp]):
|
||||
return (len([uop for uop in uops if uop.op is Ops.LOAD and uop.dtype.scalar() == dtypes.half and uop.shape == (4,)]),
|
||||
len([uop for uop in uops if uop.op is Ops.STORE and uop.src[1].dtype.scalar() == dtypes.half and uop.shape == (4,)]))
|
||||
return (len([uop for uop in uops if uop.op is Ops.LOAD and uop.dtype == dtypes.half and uop.shape == (4,)]),
|
||||
len([uop for uop in uops if uop.op is Ops.STORE and uop.src[1].dtype == dtypes.half and uop.shape == (4,)]))
|
||||
|
||||
def test_float4_basic(self):
|
||||
a = Tensor.empty(2, 8).realize()
|
||||
|
||||
@@ -2,6 +2,7 @@ import unittest
|
||||
from tinygrad import Tensor, UOp, dtypes
|
||||
from tinygrad.helpers import Context
|
||||
from tinygrad.uop.ops import Ops
|
||||
from test.helpers import KernelCountException
|
||||
|
||||
class TestRingAllReduce(unittest.TestCase):
|
||||
def test_schedule_ring(self):
|
||||
@@ -13,7 +14,7 @@ class TestRingAllReduce(unittest.TestCase):
|
||||
copies = [si for si in linear.src if si.src[0].op is Ops.COPY]
|
||||
pairs = [(c.src[1].buffer.device, c.src[2].buffer.device) for c in copies]
|
||||
# N*(N-1) scatter reduce, and N*(N-1) allgather
|
||||
self.assertEqual(len(pairs), N*(N-1)*2)
|
||||
if len(pairs) != N*(N-1)*2: raise KernelCountException(N*(N-1)*2, len(pairs))
|
||||
# copy topology forms a ring
|
||||
self.assertEqual(len(set(pairs)), N)
|
||||
|
||||
@@ -25,8 +26,8 @@ class TestRingAllReduce(unittest.TestCase):
|
||||
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]
|
||||
self.assertEqual(len(copies), 24)
|
||||
self.assertEqual(len(sinks), 26)
|
||||
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):
|
||||
@@ -39,8 +40,8 @@ class TestRingAllReduce(unittest.TestCase):
|
||||
sinks = [si for si in linear.src if si.src[0].op is Ops.SINK]
|
||||
pairs = [(c.src[1].buffer.device, c.src[2].buffer.device) for c in copies]
|
||||
|
||||
self.assertEqual(len(pairs), N*(N-1))
|
||||
self.assertEqual(len(sinks), 2)
|
||||
if len(pairs) != N*(N-1): raise KernelCountException(N*(N-1), len(pairs))
|
||||
if len(sinks) != 2: raise KernelCountException(2, len(sinks))
|
||||
self.assertTrue(all(dst != src for dst, src in pairs))
|
||||
|
||||
def test_symbolic_shape(self):
|
||||
@@ -64,7 +65,7 @@ class TestAllreduceCast(unittest.TestCase):
|
||||
with Context(ALLREDUCE_CAST=allreduce_cast, RING=0, SCACHE=0):
|
||||
t = Tensor.empty(4, 4, dtype=dtype).shard(ds, axis=0)
|
||||
linear = t.sum(0).linear_with_vars()[0]
|
||||
return {si.src[1].buffer.dtype.scalar() for si in linear.src if si.src[0].op is Ops.COPY}
|
||||
return {si.src[1].buffer.dtype for si in linear.src if si.src[0].op is Ops.COPY}
|
||||
|
||||
def test_allreduce_cast_bf16(self):
|
||||
# with ALLREDUCE_CAST, allreduce copies stay in bfloat16 instead of promoting to float32
|
||||
|
||||
@@ -215,6 +215,29 @@ class TestGatedDeltaNetBlock(unittest.TestCase):
|
||||
np.testing.assert_allclose(prefill_conv, decode_conv, rtol=1e-3, atol=1e-3)
|
||||
np.testing.assert_allclose(prefill_recurrent, decode_recurrent, rtol=1e-3, atol=1e-3)
|
||||
|
||||
def test_varied_chunk_sizes_match_decode(self):
|
||||
for kda in (False, True):
|
||||
ssm = SSMConfig(conv_kernel=2, state_size=32, group_count=1, time_step_rank=1, inner_size=32, kda=kda)
|
||||
config = self._make_config(ssm=ssm)
|
||||
if kda:
|
||||
block = GatedDeltaNetBlock(config, config.ssm)
|
||||
for p in nn.state.get_parameters(block):
|
||||
p.replace(self._tensor_linspace(-0.05, 0.05, p.shape) if len(p.shape) > 1 else self._tensor_linspace(0.05, 0.1, p.shape))
|
||||
else: block = self._make_block(config)
|
||||
x = self._tensor_linspace(-0.5, 0.5, (1, 4, config.dim))
|
||||
decode = np.concatenate([self._run_attention(block, x[:, i:i+1], i) for i in range(4)], axis=1)
|
||||
decode_conv, decode_recurrent = self._cache_views(block)
|
||||
for chunking in ([4], [2, 2], [1, 3], [3, 1], [2, 1, 1]):
|
||||
self._reset_state(block)
|
||||
outs, start = [], 0
|
||||
for size in chunking:
|
||||
outs.append(self._run_attention(block, x[:, start:start+size], start))
|
||||
start += size
|
||||
chunked_conv, chunked_recurrent = self._cache_views(block)
|
||||
np.testing.assert_allclose(np.concatenate(outs, axis=1), decode, rtol=1e-3, atol=1e-3, err_msg=f"{kda=} {chunking=}")
|
||||
np.testing.assert_allclose(chunked_conv, decode_conv, rtol=1e-3, atol=1e-3, err_msg=f"{kda=} {chunking=}")
|
||||
np.testing.assert_allclose(chunked_recurrent, decode_recurrent, rtol=1e-3, atol=1e-3, err_msg=f"{kda=} {chunking=}")
|
||||
|
||||
def test_start_zero_resets_realized_state(self):
|
||||
config, x = self._make_config(max_context=3), self._tensor_linspace(-1, 1, (1, 3, 32))
|
||||
block = self._make_block(config)
|
||||
|
||||
@@ -224,6 +224,21 @@ 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)
|
||||
|
||||
@@ -3,11 +3,12 @@ import tempfile, unittest, math
|
||||
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, dtype_from_uop, graph_rewrite
|
||||
from tinygrad.uop.ops import UOp, Ops, GroupOp, dtype_from_uop, graph_rewrite
|
||||
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
|
||||
from test.helpers import full_rewrite
|
||||
|
||||
|
||||
class TestWeakPromotion(unittest.TestCase):
|
||||
@@ -288,5 +289,18 @@ class TestSignedUint64Weakfloat(unittest.TestCase):
|
||||
self.assertAlmostEqual((i64 + u64).sin().item(), math.sin(2), places=5) # Unary lowers before transcendental
|
||||
|
||||
|
||||
class TestNoRedundantWide(unittest.TestCase):
|
||||
def wide_alu(self, t:Tensor) -> int:
|
||||
return sum(sum(1 for u in full_rewrite(call.src[0]).toposort() if u.op in GroupOp.ALU and u.dtype in {dtypes.long, dtypes.ulong})
|
||||
for call in t.schedule_linear().src if call.src[0].op is Ops.SINK)
|
||||
|
||||
def test_unbounded_long_stays_long(self):
|
||||
self.assertGreater(self.wide_alu(Tensor.empty(16, dtype=dtypes.long)*3 + 1), 0)
|
||||
|
||||
def test_fancy_index_has_no_wide_alu(self):
|
||||
j, o = Tensor([0, 1, 2]).reshape(3, 1), Tensor([0, 1]).reshape(1, 2)
|
||||
self.assertEqual(self.wide_alu(Tensor.empty(8, 9, 10, 11, 12)[1, j, 2, o, 2]), 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -4,7 +4,7 @@ from tinygrad.function import function
|
||||
from tinygrad import Tensor, GlobalCounters, Device
|
||||
from tinygrad.dtype import Invalid
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo, ProgramInfo
|
||||
from test.helpers import assert_kernel_count
|
||||
from test.helpers import assert_kernel_count, KernelCountException
|
||||
|
||||
class TestFunction(unittest.TestCase):
|
||||
def test_simple(self):
|
||||
@@ -516,7 +516,7 @@ class TestFunctionTuple(unittest.TestCase):
|
||||
Tensor.realize(a)
|
||||
c = f(a)
|
||||
|
||||
self.assertEqual(count_kernels(c), 1)
|
||||
if count_kernels(c) != 1: raise KernelCountException(1, count_kernels(c))
|
||||
|
||||
c.sum().backward()
|
||||
Tensor.realize(a.grad)
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import unittest
|
||||
import numpy as np
|
||||
from tinygrad import Tensor, UOp
|
||||
from tinygrad.llm.model import gated_delta_prefill
|
||||
|
||||
def numpy_ref(q, k, v, beta, alpha, initial):
|
||||
state, out = initial.copy(), np.empty_like(v)
|
||||
for t in range(q.shape[2]):
|
||||
av = alpha[:, :, t, :, None] if alpha.ndim == 4 else alpha[:, :, t, None, None]
|
||||
sa = alpha[:, :, t] if alpha.ndim == 4 else alpha[:, :, t, None]
|
||||
previous = state.copy()
|
||||
delta = (v[:, :, t] - (previous*k[:, :, t, None]).sum(-1)*sa) * beta[:, :, t, None]
|
||||
state = previous*av + delta[..., None]*k[:, :, t, None, :]
|
||||
out[:, :, t] = (previous*q[:, :, t, None]).sum(-1)*sa + delta*(q[:, :, t]*k[:, :, t]).sum(-1, keepdims=True)
|
||||
return out, state
|
||||
|
||||
class TestGatedDeltaPrefill(unittest.TestCase):
|
||||
def _make(self, B, H, T, V, K, alpha_4d=False, seed=42):
|
||||
rng = np.random.default_rng(seed)
|
||||
# normalize like the model does: with raw unit-norm keys the delta rule is stable, random keys make it diverge
|
||||
q, k = (rng.normal(size=(B, H, T, K)).astype(np.float32) for _ in range(2))
|
||||
k = k / np.maximum(np.sqrt((k*k).sum(-1, keepdims=True)), 1e-6)
|
||||
v, beta = rng.normal(size=(B, H, T, V)).astype(np.float32), rng.uniform(size=(B, H, T)).astype(np.float32)
|
||||
alpha = rng.uniform(0.9, 1, size=(B, H, T, V) if alpha_4d else (B, H, T)).astype(np.float32)
|
||||
initial = rng.normal(size=(B, H, V, K)).astype(np.float32)
|
||||
return q, k, v, beta, alpha, initial
|
||||
|
||||
def test_rectangular_state_and_row_decay(self):
|
||||
q, k, v, beta, alpha, initial = self._make(1, 1, 3, 4, 32, alpha_4d=True)
|
||||
expected_out, expected_state = numpy_ref(q, k, v, beta, alpha, initial)
|
||||
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 test_prefill_matches_single_steps(self):
|
||||
# one T=32 kernel call must match 32 sequential T=1 calls with in-place state
|
||||
q, k, v, beta, alpha, initial = self._make(1, 4, 32, 128, 128)
|
||||
state_a = Tensor(initial).contiguous().realize()
|
||||
out_a = gated_delta_prefill(Tensor(q), Tensor(k), Tensor(v), Tensor(beta), Tensor(alpha), state_a).realize()
|
||||
outs, state_b = [], Tensor(initial).contiguous().realize()
|
||||
for t in range(32):
|
||||
outs.append(gated_delta_prefill(Tensor(q[:, :, t:t+1]), Tensor(k[:, :, t:t+1]), Tensor(v[:, :, t:t+1]),
|
||||
Tensor(beta[:, :, t:t+1]), Tensor(alpha[:, :, t:t+1]), state_b).realize())
|
||||
np.testing.assert_allclose(out_a.numpy(), Tensor.stack(*outs, dim=2).squeeze(3).numpy(), rtol=1e-4, atol=1e-4)
|
||||
np.testing.assert_allclose(state_a.numpy(), state_b.numpy(), rtol=1e-4, atol=1e-4)
|
||||
|
||||
def test_start_pos_zero_resets_state(self):
|
||||
q, k, v, beta, alpha, initial = self._make(1, 2, 5, 8, 16)
|
||||
# garbage state must be ignored when start_pos binds to 0
|
||||
garbage = np.full_like(initial, 1.0e9)
|
||||
def run(sp, init):
|
||||
state = Tensor(init).contiguous().realize()
|
||||
initial = Tensor(UOp.variable("start_pos", 0, 63).bind(sp)).eq(0)
|
||||
return gated_delta_prefill(Tensor(q), Tensor(k), Tensor(v), Tensor(beta), Tensor(alpha), state, initial).realize(), state
|
||||
out_reset, state_reset = run(0, garbage)
|
||||
expected_out, expected_state = numpy_ref(q, k, v, beta, alpha, np.zeros_like(initial))
|
||||
np.testing.assert_allclose(out_reset.numpy(), expected_out, rtol=1e-4, atol=1e-4)
|
||||
np.testing.assert_allclose(state_reset.numpy(), expected_state, rtol=1e-4, atol=1e-4)
|
||||
# nonzero start_pos must use the provided state
|
||||
out_cont, state_cont = run(3, initial)
|
||||
expected_out, expected_state = numpy_ref(q, k, v, beta, alpha, initial)
|
||||
np.testing.assert_allclose(out_cont.numpy(), expected_out, rtol=1e-4, atol=1e-4)
|
||||
np.testing.assert_allclose(state_cont.numpy(), expected_state, rtol=1e-4, atol=1e-4)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -2,7 +2,7 @@ import unittest
|
||||
import numpy as np
|
||||
from dataclasses import replace
|
||||
from tinygrad import Tensor
|
||||
from tinygrad.llm.model import TransformerBlock, TransformerConfig
|
||||
from tinygrad.llm.model import ExpertGating, TransformerBlock, TransformerConfig
|
||||
|
||||
def _moe_config(dim=8, hidden=16, n_heads=2, num_experts=4, num_experts_per_tok=2):
|
||||
return TransformerConfig(
|
||||
@@ -96,5 +96,32 @@ class TestMoEFeedForward(unittest.TestCase):
|
||||
expected = moe_expected + shared_expected
|
||||
np.testing.assert_allclose(out.numpy(), expected, rtol=1e-2)
|
||||
|
||||
def test_moe_feed_forward_gating_funcs(self):
|
||||
dim, hidden, n_heads = 8, 16, 2
|
||||
num_experts, k = 4, 2
|
||||
logits = np.array([4.0, 3.0, 0.0, -1.0], dtype=np.float32)
|
||||
def softmax(x):
|
||||
probs = np.exp(x - x.max())
|
||||
return probs / probs.sum()
|
||||
for gating_func in ExpertGating:
|
||||
for norm_topk_prob in (False, True):
|
||||
block = TransformerBlock(replace(_moe_config(dim, hidden, n_heads, num_experts, k),
|
||||
expert_gating_func=gating_func, norm_topk_prob=norm_topk_prob))
|
||||
block.ffn_gate_exps.weight = Tensor.stack(*[Tensor.eye(hidden, dim) for _ in range(num_experts)])
|
||||
block.ffn_up_exps.weight = Tensor.stack(*[Tensor.eye(hidden, dim) * (i + 1) for i in range(num_experts)])
|
||||
block.ffn_down_exps.weight = Tensor.stack(*[Tensor.eye(dim, hidden) for _ in range(num_experts)])
|
||||
block.ffn_gate_inp.weight = Tensor((logits / dim)[None, :].repeat(dim, 0).T)
|
||||
out = block._feed_forward(Tensor.ones(1, 1, dim)).numpy()[0, 0, 0]
|
||||
|
||||
if gating_func == ExpertGating.SOFTMAX: selection_scores = softmax(logits)
|
||||
elif gating_func == ExpertGating.SIGMOID: selection_scores = 1 / (1 + np.exp(-logits))
|
||||
elif gating_func == ExpertGating.SOFTMAX_WEIGHT: selection_scores = logits
|
||||
else: selection_scores = np.sqrt(np.logaddexp(0, logits))
|
||||
sel = np.argsort(selection_scores)[-k:]
|
||||
weights = softmax(logits[sel]) if gating_func == ExpertGating.SOFTMAX_WEIGHT else selection_scores[sel]
|
||||
if norm_topk_prob: weights /= weights.sum()
|
||||
expected = (weights * (sel + 1)).sum() / (1 + np.exp(-1))
|
||||
np.testing.assert_allclose(out, expected, rtol=1e-3)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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
|
||||
@@ -42,8 +44,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]))
|
||||
# recurrent blocks prefill chunks like attention blocks: the 2 new tokens go through one chunked call
|
||||
self.assertEqual(calls, [((1, V_TOKS.bind(2)), V_START_POS.bind(5))])
|
||||
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), []
|
||||
@@ -153,6 +154,22 @@ 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)
|
||||
|
||||
|
||||
@@ -390,6 +390,12 @@ class TestMultiTensor(unittest.TestCase):
|
||||
self.assertEqual(out.shape, (rows, 8))
|
||||
np.testing.assert_equal(out[:3].to(Device.DEFAULT).numpy(), np.ones((3, 8)))
|
||||
|
||||
def test_symbolic_broadcast_consumed(self):
|
||||
rows = Variable("rows", 1, 4).bind(3)
|
||||
out = (Tensor.ones(rows).to(devices_2) + 1).realize()
|
||||
self.assertEqual(out.shape, (rows,))
|
||||
np.testing.assert_equal(out[:3].to(Device.DEFAULT).numpy(), np.full(3, 2))
|
||||
|
||||
def test_multitensor_jit_in_list(self):
|
||||
# test MULTI tensor inside a list container - exercises the container unpacking + MULTI unpacking
|
||||
@TinyJit
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from dataclasses import replace, dataclass
|
||||
import itertools, functools
|
||||
from tinygrad.helpers import DISABLE_FAST_IDIV, TRANSCENDENTAL, SPEC, DEBUG, VIZ, IMAGE, NOOPT, EMULATED_DTYPES, NOLOCALS, USE_TC
|
||||
from tinygrad.helpers import ALLOW_TF32, DEFAULT_FLOAT, DEFAULT_INT, TracingKey, Context, panic
|
||||
from tinygrad.helpers import ALLOW_TF32, DEFAULT_FLOAT, DEFAULT_INT, NUM_CPU_THREADS, TC_SELECT, TC_OPT, 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_index_dtype, pm_commit_weak, pm_cast_weak
|
||||
from tinygrad.uop.render import pyrender
|
||||
@@ -153,8 +153,8 @@ devectorizer2 = mop_cleanup+pm_mops+PatternMatcher([
|
||||
# unpack WMMA
|
||||
(UPat(Ops.WMMA, name="u"), do_stack_wmma),
|
||||
# stacked INDEX is many INDEX
|
||||
(UPat(Ops.INDEX, src=(UPat((Ops.PARAM, Ops.BUFFER), name="b"), UPat(Ops.STACK, name="s"))),
|
||||
lambda b,s: UOp.stack(*[b.index(u) for u in s.src])),
|
||||
(UPat(Ops.INDEX, src=(UPat((Ops.PARAM, Ops.BUFFER), name="b"), UPat(Ops.STACK, name="s")), name="x"),
|
||||
lambda b,s,x: UOp.stack(*[x.replace(src=(b,u)) for u in s.src])),
|
||||
# INDEX into RESHAPE moves the RESHAPE
|
||||
(UPat(Ops.INDEX, src=(UPat((Ops.PARAM, Ops.BUFFER), name="b"), UPat(Ops.RESHAPE, name="s"))),
|
||||
lambda b,s: b.index(s.src[0]).reshape(s.shape)),
|
||||
@@ -281,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))
|
||||
@@ -373,6 +377,10 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
|
||||
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")
|
||||
|
||||
# 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)
|
||||
|
||||
# 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")
|
||||
|
||||
@@ -451,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,**kwargs: TracingKey(ret.src[0].arg.name,(ret.src[0].arg.function_name, ast), ret=renderer), replay=True)
|
||||
@rewrite_group(name=lambda ast,renderer,ret,**_: TracingKey((k:=ret.src[0].arg).name,(k.function_name, ast, ret.key),ret=renderer), replay=True)
|
||||
@Context(ALLOW_DEVICE_USAGE=0)
|
||||
def do_to_program(ast:UOp, renderer:Renderer) -> UOp:
|
||||
"""
|
||||
@@ -480,9 +488,14 @@ 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:
|
||||
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)
|
||||
if (prg:=to_program_cache.get(key:=to_program_key(ast, renderer))) is None: to_program_cache[key] = prg = do_to_program(ast, renderer)
|
||||
return prg
|
||||
|
||||
@@ -33,7 +33,8 @@ def l2i(op: Ops, dt: DType, *uops:UOp):
|
||||
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:
|
||||
small = (a1.eq(0) & (a0 >= 0)) | (a1.eq(-1) & (a0 < 0))
|
||||
return small.where(a0.cast(dt), ((a1.cast(dtypes.float32) * (2**32)) + a0.bitcast(dtypes.uint).cast(dtypes.float32)).cast(dt))
|
||||
cdt = dt if dt == dtypes.float64 else dtypes.float32
|
||||
return small.where(a0.cast(dt), ((a1.cast(cdt) * (2**32)) + a0.bitcast(dtypes.uint).cast(cdt)).cast(dt))
|
||||
case Ops.CAST: return a0.bitcast(dtypes.uint).cast(dt)
|
||||
case Ops.BITCAST: return a0.bitcast(dt), a1.bitcast(dt)
|
||||
case Ops.SHL:
|
||||
|
||||
@@ -128,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: 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))]
|
||||
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))]
|
||||
return PatternMatcher(pat)
|
||||
|
||||
@@ -43,14 +43,6 @@ def add_gpudims(ctx:Renderer, s:UOp):
|
||||
s_topo = list(s.toposort())
|
||||
if any(x.op is Ops.SPECIAL for x in s_topo): return None
|
||||
|
||||
# renderers without local workgroups execute LOCAL/WARP ranges as sequential loops in the thread.
|
||||
# this is only valid without cross-thread communication (BARRIER), local memory stays unsupported
|
||||
if not ctx.has_local and any(r.op is Ops.RANGE and r.arg[-1] in (AxisType.LOCAL, AxisType.WARP) for r in s_topo):
|
||||
if any(x.op is Ops.BARRIER or (x.op is Ops.BUFFER and x.addrspace is AddrSpace.LOCAL) for x in s_topo): return None
|
||||
s = s.substitute({r: r.replace(arg=r.arg[0:-1]+(AxisType.LOOP,)) for r in s_topo if r.op is Ops.RANGE
|
||||
and r.arg[-1] in (AxisType.LOCAL, AxisType.WARP)})
|
||||
s_topo = list(s.toposort())
|
||||
|
||||
# get ranges
|
||||
all_ranges = {x.arg[0:-1]:x for x in s_topo if x.op is Ops.RANGE}
|
||||
|
||||
|
||||
@@ -51,8 +51,8 @@ def simplify_valid_image_load(buf:UOp, idx_y:UOp, idx_x:UOp, valid:UOp) -> UOp|N
|
||||
if not drop_stmt and idx is start_idx: return None
|
||||
new_valid = UOp.uprod(*ss) if (ss:=[s for s in valid.split_uop(Ops.AND) if s not in drop_stmt]) else None
|
||||
idx_y, idx_x = idx.index(1), idx.index(0)
|
||||
if new_valid is not None: return buf.index(idx_y.valid(new_valid), idx_x.valid(new_valid), dtype=dtypes.float)
|
||||
return buf.index(idx_y, idx_x, dtype=dtypes.float)
|
||||
if new_valid is not None: return buf.index(idx_y.valid(new_valid), idx_x.valid(new_valid))
|
||||
return buf.index(idx_y, idx_x)
|
||||
|
||||
indexing_simplify = PatternMatcher([
|
||||
# image load valid idx simplification
|
||||
@@ -88,9 +88,9 @@ def transform_to_image(ctx, buf:UOp, x:UOp) -> UOp|None:
|
||||
buf = buf.replace(src=(shape_to_shape_arg((h, w, 4)),))
|
||||
shapes[buf.arg.slot] = (h, w)
|
||||
if valid.op is not Ops.CONST or valid.val is not True:
|
||||
return buf.index(cidx.src[1].valid(valid), cidx.src[0].valid(valid), dtype=dtypes.float)
|
||||
return buf.index(cidx.src[1].valid(valid), cidx.src[0].valid(valid))
|
||||
else:
|
||||
return buf.index(cidx.src[1], cidx.src[0], dtype=dtypes.float)
|
||||
return buf.index(cidx.src[1], cidx.src[0])
|
||||
|
||||
pm_simplify_add_image = PatternMatcher([
|
||||
(UPat(Ops.SHRINK, src=(UPat(Ops.PARAM, name="buf"), UPat(name="x"), UPat(arg=4))), transform_to_image),
|
||||
|
||||
@@ -10,10 +10,10 @@ pm_move_gates_from_index = PatternMatcher([
|
||||
# for image idx (must be first)
|
||||
(UPat.var("buf").index(UPat.var("gate").where(UPat.var("idx_y"), UPat(arg=Invalid)),
|
||||
UPat.var("gate").where(UPat.var("idx_x"), UPat(arg=Invalid))).load(name="l"),
|
||||
lambda buf,gate,idx_y,idx_x,l: buf.index(idx_y, idx_x, dtype=dtypes.float).load(l.vconst_like(0), gate)),
|
||||
lambda buf,gate,idx_y,idx_x,l: buf.index(idx_y, idx_x).load(l.vconst_like(0), gate)),
|
||||
(UPat.var("buf").index(UPat.var("gate").where(UPat.var("idx_y"), UPat(arg=Invalid)),
|
||||
UPat.var("gate").where(UPat.var("idx_x"), UPat(arg=Invalid))).store(UPat.var("data")),
|
||||
lambda buf,gate,idx_y,idx_x,data: buf.index(idx_y, idx_x, dtype=dtypes.float).store(data, gate)),
|
||||
lambda buf,gate,idx_y,idx_x,data: buf.index(idx_y, idx_x).store(data, gate)),
|
||||
|
||||
# here we create the alt value for load to be 0s and remove the where Invalid
|
||||
(UPat((Ops.INDEX, Ops.SHRINK), src=(UPat(), UPat.var("gate").where(UPat.var("idx"), UPat(arg=Invalid)),), name="mop", allow_any_len=True) \
|
||||
|
||||
@@ -4,7 +4,7 @@ from tinygrad.uop.ops import UOp, Ops, PatternMatcher, UPat
|
||||
from tinygrad.renderer.isa import ISARenderer, Register, greg
|
||||
from tinygrad.dtype import dtypes
|
||||
|
||||
PSEUDO_OPS = {Ops.CONST, Ops.NOOP, Ops.AFTER, Ops.BARRIER, Ops.GROUP, Ops.STACK}
|
||||
PSEUDO_OPS = {Ops.CONST, Ops.CAST, Ops.NOOP, Ops.AFTER, Ops.BARRIER, Ops.GROUP, Ops.STACK}
|
||||
|
||||
class LinearScanRegallocContext:
|
||||
# returns the uop that defines the virtual register
|
||||
@@ -52,7 +52,7 @@ class LinearScanRegallocContext:
|
||||
# the value of a BUFFER is its 64bit address, XMM registers need 16 bytes
|
||||
sz = 16 if v.cons[0].size == 16 else (8 if self.vdef(v).op is Ops.BUFFER else self.vdef(v).dtype.itemsize)
|
||||
offset = self.stack_size + (sz - self.stack_size % sz) % sz
|
||||
self.spills[v] = UOp.const(offset, dtypes.int32)
|
||||
self.spills[v] = UOp.cconst(offset, dtypes.int32)
|
||||
self.stack_size = offset + sz
|
||||
r = alloc(cons if cons is not None else v.cons, i)
|
||||
self.insert_before.setdefault(i, []).append((v, r))
|
||||
@@ -84,7 +84,7 @@ class LinearScanRegallocContext:
|
||||
|
||||
# allocate stack array
|
||||
if u.op is Ops.BUFFER:
|
||||
self.locals[u] = UOp.const(self.stack_size, dtypes.int32)
|
||||
self.locals[u] = UOp.cconst(self.stack_size, dtypes.int32)
|
||||
self.stack_size += u.max_numel() * u.dtype.itemsize
|
||||
|
||||
# loop prologue, avoid loading inside the loop
|
||||
@@ -125,7 +125,7 @@ def regalloc_rewrite(ctx:LinearScanRegallocContext, x:UOp):
|
||||
# alloc/dealloc stack
|
||||
if ctx.stack_size > 0:
|
||||
sp = ctx.ren.stack_pointer()
|
||||
offset = UOp.const(ctx.stack_size, sp.dtype)
|
||||
offset = UOp.cconst(ctx.stack_size, sp.dtype)
|
||||
if i == 0: before = [ctx.ren.isel_matcher.rewrite(UOp(Ops.SUB, src=(sp, offset), tag=sp.tag))] + before
|
||||
elif i == len(ctx.uops) - 2: before += [ctx.ren.isel_matcher.rewrite(UOp(Ops.ADD, src=(sp, offset), tag=sp.tag))]
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from __future__ import annotations
|
||||
import math, itertools
|
||||
from collections import defaultdict
|
||||
from typing import cast, Final
|
||||
from typing import cast
|
||||
from tinygrad.uop.ops import Ops, UOp, KernelInfo, graph_rewrite, AxisType, ssimplify, remove_all_tags
|
||||
from tinygrad.uop.ops import axis_letters, axis_colors, axis_to_pos
|
||||
from tinygrad.device import Buffer
|
||||
from tinygrad.dtype import dtypes, Invalid
|
||||
from tinygrad.helpers import colored, getenv, DEBUG, to_function_name, NOOPT, argsort, round_up, prod, merge_dicts, get_single_element, flatten
|
||||
from tinygrad.helpers import colored, getenv, DEBUG, NOOPT, argsort, round_up, prod, merge_dicts, get_single_element, flatten
|
||||
from tinygrad.helpers import ALLOW_TF32, count, Context
|
||||
from tinygrad.codegen.opt import Opt, OptOps, KernelOptError, check
|
||||
from tinygrad.codegen.simplify import pm_flatten_range
|
||||
@@ -48,7 +47,6 @@ class Scheduler:
|
||||
if hasattr(self, 'tensor_core'): ret.tensor_core = self.tensor_core
|
||||
return ret
|
||||
|
||||
kernel_cnt: Final[defaultdict[str, int]] = defaultdict(int)
|
||||
def get_optimized_ast(self, name_override:str|None=None) -> UOp:
|
||||
if name_override is not None: name = name_override
|
||||
else:
|
||||
@@ -56,9 +54,6 @@ class Scheduler:
|
||||
special_uops = sorted([x for x in self.ast.toposort() if x.op is Ops.SPECIAL], key=lambda x: x.arg)
|
||||
special_ops = [colored(str(x.vmax+1), "blue" if x.arg[0] == "g" else "cyan") for x in special_uops]
|
||||
name = k_type + colored('_', 'BLACK').join(['']+special_ops+[colored(x.src[0].render(), color) for x,color in zip(self.rngs, self.colors())])
|
||||
Scheduler.kernel_cnt[(function_name := to_function_name(name))] += 1
|
||||
num = f"n{Scheduler.kernel_cnt[function_name]-1}" if Scheduler.kernel_cnt[function_name] > 1 else ""
|
||||
name += colored(num, 'BLACK')
|
||||
self.ast = graph_rewrite(self.ast, pm_flatten_range, name="flatten range")
|
||||
return self.ast.replace(arg=KernelInfo(name=name, applied_opts=tuple(self.applied_opts), dont_use_locals=self.dont_use_locals), tag=1)
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import math, time, multiprocessing, traceback, signal, atexit
|
||||
import math, time, traceback, signal
|
||||
from dataclasses import replace
|
||||
from tinygrad.uop.ops import sym_infer, AxisType, UOp, Ops
|
||||
from tinygrad.uop.render import pyrender
|
||||
from tinygrad.device import Device, Buffer
|
||||
from tinygrad.helpers import prod, flatten, DEBUG, CACHELEVEL, diskcache_get, diskcache_put, getenv, Context, colored, time_to_str
|
||||
from tinygrad.helpers import prod, flatten, DEBUG, CACHELEVEL, diskcache_get, diskcache_put, getenv, colored, time_to_str
|
||||
from tinygrad.helpers import IGNORE_BEAM_CACHE
|
||||
from tinygrad.codegen.opt import Opt, OptOps, KernelOptError
|
||||
from tinygrad.engine.realize import time_call
|
||||
from tinygrad.engine.worker import get_worker_pool, terminate_worker_pool
|
||||
from tinygrad.codegen import to_program
|
||||
from tinygrad.codegen.opt.postrange import Scheduler
|
||||
|
||||
@@ -42,9 +43,9 @@ def _time_program(prg:UOp, var_vals:dict[str, int], rawbufs:list[Buffer], early_
|
||||
global_size, factor = get_test_global_size(prg.arg.global_size, max_global_size, var_vals)
|
||||
prg = prg.replace(arg=replace(prg.arg, global_size=tuple(global_size)))
|
||||
call = prg.call(*[UOp.from_buffer(b) for b in rawbufs])
|
||||
tms = []
|
||||
tms, timer = [], time_call(call, var_vals, timeout=timeout, clear_l2=clear_l2)
|
||||
for _ in range(cnt):
|
||||
try: tms.append(time_call(call, var_vals, timeout=timeout, clear_l2=clear_l2) * factor)
|
||||
try: tms.append(next(timer) * factor)
|
||||
except AssertionError: return [math.inf] * cnt
|
||||
if early_stop is not None and early_stop < min(tms): break
|
||||
return tms
|
||||
@@ -78,11 +79,6 @@ def _try_compile(x:tuple[int,Scheduler]) -> tuple[int, tuple[UOp, float]|None]:
|
||||
if hasattr(signal, "alarm"): signal.alarm(0)
|
||||
return x[0], ret
|
||||
|
||||
# workers should not open devices and should ignore ctrl c and should not launch VIZ
|
||||
def _init_worker():
|
||||
Context(ALLOW_DEVICE_USAGE=0, VIZ=0, TRACK_MATCH_STATS=0).__enter__()
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
def _ensure_buffer_alloc(bufs:list[Buffer]) -> list[Buffer]: return [buf.ensure_allocated() if buf is not None else buf for buf in bufs]
|
||||
|
||||
# *** external API ***
|
||||
@@ -111,9 +107,8 @@ def get_kernel_actions(s:Scheduler, include_0=True, max_up:int|None=None) -> dic
|
||||
except KernelOptError: pass
|
||||
return acted
|
||||
|
||||
beam_pool, BEAM_DEBUG = None, getenv("BEAM_DEBUG")
|
||||
BEAM_DEBUG = getenv("BEAM_DEBUG")
|
||||
def beam_search(s:Scheduler, rawbufs:list[Buffer], var_vals:dict[str,int], amt:int, allow_test_size=True, disable_cache=IGNORE_BEAM_CACHE.value):
|
||||
global beam_pool
|
||||
key = {"ast": s.ast.key, "amt": amt, "allow_test_size": allow_test_size, "device": s.ren.target.device, "suffix": s.ren.suffix}
|
||||
if not disable_cache and CACHELEVEL >= 1 and (val:=diskcache_get("beam_search", key)) is not None:
|
||||
ret = s.copy()
|
||||
@@ -123,11 +118,7 @@ def beam_search(s:Scheduler, rawbufs:list[Buffer], var_vals:dict[str,int], amt:i
|
||||
beam: list[tuple[Scheduler, float]] = [(s, float("inf"))]
|
||||
seen_libs = set()
|
||||
|
||||
default_parallel = multiprocessing.cpu_count() if s.ren.target.device in {"CUDA", "AMD", "NV", "METAL", "HIP"} else 0
|
||||
if beam_pool is None and (workers := getenv("PARALLEL", default_parallel)):
|
||||
beam_pool = multiprocessing.get_context("spawn").Pool(workers, _init_worker, (), getenv("BEAM_MAX_TASKS_PER_CHILD", 16))
|
||||
@atexit.register
|
||||
def close_pool(): beam_pool.close()
|
||||
pool = get_worker_pool()
|
||||
|
||||
min_progress = getenv("BEAM_MIN_PROGRESS", 0.01)/1e6
|
||||
if BEAM_DEBUG:
|
||||
@@ -143,7 +134,7 @@ def beam_search(s:Scheduler, rawbufs:list[Buffer], var_vals:dict[str,int], amt:i
|
||||
candidates: list[Scheduler] = flatten([get_kernel_actions(si, include_0=False).values() for si,_ in beam])
|
||||
timed: list[tuple[Scheduler, float]] = []
|
||||
least_compute_ops = math.inf
|
||||
for i, proc in ((map if beam_pool is None else beam_pool.imap_unordered)(_try_compile, enumerate(candidates))):
|
||||
for i, proc in ((map if pool is None else pool.imap_unordered)(_try_compile, enumerate(candidates))):
|
||||
if proc is None: continue
|
||||
prg, compile_et = proc
|
||||
if (lib:=prg.src[3].arg) in seen_libs: continue
|
||||
@@ -179,7 +170,7 @@ def beam_search(s:Scheduler, rawbufs:list[Buffer], var_vals:dict[str,int], amt:i
|
||||
print(f"\r{time.perf_counter() - st:7.2f}s:", colored(time_to_str(beam[0][1], w=12), "green" if exiting else None),
|
||||
f"from {len(candidates):3d} -> {len(opts):3d} actions\033[K", beam[0][0].colored_shape())
|
||||
except KeyboardInterrupt as e:
|
||||
if beam_pool is not None: beam_pool.terminate()
|
||||
terminate_worker_pool()
|
||||
raise e
|
||||
|
||||
if CACHELEVEL >= 1: diskcache_put("beam_search", key, beam[0][0].applied_opts)
|
||||
|
||||
+13
-4
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass, replace
|
||||
from collections import defaultdict
|
||||
from typing import Any, Callable, Generic, TypeVar, Iterator, Generator, Self, TYPE_CHECKING
|
||||
import importlib, inspect, functools, pathlib, os, contextlib, re, atexit, pickle, decimal
|
||||
import importlib, inspect, functools, pathlib, os, contextlib, re, atexit, pickle, decimal, subprocess, struct
|
||||
from tinygrad.helpers import LRU, getenv, diskcache_get, diskcache_put, DEBUG, GlobalCounters, PROFILE, temp, colored
|
||||
from tinygrad.helpers import Context, CCACHE, ALLOW_DEVICE_USAGE, MAX_BUFFER_SIZE, cpu_events, ProfileEvent, ProfilePointEvent, suppress_finalizing
|
||||
from tinygrad.helpers import select_by_name, select_first_inited, DEV, TracingKey, size_to_str, pluralize, Target, unwrap, round_up
|
||||
@@ -66,10 +66,10 @@ def canonicalize_device(device:str|tuple|list|None) -> str|tuple[str, ...]:
|
||||
class ProfileDeviceEvent(ProfileEvent): device:str; tdiff:decimal.Decimal=decimal.Decimal(0); props:dict[str,Any]|None=None # noqa: E702
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProfileProgramEvent(ProfileEvent): device:str; name:str; lib:bytes|None; base:int|None; tag:int|None=None # noqa: E702
|
||||
class ProfileProgramEvent(ProfileEvent): device:str; name:str; lib:bytes|None; base:int|None; tag:int|None=None; profile_key:bytes|None=None # noqa: E702
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProfileGraphEntry: device:str; name:str|TracingKey; st_id:int; en_id:int # noqa: E702
|
||||
class ProfileGraphEntry: device:str; name:str|TracingKey; st_id:int; en_id:int; profile_key:bytes|None=None # noqa: E702
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProfileGraphEvent(ProfileEvent): ents:list[ProfileGraphEntry]; deps:list[list[int]]; sigs:list[decimal.Decimal] # noqa: E702
|
||||
@@ -133,7 +133,7 @@ class Buffer:
|
||||
# check if the underlying buffer is allocated, possibly from the base object
|
||||
def is_allocated(self) -> bool: return self.base.is_allocated() if self._base is not None else self.device in self._bufs
|
||||
def get_buf(self, device: str) -> Any:
|
||||
if (device:=Device.canonicalize(device)) not in self._bufs:
|
||||
if device not in self._bufs and (device:=Device.canonicalize(device)) not in self._bufs:
|
||||
allocator = Device[device].allocator
|
||||
if device == self.device: self.ensure_allocated()
|
||||
elif self._base is not None: self._bufs[device] = allocator._offset(self._base.get_buf(device), self.nbytes, self.offset)
|
||||
@@ -310,6 +310,14 @@ class Compiler:
|
||||
if self.cachekey is not None: diskcache_put(self.cachekey, src, lib)
|
||||
return lib
|
||||
def disassemble(self, lib:bytes): pass
|
||||
def server(self, cmd:str, arch:str, *args) -> subprocess.Popen:
|
||||
argv = f"{cmd} {pathlib.Path(__file__).parent}/runtime/support/compileserver.py {type(self).__module__}:{type(self).__name__} {arch}"
|
||||
return subprocess.Popen(argv.split() + [str(a) for a in args], stdout=subprocess.PIPE, stdin=subprocess.PIPE, bufsize=0)
|
||||
def compile_server(self, src:str, proc:subprocess.Popen) -> bytes:
|
||||
unwrap(proc.stdin).write(struct.pack("I", len(src.encode())) + src.encode())
|
||||
if (lib:=unwrap(proc.stdout).read(struct.unpack("I", unwrap(proc.stdout).read(4))[0])): return lib
|
||||
raise CompileError("Compilation Error")
|
||||
|
||||
|
||||
@dataclass
|
||||
class TinyELF:
|
||||
@@ -318,6 +326,7 @@ class TinyELF:
|
||||
target: Target
|
||||
# tuple of (name, slot, dtype, shape)
|
||||
signature: tuple[tuple[str|None, int, DType, tuple], ...]
|
||||
profile_key: bytes|None = None
|
||||
|
||||
@staticmethod
|
||||
def iter_sig(signature:tuple[tuple[str|None, int, DType, tuple], ...], offset:int=0) -> Generator[tuple[int, DType], None, None]:
|
||||
|
||||
+1
-2
@@ -66,7 +66,6 @@ class DType(metaclass=DTypeMetaClass):
|
||||
def __reduce__(self): return type(self), tuple(getattr(self, f.name) for f in fields(self))
|
||||
def __repr__(self): return f"dtypes.{INVERSE_DTYPES_DICT[self.name]}"
|
||||
def __lt__(self, o:DType): return (self.priority, self.bitsize, self.name, self.fmt) < (o.priority, o.bitsize, o.name, o.fmt)
|
||||
def scalar(self) -> DType: return self
|
||||
@functools.cached_property
|
||||
def min(self):
|
||||
if dtypes.is_int(self): return 0 if dtypes.is_unsigned(self) else -2**(self.bitsize-1)
|
||||
@@ -222,7 +221,7 @@ def float_to_fp16(x):
|
||||
|
||||
def float_to_bf16(x):
|
||||
if not math.isfinite(x): return x
|
||||
u = struct.unpack('I', struct.pack('f', x))[0]
|
||||
u = struct.unpack('I', struct.pack('f', truncate[dtypes.float](x)))[0]
|
||||
u = (u + 0x7FFF + ((u >> 16) & 1)) & 0xFFFF0000
|
||||
return struct.unpack('f', struct.pack('I', u))[0]
|
||||
|
||||
|
||||
@@ -269,10 +269,14 @@ class _TinyJit(Generic[ReturnType]):
|
||||
big_linear, onetime_linear = prune_linear(big_linear, set(input_buf_uops))
|
||||
if DEBUG >= 1: print(f"pruned from {len(big_linear.src) + len(onetime_linear.src)} -> {len(big_linear.src)} kernels")
|
||||
run_linear(onetime_linear, var_vals)
|
||||
del onetime_linear
|
||||
|
||||
# hold all buffers reachable from live Tensors (e.g. lazy .grad created during capture), the memory planner can't suballocate those
|
||||
held_bufs = set(buffers) | {u for tref in list(all_tensors) if (t:=tref()) is not None for u in t.uop.toposort() if u.op is Ops.BUFFER}
|
||||
linear = jit_lower(big_linear, held_bufs, input_buf_uops)
|
||||
# drop the pre-planning graph: it keeps the whole capture-time working set allocated (big_linear) or referenced (held_bufs).
|
||||
# the planned linear only uses the arena/held buffers, so the intermediates must be freed before linking and first exec
|
||||
del big_linear, held_bufs
|
||||
self.captured = CapturedJit(ret, linear, names, expected_input_info)
|
||||
ret = self.captured(input_buf_uops, var_vals)
|
||||
elif self.cnt >= 2:
|
||||
|
||||
+143
-103
@@ -1,18 +1,23 @@
|
||||
from __future__ import annotations
|
||||
from typing import cast, Iterator, Any, Sequence
|
||||
import time, random, itertools, math, contextlib, weakref, array
|
||||
import random, itertools, math, weakref, array, decimal
|
||||
from dataclasses import dataclass, replace, field
|
||||
from tinygrad.helpers import colored, DEBUG, GlobalCounters, ansilen, all_int, prod, flatten, Context, getenv, to_tuple
|
||||
from tinygrad.helpers import BEAM, size_to_str, time_to_str, VALIDATE_WITH_CPU, PROFILE, ProfilePointEvent, cpu_events, wait_cond
|
||||
from tinygrad.uop.ops import Ops, PatternMatcher, UOp, UPat, AxisType, sym_infer, graph_rewrite
|
||||
from tinygrad.helpers import colored, DEBUG, GlobalCounters, ansipad, all_int, prod, flatten, Context, getenv, to_tuple, tqdm
|
||||
from tinygrad.helpers import BEAM, size_to_str, time_to_str, VALIDATE_WITH_CPU, PROFILE, ProfilePointEvent, cpu_events, perf_counter_us
|
||||
from tinygrad.uop.ops import Ops, PatternMatcher, UOp, UPat, AxisType, sym_infer, graph_rewrite, ProgramInfo
|
||||
from tinygrad.device import Device, Buffer, MultiBuffer, ProfileGraphEntry
|
||||
from tinygrad.renderer import Estimates
|
||||
from tinygrad.codegen import to_program
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.renderer import Estimates, Renderer
|
||||
from tinygrad.codegen import to_program, to_program_cache, to_program_key, to_program_context
|
||||
from tinygrad.codegen.opt.postrange import args_from_ast
|
||||
from tinygrad.engine.worker import get_worker_pool, terminate_worker_pool
|
||||
|
||||
# **************** Helpers ****************
|
||||
|
||||
def get_call_arg_uops(call:UOp) -> tuple[UOp, ...]: return tuple(s for s in call.src[1:] if not s.is_bound_var)
|
||||
def get_call_var_uops(call:UOp, prg:UOp) -> list[UOp]:
|
||||
bound = {s.src[0].expr: s.src[1].src[1] for s in call.src[1:] if s.is_bound_var}
|
||||
return [bound.get(v.expr, v) for v in prg.arg.vars]
|
||||
|
||||
def get_call_outs_ins(call:UOp) -> tuple[tuple[int, ...], tuple[int, ...]]:
|
||||
ast = call.src[0]
|
||||
@@ -21,6 +26,12 @@ def get_call_outs_ins(call:UOp) -> tuple[tuple[int, ...], tuple[int, ...]]:
|
||||
if ast.op is Ops.CUSTOM_FUNCTION and ast.arg == "encdec": return (0,), tuple(range(1, len(get_call_arg_uops(call))))
|
||||
return (), ()
|
||||
|
||||
def get_call_kernels(call:UOp) -> list[tuple[str, UOp]]:
|
||||
if (ast:=call.src[0]).op is Ops.CUSTOM_FUNCTION and ast.arg == "hcq": return [(d, k) for devs, k, _ in call.arg.aux.kernels for d in devs]
|
||||
if ast.op is Ops.CUSTOM_FUNCTION and ast.arg == "graph": return [(to_tuple(ast.device)[0], call)]
|
||||
if ast.op is Ops.CUSTOM_FUNCTION and ast.arg == "validate": return []
|
||||
return [(d, call) for d in to_tuple(call.src[1].device)]
|
||||
|
||||
def get_call_name(call:UOp, bufs:Sequence[Buffer|UOp], var_vals:dict[str, int]|None=None) -> str:
|
||||
def _uop_sz_to_str(uop:UOp) -> str: return size_to_str(sym_infer(prod(uop.shape) * uop.dtype.itemsize, var_vals or {}))
|
||||
def _dev_str(buf:Buffer|UOp) -> str: return ', '.join(d[:7] for d in to_tuple(buf.device))
|
||||
@@ -36,49 +47,52 @@ def get_call_name(call:UOp, bufs:Sequence[Buffer|UOp], var_vals:dict[str, int]|N
|
||||
# **************** Stat ****************
|
||||
|
||||
def estimate_uop(call:UOp) -> Estimates:
|
||||
ast = call.src[0]
|
||||
if ast.op is Ops.PROGRAM: return ast.src[0].arg.estimates or Estimates()
|
||||
if (ast:=call.src[0]).op is Ops.PROGRAM: return ast.src[0].arg.estimates or Estimates()
|
||||
if ast.op is Ops.COPY or (ast.op is Ops.CUSTOM_FUNCTION and ast.arg == "encdec"):
|
||||
nbytes = prod(call.src[1].shape) * call.src[1].dtype.itemsize
|
||||
return Estimates(lds=nbytes, mem=nbytes)
|
||||
return Estimates(lds=(nbytes:=prod(call.src[1].shape) * call.src[1].dtype.itemsize), mem=nbytes)
|
||||
if ast.op is Ops.CUSTOM_FUNCTION and ast.arg == "graph": return get_graph_runtime(ast).estimates
|
||||
if ast.op is Ops.CUSTOM_FUNCTION and ast.arg == "hcq": return call.arg.aux.estimates
|
||||
return Estimates()
|
||||
|
||||
first_run_cache:set[bytes] = set()
|
||||
@contextlib.contextmanager
|
||||
def track_stats(ctx:ExecContext, call:UOp, device:str, bufs:list[Buffer], var_vals:dict[str, int]):
|
||||
if PROFILE:
|
||||
outputs, inputs = get_call_outs_ins(call)
|
||||
cpu_events.append(ProfilePointEvent(device, "exec", len(cpu_events), {"var_vals": var_vals,
|
||||
"bufs": [b.trace_num for b in bufs], "name": get_call_name(call, bufs, var_vals), "outputs": outputs, "inputs": inputs}))
|
||||
et: list[float|None] = [None]
|
||||
if DEBUG >= 2: st = time.perf_counter()
|
||||
yield et
|
||||
if not ctx.update_stats: return
|
||||
def track_stats(ctx:ExecContext, call:UOp, st:decimal.Decimal, ets:list[float|None]):
|
||||
if ctx.update_stats:
|
||||
is_hcq = (ast:=call.src[0]).op is Ops.CUSTOM_FUNCTION and ast.arg == "hcq"
|
||||
estimates, n = estimate_uop(call), 1 if is_hcq else len(get_call_kernels(call))
|
||||
GlobalCounters.kernel_count += len(call.arg.aux.kernels) if is_hcq else n
|
||||
GlobalCounters.global_ops += n*sym_infer(estimates.ops, ctx.var_vals)
|
||||
GlobalCounters.global_mem += n*sym_infer(estimates.mem, ctx.var_vals)
|
||||
GlobalCounters.time_sum_s += sum(et for et in ets if et is not None)
|
||||
if DEBUG < 2 and not PROFILE: return
|
||||
|
||||
if DEBUG >= 2 and et[0] is None:
|
||||
Device[device].synchronize()
|
||||
et[0] = time.perf_counter() - st
|
||||
kernels = get_call_kernels(call) # everything below is the per kernel display: exec events for the profiler and DEBUG=2 lines
|
||||
args = resolve_params(call, ctx.input_uops) if kernels and kernels[0][1] is call else []
|
||||
lanes = list(unwrap_multi(call, [args[g] for g in call.src[0].arg.globals] if call.src[0].op is Ops.PROGRAM else args)) if args else []
|
||||
for i, (device, kcall) in enumerate(kernels):
|
||||
et, bufs = ets[i] if i < len(ets) else None, lanes[i][0] if i < len(lanes) else []
|
||||
if PROFILE: # backdate the event to the start of the call, the viz matches a device range with the exec event before it
|
||||
outputs, inputs = get_call_outs_ins(kcall)
|
||||
cpu_events.append(ProfilePointEvent(device, "exec", len(cpu_events), {"var_vals": ctx.var_vals,
|
||||
"bufs": [b.trace_num for b in bufs], "name": get_call_name(kcall, bufs, ctx.var_vals), "outputs": outputs, "inputs": inputs}, ts=st))
|
||||
if DEBUG < 2 or not ctx.update_stats: continue
|
||||
if et is None:
|
||||
Device[device].synchronize()
|
||||
et, st = float(perf_counter_us() - st)*1e-6, perf_counter_us()
|
||||
GlobalCounters.time_sum_s += et
|
||||
|
||||
estimates = estimate_uop(call)
|
||||
GlobalCounters.kernel_count += 1
|
||||
GlobalCounters.global_ops += (op_est:=sym_infer(estimates.ops, var_vals))
|
||||
GlobalCounters.global_mem += (mem_est:=sym_infer(estimates.mem, var_vals))
|
||||
if et[0] is not None: GlobalCounters.time_sum_s += et[0]
|
||||
if DEBUG >= 2:
|
||||
display_name = get_call_name(call, bufs, var_vals)
|
||||
lds_est = sym_infer(estimates.lds, var_vals)
|
||||
header_color = 'magenta' if ctx.jit else ('green' if call.src[0].key not in first_run_cache else None)
|
||||
ptm = colored(time_to_str(et[0], w=9), "yellow" if et[0] > 0.01 else None) if et[0] is not None else ""
|
||||
flops, membw, ldsbw = op_est/(et[0] or 1e-20), mem_est/(et[0] or 1e-20), lds_est/(et[0] or 1e-20)
|
||||
estimates = estimate_uop(kcall)
|
||||
display_name = get_call_name(kcall, bufs, ctx.var_vals)
|
||||
op_est, mem_est, lds_est = (sym_infer(x, ctx.var_vals) for x in (estimates.ops, estimates.mem, estimates.lds))
|
||||
header_color = 'magenta' if ctx.jit else ('green' if kcall.src[0].key not in first_run_cache else None)
|
||||
ptm = colored(time_to_str(et, w=9), "yellow" if et > 0.01 else None) if et is not None else ""
|
||||
flops, membw, ldsbw = op_est/(et or 1e-20), mem_est/(et or 1e-20), lds_est/(et or 1e-20)
|
||||
flops_str = f"{flops*1e-9:7.0f} GFLOPS" if flops < 1e14 else colored(f"{flops*1e-12:7.0f} TFLOPS", 'green')
|
||||
mem_str = f"{membw*1e-9:4.0f}|{ldsbw*1e-9:<6.0f} GB/s" if membw < 1e13 and ldsbw < 1e15 else \
|
||||
colored(f"{membw*1e-12:4.0f}|{ldsbw*1e-12:<6.0f} TB/s", 'green')
|
||||
print(f"{colored(f'*** {device[:7]:7s} {GlobalCounters.kernel_count:4d}', header_color)}"+
|
||||
f" {display_name+' '*(46-ansilen(display_name))} arg {len(bufs):2d} mem {GlobalCounters.mem_used/1e9:6.2f} GB"+
|
||||
("" if et[0] is None else f" tm {ptm}/{GlobalCounters.time_sum_s*1e3:9.2f}ms ({flops_str} {mem_str})"))
|
||||
first_run_cache.add(call.src[0].key)
|
||||
f" {ansipad(display_name, 46)} arg {len(bufs):2d} mem {GlobalCounters.mem_used/1e9:6.2f} GB"+
|
||||
("" if et is None else f" tm {ptm}/{GlobalCounters.time_sum_s*1e3:9.2f}ms ({flops_str} {mem_str})"))
|
||||
first_run_cache.add(kcall.src[0].key)
|
||||
|
||||
local_size_cache: dict[bytes, tuple[int, ...]] = {}
|
||||
def optimize_local_size(call:UOp, prg:UOp) -> UOp|None:
|
||||
@@ -151,32 +165,31 @@ def unwrap_multi(call:UOp, resolved:list[UOp]) -> Iterator[tuple[list[Buffer], d
|
||||
for x in call.src[0].toposort())
|
||||
for j, per_dev in enumerate(zip(*[cast(MultiBuffer, b).bufs for b in bufs])): yield list(per_dev), {"_device_num": j} if has_dnum else {}
|
||||
|
||||
def exec_copy(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
|
||||
def exec_copy(ctx:ExecContext, call:UOp, ast:UOp) -> list[float|None]:
|
||||
for bufs, device_vars in unwrap_multi(call, resolve_params(call, ctx.input_uops)):
|
||||
dest, src = bufs[0].ensure_allocated(), bufs[1].ensure_allocated()
|
||||
with track_stats(ctx, call, dest.device, [dest, src], ctx.var_vals):
|
||||
if hasattr(dest.allocator,'_transfer') and dest.allocator.supports_transfer and dest.device.split(":")[0] == src.device.split(":")[0]:
|
||||
dest.allocator._transfer(dest._buf, src._buf, dest.nbytes, src_dev=src.allocator.dev, dest_dev=dest.allocator.dev)
|
||||
elif src.device.startswith("DISK") and getattr(src.allocator.dev, 'fd', None) is not None \
|
||||
and hasattr(dest.allocator, 'copy_from_disk') and src.nbytes >= 4096 and dest.allocator.supports_copy_from_disk:
|
||||
dest.allocator.copy_from_disk(dest._buf, src._buf, src.nbytes)
|
||||
elif hasattr(dest.allocator, '_as_buffer'): src.allocator._copyout(dest.as_memoryview(force_zero_copy=True), src._buf)
|
||||
else: dest.allocator._copyin(dest._buf, src.as_memoryview(allow_zero_copy=True))
|
||||
return None
|
||||
if hasattr(dest.allocator,'_transfer') and dest.allocator.supports_transfer and dest.device.split(":")[0] == src.device.split(":")[0]:
|
||||
dest.allocator._transfer(dest._buf, src._buf, dest.nbytes, src_dev=src.allocator.dev, dest_dev=dest.allocator.dev)
|
||||
elif src.device.startswith("DISK") and getattr(src.allocator.dev, 'fd', None) is not None \
|
||||
and hasattr(dest.allocator, 'copy_from_disk') and src.nbytes >= 4096 and dest.allocator.supports_copy_from_disk:
|
||||
dest.allocator.copy_from_disk(dest._buf, src._buf, src.nbytes)
|
||||
elif hasattr(dest.allocator, '_as_buffer'): src.allocator._copyout(dest.as_memoryview(force_zero_copy=True), src._buf)
|
||||
else: dest.allocator._copyin(dest._buf, src.as_memoryview(allow_zero_copy=True))
|
||||
return []
|
||||
|
||||
def exec_kernel(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
|
||||
et = None
|
||||
for device, (bufs, device_vars) in zip(to_tuple(call.src[1].device), unwrap_multi(call, resolve_params(call, ctx.input_uops))):
|
||||
def exec_kernel(ctx:ExecContext, call:UOp, ast:UOp) -> list[float|None]:
|
||||
ets:list[float|None] = []
|
||||
resolved = resolve_params(call, ctx.input_uops)
|
||||
for device, (bufs, device_vars) in zip(to_tuple(call.src[1].device), unwrap_multi(call, [resolved[i] for i in ast.arg.globals])):
|
||||
var_vals = {**ctx.var_vals, **device_vars}
|
||||
prg_bufs = [bufs[i].ensure_allocated() for i in ast.arg.globals]
|
||||
prg_bufs = [b.ensure_allocated() for b in bufs]
|
||||
rt = get_runtime(device, ast, cache=ctx.cache)
|
||||
global_size, local_size = ast.arg.launch_dims(var_vals)
|
||||
with track_stats(ctx, call, device, prg_bufs, var_vals) as tm:
|
||||
et = tm[0] = rt(*[b.get_buf(device) for b in prg_bufs], global_size=global_size, local_size=local_size, vals=ast.arg.vals(var_vals),
|
||||
wait=ctx.wait, timeout=ctx.timeout)
|
||||
return et
|
||||
ets.append(rt(*[b.get_buf(device) for b in prg_bufs], global_size=global_size, local_size=local_size, vals=ast.arg.vals(var_vals),
|
||||
wait=ctx.wait, timeout=ctx.timeout))
|
||||
return ets
|
||||
|
||||
def exec_validate(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
|
||||
def exec_validate(ctx:ExecContext, call:UOp, ast:UOp) -> list[float|None]:
|
||||
import numpy as np
|
||||
for bufs, device_vars in unwrap_multi(call, resolve_params(call, ctx.input_uops)):
|
||||
bufs, dev_bufs = bufs[:len(bufs)//2], bufs[len(bufs)//2:]
|
||||
@@ -185,45 +198,36 @@ def exec_validate(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
|
||||
global_size, local_size = prg.arg.launch_dims(var_vals)
|
||||
cpu_rt(*[bufs[i].ensure_allocated()._buf for i in prg.arg.globals], global_size=global_size, local_size=local_size, vals=prg.arg.vals(var_vals))
|
||||
for i in prg.arg.outs: np.testing.assert_allclose(dev_bufs[i].ensure_allocated().numpy(), bufs[i].numpy(), rtol=1e-3, atol=1e-3)
|
||||
return None
|
||||
return []
|
||||
|
||||
def exec_encdec(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
|
||||
def exec_encdec(ctx:ExecContext, call:UOp, ast:UOp) -> list[float|None]:
|
||||
bufs = [cast(Buffer, b.buffer).ensure_allocated() for b in resolve_params(call, ctx.input_uops)]
|
||||
shape, pos_var = tuple(s.val for s in ast.src if s.op is Ops.CONST), ast.variables()[0].expr
|
||||
with track_stats(ctx, call, bufs[0].device, bufs, ctx.var_vals):
|
||||
bufs[0].allocator._encode_decode(bufs[0]._buf, bufs[1]._buf, bufs[2]._buf, [x._buf for x in bufs[3:]], shape, ctx.var_vals[pos_var])
|
||||
return None
|
||||
bufs[0].allocator._encode_decode(bufs[0]._buf, bufs[1]._buf, bufs[2]._buf, [x._buf for x in bufs[3:]], shape, ctx.var_vals[pos_var])
|
||||
return []
|
||||
|
||||
def exec_graph(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
|
||||
rt = get_graph_runtime(ast, ctx.input_uops)
|
||||
with track_stats(ctx, call, rt.device, [], ctx.var_vals) as t: t[0] = rt(ctx.input_uops, ctx.var_vals, wait=ctx.wait)
|
||||
return t[0]
|
||||
def exec_graph(ctx:ExecContext, call:UOp, ast:UOp) -> list[float|None]:
|
||||
return [get_graph_runtime(ast, ctx.input_uops)(ctx.input_uops, ctx.var_vals, wait=ctx.wait)]
|
||||
|
||||
def exec_hcq(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
|
||||
if (info:=call.arg.aux).inputs is not None:
|
||||
bufs = [_resolve(ctx.input_uops[i], ctx.input_uops).buffer for i in call.arg.aux.input_idxs]
|
||||
table = call.src[1+info.inputs].buffer
|
||||
for j,dev in enumerate(call.arg.aux.device):
|
||||
addrs = array.array('Q', [(b.bufs[j] if isinstance(b, MultiBuffer) else b).get_buf(dev).va_addr for b in bufs])
|
||||
mv = (table.bufs[j] if isinstance(table, MultiBuffer) else table).ensure_allocated()._buf.cpu_view().view(fmt='Q')
|
||||
wait_cond(lambda: mv[0], value=0, timeout_ms=ctx.timeout or getenv("HCQDEV_WAIT_TIMEOUT_MS", 30000), msg=f"{dev} hang detected")
|
||||
mv[:len(addrs)] = addrs
|
||||
def exec_hcq(ctx:ExecContext, call:UOp, ast:UOp) -> list[float|None]:
|
||||
dev = cast(Any, Device[(info:= call.arg.aux).device[0]])
|
||||
addrs = [(b.bufs[j] if isinstance(b:=_resolve(ctx.input_uops[k], ctx.input_uops).buffer, MultiBuffer) else b).get_buf(dev_name).va_addr
|
||||
for devs, idxs in info.input_idxs for j, dev_name in enumerate(devs) for k in idxs]
|
||||
dev.rt_buffer()._buf.cpu_view().view(offset=(base:=dev.rt_allocator.alloc(len(addrs) * 8)), fmt='Q')[:len(addrs)] = array.array('Q', addrs)
|
||||
|
||||
exec_kernel(replace(ctx, update_stats=DEBUG>=3), call, ast)
|
||||
if info.inputs is not None:
|
||||
tables = [UOp.from_buffer(dev.rt_buffer().view(len(idxs), dtypes.uint64, base + j*len(idxs)*8), HCQ_RUNTIME_DEV.value)
|
||||
for devs, idxs in info.input_idxs for j in range(len(devs))]
|
||||
call = call.substitute({call.src[1+info.inputs]: UOp.mstack(*tables)})
|
||||
exec_kernel(replace(ctx, var_vals={**ctx.var_vals, "hcq_inputs_ptr": dev.rt_buffer()._buf.va_addr + base}), call, ast)
|
||||
|
||||
tms = []
|
||||
for devices,name,estimates,prof in info.kernels:
|
||||
for device in devices:
|
||||
tm = None
|
||||
if prof:
|
||||
(d:=cast(Any, Device[device])).prof_ents[prof[0]] = ProfileGraphEntry(device, name, *prof)
|
||||
if ctx.wait:
|
||||
d.synchronize(timeout=ctx.timeout)
|
||||
st, en = (d.signal(x)._buf.cpu_view().view(fmt='Q')[0] for x in prof)
|
||||
tms.append(tm:=float(en-st)/d.timestamp_divider/1e6)
|
||||
stat_call = call.replace(arg=replace(call.arg, name=name, aux=replace(info, estimates=estimates, kernels=())))
|
||||
with track_stats(ctx, stat_call, device, [], ctx.var_vals) as et: et[0] = tm
|
||||
return max(tms) if tms else None
|
||||
def _prof_tm(device:str, stat_call:UOp, prof:tuple[int, ...]) -> float|None:
|
||||
(d:=cast(Any, Device[device])).prof_ents[prof[0]] = ProfileGraphEntry(device, stat_call.arg.name, prof[0], prof[1], stat_call.key)
|
||||
if not ctx.wait: return None
|
||||
d.synchronize(timeout=ctx.timeout)
|
||||
st, en = (d.signal(x)._buf.cpu_view().view(fmt='Q')[0] for x in prof)
|
||||
return float(en-st)/d.timestamp_divider/1e6
|
||||
return [_prof_tm(device, k, prof) for devices, k, prof in info.kernels if prof for device in devices] if PROFILE or ctx.wait else []
|
||||
|
||||
# flatten LINEAR-in-LINEAR: any nested LINEAR child gets inlined into its parent's src
|
||||
pm_flatten_linear = PatternMatcher([
|
||||
@@ -244,10 +248,44 @@ pm_beam = PatternMatcher([
|
||||
lambda ctx,call,sink: call.replace(src=(sink.replace(arg=replace(sink.arg, beam=ctx)), *call.src[1:])) if sink.arg.beam == 0 else None),
|
||||
])
|
||||
|
||||
pm_compile = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat((Ops.SINK, Ops.PROGRAM), name="ast"),), name="call", allow_any_len=True), lambda call,ast:
|
||||
call.replace(src=(to_program(ast, Device[call.device if isinstance(call.device, str) else call.device[0]].renderer), *call.src[1:]))),
|
||||
])
|
||||
# **************** parallel lowering + compilation ****************
|
||||
|
||||
def _compile_kernel(x:tuple[int, tuple[UOp, Renderer], dict]) -> tuple[int, UOp]:
|
||||
with Context(**x[2]): return x[0], to_program(*x[1])
|
||||
|
||||
def _needs_compile(c:UOp) -> bool:
|
||||
if c.op is not Ops.CALL: return False
|
||||
if c.src[0].op is Ops.SINK: return True
|
||||
# a PROGRAM with a ProgramInfo and a BINARY is already compiled
|
||||
return c.src[0].op is Ops.PROGRAM and not (isinstance(c.src[0].arg, ProgramInfo) and c.src[0].src[-1].op is Ops.BINARY)
|
||||
|
||||
def lower_and_compile(linear:UOp) -> UOp:
|
||||
# collect the kernels to lower and compile, deduped by their compile cache key
|
||||
calls = [c for c in linear.toposort() if _needs_compile(c)]
|
||||
rens = {c: Device[c.device if isinstance(c.device, str) else c.device[0]].renderer for c in calls}
|
||||
keys = {c: to_program_key(c.src[0], rens[c]) for c in calls}
|
||||
if not len(calls): return linear
|
||||
|
||||
# lower and compile what's not cached, in parallel if there's a worker pool
|
||||
todo = list({keys[c]: (c.src[0], rens[c]) for c in calls if keys[c] not in to_program_cache}.items())
|
||||
if len(todo):
|
||||
# kernels that beam search must compile in the parent, beam needs device access to time candidates
|
||||
|
||||
pool = None if len(todo) == 1 or any(getattr(c.src[0].arg, "beam", 0) for c in calls) else get_worker_pool()
|
||||
ctx = {v.key: v.value for v in to_program_context}
|
||||
tasks = ((i, ast_ren, ctx) for i, (_, ast_ren) in enumerate(todo))
|
||||
try:
|
||||
with tqdm(total=len(todo), desc="compiling", disable=DEBUG<1) as pbar:
|
||||
for i, prg in (map if pool is None else pool.imap_unordered)(_compile_kernel, tasks):
|
||||
pbar.set_description(f"compiling {ansipad(prg.src[0].arg.name, 40)}")
|
||||
to_program_cache[todo[i][0]] = prg
|
||||
pbar.update(1)
|
||||
except KeyboardInterrupt:
|
||||
if pool is not None: terminate_worker_pool()
|
||||
raise
|
||||
|
||||
# swap the compiled PROGRAMs into the calls
|
||||
return linear.substitute({c: c.replace(src=(to_program_cache[keys[c]], *c.src[1:])) for c in calls}, name="precompile kernels")
|
||||
|
||||
pm_optimize_local_size = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.PROGRAM, name="prg"),), name="call", allow_any_len=True), optimize_local_size),
|
||||
@@ -262,14 +300,15 @@ pm_exec = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="validate", name="ast"),), name="call", allow_any_len=True), exec_validate),
|
||||
])
|
||||
|
||||
if getenv("HCQ2"): from tinygrad.runtime.support.hcq2 import hcq_compile, hcq_link # noqa: E402 # down here, hcq2 imports the helpers above
|
||||
if getenv("HCQ2"): from tinygrad.runtime.support.hcq2 import hcq_compile, hcq_link, HCQ_RUNTIME_DEV # noqa: E402 # down here, hcq2 imports realize
|
||||
|
||||
def compile_linear(linear:UOp, beam:int|None=None, validate=False, input_uops:list[UOp]|None=None, profile:bool|None=None) -> UOp:
|
||||
if validate: linear = graph_rewrite(linear, pm_validate, name="validate", walk=True)
|
||||
if (beam_val:=BEAM.value if beam is None else beam) >= 1: linear = graph_rewrite(linear, pm_beam, ctx=beam_val, walk=True)
|
||||
linear = graph_rewrite(linear, pm_compile, name="precompile kernels", walk=True)
|
||||
linear = lower_and_compile(linear)
|
||||
linear = graph_rewrite(linear, pm_optimize_local_size, name="optimize local size", walk=True)
|
||||
if getenv("HCQ2"): linear = hcq_compile(linear, input_uops, bool(PROFILE or DEBUG >= 2) if profile is None else profile)
|
||||
return graph_rewrite(linear, pm_optimize_local_size, name="optimize local size", walk=True)
|
||||
return linear
|
||||
|
||||
def link_linear(linear:UOp, cache=True) -> UOp: return hcq_link(linear, cache=cache) if getenv("HCQ2") else linear
|
||||
|
||||
@@ -277,14 +316,15 @@ def run_linear(linear:UOp, var_vals:dict[str, int]|None=None, input_uops:Sequenc
|
||||
inputs = list(input_uops)
|
||||
if not jit: linear = link_linear(compile_linear(linear, validate=VALIDATE_WITH_CPU, input_uops=inputs))
|
||||
ctx = ExecContext(var_vals or {}, tuple(inputs), update_stats, jit, wait or DEBUG>=2)
|
||||
for call in linear.src: pm_exec.rewrite(call, ctx)
|
||||
for call in linear.src: track_stats(ctx, call, perf_counter_us(), pm_exec.rewrite(call, ctx))
|
||||
|
||||
def time_call(call:UOp, var_vals:dict[str, int]|None=None, timeout:int|None=None, clear_l2:bool=False) -> float:
|
||||
if clear_l2:
|
||||
if hasattr(dev:=Device[call.src[1].device], 'invalidate_caches'): dev.invalidate_caches()
|
||||
else:
|
||||
from tinygrad.tensor import Tensor
|
||||
with Context(DEBUG=0, BEAM=0, CAPTURING=0, TRACK_MATCH_STATS=0): Tensor.ones(1024, 1024).contiguous().realize(do_update_stats=False)
|
||||
def time_call(call:UOp, var_vals:dict[str, int]|None=None, timeout:int|None=None, clear_l2:bool=False) -> Iterator[float]:
|
||||
ctx = ExecContext(var_vals or {}, update_stats=False, wait=True, timeout=timeout, cache=False)
|
||||
linear = link_linear(compile_linear(UOp(Ops.LINEAR, src=(call,)), beam=0, profile=True), cache=ctx.cache)
|
||||
return max(pm_exec.rewrite(c, ctx) or 0.0 for c in linear.src)
|
||||
while True:
|
||||
if clear_l2:
|
||||
if hasattr(dev:=Device[call.src[1].device], 'invalidate_caches'): dev.invalidate_caches()
|
||||
else:
|
||||
from tinygrad.tensor import Tensor
|
||||
with Context(DEBUG=0, BEAM=0, CAPTURING=0, TRACK_MATCH_STATS=0): Tensor.ones(1024, 1024).contiguous().realize(do_update_stats=False)
|
||||
yield max(et for c in linear.src for et in pm_exec.rewrite(c, ctx) or [0.0])
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import multiprocessing, atexit, signal, sys, threading, contextlib
|
||||
from multiprocessing.context import SpawnContext, SpawnProcess
|
||||
from tinygrad.helpers import Context, getenv, PARALLEL
|
||||
|
||||
# generic pool of worker processes for parallel compilation, shared by kernel lowering and BEAM search
|
||||
|
||||
# workers should not open devices and should ignore ctrl c and should not launch VIZ
|
||||
def _init_worker():
|
||||
Context(ALLOW_DEVICE_USAGE=0, VIZ=0, TRACK_MATCH_STATS=0).__enter__()
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
# spawn normally reimports the user's __main__ before _init_worker. This replays top-level code and can recursively create pools. There is no public
|
||||
# multiprocessing switch to skip that import, so hide the two attributes used to locate __main__ while each worker (including replacements) starts.
|
||||
_spawn_lock, _missing = threading.Lock(), object()
|
||||
@contextlib.contextmanager
|
||||
def _without_main():
|
||||
main = sys.modules.get("__main__")
|
||||
if main is None:
|
||||
yield
|
||||
return
|
||||
with _spawn_lock:
|
||||
saved = {name:getattr(main, name, _missing) for name in ("__file__", "__spec__")}
|
||||
try:
|
||||
for name in saved: setattr(main, name, None)
|
||||
yield
|
||||
finally:
|
||||
for name,value in saved.items(): delattr(main, name) if value is _missing else setattr(main, name, value)
|
||||
|
||||
class _WorkerProcess(SpawnProcess):
|
||||
@staticmethod
|
||||
def _Popen(process_obj):
|
||||
with _without_main(): return SpawnProcess._Popen(process_obj)
|
||||
|
||||
class _WorkerContext(SpawnContext): Process = _WorkerProcess
|
||||
|
||||
worker_pool = None
|
||||
def get_worker_pool():
|
||||
global worker_pool
|
||||
if multiprocessing.current_process().daemon or PARALLEL == 0: return None
|
||||
if worker_pool is None:
|
||||
worker_pool = _WorkerContext().Pool(PARALLEL.value, _init_worker, (), getenv("BEAM_MAX_TASKS_PER_CHILD", 16))
|
||||
@atexit.register
|
||||
def close_pool(pool=worker_pool): pool.close()
|
||||
return worker_pool
|
||||
|
||||
def terminate_worker_pool():
|
||||
global worker_pool
|
||||
if worker_pool is not None: worker_pool.terminate()
|
||||
worker_pool = None
|
||||
+25
-7
@@ -44,6 +44,7 @@ def time_to_str(t:float, w=8) -> str: return next((f"{t * d:{w}.2f}{pr}" for d,p
|
||||
def size_to_str(s:int) -> str: return next((f"{s / d:.2f} {pr}" for d,pr in [(1<<30, "GB"),(1<<20, "MB"),(1<<10, "KB")] if s >= d), f"{s} B")
|
||||
def ansistrip(s:str): return re.sub('\x1b\\[(K|.*?m)', '', s)
|
||||
def ansilen(s:str): return len(ansistrip(s))
|
||||
def ansipad(s:str, w:int): return s+' '*max(w-ansilen(s), 0)
|
||||
def make_tuple(x:int|Sequence[int], cnt:int) -> tuple[int, ...]: return (x,)*cnt if isinstance(x, int) else tuple(x)
|
||||
def to_tuple(x:T|tuple[T, ...]) -> tuple[T, ...]: return x if isinstance(x, tuple) else (x,)
|
||||
def flatten(l:Iterable[Iterable[T]]): return [item for sublist in l for item in sublist]
|
||||
@@ -263,6 +264,9 @@ NUM_CPU_THREADS = ContextVar("NUM_CPU_THREADS", _get_cpu_count())
|
||||
NULL_ALLOW_COPYOUT = ContextVar("NULL_ALLOW_COPYOUT", 0)
|
||||
# VIZ implies PROFILE, but you can run PROFILE without VIZ
|
||||
VIZ = ContextVar("VIZ", 0)
|
||||
# this PARALLEL is for BEAM and compilation, it's currently disabled if you are using VIZ
|
||||
# pytest-xdist workers share the CPU budget, explicit PARALLEL still overrides this default
|
||||
PARALLEL = ContextVar("PARALLEL", NUM_CPU_THREADS.value // max(1, getenv("PYTEST_XDIST_WORKER_COUNT", 1)) if VIZ == 0 else 0)
|
||||
PROFILE = ContextVar("PROFILE", abs(VIZ.value))
|
||||
SPEC = ContextVar("SPEC", 1)
|
||||
# TODO: disable by default due to speed
|
||||
@@ -360,7 +364,8 @@ class TracingKey:
|
||||
class ProfileEvent: pass
|
||||
|
||||
@dataclass
|
||||
class ProfileRangeEvent(ProfileEvent): device:str; name:str|TracingKey; st:decimal.Decimal; en:decimal.Decimal|None=None # noqa: E702
|
||||
class ProfileRangeEvent(ProfileEvent):
|
||||
device:str; name:str|TracingKey; st:decimal.Decimal; en:decimal.Decimal|None=None; profile_key:bytes|None=None # noqa: E702
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProfilePointEvent(ProfileEvent):
|
||||
@@ -368,8 +373,8 @@ class ProfilePointEvent(ProfileEvent):
|
||||
|
||||
cpu_events:list[ProfileEvent] = []
|
||||
@contextlib.contextmanager
|
||||
def cpu_profile(name:str|TracingKey, device="TINY", display=True) -> Generator[ProfileRangeEvent, None, None]:
|
||||
res = ProfileRangeEvent(device, name, perf_counter_us())
|
||||
def cpu_profile(name:str|TracingKey, device="TINY", display=True, profile_key:bytes|None=None) -> Generator[ProfileRangeEvent, None, None]:
|
||||
res = ProfileRangeEvent(device, name, perf_counter_us(), profile_key=profile_key)
|
||||
try: yield res
|
||||
finally:
|
||||
res.en = perf_counter_us()
|
||||
@@ -460,14 +465,16 @@ def _ensure_downloads_dir() -> pathlib.Path:
|
||||
return pathlib.Path(cache_dir) / "downloads"
|
||||
|
||||
def fetch(url:str, name:pathlib.Path|str|None=None, subdir:str|None=None, gunzip:bool=False, allow_caching=not getenv("DISABLE_HTTP_CACHE"),
|
||||
headers:dict[str, str]={}, sha256:str|None=None) -> pathlib.Path:
|
||||
headers:dict[str, str]={}, sha256:str|None=None, extract:bool=False) -> pathlib.Path:
|
||||
import urllib.request
|
||||
if url.startswith(("/", ".")): return pathlib.Path(url)
|
||||
if name is not None and (isinstance(name, pathlib.Path) or '/' in name): fp = pathlib.Path(name)
|
||||
else:
|
||||
hh = "_"+hashlib.md5(("\n".join(f"{k.strip()}:{v.strip()}" for k,v in sorted(headers.items()))).encode("utf-8")).hexdigest() if headers else ""
|
||||
fp = _ensure_downloads_dir() / (subdir or "") / ((name or hashlib.md5(url.encode('utf-8')).hexdigest()) + hh + (".gunzip" if gunzip else ""))
|
||||
extract_dir = fp.parent / f"{fp.name}.extract"
|
||||
if not fp.is_file() or not allow_caching or (sha256 and hashlib.sha256(fp.read_bytes()).hexdigest() != sha256):
|
||||
if extract: shutil.rmtree(extract_dir, ignore_errors=True)
|
||||
(_dir := fp.parent).mkdir(parents=True, exist_ok=True)
|
||||
with urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "tinygrad 0.13.0", **headers}), timeout=10) as r:
|
||||
assert r.status in {200, 206}, r.status
|
||||
@@ -484,13 +491,24 @@ def fetch(url:str, name:pathlib.Path|str|None=None, subdir:str|None=None, gunzip
|
||||
pathlib.Path(f.name).rename(fp)
|
||||
progress_bar.update(close=True)
|
||||
if length and (file_size:=os.stat(fp).st_size) < length: raise RuntimeError(f"fetch size incomplete, {file_size} < {length}")
|
||||
if extract:
|
||||
if not extract_dir.is_dir():
|
||||
import tarfile
|
||||
tmpdir = tempfile.mkdtemp(dir=fp.parent)
|
||||
try:
|
||||
with tarfile.open(fp) as t: t.extractall(tmpdir, filter="data")
|
||||
try: os.rename(tmpdir, extract_dir) # rename is atomic, so concurrent fetches can't see a partial extraction
|
||||
except OSError:
|
||||
if not extract_dir.is_dir(): raise
|
||||
finally: shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
return extract_dir
|
||||
return fp
|
||||
|
||||
def fetch_fw(path:str, name:str, sha256:str) -> bytes:
|
||||
if sys.version_info >= (3,14) and (p:=pathlib.Path(f"/lib/firmware/{path}/{name}.zst")).is_file():
|
||||
from compression.zstd import decompress
|
||||
if hashlib.sha256(b:=decompress(p.read_bytes())).hexdigest() == sha256: return b
|
||||
return fetch(f"https://gitlab.com/kernel-firmware/linux-firmware/-/raw/1e2c15348485939baf1b6d1f5a7a3b799d80703d/{path}/{name}",
|
||||
return fetch(f"https://gitlab.com/kernel-firmware/linux-firmware/-/raw/0a6871b19abf5d6e024b5d208b101ae53e7fa0de/{path}/{name}",
|
||||
subdir="fw", sha256=sha256).read_bytes()
|
||||
|
||||
# *** Exec helpers
|
||||
@@ -585,9 +603,9 @@ class tqdm(Generic[T]):
|
||||
est_text = f'<{HMS(elapsed/prog-elapsed) if self.n else "?"}' if self.t else ''
|
||||
it_text = (SI(self.n/elapsed) if self.unit_scale else f"{self.n/elapsed:5.2f}") if self.n else "?"
|
||||
suf = f'{prog_text} [{HMS(elapsed)}{est_text}, {it_text}{self.unit}/s]'
|
||||
sz = max(ncols-len(self.desc)-3-2-2-len(suf), 1)
|
||||
sz = max(ncols-ansilen(self.desc)-3-2-2-len(suf), 1)
|
||||
bar = '\r' + self.desc + (f'{100*prog:3.0f}%|{("█"*int(num:=sz*prog)+" ▏▎▍▌▋▊▉"[int(8*num)%8].strip()).ljust(sz," ")}| ' if self.t else '') + suf
|
||||
print(bar[:ncols+1], flush=True, end='\n'*close, file=sys.stderr)
|
||||
print(bar, flush=True, end='\n'*close, file=sys.stderr)
|
||||
@classmethod
|
||||
def write(cls, s:str): print(f"\r\033[K{s}", flush=True, file=sys.stderr)
|
||||
|
||||
|
||||
+46
-89
@@ -1,12 +1,16 @@
|
||||
from __future__ import annotations
|
||||
import functools, itertools, pathlib
|
||||
import enum, functools, itertools, pathlib
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import cast
|
||||
from tinygrad import Tensor, nn, UOp, TinyJit, getenv, function, dtypes
|
||||
from tinygrad.dtype import AddrSpace
|
||||
from tinygrad.nn import Linear
|
||||
from tinygrad.llm.gguf import gguf_load
|
||||
from tinygrad.uop.ops import resolve, AxisType, KernelInfo, Ops, sint
|
||||
from tinygrad.uop.ops import resolve
|
||||
|
||||
class ExpertGating(enum.IntEnum):
|
||||
SOFTMAX = 1
|
||||
SIGMOID = 2
|
||||
SOFTMAX_WEIGHT = 3 # softmax over the top-k selected logits
|
||||
SQRT_SOFTPLUS = 4
|
||||
|
||||
@functools.cache
|
||||
def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0, device:str|None=None) -> Tensor:
|
||||
@@ -63,6 +67,7 @@ class TransformerConfig:
|
||||
num_experts: int = 0
|
||||
num_experts_per_tok: int = 0
|
||||
norm_topk_prob: bool = False
|
||||
expert_gating_func: ExpertGating = ExpertGating.SOFTMAX
|
||||
q_lora_rank: int = 0
|
||||
kv_lora_rank: int = 0
|
||||
shared_expert_dim: int = 0
|
||||
@@ -105,14 +110,21 @@ class FFNBlock:
|
||||
if hasattr(self, 'ffn_gate_exps'):
|
||||
h = x.unsqueeze(2) # (B, T, 1, D) - add expert dim for broadcasting
|
||||
logits = self.ffn_gate_inp(x)
|
||||
if hasattr(self, 'exp_probs_b'):
|
||||
probs = logits.sigmoid()
|
||||
_, sel = pairwise_topk(probs + self.exp_probs_b["bias"], self.config.num_experts_per_tok)
|
||||
probs = probs.gather(-1, sel)
|
||||
if self.config.norm_topk_prob: probs = probs / probs.sum(axis=-1, keepdim=True)
|
||||
else:
|
||||
vals, sel = pairwise_topk(logits, self.config.num_experts_per_tok)
|
||||
probs = vals.softmax(-1) if self.config.norm_topk_prob else logits.softmax(-1).gather(-1, sel)
|
||||
bias = self.exp_probs_b["bias"] if hasattr(self, 'exp_probs_b') else None
|
||||
gating, normalize_topk = self.config.expert_gating_func, self.config.norm_topk_prob
|
||||
# fast path: without selection bias, normalized SOFTMAX is equivalent to SOFTMAX_WEIGHT
|
||||
if gating == ExpertGating.SOFTMAX and bias is None and normalize_topk:
|
||||
gating, normalize_topk = ExpertGating.SOFTMAX_WEIGHT, False
|
||||
if gating == ExpertGating.SOFTMAX_WEIGHT: scores = logits
|
||||
elif gating == ExpertGating.SOFTMAX: scores = logits.softmax(-1)
|
||||
elif gating == ExpertGating.SIGMOID: scores = logits.sigmoid()
|
||||
elif gating == ExpertGating.SQRT_SOFTPLUS: scores = logits.softplus().sqrt()
|
||||
|
||||
_, sel = pairwise_topk(scores if bias is None else scores + bias, self.config.num_experts_per_tok)
|
||||
probs = scores.gather(-1, sel)
|
||||
# SOFTMAX_WEIGHT applies softmax after top-k selection
|
||||
if gating == ExpertGating.SOFTMAX_WEIGHT: probs = probs.softmax(-1)
|
||||
if normalize_topk: probs = probs / probs.sum(axis=-1, keepdim=True)
|
||||
probs = probs * self.config.routed_scaling_factor
|
||||
x_down = self.ffn_down_exps(sel, (self.ffn_gate_exps(sel, h).silu() * self.ffn_up_exps(sel, h)).contiguous()) # (B, T, k, D)
|
||||
out = (x_down * probs.unsqueeze(-1)).sum(axis=2) # (B, T, D)
|
||||
@@ -238,73 +250,6 @@ class MLATransformerBlock(FFNBlock):
|
||||
self.cache_k = Tensor.empty(x.shape[0], 1, self.config.max_context, self.config.kv_lora_rank + self.config.rope_dim, device=x.device)
|
||||
self.freqs_cis = precompute_freqs_cis(self.config.rope_dim, self.config.max_context, self.config.rope_theta, device=x.device)
|
||||
|
||||
def _tree_sum(xs:list[UOp]) -> UOp:
|
||||
# balanced tree keeps the reduction depth at log2(n) (compilers can't reassociate floats, so this shape reaches the ALU)
|
||||
if not xs: return UOp.const(0, dtypes.float32)
|
||||
while len(xs) > 1: xs = [a+b for a, b in zip(xs[::2], xs[1::2])] + xs[2*(len(xs)//2):]
|
||||
return xs[0]
|
||||
|
||||
@functools.cache
|
||||
def _gated_delta_prefill_kernel(core:UOp, q:UOp, k:UOp, v:UOp, beta:UOp, alpha:UOp, state:UOp, kq:UOp,
|
||||
initial:UOp|None=None) -> UOp:
|
||||
batch, heads, tokens, value_dim = cast(tuple[int, int, int, int], core.shape)
|
||||
key_dim, alpha_dim = cast(int, q.shape[-1]), cast(int, alpha.shape[-1]) if len(alpha.shape) == 4 else 1
|
||||
core, v = (x.reshape(batch*heads, tokens, value_dim) for x in (core, v))
|
||||
q, k = (x.reshape(batch*heads, tokens, key_dim) for x in (q, k))
|
||||
beta, kq = (x.reshape(batch*heads, tokens) for x in (beta, kq))
|
||||
alpha, state = alpha.reshape(batch*heads, tokens, alpha_dim), state.reshape(batch*heads, value_dim, key_dim)
|
||||
# parallel over (batch*head, state row): one thread owns one state row in registers across the sequential token loop.
|
||||
# one block per (batch*head), rows are the LOCAL threads so k/q token loads broadcast within the block
|
||||
# (on renderers without local workgroups, gpudims reruns the rows as a sequential in-thread loop)
|
||||
bh = UOp.range(batch*heads, 0, AxisType.GLOBAL)
|
||||
row = UOp.range(value_dim, 1, AxisType.LOCAL)
|
||||
cols = tuple(range(key_dim))
|
||||
current = UOp.placeholder((key_dim,), dtypes.float32, slot=0, addrspace=AddrSpace.REG)
|
||||
# the state starts from zero when the (scalar bool) initial flag is set; otherwise it resumes from `state`
|
||||
reset = None if initial is None else initial.reshape(1)[0].load()
|
||||
current = current.after(UOp.group(*(current[col].store(state[bh, row, col].float() if reset is None else
|
||||
reset.where(0, state[bh, row, col].float())) for col in cols)))
|
||||
token = UOp.range(tokens, 3, AxisType.REDUCE)
|
||||
previous = tuple(current.after(token)[col].load() for col in cols)
|
||||
keys, queries = (tuple(x[bh, token, col].load() for col in cols) for x in (k, q))
|
||||
av, bv = alpha[bh, token, row if alpha_dim > 1 else 0].load(), beta[bh, token].load()
|
||||
state_k = _tree_sum([x*y for x, y in zip(previous, keys)])
|
||||
state_q = _tree_sum([x*y for x, y in zip(previous, queries)])
|
||||
delta = (v[bh, token, row].load() - state_k*av) * bv
|
||||
step = UOp.group(core[bh, token, row].store(state_q*av + delta*kq[bh, token]),
|
||||
*(current[col].store(x*av + delta*y) for col, x, y in zip(cols, previous, keys))).end(token)
|
||||
stores = (state[bh, row, col].store(current.after(step)[col].load().cast(state.dtype)) for col in cols)
|
||||
return UOp.group(*stores).end(row, bh).sink(arg=KernelInfo(name="gated_delta_prefill", opts_to_apply=()))
|
||||
|
||||
def gated_delta_prefill(q:Tensor, k:Tensor, v:Tensor, beta:Tensor, alpha:Tensor, state:Tensor, initial:Tensor|None=None) -> Tensor:
|
||||
"""Gated delta rule over `tokens` steps in one kernel, updating the recurrent state in place.
|
||||
|
||||
q, k: (batch, heads, tokens, key_dim). v: (batch, heads, tokens, value_dim). beta: (batch, heads, tokens).
|
||||
alpha: (batch, heads, tokens) for head-wise decay, or (batch, heads, tokens, value_dim) for per-channel decay.
|
||||
state: (batch, heads, value_dim, key_dim), updated in place. initial: scalar bool Tensor; state starts from zero when set.
|
||||
`tokens` may be symbolic: the sequence is padded to its maximum size and masked (beta=0, alpha=1), so one
|
||||
graph serves every chunk size. Decoding (tokens == 1) takes a static path without padding.
|
||||
"""
|
||||
tokens:sint = q.shape[2]
|
||||
batch, heads, _, key_dim = q.shape
|
||||
value_dim = cast(int, v.shape[-1])
|
||||
assert isinstance(key_dim, int), "key/value dims must be static"
|
||||
assert q.shape == k.shape and v.shape[:3] == q.shape[:3] and beta.shape == (batch, heads, tokens)
|
||||
assert alpha.shape in ((batch, heads, tokens), (batch, heads, tokens, value_dim))
|
||||
assert state.shape == (batch, heads, value_dim, key_dim)
|
||||
static = isinstance(tokens, int)
|
||||
out_shape = v.shape
|
||||
if not static:
|
||||
# pad the variable-length sequence to its max size with no-op steps: beta=0 and alpha=1 leave the state untouched
|
||||
q, k, v, beta = (x.pad_to(x.max_shape) for x in (q, k, v, beta))
|
||||
alpha = alpha.pad_to(alpha.max_shape, value=1)
|
||||
tokens = q.shape[2]
|
||||
core, kq = Tensor.empty(batch, heads, tokens, value_dim), (q*k).sum(-1).contiguous()
|
||||
state = state if state.uop.op is Ops.AFTER else state.contiguous() # keep the AFTER chain of in-place state updates
|
||||
srcs = (core, q.contiguous(), k.contiguous(), v.contiguous(), beta.contiguous(), alpha.contiguous(), state, kq)
|
||||
out = Tensor.custom_kernel(*srcs, *(() if initial is None else (initial,)), fxn=_gated_delta_prefill_kernel)[0]
|
||||
return (out if static else out[:, :, :out_shape[2]]).reshape(out_shape)
|
||||
|
||||
class GatedDeltaNetBlock(FFNBlock):
|
||||
def __init__(self, config:TransformerConfig, ssm:SSMConfig):
|
||||
super().__init__(config)
|
||||
@@ -340,15 +285,14 @@ class GatedDeltaNetBlock(FFNBlock):
|
||||
out_gate = out_gate.reshape(B, T, self.num_v_heads, self.head_v_dim)
|
||||
beta = self.ssm_beta(x).sigmoid().reshape(B, T, self.num_v_heads)
|
||||
alpha = self.ssm_f_b(self.ssm_f_a(x)) if is_kda else self.ssm_alpha(x)
|
||||
log_alpha = ((alpha.float() + self.ssm_dt["bias"]).softplus().reshape(B, T, self.num_v_heads, -1) * self.ssm_a).squeeze(-1) \
|
||||
if is_kda else ((alpha.float() + self.ssm_dt["bias"]).softplus() * self.ssm_a).reshape(B, T, self.num_v_heads)
|
||||
log_alpha = ((alpha.float() + self.ssm_dt["bias"]).softplus().reshape(B, T, self.num_v_heads, -1) *
|
||||
self.ssm_a.reshape(self.num_v_heads, -1))
|
||||
|
||||
# qkv conv, conv_state is reset when starting from position 0
|
||||
conv_state = initial.where(0, self.conv_state)
|
||||
# assemble the conv window in a static-size buffer: [conv_state | qkv rows | zero-pad].
|
||||
# padded steps are exact no-ops: beta=0 (delta rule off), log_alpha=0 (decay 1 after exp)
|
||||
conv_window = Tensor.zeros(B, self.ssm_conv_kernel-1 + T_pad, self.conv_channels)
|
||||
win = conv_window.uop
|
||||
win = Tensor.zeros(B, self.ssm_conv_kernel-1 + T_pad, self.conv_channels).uop
|
||||
win = win.after(win[:, :self.ssm_conv_kernel-1].store(conv_state.cast(win.dtype).uop))
|
||||
win = win.after(win[:, self.ssm_conv_kernel-1:self.ssm_conv_kernel-1+T].store(self.attn_qkv(x).cast(win.dtype).uop))
|
||||
conv_window = Tensor(win)
|
||||
@@ -359,18 +303,29 @@ class GatedDeltaNetBlock(FFNBlock):
|
||||
(conv_window[:, i:i+T_pad] * self.ssm_conv1d["weight"][:, i] for i in range(self.ssm_conv_kernel))).silu()
|
||||
if symbolic:
|
||||
out_gate = out_gate.pad_to((B, T_pad, self.num_v_heads, self.head_v_dim))
|
||||
beta, log_alpha = beta.pad_to((B, T_pad, self.num_v_heads)), log_alpha.pad_to((B, T_pad, self.num_v_heads))
|
||||
beta, log_alpha = beta.pad_to((B, T_pad, self.num_v_heads)), log_alpha.pad_to((B, T_pad, *log_alpha.shape[2:]))
|
||||
q, k, v = conv_out.split([self.q_dim, self.q_dim, self.conv_channels - 2*self.q_dim], dim=-1)
|
||||
qk_eps = 1e-12 if is_kda else 1e-6
|
||||
q, k = (z.reshape(B, T_pad, self.num_k_heads, self.head_k_dim).normalize(dim=-1, eps=qk_eps)
|
||||
.repeat(1, 1, self.num_v_heads//self.num_k_heads, 1) for z in (q, k))
|
||||
v = v.reshape(B, T_pad, self.num_v_heads, self.head_v_dim)
|
||||
q, k, v, beta = [z.transpose(1, 2).float() for z in (q, k, v, beta)]
|
||||
alpha = log_alpha.transpose(1, 2).exp()
|
||||
# layout the per-step operands to broadcast against the (B, H, V, K) state
|
||||
q, k, v, beta = (z.transpose(1, 2).float() for z in (q, k, v, beta))
|
||||
q, k, v, beta = q.unsqueeze(-2) * self.head_k_dim**-0.5, k.unsqueeze(-2), v.unsqueeze(-1), beta.unsqueeze(-1).unsqueeze(-1)
|
||||
alpha = log_alpha.transpose(1, 2).exp().unsqueeze(-1) # per-channel decay for kda, per-head otherwise (B, H, T, V|1, 1)
|
||||
|
||||
# recurrent: the conv and recurrent states are updated in place
|
||||
state = Tensor(self.recurrent_state.uop.after(conv_state_store))
|
||||
core = gated_delta_prefill(q * self.head_k_dim**-0.5, k, v, beta, alpha, state, initial).transpose(1, 2)
|
||||
# recurrent: scan over the (padded) tokens, updating the recurrent state. collect the per-step outputs
|
||||
state = Tensor(self.recurrent_state.uop.after(conv_state_store)).float() # carry the conv write into this graph
|
||||
state = initial.where(0, state)
|
||||
outs = []
|
||||
for t in range(T_pad):
|
||||
s1 = state * alpha[:, :, t] # decay the state
|
||||
delta = (v[:, :, t] - (s1*k[:, :, t]).sum(-1, keepdim=True)) * beta[:, :, t] # the delta rule update
|
||||
state = s1 + delta * k[:, :, t]
|
||||
outs.append((state * q[:, :, t]).sum(-1))
|
||||
|
||||
# store the updated recurrent state in place, then read the stacked outputs after the write
|
||||
core = Tensor(outs[0].stack(*outs[1:], dim=1).contiguous().uop.after(self.recurrent_state.uop.store(state.cast(self.recurrent_state.dtype).uop)))
|
||||
|
||||
# output; undo the padding before the output projection
|
||||
z = (self.ssm_norm(core) * (out_gate.sigmoid() if is_kda else out_gate.silu())).cast(x.dtype).contiguous()
|
||||
@@ -475,6 +430,7 @@ class Transformer:
|
||||
qk_norm=int(state_dict['blk.0.attn_q_norm.weight'].shape[0]) if 'blk.0.attn_q_norm.weight' in state_dict else 0,
|
||||
num_experts=kv.get(f'{arch}.expert_count', 0), num_experts_per_tok=kv.get(f'{arch}.expert_used_count', 0),
|
||||
norm_topk_prob=kv.get(f'{arch}.expert_weights_norm', arch in ('qwen3moe', 'qwen35moe', 'kimi-linear')),
|
||||
expert_gating_func=ExpertGating(kv.get(f'{arch}.expert_gating_func', ExpertGating.SOFTMAX)),
|
||||
kv_lora_rank=kv_lora_rank, q_lora_rank=kv.get(f'{arch}.attention.q_lora_rank', 0),
|
||||
leading_dense_blocks=kv.get(f'{arch}.leading_dense_block_count', 0),
|
||||
shared_expert_dim=kv.get(
|
||||
@@ -506,6 +462,7 @@ class Transformer:
|
||||
return min(block._reusable_prefix_len(prefix_len, len(self._cached_tokens)) for block in self.blk)
|
||||
|
||||
def generate(self, tokens:list[int], chunk_size:int=32, temperature:float=0.0):
|
||||
if self.has_recurrent_block: chunk_size = 1
|
||||
v_start_pos = UOp.variable("start_pos", 0, self.max_context-1)
|
||||
v_toks = UOp.variable("toks", 1, chunk_size)
|
||||
# TODO: use UOp.variable for temperature once float variables are supported
|
||||
|
||||
@@ -33,7 +33,7 @@ def call_gradient(ctx:UOp, k:UOp, needed:set[int]) -> tuple[UOp|None, ...]:
|
||||
params = {x.arg.slot:x for x in fxn.toposort(enter_calls=False) if x.op == Ops.PARAM}
|
||||
grad_args = ctx.src
|
||||
root_grad = UOp(Ops.TUPLE, src=tuple(UOp(Ops.NOOP) if g.op is Ops.NOOP else
|
||||
g if g.base.op is Ops.CONST else g.param_like(len(args)+i) for i,g in enumerate(grad_args)))
|
||||
g if g.device is None else g.param_like(len(args)+i) for i,g in enumerate(grad_args)))
|
||||
grads = compute_gradient(fxn, root_grad, set(params.values()))
|
||||
# for precompiled calls, substitute forward outputs with params so intermediates aren't recomputed
|
||||
fwd_subs = {src: src.param_like(len(args)+len(grad_args)+i) for i, src in enumerate(fxn.src)} if k.arg.precompile else {}
|
||||
|
||||
@@ -550,6 +550,7 @@ class MovementMixin:
|
||||
if dims is None: return self.flatten().roll(shifts, 0).reshape(self.shape)
|
||||
dims, shifts = tuple(self._resolve_dim(d) for d in make_tuple(dims, 1)), make_tuple(shifts, 1)
|
||||
if len(dims) != len(shifts): raise RuntimeError(f"{len(dims)=} != {len(shifts)=}")
|
||||
if 0 in self.shape: return self
|
||||
shrink_arg: list[tuple[sint, sint]|None] = [None] * self.ndim
|
||||
for d, s in zip(dims, shifts): shrink_arg[d] = (delta:=self.shape[d]-s%self.shape[d], delta+self.shape[d])
|
||||
return self.repeat(*tuple(2 if i in dims else 1 for i in range(self.ndim))).shrink(tuple(shrink_arg))
|
||||
|
||||
@@ -35,8 +35,8 @@ class Estimates:
|
||||
while len(buf.src) and buf.op is not Ops.PARAM: buf = buf.src[0]
|
||||
if buf.op is Ops.PARAM:
|
||||
# u.src[0] is INDEX, cap at buffer size for re-reads (e.g. matmul)
|
||||
accessed = mem.get((buf, u.op), 0) + u.src[0].max_numel() * u.src[0].dtype.scalar().itemsize * mults
|
||||
mem[(buf, u.op)] = smin(accessed, buf.max_numel() * buf.dtype.scalar().itemsize)
|
||||
accessed = mem.get((buf, u.op), 0) + u.src[0].max_numel() * u.src[0].dtype.itemsize * mults
|
||||
mem[(buf, u.op)] = smin(accessed, buf.max_numel() * buf.dtype.itemsize)
|
||||
if u.op is Ops.RANGE:
|
||||
mult_stack.append(mults)
|
||||
if u.dtype is not dtypes.void: # unbounded loop, unknown trip count
|
||||
@@ -47,9 +47,9 @@ class Estimates:
|
||||
elif u.op is Ops.SPECIAL: mults *= cast(sint, u.src[0].ssimplify()) # NOTE: we don't push to the mult_stack here, you can't end these
|
||||
elif u.op is Ops.PARAM and u.arg.addrspace == AddrSpace.ALU and u.expr == 'core_id': mults *= int(u.vmax) + 1
|
||||
elif u.op is Ops.LOAD and u.src[0].addrspace != AddrSpace.REG:
|
||||
lds += u.max_numel() * u.dtype.scalar().itemsize * mults
|
||||
lds += u.max_numel() * u.dtype.itemsize * mults
|
||||
elif u.op is Ops.STORE and u.src[0].addrspace != AddrSpace.REG:
|
||||
lds += u.max_numel() * u.src[1].dtype.scalar().itemsize * mults
|
||||
lds += u.max_numel() * u.src[1].dtype.itemsize * mults
|
||||
elif u.op in GroupOp.ALU and u not in excluded:
|
||||
flops += (mults * (2 if u.op is Ops.MULACC else 1)) * u.max_numel()
|
||||
elif u.op is Ops.WMMA and u not in excluded:
|
||||
|
||||
+36
-38
@@ -7,7 +7,6 @@ from tinygrad.helpers import strip_parens, getenv, prod, dedup, Target, NUM_CPU_
|
||||
from tinygrad.dtype import dtypes, DType, AddrSpace, truncate, float_to_bf16
|
||||
from tinygrad.renderer import Renderer
|
||||
|
||||
|
||||
base_rewrite = PatternMatcher([
|
||||
# local/reg buffers
|
||||
(UPat(Ops.BUFFER, name="x"), lambda ctx,x: ctx.render_buffer(x)),
|
||||
@@ -20,6 +19,21 @@ base_rewrite = PatternMatcher([
|
||||
(UPat(Ops.IF, name="x"), lambda ctx,x: f"if ({ctx[x.src[0]]}) {{"),
|
||||
(UPat((Ops.ENDIF, Ops.END)), lambda ctx: "}"),
|
||||
|
||||
# const
|
||||
(UPat.cvar("c").cast(dtypes.floats, name="x"), lambda ctx,x,c: None if math.isfinite(v:=c.val) else \
|
||||
f"({ctx.render_cast(x, ctx.nan if math.isnan(v) else ctx.infinity if v > 0 else f'-{ctx.infinity}')})"),
|
||||
(UPat.cvar("c").cast(dtypes.float), lambda ctx,c: f"{c.val}f"),
|
||||
(UPat.cvar("c").cast(dtypes.int64), lambda ctx,c: f"{c.val}l"),
|
||||
(UPat.cvar("c").cast(dtypes.uint64, name="x"), lambda ctx,x,c: f"{truncate[x.dtype](c.val)}ul"),
|
||||
(UPat.cvar("c").cast(dtypes.uint32, name="x"), lambda ctx,x,c: f"{truncate[x.dtype](c.val)}u"),
|
||||
(UPat.cvar("c").cast(dtypes.bool), lambda ctx,c: "1" if c.val else "0"),
|
||||
# consts are rendered to larger type and casted
|
||||
(UPat.cvar("c").cast((*dtypes.fp8s, dtypes.bfloat16, dtypes.half), name="x"), lambda ctx,x,c: f"({ctx.render_cast(x, f'{c.val}f')})"),
|
||||
(UPat.cvar("c").cast((dtypes.uint8, dtypes.uint16), name="x"), lambda ctx,x,c: f"({ctx.render_cast(x, f'{c.val}u')})"),
|
||||
(UPat.cvar("c").cast((dtypes.int8, dtypes.int16), name="x"), lambda ctx,x,c: f"({ctx.render_cast(x, str(c.val))})"),
|
||||
# default const render
|
||||
(UPat.cvar("c").cast(), lambda ctx,c: str(c.val)),
|
||||
|
||||
# casting
|
||||
(UPat(Ops.CAST, name="x"), lambda ctx,x: f"__builtin_convertvector({ctx[x.src[0]]}, {ctx.render_type(x)})" \
|
||||
if x.max_numel() > 1 and x.addrspace is AddrSpace.REG else None),
|
||||
@@ -31,25 +45,9 @@ base_rewrite = PatternMatcher([
|
||||
(UPat(Ops.BARRIER), lambda ctx: ctx.barrier),
|
||||
(UPat(Ops.SPECIAL, name="x"), lambda ctx,x: f"{ctx.code_for_workitem[x.arg[0]](x.arg[-1])}; /* {(x.src[0]).render()} */"),
|
||||
|
||||
# const
|
||||
(UPat(Ops.CONST, arg=math.inf, name="x"), lambda ctx, x: f"({ctx.render_cast(x, ctx.infinity)})"),
|
||||
(UPat(Ops.CONST, arg=-math.inf, name="x"), lambda ctx, x: f"({ctx.render_cast(x, f'-{ctx.infinity}')})"),
|
||||
(UPat(Ops.CONST, dtype=dtypes.floats, name="x"), lambda ctx,x: f"({ctx.render_cast(x, ctx.nan)})" if math.isnan(x.val) else None),
|
||||
(UPat(Ops.CONST, dtype=dtypes.float, name="x"), lambda ctx,x: f"{x.val}f"),
|
||||
(UPat(Ops.CONST, dtype=dtypes.int64, name="x"), lambda ctx,x: f"{x.val}l"),
|
||||
(UPat(Ops.CONST, dtype=dtypes.uint64, name="x"), lambda ctx,x: f"{truncate[x.dtype](x.val)}ul"),
|
||||
(UPat(Ops.CONST, dtype=dtypes.uint32, name="x"), lambda ctx,x: f"{truncate[x.dtype](x.val)}u"),
|
||||
(UPat(Ops.CONST, dtype=dtypes.bool, name="x"), lambda ctx,x: "1" if x.val else "0"),
|
||||
# consts are rendered to larger type and casted
|
||||
(UPat(Ops.CONST, (*dtypes.fp8s, dtypes.bfloat16, dtypes.half), name="x"), lambda ctx,x: f"({ctx.render_cast(x, f'{x.val}f')})"),
|
||||
(UPat(Ops.CONST, (dtypes.uint8, dtypes.uint16), name="x"), lambda ctx,x: f"({ctx.render_cast(x, f'{x.val}u')})"),
|
||||
(UPat(Ops.CONST, (dtypes.int8, dtypes.int16), name="x"), lambda ctx,x: f"({ctx.render_cast(x, str(x.val))})"),
|
||||
# default const render
|
||||
(UPat(Ops.CONST, name="x"), lambda ctx,x: str(x.val)),
|
||||
|
||||
# SHRINK/INDEX
|
||||
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var('idx')), name="x"), lambda ctx,**kwargs: ctx.render_index(**kwargs)),
|
||||
(UPat(Ops.SHRINK, src=(UPat.var("buf"), UPat.var('idx'), UPat.cvar()), name="x"), lambda ctx,**kwargs: ctx.render_index(**kwargs)),
|
||||
(UPat(Ops.SHRINK, src=(UPat.var("buf"), UPat.var('idx'), UPat.cvar().cast()), name="x"), lambda ctx,**kwargs: ctx.render_index(**kwargs)),
|
||||
(UPat(Ops.STACK, name="x"),
|
||||
lambda ctx,x: f"{ctx.float4.replace('float4', ctx.render_type(x))}" + \
|
||||
f"{ctx.float4_style[0]}{','.join([ctx[y] for y in x.src])}{ctx.float4_style[1]}"),
|
||||
@@ -107,11 +105,11 @@ def uops_to_dtypes(uops:list[UOp]) -> list[tuple[DType, int]]:
|
||||
|
||||
def _wmma_name(u:UOp) -> str:
|
||||
# sanitize spaces in DType.name (int8 = "signed char")
|
||||
return f"WMMA_{'_'.join(map(str, u.arg[0]))}_{u.arg[1].name}_{u.dtype.scalar().name}".replace(" ", "_")
|
||||
return f"WMMA_{'_'.join(map(str, u.arg[0]))}_{u.arg[1].name}_{u.dtype.name}".replace(" ", "_")
|
||||
|
||||
# (name, dims, dtype_in, dtype_out, device, threads, upcast_sizes)
|
||||
def wmma_args(uops:list[UOp]):
|
||||
return dedup((_wmma_name(uop), uop.arg[0], uop.arg[1], uop.dtype.scalar(), *(uop.arg[2:4]),
|
||||
return dedup((_wmma_name(uop), uop.arg[0], uop.arg[1], uop.dtype, *(uop.arg[2:4]),
|
||||
tuple(uop.src[i].shape[-1] for i in range(3)))
|
||||
for uop in uops if uop.op is Ops.WMMA)
|
||||
|
||||
@@ -163,8 +161,8 @@ class CStyleLanguage(Renderer):
|
||||
def render_index(self, x:UOp, buf:UOp, idx:UOp):
|
||||
if buf.addrspace == AddrSpace.ALU:
|
||||
# this is lane access in C
|
||||
if idx.op is not Ops.CONST: return f"({self[buf]})[{self[idx]}]"
|
||||
return self[buf]+(f"[{idx.val}]" if buf.max_numel() > self.gep_arr_threshold else f".{'xyzwabcd'[idx.val]}")
|
||||
if not (idx.op is Ops.CAST and idx.src[0].op is Ops.CONST): return f"({self[buf]})[{self[idx]}]"
|
||||
return self[buf]+(f"[{idx.src[0].val}]" if buf.max_numel() > self.gep_arr_threshold else f".{'xyzwabcd'[idx.src[0].val]}")
|
||||
return f"({self[buf]}+{strip_parens(self[idx]) if idx.arg == Ops.ADD else self[idx]})"
|
||||
|
||||
def render_buffer(self, x:UOp):
|
||||
@@ -182,8 +180,8 @@ class CStyleLanguage(Renderer):
|
||||
if addrspace in (AddrSpace.LOCAL, AddrSpace.GLOBAL) or override_ptr:
|
||||
suffix = "*"
|
||||
if sz > 1:
|
||||
return prefix + self.type_map.get(scalar:=dtype.scalar(), scalar.name).replace(" ", "_") + str(sz) + suffix
|
||||
return prefix + self.type_map.get(scalar:=dtype.scalar(), scalar.name) + suffix
|
||||
return prefix + self.type_map.get(dtype, dtype.name).replace(" ", "_") + str(sz) + suffix
|
||||
return prefix + self.type_map.get(dtype, dtype.name) + suffix
|
||||
|
||||
def render_type(self, u:UOp): return self._render_dtype(u.dtype, u.max_numel(), u.addrspace, shape=u._shape)
|
||||
def render_access(self, u:UOp):
|
||||
@@ -210,7 +208,7 @@ class CStyleLanguage(Renderer):
|
||||
c: defaultdict[str, int] = defaultdict(int)
|
||||
name = "test"
|
||||
for u in uops:
|
||||
if u.op in {Ops.NOOP, Ops.GROUP}: continue
|
||||
if u.op in {Ops.NOOP, Ops.GROUP, Ops.CONST}: continue
|
||||
if u.op == Ops.STACK and len(u.src) == 0: continue
|
||||
if u.op is Ops.AFTER:
|
||||
r[u] = r[u.src[0]]
|
||||
@@ -228,7 +226,7 @@ class CStyleLanguage(Renderer):
|
||||
if u.op is Ops.SPECIAL: r[u] = u.arg
|
||||
elif u.op is Ops.RANGE: r[u] = f"{axis_letters[u.arg[-1]]}idx"+range_str(u)
|
||||
else:
|
||||
prefix = {Ops.WMMA: "wmma", Ops.CONST: "const", Ops.BUFFER: "buf", Ops.CAST: "cast", Ops.BITCAST: "cast", Ops.STACK: "cast",
|
||||
prefix = {Ops.WMMA: "wmma", Ops.BUFFER: "buf", Ops.CAST: "cast", Ops.BITCAST: "cast", Ops.STACK: "cast",
|
||||
Ops.INDEX: "bidx", Ops.LOAD: "val"}.get(u.op, "alu")
|
||||
r[u] = f"{prefix}{c[prefix]}"
|
||||
|
||||
@@ -236,7 +234,8 @@ class CStyleLanguage(Renderer):
|
||||
assert l is not None, f"failed to render {u.op} {u.dtype} {[(x.op,x.dtype) for x in u.src]} {u.arg}"
|
||||
|
||||
if u.op in {Ops.ENDIF, Ops.END}: depth -= 1
|
||||
if (u.op is not Ops.CAST or u.max_numel() == 1) and (u.op in {Ops.CONST, Ops.INDEX, Ops.SHRINK, Ops.CUSTOMI} or \
|
||||
if (u.op is not Ops.CAST or u.max_numel() == 1) and ((u.op is Ops.CAST and u.src[0].op is Ops.CONST) or \
|
||||
u.op in {Ops.INDEX, Ops.SHRINK, Ops.CUSTOMI} or \
|
||||
(u.op is Ops.LOAD and u.src[0].addrspace == AddrSpace.REG and child_count[u] == 1) or \
|
||||
(u.op is Ops.CAST and u.addrspace in (AddrSpace.GLOBAL, AddrSpace.LOCAL)) or \
|
||||
(u.op in {Ops.STACK, *(GroupOp.ALU-{Ops.WHERE}), Ops.CAST, Ops.BITCAST} and child_count[u] == 1 and not getenv("EXPAND_SSA"))):
|
||||
@@ -259,7 +258,8 @@ class ClangRenderer(CStyleLanguage):
|
||||
gep_arr_threshold = 0
|
||||
has_local = False
|
||||
has_threads = bool(getenv("THREADS", 1))
|
||||
global_max = (NUM_CPU_THREADS.value, 0, 0)
|
||||
@property
|
||||
def global_max(self): return (NUM_CPU_THREADS.value, 0, 0) # type: ignore[override]
|
||||
infinity = "__builtin_inff()"
|
||||
nan = '__builtin_nanf("")'
|
||||
|
||||
@@ -320,8 +320,7 @@ class OpenCLRenderer(CStyleLanguage):
|
||||
string_rewrite = PatternMatcher([
|
||||
(UPat(Ops.BITCAST, name="x"), lambda ctx,x: f"as_{ctx.render_dtype(x.dtype)}(({ctx.render_dtype(x.src[0].dtype)})({ctx[x.src[0]]}))"),
|
||||
# bfloat16 constants need to be rendered as their bit pattern since bf16 is stored as ushort
|
||||
(UPat(Ops.CONST, dtypes.bfloat16, name="x"),
|
||||
lambda ctx,x: f"{(struct.unpack('I', struct.pack('f', float_to_bf16(x.val)))[0] >> 16)}u"),
|
||||
(UPat.cvar("c").cast(dtypes.bfloat16), lambda ctx,c: f"{(struct.unpack('I', struct.pack('f', float_to_bf16(c.val)))[0] >> 16)}u"),
|
||||
# load/store image (OpenCL)
|
||||
(UPat.var('buf').index(UPat.var('idx_y'), UPat.var('idx_x')), lambda ctx,buf,idx_y,idx_x: f"IMAGE<{ctx[buf]}, {ctx[idx_y]}, {ctx[idx_x]}>"),
|
||||
(UPat(Ops.LOAD, dtype=dtypes.float, src=(UPat.var('buf').index(UPat.var('idx_y'), UPat.var('idx_x')), UPat.var("var"), UPat.var("gate"))),
|
||||
@@ -472,7 +471,7 @@ class CUDARenderer(CStyleLanguage):
|
||||
class NVCCRenderer(CUDARenderer):
|
||||
def __init__(self, target:Target): super().__init__(target, use_nvcc=True)
|
||||
|
||||
def fp8_index(dtype: DType): return (dtypes.fp8e4m3, dtypes.fp8e5m2).index(dtype.scalar())
|
||||
def fp8_index(dtype: DType): return (dtypes.fp8e4m3, dtypes.fp8e5m2).index(dtype)
|
||||
def _ocml(op): return lambda x,dtype: f"__ocml_{op}_f{ {dtypes.half:16, dtypes.double:64}.get(dtype, 32)}({x})"
|
||||
|
||||
class HIPRenderer(CStyleLanguage):
|
||||
@@ -495,10 +494,9 @@ class HIPRenderer(CStyleLanguage):
|
||||
(UPat(Ops.WMMA, name="x"), lambda ctx,x: f"__{_wmma_name(x)}({ctx[x.src[0]]}, {ctx[x.src[1]]}, {ctx[x.src[2]]},"
|
||||
f" {fp8_index(x.src[0].dtype)}, {fp8_index(x.src[0].dtype)}, 0, 0, 0, 0)" if x.arg[0][2] == 128 else None),
|
||||
(UPat(Ops.WMMA, name="x"), lambda ctx,x: f"__{_wmma_name(x)}({ctx[x.src[0]]}, {ctx[x.src[1]]}, {ctx[x.src[2]]}, 0, 0, 0)"),
|
||||
(UPat(Ops.CONST, dtypes.fp8s, name="x"), lambda ctx,x: f"f32_to_fp8({ctx.nan}, {fp8_index(x.dtype)})" if math.isnan(x.val) else None),
|
||||
(UPat(Ops.CONST, dtypes.fp8s, arg=math.inf, name="x"), lambda ctx,x: f"f32_to_fp8({ctx.infinity}, {fp8_index(x.dtype)})"),
|
||||
(UPat(Ops.CONST, dtypes.fp8s, arg=-math.inf, name="x"), lambda ctx,x: f"f32_to_fp8(-{ctx.infinity}, {fp8_index(x.dtype)})"),
|
||||
(UPat(Ops.CONST, dtypes.fp8s, name="x"), lambda ctx,x: f"f32_to_fp8({x.val}f, {fp8_index(x.dtype)})"),
|
||||
(UPat.cvar("c").cast(dtypes.fp8s, name="x"), lambda ctx,x,c:
|
||||
f"f32_to_fp8({ctx.nan if math.isnan(v:=c.val) else ctx.infinity if v == math.inf else f'-{ctx.infinity}' if v == -math.inf else f'{v}f'},"
|
||||
f" {fp8_index(x.dtype)})"),
|
||||
(UPat(Ops.CAST, dtypes.fp8s, (UPat(dtype=dtypes.float),), name="x",),
|
||||
lambda ctx,x: f"f32_to_fp8({ctx[x.src[0]]}, {fp8_index(x.dtype)})"),
|
||||
(UPat(Ops.CAST, dtypes.float, (UPat.var("y", dtypes.fp8s),), name="x",),
|
||||
@@ -539,21 +537,21 @@ class HIPRenderer(CStyleLanguage):
|
||||
prefix, ockl = [], []
|
||||
type_map = { dtypes.bfloat16: "bf16", dtypes.float: "f32", dtypes.half: "f16", dtypes.fp8e4m3: "_fp8_fp8", dtypes.fp8e5m2: "_bf8_bf8" }
|
||||
used_dtypes = uops_to_dtypes(uops)
|
||||
if any(u.op is Ops.CONST and not math.isfinite(u.val) for u in uops):
|
||||
if any(u.op is Ops.CAST and u.src[0].op is Ops.CONST and not math.isfinite(u.src[0].val) for u in uops):
|
||||
prefix += ["#define INFINITY (__builtin_inff())", "#define NAN (__builtin_nanf(\"\"))"]
|
||||
if any(u.op is Ops.SPECIAL for u in uops):
|
||||
prefix.append("typedef long unsigned int size_t;")
|
||||
ockl = [(f"__ockl_get_{name}", "unsigned int", "size_t", "const") for name in ["local_id", "group_id", "local_size"]]
|
||||
ocml_ops = {Ops.EXP2: ("exp2", "pure"), Ops.LOG2: ("log2", "pure"), Ops.SQRT: ("sqrt", "const"), Ops.SIN: ("sin", ""), Ops.TRUNC: ("trunc", "")}
|
||||
ocml = [(f"__ocml_{ocml_ops[op][0]}_f{dt.bitsize}", dt.name, dt.name, ocml_ops[op][1])
|
||||
for op, dt in dedup((u.op, u.dtype.scalar()) for u in uops) if op in ocml_ops and dt in (dtypes.half, dtypes.float, dtypes.double)]
|
||||
for op, dt in dedup((u.op, u.dtype) for u in uops) if op in ocml_ops and dt in (dtypes.half, dtypes.float, dtypes.double)]
|
||||
if any(dt == dtypes.bfloat16 for dt, _ in used_dtypes):
|
||||
prefix.append(f"typedef {'__bf16' if self.is_cdna4(self.target.arch) else 'unsigned short'} hip_bfloat16;")
|
||||
if any(dt == dtypes.half for dt, _ in used_dtypes): prefix.append("#define half _Float16")
|
||||
if any(dt in dtypes.fp8s for dt, _ in used_dtypes):
|
||||
prefix += ["typedef unsigned char hip_bf8;", "typedef unsigned char hip_fp8;"]
|
||||
if any((u.op is Ops.CAST and u.dtype in dtypes.fp8s and u.src[0].dtype == dtypes.float) or
|
||||
(u.op is Ops.CONST and u.dtype in dtypes.fp8s) for u in uops):
|
||||
(u.op is Ops.CAST and u.src[0].op is Ops.CONST and u.dtype in dtypes.fp8s) for u in uops):
|
||||
prefix.append("""static inline __attribute__((device)) unsigned char f32_to_fp8(float v, int is_bf8) {
|
||||
v = (((*(unsigned*)&v)&0x7F800000)!=0x7F800000)?__builtin_amdgcn_fmed3f(v,is_bf8?57344.0f:448.0f,is_bf8?-57344.0f:-448.0f) : v;
|
||||
return (unsigned char)(is_bf8?__builtin_amdgcn_cvt_pk_bf8_f32(v,v,0,false):__builtin_amdgcn_cvt_pk_fp8_f32(v,v,0,false));\n}""")
|
||||
|
||||
@@ -165,16 +165,16 @@ def scratch_buffer(elem_dt:DType, count:int, slot:int) -> UOp:
|
||||
return UOp.placeholder((count,), elem_dt, slot, AddrSpace.LOCAL)
|
||||
|
||||
def gated_load(ctx, addr:UOp, alt:UOp, gate:UOp, x:UOp):
|
||||
local = scratch_buffer(addr.src[0].dtype.scalar(), x.max_numel(), next(ctx))
|
||||
local_idx = local.index(UOp.const(0, dtypes.int32), dtype=dtypes.uint64)
|
||||
local = scratch_buffer(addr.src[0].dtype, x.max_numel(), next(ctx))
|
||||
local_idx = local.index(UOp.cconst(0, dtypes.int32), dtype=dtypes.uint64)
|
||||
# the selected address is a 64bit value, the AFTER orders the load after the scratch store and carries the element dtype for the encoder
|
||||
sel = gate.where(addr.replace(dtype=dtypes.uint64), local_idx)
|
||||
ptr = UOp(Ops.AFTER, addr.dtype, (sel, (local_idx if x.max_numel() == 1 else local).store(alt)))
|
||||
return ptr.load(dtype=x.dtype)
|
||||
|
||||
def gated_store(addr:UOp, gate:UOp, val:UOp):
|
||||
local = scratch_buffer(addr.src[0].dtype.scalar(), val.max_numel(), -1)
|
||||
sel = gate.where(addr.replace(dtype=dtypes.uint64), local.index(UOp.const(0, dtypes.int32), dtype=dtypes.uint64))
|
||||
local = scratch_buffer(addr.src[0].dtype, val.max_numel(), -1)
|
||||
sel = gate.where(addr.replace(dtype=dtypes.uint64), local.index(UOp.cconst(0, dtypes.int32), dtype=dtypes.uint64))
|
||||
return UOp(Ops.AFTER, addr.dtype, (sel,)).store(val)
|
||||
|
||||
# legalize the new style graph for isel. NOTE: this runs after the spec is verified, some of these rewrites violate it
|
||||
@@ -195,7 +195,7 @@ pre_isel_matcher = PatternMatcher([
|
||||
# if gate in scalar int cmove is not a comparison need to add one to set the flag
|
||||
# NOTE: the 0 is int so the bool gate zero-extends and compares as int (a byte compare renders different kernels)
|
||||
(UPat.var("m", dtypes.bool).where(UPat.var("a"), UPat.var("b")),
|
||||
lambda m,a,b: m.ne(UOp.const(0, dtypes.int)).where(a,b) if m.op not in GroupOp.Comparison else None),
|
||||
lambda m,a,b: m.ne(UOp.cconst(0, dtypes.int)).where(a,b) if m.op not in GroupOp.Comparison else None),
|
||||
])
|
||||
|
||||
# ***** X86 registers *****
|
||||
@@ -221,15 +221,14 @@ reg_strs = {"rax": {4:"eax", 2:"ax", 1:"al"}, "rcx": {4:"ecx", 2:"cx", 1:"cl"},
|
||||
|
||||
# ***** X86 instruction selection *****
|
||||
def base(x:UOp, i:int) -> UOp: return s.src[0] if (s:=x.src[i]).op is Ops.INDEX else s
|
||||
def lane(x:UOp, i:int) -> int: return s.src[1].val if (s:=x.src[i]).op is Ops.INDEX else 0
|
||||
def lane(x:UOp, i:int) -> int: return s.src[1].src[0].val if (s:=x.src[i]).op is Ops.INDEX else 0
|
||||
def to_int(dt:DType): return {dtypes.float16: dtypes.int16, dtypes.float32: dtypes.int32, dtypes.float64: dtypes.int64}[dt]
|
||||
def def_reg(dt:DType, reg:Register|None=None) -> UOp: return UOp(Ops.INS, dt, arg=X86Ops.DEFINE, tag=None if reg is None else (reg,))
|
||||
def imm(dt:DType, v:int) -> UOp: return UOp.const(truncate[dt](v), dt).rtag()
|
||||
def imm(dt:DType, v:int) -> UOp: return UOp.cconst(truncate[dt](v), dt).rtag()
|
||||
def to_imm(c:UOp) -> UOp|None:
|
||||
if c.op is not Ops.CONST: return None
|
||||
if c.dtype is dtypes.int64: return imm(dtypes.int32, c.val) if not c.overflows(dtypes.int32) else None
|
||||
if c.dtype is dtypes.uint64: return imm(dtypes.uint32, c.val) if not c.overflows(dtypes.uint32) else None
|
||||
if c.dtype in dtypes.ints+(dtypes.bool,): return imm(c.dtype, c.val)
|
||||
if not (c.op is Ops.CAST and (v:=c.src[0]).op is Ops.CONST): return None
|
||||
if c.dtype in dtypes.int64s: return imm(dtypes.int32, v.val) if not v.overflows(dtypes.int32) else None
|
||||
if c.dtype in dtypes.ints+(dtypes.bool,): return imm(c.dtype, v.val)
|
||||
return None
|
||||
def cmp(x:UOp) -> UOp:
|
||||
if x.src[0].dtype is dtypes.float32: return x.ins(X86Ops.VUCOMISS, dtype=dtypes.void)
|
||||
@@ -237,7 +236,7 @@ def cmp(x:UOp) -> UOp:
|
||||
return x.ins(X86Ops.CMP, dtype=dtypes.void) if (i:=to_imm(x.src[1])) is None else x.ins(X86Ops.CMPi, dtype=dtypes.void, src=(x.src[0], i))
|
||||
def vcmp(x:UOp) -> UOp:
|
||||
v = imm(dtypes.uint8, {Ops.CMPLT: 1, Ops.CMPNE: 4, Ops.CMPEQ: 0}[x.op])
|
||||
if x.dtype.scalar() is dtypes.float32: return x.ins(X86Ops.VCMPSS if x.max_numel() == 1 else X86Ops.VCMPPS, src=x.src + (v,))
|
||||
if x.dtype is dtypes.float32: return x.ins(X86Ops.VCMPSS if x.max_numel() == 1 else X86Ops.VCMPPS, src=x.src + (v,))
|
||||
return x.ins(X86Ops.VCMPSD if x.max_numel() == 1 else X86Ops.VCMPPD, src=x.src + (v,))
|
||||
|
||||
# vinsertps xmm2, xmm0, xmm1, imm
|
||||
@@ -252,7 +251,7 @@ def vinsertps(x:UOp) -> UOp:
|
||||
# vpinsq xmm2, xmm0, rax, imm
|
||||
# inserts element in rax into any position in xmm0, result is written to xmm2 according to imm
|
||||
def vpins(x:UOp) -> UOp:
|
||||
op = {1: X86Ops.VPINSRB, 2: X86Ops.VPINSRW, 4: X86Ops.VPINSRD, 8: X86Ops.VPINSRQ}[x.dtype.scalar().itemsize]
|
||||
op = {1: X86Ops.VPINSRB, 2: X86Ops.VPINSRW, 4: X86Ops.VPINSRD, 8: X86Ops.VPINSRQ}[x.dtype.itemsize]
|
||||
return functools.reduce(lambda ret,i: x.ins(op, src=(ret, x.src[i], imm(dtypes.uint8, i))), range(len(x.src)), def_reg(x.dtype))
|
||||
|
||||
# we don't call ctx.vreg on the srcs to avoid duplicates, a rewrite will assign the tuple of valid registers to a vreg
|
||||
@@ -289,8 +288,9 @@ def fold_address(x:UOp) -> tuple[UOp, UOp, UOp, UOp]:
|
||||
# buffers are indexed by element, everything else (the stack pointer) by byte
|
||||
scale = base.dtype.itemsize if base.op in {Ops.PARAM, Ops.BUFFER, Ops.AFTER} else 1
|
||||
sz = imm(dtypes.uint8, base.dtype.itemsize)
|
||||
if idx.op is Ops.ADD and idx.src[1].op is Ops.CONST: return (base, _cast(idx.src[0]), _disp(idx.src[1].val * scale), sz)
|
||||
if idx.op is Ops.CONST: return (base, UOp(Ops.NOOP), _disp(idx.val * scale), sz)
|
||||
if idx.op is Ops.ADD and (c:=idx.src[1]).op is Ops.CAST and c.src[0].op is Ops.CONST:
|
||||
return (base, _cast(idx.src[0]), _disp(c.src[0].val * scale), sz)
|
||||
if idx.op is Ops.CAST and idx.src[0].op is Ops.CONST: return (base, UOp(Ops.NOOP), _disp(idx.src[0].val * scale), sz)
|
||||
return (base, _cast(idx), _disp(0), sz)
|
||||
|
||||
def abi(ctx:IselContext, x:UOp) -> UOp|None:
|
||||
@@ -353,7 +353,7 @@ isel_matcher = PatternMatcher([
|
||||
# cast of void is a noop
|
||||
(UPat.var("y").cast(name="x"), lambda y,x: y if y.dtype == dtypes.void else None),
|
||||
# range is lowered to acc, cmp, jmp after regalloc
|
||||
(UPat(Ops.RANGE, src=(UPat.cvar("c"),), allow_any_len=True, name="x"), lambda c,x: x.replace(src=(imm(c.dtype, c.val),) + x.src[1:])),
|
||||
(UPat(Ops.RANGE, src=(UPat.cvar("c").cast(),), allow_any_len=True, name="x"), lambda c,x: x.replace(src=(imm(x.dtype, c.val),) + x.src[1:])),
|
||||
(UPat(Ops.RANGE, name="x"), lambda ctx,x: x.replace(tag=(ctx.vreg(WGPR),)) if not isinstance(x.tag, tuple) else None),
|
||||
# really all a backedge END is is an IF with a tag referencing the RANGE start label
|
||||
(UPat(Ops.END, src=(UPat(), UPat(), UPat(GroupOp.Comparison, name="cond")), name="x"),
|
||||
@@ -367,10 +367,10 @@ isel_matcher = PatternMatcher([
|
||||
# function abi constraints
|
||||
(UPat((Ops.PARAM, Ops.SPECIAL), name="x"), abi),
|
||||
# constants that can't be immediates, move them to registers
|
||||
(UPat.cvar("x", dtypes.int64s), lambda x: x.ins(X86Ops.MOVABS, src=(imm(x.dtype, x.val),)) if not x.tag else None),
|
||||
(UPat.cvar("x", dtypes.ints+(dtypes.bool,)), lambda x: x.ins(X86Ops.MOVi, src=(imm(x.dtype, x.val),)) if not x.tag else None),
|
||||
(UPat.cvar("x", dtypes.floats), lambda x:
|
||||
UOp.const(struct.unpack((dt:=to_int(x.dtype)).fmt, struct.pack(x.dtype.fmt, x.val))[0], dt).bitcast(x.dtype) if not x.tag else None),
|
||||
(UPat.cvar("c").cast(dtypes.int64s, name="x"), lambda c,x: x.ins(X86Ops.MOVABS, src=(imm(x.dtype, c.val),)) if not x.tag else None),
|
||||
(UPat.cvar("c").cast(dtypes.ints+(dtypes.bool,), name="x"), lambda c,x: x.ins(X86Ops.MOVi, src=(imm(x.dtype, c.val),)) if not x.tag else None),
|
||||
(UPat.cvar("c").cast(dtypes.floats, name="x"), lambda c,x:
|
||||
UOp.cconst(struct.unpack((dt:=to_int(x.dtype)).fmt, struct.pack(x.dtype.fmt, c.val))[0], dt).bitcast(x.dtype) if not x.tag else None),
|
||||
# conditional moves that use masks NOTE: these currently assume a mask producing cmp exists
|
||||
(UPat.var("m").where(UPat.var("a", dtypes.int8s+dtypes.int16s+dtypes.int32s+(dtypes.int64,)), UPat.var("b")), lambda m,a,b:
|
||||
a.ins(X86Ops.VPBLENDVB, src=(b, a, m.replace(dtype=m.src[0].dtype))) if a.max_numel() > 1 else None),
|
||||
@@ -380,7 +380,7 @@ isel_matcher = PatternMatcher([
|
||||
a.ins(X86Ops.VBLENDVPD, src=(b, a, m.replace(dtype=m.src[0].dtype)))),
|
||||
# in this case we have a mask producing comparison whose user expects a bool, so we convert to bool
|
||||
(UPat(GroupOp.Comparison, dtypes.bool, (UPat.var("y", (dtypes.float32, dtypes.float64)), UPat()), name="x"), lambda y,x:
|
||||
UOp(Ops.AND, src=(x.replace(dtype=y.dtype).bitcast(dt:=to_int(y.dtype)), UOp.const(1, dt))).f(Ops.NOOP, dtype=dtypes.bool)),
|
||||
UOp(Ops.AND, src=(x.replace(dtype=y.dtype).bitcast(dt:=to_int(y.dtype)), UOp.cconst(1, dt))).f(Ops.NOOP, dtype=dtypes.bool)),
|
||||
# conditional moves that use flags
|
||||
(UPat(Ops.CMPLT, src=(UPat(dtype=dtypes.sints), UPat()), name="m").where(UPat.var("a"), UPat.var("b")), lambda m,a,b:
|
||||
a.ins(X86Ops.CMOVL, src=(b, a, cmp(m)))),
|
||||
@@ -420,15 +420,15 @@ isel_matcher = PatternMatcher([
|
||||
(UPat(Ops.STACK, dtypes.float32, name="x"), vinsertps),
|
||||
(UPat(Ops.STACK, dtypes.ints+(dtypes.bool,), name="x"), vpins),
|
||||
# INDEX on a vector register value extracts a single element
|
||||
(UPat.var("y", dtypes.int8s+(dtypes.bool,)).index(UPat.cvar("c"), name="x"),
|
||||
(UPat.var("y", dtypes.int8s+(dtypes.bool,)).index(UPat.cvar("c").cast(), name="x"),
|
||||
lambda y,c,x: x.ins(X86Ops.VPEXTRB, src=(y, imm(dtypes.uint8, c.val))) if _is_vec_xmm(y) else None),
|
||||
(UPat.var("y", dtypes.int16s).index(UPat.cvar("c"), name="x"),
|
||||
(UPat.var("y", dtypes.int16s).index(UPat.cvar("c").cast(), name="x"),
|
||||
lambda y,c,x: x.ins(X86Ops.VPEXTRW, src=(y, imm(dtypes.uint8, c.val))) if _is_vec_xmm(y) else None),
|
||||
(UPat.var("y", dtypes.int32s).index(UPat.cvar("c"), name="x"),
|
||||
(UPat.var("y", dtypes.int32s).index(UPat.cvar("c").cast(), name="x"),
|
||||
lambda y,c,x: x.ins(X86Ops.VPEXTRD, src=(y, imm(dtypes.uint8, c.val))) if _is_vec_xmm(y) else None),
|
||||
(UPat.var("y", dtypes.int64s).index(UPat.cvar("c"), name="x"),
|
||||
(UPat.var("y", dtypes.int64s).index(UPat.cvar("c").cast(), name="x"),
|
||||
lambda y,c,x: x.ins(X86Ops.VPEXTRQ, src=(y, imm(dtypes.uint8, c.val))) if _is_vec_xmm(y) else None),
|
||||
(UPat.var("y", dtypes.floats).index(UPat.cvar("c"), name="x"),
|
||||
(UPat.var("y", dtypes.floats).index(UPat.cvar("c").cast(), name="x"),
|
||||
lambda y,c,x: x.ins(X86Ops.VPSRLDQ, src=(y, imm(dtypes.uint8, c.val * x.dtype.itemsize))) if _is_vec_xmm(y) else None),
|
||||
# packed bitwise
|
||||
((UPat() & UPat()).named("x"), lambda x: x.ins(X86Ops.VPAND) if x.max_numel() > 1 else None),
|
||||
@@ -453,15 +453,19 @@ isel_matcher = PatternMatcher([
|
||||
# scalar int binary
|
||||
((UPat(dtype=dtypes.ints).alu(Ops.CDIV, UPat())).named("x"), idiv),
|
||||
# scalar int binary with immediate
|
||||
(UPat.var("a", dtypes.ints) << UPat.cvar("c"), lambda a,c: a.ins(X86Ops.SHLi, src=(a, imm(dtypes.uint8, c.val)))),
|
||||
(UPat.var("a", dtypes.uints) >> UPat.cvar("c"), lambda a,c: a.ins(X86Ops.SHRi, src=(a, imm(dtypes.uint8, c.val)))),
|
||||
(UPat.var("a", dtypes.sints) >> UPat.cvar("c"), lambda a,c: a.ins(X86Ops.SARi, src=(a, imm(dtypes.uint8, c.val)))),
|
||||
(UPat.var("a", dtypes.ints) + UPat.cvar("c"), lambda a,c: a.ins(X86Ops.ADDi, src=(a, i)) if (i:=to_imm(c)) is not None else None),
|
||||
(UPat.var("a", dtypes.ints) * UPat.cvar("c"), lambda a,c: a.ins(X86Ops.IMULi, src=(a, i)) if (i:=to_imm(c)) is not None else None),
|
||||
(UPat.var("a", dtypes.ints+(dtypes.bool,)) & UPat.cvar("c"), lambda a,c: a.ins(X86Ops.ANDi, src=(a, i)) if (i:=to_imm(c)) is not None else None),
|
||||
(UPat.var("a", dtypes.ints+(dtypes.bool,)) | UPat.cvar("c"), lambda a,c: a.ins(X86Ops.ORi, src=(a, i)) if (i:=to_imm(c)) is not None else None),
|
||||
(UPat.var("a", dtypes.ints+(dtypes.bool,)) ^ UPat.cvar("c"), lambda a,c: a.ins(X86Ops.XORi, src=(a, i)) if (i:=to_imm(c)) is not None else None),
|
||||
(UPat(Ops.SUB, dtypes.ints, (UPat.var("a"), UPat.cvar("c"))), lambda a,c: a.ins(X86Ops.SUBi, src=(a, i)) if (i:=to_imm(c)) is not None else None),
|
||||
(UPat.var("a", dtypes.ints) << UPat.cvar("c").cast(), lambda a,c: a.ins(X86Ops.SHLi, src=(a, imm(dtypes.uint8, c.val)))),
|
||||
(UPat.var("a", dtypes.uints) >> UPat.cvar("c").cast(), lambda a,c: a.ins(X86Ops.SHRi, src=(a, imm(dtypes.uint8, c.val)))),
|
||||
(UPat.var("a", dtypes.sints) >> UPat.cvar("c").cast(), lambda a,c: a.ins(X86Ops.SARi, src=(a, imm(dtypes.uint8, c.val)))),
|
||||
(UPat.var("a", dtypes.ints) + UPat.cvar().cast(name="c"), lambda a,c: a.ins(X86Ops.ADDi, src=(a, i)) if (i:=to_imm(c)) is not None else None),
|
||||
(UPat.var("a", dtypes.ints) * UPat.cvar().cast(name="c"), lambda a,c: a.ins(X86Ops.IMULi, src=(a, i)) if (i:=to_imm(c)) is not None else None),
|
||||
(UPat.var("a", dtypes.ints+(dtypes.bool,)) & UPat.cvar().cast(name="c"),
|
||||
lambda a,c: a.ins(X86Ops.ANDi, src=(a, i)) if (i:=to_imm(c)) is not None else None),
|
||||
(UPat.var("a", dtypes.ints+(dtypes.bool,)) | UPat.cvar().cast(name="c"),
|
||||
lambda a,c: a.ins(X86Ops.ORi, src=(a, i)) if (i:=to_imm(c)) is not None else None),
|
||||
(UPat.var("a", dtypes.ints+(dtypes.bool,)) ^ UPat.cvar().cast(name="c"),
|
||||
lambda a,c: a.ins(X86Ops.XORi, src=(a, i)) if (i:=to_imm(c)) is not None else None),
|
||||
(UPat(Ops.SUB, dtypes.ints, (UPat.var("a"), UPat.cvar().cast(name="c"))),
|
||||
lambda a,c: a.ins(X86Ops.SUBi, src=(a, i)) if (i:=to_imm(c)) is not None else None),
|
||||
# scalar int binary with register
|
||||
((UPat(dtype=dtypes.ints) << UPat()).named("x"), lambda x: shift(x, X86Ops.SHL)),
|
||||
((UPat(dtype=dtypes.uints) >> UPat()).named("x"), lambda x: shift(x, X86Ops.SHR)),
|
||||
@@ -572,7 +576,7 @@ def lower_range(ctx, x:UOp) -> tuple[UOp, list[UOp]]:
|
||||
if x.dtype is dtypes.void: return (label, [label])
|
||||
else:
|
||||
acc = x.ins(X86Ops.MOVi, src=(imm(x.dtype, 0),) + x.src[1:])
|
||||
cmp = UOp(Ops.INS, arg=X86Ops.CMPi if x.src[0].op is Ops.CONST else X86Ops.CMP, src=(acc, x.src[0]))
|
||||
cmp = UOp(Ops.INS, arg=X86Ops.CMPi if x.src[0].op is Ops.CAST else X86Ops.CMP, src=(acc, x.src[0]))
|
||||
jump_out = UOp(Ops.INS, arg=X86Ops.JGE, src=(cmp,), tag=f".LOOP_OUT_{loop_label}")
|
||||
ctx.loop_label[acc] = loop_label
|
||||
return (acc, [acc, label, cmp, jump_out])
|
||||
@@ -591,7 +595,7 @@ def lower_loop(ctx, x:UOp) -> tuple[UOp, list[UOp]]:
|
||||
# final rewrite to match the isa spec
|
||||
post_regalloc_matcher = PatternMatcher([
|
||||
# rewrite FRAME_INDEX to IMM now that the stack size is known
|
||||
(UPat(Ops.INS, arg=X86Ops.FRAME_INDEX, name="x"), lambda ctx,x: (nx:=x.const_like(ctx.stack_size + x.tag), [nx])),
|
||||
(UPat(Ops.INS, arg=X86Ops.FRAME_INDEX, name="x"), lambda ctx,x: (nx:=UOp.cconst(ctx.stack_size + x.tag, x.dtype), [nx])),
|
||||
# expand the cmp here so we can preserve rng src edge to get label from ctx
|
||||
(UPat(Ops.INS, arg=X86Ops.LOOP_CMP, name="x"), lower_loop),
|
||||
# rewrite RANGE to ACC = 0 -> LABEL -> JUMP if ACC >= loop bound
|
||||
@@ -614,7 +618,7 @@ def encode(x:UOp, opc:int, reg:int|None=None, pp:int=0, sel:int=0, we:int=0) ->
|
||||
rm = cast(Register, greg(rm_uop)).index
|
||||
idx = cast(Register, greg(idx_uop)).index if idx_uop is not None and greg(idx_uop) is not None else 4
|
||||
# for a memory operand the rm size is the element size from the address, otherwise it's the size of the value in the register
|
||||
rm_sz = sz_uop.val if sz_uop is not None else rm_uop.dtype.itemsize
|
||||
rm_sz = sz_uop.src[0].val if sz_uop is not None else rm_uop.dtype.itemsize
|
||||
reg_sz = reg_uop.dtype.itemsize if reg_uop is not None else 0
|
||||
sz = reg_sz or rm_sz
|
||||
|
||||
@@ -633,10 +637,12 @@ def encode(x:UOp, opc:int, reg:int|None=None, pp:int=0, sel:int=0, we:int=0) ->
|
||||
if sz == 2: inst += bytes([0x66])
|
||||
# bit signaling 64 bit variant of instruction
|
||||
w = sz == 8
|
||||
# REX byte is required when 64 bit or an extended reg is used (index 8 - 15) or lower 8 bits of (rsp, rbp, rsi, rdi) are accessed
|
||||
if w | r | _x | b | (reg_sz == 1 & reg >> 2) | (rm_sz == 1 & rm >> 2): inst += bytes([0b0100 << 4 | w << 3 | r << 2 | _x << 1 | b])
|
||||
# legacy 8bit opcode is 1 less than 16-64bit variants
|
||||
if (rm_sz == 1 or reg_sz == 1) and x.arg not in X86GroupOp.ReadFlags | {X86Ops.LEA}: opc -= 1
|
||||
demote = (rm_sz == 1 or reg_sz == 1) and x.arg not in X86GroupOp.ReadFlags | {X86Ops.LEA}
|
||||
# REX byte is required when 64 bit or an extended reg is used (index 8 - 15) or lower 8 bits of (rsp, rbp, rsi, rdi) are accessed
|
||||
if w | r | _x | b | (reg_sz == 1 & reg >> 2) | (rm_sz == 1 & rm >> 2) | (demote and disp_uop is None and rm >= 4):
|
||||
inst += bytes([0b0100 << 4 | w << 3 | r << 2 | _x << 1 | b])
|
||||
if demote: opc -= 1
|
||||
# OPCODE byte
|
||||
inst += opc.to_bytes((opc.bit_length() + 7) // 8, 'big')
|
||||
# MODRM byte
|
||||
@@ -647,10 +653,10 @@ def encode(x:UOp, opc:int, reg:int|None=None, pp:int=0, sel:int=0, we:int=0) ->
|
||||
# 0b10 -- signals memory access with 32bit displacement
|
||||
# 0b11 -- signals no memory access
|
||||
if disp_uop is not None:
|
||||
assert disp_uop.op is Ops.CONST, "displacement must be a constant"
|
||||
assert disp_uop.op is Ops.CAST, "displacement must be a literal"
|
||||
assert disp_uop.dtype in (dtypes.int8, dtypes.int32), "displacement can only be 1 or 4 byte signed int"
|
||||
# rbp/r13 always require a displacement
|
||||
if disp_uop.val != 0 or rm == 0b101: mod = 0b01 if disp_uop.dtype.itemsize == 1 else 0b10
|
||||
if disp_uop.src[0].val != 0 or rm == 0b101: mod = 0b01 if disp_uop.dtype.itemsize == 1 else 0b10
|
||||
else: mod = 0b00
|
||||
else: mod = 0b11
|
||||
# x 0b0 and idx 0b100 means rsp which means no index exists
|
||||
@@ -664,10 +670,10 @@ def encode(x:UOp, opc:int, reg:int|None=None, pp:int=0, sel:int=0, we:int=0) ->
|
||||
# DISP byte
|
||||
if mod == 0b01 or mod == 0b10:
|
||||
assert disp_uop is not None
|
||||
inst += struct.pack(unwrap(disp_uop.dtype.fmt), disp_uop.val)
|
||||
inst += struct.pack(unwrap(disp_uop.dtype.fmt), disp_uop.src[0].val)
|
||||
# IMM byte
|
||||
if imm_uop is not None:
|
||||
if imm_uop.op is Ops.CONST: inst += struct.pack(unwrap(imm_uop.dtype.fmt), imm_uop.val)
|
||||
if imm_uop.op is Ops.CAST: inst += struct.pack(unwrap(imm_uop.dtype.fmt), imm_uop.src[0].val)
|
||||
elif isinstance(greg(imm_uop), Register): inst += bytes([(greg(imm_uop).index & 0b1111) << 4 | 0b0000])
|
||||
return inst
|
||||
|
||||
@@ -677,13 +683,13 @@ def encode(x:UOp, opc:int, reg:int|None=None, pp:int=0, sel:int=0, we:int=0) ->
|
||||
if x.arg in X86GroupOp.WriteMem:
|
||||
if len(x.src) > 4: address, rest = x.src[:4], x.src[4:]
|
||||
else: address, rest = (x, None, None, None), x.src
|
||||
imm_uop = rest[:1] if rest and rest[0].op is Ops.CONST else (None,)
|
||||
imm_uop = rest[:1] if rest and rest[0].op is Ops.CAST else (None,)
|
||||
return _encode(rest[0], *address, *(None, *rest[1:])) if reg is None else _encode(None, *address, *(None, *imm_uop))
|
||||
|
||||
if x.arg in X86GroupOp.Rm1st:
|
||||
if len(x.src) > 3: address, rest = x.src[:4], x.src[4:]
|
||||
else: address, rest = (x.src[0], None, None, None), x.src[1:]
|
||||
imm_uop = rest[:1] if rest and rest[0].op is Ops.CONST else (None,)
|
||||
imm_uop = rest[:1] if rest and rest[0].op is Ops.CAST else (None,)
|
||||
return _encode(x, *address, *(None, *imm_uop)) if reg is None else _encode(None, *address, *(x if sel else None, *imm_uop))
|
||||
|
||||
if x.arg in X86GroupOp.Rm2nd:
|
||||
@@ -701,7 +707,7 @@ def encode(x:UOp, opc:int, reg:int|None=None, pp:int=0, sel:int=0, we:int=0) ->
|
||||
encodings = {
|
||||
# moves
|
||||
X86Ops.MOVABS: lambda x:
|
||||
bytes([0b0100 << 4 | 0b1 << 3 | 0b00 << 2 | greg(x).index >> 3, 0xB8 + (greg(x).index & 0b111)]) + struct.pack(x.dtype.fmt, x.src[0].val),
|
||||
bytes([0b0100 << 4 | 0b1 << 3 | 0b00 << 2 | greg(x).index >> 3, 0xB8 + (greg(x).index & 0b111)]) + struct.pack(x.dtype.fmt, x.src[0].src[0].val),
|
||||
X86Ops.MOV: lambda x: encode(x, 0x8B), X86Ops.MOVi: lambda x: encode(x, 0xC7, reg=0),
|
||||
X86Ops.MOVm: lambda x: encode(x, 0x89), X86Ops.LEA: lambda x: encode(x, 0x8D),
|
||||
X86Ops.VMOVSS: lambda x: encode(x, 0x10, pp=2, sel=1), X86Ops.VMOVSSm: lambda x: encode(x, 0x11, pp=2, sel=1),
|
||||
@@ -724,8 +730,8 @@ encodings = {
|
||||
X86Ops.VCVTPS2PD: lambda x: encode(x, 0x5A, pp=0, sel=1), X86Ops.VCVTPD2PS: lambda x: encode(x, 0x5A, pp=1, sel=1),
|
||||
X86Ops.VCVTTPS2DQ: lambda x: encode(x, 0x5B, pp=2, sel=1), X86Ops.VCVTTPD2DQ: lambda x: encode(x, 0xE6, pp=1, sel=1),
|
||||
# the int src is the 2nd src (the rm field), if it was folded into a memory operand its width is the element size of the address
|
||||
X86Ops.VCVTSI2SS: lambda x: encode(x, 0x2A, pp=2, sel=1, we=(x.src[4].val if len(x.src) > 4 else x.src[1].dtype.itemsize) == 8),
|
||||
X86Ops.VCVTSI2SD: lambda x: encode(x, 0x2A, pp=3, sel=1, we=(x.src[4].val if len(x.src) > 4 else x.src[1].dtype.itemsize) == 8),
|
||||
X86Ops.VCVTSI2SS: lambda x: encode(x, 0x2A, pp=2, sel=1, we=(x.src[4].src[0].val if len(x.src) > 4 else x.src[1].dtype.itemsize) == 8),
|
||||
X86Ops.VCVTSI2SD: lambda x: encode(x, 0x2A, pp=3, sel=1, we=(x.src[4].src[0].val if len(x.src) > 4 else x.src[1].dtype.itemsize) == 8),
|
||||
X86Ops.VCVTTSS2SI: lambda x: encode(x, 0x2C, pp=2, sel=1, we=x.dtype.itemsize == 8),
|
||||
X86Ops.VCVTTSD2SI: lambda x: encode(x, 0x2C, pp=3, sel=1, we=x.dtype.itemsize == 8),
|
||||
# int division
|
||||
@@ -804,7 +810,8 @@ class X86Renderer(ISARenderer):
|
||||
device = "CPU"
|
||||
has_local = False
|
||||
has_threads = bool(getenv("THREADS", 1))
|
||||
global_max = (NUM_CPU_THREADS.value, 0, 0)
|
||||
@property
|
||||
def global_max(self): return (NUM_CPU_THREADS.value, 0, 0) # type: ignore[override]
|
||||
extra_matcher = extra_matcher
|
||||
pre_isel_matcher = pre_isel_matcher
|
||||
isel_matcher = isel_matcher
|
||||
@@ -840,10 +847,10 @@ class X86Renderer(ISARenderer):
|
||||
def _format_op(x:UOp) -> str: return f" {(o[7:-1] if (o:=str(x.arg))[-1] in ('i', 'm') else o[7:]).lower():7s}"
|
||||
def _format_operands(x:UOp) -> str:
|
||||
def _format(src:tuple[UOp, ...]) -> list[str]:
|
||||
return [str(s.val) if s.op is Ops.CONST else reg_strs[o].get(s.dtype.itemsize, o) if \
|
||||
return [str(s.src[0].val) if s.op is Ops.CAST else reg_strs[o].get(s.dtype.itemsize, o) if \
|
||||
(o:=str(greg(s))) in reg_strs else o for s in src if greg(s) is not None]
|
||||
def _mem_adress(base:UOp, idx:UOp, disp:UOp, sz:UOp) -> list[str]:
|
||||
return [f"[{greg(base)}" + (f" + {greg(idx)}*{sz.val}" if greg(idx) else "") + (f" + {disp.val}" if disp.val else "") + "]"]
|
||||
return [f"[{greg(base)}" + (f" + {greg(idx)}*{sz.src[0].val}" if greg(idx) else "") + (f" + {d}" if (d:=disp.src[0].val) else "") + "]"]
|
||||
|
||||
if len(x.src) > 4 and x.arg in X86GroupOp.WriteMem: ret = _mem_adress(*x.src[:4]) + _format(x.src[4:])
|
||||
elif len(x.src) > 3 and x.arg in X86GroupOp.Rm1st: ret = _format((x,)) + _mem_adress(*x.src[:4]) + _format(x.src[4:])
|
||||
|
||||
@@ -81,8 +81,8 @@ base_rewrite = PatternMatcher([
|
||||
(UPat((Ops.INDEX, Ops.SHRINK), src=(UPat((Ops.BUFFER, Ops.PARAM, Ops.AFTER)),), allow_any_len=True, name="x"), lambda ctx,x:
|
||||
f" {ctx[x]} = getelementptr inbounds {ldt(x.dtype)}, {ldt(x.dtype, ptr=True)} {ctx[x.src[0]]}, {ldt(x.src[1].dtype)} {ctx[x.src[1]]}"),
|
||||
# register index
|
||||
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.cvar("idx")), name="x"), lambda ctx,buf,idx,x:
|
||||
f" {ctx[x]} = extractelement {ldt(buf.dtype, buf.max_numel())} {ctx[buf]}, i32 {idx.val}" if buf.addrspace == AddrSpace.ALU else None),
|
||||
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.cvar("c").cast()), name="x"), lambda ctx,buf,c,x:
|
||||
f" {ctx[x]} = extractelement {ldt(buf.dtype, buf.max_numel())} {ctx[buf]}, i32 {c.val}" if buf.addrspace == AddrSpace.ALU else None),
|
||||
|
||||
# load/store
|
||||
(UPat(Ops.LOAD, src=(UPat.var("idx"), UPat.var("alt"), UPat.var("mask")), name="x"),
|
||||
@@ -165,7 +165,7 @@ class LLVMRenderer(Renderer):
|
||||
local_args: list[str] = []
|
||||
name = "test"
|
||||
for u in uops:
|
||||
if u.op in {Ops.NOOP, Ops.GROUP}: continue
|
||||
if u.op in {Ops.NOOP, Ops.GROUP, Ops.CONST}: continue
|
||||
if u.op is Ops.AFTER:
|
||||
r[u] = r[u.src[0]]
|
||||
continue
|
||||
@@ -185,7 +185,7 @@ class LLVMRenderer(Renderer):
|
||||
kernel.append(f" {r[u]} = addrspacecast [{size} x {ldt(u.dtype)}] addrspace(3)* @{r[u][1:]} to [{size} x {ldt(u.dtype)}]*")
|
||||
else:
|
||||
kernel.append(f" {r[u]} = alloca [{size} x {ldt(u.dtype)}], align 16")
|
||||
elif u.op is Ops.CONST: r[u] = lconst(u.val, u.dtype)
|
||||
elif u.op is Ops.CAST and u.src[0].op is Ops.CONST: r[u] = lconst(u.src[0].val, u.dtype)
|
||||
elif u.op is Ops.CAST and ldt(u.dtype) == ldt(u.src[0].dtype):
|
||||
r[u] = r[u.src[0]] # cast from signed to unsigned of the same size is a noop, or pointer cast
|
||||
else:
|
||||
@@ -204,7 +204,8 @@ class LLVMRenderer(Renderer):
|
||||
class CPULLVMRenderer(LLVMRenderer):
|
||||
has_local = False
|
||||
has_threads = bool(getenv("THREADS", 1))
|
||||
global_max = (NUM_CPU_THREADS.value, 0, 0)
|
||||
@property
|
||||
def global_max(self): return (NUM_CPU_THREADS.value, 0, 0) # type: ignore[override]
|
||||
abi = 'win64cc' if sys.platform == 'win32' else None
|
||||
string_rewrite = base_rewrite
|
||||
def render(self, uops: list[UOp]) -> str: return "\n".join((k:=self._render_kernel(uops))[0] + (k[1], self._render_footer(uops)))
|
||||
|
||||
@@ -145,7 +145,7 @@ class NIRRenderer(Renderer):
|
||||
])
|
||||
|
||||
def_rewrite = PatternMatcher([
|
||||
(UPat(Ops.CONST, name="x"), lambda ctx,x: nimm(ctx.b, x.val, x.dtype)),
|
||||
(UPat.cvar("c").cast(name="x"), lambda ctx,x,c: nimm(ctx.b, c.val, x.dtype)),
|
||||
(UPat(Ops.PARAM, name="x"), lambda ctx,x: ctx.param(ctx.b, x, x.dtype.itemsize if x.addrspace is AddrSpace.ALU else 8)),
|
||||
(UPat(Ops.SPECIAL, name="x"), lambda ctx,x: nchannel(ctx.b, {'g':ngid, 'l':nlid, 'i': nid}[x.arg[0]](ctx.b), int(x.arg[-1]))),
|
||||
(UPat(Ops.STORE, src=(UPat((Ops.INDEX, Ops.SHRINK), src=(UPat.var("buf"),UPat.var("off")), allow_any_len=True), UPat.var("val"))),
|
||||
@@ -186,16 +186,17 @@ class NIRRenderer(Renderer):
|
||||
|
||||
def render(self, uops:list[UOp]):
|
||||
self.prerender(uops)
|
||||
for u in [u for u in uops if u.op is Ops.SPECIAL and u.arg[0] == "l"]: self.b.shader.contents.info.workgroup_size[int(u.arg[-1])] = u.src[0].val
|
||||
for u in [u for u in uops if u.op is Ops.SPECIAL and u.arg[0] == "l"]:
|
||||
self.b.shader.contents.info.workgroup_size[int(u.arg[-1])] = u.src[0].src[0].val
|
||||
self.r: dict[UOp, Any] = {}
|
||||
self.param_idx = 0
|
||||
ranges: list[mesa.nir_def|None] = []
|
||||
|
||||
for u in uops:
|
||||
if u.op in {Ops.NOOP, Ops.GROUP} or (u.op is Ops.STACK and len(u.src) == 0): pass
|
||||
if u.op in {Ops.NOOP, Ops.GROUP, Ops.CONST} or (u.op is Ops.STACK and len(u.src) == 0): pass
|
||||
elif u.op in {Ops.INDEX, Ops.SHRINK}:
|
||||
# INDEX on a register value picks the element, memory INDEX is handled in the LOAD/STORE patterns
|
||||
if u.src[0].op not in {Ops.PARAM, Ops.BUFFER, Ops.AFTER}: self.r[u] = nchannel(self.b, self.r[u.src[0]], u.src[1].val)
|
||||
if u.src[0].op not in {Ops.PARAM, Ops.BUFFER, Ops.AFTER}: self.r[u] = nchannel(self.b, self.r[u.src[0]], u.src[1].src[0].val)
|
||||
elif u.op is Ops.AFTER:
|
||||
self.r[u] = self.r[u.src[0]]
|
||||
elif u.op == Ops.SINK:
|
||||
|
||||
+19
-19
@@ -64,7 +64,7 @@ def render_wmma(ctx: "PTXRenderer", wmma: UOp):
|
||||
|
||||
for src, regs in zip(wmma.src, ctx.wmma_r):
|
||||
for i, reg in enumerate(regs): # pack input and acc registers
|
||||
if (elems_per_reg := 4 // src.dtype.scalar().itemsize) == 1: yield f"mov.b32 {reg}, {ctx.r[src][i]};"
|
||||
if (elems_per_reg := 4 // src.dtype.itemsize) == 1: yield f"mov.b32 {reg}, {ctx.r[src][i]};"
|
||||
else: yield f"mov.b32 {reg}, {{{', '.join(ctx.r[src][i * elems_per_reg : (i+1) * elems_per_reg])}}};"
|
||||
|
||||
dt_map_in, dt_map_out = {dtypes.float: "tf32", dtypes.half: "f16"}, {dtypes.float: "f32", dtypes.half: "f16"}
|
||||
@@ -79,8 +79,8 @@ def modifier(a: DType, b: DType): return '.rzi' if dtypes.is_int(a) and dtypes.i
|
||||
(a.itemsize < b.itemsize or dtypes.is_int(b) or b == dtypes.bool) else ''
|
||||
|
||||
string_rewrite = PatternMatcher([
|
||||
(UPat.cvar("x", dtypes.bool), lambda ctx, x: f"setp.ne.s16 {ctx.r[x]}, {render_val(x.val, x.dtype)}, 0;"),
|
||||
(UPat.cvar("x"), lambda ctx, x: f"mov.b{ctx.types[x.dtype][1:]} {ctx.r[x]}, {render_val(x.val, x.dtype)};"),
|
||||
(UPat.cvar("c").cast(dtypes.bool, name="x"), lambda ctx, x, c: f"setp.ne.s16 {ctx.r[x]}, {render_val(c.val, x.dtype)}, 0;"),
|
||||
(UPat.cvar("c").cast(name="x"), lambda ctx, x, c: f"mov.b{ctx.types[x.dtype][1:]} {ctx.r[x]}, {render_val(c.val, x.dtype)};"),
|
||||
(UPat(Ops.SPECIAL, name="x"), lambda ctx,x: f"mov.u32 %{x.arg}, %{'ctaid' if x.arg[0] == 'g' else 'tid'}.{chr(120+int(x.arg[-1]))};"),
|
||||
(UPat(Ops.PARAM, name="x"), lambda ctx, x:
|
||||
f"ld.param.{ctx.types[dtypes.ulong] if x.addrspace is AddrSpace.GLOBAL else ctx.mem_types[x.dtype]} {ctx.r[x]}, [data{x.arg.slot}+0];"),
|
||||
@@ -101,17 +101,17 @@ string_rewrite = PatternMatcher([
|
||||
if loc.addrspace == AddrSpace.REG else None),
|
||||
(UPat(Ops.STORE, src=(UPat((Ops.INDEX, Ops.SHRINK), name="loc"), UPat.var("var"))),
|
||||
lambda ctx, loc, var: f"st.{mem_type(loc)}" + \
|
||||
f"{f'.v{cnt}' if ((cnt:=var.max_numel())>1) else ''}.{ctx.mem_types[var.dtype.scalar()]} " + \
|
||||
f"{f'.v{cnt}' if ((cnt:=var.max_numel())>1) else ''}.{ctx.mem_types[var.dtype]} " + \
|
||||
f"[{ctx.r[loc]}+0], {('{' + ', '.join(ctx.r[var]) + '}') if var.max_numel() > 1 else ctx.r[var]};"),
|
||||
(UPat(Ops.LOAD, name="x", src=(UPat((Ops.INDEX, Ops.SHRINK), name="loc"), UPat.var("alt"), UPat.var("gate"))),
|
||||
lambda ctx, x, loc, alt, gate: flatten([
|
||||
[f"mov.{ctx.mem_types[x.dtype.scalar()]} {v}, {render_val(0, x.dtype.scalar())};" for v in ctx.r[x]],
|
||||
[f"@{ctx.r[gate]} ld.{mem_type(loc)}.v{x.max_numel()}.{ctx.mem_types[x.dtype.scalar()]} {{{', '.join(ctx.r[x])}}}, [{ctx.r[loc]}+0];"]
|
||||
[f"mov.{ctx.mem_types[x.dtype]} {v}, {render_val(0, x.dtype)};" for v in ctx.r[x]],
|
||||
[f"@{ctx.r[gate]} ld.{mem_type(loc)}.v{x.max_numel()}.{ctx.mem_types[x.dtype]} {{{', '.join(ctx.r[x])}}}, [{ctx.r[loc]}+0];"]
|
||||
]) if alt.max_numel() > 1 else [
|
||||
f"@{ctx.r[gate]} ld.{mem_type(loc)}.{ctx.mem_types[x.dtype.scalar()]} {ctx.r[x]}, [{ctx.r[loc]}+0];",
|
||||
f"@!{ctx.r[gate]} mov.b{ctx.types[x.dtype.scalar()][1:]} {ctx.r[x]}, {ctx.r[alt]};"]),
|
||||
f"@{ctx.r[gate]} ld.{mem_type(loc)}.{ctx.mem_types[x.dtype]} {ctx.r[x]}, [{ctx.r[loc]}+0];",
|
||||
f"@!{ctx.r[gate]} mov.b{ctx.types[x.dtype][1:]} {ctx.r[x]}, {ctx.r[alt]};"]),
|
||||
(UPat(Ops.LOAD, name="x", src=(UPat((Ops.INDEX, Ops.SHRINK), name="loc"),)),
|
||||
lambda ctx, x, loc: f"ld.{mem_type(loc)}.v{x.max_numel()}.{ctx.mem_types[x.dtype.scalar()]} {{{', '.join(ctx.r[x])}}}, [{ctx.r[loc]}+0];" \
|
||||
lambda ctx, x, loc: f"ld.{mem_type(loc)}.v{x.max_numel()}.{ctx.mem_types[x.dtype]} {{{', '.join(ctx.r[x])}}}, [{ctx.r[loc]}+0];" \
|
||||
if x.max_numel() > 1 else f"ld.{mem_type(loc)}.{ctx.mem_types[x.dtype]} {ctx.r[x]}, [{ctx.r[loc]}+0];"),
|
||||
# simple
|
||||
(UPat(Ops.BUFFER, name="x"), lambda ctx, x: [] if x.addrspace == AddrSpace.REG else [
|
||||
@@ -186,7 +186,7 @@ class PTXRenderer(Renderer):
|
||||
|
||||
name = "test"
|
||||
for u in uops:
|
||||
if u.op in {Ops.NOOP, Ops.GROUP}: continue
|
||||
if u.op in {Ops.NOOP, Ops.GROUP, Ops.CONST}: continue
|
||||
if u.op is Ops.AFTER:
|
||||
self.r[u] = self.r[u.src[0]]
|
||||
continue
|
||||
@@ -197,26 +197,26 @@ class PTXRenderer(Renderer):
|
||||
r[u] = [cast(str,r[x]) for x in u.src]
|
||||
continue
|
||||
if u.op is Ops.BUFFER and u.addrspace == AddrSpace.REG:
|
||||
r[u] = [ssa("reg", u, self.types[u.dtype.scalar()]) for _ in range(u.max_numel())]
|
||||
r[u] = [ssa("reg", u, self.types[u.dtype]) for _ in range(u.max_numel())]
|
||||
continue
|
||||
if u.op in {Ops.INDEX, Ops.SHRINK, Ops.LOAD} and u.src[0].addrspace in (AddrSpace.REG, AddrSpace.ALU):
|
||||
# on REG, INDEX/SHRINK pick the register (must be CONST) and LOAD is a noop
|
||||
if u.op is not Ops.LOAD and u.src[1].op is not Ops.CONST:
|
||||
if u.op is not Ops.LOAD and not (u.src[1].op is Ops.CAST and u.src[1].src[0].op is Ops.CONST):
|
||||
raise RuntimeError(f"PTX does not support dynamic register indexing: {u}")
|
||||
r[u] = r[u.src[0]] if u.op is Ops.LOAD else r[u.src[0]][u.src[1].val]
|
||||
r[u] = r[u.src[0]] if u.op is Ops.LOAD else r[u.src[0]][u.src[1].src[0].val]
|
||||
continue
|
||||
if u.op is Ops.SPECIAL: r[u] = "%" + u.arg
|
||||
elif u.op is Ops.LOAD:
|
||||
r[u] = [ssa('val', dtype=self.types[u.dtype.scalar()]) for _ in range(u.max_numel())] if u.max_numel() > 1 else ssa('val', u)
|
||||
r[u] = [ssa('val', dtype=self.types[u.dtype]) for _ in range(u.max_numel())] if u.max_numel() > 1 else ssa('val', u)
|
||||
elif u.op is Ops.PARAM: bufs.append((f"data{u.arg.slot}", u))
|
||||
elif u.op is Ops.WMMA:
|
||||
# registers for packing/unpacking input and acc
|
||||
self.wmma_r = [[ssa("wmma_in", dtype="b32") for _ in range(0, len(r[u.src[0]]), 4 // u.src[0].dtype.scalar().itemsize)],
|
||||
[ssa("wmma_in", dtype="b32") for _ in range(0, len(r[u.src[1]]), 4 // u.src[0].dtype.scalar().itemsize)],
|
||||
[ssa("wmma_acc", dtype="b32") for _ in range(0, len(r[u.src[2]]), 4 // u.dtype.scalar().itemsize)]]
|
||||
r[u] = [ssa("wmma", dtype=self.types[u.dtype.scalar()]) for _ in range(u.max_numel())]
|
||||
self.wmma_r = [[ssa("wmma_in", dtype="b32") for _ in range(0, len(r[u.src[0]]), 4 // u.src[0].dtype.itemsize)],
|
||||
[ssa("wmma_in", dtype="b32") for _ in range(0, len(r[u.src[1]]), 4 // u.src[0].dtype.itemsize)],
|
||||
[ssa("wmma_acc", dtype="b32") for _ in range(0, len(r[u.src[2]]), 4 // u.dtype.itemsize)]]
|
||||
r[u] = [ssa("wmma", dtype=self.types[u.dtype]) for _ in range(u.max_numel())]
|
||||
prefix, dtype = {Ops.CAST: ("cast", None), Ops.BITCAST: ("cast", None), Ops.END: ("pred", "pred"), Ops.RANGE: ("ridx", None),
|
||||
Ops.CONST: ("const", None), Ops.BUFFER: ("local", "u64"), Ops.INDEX: ("bidx", "u64"), Ops.SHRINK: ("bidx", "u64"),
|
||||
Ops.BUFFER: ("local", "u64"), Ops.INDEX: ("bidx", "u64"), Ops.SHRINK: ("bidx", "u64"),
|
||||
Ops.PARAM: ("dat", "u64" if u.addrspace is AddrSpace.GLOBAL else None), **{op: ("alu", None) for op in GroupOp.ALU}}.get(u.op, (None, None))
|
||||
if u.op is Ops.RANGE and u.dtype == dtypes.void: prefix = None # loop headers don't have a register
|
||||
if prefix: r[u] = ssa(prefix, u, dtype)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from tinygrad.dtype import DType, dtypes, truncate, AddrSpace
|
||||
from tinygrad.uop.ops import UOp, Ops, PatternMatcher, UPat
|
||||
from tinygrad.renderer.cstyle import CStyleLanguage, base_rewrite
|
||||
from tinygrad.helpers import strip_parens
|
||||
from tinygrad.helpers import strip_parens, ceildiv
|
||||
|
||||
def _mask(dt:DType): return 0xFF if dt.itemsize == 1 else 0xFFFF
|
||||
|
||||
@@ -34,7 +34,7 @@ def is_packed(x:UOp):
|
||||
elif x.op is Ops.STORE: dt, addrspace = x.src[1].dtype, x.src[0].addrspace
|
||||
else: dt, addrspace = x.dtype, x.addrspace
|
||||
return dt.itemsize < 4 and dt != dtypes.half and addrspace != AddrSpace.REG
|
||||
def _packed_size(u:UOp): return u.max_numel() // (4//u.dtype.itemsize) if is_packed(u) else u.max_numel()
|
||||
def _packed_size(u:UOp): return ceildiv(u.max_numel(), 4//u.dtype.itemsize) if is_packed(u) else u.max_numel()
|
||||
def is_nan(a):
|
||||
bs, (exp, mant) = a.dtype.bitsize, dtypes.finfo(a.dtype)
|
||||
return (a.bitcast(getattr(dtypes, f"uint{bs}")) & ((1 << (bs - 1)) - 1)) > (((1 << exp) - 1) << mant)
|
||||
@@ -69,10 +69,10 @@ class WGSLRenderer(CStyleLanguage):
|
||||
|
||||
string_rewrite = PatternMatcher([
|
||||
(UPat(Ops.NEG, dtypes.uints, src=(UPat.var('x'))), lambda ctx,x: f"(0-{ctx[x]})"),
|
||||
(UPat.cvar("x", dtype=dtypes.bool), lambda x: "true" if x.val else "false"),
|
||||
(UPat(Ops.CONST, dtype=(dtypes.uchar, dtypes.ushort, dtypes.uint32), name="x"),
|
||||
lambda x: f"bitcast<u32>({x.val})" if x.val < 0 else f"{x.val&0xFFFFFFFF}u"),
|
||||
(UPat(Ops.CONST, dtype=dtypes.int32, name="x"), lambda ctx,x: f"{truncate[x.dtype](x.val)}"),
|
||||
(UPat.cvar("c").cast(dtypes.bool), lambda c: "true" if c.val else "false"),
|
||||
(UPat.cvar("c").cast((dtypes.uchar, dtypes.ushort, dtypes.uint32)),
|
||||
lambda c: f"bitcast<u32>({c.val})" if c.val < 0 else f"{c.val&0xFFFFFFFF}u"),
|
||||
(UPat.cvar("c").cast(dtypes.int32, name="x"), lambda ctx,x,c: f"{truncate[x.dtype](c.val)}"),
|
||||
(UPat(Ops.BUFFER, name="x"), lambda ctx,x:
|
||||
f"var{'<workgroup>' if x.addrspace == AddrSpace.LOCAL else ''} {ctx[x]}: array<{ctx.buf_map(x)},{_packed_size(x)}>;"),
|
||||
(UPat(Ops.BITCAST, dtype=dtypes.half, name="x", src=(UPat(dtype=(dtypes.short, dtypes.ushort, dtypes.uint32),),)),
|
||||
|
||||
@@ -8,7 +8,7 @@ am_src="https://github.com/ROCm/ROCK-Kernel-Driver/archive/33970e1351f5e51102960
|
||||
rocm_src="https://github.com/ROCm/rocm-systems/archive/cccc350dc620e61ae2554978b62ab3532dc10bd9.tar.gz"
|
||||
AMD, AMDINC = "{}/drivers/gpu/drm/amd", "{}/drivers/gpu/drm/amd/include"
|
||||
inc, kern_rules = ["-include", "stdint.h"], [(r'le32_to_cpu', ''),]
|
||||
fw_src="https://gitlab.com/kernel-firmware/linux-firmware/-/archive/1e2c15348485939baf1b6d1f5a7a3b799d80703d/1e2c15348485939baf1b6d1f5a7a3b799d80703d.tar.gz"
|
||||
fw_src="https://gitlab.com/kernel-firmware/linux-firmware/-/archive/0a6871b19abf5d6e024b5d208b101ae53e7fa0de/0a6871b19abf5d6e024b5d208b101ae53e7fa0de.tar.gz"
|
||||
pmc_src="https://raw.githubusercontent.com/ROCm/rocm-systems/cccc350dc620e61ae2554978b62ab3532dc10bd9/projects/rocprofiler-compute/src/rocprof_compute_soc/profile_configs/counter_defs.yaml"
|
||||
|
||||
reg_files = {
|
||||
|
||||
@@ -1,81 +1,82 @@
|
||||
hashes = {
|
||||
'psp_13_0_0_sos.bin': 'b5592f46885585b935e013f46c949db8ff2f15c0b346caf70e7fcd2776623d13',
|
||||
'psp_13_0_0_sos.bin': '4a51299f6d0a15bbba9694419f7891e6accc01dbd2dd67c06add7bfd75a45ac6',
|
||||
'psp_13_0_10_sos.bin': '0bcaaad9cd8578d3841ae69155a6bd4fc3ceae8f4fb5a6ba4f576e7ace94d1d9',
|
||||
'psp_13_0_12_sos.bin': '89da90bf4286b38678b1fd175c78462a426afa3d258d15872cd14072d7098b9b',
|
||||
'psp_13_0_14_sos.bin': 'a4f0d5f76d27b77409ec0b71d7cc6a848ddfd29f8c84f3003edf74ad3999fb7d',
|
||||
'psp_13_0_6_sos.bin': '27657daa0f91ad8095d3610224a7de748b8b348a4cb211ecb5fccabe47369716',
|
||||
'psp_13_0_7_sos.bin': 'ef1af0ecea38abbac6f85cce71789f19848c498d0cb8ef13748dab2d65b23c31',
|
||||
'psp_13_0_12_sos.bin': '7113a165c75c232d4cb7193a920b503e0bf082689adde3b45fdc38f58bfd18b3',
|
||||
'psp_13_0_14_sos.bin': 'db863768cb25e806b68033e9237e0869f9f3603119df4d369ff4d80418d585d0',
|
||||
'psp_13_0_15_sos.bin': '3b28d53e75a88131155e3931378ac8434eca4880ada9211d3b4e8915b6289583',
|
||||
'psp_13_0_6_sos.bin': '36cce3a9441a0dcde81badd8fcf0416de8e4c39a7707865eff4d9d75e6bb0466',
|
||||
'psp_13_0_7_sos.bin': '94db505fa6482f258c33a0a8d412050f6d843ab4ada368252e988f82f8a26fa8',
|
||||
'psp_14_0_2_sos.bin': '7b538448b57d4f9dd06b2eea90d4f86a16e65e3027cdecee8db71c2c5f1fa243',
|
||||
'psp_14_0_3_sos.bin': '23bea01a0c6f36d00759d0765d46cb4cb4aa87398b2fbccacbf547a890c0bf51',
|
||||
'smu_13_0_0.bin': '2ffac37fd8534965eeba19755db0e5ec80278213487dc4af0fbc8453befb64b1',
|
||||
'smu_13_0_0_kicker.bin': '7f83656a2a89b7fce1c8a85e96d91cd8265a91fe883a7027f1a0ed18ced501de',
|
||||
'smu_13_0_10.bin': 'daedb9cbdf48942be7ffe00d31b7c16bb36e11ff5a9d7495f218e95c07717b71',
|
||||
'psp_14_0_3_sos.bin': '28469a0857c813c54a0492423cdf0b0caf757428400036377e19c47e5af62478',
|
||||
'smu_13_0_0.bin': '93e46a5526f19dcc3d13bfd9e23f88bc8eee52138bfe9caf0951b4eef5e49914',
|
||||
'smu_13_0_0_kicker.bin': 'd0ef51d9ed06d0c17e06667302be21e7aedd86ed7a72be6e2f55b102214131cc',
|
||||
'smu_13_0_10.bin': '9376ae64149e6b0b684898ffbc12c2230f8c50a2e9447dc7dafc95c0c16b5227',
|
||||
'smu_13_0_14.bin': 'a4f36de75fdcecd8000246762e027b4be489b6787afea57675225b0b39d35625',
|
||||
'smu_13_0_6.bin': 'ad7232264e8c57c2094244fbdd5a55d7a4575ffe9b44d229884bc0b6a44fb0b1',
|
||||
'smu_13_0_7.bin': 'ccecc0fd0196b9613c920a51c2fd9436e739ff19dda5bdf74d97562387231732',
|
||||
'smu_14_0_2.bin': '6951995d1d606f4dc60c895f19d34ed18aa40e62129f83d8510c45e8aa9ae2fc',
|
||||
'smu_14_0_3.bin': 'df230947ddb7bbfd6e77d1280001db886e69adf2b2a448b47fa668a48bc0009f',
|
||||
'smu_14_0_3_kicker.bin': '8ddc1da5b4e1619796c2cc81f19f388a35bf7d78bfe476cee559625589cb4dc7',
|
||||
'sdma_4_4_2.bin': '456061b814268425843537da6f2191c8861d4e1a18d4c5d90c44ea6be18c78ff',
|
||||
'sdma_4_4_4.bin': 'af47a2940e72b932d3e3a7e8f34f7a182624e5e433f7c56dff939ca5549cd33a',
|
||||
'sdma_4_4_5.bin': '6127baabea3de7b18db3868c983b02c0fbf2cd75997f7f11241a5b1be27e5134',
|
||||
'smu_13_0_7.bin': '68ec18bd605e680085c927ff72c609f8c771aff0718d0cfab58a3744dff8e5b7',
|
||||
'smu_14_0_2.bin': '1b2054e3f710d1ab8dbdf6ff35914ad376b51caa6337831260c955add874b2ee',
|
||||
'smu_14_0_3.bin': '4e1522d3c96c1028be2961dfcfc5f1ff783fb77724b260a99c4c8b4a901ef3fe',
|
||||
'smu_14_0_3_kicker.bin': '9ff142656ae5f57be1b5ecc134e9da8f76650e793fbc5c499acd75094ff24453',
|
||||
'sdma_4_4_2.bin': 'ff885711d2d5d75ceed51cf239e93c882584b918cd5d5d1ff58ee5aecc0c50ae',
|
||||
'sdma_4_4_4.bin': '06a9d4d02c187844313a78469321d6091e59a334f0ce3b61d770d810c984d70b',
|
||||
'sdma_4_4_5.bin': 'e2a30faa9403933fbfba7ce8e9feba460fff6ecdb15304818d24c9f3eeaad0a6',
|
||||
'sdma_5_2_6.bin': '3a163db00eb7e4752be8adbd61cf7dd8f08d924e59a6f798ced7dfcd89f340ed',
|
||||
'sdma_5_2_7.bin': '16fe80dc866b323e15a06f51646ef0f036878ad34da66921fcdb8167207d6b2b',
|
||||
'sdma_6_0_0.bin': '0f3da6b211f376356335b41be07149f650c10cfa4e23f7e25d53836006ed11f5',
|
||||
'sdma_6_0_1.bin': 'ff565d3c215a30737560d4e3df6fc2c637738407e91d212fb200fdfb185b6744',
|
||||
'sdma_6_0_2.bin': '398380184bb69113ef4c8964a3b55f6184deb0c1ffd96c9683490a3eec3ba8f3',
|
||||
'sdma_6_0_3.bin': '0e8a83513087db865ba926f8b65cfb003fd41098f707e178d7a7ae2941fed0b1',
|
||||
'sdma_6_1_0.bin': '22e55d0ad5f0247a7f0fffc67cfd3161b39f24ad6062ff3c91ec7ff38bd7e1e1',
|
||||
'sdma_6_1_1.bin': '74533a581b8e3e2743b3c9c803d0666405e80898c4a630acefed82cb6b516ba2',
|
||||
'sdma_6_1_2.bin': '4fe04b0286ec739b0414e8aee17e62e85e691f0246d1d9b56bc18a1219072314',
|
||||
'sdma_6_1_3.bin': '35c9ed7e3a237c0d4a83b4975c63b62488f72aeafbb648342f384618e103f66b',
|
||||
'sdma_6_0_0.bin': '82cd01a24171af12de6c7ac4ee7471aa2dfcf51f9677e7bae9cd4c75e07761ee',
|
||||
'sdma_6_0_1.bin': '708c2c2c45262c98ebe8e34e32c3f1ce8eb5b007bab560c9ea9b576a8e4d6768',
|
||||
'sdma_6_0_2.bin': '16c374344d2894da751f7028f9ec1f7520035fa9548d8c750d99a00a4afa86c7',
|
||||
'sdma_6_0_3.bin': 'd47ac4db523aa35d77b27d57c35d4c04f431229dec6c0d667c36d98b985a6933',
|
||||
'sdma_6_1_0.bin': '85f9f3714de68eee74cdf4852d709bc36a5c73a00e943b707bc2ce10d6b7bab4',
|
||||
'sdma_6_1_1.bin': 'e7b7a23923ab691665e6ad16bbc8431a92f7c049da4b0b19a82c45fba03d4979',
|
||||
'sdma_6_1_2.bin': '5947d78eb308a3f6a62d772c5a6493b21439c73eac139f9c22f080f660b4f4f3',
|
||||
'sdma_6_1_3.bin': '8c651f32cbf030b6239ecc44f01bc9f5d5a193f333e21f2103736aff33227361',
|
||||
'sdma_7_0_0.bin': 'beaafb53993a106edd392392d5896245ae2a957c6d0f495d0002eec72ad8ad38',
|
||||
'sdma_7_0_1.bin': '73c29e1c1714ebc95d2221ba56e187910902891593010653bf9518937e414a59',
|
||||
'gc_10_3_6_pfp.bin': '793d678427887a0e724c79e356440aec33e6d1301f2a4e63543500249ebec064',
|
||||
'gc_10_3_6_pfp.bin': '042f5d2d223aac6a62b500a47d0d0bf33984200110da0ffca4fe5df9a96571c0',
|
||||
'gc_10_3_7_pfp.bin': '3ae29aac3f424f7de97f82ce7158beba69509afb2dcbf1a428dc315df474a524',
|
||||
'gc_11_0_0_pfp.bin': 'e175cb0f580a38c961a6f7366142c08e413995f57f78f39795368b15442df8a3',
|
||||
'gc_11_0_1_pfp.bin': 'f5bf21dfbd9e72a30b4caf4704282c27854710c1b7c4affbb2a19530466b12a8',
|
||||
'gc_11_0_2_pfp.bin': '001c4dec1119e29314d725cc1280fc4f0cd9cabdf61ea5ee2260cfd4e62ec141',
|
||||
'gc_11_0_3_pfp.bin': '0488034c85be97125e39e860308d33c3f76a01df8250092a32d4d55acb2526fd',
|
||||
'gc_11_0_4_pfp.bin': '5ae8b7bb6316f87ae8b978354c088e3bd8c890959382d72886377cda25b1ffd1',
|
||||
'gc_11_5_0_pfp.bin': '0124f540871a7759fa8aaae046d458dfb34aeea12a1183ff962c3f1a33067d5a',
|
||||
'gc_11_5_1_pfp.bin': '7794ea46d0d3cf9cb3f7938affbdf09dd7a9970340da5cd02b774cb393436d24',
|
||||
'gc_11_5_2_pfp.bin': '55e64741de28c506524959f7f696713a72aafe46f49ccd827781d67a9475b386',
|
||||
'gc_11_5_3_pfp.bin': 'ce805040fb347fddbc89b2715e66b446865dda9e2056a9b233269b72bc09c387',
|
||||
'gc_12_0_0_pfp.bin': '16bfd64c10fe73b5e760055069a60e5841dba16c0ed4edb56c20d675e23901f6',
|
||||
'gc_12_0_1_pfp.bin': '49efb319305c5fffd90ac1eef7d7a0bdec72998ecb5cf4526996311788a53dc3',
|
||||
'gc_10_3_6_me.bin': '141b59faad3f2f1be16a2178833b7ca8e97519e1e844c8fda6689572c3767902',
|
||||
'gc_11_0_0_pfp.bin': 'b360393c8629144b194f69a3cd961ed509331feff7a5cc1e4eb21c901da2710a',
|
||||
'gc_11_0_1_pfp.bin': 'fb1ee527c05c55679c80a8bcf60fbb533724891baeb0eabc2917fc44e63a45dc',
|
||||
'gc_11_0_2_pfp.bin': '9020f53788ad881fa01aa656fc082f9f8d3cdfc81f70aaac0bed6e6001491128',
|
||||
'gc_11_0_3_pfp.bin': '362db904fa16c1fea2af7ad1295532434df7f85662b4a69332f51ae6c7290b61',
|
||||
'gc_11_0_4_pfp.bin': 'aad22ca342c47d857bc1107a9aa9127e5e4ba7f7fd42d432213b1850bda1f4e1',
|
||||
'gc_11_5_0_pfp.bin': '82ccf0265d841351183b011a79422799431f0c11f6d11165d64d7dfe404bda31',
|
||||
'gc_11_5_1_pfp.bin': '633404d8db1dc03fe997f7d0d0e15ef908069727abaf9de55841be3f3c97348b',
|
||||
'gc_11_5_2_pfp.bin': 'baee1456dd1800cdaedd4998c2dd7d76cdc0cf0ec928679fe67b02485905ea2c',
|
||||
'gc_11_5_3_pfp.bin': 'fee840b049b5e082215df72a93fad80a64f07ef6f638408a2d56fae97449a2cb',
|
||||
'gc_12_0_0_pfp.bin': 'd1b043c60920e509e5c8f9677221fb78ff7985f68b605e8f39a04a57333a9366',
|
||||
'gc_12_0_1_pfp.bin': '9d8d6188efeca5ef05482d9299c4f102fab7db3dae51a23e59de9baa34997123',
|
||||
'gc_10_3_6_me.bin': '776d2299bc4f3abffd4a7999f5a21a4e38aced8b6b4c199a83610dbabf08176d',
|
||||
'gc_10_3_7_me.bin': '9eb0b56e9bcc9dad5d53437b162226fcb37e5df102832260f1232832f3658edf',
|
||||
'gc_11_0_0_me.bin': 'f8fba8a63dd4293b8fc1e4aab78b6fac630e575d1d62838c7996d9210f82aea1',
|
||||
'gc_11_0_1_me.bin': '5030040b00955de94876341ec64ea43b96640413d7a03dc460a83c8386bf76e0',
|
||||
'gc_11_0_2_me.bin': '0f21fd43f1dfbc6ccced9a2b3774de25c993c61a689aabab8b45333937b7945e',
|
||||
'gc_11_0_3_me.bin': '3acb5061dba342ade81d329d1932f19ec01f0c5bf44e6e3568008a951a351bac',
|
||||
'gc_11_0_4_me.bin': 'e4f1f6abcd213d54ad9e885d9f550083b0e2f67d983566015e8a53981e1cb155',
|
||||
'gc_11_5_0_me.bin': '8f906b64d0a29503daa662c93ec44d076fcac11b78f70cd50ce0af2b500a05a6',
|
||||
'gc_11_5_1_me.bin': '7e42602bcbaf1e511f8b4f6ed2246844ad1f6e351ce2b663d89062a7be263663',
|
||||
'gc_11_5_2_me.bin': 'aae26255d8efff81e0e3bbcb727efb8b837d8e25fe85c708545f5328f1077b50',
|
||||
'gc_11_5_3_me.bin': '93cd588348b16fe432609fe8da6e6b5da0a52da5c5884882aecf7b1001f72700',
|
||||
'gc_12_0_0_me.bin': 'd7eba5197f2580f32b8256b1d9cb68e723e9e644293a34446a7913e3c093cba5',
|
||||
'gc_12_0_1_me.bin': '365e7f193b39cbb10d3af44905fefaca0e9844721801755276baebac7b19c1ea',
|
||||
'gc_10_3_6_mec.bin': '247943415658159704a21f670dd7b3e7cb2d2fc0c17b000a5098715979c8d95e',
|
||||
'gc_11_0_0_me.bin': 'f2f5a793d811c6abad1a18af0fcf7694c443478f224176da86650c22aa71ca7a',
|
||||
'gc_11_0_1_me.bin': '476db2ec7e33d1e126b1736649208443e3ccc68aa60e4978574cdbced2b26543',
|
||||
'gc_11_0_2_me.bin': 'f5fe48f97acbd3ce13b35929290bfbac01ce522631cc91dcef1fdeb3ff35c8ed',
|
||||
'gc_11_0_3_me.bin': 'd02c25070e5bdf0ec0146f5c9d6d2f8b86de43bd2a318a0b67eb5201963bafdc',
|
||||
'gc_11_0_4_me.bin': 'f075220f75ffe43eacc5986ff8448946c27405e632764fda83323e7ec8d55566',
|
||||
'gc_11_5_0_me.bin': '338019a1fcdab39729e3f492ffc9f5970c2c81b12c8a4f431494ca28cfdadedf',
|
||||
'gc_11_5_1_me.bin': '4c4dd30c22d4f7f2c5d3a19c645f505e30cdac115a91c65791e2651b22932175',
|
||||
'gc_11_5_2_me.bin': 'cab2999186d26c0e9a3d46b5a43d2854d88be880cb764c096ad2b43038566384',
|
||||
'gc_11_5_3_me.bin': '94e2d74e834725b3d51e03e830160e95c56f3a31e93f5d61c852afd8fe8cc779',
|
||||
'gc_12_0_0_me.bin': 'fb10cb3535ae4a6a8fb3e78166cf30c5b717341b1f20cde73065c62b642adfed',
|
||||
'gc_12_0_1_me.bin': '56a1ae0031aa938f6b61348a56404ab2cee92f1f45630fc82a801aa4d908f98a',
|
||||
'gc_10_3_6_mec.bin': '7003c4a77537e9edaf67064104cd9371fac38a84f71f948349140b28d3c210e8',
|
||||
'gc_10_3_7_mec.bin': 'ee58a523375bcf5b89400b32b801f95e182b632a26bce4f2bed5c07928d486dc',
|
||||
'gc_11_0_0_mec.bin': '801a09c9bf06188260db9b51ad8f978f15d84c72ca91b90643a2ef8af4074776',
|
||||
'gc_11_0_1_mec.bin': '6afadcb7504bb11bcc9d4a205cdf73f7934a615e28f178fcf7285971df2ccd05',
|
||||
'gc_11_0_2_mec.bin': '0da0edee28c73a6fa1191f77853d380ec2503cbf43e0aaae4617f32f1f8a48fa',
|
||||
'gc_11_0_3_mec.bin': '323cfa6658b6b5169830f852e2ff0552acae8dfb9e44b42c63de7b2900d3fd9e',
|
||||
'gc_11_0_4_mec.bin': '5d89cf6b60354f3746c2cbd1ff0cb1a741556ca20d72745242cb69b553d0985c',
|
||||
'gc_11_5_0_mec.bin': 'a01c324ab14ec89792449a621a541829b9af26865019027a411a14b910145dfa',
|
||||
'gc_11_5_1_mec.bin': 'eab05719371caa68df09d4f7574e3958a3c4f5044ab3c7b0d2b214add0c6d1c4',
|
||||
'gc_11_5_2_mec.bin': 'a374b2335802e24f8b9a3ce40000a1d37a52a14eb87099bebcc6680c27cc93e5',
|
||||
'gc_11_5_3_mec.bin': '165025437cba80dd32c19ebbc83b756fa7adac7053ff7780ba4aa2f8089c6a3f',
|
||||
'gc_12_0_0_mec.bin': '1931593440b8f9423580d9e2cdc5b34e7c682cdffe1ca4b74b0c2f6a0420236d',
|
||||
'gc_12_0_1_mec.bin': 'f57541688a5108730bf210663f1137ffc2121f3acfe614a6de09ec1982c69a2f',
|
||||
'gc_9_4_3_mec.bin': '3159176e72301fb88dc416721fb3d0ab82ece484cf93a43c3f37430c7e6673a1',
|
||||
'gc_9_4_3_sjt_mec.bin': 'd19468dbb47849640bd0e6cdc8d7e25a3c8442c7ca2ca81357702e0d6baab50f',
|
||||
'gc_9_4_4_mec.bin': '5004f73e43db2dd45e77d65942e33d4a69e7157618cfd23944c30f801c77a0f3',
|
||||
'gc_9_4_4_sjt_mec.bin': '627a9e98102e70fe3bf0947eb764187f29f5e775d1130c7310e0ba5fc0502dbe',
|
||||
'gc_9_5_0_mec.bin': 'c5eca4311a6f6e8f81cf41c2c46941d5dcf90789ee8326901da2dfc86ac14c31',
|
||||
'gc_9_5_0_sjt_mec.bin': 'f162e509379288e3f3b1eead541b315c2262d625d433287ecd34ca185614d312',
|
||||
'gc_11_0_0_mec.bin': '1dd1de8ecf5455ea4719c502b64b32ac18763d5601128c01b4a4a36211a122c2',
|
||||
'gc_11_0_1_mec.bin': '505ae64eccb2e4b4751fe18ec1b584e1f6b4c81d0f5ec089afbcf378cad59711',
|
||||
'gc_11_0_2_mec.bin': '19bf080d6e672de5ed3fb86e3fdbdda4d700d8e3bda2dbdcc923101484ad645b',
|
||||
'gc_11_0_3_mec.bin': 'a37bc1a4e245300a5c3e26da34ea213842447d7df6c5c81e9fc78887a2fde26f',
|
||||
'gc_11_0_4_mec.bin': '850d5302b4fee6022f42f706c2de103531b45b7794a45f2d6dce6015767a1ad6',
|
||||
'gc_11_5_0_mec.bin': '5e022bae6638967d82e2b1077e3024f52bc83b3cb850aa31fba51469c7517c4c',
|
||||
'gc_11_5_1_mec.bin': 'e49964d5e58686c53e66d98d4e3b9fab70e98fad3b28379c6e60aed03c83ee80',
|
||||
'gc_11_5_2_mec.bin': '9691d7bff5d2c933d8eecb7d171635612a76a2dd1441cffcd65a8a02bdb5a2c5',
|
||||
'gc_11_5_3_mec.bin': 'd368f3886b9245dd0d21d57fccfd8aa7e872c2564e23f292abe735348121277e',
|
||||
'gc_12_0_0_mec.bin': '9c7602d6ebf1f7e6ec7a5d1ceefded18f35fa1c08fbea1e3e1a0d78d519db8e8',
|
||||
'gc_12_0_1_mec.bin': 'caf1dbaf72b0ef0c4c973947414033aeec002994f63967bb53e9165195a3c2c3',
|
||||
'gc_9_4_3_mec.bin': '99bc12230f00b930cf286105a35cc6110d87461cd48cb4fdf3cb6caff73ac1e7',
|
||||
'gc_9_4_3_sjt_mec.bin': '2945dbd098c4158870df7dc4ccb33d40031fd1cce37cdbe5df291d8941d03567',
|
||||
'gc_9_4_4_mec.bin': '7f14258f8301d2717e0a707ccfad7b3091af478b0df6d5134adfd56caa7429d8',
|
||||
'gc_9_4_4_sjt_mec.bin': '0bbef279bbc07c502098b80765b876f69fcda9834e5ed269a7d8236c85e89e19',
|
||||
'gc_9_5_0_mec.bin': '0c39078c53e10e99538901df5fc14e7f1b1f3639ea825b1b3126ae87a28b2464',
|
||||
'gc_9_5_0_sjt_mec.bin': 'a769745367567fc6f389695aa5f48c154c07560e21a93052185e19f950205240',
|
||||
'gc_11_0_0_imu.bin': 'b4f8fc056b45709a6abf48e7885fb1b4ab8d3cc092cbfa2c554a78564a6403bc',
|
||||
'gc_11_0_1_imu.bin': 'ac71f4eec713fc35b4a1fe27531e3eb04edd81eeac2cef64df01ac50d8510805',
|
||||
'gc_11_0_2_imu.bin': '9befca62b0b0cfd252c3df4a9edca295526f4d43821cd99a6326454995a6ca2d',
|
||||
@@ -90,17 +91,17 @@ hashes = {
|
||||
'gc_10_3_6_rlc.bin': 'acfbac75c0dcfbfe40e222640ef17eb3dc8d206d30bc3863f275f2dd1cb132a5',
|
||||
'gc_10_3_7_rlc.bin': 'a02585ebe3b36d942e883057119572d9497600c52fc65b8a523487eb65d874f2',
|
||||
'gc_11_0_0_rlc.bin': 'dabd49039772d02f5fd5e48dc21d35ad52a6b1283b470dabca86ca159c4c7c8e',
|
||||
'gc_11_0_1_rlc.bin': '86145719a58e9428562930c6b5ee3b6ced4701d34a80d0b4d84d6026c93134f2',
|
||||
'gc_11_0_1_rlc.bin': '5f07dc1f0a75ecd9cb56d805ea869184a50ed9e43d811ebf833b8906534650ef',
|
||||
'gc_11_0_2_rlc.bin': 'b43eb2fd0600f50a1a5796bc9983d6b39b5c20960234920f5e89cb362193e0b8',
|
||||
'gc_11_0_3_rlc.bin': '29b0b456f5b53076ddffa6f09de3bb697219e8e7b33504bf6c197e8b858426dc',
|
||||
'gc_11_0_4_rlc.bin': '823573078b608108fbe4dd8176c396ec582632913db9c59a512d82b068f8eba0',
|
||||
'gc_11_5_0_rlc.bin': '68cd85567f4f2f8d6b80db294988806d956bf826979c3597daccb71c7ee6aadd',
|
||||
'gc_11_0_3_rlc.bin': '890d8e0123efb40c0179dd8ac3e9af073a0b87cbbccfec1db54e5ed2315a8d39',
|
||||
'gc_11_0_4_rlc.bin': '257ced82d7bec41249b06592ee0c44fb8f9262de2c6af9c52dc6f6a8a702063e',
|
||||
'gc_11_5_0_rlc.bin': '0dc8b6ef5530a4a53938c8baa0d49cd458607d95233237859fa98d44feb3e985',
|
||||
'gc_11_5_1_rlc.bin': '92731ecabbeb77865fb71787b4268dc738a58779f1190bdc2056482cb88a08f6',
|
||||
'gc_11_5_2_rlc.bin': 'ef3a9209d3eccfbe18fce9e972c146ac283719798bb788096c176b796dc9aee5',
|
||||
'gc_11_5_2_rlc.bin': 'c9ad70b8ac309257cb8929bb6b4efa6b551ec1e5229d7a419332a9797f31fc9e',
|
||||
'gc_11_5_3_rlc.bin': '10a68940c6258d5818d9c05fd98eb0ccc8d5aee99b2769fbad30e5abd0d9327e',
|
||||
'gc_12_0_0_rlc.bin': '6436b582734a413456fff3d3c7195e71cc9e78a7ed31ee21c83ffd6fae1ad186',
|
||||
'gc_12_0_1_rlc.bin': '6ba4459532246a5c415d3cb33c9b1248294e48f67b827e2accb292a8d1a5c0ec',
|
||||
'gc_9_4_3_rlc.bin': '5345d388712d547b0ae16f199ad5ccadb65643584b3efa7817049ddeb3fdcd12',
|
||||
'gc_9_4_3_rlc.bin': '54cbd0de3a0ec35d2e58e992babeee2a237f870ccdf37e734652e4daeeba59d5',
|
||||
'gc_9_4_4_rlc.bin': 'e0c3585c72f8136670ca63e607fba32c1ae4948f493f13e33fc4d466bd6318a8',
|
||||
'gc_9_5_0_rlc.bin': '9b1268f5751153fe57f527c9acb417bfa53ed42c9bc083c9d3da2ba61fe5fdc4',
|
||||
}
|
||||
@@ -139,7 +139,8 @@ class HCQGraph(MultiGraphRunner):
|
||||
prof_ji_desc = runtime.name if runtime is not None else TracingKey(f"{bufs[1].device} -> {bufs[0].device}", ret=bufs[0].nbytes)
|
||||
|
||||
prof_name = enqueue_dev.device if runtime is not None else f"{enqueue_dev.device}:SDMA:{queue_idx}"
|
||||
self.prof_graph_entries.append(ProfileGraphEntry(prof_name, prof_ji_desc, sig_st, j * 2 + 1))
|
||||
self.prof_graph_entries.append(ProfileGraphEntry(prof_name, prof_ji_desc, sig_st, j * 2 + 1,
|
||||
runtime.profile_key if runtime is not None else None))
|
||||
self.prof_graph_deps.append([d - 1 for _, d in rdeps])
|
||||
|
||||
self.last_j[enqueue_queue] = j
|
||||
|
||||
@@ -102,7 +102,7 @@ class MetalGraph(GraphRunner):
|
||||
def collect_timestamps(self):
|
||||
# create a graph event and evenly space each program
|
||||
st, en = decimal.Decimal(self.command_buffer.GPUStartTime()) * 1000000, decimal.Decimal(self.command_buffer.GPUEndTime()) * 1000000
|
||||
ents = [ProfileGraphEntry(self.device, rt.name, i, i+1) for i, rt in enumerate(self.runtimes) if rt is not None]
|
||||
ents = [ProfileGraphEntry(self.device, rt.name, i, i+1, rt.profile_key) for i, rt in enumerate(self.runtimes) if rt is not None]
|
||||
self.dev.profile_events += [ProfileGraphEvent(ents, [], [st + (en-st)/len(ents)*i for i in range(len(ents)+1)])]
|
||||
|
||||
def __del__(self):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
from typing import cast
|
||||
import os, ctypes, struct, hashlib, functools, importlib, mmap, errno, array, contextlib, sys, weakref, itertools, collections, atexit
|
||||
import os, ctypes, struct, hashlib, functools, importlib, mmap, errno, array, contextlib, sys, weakref, itertools, collections, atexit, time
|
||||
assert sys.platform != 'win32'
|
||||
from dataclasses import dataclass
|
||||
from tinygrad.runtime.support.hcq import HCQCompiled, HCQAllocator, HCQBuffer, HWQueue, CLikeArgsState, HCQSignal, HCQProgram, FileIOInterface
|
||||
@@ -649,6 +649,56 @@ class AMDAllocator(HCQAllocator['AMDDevice']):
|
||||
|
||||
def _do_map(self, buf:HCQBuffer): return self.dev.iface.map(buf._base if buf._base is not None else buf)
|
||||
|
||||
def _copyin(self, dest:HCQBuffer, src:memoryview):
|
||||
if not self.dev.is_usb(): return super()._copyin(dest, src)
|
||||
from tinygrad.runtime.support.usb import alloc_cbuffer
|
||||
# Pipelined copyin over the 0xF2 engine. 240KB chunks stream into two alternating 256KB SRAM bounce windows; the
|
||||
# engine can't signal data landing, so each chunk ends in a 512B sentinel sector tagged with its sequence number.
|
||||
# A prebuilt SDMA ring polls each chunk's sentinel before copying it to VRAM, then bumps a drain fence; the host
|
||||
# waits on that fence before re-arming a window. No timing is assumed in either direction.
|
||||
dev, usb, ts, sdma = self.dev, self.dev.iface.pci_dev.usb, self.dev.timeline_signal, self.dev.sdma
|
||||
CHUNK, src_mv = 0x3C000, src.cast('B') # 15 16KB slots: the wire image must end mid-window (full windows corrupt)
|
||||
nchunks = ceildiv(src.nbytes, CHUNK)
|
||||
FENCE = 0xA800 # drain fence: the GPU writes it via sys_buf (PCIe 0x820800), the host reads it here (xdata)
|
||||
if not hasattr(self, '_usb_seq'): # one-time: clear the fence and zero both windows so garbage can't match a sentinel
|
||||
self._usb_seq, self._usb_stage = 0, [alloc_cbuffer(0x40000) for _ in range(2)] # (backing array, memoryview) pairs
|
||||
self._usb_wins = (self.b[0].offset(0, 0x40000), self.b[0].offset(0x40000, 0x40000)) # two windows, engine slots 0/16
|
||||
usb.write(FENCE, bytes(8))
|
||||
for bi in range(2): usb.scsi_write(bytes(0x40000), slot_start=bi * 16)
|
||||
|
||||
def wait_drain(count): # spin until the drain fence reaches count, i.e. chunks 0..count-1 are fully in VRAM
|
||||
t0 = time.monotonic()
|
||||
while int.from_bytes(usb.read(FENCE, 8), 'little') < count:
|
||||
if time.monotonic() - t0 > 10: raise RuntimeError(f"GPU failed to drain USB copyin chunk {count - 1} (10s, hung GPU?)")
|
||||
|
||||
# build the whole ring upfront: per chunk, poll the sentinel, copy SRAM->VRAM, bump the fence; then one doorbell
|
||||
POLL_EQ = sdma.SDMA_OP_POLL_REGMEM | sdma.SDMA_PKT_POLL_REGMEM_HEADER_FUNC(3) | sdma.SDMA_PKT_POLL_REGMEM_HEADER_MEM_POLL(1)
|
||||
POLL_DW5 = sdma.SDMA_PKT_POLL_REGMEM_DW5_INTERVAL(0x04) | sdma.SDMA_PKT_POLL_REGMEM_DW5_RETRY_COUNT(0xfff)
|
||||
q = dev.hw_copy_queue_t().wait(ts, dev.timeline_value - 1)
|
||||
for c in range(nchunks):
|
||||
seq, size = self._usb_seq + c, min(CHUNK, src.nbytes - c * CHUNK)
|
||||
q.q(POLL_EQ, *data64_le(self._usb_wins[seq & 1].va_addr + round_up(size, 512)), 0x51000000 | (seq & 0xFFFFFF), 0xFFFFFFFF, POLL_DW5)
|
||||
q.copy(dest.offset(c * CHUNK), self._usb_wins[seq & 1], size)
|
||||
q.write(dev.iface.sys_buf.offset(0x800, 8), seq + 1, b64=True)
|
||||
q.signal(ts, dev.next_timeline()).submit(dev)
|
||||
|
||||
# stream the chunks: stage the wire image [payload][sentinel], arm the window, send. A window is reusable once
|
||||
# its previous occupant (seq-2) is both fully sent (tag reaped) and fully drained to VRAM (the fence).
|
||||
inflight = [None, None]
|
||||
for c in range(nchunks):
|
||||
seq, size = self._usb_seq + c, min(CHUNK, src.nbytes - c * CHUNK)
|
||||
if inflight[seq & 1] is not None: usb.usb.bulk_wait(inflight[seq & 1])
|
||||
buf = self._usb_stage[seq & 1][1]
|
||||
buf[:size] = src_mv[c * CHUNK : c * CHUNK + size]
|
||||
struct.pack_into('<I', buf, round_up(size, 512), 0x51000000 | (seq & 0xFFFFFF)) # the sentinel sector
|
||||
wait_drain(seq - 1)
|
||||
wire = round_up(size, 512) + 512 # payload padded to 512B sectors, plus the sentinel sector
|
||||
usb.usb.control_write(0xF2, wire // 512, (seq & 1) * 16 | (ceildiv(wire, 0x4000) << 8)) # wValue=sectors, wIndex=slot|count
|
||||
inflight[seq & 1] = usb.usb.bulk_write_async(buf[:wire])
|
||||
for tag in inflight: usb.usb.bulk_wait(tag)
|
||||
self._usb_seq += nchunks
|
||||
wait_drain(self._usb_seq) # copyin is synchronous: everything must be in VRAM before returning
|
||||
|
||||
def _copyout(self, dest:memoryview, src:HCQBuffer):
|
||||
if not self.dev.is_usb(): return super()._copyout(dest, src)
|
||||
self.dev.synchronize()
|
||||
@@ -842,7 +892,7 @@ class KFDIface:
|
||||
|
||||
class PCIIface(PCIIfaceBase):
|
||||
def __init__(self, dev, dev_id):
|
||||
super().__init__(dev, dev_id, vendor=0x1002, devices=((0xffff, (0x74a1,0x744c,0x7480,0x7550,0x7551,0x7590,0x75a0)),), vram_bar=0,
|
||||
super().__init__(dev, dev_id, vendor=0x1002, devices=((0xffff, (0x74a1,0x744c,0x7480,0x7550,0x7551,0x7590,0x75a0,0x75a8)),), vram_bar=0,
|
||||
va_start=AMMemoryManager.va_allocator.base, va_size=AMMemoryManager.va_allocator.size, dev_impl_t=AMDev)
|
||||
self._compute_props()
|
||||
|
||||
@@ -926,9 +976,8 @@ class USBIface(PCIIface):
|
||||
|
||||
def alloc(self, size:int, host=False, uncached=False, cpu_access=False, contiguous=False, force_devmem=False, **kwargs) -> HCQBuffer:
|
||||
# usb allocates uncached and cpu_access in vram. vram writes are faster than sram writes
|
||||
if host and self.sys_next_off + size < self.sys_buf.size:
|
||||
self.sys_next_off += size
|
||||
return self.sys_buf.offset(self.sys_next_off - size, size)
|
||||
# NOTE: host allocs deliberately do NOT use sys_buf (the 0x820000 NVMe SQ region): the GPU's signal writes there
|
||||
# collide with the 0xF2 engine mid-stream. Signals in VRAM are read back via 0xF0 streaming reads instead.
|
||||
|
||||
# force devmem
|
||||
return super().alloc(size, host=False, uncached=uncached, cpu_access=cpu_access, contiguous=contiguous, force_devmem=True, **kwargs)
|
||||
@@ -1048,7 +1097,8 @@ class AMDDevice(HCQCompiled):
|
||||
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, 0x200 if self.is_usb() else (16 << 20), idx=idx)
|
||||
# USB: a copyin submits its whole ring at once (3 packets per 240KB chunk), so it needs more than the 0x200 default
|
||||
self.sdma_queues[idx] = self.create_queue(kfd.KFD_IOC_QUEUE_TYPE_SDMA, (1 << 20) if self.is_usb() else (16 << 20), idx=idx)
|
||||
return self.sdma_queues.get(idx, None)
|
||||
|
||||
def _ensure_has_local_memory(self, private_segment_size):
|
||||
|
||||
+30
-20
@@ -6,6 +6,7 @@ from tinygrad.helpers import to_mv, from_mv, OSX, WIN, Context, mv_address, supp
|
||||
from tinygrad.device import Buffer, BufferSpec, TinyELF, Program, Device
|
||||
from tinygrad.runtime.support.hcq import HCQBuffer, MMIOInterface
|
||||
from tinygrad.runtime.support.hcq2 import HCQ2Compiled, HCQAllocator, make_cmdbuf, make_signal
|
||||
from tinygrad.runtime.support.c import DLL
|
||||
from tinygrad.renderer.cstyle import ClangRenderer
|
||||
from tinygrad.renderer.llvmir import CPULLVMRenderer
|
||||
from tinygrad.renderer.nir import LVPRenderer
|
||||
@@ -13,7 +14,7 @@ from tinygrad.renderer.isa.x86 import X86Renderer
|
||||
from tinygrad.runtime.support.elf import jit_loader
|
||||
from tinygrad.runtime.autogen import libc
|
||||
from tinygrad.codegen import do_to_program
|
||||
from tinygrad.engine.realize import pm_flatten_linear, get_call_arg_uops, get_runtime
|
||||
from tinygrad.engine.realize import pm_flatten_linear, get_call_arg_uops, get_call_var_uops, get_runtime
|
||||
from tinygrad import UOp, dtypes
|
||||
from tinygrad.dtype import AddrSpace
|
||||
from tinygrad.uop.ops import KernelInfo, Ops, UPat, PatternMatcher, graph_rewrite
|
||||
@@ -64,7 +65,7 @@ def cpu_cmd(devs:tuple[str, ...], prog, *args:UOp) -> UOp:
|
||||
return UOp(Ops.INS, dtypes.void, words + (UOp.const(0, dtypes.uint64),) * (CMD_SIZE - len(words)), arg="cmd")
|
||||
|
||||
def cpu_exec(ctx:tuple[str, ...], call:UOp, prg:UOp) -> UOp:
|
||||
args = [get_call_arg_uops(call)[i].getaddr(ctx) for i in prg.arg.globals] + [v.cast(dtypes.uint64) for v in prg.arg.vars]
|
||||
args = [get_call_arg_uops(call)[i].getaddr(ctx) for i in prg.arg.globals] + [v.cast(dtypes.uint64) for v in get_call_var_uops(call, prg)]
|
||||
if (core:=prg.arg.runtimevars.get('core_id')) is None: return cpu_cmd(ctx, prg, *args)
|
||||
|
||||
la = [cpu_cmd(ctx,prg,*args[:(cid:=(len(prg.arg.globals)+core))],UOp.const(t, dtypes.uint64),*args[cid+1:]) for t in range(prg.arg.global_size[0])]
|
||||
@@ -99,10 +100,11 @@ def encode_queue(q:UOp) -> UOp:
|
||||
e = UOp.range(cnt, next(UOp.unique_num), dtype=dtypes.int, src=(cmdbuf, ring))
|
||||
copy = UOp.group(*[ring.index((base + e*CMD_SIZE + w) % ring_words).store(cmdbuf.index(e*CMD_SIZE + w).load()) for w in range(CMD_SIZE)])
|
||||
|
||||
# wake the worker after each entry, keeping the post with the stores stops it from hoisting out of the loop
|
||||
wake = copy.end(e) if WIN else make_signal(devs, tag="func:sem_post").after(copy).index(0).load().call(sem.index(0), ret_dtype=dtypes.void).end(e)
|
||||
bumped = put.after(wake).index(0).store(put.index(0).load() + cnt)
|
||||
return sysbuf.after(bumped).index(0).store(put.index(0).load() + cnt) if WIN else bumped
|
||||
bumped = put.after(copy.end(e)).index(0).store(put.index(0).load() + cnt)
|
||||
if WIN: return sysbuf.after(bumped).index(0).store(put.after(bumped).index(0).load())
|
||||
|
||||
e = UOp.range(cnt, next(UOp.unique_num), dtype=dtypes.int, src=(bumped,))
|
||||
return make_signal(devs, tag="func:sem_post").after(e).index(0).load().call(sem.after(e).index(0), ret_dtype=dtypes.void).end(e)
|
||||
|
||||
# *****************
|
||||
|
||||
@@ -110,9 +112,9 @@ def encode_queue(q:UOp) -> UOp:
|
||||
MAP_JIT = 0x0800
|
||||
|
||||
class CPUProgram(Program['CPUDevice']):
|
||||
rt_lib = None
|
||||
try: rt_lib = ctypes.CDLL(ctypes.util.find_library('System' if OSX else 'kernel32') if OSX or WIN else 'libgcc_s.so.1')
|
||||
except OSError: pass
|
||||
rt_lib, libm = DLL('rt', 'System' if OSX else 'kernel' if WIN else 'gcc_s'), DLL('m', 'm')
|
||||
|
||||
def _load(self, lib, base=0): return lib if lib[:4] != libc.ELFMAG.encode() else jit_loader(lib, base=base, link_libs=[self.libm, self.rt_lib])
|
||||
|
||||
def __init__(self, dev:CPUDevice, obj:TinyELF):
|
||||
self.dev, self.name, self.signature = dev, obj.name, obj.signature
|
||||
@@ -124,10 +126,10 @@ class CPUProgram(Program['CPUDevice']):
|
||||
ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_void_p
|
||||
self.addr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_void_p(0), ctypes.c_size_t(len(obj.lib)), MEM_COMMIT | MEM_RESERVE,
|
||||
PAGE_EXECUTE_READWRITE)
|
||||
ctypes.memmove(self.addr, obj.lib, len(obj.lib))
|
||||
ctypes.memmove(self.addr, (loaded:=self._load(obj.lib, self.addr)), len(loaded))
|
||||
ctypes.windll.kernel32.GetCurrentProcess.restype = ctypes.c_void_p
|
||||
proc = ctypes.windll.kernel32.GetCurrentProcess()
|
||||
ctypes.windll.kernel32.FlushInstructionCache(ctypes.c_void_p(proc), ctypes.c_void_p(self.addr), ctypes.c_size_t(len(obj.lib)))
|
||||
ctypes.windll.kernel32.FlushInstructionCache(ctypes.c_void_p(proc), ctypes.c_void_p(self.addr), ctypes.c_size_t(len(loaded)))
|
||||
self.fxn = ctypes.CFUNCTYPE(None, ctypes.c_void_p)(self.addr) if self.lvp else ctypes.CFUNCTYPE(None)(self.addr)
|
||||
else:
|
||||
# On apple silicon with SPRR enabled (it always is in macos) RWX pages are unrepresentable: https://blog.svenpeter.dev/posts/m1_sprr_gxf/
|
||||
@@ -136,18 +138,17 @@ class CPUProgram(Program['CPUDevice']):
|
||||
self.addr = mv_address(self.mem)
|
||||
|
||||
if OSX: unwrap(CPUProgram.rt_lib).pthread_jit_write_protect_np(False)
|
||||
lib = jit_loader(obj.lib, base=ctypes.addressof(ctypes.c_void_p.from_buffer(self.mem)), link_libs=['m']) if self.lvp else obj.lib
|
||||
self.mem.write(lib)
|
||||
self.mem.write(loaded:=self._load(obj.lib, mv_address(self.mem)))
|
||||
if OSX: unwrap(CPUProgram.rt_lib).pthread_jit_write_protect_np(True)
|
||||
|
||||
# __clear_cache isn't a normal libc function, but a compiler support routine found in libgcc_s for gcc and compiler-rt for clang.
|
||||
# libgcc_s comes as shared library but compiler-rt is only a bunch of static library archives which we can't directly load, but fortunately
|
||||
# it somehow found its way into libSystem on macos (likely because it used __builtin_clear_cache) and libgcc_s is ~always present on linux
|
||||
# Using ["name"] instead of .name because otherwise name is getting mangled: https://docs.python.org/3.12/reference/expressions.html#index-5
|
||||
if CPUProgram.rt_lib is not None: CPUProgram.rt_lib["__clear_cache"](ctypes.c_void_p(self.addr), ctypes.c_void_p(self.addr + len(lib)))
|
||||
if 'rt' in DLL._loaded_: CPUProgram.rt_lib["__clear_cache"](ctypes.c_void_p(self.addr), ctypes.c_void_p(self.addr + len(loaded)))
|
||||
else:
|
||||
# msync should be a universal POSIX way to do this
|
||||
libc.msync(ctypes.c_void_p(self.addr), len(lib), libc.MS_SYNC | libc.MS_INVALIDATE)
|
||||
libc.msync(ctypes.c_void_p(self.addr), len(loaded), libc.MS_SYNC | libc.MS_INVALIDATE)
|
||||
|
||||
self.fxn = ctypes.CFUNCTYPE(None, ctypes.c_void_p)(self.addr) if self.lvp else ctypes.CFUNCTYPE(None)(self.addr)
|
||||
|
||||
@@ -196,11 +197,13 @@ class CPUDevice(HCQ2Compiled):
|
||||
pm_lower = PatternMatcher([(UPat(Ops.CUSTOM_FUNCTION, arg="submit_cmdbuf", src=(UPat(Ops.LINEAR, name="q"),)), encode_queue)])
|
||||
|
||||
def __init__(self, device:str=""):
|
||||
self.workers:list[CPUWorker] = []
|
||||
super().__init__(device, CPUAllocator(self), [ClangRenderer, CPULLVMRenderer, LVPRenderer, X86Renderer], CPUProgram,
|
||||
arch={'amd64':'x86_64', 'aarch64':'arm64'}.get(m:=platform.machine().lower(), m)+",native")
|
||||
|
||||
self.pm_bufferize = PatternMatcher(
|
||||
[(UPat(Ops.PARAM, tag=f"COMPUTE:0_{n}"), lambda ctx, n=n: getattr(ctx[0].worker, n)) for n in ("ring", "put", "sem", "sys", "done")] +
|
||||
[(UPat(Ops.PARAM, tag=f"{q}_{n}"), lambda ctx, q=q,n=n: getattr(ctx[0].worker(q), n))
|
||||
for q in ("COMPUTE:0", "SUBMIT:0") for n in ("ring", "put", "sem", "sys", "done")] +
|
||||
[(UPat(Ops.PARAM, tag=f"func:{f}"), lambda ctx, f=f: ctx[0].func_ptr(f)) for f in FUNCS]) + self.pm_bufferize
|
||||
|
||||
with Context(EMULATED_DTYPES="", TRACK_MATCH_STATS=0):
|
||||
@@ -210,6 +213,12 @@ class CPUDevice(HCQ2Compiled):
|
||||
|
||||
def func_ptr(self, name:str) -> Buffer: return self.func_table.view(1, dtypes.uint64, FUNCS.index(name)*8).ensure_allocated()
|
||||
|
||||
def synchronize(self, timeout:int|None=None):
|
||||
for worker in self.workers:
|
||||
put, done = (getattr(worker, x)._buf.cpu_view().view(fmt='Q') for x in ("put", "done"))
|
||||
while done[0] < put[0]: self._wait_signal(done, put[0], timeout)
|
||||
super().synchronize(timeout)
|
||||
|
||||
@functools.cached_property
|
||||
def func_table(self) -> Buffer:
|
||||
lib = ctypes.windll.kernel32 if sys.platform == "win32" else libc.dll # type: ignore[attr-defined]
|
||||
@@ -217,8 +226,8 @@ class CPUDevice(HCQ2Compiled):
|
||||
array.array('Q', [unwrap(ctypes.cast(getattr(lib, f), ctypes.c_void_p).value) for f in FUNCS])
|
||||
return ft
|
||||
|
||||
@functools.cached_property
|
||||
def worker(self) -> CPUWorker:
|
||||
@functools.cache
|
||||
def worker(self, queue:str) -> CPUWorker:
|
||||
ring, put, sysbuf, done = (Buffer(self.device, sz, dtypes.uint64, preallocate=True) for sz in (RING_SLOTS*CMD_SIZE, 1, 1, 1))
|
||||
addr, hsem = 0, None
|
||||
|
||||
@@ -230,5 +239,6 @@ class CPUDevice(HCQ2Compiled):
|
||||
sem = Buffer(self.device, 1, dtypes.uint64, options=BufferSpec(external_ptr=addr), preallocate=True)
|
||||
|
||||
worker_args = [ring._buf.va_addr, sysbuf._buf.va_addr if WIN else self.func_ptr('sem_wait')._buf.va_addr, done._buf.va_addr, addr]
|
||||
(worker:=threading.Thread(target=self.prgs[worker_prog].fxn, daemon=True, args=[ctypes.c_uint64(x) for x in worker_args])).start()
|
||||
return CPUWorker(ring, put, sem, sysbuf, done, worker)
|
||||
(thread:=threading.Thread(target=self.prgs[worker_prog].fxn, daemon=True, args=[ctypes.c_uint64(x) for x in worker_args])).start()
|
||||
self.workers.append(worker:=CPUWorker(ring, put, sem, sysbuf, done, thread))
|
||||
return worker
|
||||
|
||||
@@ -49,8 +49,7 @@ class DiskDevice(Compiled):
|
||||
DiskDevice._tried_io_uring_init = True
|
||||
|
||||
if sys.platform == 'linux' and not hasattr(sys, "getandroidapilevel"):
|
||||
p = io_uring.struct_io_uring_params(flags=io_uring.IORING_SETUP_SQPOLL, sq_thread_idle=0xffffffff)
|
||||
fd = libc.syscall(io_uring.NR_io_uring_setup, 4096, ctypes.byref(p))
|
||||
fd = libc.syscall(io_uring.NR_io_uring_setup, 4096, ctypes.byref(p:=io_uring.struct_io_uring_params()))
|
||||
if fd < 0: return
|
||||
|
||||
sq_ptr = libc.mmap(0, p.sq_off.array + p.sq_entries * 4, mmap.PROT_READ | mmap.PROT_WRITE, mmap.MAP_SHARED | MAP_POPULATE, fd, 0)
|
||||
@@ -68,7 +67,6 @@ class DiskDevice(Compiled):
|
||||
kring_mask=u32ptr(sq_ptr+p.cq_off.ring_mask), cqes=ctypes.cast(cq_ptr+p.cq_off.cqes, ctypes.POINTER(io_uring.struct_io_uring_cqe)))
|
||||
|
||||
DiskDevice.io_uring = io_uring.struct_io_uring(ring_fd=fd, sq=sqdesc, cq=cqdesc) # type: ignore
|
||||
libc.syscall(io_uring.NR_io_uring_enter, fd, 0, 0, io_uring.IORING_ENTER_SQ_WAKEUP)
|
||||
|
||||
class DiskBuffer:
|
||||
def __init__(self, device:DiskDevice, size:int, offset=0):
|
||||
@@ -126,6 +124,7 @@ class DiskAllocator(Allocator):
|
||||
# Send sqe
|
||||
DiskDevice.io_uring.sq.array[sqe_index] = sqe_index
|
||||
DiskDevice.io_uring.sq.ktail[0] = tail + 1
|
||||
libc.syscall(io_uring.NR_io_uring_enter, DiskDevice.io_uring.ring_fd, 1, 1, io_uring.IORING_ENTER_GETEVENTS)
|
||||
|
||||
reqs.append((copy_batch, copied_in, minor_offset, real_copy_size:=min(sqe.len - minor_offset, size - copied_in)))
|
||||
next_read_offset += sqe.len
|
||||
|
||||
@@ -34,6 +34,7 @@ class MetalDevice(Compiled):
|
||||
self.mtl_queue = self.sysdevice.newCommandQueueWithMaxCommandBufferCount(1024)
|
||||
if self.mtl_queue is None: raise RuntimeError("Cannot allocate a new command queue")
|
||||
self.mtl_buffers_in_flight: list[metal.MTLCommandBuffer] = []
|
||||
self.mtl_profile_keys: dict[int, bytes] = {}
|
||||
self.timeline_signal = self.sysdevice.newSharedEvent()
|
||||
self.timeline_value = 0
|
||||
|
||||
@@ -55,7 +56,7 @@ class MetalDevice(Compiled):
|
||||
st, en = decimal.Decimal(cbuf.GPUStartTime()) * 1000000, decimal.Decimal(cbuf.GPUEndTime()) * 1000000
|
||||
# NOTE: command buffers from MetalGraph are not profiled here
|
||||
if PROFILE and (lb:=cmdbuf_label(cbuf)) is not None and not lb.startswith("batched"):
|
||||
Compiled.profile_events += [ProfileRangeEvent(self.device, lb, st, en)]
|
||||
Compiled.profile_events += [ProfileRangeEvent(self.device, lb, st, en, self.mtl_profile_keys.pop(id(cbuf), None))]
|
||||
self.mtl_buffers_in_flight.clear()
|
||||
|
||||
class MetalCompiler(Compiler):
|
||||
@@ -113,7 +114,7 @@ class MetalCompiler(Compiler):
|
||||
|
||||
class MetalProgram(Program[MetalDevice]):
|
||||
def __init__(self, dev:MetalDevice, obj:TinyELF):
|
||||
self.dev, self.name, self.lib, self.signature = dev, obj.name, obj.lib, obj.signature
|
||||
self.dev, self.name, self.lib, self.signature, self.profile_key = dev, obj.name, obj.lib, obj.signature, obj.profile_key
|
||||
data = objc.dispatch_data_create(obj.lib, len(obj.lib), None, None)
|
||||
self.library = self.dev.sysdevice.newLibraryWithData_error(data, ctypes.byref(error_lib:=metal.NSError().retained())).retained()
|
||||
error_check(error_lib)
|
||||
@@ -145,6 +146,7 @@ class MetalProgram(Program[MetalDevice]):
|
||||
command_buffer.setLabel(to_ns_str(self.name)) # TODO: is this always needed?
|
||||
command_buffer.commit()
|
||||
self.dev.mtl_buffers_in_flight.append(command_buffer)
|
||||
if PROFILE and self.profile_key is not None: self.dev.mtl_profile_keys[id(command_buffer)] = self.profile_key
|
||||
if wait:
|
||||
wait_check(command_buffer)
|
||||
return command_buffer.GPUEndTime() - command_buffer.GPUStartTime()
|
||||
|
||||
@@ -6,6 +6,8 @@ class NpyAllocator(Allocator['NpyDevice']):
|
||||
def _alloc(self, size:int, options=None) -> np.ndarray: return np.empty(size, dtype=np.uint8)
|
||||
def _as_buffer(self, src:np.ndarray) -> memoryview: return flat_mv(np.require(src, requirements='C').data)
|
||||
def _copyout(self, dest:memoryview, src:np.ndarray): dest[:] = self._as_buffer(src)
|
||||
def _offset(self, buf:np.ndarray, size:int, offset:int) -> np.ndarray:
|
||||
return np.require(buf, requirements='C').reshape(-1).view(np.uint8)[offset:offset+size]
|
||||
|
||||
class NpyDevice(Compiled):
|
||||
def __init__(self, device:str): super().__init__(device, NpyAllocator(self), [], None)
|
||||
|
||||
@@ -17,9 +17,9 @@ class NullRenderer(CStyleLanguage):
|
||||
return assemble_linear(prg, lin, self.target.arch)
|
||||
|
||||
class NullProgram(Program['NullDevice']):
|
||||
def __init__(self, dev:'NullDevice', obj:TinyELF): self.device, self.name = dev.device, obj.name
|
||||
def __init__(self, dev:'NullDevice', obj:TinyELF): self.device, self.name, self.profile_key = dev.device, obj.name, obj.profile_key
|
||||
def __call__(self, *bufs, global_size:tuple[int,int,int]=(1,1,1), local_size:tuple[int,int,int]=(1,1,1), vals:tuple[int, ...]=(), wait=False, **kw):
|
||||
with cpu_profile(self.name, self.device): return 1e-3
|
||||
with cpu_profile(self.name, self.device, profile_key=self.profile_key): return 1e-3
|
||||
|
||||
class NullAllocator(Allocator['NullDevice']):
|
||||
def _alloc(self, size, options): pass
|
||||
@@ -38,13 +38,14 @@ class NullGraph(MultiGraphRunner):
|
||||
for (_,_,bufs,_),runtime in zip(self.calls, self.runtimes):
|
||||
# description based on command, copied from HCQ graph
|
||||
device = runtime.device if runtime is not None else f"{bufs[1].device}:SDMA:0"
|
||||
descs.append((device, runtime.name if runtime is not None else f"{bufs[1].device} -> {bufs[0].device}", count:=event_count.get(device, 0)))
|
||||
descs.append((device, runtime.name if runtime is not None else f"{bufs[1].device} -> {bufs[0].device}",
|
||||
runtime.profile_key if runtime is not None else None, count:=event_count.get(device, 0)))
|
||||
event_count[device] = count+1
|
||||
# pack events evenly per device
|
||||
dur, sigs, ents = max(1, math.ceil((perf_counter_us()-st)/max(event_count.values()))), [], []
|
||||
for i,(device,name,count) in enumerate(descs):
|
||||
for i,(device,name,profile_key,count) in enumerate(descs):
|
||||
sigs += [st+count*dur, st+(count+1)*dur]
|
||||
ents.append(ProfileGraphEntry(device, name, 2*i, 2*i+1))
|
||||
ents.append(ProfileGraphEntry(device, name, 2*i, 2*i+1, profile_key))
|
||||
cpu_events.append(ProfileGraphEvent(ents, [], sigs))
|
||||
return 1e-1
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user