EXTENDING HYPERFOLD

Register custom kernels.

From version 0.1.1

Use Python callbacks for prototyping, orchestration and tests. Declare an operation contract, register exact backend signatures, then seal them into a context.

OperationSpec owns operand roles and access modes. KernelSpec supplies a backend-specific signature and callback. Execute the finished operation with a mapping keyed by OperandRole; outputs are caller-owned.

PYTHON CALLBACKS

Declare, register and execute an operation.

Python callbacks are synchronous and suit prototypes, orchestration and tests. The callback receives the original caller-owned objects in an immutable role mapping and writes every declared output.

PythonComplete CPU example
import numpy as np
import hyperfold as hf

operands = (
    hf.OperandSpec(hf.OperandRole.INPUT_0, hf.OperandKind.VECTOR),
    hf.OperandSpec(
        hf.OperandRole.OUTPUT,
        hf.OperandKind.VECTOR,
        hf.OperandAccess.WRITE,
    ),
)

def double(invocation: hf.Invocation) -> None:
    source = invocation.bindings[hf.OperandRole.INPUT_0]
    output = invocation.bindings[hf.OperandRole.OUTPUT]
    output[:] = 2 * source

backend_operands = (
    hf.BackendOperandSpec(hf.OperandRole.INPUT_0, hf.OperandKind.VECTOR),
    hf.BackendOperandSpec(
        hf.OperandRole.OUTPUT,
        hf.OperandKind.VECTOR,
        hf.OperandAccess.WRITE,
    ),
)

builder = hf.ContextBuilder()
extension = builder.extension("acme")
extension.register_operation(hf.OperationSpec("acme.double", operands))
extension.register_kernel(hf.KernelSpec(
    "acme.double", hf.BackendKind.CPU_SCALAR, backend_operands, double
))
context = builder.build()

source = np.array([1, -2, 4], dtype=np.float32)
output = np.empty_like(source)
result = context.execute(
    "acme.double",
    {hf.OperandRole.INPUT_0: source, hf.OperandRole.OUTPUT: output},
)
assert result is output

Inputs must be contiguous, ordered, read-only roles followed by contiguous mandatory write or read-write outputs. The builder rejects gaps, duplicates, reordered roles and access mismatches. A single-output call returns the supplied Output0 object; a multiple-output call returns the supplied output objects as an ordered tuple.

DISPATCH CONTRACT

Python kernels match exact storage.

BackendOperandSpec registers an exact backend, dtype, compute dtype, location, layout and contiguity signature. Python callbacks do not use native adaptation. Pass an optional role-to-dtype mapping as compute_dtypes= to Context.execute() to select a registered storage/compute pair without casting storage. The callback reads the chosen values from Invocation.compute_dtypes.

Validators receive an immutable binding map before dispatch and return None; raise an exception to reject the call. Callback exceptions propagate to the caller. A CUDA callback receives the selected device and Hyperfold stream pointer through Invocation.cuda_device and Invocation.stream; Hyperfold synchronises that stream before returning.

A builder is mutable until build() and may be built only once. The finished context owns its registered Python callbacks. See the Python extension reference for every builder, descriptor, enum and numerical metadata record.

COMPILED KERNELS · DEVELOPMENT ABI

Use a compiled kernel from Python.

Build your kernel as a shared plug-in, then load it into the context that will execute it. Python only selects and loads the plug-in; the shared library owns the native CPU or CUDA implementation.

  1. 01
    Build the shared plug-in

    Compile against the public headers exposed by the installed Python package. A C ABI plug-in does not link against Hyperfold.

  2. 02
    Load it into a context

    Pass the plug-in path to ContextBuilder.load_plugin() before calling build().

  3. 03
    Inspect and execute

    Use operations() and describe() to check registration, then execute the operation through the context.

Build for the target host

Compile your C11 plug-in as a shared module against the public headers supplied by the Python package. The plug-in does not link against Python or Hyperfold; match the host ABI, operating system, architecture and runtime.

# CMakeLists.txt
add_library(acme_plugin MODULE acme_plugin.c)
target_include_directories(acme_plugin PRIVATE
  "${HYPERFOLD_INCLUDE_DIR}")

# Supply HYPERFOLD_INCLUDE_DIR from Python:
# python -c "import hyperfold; print(hyperfold.get_include())"
Load and verify in Python

Load the compiled library before building the context, then inspect its registered operation to confirm that the plug-in was accepted.

from pathlib import Path
import hyperfold

context = (
    hyperfold.ContextBuilder()
    .load_plugin(Path("build/libacme_plugin.so"))
    .build()
)

description = context.describe("acme.my_kernel")
print(description.implementations)

Use the actual CMake output path for your platform; Windows modules use .dll.

WHAT THE PLUG-IN MUST PROVIDE

Export hyperfold_plugin_init, return plug-in metadata, and register kernel descriptors through host->register_kernel. The first kernel defines the operation contract; subsequent implementations must agree on roles, access and kinds. Check the host structure size and exact HYPERFOLD_PLUGIN_ABI_REVISION before registration; rebuild plug-ins against the current headers. Use hyperfold/plugin/abi.h for C11, or hyperfold/plugin/sdk.h for C++17 helper utilities.

  • Callbacks check operation-specific shapes after adaptation and before writing outputs or launching work. CPU callbacks complete their output before returning. CUDA callbacks enqueue work on the stream supplied by Hyperfold.
  • Do not let C++ exceptions cross an ABI callback; use Hyperfold::Plugin::translate_exceptions with the C++ helper.
  • The context retains the library while its operations or executions still need it.

The complete plug-in ABI reference documents every C type and constant, callback lifetime, CPU and CUDA execution rules, CMake setup, and complete C and C++17 entry-point examples.