Skip to main content

VulPy

Write Python. Run on any GPU. Inspect the GLSL it generates.

VulPy is a Vulkan compute runtime with a Python front end. You write Python, with access to any architectural pattern the language supports, and VulPy's AST transpiler builds it into GLSL, compiles to SPIR-V, and dispatches on whatever Vulkan can reach: an RTX 4090, an Intel Iris Xe, a phone's Adreno 730 in Termux. No CUDA, no vendor lock-in, no separate backends.

The generated GLSL is always inspectable. If you want to see what a persistent kernel looks like explicitly in shader code, or what a zero-copy memory channel compiles to, it's right there, readable and modifiable. VulPy can be a prototyping layer you eventually leave, or a runtime you stay in. Either way you end up understanding what your code actually does on the GPU.

CuPy had the right idea making GPU compute feel like NumPy. VulPy follows the same instinct but targets Vulkan. And because GLSL and SPIR-V are already the common language every major vendor builds tooling around — the one GPU IR whose drivers stay maintained on every platform because the games industry funds them — the output isn't just inspectable, it's portable twice over. Share the .vul.py file and anyone with pip install vulpy runs it on whatever Vulkan device they have. Or run python -m vulpy emit-glsl kernel.vul.py and share the GLSL itself: readable, compilable, zero VulPy dependency. Working in an Nvidia shop? Transliterate to CUDA. AMD? ROCm. The generated shader is a working starting point, not a blank page.

import numpy as np
import vulpy as vp

x = np.linspace(0.0, 1.0, 1_000_000, dtype=np.float32)
y = vp.sin(x) + vp.exp(x)   # GPU-accelerated, returns NumPy array

Install

pip install vulpy

Requires glslangValidator on PATH for runtime GLSL → SPIR-V compilation.

Linux (Ubuntu / Debian)

sudo apt install glslang-tools
pip install vulpy

Android / Termux

pkg install python-numpy glslang
pip install vulkan vulpy vulkan-tools
pkg uninstall vulkan-loader-generic
pkg install vulkan-loader-android

Use pkg install python-numpy rather than pip install numpy, because pip throws a fit and crashes building the wheel on android.

macOS / Windows

Expected to work via MoltenVK (macOS) or the LunarG Vulkan SDK (Windows), but I don't have a mac and I haven't run Windows in a year. Please let me know if you get it working, the steps you took, and if anything breaks.


Tested Hardware

Device GPU Platform
Laptop NVIDIA RTX 4050 Mobile Ubuntu Linux
Laptop Intel Iris Xe (iGPU, same machine) Ubuntu Linux
Vivo X Fold Adreno 730 Android / Termux
Nubia RedMagic 8 Adreno 740 Android / Termux

Any device with Vulkan compute support should work. Android phones don't need root, just Termux, Python, and the correct Vulkan driver package.


Memory Model

Currently, VulPy exposes four memory kinds, corresponding directly to Vulkan memory property flags:

Kind GPU speed CPU readable Use for
"device" Full VRAM bandwidth No Large working arrays, anything the GPU owns entirely
"host" PCIe bandwidth Yes (write-combining — fast write, slow read) Staging, control buffers, write-only channels
"cached" PCIe bandwidth Yes (full DDR bandwidth) Read-back, inter-GPU data bridges
"rebar" Full VRAM bandwidth Yes (limited) Small buffers that need occasional CPU access

On integrated GPUs and phones (Unified Memory Architecture), "device" and "host" collapse to the same physical DRAM. The CPU and GPU are on the same die sharing the same memory bus; the distinction disappears at the hardware level.

Zero-copy: one dataset, multiple devices

VulPy can allocate frame buffers backed by page-aligned host DRAM and import those physical pages into multiple GPU address spaces simultaneously via VK_EXT_external_memory_host. The GPU can write directly to system DRAM. A NumPy array pointing at the same pages can then see the result immediately. No copy, no transfer, no staging buffer.

