arXiv is now an independent nonprofit! Learn more
License: arXiv.org perpetual non-exclusive license
arXiv:2603.18695v1 [cs.DC] 19 Mar 2026

High-Performance Portable GPU Primitives for Arbitrary Types and Operators in Julia

Emmanuel Pilliat Affiliation: Univ Rennes, Ensai, CNRS, CREST—UMR 9194, F-35000
Rennes, France
emmanuel.pilliat@ensai.fr
Abstract

Portable GPU frameworks such as Kokkos and RAJA reduce the burden of cross-architecture development but typically incur measurable overhead on fundamental parallel primitives relative to vendor-optimized libraries. We present KernelForge.jl, a Julia library that implements scan, mapreduce, and matrix–vector primitives through a two-layer portable architecture: KernelIntrinsics.jl provides backend-agnostic abstractions for warp-level shuffles, memory fences, and vectorized memory access, while KernelForge.jl builds high-performance algorithms exclusively on top of these interfaces. Evaluated on an NVIDIA A40 and an AMD MI300X, KernelForge.jl matches or exceeds CUB kernel execution time on scan and mapreduce on the A40, and matches cuBLAS throughput on matrix–vector operations across most tested configurations—demonstrating, as a proof of concept, that portable JIT-compiled abstractions can achieve vendor-level throughput without sacrificing generality.

Index Terms: 
GPU computing, performance portability, parallel primitives, Julia, scan, mapreduce, matrix–vector products, CUDA, ROCm, JIT compilation.

I Introduction

Modern scientific computing, machine learning, and data analytics increasingly rely on GPU acceleration to achieve necessary performance levels. However, the GPU computing landscape has evolved from a single-vendor dominated ecosystem to a heterogeneous environment with multiple competing architectures. NVIDIA’s CUDA platform, AMD’s ROCm, Intel’s oneAPI, and emerging architectures each offer distinct hardware capabilities and programming models. This diversity creates a fundamental tension: applications optimized for one architecture may fail to compile or perform poorly on others.

The traditional approach of maintaining separate implementations for each vendor’s platform imposes substantial development and maintenance costs. Research groups and software developers face a persistent trade-off: write portable code that may underperform, or maintain multiple specialized implementations that maximize performance but increase complexity. This tension between performance and portability has been extensively documented in the HPC community [1, 2].

Existing performance portability frameworks such as RAJA and Kokkos can incur up to 100% overhead compared to hand-optimized vendor implementations [3, 4] due to suboptimal code generation through abstraction layers. This performance gap is particularly acute for fundamental parallel primitives—like scan, reduction, matrix-vector or vector matrix operations—that serve as building blocks for complex algorithms. These primitives underpin a wide range of workloads, from sparse linear algebra solvers to graph analytics and sorting routines; even modest overhead at this level compounds through higher-level applications. A second limitation of existing frameworks is their reliance on ahead-of-time compilation, which complicates integration with dynamic, high-level languages such as Python and Julia that increasingly dominate scientific computing workflows.

Within the Julia ecosystem specifically, GPU programming has matured rapidly through packages like CUDA.jl [5] and KernelAbstractions.jl [6]. Yet even Julia’s native GPU compiler introduces constant overhead versus vendor implementations due to differences in low-level code generation [7], and portable GPU libraries in Julia have not closed this performance gap for core primitives. Bridging this gap would benefit the growing Julia HPC and machine learning community, which relies on these primitives as foundational infrastructure.

We show that, contrary to common expectation, performance portability need not require either performance compromise or mastery of complex low-level syntax. Through careful design leveraging Julia’s high-level abstractions, metaprogramming capabilities, and just-in-time (JIT) compilation, it is possible to achieve both portability and throughput competitive with hand-optimized vendor libraries.

We present KernelForge.jl [8], a GPU library for Julia that implements fundamental parallel primitives with throughput matching vendor-optimized implementations. KernelForge.jl is architected around three goals: performance, matching vendor-optimized libraries on core primitives; flexibility, supporting arbitrary associative operators and any Bitstype element—including for matrix–vector products, where vendor libraries are restricted to standard numeric arithmetic; and portability, expressing all algorithms through backend-agnostic abstractions with vendor-specific functionality isolated in a thin extension layer. The current implementation targets NVIDIA GPUs via CUDA.jl and AMD GPUs via AMDGPU.jl; extending to Intel oneAPI and other backends requires only providing backend-specific implementations of the low-level intrinsics, with no changes to the algorithmic layer. The principal contributions of this work are:

  • A two-layer architecture in which KernelIntrinsics.jl [9] provides low-level, portable intrinsics for vectorized memory access, configurable memory fences, and warp-level operations on arbitrary types, and KernelForge.jl implements high-performance parallel algorithms exclusively through KernelIntrinsics.jl and KernelAbstractions.jl [6]. Backend-specific functionality is confined to KernelIntrinsics.jl extension modules via Julia’s package extension mechanism and @device_override, ensuring that the algorithmic layer requires no modification when targeting new architectures.

  • An empirical demonstration that JIT-compiled, high-level abstractions can match—and in some cases exceed—the throughput of hand-optimized vendor libraries on scan, reduce, and matrix–vector primitives, across problem sizes ranging from 10610^{6} to 10910^{9} elements, evaluated on an NVIDIA A40 and an AMD MI300X. On the A40, we report kernel-only execution time measured via CUDA Events alongside full pipeline time (memory allocation, host-side dispatch, and kernel execution); on the MI300X, we report full pipeline time only.

  • A comparative evaluation against CUB, CUDA.jl, AcceleratedKernels.jl [10], and Kokkos (scan only), demonstrating that KernelForge.jl matches vendor-optimized performance on scan, reduce, and matrix–vector primitives on the A40, and achieves competitive throughput on the MI300X (vendor-optimized baselines for scan and mapreduce are unavailable on that platform). KernelForge.jl exposes a lightweight tuning mechanism based on Julia’s native multiple dispatch, allowing architecture-specific parameters to be selected at compile time with no changes to the algorithmic layer.

We emphasize that KernelForge.jl is a proof of concept rather than a production-ready library. The current implementation targets NVIDIA GPUs fully; AMD support via AMDGPU.jl is functional but not yet accompanied by vendor-optimized baselines for scan and mapreduce on the MI300X—rocPRIM benchmarks are absent from this evaluation, so MI300X results for those primitives demonstrate portability and correctness rather than substantiating a no-portability-tax claim on that platform. Matrix–vector and vector–matrix primitives are an exception: AMDGPU.jl dispatches LinearAlgebra.mul! to rocblas_gemv internally, providing an effective rocBLAS baseline for those operations. Backends beyond CUDA and ROCm remain untested. The primary contribution is architectural and empirical: demonstrating that the portability–performance trade-off is not fundamental, and that a carefully designed high-level abstraction layer can match vendor-optimized throughput on core parallel primitives.

II Background

II-A GPU Architecture

GPUs execute thousands of threads organized into warps—the atomic unit of execution—and blocks of warps scheduled across Streaming Multiprocessors (SMs) [11]. A memory hierarchy trades capacity for speed: global memory is large but slow, shared memory is block-scoped and fast, and registers are per-thread and fastest. Threads within a warp can exchange data directly via shuffle instructions without touching memory [12]; warps within a block synchronize through shared memory [13]; cross-block coordination requires global memory fences or atomics [14, 15].

Memory access performance depends critically on coalescing—contiguous, aligned warp accesses are serviced in a single transaction—and on issuing wide loads (e.g., 128-bit float4 in CUDA) to maximize bandwidth per transaction [16, 17].

These three mechanisms—warp-level primitives, memory fences, and vectorized memory access—are the low-level capabilities required for vendor-competitive parallel primitives, and form the focus of KernelIntrinsics.jl (Section IV). Peak performance further requires tuning block count and items-per-thread to hardware characteristics such as warp width and L2 cache size that vary across architectures.

II-B Cross-Architecture GPU Programming in Julia

Julia’s GPU ecosystem is built on a layered stack. At the lowest level, vendor-specific packages—CUDA.jl for NVIDIA, AMDGPU.jl for AMD ROCm, oneAPI.jl for Intel, and Metal.jl for Apple—provide full access to each platform’s programming model and vendor libraries. Above this, KernelAbstractions.jl [6] provides a backend-agnostic kernel language: a single kernel definition can be compiled to any supported backend through Julia’s multiple dispatch and JIT compilation.

KernelAbstractions.jl exposes a programming model in which the user defines kernels using the @kernel macro. Kernels are parameterized by a backend (e.g., CUDABackend(), ROCBackend()) and compiled at first invocation for the target device. The framework provides workgroup-level abstractions—including workgroup indices, local indices, and @synchronize barriers—that map to thread blocks, thread-local indices, and __syncthreads() on NVIDIA, with analogous mappings on other backends. Shared memory is allocated via @localmem, which maps to __shared__ memory on CUDA and local data share on AMD.

Listing 1: Copy kernel in KernelAbstractions.jl, from GPUArraysCore.jl. This kernel compiles to NVIDIA PTX, AMD GCN, Intel SPIR-V, or Apple AIR from a single source definition.
1 @kernel function copy_kernel!(dst, src)
2 I = @index(Global)
3 dst[I] = src[I]
4 end

