全部文章

GPU 加速的零知识证明生成

·
2025年9月7日
·
阅读约 5 分钟
·
••• 次浏览
·
••• 次点赞

本文译自英文原文。 阅读英文原文

GPU 加速的零知识证明生成

零知识证明让计算可以被验证,同时又不会暴露私密交易,为隐私保护交易和可扩展的链上验证提供了强大的基础构件。然而,证明生成的计算强度长期以来一直是一个大问题。ZK 系统中有一个众所周知的权衡,那就是“资源饥渴型证明者”:要得到一个体积小、验证快的证明,就需要大量的计算,而且往往相当慢,导致它无法用于实时场景。

现代 GPU 彻底改变了这个等式。借助其并行架构,GPU 把原本的根本性难题,变成了一个可以解决的工程问题。如今的 ZK 系统主要使用两种构造:zk-SNARKs 和 zk-STARKs,它们所依赖的核心运算都与 GPU 加速完美契合。

到底是什么这么耗时?

几乎所有的证明时间都花在了两种运算上:多标量乘法(Multi-Scalar Multiplication,MSM)和数论变换(Number Theoretic Transform,NTT)。

什么是多标量乘法?

MSM 计算的是一组经过标量缩放的椭圆曲线点之和:S=∑i=1nai⋅PiS = \sum_{i=1}^{n} a_i \cdot P_i。在 SNARKs 中,单单这一个运算就要吃掉高达 60-80% 的证明时间。它本身需要进行大量的随机内存访问来获取椭圆曲线点,这让内存带宽成为了这里的主要性能瓶颈。Pippenger 算法通过把标量分拣到不同的桶(bucket)中,来实现这项任务的并行化。

fn pippenger_msm<G: CurveGroup>(
    bases: &[G::Affine],
    scalars: &[G::ScalarField],
    window_size: usize,
) -> G {
    let num_buckets = 1 << window_size;
    let mut buckets = vec![G::zero(); num_buckets];
    
    for (base, scalar) in bases.iter().zip(scalars.iter()) {
        let bucket_index = extract_window(scalar, window_size);
        buckets[bucket_index] += base;
    }
    
    // bucket aggregation
    buckets.into_iter()
           .enumerate()
           .map(|(i, bucket)| bucket * G::ScalarField::from(i))
           .fold(G::zero(), |acc, term| acc + term)
}

所以,GPU 实现所做的,就是把这项工作分摊到数千个线程上,在 RTX 4090 上可以达到每秒超过 700 million 次点运算。

什么是数论变换?

另一方面,NTT 用来快速计算多项式乘法。与 MSM 的随机内存跳转不同,NTT 遵循一种可预测的访问模式,而这正是 GPU 所擅长的。

__global__ void ntt_radix2_kernel(
    uint64_t* coeffs,
    uint64_t* twiddles,
    uint32_t stage,
    uint32_t n
) {
    uint32_t tid = threadIdx.x + blockIdx.x * blockDim.x;
    uint32_t stride = 1 << stage;
    uint32_t m = n >> (stage + 1);
    
    if (tid >= m) return;
    
    uint32_t k = tid % stride;
    uint32_t j = tid / stride;
    uint32_t idx1 = j * (stride << 1) + k;
    uint32_t idx2 = idx1 + stride;
    
    uint64_t u = coeffs[idx1];
    uint64_t v = mod_mul(coeffs[idx2], twiddles[k], MODULUS);
    
    coeffs[idx1] = mod_add(u, v, MODULUS);
    coeffs[idx2] = mod_sub(u, v, MODULUS);
}

这种顺序访问模式让高性能的 NTT 实现能够达到 85-95% 的内存带宽利用率。

案例研究:CPU 与 GPU 的证明性能对比

假设 Alice 和 Bob 都在为各自的 Layer-2 链运营 zkEVM Sequencers。他们处理的是完全相同的交易批次,但使用的证明基础设施不同。

场景:zkEVM 区块证明

两个排序器都必须为一个包含 500 笔交易的区块生成证明,其中包括以下操作:

这会生成一个大约有 2262^{26} 个约束的电路,对于一个中等规模的 zkEVM 区块来说,这很典型。

Alice 的 CPU 方案

Alice 在一台高端 CPU 上运行她的证明者 - AMD EPYC 7763, 64 cores:

impl CpuZkEvmProver {
    fn prove_block(&self, block: &Block) -> ProofResult {
        let start = Instant::now();
        
        // circuit compilation
        let circuit = self.compile_block_circuit(block); // 45s
        
        // msm operations
        let witness_commitment = self.cpu_msm(&circuit.witnesses); // 180s
        
        // ntt operations
        let constraint_proof = self.cpu_ntt(&circuit.constraints); // 90s
        
        // final proof
        let proof = self.assemble_proof(witness_commitment, constraint_proof);
        
        ProofResult {
            proof,
            proving_time: start.elapsed(), // ~315s total
            memory_usage: 45_000_000_000, // 45GB peak
        }
    }
    
    fn cpu_msm(&self, witnesses: &[Fr]) -> G1Projective {
        witnesses.iter()
                .zip(self.setup.iter())
                .map(|(scalar, base)| base.mul(scalar))
                .fold(G1Projective::identity(), |acc, point| acc + point)
    }
}

Alice 的结果:

Bob 的 GPU 加速方案

Bob 使用的是一块搭配 ICICLE 加速的 RTX 4090:

use icicle_bls12_381::{curve::G1Projective, msm};
use icicle_core::msm::MSMConfig;
 
impl GpuZkEvmProver {
    fn prove_block(&self, block: &Block) -> ProofResult {
        let start = Instant::now();
        
        // circuit compilation
        let circuit = self.compile_block_circuit_gpu(block); // 12s
        
        // gpu msm - 16,384 cores parallel
        let witness_commitment = self.gpu_msm(&circuit.witnesses); // 8s
        
        // gpu ntt
        let constraint_proof = self.gpu_ntt(&circuit.constraints); // 3 seconds
        
        // final proof
        let proof = self.assemble_proof(witness_commitment, constraint_proof);
        
        ProofResult {
            proof,
            proving_time: start.elapsed(), // ~23s total
            memory_usage: 18_000_000_000, // 18GB peak
        }
    }
    
    fn gpu_msm(&self, witnesses: &[Fr]) -> Result<G1Projective> {
        let cfg = MSMConfig {
            precompute_factor: 8,
            c: 16,
            ..Default::default()
        };
        
        msm::msm(witnesses, &self.gpu_setup, &cfg)
    }
}

Bob 的结果:

结果对比

操作Alice (CPU)Bob (GPU)加速比
电路编译45s12s3.8×
MSM 运算180s8s22.5×
NTT 运算90s3s30×
区块证明总耗时315s23s13.7×

为什么这改变了一切

网络最终确定性:

每笔交易的成本

// one every 12s
const BLOCKS_PER_DAY: u32 = 7200;
 
impl ProvingEconomics {
    fn daily_capacity(&self, proving_time_seconds: u32) -> u32 {
        let daily_seconds = 24 * 60 * 60;
        daily_seconds / proving_time_seconds
    }
}

每日证明能力(Alice 对比 Bob):

Alice 要怎样才能跟上

Alice 需要 14 台 CPU 服务器,才能赶上 Bob 单块 GPU 的配置,这会导致:

内存是如何被读取的

性能差异从根本上源于这两台服务器访问内存的方式:

// alice's cpu msm - sequential, cache-bound
for (scalar, base) in witnesses.iter().zip(setup.iter()) {
    result += base * scalar; // cache miss every iteration
}
 
// bob's gpu msm - parallel, lightweight
__global__ void msm_kernel(scalar_t* scalars, point_t* bases, point_t* buckets) {
    int tid = blockIdx.x * blockDim.x + threadIdx.x;
    int bucket_idx = extract_window(scalars[tid], WINDOW_SIZE);
    atomicAdd(&buckets[bucket_idx], bases[tid]); // coalesced memory access
}

这个案例研究简单地说明了,为什么 GPU 加速已经成为生产环境 zkEVM 部署的必需品。Bob 的基础设施可以服务多出 13.7× 的用户,同时还能保持更快的最终确定性和更低的运营成本。

SNARK 与 STARK 的加速模式

SNARK 系统

Groth16 和 PLONK 都是以 MSM 为主的系统。以 Groth16 为例,它大约 80% 的证明时间都花在椭圆曲线类的运算上。而 PLONK 则把负载更均匀地分配在 MSM(~60%)和多项式运算(~30%)之间,因此需要对两者进行均衡的优化。

impl<E: Pairing> Groth16Prover<E> {
    fn create_proof(&self, circuit: &Circuit<E::ScalarField>) -> Result<Proof<E>> {
        let (a, b, c) = circuit.to_matrices();
        
        let g_a = self.params.alpha_g1.msm(&a);
        let g_b = self.params.beta_g2.msm(&b);  
        let g_c = self.params.gamma_g1.msm(&c);
        
        Ok(Proof { a: g_a, b: g_b, c: g_c })
    }
}

STARK 系统

STARKs 的主要开销在于多项式运算 NTTs 和哈希运算,比如 Poseidon 或 Blake3。由于 STARKs 中的内存访问模式非常规整,与 SNARK 的 MSM 运算中更偏随机的访问模式相比,它通常能更好地利用 GPU 的内存带宽。

impl<F: PrimeField> StarkProver<F> {
    fn prove(&self, trace: &ExecutionTrace<F>) -> StarkProof<F> {
        // polynomial interpolation via ntt
        let trace_poly = self.interpolate(trace);
        
        let composition_poly = self.evaluate_constraints(&trace_poly);
        
        let fri_proof = self.fri_commit(&composition_poly);
        
        StarkProof {
            trace_commitment: self.commit(&trace_poly),
            composition_commitment: self.commit(&composition_poly),
            fri_proof,
        }
    }
}

zkEVM 加速

由于 EVM 指令的复杂性和庞大的状态表示,zkEVM 面临着独特的挑战。证明过程需要为每一个 EVM 操作码和每一次状态转换生成约束。

证明 EVM

impl ZkEvmProver {
    fn prove_block(&self, block: &Block) -> ZkEvmProof {
        let mut circuit = EvmCircuit::new();
        
        for tx in &block.transactions {
            for (pc, opcode) in tx.trace.iter().enumerate() {
                match opcode {
                    OpCode::ADD => {
                        let (a, b) = self.stack.pop2();
                        let result = a + b;
                        circuit.add_constraint(a + b - result);
                        self.stack.push(result);
                    },
                    OpCode::SSTORE => {
                        let (key, value) = self.stack.pop2();
                        circuit.add_storage_constraint(key, value);
                        self.update_state_root(key, value);
                    },
                    // ...
                }
            }
        }
        
        self.prove_circuit(circuit)
    }
}

从 ADD 到 SSTORE,每一个操作码都必须被转换成一组多项式约束。

证明状态树

zkEVM 通常必须证明内存访问模式和存储更新的正确性。这一般会涉及用于验证状态根的 Merkle tree 操作

fn constrain_merkle_update(
    circuit: &mut Circuit,
    old_root: Variable,
    new_root: Variable,
    key: Variable,
    old_value: Variable,
    new_value: Variable,
    path: &[Variable],
) {
    let mut current = old_value;
    let mut updated = new_value;
    
    for (i, &sibling) in path.iter().enumerate() {
        let bit = circuit.extract_bit(key, i);
        
        let hash_input = circuit.select(bit, (sibling, current), (current, sibling));
        current = circuit.poseidon_hash(hash_input.0, hash_input.1);
        
        let updated_input = circuit.select(bit, (sibling, updated), (updated, sibling));
        updated = circuit.poseidon_hash(updated_input.0, updated_input.1);
    }
    
    circuit.assert_equal(current, old_root);
    circuit.assert_equal(updated, new_root);
}

对于状态转换验证中所需的大量哈希运算来说,GPU 加速通常就变得非常重要了

总结

GPU 本身并不只是加快了 ZK 证明的生成速度,它通过解决“资源饥渴型证明者”问题,从根本上改变了 ZK 证明的生成方式。现在,我们有了能在几秒内完成最终确定的 Layer 2,也有了在生产环境中运行的隐私 DeFi,这些在以前都是不太可能实现的。随着 zkEVMs 这类应用的电路复杂度不断增加,GPU 加速已经不再只是一种优化手段,而是生产环境部署的基本要求。

重要链接

•••
分享本文:

获取最新动态

将最新文章直接发送到你的收件箱。