The name
Hana Jit — هانا جيت — is written as two words, and is a bilingual play on words.
- Hana — from Moroccan Arabic (Darija) ها أنا (ha ana), meaning "here I am."
- Jit — JIT, as in a Just-In-Time compiler. In Moroccan Arabic, جيت (jit) means "I arrived."
Read either way, "Hana Jit" resolves to "here I am, a JIT compiler" or "here I am, I've arrived." The installable package is named hanajit (PyPI names cannot contain spaces).
Overview
Hana Jit compiles a Python function to native machine code through LLVM (via llvmlite) and runs that in place of the interpreter. Typical results are 10–100× faster than CPython, and comparable to Numba on the workloads it targets.
It requires no type annotations, no restructured data, and no new language. Add a decorator:
from hanajit import jit @jit def sum_squares(x): total = 0.0 for i in range(len(x)): total += x[i] * x[i] return total
The first call with a given argument type compiles a specialization; subsequent calls with the same types reuse it. Code that Hana Jit cannot compile falls back to the CPython interpreter with a single warning, so existing programs continue to run.
flowchart TD
Call[Call jitted function] --> Seen{Seen these<br/>argument types?}
Seen -->|yes| Cached[Run cached<br/>native code]
Seen -->|no| Infer{Types in the<br/>supported set?}
Infer -->|yes| Compile[Compile specialization<br/>cache it, run native]
Infer -->|no| Fallback[Warn once<br/>run in CPython]
style Cached fill:#EAEBF6,stroke:#2B3FC4
style Compile fill:#EAEBF6,stroke:#2B3FC4
style Fallback fill:#FDECEC,stroke:#C44B3F
Design goals:
- No DSL. It compiles the Python you wrote, parsed by CPython's own
astmodule — not a restricted dialect or a new syntax. - Correctness. Every optimization is either provably equivalent to the original code, or an opt-in trade-off (such as float32 precision) documented with its exact cost. Code that cannot be compiled safely runs in the interpreter rather than being miscompiled.
- Reproducible measurement. The benchmark figures below are measured and reproducible from the scripts in
benchmarks/.
Hana Jit was developed in the R&D pipeline at EZducate to accelerate numeric and array-heavy code — on-device inference, simulation, and data processing.
Status
Hana Jit is alpha software. The CPU compiler is stable and tested: 270 tests pass across Python 3.10–3.14 on Linux, Windows 11, and macOS (Apple Silicon). GPU kernels execute on-device via f.launch() through ctypes driver bridges — CUDA, Level Zero, and Vulkan validated on real hardware; HIP and Metal code-complete (see Limitations). WebAssembly and FPGA are export paths for external toolchains. APIs may change before 1.0; pin a version if you depend on it.
Installation
Requires Python 3.10 or newer. The only dependency is llvmlite, which ships prebuilt LLVM wheels for all major platforms. A separate LLVM installation is not required.
From PyPI:
From GitHub:
pip install "git+https://github.com/ezducate/HanaJit.git" # pin to a released tag pip install "git+https://github.com/ezducate/HanaJit.git@v0.23.0"
For development:
git clone https://github.com/ezducate/HanaJit.git cd HanaJit pip install -e ".[test]" # editable install with test dependencies python -m pytest tests/ -q # run the suite python -m hanajit.doctor # environment and capability diagnostic
Optional extras: hanajit[bench] adds numba and scipy for the comparison benchmarks; hanajit[test] adds the test dependencies.
Features
All features beyond the base @jit decorator are opt-in.
Base decorator
from hanajit import jit import numpy as np @jit def norm(x): total = 0.0 for i in range(len(x)): total += x[i] * x[i] return total ** 0.5 norm(np.random.rand(1_000_000))
The first call with a given argument type compiles a specialization; later calls with the same types reuse it. A call with a different type compiles a separate specialization.
Fusion engine
A NumPy expression normally allocates a temporary array for every operation (a * b produces one array, + c another). Numba does the same. Hana Jit compiles the entire expression into a single loop with no intermediate arrays:
@jit def score(a, b): # compiled to one pass over the data; no temporary arrays are allocated return np.sum(np.exp(-a * a) * b + np.where(a > 0, a, 2 * a) - np.clip(b, 0.2, 1.5))
This is a structural difference rather than a flag, so it is not affected by Numba tuning options. On a 5-operation expression, Hana Jit runs about 3× faster than NumPy and 3.7× faster than Numba.
flowchart LR
subgraph NumPy["NumPy / Numba — temporaries"]
direction LR
n1["a*a"] --> t1[(temp 1)]
t1 --> n2["exp(...)"] --> t2[(temp 2)]
t2 --> n3["* b"] --> t3[(temp 3)]
t3 --> n4["sum"]
end
subgraph Hana["Hana Jit — fused"]
direction LR
f1["one loop:<br/>acc += exp(-a[i]*a[i]) * b[i] ..."] --> f2["sum"]
end
style t1 fill:#FDECEC,stroke:#C44B3F
style t2 fill:#FDECEC,stroke:#C44B3F
style t3 fill:#FDECEC,stroke:#C44B3F
style f1 fill:#EAEBF6,stroke:#2B3FC4
The engine supports ufuncs (exp, sqrt, sin, …), comparisons, np.where, np.clip, np.minimum/maximum, and virtual arrays such as np.arange and np.linspace that are never materialized. Operations outside its scope fall back.
reduce_reassoc
A summation loop (total += x[i]) cannot be vectorized by default because each iteration depends on the previous one. NumPy reorders its summation (pairwise) to work around this. reduce_reassoc=True grants Hana Jit the same reordering permission, applied only to reduction accumulators:
@jit(reduce_reassoc=True) def total(x): acc = 0.0 for i in range(len(x)): acc += x[i] # vectorizes into parallel SIMD accumulators return acc
This reaches NumPy-class reduction throughput (about 1.5× the default) without enabling global fast-math. Integer reductions remain bit-exact. Float reductions are reordered the same way NumPy reorders them, matching NumPy to approximately 1 part in 10¹⁰ — not identical to a strict left-to-right sum, but no less accurate than np.sum. It also applies to np.sum, np.dot, and np.mean.
float32
A float32 array compiles with 32-bit arithmetic: half the memory traffic and twice the SIMD lane count of float64. The dtype selects the path; no flag is required:
@jit(reduce_reassoc=True) def total(x): acc = 0.0 for i in range(len(x)): acc += x[i] return acc total(x.astype(np.float32)) # 32-bit compute path
On a memory-bound reduction, float32 with reduce_reassoc runs about 2.7× the float64 baseline. The result carries float32 precision (approximately 7 significant digits) — a bounded trade-off, equivalent to computing in float32 elsewhere. Use it where float32 precision is sufficient.
narrow (experimental)
The integer companion to float32 mode. For a memory-bandwidth-bound integer reduction over a large 1-D int8 / int16 / int32 array, narrow mode loads the narrow elements as SIMD vectors and accumulates in a wide int64 vector — moving far fewer bytes per element while keeping the result exact:
import numpy as np from hanajit import jit @jit def total(x): acc = 0 for i in range(len(x)): acc += x[i] return acc data = np.random.default_rng(0).integers(-100, 100, 50_000_000).astype(np.int8) result = total.narrow(data, confirmed=True) # exact int64 sum, ~3× faster
The result is bit-identical to the int64 sum because accumulation is always 64-bit — there is no accumulator overflow (the failure mode of naive narrowing, where an int8 accumulator wraps around). On a memory-bound sum, measured speedups are roughly int8 2.3–3.2×, int16 2.0–2.3×, and int32 1.5× over an int64 baseline; these are bandwidth-dependent and vary by hardware.
This mode is experimental and opt-in: it requires confirmed=True, exactly like the hyper-aggressive optimizer. Unlike hyper mode, the result is exact — what is experimental is the specialized codegen path and the requirement that the input already be a narrow-dtype array. It currently accelerates the sum reduction over one narrow array; other patterns fall back to the normal compiler with a warning. int4 and int2 are not supported on CPU, because there are no sub-byte SIMD load instructions and the bit-unpacking they require eats the bandwidth saving.
For a worked scientific example, examples/rdf_narrow.py computes protein-water coordination numbers (a radial distribution function analysis) on a real solvated protein, using narrow to reduce millions of per-pair int8 indicators — the memory-bound integer sum that narrow targets.
Genetic optimizer
Different CPUs favor different compilation choices (unroll factors, vectorization widths). evolve() runs a genetic search over compilation strategies, times each candidate on the current hardware with the supplied data, and installs the fastest:
f = jit(heavy_kernel) f(example_args) # compile report = f.evolve(example_args) # search; installs the winner print(report["speedup"])
Every candidate is guaranteed to compute the same result: the genes are semantics-preserving transforms, and each candidate is checked against the baseline before it is timed. In the benchmarks below it is consistently the largest correctness-preserving gain, up to approximately 5× on some kernels.
Parallelism
from hanajit import jit, prange # auto-parallelize the outermost loop @jit(parallel=True) def process(x, out): for i in range(len(x)): out[i] = expensive(x[i]) return 0 # or use prange explicitly @jit def process2(x, out): for i in prange(len(x)): out[i] = expensive(x[i]) return 0
@jit(nogil=True) releases the GIL around a kernel so it can run alongside other Python threads. pmap parallelizes a function across a batch of argument tuples. Measured speedups on multi-core machines are in the 1.8–3.6× range; memory bandwidth is typically the limiting factor.
Dispatch overhead
On CPython 3.12+, each jitted function becomes a native vectorcall object whose dispatch is itself compiled machine code. Call overhead is approximately 20–50 nanoseconds, about 3.6× less than Numba.
flowchart TD
C[Function call] --> T1{Native vectorcall<br/>available? CPython 3.12+}
T1 -->|yes| V["HanaFunction proxy<br/>~20-50 ns"]
T1 -->|no| T2{Fastcall<br/>path?}
T2 -->|yes| FC["fastcall wrapper"]
T2 -->|no| DP["Python Dispatcher<br/>fallback"]
V --> N[Native machine code]
FC --> N
DP --> N
style V fill:#EAEBF6,stroke:#2B3FC4
style N fill:#FBF0DD,stroke:#E8A020
Helper inlining
A small @jit function called from another @jit function is inlined at the source level before compilation, removing call overhead and allowing the fusion engine to see through it:
@jit def sq(x): return x * x @jit def energy(a): total = 0.0 for i in range(len(a)): total += sq(a[i]) + sq(a[i] + 1) # sq() is inlined return total
Experimental features
Three features are gated behind explicit opt-ins because they carry additional risk. All are documented in docs/experimental.md.
@jit(rewrite=True) applies pattern-matched algebraic rewrites — for example, a loop summing an arithmetic series is replaced by its closed-form expression. Each rewrite is individually proven correct and fires only on an exact pattern match.
evolve_hyper(..., confirmed=True) extends evolve() with unsafe floating-point transforms (aggressive reassociation, reciprocals, approximate functions). It keeps the fastest candidate that matches the original within a tolerance across a large batch of random inputs. It does not guarantee correctness on untested inputs, requires confirmed=True, and is never cached. In the benchmark table below it is frequently a no-op, because the safe evolve() has usually already reached the hardware limit. It is intended for kernels where the aggressive transforms unlock additional gains, and should not be used where an incorrect result is unacceptable.
narrow(..., confirmed=True) (see the narrow section above) accelerates a memory-bound integer sum over an int8 / int16 / int32 array using narrow SIMD loads with wide accumulation. Unlike the two features above, its result is always exact; the opt-in reflects the specialized codegen path and the narrow-storage requirement, not a correctness trade-off.
Benchmarks
Measured on a single core in a shared CI container. Treat the ratios as the signal; absolute milliseconds are noisy — rerun on target hardware with the scripts in benchmarks/. Compared against NumPy 2.x and Numba 0.66.
Summary
| Benchmark | Result |
|---|---|
| 5-operation fused NumPy expression | 3.0× vs NumPy, 3.7× vs Numba |
Reduction, reduce_reassoc (float64) |
~1.5× over the default |
Reduction, reduce_reassoc + float32 |
~2.7× over the float64 baseline |
Reduction, narrow int8 (memory-bound sum) |
~2.3–3.2× over the int64 baseline |
Reduction, narrow int16 (memory-bound sum) |
~2.0–2.3× over the int64 baseline |
evolve() genetic optimizer |
up to ~5×, correctness-verified |
| Call / dispatch overhead | ~46 ns (3.6× less than Numba) |
fib(30) recursion |
1.7× vs Numba |
With GA, without GA, hyper-aggressive, and Numba
The same kernel compiled four ways:
| Workload | Hana Jit (plain) | + evolve() (safe GA) |
+ hyper-aggressive | Numba |
|---|---|---|---|---|
| fp reduction | 0.78 ms | 0.23 ms | 0.79 ms | 0.74 ms |
| poly5 eval | 1.04 ms | 0.22 ms | 1.01 ms | 0.96 ms |
| transcendental | 3.46 ms | 3.50 ms | 3.46 ms | 3.42 ms |
| dot product | 0.80 ms | 0.31 ms | 0.37 ms | 0.74 ms |
Notes:
- On scalar loops, plain Hana Jit and Numba are approximately equal, as they share the LLVM backend. Hana Jit's advantages are in fusion, dispatch, float32, and cold start.
- The safe GA (
evolve()) is the largest gain — up to ~4-5× — and exceeds Numba on every row with available headroom, while guaranteeing an identical result. - The hyper-aggressive column is frequently a no-op and in some rows slower than the safe GA, because the safe GA already reaches the hardware limit on these kernels. Recommendation: use the safe GA; the hyper-aggressive mode applies only to the narrow set of kernels where the unsafe transforms yield further gains.
- The transcendental row barely changes in any column, as it is bound by the hardware
exp/sqrtunits.
Reproduce:
pip install "hanajit[bench]" python benchmarks/bench_experimental.py # rewrite + hyper-aggressive python benchmarks/bench_reductions.py # reduce_reassoc + float32 python benchmarks/fourway.py # the four-way comparison
Architecture
Hana Jit is approximately 3,000 lines of Python. One intermediate representation, multiple targets:
- Frontend —
inspect.getsource+ast.parseproduce the exact tree CPython would execute. There is no custom parser. - Type inference — a fixpoint over a small set of types (
int64,float64,float32,bool, pointers, array shapes). Anything outside the set raises an internalUnsupportedError, which becomes a fallback to the interpreter. - Code generation — the typed tree lowers to LLVM IR, including the fusion engine that compiles array expressions into element generators fused into one loop.
- Backends — the IR module is optimized (
-O3) and either JIT-compiled for the host CPU, re-targeted for a GPU, exported as WebAssembly, or exported for FPGA synthesis.
flowchart TD
IR["Typed LLVM IR<br/>(one module)"] --> OPT["LLVM -O3"]
OPT --> CPU["CPU backend<br/>JIT → runs now ✓"]
OPT --> NV["NVIDIA → PTX<br/>launch() via nvcuda ✓"]
OPT --> AMD["AMD → GCN<br/>launch() via HIP + clang"]
OPT --> INT["Intel → SPIR-V<br/>launch() via Level Zero ✓"]
OPT --> VLK["Vulkan → SPIR-V GLCompute<br/>launch() via vulkan-1 ✓"]
OPT --> APL["Apple → Metal<br/>launch() via Metal.framework"]
OPT --> WASM["WebAssembly → .ll/.s + JS loader<br/>clang links .wasm"]
OPT --> FPGA["FPGA → HLS C++ + IR + TCL<br/>export-only"]
style CPU fill:#EAEBF6,stroke:#2B3FC4
style NV fill:#FBF0DD,stroke:#E8A020
style AMD fill:#FBF0DD,stroke:#E8A020
style INT fill:#FBF0DD,stroke:#E8A020
style VLK fill:#FBF0DD,stroke:#E8A020
style APL fill:#FBF0DD,stroke:#E8A020
style WASM fill:#FBF0DD,stroke:#E8A020
style FPGA fill:#FBF0DD,stroke:#E8A020
The CPU backend runs compiled code directly. GPU kernels emit inspectable device code and execute on the device through f.launch() — pure-ctypes bridges over the vendor driver libraries, no SDK required (see GPU execution and Limitations). The WebAssembly and FPGA paths export artifacts for external toolchains (clang / Vitis HLS).
flowchart LR
A[Python function] -->|inspect.getsource| B[Source text]
B -->|ast.parse| C[AST]
C --> D[Type inference<br/>fixpoint lattice]
D -->|supported| E[LLVM IR<br/>+ fusion engine]
D -->|unsupported| F[Interpreter fallback<br/>+ one warning]
E --> G[LLVM -O3]
G --> H1[Host CPU<br/>JIT machine code]
G --> H2[GPU target<br/>PTX / GCN / SPIR-V / Vulkan / Metal]
G --> H3[FPGA<br/>HLS C++ + IR export]
G --> H4[WebAssembly<br/>wasm32/wasm64 export]
style F fill:#FDECEC,stroke:#C44B3F
style H1 fill:#EAEBF6,stroke:#2B3FC4
style H2 fill:#FBF0DD,stroke:#E8A020
style H3 fill:#FBF0DD,stroke:#E8A020
style H4 fill:#FBF0DD,stroke:#E8A020
See docs/architecture.md for detail.
GPU execution
GPU-target kernels run on the device with f.launch(). Each vendor bridge is pure ctypes over the library the GPU driver already installs — no CUDA toolkit, no Vulkan SDK, no build step:
import numpy as np from hanajit import jit @jit(target="cuda", signature="f64*, f64*, f64, i64") # or "intel", "vulkan", "amd", "metal" def saxpy(y, x, a, n): i = block_id() * block_dim() + thread_id() if i < n: y[i] = a * x[i] + y[i] return 0 y = np.random.rand(1_000_000); x = np.random.rand(1_000_000) saxpy.launch(y, x, 2.0, len(y)) # arrays copied over, kernel runs, results copied back
Keep data resident and launch asynchronously for tight iteration loops:
yd, xd = saxpy.to_device(y), saxpy.to_device(x) # upload once for _ in range(1000): saxpy.launch(yd, xd, 0.01, len(y), sync=False) # ~0.2 ms/launch on CUDA saxpy.synchronize() result = yd.to_host()
Kernels have workgroup-shared memory, barriers, atomics, and 2-D/3-D thread indexing — enough for the standard reduction patterns:
@jit(target="cuda", signature="f64*, f64*, f64*, i64") def dot_partials(partials, a, b, n): tid = thread_id() i = block_id() * block_dim() + tid s = shared_f64(256) # workgroup-shared array acc = 0.0 if i < n: acc = a[i] * b[i] s[tid] = acc barrier() step = 128 while step > 0: # tree reduction in shared memory if tid < step: s[tid] = s[tid] + s[tid + step] barrier() step = step // 2 if tid == 0: partials[block_id()] = s[0] return 0
| target | driver library | validated on |
|---|---|---|
cuda |
nvcuda (driver API; PTX driver-JITed) |
RTX 2080 Max-Q — bit-exact vs NumPy |
intel |
ze_loader (Level Zero; hanajit's own SPIR-V generator) |
UHD Graphics 630 — bit-exact |
vulkan |
vulkan-1 (vendor-neutral; any 1.1 device with f64/i64 shaders) |
RTX 2080 Max-Q — bit-exact |
amd |
amdhip64 (HIP; GCN assembled by clang) |
code-complete, awaiting AMD hardware |
metal |
Metal.framework (macOS; runtime-compiled MSL) | code-complete, awaiting Apple hardware |
CUDA transcendentals (sin/exp/log/pow) link NVIDIA's libdevice automatically when found — pip install hanajit[cuda-math] provides it without a CUDA toolkit. Full details, caveats, and multi-GPU selection: docs/gpu.md.
WebAssembly export
export_wasm retargets the same typed LLVM IR to wasm32 (or wasm64) and writes everything needed to run the kernel in a browser or Node:
from hanajit import jit @jit def sum_squares(n): total = 0.0 for i in range(n): total += i * i return total sum_squares(10) # compile first (or pass sig=) out = sum_squares.export_wasm("out/ss") # WasmExport(ll, s, mjs, build, wasm)
<prefix>.ll— the kernel retargeted towasm32-unknown-unknown(clang input).<prefix>.s— WebAssembly assembly emitted by LLVM's wasm backend, for inspection.<prefix>.mjs— an ES-module loader: instantiates the module and maps libm calls (sin,exp, …) to JavaScriptMathimports. At the JS boundaryi64isBigInt,f64/boolareNumber.<prefix>.build.sh— the exact clang command (--target=wasm32 … -Wl,--export=<fn>). Any standard clang has the WebAssembly backend; no Emscripten needed.<prefix>.wasm— the linked module, produced automatically when clang is on PATH (orHANAJIT_WASM_CLANGpoints to one); otherwise run the build script.
inspect_wasm() returns (text, native) — the assembly text without touching disk. export_wasm(prefix, sig="f64, f64") exports a specialization without calling the function first. Pass bits=64 for wasm64.
FPGA export
An FPGA is not a processor that executes an instruction stream; it is reconfigurable hardware. An algorithm targeting an FPGA is synthesized into a circuit — loops become pipelined datapaths, multiplies map to DSP blocks, arrays to on-chip memory. Synthesis requires a licensed toolchain and produces a bitstream that configures the device. This process is ahead-of-time and cannot be performed just-in-time.
Hana Jit exports a complete Vitis HLS project kit. The export_fpga method writes up to four files:
from hanajit import jit @jit(signature="f64*, f64*, f64, i64") def saxpy(y, x, a, n): for i in range(n): y[i] = a * x[i] + y[i] return 0 out = saxpy.export_fpga("out/saxpy") # FpgaExport(ll, tcl, cpp, tb) print(out.cpp) # out/saxpy_hls.cpp — synthesizable HLS C++ top function print(out.tcl) # out/saxpy_hls.tcl — runnable Vitis HLS script
<prefix>_hls.cpp— the kernel transpiled from the typed Python AST to synthesizable C++ (the same way the Metal backend transpiles to MSL), with HLS pragmas already in place:PIPELINE II=1on innermost loops,m_axiinterfaces for pointer arguments,s_axilitefor scalars and control. Vitis HLS synthesizes this directly — the turnkey route.<prefix>_tb.cpp— a C-simulation testbench forcsim_design.<prefix>_hls.tcl— a runnable project script:csim → csynth → export_design, targeting an Alveo U250 at 3.3 ns by default. Both are parameters:export_fpga(prefix, part="xcvu9p-…", clock_ns=5.0).<prefix>.ll— the typed LLVM IR, for IR-level flows: AMD/Xilinx Vitis HLS ingests IR through its LLVM front-end flow, and LLVM's CIRCT project lowers LLVM IR to hardware dialects (FIRRTL/Calyx) and emits Verilog.
Kernels outside the transpilable subset (e.g. NumPy-array signatures — use raw-pointer signatures like "f64*" instead) still export .ll plus a TCL stub; out.cpp and out.tb are None.
Testing the FPGA export
The export can be tested without FPGA hardware or a licensed toolchain:
from hanajit import jit @jit(signature="f64*, f64*, i64") def dot(a, b, n): s = 0.0 for i in range(n): s += a[i] * b[i] return s out = dot.export_fpga("dot_export") print(open(out.cpp).read()) # HLS C++ with pragmas print(open(out.tcl).read()) # Vitis HLS script
With the Vitis toolchain and a board, the next step is vitis_hls -f dot_export_hls.tcl, followed by place-and-route to a bitstream — steps that occur in AMD's tools, outside Hana Jit. The export path is tested (the files are written, the C++ is self-contained, and the IR is self-contained); no bitstream is produced in CI, as that requires Vitis and hardware.
Limitations
Supported: numeric code, loops, recursion, scalar math, and a subset of NumPy (elementwise operations, the fusion-engine operations, reductions, slicing, 1-D and 2-D indexing, float32/float64/int64 arrays).
Falls back to the interpreter (with one warning): allocating new arrays inside a kernel, most of the object model (classes, dictionaries, arbitrary objects), generators, exceptions as control flow, string manipulation, float16/complex dtypes, and the remainder of the NumPy API. Hana Jit targets numeric kernels; code outside that scope runs in the interpreter.
GPU execution is explicit and experimental. f.launch(*args, grid=, block=) executes a GPU-target kernel on the device through a pure-ctypes bridge over the vendor's driver library (no SDK needed): CUDA (nvcuda), Intel (Level Zero), Vulkan (any 1.1 device with shaderFloat64/shaderInt64), AMD (HIP; needs a clang to assemble the code object), and Metal (macOS). The CUDA, Level Zero, and Vulkan bridges are validated on real hardware (RTX 2080 Max-Q, UHD 630); the HIP and Metal bridges are code-complete but not yet hardware-validated. Calling a GPU-target function directly (f(...)) still falls back to CPython — device execution never happens implicitly. Per-vendor caveats: CUDA transcendentals (sin/exp/log/pow) run at full f64 precision when NVIDIA's libdevice is found (CUDA toolkit or pip install hanajit[cuda-math]), and refuse to launch without it; Vulkan computes those at float32 precision; Metal computes all f64 at float32 (no double in Metal). Plain launches copy arrays both ways; f.to_device(arr) returns a resident DeviceArray that skips the copies across launches (~80× lower launch overhead measured on CUDA), and launch(..., sync=False) + f.synchronize() queues kernels without blocking. Kernels can use workgroup-shared memory (shared_f64(N)), barrier(), atomic_add() (all targets except Metal), and 2-D/3-D thread indexing — enough for the standard shared-memory reduction patterns, verified bit-exact on CUDA, Level Zero, and Vulkan.
Vulkan SPIR-V emission is best-effort. LLVM's shader-flavor SPIR-V backend rejects constructs that are routine in the other targets (notably buffer indexing under logical addressing), so emission runs in an isolated subprocess: kernels it accepts yield real GLCompute SPIR-V; the rest fall back to annotated LLVM IR (hlsl.shader/hlsl.numthreads attributes in place) for offline lowering. The workgroup size is fixed at compile time (HANAJIT_VULKAN_LOCAL_SIZE, default 64,1,1), and block_dim() folds to that constant.
WebAssembly is export only. export_wasm writes retargeted IR, wasm assembly, a JS loader, and a build script; the final .wasm link needs any standard clang (run automatically when found). Hana Jit does not embed a wasm runtime.
FPGA is export only — see the section above. It writes synthesizable HLS C++, a testbench, IR, and an HLS script; synthesis occurs in external tools.
Numerical behavior: reduce_reassoc reorders float additions (as NumPy does), so results are not bit-identical to a sequential sum but remain within NumPy-level tolerance; integers are unaffected. float32 carries float32 precision. evolve_hyper does not guarantee correctness on untested inputs.
See docs/limitations.md for the full list.
Diagnostics
The diagnostic checks compilation, dispatch, threading, caching, and the GPU code-generation backends, and its launch section runs a real kernel on every available GPU runtime bridge and reports the device it used (or the precise reason a vendor cannot launch on this machine). If ptxas or llvm-mc are on the PATH, it also runs the vendor assemblers to validate the generated GPU code. It writes hanajit_report_<platform>.md. Example reports for Linux, Windows, and macOS are in reports/.
Documentation
docs/quickstart.md— walkthroughdocs/api.md— full API referencedocs/architecture.md— compiler internalsdocs/gpu.md— GPU backends and validationdocs/performance.md— benchmark detaildocs/numpy-coverage.md— supported NumPy operationsdocs/experimental.md— experimental featuresdocs/limitations.md— full limitations listdocs/publishing.md— release processexamples/— example programs
Contributing
Issues and pull requests are welcome. Run the suite before submitting:
pip install -e ".[test]"
python -m pytest tests/ -qNew optimizations must include tests that verify the result against a reference before any performance claim. Contributions are accepted under the repository's license.
Contact
Hana Jit is developed by Iqbal Addou at EZducate (www.ezducate.ai).
- Email: iqbal.addou@gmail.com
- Work: cto@ezducate.ai
- Issues: github.com/ezducate/HanaJit/issues
License
Apache License 2.0 — see LICENSE.
Acknowledgements
Built on LLVM and llvmlite. Benchmarked against NumPy and Numba. The helper-inlining and auto-parallelization features were informed by Taichi, implemented without a DSL. Developed at EZducate.