Vector

Before allocating memory or launching a kernel, it is worth getting comfortable with Vec. alpaka uses vectors for extents, indices, and frame shapes, so understanding the basic access rules early avoids many beginner mistakes later. A vector is designed for integral values, but using a vector with floating-point numbers also works.

The elements of the first four dimensions can be accessed either via an index (e.g., vec[0]) or via a named component (e.g., vec.x()). The order of the components is w/z/y/x. This is the reverse order compared to CUDA or HIP, where x/y/z is used.

The mapping of the index position [1] to the named component is as follows:

  • alpaka: vec.w() -> vec[0]; vec.z() -> vec[1]; vec.y() -> vec[2]; vec.x() -> vec[3];

  • CUDA/HIP: vec.x() -> vec[0]; vec.y() -> vec[1]; vec.z() -> vec[2];

Unlike CUDA/HIP, alpaka vectors are not limited to three dimensions but can have any number of dimensions. If the alpaka vector is smaller than 4D, not all named components are available. For example, a 3D vector does not have the .w() component. If, on the other hand, the vector is larger than 4D, all additional dimensions can only be accessed via the access operator.

Vec

// create a one-dimensional vector of unsigned int
constexpr auto vec0 = Vec{3u};
// equal to
constexpr auto vec1 = Vec<uint32_t, 1u>{3u};
// a vector is not required to be constexpr
auto vec2 = Vec{3u};

A vector does not implicitly cast the value type except during initialization.

// explicit type conversion from double to unsigned int
constexpr auto vec3 = Vec<uint32_t, 1u>{3.0};
static_assert(std::is_same_v<ALPAKA_TYPEOF(vec0), ALPAKA_TYPEOF(vec3)>);

The dimension of the vector can be queried via dim().

// check the number of components aka dimensions
auto vec4 = Vec<uint32_t, 1u>{42u};
static_assert(vec4.dim() == 1u);

The dimensions in a multidimensional vector can be accessed via named functions or indices.

// create a three-dimensional vector of uint32_t
constexpr auto vec = Vec{5u, 7u, 11u};

// x is the fast moving index in cases where a vector is used to describe an index within an index space
static_assert(vec.x() == 11u);
static_assert(vec[2u] == 11u);

static_assert(vec.y() == 7u);
static_assert(vec[1u] == 7u);

static_assert(vec.z() == 5u);
static_assert(vec[0u] == 5u);

static_assert(vec.dim() == 3u);

Warning

If you are familiar with CUDA/HIP, these named components are well-suited for porting CUDA/HIP algorithms to alpaka. However, be careful when using multidimensional data structures from CUDA/HIP applications in alpaka applications, as the order of the indices is reversed for these named components.

The next example shows how to iterate over a C array where the size is defined by a vector. The slow moving index is the leftmost with the index 0 and the fast moving index is dim() - 1u. This is the same ordering you should keep in mind for alpaka buffers and multidimensional kernels later in the tutorial.

constexpr auto vec = Vec{2u, 3u};
float cArray2D[vec.y()][vec.x()] = {{1., 2., 3.}, {4., 5., 6.}};

for(uint32_t y = 0u; y < vec.y(); ++y)
{
    for(uint32_t x = 0u; x < vec.x(); ++x)
        printf("%f ", cArray2D[y][x]);
    printf("\n");
}

Output:

1.000000 2.000000 3.000000
4.000000 5.000000 6.000000

You can perform typical arithmetic operations on vectors, e.g. +, -, … Operations on a vector work element wise, except for == and !=.

constexpr concepts::CVector auto cvec0 = CVec<uint32_t, 1u, 2u, 3u>{};
constexpr concepts::CVector auto cvec1 = CVec<uint32_t, 13u, 17u, 19u>{};

// performing operations on a compile-time vector will result in a runtime vector type
constexpr concepts::Vector auto vResult = cvec0 + cvec1;
static_assert(vResult.z() == 14u && vResult.y() == 19u && vResult.x() == 22u);

A very useful function is the element permutation called swizzle. It returns a permuted copy of the original vector. The swizzle operator is using CVec, which will be shown later.

constexpr concepts::Vector auto cvec0 = CVec<uint32_t, 3u, 5u, 7u>{};

