mirror of
https://github.com/tinygrad/tinygrad.git
synced 2026-09-09 09:46:13 +00:00
Compare commits
465
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad240b13a1 | ||
|
|
8e1258fdfa | ||
|
|
f1c3d41c05 | ||
|
|
a65091b00a | ||
|
|
f49a8f1a93 | ||
|
|
d1f7e4f9bd | ||
|
|
c60916b15a | ||
|
|
ab24cc89e7 | ||
|
|
1057f7d27a | ||
|
|
649054b204 | ||
|
|
f51574906f | ||
|
|
fd78b62cc3 | ||
|
|
b450bf868f | ||
|
|
a3278fa76f | ||
|
|
00f4eed595 | ||
|
|
0ef0271591 | ||
|
|
719692bf6d | ||
|
|
b92fb8cc6a | ||
|
|
eb148233a2 | ||
|
|
3c54a89bf2 | ||
|
|
b1cc9ed244 | ||
|
|
918a2d9fdd | ||
|
|
79c5d24189 | ||
|
|
ee3cd40969 | ||
|
|
c591fa70d6 | ||
|
|
2c19a6450a | ||
|
|
5a906101e2 | ||
|
|
f7a3ef2ab8 | ||
|
|
d1ec955346 | ||
|
|
9f8144c914 | ||
|
|
0f99c5d71e | ||
|
|
e4bac3fa9b | ||
|
|
69915d61c2 | ||
|
|
65558fef9d | ||
|
|
a4ac2605fb | ||
|
|
5f06e19fbd | ||
|
|
48c8736dc2 | ||
|
|
00a5b14216 | ||
|
|
af598b33bb | ||
|
|
dabcec6691 | ||
|
|
86baa8d125 | ||
|
|
eb6bca255d | ||
|
|
1f114dc961 | ||
|
|
5a4831bca0 | ||
|
|
e0413ba189 | ||
|
|
c1560cb44b | ||
|
|
b6deae1e9c | ||
|
|
9fca24ffb7 | ||
|
|
f5528f3eb5 | ||
|
|
2b787196b3 | ||
|
|
f34f308b61 | ||
|
|
6a6c3042f4 | ||
|
|
5ae6526d47 | ||
|
|
020c7a14fd | ||
|
|
f9ae840f91 | ||
|
|
371ac77173 | ||
|
|
1d878ac67a | ||
|
|
7e14f3690d | ||
|
|
479e077ecf | ||
|
|
2731aa27f7 | ||
|
|
405f292dae | ||
|
|
c84876fdd2 | ||
|
|
1f3c0ac89c | ||
|
|
a609a0a29d | ||
|
|
24af3a0941 | ||
|
|
ac40497945 | ||
|
|
b536514c83 | ||
|
|
82bd6d5476 | ||
|
|
226556ddf3 | ||
|
|
33cd373ad3 | ||
|
|
707d87e97b | ||
|
|
ded106b183 | ||
|
|
39e246848c | ||
|
|
8e9c929a51 | ||
|
|
0dc55feddc | ||
|
|
0319b1e75f | ||
|
|
aaf76ca406 | ||
|
|
6fd714d069 | ||
|
|
e8c8ba1c77 | ||
|
|
4f4f8e4f95 | ||
|
|
39f9bd0461 | ||
|
|
e1ba1755b7 | ||
|
|
4f44116bd6 | ||
|
|
5231b5274c | ||
|
|
01647028fb | ||
|
|
404cda437a | ||
|
|
6c26eaf724 | ||
|
|
a3e85c297a | ||
|
|
543da4dcb6 | ||
|
|
cac1bb1c9a | ||
|
|
e3431a2172 | ||
|
|
7cd71fb54a | ||
|
|
67ef401f41 | ||
|
|
290aa54df5 | ||
|
|
f0bdf2d9e9 | ||
|
|
313221aac2 | ||
|
|
6201202e23 | ||
|
|
a4fd692435 | ||
|
|
ec18aadf43 | ||
|
|
c7b6ebbc21 | ||
|
|
f4c7aa7cca | ||
|
|
f7742b7758 | ||
|
|
54a39db8dc | ||
|
|
83ee6144f8 | ||
|
|
f48b583ee0 | ||
|
|
2c4e5bb50b | ||
|
|
ccf14f0530 | ||
|
|
d34e0030ef | ||
|
|
1d134dadcd | ||
|
|
b77ffbd200 | ||
|
|
c9ed7f3961 | ||
|
|
1b06b01144 | ||
|
|
e740ded0ed | ||
|
|
8bf84d0e4f | ||
|
|
6f4bfde234 | ||
|
|
88face1a98 | ||
|
|
34db1c81ad | ||
|
|
c68c174daa | ||
|
|
5fb32893ba | ||
|
|
5b05106537 | ||
|
|
69be34faa1 | ||
|
|
be7e3891ab | ||
|
|
7807ba0b9c | ||
|
|
18960ec154 | ||
|
|
68d76d4d61 | ||
|
|
deed921d84 | ||
|
|
0bcc92382e | ||
|
|
f626844e5f | ||
|
|
f2822d1077 | ||
|
|
ce086fefb4 | ||
|
|
a5c833702f | ||
|
|
f2b9fe1653 | ||
|
|
f6fc4e3f2c | ||
|
|
60f1cd916b | ||
|
|
5ae064e51a | ||
|
|
a33577b067 | ||
|
|
98be30f1b0 | ||
|
|
017d51bb14 | ||
|
|
9cd01059c2 | ||
|
|
eb5cfe9036 | ||
|
|
93d8f02551 | ||
|
|
5c6af9cb18 | ||
|
|
9e2fe7a15a | ||
|
|
13a0169360 | ||
|
|
674466abad | ||
|
|
0d60a6cb5d | ||
|
|
cde95fd3b3 | ||
|
|
06f26aaa86 | ||
|
|
9dbcddc649 | ||
|
|
fd10521869 | ||
|
|
ed0848c055 | ||
|
|
613ac4bec1 | ||
|
|
46bf11106f | ||
|
|
fe95f765c9 | ||
|
|
c2aada33ff | ||
|
|
cf6fff13aa | ||
|
|
b87159cee1 | ||
|
|
a3bde519d6 | ||
|
|
37738a7276 | ||
|
|
c12eefe447 | ||
|
|
79250d76de | ||
|
|
4197f74231 | ||
|
|
8f4bd50ddd | ||
|
|
32bbf82d5b | ||
|
|
60cc1d7827 | ||
|
|
d025993642 | ||
|
|
af079e8cc8 | ||
|
|
e3c4b3d9b2 | ||
|
|
3123f947b0 | ||
|
|
b48cb9fce7 | ||
|
|
a6c810eb03 | ||
|
|
593aa1e17b | ||
|
|
fdb4109f5f | ||
|
|
85841ce0d1 | ||
|
|
1c183e6de9 | ||
|
|
f5b00de319 | ||
|
|
902a1efe06 | ||
|
|
9c49e35aab | ||
|
|
7cc973a4e0 | ||
|
|
61f7fd3735 | ||
|
|
1a5a364f3e | ||
|
|
0b58ff679c | ||
|
|
0bd725376b | ||
|
|
d7913e206b | ||
|
|
2f5572ab20 | ||
|
|
0a1bfc9fad | ||
|
|
8c7906d200 | ||
|
|
661440e672 | ||
|
|
9aa3c48200 | ||
|
|
02d67a8333 | ||
|
|
12f34d8eb1 | ||
|
|
a9830e2b4d | ||
|
|
812d5cef62 | ||
|
|
0640cfce35 | ||
|
|
086f45cb81 | ||
|
|
cb0db2d042 | ||
|
|
793c1664f3 | ||
|
|
bd2711b7c1 | ||
|
|
b3dad4fa0c | ||
|
|
49627aecb5 | ||
|
|
959958135d | ||
|
|
f196af2327 | ||
|
|
112e50ec3a | ||
|
|
ae63f23557 | ||
|
|
287679a88a | ||
|
|
2eacd4fa68 | ||
|
|
7fdc58b1cc | ||
|
|
cb857a5a40 | ||
|
|
6083de5cff | ||
|
|
55c8e18930 | ||
|
|
76dce1eb8d | ||
|
|
6242b09066 | ||
|
|
92d92c70a9 | ||
|
|
26c4b6319e | ||
|
|
53cad325c7 | ||
|
|
4456e62343 | ||
|
|
39d7fd0981 | ||
|
|
4e6bdac412 | ||
|
|
bc485d271f | ||
|
|
6b361e4de7 | ||
|
|
38e0fe103b | ||
|
|
e6ad2907a8 | ||
|
|
fede358811 | ||
|
|
f06832bf6f | ||
|
|
ee3161e924 | ||
|
|
38fa0643cf | ||
|
|
f267a49639 | ||
|
|
14cbecef8f | ||
|
|
54d69f439f | ||
|
|
980c42d611 | ||
|
|
7eb763a3c2 | ||
|
|
5c3d044465 | ||
|
|
f19d89e29a | ||
|
|
2d4e7afcbf | ||
|
|
0d66815849 | ||
|
|
d16a2510ad | ||
|
|
118e812b31 | ||
|
|
0820c7a424 | ||
|
|
22ffff3258 | ||
|
|
a0a901c8e4 | ||
|
|
c015351ac5 | ||
|
|
6074c002e1 | ||
|
|
6042b87272 | ||
|
|
cc72b9f7be | ||
|
|
6a3b297548 | ||
|
|
ea6c82f3be | ||
|
|
0abcf09b74 | ||
|
|
4bdc865131 | ||
|
|
4c20f1d357 | ||
|
|
ecf79e260d | ||
|
|
9860e5d285 | ||
|
|
625c05df1e | ||
|
|
b49c03fb1c | ||
|
|
dc04c7820e | ||
|
|
6ece327cf3 | ||
|
|
07268b724f | ||
|
|
55032514ce | ||
|
|
2824504f90 | ||
|
|
7fef98c86e | ||
|
|
b831ca62d9 | ||
|
|
d851aca9ae | ||
|
|
7dc8b666e7 | ||
|
|
023bfdb380 | ||
|
|
9f01775cf4 | ||
|
|
9607787ce1 | ||
|
|
ab79879613 | ||
|
|
a5678317c2 | ||
|
|
1d694dd700 | ||
|
|
65ca68567e | ||
|
|
a7df1a1ace | ||
|
|
d9004cff22 | ||
|
|
6f87158d77 | ||
|
|
021c015eb4 | ||
|
|
2e7790f16c | ||
|
|
8df3dac0ec | ||
|
|
ed110993d3 | ||
|
|
f4cc28824c | ||
|
|
76bf6b7eec | ||
|
|
08fbd25f1a | ||
|
|
5aabbb1991 | ||
|
|
7a887e84b2 | ||
|
|
7b21ffac00 | ||
|
|
9b481a4893 | ||
|
|
9b0c65688d | ||
|
|
7e561fcb97 | ||
|
|
a5ea95d8b8 | ||
|
|
df528499ce | ||
|
|
4ee114b42a | ||
|
|
9d0cd0ebcb | ||
|
|
11edcc144c | ||
|
|
a31aca9e52 | ||
|
|
8b164aefea | ||
|
|
477b573807 | ||
|
|
bb0e99acbf | ||
|
|
93865e2c66 | ||
|
|
5b60a09ab0 | ||
|
|
b0a1285330 | ||
|
|
a2e64e16aa | ||
|
|
a9069c177a | ||
|
|
8950942e75 | ||
|
|
356f665377 | ||
|
|
7204d46786 | ||
|
|
af242819d8 | ||
|
|
52596dbf38 | ||
|
|
07cce78cec | ||
|
|
f986829461 | ||
|
|
4fd4eafb23 | ||
|
|
daa154aa22 | ||
|
|
12f889aaad | ||
|
|
298748ebd3 | ||
|
|
3082956a17 | ||
|
|
402bea7ddd | ||
|
|
8f9cbdf0cc | ||
|
|
8f59041ee5 | ||
|
|
1cf8a2c7fe | ||
|
|
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 | ||
|
|
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 | ||
|
|
e7bf2a811d | ||
|
|
e688e07758 | ||
|
|
97022960ae | ||
|
|
417563ca20 | ||
|
|
5ca87f1bac | ||
|
|
26cbadd69a | ||
|
|
fae893753b | ||
|
|
c17849a1f8 | ||
|
|
539a03343a | ||
|
|
a57569349c | ||
|
|
5c43a89fb1 | ||
|
|
e6f5bb9c09 | ||
|
|
4b0525e594 | ||
|
|
64ccbde3bb | ||
|
|
6ea665ed66 | ||
|
|
0725acc392 | ||
|
|
4a1f32977c | ||
|
|
13c381b0c0 | ||
|
|
ac7067ac60 | ||
|
|
80169c6758 | ||
|
|
adacaa3e17 | ||
|
|
89ab344c42 | ||
|
|
25c3bd027b | ||
|
|
6b35220622 | ||
|
|
b1859805b1 | ||
|
|
faba071b1d | ||
|
|
81dc8ec232 | ||
|
|
95ca5081fe | ||
|
|
303d1677b3 | ||
|
|
673c6463f9 | ||
|
|
849074f0db | ||
|
|
0252cb8fa7 | ||
|
|
cc6d33bde7 | ||
|
|
16c5ff2490 | ||
|
|
1b7f040984 | ||
|
|
39d144546e | ||
|
|
e103fb2a10 | ||
|
|
2297118541 | ||
|
|
cd6d0d6ee3 | ||
|
|
ff0cb28c21 | ||
|
|
ed8297a102 | ||
|
|
de04781b36 | ||
|
|
4f106ebe87 | ||
|
|
04c271ac41 | ||
|
|
2e5a9a4121 | ||
|
|
e1013a6356 | ||
|
|
f891f5ffd0 | ||
|
|
3686a1758f | ||
|
|
4a253db9b4 | ||
|
|
479ffb0cda | ||
|
|
2b5018e86a | ||
|
|
e11df72e0f | ||
|
|
a8c84ab34e | ||
|
|
ffef35c53e | ||
|
|
55e4f9d4f3 | ||
|
|
0c6a2c7dd6 | ||
|
|
ad2fdeae69 | ||
|
|
115bf9940f | ||
|
|
7edf80a48a | ||
|
|
22722ea2e7 | ||
|
|
d41ca5e60f | ||
|
|
e29606f07e | ||
|
|
8611fe22a7 | ||
|
|
2821bd646f | ||
|
|
44f1f45cd5 | ||
|
|
566f32fe9f |
@@ -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
|
||||
@@ -41,12 +41,12 @@ inputs:
|
||||
description: "Install LLVM?"
|
||||
required: false
|
||||
default: 'false'
|
||||
tinydreno:
|
||||
description: "Install tinydreno"
|
||||
qemu:
|
||||
description: "Install qemu?"
|
||||
required: false
|
||||
default: 'false'
|
||||
qemu:
|
||||
description: "Install qemu"
|
||||
ninja:
|
||||
description: "Install ninja?"
|
||||
required: false
|
||||
default: 'false'
|
||||
runs:
|
||||
@@ -59,18 +59,18 @@ 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"
|
||||
echo "UV_PYTHON_INSTALL_DIR=/opt/python" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "VIRTUAL_ENV=${{ github.workspace }}/.venv" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
|
||||
with:
|
||||
enable-cache: 'false' # see below for manual caching
|
||||
|
||||
- name: Set up Python ${{ inputs.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
if: inputs.python-version != ''
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
|
||||
# **** Caching packages ****
|
||||
|
||||
- name: Cache Python packages (PR)
|
||||
@@ -109,15 +109,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
|
||||
@@ -125,16 +125,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
|
||||
@@ -162,7 +161,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: |
|
||||
@@ -187,25 +186,37 @@ 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"
|
||||
|
||||
installed=true
|
||||
for pkg in $pkgs; do
|
||||
info=$(dpkg-query -W -f='${db:Status-Abbrev} ${Version}' "$pkg" 2> /dev/null || true)
|
||||
echo "${pkg}: ${info:-not in dpkg database}"
|
||||
[[ "$info" == ii* ]] || installed=false
|
||||
done
|
||||
echo "installed=$installed" >> "$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' && steps.apt-pkgs.outputs.installed == 'false'
|
||||
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' && steps.apt-pkgs.outputs.installed == 'false'
|
||||
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') && steps.apt-pkgs.outputs.installed == 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt -qq update || true
|
||||
@@ -219,9 +230,12 @@ runs:
|
||||
sudo chown -R $USER:$USER /var/cache/apt/archives/
|
||||
|
||||
- name: Add clang to PATH (Linux)
|
||||
if: inputs.llvm == 'true' && runner.os == 'Linux'
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: echo "/usr/lib/llvm-20/bin" >> "$GITHUB_PATH"
|
||||
run: |
|
||||
if [ -d /usr/lib/llvm-20/bin ]; then
|
||||
echo "/usr/lib/llvm-20/bin" >> "$GITHUB_PATH"
|
||||
fi
|
||||
|
||||
# **** AMD ****
|
||||
- name: Setup AMD (Linux)
|
||||
@@ -277,12 +291,6 @@ runs:
|
||||
shell: bash
|
||||
run: brew install llvm@20
|
||||
|
||||
# *** tinydreno ***
|
||||
- name: Install tinydreno (linux)
|
||||
if: inputs.tinydreno == 'true' && runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: sudo curl -fL https://github.com/sirhcm/tinydreno/raw/refs/heads/master/libllvm-qcom.so -o /usr/lib/libllvm-qcom.so
|
||||
|
||||
# *** OpenCL ***
|
||||
- name: Install rusticl
|
||||
if: inputs.opencl == 'true'
|
||||
|
||||
@@ -35,15 +35,15 @@ 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
|
||||
run: |
|
||||
find tinygrad/runtime/autogen -type f -name "*.py" -not -path "*/amd/*" -not -name "__init__.py" -not -name "comgr.py" -not -name "metal.py" -not -name "iokit.py" -not -name "corefoundation.py" -not -name "libclang.py" -delete
|
||||
find tinygrad/runtime/autogen -type f -name "*.py" -not -path "*/amd/*" -not -name "__init__.py" -not -name "metal.py" -not -name "iokit.py" -not -name "corefoundation.py" -not -name "libclang.py" -delete
|
||||
python3 -c "from tinygrad.runtime.autogen import opencl"
|
||||
python3 -c "from tinygrad.runtime.autogen import cuda, nvrtc, nvjitlink, nv_570, nv_580, nv_610, nv"
|
||||
python3 -c "from tinygrad.runtime.autogen import comgr_3, hsa, hip, amd_gpu, sqtt, rocprof, amdgpu_kd, amdgpu_drm"
|
||||
python3 -c "from tinygrad.runtime.autogen import comgr, comgr_3, hsa, hip, amd_gpu, sqtt, rocprof, amdgpu_kd, amdgpu_drm"
|
||||
python3 -c "from tinygrad.runtime.autogen.am import *"
|
||||
python3 -c "from tinygrad.runtime.autogen.nv_regs import *"
|
||||
python3 -c "from tinygrad.runtime.autogen import libc, kfd, io_uring, pci, vfio"
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
python3 -c "from tinygrad.runtime.autogen import mesa"
|
||||
python3 -c "from tinygrad.runtime.autogen import avcodec"
|
||||
python3 -c "from tinygrad.runtime.autogen import llvm_qcom"
|
||||
python3 -c "from tinygrad.runtime.autogen import mlx5"
|
||||
python3 -c "from tinygrad.runtime.autogen import mlx5, bnxt"
|
||||
python3 -c "from tinygrad.runtime.autogen import ggml_common"
|
||||
REGEN=1 python3 -c "from tinygrad.runtime.autogen import libclang"
|
||||
- name: Check for differences
|
||||
@@ -102,42 +102,3 @@ jobs:
|
||||
with:
|
||||
name: autogen-macos-patch
|
||||
path: autogen-macos.patch
|
||||
|
||||
autogen-comgr-2:
|
||||
name: In-tree Autogen (comgr 2)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup Environment
|
||||
uses: ./.github/actions/setup-tinygrad
|
||||
with:
|
||||
key: 'autogen-comgr'
|
||||
- name: Install autogen support packages
|
||||
run: |
|
||||
wget https://repo.radeon.com/rocm/rocm.gpg.key -O - | gpg --dearmor | sudo tee /etc/apt/keyrings/rocm.gpg > /dev/null
|
||||
sudo tee /etc/apt/sources.list.d/rocm.list <<EOF
|
||||
deb [arch=amd64 signed-by=/etc/apt/keyrings/rocm.gpg] https://repo.radeon.com/rocm/apt/6.2 $(lsb_release -cs) main
|
||||
EOF
|
||||
echo -e 'Package: *\nPin: release o=repo.radeon.com\nPin-Priority: 600' | sudo tee /etc/apt/preferences.d/rocm-pin-600
|
||||
sudo apt -qq update || true
|
||||
sudo apt-get install -y --no-install-recommends libclang-20-dev comgr
|
||||
- name: Regenerate autogen files
|
||||
run: |
|
||||
rm tinygrad/runtime/autogen/comgr.py
|
||||
python3 -c "from tinygrad.runtime.autogen import comgr"
|
||||
- name: Check for differences
|
||||
run: |
|
||||
if ! git diff --quiet; then
|
||||
git diff
|
||||
git diff > autogen-comgr2.patch
|
||||
echo "Autogen mismatch detected. Patch available at: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}#artifacts"
|
||||
exit 1
|
||||
fi
|
||||
- name: Upload patch artifact
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: autogen-comgr2-patch
|
||||
path: autogen-comgr2.patch
|
||||
|
||||
+154
-146
@@ -82,13 +82,16 @@ jobs:
|
||||
# pytest -nauto --durations=20
|
||||
|
||||
llmbenchmark:
|
||||
name: LLM (DEV=${{ matrix.dev }})
|
||||
name: Benchmark ${{ matrix.model }} (DEV=${{ matrix.dev }})
|
||||
runs-on: [self-hosted, "${{ matrix.dev == 'METAL' && 'macOS' || matrix.dev == 'AMD' && 'tinybox' || 'tinyboxgreen' }}"]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
dev: ['METAL', 'AMD', 'NV']
|
||||
timeout-minutes: 60
|
||||
model: ['llama3.2:3b-f16', 'qwen3.8:27b', 'olmoe']
|
||||
# qwen3.8:27b doesn't fit on mac
|
||||
exclude: [{ dev: 'METAL', model: 'qwen3.8:27b' }, { dev: 'AMD', model: 'olmoe' }, { dev: 'NV', model: 'olmoe' }]
|
||||
timeout-minutes: 15
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
@@ -102,16 +105,11 @@ jobs:
|
||||
- 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
|
||||
./extra/hcq/hcq_smi.py amd rmmod --expect
|
||||
./extra/hcq/hcq_smi.py amd kill_pids --sudoless
|
||||
- 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
|
||||
run: lsof -tQ /dev/nvidia* | { xargs -r kill -9 || true; }
|
||||
- name: setup staging db
|
||||
if: github.ref == 'refs/heads/update_benchmark_staging'
|
||||
run: |
|
||||
@@ -119,20 +117,10 @@ jobs:
|
||||
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 llama3.2
|
||||
run: BENCHMARK_LOG=llama32_3b-f16 JITBEAM=2 IGNORE_BEAM_CACHE=1 python3 -m tinygrad.llm -m llama3.2:3b-f16 --benchmark --warmup
|
||||
- name: Run qwen3.6
|
||||
# qwen3.6:35b-a3b doesn't fit on mac
|
||||
if: ${{ matrix.dev != 'METAL' }}
|
||||
run: BENCHMARK_LOG=qwen36_35b-a3b JITBEAM=2 IGNORE_BEAM_CACHE=1 python3 -m tinygrad.llm -m qwen3.6:35b-a3b --benchmark --warmup
|
||||
- name: Run olmoe
|
||||
# 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 ${{ matrix.model }}
|
||||
run: |
|
||||
MODEL=${{ matrix.model }}
|
||||
BENCHMARK_LOG=${MODEL//./} JITBEAM=2 IGNORE_BEAM_CACHE=1 python3 -m tinygrad.llm -m $MODEL --benchmark --warmup
|
||||
- name: Run process replay tests
|
||||
uses: ./.github/actions/process-replay
|
||||
|
||||
@@ -143,7 +131,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
dev: ['METAL', 'AMD', 'NV']
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 10
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
@@ -157,12 +145,11 @@ jobs:
|
||||
- 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
|
||||
./extra/hcq/hcq_smi.py amd rmmod --expect
|
||||
./extra/hcq/hcq_smi.py amd kill_pids --sudoless
|
||||
- name: Setup (NV)
|
||||
if: ${{ matrix.dev == 'NV' }}
|
||||
run: sudo lsof -tQ /dev/nvidia* | { xargs -r sudo kill -9 || true; }
|
||||
run: lsof -tQ /dev/nvidia* | { xargs -r kill -9 || true; }
|
||||
- name: setup staging db
|
||||
if: github.ref == 'refs/heads/update_benchmark_staging'
|
||||
run: |
|
||||
@@ -182,10 +169,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
|
||||
|
||||
@@ -196,7 +179,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
dev: ['AMD', 'NV']
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 5
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
@@ -210,12 +193,11 @@ jobs:
|
||||
- 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
|
||||
./extra/hcq/hcq_smi.py amd rmmod --expect
|
||||
./extra/hcq/hcq_smi.py amd kill_pids --sudoless
|
||||
- name: Setup (NV)
|
||||
if: ${{ matrix.dev == 'NV' }}
|
||||
run: sudo lsof -tQ /dev/nvidia* | { xargs -r sudo kill -9 || true; }
|
||||
run: lsof -tQ /dev/nvidia* | { xargs -r kill -9 || true; }
|
||||
- name: Symlink models and datasets
|
||||
run: |
|
||||
mkdir -p extra/datasets
|
||||
@@ -227,15 +209,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
|
||||
|
||||
@@ -246,7 +221,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
dev: ['METAL', 'AMD', 'NV']
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 15
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
@@ -260,12 +235,11 @@ jobs:
|
||||
- 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
|
||||
./extra/hcq/hcq_smi.py amd rmmod --expect
|
||||
./extra/hcq/hcq_smi.py amd kill_pids --sudoless
|
||||
- name: Setup (NV)
|
||||
if: ${{ matrix.dev == 'NV' }}
|
||||
run: sudo lsof -tQ /dev/nvidia* | { xargs -r sudo kill -9 || true; }
|
||||
run: lsof -tQ /dev/nvidia* | { xargs -r kill -9 || true; }
|
||||
- name: setup staging db
|
||||
if: github.ref == 'refs/heads/update_benchmark_staging'
|
||||
run: |
|
||||
@@ -285,6 +259,59 @@ 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: 20
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup (AMD)
|
||||
if: ${{ matrix.dev == 'AMD' }}
|
||||
run: |
|
||||
./extra/hcq/hcq_smi.py amd rmmod --expect
|
||||
./extra/hcq/hcq_smi.py amd kill_pids --sudoless
|
||||
- name: Setup (NV)
|
||||
if: ${{ matrix.dev == 'NV' }}
|
||||
run: lsof -tQ /dev/nvidia* | { xargs -r 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' }}"]
|
||||
@@ -292,7 +319,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
dev: ['METAL', 'AMD', 'NV']
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 11
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
@@ -305,12 +332,11 @@ jobs:
|
||||
- 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
|
||||
./extra/hcq/hcq_smi.py amd rmmod --expect
|
||||
./extra/hcq/hcq_smi.py amd kill_pids --sudoless
|
||||
- name: Setup (NV)
|
||||
if: ${{ matrix.dev == 'NV' }}
|
||||
run: sudo lsof -tQ /dev/nvidia* | { xargs -r sudo kill -9 || true; }
|
||||
run: lsof -tQ /dev/nvidia* | { xargs -r kill -9 || true; }
|
||||
- name: setup staging db
|
||||
if: github.ref == 'refs/heads/update_benchmark_staging'
|
||||
run: |
|
||||
@@ -372,9 +398,10 @@ jobs:
|
||||
- name: Test benchmark allreduce
|
||||
if: ${{ matrix.dev == 'NV' }}
|
||||
run: python test/external/external_benchmark_multitensor_allreduce.py
|
||||
- name: HEVC Decode Benchmark
|
||||
if: ${{ matrix.dev == 'NV' }}
|
||||
run: VALIDATE=1 MAX_FRAMES=100 ASSERT_FPS=1400 JITBEAM=1 PYTHONPATH=. python3 extra/hevc/decode.py
|
||||
# TODO: HEVC decode timing test
|
||||
# - name: HEVC Decode Benchmark
|
||||
# if: ${{ matrix.dev == 'NV' }}
|
||||
# run: IGNORE_BEAM_CACHE=1 VALIDATE=1 MAX_FRAMES=100 ASSERT_FPS=1400 JITBEAM=1 PYTHONPATH=. python3 extra/hevc/decode.py
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: ${{ matrix.dev != 'AMD' }}
|
||||
with:
|
||||
@@ -387,7 +414,7 @@ jobs:
|
||||
testusbgpu:
|
||||
name: UsbGPU Benchmark
|
||||
runs-on: [self-hosted, macOS]
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 3
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
@@ -402,32 +429,64 @@ jobs:
|
||||
rm -f /tmp/staging.db /tmp/staging.db-shm /tmp/staging.db-wal
|
||||
- name: Kill stale pids
|
||||
run: |
|
||||
PYTHONPATH=. ./extra/hcq/hcq_smi.py amd kill_pids
|
||||
PYTHONPATH=. ./extra/hcq/hcq_smi.py nv kill_pids
|
||||
# since sudo is required for usbgpu on macos, do not write bytecode, as some of the files are owned by root
|
||||
./extra/hcq/hcq_smi.py amd kill_pids --sudoless
|
||||
./extra/hcq/hcq_smi.py nv kill_pids --sudoless
|
||||
- name: UsbGPU boot time
|
||||
run: sudo -E PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=. GMMU=0 DEBUG=2 AM_RESET=1 DEV=USB+AMD time python3.11 test/test_tiny.py TestTiny.test_plus
|
||||
run: GMMU=0 DEBUG=2 AM_RESET=1 DEV=USB+AMD time python3.11 test/test_tiny.py TestTiny.test_plus
|
||||
- name: UsbGPU tiny tests
|
||||
run: sudo -E PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=. GMMU=0 DEV=USB+AMD python3.11 test/test_tiny.py
|
||||
run: 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
|
||||
#- name: UsbGPU openpilot test
|
||||
# run: sudo -E PYTHONPATH=. GMMU=0 DEV=USB+AMD GRAPH_ONE_KERNEL=1 python3.11 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/9118973ed03c1ae1d40cf69a29507ec2cc78efd7/selfdrive/modeld/models/supercombo.onnx
|
||||
- name: UsbGPU (USB4/TB) install script
|
||||
run: PYTHONPATH=. sh extra/setup_tinygpu_osx.sh
|
||||
- name: UsbGPU (USB4/TB) boot time
|
||||
run: PYTHONPATH=. DEBUG=3 DEV=PCI+NV:NAK time python3.11 test/test_tiny.py TestTiny.test_plus
|
||||
- name: UsbGPU (USB4/TB) tiny tests
|
||||
run: PYTHONPATH=. DEV=PCI+NV:NAK python3.11 test/test_tiny.py
|
||||
run: SIZE=64000000 PYTHONPATH=. GMMU=0 DEV=USB+AMD python3.11 test/external/external_test_usb_asm24.py
|
||||
|
||||
testcommalatest:
|
||||
name: comma Benchmark (0.11.2)
|
||||
runs-on: [self-hosted, Linux, comma]
|
||||
timeout-minutes: 12
|
||||
testcomma:
|
||||
strategy:
|
||||
matrix:
|
||||
dev: ['QCOM', 'QCOM:IR3']
|
||||
version: ['0.11.0', '0.11.2']
|
||||
model: ['vision', 'policy', 'supercombo', 'dmonitoring']
|
||||
# exclude non-existent models
|
||||
exclude: [{ version: '0.11.0', model: supercombo }, { version: '0.11.2', model: vision }, { version: '0.11.2', model: policy }]
|
||||
include:
|
||||
- version: '0.11.0'
|
||||
model: vision
|
||||
url: https://github.com/commaai/openpilot/raw/v0.11.0/selfdrive/modeld/models/driving_vision.onnx
|
||||
timing: 18
|
||||
- version: '0.11.0'
|
||||
model: policy
|
||||
url: https://github.com/commaai/openpilot/raw/v0.11.0/selfdrive/modeld/models/driving_policy.onnx
|
||||
timing: 3.4
|
||||
- version: '0.11.0'
|
||||
model: dmonitoring
|
||||
url: https://github.com/commaai/openpilot/raw/v0.11.0/selfdrive/modeld/models/dmonitoring_model.onnx
|
||||
timing: 13
|
||||
- version: '0.11.2'
|
||||
model: supercombo
|
||||
url: https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/433f85f956837606ad1f1cbee4aa7e2158ad23c768dea914b20436c97232741b
|
||||
timing: 28
|
||||
- dev: QCOM:IR3
|
||||
version: '0.11.2'
|
||||
model: supercombo
|
||||
timing: 29
|
||||
- version: '0.11.2'
|
||||
model: dmonitoring
|
||||
url: https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/3e7b31dfbc0a5234f1baf196513b77fc6af12204b8a8ffe8ee0417e48352f316
|
||||
timing: 12.5
|
||||
# IR3 dmonitoring is slightly slower
|
||||
- dev: QCOM:IR3
|
||||
model: dmonitoring
|
||||
timing: 13.0
|
||||
fail-fast: false
|
||||
name: openpilot ${{ matrix.version }} compile3 ${{ matrix.model }} (DEV=${{ matrix.dev }})
|
||||
runs-on: [self-hosted, Linux, comma4]
|
||||
timeout-minutes: 5
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
ASSERT_MIN_STEP_TIME: ${{ matrix.timing }}
|
||||
BENCHMARK_LOG: ${{ matrix.dev == 'QCOM:IR3' && 'ir3_' || '' }}openpilot_${{ matrix.version }}_${{ matrix.model }}
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
@@ -438,45 +497,10 @@ 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: openpilot compile3 0.11.2 supercombo
|
||||
run: BENCHMARK_LOG=openpilot_0_11_2_supercombo PYTHONPATH="." ASSERT_MIN_STEP_TIME=26 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/433f85f956837606ad1f1cbee4aa7e2158ad23c768dea914b20436c97232741b
|
||||
- name: openpilot compile3 0.11.2 supercombo (from pickle)
|
||||
run: BENCHMARK_LOG=openpilot_0_11_2_supercombo_run_pickle RUN_PICKLE=1 PYTHONPATH="." ASSERT_MIN_STEP_TIME=26 DEV=QCOM taskset -c 4-7 python3 examples/openpilot/compile3.py
|
||||
- name: IR3 openpilot compile3 0.11.2 supercombo
|
||||
run: BENCHMARK_LOG=ir3_openpilot_0_11_2_supercombo PYTHONPATH="." ASSERT_MIN_STEP_TIME=41 DEV=QCOM:IR3 FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/433f85f956837606ad1f1cbee4aa7e2158ad23c768dea914b20436c97232741b
|
||||
- name: openpilot compile3 0.11.2 dmonitoring
|
||||
run: BENCHMARK_LOG=openpilot_0_11_2_dmonitoring PYTHONPATH="." ASSERT_MIN_STEP_TIME=11 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/3e7b31dfbc0a5234f1baf196513b77fc6af12204b8a8ffe8ee0417e48352f316
|
||||
- name: Run process replay tests
|
||||
uses: ./.github/actions/process-replay
|
||||
|
||||
testcommaold:
|
||||
name: comma Benchmark (0.11.0)
|
||||
runs-on: [self-hosted, Linux, comma]
|
||||
timeout-minutes: 12
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
- name: setup staging db
|
||||
if: github.ref == 'refs/heads/update_benchmark_staging'
|
||||
run: |
|
||||
echo "CACHEDB=/tmp/staging.db" >> $GITHUB_ENV
|
||||
rm -f /tmp/staging.db /tmp/staging.db-shm /tmp/staging.db-wal
|
||||
- name: reset process replay
|
||||
run: test/external/process_replay/reset.py
|
||||
- name: openpilot compile3 0.11.0 driving_vision
|
||||
run: BENCHMARK_LOG=openpilot_0_11_0_vision PYTHONPATH="." ASSERT_MIN_STEP_TIME=17 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/v0.11.0/selfdrive/modeld/models/driving_vision.onnx
|
||||
- name: openpilot compile3 0.11.0 driving_vision (from pickle)
|
||||
run: BENCHMARK_LOG=openpilot_0_11_0_vision_run_pickle RUN_PICKLE=1 PYTHONPATH="." ASSERT_MIN_STEP_TIME=17 DEV=QCOM taskset -c 4-7 python3 examples/openpilot/compile3.py
|
||||
- name: IR3 openpilot compile3 0.11.0 driving_vision
|
||||
run: BENCHMARK_LOG=ir3_openpilot_0_11_0_vision PYTHONPATH="." ASSERT_MIN_STEP_TIME=18 DEV=QCOM:IR3 FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/v0.11.0/selfdrive/modeld/models/driving_vision.onnx
|
||||
- name: openpilot compile3 0.11.0 driving_policy
|
||||
run: BENCHMARK_LOG=openpilot_0_11_0_policy PYTHONPATH="." ASSERT_MIN_STEP_TIME=3.2 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/v0.11.0/selfdrive/modeld/models/driving_policy.onnx
|
||||
- name: openpilot compile3 0.11.0 dmonitoring
|
||||
run: BENCHMARK_LOG=openpilot_0_11_0_dmonitoring PYTHONPATH="." ASSERT_MIN_STEP_TIME=11 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/v0.11.0/selfdrive/modeld/models/dmonitoring_model.onnx
|
||||
- name: compile
|
||||
run: FLOAT16=1 IMAGE=1 taskset -c 4-7 python3 examples/openpilot/compile3.py ${{ matrix.url }} openpilot.pkl
|
||||
- name: run pickle
|
||||
run: BENCHMARK_LOG="${BENCHMARK_LOG}_run_pickle" RUN_PICKLE=1 taskset -c 4-7 python3 examples/openpilot/compile3.py - openpilot.pkl
|
||||
- name: Run process replay tests
|
||||
uses: ./.github/actions/process-replay
|
||||
|
||||
@@ -489,15 +513,6 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
- name: setup staging db
|
||||
if: github.ref == 'refs/heads/update_benchmark_staging'
|
||||
run: |
|
||||
echo "CACHEDB=/tmp/staging.db" >> $GITHUB_ENV
|
||||
rm -f /tmp/staging.db /tmp/staging.db-shm /tmp/staging.db-wal
|
||||
- name: reset process replay
|
||||
run: test/external/process_replay/reset.py
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
- name: setup staging db
|
||||
@@ -510,8 +525,8 @@ jobs:
|
||||
- name: benchmark MobileNetV2 on DSP
|
||||
run: |
|
||||
# generate quantized weights
|
||||
ln -s /data/home/tiny/tinygrad/extra/datasets/imagenet extra/datasets/imagenet
|
||||
ln -s /data/home/tiny/tinygrad/testsig-*.so .
|
||||
ln -s ~/tinygrad/extra/datasets/imagenet extra/datasets/imagenet
|
||||
ln -s ~/tinygrad/testsig-*.so .
|
||||
PYTHONPATH=. DEV=CPU QUANT=1 CNT=0 python3 examples/test_onnx_imagenet.py https://github.com/xamcat/mobcat-samples/raw/refs/heads/master/onnx_runtime/InferencingSample/InferencingSample/mobilenetv2-7.onnx /tmp/model.quant.onnx
|
||||
# benchmark on DSP with NOOPT=1, the devectorizer has issues
|
||||
PYTHONPATH=. DEV=DSP NOOPT=1 CNT=2 DEBUG=2 python3 examples/test_onnx_imagenet.py /tmp/model.quant.onnx
|
||||
@@ -521,7 +536,7 @@ jobs:
|
||||
testcommausbgpubenchmark:
|
||||
name: UsbGPU Benchmark (comma)
|
||||
runs-on: [self-hosted, Linux, comma4]
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 14
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
@@ -541,7 +556,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
|
||||
|
||||
driverbenchmarks:
|
||||
name: PCI Driver Benchmark (DEV=${{ matrix.dev }})
|
||||
@@ -550,7 +565,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
dev: ['AMD', 'NV']
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 5
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -e -o pipefail {0}
|
||||
@@ -562,9 +577,8 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
- 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 --expect
|
||||
./extra/hcq/hcq_smi.py ${{ matrix.dev }} kill_pids --sudoless
|
||||
mkdir -p extra/datasets
|
||||
ln -s /raid/datasets/imagenet extra/datasets/imagenet
|
||||
- name: setup staging db
|
||||
@@ -599,34 +613,28 @@ jobs:
|
||||
run: |
|
||||
GRAPH_ONE_KERNEL=1 NSZ=8192 python3 test/speed/external_test_copy_speed.py TestCopySpeed.testCopyDefaulttoCPUJit
|
||||
GRAPH_ONE_KERNEL=1 NSZ=8192 python3 test/speed/external_test_copy_speed.py TestCopySpeed.testCopyCPUtoDefaultJit
|
||||
# TODO: HEVC decode timing test
|
||||
# - name: HEVC Decode Benchmark
|
||||
# if: ${{ matrix.dev == 'NV' }}
|
||||
# run: IGNORE_BEAM_CACHE=1 VALIDATE=1 MAX_FRAMES=100 ASSERT_FPS=1400 JITBEAM=1 PYTHONPATH=. python3 extra/hevc/decode.py
|
||||
- name: Run 10 MLPerf ResNet50 training steps (1 gpu)
|
||||
if: ${{ matrix.dev == 'NV' }}
|
||||
run: BENCHMARK_LOG=resnet_10steps MNISTMOCK=1 DEFAULT_FLOAT=HALF BENCHMARK=10 BS=256 GPUS=1 MODEL=resnet python3 examples/mlperf/model_train.py
|
||||
- name: Run 10 MLPerf Bert training steps (1 gpu)
|
||||
# TODO: remove BERT_LAYERS once scheduler is fast
|
||||
run: BENCHMARK_LOG=bert_10steps CAPTURE_PROCESS_REPLAY=0 DEFAULT_FLOAT=HALF BENCHMARK=10 BS=66 GPUS=1 BERT_LAYERS=2 MODEL=bert python3 examples/mlperf/model_train.py
|
||||
- name: Remote
|
||||
run: |
|
||||
pkill -f 'extra/remote/serve.py' || true
|
||||
PYTHONPATH=. python3 extra/remote/serve.py 6482 &
|
||||
sleep 1
|
||||
DEBUG=2 PYTHONPATH=. REMOTE=127.0.0.1:6482 AM_RESET=1 python3 test/test_tiny.py
|
||||
if [[ "${{ matrix.dev }}" == "AMD" ]]; then
|
||||
DEBUG=2 PYTHONPATH=. REMOTE=127.0.0.1:6482 AM_RESET=1 AMD_AQL=1 python3 test/test_tiny.py
|
||||
fi
|
||||
pkill -f 'extra/remote/serve.py' || true
|
||||
- name: Run process replay tests
|
||||
uses: ./.github/actions/process-replay
|
||||
|
||||
llvmspeed:
|
||||
name: LLVM Speed
|
||||
runs-on: [self-hosted, Linux, tinyboxrandom]
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 10
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
- name: Speed Test
|
||||
run: DEV=CPU:LLVM THREADS=0 python3 test/speed/external_test_speed_v_torch.py
|
||||
run: DEV=CPU:LLVM python3 test/speed/external_test_speed_v_torch.py
|
||||
- name: Speed Test (BEAM=2)
|
||||
run: BEAM=2 DEV=CPU:LLVM THREADS=0 python3 test/speed/external_test_speed_v_torch.py
|
||||
run: IGNORE_BEAM_CACHE=1 BEAM=2 DEV=CPU:LLVM python3 test/speed/external_test_speed_v_torch.py
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -36,6 +36,8 @@ jobs:
|
||||
deps: testing_unit
|
||||
- name: Run unit tests
|
||||
run: DEV=METAL python -m pytest -n=auto test/unit/ --durations=20
|
||||
- name: Run opt tests
|
||||
run: DEV=METAL python -m pytest -n=auto test/opt --durations=20
|
||||
- name: Test tensor core ops (fake)
|
||||
run: DEV=METAL DEBUG=3 TC=2 python test/backend/test_ops.py TestOps.test_gemm
|
||||
- name: Test tensor core ops (real)
|
||||
@@ -68,17 +70,19 @@ jobs:
|
||||
- name: Run pytest (amd)
|
||||
env:
|
||||
DEV: MOCKKFD+AMD
|
||||
HCQ_RUNTIME_DEV: PYTHON
|
||||
FORWARD_ONLY: 1
|
||||
run: |
|
||||
python3 -m pytest -n=auto test/device/test_hcq.py test/test_tiny.py --durations=20
|
||||
python3 -m pytest -n=auto test/device/test_hcq2.py test/test_tiny.py --durations=20
|
||||
- name: Run pytest (ptx)
|
||||
env:
|
||||
DEV: "MOCK+NV:PTX"
|
||||
HCQ_RUNTIME_DEV: PYTHON
|
||||
FORWARD_ONLY: 1
|
||||
# TODO: failing due to library loading error
|
||||
CAPTURE_PROCESS_REPLAY: 0
|
||||
run: |
|
||||
python3 -m pytest -n=auto test/device/test_hcq.py test/test_tiny.py \
|
||||
python3 -m pytest -n=auto test/device/test_hcq2.py test/test_tiny.py \
|
||||
test/testextra/test_hevc.py::TestHevc::test_hevc_decode_compile --durations=20
|
||||
- name: Run process replay tests
|
||||
uses: ./.github/actions/process-replay
|
||||
@@ -166,7 +170,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
|
||||
@@ -179,35 +183,3 @@ jobs:
|
||||
- name: Run test_tiny
|
||||
shell: bash
|
||||
run: python -m pytest -n=auto test/test_tiny.py --durations=20
|
||||
|
||||
|
||||
qcomclcompiletests:
|
||||
name: Compile-only (QCOM CL)
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup Environment
|
||||
uses: ./.github/actions/setup-tinygrad
|
||||
with:
|
||||
key: compile-qcomcl
|
||||
deps: testing_unit
|
||||
tinydreno: 'true'
|
||||
- name: Set env
|
||||
shell: bash
|
||||
run: printf "DEV=NULL:QCOMCL:a630\nNULL_ALLOW_COPYOUT=1" >> $GITHUB_ENV
|
||||
- name: Run test_ops
|
||||
shell: bash
|
||||
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
|
||||
- name: Run test_ops (IMAGE)
|
||||
shell: bash
|
||||
env:
|
||||
IMAGE: 1
|
||||
DEV: "NULL:QCOMCL:a630,IMAGE_PITCH_ALIGNMENT=64"
|
||||
run: |
|
||||
DEBUG=4 python test/backend/test_ops.py TestOps.test_gemm | grep read_imagef
|
||||
python -m pytest -n=auto test/backend/test_ops.py --durations=20
|
||||
|
||||
@@ -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:
|
||||
|
||||
+79
-85
@@ -21,7 +21,7 @@ concurrency:
|
||||
jobs:
|
||||
docs:
|
||||
name: Docs
|
||||
runs-on: &linux ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
|
||||
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: 10
|
||||
env:
|
||||
CHECK_OOB: 0
|
||||
@@ -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
|
||||
@@ -61,7 +60,7 @@ jobs:
|
||||
|
||||
torchbackend:
|
||||
name: Torch Backend Tests
|
||||
runs-on: *linux
|
||||
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
|
||||
@@ -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,9 +82,26 @@ 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: *linux
|
||||
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
|
||||
@@ -126,7 +139,7 @@ jobs:
|
||||
|
||||
linter:
|
||||
name: Linters
|
||||
runs-on: *linux
|
||||
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: 10
|
||||
|
||||
steps:
|
||||
@@ -157,7 +170,7 @@ jobs:
|
||||
|
||||
nulltest:
|
||||
name: Null Tests
|
||||
runs-on: *linux
|
||||
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:
|
||||
@@ -191,7 +204,7 @@ jobs:
|
||||
|
||||
unittest:
|
||||
name: Unit Tests
|
||||
runs-on: *linux
|
||||
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:
|
||||
@@ -220,7 +233,7 @@ jobs:
|
||||
- name: Run process replay tests
|
||||
uses: ./.github/actions/process-replay
|
||||
- name: Repo line count <= 26000 lines
|
||||
run: MAX_LINE_COUNT=26000 python sz.py
|
||||
run: MAX_LINE_COUNT=26500 python sz.py
|
||||
|
||||
spec:
|
||||
strategy:
|
||||
@@ -228,7 +241,7 @@ jobs:
|
||||
matrix:
|
||||
group: [1, 2]
|
||||
name: SPEC=2 (${{ matrix.group }})
|
||||
runs-on: *linux
|
||||
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
|
||||
@@ -240,11 +253,11 @@ jobs:
|
||||
deps: testing_unit
|
||||
llvm: 'true'
|
||||
- name: Test SPEC=2
|
||||
run: SPEC=2 pytest --maxfail=10 -n auto --durations=30 test/unit test/backend test/opt --ignore test/backend/test_custom_kernel.py --ignore test/unit/test_hashing.py -k "not test_setitem_big" -k "not test_conv2d_ceildiv_edge_case" --splits 2 --group ${{ matrix.group }}
|
||||
run: SPEC=2 pytest --maxfail=10 -n auto --durations=30 test/unit test/backend test/opt --ignore test/backend/test_custom_kernel.py --ignore test/unit/test_hashing.py --splits 2 --group ${{ matrix.group }}
|
||||
|
||||
fuzzing:
|
||||
name: Fuzzing
|
||||
runs-on: *linux
|
||||
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: 10
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -260,7 +273,7 @@ jobs:
|
||||
|
||||
testopenclimage:
|
||||
name: CL IMAGE Tests
|
||||
runs-on: *linux
|
||||
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
|
||||
@@ -280,7 +293,7 @@ jobs:
|
||||
|
||||
testopenpilot:
|
||||
name: openpilot Compile Tests
|
||||
runs-on: *linux
|
||||
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
|
||||
@@ -309,7 +322,7 @@ jobs:
|
||||
|
||||
testonnxcpu:
|
||||
name: ONNX (CPU) Tests
|
||||
runs-on: *linux
|
||||
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: 20
|
||||
|
||||
steps:
|
||||
@@ -328,7 +341,7 @@ jobs:
|
||||
|
||||
testoptim:
|
||||
name: Optimization Tests
|
||||
runs-on: *linux
|
||||
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: 20
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -360,7 +373,7 @@ jobs:
|
||||
|
||||
testllm:
|
||||
name: Test LLM
|
||||
runs-on: *linux
|
||||
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
|
||||
env:
|
||||
CHECK_OOB: 0
|
||||
@@ -377,17 +390,17 @@ jobs:
|
||||
run: |
|
||||
parallel --link --tagstring '[{1}]' '{2}' \
|
||||
::: llama 'llama q4' qwen3.5 qwen \
|
||||
::: $'echo "What\'s a male chicken called? Answer with only one word." | python3 -m tinygrad.llm --model llama3.2:1b | tee /dev/stderr | grep -i rooster' \
|
||||
$'echo "What\'s a male chicken called? Answer with only one word." | python3 -m tinygrad.llm --model llama3.2:1b-q4 | tee /dev/stderr | grep -i rooster' \
|
||||
$'echo "What\'s a male chicken called? Answer with only one word." | python3 -m tinygrad.llm --model qwen3.5:0.8b | tee /dev/stderr | grep -i rooster' \
|
||||
$'echo "What\'s a female chicken called? Answer with only one word." | python3 -m tinygrad.llm --model qwen3:0.6b | tee /dev/stderr | grep -i hen'
|
||||
::: $'echo "What\'s a male chicken called? Answer with only one word." | python3 -m tinygrad.llm --no_chat_template --model llama3.2:1b | tee /dev/stderr | grep -i rooster' \
|
||||
$'echo "What\'s a male chicken called? Answer with only one word." | python3 -m tinygrad.llm --no_chat_template --model llama3.2:1b-q4 | tee /dev/stderr | grep -i rooster' \
|
||||
$'echo "What\'s a male chicken called? Answer with only one word." | python3 -m tinygrad.llm --no_chat_template --model qwen3.5:0.8b | tee /dev/stderr | grep -i rooster' \
|
||||
$'echo "What\'s a female chicken called? Answer with only one word." | python3 -m tinygrad.llm --no_chat_template --model qwen3:0.6b | tee /dev/stderr | grep -i hen'
|
||||
# NOTE: qwen is dumb and only knows about female chickens
|
||||
|
||||
# ****** Models Tests ******
|
||||
|
||||
testmodels:
|
||||
name: Models
|
||||
runs-on: *linux
|
||||
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
|
||||
@@ -407,7 +420,7 @@ jobs:
|
||||
|
||||
testdsp:
|
||||
name: Linux (DSP)
|
||||
runs-on: *linux
|
||||
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
|
||||
@@ -435,7 +448,7 @@ jobs:
|
||||
- 'WEBGPU'
|
||||
|
||||
name: Linux (DEV=${{ matrix.dev }})
|
||||
runs-on: *linux
|
||||
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: 20
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -461,10 +474,11 @@ jobs:
|
||||
|
||||
testamdasm:
|
||||
name: AMD ASM IDE
|
||||
runs-on: *linux
|
||||
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: 20
|
||||
env:
|
||||
DEV: MOCKKFD+AMD
|
||||
HCQ_RUNTIME_DEV: PYTHON
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
@@ -491,7 +505,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: SQTT_BUFFER_SIZE=16 VIZ=-2 python3 -m pytest -n=auto test/amd/test_sqtt_profiler.py
|
||||
- name: Run AMD emulated tests on NULL backend
|
||||
env:
|
||||
AMD: 0
|
||||
@@ -505,35 +519,15 @@ jobs:
|
||||
- name: Run LLVM test
|
||||
run: DEV=MOCKKFD+AMD:LLVM python test/device/test_amd_llvm.py
|
||||
|
||||
hcq2:
|
||||
name: hcq2
|
||||
runs-on: *linux
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup Environment
|
||||
uses: ./.github/actions/setup-tinygrad
|
||||
with:
|
||||
key: hcq2
|
||||
deps: testing_unit
|
||||
amd: 'true'
|
||||
- name: Run HCQ2 tests
|
||||
run: HCQ_RUNTIME_DEV=PYTHON HCQ2=1 DEV=MOCKKFD+AMD FORWARD_ONLY=1 PYTHONPATH=. python test/test_tiny.py
|
||||
- name: Run HCQ2 multi-device tests
|
||||
run: |
|
||||
HCQ_RUNTIME_DEV=PYTHON HCQ2=1 DEV=MOCKKFD+AMD FORWARD_ONLY=1 PYTHONPATH=. python test/unit/test_multitensor.py \
|
||||
TestMultiTensor.test_simple_add TestMultiTensor.test_shard_reduce \
|
||||
TestMultiTensor.test_backward_sum TestMultiTensor.test_matmul_shard_0_0
|
||||
- name: Run HCQ2 JIT tests
|
||||
run: HCQ_RUNTIME_DEV=PYTHON HCQ2=1 DEV=MOCKKFD+AMD FORWARD_ONLY=1 PYTHONPATH=. python test/unit/test_jit.py
|
||||
|
||||
testmockam:
|
||||
name: Linux (am)
|
||||
runs-on: *linux
|
||||
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
|
||||
env:
|
||||
DEV: MOCKPCI+AMD
|
||||
HCQ2: 1
|
||||
HCQ_RUNTIME_DEV: PYTHON
|
||||
PYTHONPATH: .
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
@@ -543,20 +537,14 @@ jobs:
|
||||
key: mockam
|
||||
deps: testing_unit
|
||||
amd: 'true'
|
||||
- name: Run test_tiny on MOCKAM
|
||||
run: python test/test_tiny.py
|
||||
- name: Run tests on MOCKAM
|
||||
run: python -m pytest test/test_tiny.py test/unit/test_jit.py
|
||||
- name: Run test_tiny on MOCKUSB
|
||||
run: GMMU=0 DEV=MOCKUSB+AMD python test/test_tiny.py
|
||||
- name: Run test_hcq on MOCKPCI
|
||||
run: python -m pytest test/device/test_hcq.py
|
||||
run: HCQ2=0 GMMU=0 DEV=MOCKUSB+AMD python test/test_tiny.py
|
||||
- name: Run test_hcq2 on MOCKPCI
|
||||
run: python -m pytest test/device/test_hcq2.py
|
||||
- name: Run disk copy tests on MOCKPCI
|
||||
run: python -m pytest test/unit/test_disk_tensor.py -k test_copy_from_disk
|
||||
- name: Run test_tiny on MOCKPCI Remote
|
||||
run: |
|
||||
python extra/remote/serve.py 6667 &
|
||||
sleep 2
|
||||
REMOTE=127.0.0.1:6667 python test/test_tiny.py
|
||||
REMOTE=127.0.0.1:6667 python -m pytest test/unit/test_disk_tensor.py -k test_copy_from_disk; kill %1
|
||||
|
||||
testamd:
|
||||
strategy:
|
||||
@@ -566,11 +554,14 @@ jobs:
|
||||
arch: [gfx1100, gfx1201, gfx950]
|
||||
|
||||
name: Linux (${{ matrix.backend }} ${{ matrix.arch }})
|
||||
runs-on: *linux
|
||||
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
|
||||
env:
|
||||
DEV: MOCKKFD+AMD:${{ matrix.backend == 'amdllvm' && 'LLVM' || '' }}:${{ matrix.arch }}
|
||||
SKIP_SLOW_TEST: 1
|
||||
HCQ2: 1
|
||||
HCQ_RUNTIME_DEV: PYTHON
|
||||
PYTHONPATH: .
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
@@ -587,9 +578,11 @@ jobs:
|
||||
DEBUG=5 FORWARD_ONLY=1 python3 test/test_tiny.py TestTiny.test_plus
|
||||
- name: Run MXFP4 Llama training on NULL backend
|
||||
if: ${{ matrix.backend == 'amd' && matrix.arch == 'gfx950' }}
|
||||
run: PYTHONPATH=. DEV=NULL:HIP:gfx950 MXFP4=1 LLAMA_LAYERS=2 BENCHMARK=3 NULL_ALLOW_COPYOUT=1 NO_HIPCC=1 ROCM_PATH=/opt/rocm JITBEAM=0 examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama31_8b/implementations/tinybox_8xMI350X/profile.sh
|
||||
run: HCQ2=0 PYTHONPATH=. DEV=NULL:HIP:gfx950 MXFP4=1 LLAMA_LAYERS=2 BENCHMARK=3 NULL_ALLOW_COPYOUT=1 NO_HIPCC=1 ROCM_PATH=/opt/rocm JITBEAM=0 examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama31_8b/implementations/tinybox_8xMI350X/profile.sh
|
||||
- name: Run pytest (amd)
|
||||
run: python -m pytest -n=auto test/backend/test_ops.py test/backend/test_dtype.py test/backend/test_dtype_alu.py test/backend/test_linearizer.py test/backend/test_randomness.py test/backend/test_jit.py test/backend/test_graph.py test/backend/test_multitensor.py test/device/test_hcq.py test/external/external_test_am.py test/backend/test_asm_gemm.py::TestAsmGEMM test/opt/test_tensor_cores.py --durations=20
|
||||
run: python -m pytest -n=auto test/backend/test_ops.py test/backend/test_dtype.py test/backend/test_dtype_alu.py test/backend/test_linearizer.py test/backend/test_randomness.py test/backend/test_jit.py test/backend/test_graph.py test/backend/test_multitensor.py test/device/test_hcq2.py test/external/external_test_am.py test/backend/test_asm_gemm.py::TestAsmGEMM --durations=20
|
||||
- name: Run opt tests
|
||||
run: python -m pytest -n=auto test/opt --durations=20
|
||||
- name: Run disk copy tests
|
||||
run: python -m pytest test/unit/test_disk_tensor.py -k test_copy_from_disk
|
||||
- name: Run TRANSCENDENTAL math
|
||||
@@ -604,7 +597,7 @@ jobs:
|
||||
backend: [ptx, nv]
|
||||
|
||||
name: Linux (${{ matrix.backend }})
|
||||
runs-on: *linux
|
||||
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: 20
|
||||
env:
|
||||
FORWARD_ONLY: 1
|
||||
@@ -619,7 +612,7 @@ jobs:
|
||||
cuda: 'true'
|
||||
ocelot: 'true'
|
||||
- name: Set env
|
||||
run: printf "${{ matrix.backend == 'ptx' && 'DEV=MOCK+CUDA:PTX' || matrix.backend == 'nv' && 'DEV=MOCK+NV\nSKIP_SLOW_TEST=1' }}" >> $GITHUB_ENV
|
||||
run: printf "${{ matrix.backend == 'ptx' && 'DEV=MOCK+CUDA:PTX' || matrix.backend == 'nv' && 'DEV=MOCK+NV\nSKIP_SLOW_TEST=1\nHCQ_RUNTIME_DEV=PYTHON' }}" >> $GITHUB_ENV
|
||||
- name: Check Device.DEFAULT and print some source
|
||||
run: |
|
||||
python3 -c "from tinygrad import Device; assert Device.DEFAULT in ['CUDA','NV'], Device.DEFAULT"
|
||||
@@ -638,10 +631,17 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
backend: [ir3, nak]
|
||||
name: Compile-only (${{ matrix.backend }})
|
||||
runs-on: *linux
|
||||
dev:
|
||||
- 'NULL:IR3:a630'
|
||||
- 'NULL:QCOMCL:a630'
|
||||
- 'NULL:NAK:sm_120'
|
||||
name: Compile-only (DEV=${{ matrix.dev }})
|
||||
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
|
||||
env:
|
||||
NULL_ALLOW_COPYOUT: 1
|
||||
DEV: ${{ matrix.dev }}${{ contains(matrix.dev, 'a630') && ',IMAGE_PITCH_ALIGNMENT=64' || '' }}
|
||||
IMAGE: ${{ contains(matrix.dev, 'a630') && '1' || '0' }}
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
@@ -650,21 +650,15 @@ jobs:
|
||||
with:
|
||||
key: compile-${{ matrix.backend }}
|
||||
deps: "testing_unit mesa"
|
||||
- name: Set env
|
||||
qemu: ${{ contains(matrix.dev, 'QCOMCL') }}
|
||||
- name: Test IMAGE
|
||||
shell: bash
|
||||
run: printf "NULL_ALLOW_COPYOUT=1\n${{ matrix.backend == 'ir3' && 'DEV=NULL:IR3:a630' || matrix.backend == 'nak' && 'DEV=NULL:NAK:sm_120' }}" >> $GITHUB_ENV
|
||||
if: contains(matrix.dev, 'a630')
|
||||
run: DEBUG=7 python3 test/backend/test_ops.py TestOps.test_gemm | grep isam
|
||||
- name: Run test_ops
|
||||
shell: bash
|
||||
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
|
||||
- name: Run test_ops (IMAGE)
|
||||
if: matrix.backend == 'ir3'
|
||||
shell: bash
|
||||
env:
|
||||
IMAGE: 1
|
||||
DEV: "NULL:IR3:a630,IMAGE_PITCH_ALIGNMENT=64"
|
||||
run: |
|
||||
DEBUG=4 python3 test/backend/test_ops.py TestOps.test_gemm | grep image_load
|
||||
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
|
||||
|
||||
@@ -69,3 +69,4 @@ mutants
|
||||
dagre/
|
||||
graphlib/
|
||||
uv.lock
|
||||
pi_session_window0.jsonl
|
||||
|
||||
@@ -28,7 +28,7 @@ repos:
|
||||
pass_filenames: false
|
||||
- id: tests
|
||||
name: comprehensive test suite
|
||||
entry: env OMP_NUM_THREADS=1 SKIP_SLOW_TEST=1 PYTHONPATH="." python3 -m pytest -n=6 test/backend/test_ops.py test/backend/test_schedule.py test/unit/test_assign.py test/backend/test_tensor.py test/backend/test_jit.py test/unit/test_schedule_cache.py test/null/test_pattern_matcher.py test/null/test_uop_symbolic.py test/unit/test_helpers.py
|
||||
entry: env OMP_NUM_THREADS=1 SKIP_SLOW_TEST=1 PYTHONPATH="." python3 -m pytest -n=6 test/backend/test_ops.py test/backend/test_schedule.py test/backend/test_assign.py test/backend/test_tensor.py test/backend/test_jit.py test/unit/test_schedule_cache.py test/null/test_pattern_matcher.py test/null/test_uop_symbolic.py test/unit/test_helpers.py
|
||||
language: system
|
||||
always_run: true
|
||||
pass_filenames: false
|
||||
|
||||
@@ -4,3 +4,5 @@
|
||||
- Run `python -m mypy tinygrad/` to typecheck
|
||||
- Run `python -m ruff check .` to lint
|
||||
- Read `./tinygrad/viz/README.md` for profiling and debugging rewrite rules
|
||||
- Do not do amend commits. Always do a new commit if a force push to origin would be required.
|
||||
- tinygrad has user space PCI drivers for AMD and NVIDIA GPUs. Do not insert the unneeded kernel modules.
|
||||
|
||||
@@ -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()
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import os, pytest, signal, threading
|
||||
|
||||
@pytest.hookimpl(wrapper=True)
|
||||
def pytest_runtest_call(item):
|
||||
t = threading.Timer(int(os.getenv("TEST_TIMEOUT", 300)), os.kill, args=(os.getpid(), signal.SIGABRT))
|
||||
t = threading.Timer(int(os.getenv("TEST_TIMEOUT", 120)), os.kill, args=(os.getpid(), signal.SIGABRT))
|
||||
t.start()
|
||||
try: yield
|
||||
finally:
|
||||
|
||||
@@ -122,7 +122,7 @@ def example_5_custom_assembly(a:Tensor, correct):
|
||||
offset_dwords = (self.labels[inst._target] - inst._pos - inst.size()) // 4
|
||||
if not -32768 <= offset_dwords <= 32767: raise ValueError(f"branch to '{inst._target}' offset {offset_dwords} exceeds simm16 range")
|
||||
inst.simm16 = offset_dwords
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=x) for x in self.instructions]))))
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=(x, dtypes.void)) for x in self.instructions]))))
|
||||
|
||||
CU_COUNT = 32
|
||||
LANES = 64
|
||||
|
||||
@@ -40,7 +40,3 @@ Then we render the UOps into code with a `Renderer`, then we compile the code to
|
||||
Runtimes are responsible for device-specific interactions. They handle tasks such as initializing devices, allocating memory, loading/launching programs, and more. You can find more information about the runtimes API on the [runtime overview page](runtime.md).
|
||||
|
||||
All runtime implementations can be found in the [runtime directory](https://github.com/tinygrad/tinygrad/tree/master/tinygrad/runtime).
|
||||
|
||||
### HCQ Compatible Runtimes
|
||||
|
||||
HCQ API is a lower-level API for defining runtimes. Interaction with HCQ-compatible devices occurs at a lower level, with commands issued directly to hardware queues. Some examples of such backends are [NV](https://github.com/tinygrad/tinygrad/tree/master/tinygrad/runtime/ops_nv.py) and [AMD](https://github.com/tinygrad/tinygrad/tree/master/tinygrad/runtime/ops_amd.py), which are userspace drivers for NVIDIA and AMD devices respectively. You can find more information about the API on [HCQ overview page](hcq.md)
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
# HCQ Compatible Runtime
|
||||
|
||||
## Overview
|
||||
|
||||
The main aspect of HCQ-compatible runtimes is how they interact with devices. In HCQ, all interactions with devices occur in a hardware-friendly manner using [command queues](#command-queues). This approach allows commands to be issued directly to devices, bypassing runtime overhead such as HIP or CUDA. Additionally, by using the HCQ API, these runtimes can benefit from various optimizations and features, including [HCQGraph](#hcqgraph) and built-in profiling capabilities.
|
||||
|
||||
### Command Queues
|
||||
|
||||
To interact with devices you create a `HWQueue`. Some methods are required, like timestamp and synchronization methods like [signal](#tinygrad.runtime.support.hcq.HWQueue.signal) and [wait](#tinygrad.runtime.support.hcq.HWQueue.wait), while others are dependent on it being a compute or copy queue.
|
||||
|
||||
For example, the following Python code enqueues a wait, execute, and signal command on the HCQ-compatible device:
|
||||
```python
|
||||
HWQueue().wait(signal_to_wait, value_to_wait) \
|
||||
.exec(program, args_state, global_dims, local_dims) \
|
||||
.signal(signal_to_fire, value_to_fire) \
|
||||
.submit(your_device)
|
||||
```
|
||||
|
||||
Each runtime should implement the required functions that are defined in the `HWQueue` classes.
|
||||
|
||||
::: tinygrad.runtime.support.hcq.HWQueue
|
||||
options:
|
||||
members: [
|
||||
"signal",
|
||||
"wait",
|
||||
"timestamp",
|
||||
"bind",
|
||||
"submit",
|
||||
"memory_barrier",
|
||||
"exec",
|
||||
"copy",
|
||||
]
|
||||
show_source: false
|
||||
|
||||
### HCQ Compatible Device
|
||||
|
||||
The `HCQCompiled` class defines the API for HCQ-compatible devices. This class serves as an abstract base class that device-specific implementations should inherit from and implement.
|
||||
|
||||
::: tinygrad.runtime.support.hcq.HCQCompiled
|
||||
options:
|
||||
show_source: false
|
||||
|
||||
#### Signals
|
||||
|
||||
Signals are device-dependent structures used for synchronization and timing in HCQ-compatible devices. They should be designed to record both a `value` and a `timestamp` within the same signal. HCQ-compatible backend implementations should use `HCQSignal` as a base class.
|
||||
|
||||
::: tinygrad.runtime.support.hcq.HCQSignal
|
||||
options:
|
||||
members: [value, timestamp, wait]
|
||||
show_source: false
|
||||
|
||||
The following Python code demonstrates the usage of signals:
|
||||
|
||||
```python
|
||||
signal = your_device.new_signal(value=0)
|
||||
|
||||
HWQueue().timestamp(signal) \
|
||||
.signal(signal, value_to_fire) \
|
||||
.submit(your_device)
|
||||
|
||||
signal.wait(value_to_fire)
|
||||
signaled_value = signal.value # should be the same as `value_to_fire`
|
||||
timestamp = signal.timestamp
|
||||
```
|
||||
|
||||
##### Synchronization signals
|
||||
|
||||
Each HCQ-compatible device must allocate two signals for global synchronization purposes. These signals are passed to the `HCQCompiled` base class during initialization: an active timeline signal `self.timeline_signal` and a shadow timeline signal `self._shadow_timeline_signal` which helps to handle signal value overflow issues. You can find more about synchronization in the [synchronization section](#synchronization)
|
||||
|
||||
### HCQ Compatible Allocator
|
||||
|
||||
The `HCQAllocator` base class simplifies allocator logic by leveraging [command queues](#command-queues) abstractions. This class efficiently handles copy and transfer operations, leaving only the alloc and free functions to be implemented by individual backends.
|
||||
|
||||
::: tinygrad.runtime.support.hcq.HCQAllocator
|
||||
options:
|
||||
members: [
|
||||
"_alloc",
|
||||
"_free",
|
||||
]
|
||||
show_source: false
|
||||
|
||||
#### HCQ Allocator Result Protocol
|
||||
|
||||
Backends must adhere to the `HCQBuffer` protocol when returning allocation results.
|
||||
|
||||
::: tinygrad.runtime.support.hcq.HCQBuffer
|
||||
options:
|
||||
members: true
|
||||
show_source: false
|
||||
|
||||
### HCQ Compatible Program
|
||||
|
||||
`HCQProgram` is a base class for defining programs compatible with HCQ-enabled devices. It provides a flexible framework for handling different argument layouts (see `HCQArgsState`).
|
||||
|
||||
::: tinygrad.runtime.support.hcq.HCQProgram
|
||||
options:
|
||||
members: true
|
||||
show_source: false
|
||||
|
||||
#### Arguments State
|
||||
|
||||
`HCQArgsState` is a base class for managing the argument state for HCQ programs. Backend implementations should create a subclass of `HCQArgsState` to manage arguments for the given program.
|
||||
|
||||
::: tinygrad.runtime.support.hcq.HCQArgsState
|
||||
options:
|
||||
members: true
|
||||
show_source: false
|
||||
|
||||
**Lifetime**: The `HCQArgsState` is passed to `HWQueue.exec` and is guaranteed not to be freed until `HWQueue.submit` for the same queue is called.
|
||||
|
||||
### Synchronization
|
||||
|
||||
HCQ-compatible devices use a global timeline signal for synchronizing all operations. This mechanism ensures proper ordering and completion of tasks across the device. By convention, `self.timeline_value` points to the next value to signal. So, to wait for all previous operations on the device to complete, wait for `self.timeline_value - 1` value. The following Python code demonstrates the typical usage of signals to synchronize execution to other operations on the device:
|
||||
|
||||
```python
|
||||
HWQueue().wait(your_device.timeline_signal, your_device.timeline_value - 1) \
|
||||
.exec(...)
|
||||
.signal(your_device.timeline_signal, your_device.next_timeline()) \
|
||||
.submit(your_device)
|
||||
|
||||
# Optionally wait for execution
|
||||
your_device.timeline_signal.wait(your_device.timeline_value - 1)
|
||||
```
|
||||
|
||||
## HCQGraph
|
||||
|
||||
[HCQGraph](https://github.com/tinygrad/tinygrad/tree/master/tinygrad/runtime/graph/hcq.py) is a core feature that implements `GraphRunner` for HCQ-compatible devices. `HCQGraph` builds static `HWQueue` for all operations per device. To optimize enqueue time, only the necessary parts of the queues are updated for each run using the symbolic variables, avoiding a complete rebuild.
|
||||
Optionally, queues can implement a `bind` API, which allows further optimization by eliminating the need to copy the queues into the device ring.
|
||||
@@ -22,18 +22,13 @@ The `Compiled` class is responsible for initializing and managing a device.
|
||||
|
||||
### Allocator
|
||||
|
||||
The `Allocator` class is responsible for managing memory on the device. There is also a version called the `LRUAllocator`, which caches allocated buffers to optimize performance.
|
||||
The `Allocator` class manages memory on the device and caches allocated buffers for reuse.
|
||||
|
||||
::: tinygrad.device.Allocator
|
||||
options:
|
||||
members: true
|
||||
show_source: false
|
||||
|
||||
::: tinygrad.device.LRUAllocator
|
||||
options:
|
||||
members: true
|
||||
show_source: false
|
||||
|
||||
### Program
|
||||
|
||||
The `Program` class is created for each loaded program. It is responsible for executing the program on the device. As an example, here is a `CPUProgram` implementation which loads program and runs it.
|
||||
|
||||
@@ -52,7 +52,7 @@ In `kernel.py` we have a set of `OptOps`, these control the parameters of the sp
|
||||
|
||||
The main bottleneck in most kernels is accessing memory. In a freshman algorithms class, you'll learn about cache aware matrix multiplication, and this is all forms of that. While the same math is run, the order in which you run it can have large impacts on the speed depending on if the data you are loading. OptOps will change this order.
|
||||
|
||||
Memory, even cache, is often much slower than accessing the register file. The amount of times data is used in math is called the "arithmetic intensity". For operations like BS=1 GEMV, the arithmetic intensity is 1, but for GEMMs and convs it can be much higher. OptOps like UPCAST and UNROLL can increase this, but be careful of making them too large, as if there's too much register pressure on the GPU the warp scheduler may not be able to fit many warps, or even worse, it could be spilling to local memory.
|
||||
Memory, even cache, is often much slower than accessing the register file. The amount of times data is used in math is called the "arithmetic intensity". For operations like BS=1 GEMV, the arithmetic intensity is 1, but for GEMMs and convs it can be much higher. Splitting an axis into UPCAST can increase this, but be careful of making them too large, as if there's too much register pressure on the GPU the warp scheduler may not be able to fit many warps, or even worse, it could be spilling to local memory.
|
||||
|
||||
4090s have 1 TB/s of ram bandwidth and ~160 TFLOPS of compute, so you need to use each loaded value ~100 times. The L1 cache has around 40 TB/s of bandwidth, so in order to get full compute utilization you need to use each value ~4 times.
|
||||
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
# Kimi K3 on 8× MI350X
|
||||
|
||||
This branch targets text generation directly from the official `moonshotai/Kimi-K3` checkpoint at `/raid/weights/kimi-k3`. It intentionally ignores the vision tower and multimodal projector. The checkpoint remains in its official 96-shard format; the loader never converts, rewrites, or creates a second 1.56 TB copy.
|
||||
|
||||
The checked TP8 layout consumes 196.78 GB (183.27 GiB) of text weights per GPU. The compressed MLA cache adds 28.99 GB (27 GiB) per GPU at the full 1,048,576-token context, leaving approximately 62.23 GB of each nominal 288 GB MI350X for execution buffers and allocator overhead. Start much smaller.
|
||||
|
||||
## Resume the current optimization session
|
||||
|
||||
Work on branch `kimi_slop`. It was cleanly rebased onto `origin/kimi_slop` commit `553bdf68e` on 2026-08-10. The retained K3 commits after that base are `1b3732a6e`, `c6ac4961d`, `1d8620471`, `224bac031`, `f53f0e7e7`, and `2b1b8c22a`; verify the current hashes with `git log` because a later rebase may rewrite them. Before starting any benchmark, check that the worktree is clean and that no model process remains:
|
||||
|
||||
```sh
|
||||
git status --short --branch
|
||||
git log --oneline --decorate -10
|
||||
pgrep -af 'tinygrad.llm.cli|benchmark_kimi_k3' || true
|
||||
```
|
||||
|
||||
The active acceptance target is **more than 100 tok/s decode, more than 200 tok/s prefill, and less than 180 seconds cold startup** on TP8/gfx950. None is currently met. The authoritative official-checkpoint baseline is 389.84 seconds startup, 38.65 tok/s prefill, and 6.25 tok/s decode. The 1.56 TB checkpoint has a measured 6.9 GB/s single-XFS-NVMe read ceiling, giving a roughly 227-second physical cold-read floor; meeting the startup target therefore also requires a faster storage path, not only loader code.
|
||||
|
||||
Use the fake-weight, one-layer loop for development. Do not repeatedly load the official checkpoint while optimizing:
|
||||
|
||||
```sh
|
||||
DEV=AMD python extra/benchmark_kimi_k3_fake.py --mode attention --iterations 30
|
||||
DEV=AMD python extra/benchmark_kimi_k3_fake.py --mode block --iterations 30
|
||||
PROFILE=1 DEV=AMD python extra/benchmark_kimi_k3_fake.py --mode block --iterations 5
|
||||
```
|
||||
|
||||
The clean retained baseline is about 0.630 ms per attention layer and 1.37 ms per complete block, with fake initialization taking about 0.9/2 seconds respectively after the rebase. Since K3 has 93 sequential blocks, a 100 tok/s projection requires at most approximately 0.108 ms per complete block. Only run another 96-shard official validation after a candidate produces a large whole-block gain, remains finite and deterministic, and passes a direct numerical comparison. Test one candidate at a time and remove failed experiments before moving on.
|
||||
|
||||
The immediate bottleneck is launch and synchronization granularity: an official four-token decode profile contained 6,304 kernel events, while packed expert work was only a small fraction of total GPU time. Continue with whole-component or whole-block fusion/replay work, not isolated expert microkernels. The latest fake-loop A/B retested the previously rejected dual gate/up and weighted-down MFMA prototypes: 1.374 ms baseline versus 1.375 ms fused, so they were removed again. A fused whole-core KDA recurrence was also slower in the exact fake attention gate (0.665 versus 0.633 ms) and must not be restored unchanged.
|
||||
|
||||
Preserve these invariants when official validation resumes: use `/raid/weights/kimi-k3` directly, keep all 96 shards byte-for-byte untouched, run only one model process, begin at context 128, verify all eight devices are `gfx950`, and preserve the first failure instead of retrying over it. The most recent preserved official failure from a rejected KDA experiment was the invalid sequence `[198, 163840, 163840, 163840]`; token 163840 is outside the valid vocabulary. The retained path before that experiment produced deterministic in-range replay.
|
||||
|
||||
After a synthetic candidate passes, run correctness and performance in this order: NULL gfx950 compile coverage, focused tests with `-n12` where supported, TP8 fake numerical comparison, official context-128 deterministic tokens, load/prefill/decode timing, and then context admission at 4K, 32K, 131K, and 262K. Run `python -m mypy tinygrad/` and `python -m ruff check .` when those tools are installed. Read `tinygrad/viz/README.md` before inspecting rewrite or device profiles.
|
||||
|
||||
## Before renting the machine
|
||||
|
||||
- Keep the existing 96 shards in `/raid/weights/kimi-k3`; no additional model-sized free space is required. Leave ordinary headroom for logs and temporary files.
|
||||
- The host should have roughly 3 TB RAM, in line with AMD's MI350X platform guidance. The loader itself is streaming and must not need checkpoint-sized RAM.
|
||||
- Use a recent kernel/ROCm stack supported by the host vendor, although tinygrad uses its own AMD userspace driver when `DEV=AMD`.
|
||||
- Clone this exact commit/branch and keep the official checkpoint directory separate from the repository.
|
||||
|
||||
Validate the existing directory without modifying it:
|
||||
|
||||
```sh
|
||||
python examples/kimi_k3_prepare.py /raid/weights/kimi-k3 --context 4096
|
||||
```
|
||||
|
||||
For a metadata-only preflight, place the official `config.json` and `model.safetensors.index.json` in a directory and run:
|
||||
|
||||
```sh
|
||||
python examples/kimi_k3_prepare.py /raid/weights/kimi-k3 --metadata-only
|
||||
```
|
||||
|
||||
## Hardware admission checks
|
||||
|
||||
Do these before loading weights. Stop if any device is missing or reports a different architecture.
|
||||
|
||||
```sh
|
||||
lspci -d 1002:75a0
|
||||
amd-smi list
|
||||
DEV=AMD DEBUG=2 python - <<'PY'
|
||||
from tinygrad import Device
|
||||
for i in range(8):
|
||||
dev = Device[f"AMD:{i}"]
|
||||
print(i, dev.arch)
|
||||
PY
|
||||
```
|
||||
|
||||
Expected architecture: `gfx950` on all eight devices. Then run the small TP8 graph tests:
|
||||
|
||||
```sh
|
||||
python -m pytest test/unit/test_llm_k3.py test/null/test_kimi_k3.py -q -n12
|
||||
DEV=NULL:HIP:gfx950 NULL_ALLOW_COPYOUT=1 python -m pytest \
|
||||
test/unit/test_llm_k3.py::TestKimiK3::test_chunked_recurrent_generate -q -n1
|
||||
DEV=AMD python examples/kimi_k3_smoke.py --devices 8
|
||||
```
|
||||
|
||||
The last two commands are deliberately small. They compile CDNA4 kernels and then exercise the complete TP8 topology without loading the checkpoint.
|
||||
|
||||
For performance iteration, use the exact-width fake-weight harness before another official load:
|
||||
|
||||
```sh
|
||||
DEV=AMD python extra/benchmark_kimi_k3_fake.py --mode attention --iterations 20
|
||||
DEV=AMD python extra/benchmark_kimi_k3_fake.py --mode block --iterations 20
|
||||
```
|
||||
|
||||
It retains K3's 7,168-wide residual stream, 12,288-wide KDA state, 96 heads, 128×128 recurrent matrices, TP8 layouts, top-k 16 routing, packed MXFP4 expert shapes, collectives, and decode JIT, but uses one layer and 16 fake experts. Fake attention weights initialize in about 0.9 seconds and the full block in about 3 seconds. The retained path measured 0.630 ms per fake attention layer and 1.367 ms per complete fake block, projecting about 7.87 tok/s across 93 identical blocks versus 6.25 tok/s for the official heterogeneous model. Treat this as a candidate admission benchmark, not a correctness substitute for official weights.
|
||||
|
||||
## First official load
|
||||
|
||||
Start at a short context so cache allocation and compilation are bounded. The loader reads disk-backed safetensors, TP-shards every destination before realizing it, and drops each source shard/projection immediately afterward.
|
||||
|
||||
```sh
|
||||
/usr/bin/time -v env DEV=AMD DEBUG=1 python -m tinygrad.llm.cli \
|
||||
--model /raid/weights/kimi-k3 --devices 8 --max_context 128 </dev/null 2>&1 | tee kimi-k3-load.log
|
||||
```
|
||||
|
||||
Watch host RAM, swap, HBM, temperatures, and XGMI traffic from a second terminal. Do not start with a one-million-token cache. If loading fails, preserve the first exception and the last loader progress line; do not retry with a larger host-side cache.
|
||||
|
||||
## Correctness and performance sequence
|
||||
|
||||
1. Load with context 128 and generate one token.
|
||||
2. Repeat a fixed prompt twice and confirm token-for-token deterministic greedy output.
|
||||
3. Compare the first several greedy tokens against the official Transformers implementation at temperature zero.
|
||||
4. Benchmark decode only after two warm-up tokens.
|
||||
5. Benchmark prefill at 128, 512, 2K, and 8K tokens. Increase context only while HBM and compile time remain healthy.
|
||||
6. Use `VIZ=1` plus `python -m tinygrad.viz.cli` to inspect kernels; use `VIZ=2` only for short SQTT captures because it adds overhead.
|
||||
|
||||
Example decode benchmark:
|
||||
|
||||
```sh
|
||||
DEV=AMD DEBUG=1 python -m tinygrad.llm.cli --model /raid/weights/kimi-k3 \
|
||||
--devices 8 --max_context 4096 --warmup --benchmark 20
|
||||
```
|
||||
|
||||
## MI350X validation results (2026-08-10)
|
||||
|
||||
The official directory was audited in place: 96 shards, 497,220 indexed tensors, 497,052 language tensors, and 1,560,860,324,864 total bytes. All eight devices reported `gfx950`. No checkpoint file was converted, copied, or modified, and every model run used a single process. The actual text tower is 1,559,965,606,912 bytes; its checked TP8 layout is 196,784,397,312 bytes per GPU.
|
||||
|
||||
The preserved first full-checkpoint error was an `A_log` shape mismatch, `(128,) -> (96, 1)`. K3 stores one decay value per 128-wide KDA channel, not one per head. The loader now keeps this field replicated and applies the official channel-wise broadcast. A numerical unit test covers the distinction from the older head-wise Kimi Linear behavior.
|
||||
|
||||
Load speed was fixed before generation. The original loader opened thousands of individual expert tensors and independently realized eight strided TP slices. The MI350 path now does the following without changing the checkpoint:
|
||||
|
||||
- parses safetensor headers selectively, constructing disk-backed tensors only for the 2,460 non-expert entries consumed by that pass instead of materializing metadata objects for every expert entry twice;
|
||||
- copies contiguous axis-zero shards and replicas directly into their final device buffers;
|
||||
- reads a replicated tensor once and fans it out over XGMI instead of issuing eight identical direct reads (14.31 GB less RAID traffic);
|
||||
- stages an inner-axis tensor once and schedules all eight TP slices together;
|
||||
- reads each layer's contiguous 15.72 GB expert region once, reorders its lexicographically stored expert records on GPU 0, and realizes all six packed/scale destinations together;
|
||||
- retains only final MultiBuffer identities, drops the reorder graph, and flushes the 15.72 GB staging allocation before the next layer.
|
||||
|
||||
One real expert layer leaves exactly 1,965,293,568 bytes resident on each GPU and zero bytes in the GPU-0 allocator cache. Complete context-128 loads measured 527.20 seconds before the final staging cleanup and 490.05/489.59 seconds afterward. Peak host RSS for the unprofiled correctness run was 2.11 GiB with zero swap. RAID variability produced later loads from 489.06 to 532.85 seconds.
|
||||
|
||||
The selective-metadata and bounded-GC pass reduced non-expert loading from 125.77 to 57.77 seconds. A subsequent full official context-128 load completed in 411.49 seconds, 78.10 seconds (16.0%) faster than the 489.59-second baseline. It read the 96 shards in place with 1,049,688 KiB peak host RSS and zero swap; no weight payload was converted, copied, or modified. Direct-I/O probes measured approximately 6.9 GB/s aggregate for both one and eight concurrent 1 GiB reads. At that rate the 1.56 TB checkpoint has a roughly 227-second cold-read lower bound, so this RAID cannot meet a true cold sub-three-minute startup regardless of loader overhead.
|
||||
|
||||
Expert staging graphs are acyclic and are released by reference counting after each layer, so the loader now suppresses unnecessary cyclic-collector scans only around that loop and restores its prior state on every exit. A quiet context-128 load then completed in 391.54 seconds, 30.14 seconds (7.1%) faster than the immediately preceding 421.68-second run, with 1.04 GiB peak RSS and zero swap, although storage variability contributes to run-to-run timing. The host used for these measurements actually mounts `/raid` from one 3.5 TB XFS NVMe, not a multi-drive RAID; shard 28 has 218 extents and live reads fell to roughly 160 MB/s there. This storage layout, plus the physical checkpoint size, remains the limiting cold-start constraint. The weights were not defragmented, copied, or modified.
|
||||
|
||||
The fixed XTML prompt `Reply with exactly: OK` encodes to 93 tokens. After excluding the cold JIT capture from replay comparison, two greedy runs produced the identical eight-token sequence:
|
||||
|
||||
```text
|
||||
[9545, 59991, 10580, 14404, 9545, 59991, 9545, 59991]
|
||||
```
|
||||
|
||||
At context 128, steady prefill was 14.32 seconds (6.49 tok/s) and eight-token decode was 2.27 seconds (3.53 tok/s, 283.3 ms/token). The same first tokens remained stable at every admitted context. These rates are much lower than the planning estimates below and should be treated as the current measured baseline.
|
||||
|
||||
The retained gfx950 serving pass enables the validated wave64 recurrent prefill kernel with 128-token chunks, uses exact BF16 decode projections, combines the routed/shared final TP partials into one collective, and tiles four adjacent packed-expert outputs during multi-token execution. On the same 93-token prompt, two replay trials produced the identical sequence `[198, 92652, 220, 80225]`. Prefill replay measured 2.418--2.482 seconds (37.47--38.46 tok/s), and eight-token decode measured 1.294 seconds (6.18 tok/s, 161.81 ms/token). Peak RSS was 2.77 GiB with zero swap. The packed prefill tile changes floating-point reduction order: direct official-layer comparison against the original kernel had maximum differences of 0.015625 for gate and 0.0078125 for down, and the end-to-end greedy sequence was stable across replay.
|
||||
|
||||
A subsequent gfx950 decode pass split the 7,168-wide replicated BF16 projections across eight waves per 16 output channels and used CDNA4 BF16 MFMA, with one FP32 LDS reduction at the end. It is enabled only for batch-one/token-one replicated projections whose dimensions satisfy the hardware tile; prefill, the FP32 router, and the output-sharded 12,288-wide KDA gate remain unchanged. The official retained path uses it for MLA q-a/kv-a and KDA f-a. Isolated TP8 measurements improved replicated 128/576-output projections by about 16--18%; applying it to the already output-sharded KDA gate was slower and was rejected. Random-shape comparison against the generic graph had maximum/mean absolute BF16 differences of 2.0/0.1114 because the split changes reduction order. Against a serial FP32 accumulation rounded once to BF16, the 7,168-to-1,536 kernel was bit-exact in the tested sample.
|
||||
|
||||
The final official context-128 validation loaded in 389.84 seconds with 2.71 GiB peak RSS and zero swap. Two replay trials produced the identical four-token sequence `[198, 59675, 9817, 12519]`; prefill remained 2.406 seconds (38.65 tok/s), while eight-token decode improved to 1.280 seconds (6.25 tok/s, 160.00 ms/token). A one-wave MFMA variant and a full-wave fused decode recurrence were both rejected: the former delivered 6.02 tok/s, and the latter 6.179 tok/s, while both changed the greedy sequence without a useful speed gain.
|
||||
|
||||
A final load-first experiment increased the disk-to-HBM io_uring queue depth from one to the 32 existing bounded 2 MiB staging buffers. On a direct 1 GiB read from fragmented shard 28 it measured 6.834 GB/s versus 6.832 GB/s for the original path, so the change was rejected. The subsequent unmodified official 96-shard load completed in 389.48 seconds, confirming both the prior result and the single-NVMe lower bound. Peak RSS was 2.75 GiB with zero swap.
|
||||
|
||||
Two direct packed-expert MFMA prototypes were also rejected after that load. A fused gate/up kernel was about 29% faster in isolation at the TP8-local shape, and a routed-down kernel which combined projection, probability weighting, and route reduction measured 1.45 ms versus 2.42 ms in isolation. End-to-end, however, stable replay produced `[198, 2338, 2127, 148297]`, prefill measured 38.87 tok/s, and decode measured 6.263 tok/s. That is indistinguishable from the retained 38.65/6.25 tok/s path while changing floating-point reduction order, so neither kernel was retained.
|
||||
|
||||
A whole-core KDA decode experiment fused convolution, Q/K normalization, channel decay, recurrence, RMS normalization, output gating, and four persistent state updates. Its raw kernel replayed in about 109 microseconds per local KDA layer and matched a one-step synthetic reference within `9.77e-4` output and `8.13e-4` state maximum error. The exact-width fake-layer gate caught that it was slower than the retained attention path (0.665 versus 0.633 ms/layer). The already-running official validation was stopped after its first invalid greedy sequence, `[198, 163840, 163840, 163840]`, where 163840 is outside the checkpoint's vocabulary. The kernel was rejected and removed.
|
||||
|
||||
| Maximum context | Load | Short-prompt replay | Result |
|
||||
|---:|---:|---:|---|
|
||||
| 128 | 489.59s | 14.32s | stable 8-token replay |
|
||||
| 4,096 | 489.06s | 14.32s | stable replay, zero swap |
|
||||
| 32,768 | 532.85s | 14.33s | stable replay, zero swap |
|
||||
| 131,072 | 520.91s | 14.37s | stable first token, zero swap |
|
||||
| 262,144 | 497.34s | 14.41s | stable first token, zero swap |
|
||||
|
||||
These are maximum-context/cache admission tests with the same 93-token prompt, not full-length 32K/131K/262K prefills. The full cache allocation path was exercised, but filling those contexts remains a separate long-running throughput test.
|
||||
|
||||
Runtime profiling bracketed four steady decode tokens. It recorded 6,304 kernel events and about 474--478 ms of summed GPU work across the eight devices inside a roughly 1.5-second profiled wall interval. The packed `mxfp4_expert_linear_wave64` kernels accounted for only about 22.5 ms summed; the largest families were small 1,792-wide reductions. This identifies launch/synchronization granularity as the immediate MI350 bottleneck rather than packed-weight bandwidth. `JIT_BATCH_SIZE=64` produced the same original 3.53 tok/s as 32. A gfx950 fused MXFP8 QDQ experiment was bit-exact but slower on the real device (about 95 microseconds versus 57--64 microseconds), so it was rejected. Combining the routed and shared final TP partials removed one collective per routed decode layer and helped raise unprofiled decode to 6.18 tok/s, but the remaining sequential launch boundaries still dominate.
|
||||
|
||||
The checkpoint's bundled Transformers code was used as the architectural reference for channel decay and tensor mapping. A full independent Transformers/vLLM token comparison was not run on this host because the required `compressed_tensors`/serving backend is not installed; deterministic tinygrad replay and the numerical KDA, loader-layout, NULL gfx950 compile, and real TP8 smoke tests are the completed correctness gates.
|
||||
|
||||
## Known hardware-only gate
|
||||
|
||||
The correctness path now consumes packed MXFP4 expert weights directly on gfx950 with a wave64 software-decode kernel, so it does not create selected-expert BF16 weight expansions. MXFP8 activation quantization is still emulated. tinygrad has gfx950/CDNA4 BF16 and FP8 matrix-core support, but this branch does not yet have a hardware-validated native MXFP4×MXFP8 expert GEMM. Expect the first run to be a correctness bring-up, not production throughput. Capture profiles on MI350X before changing the representation: native FP4 work cannot be validated faithfully on the available gfx1100 cards.
|
||||
|
||||
Recurrent prefill is fused. The gfx950 wave-parallel kernel was compared directly with the portable graph at the official per-GPU shape through 128 tokens: maximum core/state differences remained below `8e-6`/`1e-6`, outputs were finite, and replay was about 2.7 ms versus about 8 ms for the portable kernel in the isolated test. Full K3 therefore uses 128-token recurrent chunks on gfx950. Chunk size remains part of the numerical configuration because different reduction orders can select different final greedy tokens.
|
||||
|
||||
The following serving changes apply to the official K3 path: recurrent-state reset graph capture, direct AMD scalar readback without rebuilding a scheduler graph, materialized gate/up boundaries, separate greedy decode JITs, K3's uncorrected routed probability semantics, gfx950 KDA Q/K/V and exact BF16 partial projections, one combined routed/shared final collective, a gfx950 greedy output-head kernel, the wave64 packed-expert path, and the multi-token four-output packed tile. Software MXFP8 remains in use.
|
||||
|
||||
After hardware admission on MI350X, profile before porting those kernels. The likely implementation order is:
|
||||
|
||||
1. A native packed MXFP4×MXFP8 grouped expert GEMM using CDNA4 matrix instructions.
|
||||
2. A wave64/MFMA KDA Q/K/V decode projection.
|
||||
3. Combined routed/shared down-projection TP partials so each layer performs one XGMI all-reduce.
|
||||
4. A CDNA4 output-head matvec and router matvec if they remain visible in the profile.
|
||||
|
||||
Every port needs a direct numerical comparison with the generic graph and an end-to-end greedy-token comparison before performance measurements. The wave64 packed-expert kernel has compile coverage through `NULL:HIP:gfx950`; numerical and performance validation still require real MI350X hardware. None of the remaining gfx11-only kernels should be enabled on gfx950 by changing only the architecture guard.
|
||||
|
||||
## MI350X performance expectation
|
||||
|
||||
Treat the first rental as bring-up, not a guaranteed throughput run. The loader reads every official expert tensor once into a transient GPU-0 staging buffer (at most one packed projection), then redistributes TP8 slices over the GPU fabric; it does not generate files or require checkpoint-sized host RAM. A reasonable planning range for the full text model on eight MI350X cards is 3–8 minutes to stream and TP-shard the 1.56 TB checkpoint, 150–400 tok/s for initial short/medium prefill, and 25–60 tok/s decode with the software packed-expert path. After a native CDNA4 MXFP4×MXFP8 grouped expert kernel, wave64/MFMA recurrent projections, and XGMI collective tuning, 500+ tok/s prefill and roughly 80–150 tok/s decode are plausible targets. These ranges are engineering estimates, not measurements.
|
||||
|
||||
The nominal HBM bandwidth is not the main uncertainty: eight MI350X devices have enough aggregate bandwidth for K3's active weights. Utilization is limited by 93 sequential layers, small routed projections, and synchronization after TP input-sharded projections. Record actual HBM and XGMI counters before deciding whether the next port should target matrix instructions or collective count.
|
||||
|
||||
The official checkpoint also contains MoonViT-V2 and multimodal projector weights. They are skipped by the text loader. Image input remains a separate implementation and validation task.
|
||||
|
||||
## Local TP4 performance baseline
|
||||
|
||||
The pre-rental benchmark uses the converted `Kimi-Linear-48B-A3B-Instruct-MXFP4-v2` checkpoint on four gfx1100 GPUs. It is a useful regression test for the KDA/MLA/MoE text path, not a projection of K3 throughput on MI350X.
|
||||
|
||||
```sh
|
||||
DEV=AMD JIT_BATCH_SIZE=64 python extra/benchmark_kimi.py \
|
||||
/raid/models/Kimi-Linear-48B-A3B-Instruct-MXFP4-v2 \
|
||||
--devices 4 --max-context 128 --prompt-tokens 32 --decode-tokens 32 --chunk-size 32
|
||||
```
|
||||
|
||||
Results from 2026-08-10:
|
||||
|
||||
- load from RAID: 44.28s for the 29.27 GB checkpoint
|
||||
- first 32-token prefill includes roughly 10s of compilation/capture
|
||||
- steady fresh-prompt prefill replay: 0.118s, 270.20 tok/s
|
||||
- steady context-32 decode replay: 101.82 tok/s, 9.82 ms/token
|
||||
- peak host RSS: 729.9 MiB; swap was not used
|
||||
|
||||
The load, prefill, and decode targets are all met in the bounded prompt-32 run. Decode improved from 23.03 tok/s to 101.82 tok/s. The retained greedy output was checked across 32 decode steps; rejected half-wave and unrounded recurrent reductions were faster but diverged and eventually collapsed to a repeated token.
|
||||
|
||||
Fully warmed HTTP serving was also measured with `--max_context 4096`. Startup, including weight load, capture, and replay of both serving shapes, took 113.73s. After a two-turn cache test, the first aligned 64-token request reported 271 tok/s prefill and 101 tok/s decode over 64 generated tokens. A 99-token prompt reported 254 tok/s prefill and 99 tok/s decode; decode falls slightly as MLA context grows.
|
||||
|
||||
Recurrent serving uses only the captured 32-token prefill graph and captured single-token graph. Warmup uses two consecutive chunks so both initial and nonzero-position prefill execution are ready before the socket opens. A prompt tail shorter than 32 tokens runs through the single-token graph instead of compiling a new static shape, so no request-time JIT capture is required. Exact extensions reuse recurrent and KV state—the live second turn logged `in: 18 + 15`—while divergent prompts reset both safely. Very short prompts can report less than 200 aggregate prefill tok/s because fixed reset and single-token costs dominate; aligned and medium/long prompts exercise the 200+ tok/s prefill path.
|
||||
|
||||
Four 7900 XTX cards provide 96 GB aggregate VRAM and about 3.84 TB/s aggregate physical memory bandwidth. Their nominal aggregate vector FP16 rate is about 245.6 TFLOP/s, or about 492 TFLOP/s through matrix instructions. Kimi Linear activates roughly 3.107B parameters per token; a simple active-weight accounting gives approximately 4.05 GB/token and an optimistic bandwidth-only ceiling near 948 tok/s. The measured decode rate is much lower because this MoE decode workload is a collection of small matrix-vector operations plus PCIe collectives, not one ideal streaming kernel.
|
||||
|
||||
The generic loader currently rereads logical TP shards and accounts for roughly 227 GB of disk traffic for a TP4 load. RAID bandwidth hides that inefficiency locally, but a direct one-pass shard loader remains worthwhile before slow remote storage is used. It was not retained here because the attempted direct-shard graph exposed an unresolved scheduler/renderer edge; correctness and bounded memory take priority over avoiding the redundant reads.
|
||||
|
||||
Different chunk sizes can choose a different final token because their matrix kernels use different floating-point reduction orders. Each measured shape was repeatable between cold and captured execution. For official K3 validation, compare logits/tokens against the reference at one fixed chunk size and greedy settings rather than requiring bitwise agreement between performance shapes.
|
||||
@@ -97,4 +97,3 @@ if __name__ == "__main__":
|
||||
tf_output = keras_model(test_input).numpy()[0]
|
||||
print("keras: ", tf_output, file=sys.stderr)
|
||||
np.testing.assert_allclose(tf_output, test_output, atol=1e-5, rtol=1e-5)
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import argparse
|
||||
from tinygrad.llm.kimi import convert_kimi
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Convert official Kimi-Linear-48B-A3B BF16 weights to tinygrad MXFP4/BF16")
|
||||
parser.add_argument("source", help="downloaded moonshotai/Kimi-Linear-48B-A3B-Instruct directory")
|
||||
parser.add_argument("output", help="output directory")
|
||||
args = parser.parse_args()
|
||||
convert_kimi(args.source, args.output)
|
||||
+1
-1
@@ -57,7 +57,7 @@ class TransformerBlock:
|
||||
|
||||
def __call__(self, x:Tensor, start_pos:Variable, mask:Optional[Tensor]):
|
||||
h = x + self.attn(self.ln_1(x), start_pos, mask).float()
|
||||
return (h + self.mlp(self.ln_2(h))).contiguous()
|
||||
return (h + self.mlp(self.ln_2(h))).clone()
|
||||
|
||||
class Transformer:
|
||||
def __init__(self, dim, n_heads, n_layers, norm_eps, vocab_size, max_seq_len=1024):
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cheap preflight for an official moonshotai/Kimi-K3 checkout. Does not load model weights."""
|
||||
import argparse, json, pathlib, shutil
|
||||
from tinygrad.llm.kimi_k3 import KIMI_K3_TP8_BYTES_PER_GPU, audit_kimi_k3_checkpoint
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("model_dir", type=pathlib.Path)
|
||||
parser.add_argument("--metadata-only", action="store_true", help="permit absent weight shards")
|
||||
parser.add_argument("--context", type=int, default=4096, help="context length used for the memory estimate")
|
||||
args = parser.parse_args()
|
||||
stats = audit_kimi_k3_checkpoint(args.model_dir, require_shards=not args.metadata_only)
|
||||
if not 1 <= args.context <= 1_048_576: raise ValueError("--context must be between 1 and 1048576")
|
||||
|
||||
# K3 has 24 MLA layers. Each token stores the 512-value compressed latent plus 64 RoPE values in BF16.
|
||||
per_gpu_weights = KIMI_K3_TP8_BYTES_PER_GPU
|
||||
mla_cache = 24 * args.context * (512 + 64) * 2
|
||||
hbm = 288_000_000_000
|
||||
print(json.dumps(stats, indent=2))
|
||||
print(f"exact text weights/GPU under this TP8 layout: {per_gpu_weights/1e9:.2f} GB ({per_gpu_weights/2**30:.2f} GiB)")
|
||||
print(f"replicated MLA cache/GPU at {args.context:,} tokens: {mla_cache/1e9:.2f} GB ({mla_cache/2**30:.2f} GiB)")
|
||||
print(f"nominal MI350X headroom before runtime buffers: {(hbm-per_gpu_weights-mla_cache)/1e9:.2f} GB")
|
||||
if not args.metadata_only:
|
||||
usage = shutil.disk_usage(args.model_dir)
|
||||
print(f"filesystem free space: {usage.free/1e9:.2f} GB")
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run a reduced, architecture-complete K3 prefill/decode on tensor-parallel devices."""
|
||||
import argparse, time
|
||||
from tinygrad import Tensor, Device, dtypes, nn
|
||||
from tinygrad.llm.kimi_k3 import _shard_kimi_k3, kimi_k3_smoke_config
|
||||
from tinygrad.llm.model import Transformer
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--devices", type=int, default=8)
|
||||
args = parser.parse_args()
|
||||
if args.devices not in (1, 2, 4, 8): raise ValueError("the K3 admission smoke test supports 1, 2, 4, or 8 devices")
|
||||
devices = tuple(f"AMD:{i}" for i in range(args.devices))
|
||||
model = Transformer(kimi_k3_smoke_config())
|
||||
for name,value in nn.state.get_state_dict(model).items():
|
||||
fill = 127 if name.endswith("weight_scale") else 0
|
||||
dtype = value.dtype if value.dtype is dtypes.uint8 else dtypes.bfloat16
|
||||
value.replace(Tensor.full(value.shape, fill, dtype=dtype, device="CPU"))
|
||||
_shard_kimi_k3(model, devices)
|
||||
temperature = Tensor([0.0], device=devices)
|
||||
for label,tokens,start in (("prefill", [[1, 2]], 0), ("decode", [[3]], 2), ("decode replay", [[4]], 3)):
|
||||
begin = time.perf_counter()
|
||||
out = model(Tensor(tokens, dtype=dtypes.int32, device=devices), start, temperature).realize()
|
||||
for device in devices: Device[device].synchronize()
|
||||
print(f"{label}: shape={out.shape}, {time.perf_counter()-begin:.3f}s")
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -5,7 +5,7 @@ from multiprocessing import Queue, Process, shared_memory, connection, Lock
|
||||
|
||||
import numpy as np
|
||||
from tinygrad import dtypes, Tensor
|
||||
from tinygrad.helpers import getenv, prod, Context, round_up, tqdm, OSX, NUM_CPU_THREADS
|
||||
from tinygrad.helpers import getenv, prod, Context, round_up, tqdm, OSX, CPU_COUNT
|
||||
from tinygrad.nn.state import TensorIO
|
||||
|
||||
### ResNet
|
||||
@@ -131,7 +131,7 @@ def batch_load_resnet(batch_size=64, val=False, shuffle=True, seed=None, pad_fir
|
||||
else: X = Tensor.empty(*sz, dtype=dtypes.uint8, device=f"disk:/dev/shm/{shm_name}")
|
||||
Y = [None] * (batch_size*BATCH_COUNT)
|
||||
|
||||
for _ in range(NUM_CPU_THREADS.value):
|
||||
for _ in range(CPU_COUNT):
|
||||
p = Process(target=loader_process, args=(q_in, q_out, X, seed))
|
||||
p.daemon = True
|
||||
p.start()
|
||||
@@ -212,7 +212,7 @@ def batch_load_train_bert(BS:int, seed:int|None=None):
|
||||
rng.shuffle(fs)
|
||||
train_files.append(fs.pop(0))
|
||||
|
||||
cycle_length = min(NUM_CPU_THREADS.value, len(train_files))
|
||||
cycle_length = min(CPU_COUNT, len(train_files))
|
||||
assert cycle_length > 0, "cycle_length must be greater than 0"
|
||||
|
||||
dataset = InterleavedDataset(train_files, cycle_length)
|
||||
@@ -301,7 +301,7 @@ def batch_load_unet3d(preprocessed_dataset_dir:Path, batch_size:int=6, val:bool=
|
||||
X = Tensor.empty(*sz, dtype=dtypes.float32, device=f"disk:/dev/shm/{shm_name_x}")
|
||||
Y = Tensor.empty(*sz, dtype=dtypes.uint8, device=f"disk:/dev/shm/{shm_name_y}")
|
||||
|
||||
for _ in range(NUM_CPU_THREADS.value):
|
||||
for _ in range(CPU_COUNT):
|
||||
proc = Process(target=load_unet3d_data, args=(preprocessed_dataset_dir, seed, queue_in, queue_out, X, Y))
|
||||
proc.daemon = True
|
||||
proc.start()
|
||||
@@ -437,7 +437,7 @@ def batch_load_retinanet(dataset, val:bool, base_dir:Path, batch_size:int=32, sh
|
||||
dataset_iter = iter(image_ids)
|
||||
|
||||
try:
|
||||
for _ in range(NUM_CPU_THREADS.value):
|
||||
for _ in range(CPU_COUNT):
|
||||
proc = Process(
|
||||
target=load_retinanet_data,
|
||||
args=(base_dir, val, queue_in, queue_out, imgs, boxes, labels),
|
||||
|
||||
@@ -1667,15 +1667,14 @@ def train_llama3():
|
||||
def train_gptoss():
|
||||
from examples.mlperf.models.gpt_oss import GPTOSS, GPT_OSS_20B, apply_grad, FP8_DTYPE
|
||||
from examples.mlperf.lr_schedulers import CosineAnnealingLRWithWarmup
|
||||
from examples.mlperf.optim import GradAccClipAdamW, GradAccClipAdamWGroup, clip_grads
|
||||
from examples.mlperf.optim import GradAccClipAdamW, GradAccClipAdamWGroup, fclip_grads
|
||||
|
||||
BENCHMARK = getenv("BENCHMARK")
|
||||
|
||||
config = {}
|
||||
BASEDIR = config["BASEDIR"] = Path(getenv("BASEDIR", "/raid/datasets/c4-8b/"))
|
||||
BS = config["BS"] = getenv("BS", 16)
|
||||
grad_acc = config["GRADIENT_ACC_STEPS"] = getenv("GRADIENT_ACC_STEPS", 1)
|
||||
GBS = config["GLOBAL_BATCH_SIZE"] = BS * grad_acc
|
||||
GBS = config["GLOBAL_BATCH_SIZE"] = BS
|
||||
SEED = config["SEED"] = getenv("SEED", 5760)
|
||||
DATA_SEED = config["DATA_SEED"] = getenv("DATA_SEED", SEED)
|
||||
SEQLEN = config["SEQLEN"] = getenv("SEQLEN", 8192)
|
||||
@@ -1737,13 +1736,13 @@ def train_gptoss():
|
||||
params_wd = [p for p in params if p.ndim >= 3]
|
||||
params_no_wd = [p for p in params if p.ndim < 3]
|
||||
optim = GradAccClipAdamWGroup(
|
||||
GradAccClipAdamW(params_wd, lr=0.0, b1=opt_adamw_beta_1, b2=opt_adamw_beta_2, eps=opt_adamw_epsilon, weight_decay=opt_adamw_weight_decay, grad_acc=grad_acc, device=optim_device),
|
||||
GradAccClipAdamW(params_no_wd, lr=0.0, b1=opt_adamw_beta_1, b2=opt_adamw_beta_2, eps=opt_adamw_epsilon, weight_decay=0.0, grad_acc=grad_acc, device=optim_device),
|
||||
GradAccClipAdamW(params_wd, lr=0.0, b1=opt_adamw_beta_1, b2=opt_adamw_beta_2, eps=opt_adamw_epsilon, weight_decay=opt_adamw_weight_decay, grad_acc=1, device=optim_device),
|
||||
GradAccClipAdamW(params_no_wd, lr=0.0, b1=opt_adamw_beta_1, b2=opt_adamw_beta_2, eps=opt_adamw_epsilon, weight_decay=0.0, grad_acc=1, device=optim_device),
|
||||
)
|
||||
|
||||
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
|
||||
@@ -1770,31 +1769,32 @@ def train_gptoss():
|
||||
|
||||
@TinyJit
|
||||
@Context(TRAINING=1)
|
||||
def minibatch(tokens:Tensor):
|
||||
def step(tokens:Tensor):
|
||||
if is_dp: tokens = tokens.to(None).shard(device, 0)
|
||||
if not is_sharding: tokens = tokens.to(None)
|
||||
|
||||
logits:Tensor = model(tokens[:, :-1], save=True)
|
||||
loss = logits.sparse_categorical_crossentropy(tokens[:, 1:])
|
||||
if getenv("FUSED_CE", 0):
|
||||
from extra.llama_kernels.fused_ce import fused_ce_loss
|
||||
loss = fused_ce_loss(logits.cast(dtypes.bfloat16), tokens[:, 1:], label_smoothing=0.0)
|
||||
else:
|
||||
loss = logits.sparse_categorical_crossentropy(tokens[:, 1:])
|
||||
|
||||
for g, new_g in zip(grads, loss.gradient(*optim.params)):
|
||||
apply_grad(g, new_g.uop)
|
||||
|
||||
loss_cpu = loss.flatten().float().to("CPU")
|
||||
return loss_cpu.realize(*grads)
|
||||
Tensor.realize(loss, *grads)
|
||||
|
||||
@TinyJit
|
||||
def optim_step():
|
||||
grad_norm = clip_grads(grads, grad_acc, 1.0)
|
||||
optim.fstep(grads, grad_norm)
|
||||
clipped_grads, grad_norm = fclip_grads(grads, 1.0)
|
||||
optim.fstep(clipped_grads, grad_norm)
|
||||
scheduler.step()
|
||||
|
||||
for g in grads: g.assign(0)
|
||||
|
||||
loss_cpu = loss.flatten().float().to("CPU")
|
||||
lr_cpu = optim.lr.float().to("CPU")
|
||||
grad_norm_cpu = grad_norm.float().to("CPU")
|
||||
Tensor.realize(lr_cpu, grad_norm_cpu, *grads, *fp8_inv_scales)
|
||||
Tensor.realize(loss_cpu, lr_cpu, grad_norm_cpu, *grads, *fp8_inv_scales)
|
||||
|
||||
return lr_cpu, grad_norm_cpu
|
||||
return loss_cpu, lr_cpu, grad_norm_cpu
|
||||
|
||||
@TinyJit
|
||||
@Context(TRAINING=0)
|
||||
@@ -1843,30 +1843,20 @@ def train_gptoss():
|
||||
profile_marker(f"train @ {i}")
|
||||
st = time.perf_counter()
|
||||
|
||||
stopped = False
|
||||
losses, data_time, dev_time = [], 0, 0
|
||||
for _ in range(grad_acc if i >= 2 else 1):
|
||||
ist = time.perf_counter()
|
||||
try: tokens = next(train_iter)
|
||||
except StopIteration:
|
||||
stopped = True
|
||||
break
|
||||
mst = time.perf_counter()
|
||||
data_time += mst - ist
|
||||
losses.append(minibatch(tokens).item())
|
||||
dev_time += time.perf_counter() - mst
|
||||
if stopped: break
|
||||
ist = time.perf_counter()
|
||||
|
||||
gt = time.perf_counter()
|
||||
ret = optim_step()
|
||||
lr, grad_norm = ret[0].item(), ret[1].item()
|
||||
try: tokens = next(train_iter)
|
||||
except StopIteration: break
|
||||
mst = time.perf_counter()
|
||||
data_time = mst - ist
|
||||
|
||||
ret = step(tokens)
|
||||
dev_time = time.perf_counter() - mst
|
||||
|
||||
loss, lr, grad_norm = ret[0].item(), ret[1].item(), ret[2].item()
|
||||
et = time.perf_counter()
|
||||
|
||||
loss = sum(losses) / len(losses)
|
||||
optim_time = et - gt
|
||||
dev_time += optim_time
|
||||
step_time = et - st
|
||||
gbs_time = gt - st
|
||||
if BENCHMARK: step_times.append(step_time)
|
||||
|
||||
i += 1
|
||||
@@ -1876,7 +1866,7 @@ def train_gptoss():
|
||||
gflops = GlobalCounters.global_ops / 1e9 / dev_time
|
||||
mfu = ((6 * num_params * SEQLEN * GBS) / (dev_time * device_count * 4.6e15)) * 100
|
||||
tqdm.write(
|
||||
f"{i:5} {step_time:.3f} s step, {gbs_time:.3f} s gbs, {optim_time:.3f} s optim, {data_time:.3f} s data, {loss:.4f} loss, " \
|
||||
f"{i:5} {step_time:.3f} s step, {dev_time:.3f} s dev, {data_time:.3f} s data, {loss:.4f} loss, " \
|
||||
f"{lr:.12f} LR, {grad_norm:.6f} grad_norm, {mem_gb:.2f} GB used, {gflops:9.2f} GFLOPS, {mfu:5.2f}% MFU")
|
||||
if DEBUG >= 1: tqdm.write(" mem per device: " + ', '.join(f"{dev}: {mem/1e9:.2f} GB" for dev, mem in sorted(GlobalCounters.mem_used_per_device.items())))
|
||||
|
||||
@@ -1886,8 +1876,6 @@ def train_gptoss():
|
||||
"train/lr": lr,
|
||||
"train/grad_norm": grad_norm,
|
||||
"train/step_time": step_time,
|
||||
"train/gbs_time": gbs_time,
|
||||
"train/optim_time": optim_time,
|
||||
"train/dev_time": dev_time,
|
||||
"train/data_time": data_time,
|
||||
"train/mem": mem_gb,
|
||||
|
||||
@@ -12,9 +12,9 @@ from tinygrad.helpers import Timing, colored, GlobalCounters, profile_marker
|
||||
from tinygrad.uop.ops import Ops, UOp
|
||||
from extra.models.llama import apply_rotary_emb
|
||||
from extra.llama_kernels.rmsnorm import rmsnorm
|
||||
from extra.gemm.cdna_asm_gemm import _mx_block_scale, _mx_block_scale_3d, quantize_mxfp8
|
||||
from extra.gemm.cdna_asm_gemm import _mx_block_scale, _mx_block_scale_3d, quantize_mxfp8, asm_gemm, can_use_asm_gemm, mx_pack
|
||||
from extra.gemm.moe_gemm import grouped_mx_gemm
|
||||
from extra.gemm.moe_routing import route, dispatch, combine
|
||||
from extra.gemm.moe_routing import route, dispatch, combine, router_mfma
|
||||
|
||||
FP8_DTYPE = dtypes.fp8e4m3
|
||||
FP8_MAX = 448.0
|
||||
@@ -41,7 +41,7 @@ def _quant_dequant_bwd(grad:UOp, call:UOp) -> tuple:
|
||||
|
||||
def quant_dequant_mx(x:Tensor) -> Tensor:
|
||||
fxn = _quant_dequant_fwd_fxn(x.as_param(0).uop, x.device)
|
||||
return Tensor(UOp.maketuple(fxn.uop).call(x.uop, grad_fxn=_quant_dequant_bwd).gettuple(0))
|
||||
return Tensor(fxn.uop.call_with_output(x.uop, grad_fxn=_quant_dequant_bwd))
|
||||
|
||||
def _mx_scale(e8:Tensor) -> Tensor:
|
||||
return _mx_block_scale(e8) if e8.ndim == 2 else _mx_block_scale_3d(e8)
|
||||
@@ -58,10 +58,27 @@ def _dequant_bwd(grad:UOp, call:UOp) -> tuple:
|
||||
|
||||
def dequant_weight(w_q:Tensor, w_scale:Tensor) -> Tensor:
|
||||
fxn = _dequant_fwd_fxn(w_q.as_param(0).uop, w_scale.as_param(1).uop, w_q.device)
|
||||
call = UOp.maketuple(fxn.uop).call(w_q.uop, w_scale.uop, grad_fxn=_dequant_bwd)
|
||||
return Tensor(call.gettuple(0))
|
||||
return Tensor(fxn.uop.call_with_output(w_q.uop, w_scale.uop, grad_fxn=_dequant_bwd))
|
||||
|
||||
def matmul_mx(x:Tensor, w_q:Tensor, w_scale:Tensor) -> Tensor:
|
||||
def matmul_mx(x:Tensor|tuple[Tensor, Tensor], w_q:Tensor, w_scale:Tensor) -> Tensor:
|
||||
if isinstance(x, tuple):
|
||||
assert ASM_GEMM, "pre-quantized MXFP8 input requires ASM_GEMM"
|
||||
from extra.gemm.cdna_asm_gemm import asm_gemm, can_use_asm_gemm, mx_pack
|
||||
x_q, x_e8 = x
|
||||
l_shape, padded = x_q.shape[:-1], x_q.shape[-1]
|
||||
x_q, x_e8 = x_q.reshape(-1, padded), x_e8.reshape(-1, padded // 32)
|
||||
K, N = w_q.shape[1], w_q.shape[0]
|
||||
assert padded >= K and (padded - K) % 32 == 0 and x_e8.shape[-1] == padded // 32
|
||||
wq, ws = w_q, w_scale
|
||||
if (pad := padded - K):
|
||||
wq = wq.pad(((0, 0), (0, pad)))
|
||||
ws = ws.pad(((0, 0), (0, pad // 32)), value=127).cast(dtypes.uint8)
|
||||
if (npad := (-N) % 256):
|
||||
wq = wq.pad(((0, npad), (0, 0)))
|
||||
ws = ws.pad(((0, npad), (0, 0)), value=127).cast(dtypes.uint8)
|
||||
assert can_use_asm_gemm(x_q, wq.T)
|
||||
out = asm_gemm(x_q, wq.T, mx=True, mx_scales=(mx_pack(x_e8), x_e8, mx_pack(ws), ws), mx_w_stored=True)
|
||||
return (out[:, :N] if npad else out).reshape(*l_shape, N).cast(dtypes.bfloat16)
|
||||
l_shape = x.shape[:-1]
|
||||
if ASM_GEMM:
|
||||
from extra.gemm.cdna_asm_gemm import asm_gemm, can_use_asm_gemm, mx_pack
|
||||
@@ -146,6 +163,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)
|
||||
|
||||
@@ -174,20 +192,36 @@ class GPTOSS:
|
||||
def attention(self, x:Tensor, freqs_cis:Tensor, mask:Tensor, sliding:bool, *, attention_norm:Tensor, wqkv:Tensor,
|
||||
wqkv_scale:Tensor, wqkv_bias:Tensor, wo:Tensor, wo_scale:Tensor, wo_bias:Tensor, sinks:Tensor):
|
||||
bsz, seqlen, _ = x.shape
|
||||
x_normed, rrms = rmsnorm(x, self.norm_eps)
|
||||
qkv = matmul_mx(x_normed * attention_norm, wqkv, wqkv_scale) + wqkv_bias
|
||||
|
||||
if getenv("FUSED_RMSNORM_MX", 0):
|
||||
from extra.gptoss_kernels.rmsnorm import rmsnorm_mul_quantize_mxfp8
|
||||
x_q, x_e8, rrms = rmsnorm_mul_quantize_mxfp8(x, attention_norm, self.norm_eps)
|
||||
qkv = matmul_mx((x_q, x_e8), wqkv, wqkv_scale) + wqkv_bias
|
||||
norm_saves = [x_q, x_e8, rrms]
|
||||
if getenv("FUSED_RMSNORM_MUL", 0):
|
||||
from extra.gptoss_kernels.rmsnorm import rmsnorm_mul
|
||||
x_normed, rrms = rmsnorm_mul(x, attention_norm, self.norm_eps)
|
||||
qkv = matmul_mx(x_normed, wqkv, wqkv_scale) + wqkv_bias
|
||||
norm_saves = [x_normed, rrms]
|
||||
else:
|
||||
x_normed, rrms = rmsnorm(x, self.norm_eps)
|
||||
qkv = matmul_mx(x_normed * attention_norm, wqkv, wqkv_scale) + wqkv_bias
|
||||
norm_saves = [x_normed, rrms]
|
||||
|
||||
qkv = qkv.reshape(bsz, seqlen, self.n_kv_heads, self.n_rep + 2, self.head_dim)
|
||||
xq = qkv[:, :, :, :self.n_rep].reshape(bsz, seqlen, self.n_heads, self.head_dim)
|
||||
xk, xv = qkv[:, :, :, self.n_rep], qkv[:, :, :, self.n_rep + 1]
|
||||
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)
|
||||
|
||||
if sliding:
|
||||
attn = self._sliding_attention(xq, xk, xv, sinks)
|
||||
elif getenv("HK_FLASH_ATTENTION"):
|
||||
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)
|
||||
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:
|
||||
xqm = xq.reshape(bsz, seqlen, self.n_kv_heads, self.n_rep, self.head_dim).permute(0, 2, 3, 1, 4)
|
||||
xkm, xvm = xk.permute(0, 2, 1, 3).unsqueeze(2), xv.permute(0, 2, 1, 3).unsqueeze(2)
|
||||
@@ -199,14 +233,20 @@ 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, [attn] + norm_saves + 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,
|
||||
w_down:Tensor, w_down_scale:Tensor, w_down_bias:Tensor):
|
||||
x_normed, rrms = rmsnorm(x, self.norm_eps)
|
||||
inp = x_normed * ffn_norm
|
||||
logits = inp.float() @ gate.float().T + gate_bias.float()
|
||||
if getenv("FUSED_RMSNORM_MUL", 0):
|
||||
from extra.gptoss_kernels.rmsnorm import rmsnorm_mul
|
||||
x_normed, rrms = rmsnorm_mul(x, ffn_norm, self.norm_eps)
|
||||
inp = x_normed
|
||||
else:
|
||||
x_normed, rrms = rmsnorm(x, self.norm_eps)
|
||||
inp = x_normed * ffn_norm
|
||||
|
||||
logits = router_mfma(inp, gate, gate_bias) if getenv("ROUTER_MFMA", 0) else inp.float() @ gate.float().T + gate_bias.float()
|
||||
dim, inter = self.dim, self.intermediate_size
|
||||
|
||||
if getenv("GROUPED_MOE", 0):
|
||||
@@ -220,6 +260,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)
|
||||
@@ -263,7 +304,26 @@ class GPTOSS:
|
||||
w_down=self.w_down[i], w_down_scale=self.w_down_scale[i], w_down_bias=self.w_down_bias[i])
|
||||
h, *_ = self.run_layer(h, freqs_cis, mask_full, i % 2 == 0, attn_kwargs, ffn_kwargs, save=save)
|
||||
|
||||
logits = self.norm(h) @ self.output.T
|
||||
h_normed = self.norm(h)
|
||||
|
||||
if getenv("FP8_LMHEAD", 0) and ASM_GEMM:
|
||||
pad = (-self.dim) % 256
|
||||
h2 = h_normed.reshape(-1, self.dim).pad(((0, 0), (0, pad)))
|
||||
w2 = self.output.pad(((0, 0), (0, pad)))
|
||||
hq, he8, hsi = quantize_mxfp8(h2)
|
||||
oq, oe8, _ = quantize_mxfp8(w2)
|
||||
if hsi is not None and can_use_asm_gemm(hq, oq.T):
|
||||
logits = asm_gemm(hq, oq.T, mx=True, mx_scales=(hsi, he8, mx_pack(oe8), oe8), mx_w_stored=False)
|
||||
logits = logits.reshape(bsz, seqlen, self.vocab_size).cast(dtypes.bfloat16)
|
||||
else:
|
||||
logits = h_normed @ self.output.T
|
||||
elif ASM_GEMM:
|
||||
pad = (-self.dim) % 256
|
||||
h_padded, w_padded = h_normed.pad((None, None, (0, pad))), self.output.pad(((0, 0), (0, pad)))
|
||||
logits = asm_gemm(h_padded, w_padded.T) if can_use_asm_gemm(h_padded, w_padded.T) and getenv("VOCAB_ASM", 1) else h_normed @ self.output.T
|
||||
else:
|
||||
logits = h_normed @ self.output.T
|
||||
|
||||
return logits
|
||||
|
||||
def _get_pads(uop:UOp) -> list[UOp]:
|
||||
@@ -274,14 +334,14 @@ def apply_grad(grad_buf:Tensor, new_grad:UOp):
|
||||
pads = _get_pads(new_grad)
|
||||
if len(pads) <= 1:
|
||||
new_grad = new_grad.cast(grad_buf.dtype)
|
||||
grad_buf.uop = grad_buf.uop.after(grad_buf.uop.store(grad_buf.uop + new_grad))
|
||||
grad_buf.uop = grad_buf.uop.after(grad_buf.uop.store(new_grad))
|
||||
return
|
||||
cur = grad_buf.uop
|
||||
for pad in sorted(pads, key=lambda p: p.marg[0][0] if p.op == Ops.PAD else 0, reverse=True):
|
||||
if pad.op == Ops.PAD:
|
||||
grad_shrink = tuple([(p[0], s+p[0]) for s,p in zip(pad.src[0].shape, pad.marg)])
|
||||
grad_shrink = tuple((p[0], s+p[0]) for s,p in zip(pad.src[0].shape, pad.marg))
|
||||
buf_slice = cur.shrink(grad_shrink)
|
||||
cur = cur.after(buf_slice.store(buf_slice + pad.src[0].cast(cur.dtype)))
|
||||
cur = cur.after(buf_slice.store(pad.src[0].cast(cur.dtype)))
|
||||
else:
|
||||
cur = cur.after(cur.store(cur + pad.cast(cur.dtype)))
|
||||
grad_buf.uop = cur
|
||||
|
||||
@@ -15,7 +15,7 @@ def stochastic_round_bf16(x:Tensor) -> Tensor:
|
||||
bits = x.bitcast(dtypes.uint32)
|
||||
if isinstance(x.device, tuple):
|
||||
shape = x.uop.shard_shape if x.uop.axis is not None else x.shape
|
||||
noise = Tensor(UOp(Ops.MSTACK, dtypes.default_float, tuple(Tensor.rand(*shape, device=d).uop for d in x.device)))
|
||||
noise = Tensor(UOp(Ops.MSTACK, src=tuple(Tensor.rand(*shape, device=d).uop for d in x.device)))
|
||||
else:
|
||||
noise = x.rand_like()
|
||||
noise = (noise * 0xFFFF).cast(dtypes.uint32)
|
||||
@@ -27,6 +27,11 @@ def clip_grads(grads:list[Tensor], grad_acc, clip_norm) -> Tensor:
|
||||
for g in grads: g.assign((g * (clip_norm / (total_norm + 1e-6)).clamp(max_=1.0)).cast(g.dtype))
|
||||
return total_norm
|
||||
|
||||
def fclip_grads(grads:list[Tensor], clip_norm) -> Tensor:
|
||||
total_norm = Tensor.stack(*[g.float().square().sum() for g in grads]).sum().sqrt().contiguous()
|
||||
scale = (clip_norm / (total_norm + 1e-6)).clamp(max_=1.0)
|
||||
return [(g * scale).cast(g.dtype) for g in grads], total_norm
|
||||
|
||||
class GradAccClipAdamW(Optimizer):
|
||||
def __init__(self, params:list[Tensor], lr=0.001, b1=0.9, b2=0.999, eps=1e-6, weight_decay=0.0, grad_acc=1, clip_norm=1.0, device=None, fused=FUSE_OPTIM):
|
||||
super().__init__(params, lr, device, fused)
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
from pathlib import Path
|
||||
|
||||
from examples.mlperf.dataloader import get_llama3_dataset
|
||||
from tinygrad.helpers import getenv
|
||||
|
||||
BASEDIR = Path(getenv("BASEDIR", "/raid/datasets/c4-8b/"))
|
||||
SAMPLES = getenv("SAMPLES", 1_200_000 * 32)
|
||||
EVAL_SAMPLES = getenv("EVAL_SAMPLES", 1024)
|
||||
SEQLEN = getenv("SEQLEN", 8192)
|
||||
DATA_SEED = getenv("DATA_SEED", 5760)
|
||||
|
||||
get_llama3_dataset(SAMPLES, SEQLEN, BASEDIR, seed=DATA_SEED, val=False, small=True)
|
||||
get_llama3_dataset(EVAL_SAMPLES, SEQLEN, BASEDIR, seed=0, val=True, small=True)
|
||||
+3
-3
@@ -26,7 +26,7 @@ export FUSED_SILU_W13=${FUSED_SILU_W13:-1}
|
||||
export SPLIT_W13=${SPLIT_W13:-0}
|
||||
export OFFLOAD_OPTIM=${OFFLOAD_OPTIM:-0}
|
||||
|
||||
export DEFAULT_FLOAT="bfloat16" OPTIM_DTYPE="bfloat16"
|
||||
export DEFAULT_FLOAT="bfloat16" OPTIM_DTYPE="float32"
|
||||
export DP=${DP:-8} MP=${MP:-1} BS=${BS:-16} EVAL_BS=${EVAL_BS:-8} GRADIENT_ACC_STEPS=${GRADIENT_ACC_STEPS:-2}
|
||||
export GBS=$((BS * GRADIENT_ACC_STEPS))
|
||||
|
||||
@@ -35,7 +35,7 @@ export BASEDIR="/raid/datasets/c4-8b/"
|
||||
export SMALL=1
|
||||
export LLAMA3_SIZE=${LLAMA3_SIZE:-"8B"}
|
||||
export EVAL_TARGET=3.3 EVAL_FREQ=12288
|
||||
export LR="1e-3" END_LR="1e-4" WARMUP_SAMPLES=4096 MAX_STEPS=1200000
|
||||
export LR="1e-3" END_LR="1e-4" WARMUP_SAMPLES=2048 MAX_STEPS=1200000
|
||||
export WARMUP_STEPS=$((WARMUP_SAMPLES / GBS))
|
||||
export SAMPLES=$((MAX_STEPS * GBS))
|
||||
export SEQLEN=${SEQLEN:-8192}
|
||||
@@ -44,7 +44,7 @@ export SEED=${SEED:-5760}
|
||||
export DATA_SEED=${DATA_SEED:-5760}
|
||||
|
||||
export JITBEAM=${JITBEAM:-3}
|
||||
export BEAM_UOPS_MAX=6000 BEAM_UPCAST_MAX=256 BEAM_LOCAL_MAX=1024 BEAM_MIN_PROGRESS=5 BEAM_PADTO=1
|
||||
export BEAM_UOPS_MAX=6000 BEAM_UPCAST_MAX=256 BEAM_LOCAL_MAX=1024 BEAM_MIN_PROGRESS=5 BEAM_PADTO=0
|
||||
|
||||
export FAKEDATA=${FAKEDATA:-1} BENCHMARK=${BENCHMARK:-10}
|
||||
if [ -z "$FULL_LAYERS" ]; then
|
||||
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export PYTHONPATH="."
|
||||
export PATH="/opt/rocm-7.1.1/bin:$PATH"
|
||||
export ROCM_PATH="/opt/rocm-7.1.1"
|
||||
export ROCM_PATH=${ROCM_PATH:-/opt/rocm-7.1.1}
|
||||
export PATH="$ROCM_PATH/bin:$PATH"
|
||||
export DEV=${DEV:-AMD}
|
||||
export CHECK_OOB=0
|
||||
export REWRITE_STACK_LIMIT=5000000 HCQDEV_WAIT_TIMEOUT_MS=240000
|
||||
@@ -26,7 +26,7 @@ export FUSED_SILU_W13=${FUSED_SILU_W13:-1}
|
||||
export SPLIT_W13=${SPLIT_W13:-0}
|
||||
export OFFLOAD_OPTIM=${OFFLOAD_OPTIM:-0}
|
||||
|
||||
export DEFAULT_FLOAT="bfloat16" OPTIM_DTYPE="bfloat16"
|
||||
export DEFAULT_FLOAT="bfloat16" OPTIM_DTYPE="float32"
|
||||
export DP=${DP:-8} MP=${MP:-1} BS=${BS:-16} EVAL_BS=${EVAL_BS:-8} GRADIENT_ACC_STEPS=${GRADIENT_ACC_STEPS:-2}
|
||||
export GBS=$((BS * GRADIENT_ACC_STEPS))
|
||||
|
||||
@@ -35,7 +35,7 @@ export BASEDIR="/raid/datasets/c4-8b/"
|
||||
export SMALL=1
|
||||
export LLAMA3_SIZE=${LLAMA3_SIZE:-"8B"}
|
||||
export EVAL_TARGET=3.3 EVAL_FREQ=12288
|
||||
export LR="1e-3" END_LR="1e-4" WARMUP_SAMPLES=4096 MAX_STEPS=1200000
|
||||
export LR="1e-3" END_LR="1e-4" WARMUP_SAMPLES=2048 MAX_STEPS=1200000
|
||||
export WARMUP_STEPS=$((WARMUP_SAMPLES / GBS))
|
||||
export SAMPLES=$((MAX_STEPS * GBS))
|
||||
export SEQLEN=${SEQLEN:-8192}
|
||||
@@ -44,6 +44,6 @@ export SEED=${SEED:-$RANDOM}
|
||||
export DATA_SEED=${DATA_SEED:-5760}
|
||||
|
||||
export JITBEAM=${JITBEAM:-3}
|
||||
export BEAM_UOPS_MAX=6000 BEAM_UPCAST_MAX=256 BEAM_LOCAL_MAX=1024 BEAM_MIN_PROGRESS=5 BEAM_PADTO=1
|
||||
export BEAM_UOPS_MAX=6000 BEAM_UPCAST_MAX=256 BEAM_LOCAL_MAX=1024 BEAM_MIN_PROGRESS=5 BEAM_PADTO=0
|
||||
|
||||
python3 examples/mlperf/model_train.py
|
||||
|
||||
+3
-3
@@ -17,7 +17,7 @@ export USE_ATOMICS=1
|
||||
export ASM_GEMM=1
|
||||
export WQKV=1
|
||||
export MASTER_WEIGHTS=1
|
||||
export FP8=1
|
||||
export MXFP4=1
|
||||
export ALLREDUCE_CAST=1
|
||||
export FAST_CE=1
|
||||
export FUSED_INPUT_QUANTIZE=1
|
||||
@@ -26,7 +26,7 @@ export FUSED_ADD_NORM_MUL_QUANTIZE=1
|
||||
export FUSED_SILU_W13=1
|
||||
export SPLIT_W13=0
|
||||
|
||||
export DEFAULT_FLOAT="bfloat16" OPTIM_DTYPE="bfloat16"
|
||||
export DEFAULT_FLOAT="bfloat16" OPTIM_DTYPE="float32"
|
||||
export DP=8 MP=1 BS=16 EVAL_BS=8 GRADIENT_ACC_STEPS=2
|
||||
export GBS=$((BS * GRADIENT_ACC_STEPS))
|
||||
|
||||
@@ -44,7 +44,7 @@ export SEED=$RANDOM
|
||||
export DATA_SEED=$SEED
|
||||
|
||||
export JITBEAM=3
|
||||
export BEAM_UOPS_MAX=6000 BEAM_UPCAST_MAX=256 BEAM_LOCAL_MAX=1024 BEAM_MIN_PROGRESS=5 BEAM_PADTO=1
|
||||
export BEAM_UOPS_MAX=6000 BEAM_UPCAST_MAX=256 BEAM_LOCAL_MAX=1024 BEAM_MIN_PROGRESS=5 BEAM_PADTO=0
|
||||
|
||||
export LOGMLPERF=1
|
||||
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ export DEVICE_IN_FUNCTION_BUG=1
|
||||
export DEBUG=${DEBUG:-2}
|
||||
export HK_FLASH_ATTENTION=${HK_FLASH_ATTENTION:-1}
|
||||
export ASM_GEMM=${ASM_GEMM:-1}
|
||||
export GROUPED_MOE=${GROUPED_MOE:-1}
|
||||
export ALL2ALL=${ALL2ALL:-1}
|
||||
export LATE_ALLREDUCE=${LATE_ALLREDUCE:-0}
|
||||
export ALLREDUCE_CAST=${ALLREDUCE_CAST:-1}
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ export DEVICE_IN_FUNCTION_BUG=1
|
||||
export DEBUG=${DEBUG:-0}
|
||||
export HK_FLASH_ATTENTION=${HK_FLASH_ATTENTION:-1}
|
||||
export ASM_GEMM=${ASM_GEMM:-1}
|
||||
export GROUPED_MOE=${GROUPED_MOE:-1}
|
||||
export ALL2ALL=${ALL2ALL:-1}
|
||||
export LATE_ALLREDUCE=${LATE_ALLREDUCE:-0}
|
||||
export ALLREDUCE_CAST=${ALLREDUCE_CAST:-1}
|
||||
|
||||
@@ -107,14 +107,21 @@ def compile(onnx_file):
|
||||
return inputs, test_val
|
||||
|
||||
def test_vs_compile(run, inputs, test_val=None):
|
||||
if (log:=bool(getenv("BENCHMARK_LOG", ""))): from extra.bench_log import WallTimeEvent, BenchEvent
|
||||
|
||||
# run 20 times
|
||||
step_times = []
|
||||
for _ in range(20):
|
||||
st = time.perf_counter()
|
||||
out = run(**inputs)
|
||||
mt = time.perf_counter()
|
||||
val = out.numpy()
|
||||
if log:
|
||||
with WallTimeEvent(BenchEvent.STEP):
|
||||
out = run(**inputs)
|
||||
mt = time.perf_counter()
|
||||
val = out.numpy()
|
||||
else:
|
||||
out = run(**inputs)
|
||||
mt = time.perf_counter()
|
||||
val = out.numpy()
|
||||
et = time.perf_counter()
|
||||
step_times.append((et-st)*1e3)
|
||||
print(f"enqueue {(mt-st)*1e3:6.2f} ms -- total run {step_times[-1]:6.2f} ms")
|
||||
@@ -160,12 +167,6 @@ def test_vs_onnx(new_inputs, test_val, onnx_file, tol):
|
||||
print("test vs onnx passed")
|
||||
return timings
|
||||
|
||||
def bench(run, inputs):
|
||||
from extra.bench_log import WallTimeEvent, BenchEvent
|
||||
for _ in range(10):
|
||||
with WallTimeEvent(BenchEvent.STEP):
|
||||
run(**inputs).numpy()
|
||||
|
||||
if __name__ == "__main__":
|
||||
if getenv("RUN_PICKLE"):
|
||||
with open(OUTPUT, "rb") as f: pickle_loaded = load_pickle(f)
|
||||
@@ -181,6 +182,3 @@ if __name__ == "__main__":
|
||||
test_vs_compile(pickle_loaded, inputs, outputs)
|
||||
if getenv("SELFTEST"):
|
||||
test_vs_onnx(inputs, outputs, onnx_file, 1e-4)
|
||||
|
||||
if getenv("BENCHMARK_LOG", ""):
|
||||
bench(pickle_loaded, inputs)
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# Navi31 flash tools
|
||||
|
||||
Utilities for reading and recovering the 2 MiB SPI flash on Navi31 boards.
|
||||
Run them from the tinygrad repository root. No image is bundled; keep a verified
|
||||
full-ROM backup before performing any write.
|
||||
|
||||
`fw_live.py` accesses BAR5 through tinygrad's `PCIDevice.map_bar()` abstraction
|
||||
and supports either the custom ASM24 USB-PCIe bridge or native PCIe. Select the
|
||||
transport before the subcommand:
|
||||
|
||||
```sh
|
||||
python3 extra/amdflash/fw_live.py --transport usb probe
|
||||
python3 extra/amdflash/fw_live.py --transport pci probe
|
||||
```
|
||||
|
||||
The default, `--transport auto`, considers USB devices first and then native
|
||||
PCI devices. Native PCI access requires the usual tinygrad PCI permissions and
|
||||
an unbound kernel driver.
|
||||
|
||||
## Access paths and hardware state
|
||||
|
||||
The paths are state-dependent and are not interchangeable:
|
||||
|
||||
* **`romless.py`** drives SMUIO `ROM_SW_*` directly through the ASM24 bridge.
|
||||
Use it only when an empty or corrupt flash has stalled the PSP PBL. Healthy
|
||||
autonomous boot gates this engine; the usual gated status is
|
||||
`ROM_SW_STATUS=0x04000800`.
|
||||
* **`fw_live.py probe`** queries the early PSP boot-firmware mailbox.
|
||||
* Firmware-mediated write commands are retained for protocol documentation but
|
||||
are disabled because an exact stock reflash did not validate safely.
|
||||
* **`fw_live.py dump`** reads an exact 2 MiB raw image through
|
||||
`ROM_INDEX/ROM_DATA`. It refuses devices where the raw SMUIO controller is
|
||||
unavailable; the NBIO SOC15 function-ROM aperture is not a physical SPI
|
||||
mapping and is deliberately not used as a fallback.
|
||||
|
||||
The tools do not reset or power-cycle the board.
|
||||
|
||||
## Raw ROM_SW recovery
|
||||
|
||||
Identification and read-only operations:
|
||||
|
||||
```sh
|
||||
python3 extra/amdflash/romless.py info
|
||||
python3 extra/amdflash/romless.py read 0 0x40
|
||||
python3 extra/amdflash/romless.py dump spi.bin
|
||||
python3 extra/amdflash/romless.py verify known-good.bin
|
||||
```
|
||||
|
||||
Restore an exact 2 MiB image:
|
||||
|
||||
```sh
|
||||
python3 extra/amdflash/romless.py flash known-good.bin --yes
|
||||
```
|
||||
|
||||
If GD25 status-register bit `SR2.CMP` protects the complete array, clearing it
|
||||
requires separate authorization:
|
||||
|
||||
```sh
|
||||
python3 extra/amdflash/romless.py flash known-good.bin --clear-cmp --yes
|
||||
```
|
||||
|
||||
Programming is sector-granular. Every written 4 KiB sector is immediately read
|
||||
back and compared with the input. A range can be resumed independently:
|
||||
|
||||
```sh
|
||||
python3 extra/amdflash/romless.py flash known-good.bin \
|
||||
--start-sector 128 --sector-count 64 --yes
|
||||
```
|
||||
|
||||
Navi31 ROM_SW details used by the implementation:
|
||||
|
||||
* `ROM_SW_COMMAND = (address << 8) | opcode`
|
||||
* TX data uses big-endian stream dwords
|
||||
* `RETURN_DATA_EN` (bit 19) is clear for TX and set for RX
|
||||
* the RX window exposes the preceding transaction, so reads are primed once
|
||||
|
||||
## Firmware-mediated access
|
||||
|
||||
The read-only commands are:
|
||||
|
||||
```sh
|
||||
python3 extra/amdflash/fw_live.py probe
|
||||
python3 extra/amdflash/fw_live.py dump current-spi.bin
|
||||
```
|
||||
|
||||
`dump` produces exactly `0x200000` bytes, requires the raw IFWI magic at offset
|
||||
zero, rejects mirrored 1 MiB apertures, and restores the ROM controller/index
|
||||
state before writing output.
|
||||
|
||||
The validated early-firmware sequence is available as:
|
||||
|
||||
```sh
|
||||
python3 extra/amdflash/fw_live.py --transport usb ifwi-all full-ifwi.bin --yes
|
||||
```
|
||||
|
||||
It resolves at most Navi31's configured 19 items, streams the item associated
|
||||
with terminal phase `0x2xx`, and then stops. PSP selects the destination
|
||||
partition; item `0x08` always comes from the payload referenced by the first
|
||||
ISH descriptor, matching AMDVBFlash. A hard power cycle is required afterward.
|
||||
|
||||
A successful PSP update is not a byte-identical raw rewrite. On the validated
|
||||
stock test, both A/B payloads matched the source exactly, PSP selected and
|
||||
booted the updated B partition, and firmware changed only its update cookie,
|
||||
B descriptor counter/checksum, and generated metadata near `0x1ef000`.
|
||||
|
||||
The `stream`, `ifwi-step`, and `live-flash` commands remain disabled. Testing
|
||||
showed that the PSP live path parses a raw stock IFWI but fails with status
|
||||
`0xC` (`PSP Write To SPI Error`) after writing an `$AMDVBFL` cookie. Use the
|
||||
verified ROM_SW path for recovery.
|
||||
|
||||
## Safety
|
||||
|
||||
ROM_SW erase/program and `ifwi-all` commands require `--yes`; other
|
||||
firmware-streaming commands are disabled. Read-only commands still touch controller and mailbox registers but
|
||||
do not issue SPI program/erase or PSP transfer-start commands. Preserve a
|
||||
known-good full dump outside the repository.
|
||||
@@ -0,0 +1,53 @@
|
||||
from __future__ import annotations
|
||||
import struct, sys, time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT))
|
||||
from tinygrad.runtime.support.usb import USB3
|
||||
from tinygrad.runtime.support.system import PCIDevice, System, USBPCIDevice
|
||||
|
||||
USB_IDS = ((0x3801, 0x0001), (0xADD1, 0x0001))
|
||||
NAVI31_DEVICES = ((0xffff, (0x744c,)),)
|
||||
|
||||
|
||||
def open_gpu(index: int = 0, transport: str = 'auto') -> PCIDevice:
|
||||
"""Open an AMD GPU through tinygrad's transport-independent PCI interface."""
|
||||
if transport not in ('auto', 'usb', 'pci'): raise ValueError(f"unsupported transport {transport!r}")
|
||||
candidates = []
|
||||
if transport in ('auto', 'usb'):
|
||||
for vendor, product in USB_IDS:
|
||||
candidates += [(USBPCIDevice, dev) for dev in USB3.list_devices(vendor, product)]
|
||||
if transport in ('auto', 'pci'):
|
||||
candidates += System.list_devices(0x1002, NAVI31_DEVICES)
|
||||
if not candidates: raise RuntimeError(f"no supported {transport} AMD GPU found")
|
||||
if not 0 <= index < len(candidates): raise RuntimeError(f"device index {index} out of range (found {len(candidates)})")
|
||||
cls, descriptor = candidates[index]
|
||||
return cls("AM", *descriptor) if cls is USBPCIDevice else cls("AM", descriptor)
|
||||
|
||||
|
||||
class MMIO:
|
||||
"""Transport-independent byte view of BAR5."""
|
||||
def __init__(self, pci_dev: PCIDevice): self.bar = pci_dev.map_bar(5, fmt='B')
|
||||
|
||||
def read32(self, offset: int) -> int:
|
||||
return struct.unpack('<I', bytes(self.bar[offset:offset+4]))[0]
|
||||
|
||||
def write32(self, offset: int, value: int):
|
||||
self.write(offset, struct.pack('<I', value & 0xffffffff))
|
||||
|
||||
def read(self, offset: int, size: int) -> bytes:
|
||||
return bytes(self.bar[offset:offset+size])
|
||||
|
||||
def write(self, offset: int, data: bytes):
|
||||
self.bar[offset:offset+len(data)] = data
|
||||
|
||||
|
||||
def wait_until(fn, timeout: float, message: str, interval: float = 0.001):
|
||||
if timeout <= 0 or timeout > 60: raise ValueError("timeout must be in (0, 60] seconds")
|
||||
end = time.monotonic() + timeout
|
||||
while True:
|
||||
value = fn()
|
||||
if value: return value
|
||||
if time.monotonic() >= end: raise TimeoutError(message)
|
||||
time.sleep(interval)
|
||||
Executable
+292
@@ -0,0 +1,292 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Navi31 firmware-mediated flash access and ROM aperture dumping.
|
||||
|
||||
Early item streaming must run after autonomous PSP boot but before a host
|
||||
driver or AMDev loads SOS. A fully initialized SOS rejects those commands.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import argparse, struct, sys, time
|
||||
from pathlib import Path
|
||||
from common import MMIO, open_gpu, wait_until
|
||||
|
||||
ROM_CNTL, ROM_INDEX, ROM_DATA = 0x5A380, 0x5A390, 0x5A394
|
||||
FLASH_SIZE, INDEX_PAGE = 0x200000, 0x10000
|
||||
|
||||
def bswap32(value: int) -> int: return int.from_bytes(value.to_bytes(4, 'little'), 'big')
|
||||
|
||||
COMMAND_DATA, COMMAND, DOORBELL = 0x582D0, 0x582CC, 0x58224
|
||||
GET_BOOT_PARTITION, GET_FB_STATE, GET_TRANSFER_TYPE = 0x01, 0x06, 0x07
|
||||
START_TRANSFER, DATA_TRANSFER, END_TRANSFER = 0x08, 0x09, 0x0A
|
||||
SPI_GET_MODEL_ID = 0x0B
|
||||
LIVE_ADDR_LO, LIVE_ADDR_HI, LIVE_UPDATE = 0x02, 0x03, 0x04
|
||||
PSP_ERRORS = {
|
||||
0x01: "generic error", 0x02: "out of bounds", 0x03: "invalid parameter",
|
||||
0x04: "off-chip boot error", 0x05: "address not set", 0x06: "parse off-chip error",
|
||||
0x07: "address map error", 0x08: "parse on-chip error", 0x09: "full update error",
|
||||
0x0A: "partition update error", 0x0B: "map on-chip error", 0x0C: "write to SPI error",
|
||||
0x0D: "signature validation error", 0x0E: "invalid command", 0x0F: "signature not found",
|
||||
0x10: "state machine not initialized", 0x11: "state machine transfer error",
|
||||
0x12: "initialization error",
|
||||
}
|
||||
|
||||
|
||||
class PSPFlashMailbox:
|
||||
def __init__(self, pci_dev): self.mmio = MMIO(pci_dev)
|
||||
|
||||
def command(self, command: int, data: int | None = None, *, timeout: float = 10.0) -> tuple[int, int]:
|
||||
status = self.mmio.read32(COMMAND)
|
||||
if not status & 0x80000000:
|
||||
raise RuntimeError(f"PSP mailbox is not ready before command {command:#x}: status={status:#010x}")
|
||||
if data is not None: self.mmio.write32(COMMAND_DATA, data)
|
||||
self.mmio.write32(COMMAND, command << 16)
|
||||
self.mmio.write32(DOORBELL, 1)
|
||||
wait_until(lambda: self.mmio.read32(COMMAND) & 0x80000000, timeout,
|
||||
f"PSP mailbox command {command:#x} timed out")
|
||||
value = self.mmio.read32(COMMAND)
|
||||
return value & 0xffff, self.mmio.read32(COMMAND_DATA)
|
||||
|
||||
def require(self, command: int, data: int | None = None, *, timeout: float = 10.0, name: str = '') -> int:
|
||||
error, response = self.command(command, data, timeout=timeout)
|
||||
if error:
|
||||
detail = PSP_ERRORS.get(error, "unknown error")
|
||||
raise RuntimeError(f"PSP {name or hex(command)} failed: error={error:#x} ({detail})")
|
||||
return response
|
||||
|
||||
def probe(self) -> dict[str, tuple[int, int]]:
|
||||
result = {}
|
||||
for name, command in (("boot_partition", GET_BOOT_PARTITION), ("fb_state", GET_FB_STATE),
|
||||
("model_id", SPI_GET_MODEL_ID), ("transfer_type", GET_TRANSFER_TYPE)):
|
||||
result[name] = self.command(command)
|
||||
return result
|
||||
|
||||
def stream(self, payload: bytes, item_type: int, transfer_type: int | None = None):
|
||||
if not payload: raise ValueError("payload is empty")
|
||||
if len(payload) > 0xFFFFFF: raise ValueError("payload exceeds the mailbox's 24-bit size field")
|
||||
if len(payload) & 3: raise ValueError("payload size must be divisible by four")
|
||||
if not 0 <= item_type <= 0xff: raise ValueError("item type must fit in eight bits")
|
||||
if transfer_type is None: transfer_type = self.require(GET_TRANSFER_TYPE, name="GET_TRANSFER_TYPE")
|
||||
requested = transfer_type & 0xff
|
||||
print(f"firmware transfer_type={transfer_type:#x}", flush=True)
|
||||
if requested != item_type:
|
||||
raise RuntimeError(f"firmware requests item {requested:#x}, not {item_type:#x}")
|
||||
self.require(START_TRANSFER, (len(payload) << 8) | item_type, name="START_TRANSFER")
|
||||
sent, started = 0, time.monotonic()
|
||||
try:
|
||||
for offset in range(0, len(payload), 4):
|
||||
word = struct.unpack_from('<I', payload, offset)[0]
|
||||
self.require(DATA_TRANSFER, word, name=f"DATA_TRANSFER@{offset:#x}")
|
||||
sent = offset + 4
|
||||
if sent % 0x1000 == 0:
|
||||
print(f"{sent:#x}/{len(payload):#x} ({sent/(time.monotonic()-started)/1024:.1f} KiB/s)", flush=True)
|
||||
self.require(END_TRANSFER, (sent << 8) | item_type, timeout=60.0, name="END_TRANSFER")
|
||||
except BaseException:
|
||||
# Give firmware a chance to terminate an interrupted partial session. Do
|
||||
# not submit END_TRANSFER twice if firmware rejected the original END.
|
||||
if sent != len(payload):
|
||||
try: self.command(END_TRANSFER, (sent << 8) | item_type, timeout=10.0)
|
||||
except Exception: pass
|
||||
raise
|
||||
print(f"stream complete: type={item_type:#x} size={sent:#x} elapsed={time.monotonic()-started:.1f}s")
|
||||
|
||||
|
||||
def resolve_ifwi_item(image: bytes, item_type: int) -> tuple[int, bytes]:
|
||||
"""Resolve AMDVBFlash recovery-layout item types to exact IFWI bytes."""
|
||||
if item_type == 0x01: offset, size = 0, 0x54
|
||||
elif item_type in (0x02, 0x03):
|
||||
offset = 0x2000 if item_type == 0x02 else 0x3000
|
||||
if image[offset:offset+4] != b'$PSP': raise ValueError(f"invalid PSP directory at {offset:#x}")
|
||||
size = (struct.unpack_from('<I', image, offset + 8)[0] + 1) * 0x10
|
||||
elif item_type == 0x04: offset, size = 0x10000, 0x1000
|
||||
elif item_type == 0x05: offset, size = 0x11000, 0x1000
|
||||
elif item_type == 0x06: offset, size = 0x12000, 0x20
|
||||
elif item_type == 0x07: offset, size = 0x13000, 0x20
|
||||
elif item_type == 0x80: offset, size = 0x1000, 4
|
||||
elif item_type == 0x81:
|
||||
offset = struct.unpack_from('<I', image, 0x1000)[0]
|
||||
if image[offset:offset+4] != b'$SGN': raise ValueError("invalid $SGN table pointer")
|
||||
size = (struct.unpack_from('<I', image, offset + 8)[0] + 1) * 0x10
|
||||
elif 0x82 <= item_type <= 0x88:
|
||||
table = struct.unpack_from('<I', image, 0x1000)[0]
|
||||
if image[table:table+4] != b'$SGN': raise ValueError("invalid $SGN table pointer")
|
||||
wanted = item_type - 0x81 # 82h..88h map to SIGN_TYPE 1..7
|
||||
count = struct.unpack_from('<I', image, table + 8)[0]
|
||||
entries = [struct.unpack_from('<IIII', image, table + 0x10 + i*0x10) for i in range(count)]
|
||||
match = [entry for entry in entries if entry[0] == wanted]
|
||||
if len(match) != 1: raise ValueError(f"missing $SGN type {wanted}")
|
||||
_, _, size, offset = match[0]
|
||||
elif item_type == 0x89: offset, size = 0x1f0000, 0x100
|
||||
elif item_type == 0x08:
|
||||
# AMDVBFlash's GetPartitionDetails follows the first ISH entry (firmware ID
|
||||
# 0x13c) and streams its payload. PSP, not the host resolver, selects the
|
||||
# destination partition.
|
||||
offset = struct.unpack_from('<I', image, 0x12000 + 0x10)[0]
|
||||
size = struct.unpack_from('<I', image, 0x12000 + 0x18)[0]
|
||||
else:
|
||||
raise ValueError(f"IFWI resolver does not yet support requested item {item_type:#x}")
|
||||
payload = image[offset:offset+size]
|
||||
if len(payload) != size: raise ValueError(f"item {item_type:#x} extends beyond IFWI")
|
||||
print(f"resolved requested item {item_type:#x}: offset={offset:#x} size={size:#x}")
|
||||
return offset, payload
|
||||
|
||||
|
||||
class LivePSPFlash:
|
||||
"""Linux psp_v13_0_update_spirom protocol, used with SOS and trained VRAM."""
|
||||
def __init__(self, pci_dev): self.mailbox = PSPFlashMailbox(pci_dev)
|
||||
|
||||
def command(self, command: int, data: int | None = None, timeout: float = 10.0):
|
||||
# Same C2PMSG registers, but the live PSP command set uses IDs 2/3/4.
|
||||
return self.mailbox.require(command, data, timeout=timeout, name=f"LIVE_SPI_{command:#x}")
|
||||
|
||||
def update(self, mc_address: int):
|
||||
status = self.mailbox.mmio.read32(COMMAND)
|
||||
if not status & 0x80000000: raise RuntimeError(f"live PSP mailbox is not ready: {status:#x}")
|
||||
self.command(LIVE_ADDR_LO, mc_address & 0xffffffff)
|
||||
self.command(LIVE_ADDR_HI, mc_address >> 32)
|
||||
self.command(LIVE_UPDATE, timeout=60.0)
|
||||
|
||||
|
||||
def open_mailbox(args): return PSPFlashMailbox(open_gpu(args.device, args.transport))
|
||||
|
||||
|
||||
def reject_unvalidated_firmware_write():
|
||||
raise RuntimeError("firmware writes are disabled: stock reflash validation failed; use romless.py for recovery")
|
||||
|
||||
|
||||
def cmd_probe(args):
|
||||
result = open_mailbox(args).probe()
|
||||
for name, (error, response) in result.items(): print(f"{name}: error={error:#x} response={response:#x}")
|
||||
if result['transfer_type'][0] == 0xA: print("update commands gated: reset card and do not initialize AMDev/SOS", file=sys.stderr)
|
||||
|
||||
|
||||
def cmd_stream(args):
|
||||
if not args.yes: raise RuntimeError("refusing to stream without --yes")
|
||||
reject_unvalidated_firmware_write()
|
||||
payload = Path(args.image).read_bytes()
|
||||
open_mailbox(args).stream(payload, args.item_type)
|
||||
|
||||
|
||||
def cmd_ifwi_step(args):
|
||||
if not args.yes: raise RuntimeError("refusing to stream without --yes")
|
||||
reject_unvalidated_firmware_write()
|
||||
image = Path(args.ifwi).read_bytes()
|
||||
if len(image) != 0x200000: raise ValueError("Navi31 IFWI image must be exactly 2 MiB")
|
||||
mailbox = open_mailbox(args)
|
||||
state = mailbox.require(GET_TRANSFER_TYPE, name="GET_TRANSFER_TYPE")
|
||||
request = state & 0xff
|
||||
_, payload = resolve_ifwi_item(image, request)
|
||||
mailbox.stream(payload, request, transfer_type=state)
|
||||
next_request = mailbox.require(GET_TRANSFER_TYPE, name="GET_TRANSFER_TYPE")
|
||||
print(f"next firmware transfer_type={next_request:#x}")
|
||||
|
||||
|
||||
def cmd_ifwi_all(args):
|
||||
if not args.yes: raise RuntimeError("refusing to stream without --yes")
|
||||
image = Path(args.ifwi).read_bytes()
|
||||
if len(image) != 0x200000: raise ValueError("Navi31 IFWI image must be exactly 2 MiB")
|
||||
mailbox = open_mailbox(args)
|
||||
current = mailbox.require(GET_TRANSFER_TYPE, name="GET_TRANSFER_TYPE")
|
||||
for step in range(19): # Navi31 ROMItemCount from AMDVBFlash ASICDetails.xml
|
||||
request, phase = current & 0xff, current >> 8
|
||||
print(f"IFWI step {step}: state={current:#x} item={request:#x} phase={phase}", flush=True)
|
||||
_, payload = resolve_ifwi_item(image, request)
|
||||
mailbox.stream(payload, request, transfer_type=current)
|
||||
# AMDVBFlash tests the high byte belonging to the item just streamed. Phase
|
||||
# 2 terminates the loop only after that item has completed successfully.
|
||||
if phase == 2:
|
||||
print(f"IFWI stream complete after terminal state {current:#x}; hard power cycle required")
|
||||
return
|
||||
current = mailbox.require(GET_TRANSFER_TYPE, name="GET_TRANSFER_TYPE")
|
||||
raise RuntimeError(f"IFWI stream did not reach terminal phase after 19 items (state={current:#x})")
|
||||
|
||||
|
||||
def cmd_live_flash(args):
|
||||
if not args.yes: raise RuntimeError("refusing to flash without --yes")
|
||||
reject_unvalidated_firmware_write()
|
||||
image = Path(args.ifwi).read_bytes()
|
||||
if not image or len(image) > 16 * 1024 * 1024 or len(image) & 3:
|
||||
raise ValueError("live PSP image must be non-empty, 4-byte aligned, and at most 16 MiB")
|
||||
pci_dev = open_gpu(args.device, args.transport)
|
||||
from tinygrad.runtime.support.am.amdev import AMDev
|
||||
started = time.monotonic()
|
||||
adev = AMDev(pci_dev)
|
||||
print(f"AMDev booted, SOS alive={adev.psp.is_sos_alive()}", flush=True)
|
||||
paddr = adev.mm.palloc(len(image), align=0x1000, zero=False)
|
||||
try:
|
||||
adev.vram.view(paddr, len(image), 'B')[:] = image
|
||||
adev.gmc.flush_hdp()
|
||||
mc_address = adev.paddr2mc(paddr)
|
||||
print(f"staged IFWI at VRAM paddr={paddr:#x} mc={mc_address:#x}", flush=True)
|
||||
LivePSPFlash(pci_dev).update(mc_address)
|
||||
print(f"live PSP flash update complete in {time.monotonic()-started:.1f}s")
|
||||
finally:
|
||||
adev.mm.pfree(paddr)
|
||||
|
||||
|
||||
def cmd_dump(args):
|
||||
import hashlib
|
||||
pci_dev = open_gpu(args.device, args.transport)
|
||||
mmio, output, started = MMIO(pci_dev), bytearray(), time.monotonic()
|
||||
original_cntl, original_index = mmio.read32(ROM_CNTL), mmio.read32(ROM_INDEX)
|
||||
if original_cntl == 0xFFFFFFFF:
|
||||
raise RuntimeError("raw SMUIO ROM controller is unavailable; the SOC15 function-ROM aperture is not a raw SPI dump")
|
||||
try:
|
||||
# ROM_DATA must be read one dword at a time; a block read increments MMIO
|
||||
# addresses rather than repeatedly reading the flash aperture register.
|
||||
mmio.write32(ROM_CNTL, bswap32(original_cntl | (1 << 29)))
|
||||
for page in range(0, FLASH_SIZE, INDEX_PAGE):
|
||||
mmio.write32(ROM_INDEX, bswap32(page >> 8))
|
||||
for _ in range(INDEX_PAGE // 4): output += struct.pack('<I', mmio.read32(ROM_DATA))
|
||||
print(f"{page+INDEX_PAGE:#08x}/{FLASH_SIZE:#08x}", flush=True)
|
||||
finally:
|
||||
mmio.write32(ROM_INDEX, bswap32(original_index))
|
||||
mmio.write32(ROM_CNTL, bswap32(original_cntl))
|
||||
if len(output) != FLASH_SIZE or output[:4] != b'\xaa\x55\xaa\x55':
|
||||
raise RuntimeError(f"invalid raw flash dump: size={len(output):#x} magic={output[:4].hex()}")
|
||||
if output[:FLASH_SIZE//2] == output[FLASH_SIZE//2:]:
|
||||
raise RuntimeError("ROM aperture contains mirrored 1 MiB halves; refusing to write a non-raw 2 MiB dump")
|
||||
Path(args.output).write_bytes(output)
|
||||
print(f"dumped {len(output):#x} bytes in {time.monotonic()-started:.1f}s sha256={hashlib.sha256(output).hexdigest()}")
|
||||
|
||||
|
||||
def parser():
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--device', type=int, default=0, help='device index for the selected transport')
|
||||
p.add_argument('--transport', choices=('auto', 'usb', 'pci'), default='auto', help='PCIe transport (default: USB first, then native PCI)')
|
||||
sub = p.add_subparsers(dest='command', required=True)
|
||||
sub.add_parser('probe', help='query firmware mailbox state without writing').set_defaults(func=cmd_probe)
|
||||
|
||||
s = sub.add_parser('stream', help='stream one exact PSP ROM-item payload')
|
||||
s.add_argument('item_type', type=lambda x:int(x, 0))
|
||||
s.add_argument('image')
|
||||
s.add_argument('--yes', action='store_true')
|
||||
s.set_defaults(func=cmd_stream)
|
||||
|
||||
v = sub.add_parser('ifwi-step', help='resolve and stream the next early-firmware-requested item from a 2 MiB IFWI')
|
||||
v.add_argument('ifwi')
|
||||
v.add_argument('--yes', action='store_true')
|
||||
v.set_defaults(func=cmd_ifwi_step)
|
||||
|
||||
a = sub.add_parser('ifwi-all', help='stream requested IFWI items until firmware reports completion')
|
||||
a.add_argument('ifwi')
|
||||
a.add_argument('--yes', action='store_true')
|
||||
a.set_defaults(func=cmd_ifwi_all)
|
||||
|
||||
l = sub.add_parser('live-flash', help='stage an image in VRAM and invoke the PSP v13 live-update command')
|
||||
l.add_argument('ifwi')
|
||||
l.add_argument('--yes', action='store_true')
|
||||
l.set_defaults(func=cmd_live_flash)
|
||||
|
||||
d = sub.add_parser('dump', help='dump the exact 2 MiB flash through ROM_INDEX/ROM_DATA')
|
||||
d.add_argument('output')
|
||||
d.set_defaults(func=cmd_dump)
|
||||
return p
|
||||
|
||||
|
||||
def main():
|
||||
args = parser().parse_args()
|
||||
try: args.func(args)
|
||||
except (RuntimeError, TimeoutError, ValueError, OSError) as error:
|
||||
print(f"error: {error}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
if __name__ == '__main__': main()
|
||||
Executable
+249
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Direct Navi31 ROM_SW access for GD25LQ16E-class 2 MiB SPI flash."""
|
||||
from __future__ import annotations
|
||||
import argparse, hashlib, sys, time
|
||||
from pathlib import Path
|
||||
from common import MMIO, open_gpu, wait_until
|
||||
|
||||
FLASH_SIZE, SECTOR_SIZE, PAGE_SIZE, MAX_DATA = 0x200000, 0x1000, 0x100, 0x100
|
||||
ROM_CNTL, PAGE_MIRROR_CNTL = 0x5A380, 0x5A384
|
||||
ROM_SW_CNTL, ROM_SW_STATUS, ROM_SW_COMMAND, ROM_SW_DATA = 0x5A3A0, 0x5A3A4, 0x5A3A8, 0x5A3B0
|
||||
GPIO_PAD_MASK, GPIO_PAD_A, GPIO_PAD_EN = 0x5A504, 0x5A508, 0x5A510
|
||||
SPI_GPIO_BITS, RETURN_DATA_EN = 0x780, 0x80000
|
||||
EXPECTED_JEDEC = b'\xc8\x60\x15'
|
||||
|
||||
|
||||
class Navi31SPI:
|
||||
def __init__(self, pci_dev, prescale: int = 8):
|
||||
if not 0 <= prescale <= 15: raise ValueError("prescale must be 0..15")
|
||||
self.mmio = MMIO(pci_dev)
|
||||
rc = self.mmio.read32(ROM_CNTL)
|
||||
# Select the prescaler instead of inheriting a potentially unusable BL value.
|
||||
self.mmio.write32(ROM_CNTL, (rc & 0xE0FFFFFF) | (1 << 28) | (prescale << 24) | 1)
|
||||
|
||||
def transfer(self, opcode: int, *, address: int = 0, address_len: int = 0,
|
||||
data_out: bytes = b'', data_in: int = 0, timeout: float = 2.0) -> bytes:
|
||||
if data_out and data_in: raise ValueError("simultaneous TX and RX is unsupported")
|
||||
if not 0 <= address_len <= 3: raise ValueError("address_len must be 0..3")
|
||||
count = len(data_out) if data_out else data_in
|
||||
if not 0 <= count <= MAX_DATA: raise ValueError(f"transfer data must be <= {MAX_DATA} bytes")
|
||||
ncmd = 1 + address_len
|
||||
m = self.mmio
|
||||
gpio_mask, gpio_a, gpio_en = m.read32(GPIO_PAD_MASK), m.read32(GPIO_PAD_A), m.read32(GPIO_PAD_EN)
|
||||
page_mirror, rom_cntl = m.read32(PAGE_MIRROR_CNTL), m.read32(ROM_CNTL)
|
||||
try:
|
||||
m.write32(GPIO_PAD_MASK, gpio_mask & ~SPI_GPIO_BITS)
|
||||
m.write32(GPIO_PAD_A, gpio_a & ~SPI_GPIO_BITS)
|
||||
m.write32(GPIO_PAD_EN, gpio_en & ~SPI_GPIO_BITS)
|
||||
m.write32(PAGE_MIRROR_CNTL, (page_mirror & 0xF1FFFFFF) | 0x06000000)
|
||||
m.write32(ROM_CNTL, (rom_cntl & ~0xF) | 8)
|
||||
m.write32(ROM_SW_CNTL, 0)
|
||||
m.write32(ROM_SW_STATUS, 0)
|
||||
if m.read32(ROM_SW_STATUS) != 0: raise RuntimeError("ROM_SW_STATUS did not clear")
|
||||
|
||||
# Navi31 serializes the low instruction byte first, followed by ADDRESS[23:0].
|
||||
m.write32(ROM_SW_COMMAND, ((address & 0xFFFFFF) << 8) | (opcode & 0xFF))
|
||||
for offset in range(0, len(data_out), 4):
|
||||
word = data_out[offset:offset+4].ljust(4, b'\0')
|
||||
m.write32(ROM_SW_DATA + offset, int.from_bytes(word, 'big'))
|
||||
|
||||
control = ((ncmd - 1) << 16) | (RETURN_DATA_EN if data_in else 0) | count
|
||||
m.write32(ROM_SW_CNTL, control)
|
||||
m.read32(ROM_SW_CNTL) # posted-write flush
|
||||
wait_until(lambda: m.read32(ROM_SW_STATUS) & 1, timeout,
|
||||
f"ROM_SW transaction timeout (status={m.read32(ROM_SW_STATUS):#x}); engine may be gated after SOS boot")
|
||||
return m.read(ROM_SW_DATA, (data_in + 3) & ~3)[:data_in] if data_in else b''
|
||||
finally:
|
||||
m.write32(ROM_SW_CNTL, 0)
|
||||
m.write32(ROM_SW_STATUS, 0)
|
||||
m.write32(ROM_CNTL, rom_cntl)
|
||||
m.write32(PAGE_MIRROR_CNTL, page_mirror)
|
||||
m.write32(GPIO_PAD_A, gpio_a)
|
||||
m.write32(GPIO_PAD_EN, gpio_en)
|
||||
m.write32(GPIO_PAD_MASK, gpio_mask)
|
||||
|
||||
|
||||
class GD25LQ16E:
|
||||
def __init__(self, spi: Navi31SPI): self.spi = spi
|
||||
|
||||
def read_register(self, opcode: int, count: int = 1) -> bytes:
|
||||
# Navi31 exposes the preceding transaction's RX capture. Prime identically.
|
||||
self.spi.transfer(opcode, data_in=max(2, count))
|
||||
return self.spi.transfer(opcode, data_in=count)
|
||||
|
||||
def status(self, opcode: int = 0x05) -> int: return self.read_register(opcode)[0]
|
||||
def rdid(self) -> bytes: return self.read_register(0x9F, 4)
|
||||
|
||||
def sfdp(self, count: int = 20) -> bytes:
|
||||
# 5Ah has one dummy byte after its 24-bit address; retain it for diagnostics.
|
||||
self.spi.transfer(0x5A, address_len=3, data_in=count)
|
||||
return self.spi.transfer(0x5A, address_len=3, data_in=count)
|
||||
|
||||
def wait_idle(self, timeout: float = 2.0) -> int:
|
||||
end = time.monotonic() + timeout
|
||||
while time.monotonic() < end:
|
||||
sr1 = self.status()
|
||||
if not sr1 & 1: return sr1
|
||||
time.sleep(0.002)
|
||||
raise TimeoutError(f"flash remained busy for {timeout}s")
|
||||
|
||||
def write_enable(self):
|
||||
self.spi.transfer(0x06)
|
||||
sr1 = self.status()
|
||||
if not sr1 & 2: raise RuntimeError(f"WREN failed (SR1={sr1:#04x})")
|
||||
|
||||
def clear_cmp(self):
|
||||
sr1, sr2 = self.status(), self.status(0x35)
|
||||
if not sr2 & 0x40: return False
|
||||
self.write_enable()
|
||||
# BUSY/WEL are not writable; preserve all protection/QE fields except CMP.
|
||||
self.spi.transfer(0x01, data_out=bytes((sr1 & 0xFC, sr2 & ~0x40)))
|
||||
self.wait_idle(1.0)
|
||||
new_sr2 = self.status(0x35)
|
||||
if new_sr2 & 0x40: raise RuntimeError(f"failed to clear CMP (SR2={new_sr2:#04x})")
|
||||
return True
|
||||
|
||||
def erase_sector(self, address: int):
|
||||
if address & (SECTOR_SIZE - 1): raise ValueError("sector address is not 4 KiB aligned")
|
||||
self.write_enable()
|
||||
self.spi.transfer(0x20, address=address, address_len=3)
|
||||
self.wait_idle(2.0)
|
||||
|
||||
def program_page(self, address: int, data: bytes):
|
||||
if not data or len(data) > PAGE_SIZE or (address & 0xFF) + len(data) > PAGE_SIZE:
|
||||
raise ValueError("page program crosses a 256-byte boundary")
|
||||
self.write_enable()
|
||||
self.spi.transfer(0x02, address=address, address_len=3, data_out=data)
|
||||
self.wait_idle(1.0)
|
||||
|
||||
def read(self, address: int, count: int) -> bytes:
|
||||
if address < 0 or count < 0 or address + count > FLASH_SIZE: raise ValueError("read outside 2 MiB flash")
|
||||
output = bytearray()
|
||||
while count:
|
||||
size = min(count, MAX_DATA)
|
||||
self.spi.transfer(0x03, address=address, address_len=3, data_in=size)
|
||||
output += self.spi.transfer(0x03, address=address, address_len=3, data_in=size)
|
||||
address, count = address + size, count - size
|
||||
return bytes(output)
|
||||
|
||||
|
||||
def has_jedec(raw: bytes) -> bool:
|
||||
return EXPECTED_JEDEC in raw + raw[:2]
|
||||
|
||||
|
||||
def open_flash(args) -> GD25LQ16E:
|
||||
flash = GD25LQ16E(Navi31SPI(open_gpu(args.device, 'usb'), args.prescale))
|
||||
raw = flash.rdid()
|
||||
if not has_jedec(raw): raise RuntimeError(f"unexpected GD25LQ16E JEDEC capture: {raw.hex()}")
|
||||
return flash
|
||||
|
||||
|
||||
def cmd_info(args):
|
||||
f = open_flash(args)
|
||||
sr1, sr2, sr3 = f.status(), f.status(0x35), f.status(0x15)
|
||||
sfdp = f.sfdp(24)
|
||||
pos = sfdp.find(b'SFDP')
|
||||
print(f"JEDEC capture: {f.rdid().hex()} (C8 60 15 detected)")
|
||||
print(f"SR1/SR2/SR3: {sr1:02x}/{sr2:02x}/{sr3:02x} CMP={'set' if sr2 & 0x40 else 'clear'}")
|
||||
print(f"SFDP capture: {sfdp.hex()} signature_offset={pos}")
|
||||
|
||||
|
||||
def cmd_read(args):
|
||||
data = open_flash(args).read(args.address, args.size)
|
||||
if args.output: Path(args.output).write_bytes(data)
|
||||
else: print(data.hex())
|
||||
|
||||
|
||||
def cmd_dump(args):
|
||||
f = open_flash(args)
|
||||
out = Path(args.output)
|
||||
digest = hashlib.sha256()
|
||||
with out.open('wb') as file:
|
||||
for address in range(0, FLASH_SIZE, SECTOR_SIZE):
|
||||
data = f.read(address, SECTOR_SIZE)
|
||||
file.write(data)
|
||||
digest.update(data)
|
||||
if not (address & 0xFFFF): print(f"{address + SECTOR_SIZE:#08x}/{FLASH_SIZE:#08x}", flush=True)
|
||||
print(f"wrote {out} sha256={digest.hexdigest()}")
|
||||
|
||||
|
||||
def cmd_verify(args):
|
||||
expected = Path(args.image).read_bytes()
|
||||
if len(expected) != FLASH_SIZE: raise ValueError(f"image must be exactly {FLASH_SIZE:#x} bytes")
|
||||
f = open_flash(args)
|
||||
digest = hashlib.sha256()
|
||||
for address in range(0, FLASH_SIZE, SECTOR_SIZE):
|
||||
got, wanted = f.read(address, SECTOR_SIZE), expected[address:address+SECTOR_SIZE]
|
||||
digest.update(got)
|
||||
if got != wanted:
|
||||
index = next(i for i, (a, b) in enumerate(zip(got, wanted)) if a != b)
|
||||
raise RuntimeError(f"verify mismatch at {address+index:#x}: flash={got[index]:02x} image={wanted[index]:02x}")
|
||||
print(f"verified {FLASH_SIZE:#x} bytes sha256={digest.hexdigest()}")
|
||||
|
||||
|
||||
def cmd_flash(args):
|
||||
if not args.yes: raise RuntimeError("refusing to write without --yes")
|
||||
image = Path(args.image).read_bytes()
|
||||
if len(image) != FLASH_SIZE: raise ValueError(f"image must be exactly {FLASH_SIZE:#x} bytes")
|
||||
total_sectors = FLASH_SIZE // SECTOR_SIZE
|
||||
start, count = args.start_sector, args.sector_count if args.sector_count is not None else total_sectors - args.start_sector
|
||||
if not 0 <= start < total_sectors or not 1 <= count <= total_sectors - start: raise ValueError("invalid sector range")
|
||||
f = open_flash(args)
|
||||
if f.status(0x35) & 0x40:
|
||||
if not args.clear_cmp: raise RuntimeError("CMP protects the full array; rerun with --clear-cmp")
|
||||
f.clear_cmp()
|
||||
print("cleared SR2.CMP", flush=True)
|
||||
begin = time.monotonic()
|
||||
for sector in range(start, start + count):
|
||||
address = sector * SECTOR_SIZE
|
||||
wanted = image[address:address+SECTOR_SIZE]
|
||||
f.erase_sector(address)
|
||||
for offset in range(0, SECTOR_SIZE, PAGE_SIZE):
|
||||
page = wanted[offset:offset+PAGE_SIZE]
|
||||
if page != b'\xff' * PAGE_SIZE: f.program_page(address + offset, page)
|
||||
got = f.read(address, SECTOR_SIZE)
|
||||
if got != wanted:
|
||||
index = next(i for i, (a, b) in enumerate(zip(got, wanted)) if a != b)
|
||||
raise RuntimeError(f"verify mismatch at {address+index:#x}: flash={got[index]:02x} image={wanted[index]:02x}")
|
||||
print(f"OK sector {sector:03d}/{total_sectors-1} @{address:#07x} elapsed={time.monotonic()-begin:.1f}s", flush=True)
|
||||
|
||||
|
||||
def parser():
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--device', type=int, default=0, help='USB bridge device index')
|
||||
p.add_argument('--prescale', type=int, default=8, help='SCK prescaler 0..15 (default: 8)')
|
||||
sub = p.add_subparsers(dest='command', required=True)
|
||||
sub.add_parser('info', help='read JEDEC, status and SFDP').set_defaults(func=cmd_info)
|
||||
|
||||
r = sub.add_parser('read', help='read a flash range')
|
||||
r.add_argument('address', type=lambda x:int(x, 0))
|
||||
r.add_argument('size', type=lambda x:int(x, 0))
|
||||
r.add_argument('-o', '--output')
|
||||
r.set_defaults(func=cmd_read)
|
||||
|
||||
d = sub.add_parser('dump', help='dump the complete 2 MiB flash')
|
||||
d.add_argument('output')
|
||||
d.set_defaults(func=cmd_dump)
|
||||
|
||||
v = sub.add_parser('verify', help='compare the complete flash with an image')
|
||||
v.add_argument('image')
|
||||
v.set_defaults(func=cmd_verify)
|
||||
|
||||
w = sub.add_parser('flash', help='erase, program, and verify one or more sectors')
|
||||
w.add_argument('image')
|
||||
w.add_argument('--start-sector', type=lambda x:int(x, 0), default=0)
|
||||
w.add_argument('--sector-count', type=lambda x:int(x, 0))
|
||||
w.add_argument('--clear-cmp', action='store_true')
|
||||
w.add_argument('--yes', action='store_true')
|
||||
w.set_defaults(func=cmd_flash)
|
||||
return p
|
||||
|
||||
|
||||
def main():
|
||||
args = parser().parse_args()
|
||||
try: args.func(args)
|
||||
except (RuntimeError, TimeoutError, ValueError, OSError) as error:
|
||||
print(f"error: {error}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
if __name__ == '__main__': main()
|
||||
@@ -66,6 +66,8 @@ class AMSMI(AMDev):
|
||||
def __init__(self, pcibus, vram_bar:MMIOInterface, doorbell_bar:MMIOInterface, mmio_bar:MMIOInterface):
|
||||
self.pcibus, self.devfmt = pcibus, pcibus
|
||||
self.vram, self.doorbell64, self.mmio = vram_bar, doorbell_bar, mmio_bar
|
||||
self.is_vf = bool(self.mmio[am.mmRCC_IOV_FUNC_IDENTIFIER] & 1)
|
||||
self.vf_rlc_gated:list[tuple[int, int]] = []
|
||||
self.pci_state = self.read_pci_state()
|
||||
if self.pci_state == "D0": self._init_from_d0()
|
||||
|
||||
@@ -84,7 +86,8 @@ class AMSMI(AMDev):
|
||||
with open(f"/sys/bus/pci/devices/{self.pcibus}/power_state", "r") as f: return f.read().strip().rstrip()
|
||||
|
||||
class SMICtx:
|
||||
def __init__(self):
|
||||
def __init__(self, dev_filter=None):
|
||||
self.dev_filter = dev_filter
|
||||
self.devs = []
|
||||
self.opened_pcidevs = []
|
||||
self.opened_pci_resources = {}
|
||||
@@ -135,6 +138,7 @@ class SMICtx:
|
||||
pattern = os.path.join('/tmp', 'am_*.lock')
|
||||
for d in [f[8:-5] for f in glob.glob(pattern)]:
|
||||
if d.startswith("usb"): continue
|
||||
if self.dev_filter is not None and d != self.dev_filter: continue
|
||||
if d not in self.opened_pcidevs:
|
||||
self._open_am_device(d)
|
||||
|
||||
@@ -406,7 +410,7 @@ if __name__ == "__main__":
|
||||
|
||||
try:
|
||||
if not args.list: os.system('clear')
|
||||
smi_ctx = SMICtx()
|
||||
smi_ctx = SMICtx(args.dev)
|
||||
while True:
|
||||
smi_ctx.rescan_devs()
|
||||
smi_ctx.draw(args.list)
|
||||
|
||||
+9
-9
@@ -35,7 +35,7 @@ class WallTimeEvent:
|
||||
return self
|
||||
def __exit__(self, *_):
|
||||
self.time = time.monotonic() - self.start
|
||||
_events[self.event]["wall"].append(self.time)
|
||||
_events[self.event]["wall"].append((self.time, BENCHMARK_LOG.value))
|
||||
return False
|
||||
|
||||
class KernelTimeEvent:
|
||||
@@ -47,19 +47,19 @@ class KernelTimeEvent:
|
||||
self.start = GlobalCounters.time_sum_s
|
||||
return self
|
||||
def __exit__(self, *_):
|
||||
_events[self.event]["kernel"].append(GlobalCounters.time_sum_s - self.start)
|
||||
_events[self.event]["kernel"].append((GlobalCounters.time_sum_s - self.start, BENCHMARK_LOG.value))
|
||||
return False
|
||||
|
||||
def log_event_instant(event:InstantBenchEvent, value:float):
|
||||
_events[event].append(value)
|
||||
_events[event].append((value, BENCHMARK_LOG.value))
|
||||
|
||||
if BENCHMARK_LOG:
|
||||
INFLUXDB_HOST = getenv("INFLUXDB_HOST", "")
|
||||
INFLUXDB_ORG = getenv("INFLUXDB_ORG", "tiny")
|
||||
INFLUXDB_TOKEN = getenv("INFLUXDB_TOKEN", "")
|
||||
|
||||
def _create_point(run_id, i, attempt, ref, commit, name, value, run):
|
||||
point = Point(BENCHMARK_LOG.value).tag("id", run_id).tag("index", i)
|
||||
def _create_point(run_id, i, attempt, ref, commit, name, value, log_name, run):
|
||||
point = Point(log_name.replace(':', '_').replace('.', '_')).tag("id", run_id).tag("index", i)
|
||||
point = point.tag("device", Device.DEFAULT)
|
||||
point = point.tag("attempt", attempt).tag("ref", ref).tag("commit", commit)
|
||||
point = point.field(name, value).field("x", run)
|
||||
@@ -91,12 +91,12 @@ if BENCHMARK_LOG:
|
||||
run_id = str(uuid.uuid4())
|
||||
if isinstance(event, BenchEvent):
|
||||
for event_type, values in _events[event].items():
|
||||
for i, value in enumerate(values):
|
||||
point = _create_point(run_id, i, attempt, ref, commit, f"{event.value}_{event_type}", value, run)
|
||||
for i, (value, log_name) in enumerate(values):
|
||||
point = _create_point(run_id, i, attempt, ref, commit, f"{event.value}_{event_type}", value, log_name, run)
|
||||
points.append(point)
|
||||
else:
|
||||
for i, value in enumerate(_events[event]):
|
||||
point = _create_point(run_id, i, attempt, ref, commit, event.value, value, run)
|
||||
for i, (value, log_name) in enumerate(_events[event]):
|
||||
point = _create_point(run_id, i, attempt, ref, commit, event.value, value, log_name, run)
|
||||
points.append(point)
|
||||
|
||||
write_options = WriteOptions(write_type=WriteType.synchronous, retry_interval=5000, max_retries=5, max_retry_delay=30000, exponential_base=2)
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Benchmark Kimi-Linear load, prefill, and decode on its TP4 checkpoint."""
|
||||
import argparse, resource, time
|
||||
from tinygrad import Device, TinyJit
|
||||
from tinygrad.helpers import profile_marker
|
||||
from tinygrad.llm.kimi import load_kimi
|
||||
|
||||
def sync(devices:int) -> None:
|
||||
for i in range(devices): Device[f"AMD:{i}"].synchronize()
|
||||
|
||||
def timed_next(gen, devices:int) -> tuple[int, float]:
|
||||
begin = time.perf_counter()
|
||||
token = next(gen)
|
||||
sync(devices)
|
||||
return token, time.perf_counter()-begin
|
||||
|
||||
def fresh_generate(model, prompt:list[int], chunk_size:int):
|
||||
# Force recurrent/KV state reset so repeated runs and chunk sweeps measure the entire prompt,
|
||||
# rather than silently reusing the prefix cached by the previous measurement.
|
||||
model._cached_tokens = [-1] * len(prompt)
|
||||
return model.generate(prompt.copy(), chunk_size=chunk_size)
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("model", help="converted Kimi-Linear-48B-A3B MXFP4-v2 directory")
|
||||
parser.add_argument("--devices", type=int, default=4)
|
||||
parser.add_argument("--max-context", type=int, default=128)
|
||||
parser.add_argument("--prompt-tokens", type=int, default=32)
|
||||
parser.add_argument("--decode-tokens", type=int, default=8)
|
||||
parser.add_argument("--chunk-size", type=int, default=32)
|
||||
parser.add_argument("--sweep-chunks", help="comma-separated prefill chunk sizes; uses the fastest for decode")
|
||||
args = parser.parse_args()
|
||||
if args.prompt_tokens < 1 or args.prompt_tokens + args.decode_tokens + 1 > args.max_context:
|
||||
raise ValueError("prompt and decode tokens must fit within --max-context")
|
||||
|
||||
begin = time.perf_counter()
|
||||
model = load_kimi(args.model, max_context=args.max_context, devices=args.devices)
|
||||
sync(args.devices)
|
||||
print(f"load: {time.perf_counter()-begin:.3f}s", flush=True)
|
||||
|
||||
prompt = [1] + [1000+i%1000 for i in range(args.prompt_tokens-1)]
|
||||
chunks = [int(x) for x in args.sweep_chunks.split(",")] if args.sweep_chunks else [args.chunk_size]
|
||||
if any(x < 1 or x > args.prompt_tokens for x in chunks): raise ValueError("prefill chunks must be between 1 and --prompt-tokens")
|
||||
timings:list[tuple[float, int]] = []
|
||||
prefill_jits:dict[int, TinyJit] = {}
|
||||
for chunk in chunks:
|
||||
# Recurrent prefill has a static token dimension. Give each swept shape its own capture;
|
||||
# the rollout JIT remains shared and independently benchmarks chunk 1/decode.
|
||||
if chunk != 1: model.prefill_jit = TinyJit(model.forward)
|
||||
cold = fresh_generate(model, prompt, chunk)
|
||||
first, cold_prefill = timed_next(cold, args.devices)
|
||||
print(f"chunk {chunk}: cold prefill {cold_prefill:.3f}s, token={first}", flush=True)
|
||||
warm = fresh_generate(model, prompt, chunk)
|
||||
warm_first, prefill = timed_next(warm, args.devices)
|
||||
if first != warm_first: raise RuntimeError(f"chunk {chunk} is not repeatable: cold={first}, warm={warm_first}")
|
||||
timings.append((prefill, chunk))
|
||||
if chunk != 1: prefill_jits[chunk] = model.prefill_jit
|
||||
print(f"chunk {chunk}: prefill {prefill:.3f}s ({args.prompt_tokens/prefill:.3f} tok/s), token={first}", flush=True)
|
||||
|
||||
prefill, best_chunk = min(timings)
|
||||
if best_chunk != 1: model.prefill_jit = prefill_jits[best_chunk]
|
||||
warm = fresh_generate(model, prompt, best_chunk)
|
||||
first, replay_prefill = timed_next(warm, args.devices)
|
||||
_, cold_decode = timed_next(warm, args.devices)
|
||||
_, capture_decode = timed_next(warm, args.devices)
|
||||
print(f"selected chunk: {best_chunk}; prefill replay {replay_prefill:.3f}s "
|
||||
f"({args.prompt_tokens/replay_prefill:.3f} tok/s), token={first}", flush=True)
|
||||
print(f"cold decode: {cold_decode:.3f}s", flush=True)
|
||||
print(f"capture decode: {capture_decode:.3f}s", flush=True)
|
||||
profile_marker("kimi decode steady start")
|
||||
begin = time.perf_counter()
|
||||
output = [next(warm) for _ in range(args.decode_tokens)]
|
||||
sync(args.devices)
|
||||
decode = time.perf_counter()-begin
|
||||
profile_marker("kimi decode steady end")
|
||||
print(f"decode: {decode:.3f}s ({args.decode_tokens/decode:.3f} tok/s, {decode/args.decode_tokens*1e3:.3f} ms/tok), output={output}", flush=True)
|
||||
print(f"peak RSS: {resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024:.1f} MiB", flush=True)
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -1,83 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Bounded correctness and load/prefill/decode benchmark for the official TP8 Kimi K3 checkpoint."""
|
||||
import argparse, resource, time
|
||||
|
||||
from tinygrad import Device
|
||||
from tinygrad.helpers import profile_marker
|
||||
from tinygrad.llm.cli import KimiK3Template, SimpleTokenizer
|
||||
from tinygrad.llm.kimi_k3 import load_kimi_k3, load_kimi_tokenizer_data
|
||||
|
||||
def sync(devices:int) -> None:
|
||||
for i in range(devices): Device[f"AMD:{i}"].synchronize()
|
||||
|
||||
def fresh_generate(model, prompt:list[int], chunk_size:int):
|
||||
# Never reuse a prefix or recurrent state across correctness/benchmark trials.
|
||||
model._cached_tokens = [-1] * len(prompt)
|
||||
return model.generate(prompt.copy(), chunk_size=chunk_size, temperature=0.0)
|
||||
|
||||
def timed_next(gen, devices:int) -> tuple[int, float]:
|
||||
begin = time.perf_counter()
|
||||
token = next(gen)
|
||||
sync(devices)
|
||||
return token, time.perf_counter()-begin
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("model", help="official unmodified Kimi K3 checkpoint directory")
|
||||
parser.add_argument("--devices", type=int, default=8)
|
||||
parser.add_argument("--max-context", type=int, default=128)
|
||||
parser.add_argument("--prompt", default="Reply with exactly: OK")
|
||||
parser.add_argument("--stable-tokens", type=int, default=8)
|
||||
parser.add_argument("--decode-tokens", type=int, default=8)
|
||||
parser.add_argument("--chunk-size", type=int, default=128)
|
||||
args = parser.parse_args()
|
||||
|
||||
begin = time.perf_counter()
|
||||
model = load_kimi_k3(args.model, max_context=args.max_context, devices=args.devices)
|
||||
sync(args.devices)
|
||||
load_time = time.perf_counter()-begin
|
||||
print(f"load: {load_time:.3f}s", flush=True)
|
||||
|
||||
normal, special, bos, eos = load_kimi_tokenizer_data(args.model)
|
||||
tok = SimpleTokenizer(normal, special, "kimi-k2", bos_id=bos, eos_id=eos, eot_id=eos)
|
||||
rendered = KimiK3Template().render(messages=[{"role":"user", "content":args.prompt}], add_generation_prompt=True)
|
||||
prompt = tok.encode(rendered)
|
||||
needed = len(prompt) + max(args.stable_tokens, args.decode_tokens+3)
|
||||
if needed > args.max_context: raise ValueError(f"prompt and output need {needed} tokens but max context is {args.max_context}")
|
||||
print(f"prompt: {len(prompt)} tokens, chunk={args.chunk_size}", flush=True)
|
||||
|
||||
sequences:list[list[int]] = []
|
||||
# TinyJit executes uncaptured once, captures the second call, and replays from the third call.
|
||||
# Compare two replay paths rather than capture numerics/timing against replay.
|
||||
for trial in range(4):
|
||||
gen = fresh_generate(model, prompt, args.chunk_size)
|
||||
sequence:list[int] = []
|
||||
prefill = 0.0
|
||||
for step in range(args.stable_tokens):
|
||||
token, elapsed = timed_next(gen, args.devices)
|
||||
sequence.append(token)
|
||||
if step == 0: prefill = elapsed
|
||||
if trial >= 2: sequences.append(sequence)
|
||||
label = ("uncaptured warmup", "capture warmup", "stable trial 1", "stable trial 2")[trial]
|
||||
print(f"{label}: prefill={prefill:.3f}s "
|
||||
f"({len(prompt)/prefill:.3f} tok/s), tokens={sequence}", flush=True)
|
||||
if sequences[0] != sequences[1]: raise RuntimeError(f"greedy output is not repeatable: {sequences}")
|
||||
print(f"stable text: {tok.decode(sequences[0])!r}", flush=True)
|
||||
|
||||
gen = fresh_generate(model, prompt, args.chunk_size)
|
||||
profile_marker("kimi k3 steady prefill start")
|
||||
first, prefill = timed_next(gen, args.devices)
|
||||
profile_marker("kimi k3 steady prefill end")
|
||||
warmup = [timed_next(gen, args.devices)[0] for _ in range(2)]
|
||||
profile_marker("kimi k3 steady decode start")
|
||||
begin = time.perf_counter()
|
||||
output = [next(gen) for _ in range(args.decode_tokens)]
|
||||
sync(args.devices)
|
||||
decode = time.perf_counter()-begin
|
||||
profile_marker("kimi k3 steady decode end")
|
||||
print(f"prefill replay: {prefill:.3f}s ({len(prompt)/prefill:.3f} tok/s), token={first}", flush=True)
|
||||
print(f"decode after warmup {warmup}: {decode:.3f}s ({args.decode_tokens/decode:.3f} tok/s, "
|
||||
f"{decode/args.decode_tokens*1e3:.3f} ms/tok), output={output}", flush=True)
|
||||
print(f"peak RSS: {resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024:.1f} MiB", flush=True)
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -1,84 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fast exact-shape K3 KDA/layer benchmark using bounded fake weights instead of the 1.56 TB checkpoint."""
|
||||
from __future__ import annotations
|
||||
import argparse, statistics, time
|
||||
from dataclasses import replace
|
||||
|
||||
from tinygrad import Device, Tensor, TinyJit, dtypes, nn
|
||||
from tinygrad.helpers import profile_marker
|
||||
from tinygrad.llm.kimi_k3 import kimi_k3_config
|
||||
from tinygrad.llm.model import GatedDeltaNetBlock
|
||||
|
||||
def tp_axis(name:str) -> int|None:
|
||||
if "ffn_gate_exps.weight" in name or "ffn_up_exps.weight" in name: return 1
|
||||
if "ffn_gate_exps.weight_scale" in name or "ffn_up_exps.weight_scale" in name: return 1
|
||||
if "ffn_down_exps.weight" in name or "ffn_down_exps.weight_scale" in name: return 2
|
||||
if name.endswith(("ffn_gate_shexp.weight", "ffn_up_shexp.weight")): return 0
|
||||
if name.endswith(("ffn_down_shexp.weight", "ffn_routed_down.weight", "ffn_routed_up.weight", "ssm_out.weight")): return 1
|
||||
if name.endswith(("attn_q.weight", "attn_k.weight", "attn_v.weight", "ssm_g_full.weight", "ssm_f_b.weight", "ssm_beta.weight")): return 0
|
||||
if name.endswith(("ssm_q_conv1d.weight", "ssm_k_conv1d.weight", "ssm_v_conv1d.weight", "ssm_dt.bias")): return 0
|
||||
return None
|
||||
|
||||
def fake_value(name:str) -> tuple[int|float, object]:
|
||||
if name.endswith("weight_scale"): return 120, dtypes.uint8
|
||||
if name.endswith("_exps.weight"): return 0x11, dtypes.uint8
|
||||
if name.endswith("ssm_a"): return -0.1, dtypes.float32
|
||||
if name.endswith("ssm_dt.bias"): return 0.1, dtypes.float32
|
||||
if "conv1d.weight" in name: return 0.1, dtypes.float32
|
||||
if name.endswith("exp_probs_b.bias"): return 0.0, dtypes.float32
|
||||
if name.endswith("norm.weight"): return 1.0, dtypes.bfloat16
|
||||
return 0.001, dtypes.bfloat16
|
||||
|
||||
def fake_tp_tensor(shape:tuple[int, ...], value:int|float, dtype, devices:tuple[str, ...], axis:int|None) -> Tensor:
|
||||
if axis is not None and shape[axis] % len(devices): raise ValueError(f"shape {shape} is not TP{len(devices)} divisible on axis {axis}")
|
||||
source = Tensor.full(shape, value, dtype=dtype, device=devices[0]).clone().realize()
|
||||
return source.shard(devices, axis=axis).realize()
|
||||
|
||||
def sync(devices:tuple[str, ...]) -> None:
|
||||
for device in devices: Device[device].synchronize()
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--devices", type=int, default=8)
|
||||
parser.add_argument("--mode", choices=("attention", "block"), default="attention")
|
||||
parser.add_argument("--iterations", type=int, default=20)
|
||||
args = parser.parse_args()
|
||||
devices = tuple(f"AMD:{i}" for i in range(args.devices))
|
||||
# One exact-width KDA layer, but only 16 fake routed experts. This retains top-k 16 and every
|
||||
# official per-GPU matrix/state shape while keeping fake expert storage below 300 MB per layer.
|
||||
config = replace(kimi_k3_config(4), num_blocks=1, num_experts=16, num_experts_per_tok=16, ssm_layers=(True,),
|
||||
attn_res_block_size=0)
|
||||
block = GatedDeltaNetBlock(config, config.ssm)
|
||||
begin = time.perf_counter()
|
||||
for name,tensor in nn.state.get_state_dict(block).items():
|
||||
if args.mode == "attention" and name.startswith(("ffn_", "exp_probs_")): continue
|
||||
value, dtype = fake_value(name)
|
||||
tensor.replace(fake_tp_tensor(tuple(int(x) for x in tensor.shape), value, dtype, devices, tp_axis(name)))
|
||||
sync(devices)
|
||||
print(f"fake weights: {time.perf_counter()-begin:.3f}s", flush=True)
|
||||
x_source = (((Tensor.arange(config.dim, dtype=dtypes.float32).reshape(1, 1, config.dim) % 31) / 31) \
|
||||
.cast(dtypes.bfloat16).to(devices[0])).clone().realize()
|
||||
x = x_source.shard(devices, axis=None).realize()
|
||||
block._init_state(x)
|
||||
# Use direct buffer-backed state shards. The production path reaches this form after prefill;
|
||||
# the fake harness begins immediately at decode and must not feed lazy clone graphs to TinyJit.
|
||||
for state,axis in ((block.conv_state_q, 2), (block.conv_state_k, 2), (block.conv_state_v, 2), (block.recurrent_state, 1)):
|
||||
state.replace(Tensor.zeros(*state.shape, dtype=state.dtype, device=devices[0]).shard(devices, axis=axis).realize())
|
||||
|
||||
@TinyJit
|
||||
def run(inp:Tensor) -> Tensor:
|
||||
if args.mode == "attention": return block._attention(block.attn_norm(inp), 0).realize()
|
||||
return block(inp, 0).realize()
|
||||
|
||||
# uncaptured, capture, then replay only
|
||||
run(x); sync(devices)
|
||||
run(x); sync(devices)
|
||||
samples:list[float] = []
|
||||
profile_marker(f"fake K3 {args.mode} start")
|
||||
for _ in range(args.iterations):
|
||||
begin = time.perf_counter(); out = run(x); sync(devices); samples.append((time.perf_counter()-begin)*1e3)
|
||||
profile_marker(f"fake K3 {args.mode} end")
|
||||
print(f"{args.mode}: median={statistics.median(samples):.3f} ms/layer, min={min(samples):.3f} ms/layer, "
|
||||
f"projected_93_layer_rate={1000/(statistics.median(samples)*93):.3f} tok/s, finite={out.float().isfinite().all().item()}")
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -0,0 +1,238 @@
|
||||
import ctypes, struct
|
||||
from tinygrad.helpers import ceildiv, getenv, wait_cond, DEBUG
|
||||
from tinygrad.runtime.autogen import bnxt, pci
|
||||
from tinygrad.runtime.support.system import PCIDevice, System, ipv4_to_gid
|
||||
|
||||
BNXT_DEBUG = getenv("BNXT_DEBUG", 0)
|
||||
BNXT_ACCESS, BNXT_INIT_MASK, BNXT_RTR_MASK, BNXT_RTS_MASK = 3, 0xd, 0x41515ad, 0xae005
|
||||
BNXT_CHIMP_COMM, BNXT_CHIMP_COMM_TRIGGER = 0x0, 0x100
|
||||
BNXT_BACKING_STORE = ((0, 2), (1, 0), (2, 2), (3, 0), (4, 2), (5, 0), (6, 0), (14, 2), (15, 0))
|
||||
|
||||
def db_value(xid, typ, index, epoch):
|
||||
return (xid & bnxt.DBC_DBC_XID_MASK | bnxt.DBC_DBC_PATH_ROCE | typ | bnxt.BNXT_QPLIB_DBR_VALID) << 32 | \
|
||||
index & bnxt.DBC_DBC_INDEX_MASK | epoch << bnxt.BNXT_QPLIB_DBR_EPOCH_SHIFT
|
||||
|
||||
def _pbl(dev, paddrs, queue=False):
|
||||
if len(paddrs) == 1: return 0, paddrs[0]
|
||||
values = [p | bnxt.PTU_PTE_VALID for p in paddrs]
|
||||
if queue:
|
||||
values[-1] |= bnxt.PTU_PTE_LAST
|
||||
if len(values) > 1: values[-2] |= bnxt.PTU_PTE_NEXT_TO_LAST
|
||||
table, table_paddrs = dev.pci_dev.alloc_sysmem(ceildiv(len(values), 512) * 0x1000)
|
||||
table[:len(values) * 8] = struct.pack(f"<{len(values)}Q", *values)
|
||||
if len(table_paddrs) == 1: return 1, table_paddrs[0]
|
||||
top, top_paddrs = dev.pci_dev.alloc_sysmem(0x1000)
|
||||
top[:len(table_paddrs) * 8] = struct.pack(f"<{len(table_paddrs)}Q", *(p | bnxt.PTU_PTE_VALID for p in table_paddrs))
|
||||
return 2, top_paddrs[0]
|
||||
|
||||
def _queue(dev, stride:int=16, aux=False):
|
||||
mem, paddrs = dev.pci_dev.alloc_sysmem(0x1000 + aux * 0x400)
|
||||
level, base = _pbl(dev, paddrs, queue=True)
|
||||
return {"mem":mem, "paddrs":paddrs, "stride":stride, "prod":0, "cons":0, "level":level, "base":base}
|
||||
|
||||
def _qread(q, i):
|
||||
off = (i & 15) * q["stride"]
|
||||
return q["mem"][off:off + q["stride"]]
|
||||
|
||||
def _qwrite(q, i, data, aux=False):
|
||||
off = 0x1000 + i % 128 * 8 if aux else (i & 15) * q["stride"]
|
||||
q["mem"][off:off + len(data)] = data
|
||||
|
||||
class BNXTDev:
|
||||
def __init__(self, pci_dev:PCIDevice, ip:str=getenv("BNXT_IP", "10.0.0.1")):
|
||||
self.pci_dev, self.devfmt = pci_dev, pci_dev.pcibus
|
||||
self.bar0, self.db = pci_dev.map_bar(0, fmt='I'), pci_dev.map_bar(2, fmt='Q')
|
||||
pci_dev.write_config(pci.PCI_COMMAND, pci_dev.read_config(pci.PCI_COMMAND, 2) | pci.PCI_COMMAND_MASTER, 2)
|
||||
self.resp, self.resp_pa = pci_dev.alloc_sysmem(0x1000)
|
||||
self.seq = 0
|
||||
|
||||
ver = self.hwrm("ver_get")
|
||||
if DEBUG >= 2: print(f"bnxt {self.devfmt}: firmware {ver.hwrm_fw_maj_8b}.{ver.hwrm_fw_min_8b}.{ver.hwrm_fw_bld_8b}")
|
||||
self.hwrm("func_reset", timeout_ms=40000)
|
||||
caps = self.hwrm("func_qcaps", fid=0xffff)
|
||||
self.mac, self.port_id = int.from_bytes(bytes(caps.mac_address), 'big'), caps.port_id
|
||||
self.hwrm("func_drv_rgtr")
|
||||
self.db_off = self.hwrm("func_qcfg", fid=0xffff).legacy_l2_db_size_kb * 1024
|
||||
|
||||
self.setup_backing_store()
|
||||
self._open_rcfw()
|
||||
self._open_l2()
|
||||
self.local_gid = ipv4_to_gid(ip)
|
||||
gids, mac = (ctypes.c_uint32 * 4)(*(int.from_bytes(self.local_gid[i:i + 4], 'big') for i in (12, 8, 4, 0))), self.mac.to_bytes(6, 'big')
|
||||
smac = (ctypes.c_uint16 * 3)(*(int.from_bytes(mac[i:i + 2], 'big') for i in (0, 2, 4)))
|
||||
self.gid_id = self.rcfw("add_gid", gid=gids, src_mac=smac).xid
|
||||
|
||||
if DEBUG >= 2: print(f"bnxt {self.devfmt}: booted mac={self.mac.to_bytes(6, 'big').hex(':')} gid={self.local_gid.hex()}")
|
||||
|
||||
def hwrm(self, name, timeout_ms=10000, **fields):
|
||||
inp, out = getattr(bnxt, f"struct_hwrm_{name}_input"), getattr(bnxt, f"struct_hwrm_{name}_output")
|
||||
opcode = getattr(bnxt, f"HWRM_{name.upper()}")
|
||||
self.seq = (self.seq + 1) & 0xffff
|
||||
data = bytes(inp(req_type=opcode, cmpl_ring=bnxt.BNXT_HWRM_NO_CMPL_RING, seq_id=self.seq, target_id=bnxt.BNXT_HWRM_TARGET,
|
||||
resp_addr=self.resp_pa[0], **fields))
|
||||
self.resp[:] = bytes(len(self.resp))
|
||||
System.memory_barrier()
|
||||
for i, w in enumerate(memoryview(bytearray(data.ljust(bnxt.HWRM_MAX_REQ_LEN, b'\0'))).cast('I')):
|
||||
self.bar0[BNXT_CHIMP_COMM // 4 + i] = w
|
||||
self.bar0[BNXT_CHIMP_COMM_TRIGGER // 4] = 1
|
||||
def hdr(): return bnxt.struct_hwrm_resp_hdr.from_buffer_copy(bytes(self.resp[:8]))
|
||||
wait_cond(lambda: (n := hdr().resp_len) and hdr().seq_id == self.seq and self.resp[n - 1], timeout_ms=timeout_ms, msg=f"HWRM {name}")
|
||||
ret = out.from_buffer_copy(bytes(self.resp[:ctypes.sizeof(out)]))
|
||||
assert ret.error_code == 0, f"HWRM {name}: {ret.error_code}"
|
||||
return ret
|
||||
|
||||
def setup_backing_store(self):
|
||||
counts: dict[int, int] = {}
|
||||
for typ, extra in BNXT_BACKING_STORE:
|
||||
caps = self.hwrm("func_backing_store_qcaps_v2", type=typ)
|
||||
size, splits = caps.entry_size, tuple(getattr(caps, f"split_entry_{j}") for j in range(caps.subtype_valid_cnt))
|
||||
counts[typ] = n = counts[0] if typ == 15 else max(caps.min_num_entries, sum(splits) + extra)
|
||||
# a zero bitmap means the type has a single instance 0
|
||||
for instance in [i for i in range(8) if caps.instance_bit_map >> i & 1] or [0]:
|
||||
mem, paddrs = self.pci_dev.alloc_sysmem(ceildiv(n * size, 0x1000) * 0x1000)
|
||||
if caps.ctx_init_value:
|
||||
for off in range(caps.ctx_init_offset, len(mem), size): mem[off] = caps.ctx_init_value
|
||||
lvl, base = _pbl(self, paddrs)
|
||||
self.hwrm("func_backing_store_cfg_v2", type=typ, instance=instance, entry_size=size, num_entries=n, page_dir=base,
|
||||
page_size_pbl_level=lvl, subtype_valid_cnt=len(splits),
|
||||
flags=bnxt.FUNC_BACKING_STORE_CFG_V2_REQ_FLAGS_BS_CFG_ALL_DONE if typ == 15 else 0,
|
||||
**{f"split_entry_{j}": v for j, v in enumerate(splits)})
|
||||
|
||||
def _open_rcfw(self):
|
||||
self.rcfw_first = True
|
||||
|
||||
self.creq = _queue(self)
|
||||
self.creq_id = self.hwrm("ring_alloc", ring_type=bnxt.RING_ALLOC_REQ_RING_TYPE_NQ, page_tbl_addr=self.creq["base"],
|
||||
page_size=12, page_tbl_depth=self.creq["level"], length=16, int_mode=bnxt.RING_ALLOC_REQ_INT_MODE_MSIX).ring_id
|
||||
|
||||
self.cmdq = _queue(self)
|
||||
self.doorbell(self.creq_id, bnxt.DBC_DBC_TYPE_NQ_ARM, 0, 0)
|
||||
init = bnxt.struct_cmdq_init(cmdq_pbl=self.cmdq["base"], creq_ring_id=self.creq_id,
|
||||
cmdq_size_cmdq_lvl=16 << bnxt.CMDQ_INIT_CMDQ_SIZE_SFT)
|
||||
|
||||
System.memory_barrier()
|
||||
for i, w in enumerate(memoryview(bytearray(bytes(init))).cast('I')): self.bar0[bnxt.RCFW_COMM_BASE_OFFSET // 4 + i] = w
|
||||
|
||||
_, p = self.pci_dev.alloc_sysmem(0x1000)
|
||||
self.rcfw("initialize_fw", stat_ctx_id=self.hwrm("stat_ctx_alloc", stats_dma_addr=p[0], stats_dma_length=176).stat_ctx_id,
|
||||
flags=bnxt.CMDQ_INITIALIZE_FW_FLAGS_HW_REQUESTER_RETX_SUPPORTED)
|
||||
|
||||
# RoCE notification ring: never armed or serviced, but CQ and L2 ring allocation require one
|
||||
nq = _queue(self)
|
||||
self.nq_id = self.hwrm("ring_alloc", ring_type=bnxt.RING_ALLOC_REQ_RING_TYPE_NQ, page_tbl_addr=nq["base"],
|
||||
page_size=12, page_tbl_depth=nq["level"], length=16, logical_id=1, int_mode=bnxt.RING_ALLOC_REQ_INT_MODE_MSIX).ring_id
|
||||
|
||||
def rcfw(self, name, timeout_ms=20000, **fields):
|
||||
req_t, resp_t = getattr(bnxt, f"struct_cmdq_{name}"), getattr(bnxt, f"struct_creq_{name}_resp")
|
||||
op = getattr(bnxt, f"CMDQ_BASE_OPCODE_{name.upper()}")
|
||||
data = bytes(req_t(opcode=op, cmd_size=(slots := ceildiv(ctypes.sizeof(req_t), 16)), **fields)).ljust(slots * 16, b'\0')
|
||||
for i in range(slots): _qwrite(self.cmdq, self.cmdq["prod"] + i, data[i * 16:(i + 1) * 16])
|
||||
|
||||
self.cmdq["prod"] += slots
|
||||
prod = self.cmdq["prod"] & 0xffff
|
||||
if self.rcfw_first: prod, self.rcfw_first = prod | 1 << bnxt.FIRMWARE_FIRST_FLAG, False
|
||||
|
||||
System.memory_barrier()
|
||||
|
||||
self.bar0[(bnxt.RCFW_COMM_BASE_OFFSET + bnxt.RCFW_PF_VF_COMM_PROD_OFFSET) // 4] = prod
|
||||
self.bar0[(bnxt.RCFW_COMM_BASE_OFFSET + bnxt.RCFW_COMM_TRIG_OFFSET) // 4] = bnxt.RCFW_CMDQ_TRIG_VAL
|
||||
|
||||
def poll():
|
||||
h = bnxt.struct_creq_base.from_buffer_copy(bytes(_qread(self.creq, self.creq["cons"])))
|
||||
return bool(h.v & bnxt.CREQ_BASE_V) != bool((self.creq["cons"] // 16) & 1)
|
||||
wait_cond(poll, timeout_ms=timeout_ms, msg=f"RCFW {name}")
|
||||
|
||||
ret = resp_t.from_buffer_copy(bytes(_qread(self.creq, self.creq["cons"])))
|
||||
self.creq["cons"] += 1
|
||||
|
||||
# NQ_ARM also publishes the CREQ consumer index, which is what frees ring space for the next command
|
||||
self.doorbell(self.creq_id, bnxt.DBC_DBC_TYPE_NQ_ARM, self.creq["cons"] & 15, (self.creq["cons"] // 16) & 1)
|
||||
assert ret.status == 0, f"RCFW {name}: {ret.status}"
|
||||
|
||||
if BNXT_DEBUG >= 1: print(f"bnxt {self.devfmt}: rcfw {name} xid={getattr(ret, 'xid', 0):#x}")
|
||||
return ret
|
||||
|
||||
def doorbell(self, xid, typ, index, epoch):
|
||||
System.memory_barrier()
|
||||
self.db[self.db_off // 8] = db_value(xid, typ, index, epoch)
|
||||
|
||||
# L2 receive path, required for RoCE ingress even though no ethernet receive buffers are posted
|
||||
def _open_l2(self):
|
||||
cq = _queue(self)
|
||||
ci = self.hwrm("ring_alloc", enables=bnxt.RING_ALLOC_REQ_ENABLES_NQ_RING_ID_VALID, ring_type=bnxt.RING_ALLOC_REQ_RING_TYPE_L2_CMPL,
|
||||
page_tbl_addr=cq["base"], page_size=12, page_tbl_depth=cq["level"], length=16, nq_ring_id=self.nq_id).ring_id
|
||||
rx = _queue(self)
|
||||
ri = self.hwrm("ring_alloc", enables=bnxt.RING_ALLOC_REQ_ENABLES_NQ_RING_ID_VALID |
|
||||
bnxt.RING_ALLOC_REQ_ENABLES_RX_BUF_SIZE_VALID, ring_type=bnxt.RING_ALLOC_REQ_RING_TYPE_RX, page_tbl_addr=rx["base"],
|
||||
page_size=12, page_tbl_depth=rx["level"], length=16, rx_buf_size=640, nq_ring_id=self.nq_id).ring_id
|
||||
vi = self.hwrm("vnic_alloc").vnic_id
|
||||
self.hwrm("vnic_cfg", enables=bnxt.VNIC_CFG_REQ_ENABLES_MRU | bnxt.VNIC_CFG_REQ_ENABLES_DEFAULT_RX_RING_ID |
|
||||
bnxt.VNIC_CFG_REQ_ENABLES_DEFAULT_CMPL_RING_ID, vnic_id=vi, mru=9018,
|
||||
default_rx_ring_id=ri, default_cmpl_ring_id=ci)
|
||||
self.hwrm("cfa_l2_filter_alloc", flags=bnxt.CFA_L2_FILTER_ALLOC_REQ_FLAGS_PATH_RX,
|
||||
enables=bnxt.CFA_L2_FILTER_ALLOC_REQ_ENABLES_L2_ADDR | bnxt.CFA_L2_FILTER_ALLOC_REQ_ENABLES_L2_ADDR_MASK |
|
||||
bnxt.CFA_L2_FILTER_ALLOC_REQ_ENABLES_DST_ID, l2_addr=tuple(self.mac.to_bytes(6, 'big')), l2_addr_mask=(0xff,) * 6, dst_id=vi)
|
||||
|
||||
def register_mem(self, paddrs:list[int], size:int, log_page_size:int=12) -> int:
|
||||
level, base = _pbl(self, paddrs[:ceildiv(size, 1 << log_page_size)])
|
||||
return self.rcfw("register_mr", flags=bnxt.CMDQ_REGISTER_MR_FLAGS_ALLOC_MR,
|
||||
log2_pg_size_lvl=level << bnxt.CMDQ_REGISTER_MR_LVL_SFT | log_page_size << bnxt.CMDQ_REGISTER_MR_LOG2_PG_SIZE_SFT,
|
||||
access=bnxt.CMDQ_REGISTER_MR_ACCESS_LOCAL_WRITE | bnxt.CMDQ_REGISTER_MR_ACCESS_REMOTE_WRITE,
|
||||
log2_pbl_pg_size=12, pbl=base, va=paddrs[0], mr_size=size).xid
|
||||
|
||||
class BNXTQP:
|
||||
def __init__(self, dev:BNXTDev):
|
||||
self.dev, self.sq_psn, self.msn = dev, 0, 0
|
||||
|
||||
self.cqq = _queue(dev, ctypes.sizeof(bnxt.struct_cq_base))
|
||||
self.cq_id = dev.rcfw("create_cq", cq_size=16, pbl=self.cqq["base"],
|
||||
pg_size_lvl=self.cqq["level"], cq_fco_cnq_id=dev.nq_id).xid
|
||||
|
||||
self.sq = _queue(dev, aux=True)
|
||||
self.qpn = dev.rcfw("create_qp", type=bnxt.CMDQ_CREATE_QP_TYPE_RC,
|
||||
sq_size=16, sq_fwo_sq_sge=1, scq_cid=self.cq_id, rcq_cid=self.cq_id,
|
||||
sq_pbl=self.sq["base"], sq_pg_size_sq_lvl=self.sq["level"]).xid
|
||||
self.qp_op(1, BNXT_INIT_MASK, access=BNXT_ACCESS, pkey=0xffff)
|
||||
|
||||
def qp_op(self, state, mask, network_type=0, **fields):
|
||||
self.dev.rcfw("modify_qp", qp_cid=self.qpn, modify_mask=mask,
|
||||
network_type_en_sqd_async_notify_new_state=state | network_type, **fields)
|
||||
|
||||
def connect(self, qpn:int, gid:bytes, mac:int):
|
||||
network_type = bnxt.CMDQ_MODIFY_QP_NETWORK_TYPE_ROCEV2_IPV4
|
||||
dgid = (ctypes.c_uint32 * 4)(*(int.from_bytes(gid[i:i + 4], 'little') for i in (0, 4, 8, 12)))
|
||||
dmac = (ctypes.c_uint16 * 3)(*(int.from_bytes(mac.to_bytes(6, 'big')[i:i + 2], 'little') for i in (0, 2, 4)))
|
||||
|
||||
self.qp_op(2, BNXT_RTR_MASK, network_type=network_type, qp_type=bnxt.CMDQ_MODIFY_QP_QP_TYPE_RC, access=BNXT_ACCESS,
|
||||
pkey=0xffff, dgid=dgid, sgid_index=self.dev.gid_id, hop_limit=64, dest_mac=dmac,
|
||||
path_mtu_pingpong_push_enable=bnxt.CMDQ_MODIFY_QP_PATH_MTU_MTU_1024, max_dest_rd_atomic=4,
|
||||
dest_qp_id=qpn)
|
||||
self.qp_op(3, BNXT_RTS_MASK, network_type=network_type, qp_type=bnxt.CMDQ_MODIFY_QP_QP_TYPE_RC, access=BNXT_ACCESS,
|
||||
max_rd_atomic=1)
|
||||
|
||||
if BNXT_DEBUG >= 1: print(f"bnxt: QP {self.qpn:#x} connected (remote={qpn:#x})")
|
||||
|
||||
def _poll(self, timeout):
|
||||
def poll():
|
||||
base = bnxt.struct_cq_base.from_buffer_copy(bytes(_qread(self.cqq, self.cqq["cons"])))
|
||||
return bool(base.cqe_type_toggle & bnxt.CQ_BASE_TOGGLE) == (not bool((self.cqq["cons"] // 16) & 1))
|
||||
wait_cond(poll, timeout_ms=timeout, msg="BNXT CQ")
|
||||
raw = bytes(_qread(self.cqq, self.cqq["cons"]))
|
||||
self.cqq["cons"] += 1
|
||||
self.dev.doorbell(self.cq_id, bnxt.DBC_DBC_TYPE_CQ, self.cqq["cons"] & 15, (self.cqq["cons"] // 16) & 1)
|
||||
return raw
|
||||
|
||||
def rdma_write(self, rva, rkey, lva, lkey, size, timeout_ms=20000):
|
||||
start = self.sq["prod"] & 15
|
||||
hdr = bytes(bnxt.struct_sq_rdma_hdr(wqe_type=bnxt.SQ_RDMA_HDR_WQE_TYPE_WRITE_WQE,
|
||||
flags=bnxt.SQ_SEND_FLAGS_SIGNAL_COMP, wqe_size=3, length=size, remote_va=rva, remote_key=rkey))
|
||||
for i, data in enumerate((hdr[:16], hdr[16:32], bytes(bnxt.struct_sq_sge(va_or_pa=lva, l_key=lkey, size=size)))):
|
||||
_qwrite(self.sq, start + i, data)
|
||||
nxt = (self.sq_psn + max(1, ceildiv(size, 1024))) & 0xffffff
|
||||
value = start << bnxt.SQ_MSN_SEARCH_START_IDX_SFT | nxt << bnxt.SQ_MSN_SEARCH_NEXT_PSN_SFT | self.sq_psn
|
||||
_qwrite(self.sq, self.msn, struct.pack("<Q", value), aux=True)
|
||||
|
||||
self.msn, self.sq_psn, self.sq["prod"] = (self.msn + 1) % 128, nxt, self.sq["prod"] + 3
|
||||
self.dev.doorbell(self.qpn, bnxt.DBC_DBC_TYPE_SQ, self.sq["prod"] & 15, (self.sq["prod"] // 16) & 1)
|
||||
cqe = bnxt.struct_cq_req.from_buffer_copy(self._poll(timeout_ms))
|
||||
assert cqe.status == 0
|
||||
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Send and validate one RDMA WRITE between two Broadcom BNXT hosts.
|
||||
|
||||
This follows ``extra/mlx_driver/connect.py``: sync the driver, start the remote
|
||||
endpoint over SSH, exchange QP/GID/MAC/MR metadata, move both RC QPs to RTS,
|
||||
write bytes into the remote MR, and verify the bytes on the remote host.
|
||||
|
||||
Both PCI functions must be unbound from bnxt_en/bnxt_re first.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any, IO
|
||||
|
||||
TINYGRAD = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../.."))
|
||||
sys.path.insert(0, TINYGRAD)
|
||||
|
||||
from extra.bnxt_driver.bnxtdev import BNXTDev, BNXTQP
|
||||
from tinygrad.runtime.support.system import PCIDevice
|
||||
|
||||
REMOTE_HOST = os.getenv("REMOTE_HOST", "192.168.52.213")
|
||||
REMOTE_USER = os.getenv("REMOTE_USER", "nimlgen")
|
||||
LOCAL_PCI = os.getenv("BNXT_PCI", "0000:41:00.0")
|
||||
REMOTE_PCI = os.getenv("REMOTE_PCI", "0000:41:00.0")
|
||||
LOCAL_IP = os.getenv("LOCAL_IP", "10.0.200.5")
|
||||
REMOTE_IP = os.getenv("REMOTE_IP", "10.0.200.6")
|
||||
MESSAGE = os.getenv("RDMA_MESSAGE", "Test message, rdma works!").encode()
|
||||
REMOTE = f"{REMOTE_USER}@{REMOTE_HOST}"
|
||||
SSH = ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "-o", "StrictHostKeyChecking=accept-new", REMOTE]
|
||||
SYNC_FILES = ("tinygrad/runtime/autogen/bnxt.py", "tinygrad/runtime/support/system.py",
|
||||
"extra/bnxt_driver/bnxtdev.py", "extra/bnxt_driver/connect.py")
|
||||
|
||||
def read_json(stream:IO[str], what:str) -> dict[str, Any]:
|
||||
for line in iter(stream.readline, ""):
|
||||
print(f" [remote] {line}", end="")
|
||||
try: value = json.loads(line)
|
||||
except json.JSONDecodeError: continue
|
||||
if isinstance(value, dict): return value
|
||||
raise RuntimeError(f"remote exited before publishing {what}")
|
||||
|
||||
def wait_line(stream:IO[str], text:str) -> str:
|
||||
for line in iter(stream.readline, ""):
|
||||
print(f" [remote] {line}", end="")
|
||||
if text in line: return line
|
||||
raise RuntimeError(f"remote exited before reporting {text!r}")
|
||||
|
||||
def send_line(stream:IO[str], value:str|dict[str, Any]):
|
||||
stream.write((json.dumps(value) if isinstance(value, dict) else value) + "\n")
|
||||
stream.flush()
|
||||
|
||||
def qp_info(dev:BNXTDev, qp:BNXTQP) -> dict[str, Any]:
|
||||
return {"qpn":qp.qpn, "mac":dev.mac.to_bytes(6, "big").hex(), "gid":dev.local_gid.hex()}
|
||||
|
||||
def server():
|
||||
dev = BNXTDev(PCIDevice("bnxt", os.getenv("BNXT_PCI", "0000:41:00.0")), ip=os.getenv("BNXT_IP", REMOTE_IP))
|
||||
qp = BNXTQP(dev)
|
||||
print(json.dumps(qp_info(dev, qp)), flush=True)
|
||||
|
||||
peer = json.loads(sys.stdin.readline())
|
||||
qp.connect(peer["qpn"], bytes.fromhex(peer["gid"]), int(peer["mac"], 16))
|
||||
print("connected", flush=True)
|
||||
|
||||
target, target_paddrs = dev.pci_dev.alloc_sysmem(0x1000)
|
||||
target[:0x1000] = bytes(0x1000)
|
||||
rkey = dev.register_mem(target_paddrs, 0x1000)
|
||||
print(json.dumps({"target_addr":target_paddrs[0], "rkey":rkey}), flush=True)
|
||||
|
||||
assert sys.stdin.readline().strip() == "done"
|
||||
received = bytes(target).rstrip(b"\0")
|
||||
print(f"AS TEXT: {received.decode(errors='replace')!r}", flush=True)
|
||||
print(json.dumps({"data":received.hex()}), flush=True)
|
||||
|
||||
def sync_remote():
|
||||
if os.getenv("SYNC", "1") == "0": return
|
||||
print("syncing BNXT driver to remote")
|
||||
subprocess.run(["rsync", "-azR", *SYNC_FILES, f"{REMOTE}:~/tinygrad/"], cwd=TINYGRAD, check=True)
|
||||
|
||||
def start_remote() -> subprocess.Popen[str]:
|
||||
print("booting remote")
|
||||
command = (f"cd ~/tinygrad && sudo env PYTHONPATH=. PYTHONUNBUFFERED=1 BNXT_DEBUG={os.getenv('BNXT_DEBUG', '0')} "
|
||||
f"BNXT_PCI={REMOTE_PCI} BNXT_IP={REMOTE_IP} python3 extra/bnxt_driver/connect.py --server")
|
||||
return subprocess.Popen(SSH + [command], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=sys.stderr, text=True)
|
||||
|
||||
def client():
|
||||
assert 0 < len(MESSAGE) <= 0x1000
|
||||
sync_remote()
|
||||
remote = start_remote()
|
||||
assert remote.stdin is not None and remote.stdout is not None
|
||||
remote_info = read_json(remote.stdout, "QP information")
|
||||
print("booting local")
|
||||
dev = BNXTDev(PCIDevice("bnxt", LOCAL_PCI), ip=LOCAL_IP)
|
||||
qp = BNXTQP(dev)
|
||||
|
||||
send_line(remote.stdin, qp_info(dev, qp))
|
||||
wait_line(remote.stdout, "connected")
|
||||
qp.connect(remote_info["qpn"], bytes.fromhex(remote_info["gid"]), int(remote_info["mac"], 16))
|
||||
print("both QPs in RTS")
|
||||
|
||||
remote_target = read_json(remote.stdout, "MR information")
|
||||
source, source_paddrs = dev.pci_dev.alloc_sysmem(0x1000)
|
||||
source[:len(MESSAGE)] = MESSAGE
|
||||
lkey = dev.register_mem(source_paddrs, 0x1000)
|
||||
print(f"RDMA WRITE {len(MESSAGE)}B to remote phys 0x{remote_target['target_addr']:x}")
|
||||
qp.rdma_write(remote_target["target_addr"], remote_target["rkey"], source_paddrs[0], lkey, len(MESSAGE))
|
||||
|
||||
send_line(remote.stdin, "done")
|
||||
wait_line(remote.stdout, "AS TEXT")
|
||||
result = read_json(remote.stdout, "RDMA result")
|
||||
assert bytes.fromhex(result["data"]) == MESSAGE
|
||||
print("RDMA WRITE data verified")
|
||||
|
||||
remote.stdin.close()
|
||||
assert remote.wait() == 0
|
||||
print("RDMA WRITE test complete")
|
||||
|
||||
if __name__ == "__main__":
|
||||
server() if "--server" in sys.argv else client()
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Local BNXT RoCEv2 RDMA WRITE loopback using the firmware's PHY loopback mode.
|
||||
|
||||
The kernel bnxt_en/bnxt_re modules must be unloaded first.
|
||||
|
||||
sudo PYTHONPATH=. BNXT_PCI=0000:41:00.0 BNXT_IP=10.0.200.5 python3 extra/bnxt_driver/loopback.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "../.."))
|
||||
|
||||
from extra.bnxt_driver.bnxtdev import BNXTDev, BNXTQP
|
||||
from tinygrad.runtime.autogen import bnxt
|
||||
from tinygrad.runtime.support.system import PCIDevice
|
||||
|
||||
BUF_SIZE = 0x1000
|
||||
BNXT_PCI = os.getenv("BNXT_PCI", "0000:41:00.0")
|
||||
BNXT_IP = os.getenv("BNXT_IP", "10.0.200.5")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"[init] BNXT at {BNXT_PCI}")
|
||||
dev = BNXTDev(PCIDevice("bnxt", BNXT_PCI), ip=BNXT_IP)
|
||||
tx_qp, rx_qp = BNXTQP(dev), BNXTQP(dev)
|
||||
print(f"[init] loopback-connect TX QP 0x{tx_qp.qpn:x} <-> RX QP 0x{rx_qp.qpn:x}")
|
||||
tx_qp.connect(rx_qp.qpn, dev.local_gid, dev.mac)
|
||||
rx_qp.connect(tx_qp.qpn, dev.local_gid, dev.mac)
|
||||
|
||||
src, src_paddrs = dev.pci_dev.alloc_sysmem(BUF_SIZE)
|
||||
dst, dst_paddrs = dev.pci_dev.alloc_sysmem(BUF_SIZE)
|
||||
message = b"Hello from BNXT RoCE PHY loopback!"
|
||||
src[:BUF_SIZE], dst[:BUF_SIZE] = bytes(BUF_SIZE), bytes(BUF_SIZE)
|
||||
src[:len(message)] = message
|
||||
lkey = dev.register_mem(src_paddrs, BUF_SIZE)
|
||||
rkey = dev.register_mem(dst_paddrs, BUF_SIZE)
|
||||
|
||||
print("[loopback] enabling local PHY loopback")
|
||||
dev.hwrm("port_phy_cfg", port_id=dev.port_id, enables=bnxt.PORT_PHY_CFG_REQ_ENABLES_LPBK, lpbk=bnxt.PORT_PHY_CFG_REQ_LPBK_LOCAL)
|
||||
time.sleep(1)
|
||||
tx_qp.rdma_write(dst_paddrs[0], rkey, src_paddrs[0], lkey, len(message))
|
||||
got = bytes(dst[:len(message)])
|
||||
print(f"[result] {got!r}")
|
||||
assert got == message
|
||||
print("BNXT RoCE PHY loopback RDMA WRITE passed")
|
||||
dev.hwrm("port_phy_cfg", port_id=dev.port_id, enables=bnxt.PORT_PHY_CFG_REQ_ENABLES_LPBK, lpbk=bnxt.PORT_PHY_CFG_REQ_LPBK_NONE)
|
||||
@@ -315,7 +315,7 @@ return 0; }
|
||||
if __name__ == "__main__":
|
||||
dev = DSPDevice()
|
||||
|
||||
bufs = [dev.allocator.alloc(0x60000) for _ in range(4)]
|
||||
bufs = [dev.allocator.alloc(0x60000)[0][0] for _ in range(4)]
|
||||
|
||||
only_entry = dev.compiler.compile(entry)
|
||||
app1 = dev.runtime("test", only_entry)
|
||||
|
||||
@@ -268,7 +268,7 @@ return HAP_perf_get_time_us() == 1 ? 4 : 0;
|
||||
if __name__ == "__main__":
|
||||
dev = DSPDevice()
|
||||
|
||||
bufs = [dev.allocator.alloc(0x60000) for _ in range(4)]
|
||||
bufs = [dev.allocator.alloc(0x60000)[0][0] for _ in range(4)]
|
||||
|
||||
only_entry = dev.compiler.compile(entry)
|
||||
app1 = dev.runtime("test", only_entry)
|
||||
|
||||
@@ -35,7 +35,7 @@ def compile_net(linear:UOp, output_bufs:List[Buffer]) -> Tuple[Dict[str,str], Li
|
||||
return name
|
||||
|
||||
for call in iter_kernel_calls(linear):
|
||||
arg_uops = [b for b in call.src[1:] if b.op is not Ops.BIND]
|
||||
arg_uops = [b for b in call.src[1:] if not b.is_bound_var]
|
||||
prg = to_program(call.src[0], Device[arg_uops[0].device].renderer)
|
||||
info = prg.arg
|
||||
functions[info.function_name] = prg.src[2].arg
|
||||
@@ -241,8 +241,7 @@ export default {model_name};
|
||||
def export_model(model, target:str, *inputs, model_name: Optional[str] = "model", stream_weights=False):
|
||||
assert Device.DEFAULT in EXPORT_SUPPORTED_DEVICE, f"only {', '.join(EXPORT_SUPPORTED_DEVICE)} are supported"
|
||||
|
||||
# NOTE: NUM_CPU_THREADS=1, since export does not support threading
|
||||
with Context(JIT=2, NUM_CPU_THREADS=1): linear, output_bufs = jit_model(model, *inputs)
|
||||
with Context(JIT=2): linear, output_bufs = jit_model(model, *inputs)
|
||||
functions, statements, bufs, bufs_to_save = compile_net(linear, output_bufs)
|
||||
state = get_state_dict(model)
|
||||
weight_names = {(id(b), b.offset, b.size, b.dtype): name for name, x in state.items() if (b:=x.uop.base.realized) is not None}
|
||||
|
||||
@@ -462,7 +462,7 @@ def test_matmul():
|
||||
lds = UOp.placeholder((lds_size,), dtypes.uint8, 0, AddrSpace.LOCAL)
|
||||
sink = UOp.sink(A.base, B.base, C.base, lds, *gidxs, *lidxs, arg=KernelInfo(name=colored("kernel", "cyan"),
|
||||
estimates=Estimates(ops=N*N*N*2, mem=N*N*4*3)))
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=x) for x in insts]))))
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=(x, dtypes.void)) for x in insts]))))
|
||||
c = Tensor.custom_kernel(a, b, c, fxn=asm_kernel)[2]
|
||||
linear = c.schedule_linear()
|
||||
|
||||
|
||||
@@ -122,9 +122,10 @@ def custom_mxfp4_gemm(C:UOp, A:UOp, B:UOp, scale_a:UOp, scale_b:UOp, *extra:UOp,
|
||||
groups_x, groups_y = UOp.special(ceildiv(N, tile_n), "gidx0"), UOp.special(ceildiv(M, tile_m), "gidx1")
|
||||
lds = UOp.placeholder((163840,), dtypes.uint8, 0, AddrSpace.LOCAL)
|
||||
sink = UOp.sink(C.base, A.base, B.base, scale_a.base, scale_b.base, *(x.base for x in extra), lds, threads, groups_x, groups_y,
|
||||
arg=KernelInfo(f"custom_mxfp4_gemm_{M}_{N}_{K}", estimates=Estimates(ops=2*M*N*K)))
|
||||
arg=KernelInfo(f"mxfp4_gemm_{M}_{N}_{K}",
|
||||
estimates=Estimates(ops=2*M*N*K, mem=(M*half_k+N*half_k)*A.dtype.itemsize+M*N*C.dtype.itemsize)))
|
||||
insts = build_kernel(M, N, K, tile_m, tile_n)
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple(UOp(Ops.INS, arg=x) for x in insts))))
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple(UOp(Ops.INS, arg=(x, dtypes.void)) for x in insts))))
|
||||
|
||||
def _mxfp4_gemm_quantized(a_q:Tensor, b_q:Tensor, scale_a:Tensor, scale_b:Tensor) -> Tensor:
|
||||
M, half_k = a_q.shape
|
||||
@@ -214,7 +215,7 @@ def custom_uop_gemm(C:UOp, A:UOp, B:UOp) -> UOp:
|
||||
k = UOp.range(K, 0, AxisType.REDUCE)
|
||||
mul = (A.flatten().index((m*UOp.const(K)+k))*
|
||||
B.flatten().index((k*UOp.const(N)+n))).cast(dtypes.float32)
|
||||
red = mul.reduce(k, arg=Ops.ADD, dtype=dtypes.float32).cast(C.dtype)
|
||||
red = mul.reduce(k, arg=Ops.ADD).cast(C.dtype)
|
||||
store = C.flatten().index((m*UOp.const(N)+n)).store(red).end(m, n)
|
||||
return store.sink(arg=KernelInfo(name=f'uop_gemm_{M}_{N}_{K}'))
|
||||
|
||||
|
||||
+68
-794
File diff suppressed because it is too large
Load Diff
@@ -34,9 +34,9 @@ num_threads = prod(local_size)
|
||||
# Can AMDAllocator initialized as device=0 by default?
|
||||
device = AMDDevice()
|
||||
hipallocator = AMDAllocator(device)
|
||||
a = hipallocator.alloc(N*N*4)
|
||||
b = hipallocator.alloc(N*N*2)
|
||||
c = hipallocator.alloc(N*N*2)
|
||||
a = hipallocator.alloc(N*N*4)[0][0]
|
||||
b = hipallocator.alloc(N*N*2)[0][0]
|
||||
c = hipallocator.alloc(N*N*2)[0][0]
|
||||
na = np.empty(N*N, np.float32)
|
||||
nb = np.random.default_rng().standard_normal(size=(N,N), dtype=np.float32).astype(np.float16)
|
||||
nc = np.random.default_rng().standard_normal(size=(N,N), dtype=np.float32).astype(np.float16)
|
||||
|
||||
@@ -113,7 +113,6 @@ if __name__ == "__main__":
|
||||
}
|
||||
elif GEMM_VARIATION == "hcopt" and M == N == K == 4096 and DTYPE_IN == dtypes.half and DTYPE_OUT == dtypes.half and DTYPE_ACC == dtypes.float:
|
||||
print("Using CUDA and generated hcopt")
|
||||
# [Opt(op=OptOps.TC, axis=0, amt=0), Opt(op=OptOps.UPCAST, axis=0, amt=4), Opt(op=OptOps.UPCAST, axis=1, amt=4), Opt(op=OptOps.LOCAL, axis=1, amt=4)]
|
||||
prog = CUDAProgram(device, "wmma_example", compiler.compile(open(os.path.join(script_dir, 'max_kernels/nv.fp16_fp32_fp16.hcopt.cu')).read()))
|
||||
args = (c, a, b)
|
||||
kwargs = {
|
||||
|
||||
+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:
|
||||
|
||||
@@ -1,8 +1,53 @@
|
||||
import functools, math, pathlib
|
||||
from tinygrad import Tensor, dtypes
|
||||
from tinygrad.helpers import getenv
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo, AxisType
|
||||
from tinygrad.renderer import Estimates
|
||||
from tinygrad.runtime.support.compiler_amd import HIPCCCompiler
|
||||
|
||||
BLOCK_ROW = 256
|
||||
|
||||
@functools.cache
|
||||
def _router_mfma_fwd(out:UOp, x:UOp, weight:UOp, bias:UOp, *, dname:str) -> UOp:
|
||||
*lead, K = x.shape
|
||||
M = math.prod(lead)
|
||||
E = weight.shape[0]
|
||||
threads = UOp.special(256, "lidx0")
|
||||
workgroups = UOp.special((M + 63) // 64, "gidx0")
|
||||
sink = UOp.sink(out.base, x.base, weight.base, bias.base, threads, workgroups,
|
||||
arg=KernelInfo(f"moe_router_mfma_{M}_{K}_{E}", estimates=Estimates(ops=2*M*E*K, mem=(M*K+E*K+E)*2+M*E*4)))
|
||||
amd = pathlib.Path(__file__).parent.parent/"thunder"/"amd"
|
||||
src = (amd/"moe_router_mfma.cpp").read_text()
|
||||
lib = HIPCCCompiler("gfx950", [f"-I{(amd/'include').as_posix()}", "-std=c++20", "-DKITTENS_CDNA4", "-DHIP_ENABLE_WARP_SYNC_BUILTINS",
|
||||
f"-DROUTER_M={M}", f"-DROUTER_K={K}", f"-DROUTER_E={E}"]).compile_cached(src)
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=(*sink.src, sink)), UOp(Ops.SOURCE, arg=src), UOp(Ops.BINARY, arg=lib)))
|
||||
|
||||
def _router_mfma_bwd(gradient:UOp, kernel:UOp) -> tuple:
|
||||
_, x_u, weight_u, bias_u = kernel.src[1:5]
|
||||
x, weight, bias = (Tensor(u, device=u.device) for u in (x_u, weight_u, bias_u))
|
||||
reference = x.float() @ weight.float().T + bias.float()
|
||||
grad_x, grad_weight, grad_bias = reference.gradient(x, weight, bias, gradient=Tensor(gradient, device=x_u.device))
|
||||
return None, grad_x.uop, grad_weight.uop, grad_bias.uop
|
||||
|
||||
def router_mfma(x:Tensor, weight:Tensor, bias:Tensor) -> Tensor:
|
||||
assert x.ndim >= 2 and weight.ndim == 2 and bias.ndim == 1
|
||||
K = x.shape[-1]
|
||||
E = weight.shape[0]
|
||||
assert weight.shape == (E, K) and bias.shape == (E,)
|
||||
assert x.dtype == weight.dtype == bias.dtype == dtypes.bfloat16
|
||||
assert E == 32 and K % 64 == 0
|
||||
if isinstance(x.device, tuple):
|
||||
assert x.uop.axis == 0, f"router MFMA requires axis-0 sharding, got axis={x.uop.axis}"
|
||||
local_shape = x.uop.shard_shape
|
||||
assert local_shape[-1] == K and math.prod(local_shape[:-1]) % 64 == 0, f"unsupported local router shape {local_shape}"
|
||||
else:
|
||||
assert math.prod(x.shape[:-1]) % 64 == 0
|
||||
x, weight, bias = x.contiguous(), weight.contiguous(), bias.contiguous()
|
||||
out = _sharded_invalids((*x.shape[:-1], E), dtypes.float32, x.device)
|
||||
out, *_ = Tensor.custom_kernel(out, x, weight, bias,
|
||||
fxn=functools.partial(_router_mfma_fwd, dname=str(x.device)), grad_fxn=_router_mfma_bwd)
|
||||
return out
|
||||
|
||||
def _sharded_invalids(shape:tuple[int, ...], dtype, device) -> Tensor:
|
||||
if isinstance(device, tuple):
|
||||
per = Tensor.invalids(shape[0]//len(device), *shape[1:], dtype=dtype, device=device)
|
||||
@@ -53,7 +98,7 @@ def _ggather_bwd(gradient:UOp, kernel:UOp) -> tuple:
|
||||
g, m, j, jo, ji = _kv_ranges(Gk, M, Dk, _blk_for(Dk))
|
||||
row = idx.index(g, m).cast(dtypes.weakint)
|
||||
val = gout.index(g, m, j).load().cast(dtypes.float32)
|
||||
atomic = UOp(Ops.CUSTOM, dtypes.void, (gtab.index(g, row, j), val), arg=atomic_str)
|
||||
atomic = UOp(Ops.CUSTOM, src=(gtab.index(g, row, j), val), arg=(atomic_str, dtypes.void))
|
||||
return atomic.end(g, m, jo, ji).sink(arg=KernelInfo(name=f"ggather_bwd_{M}_{Dk}", opts_to_apply=()))
|
||||
grad_table = Tensor.custom_kernel(gt, go, Tensor(idx_u, device=dev), fxn=_bwd_kernel)[0]
|
||||
return (None, grad_table.cast(table_u.dtype).uop, None)
|
||||
|
||||
@@ -223,7 +223,7 @@ def test_matmul():
|
||||
lds = UOp.placeholder((lds_size,), dtypes.uint8, 0, AddrSpace.LOCAL)
|
||||
sink = UOp.sink(A.base, B.base, C.base, lds, *gidxs, *lidxs,
|
||||
arg=KernelInfo(name=colored("kernel","cyan"), estimates=Estimates(ops=N*N*N*2, mem=N*N*2*3)))
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=x) for x in insts]))))
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=(x, dtypes.void)) for x in insts]))))
|
||||
|
||||
c = Tensor.custom_kernel(a, b, c, fxn=asm_kernel)[2]
|
||||
linear = c.schedule_linear()
|
||||
|
||||
@@ -2,7 +2,7 @@ import numpy as np
|
||||
from tinygrad import dtypes, Tensor
|
||||
from tinygrad.helpers import getenv, get_single_element
|
||||
from tinygrad.dtype import _to_np_dtype
|
||||
from tinygrad.engine.realize import compile_linear
|
||||
from tinygrad.engine.realize import lower_and_compile
|
||||
from tinygrad.codegen.opt import OptOps
|
||||
|
||||
dtype_in = (dtypes.half if getenv("HALF") else dtypes.bfloat16 if getenv("BFLOAT16") else
|
||||
@@ -39,7 +39,7 @@ if __name__ == "__main__":
|
||||
c = a.matmul(b, dtype=acc_dtype).realize()
|
||||
|
||||
if getenv("SHOULD_USE_TC"):
|
||||
linear = compile_linear(a.matmul(b, dtype=acc_dtype).schedule_linear())
|
||||
linear = lower_and_compile(a.matmul(b, dtype=acc_dtype).schedule_linear())
|
||||
call = get_single_element(list(linear.src))
|
||||
applied_opts = call.src[0].src[0].arg.applied_opts
|
||||
assert any(opt.op is OptOps.TC for opt in applied_opts), f"TC not triggered, {applied_opts}"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from tinygrad import Tensor, dtypes, Context
|
||||
from tinygrad.helpers import getenv
|
||||
from tinygrad.codegen.opt import Opt, OptOps
|
||||
from tinygrad.uop.ops import AxisType
|
||||
from tinygrad.engine.realize import run_linear
|
||||
from dataclasses import replace
|
||||
|
||||
@@ -13,17 +14,17 @@ if __name__ == "__main__":
|
||||
C = A.matmul(B)
|
||||
if getenv("GEMV"):
|
||||
opts = [
|
||||
Opt(op=OptOps.UNROLL, axis=0, amt=8),
|
||||
Opt(op=OptOps.GROUP, axis=0, amt=32),
|
||||
Opt(op=OptOps.SPLIT, axis=1, arg=(8, AxisType.UNROLL)),
|
||||
Opt(op=OptOps.SPLIT, axis=1, arg=(32, AxisType.GROUP_REDUCE)),
|
||||
]
|
||||
else:
|
||||
opts = [
|
||||
Opt(op=OptOps.TC, axis=0, amt=0),
|
||||
Opt(op=OptOps.UPCAST, axis=0, amt=4),
|
||||
Opt(op=OptOps.UPCAST, axis=1, amt=8),
|
||||
Opt(op=OptOps.LOCAL, axis=0, amt=2),
|
||||
Opt(op=OptOps.LOCAL, axis=1, amt=2),
|
||||
Opt(op=OptOps.LOCAL, axis=0, amt=2),
|
||||
Opt(op=OptOps.SPLIT, axis=0, arg=(4, AxisType.UPCAST)),
|
||||
Opt(op=OptOps.SPLIT, axis=1, arg=(8, AxisType.UPCAST)),
|
||||
Opt(op=OptOps.SPLIT, axis=0, arg=(2, AxisType.LOCAL)),
|
||||
Opt(op=OptOps.SPLIT, axis=1, arg=(2, AxisType.LOCAL)),
|
||||
Opt(op=OptOps.SPLIT, axis=0, arg=(2, AxisType.LOCAL)),
|
||||
]
|
||||
linear = C.schedule_linear()
|
||||
call = linear.src[-1]
|
||||
|
||||
@@ -79,7 +79,7 @@ if __name__ == "__main__":
|
||||
linear, var_vals = C.linear_with_vars()
|
||||
last_call = linear.src[-1]
|
||||
ast = last_call.src[0]
|
||||
bufs = [s.buffer for s in last_call.src[1:] if s.op is not Ops.BIND]
|
||||
bufs = [s.buffer for s in last_call.src[1:] if not s.is_bound_var]
|
||||
|
||||
src = compiled.asm["ptx"]
|
||||
# specify the shared memory here so we don't need to do it dynamically
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
from __future__ import annotations
|
||||
import functools, math, pathlib
|
||||
from tinygrad import Tensor, dtypes
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo
|
||||
from tinygrad.renderer import Estimates
|
||||
from extra.gemm.cdna_asm_gemm import FP8_DTYPE
|
||||
from extra.llama_kernels import NUM_WG, THREADS_PER_WG, alloc_like, alloc_local, compile_hip, dname_of
|
||||
|
||||
def rmsnorm_mul_fwd(x_in:Tensor, weight:Tensor, eps:float) -> tuple[Tensor, Tensor]:
|
||||
x = x_in.float()
|
||||
rrms = (x.square().mean(-1, keepdim=True) + eps).rsqrt()
|
||||
return ((x * rrms) * weight.float()).cast(x_in.dtype), rrms
|
||||
|
||||
@functools.cache
|
||||
def _rmsnorm_mul_fwd_fxn(x_in_p, w_p, eps, device):
|
||||
return rmsnorm_mul_fwd(Tensor(x_in_p, device=device), Tensor(w_p, device=device), eps)
|
||||
|
||||
def _rmsnorm_mul_bwd(grad:UOp, call:UOp) -> tuple:
|
||||
x = Tensor(call.src[1]).float(); weight = Tensor(call.src[2]).float()
|
||||
rrms = Tensor(call.unbound_outputs[1])
|
||||
x_normed = x * rrms # recompute unweighted normed (x is call.src[1])
|
||||
d_y = Tensor(grad).float()
|
||||
dxn = d_y * weight # d/d(x_normed)
|
||||
d_x = rrms * (dxn - x_normed * (dxn * x_normed).mean(-1, keepdim=True))
|
||||
dw = d_y * x_normed
|
||||
d_weight = dw.sum(axis=tuple(range(dw.ndim - 1))) # reduce batch/seq -> [dim]
|
||||
return (d_x.cast(call.src[1].dtype).uop, d_weight.cast(call.src[2].dtype).uop)
|
||||
|
||||
def rmsnorm_mul(x_in:Tensor, weight:Tensor, eps:float) -> tuple[Tensor, Tensor]:
|
||||
fxn = _rmsnorm_mul_fwd_fxn(x_in.as_param(0).uop, weight.as_param(1).uop, eps, x_in.device)
|
||||
outs = UOp.call_with_outputs((fxn[0].uop, fxn[1].uop), x_in.uop, weight.uop, grad_fxn=_rmsnorm_mul_bwd)
|
||||
return Tensor(outs[0]), Tensor(outs[1])
|
||||
|
||||
@functools.cache
|
||||
def _custom_rmsnorm_mul_quantize_mxfp8_fwd(q:UOp, e8:UOp, rrms:UOp, x:UOp, weight:UOp, *, dname:str, eps:float) -> UOp:
|
||||
*lead, hidden = x.shape
|
||||
rows, padded = math.prod(lead), q.shape[-1]
|
||||
num_wg = min(NUM_WG, rows)
|
||||
threads, workgroups = UOp.special(THREADS_PER_WG, "lidx0"), UOp.special(num_wg, "gidx0")
|
||||
sink = UOp.sink(q.base, e8.base, rrms.base, x.base, weight.base, threads, workgroups,
|
||||
arg=KernelInfo(f"rmsnorm_mul_quantize_mxfp8_{rows}_{hidden}_{padded}",
|
||||
estimates=Estimates(ops=8*rows*hidden, mem=rows*(hidden*2+padded+padded//32+4)+hidden*2)))
|
||||
src = (pathlib.Path(__file__).parent/"rmsnorm_mul_quantize_mxfp8.cpp").read_text()
|
||||
defines = [f"-DN_ELEMS={rows*hidden}", f"-DHIDDEN={hidden}", f"-DPADDED={padded}",
|
||||
f"-DNUM_WG={num_wg}", f"-DTHREADS_PER_WG={THREADS_PER_WG}", f"-DEPS_LITERAL={eps}f"]
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=(*sink.src, sink)), UOp(Ops.SOURCE, arg=src),
|
||||
UOp(Ops.BINARY, arg=compile_hip(src, defines))))
|
||||
|
||||
@functools.cache
|
||||
def _custom_rmsnorm_mul_quantize_mxfp8_bwd(grad_x:UOp, grad_weight_partial:UOp, grad_q:UOp, x:UOp, weight:UOp, e8:UOp, rrms:UOp,
|
||||
*, dname:str) -> UOp:
|
||||
*lead, hidden = x.shape
|
||||
rows, padded = math.prod(lead), grad_q.shape[-1]
|
||||
num_wg = min(NUM_WG, rows)
|
||||
threads, workgroups = UOp.special(THREADS_PER_WG, "lidx0"), UOp.special(num_wg, "gidx0")
|
||||
sink = UOp.sink(grad_x.base, grad_weight_partial.base, grad_q.base, x.base, weight.base, e8.base, rrms.base,
|
||||
threads, workgroups,
|
||||
arg=KernelInfo(f"rmsnorm_mul_quantize_mxfp8_bwd_{rows}_{hidden}_{padded}",
|
||||
estimates=Estimates(ops=10*rows*hidden, mem=rows*(hidden*6+padded*2+padded//32+4)+num_wg*hidden*4)))
|
||||
src = (pathlib.Path(__file__).parent/"rmsnorm_mul_quantize_mxfp8_bwd.cpp").read_text()
|
||||
defines = [f"-DN_ELEMS={rows*hidden}", f"-DHIDDEN={hidden}", f"-DPADDED={padded}", f"-DNUM_WG={num_wg}", f"-DTHREADS_PER_WG={THREADS_PER_WG}"]
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=(*sink.src, sink)), UOp(Ops.SOURCE, arg=src),
|
||||
UOp(Ops.BINARY, arg=compile_hip(src, defines))))
|
||||
|
||||
def _rmsnorm_mul_quantize_mxfp8_backward(gradient:UOp, kernel:UOp) -> tuple:
|
||||
_, e8_u, rrms_u, x_u, weight_u = kernel.src[1:]
|
||||
device = x_u.device
|
||||
axis = x_u.axis if isinstance(device, tuple) else None
|
||||
*lead, hidden = x_u.shape
|
||||
num_wg = min(NUM_WG, math.prod(lead))
|
||||
grad_x = alloc_like(x_u.shape, x_u.dtype, device, axis)
|
||||
grad_weight_partial = alloc_local((num_wg, hidden), dtypes.float32, device, axis)
|
||||
grad_q = Tensor(gradient, device=device).cast(dtypes.bfloat16).contiguous()
|
||||
grad_x, grad_weight_partial, *_ = Tensor.custom_kernel(
|
||||
grad_x, grad_weight_partial, grad_q, Tensor(x_u, device=device), Tensor(weight_u, device=device),
|
||||
Tensor(e8_u.after(kernel), device=device), Tensor(rrms_u.after(kernel), device=device),
|
||||
fxn=functools.partial(_custom_rmsnorm_mul_quantize_mxfp8_bwd, dname=dname_of(device)))
|
||||
grad_weight = grad_weight_partial.sum(0).cast(weight_u.dtype)
|
||||
return None, None, None, grad_x.uop, grad_weight.uop
|
||||
|
||||
def rmsnorm_mul_quantize_mxfp8(x:Tensor, weight:Tensor, eps:float, padded:int|None=None) -> tuple[Tensor, Tensor, Tensor]:
|
||||
"""RMSNorm(x)*weight directly to rowwise MXFP8. Returns (q, e8, rrms), without a BF16 normalized round-trip."""
|
||||
assert x.dtype == weight.dtype == dtypes.bfloat16 and x.shape[-1] == weight.shape[0], f"{x.shape=} {weight.shape=}"
|
||||
hidden = x.shape[-1]
|
||||
padded = math.ceil(hidden / 256) * 256 if padded is None else padded
|
||||
assert padded >= hidden and padded % 256 == 0 and hidden % 32 == 0
|
||||
axis = x.uop.axis if isinstance(x.device, tuple) else None
|
||||
q = alloc_like((*x.shape[:-1], padded), FP8_DTYPE, x.device, axis)
|
||||
e8 = alloc_like((*x.shape[:-1], padded // 32), dtypes.uint8, x.device, axis)
|
||||
rrms = alloc_like((*x.shape[:-1], 1), dtypes.float32, x.device, axis)
|
||||
q, e8, rrms, *_ = Tensor.custom_kernel(q, e8, rrms, x, weight,
|
||||
fxn=functools.partial(_custom_rmsnorm_mul_quantize_mxfp8_fwd, dname=dname_of(x.device), eps=eps),
|
||||
grad_fxn=_rmsnorm_mul_quantize_mxfp8_backward)
|
||||
return q, e8, rrms
|
||||
@@ -0,0 +1,95 @@
|
||||
#include <hip/hip_runtime.h>
|
||||
#include <hip/hip_bf16.h>
|
||||
#include <hip/hip_fp8.h>
|
||||
|
||||
#ifndef N_ELEMS
|
||||
#define N_ELEMS 47185920
|
||||
#endif
|
||||
#ifndef HIDDEN
|
||||
#define HIDDEN 2880
|
||||
#endif
|
||||
#ifndef PADDED
|
||||
#define PADDED 3072
|
||||
#endif
|
||||
#ifndef NUM_WG
|
||||
#define NUM_WG 1024
|
||||
#endif
|
||||
#ifndef THREADS_PER_WG
|
||||
#define THREADS_PER_WG 256
|
||||
#endif
|
||||
#ifndef EPS_LITERAL
|
||||
#define EPS_LITERAL 1e-5f
|
||||
#endif
|
||||
|
||||
constexpr int ROWS = N_ELEMS / HIDDEN;
|
||||
constexpr int BLOCK = 32;
|
||||
constexpr int SCALE_BLOCKS = PADDED / BLOCK;
|
||||
constexpr float FP8_MAX = 448.0f;
|
||||
|
||||
static_assert(N_ELEMS % HIDDEN == 0, "N_ELEMS must be divisible by HIDDEN");
|
||||
static_assert(HIDDEN % BLOCK == 0 && PADDED % BLOCK == 0 && PADDED >= HIDDEN,
|
||||
"HIDDEN and PADDED must be block aligned");
|
||||
static_assert(SCALE_BLOCKS <= THREADS_PER_WG, "one thread handles each MXFP8 block");
|
||||
|
||||
extern "C" __global__ __launch_bounds__(THREADS_PER_WG) void rmsnorm_mul_quantize_mxfp8(
|
||||
__hip_fp8_storage_t *__restrict__ q_out,
|
||||
uint8_t *__restrict__ e8_out,
|
||||
float *__restrict__ rrms_out,
|
||||
const __hip_bfloat16 *__restrict__ x,
|
||||
const __hip_bfloat16 *__restrict__ weight) {
|
||||
__shared__ float reduce[THREADS_PER_WG];
|
||||
__shared__ __hip_bfloat16 x_row[HIDDEN];
|
||||
|
||||
const int tid = threadIdx.x;
|
||||
for (int row = blockIdx.x; row < ROWS; row += NUM_WG) {
|
||||
const long long xbase = (long long)row * HIDDEN;
|
||||
float sum_sq = 0.0f;
|
||||
for (int col = tid; col < HIDDEN; col += THREADS_PER_WG) {
|
||||
__hip_bfloat16 xb = x[xbase + col];
|
||||
x_row[col] = xb;
|
||||
float xf = (float)xb;
|
||||
sum_sq = fmaf(xf, xf, sum_sq);
|
||||
}
|
||||
|
||||
reduce[tid] = sum_sq;
|
||||
__syncthreads();
|
||||
for (int s = THREADS_PER_WG / 2; s > 0; s >>= 1) {
|
||||
if (tid < s) reduce[tid] += reduce[tid + s];
|
||||
__syncthreads();
|
||||
}
|
||||
const float rrms = rsqrtf(reduce[0] * (1.0f / (float)HIDDEN) + EPS_LITERAL);
|
||||
if (tid == 0) rrms_out[row] = rrms;
|
||||
|
||||
if (tid < SCALE_BLOCKS) {
|
||||
const int col_base = tid * BLOCK;
|
||||
float vals[BLOCK];
|
||||
float amax = 0.0f;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < BLOCK; i++) {
|
||||
const int col = col_base + i;
|
||||
float v = 0.0f;
|
||||
if (col < HIDDEN) {
|
||||
float xn = (float)x_row[col] * rrms;
|
||||
__hip_bfloat16 yb = (__hip_bfloat16)(xn * (float)weight[col]);
|
||||
v = (float)yb;
|
||||
}
|
||||
vals[i] = v;
|
||||
amax = fmaxf(amax, fabsf(v));
|
||||
}
|
||||
int e8 = (int)floorf(log2f(fmaxf(amax, 1e-38f))) + 127;
|
||||
e8 = max(0, min(254, e8));
|
||||
const float qscale = exp2f((float)(127 - e8));
|
||||
__hip_fp8_storage_t packed[BLOCK];
|
||||
#pragma unroll
|
||||
for (int i = 0; i < BLOCK; i++) {
|
||||
float v = fmaxf(-FP8_MAX, fminf(FP8_MAX, vals[i] * qscale));
|
||||
packed[i] = __hip_cvt_float_to_fp8(v, __HIP_SATFINITE, __HIP_E4M3);
|
||||
}
|
||||
const long long qbase = (long long)row * PADDED + col_base;
|
||||
*reinterpret_cast<uint4 *>(&q_out[qbase]) = *reinterpret_cast<uint4 *>(&packed[0]);
|
||||
*reinterpret_cast<uint4 *>(&q_out[qbase + 16]) = *reinterpret_cast<uint4 *>(&packed[16]);
|
||||
e8_out[(long long)row * SCALE_BLOCKS + tid] = (uint8_t)e8;
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
#include <hip/hip_runtime.h>
|
||||
#include <hip/hip_bf16.h>
|
||||
|
||||
#ifndef N_ELEMS
|
||||
#define N_ELEMS 47185920
|
||||
#endif
|
||||
#ifndef HIDDEN
|
||||
#define HIDDEN 2880
|
||||
#endif
|
||||
#ifndef PADDED
|
||||
#define PADDED 3072
|
||||
#endif
|
||||
#ifndef NUM_WG
|
||||
#define NUM_WG 1024
|
||||
#endif
|
||||
#ifndef THREADS_PER_WG
|
||||
#define THREADS_PER_WG 256
|
||||
#endif
|
||||
|
||||
constexpr int ROWS = N_ELEMS / HIDDEN;
|
||||
constexpr int BLOCK = 32;
|
||||
constexpr int SCALE_BLOCKS = PADDED / BLOCK;
|
||||
constexpr int ELEMS_PER_THREAD = (HIDDEN + THREADS_PER_WG - 1) / THREADS_PER_WG;
|
||||
|
||||
static_assert(N_ELEMS % HIDDEN == 0, "N_ELEMS must be divisible by HIDDEN");
|
||||
static_assert(HIDDEN % BLOCK == 0 && PADDED % BLOCK == 0 && PADDED >= HIDDEN,
|
||||
"HIDDEN and PADDED must be block aligned");
|
||||
|
||||
extern "C" __global__ __launch_bounds__(THREADS_PER_WG) void rmsnorm_mul_quantize_mxfp8_bwd(
|
||||
__hip_bfloat16 *__restrict__ grad_x,
|
||||
float *__restrict__ grad_weight_partial,
|
||||
const __hip_bfloat16 *__restrict__ grad_q,
|
||||
const __hip_bfloat16 *__restrict__ x,
|
||||
const __hip_bfloat16 *__restrict__ weight,
|
||||
const uint8_t *__restrict__ e8,
|
||||
const float *__restrict__ rrms) {
|
||||
__shared__ float reduce[THREADS_PER_WG];
|
||||
const int tid = threadIdx.x;
|
||||
const int wg = blockIdx.x;
|
||||
|
||||
float w[ELEMS_PER_THREAD];
|
||||
float gw[ELEMS_PER_THREAD];
|
||||
#pragma unroll
|
||||
for (int i = 0; i < ELEMS_PER_THREAD; i++) {
|
||||
int col = tid + i * THREADS_PER_WG;
|
||||
w[i] = col < HIDDEN ? (float)weight[col] : 0.0f;
|
||||
gw[i] = 0.0f;
|
||||
}
|
||||
|
||||
for (int row = wg; row < ROWS; row += NUM_WG) {
|
||||
const long long xbase = (long long)row * HIDDEN;
|
||||
const long long qbase = (long long)row * PADDED;
|
||||
const long long ebase = (long long)row * SCALE_BLOCKS;
|
||||
const float r = rrms[row];
|
||||
float xn[ELEMS_PER_THREAD];
|
||||
float gxn[ELEMS_PER_THREAD];
|
||||
float local_dot = 0.0f;
|
||||
|
||||
#pragma unroll
|
||||
for (int i = 0; i < ELEMS_PER_THREAD; i++) {
|
||||
const int col = tid + i * THREADS_PER_WG;
|
||||
if (col < HIDDEN) {
|
||||
const float xnf = (float)x[xbase + col] * r;
|
||||
const unsigned se = (unsigned)(254 - (int)e8[ebase + col / BLOCK]) << 23;
|
||||
const float qscale = __builtin_bit_cast(float, se);
|
||||
const float gy = (float)grad_q[qbase + col] * qscale;
|
||||
const float gxnf = gy * w[i];
|
||||
xn[i] = xnf;
|
||||
gxn[i] = gxnf;
|
||||
gw[i] += gy * xnf;
|
||||
local_dot = fmaf(gxnf, xnf, local_dot);
|
||||
} else {
|
||||
xn[i] = gxn[i] = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
reduce[tid] = local_dot;
|
||||
__syncthreads();
|
||||
for (int s = THREADS_PER_WG / 2; s > 0; s >>= 1) {
|
||||
if (tid < s) reduce[tid] += reduce[tid + s];
|
||||
__syncthreads();
|
||||
}
|
||||
const float mean_term = reduce[0] * (1.0f / (float)HIDDEN);
|
||||
|
||||
#pragma unroll
|
||||
for (int i = 0; i < ELEMS_PER_THREAD; i++) {
|
||||
const int col = tid + i * THREADS_PER_WG;
|
||||
if (col < HIDDEN) grad_x[xbase + col] = (__hip_bfloat16)(r * (gxn[i] - xn[i] * mean_term));
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
const long long gwbase = (long long)wg * HIDDEN;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < ELEMS_PER_THREAD; i++) {
|
||||
const int col = tid + i * THREADS_PER_WG;
|
||||
if (col < HIDDEN) grad_weight_partial[gwbase + col] = gw[i];
|
||||
}
|
||||
}
|
||||
+14
-5
@@ -16,9 +16,12 @@ def _do_reset_device(pci_bus): os.system(f"sudo sh -c 'echo 1 > /sys/bus/pci/dev
|
||||
def _is_module_loaded(name: str) -> bool: return os.path.isdir(f"/sys/module/{name}")
|
||||
|
||||
def cmd_remove_module(args):
|
||||
modules = ["nvidia_drm", "nvidia_modeset", "nvidia_uvm", "nvidia", "ast"] if args.backend == "nv" else ["amdgpu"]
|
||||
modules = ["nvidia_drm", "nvidia_modeset", "nvidia_uvm", "nvidia"] if args.backend == "nv" else ["amdgpu"]
|
||||
to_unload = [m for m in modules if _is_module_loaded(m)]
|
||||
if not to_unload: print("Kernel modules are not loaded")
|
||||
elif getattr(args, "expect", False):
|
||||
print(f"Kernel modules are loaded: {to_unload}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("Removing kernel modules:", ", ".join(to_unload))
|
||||
try: subprocess.run(["sudo", "modprobe", "-r", *to_unload], check=True)
|
||||
@@ -60,17 +63,19 @@ def cmd_show_pids(args):
|
||||
|
||||
def cmd_kill_pids(args):
|
||||
devs = scan_devs_based_on_lock(prefix:={"amd":"am", "nv":"nv"}[args.backend], args)
|
||||
use_sudo = not getattr(args, "sudoless", False)
|
||||
|
||||
for dev in devs:
|
||||
for i in range(128):
|
||||
if i > 0: time.sleep(0.2)
|
||||
|
||||
try:
|
||||
try: pid = subprocess.check_output(['sudo', 'lsof', temp(f'{prefix}_{dev}.lock')]).decode('utf-8').strip().split('\n')[1].split()[1]
|
||||
try: pid = subprocess.check_output((['sudo'] if use_sudo else []) +
|
||||
['lsof', temp(f'{prefix}_{dev}.lock')]).decode('utf-8').strip().split('\n')[1].split()[1]
|
||||
except subprocess.CalledProcessError: break
|
||||
|
||||
print(f"Killing process {pid} (which uses {dev})")
|
||||
subprocess.run(['sudo', 'kill', '-9', pid], check=True)
|
||||
subprocess.run((['sudo'] if use_sudo else []) + ['kill', '-9', pid], check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Failed to kill process for device {dev}: {e}", file=sys.stderr)
|
||||
|
||||
@@ -79,6 +84,7 @@ def add_common_commands(parent_subparsers):
|
||||
p_insmod.set_defaults(func=cmd_insert_module)
|
||||
|
||||
p_rmmod = parent_subparsers.add_parser("rmmod", help="Remove a kernel module")
|
||||
p_rmmod.add_argument("--expect", action="store_true", help="Just assert that module is already unloaded")
|
||||
p_rmmod.set_defaults(func=cmd_remove_module)
|
||||
|
||||
p_reset = parent_subparsers.add_parser("reset", help="Reset a device")
|
||||
@@ -91,17 +97,20 @@ def add_common_commands(parent_subparsers):
|
||||
|
||||
p_reset = parent_subparsers.add_parser("kill_pids", help="Kill pids of processes using the device")
|
||||
p_reset.add_argument("--pci_bus", default="", help="PCI bus ID of the device")
|
||||
p_reset.add_argument("--sudoless", action="store_true", help="Do not use sudo when detecting or killing pids")
|
||||
p_reset.set_defaults(func=cmd_kill_pids)
|
||||
|
||||
if __name__ == "__main__":
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import collections, time
|
||||
from typing import Any, cast
|
||||
from tinygrad.helpers import round_up, PROFILE, ALL2ALL, merge_dicts, getenv, suppress_finalizing, TracingKey, unwrap
|
||||
from tinygrad.runtime.support.hcq import HCQCompiled, HCQAllocator, HCQSignal, HCQBuffer, HWQueue, HCQArgsState, BumpAllocator, MMIOInterface
|
||||
from extra.hcq1.hcq import HCQCompiled, HCQAllocator, HCQSignal, HWQueue, HCQArgsState
|
||||
from tinygrad.runtime.support.hcq import HCQBuffer, BumpAllocator, MMIOInterface
|
||||
from tinygrad.device import Buffer, BufferSpec, Compiled, Device, MultiBuffer, ProfileGraphEntry, ProfileGraphEvent
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.uop.ops import UOp, Ops, Variable
|
||||
from tinygrad.engine.jit import GraphRunner, MultiGraphRunner
|
||||
from tinygrad.runtime.ops_rdma import RDMACopyQueue
|
||||
|
||||
class HCQGraph(MultiGraphRunner):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -30,7 +30,7 @@ class HCQGraph(MultiGraphRunner):
|
||||
for runtime in self.runtimes:
|
||||
if runtime is None: continue
|
||||
kernargs_size[runtime.dev] += round_up(runtime.kernargs_alloc_size, 16)
|
||||
self.kernargs_bufs: dict[Compiled, HCQBuffer] = {d:d.allocator._alloc(max(sz, 1), BufferSpec(cpu_access=True)) for d,sz in kernargs_size.items()}
|
||||
self.kernargs_bufs: dict[Compiled, HCQBuffer] = {d:d.allocator._alloc(max(sz, 1), BufferSpec(cpu_access=True))[0][0] for d,sz in kernargs_size.items()}
|
||||
|
||||
# Fill initial arguments.
|
||||
self.ji_args: dict[int, HCQArgsState] = {}
|
||||
@@ -50,7 +50,7 @@ class HCQGraph(MultiGraphRunner):
|
||||
|
||||
self.comp_queues: dict[HCQCompiled, HWQueue] = {dev: unwrap(dev.hw_compute_queue_t)() for dev in self.devices}
|
||||
self.copy_queues: dict[tuple[HCQCompiled, int], HWQueue] = {} # lazy allocation, keyed by (device, queue_idx)
|
||||
self.rdma_queues: dict[tuple[HCQCompiled, HCQCompiled], RDMACopyQueue] = {} # lazy allocation, keyed by device pair
|
||||
self.rdma_queues: dict[tuple[HCQCompiled, HCQCompiled], "RDMACopyQueue"] = {} # lazy allocation, keyed by device pair
|
||||
self.num_copy_queues: int = getenv("HCQ_NUM_SDMA", min(len(self.devices), 8) if ALL2ALL >= 1 else 1)
|
||||
self.num_rdma_ops: dict[tuple[HCQCompiled, HCQCompiled], int] = collections.defaultdict(int)
|
||||
|
||||
@@ -97,13 +97,13 @@ class HCQGraph(MultiGraphRunner):
|
||||
|
||||
# set any fixedvars on the device
|
||||
self.device_vars[enqueue_dev] = merge_dicts([self.device_vars.get(enqueue_dev, {}), device_vars])
|
||||
if runtime is not None: self.device_vars[enqueue_dev] = merge_dicts([self.device_vars[enqueue_dev], {k: 0 for k in ast.arg.runtimevars}])
|
||||
|
||||
if runtime is not None:
|
||||
enqueue_queue = self.comp_queues[enqueue_dev]
|
||||
elif is_rdma:
|
||||
enqueue_queue = self.comp_queues[enqueue_dev]
|
||||
rdma_key = (cast(HCQCompiled, Device[bufs[0].device]).rdma_dev(), enqueue_dev.rdma_dev())
|
||||
from extra.hcq1.ops_rdma import RDMACopyQueue
|
||||
self.rdma_queues.setdefault(rdma_key, RDMACopyQueue(enqueue_dev.rdma_dev()))
|
||||
else:
|
||||
assert (enqueue_dev.hw_copy_queue_t is not None), "device must implement a copy queue"
|
||||
@@ -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
|
||||
@@ -172,7 +173,7 @@ class HCQGraph(MultiGraphRunner):
|
||||
|
||||
# Encode main commands based on ji type.
|
||||
if runtime is not None:
|
||||
enqueue_queue.exec(runtime, self.ji_args[j], ast.arg.global_size or (1,1,1), ast.arg.local_size or (1,1,1))
|
||||
enqueue_queue.exec(runtime, self.ji_args[j], ast.arg.global_size, ast.arg.local_size)
|
||||
elif j in self.rdma_deps:
|
||||
dest_queue, dest_deps, dest_out_signal, dest_out_val = self.rdma_deps[j]
|
||||
for sig, val in dest_deps: dest_queue.wait(sig, val)
|
||||
@@ -0,0 +1,549 @@
|
||||
from __future__ import annotations
|
||||
from typing import cast, Callable, Type, TypeVar, Generic, Any
|
||||
import contextlib, decimal, statistics, time, ctypes, array, collections, itertools
|
||||
from tinygrad.helpers import PROFILE, getenv, from_mv, cpu_profile, ProfileRangeEvent, unwrap
|
||||
from tinygrad.helpers import suppress_finalizing, TracingKey
|
||||
from tinygrad.device import BufferSpec, Compiled, Allocator, ProfileDeviceEvent, ProfileProgramEvent, Program, TinyELF
|
||||
from tinygrad.uop.ops import sym_infer, sint, UOp
|
||||
from tinygrad.runtime.support.memory import BumpAllocator, MMIOInterface
|
||||
from tinygrad.renderer import Renderer
|
||||
from tinygrad.runtime.support.hcq import HCQBuffer
|
||||
|
||||
SignalType = TypeVar('SignalType', bound='HCQSignal')
|
||||
HCQDeviceType = TypeVar('HCQDeviceType', bound='HCQCompiled')
|
||||
ProgramType = TypeVar('ProgramType', bound='HCQProgram')
|
||||
ArgsStateType = TypeVar('ArgsStateType', bound='HCQArgsState')
|
||||
|
||||
class HWQueue(Generic[SignalType, HCQDeviceType, ProgramType, ArgsStateType]):
|
||||
"""
|
||||
A base class for hardware command queues in the HCQ (Hardware Command Queue) API.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._q:Any = []
|
||||
self.binded_device:HCQDeviceType|None = None
|
||||
self.q_sints:list[tuple[int, int]] = []
|
||||
self.mv_sints:list[tuple[MMIOInterface, int, int, int|None]] = []
|
||||
self.syms:list[sint] = []
|
||||
self._prev_resolved_syms:list[int|None] = []
|
||||
|
||||
def _new_sym(self, sym:sint) -> int:
|
||||
if sym not in self.syms:
|
||||
self.syms.append(sym)
|
||||
self._prev_resolved_syms.append(None)
|
||||
return self.syms.index(sym)
|
||||
|
||||
def q(self, *values):
|
||||
"""
|
||||
Enqueues values in the queue.
|
||||
|
||||
Args:
|
||||
values: The values to enqueue in the queue.
|
||||
"""
|
||||
|
||||
for v in values:
|
||||
if isinstance(v, UOp):
|
||||
self.q_sints.append((len(self._q), self._new_sym(v)))
|
||||
self._q.append(0xbadc0ded)
|
||||
else: self._q.append(v)
|
||||
|
||||
# *** common commands ***
|
||||
|
||||
def timestamp(self, signal:SignalType):
|
||||
"""
|
||||
Enqueues a timestamp command which records the current time in a signal after all previously enqueued commands are completed.
|
||||
|
||||
Args:
|
||||
signal: The signal to store the timestamp
|
||||
"""
|
||||
|
||||
def signal(self, signal:SignalType, value:sint):
|
||||
"""
|
||||
Enqueues a signal command which sets the signal to the given value, ensuring all previous operations are completed.
|
||||
|
||||
Args:
|
||||
signal: The signal to set
|
||||
value: The value to set the signal to
|
||||
"""
|
||||
|
||||
def wait(self, signal:SignalType, value:sint):
|
||||
"""
|
||||
Enqueues a wait command which halts execution until the signal is greater than or equal to a specific value.
|
||||
|
||||
Args:
|
||||
signal: The signal to wait on
|
||||
value: The value to wait for
|
||||
"""
|
||||
|
||||
# *** commands for compute queues ***
|
||||
|
||||
def memory_barrier(self):
|
||||
"""
|
||||
Enqueues a memory barrier command to ensure memory coherence between agents. Only on compute queues.
|
||||
"""
|
||||
|
||||
def exec(self, prg:ProgramType, args_state:ArgsStateType, global_size:tuple[sint, ...], local_size:tuple[sint, ...]):
|
||||
"""
|
||||
Enqueues an execution command for a kernel program. Only on compute queues.
|
||||
|
||||
Args:
|
||||
prg: The program to execute
|
||||
args_state: The args state to execute program with
|
||||
global_size: The global work size
|
||||
local_size: The local work size
|
||||
"""
|
||||
|
||||
def write(self, b:HCQBuffer, val:sint, b64:bool=False):
|
||||
"""
|
||||
Enqueues a command to write a value to a buffer address after all previously enqueued commands are completed.
|
||||
|
||||
Args:
|
||||
b: The buffer to write to
|
||||
val: The value to write
|
||||
b64: If True, write a 64-bit value; otherwise write 32-bit
|
||||
"""
|
||||
raise NotImplementedError("write not implemented")
|
||||
|
||||
def poll_bit(self, b:HCQBuffer, val:sint, mask:int):
|
||||
"""
|
||||
Enqueues a poll command which halts execution until (mem[b] & mask) == val.
|
||||
val must be 0 or mask (i.e. checks if masked bits are all clear or all set).
|
||||
|
||||
Args:
|
||||
b: The buffer to poll
|
||||
val: The expected value after masking (0 or mask)
|
||||
mask: The bit mask to test
|
||||
"""
|
||||
raise NotImplementedError("poll_bit not implemented")
|
||||
|
||||
# *** commands for copy queues ***
|
||||
|
||||
def copy(self, dest:HCQBuffer, src:HCQBuffer, copy_size:int):
|
||||
"""
|
||||
Enqueues a copy command to transfer data. Only on copy queues.
|
||||
|
||||
Args:
|
||||
dest: The destination buffer of the copy
|
||||
src: The source buffer of the copy
|
||||
copy_size: The size of data to copy
|
||||
"""
|
||||
|
||||
# *** submit and bind commands ***
|
||||
|
||||
def bind(self, dev:HCQDeviceType):
|
||||
"""
|
||||
Associates the queue with a specific device for optimized execution.
|
||||
|
||||
This optional method allows backend implementations to tailor the queue for efficient use on the given device. When implemented, it can eliminate
|
||||
the need to copy queues into the device, thereby enhancing performance.
|
||||
|
||||
Args:
|
||||
dev: The target device for queue optimization.
|
||||
|
||||
Note:
|
||||
Implementing this method is optional but recommended for performance gains.
|
||||
"""
|
||||
|
||||
def bind_args_state(self, args_state:ArgsStateType):
|
||||
for vals, mem, fmt in args_state.bind_data: self.bind_sints_to_mem(*vals, mem=mem, fmt=fmt)
|
||||
|
||||
def bind_sints(self, *vals:sint, mem:MMIOInterface, struct_t:Type[ctypes.Structure], start_field:str, fmt, mask:int|None=None):
|
||||
self.bind_sints_to_mem(*vals, mem=mem, fmt=fmt, mask=mask, offset=getattr(struct_t, start_field).offset)
|
||||
|
||||
def bind_sints_to_mem(self, *vals:sint, mem:MMIOInterface, fmt, mask:int|None=None, offset:int=0):
|
||||
mv = mem.view(offset=offset, size=len(vals)*8, fmt=fmt)
|
||||
for i, val in enumerate(vals):
|
||||
if isinstance(val, int): mv[i] = val if mask is None else ((mv[i] & ~mask) | val)
|
||||
else: self.mv_sints.append((mv, i, self._new_sym(val), mask))
|
||||
|
||||
def _apply_var_vals(self, var_vals:dict[str, int]):
|
||||
resolved_syms: list[int|None] = [sym_infer(sym, var_vals) for sym in self.syms]
|
||||
|
||||
for off, sym_idx in self.q_sints:
|
||||
if self._prev_resolved_syms[sym_idx] == resolved_syms[sym_idx]: continue
|
||||
self._q[off] = resolved_syms[sym_idx]
|
||||
|
||||
for mv, off, sym_idx, mask in self.mv_sints:
|
||||
if self._prev_resolved_syms[sym_idx] == resolved_syms[sym_idx]: continue
|
||||
mv[off] = resolved_syms[sym_idx] if mask is None else ((mv[off] & ~mask) | resolved_syms[sym_idx])
|
||||
|
||||
self._prev_resolved_syms = resolved_syms
|
||||
|
||||
def submit(self, dev:HCQDeviceType, var_vals:dict[str, int]|None=None):
|
||||
"""
|
||||
Submits the command queue to a specific device for execution.
|
||||
|
||||
Args:
|
||||
dev: The device to submit the queue to
|
||||
"""
|
||||
|
||||
if var_vals is not None: self._apply_var_vals(var_vals)
|
||||
self._submit(dev)
|
||||
return self
|
||||
def _submit(self, dev:HCQDeviceType): raise NotImplementedError("need _submit")
|
||||
|
||||
class HCQSignal(Generic[HCQDeviceType]):
|
||||
def __init__(self, base_buf:HCQBuffer, value:int=0, owner:HCQDeviceType|None=None, is_timeline:bool=False, timestamp_divider=1000, virt=False):
|
||||
self.base_buf, self.owner, self.is_timeline = base_buf, owner, is_timeline
|
||||
self.should_return = isinstance(self.base_buf.va_addr, int) and self.owner is not None and not virt
|
||||
self.timestamp_divider:decimal.Decimal = decimal.Decimal(timestamp_divider)
|
||||
if isinstance(self.base_buf.va_addr, int) and not virt: self.value = value
|
||||
|
||||
def __del__(self):
|
||||
if self.should_return: HCQCompiled.signal_pool[unwrap(self.owner).peer_group].append(self.base_buf)
|
||||
|
||||
@property
|
||||
def value_addr(self) -> sint: return self.base_buf.va_addr
|
||||
|
||||
@property
|
||||
def timestamp_addr(self) -> sint: return self.base_buf.va_addr + 8
|
||||
|
||||
@property
|
||||
def value(self) -> int: return self.base_buf.cpu_view().view(0, 8, 'Q')[0]
|
||||
|
||||
@value.setter
|
||||
def value(self, new_value:int): self.base_buf.cpu_view().view(0, 8, 'Q')[0] = new_value
|
||||
|
||||
@property
|
||||
def timestamp(self) -> decimal.Decimal:
|
||||
"""
|
||||
Get the timestamp field of the signal.
|
||||
|
||||
This property provides read-only access to the signal's timestamp.
|
||||
|
||||
Returns:
|
||||
The timestamp in microseconds.
|
||||
"""
|
||||
return self.base_buf.cpu_view().view(8, 8, 'Q')[0] / self.timestamp_divider
|
||||
|
||||
def _sleep(self, time_spent_since_last_sleep_ms:int):
|
||||
"""
|
||||
Optional function which can implement sleep functionality for the signal.
|
||||
Raises RuntimeError if a fault is detected.
|
||||
"""
|
||||
|
||||
def wait(self, value:int, timeout:int|None=None):
|
||||
"""
|
||||
Waits the signal is greater than or equal to a specific value.
|
||||
|
||||
Args:
|
||||
value: The value to wait for.
|
||||
timeout: Maximum time to wait in milliseconds. Defaults to 30s.
|
||||
"""
|
||||
timeout = timeout or getenv("HCQDEV_WAIT_TIMEOUT_MS", 30000)
|
||||
start_time = int(time.perf_counter() * 1000)
|
||||
while (not_passed:=(prev_value:=self.value) < value) and (cur_time:=int(time.perf_counter() * 1000)) - start_time < timeout:
|
||||
self._sleep(cur_time - start_time)
|
||||
if self.value != prev_value: start_time = int(time.perf_counter() * 1000) # progress was made, reset timer
|
||||
if not_passed and self.value < value: raise RuntimeError(f"Wait timeout: {timeout} ms! (the signal is not set to {value}, but {self.value})")
|
||||
|
||||
@contextlib.contextmanager
|
||||
def hcq_profile(dev:HCQCompiled, enabled, desc, queue_type:Callable[[], HWQueue]|None=None, queue:HWQueue|None=None, dev_suff:str|None=None,
|
||||
profile_key:bytes|None=None):
|
||||
st, en = (dev.new_signal(), dev.new_signal()) if enabled else (None, None)
|
||||
assert queue is not None or queue_type is not None, "Either queue or queue_type must be provided"
|
||||
|
||||
if enabled and queue is not None: queue.timestamp(st)
|
||||
elif enabled and queue_type is not None:
|
||||
queue_type().wait(dev.timeline_signal, dev.timeline_value - 1).timestamp(st).signal(dev.timeline_signal, dev.next_timeline()).submit(dev)
|
||||
|
||||
try: yield (st, en)
|
||||
finally:
|
||||
if enabled and queue is not None: queue.timestamp(en)
|
||||
elif enabled and queue_type is not None:
|
||||
queue_type().wait(dev.timeline_signal, dev.timeline_value - 1).timestamp(en).signal(dev.timeline_signal, dev.next_timeline()).submit(dev)
|
||||
|
||||
if enabled and PROFILE: dev.sig_prof_records.append((unwrap(st), unwrap(en), desc, f"{dev.device}:{dev_suff}" if dev_suff else dev.device,
|
||||
profile_key))
|
||||
|
||||
class HCQArgsState(Generic[ProgramType]):
|
||||
def __init__(self, buf:HCQBuffer, prg:ProgramType, bufs:tuple[HCQBuffer, ...], vals:tuple[sint|None, ...]=()):
|
||||
self.buf, self.prg, self.bufs, self.vals = buf, prg, bufs, vals
|
||||
self.bind_data:list[tuple[tuple[sint, ...], MMIOInterface, str]] = []
|
||||
|
||||
def bind_sints_to_buf(self, *vals:sint, buf:HCQBuffer, fmt, offset=0): self.bind_data.append((vals, buf.cpu_view().view(offset=offset), fmt))
|
||||
|
||||
class CLikeArgsState(HCQArgsState[ProgramType]):
|
||||
def __init__(self, buf:HCQBuffer, prg:ProgramType, bufs:tuple[HCQBuffer, ...], vals:tuple[sint|None, ...]=(), prefix:list[int]|None=None):
|
||||
super().__init__(buf, prg, bufs, vals=vals)
|
||||
|
||||
if prefix is not None: self.buf.cpu_view().view(size=len(prefix) * 4, fmt='I')[:] = array.array('I', prefix)
|
||||
|
||||
self.bind_sints_to_buf(*[b.va_addr for b in bufs], buf=self.buf, fmt='Q', offset=len(prefix or []) * 4)
|
||||
for v,(val_offset,dt) in zip(vals, TinyELF.iter_sig(prg.signature[-len(vals):], len(bufs) * 8)):
|
||||
assert v is not None
|
||||
self.bind_sints_to_buf(v, buf=self.buf, fmt=dt.fmt, offset=len(prefix or []) * 4 + val_offset)
|
||||
|
||||
class HCQProgram(Program[HCQDeviceType]):
|
||||
def __init__(self, args_state_t:Type[HCQArgsState], dev:HCQDeviceType, obj:TinyELF, kernargs_alloc_size:int, base:int|None=None):
|
||||
self.args_state_t, self.dev, self.name, self.signature, self.kernargs_alloc_size = args_state_t, dev, obj.name, obj.signature, kernargs_alloc_size
|
||||
self.profile_key = obj.profile_key
|
||||
self.prof_prg_counter = next(self.dev.prof_prg_counter)
|
||||
if PROFILE: Compiled.profile_events += [ProfileProgramEvent(dev.device, obj.name, obj.lib, base, self.prof_prg_counter, self.profile_key)]
|
||||
|
||||
@staticmethod
|
||||
def _fini(dev, buf, spec): dev.allocator.free(((buf, buf.meta), buf.view), buf.size, spec)
|
||||
|
||||
def fill_kernargs(self, bufs:tuple[HCQBuffer, ...], vals:tuple[int|None, ...]=(), kernargs:HCQBuffer|None=None) -> HCQArgsState:
|
||||
"""
|
||||
Fills arguments for the kernel, optionally allocating space from the device if `kernargs_ptr` is not provided.
|
||||
Args:
|
||||
bufs: Buffers to be written to kernel arguments.
|
||||
vals: Values to be written to kernel arguments.
|
||||
kernargs_ptr: Optional pointer to pre-allocated kernel arguments memory.
|
||||
Returns:
|
||||
Arguments state with the given buffers and values set for the program.
|
||||
"""
|
||||
argsbuf = kernargs or self.dev.kernargs_buf.offset(offset=self.dev.kernargs_offset_allocator.alloc(self.kernargs_alloc_size, 8),
|
||||
size=self.kernargs_alloc_size)
|
||||
return self.args_state_t(argsbuf, self, bufs, vals=vals)
|
||||
|
||||
def __call__(self, *bufs:HCQBuffer, global_size:tuple[int,int,int]=(1,1,1), local_size:tuple[int,int,int]=(1,1,1),
|
||||
vals:tuple[int|None, ...]=(), wait:bool=False, timeout:int|None=None) -> float|None:
|
||||
"""
|
||||
Enqueues the program for execution with the given arguments and dimensions.
|
||||
|
||||
Args:
|
||||
bufs: Buffer arguments to execute the kernel with.
|
||||
global_size: Specifies the global work size for kernel execution (equivalent to CUDA's grid size).
|
||||
local_size: Specifies the local work size for kernel execution (equivalent to CUDA's block size).
|
||||
vals: Value arguments to execute the kernel with.
|
||||
wait: If True, waits for the kernel to complete execution.
|
||||
|
||||
Returns:
|
||||
Execution time of the kernel if 'wait' is True, otherwise None.
|
||||
"""
|
||||
|
||||
kernargs = self.fill_kernargs(bufs, vals)
|
||||
q = unwrap(self.dev.hw_compute_queue_t)().wait(self.dev.timeline_signal, self.dev.timeline_value - 1).memory_barrier()
|
||||
|
||||
self.dev.prof_exec_counter += 1
|
||||
with hcq_profile(self.dev, queue=q, desc=self.name, enabled=wait or PROFILE, profile_key=self.profile_key) as (sig_st, sig_en):
|
||||
q.exec(self, kernargs, global_size, local_size)
|
||||
|
||||
q.signal(self.dev.timeline_signal, self.dev.next_timeline()).submit(self.dev)
|
||||
|
||||
if wait: self.dev.synchronize(timeout=timeout)
|
||||
return (float(sig_en.timestamp - sig_st.timestamp) / 1e6) if wait else None
|
||||
|
||||
class HCQCompiled(Compiled, Generic[SignalType]):
|
||||
"""
|
||||
A base class for devices compatible with the HCQ (Hardware Command Queue) API.
|
||||
"""
|
||||
peer_groups: dict[str, list[HCQCompiled]] = collections.defaultdict(list)
|
||||
signal_pages: dict[str, list[HCQBuffer]] = collections.defaultdict(list) # per peer group
|
||||
signal_pool: dict[str, list[HCQBuffer]] = collections.defaultdict(list) # per peer group
|
||||
cpu_devices: list[HCQCompiled] = []
|
||||
|
||||
def __init__(self, device:str, allocator:HCQAllocatorBase, compilers:list[type[Renderer]], runtime:type[Program]|None,
|
||||
signal_t:Type[SignalType]|None=None, comp_queue_t:Callable[..., HWQueue]|None=None, copy_queue_t:Callable[..., HWQueue]|None=None,
|
||||
kernargs_size=(16 << 20), sigalloc_size=0x1000, can_recover:bool=False, arch=None):
|
||||
from extra.hcq1.graph import HCQGraph
|
||||
super().__init__(device, allocator, compilers, runtime, HCQGraph, arch=arch)
|
||||
|
||||
self.peer_group = getattr(getattr(self, 'iface', None), 'peer_group', device.split(":")[0])
|
||||
HCQCompiled.peer_groups[self.peer_group].append(self)
|
||||
|
||||
self.signal_t, self.hw_compute_queue_t, self.hw_copy_queue_t = signal_t, comp_queue_t, copy_queue_t
|
||||
|
||||
self.timeline_value:int = 1
|
||||
self.sig_prof_records:list[tuple[HCQSignal, HCQSignal, str|TracingKey, str, bytes|None]] = []
|
||||
self.prof_exec_counter:int = 0
|
||||
self.prof_prg_counter = itertools.count(0)
|
||||
|
||||
if signal_t is not None:
|
||||
# Map signals if any
|
||||
for sig_page in HCQCompiled.signal_pages[self.peer_group]: cast(HCQAllocator, self.allocator)._map(sig_page)
|
||||
|
||||
self.sigalloc_size = sigalloc_size
|
||||
self.timeline_signal, self._shadow_timeline_signal = self.new_signal(value=0, is_timeline=True), self.new_signal(value=0, is_timeline=True)
|
||||
|
||||
if comp_queue_t is not None:
|
||||
self.kernargs_buf:HCQBuffer = self.allocator.alloc(kernargs_size, BufferSpec(cpu_access=True))[0][0]
|
||||
self.kernargs_offset_allocator:BumpAllocator = BumpAllocator(self.kernargs_buf.size, wrap=True)
|
||||
|
||||
self.can_recover = can_recover # Whether the device can recover from faults or timeouts
|
||||
self.error_state:Exception|None = None # Exception if error is unrecoverable and sync will always fail
|
||||
|
||||
if self._is_cpu(): HCQCompiled.cpu_devices.append(self)
|
||||
|
||||
def synchronize(self, timeout:int|None=None):
|
||||
if self.error_state is not None: raise self.error_state
|
||||
if not hasattr(self, 'timeline_signal'): return
|
||||
|
||||
# If we have any work on CPU devices, need to synchronize them. This is just an optimization to release GIL allowing to finish faster.
|
||||
if not self._is_cpu():
|
||||
for dev in HCQCompiled.cpu_devices: dev.synchronize()
|
||||
|
||||
try: self.timeline_signal.wait(self.timeline_value - 1, timeout=timeout if timeout is not None and self.can_recover else None)
|
||||
except RuntimeError as e:
|
||||
self.error_state = e
|
||||
if hasattr(self, 'on_device_hang'): self.on_device_hang()
|
||||
raise e
|
||||
|
||||
if self.timeline_value > (1 << 31): self._wrap_timeline_signal()
|
||||
if PROFILE:
|
||||
Compiled.profile_events += [ProfileRangeEvent(dev, name, st.timestamp, en.timestamp, pk) for st,en,name,dev,pk in self.sig_prof_records]
|
||||
self.sig_prof_records = []
|
||||
|
||||
def next_timeline(self):
|
||||
self.timeline_value += 1
|
||||
return self.timeline_value - 1
|
||||
|
||||
def new_signal(self, **kwargs) -> SignalType:
|
||||
assert self.signal_t is not None, "Device does not support signals"
|
||||
if not HCQCompiled.signal_pool[pg:=self.peer_group]:
|
||||
HCQCompiled.signal_pages[pg].append(alc:=self.allocator.alloc(self.sigalloc_size, BufferSpec(host=True, uncached=True, cpu_access=True))[0][0])
|
||||
HCQCompiled.signal_pool[pg] += [alc.offset(offset=off, size=16) for off in range(0, alc.size, 16)]
|
||||
for dev in HCQCompiled.peer_groups[pg]: cast(HCQAllocator, dev.allocator)._map(alc)
|
||||
return self.signal_t(base_buf=HCQCompiled.signal_pool[pg].pop(), owner=self, **kwargs)
|
||||
|
||||
def device_props(self) -> dict[str,Any]: return {} # to be overridden if needed. dict keys are backend dependent.
|
||||
|
||||
def hw_compute_queues(self) -> list[tuple[str|None, Callable[[], HWQueue]]]:
|
||||
return [(None, self.hw_compute_queue_t)] if self.hw_compute_queue_t is not None else []
|
||||
def hw_copy_queues(self) -> list[tuple[str, Callable[[], HWQueue]]]:
|
||||
return [("SDMA:0", self.hw_copy_queue_t)] if self.hw_copy_queue_t is not None else []
|
||||
|
||||
def _at_profile_finalize(self):
|
||||
self.synchronize() # Expect device to be synchronizes
|
||||
|
||||
def _sync(d:HCQCompiled, q_t:Callable[[], HWQueue]):
|
||||
q_t().timestamp(d.timeline_signal).signal(d.timeline_signal, d.next_timeline()).submit(d)
|
||||
st = time.perf_counter_ns()
|
||||
d.timeline_signal.wait(d.timeline_value - 1) # average of the two
|
||||
et = time.perf_counter_ns()
|
||||
return (decimal.Decimal(et+st) / 2000) - d.timeline_signal.timestamp
|
||||
|
||||
for prefix, q_t in self.hw_compute_queues() + self.hw_copy_queues():
|
||||
devname = f"{self.device}:{prefix}" if prefix else self.device
|
||||
Compiled.profile_events += [ProfileDeviceEvent(devname, statistics.median([_sync(self, q_t) for _ in range(40)]), props=self.device_props())]
|
||||
|
||||
def _wrap_timeline_signal(self):
|
||||
self.timeline_signal, self._shadow_timeline_signal, self.timeline_value = self._shadow_timeline_signal, self.timeline_signal, 1
|
||||
self.timeline_signal.value = 0
|
||||
cast(HCQAllocatorBase, self.allocator).b_timeline = [0] * len(cast(HCQAllocatorBase, self.allocator).b)
|
||||
|
||||
def _realloc(self, oldbuf:HCQBuffer|None, new_size:int, options:BufferSpec|None=None, force=False) -> tuple[HCQBuffer, bool]:
|
||||
if oldbuf is not None: self.allocator.free(((oldbuf, oldbuf.meta), oldbuf.view), oldbuf.size, options=options)
|
||||
try: buf, realloced = self.allocator.alloc(new_size, options=options)[0][0], True
|
||||
except MemoryError:
|
||||
if force: raise
|
||||
buf, realloced = self.allocator.alloc(oldbuf.size if oldbuf is not None else new_size, options=options)[0][0], False
|
||||
return buf, realloced
|
||||
|
||||
def _is_cpu(self) -> bool: return hasattr(self, 'device') and self.device.split(":")[0] == "CPU"
|
||||
|
||||
def rdma_dev(self):
|
||||
from extra.hcq1.ops_rdma import get_rdma_device
|
||||
for i in itertools.count():
|
||||
if (dev:=next((d for d in HCQCompiled.peer_groups[self.peer_group] if type(d).__name__ == 'RDMADevice'), None)): return dev
|
||||
try: get_rdma_device(i)
|
||||
except IndexError: raise RuntimeError(f"No RDMA found for peer group '{self.peer_group}'")
|
||||
|
||||
def finalize(self):
|
||||
try: self.synchronize() # Try to finalize device in any case.
|
||||
except RuntimeError as e: print(f"{self.device} synchronization failed before finalizing: {e}")
|
||||
super().finalize()
|
||||
|
||||
class HCQAllocatorBase(Allocator[HCQDeviceType], Generic[HCQDeviceType]):
|
||||
"""
|
||||
A base allocator class compatible with the HCQ (Hardware Command Queue) API.
|
||||
|
||||
This class implements basic copy operations following the HCQ API, utilizing both types of `HWQueue`.
|
||||
"""
|
||||
|
||||
def __init__(self, dev:HCQDeviceType, batch_size:int=(2 << 20), batch_cnt:int=32, copy_bufs=None, **kwargs):
|
||||
super().__init__(dev, **kwargs)
|
||||
self.b = copy_bufs or [self._alloc(batch_size, BufferSpec(host=True))[0][0] for _ in range(batch_cnt)]
|
||||
self.b_timeline, self.b_next = [0] * len(self.b), 0
|
||||
|
||||
def _map(self, buf:HCQBuffer) -> tuple:
|
||||
if self.dev not in buf.mapped_devs:
|
||||
if buf.owner is None: raise RuntimeError(f"map failed: buffer {buf.va_addr} has no owner, it's a virtual buffer")
|
||||
if not hasattr(self, '_do_map'): raise NotImplementedError("map failed: no method implemented")
|
||||
if (mb:=self._do_map(buf)) is not None: buf.mappings[self.dev] = mb
|
||||
buf.mapped_devs.append(self.dev)
|
||||
mapped = buf.mappings.get(self.dev, buf)
|
||||
return mapped, mapped.meta
|
||||
|
||||
@suppress_finalizing
|
||||
def _free(self, buf:HCQBuffer, options:BufferSpec|None=None):
|
||||
for dev in buf.mapped_devs: dev.synchronize()
|
||||
for d, mb in buf.mappings.items(): d.allocator._do_unmap(mb)
|
||||
if hasattr(self, '_do_free'): self._do_free(buf, options)
|
||||
|
||||
def _do_unmap(self, mb): self.dev.iface.free(mb)
|
||||
|
||||
def _offset(self, buf, size:int, offset:int) -> HCQBuffer: return buf.offset(offset=offset, size=size)
|
||||
|
||||
class HCQAllocator(HCQAllocatorBase, Generic[HCQDeviceType]):
|
||||
def _copyin(self, dest:HCQBuffer, src:memoryview):
|
||||
if self.dev.hw_copy_queue_t is None:
|
||||
self.dev.synchronize()
|
||||
with cpu_profile(f'TINY -> {self.dev.device}', f"{self.dev.device}:COPY"): ctypes.memmove(int(dest.va_addr), from_mv(src), len(src))
|
||||
return
|
||||
|
||||
with hcq_profile(self.dev, queue_type=self.dev.hw_copy_queue_t, desc=TracingKey(f"TINY -> {self.dev.device}", ret=src.nbytes), enabled=PROFILE,
|
||||
dev_suff="SDMA:0"):
|
||||
for i in range(0, src.nbytes, self.b[0].size):
|
||||
self.b_next = (self.b_next + 1) % len(self.b)
|
||||
self.dev.timeline_signal.wait(self.b_timeline[self.b_next])
|
||||
|
||||
lsize = min(self.b[self.b_next].size, src.nbytes - i)
|
||||
self.b[self.b_next].cpu_view().view(size=lsize, fmt='B')[:] = src.cast('B')[i:i+lsize]
|
||||
self.dev.hw_copy_queue_t().wait(self.dev.timeline_signal, self.dev.timeline_value - 1) \
|
||||
.copy(dest.offset(i), self.b[self.b_next], lsize) \
|
||||
.signal(self.dev.timeline_signal, self.dev.next_timeline()).submit(self.dev)
|
||||
self.b_timeline[self.b_next] = self.dev.timeline_value - 1
|
||||
|
||||
def copy_from_disk(self, dest:HCQBuffer, src, size):
|
||||
def _get_temp_buf():
|
||||
# Check if the next buffer is safe to be used (its signal has passed) and reserve it.
|
||||
if self.b_timeline[(self.b_next + 1) % len(self.b)] <= self.dev.timeline_signal.value:
|
||||
self.b_timeline[(self.b_next + 1) % len(self.b)], self.b_next = (1 << 64), (self.b_next + 1) % len(self.b)
|
||||
return (self.b[self.b_next].cpu_view(), self.b_next)
|
||||
return None
|
||||
|
||||
assert self.dev.hw_copy_queue_t is not None
|
||||
with hcq_profile(self.dev, queue_type=self.dev.hw_copy_queue_t, desc=TracingKey(f"DISK -> {self.dev.device}", ret=size), enabled=PROFILE,
|
||||
dev_suff="SDMA:0"):
|
||||
for (batch_info, dst_off, src_off, copy_size) in src.device.allocator._copyout_sharded(src, size, _get_temp_buf, seg_len=self.b[0].size,
|
||||
use_ioring=type(self.b[0].cpu_view()) is MMIOInterface):
|
||||
self.dev.hw_copy_queue_t().wait(self.dev.timeline_signal, self.dev.timeline_value - 1) \
|
||||
.copy(dest.offset(dst_off), self.b[batch_info[1]].offset(src_off), copy_size) \
|
||||
.signal(self.dev.timeline_signal, self.dev.next_timeline()).submit(self.dev)
|
||||
self.b_timeline[batch_info[1]] = self.dev.timeline_value - 1
|
||||
|
||||
def _copyout(self, dest:memoryview, src:HCQBuffer):
|
||||
self.dev.synchronize()
|
||||
if self.dev.hw_copy_queue_t is None:
|
||||
with cpu_profile(f'{self.dev.device} -> TINY', f"{self.dev.device}:COPY"): ctypes.memmove(from_mv(dest), int(src.va_addr), len(dest))
|
||||
return
|
||||
|
||||
with hcq_profile(self.dev, queue_type=self.dev.hw_copy_queue_t, desc=TracingKey(f"{self.dev.device} -> TINY", ret=dest.nbytes), enabled=PROFILE,
|
||||
dev_suff="SDMA:0"):
|
||||
for i in range(0, dest.nbytes, cp_size:=self.b[0].size):
|
||||
self.dev.hw_copy_queue_t().wait(self.dev.timeline_signal, self.dev.timeline_value - 1) \
|
||||
.copy(self.b[0], src.offset(i), lsize:=min(cp_size, dest.nbytes-i)) \
|
||||
.signal(self.dev.timeline_signal, self.dev.next_timeline()).submit(self.dev)
|
||||
self.dev.timeline_signal.wait(self.dev.timeline_value - 1)
|
||||
dest.cast('B')[i:i+lsize] = self.b[0].cpu_view().view(size=lsize, fmt='B')[:]
|
||||
|
||||
def _transfer(self, dest:HCQBuffer, src:HCQBuffer, sz:int, src_dev:HCQDeviceType, dest_dev:HCQDeviceType):
|
||||
if src_dev.peer_group != dest_dev.peer_group: return src_dev.rdma_dev().allocator._transfer(dest, src, sz, src_dev, dest_dev)
|
||||
|
||||
cast(HCQAllocator, src_dev.allocator)._map(dest)
|
||||
|
||||
assert src_dev.hw_copy_queue_t is not None
|
||||
with hcq_profile(src_dev, queue_type=src_dev.hw_copy_queue_t, desc=TracingKey(f"{src_dev.device} -> {dest_dev.device}", ret=sz), enabled=PROFILE,
|
||||
dev_suff="SDMA:0"):
|
||||
src_dev.hw_copy_queue_t().wait(src_dev.timeline_signal, src_dev.timeline_value - 1) \
|
||||
.wait(dest_dev.timeline_signal, dest_dev.timeline_value - 1) \
|
||||
.copy(dest, src, sz) \
|
||||
.signal(src_dev.timeline_signal, src_dev.next_timeline()).submit(src_dev)
|
||||
|
||||
if src_dev != dest_dev:
|
||||
unwrap(dest_dev.hw_compute_queue_t)().wait(src_dev.timeline_signal, src_dev.timeline_value - 1) \
|
||||
.wait(dest_dev.timeline_signal, dest_dev.timeline_value - 1) \
|
||||
.signal(dest_dev.timeline_signal, dest_dev.next_timeline()).submit(dest_dev)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
||||
from __future__ import annotations
|
||||
import mmap, struct, functools
|
||||
import mmap, struct, functools, atexit
|
||||
from typing import cast
|
||||
from tinygrad.uop.ops import sint
|
||||
from tinygrad.runtime.support.hcq import HCQCompiled, HCQAllocatorBase, HCQAllocator, HWQueue, HCQBuffer, FileIOInterface
|
||||
from extra.hcq1.hcq import HCQCompiled, HCQAllocatorBase, HCQAllocator, HWQueue
|
||||
from tinygrad.runtime.support.hcq import HCQBuffer, FileIOInterface
|
||||
from tinygrad.runtime.support.system import System, PCIIfaceBase, PCIAllocationMeta
|
||||
from tinygrad.runtime.support.memory import VirtMapping, AddrSpace
|
||||
from tinygrad.runtime.support.mlx.mlxdev import MLXDev, MLXQP
|
||||
@@ -81,7 +82,7 @@ class RDMAAllocator(HCQAllocatorBase):
|
||||
meta=self.dev.iface.mlx_dev.register_mem(pages, len(pages) * page_sz, page_sz.bit_length() - 1))
|
||||
|
||||
def _do_free(self, buf:HCQBuffer, options): self.dev.iface.mlx_dev.unregister_mem(buf.meta)
|
||||
def _unmap(self, mb): self.dev.iface.mlx_dev.unregister_mem(mb.meta)
|
||||
def _do_unmap(self, mb): self.dev.iface.mlx_dev.unregister_mem(mb.meta)
|
||||
|
||||
def _transfer(self, dest:HCQBuffer, src:HCQBuffer, sz:int, src_dev:HCQCompiled, dest_dev:HCQCompiled):
|
||||
# sync device
|
||||
@@ -101,6 +102,11 @@ class RDMAAllocator(HCQAllocatorBase):
|
||||
|
||||
class RDMADevice(HCQCompiled):
|
||||
def __init__(self, device:str=""):
|
||||
self.device_id = int(device.split(":")[1]) if ":" in device else 0
|
||||
self.iface = MLXIface(self, self.device_id)
|
||||
self.iface = MLXIface(self, int(device.split(":")[1]) if ":" in device else 0)
|
||||
super().__init__(device, RDMAAllocator(self), [], None, signal_t=None)
|
||||
|
||||
@functools.cache
|
||||
def get_rdma_device(index:int) -> RDMADevice:
|
||||
dev = RDMADevice(f"RDMA:{index}")
|
||||
atexit.register(dev.finalize)
|
||||
return dev
|
||||
@@ -0,0 +1,143 @@
|
||||
from __future__ import annotations
|
||||
import os, mmap, array, functools, contextlib, itertools, struct, socket, subprocess, time, enum, atexit
|
||||
from tinygrad.helpers import getenv, temp, ceildiv, unwrap, fetch, system, _ensure_downloads_dir, DEBUG, flatten
|
||||
from tinygrad.runtime.support.hcq import FileIOInterface, MMIOInterface
|
||||
from tinygrad.runtime.support.system import PCIDevice, System
|
||||
|
||||
class RemoteCmd(enum.IntEnum):
|
||||
PROBE,MAP_BAR,MAP_SYSMEM_FD,CFG_READ,CFG_WRITE,RESET,MMIO_READ,MMIO_WRITE,MAP_SYSMEM,SYSMEM_READ,SYSMEM_WRITE,RESIZE_BAR,PING = range(13)
|
||||
|
||||
class RemoteMMIOInterface(MMIOInterface):
|
||||
def __init__(self, dev:RemotePCIDevice, residx:int, nbytes:int, fmt='B', off=0, rd_cmd=RemoteCmd.MMIO_READ, wr_cmd=RemoteCmd.MMIO_WRITE):
|
||||
self.dev, self.residx, self.nbytes, self.fmt, self.off, self.el_sz = dev, residx, nbytes, fmt, off, struct.calcsize(fmt)
|
||||
self.rd_cmd, self.wr_cmd = rd_cmd, wr_cmd
|
||||
|
||||
def __getitem__(self, index):
|
||||
sl = index if isinstance(index, slice) else slice(index, index + 1)
|
||||
start, stop = (sl.start or 0) * self.el_sz, (sl.stop or len(self)) * self.el_sz
|
||||
data = self.dev._bulk_read(self.rd_cmd, self.residx, self.off + start, stop - start)
|
||||
result = data if self.fmt == 'B' else list(struct.unpack(f'<{(stop - start) // self.el_sz}{self.fmt}', data))
|
||||
return result if isinstance(index, slice) else result[0]
|
||||
|
||||
def __setitem__(self, index, val):
|
||||
start = (index.start or 0) * self.el_sz if isinstance(index, slice) else index * self.el_sz
|
||||
data = (val if self.fmt == 'B' else struct.pack(f'<{len(val)}{self.fmt}', *val)) if isinstance(index, slice) else struct.pack(f'<{self.fmt}', val)
|
||||
self.dev._bulk_write(self.wr_cmd, self.residx, self.off + start, data)
|
||||
|
||||
def view(self, offset:int=0, size:int|None=None, fmt=None):
|
||||
return RemoteMMIOInterface(self.dev, self.residx, size or (self.nbytes - offset), fmt or self.fmt, self.off + offset, self.rd_cmd, self.wr_cmd)
|
||||
|
||||
class RemotePCIDevice(PCIDevice):
|
||||
_bulk_sent:int = 0
|
||||
_bulk_recv:int = 0
|
||||
_rpc_count:int = 0
|
||||
_start_time:float = 0.0
|
||||
|
||||
@staticmethod
|
||||
@functools.cache
|
||||
def remote_sock(host:str, port:int) -> socket.socket:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
sock.settimeout(getenv("REMOTE_TIMEOUT", 3))
|
||||
sock.connect((host, port))
|
||||
sock.settimeout(None)
|
||||
if DEBUG >= 1 and RemotePCIDevice._start_time == 0.0:
|
||||
RemotePCIDevice._start_time = time.perf_counter()
|
||||
def _print_stats():
|
||||
dt = time.perf_counter() - RemotePCIDevice._start_time
|
||||
sent_mb, recv_mb = RemotePCIDevice._bulk_sent / 1e6, RemotePCIDevice._bulk_recv / 1e6
|
||||
print(f"remote: sent {sent_mb:,.2f} MB ({sent_mb/dt:,.2f} MB/s), recv {recv_mb:,.2f} MB ({recv_mb/dt:,.2f} MB/s), "
|
||||
f"{RemotePCIDevice._rpc_count:,} roundtrips in {dt:.2f}s")
|
||||
atexit.register(_print_stats)
|
||||
return sock
|
||||
|
||||
@staticmethod
|
||||
@functools.cache
|
||||
def remote_list(vendor:int, devices:tuple[tuple[int, tuple[int, ...]], ...], base_class:int|None) -> list[tuple[socket.socket, str]]:
|
||||
payload = array.array('I', itertools.chain.from_iterable((m, d) for m, ds in devices for d in ds)).tobytes()
|
||||
def q(r:str) -> list[tuple[socket.socket, str]]:
|
||||
sock = RemotePCIDevice.remote_sock((host:=r.strip().split(":")[0]), (port:=int(r.strip().split(":")[1]) if ":" in r else 6667))
|
||||
data_len, _, _, _ = RemotePCIDevice._rpc(sock, 0, RemoteCmd.PROBE, base_class or 0, len(payload), vendor, payload=payload)
|
||||
return [(sock, f"remote:{host}:{port}:{d}") for d in RemotePCIDevice._recvall(sock, data_len).decode().split('\n')]
|
||||
return flatten([q(r) for r in getenv("REMOTE", "").split(",") if r.strip()])
|
||||
|
||||
@staticmethod
|
||||
def _recvall(sock:socket.socket, n:int) -> bytes:
|
||||
data = b''
|
||||
while len(data) < n and (chunk:=sock.recv(n - len(data))): data += chunk
|
||||
if len(data) < n: raise RuntimeError("Connection closed")
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def _rpc(sock:socket.socket, dev_id:int, cmd:int, *args:int, bar:int=0, readout_size:int=0, payload:bytes=b'', has_fd=False):
|
||||
sock.sendall(struct.pack('<BIIQQQ', cmd, dev_id, bar, *(*args, 0, 0, 0)[:3]) + payload)
|
||||
if has_fd:
|
||||
msg, anc, _, _ = sock.recvmsg(17, socket.CMSG_LEN(4))
|
||||
fd = struct.unpack('<i', anc[0][2][:4])[0]
|
||||
else: msg, fd = RemotePCIDevice._recvall(sock, 17), None
|
||||
if (resp:=struct.unpack('<BQQ', msg))[0] != 0:
|
||||
raise RuntimeError(f"RPC failed: {RemotePCIDevice._recvall(sock, resp[1]).decode('utf-8') if resp[1] > 0 else 'unknown error'}")
|
||||
RemotePCIDevice._rpc_count += 1
|
||||
return (resp[1], resp[2]) + ((RemotePCIDevice._recvall(sock, readout_size) if readout_size > 0 else None),) + (fd,)
|
||||
|
||||
def __init__(self, devpref:str, pcibus:str, sock:socket.socket):
|
||||
self.sock, self.pcibus, self.dev_id = sock, pcibus, int(pcibus.split(':')[-1]) if ':' in pcibus else 0
|
||||
self.peer_group = sock.getpeername()[0]
|
||||
for buft in [socket.SO_SNDBUF, socket.SO_RCVBUF]: self.sock.setsockopt(socket.SOL_SOCKET, buft, 64 << 20)
|
||||
|
||||
self.lock_fd = System.flock_acquire(f"{devpref.lower()}_{pcibus.lower()}.lock")
|
||||
|
||||
def _bulk_read(self, cmd:int, idx:int, offset:int, size:int) -> bytes:
|
||||
RemotePCIDevice._bulk_recv += size
|
||||
return unwrap(self._rpc(self.sock, self.dev_id, cmd, offset, size, bar=idx, readout_size=size)[2])
|
||||
def _bulk_write(self, cmd:int, idx:int, offset:int, data:bytes):
|
||||
RemotePCIDevice._bulk_sent += len(data)
|
||||
self.sock.sendall(struct.pack('<BIIQQQ', cmd, self.dev_id, idx, offset, len(data), 0) + data)
|
||||
|
||||
def alloc_sysmem(self, size:int, vaddr:int=0, contiguous:bool=False) -> tuple[MMIOInterface, list[int]]:
|
||||
paddrs_len, handle, _, _ = self._rpc(self.sock, self.dev_id, RemoteCmd.MAP_SYSMEM, size, int(contiguous))
|
||||
paddrs = list(struct.unpack(f'<{paddrs_len // 8}Q', self._recvall(self.sock, paddrs_len)))
|
||||
return RemoteMMIOInterface(self, handle, size, fmt='B', rd_cmd=RemoteCmd.SYSMEM_READ, wr_cmd=RemoteCmd.SYSMEM_WRITE), paddrs
|
||||
|
||||
def reset(self): self._rpc(self.sock, self.dev_id, RemoteCmd.RESET)
|
||||
def read_config(self, offset:int, size:int): return self._rpc(self.sock, self.dev_id, RemoteCmd.CFG_READ, offset, size)[0]
|
||||
def write_config(self, offset:int, value:int, size:int): self._rpc(self.sock, self.dev_id, RemoteCmd.CFG_WRITE, offset, size, value)
|
||||
|
||||
@functools.cache
|
||||
def bar_info(self, bar_idx:int) -> tuple[int, int]: return self._rpc(self.sock, self.dev_id, RemoteCmd.MAP_BAR, bar=bar_idx)[:2]
|
||||
def map_bar(self, bar:int, off:int=0, addr:int=0, size:int|None=None, fmt='B') -> MMIOInterface:
|
||||
return RemoteMMIOInterface(self, bar, size or self.bar_info(bar)[1], fmt).view(off, size, fmt)
|
||||
def resize_bar(self, bar_idx:int): self._rpc(self.sock, self.dev_id, RemoteCmd.RESIZE_BAR, bar=bar_idx)
|
||||
|
||||
class APLRemotePCIDevice(RemotePCIDevice):
|
||||
APP_PATH = "/Applications/TinyGPU.app/Contents/MacOS/TinyGPU"
|
||||
|
||||
@classmethod
|
||||
def ensure_app(cls):
|
||||
commit = "c0d024f9ff0e1dc8fdf217f255da7101d91e8323"
|
||||
app_name = f"TinyGPU_{commit}.zip"
|
||||
if (_ensure_downloads_dir() / app_name).is_file() and os.path.exists(cls.APP_PATH): return
|
||||
print("Downloading TinyGPU.app...")
|
||||
with contextlib.suppress(RuntimeError): system("pkill -f TinyGPU")
|
||||
system(f"ditto -xk {fetch(f'https://github.com/tinygrad/tinygpu_releases/raw/{commit}/TinyGPU.zip', name=app_name)} /Applications")
|
||||
print(system(f"{cls.APP_PATH} install"))
|
||||
|
||||
def __init__(self, devpref:str, pcibus:str):
|
||||
self.ensure_app()
|
||||
sock_path, sock = getenv("APL_REMOTE_SOCK", temp("tinygpu.sock")), socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
for i in range(100):
|
||||
with contextlib.suppress(ConnectionRefusedError, FileNotFoundError):
|
||||
sock.connect(sock_path)
|
||||
break
|
||||
if i == 0: subprocess.Popen([self.APP_PATH, "server", sock_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
time.sleep(0.05)
|
||||
else: raise RuntimeError(f"Failed to connect to TinyGPU server at {sock_path}.")
|
||||
super().__init__(devpref, "usb4", sock=sock)
|
||||
|
||||
def alloc_sysmem(self, size:int, vaddr:int=0, contiguous:bool=False) -> tuple[MMIOInterface, list[int]]:
|
||||
mapped_size, _, _, fd = self._rpc(self.sock, self.dev_id, RemoteCmd.MAP_SYSMEM_FD, size, int(contiguous), has_fd=True)
|
||||
memview = MMIOInterface(FileIOInterface(fd=fd).mmap(0, mapped_size, mmap.PROT_READ | mmap.PROT_WRITE, mmap.MAP_SHARED, 0), mapped_size, fmt='B')
|
||||
|
||||
# paddrs are returned as (paddr, size) pairs until a (paddr=0, size=0) terminator in the beginning of the mapping.
|
||||
paddrs_raw = list(itertools.takewhile(lambda p: p[1] != 0, zip(memview.view(fmt='Q')[0::2], memview.view(fmt='Q')[1::2])))
|
||||
return memview, [p + i for p, sz in paddrs_raw for i in range(0, sz, 0x1000)][:ceildiv(size, 0x1000)]
|
||||
@@ -3,12 +3,14 @@ from tinygrad import Device, Tensor, dtypes
|
||||
from tinygrad.helpers import mv_address, DEBUG, DEV
|
||||
from test.helpers import slow, replace_opts
|
||||
from tinygrad.device import Buffer, BufferSpec
|
||||
from tinygrad.runtime.support.hcq import HCQCompiled, HCQBuffer
|
||||
from extra.hcq1.hcq import HCQCompiled
|
||||
from tinygrad.runtime.support.hcq import HCQBuffer
|
||||
from tinygrad.runtime.autogen import libc
|
||||
from tinygrad.runtime.support.system import PCIIfaceBase
|
||||
from tinygrad.engine.realize import get_runtime
|
||||
from tinygrad.codegen import to_program
|
||||
from tinygrad.codegen.opt import Opt, OptOps
|
||||
from tinygrad.uop.ops import AxisType
|
||||
from tinygrad import Variable
|
||||
|
||||
MOCKGPU = DEV.interface.startswith("MOCK")
|
||||
@@ -167,7 +169,7 @@ class TestHCQ(unittest.TestCase):
|
||||
b = a + 1
|
||||
si = b.schedule_linear().src[-1]
|
||||
|
||||
prg = to_program(replace_opts(si.src[0], [Opt(op=OptOps.LOCAL, axis=0, arg=3) for _ in range(3)]), TestHCQ.d0.renderer)
|
||||
prg = to_program(replace_opts(si.src[0], [Opt(op=OptOps.SPLIT, axis=0, arg=(3, AxisType.LOCAL)) for _ in range(3)]), TestHCQ.d0.renderer)
|
||||
runtime = get_runtime(Device.DEFAULT, prg)
|
||||
|
||||
zb = Buffer(Device.DEFAULT, 3 * 3 * 3, dtypes.int, options=BufferSpec(cpu_access=True, nolru=True)).ensure_allocated()
|
||||
@@ -234,7 +236,7 @@ class TestHCQ(unittest.TestCase):
|
||||
buf2 = Buffer(Device.DEFAULT, sz, dtypes.int8, options=BufferSpec(host=True, nolru=True)).ensure_allocated()
|
||||
|
||||
ctypes.memset(buf2._buf.va_addr, 0x3e, sz)
|
||||
buf2_q_view = buf2._buf.cpu_view().view(fmt='Q')
|
||||
buf2_q_view = buf2.host.view(fmt='Q')
|
||||
for i in range(0, sz//8, 0x1000):
|
||||
for j in range(32): buf2_q_view[min(max(i + j - 16, 0), (sz // 8) - 1)] = random.randint(0, 0xffffffffffffffff)
|
||||
|
||||
@@ -566,7 +568,7 @@ class TestHCQ(unittest.TestCase):
|
||||
|
||||
sz = 0x2000
|
||||
cpu_buffer = Buffer("CPU", sz, dtypes.uint8, options=BufferSpec(cpu_access=True)).ensure_allocated()
|
||||
cpu_buffer._buf.cpu_view().view(fmt='B')[:] = bytes([x & 0xff for x in range(sz)])
|
||||
cpu_buffer.host.view(fmt='B')[:] = bytes([x & 0xff for x in range(sz)])
|
||||
|
||||
for devid in range(6):
|
||||
if DEBUG >= 2: print(f"Testing map to device {Device.DEFAULT}:{devid}")
|
||||
@@ -3,8 +3,8 @@ from tinygrad import Device, Tensor
|
||||
from tinygrad.engine.jit import TinyJit
|
||||
from tinygrad.uop.ops import UOp, Ops
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.runtime.graph.hcq import HCQGraph
|
||||
from tinygrad.runtime.support.hcq import HCQCompiled
|
||||
from extra.hcq1.graph import HCQGraph
|
||||
from extra.hcq1.hcq import HCQCompiled
|
||||
from tinygrad.runtime.support.usb import USBMMIOInterface
|
||||
from test.mockgpu.usb import MockUSB
|
||||
|
||||
@@ -25,10 +25,10 @@ class TestHCQUnit(unittest.TestCase):
|
||||
cpu_call = UOp(Ops.PROGRAM, src=(UOp.sink(),)).call(UOp.new_buffer("CPU", 1, dtypes.float))
|
||||
gpu_devs = [d0]
|
||||
|
||||
# local MMIO: GPU works alone and with CPU in batch (cpu_support=True)
|
||||
# CPU uses HCQ2 and is no longer batched into legacy HCQ graphs.
|
||||
assert HCQGraph.supports_uop(gpu_devs, gpu_call) is True
|
||||
assert HCQGraph.supports_uop(gpu_devs, cpu_call) is True
|
||||
assert HCQGraph.supports_uop(gpu_devs + [cpu_dev], gpu_call) is True
|
||||
assert HCQGraph.supports_uop(gpu_devs, cpu_call) is False
|
||||
assert HCQGraph.supports_uop(gpu_devs + [cpu_dev], gpu_call) is False
|
||||
|
||||
# USB MMIO: GPU-only still works, but CPU batching must be rejected (cpu_support=False)
|
||||
orig_view = d0.timeline_signal.base_buf.view
|
||||
@@ -1,709 +0,0 @@
|
||||
from __future__ import annotations
|
||||
from typing import cast, Any, Callable
|
||||
import os, ctypes, struct, hashlib, functools, importlib, mmap, errno, array, contextlib, sys, weakref, itertools, collections, atexit
|
||||
assert sys.platform != 'win32'
|
||||
from dataclasses import dataclass
|
||||
from tinygrad.runtime.support.hcq2 import HCQ2Compiled, HCQAllocator, HCQ2Buffer, encode_kernargs_clike, make_cmdbuf
|
||||
from tinygrad.runtime.support.hcq2 import make_binary_patch
|
||||
from tinygrad.uop.ops import sint, UOp
|
||||
from tinygrad.device import Compiled, BufferSpec, Buffer, Device
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.helpers import getenv, round_up, data64_le, DEBUG, PROFILE, ProfileEvent, lo32, hi32, colored, prod, ContextVar, TracingKey
|
||||
from tinygrad.helpers import VIZ, ceildiv, unwrap, pluralize, to_tuple
|
||||
from tinygrad.renderer.cstyle import HIPRenderer, HIPCCRenderer
|
||||
from tinygrad.renderer.llvmir import AMDLLVMRenderer
|
||||
from tinygrad.runtime.autogen import kfd, hsa, sqtt, amdgpu_kd, amdgpu_drm
|
||||
from tinygrad.runtime.autogen.am import am
|
||||
from tinygrad.runtime.support.elf import elf_loader
|
||||
from tinygrad.runtime.support.hcq import FileIOInterface, HCQBuffer, MMIOInterface, hcq_filter_visible_devices
|
||||
from tinygrad.runtime.support.am.amdev import AMDev, AMMemoryManager
|
||||
from tinygrad.runtime.support.amd import AMDReg, AMDIP, import_module, import_soc, import_pmc
|
||||
from tinygrad.runtime.support.system import PCIIfaceBase, PCIAllocationMeta, USBPCIDevice, MAP_FIXED, MAP_NORESERVE
|
||||
from tinygrad.runtime.support.usb import USB3
|
||||
from tinygrad.runtime.support.memory import AddrSpace, BumpAllocator
|
||||
from tinygrad.runtime.ops_amd import SQTT, SQTT_ITRACE_SE_MASK, SQTT_LIMIT_SE, SQTT_SIMD_SEL, SQTT_TOKEN_EXCLUDE, PMC
|
||||
from tinygrad.runtime.ops_amd import EVENT_INDEX_PARTIAL_FLUSH, WAIT_REG_MEM_FUNCTION_EQ, WAIT_REG_MEM_FUNCTION_NEQ, WAIT_REG_MEM_FUNCTION_GEQ
|
||||
if getenv("IOCTL"): import extra.hip_gpu_driver.hip_ioctl # noqa: F401 # pylint: disable=unused-import
|
||||
|
||||
from tinygrad.engine.realize import get_runtime, pm_flatten_linear
|
||||
from tinygrad.uop import FastEnum, auto
|
||||
from tinygrad.uop.ops import Ops, UPat, PatternMatcher, graph_rewrite
|
||||
|
||||
# *****************
|
||||
# PM4
|
||||
|
||||
class PM4Ops(FastEnum):
|
||||
SET_SH_REG = auto(); SET_UCONFIG_REG = auto(); WAIT_REG_MEM = auto(); ACQUIRE_MEM = auto() # noqa: E702
|
||||
RELEASE_MEM = auto(); DISPATCH_DIRECT = auto(); EVENT_WRITE = auto() # noqa: E702
|
||||
|
||||
def pkt3(ctx, op:PM4Ops, *vals):
|
||||
return UOp(Ops.INS, arg=op, src=tuple(UOp.const(x, dtypes.uint32)
|
||||
for x in (ctx.pm4.PACKET3(getattr(ctx.pm4, f"PACKET3_{op.name}"), len(vals) - 1), *vals)))
|
||||
|
||||
def wreg(ctx, reg:AMDReg, *args:sint, **kwargs:int):
|
||||
if bool(args) == bool(kwargs): raise RuntimeError('One (and only one) of *args or **kwargs must be specified')
|
||||
if ctx.pm4.PACKET3_SET_SH_REG_START <= reg.addr[0] < ctx.pm4.PACKET3_SET_SH_REG_END:
|
||||
op, set_packet_start = PM4Ops.SET_SH_REG, ctx.pm4.PACKET3_SET_SH_REG_START
|
||||
elif ctx.pm4.PACKET3_SET_UCONFIG_REG_START <= reg.addr[0] < ctx.pm4.PACKET3_SET_UCONFIG_REG_START + 2**16-1:
|
||||
op, set_packet_start = PM4Ops.SET_UCONFIG_REG, ctx.pm4.PACKET3_SET_UCONFIG_REG_START
|
||||
else: raise RuntimeError(f'Cannot set {reg.name} ({reg.addr[0]}) via pm4 packet')
|
||||
return pkt3(ctx, op, reg.addr[0] - set_packet_start, *(args or (reg.encode(**kwargs),)))
|
||||
|
||||
def wait_reg_mem(ctx, value, mask=0xffffffff, mem=None, reg=None, reg_done=0, op=WAIT_REG_MEM_FUNCTION_GEQ):
|
||||
wrm_info_dw = ctx.pm4.WAIT_REG_MEM_MEM_SPACE(int(mem is not None)) | ctx.pm4.WAIT_REG_MEM_OPERATION(int(mem is None and reg_done > 0)) \
|
||||
| ctx.pm4.WAIT_REG_MEM_FUNCTION(op) | ctx.pm4.WAIT_REG_MEM_ENGINE(0)
|
||||
return pkt3(ctx, PM4Ops.WAIT_REG_MEM, wrm_info_dw, *(data64_le(mem) if mem is not None else (reg, reg_done)), value, mask, 4)
|
||||
|
||||
def acquire_mem(ctx, addr=0x0, sz=(1 << 64)-1, gli=1, glm=1, glk=1, glv=1, gl1=1, gl2=1):
|
||||
if ctx.target[0] != 9:
|
||||
cache_flags_dw = ctx.pm4.PACKET3_ACQUIRE_MEM_GCR_CNTL_GLI_INV(gli) \
|
||||
| ctx.pm4.PACKET3_ACQUIRE_MEM_GCR_CNTL_GLM_INV(glm) | ctx.pm4.PACKET3_ACQUIRE_MEM_GCR_CNTL_GLM_WB(glm) \
|
||||
| ctx.pm4.PACKET3_ACQUIRE_MEM_GCR_CNTL_GLK_INV(glk) | ctx.pm4.PACKET3_ACQUIRE_MEM_GCR_CNTL_GLK_WB(glk) \
|
||||
| ctx.pm4.PACKET3_ACQUIRE_MEM_GCR_CNTL_GLV_INV(glv) | ctx.pm4.PACKET3_ACQUIRE_MEM_GCR_CNTL_GL1_INV(gl1) \
|
||||
| ctx.pm4.PACKET3_ACQUIRE_MEM_GCR_CNTL_GL2_INV(gl2) | ctx.pm4.PACKET3_ACQUIRE_MEM_GCR_CNTL_GL2_WB(gl2)
|
||||
return pkt3(ctx, PM4Ops.ACQUIRE_MEM, 0, *data64_le(sz), *data64_le(addr), 0, cache_flags_dw)
|
||||
cp_coher_cntl = ctx.pm4.PACKET3_ACQUIRE_MEM_CP_COHER_CNTL_SH_ICACHE_ACTION_ENA(gli) | \
|
||||
ctx.pm4.PACKET3_ACQUIRE_MEM_CP_COHER_CNTL_SH_KCACHE_ACTION_ENA(glk) | \
|
||||
ctx.pm4.PACKET3_ACQUIRE_MEM_CP_COHER_CNTL_TC_ACTION_ENA(gl2) | \
|
||||
ctx.pm4.PACKET3_ACQUIRE_MEM_CP_COHER_CNTL_TCL1_ACTION_ENA(gl1) | \
|
||||
ctx.pm4.PACKET3_ACQUIRE_MEM_CP_COHER_CNTL_TC_WB_ACTION_ENA(gl2)
|
||||
return pkt3(ctx, PM4Ops.ACQUIRE_MEM, cp_coher_cntl, *data64_le(sz), *data64_le(addr), 0x0000000A)
|
||||
|
||||
def release_mem(ctx, address=0x0, value=0, data_sel=0, int_sel=2, ctxid=0, cache_flush=False):
|
||||
if ctx.target[0] != 9:
|
||||
cache_flags_dw = 0 if not cache_flush else (ctx.pm4.PACKET3_RELEASE_MEM_GCR_GLV_INV | ctx.pm4.PACKET3_RELEASE_MEM_GCR_GL1_INV \
|
||||
| ctx.pm4.PACKET3_RELEASE_MEM_GCR_GL2_INV | ctx.pm4.PACKET3_RELEASE_MEM_GCR_GLM_WB \
|
||||
| ctx.pm4.PACKET3_RELEASE_MEM_GCR_GLM_INV | ctx.pm4.PACKET3_RELEASE_MEM_GCR_GL2_WB | ctx.pm4.PACKET3_RELEASE_MEM_GCR_SEQ)
|
||||
event_dw = ctx.pm4.PACKET3_RELEASE_MEM_EVENT_TYPE(ctx.pm4.CACHE_FLUSH_AND_INV_TS_EVENT) \
|
||||
| ctx.pm4.PACKET3_RELEASE_MEM_EVENT_INDEX(ctx.pm4.event_index__mec_release_mem__end_of_pipe)
|
||||
memsel_dw = ctx.pm4.PACKET3_RELEASE_MEM_DATA_SEL(data_sel) | ctx.pm4.PACKET3_RELEASE_MEM_INT_SEL(int_sel) \
|
||||
| ctx.pm4.PACKET3_RELEASE_MEM_DST_SEL(0)
|
||||
else:
|
||||
cache_flags_dw = 0 if not cache_flush else (ctx.pm4.EOP_TC_WB_ACTION_EN | ctx.pm4.EOP_TC_NC_ACTION_EN)
|
||||
event_dw = ctx.pm4.EVENT_TYPE(ctx.pm4.CACHE_FLUSH_AND_INV_TS_EVENT) | ctx.pm4.EVENT_INDEX(ctx.pm4.event_index__mec_release_mem__end_of_pipe)
|
||||
memsel_dw = ctx.pm4.DATA_SEL(data_sel) | ctx.pm4.INT_SEL(int_sel)
|
||||
ctxid = 0
|
||||
return pkt3(ctx, PM4Ops.RELEASE_MEM, event_dw | cache_flags_dw, memsel_dw, *data64_le(address), *data64_le(value), ctxid)
|
||||
|
||||
def memory_barrier(ctx):
|
||||
pf = '' if ctx.nbio.version[0] == 2 else '0' if ctx.nbio.version[:2] != (7, 11) else '1'
|
||||
return UOp(Ops.LINEAR, dtypes.void, (
|
||||
wait_reg_mem(ctx, reg=getattr(ctx.nbio, f'regBIF_BX_PF{pf}_GPU_HDP_FLUSH_REQ').addr[0],
|
||||
reg_done=getattr(ctx.nbio, f'regBIF_BX_PF{pf}_GPU_HDP_FLUSH_DONE').addr[0], value=0xffffffff),
|
||||
acquire_mem(ctx)))
|
||||
|
||||
def pm4_wait(ctx, dst, val): return wait_reg_mem(ctx, val, mem=dst.getaddr(ctx.devs))
|
||||
|
||||
def pm4_barrier(ctx): return memory_barrier(ctx)
|
||||
|
||||
def pm4_store(ctx, dst, val):
|
||||
if val.op is Ops.BINARY: return None
|
||||
return release_mem(ctx, dst.getaddr(ctx.devs), val, ctx.pm4.data_sel__mec_release_mem__send_32_bit_low,
|
||||
ctx.pm4.int_sel__mec_release_mem__send_interrupt_after_write_confirm, cache_flush=True)
|
||||
|
||||
def pm4_timestamp(ctx, dst):
|
||||
return release_mem(ctx, dst.getaddr(ctx.devs), 0, ctx.pm4.data_sel__mec_release_mem__send_gpu_clock_counter,
|
||||
ctx.pm4.int_sel__mec_release_mem__none)
|
||||
|
||||
def pm4_program(ctx, call, prg):
|
||||
data, info = prg.arg
|
||||
lib_gpu = prg.src[0]
|
||||
args = encode_kernargs_clike(call, prg, ctx.devs)
|
||||
prog_addr = lib_gpu.getaddr(ctx.devs) + data.entry_point_offset
|
||||
scratch_addr = UOp.placeholder((data.private_segment_size,), dtypes.uint8, 0, device=ctx.devs).rtag("scratch").getaddr(ctx.devs)
|
||||
args_addr = args.getaddr(ctx.devs)
|
||||
|
||||
user_regs = []
|
||||
if data.enable_private_segment_sgpr:
|
||||
scratch_hilo = data64_le(scratch_addr)
|
||||
user_regs = [scratch_hilo[0], scratch_hilo[1] | 1 << 31, 0xffffffff, 0x20c14000]
|
||||
if data.enable_dispatch_ptr: user_regs += [*data64_le(args_addr + data.kernargs_segment_size)]
|
||||
user_regs += [*data64_le(args_addr)]
|
||||
|
||||
dispatch_init = ctx.gc.regCOMPUTE_DISPATCH_INITIATOR.encode(
|
||||
**({'cs_w32_en': int(data.wave32)} if ctx.target[0] != 9 else {}), force_start_at_000=1, compute_shader_en=1)
|
||||
ins = [acquire_mem(ctx, gli=0, gl2=0),
|
||||
wreg(ctx, ctx.gc.regCOMPUTE_PGM_LO, *data64_le(prog_addr >> 8)),
|
||||
wreg(ctx, ctx.gc.regCOMPUTE_PGM_RSRC1, data.rsrc1, data.rsrc2),
|
||||
wreg(ctx, ctx.gc.regCOMPUTE_PGM_RSRC3, data.rsrc3),
|
||||
wreg(ctx, ctx.gc.regCOMPUTE_TMPRING_SIZE, ctx.tmpring_size(data.private_segment_size))]
|
||||
ins += [wreg(ctx, ctx.gc.regCOMPUTE_DISPATCH_SCRATCH_BASE_LO, *data64_le((scratch_addr + data.private_segment_size // ctx.xccs * xcc_id) >> 8))
|
||||
for xcc_id in range(ctx.xccs)]
|
||||
ins += [wreg(ctx, ctx.gc.regCOMPUTE_RESTART_X, 0, 0, 0),
|
||||
wreg(ctx, ctx.gc.regCOMPUTE_USER_DATA_0, *user_regs),
|
||||
wreg(ctx, ctx.gc.regCOMPUTE_RESOURCE_LIMITS, ctx.gc.regCOMPUTE_RESOURCE_LIMITS.encode(waves_per_sh=getenv("WAVES_PER_SH"))),
|
||||
wreg(ctx, ctx.gc.regCOMPUTE_START_X, 0, 0, 0, *(info.local_size or (1, 1, 1)), 0, 0),
|
||||
pkt3(ctx, PM4Ops.DISPATCH_DIRECT, *info.global_size, dispatch_init),
|
||||
pkt3(ctx, PM4Ops.EVENT_WRITE, ctx.pm4.EVENT_TYPE(ctx.soc.CS_PARTIAL_FLUSH) | ctx.pm4.EVENT_INDEX(EVENT_INDEX_PARTIAL_FLUSH))]
|
||||
return UOp(Ops.LINEAR, dtypes.void, tuple(ins))
|
||||
|
||||
pm_pm4_opsel = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.PROGRAM, name="prg"),), name="call", allow_any_len=True), pm4_program),
|
||||
|
||||
(UPat(Ops.INS, arg="wait", src=(UPat(name="dst"), UPat(name="val"))), pm4_wait),
|
||||
(UPat(Ops.INS, arg="barrier"), pm4_barrier),
|
||||
(UPat(Ops.INS, arg="timestamp", src=(UPat(name="dst"),)), pm4_timestamp),
|
||||
(UPat(Ops.INS, arg="store", src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val"))), pm4_store),
|
||||
])
|
||||
|
||||
def pm4_submit(ctx, lin):
|
||||
# ensure compute queues are allocated
|
||||
for d in (devs:=ctx.devs): q = Device[d].compute_queue
|
||||
ring, wptr, doorbell, put_ptr = (UOp.placeholder((b.size,), b.dtype, 0, device=devs).rtag(f"COMPUTE:0_{name}")
|
||||
for name, b in (("ring", q.ring), ("write_ptr", q.write_ptr), ("doorbell", q.doorbell), ("put_value", q.put_value)))
|
||||
|
||||
# the host fence at the start of the batch guarantees the ib is free to reuse
|
||||
size_dw = sum(len(ins.src) for ins in lin.src)
|
||||
assert size_dw < (1 << 20), f"indirect buffer of {size_dw} dwords doesn't fit one packet"
|
||||
|
||||
ib = UOp.placeholder((size_dw,), dtypes.uint32, next(UOp.unique_num), device=devs, volatile=True).rtag("cmdbuf")
|
||||
cmdbuf = make_cmdbuf(lin, devs, buf=ib)
|
||||
|
||||
# the ring itself only carries a packet pointing at the ib, wrapping the ring
|
||||
put = put_ptr.index(zero:=UOp.const(0, dtypes.int))
|
||||
pkt = (ctx.pm4.PACKET3(ctx.pm4.PACKET3_INDIRECT_BUFFER, 2), *data64_le(cmdbuf.getaddr(devs)), size_dw | ctx.pm4.INDIRECT_BUFFER_VALID)
|
||||
write_pkt = UOp.barrier(*[ring.index(((put + off) % q.ring.size).cast(dtypes.int)).store(UOp.const(x, dtypes.uint32)) for off,x in enumerate(pkt)])
|
||||
|
||||
# advance the put/write pointers past the packet
|
||||
bump_put_ptr = put_ptr.index(zero).store(put + len(pkt))
|
||||
bump_wptr = wptr.index(zero).store(put + len(pkt))
|
||||
flush = UOp.barrier(write_pkt, bump_put_ptr, bump_wptr)
|
||||
return doorbell.after(flush).index(zero).store(put + len(pkt))
|
||||
|
||||
pm_pm4_submit = PatternMatcher([(UPat(Ops.LINEAR, name="lin"), pm4_submit)])
|
||||
|
||||
# *****************
|
||||
# SDMA
|
||||
|
||||
class SDMAOps(FastEnum): COPY = auto(); POLL_REGMEM = auto(); FENCE = auto(); TRAP = auto(); TIMESTAMP = auto() # noqa: E702
|
||||
|
||||
def sdma_copy(ctx, call):
|
||||
sz = call.src[2].max_numel() * call.src[2].dtype.itemsize
|
||||
src_addr, dst_addr = call.src[2].getaddr(ctx.devs), call.src[1].getaddr(ctx.devs)
|
||||
return call.ins(SDMAOps.COPY, src=tuple(UOp.const(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)))))
|
||||
|
||||
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) \
|
||||
| ctx.sdma.SDMA_PKT_POLL_REGMEM_HEADER_MEM_POLL(1)
|
||||
return ins.ins(SDMAOps.POLL_REGMEM, src=tuple(UOp.const(x, dtypes.uint32) for x in (
|
||||
op, *data64_le(dst.getaddr(ctx.devs)), val, 0xffffffff,
|
||||
ctx.sdma.SDMA_PKT_POLL_REGMEM_DW5_INTERVAL(0x04) | ctx.sdma.SDMA_PKT_POLL_REGMEM_DW5_RETRY_COUNT(0xfff))))
|
||||
|
||||
def sdma_store(ctx, ins, dst, val):
|
||||
op = ctx.sdma.SDMA_OP_FENCE | (ctx.sdma.SDMA_PKT_FENCE_HEADER_MTYPE(3) if ctx.target[0] != 9 else 0)
|
||||
return UOp(Ops.LINEAR, src=(
|
||||
ins.ins(SDMAOps.FENCE, src=tuple(UOp.const(x, dtypes.uint32) for x in (op, *data64_le(dst.getaddr(ctx.devs)), val))),
|
||||
ins.ins(SDMAOps.TRAP, src=tuple(UOp.const(x, dtypes.uint32) for x in (ctx.sdma.SDMA_OP_TRAP, 0)))))
|
||||
|
||||
def sdma_timestamp(ctx, ins, dst):
|
||||
op = ctx.sdma.SDMA_OP_TIMESTAMP | ctx.sdma.SDMA_PKT_TIMESTAMP_GET_HEADER_SUB_OP(ctx.sdma.SDMA_SUBOP_TIMESTAMP_GET_GLOBAL)
|
||||
return ins.ins(SDMAOps.TIMESTAMP, src=tuple(UOp.const(x, dtypes.uint32) for x in (op, *data64_le(dst.getaddr(ctx.devs)))))
|
||||
|
||||
pm_sdma_opsel = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.COPY),), name="call", allow_any_len=True), sdma_copy),
|
||||
|
||||
(UPat(Ops.INS, arg="barrier"), lambda: UOp(Ops.NOOP, dtypes.void, ())),
|
||||
(UPat(Ops.INS, arg="wait", src=(UPat(name="dst"), UPat(name="val")), name="ins"), sdma_wait),
|
||||
(UPat(Ops.INS, arg="timestamp", src=(UPat(name="dst"),), name="ins"), sdma_timestamp),
|
||||
(UPat(Ops.INS, arg="store", src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val")), name="ins"), sdma_store),
|
||||
])
|
||||
|
||||
def sdma_submit(cmdbuf, devs):
|
||||
# the cmdbuf to submit + the patch writes that fill it
|
||||
size_dw, zero = cmdbuf.nbytes() // dtypes.uint32.itemsize, UOp.const(0, dtypes.int)
|
||||
|
||||
# the sdma queue's ring and its host-side ring/write/put pointers
|
||||
for d in devs: q = Device[d].sdma_queue(0)
|
||||
ring, wptr, doorbell, put_ptr = (UOp.placeholder((b.size,), b.dtype, 0, device=devs).rtag(f"COPY:0_{name}")
|
||||
for name, b in (("ring", q.ring), ("write_ptr", q.write_ptr), ("doorbell", q.doorbell), ("put_value", q.put_value)))
|
||||
|
||||
# sdma needs the cmdbuf contiguous: if it won't fit before the ring end, restart at 0 and zero the tail
|
||||
put_b = put_ptr.index(zero)
|
||||
tail_off_dw = ((put_b % (q.ring.size * 4)) // 4).cast(dtypes.int)
|
||||
fits = (size_dw <= q.ring.size - tail_off_dw).cast(dtypes.int)
|
||||
start_dw = fits * tail_off_dw
|
||||
zero_amt_dw = (1 - fits) * (q.ring.size - tail_off_dw)
|
||||
|
||||
# zero the wrapped tail, then copy the cmdbuf into the ring
|
||||
zi = UOp.range(zero_amt_dw, 0, dtype=dtypes.int, src=(cmdbuf,))
|
||||
zero_tail = ring.index(tail_off_dw + zi).store(UOp.const(0, dtypes.uint32)).end(zi)
|
||||
i = UOp.range(UOp.const(size_dw, dtypes.int), 0, dtype=dtypes.int, src=(cmdbuf,))
|
||||
copy_to_ring = ring.index(start_dw + i).store(cmdbuf.index(i).load()).end(i)
|
||||
|
||||
# advance the put/write pointers past the zeroed tail and the cmdbuf
|
||||
next_put_b = put_b + ((zero_amt_dw + size_dw) * 4).cast(put_b.dtype)
|
||||
bump_put_ptr = put_ptr.index(zero).store(next_put_b)
|
||||
bump_wptr = wptr.index(zero).store(next_put_b)
|
||||
|
||||
# ring the doorbell once the writes have landed
|
||||
flush = UOp.barrier(zero_tail, copy_to_ring, bump_put_ptr, bump_wptr)
|
||||
return doorbell.after(flush).index(zero).store(next_put_b)
|
||||
|
||||
pm_sdma_submit = PatternMatcher([(UPat(Ops.LINEAR, name="lin"),
|
||||
lambda ctx, lin: sdma_submit(make_cmdbuf(lin, ctx.devs), ctx.devs))])
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AMDEncodeCtx: # encode-time constants for one queue: devs (every cmdbuf address resolves into these) + gfx version + packet/ip modules
|
||||
devs: tuple[str, ...]; target: tuple[int, ...]; pm4: Any; sdma: Any; soc: Any # noqa: E702
|
||||
gc: AMDIP; nbio: AMDIP; xccs: int; max_copy_size: int; tmpring_size: Callable # noqa: E702
|
||||
|
||||
def encode_queue(q:UOp) -> UOp|None:
|
||||
d = Device[(devs:=to_tuple(q.arg[0]))[0]]
|
||||
ctx = AMDEncodeCtx(devs, d.target, d.pm4, d.sdma, d.soc, d.gc, d.nbio, d.xccs, d.max_copy_size, d.tmpring_size)
|
||||
opsel, submit = (pm_pm4_opsel, pm_pm4_submit) if q.arg[1].startswith("COMPUTE") else (pm_sdma_opsel, pm_sdma_submit)
|
||||
return submit.rewrite(graph_rewrite(q, opsel + pm_flatten_linear, walk=True, ctx=ctx, name=f"{q.arg[1]} opsel"), ctx)
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AMDProgramData:
|
||||
entry_point_offset:int; rsrc1:int; rsrc2:int; rsrc3:int; wave32:bool
|
||||
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]] = {}
|
||||
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:
|
||||
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:
|
||||
assert typ == 5, f"unknown AMD reloc {typ}" # R_AMDGPU_REL64
|
||||
image[off:off+8] = struct.pack('<q', sym - off + addent)
|
||||
desc = amdgpu_kd.llvm_amdhsa_kernel_descriptor_t.from_buffer_copy(bytes(image[rodata:rodata+ctypes.sizeof(amdgpu_kd.llvm_amdhsa_kernel_descriptor_t)]))
|
||||
if (lds:=((desc.group_segment_fixed_size+511)//512)&0x1FF) > (dev.iface.props['lds_size_in_kb']*1024)//512:
|
||||
raise RuntimeError("Too many resources requested: group_segment_size")
|
||||
edp = desc.kernel_code_properties & hsa.AMD_KERNEL_CODE_PROPERTIES_ENABLE_SGPR_DISPATCH_PTR
|
||||
|
||||
data = AMDProgramData(entry_point_offset=rodata + desc.kernel_code_entry_byte_offset,
|
||||
rsrc1=desc.compute_pgm_rsrc1 | ((1<<20) if dev.target[0]==11 else 0), # priv=1 on gfx11 for cwsr
|
||||
rsrc2=desc.compute_pgm_rsrc2 | (lds<<15), rsrc3=desc.compute_pgm_rsrc3,
|
||||
wave32=bool(desc.kernel_code_properties & 0x400), private_segment_size=desc.private_segment_fixed_size, kernargs_segment_size=desc.kernarg_size,
|
||||
kernargs_alloc_size=desc.kernarg_size + (ctypes.sizeof(hsa.hsa_kernel_dispatch_packet_t) if edp else 0), enable_dispatch_ptr=edp,
|
||||
enable_private_segment_sgpr=desc.kernel_code_properties & hsa.AMD_KERNEL_CODE_PROPERTIES_ENABLE_SGPR_PRIVATE_SEGMENT_BUFFER)
|
||||
buf = UOp.placeholder((len(image),), dtypes.uint8, next(UOp.unique_num), device=prg.device).rtag("program")
|
||||
cached = _amd_program_cache[key] = prg.replace(src=(buf.after(make_binary_patch(buf, bytes(image))),), arg=(data, prg.arg))
|
||||
return cached
|
||||
|
||||
class AMDAllocator(HCQAllocator['AMDDevice']):
|
||||
def __init__(self, dev:AMDDevice):
|
||||
super().__init__(dev, supports_copy_from_disk=dev.has_sdma_queue, supports_transfer=dev.has_sdma_queue and not dev.is_usb())
|
||||
|
||||
def _alloc(self, size:int, options:BufferSpec) -> HCQ2Buffer:
|
||||
return self.dev.iface.alloc(size, host=options.host, uncached=options.uncached, cpu_access=options.cpu_access or not self.dev.has_sdma_queue)
|
||||
|
||||
def _do_free(self, opaque, options:BufferSpec): self.dev.iface.free(opaque)
|
||||
|
||||
def _do_map(self, buf:HCQ2Buffer): return self.dev.iface.map(buf._base if buf._base is not None else buf)
|
||||
|
||||
@dataclass
|
||||
class AMDQueueDesc:
|
||||
ring: Buffer; read_ptr: Buffer; write_ptr: Buffer; doorbell: Buffer; put_value: Buffer # noqa: E702
|
||||
eop_buffer: Buffer|None = None; cwsr_buffer: Buffer|None = None; params: tuple|None = None # noqa: E702
|
||||
|
||||
class KFDIface:
|
||||
kfd:FileIOInterface|None = None
|
||||
event_page:HCQBuffer|None = None
|
||||
gpus:list[FileIOInterface] = []
|
||||
count:int = 0
|
||||
|
||||
def _is_usable_gpu(self, gpu_id):
|
||||
with contextlib.suppress(OSError): return int(gpu_id.read()) != 0
|
||||
return False
|
||||
|
||||
def __init__(self, dev, device_id):
|
||||
self.dev = dev
|
||||
|
||||
kfd_topo_path = "/sys/devices/virtual/kfd/kfd/topology/nodes"
|
||||
|
||||
# Initialize KFD interface during first run
|
||||
if KFDIface.kfd is None:
|
||||
KFDIface.kfd = FileIOInterface("/dev/kfd", os.O_RDWR)
|
||||
gpus = [g for g in FileIOInterface(kfd_topo_path).listdir() if self._is_usable_gpu(FileIOInterface(f"{kfd_topo_path}/{g}/gpu_id"))]
|
||||
KFDIface.gpus = hcq_filter_visible_devices(sorted(gpus, key=lambda x: int(x.split('/')[-1])), "AMD")
|
||||
KFDIface.count = len(KFDIface.gpus)
|
||||
|
||||
if device_id >= len(KFDIface.gpus): raise RuntimeError(f"No device found for {device_id}. Requesting more devices than the system has?")
|
||||
|
||||
self.gpu_id = int(FileIOInterface(f"{kfd_topo_path}/{KFDIface.gpus[device_id]}/gpu_id").read())
|
||||
self.props = {(p:=l.split())[0]: int(p[1]) for l in FileIOInterface(f"{kfd_topo_path}/{KFDIface.gpus[device_id]}/properties").read().splitlines()}
|
||||
self.dev_sysfs_path = f"/sys/class/drm/renderD{self.props['drm_render_minor']}/device"
|
||||
ip_base = f"{self.dev_sysfs_path}/ip_discovery/die/0"
|
||||
id2ip = {am.GC_HWID: am.GC_HWIP, am.SDMA0_HWID: am.SDMA0_HWIP, am.NBIF_HWID: am.NBIF_HWIP}
|
||||
ip_hw = [(id2ip[int(hwid)], int(hwid)) for hwid in FileIOInterface(ip_base).listdir() if hwid.isnumeric() and int(hwid) in id2ip]
|
||||
self.ip_versions = {ip:tuple(int(FileIOInterface(f'{ip_base}/{hw}/0/{part}').read()) for part in ['major','minor','revision']) for ip,hw in ip_hw}
|
||||
self.drm_fd = FileIOInterface(f"/dev/dri/renderD{self.props['drm_render_minor']}", os.O_RDWR)
|
||||
|
||||
self.kfd_ver = ((ver_st:=kfd.AMDKFD_IOC_GET_VERSION(KFDIface.kfd)).major_version, ver_st.minor_version)
|
||||
kfd.AMDKFD_IOC_ACQUIRE_VM(KFDIface.kfd, drm_fd=self.drm_fd.fd, gpu_id=self.gpu_id)
|
||||
if self.kfd_ver >= (1,14): kfd.AMDKFD_IOC_RUNTIME_ENABLE(KFDIface.kfd, mode_mask=0)
|
||||
|
||||
# Set these for our device.
|
||||
if KFDIface.event_page is None:
|
||||
KFDIface.event_page = self.alloc(0x8000, uncached=True)
|
||||
kfd.AMDKFD_IOC_CREATE_EVENT(KFDIface.kfd, event_page_offset=KFDIface.event_page.meta.handle)
|
||||
else: self.map(KFDIface.event_page)
|
||||
|
||||
# Event to wait for queues completion
|
||||
self.dev.queue_event = kfd.AMDKFD_IOC_CREATE_EVENT(KFDIface.kfd, event_type=kfd.KFD_IOC_EVENT_SIGNAL, auto_reset=1)
|
||||
self.dev.queue_event_mailbox_ptr = KFDIface.event_page.va_addr + self.dev.queue_event.event_slot_index * 8
|
||||
|
||||
# OS events to collect memory and hardware faults
|
||||
self.mem_fault_event = kfd.AMDKFD_IOC_CREATE_EVENT(KFDIface.kfd, event_type=kfd.KFD_IOC_EVENT_MEMORY)
|
||||
self.hw_fault_event = kfd.AMDKFD_IOC_CREATE_EVENT(KFDIface.kfd, event_type=kfd.KFD_IOC_EVENT_HW_EXCEPTION)
|
||||
|
||||
self.queue_event_arr = (kfd.struct_kfd_event_data * 3)(kfd.struct_kfd_event_data(event_id=self.dev.queue_event.event_id),
|
||||
kfd.struct_kfd_event_data(event_id=self.mem_fault_event.event_id), kfd.struct_kfd_event_data(event_id=self.hw_fault_event.event_id))
|
||||
self.queue_event_arr_ptr = ctypes.addressof(self.queue_event_arr)
|
||||
|
||||
def alloc(self, size:int, host=False, uncached=False, cpu_access=False, contiguous=False, cpu_addr=None) -> HCQBuffer:
|
||||
flags = kfd.KFD_IOC_ALLOC_MEM_FLAGS_WRITABLE | kfd.KFD_IOC_ALLOC_MEM_FLAGS_EXECUTABLE | kfd.KFD_IOC_ALLOC_MEM_FLAGS_NO_SUBSTITUTE
|
||||
|
||||
if uncached: flags |= kfd.KFD_IOC_ALLOC_MEM_FLAGS_COHERENT | kfd.KFD_IOC_ALLOC_MEM_FLAGS_UNCACHED | kfd.KFD_IOC_ALLOC_MEM_FLAGS_GTT
|
||||
else: flags |= (kfd.KFD_IOC_ALLOC_MEM_FLAGS_USERPTR if host else kfd.KFD_IOC_ALLOC_MEM_FLAGS_VRAM)
|
||||
|
||||
# Make mapped cpu address to be uncachable
|
||||
if cpu_addr is not None: flags |= kfd.KFD_IOC_ALLOC_MEM_FLAGS_COHERENT | kfd.KFD_IOC_ALLOC_MEM_FLAGS_UNCACHED
|
||||
|
||||
if cpu_access or host: flags |= kfd.KFD_IOC_ALLOC_MEM_FLAGS_PUBLIC
|
||||
|
||||
if flags & kfd.KFD_IOC_ALLOC_MEM_FLAGS_USERPTR:
|
||||
buf = addr = cpu_addr or FileIOInterface.anon_mmap(0, size, mmap.PROT_READ | mmap.PROT_WRITE, mmap.MAP_SHARED | mmap.MAP_ANONYMOUS, 0)
|
||||
else: buf, addr = 0, FileIOInterface.anon_mmap(0, size, 0, mmap.MAP_PRIVATE | mmap.MAP_ANONYMOUS | MAP_NORESERVE, 0)
|
||||
|
||||
try: mem = kfd.AMDKFD_IOC_ALLOC_MEMORY_OF_GPU(self.kfd, va_addr=addr, size=size, gpu_id=self.gpu_id, flags=flags, mmap_offset=buf)
|
||||
except OSError as e:
|
||||
if e.errno == errno.EINVAL and (flags & kfd.KFD_IOC_ALLOC_MEM_FLAGS_VRAM) and cpu_access:
|
||||
raise MemoryError("Cannot allocate host-visible VRAM. Ensure the resizable BAR option is enabled on your system.") from e
|
||||
if e.errno == errno.ENOMEM: raise MemoryError(f"Cannot allocate {size} bytes: no memory is available.") from e
|
||||
raise
|
||||
|
||||
if not (flags & kfd.KFD_IOC_ALLOC_MEM_FLAGS_USERPTR):
|
||||
buf = self.drm_fd.mmap(mem.va_addr, mem.size, mmap.PROT_READ | mmap.PROT_WRITE, mmap.MAP_SHARED | MAP_FIXED, mem.mmap_offset)
|
||||
assert addr == buf == mem.va_addr
|
||||
|
||||
view = MMIOInterface(mem.va_addr, mem.size, fmt='B') if cpu_access or host else None
|
||||
self.map(hcqbuf:=HCQBuffer(mem.va_addr, mem.size, meta=mem, view=view, owner=self.dev))
|
||||
return hcqbuf
|
||||
|
||||
def free(self, mem):
|
||||
gpus = (ctypes.c_int32 * 1)(self.gpu_id)
|
||||
stm = kfd.AMDKFD_IOC_UNMAP_MEMORY_FROM_GPU(self.kfd, handle=mem.meta.handle, device_ids_array_ptr=ctypes.addressof(gpus), n_devices=1)
|
||||
assert stm.n_success == 1
|
||||
if mem.owner == self.dev:
|
||||
if mem.va_addr: FileIOInterface.munmap(mem.va_addr, mem.size)
|
||||
kfd.AMDKFD_IOC_FREE_MEMORY_OF_GPU(self.kfd, handle=mem.meta.handle)
|
||||
|
||||
def map(self, mem):
|
||||
if mem.owner is not None and mem.owner._is_cpu(): return self.alloc(mem.size, host=True, cpu_addr=mem.va_addr)
|
||||
|
||||
c_gpus = (ctypes.c_int32 * 1)(self.gpu_id)
|
||||
stm = kfd.AMDKFD_IOC_MAP_MEMORY_TO_GPU(self.kfd, handle=mem.meta.handle, device_ids_array_ptr=ctypes.addressof(c_gpus), n_devices=1)
|
||||
assert stm.n_success == 1
|
||||
return HCQBuffer(mem.va_addr, mem.size, meta=mem.meta, owner=mem.owner)
|
||||
|
||||
def create_queue(self, queue_type, ring, gart, rptr, wptr, eop_buffer=None, cwsr_buffer=None, ctl_stack_size=0, ctx_save_restore_size=0,
|
||||
xcc_id=0, idx=0):
|
||||
queue = kfd.AMDKFD_IOC_CREATE_QUEUE(KFDIface.kfd, ring_base_address=ring._buf.va_addr, ring_size=ring._buf.size, gpu_id=self.gpu_id,
|
||||
queue_type=queue_type, queue_percentage=kfd.KFD_MAX_QUEUE_PERCENTAGE|(xcc_id<<8), queue_priority=getenv("AMD_KFD_QUEUE_PRIORITY", 7),
|
||||
eop_buffer_address=eop_buffer._buf.va_addr if eop_buffer else 0, eop_buffer_size=eop_buffer._buf.size if eop_buffer else 0,
|
||||
ctl_stack_size=ctl_stack_size, ctx_save_restore_address=cwsr_buffer._buf.va_addr if cwsr_buffer else 0, ctx_save_restore_size=ctx_save_restore_size,
|
||||
write_pointer_address=gart._buf.va_addr+wptr, read_pointer_address=gart._buf.va_addr+rptr+8*xcc_id)
|
||||
|
||||
if not hasattr(self, 'doorbells'):
|
||||
self.doorbells_base = queue.doorbell_offset & (~0x1fff) # doorbell is two pages
|
||||
self.doorbells = cast(FileIOInterface, KFDIface.kfd).mmap(0, 0x2000, mmap.PROT_READ|mmap.PROT_WRITE, mmap.MAP_SHARED, self.doorbells_base)
|
||||
|
||||
(put_value := Buffer("CPU", 1, dtypes.uint64, preallocate=True))._buf.view.view(fmt='Q')[0] = 0
|
||||
doorbell = Buffer("CPU", 1, dtypes.uint64,
|
||||
options=BufferSpec(external_ptr=self.doorbells + queue.doorbell_offset - self.doorbells_base), preallocate=True)
|
||||
return AMDQueueDesc(ring=ring, doorbell=doorbell, read_ptr=gart.view(1, dtypes.uint64, rptr+8*xcc_id).ensure_allocated(),
|
||||
write_ptr=gart.view(1, dtypes.uint64, wptr).ensure_allocated(), put_value=put_value, eop_buffer=eop_buffer, cwsr_buffer=cwsr_buffer)
|
||||
|
||||
def sleep(self, tm:int):
|
||||
kfd.AMDKFD_IOC_WAIT_EVENTS(KFDIface.kfd, events_ptr=self.queue_event_arr_ptr, num_events=3, wait_for_all=0, timeout=tm)
|
||||
if self.queue_event_arr[1].memory_exception_data.gpu_id or self.queue_event_arr[2].hw_exception_data.gpu_id: self.on_device_hang()
|
||||
|
||||
def on_device_hang(self):
|
||||
def _str(st): return ' '.join(f'{k[0]}={getattr(st, k[0])}' for k in st._real_fields_)
|
||||
|
||||
# try to collect fault info if not already set from sleep().
|
||||
if not self.queue_event_arr[1].memory_exception_data.gpu_id and not self.queue_event_arr[2].hw_exception_data.gpu_id:
|
||||
with contextlib.suppress(RuntimeError): self.sleep(tm=1)
|
||||
|
||||
report = []
|
||||
if self.queue_event_arr[1].memory_exception_data.gpu_id:
|
||||
report += [f"MMU fault: 0x{self.queue_event_arr[1].memory_exception_data.va:X} | {_str(self.queue_event_arr[1].memory_exception_data.failure)}"]
|
||||
if self.queue_event_arr[2].hw_exception_data.gpu_id: report += [f"HW fault: {_str(self.queue_event_arr[2].hw_exception_data)}"]
|
||||
|
||||
raise RuntimeError("\n".join(report))
|
||||
|
||||
def require_profile_mode(self, can_set_mode=True):
|
||||
if self.dev.target[0] == 9: return
|
||||
fn = f'{self.dev_sysfs_path}/power_dpm_force_performance_level'
|
||||
if (perflevel:=FileIOInterface(fn).read().strip()) != 'profile_standard':
|
||||
if can_set_mode:
|
||||
atexit.register(lambda: os.system(f"echo '{perflevel}' | sudo tee {fn} > /dev/null"))
|
||||
os.system(f"echo 'profile_standard' | sudo tee {fn} > /dev/null")
|
||||
self.require_profile_mode(can_set_mode=False)
|
||||
else:
|
||||
raise RuntimeError("PMC/SQTT requires stable power state: run `amd-smi set -l stable_std` for KFD iface")
|
||||
|
||||
@functools.cached_property
|
||||
def drm_dev_info(self) -> amdgpu_drm.struct_drm_amdgpu_info_device:
|
||||
amdgpu_drm.DRM_IOCTL_AMDGPU_INFO(self.drm_fd, query=amdgpu_drm.AMDGPU_INFO_DEV_INFO,
|
||||
return_pointer=ctypes.addressof(inf:=amdgpu_drm.struct_drm_amdgpu_info_device()), return_size=ctypes.sizeof(inf))
|
||||
return inf
|
||||
def is_wgp_active(self, xcc, se, sa, wgp) -> bool: return ((self.drm_dev_info.cu_bitmap[se % 4][sa + (se // 4) * 2] >> (2 * wgp)) & 0x3) == 0x3
|
||||
|
||||
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,
|
||||
va_start=AMMemoryManager.va_allocator.base, va_size=AMMemoryManager.va_allocator.size, dev_impl_t=AMDev)
|
||||
self._compute_props()
|
||||
|
||||
def p2p_paddrs(self, paddrs:list[tuple[int,int]]) -> tuple[list[tuple[int,int]], AddrSpace]:
|
||||
return ([(self.dev_impl.paddr2xgmi(p), sz) for p, sz in paddrs], AddrSpace.PEER) if self.dev_impl.is_hive() else super().p2p_paddrs(paddrs)
|
||||
|
||||
def require_profile_mode(self): return True
|
||||
def is_wgp_active(self, xcc, se, sa, wgp) -> bool: return True # TODO: account for WGP disablement on some asics.
|
||||
|
||||
def _compute_props(self):
|
||||
self.ip_versions = self.dev_impl.ip_ver
|
||||
|
||||
gfxver = int(f"{self.dev_impl.ip_ver[am.GC_HWIP][0]:02d}{self.dev_impl.ip_ver[am.GC_HWIP][1]:02d}{self.dev_impl.ip_ver[am.GC_HWIP][2]:02d}")
|
||||
if self.dev_impl.gc_info.header.version_major == 2:
|
||||
cu_per_sa = self.dev_impl.gc_info.gc_num_cu_per_sh
|
||||
max_sh_per_se = self.dev_impl.gc_info.gc_num_sh_per_se
|
||||
else:
|
||||
cu_per_sa = 2 * (self.dev_impl.gc_info.gc_num_wgp0_per_sa + self.dev_impl.gc_info.gc_num_wgp1_per_sa)
|
||||
max_sh_per_se = self.dev_impl.gc_info.gc_num_sa_per_se
|
||||
|
||||
array_count = max_sh_per_se * self.dev_impl.gc_info.gc_num_se * self.dev_impl.gfx.xccs
|
||||
self.props = {'cu_per_simd_array': cu_per_sa, 'simd_count': 2 * cu_per_sa * array_count, 'simd_per_cu': 2, 'array_count': array_count,
|
||||
'max_slots_scratch_cu': self.dev_impl.gc_info.gc_max_scratch_slots_per_cu, 'max_waves_per_simd': self.dev_impl.gc_info.gc_max_waves_per_simd,
|
||||
'simd_arrays_per_engine': max_sh_per_se, 'lds_size_in_kb': self.dev_impl.gc_info.gc_lds_size, 'num_xcc': self.dev_impl.gfx.xccs,
|
||||
'gfx_target_version': {90403: 90402}.get(gfxver, gfxver)}
|
||||
|
||||
def create_queue(self, queue_type, ring, gart, rptr, wptr, eop_buffer=None, cwsr_buffer=None, ctl_stack_size=0, ctx_save_restore_size=0,
|
||||
xcc_id=0, idx=0):
|
||||
assert cwsr_buffer is None, "no cwsr buffer for am"
|
||||
|
||||
rcvr_params: tuple
|
||||
if queue_type == kfd.KFD_IOC_QUEUE_TYPE_SDMA:
|
||||
doorbell_index = self.dev_impl.sdma.setup_ring(*(rcvr_params:=(ring._buf.va_addr, ring._buf.size, gart._buf.va_addr+rptr,
|
||||
gart._buf.va_addr+wptr, idx)))
|
||||
else:
|
||||
doorbell_index = self.dev_impl.gfx.setup_ring(*(rcvr_params:=(ring._buf.va_addr, ring._buf.size, gart._buf.va_addr+rptr,
|
||||
gart._buf.va_addr+wptr, eop_buffer._buf.va_addr, eop_buffer._buf.size, is_aql:=(queue_type==kfd.KFD_IOC_QUEUE_TYPE_COMPUTE_AQL), is_aql)))
|
||||
|
||||
(put_value := Buffer("CPU", 1, dtypes.uint64, preallocate=True))._buf.view.view(fmt='Q')[0] = 0
|
||||
doorbell = Buffer("CPU", 1, dtypes.uint64, options=BufferSpec(external_ptr=self.dev_impl.doorbell64.addr + doorbell_index*8), preallocate=True)
|
||||
return AMDQueueDesc(ring=ring, doorbell=doorbell, read_ptr=gart.view(1, dtypes.uint64, rptr).ensure_allocated(),
|
||||
write_ptr=gart.view(1, dtypes.uint64, wptr).ensure_allocated(), put_value=put_value, eop_buffer=eop_buffer, params=rcvr_params)
|
||||
|
||||
def _collect_interrupts(self, reset=False, drain_only=False):
|
||||
d = self.dev
|
||||
if drain_only: d.iface.dev_impl.ih.drain()
|
||||
else: d.iface.dev_impl.ih.interrupt_handler()
|
||||
|
||||
if reset and d.iface.dev_impl.recover(force=True):
|
||||
cq = d.compute_queue
|
||||
for b in (cq.put_value, cq.read_ptr, cq.write_ptr): b._buf.view.view(fmt='Q')[0] = 0
|
||||
d.iface.dev_impl.gfx.setup_ring(*cq.params)
|
||||
d.signal('timeline')._buf.cpu_view().mv.cast('Q')[0] = \
|
||||
d.signal('value', 1).as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')[0] - 1
|
||||
|
||||
def sleep(self, timeout):
|
||||
if hasattr(self.pci_dev, 'irq_poller') and self.pci_dev.irq_poller is not None and (events_cnt:=len(self.pci_dev.irq_poller.poll(timeout))):
|
||||
self.pci_dev.irq_fd.read(8 * events_cnt)
|
||||
self._collect_interrupts()
|
||||
if self.dev_impl.is_err_state: raise RuntimeError("Device is in error state")
|
||||
|
||||
def on_device_hang(self):
|
||||
self._collect_interrupts(reset=True)
|
||||
raise RuntimeError("Device hang detected")
|
||||
|
||||
def device_fini(self): self.dev_impl.fini()
|
||||
|
||||
def _mock(iface, name=None): return type(name or f"MOCK{iface.__name__}", (iface,), {})
|
||||
|
||||
class AMDDevice(HCQ2Compiled):
|
||||
pm_lower = PatternMatcher([
|
||||
# prep program
|
||||
(UPat(Ops.PROGRAM, src=(UPat(), UPat(), UPat(), UPat(Ops.BINARY)), name="prg"), amd_build_program),
|
||||
|
||||
# encoding of cmdbuf
|
||||
(UPat(Ops.CUSTOM_FUNCTION, arg="submit_cmdbuf", src=(UPat(Ops.LINEAR, name="q"),)), encode_queue),
|
||||
])
|
||||
|
||||
timestamp_divider = 100.0 # AMD GPU clock: ticks/us
|
||||
max_scratch_psize = 0
|
||||
|
||||
ifaces = [KFDIface, PCIIface, _mock(KFDIface, "MOCKIface"), _mock(KFDIface), _mock(PCIIface)]
|
||||
|
||||
def device_props(self): return self.iface.props
|
||||
|
||||
def is_am(self) -> bool: return isinstance(self.iface, (PCIIface,))
|
||||
def is_usb(self) -> bool: return False
|
||||
|
||||
def __init__(self, device:str=""):
|
||||
self.device_id = int(device.split(":")[1]) if ":" in device else 0
|
||||
|
||||
self.iface = self._select_iface()
|
||||
|
||||
self.target:tuple[int, ...] = ((trgt:=self.iface.props['gfx_target_version']) // 10000, (trgt // 100) % 100, trgt % 100)
|
||||
self.arch = "gfx%d%x%x" % self.target
|
||||
assert (self.target in ((9,4,2),(9,5,0))) or self.target[0] in (11, 12), f"Unsupported arch: {self.arch}"
|
||||
if DEBUG >= 1: print(f"AMDDevice: opening {self.device_id} with target {self.target} arch {self.arch}")
|
||||
|
||||
self.xccs = self.iface.props.get('num_xcc', 1)
|
||||
self.se_cnt = self.iface.props['array_count'] // self.iface.props['simd_arrays_per_engine'] // self.xccs
|
||||
self.cu_cnt = self.iface.props['simd_count'] // self.iface.props['simd_per_cu'] // self.xccs
|
||||
self.waves_per_cu = self.iface.props['max_waves_per_simd'] * self.iface.props['simd_per_cu']
|
||||
self.wave_cnt = (self.cu_cnt * self.waves_per_cu) if self.target[0] != 9 else min(self.cu_cnt * 40, self.se_cnt * self.xccs * 512)
|
||||
|
||||
self.ip_off = importlib.import_module(f"tinygrad.runtime.autogen.am.{'vega' if self.target[0] == 9 else 'navi'}_offsets")
|
||||
self.soc = import_soc(self.target)
|
||||
self.pm4 = importlib.import_module(f"tinygrad.runtime.autogen.am.pm4_{'soc15' if self.target[0] == 9 else 'nv'}")
|
||||
self.sdma = import_module('sdma', min(self.iface.ip_versions[am.SDMA0_HWIP], (6, 0, 0)))
|
||||
self.gc = AMDIP('gc', self.iface.ip_versions[am.GC_HWIP],
|
||||
bases={i: tuple(getattr(self.ip_off, f'GC_BASE__INST{i}_SEG{s}', 0) for s in range(6)) for i in range(6)})
|
||||
|
||||
self.nbio = AMDIP('nbio' if self.target[0] < 12 else 'nbif', self.iface.ip_versions[am.NBIF_HWIP],
|
||||
bases={i: tuple(getattr(self.ip_off, f'NBIO_BASE__INST{i}_SEG{s}', 0) for s in range(9)) for i in range(6)})
|
||||
|
||||
self.is_aql = getenv("AMD_AQL", int(self.xccs > 1))
|
||||
if self.is_aql:
|
||||
self.pm4_ibs = self.iface.alloc(0x2000 if self.is_usb() else (16 << 20), uncached=True, cpu_access=True)
|
||||
self.pm4_ib_alloc = BumpAllocator(self.pm4_ibs.size, wrap=True)
|
||||
|
||||
self.max_copy_size = 0x40000000 if self.iface.ip_versions[am.SDMA0_HWIP][0] >= 5 else 0x400000
|
||||
self.sdma_queues:dict = {}
|
||||
self.has_sdma_queue = True # self.sdma_queue(0) is not None, TODO: think of this
|
||||
|
||||
super().__init__(device, AMDAllocator(self), [HIPRenderer, AMDLLVMRenderer, HIPCCRenderer], None, can_recover=self.is_am(), arch=self.arch)
|
||||
|
||||
# Scratch setup
|
||||
self.max_private_segment_size = 0
|
||||
self.pm_bufferize = PatternMatcher([(UPat(Ops.PARAM, tag="scratch", name="b"), lambda ctx, b: ctx[0].scratch_buffer(b.max_numel()))]) + self.pm_bufferize
|
||||
|
||||
self.pmc_enabled:bool = PROFILE > 0 and PMC > 0
|
||||
if self.pmc_enabled:
|
||||
self.iface.require_profile_mode()
|
||||
|
||||
self.pmc_sched:list[PMCSample] = []
|
||||
self.pmc_counters = import_pmc(self.target)
|
||||
|
||||
# validate counters: SQ for SIMD busy/instruction counts, LDS stats, GRBM for GPU cycles, L2 cache hits/misses
|
||||
l2, lds = ("TCC", "SQ") if self.target[0] == 9 else ("GL2C", "SQC")
|
||||
pmc_default = f"SQ_BUSY_CYCLES,SQ_INSTS_VALU,SQ_INSTS_SALU,{lds}_LDS_IDX_ACTIVE,{lds}_LDS_BANK_CONFLICT,GRBM_GUI_ACTIVE,{l2}_HIT,{l2}_MISS"
|
||||
for k in (PMC_COUNTERS:=getenv("PMC_COUNTERS", pmc_default).split(",")):
|
||||
if k not in self.pmc_counters: raise RuntimeError(f"PMC counter {k} is not supported. Available: {','.join(self.pmc_counters.keys())}")
|
||||
|
||||
raise NotImplementedError("PMC start not migrated to hcq2 yet")
|
||||
|
||||
# SQTT is disabled by default because of runtime overhead and big file sizes (~200mb to Tensor.full() two 4096x4096 tensors and matmul them)
|
||||
self.sqtt_enabled:bool = PROFILE > 0 and SQTT > 0
|
||||
if self.sqtt_enabled:
|
||||
self.iface.require_profile_mode()
|
||||
|
||||
SQTT_BUFFER_SIZE = getenv("SQTT_BUFFER_SIZE", 256) # in mb, per shader engine
|
||||
self.sqtt_buffers = [self.allocator.alloc(SQTT_BUFFER_SIZE<<20, BufferSpec(nolru=True, uncached=True)) for _ in range(self.se_cnt * self.xccs)]
|
||||
self.sqtt_wptrs = self.allocator.alloc(round_up(self.se_cnt * self.xccs * 4, 0x1000), BufferSpec(cpu_access=True, nolru=True))
|
||||
self.sqtt_next_cmd_id = itertools.count(0)
|
||||
|
||||
def create_queue(self, queue_type, ring_size, ctx_save_restore_size=0, eop_buffer_size=0, ctl_stack_size=0, debug_memory_size=0, idx=0):
|
||||
ring = Buffer(self.device, ring_size // 4, dtypes.uint32, options=BufferSpec(uncached=True, cpu_access=True), preallocate=True)
|
||||
gart = Buffer(self.device, 0x100, dtypes.uint8, options=BufferSpec(uncached=True, cpu_access=True), preallocate=True)
|
||||
|
||||
if queue_type == kfd.KFD_IOC_QUEUE_TYPE_COMPUTE_AQL:
|
||||
self.aql_gart = gart
|
||||
self.aql_desc = hsa.amd_queue_t(queue_properties=hsa.AMD_QUEUE_PROPERTIES_IS_PTR64 | hsa.AMD_QUEUE_PROPERTIES_ENABLE_PROFILING,
|
||||
read_dispatch_id_field_base_byte_offset=getattr(hsa.amd_queue_t, 'read_dispatch_id').offset,
|
||||
max_cu_id=(self.cu_cnt * self.xccs) - 1, max_wave_id=self.waves_per_cu - 1)
|
||||
self.aql_gart._buf.cpu_view().view(fmt='B')[:ctypes.sizeof(self.aql_desc)] = bytes(self.aql_desc)
|
||||
|
||||
cwsr_buffer_size = round_up((ctx_save_restore_size + debug_memory_size) * self.xccs, mmap.PAGESIZE)
|
||||
cwsr_buffer = Buffer(self.device, cwsr_buffer_size, dtypes.uint8, preallocate=True) if ctx_save_restore_size else None
|
||||
eop_buffer = Buffer(self.device, eop_buffer_size, dtypes.uint8, preallocate=True) if eop_buffer_size else None
|
||||
|
||||
queue = (self.iface.create_queue(queue_type, ring, gart, rptr=getattr(hsa.amd_queue_t, 'read_dispatch_id').offset,
|
||||
wptr=getattr(hsa.amd_queue_t, 'write_dispatch_id').offset, eop_buffer=eop_buffer, cwsr_buffer=cwsr_buffer,
|
||||
ctx_save_restore_size=ctx_save_restore_size, ctl_stack_size=ctl_stack_size, idx=idx))
|
||||
|
||||
qname = f"{'COPY' if queue_type == kfd.KFD_IOC_QUEUE_TYPE_SDMA else 'COMPUTE'}:{idx}"
|
||||
self.pm_bufferize = PatternMatcher([
|
||||
(UPat(Ops.PARAM, tag=f"{qname}_{name}"), lambda ctx, b=getattr(queue, name): b) for name in ["ring", "write_ptr", "doorbell", "put_value"]
|
||||
]) + self.pm_bufferize
|
||||
|
||||
return queue
|
||||
|
||||
@functools.cached_property
|
||||
def compute_queue(self) -> AMDQueueDesc:
|
||||
# https://gitlab.freedesktop.org/agd5f/linux/-/blob/a1fc9f584c4aaf8bc1ebfa459fc57a3f26a290d8/drivers/gpu/drm/amd/amdkfd/kfd_queue.c#L391
|
||||
sgrp_size_per_cu, hwreg_size_per_cu = 0x4000, 0x1000
|
||||
lds_size_per_cu = self.iface.props["lds_size_in_kb"] << 10 if self.target[:2] == (9,5) else 0x10000
|
||||
vgpr_size_per_cu = 0x60000 if self.target in {(11,0,0), (11,0,1), (11,5,1), (12,0,0), (12,0,1)} else 0x80000 if self.target[0] == 9 else 0x40000
|
||||
wg_data_size = round_up((vgpr_size_per_cu + sgrp_size_per_cu + lds_size_per_cu + hwreg_size_per_cu) * self.cu_cnt, mmap.PAGESIZE)
|
||||
ctl_stack_size = round_up((12 if self.target[0] != 9 else 8) * self.wave_cnt + 8 + 40, mmap.PAGESIZE)
|
||||
return self.create_queue(kfd.KFD_IOC_QUEUE_TYPE_COMPUTE_AQL if self.is_aql else kfd.KFD_IOC_QUEUE_TYPE_COMPUTE,
|
||||
0x2000 if self.is_usb() else (16 << 20), eop_buffer_size=0x1000,
|
||||
ctx_save_restore_size=0 if self.is_am() else wg_data_size + ctl_stack_size, ctl_stack_size=ctl_stack_size,
|
||||
debug_memory_size=round_up(self.wave_cnt * 32, 64))
|
||||
|
||||
def sdma_queue(self, idx:int):
|
||||
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)
|
||||
return self.sdma_queues.get(idx, None)
|
||||
|
||||
def tmpring_size(self, private_segment_size):
|
||||
private_segment_size = max(private_segment_size, 128)
|
||||
|
||||
lanes_per_wave = 64 # wave64
|
||||
mem_alignment_size = 256 if self.target[0] != 9 else 1024
|
||||
size_per_thread = round_up(private_segment_size, mem_alignment_size // lanes_per_wave)
|
||||
size_per_xcc = size_per_thread * lanes_per_wave * self.iface.props['max_slots_scratch_cu'] * self.cu_cnt
|
||||
|
||||
# NOTE: xcc logic is correct only for GFX9.
|
||||
max_scratch_waves = self.cu_cnt * self.iface.props['max_slots_scratch_cu'] * self.xccs
|
||||
wave_scratch = ceildiv(lanes_per_wave * size_per_thread, mem_alignment_size)
|
||||
num_waves = (size_per_xcc // (wave_scratch * mem_alignment_size)) // (self.se_cnt if self.target[0] != 9 else 1)
|
||||
|
||||
tmpring_t = getattr(hsa, f'union_COMPUTE_TMPRING_SIZE{"_GFX"+str(self.target[0]) if self.target[0] != 9 else ""}_bitfields')
|
||||
tmpring = int.from_bytes(tmpring_t(WAVES=min(num_waves, max_scratch_waves), WAVESIZE=wave_scratch), 'little')
|
||||
|
||||
if hasattr(self, 'aql_desc'):
|
||||
gfx9_rsrc = {'NUM_FORMAT':hsa.BUF_NUM_FORMAT_UINT, 'DATA_FORMAT':hsa.BUF_DATA_FORMAT_32, 'ELEMENT_SIZE':1, 'INDEX_STRIDE':3}
|
||||
rsrc = {'DST_SEL_X':hsa.SQ_SEL_X, 'DST_SEL_Y':hsa.SQ_SEL_Y, 'DST_SEL_Z':hsa.SQ_SEL_Z, 'DST_SEL_W':hsa.SQ_SEL_W, 'ADD_TID_ENABLE':1,
|
||||
'TYPE':hsa.SQ_RSRC_BUF, **(gfx9_rsrc if self.target[0] == 9 else {'FORMAT':hsa.BUF_FORMAT_32_UINT, 'OOB_SELECT':2})}
|
||||
rsrc1_t = getattr(hsa, f'union_SQ_BUF_RSRC_WORD1{"_GFX11" if self.target[0] != 9 else ""}_bitfields')
|
||||
rsrc3_t = getattr(hsa, f'union_SQ_BUF_RSRC_WORD3{"_GFX"+str(self.target[0]) if self.target[0] != 9 else ""}_bitfields')
|
||||
|
||||
self.aql_desc.scratch_backing_memory_location = int(self.scratch.get_buf().va_addr)
|
||||
self.aql_desc.scratch_wave64_lane_byte_size = self.max_private_segment_size * lanes_per_wave // 64
|
||||
self.aql_desc.scratch_resource_descriptor[:] = [lo32(self.scratch.get_buf().va_addr),
|
||||
int.from_bytes(rsrc1_t(BASE_ADDRESS_HI=hi32(self.scratch.get_buf().va_addr), SWIZZLE_ENABLE=1), 'little'),
|
||||
lo32(size_per_xcc), int.from_bytes(bytes(rsrc3_t(**rsrc)), 'little')]
|
||||
self.aql_desc.compute_tmpring_size = tmpring
|
||||
self.aql_gart._buf.cpu_view()[:ctypes.sizeof(self.aql_desc)] = bytes(self.aql_desc)
|
||||
|
||||
return tmpring
|
||||
|
||||
def scratch_buffer(self, private_segment_size):
|
||||
AMDDevice.max_scratch_psize = private_segment_size = max(private_segment_size, 128, AMDDevice.max_scratch_psize)
|
||||
if self.max_private_segment_size < private_segment_size:
|
||||
lanes_per_wave = 64 # wave64
|
||||
mem_alignment_size = 256 if self.target[0] != 9 else 1024
|
||||
size_per_thread = round_up(private_segment_size, mem_alignment_size // lanes_per_wave)
|
||||
size_per_xcc = size_per_thread * lanes_per_wave * self.iface.props['max_slots_scratch_cu'] * self.cu_cnt
|
||||
self.scratch = Buffer(self.device, size_per_xcc * self.xccs, dtypes.uint8, options=BufferSpec(nolru=True), preallocate=True)
|
||||
self.max_private_segment_size = private_segment_size
|
||||
return self.scratch
|
||||
|
||||
def on_device_hang(self): self.iface.on_device_hang()
|
||||
|
||||
def device_props(self): return self.iface.props
|
||||
@@ -9,7 +9,7 @@ def print_objects():
|
||||
tensors = [x for x in gc.get_objects() if isinstance(x, Tensor)]
|
||||
tensor_ram_used = sum([prod(x.shape)*4 for x in tensors])
|
||||
lazybuffers = [x for x in gc.get_objects() if isinstance(x, UOp)]
|
||||
gpubuffers = [x for x in gc.get_objects() if isinstance(x, Buffer) and x.is_initialized()]
|
||||
gpubuffers = [x for x in gc.get_objects() if isinstance(x, Buffer) and x.is_allocated()]
|
||||
realized_buffers = [x.realized for x in lazybuffers if x.base == x and x.realized]
|
||||
gpubuffers_orphaned = [x for x in gpubuffers if x not in realized_buffers]
|
||||
|
||||
@@ -31,8 +31,7 @@ def print_objects():
|
||||
cnt += 1
|
||||
|
||||
for x in gpubuffers_orphaned:
|
||||
if getattr(x, '_buf', None): del x._buf
|
||||
if getattr(x, '_image', None): del x._image
|
||||
if x.base.is_allocated(): x.base.deallocate()
|
||||
|
||||
return len(gpubuffers_orphaned)
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from __future__ import annotations
|
||||
import functools, pathlib
|
||||
from dataclasses import replace
|
||||
from tinygrad import Tensor, dtypes
|
||||
from tinygrad.uop.ops import shape_to_shape_arg
|
||||
from tinygrad import Tensor
|
||||
from tinygrad.runtime.support.compiler_amd import HIPCCCompiler
|
||||
|
||||
FP8_MAX = 448.0
|
||||
@@ -12,13 +10,13 @@ NUM_WG, THREADS_PER_WG = 1024, 256
|
||||
@functools.cache
|
||||
def _local_abs_max_fxn(x_p, device):
|
||||
x = Tensor(x_p, device=device)
|
||||
inner = Tensor(x.uop.replace(src=(shape_to_shape_arg(x.uop.shard_shape),), arg=replace(x.uop.arg, axis=None))) if x.uop.axis is not None else x
|
||||
inner = Tensor(x.uop.src[0]) if x.uop.axis is not None else x # the per-shard view of the flat param
|
||||
return (inner.abs().max(),)
|
||||
|
||||
def local_abs_max(x:Tensor) -> Tensor:
|
||||
param = x.as_param(0)
|
||||
fxn = _local_abs_max_fxn(param.uop, x.device)
|
||||
return Tensor(fxn[0].uop.call(x.uop).gettuple(0))
|
||||
return Tensor(fxn[0].uop.call_with_output(x.uop))
|
||||
|
||||
def shard_shape(shape:tuple, axis:int, ndev:int) -> list:
|
||||
s = list(shape)
|
||||
|
||||
@@ -50,7 +50,7 @@ def _custom_quantize_fp8_with_amax(fp8_out:UOp, amax_out:UOp, x:UOp, amax_state:
|
||||
else: raise NotImplementedError(f"no atomic max for device {device}")
|
||||
amax_idx = amax_out.reshape((1,)).index(UOp.const(0))
|
||||
max_val = lds[0].load()
|
||||
atomic = UOp(Ops.CUSTOM, dtypes.void, (amax_idx, max_val.bitcast(dtypes.int32), max_val, amax_idx.load()), arg=atomic_arg)
|
||||
atomic = UOp(Ops.CUSTOM, src=(amax_idx, max_val.bitcast(dtypes.int32), max_val, amax_idx.load()), arg=(atomic_arg, dtypes.void))
|
||||
return atomic.end(tid, wg).sink(arg=KernelInfo(f"quantize_fp8_with_amax_{n_elems}", opts_to_apply=()))
|
||||
|
||||
@functools.cache
|
||||
|
||||
@@ -12,7 +12,7 @@ def _custom_quantize_mxfp4(row_fp4:UOp, row_scale:UOp, col_fp4:UOp, col_scale:UO
|
||||
mem = M*N*2 + M*N + M*N//16 # read bf16, write row+col fp4 + e8m0
|
||||
outputs = (row_fp4, row_scale, col_fp4, col_scale)
|
||||
sink = UOp.sink(*(o.base for o in outputs), x.base,
|
||||
*(UOp(Ops.CUSTOM, dtypes.void, (o.base.index(0),), arg="") for o in outputs),
|
||||
*(UOp(Ops.CUSTOM, src=(o.base.index(0),), arg=("", dtypes.void)) for o in outputs),
|
||||
UOp.special(256, "lidx0"), UOp.special(M//128, "gidx0"), UOp.special(N//64, "gidx1"),
|
||||
arg=KernelInfo(name, estimates=Estimates(ops=12*M*N, mem=mem)))
|
||||
src = (pathlib.Path(__file__).parent/"quantize_mxfp4.cpp").read_text()
|
||||
|
||||
@@ -13,12 +13,13 @@ def _rmsnorm_fwd_fxn(x_in_p, eps, device):
|
||||
return rmsnorm_fwd(Tensor(x_in_p, device=device), eps)
|
||||
|
||||
def _rmsnorm_bwd(grad:UOp, call:UOp) -> tuple:
|
||||
x_normed = Tensor(call.gettuple(0)).float()
|
||||
outs = call.unbound_outputs
|
||||
x_normed = Tensor(outs[0]).float()
|
||||
do_float = Tensor(grad).float()
|
||||
d_x = Tensor(call.gettuple(1)) * (do_float - x_normed * (do_float * x_normed).mean(-1, keepdim=True))
|
||||
d_x = Tensor(outs[1]) * (do_float - x_normed * (do_float * x_normed).mean(-1, keepdim=True))
|
||||
return (d_x.cast(call.src[1].dtype).uop,)
|
||||
|
||||
def rmsnorm(x_in:Tensor, eps:float) -> tuple[Tensor, Tensor]:
|
||||
fxn = _rmsnorm_fwd_fxn(x_in.as_param(0).uop, eps, x_in.device)
|
||||
call = UOp.maketuple(fxn[0].uop, fxn[1].uop).call(x_in.uop, grad_fxn=_rmsnorm_bwd)
|
||||
return Tensor(call.gettuple(0)), Tensor(call.gettuple(1))
|
||||
outs = UOp.call_with_outputs((fxn[0].uop, fxn[1].uop), x_in.uop, grad_fxn=_rmsnorm_bwd)
|
||||
return Tensor(outs[0]), Tensor(outs[1])
|
||||
|
||||
@@ -30,8 +30,8 @@ print(f"[init] loopback connect QP 0x{qp.qp_info['qpn']:x}")
|
||||
qp.connect(qp.qp_info['qpn'], dev.mac, int.from_bytes(dev.local_gid, 'big'))
|
||||
|
||||
# allocate src/dst via AMD GPU allocator
|
||||
buf_src = gpu.allocator.alloc(BUF_SIZE, BufferSpec(nolru=True))
|
||||
buf_dst = gpu.allocator.alloc(BUF_SIZE, BufferSpec(nolru=True))
|
||||
buf_src = gpu.allocator.alloc(BUF_SIZE, BufferSpec(nolru=True))[0][0]
|
||||
buf_dst = gpu.allocator.alloc(BUF_SIZE, BufferSpec(nolru=True))[0][0]
|
||||
|
||||
bar_base = gpu.iface.pci_dev.bar_info(gpu.iface.vram_bar)[0]
|
||||
src_paddr = buf_src.meta.mapping.paddrs[0][0] + bar_base
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
# TODO: there is a timing bug without this
|
||||
os.environ["AMD_AQL"] = "1"
|
||||
|
||||
from tinygrad import Tensor, Device, GlobalCounters, Context
|
||||
from tinygrad import Tensor, Device, GlobalCounters, Context, dtypes
|
||||
from tinygrad.helpers import getenv, DEV
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo
|
||||
from tinygrad.renderer import Estimates
|
||||
@@ -37,7 +37,7 @@ def launchBenchmark(instruction, vgprIndices, dense=True, accum=False, **kwargs)
|
||||
gidx = UOp.special(NUM_WORKGROUPS, "gidx0")
|
||||
FLOPs = FLOPS_PER_MATMUL * NUM_WAVES * NUM_WORKGROUPS * INTERNAL_LOOP * INSTRUCTIONS_PER_LOOP
|
||||
sink = UOp.sink(A.base, threads, gidx, arg=KernelInfo(inst.op.name.lower(), estimates=Estimates(ops=FLOPs, mem=0)))
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=x) for x in insts]))))
|
||||
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple([UOp(Ops.INS, arg=(x, dtypes.void)) for x in insts]))))
|
||||
dummy = Tensor.zeros(1).contiguous().realize()
|
||||
out = Tensor.custom_kernel(dummy, fxn=fxn)[0]
|
||||
linear = out.schedule_linear()
|
||||
|
||||
@@ -5,9 +5,9 @@ from tinygrad.helpers import getenv, DEBUG
|
||||
|
||||
# https://github.com/facebookresearch/llama/blob/1076b9c51c77ad06e9d7ba8a4c6df775741732bd/llama/model.py#L47
|
||||
def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0) -> Tensor:
|
||||
freqs = 1.0 / (theta ** (Tensor.arange(0, dim, 2)[:(dim // 2)] / dim))
|
||||
freqs = Tensor.arange(end).unsqueeze(dim=1) * freqs.unsqueeze(dim=0)
|
||||
return Tensor.stack(freqs.cos(), freqs.sin(), dim=-1).reshape(1, end, 1, dim//2, 2)
|
||||
freqs = 1.0 / (theta ** (Tensor.arange(0, dim, 2, dtype=dtypes.float32)[:(dim // 2)] / dim))
|
||||
freqs = Tensor.arange(end, dtype=dtypes.float32).unsqueeze(dim=1) * freqs.unsqueeze(dim=0)
|
||||
return Tensor.stack(freqs.cos(), freqs.sin(), dim=-1).cast(dtypes.default_float).reshape(1, end, 1, dim//2, 2)
|
||||
|
||||
# matches meta, non hugging face weights
|
||||
# (a+i*b) * (c+i*d) = (ac-bd) + i*(ad+bc)
|
||||
@@ -139,7 +139,7 @@ class TransformerBlock:
|
||||
|
||||
def __call__(self, x:Tensor, start_pos:Union[Variable,int], freqs_cis:Tensor, mask:Optional[Tensor]):
|
||||
h = x + self.attention(self.attention_norm(x), start_pos, freqs_cis, mask)
|
||||
return (h + self.feed_forward(self.ffn_norm(h))).contiguous().contiguous_backward()
|
||||
return (h + self.feed_forward(self.ffn_norm(h))).clone().contiguous_backward()
|
||||
|
||||
# standard openai sampling
|
||||
def sample(logits: Tensor, temp: float, k: int, p: float, af: float, ap: float):
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from tinygrad import Tensor
|
||||
import os
|
||||
from tinygrad.tensor import _to_np_dtype
|
||||
from tinygrad.nn.onnx import OnnxRunner, OnnxValue
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
ort_options = ort.SessionOptions()
|
||||
ort_options.log_severity_level = 3
|
||||
ort_options.intra_op_num_threads = os.cpu_count() or 1
|
||||
|
||||
def get_example_inputs(graph_inputs:dict[str, OnnxValue], config={}):
|
||||
"""
|
||||
|
||||
@@ -89,7 +89,8 @@ class TestBeamSearch(unittest.TestCase):
|
||||
s.apply_opt(Opt(OptOps.TC, 0, (-1, 0, 1)))
|
||||
up = prod([x for x, t in zip(s.full_shape, s.axis_types) if t in (AxisType.UPCAST, AxisType.UNROLL)])
|
||||
actions = get_kernel_actions(s, include_0=False, max_up=int(up))
|
||||
upcasted = [s for s in actions.values() if any(opt.op in (OptOps.UPCAST, OptOps.UNROLL) for opt in s.applied_opts)]
|
||||
upcasted = [s for s in actions.values() if any(o.op is OptOps.SPLIT and o.arg[1] in (AxisType.UPCAST, AxisType.UNROLL)
|
||||
for o in s.applied_opts)]
|
||||
assert len(upcasted) > 0, f"expected upcast/unroll actions after TC with max_up={up}, but got none"
|
||||
|
||||
def test_max_up(self):
|
||||
@@ -98,8 +99,8 @@ class TestBeamSearch(unittest.TestCase):
|
||||
s = Scheduler(ast, Device[Device.DEFAULT].renderer)
|
||||
for max_up in (2, 4):
|
||||
actions = get_kernel_actions(s, include_0=False, max_up=max_up)
|
||||
for up_opts in [s.applied_opts for s in actions.values() if any(opt.op in (OptOps.UPCAST, OptOps.UNROLL) for opt in s.applied_opts)]:
|
||||
assert len([opt for opt in up_opts if opt.arg > max_up]) == 0 and len([op for op in up_opts if op.arg <= max_up]) > 0
|
||||
up_opts = [o for s in actions.values() for o in s.applied_opts if o.op is OptOps.SPLIT and o.arg[1] in (AxisType.UPCAST, AxisType.UNROLL)]
|
||||
assert len([opt for opt in up_opts if opt.arg[0] > max_up]) == 0 and len([op for op in up_opts if op.arg[0] <= max_up]) > 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import os, sys, time
|
||||
from tinygrad.runtime.support.system import RemotePCIDevice
|
||||
from extra.hcq1.remote import RemotePCIDevice
|
||||
|
||||
LAT_N_RUNS = 500
|
||||
THROUGHPUT_N_RUNS = 8
|
||||
@@ -18,7 +18,7 @@ if __name__ == "__main__":
|
||||
print(f"connected to {os.environ['REMOTE']}, device: {name}\n")
|
||||
|
||||
# ping (minimal server round-trip, no device I/O)
|
||||
from tinygrad.runtime.support.system import RemoteCmd
|
||||
from extra.hcq1.remote import RemoteCmd
|
||||
sock = pci.sock
|
||||
for _ in range(10): RemotePCIDevice._rpc(sock, 0, RemoteCmd.PING)
|
||||
st = time.perf_counter()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import socket, struct, sys
|
||||
from tinygrad.runtime.support.system import PCIDevice, RemoteCmd, System
|
||||
from tinygrad.runtime.support.system import PCIDevice, System
|
||||
from extra.hcq1.remote import RemoteCmd
|
||||
from tinygrad.helpers import DEBUG, OSX
|
||||
|
||||
def resp(resp0=0, resp1=0, status=0): return struct.pack('<BQQ', status, resp0, resp1)
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
# Runbook: Llama 3 8B Training on DigitalOcean MI350X
|
||||
|
||||
## Machine Specs
|
||||
- 8x MI350X GPUs (gfx950, device ID 75b0), 288GB VRAM each
|
||||
- 2TB RAM, 192 CPUs, 2TB disk
|
||||
- ROCm 7.14 at `/opt/rocm` (NOT `/opt/rocm-7.1.1` like the submission scripts assume)
|
||||
- Python 3.12
|
||||
|
||||
## Phase 1: System Setup
|
||||
|
||||
### 1.1 Install packages
|
||||
```bash
|
||||
apt-get update
|
||||
apt-get install -y python3-pip python3-venv git tmux rclone clang
|
||||
```
|
||||
|
||||
### 1.2 Install Python deps
|
||||
```bash
|
||||
python3 -m pip install --break-system-packages --ignore-installed typing-extensions numpy tqdm wandb tiktoken sentencepiece
|
||||
```
|
||||
Note: `--ignore-installed typing-extensions` is needed because the base image ships typing-extensions 4.10.0 without a RECORD file, so pip cannot uninstall it.
|
||||
|
||||
### 1.3 Install ROCm dev headers
|
||||
The base image has ROCm runtime but NOT the HIP dev headers. Need:
|
||||
```bash
|
||||
apt-get install -y amdrocm-core-dev
|
||||
```
|
||||
This installs `hip/hip_runtime.h` at `/opt/rocm/core-7.14/include/hip/hip_runtime.h`.
|
||||
The symlink `/opt/rocm/include` → `/opt/rocm/core-7.14/include` makes it available at `/opt/rocm/include/hip/hip_runtime.h`.
|
||||
|
||||
### 1.4 Configure ROCm comgr
|
||||
ROCm 7.14 ships comgr 3.3 at `/opt/rocm/lib/libamd_comgr.so`. tinygrad's DLL loader needs explicit env vars to find it (it searches for `libcomgr.so*` by default, not `libamd_comgr.so*`). Set these in the run command:
|
||||
```bash
|
||||
export COMGR_PATH=/opt/rocm/lib/libamd_comgr.so
|
||||
export COMGR_3_PATH=/opt/rocm/lib/libamd_comgr.so
|
||||
```
|
||||
Also add ROCm libs to ldconfig so comgr's shared library dependencies resolve:
|
||||
```bash
|
||||
cat > /etc/ld.so.conf.d/rocm.conf << 'EOF'
|
||||
/opt/rocm/lib
|
||||
/opt/rocm/lib/llvm/lib
|
||||
/opt/rocm/lib/rocm_sysdeps/lib
|
||||
EOF
|
||||
ldconfig
|
||||
```
|
||||
|
||||
### 1.5 Install geohot tmux config
|
||||
```bash
|
||||
curl -sL https://raw.githubusercontent.com/geohot/configuration/master/.tmux.conf -o ~/.tmux.conf
|
||||
```
|
||||
|
||||
### 1.6 Verify GPU PCI access
|
||||
The AM userspace driver accesses the GPUs directly over PCI. Do not load `amdgpu`. `/dev/kfd` is not required.
|
||||
```bash
|
||||
rmmod amdgpu
|
||||
lspci -nnk -d 1002:
|
||||
```
|
||||
The MI350X devices should not show a `Kernel driver in use: amdgpu`.
|
||||
|
||||
## Phase 2: Clone tinygrad
|
||||
```bash
|
||||
cd /root
|
||||
git clone https://github.com/tinygrad/tinygrad.git
|
||||
cd tinygrad
|
||||
python3 -m pip install --break-system-packages -e .
|
||||
```
|
||||
|
||||
## Phase 3: Download C4 Dataset
|
||||
|
||||
The C4 data is on the MLCommons Cloudflare R2 bucket in Megatron-LM indexed format.
|
||||
|
||||
```bash
|
||||
rclone config create mlc-training s3 provider=Cloudflare \
|
||||
access_key_id=76ea42eadb867e854061a1806220ee1e \
|
||||
secret_access_key=a53625c4d45e3ca8ac0df8a353ea3a41ffc3292aa25259addd8b7dc5a6ce2936 \
|
||||
endpoint=c2686074cb2caf5cbaf6d134bdba8b47.r2.cloudflarestorage.com
|
||||
|
||||
mkdir -p /raid/datasets/c4-8b
|
||||
(rclone copy mlc-training:mlcommons-training-wg-public/llama3_1/datasets/c4/llama3_1_8b/ /raid/datasets/c4-8b/ -P && \
|
||||
PYTHONPATH=. python3 examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama31_8b/implementations/tinybox_8xMI350X/buid_dataset_cache.py) \
|
||||
> /root/dataset_cache.log 2>&1 &
|
||||
```
|
||||
Leave this running and proceed to the beam step while the dataset downloads and its cache builds.
|
||||
|
||||
### 3.1 Smoke test (beam search, 2 layers, fake data)
|
||||
Always run beam first to validate the pipeline:
|
||||
```bash
|
||||
tmux new-session -d -s beam 'cd /root/tinygrad && COMGR_PATH=/opt/rocm/lib/libamd_comgr.so COMGR_3_PATH=/opt/rocm/lib/libamd_comgr.so CC=/opt/rocm/core-7.14/lib/llvm/bin/clang DEV=PCI+AMD:HIP ROCM_PATH=/opt/rocm bash examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama31_8b/implementations/tinybox_8xMI350X/dev_beam.sh 2>&1 | tee /root/beam.log'
|
||||
```
|
||||
|
||||
The beam test runs 10 training steps with 2 layers. Expected results:
|
||||
- ~0.29s per step after warmup
|
||||
- ~700K GFLOPS, ~7% MFU (low because only 2 layers)
|
||||
- ~380 GB VRAM used
|
||||
- Loss stable at ~12.55 with random init
|
||||
|
||||
Files downloaded (~85GB total, ~6 minutes):
|
||||
- `c4-train.en_6_text_document.bin` (79 GB)
|
||||
- `c4-train.en_6_text_document.idx` (870 MB)
|
||||
- `c4-validation-91205-samples.en_text_document.bin` (159 MB)
|
||||
- `c4-validation-91205-samples.en_text_document.idx` (1.8 MB)
|
||||
- `LICENSE.txt`, `NOTICE.txt`
|
||||
|
||||
**Wait for rclone to fully complete before starting training.** Starting training while the dataset is still downloading will read a truncated .bin file, causing `ValueError: all input arrays must have the same shape` in the dataloader. The stale `.index_cache` and `.blend_cache` files must also be deleted if this happens:
|
||||
```bash
|
||||
rm -f /raid/datasets/c4-8b/*.index_cache /raid/datasets/c4-8b/*.blend_cache
|
||||
```
|
||||
|
||||
## Phase 4: wandb Login
|
||||
```bash
|
||||
wandb login
|
||||
```
|
||||
Enter API key from https://wandb.ai/authorize
|
||||
|
||||
Alternatively, pass the key directly:
|
||||
```bash
|
||||
wandb login <API_KEY>
|
||||
```
|
||||
|
||||
## Phase 5: Run Training
|
||||
|
||||
Run training in tmux so it survives SSH disconnects:
|
||||
```bash
|
||||
tmux new-session -d -s train 'cd /root/tinygrad && COMGR_PATH=/opt/rocm/lib/libamd_comgr.so COMGR_3_PATH=/opt/rocm/lib/libamd_comgr.so CC=/opt/rocm/core-7.14/lib/llvm/bin/clang DEV=PCI+AMD:HIP ROCM_PATH=/opt/rocm WANDB=1 bash examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama31_8b/implementations/tinybox_8xMI350X/dev_run.sh 2>&1 | tee /root/train.log'
|
||||
```
|
||||
Attach with `tmux attach -t train`.
|
||||
|
||||
### 5.1 Full training run
|
||||
```bash
|
||||
tmux new-session -d -s train 'cd /root/tinygrad && COMGR_PATH=/opt/rocm/lib/libamd_comgr.so COMGR_3_PATH=/opt/rocm/lib/libamd_comgr.so CC=/opt/rocm/core-7.14/lib/llvm/bin/clang DEV=PCI+AMD:HIP ROCM_PATH=/opt/rocm WANDB=1 bash examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama31_8b/implementations/tinybox_8xMI350X/dev_run.sh 2>&1 | tee /root/train.log'
|
||||
```
|
||||
|
||||
## Environment Variable Reference
|
||||
|
||||
| Variable | Value | Why |
|
||||
|---|---|---|
|
||||
| `COMGR_PATH` | `/opt/rocm/lib/libamd_comgr.so` | tinygrad's DLL loader needs explicit path to find comgr 3.3 |
|
||||
| `COMGR_3_PATH` | `/opt/rocm/lib/libamd_comgr.so` | comgr 3.x uses a separate `comgr_3` module with its own path var |
|
||||
| `CC` | `/opt/rocm/core-7.14/lib/llvm/bin/clang` | System clang doesn't know gfx950; must use ROCm's bundled clang |
|
||||
| `DEV` | `PCI+AMD:HIP` | Force HIPRenderer (comgr-based) over HIPCCRenderer (hipcc subprocess) |
|
||||
| `ROCM_PATH` | `/opt/rocm` | Script defaults to `/opt/rocm-7.1.1` which doesn't exist |
|
||||
| `WANDB` | `1` | Enable wandb logging (off by default) |
|
||||
|
||||
## Architecture
|
||||
|
||||
| Component | Source file |
|
||||
|---|---|
|
||||
| Model | `examples/mlperf/models/flat_llama.py` — FlatTransformer, FP8 MXFP4 weights, fused QKV, flash attention |
|
||||
| Trainer | `examples/mlperf/model_train.py` → `train_llama3()` |
|
||||
| Optimizer | `examples/mlperf/optim.py` — GradAccClipAdamW, master weights, FP8 re-quant |
|
||||
| LR schedule | `examples/mlperf/lr_schedulers.py` — CosineAnnealingLRWithWarmup |
|
||||
| Dataloader | `examples/mlperf/dataloader.py` — Megatron-LM indexed bin format |
|
||||
| ASM GEMM | `extra/gemm/cdna_asm_gemm.py` — gfx950 MFMA assembly, MXFP4 |
|
||||
| Flash attention | `extra/thunder/amd/fa.py` |
|
||||
| Fused kernels | `extra/llama_kernels/` — rmsnorm, silu, quantize, fused_ce |
|
||||
| GPU driver | `tinygrad/runtime/ops_amd.py` — HCQ, using the AM userspace PCI interface |
|
||||
| Renderer | `tinygrad/renderer/cstyle.py` — HIPRenderer for gfx950 |
|
||||
| comgr compiler | `tinygrad/runtime/support/compiler_amd.py` — HIPCompiler using comgr 3.3 |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `'hip/hip_runtime.h' file not found`
|
||||
Install `amdrocm-core-dev`:
|
||||
```bash
|
||||
apt-get install -y amdrocm-core-dev
|
||||
```
|
||||
|
||||
### `'gfx950' is not a recognized processor` + LLVM crash
|
||||
System clang doesn't know gfx950. Set `CC=/opt/rocm/core-7.14/lib/llvm/bin/clang`.
|
||||
|
||||
### `comgr not available: try setting COMGR_PATH?`
|
||||
Add ROCm libs to ldconfig and set `COMGR_PATH` and `COMGR_3_PATH`:
|
||||
```bash
|
||||
# /etc/ld.so.conf.d/rocm.conf should contain /opt/rocm/lib paths
|
||||
ldconfig
|
||||
```
|
||||
|
||||
### `comgr not available: try setting COMGR_3_PATH?`
|
||||
comgr 3.x uses a separate module. Set `COMGR_3_PATH=/opt/rocm/lib/libamd_comgr.so` too.
|
||||
|
||||
### `No such file or directory: 'clang'`
|
||||
Install clang: `apt-get install -y clang` (for CPU compilation).
|
||||
For gfx950 HIP compilation, comgr (not clang) is used — ensure the ROCm 7.14 comgr 3.3 is properly loaded via `COMGR_PATH` and `COMGR_3_PATH`.
|
||||
|
||||
## Appendix: KVM Virtualization Observations
|
||||
|
||||
### Virtualization detection
|
||||
```
|
||||
$ systemd-detect-virt
|
||||
kvm
|
||||
$ lspci -nn | grep AMD
|
||||
83:00.0 ... Device [1002:75b0]
|
||||
```
|
||||
CPU flags include `hypervisor`. `dmesg` shows `Hypervisor detected: KVM`.
|
||||
|
||||
### No fan control
|
||||
No `fan*` or `pwm*` hwmon entries exist. Only `temp*`, `power*`, `freq*` are exposed. GPU temps read 56-63°C, power ~265W per GPU.
|
||||
@@ -1,4 +1,4 @@
|
||||
import os, subprocess, sys, shlex
|
||||
import os, subprocess, sys, shlex, pickle
|
||||
from pathlib import Path
|
||||
from tinygrad.helpers import temp, getenv
|
||||
|
||||
@@ -23,5 +23,8 @@ if __name__ == "__main__":
|
||||
# AM_RESET=1 gets a clear trace, does not work on mi300 machines
|
||||
subprocess.run([sys.executable, *shlex.split(test)], cwd=EXAMPLES_DIR.parent.parent.parent,
|
||||
env={**os.environ, "DEV":"AMD", "AM_RESET":"1" if not arch.startswith("gfx9") else "0", "VIZ":"-2", "PYTHONPATH":"."})
|
||||
with open(PROFILE_PATH, "rb") as f: events = pickle.load(f)
|
||||
with open(PROFILE_PATH, "wb") as f:
|
||||
pickle.dump([e for e in events if type(e).__name__ in {"ProfilePMCEvent", "ProfileSQTTEvent", "ProfileProgramEvent"}], f)
|
||||
PROFILE_PATH.rename(dest:=EXAMPLES_DIR/arch/f"profile_{name}_run_{i}.pkl")
|
||||
print(f"saved SQTT trace to {dest}")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user