Kernel - Parallelism
Once the first kernels from previous examples are working, the next step is to understand how alpaka maps logical work onto frames, blocks, threads, and warps. The important distinction is:
IdxRangedescribes the logical work that must be completed.FrameSpecdescribes the available parallel structure for one launch.makeIdxMapmaps worker threads to valid indices of the logical work.
alpaka kernels can stay data-centric instead of being written around manual global-thread index formulas.
Frames, Blocks, Threads, and Warps
A good mental model is:
Choose the frame extent from the tile shape you want in the data.
Map thread blocks to cover the elements inside a tile/chunk.
Use 1-dimensional warps only when there is a naturally one-dimensional inner direction.
The following kernel uses a small 2D image-style example to show how blocks, threads, and warps relate to one another in practice.
struct ImageTileHierarchyKernel { ALPAKA_FN_ACC void operator()( onAcc::concepts::Acc auto const& acc, concepts::Vector auto const tileExtent, concepts::IDataSource auto const& input, concepts::IMdSpan auto mask, concepts::IMdSpan auto rowCounts, int threshold) const { auto const imageExtent = input.getExtents(); for(auto tileStart : onAcc::makeIdxMap(acc, onAcc::worker::blocksInGrid, IdxRange{Vec{0u, 0u}, imageExtent, tileExtent})) { for(auto localIdx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{tileExtent})) { auto globalIdx = tileStart + localIdx; if(globalIdx[0u] < imageExtent[0u] && globalIdx[1u] < imageExtent[1u]) { mask[globalIdx] = input[globalIdx] >= threshold ? 1u : 0u; } } for(auto warpRow : onAcc::makeIdxMap(acc, onAcc::worker::linearWarpsInBlock, onAcc::range::linearWarpsInBlock)) { auto rowStart = tileStart + Vec{warpRow.x(), 0u}; if(rowStart[0u] >= imageExtent[0u] || warpRow.x() >= tileExtent[0u]) { continue; } for(auto lane : onAcc::makeIdxMap(acc, onAcc::worker::linearThreadsInWarp, onAcc::range::linearThreadsInWarp)) { auto globalIdx = rowStart + Vec{0u, lane.x()}; if(lane.x() < tileExtent[1u] && globalIdx[1u] < imageExtent[1u] && input[globalIdx] >= threshold) { onAcc::atomicAdd(acc, &rowCounts[Vec{rowStart[0u]}], 1u); } } } } } };
The structure is the important part:
onAcc::worker::blocksInGridchooses tile offsets in the full 2D image.onAcc::worker::threadsInBlockiterates the pixels inside one tile.onAcc::worker::linearWarpsInBlockandlinearThreadsInWarpreuse the same tile in a one-dimensional way.
Launching a Hierarchical Kernel
onHost::concepts::FrameSpec auto frameSpec = onHost::FrameSpec{divExZero(imageExtent, tileExtent), tileExtent}; queue.enqueue( frameSpec, KernelBundle{ImageTileHierarchyKernel{}, tileExtent, inputBuffer, maskBuffer, rowCountsBuffer, 5});
Chunked and Tiled Kernels
After the plain element-wise style, the next natural alpaka pattern is a chunked kernel. Here frames stop being just a launch shape and become reusable tiles of work.
struct ChunkedVectorAddKernel { ALPAKA_FN_ACC void operator()( onAcc::concepts::Acc auto const& acc, auto const linearNumFrames, concepts::CVector auto const linearFrameExtent, concepts::IMdSpan auto out, concepts::IDataSource auto const& in0, concepts::IDataSource auto const& in1) const { for(auto linearFrameIdx : onAcc::makeIdxMap(acc, onAcc::worker::linearBlocksInGrid, IdxRange{linearNumFrames})) { auto tile = onAcc::declareSharedMdArray<int, uniqueId()>(acc, linearFrameExtent); for(auto linearFrameElem : onAcc::makeIdxMap(acc, onAcc::worker::linearThreadsInBlock, IdxRange{linearFrameExtent})) { auto globalIdx = linearFrameIdx * linearFrameExtent + linearFrameElem; tile[linearFrameElem] = in0[globalIdx]; } onAcc::syncBlockThreads(acc); for(auto linearFrameElem : onAcc::makeIdxMap(acc, onAcc::worker::linearThreadsInBlock, IdxRange{linearFrameExtent})) { auto globalIdx = linearFrameIdx * linearFrameExtent + linearFrameElem; out[globalIdx] = tile[linearFrameElem] + in1[globalIdx]; } onAcc::syncBlockThreads(acc); } } };
There are a few moving parts in this pattern:
linearBlocksInGridlets blocks iterate over frames.linearThreadsInBlocklets threads iterate over elements inside one frame.constexpr auto frameExtent = CVec<uint32_t, 4u>{}; auto const totalElems = static_cast<uint32_t>(hostOut.size()); auto const frameElementCount = frameExtent.product(); REQUIRE(totalElems % frameElementCount == 0u); auto numFrames = Vec{totalElems / frameElementCount}; onHost::concepts::FrameSpec auto frameSpec = onHost::FrameSpec{numFrames, frameExtent}; queue.enqueue( frameSpec, KernelBundle{ChunkedVectorAddKernel{}, numFrames.product(), frameExtent, outBuffer, in0Buffer, in1Buffer});
Practical Advice
Start with unnested
makeIdxMapwhen the kernel is just “process every element once”.Use hierarchy chunked kernels when there is real data reuse or tiled traversal.
Treat warps as one-dimensional helpers inside a block, not as a replacement for multidimensional mapping.
There are cases where explicit thread or block indices can be useful, for example:
Implementing a very specific CPU/GPU mapping.
Using an algorithm that must reason about exact block-local cooperation.
Porting low-level CUDA/HIP code step by step.
That is not the best starting point for most kernels.
For portable code, prefer using FrameSpec with makeIdxMap.
Once the algorithm is correct and tested, you can move to more specialized mappings if profiling shows that you need them.
Complete Source File
090_kernelParallelism.cpp
1/* Copyright 2026 René Widera
2 * SPDX-License-Identifier: ISC
3 */
4
5#include "docsTest.hpp"
6
7#include <alpaka/alpaka.hpp>
8
9#include <catch2/catch_template_test_macros.hpp>
10#include <catch2/catch_test_macros.hpp>
11
12#include <array>
13
14using namespace alpaka;
15
16struct ImageTileHierarchyKernel
17{
18 ALPAKA_FN_ACC void operator()(
19 onAcc::concepts::Acc auto const& acc,
20 concepts::Vector auto const tileExtent,
21 concepts::IDataSource auto const& input,
22 concepts::IMdSpan auto mask,
23 concepts::IMdSpan auto rowCounts,
24 int threshold) const
25 {
26 auto const imageExtent = input.getExtents();
27
28 for(auto tileStart :
29 onAcc::makeIdxMap(acc, onAcc::worker::blocksInGrid, IdxRange{Vec{0u, 0u}, imageExtent, tileExtent}))
30 {
31 for(auto localIdx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{tileExtent}))
32 {
33 auto globalIdx = tileStart + localIdx;
34 if(globalIdx[0u] < imageExtent[0u] && globalIdx[1u] < imageExtent[1u])
35 {
36 mask[globalIdx] = input[globalIdx] >= threshold ? 1u : 0u;
37 }
38 }
39
40 for(auto warpRow :
41 onAcc::makeIdxMap(acc, onAcc::worker::linearWarpsInBlock, onAcc::range::linearWarpsInBlock))
42 {
43 auto rowStart = tileStart + Vec{warpRow.x(), 0u};
44 if(rowStart[0u] >= imageExtent[0u] || warpRow.x() >= tileExtent[0u])
45 {
46 continue;
47 }
48
49 for(auto lane :
50 onAcc::makeIdxMap(acc, onAcc::worker::linearThreadsInWarp, onAcc::range::linearThreadsInWarp))
51 {
52 auto globalIdx = rowStart + Vec{0u, lane.x()};
53 if(lane.x() < tileExtent[1u] && globalIdx[1u] < imageExtent[1u] && input[globalIdx] >= threshold)
54 {
55 onAcc::atomicAdd(acc, &rowCounts[Vec{rowStart[0u]}], 1u);
56 }
57 }
58 }
59 }
60 }
61};
62
63
64TEMPLATE_LIST_TEST_CASE("tutorial hierarchy blocks threads warps", "[docs]", docs::test::TestBackends)
65{
66 auto selector = onHost::makeDeviceSelector(TestType::makeDict());
67 if(!selector.isAvailable())
68 return;
69 onHost::concepts::Device auto device = selector.makeDevice(0);
70 onHost::Queue queue = device.makeQueue(queueKind::blocking);
71
72 auto const warpSize = device.getDeviceProperties().warpSize;
73 auto const imageExtent = Vec{4u, 2u * warpSize};
74 auto const tileExtent = Vec{1u, warpSize};
75
76 auto hostInput = onHost::allocHost<int>(imageExtent);
77 auto hostMask = onHost::allocHost<uint32_t>(imageExtent);
78 auto hostRowCounts = onHost::allocHost<uint32_t>(Vec{imageExtent[0u]});
79
80 for(auto idx : IdxRange{imageExtent})
81 {
82 if(idx[0u] == 0u)
83 {
84 hostInput[idx] = 10;
85 }
86 else if(idx[0u] == 1u)
87 {
88 hostInput[idx] = idx[1u] < warpSize ? 0 : 10;
89 }
90 else if(idx[0u] == 2u)
91 {
92 hostInput[idx] = (idx[1u] % 2u == 0u) ? 10 : 0;
93 }
94 else
95 {
96 hostInput[idx] = 0;
97 }
98 }
99
100 auto inputBuffer = onHost::allocLike(device, hostInput);
101 auto maskBuffer = onHost::allocLike(device, hostMask);
102 auto rowCountsBuffer = onHost::allocLike(device, hostRowCounts);
103
104 onHost::memcpy(queue, inputBuffer, hostInput);
105 onHost::fill(queue, rowCountsBuffer, 0u);
106
107 onHost::concepts::FrameSpec auto frameSpec = onHost::FrameSpec{divExZero(imageExtent, tileExtent), tileExtent};
108 queue.enqueue(
109 frameSpec,
110 KernelBundle{ImageTileHierarchyKernel{}, tileExtent, inputBuffer, maskBuffer, rowCountsBuffer, 5});
111
112 onHost::memcpy(queue, hostMask, maskBuffer);
113 onHost::memcpy(queue, hostRowCounts, rowCountsBuffer);
114 onHost::wait(queue);
115
116 for(auto idx : IdxRange{imageExtent})
117 {
118 if(idx[0u] == 0u)
119 {
120 CHECK(hostMask[idx] == 1u);
121 }
122 else if(idx[0u] == 1u)
123 {
124 CHECK(hostMask[idx] == (idx[1u] < warpSize ? 0u : 1u));
125 }
126 else if(idx[0u] == 2u)
127 {
128 CHECK(hostMask[idx] == (idx[1u] % 2u == 0u ? 1u : 0u));
129 }
130 else
131 {
132 CHECK(hostMask[idx] == 0u);
133 }
134 }
135
136 CHECK(hostRowCounts[0u] == 2u * warpSize);
137 CHECK(hostRowCounts[1u] == warpSize);
138 CHECK(hostRowCounts[2u] == warpSize);
139 CHECK(hostRowCounts[3u] == 0u);
140}
141
142struct ChunkedVectorAddKernel
143{
144 ALPAKA_FN_ACC void operator()(
145 onAcc::concepts::Acc auto const& acc,
146 auto const linearNumFrames,
147 concepts::CVector auto const linearFrameExtent,
148 concepts::IMdSpan auto out,
149 concepts::IDataSource auto const& in0,
150 concepts::IDataSource auto const& in1) const
151 {
152 for(auto linearFrameIdx : onAcc::makeIdxMap(acc, onAcc::worker::linearBlocksInGrid, IdxRange{linearNumFrames}))
153 {
154 auto tile = onAcc::declareSharedMdArray<int, uniqueId()>(acc, linearFrameExtent);
155
156 for(auto linearFrameElem :
157 onAcc::makeIdxMap(acc, onAcc::worker::linearThreadsInBlock, IdxRange{linearFrameExtent}))
158 {
159 auto globalIdx = linearFrameIdx * linearFrameExtent + linearFrameElem;
160 tile[linearFrameElem] = in0[globalIdx];
161 }
162
163 onAcc::syncBlockThreads(acc);
164
165 for(auto linearFrameElem :
166 onAcc::makeIdxMap(acc, onAcc::worker::linearThreadsInBlock, IdxRange{linearFrameExtent}))
167 {
168 auto globalIdx = linearFrameIdx * linearFrameExtent + linearFrameElem;
169 out[globalIdx] = tile[linearFrameElem] + in1[globalIdx];
170 }
171
172 onAcc::syncBlockThreads(acc);
173 }
174 }
175};
176
177
178TEMPLATE_LIST_TEST_CASE("tutorial chunked frames kernel", "[docs]", docs::test::TestBackends)
179{
180 auto selector = onHost::makeDeviceSelector(TestType::makeDict());
181 if(!selector.isAvailable())
182 return;
183 onHost::concepts::Device auto device = selector.makeDevice(0);
184 onHost::Queue queue = device.makeQueue(queueKind::blocking);
185
186 std::array<int, 8u> hostIn0{0, 1, 2, 3, 4, 5, 6, 7};
187 std::array<int, 8u> hostIn1{10, 10, 10, 10, 10, 10, 10, 10};
188 std::array<int, 8u> hostOut{};
189
190 auto in0Buffer = onHost::allocLike(device, hostIn0);
191 auto in1Buffer = onHost::allocLike(device, hostIn1);
192 auto outBuffer = onHost::allocLike(device, hostIn0);
193
194 onHost::memcpy(queue, in0Buffer, hostIn0);
195 onHost::memcpy(queue, in1Buffer, hostIn1);
196
197 constexpr auto frameExtent = CVec<uint32_t, 4u>{};
198 auto const totalElems = static_cast<uint32_t>(hostOut.size());
199 auto const frameElementCount = frameExtent.product();
200 REQUIRE(totalElems % frameElementCount == 0u);
201 auto numFrames = Vec{totalElems / frameElementCount};
202 onHost::concepts::FrameSpec auto frameSpec = onHost::FrameSpec{numFrames, frameExtent};
203
204 queue.enqueue(
205 frameSpec,
206 KernelBundle{ChunkedVectorAddKernel{}, numFrames.product(), frameExtent, outBuffer, in0Buffer, in1Buffer});
207
208 onHost::memcpy(queue, hostOut, outBuffer);
209 onHost::wait(queue);
210
211 CHECK(hostOut[0] == 10);
212 CHECK(hostOut[1] == 11);
213 CHECK(hostOut[2] == 12);
214 CHECK(hostOut[3] == 13);
215 CHECK(hostOut[4] == 14);
216 CHECK(hostOut[5] == 15);
217 CHECK(hostOut[6] == 16);
218 CHECK(hostOut[7] == 17);
219}