#define NUMKONG_TENSOR_MAX_RANK (64) The native containers are where most integration mistakes happen. They need to be documented explicitly.
vector<T, A>owns storage and defaults toaligned_allocator<T, 64>.vector_view<T>is a const strided non-owning view.vector_span<T>is a mutable strided non-owning view.tensor<T, A, R>owns rank-Rstorage and also defaults to aligned allocation.tensor_view<T>andtensor_span<T>are the view forms.matrix,matrix_view, andmatrix_spanare rank-2 aliases.
The important layout rules are:
- Signed strides are supported by the view types.
- Reversed and sliced views are valid for many elementwise and reduction kernels.
reshapeandflattenrequire contiguous layout.- Matrix-style kernels care about row contiguity, not just total tensor contiguity.
- Negative strides are conceptually valid views, but matrix packing and packed matmul workflows are not written around them.
Memory ownership is explicit. vector and tensor deallocate through their allocator. vector_view, tensor_view, matrix_view, and spans never own memory.
And heterogenous index types for operator[] enable more interesting access patterns:
#include <numkong/numkong.hpp>
namespace nk = ashvardanian::numkong;
using nk::slice, nk::all, nk::f32_t, nk::tensor, nk::tensor_view;
auto [t, status] = tensor<f32_t>::from({
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
});
assert(nk::succeeded(status) && "allocated");
f32_t scalar_at_2d_coordinate = t[1, -1];
f32_t scalar_at_global_offset = t[5];
assert(scalar_at_2d_coordinate == scalar_at_global_offset && "same value");
tensor_view<f32_t> scalar_as_tensor = t[1, 1, slice];
tensor_view<f32_t> second_row = t[1, slice];
tensor_view<f32_t> second_column = t[all, 1, slice];
assert(second_row[1] == second_column[1] && "same value");You can also use a more traditional syntax with member functions, also leveraging built-in functionality for hardware-accelerated strided reductions and elementwise operations along any axis combination. Similar to NumPy, but statically typed:
tensor_view<f32_t> second_column = t[all, 1, slice]; // strided column view → {2, 5, 8}
auto minimum_index = nk::argmin(second_column); // index of the minimum in the second columnThe view types are conceptually close to std::mdspan from C++23.
The main differences are sub-byte element support, signed strides, and the kernel dispatch integration that std::mdspan does not provide.
If your codebase already uses std::mdspan, converting at the NumKong call boundary is straightforward:
#include <numkong/numkong.hpp>
#include <mdspan>
namespace nk = ashvardanian::numkong;
// Existing std::mdspan from your codebase
float data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
auto md = std::mdspan<float, std::extents<std::size_t, 3, 3>>(data);
// Wrap into a NumKong matrix_view — data pointer, extents, and strides map directly
auto view = nk::matrix_view<nk::f32_t>(
reinterpret_cast<nk::f32_t const *>(md.data_handle()),
md.extent(0), md.extent(1));
// Now use any NumKong kernel on it
auto [dot, status] = nk::dot<nk::f32_t>(view.row(0).as_vector(), view.row(1).as_vector());