PR: triton-vm — GPU acceleration hooks
Target repo: TritonVM/triton-vm
Target branch: main
Title: feat: add GpuAccelerator trait for pluggable GPU backends
Motivation
Triton VM's prover is CPU-bound at three hot paths that map cleanly onto GPU parallelism: Goldilocks NTT/iNTT (polynomial interpolation, ~40-50% of prove time), Tip5 batch hashing (Merkle leaf construction, ~20-30%), and FRI folding. There is currently no extension point that lets an external crate supply GPU implementations of these operations without forking or patching triton-vm at build time.
This PR adds a zero-overhead pluggable dispatch mechanism: a GpuAccelerator
trait registered once via a OnceLock, with CPU defaults for every
method. If no accelerator is registered the prover behaves identically to
today. When one is registered, the seven hot dispatch points delegate to
it.
Measured benefit (M1 Max, Metal via wgpu): 4.3× proving speedup on representative programs, 24 MH/s Tip5 mining throughput.
Changes
New file: src/gpu.rs
//! GPU acceleration hooks for triton-vm operations.
//!
//! Register a GPU accelerator via [`set_gpu_accelerator`] before proving.
//! The prover dispatches Tip5 hashing, NTT, iNTT, Merkle tree construction,
//! FRI folding, and GEMV to the GPU when an accelerator is registered.
//!
//! All methods have correct CPU fallback implementations — if no accelerator
//! is registered, the prover behaves identically to the unpatched version.
use OnceLock;
use intt;
use *;
use MerkleTree;
/// Trait for GPU-accelerated operations used during STARK proving.
///
/// Implement this trait to dispatch hot prover paths to GPU hardware.
/// Every method has a default CPU implementation — override only the
/// paths your backend accelerates.
///
/// Acceleration targets (in order of prove-time impact):
/// 1. `intt_bfe` / `intt_xfe` — polynomial interpolation, ~40-50%
/// 2. `hash_varlen_batch` — Merkle leaf hashing, ~20-30%
/// 3. `merkle_tree` — internal node hashing, ~10-15%
/// 4. `fri_fold` — FRI query folding
/// 5. `gemv_bfe` / `gemv_xfe` — weighted column sums
/// 6. `ntt_bfe` / `ntt_xfe` — forward NTT (smaller share)
static GPU_ACCELERATOR: = new;
/// Register a GPU accelerator for use during proving.
///
/// Must be called before the first `prove()` invocation. Returns `Err`
/// containing the accelerator if one was already registered.
/// Get the registered GPU accelerator, if any.
src/lib.rs — export the module
pub mod fri;
+pub mod gpu;
pub mod instruction;
twenty-first: MerkleTree::from_nodes constructor
GPU backends build the tree node array on the GPU and need to wrap it
without re-hashing on CPU. Add to MerkleTree:
/// Construct a MerkleTree from a pre-computed flat node array.
///
/// The caller guarantees the nodes are valid: root at index 1,
/// leaves at indices [n..2n), all internal nodes correctly hashed.
/// Used by GPU backends that compute the full tree on device.
src/stark.rs — 4 dispatch points
Import (top of file):
+use crate::gpu;
Tip5 hash dispatch (inside fn prove, profiler block "hash rows of quotient segments"):
- let hash_row = |row: ArrayView1<_>| {
- let row_as_bfes = row.iter().map(interpret_xfe_as_bfes).concat();
- Tip5::hash_varlen(&row_as_bfes)
- };
- let quotient_segments_rows = fri_domain_quotient_segment_codewords
- .axis_iter(ROW_AXIS)
- .into_par_iter();
- let fri_domain_quotient_segment_codewords_digests =
- quotient_segments_rows.map(hash_row).collect::<Vec<_>>();
+ let fri_domain_quotient_segment_codewords_digests =
+ if let Some(gpu) = gpu::gpu_accelerator() {
+ let rows: Vec<Vec<BFieldElement>> = fri_domain_quotient_segment_codewords
+ .axis_iter(ROW_AXIS)
+ .map(|row| row.iter().flat_map(interpret_xfe_as_bfes).collect())
+ .collect();
+ let row_refs: Vec<&[BFieldElement]> =
+ rows.iter().map(|r| r.as_slice()).collect();
+ gpu.hash_varlen_batch(&row_refs)
+ } else {
+ let hash_row = |row: ArrayView1<_>| {
+ let row_as_bfes = row.iter().map(interpret_xfe_as_bfes).concat();
+ Tip5::hash_varlen(&row_as_bfes)
+ };
+ fri_domain_quotient_segment_codewords
+ .axis_iter(ROW_AXIS)
+ .into_par_iter()
+ .map(hash_row)
+ .collect::<Vec<_>>()
+ };
Quotient Merkle tree dispatch:
- let quot_merkle_tree =
- MerkleTree::par_new(&fri_domain_quotient_segment_codewords_digests)?;
+ let quot_merkle_tree = if let Some(gpu) = gpu::gpu_accelerator() {
+ gpu.merkle_tree(&fri_domain_quotient_segment_codewords_digests)
+ } else {
+ MerkleTree::par_new(&fri_domain_quotient_segment_codewords_digests)?
+ };
iNTT dispatch (profiler block "poly interpolate"):
- main_table.trace_table_mut().axis_iter_mut(COL_AXIS)
- .into_par_iter()
- .for_each(|mut col| intt(col.as_slice_mut().unwrap()));
- aux_table.trace_table_mut().axis_iter_mut(COL_AXIS)
- .into_par_iter()
- .for_each(|mut col| intt(col.as_slice_mut().unwrap()));
+ if let Some(gpu) = gpu::gpu_accelerator() {
+ let mut trace = main_table.trace_table_mut();
+ (0..trace.ncols()).for_each(|c| {
+ gpu.intt_bfe(trace.column_mut(c).into_slice_memory_order().unwrap());
+ });
+ let mut trace = aux_table.trace_table_mut();
+ (0..trace.ncols()).for_each(|c| {
+ gpu.intt_xfe(trace.column_mut(c).into_slice_memory_order().unwrap());
+ });
+ } else {
+ main_table.trace_table_mut().axis_iter_mut(COL_AXIS)
+ .into_par_iter()
+ .for_each(|mut col| intt(col.as_slice_mut().unwrap()));
+ aux_table.trace_table_mut().axis_iter_mut(COL_AXIS)
+ .into_par_iter()
+ .for_each(|mut col| intt(col.as_slice_mut().unwrap()));
+ }
Forward NTT dispatch (profiler block "restore original trace"):
- main_table.trace_table_mut().axis_iter_mut(COL_AXIS)
- .into_par_iter()
- .for_each(|mut col| ntt(col.as_slice_mut().unwrap()));
- aux_table.trace_table_mut().axis_iter_mut(COL_AXIS)
- .into_par_iter()
- .for_each(|mut col| ntt(col.as_slice_mut().unwrap()));
+ if let Some(gpu) = gpu::gpu_accelerator() {
+ let mut trace = main_table.trace_table_mut();
+ (0..trace.ncols()).for_each(|c| {
+ gpu.ntt_bfe(trace.column_mut(c).into_slice_memory_order().unwrap());
+ });
+ let mut trace = aux_table.trace_table_mut();
+ (0..trace.ncols()).for_each(|c| {
+ gpu.ntt_xfe(trace.column_mut(c).into_slice_memory_order().unwrap());
+ });
+ } else {
+ main_table.trace_table_mut().axis_iter_mut(COL_AXIS)
+ .into_par_iter()
+ .for_each(|mut col| ntt(col.as_slice_mut().unwrap()));
+ aux_table.trace_table_mut().axis_iter_mut(COL_AXIS)
+ .into_par_iter()
+ .for_each(|mut col| ntt(col.as_slice_mut().unwrap()));
+ }
src/table/master_table.rs — 3 dispatch points
Import:
+use crate::gpu;
Tip5 hash dispatch (Merkle leaf hashing):
- let all_digests = fri_domain_table
- .axis_iter(ROW_AXIS)
- .into_par_iter()
- .map(|row| row.to_slice().unwrap())
- .map(Self::Field::bfe_slice)
- .map(Tip5::hash_varlen)
- .collect();
+ let all_digests = if let Some(gpu) = gpu::gpu_accelerator() {
+ let rows: Vec<Vec<BFieldElement>> = fri_domain_table
+ .axis_iter(ROW_AXIS)
+ .map(|row| Self::Field::bfe_slice(row.to_slice().unwrap()).to_vec())
+ .collect();
+ let row_refs: Vec<&[BFieldElement]> =
+ rows.iter().map(|r| r.as_slice()).collect();
+ gpu.hash_varlen_batch(&row_refs)
+ } else {
+ fri_domain_table
+ .axis_iter(ROW_AXIS)
+ .into_par_iter()
+ .map(|row| row.to_slice().unwrap())
+ .map(Self::Field::bfe_slice)
+ .map(Tip5::hash_varlen)
+ .collect()
+ };
Merkle tree dispatch:
- let merkle_tree = MerkleTree::par_new(&hashed_rows).unwrap();
+ let merkle_tree = if let Some(gpu) = gpu::gpu_accelerator() {
+ gpu.merkle_tree(&hashed_rows)
+ } else {
+ MerkleTree::par_new(&hashed_rows).unwrap()
+ };
GEMV dispatch (weighted_sum_of_columns):
- let weighted_sum = self
- .trace_table()
- .axis_iter(ROW_AXIS)
- .into_par_iter()
- .map(|row| row.iter().zip_eq(&weights).map(|(&r, &w)| r * w).sum())
- .collect::<Vec<_>>();
+ let weighted_sum = if let Some(gpu) = gpu::gpu_accelerator() {
+ // Transpose column-major trace into row-major flat buffer for GPU.
+ let trace = self.trace_table();
+ let (nrows, ncols) = (trace.nrows(), trace.ncols());
+ let weights_slice = weights.as_slice().unwrap();
+ // dispatch based on field type via monomorphisation
+ gpu_weighted_sum(gpu, &trace, nrows, ncols, weights_slice)
+ } else {
+ self.trace_table()
+ .axis_iter(ROW_AXIS)
+ .into_par_iter()
+ .map(|row| row.iter().zip_eq(&weights).map(|(&r, &w)| r * w).sum())
+ .collect::<Vec<_>>()
+ };
src/fri.rs — 1 dispatch point
Import:
+use crate::gpu;
FRI fold dispatch (in split_and_fold):
fn split_and_fold(&self, folding_challenge: XFieldElement) -> Vec<XFieldElement> {
+ let domain_points = self.domain.values();
+ let domain_point_inverses = BFieldElement::batch_inversion(domain_points);
+
+ if let Some(gpu) = gpu::gpu_accelerator() {
+ return gpu.fri_fold(&self.codeword, &domain_point_inverses, folding_challenge);
+ }
+
let one = xfe!(1);
let two_inverse = xfe!(2).inverse();
- let domain_points = self.domain.values();
- let domain_point_inverses = BFieldElement::batch_inversion(domain_points);
...
Properties preserved
- Zero cost when unused: all dispatch points are
if let Some(gpu) = gpu_accelerator()— no overhead when no accelerator is registered.OnceLockreads are a single atomic load after the first call. - Correct CPU fallback: every
elsebranch is identical to the current code path. Behaviour without a registered accelerator is bit-for-bit unchanged. - No new required dependencies: the
GpuAcceleratortrait uses only types already in scope (BFieldElement,XFieldElement,Digest,MerkleTree). - Thread safe:
OnceLock<Box<dyn GpuAccelerator>>withSend + Syncbound.
Testing
The existing test suite exercises all patched code paths with no accelerator registered (CPU fallback). A GPU accelerator that returns wrong results would cause proof verification failure — the prover is self-checking.
A reference wgpu implementation exists at https://github.com/cyberia-to/trisha and passes all 15 integration tests with Metal on macOS.