For instance, on a system with both a discrete GPU and an integrated GPU:

  1. The dGPU can run compute against its VRAM-resident buffers at full GPU memory bandwidth
  2. A dedicated workgroup can then write selected results to the shared host pages, once, over PCIe
  3. The iGPU can import those same physical pages into its own address space and run against them
  4. A NumPy view on the CPU reads from those same DRAM cells
  5. Anything else on the CPU reads from that NumPy view

One write. One dataset. Three concurrent possible readers. In persistent mode there are no dispatch signals, no shader reloads, no handshakes. The only traffic on the PCIe bus is data you explicitly committed. That's why the transfer is zero cost in the "persistent shader" architecture outlined below and in the tutorials.

On UMA hardware, step 2 is a same-die write, so PCIe doesn't exist to cross.


Features

VulPy has several features ranging from drop-in NumPy-like statements to full persistent GPU-side programs. The goal is to provide several ways to interact with your graphics cards, ranging from in-line VulPy statements to entire .vul.py kernels.

Convenience mode

Accepts NumPy arrays, returns NumPy arrays. No setup required.

x = np.linspace(0.0, 1.0, 1_000_000, dtype=np.float32)
y = vp.sin(x) + vp.exp(x)

Resident mode

Keeps arrays in Vulkan-owned memory across dispatches. numpy_view() returns a NumPy array whose data pointer is the persistently mapped address of the Vulkan allocation. Same physical bytes, no copy.

xv = vp.asarray(x)
yv = vp.empty_like(xv)
vp.sin(xv, out=yv)
result = yv.numpy_view()   # NumPy data pointer == Vulkan buffer pointer

Fusion mode

vp.jit traces a Python lambda and emits one shader for the whole expression.

f = vp.jit(lambda a, b: vp.where(a > b, vp.sin(a), vp.cos(b)))
out = f(xv, xv * 0.5)

Kernel decorator mode

Write a kernel as an annotated Python function. VulPy transpiles the body to GLSL.

@vp.kernel(local_size_x=64)
def push_particles(
    pos: vp.Buffer[vp.vec4],
    vel: vp.Buffer[vp.vec4],
    n:   vp.uint32,
    dt:  vp.float32,
):
    i    = vp.global_id()
    x    = vp.var(pos[i].xyz)
    v    = vp.var(vel[i].xyz)
    x[:] = x + v * dt
    pos[i] = vp.vec4(x, pos[i].w)

push_particles(pos_v, vel_v, dispatch=N, n=N, dt=0.001)

.vul.py source mode

Write GPU source in a .vul.py file. Python syntax compiled to GLSL/SPIR-V by VulPy's AST transpiler. The generated shader is always inspectable.

# my_kernel.vul.py
import vulpy as vp

class PushConstants:
    N:  uint32
    dt: float32

def main(pos: vp.Buffer[vp.float64], vel: vp.Buffer[vp.float64]):
    i: uint32 = vp.global_id()
    if i >= pc.N:
        return
    pos[i] = pos[i] + vel[i] * pc.dt
shader = vp.compile("my_kernel.vul.py", local_size_x=256, context=ctx)
shader.allocate(pos=pos_array, vel=vel_array)   # sizes the buffers — no data moves
shader.upload("pos", pos_array)
shader.upload("vel", vel_array)
shader.run(dispatch=N, N=N, dt=0.001)
# inspect the shader
python -m vulpy emit-glsl my_kernel.vul.py

Raw GLSL mode

Pass handwritten GLSL source directly for full manual control.

k = vp.kernel(
    GLSL_SOURCE,
    buffers=2,
    push=[("n", "uint32"), ("dt", "float32")],
)
k(pos, vel, dispatch=N, n=N, dt=0.001)

Control-buffer mode

Resident uint32 buffers that both CPU and GPU can read and write atomically. Use for flags, counters, GPU-written event queues, and adaptive kernel control.

control = vp.ControlBuffer(["mode", "hot_count"], words=16)
control["hot_count"] = 0
# GPU kernel: atomicAdd(control[hot_count], 1)
print(control["hot_count"])

Memory Sockets (MemSock)

