Run your first kernel#

Write one line of math, compile it once, and run it unchanged on the CPU or a GPU.

Time: ~5 min · Runs on: CPU, GPU switch · You need: eagle, hawk, NumPy and matplotlib

A kernel is a plain Python function decorated @hawk.kernel: each parameter’s type annotation says what role it plays for every sample.

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 python -m raptor_docs.nb colab_setup (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, f"raptor-eagle{extra}", f"raptor-hawk{extra}"])
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "matplotlib"])
    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 (
        "docs/content/_shared/nb_helpers.py",
        "docs/content/_shared/themed_figure.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").mkdir(parents=True, exist_ok=True)
    os.chdir(root / "docs/content/userguide")
    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, matplotlib  # noqa: F401
except ImportError as err:
    raise ImportError(
        "eagle, hawk, matplotlib not found. Install with `pip install matplotlib raptor-eagle raptor-hawk`. For a GPU, add [cuda12] (or [cuda13]) to raptor-eagle and raptor-hawk."
    ) from err
import hawk
from hawk import Mutable, Param, Scalar


@hawk.kernel
def scale(x: Scalar,              # a per-sample input
          a: Param, b: Param,     # a shared constant: one value for the run
          y: Mutable[Scalar]):    # an output plane the body assigns
    y = a * x + b


scale
<Kernel scale slots=4>

hawk.artifact.build compiles it for a chosen target — "host" means the CPU alone, no GPU or eagle involved:

import pathlib
import tempfile

import hawk.artifact

work_dir = pathlib.Path(tempfile.mkdtemp())
hawk.artifact.build(scale, work_dir, targets=("host",));

hawk.load loads the compiled artifact and hawk.run runs it directly:

import numpy as np

host_kernel = hawk.load(work_dir, "scale")
x = np.linspace(0.0, 7.0, 8)
y = np.zeros(8)
hawk.run(host_kernel, x=x, a=2.0, b=1.0, y=y)
y
array([ 1.,  3.,  5.,  7.,  9., 11., 13., 15.])

Every other notebook skips these steps and calls the kernel: it is built on first use, and a kernel with one Mutable output returns that array (several come back as a dict, out["name"]). build, load and run are for when you want the artifact itself.

y_call = np.asarray(scale(x=x, a=2.0, b=1.0))
np.allclose(y_call, y)
True

That is the whole authoring surface for this kernel: declare the planes, write the arithmetic, build it, load/run it, or just call it — no eagle and no GPU anywhere above.

The same kernel, wherever the data lives#

eagle.deploy(scale) builds the same kernel for host and GPU through hawk’s cache (a rerun compiles nothing) and returns a plan — a small object that knows how to run this kernel on whatever array you hand it, NumPy or CuPy. The hidden cell below picks a GPU automatically when one is visible, and falls back to the CPU path otherwise — every tutorial in this family runs on CPU.

Hide code cell source

import sys, pathlib as _pathlib
sys.path.insert(0, str(_pathlib.Path.cwd().parent / "_shared"))
from nb_helpers import DEVICE, plot_style, show, to_numpy, xp
plot_style()
import eagle

plan = eagle.deploy(scale, cache_dir=work_dir)
y_xp = plan.run(x=xp.asarray(x), a=2.0, b=1.0)
agrees = np.allclose(to_numpy(y_xp), y)
print("DEVICE:", DEVICE, "| y matches the CPU run:", agrees)
DEVICE: cpu | y matches the CPU run: True

Same x, run on the host above and through eagle.deploy here — the numbers agree. One plot makes it visible:

Hide code cell source

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(4.5, 3))
ax.plot(x, y, color="#898781", lw=1.5, zorder=1, label="y = 2x + 1")
ax.scatter(x, y, color="#2a78d6", s=36, zorder=2, label="host (CPU)")
ax.scatter(x, to_numpy(y_xp), color="#eb6834", s=16, zorder=3, label="eagle")
ax.set_xlabel("x"); ax.set_ylabel("y"); ax.legend(frameon=False)
ax.grid(alpha=0.4)
show()
Chart

What just happened#

  • A kernel’s parameter annotations declare its planes (Scalar, Param, Mutable[...]); the body is ordinary arithmetic over them.

  • hawk.artifact.build + hawk.load/hawk.run compile and run a kernel on the host alone, with no eagle, no GPU, and no manifest involved.

  • Calling the kernel, scale(x=x, a=2.0, b=1.0), does the same in one step; every other notebook does that and links here for the build.

  • eagle.deploy(kernel) builds the same kernel for every target once and runs it wherever its data lives — a NumPy array on the CPU, a CuPy array on the GPU — so one plan serves both without a second code path.

Try this#

In the build/run cells above, change b=1.0 to b=0.0 and re-run all three cells: the line now passes through the origin, and nothing else about the kernel changes.

Next#

Arguments and shapes — pick the right plane type for a vector, a matrix, or a quaternion, and see how a batch of samples is laid out in memory. Deeper: Interoperability for every detail of how a plane binds to a NumPy/CuPy/Torch array.