// permute the vector arguments
constexpr concepts::Vector auto vResult = cvec0[CVec<uint32_t, 1u, 0u, 2u>{}];
// access components via names, order [z,y,x]
static_assert(vResult.z() == 5u && vResult.y() == 3u && vResult.x() == 7u);
// access components via operator[], order [0,1,...,dim-1])
static_assert(vResult[0] == 5u && vResult[1] == 3u && vResult[2] == 7u);

Sometimes it is useful to assign values only to a few components. The next example permutes the initial vector and broadcast-assigns a scalar to all selected components only. Note that you can only assign vectors with the same dimensionality and value type, or scalars those are lossless convertible.

concepts::Vector auto vec0 = Vec{3u, 5u, 7u};

// creates a permuted view to an existing vector to modify only a subset of components
concepts::Vector auto vecView = vec0.ref(CVec<uint32_t, 0u, 2u>{});
static_assert(vecView.dim() == 2u);
CHECK((vecView[0] == 3u && vecView[1] == 7u));
vecView = 42u;
CHECK((vecView[0] == 42u && vecView[1] == 42u));

CHECK((vec0.z() == 42u && vec0.y() == 5u && vec0.x() == 42u));
CHECK((vec0[0] == 42u && vec0[1] == 5u && vec0[2] == 42u));

Since most vector operators work element wise, you need sometimes reduction methods like sum() or product() to accumulate all components to a single scalar value.

constexpr concepts::Vector<size_t> auto vec0 = Vec<size_t, 3u>{3llu, 5llu, 7llu};

// accumulate all elements of a vector
static_assert(vec0.sum() == 15u);
// same as above but supports all std functional operations
static_assert(vec0.reduce(std::plus{}) == 15u);

// All vector operations require that the lhs type is equal to the vector component type.
// This is relaxed if the rhs or lhs of a vector can be up-casted without precision loss, or sign flips, and is a
// scalar value. Operations with two vectors require equal value types. In this example `7u` is up-casted to
// size_t.
constexpr concepts::Vector auto vec1 = vec0 >= 7u;
static_assert(vec1.z() == false && vec1.y() == false && vec1.x() == true);
static_assert(vec1.reduce(std::logical_and{}) == false);

// vector and scalar has the same precision
Vec<uint64_t, 1> vec2 = Vec<uint64_t, 1>{42} * uint64_t{2};
// vector has a higher precession than the scalar
// up-cast possible
Vec<uint64_t, 1> vec3 = Vec<uint64_t, 1>{42} * uint32_t{2};
// vector has a lower precession than the scalar
// does not compile because of precision loss
// Vec<uint32_t, 1> vec4 = Vec<uint32_t, 1>{42} * uint64_t{2};

CVec

Vec is a vector whose component values are stored at runtime, while CVec stores the components in the template signature, which allows compile-time information to be propagated to called functions and used there at compile time. CVec behaves like Vec and follows concepts::Vector but keeps the values available at compile time even if you pass it to a non constexpr function. The next code will show you that you can use static_assert() which would not be possible with Vec.

void foo(concepts::CVector<uint32_t> auto value)
{
    static_assert(value.dim() == 3u);

    // this would fail if the vector values are not known fully at compile time
    static_assert(value.x() == 11u);
    static_assert(value.y() == 7u);
    static_assert(value.z() == 5u);
}
constexpr auto cvec = CVec<uint32_t, 5u, 7u, 11u>{};

// compile-time vectors can be passed as function arguments and preserve compile-time values
foo(cvec);

If you call operators like + on a CVec variable, the result type will be Vec and it will not keep the results compile time available if you pass the result to a function. In this example it can only be validated with static_assert() because the operation is marked constexpr.

constexpr auto cvec0 = CVec<uint32_t, 5u, 7u>{};
constexpr auto cvec1 = CVec<uint32_t, 37u, 35u>{};

constexpr Vec result = cvec0 + cvec1;
static_assert(result[0] == result[1] && result[0] == 42u);

Complete Source File

