Back to projects

05 / Computer Architecture · FPGA

Post-Quantum Cryptography Acceleration

Algorithm: ML-KEM-512 (NIST FIPS 203) Target: AMD Alveo U280 (UltraScale+) Tools: Vitis HLS 2023.1, Sniper 7.3 Course: Advanced Computer Organization, Tufts Date: Spring 2026

Measuring what an FPGA actually buys you on the key exchange that replaces RSA. An ML-KEM-512 accelerator ran a full key-encapsulation cycle 11 times faster than a simulated out-of-order CPU core and 43 times faster than an in-order one, at roughly a ninth of the clock rate and about half the power of the faster core. Then we tried to speed it up further, and mostly found out why we could not.

The problem

Nearly all public-key cryptography in use today rests on factoring large numbers (RSA) or the discrete logarithm over an elliptic curve (ECC), and Shor's algorithm removes the asymmetry both depend on once a large enough quantum computer exists. The threat does not wait for that hardware: traffic captured today can be stored until a machine exists that can open it, so anything with a long secrecy lifetime is already exposed. NIST's answer is ML-KEM, standardized in FIPS 203 and formerly known as CRYSTALS-Kyber. Its hardness comes from the Module Learning With Errors problem over polynomial rings with modulus 3329, believed to resist classical and quantum attacks alike.

ML-KEM was designed to be practical on a general-purpose CPU, and it is. That is a different question from whether it should run there. A data center terminating TLS performs key encapsulation constantly, and full adoption of the standard turns that into an enormous aggregate workload. Two operations dominate the cost: the Number-Theoretic Transform, which moves polynomials into a representation where multiplication is cheap, and the Keccak permutation inside the SHAKE hash functions, which expands a 32-byte seed into the public matrix. The NTT unrolls into layers of independent butterfly operations, and independence is exactly the property that makes a workload worth putting on an FPGA.

Two tracks, measured against each other

The accelerator side started from BSC LOCA's open-source Vitis HLS design rather than RTL of our own, synthesized under Vitis 2023.1 for part xcu280-fsvh2892-2L-e at a 300 MHz target clock. Cycle latency came from a full C/RTL co-simulation rather than a complete Vivado implementation. Driving the real implementation means wrapping the AXI interfaces and supplying clock and reset from a host design, and a first attempt without that wrapper asked for more than 2,000 I/O pins, which no real part provides. Since the comparison is cycles, resources, and power rather than board-level throughput, co-simulation gave the accuracy we needed and left enough time to try different HLS configurations.

The software side used the PQClean implementation of ML-KEM-512, chosen because it is a clean, widely used reference. We wrote a testbench that runs KeyGen, Encap, and Decap in series inside a marked region of interest, so the numbers cover the cryptography and nothing around it. That ran under Sniper 7.3 on two configurations: gainestown, a 2.66 GHz out-of-order Nehalem-class core, and silvermont, a 1 GHz in-order Atom-class core. Ten trials each, to catch run-to-run variance. I built and ran that software side myself, from the PQClean testbench through both Sniper configurations, and implemented both of the optimization experiments further down this page.

Both sides being simulated is a feature of the comparison, not a compromise in it. No thermal throttling, no OS scheduler, no background load, and neither side gets an advantage the other lacks. The absolute numbers would move on real silicon; the ratio between them is cleaner this way.

One asymmetry we had to state rather than hide: the HLS design exposes no separate KeyGen kernel. The expensive pieces of key generation, matrix expansion and centered-binomial sampling, happen inside Encapsulate, so its hardware cost is counted, but a KeyGen-to-KeyGen comparison does not exist. Totals still compare.

What the numbers said

Cycle counts flatter the FPGA, which runs at 300 MHz against 2.66 GHz, so the honest number is wall-clock time. On that measure the accelerator finishes 11 times sooner than the out-of-order core and 43 times sooner than the in-order one.

One full ML-KEM-512 key-encapsulation pipeline, measured the same way on all three.
Measure ML-KEM accelerator Gainestown, out-of-order Silvermont, in-order
Cycles 11,948 1,195,000 1,724,000
Wall-clock time 0.0398 ms 0.448 ms 1.724 ms
Average power 11.09 W 20.15 W 6.56 W
Bar chart of average power consumption: Gainestown CPU 20.15 W, Silvermont CPU 6.56 W, and three FPGA accelerator configurations at 11.09 W, 11.40 W, and 11.06 W
Average power across all five configurations. The accelerator draws about half of what the out-of-order CPU core does, and about 70% more than the in-order core.

Beating a fast core on both time and power is the result that matters for the data-center case; losing to the low-power in-order core on watts while finishing 43 times sooner is a trade most operators would take. Thermal and environmental parameters came from Vivado's U280 data, with heat sink and airflow set to what a rack deployment would look like.

The resource report is what set up the rest of the project. The design fits in 13% of the U280's LUTs, 3% of its DSPs, and 1% of its block RAM. Most of the chip sits unused, which is an invitation to spend it.

Two optimizations, one halved and neither faster

The obvious lever was unrolling, and the reference design had already pulled it. The NTT and inverse NTT layers and their inner butterfly loops were unrolled about as far as they go. What was left was the module rank K, the parameter that sets the security level. ML-KEM-512 uses K=2, so two polynomials run through the NTT sequentially. We duplicated the module inside NTT_Layer and called it once per polynomial, which cut per-layer latency from 521 cycles to 265, a 49% reduction. The same approach extends to K=3 and K=4 for the higher security levels, at the cost of the parameterized modularity we gave up.

Bar chart comparing NTT per-layer latency: 521 cycles for the original configuration and 265 cycles for the parallelized configuration
Per-layer NTT latency before and after module duplication. The stage got twice as fast; the program did not.

Total cycles for the full pipeline went from 11,948 to 11,967. Halving a stage bought nothing at all, and the resource cost was real: 21% more BRAM, 23% more DSPs, 10% more LUTs, plus a 2.8% increase in power. Whatever the NTT was waiting on, it was still waiting on it.

So the NTT was not on the critical path. Finding what was took reading the design, because the synthesis report does not label which stages run concurrently in a dataflow region. Going through the code alongside the per-stage latencies and initiation intervals pointed at Readmem, Split, basemul_montgomery, and basemul_add. The two basemul modules handle coefficient-wise multiplication in the NTT domain across 128 to 256 coefficients. Widening their buffers to move four coefficients per pass instead of one should have raised throughput.

It did not move the cycle counts either: 529 to 528 on basemul_montgomery, and 515 to 515 on basemul_add. The resource numbers confirm the architecture genuinely changed, with flip-flop usage on basemul_add going from 69 to 232, so the hardware is different and the schedule is not. The likeliest explanation is data starvation. Widening two modules accomplishes little when most of the ports feeding them still carry one coefficient at a time, which means the buffers would have to be widened through the whole design rather than at the modules that looked slowest. Confirming that needs a co-simulation of the widened build, and that run exhausted the RAM on the machines we had. It stays an unproven hypothesis rather than a conclusion.

The other find was a bug in the reference design: the AXI master port bundles had no stream depths specified. We set them manually to get data flowing at all. Synthesis and implementation take about two hours per configuration, which put a hard ceiling on how many variants we could try, and experimenting with those stream depths never made the cut.

What I took away

A faster block is not a faster program. I knew that as a formula before this project, but watching a 49% latency reduction produce a 0.16% change in total cycles is a different kind of knowing. The interesting part was that Amdahl's law was the wrong explanation. The NTT is not a small fraction of the work; it was already fast enough that something else in the dataflow architecture set the pace, and no amount of improvement there was going to show up at the top.

Most of the real skill turned out to be reading a design nobody on the team wrote. The tools report latency and resource usage per module and leave the causal structure to you, so locating a bottleneck meant cross-referencing initiation intervals against the code and forming a hypothesis that could be wrong. Ours was wrong twice.

A two-hour build loop changes how you plan, too. Batching configurations and deciding what a run would prove before starting it beats iterating by feel.

Neither optimization worked, and the paper says so. That was the most useful part to write.

Credits & sources

  • Co-author: Patrick Johnson (Tufts University). The design, benchmarking, and analysis were joint work throughout.
  • Accelerator source: BSC LOCA, PQC-Crystals-HLS-Accelerators, GitHub repository, 2024. The accelerator is their open-source design and we did not write the bulk of that code. Our work was building and co-simulating it for our target, measuring it against CPU baselines, and attempting two optimizations on top of it.
  • CPU baseline source: The PQClean Project, portable implementations of post-quantum cryptography.
  • Standard: NIST, Module-Lattice-Based Key-Encapsulation Mechanism Standard, FIPS 203 (Final), August 2024.

Figures are taken from our own paper, linked at the top of this page.