Shared Memory
Shared memory is a scratchpad memory accessible only by threads within thread block in a kernel. It is useful when several threads in the same block need to reuse the same data or communicate through a fast local data chunk. Typical use cases are, chunked stencil kernels, block-local reductions and scans, transposes, and small reusable data sets loaded once and consumed many times. The amount of shared memory per thread block depends on the device and is usually limited to around 64 KiB, so it is not a good choice for large data sets. In alpaka there are three common ways to declare shared memory:
Declare a single shared value with
declareSharedVar().Declare fixed-size shared multidimensional array or chunk with compile-time known extents with
declareSharedMdArray().And dynamic shared memory with
getDynSharedMem()when the size is only known at launch time.
A Single Shared Value
Not every shared-memory kernel needs a shared data chunk. Sometimes one shared scalar is enough. The next example is a very simple form of a global reduction with atomics. All threads within a thread block accumulate into a shared memory thread block partial result. After all threads in the thread block finished a single thread is accumulating the partial result into the output.
struct BlockSumKernel { ALPAKA_FN_ACC void operator()( onAcc::concepts::Acc auto const& acc, concepts::IMdSpan auto out, concepts::IDataSource auto const& in) const { /* Each shared memory declaration is required to have a unique id. * Use the preprocessor macro `__COUNTER__` or `alpaka::uniqueId()`. */ auto& blockSum = onAcc::declareSharedVar<int, uniqueId()>(acc); // initialize the shared variable for([[maybe_unused]] auto idx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{1u})) blockSum = 0; onAcc::syncBlockThreads(acc); // iterate over the full input data for(auto inputIdx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInGrid, IdxRange{static_cast<uint32_t>(in.getExtents().x())})) { onAcc::atomicAdd(acc, &blockSum, in[inputIdx]); } // wait that all threads wrote there changes onAcc::syncBlockThreads(acc); // A single thread is flushing the data to the output for([[maybe_unused]] auto idx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{1u})) onAcc::atomicAdd(acc, &out[0u], blockSum); } };
This pattern is useful for block-local counters, flags, or partial reductions. The important detail is that the scalar still belongs to the whole thread block, not to a single thread.
Attention
Do not forget to store the return type explicitly as reference, in this case auto&, otherwise you will get a thread local copy instead of the shared one.
Static Shared Memory Array
The next example is showing a chunk-wise permutation of the indices.
For each chunk the id’s should be stored in reverse order into the output.
The frame extent from the kernel launch parameters and the chunk extents are not required to match.
The chunks extent is a CVec and therefore known at compile time, this allows its usage as extents to declare static shared memory.
Static shared memory compared to dynamic shared memory, shown in the next example,
has the benefits that the developer is not required to manage the shared memory chunk by hand and in case it is multidimensional it provides address calculations optimizations.
struct ReverseChunkKernel { ALPAKA_FN_ACC void operator()( onAcc::concepts::Acc auto const& acc, concepts::IMdSpan auto out, concepts::IDataSource auto const& in, concepts::CVector auto chunkExtents) const { /* Each shared memory declaration is required to have a unique id. * Use the preprocessor macro `__COUNTER__` or `alpaka::uniqueId()`. */ auto chunk = onAcc::declareSharedMdArray<int, uniqueId()>(acc, chunkExtents); /* Iterate in chunks over the output data. * It is assumed that the input data have at least the extents of the output. */ for(auto chunkOffset : onAcc::makeIdxMap( acc, onAcc::worker::blocksInGrid, IdxRange{Vec{0u}, static_cast<uint32_t>(out.getExtents().x()), chunkExtents})) { // initialize the shared chunk for(auto idx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{chunkExtents})) chunk[idx] = in[chunkOffset + idx]; onAcc::syncBlockThreads(acc); // each thread is flushing to the output for(auto idx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{chunkExtents})) { auto reverseIdx = Vec{chunkExtents - 1u - idx.x()}; out[chunkOffset + idx] = chunk[reverseIdx]; } // avoid data race with the next loop iteration onAcc::syncBlockThreads(acc); } } };
The “reverse order” work is only there to keep the example small. The same structure is what you would use in more realistic kernels:
Load a small image chunk before applying a blur or stencil.
Stage a matrix chunk before a transpose or matrix multiply step.
Cache a short chunk of data before several neighboring threads reuse it.
Launching a Shared-Memory Kernel
constexpr uint32_t frameExtents = 4u; // Use a larger chunk size than the frame extent to guarantee each thread is calculating at least two values. constexpr uint32_t chunkSize = frameExtents * 2u; auto chunkExtents = CVec<uint32_t, chunkSize>{}; onHost::concepts::FrameSpec auto frameSpec = onHost::FrameSpec{alpaka::divCeil(dataExtent, chunkSize), frameExtents}; queue.enqueue(frameSpec, KernelBundle{ReverseChunkKernel{}, outputBuffer, inputBuffer, chunkExtents});
Dynamic Shared Memory
Dynamic shared memory is useful when the amount of shared memory depends on kernel launch parameters or the kernel arguments.
In alpaka it will be automatically allocated indirectly for each thread block before kernel invocation.
Again the chunk-wise index reverse example is used.
The difference to the example before is that the chunk extent is now a runtime value and to get the shared memory within the kernel onAcc::getDynSharedMem<T>(acc) is used.
You will only get a flat pointer to the allocated data without any information about how many values are valid.
The developer is responsible that the number of allocated bytes at kernel launch time and used within a kernel match.
There are two supported ways to tell alpaka how many bytes to reserve.
Dynamic Size Through a Kernel Member
The most direct option is to give the kernel object a public uint32_t dynSharedMemBytes member.
This works well when the required size is already known when the kernel object is created.
struct DynamicReverseKernel { uint32_t dynSharedMemBytes; ALPAKA_FN_ACC void operator()( onAcc::concepts::Acc auto const& acc, concepts::IMdSpan auto out, concepts::IDataSource auto const& in, uint32_t chunkSize) const { auto* chunk = onAcc::getDynSharedMem<int>(acc); /* Iterate in chunks over the output data. * It is assumed that the input data have at least the extents of the output. */ for(auto chunkOffset : onAcc::makeIdxMap( acc, onAcc::worker::blocksInGrid, IdxRange{Vec{0u}, static_cast<uint32_t>(out.getExtents().x()), chunkSize})) { // initialize the shared chunk for(auto idx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{chunkSize})) chunk[idx.x()] = in[chunkOffset + idx]; onAcc::syncBlockThreads(acc); // each thread is flushing to the output for(auto idx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{chunkSize})) { auto reverseIdx = chunkSize - 1u - idx.x(); out[chunkOffset + idx] = chunk[reverseIdx]; } // avoid data race with the next loop iteration onAcc::syncBlockThreads(acc); } } };
When you launch that kernel, set the byte count in the kernel object itself.
uint32_t frameExtents = 4u; // Use a larger chunk size than the frame extent to guarantee each thread is calculating at least two values. uint32_t chunkSize = frameExtents * 2u; onHost::concepts::FrameSpec auto frameSpec = onHost::FrameSpec{alpaka::divCeil(dataExtent, chunkSize), frameExtents}; queue.enqueue( frameSpec, KernelBundle{ DynamicReverseKernel{static_cast<uint32_t>(chunkSize * sizeof(int))}, outputBuffer, inputBuffer, chunkSize});
This form is simple and readable, but it is intentionally limited: the size can only depend on data you put into the kernel object.
Dynamic Size Through BlockDynSharedMemBytes Trait
When the size should depend on the executor or the kernel arguments, alpaka uses a trait specialization.
If you call a kernel with a frame specification the thread specification in the constructor of the trait will be the derived specification used to launch the kernel.
The user-defined data chunk size passed through the kernel arguments is used to calculate the required amount of shared memory to hold a single chunk per thread block.
The required data to hold a chunk is intended to be independent of the thread specification to control the amount of reused data.
If you provide neither a dynSharedMemBytes member nor a trait implementation alpaka::onHost::trait::BlockDynSharedMemBytes specialization, alpaka reserves no dynamic shared memory for that kernel.
namespace alpaka::onHost::trait { template<concepts::ThreadSpec T_Spec> struct BlockDynSharedMemBytes<DynamicScaleKernel, T_Spec> { BlockDynSharedMemBytes(DynamicScaleKernel const&, T_Spec const& spec) : m_spec(spec) { } uint32_t operator()(auto const& out, auto const& in, int factor, uint32_t chunkSize) const { alpaka::unused(out, in, factor); return static_cast<uint32_t>(chunkSize * sizeof(int)); } private: T_Spec m_spec; }; } // namespace alpaka::onHost::trait
The kernel itself still uses getDynSharedMem in the normal way.
If your kernel provides a member uint32_t dynSharedMemBytes as shown in the previous example the member variable is ignored and the trait specialization is used instead.
struct DynamicScaleKernel { ALPAKA_FN_ACC void operator()( onAcc::concepts::Acc auto const& acc, concepts::IMdSpan auto out, concepts::IDataSource auto const& in, int factor, uint32_t chunkSize) const { auto* cache = onAcc::getDynSharedMem<int>(acc); /* Iterate in chunks over the output data. * It is assumed that the input data have at least the extents of the output. */ for(auto chunkOffset : onAcc::makeIdxMap( acc, onAcc::worker::blocksInGrid, IdxRange{Vec{0u}, static_cast<uint32_t>(out.getExtents().x()), Vec{chunkSize}})) { // initialize the shared chunk for(auto idx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{chunkSize})) cache[idx.x()] = in[chunkOffset + idx] * factor; onAcc::syncBlockThreads(acc); // each thread is flushing to the output for(auto idx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{chunkSize})) out[chunkOffset + idx] = cache[idx.x()]; // avoid data race with the next loop iteration onAcc::syncBlockThreads(acc); } } };
The difference when launching the kernel in comparison to the previous example is that the kernel is not initialized with the byte value and there is an additional chunk size argument.
int factor = 3; uint32_t chunkSize = 8u; onHost::concepts::FrameSpec auto frameSpec = onHost::FrameSpec{1u, chunkSize}; queue.enqueue(frameSpec, KernelBundle{DynamicScaleKernel{}, outputBuffer, inputBuffer, factor, chunkSize});
Practical Advice
Shared memory is local to the thread block. Different blocks cannot see each other’s shared data.
Shared memory is not initialized automatically.
Every thread that reads shared data written by other threads usually needs a block synchronization first.
Reusing the same shared-memory id returns the same storage again; a different id gives you different storage.
use
declareSharedVar()for a single shared scalar or one small fixed object.Use
declareSharedMdArray()multidimensional data.Use
getDynSharedMem()when the temporary size depends on kernel arguments.Start with small chunks and a simple mapping before trying to micro-optimize the memory layout.
Common Mistakes
Treating shared memory as if different blocks could see the same storage.
Reading shared values before a required block synchronization.
Introducing shared memory before checking that the data is reused.
Using dynamic shared memory when a small fixed chunk would already be simpler and clearer.
Complete Source File
120_sharedMemory.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 <cassert>
13#include <vector>
14
15using namespace alpaka;
16
17struct BlockSumKernel
18{
19 ALPAKA_FN_ACC void operator()(
20 onAcc::concepts::Acc auto const& acc,
21 concepts::IMdSpan auto out,
22 concepts::IDataSource auto const& in) const
23 {
24 /* Each shared memory declaration is required to have a unique id.
25 * Use the preprocessor macro `__COUNTER__` or `alpaka::uniqueId()`.
26 */
27 auto& blockSum = onAcc::declareSharedVar<int, uniqueId()>(acc);
28
29 // initialize the shared variable
30 for([[maybe_unused]] auto idx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{1u}))
31 blockSum = 0;
32
33 onAcc::syncBlockThreads(acc);
34
35 // iterate over the full input data
36 for(auto inputIdx :
37 onAcc::makeIdxMap(acc, onAcc::worker::threadsInGrid, IdxRange{static_cast<uint32_t>(in.getExtents().x())}))
38 {
39 onAcc::atomicAdd(acc, &blockSum, in[inputIdx]);
40 }
41
42 // wait that all threads wrote there changes
43 onAcc::syncBlockThreads(acc);
44
45 // A single thread is flushing the data to the output
46 for([[maybe_unused]] auto idx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{1u}))
47 onAcc::atomicAdd(acc, &out[0u], blockSum);
48 }
49};
50
51
52struct ReverseChunkKernel
53{
54 ALPAKA_FN_ACC void operator()(
55 onAcc::concepts::Acc auto const& acc,
56 concepts::IMdSpan auto out,
57 concepts::IDataSource auto const& in,
58 concepts::CVector auto chunkExtents) const
59 {
60 /* Each shared memory declaration is required to have a unique id.
61 * Use the preprocessor macro `__COUNTER__` or `alpaka::uniqueId()`.
62 */
63 auto chunk = onAcc::declareSharedMdArray<int, uniqueId()>(acc, chunkExtents);
64
65 /* Iterate in chunks over the output data.
66 * It is assumed that the input data have at least the extents of the output.
67 */
68 for(auto chunkOffset : onAcc::makeIdxMap(
69 acc,
70 onAcc::worker::blocksInGrid,
71 IdxRange{Vec{0u}, static_cast<uint32_t>(out.getExtents().x()), chunkExtents}))
72 {
73 // initialize the shared chunk
74 for(auto idx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{chunkExtents}))
75 chunk[idx] = in[chunkOffset + idx];
76
77 onAcc::syncBlockThreads(acc);
78
79 // each thread is flushing to the output
80 for(auto idx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{chunkExtents}))
81 {
82 auto reverseIdx = Vec{chunkExtents - 1u - idx.x()};
83 out[chunkOffset + idx] = chunk[reverseIdx];
84 }
85
86 // avoid data race with the next loop iteration
87 onAcc::syncBlockThreads(acc);
88 }
89 }
90};
91
92
93struct DynamicReverseKernel
94{
95 uint32_t dynSharedMemBytes;
96
97 ALPAKA_FN_ACC void operator()(
98 onAcc::concepts::Acc auto const& acc,
99 concepts::IMdSpan auto out,
100 concepts::IDataSource auto const& in,
101 uint32_t chunkSize) const
102 {
103 auto* chunk = onAcc::getDynSharedMem<int>(acc);
104
105 /* Iterate in chunks over the output data.
106 * It is assumed that the input data have at least the extents of the output.
107 */
108 for(auto chunkOffset : onAcc::makeIdxMap(
109 acc,
110 onAcc::worker::blocksInGrid,
111 IdxRange{Vec{0u}, static_cast<uint32_t>(out.getExtents().x()), chunkSize}))
112 {
113 // initialize the shared chunk
114 for(auto idx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{chunkSize}))
115 chunk[idx.x()] = in[chunkOffset + idx];
116
117 onAcc::syncBlockThreads(acc);
118
119 // each thread is flushing to the output
120 for(auto idx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{chunkSize}))
121 {
122 auto reverseIdx = chunkSize - 1u - idx.x();
123 out[chunkOffset + idx] = chunk[reverseIdx];
124 }
125
126 // avoid data race with the next loop iteration
127 onAcc::syncBlockThreads(acc);
128 }
129 }
130};
131
132
133struct DynamicScaleKernel
134{
135 ALPAKA_FN_ACC void operator()(
136 onAcc::concepts::Acc auto const& acc,
137 concepts::IMdSpan auto out,
138 concepts::IDataSource auto const& in,
139 int factor,
140 uint32_t chunkSize) const
141 {
142 auto* cache = onAcc::getDynSharedMem<int>(acc);
143
144 /* Iterate in chunks over the output data.
145 * It is assumed that the input data have at least the extents of the output.
146 */
147 for(auto chunkOffset : onAcc::makeIdxMap(
148 acc,
149 onAcc::worker::blocksInGrid,
150 IdxRange{Vec{0u}, static_cast<uint32_t>(out.getExtents().x()), Vec{chunkSize}}))
151 {
152 // initialize the shared chunk
153 for(auto idx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{chunkSize}))
154 cache[idx.x()] = in[chunkOffset + idx] * factor;
155
156 onAcc::syncBlockThreads(acc);
157
158 // each thread is flushing to the output
159 for(auto idx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{chunkSize}))
160 out[chunkOffset + idx] = cache[idx.x()];
161
162 // avoid data race with the next loop iteration
163 onAcc::syncBlockThreads(acc);
164 }
165 }
166};
167
168
169namespace alpaka::onHost::trait
170{
171 template<concepts::ThreadSpec T_Spec>
172 struct BlockDynSharedMemBytes<DynamicScaleKernel, T_Spec>
173 {
174 BlockDynSharedMemBytes(DynamicScaleKernel const&, T_Spec const& spec) : m_spec(spec)
175 {
176 }
177
178 uint32_t operator()(auto const& out, auto const& in, int factor, uint32_t chunkSize) const
179 {
180 alpaka::unused(out, in, factor);
181 return static_cast<uint32_t>(chunkSize * sizeof(int));
182 }
183
184 private:
185 T_Spec m_spec;
186 };
187} // namespace alpaka::onHost::trait
188
189
190TEMPLATE_LIST_TEST_CASE("tutorial shared memory chunk", "[docs]", docs::test::TestBackends)
191{
192 auto selector = onHost::makeDeviceSelector(TestType::makeDict());
193 if(!selector.isAvailable())
194 return;
195 onHost::concepts::Device auto device = selector.makeDevice(0);
196 onHost::Queue queue = device.makeQueue(queueKind::blocking);
197
198 uint32_t dataExtent = 16u;
199 std::vector<int> hostInput(dataExtent);
200 std::iota(hostInput.begin(), hostInput.end(), 0);
201 std::vector<int> hostOutput(dataExtent);
202
203 auto inputBuffer = onHost::allocLike(device, hostInput);
204 auto outputBuffer = onHost::allocLike(device, hostInput);
205
206 onHost::memcpy(queue, inputBuffer, hostInput);
207
208 constexpr uint32_t frameExtents = 4u;
209 // Use a larger chunk size than the frame extent to guarantee each thread is calculating at least two values.
210 constexpr uint32_t chunkSize = frameExtents * 2u;
211 auto chunkExtents = CVec<uint32_t, chunkSize>{};
212 onHost::concepts::FrameSpec auto frameSpec
213 = onHost::FrameSpec{alpaka::divCeil(dataExtent, chunkSize), frameExtents};
214 queue.enqueue(frameSpec, KernelBundle{ReverseChunkKernel{}, outputBuffer, inputBuffer, chunkExtents});
215
216 onHost::memcpy(queue, hostOutput, outputBuffer);
217 onHost::wait(queue);
218
219 for(size_t i = 0; i < dataExtent; ++i)
220 CHECK(hostOutput[i] == static_cast<int>((i / chunkSize * chunkSize) + (chunkSize - 1 - (i % chunkSize))));
221}
222
223TEMPLATE_LIST_TEST_CASE("tutorial shared memory scalar value", "[docs]", docs::test::TestBackends)
224{
225 auto selector = onHost::makeDeviceSelector(TestType::makeDict());
226 if(!selector.isAvailable())
227 return;
228 onHost::concepts::Device auto device = selector.makeDevice(0);
229 onHost::Queue queue = device.makeQueue(queueKind::blocking);
230
231 uint32_t dataExtent = 16u;
232 std::vector<int> hostInput(dataExtent);
233 std::iota(hostInput.begin(), hostInput.end(), 1);
234
235 auto inputBuffer = onHost::allocLike(device, hostInput);
236 auto outputBuffer = onHost::allocUnified<int>(device, 1);
237 outputBuffer[0] = 0;
238
239 onHost::memcpy(queue, inputBuffer, hostInput);
240
241 constexpr uint32_t chunkSize = 8u;
242 onHost::concepts::FrameSpec auto frameSpec = onHost::FrameSpec{alpaka::divCeil(dataExtent, chunkSize), chunkSize};
243 queue.enqueue(frameSpec, KernelBundle{BlockSumKernel{}, outputBuffer, inputBuffer});
244
245 onHost::wait(queue);
246
247 // Gauss's summation formula
248 CHECK(outputBuffer[0] == static_cast<int>(((dataExtent + 1) * dataExtent) / 2));
249}
250
251TEMPLATE_LIST_TEST_CASE("tutorial dynamic shared memory via member", "[docs]", docs::test::TestBackends)
252{
253 auto selector = onHost::makeDeviceSelector(TestType::makeDict());
254 if(!selector.isAvailable())
255 return;
256 onHost::concepts::Device auto device = selector.makeDevice(0);
257 onHost::Queue queue = device.makeQueue(queueKind::blocking);
258
259 uint32_t dataExtent = 16u;
260 std::vector<int> hostInput(dataExtent);
261 std::iota(hostInput.begin(), hostInput.end(), 0);
262 std::vector<int> hostOutput(dataExtent);
263
264 auto inputBuffer = onHost::allocLike(device, hostInput);
265 auto outputBuffer = onHost::allocLike(device, hostInput);
266
267 onHost::memcpy(queue, inputBuffer, hostInput);
268
269 uint32_t frameExtents = 4u;
270 // Use a larger chunk size than the frame extent to guarantee each thread is calculating at least two values.
271 uint32_t chunkSize = frameExtents * 2u;
272 onHost::concepts::FrameSpec auto frameSpec
273 = onHost::FrameSpec{alpaka::divCeil(dataExtent, chunkSize), frameExtents};
274 queue.enqueue(
275 frameSpec,
276 KernelBundle{
277 DynamicReverseKernel{static_cast<uint32_t>(chunkSize * sizeof(int))},
278 outputBuffer,
279 inputBuffer,
280 chunkSize});
281
282 onHost::memcpy(queue, hostOutput, outputBuffer);
283 onHost::wait(queue);
284
285 for(uint32_t i = 0u; i < dataExtent; ++i)
286 {
287 CHECK(hostOutput[i] == static_cast<int>((i / chunkSize * chunkSize) + (chunkSize - 1 - (i % chunkSize))));
288 }
289}
290
291TEMPLATE_LIST_TEST_CASE("tutorial dynamic shared memory via trait", "[docs]", docs::test::TestBackends)
292{
293 auto selector = onHost::makeDeviceSelector(TestType::makeDict());
294 if(!selector.isAvailable())
295 return;
296 onHost::concepts::Device auto device = selector.makeDevice(0);
297 onHost::Queue queue = device.makeQueue(queueKind::blocking);
298
299 uint32_t dataExtent = 16u;
300 std::vector<int> hostInput(dataExtent);
301 std::iota(hostInput.begin(), hostInput.end(), 0);
302 std::vector<int> hostOutput(dataExtent);
303
304 auto inputBuffer = onHost::alloc<int>(device, dataExtent);
305 auto outputBuffer = onHost::alloc<int>(device, dataExtent);
306
307 onHost::memcpy(queue, inputBuffer, hostInput);
308
309 int factor = 3;
310 uint32_t chunkSize = 8u;
311 onHost::concepts::FrameSpec auto frameSpec = onHost::FrameSpec{1u, chunkSize};
312 queue.enqueue(frameSpec, KernelBundle{DynamicScaleKernel{}, outputBuffer, inputBuffer, factor, chunkSize});
313
314 onHost::memcpy(queue, hostOutput, outputBuffer);
315 onHost::wait(queue);
316
317 for(uint32_t i = 0u; i < dataExtent; ++i)
318 CHECK(hostOutput[i] == static_cast<int>(i) * factor);
319}