Skip to main content

PyGPGPU

English | Chinese

Decorate a Python function and it runs on the GPU — one codebase, three backends (OpenCL / CUDA / HIP).

import numpy as np
from pygpgpu import kernel

@kernel
def saxpy(x, y, out, alpha):
    for i in range(out.shape[0]):
        out[i] = x[i] * alpha + y[i]

x = np.arange(1_000_000, dtype=np.float32)
out = np.zeros_like(x)

saxpy(x, x, out, 2.0)              # no backend given: probe cuda -> hip -> opencl, take the first available
saxpy["cuda"](x, x, out, 2.0)      # bracket switches the backend
saxpy["hip:gfx1036"](x, x, out, 2.0)   # backend:device

No C code to write, no build system to configure; the first call translates, compiles and caches, every later call is free. Existing handwritten kernels are fine too — .cl/.cu/.hip files can be imported like Python modules.

Features

  • @kernel decorator — decorate a Python function directly (class methods and static methods included); at call time it is specialized per argument type, translated to backend source and compiled (monomorphization).
  • Plain-Python helper functions — a helper called from a kernel needs no decorator and no type annotations: it is translated into a device function and monomorphized from the call site, recursively for helpers that call helpers.
  • All three backends in one process — the bracket chooses the backend per call, so a single process (and even a single kernel object) can mix OpenCL, CUDA and HIP; each backend/device pair is specialized and cached separately.
  • Handwritten kernel file import — .cl / .cu / .hip files import like Python modules: kernels and struct types become module attributes, and a .pyi type stub is generated automatically.
  • Low-level OOP API — Device/Context/Buffer/Program/Kernel, for when you need full control.
  • One bracket syntax for backend/device/work size (identical to handwritten kernels, order does not matter):
    k(args)                          # fully automatic
    k["cuda"](args)                  # backend (opencl/cuda/hip, aliases ocl/cu/rocm/amd)
    k["opencl:gfx1036"](args)        # backend:device
    k[(1024, 256)](args)             # work size: (total, per group), None = automatic
    k["cuda"][(1024, 256)](args)     # combined
    
  • Total-work semantics (OpenCL/SYCL style): in (global, local), global is the total number of work items; CUDA/HIP grid dimensions are derived as global / local, so all three backends behave exactly the same.
  • Static read/write analysis: the kernel body is parsed to infer whether each pointer argument is read-only, write-only or read-write — write-only arguments skip the upload, read-only arguments skip the read-back, and data stays on the device across calls (zero intermediate copies for gnp arrays).
  • Vector types: 50 vector types (float4/int3/...) with .x/.y/.z/.w members, swizzles, construction, arithmetic (scalar broadcast), comparisons and component-wise math.
  • Dual-sided function library pygpgpu.funcs (aka pygpgpu.vec_types.funcs): dot/cross/normalize/clamp/... run as-is in host Python and are translated to device builtins inside @kernel.
  • Device builtin namespace gpu.*: get_global_id/barrier/atomic_add/dot/... covers GPU builtins that have no Python counterpart, mapped per backend.
  • Multi-dimensional iteration: np.ndindex(shape) / np.ndenumerate(arr) magic loops (2D/3D), enumerate(arr) for a 1D index plus value snapshot.
  • Three cache layers: parsed metadata / compiled artifacts / generated sources, all invalidated by mtime — unchanged sources mean zero recompilation.

Installation

pip install -e .

Python ≥ 3.12. Bring your own GPU runtime (any one of them is enough):

Backend What you need Notes
OpenCL GPU driver (usually already installed) Nothing extra to install; works across NVIDIA/AMD/Intel
CUDA NVIDIA driver (includes NVRTC) The Toolkit is not required — the driver-provided nvrtc is enough
HIP AMD HIP SDK On Windows compilation runs through the SDK's clang; if MSVC/Windows SDK headers are not on the default paths, point PYGPGPU_HIP_INCLUDES at the include directories (os.pathsep-separated)

When @kernel has no explicit backend, backends are probed in cuda -> hip -> opencl order and the first one that enumerates a device wins.

Quick start

Every commonly used API is importable straight from the pygpgpu top level — no need to remember submodule names:

from pygpgpu import kernel, gpu, funcs, float4       # @kernel / device builtins / dual-sided funcs / vector types
from pygpgpu import available_devices, get_device
from pygpgpu import climport, cuimport, hipimport    # import hooks for handwritten .cl/.cu/.hip files

The submodule paths (pygpgpu.kernel / pygpgpu.vec_types / pygpgpu.opencl / ...) still work. Note that top-level pygpgpu.kernel is the @kernel decorator; to reach things inside the submodule write from pygpgpu.kernel import ....

The three import hooks load lazily: the corresponding backend is only loaded when you actually use the hook, so import pygpgpu itself needs no GPU runtime at all.

The @kernel decorator

import math
import numpy as np
from pygpgpu import kernel, gpu, funcs, float4

def norm2(v):
    return funcs.dot(v, v)

@kernel
def normalize_all(vecs, out):
    for i, v in enumerate(vecs):          # 1D: index + float4 value snapshot
        out[i] = funcs.normalize(v)       # expanded component-wise on device

@kernel
def matrix_scale(m, out, k):
    for y, x in np.ndindex(out.shape):    # 2D NDRange
        out[y, x] = m[y, x] * k

@kernel(backend="cuda")
def histogram(data, counts):
    gpu.work_size(data)                   # iteration domain for a gid-style kernel
    i = gpu.get_global_id(0)
    gpu.atomic_add(counts, data[i], 1)    # atomics, results read back automatically

Supported: scalar/array/vector arguments, if/while/for-range, arithmetic/comparison/bit ops, math.* and np.* math functions, class methods (self.xxx attributes become arguments automatically), static methods. Unsupported syntax raises a SyntaxError carrying the line number.

A kernel needs an iteration domain for its work size to be derived. Either use a magic loop (for i in range(x.shape[0]), whose bound is the domain) or, when dispatching on gpu.get_global_id, declare gpu.work_size(x) — that statement is host-side only, erased by the translator, and contributes all of x's dimensions. A k[(total, local)] bracket also works. Without any of these the call raises instead of guessing.

Handwritten kernel files

Already have C-style kernels? .cl / .cu / .hip files (native syntax of the matching backend) import like Python modules. Write a vecadd.cl:

__kernel void vecadd(__global const float* a, __global const float* b,
                     __global float* out, const int n)
{
    int i = get_global_id(0);
    if (i >= n) return;
    out[i] = a[i] + b[i];
}
from pygpgpu import climport             # cuimport / hipimport work the same way
import numpy as np
import vecadd                          # just like importing a Python module

a = np.random.rand(1_000_000).astype(np.float32)
out = np.zeros_like(a)
vecadd.vecadd(a, a, out, np.int32(len(a)))    # work size inferred
vecadd.vecadd[(1_000_192, 256)](a, a, out, np.int32(len(a)))   # explicit (total, per group)

Note the n argument: a handwritten kernel must clamp against the real element count, because the launch size is rounded up to a multiple of the local size. get_global_size(0) reports that rounded size, not the buffer length, so it is not a usable bound.

Struct definitions map to ctypes types; a .pyi stub is generated next to the kernel so IDE completion works.

Class methods

class Model:
    def __init__(self):
        self.weights = np.random.rand(1024).astype(np.float32)
        self.lr = 0.01

    @kernel
    def apply(self, x, out):
        for i in range(out.shape[0]):
            out[i] = x[i] * self.weights[i] + self.lr

m = Model()
m.apply(x, out)                     # self.weights/self.lr are passed as kernel arguments
m.apply["cuda"](x, out)             # brackets still available

Copy optimization

Each pointer argument goes through function-body-level static analysis (points[i] = ... is a write, x = a[i] is a read, plus pointer-alias propagation and interprocedural analysis across functions):

@kernel
def k(a, b, out):
    for i in range(out.shape[0]):
        out[i] = a[i] + b[i] * 2.0   # a read-only, b read-only, out write-only
# at runtime: a/b skip the read-back after upload, out skips the upload and only reads back

Combined with pygpgpu.numpy (gnp) arrays, data stays entirely on the device between consecutive kernel calls and is synchronized only when the host touches it.

How it compares