Listing 1 illustrates the simplicity of the programming model. This abstraction covers a substantial portion of the GPU programming surface, enabling portable kernels for many workloads. However, several capabilities essential for vendor-competitive performance on fundamental primitives are not exposed:

  • Warp-level shuffle operations. There is no portable abstraction for shfl_sync, shfl_down_sync, or their equivalents. Kernels requiring register-level data exchange within a warp—critical for fast reductions and scans—must fall back to vendor-specific intrinsics, breaking portability.

  • Ordered memory accesses. Fine-grained memory fences (threadfence(), threadfence_block()) that control the visibility ordering of memory operations across threads, blocks, or the device are not available. These are required for inter-block coordination protocols such as the decoupled look-back used in single-pass scan algorithms [18].

  • Vectorized loads and stores. There is no mechanism to emit 64-bit or 128-bit load/store instructions. Achieving peak memory bandwidth in copy and bandwidth-bound kernels requires this capability.

These gaps motivate our KernelIntrinsics.jl layer (Section IV), which provides portable, backend-dispatched implementations of all three capabilities, enabling KernelForge.jl to build vendor-competitive algorithms without sacrificing portability.

II-C Parallel Primitives

Beyond the copy operation—which serves as the practical bandwidth ceiling against which other primitives are measured (cf. Figure 1)—several fundamental operations form the backbone of GPU computation, serving a role analogous to BLAS [19] in linear algebra. We focus on three: mapreduce, scan, and matrix–vector/vector–matrix products.

The mapreduce operation transforms each element via a mapping function f:TSf:T\to S, then combines all transformed values using a commutative and associative operator op\mathop{\mathrm{op}} (commutativity is required here, in contrast to scan which requires only associativity) to compute op(f(src[1]),f(src[2]),,f(src[n]))\mathop{\mathrm{op}}(f(\texttt{src}[1]),f(\texttt{src}[2]),\ldots,f(\texttt{src}[n])). Efficient GPU reduction proceeds hierarchically: threads first reduce within registers, then within warps using shuffle operations, then across warps within a block using shared memory, and finally across blocks. Implementations leveraging warp-level primitives achieve up to 7.8×7.8\times speedup over tree-based reductions through shared memory alone [20, 12].

The scan (prefix sum) operation relaxes the commutativity requirement, taking an associative operator op\mathop{\mathrm{op}} (not necessarily commutative) and computing a sequence of partial reductions: the output dst[i]=op(src[1],src[2],,src[i])\texttt{dst}[i]=\mathop{\mathrm{op}}(\texttt{src}[1],\texttt{src}[2],\ldots,\texttt{src}[i]) contains the accumulated result up to position ii for all 1in1\leq i\leq n. Scan is among the most important parallel primitives: it enables stream compaction, radix sort, histogram computation, and sparse matrix operations [21, 22]. The state-of-the-art is the single-pass parallel prefix algorithm with decoupled look-back [18], which achieves throughput approaching copy operations by requiring only approximately 2n2n data movement (nn inputs read, nn outputs written) and dissociating local computation from global prefix propagation latencies through strategic redundant work. This protocol requires inter-block communication through global memory with careful use of memory fences and status flags—precisely the capabilities absent from existing portable frameworks – see Section II-B.

Finally, we treat matrix–vector and vector–matrix products as distinct primitives. Given an n×pn\times p matrix AA, a vector xTnx\in T^{n}, a mapping function f:T×TSf:T\times T\to S, and an associative reduction operator op\mathop{\mathrm{op}} over SS, the matrix–vector product computes a vector ySpy\in S^{p} where y[j]=opi=1nf(x[i],A[i,j])y[j]=\mathop{\mathrm{op}}_{i=1}^{n}f(x[i],\,A[i,j]), reducing over the rows of AA. The vector–matrix product symmetrically computes zSnz\in S^{n} where z[i]=opj=1pf(A[i,j],x[j])z[i]=\mathop{\mathrm{op}}_{j=1}^{p}f(A[i,j],\,x[j]), reducing over the columns. Setting f=×f=\times and op=+\mathop{\mathrm{op}}=+ recovers the standard BLAS GEMV operation, but the generalized formulation supports arbitrary algebraic structures—for instance, tropical semirings (where f=+f=+ and op=min\mathop{\mathrm{op}}=\min) used in shortest-path algorithms, or log-space operations for numerical stability. These two operations are not symmetric on GPUs: because arrays are stored in column-major order, matrix–vector products access AA with stride-one (coalesced) reads along columns, while vector–matrix products must read along rows, resulting in strided access patterns. Achieving high throughput for both orientations requires distinct kernel strategies, as we describe in Section V-C.

III Related Work

III-A C++ Performance Portability Frameworks

Performance portability in GPU computing has been addressed through various C++ frameworks, each embodying different design philosophies. RAJA [23] achieves portability by decoupling algorithm implementation from execution strategy through execution policies. Kokkos [24] offers a more encompassing framework that abstracts both execution and memory spaces. Additional frameworks pursuing similar goals include OpenACC, OpenMP, and SYCL. Despite their differences, these frameworks share a fundamental constraint: their dependence on ahead-of-time compilation hinders integration with dynamic languages such as Python and Julia.

Performance studies reveal a consistent pattern: portable frameworks typically incur overhead compared to hand-optimized vendor implementations. Martineau et al. [3] found 5–30% penalties versus architecture-specific code; Artigues et al. [4] reported 2–3×\times slowdowns on NVIDIA V100s compared to native CUDA. Davis et al. [25] confirmed that Kokkos performs moderately worse than CUDA across five proxy applications on V100, A100, and H100 GPUs. Sedova et al. [1] concluded that non-portable optimizations are essential to production molecular dynamics codes. These studies establish the conventional wisdom that a portability tax is unavoidable when using abstraction layers.

III-B Vendor-Optimized Libraries

For 1-D parallel primitives (copy, reduce, scan), NVIDIA’s CUB [26] and Thrust libraries provide highly optimized CUDA implementations, achieving near-peak performance through careful exploitation of warp-level operations, shared memory banking patterns, and instruction-level parallelism. AMD’s rocPRIM and Intel’s oneDPL offer similar functionality for their respective platforms. The existence of multiple vendor-specific libraries with incompatible APIs illustrates the fundamental challenge: achieving peak performance currently requires separate implementations per vendor.

For matrix–vector operations, vendor libraries such as cuBLAS, rocBLAS, and oneMKL provide optimized GEMV routines, but only for standard numeric types with fixed arithmetic (f=×f=\times, =+\oplus=+). Applications requiring non-standard algebraic structures—such as tropical semirings for shortest-path problems or log-space operations for numerical stability—cannot use these libraries and must resort to custom kernels, forfeiting vendor-level optimization. KernelForge.jl’s generalized formulation subsumes standard GEMV while extending it to arbitrary types and operators, without sacrificing performance on the standard numerical case.

III-C Julia GPU Ecosystem

The Julia GPU ecosystem provides comprehensive GPU support through several packages. CUDA.jl offers both high-level array abstractions and low-level kernel programming for NVIDIA GPUs. KernelAbstractions.jl [6] extends portability to multiple backends (CUDA, ROCm, oneAPI, Metal) through unified kernel syntax and backend dispatch. AcceleratedKernels.jl [10] builds atop KernelAbstractions.jl to provide portable implementations of common operations. However, existing Julia GPU libraries prioritize generality over optimization: CUDA.jl’s implementations favor simplicity, while AcceleratedKernels.jl focuses on broad portability rather than matching vendor library performance.

III-D Our Contribution

KernelForge.jl differs from the frameworks above in two respects. First, unlike C++ portability layers that compile ahead of time, it exploits Julia’s JIT compilation to generate code specialized to the concrete data type, operator, and problem size at the call site—eliminating the abstraction overhead that accounts for the 5–30% portability tax reported in existing studies. Second, unlike Julia GPU libraries that prioritize API breadth, KernelForge.jl targets a narrow set of primitives—scan, mapreduce, and matrix–vector products—and optimizes them through a two-layer architecture: KernelIntrinsics.jl confines all vendor-specific functionality (warp shuffles, memory fences, vectorized loads) to a thin extension layer, while the algorithmic layer builds exclusively on these portable abstractions. On the A40, this design matches CUB and cuBLAS throughput on all three primitives; on the MI300X, it delivers competitive performance on mapreduce and scan relative to the available Julia baselines, and matches rocBLAS on matrix–vector operations for wide-matrix configurations.

IV Design of Cross Architecture Kernel Intrinsics

IV-A Shuffle Operations

A shuffle operation within a warp enables a given lane to read a value directly from the register of another lane, as determined by a source index or mask. Vendor implementations, however, define shuffle intrinsics only for a narrow set of types—typically 32-bit primitives such as Float32 and Int32.

CUDA.jl partially addresses this limitation through a recursion mechanism that extends shuffling to types like Int64 or complex numbers, but support for more composite types such as tuples or quaternions remains unavailable.

KernelIntrinsics.jl generalizes this approach by leveraging Julia’s @generated functions to recursively decompose any composite type at compile time. The key observation is that once shuffle intrinsics are defined for a single 32-bit primitive (e.g., UInt32), any composite type can be supported by decomposing it into its constituent primitive fields and shuffling each independently. We formalize this through a recursive definition: a Bitstype is either a concrete primitive of at most 64 bits, a tuple of Bitstypes, or a struct whose fields are all Bitstypes. Using @generated functions, the compiler statically unrolls the recursion over struct fields and tuple elements, producing specialized, zero-overhead code for each concrete type. KernelIntrinsics.jl implements shuffle operations over arbitrary Bitstypes under this definition, fully abstracting away vendor-specific type restrictions with no runtime cost.

