Uploaded using `kernel-builder`.
Browse files- .gitattributes +9 -0
- build/torch211-cxx11-cu126-x86_64-linux/__init__.py +863 -0
- build/torch211-cxx11-cu126-x86_64-linux/_metakernel_cuda_4611d12.abi3.so +3 -0
- build/torch211-cxx11-cu126-x86_64-linux/_ops.py +9 -0
- build/torch211-cxx11-cu126-x86_64-linux/metadata.json +36 -0
- build/torch211-cxx11-cu126-x86_64-linux/metakernel/__init__.py +26 -0
- build/torch211-cxx11-cu128-x86_64-linux/__init__.py +863 -0
- build/torch211-cxx11-cu128-x86_64-linux/_metakernel_cuda_4611d12.abi3.so +3 -0
- build/torch211-cxx11-cu128-x86_64-linux/_ops.py +9 -0
- build/torch211-cxx11-cu128-x86_64-linux/metadata.json +38 -0
- build/torch211-cxx11-cu128-x86_64-linux/metakernel/__init__.py +26 -0
- build/torch211-cxx11-cu130-x86_64-linux/__init__.py +863 -0
- build/torch211-cxx11-cu130-x86_64-linux/_metakernel_cuda_4611d12.abi3.so +3 -0
- build/torch211-cxx11-cu130-x86_64-linux/_ops.py +9 -0
- build/torch211-cxx11-cu130-x86_64-linux/metadata.json +38 -0
- build/torch211-cxx11-cu130-x86_64-linux/metakernel/__init__.py +26 -0
- build/torch212-cxx11-cu126-x86_64-linux/__init__.py +863 -0
- build/torch212-cxx11-cu126-x86_64-linux/_metakernel_cuda_4611d12.abi3.so +3 -0
- build/torch212-cxx11-cu126-x86_64-linux/_ops.py +9 -0
- build/torch212-cxx11-cu126-x86_64-linux/metadata.json +36 -0
- build/torch212-cxx11-cu126-x86_64-linux/metakernel/__init__.py +26 -0
- build/torch212-cxx11-cu130-x86_64-linux/__init__.py +863 -0
- build/torch212-cxx11-cu130-x86_64-linux/_metakernel_cuda_4611d12.abi3.so +3 -0
- build/torch212-cxx11-cu130-x86_64-linux/_ops.py +9 -0
- build/torch212-cxx11-cu130-x86_64-linux/metadata.json +38 -0
- build/torch212-cxx11-cu130-x86_64-linux/metakernel/__init__.py +26 -0
- build/torch212-cxx11-cu132-x86_64-linux/__init__.py +863 -0
- build/torch212-cxx11-cu132-x86_64-linux/_metakernel_cuda_4611d12.abi3.so +3 -0
- build/torch212-cxx11-cu132-x86_64-linux/_ops.py +9 -0
- build/torch212-cxx11-cu132-x86_64-linux/metadata.json +38 -0
- build/torch212-cxx11-cu132-x86_64-linux/metakernel/__init__.py +26 -0
- build/torch213-cxx11-cu126-x86_64-linux/__init__.py +863 -0
- build/torch213-cxx11-cu126-x86_64-linux/_metakernel_cuda_4611d12.abi3.so +3 -0
- build/torch213-cxx11-cu126-x86_64-linux/_ops.py +9 -0
- build/torch213-cxx11-cu126-x86_64-linux/metadata.json +36 -0
- build/torch213-cxx11-cu126-x86_64-linux/metakernel/__init__.py +26 -0
- build/torch213-cxx11-cu130-x86_64-linux/__init__.py +863 -0
- build/torch213-cxx11-cu130-x86_64-linux/_metakernel_cuda_4611d12.abi3.so +3 -0
- build/torch213-cxx11-cu130-x86_64-linux/_ops.py +9 -0
- build/torch213-cxx11-cu130-x86_64-linux/metadata.json +38 -0
- build/torch213-cxx11-cu130-x86_64-linux/metakernel/__init__.py +26 -0
- build/torch213-cxx11-cu132-x86_64-linux/__init__.py +863 -0
- build/torch213-cxx11-cu132-x86_64-linux/_metakernel_cuda_4611d12.abi3.so +3 -0
- build/torch213-cxx11-cu132-x86_64-linux/_ops.py +9 -0
- build/torch213-cxx11-cu132-x86_64-linux/metadata.json +38 -0
- build/torch213-cxx11-cu132-x86_64-linux/metakernel/__init__.py +26 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,12 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
build/torch211-cxx11-cu126-x86_64-linux/_metakernel_cuda_4611d12.abi3.so filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
build/torch211-cxx11-cu128-x86_64-linux/_metakernel_cuda_4611d12.abi3.so filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
build/torch211-cxx11-cu130-x86_64-linux/_metakernel_cuda_4611d12.abi3.so filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
build/torch212-cxx11-cu126-x86_64-linux/_metakernel_cuda_4611d12.abi3.so filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
build/torch212-cxx11-cu130-x86_64-linux/_metakernel_cuda_4611d12.abi3.so filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
build/torch212-cxx11-cu132-x86_64-linux/_metakernel_cuda_4611d12.abi3.so filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
build/torch213-cxx11-cu126-x86_64-linux/_metakernel_cuda_4611d12.abi3.so filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
build/torch213-cxx11-cu130-x86_64-linux/_metakernel_cuda_4611d12.abi3.so filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
build/torch213-cxx11-cu132-x86_64-linux/_metakernel_cuda_4611d12.abi3.so filter=lfs diff=lfs merge=lfs -text
|
build/torch211-cxx11-cu126-x86_64-linux/__init__.py
ADDED
|
@@ -0,0 +1,863 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""metakernel: the measuring instrument for the kernel-design loop.
|
| 2 |
+
|
| 3 |
+
An agent iterating on CUDA candidates is good at writing code and bad at
|
| 4 |
+
trusting its own measurements; it needs trustworthy timing, adversarial
|
| 5 |
+
correctness comparison, and measured facts about the device it targets.
|
| 6 |
+
metakernel ships those as loadable ops:
|
| 7 |
+
|
| 8 |
+
from kernels import get_kernel
|
| 9 |
+
mk = get_kernel("phanerozoic/metakernel", version=1,
|
| 10 |
+
trust_remote_code=True)
|
| 11 |
+
|
| 12 |
+
dossier = mk.probe_device() # measured facts
|
| 13 |
+
report = mk.bench(lambda: op(x), iters=200) # throttle-rejected
|
| 14 |
+
verdict = mk.compare(ref, cand, gen) # ULP evidence
|
| 15 |
+
table = mk.sweep(factory, {"BLOCK": [64, 128]}) # config search
|
| 16 |
+
|
| 17 |
+
Every op returns plain dicts and tensors, never prose. Every dossier entry
|
| 18 |
+
is produced by a dedicated probe kernel rather than a spec sheet, carries
|
| 19 |
+
the SM clock it was measured under, and labels throttled samples instead
|
| 20 |
+
of letting them poison medians. No hardware counters are used: everything
|
| 21 |
+
is inferred from timing, clocks, and the occupancy API, so entries carry a
|
| 22 |
+
confidence field (timing-differential inference can misattribute).
|
| 23 |
+
"""
|
| 24 |
+
import math
|
| 25 |
+
import time
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
|
| 29 |
+
from ._ops import ops
|
| 30 |
+
|
| 31 |
+
__all__ = ["probe_device", "bench", "compare", "order_shuffles",
|
| 32 |
+
"fuzz_shapes", "sweep", "alloc_stamps", "read_stamps",
|
| 33 |
+
"measure_clock", "ulp_diff", "ops"]
|
| 34 |
+
|
| 35 |
+
_MMA_KINDS = {
|
| 36 |
+
"fp16": (0, 16 * 16 * 16),
|
| 37 |
+
"bf16": (1, 16 * 16 * 16),
|
| 38 |
+
"tf32": (2, 16 * 16 * 8),
|
| 39 |
+
"int8": (3, 16 * 16 * 16),
|
| 40 |
+
"fp8_e4m3": (4, 16 * 8 * 32),
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _sync():
|
| 45 |
+
torch.cuda.synchronize()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _events(fn, reps):
|
| 49 |
+
"""Time reps calls of fn with one event pair each; returns ms list."""
|
| 50 |
+
starts = [torch.cuda.Event(enable_timing=True) for _ in range(reps)]
|
| 51 |
+
stops = [torch.cuda.Event(enable_timing=True) for _ in range(reps)]
|
| 52 |
+
for s, e in zip(starts, stops):
|
| 53 |
+
s.record()
|
| 54 |
+
fn()
|
| 55 |
+
e.record()
|
| 56 |
+
_sync()
|
| 57 |
+
return [s.elapsed_time(e) for s, e in zip(starts, stops)]
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def measure_clock(device="cuda", ticks=4_000_000, blocks=None):
|
| 61 |
+
"""Effective SM clock in GHz under an FMA spin of `ticks` clock64 ticks.
|
| 62 |
+
Wall time over elapsed ticks is the frequency the ticks ran at."""
|
| 63 |
+
props = torch.cuda.get_device_properties(device)
|
| 64 |
+
if blocks is None:
|
| 65 |
+
blocks = props.multi_processor_count
|
| 66 |
+
out = torch.zeros(2, dtype=torch.int64, device=device)
|
| 67 |
+
sink = torch.zeros(2, dtype=torch.float32, device=device)
|
| 68 |
+
ops.mk_spin(blocks, 256, ticks, out, sink) # warm
|
| 69 |
+
start = torch.cuda.Event(enable_timing=True)
|
| 70 |
+
stop = torch.cuda.Event(enable_timing=True)
|
| 71 |
+
start.record()
|
| 72 |
+
ops.mk_spin(blocks, 256, ticks, out, sink)
|
| 73 |
+
stop.record()
|
| 74 |
+
_sync()
|
| 75 |
+
ms = start.elapsed_time(stop)
|
| 76 |
+
elapsed = int(out[0].item())
|
| 77 |
+
return elapsed / (ms * 1e6)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _median(xs):
|
| 81 |
+
s = sorted(xs)
|
| 82 |
+
n = len(s)
|
| 83 |
+
return s[n // 2] if n % 2 else 0.5 * (s[n // 2 - 1] + s[n // 2])
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _entry(value, units, method, clock_ghz, confidence, note=""):
|
| 87 |
+
d = {"value": value, "units": units, "method": method,
|
| 88 |
+
"clock_ghz": round(clock_ghz, 3), "confidence": confidence}
|
| 89 |
+
if note:
|
| 90 |
+
d["note"] = note
|
| 91 |
+
return d
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _sattolo_ring(n, device, generator=None):
|
| 95 |
+
"""A random single-cycle permutation as an int32 next-pointer table, so
|
| 96 |
+
a pointer chase visits every slot before repeating."""
|
| 97 |
+
perm = torch.randperm(n, generator=generator)
|
| 98 |
+
ring = torch.empty(n, dtype=torch.int32)
|
| 99 |
+
ring[perm] = torch.roll(perm, -1).to(torch.int32)
|
| 100 |
+
return ring.to(device)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def probe_device(device="cuda", quick=False):
|
| 104 |
+
"""Empirical device dossier. Each entry is measured by a dedicated
|
| 105 |
+
probe kernel and carries units, method, the SM clock it ran under, and
|
| 106 |
+
a confidence field."""
|
| 107 |
+
props = torch.cuda.get_device_properties(device)
|
| 108 |
+
sm = props.multi_processor_count
|
| 109 |
+
l2 = props.L2_cache_size
|
| 110 |
+
d = {
|
| 111 |
+
"device": {
|
| 112 |
+
"name": props.name,
|
| 113 |
+
"sm_arch": f"sm{props.major}{props.minor}",
|
| 114 |
+
"sms": sm,
|
| 115 |
+
"l2_bytes": l2,
|
| 116 |
+
"vram_bytes": props.total_memory,
|
| 117 |
+
"torch": torch.__version__,
|
| 118 |
+
"cuda_runtime": torch.version.cuda,
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
sink = torch.zeros(2, dtype=torch.float32, device=device)
|
| 122 |
+
out64 = torch.zeros(2, dtype=torch.int64, device=device)
|
| 123 |
+
reps = 3 if quick else 5
|
| 124 |
+
|
| 125 |
+
# ---- clock under sustained load
|
| 126 |
+
traj = []
|
| 127 |
+
windows = 8 if quick else 24
|
| 128 |
+
for _ in range(windows):
|
| 129 |
+
traj.append(measure_clock(device, ticks=30_000_000))
|
| 130 |
+
d["clock"] = {
|
| 131 |
+
"max_ghz": round(max(traj), 3),
|
| 132 |
+
"min_ghz": round(min(traj), 3),
|
| 133 |
+
"final_ghz": round(traj[-1], 3),
|
| 134 |
+
"sag_pct": round(100 * (1 - traj[-1] / max(traj)), 1),
|
| 135 |
+
"units": "GHz",
|
| 136 |
+
"method": f"{windows} back-to-back FMA spin windows of 30M ticks; "
|
| 137 |
+
"wall/ticks per window",
|
| 138 |
+
"confidence": "high",
|
| 139 |
+
}
|
| 140 |
+
clk = max(traj)
|
| 141 |
+
|
| 142 |
+
# ---- HBM bandwidth: triad per access width, plus read-only,
|
| 143 |
+
# write-only, and random-gather
|
| 144 |
+
n4 = 16 << 20 # 16M float4 = 256 MB per buffer
|
| 145 |
+
if props.total_memory < 4 << 30:
|
| 146 |
+
n4 = 4 << 20
|
| 147 |
+
a = torch.rand(n4 * 4, device=device)
|
| 148 |
+
b = torch.rand(n4 * 4, device=device)
|
| 149 |
+
c = torch.empty(n4 * 4, device=device)
|
| 150 |
+
hbm = {}
|
| 151 |
+
for width in (1, 2, 4):
|
| 152 |
+
ops.mk_triad(a, b, c, width, 1.5) # warm
|
| 153 |
+
ms = _median(_events(lambda w=width: ops.mk_triad(a, b, c, w, 1.5),
|
| 154 |
+
reps))
|
| 155 |
+
gbs = 3 * a.numel() * 4 / (ms * 1e-3) / 1e9
|
| 156 |
+
hbm[f"triad_{4 * width}B"] = round(gbs, 1)
|
| 157 |
+
ops.mk_read(a, sink)
|
| 158 |
+
ms = _median(_events(lambda: ops.mk_read(a, sink), reps))
|
| 159 |
+
hbm["read_only"] = round(a.numel() * 4 / (ms * 1e-3) / 1e9, 1)
|
| 160 |
+
ops.mk_write(c, 1.0)
|
| 161 |
+
ms = _median(_events(lambda: ops.mk_write(c, 1.0), reps))
|
| 162 |
+
hbm["write_only"] = round(c.numel() * 4 / (ms * 1e-3) / 1e9, 1)
|
| 163 |
+
g = torch.Generator(device="cpu").manual_seed(7)
|
| 164 |
+
idx = torch.randint(0, n4, (n4,), generator=g,
|
| 165 |
+
dtype=torch.int32).to(device)
|
| 166 |
+
ops.mk_gather(a, idx, c, 256, 0)
|
| 167 |
+
ms = _median(_events(lambda: ops.mk_gather(a, idx, c, 256, 0), reps))
|
| 168 |
+
# traffic accounting: idx reads + 16B random loads + streamed stores
|
| 169 |
+
hbm["random_gather_16B"] = round(
|
| 170 |
+
(idx.numel() * 4 + 2 * n4 * 16) / (ms * 1e-3) / 1e9, 1)
|
| 171 |
+
d["hbm_bw"] = _entry(hbm, "GB/s", "streaming triad c=a+s*b per access "
|
| 172 |
+
"width; read-only fold; write-only fill; random "
|
| 173 |
+
"float4 gather (16B granules, nominal traffic)",
|
| 174 |
+
clk, "high",
|
| 175 |
+
"gather moves full cache lines per 16B granule, "
|
| 176 |
+
"so its nominal number is the achievable rate for "
|
| 177 |
+
"random access, not a line-efficiency claim")
|
| 178 |
+
|
| 179 |
+
# ---- L2 bandwidth (working set 3 buffers inside L2)
|
| 180 |
+
ws = max(l2 // 4, 1 << 20)
|
| 181 |
+
nl2 = ws // 16 # float4 elems per buffer
|
| 182 |
+
al = a[:nl2 * 4].contiguous()
|
| 183 |
+
bl = b[:nl2 * 4].contiguous()
|
| 184 |
+
cl = c[:nl2 * 4].contiguous()
|
| 185 |
+
passes = 64
|
| 186 |
+
ops.mk_triad_passes(al, bl, cl, 1.5, passes)
|
| 187 |
+
ms = _median(_events(
|
| 188 |
+
lambda: ops.mk_triad_passes(al, bl, cl, 1.5, passes), reps))
|
| 189 |
+
gbs = 3 * al.numel() * 4 * passes / (ms * 1e-3) / 1e9
|
| 190 |
+
d["l2_bw"] = _entry(round(gbs, 1), "GB/s",
|
| 191 |
+
f"in-kernel repeated triad passes over "
|
| 192 |
+
f"{3 * ws >> 20} MB resident working set", clk,
|
| 193 |
+
"medium", "first pass streams from HBM; amortized "
|
| 194 |
+
f"over {passes} passes")
|
| 195 |
+
|
| 196 |
+
# ---- latencies: pointer chase per level. Footprints are chosen so
|
| 197 |
+
# the chain hits the intended level: the L2 ring (2 MB) exceeds L1,
|
| 198 |
+
# fits L2 on every supported part, and spans few enough pages to stay
|
| 199 |
+
# TLB-resident after a full-cycle warm walk; the HBM chain is long
|
| 200 |
+
# enough (2x L2 in touched lines) that it evicts itself and cannot be
|
| 201 |
+
# served from cache.
|
| 202 |
+
lat = {}
|
| 203 |
+
idle_clk = measure_clock(device, ticks=2_000_000, blocks=1)
|
| 204 |
+
ring_s = _sattolo_ring(8192, device)
|
| 205 |
+
ops.mk_chase_shared(ring_s, 1 << 14, out64)
|
| 206 |
+
_sync()
|
| 207 |
+
hops = 1 << 16
|
| 208 |
+
ops.mk_chase_shared(ring_s, hops, out64)
|
| 209 |
+
_sync()
|
| 210 |
+
lat["smem_cycles"] = round(int(out64[0].item()) / hops, 1)
|
| 211 |
+
|
| 212 |
+
l1_ring_n = (16 << 10) // 4 # 16 KB: L1-resident after a warm walk
|
| 213 |
+
ring_l1 = _sattolo_ring(l1_ring_n, device)
|
| 214 |
+
ops.mk_chase_global(ring_l1, l1_ring_n, out64)
|
| 215 |
+
_sync()
|
| 216 |
+
hops_l1 = 1 << 15
|
| 217 |
+
ops.mk_chase_global(ring_l1, hops_l1, out64)
|
| 218 |
+
_sync()
|
| 219 |
+
lat["l1_cycles"] = round(int(out64[0].item()) / hops_l1, 1)
|
| 220 |
+
|
| 221 |
+
l2_ring_n = (2 << 20) // 4
|
| 222 |
+
ring_l2 = _sattolo_ring(l2_ring_n, device)
|
| 223 |
+
ops.mk_chase_global(ring_l2, l2_ring_n, out64) # full-cycle warm walk
|
| 224 |
+
_sync()
|
| 225 |
+
hops_l2 = 1 << 14
|
| 226 |
+
ops.mk_chase_global(ring_l2, hops_l2, out64)
|
| 227 |
+
_sync()
|
| 228 |
+
lat["l2_cycles"] = round(int(out64[0].item()) / hops_l2, 1)
|
| 229 |
+
|
| 230 |
+
hbm_bytes = min(max(4 * l2, 64 << 20), props.total_memory // 8)
|
| 231 |
+
ring_h = _sattolo_ring(int(hbm_bytes) // 4, device)
|
| 232 |
+
hops_h = max((2 * l2) // 128, 1 << 17)
|
| 233 |
+
ops.mk_chase_global(ring_h, 1 << 14, out64) # fault pages / warm TLB
|
| 234 |
+
_sync()
|
| 235 |
+
ops.mk_chase_global(ring_h, hops_h, out64)
|
| 236 |
+
_sync()
|
| 237 |
+
lat["hbm_cycles"] = round(int(out64[0].item()) / hops_h, 1)
|
| 238 |
+
lat["ns_per_cycle"] = round(1.0 / idle_clk, 3)
|
| 239 |
+
d["latency"] = _entry(lat, "cycles (single-lane dependent loads)",
|
| 240 |
+
"Sattolo-cycle pointer chase; 16 KB ring for L1, "
|
| 241 |
+
"2 MB TLB-resident ring for L2, self-evicting "
|
| 242 |
+
"chain (2x L2 in touched lines) for HBM",
|
| 243 |
+
idle_clk, "high",
|
| 244 |
+
"smem includes index arithmetic; HBM includes "
|
| 245 |
+
"residual TLB effects at large footprints")
|
| 246 |
+
del ring_h, ring_l2, ring_l1, ring_s
|
| 247 |
+
|
| 248 |
+
# ---- fp32 FMA and tensor-core throughput
|
| 249 |
+
blocks = sm * 4
|
| 250 |
+
iters = 20_000 if quick else 60_000
|
| 251 |
+
ops.mk_fma_f32(blocks, 256, iters, sink)
|
| 252 |
+
ms = _median(_events(lambda: ops.mk_fma_f32(blocks, 256, iters, sink),
|
| 253 |
+
reps))
|
| 254 |
+
flops = blocks * 256 * iters * 16 * 2 * 2 # 2 chains x 2 flops x unroll
|
| 255 |
+
d["fma_f32"] = _entry(round(flops / (ms * 1e-3) / 1e12, 2), "TFLOP/s",
|
| 256 |
+
"register-resident FMA chains", clk, "high")
|
| 257 |
+
sink64 = torch.zeros(2, dtype=torch.float64, device=device)
|
| 258 |
+
it64 = max(iters // 16, 1000)
|
| 259 |
+
ops.mk_fma_f64(blocks, 256, it64, sink64)
|
| 260 |
+
ms = _median(_events(
|
| 261 |
+
lambda: ops.mk_fma_f64(blocks, 256, it64, sink64), reps))
|
| 262 |
+
flops64 = blocks * 256 * it64 * 16 * 2 * 2
|
| 263 |
+
d["fma_f64"] = _entry(round(flops64 / (ms * 1e-3) / 1e12, 3), "TFLOP/s",
|
| 264 |
+
"register-resident double FMA chains (the DFMA "
|
| 265 |
+
"pipe)", clk, "high")
|
| 266 |
+
mma = {}
|
| 267 |
+
for name, (kind, mnk) in _MMA_KINDS.items():
|
| 268 |
+
it = 30_000 if quick else 100_000
|
| 269 |
+
r = ops.mk_mma(kind, blocks, 8, 512, sink)
|
| 270 |
+
if r != 0:
|
| 271 |
+
mma[name] = "unsupported"
|
| 272 |
+
continue
|
| 273 |
+
_sync()
|
| 274 |
+
ms = _median(_events(
|
| 275 |
+
lambda k=kind, i=it: ops.mk_mma(k, blocks, 8, i, sink), reps))
|
| 276 |
+
flops = blocks * 8 * it * 4 * 2 * mnk
|
| 277 |
+
mma[name] = round(flops / (ms * 1e-3) / 1e12, 1)
|
| 278 |
+
d["mma"] = _entry(mma, "TFLOP/s (TOP/s for int8)",
|
| 279 |
+
"register-fragment mma_sync loops (wmma; fp8 via "
|
| 280 |
+
"inline PTX m16n8k32)", clk, "medium",
|
| 281 |
+
"dense-tile rate without memory traffic; real "
|
| 282 |
+
"kernels see this only when staging hides behind it")
|
| 283 |
+
|
| 284 |
+
# ---- atomic contention curve: throughput vs number of distinct
|
| 285 |
+
# addresses the whole grid hammers (slots = 1 is full contention;
|
| 286 |
+
# slots = threads is contention-free), plus the shared-memory point
|
| 287 |
+
n_thr = blocks * 256
|
| 288 |
+
buf = torch.zeros(n_thr, dtype=torch.float32, device=device)
|
| 289 |
+
at = {"curve_slots": {}}
|
| 290 |
+
it_at = 2_000 if quick else 6_000
|
| 291 |
+
slot_points = [1, 8, 64, 512, 4096, n_thr]
|
| 292 |
+
for slots in slot_points:
|
| 293 |
+
buf.zero_()
|
| 294 |
+
ops.mk_atomics(1, blocks, 256, it_at, buf, slots)
|
| 295 |
+
ms = _median(_events(
|
| 296 |
+
lambda s=slots: ops.mk_atomics(1, blocks, 256, it_at, buf, s),
|
| 297 |
+
reps))
|
| 298 |
+
at["curve_slots"][str(slots)] = round(
|
| 299 |
+
n_thr * it_at / (ms * 1e-3) / 1e9, 2)
|
| 300 |
+
ops.mk_atomics(3, blocks, 256, it_at, buf, 1)
|
| 301 |
+
ms = _median(_events(
|
| 302 |
+
lambda: ops.mk_atomics(3, blocks, 256, it_at, buf, 1), reps))
|
| 303 |
+
at["shared_same_addr"] = round(n_thr * it_at / (ms * 1e-3) / 1e9, 2)
|
| 304 |
+
d["atomics_f32"] = _entry(at, "Gop/s",
|
| 305 |
+
"atomicAdd hammer; curve sweeps the number "
|
| 306 |
+
"of distinct global addresses from 1 (full "
|
| 307 |
+
"contention) to one per thread", clk, "high")
|
| 308 |
+
|
| 309 |
+
# ---- launch overhead
|
| 310 |
+
anchor = sink
|
| 311 |
+
n_launch = 500 if quick else 2000
|
| 312 |
+
_sync()
|
| 313 |
+
t0 = time.perf_counter()
|
| 314 |
+
for _ in range(n_launch):
|
| 315 |
+
ops.mk_empty(anchor)
|
| 316 |
+
_sync()
|
| 317 |
+
queued = (time.perf_counter() - t0) / n_launch * 1e6
|
| 318 |
+
n_rt = 100 if quick else 300
|
| 319 |
+
t0 = time.perf_counter()
|
| 320 |
+
for _ in range(n_rt):
|
| 321 |
+
ops.mk_empty(anchor)
|
| 322 |
+
_sync()
|
| 323 |
+
rt = (time.perf_counter() - t0) / n_rt * 1e6
|
| 324 |
+
d["launch_overhead"] = _entry(
|
| 325 |
+
{"queued_us": round(queued, 2), "round_trip_us": round(rt, 2)},
|
| 326 |
+
"us", "empty-kernel trains, queued vs launch+sync", clk, "high",
|
| 327 |
+
"host-dependent: includes torch op dispatch")
|
| 328 |
+
|
| 329 |
+
# ---- grid barrier
|
| 330 |
+
bar_iters = 500 if quick else 2000
|
| 331 |
+
r = ops.mk_barrier(sm, 256, bar_iters, out64)
|
| 332 |
+
if r == 0:
|
| 333 |
+
_sync()
|
| 334 |
+
ticks = int(out64[0].item())
|
| 335 |
+
d["grid_barrier"] = _entry(
|
| 336 |
+
round(ticks / bar_iters / clk / 1000.0, 2), "us per grid.sync",
|
| 337 |
+
f"cooperative kernel, {sm} blocks x 256 threads", clk, "high")
|
| 338 |
+
else:
|
| 339 |
+
d["grid_barrier"] = _entry("unsupported", "", "cooperative launch "
|
| 340 |
+
"unavailable or grid too large", clk,
|
| 341 |
+
"high")
|
| 342 |
+
|
| 343 |
+
# ---- occupancy table (API) plus timed knees: the same random gather
|
| 344 |
+
# at every (threads, dyn_smem) point; when the forced blocks/SM stop
|
| 345 |
+
# covering memory latency, achieved bandwidth falls off the cliff
|
| 346 |
+
occ = {}
|
| 347 |
+
for threads in (128, 256, 512, 1024):
|
| 348 |
+
row = {}
|
| 349 |
+
for smem_kb in (0, 16, 32, 48):
|
| 350 |
+
row[f"{smem_kb}KB"] = int(ops.mk_occupancy(
|
| 351 |
+
threads, smem_kb * 1024, anchor))
|
| 352 |
+
occ[f"t{threads}"] = row
|
| 353 |
+
d["occupancy_blocks_per_sm"] = _entry(
|
| 354 |
+
occ, "blocks/SM", "occupancy API on the float4 triad kernel",
|
| 355 |
+
clk, "high", "API-derived, not timing-derived")
|
| 356 |
+
|
| 357 |
+
knee_iters = 6_000 if quick else 20_000
|
| 358 |
+
knee_blocks = sm * 8
|
| 359 |
+
knees = {}
|
| 360 |
+
for threads in (128, 256, 1024):
|
| 361 |
+
row = {}
|
| 362 |
+
for smem_kb in (0, 32, 48):
|
| 363 |
+
ops.mk_fma_dep(knee_blocks, threads, smem_kb * 1024,
|
| 364 |
+
knee_iters, sink)
|
| 365 |
+
ms = _median(_events(
|
| 366 |
+
lambda t=threads, s=smem_kb: ops.mk_fma_dep(
|
| 367 |
+
knee_blocks, t, s * 1024, knee_iters, sink), reps))
|
| 368 |
+
fl = knee_blocks * threads * knee_iters * 16 * 2
|
| 369 |
+
row[f"{smem_kb}KB"] = round(fl / (ms * 1e-3) / 1e12, 2)
|
| 370 |
+
knees[f"t{threads}"] = row
|
| 371 |
+
d["occupancy_knees_fma_tflops"] = _entry(
|
| 372 |
+
knees, "TFLOP/s", "a single dependent FMA chain per thread, timed "
|
| 373 |
+
"at forced (threads, dyn smem) occupancies over fixed total work; "
|
| 374 |
+
"throughput is proportional to resident threads until the pipe "
|
| 375 |
+
"saturates, so the cliff marks where occupancy stops covering "
|
| 376 |
+
"latency", clk, "high",
|
| 377 |
+
"timing-derived counterpart to the API table above")
|
| 378 |
+
return d
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def bench(fn, iters=100, warmup=10, bytes=None, flops=None, dossier=None,
|
| 382 |
+
dtype="fp16", clock_guard=0.97, clock_stride=8, graph=False,
|
| 383 |
+
device="cuda"):
|
| 384 |
+
"""Median-of-N cudaEvent timing with throttle rejection.
|
| 385 |
+
|
| 386 |
+
The SM clock is probed every `clock_stride` samples; samples inside a
|
| 387 |
+
window whose clock sagged below `clock_guard` x the best observed
|
| 388 |
+
clock are rejected and counted rather than silently averaged. With
|
| 389 |
+
declared `bytes`/`flops` per call, reports achieved GB/s / TFLOP/s;
|
| 390 |
+
with a `dossier`, adds roofline placement against the measured peaks.
|
| 391 |
+
|
| 392 |
+
`graph=True` captures fn() into a CUDA graph and times graph replays,
|
| 393 |
+
stripping Python and torch dispatch from the measurement: the right
|
| 394 |
+
mode for kernels in the few-microsecond range, where dispatch is
|
| 395 |
+
otherwise the thing being measured. Capture requires fn to be
|
| 396 |
+
graph-safe (no sync, no allocation growth, static shapes)."""
|
| 397 |
+
for _ in range(warmup):
|
| 398 |
+
fn()
|
| 399 |
+
_sync()
|
| 400 |
+
if graph:
|
| 401 |
+
g = torch.cuda.CUDAGraph()
|
| 402 |
+
with torch.cuda.graph(g):
|
| 403 |
+
fn()
|
| 404 |
+
fn = g.replay
|
| 405 |
+
for _ in range(warmup):
|
| 406 |
+
fn()
|
| 407 |
+
_sync()
|
| 408 |
+
samples = []
|
| 409 |
+
clocks = []
|
| 410 |
+
stride = clock_stride
|
| 411 |
+
for base in range(0, iters, stride):
|
| 412 |
+
clocks.append(measure_clock(device, ticks=1_500_000))
|
| 413 |
+
n = min(stride, iters - base)
|
| 414 |
+
samples.append((_events(fn, n), len(clocks) - 1))
|
| 415 |
+
clocks.append(measure_clock(device, ticks=1_500_000))
|
| 416 |
+
best_clk = max(clocks)
|
| 417 |
+
kept, rejected = [], 0
|
| 418 |
+
flat = []
|
| 419 |
+
for group, ci in samples:
|
| 420 |
+
window_ok = (clocks[ci] >= clock_guard * best_clk and
|
| 421 |
+
clocks[min(ci + 1, len(clocks) - 1)] >=
|
| 422 |
+
clock_guard * best_clk)
|
| 423 |
+
for ms in group:
|
| 424 |
+
flat.append(ms)
|
| 425 |
+
if window_ok:
|
| 426 |
+
kept.append(ms)
|
| 427 |
+
else:
|
| 428 |
+
rejected += 1
|
| 429 |
+
if not kept:
|
| 430 |
+
kept = flat
|
| 431 |
+
med = _median(kept)
|
| 432 |
+
rep = {
|
| 433 |
+
"median_ms": round(med, 4),
|
| 434 |
+
"min_ms": round(min(kept), 4),
|
| 435 |
+
"mean_ms": round(sum(kept) / len(kept), 4),
|
| 436 |
+
"samples": len(flat),
|
| 437 |
+
"rejected_throttled": rejected,
|
| 438 |
+
"graph_captured": bool(graph),
|
| 439 |
+
"clock_ghz": {"best": round(best_clk, 3),
|
| 440 |
+
"worst": round(min(clocks), 3)},
|
| 441 |
+
}
|
| 442 |
+
if bytes is not None:
|
| 443 |
+
rep["achieved_gbs"] = round(bytes / (med * 1e-3) / 1e9, 1)
|
| 444 |
+
if flops is not None:
|
| 445 |
+
rep["achieved_tflops"] = round(flops / (med * 1e-3) / 1e12, 2)
|
| 446 |
+
if bytes and flops:
|
| 447 |
+
rep["arithmetic_intensity"] = round(flops / bytes, 2)
|
| 448 |
+
if dossier is not None and bytes and flops:
|
| 449 |
+
bw = max(dossier["hbm_bw"]["value"].values()) * 1e9
|
| 450 |
+
mma_v = dossier["mma"]["value"].get(dtype)
|
| 451 |
+
peak = (mma_v if isinstance(mma_v, (int, float))
|
| 452 |
+
else dossier["fma_f32"]["value"]) * 1e12
|
| 453 |
+
ai = flops / bytes
|
| 454 |
+
roof = min(peak, ai * bw)
|
| 455 |
+
rep["roofline"] = {
|
| 456 |
+
"bound": "memory" if ai * bw < peak else "compute",
|
| 457 |
+
"ridge_flops_per_byte": round(peak / bw, 1),
|
| 458 |
+
"pct_of_roof": round(100 * (flops / (med * 1e-3)) / roof, 1),
|
| 459 |
+
"peak_dtype": dtype,
|
| 460 |
+
}
|
| 461 |
+
return rep
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
def _to_ordered_int(t):
|
| 465 |
+
"""Map float bit patterns to integers whose difference is the ULP
|
| 466 |
+
distance: positive floats keep their bits, negative floats mirror
|
| 467 |
+
below zero, so the mapping is monotone in the float value."""
|
| 468 |
+
if t.dtype == torch.float32:
|
| 469 |
+
u = t.view(torch.int32).to(torch.int64) & 0xFFFFFFFF
|
| 470 |
+
half = 1 << 31
|
| 471 |
+
elif t.dtype in (torch.float16, torch.bfloat16):
|
| 472 |
+
u = t.view(torch.int16).to(torch.int64) & 0xFFFF
|
| 473 |
+
half = 1 << 15
|
| 474 |
+
elif t.dtype == torch.float64:
|
| 475 |
+
i = t.view(torch.int64)
|
| 476 |
+
return torch.where(i >= 0, i, (-(1 << 63)) - i)
|
| 477 |
+
else:
|
| 478 |
+
raise ValueError(f"unsupported dtype {t.dtype}")
|
| 479 |
+
return torch.where(u < half, u, half - u)
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
def ulp_diff(a, b):
|
| 483 |
+
"""Elementwise ULP distance between same-dtype float tensors. NaNs in
|
| 484 |
+
the same position count as 0; a one-sided NaN counts as 2**62."""
|
| 485 |
+
assert a.dtype == b.dtype and a.shape == b.shape
|
| 486 |
+
ia, ib = _to_ordered_int(a), _to_ordered_int(b)
|
| 487 |
+
d = (ia - ib).abs()
|
| 488 |
+
both_nan = torch.isnan(a) & torch.isnan(b)
|
| 489 |
+
one_nan = torch.isnan(a) ^ torch.isnan(b)
|
| 490 |
+
d = torch.where(both_nan, torch.zeros_like(d), d)
|
| 491 |
+
d = torch.where(one_nan, torch.full_like(d, 1 << 62), d)
|
| 492 |
+
return d
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
class order_shuffles:
|
| 496 |
+
"""Built-in order shuffles for the common contractual-noise axes."""
|
| 497 |
+
|
| 498 |
+
@staticmethod
|
| 499 |
+
def last_dim():
|
| 500 |
+
"""Permute the last dim of every tensor arg identically: the
|
| 501 |
+
accumulation order of a last-dim reduction changes, the value
|
| 502 |
+
does not."""
|
| 503 |
+
def shuffle(args, seed):
|
| 504 |
+
g = torch.Generator(device=args[0].device).manual_seed(seed)
|
| 505 |
+
p = torch.randperm(args[0].shape[-1], device=args[0].device,
|
| 506 |
+
generator=g)
|
| 507 |
+
return tuple(x[..., p] for x in args)
|
| 508 |
+
return shuffle
|
| 509 |
+
|
| 510 |
+
@staticmethod
|
| 511 |
+
def matmul_k():
|
| 512 |
+
"""Permute K of (A [.., M, K], B [.., K, N]) consistently: the
|
| 513 |
+
product is unchanged, the accumulation order is not."""
|
| 514 |
+
def shuffle(args, seed):
|
| 515 |
+
a_m, b_m = args[0], args[1]
|
| 516 |
+
g = torch.Generator(device=a_m.device).manual_seed(seed)
|
| 517 |
+
p = torch.randperm(a_m.shape[-1], device=a_m.device,
|
| 518 |
+
generator=g)
|
| 519 |
+
return (a_m[..., p], b_m[..., p, :]) + tuple(args[2:])
|
| 520 |
+
return shuffle
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
def _as_outputs(x):
|
| 524 |
+
return list(x) if isinstance(x, (tuple, list)) else [x]
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
def compare(ref, cand, gen, trials=8, order_shuffle=None, order_trials=8,
|
| 528 |
+
ulp_tol=None, atol=None, rtol=None, band_margin=4.0, seed=0):
|
| 529 |
+
"""Adversarial correctness comparison with ULP evidence.
|
| 530 |
+
|
| 531 |
+
`gen(seed) -> args` builds inputs; `ref(*args)` and `cand(*args)`
|
| 532 |
+
return a tensor or a tuple of tensors (each output is compared and
|
| 533 |
+
the worst governs). If `order_shuffle(args, seed) -> args` is given
|
| 534 |
+
and reorders contractual-noise dimensions (see `order_shuffles` for
|
| 535 |
+
the common cases), the reference is treated as a distribution:
|
| 536 |
+
candidates are judged against the measured reduction-order band
|
| 537 |
+
rather than a point. Verdict, stated in the report: max ULP <=
|
| 538 |
+
band_margin * band + 2 when a band is measured, else max ULP <=
|
| 539 |
+
ulp_tol (default 2); if `atol`/`rtol` are given they are an
|
| 540 |
+
alternative acceptance route, checked as |c - r| <= atol + rtol|r|."""
|
| 541 |
+
worst = {"max_ulp": -1}
|
| 542 |
+
total_hist = {}
|
| 543 |
+
per_trial = []
|
| 544 |
+
max_abs_seen = 0.0
|
| 545 |
+
max_rel_seen = 0.0
|
| 546 |
+
for t in range(trials):
|
| 547 |
+
args = gen(seed + t)
|
| 548 |
+
r_outs = _as_outputs(ref(*args))
|
| 549 |
+
c_outs = _as_outputs(cand(*args))
|
| 550 |
+
assert len(r_outs) == len(c_outs), "output arity mismatch"
|
| 551 |
+
# the order band is POINTWISE: a near-zero reduction output is
|
| 552 |
+
# legitimately thousands of ULP order-sensitive (cancellation)
|
| 553 |
+
# while its neighbors are stable, so each element is judged
|
| 554 |
+
# against its own measured sensitivity, not a global max
|
| 555 |
+
bands = None
|
| 556 |
+
band = 0
|
| 557 |
+
band_score = 0.0
|
| 558 |
+
if order_shuffle is not None:
|
| 559 |
+
bands = [torch.zeros_like(ro, dtype=torch.int64)
|
| 560 |
+
for ro in r_outs]
|
| 561 |
+
for k in range(order_trials):
|
| 562 |
+
r2 = _as_outputs(
|
| 563 |
+
ref(*order_shuffle(args, 1000 + seed + t * 131 + k)))
|
| 564 |
+
for bi, (ro, r2o) in enumerate(zip(r_outs, r2)):
|
| 565 |
+
bands[bi] = torch.maximum(bands[bi],
|
| 566 |
+
ulp_diff(ro, r2o))
|
| 567 |
+
band = max(int(b.max().item()) if b.numel() else 0
|
| 568 |
+
for b in bands)
|
| 569 |
+
max_ulp = 0
|
| 570 |
+
mean_ulp = 0.0
|
| 571 |
+
first_div = -1
|
| 572 |
+
n_el = 0
|
| 573 |
+
nan_r = nan_c = inf_r = inf_c = 0
|
| 574 |
+
for oi, (r, c) in enumerate(zip(r_outs, c_outs)):
|
| 575 |
+
d = ulp_diff(r, c)
|
| 576 |
+
if bands is not None and d.numel():
|
| 577 |
+
ratio = d.double() / (bands[oi].double() + 2.0)
|
| 578 |
+
band_score = max(band_score, float(ratio.max().item()))
|
| 579 |
+
if d.numel():
|
| 580 |
+
max_ulp = max(max_ulp, int(d.max().item()))
|
| 581 |
+
mean_ulp += float(d.double().sum().item())
|
| 582 |
+
n_el += d.numel()
|
| 583 |
+
nz = (d > 0)
|
| 584 |
+
if first_div < 0 and nz.any():
|
| 585 |
+
first_div = int(nz.flatten().nonzero()[0].item())
|
| 586 |
+
if nz.any():
|
| 587 |
+
buckets = torch.clamp(
|
| 588 |
+
torch.log2(d[nz].double()).floor(), 0, 62
|
| 589 |
+
).to(torch.int64)
|
| 590 |
+
counts = torch.bincount(buckets, minlength=63)
|
| 591 |
+
for bkt in counts.nonzero().flatten().tolist():
|
| 592 |
+
total_hist[bkt] = total_hist.get(bkt, 0) + int(
|
| 593 |
+
counts[bkt].item())
|
| 594 |
+
fin = torch.isfinite(r) & torch.isfinite(c)
|
| 595 |
+
if fin.any():
|
| 596 |
+
diff = (c[fin].double() - r[fin].double()).abs()
|
| 597 |
+
max_abs_seen = max(max_abs_seen,
|
| 598 |
+
float(diff.max().item()))
|
| 599 |
+
rel = diff / r[fin].double().abs().clamp(min=1e-300)
|
| 600 |
+
max_rel_seen = max(max_rel_seen,
|
| 601 |
+
float(rel.max().item()))
|
| 602 |
+
nan_r += int(torch.isnan(r).sum().item())
|
| 603 |
+
nan_c += int(torch.isnan(c).sum().item())
|
| 604 |
+
inf_r += int(torch.isinf(r).sum().item())
|
| 605 |
+
inf_c += int(torch.isinf(c).sum().item())
|
| 606 |
+
trial_rep = {
|
| 607 |
+
"max_ulp": max_ulp,
|
| 608 |
+
"mean_ulp": round(mean_ulp / max(n_el, 1), 3),
|
| 609 |
+
"first_divergent_index": first_div,
|
| 610 |
+
"nan_ref": nan_r, "nan_cand": nan_c,
|
| 611 |
+
"inf_ref": inf_r, "inf_cand": inf_c,
|
| 612 |
+
"order_band_ulp": band,
|
| 613 |
+
"band_score": round(band_score, 2),
|
| 614 |
+
}
|
| 615 |
+
per_trial.append(trial_rep)
|
| 616 |
+
if max_ulp > worst["max_ulp"]:
|
| 617 |
+
worst = trial_rep
|
| 618 |
+
band_max = max(t["order_band_ulp"] for t in per_trial)
|
| 619 |
+
max_seen = max(t["max_ulp"] for t in per_trial)
|
| 620 |
+
if order_shuffle is not None:
|
| 621 |
+
score = max(t["band_score"] for t in per_trial)
|
| 622 |
+
ulp_ok = score <= band_margin
|
| 623 |
+
criterion = (f"pointwise max(ulp / (order_band + 2)) = "
|
| 624 |
+
f"{score:.2f} <= band_margin {band_margin}")
|
| 625 |
+
else:
|
| 626 |
+
tol = ulp_tol if ulp_tol is not None else 2
|
| 627 |
+
ulp_ok = max_seen <= tol
|
| 628 |
+
criterion = f"max_ulp {max_seen} <= ulp_tol {tol}"
|
| 629 |
+
tol_ok = False
|
| 630 |
+
if atol is not None or rtol is not None:
|
| 631 |
+
tol_ok = max_abs_seen <= (atol or 0) + (rtol or 0) * 1.0 or \
|
| 632 |
+
max_rel_seen <= (rtol or 0) or max_abs_seen <= (atol or 0)
|
| 633 |
+
criterion += (f"; OR max_abs {max_abs_seen:.3e} <= atol {atol} / "
|
| 634 |
+
f"max_rel {max_rel_seen:.3e} <= rtol {rtol}")
|
| 635 |
+
return {
|
| 636 |
+
"pass": bool(ulp_ok or tol_ok),
|
| 637 |
+
"criterion": criterion,
|
| 638 |
+
"worst_trial": worst,
|
| 639 |
+
"max_abs": max_abs_seen,
|
| 640 |
+
"max_rel": max_rel_seen,
|
| 641 |
+
"trials": per_trial,
|
| 642 |
+
"ulp_hist_log2": dict(sorted(total_hist.items())),
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
|
| 646 |
+
_FUZZ_VARIANTS = ("contig", "transposed", "strided_rows", "offset_slice",
|
| 647 |
+
"overflow_expand")
|
| 648 |
+
|
| 649 |
+
|
| 650 |
+
def _context_alive(device):
|
| 651 |
+
try:
|
| 652 |
+
t = torch.ones(4, device=device)
|
| 653 |
+
torch.cuda.synchronize()
|
| 654 |
+
return bool(t.sum().item() == 4.0)
|
| 655 |
+
except Exception: # noqa: BLE001
|
| 656 |
+
return False
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
def fuzz_shapes(op, ref, make, shapes=None, variants=_FUZZ_VARIANTS,
|
| 660 |
+
dtypes=(torch.float32,), ulp_tol=64, device="cuda"):
|
| 661 |
+
"""The ugly envelope: run op vs ref across edge shapes, dtypes, and
|
| 662 |
+
layout mutations; report ok / mismatch / exception per case.
|
| 663 |
+
|
| 664 |
+
`make(shape, dtype)` (or `make(shape)`) builds a base input, either a
|
| 665 |
+
tensor or a tuple of tensors for multi-argument ops; layout variants
|
| 666 |
+
derive non-contiguous and misaligned views whose CONTENT equals a
|
| 667 |
+
contiguous copy, so `op(*xs)` must match `ref(*xs)` on every variant.
|
| 668 |
+
`overflow_expand` stride-0-expands the input past 2**31 logical
|
| 669 |
+
elements to catch int32 indexing, and is skipped with status
|
| 670 |
+
`skipped_vram` when materializing the output would not fit. After any
|
| 671 |
+
exception the CUDA context is health-checked; if a candidate killed
|
| 672 |
+
it, remaining cases are marked `context_lost` instead of producing
|
| 673 |
+
garbage."""
|
| 674 |
+
if shapes is None:
|
| 675 |
+
shapes = [(0, 8), (1, 1), (1, 17), (3, 63), (17, 129), (128, 128),
|
| 676 |
+
(1023, 5), (2, 4096)]
|
| 677 |
+
|
| 678 |
+
def build(shape, dtype):
|
| 679 |
+
try:
|
| 680 |
+
out = make(tuple(shape), dtype)
|
| 681 |
+
except TypeError:
|
| 682 |
+
out = make(tuple(shape))
|
| 683 |
+
return out if isinstance(out, (tuple, list)) else (out,)
|
| 684 |
+
|
| 685 |
+
def variant_of(x, v):
|
| 686 |
+
if v == "contig":
|
| 687 |
+
return x
|
| 688 |
+
if v == "transposed":
|
| 689 |
+
return x.t() if x.dim() == 2 else x
|
| 690 |
+
if v == "strided_rows":
|
| 691 |
+
return x[::2] if x.shape[0] >= 2 else x
|
| 692 |
+
if v == "offset_slice":
|
| 693 |
+
# a storage-offset-1 view: misaligned for vectorized loads
|
| 694 |
+
return x.flatten()[1:] if x.numel() > 1 else x
|
| 695 |
+
if v == "overflow_expand":
|
| 696 |
+
if x.numel() == 0:
|
| 697 |
+
return None
|
| 698 |
+
per = max(x.numel(), 1)
|
| 699 |
+
rep = (1 << 31) // per + 2
|
| 700 |
+
return x.reshape((1,) + tuple(x.shape)).expand(
|
| 701 |
+
(rep,) + tuple(x.shape))
|
| 702 |
+
return x
|
| 703 |
+
|
| 704 |
+
rows = []
|
| 705 |
+
lost = False
|
| 706 |
+
for dtype in dtypes:
|
| 707 |
+
for shape in shapes:
|
| 708 |
+
bases = build(shape, dtype)
|
| 709 |
+
for v in variants:
|
| 710 |
+
case = {"shape": tuple(shape), "variant": v,
|
| 711 |
+
"dtype": str(dtype).replace("torch.", "")}
|
| 712 |
+
if lost:
|
| 713 |
+
case["status"] = "context_lost"
|
| 714 |
+
rows.append(case)
|
| 715 |
+
continue
|
| 716 |
+
xs = [variant_of(b, v) for b in bases]
|
| 717 |
+
if any(x is None for x in xs):
|
| 718 |
+
case["status"] = "skipped_empty"
|
| 719 |
+
rows.append(case)
|
| 720 |
+
continue
|
| 721 |
+
if v == "overflow_expand":
|
| 722 |
+
logical = max(x.numel() for x in xs)
|
| 723 |
+
need = logical * xs[0].element_size() * 2
|
| 724 |
+
free, _total = torch.cuda.mem_get_info()
|
| 725 |
+
if need > free * 0.8:
|
| 726 |
+
case["status"] = "skipped_vram"
|
| 727 |
+
case["logical_elems"] = int(logical)
|
| 728 |
+
rows.append(case)
|
| 729 |
+
continue
|
| 730 |
+
case["logical_elems"] = int(logical)
|
| 731 |
+
case["contiguous"] = bool(xs[0].is_contiguous())
|
| 732 |
+
try:
|
| 733 |
+
got = op(*xs)
|
| 734 |
+
want = ref(*xs)
|
| 735 |
+
if v == "overflow_expand":
|
| 736 |
+
# bounded comparison: the head plus the high-index
|
| 737 |
+
# tail, which is where int32 flat indexing wraps
|
| 738 |
+
w = 1 << 20
|
| 739 |
+
gf, wf = got.flatten(), want.flatten()
|
| 740 |
+
pairs = ((gf[:w], wf[:w]), (gf[-w:], wf[-w:]))
|
| 741 |
+
else:
|
| 742 |
+
pairs = ((got, want),)
|
| 743 |
+
mx = 0
|
| 744 |
+
for gp, wp in pairs:
|
| 745 |
+
d = ulp_diff(wp, gp) if gp.is_floating_point() \
|
| 746 |
+
else (wp != gp).to(torch.int64) * (1 << 62)
|
| 747 |
+
if d.numel():
|
| 748 |
+
mx = max(mx, int(d.max().item()))
|
| 749 |
+
del d
|
| 750 |
+
case["status"] = "ok" if mx <= ulp_tol else "mismatch"
|
| 751 |
+
case["max_ulp"] = mx
|
| 752 |
+
del got, want
|
| 753 |
+
except Exception as e: # noqa: BLE001 - report, don't die
|
| 754 |
+
case["status"] = "exception"
|
| 755 |
+
case["error"] = f"{type(e).__name__}: {str(e)[:120]}"
|
| 756 |
+
if not _context_alive(device):
|
| 757 |
+
lost = True
|
| 758 |
+
case["status"] = "context_killed"
|
| 759 |
+
rows.append(case)
|
| 760 |
+
n_bad = sum(1 for r in rows
|
| 761 |
+
if r["status"] not in ("ok", "skipped_vram",
|
| 762 |
+
"skipped_empty"))
|
| 763 |
+
return {"cases": rows, "failed": n_bad, "total": len(rows),
|
| 764 |
+
"context_lost": lost}
|
| 765 |
+
|
| 766 |
+
|
| 767 |
+
def sweep(factory, grid, iters=30, warmup=5, bytes=None, flops=None,
|
| 768 |
+
check=None, budget_s=None):
|
| 769 |
+
"""Launch-configuration search: `factory(**point) -> callable`; one
|
| 770 |
+
bench row per grid point, sorted fastest first. A point whose factory
|
| 771 |
+
or run raises is reported, not skipped silently. `check(fn) -> bool`
|
| 772 |
+
is an optional correctness gate recorded per row (a fast wrong config
|
| 773 |
+
must not win a sweep). `budget_s` caps total sweep time; points not
|
| 774 |
+
reached are reported as `not_run` rather than silently dropped."""
|
| 775 |
+
keys = list(grid.keys())
|
| 776 |
+
|
| 777 |
+
def points(i, acc):
|
| 778 |
+
if i == len(keys):
|
| 779 |
+
yield dict(acc)
|
| 780 |
+
return
|
| 781 |
+
for v in grid[keys[i]]:
|
| 782 |
+
acc[keys[i]] = v
|
| 783 |
+
yield from points(i + 1, acc)
|
| 784 |
+
|
| 785 |
+
rows = []
|
| 786 |
+
t_start = time.perf_counter()
|
| 787 |
+
over_budget = False
|
| 788 |
+
for p in points(0, {}):
|
| 789 |
+
row = dict(p)
|
| 790 |
+
if over_budget or (budget_s is not None and
|
| 791 |
+
time.perf_counter() - t_start > budget_s):
|
| 792 |
+
over_budget = True
|
| 793 |
+
row["status"] = "not_run"
|
| 794 |
+
row["median_ms"] = float("inf")
|
| 795 |
+
rows.append(row)
|
| 796 |
+
continue
|
| 797 |
+
try:
|
| 798 |
+
fn = factory(**p)
|
| 799 |
+
if check is not None:
|
| 800 |
+
row["check"] = bool(check(fn))
|
| 801 |
+
rep = bench(fn, iters=iters, warmup=warmup, bytes=bytes,
|
| 802 |
+
flops=flops)
|
| 803 |
+
row.update({"median_ms": rep["median_ms"],
|
| 804 |
+
"rejected_throttled": rep["rejected_throttled"]})
|
| 805 |
+
if "achieved_gbs" in rep:
|
| 806 |
+
row["achieved_gbs"] = rep["achieved_gbs"]
|
| 807 |
+
if "achieved_tflops" in rep:
|
| 808 |
+
row["achieved_tflops"] = rep["achieved_tflops"]
|
| 809 |
+
except Exception as e: # noqa: BLE001
|
| 810 |
+
row["error"] = f"{type(e).__name__}: {str(e)[:120]}"
|
| 811 |
+
row["median_ms"] = float("inf")
|
| 812 |
+
rows.append(row)
|
| 813 |
+
rows.sort(key=lambda r: (not r.get("check", True), r["median_ms"]))
|
| 814 |
+
return rows
|
| 815 |
+
|
| 816 |
+
|
| 817 |
+
def alloc_stamps(n, device="cuda"):
|
| 818 |
+
"""Device buffer for the MK_STAMP convention (metakernel_cuda/
|
| 819 |
+
stamps.cuh): the candidate kernel writes clock64() into slot i."""
|
| 820 |
+
return torch.zeros(n, dtype=torch.int64, device=device)
|
| 821 |
+
|
| 822 |
+
|
| 823 |
+
def read_stamps(stamps, labels=None, clock_ghz=None):
|
| 824 |
+
"""Decode MK_STAMP phases: consecutive deltas in ticks and in
|
| 825 |
+
microseconds under the measured (or provided) SM clock. A 1-D buffer
|
| 826 |
+
(MK_STAMP: block 0 only) yields one delta per phase; a 2-D [B, n]
|
| 827 |
+
buffer (MK_STAMP_BLOCK) yields min / median / max across blocks per
|
| 828 |
+
phase, with blocks whose stamps stayed zero excluded."""
|
| 829 |
+
if clock_ghz is None:
|
| 830 |
+
clock_ghz = measure_clock(stamps.device)
|
| 831 |
+
|
| 832 |
+
def us(ticks):
|
| 833 |
+
return round(ticks / clock_ghz / 1000.0, 3)
|
| 834 |
+
|
| 835 |
+
if stamps.dim() == 1:
|
| 836 |
+
vals = stamps.cpu().tolist()
|
| 837 |
+
phases = []
|
| 838 |
+
for i in range(len(vals) - 1):
|
| 839 |
+
ticks = vals[i + 1] - vals[i]
|
| 840 |
+
name = labels[i] if labels and i < len(labels) else f"phase_{i}"
|
| 841 |
+
phases.append({"phase": name, "ticks": ticks, "us": us(ticks)})
|
| 842 |
+
return {"phases": phases, "clock_ghz": round(clock_ghz, 3)}
|
| 843 |
+
|
| 844 |
+
m = stamps.cpu()
|
| 845 |
+
live = (m != 0).all(dim=1)
|
| 846 |
+
m = m[live]
|
| 847 |
+
deltas = (m[:, 1:] - m[:, :-1]).to(torch.float64)
|
| 848 |
+
phases = []
|
| 849 |
+
for i in range(deltas.shape[1]):
|
| 850 |
+
col = deltas[:, i]
|
| 851 |
+
name = labels[i] if labels and i < len(labels) else f"phase_{i}"
|
| 852 |
+
med = float(col.median().item()) if col.numel() else 0.0
|
| 853 |
+
phases.append({
|
| 854 |
+
"phase": name,
|
| 855 |
+
"ticks_median": int(med),
|
| 856 |
+
"ticks_min": int(col.min().item()) if col.numel() else 0,
|
| 857 |
+
"ticks_max": int(col.max().item()) if col.numel() else 0,
|
| 858 |
+
"us_median": us(med),
|
| 859 |
+
"blocks": int(col.numel()),
|
| 860 |
+
})
|
| 861 |
+
return {"phases": phases, "clock_ghz": round(clock_ghz, 3),
|
| 862 |
+
"blocks_reporting": int(live.sum().item()),
|
| 863 |
+
"blocks_total": int(live.numel())}
|
build/torch211-cxx11-cu126-x86_64-linux/_metakernel_cuda_4611d12.abi3.so
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:474c2f8fa87fd25a548c07014c46827295f5b2eace19b7e190f0090fa61e8877
|
| 3 |
+
size 626176
|
build/torch211-cxx11-cu126-x86_64-linux/_ops.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from . import _metakernel_cuda_4611d12
|
| 3 |
+
ops = torch.ops._metakernel_cuda_4611d12
|
| 4 |
+
|
| 5 |
+
def add_op_namespace_prefix(op_name: str):
|
| 6 |
+
"""
|
| 7 |
+
Prefix op by namespace.
|
| 8 |
+
"""
|
| 9 |
+
return f"_metakernel_cuda_4611d12::{op_name}"
|
build/torch211-cxx11-cu126-x86_64-linux/metadata.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "metakernel",
|
| 3 |
+
"id": "_metakernel_cuda_4611d12",
|
| 4 |
+
"version": 1,
|
| 5 |
+
"license": "Apache-2.0",
|
| 6 |
+
"python-depends": [],
|
| 7 |
+
"backend": {
|
| 8 |
+
"type": "cuda",
|
| 9 |
+
"archs": [
|
| 10 |
+
"8.0",
|
| 11 |
+
"8.6",
|
| 12 |
+
"8.9",
|
| 13 |
+
"9.0"
|
| 14 |
+
]
|
| 15 |
+
},
|
| 16 |
+
"digest": {
|
| 17 |
+
"algorithm": "sha256",
|
| 18 |
+
"files": {
|
| 19 |
+
"__init__.py": "cZxKX5I/2EKCMC2gdCwflUsZaQJWJesmAHq2w+fVjXo=",
|
| 20 |
+
"_metakernel_cuda_4611d12.abi3.so": "R0wvj6h/0lpUjAcBTEaCcpX1surOGbfhkPAJD6YeiHc=",
|
| 21 |
+
"_ops.py": "uZlR720EcTy8M7BCydmvaZAlKonkwTU3778nhR9yQOE=",
|
| 22 |
+
"metakernel/__init__.py": "DFYPlrhXwYjEqCl/8n0SmWGZV8NFml5DPhMjKfv98GY="
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"provenance": {
|
| 26 |
+
"kernel-builder": {
|
| 27 |
+
"version": "0.17.0-dev0",
|
| 28 |
+
"sha": "19aaa6421e674e9fecc352bbae6eab81d19a6bf4",
|
| 29 |
+
"dirty": false
|
| 30 |
+
},
|
| 31 |
+
"kernel": {
|
| 32 |
+
"sha": "4611d1213a76a9ec1d12646553a528feb60394ff",
|
| 33 |
+
"dirty": false
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
}
|
build/torch211-cxx11-cu126-x86_64-linux/metakernel/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ctypes
|
| 2 |
+
import importlib.util
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from types import ModuleType
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def _import_from_path(file_path: Path) -> ModuleType:
|
| 9 |
+
# We cannot use the module name as-is, after adding it to `sys.modules`,
|
| 10 |
+
# it would also be used for other imports. So, we make a module name that
|
| 11 |
+
# depends on the path for it to be unique using the hex-encoded hash of
|
| 12 |
+
# the path.
|
| 13 |
+
path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
|
| 14 |
+
module_name = path_hash
|
| 15 |
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
| 16 |
+
if spec is None:
|
| 17 |
+
raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
|
| 18 |
+
module = importlib.util.module_from_spec(spec)
|
| 19 |
+
if module is None:
|
| 20 |
+
raise ImportError(f"Cannot load module {module_name} from spec")
|
| 21 |
+
sys.modules[module_name] = module
|
| 22 |
+
spec.loader.exec_module(module) # type: ignore
|
| 23 |
+
return module
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
|
build/torch211-cxx11-cu128-x86_64-linux/__init__.py
ADDED
|
@@ -0,0 +1,863 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""metakernel: the measuring instrument for the kernel-design loop.
|
| 2 |
+
|
| 3 |
+
An agent iterating on CUDA candidates is good at writing code and bad at
|
| 4 |
+
trusting its own measurements; it needs trustworthy timing, adversarial
|
| 5 |
+
correctness comparison, and measured facts about the device it targets.
|
| 6 |
+
metakernel ships those as loadable ops:
|
| 7 |
+
|
| 8 |
+
from kernels import get_kernel
|
| 9 |
+
mk = get_kernel("phanerozoic/metakernel", version=1,
|
| 10 |
+
trust_remote_code=True)
|
| 11 |
+
|
| 12 |
+
dossier = mk.probe_device() # measured facts
|
| 13 |
+
report = mk.bench(lambda: op(x), iters=200) # throttle-rejected
|
| 14 |
+
verdict = mk.compare(ref, cand, gen) # ULP evidence
|
| 15 |
+
table = mk.sweep(factory, {"BLOCK": [64, 128]}) # config search
|
| 16 |
+
|
| 17 |
+
Every op returns plain dicts and tensors, never prose. Every dossier entry
|
| 18 |
+
is produced by a dedicated probe kernel rather than a spec sheet, carries
|
| 19 |
+
the SM clock it was measured under, and labels throttled samples instead
|
| 20 |
+
of letting them poison medians. No hardware counters are used: everything
|
| 21 |
+
is inferred from timing, clocks, and the occupancy API, so entries carry a
|
| 22 |
+
confidence field (timing-differential inference can misattribute).
|
| 23 |
+
"""
|
| 24 |
+
import math
|
| 25 |
+
import time
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
|
| 29 |
+
from ._ops import ops
|
| 30 |
+
|
| 31 |
+
__all__ = ["probe_device", "bench", "compare", "order_shuffles",
|
| 32 |
+
"fuzz_shapes", "sweep", "alloc_stamps", "read_stamps",
|
| 33 |
+
"measure_clock", "ulp_diff", "ops"]
|
| 34 |
+
|
| 35 |
+
_MMA_KINDS = {
|
| 36 |
+
"fp16": (0, 16 * 16 * 16),
|
| 37 |
+
"bf16": (1, 16 * 16 * 16),
|
| 38 |
+
"tf32": (2, 16 * 16 * 8),
|
| 39 |
+
"int8": (3, 16 * 16 * 16),
|
| 40 |
+
"fp8_e4m3": (4, 16 * 8 * 32),
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _sync():
|
| 45 |
+
torch.cuda.synchronize()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _events(fn, reps):
|
| 49 |
+
"""Time reps calls of fn with one event pair each; returns ms list."""
|
| 50 |
+
starts = [torch.cuda.Event(enable_timing=True) for _ in range(reps)]
|
| 51 |
+
stops = [torch.cuda.Event(enable_timing=True) for _ in range(reps)]
|
| 52 |
+
for s, e in zip(starts, stops):
|
| 53 |
+
s.record()
|
| 54 |
+
fn()
|
| 55 |
+
e.record()
|
| 56 |
+
_sync()
|
| 57 |
+
return [s.elapsed_time(e) for s, e in zip(starts, stops)]
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def measure_clock(device="cuda", ticks=4_000_000, blocks=None):
|
| 61 |
+
"""Effective SM clock in GHz under an FMA spin of `ticks` clock64 ticks.
|
| 62 |
+
Wall time over elapsed ticks is the frequency the ticks ran at."""
|
| 63 |
+
props = torch.cuda.get_device_properties(device)
|
| 64 |
+
if blocks is None:
|
| 65 |
+
blocks = props.multi_processor_count
|
| 66 |
+
out = torch.zeros(2, dtype=torch.int64, device=device)
|
| 67 |
+
sink = torch.zeros(2, dtype=torch.float32, device=device)
|
| 68 |
+
ops.mk_spin(blocks, 256, ticks, out, sink) # warm
|
| 69 |
+
start = torch.cuda.Event(enable_timing=True)
|
| 70 |
+
stop = torch.cuda.Event(enable_timing=True)
|
| 71 |
+
start.record()
|
| 72 |
+
ops.mk_spin(blocks, 256, ticks, out, sink)
|
| 73 |
+
stop.record()
|
| 74 |
+
_sync()
|
| 75 |
+
ms = start.elapsed_time(stop)
|
| 76 |
+
elapsed = int(out[0].item())
|
| 77 |
+
return elapsed / (ms * 1e6)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _median(xs):
|
| 81 |
+
s = sorted(xs)
|
| 82 |
+
n = len(s)
|
| 83 |
+
return s[n // 2] if n % 2 else 0.5 * (s[n // 2 - 1] + s[n // 2])
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _entry(value, units, method, clock_ghz, confidence, note=""):
|
| 87 |
+
d = {"value": value, "units": units, "method": method,
|
| 88 |
+
"clock_ghz": round(clock_ghz, 3), "confidence": confidence}
|
| 89 |
+
if note:
|
| 90 |
+
d["note"] = note
|
| 91 |
+
return d
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _sattolo_ring(n, device, generator=None):
|
| 95 |
+
"""A random single-cycle permutation as an int32 next-pointer table, so
|
| 96 |
+
a pointer chase visits every slot before repeating."""
|
| 97 |
+
perm = torch.randperm(n, generator=generator)
|
| 98 |
+
ring = torch.empty(n, dtype=torch.int32)
|
| 99 |
+
ring[perm] = torch.roll(perm, -1).to(torch.int32)
|
| 100 |
+
return ring.to(device)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def probe_device(device="cuda", quick=False):
|
| 104 |
+
"""Empirical device dossier. Each entry is measured by a dedicated
|
| 105 |
+
probe kernel and carries units, method, the SM clock it ran under, and
|
| 106 |
+
a confidence field."""
|
| 107 |
+
props = torch.cuda.get_device_properties(device)
|
| 108 |
+
sm = props.multi_processor_count
|
| 109 |
+
l2 = props.L2_cache_size
|
| 110 |
+
d = {
|
| 111 |
+
"device": {
|
| 112 |
+
"name": props.name,
|
| 113 |
+
"sm_arch": f"sm{props.major}{props.minor}",
|
| 114 |
+
"sms": sm,
|
| 115 |
+
"l2_bytes": l2,
|
| 116 |
+
"vram_bytes": props.total_memory,
|
| 117 |
+
"torch": torch.__version__,
|
| 118 |
+
"cuda_runtime": torch.version.cuda,
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
sink = torch.zeros(2, dtype=torch.float32, device=device)
|
| 122 |
+
out64 = torch.zeros(2, dtype=torch.int64, device=device)
|
| 123 |
+
reps = 3 if quick else 5
|
| 124 |
+
|
| 125 |
+
# ---- clock under sustained load
|
| 126 |
+
traj = []
|
| 127 |
+
windows = 8 if quick else 24
|
| 128 |
+
for _ in range(windows):
|
| 129 |
+
traj.append(measure_clock(device, ticks=30_000_000))
|
| 130 |
+
d["clock"] = {
|
| 131 |
+
"max_ghz": round(max(traj), 3),
|
| 132 |
+
"min_ghz": round(min(traj), 3),
|
| 133 |
+
"final_ghz": round(traj[-1], 3),
|
| 134 |
+
"sag_pct": round(100 * (1 - traj[-1] / max(traj)), 1),
|
| 135 |
+
"units": "GHz",
|
| 136 |
+
"method": f"{windows} back-to-back FMA spin windows of 30M ticks; "
|
| 137 |
+
"wall/ticks per window",
|
| 138 |
+
"confidence": "high",
|
| 139 |
+
}
|
| 140 |
+
clk = max(traj)
|
| 141 |
+
|
| 142 |
+
# ---- HBM bandwidth: triad per access width, plus read-only,
|
| 143 |
+
# write-only, and random-gather
|
| 144 |
+
n4 = 16 << 20 # 16M float4 = 256 MB per buffer
|
| 145 |
+
if props.total_memory < 4 << 30:
|
| 146 |
+
n4 = 4 << 20
|
| 147 |
+
a = torch.rand(n4 * 4, device=device)
|
| 148 |
+
b = torch.rand(n4 * 4, device=device)
|
| 149 |
+
c = torch.empty(n4 * 4, device=device)
|
| 150 |
+
hbm = {}
|
| 151 |
+
for width in (1, 2, 4):
|
| 152 |
+
ops.mk_triad(a, b, c, width, 1.5) # warm
|
| 153 |
+
ms = _median(_events(lambda w=width: ops.mk_triad(a, b, c, w, 1.5),
|
| 154 |
+
reps))
|
| 155 |
+
gbs = 3 * a.numel() * 4 / (ms * 1e-3) / 1e9
|
| 156 |
+
hbm[f"triad_{4 * width}B"] = round(gbs, 1)
|
| 157 |
+
ops.mk_read(a, sink)
|
| 158 |
+
ms = _median(_events(lambda: ops.mk_read(a, sink), reps))
|
| 159 |
+
hbm["read_only"] = round(a.numel() * 4 / (ms * 1e-3) / 1e9, 1)
|
| 160 |
+
ops.mk_write(c, 1.0)
|
| 161 |
+
ms = _median(_events(lambda: ops.mk_write(c, 1.0), reps))
|
| 162 |
+
hbm["write_only"] = round(c.numel() * 4 / (ms * 1e-3) / 1e9, 1)
|
| 163 |
+
g = torch.Generator(device="cpu").manual_seed(7)
|
| 164 |
+
idx = torch.randint(0, n4, (n4,), generator=g,
|
| 165 |
+
dtype=torch.int32).to(device)
|
| 166 |
+
ops.mk_gather(a, idx, c, 256, 0)
|
| 167 |
+
ms = _median(_events(lambda: ops.mk_gather(a, idx, c, 256, 0), reps))
|
| 168 |
+
# traffic accounting: idx reads + 16B random loads + streamed stores
|
| 169 |
+
hbm["random_gather_16B"] = round(
|
| 170 |
+
(idx.numel() * 4 + 2 * n4 * 16) / (ms * 1e-3) / 1e9, 1)
|
| 171 |
+
d["hbm_bw"] = _entry(hbm, "GB/s", "streaming triad c=a+s*b per access "
|
| 172 |
+
"width; read-only fold; write-only fill; random "
|
| 173 |
+
"float4 gather (16B granules, nominal traffic)",
|
| 174 |
+
clk, "high",
|
| 175 |
+
"gather moves full cache lines per 16B granule, "
|
| 176 |
+
"so its nominal number is the achievable rate for "
|
| 177 |
+
"random access, not a line-efficiency claim")
|
| 178 |
+
|
| 179 |
+
# ---- L2 bandwidth (working set 3 buffers inside L2)
|
| 180 |
+
ws = max(l2 // 4, 1 << 20)
|
| 181 |
+
nl2 = ws // 16 # float4 elems per buffer
|
| 182 |
+
al = a[:nl2 * 4].contiguous()
|
| 183 |
+
bl = b[:nl2 * 4].contiguous()
|
| 184 |
+
cl = c[:nl2 * 4].contiguous()
|
| 185 |
+
passes = 64
|
| 186 |
+
ops.mk_triad_passes(al, bl, cl, 1.5, passes)
|
| 187 |
+
ms = _median(_events(
|
| 188 |
+
lambda: ops.mk_triad_passes(al, bl, cl, 1.5, passes), reps))
|
| 189 |
+
gbs = 3 * al.numel() * 4 * passes / (ms * 1e-3) / 1e9
|
| 190 |
+
d["l2_bw"] = _entry(round(gbs, 1), "GB/s",
|
| 191 |
+
f"in-kernel repeated triad passes over "
|
| 192 |
+
f"{3 * ws >> 20} MB resident working set", clk,
|
| 193 |
+
"medium", "first pass streams from HBM; amortized "
|
| 194 |
+
f"over {passes} passes")
|
| 195 |
+
|
| 196 |
+
# ---- latencies: pointer chase per level. Footprints are chosen so
|
| 197 |
+
# the chain hits the intended level: the L2 ring (2 MB) exceeds L1,
|
| 198 |
+
# fits L2 on every supported part, and spans few enough pages to stay
|
| 199 |
+
# TLB-resident after a full-cycle warm walk; the HBM chain is long
|
| 200 |
+
# enough (2x L2 in touched lines) that it evicts itself and cannot be
|
| 201 |
+
# served from cache.
|
| 202 |
+
lat = {}
|
| 203 |
+
idle_clk = measure_clock(device, ticks=2_000_000, blocks=1)
|
| 204 |
+
ring_s = _sattolo_ring(8192, device)
|
| 205 |
+
ops.mk_chase_shared(ring_s, 1 << 14, out64)
|
| 206 |
+
_sync()
|
| 207 |
+
hops = 1 << 16
|
| 208 |
+
ops.mk_chase_shared(ring_s, hops, out64)
|
| 209 |
+
_sync()
|
| 210 |
+
lat["smem_cycles"] = round(int(out64[0].item()) / hops, 1)
|
| 211 |
+
|
| 212 |
+
l1_ring_n = (16 << 10) // 4 # 16 KB: L1-resident after a warm walk
|
| 213 |
+
ring_l1 = _sattolo_ring(l1_ring_n, device)
|
| 214 |
+
ops.mk_chase_global(ring_l1, l1_ring_n, out64)
|
| 215 |
+
_sync()
|
| 216 |
+
hops_l1 = 1 << 15
|
| 217 |
+
ops.mk_chase_global(ring_l1, hops_l1, out64)
|
| 218 |
+
_sync()
|
| 219 |
+
lat["l1_cycles"] = round(int(out64[0].item()) / hops_l1, 1)
|
| 220 |
+
|
| 221 |
+
l2_ring_n = (2 << 20) // 4
|
| 222 |
+
ring_l2 = _sattolo_ring(l2_ring_n, device)
|
| 223 |
+
ops.mk_chase_global(ring_l2, l2_ring_n, out64) # full-cycle warm walk
|
| 224 |
+
_sync()
|
| 225 |
+
hops_l2 = 1 << 14
|
| 226 |
+
ops.mk_chase_global(ring_l2, hops_l2, out64)
|
| 227 |
+
_sync()
|
| 228 |
+
lat["l2_cycles"] = round(int(out64[0].item()) / hops_l2, 1)
|
| 229 |
+
|
| 230 |
+
hbm_bytes = min(max(4 * l2, 64 << 20), props.total_memory // 8)
|
| 231 |
+
ring_h = _sattolo_ring(int(hbm_bytes) // 4, device)
|
| 232 |
+
hops_h = max((2 * l2) // 128, 1 << 17)
|
| 233 |
+
ops.mk_chase_global(ring_h, 1 << 14, out64) # fault pages / warm TLB
|
| 234 |
+
_sync()
|
| 235 |
+
ops.mk_chase_global(ring_h, hops_h, out64)
|
| 236 |
+
_sync()
|
| 237 |
+
lat["hbm_cycles"] = round(int(out64[0].item()) / hops_h, 1)
|
| 238 |
+
lat["ns_per_cycle"] = round(1.0 / idle_clk, 3)
|
| 239 |
+
d["latency"] = _entry(lat, "cycles (single-lane dependent loads)",
|
| 240 |
+
"Sattolo-cycle pointer chase; 16 KB ring for L1, "
|
| 241 |
+
"2 MB TLB-resident ring for L2, self-evicting "
|
| 242 |
+
"chain (2x L2 in touched lines) for HBM",
|
| 243 |
+
idle_clk, "high",
|
| 244 |
+
"smem includes index arithmetic; HBM includes "
|
| 245 |
+
"residual TLB effects at large footprints")
|
| 246 |
+
del ring_h, ring_l2, ring_l1, ring_s
|
| 247 |
+
|
| 248 |
+
# ---- fp32 FMA and tensor-core throughput
|
| 249 |
+
blocks = sm * 4
|
| 250 |
+
iters = 20_000 if quick else 60_000
|
| 251 |
+
ops.mk_fma_f32(blocks, 256, iters, sink)
|
| 252 |
+
ms = _median(_events(lambda: ops.mk_fma_f32(blocks, 256, iters, sink),
|
| 253 |
+
reps))
|
| 254 |
+
flops = blocks * 256 * iters * 16 * 2 * 2 # 2 chains x 2 flops x unroll
|
| 255 |
+
d["fma_f32"] = _entry(round(flops / (ms * 1e-3) / 1e12, 2), "TFLOP/s",
|
| 256 |
+
"register-resident FMA chains", clk, "high")
|
| 257 |
+
sink64 = torch.zeros(2, dtype=torch.float64, device=device)
|
| 258 |
+
it64 = max(iters // 16, 1000)
|
| 259 |
+
ops.mk_fma_f64(blocks, 256, it64, sink64)
|
| 260 |
+
ms = _median(_events(
|
| 261 |
+
lambda: ops.mk_fma_f64(blocks, 256, it64, sink64), reps))
|
| 262 |
+
flops64 = blocks * 256 * it64 * 16 * 2 * 2
|
| 263 |
+
d["fma_f64"] = _entry(round(flops64 / (ms * 1e-3) / 1e12, 3), "TFLOP/s",
|
| 264 |
+
"register-resident double FMA chains (the DFMA "
|
| 265 |
+
"pipe)", clk, "high")
|
| 266 |
+
mma = {}
|
| 267 |
+
for name, (kind, mnk) in _MMA_KINDS.items():
|
| 268 |
+
it = 30_000 if quick else 100_000
|
| 269 |
+
r = ops.mk_mma(kind, blocks, 8, 512, sink)
|
| 270 |
+
if r != 0:
|
| 271 |
+
mma[name] = "unsupported"
|
| 272 |
+
continue
|
| 273 |
+
_sync()
|
| 274 |
+
ms = _median(_events(
|
| 275 |
+
lambda k=kind, i=it: ops.mk_mma(k, blocks, 8, i, sink), reps))
|
| 276 |
+
flops = blocks * 8 * it * 4 * 2 * mnk
|
| 277 |
+
mma[name] = round(flops / (ms * 1e-3) / 1e12, 1)
|
| 278 |
+
d["mma"] = _entry(mma, "TFLOP/s (TOP/s for int8)",
|
| 279 |
+
"register-fragment mma_sync loops (wmma; fp8 via "
|
| 280 |
+
"inline PTX m16n8k32)", clk, "medium",
|
| 281 |
+
"dense-tile rate without memory traffic; real "
|
| 282 |
+
"kernels see this only when staging hides behind it")
|
| 283 |
+
|
| 284 |
+
# ---- atomic contention curve: throughput vs number of distinct
|
| 285 |
+
# addresses the whole grid hammers (slots = 1 is full contention;
|
| 286 |
+
# slots = threads is contention-free), plus the shared-memory point
|
| 287 |
+
n_thr = blocks * 256
|
| 288 |
+
buf = torch.zeros(n_thr, dtype=torch.float32, device=device)
|
| 289 |
+
at = {"curve_slots": {}}
|
| 290 |
+
it_at = 2_000 if quick else 6_000
|
| 291 |
+
slot_points = [1, 8, 64, 512, 4096, n_thr]
|
| 292 |
+
for slots in slot_points:
|
| 293 |
+
buf.zero_()
|
| 294 |
+
ops.mk_atomics(1, blocks, 256, it_at, buf, slots)
|
| 295 |
+
ms = _median(_events(
|
| 296 |
+
lambda s=slots: ops.mk_atomics(1, blocks, 256, it_at, buf, s),
|
| 297 |
+
reps))
|
| 298 |
+
at["curve_slots"][str(slots)] = round(
|
| 299 |
+
n_thr * it_at / (ms * 1e-3) / 1e9, 2)
|
| 300 |
+
ops.mk_atomics(3, blocks, 256, it_at, buf, 1)
|
| 301 |
+
ms = _median(_events(
|
| 302 |
+
lambda: ops.mk_atomics(3, blocks, 256, it_at, buf, 1), reps))
|
| 303 |
+
at["shared_same_addr"] = round(n_thr * it_at / (ms * 1e-3) / 1e9, 2)
|
| 304 |
+
d["atomics_f32"] = _entry(at, "Gop/s",
|
| 305 |
+
"atomicAdd hammer; curve sweeps the number "
|
| 306 |
+
"of distinct global addresses from 1 (full "
|
| 307 |
+
"contention) to one per thread", clk, "high")
|
| 308 |
+
|
| 309 |
+
# ---- launch overhead
|
| 310 |
+
anchor = sink
|
| 311 |
+
n_launch = 500 if quick else 2000
|
| 312 |
+
_sync()
|
| 313 |
+
t0 = time.perf_counter()
|
| 314 |
+
for _ in range(n_launch):
|
| 315 |
+
ops.mk_empty(anchor)
|
| 316 |
+
_sync()
|
| 317 |
+
queued = (time.perf_counter() - t0) / n_launch * 1e6
|
| 318 |
+
n_rt = 100 if quick else 300
|
| 319 |
+
t0 = time.perf_counter()
|
| 320 |
+
for _ in range(n_rt):
|
| 321 |
+
ops.mk_empty(anchor)
|
| 322 |
+
_sync()
|
| 323 |
+
rt = (time.perf_counter() - t0) / n_rt * 1e6
|
| 324 |
+
d["launch_overhead"] = _entry(
|
| 325 |
+
{"queued_us": round(queued, 2), "round_trip_us": round(rt, 2)},
|
| 326 |
+
"us", "empty-kernel trains, queued vs launch+sync", clk, "high",
|
| 327 |
+
"host-dependent: includes torch op dispatch")
|
| 328 |
+
|
| 329 |
+
# ---- grid barrier
|
| 330 |
+
bar_iters = 500 if quick else 2000
|
| 331 |
+
r = ops.mk_barrier(sm, 256, bar_iters, out64)
|
| 332 |
+
if r == 0:
|
| 333 |
+
_sync()
|
| 334 |
+
ticks = int(out64[0].item())
|
| 335 |
+
d["grid_barrier"] = _entry(
|
| 336 |
+
round(ticks / bar_iters / clk / 1000.0, 2), "us per grid.sync",
|
| 337 |
+
f"cooperative kernel, {sm} blocks x 256 threads", clk, "high")
|
| 338 |
+
else:
|
| 339 |
+
d["grid_barrier"] = _entry("unsupported", "", "cooperative launch "
|
| 340 |
+
"unavailable or grid too large", clk,
|
| 341 |
+
"high")
|
| 342 |
+
|
| 343 |
+
# ---- occupancy table (API) plus timed knees: the same random gather
|
| 344 |
+
# at every (threads, dyn_smem) point; when the forced blocks/SM stop
|
| 345 |
+
# covering memory latency, achieved bandwidth falls off the cliff
|
| 346 |
+
occ = {}
|
| 347 |
+
for threads in (128, 256, 512, 1024):
|
| 348 |
+
row = {}
|
| 349 |
+
for smem_kb in (0, 16, 32, 48):
|
| 350 |
+
row[f"{smem_kb}KB"] = int(ops.mk_occupancy(
|
| 351 |
+
threads, smem_kb * 1024, anchor))
|
| 352 |
+
occ[f"t{threads}"] = row
|
| 353 |
+
d["occupancy_blocks_per_sm"] = _entry(
|
| 354 |
+
occ, "blocks/SM", "occupancy API on the float4 triad kernel",
|
| 355 |
+
clk, "high", "API-derived, not timing-derived")
|
| 356 |
+
|
| 357 |
+
knee_iters = 6_000 if quick else 20_000
|
| 358 |
+
knee_blocks = sm * 8
|
| 359 |
+
knees = {}
|
| 360 |
+
for threads in (128, 256, 1024):
|
| 361 |
+
row = {}
|
| 362 |
+
for smem_kb in (0, 32, 48):
|
| 363 |
+
ops.mk_fma_dep(knee_blocks, threads, smem_kb * 1024,
|
| 364 |
+
knee_iters, sink)
|
| 365 |
+
ms = _median(_events(
|
| 366 |
+
lambda t=threads, s=smem_kb: ops.mk_fma_dep(
|
| 367 |
+
knee_blocks, t, s * 1024, knee_iters, sink), reps))
|
| 368 |
+
fl = knee_blocks * threads * knee_iters * 16 * 2
|
| 369 |
+
row[f"{smem_kb}KB"] = round(fl / (ms * 1e-3) / 1e12, 2)
|
| 370 |
+
knees[f"t{threads}"] = row
|
| 371 |
+
d["occupancy_knees_fma_tflops"] = _entry(
|
| 372 |
+
knees, "TFLOP/s", "a single dependent FMA chain per thread, timed "
|
| 373 |
+
"at forced (threads, dyn smem) occupancies over fixed total work; "
|
| 374 |
+
"throughput is proportional to resident threads until the pipe "
|
| 375 |
+
"saturates, so the cliff marks where occupancy stops covering "
|
| 376 |
+
"latency", clk, "high",
|
| 377 |
+
"timing-derived counterpart to the API table above")
|
| 378 |
+
return d
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def bench(fn, iters=100, warmup=10, bytes=None, flops=None, dossier=None,
|
| 382 |
+
dtype="fp16", clock_guard=0.97, clock_stride=8, graph=False,
|
| 383 |
+
device="cuda"):
|
| 384 |
+
"""Median-of-N cudaEvent timing with throttle rejection.
|
| 385 |
+
|
| 386 |
+
The SM clock is probed every `clock_stride` samples; samples inside a
|
| 387 |
+
window whose clock sagged below `clock_guard` x the best observed
|
| 388 |
+
clock are rejected and counted rather than silently averaged. With
|
| 389 |
+
declared `bytes`/`flops` per call, reports achieved GB/s / TFLOP/s;
|
| 390 |
+
with a `dossier`, adds roofline placement against the measured peaks.
|
| 391 |
+
|
| 392 |
+
`graph=True` captures fn() into a CUDA graph and times graph replays,
|
| 393 |
+
stripping Python and torch dispatch from the measurement: the right
|
| 394 |
+
mode for kernels in the few-microsecond range, where dispatch is
|
| 395 |
+
otherwise the thing being measured. Capture requires fn to be
|
| 396 |
+
graph-safe (no sync, no allocation growth, static shapes)."""
|
| 397 |
+
for _ in range(warmup):
|
| 398 |
+
fn()
|
| 399 |
+
_sync()
|
| 400 |
+
if graph:
|
| 401 |
+
g = torch.cuda.CUDAGraph()
|
| 402 |
+
with torch.cuda.graph(g):
|
| 403 |
+
fn()
|
| 404 |
+
fn = g.replay
|
| 405 |
+
for _ in range(warmup):
|
| 406 |
+
fn()
|
| 407 |
+
_sync()
|
| 408 |
+
samples = []
|
| 409 |
+
clocks = []
|
| 410 |
+
stride = clock_stride
|
| 411 |
+
for base in range(0, iters, stride):
|
| 412 |
+
clocks.append(measure_clock(device, ticks=1_500_000))
|
| 413 |
+
n = min(stride, iters - base)
|
| 414 |
+
samples.append((_events(fn, n), len(clocks) - 1))
|
| 415 |
+
clocks.append(measure_clock(device, ticks=1_500_000))
|
| 416 |
+
best_clk = max(clocks)
|
| 417 |
+
kept, rejected = [], 0
|
| 418 |
+
flat = []
|
| 419 |
+
for group, ci in samples:
|
| 420 |
+
window_ok = (clocks[ci] >= clock_guard * best_clk and
|
| 421 |
+
clocks[min(ci + 1, len(clocks) - 1)] >=
|
| 422 |
+
clock_guard * best_clk)
|
| 423 |
+
for ms in group:
|
| 424 |
+
flat.append(ms)
|
| 425 |
+
if window_ok:
|
| 426 |
+
kept.append(ms)
|
| 427 |
+
else:
|
| 428 |
+
rejected += 1
|
| 429 |
+
if not kept:
|
| 430 |
+
kept = flat
|
| 431 |
+
med = _median(kept)
|
| 432 |
+
rep = {
|
| 433 |
+
"median_ms": round(med, 4),
|
| 434 |
+
"min_ms": round(min(kept), 4),
|
| 435 |
+
"mean_ms": round(sum(kept) / len(kept), 4),
|
| 436 |
+
"samples": len(flat),
|
| 437 |
+
"rejected_throttled": rejected,
|
| 438 |
+
"graph_captured": bool(graph),
|
| 439 |
+
"clock_ghz": {"best": round(best_clk, 3),
|
| 440 |
+
"worst": round(min(clocks), 3)},
|
| 441 |
+
}
|
| 442 |
+
if bytes is not None:
|
| 443 |
+
rep["achieved_gbs"] = round(bytes / (med * 1e-3) / 1e9, 1)
|
| 444 |
+
if flops is not None:
|
| 445 |
+
rep["achieved_tflops"] = round(flops / (med * 1e-3) / 1e12, 2)
|
| 446 |
+
if bytes and flops:
|
| 447 |
+
rep["arithmetic_intensity"] = round(flops / bytes, 2)
|
| 448 |
+
if dossier is not None and bytes and flops:
|
| 449 |
+
bw = max(dossier["hbm_bw"]["value"].values()) * 1e9
|
| 450 |
+
mma_v = dossier["mma"]["value"].get(dtype)
|
| 451 |
+
peak = (mma_v if isinstance(mma_v, (int, float))
|
| 452 |
+
else dossier["fma_f32"]["value"]) * 1e12
|
| 453 |
+
ai = flops / bytes
|
| 454 |
+
roof = min(peak, ai * bw)
|
| 455 |
+
rep["roofline"] = {
|
| 456 |
+
"bound": "memory" if ai * bw < peak else "compute",
|
| 457 |
+
"ridge_flops_per_byte": round(peak / bw, 1),
|
| 458 |
+
"pct_of_roof": round(100 * (flops / (med * 1e-3)) / roof, 1),
|
| 459 |
+
"peak_dtype": dtype,
|
| 460 |
+
}
|
| 461 |
+
return rep
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
def _to_ordered_int(t):
|
| 465 |
+
"""Map float bit patterns to integers whose difference is the ULP
|
| 466 |
+
distance: positive floats keep their bits, negative floats mirror
|
| 467 |
+
below zero, so the mapping is monotone in the float value."""
|
| 468 |
+
if t.dtype == torch.float32:
|
| 469 |
+
u = t.view(torch.int32).to(torch.int64) & 0xFFFFFFFF
|
| 470 |
+
half = 1 << 31
|
| 471 |
+
elif t.dtype in (torch.float16, torch.bfloat16):
|
| 472 |
+
u = t.view(torch.int16).to(torch.int64) & 0xFFFF
|
| 473 |
+
half = 1 << 15
|
| 474 |
+
elif t.dtype == torch.float64:
|
| 475 |
+
i = t.view(torch.int64)
|
| 476 |
+
return torch.where(i >= 0, i, (-(1 << 63)) - i)
|
| 477 |
+
else:
|
| 478 |
+
raise ValueError(f"unsupported dtype {t.dtype}")
|
| 479 |
+
return torch.where(u < half, u, half - u)
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
def ulp_diff(a, b):
|
| 483 |
+
"""Elementwise ULP distance between same-dtype float tensors. NaNs in
|
| 484 |
+
the same position count as 0; a one-sided NaN counts as 2**62."""
|
| 485 |
+
assert a.dtype == b.dtype and a.shape == b.shape
|
| 486 |
+
ia, ib = _to_ordered_int(a), _to_ordered_int(b)
|
| 487 |
+
d = (ia - ib).abs()
|
| 488 |
+
both_nan = torch.isnan(a) & torch.isnan(b)
|
| 489 |
+
one_nan = torch.isnan(a) ^ torch.isnan(b)
|
| 490 |
+
d = torch.where(both_nan, torch.zeros_like(d), d)
|
| 491 |
+
d = torch.where(one_nan, torch.full_like(d, 1 << 62), d)
|
| 492 |
+
return d
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
class order_shuffles:
|
| 496 |
+
"""Built-in order shuffles for the common contractual-noise axes."""
|
| 497 |
+
|
| 498 |
+
@staticmethod
|
| 499 |
+
def last_dim():
|
| 500 |
+
"""Permute the last dim of every tensor arg identically: the
|
| 501 |
+
accumulation order of a last-dim reduction changes, the value
|
| 502 |
+
does not."""
|
| 503 |
+
def shuffle(args, seed):
|
| 504 |
+
g = torch.Generator(device=args[0].device).manual_seed(seed)
|
| 505 |
+
p = torch.randperm(args[0].shape[-1], device=args[0].device,
|
| 506 |
+
generator=g)
|
| 507 |
+
return tuple(x[..., p] for x in args)
|
| 508 |
+
return shuffle
|
| 509 |
+
|
| 510 |
+
@staticmethod
|
| 511 |
+
def matmul_k():
|
| 512 |
+
"""Permute K of (A [.., M, K], B [.., K, N]) consistently: the
|
| 513 |
+
product is unchanged, the accumulation order is not."""
|
| 514 |
+
def shuffle(args, seed):
|
| 515 |
+
a_m, b_m = args[0], args[1]
|
| 516 |
+
g = torch.Generator(device=a_m.device).manual_seed(seed)
|
| 517 |
+
p = torch.randperm(a_m.shape[-1], device=a_m.device,
|
| 518 |
+
generator=g)
|
| 519 |
+
return (a_m[..., p], b_m[..., p, :]) + tuple(args[2:])
|
| 520 |
+
return shuffle
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
def _as_outputs(x):
|
| 524 |
+
return list(x) if isinstance(x, (tuple, list)) else [x]
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
def compare(ref, cand, gen, trials=8, order_shuffle=None, order_trials=8,
|
| 528 |
+
ulp_tol=None, atol=None, rtol=None, band_margin=4.0, seed=0):
|
| 529 |
+
"""Adversarial correctness comparison with ULP evidence.
|
| 530 |
+
|
| 531 |
+
`gen(seed) -> args` builds inputs; `ref(*args)` and `cand(*args)`
|
| 532 |
+
return a tensor or a tuple of tensors (each output is compared and
|
| 533 |
+
the worst governs). If `order_shuffle(args, seed) -> args` is given
|
| 534 |
+
and reorders contractual-noise dimensions (see `order_shuffles` for
|
| 535 |
+
the common cases), the reference is treated as a distribution:
|
| 536 |
+
candidates are judged against the measured reduction-order band
|
| 537 |
+
rather than a point. Verdict, stated in the report: max ULP <=
|
| 538 |
+
band_margin * band + 2 when a band is measured, else max ULP <=
|
| 539 |
+
ulp_tol (default 2); if `atol`/`rtol` are given they are an
|
| 540 |
+
alternative acceptance route, checked as |c - r| <= atol + rtol|r|."""
|
| 541 |
+
worst = {"max_ulp": -1}
|
| 542 |
+
total_hist = {}
|
| 543 |
+
per_trial = []
|
| 544 |
+
max_abs_seen = 0.0
|
| 545 |
+
max_rel_seen = 0.0
|
| 546 |
+
for t in range(trials):
|
| 547 |
+
args = gen(seed + t)
|
| 548 |
+
r_outs = _as_outputs(ref(*args))
|
| 549 |
+
c_outs = _as_outputs(cand(*args))
|
| 550 |
+
assert len(r_outs) == len(c_outs), "output arity mismatch"
|
| 551 |
+
# the order band is POINTWISE: a near-zero reduction output is
|
| 552 |
+
# legitimately thousands of ULP order-sensitive (cancellation)
|
| 553 |
+
# while its neighbors are stable, so each element is judged
|
| 554 |
+
# against its own measured sensitivity, not a global max
|
| 555 |
+
bands = None
|
| 556 |
+
band = 0
|
| 557 |
+
band_score = 0.0
|
| 558 |
+
if order_shuffle is not None:
|
| 559 |
+
bands = [torch.zeros_like(ro, dtype=torch.int64)
|
| 560 |
+
for ro in r_outs]
|
| 561 |
+
for k in range(order_trials):
|
| 562 |
+
r2 = _as_outputs(
|
| 563 |
+
ref(*order_shuffle(args, 1000 + seed + t * 131 + k)))
|
| 564 |
+
for bi, (ro, r2o) in enumerate(zip(r_outs, r2)):
|
| 565 |
+
bands[bi] = torch.maximum(bands[bi],
|
| 566 |
+
ulp_diff(ro, r2o))
|
| 567 |
+
band = max(int(b.max().item()) if b.numel() else 0
|
| 568 |
+
for b in bands)
|
| 569 |
+
max_ulp = 0
|
| 570 |
+
mean_ulp = 0.0
|
| 571 |
+
first_div = -1
|
| 572 |
+
n_el = 0
|
| 573 |
+
nan_r = nan_c = inf_r = inf_c = 0
|
| 574 |
+
for oi, (r, c) in enumerate(zip(r_outs, c_outs)):
|
| 575 |
+
d = ulp_diff(r, c)
|
| 576 |
+
if bands is not None and d.numel():
|
| 577 |
+
ratio = d.double() / (bands[oi].double() + 2.0)
|
| 578 |
+
band_score = max(band_score, float(ratio.max().item()))
|
| 579 |
+
if d.numel():
|
| 580 |
+
max_ulp = max(max_ulp, int(d.max().item()))
|
| 581 |
+
mean_ulp += float(d.double().sum().item())
|
| 582 |
+
n_el += d.numel()
|
| 583 |
+
nz = (d > 0)
|
| 584 |
+
if first_div < 0 and nz.any():
|
| 585 |
+
first_div = int(nz.flatten().nonzero()[0].item())
|
| 586 |
+
if nz.any():
|
| 587 |
+
buckets = torch.clamp(
|
| 588 |
+
torch.log2(d[nz].double()).floor(), 0, 62
|
| 589 |
+
).to(torch.int64)
|
| 590 |
+
counts = torch.bincount(buckets, minlength=63)
|
| 591 |
+
for bkt in counts.nonzero().flatten().tolist():
|
| 592 |
+
total_hist[bkt] = total_hist.get(bkt, 0) + int(
|
| 593 |
+
counts[bkt].item())
|
| 594 |
+
fin = torch.isfinite(r) & torch.isfinite(c)
|
| 595 |
+
if fin.any():
|
| 596 |
+
diff = (c[fin].double() - r[fin].double()).abs()
|
| 597 |
+
max_abs_seen = max(max_abs_seen,
|
| 598 |
+
float(diff.max().item()))
|
| 599 |
+
rel = diff / r[fin].double().abs().clamp(min=1e-300)
|
| 600 |
+
max_rel_seen = max(max_rel_seen,
|
| 601 |
+
float(rel.max().item()))
|
| 602 |
+
nan_r += int(torch.isnan(r).sum().item())
|
| 603 |
+
nan_c += int(torch.isnan(c).sum().item())
|
| 604 |
+
inf_r += int(torch.isinf(r).sum().item())
|
| 605 |
+
inf_c += int(torch.isinf(c).sum().item())
|
| 606 |
+
trial_rep = {
|
| 607 |
+
"max_ulp": max_ulp,
|
| 608 |
+
"mean_ulp": round(mean_ulp / max(n_el, 1), 3),
|
| 609 |
+
"first_divergent_index": first_div,
|
| 610 |
+
"nan_ref": nan_r, "nan_cand": nan_c,
|
| 611 |
+
"inf_ref": inf_r, "inf_cand": inf_c,
|
| 612 |
+
"order_band_ulp": band,
|
| 613 |
+
"band_score": round(band_score, 2),
|
| 614 |
+
}
|
| 615 |
+
per_trial.append(trial_rep)
|
| 616 |
+
if max_ulp > worst["max_ulp"]:
|
| 617 |
+
worst = trial_rep
|
| 618 |
+
band_max = max(t["order_band_ulp"] for t in per_trial)
|
| 619 |
+
max_seen = max(t["max_ulp"] for t in per_trial)
|
| 620 |
+
if order_shuffle is not None:
|
| 621 |
+
score = max(t["band_score"] for t in per_trial)
|
| 622 |
+
ulp_ok = score <= band_margin
|
| 623 |
+
criterion = (f"pointwise max(ulp / (order_band + 2)) = "
|
| 624 |
+
f"{score:.2f} <= band_margin {band_margin}")
|
| 625 |
+
else:
|
| 626 |
+
tol = ulp_tol if ulp_tol is not None else 2
|
| 627 |
+
ulp_ok = max_seen <= tol
|
| 628 |
+
criterion = f"max_ulp {max_seen} <= ulp_tol {tol}"
|
| 629 |
+
tol_ok = False
|
| 630 |
+
if atol is not None or rtol is not None:
|
| 631 |
+
tol_ok = max_abs_seen <= (atol or 0) + (rtol or 0) * 1.0 or \
|
| 632 |
+
max_rel_seen <= (rtol or 0) or max_abs_seen <= (atol or 0)
|
| 633 |
+
criterion += (f"; OR max_abs {max_abs_seen:.3e} <= atol {atol} / "
|
| 634 |
+
f"max_rel {max_rel_seen:.3e} <= rtol {rtol}")
|
| 635 |
+
return {
|
| 636 |
+
"pass": bool(ulp_ok or tol_ok),
|
| 637 |
+
"criterion": criterion,
|
| 638 |
+
"worst_trial": worst,
|
| 639 |
+
"max_abs": max_abs_seen,
|
| 640 |
+
"max_rel": max_rel_seen,
|
| 641 |
+
"trials": per_trial,
|
| 642 |
+
"ulp_hist_log2": dict(sorted(total_hist.items())),
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
|
| 646 |
+
_FUZZ_VARIANTS = ("contig", "transposed", "strided_rows", "offset_slice",
|
| 647 |
+
"overflow_expand")
|
| 648 |
+
|
| 649 |
+
|
| 650 |
+
def _context_alive(device):
|
| 651 |
+
try:
|
| 652 |
+
t = torch.ones(4, device=device)
|
| 653 |
+
torch.cuda.synchronize()
|
| 654 |
+
return bool(t.sum().item() == 4.0)
|
| 655 |
+
except Exception: # noqa: BLE001
|
| 656 |
+
return False
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
def fuzz_shapes(op, ref, make, shapes=None, variants=_FUZZ_VARIANTS,
|
| 660 |
+
dtypes=(torch.float32,), ulp_tol=64, device="cuda"):
|
| 661 |
+
"""The ugly envelope: run op vs ref across edge shapes, dtypes, and
|
| 662 |
+
layout mutations; report ok / mismatch / exception per case.
|
| 663 |
+
|
| 664 |
+
`make(shape, dtype)` (or `make(shape)`) builds a base input, either a
|
| 665 |
+
tensor or a tuple of tensors for multi-argument ops; layout variants
|
| 666 |
+
derive non-contiguous and misaligned views whose CONTENT equals a
|
| 667 |
+
contiguous copy, so `op(*xs)` must match `ref(*xs)` on every variant.
|
| 668 |
+
`overflow_expand` stride-0-expands the input past 2**31 logical
|
| 669 |
+
elements to catch int32 indexing, and is skipped with status
|
| 670 |
+
`skipped_vram` when materializing the output would not fit. After any
|
| 671 |
+
exception the CUDA context is health-checked; if a candidate killed
|
| 672 |
+
it, remaining cases are marked `context_lost` instead of producing
|
| 673 |
+
garbage."""
|
| 674 |
+
if shapes is None:
|
| 675 |
+
shapes = [(0, 8), (1, 1), (1, 17), (3, 63), (17, 129), (128, 128),
|
| 676 |
+
(1023, 5), (2, 4096)]
|
| 677 |
+
|
| 678 |
+
def build(shape, dtype):
|
| 679 |
+
try:
|
| 680 |
+
out = make(tuple(shape), dtype)
|
| 681 |
+
except TypeError:
|
| 682 |
+
out = make(tuple(shape))
|
| 683 |
+
return out if isinstance(out, (tuple, list)) else (out,)
|
| 684 |
+
|
| 685 |
+
def variant_of(x, v):
|
| 686 |
+
if v == "contig":
|
| 687 |
+
return x
|
| 688 |
+
if v == "transposed":
|
| 689 |
+
return x.t() if x.dim() == 2 else x
|
| 690 |
+
if v == "strided_rows":
|
| 691 |
+
return x[::2] if x.shape[0] >= 2 else x
|
| 692 |
+
if v == "offset_slice":
|
| 693 |
+
# a storage-offset-1 view: misaligned for vectorized loads
|
| 694 |
+
return x.flatten()[1:] if x.numel() > 1 else x
|
| 695 |
+
if v == "overflow_expand":
|
| 696 |
+
if x.numel() == 0:
|
| 697 |
+
return None
|
| 698 |
+
per = max(x.numel(), 1)
|
| 699 |
+
rep = (1 << 31) // per + 2
|
| 700 |
+
return x.reshape((1,) + tuple(x.shape)).expand(
|
| 701 |
+
(rep,) + tuple(x.shape))
|
| 702 |
+
return x
|
| 703 |
+
|
| 704 |
+
rows = []
|
| 705 |
+
lost = False
|
| 706 |
+
for dtype in dtypes:
|
| 707 |
+
for shape in shapes:
|
| 708 |
+
bases = build(shape, dtype)
|
| 709 |
+
for v in variants:
|
| 710 |
+
case = {"shape": tuple(shape), "variant": v,
|
| 711 |
+
"dtype": str(dtype).replace("torch.", "")}
|
| 712 |
+
if lost:
|
| 713 |
+
case["status"] = "context_lost"
|
| 714 |
+
rows.append(case)
|
| 715 |
+
continue
|
| 716 |
+
xs = [variant_of(b, v) for b in bases]
|
| 717 |
+
if any(x is None for x in xs):
|
| 718 |
+
case["status"] = "skipped_empty"
|
| 719 |
+
rows.append(case)
|
| 720 |
+
continue
|
| 721 |
+
if v == "overflow_expand":
|
| 722 |
+
logical = max(x.numel() for x in xs)
|
| 723 |
+
need = logical * xs[0].element_size() * 2
|
| 724 |
+
free, _total = torch.cuda.mem_get_info()
|
| 725 |
+
if need > free * 0.8:
|
| 726 |
+
case["status"] = "skipped_vram"
|
| 727 |
+
case["logical_elems"] = int(logical)
|
| 728 |
+
rows.append(case)
|
| 729 |
+
continue
|
| 730 |
+
case["logical_elems"] = int(logical)
|
| 731 |
+
case["contiguous"] = bool(xs[0].is_contiguous())
|
| 732 |
+
try:
|
| 733 |
+
got = op(*xs)
|
| 734 |
+
want = ref(*xs)
|
| 735 |
+
if v == "overflow_expand":
|
| 736 |
+
# bounded comparison: the head plus the high-index
|
| 737 |
+
# tail, which is where int32 flat indexing wraps
|
| 738 |
+
w = 1 << 20
|
| 739 |
+
gf, wf = got.flatten(), want.flatten()
|
| 740 |
+
pairs = ((gf[:w], wf[:w]), (gf[-w:], wf[-w:]))
|
| 741 |
+
else:
|
| 742 |
+
pairs = ((got, want),)
|
| 743 |
+
mx = 0
|
| 744 |
+
for gp, wp in pairs:
|
| 745 |
+
d = ulp_diff(wp, gp) if gp.is_floating_point() \
|
| 746 |
+
else (wp != gp).to(torch.int64) * (1 << 62)
|
| 747 |
+
if d.numel():
|
| 748 |
+
mx = max(mx, int(d.max().item()))
|
| 749 |
+
del d
|
| 750 |
+
case["status"] = "ok" if mx <= ulp_tol else "mismatch"
|
| 751 |
+
case["max_ulp"] = mx
|
| 752 |
+
del got, want
|
| 753 |
+
except Exception as e: # noqa: BLE001 - report, don't die
|
| 754 |
+
case["status"] = "exception"
|
| 755 |
+
case["error"] = f"{type(e).__name__}: {str(e)[:120]}"
|
| 756 |
+
if not _context_alive(device):
|
| 757 |
+
lost = True
|
| 758 |
+
case["status"] = "context_killed"
|
| 759 |
+
rows.append(case)
|
| 760 |
+
n_bad = sum(1 for r in rows
|
| 761 |
+
if r["status"] not in ("ok", "skipped_vram",
|
| 762 |
+
"skipped_empty"))
|
| 763 |
+
return {"cases": rows, "failed": n_bad, "total": len(rows),
|
| 764 |
+
"context_lost": lost}
|
| 765 |
+
|
| 766 |
+
|
| 767 |
+
def sweep(factory, grid, iters=30, warmup=5, bytes=None, flops=None,
|
| 768 |
+
check=None, budget_s=None):
|
| 769 |
+
"""Launch-configuration search: `factory(**point) -> callable`; one
|
| 770 |
+
bench row per grid point, sorted fastest first. A point whose factory
|
| 771 |
+
or run raises is reported, not skipped silently. `check(fn) -> bool`
|
| 772 |
+
is an optional correctness gate recorded per row (a fast wrong config
|
| 773 |
+
must not win a sweep). `budget_s` caps total sweep time; points not
|
| 774 |
+
reached are reported as `not_run` rather than silently dropped."""
|
| 775 |
+
keys = list(grid.keys())
|
| 776 |
+
|
| 777 |
+
def points(i, acc):
|
| 778 |
+
if i == len(keys):
|
| 779 |
+
yield dict(acc)
|
| 780 |
+
return
|
| 781 |
+
for v in grid[keys[i]]:
|
| 782 |
+
acc[keys[i]] = v
|
| 783 |
+
yield from points(i + 1, acc)
|
| 784 |
+
|
| 785 |
+
rows = []
|
| 786 |
+
t_start = time.perf_counter()
|
| 787 |
+
over_budget = False
|
| 788 |
+
for p in points(0, {}):
|
| 789 |
+
row = dict(p)
|
| 790 |
+
if over_budget or (budget_s is not None and
|
| 791 |
+
time.perf_counter() - t_start > budget_s):
|
| 792 |
+
over_budget = True
|
| 793 |
+
row["status"] = "not_run"
|
| 794 |
+
row["median_ms"] = float("inf")
|
| 795 |
+
rows.append(row)
|
| 796 |
+
continue
|
| 797 |
+
try:
|
| 798 |
+
fn = factory(**p)
|
| 799 |
+
if check is not None:
|
| 800 |
+
row["check"] = bool(check(fn))
|
| 801 |
+
rep = bench(fn, iters=iters, warmup=warmup, bytes=bytes,
|
| 802 |
+
flops=flops)
|
| 803 |
+
row.update({"median_ms": rep["median_ms"],
|
| 804 |
+
"rejected_throttled": rep["rejected_throttled"]})
|
| 805 |
+
if "achieved_gbs" in rep:
|
| 806 |
+
row["achieved_gbs"] = rep["achieved_gbs"]
|
| 807 |
+
if "achieved_tflops" in rep:
|
| 808 |
+
row["achieved_tflops"] = rep["achieved_tflops"]
|
| 809 |
+
except Exception as e: # noqa: BLE001
|
| 810 |
+
row["error"] = f"{type(e).__name__}: {str(e)[:120]}"
|
| 811 |
+
row["median_ms"] = float("inf")
|
| 812 |
+
rows.append(row)
|
| 813 |
+
rows.sort(key=lambda r: (not r.get("check", True), r["median_ms"]))
|
| 814 |
+
return rows
|
| 815 |
+
|
| 816 |
+
|
| 817 |
+
def alloc_stamps(n, device="cuda"):
|
| 818 |
+
"""Device buffer for the MK_STAMP convention (metakernel_cuda/
|
| 819 |
+
stamps.cuh): the candidate kernel writes clock64() into slot i."""
|
| 820 |
+
return torch.zeros(n, dtype=torch.int64, device=device)
|
| 821 |
+
|
| 822 |
+
|
| 823 |
+
def read_stamps(stamps, labels=None, clock_ghz=None):
|
| 824 |
+
"""Decode MK_STAMP phases: consecutive deltas in ticks and in
|
| 825 |
+
microseconds under the measured (or provided) SM clock. A 1-D buffer
|
| 826 |
+
(MK_STAMP: block 0 only) yields one delta per phase; a 2-D [B, n]
|
| 827 |
+
buffer (MK_STAMP_BLOCK) yields min / median / max across blocks per
|
| 828 |
+
phase, with blocks whose stamps stayed zero excluded."""
|
| 829 |
+
if clock_ghz is None:
|
| 830 |
+
clock_ghz = measure_clock(stamps.device)
|
| 831 |
+
|
| 832 |
+
def us(ticks):
|
| 833 |
+
return round(ticks / clock_ghz / 1000.0, 3)
|
| 834 |
+
|
| 835 |
+
if stamps.dim() == 1:
|
| 836 |
+
vals = stamps.cpu().tolist()
|
| 837 |
+
phases = []
|
| 838 |
+
for i in range(len(vals) - 1):
|
| 839 |
+
ticks = vals[i + 1] - vals[i]
|
| 840 |
+
name = labels[i] if labels and i < len(labels) else f"phase_{i}"
|
| 841 |
+
phases.append({"phase": name, "ticks": ticks, "us": us(ticks)})
|
| 842 |
+
return {"phases": phases, "clock_ghz": round(clock_ghz, 3)}
|
| 843 |
+
|
| 844 |
+
m = stamps.cpu()
|
| 845 |
+
live = (m != 0).all(dim=1)
|
| 846 |
+
m = m[live]
|
| 847 |
+
deltas = (m[:, 1:] - m[:, :-1]).to(torch.float64)
|
| 848 |
+
phases = []
|
| 849 |
+
for i in range(deltas.shape[1]):
|
| 850 |
+
col = deltas[:, i]
|
| 851 |
+
name = labels[i] if labels and i < len(labels) else f"phase_{i}"
|
| 852 |
+
med = float(col.median().item()) if col.numel() else 0.0
|
| 853 |
+
phases.append({
|
| 854 |
+
"phase": name,
|
| 855 |
+
"ticks_median": int(med),
|
| 856 |
+
"ticks_min": int(col.min().item()) if col.numel() else 0,
|
| 857 |
+
"ticks_max": int(col.max().item()) if col.numel() else 0,
|
| 858 |
+
"us_median": us(med),
|
| 859 |
+
"blocks": int(col.numel()),
|
| 860 |
+
})
|
| 861 |
+
return {"phases": phases, "clock_ghz": round(clock_ghz, 3),
|
| 862 |
+
"blocks_reporting": int(live.sum().item()),
|
| 863 |
+
"blocks_total": int(live.numel())}
|
build/torch211-cxx11-cu128-x86_64-linux/_metakernel_cuda_4611d12.abi3.so
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:741d3ad6ca34a13caade26ca26546a48c2a91c819c197dbb10e98336b5470d98
|
| 3 |
+
size 941440
|
build/torch211-cxx11-cu128-x86_64-linux/_ops.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from . import _metakernel_cuda_4611d12
|
| 3 |
+
ops = torch.ops._metakernel_cuda_4611d12
|
| 4 |
+
|
| 5 |
+
def add_op_namespace_prefix(op_name: str):
|
| 6 |
+
"""
|
| 7 |
+
Prefix op by namespace.
|
| 8 |
+
"""
|
| 9 |
+
return f"_metakernel_cuda_4611d12::{op_name}"
|
build/torch211-cxx11-cu128-x86_64-linux/metadata.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "metakernel",
|
| 3 |
+
"id": "_metakernel_cuda_4611d12",
|
| 4 |
+
"version": 1,
|
| 5 |
+
"license": "Apache-2.0",
|
| 6 |
+
"python-depends": [],
|
| 7 |
+
"backend": {
|
| 8 |
+
"type": "cuda",
|
| 9 |
+
"archs": [
|
| 10 |
+
"10.0",
|
| 11 |
+
"12.0",
|
| 12 |
+
"8.0",
|
| 13 |
+
"8.6",
|
| 14 |
+
"8.9",
|
| 15 |
+
"9.0"
|
| 16 |
+
]
|
| 17 |
+
},
|
| 18 |
+
"digest": {
|
| 19 |
+
"algorithm": "sha256",
|
| 20 |
+
"files": {
|
| 21 |
+
"__init__.py": "cZxKX5I/2EKCMC2gdCwflUsZaQJWJesmAHq2w+fVjXo=",
|
| 22 |
+
"_metakernel_cuda_4611d12.abi3.so": "dB061so0oTyq3ibKJlRqSMKpHIGcGX27EOmDNrVHDZg=",
|
| 23 |
+
"_ops.py": "uZlR720EcTy8M7BCydmvaZAlKonkwTU3778nhR9yQOE=",
|
| 24 |
+
"metakernel/__init__.py": "DFYPlrhXwYjEqCl/8n0SmWGZV8NFml5DPhMjKfv98GY="
|
| 25 |
+
}
|
| 26 |
+
},
|
| 27 |
+
"provenance": {
|
| 28 |
+
"kernel-builder": {
|
| 29 |
+
"version": "0.17.0-dev0",
|
| 30 |
+
"sha": "19aaa6421e674e9fecc352bbae6eab81d19a6bf4",
|
| 31 |
+
"dirty": false
|
| 32 |
+
},
|
| 33 |
+
"kernel": {
|
| 34 |
+
"sha": "4611d1213a76a9ec1d12646553a528feb60394ff",
|
| 35 |
+
"dirty": false
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
}
|
build/torch211-cxx11-cu128-x86_64-linux/metakernel/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ctypes
|
| 2 |
+
import importlib.util
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from types import ModuleType
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def _import_from_path(file_path: Path) -> ModuleType:
|
| 9 |
+
# We cannot use the module name as-is, after adding it to `sys.modules`,
|
| 10 |
+
# it would also be used for other imports. So, we make a module name that
|
| 11 |
+
# depends on the path for it to be unique using the hex-encoded hash of
|
| 12 |
+
# the path.
|
| 13 |
+
path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
|
| 14 |
+
module_name = path_hash
|
| 15 |
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
| 16 |
+
if spec is None:
|
| 17 |
+
raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
|
| 18 |
+
module = importlib.util.module_from_spec(spec)
|
| 19 |
+
if module is None:
|
| 20 |
+
raise ImportError(f"Cannot load module {module_name} from spec")
|
| 21 |
+
sys.modules[module_name] = module
|
| 22 |
+
spec.loader.exec_module(module) # type: ignore
|
| 23 |
+
return module
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
|
build/torch211-cxx11-cu130-x86_64-linux/__init__.py
ADDED
|
@@ -0,0 +1,863 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""metakernel: the measuring instrument for the kernel-design loop.
|
| 2 |
+
|
| 3 |
+
An agent iterating on CUDA candidates is good at writing code and bad at
|
| 4 |
+
trusting its own measurements; it needs trustworthy timing, adversarial
|
| 5 |
+
correctness comparison, and measured facts about the device it targets.
|
| 6 |
+
metakernel ships those as loadable ops:
|
| 7 |
+
|
| 8 |
+
from kernels import get_kernel
|
| 9 |
+
mk = get_kernel("phanerozoic/metakernel", version=1,
|
| 10 |
+
trust_remote_code=True)
|
| 11 |
+
|
| 12 |
+
dossier = mk.probe_device() # measured facts
|
| 13 |
+
report = mk.bench(lambda: op(x), iters=200) # throttle-rejected
|
| 14 |
+
verdict = mk.compare(ref, cand, gen) # ULP evidence
|
| 15 |
+
table = mk.sweep(factory, {"BLOCK": [64, 128]}) # config search
|
| 16 |
+
|
| 17 |
+
Every op returns plain dicts and tensors, never prose. Every dossier entry
|
| 18 |
+
is produced by a dedicated probe kernel rather than a spec sheet, carries
|
| 19 |
+
the SM clock it was measured under, and labels throttled samples instead
|
| 20 |
+
of letting them poison medians. No hardware counters are used: everything
|
| 21 |
+
is inferred from timing, clocks, and the occupancy API, so entries carry a
|
| 22 |
+
confidence field (timing-differential inference can misattribute).
|
| 23 |
+
"""
|
| 24 |
+
import math
|
| 25 |
+
import time
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
|
| 29 |
+
from ._ops import ops
|
| 30 |
+
|
| 31 |
+
__all__ = ["probe_device", "bench", "compare", "order_shuffles",
|
| 32 |
+
"fuzz_shapes", "sweep", "alloc_stamps", "read_stamps",
|
| 33 |
+
"measure_clock", "ulp_diff", "ops"]
|
| 34 |
+
|
| 35 |
+
_MMA_KINDS = {
|
| 36 |
+
"fp16": (0, 16 * 16 * 16),
|
| 37 |
+
"bf16": (1, 16 * 16 * 16),
|
| 38 |
+
"tf32": (2, 16 * 16 * 8),
|
| 39 |
+
"int8": (3, 16 * 16 * 16),
|
| 40 |
+
"fp8_e4m3": (4, 16 * 8 * 32),
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _sync():
|
| 45 |
+
torch.cuda.synchronize()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _events(fn, reps):
|
| 49 |
+
"""Time reps calls of fn with one event pair each; returns ms list."""
|
| 50 |
+
starts = [torch.cuda.Event(enable_timing=True) for _ in range(reps)]
|
| 51 |
+
stops = [torch.cuda.Event(enable_timing=True) for _ in range(reps)]
|
| 52 |
+
for s, e in zip(starts, stops):
|
| 53 |
+
s.record()
|
| 54 |
+
fn()
|
| 55 |
+
e.record()
|
| 56 |
+
_sync()
|
| 57 |
+
return [s.elapsed_time(e) for s, e in zip(starts, stops)]
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def measure_clock(device="cuda", ticks=4_000_000, blocks=None):
|
| 61 |
+
"""Effective SM clock in GHz under an FMA spin of `ticks` clock64 ticks.
|
| 62 |
+
Wall time over elapsed ticks is the frequency the ticks ran at."""
|
| 63 |
+
props = torch.cuda.get_device_properties(device)
|
| 64 |
+
if blocks is None:
|
| 65 |
+
blocks = props.multi_processor_count
|
| 66 |
+
out = torch.zeros(2, dtype=torch.int64, device=device)
|
| 67 |
+
sink = torch.zeros(2, dtype=torch.float32, device=device)
|
| 68 |
+
ops.mk_spin(blocks, 256, ticks, out, sink) # warm
|
| 69 |
+
start = torch.cuda.Event(enable_timing=True)
|
| 70 |
+
stop = torch.cuda.Event(enable_timing=True)
|
| 71 |
+
start.record()
|
| 72 |
+
ops.mk_spin(blocks, 256, ticks, out, sink)
|
| 73 |
+
stop.record()
|
| 74 |
+
_sync()
|
| 75 |
+
ms = start.elapsed_time(stop)
|
| 76 |
+
elapsed = int(out[0].item())
|
| 77 |
+
return elapsed / (ms * 1e6)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _median(xs):
|
| 81 |
+
s = sorted(xs)
|
| 82 |
+
n = len(s)
|
| 83 |
+
return s[n // 2] if n % 2 else 0.5 * (s[n // 2 - 1] + s[n // 2])
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _entry(value, units, method, clock_ghz, confidence, note=""):
|
| 87 |
+
d = {"value": value, "units": units, "method": method,
|
| 88 |
+
"clock_ghz": round(clock_ghz, 3), "confidence": confidence}
|
| 89 |
+
if note:
|
| 90 |
+
d["note"] = note
|
| 91 |
+
return d
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _sattolo_ring(n, device, generator=None):
|
| 95 |
+
"""A random single-cycle permutation as an int32 next-pointer table, so
|
| 96 |
+
a pointer chase visits every slot before repeating."""
|
| 97 |
+
perm = torch.randperm(n, generator=generator)
|
| 98 |
+
ring = torch.empty(n, dtype=torch.int32)
|
| 99 |
+
ring[perm] = torch.roll(perm, -1).to(torch.int32)
|
| 100 |
+
return ring.to(device)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def probe_device(device="cuda", quick=False):
|
| 104 |
+
"""Empirical device dossier. Each entry is measured by a dedicated
|
| 105 |
+
probe kernel and carries units, method, the SM clock it ran under, and
|
| 106 |
+
a confidence field."""
|
| 107 |
+
props = torch.cuda.get_device_properties(device)
|
| 108 |
+
sm = props.multi_processor_count
|
| 109 |
+
l2 = props.L2_cache_size
|
| 110 |
+
d = {
|
| 111 |
+
"device": {
|
| 112 |
+
"name": props.name,
|
| 113 |
+
"sm_arch": f"sm{props.major}{props.minor}",
|
| 114 |
+
"sms": sm,
|
| 115 |
+
"l2_bytes": l2,
|
| 116 |
+
"vram_bytes": props.total_memory,
|
| 117 |
+
"torch": torch.__version__,
|
| 118 |
+
"cuda_runtime": torch.version.cuda,
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
sink = torch.zeros(2, dtype=torch.float32, device=device)
|
| 122 |
+
out64 = torch.zeros(2, dtype=torch.int64, device=device)
|
| 123 |
+
reps = 3 if quick else 5
|
| 124 |
+
|
| 125 |
+
# ---- clock under sustained load
|
| 126 |
+
traj = []
|
| 127 |
+
windows = 8 if quick else 24
|
| 128 |
+
for _ in range(windows):
|
| 129 |
+
traj.append(measure_clock(device, ticks=30_000_000))
|
| 130 |
+
d["clock"] = {
|
| 131 |
+
"max_ghz": round(max(traj), 3),
|
| 132 |
+
"min_ghz": round(min(traj), 3),
|
| 133 |
+
"final_ghz": round(traj[-1], 3),
|
| 134 |
+
"sag_pct": round(100 * (1 - traj[-1] / max(traj)), 1),
|
| 135 |
+
"units": "GHz",
|
| 136 |
+
"method": f"{windows} back-to-back FMA spin windows of 30M ticks; "
|
| 137 |
+
"wall/ticks per window",
|
| 138 |
+
"confidence": "high",
|
| 139 |
+
}
|
| 140 |
+
clk = max(traj)
|
| 141 |
+
|
| 142 |
+
# ---- HBM bandwidth: triad per access width, plus read-only,
|
| 143 |
+
# write-only, and random-gather
|
| 144 |
+
n4 = 16 << 20 # 16M float4 = 256 MB per buffer
|
| 145 |
+
if props.total_memory < 4 << 30:
|
| 146 |
+
n4 = 4 << 20
|
| 147 |
+
a = torch.rand(n4 * 4, device=device)
|
| 148 |
+
b = torch.rand(n4 * 4, device=device)
|
| 149 |
+
c = torch.empty(n4 * 4, device=device)
|
| 150 |
+
hbm = {}
|
| 151 |
+
for width in (1, 2, 4):
|
| 152 |
+
ops.mk_triad(a, b, c, width, 1.5) # warm
|
| 153 |
+
ms = _median(_events(lambda w=width: ops.mk_triad(a, b, c, w, 1.5),
|
| 154 |
+
reps))
|
| 155 |
+
gbs = 3 * a.numel() * 4 / (ms * 1e-3) / 1e9
|
| 156 |
+
hbm[f"triad_{4 * width}B"] = round(gbs, 1)
|
| 157 |
+
ops.mk_read(a, sink)
|
| 158 |
+
ms = _median(_events(lambda: ops.mk_read(a, sink), reps))
|
| 159 |
+
hbm["read_only"] = round(a.numel() * 4 / (ms * 1e-3) / 1e9, 1)
|
| 160 |
+
ops.mk_write(c, 1.0)
|
| 161 |
+
ms = _median(_events(lambda: ops.mk_write(c, 1.0), reps))
|
| 162 |
+
hbm["write_only"] = round(c.numel() * 4 / (ms * 1e-3) / 1e9, 1)
|
| 163 |
+
g = torch.Generator(device="cpu").manual_seed(7)
|
| 164 |
+
idx = torch.randint(0, n4, (n4,), generator=g,
|
| 165 |
+
dtype=torch.int32).to(device)
|
| 166 |
+
ops.mk_gather(a, idx, c, 256, 0)
|
| 167 |
+
ms = _median(_events(lambda: ops.mk_gather(a, idx, c, 256, 0), reps))
|
| 168 |
+
# traffic accounting: idx reads + 16B random loads + streamed stores
|
| 169 |
+
hbm["random_gather_16B"] = round(
|
| 170 |
+
(idx.numel() * 4 + 2 * n4 * 16) / (ms * 1e-3) / 1e9, 1)
|
| 171 |
+
d["hbm_bw"] = _entry(hbm, "GB/s", "streaming triad c=a+s*b per access "
|
| 172 |
+
"width; read-only fold; write-only fill; random "
|
| 173 |
+
"float4 gather (16B granules, nominal traffic)",
|
| 174 |
+
clk, "high",
|
| 175 |
+
"gather moves full cache lines per 16B granule, "
|
| 176 |
+
"so its nominal number is the achievable rate for "
|
| 177 |
+
"random access, not a line-efficiency claim")
|
| 178 |
+
|
| 179 |
+
# ---- L2 bandwidth (working set 3 buffers inside L2)
|
| 180 |
+
ws = max(l2 // 4, 1 << 20)
|
| 181 |
+
nl2 = ws // 16 # float4 elems per buffer
|
| 182 |
+
al = a[:nl2 * 4].contiguous()
|
| 183 |
+
bl = b[:nl2 * 4].contiguous()
|
| 184 |
+
cl = c[:nl2 * 4].contiguous()
|
| 185 |
+
passes = 64
|
| 186 |
+
ops.mk_triad_passes(al, bl, cl, 1.5, passes)
|
| 187 |
+
ms = _median(_events(
|
| 188 |
+
lambda: ops.mk_triad_passes(al, bl, cl, 1.5, passes), reps))
|
| 189 |
+
gbs = 3 * al.numel() * 4 * passes / (ms * 1e-3) / 1e9
|
| 190 |
+
d["l2_bw"] = _entry(round(gbs, 1), "GB/s",
|
| 191 |
+
f"in-kernel repeated triad passes over "
|
| 192 |
+
f"{3 * ws >> 20} MB resident working set", clk,
|
| 193 |
+
"medium", "first pass streams from HBM; amortized "
|
| 194 |
+
f"over {passes} passes")
|
| 195 |
+
|
| 196 |
+
# ---- latencies: pointer chase per level. Footprints are chosen so
|
| 197 |
+
# the chain hits the intended level: the L2 ring (2 MB) exceeds L1,
|
| 198 |
+
# fits L2 on every supported part, and spans few enough pages to stay
|
| 199 |
+
# TLB-resident after a full-cycle warm walk; the HBM chain is long
|
| 200 |
+
# enough (2x L2 in touched lines) that it evicts itself and cannot be
|
| 201 |
+
# served from cache.
|
| 202 |
+
lat = {}
|
| 203 |
+
idle_clk = measure_clock(device, ticks=2_000_000, blocks=1)
|
| 204 |
+
ring_s = _sattolo_ring(8192, device)
|
| 205 |
+
ops.mk_chase_shared(ring_s, 1 << 14, out64)
|
| 206 |
+
_sync()
|
| 207 |
+
hops = 1 << 16
|
| 208 |
+
ops.mk_chase_shared(ring_s, hops, out64)
|
| 209 |
+
_sync()
|
| 210 |
+
lat["smem_cycles"] = round(int(out64[0].item()) / hops, 1)
|
| 211 |
+
|
| 212 |
+
l1_ring_n = (16 << 10) // 4 # 16 KB: L1-resident after a warm walk
|
| 213 |
+
ring_l1 = _sattolo_ring(l1_ring_n, device)
|
| 214 |
+
ops.mk_chase_global(ring_l1, l1_ring_n, out64)
|
| 215 |
+
_sync()
|
| 216 |
+
hops_l1 = 1 << 15
|
| 217 |
+
ops.mk_chase_global(ring_l1, hops_l1, out64)
|
| 218 |
+
_sync()
|
| 219 |
+
lat["l1_cycles"] = round(int(out64[0].item()) / hops_l1, 1)
|
| 220 |
+
|
| 221 |
+
l2_ring_n = (2 << 20) // 4
|
| 222 |
+
ring_l2 = _sattolo_ring(l2_ring_n, device)
|
| 223 |
+
ops.mk_chase_global(ring_l2, l2_ring_n, out64) # full-cycle warm walk
|
| 224 |
+
_sync()
|
| 225 |
+
hops_l2 = 1 << 14
|
| 226 |
+
ops.mk_chase_global(ring_l2, hops_l2, out64)
|
| 227 |
+
_sync()
|
| 228 |
+
lat["l2_cycles"] = round(int(out64[0].item()) / hops_l2, 1)
|
| 229 |
+
|
| 230 |
+
hbm_bytes = min(max(4 * l2, 64 << 20), props.total_memory // 8)
|
| 231 |
+
ring_h = _sattolo_ring(int(hbm_bytes) // 4, device)
|
| 232 |
+
hops_h = max((2 * l2) // 128, 1 << 17)
|
| 233 |
+
ops.mk_chase_global(ring_h, 1 << 14, out64) # fault pages / warm TLB
|
| 234 |
+
_sync()
|
| 235 |
+
ops.mk_chase_global(ring_h, hops_h, out64)
|
| 236 |
+
_sync()
|
| 237 |
+
lat["hbm_cycles"] = round(int(out64[0].item()) / hops_h, 1)
|
| 238 |
+
lat["ns_per_cycle"] = round(1.0 / idle_clk, 3)
|
| 239 |
+
d["latency"] = _entry(lat, "cycles (single-lane dependent loads)",
|
| 240 |
+
"Sattolo-cycle pointer chase; 16 KB ring for L1, "
|
| 241 |
+
"2 MB TLB-resident ring for L2, self-evicting "
|
| 242 |
+
"chain (2x L2 in touched lines) for HBM",
|
| 243 |
+
idle_clk, "high",
|
| 244 |
+
"smem includes index arithmetic; HBM includes "
|
| 245 |
+
"residual TLB effects at large footprints")
|
| 246 |
+
del ring_h, ring_l2, ring_l1, ring_s
|
| 247 |
+
|
| 248 |
+
# ---- fp32 FMA and tensor-core throughput
|
| 249 |
+
blocks = sm * 4
|
| 250 |
+
iters = 20_000 if quick else 60_000
|
| 251 |
+
ops.mk_fma_f32(blocks, 256, iters, sink)
|
| 252 |
+
ms = _median(_events(lambda: ops.mk_fma_f32(blocks, 256, iters, sink),
|
| 253 |
+
reps))
|
| 254 |
+
flops = blocks * 256 * iters * 16 * 2 * 2 # 2 chains x 2 flops x unroll
|
| 255 |
+
d["fma_f32"] = _entry(round(flops / (ms * 1e-3) / 1e12, 2), "TFLOP/s",
|
| 256 |
+
"register-resident FMA chains", clk, "high")
|
| 257 |
+
sink64 = torch.zeros(2, dtype=torch.float64, device=device)
|
| 258 |
+
it64 = max(iters // 16, 1000)
|
| 259 |
+
ops.mk_fma_f64(blocks, 256, it64, sink64)
|
| 260 |
+
ms = _median(_events(
|
| 261 |
+
lambda: ops.mk_fma_f64(blocks, 256, it64, sink64), reps))
|
| 262 |
+
flops64 = blocks * 256 * it64 * 16 * 2 * 2
|
| 263 |
+
d["fma_f64"] = _entry(round(flops64 / (ms * 1e-3) / 1e12, 3), "TFLOP/s",
|
| 264 |
+
"register-resident double FMA chains (the DFMA "
|
| 265 |
+
"pipe)", clk, "high")
|
| 266 |
+
mma = {}
|
| 267 |
+
for name, (kind, mnk) in _MMA_KINDS.items():
|
| 268 |
+
it = 30_000 if quick else 100_000
|
| 269 |
+
r = ops.mk_mma(kind, blocks, 8, 512, sink)
|
| 270 |
+
if r != 0:
|
| 271 |
+
mma[name] = "unsupported"
|
| 272 |
+
continue
|
| 273 |
+
_sync()
|
| 274 |
+
ms = _median(_events(
|
| 275 |
+
lambda k=kind, i=it: ops.mk_mma(k, blocks, 8, i, sink), reps))
|
| 276 |
+
flops = blocks * 8 * it * 4 * 2 * mnk
|
| 277 |
+
mma[name] = round(flops / (ms * 1e-3) / 1e12, 1)
|
| 278 |
+
d["mma"] = _entry(mma, "TFLOP/s (TOP/s for int8)",
|
| 279 |
+
"register-fragment mma_sync loops (wmma; fp8 via "
|
| 280 |
+
"inline PTX m16n8k32)", clk, "medium",
|
| 281 |
+
"dense-tile rate without memory traffic; real "
|
| 282 |
+
"kernels see this only when staging hides behind it")
|
| 283 |
+
|
| 284 |
+
# ---- atomic contention curve: throughput vs number of distinct
|
| 285 |
+
# addresses the whole grid hammers (slots = 1 is full contention;
|
| 286 |
+
# slots = threads is contention-free), plus the shared-memory point
|
| 287 |
+
n_thr = blocks * 256
|
| 288 |
+
buf = torch.zeros(n_thr, dtype=torch.float32, device=device)
|
| 289 |
+
at = {"curve_slots": {}}
|
| 290 |
+
it_at = 2_000 if quick else 6_000
|
| 291 |
+
slot_points = [1, 8, 64, 512, 4096, n_thr]
|
| 292 |
+
for slots in slot_points:
|
| 293 |
+
buf.zero_()
|
| 294 |
+
ops.mk_atomics(1, blocks, 256, it_at, buf, slots)
|
| 295 |
+
ms = _median(_events(
|
| 296 |
+
lambda s=slots: ops.mk_atomics(1, blocks, 256, it_at, buf, s),
|
| 297 |
+
reps))
|
| 298 |
+
at["curve_slots"][str(slots)] = round(
|
| 299 |
+
n_thr * it_at / (ms * 1e-3) / 1e9, 2)
|
| 300 |
+
ops.mk_atomics(3, blocks, 256, it_at, buf, 1)
|
| 301 |
+
ms = _median(_events(
|
| 302 |
+
lambda: ops.mk_atomics(3, blocks, 256, it_at, buf, 1), reps))
|
| 303 |
+
at["shared_same_addr"] = round(n_thr * it_at / (ms * 1e-3) / 1e9, 2)
|
| 304 |
+
d["atomics_f32"] = _entry(at, "Gop/s",
|
| 305 |
+
"atomicAdd hammer; curve sweeps the number "
|
| 306 |
+
"of distinct global addresses from 1 (full "
|
| 307 |
+
"contention) to one per thread", clk, "high")
|
| 308 |
+
|
| 309 |
+
# ---- launch overhead
|
| 310 |
+
anchor = sink
|
| 311 |
+
n_launch = 500 if quick else 2000
|
| 312 |
+
_sync()
|
| 313 |
+
t0 = time.perf_counter()
|
| 314 |
+
for _ in range(n_launch):
|
| 315 |
+
ops.mk_empty(anchor)
|
| 316 |
+
_sync()
|
| 317 |
+
queued = (time.perf_counter() - t0) / n_launch * 1e6
|
| 318 |
+
n_rt = 100 if quick else 300
|
| 319 |
+
t0 = time.perf_counter()
|
| 320 |
+
for _ in range(n_rt):
|
| 321 |
+
ops.mk_empty(anchor)
|
| 322 |
+
_sync()
|
| 323 |
+
rt = (time.perf_counter() - t0) / n_rt * 1e6
|
| 324 |
+
d["launch_overhead"] = _entry(
|
| 325 |
+
{"queued_us": round(queued, 2), "round_trip_us": round(rt, 2)},
|
| 326 |
+
"us", "empty-kernel trains, queued vs launch+sync", clk, "high",
|
| 327 |
+
"host-dependent: includes torch op dispatch")
|
| 328 |
+
|
| 329 |
+
# ---- grid barrier
|
| 330 |
+
bar_iters = 500 if quick else 2000
|
| 331 |
+
r = ops.mk_barrier(sm, 256, bar_iters, out64)
|
| 332 |
+
if r == 0:
|
| 333 |
+
_sync()
|
| 334 |
+
ticks = int(out64[0].item())
|
| 335 |
+
d["grid_barrier"] = _entry(
|
| 336 |
+
round(ticks / bar_iters / clk / 1000.0, 2), "us per grid.sync",
|
| 337 |
+
f"cooperative kernel, {sm} blocks x 256 threads", clk, "high")
|
| 338 |
+
else:
|
| 339 |
+
d["grid_barrier"] = _entry("unsupported", "", "cooperative launch "
|
| 340 |
+
"unavailable or grid too large", clk,
|
| 341 |
+
"high")
|
| 342 |
+
|
| 343 |
+
# ---- occupancy table (API) plus timed knees: the same random gather
|
| 344 |
+
# at every (threads, dyn_smem) point; when the forced blocks/SM stop
|
| 345 |
+
# covering memory latency, achieved bandwidth falls off the cliff
|
| 346 |
+
occ = {}
|
| 347 |
+
for threads in (128, 256, 512, 1024):
|
| 348 |
+
row = {}
|
| 349 |
+
for smem_kb in (0, 16, 32, 48):
|
| 350 |
+
row[f"{smem_kb}KB"] = int(ops.mk_occupancy(
|
| 351 |
+
threads, smem_kb * 1024, anchor))
|
| 352 |
+
occ[f"t{threads}"] = row
|
| 353 |
+
d["occupancy_blocks_per_sm"] = _entry(
|
| 354 |
+
occ, "blocks/SM", "occupancy API on the float4 triad kernel",
|
| 355 |
+
clk, "high", "API-derived, not timing-derived")
|
| 356 |
+
|
| 357 |
+
knee_iters = 6_000 if quick else 20_000
|
| 358 |
+
knee_blocks = sm * 8
|
| 359 |
+
knees = {}
|
| 360 |
+
for threads in (128, 256, 1024):
|
| 361 |
+
row = {}
|
| 362 |
+
for smem_kb in (0, 32, 48):
|
| 363 |
+
ops.mk_fma_dep(knee_blocks, threads, smem_kb * 1024,
|
| 364 |
+
knee_iters, sink)
|
| 365 |
+
ms = _median(_events(
|
| 366 |
+
lambda t=threads, s=smem_kb: ops.mk_fma_dep(
|
| 367 |
+
knee_blocks, t, s * 1024, knee_iters, sink), reps))
|
| 368 |
+
fl = knee_blocks * threads * knee_iters * 16 * 2
|
| 369 |
+
row[f"{smem_kb}KB"] = round(fl / (ms * 1e-3) / 1e12, 2)
|
| 370 |
+
knees[f"t{threads}"] = row
|
| 371 |
+
d["occupancy_knees_fma_tflops"] = _entry(
|
| 372 |
+
knees, "TFLOP/s", "a single dependent FMA chain per thread, timed "
|
| 373 |
+
"at forced (threads, dyn smem) occupancies over fixed total work; "
|
| 374 |
+
"throughput is proportional to resident threads until the pipe "
|
| 375 |
+
"saturates, so the cliff marks where occupancy stops covering "
|
| 376 |
+
"latency", clk, "high",
|
| 377 |
+
"timing-derived counterpart to the API table above")
|
| 378 |
+
return d
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def bench(fn, iters=100, warmup=10, bytes=None, flops=None, dossier=None,
|
| 382 |
+
dtype="fp16", clock_guard=0.97, clock_stride=8, graph=False,
|
| 383 |
+
device="cuda"):
|
| 384 |
+
"""Median-of-N cudaEvent timing with throttle rejection.
|
| 385 |
+
|
| 386 |
+
The SM clock is probed every `clock_stride` samples; samples inside a
|
| 387 |
+
window whose clock sagged below `clock_guard` x the best observed
|
| 388 |
+
clock are rejected and counted rather than silently averaged. With
|
| 389 |
+
declared `bytes`/`flops` per call, reports achieved GB/s / TFLOP/s;
|
| 390 |
+
with a `dossier`, adds roofline placement against the measured peaks.
|
| 391 |
+
|
| 392 |
+
`graph=True` captures fn() into a CUDA graph and times graph replays,
|
| 393 |
+
stripping Python and torch dispatch from the measurement: the right
|
| 394 |
+
mode for kernels in the few-microsecond range, where dispatch is
|
| 395 |
+
otherwise the thing being measured. Capture requires fn to be
|
| 396 |
+
graph-safe (no sync, no allocation growth, static shapes)."""
|
| 397 |
+
for _ in range(warmup):
|
| 398 |
+
fn()
|
| 399 |
+
_sync()
|
| 400 |
+
if graph:
|
| 401 |
+
g = torch.cuda.CUDAGraph()
|
| 402 |
+
with torch.cuda.graph(g):
|
| 403 |
+
fn()
|
| 404 |
+
fn = g.replay
|
| 405 |
+
for _ in range(warmup):
|
| 406 |
+
fn()
|
| 407 |
+
_sync()
|
| 408 |
+
samples = []
|
| 409 |
+
clocks = []
|
| 410 |
+
stride = clock_stride
|
| 411 |
+
for base in range(0, iters, stride):
|
| 412 |
+
clocks.append(measure_clock(device, ticks=1_500_000))
|
| 413 |
+
n = min(stride, iters - base)
|
| 414 |
+
samples.append((_events(fn, n), len(clocks) - 1))
|
| 415 |
+
clocks.append(measure_clock(device, ticks=1_500_000))
|
| 416 |
+
best_clk = max(clocks)
|
| 417 |
+
kept, rejected = [], 0
|
| 418 |
+
flat = []
|
| 419 |
+
for group, ci in samples:
|
| 420 |
+
window_ok = (clocks[ci] >= clock_guard * best_clk and
|
| 421 |
+
clocks[min(ci + 1, len(clocks) - 1)] >=
|
| 422 |
+
clock_guard * best_clk)
|
| 423 |
+
for ms in group:
|
| 424 |
+
flat.append(ms)
|
| 425 |
+
if window_ok:
|
| 426 |
+
kept.append(ms)
|
| 427 |
+
else:
|
| 428 |
+
rejected += 1
|
| 429 |
+
if not kept:
|
| 430 |
+
kept = flat
|
| 431 |
+
med = _median(kept)
|
| 432 |
+
rep = {
|
| 433 |
+
"median_ms": round(med, 4),
|
| 434 |
+
"min_ms": round(min(kept), 4),
|
| 435 |
+
"mean_ms": round(sum(kept) / len(kept), 4),
|
| 436 |
+
"samples": len(flat),
|
| 437 |
+
"rejected_throttled": rejected,
|
| 438 |
+
"graph_captured": bool(graph),
|
| 439 |
+
"clock_ghz": {"best": round(best_clk, 3),
|
| 440 |
+
"worst": round(min(clocks), 3)},
|
| 441 |
+
}
|
| 442 |
+
if bytes is not None:
|
| 443 |
+
rep["achieved_gbs"] = round(bytes / (med * 1e-3) / 1e9, 1)
|
| 444 |
+
if flops is not None:
|
| 445 |
+
rep["achieved_tflops"] = round(flops / (med * 1e-3) / 1e12, 2)
|
| 446 |
+
if bytes and flops:
|
| 447 |
+
rep["arithmetic_intensity"] = round(flops / bytes, 2)
|
| 448 |
+
if dossier is not None and bytes and flops:
|
| 449 |
+
bw = max(dossier["hbm_bw"]["value"].values()) * 1e9
|
| 450 |
+
mma_v = dossier["mma"]["value"].get(dtype)
|
| 451 |
+
peak = (mma_v if isinstance(mma_v, (int, float))
|
| 452 |
+
else dossier["fma_f32"]["value"]) * 1e12
|
| 453 |
+
ai = flops / bytes
|
| 454 |
+
roof = min(peak, ai * bw)
|
| 455 |
+
rep["roofline"] = {
|
| 456 |
+
"bound": "memory" if ai * bw < peak else "compute",
|
| 457 |
+
"ridge_flops_per_byte": round(peak / bw, 1),
|
| 458 |
+
"pct_of_roof": round(100 * (flops / (med * 1e-3)) / roof, 1),
|
| 459 |
+
"peak_dtype": dtype,
|
| 460 |
+
}
|
| 461 |
+
return rep
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
def _to_ordered_int(t):
|
| 465 |
+
"""Map float bit patterns to integers whose difference is the ULP
|
| 466 |
+
distance: positive floats keep their bits, negative floats mirror
|
| 467 |
+
below zero, so the mapping is monotone in the float value."""
|
| 468 |
+
if t.dtype == torch.float32:
|
| 469 |
+
u = t.view(torch.int32).to(torch.int64) & 0xFFFFFFFF
|
| 470 |
+
half = 1 << 31
|
| 471 |
+
elif t.dtype in (torch.float16, torch.bfloat16):
|
| 472 |
+
u = t.view(torch.int16).to(torch.int64) & 0xFFFF
|
| 473 |
+
half = 1 << 15
|
| 474 |
+
elif t.dtype == torch.float64:
|
| 475 |
+
i = t.view(torch.int64)
|
| 476 |
+
return torch.where(i >= 0, i, (-(1 << 63)) - i)
|
| 477 |
+
else:
|
| 478 |
+
raise ValueError(f"unsupported dtype {t.dtype}")
|
| 479 |
+
return torch.where(u < half, u, half - u)
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
def ulp_diff(a, b):
|
| 483 |
+
"""Elementwise ULP distance between same-dtype float tensors. NaNs in
|
| 484 |
+
the same position count as 0; a one-sided NaN counts as 2**62."""
|
| 485 |
+
assert a.dtype == b.dtype and a.shape == b.shape
|
| 486 |
+
ia, ib = _to_ordered_int(a), _to_ordered_int(b)
|
| 487 |
+
d = (ia - ib).abs()
|
| 488 |
+
both_nan = torch.isnan(a) & torch.isnan(b)
|
| 489 |
+
one_nan = torch.isnan(a) ^ torch.isnan(b)
|
| 490 |
+
d = torch.where(both_nan, torch.zeros_like(d), d)
|
| 491 |
+
d = torch.where(one_nan, torch.full_like(d, 1 << 62), d)
|
| 492 |
+
return d
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
class order_shuffles:
|
| 496 |
+
"""Built-in order shuffles for the common contractual-noise axes."""
|
| 497 |
+
|
| 498 |
+
@staticmethod
|
| 499 |
+
def last_dim():
|
| 500 |
+
"""Permute the last dim of every tensor arg identically: the
|
| 501 |
+
accumulation order of a last-dim reduction changes, the value
|
| 502 |
+
does not."""
|
| 503 |
+
def shuffle(args, seed):
|
| 504 |
+
g = torch.Generator(device=args[0].device).manual_seed(seed)
|
| 505 |
+
p = torch.randperm(args[0].shape[-1], device=args[0].device,
|
| 506 |
+
generator=g)
|
| 507 |
+
return tuple(x[..., p] for x in args)
|
| 508 |
+
return shuffle
|
| 509 |
+
|
| 510 |
+
@staticmethod
|
| 511 |
+
def matmul_k():
|
| 512 |
+
"""Permute K of (A [.., M, K], B [.., K, N]) consistently: the
|
| 513 |
+
product is unchanged, the accumulation order is not."""
|
| 514 |
+
def shuffle(args, seed):
|
| 515 |
+
a_m, b_m = args[0], args[1]
|
| 516 |
+
g = torch.Generator(device=a_m.device).manual_seed(seed)
|
| 517 |
+
p = torch.randperm(a_m.shape[-1], device=a_m.device,
|
| 518 |
+
generator=g)
|
| 519 |
+
return (a_m[..., p], b_m[..., p, :]) + tuple(args[2:])
|
| 520 |
+
return shuffle
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
def _as_outputs(x):
|
| 524 |
+
return list(x) if isinstance(x, (tuple, list)) else [x]
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
def compare(ref, cand, gen, trials=8, order_shuffle=None, order_trials=8,
|
| 528 |
+
ulp_tol=None, atol=None, rtol=None, band_margin=4.0, seed=0):
|
| 529 |
+
"""Adversarial correctness comparison with ULP evidence.
|
| 530 |
+
|
| 531 |
+
`gen(seed) -> args` builds inputs; `ref(*args)` and `cand(*args)`
|
| 532 |
+
return a tensor or a tuple of tensors (each output is compared and
|
| 533 |
+
the worst governs). If `order_shuffle(args, seed) -> args` is given
|
| 534 |
+
and reorders contractual-noise dimensions (see `order_shuffles` for
|
| 535 |
+
the common cases), the reference is treated as a distribution:
|
| 536 |
+
candidates are judged against the measured reduction-order band
|
| 537 |
+
rather than a point. Verdict, stated in the report: max ULP <=
|
| 538 |
+
band_margin * band + 2 when a band is measured, else max ULP <=
|
| 539 |
+
ulp_tol (default 2); if `atol`/`rtol` are given they are an
|
| 540 |
+
alternative acceptance route, checked as |c - r| <= atol + rtol|r|."""
|
| 541 |
+
worst = {"max_ulp": -1}
|
| 542 |
+
total_hist = {}
|
| 543 |
+
per_trial = []
|
| 544 |
+
max_abs_seen = 0.0
|
| 545 |
+
max_rel_seen = 0.0
|
| 546 |
+
for t in range(trials):
|
| 547 |
+
args = gen(seed + t)
|
| 548 |
+
r_outs = _as_outputs(ref(*args))
|
| 549 |
+
c_outs = _as_outputs(cand(*args))
|
| 550 |
+
assert len(r_outs) == len(c_outs), "output arity mismatch"
|
| 551 |
+
# the order band is POINTWISE: a near-zero reduction output is
|
| 552 |
+
# legitimately thousands of ULP order-sensitive (cancellation)
|
| 553 |
+
# while its neighbors are stable, so each element is judged
|
| 554 |
+
# against its own measured sensitivity, not a global max
|
| 555 |
+
bands = None
|
| 556 |
+
band = 0
|
| 557 |
+
band_score = 0.0
|
| 558 |
+
if order_shuffle is not None:
|
| 559 |
+
bands = [torch.zeros_like(ro, dtype=torch.int64)
|
| 560 |
+
for ro in r_outs]
|
| 561 |
+
for k in range(order_trials):
|
| 562 |
+
r2 = _as_outputs(
|
| 563 |
+
ref(*order_shuffle(args, 1000 + seed + t * 131 + k)))
|
| 564 |
+
for bi, (ro, r2o) in enumerate(zip(r_outs, r2)):
|
| 565 |
+
bands[bi] = torch.maximum(bands[bi],
|
| 566 |
+
ulp_diff(ro, r2o))
|
| 567 |
+
band = max(int(b.max().item()) if b.numel() else 0
|
| 568 |
+
for b in bands)
|
| 569 |
+
max_ulp = 0
|
| 570 |
+
mean_ulp = 0.0
|
| 571 |
+
first_div = -1
|
| 572 |
+
n_el = 0
|
| 573 |
+
nan_r = nan_c = inf_r = inf_c = 0
|
| 574 |
+
for oi, (r, c) in enumerate(zip(r_outs, c_outs)):
|
| 575 |
+
d = ulp_diff(r, c)
|
| 576 |
+
if bands is not None and d.numel():
|
| 577 |
+
ratio = d.double() / (bands[oi].double() + 2.0)
|
| 578 |
+
band_score = max(band_score, float(ratio.max().item()))
|
| 579 |
+
if d.numel():
|
| 580 |
+
max_ulp = max(max_ulp, int(d.max().item()))
|
| 581 |
+
mean_ulp += float(d.double().sum().item())
|
| 582 |
+
n_el += d.numel()
|
| 583 |
+
nz = (d > 0)
|
| 584 |
+
if first_div < 0 and nz.any():
|
| 585 |
+
first_div = int(nz.flatten().nonzero()[0].item())
|
| 586 |
+
if nz.any():
|
| 587 |
+
buckets = torch.clamp(
|
| 588 |
+
torch.log2(d[nz].double()).floor(), 0, 62
|
| 589 |
+
).to(torch.int64)
|
| 590 |
+
counts = torch.bincount(buckets, minlength=63)
|
| 591 |
+
for bkt in counts.nonzero().flatten().tolist():
|
| 592 |
+
total_hist[bkt] = total_hist.get(bkt, 0) + int(
|
| 593 |
+
counts[bkt].item())
|
| 594 |
+
fin = torch.isfinite(r) & torch.isfinite(c)
|
| 595 |
+
if fin.any():
|
| 596 |
+
diff = (c[fin].double() - r[fin].double()).abs()
|
| 597 |
+
max_abs_seen = max(max_abs_seen,
|
| 598 |
+
float(diff.max().item()))
|
| 599 |
+
rel = diff / r[fin].double().abs().clamp(min=1e-300)
|
| 600 |
+
max_rel_seen = max(max_rel_seen,
|
| 601 |
+
float(rel.max().item()))
|
| 602 |
+
nan_r += int(torch.isnan(r).sum().item())
|
| 603 |
+
nan_c += int(torch.isnan(c).sum().item())
|
| 604 |
+
inf_r += int(torch.isinf(r).sum().item())
|
| 605 |
+
inf_c += int(torch.isinf(c).sum().item())
|
| 606 |
+
trial_rep = {
|
| 607 |
+
"max_ulp": max_ulp,
|
| 608 |
+
"mean_ulp": round(mean_ulp / max(n_el, 1), 3),
|
| 609 |
+
"first_divergent_index": first_div,
|
| 610 |
+
"nan_ref": nan_r, "nan_cand": nan_c,
|
| 611 |
+
"inf_ref": inf_r, "inf_cand": inf_c,
|
| 612 |
+
"order_band_ulp": band,
|
| 613 |
+
"band_score": round(band_score, 2),
|
| 614 |
+
}
|
| 615 |
+
per_trial.append(trial_rep)
|
| 616 |
+
if max_ulp > worst["max_ulp"]:
|
| 617 |
+
worst = trial_rep
|
| 618 |
+
band_max = max(t["order_band_ulp"] for t in per_trial)
|
| 619 |
+
max_seen = max(t["max_ulp"] for t in per_trial)
|
| 620 |
+
if order_shuffle is not None:
|
| 621 |
+
score = max(t["band_score"] for t in per_trial)
|
| 622 |
+
ulp_ok = score <= band_margin
|
| 623 |
+
criterion = (f"pointwise max(ulp / (order_band + 2)) = "
|
| 624 |
+
f"{score:.2f} <= band_margin {band_margin}")
|
| 625 |
+
else:
|
| 626 |
+
tol = ulp_tol if ulp_tol is not None else 2
|
| 627 |
+
ulp_ok = max_seen <= tol
|
| 628 |
+
criterion = f"max_ulp {max_seen} <= ulp_tol {tol}"
|
| 629 |
+
tol_ok = False
|
| 630 |
+
if atol is not None or rtol is not None:
|
| 631 |
+
tol_ok = max_abs_seen <= (atol or 0) + (rtol or 0) * 1.0 or \
|
| 632 |
+
max_rel_seen <= (rtol or 0) or max_abs_seen <= (atol or 0)
|
| 633 |
+
criterion += (f"; OR max_abs {max_abs_seen:.3e} <= atol {atol} / "
|
| 634 |
+
f"max_rel {max_rel_seen:.3e} <= rtol {rtol}")
|
| 635 |
+
return {
|
| 636 |
+
"pass": bool(ulp_ok or tol_ok),
|
| 637 |
+
"criterion": criterion,
|
| 638 |
+
"worst_trial": worst,
|
| 639 |
+
"max_abs": max_abs_seen,
|
| 640 |
+
"max_rel": max_rel_seen,
|
| 641 |
+
"trials": per_trial,
|
| 642 |
+
"ulp_hist_log2": dict(sorted(total_hist.items())),
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
|
| 646 |
+
_FUZZ_VARIANTS = ("contig", "transposed", "strided_rows", "offset_slice",
|
| 647 |
+
"overflow_expand")
|
| 648 |
+
|
| 649 |
+
|
| 650 |
+
def _context_alive(device):
|
| 651 |
+
try:
|
| 652 |
+
t = torch.ones(4, device=device)
|
| 653 |
+
torch.cuda.synchronize()
|
| 654 |
+
return bool(t.sum().item() == 4.0)
|
| 655 |
+
except Exception: # noqa: BLE001
|
| 656 |
+
return False
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
def fuzz_shapes(op, ref, make, shapes=None, variants=_FUZZ_VARIANTS,
|
| 660 |
+
dtypes=(torch.float32,), ulp_tol=64, device="cuda"):
|
| 661 |
+
"""The ugly envelope: run op vs ref across edge shapes, dtypes, and
|
| 662 |
+
layout mutations; report ok / mismatch / exception per case.
|
| 663 |
+
|
| 664 |
+
`make(shape, dtype)` (or `make(shape)`) builds a base input, either a
|
| 665 |
+
tensor or a tuple of tensors for multi-argument ops; layout variants
|
| 666 |
+
derive non-contiguous and misaligned views whose CONTENT equals a
|
| 667 |
+
contiguous copy, so `op(*xs)` must match `ref(*xs)` on every variant.
|
| 668 |
+
`overflow_expand` stride-0-expands the input past 2**31 logical
|
| 669 |
+
elements to catch int32 indexing, and is skipped with status
|
| 670 |
+
`skipped_vram` when materializing the output would not fit. After any
|
| 671 |
+
exception the CUDA context is health-checked; if a candidate killed
|
| 672 |
+
it, remaining cases are marked `context_lost` instead of producing
|
| 673 |
+
garbage."""
|
| 674 |
+
if shapes is None:
|
| 675 |
+
shapes = [(0, 8), (1, 1), (1, 17), (3, 63), (17, 129), (128, 128),
|
| 676 |
+
(1023, 5), (2, 4096)]
|
| 677 |
+
|
| 678 |
+
def build(shape, dtype):
|
| 679 |
+
try:
|
| 680 |
+
out = make(tuple(shape), dtype)
|
| 681 |
+
except TypeError:
|
| 682 |
+
out = make(tuple(shape))
|
| 683 |
+
return out if isinstance(out, (tuple, list)) else (out,)
|
| 684 |
+
|
| 685 |
+
def variant_of(x, v):
|
| 686 |
+
if v == "contig":
|
| 687 |
+
return x
|
| 688 |
+
if v == "transposed":
|
| 689 |
+
return x.t() if x.dim() == 2 else x
|
| 690 |
+
if v == "strided_rows":
|
| 691 |
+
return x[::2] if x.shape[0] >= 2 else x
|
| 692 |
+
if v == "offset_slice":
|
| 693 |
+
# a storage-offset-1 view: misaligned for vectorized loads
|
| 694 |
+
return x.flatten()[1:] if x.numel() > 1 else x
|
| 695 |
+
if v == "overflow_expand":
|
| 696 |
+
if x.numel() == 0:
|
| 697 |
+
return None
|
| 698 |
+
per = max(x.numel(), 1)
|
| 699 |
+
rep = (1 << 31) // per + 2
|
| 700 |
+
return x.reshape((1,) + tuple(x.shape)).expand(
|
| 701 |
+
(rep,) + tuple(x.shape))
|
| 702 |
+
return x
|
| 703 |
+
|
| 704 |
+
rows = []
|
| 705 |
+
lost = False
|
| 706 |
+
for dtype in dtypes:
|
| 707 |
+
for shape in shapes:
|
| 708 |
+
bases = build(shape, dtype)
|
| 709 |
+
for v in variants:
|
| 710 |
+
case = {"shape": tuple(shape), "variant": v,
|
| 711 |
+
"dtype": str(dtype).replace("torch.", "")}
|
| 712 |
+
if lost:
|
| 713 |
+
case["status"] = "context_lost"
|
| 714 |
+
rows.append(case)
|
| 715 |
+
continue
|
| 716 |
+
xs = [variant_of(b, v) for b in bases]
|
| 717 |
+
if any(x is None for x in xs):
|
| 718 |
+
case["status"] = "skipped_empty"
|
| 719 |
+
rows.append(case)
|
| 720 |
+
continue
|
| 721 |
+
if v == "overflow_expand":
|
| 722 |
+
logical = max(x.numel() for x in xs)
|
| 723 |
+
need = logical * xs[0].element_size() * 2
|
| 724 |
+
free, _total = torch.cuda.mem_get_info()
|
| 725 |
+
if need > free * 0.8:
|
| 726 |
+
case["status"] = "skipped_vram"
|
| 727 |
+
case["logical_elems"] = int(logical)
|
| 728 |
+
rows.append(case)
|
| 729 |
+
continue
|
| 730 |
+
case["logical_elems"] = int(logical)
|
| 731 |
+
case["contiguous"] = bool(xs[0].is_contiguous())
|
| 732 |
+
try:
|
| 733 |
+
got = op(*xs)
|
| 734 |
+
want = ref(*xs)
|
| 735 |
+
if v == "overflow_expand":
|
| 736 |
+
# bounded comparison: the head plus the high-index
|
| 737 |
+
# tail, which is where int32 flat indexing wraps
|
| 738 |
+
w = 1 << 20
|
| 739 |
+
gf, wf = got.flatten(), want.flatten()
|
| 740 |
+
pairs = ((gf[:w], wf[:w]), (gf[-w:], wf[-w:]))
|
| 741 |
+
else:
|
| 742 |
+
pairs = ((got, want),)
|
| 743 |
+
mx = 0
|
| 744 |
+
for gp, wp in pairs:
|
| 745 |
+
d = ulp_diff(wp, gp) if gp.is_floating_point() \
|
| 746 |
+
else (wp != gp).to(torch.int64) * (1 << 62)
|
| 747 |
+
if d.numel():
|
| 748 |
+
mx = max(mx, int(d.max().item()))
|
| 749 |
+
del d
|
| 750 |
+
case["status"] = "ok" if mx <= ulp_tol else "mismatch"
|
| 751 |
+
case["max_ulp"] = mx
|
| 752 |
+
del got, want
|
| 753 |
+
except Exception as e: # noqa: BLE001 - report, don't die
|
| 754 |
+
case["status"] = "exception"
|
| 755 |
+
case["error"] = f"{type(e).__name__}: {str(e)[:120]}"
|
| 756 |
+
if not _context_alive(device):
|
| 757 |
+
lost = True
|
| 758 |
+
case["status"] = "context_killed"
|
| 759 |
+
rows.append(case)
|
| 760 |
+
n_bad = sum(1 for r in rows
|
| 761 |
+
if r["status"] not in ("ok", "skipped_vram",
|
| 762 |
+
"skipped_empty"))
|
| 763 |
+
return {"cases": rows, "failed": n_bad, "total": len(rows),
|
| 764 |
+
"context_lost": lost}
|
| 765 |
+
|
| 766 |
+
|
| 767 |
+
def sweep(factory, grid, iters=30, warmup=5, bytes=None, flops=None,
|
| 768 |
+
check=None, budget_s=None):
|
| 769 |
+
"""Launch-configuration search: `factory(**point) -> callable`; one
|
| 770 |
+
bench row per grid point, sorted fastest first. A point whose factory
|
| 771 |
+
or run raises is reported, not skipped silently. `check(fn) -> bool`
|
| 772 |
+
is an optional correctness gate recorded per row (a fast wrong config
|
| 773 |
+
must not win a sweep). `budget_s` caps total sweep time; points not
|
| 774 |
+
reached are reported as `not_run` rather than silently dropped."""
|
| 775 |
+
keys = list(grid.keys())
|
| 776 |
+
|
| 777 |
+
def points(i, acc):
|
| 778 |
+
if i == len(keys):
|
| 779 |
+
yield dict(acc)
|
| 780 |
+
return
|
| 781 |
+
for v in grid[keys[i]]:
|
| 782 |
+
acc[keys[i]] = v
|
| 783 |
+
yield from points(i + 1, acc)
|
| 784 |
+
|
| 785 |
+
rows = []
|
| 786 |
+
t_start = time.perf_counter()
|
| 787 |
+
over_budget = False
|
| 788 |
+
for p in points(0, {}):
|
| 789 |
+
row = dict(p)
|
| 790 |
+
if over_budget or (budget_s is not None and
|
| 791 |
+
time.perf_counter() - t_start > budget_s):
|
| 792 |
+
over_budget = True
|
| 793 |
+
row["status"] = "not_run"
|
| 794 |
+
row["median_ms"] = float("inf")
|
| 795 |
+
rows.append(row)
|
| 796 |
+
continue
|
| 797 |
+
try:
|
| 798 |
+
fn = factory(**p)
|
| 799 |
+
if check is not None:
|
| 800 |
+
row["check"] = bool(check(fn))
|
| 801 |
+
rep = bench(fn, iters=iters, warmup=warmup, bytes=bytes,
|
| 802 |
+
flops=flops)
|
| 803 |
+
row.update({"median_ms": rep["median_ms"],
|
| 804 |
+
"rejected_throttled": rep["rejected_throttled"]})
|
| 805 |
+
if "achieved_gbs" in rep:
|
| 806 |
+
row["achieved_gbs"] = rep["achieved_gbs"]
|
| 807 |
+
if "achieved_tflops" in rep:
|
| 808 |
+
row["achieved_tflops"] = rep["achieved_tflops"]
|
| 809 |
+
except Exception as e: # noqa: BLE001
|
| 810 |
+
row["error"] = f"{type(e).__name__}: {str(e)[:120]}"
|
| 811 |
+
row["median_ms"] = float("inf")
|
| 812 |
+
rows.append(row)
|
| 813 |
+
rows.sort(key=lambda r: (not r.get("check", True), r["median_ms"]))
|
| 814 |
+
return rows
|
| 815 |
+
|
| 816 |
+
|
| 817 |
+
def alloc_stamps(n, device="cuda"):
|
| 818 |
+
"""Device buffer for the MK_STAMP convention (metakernel_cuda/
|
| 819 |
+
stamps.cuh): the candidate kernel writes clock64() into slot i."""
|
| 820 |
+
return torch.zeros(n, dtype=torch.int64, device=device)
|
| 821 |
+
|
| 822 |
+
|
| 823 |
+
def read_stamps(stamps, labels=None, clock_ghz=None):
|
| 824 |
+
"""Decode MK_STAMP phases: consecutive deltas in ticks and in
|
| 825 |
+
microseconds under the measured (or provided) SM clock. A 1-D buffer
|
| 826 |
+
(MK_STAMP: block 0 only) yields one delta per phase; a 2-D [B, n]
|
| 827 |
+
buffer (MK_STAMP_BLOCK) yields min / median / max across blocks per
|
| 828 |
+
phase, with blocks whose stamps stayed zero excluded."""
|
| 829 |
+
if clock_ghz is None:
|
| 830 |
+
clock_ghz = measure_clock(stamps.device)
|
| 831 |
+
|
| 832 |
+
def us(ticks):
|
| 833 |
+
return round(ticks / clock_ghz / 1000.0, 3)
|
| 834 |
+
|
| 835 |
+
if stamps.dim() == 1:
|
| 836 |
+
vals = stamps.cpu().tolist()
|
| 837 |
+
phases = []
|
| 838 |
+
for i in range(len(vals) - 1):
|
| 839 |
+
ticks = vals[i + 1] - vals[i]
|
| 840 |
+
name = labels[i] if labels and i < len(labels) else f"phase_{i}"
|
| 841 |
+
phases.append({"phase": name, "ticks": ticks, "us": us(ticks)})
|
| 842 |
+
return {"phases": phases, "clock_ghz": round(clock_ghz, 3)}
|
| 843 |
+
|
| 844 |
+
m = stamps.cpu()
|
| 845 |
+
live = (m != 0).all(dim=1)
|
| 846 |
+
m = m[live]
|
| 847 |
+
deltas = (m[:, 1:] - m[:, :-1]).to(torch.float64)
|
| 848 |
+
phases = []
|
| 849 |
+
for i in range(deltas.shape[1]):
|
| 850 |
+
col = deltas[:, i]
|
| 851 |
+
name = labels[i] if labels and i < len(labels) else f"phase_{i}"
|
| 852 |
+
med = float(col.median().item()) if col.numel() else 0.0
|
| 853 |
+
phases.append({
|
| 854 |
+
"phase": name,
|
| 855 |
+
"ticks_median": int(med),
|
| 856 |
+
"ticks_min": int(col.min().item()) if col.numel() else 0,
|
| 857 |
+
"ticks_max": int(col.max().item()) if col.numel() else 0,
|
| 858 |
+
"us_median": us(med),
|
| 859 |
+
"blocks": int(col.numel()),
|
| 860 |
+
})
|
| 861 |
+
return {"phases": phases, "clock_ghz": round(clock_ghz, 3),
|
| 862 |
+
"blocks_reporting": int(live.sum().item()),
|
| 863 |
+
"blocks_total": int(live.numel())}
|
build/torch211-cxx11-cu130-x86_64-linux/_metakernel_cuda_4611d12.abi3.so
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b2df8ca3e350475a7f0aebd96c8820a08c92a203bc890aa26182e7c4b8234386
|
| 3 |
+
size 961480
|
build/torch211-cxx11-cu130-x86_64-linux/_ops.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from . import _metakernel_cuda_4611d12
|
| 3 |
+
ops = torch.ops._metakernel_cuda_4611d12
|
| 4 |
+
|
| 5 |
+
def add_op_namespace_prefix(op_name: str):
|
| 6 |
+
"""
|
| 7 |
+
Prefix op by namespace.
|
| 8 |
+
"""
|
| 9 |
+
return f"_metakernel_cuda_4611d12::{op_name}"
|
build/torch211-cxx11-cu130-x86_64-linux/metadata.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "metakernel",
|
| 3 |
+
"id": "_metakernel_cuda_4611d12",
|
| 4 |
+
"version": 1,
|
| 5 |
+
"license": "Apache-2.0",
|
| 6 |
+
"python-depends": [],
|
| 7 |
+
"backend": {
|
| 8 |
+
"type": "cuda",
|
| 9 |
+
"archs": [
|
| 10 |
+
"10.0",
|
| 11 |
+
"12.0",
|
| 12 |
+
"8.0",
|
| 13 |
+
"8.6",
|
| 14 |
+
"8.9",
|
| 15 |
+
"9.0"
|
| 16 |
+
]
|
| 17 |
+
},
|
| 18 |
+
"digest": {
|
| 19 |
+
"algorithm": "sha256",
|
| 20 |
+
"files": {
|
| 21 |
+
"__init__.py": "cZxKX5I/2EKCMC2gdCwflUsZaQJWJesmAHq2w+fVjXo=",
|
| 22 |
+
"_metakernel_cuda_4611d12.abi3.so": "st+Mo+NQR1p/CuvZbIggoIySogO8iQqiYYLnxLgjQ4Y=",
|
| 23 |
+
"_ops.py": "uZlR720EcTy8M7BCydmvaZAlKonkwTU3778nhR9yQOE=",
|
| 24 |
+
"metakernel/__init__.py": "DFYPlrhXwYjEqCl/8n0SmWGZV8NFml5DPhMjKfv98GY="
|
| 25 |
+
}
|
| 26 |
+
},
|
| 27 |
+
"provenance": {
|
| 28 |
+
"kernel-builder": {
|
| 29 |
+
"version": "0.17.0-dev0",
|
| 30 |
+
"sha": "19aaa6421e674e9fecc352bbae6eab81d19a6bf4",
|
| 31 |
+
"dirty": false
|
| 32 |
+
},
|
| 33 |
+
"kernel": {
|
| 34 |
+
"sha": "4611d1213a76a9ec1d12646553a528feb60394ff",
|
| 35 |
+
"dirty": false
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
}
|
build/torch211-cxx11-cu130-x86_64-linux/metakernel/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ctypes
|
| 2 |
+
import importlib.util
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from types import ModuleType
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def _import_from_path(file_path: Path) -> ModuleType:
|
| 9 |
+
# We cannot use the module name as-is, after adding it to `sys.modules`,
|
| 10 |
+
# it would also be used for other imports. So, we make a module name that
|
| 11 |
+
# depends on the path for it to be unique using the hex-encoded hash of
|
| 12 |
+
# the path.
|
| 13 |
+
path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
|
| 14 |
+
module_name = path_hash
|
| 15 |
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
| 16 |
+
if spec is None:
|
| 17 |
+
raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
|
| 18 |
+
module = importlib.util.module_from_spec(spec)
|
| 19 |
+
if module is None:
|
| 20 |
+
raise ImportError(f"Cannot load module {module_name} from spec")
|
| 21 |
+
sys.modules[module_name] = module
|
| 22 |
+
spec.loader.exec_module(module) # type: ignore
|
| 23 |
+
return module
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
|
build/torch212-cxx11-cu126-x86_64-linux/__init__.py
ADDED
|
@@ -0,0 +1,863 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""metakernel: the measuring instrument for the kernel-design loop.
|
| 2 |
+
|
| 3 |
+
An agent iterating on CUDA candidates is good at writing code and bad at
|
| 4 |
+
trusting its own measurements; it needs trustworthy timing, adversarial
|
| 5 |
+
correctness comparison, and measured facts about the device it targets.
|
| 6 |
+
metakernel ships those as loadable ops:
|
| 7 |
+
|
| 8 |
+
from kernels import get_kernel
|
| 9 |
+
mk = get_kernel("phanerozoic/metakernel", version=1,
|
| 10 |
+
trust_remote_code=True)
|
| 11 |
+
|
| 12 |
+
dossier = mk.probe_device() # measured facts
|
| 13 |
+
report = mk.bench(lambda: op(x), iters=200) # throttle-rejected
|
| 14 |
+
verdict = mk.compare(ref, cand, gen) # ULP evidence
|
| 15 |
+
table = mk.sweep(factory, {"BLOCK": [64, 128]}) # config search
|
| 16 |
+
|
| 17 |
+
Every op returns plain dicts and tensors, never prose. Every dossier entry
|
| 18 |
+
is produced by a dedicated probe kernel rather than a spec sheet, carries
|
| 19 |
+
the SM clock it was measured under, and labels throttled samples instead
|
| 20 |
+
of letting them poison medians. No hardware counters are used: everything
|
| 21 |
+
is inferred from timing, clocks, and the occupancy API, so entries carry a
|
| 22 |
+
confidence field (timing-differential inference can misattribute).
|
| 23 |
+
"""
|
| 24 |
+
import math
|
| 25 |
+
import time
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
|
| 29 |
+
from ._ops import ops
|
| 30 |
+
|
| 31 |
+
__all__ = ["probe_device", "bench", "compare", "order_shuffles",
|
| 32 |
+
"fuzz_shapes", "sweep", "alloc_stamps", "read_stamps",
|
| 33 |
+
"measure_clock", "ulp_diff", "ops"]
|
| 34 |
+
|
| 35 |
+
_MMA_KINDS = {
|
| 36 |
+
"fp16": (0, 16 * 16 * 16),
|
| 37 |
+
"bf16": (1, 16 * 16 * 16),
|
| 38 |
+
"tf32": (2, 16 * 16 * 8),
|
| 39 |
+
"int8": (3, 16 * 16 * 16),
|
| 40 |
+
"fp8_e4m3": (4, 16 * 8 * 32),
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _sync():
|
| 45 |
+
torch.cuda.synchronize()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _events(fn, reps):
|
| 49 |
+
"""Time reps calls of fn with one event pair each; returns ms list."""
|
| 50 |
+
starts = [torch.cuda.Event(enable_timing=True) for _ in range(reps)]
|
| 51 |
+
stops = [torch.cuda.Event(enable_timing=True) for _ in range(reps)]
|
| 52 |
+
for s, e in zip(starts, stops):
|
| 53 |
+
s.record()
|
| 54 |
+
fn()
|
| 55 |
+
e.record()
|
| 56 |
+
_sync()
|
| 57 |
+
return [s.elapsed_time(e) for s, e in zip(starts, stops)]
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def measure_clock(device="cuda", ticks=4_000_000, blocks=None):
|
| 61 |
+
"""Effective SM clock in GHz under an FMA spin of `ticks` clock64 ticks.
|
| 62 |
+
Wall time over elapsed ticks is the frequency the ticks ran at."""
|
| 63 |
+
props = torch.cuda.get_device_properties(device)
|
| 64 |
+
if blocks is None:
|
| 65 |
+
blocks = props.multi_processor_count
|
| 66 |
+
out = torch.zeros(2, dtype=torch.int64, device=device)
|
| 67 |
+
sink = torch.zeros(2, dtype=torch.float32, device=device)
|
| 68 |
+
ops.mk_spin(blocks, 256, ticks, out, sink) # warm
|
| 69 |
+
start = torch.cuda.Event(enable_timing=True)
|
| 70 |
+
stop = torch.cuda.Event(enable_timing=True)
|
| 71 |
+
start.record()
|
| 72 |
+
ops.mk_spin(blocks, 256, ticks, out, sink)
|
| 73 |
+
stop.record()
|
| 74 |
+
_sync()
|
| 75 |
+
ms = start.elapsed_time(stop)
|
| 76 |
+
elapsed = int(out[0].item())
|
| 77 |
+
return elapsed / (ms * 1e6)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _median(xs):
|
| 81 |
+
s = sorted(xs)
|
| 82 |
+
n = len(s)
|
| 83 |
+
return s[n // 2] if n % 2 else 0.5 * (s[n // 2 - 1] + s[n // 2])
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _entry(value, units, method, clock_ghz, confidence, note=""):
|
| 87 |
+
d = {"value": value, "units": units, "method": method,
|
| 88 |
+
"clock_ghz": round(clock_ghz, 3), "confidence": confidence}
|
| 89 |
+
if note:
|
| 90 |
+
d["note"] = note
|
| 91 |
+
return d
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _sattolo_ring(n, device, generator=None):
|
| 95 |
+
"""A random single-cycle permutation as an int32 next-pointer table, so
|
| 96 |
+
a pointer chase visits every slot before repeating."""
|
| 97 |
+
perm = torch.randperm(n, generator=generator)
|
| 98 |
+
ring = torch.empty(n, dtype=torch.int32)
|
| 99 |
+
ring[perm] = torch.roll(perm, -1).to(torch.int32)
|
| 100 |
+
return ring.to(device)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def probe_device(device="cuda", quick=False):
|
| 104 |
+
"""Empirical device dossier. Each entry is measured by a dedicated
|
| 105 |
+
probe kernel and carries units, method, the SM clock it ran under, and
|
| 106 |
+
a confidence field."""
|
| 107 |
+
props = torch.cuda.get_device_properties(device)
|
| 108 |
+
sm = props.multi_processor_count
|
| 109 |
+
l2 = props.L2_cache_size
|
| 110 |
+
d = {
|
| 111 |
+
"device": {
|
| 112 |
+
"name": props.name,
|
| 113 |
+
"sm_arch": f"sm{props.major}{props.minor}",
|
| 114 |
+
"sms": sm,
|
| 115 |
+
"l2_bytes": l2,
|
| 116 |
+
"vram_bytes": props.total_memory,
|
| 117 |
+
"torch": torch.__version__,
|
| 118 |
+
"cuda_runtime": torch.version.cuda,
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
sink = torch.zeros(2, dtype=torch.float32, device=device)
|
| 122 |
+
out64 = torch.zeros(2, dtype=torch.int64, device=device)
|
| 123 |
+
reps = 3 if quick else 5
|
| 124 |
+
|
| 125 |
+
# ---- clock under sustained load
|
| 126 |
+
traj = []
|
| 127 |
+
windows = 8 if quick else 24
|
| 128 |
+
for _ in range(windows):
|
| 129 |
+
traj.append(measure_clock(device, ticks=30_000_000))
|
| 130 |
+
d["clock"] = {
|
| 131 |
+
"max_ghz": round(max(traj), 3),
|
| 132 |
+
"min_ghz": round(min(traj), 3),
|
| 133 |
+
"final_ghz": round(traj[-1], 3),
|
| 134 |
+
"sag_pct": round(100 * (1 - traj[-1] / max(traj)), 1),
|
| 135 |
+
"units": "GHz",
|
| 136 |
+
"method": f"{windows} back-to-back FMA spin windows of 30M ticks; "
|
| 137 |
+
"wall/ticks per window",
|
| 138 |
+
"confidence": "high",
|
| 139 |
+
}
|
| 140 |
+
clk = max(traj)
|
| 141 |
+
|
| 142 |
+
# ---- HBM bandwidth: triad per access width, plus read-only,
|
| 143 |
+
# write-only, and random-gather
|
| 144 |
+
n4 = 16 << 20 # 16M float4 = 256 MB per buffer
|
| 145 |
+
if props.total_memory < 4 << 30:
|
| 146 |
+
n4 = 4 << 20
|
| 147 |
+
a = torch.rand(n4 * 4, device=device)
|
| 148 |
+
b = torch.rand(n4 * 4, device=device)
|
| 149 |
+
c = torch.empty(n4 * 4, device=device)
|
| 150 |
+
hbm = {}
|
| 151 |
+
for width in (1, 2, 4):
|
| 152 |
+
ops.mk_triad(a, b, c, width, 1.5) # warm
|
| 153 |
+
ms = _median(_events(lambda w=width: ops.mk_triad(a, b, c, w, 1.5),
|
| 154 |
+
reps))
|
| 155 |
+
gbs = 3 * a.numel() * 4 / (ms * 1e-3) / 1e9
|
| 156 |
+
hbm[f"triad_{4 * width}B"] = round(gbs, 1)
|
| 157 |
+
ops.mk_read(a, sink)
|
| 158 |
+
ms = _median(_events(lambda: ops.mk_read(a, sink), reps))
|
| 159 |
+
hbm["read_only"] = round(a.numel() * 4 / (ms * 1e-3) / 1e9, 1)
|
| 160 |
+
ops.mk_write(c, 1.0)
|
| 161 |
+
ms = _median(_events(lambda: ops.mk_write(c, 1.0), reps))
|
| 162 |
+
hbm["write_only"] = round(c.numel() * 4 / (ms * 1e-3) / 1e9, 1)
|
| 163 |
+
g = torch.Generator(device="cpu").manual_seed(7)
|
| 164 |
+
idx = torch.randint(0, n4, (n4,), generator=g,
|
| 165 |
+
dtype=torch.int32).to(device)
|
| 166 |
+
ops.mk_gather(a, idx, c, 256, 0)
|
| 167 |
+
ms = _median(_events(lambda: ops.mk_gather(a, idx, c, 256, 0), reps))
|
| 168 |
+
# traffic accounting: idx reads + 16B random loads + streamed stores
|
| 169 |
+
hbm["random_gather_16B"] = round(
|
| 170 |
+
(idx.numel() * 4 + 2 * n4 * 16) / (ms * 1e-3) / 1e9, 1)
|
| 171 |
+
d["hbm_bw"] = _entry(hbm, "GB/s", "streaming triad c=a+s*b per access "
|
| 172 |
+
"width; read-only fold; write-only fill; random "
|
| 173 |
+
"float4 gather (16B granules, nominal traffic)",
|
| 174 |
+
clk, "high",
|
| 175 |
+
"gather moves full cache lines per 16B granule, "
|
| 176 |
+
"so its nominal number is the achievable rate for "
|
| 177 |
+
"random access, not a line-efficiency claim")
|
| 178 |
+
|
| 179 |
+
# ---- L2 bandwidth (working set 3 buffers inside L2)
|
| 180 |
+
ws = max(l2 // 4, 1 << 20)
|
| 181 |
+
nl2 = ws // 16 # float4 elems per buffer
|
| 182 |
+
al = a[:nl2 * 4].contiguous()
|
| 183 |
+
bl = b[:nl2 * 4].contiguous()
|
| 184 |
+
cl = c[:nl2 * 4].contiguous()
|
| 185 |
+
passes = 64
|
| 186 |
+
ops.mk_triad_passes(al, bl, cl, 1.5, passes)
|
| 187 |
+
ms = _median(_events(
|
| 188 |
+
lambda: ops.mk_triad_passes(al, bl, cl, 1.5, passes), reps))
|
| 189 |
+
gbs = 3 * al.numel() * 4 * passes / (ms * 1e-3) / 1e9
|
| 190 |
+
d["l2_bw"] = _entry(round(gbs, 1), "GB/s",
|
| 191 |
+
f"in-kernel repeated triad passes over "
|
| 192 |
+
f"{3 * ws >> 20} MB resident working set", clk,
|
| 193 |
+
"medium", "first pass streams from HBM; amortized "
|
| 194 |
+
f"over {passes} passes")
|
| 195 |
+
|
| 196 |
+
# ---- latencies: pointer chase per level. Footprints are chosen so
|
| 197 |
+
# the chain hits the intended level: the L2 ring (2 MB) exceeds L1,
|
| 198 |
+
# fits L2 on every supported part, and spans few enough pages to stay
|
| 199 |
+
# TLB-resident after a full-cycle warm walk; the HBM chain is long
|
| 200 |
+
# enough (2x L2 in touched lines) that it evicts itself and cannot be
|
| 201 |
+
# served from cache.
|
| 202 |
+
lat = {}
|
| 203 |
+
idle_clk = measure_clock(device, ticks=2_000_000, blocks=1)
|
| 204 |
+
ring_s = _sattolo_ring(8192, device)
|
| 205 |
+
ops.mk_chase_shared(ring_s, 1 << 14, out64)
|
| 206 |
+
_sync()
|
| 207 |
+
hops = 1 << 16
|
| 208 |
+
ops.mk_chase_shared(ring_s, hops, out64)
|
| 209 |
+
_sync()
|
| 210 |
+
lat["smem_cycles"] = round(int(out64[0].item()) / hops, 1)
|
| 211 |
+
|
| 212 |
+
l1_ring_n = (16 << 10) // 4 # 16 KB: L1-resident after a warm walk
|
| 213 |
+
ring_l1 = _sattolo_ring(l1_ring_n, device)
|
| 214 |
+
ops.mk_chase_global(ring_l1, l1_ring_n, out64)
|
| 215 |
+
_sync()
|
| 216 |
+
hops_l1 = 1 << 15
|
| 217 |
+
ops.mk_chase_global(ring_l1, hops_l1, out64)
|
| 218 |
+
_sync()
|
| 219 |
+
lat["l1_cycles"] = round(int(out64[0].item()) / hops_l1, 1)
|
| 220 |
+
|
| 221 |
+
l2_ring_n = (2 << 20) // 4
|
| 222 |
+
ring_l2 = _sattolo_ring(l2_ring_n, device)
|
| 223 |
+
ops.mk_chase_global(ring_l2, l2_ring_n, out64) # full-cycle warm walk
|
| 224 |
+
_sync()
|
| 225 |
+
hops_l2 = 1 << 14
|
| 226 |
+
ops.mk_chase_global(ring_l2, hops_l2, out64)
|
| 227 |
+
_sync()
|
| 228 |
+
lat["l2_cycles"] = round(int(out64[0].item()) / hops_l2, 1)
|
| 229 |
+
|
| 230 |
+
hbm_bytes = min(max(4 * l2, 64 << 20), props.total_memory // 8)
|
| 231 |
+
ring_h = _sattolo_ring(int(hbm_bytes) // 4, device)
|
| 232 |
+
hops_h = max((2 * l2) // 128, 1 << 17)
|
| 233 |
+
ops.mk_chase_global(ring_h, 1 << 14, out64) # fault pages / warm TLB
|
| 234 |
+
_sync()
|
| 235 |
+
ops.mk_chase_global(ring_h, hops_h, out64)
|
| 236 |
+
_sync()
|
| 237 |
+
lat["hbm_cycles"] = round(int(out64[0].item()) / hops_h, 1)
|
| 238 |
+
lat["ns_per_cycle"] = round(1.0 / idle_clk, 3)
|
| 239 |
+
d["latency"] = _entry(lat, "cycles (single-lane dependent loads)",
|
| 240 |
+
"Sattolo-cycle pointer chase; 16 KB ring for L1, "
|
| 241 |
+
"2 MB TLB-resident ring for L2, self-evicting "
|
| 242 |
+
"chain (2x L2 in touched lines) for HBM",
|
| 243 |
+
idle_clk, "high",
|
| 244 |
+
"smem includes index arithmetic; HBM includes "
|
| 245 |
+
"residual TLB effects at large footprints")
|
| 246 |
+
del ring_h, ring_l2, ring_l1, ring_s
|
| 247 |
+
|
| 248 |
+
# ---- fp32 FMA and tensor-core throughput
|
| 249 |
+
blocks = sm * 4
|
| 250 |
+
iters = 20_000 if quick else 60_000
|
| 251 |
+
ops.mk_fma_f32(blocks, 256, iters, sink)
|
| 252 |
+
ms = _median(_events(lambda: ops.mk_fma_f32(blocks, 256, iters, sink),
|
| 253 |
+
reps))
|
| 254 |
+
flops = blocks * 256 * iters * 16 * 2 * 2 # 2 chains x 2 flops x unroll
|
| 255 |
+
d["fma_f32"] = _entry(round(flops / (ms * 1e-3) / 1e12, 2), "TFLOP/s",
|
| 256 |
+
"register-resident FMA chains", clk, "high")
|
| 257 |
+
sink64 = torch.zeros(2, dtype=torch.float64, device=device)
|
| 258 |
+
it64 = max(iters // 16, 1000)
|
| 259 |
+
ops.mk_fma_f64(blocks, 256, it64, sink64)
|
| 260 |
+
ms = _median(_events(
|
| 261 |
+
lambda: ops.mk_fma_f64(blocks, 256, it64, sink64), reps))
|
| 262 |
+
flops64 = blocks * 256 * it64 * 16 * 2 * 2
|
| 263 |
+
d["fma_f64"] = _entry(round(flops64 / (ms * 1e-3) / 1e12, 3), "TFLOP/s",
|
| 264 |
+
"register-resident double FMA chains (the DFMA "
|
| 265 |
+
"pipe)", clk, "high")
|
| 266 |
+
mma = {}
|
| 267 |
+
for name, (kind, mnk) in _MMA_KINDS.items():
|
| 268 |
+
it = 30_000 if quick else 100_000
|
| 269 |
+
r = ops.mk_mma(kind, blocks, 8, 512, sink)
|
| 270 |
+
if r != 0:
|
| 271 |
+
mma[name] = "unsupported"
|
| 272 |
+
continue
|
| 273 |
+
_sync()
|
| 274 |
+
ms = _median(_events(
|
| 275 |
+
lambda k=kind, i=it: ops.mk_mma(k, blocks, 8, i, sink), reps))
|
| 276 |
+
flops = blocks * 8 * it * 4 * 2 * mnk
|
| 277 |
+
mma[name] = round(flops / (ms * 1e-3) / 1e12, 1)
|
| 278 |
+
d["mma"] = _entry(mma, "TFLOP/s (TOP/s for int8)",
|
| 279 |
+
"register-fragment mma_sync loops (wmma; fp8 via "
|
| 280 |
+
"inline PTX m16n8k32)", clk, "medium",
|
| 281 |
+
"dense-tile rate without memory traffic; real "
|
| 282 |
+
"kernels see this only when staging hides behind it")
|
| 283 |
+
|
| 284 |
+
# ---- atomic contention curve: throughput vs number of distinct
|
| 285 |
+
# addresses the whole grid hammers (slots = 1 is full contention;
|
| 286 |
+
# slots = threads is contention-free), plus the shared-memory point
|
| 287 |
+
n_thr = blocks * 256
|
| 288 |
+
buf = torch.zeros(n_thr, dtype=torch.float32, device=device)
|
| 289 |
+
at = {"curve_slots": {}}
|
| 290 |
+
it_at = 2_000 if quick else 6_000
|
| 291 |
+
slot_points = [1, 8, 64, 512, 4096, n_thr]
|
| 292 |
+
for slots in slot_points:
|
| 293 |
+
buf.zero_()
|
| 294 |
+
ops.mk_atomics(1, blocks, 256, it_at, buf, slots)
|
| 295 |
+
ms = _median(_events(
|
| 296 |
+
lambda s=slots: ops.mk_atomics(1, blocks, 256, it_at, buf, s),
|
| 297 |
+
reps))
|
| 298 |
+
at["curve_slots"][str(slots)] = round(
|
| 299 |
+
n_thr * it_at / (ms * 1e-3) / 1e9, 2)
|
| 300 |
+
ops.mk_atomics(3, blocks, 256, it_at, buf, 1)
|
| 301 |
+
ms = _median(_events(
|
| 302 |
+
lambda: ops.mk_atomics(3, blocks, 256, it_at, buf, 1), reps))
|
| 303 |
+
at["shared_same_addr"] = round(n_thr * it_at / (ms * 1e-3) / 1e9, 2)
|
| 304 |
+
d["atomics_f32"] = _entry(at, "Gop/s",
|
| 305 |
+
"atomicAdd hammer; curve sweeps the number "
|
| 306 |
+
"of distinct global addresses from 1 (full "
|
| 307 |
+
"contention) to one per thread", clk, "high")
|
| 308 |
+
|
| 309 |
+
# ---- launch overhead
|
| 310 |
+
anchor = sink
|
| 311 |
+
n_launch = 500 if quick else 2000
|
| 312 |
+
_sync()
|
| 313 |
+
t0 = time.perf_counter()
|
| 314 |
+
for _ in range(n_launch):
|
| 315 |
+
ops.mk_empty(anchor)
|
| 316 |
+
_sync()
|
| 317 |
+
queued = (time.perf_counter() - t0) / n_launch * 1e6
|
| 318 |
+
n_rt = 100 if quick else 300
|
| 319 |
+
t0 = time.perf_counter()
|
| 320 |
+
for _ in range(n_rt):
|
| 321 |
+
ops.mk_empty(anchor)
|
| 322 |
+
_sync()
|
| 323 |
+
rt = (time.perf_counter() - t0) / n_rt * 1e6
|
| 324 |
+
d["launch_overhead"] = _entry(
|
| 325 |
+
{"queued_us": round(queued, 2), "round_trip_us": round(rt, 2)},
|
| 326 |
+
"us", "empty-kernel trains, queued vs launch+sync", clk, "high",
|
| 327 |
+
"host-dependent: includes torch op dispatch")
|
| 328 |
+
|
| 329 |
+
# ---- grid barrier
|
| 330 |
+
bar_iters = 500 if quick else 2000
|
| 331 |
+
r = ops.mk_barrier(sm, 256, bar_iters, out64)
|
| 332 |
+
if r == 0:
|
| 333 |
+
_sync()
|
| 334 |
+
ticks = int(out64[0].item())
|
| 335 |
+
d["grid_barrier"] = _entry(
|
| 336 |
+
round(ticks / bar_iters / clk / 1000.0, 2), "us per grid.sync",
|
| 337 |
+
f"cooperative kernel, {sm} blocks x 256 threads", clk, "high")
|
| 338 |
+
else:
|
| 339 |
+
d["grid_barrier"] = _entry("unsupported", "", "cooperative launch "
|
| 340 |
+
"unavailable or grid too large", clk,
|
| 341 |
+
"high")
|
| 342 |
+
|
| 343 |
+
# ---- occupancy table (API) plus timed knees: the same random gather
|
| 344 |
+
# at every (threads, dyn_smem) point; when the forced blocks/SM stop
|
| 345 |
+
# covering memory latency, achieved bandwidth falls off the cliff
|
| 346 |
+
occ = {}
|
| 347 |
+
for threads in (128, 256, 512, 1024):
|
| 348 |
+
row = {}
|
| 349 |
+
for smem_kb in (0, 16, 32, 48):
|
| 350 |
+
row[f"{smem_kb}KB"] = int(ops.mk_occupancy(
|
| 351 |
+
threads, smem_kb * 1024, anchor))
|
| 352 |
+
occ[f"t{threads}"] = row
|
| 353 |
+
d["occupancy_blocks_per_sm"] = _entry(
|
| 354 |
+
occ, "blocks/SM", "occupancy API on the float4 triad kernel",
|
| 355 |
+
clk, "high", "API-derived, not timing-derived")
|
| 356 |
+
|
| 357 |
+
knee_iters = 6_000 if quick else 20_000
|
| 358 |
+
knee_blocks = sm * 8
|
| 359 |
+
knees = {}
|
| 360 |
+
for threads in (128, 256, 1024):
|
| 361 |
+
row = {}
|
| 362 |
+
for smem_kb in (0, 32, 48):
|
| 363 |
+
ops.mk_fma_dep(knee_blocks, threads, smem_kb * 1024,
|
| 364 |
+
knee_iters, sink)
|
| 365 |
+
ms = _median(_events(
|
| 366 |
+
lambda t=threads, s=smem_kb: ops.mk_fma_dep(
|
| 367 |
+
knee_blocks, t, s * 1024, knee_iters, sink), reps))
|
| 368 |
+
fl = knee_blocks * threads * knee_iters * 16 * 2
|
| 369 |
+
row[f"{smem_kb}KB"] = round(fl / (ms * 1e-3) / 1e12, 2)
|
| 370 |
+
knees[f"t{threads}"] = row
|
| 371 |
+
d["occupancy_knees_fma_tflops"] = _entry(
|
| 372 |
+
knees, "TFLOP/s", "a single dependent FMA chain per thread, timed "
|
| 373 |
+
"at forced (threads, dyn smem) occupancies over fixed total work; "
|
| 374 |
+
"throughput is proportional to resident threads until the pipe "
|
| 375 |
+
"saturates, so the cliff marks where occupancy stops covering "
|
| 376 |
+
"latency", clk, "high",
|
| 377 |
+
"timing-derived counterpart to the API table above")
|
| 378 |
+
return d
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def bench(fn, iters=100, warmup=10, bytes=None, flops=None, dossier=None,
|
| 382 |
+
dtype="fp16", clock_guard=0.97, clock_stride=8, graph=False,
|
| 383 |
+
device="cuda"):
|
| 384 |
+
"""Median-of-N cudaEvent timing with throttle rejection.
|
| 385 |
+
|
| 386 |
+
The SM clock is probed every `clock_stride` samples; samples inside a
|
| 387 |
+
window whose clock sagged below `clock_guard` x the best observed
|
| 388 |
+
clock are rejected and counted rather than silently averaged. With
|
| 389 |
+
declared `bytes`/`flops` per call, reports achieved GB/s / TFLOP/s;
|
| 390 |
+
with a `dossier`, adds roofline placement against the measured peaks.
|
| 391 |
+
|
| 392 |
+
`graph=True` captures fn() into a CUDA graph and times graph replays,
|
| 393 |
+
stripping Python and torch dispatch from the measurement: the right
|
| 394 |
+
mode for kernels in the few-microsecond range, where dispatch is
|
| 395 |
+
otherwise the thing being measured. Capture requires fn to be
|
| 396 |
+
graph-safe (no sync, no allocation growth, static shapes)."""
|
| 397 |
+
for _ in range(warmup):
|
| 398 |
+
fn()
|
| 399 |
+
_sync()
|
| 400 |
+
if graph:
|
| 401 |
+
g = torch.cuda.CUDAGraph()
|
| 402 |
+
with torch.cuda.graph(g):
|
| 403 |
+
fn()
|
| 404 |
+
fn = g.replay
|
| 405 |
+
for _ in range(warmup):
|
| 406 |
+
fn()
|
| 407 |
+
_sync()
|
| 408 |
+
samples = []
|
| 409 |
+
clocks = []
|
| 410 |
+
stride = clock_stride
|
| 411 |
+
for base in range(0, iters, stride):
|
| 412 |
+
clocks.append(measure_clock(device, ticks=1_500_000))
|
| 413 |
+
n = min(stride, iters - base)
|
| 414 |
+
samples.append((_events(fn, n), len(clocks) - 1))
|
| 415 |
+
clocks.append(measure_clock(device, ticks=1_500_000))
|
| 416 |
+
best_clk = max(clocks)
|
| 417 |
+
kept, rejected = [], 0
|
| 418 |
+
flat = []
|
| 419 |
+
for group, ci in samples:
|
| 420 |
+
window_ok = (clocks[ci] >= clock_guard * best_clk and
|
| 421 |
+
clocks[min(ci + 1, len(clocks) - 1)] >=
|
| 422 |
+
clock_guard * best_clk)
|
| 423 |
+
for ms in group:
|
| 424 |
+
flat.append(ms)
|
| 425 |
+
if window_ok:
|
| 426 |
+
kept.append(ms)
|
| 427 |
+
else:
|
| 428 |
+
rejected += 1
|
| 429 |
+
if not kept:
|
| 430 |
+
kept = flat
|
| 431 |
+
med = _median(kept)
|
| 432 |
+
rep = {
|
| 433 |
+
"median_ms": round(med, 4),
|
| 434 |
+
"min_ms": round(min(kept), 4),
|
| 435 |
+
"mean_ms": round(sum(kept) / len(kept), 4),
|
| 436 |
+
"samples": len(flat),
|
| 437 |
+
"rejected_throttled": rejected,
|
| 438 |
+
"graph_captured": bool(graph),
|
| 439 |
+
"clock_ghz": {"best": round(best_clk, 3),
|
| 440 |
+
"worst": round(min(clocks), 3)},
|
| 441 |
+
}
|
| 442 |
+
if bytes is not None:
|
| 443 |
+
rep["achieved_gbs"] = round(bytes / (med * 1e-3) / 1e9, 1)
|
| 444 |
+
if flops is not None:
|
| 445 |
+
rep["achieved_tflops"] = round(flops / (med * 1e-3) / 1e12, 2)
|
| 446 |
+
if bytes and flops:
|
| 447 |
+
rep["arithmetic_intensity"] = round(flops / bytes, 2)
|
| 448 |
+
if dossier is not None and bytes and flops:
|
| 449 |
+
bw = max(dossier["hbm_bw"]["value"].values()) * 1e9
|
| 450 |
+
mma_v = dossier["mma"]["value"].get(dtype)
|
| 451 |
+
peak = (mma_v if isinstance(mma_v, (int, float))
|
| 452 |
+
else dossier["fma_f32"]["value"]) * 1e12
|
| 453 |
+
ai = flops / bytes
|
| 454 |
+
roof = min(peak, ai * bw)
|
| 455 |
+
rep["roofline"] = {
|
| 456 |
+
"bound": "memory" if ai * bw < peak else "compute",
|
| 457 |
+
"ridge_flops_per_byte": round(peak / bw, 1),
|
| 458 |
+
"pct_of_roof": round(100 * (flops / (med * 1e-3)) / roof, 1),
|
| 459 |
+
"peak_dtype": dtype,
|
| 460 |
+
}
|
| 461 |
+
return rep
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
def _to_ordered_int(t):
|
| 465 |
+
"""Map float bit patterns to integers whose difference is the ULP
|
| 466 |
+
distance: positive floats keep their bits, negative floats mirror
|
| 467 |
+
below zero, so the mapping is monotone in the float value."""
|
| 468 |
+
if t.dtype == torch.float32:
|
| 469 |
+
u = t.view(torch.int32).to(torch.int64) & 0xFFFFFFFF
|
| 470 |
+
half = 1 << 31
|
| 471 |
+
elif t.dtype in (torch.float16, torch.bfloat16):
|
| 472 |
+
u = t.view(torch.int16).to(torch.int64) & 0xFFFF
|
| 473 |
+
half = 1 << 15
|
| 474 |
+
elif t.dtype == torch.float64:
|
| 475 |
+
i = t.view(torch.int64)
|
| 476 |
+
return torch.where(i >= 0, i, (-(1 << 63)) - i)
|
| 477 |
+
else:
|
| 478 |
+
raise ValueError(f"unsupported dtype {t.dtype}")
|
| 479 |
+
return torch.where(u < half, u, half - u)
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
def ulp_diff(a, b):
|
| 483 |
+
"""Elementwise ULP distance between same-dtype float tensors. NaNs in
|
| 484 |
+
the same position count as 0; a one-sided NaN counts as 2**62."""
|
| 485 |
+
assert a.dtype == b.dtype and a.shape == b.shape
|
| 486 |
+
ia, ib = _to_ordered_int(a), _to_ordered_int(b)
|
| 487 |
+
d = (ia - ib).abs()
|
| 488 |
+
both_nan = torch.isnan(a) & torch.isnan(b)
|
| 489 |
+
one_nan = torch.isnan(a) ^ torch.isnan(b)
|
| 490 |
+
d = torch.where(both_nan, torch.zeros_like(d), d)
|
| 491 |
+
d = torch.where(one_nan, torch.full_like(d, 1 << 62), d)
|
| 492 |
+
return d
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
class order_shuffles:
|
| 496 |
+
"""Built-in order shuffles for the common contractual-noise axes."""
|
| 497 |
+
|
| 498 |
+
@staticmethod
|
| 499 |
+
def last_dim():
|
| 500 |
+
"""Permute the last dim of every tensor arg identically: the
|
| 501 |
+
accumulation order of a last-dim reduction changes, the value
|
| 502 |
+
does not."""
|
| 503 |
+
def shuffle(args, seed):
|
| 504 |
+
g = torch.Generator(device=args[0].device).manual_seed(seed)
|
| 505 |
+
p = torch.randperm(args[0].shape[-1], device=args[0].device,
|
| 506 |
+
generator=g)
|
| 507 |
+
return tuple(x[..., p] for x in args)
|
| 508 |
+
return shuffle
|
| 509 |
+
|
| 510 |
+
@staticmethod
|
| 511 |
+
def matmul_k():
|
| 512 |
+
"""Permute K of (A [.., M, K], B [.., K, N]) consistently: the
|
| 513 |
+
product is unchanged, the accumulation order is not."""
|
| 514 |
+
def shuffle(args, seed):
|
| 515 |
+
a_m, b_m = args[0], args[1]
|
| 516 |
+
g = torch.Generator(device=a_m.device).manual_seed(seed)
|
| 517 |
+
p = torch.randperm(a_m.shape[-1], device=a_m.device,
|
| 518 |
+
generator=g)
|
| 519 |
+
return (a_m[..., p], b_m[..., p, :]) + tuple(args[2:])
|
| 520 |
+
return shuffle
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
def _as_outputs(x):
|
| 524 |
+
return list(x) if isinstance(x, (tuple, list)) else [x]
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
def compare(ref, cand, gen, trials=8, order_shuffle=None, order_trials=8,
|
| 528 |
+
ulp_tol=None, atol=None, rtol=None, band_margin=4.0, seed=0):
|
| 529 |
+
"""Adversarial correctness comparison with ULP evidence.
|
| 530 |
+
|
| 531 |
+
`gen(seed) -> args` builds inputs; `ref(*args)` and `cand(*args)`
|
| 532 |
+
return a tensor or a tuple of tensors (each output is compared and
|
| 533 |
+
the worst governs). If `order_shuffle(args, seed) -> args` is given
|
| 534 |
+
and reorders contractual-noise dimensions (see `order_shuffles` for
|
| 535 |
+
the common cases), the reference is treated as a distribution:
|
| 536 |
+
candidates are judged against the measured reduction-order band
|
| 537 |
+
rather than a point. Verdict, stated in the report: max ULP <=
|
| 538 |
+
band_margin * band + 2 when a band is measured, else max ULP <=
|
| 539 |
+
ulp_tol (default 2); if `atol`/`rtol` are given they are an
|
| 540 |
+
alternative acceptance route, checked as |c - r| <= atol + rtol|r|."""
|
| 541 |
+
worst = {"max_ulp": -1}
|
| 542 |
+
total_hist = {}
|
| 543 |
+
per_trial = []
|
| 544 |
+
max_abs_seen = 0.0
|
| 545 |
+
max_rel_seen = 0.0
|
| 546 |
+
for t in range(trials):
|
| 547 |
+
args = gen(seed + t)
|
| 548 |
+
r_outs = _as_outputs(ref(*args))
|
| 549 |
+
c_outs = _as_outputs(cand(*args))
|
| 550 |
+
assert len(r_outs) == len(c_outs), "output arity mismatch"
|
| 551 |
+
# the order band is POINTWISE: a near-zero reduction output is
|
| 552 |
+
# legitimately thousands of ULP order-sensitive (cancellation)
|
| 553 |
+
# while its neighbors are stable, so each element is judged
|
| 554 |
+
# against its own measured sensitivity, not a global max
|
| 555 |
+
bands = None
|
| 556 |
+
band = 0
|
| 557 |
+
band_score = 0.0
|
| 558 |
+
if order_shuffle is not None:
|
| 559 |
+
bands = [torch.zeros_like(ro, dtype=torch.int64)
|
| 560 |
+
for ro in r_outs]
|
| 561 |
+
for k in range(order_trials):
|
| 562 |
+
r2 = _as_outputs(
|
| 563 |
+
ref(*order_shuffle(args, 1000 + seed + t * 131 + k)))
|
| 564 |
+
for bi, (ro, r2o) in enumerate(zip(r_outs, r2)):
|
| 565 |
+
bands[bi] = torch.maximum(bands[bi],
|
| 566 |
+
ulp_diff(ro, r2o))
|
| 567 |
+
band = max(int(b.max().item()) if b.numel() else 0
|
| 568 |
+
for b in bands)
|
| 569 |
+
max_ulp = 0
|
| 570 |
+
mean_ulp = 0.0
|
| 571 |
+
first_div = -1
|
| 572 |
+
n_el = 0
|
| 573 |
+
nan_r = nan_c = inf_r = inf_c = 0
|
| 574 |
+
for oi, (r, c) in enumerate(zip(r_outs, c_outs)):
|
| 575 |
+
d = ulp_diff(r, c)
|
| 576 |
+
if bands is not None and d.numel():
|
| 577 |
+
ratio = d.double() / (bands[oi].double() + 2.0)
|
| 578 |
+
band_score = max(band_score, float(ratio.max().item()))
|
| 579 |
+
if d.numel():
|
| 580 |
+
max_ulp = max(max_ulp, int(d.max().item()))
|
| 581 |
+
mean_ulp += float(d.double().sum().item())
|
| 582 |
+
n_el += d.numel()
|
| 583 |
+
nz = (d > 0)
|
| 584 |
+
if first_div < 0 and nz.any():
|
| 585 |
+
first_div = int(nz.flatten().nonzero()[0].item())
|
| 586 |
+
if nz.any():
|
| 587 |
+
buckets = torch.clamp(
|
| 588 |
+
torch.log2(d[nz].double()).floor(), 0, 62
|
| 589 |
+
).to(torch.int64)
|
| 590 |
+
counts = torch.bincount(buckets, minlength=63)
|
| 591 |
+
for bkt in counts.nonzero().flatten().tolist():
|
| 592 |
+
total_hist[bkt] = total_hist.get(bkt, 0) + int(
|
| 593 |
+
counts[bkt].item())
|
| 594 |
+
fin = torch.isfinite(r) & torch.isfinite(c)
|
| 595 |
+
if fin.any():
|
| 596 |
+
diff = (c[fin].double() - r[fin].double()).abs()
|
| 597 |
+
max_abs_seen = max(max_abs_seen,
|
| 598 |
+
float(diff.max().item()))
|
| 599 |
+
rel = diff / r[fin].double().abs().clamp(min=1e-300)
|
| 600 |
+
max_rel_seen = max(max_rel_seen,
|
| 601 |
+
float(rel.max().item()))
|
| 602 |
+
nan_r += int(torch.isnan(r).sum().item())
|
| 603 |
+
nan_c += int(torch.isnan(c).sum().item())
|
| 604 |
+
inf_r += int(torch.isinf(r).sum().item())
|
| 605 |
+
inf_c += int(torch.isinf(c).sum().item())
|
| 606 |
+
trial_rep = {
|
| 607 |
+
"max_ulp": max_ulp,
|
| 608 |
+
"mean_ulp": round(mean_ulp / max(n_el, 1), 3),
|
| 609 |
+
"first_divergent_index": first_div,
|
| 610 |
+
"nan_ref": nan_r, "nan_cand": nan_c,
|
| 611 |
+
"inf_ref": inf_r, "inf_cand": inf_c,
|
| 612 |
+
"order_band_ulp": band,
|
| 613 |
+
"band_score": round(band_score, 2),
|
| 614 |
+
}
|
| 615 |
+
per_trial.append(trial_rep)
|
| 616 |
+
if max_ulp > worst["max_ulp"]:
|
| 617 |
+
worst = trial_rep
|
| 618 |
+
band_max = max(t["order_band_ulp"] for t in per_trial)
|
| 619 |
+
max_seen = max(t["max_ulp"] for t in per_trial)
|
| 620 |
+
if order_shuffle is not None:
|
| 621 |
+
score = max(t["band_score"] for t in per_trial)
|
| 622 |
+
ulp_ok = score <= band_margin
|
| 623 |
+
criterion = (f"pointwise max(ulp / (order_band + 2)) = "
|
| 624 |
+
f"{score:.2f} <= band_margin {band_margin}")
|
| 625 |
+
else:
|
| 626 |
+
tol = ulp_tol if ulp_tol is not None else 2
|
| 627 |
+
ulp_ok = max_seen <= tol
|
| 628 |
+
criterion = f"max_ulp {max_seen} <= ulp_tol {tol}"
|
| 629 |
+
tol_ok = False
|
| 630 |
+
if atol is not None or rtol is not None:
|
| 631 |
+
tol_ok = max_abs_seen <= (atol or 0) + (rtol or 0) * 1.0 or \
|
| 632 |
+
max_rel_seen <= (rtol or 0) or max_abs_seen <= (atol or 0)
|
| 633 |
+
criterion += (f"; OR max_abs {max_abs_seen:.3e} <= atol {atol} / "
|
| 634 |
+
f"max_rel {max_rel_seen:.3e} <= rtol {rtol}")
|
| 635 |
+
return {
|
| 636 |
+
"pass": bool(ulp_ok or tol_ok),
|
| 637 |
+
"criterion": criterion,
|
| 638 |
+
"worst_trial": worst,
|
| 639 |
+
"max_abs": max_abs_seen,
|
| 640 |
+
"max_rel": max_rel_seen,
|
| 641 |
+
"trials": per_trial,
|
| 642 |
+
"ulp_hist_log2": dict(sorted(total_hist.items())),
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
|
| 646 |
+
_FUZZ_VARIANTS = ("contig", "transposed", "strided_rows", "offset_slice",
|
| 647 |
+
"overflow_expand")
|
| 648 |
+
|
| 649 |
+
|
| 650 |
+
def _context_alive(device):
|
| 651 |
+
try:
|
| 652 |
+
t = torch.ones(4, device=device)
|
| 653 |
+
torch.cuda.synchronize()
|
| 654 |
+
return bool(t.sum().item() == 4.0)
|
| 655 |
+
except Exception: # noqa: BLE001
|
| 656 |
+
return False
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
def fuzz_shapes(op, ref, make, shapes=None, variants=_FUZZ_VARIANTS,
|
| 660 |
+
dtypes=(torch.float32,), ulp_tol=64, device="cuda"):
|
| 661 |
+
"""The ugly envelope: run op vs ref across edge shapes, dtypes, and
|
| 662 |
+
layout mutations; report ok / mismatch / exception per case.
|
| 663 |
+
|
| 664 |
+
`make(shape, dtype)` (or `make(shape)`) builds a base input, either a
|
| 665 |
+
tensor or a tuple of tensors for multi-argument ops; layout variants
|
| 666 |
+
derive non-contiguous and misaligned views whose CONTENT equals a
|
| 667 |
+
contiguous copy, so `op(*xs)` must match `ref(*xs)` on every variant.
|
| 668 |
+
`overflow_expand` stride-0-expands the input past 2**31 logical
|
| 669 |
+
elements to catch int32 indexing, and is skipped with status
|
| 670 |
+
`skipped_vram` when materializing the output would not fit. After any
|
| 671 |
+
exception the CUDA context is health-checked; if a candidate killed
|
| 672 |
+
it, remaining cases are marked `context_lost` instead of producing
|
| 673 |
+
garbage."""
|
| 674 |
+
if shapes is None:
|
| 675 |
+
shapes = [(0, 8), (1, 1), (1, 17), (3, 63), (17, 129), (128, 128),
|
| 676 |
+
(1023, 5), (2, 4096)]
|
| 677 |
+
|
| 678 |
+
def build(shape, dtype):
|
| 679 |
+
try:
|
| 680 |
+
out = make(tuple(shape), dtype)
|
| 681 |
+
except TypeError:
|
| 682 |
+
out = make(tuple(shape))
|
| 683 |
+
return out if isinstance(out, (tuple, list)) else (out,)
|
| 684 |
+
|
| 685 |
+
def variant_of(x, v):
|
| 686 |
+
if v == "contig":
|
| 687 |
+
return x
|
| 688 |
+
if v == "transposed":
|
| 689 |
+
return x.t() if x.dim() == 2 else x
|
| 690 |
+
if v == "strided_rows":
|
| 691 |
+
return x[::2] if x.shape[0] >= 2 else x
|
| 692 |
+
if v == "offset_slice":
|
| 693 |
+
# a storage-offset-1 view: misaligned for vectorized loads
|
| 694 |
+
return x.flatten()[1:] if x.numel() > 1 else x
|
| 695 |
+
if v == "overflow_expand":
|
| 696 |
+
if x.numel() == 0:
|
| 697 |
+
return None
|
| 698 |
+
per = max(x.numel(), 1)
|
| 699 |
+
rep = (1 << 31) // per + 2
|
| 700 |
+
return x.reshape((1,) + tuple(x.shape)).expand(
|
| 701 |
+
(rep,) + tuple(x.shape))
|
| 702 |
+
return x
|
| 703 |
+
|
| 704 |
+
rows = []
|
| 705 |
+
lost = False
|
| 706 |
+
for dtype in dtypes:
|
| 707 |
+
for shape in shapes:
|
| 708 |
+
bases = build(shape, dtype)
|
| 709 |
+
for v in variants:
|
| 710 |
+
case = {"shape": tuple(shape), "variant": v,
|
| 711 |
+
"dtype": str(dtype).replace("torch.", "")}
|
| 712 |
+
if lost:
|
| 713 |
+
case["status"] = "context_lost"
|
| 714 |
+
rows.append(case)
|
| 715 |
+
continue
|
| 716 |
+
xs = [variant_of(b, v) for b in bases]
|
| 717 |
+
if any(x is None for x in xs):
|
| 718 |
+
case["status"] = "skipped_empty"
|
| 719 |
+
rows.append(case)
|
| 720 |
+
continue
|
| 721 |
+
if v == "overflow_expand":
|
| 722 |
+
logical = max(x.numel() for x in xs)
|
| 723 |
+
need = logical * xs[0].element_size() * 2
|
| 724 |
+
free, _total = torch.cuda.mem_get_info()
|
| 725 |
+
if need > free * 0.8:
|
| 726 |
+
case["status"] = "skipped_vram"
|
| 727 |
+
case["logical_elems"] = int(logical)
|
| 728 |
+
rows.append(case)
|
| 729 |
+
continue
|
| 730 |
+
case["logical_elems"] = int(logical)
|
| 731 |
+
case["contiguous"] = bool(xs[0].is_contiguous())
|
| 732 |
+
try:
|
| 733 |
+
got = op(*xs)
|
| 734 |
+
want = ref(*xs)
|
| 735 |
+
if v == "overflow_expand":
|
| 736 |
+
# bounded comparison: the head plus the high-index
|
| 737 |
+
# tail, which is where int32 flat indexing wraps
|
| 738 |
+
w = 1 << 20
|
| 739 |
+
gf, wf = got.flatten(), want.flatten()
|
| 740 |
+
pairs = ((gf[:w], wf[:w]), (gf[-w:], wf[-w:]))
|
| 741 |
+
else:
|
| 742 |
+
pairs = ((got, want),)
|
| 743 |
+
mx = 0
|
| 744 |
+
for gp, wp in pairs:
|
| 745 |
+
d = ulp_diff(wp, gp) if gp.is_floating_point() \
|
| 746 |
+
else (wp != gp).to(torch.int64) * (1 << 62)
|
| 747 |
+
if d.numel():
|
| 748 |
+
mx = max(mx, int(d.max().item()))
|
| 749 |
+
del d
|
| 750 |
+
case["status"] = "ok" if mx <= ulp_tol else "mismatch"
|
| 751 |
+
case["max_ulp"] = mx
|
| 752 |
+
del got, want
|
| 753 |
+
except Exception as e: # noqa: BLE001 - report, don't die
|
| 754 |
+
case["status"] = "exception"
|
| 755 |
+
case["error"] = f"{type(e).__name__}: {str(e)[:120]}"
|
| 756 |
+
if not _context_alive(device):
|
| 757 |
+
lost = True
|
| 758 |
+
case["status"] = "context_killed"
|
| 759 |
+
rows.append(case)
|
| 760 |
+
n_bad = sum(1 for r in rows
|
| 761 |
+
if r["status"] not in ("ok", "skipped_vram",
|
| 762 |
+
"skipped_empty"))
|
| 763 |
+
return {"cases": rows, "failed": n_bad, "total": len(rows),
|
| 764 |
+
"context_lost": lost}
|
| 765 |
+
|
| 766 |
+
|
| 767 |
+
def sweep(factory, grid, iters=30, warmup=5, bytes=None, flops=None,
|
| 768 |
+
check=None, budget_s=None):
|
| 769 |
+
"""Launch-configuration search: `factory(**point) -> callable`; one
|
| 770 |
+
bench row per grid point, sorted fastest first. A point whose factory
|
| 771 |
+
or run raises is reported, not skipped silently. `check(fn) -> bool`
|
| 772 |
+
is an optional correctness gate recorded per row (a fast wrong config
|
| 773 |
+
must not win a sweep). `budget_s` caps total sweep time; points not
|
| 774 |
+
reached are reported as `not_run` rather than silently dropped."""
|
| 775 |
+
keys = list(grid.keys())
|
| 776 |
+
|
| 777 |
+
def points(i, acc):
|
| 778 |
+
if i == len(keys):
|
| 779 |
+
yield dict(acc)
|
| 780 |
+
return
|
| 781 |
+
for v in grid[keys[i]]:
|
| 782 |
+
acc[keys[i]] = v
|
| 783 |
+
yield from points(i + 1, acc)
|
| 784 |
+
|
| 785 |
+
rows = []
|
| 786 |
+
t_start = time.perf_counter()
|
| 787 |
+
over_budget = False
|
| 788 |
+
for p in points(0, {}):
|
| 789 |
+
row = dict(p)
|
| 790 |
+
if over_budget or (budget_s is not None and
|
| 791 |
+
time.perf_counter() - t_start > budget_s):
|
| 792 |
+
over_budget = True
|
| 793 |
+
row["status"] = "not_run"
|
| 794 |
+
row["median_ms"] = float("inf")
|
| 795 |
+
rows.append(row)
|
| 796 |
+
continue
|
| 797 |
+
try:
|
| 798 |
+
fn = factory(**p)
|
| 799 |
+
if check is not None:
|
| 800 |
+
row["check"] = bool(check(fn))
|
| 801 |
+
rep = bench(fn, iters=iters, warmup=warmup, bytes=bytes,
|
| 802 |
+
flops=flops)
|
| 803 |
+
row.update({"median_ms": rep["median_ms"],
|
| 804 |
+
"rejected_throttled": rep["rejected_throttled"]})
|
| 805 |
+
if "achieved_gbs" in rep:
|
| 806 |
+
row["achieved_gbs"] = rep["achieved_gbs"]
|
| 807 |
+
if "achieved_tflops" in rep:
|
| 808 |
+
row["achieved_tflops"] = rep["achieved_tflops"]
|
| 809 |
+
except Exception as e: # noqa: BLE001
|
| 810 |
+
row["error"] = f"{type(e).__name__}: {str(e)[:120]}"
|
| 811 |
+
row["median_ms"] = float("inf")
|
| 812 |
+
rows.append(row)
|
| 813 |
+
rows.sort(key=lambda r: (not r.get("check", True), r["median_ms"]))
|
| 814 |
+
return rows
|
| 815 |
+
|
| 816 |
+
|
| 817 |
+
def alloc_stamps(n, device="cuda"):
|
| 818 |
+
"""Device buffer for the MK_STAMP convention (metakernel_cuda/
|
| 819 |
+
stamps.cuh): the candidate kernel writes clock64() into slot i."""
|
| 820 |
+
return torch.zeros(n, dtype=torch.int64, device=device)
|
| 821 |
+
|
| 822 |
+
|
| 823 |
+
def read_stamps(stamps, labels=None, clock_ghz=None):
|
| 824 |
+
"""Decode MK_STAMP phases: consecutive deltas in ticks and in
|
| 825 |
+
microseconds under the measured (or provided) SM clock. A 1-D buffer
|
| 826 |
+
(MK_STAMP: block 0 only) yields one delta per phase; a 2-D [B, n]
|
| 827 |
+
buffer (MK_STAMP_BLOCK) yields min / median / max across blocks per
|
| 828 |
+
phase, with blocks whose stamps stayed zero excluded."""
|
| 829 |
+
if clock_ghz is None:
|
| 830 |
+
clock_ghz = measure_clock(stamps.device)
|
| 831 |
+
|
| 832 |
+
def us(ticks):
|
| 833 |
+
return round(ticks / clock_ghz / 1000.0, 3)
|
| 834 |
+
|
| 835 |
+
if stamps.dim() == 1:
|
| 836 |
+
vals = stamps.cpu().tolist()
|
| 837 |
+
phases = []
|
| 838 |
+
for i in range(len(vals) - 1):
|
| 839 |
+
ticks = vals[i + 1] - vals[i]
|
| 840 |
+
name = labels[i] if labels and i < len(labels) else f"phase_{i}"
|
| 841 |
+
phases.append({"phase": name, "ticks": ticks, "us": us(ticks)})
|
| 842 |
+
return {"phases": phases, "clock_ghz": round(clock_ghz, 3)}
|
| 843 |
+
|
| 844 |
+
m = stamps.cpu()
|
| 845 |
+
live = (m != 0).all(dim=1)
|
| 846 |
+
m = m[live]
|
| 847 |
+
deltas = (m[:, 1:] - m[:, :-1]).to(torch.float64)
|
| 848 |
+
phases = []
|
| 849 |
+
for i in range(deltas.shape[1]):
|
| 850 |
+
col = deltas[:, i]
|
| 851 |
+
name = labels[i] if labels and i < len(labels) else f"phase_{i}"
|
| 852 |
+
med = float(col.median().item()) if col.numel() else 0.0
|
| 853 |
+
phases.append({
|
| 854 |
+
"phase": name,
|
| 855 |
+
"ticks_median": int(med),
|
| 856 |
+
"ticks_min": int(col.min().item()) if col.numel() else 0,
|
| 857 |
+
"ticks_max": int(col.max().item()) if col.numel() else 0,
|
| 858 |
+
"us_median": us(med),
|
| 859 |
+
"blocks": int(col.numel()),
|
| 860 |
+
})
|
| 861 |
+
return {"phases": phases, "clock_ghz": round(clock_ghz, 3),
|
| 862 |
+
"blocks_reporting": int(live.sum().item()),
|
| 863 |
+
"blocks_total": int(live.numel())}
|
build/torch212-cxx11-cu126-x86_64-linux/_metakernel_cuda_4611d12.abi3.so
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:95a7ad4c20e35be975f172307ed4ccfdfaacda544ffded4a0394a6c66451920c
|
| 3 |
+
size 636184
|
build/torch212-cxx11-cu126-x86_64-linux/_ops.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from . import _metakernel_cuda_4611d12
|
| 3 |
+
ops = torch.ops._metakernel_cuda_4611d12
|
| 4 |
+
|
| 5 |
+
def add_op_namespace_prefix(op_name: str):
|
| 6 |
+
"""
|
| 7 |
+
Prefix op by namespace.
|
| 8 |
+
"""
|
| 9 |
+
return f"_metakernel_cuda_4611d12::{op_name}"
|
build/torch212-cxx11-cu126-x86_64-linux/metadata.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "metakernel",
|
| 3 |
+
"id": "_metakernel_cuda_4611d12",
|
| 4 |
+
"version": 1,
|
| 5 |
+
"license": "Apache-2.0",
|
| 6 |
+
"python-depends": [],
|
| 7 |
+
"backend": {
|
| 8 |
+
"type": "cuda",
|
| 9 |
+
"archs": [
|
| 10 |
+
"8.0",
|
| 11 |
+
"8.6",
|
| 12 |
+
"8.9",
|
| 13 |
+
"9.0"
|
| 14 |
+
]
|
| 15 |
+
},
|
| 16 |
+
"digest": {
|
| 17 |
+
"algorithm": "sha256",
|
| 18 |
+
"files": {
|
| 19 |
+
"__init__.py": "cZxKX5I/2EKCMC2gdCwflUsZaQJWJesmAHq2w+fVjXo=",
|
| 20 |
+
"_metakernel_cuda_4611d12.abi3.so": "laetTCDjW+l18XIwftTM/fqs2lRP/e1KA5SmxmRRkgw=",
|
| 21 |
+
"_ops.py": "uZlR720EcTy8M7BCydmvaZAlKonkwTU3778nhR9yQOE=",
|
| 22 |
+
"metakernel/__init__.py": "DFYPlrhXwYjEqCl/8n0SmWGZV8NFml5DPhMjKfv98GY="
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"provenance": {
|
| 26 |
+
"kernel-builder": {
|
| 27 |
+
"version": "0.17.0-dev0",
|
| 28 |
+
"sha": "19aaa6421e674e9fecc352bbae6eab81d19a6bf4",
|
| 29 |
+
"dirty": false
|
| 30 |
+
},
|
| 31 |
+
"kernel": {
|
| 32 |
+
"sha": "4611d1213a76a9ec1d12646553a528feb60394ff",
|
| 33 |
+
"dirty": false
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
}
|
build/torch212-cxx11-cu126-x86_64-linux/metakernel/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ctypes
|
| 2 |
+
import importlib.util
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from types import ModuleType
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def _import_from_path(file_path: Path) -> ModuleType:
|
| 9 |
+
# We cannot use the module name as-is, after adding it to `sys.modules`,
|
| 10 |
+
# it would also be used for other imports. So, we make a module name that
|
| 11 |
+
# depends on the path for it to be unique using the hex-encoded hash of
|
| 12 |
+
# the path.
|
| 13 |
+
path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
|
| 14 |
+
module_name = path_hash
|
| 15 |
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
| 16 |
+
if spec is None:
|
| 17 |
+
raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
|
| 18 |
+
module = importlib.util.module_from_spec(spec)
|
| 19 |
+
if module is None:
|
| 20 |
+
raise ImportError(f"Cannot load module {module_name} from spec")
|
| 21 |
+
sys.modules[module_name] = module
|
| 22 |
+
spec.loader.exec_module(module) # type: ignore
|
| 23 |
+
return module
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
|
build/torch212-cxx11-cu130-x86_64-linux/__init__.py
ADDED
|
@@ -0,0 +1,863 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""metakernel: the measuring instrument for the kernel-design loop.
|
| 2 |
+
|
| 3 |
+
An agent iterating on CUDA candidates is good at writing code and bad at
|
| 4 |
+
trusting its own measurements; it needs trustworthy timing, adversarial
|
| 5 |
+
correctness comparison, and measured facts about the device it targets.
|
| 6 |
+
metakernel ships those as loadable ops:
|
| 7 |
+
|
| 8 |
+
from kernels import get_kernel
|
| 9 |
+
mk = get_kernel("phanerozoic/metakernel", version=1,
|
| 10 |
+
trust_remote_code=True)
|
| 11 |
+
|
| 12 |
+
dossier = mk.probe_device() # measured facts
|
| 13 |
+
report = mk.bench(lambda: op(x), iters=200) # throttle-rejected
|
| 14 |
+
verdict = mk.compare(ref, cand, gen) # ULP evidence
|
| 15 |
+
table = mk.sweep(factory, {"BLOCK": [64, 128]}) # config search
|
| 16 |
+
|
| 17 |
+
Every op returns plain dicts and tensors, never prose. Every dossier entry
|
| 18 |
+
is produced by a dedicated probe kernel rather than a spec sheet, carries
|
| 19 |
+
the SM clock it was measured under, and labels throttled samples instead
|
| 20 |
+
of letting them poison medians. No hardware counters are used: everything
|
| 21 |
+
is inferred from timing, clocks, and the occupancy API, so entries carry a
|
| 22 |
+
confidence field (timing-differential inference can misattribute).
|
| 23 |
+
"""
|
| 24 |
+
import math
|
| 25 |
+
import time
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
|
| 29 |
+
from ._ops import ops
|
| 30 |
+
|
| 31 |
+
__all__ = ["probe_device", "bench", "compare", "order_shuffles",
|
| 32 |
+
"fuzz_shapes", "sweep", "alloc_stamps", "read_stamps",
|
| 33 |
+
"measure_clock", "ulp_diff", "ops"]
|
| 34 |
+
|
| 35 |
+
_MMA_KINDS = {
|
| 36 |
+
"fp16": (0, 16 * 16 * 16),
|
| 37 |
+
"bf16": (1, 16 * 16 * 16),
|
| 38 |
+
"tf32": (2, 16 * 16 * 8),
|
| 39 |
+
"int8": (3, 16 * 16 * 16),
|
| 40 |
+
"fp8_e4m3": (4, 16 * 8 * 32),
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _sync():
|
| 45 |
+
torch.cuda.synchronize()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _events(fn, reps):
|
| 49 |
+
"""Time reps calls of fn with one event pair each; returns ms list."""
|
| 50 |
+
starts = [torch.cuda.Event(enable_timing=True) for _ in range(reps)]
|
| 51 |
+
stops = [torch.cuda.Event(enable_timing=True) for _ in range(reps)]
|
| 52 |
+
for s, e in zip(starts, stops):
|
| 53 |
+
s.record()
|
| 54 |
+
fn()
|
| 55 |
+
e.record()
|
| 56 |
+
_sync()
|
| 57 |
+
return [s.elapsed_time(e) for s, e in zip(starts, stops)]
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def measure_clock(device="cuda", ticks=4_000_000, blocks=None):
|
| 61 |
+
"""Effective SM clock in GHz under an FMA spin of `ticks` clock64 ticks.
|
| 62 |
+
Wall time over elapsed ticks is the frequency the ticks ran at."""
|
| 63 |
+
props = torch.cuda.get_device_properties(device)
|
| 64 |
+
if blocks is None:
|
| 65 |
+
blocks = props.multi_processor_count
|
| 66 |
+
out = torch.zeros(2, dtype=torch.int64, device=device)
|
| 67 |
+
sink = torch.zeros(2, dtype=torch.float32, device=device)
|
| 68 |
+
ops.mk_spin(blocks, 256, ticks, out, sink) # warm
|
| 69 |
+
start = torch.cuda.Event(enable_timing=True)
|
| 70 |
+
stop = torch.cuda.Event(enable_timing=True)
|
| 71 |
+
start.record()
|
| 72 |
+
ops.mk_spin(blocks, 256, ticks, out, sink)
|
| 73 |
+
stop.record()
|
| 74 |
+
_sync()
|
| 75 |
+
ms = start.elapsed_time(stop)
|
| 76 |
+
elapsed = int(out[0].item())
|
| 77 |
+
return elapsed / (ms * 1e6)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _median(xs):
|
| 81 |
+
s = sorted(xs)
|
| 82 |
+
n = len(s)
|
| 83 |
+
return s[n // 2] if n % 2 else 0.5 * (s[n // 2 - 1] + s[n // 2])
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _entry(value, units, method, clock_ghz, confidence, note=""):
|
| 87 |
+
d = {"value": value, "units": units, "method": method,
|
| 88 |
+
"clock_ghz": round(clock_ghz, 3), "confidence": confidence}
|
| 89 |
+
if note:
|
| 90 |
+
d["note"] = note
|
| 91 |
+
return d
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _sattolo_ring(n, device, generator=None):
|
| 95 |
+
"""A random single-cycle permutation as an int32 next-pointer table, so
|
| 96 |
+
a pointer chase visits every slot before repeating."""
|
| 97 |
+
perm = torch.randperm(n, generator=generator)
|
| 98 |
+
ring = torch.empty(n, dtype=torch.int32)
|
| 99 |
+
ring[perm] = torch.roll(perm, -1).to(torch.int32)
|
| 100 |
+
return ring.to(device)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def probe_device(device="cuda", quick=False):
|
| 104 |
+
"""Empirical device dossier. Each entry is measured by a dedicated
|
| 105 |
+
probe kernel and carries units, method, the SM clock it ran under, and
|
| 106 |
+
a confidence field."""
|
| 107 |
+
props = torch.cuda.get_device_properties(device)
|
| 108 |
+
sm = props.multi_processor_count
|
| 109 |
+
l2 = props.L2_cache_size
|
| 110 |
+
d = {
|
| 111 |
+
"device": {
|
| 112 |
+
"name": props.name,
|
| 113 |
+
"sm_arch": f"sm{props.major}{props.minor}",
|
| 114 |
+
"sms": sm,
|
| 115 |
+
"l2_bytes": l2,
|
| 116 |
+
"vram_bytes": props.total_memory,
|
| 117 |
+
"torch": torch.__version__,
|
| 118 |
+
"cuda_runtime": torch.version.cuda,
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
sink = torch.zeros(2, dtype=torch.float32, device=device)
|
| 122 |
+
out64 = torch.zeros(2, dtype=torch.int64, device=device)
|
| 123 |
+
reps = 3 if quick else 5
|
| 124 |
+
|
| 125 |
+
# ---- clock under sustained load
|
| 126 |
+
traj = []
|
| 127 |
+
windows = 8 if quick else 24
|
| 128 |
+
for _ in range(windows):
|
| 129 |
+
traj.append(measure_clock(device, ticks=30_000_000))
|
| 130 |
+
d["clock"] = {
|
| 131 |
+
"max_ghz": round(max(traj), 3),
|
| 132 |
+
"min_ghz": round(min(traj), 3),
|
| 133 |
+
"final_ghz": round(traj[-1], 3),
|
| 134 |
+
"sag_pct": round(100 * (1 - traj[-1] / max(traj)), 1),
|
| 135 |
+
"units": "GHz",
|
| 136 |
+
"method": f"{windows} back-to-back FMA spin windows of 30M ticks; "
|
| 137 |
+
"wall/ticks per window",
|
| 138 |
+
"confidence": "high",
|
| 139 |
+
}
|
| 140 |
+
clk = max(traj)
|
| 141 |
+
|
| 142 |
+
# ---- HBM bandwidth: triad per access width, plus read-only,
|
| 143 |
+
# write-only, and random-gather
|
| 144 |
+
n4 = 16 << 20 # 16M float4 = 256 MB per buffer
|
| 145 |
+
if props.total_memory < 4 << 30:
|
| 146 |
+
n4 = 4 << 20
|
| 147 |
+
a = torch.rand(n4 * 4, device=device)
|
| 148 |
+
b = torch.rand(n4 * 4, device=device)
|
| 149 |
+
c = torch.empty(n4 * 4, device=device)
|
| 150 |
+
hbm = {}
|
| 151 |
+
for width in (1, 2, 4):
|
| 152 |
+
ops.mk_triad(a, b, c, width, 1.5) # warm
|
| 153 |
+
ms = _median(_events(lambda w=width: ops.mk_triad(a, b, c, w, 1.5),
|
| 154 |
+
reps))
|
| 155 |
+
gbs = 3 * a.numel() * 4 / (ms * 1e-3) / 1e9
|
| 156 |
+
hbm[f"triad_{4 * width}B"] = round(gbs, 1)
|
| 157 |
+
ops.mk_read(a, sink)
|
| 158 |
+
ms = _median(_events(lambda: ops.mk_read(a, sink), reps))
|
| 159 |
+
hbm["read_only"] = round(a.numel() * 4 / (ms * 1e-3) / 1e9, 1)
|
| 160 |
+
ops.mk_write(c, 1.0)
|
| 161 |
+
ms = _median(_events(lambda: ops.mk_write(c, 1.0), reps))
|
| 162 |
+
hbm["write_only"] = round(c.numel() * 4 / (ms * 1e-3) / 1e9, 1)
|
| 163 |
+
g = torch.Generator(device="cpu").manual_seed(7)
|
| 164 |
+
idx = torch.randint(0, n4, (n4,), generator=g,
|
| 165 |
+
dtype=torch.int32).to(device)
|
| 166 |
+
ops.mk_gather(a, idx, c, 256, 0)
|
| 167 |
+
ms = _median(_events(lambda: ops.mk_gather(a, idx, c, 256, 0), reps))
|
| 168 |
+
# traffic accounting: idx reads + 16B random loads + streamed stores
|
| 169 |
+
hbm["random_gather_16B"] = round(
|
| 170 |
+
(idx.numel() * 4 + 2 * n4 * 16) / (ms * 1e-3) / 1e9, 1)
|
| 171 |
+
d["hbm_bw"] = _entry(hbm, "GB/s", "streaming triad c=a+s*b per access "
|
| 172 |
+
"width; read-only fold; write-only fill; random "
|
| 173 |
+
"float4 gather (16B granules, nominal traffic)",
|
| 174 |
+
clk, "high",
|
| 175 |
+
"gather moves full cache lines per 16B granule, "
|
| 176 |
+
"so its nominal number is the achievable rate for "
|
| 177 |
+
"random access, not a line-efficiency claim")
|
| 178 |
+
|
| 179 |
+
# ---- L2 bandwidth (working set 3 buffers inside L2)
|
| 180 |
+
ws = max(l2 // 4, 1 << 20)
|
| 181 |
+
nl2 = ws // 16 # float4 elems per buffer
|
| 182 |
+
al = a[:nl2 * 4].contiguous()
|
| 183 |
+
bl = b[:nl2 * 4].contiguous()
|
| 184 |
+
cl = c[:nl2 * 4].contiguous()
|
| 185 |
+
passes = 64
|
| 186 |
+
ops.mk_triad_passes(al, bl, cl, 1.5, passes)
|
| 187 |
+
ms = _median(_events(
|
| 188 |
+
lambda: ops.mk_triad_passes(al, bl, cl, 1.5, passes), reps))
|
| 189 |
+
gbs = 3 * al.numel() * 4 * passes / (ms * 1e-3) / 1e9
|
| 190 |
+
d["l2_bw"] = _entry(round(gbs, 1), "GB/s",
|
| 191 |
+
f"in-kernel repeated triad passes over "
|
| 192 |
+
f"{3 * ws >> 20} MB resident working set", clk,
|
| 193 |
+
"medium", "first pass streams from HBM; amortized "
|
| 194 |
+
f"over {passes} passes")
|
| 195 |
+
|
| 196 |
+
# ---- latencies: pointer chase per level. Footprints are chosen so
|
| 197 |
+
# the chain hits the intended level: the L2 ring (2 MB) exceeds L1,
|
| 198 |
+
# fits L2 on every supported part, and spans few enough pages to stay
|
| 199 |
+
# TLB-resident after a full-cycle warm walk; the HBM chain is long
|
| 200 |
+
# enough (2x L2 in touched lines) that it evicts itself and cannot be
|
| 201 |
+
# served from cache.
|
| 202 |
+
lat = {}
|
| 203 |
+
idle_clk = measure_clock(device, ticks=2_000_000, blocks=1)
|
| 204 |
+
ring_s = _sattolo_ring(8192, device)
|
| 205 |
+
ops.mk_chase_shared(ring_s, 1 << 14, out64)
|
| 206 |
+
_sync()
|
| 207 |
+
hops = 1 << 16
|
| 208 |
+
ops.mk_chase_shared(ring_s, hops, out64)
|
| 209 |
+
_sync()
|
| 210 |
+
lat["smem_cycles"] = round(int(out64[0].item()) / hops, 1)
|
| 211 |
+
|
| 212 |
+
l1_ring_n = (16 << 10) // 4 # 16 KB: L1-resident after a warm walk
|
| 213 |
+
ring_l1 = _sattolo_ring(l1_ring_n, device)
|
| 214 |
+
ops.mk_chase_global(ring_l1, l1_ring_n, out64)
|
| 215 |
+
_sync()
|
| 216 |
+
hops_l1 = 1 << 15
|
| 217 |
+
ops.mk_chase_global(ring_l1, hops_l1, out64)
|
| 218 |
+
_sync()
|
| 219 |
+
lat["l1_cycles"] = round(int(out64[0].item()) / hops_l1, 1)
|
| 220 |
+
|
| 221 |
+
l2_ring_n = (2 << 20) // 4
|
| 222 |
+
ring_l2 = _sattolo_ring(l2_ring_n, device)
|
| 223 |
+
ops.mk_chase_global(ring_l2, l2_ring_n, out64) # full-cycle warm walk
|
| 224 |
+
_sync()
|
| 225 |
+
hops_l2 = 1 << 14
|
| 226 |
+
ops.mk_chase_global(ring_l2, hops_l2, out64)
|
| 227 |
+
_sync()
|
| 228 |
+
lat["l2_cycles"] = round(int(out64[0].item()) / hops_l2, 1)
|
| 229 |
+
|
| 230 |
+
hbm_bytes = min(max(4 * l2, 64 << 20), props.total_memory // 8)
|
| 231 |
+
ring_h = _sattolo_ring(int(hbm_bytes) // 4, device)
|
| 232 |
+
hops_h = max((2 * l2) // 128, 1 << 17)
|
| 233 |
+
ops.mk_chase_global(ring_h, 1 << 14, out64) # fault pages / warm TLB
|
| 234 |
+
_sync()
|
| 235 |
+
ops.mk_chase_global(ring_h, hops_h, out64)
|
| 236 |
+
_sync()
|
| 237 |
+
lat["hbm_cycles"] = round(int(out64[0].item()) / hops_h, 1)
|
| 238 |
+
lat["ns_per_cycle"] = round(1.0 / idle_clk, 3)
|
| 239 |
+
d["latency"] = _entry(lat, "cycles (single-lane dependent loads)",
|
| 240 |
+
"Sattolo-cycle pointer chase; 16 KB ring for L1, "
|
| 241 |
+
"2 MB TLB-resident ring for L2, self-evicting "
|
| 242 |
+
"chain (2x L2 in touched lines) for HBM",
|
| 243 |
+
idle_clk, "high",
|
| 244 |
+
"smem includes index arithmetic; HBM includes "
|
| 245 |
+
"residual TLB effects at large footprints")
|
| 246 |
+
del ring_h, ring_l2, ring_l1, ring_s
|
| 247 |
+
|
| 248 |
+
# ---- fp32 FMA and tensor-core throughput
|
| 249 |
+
blocks = sm * 4
|
| 250 |
+
iters = 20_000 if quick else 60_000
|
| 251 |
+
ops.mk_fma_f32(blocks, 256, iters, sink)
|
| 252 |
+
ms = _median(_events(lambda: ops.mk_fma_f32(blocks, 256, iters, sink),
|
| 253 |
+
reps))
|
| 254 |
+
flops = blocks * 256 * iters * 16 * 2 * 2 # 2 chains x 2 flops x unroll
|
| 255 |
+
d["fma_f32"] = _entry(round(flops / (ms * 1e-3) / 1e12, 2), "TFLOP/s",
|
| 256 |
+
"register-resident FMA chains", clk, "high")
|
| 257 |
+
sink64 = torch.zeros(2, dtype=torch.float64, device=device)
|
| 258 |
+
it64 = max(iters // 16, 1000)
|
| 259 |
+
ops.mk_fma_f64(blocks, 256, it64, sink64)
|
| 260 |
+
ms = _median(_events(
|
| 261 |
+
lambda: ops.mk_fma_f64(blocks, 256, it64, sink64), reps))
|
| 262 |
+
flops64 = blocks * 256 * it64 * 16 * 2 * 2
|
| 263 |
+
d["fma_f64"] = _entry(round(flops64 / (ms * 1e-3) / 1e12, 3), "TFLOP/s",
|
| 264 |
+
"register-resident double FMA chains (the DFMA "
|
| 265 |
+
"pipe)", clk, "high")
|
| 266 |
+
mma = {}
|
| 267 |
+
for name, (kind, mnk) in _MMA_KINDS.items():
|
| 268 |
+
it = 30_000 if quick else 100_000
|
| 269 |
+
r = ops.mk_mma(kind, blocks, 8, 512, sink)
|
| 270 |
+
if r != 0:
|
| 271 |
+
mma[name] = "unsupported"
|
| 272 |
+
continue
|
| 273 |
+
_sync()
|
| 274 |
+
ms = _median(_events(
|
| 275 |
+
lambda k=kind, i=it: ops.mk_mma(k, blocks, 8, i, sink), reps))
|
| 276 |
+
flops = blocks * 8 * it * 4 * 2 * mnk
|
| 277 |
+
mma[name] = round(flops / (ms * 1e-3) / 1e12, 1)
|
| 278 |
+
d["mma"] = _entry(mma, "TFLOP/s (TOP/s for int8)",
|
| 279 |
+
"register-fragment mma_sync loops (wmma; fp8 via "
|
| 280 |
+
"inline PTX m16n8k32)", clk, "medium",
|
| 281 |
+
"dense-tile rate without memory traffic; real "
|
| 282 |
+
"kernels see this only when staging hides behind it")
|
| 283 |
+
|
| 284 |
+
# ---- atomic contention curve: throughput vs number of distinct
|
| 285 |
+
# addresses the whole grid hammers (slots = 1 is full contention;
|
| 286 |
+
# slots = threads is contention-free), plus the shared-memory point
|
| 287 |
+
n_thr = blocks * 256
|
| 288 |
+
buf = torch.zeros(n_thr, dtype=torch.float32, device=device)
|
| 289 |
+
at = {"curve_slots": {}}
|
| 290 |
+
it_at = 2_000 if quick else 6_000
|
| 291 |
+
slot_points = [1, 8, 64, 512, 4096, n_thr]
|
| 292 |
+
for slots in slot_points:
|
| 293 |
+
buf.zero_()
|
| 294 |
+
ops.mk_atomics(1, blocks, 256, it_at, buf, slots)
|
| 295 |
+
ms = _median(_events(
|
| 296 |
+
lambda s=slots: ops.mk_atomics(1, blocks, 256, it_at, buf, s),
|
| 297 |
+
reps))
|
| 298 |
+
at["curve_slots"][str(slots)] = round(
|
| 299 |
+
n_thr * it_at / (ms * 1e-3) / 1e9, 2)
|
| 300 |
+
ops.mk_atomics(3, blocks, 256, it_at, buf, 1)
|
| 301 |
+
ms = _median(_events(
|
| 302 |
+
lambda: ops.mk_atomics(3, blocks, 256, it_at, buf, 1), reps))
|
| 303 |
+
at["shared_same_addr"] = round(n_thr * it_at / (ms * 1e-3) / 1e9, 2)
|
| 304 |
+
d["atomics_f32"] = _entry(at, "Gop/s",
|
| 305 |
+
"atomicAdd hammer; curve sweeps the number "
|
| 306 |
+
"of distinct global addresses from 1 (full "
|
| 307 |
+
"contention) to one per thread", clk, "high")
|
| 308 |
+
|
| 309 |
+
# ---- launch overhead
|
| 310 |
+
anchor = sink
|
| 311 |
+
n_launch = 500 if quick else 2000
|
| 312 |
+
_sync()
|
| 313 |
+
t0 = time.perf_counter()
|
| 314 |
+
for _ in range(n_launch):
|
| 315 |
+
ops.mk_empty(anchor)
|
| 316 |
+
_sync()
|
| 317 |
+
queued = (time.perf_counter() - t0) / n_launch * 1e6
|
| 318 |
+
n_rt = 100 if quick else 300
|
| 319 |
+
t0 = time.perf_counter()
|
| 320 |
+
for _ in range(n_rt):
|
| 321 |
+
ops.mk_empty(anchor)
|
| 322 |
+
_sync()
|
| 323 |
+
rt = (time.perf_counter() - t0) / n_rt * 1e6
|
| 324 |
+
d["launch_overhead"] = _entry(
|
| 325 |
+
{"queued_us": round(queued, 2), "round_trip_us": round(rt, 2)},
|
| 326 |
+
"us", "empty-kernel trains, queued vs launch+sync", clk, "high",
|
| 327 |
+
"host-dependent: includes torch op dispatch")
|
| 328 |
+
|
| 329 |
+
# ---- grid barrier
|
| 330 |
+
bar_iters = 500 if quick else 2000
|
| 331 |
+
r = ops.mk_barrier(sm, 256, bar_iters, out64)
|
| 332 |
+
if r == 0:
|
| 333 |
+
_sync()
|
| 334 |
+
ticks = int(out64[0].item())
|
| 335 |
+
d["grid_barrier"] = _entry(
|
| 336 |
+
round(ticks / bar_iters / clk / 1000.0, 2), "us per grid.sync",
|
| 337 |
+
f"cooperative kernel, {sm} blocks x 256 threads", clk, "high")
|
| 338 |
+
else:
|
| 339 |
+
d["grid_barrier"] = _entry("unsupported", "", "cooperative launch "
|
| 340 |
+
"unavailable or grid too large", clk,
|
| 341 |
+
"high")
|
| 342 |
+
|
| 343 |
+
# ---- occupancy table (API) plus timed knees: the same random gather
|
| 344 |
+
# at every (threads, dyn_smem) point; when the forced blocks/SM stop
|
| 345 |
+
# covering memory latency, achieved bandwidth falls off the cliff
|
| 346 |
+
occ = {}
|
| 347 |
+
for threads in (128, 256, 512, 1024):
|
| 348 |
+
row = {}
|
| 349 |
+
for smem_kb in (0, 16, 32, 48):
|
| 350 |
+
row[f"{smem_kb}KB"] = int(ops.mk_occupancy(
|
| 351 |
+
threads, smem_kb * 1024, anchor))
|
| 352 |
+
occ[f"t{threads}"] = row
|
| 353 |
+
d["occupancy_blocks_per_sm"] = _entry(
|
| 354 |
+
occ, "blocks/SM", "occupancy API on the float4 triad kernel",
|
| 355 |
+
clk, "high", "API-derived, not timing-derived")
|
| 356 |
+
|
| 357 |
+
knee_iters = 6_000 if quick else 20_000
|
| 358 |
+
knee_blocks = sm * 8
|
| 359 |
+
knees = {}
|
| 360 |
+
for threads in (128, 256, 1024):
|
| 361 |
+
row = {}
|
| 362 |
+
for smem_kb in (0, 32, 48):
|
| 363 |
+
ops.mk_fma_dep(knee_blocks, threads, smem_kb * 1024,
|
| 364 |
+
knee_iters, sink)
|
| 365 |
+
ms = _median(_events(
|
| 366 |
+
lambda t=threads, s=smem_kb: ops.mk_fma_dep(
|
| 367 |
+
knee_blocks, t, s * 1024, knee_iters, sink), reps))
|
| 368 |
+
fl = knee_blocks * threads * knee_iters * 16 * 2
|
| 369 |
+
row[f"{smem_kb}KB"] = round(fl / (ms * 1e-3) / 1e12, 2)
|
| 370 |
+
knees[f"t{threads}"] = row
|
| 371 |
+
d["occupancy_knees_fma_tflops"] = _entry(
|
| 372 |
+
knees, "TFLOP/s", "a single dependent FMA chain per thread, timed "
|
| 373 |
+
"at forced (threads, dyn smem) occupancies over fixed total work; "
|
| 374 |
+
"throughput is proportional to resident threads until the pipe "
|
| 375 |
+
"saturates, so the cliff marks where occupancy stops covering "
|
| 376 |
+
"latency", clk, "high",
|
| 377 |
+
"timing-derived counterpart to the API table above")
|
| 378 |
+
return d
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def bench(fn, iters=100, warmup=10, bytes=None, flops=None, dossier=None,
|
| 382 |
+
dtype="fp16", clock_guard=0.97, clock_stride=8, graph=False,
|
| 383 |
+
device="cuda"):
|
| 384 |
+
"""Median-of-N cudaEvent timing with throttle rejection.
|
| 385 |
+
|
| 386 |
+
The SM clock is probed every `clock_stride` samples; samples inside a
|
| 387 |
+
window whose clock sagged below `clock_guard` x the best observed
|
| 388 |
+
clock are rejected and counted rather than silently averaged. With
|
| 389 |
+
declared `bytes`/`flops` per call, reports achieved GB/s / TFLOP/s;
|
| 390 |
+
with a `dossier`, adds roofline placement against the measured peaks.
|
| 391 |
+
|
| 392 |
+
`graph=True` captures fn() into a CUDA graph and times graph replays,
|
| 393 |
+
stripping Python and torch dispatch from the measurement: the right
|
| 394 |
+
mode for kernels in the few-microsecond range, where dispatch is
|
| 395 |
+
otherwise the thing being measured. Capture requires fn to be
|
| 396 |
+
graph-safe (no sync, no allocation growth, static shapes)."""
|
| 397 |
+
for _ in range(warmup):
|
| 398 |
+
fn()
|
| 399 |
+
_sync()
|
| 400 |
+
if graph:
|
| 401 |
+
g = torch.cuda.CUDAGraph()
|
| 402 |
+
with torch.cuda.graph(g):
|
| 403 |
+
fn()
|
| 404 |
+
fn = g.replay
|
| 405 |
+
for _ in range(warmup):
|
| 406 |
+
fn()
|
| 407 |
+
_sync()
|
| 408 |
+
samples = []
|
| 409 |
+
clocks = []
|
| 410 |
+
stride = clock_stride
|
| 411 |
+
for base in range(0, iters, stride):
|
| 412 |
+
clocks.append(measure_clock(device, ticks=1_500_000))
|
| 413 |
+
n = min(stride, iters - base)
|
| 414 |
+
samples.append((_events(fn, n), len(clocks) - 1))
|
| 415 |
+
clocks.append(measure_clock(device, ticks=1_500_000))
|
| 416 |
+
best_clk = max(clocks)
|
| 417 |
+
kept, rejected = [], 0
|
| 418 |
+
flat = []
|
| 419 |
+
for group, ci in samples:
|
| 420 |
+
window_ok = (clocks[ci] >= clock_guard * best_clk and
|
| 421 |
+
clocks[min(ci + 1, len(clocks) - 1)] >=
|
| 422 |
+
clock_guard * best_clk)
|
| 423 |
+
for ms in group:
|
| 424 |
+
flat.append(ms)
|
| 425 |
+
if window_ok:
|
| 426 |
+
kept.append(ms)
|
| 427 |
+
else:
|
| 428 |
+
rejected += 1
|
| 429 |
+
if not kept:
|
| 430 |
+
kept = flat
|
| 431 |
+
med = _median(kept)
|
| 432 |
+
rep = {
|
| 433 |
+
"median_ms": round(med, 4),
|
| 434 |
+
"min_ms": round(min(kept), 4),
|
| 435 |
+
"mean_ms": round(sum(kept) / len(kept), 4),
|
| 436 |
+
"samples": len(flat),
|
| 437 |
+
"rejected_throttled": rejected,
|
| 438 |
+
"graph_captured": bool(graph),
|
| 439 |
+
"clock_ghz": {"best": round(best_clk, 3),
|
| 440 |
+
"worst": round(min(clocks), 3)},
|
| 441 |
+
}
|
| 442 |
+
if bytes is not None:
|
| 443 |
+
rep["achieved_gbs"] = round(bytes / (med * 1e-3) / 1e9, 1)
|
| 444 |
+
if flops is not None:
|
| 445 |
+
rep["achieved_tflops"] = round(flops / (med * 1e-3) / 1e12, 2)
|
| 446 |
+
if bytes and flops:
|
| 447 |
+
rep["arithmetic_intensity"] = round(flops / bytes, 2)
|
| 448 |
+
if dossier is not None and bytes and flops:
|
| 449 |
+
bw = max(dossier["hbm_bw"]["value"].values()) * 1e9
|
| 450 |
+
mma_v = dossier["mma"]["value"].get(dtype)
|
| 451 |
+
peak = (mma_v if isinstance(mma_v, (int, float))
|
| 452 |
+
else dossier["fma_f32"]["value"]) * 1e12
|
| 453 |
+
ai = flops / bytes
|
| 454 |
+
roof = min(peak, ai * bw)
|
| 455 |
+
rep["roofline"] = {
|
| 456 |
+
"bound": "memory" if ai * bw < peak else "compute",
|
| 457 |
+
"ridge_flops_per_byte": round(peak / bw, 1),
|
| 458 |
+
"pct_of_roof": round(100 * (flops / (med * 1e-3)) / roof, 1),
|
| 459 |
+
"peak_dtype": dtype,
|
| 460 |
+
}
|
| 461 |
+
return rep
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
def _to_ordered_int(t):
|
| 465 |
+
"""Map float bit patterns to integers whose difference is the ULP
|
| 466 |
+
distance: positive floats keep their bits, negative floats mirror
|
| 467 |
+
below zero, so the mapping is monotone in the float value."""
|
| 468 |
+
if t.dtype == torch.float32:
|
| 469 |
+
u = t.view(torch.int32).to(torch.int64) & 0xFFFFFFFF
|
| 470 |
+
half = 1 << 31
|
| 471 |
+
elif t.dtype in (torch.float16, torch.bfloat16):
|
| 472 |
+
u = t.view(torch.int16).to(torch.int64) & 0xFFFF
|
| 473 |
+
half = 1 << 15
|
| 474 |
+
elif t.dtype == torch.float64:
|
| 475 |
+
i = t.view(torch.int64)
|
| 476 |
+
return torch.where(i >= 0, i, (-(1 << 63)) - i)
|
| 477 |
+
else:
|
| 478 |
+
raise ValueError(f"unsupported dtype {t.dtype}")
|
| 479 |
+
return torch.where(u < half, u, half - u)
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
def ulp_diff(a, b):
|
| 483 |
+
"""Elementwise ULP distance between same-dtype float tensors. NaNs in
|
| 484 |
+
the same position count as 0; a one-sided NaN counts as 2**62."""
|
| 485 |
+
assert a.dtype == b.dtype and a.shape == b.shape
|
| 486 |
+
ia, ib = _to_ordered_int(a), _to_ordered_int(b)
|
| 487 |
+
d = (ia - ib).abs()
|
| 488 |
+
both_nan = torch.isnan(a) & torch.isnan(b)
|
| 489 |
+
one_nan = torch.isnan(a) ^ torch.isnan(b)
|
| 490 |
+
d = torch.where(both_nan, torch.zeros_like(d), d)
|
| 491 |
+
d = torch.where(one_nan, torch.full_like(d, 1 << 62), d)
|
| 492 |
+
return d
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
class order_shuffles:
|
| 496 |
+
"""Built-in order shuffles for the common contractual-noise axes."""
|
| 497 |
+
|
| 498 |
+
@staticmethod
|
| 499 |
+
def last_dim():
|
| 500 |
+
"""Permute the last dim of every tensor arg identically: the
|
| 501 |
+
accumulation order of a last-dim reduction changes, the value
|
| 502 |
+
does not."""
|
| 503 |
+
def shuffle(args, seed):
|
| 504 |
+
g = torch.Generator(device=args[0].device).manual_seed(seed)
|
| 505 |
+
p = torch.randperm(args[0].shape[-1], device=args[0].device,
|
| 506 |
+
generator=g)
|
| 507 |
+
return tuple(x[..., p] for x in args)
|
| 508 |
+
return shuffle
|
| 509 |
+
|
| 510 |
+
@staticmethod
|
| 511 |
+
def matmul_k():
|
| 512 |
+
"""Permute K of (A [.., M, K], B [.., K, N]) consistently: the
|
| 513 |
+
product is unchanged, the accumulation order is not."""
|
| 514 |
+
def shuffle(args, seed):
|
| 515 |
+
a_m, b_m = args[0], args[1]
|
| 516 |
+
g = torch.Generator(device=a_m.device).manual_seed(seed)
|
| 517 |
+
p = torch.randperm(a_m.shape[-1], device=a_m.device,
|
| 518 |
+
generator=g)
|
| 519 |
+
return (a_m[..., p], b_m[..., p, :]) + tuple(args[2:])
|
| 520 |
+
return shuffle
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
def _as_outputs(x):
|
| 524 |
+
return list(x) if isinstance(x, (tuple, list)) else [x]
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
def compare(ref, cand, gen, trials=8, order_shuffle=None, order_trials=8,
|
| 528 |
+
ulp_tol=None, atol=None, rtol=None, band_margin=4.0, seed=0):
|
| 529 |
+
"""Adversarial correctness comparison with ULP evidence.
|
| 530 |
+
|
| 531 |
+
`gen(seed) -> args` builds inputs; `ref(*args)` and `cand(*args)`
|
| 532 |
+
return a tensor or a tuple of tensors (each output is compared and
|
| 533 |
+
the worst governs). If `order_shuffle(args, seed) -> args` is given
|
| 534 |
+
and reorders contractual-noise dimensions (see `order_shuffles` for
|
| 535 |
+
the common cases), the reference is treated as a distribution:
|
| 536 |
+
candidates are judged against the measured reduction-order band
|
| 537 |
+
rather than a point. Verdict, stated in the report: max ULP <=
|
| 538 |
+
band_margin * band + 2 when a band is measured, else max ULP <=
|
| 539 |
+
ulp_tol (default 2); if `atol`/`rtol` are given they are an
|
| 540 |
+
alternative acceptance route, checked as |c - r| <= atol + rtol|r|."""
|
| 541 |
+
worst = {"max_ulp": -1}
|
| 542 |
+
total_hist = {}
|
| 543 |
+
per_trial = []
|
| 544 |
+
max_abs_seen = 0.0
|
| 545 |
+
max_rel_seen = 0.0
|
| 546 |
+
for t in range(trials):
|
| 547 |
+
args = gen(seed + t)
|
| 548 |
+
r_outs = _as_outputs(ref(*args))
|
| 549 |
+
c_outs = _as_outputs(cand(*args))
|
| 550 |
+
assert len(r_outs) == len(c_outs), "output arity mismatch"
|
| 551 |
+
# the order band is POINTWISE: a near-zero reduction output is
|
| 552 |
+
# legitimately thousands of ULP order-sensitive (cancellation)
|
| 553 |
+
# while its neighbors are stable, so each element is judged
|
| 554 |
+
# against its own measured sensitivity, not a global max
|
| 555 |
+
bands = None
|
| 556 |
+
band = 0
|
| 557 |
+
band_score = 0.0
|
| 558 |
+
if order_shuffle is not None:
|
| 559 |
+
bands = [torch.zeros_like(ro, dtype=torch.int64)
|
| 560 |
+
for ro in r_outs]
|
| 561 |
+
for k in range(order_trials):
|
| 562 |
+
r2 = _as_outputs(
|
| 563 |
+
ref(*order_shuffle(args, 1000 + seed + t * 131 + k)))
|
| 564 |
+
for bi, (ro, r2o) in enumerate(zip(r_outs, r2)):
|
| 565 |
+
bands[bi] = torch.maximum(bands[bi],
|
| 566 |
+
ulp_diff(ro, r2o))
|
| 567 |
+
band = max(int(b.max().item()) if b.numel() else 0
|
| 568 |
+
for b in bands)
|
| 569 |
+
max_ulp = 0
|
| 570 |
+
mean_ulp = 0.0
|
| 571 |
+
first_div = -1
|
| 572 |
+
n_el = 0
|
| 573 |
+
nan_r = nan_c = inf_r = inf_c = 0
|
| 574 |
+
for oi, (r, c) in enumerate(zip(r_outs, c_outs)):
|
| 575 |
+
d = ulp_diff(r, c)
|
| 576 |
+
if bands is not None and d.numel():
|
| 577 |
+
ratio = d.double() / (bands[oi].double() + 2.0)
|
| 578 |
+
band_score = max(band_score, float(ratio.max().item()))
|
| 579 |
+
if d.numel():
|
| 580 |
+
max_ulp = max(max_ulp, int(d.max().item()))
|
| 581 |
+
mean_ulp += float(d.double().sum().item())
|
| 582 |
+
n_el += d.numel()
|
| 583 |
+
nz = (d > 0)
|
| 584 |
+
if first_div < 0 and nz.any():
|
| 585 |
+
first_div = int(nz.flatten().nonzero()[0].item())
|
| 586 |
+
if nz.any():
|
| 587 |
+
buckets = torch.clamp(
|
| 588 |
+
torch.log2(d[nz].double()).floor(), 0, 62
|
| 589 |
+
).to(torch.int64)
|
| 590 |
+
counts = torch.bincount(buckets, minlength=63)
|
| 591 |
+
for bkt in counts.nonzero().flatten().tolist():
|
| 592 |
+
total_hist[bkt] = total_hist.get(bkt, 0) + int(
|
| 593 |
+
counts[bkt].item())
|
| 594 |
+
fin = torch.isfinite(r) & torch.isfinite(c)
|
| 595 |
+
if fin.any():
|
| 596 |
+
diff = (c[fin].double() - r[fin].double()).abs()
|
| 597 |
+
max_abs_seen = max(max_abs_seen,
|
| 598 |
+
float(diff.max().item()))
|
| 599 |
+
rel = diff / r[fin].double().abs().clamp(min=1e-300)
|
| 600 |
+
max_rel_seen = max(max_rel_seen,
|
| 601 |
+
float(rel.max().item()))
|
| 602 |
+
nan_r += int(torch.isnan(r).sum().item())
|
| 603 |
+
nan_c += int(torch.isnan(c).sum().item())
|
| 604 |
+
inf_r += int(torch.isinf(r).sum().item())
|
| 605 |
+
inf_c += int(torch.isinf(c).sum().item())
|
| 606 |
+
trial_rep = {
|
| 607 |
+
"max_ulp": max_ulp,
|
| 608 |
+
"mean_ulp": round(mean_ulp / max(n_el, 1), 3),
|
| 609 |
+
"first_divergent_index": first_div,
|
| 610 |
+
"nan_ref": nan_r, "nan_cand": nan_c,
|
| 611 |
+
"inf_ref": inf_r, "inf_cand": inf_c,
|
| 612 |
+
"order_band_ulp": band,
|
| 613 |
+
"band_score": round(band_score, 2),
|
| 614 |
+
}
|
| 615 |
+
per_trial.append(trial_rep)
|
| 616 |
+
if max_ulp > worst["max_ulp"]:
|
| 617 |
+
worst = trial_rep
|
| 618 |
+
band_max = max(t["order_band_ulp"] for t in per_trial)
|
| 619 |
+
max_seen = max(t["max_ulp"] for t in per_trial)
|
| 620 |
+
if order_shuffle is not None:
|
| 621 |
+
score = max(t["band_score"] for t in per_trial)
|
| 622 |
+
ulp_ok = score <= band_margin
|
| 623 |
+
criterion = (f"pointwise max(ulp / (order_band + 2)) = "
|
| 624 |
+
f"{score:.2f} <= band_margin {band_margin}")
|
| 625 |
+
else:
|
| 626 |
+
tol = ulp_tol if ulp_tol is not None else 2
|
| 627 |
+
ulp_ok = max_seen <= tol
|
| 628 |
+
criterion = f"max_ulp {max_seen} <= ulp_tol {tol}"
|
| 629 |
+
tol_ok = False
|
| 630 |
+
if atol is not None or rtol is not None:
|
| 631 |
+
tol_ok = max_abs_seen <= (atol or 0) + (rtol or 0) * 1.0 or \
|
| 632 |
+
max_rel_seen <= (rtol or 0) or max_abs_seen <= (atol or 0)
|
| 633 |
+
criterion += (f"; OR max_abs {max_abs_seen:.3e} <= atol {atol} / "
|
| 634 |
+
f"max_rel {max_rel_seen:.3e} <= rtol {rtol}")
|
| 635 |
+
return {
|
| 636 |
+
"pass": bool(ulp_ok or tol_ok),
|
| 637 |
+
"criterion": criterion,
|
| 638 |
+
"worst_trial": worst,
|
| 639 |
+
"max_abs": max_abs_seen,
|
| 640 |
+
"max_rel": max_rel_seen,
|
| 641 |
+
"trials": per_trial,
|
| 642 |
+
"ulp_hist_log2": dict(sorted(total_hist.items())),
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
|
| 646 |
+
_FUZZ_VARIANTS = ("contig", "transposed", "strided_rows", "offset_slice",
|
| 647 |
+
"overflow_expand")
|
| 648 |
+
|
| 649 |
+
|
| 650 |
+
def _context_alive(device):
|
| 651 |
+
try:
|
| 652 |
+
t = torch.ones(4, device=device)
|
| 653 |
+
torch.cuda.synchronize()
|
| 654 |
+
return bool(t.sum().item() == 4.0)
|
| 655 |
+
except Exception: # noqa: BLE001
|
| 656 |
+
return False
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
def fuzz_shapes(op, ref, make, shapes=None, variants=_FUZZ_VARIANTS,
|
| 660 |
+
dtypes=(torch.float32,), ulp_tol=64, device="cuda"):
|
| 661 |
+
"""The ugly envelope: run op vs ref across edge shapes, dtypes, and
|
| 662 |
+
layout mutations; report ok / mismatch / exception per case.
|
| 663 |
+
|
| 664 |
+
`make(shape, dtype)` (or `make(shape)`) builds a base input, either a
|
| 665 |
+
tensor or a tuple of tensors for multi-argument ops; layout variants
|
| 666 |
+
derive non-contiguous and misaligned views whose CONTENT equals a
|
| 667 |
+
contiguous copy, so `op(*xs)` must match `ref(*xs)` on every variant.
|
| 668 |
+
`overflow_expand` stride-0-expands the input past 2**31 logical
|
| 669 |
+
elements to catch int32 indexing, and is skipped with status
|
| 670 |
+
`skipped_vram` when materializing the output would not fit. After any
|
| 671 |
+
exception the CUDA context is health-checked; if a candidate killed
|
| 672 |
+
it, remaining cases are marked `context_lost` instead of producing
|
| 673 |
+
garbage."""
|
| 674 |
+
if shapes is None:
|
| 675 |
+
shapes = [(0, 8), (1, 1), (1, 17), (3, 63), (17, 129), (128, 128),
|
| 676 |
+
(1023, 5), (2, 4096)]
|
| 677 |
+
|
| 678 |
+
def build(shape, dtype):
|
| 679 |
+
try:
|
| 680 |
+
out = make(tuple(shape), dtype)
|
| 681 |
+
except TypeError:
|
| 682 |
+
out = make(tuple(shape))
|
| 683 |
+
return out if isinstance(out, (tuple, list)) else (out,)
|
| 684 |
+
|
| 685 |
+
def variant_of(x, v):
|
| 686 |
+
if v == "contig":
|
| 687 |
+
return x
|
| 688 |
+
if v == "transposed":
|
| 689 |
+
return x.t() if x.dim() == 2 else x
|
| 690 |
+
if v == "strided_rows":
|
| 691 |
+
return x[::2] if x.shape[0] >= 2 else x
|
| 692 |
+
if v == "offset_slice":
|
| 693 |
+
# a storage-offset-1 view: misaligned for vectorized loads
|
| 694 |
+
return x.flatten()[1:] if x.numel() > 1 else x
|
| 695 |
+
if v == "overflow_expand":
|
| 696 |
+
if x.numel() == 0:
|
| 697 |
+
return None
|
| 698 |
+
per = max(x.numel(), 1)
|
| 699 |
+
rep = (1 << 31) // per + 2
|
| 700 |
+
return x.reshape((1,) + tuple(x.shape)).expand(
|
| 701 |
+
(rep,) + tuple(x.shape))
|
| 702 |
+
return x
|
| 703 |
+
|
| 704 |
+
rows = []
|
| 705 |
+
lost = False
|
| 706 |
+
for dtype in dtypes:
|
| 707 |
+
for shape in shapes:
|
| 708 |
+
bases = build(shape, dtype)
|
| 709 |
+
for v in variants:
|
| 710 |
+
case = {"shape": tuple(shape), "variant": v,
|
| 711 |
+
"dtype": str(dtype).replace("torch.", "")}
|
| 712 |
+
if lost:
|
| 713 |
+
case["status"] = "context_lost"
|
| 714 |
+
rows.append(case)
|
| 715 |
+
continue
|
| 716 |
+
xs = [variant_of(b, v) for b in bases]
|
| 717 |
+
if any(x is None for x in xs):
|
| 718 |
+
case["status"] = "skipped_empty"
|
| 719 |
+
rows.append(case)
|
| 720 |
+
continue
|
| 721 |
+
if v == "overflow_expand":
|
| 722 |
+
logical = max(x.numel() for x in xs)
|
| 723 |
+
need = logical * xs[0].element_size() * 2
|
| 724 |
+
free, _total = torch.cuda.mem_get_info()
|
| 725 |
+
if need > free * 0.8:
|
| 726 |
+
case["status"] = "skipped_vram"
|
| 727 |
+
case["logical_elems"] = int(logical)
|
| 728 |
+
rows.append(case)
|
| 729 |
+
continue
|
| 730 |
+
case["logical_elems"] = int(logical)
|
| 731 |
+
case["contiguous"] = bool(xs[0].is_contiguous())
|
| 732 |
+
try:
|
| 733 |
+
got = op(*xs)
|
| 734 |
+
want = ref(*xs)
|
| 735 |
+
if v == "overflow_expand":
|
| 736 |
+
# bounded comparison: the head plus the high-index
|
| 737 |
+
# tail, which is where int32 flat indexing wraps
|
| 738 |
+
w = 1 << 20
|
| 739 |
+
gf, wf = got.flatten(), want.flatten()
|
| 740 |
+
pairs = ((gf[:w], wf[:w]), (gf[-w:], wf[-w:]))
|
| 741 |
+
else:
|
| 742 |
+
pairs = ((got, want),)
|
| 743 |
+
mx = 0
|
| 744 |
+
for gp, wp in pairs:
|
| 745 |
+
d = ulp_diff(wp, gp) if gp.is_floating_point() \
|
| 746 |
+
else (wp != gp).to(torch.int64) * (1 << 62)
|
| 747 |
+
if d.numel():
|
| 748 |
+
mx = max(mx, int(d.max().item()))
|
| 749 |
+
del d
|
| 750 |
+
case["status"] = "ok" if mx <= ulp_tol else "mismatch"
|
| 751 |
+
case["max_ulp"] = mx
|
| 752 |
+
del got, want
|
| 753 |
+
except Exception as e: # noqa: BLE001 - report, don't die
|
| 754 |
+
case["status"] = "exception"
|
| 755 |
+
case["error"] = f"{type(e).__name__}: {str(e)[:120]}"
|
| 756 |
+
if not _context_alive(device):
|
| 757 |
+
lost = True
|
| 758 |
+
case["status"] = "context_killed"
|
| 759 |
+
rows.append(case)
|
| 760 |
+
n_bad = sum(1 for r in rows
|
| 761 |
+
if r["status"] not in ("ok", "skipped_vram",
|
| 762 |
+
"skipped_empty"))
|
| 763 |
+
return {"cases": rows, "failed": n_bad, "total": len(rows),
|
| 764 |
+
"context_lost": lost}
|
| 765 |
+
|
| 766 |
+
|
| 767 |
+
def sweep(factory, grid, iters=30, warmup=5, bytes=None, flops=None,
|
| 768 |
+
check=None, budget_s=None):
|
| 769 |
+
"""Launch-configuration search: `factory(**point) -> callable`; one
|
| 770 |
+
bench row per grid point, sorted fastest first. A point whose factory
|
| 771 |
+
or run raises is reported, not skipped silently. `check(fn) -> bool`
|
| 772 |
+
is an optional correctness gate recorded per row (a fast wrong config
|
| 773 |
+
must not win a sweep). `budget_s` caps total sweep time; points not
|
| 774 |
+
reached are reported as `not_run` rather than silently dropped."""
|
| 775 |
+
keys = list(grid.keys())
|
| 776 |
+
|
| 777 |
+
def points(i, acc):
|
| 778 |
+
if i == len(keys):
|
| 779 |
+
yield dict(acc)
|
| 780 |
+
return
|
| 781 |
+
for v in grid[keys[i]]:
|
| 782 |
+
acc[keys[i]] = v
|
| 783 |
+
yield from points(i + 1, acc)
|
| 784 |
+
|
| 785 |
+
rows = []
|
| 786 |
+
t_start = time.perf_counter()
|
| 787 |
+
over_budget = False
|
| 788 |
+
for p in points(0, {}):
|
| 789 |
+
row = dict(p)
|
| 790 |
+
if over_budget or (budget_s is not None and
|
| 791 |
+
time.perf_counter() - t_start > budget_s):
|
| 792 |
+
over_budget = True
|
| 793 |
+
row["status"] = "not_run"
|
| 794 |
+
row["median_ms"] = float("inf")
|
| 795 |
+
rows.append(row)
|
| 796 |
+
continue
|
| 797 |
+
try:
|
| 798 |
+
fn = factory(**p)
|
| 799 |
+
if check is not None:
|
| 800 |
+
row["check"] = bool(check(fn))
|
| 801 |
+
rep = bench(fn, iters=iters, warmup=warmup, bytes=bytes,
|
| 802 |
+
flops=flops)
|
| 803 |
+
row.update({"median_ms": rep["median_ms"],
|
| 804 |
+
"rejected_throttled": rep["rejected_throttled"]})
|
| 805 |
+
if "achieved_gbs" in rep:
|
| 806 |
+
row["achieved_gbs"] = rep["achieved_gbs"]
|
| 807 |
+
if "achieved_tflops" in rep:
|
| 808 |
+
row["achieved_tflops"] = rep["achieved_tflops"]
|
| 809 |
+
except Exception as e: # noqa: BLE001
|
| 810 |
+
row["error"] = f"{type(e).__name__}: {str(e)[:120]}"
|
| 811 |
+
row["median_ms"] = float("inf")
|
| 812 |
+
rows.append(row)
|
| 813 |
+
rows.sort(key=lambda r: (not r.get("check", True), r["median_ms"]))
|
| 814 |
+
return rows
|
| 815 |
+
|
| 816 |
+
|
| 817 |
+
def alloc_stamps(n, device="cuda"):
|
| 818 |
+
"""Device buffer for the MK_STAMP convention (metakernel_cuda/
|
| 819 |
+
stamps.cuh): the candidate kernel writes clock64() into slot i."""
|
| 820 |
+
return torch.zeros(n, dtype=torch.int64, device=device)
|
| 821 |
+
|
| 822 |
+
|
| 823 |
+
def read_stamps(stamps, labels=None, clock_ghz=None):
|
| 824 |
+
"""Decode MK_STAMP phases: consecutive deltas in ticks and in
|
| 825 |
+
microseconds under the measured (or provided) SM clock. A 1-D buffer
|
| 826 |
+
(MK_STAMP: block 0 only) yields one delta per phase; a 2-D [B, n]
|
| 827 |
+
buffer (MK_STAMP_BLOCK) yields min / median / max across blocks per
|
| 828 |
+
phase, with blocks whose stamps stayed zero excluded."""
|
| 829 |
+
if clock_ghz is None:
|
| 830 |
+
clock_ghz = measure_clock(stamps.device)
|
| 831 |
+
|
| 832 |
+
def us(ticks):
|
| 833 |
+
return round(ticks / clock_ghz / 1000.0, 3)
|
| 834 |
+
|
| 835 |
+
if stamps.dim() == 1:
|
| 836 |
+
vals = stamps.cpu().tolist()
|
| 837 |
+
phases = []
|
| 838 |
+
for i in range(len(vals) - 1):
|
| 839 |
+
ticks = vals[i + 1] - vals[i]
|
| 840 |
+
name = labels[i] if labels and i < len(labels) else f"phase_{i}"
|
| 841 |
+
phases.append({"phase": name, "ticks": ticks, "us": us(ticks)})
|
| 842 |
+
return {"phases": phases, "clock_ghz": round(clock_ghz, 3)}
|
| 843 |
+
|
| 844 |
+
m = stamps.cpu()
|
| 845 |
+
live = (m != 0).all(dim=1)
|
| 846 |
+
m = m[live]
|
| 847 |
+
deltas = (m[:, 1:] - m[:, :-1]).to(torch.float64)
|
| 848 |
+
phases = []
|
| 849 |
+
for i in range(deltas.shape[1]):
|
| 850 |
+
col = deltas[:, i]
|
| 851 |
+
name = labels[i] if labels and i < len(labels) else f"phase_{i}"
|
| 852 |
+
med = float(col.median().item()) if col.numel() else 0.0
|
| 853 |
+
phases.append({
|
| 854 |
+
"phase": name,
|
| 855 |
+
"ticks_median": int(med),
|
| 856 |
+
"ticks_min": int(col.min().item()) if col.numel() else 0,
|
| 857 |
+
"ticks_max": int(col.max().item()) if col.numel() else 0,
|
| 858 |
+
"us_median": us(med),
|
| 859 |
+
"blocks": int(col.numel()),
|
| 860 |
+
})
|
| 861 |
+
return {"phases": phases, "clock_ghz": round(clock_ghz, 3),
|
| 862 |
+
"blocks_reporting": int(live.sum().item()),
|
| 863 |
+
"blocks_total": int(live.numel())}
|
build/torch212-cxx11-cu130-x86_64-linux/_metakernel_cuda_4611d12.abi3.so
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c1808009535a071b8ee0439dafe12d3b6a84dc9be242f5c700957cffb9b26670
|
| 3 |
+
size 967576
|
build/torch212-cxx11-cu130-x86_64-linux/_ops.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from . import _metakernel_cuda_4611d12
|
| 3 |
+
ops = torch.ops._metakernel_cuda_4611d12
|
| 4 |
+
|
| 5 |
+
def add_op_namespace_prefix(op_name: str):
|
| 6 |
+
"""
|
| 7 |
+
Prefix op by namespace.
|
| 8 |
+
"""
|
| 9 |
+
return f"_metakernel_cuda_4611d12::{op_name}"
|
build/torch212-cxx11-cu130-x86_64-linux/metadata.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "metakernel",
|
| 3 |
+
"id": "_metakernel_cuda_4611d12",
|
| 4 |
+
"version": 1,
|
| 5 |
+
"license": "Apache-2.0",
|
| 6 |
+
"python-depends": [],
|
| 7 |
+
"backend": {
|
| 8 |
+
"type": "cuda",
|
| 9 |
+
"archs": [
|
| 10 |
+
"10.0",
|
| 11 |
+
"12.0",
|
| 12 |
+
"8.0",
|
| 13 |
+
"8.6",
|
| 14 |
+
"8.9",
|
| 15 |
+
"9.0"
|
| 16 |
+
]
|
| 17 |
+
},
|
| 18 |
+
"digest": {
|
| 19 |
+
"algorithm": "sha256",
|
| 20 |
+
"files": {
|
| 21 |
+
"__init__.py": "cZxKX5I/2EKCMC2gdCwflUsZaQJWJesmAHq2w+fVjXo=",
|
| 22 |
+
"_metakernel_cuda_4611d12.abi3.so": "wYCACVNaBxuO4EOdr+EtO2qE3JviQvXHAJV8/7myZnA=",
|
| 23 |
+
"_ops.py": "uZlR720EcTy8M7BCydmvaZAlKonkwTU3778nhR9yQOE=",
|
| 24 |
+
"metakernel/__init__.py": "DFYPlrhXwYjEqCl/8n0SmWGZV8NFml5DPhMjKfv98GY="
|
| 25 |
+
}
|
| 26 |
+
},
|
| 27 |
+
"provenance": {
|
| 28 |
+
"kernel-builder": {
|
| 29 |
+
"version": "0.17.0-dev0",
|
| 30 |
+
"sha": "19aaa6421e674e9fecc352bbae6eab81d19a6bf4",
|
| 31 |
+
"dirty": false
|
| 32 |
+
},
|
| 33 |
+
"kernel": {
|
| 34 |
+
"sha": "4611d1213a76a9ec1d12646553a528feb60394ff",
|
| 35 |
+
"dirty": false
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
}
|
build/torch212-cxx11-cu130-x86_64-linux/metakernel/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ctypes
|
| 2 |
+
import importlib.util
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from types import ModuleType
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def _import_from_path(file_path: Path) -> ModuleType:
|
| 9 |
+
# We cannot use the module name as-is, after adding it to `sys.modules`,
|
| 10 |
+
# it would also be used for other imports. So, we make a module name that
|
| 11 |
+
# depends on the path for it to be unique using the hex-encoded hash of
|
| 12 |
+
# the path.
|
| 13 |
+
path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
|
| 14 |
+
module_name = path_hash
|
| 15 |
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
| 16 |
+
if spec is None:
|
| 17 |
+
raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
|
| 18 |
+
module = importlib.util.module_from_spec(spec)
|
| 19 |
+
if module is None:
|
| 20 |
+
raise ImportError(f"Cannot load module {module_name} from spec")
|
| 21 |
+
sys.modules[module_name] = module
|
| 22 |
+
spec.loader.exec_module(module) # type: ignore
|
| 23 |
+
return module
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
|
build/torch212-cxx11-cu132-x86_64-linux/__init__.py
ADDED
|
@@ -0,0 +1,863 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""metakernel: the measuring instrument for the kernel-design loop.
|
| 2 |
+
|
| 3 |
+
An agent iterating on CUDA candidates is good at writing code and bad at
|
| 4 |
+
trusting its own measurements; it needs trustworthy timing, adversarial
|
| 5 |
+
correctness comparison, and measured facts about the device it targets.
|
| 6 |
+
metakernel ships those as loadable ops:
|
| 7 |
+
|
| 8 |
+
from kernels import get_kernel
|
| 9 |
+
mk = get_kernel("phanerozoic/metakernel", version=1,
|
| 10 |
+
trust_remote_code=True)
|
| 11 |
+
|
| 12 |
+
dossier = mk.probe_device() # measured facts
|
| 13 |
+
report = mk.bench(lambda: op(x), iters=200) # throttle-rejected
|
| 14 |
+
verdict = mk.compare(ref, cand, gen) # ULP evidence
|
| 15 |
+
table = mk.sweep(factory, {"BLOCK": [64, 128]}) # config search
|
| 16 |
+
|
| 17 |
+
Every op returns plain dicts and tensors, never prose. Every dossier entry
|
| 18 |
+
is produced by a dedicated probe kernel rather than a spec sheet, carries
|
| 19 |
+
the SM clock it was measured under, and labels throttled samples instead
|
| 20 |
+
of letting them poison medians. No hardware counters are used: everything
|
| 21 |
+
is inferred from timing, clocks, and the occupancy API, so entries carry a
|
| 22 |
+
confidence field (timing-differential inference can misattribute).
|
| 23 |
+
"""
|
| 24 |
+
import math
|
| 25 |
+
import time
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
|
| 29 |
+
from ._ops import ops
|
| 30 |
+
|
| 31 |
+
__all__ = ["probe_device", "bench", "compare", "order_shuffles",
|
| 32 |
+
"fuzz_shapes", "sweep", "alloc_stamps", "read_stamps",
|
| 33 |
+
"measure_clock", "ulp_diff", "ops"]
|
| 34 |
+
|
| 35 |
+
_MMA_KINDS = {
|
| 36 |
+
"fp16": (0, 16 * 16 * 16),
|
| 37 |
+
"bf16": (1, 16 * 16 * 16),
|
| 38 |
+
"tf32": (2, 16 * 16 * 8),
|
| 39 |
+
"int8": (3, 16 * 16 * 16),
|
| 40 |
+
"fp8_e4m3": (4, 16 * 8 * 32),
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _sync():
|
| 45 |
+
torch.cuda.synchronize()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _events(fn, reps):
|
| 49 |
+
"""Time reps calls of fn with one event pair each; returns ms list."""
|
| 50 |
+
starts = [torch.cuda.Event(enable_timing=True) for _ in range(reps)]
|
| 51 |
+
stops = [torch.cuda.Event(enable_timing=True) for _ in range(reps)]
|
| 52 |
+
for s, e in zip(starts, stops):
|
| 53 |
+
s.record()
|
| 54 |
+
fn()
|
| 55 |
+
e.record()
|
| 56 |
+
_sync()
|
| 57 |
+
return [s.elapsed_time(e) for s, e in zip(starts, stops)]
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def measure_clock(device="cuda", ticks=4_000_000, blocks=None):
|
| 61 |
+
"""Effective SM clock in GHz under an FMA spin of `ticks` clock64 ticks.
|
| 62 |
+
Wall time over elapsed ticks is the frequency the ticks ran at."""
|
| 63 |
+
props = torch.cuda.get_device_properties(device)
|
| 64 |
+
if blocks is None:
|
| 65 |
+
blocks = props.multi_processor_count
|
| 66 |
+
out = torch.zeros(2, dtype=torch.int64, device=device)
|
| 67 |
+
sink = torch.zeros(2, dtype=torch.float32, device=device)
|
| 68 |
+
ops.mk_spin(blocks, 256, ticks, out, sink) # warm
|
| 69 |
+
start = torch.cuda.Event(enable_timing=True)
|
| 70 |
+
stop = torch.cuda.Event(enable_timing=True)
|
| 71 |
+
start.record()
|
| 72 |
+
ops.mk_spin(blocks, 256, ticks, out, sink)
|
| 73 |
+
stop.record()
|
| 74 |
+
_sync()
|
| 75 |
+
ms = start.elapsed_time(stop)
|
| 76 |
+
elapsed = int(out[0].item())
|
| 77 |
+
return elapsed / (ms * 1e6)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _median(xs):
|
| 81 |
+
s = sorted(xs)
|
| 82 |
+
n = len(s)
|
| 83 |
+
return s[n // 2] if n % 2 else 0.5 * (s[n // 2 - 1] + s[n // 2])
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _entry(value, units, method, clock_ghz, confidence, note=""):
|
| 87 |
+
d = {"value": value, "units": units, "method": method,
|
| 88 |
+
"clock_ghz": round(clock_ghz, 3), "confidence": confidence}
|
| 89 |
+
if note:
|
| 90 |
+
d["note"] = note
|
| 91 |
+
return d
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _sattolo_ring(n, device, generator=None):
|
| 95 |
+
"""A random single-cycle permutation as an int32 next-pointer table, so
|
| 96 |
+
a pointer chase visits every slot before repeating."""
|
| 97 |
+
perm = torch.randperm(n, generator=generator)
|
| 98 |
+
ring = torch.empty(n, dtype=torch.int32)
|
| 99 |
+
ring[perm] = torch.roll(perm, -1).to(torch.int32)
|
| 100 |
+
return ring.to(device)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def probe_device(device="cuda", quick=False):
|
| 104 |
+
"""Empirical device dossier. Each entry is measured by a dedicated
|
| 105 |
+
probe kernel and carries units, method, the SM clock it ran under, and
|
| 106 |
+
a confidence field."""
|
| 107 |
+
props = torch.cuda.get_device_properties(device)
|
| 108 |
+
sm = props.multi_processor_count
|
| 109 |
+
l2 = props.L2_cache_size
|
| 110 |
+
d = {
|
| 111 |
+
"device": {
|
| 112 |
+
"name": props.name,
|
| 113 |
+
"sm_arch": f"sm{props.major}{props.minor}",
|
| 114 |
+
"sms": sm,
|
| 115 |
+
"l2_bytes": l2,
|
| 116 |
+
"vram_bytes": props.total_memory,
|
| 117 |
+
"torch": torch.__version__,
|
| 118 |
+
"cuda_runtime": torch.version.cuda,
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
sink = torch.zeros(2, dtype=torch.float32, device=device)
|
| 122 |
+
out64 = torch.zeros(2, dtype=torch.int64, device=device)
|
| 123 |
+
reps = 3 if quick else 5
|
| 124 |
+
|
| 125 |
+
# ---- clock under sustained load
|
| 126 |
+
traj = []
|
| 127 |
+
windows = 8 if quick else 24
|
| 128 |
+
for _ in range(windows):
|
| 129 |
+
traj.append(measure_clock(device, ticks=30_000_000))
|
| 130 |
+
d["clock"] = {
|
| 131 |
+
"max_ghz": round(max(traj), 3),
|
| 132 |
+
"min_ghz": round(min(traj), 3),
|
| 133 |
+
"final_ghz": round(traj[-1], 3),
|
| 134 |
+
"sag_pct": round(100 * (1 - traj[-1] / max(traj)), 1),
|
| 135 |
+
"units": "GHz",
|
| 136 |
+
"method": f"{windows} back-to-back FMA spin windows of 30M ticks; "
|
| 137 |
+
"wall/ticks per window",
|
| 138 |
+
"confidence": "high",
|
| 139 |
+
}
|
| 140 |
+
clk = max(traj)
|
| 141 |
+
|
| 142 |
+
# ---- HBM bandwidth: triad per access width, plus read-only,
|
| 143 |
+
# write-only, and random-gather
|
| 144 |
+
n4 = 16 << 20 # 16M float4 = 256 MB per buffer
|
| 145 |
+
if props.total_memory < 4 << 30:
|
| 146 |
+
n4 = 4 << 20
|
| 147 |
+
a = torch.rand(n4 * 4, device=device)
|
| 148 |
+
b = torch.rand(n4 * 4, device=device)
|
| 149 |
+
c = torch.empty(n4 * 4, device=device)
|
| 150 |
+
hbm = {}
|
| 151 |
+
for width in (1, 2, 4):
|
| 152 |
+
ops.mk_triad(a, b, c, width, 1.5) # warm
|
| 153 |
+
ms = _median(_events(lambda w=width: ops.mk_triad(a, b, c, w, 1.5),
|
| 154 |
+
reps))
|
| 155 |
+
gbs = 3 * a.numel() * 4 / (ms * 1e-3) / 1e9
|
| 156 |
+
hbm[f"triad_{4 * width}B"] = round(gbs, 1)
|
| 157 |
+
ops.mk_read(a, sink)
|
| 158 |
+
ms = _median(_events(lambda: ops.mk_read(a, sink), reps))
|
| 159 |
+
hbm["read_only"] = round(a.numel() * 4 / (ms * 1e-3) / 1e9, 1)
|
| 160 |
+
ops.mk_write(c, 1.0)
|
| 161 |
+
ms = _median(_events(lambda: ops.mk_write(c, 1.0), reps))
|
| 162 |
+
hbm["write_only"] = round(c.numel() * 4 / (ms * 1e-3) / 1e9, 1)
|
| 163 |
+
g = torch.Generator(device="cpu").manual_seed(7)
|
| 164 |
+
idx = torch.randint(0, n4, (n4,), generator=g,
|
| 165 |
+
dtype=torch.int32).to(device)
|
| 166 |
+
ops.mk_gather(a, idx, c, 256, 0)
|
| 167 |
+
ms = _median(_events(lambda: ops.mk_gather(a, idx, c, 256, 0), reps))
|
| 168 |
+
# traffic accounting: idx reads + 16B random loads + streamed stores
|
| 169 |
+
hbm["random_gather_16B"] = round(
|
| 170 |
+
(idx.numel() * 4 + 2 * n4 * 16) / (ms * 1e-3) / 1e9, 1)
|
| 171 |
+
d["hbm_bw"] = _entry(hbm, "GB/s", "streaming triad c=a+s*b per access "
|
| 172 |
+
"width; read-only fold; write-only fill; random "
|
| 173 |
+
"float4 gather (16B granules, nominal traffic)",
|
| 174 |
+
clk, "high",
|
| 175 |
+
"gather moves full cache lines per 16B granule, "
|
| 176 |
+
"so its nominal number is the achievable rate for "
|
| 177 |
+
"random access, not a line-efficiency claim")
|
| 178 |
+
|
| 179 |
+
# ---- L2 bandwidth (working set 3 buffers inside L2)
|
| 180 |
+
ws = max(l2 // 4, 1 << 20)
|
| 181 |
+
nl2 = ws // 16 # float4 elems per buffer
|
| 182 |
+
al = a[:nl2 * 4].contiguous()
|
| 183 |
+
bl = b[:nl2 * 4].contiguous()
|
| 184 |
+
cl = c[:nl2 * 4].contiguous()
|
| 185 |
+
passes = 64
|
| 186 |
+
ops.mk_triad_passes(al, bl, cl, 1.5, passes)
|
| 187 |
+
ms = _median(_events(
|
| 188 |
+
lambda: ops.mk_triad_passes(al, bl, cl, 1.5, passes), reps))
|
| 189 |
+
gbs = 3 * al.numel() * 4 * passes / (ms * 1e-3) / 1e9
|
| 190 |
+
d["l2_bw"] = _entry(round(gbs, 1), "GB/s",
|
| 191 |
+
f"in-kernel repeated triad passes over "
|
| 192 |
+
f"{3 * ws >> 20} MB resident working set", clk,
|
| 193 |
+
"medium", "first pass streams from HBM; amortized "
|
| 194 |
+
f"over {passes} passes")
|
| 195 |
+
|
| 196 |
+
# ---- latencies: pointer chase per level. Footprints are chosen so
|
| 197 |
+
# the chain hits the intended level: the L2 ring (2 MB) exceeds L1,
|
| 198 |
+
# fits L2 on every supported part, and spans few enough pages to stay
|
| 199 |
+
# TLB-resident after a full-cycle warm walk; the HBM chain is long
|
| 200 |
+
# enough (2x L2 in touched lines) that it evicts itself and cannot be
|
| 201 |
+
# served from cache.
|
| 202 |
+
lat = {}
|
| 203 |
+
idle_clk = measure_clock(device, ticks=2_000_000, blocks=1)
|
| 204 |
+
ring_s = _sattolo_ring(8192, device)
|
| 205 |
+
ops.mk_chase_shared(ring_s, 1 << 14, out64)
|
| 206 |
+
_sync()
|
| 207 |
+
hops = 1 << 16
|
| 208 |
+
ops.mk_chase_shared(ring_s, hops, out64)
|
| 209 |
+
_sync()
|
| 210 |
+
lat["smem_cycles"] = round(int(out64[0].item()) / hops, 1)
|
| 211 |
+
|
| 212 |
+
l1_ring_n = (16 << 10) // 4 # 16 KB: L1-resident after a warm walk
|
| 213 |
+
ring_l1 = _sattolo_ring(l1_ring_n, device)
|
| 214 |
+
ops.mk_chase_global(ring_l1, l1_ring_n, out64)
|
| 215 |
+
_sync()
|
| 216 |
+
hops_l1 = 1 << 15
|
| 217 |
+
ops.mk_chase_global(ring_l1, hops_l1, out64)
|
| 218 |
+
_sync()
|
| 219 |
+
lat["l1_cycles"] = round(int(out64[0].item()) / hops_l1, 1)
|
| 220 |
+
|
| 221 |
+
l2_ring_n = (2 << 20) // 4
|
| 222 |
+
ring_l2 = _sattolo_ring(l2_ring_n, device)
|
| 223 |
+
ops.mk_chase_global(ring_l2, l2_ring_n, out64) # full-cycle warm walk
|
| 224 |
+
_sync()
|
| 225 |
+
hops_l2 = 1 << 14
|
| 226 |
+
ops.mk_chase_global(ring_l2, hops_l2, out64)
|
| 227 |
+
_sync()
|
| 228 |
+
lat["l2_cycles"] = round(int(out64[0].item()) / hops_l2, 1)
|
| 229 |
+
|
| 230 |
+
hbm_bytes = min(max(4 * l2, 64 << 20), props.total_memory // 8)
|
| 231 |
+
ring_h = _sattolo_ring(int(hbm_bytes) // 4, device)
|
| 232 |
+
hops_h = max((2 * l2) // 128, 1 << 17)
|
| 233 |
+
ops.mk_chase_global(ring_h, 1 << 14, out64) # fault pages / warm TLB
|
| 234 |
+
_sync()
|
| 235 |
+
ops.mk_chase_global(ring_h, hops_h, out64)
|
| 236 |
+
_sync()
|
| 237 |
+
lat["hbm_cycles"] = round(int(out64[0].item()) / hops_h, 1)
|
| 238 |
+
lat["ns_per_cycle"] = round(1.0 / idle_clk, 3)
|
| 239 |
+
d["latency"] = _entry(lat, "cycles (single-lane dependent loads)",
|
| 240 |
+
"Sattolo-cycle pointer chase; 16 KB ring for L1, "
|
| 241 |
+
"2 MB TLB-resident ring for L2, self-evicting "
|
| 242 |
+
"chain (2x L2 in touched lines) for HBM",
|
| 243 |
+
idle_clk, "high",
|
| 244 |
+
"smem includes index arithmetic; HBM includes "
|
| 245 |
+
"residual TLB effects at large footprints")
|
| 246 |
+
del ring_h, ring_l2, ring_l1, ring_s
|
| 247 |
+
|
| 248 |
+
# ---- fp32 FMA and tensor-core throughput
|
| 249 |
+
blocks = sm * 4
|
| 250 |
+
iters = 20_000 if quick else 60_000
|
| 251 |
+
ops.mk_fma_f32(blocks, 256, iters, sink)
|
| 252 |
+
ms = _median(_events(lambda: ops.mk_fma_f32(blocks, 256, iters, sink),
|
| 253 |
+
reps))
|
| 254 |
+
flops = blocks * 256 * iters * 16 * 2 * 2 # 2 chains x 2 flops x unroll
|
| 255 |
+
d["fma_f32"] = _entry(round(flops / (ms * 1e-3) / 1e12, 2), "TFLOP/s",
|
| 256 |
+
"register-resident FMA chains", clk, "high")
|
| 257 |
+
sink64 = torch.zeros(2, dtype=torch.float64, device=device)
|
| 258 |
+
it64 = max(iters // 16, 1000)
|
| 259 |
+
ops.mk_fma_f64(blocks, 256, it64, sink64)
|
| 260 |
+
ms = _median(_events(
|
| 261 |
+
lambda: ops.mk_fma_f64(blocks, 256, it64, sink64), reps))
|
| 262 |
+
flops64 = blocks * 256 * it64 * 16 * 2 * 2
|
| 263 |
+
d["fma_f64"] = _entry(round(flops64 / (ms * 1e-3) / 1e12, 3), "TFLOP/s",
|
| 264 |
+
"register-resident double FMA chains (the DFMA "
|
| 265 |
+
"pipe)", clk, "high")
|
| 266 |
+
mma = {}
|
| 267 |
+
for name, (kind, mnk) in _MMA_KINDS.items():
|
| 268 |
+
it = 30_000 if quick else 100_000
|
| 269 |
+
r = ops.mk_mma(kind, blocks, 8, 512, sink)
|
| 270 |
+
if r != 0:
|
| 271 |
+
mma[name] = "unsupported"
|
| 272 |
+
continue
|
| 273 |
+
_sync()
|
| 274 |
+
ms = _median(_events(
|
| 275 |
+
lambda k=kind, i=it: ops.mk_mma(k, blocks, 8, i, sink), reps))
|
| 276 |
+
flops = blocks * 8 * it * 4 * 2 * mnk
|
| 277 |
+
mma[name] = round(flops / (ms * 1e-3) / 1e12, 1)
|
| 278 |
+
d["mma"] = _entry(mma, "TFLOP/s (TOP/s for int8)",
|
| 279 |
+
"register-fragment mma_sync loops (wmma; fp8 via "
|
| 280 |
+
"inline PTX m16n8k32)", clk, "medium",
|
| 281 |
+
"dense-tile rate without memory traffic; real "
|
| 282 |
+
"kernels see this only when staging hides behind it")
|
| 283 |
+
|
| 284 |
+
# ---- atomic contention curve: throughput vs number of distinct
|
| 285 |
+
# addresses the whole grid hammers (slots = 1 is full contention;
|
| 286 |
+
# slots = threads is contention-free), plus the shared-memory point
|
| 287 |
+
n_thr = blocks * 256
|
| 288 |
+
buf = torch.zeros(n_thr, dtype=torch.float32, device=device)
|
| 289 |
+
at = {"curve_slots": {}}
|
| 290 |
+
it_at = 2_000 if quick else 6_000
|
| 291 |
+
slot_points = [1, 8, 64, 512, 4096, n_thr]
|
| 292 |
+
for slots in slot_points:
|
| 293 |
+
buf.zero_()
|
| 294 |
+
ops.mk_atomics(1, blocks, 256, it_at, buf, slots)
|
| 295 |
+
ms = _median(_events(
|
| 296 |
+
lambda s=slots: ops.mk_atomics(1, blocks, 256, it_at, buf, s),
|
| 297 |
+
reps))
|
| 298 |
+
at["curve_slots"][str(slots)] = round(
|
| 299 |
+
n_thr * it_at / (ms * 1e-3) / 1e9, 2)
|
| 300 |
+
ops.mk_atomics(3, blocks, 256, it_at, buf, 1)
|
| 301 |
+
ms = _median(_events(
|
| 302 |
+
lambda: ops.mk_atomics(3, blocks, 256, it_at, buf, 1), reps))
|
| 303 |
+
at["shared_same_addr"] = round(n_thr * it_at / (ms * 1e-3) / 1e9, 2)
|
| 304 |
+
d["atomics_f32"] = _entry(at, "Gop/s",
|
| 305 |
+
"atomicAdd hammer; curve sweeps the number "
|
| 306 |
+
"of distinct global addresses from 1 (full "
|
| 307 |
+
"contention) to one per thread", clk, "high")
|
| 308 |
+
|
| 309 |
+
# ---- launch overhead
|
| 310 |
+
anchor = sink
|
| 311 |
+
n_launch = 500 if quick else 2000
|
| 312 |
+
_sync()
|
| 313 |
+
t0 = time.perf_counter()
|
| 314 |
+
for _ in range(n_launch):
|
| 315 |
+
ops.mk_empty(anchor)
|
| 316 |
+
_sync()
|
| 317 |
+
queued = (time.perf_counter() - t0) / n_launch * 1e6
|
| 318 |
+
n_rt = 100 if quick else 300
|
| 319 |
+
t0 = time.perf_counter()
|
| 320 |
+
for _ in range(n_rt):
|
| 321 |
+
ops.mk_empty(anchor)
|
| 322 |
+
_sync()
|
| 323 |
+
rt = (time.perf_counter() - t0) / n_rt * 1e6
|
| 324 |
+
d["launch_overhead"] = _entry(
|
| 325 |
+
{"queued_us": round(queued, 2), "round_trip_us": round(rt, 2)},
|
| 326 |
+
"us", "empty-kernel trains, queued vs launch+sync", clk, "high",
|
| 327 |
+
"host-dependent: includes torch op dispatch")
|
| 328 |
+
|
| 329 |
+
# ---- grid barrier
|
| 330 |
+
bar_iters = 500 if quick else 2000
|
| 331 |
+
r = ops.mk_barrier(sm, 256, bar_iters, out64)
|
| 332 |
+
if r == 0:
|
| 333 |
+
_sync()
|
| 334 |
+
ticks = int(out64[0].item())
|
| 335 |
+
d["grid_barrier"] = _entry(
|
| 336 |
+
round(ticks / bar_iters / clk / 1000.0, 2), "us per grid.sync",
|
| 337 |
+
f"cooperative kernel, {sm} blocks x 256 threads", clk, "high")
|
| 338 |
+
else:
|
| 339 |
+
d["grid_barrier"] = _entry("unsupported", "", "cooperative launch "
|
| 340 |
+
"unavailable or grid too large", clk,
|
| 341 |
+
"high")
|
| 342 |
+
|
| 343 |
+
# ---- occupancy table (API) plus timed knees: the same random gather
|
| 344 |
+
# at every (threads, dyn_smem) point; when the forced blocks/SM stop
|
| 345 |
+
# covering memory latency, achieved bandwidth falls off the cliff
|
| 346 |
+
occ = {}
|
| 347 |
+
for threads in (128, 256, 512, 1024):
|
| 348 |
+
row = {}
|
| 349 |
+
for smem_kb in (0, 16, 32, 48):
|
| 350 |
+
row[f"{smem_kb}KB"] = int(ops.mk_occupancy(
|
| 351 |
+
threads, smem_kb * 1024, anchor))
|
| 352 |
+
occ[f"t{threads}"] = row
|
| 353 |
+
d["occupancy_blocks_per_sm"] = _entry(
|
| 354 |
+
occ, "blocks/SM", "occupancy API on the float4 triad kernel",
|
| 355 |
+
clk, "high", "API-derived, not timing-derived")
|
| 356 |
+
|
| 357 |
+
knee_iters = 6_000 if quick else 20_000
|
| 358 |
+
knee_blocks = sm * 8
|
| 359 |
+
knees = {}
|
| 360 |
+
for threads in (128, 256, 1024):
|
| 361 |
+
row = {}
|
| 362 |
+
for smem_kb in (0, 32, 48):
|
| 363 |
+
ops.mk_fma_dep(knee_blocks, threads, smem_kb * 1024,
|
| 364 |
+
knee_iters, sink)
|
| 365 |
+
ms = _median(_events(
|
| 366 |
+
lambda t=threads, s=smem_kb: ops.mk_fma_dep(
|
| 367 |
+
knee_blocks, t, s * 1024, knee_iters, sink), reps))
|
| 368 |
+
fl = knee_blocks * threads * knee_iters * 16 * 2
|
| 369 |
+
row[f"{smem_kb}KB"] = round(fl / (ms * 1e-3) / 1e12, 2)
|
| 370 |
+
knees[f"t{threads}"] = row
|
| 371 |
+
d["occupancy_knees_fma_tflops"] = _entry(
|
| 372 |
+
knees, "TFLOP/s", "a single dependent FMA chain per thread, timed "
|
| 373 |
+
"at forced (threads, dyn smem) occupancies over fixed total work; "
|
| 374 |
+
"throughput is proportional to resident threads until the pipe "
|
| 375 |
+
"saturates, so the cliff marks where occupancy stops covering "
|
| 376 |
+
"latency", clk, "high",
|
| 377 |
+
"timing-derived counterpart to the API table above")
|
| 378 |
+
return d
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def bench(fn, iters=100, warmup=10, bytes=None, flops=None, dossier=None,
|
| 382 |
+
dtype="fp16", clock_guard=0.97, clock_stride=8, graph=False,
|
| 383 |
+
device="cuda"):
|
| 384 |
+
"""Median-of-N cudaEvent timing with throttle rejection.
|
| 385 |
+
|
| 386 |
+
The SM clock is probed every `clock_stride` samples; samples inside a
|
| 387 |
+
window whose clock sagged below `clock_guard` x the best observed
|
| 388 |
+
clock are rejected and counted rather than silently averaged. With
|
| 389 |
+
declared `bytes`/`flops` per call, reports achieved GB/s / TFLOP/s;
|
| 390 |
+
with a `dossier`, adds roofline placement against the measured peaks.
|
| 391 |
+
|
| 392 |
+
`graph=True` captures fn() into a CUDA graph and times graph replays,
|
| 393 |
+
stripping Python and torch dispatch from the measurement: the right
|
| 394 |
+
mode for kernels in the few-microsecond range, where dispatch is
|
| 395 |
+
otherwise the thing being measured. Capture requires fn to be
|
| 396 |
+
graph-safe (no sync, no allocation growth, static shapes)."""
|
| 397 |
+
for _ in range(warmup):
|
| 398 |
+
fn()
|
| 399 |
+
_sync()
|
| 400 |
+
if graph:
|
| 401 |
+
g = torch.cuda.CUDAGraph()
|
| 402 |
+
with torch.cuda.graph(g):
|
| 403 |
+
fn()
|
| 404 |
+
fn = g.replay
|
| 405 |
+
for _ in range(warmup):
|
| 406 |
+
fn()
|
| 407 |
+
_sync()
|
| 408 |
+
samples = []
|
| 409 |
+
clocks = []
|
| 410 |
+
stride = clock_stride
|
| 411 |
+
for base in range(0, iters, stride):
|
| 412 |
+
clocks.append(measure_clock(device, ticks=1_500_000))
|
| 413 |
+
n = min(stride, iters - base)
|
| 414 |
+
samples.append((_events(fn, n), len(clocks) - 1))
|
| 415 |
+
clocks.append(measure_clock(device, ticks=1_500_000))
|
| 416 |
+
best_clk = max(clocks)
|
| 417 |
+
kept, rejected = [], 0
|
| 418 |
+
flat = []
|
| 419 |
+
for group, ci in samples:
|
| 420 |
+
window_ok = (clocks[ci] >= clock_guard * best_clk and
|
| 421 |
+
clocks[min(ci + 1, len(clocks) - 1)] >=
|
| 422 |
+
clock_guard * best_clk)
|
| 423 |
+
for ms in group:
|
| 424 |
+
flat.append(ms)
|
| 425 |
+
if window_ok:
|
| 426 |
+
kept.append(ms)
|
| 427 |
+
else:
|
| 428 |
+
rejected += 1
|
| 429 |
+
if not kept:
|
| 430 |
+
kept = flat
|
| 431 |
+
med = _median(kept)
|
| 432 |
+
rep = {
|
| 433 |
+
"median_ms": round(med, 4),
|
| 434 |
+
"min_ms": round(min(kept), 4),
|
| 435 |
+
"mean_ms": round(sum(kept) / len(kept), 4),
|
| 436 |
+
"samples": len(flat),
|
| 437 |
+
"rejected_throttled": rejected,
|
| 438 |
+
"graph_captured": bool(graph),
|
| 439 |
+
"clock_ghz": {"best": round(best_clk, 3),
|
| 440 |
+
"worst": round(min(clocks), 3)},
|
| 441 |
+
}
|
| 442 |
+
if bytes is not None:
|
| 443 |
+
rep["achieved_gbs"] = round(bytes / (med * 1e-3) / 1e9, 1)
|
| 444 |
+
if flops is not None:
|
| 445 |
+
rep["achieved_tflops"] = round(flops / (med * 1e-3) / 1e12, 2)
|
| 446 |
+
if bytes and flops:
|
| 447 |
+
rep["arithmetic_intensity"] = round(flops / bytes, 2)
|
| 448 |
+
if dossier is not None and bytes and flops:
|
| 449 |
+
bw = max(dossier["hbm_bw"]["value"].values()) * 1e9
|
| 450 |
+
mma_v = dossier["mma"]["value"].get(dtype)
|
| 451 |
+
peak = (mma_v if isinstance(mma_v, (int, float))
|
| 452 |
+
else dossier["fma_f32"]["value"]) * 1e12
|
| 453 |
+
ai = flops / bytes
|
| 454 |
+
roof = min(peak, ai * bw)
|
| 455 |
+
rep["roofline"] = {
|
| 456 |
+
"bound": "memory" if ai * bw < peak else "compute",
|
| 457 |
+
"ridge_flops_per_byte": round(peak / bw, 1),
|
| 458 |
+
"pct_of_roof": round(100 * (flops / (med * 1e-3)) / roof, 1),
|
| 459 |
+
"peak_dtype": dtype,
|
| 460 |
+
}
|
| 461 |
+
return rep
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
def _to_ordered_int(t):
|
| 465 |
+
"""Map float bit patterns to integers whose difference is the ULP
|
| 466 |
+
distance: positive floats keep their bits, negative floats mirror
|
| 467 |
+
below zero, so the mapping is monotone in the float value."""
|
| 468 |
+
if t.dtype == torch.float32:
|
| 469 |
+
u = t.view(torch.int32).to(torch.int64) & 0xFFFFFFFF
|
| 470 |
+
half = 1 << 31
|
| 471 |
+
elif t.dtype in (torch.float16, torch.bfloat16):
|
| 472 |
+
u = t.view(torch.int16).to(torch.int64) & 0xFFFF
|
| 473 |
+
half = 1 << 15
|
| 474 |
+
elif t.dtype == torch.float64:
|
| 475 |
+
i = t.view(torch.int64)
|
| 476 |
+
return torch.where(i >= 0, i, (-(1 << 63)) - i)
|
| 477 |
+
else:
|
| 478 |
+
raise ValueError(f"unsupported dtype {t.dtype}")
|
| 479 |
+
return torch.where(u < half, u, half - u)
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
def ulp_diff(a, b):
|
| 483 |
+
"""Elementwise ULP distance between same-dtype float tensors. NaNs in
|
| 484 |
+
the same position count as 0; a one-sided NaN counts as 2**62."""
|
| 485 |
+
assert a.dtype == b.dtype and a.shape == b.shape
|
| 486 |
+
ia, ib = _to_ordered_int(a), _to_ordered_int(b)
|
| 487 |
+
d = (ia - ib).abs()
|
| 488 |
+
both_nan = torch.isnan(a) & torch.isnan(b)
|
| 489 |
+
one_nan = torch.isnan(a) ^ torch.isnan(b)
|
| 490 |
+
d = torch.where(both_nan, torch.zeros_like(d), d)
|
| 491 |
+
d = torch.where(one_nan, torch.full_like(d, 1 << 62), d)
|
| 492 |
+
return d
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
class order_shuffles:
|
| 496 |
+
"""Built-in order shuffles for the common contractual-noise axes."""
|
| 497 |
+
|
| 498 |
+
@staticmethod
|
| 499 |
+
def last_dim():
|
| 500 |
+
"""Permute the last dim of every tensor arg identically: the
|
| 501 |
+
accumulation order of a last-dim reduction changes, the value
|
| 502 |
+
does not."""
|
| 503 |
+
def shuffle(args, seed):
|
| 504 |
+
g = torch.Generator(device=args[0].device).manual_seed(seed)
|
| 505 |
+
p = torch.randperm(args[0].shape[-1], device=args[0].device,
|
| 506 |
+
generator=g)
|
| 507 |
+
return tuple(x[..., p] for x in args)
|
| 508 |
+
return shuffle
|
| 509 |
+
|
| 510 |
+
@staticmethod
|
| 511 |
+
def matmul_k():
|
| 512 |
+
"""Permute K of (A [.., M, K], B [.., K, N]) consistently: the
|
| 513 |
+
product is unchanged, the accumulation order is not."""
|
| 514 |
+
def shuffle(args, seed):
|
| 515 |
+
a_m, b_m = args[0], args[1]
|
| 516 |
+
g = torch.Generator(device=a_m.device).manual_seed(seed)
|
| 517 |
+
p = torch.randperm(a_m.shape[-1], device=a_m.device,
|
| 518 |
+
generator=g)
|
| 519 |
+
return (a_m[..., p], b_m[..., p, :]) + tuple(args[2:])
|
| 520 |
+
return shuffle
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
def _as_outputs(x):
|
| 524 |
+
return list(x) if isinstance(x, (tuple, list)) else [x]
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
def compare(ref, cand, gen, trials=8, order_shuffle=None, order_trials=8,
|
| 528 |
+
ulp_tol=None, atol=None, rtol=None, band_margin=4.0, seed=0):
|
| 529 |
+
"""Adversarial correctness comparison with ULP evidence.
|
| 530 |
+
|
| 531 |
+
`gen(seed) -> args` builds inputs; `ref(*args)` and `cand(*args)`
|
| 532 |
+
return a tensor or a tuple of tensors (each output is compared and
|
| 533 |
+
the worst governs). If `order_shuffle(args, seed) -> args` is given
|
| 534 |
+
and reorders contractual-noise dimensions (see `order_shuffles` for
|
| 535 |
+
the common cases), the reference is treated as a distribution:
|
| 536 |
+
candidates are judged against the measured reduction-order band
|
| 537 |
+
rather than a point. Verdict, stated in the report: max ULP <=
|
| 538 |
+
band_margin * band + 2 when a band is measured, else max ULP <=
|
| 539 |
+
ulp_tol (default 2); if `atol`/`rtol` are given they are an
|
| 540 |
+
alternative acceptance route, checked as |c - r| <= atol + rtol|r|."""
|
| 541 |
+
worst = {"max_ulp": -1}
|
| 542 |
+
total_hist = {}
|
| 543 |
+
per_trial = []
|
| 544 |
+
max_abs_seen = 0.0
|
| 545 |
+
max_rel_seen = 0.0
|
| 546 |
+
for t in range(trials):
|
| 547 |
+
args = gen(seed + t)
|
| 548 |
+
r_outs = _as_outputs(ref(*args))
|
| 549 |
+
c_outs = _as_outputs(cand(*args))
|
| 550 |
+
assert len(r_outs) == len(c_outs), "output arity mismatch"
|
| 551 |
+
# the order band is POINTWISE: a near-zero reduction output is
|
| 552 |
+
# legitimately thousands of ULP order-sensitive (cancellation)
|
| 553 |
+
# while its neighbors are stable, so each element is judged
|
| 554 |
+
# against its own measured sensitivity, not a global max
|
| 555 |
+
bands = None
|
| 556 |
+
band = 0
|
| 557 |
+
band_score = 0.0
|
| 558 |
+
if order_shuffle is not None:
|
| 559 |
+
bands = [torch.zeros_like(ro, dtype=torch.int64)
|
| 560 |
+
for ro in r_outs]
|
| 561 |
+
for k in range(order_trials):
|
| 562 |
+
r2 = _as_outputs(
|
| 563 |
+
ref(*order_shuffle(args, 1000 + seed + t * 131 + k)))
|
| 564 |
+
for bi, (ro, r2o) in enumerate(zip(r_outs, r2)):
|
| 565 |
+
bands[bi] = torch.maximum(bands[bi],
|
| 566 |
+
ulp_diff(ro, r2o))
|
| 567 |
+
band = max(int(b.max().item()) if b.numel() else 0
|
| 568 |
+
for b in bands)
|
| 569 |
+
max_ulp = 0
|
| 570 |
+
mean_ulp = 0.0
|
| 571 |
+
first_div = -1
|
| 572 |
+
n_el = 0
|
| 573 |
+
nan_r = nan_c = inf_r = inf_c = 0
|
| 574 |
+
for oi, (r, c) in enumerate(zip(r_outs, c_outs)):
|
| 575 |
+
d = ulp_diff(r, c)
|
| 576 |
+
if bands is not None and d.numel():
|
| 577 |
+
ratio = d.double() / (bands[oi].double() + 2.0)
|
| 578 |
+
band_score = max(band_score, float(ratio.max().item()))
|
| 579 |
+
if d.numel():
|
| 580 |
+
max_ulp = max(max_ulp, int(d.max().item()))
|
| 581 |
+
mean_ulp += float(d.double().sum().item())
|
| 582 |
+
n_el += d.numel()
|
| 583 |
+
nz = (d > 0)
|
| 584 |
+
if first_div < 0 and nz.any():
|
| 585 |
+
first_div = int(nz.flatten().nonzero()[0].item())
|
| 586 |
+
if nz.any():
|
| 587 |
+
buckets = torch.clamp(
|
| 588 |
+
torch.log2(d[nz].double()).floor(), 0, 62
|
| 589 |
+
).to(torch.int64)
|
| 590 |
+
counts = torch.bincount(buckets, minlength=63)
|
| 591 |
+
for bkt in counts.nonzero().flatten().tolist():
|
| 592 |
+
total_hist[bkt] = total_hist.get(bkt, 0) + int(
|
| 593 |
+
counts[bkt].item())
|
| 594 |
+
fin = torch.isfinite(r) & torch.isfinite(c)
|
| 595 |
+
if fin.any():
|
| 596 |
+
diff = (c[fin].double() - r[fin].double()).abs()
|
| 597 |
+
max_abs_seen = max(max_abs_seen,
|
| 598 |
+
float(diff.max().item()))
|
| 599 |
+
rel = diff / r[fin].double().abs().clamp(min=1e-300)
|
| 600 |
+
max_rel_seen = max(max_rel_seen,
|
| 601 |
+
float(rel.max().item()))
|
| 602 |
+
nan_r += int(torch.isnan(r).sum().item())
|
| 603 |
+
nan_c += int(torch.isnan(c).sum().item())
|
| 604 |
+
inf_r += int(torch.isinf(r).sum().item())
|
| 605 |
+
inf_c += int(torch.isinf(c).sum().item())
|
| 606 |
+
trial_rep = {
|
| 607 |
+
"max_ulp": max_ulp,
|
| 608 |
+
"mean_ulp": round(mean_ulp / max(n_el, 1), 3),
|
| 609 |
+
"first_divergent_index": first_div,
|
| 610 |
+
"nan_ref": nan_r, "nan_cand": nan_c,
|
| 611 |
+
"inf_ref": inf_r, "inf_cand": inf_c,
|
| 612 |
+
"order_band_ulp": band,
|
| 613 |
+
"band_score": round(band_score, 2),
|
| 614 |
+
}
|
| 615 |
+
per_trial.append(trial_rep)
|
| 616 |
+
if max_ulp > worst["max_ulp"]:
|
| 617 |
+
worst = trial_rep
|
| 618 |
+
band_max = max(t["order_band_ulp"] for t in per_trial)
|
| 619 |
+
max_seen = max(t["max_ulp"] for t in per_trial)
|
| 620 |
+
if order_shuffle is not None:
|
| 621 |
+
score = max(t["band_score"] for t in per_trial)
|
| 622 |
+
ulp_ok = score <= band_margin
|
| 623 |
+
criterion = (f"pointwise max(ulp / (order_band + 2)) = "
|
| 624 |
+
f"{score:.2f} <= band_margin {band_margin}")
|
| 625 |
+
else:
|
| 626 |
+
tol = ulp_tol if ulp_tol is not None else 2
|
| 627 |
+
ulp_ok = max_seen <= tol
|
| 628 |
+
criterion = f"max_ulp {max_seen} <= ulp_tol {tol}"
|
| 629 |
+
tol_ok = False
|
| 630 |
+
if atol is not None or rtol is not None:
|
| 631 |
+
tol_ok = max_abs_seen <= (atol or 0) + (rtol or 0) * 1.0 or \
|
| 632 |
+
max_rel_seen <= (rtol or 0) or max_abs_seen <= (atol or 0)
|
| 633 |
+
criterion += (f"; OR max_abs {max_abs_seen:.3e} <= atol {atol} / "
|
| 634 |
+
f"max_rel {max_rel_seen:.3e} <= rtol {rtol}")
|
| 635 |
+
return {
|
| 636 |
+
"pass": bool(ulp_ok or tol_ok),
|
| 637 |
+
"criterion": criterion,
|
| 638 |
+
"worst_trial": worst,
|
| 639 |
+
"max_abs": max_abs_seen,
|
| 640 |
+
"max_rel": max_rel_seen,
|
| 641 |
+
"trials": per_trial,
|
| 642 |
+
"ulp_hist_log2": dict(sorted(total_hist.items())),
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
|
| 646 |
+
_FUZZ_VARIANTS = ("contig", "transposed", "strided_rows", "offset_slice",
|
| 647 |
+
"overflow_expand")
|
| 648 |
+
|
| 649 |
+
|
| 650 |
+
def _context_alive(device):
|
| 651 |
+
try:
|
| 652 |
+
t = torch.ones(4, device=device)
|
| 653 |
+
torch.cuda.synchronize()
|
| 654 |
+
return bool(t.sum().item() == 4.0)
|
| 655 |
+
except Exception: # noqa: BLE001
|
| 656 |
+
return False
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
def fuzz_shapes(op, ref, make, shapes=None, variants=_FUZZ_VARIANTS,
|
| 660 |
+
dtypes=(torch.float32,), ulp_tol=64, device="cuda"):
|
| 661 |
+
"""The ugly envelope: run op vs ref across edge shapes, dtypes, and
|
| 662 |
+
layout mutations; report ok / mismatch / exception per case.
|
| 663 |
+
|
| 664 |
+
`make(shape, dtype)` (or `make(shape)`) builds a base input, either a
|
| 665 |
+
tensor or a tuple of tensors for multi-argument ops; layout variants
|
| 666 |
+
derive non-contiguous and misaligned views whose CONTENT equals a
|
| 667 |
+
contiguous copy, so `op(*xs)` must match `ref(*xs)` on every variant.
|
| 668 |
+
`overflow_expand` stride-0-expands the input past 2**31 logical
|
| 669 |
+
elements to catch int32 indexing, and is skipped with status
|
| 670 |
+
`skipped_vram` when materializing the output would not fit. After any
|
| 671 |
+
exception the CUDA context is health-checked; if a candidate killed
|
| 672 |
+
it, remaining cases are marked `context_lost` instead of producing
|
| 673 |
+
garbage."""
|
| 674 |
+
if shapes is None:
|
| 675 |
+
shapes = [(0, 8), (1, 1), (1, 17), (3, 63), (17, 129), (128, 128),
|
| 676 |
+
(1023, 5), (2, 4096)]
|
| 677 |
+
|
| 678 |
+
def build(shape, dtype):
|
| 679 |
+
try:
|
| 680 |
+
out = make(tuple(shape), dtype)
|
| 681 |
+
except TypeError:
|
| 682 |
+
out = make(tuple(shape))
|
| 683 |
+
return out if isinstance(out, (tuple, list)) else (out,)
|
| 684 |
+
|
| 685 |
+
def variant_of(x, v):
|
| 686 |
+
if v == "contig":
|
| 687 |
+
return x
|
| 688 |
+
if v == "transposed":
|
| 689 |
+
return x.t() if x.dim() == 2 else x
|
| 690 |
+
if v == "strided_rows":
|
| 691 |
+
return x[::2] if x.shape[0] >= 2 else x
|
| 692 |
+
if v == "offset_slice":
|
| 693 |
+
# a storage-offset-1 view: misaligned for vectorized loads
|
| 694 |
+
return x.flatten()[1:] if x.numel() > 1 else x
|
| 695 |
+
if v == "overflow_expand":
|
| 696 |
+
if x.numel() == 0:
|
| 697 |
+
return None
|
| 698 |
+
per = max(x.numel(), 1)
|
| 699 |
+
rep = (1 << 31) // per + 2
|
| 700 |
+
return x.reshape((1,) + tuple(x.shape)).expand(
|
| 701 |
+
(rep,) + tuple(x.shape))
|
| 702 |
+
return x
|
| 703 |
+
|
| 704 |
+
rows = []
|
| 705 |
+
lost = False
|
| 706 |
+
for dtype in dtypes:
|
| 707 |
+
for shape in shapes:
|
| 708 |
+
bases = build(shape, dtype)
|
| 709 |
+
for v in variants:
|
| 710 |
+
case = {"shape": tuple(shape), "variant": v,
|
| 711 |
+
"dtype": str(dtype).replace("torch.", "")}
|
| 712 |
+
if lost:
|
| 713 |
+
case["status"] = "context_lost"
|
| 714 |
+
rows.append(case)
|
| 715 |
+
continue
|
| 716 |
+
xs = [variant_of(b, v) for b in bases]
|
| 717 |
+
if any(x is None for x in xs):
|
| 718 |
+
case["status"] = "skipped_empty"
|
| 719 |
+
rows.append(case)
|
| 720 |
+
continue
|
| 721 |
+
if v == "overflow_expand":
|
| 722 |
+
logical = max(x.numel() for x in xs)
|
| 723 |
+
need = logical * xs[0].element_size() * 2
|
| 724 |
+
free, _total = torch.cuda.mem_get_info()
|
| 725 |
+
if need > free * 0.8:
|
| 726 |
+
case["status"] = "skipped_vram"
|
| 727 |
+
case["logical_elems"] = int(logical)
|
| 728 |
+
rows.append(case)
|
| 729 |
+
continue
|
| 730 |
+
case["logical_elems"] = int(logical)
|
| 731 |
+
case["contiguous"] = bool(xs[0].is_contiguous())
|
| 732 |
+
try:
|
| 733 |
+
got = op(*xs)
|
| 734 |
+
want = ref(*xs)
|
| 735 |
+
if v == "overflow_expand":
|
| 736 |
+
# bounded comparison: the head plus the high-index
|
| 737 |
+
# tail, which is where int32 flat indexing wraps
|
| 738 |
+
w = 1 << 20
|
| 739 |
+
gf, wf = got.flatten(), want.flatten()
|
| 740 |
+
pairs = ((gf[:w], wf[:w]), (gf[-w:], wf[-w:]))
|
| 741 |
+
else:
|
| 742 |
+
pairs = ((got, want),)
|
| 743 |
+
mx = 0
|
| 744 |
+
for gp, wp in pairs:
|
| 745 |
+
d = ulp_diff(wp, gp) if gp.is_floating_point() \
|
| 746 |
+
else (wp != gp).to(torch.int64) * (1 << 62)
|
| 747 |
+
if d.numel():
|
| 748 |
+
mx = max(mx, int(d.max().item()))
|
| 749 |
+
del d
|
| 750 |
+
case["status"] = "ok" if mx <= ulp_tol else "mismatch"
|
| 751 |
+
case["max_ulp"] = mx
|
| 752 |
+
del got, want
|
| 753 |
+
except Exception as e: # noqa: BLE001 - report, don't die
|
| 754 |
+
case["status"] = "exception"
|
| 755 |
+
case["error"] = f"{type(e).__name__}: {str(e)[:120]}"
|
| 756 |
+
if not _context_alive(device):
|
| 757 |
+
lost = True
|
| 758 |
+
case["status"] = "context_killed"
|
| 759 |
+
rows.append(case)
|
| 760 |
+
n_bad = sum(1 for r in rows
|
| 761 |
+
if r["status"] not in ("ok", "skipped_vram",
|
| 762 |
+
"skipped_empty"))
|
| 763 |
+
return {"cases": rows, "failed": n_bad, "total": len(rows),
|
| 764 |
+
"context_lost": lost}
|
| 765 |
+
|
| 766 |
+
|
| 767 |
+
def sweep(factory, grid, iters=30, warmup=5, bytes=None, flops=None,
|
| 768 |
+
check=None, budget_s=None):
|
| 769 |
+
"""Launch-configuration search: `factory(**point) -> callable`; one
|
| 770 |
+
bench row per grid point, sorted fastest first. A point whose factory
|
| 771 |
+
or run raises is reported, not skipped silently. `check(fn) -> bool`
|
| 772 |
+
is an optional correctness gate recorded per row (a fast wrong config
|
| 773 |
+
must not win a sweep). `budget_s` caps total sweep time; points not
|
| 774 |
+
reached are reported as `not_run` rather than silently dropped."""
|
| 775 |
+
keys = list(grid.keys())
|
| 776 |
+
|
| 777 |
+
def points(i, acc):
|
| 778 |
+
if i == len(keys):
|
| 779 |
+
yield dict(acc)
|
| 780 |
+
return
|
| 781 |
+
for v in grid[keys[i]]:
|
| 782 |
+
acc[keys[i]] = v
|
| 783 |
+
yield from points(i + 1, acc)
|
| 784 |
+
|
| 785 |
+
rows = []
|
| 786 |
+
t_start = time.perf_counter()
|
| 787 |
+
over_budget = False
|
| 788 |
+
for p in points(0, {}):
|
| 789 |
+
row = dict(p)
|
| 790 |
+
if over_budget or (budget_s is not None and
|
| 791 |
+
time.perf_counter() - t_start > budget_s):
|
| 792 |
+
over_budget = True
|
| 793 |
+
row["status"] = "not_run"
|
| 794 |
+
row["median_ms"] = float("inf")
|
| 795 |
+
rows.append(row)
|
| 796 |
+
continue
|
| 797 |
+
try:
|
| 798 |
+
fn = factory(**p)
|
| 799 |
+
if check is not None:
|
| 800 |
+
row["check"] = bool(check(fn))
|
| 801 |
+
rep = bench(fn, iters=iters, warmup=warmup, bytes=bytes,
|
| 802 |
+
flops=flops)
|
| 803 |
+
row.update({"median_ms": rep["median_ms"],
|
| 804 |
+
"rejected_throttled": rep["rejected_throttled"]})
|
| 805 |
+
if "achieved_gbs" in rep:
|
| 806 |
+
row["achieved_gbs"] = rep["achieved_gbs"]
|
| 807 |
+
if "achieved_tflops" in rep:
|
| 808 |
+
row["achieved_tflops"] = rep["achieved_tflops"]
|
| 809 |
+
except Exception as e: # noqa: BLE001
|
| 810 |
+
row["error"] = f"{type(e).__name__}: {str(e)[:120]}"
|
| 811 |
+
row["median_ms"] = float("inf")
|
| 812 |
+
rows.append(row)
|
| 813 |
+
rows.sort(key=lambda r: (not r.get("check", True), r["median_ms"]))
|
| 814 |
+
return rows
|
| 815 |
+
|
| 816 |
+
|
| 817 |
+
def alloc_stamps(n, device="cuda"):
|
| 818 |
+
"""Device buffer for the MK_STAMP convention (metakernel_cuda/
|
| 819 |
+
stamps.cuh): the candidate kernel writes clock64() into slot i."""
|
| 820 |
+
return torch.zeros(n, dtype=torch.int64, device=device)
|
| 821 |
+
|
| 822 |
+
|
| 823 |
+
def read_stamps(stamps, labels=None, clock_ghz=None):
|
| 824 |
+
"""Decode MK_STAMP phases: consecutive deltas in ticks and in
|
| 825 |
+
microseconds under the measured (or provided) SM clock. A 1-D buffer
|
| 826 |
+
(MK_STAMP: block 0 only) yields one delta per phase; a 2-D [B, n]
|
| 827 |
+
buffer (MK_STAMP_BLOCK) yields min / median / max across blocks per
|
| 828 |
+
phase, with blocks whose stamps stayed zero excluded."""
|
| 829 |
+
if clock_ghz is None:
|
| 830 |
+
clock_ghz = measure_clock(stamps.device)
|
| 831 |
+
|
| 832 |
+
def us(ticks):
|
| 833 |
+
return round(ticks / clock_ghz / 1000.0, 3)
|
| 834 |
+
|
| 835 |
+
if stamps.dim() == 1:
|
| 836 |
+
vals = stamps.cpu().tolist()
|
| 837 |
+
phases = []
|
| 838 |
+
for i in range(len(vals) - 1):
|
| 839 |
+
ticks = vals[i + 1] - vals[i]
|
| 840 |
+
name = labels[i] if labels and i < len(labels) else f"phase_{i}"
|
| 841 |
+
phases.append({"phase": name, "ticks": ticks, "us": us(ticks)})
|
| 842 |
+
return {"phases": phases, "clock_ghz": round(clock_ghz, 3)}
|
| 843 |
+
|
| 844 |
+
m = stamps.cpu()
|
| 845 |
+
live = (m != 0).all(dim=1)
|
| 846 |
+
m = m[live]
|
| 847 |
+
deltas = (m[:, 1:] - m[:, :-1]).to(torch.float64)
|
| 848 |
+
phases = []
|
| 849 |
+
for i in range(deltas.shape[1]):
|
| 850 |
+
col = deltas[:, i]
|
| 851 |
+
name = labels[i] if labels and i < len(labels) else f"phase_{i}"
|
| 852 |
+
med = float(col.median().item()) if col.numel() else 0.0
|
| 853 |
+
phases.append({
|
| 854 |
+
"phase": name,
|
| 855 |
+
"ticks_median": int(med),
|
| 856 |
+
"ticks_min": int(col.min().item()) if col.numel() else 0,
|
| 857 |
+
"ticks_max": int(col.max().item()) if col.numel() else 0,
|
| 858 |
+
"us_median": us(med),
|
| 859 |
+
"blocks": int(col.numel()),
|
| 860 |
+
})
|
| 861 |
+
return {"phases": phases, "clock_ghz": round(clock_ghz, 3),
|
| 862 |
+
"blocks_reporting": int(live.sum().item()),
|
| 863 |
+
"blocks_total": int(live.numel())}
|
build/torch212-cxx11-cu132-x86_64-linux/_metakernel_cuda_4611d12.abi3.so
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e88576465f9b2bebeea6c417bf42418137f71e9029cbf52dc03e83f86e433a6d
|
| 3 |
+
size 980192
|
build/torch212-cxx11-cu132-x86_64-linux/_ops.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from . import _metakernel_cuda_4611d12
|
| 3 |
+
ops = torch.ops._metakernel_cuda_4611d12
|
| 4 |
+
|
| 5 |
+
def add_op_namespace_prefix(op_name: str):
|
| 6 |
+
"""
|
| 7 |
+
Prefix op by namespace.
|
| 8 |
+
"""
|
| 9 |
+
return f"_metakernel_cuda_4611d12::{op_name}"
|
build/torch212-cxx11-cu132-x86_64-linux/metadata.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "metakernel",
|
| 3 |
+
"id": "_metakernel_cuda_4611d12",
|
| 4 |
+
"version": 1,
|
| 5 |
+
"license": "Apache-2.0",
|
| 6 |
+
"python-depends": [],
|
| 7 |
+
"backend": {
|
| 8 |
+
"type": "cuda",
|
| 9 |
+
"archs": [
|
| 10 |
+
"10.0",
|
| 11 |
+
"12.0",
|
| 12 |
+
"8.0",
|
| 13 |
+
"8.6",
|
| 14 |
+
"8.9",
|
| 15 |
+
"9.0"
|
| 16 |
+
]
|
| 17 |
+
},
|
| 18 |
+
"digest": {
|
| 19 |
+
"algorithm": "sha256",
|
| 20 |
+
"files": {
|
| 21 |
+
"__init__.py": "cZxKX5I/2EKCMC2gdCwflUsZaQJWJesmAHq2w+fVjXo=",
|
| 22 |
+
"_metakernel_cuda_4611d12.abi3.so": "6IV2Rl+bK+vupsQXv0JBgTf3HpApy/UtwD6D+G5DOm0=",
|
| 23 |
+
"_ops.py": "uZlR720EcTy8M7BCydmvaZAlKonkwTU3778nhR9yQOE=",
|
| 24 |
+
"metakernel/__init__.py": "DFYPlrhXwYjEqCl/8n0SmWGZV8NFml5DPhMjKfv98GY="
|
| 25 |
+
}
|
| 26 |
+
},
|
| 27 |
+
"provenance": {
|
| 28 |
+
"kernel-builder": {
|
| 29 |
+
"version": "0.17.0-dev0",
|
| 30 |
+
"sha": "19aaa6421e674e9fecc352bbae6eab81d19a6bf4",
|
| 31 |
+
"dirty": false
|
| 32 |
+
},
|
| 33 |
+
"kernel": {
|
| 34 |
+
"sha": "4611d1213a76a9ec1d12646553a528feb60394ff",
|
| 35 |
+
"dirty": false
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
}
|
build/torch212-cxx11-cu132-x86_64-linux/metakernel/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ctypes
|
| 2 |
+
import importlib.util
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from types import ModuleType
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def _import_from_path(file_path: Path) -> ModuleType:
|
| 9 |
+
# We cannot use the module name as-is, after adding it to `sys.modules`,
|
| 10 |
+
# it would also be used for other imports. So, we make a module name that
|
| 11 |
+
# depends on the path for it to be unique using the hex-encoded hash of
|
| 12 |
+
# the path.
|
| 13 |
+
path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
|
| 14 |
+
module_name = path_hash
|
| 15 |
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
| 16 |
+
if spec is None:
|
| 17 |
+
raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
|
| 18 |
+
module = importlib.util.module_from_spec(spec)
|
| 19 |
+
if module is None:
|
| 20 |
+
raise ImportError(f"Cannot load module {module_name} from spec")
|
| 21 |
+
sys.modules[module_name] = module
|
| 22 |
+
spec.loader.exec_module(module) # type: ignore
|
| 23 |
+
return module
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
|
build/torch213-cxx11-cu126-x86_64-linux/__init__.py
ADDED
|
@@ -0,0 +1,863 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""metakernel: the measuring instrument for the kernel-design loop.
|
| 2 |
+
|
| 3 |
+
An agent iterating on CUDA candidates is good at writing code and bad at
|
| 4 |
+
trusting its own measurements; it needs trustworthy timing, adversarial
|
| 5 |
+
correctness comparison, and measured facts about the device it targets.
|
| 6 |
+
metakernel ships those as loadable ops:
|
| 7 |
+
|
| 8 |
+
from kernels import get_kernel
|
| 9 |
+
mk = get_kernel("phanerozoic/metakernel", version=1,
|
| 10 |
+
trust_remote_code=True)
|
| 11 |
+
|
| 12 |
+
dossier = mk.probe_device() # measured facts
|
| 13 |
+
report = mk.bench(lambda: op(x), iters=200) # throttle-rejected
|
| 14 |
+
verdict = mk.compare(ref, cand, gen) # ULP evidence
|
| 15 |
+
table = mk.sweep(factory, {"BLOCK": [64, 128]}) # config search
|
| 16 |
+
|
| 17 |
+
Every op returns plain dicts and tensors, never prose. Every dossier entry
|
| 18 |
+
is produced by a dedicated probe kernel rather than a spec sheet, carries
|
| 19 |
+
the SM clock it was measured under, and labels throttled samples instead
|
| 20 |
+
of letting them poison medians. No hardware counters are used: everything
|
| 21 |
+
is inferred from timing, clocks, and the occupancy API, so entries carry a
|
| 22 |
+
confidence field (timing-differential inference can misattribute).
|
| 23 |
+
"""
|
| 24 |
+
import math
|
| 25 |
+
import time
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
|
| 29 |
+
from ._ops import ops
|
| 30 |
+
|
| 31 |
+
__all__ = ["probe_device", "bench", "compare", "order_shuffles",
|
| 32 |
+
"fuzz_shapes", "sweep", "alloc_stamps", "read_stamps",
|
| 33 |
+
"measure_clock", "ulp_diff", "ops"]
|
| 34 |
+
|
| 35 |
+
_MMA_KINDS = {
|
| 36 |
+
"fp16": (0, 16 * 16 * 16),
|
| 37 |
+
"bf16": (1, 16 * 16 * 16),
|
| 38 |
+
"tf32": (2, 16 * 16 * 8),
|
| 39 |
+
"int8": (3, 16 * 16 * 16),
|
| 40 |
+
"fp8_e4m3": (4, 16 * 8 * 32),
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _sync():
|
| 45 |
+
torch.cuda.synchronize()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _events(fn, reps):
|
| 49 |
+
"""Time reps calls of fn with one event pair each; returns ms list."""
|
| 50 |
+
starts = [torch.cuda.Event(enable_timing=True) for _ in range(reps)]
|
| 51 |
+
stops = [torch.cuda.Event(enable_timing=True) for _ in range(reps)]
|
| 52 |
+
for s, e in zip(starts, stops):
|
| 53 |
+
s.record()
|
| 54 |
+
fn()
|
| 55 |
+
e.record()
|
| 56 |
+
_sync()
|
| 57 |
+
return [s.elapsed_time(e) for s, e in zip(starts, stops)]
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def measure_clock(device="cuda", ticks=4_000_000, blocks=None):
|
| 61 |
+
"""Effective SM clock in GHz under an FMA spin of `ticks` clock64 ticks.
|
| 62 |
+
Wall time over elapsed ticks is the frequency the ticks ran at."""
|
| 63 |
+
props = torch.cuda.get_device_properties(device)
|
| 64 |
+
if blocks is None:
|
| 65 |
+
blocks = props.multi_processor_count
|
| 66 |
+
out = torch.zeros(2, dtype=torch.int64, device=device)
|
| 67 |
+
sink = torch.zeros(2, dtype=torch.float32, device=device)
|
| 68 |
+
ops.mk_spin(blocks, 256, ticks, out, sink) # warm
|
| 69 |
+
start = torch.cuda.Event(enable_timing=True)
|
| 70 |
+
stop = torch.cuda.Event(enable_timing=True)
|
| 71 |
+
start.record()
|
| 72 |
+
ops.mk_spin(blocks, 256, ticks, out, sink)
|
| 73 |
+
stop.record()
|
| 74 |
+
_sync()
|
| 75 |
+
ms = start.elapsed_time(stop)
|
| 76 |
+
elapsed = int(out[0].item())
|
| 77 |
+
return elapsed / (ms * 1e6)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _median(xs):
|
| 81 |
+
s = sorted(xs)
|
| 82 |
+
n = len(s)
|
| 83 |
+
return s[n // 2] if n % 2 else 0.5 * (s[n // 2 - 1] + s[n // 2])
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _entry(value, units, method, clock_ghz, confidence, note=""):
|
| 87 |
+
d = {"value": value, "units": units, "method": method,
|
| 88 |
+
"clock_ghz": round(clock_ghz, 3), "confidence": confidence}
|
| 89 |
+
if note:
|
| 90 |
+
d["note"] = note
|
| 91 |
+
return d
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _sattolo_ring(n, device, generator=None):
|
| 95 |
+
"""A random single-cycle permutation as an int32 next-pointer table, so
|
| 96 |
+
a pointer chase visits every slot before repeating."""
|
| 97 |
+
perm = torch.randperm(n, generator=generator)
|
| 98 |
+
ring = torch.empty(n, dtype=torch.int32)
|
| 99 |
+
ring[perm] = torch.roll(perm, -1).to(torch.int32)
|
| 100 |
+
return ring.to(device)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def probe_device(device="cuda", quick=False):
|
| 104 |
+
"""Empirical device dossier. Each entry is measured by a dedicated
|
| 105 |
+
probe kernel and carries units, method, the SM clock it ran under, and
|
| 106 |
+
a confidence field."""
|
| 107 |
+
props = torch.cuda.get_device_properties(device)
|
| 108 |
+
sm = props.multi_processor_count
|
| 109 |
+
l2 = props.L2_cache_size
|
| 110 |
+
d = {
|
| 111 |
+
"device": {
|
| 112 |
+
"name": props.name,
|
| 113 |
+
"sm_arch": f"sm{props.major}{props.minor}",
|
| 114 |
+
"sms": sm,
|
| 115 |
+
"l2_bytes": l2,
|
| 116 |
+
"vram_bytes": props.total_memory,
|
| 117 |
+
"torch": torch.__version__,
|
| 118 |
+
"cuda_runtime": torch.version.cuda,
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
sink = torch.zeros(2, dtype=torch.float32, device=device)
|
| 122 |
+
out64 = torch.zeros(2, dtype=torch.int64, device=device)
|
| 123 |
+
reps = 3 if quick else 5
|
| 124 |
+
|
| 125 |
+
# ---- clock under sustained load
|
| 126 |
+
traj = []
|
| 127 |
+
windows = 8 if quick else 24
|
| 128 |
+
for _ in range(windows):
|
| 129 |
+
traj.append(measure_clock(device, ticks=30_000_000))
|
| 130 |
+
d["clock"] = {
|
| 131 |
+
"max_ghz": round(max(traj), 3),
|
| 132 |
+
"min_ghz": round(min(traj), 3),
|
| 133 |
+
"final_ghz": round(traj[-1], 3),
|
| 134 |
+
"sag_pct": round(100 * (1 - traj[-1] / max(traj)), 1),
|
| 135 |
+
"units": "GHz",
|
| 136 |
+
"method": f"{windows} back-to-back FMA spin windows of 30M ticks; "
|
| 137 |
+
"wall/ticks per window",
|
| 138 |
+
"confidence": "high",
|
| 139 |
+
}
|
| 140 |
+
clk = max(traj)
|
| 141 |
+
|
| 142 |
+
# ---- HBM bandwidth: triad per access width, plus read-only,
|
| 143 |
+
# write-only, and random-gather
|
| 144 |
+
n4 = 16 << 20 # 16M float4 = 256 MB per buffer
|
| 145 |
+
if props.total_memory < 4 << 30:
|
| 146 |
+
n4 = 4 << 20
|
| 147 |
+
a = torch.rand(n4 * 4, device=device)
|
| 148 |
+
b = torch.rand(n4 * 4, device=device)
|
| 149 |
+
c = torch.empty(n4 * 4, device=device)
|
| 150 |
+
hbm = {}
|
| 151 |
+
for width in (1, 2, 4):
|
| 152 |
+
ops.mk_triad(a, b, c, width, 1.5) # warm
|
| 153 |
+
ms = _median(_events(lambda w=width: ops.mk_triad(a, b, c, w, 1.5),
|
| 154 |
+
reps))
|
| 155 |
+
gbs = 3 * a.numel() * 4 / (ms * 1e-3) / 1e9
|
| 156 |
+
hbm[f"triad_{4 * width}B"] = round(gbs, 1)
|
| 157 |
+
ops.mk_read(a, sink)
|
| 158 |
+
ms = _median(_events(lambda: ops.mk_read(a, sink), reps))
|
| 159 |
+
hbm["read_only"] = round(a.numel() * 4 / (ms * 1e-3) / 1e9, 1)
|
| 160 |
+
ops.mk_write(c, 1.0)
|
| 161 |
+
ms = _median(_events(lambda: ops.mk_write(c, 1.0), reps))
|
| 162 |
+
hbm["write_only"] = round(c.numel() * 4 / (ms * 1e-3) / 1e9, 1)
|
| 163 |
+
g = torch.Generator(device="cpu").manual_seed(7)
|
| 164 |
+
idx = torch.randint(0, n4, (n4,), generator=g,
|
| 165 |
+
dtype=torch.int32).to(device)
|
| 166 |
+
ops.mk_gather(a, idx, c, 256, 0)
|
| 167 |
+
ms = _median(_events(lambda: ops.mk_gather(a, idx, c, 256, 0), reps))
|
| 168 |
+
# traffic accounting: idx reads + 16B random loads + streamed stores
|
| 169 |
+
hbm["random_gather_16B"] = round(
|
| 170 |
+
(idx.numel() * 4 + 2 * n4 * 16) / (ms * 1e-3) / 1e9, 1)
|
| 171 |
+
d["hbm_bw"] = _entry(hbm, "GB/s", "streaming triad c=a+s*b per access "
|
| 172 |
+
"width; read-only fold; write-only fill; random "
|
| 173 |
+
"float4 gather (16B granules, nominal traffic)",
|
| 174 |
+
clk, "high",
|
| 175 |
+
"gather moves full cache lines per 16B granule, "
|
| 176 |
+
"so its nominal number is the achievable rate for "
|
| 177 |
+
"random access, not a line-efficiency claim")
|
| 178 |
+
|
| 179 |
+
# ---- L2 bandwidth (working set 3 buffers inside L2)
|
| 180 |
+
ws = max(l2 // 4, 1 << 20)
|
| 181 |
+
nl2 = ws // 16 # float4 elems per buffer
|
| 182 |
+
al = a[:nl2 * 4].contiguous()
|
| 183 |
+
bl = b[:nl2 * 4].contiguous()
|
| 184 |
+
cl = c[:nl2 * 4].contiguous()
|
| 185 |
+
passes = 64
|
| 186 |
+
ops.mk_triad_passes(al, bl, cl, 1.5, passes)
|
| 187 |
+
ms = _median(_events(
|
| 188 |
+
lambda: ops.mk_triad_passes(al, bl, cl, 1.5, passes), reps))
|
| 189 |
+
gbs = 3 * al.numel() * 4 * passes / (ms * 1e-3) / 1e9
|
| 190 |
+
d["l2_bw"] = _entry(round(gbs, 1), "GB/s",
|
| 191 |
+
f"in-kernel repeated triad passes over "
|
| 192 |
+
f"{3 * ws >> 20} MB resident working set", clk,
|
| 193 |
+
"medium", "first pass streams from HBM; amortized "
|
| 194 |
+
f"over {passes} passes")
|
| 195 |
+
|
| 196 |
+
# ---- latencies: pointer chase per level. Footprints are chosen so
|
| 197 |
+
# the chain hits the intended level: the L2 ring (2 MB) exceeds L1,
|
| 198 |
+
# fits L2 on every supported part, and spans few enough pages to stay
|
| 199 |
+
# TLB-resident after a full-cycle warm walk; the HBM chain is long
|
| 200 |
+
# enough (2x L2 in touched lines) that it evicts itself and cannot be
|
| 201 |
+
# served from cache.
|
| 202 |
+
lat = {}
|
| 203 |
+
idle_clk = measure_clock(device, ticks=2_000_000, blocks=1)
|
| 204 |
+
ring_s = _sattolo_ring(8192, device)
|
| 205 |
+
ops.mk_chase_shared(ring_s, 1 << 14, out64)
|
| 206 |
+
_sync()
|
| 207 |
+
hops = 1 << 16
|
| 208 |
+
ops.mk_chase_shared(ring_s, hops, out64)
|
| 209 |
+
_sync()
|
| 210 |
+
lat["smem_cycles"] = round(int(out64[0].item()) / hops, 1)
|
| 211 |
+
|
| 212 |
+
l1_ring_n = (16 << 10) // 4 # 16 KB: L1-resident after a warm walk
|
| 213 |
+
ring_l1 = _sattolo_ring(l1_ring_n, device)
|
| 214 |
+
ops.mk_chase_global(ring_l1, l1_ring_n, out64)
|
| 215 |
+
_sync()
|
| 216 |
+
hops_l1 = 1 << 15
|
| 217 |
+
ops.mk_chase_global(ring_l1, hops_l1, out64)
|
| 218 |
+
_sync()
|
| 219 |
+
lat["l1_cycles"] = round(int(out64[0].item()) / hops_l1, 1)
|
| 220 |
+
|
| 221 |
+
l2_ring_n = (2 << 20) // 4
|
| 222 |
+
ring_l2 = _sattolo_ring(l2_ring_n, device)
|
| 223 |
+
ops.mk_chase_global(ring_l2, l2_ring_n, out64) # full-cycle warm walk
|
| 224 |
+
_sync()
|
| 225 |
+
hops_l2 = 1 << 14
|
| 226 |
+
ops.mk_chase_global(ring_l2, hops_l2, out64)
|
| 227 |
+
_sync()
|
| 228 |
+
lat["l2_cycles"] = round(int(out64[0].item()) / hops_l2, 1)
|
| 229 |
+
|
| 230 |
+
hbm_bytes = min(max(4 * l2, 64 << 20), props.total_memory // 8)
|
| 231 |
+
ring_h = _sattolo_ring(int(hbm_bytes) // 4, device)
|
| 232 |
+
hops_h = max((2 * l2) // 128, 1 << 17)
|
| 233 |
+
ops.mk_chase_global(ring_h, 1 << 14, out64) # fault pages / warm TLB
|
| 234 |
+
_sync()
|
| 235 |
+
ops.mk_chase_global(ring_h, hops_h, out64)
|
| 236 |
+
_sync()
|
| 237 |
+
lat["hbm_cycles"] = round(int(out64[0].item()) / hops_h, 1)
|
| 238 |
+
lat["ns_per_cycle"] = round(1.0 / idle_clk, 3)
|
| 239 |
+
d["latency"] = _entry(lat, "cycles (single-lane dependent loads)",
|
| 240 |
+
"Sattolo-cycle pointer chase; 16 KB ring for L1, "
|
| 241 |
+
"2 MB TLB-resident ring for L2, self-evicting "
|
| 242 |
+
"chain (2x L2 in touched lines) for HBM",
|
| 243 |
+
idle_clk, "high",
|
| 244 |
+
"smem includes index arithmetic; HBM includes "
|
| 245 |
+
"residual TLB effects at large footprints")
|
| 246 |
+
del ring_h, ring_l2, ring_l1, ring_s
|
| 247 |
+
|
| 248 |
+
# ---- fp32 FMA and tensor-core throughput
|
| 249 |
+
blocks = sm * 4
|
| 250 |
+
iters = 20_000 if quick else 60_000
|
| 251 |
+
ops.mk_fma_f32(blocks, 256, iters, sink)
|
| 252 |
+
ms = _median(_events(lambda: ops.mk_fma_f32(blocks, 256, iters, sink),
|
| 253 |
+
reps))
|
| 254 |
+
flops = blocks * 256 * iters * 16 * 2 * 2 # 2 chains x 2 flops x unroll
|
| 255 |
+
d["fma_f32"] = _entry(round(flops / (ms * 1e-3) / 1e12, 2), "TFLOP/s",
|
| 256 |
+
"register-resident FMA chains", clk, "high")
|
| 257 |
+
sink64 = torch.zeros(2, dtype=torch.float64, device=device)
|
| 258 |
+
it64 = max(iters // 16, 1000)
|
| 259 |
+
ops.mk_fma_f64(blocks, 256, it64, sink64)
|
| 260 |
+
ms = _median(_events(
|
| 261 |
+
lambda: ops.mk_fma_f64(blocks, 256, it64, sink64), reps))
|
| 262 |
+
flops64 = blocks * 256 * it64 * 16 * 2 * 2
|
| 263 |
+
d["fma_f64"] = _entry(round(flops64 / (ms * 1e-3) / 1e12, 3), "TFLOP/s",
|
| 264 |
+
"register-resident double FMA chains (the DFMA "
|
| 265 |
+
"pipe)", clk, "high")
|
| 266 |
+
mma = {}
|
| 267 |
+
for name, (kind, mnk) in _MMA_KINDS.items():
|
| 268 |
+
it = 30_000 if quick else 100_000
|
| 269 |
+
r = ops.mk_mma(kind, blocks, 8, 512, sink)
|
| 270 |
+
if r != 0:
|
| 271 |
+
mma[name] = "unsupported"
|
| 272 |
+
continue
|
| 273 |
+
_sync()
|
| 274 |
+
ms = _median(_events(
|
| 275 |
+
lambda k=kind, i=it: ops.mk_mma(k, blocks, 8, i, sink), reps))
|
| 276 |
+
flops = blocks * 8 * it * 4 * 2 * mnk
|
| 277 |
+
mma[name] = round(flops / (ms * 1e-3) / 1e12, 1)
|
| 278 |
+
d["mma"] = _entry(mma, "TFLOP/s (TOP/s for int8)",
|
| 279 |
+
"register-fragment mma_sync loops (wmma; fp8 via "
|
| 280 |
+
"inline PTX m16n8k32)", clk, "medium",
|
| 281 |
+
"dense-tile rate without memory traffic; real "
|
| 282 |
+
"kernels see this only when staging hides behind it")
|
| 283 |
+
|
| 284 |
+
# ---- atomic contention curve: throughput vs number of distinct
|
| 285 |
+
# addresses the whole grid hammers (slots = 1 is full contention;
|
| 286 |
+
# slots = threads is contention-free), plus the shared-memory point
|
| 287 |
+
n_thr = blocks * 256
|
| 288 |
+
buf = torch.zeros(n_thr, dtype=torch.float32, device=device)
|
| 289 |
+
at = {"curve_slots": {}}
|
| 290 |
+
it_at = 2_000 if quick else 6_000
|
| 291 |
+
slot_points = [1, 8, 64, 512, 4096, n_thr]
|
| 292 |
+
for slots in slot_points:
|
| 293 |
+
buf.zero_()
|
| 294 |
+
ops.mk_atomics(1, blocks, 256, it_at, buf, slots)
|
| 295 |
+
ms = _median(_events(
|
| 296 |
+
lambda s=slots: ops.mk_atomics(1, blocks, 256, it_at, buf, s),
|
| 297 |
+
reps))
|
| 298 |
+
at["curve_slots"][str(slots)] = round(
|
| 299 |
+
n_thr * it_at / (ms * 1e-3) / 1e9, 2)
|
| 300 |
+
ops.mk_atomics(3, blocks, 256, it_at, buf, 1)
|
| 301 |
+
ms = _median(_events(
|
| 302 |
+
lambda: ops.mk_atomics(3, blocks, 256, it_at, buf, 1), reps))
|
| 303 |
+
at["shared_same_addr"] = round(n_thr * it_at / (ms * 1e-3) / 1e9, 2)
|
| 304 |
+
d["atomics_f32"] = _entry(at, "Gop/s",
|
| 305 |
+
"atomicAdd hammer; curve sweeps the number "
|
| 306 |
+
"of distinct global addresses from 1 (full "
|
| 307 |
+
"contention) to one per thread", clk, "high")
|
| 308 |
+
|
| 309 |
+
# ---- launch overhead
|
| 310 |
+
anchor = sink
|
| 311 |
+
n_launch = 500 if quick else 2000
|
| 312 |
+
_sync()
|
| 313 |
+
t0 = time.perf_counter()
|
| 314 |
+
for _ in range(n_launch):
|
| 315 |
+
ops.mk_empty(anchor)
|
| 316 |
+
_sync()
|
| 317 |
+
queued = (time.perf_counter() - t0) / n_launch * 1e6
|
| 318 |
+
n_rt = 100 if quick else 300
|
| 319 |
+
t0 = time.perf_counter()
|
| 320 |
+
for _ in range(n_rt):
|
| 321 |
+
ops.mk_empty(anchor)
|
| 322 |
+
_sync()
|
| 323 |
+
rt = (time.perf_counter() - t0) / n_rt * 1e6
|
| 324 |
+
d["launch_overhead"] = _entry(
|
| 325 |
+
{"queued_us": round(queued, 2), "round_trip_us": round(rt, 2)},
|
| 326 |
+
"us", "empty-kernel trains, queued vs launch+sync", clk, "high",
|
| 327 |
+
"host-dependent: includes torch op dispatch")
|
| 328 |
+
|
| 329 |
+
# ---- grid barrier
|
| 330 |
+
bar_iters = 500 if quick else 2000
|
| 331 |
+
r = ops.mk_barrier(sm, 256, bar_iters, out64)
|
| 332 |
+
if r == 0:
|
| 333 |
+
_sync()
|
| 334 |
+
ticks = int(out64[0].item())
|
| 335 |
+
d["grid_barrier"] = _entry(
|
| 336 |
+
round(ticks / bar_iters / clk / 1000.0, 2), "us per grid.sync",
|
| 337 |
+
f"cooperative kernel, {sm} blocks x 256 threads", clk, "high")
|
| 338 |
+
else:
|
| 339 |
+
d["grid_barrier"] = _entry("unsupported", "", "cooperative launch "
|
| 340 |
+
"unavailable or grid too large", clk,
|
| 341 |
+
"high")
|
| 342 |
+
|
| 343 |
+
# ---- occupancy table (API) plus timed knees: the same random gather
|
| 344 |
+
# at every (threads, dyn_smem) point; when the forced blocks/SM stop
|
| 345 |
+
# covering memory latency, achieved bandwidth falls off the cliff
|
| 346 |
+
occ = {}
|
| 347 |
+
for threads in (128, 256, 512, 1024):
|
| 348 |
+
row = {}
|
| 349 |
+
for smem_kb in (0, 16, 32, 48):
|
| 350 |
+
row[f"{smem_kb}KB"] = int(ops.mk_occupancy(
|
| 351 |
+
threads, smem_kb * 1024, anchor))
|
| 352 |
+
occ[f"t{threads}"] = row
|
| 353 |
+
d["occupancy_blocks_per_sm"] = _entry(
|
| 354 |
+
occ, "blocks/SM", "occupancy API on the float4 triad kernel",
|
| 355 |
+
clk, "high", "API-derived, not timing-derived")
|
| 356 |
+
|
| 357 |
+
knee_iters = 6_000 if quick else 20_000
|
| 358 |
+
knee_blocks = sm * 8
|
| 359 |
+
knees = {}
|
| 360 |
+
for threads in (128, 256, 1024):
|
| 361 |
+
row = {}
|
| 362 |
+
for smem_kb in (0, 32, 48):
|
| 363 |
+
ops.mk_fma_dep(knee_blocks, threads, smem_kb * 1024,
|
| 364 |
+
knee_iters, sink)
|
| 365 |
+
ms = _median(_events(
|
| 366 |
+
lambda t=threads, s=smem_kb: ops.mk_fma_dep(
|
| 367 |
+
knee_blocks, t, s * 1024, knee_iters, sink), reps))
|
| 368 |
+
fl = knee_blocks * threads * knee_iters * 16 * 2
|
| 369 |
+
row[f"{smem_kb}KB"] = round(fl / (ms * 1e-3) / 1e12, 2)
|
| 370 |
+
knees[f"t{threads}"] = row
|
| 371 |
+
d["occupancy_knees_fma_tflops"] = _entry(
|
| 372 |
+
knees, "TFLOP/s", "a single dependent FMA chain per thread, timed "
|
| 373 |
+
"at forced (threads, dyn smem) occupancies over fixed total work; "
|
| 374 |
+
"throughput is proportional to resident threads until the pipe "
|
| 375 |
+
"saturates, so the cliff marks where occupancy stops covering "
|
| 376 |
+
"latency", clk, "high",
|
| 377 |
+
"timing-derived counterpart to the API table above")
|
| 378 |
+
return d
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def bench(fn, iters=100, warmup=10, bytes=None, flops=None, dossier=None,
|
| 382 |
+
dtype="fp16", clock_guard=0.97, clock_stride=8, graph=False,
|
| 383 |
+
device="cuda"):
|
| 384 |
+
"""Median-of-N cudaEvent timing with throttle rejection.
|
| 385 |
+
|
| 386 |
+
The SM clock is probed every `clock_stride` samples; samples inside a
|
| 387 |
+
window whose clock sagged below `clock_guard` x the best observed
|
| 388 |
+
clock are rejected and counted rather than silently averaged. With
|
| 389 |
+
declared `bytes`/`flops` per call, reports achieved GB/s / TFLOP/s;
|
| 390 |
+
with a `dossier`, adds roofline placement against the measured peaks.
|
| 391 |
+
|
| 392 |
+
`graph=True` captures fn() into a CUDA graph and times graph replays,
|
| 393 |
+
stripping Python and torch dispatch from the measurement: the right
|
| 394 |
+
mode for kernels in the few-microsecond range, where dispatch is
|
| 395 |
+
otherwise the thing being measured. Capture requires fn to be
|
| 396 |
+
graph-safe (no sync, no allocation growth, static shapes)."""
|
| 397 |
+
for _ in range(warmup):
|
| 398 |
+
fn()
|
| 399 |
+
_sync()
|
| 400 |
+
if graph:
|
| 401 |
+
g = torch.cuda.CUDAGraph()
|
| 402 |
+
with torch.cuda.graph(g):
|
| 403 |
+
fn()
|
| 404 |
+
fn = g.replay
|
| 405 |
+
for _ in range(warmup):
|
| 406 |
+
fn()
|
| 407 |
+
_sync()
|
| 408 |
+
samples = []
|
| 409 |
+
clocks = []
|
| 410 |
+
stride = clock_stride
|
| 411 |
+
for base in range(0, iters, stride):
|
| 412 |
+
clocks.append(measure_clock(device, ticks=1_500_000))
|
| 413 |
+
n = min(stride, iters - base)
|
| 414 |
+
samples.append((_events(fn, n), len(clocks) - 1))
|
| 415 |
+
clocks.append(measure_clock(device, ticks=1_500_000))
|
| 416 |
+
best_clk = max(clocks)
|
| 417 |
+
kept, rejected = [], 0
|
| 418 |
+
flat = []
|
| 419 |
+
for group, ci in samples:
|
| 420 |
+
window_ok = (clocks[ci] >= clock_guard * best_clk and
|
| 421 |
+
clocks[min(ci + 1, len(clocks) - 1)] >=
|
| 422 |
+
clock_guard * best_clk)
|
| 423 |
+
for ms in group:
|
| 424 |
+
flat.append(ms)
|
| 425 |
+
if window_ok:
|
| 426 |
+
kept.append(ms)
|
| 427 |
+
else:
|
| 428 |
+
rejected += 1
|
| 429 |
+
if not kept:
|
| 430 |
+
kept = flat
|
| 431 |
+
med = _median(kept)
|
| 432 |
+
rep = {
|
| 433 |
+
"median_ms": round(med, 4),
|
| 434 |
+
"min_ms": round(min(kept), 4),
|
| 435 |
+
"mean_ms": round(sum(kept) / len(kept), 4),
|
| 436 |
+
"samples": len(flat),
|
| 437 |
+
"rejected_throttled": rejected,
|
| 438 |
+
"graph_captured": bool(graph),
|
| 439 |
+
"clock_ghz": {"best": round(best_clk, 3),
|
| 440 |
+
"worst": round(min(clocks), 3)},
|
| 441 |
+
}
|
| 442 |
+
if bytes is not None:
|
| 443 |
+
rep["achieved_gbs"] = round(bytes / (med * 1e-3) / 1e9, 1)
|
| 444 |
+
if flops is not None:
|
| 445 |
+
rep["achieved_tflops"] = round(flops / (med * 1e-3) / 1e12, 2)
|
| 446 |
+
if bytes and flops:
|
| 447 |
+
rep["arithmetic_intensity"] = round(flops / bytes, 2)
|
| 448 |
+
if dossier is not None and bytes and flops:
|
| 449 |
+
bw = max(dossier["hbm_bw"]["value"].values()) * 1e9
|
| 450 |
+
mma_v = dossier["mma"]["value"].get(dtype)
|
| 451 |
+
peak = (mma_v if isinstance(mma_v, (int, float))
|
| 452 |
+
else dossier["fma_f32"]["value"]) * 1e12
|
| 453 |
+
ai = flops / bytes
|
| 454 |
+
roof = min(peak, ai * bw)
|
| 455 |
+
rep["roofline"] = {
|
| 456 |
+
"bound": "memory" if ai * bw < peak else "compute",
|
| 457 |
+
"ridge_flops_per_byte": round(peak / bw, 1),
|
| 458 |
+
"pct_of_roof": round(100 * (flops / (med * 1e-3)) / roof, 1),
|
| 459 |
+
"peak_dtype": dtype,
|
| 460 |
+
}
|
| 461 |
+
return rep
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
def _to_ordered_int(t):
|
| 465 |
+
"""Map float bit patterns to integers whose difference is the ULP
|
| 466 |
+
distance: positive floats keep their bits, negative floats mirror
|
| 467 |
+
below zero, so the mapping is monotone in the float value."""
|
| 468 |
+
if t.dtype == torch.float32:
|
| 469 |
+
u = t.view(torch.int32).to(torch.int64) & 0xFFFFFFFF
|
| 470 |
+
half = 1 << 31
|
| 471 |
+
elif t.dtype in (torch.float16, torch.bfloat16):
|
| 472 |
+
u = t.view(torch.int16).to(torch.int64) & 0xFFFF
|
| 473 |
+
half = 1 << 15
|
| 474 |
+
elif t.dtype == torch.float64:
|
| 475 |
+
i = t.view(torch.int64)
|
| 476 |
+
return torch.where(i >= 0, i, (-(1 << 63)) - i)
|
| 477 |
+
else:
|
| 478 |
+
raise ValueError(f"unsupported dtype {t.dtype}")
|
| 479 |
+
return torch.where(u < half, u, half - u)
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
def ulp_diff(a, b):
|
| 483 |
+
"""Elementwise ULP distance between same-dtype float tensors. NaNs in
|
| 484 |
+
the same position count as 0; a one-sided NaN counts as 2**62."""
|
| 485 |
+
assert a.dtype == b.dtype and a.shape == b.shape
|
| 486 |
+
ia, ib = _to_ordered_int(a), _to_ordered_int(b)
|
| 487 |
+
d = (ia - ib).abs()
|
| 488 |
+
both_nan = torch.isnan(a) & torch.isnan(b)
|
| 489 |
+
one_nan = torch.isnan(a) ^ torch.isnan(b)
|
| 490 |
+
d = torch.where(both_nan, torch.zeros_like(d), d)
|
| 491 |
+
d = torch.where(one_nan, torch.full_like(d, 1 << 62), d)
|
| 492 |
+
return d
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
class order_shuffles:
|
| 496 |
+
"""Built-in order shuffles for the common contractual-noise axes."""
|
| 497 |
+
|
| 498 |
+
@staticmethod
|
| 499 |
+
def last_dim():
|
| 500 |
+
"""Permute the last dim of every tensor arg identically: the
|
| 501 |
+
accumulation order of a last-dim reduction changes, the value
|
| 502 |
+
does not."""
|
| 503 |
+
def shuffle(args, seed):
|
| 504 |
+
g = torch.Generator(device=args[0].device).manual_seed(seed)
|
| 505 |
+
p = torch.randperm(args[0].shape[-1], device=args[0].device,
|
| 506 |
+
generator=g)
|
| 507 |
+
return tuple(x[..., p] for x in args)
|
| 508 |
+
return shuffle
|
| 509 |
+
|
| 510 |
+
@staticmethod
|
| 511 |
+
def matmul_k():
|
| 512 |
+
"""Permute K of (A [.., M, K], B [.., K, N]) consistently: the
|
| 513 |
+
product is unchanged, the accumulation order is not."""
|
| 514 |
+
def shuffle(args, seed):
|
| 515 |
+
a_m, b_m = args[0], args[1]
|
| 516 |
+
g = torch.Generator(device=a_m.device).manual_seed(seed)
|
| 517 |
+
p = torch.randperm(a_m.shape[-1], device=a_m.device,
|
| 518 |
+
generator=g)
|
| 519 |
+
return (a_m[..., p], b_m[..., p, :]) + tuple(args[2:])
|
| 520 |
+
return shuffle
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
def _as_outputs(x):
|
| 524 |
+
return list(x) if isinstance(x, (tuple, list)) else [x]
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
def compare(ref, cand, gen, trials=8, order_shuffle=None, order_trials=8,
|
| 528 |
+
ulp_tol=None, atol=None, rtol=None, band_margin=4.0, seed=0):
|
| 529 |
+
"""Adversarial correctness comparison with ULP evidence.
|
| 530 |
+
|
| 531 |
+
`gen(seed) -> args` builds inputs; `ref(*args)` and `cand(*args)`
|
| 532 |
+
return a tensor or a tuple of tensors (each output is compared and
|
| 533 |
+
the worst governs). If `order_shuffle(args, seed) -> args` is given
|
| 534 |
+
and reorders contractual-noise dimensions (see `order_shuffles` for
|
| 535 |
+
the common cases), the reference is treated as a distribution:
|
| 536 |
+
candidates are judged against the measured reduction-order band
|
| 537 |
+
rather than a point. Verdict, stated in the report: max ULP <=
|
| 538 |
+
band_margin * band + 2 when a band is measured, else max ULP <=
|
| 539 |
+
ulp_tol (default 2); if `atol`/`rtol` are given they are an
|
| 540 |
+
alternative acceptance route, checked as |c - r| <= atol + rtol|r|."""
|
| 541 |
+
worst = {"max_ulp": -1}
|
| 542 |
+
total_hist = {}
|
| 543 |
+
per_trial = []
|
| 544 |
+
max_abs_seen = 0.0
|
| 545 |
+
max_rel_seen = 0.0
|
| 546 |
+
for t in range(trials):
|
| 547 |
+
args = gen(seed + t)
|
| 548 |
+
r_outs = _as_outputs(ref(*args))
|
| 549 |
+
c_outs = _as_outputs(cand(*args))
|
| 550 |
+
assert len(r_outs) == len(c_outs), "output arity mismatch"
|
| 551 |
+
# the order band is POINTWISE: a near-zero reduction output is
|
| 552 |
+
# legitimately thousands of ULP order-sensitive (cancellation)
|
| 553 |
+
# while its neighbors are stable, so each element is judged
|
| 554 |
+
# against its own measured sensitivity, not a global max
|
| 555 |
+
bands = None
|
| 556 |
+
band = 0
|
| 557 |
+
band_score = 0.0
|
| 558 |
+
if order_shuffle is not None:
|
| 559 |
+
bands = [torch.zeros_like(ro, dtype=torch.int64)
|
| 560 |
+
for ro in r_outs]
|
| 561 |
+
for k in range(order_trials):
|
| 562 |
+
r2 = _as_outputs(
|
| 563 |
+
ref(*order_shuffle(args, 1000 + seed + t * 131 + k)))
|
| 564 |
+
for bi, (ro, r2o) in enumerate(zip(r_outs, r2)):
|
| 565 |
+
bands[bi] = torch.maximum(bands[bi],
|
| 566 |
+
ulp_diff(ro, r2o))
|
| 567 |
+
band = max(int(b.max().item()) if b.numel() else 0
|
| 568 |
+
for b in bands)
|
| 569 |
+
max_ulp = 0
|
| 570 |
+
mean_ulp = 0.0
|
| 571 |
+
first_div = -1
|
| 572 |
+
n_el = 0
|
| 573 |
+
nan_r = nan_c = inf_r = inf_c = 0
|
| 574 |
+
for oi, (r, c) in enumerate(zip(r_outs, c_outs)):
|
| 575 |
+
d = ulp_diff(r, c)
|
| 576 |
+
if bands is not None and d.numel():
|
| 577 |
+
ratio = d.double() / (bands[oi].double() + 2.0)
|
| 578 |
+
band_score = max(band_score, float(ratio.max().item()))
|
| 579 |
+
if d.numel():
|
| 580 |
+
max_ulp = max(max_ulp, int(d.max().item()))
|
| 581 |
+
mean_ulp += float(d.double().sum().item())
|
| 582 |
+
n_el += d.numel()
|
| 583 |
+
nz = (d > 0)
|
| 584 |
+
if first_div < 0 and nz.any():
|
| 585 |
+
first_div = int(nz.flatten().nonzero()[0].item())
|
| 586 |
+
if nz.any():
|
| 587 |
+
buckets = torch.clamp(
|
| 588 |
+
torch.log2(d[nz].double()).floor(), 0, 62
|
| 589 |
+
).to(torch.int64)
|
| 590 |
+
counts = torch.bincount(buckets, minlength=63)
|
| 591 |
+
for bkt in counts.nonzero().flatten().tolist():
|
| 592 |
+
total_hist[bkt] = total_hist.get(bkt, 0) + int(
|
| 593 |
+
counts[bkt].item())
|
| 594 |
+
fin = torch.isfinite(r) & torch.isfinite(c)
|
| 595 |
+
if fin.any():
|
| 596 |
+
diff = (c[fin].double() - r[fin].double()).abs()
|
| 597 |
+
max_abs_seen = max(max_abs_seen,
|
| 598 |
+
float(diff.max().item()))
|
| 599 |
+
rel = diff / r[fin].double().abs().clamp(min=1e-300)
|
| 600 |
+
max_rel_seen = max(max_rel_seen,
|
| 601 |
+
float(rel.max().item()))
|
| 602 |
+
nan_r += int(torch.isnan(r).sum().item())
|
| 603 |
+
nan_c += int(torch.isnan(c).sum().item())
|
| 604 |
+
inf_r += int(torch.isinf(r).sum().item())
|
| 605 |
+
inf_c += int(torch.isinf(c).sum().item())
|
| 606 |
+
trial_rep = {
|
| 607 |
+
"max_ulp": max_ulp,
|
| 608 |
+
"mean_ulp": round(mean_ulp / max(n_el, 1), 3),
|
| 609 |
+
"first_divergent_index": first_div,
|
| 610 |
+
"nan_ref": nan_r, "nan_cand": nan_c,
|
| 611 |
+
"inf_ref": inf_r, "inf_cand": inf_c,
|
| 612 |
+
"order_band_ulp": band,
|
| 613 |
+
"band_score": round(band_score, 2),
|
| 614 |
+
}
|
| 615 |
+
per_trial.append(trial_rep)
|
| 616 |
+
if max_ulp > worst["max_ulp"]:
|
| 617 |
+
worst = trial_rep
|
| 618 |
+
band_max = max(t["order_band_ulp"] for t in per_trial)
|
| 619 |
+
max_seen = max(t["max_ulp"] for t in per_trial)
|
| 620 |
+
if order_shuffle is not None:
|
| 621 |
+
score = max(t["band_score"] for t in per_trial)
|
| 622 |
+
ulp_ok = score <= band_margin
|
| 623 |
+
criterion = (f"pointwise max(ulp / (order_band + 2)) = "
|
| 624 |
+
f"{score:.2f} <= band_margin {band_margin}")
|
| 625 |
+
else:
|
| 626 |
+
tol = ulp_tol if ulp_tol is not None else 2
|
| 627 |
+
ulp_ok = max_seen <= tol
|
| 628 |
+
criterion = f"max_ulp {max_seen} <= ulp_tol {tol}"
|
| 629 |
+
tol_ok = False
|
| 630 |
+
if atol is not None or rtol is not None:
|
| 631 |
+
tol_ok = max_abs_seen <= (atol or 0) + (rtol or 0) * 1.0 or \
|
| 632 |
+
max_rel_seen <= (rtol or 0) or max_abs_seen <= (atol or 0)
|
| 633 |
+
criterion += (f"; OR max_abs {max_abs_seen:.3e} <= atol {atol} / "
|
| 634 |
+
f"max_rel {max_rel_seen:.3e} <= rtol {rtol}")
|
| 635 |
+
return {
|
| 636 |
+
"pass": bool(ulp_ok or tol_ok),
|
| 637 |
+
"criterion": criterion,
|
| 638 |
+
"worst_trial": worst,
|
| 639 |
+
"max_abs": max_abs_seen,
|
| 640 |
+
"max_rel": max_rel_seen,
|
| 641 |
+
"trials": per_trial,
|
| 642 |
+
"ulp_hist_log2": dict(sorted(total_hist.items())),
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
|
| 646 |
+
_FUZZ_VARIANTS = ("contig", "transposed", "strided_rows", "offset_slice",
|
| 647 |
+
"overflow_expand")
|
| 648 |
+
|
| 649 |
+
|
| 650 |
+
def _context_alive(device):
|
| 651 |
+
try:
|
| 652 |
+
t = torch.ones(4, device=device)
|
| 653 |
+
torch.cuda.synchronize()
|
| 654 |
+
return bool(t.sum().item() == 4.0)
|
| 655 |
+
except Exception: # noqa: BLE001
|
| 656 |
+
return False
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
def fuzz_shapes(op, ref, make, shapes=None, variants=_FUZZ_VARIANTS,
|
| 660 |
+
dtypes=(torch.float32,), ulp_tol=64, device="cuda"):
|
| 661 |
+
"""The ugly envelope: run op vs ref across edge shapes, dtypes, and
|
| 662 |
+
layout mutations; report ok / mismatch / exception per case.
|
| 663 |
+
|
| 664 |
+
`make(shape, dtype)` (or `make(shape)`) builds a base input, either a
|
| 665 |
+
tensor or a tuple of tensors for multi-argument ops; layout variants
|
| 666 |
+
derive non-contiguous and misaligned views whose CONTENT equals a
|
| 667 |
+
contiguous copy, so `op(*xs)` must match `ref(*xs)` on every variant.
|
| 668 |
+
`overflow_expand` stride-0-expands the input past 2**31 logical
|
| 669 |
+
elements to catch int32 indexing, and is skipped with status
|
| 670 |
+
`skipped_vram` when materializing the output would not fit. After any
|
| 671 |
+
exception the CUDA context is health-checked; if a candidate killed
|
| 672 |
+
it, remaining cases are marked `context_lost` instead of producing
|
| 673 |
+
garbage."""
|
| 674 |
+
if shapes is None:
|
| 675 |
+
shapes = [(0, 8), (1, 1), (1, 17), (3, 63), (17, 129), (128, 128),
|
| 676 |
+
(1023, 5), (2, 4096)]
|
| 677 |
+
|
| 678 |
+
def build(shape, dtype):
|
| 679 |
+
try:
|
| 680 |
+
out = make(tuple(shape), dtype)
|
| 681 |
+
except TypeError:
|
| 682 |
+
out = make(tuple(shape))
|
| 683 |
+
return out if isinstance(out, (tuple, list)) else (out,)
|
| 684 |
+
|
| 685 |
+
def variant_of(x, v):
|
| 686 |
+
if v == "contig":
|
| 687 |
+
return x
|
| 688 |
+
if v == "transposed":
|
| 689 |
+
return x.t() if x.dim() == 2 else x
|
| 690 |
+
if v == "strided_rows":
|
| 691 |
+
return x[::2] if x.shape[0] >= 2 else x
|
| 692 |
+
if v == "offset_slice":
|
| 693 |
+
# a storage-offset-1 view: misaligned for vectorized loads
|
| 694 |
+
return x.flatten()[1:] if x.numel() > 1 else x
|
| 695 |
+
if v == "overflow_expand":
|
| 696 |
+
if x.numel() == 0:
|
| 697 |
+
return None
|
| 698 |
+
per = max(x.numel(), 1)
|
| 699 |
+
rep = (1 << 31) // per + 2
|
| 700 |
+
return x.reshape((1,) + tuple(x.shape)).expand(
|
| 701 |
+
(rep,) + tuple(x.shape))
|
| 702 |
+
return x
|
| 703 |
+
|
| 704 |
+
rows = []
|
| 705 |
+
lost = False
|
| 706 |
+
for dtype in dtypes:
|
| 707 |
+
for shape in shapes:
|
| 708 |
+
bases = build(shape, dtype)
|
| 709 |
+
for v in variants:
|
| 710 |
+
case = {"shape": tuple(shape), "variant": v,
|
| 711 |
+
"dtype": str(dtype).replace("torch.", "")}
|
| 712 |
+
if lost:
|
| 713 |
+
case["status"] = "context_lost"
|
| 714 |
+
rows.append(case)
|
| 715 |
+
continue
|
| 716 |
+
xs = [variant_of(b, v) for b in bases]
|
| 717 |
+
if any(x is None for x in xs):
|
| 718 |
+
case["status"] = "skipped_empty"
|
| 719 |
+
rows.append(case)
|
| 720 |
+
continue
|
| 721 |
+
if v == "overflow_expand":
|
| 722 |
+
logical = max(x.numel() for x in xs)
|
| 723 |
+
need = logical * xs[0].element_size() * 2
|
| 724 |
+
free, _total = torch.cuda.mem_get_info()
|
| 725 |
+
if need > free * 0.8:
|
| 726 |
+
case["status"] = "skipped_vram"
|
| 727 |
+
case["logical_elems"] = int(logical)
|
| 728 |
+
rows.append(case)
|
| 729 |
+
continue
|
| 730 |
+
case["logical_elems"] = int(logical)
|
| 731 |
+
case["contiguous"] = bool(xs[0].is_contiguous())
|
| 732 |
+
try:
|
| 733 |
+
got = op(*xs)
|
| 734 |
+
want = ref(*xs)
|
| 735 |
+
if v == "overflow_expand":
|
| 736 |
+
# bounded comparison: the head plus the high-index
|
| 737 |
+
# tail, which is where int32 flat indexing wraps
|
| 738 |
+
w = 1 << 20
|
| 739 |
+
gf, wf = got.flatten(), want.flatten()
|
| 740 |
+
pairs = ((gf[:w], wf[:w]), (gf[-w:], wf[-w:]))
|
| 741 |
+
else:
|
| 742 |
+
pairs = ((got, want),)
|
| 743 |
+
mx = 0
|
| 744 |
+
for gp, wp in pairs:
|
| 745 |
+
d = ulp_diff(wp, gp) if gp.is_floating_point() \
|
| 746 |
+
else (wp != gp).to(torch.int64) * (1 << 62)
|
| 747 |
+
if d.numel():
|
| 748 |
+
mx = max(mx, int(d.max().item()))
|
| 749 |
+
del d
|
| 750 |
+
case["status"] = "ok" if mx <= ulp_tol else "mismatch"
|
| 751 |
+
case["max_ulp"] = mx
|
| 752 |
+
del got, want
|
| 753 |
+
except Exception as e: # noqa: BLE001 - report, don't die
|
| 754 |
+
case["status"] = "exception"
|
| 755 |
+
case["error"] = f"{type(e).__name__}: {str(e)[:120]}"
|
| 756 |
+
if not _context_alive(device):
|
| 757 |
+
lost = True
|
| 758 |
+
case["status"] = "context_killed"
|
| 759 |
+
rows.append(case)
|
| 760 |
+
n_bad = sum(1 for r in rows
|
| 761 |
+
if r["status"] not in ("ok", "skipped_vram",
|
| 762 |
+
"skipped_empty"))
|
| 763 |
+
return {"cases": rows, "failed": n_bad, "total": len(rows),
|
| 764 |
+
"context_lost": lost}
|
| 765 |
+
|
| 766 |
+
|
| 767 |
+
def sweep(factory, grid, iters=30, warmup=5, bytes=None, flops=None,
|
| 768 |
+
check=None, budget_s=None):
|
| 769 |
+
"""Launch-configuration search: `factory(**point) -> callable`; one
|
| 770 |
+
bench row per grid point, sorted fastest first. A point whose factory
|
| 771 |
+
or run raises is reported, not skipped silently. `check(fn) -> bool`
|
| 772 |
+
is an optional correctness gate recorded per row (a fast wrong config
|
| 773 |
+
must not win a sweep). `budget_s` caps total sweep time; points not
|
| 774 |
+
reached are reported as `not_run` rather than silently dropped."""
|
| 775 |
+
keys = list(grid.keys())
|
| 776 |
+
|
| 777 |
+
def points(i, acc):
|
| 778 |
+
if i == len(keys):
|
| 779 |
+
yield dict(acc)
|
| 780 |
+
return
|
| 781 |
+
for v in grid[keys[i]]:
|
| 782 |
+
acc[keys[i]] = v
|
| 783 |
+
yield from points(i + 1, acc)
|
| 784 |
+
|
| 785 |
+
rows = []
|
| 786 |
+
t_start = time.perf_counter()
|
| 787 |
+
over_budget = False
|
| 788 |
+
for p in points(0, {}):
|
| 789 |
+
row = dict(p)
|
| 790 |
+
if over_budget or (budget_s is not None and
|
| 791 |
+
time.perf_counter() - t_start > budget_s):
|
| 792 |
+
over_budget = True
|
| 793 |
+
row["status"] = "not_run"
|
| 794 |
+
row["median_ms"] = float("inf")
|
| 795 |
+
rows.append(row)
|
| 796 |
+
continue
|
| 797 |
+
try:
|
| 798 |
+
fn = factory(**p)
|
| 799 |
+
if check is not None:
|
| 800 |
+
row["check"] = bool(check(fn))
|
| 801 |
+
rep = bench(fn, iters=iters, warmup=warmup, bytes=bytes,
|
| 802 |
+
flops=flops)
|
| 803 |
+
row.update({"median_ms": rep["median_ms"],
|
| 804 |
+
"rejected_throttled": rep["rejected_throttled"]})
|
| 805 |
+
if "achieved_gbs" in rep:
|
| 806 |
+
row["achieved_gbs"] = rep["achieved_gbs"]
|
| 807 |
+
if "achieved_tflops" in rep:
|
| 808 |
+
row["achieved_tflops"] = rep["achieved_tflops"]
|
| 809 |
+
except Exception as e: # noqa: BLE001
|
| 810 |
+
row["error"] = f"{type(e).__name__}: {str(e)[:120]}"
|
| 811 |
+
row["median_ms"] = float("inf")
|
| 812 |
+
rows.append(row)
|
| 813 |
+
rows.sort(key=lambda r: (not r.get("check", True), r["median_ms"]))
|
| 814 |
+
return rows
|
| 815 |
+
|
| 816 |
+
|
| 817 |
+
def alloc_stamps(n, device="cuda"):
|
| 818 |
+
"""Device buffer for the MK_STAMP convention (metakernel_cuda/
|
| 819 |
+
stamps.cuh): the candidate kernel writes clock64() into slot i."""
|
| 820 |
+
return torch.zeros(n, dtype=torch.int64, device=device)
|
| 821 |
+
|
| 822 |
+
|
| 823 |
+
def read_stamps(stamps, labels=None, clock_ghz=None):
|
| 824 |
+
"""Decode MK_STAMP phases: consecutive deltas in ticks and in
|
| 825 |
+
microseconds under the measured (or provided) SM clock. A 1-D buffer
|
| 826 |
+
(MK_STAMP: block 0 only) yields one delta per phase; a 2-D [B, n]
|
| 827 |
+
buffer (MK_STAMP_BLOCK) yields min / median / max across blocks per
|
| 828 |
+
phase, with blocks whose stamps stayed zero excluded."""
|
| 829 |
+
if clock_ghz is None:
|
| 830 |
+
clock_ghz = measure_clock(stamps.device)
|
| 831 |
+
|
| 832 |
+
def us(ticks):
|
| 833 |
+
return round(ticks / clock_ghz / 1000.0, 3)
|
| 834 |
+
|
| 835 |
+
if stamps.dim() == 1:
|
| 836 |
+
vals = stamps.cpu().tolist()
|
| 837 |
+
phases = []
|
| 838 |
+
for i in range(len(vals) - 1):
|
| 839 |
+
ticks = vals[i + 1] - vals[i]
|
| 840 |
+
name = labels[i] if labels and i < len(labels) else f"phase_{i}"
|
| 841 |
+
phases.append({"phase": name, "ticks": ticks, "us": us(ticks)})
|
| 842 |
+
return {"phases": phases, "clock_ghz": round(clock_ghz, 3)}
|
| 843 |
+
|
| 844 |
+
m = stamps.cpu()
|
| 845 |
+
live = (m != 0).all(dim=1)
|
| 846 |
+
m = m[live]
|
| 847 |
+
deltas = (m[:, 1:] - m[:, :-1]).to(torch.float64)
|
| 848 |
+
phases = []
|
| 849 |
+
for i in range(deltas.shape[1]):
|
| 850 |
+
col = deltas[:, i]
|
| 851 |
+
name = labels[i] if labels and i < len(labels) else f"phase_{i}"
|
| 852 |
+
med = float(col.median().item()) if col.numel() else 0.0
|
| 853 |
+
phases.append({
|
| 854 |
+
"phase": name,
|
| 855 |
+
"ticks_median": int(med),
|
| 856 |
+
"ticks_min": int(col.min().item()) if col.numel() else 0,
|
| 857 |
+
"ticks_max": int(col.max().item()) if col.numel() else 0,
|
| 858 |
+
"us_median": us(med),
|
| 859 |
+
"blocks": int(col.numel()),
|
| 860 |
+
})
|
| 861 |
+
return {"phases": phases, "clock_ghz": round(clock_ghz, 3),
|
| 862 |
+
"blocks_reporting": int(live.sum().item()),
|
| 863 |
+
"blocks_total": int(live.numel())}
|
build/torch213-cxx11-cu126-x86_64-linux/_metakernel_cuda_4611d12.abi3.so
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:97f07e5ff1283cbc7f715f1b6dda828cd79f4164e9a1a838f33b8c36889f0901
|
| 3 |
+
size 636184
|
build/torch213-cxx11-cu126-x86_64-linux/_ops.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from . import _metakernel_cuda_4611d12
|
| 3 |
+
ops = torch.ops._metakernel_cuda_4611d12
|
| 4 |
+
|
| 5 |
+
def add_op_namespace_prefix(op_name: str):
|
| 6 |
+
"""
|
| 7 |
+
Prefix op by namespace.
|
| 8 |
+
"""
|
| 9 |
+
return f"_metakernel_cuda_4611d12::{op_name}"
|
build/torch213-cxx11-cu126-x86_64-linux/metadata.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "metakernel",
|
| 3 |
+
"id": "_metakernel_cuda_4611d12",
|
| 4 |
+
"version": 1,
|
| 5 |
+
"license": "Apache-2.0",
|
| 6 |
+
"python-depends": [],
|
| 7 |
+
"backend": {
|
| 8 |
+
"type": "cuda",
|
| 9 |
+
"archs": [
|
| 10 |
+
"8.0",
|
| 11 |
+
"8.6",
|
| 12 |
+
"8.9",
|
| 13 |
+
"9.0"
|
| 14 |
+
]
|
| 15 |
+
},
|
| 16 |
+
"digest": {
|
| 17 |
+
"algorithm": "sha256",
|
| 18 |
+
"files": {
|
| 19 |
+
"__init__.py": "cZxKX5I/2EKCMC2gdCwflUsZaQJWJesmAHq2w+fVjXo=",
|
| 20 |
+
"_metakernel_cuda_4611d12.abi3.so": "l/B+X/EoPLx/cV8bbdqCjNefQWTpoag48zuMNoifCQE=",
|
| 21 |
+
"_ops.py": "uZlR720EcTy8M7BCydmvaZAlKonkwTU3778nhR9yQOE=",
|
| 22 |
+
"metakernel/__init__.py": "DFYPlrhXwYjEqCl/8n0SmWGZV8NFml5DPhMjKfv98GY="
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"provenance": {
|
| 26 |
+
"kernel-builder": {
|
| 27 |
+
"version": "0.17.0-dev0",
|
| 28 |
+
"sha": "19aaa6421e674e9fecc352bbae6eab81d19a6bf4",
|
| 29 |
+
"dirty": false
|
| 30 |
+
},
|
| 31 |
+
"kernel": {
|
| 32 |
+
"sha": "4611d1213a76a9ec1d12646553a528feb60394ff",
|
| 33 |
+
"dirty": false
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
}
|
build/torch213-cxx11-cu126-x86_64-linux/metakernel/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ctypes
|
| 2 |
+
import importlib.util
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from types import ModuleType
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def _import_from_path(file_path: Path) -> ModuleType:
|
| 9 |
+
# We cannot use the module name as-is, after adding it to `sys.modules`,
|
| 10 |
+
# it would also be used for other imports. So, we make a module name that
|
| 11 |
+
# depends on the path for it to be unique using the hex-encoded hash of
|
| 12 |
+
# the path.
|
| 13 |
+
path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
|
| 14 |
+
module_name = path_hash
|
| 15 |
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
| 16 |
+
if spec is None:
|
| 17 |
+
raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
|
| 18 |
+
module = importlib.util.module_from_spec(spec)
|
| 19 |
+
if module is None:
|
| 20 |
+
raise ImportError(f"Cannot load module {module_name} from spec")
|
| 21 |
+
sys.modules[module_name] = module
|
| 22 |
+
spec.loader.exec_module(module) # type: ignore
|
| 23 |
+
return module
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
|
build/torch213-cxx11-cu130-x86_64-linux/__init__.py
ADDED
|
@@ -0,0 +1,863 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""metakernel: the measuring instrument for the kernel-design loop.
|
| 2 |
+
|
| 3 |
+
An agent iterating on CUDA candidates is good at writing code and bad at
|
| 4 |
+
trusting its own measurements; it needs trustworthy timing, adversarial
|
| 5 |
+
correctness comparison, and measured facts about the device it targets.
|
| 6 |
+
metakernel ships those as loadable ops:
|
| 7 |
+
|
| 8 |
+
from kernels import get_kernel
|
| 9 |
+
mk = get_kernel("phanerozoic/metakernel", version=1,
|
| 10 |
+
trust_remote_code=True)
|
| 11 |
+
|
| 12 |
+
dossier = mk.probe_device() # measured facts
|
| 13 |
+
report = mk.bench(lambda: op(x), iters=200) # throttle-rejected
|
| 14 |
+
verdict = mk.compare(ref, cand, gen) # ULP evidence
|
| 15 |
+
table = mk.sweep(factory, {"BLOCK": [64, 128]}) # config search
|
| 16 |
+
|
| 17 |
+
Every op returns plain dicts and tensors, never prose. Every dossier entry
|
| 18 |
+
is produced by a dedicated probe kernel rather than a spec sheet, carries
|
| 19 |
+
the SM clock it was measured under, and labels throttled samples instead
|
| 20 |
+
of letting them poison medians. No hardware counters are used: everything
|
| 21 |
+
is inferred from timing, clocks, and the occupancy API, so entries carry a
|
| 22 |
+
confidence field (timing-differential inference can misattribute).
|
| 23 |
+
"""
|
| 24 |
+
import math
|
| 25 |
+
import time
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
|
| 29 |
+
from ._ops import ops
|
| 30 |
+
|
| 31 |
+
__all__ = ["probe_device", "bench", "compare", "order_shuffles",
|
| 32 |
+
"fuzz_shapes", "sweep", "alloc_stamps", "read_stamps",
|
| 33 |
+
"measure_clock", "ulp_diff", "ops"]
|
| 34 |
+
|
| 35 |
+
_MMA_KINDS = {
|
| 36 |
+
"fp16": (0, 16 * 16 * 16),
|
| 37 |
+
"bf16": (1, 16 * 16 * 16),
|
| 38 |
+
"tf32": (2, 16 * 16 * 8),
|
| 39 |
+
"int8": (3, 16 * 16 * 16),
|
| 40 |
+
"fp8_e4m3": (4, 16 * 8 * 32),
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _sync():
|
| 45 |
+
torch.cuda.synchronize()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _events(fn, reps):
|
| 49 |
+
"""Time reps calls of fn with one event pair each; returns ms list."""
|
| 50 |
+
starts = [torch.cuda.Event(enable_timing=True) for _ in range(reps)]
|
| 51 |
+
stops = [torch.cuda.Event(enable_timing=True) for _ in range(reps)]
|
| 52 |
+
for s, e in zip(starts, stops):
|
| 53 |
+
s.record()
|
| 54 |
+
fn()
|
| 55 |
+
e.record()
|
| 56 |
+
_sync()
|
| 57 |
+
return [s.elapsed_time(e) for s, e in zip(starts, stops)]
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def measure_clock(device="cuda", ticks=4_000_000, blocks=None):
|
| 61 |
+
"""Effective SM clock in GHz under an FMA spin of `ticks` clock64 ticks.
|
| 62 |
+
Wall time over elapsed ticks is the frequency the ticks ran at."""
|
| 63 |
+
props = torch.cuda.get_device_properties(device)
|
| 64 |
+
if blocks is None:
|
| 65 |
+
blocks = props.multi_processor_count
|
| 66 |
+
out = torch.zeros(2, dtype=torch.int64, device=device)
|
| 67 |
+
sink = torch.zeros(2, dtype=torch.float32, device=device)
|
| 68 |
+
ops.mk_spin(blocks, 256, ticks, out, sink) # warm
|
| 69 |
+
start = torch.cuda.Event(enable_timing=True)
|
| 70 |
+
stop = torch.cuda.Event(enable_timing=True)
|
| 71 |
+
start.record()
|
| 72 |
+
ops.mk_spin(blocks, 256, ticks, out, sink)
|
| 73 |
+
stop.record()
|
| 74 |
+
_sync()
|
| 75 |
+
ms = start.elapsed_time(stop)
|
| 76 |
+
elapsed = int(out[0].item())
|
| 77 |
+
return elapsed / (ms * 1e6)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _median(xs):
|
| 81 |
+
s = sorted(xs)
|
| 82 |
+
n = len(s)
|
| 83 |
+
return s[n // 2] if n % 2 else 0.5 * (s[n // 2 - 1] + s[n // 2])
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _entry(value, units, method, clock_ghz, confidence, note=""):
|
| 87 |
+
d = {"value": value, "units": units, "method": method,
|
| 88 |
+
"clock_ghz": round(clock_ghz, 3), "confidence": confidence}
|
| 89 |
+
if note:
|
| 90 |
+
d["note"] = note
|
| 91 |
+
return d
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _sattolo_ring(n, device, generator=None):
|
| 95 |
+
"""A random single-cycle permutation as an int32 next-pointer table, so
|
| 96 |
+
a pointer chase visits every slot before repeating."""
|
| 97 |
+
perm = torch.randperm(n, generator=generator)
|
| 98 |
+
ring = torch.empty(n, dtype=torch.int32)
|
| 99 |
+
ring[perm] = torch.roll(perm, -1).to(torch.int32)
|
| 100 |
+
return ring.to(device)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def probe_device(device="cuda", quick=False):
|
| 104 |
+
"""Empirical device dossier. Each entry is measured by a dedicated
|
| 105 |
+
probe kernel and carries units, method, the SM clock it ran under, and
|
| 106 |
+
a confidence field."""
|
| 107 |
+
props = torch.cuda.get_device_properties(device)
|
| 108 |
+
sm = props.multi_processor_count
|
| 109 |
+
l2 = props.L2_cache_size
|
| 110 |
+
d = {
|
| 111 |
+
"device": {
|
| 112 |
+
"name": props.name,
|
| 113 |
+
"sm_arch": f"sm{props.major}{props.minor}",
|
| 114 |
+
"sms": sm,
|
| 115 |
+
"l2_bytes": l2,
|
| 116 |
+
"vram_bytes": props.total_memory,
|
| 117 |
+
"torch": torch.__version__,
|
| 118 |
+
"cuda_runtime": torch.version.cuda,
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
sink = torch.zeros(2, dtype=torch.float32, device=device)
|
| 122 |
+
out64 = torch.zeros(2, dtype=torch.int64, device=device)
|
| 123 |
+
reps = 3 if quick else 5
|
| 124 |
+
|
| 125 |
+
# ---- clock under sustained load
|
| 126 |
+
traj = []
|
| 127 |
+
windows = 8 if quick else 24
|
| 128 |
+
for _ in range(windows):
|
| 129 |
+
traj.append(measure_clock(device, ticks=30_000_000))
|
| 130 |
+
d["clock"] = {
|
| 131 |
+
"max_ghz": round(max(traj), 3),
|
| 132 |
+
"min_ghz": round(min(traj), 3),
|
| 133 |
+
"final_ghz": round(traj[-1], 3),
|
| 134 |
+
"sag_pct": round(100 * (1 - traj[-1] / max(traj)), 1),
|
| 135 |
+
"units": "GHz",
|
| 136 |
+
"method": f"{windows} back-to-back FMA spin windows of 30M ticks; "
|
| 137 |
+
"wall/ticks per window",
|
| 138 |
+
"confidence": "high",
|
| 139 |
+
}
|
| 140 |
+
clk = max(traj)
|
| 141 |
+
|
| 142 |
+
# ---- HBM bandwidth: triad per access width, plus read-only,
|
| 143 |
+
# write-only, and random-gather
|
| 144 |
+
n4 = 16 << 20 # 16M float4 = 256 MB per buffer
|
| 145 |
+
if props.total_memory < 4 << 30:
|
| 146 |
+
n4 = 4 << 20
|
| 147 |
+
a = torch.rand(n4 * 4, device=device)
|
| 148 |
+
b = torch.rand(n4 * 4, device=device)
|
| 149 |
+
c = torch.empty(n4 * 4, device=device)
|
| 150 |
+
hbm = {}
|
| 151 |
+
for width in (1, 2, 4):
|
| 152 |
+
ops.mk_triad(a, b, c, width, 1.5) # warm
|
| 153 |
+
ms = _median(_events(lambda w=width: ops.mk_triad(a, b, c, w, 1.5),
|
| 154 |
+
reps))
|
| 155 |
+
gbs = 3 * a.numel() * 4 / (ms * 1e-3) / 1e9
|
| 156 |
+
hbm[f"triad_{4 * width}B"] = round(gbs, 1)
|
| 157 |
+
ops.mk_read(a, sink)
|
| 158 |
+
ms = _median(_events(lambda: ops.mk_read(a, sink), reps))
|
| 159 |
+
hbm["read_only"] = round(a.numel() * 4 / (ms * 1e-3) / 1e9, 1)
|
| 160 |
+
ops.mk_write(c, 1.0)
|
| 161 |
+
ms = _median(_events(lambda: ops.mk_write(c, 1.0), reps))
|
| 162 |
+
hbm["write_only"] = round(c.numel() * 4 / (ms * 1e-3) / 1e9, 1)
|
| 163 |
+
g = torch.Generator(device="cpu").manual_seed(7)
|
| 164 |
+
idx = torch.randint(0, n4, (n4,), generator=g,
|
| 165 |
+
dtype=torch.int32).to(device)
|
| 166 |
+
ops.mk_gather(a, idx, c, 256, 0)
|
| 167 |
+
ms = _median(_events(lambda: ops.mk_gather(a, idx, c, 256, 0), reps))
|
| 168 |
+
# traffic accounting: idx reads + 16B random loads + streamed stores
|
| 169 |
+
hbm["random_gather_16B"] = round(
|
| 170 |
+
(idx.numel() * 4 + 2 * n4 * 16) / (ms * 1e-3) / 1e9, 1)
|
| 171 |
+
d["hbm_bw"] = _entry(hbm, "GB/s", "streaming triad c=a+s*b per access "
|
| 172 |
+
"width; read-only fold; write-only fill; random "
|
| 173 |
+
"float4 gather (16B granules, nominal traffic)",
|
| 174 |
+
clk, "high",
|
| 175 |
+
"gather moves full cache lines per 16B granule, "
|
| 176 |
+
"so its nominal number is the achievable rate for "
|
| 177 |
+
"random access, not a line-efficiency claim")
|
| 178 |
+
|
| 179 |
+
# ---- L2 bandwidth (working set 3 buffers inside L2)
|
| 180 |
+
ws = max(l2 // 4, 1 << 20)
|
| 181 |
+
nl2 = ws // 16 # float4 elems per buffer
|
| 182 |
+
al = a[:nl2 * 4].contiguous()
|
| 183 |
+
bl = b[:nl2 * 4].contiguous()
|
| 184 |
+
cl = c[:nl2 * 4].contiguous()
|
| 185 |
+
passes = 64
|
| 186 |
+
ops.mk_triad_passes(al, bl, cl, 1.5, passes)
|
| 187 |
+
ms = _median(_events(
|
| 188 |
+
lambda: ops.mk_triad_passes(al, bl, cl, 1.5, passes), reps))
|
| 189 |
+
gbs = 3 * al.numel() * 4 * passes / (ms * 1e-3) / 1e9
|
| 190 |
+
d["l2_bw"] = _entry(round(gbs, 1), "GB/s",
|
| 191 |
+
f"in-kernel repeated triad passes over "
|
| 192 |
+
f"{3 * ws >> 20} MB resident working set", clk,
|
| 193 |
+
"medium", "first pass streams from HBM; amortized "
|
| 194 |
+
f"over {passes} passes")
|
| 195 |
+
|
| 196 |
+
# ---- latencies: pointer chase per level. Footprints are chosen so
|
| 197 |
+
# the chain hits the intended level: the L2 ring (2 MB) exceeds L1,
|
| 198 |
+
# fits L2 on every supported part, and spans few enough pages to stay
|
| 199 |
+
# TLB-resident after a full-cycle warm walk; the HBM chain is long
|
| 200 |
+
# enough (2x L2 in touched lines) that it evicts itself and cannot be
|
| 201 |
+
# served from cache.
|
| 202 |
+
lat = {}
|
| 203 |
+
idle_clk = measure_clock(device, ticks=2_000_000, blocks=1)
|
| 204 |
+
ring_s = _sattolo_ring(8192, device)
|
| 205 |
+
ops.mk_chase_shared(ring_s, 1 << 14, out64)
|
| 206 |
+
_sync()
|
| 207 |
+
hops = 1 << 16
|
| 208 |
+
ops.mk_chase_shared(ring_s, hops, out64)
|
| 209 |
+
_sync()
|
| 210 |
+
lat["smem_cycles"] = round(int(out64[0].item()) / hops, 1)
|
| 211 |
+
|
| 212 |
+
l1_ring_n = (16 << 10) // 4 # 16 KB: L1-resident after a warm walk
|
| 213 |
+
ring_l1 = _sattolo_ring(l1_ring_n, device)
|
| 214 |
+
ops.mk_chase_global(ring_l1, l1_ring_n, out64)
|
| 215 |
+
_sync()
|
| 216 |
+
hops_l1 = 1 << 15
|
| 217 |
+
ops.mk_chase_global(ring_l1, hops_l1, out64)
|
| 218 |
+
_sync()
|
| 219 |
+
lat["l1_cycles"] = round(int(out64[0].item()) / hops_l1, 1)
|
| 220 |
+
|
| 221 |
+
l2_ring_n = (2 << 20) // 4
|
| 222 |
+
ring_l2 = _sattolo_ring(l2_ring_n, device)
|
| 223 |
+
ops.mk_chase_global(ring_l2, l2_ring_n, out64) # full-cycle warm walk
|
| 224 |
+
_sync()
|
| 225 |
+
hops_l2 = 1 << 14
|
| 226 |
+
ops.mk_chase_global(ring_l2, hops_l2, out64)
|
| 227 |
+
_sync()
|
| 228 |
+
lat["l2_cycles"] = round(int(out64[0].item()) / hops_l2, 1)
|
| 229 |
+
|
| 230 |
+
hbm_bytes = min(max(4 * l2, 64 << 20), props.total_memory // 8)
|
| 231 |
+
ring_h = _sattolo_ring(int(hbm_bytes) // 4, device)
|
| 232 |
+
hops_h = max((2 * l2) // 128, 1 << 17)
|
| 233 |
+
ops.mk_chase_global(ring_h, 1 << 14, out64) # fault pages / warm TLB
|
| 234 |
+
_sync()
|
| 235 |
+
ops.mk_chase_global(ring_h, hops_h, out64)
|
| 236 |
+
_sync()
|
| 237 |
+
lat["hbm_cycles"] = round(int(out64[0].item()) / hops_h, 1)
|
| 238 |
+
lat["ns_per_cycle"] = round(1.0 / idle_clk, 3)
|
| 239 |
+
d["latency"] = _entry(lat, "cycles (single-lane dependent loads)",
|
| 240 |
+
"Sattolo-cycle pointer chase; 16 KB ring for L1, "
|
| 241 |
+
"2 MB TLB-resident ring for L2, self-evicting "
|
| 242 |
+
"chain (2x L2 in touched lines) for HBM",
|
| 243 |
+
idle_clk, "high",
|
| 244 |
+
"smem includes index arithmetic; HBM includes "
|
| 245 |
+
"residual TLB effects at large footprints")
|
| 246 |
+
del ring_h, ring_l2, ring_l1, ring_s
|
| 247 |
+
|
| 248 |
+
# ---- fp32 FMA and tensor-core throughput
|
| 249 |
+
blocks = sm * 4
|
| 250 |
+
iters = 20_000 if quick else 60_000
|
| 251 |
+
ops.mk_fma_f32(blocks, 256, iters, sink)
|
| 252 |
+
ms = _median(_events(lambda: ops.mk_fma_f32(blocks, 256, iters, sink),
|
| 253 |
+
reps))
|
| 254 |
+
flops = blocks * 256 * iters * 16 * 2 * 2 # 2 chains x 2 flops x unroll
|
| 255 |
+
d["fma_f32"] = _entry(round(flops / (ms * 1e-3) / 1e12, 2), "TFLOP/s",
|
| 256 |
+
"register-resident FMA chains", clk, "high")
|
| 257 |
+
sink64 = torch.zeros(2, dtype=torch.float64, device=device)
|
| 258 |
+
it64 = max(iters // 16, 1000)
|
| 259 |
+
ops.mk_fma_f64(blocks, 256, it64, sink64)
|
| 260 |
+
ms = _median(_events(
|
| 261 |
+
lambda: ops.mk_fma_f64(blocks, 256, it64, sink64), reps))
|
| 262 |
+
flops64 = blocks * 256 * it64 * 16 * 2 * 2
|
| 263 |
+
d["fma_f64"] = _entry(round(flops64 / (ms * 1e-3) / 1e12, 3), "TFLOP/s",
|
| 264 |
+
"register-resident double FMA chains (the DFMA "
|
| 265 |
+
"pipe)", clk, "high")
|
| 266 |
+
mma = {}
|
| 267 |
+
for name, (kind, mnk) in _MMA_KINDS.items():
|
| 268 |
+
it = 30_000 if quick else 100_000
|
| 269 |
+
r = ops.mk_mma(kind, blocks, 8, 512, sink)
|
| 270 |
+
if r != 0:
|
| 271 |
+
mma[name] = "unsupported"
|
| 272 |
+
continue
|
| 273 |
+
_sync()
|
| 274 |
+
ms = _median(_events(
|
| 275 |
+
lambda k=kind, i=it: ops.mk_mma(k, blocks, 8, i, sink), reps))
|
| 276 |
+
flops = blocks * 8 * it * 4 * 2 * mnk
|
| 277 |
+
mma[name] = round(flops / (ms * 1e-3) / 1e12, 1)
|
| 278 |
+
d["mma"] = _entry(mma, "TFLOP/s (TOP/s for int8)",
|
| 279 |
+
"register-fragment mma_sync loops (wmma; fp8 via "
|
| 280 |
+
"inline PTX m16n8k32)", clk, "medium",
|
| 281 |
+
"dense-tile rate without memory traffic; real "
|
| 282 |
+
"kernels see this only when staging hides behind it")
|
| 283 |
+
|
| 284 |
+
# ---- atomic contention curve: throughput vs number of distinct
|
| 285 |
+
# addresses the whole grid hammers (slots = 1 is full contention;
|
| 286 |
+
# slots = threads is contention-free), plus the shared-memory point
|
| 287 |
+
n_thr = blocks * 256
|
| 288 |
+
buf = torch.zeros(n_thr, dtype=torch.float32, device=device)
|
| 289 |
+
at = {"curve_slots": {}}
|
| 290 |
+
it_at = 2_000 if quick else 6_000
|
| 291 |
+
slot_points = [1, 8, 64, 512, 4096, n_thr]
|
| 292 |
+
for slots in slot_points:
|
| 293 |
+
buf.zero_()
|
| 294 |
+
ops.mk_atomics(1, blocks, 256, it_at, buf, slots)
|
| 295 |
+
ms = _median(_events(
|
| 296 |
+
lambda s=slots: ops.mk_atomics(1, blocks, 256, it_at, buf, s),
|
| 297 |
+
reps))
|
| 298 |
+
at["curve_slots"][str(slots)] = round(
|
| 299 |
+
n_thr * it_at / (ms * 1e-3) / 1e9, 2)
|
| 300 |
+
ops.mk_atomics(3, blocks, 256, it_at, buf, 1)
|
| 301 |
+
ms = _median(_events(
|
| 302 |
+
lambda: ops.mk_atomics(3, blocks, 256, it_at, buf, 1), reps))
|
| 303 |
+
at["shared_same_addr"] = round(n_thr * it_at / (ms * 1e-3) / 1e9, 2)
|
| 304 |
+
d["atomics_f32"] = _entry(at, "Gop/s",
|
| 305 |
+
"atomicAdd hammer; curve sweeps the number "
|
| 306 |
+
"of distinct global addresses from 1 (full "
|
| 307 |
+
"contention) to one per thread", clk, "high")
|
| 308 |
+
|
| 309 |
+
# ---- launch overhead
|
| 310 |
+
anchor = sink
|
| 311 |
+
n_launch = 500 if quick else 2000
|
| 312 |
+
_sync()
|
| 313 |
+
t0 = time.perf_counter()
|
| 314 |
+
for _ in range(n_launch):
|
| 315 |
+
ops.mk_empty(anchor)
|
| 316 |
+
_sync()
|
| 317 |
+
queued = (time.perf_counter() - t0) / n_launch * 1e6
|
| 318 |
+
n_rt = 100 if quick else 300
|
| 319 |
+
t0 = time.perf_counter()
|
| 320 |
+
for _ in range(n_rt):
|
| 321 |
+
ops.mk_empty(anchor)
|
| 322 |
+
_sync()
|
| 323 |
+
rt = (time.perf_counter() - t0) / n_rt * 1e6
|
| 324 |
+
d["launch_overhead"] = _entry(
|
| 325 |
+
{"queued_us": round(queued, 2), "round_trip_us": round(rt, 2)},
|
| 326 |
+
"us", "empty-kernel trains, queued vs launch+sync", clk, "high",
|
| 327 |
+
"host-dependent: includes torch op dispatch")
|
| 328 |
+
|
| 329 |
+
# ---- grid barrier
|
| 330 |
+
bar_iters = 500 if quick else 2000
|
| 331 |
+
r = ops.mk_barrier(sm, 256, bar_iters, out64)
|
| 332 |
+
if r == 0:
|
| 333 |
+
_sync()
|
| 334 |
+
ticks = int(out64[0].item())
|
| 335 |
+
d["grid_barrier"] = _entry(
|
| 336 |
+
round(ticks / bar_iters / clk / 1000.0, 2), "us per grid.sync",
|
| 337 |
+
f"cooperative kernel, {sm} blocks x 256 threads", clk, "high")
|
| 338 |
+
else:
|
| 339 |
+
d["grid_barrier"] = _entry("unsupported", "", "cooperative launch "
|
| 340 |
+
"unavailable or grid too large", clk,
|
| 341 |
+
"high")
|
| 342 |
+
|
| 343 |
+
# ---- occupancy table (API) plus timed knees: the same random gather
|
| 344 |
+
# at every (threads, dyn_smem) point; when the forced blocks/SM stop
|
| 345 |
+
# covering memory latency, achieved bandwidth falls off the cliff
|
| 346 |
+
occ = {}
|
| 347 |
+
for threads in (128, 256, 512, 1024):
|
| 348 |
+
row = {}
|
| 349 |
+
for smem_kb in (0, 16, 32, 48):
|
| 350 |
+
row[f"{smem_kb}KB"] = int(ops.mk_occupancy(
|
| 351 |
+
threads, smem_kb * 1024, anchor))
|
| 352 |
+
occ[f"t{threads}"] = row
|
| 353 |
+
d["occupancy_blocks_per_sm"] = _entry(
|
| 354 |
+
occ, "blocks/SM", "occupancy API on the float4 triad kernel",
|
| 355 |
+
clk, "high", "API-derived, not timing-derived")
|
| 356 |
+
|
| 357 |
+
knee_iters = 6_000 if quick else 20_000
|
| 358 |
+
knee_blocks = sm * 8
|
| 359 |
+
knees = {}
|
| 360 |
+
for threads in (128, 256, 1024):
|
| 361 |
+
row = {}
|
| 362 |
+
for smem_kb in (0, 32, 48):
|
| 363 |
+
ops.mk_fma_dep(knee_blocks, threads, smem_kb * 1024,
|
| 364 |
+
knee_iters, sink)
|
| 365 |
+
ms = _median(_events(
|
| 366 |
+
lambda t=threads, s=smem_kb: ops.mk_fma_dep(
|
| 367 |
+
knee_blocks, t, s * 1024, knee_iters, sink), reps))
|
| 368 |
+
fl = knee_blocks * threads * knee_iters * 16 * 2
|
| 369 |
+
row[f"{smem_kb}KB"] = round(fl / (ms * 1e-3) / 1e12, 2)
|
| 370 |
+
knees[f"t{threads}"] = row
|
| 371 |
+
d["occupancy_knees_fma_tflops"] = _entry(
|
| 372 |
+
knees, "TFLOP/s", "a single dependent FMA chain per thread, timed "
|
| 373 |
+
"at forced (threads, dyn smem) occupancies over fixed total work; "
|
| 374 |
+
"throughput is proportional to resident threads until the pipe "
|
| 375 |
+
"saturates, so the cliff marks where occupancy stops covering "
|
| 376 |
+
"latency", clk, "high",
|
| 377 |
+
"timing-derived counterpart to the API table above")
|
| 378 |
+
return d
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def bench(fn, iters=100, warmup=10, bytes=None, flops=None, dossier=None,
|
| 382 |
+
dtype="fp16", clock_guard=0.97, clock_stride=8, graph=False,
|
| 383 |
+
device="cuda"):
|
| 384 |
+
"""Median-of-N cudaEvent timing with throttle rejection.
|
| 385 |
+
|
| 386 |
+
The SM clock is probed every `clock_stride` samples; samples inside a
|
| 387 |
+
window whose clock sagged below `clock_guard` x the best observed
|
| 388 |
+
clock are rejected and counted rather than silently averaged. With
|
| 389 |
+
declared `bytes`/`flops` per call, reports achieved GB/s / TFLOP/s;
|
| 390 |
+
with a `dossier`, adds roofline placement against the measured peaks.
|
| 391 |
+
|
| 392 |
+
`graph=True` captures fn() into a CUDA graph and times graph replays,
|
| 393 |
+
stripping Python and torch dispatch from the measurement: the right
|
| 394 |
+
mode for kernels in the few-microsecond range, where dispatch is
|
| 395 |
+
otherwise the thing being measured. Capture requires fn to be
|
| 396 |
+
graph-safe (no sync, no allocation growth, static shapes)."""
|
| 397 |
+
for _ in range(warmup):
|
| 398 |
+
fn()
|
| 399 |
+
_sync()
|
| 400 |
+
if graph:
|
| 401 |
+
g = torch.cuda.CUDAGraph()
|
| 402 |
+
with torch.cuda.graph(g):
|
| 403 |
+
fn()
|
| 404 |
+
fn = g.replay
|
| 405 |
+
for _ in range(warmup):
|
| 406 |
+
fn()
|
| 407 |
+
_sync()
|
| 408 |
+
samples = []
|
| 409 |
+
clocks = []
|
| 410 |
+
stride = clock_stride
|
| 411 |
+
for base in range(0, iters, stride):
|
| 412 |
+
clocks.append(measure_clock(device, ticks=1_500_000))
|
| 413 |
+
n = min(stride, iters - base)
|
| 414 |
+
samples.append((_events(fn, n), len(clocks) - 1))
|
| 415 |
+
clocks.append(measure_clock(device, ticks=1_500_000))
|
| 416 |
+
best_clk = max(clocks)
|
| 417 |
+
kept, rejected = [], 0
|
| 418 |
+
flat = []
|
| 419 |
+
for group, ci in samples:
|
| 420 |
+
window_ok = (clocks[ci] >= clock_guard * best_clk and
|
| 421 |
+
clocks[min(ci + 1, len(clocks) - 1)] >=
|
| 422 |
+
clock_guard * best_clk)
|
| 423 |
+
for ms in group:
|
| 424 |
+
flat.append(ms)
|
| 425 |
+
if window_ok:
|
| 426 |
+
kept.append(ms)
|
| 427 |
+
else:
|
| 428 |
+
rejected += 1
|
| 429 |
+
if not kept:
|
| 430 |
+
kept = flat
|
| 431 |
+
med = _median(kept)
|
| 432 |
+
rep = {
|
| 433 |
+
"median_ms": round(med, 4),
|
| 434 |
+
"min_ms": round(min(kept), 4),
|
| 435 |
+
"mean_ms": round(sum(kept) / len(kept), 4),
|
| 436 |
+
"samples": len(flat),
|
| 437 |
+
"rejected_throttled": rejected,
|
| 438 |
+
"graph_captured": bool(graph),
|
| 439 |
+
"clock_ghz": {"best": round(best_clk, 3),
|
| 440 |
+
"worst": round(min(clocks), 3)},
|
| 441 |
+
}
|
| 442 |
+
if bytes is not None:
|
| 443 |
+
rep["achieved_gbs"] = round(bytes / (med * 1e-3) / 1e9, 1)
|
| 444 |
+
if flops is not None:
|
| 445 |
+
rep["achieved_tflops"] = round(flops / (med * 1e-3) / 1e12, 2)
|
| 446 |
+
if bytes and flops:
|
| 447 |
+
rep["arithmetic_intensity"] = round(flops / bytes, 2)
|
| 448 |
+
if dossier is not None and bytes and flops:
|
| 449 |
+
bw = max(dossier["hbm_bw"]["value"].values()) * 1e9
|
| 450 |
+
mma_v = dossier["mma"]["value"].get(dtype)
|
| 451 |
+
peak = (mma_v if isinstance(mma_v, (int, float))
|
| 452 |
+
else dossier["fma_f32"]["value"]) * 1e12
|
| 453 |
+
ai = flops / bytes
|
| 454 |
+
roof = min(peak, ai * bw)
|
| 455 |
+
rep["roofline"] = {
|
| 456 |
+
"bound": "memory" if ai * bw < peak else "compute",
|
| 457 |
+
"ridge_flops_per_byte": round(peak / bw, 1),
|
| 458 |
+
"pct_of_roof": round(100 * (flops / (med * 1e-3)) / roof, 1),
|
| 459 |
+
"peak_dtype": dtype,
|
| 460 |
+
}
|
| 461 |
+
return rep
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
def _to_ordered_int(t):
|
| 465 |
+
"""Map float bit patterns to integers whose difference is the ULP
|
| 466 |
+
distance: positive floats keep their bits, negative floats mirror
|
| 467 |
+
below zero, so the mapping is monotone in the float value."""
|
| 468 |
+
if t.dtype == torch.float32:
|
| 469 |
+
u = t.view(torch.int32).to(torch.int64) & 0xFFFFFFFF
|
| 470 |
+
half = 1 << 31
|
| 471 |
+
elif t.dtype in (torch.float16, torch.bfloat16):
|
| 472 |
+
u = t.view(torch.int16).to(torch.int64) & 0xFFFF
|
| 473 |
+
half = 1 << 15
|
| 474 |
+
elif t.dtype == torch.float64:
|
| 475 |
+
i = t.view(torch.int64)
|
| 476 |
+
return torch.where(i >= 0, i, (-(1 << 63)) - i)
|
| 477 |
+
else:
|
| 478 |
+
raise ValueError(f"unsupported dtype {t.dtype}")
|
| 479 |
+
return torch.where(u < half, u, half - u)
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
def ulp_diff(a, b):
|
| 483 |
+
"""Elementwise ULP distance between same-dtype float tensors. NaNs in
|
| 484 |
+
the same position count as 0; a one-sided NaN counts as 2**62."""
|
| 485 |
+
assert a.dtype == b.dtype and a.shape == b.shape
|
| 486 |
+
ia, ib = _to_ordered_int(a), _to_ordered_int(b)
|
| 487 |
+
d = (ia - ib).abs()
|
| 488 |
+
both_nan = torch.isnan(a) & torch.isnan(b)
|
| 489 |
+
one_nan = torch.isnan(a) ^ torch.isnan(b)
|
| 490 |
+
d = torch.where(both_nan, torch.zeros_like(d), d)
|
| 491 |
+
d = torch.where(one_nan, torch.full_like(d, 1 << 62), d)
|
| 492 |
+
return d
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
class order_shuffles:
|
| 496 |
+
"""Built-in order shuffles for the common contractual-noise axes."""
|
| 497 |
+
|
| 498 |
+
@staticmethod
|
| 499 |
+
def last_dim():
|
| 500 |
+
"""Permute the last dim of every tensor arg identically: the
|
| 501 |
+
accumulation order of a last-dim reduction changes, the value
|
| 502 |
+
does not."""
|
| 503 |
+
def shuffle(args, seed):
|
| 504 |
+
g = torch.Generator(device=args[0].device).manual_seed(seed)
|
| 505 |
+
p = torch.randperm(args[0].shape[-1], device=args[0].device,
|
| 506 |
+
generator=g)
|
| 507 |
+
return tuple(x[..., p] for x in args)
|
| 508 |
+
return shuffle
|
| 509 |
+
|
| 510 |
+
@staticmethod
|
| 511 |
+
def matmul_k():
|
| 512 |
+
"""Permute K of (A [.., M, K], B [.., K, N]) consistently: the
|
| 513 |
+
product is unchanged, the accumulation order is not."""
|
| 514 |
+
def shuffle(args, seed):
|
| 515 |
+
a_m, b_m = args[0], args[1]
|
| 516 |
+
g = torch.Generator(device=a_m.device).manual_seed(seed)
|
| 517 |
+
p = torch.randperm(a_m.shape[-1], device=a_m.device,
|
| 518 |
+
generator=g)
|
| 519 |
+
return (a_m[..., p], b_m[..., p, :]) + tuple(args[2:])
|
| 520 |
+
return shuffle
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
def _as_outputs(x):
|
| 524 |
+
return list(x) if isinstance(x, (tuple, list)) else [x]
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
def compare(ref, cand, gen, trials=8, order_shuffle=None, order_trials=8,
|
| 528 |
+
ulp_tol=None, atol=None, rtol=None, band_margin=4.0, seed=0):
|
| 529 |
+
"""Adversarial correctness comparison with ULP evidence.
|
| 530 |
+
|
| 531 |
+
`gen(seed) -> args` builds inputs; `ref(*args)` and `cand(*args)`
|
| 532 |
+
return a tensor or a tuple of tensors (each output is compared and
|
| 533 |
+
the worst governs). If `order_shuffle(args, seed) -> args` is given
|
| 534 |
+
and reorders contractual-noise dimensions (see `order_shuffles` for
|
| 535 |
+
the common cases), the reference is treated as a distribution:
|
| 536 |
+
candidates are judged against the measured reduction-order band
|
| 537 |
+
rather than a point. Verdict, stated in the report: max ULP <=
|
| 538 |
+
band_margin * band + 2 when a band is measured, else max ULP <=
|
| 539 |
+
ulp_tol (default 2); if `atol`/`rtol` are given they are an
|
| 540 |
+
alternative acceptance route, checked as |c - r| <= atol + rtol|r|."""
|
| 541 |
+
worst = {"max_ulp": -1}
|
| 542 |
+
total_hist = {}
|
| 543 |
+
per_trial = []
|
| 544 |
+
max_abs_seen = 0.0
|
| 545 |
+
max_rel_seen = 0.0
|
| 546 |
+
for t in range(trials):
|
| 547 |
+
args = gen(seed + t)
|
| 548 |
+
r_outs = _as_outputs(ref(*args))
|
| 549 |
+
c_outs = _as_outputs(cand(*args))
|
| 550 |
+
assert len(r_outs) == len(c_outs), "output arity mismatch"
|
| 551 |
+
# the order band is POINTWISE: a near-zero reduction output is
|
| 552 |
+
# legitimately thousands of ULP order-sensitive (cancellation)
|
| 553 |
+
# while its neighbors are stable, so each element is judged
|
| 554 |
+
# against its own measured sensitivity, not a global max
|
| 555 |
+
bands = None
|
| 556 |
+
band = 0
|
| 557 |
+
band_score = 0.0
|
| 558 |
+
if order_shuffle is not None:
|
| 559 |
+
bands = [torch.zeros_like(ro, dtype=torch.int64)
|
| 560 |
+
for ro in r_outs]
|
| 561 |
+
for k in range(order_trials):
|
| 562 |
+
r2 = _as_outputs(
|
| 563 |
+
ref(*order_shuffle(args, 1000 + seed + t * 131 + k)))
|
| 564 |
+
for bi, (ro, r2o) in enumerate(zip(r_outs, r2)):
|
| 565 |
+
bands[bi] = torch.maximum(bands[bi],
|
| 566 |
+
ulp_diff(ro, r2o))
|
| 567 |
+
band = max(int(b.max().item()) if b.numel() else 0
|
| 568 |
+
for b in bands)
|
| 569 |
+
max_ulp = 0
|
| 570 |
+
mean_ulp = 0.0
|
| 571 |
+
first_div = -1
|
| 572 |
+
n_el = 0
|
| 573 |
+
nan_r = nan_c = inf_r = inf_c = 0
|
| 574 |
+
for oi, (r, c) in enumerate(zip(r_outs, c_outs)):
|
| 575 |
+
d = ulp_diff(r, c)
|
| 576 |
+
if bands is not None and d.numel():
|
| 577 |
+
ratio = d.double() / (bands[oi].double() + 2.0)
|
| 578 |
+
band_score = max(band_score, float(ratio.max().item()))
|
| 579 |
+
if d.numel():
|
| 580 |
+
max_ulp = max(max_ulp, int(d.max().item()))
|
| 581 |
+
mean_ulp += float(d.double().sum().item())
|
| 582 |
+
n_el += d.numel()
|
| 583 |
+
nz = (d > 0)
|
| 584 |
+
if first_div < 0 and nz.any():
|
| 585 |
+
first_div = int(nz.flatten().nonzero()[0].item())
|
| 586 |
+
if nz.any():
|
| 587 |
+
buckets = torch.clamp(
|
| 588 |
+
torch.log2(d[nz].double()).floor(), 0, 62
|
| 589 |
+
).to(torch.int64)
|
| 590 |
+
counts = torch.bincount(buckets, minlength=63)
|
| 591 |
+
for bkt in counts.nonzero().flatten().tolist():
|
| 592 |
+
total_hist[bkt] = total_hist.get(bkt, 0) + int(
|
| 593 |
+
counts[bkt].item())
|
| 594 |
+
fin = torch.isfinite(r) & torch.isfinite(c)
|
| 595 |
+
if fin.any():
|
| 596 |
+
diff = (c[fin].double() - r[fin].double()).abs()
|
| 597 |
+
max_abs_seen = max(max_abs_seen,
|
| 598 |
+
float(diff.max().item()))
|
| 599 |
+
rel = diff / r[fin].double().abs().clamp(min=1e-300)
|
| 600 |
+
max_rel_seen = max(max_rel_seen,
|
| 601 |
+
float(rel.max().item()))
|
| 602 |
+
nan_r += int(torch.isnan(r).sum().item())
|
| 603 |
+
nan_c += int(torch.isnan(c).sum().item())
|
| 604 |
+
inf_r += int(torch.isinf(r).sum().item())
|
| 605 |
+
inf_c += int(torch.isinf(c).sum().item())
|
| 606 |
+
trial_rep = {
|
| 607 |
+
"max_ulp": max_ulp,
|
| 608 |
+
"mean_ulp": round(mean_ulp / max(n_el, 1), 3),
|
| 609 |
+
"first_divergent_index": first_div,
|
| 610 |
+
"nan_ref": nan_r, "nan_cand": nan_c,
|
| 611 |
+
"inf_ref": inf_r, "inf_cand": inf_c,
|
| 612 |
+
"order_band_ulp": band,
|
| 613 |
+
"band_score": round(band_score, 2),
|
| 614 |
+
}
|
| 615 |
+
per_trial.append(trial_rep)
|
| 616 |
+
if max_ulp > worst["max_ulp"]:
|
| 617 |
+
worst = trial_rep
|
| 618 |
+
band_max = max(t["order_band_ulp"] for t in per_trial)
|
| 619 |
+
max_seen = max(t["max_ulp"] for t in per_trial)
|
| 620 |
+
if order_shuffle is not None:
|
| 621 |
+
score = max(t["band_score"] for t in per_trial)
|
| 622 |
+
ulp_ok = score <= band_margin
|
| 623 |
+
criterion = (f"pointwise max(ulp / (order_band + 2)) = "
|
| 624 |
+
f"{score:.2f} <= band_margin {band_margin}")
|
| 625 |
+
else:
|
| 626 |
+
tol = ulp_tol if ulp_tol is not None else 2
|
| 627 |
+
ulp_ok = max_seen <= tol
|
| 628 |
+
criterion = f"max_ulp {max_seen} <= ulp_tol {tol}"
|
| 629 |
+
tol_ok = False
|
| 630 |
+
if atol is not None or rtol is not None:
|
| 631 |
+
tol_ok = max_abs_seen <= (atol or 0) + (rtol or 0) * 1.0 or \
|
| 632 |
+
max_rel_seen <= (rtol or 0) or max_abs_seen <= (atol or 0)
|
| 633 |
+
criterion += (f"; OR max_abs {max_abs_seen:.3e} <= atol {atol} / "
|
| 634 |
+
f"max_rel {max_rel_seen:.3e} <= rtol {rtol}")
|
| 635 |
+
return {
|
| 636 |
+
"pass": bool(ulp_ok or tol_ok),
|
| 637 |
+
"criterion": criterion,
|
| 638 |
+
"worst_trial": worst,
|
| 639 |
+
"max_abs": max_abs_seen,
|
| 640 |
+
"max_rel": max_rel_seen,
|
| 641 |
+
"trials": per_trial,
|
| 642 |
+
"ulp_hist_log2": dict(sorted(total_hist.items())),
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
|
| 646 |
+
_FUZZ_VARIANTS = ("contig", "transposed", "strided_rows", "offset_slice",
|
| 647 |
+
"overflow_expand")
|
| 648 |
+
|
| 649 |
+
|
| 650 |
+
def _context_alive(device):
|
| 651 |
+
try:
|
| 652 |
+
t = torch.ones(4, device=device)
|
| 653 |
+
torch.cuda.synchronize()
|
| 654 |
+
return bool(t.sum().item() == 4.0)
|
| 655 |
+
except Exception: # noqa: BLE001
|
| 656 |
+
return False
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
def fuzz_shapes(op, ref, make, shapes=None, variants=_FUZZ_VARIANTS,
|
| 660 |
+
dtypes=(torch.float32,), ulp_tol=64, device="cuda"):
|
| 661 |
+
"""The ugly envelope: run op vs ref across edge shapes, dtypes, and
|
| 662 |
+
layout mutations; report ok / mismatch / exception per case.
|
| 663 |
+
|
| 664 |
+
`make(shape, dtype)` (or `make(shape)`) builds a base input, either a
|
| 665 |
+
tensor or a tuple of tensors for multi-argument ops; layout variants
|
| 666 |
+
derive non-contiguous and misaligned views whose CONTENT equals a
|
| 667 |
+
contiguous copy, so `op(*xs)` must match `ref(*xs)` on every variant.
|
| 668 |
+
`overflow_expand` stride-0-expands the input past 2**31 logical
|
| 669 |
+
elements to catch int32 indexing, and is skipped with status
|
| 670 |
+
`skipped_vram` when materializing the output would not fit. After any
|
| 671 |
+
exception the CUDA context is health-checked; if a candidate killed
|
| 672 |
+
it, remaining cases are marked `context_lost` instead of producing
|
| 673 |
+
garbage."""
|
| 674 |
+
if shapes is None:
|
| 675 |
+
shapes = [(0, 8), (1, 1), (1, 17), (3, 63), (17, 129), (128, 128),
|
| 676 |
+
(1023, 5), (2, 4096)]
|
| 677 |
+
|
| 678 |
+
def build(shape, dtype):
|
| 679 |
+
try:
|
| 680 |
+
out = make(tuple(shape), dtype)
|
| 681 |
+
except TypeError:
|
| 682 |
+
out = make(tuple(shape))
|
| 683 |
+
return out if isinstance(out, (tuple, list)) else (out,)
|
| 684 |
+
|
| 685 |
+
def variant_of(x, v):
|
| 686 |
+
if v == "contig":
|
| 687 |
+
return x
|
| 688 |
+
if v == "transposed":
|
| 689 |
+
return x.t() if x.dim() == 2 else x
|
| 690 |
+
if v == "strided_rows":
|
| 691 |
+
return x[::2] if x.shape[0] >= 2 else x
|
| 692 |
+
if v == "offset_slice":
|
| 693 |
+
# a storage-offset-1 view: misaligned for vectorized loads
|
| 694 |
+
return x.flatten()[1:] if x.numel() > 1 else x
|
| 695 |
+
if v == "overflow_expand":
|
| 696 |
+
if x.numel() == 0:
|
| 697 |
+
return None
|
| 698 |
+
per = max(x.numel(), 1)
|
| 699 |
+
rep = (1 << 31) // per + 2
|
| 700 |
+
return x.reshape((1,) + tuple(x.shape)).expand(
|
| 701 |
+
(rep,) + tuple(x.shape))
|
| 702 |
+
return x
|
| 703 |
+
|
| 704 |
+
rows = []
|
| 705 |
+
lost = False
|
| 706 |
+
for dtype in dtypes:
|
| 707 |
+
for shape in shapes:
|
| 708 |
+
bases = build(shape, dtype)
|
| 709 |
+
for v in variants:
|
| 710 |
+
case = {"shape": tuple(shape), "variant": v,
|
| 711 |
+
"dtype": str(dtype).replace("torch.", "")}
|
| 712 |
+
if lost:
|
| 713 |
+
case["status"] = "context_lost"
|
| 714 |
+
rows.append(case)
|
| 715 |
+
continue
|
| 716 |
+
xs = [variant_of(b, v) for b in bases]
|
| 717 |
+
if any(x is None for x in xs):
|
| 718 |
+
case["status"] = "skipped_empty"
|
| 719 |
+
rows.append(case)
|
| 720 |
+
continue
|
| 721 |
+
if v == "overflow_expand":
|
| 722 |
+
logical = max(x.numel() for x in xs)
|
| 723 |
+
need = logical * xs[0].element_size() * 2
|
| 724 |
+
free, _total = torch.cuda.mem_get_info()
|
| 725 |
+
if need > free * 0.8:
|
| 726 |
+
case["status"] = "skipped_vram"
|
| 727 |
+
case["logical_elems"] = int(logical)
|
| 728 |
+
rows.append(case)
|
| 729 |
+
continue
|
| 730 |
+
case["logical_elems"] = int(logical)
|
| 731 |
+
case["contiguous"] = bool(xs[0].is_contiguous())
|
| 732 |
+
try:
|
| 733 |
+
got = op(*xs)
|
| 734 |
+
want = ref(*xs)
|
| 735 |
+
if v == "overflow_expand":
|
| 736 |
+
# bounded comparison: the head plus the high-index
|
| 737 |
+
# tail, which is where int32 flat indexing wraps
|
| 738 |
+
w = 1 << 20
|
| 739 |
+
gf, wf = got.flatten(), want.flatten()
|
| 740 |
+
pairs = ((gf[:w], wf[:w]), (gf[-w:], wf[-w:]))
|
| 741 |
+
else:
|
| 742 |
+
pairs = ((got, want),)
|
| 743 |
+
mx = 0
|
| 744 |
+
for gp, wp in pairs:
|
| 745 |
+
d = ulp_diff(wp, gp) if gp.is_floating_point() \
|
| 746 |
+
else (wp != gp).to(torch.int64) * (1 << 62)
|
| 747 |
+
if d.numel():
|
| 748 |
+
mx = max(mx, int(d.max().item()))
|
| 749 |
+
del d
|
| 750 |
+
case["status"] = "ok" if mx <= ulp_tol else "mismatch"
|
| 751 |
+
case["max_ulp"] = mx
|
| 752 |
+
del got, want
|
| 753 |
+
except Exception as e: # noqa: BLE001 - report, don't die
|
| 754 |
+
case["status"] = "exception"
|
| 755 |
+
case["error"] = f"{type(e).__name__}: {str(e)[:120]}"
|
| 756 |
+
if not _context_alive(device):
|
| 757 |
+
lost = True
|
| 758 |
+
case["status"] = "context_killed"
|
| 759 |
+
rows.append(case)
|
| 760 |
+
n_bad = sum(1 for r in rows
|
| 761 |
+
if r["status"] not in ("ok", "skipped_vram",
|
| 762 |
+
"skipped_empty"))
|
| 763 |
+
return {"cases": rows, "failed": n_bad, "total": len(rows),
|
| 764 |
+
"context_lost": lost}
|
| 765 |
+
|
| 766 |
+
|
| 767 |
+
def sweep(factory, grid, iters=30, warmup=5, bytes=None, flops=None,
|
| 768 |
+
check=None, budget_s=None):
|
| 769 |
+
"""Launch-configuration search: `factory(**point) -> callable`; one
|
| 770 |
+
bench row per grid point, sorted fastest first. A point whose factory
|
| 771 |
+
or run raises is reported, not skipped silently. `check(fn) -> bool`
|
| 772 |
+
is an optional correctness gate recorded per row (a fast wrong config
|
| 773 |
+
must not win a sweep). `budget_s` caps total sweep time; points not
|
| 774 |
+
reached are reported as `not_run` rather than silently dropped."""
|
| 775 |
+
keys = list(grid.keys())
|
| 776 |
+
|
| 777 |
+
def points(i, acc):
|
| 778 |
+
if i == len(keys):
|
| 779 |
+
yield dict(acc)
|
| 780 |
+
return
|
| 781 |
+
for v in grid[keys[i]]:
|
| 782 |
+
acc[keys[i]] = v
|
| 783 |
+
yield from points(i + 1, acc)
|
| 784 |
+
|
| 785 |
+
rows = []
|
| 786 |
+
t_start = time.perf_counter()
|
| 787 |
+
over_budget = False
|
| 788 |
+
for p in points(0, {}):
|
| 789 |
+
row = dict(p)
|
| 790 |
+
if over_budget or (budget_s is not None and
|
| 791 |
+
time.perf_counter() - t_start > budget_s):
|
| 792 |
+
over_budget = True
|
| 793 |
+
row["status"] = "not_run"
|
| 794 |
+
row["median_ms"] = float("inf")
|
| 795 |
+
rows.append(row)
|
| 796 |
+
continue
|
| 797 |
+
try:
|
| 798 |
+
fn = factory(**p)
|
| 799 |
+
if check is not None:
|
| 800 |
+
row["check"] = bool(check(fn))
|
| 801 |
+
rep = bench(fn, iters=iters, warmup=warmup, bytes=bytes,
|
| 802 |
+
flops=flops)
|
| 803 |
+
row.update({"median_ms": rep["median_ms"],
|
| 804 |
+
"rejected_throttled": rep["rejected_throttled"]})
|
| 805 |
+
if "achieved_gbs" in rep:
|
| 806 |
+
row["achieved_gbs"] = rep["achieved_gbs"]
|
| 807 |
+
if "achieved_tflops" in rep:
|
| 808 |
+
row["achieved_tflops"] = rep["achieved_tflops"]
|
| 809 |
+
except Exception as e: # noqa: BLE001
|
| 810 |
+
row["error"] = f"{type(e).__name__}: {str(e)[:120]}"
|
| 811 |
+
row["median_ms"] = float("inf")
|
| 812 |
+
rows.append(row)
|
| 813 |
+
rows.sort(key=lambda r: (not r.get("check", True), r["median_ms"]))
|
| 814 |
+
return rows
|
| 815 |
+
|
| 816 |
+
|
| 817 |
+
def alloc_stamps(n, device="cuda"):
|
| 818 |
+
"""Device buffer for the MK_STAMP convention (metakernel_cuda/
|
| 819 |
+
stamps.cuh): the candidate kernel writes clock64() into slot i."""
|
| 820 |
+
return torch.zeros(n, dtype=torch.int64, device=device)
|
| 821 |
+
|
| 822 |
+
|
| 823 |
+
def read_stamps(stamps, labels=None, clock_ghz=None):
|
| 824 |
+
"""Decode MK_STAMP phases: consecutive deltas in ticks and in
|
| 825 |
+
microseconds under the measured (or provided) SM clock. A 1-D buffer
|
| 826 |
+
(MK_STAMP: block 0 only) yields one delta per phase; a 2-D [B, n]
|
| 827 |
+
buffer (MK_STAMP_BLOCK) yields min / median / max across blocks per
|
| 828 |
+
phase, with blocks whose stamps stayed zero excluded."""
|
| 829 |
+
if clock_ghz is None:
|
| 830 |
+
clock_ghz = measure_clock(stamps.device)
|
| 831 |
+
|
| 832 |
+
def us(ticks):
|
| 833 |
+
return round(ticks / clock_ghz / 1000.0, 3)
|
| 834 |
+
|
| 835 |
+
if stamps.dim() == 1:
|
| 836 |
+
vals = stamps.cpu().tolist()
|
| 837 |
+
phases = []
|
| 838 |
+
for i in range(len(vals) - 1):
|
| 839 |
+
ticks = vals[i + 1] - vals[i]
|
| 840 |
+
name = labels[i] if labels and i < len(labels) else f"phase_{i}"
|
| 841 |
+
phases.append({"phase": name, "ticks": ticks, "us": us(ticks)})
|
| 842 |
+
return {"phases": phases, "clock_ghz": round(clock_ghz, 3)}
|
| 843 |
+
|
| 844 |
+
m = stamps.cpu()
|
| 845 |
+
live = (m != 0).all(dim=1)
|
| 846 |
+
m = m[live]
|
| 847 |
+
deltas = (m[:, 1:] - m[:, :-1]).to(torch.float64)
|
| 848 |
+
phases = []
|
| 849 |
+
for i in range(deltas.shape[1]):
|
| 850 |
+
col = deltas[:, i]
|
| 851 |
+
name = labels[i] if labels and i < len(labels) else f"phase_{i}"
|
| 852 |
+
med = float(col.median().item()) if col.numel() else 0.0
|
| 853 |
+
phases.append({
|
| 854 |
+
"phase": name,
|
| 855 |
+
"ticks_median": int(med),
|
| 856 |
+
"ticks_min": int(col.min().item()) if col.numel() else 0,
|
| 857 |
+
"ticks_max": int(col.max().item()) if col.numel() else 0,
|
| 858 |
+
"us_median": us(med),
|
| 859 |
+
"blocks": int(col.numel()),
|
| 860 |
+
})
|
| 861 |
+
return {"phases": phases, "clock_ghz": round(clock_ghz, 3),
|
| 862 |
+
"blocks_reporting": int(live.sum().item()),
|
| 863 |
+
"blocks_total": int(live.numel())}
|
build/torch213-cxx11-cu130-x86_64-linux/_metakernel_cuda_4611d12.abi3.so
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7b1c2a4f6a13c1ca3458133df0c62730801fb87f38a0eeaa556752c2eb87c25a
|
| 3 |
+
size 967576
|
build/torch213-cxx11-cu130-x86_64-linux/_ops.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from . import _metakernel_cuda_4611d12
|
| 3 |
+
ops = torch.ops._metakernel_cuda_4611d12
|
| 4 |
+
|
| 5 |
+
def add_op_namespace_prefix(op_name: str):
|
| 6 |
+
"""
|
| 7 |
+
Prefix op by namespace.
|
| 8 |
+
"""
|
| 9 |
+
return f"_metakernel_cuda_4611d12::{op_name}"
|
build/torch213-cxx11-cu130-x86_64-linux/metadata.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "metakernel",
|
| 3 |
+
"id": "_metakernel_cuda_4611d12",
|
| 4 |
+
"version": 1,
|
| 5 |
+
"license": "Apache-2.0",
|
| 6 |
+
"python-depends": [],
|
| 7 |
+
"backend": {
|
| 8 |
+
"type": "cuda",
|
| 9 |
+
"archs": [
|
| 10 |
+
"10.0",
|
| 11 |
+
"12.0",
|
| 12 |
+
"8.0",
|
| 13 |
+
"8.6",
|
| 14 |
+
"8.9",
|
| 15 |
+
"9.0"
|
| 16 |
+
]
|
| 17 |
+
},
|
| 18 |
+
"digest": {
|
| 19 |
+
"algorithm": "sha256",
|
| 20 |
+
"files": {
|
| 21 |
+
"__init__.py": "cZxKX5I/2EKCMC2gdCwflUsZaQJWJesmAHq2w+fVjXo=",
|
| 22 |
+
"_metakernel_cuda_4611d12.abi3.so": "exwqT2oTwco0WBM98MYnMIAfuH84oO6qVWdSwuuHwlo=",
|
| 23 |
+
"_ops.py": "uZlR720EcTy8M7BCydmvaZAlKonkwTU3778nhR9yQOE=",
|
| 24 |
+
"metakernel/__init__.py": "DFYPlrhXwYjEqCl/8n0SmWGZV8NFml5DPhMjKfv98GY="
|
| 25 |
+
}
|
| 26 |
+
},
|
| 27 |
+
"provenance": {
|
| 28 |
+
"kernel-builder": {
|
| 29 |
+
"version": "0.17.0-dev0",
|
| 30 |
+
"sha": "19aaa6421e674e9fecc352bbae6eab81d19a6bf4",
|
| 31 |
+
"dirty": false
|
| 32 |
+
},
|
| 33 |
+
"kernel": {
|
| 34 |
+
"sha": "4611d1213a76a9ec1d12646553a528feb60394ff",
|
| 35 |
+
"dirty": false
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
}
|
build/torch213-cxx11-cu130-x86_64-linux/metakernel/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ctypes
|
| 2 |
+
import importlib.util
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from types import ModuleType
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def _import_from_path(file_path: Path) -> ModuleType:
|
| 9 |
+
# We cannot use the module name as-is, after adding it to `sys.modules`,
|
| 10 |
+
# it would also be used for other imports. So, we make a module name that
|
| 11 |
+
# depends on the path for it to be unique using the hex-encoded hash of
|
| 12 |
+
# the path.
|
| 13 |
+
path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
|
| 14 |
+
module_name = path_hash
|
| 15 |
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
| 16 |
+
if spec is None:
|
| 17 |
+
raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
|
| 18 |
+
module = importlib.util.module_from_spec(spec)
|
| 19 |
+
if module is None:
|
| 20 |
+
raise ImportError(f"Cannot load module {module_name} from spec")
|
| 21 |
+
sys.modules[module_name] = module
|
| 22 |
+
spec.loader.exec_module(module) # type: ignore
|
| 23 |
+
return module
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
|
build/torch213-cxx11-cu132-x86_64-linux/__init__.py
ADDED
|
@@ -0,0 +1,863 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""metakernel: the measuring instrument for the kernel-design loop.
|
| 2 |
+
|
| 3 |
+
An agent iterating on CUDA candidates is good at writing code and bad at
|
| 4 |
+
trusting its own measurements; it needs trustworthy timing, adversarial
|
| 5 |
+
correctness comparison, and measured facts about the device it targets.
|
| 6 |
+
metakernel ships those as loadable ops:
|
| 7 |
+
|
| 8 |
+
from kernels import get_kernel
|
| 9 |
+
mk = get_kernel("phanerozoic/metakernel", version=1,
|
| 10 |
+
trust_remote_code=True)
|
| 11 |
+
|
| 12 |
+
dossier = mk.probe_device() # measured facts
|
| 13 |
+
report = mk.bench(lambda: op(x), iters=200) # throttle-rejected
|
| 14 |
+
verdict = mk.compare(ref, cand, gen) # ULP evidence
|
| 15 |
+
table = mk.sweep(factory, {"BLOCK": [64, 128]}) # config search
|
| 16 |
+
|
| 17 |
+
Every op returns plain dicts and tensors, never prose. Every dossier entry
|
| 18 |
+
is produced by a dedicated probe kernel rather than a spec sheet, carries
|
| 19 |
+
the SM clock it was measured under, and labels throttled samples instead
|
| 20 |
+
of letting them poison medians. No hardware counters are used: everything
|
| 21 |
+
is inferred from timing, clocks, and the occupancy API, so entries carry a
|
| 22 |
+
confidence field (timing-differential inference can misattribute).
|
| 23 |
+
"""
|
| 24 |
+
import math
|
| 25 |
+
import time
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
|
| 29 |
+
from ._ops import ops
|
| 30 |
+
|
| 31 |
+
__all__ = ["probe_device", "bench", "compare", "order_shuffles",
|
| 32 |
+
"fuzz_shapes", "sweep", "alloc_stamps", "read_stamps",
|
| 33 |
+
"measure_clock", "ulp_diff", "ops"]
|
| 34 |
+
|
| 35 |
+
_MMA_KINDS = {
|
| 36 |
+
"fp16": (0, 16 * 16 * 16),
|
| 37 |
+
"bf16": (1, 16 * 16 * 16),
|
| 38 |
+
"tf32": (2, 16 * 16 * 8),
|
| 39 |
+
"int8": (3, 16 * 16 * 16),
|
| 40 |
+
"fp8_e4m3": (4, 16 * 8 * 32),
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _sync():
|
| 45 |
+
torch.cuda.synchronize()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _events(fn, reps):
|
| 49 |
+
"""Time reps calls of fn with one event pair each; returns ms list."""
|
| 50 |
+
starts = [torch.cuda.Event(enable_timing=True) for _ in range(reps)]
|
| 51 |
+
stops = [torch.cuda.Event(enable_timing=True) for _ in range(reps)]
|
| 52 |
+
for s, e in zip(starts, stops):
|
| 53 |
+
s.record()
|
| 54 |
+
fn()
|
| 55 |
+
e.record()
|
| 56 |
+
_sync()
|
| 57 |
+
return [s.elapsed_time(e) for s, e in zip(starts, stops)]
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def measure_clock(device="cuda", ticks=4_000_000, blocks=None):
|
| 61 |
+
"""Effective SM clock in GHz under an FMA spin of `ticks` clock64 ticks.
|
| 62 |
+
Wall time over elapsed ticks is the frequency the ticks ran at."""
|
| 63 |
+
props = torch.cuda.get_device_properties(device)
|
| 64 |
+
if blocks is None:
|
| 65 |
+
blocks = props.multi_processor_count
|
| 66 |
+
out = torch.zeros(2, dtype=torch.int64, device=device)
|
| 67 |
+
sink = torch.zeros(2, dtype=torch.float32, device=device)
|
| 68 |
+
ops.mk_spin(blocks, 256, ticks, out, sink) # warm
|
| 69 |
+
start = torch.cuda.Event(enable_timing=True)
|
| 70 |
+
stop = torch.cuda.Event(enable_timing=True)
|
| 71 |
+
start.record()
|
| 72 |
+
ops.mk_spin(blocks, 256, ticks, out, sink)
|
| 73 |
+
stop.record()
|
| 74 |
+
_sync()
|
| 75 |
+
ms = start.elapsed_time(stop)
|
| 76 |
+
elapsed = int(out[0].item())
|
| 77 |
+
return elapsed / (ms * 1e6)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _median(xs):
|
| 81 |
+
s = sorted(xs)
|
| 82 |
+
n = len(s)
|
| 83 |
+
return s[n // 2] if n % 2 else 0.5 * (s[n // 2 - 1] + s[n // 2])
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _entry(value, units, method, clock_ghz, confidence, note=""):
|
| 87 |
+
d = {"value": value, "units": units, "method": method,
|
| 88 |
+
"clock_ghz": round(clock_ghz, 3), "confidence": confidence}
|
| 89 |
+
if note:
|
| 90 |
+
d["note"] = note
|
| 91 |
+
return d
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _sattolo_ring(n, device, generator=None):
|
| 95 |
+
"""A random single-cycle permutation as an int32 next-pointer table, so
|
| 96 |
+
a pointer chase visits every slot before repeating."""
|
| 97 |
+
perm = torch.randperm(n, generator=generator)
|
| 98 |
+
ring = torch.empty(n, dtype=torch.int32)
|
| 99 |
+
ring[perm] = torch.roll(perm, -1).to(torch.int32)
|
| 100 |
+
return ring.to(device)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def probe_device(device="cuda", quick=False):
|
| 104 |
+
"""Empirical device dossier. Each entry is measured by a dedicated
|
| 105 |
+
probe kernel and carries units, method, the SM clock it ran under, and
|
| 106 |
+
a confidence field."""
|
| 107 |
+
props = torch.cuda.get_device_properties(device)
|
| 108 |
+
sm = props.multi_processor_count
|
| 109 |
+
l2 = props.L2_cache_size
|
| 110 |
+
d = {
|
| 111 |
+
"device": {
|
| 112 |
+
"name": props.name,
|
| 113 |
+
"sm_arch": f"sm{props.major}{props.minor}",
|
| 114 |
+
"sms": sm,
|
| 115 |
+
"l2_bytes": l2,
|
| 116 |
+
"vram_bytes": props.total_memory,
|
| 117 |
+
"torch": torch.__version__,
|
| 118 |
+
"cuda_runtime": torch.version.cuda,
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
sink = torch.zeros(2, dtype=torch.float32, device=device)
|
| 122 |
+
out64 = torch.zeros(2, dtype=torch.int64, device=device)
|
| 123 |
+
reps = 3 if quick else 5
|
| 124 |
+
|
| 125 |
+
# ---- clock under sustained load
|
| 126 |
+
traj = []
|
| 127 |
+
windows = 8 if quick else 24
|
| 128 |
+
for _ in range(windows):
|
| 129 |
+
traj.append(measure_clock(device, ticks=30_000_000))
|
| 130 |
+
d["clock"] = {
|
| 131 |
+
"max_ghz": round(max(traj), 3),
|
| 132 |
+
"min_ghz": round(min(traj), 3),
|
| 133 |
+
"final_ghz": round(traj[-1], 3),
|
| 134 |
+
"sag_pct": round(100 * (1 - traj[-1] / max(traj)), 1),
|
| 135 |
+
"units": "GHz",
|
| 136 |
+
"method": f"{windows} back-to-back FMA spin windows of 30M ticks; "
|
| 137 |
+
"wall/ticks per window",
|
| 138 |
+
"confidence": "high",
|
| 139 |
+
}
|
| 140 |
+
clk = max(traj)
|
| 141 |
+
|
| 142 |
+
# ---- HBM bandwidth: triad per access width, plus read-only,
|
| 143 |
+
# write-only, and random-gather
|
| 144 |
+
n4 = 16 << 20 # 16M float4 = 256 MB per buffer
|
| 145 |
+
if props.total_memory < 4 << 30:
|
| 146 |
+
n4 = 4 << 20
|
| 147 |
+
a = torch.rand(n4 * 4, device=device)
|
| 148 |
+
b = torch.rand(n4 * 4, device=device)
|
| 149 |
+
c = torch.empty(n4 * 4, device=device)
|
| 150 |
+
hbm = {}
|
| 151 |
+
for width in (1, 2, 4):
|
| 152 |
+
ops.mk_triad(a, b, c, width, 1.5) # warm
|
| 153 |
+
ms = _median(_events(lambda w=width: ops.mk_triad(a, b, c, w, 1.5),
|
| 154 |
+
reps))
|
| 155 |
+
gbs = 3 * a.numel() * 4 / (ms * 1e-3) / 1e9
|
| 156 |
+
hbm[f"triad_{4 * width}B"] = round(gbs, 1)
|
| 157 |
+
ops.mk_read(a, sink)
|
| 158 |
+
ms = _median(_events(lambda: ops.mk_read(a, sink), reps))
|
| 159 |
+
hbm["read_only"] = round(a.numel() * 4 / (ms * 1e-3) / 1e9, 1)
|
| 160 |
+
ops.mk_write(c, 1.0)
|
| 161 |
+
ms = _median(_events(lambda: ops.mk_write(c, 1.0), reps))
|
| 162 |
+
hbm["write_only"] = round(c.numel() * 4 / (ms * 1e-3) / 1e9, 1)
|
| 163 |
+
g = torch.Generator(device="cpu").manual_seed(7)
|
| 164 |
+
idx = torch.randint(0, n4, (n4,), generator=g,
|
| 165 |
+
dtype=torch.int32).to(device)
|
| 166 |
+
ops.mk_gather(a, idx, c, 256, 0)
|
| 167 |
+
ms = _median(_events(lambda: ops.mk_gather(a, idx, c, 256, 0), reps))
|
| 168 |
+
# traffic accounting: idx reads + 16B random loads + streamed stores
|
| 169 |
+
hbm["random_gather_16B"] = round(
|
| 170 |
+
(idx.numel() * 4 + 2 * n4 * 16) / (ms * 1e-3) / 1e9, 1)
|
| 171 |
+
d["hbm_bw"] = _entry(hbm, "GB/s", "streaming triad c=a+s*b per access "
|
| 172 |
+
"width; read-only fold; write-only fill; random "
|
| 173 |
+
"float4 gather (16B granules, nominal traffic)",
|
| 174 |
+
clk, "high",
|
| 175 |
+
"gather moves full cache lines per 16B granule, "
|
| 176 |
+
"so its nominal number is the achievable rate for "
|
| 177 |
+
"random access, not a line-efficiency claim")
|
| 178 |
+
|
| 179 |
+
# ---- L2 bandwidth (working set 3 buffers inside L2)
|
| 180 |
+
ws = max(l2 // 4, 1 << 20)
|
| 181 |
+
nl2 = ws // 16 # float4 elems per buffer
|
| 182 |
+
al = a[:nl2 * 4].contiguous()
|
| 183 |
+
bl = b[:nl2 * 4].contiguous()
|
| 184 |
+
cl = c[:nl2 * 4].contiguous()
|
| 185 |
+
passes = 64
|
| 186 |
+
ops.mk_triad_passes(al, bl, cl, 1.5, passes)
|
| 187 |
+
ms = _median(_events(
|
| 188 |
+
lambda: ops.mk_triad_passes(al, bl, cl, 1.5, passes), reps))
|
| 189 |
+
gbs = 3 * al.numel() * 4 * passes / (ms * 1e-3) / 1e9
|
| 190 |
+
d["l2_bw"] = _entry(round(gbs, 1), "GB/s",
|
| 191 |
+
f"in-kernel repeated triad passes over "
|
| 192 |
+
f"{3 * ws >> 20} MB resident working set", clk,
|
| 193 |
+
"medium", "first pass streams from HBM; amortized "
|
| 194 |
+
f"over {passes} passes")
|
| 195 |
+
|
| 196 |
+
# ---- latencies: pointer chase per level. Footprints are chosen so
|
| 197 |
+
# the chain hits the intended level: the L2 ring (2 MB) exceeds L1,
|
| 198 |
+
# fits L2 on every supported part, and spans few enough pages to stay
|
| 199 |
+
# TLB-resident after a full-cycle warm walk; the HBM chain is long
|
| 200 |
+
# enough (2x L2 in touched lines) that it evicts itself and cannot be
|
| 201 |
+
# served from cache.
|
| 202 |
+
lat = {}
|
| 203 |
+
idle_clk = measure_clock(device, ticks=2_000_000, blocks=1)
|
| 204 |
+
ring_s = _sattolo_ring(8192, device)
|
| 205 |
+
ops.mk_chase_shared(ring_s, 1 << 14, out64)
|
| 206 |
+
_sync()
|
| 207 |
+
hops = 1 << 16
|
| 208 |
+
ops.mk_chase_shared(ring_s, hops, out64)
|
| 209 |
+
_sync()
|
| 210 |
+
lat["smem_cycles"] = round(int(out64[0].item()) / hops, 1)
|
| 211 |
+
|
| 212 |
+
l1_ring_n = (16 << 10) // 4 # 16 KB: L1-resident after a warm walk
|
| 213 |
+
ring_l1 = _sattolo_ring(l1_ring_n, device)
|
| 214 |
+
ops.mk_chase_global(ring_l1, l1_ring_n, out64)
|
| 215 |
+
_sync()
|
| 216 |
+
hops_l1 = 1 << 15
|
| 217 |
+
ops.mk_chase_global(ring_l1, hops_l1, out64)
|
| 218 |
+
_sync()
|
| 219 |
+
lat["l1_cycles"] = round(int(out64[0].item()) / hops_l1, 1)
|
| 220 |
+
|
| 221 |
+
l2_ring_n = (2 << 20) // 4
|
| 222 |
+
ring_l2 = _sattolo_ring(l2_ring_n, device)
|
| 223 |
+
ops.mk_chase_global(ring_l2, l2_ring_n, out64) # full-cycle warm walk
|
| 224 |
+
_sync()
|
| 225 |
+
hops_l2 = 1 << 14
|
| 226 |
+
ops.mk_chase_global(ring_l2, hops_l2, out64)
|
| 227 |
+
_sync()
|
| 228 |
+
lat["l2_cycles"] = round(int(out64[0].item()) / hops_l2, 1)
|
| 229 |
+
|
| 230 |
+
hbm_bytes = min(max(4 * l2, 64 << 20), props.total_memory // 8)
|
| 231 |
+
ring_h = _sattolo_ring(int(hbm_bytes) // 4, device)
|
| 232 |
+
hops_h = max((2 * l2) // 128, 1 << 17)
|
| 233 |
+
ops.mk_chase_global(ring_h, 1 << 14, out64) # fault pages / warm TLB
|
| 234 |
+
_sync()
|
| 235 |
+
ops.mk_chase_global(ring_h, hops_h, out64)
|
| 236 |
+
_sync()
|
| 237 |
+
lat["hbm_cycles"] = round(int(out64[0].item()) / hops_h, 1)
|
| 238 |
+
lat["ns_per_cycle"] = round(1.0 / idle_clk, 3)
|
| 239 |
+
d["latency"] = _entry(lat, "cycles (single-lane dependent loads)",
|
| 240 |
+
"Sattolo-cycle pointer chase; 16 KB ring for L1, "
|
| 241 |
+
"2 MB TLB-resident ring for L2, self-evicting "
|
| 242 |
+
"chain (2x L2 in touched lines) for HBM",
|
| 243 |
+
idle_clk, "high",
|
| 244 |
+
"smem includes index arithmetic; HBM includes "
|
| 245 |
+
"residual TLB effects at large footprints")
|
| 246 |
+
del ring_h, ring_l2, ring_l1, ring_s
|
| 247 |
+
|
| 248 |
+
# ---- fp32 FMA and tensor-core throughput
|
| 249 |
+
blocks = sm * 4
|
| 250 |
+
iters = 20_000 if quick else 60_000
|
| 251 |
+
ops.mk_fma_f32(blocks, 256, iters, sink)
|
| 252 |
+
ms = _median(_events(lambda: ops.mk_fma_f32(blocks, 256, iters, sink),
|
| 253 |
+
reps))
|
| 254 |
+
flops = blocks * 256 * iters * 16 * 2 * 2 # 2 chains x 2 flops x unroll
|
| 255 |
+
d["fma_f32"] = _entry(round(flops / (ms * 1e-3) / 1e12, 2), "TFLOP/s",
|
| 256 |
+
"register-resident FMA chains", clk, "high")
|
| 257 |
+
sink64 = torch.zeros(2, dtype=torch.float64, device=device)
|
| 258 |
+
it64 = max(iters // 16, 1000)
|
| 259 |
+
ops.mk_fma_f64(blocks, 256, it64, sink64)
|
| 260 |
+
ms = _median(_events(
|
| 261 |
+
lambda: ops.mk_fma_f64(blocks, 256, it64, sink64), reps))
|
| 262 |
+
flops64 = blocks * 256 * it64 * 16 * 2 * 2
|
| 263 |
+
d["fma_f64"] = _entry(round(flops64 / (ms * 1e-3) / 1e12, 3), "TFLOP/s",
|
| 264 |
+
"register-resident double FMA chains (the DFMA "
|
| 265 |
+
"pipe)", clk, "high")
|
| 266 |
+
mma = {}
|
| 267 |
+
for name, (kind, mnk) in _MMA_KINDS.items():
|
| 268 |
+
it = 30_000 if quick else 100_000
|
| 269 |
+
r = ops.mk_mma(kind, blocks, 8, 512, sink)
|
| 270 |
+
if r != 0:
|
| 271 |
+
mma[name] = "unsupported"
|
| 272 |
+
continue
|
| 273 |
+
_sync()
|
| 274 |
+
ms = _median(_events(
|
| 275 |
+
lambda k=kind, i=it: ops.mk_mma(k, blocks, 8, i, sink), reps))
|
| 276 |
+
flops = blocks * 8 * it * 4 * 2 * mnk
|
| 277 |
+
mma[name] = round(flops / (ms * 1e-3) / 1e12, 1)
|
| 278 |
+
d["mma"] = _entry(mma, "TFLOP/s (TOP/s for int8)",
|
| 279 |
+
"register-fragment mma_sync loops (wmma; fp8 via "
|
| 280 |
+
"inline PTX m16n8k32)", clk, "medium",
|
| 281 |
+
"dense-tile rate without memory traffic; real "
|
| 282 |
+
"kernels see this only when staging hides behind it")
|
| 283 |
+
|
| 284 |
+
# ---- atomic contention curve: throughput vs number of distinct
|
| 285 |
+
# addresses the whole grid hammers (slots = 1 is full contention;
|
| 286 |
+
# slots = threads is contention-free), plus the shared-memory point
|
| 287 |
+
n_thr = blocks * 256
|
| 288 |
+
buf = torch.zeros(n_thr, dtype=torch.float32, device=device)
|
| 289 |
+
at = {"curve_slots": {}}
|
| 290 |
+
it_at = 2_000 if quick else 6_000
|
| 291 |
+
slot_points = [1, 8, 64, 512, 4096, n_thr]
|
| 292 |
+
for slots in slot_points:
|
| 293 |
+
buf.zero_()
|
| 294 |
+
ops.mk_atomics(1, blocks, 256, it_at, buf, slots)
|
| 295 |
+
ms = _median(_events(
|
| 296 |
+
lambda s=slots: ops.mk_atomics(1, blocks, 256, it_at, buf, s),
|
| 297 |
+
reps))
|
| 298 |
+
at["curve_slots"][str(slots)] = round(
|
| 299 |
+
n_thr * it_at / (ms * 1e-3) / 1e9, 2)
|
| 300 |
+
ops.mk_atomics(3, blocks, 256, it_at, buf, 1)
|
| 301 |
+
ms = _median(_events(
|
| 302 |
+
lambda: ops.mk_atomics(3, blocks, 256, it_at, buf, 1), reps))
|
| 303 |
+
at["shared_same_addr"] = round(n_thr * it_at / (ms * 1e-3) / 1e9, 2)
|
| 304 |
+
d["atomics_f32"] = _entry(at, "Gop/s",
|
| 305 |
+
"atomicAdd hammer; curve sweeps the number "
|
| 306 |
+
"of distinct global addresses from 1 (full "
|
| 307 |
+
"contention) to one per thread", clk, "high")
|
| 308 |
+
|
| 309 |
+
# ---- launch overhead
|
| 310 |
+
anchor = sink
|
| 311 |
+
n_launch = 500 if quick else 2000
|
| 312 |
+
_sync()
|
| 313 |
+
t0 = time.perf_counter()
|
| 314 |
+
for _ in range(n_launch):
|
| 315 |
+
ops.mk_empty(anchor)
|
| 316 |
+
_sync()
|
| 317 |
+
queued = (time.perf_counter() - t0) / n_launch * 1e6
|
| 318 |
+
n_rt = 100 if quick else 300
|
| 319 |
+
t0 = time.perf_counter()
|
| 320 |
+
for _ in range(n_rt):
|
| 321 |
+
ops.mk_empty(anchor)
|
| 322 |
+
_sync()
|
| 323 |
+
rt = (time.perf_counter() - t0) / n_rt * 1e6
|
| 324 |
+
d["launch_overhead"] = _entry(
|
| 325 |
+
{"queued_us": round(queued, 2), "round_trip_us": round(rt, 2)},
|
| 326 |
+
"us", "empty-kernel trains, queued vs launch+sync", clk, "high",
|
| 327 |
+
"host-dependent: includes torch op dispatch")
|
| 328 |
+
|
| 329 |
+
# ---- grid barrier
|
| 330 |
+
bar_iters = 500 if quick else 2000
|
| 331 |
+
r = ops.mk_barrier(sm, 256, bar_iters, out64)
|
| 332 |
+
if r == 0:
|
| 333 |
+
_sync()
|
| 334 |
+
ticks = int(out64[0].item())
|
| 335 |
+
d["grid_barrier"] = _entry(
|
| 336 |
+
round(ticks / bar_iters / clk / 1000.0, 2), "us per grid.sync",
|
| 337 |
+
f"cooperative kernel, {sm} blocks x 256 threads", clk, "high")
|
| 338 |
+
else:
|
| 339 |
+
d["grid_barrier"] = _entry("unsupported", "", "cooperative launch "
|
| 340 |
+
"unavailable or grid too large", clk,
|
| 341 |
+
"high")
|
| 342 |
+
|
| 343 |
+
# ---- occupancy table (API) plus timed knees: the same random gather
|
| 344 |
+
# at every (threads, dyn_smem) point; when the forced blocks/SM stop
|
| 345 |
+
# covering memory latency, achieved bandwidth falls off the cliff
|
| 346 |
+
occ = {}
|
| 347 |
+
for threads in (128, 256, 512, 1024):
|
| 348 |
+
row = {}
|
| 349 |
+
for smem_kb in (0, 16, 32, 48):
|
| 350 |
+
row[f"{smem_kb}KB"] = int(ops.mk_occupancy(
|
| 351 |
+
threads, smem_kb * 1024, anchor))
|
| 352 |
+
occ[f"t{threads}"] = row
|
| 353 |
+
d["occupancy_blocks_per_sm"] = _entry(
|
| 354 |
+
occ, "blocks/SM", "occupancy API on the float4 triad kernel",
|
| 355 |
+
clk, "high", "API-derived, not timing-derived")
|
| 356 |
+
|
| 357 |
+
knee_iters = 6_000 if quick else 20_000
|
| 358 |
+
knee_blocks = sm * 8
|
| 359 |
+
knees = {}
|
| 360 |
+
for threads in (128, 256, 1024):
|
| 361 |
+
row = {}
|
| 362 |
+
for smem_kb in (0, 32, 48):
|
| 363 |
+
ops.mk_fma_dep(knee_blocks, threads, smem_kb * 1024,
|
| 364 |
+
knee_iters, sink)
|
| 365 |
+
ms = _median(_events(
|
| 366 |
+
lambda t=threads, s=smem_kb: ops.mk_fma_dep(
|
| 367 |
+
knee_blocks, t, s * 1024, knee_iters, sink), reps))
|
| 368 |
+
fl = knee_blocks * threads * knee_iters * 16 * 2
|
| 369 |
+
row[f"{smem_kb}KB"] = round(fl / (ms * 1e-3) / 1e12, 2)
|
| 370 |
+
knees[f"t{threads}"] = row
|
| 371 |
+
d["occupancy_knees_fma_tflops"] = _entry(
|
| 372 |
+
knees, "TFLOP/s", "a single dependent FMA chain per thread, timed "
|
| 373 |
+
"at forced (threads, dyn smem) occupancies over fixed total work; "
|
| 374 |
+
"throughput is proportional to resident threads until the pipe "
|
| 375 |
+
"saturates, so the cliff marks where occupancy stops covering "
|
| 376 |
+
"latency", clk, "high",
|
| 377 |
+
"timing-derived counterpart to the API table above")
|
| 378 |
+
return d
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def bench(fn, iters=100, warmup=10, bytes=None, flops=None, dossier=None,
|
| 382 |
+
dtype="fp16", clock_guard=0.97, clock_stride=8, graph=False,
|
| 383 |
+
device="cuda"):
|
| 384 |
+
"""Median-of-N cudaEvent timing with throttle rejection.
|
| 385 |
+
|
| 386 |
+
The SM clock is probed every `clock_stride` samples; samples inside a
|
| 387 |
+
window whose clock sagged below `clock_guard` x the best observed
|
| 388 |
+
clock are rejected and counted rather than silently averaged. With
|
| 389 |
+
declared `bytes`/`flops` per call, reports achieved GB/s / TFLOP/s;
|
| 390 |
+
with a `dossier`, adds roofline placement against the measured peaks.
|
| 391 |
+
|
| 392 |
+
`graph=True` captures fn() into a CUDA graph and times graph replays,
|
| 393 |
+
stripping Python and torch dispatch from the measurement: the right
|
| 394 |
+
mode for kernels in the few-microsecond range, where dispatch is
|
| 395 |
+
otherwise the thing being measured. Capture requires fn to be
|
| 396 |
+
graph-safe (no sync, no allocation growth, static shapes)."""
|
| 397 |
+
for _ in range(warmup):
|
| 398 |
+
fn()
|
| 399 |
+
_sync()
|
| 400 |
+
if graph:
|
| 401 |
+
g = torch.cuda.CUDAGraph()
|
| 402 |
+
with torch.cuda.graph(g):
|
| 403 |
+
fn()
|
| 404 |
+
fn = g.replay
|
| 405 |
+
for _ in range(warmup):
|
| 406 |
+
fn()
|
| 407 |
+
_sync()
|
| 408 |
+
samples = []
|
| 409 |
+
clocks = []
|
| 410 |
+
stride = clock_stride
|
| 411 |
+
for base in range(0, iters, stride):
|
| 412 |
+
clocks.append(measure_clock(device, ticks=1_500_000))
|
| 413 |
+
n = min(stride, iters - base)
|
| 414 |
+
samples.append((_events(fn, n), len(clocks) - 1))
|
| 415 |
+
clocks.append(measure_clock(device, ticks=1_500_000))
|
| 416 |
+
best_clk = max(clocks)
|
| 417 |
+
kept, rejected = [], 0
|
| 418 |
+
flat = []
|
| 419 |
+
for group, ci in samples:
|
| 420 |
+
window_ok = (clocks[ci] >= clock_guard * best_clk and
|
| 421 |
+
clocks[min(ci + 1, len(clocks) - 1)] >=
|
| 422 |
+
clock_guard * best_clk)
|
| 423 |
+
for ms in group:
|
| 424 |
+
flat.append(ms)
|
| 425 |
+
if window_ok:
|
| 426 |
+
kept.append(ms)
|
| 427 |
+
else:
|
| 428 |
+
rejected += 1
|
| 429 |
+
if not kept:
|
| 430 |
+
kept = flat
|
| 431 |
+
med = _median(kept)
|
| 432 |
+
rep = {
|
| 433 |
+
"median_ms": round(med, 4),
|
| 434 |
+
"min_ms": round(min(kept), 4),
|
| 435 |
+
"mean_ms": round(sum(kept) / len(kept), 4),
|
| 436 |
+
"samples": len(flat),
|
| 437 |
+
"rejected_throttled": rejected,
|
| 438 |
+
"graph_captured": bool(graph),
|
| 439 |
+
"clock_ghz": {"best": round(best_clk, 3),
|
| 440 |
+
"worst": round(min(clocks), 3)},
|
| 441 |
+
}
|
| 442 |
+
if bytes is not None:
|
| 443 |
+
rep["achieved_gbs"] = round(bytes / (med * 1e-3) / 1e9, 1)
|
| 444 |
+
if flops is not None:
|
| 445 |
+
rep["achieved_tflops"] = round(flops / (med * 1e-3) / 1e12, 2)
|
| 446 |
+
if bytes and flops:
|
| 447 |
+
rep["arithmetic_intensity"] = round(flops / bytes, 2)
|
| 448 |
+
if dossier is not None and bytes and flops:
|
| 449 |
+
bw = max(dossier["hbm_bw"]["value"].values()) * 1e9
|
| 450 |
+
mma_v = dossier["mma"]["value"].get(dtype)
|
| 451 |
+
peak = (mma_v if isinstance(mma_v, (int, float))
|
| 452 |
+
else dossier["fma_f32"]["value"]) * 1e12
|
| 453 |
+
ai = flops / bytes
|
| 454 |
+
roof = min(peak, ai * bw)
|
| 455 |
+
rep["roofline"] = {
|
| 456 |
+
"bound": "memory" if ai * bw < peak else "compute",
|
| 457 |
+
"ridge_flops_per_byte": round(peak / bw, 1),
|
| 458 |
+
"pct_of_roof": round(100 * (flops / (med * 1e-3)) / roof, 1),
|
| 459 |
+
"peak_dtype": dtype,
|
| 460 |
+
}
|
| 461 |
+
return rep
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
def _to_ordered_int(t):
|
| 465 |
+
"""Map float bit patterns to integers whose difference is the ULP
|
| 466 |
+
distance: positive floats keep their bits, negative floats mirror
|
| 467 |
+
below zero, so the mapping is monotone in the float value."""
|
| 468 |
+
if t.dtype == torch.float32:
|
| 469 |
+
u = t.view(torch.int32).to(torch.int64) & 0xFFFFFFFF
|
| 470 |
+
half = 1 << 31
|
| 471 |
+
elif t.dtype in (torch.float16, torch.bfloat16):
|
| 472 |
+
u = t.view(torch.int16).to(torch.int64) & 0xFFFF
|
| 473 |
+
half = 1 << 15
|
| 474 |
+
elif t.dtype == torch.float64:
|
| 475 |
+
i = t.view(torch.int64)
|
| 476 |
+
return torch.where(i >= 0, i, (-(1 << 63)) - i)
|
| 477 |
+
else:
|
| 478 |
+
raise ValueError(f"unsupported dtype {t.dtype}")
|
| 479 |
+
return torch.where(u < half, u, half - u)
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
def ulp_diff(a, b):
|
| 483 |
+
"""Elementwise ULP distance between same-dtype float tensors. NaNs in
|
| 484 |
+
the same position count as 0; a one-sided NaN counts as 2**62."""
|
| 485 |
+
assert a.dtype == b.dtype and a.shape == b.shape
|
| 486 |
+
ia, ib = _to_ordered_int(a), _to_ordered_int(b)
|
| 487 |
+
d = (ia - ib).abs()
|
| 488 |
+
both_nan = torch.isnan(a) & torch.isnan(b)
|
| 489 |
+
one_nan = torch.isnan(a) ^ torch.isnan(b)
|
| 490 |
+
d = torch.where(both_nan, torch.zeros_like(d), d)
|
| 491 |
+
d = torch.where(one_nan, torch.full_like(d, 1 << 62), d)
|
| 492 |
+
return d
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
class order_shuffles:
|
| 496 |
+
"""Built-in order shuffles for the common contractual-noise axes."""
|
| 497 |
+
|
| 498 |
+
@staticmethod
|
| 499 |
+
def last_dim():
|
| 500 |
+
"""Permute the last dim of every tensor arg identically: the
|
| 501 |
+
accumulation order of a last-dim reduction changes, the value
|
| 502 |
+
does not."""
|
| 503 |
+
def shuffle(args, seed):
|
| 504 |
+
g = torch.Generator(device=args[0].device).manual_seed(seed)
|
| 505 |
+
p = torch.randperm(args[0].shape[-1], device=args[0].device,
|
| 506 |
+
generator=g)
|
| 507 |
+
return tuple(x[..., p] for x in args)
|
| 508 |
+
return shuffle
|
| 509 |
+
|
| 510 |
+
@staticmethod
|
| 511 |
+
def matmul_k():
|
| 512 |
+
"""Permute K of (A [.., M, K], B [.., K, N]) consistently: the
|
| 513 |
+
product is unchanged, the accumulation order is not."""
|
| 514 |
+
def shuffle(args, seed):
|
| 515 |
+
a_m, b_m = args[0], args[1]
|
| 516 |
+
g = torch.Generator(device=a_m.device).manual_seed(seed)
|
| 517 |
+
p = torch.randperm(a_m.shape[-1], device=a_m.device,
|
| 518 |
+
generator=g)
|
| 519 |
+
return (a_m[..., p], b_m[..., p, :]) + tuple(args[2:])
|
| 520 |
+
return shuffle
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
def _as_outputs(x):
|
| 524 |
+
return list(x) if isinstance(x, (tuple, list)) else [x]
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
def compare(ref, cand, gen, trials=8, order_shuffle=None, order_trials=8,
|
| 528 |
+
ulp_tol=None, atol=None, rtol=None, band_margin=4.0, seed=0):
|
| 529 |
+
"""Adversarial correctness comparison with ULP evidence.
|
| 530 |
+
|
| 531 |
+
`gen(seed) -> args` builds inputs; `ref(*args)` and `cand(*args)`
|
| 532 |
+
return a tensor or a tuple of tensors (each output is compared and
|
| 533 |
+
the worst governs). If `order_shuffle(args, seed) -> args` is given
|
| 534 |
+
and reorders contractual-noise dimensions (see `order_shuffles` for
|
| 535 |
+
the common cases), the reference is treated as a distribution:
|
| 536 |
+
candidates are judged against the measured reduction-order band
|
| 537 |
+
rather than a point. Verdict, stated in the report: max ULP <=
|
| 538 |
+
band_margin * band + 2 when a band is measured, else max ULP <=
|
| 539 |
+
ulp_tol (default 2); if `atol`/`rtol` are given they are an
|
| 540 |
+
alternative acceptance route, checked as |c - r| <= atol + rtol|r|."""
|
| 541 |
+
worst = {"max_ulp": -1}
|
| 542 |
+
total_hist = {}
|
| 543 |
+
per_trial = []
|
| 544 |
+
max_abs_seen = 0.0
|
| 545 |
+
max_rel_seen = 0.0
|
| 546 |
+
for t in range(trials):
|
| 547 |
+
args = gen(seed + t)
|
| 548 |
+
r_outs = _as_outputs(ref(*args))
|
| 549 |
+
c_outs = _as_outputs(cand(*args))
|
| 550 |
+
assert len(r_outs) == len(c_outs), "output arity mismatch"
|
| 551 |
+
# the order band is POINTWISE: a near-zero reduction output is
|
| 552 |
+
# legitimately thousands of ULP order-sensitive (cancellation)
|
| 553 |
+
# while its neighbors are stable, so each element is judged
|
| 554 |
+
# against its own measured sensitivity, not a global max
|
| 555 |
+
bands = None
|
| 556 |
+
band = 0
|
| 557 |
+
band_score = 0.0
|
| 558 |
+
if order_shuffle is not None:
|
| 559 |
+
bands = [torch.zeros_like(ro, dtype=torch.int64)
|
| 560 |
+
for ro in r_outs]
|
| 561 |
+
for k in range(order_trials):
|
| 562 |
+
r2 = _as_outputs(
|
| 563 |
+
ref(*order_shuffle(args, 1000 + seed + t * 131 + k)))
|
| 564 |
+
for bi, (ro, r2o) in enumerate(zip(r_outs, r2)):
|
| 565 |
+
bands[bi] = torch.maximum(bands[bi],
|
| 566 |
+
ulp_diff(ro, r2o))
|
| 567 |
+
band = max(int(b.max().item()) if b.numel() else 0
|
| 568 |
+
for b in bands)
|
| 569 |
+
max_ulp = 0
|
| 570 |
+
mean_ulp = 0.0
|
| 571 |
+
first_div = -1
|
| 572 |
+
n_el = 0
|
| 573 |
+
nan_r = nan_c = inf_r = inf_c = 0
|
| 574 |
+
for oi, (r, c) in enumerate(zip(r_outs, c_outs)):
|
| 575 |
+
d = ulp_diff(r, c)
|
| 576 |
+
if bands is not None and d.numel():
|
| 577 |
+
ratio = d.double() / (bands[oi].double() + 2.0)
|
| 578 |
+
band_score = max(band_score, float(ratio.max().item()))
|
| 579 |
+
if d.numel():
|
| 580 |
+
max_ulp = max(max_ulp, int(d.max().item()))
|
| 581 |
+
mean_ulp += float(d.double().sum().item())
|
| 582 |
+
n_el += d.numel()
|
| 583 |
+
nz = (d > 0)
|
| 584 |
+
if first_div < 0 and nz.any():
|
| 585 |
+
first_div = int(nz.flatten().nonzero()[0].item())
|
| 586 |
+
if nz.any():
|
| 587 |
+
buckets = torch.clamp(
|
| 588 |
+
torch.log2(d[nz].double()).floor(), 0, 62
|
| 589 |
+
).to(torch.int64)
|
| 590 |
+
counts = torch.bincount(buckets, minlength=63)
|
| 591 |
+
for bkt in counts.nonzero().flatten().tolist():
|
| 592 |
+
total_hist[bkt] = total_hist.get(bkt, 0) + int(
|
| 593 |
+
counts[bkt].item())
|
| 594 |
+
fin = torch.isfinite(r) & torch.isfinite(c)
|
| 595 |
+
if fin.any():
|
| 596 |
+
diff = (c[fin].double() - r[fin].double()).abs()
|
| 597 |
+
max_abs_seen = max(max_abs_seen,
|
| 598 |
+
float(diff.max().item()))
|
| 599 |
+
rel = diff / r[fin].double().abs().clamp(min=1e-300)
|
| 600 |
+
max_rel_seen = max(max_rel_seen,
|
| 601 |
+
float(rel.max().item()))
|
| 602 |
+
nan_r += int(torch.isnan(r).sum().item())
|
| 603 |
+
nan_c += int(torch.isnan(c).sum().item())
|
| 604 |
+
inf_r += int(torch.isinf(r).sum().item())
|
| 605 |
+
inf_c += int(torch.isinf(c).sum().item())
|
| 606 |
+
trial_rep = {
|
| 607 |
+
"max_ulp": max_ulp,
|
| 608 |
+
"mean_ulp": round(mean_ulp / max(n_el, 1), 3),
|
| 609 |
+
"first_divergent_index": first_div,
|
| 610 |
+
"nan_ref": nan_r, "nan_cand": nan_c,
|
| 611 |
+
"inf_ref": inf_r, "inf_cand": inf_c,
|
| 612 |
+
"order_band_ulp": band,
|
| 613 |
+
"band_score": round(band_score, 2),
|
| 614 |
+
}
|
| 615 |
+
per_trial.append(trial_rep)
|
| 616 |
+
if max_ulp > worst["max_ulp"]:
|
| 617 |
+
worst = trial_rep
|
| 618 |
+
band_max = max(t["order_band_ulp"] for t in per_trial)
|
| 619 |
+
max_seen = max(t["max_ulp"] for t in per_trial)
|
| 620 |
+
if order_shuffle is not None:
|
| 621 |
+
score = max(t["band_score"] for t in per_trial)
|
| 622 |
+
ulp_ok = score <= band_margin
|
| 623 |
+
criterion = (f"pointwise max(ulp / (order_band + 2)) = "
|
| 624 |
+
f"{score:.2f} <= band_margin {band_margin}")
|
| 625 |
+
else:
|
| 626 |
+
tol = ulp_tol if ulp_tol is not None else 2
|
| 627 |
+
ulp_ok = max_seen <= tol
|
| 628 |
+
criterion = f"max_ulp {max_seen} <= ulp_tol {tol}"
|
| 629 |
+
tol_ok = False
|
| 630 |
+
if atol is not None or rtol is not None:
|
| 631 |
+
tol_ok = max_abs_seen <= (atol or 0) + (rtol or 0) * 1.0 or \
|
| 632 |
+
max_rel_seen <= (rtol or 0) or max_abs_seen <= (atol or 0)
|
| 633 |
+
criterion += (f"; OR max_abs {max_abs_seen:.3e} <= atol {atol} / "
|
| 634 |
+
f"max_rel {max_rel_seen:.3e} <= rtol {rtol}")
|
| 635 |
+
return {
|
| 636 |
+
"pass": bool(ulp_ok or tol_ok),
|
| 637 |
+
"criterion": criterion,
|
| 638 |
+
"worst_trial": worst,
|
| 639 |
+
"max_abs": max_abs_seen,
|
| 640 |
+
"max_rel": max_rel_seen,
|
| 641 |
+
"trials": per_trial,
|
| 642 |
+
"ulp_hist_log2": dict(sorted(total_hist.items())),
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
|
| 646 |
+
_FUZZ_VARIANTS = ("contig", "transposed", "strided_rows", "offset_slice",
|
| 647 |
+
"overflow_expand")
|
| 648 |
+
|
| 649 |
+
|
| 650 |
+
def _context_alive(device):
|
| 651 |
+
try:
|
| 652 |
+
t = torch.ones(4, device=device)
|
| 653 |
+
torch.cuda.synchronize()
|
| 654 |
+
return bool(t.sum().item() == 4.0)
|
| 655 |
+
except Exception: # noqa: BLE001
|
| 656 |
+
return False
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
def fuzz_shapes(op, ref, make, shapes=None, variants=_FUZZ_VARIANTS,
|
| 660 |
+
dtypes=(torch.float32,), ulp_tol=64, device="cuda"):
|
| 661 |
+
"""The ugly envelope: run op vs ref across edge shapes, dtypes, and
|
| 662 |
+
layout mutations; report ok / mismatch / exception per case.
|
| 663 |
+
|
| 664 |
+
`make(shape, dtype)` (or `make(shape)`) builds a base input, either a
|
| 665 |
+
tensor or a tuple of tensors for multi-argument ops; layout variants
|
| 666 |
+
derive non-contiguous and misaligned views whose CONTENT equals a
|
| 667 |
+
contiguous copy, so `op(*xs)` must match `ref(*xs)` on every variant.
|
| 668 |
+
`overflow_expand` stride-0-expands the input past 2**31 logical
|
| 669 |
+
elements to catch int32 indexing, and is skipped with status
|
| 670 |
+
`skipped_vram` when materializing the output would not fit. After any
|
| 671 |
+
exception the CUDA context is health-checked; if a candidate killed
|
| 672 |
+
it, remaining cases are marked `context_lost` instead of producing
|
| 673 |
+
garbage."""
|
| 674 |
+
if shapes is None:
|
| 675 |
+
shapes = [(0, 8), (1, 1), (1, 17), (3, 63), (17, 129), (128, 128),
|
| 676 |
+
(1023, 5), (2, 4096)]
|
| 677 |
+
|
| 678 |
+
def build(shape, dtype):
|
| 679 |
+
try:
|
| 680 |
+
out = make(tuple(shape), dtype)
|
| 681 |
+
except TypeError:
|
| 682 |
+
out = make(tuple(shape))
|
| 683 |
+
return out if isinstance(out, (tuple, list)) else (out,)
|
| 684 |
+
|
| 685 |
+
def variant_of(x, v):
|
| 686 |
+
if v == "contig":
|
| 687 |
+
return x
|
| 688 |
+
if v == "transposed":
|
| 689 |
+
return x.t() if x.dim() == 2 else x
|
| 690 |
+
if v == "strided_rows":
|
| 691 |
+
return x[::2] if x.shape[0] >= 2 else x
|
| 692 |
+
if v == "offset_slice":
|
| 693 |
+
# a storage-offset-1 view: misaligned for vectorized loads
|
| 694 |
+
return x.flatten()[1:] if x.numel() > 1 else x
|
| 695 |
+
if v == "overflow_expand":
|
| 696 |
+
if x.numel() == 0:
|
| 697 |
+
return None
|
| 698 |
+
per = max(x.numel(), 1)
|
| 699 |
+
rep = (1 << 31) // per + 2
|
| 700 |
+
return x.reshape((1,) + tuple(x.shape)).expand(
|
| 701 |
+
(rep,) + tuple(x.shape))
|
| 702 |
+
return x
|
| 703 |
+
|
| 704 |
+
rows = []
|
| 705 |
+
lost = False
|
| 706 |
+
for dtype in dtypes:
|
| 707 |
+
for shape in shapes:
|
| 708 |
+
bases = build(shape, dtype)
|
| 709 |
+
for v in variants:
|
| 710 |
+
case = {"shape": tuple(shape), "variant": v,
|
| 711 |
+
"dtype": str(dtype).replace("torch.", "")}
|
| 712 |
+
if lost:
|
| 713 |
+
case["status"] = "context_lost"
|
| 714 |
+
rows.append(case)
|
| 715 |
+
continue
|
| 716 |
+
xs = [variant_of(b, v) for b in bases]
|
| 717 |
+
if any(x is None for x in xs):
|
| 718 |
+
case["status"] = "skipped_empty"
|
| 719 |
+
rows.append(case)
|
| 720 |
+
continue
|
| 721 |
+
if v == "overflow_expand":
|
| 722 |
+
logical = max(x.numel() for x in xs)
|
| 723 |
+
need = logical * xs[0].element_size() * 2
|
| 724 |
+
free, _total = torch.cuda.mem_get_info()
|
| 725 |
+
if need > free * 0.8:
|
| 726 |
+
case["status"] = "skipped_vram"
|
| 727 |
+
case["logical_elems"] = int(logical)
|
| 728 |
+
rows.append(case)
|
| 729 |
+
continue
|
| 730 |
+
case["logical_elems"] = int(logical)
|
| 731 |
+
case["contiguous"] = bool(xs[0].is_contiguous())
|
| 732 |
+
try:
|
| 733 |
+
got = op(*xs)
|
| 734 |
+
want = ref(*xs)
|
| 735 |
+
if v == "overflow_expand":
|
| 736 |
+
# bounded comparison: the head plus the high-index
|
| 737 |
+
# tail, which is where int32 flat indexing wraps
|
| 738 |
+
w = 1 << 20
|
| 739 |
+
gf, wf = got.flatten(), want.flatten()
|
| 740 |
+
pairs = ((gf[:w], wf[:w]), (gf[-w:], wf[-w:]))
|
| 741 |
+
else:
|
| 742 |
+
pairs = ((got, want),)
|
| 743 |
+
mx = 0
|
| 744 |
+
for gp, wp in pairs:
|
| 745 |
+
d = ulp_diff(wp, gp) if gp.is_floating_point() \
|
| 746 |
+
else (wp != gp).to(torch.int64) * (1 << 62)
|
| 747 |
+
if d.numel():
|
| 748 |
+
mx = max(mx, int(d.max().item()))
|
| 749 |
+
del d
|
| 750 |
+
case["status"] = "ok" if mx <= ulp_tol else "mismatch"
|
| 751 |
+
case["max_ulp"] = mx
|
| 752 |
+
del got, want
|
| 753 |
+
except Exception as e: # noqa: BLE001 - report, don't die
|
| 754 |
+
case["status"] = "exception"
|
| 755 |
+
case["error"] = f"{type(e).__name__}: {str(e)[:120]}"
|
| 756 |
+
if not _context_alive(device):
|
| 757 |
+
lost = True
|
| 758 |
+
case["status"] = "context_killed"
|
| 759 |
+
rows.append(case)
|
| 760 |
+
n_bad = sum(1 for r in rows
|
| 761 |
+
if r["status"] not in ("ok", "skipped_vram",
|
| 762 |
+
"skipped_empty"))
|
| 763 |
+
return {"cases": rows, "failed": n_bad, "total": len(rows),
|
| 764 |
+
"context_lost": lost}
|
| 765 |
+
|
| 766 |
+
|
| 767 |
+
def sweep(factory, grid, iters=30, warmup=5, bytes=None, flops=None,
|
| 768 |
+
check=None, budget_s=None):
|
| 769 |
+
"""Launch-configuration search: `factory(**point) -> callable`; one
|
| 770 |
+
bench row per grid point, sorted fastest first. A point whose factory
|
| 771 |
+
or run raises is reported, not skipped silently. `check(fn) -> bool`
|
| 772 |
+
is an optional correctness gate recorded per row (a fast wrong config
|
| 773 |
+
must not win a sweep). `budget_s` caps total sweep time; points not
|
| 774 |
+
reached are reported as `not_run` rather than silently dropped."""
|
| 775 |
+
keys = list(grid.keys())
|
| 776 |
+
|
| 777 |
+
def points(i, acc):
|
| 778 |
+
if i == len(keys):
|
| 779 |
+
yield dict(acc)
|
| 780 |
+
return
|
| 781 |
+
for v in grid[keys[i]]:
|
| 782 |
+
acc[keys[i]] = v
|
| 783 |
+
yield from points(i + 1, acc)
|
| 784 |
+
|
| 785 |
+
rows = []
|
| 786 |
+
t_start = time.perf_counter()
|
| 787 |
+
over_budget = False
|
| 788 |
+
for p in points(0, {}):
|
| 789 |
+
row = dict(p)
|
| 790 |
+
if over_budget or (budget_s is not None and
|
| 791 |
+
time.perf_counter() - t_start > budget_s):
|
| 792 |
+
over_budget = True
|
| 793 |
+
row["status"] = "not_run"
|
| 794 |
+
row["median_ms"] = float("inf")
|
| 795 |
+
rows.append(row)
|
| 796 |
+
continue
|
| 797 |
+
try:
|
| 798 |
+
fn = factory(**p)
|
| 799 |
+
if check is not None:
|
| 800 |
+
row["check"] = bool(check(fn))
|
| 801 |
+
rep = bench(fn, iters=iters, warmup=warmup, bytes=bytes,
|
| 802 |
+
flops=flops)
|
| 803 |
+
row.update({"median_ms": rep["median_ms"],
|
| 804 |
+
"rejected_throttled": rep["rejected_throttled"]})
|
| 805 |
+
if "achieved_gbs" in rep:
|
| 806 |
+
row["achieved_gbs"] = rep["achieved_gbs"]
|
| 807 |
+
if "achieved_tflops" in rep:
|
| 808 |
+
row["achieved_tflops"] = rep["achieved_tflops"]
|
| 809 |
+
except Exception as e: # noqa: BLE001
|
| 810 |
+
row["error"] = f"{type(e).__name__}: {str(e)[:120]}"
|
| 811 |
+
row["median_ms"] = float("inf")
|
| 812 |
+
rows.append(row)
|
| 813 |
+
rows.sort(key=lambda r: (not r.get("check", True), r["median_ms"]))
|
| 814 |
+
return rows
|
| 815 |
+
|
| 816 |
+
|
| 817 |
+
def alloc_stamps(n, device="cuda"):
|
| 818 |
+
"""Device buffer for the MK_STAMP convention (metakernel_cuda/
|
| 819 |
+
stamps.cuh): the candidate kernel writes clock64() into slot i."""
|
| 820 |
+
return torch.zeros(n, dtype=torch.int64, device=device)
|
| 821 |
+
|
| 822 |
+
|
| 823 |
+
def read_stamps(stamps, labels=None, clock_ghz=None):
|
| 824 |
+
"""Decode MK_STAMP phases: consecutive deltas in ticks and in
|
| 825 |
+
microseconds under the measured (or provided) SM clock. A 1-D buffer
|
| 826 |
+
(MK_STAMP: block 0 only) yields one delta per phase; a 2-D [B, n]
|
| 827 |
+
buffer (MK_STAMP_BLOCK) yields min / median / max across blocks per
|
| 828 |
+
phase, with blocks whose stamps stayed zero excluded."""
|
| 829 |
+
if clock_ghz is None:
|
| 830 |
+
clock_ghz = measure_clock(stamps.device)
|
| 831 |
+
|
| 832 |
+
def us(ticks):
|
| 833 |
+
return round(ticks / clock_ghz / 1000.0, 3)
|
| 834 |
+
|
| 835 |
+
if stamps.dim() == 1:
|
| 836 |
+
vals = stamps.cpu().tolist()
|
| 837 |
+
phases = []
|
| 838 |
+
for i in range(len(vals) - 1):
|
| 839 |
+
ticks = vals[i + 1] - vals[i]
|
| 840 |
+
name = labels[i] if labels and i < len(labels) else f"phase_{i}"
|
| 841 |
+
phases.append({"phase": name, "ticks": ticks, "us": us(ticks)})
|
| 842 |
+
return {"phases": phases, "clock_ghz": round(clock_ghz, 3)}
|
| 843 |
+
|
| 844 |
+
m = stamps.cpu()
|
| 845 |
+
live = (m != 0).all(dim=1)
|
| 846 |
+
m = m[live]
|
| 847 |
+
deltas = (m[:, 1:] - m[:, :-1]).to(torch.float64)
|
| 848 |
+
phases = []
|
| 849 |
+
for i in range(deltas.shape[1]):
|
| 850 |
+
col = deltas[:, i]
|
| 851 |
+
name = labels[i] if labels and i < len(labels) else f"phase_{i}"
|
| 852 |
+
med = float(col.median().item()) if col.numel() else 0.0
|
| 853 |
+
phases.append({
|
| 854 |
+
"phase": name,
|
| 855 |
+
"ticks_median": int(med),
|
| 856 |
+
"ticks_min": int(col.min().item()) if col.numel() else 0,
|
| 857 |
+
"ticks_max": int(col.max().item()) if col.numel() else 0,
|
| 858 |
+
"us_median": us(med),
|
| 859 |
+
"blocks": int(col.numel()),
|
| 860 |
+
})
|
| 861 |
+
return {"phases": phases, "clock_ghz": round(clock_ghz, 3),
|
| 862 |
+
"blocks_reporting": int(live.sum().item()),
|
| 863 |
+
"blocks_total": int(live.numel())}
|
build/torch213-cxx11-cu132-x86_64-linux/_metakernel_cuda_4611d12.abi3.so
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5c655a858d5a27e42fbfc363c51f5e84f52df5ef7c4be8ad292cc91e4b5f9865
|
| 3 |
+
size 980192
|
build/torch213-cxx11-cu132-x86_64-linux/_ops.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from . import _metakernel_cuda_4611d12
|
| 3 |
+
ops = torch.ops._metakernel_cuda_4611d12
|
| 4 |
+
|
| 5 |
+
def add_op_namespace_prefix(op_name: str):
|
| 6 |
+
"""
|
| 7 |
+
Prefix op by namespace.
|
| 8 |
+
"""
|
| 9 |
+
return f"_metakernel_cuda_4611d12::{op_name}"
|
build/torch213-cxx11-cu132-x86_64-linux/metadata.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "metakernel",
|
| 3 |
+
"id": "_metakernel_cuda_4611d12",
|
| 4 |
+
"version": 1,
|
| 5 |
+
"license": "Apache-2.0",
|
| 6 |
+
"python-depends": [],
|
| 7 |
+
"backend": {
|
| 8 |
+
"type": "cuda",
|
| 9 |
+
"archs": [
|
| 10 |
+
"10.0",
|
| 11 |
+
"12.0",
|
| 12 |
+
"8.0",
|
| 13 |
+
"8.6",
|
| 14 |
+
"8.9",
|
| 15 |
+
"9.0"
|
| 16 |
+
]
|
| 17 |
+
},
|
| 18 |
+
"digest": {
|
| 19 |
+
"algorithm": "sha256",
|
| 20 |
+
"files": {
|
| 21 |
+
"__init__.py": "cZxKX5I/2EKCMC2gdCwflUsZaQJWJesmAHq2w+fVjXo=",
|
| 22 |
+
"_metakernel_cuda_4611d12.abi3.so": "XGVahY1aJ+Qvv8NjxR9ehPUt9e98S+itKSzJHktfmGU=",
|
| 23 |
+
"_ops.py": "uZlR720EcTy8M7BCydmvaZAlKonkwTU3778nhR9yQOE=",
|
| 24 |
+
"metakernel/__init__.py": "DFYPlrhXwYjEqCl/8n0SmWGZV8NFml5DPhMjKfv98GY="
|
| 25 |
+
}
|
| 26 |
+
},
|
| 27 |
+
"provenance": {
|
| 28 |
+
"kernel-builder": {
|
| 29 |
+
"version": "0.17.0-dev0",
|
| 30 |
+
"sha": "19aaa6421e674e9fecc352bbae6eab81d19a6bf4",
|
| 31 |
+
"dirty": false
|
| 32 |
+
},
|
| 33 |
+
"kernel": {
|
| 34 |
+
"sha": "4611d1213a76a9ec1d12646553a528feb60394ff",
|
| 35 |
+
"dirty": false
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
}
|
build/torch213-cxx11-cu132-x86_64-linux/metakernel/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ctypes
|
| 2 |
+
import importlib.util
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from types import ModuleType
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def _import_from_path(file_path: Path) -> ModuleType:
|
| 9 |
+
# We cannot use the module name as-is, after adding it to `sys.modules`,
|
| 10 |
+
# it would also be used for other imports. So, we make a module name that
|
| 11 |
+
# depends on the path for it to be unique using the hex-encoded hash of
|
| 12 |
+
# the path.
|
| 13 |
+
path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
|
| 14 |
+
module_name = path_hash
|
| 15 |
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
| 16 |
+
if spec is None:
|
| 17 |
+
raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
|
| 18 |
+
module = importlib.util.module_from_spec(spec)
|
| 19 |
+
if module is None:
|
| 20 |
+
raise ImportError(f"Cannot load module {module_name} from spec")
|
| 21 |
+
sys.modules[module_name] = module
|
| 22 |
+
spec.loader.exec_module(module) # type: ignore
|
| 23 |
+
return module
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
|