Skip to main content

Python plugins

Kernels can be plain Python functions. You decorate them with @tomii.export, reference them with Graph.py_node(), and build the bundled PyO3 bridge plugin instead of a custom dylib. The bridge embeds a Python interpreter inside the runtime and calls your functions from worker threads.

Exporting functions

From examples/matrix-compute-python/matcomp.py:

import numpy as np
import tomii

@tomii.export
def generate_vector(n: int) -> np.ndarray:
return (np.random.randn(n) + 1j * np.random.randn(n)).astype(np.complex64)

@tomii.export
def compute_fft(v: np.ndarray) -> np.ndarray:
return np.fft.fft(v).astype(np.complex64)

@tomii.export(variadic=True)
def write_to_file(path: str, mats: list) -> None:
np.savez(path, *[np.asarray(m) for m in mats])

The decorator is a zero-cost marker: it registers the function and returns it untouched, so calling it directly from Python is unaffected (tomii/_export.py). variadic=True marks fan-in sinks that receive a list of predecessor results.

Wiring and building

py_node is the Python-kernel analogue of node() — you pass the function object instead of a name string:

import matcomp

gen_vec = app.py_node("gen_vec", fn=matcomp.generate_vector,
factor=num_nodes, args=[buf_size])
fft = app.py_node("fft", fn=matcomp.compute_fft,
factor=num_nodes, args=[gen_vec.out()])

app.build(python_plugin=True)
app.run(workers=4, slots=2)

build(python_plugin=True) compiles the bundled bridge plugin. run() propagates your interpreter's sys.path to the embedded interpreter, so the bridge sees the same packages as the building process (tomii/_graph.py).

Run the full example with:

bash examples/matrix-compute-python/run_bench.sh

The GIL

Whether Python kernels run in parallel depends on what they do (examples/matrix-compute-python/README.md):

Python buildNumPy/BLAS kernelsPure-Python kernels
CPython 3.12 (stock)parallel (GIL released inside BLAS/FFT)serialized by the GIL
CPython 3.13t (free-threaded)parallelparallel

NumPy releases the GIL around BLAS and FFT calls, so the matrix-compute kernels already run in parallel on stock CPython.

Pure-Python compute: @tomii.procs()

For genuine pure-Python work (loops, comprehensions) that would serialize under the GIL, stack @tomii.procs() under the export decorator:

@tomii.export
@tomii.procs() # dispatches to a ProcessPoolExecutor; GIL released during wait
def pure_python_example(data: list) -> list:
return [x * x for x in data]

Each worker releases the GIL while waiting for its subprocess result, so N workers execute concurrently in N separate processes. The dispatch overhead is roughly 50–200 µs per call, so it only pays when compute dominates (examples/matrix-compute-python/matcomp.py).

Free-threaded Python

For 3.13t, which removes the GIL entirely, pass the interpreter at build time:

app.build(python_plugin=True, python_interpreter="python3.13t")

or from the example runner:

bash examples/matrix-compute-python/run_bench.sh --python-interpreter python3.13t