Skip to main content

Call a kernel from C++

For generic application/framework preparation without operation-specific headers, use the cold named-request facade. It produces the same checked executable consumed by Runtime; parameter decoding never enters the execution path.

For direct A65 numerical calls, every kernel follows the lifecycle below. Parameters remain operation-specific C++ types; no JSON parser is involved.

  1. Describe every input and output with TensorSpec.
  2. Supply the operation's typed parameters.
  3. Call check_inference() once for that metadata.
  4. Handle Error, or retain the returned immutable Plan.
  5. Allocate/map real storage according to the plan's requirements.
  6. Pass borrowed buffer views to execute() and handle its status.
  7. Reuse the plan with new buffers, but prepare again when specifications or parameters change.
#include <simaai/kernels/a65/cast.h>
#include <array>
#include <cstdint>
#include <stdexcept>

int main() {
namespace k = simaai::kernels;
namespace op = k::a65::cast;
const std::array input_specs{
k::TensorSpec{k::Shape{2, 3}, k::DType::Float32}};
const std::array output_specs{
k::TensorSpec{k::Shape{2, 3}, k::DType::BFloat16}};
auto checked = op::check_inference(
{input_specs, output_specs, op::Params{}});
if (const auto* error = std::get_if<k::Error>(&checked))
throw std::runtime_error(error->message);
const auto plan = std::get<op::Plan>(std::move(checked));

// These arrays satisfy this example's checked requirements.
// General callers allocate from the plan's storage/alignment requirements.
std::array<float, 6> input{1, 2, 3, 4, 5, 6};
std::array<std::uint16_t, 6> output{};
const std::array inputs{k::ConstHostBuffer{
reinterpret_cast<const std::byte*>(input.data()), sizeof(input)}};
const std::array outputs{k::HostBuffer{
reinterpret_cast<std::byte*>(output.data()), sizeof(output)}};
if (auto error = op::execute(plan, inputs, outputs))
throw std::runtime_error(error->message);
}

Why provide an output specification?​

The checker validates the caller's expected output against the operation. Even shape-preserving operations receive an explicit output shape and dtype. Storage requirements are a checked result of the specifications, not an unchecked size claim from the caller.

What happens on each execution?​

Execution checks actual buffer arity, required spans, alignment and permitted aliasing before writing output. Operations with FP-mode requirements also check the applicable execution environment. Preparation is not permission to skip checks when binding new buffers.

A pointer and capacity cannot prove that memory is mapped or owned. The caller must supply accessible storage and keep it alive until the synchronous call returns. Runtime owns mapping, coherency, synchronization and resource lifetime; those services do not belong in a numerical kernel.

Reuse, concurrency and scratch​

Independent calls may share immutable plans. They must not race on input or output storage. Operations with scratch, such as BoxDecode and SuperPoint, need a separate prepared context for each in-flight execution. Use the operation's context API; scratch is reused, not allocated each frame. Do not share a busy context between concurrent calls. Context creation and preparation can allocate and throw; successful execution is allocation-free.

See BoxDecode's reference for its multi-input, multi-output call and SuperPoint for its context lifetime. The support matrix lists backend availability.

Asynchronous Runtime integration​

The numerical A65 execute() above remains a synchronous leaf operation. The separate Runtime executor owns the asynchronous job boundary: prepare() automatically calls canonical check_inference(), and submit() returns a completion handle. Its synchronous convenience call is exactly submission followed by waiting, not a second execution implementation.

Completion means computation and required memory synchronization have retired, not merely that a driver accepted the job. Runtime retains owners and mappings even if the caller drops the completion handle. Dependent work must observe both completion and failure; empty tensors skip numerical dispatch, not dependency ordering. Host pointers must never be passed as fabricated EV addresses.

Shared Cast, Dequant and Quant requests live in simaai/kernels/ops/cast.h, simaai/kernels/ops/dequant.h and simaai/kernels/ops/quant.h; the caller selects a target during preparation and Runtime verifies that target is available. EV use additionally needs an explicitly compatible native entry and transport. Host codec or fake-transport tests alone do not establish installed firmware support or device correctness.