This generalized shuffle mechanism enables fast warp-level communication for arbitrarily complex data types entirely through registers, without resorting to shared memory. KernelForge.jl builds on this capability to expose a high-level interface: the user can simply write mapreduce(f, op, src) or scan(f, op, src) and obtain correct, efficient results even when the elements of src are complex structures, tuples, or nested compositions thereof.

IV-B Ordered Memory Access

When a thread stores an element, the write is not instantly visible to all other threads. The value is first written to the L1 cache, where it is visible only to threads within the same workgroup. It is then propagated to the L2 cache, which is shared across workgroups. This creates a potential communication hazard: a thread in a different workgroup may load a stale value from its own L1 cache before the updated value has arrived in the L2 cache.

Consider a producer-consumer pattern where one workgroup writes data to global memory and then sets a flag to signal completion. A release semantic on the flag store guarantees that all prior writes are visible to any thread that subsequently observes the flag. A matching acquire semantic on the corresponding load guarantees that all subsequent reads observe up-to-date values. Together, release–acquire pairs establish a happens-before relationship between workgroups without requiring a full system-wide fence.

KernelIntrinsics.jl exposes this pattern through the @access macro: @access flag[i] = 0x01 emits a release store, while x = @access flag[i] emits an acquire load. On NVIDIA GPUs, these lower directly to PTX memory ordering annotations — for instance, st.release.gpu.global for the store and fence.acq_rel.gpu for a GPU-scoped fence. On AMD GPUs, the equivalent semantics are expressed through GCN scope bits (sc1/sc0) combined with explicit cache-flush and invalidation instructions (buffer_wbl2, buffer_inv). Both lowerings are verified at the assembly level in KernelIntrinsics.jl’s test suite, confirming that Julia’s compilation pipeline introduces no unintended fences or reorderings. For backends where fine-grained ordering annotations are unavailable, @access falls back to a full memory fence, which is less precise but preserves the same correctness guarantees with no changes to the algorithmic layer.

IV-C Vectorized Memory Access

GPU kernel performance is limited by either compute throughput or memory bandwidth [27, 28]. For large problems that saturate memory bandwidth, vectorized loads—where each thread issues wide transactions (e.g., 128-bit loads of four Float32 values) rather than scalar ones—are essential to maximize bandwidth utilization, particularly for problems that fit within the L2 cache. KernelForge.jl defines its kernels with a static parameter Nitem that controls how many elements each thread processes, specialized at compile time via Julia’s Val type with zero overhead. The optimal Nitem is not necessarily dictated by the vector load width: the scan kernel uses 16 Float32 values per thread, since processing more elements sequentially amortizes synchronization cost across lanes and warps. Figure 1 illustrates this on a copy kernel, where 128-bit loads maximize bandwidth and KernelForge.jl matches or exceeds CUDA.jl’s internal libcuda implementation.

Vectorized loads require contiguous memory access. For strided subarrays, KernelForge.jl falls back to loading individual elements into a tuple, forgoing vectorized loads but preserving the multi-item-per-thread structure and its synchronization benefits.

Refer to caption

Fig. 1: Bandwidth (GB/s) for vectorized copy as a function of problem size. Empirical bandwidth measured via kernel timing with CUDA.@profile, shown for CUDA.jl (which internally calls libcuda) and for KernelForge.jl with 1, 4, and 8 items per thread. The dashed vertical line indicates the L2 cache size divided by 2×sizeof(element)2\times\texttt{sizeof(element)}. Peak bandwidth is achieved with 128-bit loads, where KernelForge.jl outperforms CUDA.jl.

IV-D Alignment Constraints

Vectorized loads and stores provide substantial performance gains for problems that fit within the L2 cache, but they impose strict alignment requirements. The PTX ISA mandates that the address of any memory access be aligned to a multiple of the access size [29]. For instance, a ld.v4.f32 instruction loads 16 bytes and therefore requires the starting address to be 16-byte aligned. In practice, this means that a thread issuing a 128-bit load of Float32 elements can only begin reading at element indices that are multiples of 4 (using 0-based indexing). A load starting at index 2 or 3 would violate this constraint and result in undefined behavior or a hardware fault.

More generally, for a vectorized load of Nitem elements of size ss bytes, the byte address must be a multiple of Nitem×s\texttt{Nitem}\times s. This creates a critical issue for matrix operations: since a matrix may have a number of rows nn that is not a multiple of Nitem, the starting address of the second column (and subsequent columns) is not guaranteed to satisfy the alignment constraint, even if the first column is properly aligned.

A natural solution is to decompose each misaligned load at runtime into a sequence of smaller, properly aligned loads. For example, if Nitem =4=4 and the starting index ii has a misalignment of 2 (i.e., (i1)modNitem=2(i-1)\bmod\texttt{Nitem}=2), the load can be split into a scalar load at ii, a vectorized 2-element load at i+1i+1, and a scalar load at i+3i+3. KernelIntrinsics.jl formalizes this through the function vload_pattern, which is a @generated function that emits an optimal load sequence for a statically known alignment pattern, expressed as a tuple of integers summing to Nitem (e.g., (1,2,1)(1,2,1) in the example above). The outer vload function then dispatches at runtime to the appropriate vload_pattern specialization via a compile-time-generated switch table, so that only the branch selection occurs at runtime while each load sequence itself is fully specialized.

Crucially, this mechanism is entirely hidden from the user. As shown in Listing 2, a vectorized copy kernel in KernelForge.jl requires only a call to vload and vstore!—alignment handling, pattern decomposition, and specialization are managed transparently by the library and at compile time using multiple dispatch.

Listing 2: Vectorized copy kernel with 44 elements per thread in KernelForge.jl. Alignment handling is managed transparently by vload and vstore!.
1 @kernel function vcopy!(dst, src)
2 I = @index(Global)
3 vals = vload(src, I, 4)
4 vstore!(dst, I, vals)
5 # copy remaining elts if I == ndrange
6 end

V Design of Fast Parallel Primitives

V-A Mapreduce

The mapreduce primitive (Section II-C) is implemented with a fixed grid of blocks (e.g., 100 blocks of 256 threads, tuned per architecture). Each thread strides across the input array with stride equal to the total thread count, accumulating a partial result in registers. This fixed-grid strategy processes arbitrarily large arrays without growing the launch configuration.

Within each block, partial results are reduced hierarchically: first across threads within each warp using shuffle operations, then across warps via shared memory, yielding one partial value per block. Warp-level shuffles are provided by KernelIntrinsics.jl (Section IV-A) and support arbitrary composite types at no runtime cost.

Inter-block aggregation is handled without a second kernel launch. Each block writes its partial result to global memory and raises a UInt8 completion flag, initialized to zero before launch. A release-semantic store, issued via the @access macro, guarantees that the partial result is visible to all other blocks before the flag is observed. A designated block spins on all flags using acquire-semantic loads—also via @access— and performs the final reduction once all partial results are confirmed visible. This single-launch design avoids the two-kernel sequence used by CUDA.jl.

The implementation selects among three paths based on input dimensionality. One-dimensional arrays use the strategy above. Two-dimensional arrays are dispatched to the matrix-vector and vector-matrix kernels (Section V-C), which are already optimized for coalesced access and warp-level parallelism along both axes. Higher-dimensional arrays and arbitrary iterators fall through to a general reduction kernel, at a modest performance cost relative to the specialized paths.

V-B Scan

The scan primitive computes an inclusive or exclusive prefix reduction over an input array using any associative operator. It shares the same building blocks as mapreduce—vectorized loads, warp-level shuffles, and release/acquire flag synchronization via @access—and achieves optimal single-pass throughput via the decoupled lookback algorithm of Merrill and Garland [18].

The input is partitioned into tiles of 256×Nitem256\times\texttt{Nitem} elements, one tile per thread block. Each thread loads Nitem elements using vload (Section IV-C), accumulates a local prefix entirely in registers, then participates in a warp-shuffle reduction and a shared-memory exchange to compute the tile’s aggregate. Global memory is therefore read exactly once per element. Upon completion, a designated thread stores the tile aggregate and raises a partial status flag via a release store (@access flag[i] = PARTIAL).

Global prefix propagation proceeds via the lookback phase. A single warp scans backwards over the preceding 32 tiles (64 on AMD wavefront-64 hardware), spinning on each status flag via acquire loads (@access flag[j]) until at least a partial flag is visible. If all 32 (or 64) preceding tiles have raised only partial flags, the warp reduces their aggregates via shuffle-based reduction and continues looking back at the next group of tiles. As soon as the warp encounters a tile with a prefix flag—indicating that the complete inclusive prefix through that tile is already available—it can immediately finalize the current tile’s inclusive prefix without inspecting any earlier tiles. The finalized prefix is written to global memory and the tile raises its own prefix flag via a release store, unblocking all subsequent tiles. The correctness of this scheme relies on the release–acquire ordering established by @access (Section IV-B): without it, a tile could observe a raised flag while the associated aggregate value remains stale in a remote L1 cache.

Once the inclusive prefix is known, each thread computes its final output values entirely in registers and writes them back to the destination array using vectorized stores, so global memory is written exactly once per element.

The resulting implementation is correct for any associative operator, including noncommutative cases such as quaternion multiplication, and achieves throughput competitive with CUB on standard types (Section VII).

V-C Matrix-Vector and Vector-Matrix Products

