hanajit 0.20.1__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.
hanajit/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """hanajit: an LLVM-backed JIT for Python with full-ecosystem fallback.
2
+
3
+ Uses CPython's own parser (ast), compiles a numeric subset to native code
4
+ via llvmlite/LLVM, and transparently falls back to the interpreter for
5
+ everything else — so any Python library keeps working.
6
+ """
7
+ from .decorator import jit, pmap
8
+ from .backends.detect import detect
9
+ prange = range
10
+ from .errors import UnsupportedError
11
+
12
+ __version__ = "0.20.1"
13
+ __all__ = ["jit", "pmap", "prange", "detect", "UnsupportedError"]
hanajit/autopar.py ADDED
@@ -0,0 +1,50 @@
1
+ """Automatic parallelization: `@jit(parallel=True)` promotes the
2
+ outermost eligible `for i in range(...)` loop to `prange`, so the user
3
+ writes an ordinary Python loop and gets multithreaded execution — the
4
+ ergonomic that Taichi gets from auto-parallel top-level `for`, but with
5
+ no DSL and no new syntax.
6
+
7
+ This is a pure AST rewrite: find the single top-level `for i in range(n)`
8
+ loop, replace its iterator `range` with `prange`, and hand the result to
9
+ the existing `parallel.make_parallel` machinery (chunked, GIL-released
10
+ thread pool, reassociated reductions). If the function doesn't match the
11
+ parallelizable shape, we raise UnsupportedError and the caller compiles it
12
+ serially — auto-parallel never changes results, only scheduling.
13
+
14
+ Eligibility is intentionally conservative and identical to what
15
+ `parallel.analyze` already accepts: exactly one top-level range loop,
16
+ simple pre-assignments, a body of loop-local assigns / array writes / at
17
+ most one `acc +=` reduction, and a single return. Anything else stays
18
+ serial.
19
+ """
20
+ import ast
21
+ import copy
22
+
23
+ from .errors import UnsupportedError
24
+
25
+
26
+ def rewrite_range_to_prange(fn_ast):
27
+ """Return a copy of fn_ast with the single top-level `range` loop's
28
+ iterator replaced by `prange`. Raise UnsupportedError if there isn't
29
+ exactly one eligible top-level range loop."""
30
+ tree = copy.deepcopy(fn_ast)
31
+ body = tree.body
32
+ range_loops = [
33
+ i for i, s in enumerate(body)
34
+ if isinstance(s, ast.For) and isinstance(s.iter, ast.Call)
35
+ and isinstance(s.iter.func, ast.Name)
36
+ and s.iter.func.id in ("range", "prange")
37
+ ]
38
+ if len(range_loops) != 1:
39
+ raise UnsupportedError(
40
+ "parallel=True needs exactly one top-level range loop")
41
+ loop = body[range_loops[0]]
42
+ if loop.iter.func.id == "range":
43
+ if len(loop.iter.args) not in (1, 2):
44
+ raise UnsupportedError(
45
+ "parallel loop must be range(n) or range(lo, hi) (no step)")
46
+ loop.iter.func = ast.copy_location(ast.Name(id="prange",
47
+ ctx=ast.Load()),
48
+ loop.iter.func)
49
+ ast.fix_missing_locations(tree)
50
+ return tree
File without changes
@@ -0,0 +1,193 @@
1
+ """CPU backend: LLVM MCJIT via llvmlite.binding, called through ctypes.
2
+
3
+ Compatible with both legacy (<0.45) and new-pass-manager llvmlite APIs.
4
+ """
5
+ import ctypes
6
+ from llvmlite import binding as llvm
7
+ from ..typeinfer import I64, F64, BOOL, PF64, PI64, AF64, AI64, ARRAY_ELEM
8
+
9
+ _initialized = False
10
+ _engines = [] # keep engines alive (they own the JITed code memory)
11
+
12
+ CTYPES = {I64: ctypes.c_int64, F64: ctypes.c_double, BOOL: ctypes.c_bool,
13
+ PF64: ctypes.c_void_p, PI64: ctypes.c_void_p}
14
+
15
+
16
+ def _proto(ret_type, arg_types):
17
+ from ..codegen import arr_meta_count
18
+ argt = []
19
+ for t in arg_types:
20
+ if t in ARRAY_ELEM:
21
+ argt += ([ctypes.c_void_p]
22
+ + [ctypes.c_int64] * arr_meta_count(t))
23
+ else:
24
+ argt.append(CTYPES[t])
25
+ return ctypes.CFUNCTYPE(CTYPES[ret_type], *argt)
26
+
27
+
28
+ def _init():
29
+ global _initialized
30
+ if _initialized:
31
+ return
32
+ for fn in ("initialize", "initialize_native_target",
33
+ "initialize_native_asmprinter"):
34
+ try:
35
+ getattr(llvm, fn)()
36
+ except (AttributeError, RuntimeError):
37
+ pass # newer llvmlite auto-initializes
38
+ _initialized = True
39
+
40
+
41
+ def _optimize(mod, tm, opt, tuning=None):
42
+ try: # llvmlite >= 0.45: new pass manager
43
+ pto = llvm.create_pipeline_tuning_options(speed_level=opt)
44
+ for knob, val in (tuning or {}).items():
45
+ try:
46
+ setattr(pto, knob, val)
47
+ except (AttributeError, TypeError):
48
+ pass # knob not in this llvmlite build
49
+ pb = llvm.create_pass_builder(tm, pto)
50
+ pb.getModulePassManager().run(mod, pb)
51
+ except AttributeError:
52
+ try:
53
+ pto = llvm.PipelineTuningOptions()
54
+ pto.speed_level = opt
55
+ pb = llvm.PassBuilder(tm, pto)
56
+ pb.getModulePassManager().run(mod, pb)
57
+ except Exception:
58
+ try: # legacy pass manager
59
+ pmb = llvm.PassManagerBuilder()
60
+ pmb.opt_level = opt
61
+ pm = llvm.ModulePassManager()
62
+ pmb.populate(pm)
63
+ pm.run(mod)
64
+ except Exception:
65
+ pass # run unoptimized rather than fail
66
+
67
+
68
+ def _host_target_machine(opt):
69
+ target = llvm.Target.from_default_triple()
70
+ try: # tune codegen for the actual host CPU (AVX etc.), like numba
71
+ return target.create_target_machine(
72
+ cpu=llvm.get_host_cpu_name(),
73
+ features=llvm.get_host_cpu_features().flatten(), opt=opt)
74
+ except Exception:
75
+ return target.create_target_machine(opt=opt)
76
+
77
+
78
+ def load_cached(cached, func_name, arg_types):
79
+ """Rebuild callables from cached object code. Returns
80
+ (callable, kernel_addr, ret_type)."""
81
+ obj_bytes, meta = cached
82
+ _init()
83
+ from . import fastcall as fc
84
+ fc.register() # CPython API symbols must resolve before linking
85
+ tm = _host_target_machine(3)
86
+ backing = llvm.parse_assembly("")
87
+ engine = llvm.create_mcjit_compiler(backing, tm)
88
+ engine.add_object_file(llvm.ObjectFileRef.from_data(obj_bytes))
89
+ engine.finalize_object()
90
+ _engines.append(engine)
91
+ ret_type = meta["ret"]
92
+ kernel_addr = engine.get_function_address(func_name)
93
+ if meta.get("wrapper"):
94
+ try:
95
+ waddr = engine.get_function_address(meta["wrapper"])
96
+ return fc.make_builtin(waddr, func_name), kernel_addr, ret_type
97
+ except Exception:
98
+ pass
99
+ return (_proto(ret_type, arg_types)(kernel_addr), kernel_addr,
100
+ ret_type)
101
+
102
+
103
+ def compile_module(module, func_name, arg_types, ret_type, opt=3,
104
+ fastcall=True, nogil=False, cache_key=None,
105
+ tuning=None, tm_opt=None):
106
+ """Compile an llvmlite ir.Module.
107
+
108
+ Returns (callable, is_fastcall). With fastcall=True the callable is a
109
+ genuine CPython builtin (C-level dispatch); otherwise ctypes.
110
+ """
111
+ _init()
112
+ wrapper_name = None
113
+ if any(t in ARRAY_ELEM for t in arg_types):
114
+ fastcall = False # arrays use the expanded ctypes ABI
115
+ if fastcall:
116
+ try:
117
+ from . import fastcall as fc
118
+ fc.register()
119
+ kernel = None
120
+ for f in module.functions:
121
+ if f.name == func_name:
122
+ kernel = f
123
+ break
124
+ wrapper_name = fc.add_fastcall_wrapper(
125
+ module, kernel, arg_types, ret_type, nogil=nogil)
126
+ except Exception:
127
+ wrapper_name = None
128
+ tm = _host_target_machine(tm_opt if tm_opt is not None else opt)
129
+ mod = llvm.parse_assembly(str(module))
130
+ mod.verify()
131
+ _optimize(mod, tm, opt, tuning=tuning)
132
+
133
+ engine = llvm.create_mcjit_compiler(mod, tm)
134
+ captured = {}
135
+ if cache_key is not None:
136
+ engine.set_object_cache(
137
+ notify_func=lambda m, buf: captured.setdefault("obj", buf))
138
+ engine.finalize_object()
139
+ _engines.append(engine)
140
+ if cache_key is not None and "obj" in captured:
141
+ from .. import cache as _cache
142
+ _cache.save(cache_key, captured["obj"],
143
+ {"ret": ret_type, "args": list(arg_types),
144
+ "wrapper": wrapper_name})
145
+
146
+ kernel_addr = engine.get_function_address(func_name)
147
+ if wrapper_name is not None:
148
+ try:
149
+ from . import fastcall as fc
150
+ waddr = engine.get_function_address(wrapper_name)
151
+ return fc.make_builtin(waddr, func_name), kernel_addr
152
+ except Exception:
153
+ pass
154
+ return _proto(ret_type, arg_types)(kernel_addr), kernel_addr
155
+
156
+
157
+ def compile_raw(module, func_name, opt=3):
158
+ """Compile a module and return the raw address of one function."""
159
+ _init()
160
+ tm = _host_target_machine(opt)
161
+ mod = llvm.parse_assembly(str(module))
162
+ mod.verify()
163
+ _optimize(mod, tm, opt)
164
+ engine = llvm.create_mcjit_compiler(mod, tm)
165
+ engine.finalize_object()
166
+ _engines.append(engine)
167
+ return engine.get_function_address(func_name)
168
+
169
+
170
+ def emit_assembly(module, opt=3):
171
+ """Return native assembly text for inspection."""
172
+ _init()
173
+ tm = _host_target_machine(opt)
174
+ mod = llvm.parse_assembly(str(module))
175
+ mod.verify()
176
+ return tm.emit_assembly(mod)
177
+
178
+
179
+ def emit_cross(module, triple, cpu=""):
180
+ """Cross-compile a module's IR for another architecture (portability
181
+ check, e.g. arm64-apple-darwin for Apple Silicon)."""
182
+ _init()
183
+ try:
184
+ llvm.initialize_all_targets()
185
+ llvm.initialize_all_asmprinters()
186
+ except (AttributeError, RuntimeError):
187
+ pass
188
+ target = llvm.Target.from_triple(triple)
189
+ tm = target.create_target_machine(cpu=cpu)
190
+ ir_text = str(module)
191
+ mod = llvm.parse_assembly(ir_text)
192
+ _optimize(mod, tm, 3)
193
+ return tm.emit_assembly(mod)
@@ -0,0 +1,73 @@
1
+ """Hardware auto-detection for @jit(target="auto").
2
+
3
+ Probes for vendor runtimes by attempting to load their driver libraries
4
+ (cheap, no initialization) plus platform checks. Results are cached for
5
+ the process. Override everything with HANAJIT_TARGET=<cpu|cuda|amd|intel|metal>.
6
+ """
7
+ import ctypes
8
+ import functools
9
+ import os
10
+ import sys
11
+
12
+ _PROBES = {
13
+ "cuda": {"linux": ["libcuda.so.1", "libcuda.so"],
14
+ "win32": ["nvcuda.dll"],
15
+ "darwin": []}, # NVIDIA is dead on modern macOS
16
+ "amd": {"linux": ["libamdhip64.so", "libhsa-runtime64.so.1"],
17
+ "win32": ["amdhip64.dll"],
18
+ "darwin": []},
19
+ "intel": {"linux": ["libze_loader.so.1", "libze_loader.so"],
20
+ "win32": ["ze_loader.dll"],
21
+ "darwin": []},
22
+ }
23
+ # preference order per platform ("best" = most mature backend first)
24
+ _ORDER = {"darwin": ["metal"], "win32": ["cuda", "intel", "amd"],
25
+ "linux": ["cuda", "amd", "intel"]}
26
+
27
+
28
+ def _platform():
29
+ if sys.platform == "darwin":
30
+ return "darwin"
31
+ if sys.platform.startswith("win"):
32
+ return "win32"
33
+ return "linux"
34
+
35
+
36
+ def _loadable(names):
37
+ for n in names:
38
+ try:
39
+ ctypes.CDLL(n)
40
+ return n
41
+ except OSError:
42
+ continue
43
+ return None
44
+
45
+
46
+ @functools.lru_cache(maxsize=1)
47
+ def detect():
48
+ """Return ordered list of (target, evidence) for this machine.
49
+ Always ends with ("cpu", ...) — the only target hanajit executes on
50
+ directly today; GPU entries indicate device-code emission targets."""
51
+ plat = _platform()
52
+ found = []
53
+ forced = os.environ.get("HANAJIT_TARGET")
54
+ if forced:
55
+ return [(forced, "forced via HANAJIT_TARGET")]
56
+ if plat == "darwin":
57
+ found.append(("metal", "macOS (Metal is always present)"))
58
+ for vendor in _ORDER[plat]:
59
+ if vendor == "metal":
60
+ continue
61
+ lib = _loadable(_PROBES[vendor][plat])
62
+ if lib:
63
+ found.append((vendor, f"driver library {lib}"))
64
+ found.append(("cpu", "always available"))
65
+ return found
66
+
67
+
68
+ def best_gpu():
69
+ """Best detected GPU target, or None if the machine has no GPU runtime."""
70
+ for target, _ in detect():
71
+ if target != "cpu":
72
+ return target
73
+ return None
@@ -0,0 +1,146 @@
1
+ """C-level dispatch: JIT a CPython METH_FASTCALL wrapper in LLVM.
2
+
3
+ Instead of calling native code through ctypes (~0.45us/call), we generate
4
+ an LLVM function with the CPython fastcall ABI:
5
+
6
+ PyObject *wrapper(PyObject *self, PyObject *const *args, Py_ssize_t n)
7
+
8
+ that unboxes arguments (PyLong_AsLongLong / PyFloat_AsDouble), calls the
9
+ JITed kernel directly (same module -> inlinable), boxes the result, and is
10
+ then installed as a genuine builtin via PyCFunction_NewEx. Call overhead
11
+ drops to builtin-function level — the same trick Numba's C dispatcher uses.
12
+ """
13
+ import ctypes
14
+ from llvmlite import ir, binding as llvm
15
+ from ..typeinfer import I64, F64, BOOL, PF64, PI64, POINTER_ELEM
16
+
17
+ I8P = ir.IntType(8).as_pointer()
18
+ I64T = ir.IntType(64)
19
+ F64T = ir.DoubleType()
20
+
21
+ _PYAPI = ["PyLong_AsLongLong", "PyFloat_AsDouble", "PyLong_FromLongLong",
22
+ "PyFloat_FromDouble", "PyBool_FromLong", "PyErr_Occurred",
23
+ "PyErr_SetString", "PyEval_SaveThread", "PyEval_RestoreThread"]
24
+ _registered = False
25
+ _keepalive = []
26
+
27
+
28
+ def _register_symbols():
29
+ """Point MCJIT at the CPython API symbols in this process."""
30
+ global _registered
31
+ if _registered:
32
+ return
33
+ for name in _PYAPI:
34
+ addr = ctypes.cast(getattr(ctypes.pythonapi, name), ctypes.c_void_p).value
35
+ llvm.add_symbol(name, addr)
36
+ exc = ctypes.c_void_p.in_dll(ctypes.pythonapi, "PyExc_TypeError")
37
+ llvm.add_symbol("PyExc_TypeError", ctypes.addressof(exc))
38
+ _registered = True
39
+
40
+
41
+ def _declare(module, name, ret, args):
42
+ for f in module.functions:
43
+ if f.name == name:
44
+ return f
45
+ return ir.Function(module, ir.FunctionType(ret, args), name=name)
46
+
47
+
48
+ def add_fastcall_wrapper(module, kernel, arg_types, ret_type, nogil=False):
49
+ """Append a METH_FASTCALL wrapper for `kernel` to the module."""
50
+ wname = kernel.name + "__fastcall"
51
+ wty = ir.FunctionType(I8P, [I8P, I8P.as_pointer(), I64T])
52
+ w = ir.Function(module, wty, name=wname)
53
+ self_, argv, nargs = w.args
54
+
55
+ as_i64 = _declare(module, "PyLong_AsLongLong", I64T, [I8P])
56
+ as_f64 = _declare(module, "PyFloat_AsDouble", F64T, [I8P])
57
+ from_i64 = _declare(module, "PyLong_FromLongLong", I8P, [I64T])
58
+ from_f64 = _declare(module, "PyFloat_FromDouble", I8P, [F64T])
59
+ from_bool = _declare(module, "PyBool_FromLong", I8P, [I64T])
60
+ err_occ = _declare(module, "PyErr_Occurred", I8P, [])
61
+ err_set = _declare(module, "PyErr_SetString", ir.VoidType(), [I8P, I8P])
62
+ exc_type = ir.GlobalVariable(module, I8P, "PyExc_TypeError")
63
+
64
+ msg_text = f"{kernel.name}() takes {len(arg_types)} positional arguments\0"
65
+ msg = ir.GlobalVariable(module, ir.ArrayType(ir.IntType(8), len(msg_text)),
66
+ name=wname + ".argmsg")
67
+ msg.global_constant = True
68
+ msg.initializer = ir.Constant(msg.type.pointee,
69
+ bytearray(msg_text.encode()))
70
+
71
+ entry = w.append_basic_block("entry")
72
+ bad = w.append_basic_block("badargs")
73
+ good = w.append_basic_block("unbox")
74
+ fail = w.append_basic_block("unboxfail")
75
+ ok = w.append_basic_block("call")
76
+ b = ir.IRBuilder(entry)
77
+
78
+ b.cbranch(b.icmp_signed("==", nargs, ir.Constant(I64T, len(arg_types))),
79
+ good, bad)
80
+
81
+ b.position_at_end(bad)
82
+ b.call(err_set, [b.load(exc_type), b.bitcast(msg, I8P)])
83
+ b.ret(ir.Constant(I8P, None))
84
+
85
+ b.position_at_end(good)
86
+ unboxed = []
87
+ for i, ty in enumerate(arg_types):
88
+ slot = b.load(b.gep(argv, [ir.Constant(I64T, i)]))
89
+ if ty == F64:
90
+ unboxed.append(b.call(as_f64, [slot]))
91
+ else:
92
+ v = b.call(as_i64, [slot])
93
+ if ty == BOOL:
94
+ v = b.icmp_signed("!=", v, ir.Constant(I64T, 0))
95
+ elif ty in POINTER_ELEM: # address passed as int
96
+ from ..codegen import LLTY
97
+ v = b.inttoptr(v, LLTY[ty])
98
+ unboxed.append(v)
99
+ b.cbranch(b.icmp_unsigned("==", b.call(err_occ, []),
100
+ ir.Constant(I8P, None)), ok, fail)
101
+
102
+ b.position_at_end(fail)
103
+ b.ret(ir.Constant(I8P, None))
104
+
105
+ b.position_at_end(ok)
106
+ if nogil:
107
+ save = _declare(module, "PyEval_SaveThread", I8P, [])
108
+ restore = _declare(module, "PyEval_RestoreThread", ir.VoidType(), [I8P])
109
+ tstate = b.call(save, []) # release the GIL
110
+ res = b.call(kernel, unboxed) # pure native compute, no CPython API
111
+ b.call(restore, [tstate]) # reacquire before boxing
112
+ else:
113
+ res = b.call(kernel, unboxed)
114
+ if ret_type == F64:
115
+ obj = b.call(from_f64, [res])
116
+ elif ret_type == BOOL:
117
+ obj = b.call(from_bool, [b.zext(res, I64T)])
118
+ else:
119
+ obj = b.call(from_i64, [res])
120
+ b.ret(obj)
121
+ return wname
122
+
123
+
124
+ METH_FASTCALL = 0x0080
125
+
126
+
127
+ class _PyMethodDef(ctypes.Structure):
128
+ _fields_ = [("ml_name", ctypes.c_char_p), ("ml_meth", ctypes.c_void_p),
129
+ ("ml_flags", ctypes.c_int), ("ml_doc", ctypes.c_char_p)]
130
+
131
+
132
+ def make_builtin(wrapper_addr, name):
133
+ """Wrap a JITed fastcall address as a real CPython builtin function."""
134
+ _register_symbols()
135
+ nm = name.encode()
136
+ mdef = _PyMethodDef(nm, wrapper_addr, METH_FASTCALL, None)
137
+ _keepalive.append((nm, mdef))
138
+ new = ctypes.pythonapi.PyCFunction_NewEx
139
+ new.restype = ctypes.py_object
140
+ new.argtypes = [ctypes.POINTER(_PyMethodDef), ctypes.c_void_p,
141
+ ctypes.c_void_p]
142
+ return new(ctypes.byref(mdef), None, None)
143
+
144
+
145
+ def register():
146
+ _register_symbols()
@@ -0,0 +1,39 @@
1
+ """FPGA backend (experimental).
2
+
3
+ There is no direct LLVM->bitstream path; FPGA flows go through
4
+ High-Level Synthesis (HLS). Two practical routes from our IR:
5
+
6
+ 1. LLVM IR -> Vitis HLS: AMD/Xilinx Vitis HLS is itself LLVM-based and
7
+ accepts LLVM IR through its front-end flow (inject .ll into the
8
+ hls compilation, or use the open-source Vitis HLS LLVM fork).
9
+ 2. LLVM IR -> CIRCT: the LLVM CIRCT project (circt.llvm.org) lowers to
10
+ hardware dialects (Calyx/FIRRTL) and emits Verilog.
11
+
12
+ v0.1 exports self-contained annotated IR plus a Vitis-ready C shim so
13
+ either flow can pick it up. Iqbal-note: this pairs naturally with the
14
+ existing FPGA acceleration work — the IR here is plain scalar compute,
15
+ ideal for HLS pipelining pragmas.
16
+ """
17
+ import textwrap
18
+
19
+
20
+ def export_for_hls(module, func_name, path_prefix):
21
+ """Write <prefix>.ll and a Vitis HLS TCL stub. Returns file paths."""
22
+ ll_path = f"{path_prefix}.ll"
23
+ tcl_path = f"{path_prefix}_hls.tcl"
24
+ with open(ll_path, "w") as f:
25
+ f.write(str(module))
26
+ with open(tcl_path, "w") as f:
27
+ f.write(textwrap.dedent(f"""\
28
+ # Vitis HLS flow stub for {func_name}
29
+ open_project {func_name}_prj
30
+ set_top {func_name}
31
+ # Inject LLVM IR via the Vitis HLS LLVM front-end flow:
32
+ # https://github.com/Xilinx/HLS
33
+ open_solution sol1
34
+ set_part xcu250-figd2104-2L-e
35
+ create_clock -period 3.3
36
+ csynth_design
37
+ export_design -format ip_catalog
38
+ """))
39
+ return ll_path, tcl_path
@@ -0,0 +1,126 @@
1
+ """Multi-vendor GPU backends (experimental): NVIDIA, AMD, Intel.
2
+
3
+ All three retarget the same LLVM IR; only the triple, datalayout, and
4
+ kernel calling convention differ:
5
+
6
+ - NVIDIA: nvptx64 triple + nvvm.annotations -> PTX text
7
+ - AMD: amdgcn-amd-amdhsa + amdgpu_kernel -> GCN ISA / HSA code object
8
+ (runtime: ROCm/HIP)
9
+ - Intel: spirv64 + spir_kernel -> SPIR-V
10
+ (runtime: Level Zero / oneAPI / OpenCL)
11
+
12
+ v0.1 emits device code for inspection/offline use; host-side kernel
13
+ launch bridges are on the roadmap.
14
+ """
15
+ from llvmlite import binding as llvm
16
+
17
+ TARGETS = {
18
+ "cuda": dict(triple="nvptx64-nvidia-cuda",
19
+ datalayout="e-i64:64-i128:128-v16:16-v32:32-n16:32:64",
20
+ cpu="sm_75", callconv=None), # Turing+: CUDA 11–13
21
+ "amd": dict(triple="amdgcn-amd-amdhsa",
22
+ datalayout=("e-p:64:64-p1:64:64-p2:32:32-p3:32:32-p4:64:64-"
23
+ "p5:32:32-p6:32:32-i64:64-v16:16-v24:32-v32:32-"
24
+ "v48:64-v96:128-v192:256-v256:256-v512:512-"
25
+ "v1024:1024-v2048:2048-n32:64-S32-A5-G1-ni:7"),
26
+ cpu="gfx90a", callconv="amdgpu_kernel"),
27
+ "intel": dict(triple="spirv64-unknown-unknown",
28
+ datalayout="e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-"
29
+ "v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64",
30
+ cpu="", callconv="spir_kernel"),
31
+ }
32
+
33
+ _init_done = False
34
+
35
+
36
+ import os as _os
37
+
38
+ # AMDGPU HSA code-object version. v5 is the broadly-compatible default
39
+ # (ROCm 5.x..current, LLVM 15..latest). v6 requires LLVM>=19 toolchains and
40
+ # will fail to assemble on older ROCm. Override with the env var if needed.
41
+ AMD_CODE_OBJECT_VERSION = int(
42
+ _os.environ.get("HANAJIT_AMD_CODE_OBJECT_VERSION", "5"))
43
+
44
+
45
+ def _init():
46
+ global _init_done
47
+ if not _init_done:
48
+ try:
49
+ llvm.initialize_all_targets()
50
+ llvm.initialize_all_asmprinters()
51
+ except (AttributeError, RuntimeError):
52
+ pass
53
+ # pin AMDGPU code-object version for portable GCN output
54
+ try:
55
+ llvm.set_option("hanajit",
56
+ "--amdhsa-code-object-version=%d"
57
+ % AMD_CODE_OBJECT_VERSION)
58
+ except Exception:
59
+ pass
60
+ _init_done = True
61
+
62
+
63
+ def retarget(module, kernel_name, vendor):
64
+ cfg = TARGETS[vendor]
65
+ module.triple = cfg["triple"]
66
+ module.data_layout = cfg["datalayout"]
67
+ ir_text = str(module)
68
+ if cfg["callconv"]:
69
+ ir_text = ir_text.replace(f'define {_rettype(ir_text, kernel_name)}',
70
+ f'define {cfg["callconv"]} '
71
+ f'{_rettype(ir_text, kernel_name)}', 1)
72
+ if vendor == "cuda":
73
+ ir_text += (f'\n!nvvm.annotations = !{{!0}}\n'
74
+ f'!0 = !{{ptr @{kernel_name}, !"kernel", i32 1}}\n')
75
+ return ir_text
76
+
77
+
78
+ def _rettype(ir_text, name):
79
+ # find `define <ty> @"name"` to splice the calling convention in front
80
+ for line in ir_text.splitlines():
81
+ if line.startswith("define") and f'@"{name}"' in line:
82
+ return line[len("define "):].split(f' @"{name}"')[0] + f' @"{name}"'
83
+ return ""
84
+
85
+
86
+ # environment overrides so users retarget without editing source:
87
+ # HANAJIT_CUDA_ARCH=sm_90 HANAJIT_AMD_ARCH=gfx1100 HANAJIT_INTEL_ARCH=...
88
+ import os as _os
89
+
90
+ _ARCH_ENV = {"cuda": "HANAJIT_CUDA_ARCH", "amd": "HANAJIT_AMD_ARCH",
91
+ "intel": "HANAJIT_INTEL_ARCH"}
92
+
93
+
94
+ def resolve_arch(vendor, cpu=None):
95
+ """Explicit arg > env var > portable table default."""
96
+ if cpu:
97
+ return cpu
98
+ env = _os.environ.get(_ARCH_ENV.get(vendor, ""))
99
+ if env:
100
+ return env
101
+ return TARGETS[vendor]["cpu"]
102
+
103
+
104
+ def emit(module, kernel_name, vendor, cpu=None):
105
+ """Best-effort device-code emission. Returns (text, native: bool).
106
+
107
+ Architecture is resolved as: explicit `cpu=` > env var
108
+ (HANAJIT_CUDA_ARCH / HANAJIT_AMD_ARCH / HANAJIT_INTEL_ARCH) > a portable
109
+ default (CUDA sm_75 / AMD gfx90a). PTX and GCN are forward-compatible:
110
+ the driver re-JITs device code for a newer GPU at load time, so the
111
+ conservative default runs on the widest range of installed hardware."""
112
+ _init()
113
+ cfg = TARGETS[vendor]
114
+ arch = resolve_arch(vendor, cpu)
115
+ ir_text = retarget(module, kernel_name, vendor)
116
+ try:
117
+ target = llvm.Target.from_triple(cfg["triple"])
118
+ tm = target.create_target_machine(cpu=arch)
119
+ mod = llvm.parse_assembly(ir_text)
120
+ # mem2reg & friends: AMDGPU cannot select generic-addrspace allocas,
121
+ # and optimized IR yields cleaner PTX/GCN anyway
122
+ from .cpu import _optimize
123
+ _optimize(mod, tm, 3)
124
+ return tm.emit_assembly(mod), True
125
+ except Exception:
126
+ return ir_text, False # annotated IR for offline llc/toolchain