KernelAbstractions.jl is the layer that lets one Julia kernel target four GPU backends
Heterogeneous programming in Julia
At a glance
- What is it?
- KernelAbstractions.jl is the Julia package that writes a kernel once and runs it on NVIDIA CUDA, AMD ROCm, Intel oneAPI or Apple Metal, and it is deliberately minimal, aiming to explore ways to write heterogeneous code rather than to be a framework. Its companion KernelInterface package is versioned separately and released more often than the main package.
- Who is it for?
- Adopt KernelAbstractions.jl if you write Julia code that has to run on more than one GPU vendor's hardware, because the abstraction covers the four major backends and the alternative is four separate kernel implementations.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly Julia, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the abstraction covers, and the four backends behind it
KernelAbstractions, abbreviated KA in the README, is a package that enables you to write GPU-like kernels targeting different execution backends. The stated intent is that KA is intended to be a minimal and performant library that explores ways to write heterogeneous code.
The four supported backends are the ones that matter for Julia: NVIDIA CUDA, AMD ROCm, Intel oneAPI and Apple Metal. Each is a separate JuliaGPU package, and the list is a good indication of the design, because KA itself does not implement any of them. It defines the interface, and the backend packages provide the implementation.
That division is what makes the library portable rather than merely distributed. A kernel written against KA's abstractions compiles to whatever the active backend supports, whether that is a CUDA kernel, a ROCm kernel, a SYCL kernel under oneAPI, or a Metal shader. The user writes the computation once and the choice of backend is a matter of which packages are loaded in the session.
The word minimal in the README's own description is the one to hold onto. This is not a full GPU programming framework. It does not provide a memory hierarchy abstraction, a kernel launch configuration system, a compilation cache or a device selection UI. It provides the layer that lets a piece of code be a kernel at all, and the array types that the backends can operate on.
The repository layout is consistent with that. There is src/ for the implementation, lib/ for the library code, ext/ for the Julia package extensions mechanism, which is how a backend hooks in without KA depending on it directly, test/ for the test suite, examples/ for worked examples, benchmark/ for performance measurement, and docs/ for the manual. There is a perf.md file at the root, which in a JuliaGPU repository is the file that records what changed between releases in terms of performance.
The primary language is Julia, the licence is MIT, the default branch is main, and the last push was on 2026-10-20.
KernelInterface is versioned on its own, and that is the detail to watch
The release history in the repository is unusual and worth reading carefully, because it affects how you pin dependencies.
The recent releases listed are KernelInterface-v0.4.2 from 2026-10-20, v0.10.1 from 2026-10-17, and KernelInterface-v0.4.1 from 2026-10-13.
So there are two version lines moving at once. The main package is at 0.10.1, and KernelInterface is at 0.4.2. They are separate packages with separate tags, and the KernelInterface tags appear twice in a three-week window while the main package moved once.
That is a deliberate pattern rather than an accident. KernelInterface is the lower-level piece that defines what a kernel interface is, the set of traits and the contract that a backend implements, and it has to move when the contract changes. The main package sits on top of it. If the two were versioned together, every change to the interface would force a major version bump of the whole stack and break the ecosystem's compatibility expectations. Versioning them separately lets the interface evolve on a faster cycle without forcing every user to take a new major version of the higher-level package.
The practical consequence for a user is that you cannot assume a single version number describes the state of the stack. A backend package such as CUDA.jl or Metal.jl will declare its own KernelInterface requirement, and the resolved version of KernelInterface in your environment is what determines which interface features are actually available. If you write a kernel using an interface feature that your installed KernelInterface does not have, the failure appears as a method or trait error rather than a version mismatch.
So when you pin, pin the whole set. The README's own example of the ecosystem arrangement, with four separate backend packages, means that in practice your environment has five packages to keep compatible rather than one, and the KernelInterface version is the one that changes most often.
Two CI systems, a perf.md file, and what the repository layout says about priorities
The badges at the top of the README show two continuous integration systems rather than one, and the split is informative for a GPU library.
There is a CPU CI badge pointing at the GitHub Actions workflow ci.yml, and a GPU CI badge pointing at a Buildkite project for julialang/kernelabstractions-dot-jl. The same split shows up across the JuliaGPU organisation: GitHub Actions for the fast checks and Buildkite for the runs that need real hardware.
The reason is cost and availability. A CPU runner is free and instant. A GPU runner is neither, so it sits in a queue and the tests that need it run on a schedule rather than on every push. That has a direct effect on how a contributor works: a change to the abstract interface can be checked immediately, while a change that affects backend codegen waits for the GPU pipeline.
The perf.md file at the repository root is the other signal. In a library where the output is a kernel launch, a change can be functionally identical and still be a significant regression, and a project that keeps a performance record in the repository treats that as a first-class concern rather than something to notice in a benchmark later. The benchmark/ directory is the machinery that produces those numbers.
The rest of the layout reinforces the same priorities. The ext/ directory is the Julia package extensions mechanism, and it is how a backend registers itself without KernelAbstractions taking a dependency on it, which is what keeps the dependency graph from becoming circular. The lib/ directory holds the library code and src/ the source proper, a split that in JuliaGPU repositories separates the parts that are generic from the parts that are the interface definition. The .git-blame-ignore-revs file at the root is the marker of a repository that has done a formatting or reorganisation sweep and does not want that commit to obscure the history of every file it touched.
There is also a .pre-commit-config.yaml, so formatting and lint checks run before a commit rather than in the CI, and a CITATION.cff file alongside the BibTeX entry in the README, which is the two formats that Zenodo and other archives use to pick up citation metadata automatically.
The docs/ directory holds the manual, and the README links two versions of it, a stable one at juliagpu.github.io and a development one at the dev path, so a reader can compare the documentation for the released package against the documentation for what is on main.
The examples directory tells you what the abstraction is for
The examples/ directory is the closest thing the repository has to a tutorial, and the file names describe the range of problems the abstraction is meant to solve.
There is histogram.jl, which is the shape of a kernel that operates independently on elements, and matmul.jl, which is the shape of a kernel that operates on a whole tile. There is memcopy.jl and memcopy_static.jl, which look like the same operation written two ways, one with dynamic sizing and one with sizes known at compile time. There is naive_transpose.jl, which is the classic memory-access-pattern exercise.
Then the ones with a system dimension. mpi.jl is a kernel that has to work across a message-passing boundary. numa_aware.jl is a kernel that has to be aware of non-uniform memory access, which is the problem that appears when a multi-socket machine has memory attached to different processors. There is performance.jl and performant_matmul.jl, the latter being the difference between a correct matmul and a fast one.
Read together, those filenames describe a progression. The first group is about writing a kernel at all. The second group is about the mechanics that make a kernel fast, memory access pattern and static versus dynamic shapes. The third group is about the cases where a single kernel is not enough, because the computation has to be distributed or has to respect the hardware's topology.
utils.jl in the same directory is the shared helper code, which is a normal convention for a set of examples that are meant to be read and compared against each other rather than run independently.
The benchmark/ directory at the root and the perf.md file are the other half of that story. In a GPU library, a change that is functionally correct but slower is a regression, and a project that keeps a performance file in the repository is one that treats that as something to be tracked. The README's badges also show a CPU CI workflow on GitHub Actions and a separate GPU CI on Buildkite, which is the usual split for this kind of project since GPU runners are expensive and scarce.
Where KernelAbstractions is the wrong abstraction
Three limits, and the first is the one the README's own framing implies.
The first is that minimal means some things are missing on purpose. A library that explores ways to write heterogeneous code is not trying to be the complete answer to GPU programming, and the things it leaves out are the things that matter when you need performance: no vendor-specific fast paths, no control over launch configuration, no occupancy tuning, no fused kernel pipelines. The moment your code needs an NVIDIA-specific feature or an AMD-specific feature, the abstraction is in the way, and you are writing a second implementation anyway.
That makes the decision a portfolio one. If you support four vendors and each needs its own optimised kernel, the abstraction has saved you the scaffolding and cost you the tuning. If you support one vendor, it has cost you the tuning and saved you nothing.
The second is the abstraction overhead itself, which is real and is measured differently on different backends. Every array operation, every launch, every allocation goes through the KA interface, and the indirection is not free. The numa_aware.jl example is the interesting case, because non-uniform memory access is precisely a place where the generic answer is the wrong one: on a multi-socket machine the fastest kernel is the one that knows which memory it is touching, and that knowledge is exactly what a portable layer has to abstract away. The example exists because the problem is hard, not because the abstraction solves it.
The third is the dependency graph. The ecosystem arrangement means that to use KA you load one of four backend packages, and each of those pulls in a substantial stack of its own, with its own version compatibility matrix against KernelInterface and against KA itself. A project that only ever targets one vendor carries that complexity for nothing.
The alternative approach is to write against the vendor's own package directly, CUDA.jl or Metal.jl, and accept that the kernel is not portable. The difference in approach is that KA buys you one implementation of the algorithm and the freedom to move between vendors, at the cost of performance tuning and the dependency graph, while the direct route buys you the vendor's full feature set and the best performance on the hardware you actually have. For a library that has to run on whatever GPU a user has, KA is the only workable answer. For an internal tool on a known machine, it is an extra layer between you and the hardware.
Editorial conclusion
Adopt KernelAbstractions.jl if you write Julia code that has to run on more than one GPU vendor's hardware, because the abstraction covers the four major backends and the alternative is four separate kernel implementations. Do not adopt it if your code runs on one vendor only, since you would be giving up direct access to that vendor's extensions for a portability you do not need, and do not expect a framework around it, because the README states the intent is a minimal and performant library that explores ways to write heterogeneous code. Verify first that the backend you need is actually one of the four listed, that the KernelInterface version your backend package requires matches the one you install, since the two are versioned separately and the recent release history shows KernelInterface tagged twice in the same month as the main package, and that the abstract kernel you write performs acceptably on the slowest backend you support, since the abstraction removes vendor-specific tuning from the code you write.
Frequently asked questions
What is KernelAbstractions.jl and what does it enable?
It is a Julia package that enables you to write GPU-like kernels targeting different execution backends, intended to be a minimal and performant library that explores ways to write heterogeneous code. The supported backends are NVIDIA CUDA, AMD ROCm, Intel oneAPI and Apple Metal.
Why are KernelInterface and KernelAbstractions versioned separately?
KernelInterface is the lower-level contract that backend packages implement, and it changes faster than the higher-level package built on it. Versioning them separately lets the interface evolve without forcing a major version bump of the whole stack, and the release history shows KernelInterface tagged at 0.4.1 and 0.4.2 in the same month as the main package moved to 0.10.1.
Which GPU backends does KernelAbstractions.jl support?
NVIDIA CUDA, AMD ROCm, Intel oneAPI and Apple Metal, each provided by its own package in the JuliaGPU organisation. The KernelAbstractions repository itself implements none of them, it defines the interface and the backends provide the implementations.
What examples does KernelAbstractions.jl ship?
The examples directory holds histogram.jl, matmul.jl, memcopy.jl, memcopy_static.jl, naive_transpose.jl, mpi.jl, numa_aware.jl, performance.jl, performant_matmul.jl and utils.jl, covering element-wise kernels, matrix multiplication, memory access patterns, static versus dynamic shapes and kernels that must respect distributed and NUMA topologies.
How is KernelAbstractions.jl tested across backends?
The README carries two separate CI badges, a CPU CI on GitHub Actions and a GPU build status on Buildkite, reflecting that GPU runners are a separate and scarcer resource. The repository also tracks code coverage and keeps a perf.md file for performance changes.
How do I cite KernelAbstractions.jl?
The README gives a BibTeX entry for the software, with author Churavy, Valentin, the MIT licence, the title KernelAbstractions.jl, the GitHub URL and a DOI of 10.5281/zenodo.4021259, and the project has a CITATION.cff file at the repository root.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/juliagpu-kernelabstractions-jl)Was this useful?