Matrix-vector and vector-matrix products are central to neural network inference and training, and are typically served by highly optimized proprietary libraries such as cuBLAS. These libraries, however, are vendor-locked and restricted to standard numeric arithmetic. KernelForge.jl provides open-source implementations that match cuBLAS throughput on CUDA for standard types, while supporting arbitrary element types and associative operator pairs. This generality enables use cases beyond the standard (×,+)(\times,+) semiring— such as tropical semiring operations for shortest-path computations or log-space accumulation for numerical stability—without sacrificing performance on the standard numerical case.

Because matrices are stored in column-major order, horizontal reductions (matrix-vector products) and vertical reductions (vector-matrix products) access memory along different axes and require distinct kernels with different coalescing strategies (Section II-C). The optimal thread organization also depends on matrix shape. For tall, narrow matrices, each column resembles an independent 1-D reduction: the same fixed-grid block striding used in mapreduce applies directly, with a small number of blocks (default: 100, tunable per architecture) assigned per column. For wide, short matrices, work must instead be distributed across both dimensions to keep all threads occupied; Figure 2 illustrates this layout for the vector-matrix case. KernelForge.jl selects the appropriate strategy at kernel launch based on matrix shape, with dispatch resolved statically via Julia’s Val type so that each path is independently specialized by the compiler with no runtime overhead.

Refer to caption

Fig. 2: Vector-matrix thread organization for a wide, short matrix. The xx-axis represents columns and the yy-axis represents rows. Each warp (dashed yellow boundary) is assigned 4 consecutive columns; threads stride vertically across rows, with the blue and grey regions corresponding to the first and second row strides, respectively, each thread loading 4 elements per stride. Each workgroup of 128 threads (solid orange boundary) thus covers 16 columns. This layout maintains coalesced memory access while keeping all threads occupied across multiple strides.

VI Correctness Validation

Implementing fast parallel primitives with flexible operators introduces significant testing complexity, as two distinct categories of errors arise naturally during development.

The first category concerns algorithmic correctness: verifying results on large arrays, handling edge cases such as sizes of 31 or 33 elements that straddle warp boundaries, and ensuring the absence of race conditions. Rather than formal proofs, which are beyond the scope of this work, we adopt a comprehensive empirical approach, testing across a wide range of array sizes and scalar types.

The second category concerns compilation validity. Julia compiles through LLVM before generating PTX or GCN code, and any instruction that LLVM cannot correctly lower produces errors that are often difficult to diagnose—ranging from explicit IR failures to silent device-level faults that manifest as spurious out-of-bounds accesses or misaligned memory operations, either of which can crash the Julia session without a clear error message. Such failures can be triggered by user-provided operators, custom structs with non-standard alignment, or array views with non-trivial memory layouts. Our test suite [8] exercises all three sources: custom operators, deliberately misaligned structures, and both contiguous and non-contiguous array views.

Critically, the same test suite runs unmodified on NVIDIA hardware, with competitive results on AMD, validating that KernelForge.jl produces correct results on the A40 (CUDA) and the MI300X (ROCm) without any backend-specific test paths. This cross-architecture validation confirms that the portability guarantees of the KernelIntrinsics.jl abstraction layer hold in practice: correctness is not incidental to a single backend but is a property of the shared algorithmic layer.

VII Performance Evaluation

We evaluate KernelForge.jl against several reference implementations: CUDA.jl, which provides open-source implementations of mapreduce and accumulate; AcceleratedKernels.jl, a cross-architecture Julia library; and cuBLAS, the reference for matrix-vector and vector-matrix operations (called internally by CUDA.jl for these operations on concrete types). We additionally benchmark CUB directly using an nvcc benchmark available in the perf folder of [8]. For the scan primitive, we further include Kokkos [24, 30], a widely used C++ performance portability framework, as an additional point of comparison.

VII-A Experimental Setup

Hardware Platforms

Experiments are conducted on two GPU platforms. The NVIDIA A40 is an Ampere-generation GPU with 48 GB of GDDR6 memory and 696 GB/s peak memory bandwidth, running CUDA 12.8 on Ubuntu 22.04. The AMD MI300X is a CDNA3-generation GPU with 192 GB of HBM3 memory and 5.3 TB/s peak memory bandwidth, running ROCm on Ubuntu 24.04. These two platforms represent distinct points in the GPU landscape: a mainstream data-center GPU with mature CUDA tooling, and a high-memory-bandwidth accelerator with a less mature software ecosystem.

Measurement Methodology

Performance is evaluated using two complementary metrics.

GPU execution time measures on-device kernel execution excluding launch overhead, reflecting the quality of generated code and representative of production scenarios where kernels are invoked repeatedly. For Julia libraries on CUDA this is obtained via the CUDA.@profile macro, which relies on CUDA Events internally; for CUB we use CUDA Events directly. On the AMD backend, timing uses HIP events (AMDGPU.HIP.HIPEvent), which measure device-side elapsed time including kernel launch overhead. The CUDA.@profile macro isolates individual kernel segments with higher precision than raw HIP events; however, equivalent profiling infrastructure is not yet mature in AMDGPU.jl. Allocation is excluded for all libraries that expose a pre-allocation interface; on the AMD backend this is particularly consequential, as AMDGPU.jl device memory allocation incurs substantially higher overhead than its CUDA counterpart—benchmarks for libraries without a pre-allocation interface should therefore be interpreted as upper bounds on achievable kernel time rather than direct comparisons. This metric is reported on both platforms.

End-to-end pipeline time measures total wall-clock time including CPU-side overhead: temporary memory allocation, kernel launch, result transfer from device to host, and CUDA.jl API calls. First-compilation costs due to Julia’s JIT model [31] are excluded, as they amortize across repeated invocations. This metric is reported on the A40 only; on ROCm, CPU-side timing exhibits substantially higher variance and is not reported. Note that end-to-end pipeline is not directly comparable to CUB timings, which measure only on-device execution.

Tuning Parameters.

Achieving performance competitive with vendor-optimized libraries requires careful tuning of parameters such as the number of items processed per thread, the number of threads per block, and the number of blocks per streaming multiprocessor. These parameters depend on hardware-specific characteristics such as L2 cache size, register file capacity, and memory bandwidth (see Figure 1), and each must be chosen from a discrete set of powers of two, resulting in a combinatorial tuning space. This is particularly pronounced for the matrix-vector and vector-matrix kernels, which expose several interdependent parameters controlling the partitioning of work across threads, warps, and blocks.

KernelForge.jl addresses this through an architecture dispatch hierarchy (A40 <: Ampere <: AbstractArch) that selects default parameters at compile time via Julia’s multiple dispatch, analogously to CUB’s per-PTX-version tuning policies [32]. The parameters used throughout Section VII-B were manually tuned to match vendor performance on the A40; matching performance on a new architecture would require a comparable tuning effort for that target. Automated tuning would reduce this burden and is a natural direction for future work, but is outside the scope of this paper.

VII-B Results on A40

Mapreduce

Figure 3 presents benchmark results for the mapreduce primitive, comparing KernelForge.jl against CUDA.jl, AcceleratedKernels.jl, and CUB for input sizes n{107,108}n\in\{10^{7},10^{8}\} and two data types: Float32 and a custom UnitFloat8 Julia type (UInt8 for CUB).

On Float32, all implementations achieve comparable throughput at these sizes. KernelForge.jl matches CUB while slightly outperforming CUDA.jl and AcceleratedKernels.jl. The single-launch flag-based design (Section V-A) yields a more pronounced advantage at smaller sizes where inter-block synchronization overhead dominates, as shown in Table III.

We also evaluate performance on a custom 8-bit type. UnitFloat8 encodes values in [1,1][-1,1] using 256 evenly spaced levels; elements are promoted to Float32 before summation to avoid overflow. Since UnitFloat8 is implemented in KernelForge.jl and has no CUB equivalent, the CUB benchmark uses raw UInt8 summation as a reference lower bound. Correctness is validated by checking that the sign of the result matches a reference Float64 computation on CPU.

KernelForge.jl matches CUB on raw UInt8, achieving a 1.8×1.8\times speedup over CUDA.jl and a 3.8×3.8\times speedup over AcceleratedKernels.jl at n=108n=10^{8}. Notably, this promotion incurs no measurable overhead: the kernel is memory-bound at this scale, so the additional arithmetic is fully hidden behind memory latency. This is enabled by vectorized memory access: loading multiple 8-bit elements per transaction brings effective bandwidth in line with 32-bit operations.

Refer to caption

Fig. 3: Reduction Benchmark Across Implementations (A40). Comparison of four implementations for the parallel sum operation: CUDA.jl, AcceleratedKernels.jl, KernelForge.jl, and CUB (compiled with nvcc). Results are shown for two input sizes: n=107n=10^{7} (left) and n=108n=10^{8} (right), and two data types: Float32 and UInt8. Dark bars show kernel execution time; light bars include launch overhead. Error bars indicate variability across runs. Since UnitFloat8 is a Julia-specific type, the CUB benchmark uses a dummy UInt8 summation for reference.

Scan

Figure 4 presents benchmark results for the prefix scan primitive for input sizes n{107,108}n\in\{10^{7},10^{8}\} and two data types: Float32 and Float64.

KernelForge.jl matches CUB within measurement noise across all configurations, confirming that the decoupled lookback algorithm (Section V-B) achieves vendor-level throughput for both types. CUDA.jl, which relies on a multi-launch reduction-then-scan strategy, is 3.4×3.4\times slower on Float32 and 3.9×3.9\times slower on Float64 at n=108n=10^{8}.

