Atomics

Atomics are the tool you reach for when several workers may update the same memory location. Use an atomic operation only when two or more workers can hit the same location at the same time. Atomics have performance disadvantages, so they should be used only when necessary. Typical examples are histograms, counters, sparse assembly, work lists, and some reductions.

Histogram

Histograms are a typical example because many input elements may contribute to the same bin. That means a direct bins[bin] += 1 would create a data race.

struct HistogramKernel
{
    ALPAKA_FN_ACC void operator()(
        onAcc::concepts::Acc auto const& acc,
        concepts::IDataSource auto const& input,
        concepts::IMdSpan auto bins) const
    {
        for(auto i : onAcc::makeIdxMap(acc, onAcc::worker::threadsInGrid, IdxRange{input.getExtents()}))
        {
            auto const bin = input[i];
            onAcc::atomicAdd(acc, &bins[bin], 1u, onAcc::scope::device);
        }
    }
};

Launching the Atomic Kernel

onHost::concepts::FrameSpec auto frameSpec
    = onHost::FrameSpec{divExZero(static_cast<uint32_t>(hostInput.size()), 64u), 64u};
queue.enqueue(frameSpec, KernelBundle{HistogramKernel{}, inputBuffer, binsBuffer});

Common problems where atomics are useful:

  • accumulate a global count,

  • build a histogram,

  • count errors or events,

  • update a minimum or maximum,

  • or implement a simple unordered reduction.

Atomic Scope

The atomic helpers in alpaka have an optional scope parameter. In practice, the shape looks like this:

  • onAcc::atomicAdd(acc, ptr, value)

  • onAcc::atomicAdd(acc, ptr, value, onAcc::scope::block)

  • onAcc::atomicAdd(acc, ptr, value, onAcc::scope::device)

If you do not pass a scope, the default is onAcc::scope::device.

This is useful because not every algorithm needs the same visibility:

  • scope::block means the atomic operation only needs to be coherent for threads in the same block.

  • scope::device means the atomic operation must work across the whole device.

Many algorithms only need block-local coordination. Even on device global memory using the block scope can be enough if the algorithm guarantees that only threads of the same block can update the same data chunk. If you are accumulating into shared memory or another block-private structure, block scope expresses the real requirement more precisely. If all blocks may update the same global counter, histogram bin, or reduction output, you need device scope.

alpaka provides operations such as atomicAdd, atomicMin, atomicMax, atomicExch, and atomicCas. You can find a complete list of all available atomic operations in the Doxygen documentation.

Common Mistakes

  • Choosing device scope when block-local scope would be enough.

  • Mixing atomics and non-atomic accesses to the same location without a clear code structure. This leads to data races. Try to reuse common and well-known software pattern and idioms.

  • Trying to use atomics as a substitute to synchronize threads.

Complete Source File

150_atomics.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 HistogramKernel
17{
18    ALPAKA_FN_ACC void operator()(
19        onAcc::concepts::Acc auto const& acc,
20        concepts::IDataSource auto const& input,
21        concepts::IMdSpan auto bins) const
22    {
23        for(auto i : onAcc::makeIdxMap(acc, onAcc::worker::threadsInGrid, IdxRange{input.getExtents()}))
24        {
25            auto const bin = input[i];
26            onAcc::atomicAdd(acc, &bins[bin], 1u, onAcc::scope::device);
27        }
28    }
29};
30
31
32TEMPLATE_LIST_TEST_CASE("tutorial atomics histogram", "[docs]", docs::test::TestBackends)
33{
34    auto selector = onHost::makeDeviceSelector(TestType::makeDict());
35    if(!selector.isAvailable())
36        return;
37    onHost::concepts::Device auto device = selector.makeDevice(0);
38    onHost::Queue queue = device.makeQueue();
39
40    std::array<uint32_t, 12u> hostInput{0u, 1u, 0u, 2u, 3u, 0u, 1u, 2u, 2u, 3u, 3u, 3u};
41    std::array<uint32_t, 4u> hostBins{};
42
43    auto inputBuffer = onHost::allocLike(device, hostInput);
44    auto binsBuffer = onHost::alloc<uint32_t>(device, Vec{4u});
45
46    onHost::memcpy(queue, inputBuffer, hostInput);
47    onHost::memset(queue, binsBuffer, 0x00);
48
49    onHost::concepts::FrameSpec auto frameSpec
50        = onHost::FrameSpec{divExZero(static_cast<uint32_t>(hostInput.size()), 64u), 64u};
51    queue.enqueue(frameSpec, KernelBundle{HistogramKernel{}, inputBuffer, binsBuffer});
52
53    onHost::memcpy(queue, hostBins, binsBuffer);
54    onHost::wait(queue);
55
56    CHECK(hostBins[0] == 3u);
57    CHECK(hostBins[1] == 2u);
58    CHECK(hostBins[2] == 3u);
59    CHECK(hostBins[3] == 4u);
60}