A typed, directional memory channel between GPU and CPU. WebSocket-like semantics for data transfer between VRAM and DRAM.

This is the answer to a problem the persistent model creates: when one dispatch runs for seconds or hours, the natural observation points of launch-by-launch code disappear and the run becomes a black box you wait on. Getting sight back normally means hand-rolling pinned host memory, a ring layout, and a commit protocol; MemSock packages that machinery as a first-class object. Paired with the control buffer it works in both directions — stream meaning out, write intent in — so a long-running kernel can be watched live and steered mid-flight without ever stopping it.

# host .py
pipe = vp.MemSock(
    dtype       = vp.float32,
    frame_shape = (ny, nx),
    payload     = {"snap_idx": vp.uint32},
)

@pipe.on("message")
def on_frame(frame, idx):
    imshow.set_data(frame)   # frame is a zero-copy numpy view into host DRAM

pipe.open(ctx, capacity=n_snapshots, ctrl_view=ctrl_arr, ctrl_offset=CTRL_PIPE)
# .vul.py — coordinator side
def pipe_body(snap_idx: vp.uint32):
    pipe[snap_idx * pc.nx + p] = efield[p]

if t % pc.snap_interval == 0:
    pipe.fire(pipe_body, snap_idx=t)   # transpiler wraps this in the epoch protocol

For a full working example including iGPU hot-cell overlay, see Lesson 11: 3D FDTD Scatter.

Persistent GPU-side programs

A single kernel dispatch that runs like an application on your GPU, managing its own scheduling on-device. The CPU launches it, and then begins an async event loop that listens and reacts to events emitted by the GPU over MemSocks.

The reference implementation is the 3D FDTD tutorial, but the pattern applies broadly: long-running simulations, adaptive solvers, live visualization, and any workload where the GPU should own its compute loop rather than being called one dispatch at a time.

These are established techniques — persistent threads, software work queues, GPU-initiated I/O — the same architecture as D3D12 Work Graphs and the persistent megakernels in modern LLM inference engines, usually learned from research papers and production kernels. The tutorial series teaches them as a ladder of runnable steps and maps each piece to its literature.


Device Selection

vp.list_devices()                    # list all Vulkan devices
ctx = vp.Context()                   # default device
ctx = vp.Context(device_index=1)     # by index
ctx = vp.Context(kind="dgpu")        # discrete GPU
ctx = vp.Context(kind="igpu")        # integrated GPU

Requirements

  • Python ≥ 3.11
  • NumPy ≥ 1.24
  • vulkan Python package
  • glslangValidator on PATH
  • A Vulkan-compatible GPU with Vulkan drivers installed

Documentation


Current Limitations

  • Main convenience-mode compute path is float32. float64 is available in .vul.py kernels when the device reports shaderFloat64 support (desktop GPUs; not phones).
  • Resident outputs must be C-contiguous.
  • Axis-aware reductions, fancy indexing, and bool dtype are not implemented.
  • macOS and Windows are untested.

Metadata

Release files for vulpy 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for vulpy 0.1.0
File Size Uploaded
vulpy-0.1.0.tar.gz 107.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for vulpy 0.1.0
File Interpreter ABI Platform
vulpy-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 223.3 kB

Release files / vulpy-0.1.0.tar.gz

Download URL vulpy-0.1.0.tar.gz
Size 107.8 kB
Tags Source
SHA-256 checksum
How to use checksums
cf982100bded952e79acfdc0e039b5bbd125cc2e1309be9e3ef21d72a93babeb
BLAKE2b-256 checksum
How to use checksums
609827f00f3b58f668e242d5e6e62190010404a4a44fcb65ab3a0c15cc550bcf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / vulpy-0.1.0-py3-none-any.whl

Download URL vulpy-0.1.0-py3-none-any.whl
Size 115.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0e48575f0b53f1ca7eee646b293323b4f1a85e0b4b89eadb2c84e248c95e9574
BLAKE2b-256 checksum
How to use checksums
89f6721b87d7ee0f34a3549a8ca90a80576ea2217e9a9e910d31c948782c9d37
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page