AcceleratedKernels.jl’s default scan uses a sequential inter-block accumulation pass, which becomes a bottleneck at large sizes. This is most visible on Float64 at n=108n=10^{8}, where it is 14.9×14.9\times slower than KernelForge.jl. The gap widens further at n=109n=10^{9}, where KernelForge.jl achieves up to 𝟏𝟒𝟎×\mathbf{140\times} speedup over AcceleratedKernels.jl on Float64 (Table IV). AcceleratedKernels.jl does provide a decoupled lookback variant better suited for large inputs; we do not benchmark it here as it is non-default.

Kokkos achieves 7.4ms7.4\,\text{ms} on Float64 at n=108n=10^{8}, which is 2.6×2.6\times slower relative to KernelForge.jl and CUB. Kokkos was built from source (version 4.6.x, the latest release at time of benchmarking) with -DCMAKE_BUILD_TYPE=Release, -DKokkos_ARCH_AMPERE86=ON, and C++20. The Kokkos benchmark reports zero temporary storage usage, consistent with the fact that Kokkos::parallel_scan on CUDA routes through Kokkos::Impl::CudaScan—a custom implementation that does not invoke cub::DeviceScan and therefore does not use the decoupled lookback algorithm. This gap is consistent across all tested sizes (n=107n=10^{7}, 10810^{8}, 10910^{9}), confirming that the performance difference reflects an algorithmic choice rather than abstraction overhead.

Refer to caption

Fig. 4: Scan Benchmark Across Implementations (A40). Same implementations as the reduction benchmark. Each algorithm is tested with two data types: Float32 and Float64. For reference, Kokkos achieves 0.84ms0.84\,\text{ms}, 7.4ms7.4\,\text{ms}, and 73.4ms73.4\,\text{ms} on Float64 for n=107n=10^{7}, 10810^{8}, and 10910^{9} respectively, representing a 2.6×2.6\times overhead relative to KernelForge.jl and CUB.

Vector-Matrix and Matrix-Vector Operations.

Figures 5 and 6 compare KernelForge.jl against cuBLAS for Float32 vector-matrix and matrix-vector multiplication (MatVec: row-vector ×\times matrix; VecMat: matrix ×\times column-vector) with total input size n×pn\times p fixed at 10710^{7} and 10810^{8}, across all aspect ratios. Complete results are reported in Tables V and VI.

For all non-degenerate aspect ratios, KernelForge.jl matches cuBLAS throughput at n×p=108n\times p=10^{8}. At n×p=107n\times p=10^{7}, differences within 15%\sim 15\% are observed depending on shape, with no consistent advantage on either side. This is a direct consequence of the thread partitioning scheme described in Section V-C, whose static parameters are tuned for the A40 via the architecture dispatch system.

The degenerate cases n=1n=1 (vecmat) and p=1p=1 (matvec) reduce to a plain memory copy, for which cuBLAS does not invoke an optimized path; KernelForge.jl achieves up to 3.7×3.7\times lower kernel time on these configurations. These cases are included for completeness only, as a dedicated copy kernel should be preferred in practice.

At n×p=109n\times p=10^{9}, KernelForge.jl matches cuBLAS across all aspect ratios except (n,p)=(104,105)(n,p)=(10^{4},10^{5}) for the matrix-vector product, where it is 1.45×1.45\times slower (9591.6μ9591.6\,\mus vs. 6591.4μ6591.4\,\mus). This shape was not specifically tuned for the A40, and retuning the static partitioning parameters may close part of this gap. However, it is also possible that cuBLAS exploits tensor core instructions (e.g., 4×14\times 1 MMA tiles) for this shape, in which case matching its performance without similar hardware-level access may not be achievable.

Refer to caption

Fig. 5: Vector-Matrix Product Benchmark Across Matrix Shapes (A40). Throughput comparison between cuBLAS (via CUDA.jl) and KernelForge for Float32 vector-matrix multiplication. The total input data size n×pn\times p is fixed at 10710^{7} (left) and 10810^{8} (right), with varying aspect ratios to assess performance across different memory access patterns.

Refer to caption

Fig. 6: Matrix-Vector Product Benchmark Across Matrix Shapes (A40). Throughput comparison between cuBLAS (via CUDA.jl) and KernelForge for Float32 matrix-vector multiplication. Same experimental setup as Figure 5, with total input size n×pn\times p fixed at 10710^{7} (left) and 10810^{8} (right).

VII-C Results on AMD MI300X

We evaluate KernelForge.jl on the AMD MI300X against AMDGPU.jl and AcceleratedKernels.jl (cf. Tables IVIII). Unlike the NVIDIA setting, where CUDA.@profile isolates individual kernel segments, ROCm timing relies on HIP events (AMDGPU.HIP.HIPEvent), which measure device-side elapsed time including kernel launch overhead and any on-device allocations. CPU-side pipeline timing is avoided, as it exhibits substantially higher variance on ROCm—in the worst cases, the standard deviation can exceed the mean when temporary or destination buffers are allocated within the timed region.

To mitigate this, KernelForge.jl and AcceleratedKernels.jl are benchmarked with pre-allocated temporary buffers—supplied via KernelForge’s get_allocation interface or AcceleratedKernels.jl’s temp keyword argument—which both expose uniformly across all primitives. AMDGPU.jl does not provide an equivalent facility and is therefore benchmarked with internal allocation; its reported times should be interpreted as upper bounds on what that library could achieve with a pre-allocation interface, rather than as a measure of its kernel throughput alone. Consequently, comparisons against AMDGPU.jl primarily demonstrate the practical advantage of KernelForge’s allocation-forwarding design over libraries that lack this facility, not algorithmic superiority per se. As with CUDA.jl dispatching to cuBLAS, AMDGPU.jl dispatches LinearAlgebra.mul! to rocblas_gemv internally, making it the effective rocBLAS baseline for matrix–vector and vector–matrix primitives.

Mapreduce

KernelForge.jl outperforms both baselines for Float32 and UInt8 across all tested sizes. At n=109n=10^{9} on Float32, it is 6.3×6.3\times faster than AMDGPU.jl and 1.4×1.4\times faster than AcceleratedKernels.jl. For UInt8, the advantage is larger: KernelForge.jl is 17.7×17.7\times faster than AMDGPU.jl and 3.6×3.6\times faster than AcceleratedKernels.jl at n=109n=10^{9}, consistent with the vectorized memory access advantage observed on the A40. For UnitFloat8, KernelForge.jl is 11.3×11.3\times faster than AMDGPU.jl at n=109n=10^{9}, but does not consistently outperform AcceleratedKernels.jl. On the A40, UnitFloat8 and UInt8 achieve comparable throughput despite the internal promotion to Float32; this parity does not hold on the MI300X, suggesting that type promotion is handled differently by the ROCm compiler pipeline. Further tuning for mixed-precision types on AMD hardware is left to future work.

TABLE I: Mapreduce Benchmark on AMD MI300X: Mean Kernel Time (μ\mus) ±\pm Std, measured via HIP events. AK: AcceleratedKernels.jl. UF8: UnitFloat8, U8: UInt8. AMDGPU times include temporary allocations; KernelForge and AK times exclude them.
nn Type AMDGPU AK KernelForge
10610^{6} F32 220.5±9.8220.5\pm 9.8 52.2±5.052.2\pm 5.0 31.8±2.031.8\pm 2.0
UF8\toF32 143.9±3.8143.9\pm 3.8 58.4±5.858.4\pm 5.8 72.6±12.472.6\pm 12.4
U8 110.5±3.5110.5\pm 3.5 50.4±5.250.4\pm 5.2 32.7±1.832.7\pm 1.8
10710^{7} F32 125.5±3.5125.5\pm 3.5 52.6±4.552.6\pm 4.5 35.7±2.035.7\pm 2.0
UF8\toF32 140.2±3.9140.2\pm 3.9 92.6±4.192.6\pm 4.1 82.3±13.182.3\pm 13.1
U8 213.4±8.3213.4\pm 8.3 51.2±4.151.2\pm 4.1 33.9±1.933.9\pm 1.9
10810^{8} F32 669.1±5.2669.1\pm 5.2 158.4±3.7158.4\pm 3.7 122.2±2.2122.2\pm 2.2
UF8\toF32 646.0±4.3646.0\pm 4.3 217.8±11.0217.8\pm 11.0 332.8±6.4332.8\pm 6.4
U8 637.2±3.1637.2\pm 3.1 133.9±3.0133.9\pm 3.0 54.1±1.654.1\pm 1.6
10910^{9} F32 6029±206029\pm 20 1360±111360\pm 11 961±5961\pm 5
UF8\toF32 5675±165675\pm 16 1599±161599\pm 16 501±5501\pm 5
U8 5670±105670\pm 10 1144±31144\pm 3 319±3319\pm 3

Scan

KernelForge.jl matches or outperforms both baselines at n107n\geq 10^{7}, with the advantage growing substantially at larger sizes. At n=108n=10^{8}, KernelForge.jl is 2.7×2.7\times faster than AMDGPU.jl and 3.2×3.2\times faster than AcceleratedKernels.jl on Float32, and 1.9×1.9\times and 2.3×2.3\times faster respectively on Float64. At n=109n=10^{9}, the gap widens to approximately 20×20\times over both baselines for both types—consistent with the A40 results, where AcceleratedKernels.jl’s sequential inter-block accumulation becomes the dominant bottleneck at large sizes. The near-identical AMDGPU.jl and AcceleratedKernels.jl timings are expected, as AMDGPU.jl delegates its scan implementation to AcceleratedKernels.jl internally. At n=106n=10^{6}, all three implementations are within measurement noise of each other.