The same computation, written in this library and in three other GPU-from-Python projects (Taichi, Numba, NVIDIA Warp). Snippets are cut down to the parts that differ — check each project's docs for exact syntax. The examples also show a shared helper function, which is where the API gap is widest: here it stays ordinary Python, while the others need a second decorator and typed signatures.

PyGPGPU — arrays in, arrays out; no launch geometry, no buffer management, and helper functions need no decorator at all:

from pygpgpu import kernel

def square(v):                          # plain Python: no decorator, no annotations
    return v * v

def clamp01(v):
    if v < 0.0:
        return 0.0
    if v > 1.0:
        return 1.0
    return v

def weight(v):                          # helpers may call other helpers
    return clamp01(square(v)) * 0.5 + 0.25

@kernel
def apply_all(x, out):
    for i in range(out.shape[0]):       # loop bound = work size
        out[i] = weight(x[i])

apply_all(x, out)                       # numpy arrays as-is
apply_all["hip"](x, out)                # the same function, another backend

Taichi — declare fields first, then copy data in and out by hand; a helper needs @ti.func and every argument must be annotated:

import taichi as ti
ti.init(arch=ti.gpu)

x = ti.field(ti.f32, n)
y = ti.field(ti.f32, n)
out = ti.field(ti.f32, n)
x.from_numpy(nx)
y.from_numpy(ny)

@ti.func                                  # helpers need their own decorator
def weight(v: ti.f32) -> ti.f32:
    return ti.max(0.0, ti.min(1.0, v * v)) * 0.5 + 0.25

@ti.kernel
def saxpy(a: ti.f32):
    for i in range(n):
        out[i] = x[i] * a + y[i] + weight(x[i])

saxpy(2.0)
res = out.to_numpy()

Numba — compute blocks/threads yourself, guard inside, manage device memory; callable helpers are compiled separately with @cuda.jit(device=True):

from numba import cuda

@cuda.jit(device=True)                    # helpers need device=True
def weight(v):
    return min(1.0, max(0.0, v * v)) * 0.5 + 0.25

@cuda.jit
def saxpy(x, y, out, a):
    i = cuda.grid(1)
    if i < out.size:                      # bounds guard is your job
        out[i] = x[i] * a + y[i] + weight(x[i])

