r/CUDA • u/Grated-Expectations • 1d ago
A compiler that automatically parallelises code to CUDA wherever it actually helps
The what
xc is a C-like language I've (where "I" includes Claude) been building over the last 6-7 months. Its compiler, xcc, automatically turns data-parallel code-candidates into executable blocks across various workers {CPU threads, CUDA, Metal, Vulkan or WebGPU} from the same source. On NVIDIA it generates PTX itself and loads it through the driver, so a program needs no toolkit, nvcc or extra libraries. All it needs is the driver that ships with the card.
The same source code compiles for all targets. It runs on Metal on an M4 Max and on CUDA on Windows, unchanged.
The specific block (CPU, GPU, ...) that is executed by the scheduler is bound at runtime, not compile-time, based on a measurement of the performance on the actual hardware present. Once the measurement has been taken, it is persisted to the standard 'settings' location on the platform ('defaults' on Mac, Registry on Windows, ~/.config on linux,...). If a code-block is to be run with sufficiently-different parameters than before, the measurement is re-made, with the goal being to establish the boundary condition between CPU and GPU. In this way it self-optimises.
Crucially, it's the same language used to create both CPU and GPU (and in future HLS to FPGA) code-blocks. The compiler then has the ability to prove (at the IR stage) that there are no data-hazards, and to interpose transport/memory-barriers as required to enforce that.
Recently, I've added a "C front end", the CFE uses clang to produce LLVM IR, and converts their IR into my IR and then everything flows as normal. That means plain, unadorned C code that has loops that can be proven to be non-self-dependent can be parallelised onto the GPU.
I say "GPU" but in fact xcc provides what I call 'parallel parallelism' - the built-in scheduler will quite happily tile work across multiple GPUs on a given machine, or in fact over a network (preferably high speed) or if you're on a Mac, the Thunderbolt 4/5 connections. The repo provides 'xcd' - a daemon that is run on the remote machines, which announces itself using mDNS and can take part via the scheduler.
The scheduler measures, for each block
- Its transfer time (to and from the worker), and
- Its compute time, at tile sizes that double from a minimum (×2, ×4, ×8, …).
It uses the smallest tile that reaches 90% of the best throughput, so the work isn't drowned by the transfers. For a networked worker, the smallest tile is set from the link's round trip and bandwidth, keeping latency under 5% of a tile's time.
'auto' (the default unless the programmer wants to force a particular worker for a particular block of code) then times the devices alone and together, and keeps the fastest arrangement. Within a shared run, workers take tiles from a common queue, so a faster worker naturally does more of the work. On my windows box with a 3090 and a 5080, the 5080 gets roughly 2x the work of the 3090.
The example
There are several benchmark-style results on the website, CPU, GPU, the CFE, and networked. but I wanted to see it in a real-life scenario rather than just artificial benchmarks. The application I chose was one that is pretty painful to run on a CPU - I call it Astronomancy, and it's an astrophotography frame stacker.
xc provides a cross-platform UI framework (though this is in its infancy), which is useful when you have a cross-platform compiler :) Astronomancy leverages that framework to be runnable on any of the three main platforms (Windows, Mac, Linux) without source changes. The below image is on a Mac, but the same works under either of the other two.
To generate those beautiful astrophotography images, the camera (usually a high-definition one) takes many many photos over a period of time, and the software needs to merge them together, take account of inherent camera noise, debayering, stuck pixels, passing satellites, etc. etc. The pipeline is
- calibration
- star detection and registration
- Lanczos-3 warping and Bayer drizzle
- sigma-clipped and median stacking
- background extraction
- wavelet denoising
- Richardson-Lucy deconvolution with deringing
- star separation
- stretching with local contrast
Every per-pixel stage is a par block, and none of it is hand-written CUDA
I took a short sequence (640 or so, each about 12MB) of photos of M51 (the whirlpool galaxy) together with the appropriate darks etc frames, and ran it through a processing chain...

Here's an example of what the program is working with, a single frame from the source data, and the final result after merging

M51 data: Erik Westermann (formerly RemoteAstrophotography.com), Vespera/Stellina, used with credit.
The timings, when run in various modes, are interesting:
| Mode | Windows | M4 Max Mac |
|---|---|---|
| 1 cpu thread | 1754s | 420s |
| All cpu threads | 182s | 57.5s |
| CUDA, forced, 5080 | 339s | - |
| Vulkan, forced, 5080 | 358s | - |
| CUDA, 3090 & 5080, forced | 214s | - |
| GPU, forced, metal | - | 52.1s |
| Auto mode | 113s | 42.2s |
You can tell from the numbers that there's a lot of code which is 'saxpy'-like, where the overhead in moving the data to the GPU is a significant fraction of the compute time involved. Astronomancy is written in xc, which allows annotation of the 126 'par' blocks where data-parallel execution would be possible - if it were written in C, the CFE would find them instead.
In 'auto' mode, for each of those blocks, it chose the most efficent way to compute the block of code. Putting all the loops etc. on a single GPU was not as fast as sharing between both GPUs on the machine, but the real win came from deciding which of the candidate blocks would be best-served by putting on the GPU, bringing the time down from a naive single-threaded version taking almost half an hour to approximately 2 minutes.
[aside: The Mac has an advantage on one of the blocks being (frequently) used that needs 48GB/s of bandwidth for an image, maxing out the PC's 5950X's dual-channel DDR4-3200 but the M4 Max has more than 10x that bandwidth and copes much better, the actual compute-time on the PC GPU is faster than the Mac, but it's dominated by the memory-bandwidth requirements, specifically it's the weford op:
- Items: 3072 × 2080 × 3 planes = 19.2 M.
- Bytes per item: 6 reads + 3 writes × 4 bytes = 36.
- Per run: 36 × 19.2 M ≈ 691 MB.
- On the PC: 691 MB in 14.4 ms = 48 GB/s, against a 51 GB/s peak, so it's at the limit.]
I have a policy of using real-life programs to test things, and (as usual) it turned out to be useful..
- Stale GPU copies. A block writing through
stack + k*nmarked only the slice's start as changed. CUDA then reused a stale copy of the whole array on the card, so the master dark came out all zeros. Vulkan and Metal kept no such copy, so they were right. Fixed in the 0.87.1 release - A race that crashed the app on Windows. CPU threads wrote into an array that a GPU block had just written and was still tracking, which crashed with 0xC0000005. Fixed in the 0.87.1 release.
Documentation is in the first link at the top, and also contains the repo link.
The compiler is only at v0.87, theres quite a way to go before I want to call it 'version 1' but its already fairly capable. There are certainly places it could be improved (arrays of double currently keep a block on the CPU under CUDA, for example, because it doesn't work across all platforms. I need to relax some things to allow host-specific operations to work).
Let me know if you have questions :)