TABLE II: Scan Benchmark on AMD MI300X: Mean Kernel Time (μ\mus) ±\pm Std, measured via HIP events. AK: AcceleratedKernels.jl. F32: Float32. F64: Float64. AMDGPU times include temporary allocations; KernelForge and AK times exclude them.
nn Type AMDGPU AK KernelForge
10610^{6} F32 110.6±3.2110.6\pm 3.2 41.3±2.141.3\pm 2.1 38.8±2.138.8\pm 2.1
F64 56.0±11.856.0\pm 11.8 47.5±1.947.5\pm 1.9 52.5±9.152.5\pm 9.1
10710^{7} F32 158.0±3.2158.0\pm 3.2 141.8±2.4141.8\pm 2.4 98.6±2.398.6\pm 2.3
F64 208.5±6.7208.5\pm 6.7 258.8±7.5258.8\pm 7.5 146.9±13.8146.9\pm 13.8
10810^{8} F32 2103±102103\pm 10 2454±262454\pm 26 779±28779\pm 28
F64 2568±262568\pm 26 3103±183103\pm 18 1329±371329\pm 37
10910^{9} F32 156267±57156267\pm 57 159883±55159883\pm 55 7467±127467\pm 12
F64 270080±518270080\pm 518 275408±431275408\pm 431 13056±2013056\pm 20

Vector-Matrix and Matrix-Vector Operations

KernelForge.jl achieves substantial speedups over AMDGPU.jl for wide matrices (small nn, large pp). For MatVec at n×p=108n\times p=10^{8} with n=103n=10^{3}, p=105p=10^{5}, KernelForge.jl is 3.6×3.6\times faster (cf. Table VII); similarly large speedups hold for VecMat at small nn and large pp (cf. Table VIII). However, as nn grows and pp shrinks, AMDGPU.jl becomes competitive or faster, and for square or tall-and-narrow shapes at n×p=108n\times p=10^{8}, KernelForge.jl offers no consistent advantage.

These results confirm that the algorithmic design of KernelForge.jl ports correctly to AMD hardware and delivers competitive or superior performance on mapreduce and scan without any AMD-specific tuning. The matrix operation results illustrate the limits of A40-tuned parameters when transferred to a different architecture, motivating future per-architecture tuning work and kernel rewriting for specific aspect ratios.

VIII Conclusion

We presented KernelForge.jl, a Julia library demonstrating that three properties often assumed to be mutually exclusive—performance, flexibility, and portability—can be achieved simultaneously for fundamental GPU parallel primitives. Through a two-layer architecture separating portable algorithmic logic from backend-specific intrinsics, KernelForge.jl matches vendor-optimized implementations on scan, mapreduce, and matrix–vector operations on both NVIDIA and AMD hardware, while supporting arbitrary element types and operators beyond what vendor libraries expose.

We regard this work as a proof of concept; several limitations remain. Porting to additional backends (Apple Metal, Intel oneAPI) requires a KernelIntrinsics.jl extension for each target, though the effort is confined to the thin intrinsics layer. More critically, the static tuning parameters were optimized for the A40 and would need retuning per architecture; automated grid search is left to future work. Finally, matching vendor performance on matrix–matrix products will be significantly harder, as vendor libraries exploit tensor core instructions that require specialization beyond warp-level primitives.

Appendix A Benchmark Details on A40

Tables IIIV report the raw kernel timings corresponding to the figures in Section VII.

TABLE III: Mapreduce Kernel Benchmark on A40: Mean Kernel Time (μ\mus) ±\pm Std (cf. Figure 3) for CUDA.jl, Accelerated Kernels, KernelForge and CUB.
nn Type CUDA AK KernelForge CUB
10610^{6} F32 10.1±0.510.1\pm 0.5 11.9±0.311.9\pm 0.3 6.1±0.26.1\pm 0.2 9.4±0.49.4\pm 0.4
UF8→F32 7.3±0.27.3\pm 0.2 11.0±0.211.0\pm 0.2 4.9±0.24.9\pm 0.2 8.0±0.38.0\pm 0.3
10710^{7} F32 80.9±0.580.9\pm 0.5 77.0±0.477.0\pm 0.4 71.2±0.471.2\pm 0.4 75.6±0.375.6\pm 0.3
UF8→F32 39.8±0.439.8\pm 0.4 74.4±0.474.4\pm 0.4 23.3±0.423.3\pm 0.4 25.4±3.325.4\pm 3.3
10810^{8} F32 724.9±1.2724.9\pm 1.2 705.1±0.3705.1\pm 0.3 679.9±1.8679.9\pm 1.8 683.2±0.7683.2\pm 0.7
UF8→F32 323.7±1.0323.7\pm 1.0 678.1±0.3678.1\pm 0.3 178.4±0.7178.4\pm 0.7 175.2±0.3175.2\pm 0.3
10910^{9} F32 7207±97207\pm 9 6972±26972\pm 2 6562±26562\pm 2 6809±26809\pm 2
UF8→F32 3310±53310\pm 5 6719±16719\pm 1 1718±31718\pm 3 1724±31724\pm 3
TABLE IV: Scan Kernel Benchmark on A40: Mean Kernel Time (μ\mus) ±\pm Std (cf. Figure 4) for CUDA.jl, Accelerated Kernels, KernelForge and CUB.
nn Type CUDA AK KernelForge CUB
10610^{6} F32 60.0±0.460.0\pm 0.4 21.2±0.221.2\pm 0.2 21.5±0.421.5\pm 0.4 20.7±1.320.7\pm 1.3
F64 134.2±0.6134.2\pm 0.6 64.7±0.564.7\pm 0.5 34.4±1.034.4\pm 1.0 38.9±0.738.9\pm 0.7
10710^{7} F32 509.9±1.1509.9\pm 1.1 278.4±1.9278.4\pm 1.9 149.4±1.6149.4\pm 1.6 149.5±2.0149.5\pm 2.0
F64 1120.6±1.01120.6\pm 1.0 749.5±0.9749.5\pm 0.9 290.6±2.6290.6\pm 2.6 293.6±1.3293.6\pm 1.3
10810^{8} F32 4948±2.24948\pm 2.2 5655±0.65655\pm 0.6 1460±3.51460\pm 3.5 1435±3.91435\pm 3.9
F64 11002±4.211002\pm 4.2 42285±5.642285\pm 5.6 2841±7.52841\pm 7.5 2837±3.92837\pm 3.9
10910^{9} F32 49322±549322\pm 5 423868±1482423868\pm 1482 14553±1014553\pm 10 14287±714287\pm 7
F64 109724±11109724\pm 11 3944795±413944795\pm 41 28327±2228327\pm 22 28291±1228291\pm 12
TABLE V: VecMat Kernel Benchmark on A40: Mean Kernel Time (μ\mus) ±\pm Std (cf. Figure 5). Rows are grouped by total input size n×pn\times p.
nn pp KernelForge cuBLAS
11 10610^{6} 14.2±0.314.2\pm 0.3 53.5±0.253.5\pm 0.2
1010 10510^{5} 8.1±0.28.1\pm 0.2 10.1±0.310.1\pm 0.3
100100 10410^{4} 7.3±0.17.3\pm 0.1 6.6±0.16.6\pm 0.1
10310^{3} 10310^{3} 6.7±0.16.7\pm 0.1 11.2±0.211.2\pm 0.2
10410^{4} 100100 9.6±0.29.6\pm 0.2 12.3±0.212.3\pm 0.2
10510^{5} 1010 10.8±0.410.8\pm 0.4 7.7±0.37.7\pm 0.3
10610^{6} 11 21.0±0.321.0\pm 0.3 18.2±0.318.2\pm 0.3
11 10710^{7} 140.0±0.7140.0\pm 0.7 514.7±0.4514.7\pm 0.4
1010 10610^{6} 74.7±0.374.7\pm 0.3 80.7±0.380.7\pm 0.3
100100 10510^{5} 72.7±0.372.7\pm 0.3 87.1±0.987.1\pm 0.9
10310^{3} 10410^{4} 69.2±0.369.2\pm 0.3 76.1±0.476.1\pm 0.4
10410^{4} 10310^{3} 72.5±0.472.5\pm 0.4 71.0±0.471.0\pm 0.4
10510^{5} 100100 77.8±0.677.8\pm 0.6 73.8±0.473.8\pm 0.4
10610^{6} 1010 83.8±0.483.8\pm 0.4 75.7±0.875.7\pm 0.8
10710^{7} 11 140.9±0.5140.9\pm 0.5 152.0±0.6152.0\pm 0.6
11 10810^{8} 1386.5±4.31386.5\pm 4.3 5127.4±3.05127.4\pm 3.0
1010 10710^{7} 713.9±0.4713.9\pm 0.4 710.5±0.3710.5\pm 0.3
100100 10610^{6} 693.6±0.4693.6\pm 0.4 779.0±2.0779.0\pm 2.0
10310^{3} 10510^{5} 659.5±0.4659.5\pm 0.4 734.4±1.3734.4\pm 1.3
10410^{4} 10410^{4} 673.2±0.4673.2\pm 0.4 683.4±1.0683.4\pm 1.0
10510^{5} 10310^{3} 724.5±3.1724.5\pm 3.1 683.3±0.8683.3\pm 0.8
10610^{6} 100100 693.3±1.7693.3\pm 1.7 692.1±6.7692.1\pm 6.7
10710^{7} 1010 746.4±1.5746.4\pm 1.5 755.9±3.6755.9\pm 3.6
10810^{8} 11 1324.8±1.71324.8\pm 1.7 1368.1±1.61368.1\pm 1.6
1010 10810^{8} 7104.6±1.07104.6\pm 1.0 7083.6±2.97083.6\pm 2.9
100100 10710^{7} 6903.7±1.36903.7\pm 1.3 6889.4±2.26889.4\pm 2.2
10310^{3} 10610^{6} 6567.2±1.86567.2\pm 1.8 7301.5±7.47301.5\pm 7.4
10410^{4} 10510^{5} 6670.5±0.96670.5\pm 0.9 6812.7±4.46812.7\pm 4.4
10510^{5} 10410^{4} 7228.4±8.47228.4\pm 8.4 6592.3±1.66592.3\pm 1.6
10610^{6} 10310^{3} 6954.2±35.56954.2\pm 35.5 6770.5±1.96770.5\pm 1.9
10710^{7} 100100 6927.2±47.56927.2\pm 47.5 6830.4±4.26830.4\pm 4.2
10810^{8} 1010 7259.4±20.27259.4\pm 20.2 7716.6±61.87716.6\pm 61.8
TABLE VI: MatVec Kernel Benchmark on A40: Mean Kernel Time (μ\mus) ±\pm Std (cf. Figure 6). Rows are grouped by total input size n×pn\times p.
nn pp KernelForge cuBLAS
11 10610^{6} 20.3±0.520.3\pm 0.5 18.5±0.418.5\pm 0.4
1010 10510^{5} 9.0±0.39.0\pm 0.3 13.0±0.213.0\pm 0.2
100100 10410^{4} 7.5±0.27.5\pm 0.2 7.2±0.27.2\pm 0.2
10310^{3} 10310^{3} 7.5±0.37.5\pm 0.3 10.5±0.210.5\pm 0.2
10410^{4} 100100 7.9±0.27.9\pm 0.2 11.4±0.211.4\pm 0.2
10510^{5} 1010 5.4±0.25.4\pm 0.2 4.4±0.24.4\pm 0.2
10610^{6} 11 13.6±0.313.6\pm 0.3 53.4±0.253.4\pm 0.2
11 10710^{7} 143.2±0.5143.2\pm 0.5 151.5±0.6151.5\pm 0.6
1010 10610^{6} 88.4±0.788.4\pm 0.7 80.6±1.680.6\pm 1.6
100100 10510^{5} 80.9±0.680.9\pm 0.6 78.0±0.378.0\pm 0.3
10310^{3} 10410^{4} 76.9±0.876.9\pm 0.8 76.6±0.576.6\pm 0.5
10410^{4} 10310^{3} 72.1±0.772.1\pm 0.7 74.5±0.374.5\pm 0.3
10510^{5} 100100 74.1±0.474.1\pm 0.4 71.4±0.471.4\pm 0.4
10610^{6} 1010 76.4±0.376.4\pm 0.3 76.6±0.376.6\pm 0.3
10710^{7} 11 140.5±0.6140.5\pm 0.6 514.0±0.4514.0\pm 0.4
11 10810^{8} 1380.4±1.11380.4\pm 1.1 1366.8±1.71366.8\pm 1.7
1010 10710^{7} 866.5±2.6866.5\pm 2.6 934.8±47.6934.8\pm 47.6
100100 10610^{6} 713.7±2.4713.7\pm 2.4 692.1±1.1692.1\pm 1.1
10310^{3} 10510^{5} 669.1±1.6669.1\pm 1.6 661.1±1.4661.1\pm 1.4
10410^{4} 10410^{4} 679.6±17.7679.6\pm 17.7 665.2±0.9665.2\pm 0.9
10510^{5} 10310^{3} 682.9±0.8682.9\pm 0.8 696.0±1.2696.0\pm 1.2
10610^{6} 100100 676.2±0.5676.2\pm 0.5 678.4±0.6678.4\pm 0.6
10710^{7} 1010 732.4±0.4732.4\pm 0.4 729.3±0.3729.3\pm 0.3
10810^{8} 11 1390.0±4.21390.0\pm 4.2 5117.6±2.15117.6\pm 2.1
1010 10810^{8} 8785.5±12.98785.5\pm 12.9 15063.6±186.215063.6\pm 186.2
100100 10710^{7} 7061.4±10.57061.4\pm 10.5 6814.6±4.56814.6\pm 4.5
10310^{3} 10610^{6} 6584.1±8.56584.1\pm 8.5 6686.6±10.16686.6\pm 10.1
10410^{4} 10510^{5} 9591.6±20.29591.6\pm 20.2 6591.4±24.06591.4\pm 24.0
10510^{5} 10410^{4} 6902.6±4.56902.6\pm 4.5 6794.1±11.66794.1\pm 11.6
10610^{6} 10310^{3} 6620.4±1.96620.4\pm 1.9 6843.1±2.96843.1\pm 2.9
10710^{7} 100100 6707.5±3.16707.5\pm 3.1 6735.0±4.16735.0\pm 4.1
10810^{8} 1010 7278.7±0.97278.7\pm 0.9 7259.3±0.77259.3\pm 0.7

