An LLVM-based Python JIT compiler for functions and NumPy code, no annotations needed.
An LLVM-backed JIT compiler for Python. It compiles ordinary functions and NumPy code to native machine code, and falls back to the interpreter for code it cannot compile.
Hana Jit — هانا جيت — is written as two words, and is a bilingual play on words.
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).
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
Loading Design goals:
ast module — not a restricted dialect or a new syntax.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.
Hana Jit is alpha software. The CPU compiler is stable and tested: 208 tests pass across Python 3.10–3.14 on Linux, Windows 11, and macOS (Apple Silicon). GPU support is code generation only — it emits GPU assembly that vendor toolchains accept, but does not yet launch kernels on a GPU (see Limitations). APIs may change before 1.0; pin a version if you depend on it.
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:
pip install hanajit
From GitHub:
pip install "git+https://github.com/ezducate/HanaJit.git" # pin to a released tag pip install "git+https://github.com/ezducate/[email protected]"
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.
All features beyond the base @jit decorator are opt-in.
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.
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:#2B3FC4Loading
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_reassocA 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.
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.
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.
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.
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
Loading 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
Two features are gated behind explicit opt-ins because they carry additional risk. Both 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.
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.
| 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 |
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 |
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:
evolve()) is the largest gain — up to ~4-5× — and exceeds Numba on every row with available headroom, while guaranteeing an identical result.exp/sqrt units.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
Hana Jit is approximately 3,000 lines of Python. One intermediate representation, multiple targets: