Skip to content

Add GPU-accelerated PLONK prover via ICICLE - #1800

Open
martun wants to merge 5 commits into
Consensys:masterfrom
martun:neon/gpu-plonk
Open

Add GPU-accelerated PLONK prover via ICICLE#1800
martun wants to merge 5 commits into
Consensys:masterfrom
martun:neon/gpu-plonk

Conversation

@martun

@martun martun commented Jul 27, 2026

Copy link
Copy Markdown

Description

Ports the Gnark_plonk_on_GPU work onto current gnark master.

  • New backend/accelerated/icicle/plonk/ package: full PLONK prover on GPU (NTT, MSM, quotient, linearization, KZG openings via ICICLE), selected with -tags=icicle. Like the accelerated Groth16 backend, the per-curve packages are generated from templates (backend/accelerated/icicle/internal/generator) for all four supported curves: BN254, BLS12-377, BLS12-381, BW6-761. Stage-timing breakdown available under ICICLE_STEP_PROFILE.
  • Solver output caching for the accelerated backends: a raw wire-value cache for the ICICLE PLONK prover (GNARK_RAW_SOLVER_CACHE) and a WithSolutionCachePath prover option for ICICLE Groth16. Cached data is validated against the circuit's wire count before use. Helpers are generated into constraint/<curve>/solution_cache.go; the Groth16 cache hook now lives in the groth16 template (all curves). No files owned by the main gnark generator are modified.
  • Blinding is on by default, matching the native prover. Setting GNARK_DISABLE_BLINDING trades zero-knowledge for a faster, deterministic GPU prover.
  • Depends on the extended icicle-gnark fork (batch inverse, poly eval, permutation supports, shard merge — see Add GPU vector/polynomial primitives needed for Gnark PLONK proving ingonyama-zk/icicle-gnark#4) via a local replace directive to ../icicle-gnark-extended. This PR should stay in draft until that dependency is merged and released, at which point the replace will be swapped for a released version.

Type of change

  • New feature (non-breaking change which adds functionality)

How has this been tested?

All behind -tags=icicle, on an RTX PRO 6000 (CUDA 13.0) against the icicle-gnark#4 libraries:

  • TestEndToEndLargeCircuit — 2^12-constraint circuit with a BSB22 commitment: accelerated setup → GPU prove → verify on all four curves (and on bn254: default blinding on, with GNARK_DISABLE_BLINDING, and with the raw solver cache save/load round-trip)
  • TestMarshalBN254 — proving-key marshal roundtrip; the same circuit proven natively and on GPU, both proofs verified
  • TestSetupBN254, TestKZGCommitmentICICLEMSMParity, TestGpuNTTInverseBatch_matchesCPUToCanonical
  • CPU: go test -short ./backend/plonk/... ./constraint/bn254/..., go vet, gofmt, builds with and without -tags=icicle

How has this been benchmarked?

  • sha256 circuit (~23M constraints), RTX 4090, ICICLE PLONK BN254 — see the "Solver Output Cache" section added to README.md for the stage-by-stage breakdown (~20% total prover saving from the raw solver cache alone)

Checklist:

  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works
  • I did not modify files generated from templates
  • golangci-lint does not output errors locally
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules (blocked on Add GPU vector/polynomial primitives needed for Gnark PLONK proving ingonyama-zk/icicle-gnark#4)

Note

Medium Risk
Stale or mismatched cache files could produce invalid proofs if validation is incomplete; caching is disabled for commitment circuits but raw-cache ZK caveats apply when blinding is off.

Overview
Adds solver output caching for ICICLE accelerated proving so repeated runs on the same circuit can skip the constraint solver.

README documents the feature: GNARK_RAW_SOLVER_CACHE for PLONK raw wire values, WithSolutionCachePath for Groth16 full R1CSSolution, ZK/BSB22 caveats, and benchmark numbers.

ICICLE code generator now also emits PLONK per-curve packages and generates constraint/<curve>/solution_cache.go from a new template (raw wire save/load, LRO derivation, SolveAndSaveRawValues, Groth16 solution save/load).

ICICLE Groth16 Prove (all curves, via template): when SolutionCachePath is set and there are no BSB22 commitments, loads a cached R1CSSolution after validating wire and constraint vector lengths; otherwise solves and atomically writes the cache.

Reviewed by Cursor Bugbot for commit 2150115. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread backend/accelerated/icicle/plonk/bn254/CLEANUP_PLAN_SUGGESTIONS.md Outdated
Comment thread backend/accelerated/icicle/groth16/bn254/icicle.go
@martun
martun force-pushed the neon/gpu-plonk branch 3 times, most recently from 9cfa352 to 355bc51 Compare July 27, 2026 14:07
…381, bw6-761)

Ports the Gnark_plonk_on_GPU work onto current gnark master:

- New backend/accelerated/icicle/plonk/ package: full PLONK prover on
  GPU (NTT, MSM, quotient, linearization, KZG openings via ICICLE),
  selected with -tags=icicle. Like the accelerated Groth16 backend,
  the per-curve packages are generated from templates by
  backend/accelerated/icicle/internal/generator for all four supported
  curves. Stage-timing breakdown under ICICLE_STEP_PROFILE.
- Solver output caching for the accelerated backends: raw wire-value
  cache for the ICICLE PLONK prover (GNARK_RAW_SOLVER_CACHE) and a
  WithSolutionCachePath prover option for ICICLE Groth16 (now part of
  the groth16 template, all curves). Cached data is validated against
  the circuit's wire count before use. Helpers are generated into
  constraint/<curve>/solution_cache.go; no gnark-generator files are
  modified.
- Blinding is on by default, matching the native prover. Setting
  GNARK_DISABLE_BLINDING trades zero-knowledge for a faster,
  deterministic GPU prover.
- Tests: KZG-MSM and NTT GPU/CPU parity, proving-key marshal
  roundtrip, accelerated setup, and a 2^12-constraint end-to-end
  prove/verify with a BSB22 commitment on every supported curve
  (all behind -tags=icicle).
- Depends on the extended icicle-gnark fork (batch inverse, poly eval,
  permutation supports, shard merge - see ingonyama-zk/icicle-gnark#4)
  via a local replace directive to ../icicle-gnark-extended.
Zero-knowledge: disable the raw solver cache (GNARK_RAW_SOLVER_CACHE) for
circuits with BSB22 commitments unless GNARK_DISABLE_BLINDING is set.
Replaying the cached commitment polynomials reuses their random blinding
rows across proofs, making Bsb22Commitments linkable and leaking linear
relations over the committed private wires once the polynomial has been
opened at more than two distinct zetas. Re-randomizing on load is not
possible because the commitment hash feeds back into a witness wire.

GPU resource lifecycle:
- Pair every CUDA stream in the NTT fan-outs with synchronize + destroy on
  all paths (previously ~50+ streams leaked per prove).
- Drain all NTT worker channels before returning on error so the caller
  cannot free device slices sibling workers are still writing to; return
  the scaling-vector pool slices on every exit path.
- Track outstanding pool slices (keyed by device pointer; CopyToDevice
  mutates the slice length field) and reclaim buffers abandoned by error
  paths in a new pool Shutdown at Prove teardown; mid-prove FreeAll keeps
  its idle-only semantics. Backstop hGPU / linearizedPolynomialGPU /
  blindedZCanonicalGPU / polyZLagrangeGPU there as well.
- Return the linearized polynomial to the pool instead of direct-freeing
  it (it is a pool buffer; the bypass surfaced as a double free once
  outstanding tracking existed).
- Convert device-goroutine and prover-stage panics (GPU allocation failure
  is the expected failure mode) into Prove errors via recover boundaries:
  devicePanicToError on error-channel closures, inline recover in runErr
  defers, a goStage wrapper around the errgroup stages, and error returns
  from setupDevicePointers.
- gpuEvaluateConstraints frees all temporaries via a cleanup defer on
  every path; blinding vectors now come from the temp pool.

Adds icicle-tagged regression tests: commitment-free cache round-trip,
cache skipped for BSB22 circuits with fresh commitments per prove, and
BSB22 replay under GNARK_DISABLE_BLINDING.

Verified on RTX PRO 6000 / CUDA 13: full -tags=icicle plonk suite passes
on all four curves; gofmt/go vet clean with and without the tag; the
checked-in generated files match the generator output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Reviewed by Cursor Bugbot for commit b1d7c37. Configure here.

martun and others added 3 commits July 27, 2026 19:57
- annotate the atomic cache writes with #nosec G703: the temp file comes
  from os.CreateTemp in the caller-chosen cache directory and the rename
  target is the caller-provided cache location (gosec taint analysis
  flags them as path traversal)
- replace deprecated fft.BitReverse with gnark-crypto utils.BitReverse
- merge a variable declaration with its assignment (S1021)

golangci-lint v2.10.1 (CI's version/config) now reports 0 issues on the
default build and 0 issues on ./backend/accelerated/icicle/plonk/... +
./constraint/... with -tags=icicle. Remaining icicle-tagged findings are
in the pre-existing accelerated groth16 code, untouched by this PR. Full
icicle plonk suite re-verified on GPU after the BitReverse swap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
devicePanicToError converts GPU allocation panics into Prove errors, but
closures that create a stream and only release it inside their finish /
finalize callback skipped that cleanup when the panic hit between
CreateStream and the callback — leaking one stream per recovered OOM,
which in a long-lived process eventually exhausts streams and fails
later proves.

makeFinisher now also returns a guard to defer right after stream
creation: if finish never ran, the guard synchronizes and destroys the
stream (a no-op on all non-panic paths). The four download closures that
released their stream at the tail are converted to the same
finish/guard pattern, and openZ's finalize and the eval closure's local
finish get equivalent flag-based guards (the latter also releasing its
owned eval buffer).

Verified on GPU: full -tags=icicle plonk suite 6/6 consecutive runs plus
the GNARK_DISABLE_BLINDING replay test; golangci-lint clean (default and
icicle-tagged); regenerated files match the generator output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The cached R1CSSolution was accepted after checking only the wire count,
but a circuit edit can keep the wire count while changing the constraint
count, leaving A/B/C the wrong size. That reaches computeH, whose
padding of Domain.Cardinality-len(a) panics when the stale vectors are
larger than the domain, or silently proves a stale assignment otherwise.
The load path now also requires len(A) == len(B) == len(C) ==
GetNbConstraints() and falls back to solving with a warning on any
mismatch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@martun martun changed the title Add GPU-accelerated PLONK prover (bn254) via ICICLE Add GPU-accelerated PLONK prover via ICICLE Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant