Basic Usage
Imports, Includes and Constructing Arrays
Section titled “Imports, Includes and Constructing Arrays”#include "ncarray/ncarrays.hh" // Will provide access to all NCArray*#include "ncarray/soarrays.hh" // If SOArray* arrays wanted instead
// For GPU arrays, use:#include "ncarray/ncdevarrays.cuh"#include "ncarray/sodevarrays.cuh"
#ifdef _WIN32#include <BaseTsd.h>typedef SSIZE_T ssize_t;#else#include <sys/types.h>#endif
#include <cstdint>#include <vector>
// Make some dummy non-contiguous datastd::vector<std::int32_t> data(10, 42);std::vector<ssize_t> shape { 2, 5 };std::vector<ssize_t> strides { 5 * sizeof(std::int32_t), sizeof(std::int32_t) };std::vector<void*> ptrs { data.data() };
// Create a view from this datastd::vector<ssize_t> offsets(10);ncarray::DType dtype = ncarray::DType::int32;// You can get the dtype from traits too:// ncarray::dtype_traits<std::int32_t>::value;auto view = ncarray::NCArrayView(ptrs.data(), shape.data(), strides.data(), offsets.data(), dtype, /*pointer_axis=*/0, /*read_only=*/false);// NOTE: You can make a reference type, this tends to be of particular use// in Python// ncarray::NCArrayRef ref(ptrs, shape, strides, ncarray::DType::int32, 0, false);
// Can retrieve general information about the arrayssize_t ndim = view.ndim(); // 2ssize_t* arr_shape = view.shape(); // ssize_t* with 2 items. (2, 5)ssize_t arr_size = view.size(); // 10ssize_t arr_itemsize = view.itemsize(); // 4 (bytes)ssize_t arr_nbytes = view.nbytes(); // 40 (size() * itemsize())from typing import List, Tuple
import numpy as npimport numpy.typing as npt
import ncarray as nca
# Construct a disjoint set of arrays# These are subarrays that are really part of 1 larger onesubarray_list: List[npt.NDArray[np.uint32]] = [ np.random.randint(1, 255, size=(512,1024), dtype=np.uint16) for _ in range(10)]
# Create a wrapped reference -- we can treat all of these as 1 array nowncarr: nca.NCArrayRef = nca.NCArrayRef(subarray_list)
# Can retrieve general information about the arrayndim: int = ncarr.ndim() # 3arr_shape: Tuple[int, int, int] = ncarr.shapearr_size: int = ncarr.size() # 5242880arr_itemsize: int = ncarr.itemsize() # 2arr_nbytes: int = ncarr.nbytes() # 10485760Array Indexing
Section titled “Array Indexing”Regardless of the presence (or absence) of pointer axes, their number, and location, all array kinds can be indexed into sub-views, or all the way down to a scalar. In C++ there are a number of APIs for indexing depending on whether you are selecting a sub-view of an array, or are indexing all the way down to a reference of a single array element. These are illustrated in the example below. In Python, these are all collapsed into the single standard multi-dimensional indexing, as also shown below.
// We assume we have an array called `view` from some previous step
// ---------------------- Indexing to sub array views ------------------------// For host-only code, we can use the variadic multi-argument operator[] to index subviews// The `Slice` object allows indexing between a start, and stop index of an axis (optionally with a step)using sl = ncarray::Slice; // Slice(start, stop, step); -- The slice is not inclusive of the stop valueauto subview = view[sl(0, 1), sl(2, 4), sl(3, 6, 2)];
// The multi-argument operator() has equivalent semanticsauto subview2 = view(sl(0, 1), sl(2, 4), sl(3, 6, 2));
// Can use Ellipsis to represent all intermediate axesusing elp = ncarray::Ellipsis;// The subview contains all of axis 0, axis 1 and the slice of axis 2auto subview3 = view[elp{}, sl(2, 4)];
// ---------------------- Indexing to Proxy Reference ------------------------// We can use initializer lists to index to a proxy reference// This is NOT bounds checked -- its the callers responsibility to verify this// The PROXY is not the reference yetauto proxy123_0 = view[ { 1, 2, 3 } ];// We can coerce this to the referencestd::int32_t& item123_0 = proxy123_0;std::int32_t& item123_1 = proxy123_0.get<std::int32_t>(); // This type checks in DEBUG builds.// Alternatively, can do this in one step if the proxy is not needed.std::int32_t& item123_2 = view[ { 1, 2, 3 } ];
// The proxy objects can be used for assignment as wellproxy123_0 = 12;// Can alternatively do this directly as wellview[ { 1, 2, 3 } ] = 13;
// Finally, can use `StaticCoords` objects// These are templated on dimensionality and integer type// - The second template argument allows using a narrower int if width is a concernncarray::StaticCoords<3, ssize_t> coords;coords[0] = 1;coords[1] = 2;coords[2] = 3;proxy123_1 = view[coords];std::int32_t& item123_3 = proxy123_1;std::int32_t& item123_4 = view[coords];# We assume we have an array called `view` (NCArrayView) from some previous step
# ----------------------- Indexing to sub array views ------------------------# Can select some subview -- Familiar Python/NumPy indexing and slicing appliessubview: nca.NCArrayView = view[:1, 2:4, 3:6:2]
# Ellipsis work as wellsubview2: nca.NCArrayView = view[..., 2:4]
# ---------------------- Indexing fully down to scalar -----------------------# There's nothing special here, the same indexing as aboveitem123: int = view[1, 2, 3];Array Reductions
Section titled “Array Reductions”All array variants, whether managing CPU or GPU memory, expose reduction APIs. These are host-side APIs even if managing GPU memory in which case they launch kernels. There are two variants
- Full reductions to scalar - will return a scalar.
- Axis-aware reductions: Perform the operation only along the specified axes - will return a new array.
// We assume we have an array called `arr` from some previous step
// ---------------------- Full reductions to a scalar ------------------------
// Can get the sum, max, argmax, argmin, std, var, any and allauto a_sum = arr.sum();auto a_max = arr.max();auto a_min = arr.min();
// These return linearized, ravel, indices// I.e., a single value calculated assuming a row-major traversalauto a_argmax = arr.argmax();auto a_argmin = arr.argmin();
// The variance and standard dev can be supplied a ddof - delta degrees of freedomauto a_var = arr.var(0);auto a_std = arr.std(0);
// These return true if all elements are truthy, or if any element is, respectivelyauto a_all = arr.all();auto a_any = arr.any();
// ------------------------- Axis-aware reductions ---------------------------
// We can alternatively provide a vector of axes to perform the reduction along// These overloads return new arrays
// Create a new array and initialize with: [[1, 2, 3],// [4, 5, 6]]// Will create on GPU -- but could do the same with host memorystd::vector<ssize_t> shape { 2, 3 };ncarray::NCDevArray d_arr(shape, ncarray::dtype_traits<std::int32_t>::value);d_arr = arr.iota() + 1;
// Along axis 0auto d_sum0 = d_arr.sum({ 0 }); // Sum: [5, 7, 9]auto d_max0 = d_arr.max({ 0 }); // Max: [4, 5, 6]auto d_min0 = d_arr.min({ 0 }); // Min: [1, 2, 3]
// Along axis 1auto d_argmax1 = d_arr.argmax({ 1 });auto d_argmin1 = d_arr.argmin({ 1 });
// Still can provide ddof -- Its the second argumentauto d_var1 = d_arr.var({ 1 }, 0);auto d_std1 = d_arr.std({ 1 }, 0);
// Any and all also workauto d_all1 = d_arr.all({ 1 });auto d_any1 = d_arr.any({ 1 });# We assume we have an array called `arr` (NCArrayView) from some previous step
# ----------------------- Full reductions to a scalar ------------------------# Assuming the DType is an integer varietya_sum: int = arr.sum()a_max: int = arr.max()a_min: int = arr.min()
# These return linearized, ravel, indices# I.e., a single value calculated assuming a row-major traversala_argmax: int = arr.argmax()a_argmin: int = arr.argmin()
# The variance and standard dev can be supplied a ddof - delta degrees of freedoma_var: int = arr.var(ddof=0)a_std: int = arr.std(ddof=0)
# These return true if all elements are truthy, or if any element is, respectivelya_all: bool = arr.all();a_any: bool = arr.any();
# ------------------------- Axis-aware reductions ---------------------------
# We can alternatively provide a vector of axes to perform the reduction along# These overloads return new arrays
# Assume that our array contains: [[1, 2, 3],# [4, 5, 6]]# Built over GPU memory: d_arr: nca.NCDevArray(shape=[2,3], dtype=nca.DType.int32)
# Along axis 0d_sum0: nca.NCDevArrayView = d_arr.sum(axis=[0]) # Sum: [5, 7, 9]d_max0: nca.NCDevArrayView = d_arr.max(axis=[0]) # Max: [4, 5, 6]d_min0: nca.NCDevArrayView = d_arr.min(axis=[0]) # Min: [1, 2, 3]
# Along axis 1d_argmax1: nca.NCDevArrayView = d_arr.argmax(axis=[1])d_argmin1: nca.NCDevArrayView = d_arr.argmin(axis=[1])
# Still can provide ddof -- Its the second argumentd_var1: nca.NCDevArrayView = d_arr.var(axis=[1], ddof=0)d_std1: nca.NCDevArrayView = d_arr.std(axis=[1], ddof=0)
# Any and all also workd_all1: nca.NCDevArrayView = d_arr.all(axis=[1])d_any1: nca.NCDevArrayView = d_arr.any(axis=[1])Binary and Unary Operations
Section titled “Binary and Unary Operations”Most binary operations can be performed on pairs of arrays, or between arrays and scalars. Operations are performed in an elementwise fashion, i.e., multiplication does NOT imply matrix multiplication in the linear algebra sense but rather the Hadamard (or Schur) product. Likewise, a number of unary operations, such as negation, can be applied to arrays.
ncarray uses a lazily evaluated expression engine. The result of invoking the operator (e.g. operator+ in C++, or __add__ in Python) is an expression object. These objects are “tapes” of OpCodes with their respective data, references to the views, and a history of all scalar constants. It can be thought of as a stack-based virtual machine evaluated in a reverse Polish notation (RPN) style. (Although internally, the implementation may actually be optimized to avoid the VM/stack). Inplace operations also use the expression engine; however, they are materialized immediately with no return of an expression object.
Importantly, however, in Python, to avoid confusion when first starting, despite creating the expression object, the result will immediately be evaluated (or materialized). This is toggleable, however, either via directly switching the global state, or alternatively by temporarily using a context manager.
// We assume we have two arrays called `arr0` and `arr1` from some previous step
// Can perform all standard operations -- these return EXPRESSION objectsauto sum_expr = arr0 + arr1;auto mul_expr = arr0 * arr1; // This is ELEMENTWISE
// We can get the result be assigning to an arrayncarray::NCArray sum_owner_0 = sum_expr;
// Or by using the constructorncarray::NCArray sum_owner_1(sum_expr);
// Inplace opterations evalute immediately.arr0 += arr1;
// Scalar broadcasting is supportedarr1 /= 4.2f;
// All normal comparison operations are supported - also done ELEMENTWISEauto eq_expr = (arr0 == arr1); // Can use >, <, >=, <=, &&, ||
ncarray::NCArray eq_res = eq_expr;ncarray::NCArray le_res = (arr0 <= arr1);# We assume we have two arrays called `arr0` and `arr1` from some previous step
# Can perform all standard operations# In Python, the expression engine is also used, but by default, we auto-materializesum_res: nca.NCArray = arr0 + arr1mul_res: nca.NCArray = arr0 * arr1 # This is ELEMENTWISE
# We can turn off eager evaluationnca.set_eager(False)
# Now, we get expression objectsdiv_expr: nca.HostExpr = arr0 / arr1
# We can force materialization to get the resultdiv_res: nca.NCArray = nca.materialize(div_expr)
# We can toggle backnca.set_eager(True)
# Using a context manager we can use lazy evaluation only locallywith nca.lazy_mode(): # NOTE: There are NO intermediates created now! lazy_expr: nca.HostExpr = arr0 + arr1 + arr0 * 3 lazy_res: nca.NCArray = nca.materialize(lazy_expr)
# After exiting the manager, eager evaluation is now on again
# All normal comparison operations are supported - also done ELEMENTWISE# These operations are subject to the same eager eval toggle as aboveeq_res: nca.NCArray = (arr0 == arr1) # Will be of DType boolle_res: nca.NCArray = (arr0 <= arr1) # Can use >, <, >=, <=, &&, ||