hipbridge 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hipbridge/__init__.py +69 -0
- hipbridge/analysis/__init__.py +80 -0
- hipbridge/cli.py +687 -0
- hipbridge/frontend/__init__.py +56 -0
- hipbridge/frontend/ir.py +196 -0
- hipbridge/frontend/parser.py +220 -0
- hipbridge/frontend/prelude.py +134 -0
- hipbridge/kernels/__init__.py +82 -0
- hipbridge/kernels/fused.py +146 -0
- hipbridge/kernels/norm.py +316 -0
- hipbridge/kernels/rope.py +124 -0
- hipbridge/kernels/softmax.py +103 -0
- hipbridge/kernels/wide.py +238 -0
- hipbridge/recognizers/__init__.py +6 -0
- hipbridge/recognizers/base.py +62 -0
- hipbridge/recognizers/rules/__init__.py +5 -0
- hipbridge/recognizers/rules/elementwise.py +73 -0
- hipbridge/recognizers/rules/reductions.py +77 -0
- hipbridge/synth/__init__.py +301 -0
- hipbridge/verify/__init__.py +126 -0
- hipbridge/verify/bench.py +322 -0
- hipbridge/verify/compare.py +391 -0
- hipbridge/verify/evidence.py +133 -0
- hipbridge/verify/harness.py +446 -0
- hipbridge/verify/inputs.py +203 -0
- hipbridge/verify/pipeline.py +719 -0
- hipbridge/verify/provenance.py +442 -0
- hipbridge/verify/reference.py +594 -0
- hipbridge/verify/shapes.py +103 -0
- hipbridge/verify/substitutions.py +221 -0
- hipbridge/verify/suites.py +707 -0
- hipbridge-0.1.0.dist-info/METADATA +701 -0
- hipbridge-0.1.0.dist-info/RECORD +37 -0
- hipbridge-0.1.0.dist-info/WHEEL +5 -0
- hipbridge-0.1.0.dist-info/entry_points.txt +2 -0
- hipbridge-0.1.0.dist-info/licenses/LICENSE +202 -0
- hipbridge-0.1.0.dist-info/top_level.txt +1 -0
hipbridge/__init__.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""hipbridge: recognize CUDA kernels, substitute verified AMD implementations.
|
|
2
|
+
|
|
3
|
+
Core imports cleanly with libclang alone. The heavy pieces live behind extras
|
|
4
|
+
and are reached through hipbridge.kernels / hipbridge.verify, each of which exposes
|
|
5
|
+
available() so callers can degrade instead of crashing.
|
|
6
|
+
|
|
7
|
+
`parse_file` and `parse_source` are re-exported lazily. They come from
|
|
8
|
+
frontend.parser, which imports clang.cindex at module level, and importing any
|
|
9
|
+
submodule of a package runs that package's __init__ first. So an eager
|
|
10
|
+
re-export here meant `import hipbridge.verify.provenance` loaded libclang: a
|
|
11
|
+
dependency the [verify] extra does not declare and has no use for.
|
|
12
|
+
|
|
13
|
+
Everything else stays eager, and cheaply: analysis is pure arithmetic over
|
|
14
|
+
datasheet figures and frontend.ir is stdlib dataclasses.
|
|
15
|
+
|
|
16
|
+
The recognizer package is `hipbridge.recognizers`, plural, so that `recognize`
|
|
17
|
+
names one thing. It was `hipbridge.recognize`, which collided with the function
|
|
18
|
+
re-exported here under the same name: two objects, one name, one namespace, and
|
|
19
|
+
whichever was assigned last won. Eager assignment made the function win, so the
|
|
20
|
+
collision was invisible until someone made the import lazy - and then it failed
|
|
21
|
+
only in full runs, because a module-level __getattr__ is consulted after
|
|
22
|
+
__dict__, and the import system writes a submodule into its parent's __dict__ on
|
|
23
|
+
first load. `recognize(facts)` raised "'module' object is not callable" in a
|
|
24
|
+
full test run and passed in isolation.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from typing import TYPE_CHECKING
|
|
28
|
+
|
|
29
|
+
from hipbridge.analysis import ARCHS, lds_padding, occupancy, roofline
|
|
30
|
+
from hipbridge.frontend import KernelFacts, Pattern, Recognition
|
|
31
|
+
from hipbridge.recognizers import recognize, registered
|
|
32
|
+
|
|
33
|
+
__version__ = "0.1.0"
|
|
34
|
+
|
|
35
|
+
# Defined in hipbridge.frontend.parser, which costs a libclang import.
|
|
36
|
+
_LAZY = ("parse_file", "parse_source")
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING: # pragma: no cover - for type checkers and editors only
|
|
39
|
+
from hipbridge.frontend import parse_file, parse_source
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def __getattr__(name: str):
|
|
43
|
+
if name not in _LAZY:
|
|
44
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
45
|
+
from hipbridge import frontend
|
|
46
|
+
|
|
47
|
+
value = getattr(frontend, name)
|
|
48
|
+
globals()[name] = value # resolved once, then a plain attribute
|
|
49
|
+
return value
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def __dir__() -> list[str]:
|
|
53
|
+
return sorted(__all__)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"ARCHS",
|
|
58
|
+
"KernelFacts",
|
|
59
|
+
"Pattern",
|
|
60
|
+
"Recognition",
|
|
61
|
+
"__version__",
|
|
62
|
+
"lds_padding",
|
|
63
|
+
"occupancy",
|
|
64
|
+
"parse_file",
|
|
65
|
+
"parse_source",
|
|
66
|
+
"recognize",
|
|
67
|
+
"registered",
|
|
68
|
+
"roofline",
|
|
69
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""AMD hardware analysis. Pure arithmetic over published vendor specifications.
|
|
2
|
+
|
|
3
|
+
Every number here is a datasheet figure, NOT a measurement on your device.
|
|
4
|
+
Anything derived from these is a ceiling, not a result. Use hipbridge.verify to
|
|
5
|
+
find out what your hardware actually does.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class ArchSpec:
|
|
15
|
+
name: str
|
|
16
|
+
gfx: str
|
|
17
|
+
wavefront: int
|
|
18
|
+
simds_per_cu: int
|
|
19
|
+
vgprs_per_simd: int
|
|
20
|
+
lds_bytes_per_cu: int
|
|
21
|
+
hbm_tb_s: float
|
|
22
|
+
peak_fp16_tflops: float
|
|
23
|
+
source: str = "vendor datasheet"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
ARCHS: dict[str, ArchSpec] = {
|
|
27
|
+
"cdna3": ArchSpec("MI300X", "gfx942", 64, 4, 512, 65536, 5.3, 1307.4),
|
|
28
|
+
"cdna2": ArchSpec("MI250X", "gfx90a", 64, 4, 512, 65536, 3.2, 383.0),
|
|
29
|
+
"cdna1": ArchSpec("MI100", "gfx908", 64, 4, 256, 65536, 1.2, 184.6),
|
|
30
|
+
"rdna3": ArchSpec("RX 7900 XTX", "gfx1100", 32, 2, 512, 131072, 0.96, 122.8),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def occupancy(vgprs_per_thread: int, arch: str = "cdna3") -> dict:
|
|
35
|
+
"""Wavefront occupancy per SIMD, which on AMD is VGPR limited."""
|
|
36
|
+
spec = ARCHS[arch]
|
|
37
|
+
if vgprs_per_thread <= 0:
|
|
38
|
+
raise ValueError("vgprs_per_thread must be positive")
|
|
39
|
+
max_waves = 8 if spec.wavefront == 64 else 16
|
|
40
|
+
waves = min(max_waves, spec.vgprs_per_simd // vgprs_per_thread)
|
|
41
|
+
return {
|
|
42
|
+
"arch": spec.name,
|
|
43
|
+
"gfx": spec.gfx,
|
|
44
|
+
"wavefronts_per_simd": waves,
|
|
45
|
+
"occupancy_pct": round(100.0 * waves / max_waves, 1),
|
|
46
|
+
"limited_by": "vgpr" if waves < max_waves else "none",
|
|
47
|
+
"basis": "datasheet, not measured",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def roofline(flops: float, bytes_moved: float, arch: str = "cdna3") -> dict:
|
|
52
|
+
"""Classify a kernel as compute or memory bound against the arch ceiling."""
|
|
53
|
+
spec = ARCHS[arch]
|
|
54
|
+
if bytes_moved <= 0:
|
|
55
|
+
raise ValueError("bytes_moved must be positive")
|
|
56
|
+
intensity = flops / bytes_moved
|
|
57
|
+
bw = spec.hbm_tb_s * 1e12
|
|
58
|
+
peak = spec.peak_fp16_tflops * 1e12
|
|
59
|
+
ridge = peak / bw
|
|
60
|
+
attainable = min(peak, intensity * bw)
|
|
61
|
+
return {
|
|
62
|
+
"arch": spec.name,
|
|
63
|
+
"arithmetic_intensity": round(intensity, 4),
|
|
64
|
+
"ridge_point": round(ridge, 2),
|
|
65
|
+
"bound": "compute" if intensity >= ridge else "memory",
|
|
66
|
+
"attainable_tflops": round(attainable / 1e12, 2),
|
|
67
|
+
"peak_tflops": spec.peak_fp16_tflops,
|
|
68
|
+
"basis": "datasheet ceiling, not measured",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def lds_padding(row_elems: int, elem_bytes: int = 4, banks: int = 32) -> int:
|
|
73
|
+
"""Padding in elements to break LDS bank conflicts on a 2D shared tile."""
|
|
74
|
+
if elem_bytes <= 0 or row_elems <= 0:
|
|
75
|
+
raise ValueError("row_elems and elem_bytes must be positive")
|
|
76
|
+
elems_per_bank_cycle = banks
|
|
77
|
+
return 0 if row_elems % elems_per_bank_cycle else 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
__all__ = ["ARCHS", "ArchSpec", "lds_padding", "occupancy", "roofline"]
|