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:

  1. Plain AES in Mojo — implement AES exactly as defined in FIPS 197, the original specification, to establish a clean baseline.

  2. GPU-accelerated AES — move the cipher onto the GPU.

  3. Plain CPU vs GPU — compare the naive Mojo CPU implementation against the GPU one, to measure the raw parallel speedup.

  4. Mojo CPU vs RustCrypto — benchmark both implementations against RustCrypto’s to understand where the Mojo implementation sits relative to production Rust.

  5. 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.

  6. Final comparison — put all four implementations side by side: plain Mojo CPU, GPU, RustCrypto, and Mojo with 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

LabelDescription
Mojo CPUPlain, baseline Mojo implementation
Mojo GPUMojo implementation running on the GPU
Mojo AES-NIMojo implementation using x86 AES-NI SIMD instructions via LLVM intrinsics
Rust AES-NIRustCrypto AES crate (used by rustls), also backed by AES-NI on x86

AES-128

OperationPayloadMojo CPUMojo GPUMojo AES-NIRust AES-NI
Encrypt4 KB668 µs107 µs0.62 µs0.62 µs
Encrypt8 KB1,199 µs121 µs1.21 µs1.23 µs
Encrypt16 KB2,659 µs193 µs2.45 µs2.57 µs
Decrypt4 KB1,117 µs134 µs0.62 µs0.61 µs
Decrypt8 KB2,157 µs232 µs1.24 µs1.24 µs
Decrypt16 KB4,575 µs304 µs2.47 µs2.59 µs

AES-192

OperationPayloadMojo CPUMojo GPUMojo AES-NIRust AES-NI
Encrypt4 KB745 µs119 µs0.75 µs0.76 µs
Encrypt8 KB1,629 µs131 µs1.60 µs1.49 µs
Encrypt16 KB3,095 µs190 µs3.14 µs2.96 µs
Decrypt4 KB1,347 µs152 µs0.73 µs0.77 µs
Decrypt8 KB2,725 µs221 µs1.54 µs1.48 µs
Decrypt16 KB5,585 µs390 µs2.97 µs3.01 µs

AES-256

OperationPayloadMojo CPUMojo GPUMojo AES-NIRust AES-NI
Encrypt4 KB890 µs119 µs0.86 µs0.88 µs
Encrypt8 KB1,783 µs139 µs1.77 µs1.77 µs
Encrypt16 KB3,783 µs206 µs3.48 µs3.48 µs
Decrypt4 KB1,581 µs167 µs0.86 µs0.91 µs
Decrypt8 KB3,334 µs249 µs1.71 µs1.78 µs
Decrypt16 KB6,649 µs385 µs3.61 µs3.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!