010_vector.cpp
  1/* Copyright 2025 René Widera
  2 * SPDX-License-Identifier: ISC
  3 */
  4
  5#include <alpaka/alpaka.hpp>
  6
  7#include <catch2/catch_test_macros.hpp>
  8
  9#include <cstdint>
 10#include <functional>
 11
 12/** @file Operations shown here are mostly constexpr to simplify the validation by using static_assert() */
 13
 14using namespace alpaka;
 15
 16TEST_CASE("vector 1D", "[docs]")
 17{
 18    // create a one-dimensional vector of unsigned int
 19    constexpr auto vec0 = Vec{3u};
 20    // equal to
 21    constexpr auto vec1 = Vec<uint32_t, 1u>{3u};
 22    // a vector is not required to be constexpr
 23    auto vec2 = Vec{3u};
 24
 25    static_assert(std::is_same_v<ALPAKA_TYPEOF(vec0), ALPAKA_TYPEOF(vec1)>);
 26    alpaka::unused(vec0, vec1, vec2);
 27
 28    // explicit type conversion from double to unsigned int
 29    constexpr auto vec3 = Vec<uint32_t, 1u>{3.0};
 30    static_assert(std::is_same_v<ALPAKA_TYPEOF(vec0), ALPAKA_TYPEOF(vec3)>);
 31
 32    // check the number of components aka dimensions
 33    auto vec4 = Vec<uint32_t, 1u>{42u};
 34    static_assert(vec4.dim() == 1u);
 35    alpaka::unused(vec3, vec4);
 36}
 37
 38TEST_CASE("vector 3D", "[docs]")
 39{
 40    // create a three-dimensional vector of uint32_t
 41    constexpr auto vec = Vec{5u, 7u, 11u};
 42
 43    // x is the fast moving index in cases where a vector is used to describe an index within an index space
 44    static_assert(vec.x() == 11u);
 45    static_assert(vec[2u] == 11u);
 46
 47    static_assert(vec.y() == 7u);
 48    static_assert(vec[1u] == 7u);
 49
 50    static_assert(vec.z() == 5u);
 51    static_assert(vec[0u] == 5u);
 52
 53    static_assert(vec.dim() == 3u);
 54}
 55
 56TEST_CASE("CArray 2D iterate", "[docs]")
 57{
 58    constexpr auto vec = Vec{2u, 3u};
 59    float cArray2D[vec.y()][vec.x()] = {{1., 2., 3.}, {4., 5., 6.}};
 60
 61    for(uint32_t y = 0u; y < vec.y(); ++y)
 62    {
 63        for(uint32_t x = 0u; x < vec.x(); ++x)
 64            printf("%f ", cArray2D[y][x]);
 65        printf("\n");
 66    }
 67}
 68
 69TEST_CASE("compile-time vector", "[docs]")
 70{
 71    // create a three-dimensional vector of uint32_t
 72    // dimensionality is derived from the number of template parameters
 73    constexpr auto cvec = CVec<uint32_t, 5u, 7u, 11u>{};
 74
 75    // x is the fast moving index in cases where a vector is used to describe an index within an index space
 76    static_assert(cvec.x() == 11u);
 77    static_assert(cvec[2u] == 11u);
 78
 79    static_assert(cvec.y() == 7u);
 80    static_assert(cvec[1u] == 7u);
 81
 82    static_assert(cvec.z() == 5u);
 83    static_assert(cvec[0u] == 5u);
 84}
 85
 86void foo(concepts::CVector<uint32_t> auto value)
 87{
 88    static_assert(value.dim() == 3u);
 89
 90    // this would fail if the vector values are not known fully at compile time
 91    static_assert(value.x() == 11u);
 92    static_assert(value.y() == 7u);
 93    static_assert(value.z() == 5u);
 94}
 95
 96
 97TEST_CASE("compile-time vector as function argument", "[docs]")
 98{
 99    constexpr auto cvec = CVec<uint32_t, 5u, 7u, 11u>{};
100
101    // compile-time vectors can be passed as function arguments and preserve compile-time values
102    foo(cvec);
103}
104
105TEST_CASE("compile-time vector operations", "[docs]")
106{
107    constexpr auto cvec0 = CVec<uint32_t, 5u, 7u>{};
108    constexpr auto cvec1 = CVec<uint32_t, 37u, 35u>{};
109
110    constexpr Vec result = cvec0 + cvec1;
111    static_assert(result[0] == result[1] && result[0] == 42u);
112}
113
114TEST_CASE("compile-time vector calculations", "[docs]")
115{
116    constexpr concepts::CVector auto cvec0 = CVec<uint32_t, 1u, 2u, 3u>{};
117    constexpr concepts::CVector auto cvec1 = CVec<uint32_t, 13u, 17u, 19u>{};
118
119    // performing operations on a compile-time vector will result in a runtime vector type
120    constexpr concepts::Vector auto vResult = cvec0 + cvec1;
121    static_assert(vResult.z() == 14u && vResult.y() == 19u && vResult.x() == 22u);
122}
123
124TEST_CASE("vector swizzle", "[docs]")
125{
126    constexpr concepts::Vector auto cvec0 = CVec<uint32_t, 3u, 5u, 7u>{};
127
128    // permute the vector arguments
129    constexpr concepts::Vector auto vResult = cvec0[CVec<uint32_t, 1u, 0u, 2u>{}];
130    // access components via names, order [z,y,x]
131    static_assert(vResult.z() == 5u && vResult.y() == 3u && vResult.x() == 7u);
132    // access components via operator[], order [0,1,...,dim-1])
133    static_assert(vResult[0] == 5u && vResult[1] == 3u && vResult[2] == 7u);
134}
135
136TEST_CASE("vector ref", "[docs]")
137{
138    concepts::Vector auto vec0 = Vec{3u, 5u, 7u};
139
140    // creates a permuted view to an existing vector to modify only a subset of components
141    concepts::Vector auto vecView = vec0.ref(CVec<uint32_t, 0u, 2u>{});
142    static_assert(vecView.dim() == 2u);
143    CHECK((vecView[0] == 3u && vecView[1] == 7u));
144    vecView = 42u;
145    CHECK((vecView[0] == 42u && vecView[1] == 42u));
146
147    CHECK((vec0.z() == 42u && vec0.y() == 5u && vec0.x() == 42u));
148    CHECK((vec0[0] == 42u && vec0[1] == 5u && vec0[2] == 42u));
149}
150
151TEST_CASE("vector operations", "[docs]")
152{
153    constexpr concepts::Vector<size_t> auto vec0 = Vec<size_t, 3u>{3llu, 5llu, 7llu};
154
155    // accumulate all elements of a vector
156    static_assert(vec0.sum() == 15u);
157    // same as above but supports all std functional operations
158    static_assert(vec0.reduce(std::plus{}) == 15u);
159
160    // All vector operations require that the lhs type is equal to the vector component type.
161    // This is relaxed if the rhs or lhs of a vector can be up-casted without precision loss, or sign flips, and is a
162    // scalar value. Operations with two vectors require equal value types. In this example `7u` is up-casted to
163    // size_t.
164    constexpr concepts::Vector auto vec1 = vec0 >= 7u;
165    static_assert(vec1.z() == false && vec1.y() == false && vec1.x() == true);
166    static_assert(vec1.reduce(std::logical_and{}) == false);
167
168    // vector and scalar has the same precision
169    Vec<uint64_t, 1> vec2 = Vec<uint64_t, 1>{42} * uint64_t{2};
170    // vector has a higher precession than the scalar
171    // up-cast possible
172    Vec<uint64_t, 1> vec3 = Vec<uint64_t, 1>{42} * uint32_t{2};
173    // vector has a lower precession than the scalar
174    // does not compile because of precision loss
175    // Vec<uint32_t, 1> vec4 = Vec<uint32_t, 1>{42} * uint64_t{2};
176    CHECK(vec2 == Vec<uint64_t, 1>{84});
177    CHECK(vec3 == Vec<uint64_t, 1>{84});
178}
179
180TEST_CASE("simd", "[docs]")
181{
182    // Vectors are designed for integral types and index and extent calculations and definitions.
183    constexpr concepts::Vector<size_t> auto vec0 = Vec<size_t, 4u>{3llu, 5llu, 7llu, 11llu};
184    // If vectors are required for user data, you should use Simd instead.
185    // For integral and floating point types. Simd vectors are aligned and typical therefore faster to load from
186    // memory.
187    concepts::Simd auto simd0 = Simd<size_t, 4u>{3llu, 5llu, 7llu, 11llu};
188
189    // compare component wise
190    alpaka::apply([&](auto const&... idx) { CHECK(((vec0[idx] == simd0[idx]) && ...)); }, iotaCVec<int, 4u>());
191}