Appendix B Benchmark of whole pipeline on AMD MI300X

TABLE VII: MatVec Benchmark on AMD MI300X: Mean Kernel Time (μ\mus) ±\pm Std, measured via HIP events. Rows grouped by total input size n×pn\times p. AMDGPU times include temporary allocations; KernelForge times exclude them.
nn pp KernelForge AMDGPU
11 10710^{7} 263.7±2.9263.7\pm 2.9 53558±43053558\pm 430
1010 10610^{6} 388.6±3.1388.6\pm 3.1 5681.5±39.65681.5\pm 39.6
100100 10510^{5} 262.1±2.9262.1\pm 2.9 835.6±7.5835.6\pm 7.5
10310^{3} 10410^{4} 133.8±3.9133.8\pm 3.9 148.0±1.9148.0\pm 1.9
10410^{4} 10310^{3} 189.3±3.4189.3\pm 3.4 174.5±116.8174.5\pm 116.8
10510^{5} 100100 30.9±1.230.9\pm 1.2 115.9±8.7115.9\pm 8.7
10610^{6} 1010 33.7±1.433.7\pm 1.4 29.3±14.529.3\pm 14.5
10710^{7} 11 43.7±1.943.7\pm 1.9 1021.3±6283.31021.3\pm 6283.3
11 10810^{8} 2797.6±10.32797.6\pm 10.3 737748±4006737748\pm 4006
1010 10710^{7} 4440.5±9.84440.5\pm 9.8 75250±38675250\pm 386
100100 10610^{6} 1961.0±4.81961.0\pm 4.8 10572±4410572\pm 44
10310^{3} 10510^{5} 307.3±43.2307.3\pm 43.2 1096.5±3.51096.5\pm 3.5
10410^{4} 10410^{4} 251.6±7.1251.6\pm 7.1 141.1±2.1141.1\pm 2.1
10510^{5} 10310^{3} 155.1±2.0155.1\pm 2.0 163.0±8.6163.0\pm 8.6
10610^{6} 100100 142.3±2.4142.3\pm 2.4 170.4±17.0170.4\pm 17.0
10710^{7} 1010 201.6±3.7201.6\pm 3.7 159.8±36.7159.8\pm 36.7
10810^{8} 11 285.7±4.4285.7\pm 4.4 13218±2361813218\pm 23618
1010 10810^{8} 43906±17143906\pm 171 752899±4023752899\pm 4023
100100 10710^{7} 19112±3919112\pm 39 105736±403105736\pm 403
10310^{3} 10610^{6} 2140.2±4.92140.2\pm 4.9 10869±2310869\pm 23
10410^{4} 10510^{5} 1410.7±21.41410.7\pm 21.4 1294.3±8.61294.3\pm 8.6
10510^{5} 10410^{4} 1412.5±17.11412.5\pm 17.1 1086.2±30.91086.2\pm 30.9
10610^{6} 10310^{3} 1219.1±13.71219.1\pm 13.7 1011.9±42.71011.9\pm 42.7
10710^{7} 100100 1299.1±7.11299.1\pm 7.1 1162.9±17.01162.9\pm 17.0
10810^{8} 1010 1872.9±22.01872.9\pm 22.0 27329±23353127329\pm 233531
TABLE VIII: VecMat Benchmark on AMD MI300X: Mean Kernel Time (μ\mus) ±\pm Std, measured via HIP events. Rows grouped by total input size n×pn\times p. AMDGPU times include temporary allocations; KernelForge times exclude them.
nn pp KernelForge AMDGPU
11 10710^{7} 36.9±1.136.9\pm 1.1 448.4±17.3448.4\pm 17.3
1010 10610^{6} 29.8±1.329.8\pm 1.3 133.2±16.8133.2\pm 16.8
100100 10510^{5} 27.8±1.227.8\pm 1.2 108.9±8.3108.9\pm 8.3
10310^{3} 10410^{4} 30.5±5.030.5\pm 5.0 82.8±2.382.8\pm 2.3
10410^{4} 10310^{3} 28.5±1.228.5\pm 1.2 103.0±3.1103.0\pm 3.1
10510^{5} 100100 48.9±2.048.9\pm 2.0 137.4±1.6137.4\pm 1.6
10610^{6} 1010 52.6±8.952.6\pm 8.9 98.8±3.998.8\pm 3.9
10710^{7} 11 56.9±10.256.9\pm 10.2 104.8±2.3104.8\pm 2.3
11 10810^{8} 283.6±3.5283.6\pm 3.5 4740.0±156.54740.0\pm 156.5
1010 10710^{7} 145.6±2.7145.6\pm 2.7 686.1±94.7686.1\pm 94.7
100100 10610^{6} 162.5±8.0162.5\pm 8.0 169.4±13.7169.4\pm 13.7
10310^{3} 10510^{5} 137.2±12.5137.2\pm 12.5 175.0±6.8175.0\pm 6.8
10410^{4} 10410^{4} 131.9±12.3131.9\pm 12.3 164.4±12.6164.4\pm 12.6
10510^{5} 10310^{3} 130.0±1.9130.0\pm 1.9 185.8±1.7185.8\pm 1.7
10610^{6} 100100 187.3±3.2187.3\pm 3.2 148.2±3.4148.2\pm 3.4
10710^{7} 1010 156.0±2.7156.0\pm 2.7 158.6±2.5158.6\pm 2.5
10810^{8} 11 212.4±2.8212.4\pm 2.8 232.1±3.4232.1\pm 3.4
1010 10810^{8} 1410.6±12.81410.6\pm 12.8 6877.0±82.96877.0\pm 82.9
100100 10710^{7} 1452.2±29.61452.2\pm 29.6 1247.6±30.21247.6\pm 30.2
10310^{3} 10610^{6} 1180.1±28.31180.1\pm 28.3 1495.2±14.51495.2\pm 14.5
10410^{4} 10510^{5} 1179.4±42.01179.4\pm 42.0 1079.9±8.51079.9\pm 8.5
10510^{5} 10410^{4} 1040.6±44.01040.6\pm 44.0 1112.3±4.51112.3\pm 4.5
10610^{6} 10310^{3} 1176.1±7.91176.1\pm 7.9 1640.3±4.31640.3\pm 4.3
10710^{7} 100100 1212.6±29.11212.6\pm 29.1 1159.2±53.81159.2\pm 53.8
10810^{8} 1010 1411.9±13.11411.9\pm 13.1 1242.1±66.41242.1\pm 66.4

