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 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"]