Cast
Experimental C++20 API, package version 0.1.0. Link SimaKernels::a65_cast.
The caller contract below describes supported behavior; implementation notes describe
this release, not a stable dispatch or binary ABI promise.
Header: simaai/kernels/a65/cast.h (A65/cast/backend.h).
Typed request: ops::cast::Request.
Contract
The experimental C++20 interface is
simaai/kernels/a65/cast.h (A65/cast/backend.h).
Link SimaKernels::a65_cast from the installed CMake package.
The API accepts complete input/output specifications, with logical shape
length (zero to six dimensions), distinct scalar dtypes and explicit layout.
check_inference() returns an owning, immutable plan or a structured Error.
Only checked plans can be passed to execute().
- FP32 to BF16 and BF16 to FP32 only; same-type copies and other casts are rejected.
- Each port independently accepts
Contiguousor admittedStridedstorage. Input and output byte strides may differ; logical coordinates still correspond. Use CastTess or DetessCast for MLA-tiled conversion. - Strides are nonnegative scalar-width multiples. An axis larger than one needs a positive stride; singleton/zero-sized axes may have zero stride. Reversed, broadcast and internally overlapping views are unsupported. A conservative sorted-axis span proof admits ordinary permutations, padded rows and stepped views, but can reject exotic nonoverlapping interleavings.
- Only logical output elements are written. Output gaps and excess accessible capacity are preserved; input/output bounding spans must not overlap.
- Logical ranks zero through six, with independent axes. A scalar contains one element. A shape containing a zero extent is a no-op with zero required bytes.
- Negative dimensions, mismatched output shape/rank, wrong arity, unsupported dtype/layout and storage arithmetic overflow are rejected before computation.
- FP32/BF16 storage requires four-/two-byte alignment respectively. Execution rejects insufficient extents, null nonempty bindings, misalignment, address overflow and overlapping accessed input/output ranges before any writes.
- A plan owns copies of its specifications and can be reused with different buffers. Independent invocations may share a plan; overlapping concurrent buffers still require caller/Runtime ordering. No workspace is required.
The direct A65 entrypoint remains synchronous and selects the CPU implementation.
Its request and immutable plan use the shared
ops::cast contract (common/cast/cast.h).
The separate Runtime integration supplies asynchronous submission and hardware
selection; neither scheduling nor DMA-BUF allocation belongs to this kernel.
On x86 the scalar implementation serves host development; AArch64 uses NEON.
Parameters
Params is empty. The explicit output dtype selects the conversion; there is no redundant destination-type field.
Numerical behavior
FP32 → BF16 uses the exact unsigned 32-bit rule below. The addition wraps modulo 2^32 before the high 16 bits are selected:
const std::uint16_t bf16 = static_cast<std::uint16_t>(
(bits + 0x7fffU + ((bits >> 16) & 1U)) >> 16);
For finite inputs this rounds to nearest, ties to even. Do not replace it with a
hardware conversion that has a different NaN policy: the same bit rule is applied
to NaN payloads. Some signaling NaNs become infinity; high negative NaN payloads
can wrap to a positive result. For example 0x7f800001 produces 0x7f80, and
0xffffffff produces 0x0000. This is the current contract, not a claim that all
BF16 libraries share these special-value results.
BF16 → FP32 places the input word in the upper 16 bits and sets the lower bits to zero. Expansion preserves signed zero, infinity, subnormals and NaN bits. Both conversions manipulate representations without FP arithmetic, so the conversion itself does not depend on the caller's FP rounding/flush mode.
The implementation rule is in
numeric_bits.h (A65/detail/numeric_bits.h); the test oracle is independent.
C++ call
Place the includes and namespace aliases at file scope; the remaining statements
belong in a simaai::kernels::Status-returning caller function. input_views and
output_views are caller-supplied arrays of ConstHostBuffer and HostBuffer
in port order, with accessible capacities and alignment from the checked plan.
Keep their storage alive until synchronous execution returns.
#include <simaai/kernels/a65/cast.h>
#include <array>
namespace k = simaai::kernels;
namespace cast = k::a65::cast;
const std::array inputs{
k::TensorSpec{k::Shape{2, 3}, k::DType::Float32, k::Contiguous{}}
};
const std::array outputs{
k::TensorSpec{k::Shape{2, 3}, k::DType::BFloat16, k::Contiguous{}}
};
const cast::Request request{inputs, outputs, {}};
auto checked = cast::check_inference(request);
if (const auto* error = std::get_if<k::Error>(&checked)) return *error;
const auto& plan = std::get<cast::Plan>(checked);
return cast::execute(plan, input_views, output_views);
See the correctness tests (A65/cast/tests/correctness.cpp) for concrete storage,
independent expected results and failure cases.
See calling kernels for the shared lifecycle.
Memory and lifecycle
Shape throws std::length_error if constructed with more than six dimensions;
other admission failures return Error. Shape and parameter changes require a new
plan. Output dtype is authoritative; cast::Params is empty rather than repeating
a destination-type parameter.
ConstHostBuffer and HostBuffer are borrowed, synchronous CPU views. Their
extent must describe real accessible storage, not merely the desired tensor size.
The caller retains the underlying storage until execute() returns. A
Runtime integration must resolve these from its authoritative retained buffer views,
handle mapping/coherency and enforce allocation permissions. The kernel cannot
prove allocation ownership, accessibility or lifetime from a raw pointer.
Required bytes and alignment are available through input_requirements() and
output_requirements(); they are derived, not caller-specified tensor metadata.
Execution independently checks each supplied view, including on repeated calls.
Implementation notes
Preparation preserves the shared logical tensor contract and separately chooses
an A65-private physical traversal. Compatible input/output fastest-axis prefixes
coalesce into longer conversion spans even when their outer physical orders
differ. Reordering is selected only when it lengthens the original logical span;
singleton axes are inert. A fully packed compatible permutation uses one
numerical call without changing public contiguous(), shape, strides or rank.
For padded rows/planes, a bounded outer-coordinate carry advances both ports without per-element division. Independent permutations that have no longer joint span retain the established logical carry without packed-axis decoding in each fragment. Dense calls retain their direct conversion entry. Binding and alias checks still run before any write. No whole-tensor packing buffer is introduced.
A65 uses NEON conversion blocks and scalar tails. FP32 → BF16 processes two independent eight-element groups per full 16-element iteration, then an eight-element remainder and scalar tail. This overlaps load, integer rounding and store work without changing the unsigned bit rule or reading past the input. BF16 → FP32 uses 16-element blocks, an eight-element remainder and scalar cleanup. NEON widening shifts place each BF16 word directly in its FP32 bit position. GCC builds align the conversion loops to 64-byte instruction boundaries; this does not impose additional alignment on the caller's buffers. The checked dtype pair fixes the two scalar alignments used by binding validation.
The host build provides the scalar route for development. No dense intermediate or worker pool is created. Route widths are implementation details, while the bit rule above is the contract.
Tests and qualification
The default build includes a65_cast_contract and a65_cast_benchmark.
See build and test for host and DevKit execution.
The independent correctness suite is in
A65/cast/tests/correctness.cpp (A65/cast/tests/correctness.cpp).
The generic suite covers contiguous/strided port combinations, independent view
addressing, sentinel gaps, span overflow, singleton/empty handling and an
independent numerical oracle. Latency cases distinguish dense, span and scatter
paths; the numerical rule does not change between them.
See build and test for commands and the distinction
between host development checks, A65 measurements and baseline qualification.