C · Tensors and Views

Tensors and Views

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

The native containers are where most integration mistakes happen. They need to be documented explicitly.

  • vector<T, A> owns storage and defaults to aligned_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-R storage and also defaults to aligned allocation.
  • tensor_view<T> and tensor_span<T> are the view forms.
  • matrix, matrix_view, and matrix_span are 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.
  • reshape and flatten require 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 column

The 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());

API

Macros

NUMKONG_TENSOR_MAX_RANKThe highest tensor rank the bindings accept, matching PyBUF_MAX_NDIM by default.
types.h:972 ↗
#define NUMKONG_TENSOR_MAX_RANK (64)