AES Encryption: GPU Acceleration vs CPU SIMD in Mojo
A hands-on comparison of AES encryption performance using GPU parallelism versus CPU hardware acceleration using new Mojo language.
It started, as it always does for me, with the desire to learn something new.
I came across Mojo — a systems programming language built with first-class GPU support.
A combination of Python-like syntax with modern language features and memory safety, which I believe was inspired by Rust’s approach, but with a slightly simpler way of enforcing it, in my opinion.
For me — someone who has been programming professionally in Rust for the past 5 years, while also happily reaching for Python where it suits better — Mojo felt like a natural meeting point.
As you could probably tell, I was immediately charmed and realized that was definitely the time to learn GPU programming and implement something by myself.
The broader idea that has been stuck in my head lately is a GPU-accelerated backend stack — running server-side services where the GPU isn’t just for ML inference, but powers the processing of regular requests and responses. It sounds ambitious, maybe even a little unhinged, but some early experiments have already shown real promise. cuJSON — GPU-accelerated JSON parsing — is one example of successful GPU acceleration.
So I started with AES, which is the backbone of internet encryption. I wanted to know: does it actually make sense to move AES onto the GPU, and how does it compare with existing CPU implementations?
This post is the story of that experiment.
The Plan
The first thing I needed was a reference point — something production-grade to benchmark against. The natural choice was RustCrypto’s AES implementation, which is the same crate used under the hood by rustls, the most widely deployed pure-Rust TLS library. If your Rust service speaks HTTPS, there’s a good chance your AES traffic flows through that code.
For correctness testing I used the NIST ACVP test suite — the Automated Cryptographic Validation Protocol maintained by NIST. It provides official test vectors that any AES implementation must pass before you can trust the output. There’s no point benchmarking a fast but wrong cipher.
Step-by-step plan:
-
Plain AES in Mojo — implement AES exactly as defined in FIPS 197, the original specification, to establish a clean baseline.
-
GPU-accelerated AES — move the cipher onto the GPU.
-
Plain CPU vs GPU — compare the naive Mojo CPU implementation against the GPU one, to measure the raw parallel speedup.
-
Mojo CPU vs RustCrypto — benchmark both implementations against
RustCrypto’s to understand where theMojoimplementation sits relative to production Rust. -
SIMD-accelerated AES in Mojo — implement AES using CPU SIMD instructions for x86 and ARM/AARCH64 (ARM NEON). This is what RustCrypto does internally when running on a supported CPU.
-
Final comparison — put all four implementations side by side: plain Mojo CPU, GPU,
RustCrypto, andMojowith SIMD, to get the full picture.
Results
Benchmarks were run on an Intel Core i7-8750H with an NVIDIA GeForce GTX 1050 Ti (4 GB).
You could try it by your own mojo-crypto:
pixi run bench block_ciphers aes cpu # Mojo CPU
pixi run bench block_ciphers aes gpu # Mojo GPU
pixi run bench block_ciphers aes x86 # Mojo AES-NI
pixi run bench-rust aes # Rust AES-NI
Implementations
| Label | Description |
|---|---|
| Mojo CPU | Plain, baseline Mojo implementation |
| Mojo GPU | Mojo implementation running on the GPU |
| Mojo AES-NI | Mojo implementation using x86 AES-NI SIMD instructions via LLVM intrinsics |
| Rust AES-NI | RustCrypto AES crate (used by rustls), also backed by AES-NI on x86 |
AES-128
| Operation | Payload | Mojo CPU | Mojo GPU | Mojo AES-NI | Rust AES-NI |
|---|---|---|---|---|---|
| Encrypt | 4 KB | 668 µs | 107 µs | 0.62 µs | 0.62 µs |
| Encrypt | 8 KB | 1,199 µs | 121 µs | 1.21 µs | 1.23 µs |
| Encrypt | 16 KB | 2,659 µs | 193 µs | 2.45 µs | 2.57 µs |
| Decrypt | 4 KB | 1,117 µs | 134 µs | 0.62 µs | 0.61 µs |
| Decrypt | 8 KB | 2,157 µs | 232 µs | 1.24 µs | 1.24 µs |
| Decrypt | 16 KB | 4,575 µs | 304 µs | 2.47 µs | 2.59 µs |
AES-192
| Operation | Payload | Mojo CPU | Mojo GPU | Mojo AES-NI | Rust AES-NI |
|---|---|---|---|---|---|
| Encrypt | 4 KB | 745 µs | 119 µs | 0.75 µs | 0.76 µs |
| Encrypt | 8 KB | 1,629 µs | 131 µs | 1.60 µs | 1.49 µs |
| Encrypt | 16 KB | 3,095 µs | 190 µs | 3.14 µs | 2.96 µs |
| Decrypt | 4 KB | 1,347 µs | 152 µs | 0.73 µs | 0.77 µs |
| Decrypt | 8 KB | 2,725 µs | 221 µs | 1.54 µs | 1.48 µs |
| Decrypt | 16 KB | 5,585 µs | 390 µs | 2.97 µs | 3.01 µs |
AES-256
| Operation | Payload | Mojo CPU | Mojo GPU | Mojo AES-NI | Rust AES-NI |
|---|---|---|---|---|---|
| Encrypt | 4 KB | 890 µs | 119 µs | 0.86 µs | 0.88 µs |
| Encrypt | 8 KB | 1,783 µs | 139 µs | 1.77 µs | 1.77 µs |
| Encrypt | 16 KB | 3,783 µs | 206 µs | 3.48 µs | 3.48 µs |
| Decrypt | 4 KB | 1,581 µs | 167 µs | 0.86 µs | 0.91 µs |
| Decrypt | 8 KB | 3,334 µs | 249 µs | 1.71 µs | 1.78 µs |
| Decrypt | 16 KB | 6,649 µs | 385 µs | 3.61 µs | 3.53 µs |
The GPU pulls ahead of the plain CPU implementation — roughly 6–14× faster, with the advantage growing at larger payloads as the GPU gets better utilization. But AES-NI makes both of them irrelevant: it is ~170× faster than the GPU at 4 KB, a gap that narrows to ~79× at 16 KB as the GPU catches up.
The Mojo AES-NI and Rust numbers are at parity — within a few percent of each other.
One other detail worth noting: in the plain CPU implementation, decryption is consistently ~1.5–1.7× slower than encryption due to the InvMixColumns step. With AES-NI, that asymmetry disappears entirely — the hardware makes encrypt and decrypt symmetric.
AES CPU Hardware Acceleration
AES is the de facto encryption standard of the modern internet, and CPU designers have treated it as such. Intel added AES-NI in 2010, AMD followed in 2011, and ARM baked equivalent instructions into the ARMv8-A Cryptography Extensions — present on every Apple Silicon chip and all modern Cortex-A cores. At this point, any CPU you’re likely to run server software on has dedicated AES silicon.
The instructions map directly onto the AES round operations: x86 provides AESENC / AESENCLAST (and their decryption counterparts), while ARM splits the round across AESE + AESMC, giving the scheduler more flexibility.
Since Mojo compiles through LLVM, it exposes these via LLVM intrinsics: llvm.x86.aesni.aesenc on x86 and llvm.aarch64.crypto.aese on ARM. That’s the foundation the SIMD implementation in this experiment is built on.
Summary
This was a genuinely enjoyable learning experience — getting hands-on with Mojo, writing GPU kernels, and going deep enough into AES internals to build a native implementation from scratch. And the outcome on the Mojo side was satisfying: the SIMD-accelerated implementation lands at parity with RustCrypto’s, which for a relatively young language is a strong result.
The GPU vs CPU story was a humbling one — on a GTX 1050 Ti, CPU acceleration wins and it isn’t close. That said, the numbers already show the gap narrowing as payload grows, and on a modern high-end GPU the picture could look quite different. So I don’t consider this a GPU loss — I still believe in GPU superiority, just with better hardware.
The broader lesson I’m taking away: before you GPU-accelerate something, find out what your CPU can already do. Not every problem is compute-bound in the way that makes GPU parallelism worthwhile. Sometimes the silicon right there on your chip has already solved it.
What’s Next
All of this work lives in mojo-crypto — my ongoing project to implement cryptography algorithms natively in Mojo.
The goal is to keep building on this foundation: more algorithms, more GPU experiments,
and ultimately working towards a Mojo-native TLS library.
If you’re curious about Mojo, GPU computing, feel free to follow or participate!