C++ · Quickstart

Quickstart

#include <numkong/types.hpp> 8.0.0-rc.1 · b1f866ab

#include <numkong/numkong.h>
#include <stdio.h>

int main(void) {
    nk_f32_t a[] = {1, 2, 3};
    nk_f32_t b[] = {4, 5, 6};
    nk_f64_t dot = 0;
    nk_capability_t capabilities = nk_cap_serial_k;
    nk_cpu_capabilities_enabled(&capabilities); // the capabilities this CPU runs and this build holds
    nk_cpu_configure_thread(capabilities);
    nk_status_t status = nk_dot_f32_best(a, b, 3, &dot, capabilities, NULL); // widened f32 → f64 output
    printf("dot=%f, %s\n", dot, nk_status_name(status));
    return status != nk_success_k;
}

Every operation has one such dispatch point, named with _best, which runs the best capability in the mask it gets; the root README describes the dispatch model once.

§ The C ABI

The C ABI keeps the operation family, input dtype, and output policy visible in the symbol name. That makes widening obvious at the call site.

#include <numkong/numkong.h>

nk_i8_t a[1536];
nk_i8_t b[1536];
nk_i32_t dot = 0;     // widened from int8 storage
nk_f32_t l2 = 0;      // widened from int8 storage

nk_dot_i8_best(a, b, 1536, &dot, capabilities, NULL);
nk_euclidean_i8_best(a, b, 1536, &l2, capabilities, NULL);

The examples below pass the capabilities of the Quickstart and a null stream, as every CPU call does. Each capability kernel, like nk_dot_i8_haswell, is also callable directly with the same arguments short of the mask. To resolve a kernel once and call it many times without naming a capability, library builds provide the punned finder:

nk_metric_dense_punned_t angular = 0;
nk_capability_t capability = 0;
nk_find_kernel_punned(nk_kernel_angular_k, nk_f32_k, capabilities, (nk_kernel_punned_t *)&angular, &capability);

nk_f32_t a[768], b[768];
nk_f64_t result = 0; // widened f32 → f64 output
angular(a, b, 768, &result, NULL);

That is the lowest-level dynamic path. The typed C++ wrappers usually read better unless you are building your own dispatch layer.

§ The C++ Layer

The C++ wrappers come in three layers. Raw-pointer overloads mirror the C ABI one to one, with explicit lengths, strides and task windows for sharded launches. Overloads over the nk::vector_of and matrix concepts accept any contiguous run, like std::vector, std::array, std::span, a C array or an nk::vector_view, and return their results. Owning types, like nk::tensor, nk::packed_matrix and nk::packed_attention, allocate through your allocator for repeated workloads.

#include <vector>
#include <numkong/numkong.hpp>

namespace nk = ashvardanian::numkong;

int main() {
    std::vector<nk::f32_t> a {1, 2, 3}, b {4, 5, 6};
    auto [dot, status] = nk::dot<nk::f32_t>(a, b); // nk::f32_t::dot_result_t == nk::f64_t
    if (nk::failed(status)) return 1;

    nk::f64_t raw {};
    status = nk::dot(a.data(), b.data(), a.size(), &raw, nk_cap_serial_k); // the raw layer, pinned to serial
    if (nk::failed(status)) return 1;

    auto [matrix, matrix_status] = nk::matrix<nk::f32_t>::full({2, 3}, nk::f32_t(1));
    if (nk::failed(matrix_status)) return 1;
    auto [packed, packed_status] = nk::packed_matrix<nk::f32_t>::make(matrix.view()); // owning, reused across calls
    return nk::succeeded(packed_status) && dot == raw ? 0 : 1;
}

Standard containers count storage values, so an std::vector<nk::u1x8_t> of 4 bytes holds 32 dimensions, while NumKong’s own vectors and views count dimensions. Runs must fill whole storage values, so odd bit and nibble lengths pad with zero bits up to the next byte, and a view ending mid-value is refused. Strided views are refused with nk::status_t::unexpected_dimensions_k, and vector_view::values() turns a contiguous view into an std::span or refuses the same way. Outputs of elementwise kernels, like nk::scale<nk::f32_t>(a, 2.0f, 1.0f, out), are any mutable run of the same size, and the call returns an nk::status_t. Multi-part results come back as small structs: nk::reduce_moments returns {sum, sumsq}, nk::reduce_minmax a minmax_result, and nk::shapes_packed::make an owner of many packs.

