Zero-copy interop#

One hawk kernel (y = k * x, a per-sample vector scale), fed from and handed back into NumPy, CuPy and PyTorch arrays with no copy where raptor.conformance.interop’s own declaration says there is none. This notebook runs the --host arm, which needs no GPU, cupy or torch — the kernel runs on CPU threads through eagle.exec.HostTeam, reading and writing the caller’s own numpy arrays directly.

python examples/python/zero_copy_interop.py --host

Needs hawk installed next to eagle.

Colab or Kaggle: run this first cell (it installs the packages and downloads the helper files this notebook uses). Elsewhere it only checks the imports.

Hide code cell source

# Colab setup, generated by docs/_tools/nb_colab_setup.py (do not edit).
import os

try:
    import google.colab  # noqa: F401
    HOSTED = True  # a Colab runtime
except ImportError:
    HOSTED = "KAGGLE_KERNEL_RUN_TYPE" in os.environ  # a Kaggle kernel

if HOSTED:
    import importlib.metadata, pathlib, re, shutil, subprocess, sys, urllib.request

    gpu = bool(shutil.which("nvidia-smi")) and subprocess.run(["nvidia-smi", "-L"], capture_output=True).returncode == 0
    extra = ""
    if gpu:
        # the CUDA major the runtime already has: a CuPy that is installed, else torch's, else the driver's
        major = None
        for m in ("13", "12"):
            try:
                importlib.metadata.version(f"cupy-cuda{m}x")
                major = m
                break
            except importlib.metadata.PackageNotFoundError:
                pass
        if major is None:
            try:
                import torch
                major = "13" if str(torch.version.cuda).startswith("13") else None
            except ImportError:
                pass
        if major is None:
            smi = re.search(r"CUDA Version: (\d+)", subprocess.run(["nvidia-smi"], capture_output=True, text=True).stdout)
            major = "13" if smi and int(smi.group(1)) >= 13 else "12"
        extra = f"[cuda{major}]"
        print(f"GPU found: installing the {extra} extras to match this runtime.")
    pre = ["--pre"] if os.environ.get("RAPTOR_PRE") else []  # optional: test a release candidate
    ref = os.environ.get("RAPTOR_REF", "main")  # optional: fetch the helper files from this git ref
    if pre or "RAPTOR_REF" in os.environ:
        print(f"Pre-release test: installing with --pre, helper files from {ref}.")
    # our packages with --upgrade, so a runtime that kept an older release gets the current one
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "--upgrade", *pre, "raptor-core", f"raptor-eagle{extra}", f"raptor-hawk{extra}"])
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "torch"])
    base = os.environ.get("RAPTOR_RAW_BASE", "https://raw.githubusercontent.com")  # optional: a local mirror
    top = os.environ.get("RAPTOR_WORKDIR") or ("/kaggle/working" if "KAGGLE_KERNEL_RUN_TYPE" in os.environ else "/content")
    root = pathlib.Path(top) / "eagle"
    for rel in (
        "examples/python/zero_copy_interop.py",
    ):
        (root / rel).parent.mkdir(parents=True, exist_ok=True)
        urllib.request.urlretrieve(f"{base}/amasat01/eagle/{ref}/{rel}", root / rel)
    (root / "docs/content/userguide/examples").mkdir(parents=True, exist_ok=True)
    os.chdir(root / "docs/content/userguide/examples")
    if not gpu:
        print("Running on the CPU (no GPU in this runtime). For a GPU: " + ("Settings → Accelerator → GPU T4 x2." if "KAGGLE_KERNEL_RUN_TYPE" in os.environ else "Runtime → Change runtime type → T4 GPU."))

try:
    import eagle, hawk, raptor  # noqa: F401
except ImportError as err:
    raise ImportError(
        "eagle, hawk, raptor not found. Install with `pip install raptor-core raptor-eagle raptor-hawk torch`. For a GPU, add [cuda12] (or [cuda13]) to raptor-eagle and raptor-hawk."
    ) from err

Hide code cell source

import sys, pathlib

# These examples live in the repository's own examples/python directory, not in
# the installed package — add it to sys.path so the module below
# imports straight from the checkout.
sys.path.insert(0, str(pathlib.Path("../../../../examples/python").resolve()))
import zero_copy_interop as demo
print(demo.__name__, "from examples/python/" + pathlib.Path(demo.__file__).name)
zero_copy_interop from examples/zero_copy_interop.py

The kernel this notebook hands to every framework is a plain per-sample vector scale:

import inspect
print(inspect.getsource(demo.step_kernel))
def step_kernel():
    """Trace ``y = k * x``: a ``Vector[3]`` input, a uniform, a mutable output."""
    import hawk

    @hawk.kernel
    def zc_scale(x: hawk.Vector[3], k: hawk.Param, y: hawk.Mutable[hawk.Vector[3]]):
        y = k * x

    return zc_scale

By hand: seed a batch of positions, then let hawk publish the compiled kernel into a manifest raptor validates.

import pathlib, tempfile
import numpy as np
import hawk.artifact
from raptor.schema import validate_manifest
import eagle.exec as eexec
from eagle import plan as eplan

positions = demo.seed_positions(64, seed=0)

bundle = hawk.build(
    [demo.step_kernel()], pathlib.Path(tempfile.mkdtemp()),
    mode="float32", targets=("host",))
validate_manifest(bundle.manifest)
print("exec_targets:", bundle.manifest["exec_targets"])
exec_targets: ['host']

Then hand it to eagle — this is the one call that actually launches the kernel, reading and writing the caller’s own numpy arrays:

plugin = next(iter(hawk.artifact.plugins(bundle).values()))
plan = eplan.plan(plugin, structure=eexec.HostTeam)
x = np.ascontiguousarray(positions, dtype=np.float32)
y = np.zeros_like(x)
ptr_before = x.ctypes.data
plan.bind(x=x, k=2.0, y=y).launch()
print("input array address unchanged:", x.ctypes.data == ptr_before)
input array address unchanged: True

The pieces above, assembled into one call — building, running and checking the crossing, for reference:

results = demo.run_demo(target="host")
target            : host (manifest exec_targets ['host'])
batch x k         : 64 x 2.0
host arm: plain numpy in/out through eagle.exec.HostTeam -- no row in the matrix governs this crossing (eagle's own host currency)
input array pointer stable across launch = True
result vs numpy reference: max|rel diff| = 0.000e+00 (tolerance 1e-05)

ptr_stable is the thing this crossing actually promises: the host array’s address never changes across the launch — the kernel wrote into a separate output array, it did not reallocate the input. No row in the certification matrix governs this leg (plain numpy through eagle.exec.HostTeam is eagle’s own host currency, not a framework crossing).

The device rows this notebook does not execute#

--device additionally certifies three rows, run here as read, not executed — this build’s torch is CPU-only:

  • T-IN-CUDA-ALIAS / T-OUT-CUDA-ALIAS — a CUDA-resident torch tensor crosses into and out of the kernel as a zero-copy view (eagle.to_cupy), checked by pointer identity and by mutating the input in place and relaunching the same bound plan.

  • T-IN-CPU-COPY — a CPU-resident torch tensor crosses through the identical adapter, which uploads it; the row declares a copy, not an alias, and the example asserts the pointers differ.

A CUDA-capable torch build exchanges device tensors the identical way, through the same __dlpack__ protocol a cupy array uses — nothing about the crossing mechanism changes between the two frameworks on the device side. See Interoperability for what each row certifies and which repository’s suite proves it.