d_x = cuda.to_device(nx)
d_y = cuda.to_device(ny)
d_out = cuda.device_array_like(nx)
threads = 256
saxpy[(n + threads - 1) // threads, threads](d_x, d_y, d_out, 2.0)
res = d_out.copy_to_host()

Warp — typed array parameters, the launch size is passed explicitly, and helpers need @wp.func:

import warp as wp
wp.init()

@wp.func                                  # helpers need their own decorator
def weight(v: float) -> float:
    return wp.min(1.0, wp.max(0.0, v * v)) * 0.5 + 0.25

@wp.kernel
def saxpy(x: wp.array(dtype=float), y: wp.array(dtype=float),
          out: wp.array(dtype=float), a: float):
    i = wp.tid()
    out[i] = x[i] * a + y[i] + weight(x[i])

wp.launch(saxpy, dim=n, inputs=[x, y, out, 2.0])
PyGPGPU Taichi Numba Warp
Input data numpy arrays directly wrap in ti.field, explicit from_numpy/to_numpy cuda.to_device + copy_to_host by hand wrap in wp.array
Helper functions plain Python — no decorator, no annotations @ti.func + annotated args @cuda.jit(device=True) @wp.func + annotated args
Backends in one process all three, mixed freely one, fixed by ti.init(arch=...) CUDA only CUDA only
Launch size inferred from the loop bound (or gpu.work_size(x) / a bracket) inferred from range blocks × threads by hand, plus a guard pass dim=
Bounds guard emitted for you emitted for you you write it you write it
Backends OpenCL, CUDA, HIP CPU, CUDA, Vulkan and others CPU, CUDA CPU, CUDA
AMD GPU HIP (native AMD codegen) and OpenCL via Vulkan no no
Switching backend bracket k["hip"], same source ti.init(arch=...), same source not applicable not applicable
Install footprint Python + ctypes only: no LLVM, no CUDA Toolkit ships its own runtime needs LLVM needs the CUDA Toolkit
Handwritten .cl/.cu/.hip imported as Python modules, .pyi generated no via the cuda module's PTX/cubin path no
Static read/write analysis yes — uploads/read-backs skipped per argument no no no

Several backends at the same time

Taichi fixes its backend once, at ti.init(arch=...); Numba and Warp only target CUDA. Here the bracket picks the backend per call, so one process — and even one kernel object — can use every backend the machine has:

@kernel
def apply_all(x, out):
    for i in range(out.shape[0]):
        out[i] = square(x[i]) + 1.0

apply_all["cuda"](x, out)      # NVIDIA
apply_all["hip"](x, out)       # AMD
apply_all["opencl"](x, out)    # whichever device OpenCL enumerates
apply_all(x, out)              # unqualified: probe cuda -> hip -> opencl

Each backend/device pair is specialized and cached separately, so alternating between them is just an extra dictionary lookup — no recompilation after the first call. This is useful for comparing a kernel across vendors on one machine (examples/08_compare_backends.py), or for falling back to a second GPU when the first is busy.

Where the others are ahead — worth knowing before you pick:

  • Automatic differentiation: Taichi and Warp can differentiate kernels; this library cannot.
  • Sparse and irregular data structures: Taichi has sparse grids and SNode trees; here you pass dense numpy arrays (or handle sparsity yourself).
  • CPU fallback: all three can run on the CPU; this library needs a GPU.
  • Minimum interpreter version: this library requires Python 3.12+ (it runs on 3.14); the others support a wider range of older versions.

This table was written from each project's documentation and the snippets above; their APIs move fast, so confirm against current docs before relying on a specific cell.

Device selection

Three forms of device identifier inside the brackets (same on every backend):

Form Meaning
"0", "1" index in the backend's device list (OpenCL uses flat numbering across platforms)
"gfx1036", "geforce" case-insensitive substring match on name/vendor/architecture name
"geforce:1" when a name matches several devices, take the n-th one

Helpers:

from pygpgpu import available_devices, get_device

available_devices()          # every available "backend:device" string, e.g. ['cuda:0', 'hip:0', 'opencl:0']
dev = get_device("cuda:0")   # the Device object of that backend, directly

Caching

Backend Directory Contents
OpenCL __clcache__/ parsed meta + per-device binaries
CUDA __cucache__/ parsed meta + per-device cubin/ptx
HIP __hipcache__/ parsed meta + per-device code objects

Caches are invalidated by the mtime of the source file (and every #include); sources generated by @kernel are keyed by a hash of the Python source. These directories should be in .gitignore.

Known limitations

  • @kernel does not support: strings/lists/dicts, closures, recursion, kernel return values, dynamic multi-dimensional shape indexing;
  • vector comparison, writing through a swizzle, and nesting beyond three operands are restricted in favour of cross-backend consistency (the error message suggests a workaround);
  • fp16 (np.float16) arguments are unavailable on the CUDA/HIP backends (use float32);
  • the HIP backend compiles with the clang shipped by the AMD HIP SDK; hipRTC on this machine (ROCm 7.2 Windows) fails on any input, so it is not used.

License

MIT

Metadata

Release files for pygpgpu 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 pygpgpu 0.1.0
File Size Uploaded Provenance
pygpgpu-0.1.0.tar.gz 434.1 kB Without provenance Details

Built distribution (wheel)

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

Total release size: 929.6 kB

Release files / pygpgpu-0.1.0.tar.gz

Download URL pygpgpu-0.1.0.tar.gz
Size 434.1 kB
Tags Source
SHA-256 checksum
How to use checksums
de7dc20a54dd08ddea9b5cbde398375e7da5758cb4a7d4f64df60676adc083da
BLAKE2b-256 checksum
How to use checksums
8f31f20d2e706614c525641d2c82d8c42f62d0bae3ee5e939ae620b6f9ac73ed
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.3

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

Download URL pygpgpu-0.1.0-py3-none-any.whl
Size 495.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
26aff5d8d54de89083768d44c0976704a3ec28f62e10f78072c722d074090a18
BLAKE2b-256 checksum
How to use checksums
7757479806ee3712f4c8cdb62a5740dd587b30576273a9fd33fcd04bd8b680d0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.3

Security for pygpgpu 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

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