Cast preparation, asynchronous submission, hardware execution and retirement

Open the asynchronous lifecycle diagram. The caller receives a handle independently of job progress; successful completion follows hardware retirement and memory-access synchronization.

Prepare a generic executable​

Use make_executable(request, target) when an execution service, rather than the calling thread, will invoke the operation. The factory belongs to Kernels and calls the same canonical validation as the direct API. It returns an immutable Executable containing the tensor contract and a selected implementation—not memory owners, a thread pool or a device connection.

// input_specs/output_specs remain explicit, as in the direct example above.
#include <simaai/kernels/ops/cast.h>

auto checked = simaai::kernels::ops::cast::make_executable(
simaai::kernels::ops::cast::Request{
input_specs, output_specs, {}},
simaai::kernels::Target::A65);

Runtime's generic prepare(executable) adds device-capability admission and bounded execution resources. Its typed convenience overload finds the kernel-owned factory through C++ argument-dependent lookup; adding an operation does not add an overload or operation-name switch to Runtime. Callers must link the operation's factory target. SimaKernels::executables is a convenience aggregate; Runtime itself depends only on SimaKernels::execution.

Typed request → kernel validation → immutable executable
↓
Runtime admission/resources
↓
Current owned buffers → submission → completion after retirement

The executable retains its specifications after the original request is destroyed. Preparation and resource provisioning can allocate; successful numerical execution does not. A new buffer must still pass current-capacity, alignment, ownership and dependency checks. There is no automatic CPU fallback if an EV entry is unavailable.

Reuse the prepared work​

What changes?Action
Values or buffers, with unchanged specifications and parametersReuse the plan; submit current views.
Stable allocations and mappingsBind once and replay the bound operation.
Shape, dtype, layout or ordinary parametersPrepare a new checked operation.
Geometry with a supported compatible variantCreate the immutable variant and reuse resources through the checked API.
Stable operation sequencePrepare and replay the sequence.

Do not persistently bind recyclable camera loans or mutate an in-flight plan.

Dynamic geometry and scratch​

ops::boxdecode::with_image_size(executable, image_size) produces a checked immutable BoxDecode variant. It shares model tables, tensor-contract storage and the exact resource recipe with the original executable. Runtime can admit it with the previous prepared operation to reuse compatible scratch. Existing invocations retain their own geometry. Creating the variant can allocate its small owning handle; it does not copy the tensor vectors or recreate the numerical context.

Generic consumers use the retained dynamic-variant interface for BoxDecode image_size and Preproc geometry. Preproc rechecks the complete new tensor/geometry contract; it does not claim compatible resources merely because the output canvas is unchanged. Other specification changes use the operation's factory. There is no arbitrary parameter patching or global shape cache. Substantial scratch is provisioned for simultaneous active executions, not every pending submission.

Kernels that use parallel host phases request a HostServices service during resource creation. Runtime owns the worker lifetime and shared concurrency budget. A task failure or exception drains already-started tasks before the service returns. A kernel must not create its own thread pool.

Host publication callbacks​

<simaai/kernels/host_authoring.h> exposes the opaque authoring::Contract, ContractOptions, HostOptions and HostEntry vocabulary for generic Runtime host-publication services. Consumers do not need private executable definitions. The provider supplies its checked storage requirements and immutable publication plan; this seam does not infer storage or validate an arbitrary callback's math. Numerical operations still use their canonical factories. Keep GStreamer, firmware structures and application configuration outside this vocabulary.

Package compatibility​

This interface requires matching supported package builds; it is not a stable cross-version C++ binary ABI. EV execution additionally requires matching native firmware. The kernel package supplies operation registrations and descriptor rules; Runtime retains device submission, DMA authorization and completion. A successful host factory call does not establish device correctness or speed.