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}