Every wrapper ends in the two arguments of its dispatch point, defaulted to nk::default_capabilities() and a null stream, and returns an nk::status_t. That scoped enum mirrors nk_status_t value for value, converts to it with static_cast, and is tested with nk::succeeded and nk::failed rather than as a bool. Factories, like nk::tensor<T>::zeros, and the allocating overloads return an nk::expected<T> holding the value and its status, which converts to true on success and unpacks with structured bindings. A failed factory leaves its value empty, while a zero-volume request succeeds with an empty one. nk::default_capabilities() is the nk_cpu_capabilities_enabled mask, and a zero mask runs the C++ reference template instead of any capability’s kernel. nk::device_t names one device by its kind and its runtime’s ordinal, and asks the matching nk_<kind>_* functions for its masks:

nk::device_t const cpu = nk::device_t::cpu();
if (auto enabled = cpu.capabilities_enabled()) (void)cpu.configure_thread(enabled.value);
auto const [devices, counted] = nk::device_t::count(nk::device_kind_t::cuda_k);
for (std::size_t ordinal = 0; ordinal != devices; ++ordinal)
    if (auto gpu = nk::device_t::make(nk::device_kind_t::cuda_k, ordinal))
        if (auto mask = gpu.value.capabilities_enabled()) { /* dispatch over `mask.value` on that GPU */ }

In header-only builds the dispatch points report nk_missing_library_k, so wrappers there take a zero mask or call a capability’s kernel.

The API is intentionally not STL-shaped. vector_view, tensor_view, and matrix_view prioritize signed strides, sub-byte storage, and kernel compatibility over resizable-container ergonomics.

API

Types

status_tWhy a call failed, in the vocabulary the C ABI and every binding share.
types.hpp:140 ↗
enum status_t

Every enumerator takes its value from nk_status_t in types.h, so the C++ mirror and the ABI cannot drift, and the crossing is a cast rather than a table.

Statuses fall into three categories, with no interruptions or defects to report. Resources: bad_alloc_k, missing_gpu_k, device_code_mismatch_k, missing_kernel_k and missing_library_k. Input: unexpected_dimensions_k, device_memory_mismatch_k and misaligned_k. State: pack_mismatch_k. Any verb that allocates, binds a device or touches a file may also return a Resources status. A verb's own block lists only what its contract adds.

Values

success_knk_success_kScheduled, or finished, without error.
bad_alloc_knk_bad_alloc_kAn allocation failed, or the queue could not record another block.
unexpected_dimensions_knk_unexpected_dimensions_kOperand shapes contradict each other, or exceed a rank or a capacity.
missing_gpu_knk_missing_gpu_kNo GPU of the vendor this build targets answers.
device_code_mismatch_knk_device_code_mismatch_kThe device code lacks the kernel, or it failed to build, launch or finish.
device_memory_mismatch_knk_device_memory_mismatch_kAn operand lies in memory the device cannot address.
missing_kernel_knk_missing_kernel_kNo capability in the capability mask has this kernel.
misaligned_knk_misaligned_kAn operand or stride breaks the alignment contract, like a strided sub-byte view.
pack_mismatch_knk_pack_mismatch_kThe buffer was packed by another capability or layout than the one reading it.
missing_library_knk_missing_library_kA dispatch point or finder called from a header-only build, which links no library.
expectedA result paired with the status_t explaining it; the value is only meaningful on success_k.
types.hpp:185 ↗
template <typename value_type_>
struct expected

Fields

valuevalue_type_
statusstatus_t

Functions

succeededWhether status reports success.
types.hpp:174 ↗
bool succeeded(status_t status) noexcept;
failedWhether status reports failure.
types.hpp:177 ↗
bool failed(status_t status) noexcept;
status_nameStatic, English description of status.
types.hpp:180 ↗
char const * status_name(status_t status) noexcept;

Never returns nullptr, never allocates.