Migrating from Numba to Pyvorin
What @njit gave you, what Pyvorin compilation gives you instead, which kernels should stay on Numba, and how to verify each migrated function.
Published Jun 30, 2026
Removing every @njit decorator from a codebase takes an afternoon. Knowing which of those functions will actually run faster under Pyvorin Native 1.0.9 — and which should never have been moved — takes a protocol. This page maps what Numba's JIT was doing for you onto what Pyvorin's local native compiler does instead, states plainly the workloads that should stay on Numba, and gives you a per-function verification sequence built entirely from commands we ran against the installed 1.0.9 build.
The short version: Numba and Pyvorin optimise different things. Numba specialises numerical kernels on NumPy dtypes and can target a GPU. Pyvorin compiles whole Python functions to native machine code locally, with no annotations and no dtype contract, and falls back to honest CPython execution wherever it cannot. A migration is a change of model, not a search-and-replace.
What @njit was actually doing for you
Numba's nopython mode is a type-specialising JIT. At call time it inspects the argument types — float64[:], int64[:, :] — generates machine code specialised to those dtypes, and caches the specialisation. Three consequences shaped how Numba code gets written. First, kernels are written around NumPy arrays, because that is where the type system and the speed live. Second, the first call to each type combination pays a compilation pause, which is why cache=True exists. Third, anything the type inference cannot resolve silently pushes the function back to object mode, so Numba users learn to keep functions small, typed and self-contained.
Numba also gives you a CUDA target: the same decorated kernel, launched on a GPU. That capability has no counterpart in Pyvorin 1.0.9, and it drives the single most important stay-or-go decision in this migration.
What Pyvorin gives you instead
Pyvorin compiles whole functions — loops, branches, tuples, nested calls and all — from plain Python source into native code through LLVM, in-process, on your machine. There are no decorators to keep and no type annotations to satisfy: the function you already have is the input. Compilation happens locally; the compile path contains no network code and your source never leaves the machine.
Two behavioural differences follow. Pyvorin does not hold you to a dtype contract at the Python boundary; it compiles the function as written and installs runtime guards on the arguments instead, diverting to the original Python on guard failure. And compilation is file-scoped and cache-backed rather than call-triggered: a compiled artefact lands in a disk cache keyed by the function source, so repeat runs load instead of recompile. The trade is control for reach: you give up per-dtype specialisation and get the whole function, including the parts Numba never saw.
When not to migrate
Two workload classes should stay on Numba, and saying so upfront saves you a week.
GPU workloads. If your kernels run through Numba's CUDA target, Pyvorin 1.0.9 is not a destination. The shipped wheel is a CPU build — cp312-linux_x86_64 — the CLI exposes no device or GPU flag of any kind, and the SIMD registry the compiler draws from is CPU kernels: on our test host simd-info reports AVX2 detection and 81 registered kernels, all scalar and array CPU operations. Nothing in the product targets a GPU. The fuller head-to-head is on the Pyvorin vs Numba page.
NumPy-construct-heavy kernels. This one surprises migrating teams, so here is the verified behaviour. Numba's natural habitat — tight loops over NumPy arrays with np.empty allocations and RNG calls — is only partially supported by Pyvorin 1.0.9. We migrated a representative Numba kernel and ran the diagnosis:
pairwise_distances
Status: FAILED
Error: CPython raised ImportError, compiled raised IndexError
Unsupported features:
- numpy.empty (pairwise_distances): Unsupported feature encountered;
function executes in compatibility mode
- numpy.default_rng (pairwise_distances): Unsupported feature encountered;
function executes in compatibility mode
- unknown-receiver method call: .random (pairwise_distances):
Unsupported feature encountered; function executes in compatibility mode
The same run surfaced a second, sharper constraint: the benchmark harness's ground-truth sandbox refuses NumPy imports outright — import of 'numpy' is not allowed in the sandbox — so NumPy-based functions cannot even be correctness-checked by bench on this build. Pure-Python equivalents, written over lists and arithmetic instead of arrays, compile and win: the canonical suite's numerical category runs at a 10.91x geometric mean, topped by numerical.trapezoidal_integral at 202.83x. The migration move for array kernels is usually to rewrite the kernel over Python data first, then compile — not to carry the NumPy calls across unchanged.
The mechanical migration
For everything that passes the stay-or-go filter, the mechanics are genuinely simple. Inventory the decorators:
grep -rn "@njit\|@jit\|from numba" --include="*.py" src/
Then strip the decorator and the import, keeping the function body byte-for-byte identical:
# Before
from numba import njit
@njit(cache=True, fastmath=True)
def dist2(x0, y0, x1, y1):
dx = x1 - x0
dy = y1 - y0
return dx * dx + dy * dy
# After — plain Python, no imports, no annotations
def dist2(x0, y0, x1, y1):
dx = x1 - x0
dy = y1 - y0
return dx * dx + dy * dy
Nothing else about the call sites changes: the function is still an ordinary Python callable, now a candidate for native compilation. Compile it with the product CLI — verified against the installed build:
python -m pyvorin compile src/kernels.py --explain-fallback
python -m pyvorin bench src/kernels.py --function entry --runs 5 --warmup 2
Note that the Numba-specific tuning arguments have no Pyvorin equivalents to carry across: cache=True is subsumed by the always-on disk cache, and fastmath corresponds to the CLI's --fast-math flag if you want it. The Numba cache files (__pycache__/*.nbi, *.nbc) become dead weight; delete them with the decorators.
What actually breaks: the code under the decorator
Most Numba kernels compile without drama. The failures we hit in real runs cluster around one habit Numba trains into you: the None sentinel. A nearest-neighbour kernel written the Numba way — best = None, then a loop that compares and assigns — produced this from compile --explain-fallback:
nearest:
Fallback reasons:
- Return(Optional var) (nearest): Unsupported feature encountered
during codegen: Return(Optional var)
- Compare(Optional operand) (nearest): Unsupported feature encountered
during codegen: Compare(Optional operand)
The compiler cannot give an optionally-typed variable a stable machine representation, so the function lands in compatibility mode and runs as ordinary CPython. Numba accepted this pattern because its type inference resolved the optional before codegen. The fix is a rewrite you would recognise from any C course — seed with a real value instead of a sentinel:
def nearest(points, qx, qy):
best_x, best_y = points[0]
best_d = dist2(best_x, best_y, qx, qy)
for px, py in points:
d = dist2(px, py, qx, qy)
if d < best_d:
best_d, best_x, best_y = d, px, py
return best_x, best_y
After that edit the function compiles full. Expect a handful of these patterns per codebase: None-initialised accumulators, generators feeding sums, and deeply nested calls between module-level functions, which this build sometimes declines with a plain Call (entry): Unsupported feature encountered during codegen. Each is visible in seconds via --explain-fallback; none is a mystery once seen.
A verification protocol per function
Do not migrate a file; migrate a function, prove it, move on. The sequence below is the one we used writing this page, and every command is verified against pyvorin-native 1.0.9:
- Classify.
python -m pyvorin support kernels.pyshows every function's status in one pass:COMPILED_FULL,COMPILED_PARTIAL,COMPATIBILITY_EXECUTEDorFAILED. - Diagnose. For anything not full,
python -m pyvorin compile kernels.py --explain-fallbacknames the exact construct and the causal chain — a caller falls back because it calls a compat-mode function, so fix the root, not the leaves. - Prove speed.
python -m pyvorin bench kernels.py --function entry --runs 5 --warmup 2reports the speedup against CPython ground truth, with compile time on its own line. Checkcorrect: Truebefore quoting anything. - Prove it holds.
bench --jsonadds the machine-readable fields your CI can assert on:status,fallback_used,fallback_reason,correct. A migrated function that reportsCOMPATIBILITY_EXECUTEDhas not been migrated — it has been moved. - Gate it. Add
python -m pyvorin compile kernels.py --fail-on-fallbackto pre-merge checks once the file is claimed native. The flag exits 2 on fallback, so regressions break the build instead of quietly costing the speedup.
Keep the Numba timings for the same functions before you delete anything. A migration claim you cannot compare against the old numbers is a rumour, and the honest comparison includes Numba's warm-cache steady state on both sides.
A worked judgement call
Suppose the inventory turns up thirty-one @njit functions and three of them drive eighty per cent of the runtime. The mechanical answer migrates all thirty-one in one branch. The judgement call says otherwise: migrate the three hot ones first, prove each with the protocol above, and let the remaining twenty-eight keep their decorators until the profiling data says otherwise. The reason is not laziness. Every migrated function carries a verification cost and a fallback tail risk, while a cold Numba function that runs once per deploy costs you nothing either way. Migration effort should follow measured heat, not decoration count. When the profile finally does move, the protocol has not changed — only the order has.
Where to go next
- Pyvorin vs Numba — the full workload-fit comparison, including where each tool is the honest choice.
- Quick start — install pyvorin-native and compile your first function in minutes.
- How to run a speed proof — turning a bench run into evidence a sceptic accepts.
- Benchmarks — the canonical workload table, wins and losses reported together.
Last reviewed 30 June 2026 against pyvorin-native 1.0.9 installed at /root/pvfinal. Every command and output block on this page was captured from real runs against that build on 13 September 2026; cited speedups are from the canonical benchmark table of the same date.