References

  • [1] A. Sedova, J. D. Eblen, R. Budiardja, A. Tharrington, and J. C. Smith, “High-performance molecular dynamics simulation for biological and materials sciences: Challenges of performance portability,” in 2018 IEEE/ACM International Workshop on Performance, Portability and Productivity in HPC (P3HPC). IEEE, 2018, pp. 1–13.
  • [2] S. J. Pennycook, J. D. Sewall, and V. W. Lee, “Implications of a metric for performance portability,” Future Generation Computer Systems, vol. 92, pp. 947–958, 2019.
  • [3] M. Martineau, S. McIntosh-Smith, and W. Gaudin, “Assessing the performance portability of modern parallel programming models using tealeaf,” Concurrency and Computation: Practice and Experience, vol. 29, no. 15, p. e4117, 2017.
  • [4] V. Artigues, K. Kormann, M. Rampp, and K. Reuter, “Evaluation of performance portability frameworks for the implementation of a particle-in-cell code,” Concurrency and Computation: Practice and Experience, vol. 32, no. 11, p. e5640, 2020.
  • [5] T. Besard, C. Foket, and B. De Sutter, “Effective extensible programming: Unleashing julia on gpus,” IEEE Transactions on Parallel and Distributed Systems, vol. 30, no. 4, pp. 827–841, 2019.
  • [6] V. Churavy, “KernelAbstractions.jl.” [Online]. Available: https://github.com/JuliaGPU/KernelAbstractions.jl
  • [7] W. F. Godoy, P. Valero-Lara, T. E. Dettling, C. Trefftz, I. Jorquera, T. Sheehy, R. G. Miller, M. Gonzalez-Tallada, J. S. Vetter, and V. Churavy, “Evaluating performance and portability of high-level programming models: Julia, python/numba, and kokkos on exascale nodes,” in 2023 IEEE international parallel and distributed processing symposium workshops (IPDPSW). IEEE, 2023, pp. 373–382.
  • [8] “KernelForge.jl,” available at https://github.com/epilliat/KernelForge.jl.
  • [9] “KernelIntrinsics.jl,” available at https://github.com/epilliat/KernelIntrinsics.jl.
  • [10] A.-L. Nicusan, D. Werner, S. Branford, S. Hartley, A. J. Morris, and K. Windows-Yule, “Acceleratedkernels. jl: Cross-architecture parallel algorithms from a unified, transpiled codebase,” arXiv preprint arXiv:2507.16710, 2025.
  • [11] J. Nickolls and W. J. Dally, “The gpu computing era,” IEEE micro, vol. 30, no. 2, pp. 56–69, 2010.
  • [12] S. G. De Gonzalo, S. Huang, J. Gómez-Luna, S. Hammond, O. Mutlu, and W.-m. Hwu, “Automatic generation of warp-level primitives and atomic instructions for fast and portable parallel reduction on gpus,” in 2019 IEEE/ACM International Symposium on Code Generation and Optimization (CGO). IEEE, 2019, pp. 73–84.
  • [13] B. A. Hechtman and D. J. Sorin, “Exploring memory consistency for massively-threaded throughput-oriented processors,” in Proceedings of the 40th Annual International Symposium on Computer Architecture, 2013, pp. 201–212.
  • [14] S. Xiao and W.-c. Feng, “Inter-block gpu communication via fast barrier synchronization,” in 2010 IEEE International Symposium on Parallel & Distributed Processing (IPDPS). IEEE, 2010, pp. 1–12.
  • [15] K. Wang, D. Fussell, and C. Lin, “Fast fine-grained global synchronization on gpus,” in Proceedings of the Twenty-Fourth International Conference on Architectural Support for Programming Languages and Operating Systems, 2019, pp. 793–806.
  • [16] X. Mei and X. Chu, “Dissecting gpu memory hierarchy through microbenchmarking,” IEEE Transactions on Parallel and Distributed Systems, vol. 28, no. 1, pp. 72–86, 2016.
  • [17] M. Rhu, M. Sullivan, J. Leng, and M. Erez, “A locality-aware memory hierarchy for energy-efficient gpu architectures,” in Proceedings of the 46th Annual IEEE/ACM International Symposium on Microarchitecture, 2013, pp. 86–98.
  • [18] D. Merrill and M. Garland, “Single-pass parallel prefix scan with decoupled look-back,” NVIDIA, Tech. Rep. NVR-2016-002, 2016.
  • [19] L. S. Blackford et al., “An updated set of basic linear algebra subprograms (blas),” ACM Transactions on Mathematical Software, vol. 28, no. 2, pp. 135–151, 2002.
  • [20] M. Harris, S. Sengupta, and J. D. Owens, “Parallel prefix sum (scan) with cuda,” in GPU Gems 3, H. Nguyen, Ed. Addison Wesley, 2007, pp. 851–876.
  • [21] G. E. Blelloch, “Prefix sums and their applications,” School of Computer Science, Carnegie Mellon University, Pittsburgh, PA, Tech. Rep. CMU-CS-90-190, 1990.
  • [22] ——, “Scans as primitive parallel operations,” IEEE Transactions on Computers, vol. 38, no. 11, pp. 1526–1538, 1989.
  • [23] R. D. Hornung and J. A. Keasler, “The raja portability layer: Overview and status,” Lawrence Livermore National Laboratory, Livermore, CA, Tech. Rep. LLNL-CONF-653873, 2014.
  • [24] H. C. Edwards, C. R. Trott, and D. Sunderland, “Kokkos: Enabling manycore performance portability through polymorphic memory access patterns,” Journal of parallel and distributed computing, vol. 74, no. 12, pp. 3202–3216, 2014.
  • [25] J. H. Davis, P. Sivaraman, J. Kitson, K. Parasyris, H. Menon, I. Minn, G. Georgakoudis, and A. Bhatele, “Taking gpu programming models to task for performance portability,” in Proceedings of the 39th ACM International Conference on Supercomputing, 2025, pp. 776–791.
  • [26] D. Merrill, “CUB: CUDA UnBound library,” NVIDIA Research, 2015, now part of NVIDIA CCCL (CUDA Core Compute Libraries). [Online]. Available: https://nvlabs.github.io/cub/
  • [27] S. Williams, A. Waterman, and D. Patterson, “Roofline: an insightful visual performance model for multicore architectures,” Communications of the ACM, vol. 52, no. 4, pp. 65–76, 2009.
  • [28] C. Yang, T. Kurth, and S. Williams, “Hierarchical roofline analysis for gpus: Accelerating performance optimization for the nersc-9 perlmutter system,” Concurrency and Computation: Practice and Experience, vol. 32, no. 20, p. e5547, 2020.
  • [29] NVIDIA Corporation, PTX ISA Version 8.8, 2024. [Online]. Available: https://docs.nvidia.com/cuda/parallel-thread-execution/
  • [30] C. R. Trott, D. Lebrun-Grandié, D. Arndt, J. Ciesko, V. Dang, N. Ellingwood, R. Gayatri, E. Harvey, D. S. Hollman, D. Ibanez et al., “Kokkos 3: Programming model extensions for the exascale era,” IEEE Transactions on Parallel and Distributed Systems, vol. 33, no. 4, pp. 805–817, 2021.
  • [31] T. Besard, C. Foket, and B. De Sutter, “Effective extensible programming: unleashing julia on gpus,” IEEE Transactions on Parallel and Distributed Systems, vol. 30, no. 4, pp. 827–841, 2018.
  • [32] NVIDIA, “CUB tunings,” https://nvidia.github.io/cccl/cub/tuning.html, cUDA Core Compute Libraries documentation.