A dependency-free Python client for the PJRT C API and the XLA FFI
Project description
pypjrt
Run compiled ML programs on CPUs, GPUs and TPUs from Python — with zero dependencies.
pypjrt is a pure-Python client for PJRT, the hardware-agnostic
runtime interface underneath JAX, TensorFlow and PyTorch/XLA. It loads a vendor's PJRT plugin,
compiles StableHLO, moves buffers, runs programs across devices, and lets you register custom
GPU kernels — all through ctypes, with nothing in install_requires.
import pypjrt
with pypjrt.Client.create() as client, client.devices() as devices:
exe = client.compile(stablehlo_text)
a = client.buffer_from_host(xs, 11, [8], devices[0])
(out,) = exe(a)
print(out.to_host())
Why this exists
PJRT's premise is that frameworks call one API and hardware vendors implement it, so neither
has to know about the other. That works well — in C++. In Python there is exactly one PJRT client:
the one buried inside jaxlib as a ~500 MB compiled xla_extension.so.
So today, if you want to run a compiled program on an accelerator from Python, your options are to adopt an entire framework or to write C++. There is no small, reusable way to just call PJRT.
pypjrt is that missing piece. The client half of PJRT is common infrastructure — it just happened to ship inside a framework. Unbundling it means the cost of talking to accelerator hardware drops from "take a 500 MB dependency" to "install a pure-Python wheel".
What you get
No dependencies, no build step. The core imports nothing outside the standard library. No compiler on your machine, no per-platform wheels, no linking against XLA. About 5,600 lines of hand-written Python plus a generated ABI module.
Genuinely hardware-agnostic. The same code path drives an XLA CPU plugin and an NVIDIA GB10,
differing only by a file path. Capability differences are negotiated, never assumed: every
extension goes through a probe that returns None when absent, and version skew is handled by
selecting a matching ABI rather than asserting. One build currently speaks to plugins reporting
PJRT 0.104 and 0.108 against headers pinned at 0.114.
Composes instead of competing. Zero-copy DLPack in both directions — export a buffer to torch/jax/numpy, or adopt theirs without a host copy. pypjrt is designed to sit alongside the framework you already use, not to replace it.
Custom kernels, from Python. Register a Python function as an XLA FFI handler and XLA will
call it inside a compiled program, with real device pointers and its own stream. Or compile Triton
IR through the plugin itself — no triton package, no subprocess.
A persistent compile cache. Serialize compiled executables and skip XLA compilation on restart. Measured on an NVIDIA GB10: 6.2 s cold → 66 ms warm. Cache keys include the plugin's XLA version, because an executable compiled against one XLA can silently miscompute under another.
Diagnostics that tell you what went wrong. Allocation failures carry the device's own
allocator state instead of a bare RESOURCE_EXHAUSTED:
Out of memory while trying to allocate 64.00MiB.
device memory: device 0: bytes_in_use=2.4 GiB, bytes_limit=2.4 GiB,
largest_free_block_bytes=0 B, peak_bytes_in_use=2.4 GiB, num_allocs=204
client create-options: {'preallocate': False, 'memory_fraction': 0.02}
hint: this client is already capped; the workload needs more memory than it was given
A conformance harness for plugin authors. Today a vendor validates a new PJRT plugin by
running JAX, which conflates plugin bugs with framework bugs. pypjrt.conform is a thin,
scriptable, dependency-free second opinion.
Install
pip install pypjrt
You also need a PJRT plugin — the vendor's shared library. The easiest source is a JAX plugin wheel, which pypjrt discovers automatically:
pip install jax-cuda12-plugin # or jax-cuda13-plugin, libtpu, ...
Otherwise point at one explicitly:
export PYPJRT_PLUGIN=/path/to/pjrt_plugin.so # or TPU_LIBRARY_PATH for libtpu
Optional extras: pypjrt[numpy] for array interop, pypjrt[jax] if you want JAX to produce
the StableHLO you run.
Usage
Ten runnable examples in examples/ go from "hello" to a training loop with custom
kernels; each is standalone and takes an optional plugin path. python examples/01_hello.py uses
nothing but the standard library.
Compile and run
pypjrt consumes StableHLO from any producer — hand-written, or lowered by JAX:
import jax, jax.numpy as jnp, numpy as np, pypjrt
f = lambda a, b: jnp.tanh(a * b + 1.0)
x = np.arange(8, dtype=np.float32)
mlir = jax.jit(f).lower(x, x).as_text() # any producer will do
with pypjrt.Client.create() as client, client.devices() as devices:
exe = client.compile(mlir)
a = client.buffer_from_host(x, 11, [8], devices[0])
(out,) = exe(a, a)
result = np.empty(8, np.float32)
out.to_host(result)
Portable artifacts (versioned StableHLO bytecode) work too — compile() takes str or bytes,
and plugin.stablehlo_target() tells a producer which version to serialize to.
Zero-copy interop
import numpy as np
arr = np.from_dlpack(buffer) # shares device memory, no copy
adopted = client.from_dlpack(jax_array) # and back the other way
Multiple devices
Executable is device-list shaped, because that is the shape of the underlying API:
opts = pypjrt.CompileOptions(num_partitions=2, use_spmd_partitioning=True)
exe = client.compile(sharded_mlir, options=opts)
outputs = exe.execute_sharded([[shard_a], [shard_b]])
Named programs and donation
from pypjrt.session import Session, Slot
with Session(client) as s:
step = s.program(training_step, [Slot("theta"), Slot("grad")], outputs=["theta"])
s.bind_many(lazy_weight_loaders) # only slots the program names are uploaded
for _ in range(steps):
s.feed_back(step()) # output becomes the next input, no host copy
Custom kernels
import pypjrt.ffi as ffi, pypjrt.cuda as cuda
@ffi.handler(plugin, "my_kernel")
def my_kernel(call):
(x,), (y,) = call.args, call.rets
cuda.memcpy_dtod_async(y.data, x.data, x.nbytes, call.stream())
Then reference it from StableHLO as
stablehlo.custom_call @my_kernel(...) {api_version = 4 : i32}.
Device-specific code lives in clearly-named modules — pypjrt.ffi, pypjrt.cuda,
pypjrt.triton — so that trading away portability is visible at the import. The core never names
a device.
Checking a plugin
python -m pypjrt.conform /path/to/plugin.so --json report.json
python -m pypjrt.conform --diff cpu.json gpu.json
Capabilities a plugin lacks are reported as unsupported, not as failures — a harness that can't tell "absent" from "broken" is no use to a vendor. Only real defects set a non-zero exit code.
Scope
pypjrt is everything below StableHLO, and deliberately nothing above it.
In scope: plugin loading and ABI negotiation, compilation, buffers and memory spaces, single- and multi-device execution, donation, AOT artifacts and caching, XLA FFI custom calls, DLPack, async completion, and multi-process rendezvous.
Out of scope, permanently: emitting StableHLO, automatic differentiation, sharding propagation, graph optimisation, kernel authoring DSLs, and model libraries. Those already exist in Python and are better there. Keeping the line sharp is what makes pypjrt a runtime that frameworks can build on rather than another framework.
Compatibility
| Python | 3.10+ |
| PJRT C API | headers pinned at 0.114; older plugins negotiated automatically |
| XLA FFI | 0.3 |
| Verified against | XLA CPU plugin (0.108) and NVIDIA CUDA plugin (0.104) on GB10 / aarch64 |
| Platforms | Linux. macOS untested, Windows unsupported |
132 of 138 core PJRT entry points are bound; the remainder are recorded decisions rather than omissions, and a test pins the list so it cannot drift.
Status
Pre-1.0 and honest about it. The API is settled in shape — in particular Event is future-shaped
and Executable is device-list-shaped, both fixed early so they would not need breaking later —
but names may still move before 1.0.
250 tests run across three tiers: host-only (no plugin required), CPU-plugin, and GPU. The suite treats a skipped test in a required tier as a failure, type-checks the package with pyright, regenerates the ABI from the pinned headers and fails on any diff, and keeps a directory of snippets that must not type-check.
Two capabilities remain unverified for want of hardware rather than code: compiling for a device that is not present, and a collective across processes. Both need more than one accelerator.
Development
git clone https://github.com/pedronahum/pypjrt && cd pypjrt
uv venv .venv && uv pip install --python .venv/bin/python -e '.[dev]'
./local-ci.sh # everything, on a box with plugins
PYPJRT_GPU_PLUGIN=/path/to/gpu.so ./local-ci.sh # including the GPU tier
The ABI module is generated from the vendored OpenXLA headers with libclang and committed — no struct offset or enum value is ever written by hand:
python tools/gen_abi.py
Contributing
Issues and pull requests are welcome at
github.com/pedronahum/pypjrt. Reports from a plugin this
has never been run against are especially useful — attach the output of
python -m pypjrt.conform /path/to/plugin.so -v, which is a complete, dependency-free description
of what that plugin does and does not offer.
Run ./local-ci.sh before opening a pull request; it runs everything CI runs plus the tiers that
need real hardware.
License
Apache-2.0. Vendored OpenXLA headers under vendor/ are Apache-2.0, copyright The OpenXLA
Authors.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pypjrt-0.1.0.tar.gz.
File metadata
- Download URL: pypjrt-0.1.0.tar.gz
- Upload date:
- Size: 255.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a4e6ee777e4444c47db41d522d4bf60743ef60a66a428a0cfac9f42105eaf7e3
|
|
| MD5 |
cbb46f839d33eb9f9d02244640f03043
|
|
| BLAKE2b-256 |
8588e2cbafbc9521e3353eacfd92c0abfba62e24008f2c5fc571e03cdc555de3
|
Provenance
The following attestation bundles were made for pypjrt-0.1.0.tar.gz:
Publisher:
publish.yml on pedronahum/pypjrt
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pypjrt-0.1.0.tar.gz -
Subject digest:
a4e6ee777e4444c47db41d522d4bf60743ef60a66a428a0cfac9f42105eaf7e3 - Sigstore transparency entry: 2219645002
- Sigstore integration time:
-
Permalink:
pedronahum/pypjrt@79fbf1d4c5f4d5c6b0f0eb6d1d7c389a4286951c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/pedronahum
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@79fbf1d4c5f4d5c6b0f0eb6d1d7c389a4286951c -
Trigger Event:
release
-
Statement type:
File details
Details for the file pypjrt-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pypjrt-0.1.0-py3-none-any.whl
- Upload date:
- Size: 118.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
92c92805f5495f87455becc6b9a7e02f51e10d22ec56e7bb3381911a13d7b648
|
|
| MD5 |
e9664549970c6c472cf77805b2ec4c2e
|
|
| BLAKE2b-256 |
8133ee026511c3e6b1918d8d861610e2b1431dea3661d774e0bcbb29c0824555
|
Provenance
The following attestation bundles were made for pypjrt-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on pedronahum/pypjrt
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pypjrt-0.1.0-py3-none-any.whl -
Subject digest:
92c92805f5495f87455becc6b9a7e02f51e10d22ec56e7bb3381911a13d7b648 - Sigstore transparency entry: 2219645140
- Sigstore integration time:
-
Permalink:
pedronahum/pypjrt@79fbf1d4c5f4d5c6b0f0eb6d1d7c389a4286951c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/pedronahum
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@79fbf1d4c5f4d5c6b0f0eb6d1d7c389a4286951c -
Trigger Event:
release
-
Statement type: