# Génération de preuves zero-knowledge accélérée par GPU

> Les preuves à divulgation nulle de connaissance permettent un calcul vérifiable sans exposer les transactions privées, ce qui en fait une brique puissante pour les transactions respectueuses de la vie privée et la vérification onchain à grande échelle.

Par Shodipo Ayomide, publié le 7 sept. 2025. Version web : https://blog.shodipoayomide.com/fr/gpu-accelerated-zero-knowledge-proving

![Génération de preuves zero-knowledge accélérée par GPU](https://blog.shodipoayomide.com/banners/GPU-Accelerated-Zero-Knowledge-Proving.png)

Les preuves à divulgation nulle de connaissance permettent un calcul vérifiable sans exposer les transactions privées, ce qui en fait une brique puissante pour les transactions respectueuses de la vie privée et la vérification onchain à grande échelle. Cependant, l'intensité de calcul de la génération de preuves a longtemps été un problème majeur. Un compromis bien connu des systèmes ZK est celui du « prover gourmand en ressources » : obtenir une preuve petite et rapide à vérifier demande beaucoup de calcul, et c'est souvent assez lent, ce qui bloque les usages en temps réel.

Les GPU modernes changent radicalement cette équation. En tirant parti de leur architecture parallèle, ils transforment ce qui était un problème fondamental en un problème d'ingénierie soluble. Les systèmes ZK d'aujourd'hui reposent principalement sur deux constructions : `zk-SNARKs` et `zk-STARKs`, qui s'appuient toutes deux sur des opérations de base qui se prêtent parfaitement à l'accélération GPU.

## Qu'est-ce qui prend vraiment autant de temps ?

Presque tout le temps de génération de preuve est consacré à deux opérations : la Multi-Scalar Multiplication (MSM) et la Number Theoretic Transform (NTT).

### Qu'est-ce que la Multi-Scalar Multiplication ?

La MSM calcule la somme de points de courbe elliptique multipliés par des scalaires : $S = \sum_{i=1}^{n} a_i \cdot P_i$. Cette seule opération consomme à elle seule `60-80%` du temps de génération de preuve dans les `SNARKs`. Elle nécessite en soi un grand nombre d'accès mémoire aléatoires pour récupérer les points de courbe elliptique, ce qui fait de la bande passante mémoire le principal problème de performance ici. L'algorithme de Pippenger parallélise cette tâche en répartissant les scalaires dans des buckets.

```rust
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)
}
```

Ce qui se passe dans l'implémentation GPU, c'est qu'elle répartit ce travail sur des milliers de threads, et atteint plus de `700 million` d'opérations sur des points par seconde sur une `RTX 4090`.

### Qu'est-ce que la Number Theoretic Transform ?

La NTT, de son côté, multiplie rapidement des polynômes, et contrairement aux sauts mémoire aléatoires de la MSM, la NTT suit un schéma prévisible, exactement ce pour quoi les GPU sont conçus.

```c
__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);
}
```

Ce schéma séquentiel permet aux implémentations NTT performantes d'atteindre `85-95%` de la bande passante mémoire.

## Étude de cas : performances de génération de preuves sur CPU vs GPU

Imaginez Alice et Bob, qui exploitent tous les deux des `zkEVM Sequencers` pour leurs chaînes `Layer-2` respectives. Ils traitent des lots de transactions identiques, mais utilisent une infra de génération de preuves différente.

### Scénario : génération de preuve d'un bloc zkEVM

Les deux séquenceurs doivent prouver un bloc contenant `500` transactions avec les opérations suivantes :

- **1 200** transferts ERC-20
- **300** swaps Uniswap V3
- **200** mises à jour de stockage
- **100** déploiements de contrats

Cela génère un circuit d'environ $2^{26}$ contraintes, ce qui est typique d'un bloc `zkEVM` de taille moyenne.

### L'approche CPU d'Alice

Alice fait tourner son prover sur un `CPU` haut de gamme - `AMD EPYC 7763, 64 cores` :

```rust
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)
    }
}
```

Les résultats d'Alice :

- **Temps de génération de preuve :** 5,25 minutes par bloc
- **Utilisation mémoire :** 45 Go en pic
- **Débit :** ~95 transactions prouvées par minute
- **Coût du matériel :** serveur à 8 000 $

### L'approche accélérée par GPU de Bob

Bob utilise une `RTX 4090` avec l'accélération `ICICLE` :

```rust
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)
    }
}
```

Les résultats de Bob :

- **Temps de génération de preuve :** 23 secondes par bloc
- **Utilisation mémoire :** 18 Go en pic
- **Débit :** ~1 300 transactions prouvées par minute
- **Coût du matériel :** 1 800 $ (RTX 4090 + serveur basique)

### Comparaison des résultats

| Opération | Alice (CPU) | Bob (GPU) | Accélération |
|-----------|-------------|-----------|---------|
| Compilation du circuit | 45s | 12s | 3,8× |
| Opérations MSM | 180s | 8s | **22,5×** |
| Opérations NTT | 90s | 3s | **30×** |
| **Preuve complète du bloc** | **315s** | **23s** | **13,7×** |

### Pourquoi ça change tout

Finalité du réseau :

- Réseau d'Alice : finalité de 5,25 minutes pour les confirmations L1
- Réseau de Bob : finalité de 23 secondes pour les confirmations L1

#### Coût par transaction

```rust
// 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
    }
}
```

Capacité quotidienne de génération de preuves (Alice vs Bob) :

- Alice peut prouver : 274 blocs par jour, soit 137 000 transactions
- Bob peut prouver : 3 756 blocs par jour, soit 1 878 000 transactions

#### Ce dont Alice a besoin pour rivaliser

Alice aurait besoin de 14 CPU pour égaler la configuration à un seul GPU de Bob, ce qui entraîne :

- Un coût matériel 14× plus élevé - 112 000 dollars contre 8 000 dollars
- Une consommation électrique 14× plus élevée - 14 kW contre 1 kW
- Une surcharge de coordination distribuée complexe

### Comment la mémoire est lue

La différence de performance vient fondamentalement de la façon dont les deux serveurs accèdent à la mémoire :

```rust
// 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
}
```

Cette étude de cas montre simplement pourquoi l'accélération GPU est devenue indispensable pour les déploiements `zkEVM` en production. L'infra de Bob peut servir `13.7×` plus d'utilisateurs tout en conservant une finalité plus rapide et des coûts opérationnels plus bas.

## Schémas d'accélération SNARK vs. STARK

### Systèmes SNARK

`Groth16` et `PLONK` sont dominés par la MSM. `Groth16`, par exemple, consacre environ `80%` de son temps de génération de preuve à des opérations sur courbes elliptiques. `PLONK`, en revanche, répartit la charge de façon plus équilibrée entre la MSM `~60%` et les opérations polynomiales `~30%`, ce qui demande une optimisation équilibrée des deux.

```rust
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 })
    }
}
```

### Systèmes STARK

Les `STARKs` sont dominés par l'arithmétique polynomiale `NTTs` et le hachage, par exemple `Poseidon` ou `Blake3`. Du fait de la régularité des schémas d'accès mémoire dans les `STARKs`, ils exploitent souvent mieux la bande passante mémoire du GPU que les schémas d'accès plus aléatoires des opérations MSM des `SNARK`.

```rust
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,
        }
    }
}
```

## Accélération des zkEVM

Les zkEVM posent des défis particuliers en raison de la complexité des instructions `EVM` et de la taille des représentations d'état. Le processus de génération de preuve consiste à générer des contraintes pour chaque opcode `EVM` et chaque transition d'état.

### Prouver l'EVM

```rust
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)
    }
}
```

Chaque opcode, de **ADD** à **SSTORE**, doit être traduit en un ensemble de contraintes polynomiales.

### Prouver l'arbre d'état

Les zkEVM doivent en général toujours prouver la validité des schémas d'accès mémoire et des mises à jour du stockage. Cela implique généralement des opérations sur un `Merkle tree` pour la vérification de la racine d'état

```rust
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);
}
```

L'accélération GPU devient généralement importante pour les nombreuses opérations de hachage nécessaires à la vérification des transitions d'état

## Conclusion

Les GPU n'ont pas seulement accéléré la génération de preuves ZK, ils ont en gros changé la manière de les générer en résolvant le problème du « prover gourmand en ressources ». Nous avons maintenant des Layer 2 qui atteignent la finalité en quelques secondes et de la DeFi privée qui tourne en production, ce qui n'était pas vraiment possible auparavant. À mesure que la complexité des circuits augmente avec des applications comme les `zkEVMs`, l'accélération GPU n'est plus seulement une question d'optimisation, c'est une exigence fondamentale pour les déploiements en production.

### Liens essentiels

  * **ICICLE :** Une bibliothèque GPU open source d'Ingonyama pour accélérer les primitives ZK. [GitHub](https://github.com/ingonyama-zk/icicle)
  * **Arkworks :** Un écosystème en Rust pour concevoir et implémenter des systèmes de preuves ZK. [Site web](https://arkworks.rs/)
  * **Halo2 :** Un système de preuve basé sur PLONK, développé par la Zcash Foundation et largement utilisé dans des projets comme le PSE zkEVM. [GitHub](https://github.com/zcash/halo2)
  * **ZPrize :** Une compétition du secteur axée sur l'accélération des performances ZK, côté logiciel comme côté matériel. [Site web](https://www.zprize.io/)
  * « zk-SNARKs: A Primer » de Vitalik Buterin : Une excellente explication de haut niveau de la façon dont les zk-SNARKs sont construits. [Article](https://vitalik.eth.limo/general/2021/01/26/snarks.html)
  * « Anatomy of a STARK » de StarkWare : Une plongée dans les composantes mathématiques des zk-STARKs.[Article](https://medium.com/starkware/stark-math-the-journey-begins-51bd2b063c71)
  * « Introducing ICICLE: An Open-Source GPU Library for Zero Knowledge Acceleration » d'Ingonyama [Article](https://www.ingonyama.com/post/introducing-icicle-an-open-source-gpu-library-for-zero-knowledge-acceleration)
  * « Blitzar, A novel opensource zero-knowledge proof for SQL operations » de Space and Time. Blitzar fournit un framework plus rapide et plus robuste pour construire des preuves ZK accélérées par GPU [Github](https://github.com/spaceandtimefdn/blitzar)
