Native Quant boundary
Use simaai/kernels/ops/quant.h for the same request on A65 and EV74. Link
SimaKernels::quant; the EV host codec is SimaKernels::ev74_quant.
Host admission and packet preparation do not initialize MetaWare or depend on
ConfigManager, JSON, GStreamer or private Core headers.
Shared tensor contract
Quant defines the canonical numerical contract: FP32, BF16 or IEEE FP16 input and explicit INT8 output, with matching logical shapes from scalar through rank six. Both ports independently accept contiguous or admitted nonnegative, scalar-aligned strided storage. Active axes require positive strides and must pass the shared conservative nonoverlap proof. Singleton and unused strides are normalized; output gaps remain unchanged. Tessellation is a separate operation, not an implicit interpretation of INT8.
Params requires a finite, strictly positive FP32 multiplier, including
subnormal values. It is not a divisor or a quantization scale. The zero point
defaults to zero and must be in [-128, 127]. Rounding defaults to nearest-even;
toward-zero, toward-positive and toward-negative are also supported.
Preparation, binding and submission
ops::quant::check_inference()validates parameters, shapes, dtypes, layouts and overflow-safe accessed spans, returning an owning immutable plan.ev74::quant::check_inference()checks device representability. EV addresses and accessed spans must fit its 32-bit address domain; this is a backend limit, not a change to the mathematical contract.encode_relocatable()creates an immutable packet with zero address slots.- Runtime retains the packet and tensor owners, and supplies DMA-BUF relocations
or calls
bind()with resolved device regions. Binding checks capacities, alignment, address ranges, nonaliasing and that the packet matches its plan.encode()combines packet preparation and binding. - Runtime submits asynchronously. Completion requires confirmed device retirement and completed coherency; a synchronous caller submits and waits.
Empty requests produce no numerical submission. Scalar requests lower privately to one element. No input tensor is scanned at preparation: special input values are handled numerically during execution. Allocation, mapping, cache maintenance, scheduling and recovery belong to Runtime, not to Quant.
Ordered numerical implementation
For each logical input element, preserve this order:
- Interpret the source as its declared floating-point format.
- Multiply by the FP32 multiplier, rounding the product to IEEE binary32 with round-to-nearest-even and gradual underflow.
- Round that binary32 result to an integer using the selected rounding mode.
- Add the INT8-range zero point in a wider integer representation.
- Saturate to
[-128, 127]and store one byte.
NaNs of either sign and any payload map to the zero point. Positive and negative infinity saturate by sign. Both signed zeros map to the zero point. Subnormal inputs, multipliers and intermediate products follow the same staged rules. The integer rounding mode does not change the preceding binary32 rounding.
The separate product rounding matters. For example, an exactly represented input
of 15 multiplied by FP32 0x3e99999a first rounds to binary32 4.5. Nearest-even
then gives 4, although rounding the exact real product directly would give 5.
Similarly, adding the zero point after prematurely clipping to INT8 changes
valid results: 200 with a zero point of -100 must produce 100.
Normal-product path and exact integer fallback
The shared FP32 product-rounding semantics do not require all computation to use FP32 instructions. The current implementation combines a register FP32 multiply for eligible vector blocks with integer rounding, packing and an exact integer fallback. This implementation choice requires target correctness and latency validation; it is not itself a performance claim.
For the normal-product path, FP32 input bits are used directly; BF16 expands by shifting its bits into FP32 positions. FP16 is decoded with integer exponent and significand operations, including exact normalization of half subnormals. Every nonzero FP16 subnormal is a normal FP32 value, so a flushing half-conversion instruction is unnecessary.
A 16-element block takes the exact integer path if any of these conditions holds:
- The positive multiplier is subnormal.
- Any decoded input is a nonzero FP32 subnormal.
- After multiplication, any nonzero decoded input produced a zero-exponent result, including a subnormal or a flushed-to-zero result.
Otherwise a single FP32 round-to-nearest-even multiply supplies the product bits. Integer operations implement the requested final rounding mode, zero-point addition and saturation. Normal inputs and multipliers cannot produce a spurious zero through input flushing; the result check detects output underflow even when hardware flushes it to zero. NaNs, infinities and true input zeros retain their explicit numerical policies. The ordinary multiply still requires the native execution environment's default round-to-nearest-even mode; denormal handling alone is not a proof of independence from every FP setting.
For FP16 input, every finite nonzero decoded value is normal in FP32 (the
smallest is 2^-24). Its SIMD path therefore omits the input-FP32-subnormal
classifier. It still checks subnormal multipliers and product underflow and
uses the same exact repair; this is not a relaxed numerical contract.
Fallback recomputes the entire affected block from the original source bits; it does not use the possibly flushed product. Preparation decodes the multiplier as an integer significand times a power of two, and removes exact powers of two once per invocation. This path, also used by scalar tails, is independent of floating-point flush and rounding modes.
Each fallback vector lane decodes its input significand and exponent, forms the exact product, and rounds discarded bits with explicit retained-bit, halfway and sticky-bit tests. The required right shift is the maximum of:
- zero;
- the product bit width minus 24, enforcing binary32 precision;
- the shift needed to respect the binary32 subnormal quantum
2^-149.
BF16 needs at most an 8-by-24-bit product, which fits one 32-bit lane. FP16 and FP32 need at most 35 and 48 bits, respectively; the fallback vector path uses low and high 32-bit products rather than generic 64-bit vector emulation. It then applies the selected integer rounding, bounded magnitude, signed zero-point addition and saturation. Scalar tails use the same ordered contract with a bounded 64-bit integer product. Neither path creates an intermediate FP32 tensor or an input-dependent fallback table.
Each dtype/rounding combination selects a specialized numerical entry once per invocation: three input dtypes times four rounding modes. This removes those choices from runtime dispatch inside the element loop. SIMD structure alone is not evidence of a latency improvement; target measurements remain necessary.
Data movement
Quant uses CPU0 alone for a single work unit; otherwise it launches the complete
configured active ring, capped at four workgroups. Work units are DMA blocks,
short contiguous rows or individual scatter elements, as appropriate. Launching
the complete ring preserves ownership of work unit zero when the dispatcher keeps
physical CPU/workgroup IDs; workers without an assigned unit return immediately.
The runtime schedules CPUs 1,...,N-1,0, keeping CPU0 last and preserving
physical CPU/workgroup-ID equality. Quant needs no ID remapping.
Do not combine this device implementation with the previous dispatcher archive.
Use the matching runtime cohort (EV74/firmware/runtime-cohort.json) and
full platform firmware composition. This changes dispatch, not the ordered
arithmetic, tensor descriptor or asynchronous host API.
It recognizes the common contiguous suffix of input and output layouts:
- Spans of at least 128 elements use ping-pong local input/output buffers. FP32 blocks contain 4,096 elements; BF16 and FP16 blocks contain 8,192. Each full input transfer is 16 KiB; output is 4 KiB for FP32 input or 8 KiB for BF16/FP16 input. The two local input/output slots occupy 40 KiB or 48 KiB respectively, plus existing guard padding. Input prefetch overlaps computation. Before reusing an output slot, execution waits for its previous output transfer, and waits for both final outputs before returning.
- Contiguous spans of 16–127 elements use direct vector global loads and stores, followed by exact scalar tails, avoiding DMA startup.
- Shorter or scatter spans use a coordinate carry walker. Division/modulo initializes each worker's coordinates; it is not repeated for every element.
Private block constants are shared by native-entry worker admission and OpenCL execution, so both count the same work units. This changes task geometry, not the dtype/rounding dispatch, ordered arithmetic or descriptor ABI. Runtime still submits once per invocation; it does not submit individual chunks.
Measure address-phase sensitivity with both normal finite inputs and edge-value profiles. Exact underflow fallback can increase compute cost and hide transfer variation. Retain allocations while independently varying both port offsets; a fixed shape alone does not fix DMA latency.
Vector and DMA tails operate only on valid elements. No output preclear, packing pass or tensor-sized host scratch is required. Padding and excess capacity are not written. Block sizes and worker count are implementation choices, not a promise that every shape is faster than another backend.
Descriptor and private control ABI
simaai_kernels_ev74_quant_v1 consumes a 224-byte little-endian descriptor:
| Byte offsets | Content |
|---|---|
| 0–23 | Magic, version, byte length, input conversion, rank, flags and reserved word |
| 24, 32 | Input/output 64-bit relocation slots; actual device addresses are 32-bit |
| 40, 48, 56 | Logical element count and accessed input/output byte spans |
| 64, 112, 160 | Six-capacity extents, input byte strides and output byte strides |
| 208, 212, 216 | Multiplier FP32 bits, signed zero point and rounding code |
| 220 | Reserved word, required to be zero |
Unused axes are zero. The device validates descriptor fields, parameters, spans, strides, address ranges and overlap before selecting a numerical entry. The transport must still retain a valid mapped descriptor allocation: checking an address value cannot prove that it names accessible memory.
The private ARC C++/OpenCL dispatcher control is separate from this wire packet. It has explicit 8-byte alignment; both target compilers assert every field offset and its 192-byte size. Explicit build dependencies track its header for the private OpenCL compiler. A host mock does not establish cross-language target layout or device DMA behavior.
Firmware must explicitly compose and advertise the native entry/version. Missing capability is an error, not a reason to reinterpret the packet for an unmatched entry or silently execute on A65.
Tests and qualification
ev74_quant_contract links the real native decoder to a mock dispatcher.
It covers descriptor rejection, scalar/rank-six lowering, contiguous and strided
views, packet/plan mismatch, immutable rebinding, parameter bits, empty tensors,
capacity/alignment/address failures and dispatcher errors.
The same host test compares the production integer scalar arithmetic against an independent oracle: exact binary64 multiplication, an explicit binary32 rounding stage, then independently expressed integer rounding and saturation. Hard-coded boundary goldens cover double rounding, signed zero, NaNs, infinities, underflow, overflow and saturation ordering. Exhaustive BF16/FP16 bit patterns cross multiplier profiles, all four modes and extreme zero points; FP32 cases cover every exponent class and additional boundary patterns. Both original and power-of-two-reduced multiplier representations must agree with the oracle.
When Clang is available, an additional native-host test compiles the actual OpenCL vector expressions through compatible host vector types. It compares the prepared fast/fallback path against the independent oracle, including exhaustive 16-bit input patterns, FP32 exponent boundaries, isolated exceptional lanes and normal-product underflow. On x86, it repeats with FTZ/DAZ enabled only around the production expression; expected values are generated beforehand in the default IEEE environment. CMake reports whether this optional host check is enabled. It is not required to build the portable library and is not EV instruction validation.
These checks do not execute EV vector instructions or DMA. Private compilation checks target source and ABI assertions but is also not device acceptance. Actual EV validation must exercise all 12 entries, scalar/vector/DMA boundaries, scatter, repeated invocation and untouched gaps against independent expected bytes. Runtime latency and entry-to-return latency have different scopes; report them separately. See private builds for licensed-toolchain requirements.
C++ call
Place the includes and namespace aliases at file scope; the remaining statements
belong in a simaai::kernels::Status-returning caller function. It checks metadata and
encodes a relocatable descriptor, not EV74 computation. Runtime must map actual
buffers, bind addresses and retain inputs, outputs and any workspace until
confirmed device completion.
#include <array>
#include <simaai/kernels/ev74/quant.h>
#include <simaai/kernels/ops/quant.h>
namespace k = simaai::kernels;
namespace op = k::ops::quant;
namespace ev = k::ev74::quant;
const std::array inputs{k::TensorSpec{{2, 3}, k::DType::Float32}};
const std::array outputs{k::TensorSpec{{2, 3}, k::DType::Int8}};
const op::Request request{inputs, outputs, op::Params{1.0F}};
auto checked = op::check_inference(request);
if (const auto *error = std::get_if<k::Error>(&checked)) return *error;
const auto &plan = std::get<op::Plan>(checked);
auto encoded = ev::encode_relocatable(plan);
if (const auto *error = std::get_if<k::Error>(&encoded)) return *error;
// Encoding leaves device addresses unbound; no device work has run.
return {};
See the host contract tests (EV74/quant/tests/correctness.cpp) and
device fixture (EV74/quant/tests/runtime_e2e.cpp) for binding and execution coverage.
Private physical traversal and bounded publication windows
Compatible input/output physical orders are coalesced before span selection. Different port orders use bounded transpose/gather panels, never a whole-tensor repacking allocation. Regular short rows can share existing local output roles when four useful owners and complete output-granule boundaries remain proved.
Normal SIMD prefixes, exceptional repairs and parameter-only constants retain the same ordered arithmetic. Moving a request to a larger publication window changes transport, not rounding or saturation. Source compilation or fewer DMA commands alone does not establish a target latency improvement.