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 +13 -0
- hanajit/autopar.py +50 -0
- hanajit/backends/__init__.py +0 -0
- hanajit/backends/cpu.py +193 -0
- hanajit/backends/detect.py +73 -0
- hanajit/backends/fastcall.py +146 -0
- hanajit/backends/fpga.py +39 -0
- hanajit/backends/gpu.py +126 -0
- hanajit/backends/metal.py +226 -0
- hanajit/backends/vectorcall.py +223 -0
- hanajit/cache.py +64 -0
- hanajit/codegen.py +1112 -0
- hanajit/decorator.py +637 -0
- hanajit/doctor.py +611 -0
- hanajit/errors.py +6 -0
- hanajit/evolve.py +314 -0
- hanajit/inline.py +188 -0
- hanajit/parallel.py +184 -0
- hanajit/rewrite.py +190 -0
- hanajit/typeinfer.py +398 -0
- hanajit-0.20.1.dist-info/METADATA +428 -0
- hanajit-0.20.1.dist-info/RECORD +25 -0
- hanajit-0.20.1.dist-info/WHEEL +5 -0
- hanajit-0.20.1.dist-info/licenses/LICENSE +202 -0
- hanajit-0.20.1.dist-info/top_level.txt +1 -0
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
|
hanajit/backends/cpu.py
ADDED
|
@@ -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()
|
hanajit/backends/fpga.py
ADDED
|
@@ -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
|
hanajit/backends/gpu.py
ADDED
|
@@ -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
|