Synchronization

onAcc::syncBlockThreads is the basic in-kernel synchronization primitive. It is a thread block barrier: every participating thread in the same thread block waits until the others reach the same point, and only then do they continue.

This is the primitive you need when one phase of a kernel produces data that a later phase in the same block will consume. The most common reason is block-local reuse, such as loading a chunk/tile once and then letting other threads read from it.

What syncBlockThreads Means

  • It synchronizes threads inside a thread block.

  • It does not synchronize different blocks with one another.

  • It is a thread barrier, not just a memory-ordering hint.

  • It is usually paired with shared memory, but the important concept is the collective phase boundary between “write” and “read”.

  • It must be guaranteed that every thread in the block reaches the same barrier, otherwise the behavior is undefined.

If you only need memory ordering without a blocking thread barrier, check Memory Fences.

Chunk Reuse Example

The next kernel loads one data chunk into thread block-local shared memory, synchronizes the block, and then lets every thread read its own value together with the next neighbor. The key idea is the reuse pattern: one phase fills the chunk, the next phase consumes it. Without the synchronization point, those neighbor reads could race with threads that are still writing the chunk.

struct NeighborReuseKernel
{
    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
    {
        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}))
        {
            // fill the shared data chunk
            for(auto idx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{chunkExtents}))
                chunk[idx] = in[chunkOffset + idx];

            onAcc::syncBlockThreads(acc);

            for(auto idx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{chunkExtents}))
            {
                auto neighborIdx = (idx.x() + 1u) % chunkExtents;
                out[chunkOffset + idx] = chunk[idx] + chunk[neighborIdx];
            }

            // avoid data race with the next loop iteration
            onAcc::syncBlockThreads(acc);
        }
    }
};

Practical Advice

  • Place the barrier after the last write that other threads must see and before the first dependent read.

  • Use synchronization to separate phases, not to “be safe” everywhere, else you will get a very slow kernel.

  • If one thread block reuses the same shared memory again, e.g. in loops, add another barrier before overwriting data that other threads may still read.

Complete Source File

110_synchronization.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 <numeric>
13#include <vector>
14
15using namespace alpaka;
16
17struct NeighborReuseKernel
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,
23        concepts::CVector auto chunkExtents) const
24    {
25        auto chunk = onAcc::declareSharedMdArray<int, uniqueId()>(acc, chunkExtents);
26
27        /* Iterate in chunks over the output data.
28         * It is assumed that the input data have at least the extents of the output.
29         */
30        for(auto chunkOffset : onAcc::makeIdxMap(
31                acc,
32                onAcc::worker::blocksInGrid,
33                IdxRange{Vec{0u}, static_cast<uint32_t>(out.getExtents().x()), chunkExtents}))
34        {
35            // fill the shared data chunk
36            for(auto idx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{chunkExtents}))
37                chunk[idx] = in[chunkOffset + idx];
38
39            onAcc::syncBlockThreads(acc);
40
41            for(auto idx : onAcc::makeIdxMap(acc, onAcc::worker::threadsInBlock, IdxRange{chunkExtents}))
42            {
43                auto neighborIdx = (idx.x() + 1u) % chunkExtents;
44                out[chunkOffset + idx] = chunk[idx] + chunk[neighborIdx];
45            }
46
47            // avoid data race with the next loop iteration
48            onAcc::syncBlockThreads(acc);
49        }
50    }
51};
52
53
54TEMPLATE_LIST_TEST_CASE("tutorial in-kernel synchronization", "[docs]", docs::test::TestBackends)
55{
56    auto selector = onHost::makeDeviceSelector(TestType::makeDict());
57    if(!selector.isAvailable())
58        return;
59    onHost::concepts::Device auto device = selector.makeDevice(0);
60    onHost::Queue queue = device.makeQueue(queueKind::blocking);
61
62    uint32_t dataExtent = 16u;
63    std::vector<int> hostInput(dataExtent);
64    std::iota(hostInput.begin(), hostInput.end(), 0);
65    std::vector<int> hostOutput(dataExtent);
66
67    auto inputBuffer = onHost::allocLike(device, hostInput);
68    auto outputBuffer = onHost::allocLike(device, hostInput);
69
70    onHost::memcpy(queue, inputBuffer, hostInput);
71
72    constexpr uint32_t chunkSize = 8u;
73    auto chunkExtents = CVec<uint32_t, chunkSize>{};
74    onHost::concepts::FrameSpec auto frameSpec
75        = onHost::FrameSpec{alpaka::divCeil(dataExtent, chunkSize), chunkExtents};
76    queue.enqueue(frameSpec, KernelBundle{NeighborReuseKernel{}, outputBuffer, inputBuffer, chunkExtents});
77
78    onHost::memcpy(queue, hostOutput, outputBuffer);
79    onHost::wait(queue);
80
81    for(size_t i = 0; i < dataExtent; ++i)
82    {
83        uint32_t blockOffset = i / chunkSize * chunkSize;
84        CHECK(hostOutput[i] == static_cast<int>(blockOffset + i + ((i + 1) % chunkSize)));
85    }
86}