mirror of
https://github.com/pseXperiments/icicle.git
synced 2026-01-11 00:18:08 -05:00
Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
621676bd41 | ||
|
|
badb8c5d68 | ||
|
|
1300434bbe | ||
|
|
6a67893773 | ||
|
|
0cb0b49be9 | ||
|
|
8411ed1451 | ||
|
|
53f34aade5 | ||
|
|
aacec3f72f | ||
|
|
a8fa05d0e3 | ||
|
|
877018c84c | ||
|
|
91ac666e06 | ||
|
|
46e6c20440 | ||
|
|
ea71faf1fa | ||
|
|
7fd9ed1b49 | ||
|
|
2d4059c61f | ||
|
|
e4eda8938d | ||
|
|
fb707d5350 | ||
|
|
73cd4c0a99 | ||
|
|
5516320ad7 | ||
|
|
6336e74d5a | ||
|
|
a4b1eb3de9 | ||
|
|
31083463be | ||
|
|
b908053c0c | ||
|
|
279cdc66e0 | ||
|
|
81644fc28c | ||
|
|
17732ea013 | ||
|
|
9e057c835d | ||
|
|
f08b5bb49d | ||
|
|
29da36d7be | ||
|
|
4fef542346 | ||
|
|
f812f071fa | ||
|
|
2b07513310 | ||
|
|
7831f7bd0f | ||
|
|
de25b6e203 | ||
|
|
69383e6c73 | ||
|
|
c305aade5d | ||
|
|
87bdf04a19 | ||
|
|
e152977843 | ||
|
|
3d01c09c82 | ||
|
|
8936d9c800 | ||
|
|
af9ec76506 | ||
|
|
cdd99d2a46 | ||
|
|
3e551762c0 | ||
|
|
37c22e81e7 | ||
|
|
69e73ffa3e | ||
|
|
512e1ca372 | ||
|
|
e19a869691 | ||
|
|
9c55d888ae | ||
|
|
18f51de56c | ||
|
|
33b1f3c794 | ||
|
|
3a276ef23c | ||
|
|
8e62bde16d | ||
|
|
417ca77f61 | ||
|
|
8911a32135 | ||
|
|
c6f6e61d60 | ||
|
|
4e3aa63d2f | ||
|
|
db298aefc1 | ||
|
|
19a9b76d64 | ||
|
|
1e343f17a3 | ||
|
|
cfea6ebb3b | ||
|
|
76a82bf88e | ||
|
|
b8310d577e | ||
|
|
49c7fa4b28 | ||
|
|
02059fcfaa | ||
|
|
4496520a10 | ||
|
|
88a6966a4b | ||
|
|
9c1afe8a44 | ||
|
|
972b924bc0 | ||
|
|
230a1da512 | ||
|
|
940b283c47 | ||
|
|
e0412183fd | ||
|
|
9da52bc09f | ||
|
|
49079d0d2a | ||
|
|
094683d291 | ||
|
|
c30e333819 | ||
|
|
2905d2a469 | ||
|
|
732ee51552 | ||
|
|
14997566ff | ||
|
|
a56435d2e8 | ||
|
|
41294b12e0 | ||
|
|
6134cfe177 | ||
|
|
34f0212c0d | ||
|
|
f6758f3447 | ||
|
|
e2ad621f97 | ||
|
|
bdc3da98d6 | ||
|
|
36e288c1fa | ||
|
|
f8d15e2613 | ||
|
|
14b39b57cc | ||
|
|
999167afe1 | ||
|
|
ff374fcac7 | ||
|
|
7265d18d48 | ||
|
|
a1dc0539ce | ||
|
|
cda806ff0c |
@@ -3,3 +3,4 @@ crate
|
||||
lmit
|
||||
mut
|
||||
uint
|
||||
dout
|
||||
12
.github/changed-files.yml
vendored
12
.github/changed-files.yml
vendored
@@ -3,8 +3,11 @@ golang:
|
||||
- wrappers/golang/**/*.h
|
||||
- wrappers/golang/**/*.tmpl
|
||||
- go.mod
|
||||
- .github/workflows/golang.yml
|
||||
rust:
|
||||
- wrappers/rust/**/*
|
||||
- '!wrappers/rust/README.md'
|
||||
- .github/workflows/rust.yml
|
||||
cpp:
|
||||
- icicle/**/*.cu
|
||||
- icicle/**/*.cuh
|
||||
@@ -12,4 +15,11 @@ cpp:
|
||||
- icicle/**/*.hpp
|
||||
- icicle/**/*.c
|
||||
- icicle/**/*.h
|
||||
- icicle/CMakeLists.txt
|
||||
- icicle/CMakeLists.txt
|
||||
- .github/workflows/cpp_cuda.yml
|
||||
- icicle/cmake/Common.cmake
|
||||
- icicle/cmake/CurvesCommon.cmake
|
||||
- icicle/cmake/FieldsCommon.cmake
|
||||
examples:
|
||||
- examples/**/*
|
||||
- .github/workflows/examples.yml
|
||||
|
||||
5
.github/workflows/check-changed-files.yml
vendored
5
.github/workflows/check-changed-files.yml
vendored
@@ -12,6 +12,9 @@ on:
|
||||
cpp_cuda:
|
||||
description: "Flag for if C++/CUDA files changed"
|
||||
value: ${{ jobs.check-changed-files.outputs.cpp_cuda }}
|
||||
examples:
|
||||
description: "Flag for if example files changed"
|
||||
value: ${{ jobs.check-changed-files.outputs.examples }}
|
||||
|
||||
jobs:
|
||||
check-changed-files:
|
||||
@@ -21,6 +24,7 @@ jobs:
|
||||
golang: ${{ steps.changed_files.outputs.golang }}
|
||||
rust: ${{ steps.changed_files.outputs.rust }}
|
||||
cpp_cuda: ${{ steps.changed_files.outputs.cpp_cuda }}
|
||||
examples: ${{ steps.changed_files.outputs.examples }}
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
@@ -37,3 +41,4 @@ jobs:
|
||||
echo "golang=${{ steps.changed-files-yaml.outputs.golang_any_modified }}" >> "$GITHUB_OUTPUT"
|
||||
echo "rust=${{ steps.changed-files-yaml.outputs.rust_any_modified }}" >> "$GITHUB_OUTPUT"
|
||||
echo "cpp_cuda=${{ steps.changed-files-yaml.outputs.cpp_any_modified }}" >> "$GITHUB_OUTPUT"
|
||||
echo "examples=${{ steps.changed-files-yaml.outputs.examples_any_modified }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
2
.github/workflows/codespell.yml
vendored
2
.github/workflows/codespell.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- V2
|
||||
|
||||
jobs:
|
||||
spelling-checker:
|
||||
|
||||
59
.github/workflows/cpp_cuda.yml
vendored
59
.github/workflows/cpp_cuda.yml
vendored
@@ -4,11 +4,11 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- V2
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- V2
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -29,24 +29,63 @@ jobs:
|
||||
if: needs.check-changed-files.outputs.cpp_cuda == 'true'
|
||||
run: if [[ $(find ./ \( -path ./icicle/build -prune -o -path ./**/target -prune -o -path ./examples -prune \) -iname *.h -or -iname *.cuh -or -iname *.cu -or -iname *.c -or -iname *.cpp | xargs clang-format --dry-run -ferror-limit=1 -style=file 2>&1) ]]; then echo "Please run clang-format"; exit 1; fi
|
||||
|
||||
test-linux:
|
||||
test-linux-curve:
|
||||
name: Test on Linux
|
||||
runs-on: [self-hosted, Linux, X64, icicle]
|
||||
needs: [check-changed-files, check-format]
|
||||
strategy:
|
||||
matrix:
|
||||
curve: [bn254, bls12_381, bls12_377, bw6_761]
|
||||
curve:
|
||||
- name: bn254
|
||||
build_args: -DG2=ON -DECNTT=ON
|
||||
- name: bls12_381
|
||||
build_args: -DG2=ON -DECNTT=ON
|
||||
- name: bls12_377
|
||||
build_args: -DG2=ON -DECNTT=ON
|
||||
- name: bw6_761
|
||||
build_args: -DG2=ON -DECNTT=ON
|
||||
- name: grumpkin
|
||||
build_args:
|
||||
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
- name: Build
|
||||
- name: Build curve
|
||||
working-directory: ./icicle
|
||||
if: needs.check-changed-files.outputs.cpp_cuda == 'true'
|
||||
run: |
|
||||
mkdir -p build
|
||||
cmake -DBUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Release -DCURVE=${{ matrix.curve }} -DG2_DEFINED=ON -S . -B build
|
||||
cmake --build build
|
||||
- name: Run C++ Tests
|
||||
working-directory: ./icicle/build
|
||||
mkdir -p build && rm -rf build/*
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=ON -DCURVE=${{ matrix.curve.name }} ${{ matrix.curve.build_args }} -S . -B build
|
||||
cmake --build build -j
|
||||
- name: Run C++ curve Tests
|
||||
working-directory: ./icicle/build/tests
|
||||
if: needs.check-changed-files.outputs.cpp_cuda == 'true'
|
||||
run: ctest
|
||||
|
||||
test-linux-field:
|
||||
name: Test on Linux
|
||||
runs-on: [self-hosted, Linux, X64, icicle]
|
||||
needs: [check-changed-files, check-format]
|
||||
strategy:
|
||||
matrix:
|
||||
field:
|
||||
- name: babybear
|
||||
build_args: -DEXT_FIELD=ON
|
||||
- name: stark252
|
||||
build_args: -DEXT_FIELD=OFF
|
||||
- name: m31
|
||||
build_args: -DEXT_FIELD=ON
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
- name: Build field
|
||||
working-directory: ./icicle
|
||||
if: needs.check-changed-files.outputs.cpp_cuda == 'true'
|
||||
run: |
|
||||
mkdir -p build && rm -rf build/*
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=ON -DFIELD=${{ matrix.field.name }} ${{ matrix.field.build_args }} -S . -B build
|
||||
cmake --build build -j
|
||||
- name: Run C++ field Tests
|
||||
working-directory: ./icicle/build/tests
|
||||
if: needs.check-changed-files.outputs.cpp_cuda == 'true'
|
||||
run: ctest
|
||||
8
.github/workflows/examples.yml
vendored
8
.github/workflows/examples.yml
vendored
@@ -11,11 +11,11 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- V2
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- V2
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
- name: c++ examples
|
||||
working-directory: ./examples/c++
|
||||
if: needs.check-changed-files.outputs.cpp_cuda == 'true'
|
||||
if: needs.check-changed-files.outputs.cpp_cuda == 'true' || needs.check-changed-files.outputs.examples == 'true'
|
||||
run: |
|
||||
# loop over all directories in the current directory
|
||||
for dir in $(find . -mindepth 1 -maxdepth 1 -type d); do
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
done
|
||||
- name: Rust examples
|
||||
working-directory: ./examples/rust
|
||||
if: needs.check-changed-files.outputs.rust == 'true'
|
||||
if: needs.check-changed-files.outputs.rust == 'true' || needs.check-changed-files.outputs.examples == 'true'
|
||||
run: |
|
||||
# loop over all directories in the current directory
|
||||
for dir in $(find . -mindepth 1 -maxdepth 1 -type d); do
|
||||
|
||||
95
.github/workflows/golang.yml
vendored
95
.github/workflows/golang.yml
vendored
@@ -4,11 +4,11 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- V2
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- V2
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -33,13 +33,23 @@ jobs:
|
||||
if: needs.check-changed-files.outputs.golang == 'true'
|
||||
run: if [[ $(go list ./... | xargs go fmt) ]]; then echo "Please run go fmt"; exit 1; fi
|
||||
|
||||
build-linux:
|
||||
name: Build on Linux
|
||||
build-curves-linux:
|
||||
name: Build and test curves on Linux
|
||||
runs-on: [self-hosted, Linux, X64, icicle]
|
||||
needs: [check-changed-files, check-format]
|
||||
strategy:
|
||||
matrix:
|
||||
curve: [bn254, bls12_381, bls12_377, bw6_761]
|
||||
curve:
|
||||
- name: bn254
|
||||
build_args: -g2 -ecntt
|
||||
- name: bls12_381
|
||||
build_args: -g2 -ecntt
|
||||
- name: bls12_377
|
||||
build_args: -g2 -ecntt
|
||||
- name: bw6_761
|
||||
build_args: -g2 -ecntt
|
||||
- name: grumpkin
|
||||
build_args:
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
@@ -50,19 +60,25 @@ jobs:
|
||||
- name: Build
|
||||
working-directory: ./wrappers/golang
|
||||
if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp_cuda == 'true'
|
||||
run: ./build.sh ${{ matrix.curve }} ON ON # builds a single curve with G2 and ECNTT enabled
|
||||
- name: Upload ICICLE lib artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
# builds a single curve with the curve's specified build args
|
||||
run: ./build.sh -curve=${{ matrix.curve.name }} ${{ matrix.curve.build_args }}
|
||||
- name: Test
|
||||
working-directory: ./wrappers/golang/curves
|
||||
if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp_cuda == 'true'
|
||||
with:
|
||||
name: icicle-builds-${{ matrix.curve }}-${{ github.workflow }}-${{ github.sha }}
|
||||
path: icicle/build/libingo_${{ matrix.curve }}.a
|
||||
retention-days: 1
|
||||
|
||||
test-linux:
|
||||
name: Test on Linux
|
||||
run: |
|
||||
CURVE=$(echo ${{ matrix.curve.name }} | sed -e 's/_//g')
|
||||
export CPATH=$CPATH:/usr/local/cuda/include
|
||||
go test ./$CURVE/tests -count=1 -failfast -p 2 -timeout 60m -v
|
||||
|
||||
build-fields-linux:
|
||||
name: Build and test fields on Linux
|
||||
runs-on: [self-hosted, Linux, X64, icicle]
|
||||
needs: [check-changed-files, build-linux]
|
||||
needs: [check-changed-files, check-format]
|
||||
strategy:
|
||||
matrix:
|
||||
field:
|
||||
- name: babybear
|
||||
build_args: -field-ext
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
@@ -70,20 +86,47 @@ jobs:
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.20.0'
|
||||
- name: Download ICICLE lib artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp_cuda == 'true'
|
||||
with:
|
||||
path: ./icicle/build/
|
||||
merge-multiple: true
|
||||
- name: Run Tests
|
||||
- name: Build
|
||||
working-directory: ./wrappers/golang
|
||||
if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp_cuda == 'true'
|
||||
# -count ensures the test results are not cached
|
||||
# -p controls the number of programs that can be run in parallel
|
||||
# builds a single field with the fields specified build args
|
||||
run: ./build.sh -field=${{ matrix.field.name }} ${{ matrix.field.build_args }}
|
||||
- name: Test
|
||||
working-directory: ./wrappers/golang/fields
|
||||
if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp_cuda == 'true'
|
||||
run: |
|
||||
FIELD=$(echo ${{ matrix.field.name }} | sed -e 's/_//g')
|
||||
export CPATH=$CPATH:/usr/local/cuda/include
|
||||
go test --tags=g2 ./... -count=1 -failfast -p 2 -timeout 60m
|
||||
go test ./$FIELD/tests -count=1 -failfast -p 2 -timeout 60m -v
|
||||
|
||||
build-hashes-linux:
|
||||
name: Build and test hashes on Linux
|
||||
runs-on: [self-hosted, Linux, X64, icicle]
|
||||
needs: [check-changed-files, check-format]
|
||||
strategy:
|
||||
matrix:
|
||||
hash:
|
||||
- name: keccak
|
||||
build_args:
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.20.0'
|
||||
- name: Build
|
||||
working-directory: ./wrappers/golang
|
||||
if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp_cuda == 'true'
|
||||
# builds a single hash algorithm with the hash's specified build args
|
||||
run: ./build.sh -hash=${{ matrix.hash.name }} ${{ matrix.hash.build_args }}
|
||||
- name: Test
|
||||
working-directory: ./wrappers/golang/hash
|
||||
if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp_cuda == 'true'
|
||||
run: |
|
||||
HASH=$(echo ${{ matrix.hash.name }} | sed -e 's/_//g')
|
||||
export CPATH=$CPATH:/usr/local/cuda/include
|
||||
go test ./$HASH/tests -count=1 -failfast -p 2 -timeout 60m -v
|
||||
|
||||
# TODO: bw6 on windows requires more memory than the standard runner has
|
||||
# Add a large runner and then enable this job
|
||||
|
||||
73
.github/workflows/rust.yml
vendored
73
.github/workflows/rust.yml
vendored
@@ -4,11 +4,11 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- V2
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- V2
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -60,28 +60,53 @@ jobs:
|
||||
if: needs.check-changed-files.outputs.rust == 'true' || needs.check-changed-files.outputs.cpp_cuda == 'true'
|
||||
# Running tests from the root workspace will run all workspace members' tests by default
|
||||
# We need to limit the number of threads to avoid running out of memory on weaker machines
|
||||
run: cargo test --release --verbose --features=g2 -- --test-threads=2
|
||||
# ignored tests are polynomial tests. Since they conflict with NTT tests, they are executed separately
|
||||
run: |
|
||||
cargo test --workspace --exclude icicle-babybear --exclude icicle-stark252 --exclude icicle-m31 --release --verbose --features=g2 -- --test-threads=2 --ignored
|
||||
cargo test --workspace --exclude icicle-babybear --exclude icicle-stark252 --exclude icicle-m31 --release --verbose --features=g2 -- --test-threads=2
|
||||
|
||||
build-windows:
|
||||
name: Build on Windows
|
||||
runs-on: windows-2022
|
||||
needs: check-changed-files
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
- name: Download and Install Cuda
|
||||
- name: Run baby bear tests
|
||||
working-directory: ./wrappers/rust/icicle-fields/icicle-babybear
|
||||
if: needs.check-changed-files.outputs.rust == 'true' || needs.check-changed-files.outputs.cpp_cuda == 'true'
|
||||
id: cuda-toolkit
|
||||
uses: Jimver/cuda-toolkit@v0.2.11
|
||||
with:
|
||||
cuda: '12.0.0'
|
||||
method: 'network'
|
||||
# https://docs.nvidia.com/cuda/archive/12.0.0/cuda-installation-guide-microsoft-windows/index.html
|
||||
sub-packages: '["cudart", "nvcc", "thrust", "visual_studio_integration"]'
|
||||
- name: Build targets
|
||||
working-directory: ./wrappers/rust
|
||||
run: |
|
||||
cargo test --release --verbose -- --ignored
|
||||
cargo test --release --verbose
|
||||
|
||||
- name: Run stark252 tests
|
||||
working-directory: ./wrappers/rust/icicle-fields/icicle-stark252
|
||||
if: needs.check-changed-files.outputs.rust == 'true' || needs.check-changed-files.outputs.cpp_cuda == 'true'
|
||||
env:
|
||||
CUDA_PATH: ${{ steps.cuda-toolkit.outputs.CUDA_PATH }}
|
||||
# Building from the root workspace will build all members of the workspace by default
|
||||
run: cargo build --release --verbose
|
||||
run: |
|
||||
cargo test --release --verbose -- --ignored
|
||||
cargo test --release --verbose
|
||||
|
||||
- name: Run m31 tests
|
||||
working-directory: ./wrappers/rust/icicle-fields/icicle-m31
|
||||
if: needs.check-changed-files.outputs.rust == 'true' || needs.check-changed-files.outputs.cpp_cuda == 'true'
|
||||
run: |
|
||||
cargo test --release --verbose -- --ignored
|
||||
cargo test --release --verbose
|
||||
|
||||
# build-windows:
|
||||
# name: Build on Windows
|
||||
# runs-on: windows-2022
|
||||
# needs: check-changed-files
|
||||
# steps:
|
||||
# - name: Checkout Repo
|
||||
# uses: actions/checkout@v4
|
||||
# - name: Download and Install Cuda
|
||||
# if: needs.check-changed-files.outputs.rust == 'true' || needs.check-changed-files.outputs.cpp_cuda == 'true'
|
||||
# id: cuda-toolkit
|
||||
# uses: Jimver/cuda-toolkit@v0.2.11
|
||||
# with:
|
||||
# cuda: '12.0.0'
|
||||
# method: 'network'
|
||||
# # https://docs.nvidia.com/cuda/archive/12.0.0/cuda-installation-guide-microsoft-windows/index.html
|
||||
# sub-packages: '["cudart", "nvcc", "thrust", "visual_studio_integration"]'
|
||||
# - name: Build targets
|
||||
# working-directory: ./wrappers/rust
|
||||
# if: needs.check-changed-files.outputs.rust == 'true' || needs.check-changed-files.outputs.cpp_cuda == 'true'
|
||||
# env:
|
||||
# CUDA_PATH: ${{ steps.cuda-toolkit.outputs.CUDA_PATH }}
|
||||
# CUDA_ARCH: 50 # Using CUDA_ARCH=50 env variable since the CI machines have no GPUs
|
||||
# # Building from the root workspace will build all members of the workspace by default
|
||||
# run: cargo build --release --verbose
|
||||
|
||||
2
.github/workflows/test-deploy-docs.yml
vendored
2
.github/workflows/test-deploy-docs.yml
vendored
@@ -5,7 +5,7 @@ on:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'docs/*'
|
||||
- 'docs/**'
|
||||
|
||||
jobs:
|
||||
test-deploy:
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -8,6 +8,7 @@
|
||||
*.so
|
||||
*.nsys-rep
|
||||
*.ncu-rep
|
||||
*.sage.py
|
||||
**/target
|
||||
**/.vscode
|
||||
**/.*lock*csv#
|
||||
@@ -16,6 +17,4 @@
|
||||
**/Cargo.lock
|
||||
**/icicle/build/
|
||||
**/wrappers/rust/icicle-cuda-runtime/src/bindings.rs
|
||||
**/build
|
||||
**/icicle/appUtils/large_ntt/work
|
||||
icicle/appUtils/large_ntt/work/test_ntt
|
||||
**/build*
|
||||
|
||||
@@ -119,6 +119,7 @@ This will ensure our custom hooks are run and will make it easier to follow our
|
||||
- [nonam3e](https://github.com/nonam3e), for adding Grumpkin curve support into ICICLE
|
||||
- [alxiong](https://github.com/alxiong), for adding warmup for CudaStream
|
||||
- [cyl19970726](https://github.com/cyl19970726), for updating go install source in Dockerfile
|
||||
- [PatStiles](https://github.com/PatStiles), for adding Stark252 field
|
||||
|
||||
## Help & Support
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Contributor's Guide
|
||||
|
||||
We welcome all contributions with open arms. At Ingonyama we take a village approach, believing it takes many hands and minds to build a ecosystem.
|
||||
We welcome all contributions with open arms. At Ingonyama we take a village approach, believing it takes many hands and minds to build an ecosystem.
|
||||
|
||||
## Contributing to ICICLE
|
||||
|
||||
@@ -14,9 +14,9 @@ We welcome all contributions with open arms. At Ingonyama we take a village appr
|
||||
When opening a [pull request](https://github.com/ingonyama-zk/icicle/pulls) please keep the following in mind.
|
||||
|
||||
- `Clear Purpose` - The pull request should solve a single issue and be clean of any unrelated changes.
|
||||
- `Clear description` - If the pull request is for a new feature describe what you built, why you added it and how its best that we test it. For bug fixes please describe the issue and the solution.
|
||||
- `Clear description` - If the pull request is for a new feature describe what you built, why you added it and how it's best that we test it. For bug fixes please describe the issue and the solution.
|
||||
- `Consistent style` - Rust and Golang code should be linted by the official linters (golang fmt and rust fmt) and maintain a proper style. For CUDA and C++ code we use [`clang-format`](https://github.com/ingonyama-zk/icicle/blob/main/.clang-format), [here](https://github.com/ingonyama-zk/icicle/blob/605c25f9d22135c54ac49683b710fe2ce06e2300/.github/workflows/main-format.yml#L46) you can see how we run it.
|
||||
- `Minimal Tests` - please add test which cover basic usage of your changes .
|
||||
- `Minimal Tests` - please add test which cover basic usage of your changes.
|
||||
|
||||
## Questions?
|
||||
|
||||
|
||||
196
docs/docs/icicle/core.md
Normal file
196
docs/docs/icicle/core.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# ICICLE Core
|
||||
|
||||
ICICLE Core is a library written in C++/CUDA. All the ICICLE primitives are implemented within ICICLE Core.
|
||||
|
||||
The Core is split into logical modules that can be compiled into static libraries using different [strategies](#compilation-strategies). You can then [link](#linking) these libraries with your C++ project or write your own [bindings](#writing-new-bindings-for-icicle) for other programming languages. If you want to use ICICLE with existing bindings please refer to the [Rust](/icicle/rust-bindings) or [Golang](/icicle/golang-bindings) bindings documentation.
|
||||
|
||||
## Supported curves, fields and operations
|
||||
|
||||
### Supported curves and operations
|
||||
|
||||
| Operation\Curve | [bn254](https://neuromancer.sk/std/bn/bn254) | [bls12-377](https://neuromancer.sk/std/bls/BLS12-377) | [bls12-381](https://neuromancer.sk/std/bls/BLS12-381) | [bw6-761](https://eprint.iacr.org/2020/351) | grumpkin |
|
||||
| --- | :---: | :---: | :---: | :---: | :---: |
|
||||
| [MSM][MSM_DOCS] | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| G2 | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| [NTT][NTT_DOCS] | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| ECNTT | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| [VecOps][VECOPS_CODE] | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| [Polynomials][POLY_DOCS] | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| [Poseidon](primitives/poseidon) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| [Merkle Tree](primitives/poseidon#the-tree-builder) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
### Supported fields and operations
|
||||
|
||||
| Operation\Field | [babybear](https://eprint.iacr.org/2023/824.pdf) | [Stark252](https://docs.starknet.io/documentation/architecture_and_concepts/Cryptography/p-value/) |
|
||||
| --- | :---: | :---: |
|
||||
| [VecOps][VECOPS_CODE] | ✅ | ✅ |
|
||||
| [Polynomials][POLY_DOCS] | ✅ | ✅ |
|
||||
| [NTT][NTT_DOCS] | ✅ | ✅ |
|
||||
| Extension Field | ✅ | ❌ |
|
||||
|
||||
### Supported hashes
|
||||
|
||||
| Hash | Sizes |
|
||||
| --- | :---: |
|
||||
| Keccak | 256, 512 |
|
||||
|
||||
## Compilation strategies
|
||||
|
||||
Most of the codebase is curve/field agnostic, which means it can be compiled for different curves and fields. When you build ICICLE Core you choose a single curve or field. If you need multiple curves or fields, you compile ICICLE once per curve or field that is needed. It's that simple. Currently, the following choices are supported:
|
||||
|
||||
- [Field mode][COMPILE_FIELD_MODE] - used for STARK fields like BabyBear / Mersenne / Goldilocks. Includes field arithmetic, NTT, Poseidon, Extension fields and other primitives.
|
||||
- [Curve mode][COMPILE_CURVE_MODE] - used for SNARK curves like BN254 / BLS curves / Grumpkin / etc. Curve mode is built upon field mode, so it includes everything that field does It also includes curve operations / MSM / ECNTT / G2 and other curve-related primitives.
|
||||
|
||||
:::info
|
||||
|
||||
If you only want to use a curve's scalar or base field, you still need to use curve mode. You can disable MSM with [options](#compilation-options)
|
||||
|
||||
:::
|
||||
|
||||
### Compiling for a field
|
||||
|
||||
You can compile ICICLE for a field using this command:
|
||||
|
||||
```sh
|
||||
cd icicle
|
||||
mkdir -p build
|
||||
cmake -DFIELD=<FIELD> -S . -B build
|
||||
cmake --build build -j
|
||||
```
|
||||
|
||||
This command will output `libingo_field_<FIELD>.a` into `build/lib`.
|
||||
|
||||
### Compiling for a curve
|
||||
|
||||
:::note
|
||||
|
||||
Field related primitives will be compiled for the scalar field of the curve
|
||||
|
||||
:::
|
||||
|
||||
You can compile ICICLE for a SNARK curve using this command:
|
||||
|
||||
```sh
|
||||
cd icicle
|
||||
mkdir -p build
|
||||
cmake -DCURVE=<CURVE> -S . -B build
|
||||
cmake --build build -j
|
||||
```
|
||||
|
||||
Where `<CURVE>` can be one of `bn254`/`bls12_377`/`bls12_381`/`bw6_761`/`grumpkin`.
|
||||
|
||||
This command will output both `libingo_curve_<CURVE>.a` and `libingo_field_<CURVE>.a` into `build/lib`.
|
||||
|
||||
### Compilation options
|
||||
|
||||
There exist multiple options that allow you to customize your build or enable additional functionality.
|
||||
|
||||
#### EXT_FIELD
|
||||
|
||||
Used only in [field mode][COMPILE_FIELD_MODE] to add an Extension field. Adds all supported field operations for the extension field.
|
||||
|
||||
Default: `OFF`
|
||||
|
||||
Usage: `-DEXT_FIELD=ON`
|
||||
|
||||
#### G2
|
||||
|
||||
Used only in [curve mode][COMPILE_CURVE_MODE] to add G2 definitions. Also adds G2 MSM.
|
||||
|
||||
Default: `OFF`
|
||||
|
||||
Usage: `-DG2=ON`
|
||||
|
||||
#### ECNTT
|
||||
|
||||
Used only in [curve mode][COMPILE_CURVE_MODE] to add ECNTT function.
|
||||
|
||||
Default: `OFF`
|
||||
|
||||
Usage: `-DECNTT=ON`
|
||||
|
||||
#### MSM
|
||||
|
||||
Used only in [curve mode][COMPILE_CURVE_MODE] to add MSM function. As MSM takes a lot of time to build, you can disable it with this option to reduce compilation time.
|
||||
|
||||
Default: `ON`
|
||||
|
||||
Usage: `-DMSM=OFF`
|
||||
|
||||
#### BUILD_HASH
|
||||
|
||||
Can be used in any mode to build a hash library. Currently it only includes Keccak hash function, but more are coming.
|
||||
|
||||
Default: `OFF`
|
||||
|
||||
Usage: `-DBUILD_HASH=ON`
|
||||
|
||||
#### BUILD_TESTS
|
||||
|
||||
Can be used in any mode to include tests runner binary.
|
||||
|
||||
Default: `OFF`
|
||||
|
||||
USAGE: `-DBUILD_TESTS=ON`
|
||||
|
||||
#### BUILD_BENCHMARKS
|
||||
|
||||
Can be used in any mode to include benchmarks runner binary.
|
||||
|
||||
Default: `OFF`
|
||||
|
||||
USAGE: `-DBUILD_BENCHMARKS=ON`
|
||||
|
||||
#### DEVMODE
|
||||
|
||||
Can be used in any mode to include debug symbols in the build.
|
||||
|
||||
Default: `OFF`
|
||||
|
||||
USAGE: `-DEVMODE=ON`
|
||||
|
||||
## Linking
|
||||
|
||||
To link ICICLE with your project you first need to compile ICICLE with options of your choice. After that you can use CMake `target_link_libraries` to link with the generated static libraries and `target_include_directories` to include ICICLE headers (located in `icicle/include`).
|
||||
|
||||
Refer to our [c++ examples](https://github.com/ingonyama-zk/icicle/tree/main/examples/c%2B%2B) for more info. Take a look at this [CMakeLists.txt](https://github.com/ingonyama-zk/icicle/blob/main/examples/c%2B%2B/msm/CMakeLists.txt#L22)
|
||||
|
||||
## Writing new bindings for ICICLE
|
||||
|
||||
Since ICICLE Core is written in CUDA / C++ its really simple to generate static libraries. These static libraries can be installed on any system and called by higher level languages such as Golang.
|
||||
|
||||
Static libraries can be loaded into memory once and used by multiple programs, reducing memory usage and potentially improving performance. They also allow you to separate functionality into distinct modules so your static library may need to compile only specific features that you want to use.
|
||||
|
||||
Let's review the [Golang bindings][GOLANG_BINDINGS] since its a pretty verbose example (compared to rust which hides it pretty well) of using static libraries. Golang has a library named `CGO` which can be used to link static libraries. Here's a basic example on how you can use cgo to link these libraries:
|
||||
|
||||
```go
|
||||
/*
|
||||
#cgo LDFLAGS: -L/path/to/shared/libs -lbn254 -lbls12_381 -lbls12_377 -lbw6_671
|
||||
#include "icicle.h" // make sure you use the correct header file(s)
|
||||
*/
|
||||
import "C"
|
||||
|
||||
func main() {
|
||||
// Now you can call the C functions from the ICICLE libraries.
|
||||
// Note that C function calls are prefixed with 'C.' in Go code.
|
||||
|
||||
out := (*C.BN254_projective_t)(unsafe.Pointer(p))
|
||||
in := (*C.BN254_affine_t)(unsafe.Pointer(affine))
|
||||
|
||||
C.projective_from_affine_bn254(out, in)
|
||||
}
|
||||
```
|
||||
|
||||
The comments on the first line tell `CGO` which libraries to import as well as which header files to include. You can then call methods which are part of the static library and defined in the header file, `C.projective_from_affine_bn254` is an example.
|
||||
|
||||
If you wish to create your own bindings for a language of your choice we suggest you start by investigating how you can call static libraries.
|
||||
|
||||
<!-- Begin Links -->
|
||||
[GOLANG_BINDINGS]: golang-bindings.md
|
||||
[COMPILE_CURVE_MODE]: #compiling-for-a-curve
|
||||
[COMPILE_FIELD_MODE]: #compiling-for-a-field
|
||||
[NTT_DOCS]: primitives/ntt
|
||||
[MSM_DOCS]: primitives/msm
|
||||
[POLY_DOCS]: polynomials/overview
|
||||
[VECOPS_CODE]: https://github.com/ingonyama-zk/icicle/blob/main/icicle/include/vec_ops/vec_ops.cuh
|
||||
<!-- End Links -->
|
||||
@@ -1,7 +1,7 @@
|
||||
# Golang bindings
|
||||
|
||||
Golang bindings allow you to use ICICLE as a golang library.
|
||||
The source code for all Golang libraries can be found [here](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/golang).
|
||||
The source code for all Golang packages can be found [here](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/golang).
|
||||
|
||||
The Golang bindings are comprised of multiple packages.
|
||||
|
||||
@@ -9,7 +9,7 @@ The Golang bindings are comprised of multiple packages.
|
||||
|
||||
[`cuda-runtime`](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/golang/cuda_runtime) which defines abstractions for CUDA methods for allocating memory, initializing and managing streams, and `DeviceContext` which enables users to define and keep track of devices.
|
||||
|
||||
Each curve has its own package which you can find [here](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/golang/curves). If your project uses BN254 you only need to install that single package named [`bn254`](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/golang/curves/bn254).
|
||||
Each supported curve, field, and hash has its own package which you can find in the respective directories [here](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/golang). If your project uses BN254 you only need to import that single package named [`bn254`](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/golang/curves/bn254).
|
||||
|
||||
## Using ICICLE Golang bindings in your project
|
||||
|
||||
@@ -31,36 +31,47 @@ For a specific commit
|
||||
go get github.com/ingonyama-zk/icicle@<commit_id>
|
||||
```
|
||||
|
||||
To build the shared libraries you can run this script:
|
||||
To build the shared libraries you can run [this](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/golang/build.sh) script:
|
||||
|
||||
```
|
||||
./build <curve> [G2_enabled]
|
||||
```sh
|
||||
./build.sh [-curve=<curve>] [-field=<field>] [-hash=<hash>] [-cuda_version=<version>] [-g2] [-ecntt] [-devmode]
|
||||
|
||||
curve - The name of the curve to build or "all" to build all curves
|
||||
G2_enabled - Optional - To build with G2 enabled
|
||||
curve - The name of the curve to build or "all" to build all supported curves
|
||||
field - The name of the field to build or "all" to build all supported fields
|
||||
hash - The name of the hash to build or "all" to build all supported hashes
|
||||
-g2 - Optional - build with G2 enabled
|
||||
-ecntt - Optional - build with ECNTT enabled
|
||||
-devmode - Optional - build in devmode
|
||||
-help - Optional - Displays usage information
|
||||
```
|
||||
|
||||
For example if you want to build all curves with G2 enabled you would run:
|
||||
:::note
|
||||
|
||||
If more than one curve or more than one field or more than one hash is supplied, the last one supplied will be built
|
||||
|
||||
:::
|
||||
|
||||
To build ICICLE libraries for all supported curves with G2 and ECNTT enabled.
|
||||
|
||||
```bash
|
||||
./build.sh all ON
|
||||
./build.sh -curve=all -g2 -ecntt
|
||||
```
|
||||
|
||||
If you are interested in building a specific curve you would run:
|
||||
If you wish to build for a specific curve, for example bn254, without G2 or ECNTT enabled.
|
||||
|
||||
```bash
|
||||
./build.sh bls12_381 ON
|
||||
``` bash
|
||||
./build.sh -curve=bn254
|
||||
```
|
||||
|
||||
Now you can import ICICLE into your project
|
||||
|
||||
```golang
|
||||
```go
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
|
||||
"github.com/ingonyama-zk/icicle/wrappers/golang/core"
|
||||
cr "github.com/ingonyama-zk/icicle/wrappers/golang/cuda_runtime"
|
||||
"github.com/ingonyama-zk/icicle/v2/wrappers/golang/core"
|
||||
cr "github.com/ingonyama-zk/icicle/v2/wrappers/golang/cuda_runtime"
|
||||
)
|
||||
...
|
||||
```
|
||||
@@ -70,11 +81,9 @@ import (
|
||||
To run all tests, for all curves:
|
||||
|
||||
```bash
|
||||
go test --tags=g2 ./... -count=1
|
||||
go test ./... -count=1
|
||||
```
|
||||
|
||||
If you dont want to include g2 tests then drop `--tags=g2`.
|
||||
|
||||
If you wish to run test for a specific curve:
|
||||
|
||||
```bash
|
||||
@@ -85,13 +94,13 @@ go test <path_to_curve> -count=1
|
||||
|
||||
The libraries produced from the CUDA code compilation are used to bind Golang to ICICLE's CUDA code.
|
||||
|
||||
1. These libraries (named `libingo_<curve>.a`) can be imported in your Go project to leverage the GPU accelerated functionalities provided by ICICLE.
|
||||
1. These libraries (named `libingo_curve_<curve>.a` and `libingo_field_<curve>.a`) can be imported in your Go project to leverage the GPU accelerated functionalities provided by ICICLE.
|
||||
|
||||
2. In your Go project, you can use `cgo` to link these libraries. Here's a basic example on how you can use `cgo` to link these libraries:
|
||||
|
||||
```go
|
||||
/*
|
||||
#cgo LDFLAGS: -L/path/to/shared/libs -lingo_bn254
|
||||
#cgo LDFLAGS: -L/path/to/shared/libs -lingo_curve_bn254 -L$/path/to/shared/libs -lingo_field_bn254 -lstdc++ -lm
|
||||
#include "icicle.h" // make sure you use the correct header file(s)
|
||||
*/
|
||||
import "C"
|
||||
@@ -103,3 +112,25 @@ func main() {
|
||||
```
|
||||
|
||||
Replace `/path/to/shared/libs` with the actual path where the shared libraries are located on your system.
|
||||
|
||||
## Supported curves, fields and operations
|
||||
|
||||
### Supported curves and operations
|
||||
|
||||
| Operation\Curve | bn254 | bls12_377 | bls12_381 | bw6-761 | grumpkin |
|
||||
| --- | :---: | :---: | :---: | :---: | :---: |
|
||||
| MSM | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| G2 | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| NTT | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| ECNTT | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| VecOps | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Polynomials | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
|
||||
### Supported fields and operations
|
||||
|
||||
| Operation\Field | babybear |
|
||||
| --- | :---: |
|
||||
| VecOps | ✅ |
|
||||
| Polynomials | ✅ |
|
||||
| NTT | ✅ |
|
||||
| Extension Field | ✅ |
|
||||
|
||||
92
docs/docs/icicle/golang-bindings/ecntt.md
Normal file
92
docs/docs/icicle/golang-bindings/ecntt.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# ECNTT
|
||||
|
||||
## ECNTT Method
|
||||
|
||||
The `ECNtt[T any]()` function performs the Elliptic Curve Number Theoretic Transform (EC-NTT) on the input points slice, using the provided dir (direction), cfg (configuration), and stores the results in the results slice.
|
||||
|
||||
```go
|
||||
func ECNtt[T any](points core.HostOrDeviceSlice, dir core.NTTDir, cfg *core.NTTConfig[T], results core.HostOrDeviceSlice) core.IcicleError
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- **`points`**: A slice of elliptic curve points (in projective coordinates) that will be transformed. The slice can be stored on the host or the device, as indicated by the `core.HostOrDeviceSlice` type.
|
||||
- **`dir`**: The direction of the EC-NTT transform, either `core.KForward` or `core.KInverse`.
|
||||
- **`cfg`**: A pointer to an `NTTConfig` object, containing configuration options for the NTT operation.
|
||||
- **`results`**: A slice that will store the transformed elliptic curve points (in projective coordinates). The slice can be stored on the host or the device, as indicated by the `core.HostOrDeviceSlice` type.
|
||||
|
||||
### Return Value
|
||||
|
||||
- **`CudaError`**: A `core.IcicleError` value, which will be `core.IcicleErrorCode(0)` if the EC-NTT operation was successful, or an error if something went wrong.
|
||||
|
||||
## NTT Configuration (NTTConfig)
|
||||
|
||||
The `NTTConfig` structure holds configuration parameters for the NTT operation, allowing customization of its behavior to optimize performance based on the specifics of your protocol.
|
||||
|
||||
```go
|
||||
type NTTConfig[T any] struct {
|
||||
Ctx cr.DeviceContext
|
||||
CosetGen T
|
||||
BatchSize int32
|
||||
ColumnsBatch bool
|
||||
Ordering Ordering
|
||||
areInputsOnDevice bool
|
||||
areOutputsOnDevice bool
|
||||
IsAsync bool
|
||||
NttAlgorithm NttAlgorithm
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
- **`Ctx`**: Device context containing details like device ID and stream ID.
|
||||
- **`CosetGen`**: Coset generator used for coset (i)NTTs, defaulting to no coset being used.
|
||||
- **`BatchSize`**: The number of NTTs to compute in one operation, defaulting to 1.
|
||||
- **`ColumnsBatch`**: If true the function will compute the NTTs over the columns of the input matrix and not over the rows. Defaults to `false`.
|
||||
- **`Ordering`**: Ordering of inputs and outputs (`KNN`, `KNR`, `KRN`, `KRR`), affecting how data is arranged.
|
||||
- **`areInputsOnDevice`**: Indicates if input scalars are located on the device.
|
||||
- **`areOutputsOnDevice`**: Indicates if results are stored on the device.
|
||||
- **`IsAsync`**: Controls whether the NTT operation runs asynchronously.
|
||||
- **`NttAlgorithm`**: Explicitly select the NTT algorithm. ECNTT supports running on `Radix2` algorithm.
|
||||
|
||||
### Default Configuration
|
||||
|
||||
Use `GetDefaultNTTConfig` to obtain a default configuration, customizable as needed.
|
||||
|
||||
```go
|
||||
func GetDefaultNTTConfig[T any](cosetGen T) NTTConfig[T]
|
||||
```
|
||||
|
||||
## ECNTT Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/ingonyama-zk/icicle/v2/wrappers/golang/core"
|
||||
cr "github.com/ingonyama-zk/icicle/v2/wrappers/golang/cuda_runtime"
|
||||
)
|
||||
|
||||
func Main() {
|
||||
// Obtain the default NTT configuration with a predefined coset generator.
|
||||
cfg := GetDefaultNttConfig()
|
||||
|
||||
// Define the size of the input scalars.
|
||||
size := 1 << 18
|
||||
|
||||
// Generate Points for the ECNTT operation.
|
||||
points := GenerateProjectivePoints(size)
|
||||
|
||||
// Set the direction of the NTT (forward or inverse).
|
||||
dir := core.KForward
|
||||
|
||||
// Allocate memory for the results of the NTT operation.
|
||||
results := make(core.HostSlice[Projective], size)
|
||||
|
||||
// Perform the NTT operation.
|
||||
err := ECNtt(points, dir, &cfg, results)
|
||||
if err != cr.CudaSuccess {
|
||||
panic("ECNTT operation failed")
|
||||
}
|
||||
}
|
||||
```
|
||||
94
docs/docs/icicle/golang-bindings/keccak.md
Normal file
94
docs/docs/icicle/golang-bindings/keccak.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Keccak
|
||||
|
||||
## Keccak Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/ingonyama-zk/icicle/v2/wrappers/golang/core"
|
||||
cr "github.com/ingonyama-zk/icicle/v2/wrappers/golang/cuda_runtime"
|
||||
"github.com/ingonyama-zk/icicle/v2/wrappers/golang/hash/keccak"
|
||||
)
|
||||
|
||||
func createHostSliceFromHexString(hexString string) core.HostSlice[uint8] {
|
||||
byteArray, err := hex.DecodeString(hexString)
|
||||
if err != nil {
|
||||
panic("Not a hex string")
|
||||
}
|
||||
return core.HostSliceFromElements([]uint8(byteArray))
|
||||
}
|
||||
|
||||
func main() {
|
||||
input := createHostSliceFromHexString("1725b6")
|
||||
outHost256 := make(core.HostSlice[uint8], 32)
|
||||
|
||||
cfg := keccak.GetDefaultHashConfig()
|
||||
e := keccak.Keccak256(input, int32(input.Len()), 1, outHost256, &cfg)
|
||||
if e.CudaErrorCode != cr.CudaSuccess {
|
||||
panic("Keccak256 hashing failed")
|
||||
}
|
||||
|
||||
outHost512 := make(core.HostSlice[uint8], 64)
|
||||
e = keccak.Keccak512(input, int32(input.Len()), 1, outHost512, &cfg)
|
||||
if e.CudaErrorCode != cr.CudaSuccess {
|
||||
panic("Keccak512 hashing failed")
|
||||
}
|
||||
|
||||
numberOfBlocks := 3
|
||||
outHostBatch256 := make(core.HostSlice[uint8], 32*numberOfBlocks)
|
||||
e = keccak.Keccak256(input, int32(input.Len()/numberOfBlocks), int32(numberOfBlocks), outHostBatch256, &cfg)
|
||||
if e.CudaErrorCode != cr.CudaSuccess {
|
||||
panic("Keccak256 batch hashing failed")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Keccak Methods
|
||||
|
||||
```go
|
||||
func Keccak256(input core.HostOrDeviceSlice, inputBlockSize, numberOfBlocks int32, output core.HostOrDeviceSlice, config *HashConfig) core.IcicleError
|
||||
func Keccak512(input core.HostOrDeviceSlice, inputBlockSize, numberOfBlocks int32, output core.HostOrDeviceSlice, config *HashConfig) core.IcicleError
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- **`input`**: A slice containing the input data for the Keccak256 hash function. It can reside in either host memory or device memory.
|
||||
- **`inputBlockSize`**: An integer specifying the size of the input data for a single hash.
|
||||
- **`numberOfBlocks`**: An integer specifying the number of results in the hash batch.
|
||||
- **`output`**: A slice where the resulting hash will be stored. This slice can be in host or device memory.
|
||||
- **`config`**: A pointer to a `HashConfig` object, which contains various configuration options for the Keccak256 operation.
|
||||
|
||||
### Return Value
|
||||
|
||||
- **`CudaError`**: Returns a CUDA error code indicating the success or failure of the Keccak256/Keccak512 operation.
|
||||
|
||||
## HashConfig
|
||||
|
||||
The `HashConfig` structure holds configuration parameters for the Keccak256/Keccak512 operation, allowing customization of its behavior to optimize performance based on the specifics of the operation or the underlying hardware.
|
||||
|
||||
```go
|
||||
type HashConfig struct {
|
||||
Ctx cr.DeviceContext
|
||||
areInputsOnDevice bool
|
||||
areOutputsOnDevice bool
|
||||
IsAsync bool
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
- **`Ctx`**: Device context containing details like device id and stream.
|
||||
- **`areInputsOnDevice`**: Indicates if input data is located on the device.
|
||||
- **`areOutputsOnDevice`**: Indicates if output hash is stored on the device.
|
||||
- **`IsAsync`**: If true, runs the Keccak256/Keccak512 operation asynchronously.
|
||||
|
||||
### Default Configuration
|
||||
|
||||
Use `GetDefaultHashConfig` to obtain a default configuration, which can then be customized as needed.
|
||||
|
||||
```go
|
||||
func GetDefaultHashConfig() HashConfig
|
||||
```
|
||||
@@ -2,15 +2,11 @@
|
||||
|
||||
To understand the theory behind MSM pre computation technique refer to Niall Emmart's [talk](https://youtu.be/KAWlySN7Hm8?feature=shared&t=1734).
|
||||
|
||||
### Supported curves
|
||||
|
||||
`bls12-377`, `bls12-381`, `bn254`, `bw6-761`
|
||||
|
||||
## Core package
|
||||
|
||||
## MSM `PrecomputeBases`
|
||||
### MSM PrecomputePoints
|
||||
|
||||
`PrecomputeBases` and `G2PrecomputeBases` exists for all supported curves.
|
||||
`PrecomputePoints` and `G2PrecomputePoints` exists for all supported curves.
|
||||
|
||||
#### Description
|
||||
|
||||
@@ -18,75 +14,86 @@ This function extends each provided base point $(P)$ with its multiples $(2^lP,
|
||||
|
||||
The precomputation process is crucial for optimizing MSM operations, especially when dealing with large sets of points and scalars. By precomputing and storing multiples of the base points, the MSM function can more efficiently compute the scalar-point multiplications.
|
||||
|
||||
#### `PrecomputeBases`
|
||||
#### `PrecomputePoints`
|
||||
|
||||
Precomputes bases for MSM by extending each base point with its multiples.
|
||||
Precomputes points for MSM by extending each base point with its multiples.
|
||||
|
||||
```go
|
||||
func PrecomputeBases(points core.HostOrDeviceSlice, precomputeFactor int32, c int32, ctx *cr.DeviceContext, outputBases core.DeviceSlice) cr.CudaError
|
||||
func PrecomputePoints(points core.HostOrDeviceSlice, msmSize int, cfg *core.MSMConfig, outputBases core.DeviceSlice) cr.CudaError
|
||||
```
|
||||
|
||||
##### Parameters
|
||||
|
||||
- **`points`**: A slice of the original affine points to be extended with their multiples.
|
||||
- **`precomputeFactor`**: Determines the total number of points to precompute for each base point.
|
||||
- **`c`**: Currently unused; reserved for future compatibility.
|
||||
- **`ctx`**: CUDA device context specifying the execution environment.
|
||||
- **`outputBases`**: The device slice allocated for storing the extended bases.
|
||||
- **`msmSize`**: The size of a single msm in order to determine optimal parameters.
|
||||
- **`cfg`**: The MSM configuration parameters.
|
||||
- **`outputBases`**: The device slice allocated for storing the extended points.
|
||||
|
||||
##### Example
|
||||
|
||||
```go
|
||||
cfg := GetDefaultMSMConfig()
|
||||
points := GenerateAffinePoints(1024)
|
||||
precomputeFactor := 8
|
||||
var precomputeOut core.DeviceSlice
|
||||
_, e := precomputeOut.Malloc(points[0].Size()*points.Len()*int(precomputeFactor), points[0].Size())
|
||||
package main
|
||||
|
||||
err := PrecomputeBases(points, precomputeFactor, 0, &cfg.Ctx, precomputeOut)
|
||||
if err != cr.CudaSuccess {
|
||||
log.Fatalf("PrecomputeBases failed: %v", err)
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/ingonyama-zk/icicle/v2/wrappers/golang/core"
|
||||
cr "github.com/ingonyama-zk/icicle/v2/wrappers/golang/cuda_runtime"
|
||||
bn254 "github.com/ingonyama-zk/icicle/v2/wrappers/golang/curves/bn254"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := bn254.GetDefaultMSMConfig()
|
||||
points := bn254.GenerateAffinePoints(1024)
|
||||
var precomputeFactor int32 = 8
|
||||
var precomputeOut core.DeviceSlice
|
||||
precomputeOut.Malloc(points[0].Size()*points.Len()*int(precomputeFactor), points[0].Size())
|
||||
|
||||
err := bn254.PrecomputePoints(points, 1024, &cfg, precomputeOut)
|
||||
if err != cr.CudaSuccess {
|
||||
log.Fatalf("PrecomputeBases failed: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `G2PrecomputeBases`
|
||||
#### `G2PrecomputePoints`
|
||||
|
||||
This method is the same as `PrecomputeBases` but for G2 points. Extends each G2 curve base point with its multiples for optimized MSM computations.
|
||||
This method is the same as `PrecomputePoints` but for G2 points. Extends each G2 curve base point with its multiples for optimized MSM computations.
|
||||
|
||||
```go
|
||||
func G2PrecomputeBases(points core.HostOrDeviceSlice, precomputeFactor int32, c int32, ctx *cr.DeviceContext, outputBases core.DeviceSlice) cr.CudaError
|
||||
func G2PrecomputePoints(points core.HostOrDeviceSlice, msmSize int, cfg *core.MSMConfig, outputBases core.DeviceSlice) cr.CudaError
|
||||
```
|
||||
|
||||
##### Parameters
|
||||
|
||||
- **`points`**: A slice of G2 curve points to be extended.
|
||||
- **`precomputeFactor`**: The total number of points to precompute for each base.
|
||||
- **`c`**: Reserved for future use to ensure compatibility with MSM operations.
|
||||
- **`ctx`**: Specifies the CUDA device context for execution.
|
||||
- **`outputBases`**: Allocated device slice for the extended bases.
|
||||
- **`points`**: A slice of the original affine points to be extended with their multiples.
|
||||
- **`msmSize`**: The size of a single msm in order to determine optimal parameters.
|
||||
- **`cfg`**: The MSM configuration parameters.
|
||||
- **`outputBases`**: The device slice allocated for storing the extended points.
|
||||
|
||||
##### Example
|
||||
|
||||
```go
|
||||
cfg := G2GetDefaultMSMConfig()
|
||||
points := G2GenerateAffinePoints(1024)
|
||||
precomputeFactor := 8
|
||||
var precomputeOut core.DeviceSlice
|
||||
_, e := precomputeOut.Malloc(points[0].Size()*points.Len()*int(precomputeFactor), points[0].Size())
|
||||
package main
|
||||
|
||||
err := G2PrecomputeBases(points, precomputeFactor, 0, &cfg.Ctx, precomputeOut)
|
||||
if err != cr.CudaSuccess {
|
||||
log.Fatalf("G2PrecomputeBases failed: %v", err)
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/ingonyama-zk/icicle/v2/wrappers/golang/core"
|
||||
cr "github.com/ingonyama-zk/icicle/v2/wrappers/golang/cuda_runtime"
|
||||
g2 "github.com/ingonyama-zk/icicle/v2/wrappers/golang/curves/bn254/g2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := g2.G2GetDefaultMSMConfig()
|
||||
points := g2.G2GenerateAffinePoints(1024)
|
||||
var precomputeFactor int32 = 8
|
||||
var precomputeOut core.DeviceSlice
|
||||
precomputeOut.Malloc(points[0].Size()*points.Len()*int(precomputeFactor), points[0].Size())
|
||||
|
||||
err := g2.G2PrecomputePoints(points, 1024, 0, &cfg, precomputeOut)
|
||||
if err != cr.CudaSuccess {
|
||||
log.Fatalf("PrecomputeBases failed: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Benchmarks
|
||||
|
||||
Benchmarks where performed on a Nvidia RTX 3090Ti.
|
||||
|
||||
| Pre-computation factor | bn254 size `2^20` MSM, ms. | bn254 size `2^12` MSM, size `2^10` batch, ms. | bls12-381 size `2^20` MSM, ms. | bls12-381 size `2^12` MSM, size `2^10` batch, ms. |
|
||||
| ------------- | ------------- | ------------- | ------------- | ------------- |
|
||||
| 1 | 14.1 | 82.8 | 25.5 | 136.7 |
|
||||
| 2 | 11.8 | 76.6 | 20.3 | 123.8 |
|
||||
| 4 | 10.9 | 73.8 | 18.1 | 117.8 |
|
||||
| 8 | 10.6 | 73.7 | 17.2 | 116.0 |
|
||||
|
||||
@@ -1,62 +1,60 @@
|
||||
# MSM
|
||||
|
||||
|
||||
### Supported curves
|
||||
|
||||
`bls12-377`, `bls12-381`, `bn254`, `bw6-761`
|
||||
|
||||
## MSM Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/ingonyama-zk/icicle/wrappers/golang/core"
|
||||
cr "github.com/ingonyama-zk/icicle/wrappers/golang/cuda_runtime"
|
||||
"github.com/ingonyama-zk/icicle/v2/wrappers/golang/core"
|
||||
cr "github.com/ingonyama-zk/icicle/v2/wrappers/golang/cuda_runtime"
|
||||
"github.com/ingonyama-zk/icicle/v2/wrappers/golang/curves/bn254"
|
||||
bn254_msm "github.com/ingonyama-zk/icicle/v2/wrappers/golang/curves/bn254/msm"
|
||||
)
|
||||
|
||||
func Main() {
|
||||
// Obtain the default MSM configuration.
|
||||
cfg := GetDefaultMSMConfig()
|
||||
|
||||
// Define the size of the problem, here 2^18.
|
||||
size := 1 << 18
|
||||
func main() {
|
||||
// Obtain the default MSM configuration.
|
||||
cfg := core.GetDefaultMSMConfig()
|
||||
|
||||
// Generate scalars and points for the MSM operation.
|
||||
scalars := GenerateScalars(size)
|
||||
points := GenerateAffinePoints(size)
|
||||
// Define the size of the problem, here 2^18.
|
||||
size := 1 << 18
|
||||
|
||||
// Create a CUDA stream for asynchronous operations.
|
||||
stream, _ := cr.CreateStream()
|
||||
var p Projective
|
||||
|
||||
// Allocate memory on the device for the result of the MSM operation.
|
||||
var out core.DeviceSlice
|
||||
_, e := out.MallocAsync(p.Size(), p.Size(), stream)
|
||||
// Generate scalars and points for the MSM operation.
|
||||
scalars := bn254.GenerateScalars(size)
|
||||
points := bn254.GenerateAffinePoints(size)
|
||||
|
||||
if e != cr.CudaSuccess {
|
||||
panic(e)
|
||||
}
|
||||
|
||||
// Set the CUDA stream in the MSM configuration.
|
||||
cfg.Ctx.Stream = &stream
|
||||
cfg.IsAsync = true
|
||||
|
||||
// Perform the MSM operation.
|
||||
e = Msm(scalars, points, &cfg, out)
|
||||
|
||||
if e != cr.CudaSuccess {
|
||||
panic(e)
|
||||
}
|
||||
|
||||
// Allocate host memory for the results and copy the results from the device.
|
||||
outHost := make(core.HostSlice[Projective], 1)
|
||||
cr.SynchronizeStream(&stream)
|
||||
outHost.CopyFromDevice(&out)
|
||||
|
||||
// Free the device memory allocated for the results.
|
||||
out.Free()
|
||||
// Create a CUDA stream for asynchronous operations.
|
||||
stream, _ := cr.CreateStream()
|
||||
var p bn254.Projective
|
||||
|
||||
// Allocate memory on the device for the result of the MSM operation.
|
||||
var out core.DeviceSlice
|
||||
_, e := out.MallocAsync(p.Size(), p.Size(), stream)
|
||||
|
||||
if e != cr.CudaSuccess {
|
||||
panic(e)
|
||||
}
|
||||
|
||||
// Set the CUDA stream in the MSM configuration.
|
||||
cfg.Ctx.Stream = &stream
|
||||
cfg.IsAsync = true
|
||||
|
||||
// Perform the MSM operation.
|
||||
e = bn254_msm.Msm(scalars, points, &cfg, out)
|
||||
|
||||
if e != cr.CudaSuccess {
|
||||
panic(e)
|
||||
}
|
||||
|
||||
// Allocate host memory for the results and copy the results from the device.
|
||||
outHost := make(core.HostSlice[bn254.Projective], 1)
|
||||
cr.SynchronizeStream(&stream)
|
||||
outHost.CopyFromDevice(&out)
|
||||
|
||||
// Free the device memory allocated for the results.
|
||||
out.Free()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## MSM Method
|
||||
@@ -67,14 +65,14 @@ func Msm(scalars core.HostOrDeviceSlice, points core.HostOrDeviceSlice, cfg *cor
|
||||
|
||||
### Parameters
|
||||
|
||||
- **scalars**: A slice containing the scalars for multiplication. It can reside either in host memory or device memory.
|
||||
- **points**: A slice containing the points to be multiplied with scalars. Like scalars, these can also be in host or device memory.
|
||||
- **cfg**: A pointer to an `MSMConfig` object, which contains various configuration options for the MSM operation.
|
||||
- **results**: A slice where the results of the MSM operation will be stored. This slice can be in host or device memory.
|
||||
- **`scalars`**: A slice containing the scalars for multiplication. It can reside either in host memory or device memory.
|
||||
- **`points`**: A slice containing the points to be multiplied with scalars. Like scalars, these can also be in host or device memory.
|
||||
- **`cfg`**: A pointer to an `MSMConfig` object, which contains various configuration options for the MSM operation.
|
||||
- **`results`**: A slice where the results of the MSM operation will be stored. This slice can be in host or device memory.
|
||||
|
||||
### Return Value
|
||||
|
||||
- **CudaError**: Returns a CUDA error code indicating the success or failure of the MSM operation.
|
||||
- **`CudaError`**: Returns a CUDA error code indicating the success or failure of the MSM operation.
|
||||
|
||||
## MSMConfig
|
||||
|
||||
@@ -100,19 +98,19 @@ type MSMConfig struct {
|
||||
|
||||
### Fields
|
||||
|
||||
- **Ctx**: Device context containing details like device id and stream.
|
||||
- **PrecomputeFactor**: Controls the number of extra points to pre-compute.
|
||||
- **C**: Window bitsize, a key parameter in the "bucket method" for MSM.
|
||||
- **Bitsize**: Number of bits of the largest scalar.
|
||||
- **LargeBucketFactor**: Sensitivity to frequently occurring buckets.
|
||||
- **batchSize**: Number of results to compute in one batch.
|
||||
- **areScalarsOnDevice**: Indicates if scalars are located on the device.
|
||||
- **AreScalarsMontgomeryForm**: True if scalars are in Montgomery form.
|
||||
- **arePointsOnDevice**: Indicates if points are located on the device.
|
||||
- **ArePointsMontgomeryForm**: True if point coordinates are in Montgomery form.
|
||||
- **areResultsOnDevice**: Indicates if results are stored on the device.
|
||||
- **IsBigTriangle**: If `true` MSM will run in Large triangle accumulation if `false` Bucket accumulation will be chosen. Default value: false.
|
||||
- **IsAsync**: If true, runs MSM asynchronously.
|
||||
- **`Ctx`**: Device context containing details like device id and stream.
|
||||
- **`PrecomputeFactor`**: Controls the number of extra points to pre-compute.
|
||||
- **`C`**: Window bitsize, a key parameter in the "bucket method" for MSM.
|
||||
- **`Bitsize`**: Number of bits of the largest scalar.
|
||||
- **`LargeBucketFactor`**: Sensitivity to frequently occurring buckets.
|
||||
- **`batchSize`**: Number of results to compute in one batch.
|
||||
- **`areScalarsOnDevice`**: Indicates if scalars are located on the device.
|
||||
- **`AreScalarsMontgomeryForm`**: True if scalars are in Montgomery form.
|
||||
- **`arePointsOnDevice`**: Indicates if points are located on the device.
|
||||
- **`ArePointsMontgomeryForm`**: True if point coordinates are in Montgomery form.
|
||||
- **`areResultsOnDevice`**: Indicates if results are stored on the device.
|
||||
- **`IsBigTriangle`**: If `true` MSM will run in Large triangle accumulation if `false` Bucket accumulation will be chosen. Default value: false.
|
||||
- **`IsAsync`**: If true, runs MSM asynchronously.
|
||||
|
||||
### Default Configuration
|
||||
|
||||
@@ -122,10 +120,9 @@ Use `GetDefaultMSMConfig` to obtain a default configuration, which can then be c
|
||||
func GetDefaultMSMConfig() MSMConfig
|
||||
```
|
||||
|
||||
|
||||
## How do I toggle between the supported algorithms?
|
||||
|
||||
When creating your MSM Config you may state which algorithm you wish to use. `cfg.Ctx.IsBigTriangle = true` will activate Large triangle accumulation and `cfg.Ctx.IsBigTriangle = false` will activate Bucket accumulation.
|
||||
When creating your MSM Config you may state which algorithm you wish to use. `cfg.Ctx.IsBigTriangle = true` will activate Large triangle reduction and `cfg.Ctx.IsBigTriangle = false` will activate iterative reduction.
|
||||
|
||||
```go
|
||||
...
|
||||
@@ -142,7 +139,7 @@ cfg.Ctx.IsBigTriangle = true
|
||||
|
||||
Toggling between MSM modes occurs automatically based on the number of results you are expecting from the `MSM` function.
|
||||
|
||||
The number of results is interpreted from the size of `var out core.DeviceSlice`. Thus its important when allocating memory for `var out core.DeviceSlice` to make sure that you are allocating `<number of results> X <size of a single point>`.
|
||||
The number of results is interpreted from the size of `var out core.DeviceSlice`. Thus it's important when allocating memory for `var out core.DeviceSlice` to make sure that you are allocating `<number of results> X <size of a single point>`.
|
||||
|
||||
```go
|
||||
...
|
||||
@@ -155,46 +152,47 @@ out.Malloc(batchSize*p.Size(), p.Size())
|
||||
...
|
||||
```
|
||||
|
||||
## Parameters for optimal performance
|
||||
|
||||
Please refer to the [primitive description](../primitives/msm#choosing-optimal-parameters)
|
||||
|
||||
## Support for G2 group
|
||||
|
||||
To activate G2 support first you must make sure you are building the static libraries with G2 feature enabled.
|
||||
To activate G2 support first you must make sure you are building the static libraries with G2 feature enabled as described in the [Golang building instructions](../golang-bindings.md#using-icicle-golang-bindings-in-your-project).
|
||||
|
||||
```bash
|
||||
./build.sh bls12_381 ON
|
||||
```
|
||||
|
||||
Now when importing `icicle`, you should have access to G2 features.
|
||||
Now you may import `g2` package of the specified curve.
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/ingonyama-zk/icicle/wrappers/golang/core"
|
||||
"github.com/ingonyama-zk/icicle/v2/wrappers/golang/curves/bn254/g2"
|
||||
)
|
||||
```
|
||||
|
||||
These features include `G2Projective` and `G2Affine` points as well as a `G2Msm` method.
|
||||
This package includes `G2Projective` and `G2Affine` points as well as a `G2Msm` method.
|
||||
|
||||
```go
|
||||
...
|
||||
package main
|
||||
|
||||
cfg := GetDefaultMSMConfig()
|
||||
size := 1 << 12
|
||||
batchSize := 3
|
||||
totalSize := size * batchSize
|
||||
scalars := GenerateScalars(totalSize)
|
||||
points := G2GenerateAffinePoints(totalSize)
|
||||
import (
|
||||
"github.com/ingonyama-zk/icicle/v2/wrappers/golang/core"
|
||||
bn254 "github.com/ingonyama-zk/icicle/v2/wrappers/golang/curves/bn254"
|
||||
g2 "github.com/ingonyama-zk/icicle/v2/wrappers/golang/curves/bn254/g2"
|
||||
)
|
||||
|
||||
var p G2Projective
|
||||
var out core.DeviceSlice
|
||||
out.Malloc(batchSize*p.Size(), p.Size())
|
||||
G2Msm(scalars, points, &cfg, out)
|
||||
func main() {
|
||||
cfg := core.GetDefaultMSMConfig()
|
||||
size := 1 << 12
|
||||
batchSize := 3
|
||||
totalSize := size * batchSize
|
||||
scalars := bn254.GenerateScalars(totalSize)
|
||||
points := g2.G2GenerateAffinePoints(totalSize)
|
||||
|
||||
var p g2.G2Projective
|
||||
var out core.DeviceSlice
|
||||
out.Malloc(batchSize*p.Size(), p.Size())
|
||||
g2.G2Msm(scalars, points, &cfg, out)
|
||||
}
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
`G2Msm` works the same way as normal MSM, the difference is that it uses G2 Points.
|
||||
|
||||
Additionally when you are building your application make sure to use the g2 feature flag
|
||||
|
||||
```bash
|
||||
go build -tags=g2
|
||||
```
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
To learn more about the theory of Multi GPU programming refer to [this part](../multi-gpu.md) of documentation.
|
||||
|
||||
Here we will cover the core multi GPU apis and a [example](#a-multi-gpu-example)
|
||||
|
||||
Here we will cover the core multi GPU apis and an [example](#a-multi-gpu-example)
|
||||
|
||||
## A Multi GPU example
|
||||
|
||||
@@ -13,43 +12,53 @@ In this example we will display how you can
|
||||
2. For every GPU launch a thread and set an active device per thread.
|
||||
3. Execute a MSM on each GPU
|
||||
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/ingonyama-zk/icicle/v2/wrappers/golang/core"
|
||||
cr "github.com/ingonyama-zk/icicle/v2/wrappers/golang/cuda_runtime"
|
||||
bn254 "github.com/ingonyama-zk/icicle/v2/wrappers/golang/curves/bn254"
|
||||
)
|
||||
|
||||
func main() {
|
||||
numDevices, _ := cuda_runtime.GetDeviceCount()
|
||||
numDevices, _ := cr.GetDeviceCount()
|
||||
fmt.Println("There are ", numDevices, " devices available")
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
for i := 0; i < numDevices; i++ {
|
||||
wg.Add(1)
|
||||
// RunOnDevice makes sure each MSM runs on a single thread
|
||||
cuda_runtime.RunOnDevice(i, func(args ...any) {
|
||||
// RunOnDevice makes sure each MSM runs on a single thread
|
||||
cr.RunOnDevice(i, func(args ...any) {
|
||||
defer wg.Done()
|
||||
cfg := GetDefaultMSMConfig()
|
||||
cfg := bn254.GetDefaultMSMConfig()
|
||||
cfg.IsAsync = true
|
||||
for _, power := range []int{10, 18} {
|
||||
size := 1 << power // 2^pwr
|
||||
|
||||
// generate random scalars
|
||||
scalars := GenerateScalars(size)
|
||||
points := GenerateAffinePoints(size)
|
||||
// generate random scalars
|
||||
scalars := bn254.GenerateScalars(size)
|
||||
points := bn254.GenerateAffinePoints(size)
|
||||
|
||||
// create a stream and allocate result pointer
|
||||
stream, _ := cuda_runtime.CreateStream()
|
||||
var p Projective
|
||||
// create a stream and allocate result pointer
|
||||
stream, _ := cr.CreateStream()
|
||||
var p bn254.Projective
|
||||
var out core.DeviceSlice
|
||||
_, e := out.MallocAsync(p.Size(), p.Size(), stream)
|
||||
// assign stream to device context
|
||||
out.MallocAsync(p.Size(), p.Size(), stream)
|
||||
// assign stream to device context
|
||||
cfg.Ctx.Stream = &stream
|
||||
|
||||
// execute MSM
|
||||
e = Msm(scalars, points, &cfg, out)
|
||||
// read result from device
|
||||
outHost := make(core.HostSlice[Projective], 1)
|
||||
// execute MSM
|
||||
bn254.Msm(scalars, points, &cfg, out)
|
||||
// read result from device
|
||||
outHost := make(core.HostSlice[bn254.Projective], 1)
|
||||
outHost.CopyFromDeviceAsync(&out, stream)
|
||||
out.FreeAsync(stream)
|
||||
|
||||
// sync the stream
|
||||
// sync the stream
|
||||
cr.SynchronizeStream(&stream)
|
||||
}
|
||||
})
|
||||
@@ -68,24 +77,27 @@ To streamline device management we offer as part of `cuda_runtime` package metho
|
||||
|
||||
Runs a given function on a specific GPU device, ensuring that all CUDA calls within the function are executed on the selected device.
|
||||
|
||||
In Go, most concurrency can be done via Goroutines. However, there is no guarantee that a goroutine stays on a specific host thread.
|
||||
In Go, most concurrency can be done via Goroutines. However, there is no guarantee that a goroutine stays on a specific host thread.
|
||||
|
||||
`RunOnDevice` was designed to solve this caveat and insure that the goroutine will stay on a specific host thread.
|
||||
`RunOnDevice` was designed to solve this caveat and ensure that the goroutine will stay on a specific host thread.
|
||||
|
||||
`RunOnDevice` will lock a goroutine into a specific host thread, sets a current GPU device, runs a provided function, and unlocks the goroutine from the host thread after the provided function finishes.
|
||||
`RunOnDevice` locks a goroutine into a specific host thread, sets a current GPU device, runs a provided function, and unlocks the goroutine from the host thread after the provided function finishes.
|
||||
|
||||
While the goroutine is locked to the host thread, the Go runtime will not assign other goroutine's to that host thread.
|
||||
While the goroutine is locked to the host thread, the Go runtime will not assign other goroutines to that host thread.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `deviceId int`: The ID of the device on which to run the provided function. Device IDs start from 0.
|
||||
- `funcToRun func(args ...any)`: The function to be executed on the specified device.
|
||||
- `args ...any`: Arguments to be passed to `funcToRun`.
|
||||
- **`deviceId int`**: The ID of the device on which to run the provided function. Device IDs start from 0.
|
||||
- **`funcToRun func(args ...any)`**: The function to be executed on the specified device.
|
||||
- **`args ...any`**: Arguments to be passed to `funcToRun`.
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- The function `funcToRun` is executed in a new goroutine that is locked to a specific OS thread to ensure that all CUDA calls within the function target the specified device.
|
||||
- It's important to note that any goroutines launched within `funcToRun` are not automatically bound to the same GPU device. If necessary, `RunOnDevice` should be called again within such goroutines with the same `deviceId`.
|
||||
|
||||
:::note
|
||||
Any goroutines launched within `funcToRun` are not automatically bound to the same GPU device. If necessary, `RunOnDevice` should be called again within such goroutines with the same `deviceId`.
|
||||
:::
|
||||
|
||||
**Example:**
|
||||
|
||||
@@ -100,13 +112,17 @@ RunOnDevice(0, func(args ...any) {
|
||||
|
||||
Sets the active device for the current host thread. All subsequent CUDA calls made from this thread will target the specified device.
|
||||
|
||||
:::warning
|
||||
This function should not be used directly in conjunction with goroutines. If you want to run multi-gpu scenarios with goroutines you should use [RunOnDevice](#runondevice)
|
||||
:::
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `device int`: The ID of the device to set as the current device.
|
||||
- **`device int`**: The ID of the device to set as the current device.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- `CudaError`: Error code indicating the success or failure of the operation.
|
||||
- **`CudaError`**: Error code indicating the success or failure of the operation.
|
||||
|
||||
### `GetDeviceCount`
|
||||
|
||||
@@ -114,7 +130,7 @@ Retrieves the number of CUDA-capable devices available on the host.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- `(int, CudaError)`: The number of devices and an error code indicating the success or failure of the operation.
|
||||
- **`(int, CudaError)`**: The number of devices and an error code indicating the success or failure of the operation.
|
||||
|
||||
### `GetDevice`
|
||||
|
||||
@@ -122,7 +138,7 @@ Gets the ID of the currently active device for the calling host thread.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- `(int, CudaError)`: The ID of the current device and an error code indicating the success or failure of the operation.
|
||||
- **`(int, CudaError)`**: The ID of the current device and an error code indicating the success or failure of the operation.
|
||||
|
||||
### `GetDeviceFromPointer`
|
||||
|
||||
@@ -130,10 +146,10 @@ Retrieves the device associated with a given pointer.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `ptr unsafe.Pointer`: Pointer to query.
|
||||
- **`ptr unsafe.Pointer`**: Pointer to query.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- `int`: The device ID associated with the memory pointed to by `ptr`.
|
||||
- **`int`**: The device ID associated with the memory pointed to by `ptr`.
|
||||
|
||||
This documentation should provide a clear understanding of how to effectively manage multiple GPUs in Go applications using CUDA, with a particular emphasis on the `RunOnDevice` function for executing tasks on specific GPUs.
|
||||
This documentation should provide a clear understanding of how to effectively manage multiple GPUs in Go applications using CUDA, with a particular emphasis on the `RunOnDevice` function for executing tasks on specific GPUs.
|
||||
|
||||
@@ -1,40 +1,54 @@
|
||||
# NTT
|
||||
|
||||
### Supported curves
|
||||
|
||||
`bls12-377`, `bls12-381`, `bn254`, `bw6-761`
|
||||
|
||||
## NTT Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/ingonyama-zk/icicle/wrappers/golang/core"
|
||||
cr "github.com/ingonyama-zk/icicle/wrappers/golang/cuda_runtime"
|
||||
"github.com/ingonyama-zk/icicle/v2/wrappers/golang/core"
|
||||
cr "github.com/ingonyama-zk/icicle/v2/wrappers/golang/cuda_runtime"
|
||||
bn254 "github.com/ingonyama-zk/icicle/v2/wrappers/golang/curves/bn254"
|
||||
|
||||
"github.com/consensys/gnark-crypto/ecc/bn254/fr/fft"
|
||||
)
|
||||
|
||||
func Main() {
|
||||
// Obtain the default NTT configuration with a predefined coset generator.
|
||||
cfg := GetDefaultNttConfig()
|
||||
|
||||
// Define the size of the input scalars.
|
||||
size := 1 << 18
|
||||
func init() {
|
||||
cfg := bn254.GetDefaultNttConfig()
|
||||
initDomain(18, cfg)
|
||||
}
|
||||
|
||||
// Generate scalars for the NTT operation.
|
||||
scalars := GenerateScalars(size)
|
||||
func initDomain[T any](largestTestSize int, cfg core.NTTConfig[T]) core.IcicleError {
|
||||
rouMont, _ := fft.Generator(uint64(1 << largestTestSize))
|
||||
rou := rouMont.Bits()
|
||||
rouIcicle := bn254.ScalarField{}
|
||||
|
||||
// Set the direction of the NTT (forward or inverse).
|
||||
dir := core.KForward
|
||||
rouIcicle.FromLimbs(rou[:])
|
||||
e := bn254.InitDomain(rouIcicle, cfg.Ctx, false)
|
||||
return e
|
||||
}
|
||||
|
||||
// Allocate memory for the results of the NTT operation.
|
||||
results := make(core.HostSlice[ScalarField], size)
|
||||
func main() {
|
||||
// Obtain the default NTT configuration with a predefined coset generator.
|
||||
cfg := bn254.GetDefaultNttConfig()
|
||||
|
||||
// Perform the NTT operation.
|
||||
err := Ntt(scalars, dir, &cfg, results)
|
||||
if err != cr.CudaSuccess {
|
||||
panic("NTT operation failed")
|
||||
}
|
||||
// Define the size of the input scalars.
|
||||
size := 1 << 18
|
||||
|
||||
// Generate scalars for the NTT operation.
|
||||
scalars := bn254.GenerateScalars(size)
|
||||
|
||||
// Set the direction of the NTT (forward or inverse).
|
||||
dir := core.KForward
|
||||
|
||||
// Allocate memory for the results of the NTT operation.
|
||||
results := make(core.HostSlice[bn254.ScalarField], size)
|
||||
|
||||
// Perform the NTT operation.
|
||||
err := bn254.Ntt(scalars, dir, &cfg, results)
|
||||
if err.CudaErrorCode != cr.CudaSuccess {
|
||||
panic("NTT operation failed")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -46,14 +60,14 @@ func Ntt[T any](scalars core.HostOrDeviceSlice, dir core.NTTDir, cfg *core.NTTCo
|
||||
|
||||
### Parameters
|
||||
|
||||
- **scalars**: A slice containing the input scalars for the transform. It can reside either in host memory or device memory.
|
||||
- **dir**: The direction of the NTT operation (`KForward` or `KInverse`).
|
||||
- **cfg**: A pointer to an `NTTConfig` object, containing configuration options for the NTT operation.
|
||||
- **results**: A slice where the results of the NTT operation will be stored. This slice can be in host or device memory.
|
||||
- **`scalars`**: A slice containing the input scalars for the transform. It can reside either in host memory or device memory.
|
||||
- **`dir`**: The direction of the NTT operation (`KForward` or `KInverse`).
|
||||
- **`cfg`**: A pointer to an `NTTConfig` object, containing configuration options for the NTT operation.
|
||||
- **`results`**: A slice where the results of the NTT operation will be stored. This slice can be in host or device memory.
|
||||
|
||||
### Return Value
|
||||
|
||||
- **CudaError**: Returns a CUDA error code indicating the success or failure of the NTT operation.
|
||||
- **`CudaError`**: Returns a CUDA error code indicating the success or failure of the NTT operation.
|
||||
|
||||
## NTT Configuration (NTTConfig)
|
||||
|
||||
@@ -75,15 +89,15 @@ type NTTConfig[T any] struct {
|
||||
|
||||
### Fields
|
||||
|
||||
- **Ctx**: Device context containing details like device ID and stream ID.
|
||||
- **CosetGen**: Coset generator used for coset (i)NTTs, defaulting to no coset being used.
|
||||
- **BatchSize**: The number of NTTs to compute in one operation, defaulting to 1.
|
||||
- **ColumnsBatch**: If true the function will compute the NTTs over the columns of the input matrix and not over the rows. Defaults to `false`.
|
||||
- **Ordering**: Ordering of inputs and outputs (`KNN`, `KNR`, `KRN`, `KRR`, `KMN`, `KNM`), affecting how data is arranged.
|
||||
- **areInputsOnDevice**: Indicates if input scalars are located on the device.
|
||||
- **areOutputsOnDevice**: Indicates if results are stored on the device.
|
||||
- **IsAsync**: Controls whether the NTT operation runs asynchronously.
|
||||
- **NttAlgorithm**: Explicitly select the NTT algorithm. Default value: Auto (the implementation selects radix-2 or mixed-radix algorithm based on heuristics).
|
||||
- **`Ctx`**: Device context containing details like device ID and stream ID.
|
||||
- **`CosetGen`**: Coset generator used for coset (i)NTTs, defaulting to no coset being used.
|
||||
- **`BatchSize`**: The number of NTTs to compute in one operation, defaulting to 1.
|
||||
- **`ColumnsBatch`**: If true the function will compute the NTTs over the columns of the input matrix and not over the rows. Defaults to `false`.
|
||||
- **`Ordering`**: Ordering of inputs and outputs (`KNN`, `KNR`, `KRN`, `KRR`, `KMN`, `KNM`), affecting how data is arranged.
|
||||
- **`areInputsOnDevice`**: Indicates if input scalars are located on the device.
|
||||
- **`areOutputsOnDevice`**: Indicates if results are stored on the device.
|
||||
- **`IsAsync`**: Controls whether the NTT operation runs asynchronously.
|
||||
- **`NttAlgorithm`**: Explicitly select the NTT algorithm. Default value: Auto (the implementation selects radix-2 or mixed-radix algorithm based on heuristics).
|
||||
|
||||
### Default Configuration
|
||||
|
||||
@@ -102,3 +116,36 @@ func InitDomain(primitiveRoot ScalarField, ctx cr.DeviceContext, fastTwiddles bo
|
||||
```
|
||||
|
||||
This function initializes the domain with a given primitive root, optionally using fast twiddle factors to optimize the computation.
|
||||
|
||||
### Releasing the domain
|
||||
|
||||
The `ReleaseDomain` function is responsible for releasing the resources associated with a specific domain in the CUDA device context.
|
||||
|
||||
```go
|
||||
func ReleaseDomain(ctx cr.DeviceContext) core.IcicleError
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- **`ctx`**: a reference to the `DeviceContext` object, which represents the CUDA device context.
|
||||
|
||||
### Return Value
|
||||
|
||||
The function returns a `core.IcicleError`, which represents the result of the operation. If the operation is successful, the function returns `core.IcicleErrorCode(0)`.
|
||||
|
||||
### Example
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/icicle-crypto/icicle-core/cr"
|
||||
"github.com/icicle-crypto/icicle-core/core"
|
||||
)
|
||||
|
||||
func example() {
|
||||
cfg := GetDefaultNttConfig()
|
||||
err := ReleaseDomain(cfg.Ctx)
|
||||
if err != nil {
|
||||
// Handle the error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -2,104 +2,112 @@
|
||||
|
||||
## Overview
|
||||
|
||||
The VecOps API provides efficient vector operations such as addition, subtraction, and multiplication.
|
||||
Icicle exposes a number of vector operations which a user can use:
|
||||
|
||||
## Example
|
||||
* The VecOps API provides efficient vector operations such as addition, subtraction, and multiplication.
|
||||
* MatrixTranspose API allows a user to perform a transpose on a vector representation of a matrix
|
||||
|
||||
### Vector addition
|
||||
## VecOps API Documentation
|
||||
|
||||
### Example
|
||||
|
||||
#### Vector addition
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/ingonyama-zk/icicle/wrappers/golang/core"
|
||||
cr "github.com/ingonyama-zk/icicle/wrappers/golang/cuda_runtime"
|
||||
"github.com/ingonyama-zk/icicle/v2/wrappers/golang/core"
|
||||
cr "github.com/ingonyama-zk/icicle/v2/wrappers/golang/cuda_runtime"
|
||||
bn254 "github.com/ingonyama-zk/icicle/v2/wrappers/golang/curves/bn254"
|
||||
)
|
||||
|
||||
func main() {
|
||||
testSize := 1 << 12
|
||||
a := GenerateScalars(testSize)
|
||||
b := GenerateScalars(testSize)
|
||||
out := make(core.HostSlice[ScalarField], testSize)
|
||||
cfg := core.DefaultVecOpsConfig()
|
||||
testSize := 1 << 12
|
||||
a := bn254.GenerateScalars(testSize)
|
||||
b := bn254.GenerateScalars(testSize)
|
||||
out := make(core.HostSlice[bn254.ScalarField], testSize)
|
||||
cfg := core.DefaultVecOpsConfig()
|
||||
|
||||
// Perform vector addition
|
||||
err := VecOp(a, b, out, cfg, core.Add)
|
||||
if err != cr.CudaSuccess {
|
||||
panic("Vector addition failed")
|
||||
}
|
||||
// Perform vector multiplication
|
||||
err := bn254.VecOp(a, b, out, cfg, core.Add)
|
||||
if err != cr.CudaSuccess {
|
||||
panic("Vector addition failed")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Vector Subtraction
|
||||
#### Vector Subtraction
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/ingonyama-zk/icicle/wrappers/golang/core"
|
||||
cr "github.com/ingonyama-zk/icicle/wrappers/golang/cuda_runtime"
|
||||
"github.com/ingonyama-zk/icicle/v2/wrappers/golang/core"
|
||||
cr "github.com/ingonyama-zk/icicle/v2/wrappers/golang/cuda_runtime"
|
||||
bn254 "github.com/ingonyama-zk/icicle/v2/wrappers/golang/curves/bn254"
|
||||
)
|
||||
|
||||
func main() {
|
||||
testSize := 1 << 12
|
||||
a := GenerateScalars(testSize)
|
||||
b := GenerateScalars(testSize)
|
||||
out := make(core.HostSlice[ScalarField], testSize)
|
||||
cfg := core.DefaultVecOpsConfig()
|
||||
testSize := 1 << 12
|
||||
a := bn254.GenerateScalars(testSize)
|
||||
b := bn254.GenerateScalars(testSize)
|
||||
out := make(core.HostSlice[bn254.ScalarField], testSize)
|
||||
cfg := core.DefaultVecOpsConfig()
|
||||
|
||||
// Perform vector subtraction
|
||||
err := VecOp(a, b, out, cfg, core.Sub)
|
||||
if err != cr.CudaSuccess {
|
||||
panic("Vector subtraction failed")
|
||||
}
|
||||
// Perform vector multiplication
|
||||
err := bn254.VecOp(a, b, out, cfg, core.Sub)
|
||||
if err != cr.CudaSuccess {
|
||||
panic("Vector subtraction failed")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Vector Multiplication
|
||||
#### Vector Multiplication
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/ingonyama-zk/icicle/wrappers/golang/core"
|
||||
cr "github.com/ingonyama-zk/icicle/wrappers/golang/cuda_runtime"
|
||||
"github.com/ingonyama-zk/icicle/v2/wrappers/golang/core"
|
||||
cr "github.com/ingonyama-zk/icicle/v2/wrappers/golang/cuda_runtime"
|
||||
bn254 "github.com/ingonyama-zk/icicle/v2/wrappers/golang/curves/bn254"
|
||||
)
|
||||
|
||||
func main() {
|
||||
testSize := 1 << 12
|
||||
a := GenerateScalars(testSize)
|
||||
b := GenerateScalars(testSize)
|
||||
out := make(core.HostSlice[ScalarField], testSize)
|
||||
cfg := core.DefaultVecOpsConfig()
|
||||
testSize := 1 << 12
|
||||
a := bn254.GenerateScalars(testSize)
|
||||
b := bn254.GenerateScalars(testSize)
|
||||
out := make(core.HostSlice[bn254.ScalarField], testSize)
|
||||
cfg := core.DefaultVecOpsConfig()
|
||||
|
||||
// Perform vector multiplication
|
||||
err := VecOp(a, b, out, cfg, core.Mul)
|
||||
if err != cr.CudaSuccess {
|
||||
panic("Vector multiplication failed")
|
||||
}
|
||||
// Perform vector multiplication
|
||||
err := bn254.VecOp(a, b, out, cfg, core.Mul)
|
||||
if err != cr.CudaSuccess {
|
||||
panic("Vector multiplication failed")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## VecOps Method
|
||||
### VecOps Method
|
||||
|
||||
```go
|
||||
func VecOp(a, b, out core.HostOrDeviceSlice, config core.VecOpsConfig, op core.VecOps) (ret cr.CudaError)
|
||||
```
|
||||
|
||||
### Parameters
|
||||
#### Parameters
|
||||
|
||||
- **a**: The first input vector.
|
||||
- **b**: The second input vector.
|
||||
- **out**: The output vector where the result of the operation will be stored.
|
||||
- **config**: A `VecOpsConfig` object containing various configuration options for the vector operations.
|
||||
- **op**: The operation to perform, specified as one of the constants (`Sub`, `Add`, `Mul`) from the `VecOps` type.
|
||||
- **`a`**: The first input vector.
|
||||
- **`b`**: The second input vector.
|
||||
- **`out`**: The output vector where the result of the operation will be stored.
|
||||
- **`config`**: A `VecOpsConfig` object containing various configuration options for the vector operations.
|
||||
- **`op`**: The operation to perform, specified as one of the constants (`Sub`, `Add`, `Mul`) from the `VecOps` type.
|
||||
|
||||
### Return Value
|
||||
#### Return Value
|
||||
|
||||
- **CudaError**: Returns a CUDA error code indicating the success or failure of the vector operation.
|
||||
- **`CudaError`**: Returns a CUDA error code indicating the success or failure of the vector operation.
|
||||
|
||||
## VecOpsConfig
|
||||
### VecOpsConfig
|
||||
|
||||
The `VecOpsConfig` structure holds configuration parameters for the vector operations, allowing customization of its behavior.
|
||||
|
||||
@@ -109,24 +117,72 @@ type VecOpsConfig struct {
|
||||
isAOnDevice bool
|
||||
isBOnDevice bool
|
||||
isResultOnDevice bool
|
||||
IsResultMontgomeryForm bool
|
||||
IsAsync bool
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
#### Fields
|
||||
|
||||
- **Ctx**: Device context containing details like device ID and stream ID.
|
||||
- **isAOnDevice**: Indicates if vector `a` is located on the device.
|
||||
- **isBOnDevice**: Indicates if vector `b` is located on the device.
|
||||
- **isResultOnDevice**: Specifies where the result vector should be stored (device or host memory).
|
||||
- **IsResultMontgomeryForm**: Determines if the result vector should be in Montgomery form.
|
||||
- **IsAsync**: Controls whether the vector operation runs asynchronously.
|
||||
|
||||
### Default Configuration
|
||||
#### Default Configuration
|
||||
|
||||
Use `DefaultVecOpsConfig` to obtain a default configuration, customizable as needed.
|
||||
|
||||
```go
|
||||
func DefaultVecOpsConfig() VecOpsConfig
|
||||
```
|
||||
|
||||
## MatrixTranspose API Documentation
|
||||
|
||||
This section describes the functionality of the `TransposeMatrix` function used for matrix transposition.
|
||||
|
||||
The function takes a matrix represented as a 1D slice and transposes it, storing the result in another 1D slice.
|
||||
|
||||
### Function
|
||||
|
||||
```go
|
||||
func TransposeMatrix(in, out core.HostOrDeviceSlice, columnSize, rowSize int, ctx cr.DeviceContext, onDevice, isAsync bool) (ret core.IcicleError)
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
- **`in`**: The input matrix is a `core.HostOrDeviceSlice`, stored as a 1D slice.
|
||||
- **`out`**: The output matrix is a `core.HostOrDeviceSlice`, which will be the transpose of the input matrix, stored as a 1D slice.
|
||||
- **`columnSize`**: The number of columns in the input matrix.
|
||||
- **`rowSize`**: The number of rows in the input matrix.
|
||||
- **`ctx`**: The device context `cr.DeviceContext` to be used for the matrix transpose operation.
|
||||
- **`onDevice`**: Indicates whether the input and output slices are stored on the device (GPU) or the host (CPU).
|
||||
- **`isAsync`**: Indicates whether the matrix transpose operation should be executed asynchronously.
|
||||
|
||||
## Return Value
|
||||
|
||||
The function returns a `core.IcicleError` value, which represents the result of the matrix transpose operation. If the operation is successful, the returned value will be `0`.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```go
|
||||
var input = make(core.HostSlice[ScalarField], 20)
|
||||
var output = make(core.HostSlice[ScalarField], 20)
|
||||
|
||||
// Populate the input matrix
|
||||
// ...
|
||||
|
||||
// Get device context
|
||||
ctx, _ := cr.GetDefaultDeviceContext()
|
||||
|
||||
// Transpose the matrix
|
||||
err := TransposeMatrix(input, output, 5, 4, ctx, false, false)
|
||||
if err.IcicleErrorCode != core.IcicleErrorCode(0) {
|
||||
// Handle the error
|
||||
}
|
||||
|
||||
// Use the transposed matrix
|
||||
// ...
|
||||
```
|
||||
|
||||
In this example, the `TransposeMatrix` function is used to transpose a 5x4 matrix stored in a 1D slice. The input and output slices are stored on the host (CPU), and the operation is executed synchronously.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ICICLE integrated provers
|
||||
|
||||
ICICLE has been used by companies and projects such as [Celer Network](https://github.com/celer-network), [Consensys Gnark](https://github.com/Consensys/gnark), [EZKL](https://blog.ezkl.xyz/post/acceleration/) and others to accelerate their ZK proving pipeline.
|
||||
ICICLE has been used by companies and projects such as [Celer Network](https://github.com/celer-network), [Consensys Gnark](https://github.com/Consensys/gnark), [EZKL](https://blog.ezkl.xyz/post/acceleration/), [ZKWASM](https://twitter.com/DelphinusLab/status/1762604988797513915) and others to accelerate their ZK proving pipeline.
|
||||
|
||||
Many of these integrations have been a collaboration between Ingonyama and the integrating company. We have learned a lot about designing GPU based ZK provers.
|
||||
|
||||
|
||||
@@ -8,24 +8,24 @@ This guide is oriented towards developers who want to start writing code with th
|
||||
|
||||
The diagram above displays the general architecture of ICICLE and the API layers that exist. The CUDA API, which we also call ICICLE Core, is the lowest level and is comprised of CUDA kernels which implement all primitives such as MSM as well as C++ wrappers which expose these methods for different curves.
|
||||
|
||||
ICICLE Core compiles into a static library. This library can be used with our official Golang and Rust wrappers or you can implement a wrapper for it in any language.
|
||||
ICICLE Core compiles into a static library. This library can be used with our official Golang and Rust wrappers or linked with your C++ project. You can also implement a wrapper for it in any other language.
|
||||
|
||||
Based on this dependency architecture, the ICICLE repository has three main sections, each of which is independent from the other.
|
||||
Based on this dependency architecture, the ICICLE repository has three main sections:
|
||||
|
||||
- ICICLE core
|
||||
- ICICLE Rust bindings
|
||||
- ICICLE Golang bindings
|
||||
- [ICICLE Core](#icicle-core)
|
||||
- [ICICLE Rust bindings](#icicle-rust-and-golang-bindings)
|
||||
- [ICICLE Golang bindings](#icicle-rust-and-golang-bindings)
|
||||
|
||||
### ICICLE Core
|
||||
|
||||
[ICICLE core](https://github.com/ingonyama-zk/icicle/tree/main/icicle) contains all the low level CUDA code implementing primitives such as [points](https://github.com/ingonyama-zk/icicle/tree/main/icicle/primitives) and [MSM](https://github.com/ingonyama-zk/icicle/tree/main/icicle/appUtils/msm). There also exists higher level C++ wrappers to expose the low level CUDA primitives ([example](https://github.com/ingonyama-zk/icicle/blob/c1a32a9879a7612916e05aa3098f76144de4109e/icicle/appUtils/msm/msm.cu#L1)).
|
||||
[ICICLE Core](/icicle/core) is a library that directly works with GPU by defining CUDA kernels and algorithms that invoke them. It contains code for [fast field arithmetic](https://github.com/ingonyama-zk/icicle/tree/main/icicle/include/field/field.cuh), cryptographic primitives used in ZK such as [NTT](https://github.com/ingonyama-zk/icicle/tree/main/icicle/src/ntt/), [MSM](https://github.com/ingonyama-zk/icicle/tree/main/icicle/src/msm/), [Poseidon Hash](https://github.com/ingonyama-zk/icicle/tree/main/icicle/src/poseidon/), [Polynomials](https://github.com/ingonyama-zk/icicle/tree/main/icicle/src/polynomials/) and others.
|
||||
|
||||
ICICLE Core would typically be compiled into a static library and used in a third party language such as Rust or Golang.
|
||||
ICICLE Core would typically be compiled into a static library and either used in a third party language such as Rust or Golang, or linked with your own C++ project.
|
||||
|
||||
### ICICLE Rust and Golang bindings
|
||||
|
||||
- [ICICLE Rust bindings](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/rust)
|
||||
- [ICICLE Golang bindings](https://github.com/ingonyama-zk/icicle/tree/main/goicicle)
|
||||
- [ICICLE Rust bindings](/icicle/rust-bindings)
|
||||
- [ICICLE Golang bindings](/icicle/golang-bindings)
|
||||
|
||||
These bindings allow you to easily use ICICLE in a Rust or Golang project. Setting up Golang bindings requires a bit of extra steps compared to the Rust bindings which utilize the `cargo build` tool.
|
||||
|
||||
@@ -33,6 +33,12 @@ These bindings allow you to easily use ICICLE in a Rust or Golang project. Setti
|
||||
|
||||
This guide assumes that you have a Linux or Windows machine with an Nvidia GPU installed. If you don't have access to an Nvidia GPU you can access one for free on [Google Colab](https://colab.google/).
|
||||
|
||||
:::info note
|
||||
|
||||
ICICLE can only run on Linux or Windows. **MacOS is not supported**.
|
||||
|
||||
:::
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- NVCC (version 12.0 or newer)
|
||||
@@ -50,9 +56,9 @@ If you don't wish to install these prerequisites you can follow this tutorial us
|
||||
|
||||
### Setting up ICICLE and running tests
|
||||
|
||||
The objective of this guide is to make sure you can run the ICICLE Core, Rust and Golang tests. Achieving this will ensure you know how to setup ICICLE and run a ICICLE program. For simplicity, we will be using the ICICLE docker container as our environment, however, you may install the prerequisites on your machine and follow the same commands in your terminal.
|
||||
The objective of this guide is to make sure you can run the ICICLE Core, Rust and Golang tests. Achieving this will ensure you know how to setup ICICLE and run an ICICLE program. For simplicity, we will be using the ICICLE docker container as our environment, however, you may install the prerequisites on your machine and [skip](#icicle-core-1) the docker section.
|
||||
|
||||
#### Setting up our environment
|
||||
#### Setting up environment with Docker
|
||||
|
||||
Lets begin by cloning the ICICLE repository:
|
||||
|
||||
@@ -105,29 +111,23 @@ ICICLE Core is found under [`<project_root>/icicle`](https://github.com/ingonyam
|
||||
cd icicle
|
||||
```
|
||||
|
||||
We are going to compile ICICLE for a specific curve
|
||||
For this example, we are going to compile ICICLE for a `bn254` curve. However other compilation strategies are supported.
|
||||
|
||||
```sh
|
||||
mkdir -p build
|
||||
cmake -S . -B build -DCURVE=bn254 -DBUILD_TESTS=ON
|
||||
cmake --build build
|
||||
cmake --build build -j
|
||||
```
|
||||
|
||||
`-DBUILD_TESTS=ON` compiles the tests, without this flag `ctest` won't work.
|
||||
`-DCURVE=bn254` tells the compiler which curve to build. You can find a list of supported curves [here](https://github.com/ingonyama-zk/icicle/tree/main/icicle/curves).
|
||||
`-DBUILD_TESTS` option compiles the tests, without this flag `ctest` won't work.
|
||||
`-DCURVE` option tells the compiler which curve to build. You can find a list of supported curves [here](https://github.com/ingonyama-zk/icicle/tree/main/icicle/cmake/CurvesCommon.cmake#L2).
|
||||
|
||||
The output in `build` folder should include the static libraries for the compiled curve.
|
||||
|
||||
:::info
|
||||
|
||||
Make sure to only use `-DBUILD_TESTS=ON` for running tests as the archive output will only be available when `-DBUILD_TESTS=ON` is not supplied.
|
||||
|
||||
:::
|
||||
|
||||
To run the test
|
||||
|
||||
```sh
|
||||
cd build
|
||||
cd build/tests
|
||||
ctest
|
||||
```
|
||||
|
||||
@@ -165,11 +165,56 @@ cargo bench
|
||||
|
||||
#### ICICLE Golang
|
||||
|
||||
Golang is WIP in v1, coming soon. Please checkout a previous [release v0.1.0](https://github.com/ingonyama-zk/icicle/releases/tag/v0.1.0) for golang bindings.
|
||||
The Golang bindings require compiling ICICLE Core first. We supply a [build script](https://github.com/ingonyama-zk/icicle/blob/main/wrappers/golang/build.sh) to help build what you need.
|
||||
|
||||
Script usage:
|
||||
|
||||
```sh
|
||||
./build.sh [-curve=<curve>] [-field=<field>] [-hash=<hash>] [-cuda_version=<version>] [-g2] [-ecntt] [-devmode]
|
||||
|
||||
curve - The name of the curve to build or "all" to build all supported curves
|
||||
field - The name of the field to build or "all" to build all supported fields
|
||||
hash - The name of the hash to build or "all" to build all supported hashes
|
||||
-g2 - Optional - build with G2 enabled
|
||||
-ecntt - Optional - build with ECNTT enabled
|
||||
-devmode - Optional - build in devmode
|
||||
```
|
||||
|
||||
:::note
|
||||
|
||||
If more than one curve or more than one field or more than one hash is supplied, the last one supplied will be built
|
||||
|
||||
:::
|
||||
|
||||
Once the library has been built, you can use and test the Golang bindings.
|
||||
|
||||
To test a specific curve, field or hash, change to it's directory and then run:
|
||||
|
||||
```sh
|
||||
go test ./tests -count=1 -failfast -timeout 60m -p 2 -v
|
||||
```
|
||||
|
||||
You will be able to see each test that runs, how long it takes and whether it passed or failed
|
||||
|
||||
### Running ICICLE examples
|
||||
|
||||
ICICLE examples can be found [here](https://github.com/ingonyama-zk/icicle-examples) these examples cover some simple use cases using C++, rust and golang.
|
||||
ICICLE examples can be found [here](https://github.com/ingonyama-zk/icicle/tree/main/examples) these examples cover some simple use cases using C++, rust and golang.
|
||||
|
||||
Lets run one of our C++ examples, in this case the [MSM example](https://github.com/ingonyama-zk/icicle/blob/main/examples/c%2B%2B/msm/example.cu).
|
||||
|
||||
```sh
|
||||
cd examples/c++/msm
|
||||
./compile.sh
|
||||
./run.sh
|
||||
```
|
||||
|
||||
:::tip
|
||||
|
||||
Read through the compile.sh and CMakeLists.txt to understand how to link your own C++ project with ICICLE
|
||||
|
||||
:::
|
||||
|
||||
#### Running with Docker
|
||||
|
||||
In each example directory, ZK-container files are located in a subdirectory `.devcontainer`.
|
||||
|
||||
@@ -180,21 +225,6 @@ msm/
|
||||
└── Dockerfile
|
||||
```
|
||||
|
||||
Lets run one of our C++ examples, in this case the [MSM example](https://github.com/ingonyama-zk/icicle-examples/blob/main/c%2B%2B/msm/example.cu).
|
||||
|
||||
Clone the repository
|
||||
|
||||
```sh
|
||||
git clone https://github.com/ingonyama-zk/icicle-examples.git
|
||||
cd icicle-examples
|
||||
```
|
||||
|
||||
Enter the test directory
|
||||
|
||||
```sh
|
||||
cd c++/msm
|
||||
```
|
||||
|
||||
Now lets build our docker file and run the test inside it. Make sure you have installed the [optional prerequisites](#optional-prerequisites).
|
||||
|
||||
```sh
|
||||
@@ -207,54 +237,11 @@ Lets start and enter the container
|
||||
docker run -it --rm --gpus all -v .:/icicle-example icicle-example-msm
|
||||
```
|
||||
|
||||
to run the example
|
||||
Inside the container you can run the same commands:
|
||||
|
||||
```sh
|
||||
rm -rf build
|
||||
mkdir -p build
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
./build/example
|
||||
./compile.sh
|
||||
./run.sh
|
||||
```
|
||||
|
||||
You can now experiment with our other examples, perhaps try to run a rust or golang example next.
|
||||
|
||||
## Writing new bindings for ICICLE
|
||||
|
||||
Since ICICLE Core is written in CUDA / C++ its really simple to generate static libraries. These static libraries can be installed on any system and called by higher level languages such as Golang.
|
||||
|
||||
static libraries can be loaded into memory once and used by multiple programs, reducing memory usage and potentially improving performance. They also allow you to separate functionality into distinct modules so your static library may need to compile only specific features that you want to use.
|
||||
|
||||
Lets review the Golang bindings since its a pretty verbose example (compared to rust which hides it pretty well) of using static libraries. Golang has a library named `CGO` which can be used to link static libraries. Here's a basic example on how you can use cgo to link these libraries:
|
||||
|
||||
```go
|
||||
/*
|
||||
#cgo LDFLAGS: -L/path/to/shared/libs -lbn254 -lbls12_381 -lbls12_377 -lbw6_671
|
||||
#include "icicle.h" // make sure you use the correct header file(s)
|
||||
*/
|
||||
import "C"
|
||||
|
||||
func main() {
|
||||
// Now you can call the C functions from the ICICLE libraries.
|
||||
// Note that C function calls are prefixed with 'C.' in Go code.
|
||||
|
||||
out := (*C.BN254_projective_t)(unsafe.Pointer(p))
|
||||
in := (*C.BN254_affine_t)(unsafe.Pointer(affine))
|
||||
|
||||
C.projective_from_affine_bn254(out, in)
|
||||
}
|
||||
```
|
||||
|
||||
The comments on the first line tell `CGO` which libraries to import as well as which header files to include. You can then call methods which are part of the static library and defined in the header file, `C.projective_from_affine_bn254` is an example.
|
||||
|
||||
If you wish to create your own bindings for a language of your choice we suggest you start by investigating how you can call static libraries.
|
||||
|
||||
### ICICLE Adapters
|
||||
|
||||
One of the core ideas behind ICICLE is that developers can gradually accelerate their provers. Many protocols are written using other cryptographic libraries and completely replacing them may be complex and time consuming.
|
||||
|
||||
Therefore we offer adapters for various popular libraries, these adapters allow us to convert points and scalars between different formats defined by various libraries. Here is a list:
|
||||
|
||||
Golang adapters:
|
||||
|
||||
- [Gnark crypto adapter](https://github.com/ingonyama-zk/iciclegnark)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
:::info
|
||||
|
||||
If you are looking for the Multi GPU API documentation refer here for [Rust](./rust-bindings/multi-gpu.md).
|
||||
If you are looking for the Multi GPU API documentation refer [here](./rust-bindings/multi-gpu.md) for Rust and [here](./golang-bindings/multi-gpu.md) for Golang.
|
||||
|
||||
:::
|
||||
|
||||
@@ -10,12 +10,11 @@ One common challenge with Zero-Knowledge computation is managing the large input
|
||||
|
||||
Multi-GPU programming involves developing software to operate across multiple GPU devices. Lets first explore different approaches to Multi-GPU programming then we will cover how ICICLE allows you to easily develop youR ZK computations to run across many GPUs.
|
||||
|
||||
|
||||
## Approaches to Multi GPU programming
|
||||
|
||||
There are many [different strategies](https://github.com/NVIDIA/multi-gpu-programming-models) available for implementing multi GPU, however, it can be split into two categories.
|
||||
|
||||
### GPU Server approach
|
||||
### GPU Server approach
|
||||
|
||||
This approach usually involves a single or multiple CPUs opening threads to read / write from multiple GPUs. You can think about it as a scaled up HOST - Device model.
|
||||
|
||||
@@ -23,8 +22,7 @@ This approach usually involves a single or multiple CPUs opening threads to read
|
||||
|
||||
This approach won't let us tackle larger computation sizes but it will allow us to compute multiple computations which we wouldn't be able to load onto a single GPU.
|
||||
|
||||
For example let's say that you had to compute two MSMs of size 2^26 on a 16GB VRAM GPU you would normally have to perform them asynchronously. However, if you double the number of GPUs in your system you can now run them in parallel.
|
||||
|
||||
For example let's say that you had to compute two MSMs of size 2^26 on a 16GB VRAM GPU you would normally have to perform them asynchronously. However, if you double the number of GPUs in your system you can now run them in parallel.
|
||||
|
||||
### Inter GPU approach
|
||||
|
||||
@@ -32,18 +30,17 @@ This approach involves a more sophisticated approach to multi GPU computation. U
|
||||
|
||||
This approach requires redesigning the algorithm at the software level to be compatible with splitting amongst devices. In some cases, to lower latency to a minimum, special inter GPU connections would be installed on a server to allow direct communication between multiple GPUs.
|
||||
|
||||
|
||||
# Writing ICICLE Code for Multi GPUs
|
||||
## Writing ICICLE Code for Multi GPUs
|
||||
|
||||
The approach we have taken for the moment is a GPU Server approach; we assume you have a machine with multiple GPUs and you wish to run some computation on each GPU.
|
||||
|
||||
To dive deeper and learn about the API check out the docs for our different ICICLE API
|
||||
|
||||
- [Rust Multi GPU APIs](./rust-bindings/multi-gpu.md)
|
||||
- [Golang Multi GPU APIs](./golang-bindings/multi-gpu.md)
|
||||
- C++ Multi GPU APIs
|
||||
|
||||
|
||||
## Best practices
|
||||
## Best practices
|
||||
|
||||
- Never hardcode device IDs, if you want your software to take advantage of all GPUs on a machine use methods such as `get_device_count` to support arbitrary number of GPUs.
|
||||
|
||||
@@ -57,7 +54,7 @@ Multi GPU support should work with ZK-Containers by simply defining which device
|
||||
docker run -it --gpus '"device=0,2"' zk-container-image
|
||||
```
|
||||
|
||||
If you wish to expose all GPUs
|
||||
If you wish to expose all GPUs
|
||||
|
||||
```sh
|
||||
docker run --gpus all zk-container-image
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
|
||||
[](https://github.com/ingonyama-zk/icicle/releases)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[ICICLE](https://github.com/ingonyama-zk/icicle) is a cryptography library for ZK using GPUs. ICICLE implements blazing fast cryptographic primitives such as EC operations, MSM, NTT, Poseidon hash and more on GPU.
|
||||
|
||||
ICICLE allows developers with minimal GPU experience to effortlessly accelerate their ZK application; from our experiments, even the most naive implementation may yield 10X improvement in proving times.
|
||||
@@ -17,28 +13,26 @@ ICICLE has been used by many leading ZK companies such as [Celer Network](https:
|
||||
We understand that not all developers have access to a GPU and we don't want this to limit anyone from developing with ICICLE.
|
||||
Here are some ways we can help you gain access to GPUs:
|
||||
|
||||
:::note
|
||||
|
||||
If none of the following options suit your needs, contact us on [telegram](https://t.me/RealElan) for assistance. We're committed to ensuring that a lack of a GPU doesn't become a bottleneck for you. If you need help with setup or any other issues, we're here to help you.
|
||||
|
||||
:::
|
||||
|
||||
### Grants
|
||||
|
||||
At Ingonyama we are interested in accelerating the progress of ZK and cryptography. If you are an engineer, developer or an academic researcher we invite you to checkout [our grant program](https://www.ingonyama.com/blog/icicle-for-researchers-grants-challenges). We will give you access to GPUs and even pay you to do your dream research!
|
||||
|
||||
### Google Colab
|
||||
|
||||
This is a great way to get started with ICICLE instantly. Google Colab offers free GPU access to a NVIDIA T4 instance, it's acquired with 16 GB of memory which should be enough for experimenting and even prototyping with ICICLE.
|
||||
This is a great way to get started with ICICLE instantly. Google Colab offers free GPU access to a NVIDIA T4 instance with 16 GB of memory which should be enough for experimenting and even prototyping with ICICLE.
|
||||
|
||||
For an extensive guide on how to setup Google Colab with ICICLE refer to [this article](./colab-instructions.md).
|
||||
|
||||
If none of these options are appropriate for you reach out to us on [telegram](https://t.me/RealElan) we will do our best to help you.
|
||||
|
||||
### Vast.ai
|
||||
|
||||
[Vast.ai](https://vast.ai/) is a global GPU marketplace where you can rent many different types of GPUs by the hour for [competitive pricing](https://vast.ai/pricing). They provide on-demand and interruptible rentals depending on your need or use case; you can learn more about their rental types [here](https://vast.ai/faq#rental-types).
|
||||
|
||||
:::note
|
||||
|
||||
If none of these options suit your needs, contact us on [telegram](https://t.me/RealElan) for assistance. We're committed to ensuring that a lack of a GPU doesn't become a bottleneck for you. If you need help with setup or any other issues, we're here to do our best to help you.
|
||||
|
||||
:::
|
||||
|
||||
## What can you do with ICICLE?
|
||||
|
||||
[ICICLE](https://github.com/ingonyama-zk/icicle) can be used in the same way you would use any other cryptography library. While developing and integrating ICICLE into many proof systems, we found some use case categories:
|
||||
|
||||
27
docs/docs/icicle/polynomials/ffi.uml
Normal file
27
docs/docs/icicle/polynomials/ffi.uml
Normal file
@@ -0,0 +1,27 @@
|
||||
@startuml
|
||||
skinparam componentStyle uml2
|
||||
|
||||
' Define Components
|
||||
component "C++ Template\nComponent" as CppTemplate {
|
||||
[Parameterizable Interface]
|
||||
}
|
||||
component "C API Wrapper\nComponent" as CApiWrapper {
|
||||
[C API Interface]
|
||||
}
|
||||
component "Rust Code\nComponent" as RustCode {
|
||||
[Macro Interface\n(Template Instantiation)]
|
||||
}
|
||||
|
||||
' Define Artifact
|
||||
artifact "Static Library\n«artifact»" as StaticLib
|
||||
|
||||
' Connections
|
||||
CppTemplate -down-> CApiWrapper : Instantiates
|
||||
CApiWrapper .down.> StaticLib : Compiles into
|
||||
RustCode -left-> StaticLib : Links against\nand calls via FFI
|
||||
|
||||
' Notes
|
||||
note right of CppTemplate : Generic C++\ntemplate implementation
|
||||
note right of CApiWrapper : Exposes C API for FFI\nto Rust/Go
|
||||
note right of RustCode : Uses macros to\ninstantiate templates
|
||||
@enduml
|
||||
86
docs/docs/icicle/polynomials/hw_backends.uml
Normal file
86
docs/docs/icicle/polynomials/hw_backends.uml
Normal file
@@ -0,0 +1,86 @@
|
||||
@startuml
|
||||
|
||||
' Define Interface for Polynomial Backend Operations
|
||||
interface IPolynomialBackend {
|
||||
+add()
|
||||
+subtract()
|
||||
+multiply()
|
||||
+divide()
|
||||
+evaluate()
|
||||
}
|
||||
|
||||
' Define Interface for Polynomial Context (State Management)
|
||||
interface IPolynomialContext {
|
||||
+initFromCoeffs()
|
||||
+initFromEvals()
|
||||
+getCoeffs()
|
||||
+getEvals()
|
||||
}
|
||||
|
||||
' PolynomialAPI now uses two strategies: Backend and Context
|
||||
class PolynomialAPI {
|
||||
-backendStrategy: IPolynomialBackend
|
||||
-contextStrategy: IPolynomialContext
|
||||
-setBackendStrategy(IPolynomialBackend)
|
||||
-setContextStrategy(IPolynomialContext)
|
||||
+add()
|
||||
+subtract()
|
||||
+multiply()
|
||||
+divide()
|
||||
+evaluate()
|
||||
}
|
||||
|
||||
' Backend Implementations
|
||||
class GPUPolynomialBackend implements IPolynomialBackend {
|
||||
#gpuResources: Resource
|
||||
+add()
|
||||
+subtract()
|
||||
+multiply()
|
||||
+divide()
|
||||
+evaluate()
|
||||
}
|
||||
|
||||
class ZPUPolynomialBackend implements IPolynomialBackend {
|
||||
#zpuResources: Resource
|
||||
+add()
|
||||
+subtract()
|
||||
+multiply()
|
||||
+divide()
|
||||
+evaluate()
|
||||
}
|
||||
|
||||
class TracerPolynomialBackend implements IPolynomialBackend {
|
||||
#traceData: Data
|
||||
+add()
|
||||
+subtract()
|
||||
+multiply()
|
||||
+divide()
|
||||
+evaluate()
|
||||
}
|
||||
|
||||
' Context Implementations (Placeholder for actual implementation)
|
||||
class GPUContext implements IPolynomialContext {
|
||||
+initFromCoeffs()
|
||||
+initFromEvals()
|
||||
+getCoeffs()
|
||||
+getEvals()
|
||||
}
|
||||
|
||||
class ZPUContext implements IPolynomialContext {
|
||||
+initFromCoeffs()
|
||||
+initFromEvals()
|
||||
+getCoeffs()
|
||||
+getEvals()
|
||||
}
|
||||
|
||||
class TracerContext implements IPolynomialContext {
|
||||
+initFromCoeffs()
|
||||
+initFromEvals()
|
||||
+getCoeffs()
|
||||
+getEvals()
|
||||
}
|
||||
|
||||
' Relationships
|
||||
PolynomialAPI o-- IPolynomialBackend : uses
|
||||
PolynomialAPI o-- IPolynomialContext : uses
|
||||
@enduml
|
||||
388
docs/docs/icicle/polynomials/overview.md
Normal file
388
docs/docs/icicle/polynomials/overview.md
Normal file
@@ -0,0 +1,388 @@
|
||||
# Polynomial API Overview
|
||||
|
||||
:::note
|
||||
Read our paper on the Polynomials API in ICICLE v2 by clicking [here](https://eprint.iacr.org/2024/973).
|
||||
:::
|
||||
|
||||
## Introduction
|
||||
|
||||
The Polynomial API offers a robust framework for polynomial operations within a computational environment. It's designed for flexibility and efficiency, supporting a broad range of operations like arithmetic, evaluation, and manipulation, all while abstracting from the computation and storage specifics. This enables adaptability to various backend technologies, employing modern C++ practices.
|
||||
|
||||
## Key Features
|
||||
|
||||
### Backend Agnostic Architecture
|
||||
|
||||
Our API is structured to be independent of any specific computational backend. While a CUDA backend is currently implemented, the architecture facilitates easy integration of additional backends. This capability allows users to perform polynomial operations without the need to tailor their code to specific hardware, enhancing code portability and scalability.
|
||||
|
||||
### Templating in the Polynomial API
|
||||
|
||||
The Polynomial API is designed with a templated structure to accommodate different data types for coefficients, the domain, and images. This flexibility allows the API to be adapted for various computational needs and types of data.
|
||||
|
||||
```cpp
|
||||
template <typename Coeff, typename Domain = Coeff, typename Image = Coeff>
|
||||
class Polynomial {
|
||||
// Polynomial class definition
|
||||
}
|
||||
```
|
||||
|
||||
In this template:
|
||||
|
||||
- **`Coeff`**: Represents the type of the coefficients of the polynomial.
|
||||
- **`Domain`**: Specifies the type for the input values over which the polynomial is evaluated. By default, it is the same as the type of the coefficients but can be specified separately to accommodate different computational contexts.
|
||||
- **`Image`**: Defines the type of the output values of the polynomial. This is typically the same as the coefficients.
|
||||
|
||||
#### Default instantiation
|
||||
|
||||
```cpp
|
||||
extern template class Polynomial<scalar_t>;
|
||||
```
|
||||
|
||||
#### Extended use cases
|
||||
|
||||
The templated nature of the Polynomial API also supports more complex scenarios. For example, coefficients and images could be points on an elliptic curve (EC points), which are useful in cryptographic applications and advanced algebraic structures. This approach allows the API to be extended easily to support new algebraic constructions without modifying the core implementation.
|
||||
|
||||
### Supported Operations
|
||||
|
||||
The Polynomial class encapsulates a polynomial, providing a variety of operations:
|
||||
|
||||
- **Construction**: Create polynomials from coefficients or evaluations on roots-of-unity domains.
|
||||
- **Arithmetic Operations**: Perform addition, subtraction, multiplication, and division.
|
||||
- **Evaluation**: Directly evaluate polynomials at specific points or across a domain.
|
||||
- **Manipulation**: Features like slicing polynomials, adding or subtracting monomials inplace, and computing polynomial degrees.
|
||||
- **Memory Access**: Access internal states or obtain device-memory views of polynomials.
|
||||
|
||||
## Usage
|
||||
|
||||
This section outlines how to use the Polynomial API in C++. Bindings for Rust and Go are detailed under the Bindings sections.
|
||||
|
||||
### Backend Initialization
|
||||
|
||||
Initialization with an appropriate factory is required to configure the computational context and backend.
|
||||
|
||||
```cpp
|
||||
#include "polynomials/polynomials.h"
|
||||
#include "polynomials/cuda_backend/polynomial_cuda_backend.cuh"
|
||||
|
||||
// Initialize with a CUDA backend
|
||||
Polynomial::initialize(std::make_shared<CUDAPolynomialFactory>());
|
||||
```
|
||||
|
||||
:::note
|
||||
Initialization of a factory must be done per linked curve or field.
|
||||
:::
|
||||
|
||||
### Construction
|
||||
|
||||
Polynomials can be constructed from coefficients, from evaluations on roots-of-unity domains, or by cloning existing polynomials.
|
||||
|
||||
```cpp
|
||||
// Construction
|
||||
static Polynomial from_coefficients(const Coeff* coefficients, uint64_t nof_coefficients);
|
||||
static Polynomial from_rou_evaluations(const Image* evaluations, uint64_t nof_evaluations);
|
||||
// Clone the polynomial
|
||||
Polynomial clone() const;
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```cpp
|
||||
auto p_from_coeffs = Polynomial_t::from_coefficients(coeff /* :scalar_t* */, nof_coeffs);
|
||||
auto p_from_rou_evals = Polynomial_t::from_rou_evaluations(rou_evals /* :scalar_t* */, nof_evals);
|
||||
auto p_cloned = p.clone(); // p_cloned and p do not share memory
|
||||
```
|
||||
|
||||
:::note
|
||||
The coefficients or evaluations may be allocated either on host or device memory. In both cases the memory is copied to the backend device.
|
||||
:::
|
||||
|
||||
### Arithmetic
|
||||
|
||||
Constructed polynomials can be used for various arithmetic operations:
|
||||
|
||||
```cpp
|
||||
// Addition
|
||||
Polynomial operator+(const Polynomial& rhs) const;
|
||||
Polynomial& operator+=(const Polynomial& rhs); // inplace addition
|
||||
|
||||
// Subtraction
|
||||
Polynomial operator-(const Polynomial& rhs) const;
|
||||
|
||||
// Multiplication
|
||||
Polynomial operator*(const Polynomial& rhs) const;
|
||||
Polynomial operator*(const Domain& scalar) const; // scalar multiplication
|
||||
|
||||
// Division A(x) = B(x)Q(x) + R(x)
|
||||
std::pair<Polynomial, Polynomial> divide(const Polynomial& rhs) const; // returns (Q(x), R(x))
|
||||
Polynomial operator/(const Polynomial& rhs) const; // returns quotient Q(x)
|
||||
Polynomial operator%(const Polynomial& rhs) const; // returns remainder R(x)
|
||||
Polynomial divide_by_vanishing_polynomial(uint64_t degree) const; // sdivision by the vanishing polynomial V(x)=X^N-1
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
Given polynomials A(x),B(x),C(x) and V(x) the vanishing polynomial.
|
||||
|
||||
$$
|
||||
H(x)=\frac{A(x) \cdot B(x) - C(x)}{V(x)} \space where \space V(x) = X^{N}-1
|
||||
$$
|
||||
|
||||
```cpp
|
||||
auto H = (A*B-C).divide_by_vanishing_polynomial(N);
|
||||
```
|
||||
|
||||
### Evaluation
|
||||
|
||||
Evaluate polynomials at arbitrary domain points, across a domain or on a roots-of-unity domain.
|
||||
|
||||
```cpp
|
||||
Image operator()(const Domain& x) const; // evaluate f(x)
|
||||
void evaluate(const Domain* x, Image* evals /*OUT*/) const;
|
||||
void evaluate_on_domain(Domain* domain, uint64_t size, Image* evals /*OUT*/) const; // caller allocates memory
|
||||
void evaluate_on_rou_domain(uint64_t domain_log_size, Image* evals /*OUT*/) const; // caller allocate memory
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```cpp
|
||||
Coeff x = rand();
|
||||
Image f_x = f(x); // evaluate f at x
|
||||
|
||||
// evaluate f(x) on a domain
|
||||
uint64_t domain_size = ...;
|
||||
auto domain = /*build domain*/; // host or device memory
|
||||
auto evaluations = std::make_unique<scalar_t[]>(domain_size); // can be device memory too
|
||||
f.evaluate_on_domain(domain, domain_size, evaluations);
|
||||
|
||||
// evaluate f(x) on roots of unity domain
|
||||
uint64_t domain_log_size = ...;
|
||||
auto evaluations_rou_domain = std::make_unique<scalar_t[]>(1 << domain_log_size); // can be device memory too
|
||||
f.evaluate_on_rou_domain(domain_log_size, evaluations_rou_domain);
|
||||
```
|
||||
|
||||
### Manipulations
|
||||
|
||||
Beyond arithmetic, the API supports efficient polynomial manipulations:
|
||||
|
||||
#### Monomials
|
||||
|
||||
```cpp
|
||||
// Monomial operations
|
||||
Polynomial& add_monomial_inplace(Coeff monomial_coeff, uint64_t monomial = 0);
|
||||
Polynomial& sub_monomial_inplace(Coeff monomial_coeff, uint64_t monomial = 0);
|
||||
```
|
||||
|
||||
The ability to add or subtract monomials directly and in-place is an efficient way to manipulate polynomials.
|
||||
|
||||
Example:
|
||||
|
||||
```cpp
|
||||
f.add_monomial_in_place(scalar_t::from(5)); // f(x) += 5
|
||||
f.sub_monomial_in_place(scalar_t::from(3), 8); // f(x) -= 3x^8
|
||||
```
|
||||
|
||||
#### Computing the degree of a Polynomial
|
||||
|
||||
```cpp
|
||||
// Degree computation
|
||||
int64_t degree();
|
||||
```
|
||||
|
||||
The degree of a polynomial is a fundamental characteristic that describes the highest power of the variable in the polynomial expression with a non-zero coefficient.
|
||||
The `degree()` function in the API returns the degree of the polynomial, corresponding to the highest exponent with a non-zero coefficient.
|
||||
|
||||
- For the polynomial $f(x) = x^5 + 2x^3 + 4$, the degree is 5 because the highest power of $x$ with a non-zero coefficient is 5.
|
||||
- For a scalar value such as a constant term (e.g., $f(x) = 7$, the degree is considered 0, as it corresponds to $x^0$.
|
||||
- The degree of the zero polynomial, $f(x) = 0$, where there are no non-zero coefficients, is defined as -1. This special case often represents an "empty" or undefined state in many mathematical contexts.
|
||||
|
||||
Example:
|
||||
|
||||
```cpp
|
||||
auto f = /*some expression*/;
|
||||
auto degree_of_f = f.degree();
|
||||
```
|
||||
|
||||
#### Slicing
|
||||
|
||||
```cpp
|
||||
// Slicing and selecting even or odd components.
|
||||
Polynomial slice(uint64_t offset, uint64_t stride, uint64_t size = 0 /*0 means take all elements*/);
|
||||
Polynomial even();
|
||||
Polynomial odd();
|
||||
```
|
||||
|
||||
The Polynomial API provides methods for slicing polynomials and selecting specific components, such as even or odd indexed terms. Slicing allows extracting specific sections of a polynomial based on an offset, stride, and size.
|
||||
|
||||
The following examples demonstrate folding a polynomial's even and odd parts and arbitrary slicing;
|
||||
|
||||
```cpp
|
||||
// folding a polynomials even and odd parts with randomness
|
||||
auto x = rand();
|
||||
auto even = f.even();
|
||||
auto odd = f.odd();
|
||||
auto fold_poly = even + odd * x;
|
||||
|
||||
// arbitrary slicing (first quarter)
|
||||
auto first_quarter = f.slice(0 /*offset*/, 1 /*stride*/, f.degree()/4 /*size*/);
|
||||
```
|
||||
|
||||
### Memory access (copy/view)
|
||||
|
||||
Access to the polynomial's internal state can be vital for operations like commitment schemes or when more efficient custom operations are necessary. This can be done either by copying or viewing the polynomial
|
||||
|
||||
#### Copying
|
||||
|
||||
Copies the polynomial coefficients to either host or device allocated memory.
|
||||
|
||||
:::note
|
||||
Copying to host memory is backend agnostic while copying to device memory requires the memory to be allocated on the corresponding backend.
|
||||
:::
|
||||
|
||||
```cpp
|
||||
Coeff get_coeff(uint64_t idx) const; // copy single coefficient to host
|
||||
uint64_t copy_coeffs(Coeff* coeffs, uint64_t start_idx, uint64_t end_idx) const;
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```cpp
|
||||
auto coeffs_device = /*allocate CUDA or host memory*/
|
||||
f.copy_coeffs(coeffs_device, 0/*start*/, f.degree());
|
||||
|
||||
MSMConfig cfg = msm::defaultMSMConfig();
|
||||
cfg.are_points_on_device = true; // assuming copy to device memory
|
||||
auto rv = msm::MSM(coeffs_device, points, msm_size, cfg, results);
|
||||
```
|
||||
|
||||
#### Views
|
||||
|
||||
The Polynomial API supports efficient data handling through the use of memory views. These views provide direct access to the polynomial's internal state without the need to copy data. This feature is particularly useful for operations that require direct access to device memory, enhancing both performance and memory efficiency.
|
||||
|
||||
##### What is a Memory View?
|
||||
|
||||
A memory view is essentially a pointer to data stored in device memory. By providing a direct access pathway to the data, it eliminates the need for data duplication, thus conserving both time and system resources. This is especially beneficial in high-performance computing environments where data size and operation speed are critical factors.
|
||||
|
||||
##### Applications of Memory Views
|
||||
|
||||
Memory views are extremely versatile and can be employed in various computational contexts such as:
|
||||
|
||||
- **Commitments**: Views can be used to commit polynomial states in cryptographic schemes, such as Multi-Scalar Multiplications (MSM).
|
||||
- **External Computations**: They allow external functions or algorithms to utilize the polynomial's data directly, facilitating operations outside the core polynomial API. This is useful for custom operations that are not covered by the API.
|
||||
|
||||
##### Obtaining and Using Views
|
||||
|
||||
To create and use views within the Polynomial API, functions are provided to obtain pointers to both coefficients and evaluation data. Here’s how they are generally structured:
|
||||
|
||||
```cpp
|
||||
// Obtain a view of the polynomial's coefficients
|
||||
std::tuple<IntegrityPointer<Coeff>, uint64_t /*size*/, uint64_t /*device_id*/> get_coefficients_view();
|
||||
```
|
||||
|
||||
Example usage:
|
||||
|
||||
```cpp
|
||||
auto [coeffs_view, size, device_id] = polynomial.get_coefficients_view();
|
||||
|
||||
// Use coeffs_view in a computational routine that requires direct access to polynomial coefficients
|
||||
// Example: Passing the view to a GPU-accelerated function
|
||||
gpu_accelerated_function(coeffs_view.get(),...);
|
||||
```
|
||||
|
||||
##### Integrity-Pointer: Managing Memory Views
|
||||
|
||||
Within the Polynomial API, memory views are managed through a specialized tool called the Integrity-Pointer. This pointer type is designed to safeguard operations by monitoring the validity of the memory it points to. It can detect if the memory has been modified or released, thereby preventing unsafe access to stale or non-existent data.
|
||||
The Integrity-Pointer not only acts as a regular pointer but also provides additional functionality to ensure the integrity of the data it references. Here are its key features:
|
||||
|
||||
```cpp
|
||||
// Checks whether the pointer is still considered valid
|
||||
bool isValid() const;
|
||||
|
||||
// Retrieves the raw pointer or nullptr if pointer is invalid
|
||||
const T* get() const;
|
||||
|
||||
// Dereferences the pointer. Throws exception if the pointer is invalid.
|
||||
const T& operator*() const;
|
||||
|
||||
//Provides access to the member of the pointed-to object. Throws exception if the pointer is invalid.
|
||||
const T* operator->() const;
|
||||
```
|
||||
|
||||
Consider the Following case:
|
||||
|
||||
```cpp
|
||||
auto [coeff_view, size, device] = f.get_coefficients_view();
|
||||
|
||||
// Use the coefficients view to perform external operations
|
||||
commit_to_polynomial(coeff_view.get(), size);
|
||||
|
||||
// Modification of the original polynomial
|
||||
f += g; // Any operation that modifies 'f' potentially invalidates 'coeff_view'
|
||||
|
||||
// Check if the view is still valid before using it further
|
||||
if (coeff_view.isValid()) {
|
||||
perform_additional_computation(coeff_view.get(), size);
|
||||
} else {
|
||||
handle_invalid_data();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Multi-GPU Support with CUDA Backend
|
||||
|
||||
The Polynomial API includes comprehensive support for multi-GPU environments, a crucial feature for leveraging the full computational power of systems equipped with multiple NVIDIA GPUs. This capability is part of the API's CUDA backend, which is designed to efficiently manage polynomial computations across different GPUs.
|
||||
|
||||
### Setting the CUDA Device
|
||||
|
||||
Like other components of the icicle framework, the Polynomial API allows explicit setting of the current CUDA device:
|
||||
|
||||
```cpp
|
||||
cudaSetDevice(int deviceID);
|
||||
```
|
||||
|
||||
This function sets the active CUDA device. All subsequent operations that allocate or deal with polynomial data will be performed on this device.
|
||||
|
||||
### Allocation Consistency
|
||||
|
||||
Polynomials are always allocated on the current CUDA device at the time of their creation. It is crucial to ensure that the device context is correctly set before initiating any operation that involves memory allocation:
|
||||
|
||||
```cpp
|
||||
// Set the device before creating polynomials
|
||||
cudaSetDevice(0);
|
||||
Polynomial p1 = Polynomial::from_coefficients(coeffs, size);
|
||||
|
||||
cudaSetDevice(1);
|
||||
Polynomial p2 = Polynomial::from_coefficients(coeffs, size);
|
||||
```
|
||||
|
||||
### Matching Devices for Operations
|
||||
|
||||
When performing operations that result in the creation of new polynomials (such as addition or multiplication), it is imperative that both operands are on the same CUDA device. If the operands reside on different devices, an exception is thrown:
|
||||
|
||||
```cpp
|
||||
// Ensure both operands are on the same device
|
||||
cudaSetDevice(0);
|
||||
auto p3 = p1 + p2; // Throws an exception if p1 and p2 are not on the same device
|
||||
```
|
||||
|
||||
### Device-Agnostic Operations
|
||||
|
||||
Operations that do not involve the creation of new polynomials, such as computing the degree of a polynomial or performing in-place modifications, can be executed regardless of the current device setting:
|
||||
|
||||
```cpp
|
||||
// 'degree' and in-place operations do not require device matching
|
||||
int deg = p1.degree();
|
||||
p1 += p2; // Valid if p1 and p2 are on the same device, throws otherwise
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
The API is designed to throw exceptions if operations are attempted across polynomials that are not located on the same GPU. This ensures that all polynomial operations are performed consistently and without data integrity issues due to device mismatches.
|
||||
|
||||
### Best Practices
|
||||
|
||||
To maximize the performance and avoid runtime errors in a multi-GPU setup, always ensure that:
|
||||
|
||||
- The CUDA device is set correctly before polynomial allocation.
|
||||
- Operations involving new polynomial creation are performed with operands on the same device.
|
||||
|
||||
By adhering to these guidelines, developers can effectively harness the power of multiple GPUs to handle large-scale polynomial computations efficiently.
|
||||
79
docs/docs/icicle/primitives/keccak.md
Normal file
79
docs/docs/icicle/primitives/keccak.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Keccak
|
||||
|
||||
[Keccak](https://keccak.team/files/Keccak-implementation-3.2.pdf) is a cryptographic hash function designed by Guido Bertoni, Joan Daemen, Michaël Peeters, and Gilles Van Assche. It was selected as the winner of the NIST hash function competition, becoming the basis for the [SHA-3 standard](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf).
|
||||
|
||||
Keccak operates on a message input of any length and produces a fixed-size hash output. The hash function is built upon the sponge construction, which involves absorbing the input data followed by squeezing out the hash value.
|
||||
|
||||
At its core, Keccak consists of a permutation function operating on a state array. The permutation function employs a round function that operates iteratively on the state array. Each round consists of five main steps:
|
||||
|
||||
- **Theta:** This step introduces diffusion by performing a bitwise XOR operation between the state and a linear combination of its neighboring columns.
|
||||
- **Rho:** This step performs bit rotation operations on each lane of the state array.
|
||||
- **Pi:** This step rearranges the positions of the lanes in the state array.
|
||||
- **Chi:** This step applies a nonlinear mixing operation to each lane of the state array.
|
||||
- **Iota:** This step introduces a round constant to the state array.
|
||||
|
||||
## Keccak vs Sha3
|
||||
|
||||
There exists a [confusion](https://www.cybertest.com/blog/keccak-vs-sha3) between what is called `Keccak` and `Sha3`. In ICICLE we support both. `Keccak256` relates to the old hash function used in Ethereum, and `Sha3-256` relates to the modern hash function.
|
||||
|
||||
## Using Keccak
|
||||
|
||||
ICICLE Keccak supports batch hashing, which can be utilized for constructing a merkle tree or running multiple hashes in parallel.
|
||||
|
||||
### Supported Bindings
|
||||
|
||||
- [Golang](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/golang/hash/keccak)
|
||||
- [Rust](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/rust/icicle-hash)
|
||||
|
||||
### Example usage
|
||||
|
||||
This is an example of running 1024 Keccak-256 hashes in parallel, where input strings are of size 136 bytes:
|
||||
|
||||
```rust
|
||||
use icicle_core::hash::HashConfig;
|
||||
use icicle_cuda_runtime::memory::HostSlice;
|
||||
use icicle_hash::keccak::keccak256;
|
||||
|
||||
let config = HashConfig::default();
|
||||
let input_block_len = 136;
|
||||
let number_of_hashes = 1024;
|
||||
|
||||
let preimages = vec![1u8; number_of_hashes * input_block_len];
|
||||
let mut digests = vec![0u8; number_of_hashes * 32];
|
||||
|
||||
let preimages_slice = HostSlice::from_slice(&preimages);
|
||||
let digests_slice = HostSlice::from_mut_slice(&mut digests);
|
||||
|
||||
keccak256(
|
||||
preimages_slice,
|
||||
input_block_len as u32,
|
||||
number_of_hashes as u32,
|
||||
digests_slice,
|
||||
&config,
|
||||
)
|
||||
.unwrap();
|
||||
```
|
||||
|
||||
### Merkle Tree
|
||||
|
||||
You can build a keccak merkle tree using the corresponding functions:
|
||||
|
||||
```rust
|
||||
use icicle_core::tree::{merkle_tree_digests_len, TreeBuilderConfig};
|
||||
use icicle_cuda_runtime::memory::HostSlice;
|
||||
use icicle_hash::keccak::build_keccak256_merkle_tree;
|
||||
|
||||
let mut config = TreeBuilderConfig::default();
|
||||
config.arity = 2;
|
||||
let height = 22;
|
||||
let input_block_len = 136;
|
||||
let leaves = vec![1u8; (1 << height) * input_block_len];
|
||||
let mut digests = vec![0u64; merkle_tree_digests_len((height + 1) as u32, 2, 1)];
|
||||
|
||||
let leaves_slice = HostSlice::from_slice(&leaves);
|
||||
let digests_slice = HostSlice::from_mut_slice(&mut digests);
|
||||
|
||||
build_keccak256_merkle_tree(leaves_slice, digests_slice, height, input_block_len, &config).unwrap();
|
||||
```
|
||||
|
||||
In the example above, a binary tree of height 22 is being built. Each leaf is considered to be a 136 byte long array. The leaves and digests are aligned in a flat array. You can also use keccak512 in `build_keccak512_merkle_tree` function.
|
||||
@@ -1,6 +1,6 @@
|
||||
# MSM - Multi scalar multiplication
|
||||
|
||||
MSM stands for Multi scalar multiplication, its defined as:
|
||||
MSM stands for Multi scalar multiplication, it's defined as:
|
||||
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mi>M</mi>
|
||||
@@ -43,54 +43,153 @@ $a_0, \ldots, a_n$ - Scalars
|
||||
|
||||
$MSM(a, G) \in G$ - a single EC (elliptic curve) point
|
||||
|
||||
In words, MSM is the sum of scalar and EC point multiplications. We can see from this definition that the core operations occurring are Modular Multiplication and Elliptic curve point addition. Its obvious that multiplication can be computed in parallel and then the products summed, making MSM inherently parallelizable.
|
||||
In words, MSM is the sum of scalar and EC point multiplications. We can see from this definition that the core operations occurring are Modular Multiplication and Elliptic curve point addition. It's obvious that multiplication can be computed in parallel and then the products summed, making MSM inherently parallelizable.
|
||||
|
||||
Accelerating MSM is crucial to a ZK protocol's performance due to the [large percent of run time](https://hackmd.io/@0xMonia/SkQ6-oRz3#Hardware-acceleration-in-action) they take when generating proofs.
|
||||
|
||||
You can learn more about how MSMs work from this [video](https://www.youtube.com/watch?v=Bl5mQA7UL2I) and from our resource list on [Ingopedia](https://www.ingonyama.com/ingopedia/msm).
|
||||
|
||||
## Supported curves
|
||||
|
||||
MSM supports the following curves:
|
||||
|
||||
`bls12-377`, `bls12-381`, `bn254`, `bw6-761`, `grumpkin`
|
||||
|
||||
|
||||
## Supported Bindings
|
||||
|
||||
- [Golang](../golang-bindings/msm.md)
|
||||
- [Rust](../rust-bindings//msm.md)
|
||||
|
||||
## Supported algorithms
|
||||
## Algorithm description
|
||||
|
||||
Our MSM implementation supports two algorithms `Bucket accumulation` and `Large triangle accumulation`.
|
||||
We follow the bucket method algorithm. The GPU implementation consists of four phases:
|
||||
|
||||
### Bucket accumulation
|
||||
1. Preparation phase - The scalars are split into smaller scalars of `c` bits each. These are the bucket indices. The points are grouped according to their corresponding bucket index and the buckets are sorted by size.
|
||||
2. Accumulation phase - Each bucket accumulates all of its points using a single thread. More than one thread is assigned to large buckets, in proportion to their size. A bucket is considered large if its size is above the large bucket threshold that is determined by the `large_bucket_factor` parameter. The large bucket threshold is the expected average bucket size times the `large_bucket_factor` parameter.
|
||||
3. Buckets Reduction phase - bucket results are multiplied by their corresponding bucket number and each bucket module is reduced to a small number of final results. By default, this is done by an iterative algorithm which is highly parallel. Setting `is_big_triangle` to `true` will switch this phase to the running sum algorithm described in the above YouTube talk which is much less parallel.
|
||||
4. Final accumulation phase - The final results from the last phase are accumulated using the double-and-add algorithm.
|
||||
|
||||
The Bucket Accumulation algorithm is a method of dividing the overall MSM task into smaller, more manageable sub-tasks. It involves partitioning scalars and their corresponding points into different "buckets" based on the scalar values.
|
||||
## Batched MSM
|
||||
|
||||
Bucket Accumulation can be more parallel-friendly because it involves dividing the computation into smaller, independent tasks, distributing scalar-point pairs into buckets and summing points within each bucket. This division makes it well suited for parallel processing on GPUs.
|
||||
The MSM supports batch mode - running multiple MSMs in parallel. It's always better to use the batch mode instead of running single msms in serial as long as there is enough memory available. We support running a batch of MSMs that share the same points as well as a batch of MSMs that use different points.
|
||||
|
||||
#### When should I use Bucket accumulation?
|
||||
## MSM configuration
|
||||
|
||||
In scenarios involving large MSM computations with many scalar-point pairs, the ability to parallelize operations makes Bucket Accumulation more efficient. The larger the MSM task, the more significant the potential gains from parallelization.
|
||||
```cpp
|
||||
/**
|
||||
* @struct MSMConfig
|
||||
* Struct that encodes MSM parameters to be passed into the [MSM](@ref MSM) function. The intended use of this struct
|
||||
* is to create it using [default_msm_config](@ref default_msm_config) function and then you'll hopefully only need to
|
||||
* change a small number of default values for each of your MSMs.
|
||||
*/
|
||||
struct MSMConfig {
|
||||
device_context::DeviceContext ctx; /**< Details related to the device such as its id and stream id. */
|
||||
int points_size; /**< Number of points in the MSM. If a batch of MSMs needs to be computed, this should be
|
||||
* a number of different points. So, if each MSM re-uses the same set of points, this
|
||||
* variable is set equal to the MSM size. And if every MSM uses a distinct set of
|
||||
* points, it should be set to the product of MSM size and [batch_size](@ref
|
||||
* batch_size). Default value: 0 (meaning it's equal to the MSM size). */
|
||||
int precompute_factor; /**< The number of extra points to pre-compute for each point. See the
|
||||
* [precompute_msm_points](@ref precompute_msm_points) function, `precompute_factor` passed
|
||||
* there needs to be equal to the one used here. Larger values decrease the
|
||||
* number of computations to make, on-line memory footprint, but increase the static
|
||||
* memory footprint. Default value: 1 (i.e. don't pre-compute). */
|
||||
int c; /**< \f$ c \f$ value, or "window bitsize" which is the main parameter of the "bucket
|
||||
* method" that we use to solve the MSM problem. As a rule of thumb, larger value
|
||||
* means more on-line memory footprint but also more parallelism and less computational
|
||||
* complexity (up to a certain point). Currently pre-computation is independent of
|
||||
* \f$ c \f$, however in the future value of \f$ c \f$ here and the one passed into the
|
||||
* [precompute_msm_points](@ref precompute_msm_points) function will need to be identical.
|
||||
* Default value: 0 (the optimal value of \f$ c \f$ is chosen automatically). */
|
||||
int bitsize; /**< Number of bits of the largest scalar. Typically equals the bitsize of scalar field,
|
||||
* but if a different (better) upper bound is known, it should be reflected in this
|
||||
* variable. Default value: 0 (set to the bitsize of scalar field). */
|
||||
int large_bucket_factor; /**< Variable that controls how sensitive the algorithm is to the buckets that occur
|
||||
* very frequently. Useful for efficient treatment of non-uniform distributions of
|
||||
* scalars and "top windows" with few bits. Can be set to 0 to disable separate
|
||||
* treatment of large buckets altogether. Default value: 10. */
|
||||
int batch_size; /**< The number of MSMs to compute. Default value: 1. */
|
||||
bool are_scalars_on_device; /**< True if scalars are on device and false if they're on host. Default value:
|
||||
* false. */
|
||||
bool are_scalars_montgomery_form; /**< True if scalars are in Montgomery form and false otherwise. Default value:
|
||||
* true. */
|
||||
bool are_points_on_device; /**< True if points are on device and false if they're on host. Default value: false. */
|
||||
bool are_points_montgomery_form; /**< True if coordinates of points are in Montgomery form and false otherwise.
|
||||
* Default value: true. */
|
||||
bool are_results_on_device; /**< True if the results should be on device and false if they should be on host. If set
|
||||
* to false, `is_async` won't take effect because a synchronization is needed to
|
||||
* transfer results to the host. Default value: false. */
|
||||
bool is_big_triangle; /**< Whether to do "bucket accumulation" serially. Decreases computational complexity
|
||||
* but also greatly decreases parallelism, so only suitable for large batches of MSMs.
|
||||
* Default value: false. */
|
||||
bool is_async; /**< Whether to run the MSM asynchronously. If set to true, the MSM function will be
|
||||
* non-blocking and you'd need to synchronize it explicitly by running
|
||||
* `cudaStreamSynchronize` or `cudaDeviceSynchronize`. If set to false, the MSM
|
||||
* function will block the current CPU thread. */
|
||||
};
|
||||
```
|
||||
|
||||
### Large triangle accumulation
|
||||
## Choosing optimal parameters
|
||||
|
||||
Large Triangle Accumulation is a method for optimizing MSM which focuses on reducing the number of point doublings in the computation. This algorithm is based on the observation that the number of point doublings can be minimized by structuring the computation in a specific manner.
|
||||
`is_big_triangle` should be `false` in almost all cases. It might provide better results only for very small MSMs (smaller than 2^8^) with a large batch (larger than 100) but this should be tested per scenario.
|
||||
Large buckets exist in two cases:
|
||||
1. When the scalar distribution isn't uniform.
|
||||
2. When `c` does not divide the scalar bit-size.
|
||||
|
||||
#### When should I use Large triangle accumulation?
|
||||
`large_bucket_factor` that is equal to 10 yields good results for most cases, but it's best to fine tune this parameter per `c` and per scalar distribution.
|
||||
The two most important parameters for performance are `c` and the `precompute_factor`. They affect the number of EC additions as well as the memory size. When the points are not known in advance we cannot use precomputation. In this case the best `c` value is usually around $log_2(msmSize) - 4$. However, in most protocols the points are known in advance and precomputation can be used unless limited by memory. Usually it's best to use maximum precomputation (such that we end up with only a single bucket module) combined with a `c` value around $log_2(msmSize) - 1$.
|
||||
|
||||
The Large Triangle Accumulation algorithm is more sequential in nature, as it builds upon each step sequentially (accumulating sums and then performing doubling). This structure can make it less suitable for parallelization but potentially more efficient for a <b>large batch of smaller MSM computations</b>.
|
||||
## Memory usage estimation
|
||||
|
||||
## MSM Modes
|
||||
The main memory requirements of the MSM are the following:
|
||||
|
||||
ICICLE MSM also supports two different modes `Batch MSM` and `Single MSM`
|
||||
- Scalars - `sizeof(scalar_t) * msm_size * batch_size`
|
||||
- Scalar indices - `~6 * sizeof(unsigned) * nof_bucket_modules * msm_size * batch_size`
|
||||
- Points - `sizeof(affine_t) * msm_size * precomp_factor * batch_size`
|
||||
- Buckets - `sizeof(projective_t) * nof_bucket_modules * 2^c * batch_size`
|
||||
|
||||
Batch MSM allows you to run many MSMs with a single API call, Single MSM will launch a single MSM computation.
|
||||
where `nof_bucket_modules = ceil(ceil(bitsize / c) / precompute_factor)`
|
||||
|
||||
### Which mode should I use?
|
||||
During the MSM computation first the memory for scalars and scalar indices is allocated, then the indices are freed and points and buckets are allocated. This is why a good estimation for the required memory is the following formula:
|
||||
|
||||
This decision is highly dependent on your use case and design. However, if your design allows for it, using batch mode can significantly improve efficiency. Batch processing allows you to perform multiple MSMs leveraging the parallel processing capabilities of GPUs.
|
||||
$max(scalars + scalarIndices, scalars + points + buckets)$
|
||||
|
||||
Single MSM mode should be used when batching isn't possible or when you have to run a single MSM.
|
||||
This gives a good approximation within 10% of the actual required memory for most cases.
|
||||
|
||||
## Example parameters
|
||||
|
||||
Here is a useful table showing optimal parameters for different MSMs. They are optimal for BLS12-377 curve when running on NVIDIA GeForce RTX 3090 Ti. This is the configuration used:
|
||||
|
||||
```cpp
|
||||
msm::MSMConfig config = {
|
||||
ctx, // DeviceContext
|
||||
N, // points_size
|
||||
precomp_factor, // precompute_factor
|
||||
user_c, // c
|
||||
0, // bitsize
|
||||
10, // large_bucket_factor
|
||||
batch_size, // batch_size
|
||||
false, // are_scalars_on_device
|
||||
false, // are_scalars_montgomery_form
|
||||
true, // are_points_on_device
|
||||
false, // are_points_montgomery_form
|
||||
true, // are_results_on_device
|
||||
false, // is_big_triangle
|
||||
true // is_async
|
||||
};
|
||||
```
|
||||
|
||||
Here are the parameters and the results for the different cases:
|
||||
|
||||
| MSM size | Batch size | Precompute factor | c | Memory estimation (GB) | Actual memory (GB) | Single MSM time (ms) |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 10 | 1 | 1 | 9 | 0.00227 | 0.00277 | 9.2 |
|
||||
| 10 | 1 | 23 | 11 | 0.00259 | 0.00272 | 1.76 |
|
||||
| 10 | 1000 | 1 | 7 | 0.94 | 1.09 | 0.051 |
|
||||
| 10 | 1000 | 23 | 11 | 2.59 | 2.74 | 0.025 |
|
||||
| 15 | 1 | 1 | 11 | 0.011 | 0.019 | 9.9 |
|
||||
| 15 | 1 | 16 | 16 | 0.061 | 0.065 | 2.4 |
|
||||
| 15 | 100 | 1 | 11 | 1.91 | 1.92 | 0.84 |
|
||||
| 15 | 100 | 19 | 14 | 6.32 | 6.61 | 0.56 |
|
||||
| 18 | 1 | 1 | 14 | 0.128 | 0.128 | 14.4 |
|
||||
| 18 | 1 | 15 | 17 | 0.40 | 0.42 | 5.9 |
|
||||
| 22 | 1 | 1 | 17 | 1.64 | 1.65 | 68 |
|
||||
| 22 | 1 | 13 | 21 | 5.67 | 5.94 | 54 |
|
||||
| 24 | 1 | 1 | 18 | 6.58 | 6.61 | 232 |
|
||||
| 24 | 1 | 7 | 21 | 12.4 | 13.4 | 199 |
|
||||
|
||||
The optimal values can vary per GPU and per curve. It is best to try a few combinations until you get the best results for your specific case.
|
||||
|
||||
@@ -11,24 +11,19 @@ A_k = \sum_{n=0}^{N-1} a_n \cdot \omega^{nk} \mod p
|
||||
$$
|
||||
|
||||
where:
|
||||
|
||||
- $N$ is the size of the input sequence and is a power of 2,
|
||||
- $p$ is a prime number such that $p = kN + 1$ for some integer $k$, ensuring that $p$ supports the existence of $N$th roots of unity,
|
||||
- $\omega$ is a primitive $N$th root of unity modulo $p$, meaning $\omega^N \equiv 1 \mod p$ and no smaller positive power of $\omega$ is congruent to 1 modulo $p$,
|
||||
- $k$ ranges from 0 to $N-1$, and it indexes the output sequence.
|
||||
|
||||
The NTT is particularly useful because it enables efficient polynomial multiplication under modulo arithmetic, crucial for algorithms in cryptographic protocols, and other areas requiring fast modular arithmetic operations.
|
||||
NTT is particularly useful because it enables efficient polynomial multiplication under modulo arithmetic, crucial for algorithms in cryptographic protocols and other areas requiring fast modular arithmetic operations.
|
||||
|
||||
There exists also INTT which is the inverse operation of NTT. INTT can take as input an output sequence of integers from an NTT and reconstruct the original sequence.
|
||||
|
||||
# Using NTT
|
||||
## Using NTT
|
||||
|
||||
### Supported curves
|
||||
|
||||
NTT supports the following curves:
|
||||
|
||||
`bls12-377`, `bls12-381`, `bn-254`, `bw6-761`
|
||||
|
||||
## Supported Bindings
|
||||
### Supported Bindings
|
||||
|
||||
- [Golang](../golang-bindings/ntt.md)
|
||||
- [Rust](../rust-bindings/ntt.md)
|
||||
@@ -61,19 +56,17 @@ Choosing an algorithm is heavily dependent on your use case. For example Cooley-
|
||||
|
||||
NTT also supports two different modes `Batch NTT` and `Single NTT`
|
||||
|
||||
Batch NTT allows you to run many NTTs with a single API call, Single MSM will launch a single MSM computation.
|
||||
Deciding whether to use `batch NTT` vs `single NTT` is highly dependent on your application and use case.
|
||||
|
||||
Deciding weather to use `batch NTT` vs `single NTT` is highly dependent on your application and use case.
|
||||
#### Single NTT
|
||||
|
||||
**Single NTT Mode**
|
||||
Single NTT will launch a single NTT computation.
|
||||
|
||||
- Choose this mode when your application requires processing individual NTT operations in isolation.
|
||||
Choose this mode when your application requires processing individual NTT operations in isolation.
|
||||
|
||||
**Batch NTT Mode**
|
||||
#### Batch NTT Mode
|
||||
|
||||
- Batch NTT mode can significantly reduce read/write as well as computation overhead by executing multiple NTT operations in parallel.
|
||||
|
||||
- Batch mode may also offer better utilization of computational resources (memory and compute).
|
||||
Batch NTT allows you to run many NTTs with a single API call. Batch NTT mode can significantly reduce read/write times as well as computation overhead by executing multiple NTT operations in parallel. Batch mode may also offer better utilization of computational resources (memory and compute).
|
||||
|
||||
## Supported algorithms
|
||||
|
||||
@@ -90,8 +83,8 @@ At its core, the Radix-2 NTT algorithm divides the problem into smaller sub-prob
|
||||
The algorithm recursively divides the input sequence into smaller sequences. At each step, it separates the sequence into even-indexed and odd-indexed elements, forming two subsequences that are then processed independently.
|
||||
|
||||
3. **Butterfly Operations:**
|
||||
The core computational element of the Radix-2 NTT is the "butterfly" operation, which combines pairs of elements from the sequences obtained in the decomposition step.
|
||||
|
||||
The core computational element of the Radix-2 NTT is the "butterfly" operation, which combines pairs of elements from the sequences obtained in the decomposition step.
|
||||
|
||||
Each butterfly operation involves multiplication by a "twiddle factor," which is a root of unity in the finite field, and addition or subtraction of the results, all performed modulo the prime modulus.
|
||||
|
||||
$$
|
||||
@@ -108,7 +101,6 @@ At its core, the Radix-2 NTT algorithm divides the problem into smaller sub-prob
|
||||
|
||||
$k$ - The index of the current operation within the butterfly or the transform stage
|
||||
|
||||
|
||||
The twiddle factors are precomputed to save runtime and improve performance.
|
||||
|
||||
4. **Bit-Reversal Permutation:**
|
||||
@@ -116,7 +108,7 @@ At its core, the Radix-2 NTT algorithm divides the problem into smaller sub-prob
|
||||
|
||||
### Mixed Radix
|
||||
|
||||
The Mixed Radix NTT algorithm extends the concepts of the Radix-2 algorithm by allowing the decomposition of the input sequence based on various factors of its length. Specifically ICICLEs implementation splits the input into blocks of sizes 16,32,64 compared to radix2 which is always splitting such that we end with NTT of size 2. This approach offers enhanced flexibility and efficiency, especially for input sizes that are composite numbers, by leveraging the "divide and conquer" strategy across multiple radixes.
|
||||
The Mixed Radix NTT algorithm extends the concepts of the Radix-2 algorithm by allowing the decomposition of the input sequence based on various factors of its length. Specifically ICICLEs implementation splits the input into blocks of sizes 16, 32, or 64 compared to radix2 which is always splitting such that we end with NTT of size 2. This approach offers enhanced flexibility and efficiency, especially for input sizes that are composite numbers, by leveraging the "divide and conquer" strategy across multiple radices.
|
||||
|
||||
The NTT blocks in Mixed Radix are implemented more efficiently based on winograd NTT but also optimized memory and register usage is better compared to Radix-2.
|
||||
|
||||
@@ -126,11 +118,11 @@ Mixed Radix can reduce the number of stages required to compute for large inputs
|
||||
The input to the Mixed Radix NTT is a sequence of integers $a_0, a_1, \ldots, a_{N-1}$, where $N$ is not strictly required to be a power of two. Instead, $N$ can be any composite number, ideally factorized into primes or powers of primes.
|
||||
|
||||
2. **Factorization and Decomposition:**
|
||||
Unlike the Radix-2 algorithm, which strictly divides the computational problem into halves, the Mixed Radix NTT algorithm implements a flexible decomposition approach which isn't limited to prime factorization.
|
||||
|
||||
Unlike the Radix-2 algorithm, which strictly divides the computational problem into halves, the Mixed Radix NTT algorithm implements a flexible decomposition approach which isn't limited to prime factorization.
|
||||
|
||||
For example, an NTT of size 256 can be decomposed into two stages of $16 \times \text{NTT}_{16}$, leveraging a composite factorization strategy rather than decomposing into eight stages of $\text{NTT}_{2}$. This exemplifies the use of composite factors (in this case, $256 = 16 \times 16$) to apply smaller NTT transforms, optimizing computational efficiency by adapting the decomposition strategy to the specific structure of $N$.
|
||||
|
||||
3. **Butterfly Operations with Multiple Radixes:**
|
||||
3. **Butterfly Operations with Multiple Radices:**
|
||||
The Mixed Radix algorithm utilizes butterfly operations for various radix sizes. Each sub-transform involves specific butterfly operations characterized by multiplication with twiddle factors appropriate for the radix in question.
|
||||
|
||||
The generalized butterfly operation for a radix-$r$ element can be expressed as:
|
||||
@@ -139,7 +131,15 @@ Mixed Radix can reduce the number of stages required to compute for large inputs
|
||||
X_{k,r} = \sum_{j=0}^{r-1} (A_{j,k} \cdot W^{jk}) \mod p
|
||||
$$
|
||||
|
||||
where $X_{k,r}$ is the output of the $radix-r$ butterfly operation for the $k-th$ set of inputs, $A_{j,k}$ represents the $j-th$ input element for the $k-th$ operation, $W$ is the twiddle factor, and $p$ is the prime modulus.
|
||||
where:
|
||||
|
||||
$X_{k,r}$ - is the output of the $radix-r$ butterfly operation for the $k-th$ set of inputs
|
||||
|
||||
$A_{j,k}$ - represents the $j-th$ input element for the $k-th$ operation
|
||||
|
||||
$W$ - is the twiddle factor
|
||||
|
||||
$p$ - is the prime modulus
|
||||
|
||||
4. **Recombination and Reordering:**
|
||||
After applying the appropriate butterfly operations across all decomposition levels, the Mixed Radix algorithm recombines the results into a single output sequence. Due to the varied sizes of the sub-transforms, a more complex reordering process may be required compared to Radix-2. This involves digit-reversal permutations to ensure that the final output sequence is correctly ordered.
|
||||
@@ -154,6 +154,6 @@ Mixed radix on the other hand works better for larger NTTs with larger input siz
|
||||
|
||||
Performance really depends on logn size, batch size, ordering, inverse, coset, coeff-field and which GPU you are using.
|
||||
|
||||
For this reason we implemented our [heuristic auto-selection](https://github.com/ingonyama-zk/icicle/blob/774250926c00ffe84548bc7dd97aea5227afed7e/icicle/appUtils/ntt/ntt.cu#L474) which should choose the most efficient algorithm in most cases.
|
||||
For this reason we implemented our [heuristic auto-selection](https://github.com/ingonyama-zk/icicle/blob/main/icicle/src/ntt/ntt.cu#L573) which should choose the most efficient algorithm in most cases.
|
||||
|
||||
We still recommend you benchmark for your specific use case if you think a different configuration would yield better results.
|
||||
|
||||
@@ -8,4 +8,5 @@ This section of the documentation is dedicated to the ICICLE primitives, we will
|
||||
|
||||
- [MSM](./msm.md)
|
||||
- [NTT](./ntt.md)
|
||||
- [Keccak Hash](./keccak.md)
|
||||
- [Poseidon Hash](./poseidon.md)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Poseidon
|
||||
|
||||
[Poseidon](https://eprint.iacr.org/2019/458.pdf) is a popular hash in the ZK ecosystem primarily because its optimized to work over large prime fields, a common setting for ZK proofs, thereby minimizing the number of multiplicative operations required.
|
||||
[Poseidon](https://eprint.iacr.org/2019/458.pdf) is a popular hash in the ZK ecosystem primarily because it's optimized to work over large prime fields, a common setting for ZK proofs, thereby minimizing the number of multiplicative operations required.
|
||||
|
||||
Poseidon has also been specifically designed to be efficient when implemented within ZK circuits, Poseidon uses far less constraints compared to other hash functions like Keccak or SHA-256 in the context of ZK circuits.
|
||||
|
||||
@@ -8,42 +8,41 @@ Poseidon has been used in many popular ZK protocols such as Filecoin and [Plonk]
|
||||
|
||||
Our implementation of Poseidon is implemented in accordance with the optimized [Filecoin version](https://spec.filecoin.io/algorithms/crypto/poseidon/).
|
||||
|
||||
Let understand how Poseidon works.
|
||||
Lets understand how Poseidon works.
|
||||
|
||||
### Initialization
|
||||
## Initialization
|
||||
|
||||
Poseidon starts with the initialization of its internal state, which is composed of the input elements and some pregenerated constants. An initial round constant is added to each element of the internal state. Adding The round constants ensure the state is properly mixed from the outset.
|
||||
Poseidon starts with the initialization of its internal state, which is composed of the input elements and some pre-generated constants. An initial round constant is added to each element of the internal state. Adding the round constants ensures the state is properly mixed from the beginning.
|
||||
|
||||
This is done to prevent collisions and to prevent certain cryptographic attacks by ensuring that the internal state is sufficiently mixed and unpredictable.
|
||||
|
||||

|
||||

|
||||
|
||||
### Applying full and partial rounds
|
||||
## Applying full and partial rounds
|
||||
|
||||
To generate a secure hash output, the algorithm goes through a series of "full rounds" and "partial rounds" as well as transformations between these sets of rounds.
|
||||
To generate a secure hash output, the algorithm goes through a series of "full rounds" and "partial rounds" as well as transformations between these sets of rounds in the following order:
|
||||
|
||||
First full rounds => apply SBox and Round constants => partial rounds => Last full rounds => Apply SBox
|
||||
```First full rounds -> apply S-box and Round constants -> partial rounds -> Last full rounds -> Apply S-box```
|
||||
|
||||
#### Full rounds
|
||||
### Full rounds
|
||||
|
||||

|
||||

|
||||
|
||||
**Uniform Application of S-Box:** In full rounds, the S-box (a non-linear transformation) is applied uniformly to every element of the hash function's internal state. This ensures a high degree of mixing and diffusion, contributing to the hash function's security. The functions S-box involves raising each element of the state to a certain power denoted by `α` a member of the finite field defined by the prime `p`, `α` can be different depending on the the implementation and user configuration.
|
||||
**Uniform Application of S-box:** In full rounds, the S-box (a non-linear transformation) is applied uniformly to every element of the hash function's internal state. This ensures a high degree of mixing and diffusion, contributing to the hash function's security. The functions S-box involves raising each element of the state to a certain power denoted by `α` a member of the finite field defined by the prime `p`; `α` can be different depending on the implementation and user configuration.
|
||||
|
||||
**Linear Transformation:** After applying the S-box, a linear transformation is performed on the state. This involves multiplying the state by a MDS (Maximum Distance Separable) Matrix. which further diffuses the transformations applied by the S-box across the entire state.
|
||||
|
||||
**Addition of Round Constants:** Each element of the state is then modified by adding a unique round constant. These constants are different for each round and are precomputed as part of the hash function's initialization. The addition of round constants ensures that even minor changes to the input produce significant differences in the output.
|
||||
|
||||
#### Partial Rounds
|
||||
### Partial Rounds
|
||||
|
||||

|
||||
|
||||
**Selective Application of S-Box:** Partial rounds apply the S-box transformation to only one element of the internal state per round, rather than to all elements. This selective application significantly reduces the computational complexity of the hash function without compromising its security. The choice of which element to apply the S-box to can follow a specific pattern or be fixed, depending on the design of the hash function.
|
||||
|
||||
**Linear Transformation and Round Constants:** A linear transformation is performed and round constants are added. The linear transformation in partial rounds can be designed to be less computationally intensive (this is done by using a sparse matrix) than in full rounds, further optimizing the function's efficiency.
|
||||
|
||||
|
||||
The user of Poseidon can often choose how many partial or full rounds he wishes to apply; more full rounds will increase security but degrade performance. The choice and balance is highly dependent on the use case.
|
||||
|
||||

|
||||
The user of Poseidon can often choose how many partial or full rounds he wishes to apply; more full rounds will increase security but degrade performance. The choice and balance are highly dependent on the use case.
|
||||
|
||||
## Using Poseidon
|
||||
|
||||
@@ -52,25 +51,21 @@ What that means is we calculate multiple hash-sums over multiple pre-images in p
|
||||
|
||||
So for Poseidon of arity 2 and input of size 1024 * 2, we would expect 1024 elements of output. Which means each block would be of size 2 and that would result in 1024 Poseidon hashes being performed.
|
||||
|
||||
### Supported API
|
||||
### Supported Bindings
|
||||
|
||||
[`Rust`](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/rust/icicle-core/src/poseidon), [`C++`](https://github.com/ingonyama-zk/icicle/tree/main/icicle/appUtils/poseidon)
|
||||
|
||||
### Supported curves
|
||||
|
||||
Poseidon supports the following curves:
|
||||
|
||||
`bls12-377`, `bls12-381`, `bn-254`, `bw6-761`
|
||||
[`Go`](https://github.com/ingonyama-zk/icicle/blob/main/wrappers/golang/curves/bn254/poseidon/poseidon.go)
|
||||
[`Rust`](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/rust/icicle-core/src/poseidon)
|
||||
|
||||
### Constants
|
||||
|
||||
Poseidon is extremely customizable and using different constants will produce different hashes, security levels and performance results.
|
||||
|
||||
We support pre-calculated and optimized constants for each of the [supported curves](#supported-curves).The constants can be found [here](https://github.com/ingonyama-zk/icicle/tree/main/icicle/appUtils/poseidon/constants) and are labeled clearly per curve `<curve_name>_poseidon.h`.
|
||||
We support pre-calculated and optimized constants for each of the [supported curves](../core#supported-curves-and-operations). The constants can be found [here](https://github.com/ingonyama-zk/icicle/tree/main/icicle/include/poseidon/constants) and are labeled clearly per curve `<curve_name>_poseidon.h`.
|
||||
|
||||
If you wish to generate your own constants you can use our python script which can be found [here](https://github.com/ingonyama-zk/icicle/blob/b6dded89cdef18348a5d4e2748b71ce4211c63ad/icicle/appUtils/poseidon/constants/generate_parameters.py#L1).
|
||||
If you wish to generate your own constants you can use our python script which can be found [here](https://github.com/ingonyama-zk/icicle/tree/main/icicle/include/poseidon/constants/generate_parameters.py).
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Install python 3
|
||||
- `pip install poseidon-hash`
|
||||
- `pip install galois==0.3.7`
|
||||
@@ -97,8 +92,6 @@ primitive_element = 7 # bls12-381
|
||||
# primitive_element = 15 # bw6-761
|
||||
```
|
||||
|
||||
We only support `alpha = 5` so if you want to use another alpha for SBox please reach out on discord or open a github issue.
|
||||
|
||||
### Rust API
|
||||
|
||||
This is the most basic way to use the Poseidon API.
|
||||
@@ -107,97 +100,95 @@ This is the most basic way to use the Poseidon API.
|
||||
let test_size = 1 << 10;
|
||||
let arity = 2u32;
|
||||
let ctx = get_default_device_context();
|
||||
let constants = load_optimized_poseidon_constants::<F>(arity, &ctx).unwrap();
|
||||
let config = PoseidonConfig::default();
|
||||
let poseidon = Poseidon::load(arity, &ctx).unwrap();
|
||||
let config = HashConfig::default();
|
||||
|
||||
let inputs = vec![F::one(); test_size * arity as usize];
|
||||
let outputs = vec![F::zero(); test_size];
|
||||
let mut input_slice = HostOrDeviceSlice::on_host(inputs);
|
||||
let mut output_slice = HostOrDeviceSlice::on_host(outputs);
|
||||
|
||||
poseidon_hash_many::<F>(
|
||||
poseidon.hash_many::<F>(
|
||||
&mut input_slice,
|
||||
&mut output_slice,
|
||||
test_size as u32,
|
||||
arity as u32,
|
||||
&constants,
|
||||
1, // Output length
|
||||
&config,
|
||||
)
|
||||
.unwrap();
|
||||
```
|
||||
|
||||
The `PoseidonConfig::default()` can be modified, by default the inputs and outputs are set to be on `Host` for example.
|
||||
The `HashConfig` can be modified, by default the inputs and outputs are set to be on `Host` for example.
|
||||
|
||||
|
||||
```
|
||||
impl<'a> Default for PoseidonConfig<'a> {
|
||||
```rust
|
||||
impl<'a> Default for HashConfig<'a> {
|
||||
fn default() -> Self {
|
||||
let ctx = get_default_device_context();
|
||||
Self {
|
||||
ctx,
|
||||
are_inputs_on_device: false,
|
||||
are_outputs_on_device: false,
|
||||
input_is_a_state: false,
|
||||
aligned: false,
|
||||
loop_state: false,
|
||||
is_async: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the example above `load_optimized_poseidon_constants::<F>(arity, &ctx).unwrap();` is used which will load the correct constants based on arity and curve. Its possible to [generate](#constants) your own constants and load them.
|
||||
In the example above `Poseidon::load(arity, &ctx).unwrap();` is used which will load the correct constants based on arity and curve. It's possible to [generate](#constants) your own constants and load them.
|
||||
|
||||
```rust
|
||||
let ctx = get_default_device_context();
|
||||
let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR");
|
||||
let constants_file = PathBuf::from(cargo_manifest_dir)
|
||||
.join("tests")
|
||||
.join(format!("{}_constants.bin", field_prefix));
|
||||
let mut constants_buf = vec![];
|
||||
File::open(constants_file)
|
||||
.unwrap()
|
||||
.read_to_end(&mut constants_buf)
|
||||
.unwrap();
|
||||
|
||||
let mut custom_constants = vec![];
|
||||
for chunk in constants_buf.chunks(field_bytes) {
|
||||
custom_constants.push(F::from_bytes_le(chunk));
|
||||
}
|
||||
|
||||
let custom_constants = create_optimized_poseidon_constants::<F>(
|
||||
arity as u32,
|
||||
&ctx,
|
||||
full_rounds_half,
|
||||
partial_rounds,
|
||||
&mut custom_constants,
|
||||
)
|
||||
.unwrap();
|
||||
let custom_poseidon = Poseidon::new(
|
||||
arity, // The arity of poseidon hash. The width will be equal to arity + 1
|
||||
alpha, // The S-box power
|
||||
full_rounds_half,
|
||||
partial_rounds,
|
||||
round_constants,
|
||||
mds_matrix,
|
||||
non_sparse_matrix,
|
||||
sparse_matrices,
|
||||
domain_tag,
|
||||
ctx,
|
||||
)
|
||||
.unwrap();
|
||||
```
|
||||
For more examples using different configurations refer here.
|
||||
|
||||
## The Tree Builder
|
||||
|
||||
The tree builder allows you to build Merkle trees using Poseidon.
|
||||
The tree builder allows you to build Merkle trees using Poseidon.
|
||||
|
||||
You can define both the tree's `height` and its `arity`. The tree `height` determines the number of layers in the tree, including the root and the leaf layer. The `arity` determines how many children each internal node can have.
|
||||
|
||||
```rust
|
||||
let height = 20;
|
||||
let arity = 2;
|
||||
let leaves = vec![F::one(); 1 << (height - 1)];
|
||||
let mut digests = vec![F::zero(); merkle_tree_digests_len(height, arity)];
|
||||
|
||||
let mut leaves_slice = HostOrDeviceSlice::on_host(leaves);
|
||||
|
||||
let ctx = get_default_device_context();
|
||||
let constants = load_optimized_poseidon_constants::<F>(arity, &ctx).unwrap()
|
||||
use icicle_bn254::tree::Bn254TreeBuilder;
|
||||
use icicle_bn254::poseidon::Poseidon;
|
||||
|
||||
let mut config = TreeBuilderConfig::default();
|
||||
config.keep_rows = 1;
|
||||
build_poseidon_merkle_tree::<F>(&mut leaves_slice, &mut digests, height, arity, &constants, &config).unwrap();
|
||||
let arity = 2;
|
||||
config.arity = arity as u32;
|
||||
let input_block_len = arity;
|
||||
let leaves = vec![F::one(); (1 << height) * arity];
|
||||
let mut digests = vec![F::zero(); merkle_tree_digests_len((height + 1) as u32, arity as u32, 1)];
|
||||
|
||||
println!("Root: {:?}", digests[0..1][0]);
|
||||
let leaves_slice = HostSlice::from_slice(&leaves);
|
||||
let digests_slice = HostSlice::from_mut_slice(&mut digests);
|
||||
|
||||
let ctx = device_context::DeviceContext::default();
|
||||
let hash = Poseidon::load(2, &ctx).unwrap();
|
||||
|
||||
let mut config = TreeBuilderConfig::default();
|
||||
config.keep_rows = 5;
|
||||
Bn254TreeBuilder::build_merkle_tree(
|
||||
leaves_slice,
|
||||
digests_slice,
|
||||
height,
|
||||
input_block_len,
|
||||
&hash,
|
||||
&hash,
|
||||
&config,
|
||||
)
|
||||
.unwrap();
|
||||
```
|
||||
|
||||
Similar to Poseidon, you can also configure the Tree Builder `TreeBuilderConfig::default()`
|
||||
@@ -206,9 +197,9 @@ Similar to Poseidon, you can also configure the Tree Builder `TreeBuilderConfig:
|
||||
- `are_inputs_on_device`: Have the inputs been loaded to device memory ?
|
||||
- `is_async`: Should the TreeBuilder run asynchronously? `False` will block the current CPU thread. `True` will require you call `cudaStreamSynchronize` or `cudaDeviceSynchronize` to retrieve the result.
|
||||
|
||||
### Benchmarks
|
||||
### Benchmarks
|
||||
|
||||
We ran the Poseidon tree builder on:
|
||||
We ran the Poseidon tree builder on:
|
||||
|
||||
**CPU**: 12th Gen Intel(R) Core(TM) i9-12900K/
|
||||
|
||||
@@ -218,9 +209,8 @@ We ran the Poseidon tree builder on:
|
||||
|
||||
The benchmarks include copying data from and to the device.
|
||||
|
||||
|
||||
| Rows to keep parameter | Run time, Icicle | Supranational PC2
|
||||
| ----------- | ----------- | ----------- |
|
||||
| ----------- | ----------- | -----------
|
||||
| 10 | 9.4 seconds | 13.6 seconds
|
||||
| 20 | 9.5 seconds | 13.6 seconds
|
||||
| 29 | 13.7 seconds | 13.6 seconds
|
||||
|
||||
88
docs/docs/icicle/primitives/poseidon2.md
Normal file
88
docs/docs/icicle/primitives/poseidon2.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Poseidon2
|
||||
|
||||
[Poseidon2](https://eprint.iacr.org/2023/323) is a recently released optimized version of Poseidon1. The two versions differ in two crucial points. First, Poseidon is a sponge hash function, while Poseidon2 can be either a sponge or a compression function depending on the use case. Secondly, Poseidon2 is instantiated by new and more efficient linear layers with respect to Poseidon. These changes decrease the number of multiplications in the linear layer by up to 90% and the number of constraints in Plonk circuits by up to 70%. This makes Poseidon2 currently the fastest arithmetization-oriented hash function without lookups.
|
||||
|
||||
|
||||
## Using Poseidon2
|
||||
|
||||
ICICLE Poseidon2 is implemented for GPU and parallelization is performed for each state.
|
||||
We calculate multiple hash-sums over multiple pre-images in parallel, rather than going block by block over the input vector.
|
||||
|
||||
For example, for Poseidon2 of width 16, input rate 8, output elements 8 and input of size 1024 * 8, we would expect 1024 * 8 elements of output. Which means each input block would be of size 8, resulting in 1024 Poseidon2 hashes being performed.
|
||||
|
||||
### Supported Bindings
|
||||
|
||||
[`Rust`](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/rust/icicle-core/src/poseidon2)
|
||||
|
||||
### Constants
|
||||
|
||||
Poseidon2 is also extremely customizable and using different constants will produce different hashes, security levels and performance results.
|
||||
|
||||
We support pre-calculated constants for each of the [supported curves](../core#supported-curves-and-operations). The constants can be found [here](https://github.com/ingonyama-zk/icicle/tree/main/icicle/include/poseidon2/constants) and are labeled clearly per curve `<curve_name>_poseidon2.h`.
|
||||
|
||||
You can also use your own set of constants as shown [here](https://github.com/ingonyama-zk/icicle/blob/main/wrappers/rust/icicle-fields/icicle-babybear/src/poseidon2/mod.rs#L290)
|
||||
|
||||
### Rust API
|
||||
|
||||
This is the most basic way to use the Poseidon2 API.
|
||||
|
||||
```rust
|
||||
let test_size = 1 << 10;
|
||||
let width = 16;
|
||||
let rate = 8;
|
||||
let ctx = get_default_device_context();
|
||||
let poseidon = Poseidon2::load(width, rate, MdsType::Default, DiffusionStrategy::Default, &ctx).unwrap();
|
||||
let config = HashConfig::default();
|
||||
|
||||
let inputs = vec![F::one(); test_size * rate as usize];
|
||||
let outputs = vec![F::zero(); test_size];
|
||||
let mut input_slice = HostOrDeviceSlice::on_host(inputs);
|
||||
let mut output_slice = HostOrDeviceSlice::on_host(outputs);
|
||||
|
||||
poseidon.hash_many::<F>(
|
||||
&mut input_slice,
|
||||
&mut output_slice,
|
||||
test_size as u32,
|
||||
rate as u32,
|
||||
8, // Output length
|
||||
&config,
|
||||
)
|
||||
.unwrap();
|
||||
```
|
||||
|
||||
In the example above `Poseidon2::load(width, rate, MdsType::Default, DiffusionStrategy::Default, &ctx).unwrap();` is used to load the correct constants based on width and curve. Here, the default MDS matrices and diffusion are used. If you want to get a Plonky3 compliant version, set them to `MdsType::Plonky` and `DiffusionStrategy::Montgomery` respectively.
|
||||
|
||||
## The Tree Builder
|
||||
|
||||
Similar to Poseidon1, you can use Poseidon2 in a tree builder.
|
||||
|
||||
```rust
|
||||
use icicle_bn254::tree::Bn254TreeBuilder;
|
||||
use icicle_bn254::poseidon2::Poseidon2;
|
||||
|
||||
let mut config = TreeBuilderConfig::default();
|
||||
let arity = 2;
|
||||
config.arity = arity as u32;
|
||||
let input_block_len = arity;
|
||||
let leaves = vec![F::one(); (1 << height) * arity];
|
||||
let mut digests = vec![F::zero(); merkle_tree_digests_len((height + 1) as u32, arity as u32, 1)];
|
||||
|
||||
let leaves_slice = HostSlice::from_slice(&leaves);
|
||||
let digests_slice = HostSlice::from_mut_slice(&mut digests);
|
||||
|
||||
let ctx = device_context::DeviceContext::default();
|
||||
let hash = Poseidon2::load(arity, arity, MdsType::Default, DiffusionStrategy::Default, &ctx).unwrap();
|
||||
|
||||
let mut config = TreeBuilderConfig::default();
|
||||
config.keep_rows = 5;
|
||||
Bn254TreeBuilder::build_merkle_tree(
|
||||
leaves_slice,
|
||||
digests_slice,
|
||||
height,
|
||||
input_block_len,
|
||||
&hash,
|
||||
&hash,
|
||||
&config,
|
||||
)
|
||||
.unwrap();
|
||||
```
|
||||
@@ -12,7 +12,7 @@ Rust bindings allow you to use ICICLE as a rust library.
|
||||
|
||||
Simply add the following to your `Cargo.toml`.
|
||||
|
||||
```
|
||||
```toml
|
||||
# GPU Icicle integration
|
||||
icicle-cuda-runtime = { git = "https://github.com/ingonyama-zk/icicle.git" }
|
||||
icicle-core = { git = "https://github.com/ingonyama-zk/icicle.git" }
|
||||
@@ -25,7 +25,7 @@ If you wish to point to a specific ICICLE branch add `branch = "<name_of_branch>
|
||||
|
||||
When you build your project ICICLE will be built as part of the build command.
|
||||
|
||||
# How do the rust bindings work?
|
||||
## How do the rust bindings work?
|
||||
|
||||
The rust bindings are just rust wrappers for ICICLE Core static libraries which can be compiled. We integrate the compilation of the static libraries into rusts toolchain to make usage seamless and easy. This is achieved by [extending rusts build command](https://github.com/ingonyama-zk/icicle/blob/main/wrappers/rust/icicle-curves/icicle-bn254/build.rs).
|
||||
|
||||
@@ -55,3 +55,33 @@ fn main() {
|
||||
println!("cargo:rustc-link-lib=cudart");
|
||||
}
|
||||
```
|
||||
|
||||
## Supported curves, fields and operations
|
||||
|
||||
### Supported curves and operations
|
||||
|
||||
| Operation\Curve | bn254 | bls12_377 | bls12_381 | bw6-761 | grumpkin |
|
||||
| --- | :---: | :---: | :---: | :---: | :---: |
|
||||
| MSM | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| G2 | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| NTT | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| ECNTT | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| VecOps | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Polynomials | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| Poseidon | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Merkle Tree | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
### Supported fields and operations
|
||||
|
||||
| Operation\Field | babybear | stark252 |
|
||||
| --- | :---: | :---: |
|
||||
| VecOps | ✅ | ✅ |
|
||||
| Polynomials | ✅ | ✅ |
|
||||
| NTT | ✅ | ✅ |
|
||||
| Extension Field | ✅ | ❌ |
|
||||
|
||||
### Supported hashes
|
||||
|
||||
| Hash | Sizes |
|
||||
| --- | :---: |
|
||||
| Keccak | 256, 512 |
|
||||
|
||||
31
docs/docs/icicle/rust-bindings/ecntt.md
Normal file
31
docs/docs/icicle/rust-bindings/ecntt.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# ECNTT
|
||||
|
||||
## ECNTT Method
|
||||
|
||||
The `ecntt` function computes the Elliptic Curve Number Theoretic Transform (EC-NTT) or its inverse on a batch of points of a curve.
|
||||
|
||||
```rust
|
||||
pub fn ecntt<C: Curve>(
|
||||
input: &(impl HostOrDeviceSlice<Projective<C>> + ?Sized),
|
||||
dir: NTTDir,
|
||||
cfg: &NTTConfig<C::ScalarField>,
|
||||
output: &mut (impl HostOrDeviceSlice<Projective<C>> + ?Sized),
|
||||
) -> IcicleResult<()>
|
||||
where
|
||||
C::ScalarField: FieldImpl,
|
||||
<C::ScalarField as FieldImpl>::Config: ECNTT<C>,
|
||||
{
|
||||
// ... function implementation ...
|
||||
}
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
- **`input`**: The input data as a slice of `Projective<C>`. This represents points on a specific elliptic curve `C`.
|
||||
- **`dir`**: The direction of the NTT. It can be `NTTDir::kForward` for forward NTT or `NTTDir::kInverse` for inverse NTT.
|
||||
- **`cfg`**: The NTT configuration object of type `NTTConfig<C::ScalarField>`. This object specifies parameters for the NTT computation, such as the batch size and algorithm to use.
|
||||
- **`output`**: The output buffer to write the results into. This should be a slice of `Projective<C>` with the same size as the input.
|
||||
|
||||
## Return Value
|
||||
|
||||
- **`IcicleResult<()>`**: This function returns an `IcicleResult` which is a wrapper type that indicates success or failure of the NTT computation. On success, it contains `Ok(())`.
|
||||
96
docs/docs/icicle/rust-bindings/keccak.md
Normal file
96
docs/docs/icicle/rust-bindings/keccak.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Keccak
|
||||
|
||||
## Keccak Example
|
||||
|
||||
```rust
|
||||
use icicle_cuda_runtime::memory::{DeviceVec, HostSlice};
|
||||
use icicle_hash::keccak::{keccak256, HashConfig};
|
||||
use rand::{self, Rng};
|
||||
|
||||
fn main() {
|
||||
let mut rng = rand::thread_rng();
|
||||
let initial_data: Vec<u8> = (0..120).map(|_| rng.gen::<u8>()).collect();
|
||||
println!("initial data: {}", hex::encode(&initial_data));
|
||||
let input = HostSlice::<u8>::from_slice(initial_data.as_slice());
|
||||
let mut output = DeviceVec::<u8>::cuda_malloc(32).unwrap();
|
||||
|
||||
let mut config = HashConfig::default();
|
||||
keccak256(input, initial_data.len() as i32, 1, &mut output[..], &mut config).expect("Failed to execute keccak256 hashing");
|
||||
|
||||
let mut output_host = vec![0_u8; 32];
|
||||
output.copy_to_host(HostSlice::from_mut_slice(&mut output_host[..])).unwrap();
|
||||
|
||||
println!("keccak256 result: {}", hex::encode(&output_host));
|
||||
}
|
||||
```
|
||||
|
||||
## Keccak Methods
|
||||
|
||||
```rust
|
||||
pub fn keccak256(
|
||||
input: &(impl HostOrDeviceSlice<u8> + ?Sized),
|
||||
input_block_size: i32,
|
||||
number_of_blocks: i32,
|
||||
output: &mut (impl HostOrDeviceSlice<u8> + ?Sized),
|
||||
config: &mut HashConfig,
|
||||
) -> IcicleResult<()>
|
||||
|
||||
pub fn keccak512(
|
||||
input: &(impl HostOrDeviceSlice<u8> + ?Sized),
|
||||
input_block_size: i32,
|
||||
number_of_blocks: i32,
|
||||
output: &mut (impl HostOrDeviceSlice<u8> + ?Sized),
|
||||
config: &mut HashConfig,
|
||||
) -> IcicleResult<()>
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- **`input`**: A slice containing the input data for the Keccak256 hash function. It can reside in either host memory or device memory.
|
||||
- **`input_block_size`**: An integer specifying the size of the input data for a single hash.
|
||||
- **`number_of_blocks`**: An integer specifying the number of results in the hash batch.
|
||||
- **`output`**: A slice where the resulting hash will be stored. This slice can be in host or device memory.
|
||||
- **`config`**: A pointer to a `HashConfig` object, which contains various configuration options for the Keccak256 operation.
|
||||
|
||||
### Return Value
|
||||
|
||||
- **`IcicleResult`**: Returns a CUDA error code indicating the success or failure of the Keccak256/Keccak512 operation.
|
||||
|
||||
## HashConfig
|
||||
|
||||
The `HashConfig` structure holds configuration parameters for the Keccak256/Keccak512 operation, allowing customization of its behavior to optimize performance based on the specifics of the operation or the underlying hardware.
|
||||
|
||||
```rust
|
||||
pub struct HashConfig<'a> {
|
||||
pub ctx: DeviceContext<'a>,
|
||||
pub are_inputs_on_device: bool,
|
||||
pub are_outputs_on_device: bool,
|
||||
pub is_async: bool,
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
- **`ctx`**: Device context containing details like device id and stream.
|
||||
- **`are_inputs_on_device`**: Indicates if input data is located on the device.
|
||||
- **`are_outputs_on_device`**: Indicates if output hash is stored on the device.
|
||||
- **`is_async`**: If true, runs the Keccak256/Keccak512 operation asynchronously.
|
||||
|
||||
### Usage
|
||||
|
||||
Example initialization with default settings:
|
||||
|
||||
```rust
|
||||
let default_config = HashConfig::default();
|
||||
```
|
||||
|
||||
Customizing the configuration:
|
||||
|
||||
```rust
|
||||
let custom_config = NTTConfig {
|
||||
ctx: custom_device_context,
|
||||
are_inputs_on_device: true,
|
||||
are_outputs_on_device: true,
|
||||
is_async: false,
|
||||
};
|
||||
```
|
||||
@@ -2,31 +2,24 @@
|
||||
|
||||
To understand the theory behind MSM pre computation technique refer to Niall Emmart's [talk](https://youtu.be/KAWlySN7Hm8?feature=shared&t=1734).
|
||||
|
||||
### Supported curves
|
||||
|
||||
`bls12-377`, `bls12-381`, `bn254`, `bw6-761`, `Grumpkin`
|
||||
|
||||
### `precompute_bases`
|
||||
## `precompute_points`
|
||||
|
||||
Precomputes bases for the multi-scalar multiplication (MSM) by extending each base point with its multiples, facilitating more efficient MSM calculations.
|
||||
|
||||
```rust
|
||||
pub fn precompute_bases<C: Curve + MSM<C>>(
|
||||
points: &HostOrDeviceSlice<Affine<C>>,
|
||||
precompute_factor: i32,
|
||||
_c: i32,
|
||||
ctx: &DeviceContext,
|
||||
output_bases: &mut HostOrDeviceSlice<Affine<C>>,
|
||||
pub fn precompute_points<C: Curve + MSM<C>>(
|
||||
points: &(impl HostOrDeviceSlice<Affine<C>> + ?Sized),
|
||||
msm_size: i32,
|
||||
cfg: &MSMConfig,
|
||||
output_bases: &mut DeviceSlice<Affine<C>>,
|
||||
) -> IcicleResult<()>
|
||||
```
|
||||
|
||||
|
||||
#### Parameters
|
||||
### Parameters
|
||||
|
||||
- **`points`**: The original set of affine points (\(P_1, P_2, ..., P_n\)) to be used in the MSM. For batch MSM operations, this should include all unique points concatenated together.
|
||||
- **`precompute_factor`**: Specifies the total number of points to precompute for each base, including the base point itself. This parameter directly influences the memory requirements and the potential speedup of the MSM operation.
|
||||
- **`_c`**: Currently unused. Intended for future use to align with the `c` parameter in `MSMConfig`, ensuring the precomputation is compatible with the bucket method's window size used in MSM.
|
||||
- **`ctx`**: The device context specifying the device ID and stream for execution. This context determines where the precomputation is performed (e.g., on a specific GPU).
|
||||
- **`msm_size`**: The size of a single msm in order to determine optimal parameters.
|
||||
- **`cfg`**: The MSM configuration parameters.
|
||||
- **`output_bases`**: The output buffer for the extended bases. Its size must be `points.len() * precompute_factor`. This buffer should be allocated on the device for GPU computations.
|
||||
|
||||
#### Returns
|
||||
@@ -42,22 +35,11 @@ The precomputation process is crucial for optimizing MSM operations, especially
|
||||
#### Example Usage
|
||||
|
||||
```rust
|
||||
let device_context = DeviceContext::default_for_device(0); // Use the default device
|
||||
let cfg = MSMConfig::default();
|
||||
let precompute_factor = 4; // Number of points to precompute
|
||||
let mut extended_bases = HostOrDeviceSlice::cuda_malloc(expected_size).expect("Failed to allocate memory for extended bases");
|
||||
|
||||
// Precompute the bases using the specified factor
|
||||
precompute_bases(&points, precompute_factor, 0, &device_context, &mut extended_bases)
|
||||
precompute_points(&points, msm_size, &cfg, &mut extended_bases)
|
||||
.expect("Failed to precompute bases");
|
||||
```
|
||||
|
||||
### Benchmarks
|
||||
|
||||
Benchmarks where performed on a Nvidia RTX 3090Ti.
|
||||
|
||||
| Pre-computation factor | bn254 size `2^20` MSM, ms. | bn254 size `2^12` MSM, size `2^10` batch, ms. | bls12-381 size `2^20` MSM, ms. | bls12-381 size `2^12` MSM, size `2^10` batch, ms. |
|
||||
| ------------- | ------------- | ------------- | ------------- | ------------- |
|
||||
| 1 | 14.1 | 82.8 | 25.5 | 136.7 |
|
||||
| 2 | 11.8 | 76.6 | 20.3 | 123.8 |
|
||||
| 4 | 10.9 | 73.8 | 18.1 | 117.8 |
|
||||
| 8 | 10.6 | 73.7 | 17.2 | 116.0 |
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# MSM
|
||||
|
||||
### Supported curves
|
||||
|
||||
`bls12-377`, `bls12-381`, `bn-254`, `bw6-761`, `grumpkin`
|
||||
|
||||
## Example
|
||||
|
||||
```rust
|
||||
@@ -84,7 +80,7 @@ pub struct MSMConfig<'a> {
|
||||
```
|
||||
|
||||
- **`ctx: DeviceContext`**: Specifies the device context, device id and the CUDA stream for asynchronous execution.
|
||||
- **`point_size: i32`**:
|
||||
- **`point_size: i32`**:
|
||||
- **`precompute_factor: i32`**: Determines the number of extra points to pre-compute for each point, affecting memory footprint and performance.
|
||||
- **`c: i32`**: The "window bitsize," a parameter controlling the computational complexity and memory footprint of the MSM operation.
|
||||
- **`bitsize: i32`**: The number of bits of the largest scalar, typically equal to the bit size of the scalar field.
|
||||
@@ -104,7 +100,7 @@ When performing MSM operations, it's crucial to match the size of the `scalars`
|
||||
|
||||
## How do I toggle between the supported algorithms?
|
||||
|
||||
When creating your MSM Config you may state which algorithm you wish to use. `is_big_triangle=true` will activate Large triangle accumulation and `is_big_triangle=false` will activate Bucket accumulation.
|
||||
When creating your MSM Config you may state which algorithm you wish to use. `is_big_triangle=true` will activate Large triangle reduction and `is_big_triangle=false` will activate iterative reduction.
|
||||
|
||||
```rust
|
||||
...
|
||||
@@ -120,7 +116,6 @@ msm::msm(&scalars, &points, &cfg, &mut msm_results).unwrap();
|
||||
|
||||
You may reference the rust code [here](https://github.com/ingonyama-zk/icicle/blob/77a7613aa21961030e4e12bf1c9a78a2dadb2518/wrappers/rust/icicle-core/src/msm/mod.rs#L54).
|
||||
|
||||
|
||||
## How do I toggle between MSM modes?
|
||||
|
||||
Toggling between MSM modes occurs automatically based on the number of results you are expecting from the `msm::msm` function. If you are expecting an array of `msm_results`, ICICLE will automatically split `scalars` and `points` into equal parts and run them as multiple MSMs in parallel.
|
||||
@@ -136,7 +131,6 @@ msm::msm(&scalars, &points, &cfg, &mut msm_result).unwrap();
|
||||
|
||||
In the example above we allocate a single expected result which the MSM method will interpret as `batch_size=1` and run a single MSM.
|
||||
|
||||
|
||||
In the next example, we are expecting 10 results which sets `batch_size=10` and runs 10 MSMs in batch mode.
|
||||
|
||||
```rust
|
||||
@@ -150,9 +144,13 @@ msm::msm(&scalars, &points, &cfg, &mut msm_results).unwrap();
|
||||
|
||||
Here is a [reference](https://github.com/ingonyama-zk/icicle/blob/77a7613aa21961030e4e12bf1c9a78a2dadb2518/wrappers/rust/icicle-core/src/msm/mod.rs#L108) to the code which automatically sets the batch size. For more MSM examples have a look [here](https://github.com/ingonyama-zk/icicle/blob/77a7613aa21961030e4e12bf1c9a78a2dadb2518/examples/rust/msm/src/main.rs#L1).
|
||||
|
||||
## Parameters for optimal performance
|
||||
|
||||
Please refer to the [primitive description](../primitives/msm#choosing-optimal-parameters)
|
||||
|
||||
## Support for G2 group
|
||||
|
||||
MSM also supports G2 group.
|
||||
MSM also supports G2 group.
|
||||
|
||||
Using MSM in G2 requires a G2 config, and of course your Points should also be G2 Points.
|
||||
|
||||
|
||||
@@ -62,11 +62,11 @@ Sets the current CUDA device by its ID, when calling `set_device` it will set th
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `device_id: usize`: The ID of the device to set as the current device. Device IDs start from 0.
|
||||
- **`device_id: usize`**: The ID of the device to set as the current device. Device IDs start from 0.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- `CudaResult<()>`: An empty result indicating success if the device is set successfully. In case of failure, returns a `CudaError`.
|
||||
- **`CudaResult<()>`**: An empty result indicating success if the device is set successfully. In case of failure, returns a `CudaError`.
|
||||
|
||||
**Errors:**
|
||||
|
||||
@@ -88,7 +88,7 @@ Retrieves the number of CUDA devices available on the machine.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- `CudaResult<usize>`: The number of available CUDA devices. On success, contains the count of CUDA devices. On failure, returns a `CudaError`.
|
||||
- **`CudaResult<usize>`**: The number of available CUDA devices. On success, contains the count of CUDA devices. On failure, returns a `CudaError`.
|
||||
|
||||
**Errors:**
|
||||
|
||||
@@ -109,7 +109,7 @@ Retrieves the ID of the current CUDA device.
|
||||
|
||||
**Returns:**
|
||||
|
||||
- `CudaResult<usize>`: The ID of the current CUDA device. On success, contains the device ID. On failure, returns a `CudaError`.
|
||||
- **`CudaResult<usize>`**: The ID of the current CUDA device. On success, contains the device ID. On failure, returns a `CudaError`.
|
||||
|
||||
**Errors:**
|
||||
|
||||
@@ -191,7 +191,7 @@ Validates that the specified `device_id` matches the ID of the currently active
|
||||
|
||||
#### Behavior
|
||||
|
||||
- **Panics** if the `device_id` does not match the active device's ID, preventing cross-device operation errors.
|
||||
- **`Panics`** if the `device_id` does not match the active device's ID, preventing cross-device operation errors.
|
||||
|
||||
#### Example
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
# NTT
|
||||
|
||||
### Supported curves
|
||||
|
||||
`bls12-377`, `bls12-381`, `bn-254`, `bw6-761`
|
||||
|
||||
## Example
|
||||
## Example
|
||||
|
||||
```rust
|
||||
use icicle_bn254::curve::{ScalarCfg, ScalarField};
|
||||
@@ -29,7 +25,7 @@ fn main() {
|
||||
// Create a CUDA stream
|
||||
let stream = CudaStream::create().expect("Failed to create CUDA stream");
|
||||
let ctx = DeviceContext::default(); // Assuming default device context
|
||||
ScalarCfg::initialize_domain(ScalarField::from_ark(icicle_omega), &ctx).unwrap();
|
||||
ScalarCfg::initialize_domain(ScalarField::from_ark(icicle_omega), &ctx, true).unwrap();
|
||||
|
||||
// Configure NTT
|
||||
let mut cfg = ntt::NTTConfig::default();
|
||||
@@ -61,14 +57,13 @@ pub fn ntt<F>(
|
||||
|
||||
`ntt:ntt` expects:
|
||||
|
||||
`input` - buffer to read the inputs of the NTT from. <br/>
|
||||
`dir` - whether to compute forward or inverse NTT. <br/>
|
||||
`cfg` - config used to specify extra arguments of the NTT. <br/>
|
||||
`output` - buffer to write the NTT outputs into. Must be of the same size as input.
|
||||
- **`input`** - buffer to read the inputs of the NTT from.
|
||||
- **`dir`** - whether to compute forward or inverse NTT.
|
||||
- **`cfg`** - config used to specify extra arguments of the NTT.
|
||||
- **`output`** - buffer to write the NTT outputs into. Must be of the same size as input.
|
||||
|
||||
The `input` and `output` buffers can be on device or on host. Being on host means that they will be transferred to device during runtime.
|
||||
|
||||
|
||||
### NTT Config
|
||||
|
||||
```rust
|
||||
@@ -107,8 +102,7 @@ The `NTTConfig` struct is a configuration object used to specify parameters for
|
||||
|
||||
- **`ntt_algorithm: NttAlgorithm`**: Can be one of `Auto`, `Radix2`, `MixedRadix`.
|
||||
`Auto` will select `Radix 2` or `Mixed Radix` algorithm based on heuristics.
|
||||
`Radix2` and `MixedRadix` will force the use of an algorithm regardless of the input size or other considerations. You should use one of these options when you know for sure that you want to
|
||||
|
||||
`Radix2` and `MixedRadix` will force the use of an algorithm regardless of the input size or other considerations. You should use one of these options when you know for sure that you want to
|
||||
|
||||
#### Usage
|
||||
|
||||
@@ -134,7 +128,6 @@ let custom_config = NTTConfig {
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
### Modes
|
||||
|
||||
NTT supports two different modes `Batch NTT` and `Single NTT`
|
||||
@@ -155,13 +148,13 @@ Deciding weather to use `batch NTT` vs `single NTT` is highly dependent on your
|
||||
Before performing NTT operations, its necessary to initialize the NTT domain, It only needs to be called once per GPU since the twiddles are cached.
|
||||
|
||||
```rust
|
||||
ScalarCfg::initialize_domain(ScalarField::from_ark(icicle_omega), &ctx).unwrap();
|
||||
ScalarCfg::initialize_domain(ScalarField::from_ark(icicle_omega), &ctx, true).unwrap();
|
||||
```
|
||||
|
||||
### `initialize_domain`
|
||||
|
||||
```rust
|
||||
pub fn initialize_domain<F>(primitive_root: F, ctx: &DeviceContext) -> IcicleResult<()>
|
||||
pub fn initialize_domain<F>(primitive_root: F, ctx: &DeviceContext, fast_twiddles: bool) -> IcicleResult<()>
|
||||
where
|
||||
F: FieldImpl,
|
||||
<F as FieldImpl>::Config: NTT<F>;
|
||||
@@ -177,23 +170,31 @@ where
|
||||
|
||||
- **`IcicleResult<()>`**: Will return an error if the operation fails.
|
||||
|
||||
### `initialize_domain_fast_twiddles_mode`
|
||||
#### Parameters
|
||||
|
||||
Similar to `initialize_domain`, `initialize_domain_fast_twiddles_mode` is a faster implementation and can be used for larger NTTs.
|
||||
- **`primitive_root`**: The primitive root of unity, chosen based on the maximum NTT size required for the computations. It must be of an order that is a power of two. This root is used to generate twiddle factors that are essential for the NTT operations.
|
||||
|
||||
- **`ctx`**: A reference to a `DeviceContext` specifying which device and stream the computation should be executed on.
|
||||
|
||||
#### Returns
|
||||
|
||||
- **`IcicleResult<()>`**: Will return an error if the operation fails.
|
||||
|
||||
### Releasing the domain
|
||||
|
||||
The `release_domain` function is responsible for releasing the resources associated with a specific domain in the CUDA device context.
|
||||
|
||||
```rust
|
||||
pub fn initialize_domain_fast_twiddles_mode<F>(primitive_root: F, ctx: &DeviceContext) -> IcicleResult<()>
|
||||
pub fn release_domain<F>(ctx: &DeviceContext) -> IcicleResult<()>
|
||||
where
|
||||
F: FieldImpl,
|
||||
<F as FieldImpl>::Config: NTT<F>;
|
||||
<F as FieldImpl>::Config: NTT<F>
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
|
||||
- **`primitive_root`**: The primitive root of unity, chosen based on the maximum NTT size required for the computations. It must be of an order that is a power of two. This root is used to generate twiddle factors that are essential for the NTT operations.
|
||||
|
||||
- **`ctx`**: A reference to a `DeviceContext` specifying which device and stream the computation should be executed on.
|
||||
|
||||
#### Returns
|
||||
|
||||
- **`IcicleResult<()>`**: Will return an error if the operation fails.
|
||||
The function returns an `IcicleResult<()>`, which represents the result of the operation. If the operation is successful, the function returns `Ok(())`, otherwise it returns an error.
|
||||
|
||||
287
docs/docs/icicle/rust-bindings/polynomials.md
Normal file
287
docs/docs/icicle/rust-bindings/polynomials.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# Rust FFI Bindings for Univariate Polynomial
|
||||
|
||||
:::note
|
||||
Please refer to the Polynomials overview page for a deep overview. This section is a brief description of the Rust FFI bindings.
|
||||
:::
|
||||
|
||||
This documentation is designed to provide developers with a clear understanding of how to utilize the Rust bindings for polynomial operations efficiently and effectively, leveraging the robust capabilities of both Rust and C++ in their applications.
|
||||
|
||||
## Introduction
|
||||
|
||||
The Rust FFI bindings for the Univariate Polynomial serve as a "shallow wrapper" around the underlying C++ implementation. These bindings provide a straightforward Rust interface that directly calls functions from a C++ library, effectively bridging Rust and C++ operations. The Rust layer handles simple interface translations without delving into complex logic or data structures, which are managed on the C++ side. This design ensures efficient data handling, memory management, and execution of polynomial operations directly via C++.
|
||||
Currently, these bindings are tailored specifically for polynomials where the coefficients, domain, and images are represented as scalar fields.
|
||||
|
||||
## Initialization Requirements
|
||||
|
||||
Before utilizing any functions from the polynomial API, it is mandatory to initialize the appropriate polynomial backend (e.g., CUDA). Additionally, the NTT (Number Theoretic Transform) domain must also be initialized, as the CUDA backend relies on this for certain operations. Failing to properly initialize these components can result in errors.
|
||||
|
||||
:::note
|
||||
**Field-Specific Initialization Requirement**
|
||||
|
||||
The ICICLE library is structured such that each field or curve has its dedicated library implementation. As a result, initialization must be performed individually for each field or curve to ensure the correct setup and functionality of the library.
|
||||
:::
|
||||
|
||||
## Core Trait: `UnivariatePolynomial`
|
||||
|
||||
The `UnivariatePolynomial` trait encapsulates the essential functionalities required for managing univariate polynomials in the Rust ecosystem. This trait standardizes the operations that can be performed on polynomials, regardless of the underlying implementation details. It allows for a unified approach to polynomial manipulation, providing a suite of methods that are fundamental to polynomial arithmetic.
|
||||
|
||||
### Trait Definition
|
||||
|
||||
```rust
|
||||
pub trait UnivariatePolynomial
|
||||
where
|
||||
Self::Field: FieldImpl,
|
||||
Self::FieldConfig: FieldConfig,
|
||||
{
|
||||
type Field: FieldImpl;
|
||||
type FieldConfig: FieldConfig;
|
||||
|
||||
// Methods to create polynomials from coefficients or roots-of-unity evaluations.
|
||||
fn from_coeffs<S: HostOrDeviceSlice<Self::Field> + ?Sized>(coeffs: &S, size: usize) -> Self;
|
||||
fn from_rou_evals<S: HostOrDeviceSlice<Self::Field> + ?Sized>(evals: &S, size: usize) -> Self;
|
||||
|
||||
// Method to divide this polynomial by another, returning quotient and remainder.
|
||||
fn divide(&self, denominator: &Self) -> (Self, Self) where Self: Sized;
|
||||
|
||||
// Method to divide this polynomial by the vanishing polynomial 'X^N-1'.
|
||||
fn div_by_vanishing(&self, degree: u64) -> Self;
|
||||
|
||||
// Methods to add or subtract a monomial in-place.
|
||||
fn add_monomial_inplace(&mut self, monomial_coeff: &Self::Field, monomial: u64);
|
||||
fn sub_monomial_inplace(&mut self, monomial_coeff: &Self::Field, monomial: u64);
|
||||
|
||||
// Method to slice the polynomial, creating a sub-polynomial.
|
||||
fn slice(&self, offset: u64, stride: u64, size: u64) -> Self;
|
||||
|
||||
// Methods to return new polynomials containing only the even or odd terms.
|
||||
fn even(&self) -> Self;
|
||||
fn odd(&self) -> Self;
|
||||
|
||||
// Method to evaluate the polynomial at a given domain point.
|
||||
fn eval(&self, x: &Self::Field) -> Self::Field;
|
||||
|
||||
// Method to evaluate the polynomial over a domain and store the results.
|
||||
fn eval_on_domain<D: HostOrDeviceSlice<Self::Field> + ?Sized, E: HostOrDeviceSlice<Self::Field> + ?Sized>(
|
||||
&self,
|
||||
domain: &D,
|
||||
evals: &mut E,
|
||||
);
|
||||
|
||||
// Method to evaluate the polynomial over the roots-of-unity domain for power-of-two sized domain
|
||||
fn eval_on_rou_domain<E: HostOrDeviceSlice<Self::Field> + ?Sized>(&self, domain_log_size: u64, evals: &mut E);
|
||||
|
||||
// Method to retrieve a coefficient at a specific index.
|
||||
fn get_coeff(&self, idx: u64) -> Self::Field;
|
||||
|
||||
// Method to copy coefficients into a provided slice.
|
||||
fn copy_coeffs<S: HostOrDeviceSlice<Self::Field> + ?Sized>(&self, start_idx: u64, coeffs: &mut S);
|
||||
|
||||
// Method to get the degree of the polynomial.
|
||||
fn degree(&self) -> i64;
|
||||
}
|
||||
```
|
||||
|
||||
## `DensePolynomial` Struct
|
||||
|
||||
The DensePolynomial struct represents a dense univariate polynomial in Rust, leveraging a handle to manage its underlying memory within the CUDA device context. This struct acts as a high-level abstraction over complex C++ memory management practices, facilitating the integration of high-performance polynomial operations through Rust's Foreign Function Interface (FFI) bindings.
|
||||
|
||||
```rust
|
||||
pub struct DensePolynomial {
|
||||
handle: PolynomialHandle,
|
||||
}
|
||||
```
|
||||
|
||||
### Traits implementation and methods
|
||||
|
||||
#### `Drop`
|
||||
|
||||
Ensures proper resource management by releasing the CUDA memory when a DensePolynomial instance goes out of scope. This prevents memory leaks and ensures that resources are cleaned up correctly, adhering to Rust's RAII (Resource Acquisition Is Initialization) principles.
|
||||
|
||||
#### `Clone`
|
||||
|
||||
Provides a way to create a new instance of a DensePolynomial with its own unique handle, thus duplicating the polynomial data in the CUDA context. Cloning is essential since the DensePolynomial manages external resources, which cannot be safely shared across instances without explicit duplication.
|
||||
|
||||
#### Operator Overloading: `Add`, `Sub`, `Mul`, `Rem`, `Div`
|
||||
|
||||
These traits are implemented for references to DensePolynomial (i.e., &DensePolynomial), enabling natural mathematical operations such as addition (+), subtraction (-), multiplication (*), division (/), and remainder (%). This syntactic convenience allows users to compose complex polynomial expressions in a way that is both readable and expressive.
|
||||
|
||||
#### Key Methods
|
||||
|
||||
In addition to the traits, the following methods are implemented:
|
||||
|
||||
```rust
|
||||
impl DensePolynomial {
|
||||
pub fn init_cuda_backend() -> bool {...}
|
||||
// Returns a mutable slice of the polynomial coefficients on the device
|
||||
pub fn coeffs_mut_slice(&mut self) -> &mut DeviceSlice<F> {...}
|
||||
}
|
||||
```
|
||||
|
||||
## Flexible Memory Handling With `HostOrDeviceSlice`
|
||||
|
||||
The DensePolynomial API is designed to accommodate a wide range of computational environments by supporting both host and device memory through the `HostOrDeviceSlice` trait. This approach ensures that polynomial operations can be seamlessly executed regardless of where the data resides, making the API highly adaptable and efficient for various hardware configurations.
|
||||
|
||||
### Overview of `HostOrDeviceSlice`
|
||||
|
||||
The HostOrDeviceSlice is a Rust trait that abstracts over slices of memory that can either be on the host (CPU) or the device (GPU), as managed by CUDA. This abstraction is crucial for high-performance computing scenarios where data might need to be moved between different memory spaces depending on the operations being performed and the specific hardware capabilities available.
|
||||
|
||||
### Usage in API Functions
|
||||
|
||||
Functions within the DensePolynomial API that deal with polynomial coefficients or evaluations use the HostOrDeviceSlice trait to accept inputs. This design allows the functions to be agnostic of the actual memory location of the data, whether it's in standard system RAM accessible by the CPU or in GPU memory accessible by CUDA cores.
|
||||
|
||||
```rust
|
||||
// Assume `coeffs` could either be in host memory or CUDA device memory
|
||||
let coeffs: DeviceSlice<F> = DeviceVec::<F>::cuda_malloc(coeffs_len).unwrap();
|
||||
let p_from_coeffs = PolynomialBabyBear::from_coeffs(&coeffs, coeffs.len());
|
||||
|
||||
// Similarly for evaluations from roots of unity
|
||||
let evals: HostSlice<F> = HostSlice::from_slice(&host_memory_evals);
|
||||
let p_from_evals = PolynomialBabyBear::from_rou_evals(&evals, evals.len());
|
||||
|
||||
// Same applies for any API that accepts HostOrDeviceSlice
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
This section outlines practical examples demonstrating how to utilize the `DensePolynomial` Rust API. The API is flexible, supporting multiple scalar fields. Below are examples showing how to use polynomials defined over different fields and perform a variety of operations.
|
||||
|
||||
### Initialization and Basic Operations
|
||||
|
||||
First, choose the appropriate field implementation for your polynomial operations, initializing the CUDA backend if necessary
|
||||
|
||||
```rust
|
||||
use icicle_babybear::polynomials::DensePolynomial as PolynomialBabyBear;
|
||||
|
||||
// Initialize the CUDA backend for polynomial operations
|
||||
PolynomialBabyBear::init_cuda_backend();
|
||||
let f = PolynomialBabyBear::from_coeffs(...);
|
||||
|
||||
// now use f by calling the implemented traits
|
||||
|
||||
// For operations over another field, such as BN254
|
||||
use icicle_bn254::polynomials::DensePolynomial as PolynomialBn254;
|
||||
// Use PolynomialBn254 similarly
|
||||
```
|
||||
|
||||
### Creation
|
||||
|
||||
Polynomials can be created from coefficients or evaluations:
|
||||
|
||||
```rust
|
||||
let coeffs = ...;
|
||||
let p_from_coeffs = PolynomialBabyBear::from_coeffs(HostSlice::from_slice(&coeffs), size);
|
||||
|
||||
let evals = ...;
|
||||
let p_from_evals = PolynomialBabyBear::from_rou_evals(HostSlice::from_slice(&evals), size);
|
||||
|
||||
```
|
||||
|
||||
### Arithmetic Operations
|
||||
|
||||
Utilize overloaded operators for intuitive mathematical expressions:
|
||||
|
||||
```rust
|
||||
let add = &f + &g; // Addition
|
||||
let sub = &f - &g; // Subtraction
|
||||
let mul = &f * &g; // Multiplication
|
||||
let mul_scalar = &f * &scalar; // Scalar multiplication
|
||||
```
|
||||
|
||||
### Division and Remainder
|
||||
|
||||
Compute quotient and remainder or perform division by a vanishing polynomial:
|
||||
|
||||
```rust
|
||||
let (q, r) = f.divide(&g); // Compute both quotient and remainder
|
||||
let q = &f / &g; // Quotient
|
||||
let r = &f % &g; // Remainder
|
||||
|
||||
let h = f.div_by_vanishing(N); // Division by V(x) = X^N - 1
|
||||
|
||||
```
|
||||
|
||||
### Monomial Operations
|
||||
|
||||
Add or subtract monomials in-place for efficient polynomial manipulation:
|
||||
|
||||
```rust
|
||||
f.add_monomial_inplace(&three, 1 /*monmoial*/); // Adds 3*x to f
|
||||
f.sub_monomial_inplace(&one, 0 /*monmoial*/); // Subtracts 1 from f
|
||||
```
|
||||
|
||||
### Slicing
|
||||
|
||||
Extract specific components:
|
||||
|
||||
```rust
|
||||
let even = f.even(); // Polynomial of even-indexed terms
|
||||
let odd = f.odd(); // Polynomial of odd-indexed terms
|
||||
let arbitrary_slice = f.slice(offset, stride, size);
|
||||
```
|
||||
|
||||
### Evaluate
|
||||
|
||||
Evaluate the polynoomial:
|
||||
|
||||
```rust
|
||||
let x = rand(); // Random field element
|
||||
let f_x = f.eval(&x); // Evaluate f at x
|
||||
|
||||
// Evaluate on a predefined domain
|
||||
let domain = [one, two, three];
|
||||
let mut host_evals = vec![ScalarField::zero(); domain.len()];
|
||||
f.eval_on_domain(HostSlice::from_slice(&domain), HostSlice::from_mut_slice(&mut host_evals));
|
||||
|
||||
// Evaluate on roots-of-unity-domain
|
||||
let domain_log_size = 4;
|
||||
let mut device_evals = DeviceVec::<ScalarField>::cuda_malloc(1 << domain_log_size).unwrap();
|
||||
f.eval_on_rou_domain(domain_log_size, &mut device_evals[..]);
|
||||
```
|
||||
|
||||
### Read coefficients
|
||||
|
||||
Read or copy polynomial coefficients for further processing:
|
||||
|
||||
```rust
|
||||
let x_squared_coeff = f.get_coeff(2); // Coefficient of x^2
|
||||
|
||||
// Copy coefficients to a device-specific memory space
|
||||
let mut device_mem = DeviceVec::<Field>::cuda_malloc(coeffs.len()).unwrap();
|
||||
f.copy_coeffs(0, &mut device_mem[..]);
|
||||
```
|
||||
|
||||
### Polynomial Degree
|
||||
|
||||
Determine the highest power of the variable with a non-zero coefficient:
|
||||
|
||||
```rust
|
||||
let deg = f.degree(); // Degree of the polynomial
|
||||
```
|
||||
|
||||
### Memory Management: Views (rust slices)
|
||||
|
||||
Rust enforces correct usage of views at compile time, eliminating the need for runtime checks:
|
||||
|
||||
```rust
|
||||
let mut f = Poly::from_coeffs(HostSlice::from_slice(&coeffs), size);
|
||||
|
||||
// Obtain a mutable slice of coefficients as a DeviceSlice
|
||||
let coeffs_slice_dev = f.coeffs_mut_slice();
|
||||
|
||||
// Operations on f are restricted here due to mutable borrow of coeffs_slice_dev
|
||||
|
||||
// Compute evaluations or perform other operations directly using the slice
|
||||
// example: evaluate f on a coset of roots-of-unity. Computing from GPU to HOST/GPU
|
||||
let mut config: NTTConfig<'_, F> = NTTConfig::default();
|
||||
config.coset_gen = /*some coset gen*/;
|
||||
let mut coset_evals = vec![F::zero(); coeffs_slice_dev.len()];
|
||||
ntt(
|
||||
coeffs_slice_dev,
|
||||
NTTDir::kForward,
|
||||
&config,
|
||||
HostSlice::from_mut_slice(&mut coset_evals),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// now can f can be borrowed once again
|
||||
```
|
||||
@@ -1,13 +1,6 @@
|
||||
# Vector Operations API
|
||||
|
||||
Our vector operations API which is part of `icicle-cuda-runtime` package, includes fundamental methods for addition, subtraction, and multiplication of vectors, with support for both host and device memory.
|
||||
|
||||
|
||||
## Supported curves
|
||||
|
||||
Vector operations are supported on the following curves:
|
||||
|
||||
`bls12-377`, `bls12-381`, `bn-254`, `bw6-761`, `grumpkin`
|
||||
Our vector operations API which is part of `icicle-cuda-runtime` package, includes fundamental methods for addition, subtraction, and multiplication of vectors, with support for both host and device memory.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -59,7 +52,6 @@ let cfg = VecOpsConfig::default();
|
||||
mul_scalars(&a, &ones, &mut result, &cfg).unwrap();
|
||||
```
|
||||
|
||||
|
||||
## Vector Operations Configuration
|
||||
|
||||
The `VecOpsConfig` struct encapsulates the settings for vector operations, including device context and operation modes.
|
||||
@@ -74,7 +66,6 @@ pub struct VecOpsConfig<'a> {
|
||||
is_a_on_device: bool,
|
||||
is_b_on_device: bool,
|
||||
is_result_on_device: bool,
|
||||
is_result_montgomery_form: bool,
|
||||
pub is_async: bool,
|
||||
}
|
||||
```
|
||||
@@ -85,14 +76,13 @@ pub struct VecOpsConfig<'a> {
|
||||
- **`is_a_on_device`**: Indicates if the first operand vector resides in device memory.
|
||||
- **`is_b_on_device`**: Indicates if the second operand vector resides in device memory.
|
||||
- **`is_result_on_device`**: Specifies if the result vector should be stored in device memory.
|
||||
- **`is_result_montgomery_form`**: Determines if the result should be in Montgomery form.
|
||||
- **`is_async`**: Enables asynchronous operation. If `true`, operations are non-blocking; otherwise, they block the current thread.
|
||||
|
||||
### Default Configuration
|
||||
|
||||
`VecOpsConfig` can be initialized with default settings tailored for a specific device:
|
||||
|
||||
```
|
||||
```rust
|
||||
let cfg = VecOpsConfig::default();
|
||||
```
|
||||
|
||||
@@ -112,7 +102,6 @@ impl<'a> VecOpsConfig<'a> {
|
||||
is_a_on_device: false,
|
||||
is_b_on_device: false,
|
||||
is_result_on_device: false,
|
||||
is_result_montgomery_form: false,
|
||||
is_async: false,
|
||||
}
|
||||
}
|
||||
@@ -121,7 +110,7 @@ impl<'a> VecOpsConfig<'a> {
|
||||
|
||||
## Vector Operations
|
||||
|
||||
Vector operations are implemented through the `VecOps` trait, these traits are implemented for all [supported curves](#supported-curves) providing methods for addition, subtraction, and multiplication of vectors.
|
||||
Vector operations are implemented through the `VecOps` trait, providing methods for addition, subtraction, and multiplication of vectors.
|
||||
|
||||
### `VecOps` Trait
|
||||
|
||||
@@ -157,3 +146,62 @@ All operations are element-wise operations, and the results placed into the `res
|
||||
- **`add`**: Computes the element-wise sum of two vectors.
|
||||
- **`sub`**: Computes the element-wise difference between two vectors.
|
||||
- **`mul`**: Performs element-wise multiplication of two vectors.
|
||||
|
||||
## MatrixTranspose API Documentation
|
||||
|
||||
This section describes the functionality of the `TransposeMatrix` function used for matrix transposition.
|
||||
|
||||
The function takes a matrix represented as a 1D slice and transposes it, storing the result in another 1D slice.
|
||||
|
||||
### Function
|
||||
|
||||
```rust
|
||||
pub fn transpose_matrix<F>(
|
||||
input: &HostOrDeviceSlice<F>,
|
||||
row_size: u32,
|
||||
column_size: u32,
|
||||
output: &mut HostOrDeviceSlice<F>,
|
||||
ctx: &DeviceContext,
|
||||
on_device: bool,
|
||||
is_async: bool,
|
||||
) -> IcicleResult<()>
|
||||
where
|
||||
F: FieldImpl,
|
||||
<F as FieldImpl>::Config: VecOps<F>
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- **`input`**: A slice representing the input matrix. The slice can be stored on either the host or the device.
|
||||
- **`row_size`**: The number of rows in the input matrix.
|
||||
- **`column_size`**: The number of columns in the input matrix.
|
||||
- **`output`**: A mutable slice to store the transposed matrix. The slice can be stored on either the host or the device.
|
||||
- **`ctx`**: A reference to the `DeviceContext`, which provides information about the device where the operation will be performed.
|
||||
- **`on_device`**: A boolean flag indicating whether the inputs and outputs are on the device.
|
||||
- **`is_async`**: A boolean flag indicating whether the operation should be performed asynchronously.
|
||||
|
||||
### Return Value
|
||||
|
||||
`Ok(())` if the operation is successful, or an `IcicleResult` error otherwise.
|
||||
|
||||
### Example
|
||||
|
||||
```rust
|
||||
use icicle::HostOrDeviceSlice;
|
||||
use icicle::DeviceContext;
|
||||
use icicle::FieldImpl;
|
||||
use icicle::VecOps;
|
||||
|
||||
let input: HostOrDeviceSlice<i32> = // ...;
|
||||
let mut output: HostOrDeviceSlice<i32> = // ...;
|
||||
let ctx: DeviceContext = // ...;
|
||||
|
||||
transpose_matrix(&input, 5, 4, &mut output, &ctx, true, false)
|
||||
.expect("Failed to transpose matrix");
|
||||
```
|
||||
|
||||
The function takes a matrix represented as a 1D slice, transposes it, and stores the result in another 1D slice. The input and output slices can be stored on either the host or the device, and the operation can be performed synchronously or asynchronously.
|
||||
|
||||
The function is generic and can work with any type `F` that implements the `FieldImpl` trait. The `<F as FieldImpl>::Config` type must also implement the `VecOps<F>` trait, which provides the `transpose` method used to perform the actual transposition.
|
||||
|
||||
The function returns an `IcicleResult<()>`, indicating whether the operation was successful or not.
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
# Supporting Additional Curves
|
||||
|
||||
We understand the need for ZK developers to use different curves, some common some more exotic. For this reason we designed ICICLE to allow developers to add any curve they desire.
|
||||
|
||||
## ICICLE Core
|
||||
|
||||
ICICLE core is very generic by design so all algorithms and primitives are designed to work based of configuration files [selected during compile](https://github.com/ingonyama-zk/icicle/blob/main/icicle/curves/curve_config.cuh) time. This is why we compile ICICLE Core per curve.
|
||||
|
||||
To add support for a new curve you must create a new file under [`icicle/curves`](https://github.com/ingonyama-zk/icicle/tree/main/icicle/curves). The file should be named `<curve_name>_params.cuh`.
|
||||
|
||||
### Adding curve_name_params.cuh
|
||||
|
||||
Start by copying `bn254_params.cuh` contents in your params file. Params should include:
|
||||
- **fq_config** - parameters of the Base field.
|
||||
- **limbs_count** - `ceil(field_byte_size / 4)`.
|
||||
- **modulus_bit_count** - bit-size of the modulus.
|
||||
- **num_of_reductions** - the number of times to reduce in reduce function. Use 2 if not sure.
|
||||
- **modulus** - modulus of the field.
|
||||
- **modulus_2** - modulus * 2.
|
||||
- **modulus_4** - modulus * 4.
|
||||
- **neg_modulus** - negated modulus.
|
||||
- **modulus_wide** - modulus represented as a double-sized integer.
|
||||
- **modulus_squared** - modulus**2 represented as a double-sized integer.
|
||||
- **modulus_squared_2** - 2 * modulus**2 represented as a double-sized integer.
|
||||
- **modulus_squared_4** - 4 * modulus**2 represented as a double-sized integer.
|
||||
- **m** - value used in multiplication. Can be computed as `2**(2*modulus_bit_count) // modulus`.
|
||||
- **one** - multiplicative identity.
|
||||
- **zero** - additive identity.
|
||||
- **montgomery_r** - `2 ** M % modulus` where M is a closest (larger than) bitsize multiple of 32. E.g. 384 or 768 for bls and bw curves respectively
|
||||
- **montgomery_r_inv** - `2 ** (-M) % modulus`
|
||||
- **fp_config** - parameters of the Scalar field.
|
||||
Same as fq_config, but with additional arguments:
|
||||
- **omegas_count** - [two-adicity](https://cryptologie.net/article/559/whats-two-adicity/) of the field. And thus the maximum size of NTT.
|
||||
- **omegas** - an array of omegas for NTTs. An array of size `omegas_count`. The ith element is equal to `1.nth_root(2**(2**(omegas_count-i)))`.
|
||||
- **inv** - an array of inverses of powers of two in a field. Ith element is equal to `(2 ** (i+1)) ** -1`.
|
||||
- **G1 generators points** - affine coordinates of the generator point.
|
||||
- **G2 generators points** - affine coordinates of the extension generator. Remove these if `G2` is not supported.
|
||||
- **Weierstrass b value** - base field element equal to value of `b` in the curve equation.
|
||||
- **Weierstrass b value G2** - base field element equal to value of `b` for the extension. Remove this if `G2` is not supported.
|
||||
|
||||
:::note
|
||||
|
||||
All the params are not in Montgomery form.
|
||||
|
||||
:::
|
||||
|
||||
:::note
|
||||
|
||||
To convert number values into `storage` type you can use the following python function
|
||||
|
||||
```python
|
||||
import struct
|
||||
|
||||
def unpack(x, field_size):
|
||||
return ', '.join(["0x" + format(x, '08x') for x in struct.unpack('I' * (field_size) // 4, int(x).to_bytes(field_size, 'little'))])
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
We also require some changes to [`curve_config.cuh`](https://github.com/ingonyama-zk/icicle/blob/main/icicle/curves/curve_config.cuh#L16-L29), we need to add a new curve id.
|
||||
|
||||
```
|
||||
...
|
||||
|
||||
#define BN254 1
|
||||
#define BLS12_381 2
|
||||
#define BLS12_377 3
|
||||
#define BW6_761 4
|
||||
#define GRUMPKIN 5
|
||||
#define <curve_name> 6
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Make sure to modify the [rest of the file](https://github.com/ingonyama-zk/icicle/blob/4beda3a900eda961f39af3a496f8184c52bf3b41/icicle/curves/curve_config.cuh#L16-L29) accordingly.
|
||||
|
||||
Finally we must modify the [`make` file](https://github.com/ingonyama-zk/icicle/blob/main/icicle/CMakeLists.txt#L64) to make sure we can compile our new curve.
|
||||
|
||||
```
|
||||
set(SUPPORTED_CURVES bn254;bls12_381;bls12_377;bw6_761;grumpkin;<curve_name>)
|
||||
```
|
||||
|
||||
### Adding Poseidon support
|
||||
|
||||
If you want your curve to implement a Poseidon hash function or a tree builder, you will need to pre-calculate its optimized parameters.
|
||||
Copy [constants_template.h](https://github.com/ingonyama-zk/icicle/blob/main/icicle/appUtils/poseidon/constants/constants_template.h) into `icicle/appUtils/poseidon/constants/<CURVE>_poseidon.h`. Run the [constants generation script](https://dev.ingonyama.com/icicle/primitives/poseidon#constants). The script will print the number of partial rounds and generate a `constants.bin` file. Use `xxd -i constants.bin` to parse the file into C declarations. Copy the `unsigned char constants_bin[]` contents inside your new file. Repeat this process for arities 2, 4, 8 and 11.
|
||||
|
||||
After you've generated the constants, add your curve in this [SUPPORTED_CURVES_WITH_POSEIDON](https://github.com/ingonyama-zk/icicle/blob/main/icicle/CMakeLists.txt#L72) in the `CMakeLists.txt`.
|
||||
|
||||
## Bindings
|
||||
|
||||
In order to support a new curve in the binding libraries you first must support it in ICICLE core.
|
||||
|
||||
### Rust
|
||||
|
||||
Go to [rust curves folder](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/rust/icicle-curves) and copy `icicle-curve-template` to a new folder named `icicle-<curve_name>`.
|
||||
|
||||
Find all the occurrences of `<CURVE>` placeholder inside the crate. (You can use `Ctrl+Shift+F` in VS Code or `grep -nr "<CURVE>"` in bash). You will then need to replace each occurrence with your new curve name.
|
||||
|
||||
#### Limbs
|
||||
|
||||
Go to your curve's `curve.rs` file and set `SCALAR_LIMBS`, `BASE_LIMBS` and `G2_BASE_LIMBS` (if G2 is needed) to a minimum number of `u64` required to store a single scalar field / base field element respectively.
|
||||
e.g. for bn254, scalar field is 254 bit so `SCALAR_LIMBS` is set to 4.
|
||||
|
||||
#### Primitives
|
||||
|
||||
If your curve doesn't support some of the primitives (ntt/msm/poseidon/merkle tree/), or you simply don't want to include it, just remove a corresponding module from `src` and then from `lib.rs`
|
||||
|
||||
#### G2
|
||||
|
||||
If your curve doesn't support G2 - remove all the code under `#[cfg(feature = "g2")]` and remove the feature from [Cargo.toml](https://github.com/ingonyama-zk/icicle/blob/main/wrappers/rust/icicle-curves/icicle-bn254/Cargo.toml#L29) and [build.rs](https://github.com/ingonyama-zk/icicle/blob/main/wrappers/rust/icicle-curves/icicle-bn254/build.rs#L15).
|
||||
|
||||
After this is done, add your new crate in the [global Cargo.toml](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/rust/Cargo.toml).
|
||||
|
||||
### Golang
|
||||
|
||||
Golang is WIP in v1, coming soon. Please checkout a previous [release v0.1.0](https://github.com/ingonyama-zk/icicle/releases/tag/v0.1.0) for golang bindings.
|
||||
@@ -11,7 +11,7 @@ Ingonyama is a next-generation semiconductor company, focusing on Zero-Knowledge
|
||||
Currently our flagship products are:
|
||||
|
||||
- **ICICLE**:
|
||||
[ICICLE](https://github.com/ingonyama-zk/icicle) is a fully featured GPU accelerated cryptography library for building ZK provers. ICICLE allows you to accelerate your ZK existing protocols in a matter of hours or implement your protocol from scratch on GPU.
|
||||
[ICICLE](https://github.com/ingonyama-zk/icicle) is a fully featured GPU accelerated cryptography library for building ZK provers. ICICLE allows you to accelerate your existing ZK protocols in a matter of hours or implement your protocol from scratch on GPU.
|
||||
|
||||
---
|
||||
|
||||
@@ -39,7 +39,7 @@ Learn more about ICICLE and GPUs [here][ICICLE-OVERVIEW].
|
||||
|
||||
## Get in Touch
|
||||
|
||||
If you have any questions, ideas, or are thinking of building something in this space join the discussion on [Discord]. You can explore our code on [github](https://github.com/ingonyama-zk) or read some of [our research papers](https://github.com/ingonyama-zk/papers).
|
||||
If you have any questions, ideas, or are thinking of building something in this space, join the discussion on [Discord]. You can explore our code on [github](https://github.com/ingonyama-zk) or read some of [our research papers](https://github.com/ingonyama-zk/papers).
|
||||
|
||||
Follow us on [Twitter](https://x.com/Ingo_zk) and [YouTube](https://www.youtube.com/@ingo_ZK) and sign up for our [mailing list](https://wkf.ms/3LKCbdj) to get our latest announcements.
|
||||
|
||||
|
||||
@@ -163,8 +163,17 @@ const config = {
|
||||
prism: {
|
||||
theme: lightCodeTheme,
|
||||
darkTheme: darkCodeTheme,
|
||||
additionalLanguages: ['rust', 'go'],
|
||||
},
|
||||
image: 'img/logo.png',
|
||||
announcementBar: {
|
||||
id: 'announcement', // Any value that will identify this message.
|
||||
content:
|
||||
'<strong>🎉 Read our paper on the Polynomials API in ICICLE v2 by clicking <a target="_blank" rel="noopener noreferrer" href="https://eprint.iacr.org/2024/973">here</a>! 🎉</strong>',
|
||||
backgroundColor: '#ADD8E6', // Light blue background color.
|
||||
textColor: '#000000', // Black text color.
|
||||
isCloseable: true, // Defaults to `true`.
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
9761
docs/package-lock.json
generated
9761
docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
122
docs/sidebars.js
122
docs/sidebars.js
@@ -21,8 +21,54 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "ICICLE Provers",
|
||||
id: "icicle/integrations"
|
||||
label: "ICICLE Core",
|
||||
id: "icicle/core",
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Primitives",
|
||||
link: {
|
||||
type: `doc`,
|
||||
id: 'icicle/primitives/overview',
|
||||
},
|
||||
collapsed: true,
|
||||
items: [
|
||||
{
|
||||
type: "doc",
|
||||
label: "MSM",
|
||||
id: "icicle/primitives/msm",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "NTT",
|
||||
id: "icicle/primitives/ntt",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "Keccak Hash",
|
||||
id: "icicle/primitives/keccak",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "Poseidon Hash",
|
||||
id: "icicle/primitives/poseidon",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "Poseidon2 Hash",
|
||||
id: "icicle/primitives/poseidon2",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "Polynomials",
|
||||
id: "icicle/polynomials/overview",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "Multi GPU Support",
|
||||
id: "icicle/multi-gpu",
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
@@ -54,15 +100,25 @@ module.exports = {
|
||||
label: "NTT",
|
||||
id: "icicle/golang-bindings/ntt",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "EC-NTT",
|
||||
id: "icicle/golang-bindings/ecntt",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "Vector operations",
|
||||
id: "icicle/golang-bindings/vec-ops",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "Multi GPU Support",
|
||||
id: "icicle/golang-bindings/multi-gpu",
|
||||
type: "doc",
|
||||
label: "Keccak Hash",
|
||||
id: "icicle/golang-bindings/keccak",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "Multi GPU Support",
|
||||
id: "icicle/golang-bindings/multi-gpu",
|
||||
},
|
||||
]
|
||||
},
|
||||
@@ -96,66 +152,45 @@ module.exports = {
|
||||
label: "NTT",
|
||||
id: "icicle/rust-bindings/ntt",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "EC-NTT",
|
||||
id: "icicle/rust-bindings/ecntt",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "Vector operations",
|
||||
id: "icicle/rust-bindings/vec-ops",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "Keccak Hash",
|
||||
id: "icicle/rust-bindings/keccak",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "Multi GPU Support",
|
||||
id: "icicle/rust-bindings/multi-gpu",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Primitives",
|
||||
link: {
|
||||
type: `doc`,
|
||||
id: 'icicle/primitives/overview',
|
||||
},
|
||||
collapsed: true,
|
||||
items: [
|
||||
{
|
||||
type: "doc",
|
||||
label: "MSM",
|
||||
id: "icicle/primitives/msm",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "NTT",
|
||||
id: "icicle/primitives/ntt",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "Poseidon Hash",
|
||||
id: "icicle/primitives/poseidon",
|
||||
label: "Polynomials",
|
||||
id: "icicle/rust-bindings/polynomials",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "Multi GPU Support",
|
||||
id: "icicle/multi-gpu",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "Supporting additional curves",
|
||||
id: "icicle/supporting-additional-curves",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "Google Colab Instructions",
|
||||
id: "icicle/colab-instructions",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "ICICLE Provers",
|
||||
id: "icicle/integrations"
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "ZK Containers",
|
||||
id: "ZKContainers",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
label: "Ingonyama Grant program",
|
||||
@@ -170,6 +205,7 @@ module.exports = {
|
||||
type: "category",
|
||||
label: "Additional Resources",
|
||||
collapsed: false,
|
||||
collapsible: false,
|
||||
items: [
|
||||
{
|
||||
type: "link",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ZKContainer
|
||||
|
||||
We recommend using [ZKContainer](https://ingonyama.com/blog/Immanuel-ZKDC), where we have already preinstalled all the required dependencies, to run Icicle examples.
|
||||
We recommend using [ZKContainer](https://www.ingonyama.com/blog/product-announcement-zk-containers), where we have already preinstalled all the required dependencies, to run Icicle examples.
|
||||
To use our containers you will need [Docker](https://www.docker.com/) and [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/index.html).
|
||||
|
||||
In each example directory, ZKContainer files are located in a subdirectory `.devcontainer`.
|
||||
|
||||
23
examples/c++/best-practice-ntt/CMakeLists.txt
Normal file
23
examples/c++/best-practice-ntt/CMakeLists.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
cmake_minimum_required(VERSION 3.18)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CUDA_STANDARD 17)
|
||||
set(CMAKE_CUDA_STANDARD_REQUIRED TRUE)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
|
||||
if (${CMAKE_VERSION} VERSION_LESS "3.24.0")
|
||||
set(CMAKE_CUDA_ARCHITECTURES ${CUDA_ARCH})
|
||||
else()
|
||||
set(CMAKE_CUDA_ARCHITECTURES native) # on 3.24+, on earlier it is ignored, and the target is not passed
|
||||
endif ()
|
||||
project(example LANGUAGES CUDA CXX)
|
||||
|
||||
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --expt-relaxed-constexpr")
|
||||
set(CMAKE_CUDA_FLAGS_RELEASE "")
|
||||
set(CMAKE_CUDA_FLAGS_DEBUG "${CMAKE_CUDA_FLAGS_DEBUG} -g -G -O0")
|
||||
|
||||
add_executable(
|
||||
example
|
||||
example.cu
|
||||
)
|
||||
target_include_directories(example PRIVATE "../../../icicle/include")
|
||||
target_link_libraries(example ${CMAKE_SOURCE_DIR}/build/icicle/lib/libingo_field_bn254.a)
|
||||
set_target_properties(example PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
|
||||
33
examples/c++/best-practice-ntt/README.md
Normal file
33
examples/c++/best-practice-ntt/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# ICICLE best practices: Concurrent Data Transfer and NTT Computation
|
||||
|
||||
The [Number Theoretic Transform (NTT)](https://dev.ingonyama.com/icicle/primitives/ntt) is an integral component of many cryptographic algorithms, such as polynomial multiplication in Zero Knowledge Proofs. The performance bottleneck of NTT on GPUs is the data transfer between the host (CPU) and the device (GPU). In a typical NVIDIA GPU this transfer dominates the total NTT execution time.
|
||||
|
||||
## Key-Takeaway
|
||||
|
||||
When you have to run several NTTs, consider Concurrent Data Download, Upload, and Computation to improve data bus (PCIe) and GPU utilization, and get better total execution time.
|
||||
|
||||
Typically, you concurrently
|
||||
|
||||
1. Download the output of a previous NTT back to the host
|
||||
2. Upload the input for a next NTT on the device
|
||||
3. Run current NTT
|
||||
|
||||
> [!NOTE]
|
||||
> This approach requires two on-device memory vectors, decreasing the maximum size of NTT by 2x.
|
||||
|
||||
## Best-Practices
|
||||
|
||||
1. Use three separate CUDA streams for Download, Upload, and Compute operations
|
||||
2. Use pinned (page-locked) memory on host to speed data bus transfers. Calling `cudaHostAlloc` allocates pinned memory.
|
||||
3. Use in-place NTT to save on device memory.
|
||||
|
||||
## Running the example
|
||||
|
||||
To change the default curve BN254, edit `compile.sh` and `CMakeLists.txt`
|
||||
|
||||
```sh
|
||||
./compile.sh
|
||||
./run.sh
|
||||
```
|
||||
|
||||
To compare with ICICLE baseline (i.e. non-concurrent) NTT, you can run [this example](../ntt/README.md).
|
||||
16
examples/c++/best-practice-ntt/compile.sh
Executable file
16
examples/c++/best-practice-ntt/compile.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit immediately on error
|
||||
set -e
|
||||
|
||||
mkdir -p build/example
|
||||
mkdir -p build/icicle
|
||||
|
||||
# Configure and build Icicle
|
||||
cmake -S ../../../icicle/ -B build/icicle -DCMAKE_BUILD_TYPE=Release -DCURVE=bn254 -DG2=OFF -DMSM=OFF
|
||||
cmake --build build/icicle
|
||||
|
||||
# Configure and build the example application
|
||||
cmake -S . -B build/example
|
||||
cmake --build build/example
|
||||
|
||||
149
examples/c++/best-practice-ntt/example.cu
Normal file
149
examples/c++/best-practice-ntt/example.cu
Normal file
@@ -0,0 +1,149 @@
|
||||
#include <stdio.h>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
|
||||
#include "curves/params/bn254.cuh"
|
||||
#include "api/bn254.h"
|
||||
using namespace bn254;
|
||||
using namespace ntt;
|
||||
|
||||
const std::string curve = "BN254";
|
||||
|
||||
typedef scalar_t S;
|
||||
typedef scalar_t E;
|
||||
|
||||
const unsigned max_log_ntt_size = 27;
|
||||
|
||||
void initialize_input(const unsigned ntt_size, const unsigned nof_ntts, E* elements)
|
||||
{
|
||||
for (unsigned i = 0; i < ntt_size * nof_ntts; i++) {
|
||||
elements[i] = E::from(i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
using FpMilliseconds = std::chrono::duration<float, std::chrono::milliseconds::period>;
|
||||
#define START_TIMER(timer) auto timer##_start = std::chrono::high_resolution_clock::now();
|
||||
#define END_TIMER(timer, msg) \
|
||||
printf("%s: %.0f ms\n", msg, FpMilliseconds(std::chrono::high_resolution_clock::now() - timer##_start).count());
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
cudaDeviceReset();
|
||||
cudaDeviceProp deviceProperties;
|
||||
int deviceId = 0;
|
||||
cudaGetDeviceProperties(&deviceProperties, deviceId);
|
||||
std::string gpu_full_name = deviceProperties.name;
|
||||
std::cout << gpu_full_name << std::endl;
|
||||
std::string gpu_name = gpu_full_name;
|
||||
|
||||
std::cout << "Curve: " << curve << std::endl;
|
||||
|
||||
S basic_root = S::omega(max_log_ntt_size);
|
||||
|
||||
// change these parameters to match the desired NTT size and batch size
|
||||
const unsigned log_ntt_size = 22;
|
||||
const unsigned nof_ntts = 16;
|
||||
|
||||
std::cout << "log NTT size: " << log_ntt_size << std::endl;
|
||||
const unsigned ntt_size = 1 << log_ntt_size;
|
||||
|
||||
std::cout << "Batch size: " << nof_ntts << std::endl;
|
||||
|
||||
// Create separate CUDA streams for overlapping data transfers and kernel execution.
|
||||
cudaStream_t stream_compute, stream_h2d, stream_d2h;
|
||||
cudaStreamCreate(&stream_compute);
|
||||
cudaStreamCreate(&stream_h2d);
|
||||
cudaStreamCreate(&stream_d2h);
|
||||
|
||||
// Create device context for NTT computation
|
||||
auto ctx_compute = device_context::DeviceContext{
|
||||
stream_compute, // stream
|
||||
0, // device_id
|
||||
0, // mempool
|
||||
};
|
||||
|
||||
// Initialize NTT domain and configuration
|
||||
bn254_initialize_domain(&basic_root, ctx_compute, /* fast twiddles */ true);
|
||||
NTTConfig<S> config_compute = default_ntt_config<S>(ctx_compute);
|
||||
config_compute.ntt_algorithm = NttAlgorithm::MixedRadix;
|
||||
config_compute.batch_size = nof_ntts;
|
||||
config_compute.are_inputs_on_device = true;
|
||||
config_compute.are_outputs_on_device = true;
|
||||
config_compute.is_async = true;
|
||||
|
||||
std::cout << "Concurrent Download, Upload, and Compute In-place NTT" << std::endl;
|
||||
int nof_blocks = 32;
|
||||
std::cout << "Number of blocks: " << nof_blocks << std::endl;
|
||||
int block_size = ntt_size * nof_ntts / nof_blocks;
|
||||
|
||||
// on-host pinned data
|
||||
E* h_inp[2];
|
||||
E* h_out[2];
|
||||
for (int i = 0; i < 2; i++) {
|
||||
cudaHostAlloc((void**)&h_inp[i], sizeof(E) * ntt_size * nof_ntts, cudaHostAllocDefault);
|
||||
cudaHostAlloc((void**)&h_out[i], sizeof(E) * ntt_size * nof_ntts, cudaHostAllocDefault);
|
||||
}
|
||||
|
||||
// on-device in-place data
|
||||
// we need two on-device vectors to overlap data transfers with NTT kernel execution
|
||||
E* d_vec[2];
|
||||
for (int i = 0; i < 2; i++) {
|
||||
cudaMalloc((void**)&d_vec[i], sizeof(E) * ntt_size * nof_ntts);
|
||||
}
|
||||
|
||||
// initialize input data
|
||||
initialize_input(ntt_size, nof_ntts, h_inp[0]);
|
||||
initialize_input(ntt_size, nof_ntts, h_inp[1]);
|
||||
|
||||
cudaEvent_t compute_start, compute_stop;
|
||||
cudaEventCreate(&compute_start);
|
||||
cudaEventCreate(&compute_stop);
|
||||
|
||||
for (int run = 0; run < 10; run++) {
|
||||
int vec_compute = run % 2;
|
||||
int vec_transfer = (run + 1) % 2;
|
||||
std::cout << "Run: " << run << std::endl;
|
||||
std::cout << "Compute Vector: " << vec_compute << std::endl;
|
||||
std::cout << "Transfer Vector: " << vec_transfer << std::endl;
|
||||
START_TIMER(inplace);
|
||||
cudaEventRecord(compute_start, stream_compute);
|
||||
bn254_ntt_cuda(d_vec[vec_compute], ntt_size, NTTDir::kForward, config_compute, d_vec[vec_compute]);
|
||||
cudaEventRecord(compute_stop, stream_compute);
|
||||
// we have to delay upload to device relative to download from device by one block: preserve write after read
|
||||
for (int i = 0; i <= nof_blocks; i++) {
|
||||
if (i < nof_blocks) {
|
||||
cudaMemcpyAsync(
|
||||
&h_out[vec_transfer][i * block_size], &d_vec[vec_transfer][i * block_size], sizeof(E) * block_size,
|
||||
cudaMemcpyDeviceToHost, stream_d2h);
|
||||
}
|
||||
if (i > 0) {
|
||||
cudaMemcpyAsync(
|
||||
&d_vec[vec_transfer][(i - 1) * block_size], &h_inp[vec_transfer][(i - 1) * block_size],
|
||||
sizeof(E) * block_size, cudaMemcpyHostToDevice, stream_h2d);
|
||||
}
|
||||
// synchronize upload and download at the end of the block to ensure data integrity
|
||||
cudaStreamSynchronize(stream_d2h);
|
||||
cudaStreamSynchronize(stream_h2d);
|
||||
}
|
||||
// synchronize compute stream with the end of the computation
|
||||
cudaEventSynchronize(compute_stop);
|
||||
float milliseconds = 0;
|
||||
cudaEventElapsedTime(&milliseconds, compute_start, compute_stop);
|
||||
END_TIMER(inplace, "Concurrent In-Place NTT");
|
||||
std::cout << "NTT time: " << milliseconds << " ms" << std::endl;
|
||||
};
|
||||
|
||||
// Clean-up
|
||||
for (int i = 0; i < 2; i++) {
|
||||
cudaFree(d_vec[i]);
|
||||
cudaFreeHost(h_inp[i]);
|
||||
cudaFreeHost(h_out[i]);
|
||||
}
|
||||
cudaEventDestroy(compute_start);
|
||||
cudaEventDestroy(compute_stop);
|
||||
cudaStreamDestroy(stream_compute);
|
||||
cudaStreamDestroy(stream_d2h);
|
||||
cudaStreamDestroy(stream_h2d);
|
||||
return 0;
|
||||
}
|
||||
2
examples/c++/best-practice-ntt/run.sh
Executable file
2
examples/c++/best-practice-ntt/run.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
./build/example/example
|
||||
@@ -8,18 +8,16 @@ if (${CMAKE_VERSION} VERSION_LESS "3.24.0")
|
||||
else()
|
||||
set(CMAKE_CUDA_ARCHITECTURES native) # on 3.24+, on earlier it is ignored, and the target is not passed
|
||||
endif ()
|
||||
project(icicle LANGUAGES CUDA CXX)
|
||||
project(example LANGUAGES CUDA CXX)
|
||||
|
||||
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --expt-relaxed-constexpr")
|
||||
set(CMAKE_CUDA_FLAGS_RELEASE "")
|
||||
set(CMAKE_CUDA_FLAGS_DEBUG "${CMAKE_CUDA_FLAGS_DEBUG} -g -G -O0")
|
||||
# change the path to your Icicle location
|
||||
include_directories("../../../icicle")
|
||||
|
||||
add_executable(
|
||||
example
|
||||
example.cu
|
||||
)
|
||||
|
||||
find_library(NVML_LIBRARY nvidia-ml PATHS /usr/local/cuda-12.0/targets/x86_64-linux/lib/stubs/ )
|
||||
target_link_libraries(example ${NVML_LIBRARY})
|
||||
target_include_directories(example PRIVATE "../../../icicle/include")
|
||||
target_link_libraries(example ${CMAKE_SOURCE_DIR}/build/icicle/lib/libingo_curve_bn254.a)
|
||||
set_target_properties(example PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# Icicle example: Muli-Scalar Multiplication (MSM)
|
||||
|
||||
## Best-Practices
|
||||
|
||||
We recommend to run our examples in [ZK-containers](../../ZK-containers.md) to save your time and mental energy.
|
||||
|
||||
## Key-Takeaway
|
||||
|
||||
`Icicle` provides CUDA C++ template function `MSM` to accelerate [Multi-Scalar Multiplication](https://github.com/ingonyama-zk/ingopedia/blob/master/src/msm.md).
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
# Exit immediately on error
|
||||
set -e
|
||||
|
||||
rm -rf build
|
||||
mkdir -p build
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
mkdir -p build/example
|
||||
mkdir -p build/icicle
|
||||
|
||||
# Configure and build Icicle
|
||||
cmake -S ../../../icicle/ -B build/icicle -DCMAKE_BUILD_TYPE=Release -DCURVE=bn254 -DG2=ON
|
||||
cmake --build build/icicle
|
||||
|
||||
# Configure and build the example application
|
||||
cmake -S . -B build/example
|
||||
cmake --build build/example
|
||||
@@ -2,11 +2,8 @@
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
|
||||
#define G2_DEFINED
|
||||
#define CURVE_ID 1
|
||||
// include MSM template
|
||||
#include "appUtils/msm/msm.cu"
|
||||
using namespace curve_config;
|
||||
#include "api/bn254.h"
|
||||
using namespace bn254;
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
@@ -19,16 +16,15 @@ int main(int argc, char* argv[])
|
||||
int N = batch_size * msm_size;
|
||||
|
||||
std::cout << "Part I: use G1 points" << std::endl;
|
||||
|
||||
|
||||
std::cout << "Generating random inputs on-host" << std::endl;
|
||||
scalar_t* scalars = new scalar_t[N];
|
||||
affine_t* points = new affine_t[N];
|
||||
projective_t result;
|
||||
scalar_t::RandHostMany(scalars, N);
|
||||
projective_t::RandHostManyAffine(points, N);
|
||||
scalar_t::rand_host_many(scalars, N);
|
||||
projective_t::rand_host_many_affine(points, N);
|
||||
|
||||
std::cout << "Using default MSM configuration with on-host inputs" << std::endl;
|
||||
// auto config = msm::DefaultMSMConfig();
|
||||
device_context::DeviceContext ctx = device_context::get_default_device_context();
|
||||
msm::MSMConfig config = {
|
||||
ctx, // ctx
|
||||
@@ -47,30 +43,11 @@ int main(int argc, char* argv[])
|
||||
false, // is_async
|
||||
};
|
||||
config.batch_size = batch_size;
|
||||
|
||||
|
||||
std::cout << "Running MSM kernel with on-host inputs" << std::endl;
|
||||
// Create two events to time the MSM kernel
|
||||
cudaStream_t stream = config.ctx.stream;
|
||||
cudaEvent_t start, stop;
|
||||
float time;
|
||||
cudaEventCreate(&start);
|
||||
cudaEventCreate(&stop);
|
||||
// Record the start event on the stream
|
||||
cudaEventRecord(start, stream);
|
||||
// Execute the MSM kernel
|
||||
msm::MSM<scalar_t, affine_t, projective_t>(scalars, points, msm_size, config, &result);
|
||||
// Record the stop event on the stream
|
||||
cudaEventRecord(stop, stream);
|
||||
// Wait for the stop event to complete
|
||||
cudaEventSynchronize(stop);
|
||||
// Calculate the elapsed time between the start and stop events
|
||||
cudaEventElapsedTime(&time, start, stop);
|
||||
// Destroy the events
|
||||
cudaEventDestroy(start);
|
||||
cudaEventDestroy(stop);
|
||||
// Print the elapsed time
|
||||
std::cout << "Kernel runtime: " << std::fixed << std::setprecision(3) << time * 1e-3 << " sec." << std::endl;
|
||||
// Print the result
|
||||
bn254_msm_cuda(scalars, points, msm_size, config, &result);
|
||||
std::cout << projective_t::to_affine(result) << std::endl;
|
||||
|
||||
std::cout << "Copying inputs on-device" << std::endl;
|
||||
@@ -89,24 +66,9 @@ int main(int argc, char* argv[])
|
||||
config.are_points_on_device = true;
|
||||
|
||||
std::cout << "Running MSM kernel with on-device inputs" << std::endl;
|
||||
// Create two events to time the MSM kernel
|
||||
cudaEventCreate(&start);
|
||||
cudaEventCreate(&stop);
|
||||
// Record the start event on the stream
|
||||
cudaEventRecord(start, stream);
|
||||
// Execute the MSM kernel
|
||||
msm::MSM<scalar_t, affine_t, projective_t>(scalars_d, points_d, msm_size, config, result_d);
|
||||
// Record the stop event on the stream
|
||||
cudaEventRecord(stop, stream);
|
||||
// Wait for the stop event to complete
|
||||
cudaEventSynchronize(stop);
|
||||
// Calculate the elapsed time between the start and stop events
|
||||
cudaEventElapsedTime(&time, start, stop);
|
||||
// Destroy the events
|
||||
cudaEventDestroy(start);
|
||||
cudaEventDestroy(stop);
|
||||
// Print the elapsed time
|
||||
std::cout << "Kernel runtime: " << std::fixed << std::setprecision(3) << time * 1e-3 << " sec." << std::endl;
|
||||
bn254_msm_cuda(scalars_d, points_d, msm_size, config, result_d);
|
||||
|
||||
// Copy the result back to the host
|
||||
cudaMemcpy(&result, result_d, sizeof(projective_t), cudaMemcpyDeviceToHost);
|
||||
// Print the result
|
||||
@@ -123,23 +85,14 @@ int main(int argc, char* argv[])
|
||||
std::cout << "Generating random inputs on-host" << std::endl;
|
||||
// use the same scalars
|
||||
g2_affine_t* g2_points = new g2_affine_t[N];
|
||||
g2_projective_t::RandHostManyAffine(g2_points, N);
|
||||
g2_projective_t::rand_host_many_affine(g2_points, N);
|
||||
|
||||
std::cout << "Reconfiguring MSM to use on-host inputs" << std::endl;
|
||||
config.are_results_on_device = false;
|
||||
config.are_scalars_on_device = false;
|
||||
config.are_points_on_device = false;
|
||||
g2_projective_t g2_result;
|
||||
cudaEventCreate(&start);
|
||||
cudaEventCreate(&stop);
|
||||
cudaEventRecord(start, stream);
|
||||
msm::MSM<scalar_t, g2_affine_t, g2_projective_t>(scalars, g2_points, msm_size, config, &g2_result);
|
||||
cudaEventRecord(stop, stream);
|
||||
cudaEventSynchronize(stop);
|
||||
cudaEventElapsedTime(&time, start, stop);
|
||||
cudaEventDestroy(start);
|
||||
cudaEventDestroy(stop);
|
||||
std::cout << "Kernel runtime: " << std::fixed << std::setprecision(3) << time * 1e-3 << " sec." << std::endl;
|
||||
bn254_g2_msm_cuda(scalars, g2_points, msm_size, config, &g2_result);
|
||||
std::cout << g2_projective_t::to_affine(g2_result) << std::endl;
|
||||
|
||||
std::cout << "Copying inputs on-device" << std::endl;
|
||||
@@ -157,16 +110,7 @@ int main(int argc, char* argv[])
|
||||
config.are_points_on_device = true;
|
||||
|
||||
std::cout << "Running MSM kernel with on-device inputs" << std::endl;
|
||||
cudaEventCreate(&start);
|
||||
cudaEventCreate(&stop);
|
||||
cudaEventRecord(start, stream);
|
||||
msm::MSM<scalar_t, g2_affine_t, g2_projective_t>(scalars_d, g2_points_d, msm_size, config, g2_result_d);
|
||||
cudaEventRecord(stop, stream);
|
||||
cudaEventSynchronize(stop);
|
||||
cudaEventElapsedTime(&time, start, stop);
|
||||
cudaEventDestroy(start);
|
||||
cudaEventDestroy(stop);
|
||||
std::cout << "Kernel runtime: " << std::fixed << std::setprecision(3) << time * 1e-3 << " sec." << std::endl;
|
||||
bn254_g2_msm_cuda(scalars_d, g2_points_d, msm_size, config, g2_result_d);
|
||||
cudaMemcpy(&g2_result, g2_result_d, sizeof(g2_projective_t), cudaMemcpyDeviceToHost);
|
||||
std::cout << g2_projective_t::to_affine(g2_result) << std::endl;
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#!/bin/bash
|
||||
./build/example
|
||||
./build/example/example
|
||||
|
||||
@@ -14,11 +14,13 @@ set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --expt-relaxed-constexpr")
|
||||
set(CMAKE_CUDA_FLAGS_RELEASE "")
|
||||
set(CMAKE_CUDA_FLAGS_DEBUG "${CMAKE_CUDA_FLAGS_DEBUG} -g -G -O0")
|
||||
# change the path to your Icicle location
|
||||
include_directories("../../../icicle")
|
||||
add_executable(
|
||||
example
|
||||
example.cu
|
||||
)
|
||||
target_include_directories(example PRIVATE "../../../icicle/include")
|
||||
target_link_libraries(example ${CMAKE_SOURCE_DIR}/build/icicle/lib/libingo_curve_bn254.a)
|
||||
target_link_libraries(example ${CMAKE_SOURCE_DIR}/build/icicle/lib/libingo_field_bn254.a)
|
||||
find_library(NVML_LIBRARY nvidia-ml PATHS /usr/local/cuda/targets/x86_64-linux/lib/stubs/ )
|
||||
target_link_libraries(example ${NVML_LIBRARY})
|
||||
set_target_properties(example PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
# Exit immediately on error
|
||||
set -e
|
||||
|
||||
rm -rf build
|
||||
mkdir -p build
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
mkdir -p build/example
|
||||
mkdir -p build/icicle
|
||||
|
||||
# Configure and build Icicle
|
||||
cmake -S ../../../icicle/ -B build/icicle -DCMAKE_BUILD_TYPE=Release -DCURVE=bn254
|
||||
cmake --build build/icicle
|
||||
|
||||
# Configure and build the example application
|
||||
cmake -S . -B build/example
|
||||
cmake --build build/example
|
||||
@@ -1,148 +1,152 @@
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
#include <nvml.h>
|
||||
|
||||
// select the curve
|
||||
#define CURVE_ID 2
|
||||
#include "appUtils/poseidon/poseidon.cu"
|
||||
#include "utils/error_handler.cuh"
|
||||
#include "api/bn254.h"
|
||||
#include "gpu-utils/error_handler.cuh"
|
||||
|
||||
#include "poseidon/poseidon.cuh"
|
||||
#include "hash/hash.cuh"
|
||||
|
||||
using namespace poseidon;
|
||||
using namespace curve_config;
|
||||
using namespace bn254;
|
||||
|
||||
void checkCudaError(cudaError_t error) {
|
||||
if (error != cudaSuccess) {
|
||||
std::cerr << "CUDA error: " << cudaGetErrorString(error) << std::endl;
|
||||
// Handle the error, e.g., exit the program or throw an exception.
|
||||
}
|
||||
void checkCudaError(cudaError_t error)
|
||||
{
|
||||
if (error != cudaSuccess) {
|
||||
std::cerr << "CUDA error: " << cudaGetErrorString(error) << std::endl;
|
||||
// Handle the error, e.g., exit the program or throw an exception.
|
||||
}
|
||||
}
|
||||
|
||||
// these global constants go into template calls
|
||||
const int size_col = 11;
|
||||
|
||||
// this function executes the Poseidon thread
|
||||
void threadPoseidon(device_context::DeviceContext ctx, unsigned size_partition, scalar_t * layers, scalar_t * column_hashes, PoseidonConstants<scalar_t> * constants) {
|
||||
cudaError_t err_result = CHK_STICKY(cudaSetDevice(ctx.device_id));
|
||||
if (err_result != cudaSuccess) {
|
||||
std::cerr << "CUDA error: " << cudaGetErrorString(err_result) << std::endl;
|
||||
return;
|
||||
}
|
||||
// CHK_IF_RETURN(); I can't use it in a standard thread function
|
||||
PoseidonConfig column_config = {
|
||||
ctx, // ctx
|
||||
false, // are_inputes_on_device
|
||||
false, // are_outputs_on_device
|
||||
false, // input_is_a_state
|
||||
false, // aligned
|
||||
false, // loop_state
|
||||
false, // is_async
|
||||
};
|
||||
cudaError_t err = poseidon_hash<scalar_t, size_col+1>(layers, column_hashes, (size_t) size_partition, *constants, column_config);
|
||||
checkCudaError(err);
|
||||
void threadPoseidon(
|
||||
device_context::DeviceContext ctx,
|
||||
unsigned size_partition,
|
||||
scalar_t* layers,
|
||||
scalar_t* column_hashes,
|
||||
Poseidon<scalar_t> * poseidon)
|
||||
{
|
||||
cudaError_t err_result = CHK_STICKY(cudaSetDevice(ctx.device_id));
|
||||
if (err_result != cudaSuccess) {
|
||||
std::cerr << "CUDA error: " << cudaGetErrorString(err_result) << std::endl;
|
||||
return;
|
||||
}
|
||||
HashConfig column_config = default_hash_config(ctx);
|
||||
cudaError_t err = poseidon->hash_many(layers, column_hashes, (size_t) size_partition, size_col, 1, column_config);
|
||||
checkCudaError(err);
|
||||
}
|
||||
|
||||
using FpMilliseconds = std::chrono::duration<float, std::chrono::milliseconds::period>;
|
||||
#define START_TIMER(timer) auto timer##_start = std::chrono::high_resolution_clock::now();
|
||||
#define END_TIMER(timer, msg) printf("%s: %.0f ms\n", msg, FpMilliseconds(std::chrono::high_resolution_clock::now() - timer##_start).count());
|
||||
#define END_TIMER(timer, msg) \
|
||||
printf("%s: %.0f ms\n", msg, FpMilliseconds(std::chrono::high_resolution_clock::now() - timer##_start).count());
|
||||
|
||||
#define CHECK_ALLOC(ptr) \
|
||||
if ((ptr) == nullptr) { \
|
||||
std::cerr << "Memory allocation for '" #ptr "' failed." << std::endl; \
|
||||
exit(EXIT_FAILURE); \
|
||||
}
|
||||
|
||||
#define CHECK_ALLOC(ptr) if ((ptr) == nullptr) { \
|
||||
std::cerr << "Memory allocation for '" #ptr "' failed." << std::endl; \
|
||||
exit(EXIT_FAILURE); \
|
||||
}
|
||||
|
||||
int main() {
|
||||
const unsigned size_row = (1<<30);
|
||||
const unsigned nof_partitions = 64;
|
||||
const unsigned size_partition = size_row / nof_partitions;
|
||||
// layers is allocated only for one partition, need to reuse for different partitions
|
||||
const uint32_t size_layers = size_col * size_partition;
|
||||
|
||||
nvmlInit();
|
||||
unsigned int deviceCount;
|
||||
nvmlDeviceGetCount(&deviceCount);
|
||||
std::cout << "Available GPUs: " << deviceCount << std::endl;
|
||||
int main()
|
||||
{
|
||||
const unsigned size_row = (1 << 30);
|
||||
const unsigned nof_partitions = 64;
|
||||
const unsigned size_partition = size_row / nof_partitions;
|
||||
// layers is allocated only for one partition, need to reuse for different partitions
|
||||
const uint32_t size_layers = size_col * size_partition;
|
||||
|
||||
for (unsigned int i = 0; i < deviceCount; ++i) {
|
||||
nvmlDevice_t device;
|
||||
nvmlMemory_t memory;
|
||||
char name[NVML_DEVICE_NAME_BUFFER_SIZE];
|
||||
nvmlDeviceGetHandleByIndex(i, &device);
|
||||
nvmlDeviceGetName(device, name, NVML_DEVICE_NAME_BUFFER_SIZE);
|
||||
nvmlDeviceGetMemoryInfo(device, &memory);
|
||||
std::cout << "Device ID: " << i << ", Type: " << name << ", Memory Total/Free (MiB) " << memory.total/1024/1024 << "/" << memory.free/1024/1024 << std::endl;
|
||||
}
|
||||
nvmlInit();
|
||||
unsigned int deviceCount;
|
||||
nvmlDeviceGetCount(&deviceCount);
|
||||
std::cout << "Available GPUs: " << deviceCount << std::endl;
|
||||
|
||||
const unsigned memory_partition = sizeof(scalar_t)*(size_col+1)*size_partition/1024/1024;
|
||||
std::cout << "Required Memory (MiB) " << memory_partition << std::endl;
|
||||
for (unsigned int i = 0; i < deviceCount; ++i) {
|
||||
nvmlDevice_t device;
|
||||
nvmlMemory_t memory;
|
||||
char name[NVML_DEVICE_NAME_BUFFER_SIZE];
|
||||
nvmlDeviceGetHandleByIndex(i, &device);
|
||||
nvmlDeviceGetName(device, name, NVML_DEVICE_NAME_BUFFER_SIZE);
|
||||
nvmlDeviceGetMemoryInfo(device, &memory);
|
||||
std::cout << "Device ID: " << i << ", Type: " << name << ", Memory Total/Free (MiB) " << memory.total / 1024 / 1024
|
||||
<< "/" << memory.free / 1024 / 1024 << std::endl;
|
||||
}
|
||||
|
||||
//===============================================================================
|
||||
// Key: multiple devices are supported by device context
|
||||
//===============================================================================
|
||||
const unsigned memory_partition = sizeof(scalar_t) * (size_col + 1) * size_partition / 1024 / 1024;
|
||||
std::cout << "Required Memory (MiB) " << memory_partition << std::endl;
|
||||
|
||||
device_context::DeviceContext ctx0 = device_context::get_default_device_context();
|
||||
ctx0.device_id=0;
|
||||
device_context::DeviceContext ctx1 = device_context::get_default_device_context();
|
||||
ctx1.device_id=1;
|
||||
|
||||
std::cout << "Allocate and initialize the memory for layers and hashes" << std::endl;
|
||||
scalar_t* layers0 = static_cast<scalar_t*>(malloc(size_layers * sizeof(scalar_t)));
|
||||
CHECK_ALLOC(layers0);
|
||||
scalar_t s = scalar_t::zero();
|
||||
for (unsigned i = 0; i < size_col*size_partition ; i++) {
|
||||
layers0[i] = s;
|
||||
s = s + scalar_t::one();
|
||||
}
|
||||
scalar_t* layers1 = static_cast<scalar_t*>(malloc(size_layers * sizeof(scalar_t)));
|
||||
CHECK_ALLOC(layers1);
|
||||
s = scalar_t::zero() + scalar_t::one();
|
||||
for (unsigned i = 0; i < size_col*size_partition ; i++) {
|
||||
layers1[i] = s;
|
||||
s = s + scalar_t::one();
|
||||
}
|
||||
//===============================================================================
|
||||
// Key: multiple devices are supported by device context
|
||||
//===============================================================================
|
||||
|
||||
scalar_t* column_hash0 = static_cast<scalar_t*>(malloc(size_partition * sizeof(scalar_t)));
|
||||
CHECK_ALLOC(column_hash0);
|
||||
scalar_t* column_hash1 = static_cast<scalar_t*>(malloc(size_partition * sizeof(scalar_t)));
|
||||
CHECK_ALLOC(column_hash1);
|
||||
device_context::DeviceContext ctx0 = device_context::get_default_device_context();
|
||||
ctx0.device_id = 0;
|
||||
device_context::DeviceContext ctx1 = device_context::get_default_device_context();
|
||||
ctx1.device_id = 1;
|
||||
|
||||
PoseidonConstants<scalar_t> column_constants0, column_constants1;
|
||||
init_optimized_poseidon_constants<scalar_t>(size_col, ctx0, &column_constants0);
|
||||
std::cout << "Allocate and initialize the memory for layers and hashes" << std::endl;
|
||||
scalar_t* layers0 = static_cast<scalar_t*>(malloc(size_layers * sizeof(scalar_t)));
|
||||
CHECK_ALLOC(layers0);
|
||||
scalar_t s = scalar_t::zero();
|
||||
for (unsigned i = 0; i < size_col * size_partition; i++) {
|
||||
layers0[i] = s;
|
||||
s = s + scalar_t::one();
|
||||
}
|
||||
scalar_t* layers1 = static_cast<scalar_t*>(malloc(size_layers * sizeof(scalar_t)));
|
||||
CHECK_ALLOC(layers1);
|
||||
s = scalar_t::zero() + scalar_t::one();
|
||||
for (unsigned i = 0; i < size_col * size_partition; i++) {
|
||||
layers1[i] = s;
|
||||
s = s + scalar_t::one();
|
||||
}
|
||||
|
||||
scalar_t* column_hash0 = static_cast<scalar_t*>(malloc(size_partition * sizeof(scalar_t)));
|
||||
CHECK_ALLOC(column_hash0);
|
||||
scalar_t* column_hash1 = static_cast<scalar_t*>(malloc(size_partition * sizeof(scalar_t)));
|
||||
CHECK_ALLOC(column_hash1);
|
||||
|
||||
Poseidon<scalar_t> column_poseidon0(size_col, ctx0);
|
||||
cudaError_t err_result = CHK_STICKY(cudaSetDevice(ctx1.device_id));
|
||||
if (err_result != cudaSuccess) {
|
||||
std::cerr << "CUDA error: " << cudaGetErrorString(err_result) << std::endl;
|
||||
return;
|
||||
}
|
||||
init_optimized_poseidon_constants<scalar_t>(size_col, ctx1, &column_constants1);
|
||||
Poseidon<scalar_t> column_poseidon1(size_col, ctx1);
|
||||
|
||||
std::cout << "Parallel execution of Poseidon threads" << std::endl;
|
||||
START_TIMER(parallel);
|
||||
std::thread thread0(threadPoseidon, ctx0, size_partition, layers0, column_hash0, &column_constants0);
|
||||
std::thread thread1(threadPoseidon, ctx1, size_partition, layers1, column_hash1, &column_constants1);
|
||||
std::cout << "Parallel execution of Poseidon threads" << std::endl;
|
||||
START_TIMER(parallel);
|
||||
std::thread thread0(threadPoseidon, ctx0, size_partition, layers0, column_hash0, &column_poseidon0);
|
||||
std::thread thread1(threadPoseidon, ctx1, size_partition, layers1, column_hash1, &column_poseidon1);
|
||||
|
||||
// Wait for the threads to finish
|
||||
thread0.join();
|
||||
thread1.join();
|
||||
END_TIMER(parallel,"2 GPUs");
|
||||
std::cout << "Output Data from Thread 0: ";
|
||||
std::cout << column_hash0[0] << std::endl;
|
||||
std::cout << "Output Data from Thread 1: ";
|
||||
std::cout << column_hash1[0] << std::endl;
|
||||
// Wait for the threads to finish
|
||||
thread0.join();
|
||||
thread1.join();
|
||||
END_TIMER(parallel, "2 GPUs");
|
||||
std::cout << "Output Data from Thread 0: ";
|
||||
std::cout << column_hash0[0] << std::endl;
|
||||
std::cout << "Output Data from Thread 1: ";
|
||||
std::cout << column_hash1[0] << std::endl;
|
||||
|
||||
std::cout << "Sequential execution of Poseidon threads" << std::endl;
|
||||
START_TIMER(sequential);
|
||||
std::thread thread2(threadPoseidon, ctx0, size_partition, layers0, column_hash0, &column_constants0);
|
||||
thread2.join();
|
||||
std::thread thread3(threadPoseidon, ctx0, size_partition, layers1, column_hash1, &column_constants0);
|
||||
thread3.join();
|
||||
END_TIMER(sequential,"1 GPU");
|
||||
std::cout << "Output Data from Thread 2: ";
|
||||
std::cout << column_hash0[0] << std::endl;
|
||||
std::cout << "Output Data from Thread 3: ";
|
||||
std::cout << column_hash1[0] << std::endl;
|
||||
std::cout << "Sequential execution of Poseidon threads" << std::endl;
|
||||
START_TIMER(sequential);
|
||||
std::thread thread2(threadPoseidon, ctx0, size_partition, layers0, column_hash0, &column_poseidon0);
|
||||
thread2.join();
|
||||
std::thread thread3(threadPoseidon, ctx0, size_partition, layers1, column_hash1, &column_poseidon0);
|
||||
thread3.join();
|
||||
END_TIMER(sequential, "1 GPU");
|
||||
std::cout << "Output Data from Thread 2: ";
|
||||
std::cout << column_hash0[0] << std::endl;
|
||||
std::cout << "Output Data from Thread 3: ";
|
||||
std::cout << column_hash1[0] << std::endl;
|
||||
|
||||
nvmlShutdown();
|
||||
return 0;
|
||||
nvmlShutdown();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#!/bin/bash
|
||||
./build/example
|
||||
./build/example/example
|
||||
|
||||
@@ -8,17 +8,17 @@ if (${CMAKE_VERSION} VERSION_LESS "3.24.0")
|
||||
else()
|
||||
set(CMAKE_CUDA_ARCHITECTURES native) # on 3.24+, on earlier it is ignored, and the target is not passed
|
||||
endif ()
|
||||
project(icicle LANGUAGES CUDA CXX)
|
||||
project(example LANGUAGES CUDA CXX)
|
||||
|
||||
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --expt-relaxed-constexpr")
|
||||
set(CMAKE_CUDA_FLAGS_RELEASE "")
|
||||
set(CMAKE_CUDA_FLAGS_DEBUG "${CMAKE_CUDA_FLAGS_DEBUG} -g -G -O0")
|
||||
# change the path to your Icicle location
|
||||
include_directories("../../../icicle")
|
||||
add_executable(
|
||||
example
|
||||
example.cu
|
||||
)
|
||||
target_include_directories(example PRIVATE "../../../icicle/include")
|
||||
target_link_libraries(example ${CMAKE_SOURCE_DIR}/build/icicle/lib/libingo_field_bn254.a)
|
||||
find_library(NVML_LIBRARY nvidia-ml PATHS /usr/local/cuda/targets/x86_64-linux/lib/stubs/ )
|
||||
target_link_libraries(example ${NVML_LIBRARY})
|
||||
set_target_properties(example PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# Icicle example: Multiplication
|
||||
|
||||
## Best-Practices
|
||||
|
||||
We recommend to run our examples in [ZK-containers](../../ZK-containers.md) to save your time and mental energy.
|
||||
|
||||
## Key-Takeaway
|
||||
|
||||
`Icicle` accelerates multiplication operation `*` using [Karatsuba algorithm](https://en.wikipedia.org/wiki/Karatsuba_algorithm)
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
# Exit immediately on error
|
||||
set -e
|
||||
|
||||
rm -rf build
|
||||
mkdir -p build
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
mkdir -p build/example
|
||||
mkdir -p build/icicle
|
||||
|
||||
# Configure and build Icicle
|
||||
cmake -S ../../../icicle/ -B build/icicle -DMSM=OFF -DCMAKE_BUILD_TYPE=Release -DCURVE=bn254
|
||||
cmake --build build/icicle
|
||||
|
||||
# Configure and build the example application
|
||||
cmake -S . -B build/example
|
||||
cmake --build build/example
|
||||
@@ -3,22 +3,21 @@
|
||||
#include <chrono>
|
||||
#include <nvml.h>
|
||||
|
||||
#define CURVE_ID 1
|
||||
#include "curves/curve_config.cuh"
|
||||
#include "utils/device_context.cuh"
|
||||
#include "utils/vec_ops.cu"
|
||||
#include "api/bn254.h"
|
||||
#include "vec_ops/vec_ops.cuh"
|
||||
|
||||
using namespace curve_config;
|
||||
using namespace vec_ops;
|
||||
using namespace bn254;
|
||||
|
||||
typedef scalar_t T;
|
||||
|
||||
int vector_mult(T* vec_b, T* vec_a, T* vec_result, size_t n_elments, device_context::DeviceContext ctx)
|
||||
{
|
||||
vec_ops::VecOpsConfig<scalar_t> config = vec_ops::DefaultVecOpsConfig<scalar_t>();
|
||||
vec_ops::VecOpsConfig config = vec_ops::DefaultVecOpsConfig();
|
||||
config.is_a_on_device = true;
|
||||
config.is_b_on_device = true;
|
||||
config.is_result_on_device = true;
|
||||
cudaError_t err = vec_ops::Mul<T>(vec_a, vec_b, n_elments, config, vec_result);
|
||||
cudaError_t err = bn254_mul_cuda(vec_a, vec_b, n_elments, config, vec_result);
|
||||
if (err != cudaSuccess) {
|
||||
std::cerr << "Failed to multiply vectors - " << cudaGetErrorString(err) << std::endl;
|
||||
return 0;
|
||||
@@ -63,8 +62,8 @@ int main(int argc, char** argv)
|
||||
T* host_in1 = (T*)malloc(vector_size * sizeof(T));
|
||||
T* host_in2 = (T*)malloc(vector_size * sizeof(T));
|
||||
std::cout << "Initializing vectors with random data" << std::endl;
|
||||
T::RandHostMany(host_in1, vector_size);
|
||||
T::RandHostMany(host_in2, vector_size);
|
||||
T::rand_host_many(host_in1, vector_size);
|
||||
T::rand_host_many(host_in2, vector_size);
|
||||
// device data
|
||||
device_context::DeviceContext ctx = device_context::get_default_device_context();
|
||||
T* device_in1;
|
||||
@@ -101,7 +100,7 @@ int main(int argc, char** argv)
|
||||
std::cerr << "Failed to copy data from host to device - " << cudaGetErrorString(err) << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
std::cout << "Starting warm-up" << std::endl;
|
||||
// Warm-up loop
|
||||
for (int i = 0; i < repetitions; i++) {
|
||||
@@ -152,7 +151,7 @@ int main(int argc, char** argv)
|
||||
// validate multiplication here...
|
||||
|
||||
// clean up and exit
|
||||
free(host_in1);
|
||||
free(host_in1);
|
||||
free(host_in2);
|
||||
free(host_out);
|
||||
cudaFree(device_in1);
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#!/bin/bash
|
||||
./build/example
|
||||
./build/example/example
|
||||
|
||||
@@ -8,19 +8,16 @@ if (${CMAKE_VERSION} VERSION_LESS "3.24.0")
|
||||
else()
|
||||
set(CMAKE_CUDA_ARCHITECTURES native) # on 3.24+, on earlier it is ignored, and the target is not passed
|
||||
endif ()
|
||||
project(icicle LANGUAGES CUDA CXX)
|
||||
project(example LANGUAGES CUDA CXX)
|
||||
|
||||
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --expt-relaxed-constexpr")
|
||||
set(CMAKE_CUDA_FLAGS_RELEASE "")
|
||||
set(CMAKE_CUDA_FLAGS_DEBUG "${CMAKE_CUDA_FLAGS_DEBUG} -g -G -O0")
|
||||
# change the path to your Icicle location
|
||||
include_directories("../../../icicle")
|
||||
|
||||
add_executable(
|
||||
example
|
||||
example.cu
|
||||
)
|
||||
|
||||
find_library(NVML_LIBRARY nvidia-ml PATHS /usr/local/cuda-12.0/targets/x86_64-linux/lib/stubs/ )
|
||||
target_link_libraries(example ${NVML_LIBRARY})
|
||||
set_target_properties(example PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
|
||||
|
||||
target_include_directories(example PRIVATE "../../../icicle/include")
|
||||
target_link_libraries(example ${CMAKE_SOURCE_DIR}/build/icicle/lib/libingo_field_bn254.a)
|
||||
set_target_properties(example PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
|
||||
@@ -1,9 +1,5 @@
|
||||
# Icicle example: Number-Theoretical Transform (NTT)
|
||||
|
||||
## Best-Practices
|
||||
|
||||
We recommend to run our examples in [ZK-containers](../../ZK-containers.md) to save your time and mental energy.
|
||||
|
||||
## Key-Takeaway
|
||||
|
||||
`Icicle` provides CUDA C++ template function NTT for [Number Theoretical Transform](https://github.com/ingonyama-zk/ingopedia/blob/master/src/fft.md), also known as Discrete Fourier Transform.
|
||||
|
||||
@@ -3,9 +3,13 @@
|
||||
# Exit immediately on error
|
||||
set -e
|
||||
|
||||
rm -rf build
|
||||
mkdir -p build
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
mkdir -p build/example
|
||||
mkdir -p build/icicle
|
||||
|
||||
# Configure and build Icicle
|
||||
cmake -S ../../../icicle/ -B build/icicle -DMSM=OFF -DCMAKE_BUILD_TYPE=Release -DCURVE=bn254
|
||||
cmake --build build/icicle
|
||||
|
||||
# Configure and build the example application
|
||||
cmake -S . -B build/example
|
||||
cmake --build build/example
|
||||
@@ -1,12 +1,11 @@
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
|
||||
// select the curve
|
||||
#define CURVE_ID 1
|
||||
// include NTT template
|
||||
#include "appUtils/ntt/ntt.cu"
|
||||
#include "appUtils/ntt/kernel_ntt.cu"
|
||||
using namespace curve_config;
|
||||
|
||||
#include "curves/params/bn254.cuh"
|
||||
#include "api/bn254.h"
|
||||
using namespace bn254;
|
||||
using namespace ntt;
|
||||
|
||||
// Operate on scalars
|
||||
@@ -61,8 +60,8 @@ int validate_output(const unsigned ntt_size, const unsigned nof_ntts, E* element
|
||||
|
||||
using FpMilliseconds = std::chrono::duration<float, std::chrono::milliseconds::period>;
|
||||
#define START_TIMER(timer) auto timer##_start = std::chrono::high_resolution_clock::now();
|
||||
#define END_TIMER(timer, msg) printf("%s: %.0f ms\n", msg, FpMilliseconds(std::chrono::high_resolution_clock::now() - timer##_start).count());
|
||||
|
||||
#define END_TIMER(timer, msg) \
|
||||
printf("%s: %.0f ms\n", msg, FpMilliseconds(std::chrono::high_resolution_clock::now() - timer##_start).count());
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
@@ -86,22 +85,22 @@ int main(int argc, char* argv[])
|
||||
std::cout << "Running NTT with on-host data" << std::endl;
|
||||
// Create a device context
|
||||
auto ctx = device_context::get_default_device_context();
|
||||
const S basic_root = S::omega(log_ntt_size /*NTT_LOG_SIZE*/);
|
||||
InitDomain(basic_root, ctx);
|
||||
S basic_root = S::omega(log_ntt_size /*NTT_LOG_SIZE*/);
|
||||
bn254_initialize_domain(&basic_root, ctx, true);
|
||||
// Create an NTTConfig instance
|
||||
NTTConfig<S> config = DefaultNTTConfig<S>();
|
||||
config.ntt_algorithm = NttAlgorithm::MixedRadix;
|
||||
NTTConfig<S> config = default_ntt_config<S>();
|
||||
config.ntt_algorithm = NttAlgorithm::MixedRadix;
|
||||
config.batch_size = nof_ntts;
|
||||
START_TIMER(MixedRadix);
|
||||
cudaError_t err = NTT<S, E>(input, ntt_size, NTTDir::kForward, config, output);
|
||||
cudaError_t err = bn254_ntt_cuda(input, ntt_size, NTTDir::kForward, config, output);
|
||||
END_TIMER(MixedRadix, "MixedRadix NTT");
|
||||
|
||||
|
||||
std::cout << "Validating output" << std::endl;
|
||||
validate_output(ntt_size, nof_ntts, output);
|
||||
|
||||
config.ntt_algorithm = NttAlgorithm::Radix2;
|
||||
config.ntt_algorithm = NttAlgorithm::Radix2;
|
||||
START_TIMER(Radix2);
|
||||
err = NTT<S, E>(input, ntt_size, NTTDir::kForward, config, output);
|
||||
err = bn254_ntt_cuda(input, ntt_size, NTTDir::kForward, config, output);
|
||||
END_TIMER(Radix2, "Radix2 NTT");
|
||||
|
||||
std::cout << "Validating output" << std::endl;
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#!/bin/bash
|
||||
./build/example
|
||||
./build/example/example
|
||||
@@ -8,18 +8,19 @@ if (${CMAKE_VERSION} VERSION_LESS "3.24.0")
|
||||
else()
|
||||
set(CMAKE_CUDA_ARCHITECTURES native) # on 3.24+, on earlier it is ignored, and the target is not passed
|
||||
endif ()
|
||||
project(icicle LANGUAGES CUDA CXX)
|
||||
project(example LANGUAGES CUDA CXX)
|
||||
|
||||
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --expt-relaxed-constexpr")
|
||||
set(CMAKE_CUDA_FLAGS_RELEASE "")
|
||||
set(CMAKE_CUDA_FLAGS_DEBUG "${CMAKE_CUDA_FLAGS_DEBUG} -g -G -O0")
|
||||
# change the path to your Icicle location
|
||||
include_directories("../../../icicle")
|
||||
add_executable(
|
||||
example
|
||||
example.cu
|
||||
)
|
||||
|
||||
target_include_directories(example PRIVATE "../../../icicle/include")
|
||||
target_link_libraries(example ${CMAKE_SOURCE_DIR}/build/icicle/lib/libingo_curve_bn254.a)
|
||||
target_link_libraries(example ${CMAKE_SOURCE_DIR}/build/icicle/lib/libingo_field_bn254.a)
|
||||
find_library(NVML_LIBRARY nvidia-ml PATHS /usr/local/cuda/targets/x86_64-linux/lib/stubs/ )
|
||||
target_link_libraries(example ${NVML_LIBRARY})
|
||||
set_target_properties(example PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# ICICLE example: Pedersen Commitment
|
||||
|
||||
## Best-Practices
|
||||
|
||||
We recommend to run our examples in [ZK-containers](../../ZK-containers.md) to save your time and mental energy.
|
||||
|
||||
## Key-Takeaway
|
||||
|
||||
A Pedersen Commitment is a cryptographic primitive to commit to a value or a vector of values while keeping it hidden, yet enabling the committer to reveal the value later. It provides both hiding (the commitment does not reveal any information about the value) and binding properties (once a value is committed, it cannot be changed without detection).
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
# Exit immediately on error
|
||||
set -e
|
||||
|
||||
rm -rf build
|
||||
mkdir -p build
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
mkdir -p build/example
|
||||
mkdir -p build/icicle
|
||||
|
||||
# Configure and build Icicle
|
||||
cmake -S ../../../icicle/ -B build/icicle -DCMAKE_BUILD_TYPE=Release -DCURVE=bn254
|
||||
cmake --build build/icicle
|
||||
|
||||
# Configure and build the example application
|
||||
cmake -S . -B build/example
|
||||
cmake --build build/example
|
||||
@@ -4,56 +4,54 @@
|
||||
#include <cassert>
|
||||
#include <nvml.h>
|
||||
|
||||
#define CURVE_ID BN254
|
||||
#include "appUtils/msm/msm.cu"
|
||||
using namespace curve_config;
|
||||
#include "api/bn254.h"
|
||||
#include "msm/msm.cuh"
|
||||
using namespace bn254;
|
||||
|
||||
typedef point_field_t T;
|
||||
|
||||
// modular power
|
||||
T modPow(T base, T exp) {
|
||||
T modPow(T base, T exp)
|
||||
{
|
||||
T r = T::one();
|
||||
T b = base;
|
||||
T e = exp;
|
||||
while (e != T::zero()) {
|
||||
// If exp is odd, multiply the base with result
|
||||
if (T::is_odd(e)) {
|
||||
r = r * b;
|
||||
}
|
||||
// Now exp must be even, divide it by 2
|
||||
e =T::div2(e);
|
||||
b = b * b;
|
||||
// If exp is odd, multiply the base with result
|
||||
if (T::is_odd(e)) { r = r * b; }
|
||||
// Now exp must be even, divide it by 2
|
||||
e = T::div2(e);
|
||||
b = b * b;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
// Check if y2 is a quadratic residue using Euler's Criterion
|
||||
bool quadratic_residue(T y2) {
|
||||
return modPow(y2, T::div2(T::zero() - T::one())) == T::one();
|
||||
}
|
||||
bool quadratic_residue(T y2) { return modPow(y2, T::div2(T::zero() - T::one())) == T::one(); }
|
||||
|
||||
// modular square root adapted from:
|
||||
// https://github.com/ShahjalalShohag/code-library/blob/main/Number%20Theory/Tonelli%20Shanks%20Algorithm.cpp
|
||||
bool mySQRT(T a, T *result) {
|
||||
bool mySQRT(T a, T* result)
|
||||
{
|
||||
if (a == T::zero()) {
|
||||
*result = T::zero();
|
||||
return true;
|
||||
}
|
||||
if (modPow(a, T::div2(T::zero() - T::one())) != T::one() ) {
|
||||
if (modPow(a, T::div2(T::zero() - T::one())) != T::one()) {
|
||||
return false; // solution does not exist
|
||||
}
|
||||
// TODO: consider special cases
|
||||
// if (p % 4 == 3) return power(a, (p + 1) / 4, p);
|
||||
T s = T::zero() - T::one(); // p - 1,
|
||||
T n = T::one() + T::one(); //2;
|
||||
T r = T::zero();
|
||||
// if (p % 4 == 3) return power(a, (p + 1) / 4, p);
|
||||
T s = T::zero() - T::one(); // p - 1,
|
||||
T n = T::one() + T::one(); // 2;
|
||||
T r = T::zero();
|
||||
T m;
|
||||
while (T::is_even(s)) {
|
||||
r = r + T::one();
|
||||
s = T::div2(s); //s /= 2;
|
||||
s = T::div2(s); // s /= 2;
|
||||
}
|
||||
// find a non-square mod p
|
||||
while (modPow(n, T::div2((T::zero() - T::one())) ) != T::zero() - T::one()) {
|
||||
while (modPow(n, T::div2((T::zero() - T::one()))) != T::zero() - T::one()) {
|
||||
n = n + T::one();
|
||||
}
|
||||
T x = modPow(a, T::div2(s + T::one()));
|
||||
@@ -61,92 +59,95 @@ bool mySQRT(T a, T *result) {
|
||||
T g = modPow(n, s);
|
||||
for (;; r = m) {
|
||||
T t = b;
|
||||
for (m = T::zero(); T::lt(m,r) /* m < r*/ && t != T::one(); m = m + T::one()) t = t * t;
|
||||
if (m == T::zero() ) {
|
||||
for (m = T::zero(); T::lt(m, r) /* m < r*/ && t != T::one(); m = m + T::one())
|
||||
t = t * t;
|
||||
if (m == T::zero()) {
|
||||
*result = x;
|
||||
return true;
|
||||
}
|
||||
T gs = modPow(g, modPow(T::one() + T::one(), r - m - T::one()) );
|
||||
g = gs * gs ;
|
||||
x = x * gs ;
|
||||
b = b * g ;
|
||||
T gs = modPow(g, modPow(T::one() + T::one(), r - m - T::one()));
|
||||
g = gs * gs;
|
||||
x = x * gs;
|
||||
b = b * g;
|
||||
}
|
||||
}
|
||||
|
||||
void point_near_x(T x, affine_t *point) {
|
||||
const T wb = T { weierstrass_b };
|
||||
void point_near_x(T x, affine_t* point)
|
||||
{
|
||||
const T wb = T{weierstrass_b};
|
||||
T y2;
|
||||
while (y2 = x*x*x + wb, quadratic_residue(y2) == false)
|
||||
{
|
||||
while (y2 = x * x * x + wb, quadratic_residue(y2) == false) {
|
||||
x = x + T::one();
|
||||
};
|
||||
T y;
|
||||
bool found = mySQRT(y2, &y);
|
||||
assert(y*y == y2);
|
||||
assert(y * y == y2);
|
||||
point->x = x;
|
||||
point->y = y;
|
||||
}
|
||||
|
||||
static int seed = 0;
|
||||
static HOST_INLINE T rand_host_seed()
|
||||
{
|
||||
std::mt19937_64 generator(seed++);
|
||||
std::uniform_int_distribution<unsigned> distribution;
|
||||
|
||||
T value;
|
||||
for (unsigned i = 0; i < T::TLC-1 ; i++)
|
||||
{
|
||||
std::mt19937_64 generator(seed++);
|
||||
std::uniform_int_distribution<unsigned> distribution;
|
||||
|
||||
T value;
|
||||
for (unsigned i = 0; i < T::TLC - 1; i++)
|
||||
// TODO: use the full range of limbs: for (unsigned i = 0; i < T::TLC ; i++)
|
||||
value.limbs_storage.limbs[i] = distribution(generator);
|
||||
// while (lt(Field{get_modulus()}, value))
|
||||
// value = value - Field{get_modulus()};
|
||||
return value;
|
||||
}
|
||||
value.limbs_storage.limbs[i] = distribution(generator);
|
||||
// while (lt(Field{get_modulus()}, value))
|
||||
// value = value - Field{get_modulus()};
|
||||
return value;
|
||||
}
|
||||
|
||||
using FpMilliseconds = std::chrono::duration<float, std::chrono::milliseconds::period>;
|
||||
#define START_TIMER(timer) auto timer##_start = std::chrono::high_resolution_clock::now();
|
||||
#define END_TIMER(timer, msg) printf("%s: %.0f ms\n", msg, FpMilliseconds(std::chrono::high_resolution_clock::now() - timer##_start).count());
|
||||
#define END_TIMER(timer, msg) \
|
||||
printf("%s: %.0f ms\n", msg, FpMilliseconds(std::chrono::high_resolution_clock::now() - timer##_start).count());
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
const unsigned N = pow(2, 10);
|
||||
std::cout << "Commitment vector size: " << N << "+1 for salt (a.k.a blinding factor)" << std::endl;
|
||||
T* xs = new T[N+1];
|
||||
|
||||
T* xs = new T[N + 1];
|
||||
|
||||
std::cout << "Generating random points transparently using publicly chosen seed" << std::endl;
|
||||
std::cout << "Public seed prevents committer from knowing the discrete logs of points used in the commitment" << std::endl;
|
||||
std::cout << "Public seed prevents committer from knowing the discrete logs of points used in the commitment"
|
||||
<< std::endl;
|
||||
seed = 1234;
|
||||
std::cout << "Using seed: " << seed << std::endl;
|
||||
std::cout << "Generating random field values" << std::endl;
|
||||
START_TIMER(gen);
|
||||
|
||||
|
||||
for (unsigned i = 0; i < N; i++) {
|
||||
xs[i] = rand_host_seed();
|
||||
}
|
||||
END_TIMER(gen, "Time to generate field values");
|
||||
std::cout << "xs[0]: " << xs[0] << std::endl;
|
||||
std::cout << "xs[1]: " << xs[1] << std::endl;
|
||||
|
||||
std::cout << "xs[0]: " << xs[0] << std::endl;
|
||||
std::cout << "xs[1]: " << xs[1] << std::endl;
|
||||
|
||||
// affine_t points[N];
|
||||
affine_t* points = new affine_t[N+1];
|
||||
affine_t* points = new affine_t[N + 1];
|
||||
std::cout << "Generating point about random field values" << std::endl;
|
||||
START_TIMER(points);
|
||||
for (unsigned i = 0; i < N+1; i++) {
|
||||
for (unsigned i = 0; i < N + 1; i++) {
|
||||
point_near_x(xs[i], &points[i]);
|
||||
}
|
||||
END_TIMER(points, "Time to generate points");
|
||||
|
||||
|
||||
std::cout << "Generating commitment vector" << std::endl;
|
||||
projective_t result;
|
||||
scalar_t* scalars = new scalar_t[N+1];
|
||||
scalar_t::RandHostMany(scalars, N);
|
||||
scalar_t* scalars = new scalar_t[N + 1];
|
||||
scalar_t::rand_host_many(scalars, N);
|
||||
|
||||
std::cout << "Generating salt" << std::endl;
|
||||
scalars[N] = scalar_t::rand_host();
|
||||
|
||||
std::cout << "Executing MSM" << std::endl;
|
||||
auto config = msm::DefaultMSMConfig<scalar_t>();
|
||||
auto config = msm::default_msm_config();
|
||||
START_TIMER(msm);
|
||||
msm::MSM<scalar_t, affine_t, projective_t>(scalars, points, N+1, config, &result);
|
||||
bn254_msm_cuda(scalars, points, N + 1, config, &result);
|
||||
END_TIMER(msm, "Time to execute MSM");
|
||||
|
||||
std::cout << "Computed commitment: " << result << std::endl;
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#!/bin/bash
|
||||
./build/example
|
||||
./build/example/example
|
||||
|
||||
30
examples/c++/polynomial-api/CMakeLists.txt
Normal file
30
examples/c++/polynomial-api/CMakeLists.txt
Normal file
@@ -0,0 +1,30 @@
|
||||
cmake_minimum_required(VERSION 3.18)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CUDA_STANDARD 17)
|
||||
set(CMAKE_CUDA_STANDARD_REQUIRED TRUE)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
|
||||
if (${CMAKE_VERSION} VERSION_LESS "3.24.0")
|
||||
set(CMAKE_CUDA_ARCHITECTURES ${CUDA_ARCH})
|
||||
else()
|
||||
set(CMAKE_CUDA_ARCHITECTURES native) # on 3.24+, on earlier it is ignored, and the target is not passed
|
||||
endif ()
|
||||
project(example LANGUAGES CUDA CXX)
|
||||
|
||||
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --expt-relaxed-constexpr -DCURVE_ID=BN254")
|
||||
set(CMAKE_CUDA_FLAGS_RELEASE "")
|
||||
set(CMAKE_CUDA_FLAGS_DEBUG "${CMAKE_CUDA_FLAGS_DEBUG} -g -G -O0")
|
||||
|
||||
add_executable(
|
||||
example
|
||||
example.cu
|
||||
)
|
||||
|
||||
set_target_properties(example PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
|
||||
target_include_directories(example PRIVATE "../../../icicle/include")
|
||||
|
||||
# can link to another curve/field by changing the following lib and FIELD_ID
|
||||
target_link_libraries(example
|
||||
${CMAKE_SOURCE_DIR}/build/icicle/lib/libingo_curve_bn254.a
|
||||
${CMAKE_SOURCE_DIR}/build/icicle/lib/libingo_field_bn254.a
|
||||
)
|
||||
target_compile_definitions(example PUBLIC FIELD_ID BN254)
|
||||
45
examples/c++/polynomial-api/README.md
Normal file
45
examples/c++/polynomial-api/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# ICICLE examples: computations with polynomials
|
||||
|
||||
## Key-Takeaway
|
||||
|
||||
Polynomials are crucial for Zero-Knowledge Proofs (ZKPs): they enable efficient representation and verification of computational statements, facilitate privacy-preserving protocols, and support complex mathematical operations essential for constructing and verifying proofs without revealing underlying data. Polynomial API is documented [here](https://dev.ingonyama.com/icicle/polynomials/overview)
|
||||
|
||||
## Running the example
|
||||
|
||||
To run example, from project root directory:
|
||||
|
||||
```sh
|
||||
cd examples/c++/polynomial-api
|
||||
./compile.sh
|
||||
./run.sh
|
||||
```
|
||||
|
||||
To change the scalar field, modify `compile.h` to build the corresponding lib and `CMakeLists.txt` to link to that lib and set `FIELD_ID` correspondingly.
|
||||
|
||||
## What's in the examples
|
||||
|
||||
- `example_evaluate`: Make polynomial from coefficients and evalue it at random point.
|
||||
|
||||
- `example_clone`: Make a separate copy of a polynomial.
|
||||
|
||||
- `example_from_rou`: Reconstruct polynomial from values at the roots of unity. This operation is a cornerstone in the efficient implementation of zero-knowledge proofs, particularly in the areas of proof construction, verification, and polynomial arithmetic. By leveraging the algebraic structure and computational properties of roots of unity, ZKP protocols can achieve the scalability, efficiency, and privacy necessary for practical applications in blockchain, secure computation, and beyond.
|
||||
|
||||
- `example_addition`, `example_addition_inplace`: Different flavors of polynomial addition.
|
||||
|
||||
- `example_multiplication`: A product of two polynimials
|
||||
|
||||
- `example_multiplicationScalar`: A product of scalar and a polynomial.
|
||||
|
||||
- `example_monomials`: Add/subtract a monomial to a polynom. Monomial is a single term, which is the product of a constant coefficient and a variable raised to a non-negative integer power.
|
||||
|
||||
- `example_ReadCoeffsToHost`: Download coefficients of a polynomial to a host. `ICICLE` keeps all polynomials on GPU, for on-host operation one needs such an operation.
|
||||
|
||||
- `example_divisionSmall`, `example_divisionLarge`: Different flavors of division.
|
||||
|
||||
- `example_divideByVanishingPolynomial`: A vanishing polynomial over a set S is a polynomial that evaluates to zero for every element in S. For a simple case, consider the set S={a}, a single element. The polynomial f(x)=x−a vanishes over S because f(a)=0. Mathematically, dividing a polynomial P(x) by a vanishing polynomial V(x) typically involves finding another polynomial Q(x) and possibly a remainder R(x) such that P(x)=Q(x)V(x)+R(x), where R(x) has a lower degree than V(x). In many cryptographic applications, the focus is on ensuring that P(x) is exactly divisible by V(x), meaning R(x)=0.
|
||||
|
||||
- `example_EvenOdd`: even (odd) methods keep even (odd) coefficients of the original polynomial. For $f(x) = 1+2x+3x^2+4x^3$, even polynomial is $1+3x$, odd polynomial is $2+4x$.
|
||||
|
||||
- `example_Slice`: extends even/odd methods and keeps coefficients for a given offset and stride. For $f(x) = 1+2x+3x^2+4x^3$, origin 0 stride 3 slice gives $1+4x$
|
||||
|
||||
- `example_DeviceMemoryView`: device-memory views of polynomials allow "pass" polynomials to other GPU functions. In this example the coefficients of a polynomial are committed to a Merkle tree bypassing the host.
|
||||
15
examples/c++/polynomial-api/compile.sh
Executable file
15
examples/c++/polynomial-api/compile.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit immediately on error
|
||||
set -e
|
||||
|
||||
mkdir -p build/example
|
||||
mkdir -p build/icicle
|
||||
|
||||
# Configure and build Icicle
|
||||
cmake -S ../../../icicle/ -B build/icicle -DCMAKE_BUILD_TYPE=Release -DCURVE=bn254 -DG2=OFF
|
||||
cmake --build build/icicle
|
||||
|
||||
# Configure and build the example application
|
||||
cmake -S . -B build/example
|
||||
cmake --build build/example
|
||||
448
examples/c++/polynomial-api/example.cu
Normal file
448
examples/c++/polynomial-api/example.cu
Normal file
@@ -0,0 +1,448 @@
|
||||
#include <iostream>
|
||||
#include <cassert>
|
||||
#include "polynomials/polynomials.h"
|
||||
#include "polynomials/cuda_backend/polynomial_cuda_backend.cuh"
|
||||
#include "ntt/ntt.cuh"
|
||||
|
||||
#include "api/bn254.h"
|
||||
#include <chrono>
|
||||
|
||||
using namespace polynomials;
|
||||
using namespace bn254;
|
||||
|
||||
// define the polynomial type
|
||||
typedef Polynomial<scalar_t> Polynomial_t;
|
||||
|
||||
// we'll use the following constants in the examples
|
||||
const auto zero = scalar_t::zero();
|
||||
const auto one = scalar_t::one();
|
||||
const auto two = scalar_t::from(2);
|
||||
const auto three = scalar_t::from(3);
|
||||
const auto four = scalar_t::from(4);
|
||||
const auto five = scalar_t::from(5);
|
||||
const auto minus_one = zero - one;
|
||||
|
||||
static std::unique_ptr<scalar_t[]> generate_pows(scalar_t tau, uint32_t size){
|
||||
auto vec = std::make_unique<scalar_t[]>(size);
|
||||
vec[0] = scalar_t::one();
|
||||
for (size_t i = 1; i < size; ++i) {
|
||||
vec[i] = vec[i-1] * tau;
|
||||
}
|
||||
return std::move(vec);
|
||||
}
|
||||
|
||||
static std::unique_ptr<affine_t[]> generate_SRS(uint32_t size) {
|
||||
auto secret_scalar = scalar_t::rand_host();
|
||||
auto gen = projective_t::generator();
|
||||
auto pows_of_tau = generate_pows(secret_scalar,size);
|
||||
auto SRS = std::make_unique<affine_t[]>(size);
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
SRS[i] = projective_t::to_affine(pows_of_tau[i] * gen);
|
||||
}
|
||||
return std::move(SRS);
|
||||
}
|
||||
|
||||
|
||||
void example_evaluate()
|
||||
{
|
||||
std::cout << std::endl << "Example: Polynomial evaluation on random value" << std::endl;
|
||||
const scalar_t coeffs[3] = {one, two, three};
|
||||
auto f = Polynomial_t::from_coefficients(coeffs, 3);
|
||||
std::cout << "f = " << f << std::endl;
|
||||
scalar_t x = scalar_t::rand_host();
|
||||
std::cout << "x = " << x << std::endl;
|
||||
auto fx = f(x);
|
||||
std::cout << "f(x) = " << fx << std::endl;
|
||||
}
|
||||
|
||||
void example_from_rou(const int size)
|
||||
{
|
||||
std::cout << std::endl << "Example: Reconstruct polynomial from values at roots of unity" << std::endl;
|
||||
const int log_size = (int)ceil(log2(size));
|
||||
const int nof_evals = 1 << log_size;
|
||||
auto coeff = std::make_unique<scalar_t[]>(size);
|
||||
for (int i = 0; i < size; i++)
|
||||
coeff[i] = scalar_t::rand_host();
|
||||
auto f = Polynomial_t::from_coefficients(coeff.get(), size);
|
||||
// rou: root of unity
|
||||
auto omega = scalar_t::omega(log_size);
|
||||
scalar_t evals[nof_evals] = {scalar_t::zero()};
|
||||
auto x = scalar_t::one();
|
||||
for (int i = 0; i < nof_evals; ++i) {
|
||||
evals[i] = f(x);
|
||||
x = x * omega;
|
||||
}
|
||||
// reconstruct f from evaluations
|
||||
auto fr = Polynomial_t::from_rou_evaluations(evals, nof_evals);
|
||||
// check for equality f-fr==0
|
||||
auto h = f - fr;
|
||||
std::cout << "degree of f - fr = " << h.degree() << std::endl;
|
||||
}
|
||||
|
||||
static Polynomial_t randomize_polynomial(uint32_t size)
|
||||
{
|
||||
auto coeff = std::make_unique<scalar_t[]>(size);
|
||||
for (int i = 0; i < size; i++)
|
||||
coeff[i] = scalar_t::rand_host();
|
||||
return Polynomial_t::from_coefficients(coeff.get(), size);
|
||||
}
|
||||
|
||||
static Polynomial_t incremental_values(uint32_t size)
|
||||
{
|
||||
auto coeff = std::make_unique<scalar_t[]>(size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
coeff[i] = i ? coeff[i - 1] + scalar_t::one() : scalar_t::one();
|
||||
}
|
||||
return Polynomial_t::from_coefficients(coeff.get(), size);
|
||||
}
|
||||
|
||||
static bool is_equal(Polynomial_t& lhs, Polynomial_t& rhs)
|
||||
{
|
||||
const int deg_lhs = lhs.degree();
|
||||
const int deg_rhs = rhs.degree();
|
||||
if (deg_lhs != deg_rhs) { return false; }
|
||||
auto lhs_coeffs = std::make_unique<scalar_t[]>(deg_lhs);
|
||||
auto rhs_coeffs = std::make_unique<scalar_t[]>(deg_rhs);
|
||||
lhs.copy_coeffs(lhs_coeffs.get(), 1, deg_lhs - 1);
|
||||
rhs.copy_coeffs(rhs_coeffs.get(), 1, deg_rhs - 1);
|
||||
return memcmp(lhs_coeffs.get(), rhs_coeffs.get(), deg_lhs * sizeof(scalar_t)) == 0;
|
||||
}
|
||||
|
||||
void example_addition(const int size0, const int size1)
|
||||
{
|
||||
std::cout << std::endl << "Example: Polynomial addition" << std::endl;
|
||||
auto f = randomize_polynomial(size0);
|
||||
auto g = randomize_polynomial(size1);
|
||||
auto x = scalar_t::rand_host();
|
||||
auto f_x = f(x);
|
||||
auto g_x = g(x);
|
||||
auto fx_plus_gx = f_x + g_x;
|
||||
auto h = f + g;
|
||||
auto h_x = h(x);
|
||||
std::cout << "evaluate and add: " << fx_plus_gx << std::endl;
|
||||
std::cout << "add and evaluate: " << h_x << std::endl;
|
||||
}
|
||||
|
||||
void example_addition_inplace(const int size0, const int size1)
|
||||
{
|
||||
std::cout << std::endl << "Example: Polynomial inplace addition" << std::endl;
|
||||
auto f = randomize_polynomial(size0);
|
||||
auto g = randomize_polynomial(size1);
|
||||
|
||||
auto x = scalar_t::rand_host();
|
||||
auto f_x = f(x);
|
||||
auto g_x = g(x);
|
||||
auto fx_plus_gx = f_x + g_x;
|
||||
f += g;
|
||||
auto s_x = f(x);
|
||||
std::cout << "evaluate and add: " << fx_plus_gx << std::endl;
|
||||
std::cout << "add and evaluate: " << s_x << std::endl;
|
||||
}
|
||||
|
||||
void example_multiplication(const int log0, const int log1)
|
||||
{
|
||||
std::cout << std::endl << "Example: Polynomial multiplication" << std::endl;
|
||||
const int size0 = 1 << log0, size1 = 1 << log1;
|
||||
auto f = randomize_polynomial(size0);
|
||||
auto g = randomize_polynomial(size1);
|
||||
scalar_t x = scalar_t::rand_host();
|
||||
auto fx = f(x);
|
||||
auto gx = g(x);
|
||||
auto fx_mul_gx = fx * gx;
|
||||
auto m = f * g;
|
||||
auto mx = m(x);
|
||||
std::cout << "evaluate and multiply: " << fx_mul_gx << std::endl;
|
||||
std::cout << "multiply and evaluate: " << mx << std::endl;
|
||||
}
|
||||
|
||||
void example_multiplication_scalar(const int log0)
|
||||
{
|
||||
std::cout << std::endl << "Example: Scalar by Polynomial multiplication" << std::endl;
|
||||
const int size = 1 << log0;
|
||||
auto f = randomize_polynomial(size);
|
||||
auto s = scalar_t::from(2);
|
||||
auto g = s * f;
|
||||
auto x = scalar_t::rand_host();
|
||||
auto fx = f(x);
|
||||
auto fx2 = s * fx;
|
||||
auto gx = g(x);
|
||||
std::cout << "Compare (2*f)(x) and 2*f(x): " << std::endl;
|
||||
std::cout << gx << std::endl;
|
||||
std::cout << fx2 << std::endl;
|
||||
}
|
||||
|
||||
void example_monomials()
|
||||
{
|
||||
std::cout << std::endl << "Example: Monomials" << std::endl;
|
||||
const scalar_t coeffs[3] = {one, zero, two}; // 1+2x^2
|
||||
auto f = Polynomial_t::from_coefficients(coeffs, 3);
|
||||
const auto x = three;
|
||||
auto fx = f(x);
|
||||
f.add_monomial_inplace(three, 1); // add 3x
|
||||
const auto expected_addmonmon_f_x = fx + three * x;
|
||||
const auto addmonom_f_x = f(x);
|
||||
std::cout << "Computed f'(x) = " << addmonom_f_x << std::endl;
|
||||
std::cout << "Expected f'(x) = " << expected_addmonmon_f_x << std::endl;
|
||||
}
|
||||
|
||||
void example_read_coeffs_to_host()
|
||||
{
|
||||
std::cout << std::endl << "Example: Read coefficients to host" << std::endl;
|
||||
const scalar_t coeffs_f[3] = {zero, one, two}; // 0+1x+2x^2
|
||||
auto f = Polynomial_t::from_coefficients(coeffs_f, 3);
|
||||
const scalar_t coeffs_g[3] = {one, one, one}; // 1+x+x^2
|
||||
auto g = Polynomial_t::from_coefficients(coeffs_g, 3);
|
||||
auto h = f + g; // 1+2x+3x^3
|
||||
std::cout << "Get one coefficient of h() at a time: " << std::endl;
|
||||
const auto h0 = h.get_coeff(0);
|
||||
const auto h1 = h.get_coeff(1);
|
||||
const auto h2 = h.get_coeff(2);
|
||||
std::cout << "Coefficients of h: " << std::endl;
|
||||
std::cout << "0:" << h0 << " expected: " << one << std::endl;
|
||||
std::cout << "1:" << h1 << " expected: " << two << std::endl;
|
||||
std::cout << "2:" << h2 << " expected: " << three << std::endl;
|
||||
std::cout << "Get all coefficients of h() at a time: " << std::endl;
|
||||
|
||||
scalar_t h_coeffs[3] = {0};
|
||||
// fetch the coefficients for a given range
|
||||
auto nof_coeffs = h.copy_coeffs(h_coeffs, 0, 2);
|
||||
scalar_t expected_h_coeffs[nof_coeffs] = {one, two, three};
|
||||
for (int i = 0; i < nof_coeffs; ++i) {
|
||||
std::cout << i << ":" << h_coeffs[i] << " expected: " << expected_h_coeffs[i] << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void example_division_small()
|
||||
{
|
||||
std::cout << std::endl << "Example: Polynomial division (small)" << std::endl;
|
||||
const scalar_t coeffs_a[4] = {five, zero, four, three}; // 3x^3+4x^2+5
|
||||
const scalar_t coeffs_b[3] = {minus_one, zero, one}; // x^2-1
|
||||
auto a = Polynomial_t::from_coefficients(coeffs_a, 4);
|
||||
auto b = Polynomial_t::from_coefficients(coeffs_b, 3);
|
||||
auto [q, r] = a.divide(b);
|
||||
scalar_t q_coeffs[2] = {0}; // 3x+4
|
||||
scalar_t r_coeffs[2] = {0}; // 3x+9
|
||||
const auto q_nof_coeffs = q.copy_coeffs(q_coeffs, 0, 1);
|
||||
const auto r_nof_coeffs = r.copy_coeffs(r_coeffs, 0, 1);
|
||||
std::cout << "Quotient: 0:" << q_coeffs[0] << " expected: " << scalar_t::from(4) << std::endl;
|
||||
std::cout << "Quotient: 1:" << q_coeffs[1] << " expected: " << scalar_t::from(3) << std::endl;
|
||||
std::cout << "Reminder: 0:" << r_coeffs[0] << " expected: " << scalar_t::from(9) << std::endl;
|
||||
std::cout << "Reminder: 1:" << r_coeffs[1] << " expected: " << scalar_t::from(3) << std::endl;
|
||||
}
|
||||
|
||||
void example_division_large(const int log0, const int log1)
|
||||
{
|
||||
std::cout << std::endl << "Example: Polynomial division (large)" << std::endl;
|
||||
const int size0 = 1 << log0, size1 = 1 << log1;
|
||||
auto a = randomize_polynomial(size0);
|
||||
auto b = randomize_polynomial(size1);
|
||||
auto [q, r] = a.divide(b);
|
||||
scalar_t x = scalar_t::rand_host();
|
||||
auto ax = a(x);
|
||||
auto bx = b(x);
|
||||
auto qx = q(x);
|
||||
auto rx = r(x);
|
||||
// check if a(x) == b(x)*q(x)+r(x)
|
||||
std::cout << "a(x) == b(x)*q(x)+r(x)" << std::endl;
|
||||
std::cout << "lhs = " << ax << std::endl;
|
||||
std::cout << "rhs = " << bx * qx + rx << std::endl;
|
||||
}
|
||||
|
||||
void example_divide_by_vanishing_polynomial()
|
||||
{
|
||||
std::cout << std::endl << "Example: Polynomial division by vanishing polynomial" << std::endl;
|
||||
const scalar_t coeffs_v[5] = {minus_one, zero, zero, zero, one}; // x^4-1 vanishes on 4th roots of unity
|
||||
auto v = Polynomial_t::from_coefficients(coeffs_v, 5);
|
||||
auto h = incremental_values(1 << 11);
|
||||
auto hv = h * v;
|
||||
auto [h_div, R] = hv.divide(v);
|
||||
std::cout << "h_div == h: " << is_equal(h_div, h) << std::endl;
|
||||
auto h_div_by_vanishing = hv.divide_by_vanishing_polynomial(4);
|
||||
std::cout << "h_div_by_vanishing == h: " << is_equal(h_div_by_vanishing, h) << std::endl;
|
||||
}
|
||||
|
||||
void example_clone(const int log0)
|
||||
{
|
||||
std::cout << std::endl << "Example: clone polynomial" << std::endl;
|
||||
const int size = 1 << log0;
|
||||
auto f = randomize_polynomial(size);
|
||||
const auto x = scalar_t::rand_host();
|
||||
const auto fx = f(x);
|
||||
Polynomial_t g;
|
||||
g = f.clone();
|
||||
g += f;
|
||||
auto h = g.clone();
|
||||
std::cout << "g(x) = " << g(x) << " expected: " << two * fx << std::endl;
|
||||
std::cout << "h(x) = " << h(x) << " expected: " << g(x) << std::endl;
|
||||
}
|
||||
|
||||
void example_even_odd()
|
||||
{
|
||||
std::cout << std::endl << "Example: Split into even and odd powers " << std::endl;
|
||||
const scalar_t coeffs[4] = {one, two, three, four}; // 1+2x+3x^2+4x^3
|
||||
auto f = Polynomial_t::from_coefficients(coeffs, 4);
|
||||
auto f_even = f.even();
|
||||
auto f_odd = f.odd();
|
||||
scalar_t even_coeffs[2] = {0};
|
||||
scalar_t odd_coeffs[2] = {0};
|
||||
const auto even_nof_coeffs = f_even.copy_coeffs(even_coeffs, 0, 1);
|
||||
const auto odd_nof_coeffs = f_odd.copy_coeffs(odd_coeffs, 0, 1);
|
||||
std::cout << "Even: 0:" << even_coeffs[0] << " expected: " << one << std::endl;
|
||||
std::cout << "Even: 1:" << even_coeffs[1] << " expected: " << three << std::endl;
|
||||
std::cout << "Odd: 0:" << odd_coeffs[0] << " expected: " << two << std::endl;
|
||||
std::cout << "Odd: 1:" << odd_coeffs[1] << " expected: " << four << std::endl;
|
||||
}
|
||||
|
||||
void example_slice()
|
||||
{
|
||||
std::cout << std::endl << "Example: Slice polynomial " << std::endl;
|
||||
const scalar_t coeffs[4] = {one, two, three, four}; // 1+2x+3x^2+4x^3
|
||||
auto f = Polynomial_t::from_coefficients(coeffs, 4);
|
||||
auto f_slice = f.slice(0 /*=offset*/, 3 /*= stride*/, 2 /*/= size*/); // 1+4x
|
||||
scalar_t slice_coeffs[2] = {0};
|
||||
const auto slice_nof_coeffs = f_slice.copy_coeffs(slice_coeffs, 0, 1);
|
||||
std::cout << "Slice: 0:" << slice_coeffs[0] << " expected: " << one << std::endl;
|
||||
std::cout << "Slice: 1:" << slice_coeffs[1] << " expected: " << four << std::endl;
|
||||
}
|
||||
|
||||
void example_device_memory_view()
|
||||
{
|
||||
const int log_size = 6;
|
||||
const int size = 1 << log_size;
|
||||
auto f = randomize_polynomial(size);
|
||||
auto [d_coeffs, N, device_id] = f.get_coefficients_view();
|
||||
|
||||
// compute coset evaluations
|
||||
auto coset_evals = std::make_unique<scalar_t[]>(size);
|
||||
auto ntt_config = ntt::default_ntt_config<scalar_t>();
|
||||
ntt_config.are_inputs_on_device = true; // using the device data directly as a view
|
||||
ntt_config.coset_gen = ntt::get_root_of_unity<scalar_t>(size * 2);
|
||||
ntt::ntt(d_coeffs.get(), size, ntt::NTTDir::kForward, ntt_config, coset_evals.get());
|
||||
}
|
||||
|
||||
|
||||
void example_commit_with_device_memory_view()
|
||||
{
|
||||
//declare time vars
|
||||
std::chrono::time_point<std::chrono::high_resolution_clock> start, end;
|
||||
std::chrono::milliseconds duration;
|
||||
|
||||
std::cout << std::endl << "Example: a) commit with Polynomial views [(f1+f2)^2 + (f1-f2)^2 ]_1 = [4 (f1^2+ f_2^2)]_1" << std::endl;
|
||||
std::cout<< "Example: b) commit with Polynomial views [(f1+f2)^2 - (f1-f2)^2 ]_1 = [4 f1 *f_2]_1" << std::endl;
|
||||
int N = 1025;
|
||||
|
||||
//generate group elements string of length N: (1, beta,beta^2....,beta^{N-1}). g
|
||||
std::cout << "Setup: Generating mock SRS" << std::endl;
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
auto SRS = generate_SRS(2*N);
|
||||
//Allocate memory on device (points)
|
||||
affine_t* points_d;
|
||||
cudaMalloc(&points_d, sizeof(affine_t)* 2 * N);
|
||||
// copy SRS to device (could have generated on device, but gives an indicator)
|
||||
cudaMemcpy(points_d, SRS.get(), sizeof(affine_t)* 2 * N, cudaMemcpyHostToDevice);
|
||||
end = std::chrono::high_resolution_clock::now();
|
||||
duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
||||
std::cout << "Setup: SRS of length "<< N << " generated and loaded to device. Took: " << duration.count() << " milliseconds" << std::endl;
|
||||
|
||||
//goal:
|
||||
//test commitment equality [(f1+f2)^2 + (f1-f2)^2 ]_1 = [4 (f1^2+ f_2^2)]_1
|
||||
//test commitment equality [(f1+f2)^2 - (f1-f2)^2 ]_1 = [4 f1 *f_2]_1
|
||||
//note: using polyapi to gen scalars: already on device.
|
||||
std::cout << "Setup: Generating polys (on device) f1,f2 of log degree " << log2(N-1) << std::endl;
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
auto f1 = randomize_polynomial(N);
|
||||
auto f2 = randomize_polynomial(N);
|
||||
end = std::chrono::high_resolution_clock::now();
|
||||
duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
||||
std::cout << "Setup: Gen poly done. Took: " << duration.count() << " milliseconds" << std::endl;
|
||||
|
||||
//deg 2N constraints (f1+f2)^2 + (f1-f2)^2 = 2 (f1^2+ f_2^2)
|
||||
std::cout << "Computing constraints..start "<< std::endl;
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
auto L1 = (f1+f2)*(f1+f2) + (f1-f2)*(f1-f2);
|
||||
auto R1 = scalar_t::from(2) * (f1*f1 + f2*f2);
|
||||
//deg 2N constraints (f1+f2)^2 - (f1-f2)^2 = 4 f1 *f_2
|
||||
auto L2 = (f1+f2)*(f1+f2) - (f1-f2)*(f1-f2);
|
||||
auto R2 = scalar_t::from(4) * f1 * f2;
|
||||
end = std::chrono::high_resolution_clock::now();
|
||||
duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
||||
std::cout << "Computing constraints..done. Took: " << duration.count() << " milliseconds"<< std::endl;
|
||||
|
||||
// extract coeff using coeff view
|
||||
auto [viewL1, sizeL1, device_idL1] = L1.get_coefficients_view();
|
||||
auto [viewL2, sizeL2, device_idL2] = L2.get_coefficients_view();
|
||||
auto [viewR1, sizeR1, device_idR1] = R1.get_coefficients_view();
|
||||
auto [viewR2, sizeR2, device_idR2] = R2.get_coefficients_view();
|
||||
|
||||
std::cout << "Computing Commitments with poly view"<< std::endl;
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
msm::MSMConfig config = msm::default_msm_config();
|
||||
config.are_points_on_device = true;
|
||||
config.are_scalars_on_device = true;
|
||||
|
||||
//host vars (for result)
|
||||
projective_t hL1{}, hL2{}, hR1{}, hR2{};
|
||||
|
||||
//straightforward msm bn254 api: no batching
|
||||
bn254_msm_cuda(viewL1.get(),points_d,N,config,&hL1);
|
||||
bn254_msm_cuda(viewL2.get(),points_d,N,config,&hL2);
|
||||
bn254_msm_cuda(viewR1.get(),points_d,N,config,&hR1);
|
||||
bn254_msm_cuda(viewR2.get(),points_d,N,config,&hR2);
|
||||
|
||||
end = std::chrono::high_resolution_clock::now();
|
||||
duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
||||
std::cout << "Commitments done. Took: " << duration.count() << " milliseconds"<< std::endl;
|
||||
|
||||
//sanity checks
|
||||
auto affL1 = projective_t::to_affine(hL1);
|
||||
auto affR1 = projective_t::to_affine(hR1);
|
||||
|
||||
auto affL2 = projective_t::to_affine(hL2);
|
||||
auto affR2 = projective_t::to_affine(hR2);
|
||||
|
||||
//test commitment equality [(f1+f2)^2 + (f1-f2)^2]_1 = [4 (f_1^2+f_2^2]_1
|
||||
assert(affL1.x==affR1.x && affL1.y==affR1.y);
|
||||
std::cout << "commitment [(f1+f2)^2 + (f1-f2)^2]_1:" << std::endl;
|
||||
std::cout << "[x: " << affL1.x << ", y: " << affL1.y << "]" << std::endl;
|
||||
std::cout << "commitment [[2 (f_1^2+f_2^2]_1:" <<std::endl;
|
||||
std::cout << "[x: " << affR1.x << ", y: " << affR1.y << "]" << std::endl;
|
||||
|
||||
assert(affL2.x==affR2.x && affL2.y==affR2.y);
|
||||
std::cout << "commitment [(f1+f2)^2 - (f1-f2)^2]_1:"<< std::endl;
|
||||
std::cout << "[x: " << affL2.x << ", y: " << affL2.y << "]" << std::endl;
|
||||
std::cout << "commitment [4 f_1*f_2]_1:"<<std::endl;
|
||||
std::cout << "[x: " << affR2.x << ", y: " << affR2.y << "]" << std::endl;
|
||||
}
|
||||
|
||||
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
// Initialize NTT. TODO: can we hide this in the library?
|
||||
static const int MAX_NTT_LOG_SIZE = 24;
|
||||
auto ntt_config = ntt::default_ntt_config<scalar_t>();
|
||||
const scalar_t basic_root = scalar_t::omega(MAX_NTT_LOG_SIZE);
|
||||
ntt::init_domain(basic_root, ntt_config.ctx);
|
||||
|
||||
// Virtual factory design pattern: initializing polynomimals factory for CUDA backend
|
||||
Polynomial_t::initialize(std::make_unique<CUDAPolynomialFactory<>>());
|
||||
|
||||
example_evaluate();
|
||||
example_clone(10);
|
||||
example_from_rou(100);
|
||||
example_addition(12, 17);
|
||||
example_addition_inplace(2, 2);
|
||||
example_multiplication(15, 12);
|
||||
example_multiplication_scalar(15);
|
||||
example_monomials();
|
||||
example_read_coeffs_to_host();
|
||||
example_division_small();
|
||||
example_division_large(12, 2);
|
||||
example_divide_by_vanishing_polynomial();
|
||||
example_even_odd();
|
||||
example_slice();
|
||||
example_device_memory_view();
|
||||
example_commit_with_device_memory_view();
|
||||
|
||||
return 0;
|
||||
}
|
||||
2
examples/c++/polynomial-api/run.sh
Executable file
2
examples/c++/polynomial-api/run.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
./build/example/example
|
||||
@@ -8,7 +8,7 @@ if (${CMAKE_VERSION} VERSION_LESS "3.24.0")
|
||||
else()
|
||||
set(CMAKE_CUDA_ARCHITECTURES native) # on 3.24+, on earlier it is ignored, and the target is not passed
|
||||
endif ()
|
||||
project(icicle LANGUAGES CUDA CXX)
|
||||
project(example LANGUAGES CUDA CXX)
|
||||
|
||||
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --expt-relaxed-constexpr")
|
||||
set(CMAKE_CUDA_FLAGS_RELEASE "")
|
||||
@@ -20,7 +20,8 @@ add_executable(
|
||||
example.cu
|
||||
)
|
||||
|
||||
find_library(NVML_LIBRARY nvidia-ml PATHS /usr/local/cuda-12.0/targets/x86_64-linux/lib/stubs/ )
|
||||
target_include_directories(example PRIVATE "../../../icicle/include")
|
||||
target_link_libraries(example ${CMAKE_SOURCE_DIR}/build/icicle/lib/libingo_field_bn254.a)
|
||||
find_library(NVML_LIBRARY nvidia-ml PATHS /usr/local/cuda/targets/x86_64-linux/lib/stubs/ )
|
||||
target_link_libraries(example ${NVML_LIBRARY})
|
||||
set_target_properties(example PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
|
||||
|
||||
set_target_properties(example PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
|
||||
@@ -3,9 +3,13 @@
|
||||
# Exit immediately on error
|
||||
set -e
|
||||
|
||||
rm -rf build
|
||||
mkdir -p build
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
mkdir -p build/example
|
||||
mkdir -p build/icicle
|
||||
|
||||
# Configure and build Icicle
|
||||
cmake -S ../../../icicle/ -B build/icicle -DMSM=OFF -DCMAKE_BUILD_TYPE=Release -DCURVE=bn254
|
||||
cmake --build build/icicle
|
||||
|
||||
# Configure and build the example application
|
||||
cmake -S . -B build/example
|
||||
cmake --build build/example
|
||||
@@ -1,18 +1,14 @@
|
||||
#define CURVE_ID BLS12_381
|
||||
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
#include "curves/curve_config.cuh"
|
||||
#include "appUtils/ntt/ntt.cu"
|
||||
#include "appUtils/ntt/kernel_ntt.cu"
|
||||
#include "utils/vec_ops.cu"
|
||||
#include "utils/error_handler.cuh"
|
||||
#include <memory>
|
||||
|
||||
typedef curve_config::scalar_t test_scalar;
|
||||
typedef curve_config::scalar_t test_data;
|
||||
#include "api/bn254.h"
|
||||
#include "gpu-utils/error_handler.cuh"
|
||||
|
||||
using namespace bn254;
|
||||
typedef scalar_t test_scalar;
|
||||
typedef scalar_t test_data;
|
||||
|
||||
void random_samples(test_data* res, uint32_t count)
|
||||
{
|
||||
@@ -45,7 +41,7 @@ int main(int argc, char** argv)
|
||||
CHK_IF_RETURN(cudaFree(nullptr)); // init GPU context
|
||||
|
||||
// init domain
|
||||
auto ntt_config = ntt::DefaultNTTConfig<test_scalar>();
|
||||
auto ntt_config = ntt::default_ntt_config<test_scalar>();
|
||||
const bool is_radix2_alg = (argc > 1) ? atoi(argv[1]) : false;
|
||||
ntt_config.ntt_algorithm = is_radix2_alg ? ntt::NttAlgorithm::Radix2 : ntt::NttAlgorithm::MixedRadix;
|
||||
|
||||
@@ -55,8 +51,8 @@ int main(int argc, char** argv)
|
||||
CHK_IF_RETURN(cudaEventCreate(&start));
|
||||
CHK_IF_RETURN(cudaEventCreate(&stop));
|
||||
|
||||
const test_scalar basic_root = test_scalar::omega(NTT_LOG_SIZE);
|
||||
ntt::InitDomain(basic_root, ntt_config.ctx, true /*=fast_twidddles_mode*/);
|
||||
test_scalar basic_root = test_scalar::omega(NTT_LOG_SIZE);
|
||||
bn254_initialize_domain(&basic_root, ntt_config.ctx, true /*=fast_twidddles_mode*/);
|
||||
|
||||
// (1) cpu allocation
|
||||
auto CpuA = std::make_unique<test_data[]>(NTT_SIZE);
|
||||
@@ -79,26 +75,25 @@ int main(int argc, char** argv)
|
||||
ntt_config.are_inputs_on_device = false;
|
||||
ntt_config.are_outputs_on_device = true;
|
||||
ntt_config.ordering = ntt::Ordering::kNM;
|
||||
CHK_IF_RETURN(ntt::NTT(CpuA.get(), NTT_SIZE, ntt::NTTDir::kForward, ntt_config, GpuA));
|
||||
CHK_IF_RETURN(ntt::NTT(CpuB.get(), NTT_SIZE, ntt::NTTDir::kForward, ntt_config, GpuB));
|
||||
CHK_IF_RETURN(bn254_ntt_cuda(CpuA.get(), NTT_SIZE, ntt::NTTDir::kForward, ntt_config, GpuA));
|
||||
CHK_IF_RETURN(bn254_ntt_cuda(CpuB.get(), NTT_SIZE, ntt::NTTDir::kForward, ntt_config, GpuB));
|
||||
|
||||
// (4) multiply A,B
|
||||
CHK_IF_RETURN(cudaMallocAsync(&MulGpu, sizeof(test_data) * NTT_SIZE, ntt_config.ctx.stream));
|
||||
vec_ops::VecOpsConfig<test_data> config{
|
||||
vec_ops::VecOpsConfig config{
|
||||
ntt_config.ctx,
|
||||
true, // is_a_on_device
|
||||
true, // is_b_on_device
|
||||
true, // is_result_on_device
|
||||
false, // is_montgomery
|
||||
false // is_async
|
||||
true, // is_a_on_device
|
||||
true, // is_b_on_device
|
||||
true, // is_result_on_device
|
||||
false // is_async
|
||||
};
|
||||
CHK_IF_RETURN(vec_ops::Mul(GpuA, GpuB, NTT_SIZE, config, MulGpu));
|
||||
CHK_IF_RETURN(bn254_mul_cuda(GpuA, GpuB, NTT_SIZE, config, MulGpu));
|
||||
|
||||
// (5) INTT (in place)
|
||||
ntt_config.are_inputs_on_device = true;
|
||||
ntt_config.are_outputs_on_device = true;
|
||||
ntt_config.ordering = ntt::Ordering::kMN;
|
||||
CHK_IF_RETURN(ntt::NTT(MulGpu, NTT_SIZE, ntt::NTTDir::kInverse, ntt_config, MulGpu));
|
||||
CHK_IF_RETURN(bn254_ntt_cuda(MulGpu, NTT_SIZE, ntt::NTTDir::kInverse, ntt_config, MulGpu));
|
||||
|
||||
CHK_IF_RETURN(cudaFreeAsync(GpuA, ntt_config.ctx.stream));
|
||||
CHK_IF_RETURN(cudaFreeAsync(GpuB, ntt_config.ctx.stream));
|
||||
@@ -117,7 +112,7 @@ int main(int argc, char** argv)
|
||||
benchmark(false); // warmup
|
||||
benchmark(true, 20);
|
||||
|
||||
ntt::ReleaseDomain<test_scalar>(ntt_config.ctx);
|
||||
bn254_release_domain(ntt_config.ctx);
|
||||
CHK_IF_RETURN(cudaStreamSynchronize(ntt_config.ctx.stream));
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
./build/example 1 # radix2
|
||||
./build/example 0 # mixed-radix
|
||||
./build/example/example 1 # radix2
|
||||
./build/example/example 0 # mixed-radix
|
||||
|
||||
@@ -13,13 +13,11 @@ project(icicle LANGUAGES CUDA CXX)
|
||||
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --expt-relaxed-constexpr")
|
||||
set(CMAKE_CUDA_FLAGS_RELEASE "")
|
||||
set(CMAKE_CUDA_FLAGS_DEBUG "${CMAKE_CUDA_FLAGS_DEBUG} -g -G -O0")
|
||||
# change the path to your Icicle location
|
||||
include_directories("../../../icicle")
|
||||
|
||||
add_executable(
|
||||
example
|
||||
example.cu
|
||||
)
|
||||
|
||||
find_library(NVML_LIBRARY nvidia-ml PATHS /usr/local/cuda-12.0/targets/x86_64-linux/lib/stubs/ )
|
||||
target_link_libraries(example ${NVML_LIBRARY})
|
||||
target_include_directories(example PRIVATE "../../../icicle/include")
|
||||
target_link_libraries(example ${CMAKE_SOURCE_DIR}/build/icicle/lib/libingo_field_bn254.a)
|
||||
set_target_properties(example PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
|
||||
@@ -1,9 +1,5 @@
|
||||
# Icicle example: build a Merkle tree using Poseidon hash
|
||||
|
||||
## Best-Practices
|
||||
|
||||
We recommend to run our examples in [ZK-containers](../../ZK-containers.md) to save your time and mental energy.
|
||||
|
||||
## Key-Takeaway
|
||||
|
||||
`Icicle` provides CUDA C++ template `poseidon_hash` to accelerate the popular [Poseidon hash function](https://www.poseidon-hash.info/).
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
# Exit immediately on error
|
||||
set -e
|
||||
|
||||
rm -rf build
|
||||
mkdir -p build
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
mkdir -p build/example
|
||||
mkdir -p build/icicle
|
||||
|
||||
# Configure and build Icicle
|
||||
cmake -S ../../../icicle/ -B build/icicle -DMSM=OFF -DCMAKE_BUILD_TYPE=Release -DCURVE=bn254
|
||||
cmake --build build/icicle
|
||||
|
||||
# Configure and build the example application
|
||||
cmake -S . -B build/example
|
||||
cmake --build build/example
|
||||
@@ -2,27 +2,26 @@
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
// select the curve
|
||||
#define CURVE_ID 2
|
||||
// include Poseidon template
|
||||
#include "appUtils/poseidon/poseidon.cu"
|
||||
#include "api/bn254.h"
|
||||
#include "curves/params/bn254.cuh"
|
||||
#include "poseidon/poseidon.cuh"
|
||||
#include "hash/hash.cuh"
|
||||
using namespace poseidon;
|
||||
using namespace curve_config;
|
||||
using namespace bn254;
|
||||
|
||||
device_context::DeviceContext ctx= device_context::get_default_device_context();
|
||||
device_context::DeviceContext ctx = device_context::get_default_device_context();
|
||||
|
||||
// location of a tree node in the array for a given level and offset
|
||||
inline uint32_t tree_index(uint32_t level, uint32_t offset) { return (1 << level) - 1 + offset; }
|
||||
|
||||
// We assume the tree has leaves already set, compute all other levels
|
||||
void build_tree(
|
||||
const uint32_t tree_height, scalar_t* tree, PoseidonConstants<scalar_t> * constants, PoseidonConfig config)
|
||||
const uint32_t tree_height, scalar_t* tree, Poseidon<scalar_t> &poseidon, HashConfig &config)
|
||||
{
|
||||
for (uint32_t level = tree_height - 1; level > 0; level--) {
|
||||
const uint32_t next_level = level - 1;
|
||||
const uint32_t next_level_width = 1 << next_level;
|
||||
poseidon_hash<scalar_t, 2+1>(
|
||||
&tree[tree_index(level, 0)], &tree[tree_index(next_level, 0)], next_level_width, *constants, config);
|
||||
poseidon.hash_many(&tree[tree_index(level, 0)], &tree[tree_index(next_level, 0)], next_level_width, 2, 1, config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,11 +39,7 @@ uint32_t query_membership(scalar_t query, scalar_t* tree, const uint32_t tree_he
|
||||
}
|
||||
|
||||
void generate_proof(
|
||||
uint32_t position,
|
||||
scalar_t* tree,
|
||||
const uint32_t tree_height,
|
||||
uint32_t* proof_lr,
|
||||
scalar_t* proof_hash)
|
||||
uint32_t position, scalar_t* tree, const uint32_t tree_height, uint32_t* proof_lr, scalar_t* proof_hash)
|
||||
{
|
||||
uint32_t level_index = position;
|
||||
for (uint32_t level = tree_height - 1; level > 0; level--) {
|
||||
@@ -71,8 +66,8 @@ uint32_t validate_proof(
|
||||
const uint32_t tree_height,
|
||||
const uint32_t* proof_lr,
|
||||
const scalar_t* proof_hash,
|
||||
PoseidonConstants<scalar_t> * constants,
|
||||
PoseidonConfig config)
|
||||
Poseidon<scalar_t> &poseidon,
|
||||
HashConfig &config)
|
||||
{
|
||||
scalar_t hashes_in[2], hash_out[1], level_hash;
|
||||
level_hash = hash;
|
||||
@@ -85,7 +80,7 @@ uint32_t validate_proof(
|
||||
hashes_in[1] = level_hash;
|
||||
}
|
||||
// next level hash
|
||||
poseidon_hash<scalar_t, 2+1>(hashes_in, hash_out, 1, *constants, config);
|
||||
poseidon.hash_many(hashes_in, hash_out, 1, 2, 1, config);
|
||||
level_hash = hash_out[0];
|
||||
}
|
||||
return proof_hash[0] == level_hash;
|
||||
@@ -115,16 +110,15 @@ int main(int argc, char* argv[])
|
||||
d = d + scalar_t::one();
|
||||
}
|
||||
std::cout << "Hashing blocks into tree leaves..." << std::endl;
|
||||
PoseidonConstants<scalar_t> constants;
|
||||
init_optimized_poseidon_constants<scalar_t>(data_arity, ctx, &constants);
|
||||
PoseidonConfig config = default_poseidon_config<scalar_t>(data_arity+1);
|
||||
poseidon_hash<curve_config::scalar_t, data_arity+1>(data, &tree[tree_index(leaf_level, 0)], tree_width, constants, config);
|
||||
|
||||
Poseidon<scalar_t> poseidon(data_arity, ctx);
|
||||
HashConfig config = default_hash_config(ctx);
|
||||
poseidon.hash_many(data, &tree[tree_index(leaf_level, 0)], tree_width, data_arity, 1, config);
|
||||
|
||||
std::cout << "3. Building Merkle tree" << std::endl;
|
||||
PoseidonConstants<scalar_t> tree_constants;
|
||||
init_optimized_poseidon_constants<scalar_t>(tree_arity, ctx, &tree_constants);
|
||||
PoseidonConfig tree_config = default_poseidon_config<scalar_t>(tree_arity+1);
|
||||
build_tree(tree_height, tree, &tree_constants, tree_config);
|
||||
Poseidon<scalar_t> tree_poseidon(tree_arity, ctx);
|
||||
HashConfig tree_config = default_hash_config(ctx);
|
||||
build_tree(tree_height, tree, tree_poseidon, tree_config);
|
||||
|
||||
std::cout << "4. Generate membership proof" << std::endl;
|
||||
uint32_t position = tree_width - 1;
|
||||
@@ -139,12 +133,12 @@ int main(int argc, char* argv[])
|
||||
std::cout << "5. Validate the hash membership" << std::endl;
|
||||
uint32_t validated;
|
||||
const scalar_t hash = tree[tree_index(leaf_level, query_position)];
|
||||
validated = validate_proof(hash, tree_height, proof_lr, proof_hash, &tree_constants, tree_config);
|
||||
validated = validate_proof(hash, tree_height, proof_lr, proof_hash, tree_poseidon, tree_config);
|
||||
std::cout << "Validated: " << validated << std::endl;
|
||||
|
||||
std::cout << "6. Tamper the hash" << std::endl;
|
||||
const scalar_t tampered_hash = hash + scalar_t::one();
|
||||
validated = validate_proof(tampered_hash, tree_height, proof_lr, proof_hash, &tree_constants, tree_config);
|
||||
validated = validate_proof(tampered_hash, tree_height, proof_lr, proof_hash, tree_poseidon, tree_config);
|
||||
|
||||
std::cout << "7. Invalidate tamper hash membership" << std::endl;
|
||||
std::cout << "Validated: " << validated << std::endl;
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#!/bin/bash
|
||||
./build/example
|
||||
./build/example/example
|
||||
28
examples/c++/risc0/CMakeLists.txt
Normal file
28
examples/c++/risc0/CMakeLists.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
cmake_minimum_required(VERSION 3.18)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CUDA_STANDARD 17)
|
||||
set(CMAKE_CUDA_STANDARD_REQUIRED TRUE)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
|
||||
if (${CMAKE_VERSION} VERSION_LESS "3.24.0")
|
||||
set(CMAKE_CUDA_ARCHITECTURES ${CUDA_ARCH})
|
||||
else()
|
||||
set(CMAKE_CUDA_ARCHITECTURES native) # on 3.24+, on earlier it is ignored, and the target is not passed
|
||||
endif ()
|
||||
project(example LANGUAGES CUDA CXX)
|
||||
|
||||
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --expt-relaxed-constexpr -DFIELD_ID=1001")
|
||||
# set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --expt-relaxed-constexpr")
|
||||
set(CMAKE_CUDA_FLAGS_RELEASE "")
|
||||
set(CMAKE_CUDA_FLAGS_DEBUG "${CMAKE_CUDA_FLAGS_DEBUG} -g -G -O0")
|
||||
|
||||
add_executable(
|
||||
example
|
||||
example.cu
|
||||
)
|
||||
|
||||
set_target_properties(example PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
|
||||
target_include_directories(example PRIVATE "../../../icicle/include")
|
||||
|
||||
# can link to another curve/field by changing the following lib and FIELD_ID
|
||||
target_link_libraries(example ${CMAKE_SOURCE_DIR}/build/icicle/lib/libingo_field_babybear.a)
|
||||
# target_compile_definitions(example PUBLIC FIELD_ID babybear)
|
||||
44
examples/c++/risc0/README.md
Normal file
44
examples/c++/risc0/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# ICICLE example: RISC0's Fibonacci sequence proof using Polynomial API
|
||||
|
||||
## Why RISC0?
|
||||
|
||||
[RISC0 Protocol](https://www.risczero.com/) creates computational integrity proofs (a.k.a. Zero Knowledge Proofs) for programs executing on RISC-V architecture.
|
||||
The proofs are created for sequences of values in RISC-V registers, called execution traces.
|
||||
This approach is transparent to developers and enables the use of general purpose languages.
|
||||
|
||||
## Best-Practices
|
||||
|
||||
This example builds on [ICICLE Polynomial API](../polynomial-api/README.md) so we recommend to run it first.
|
||||
|
||||
## Key-Takeaway
|
||||
|
||||
RISC0 encodes execution traces into very large polynomials and commits them using Merkle trees.
|
||||
FRI speeds-up validation of such commitments by recursively generating smaller polynomials (and trees) from larger ones.
|
||||
The key enabler for *recursion* is the *redundancy* of polynomial commitments, hence the use of Reed-Solomon codes.
|
||||
|
||||
## Running the example
|
||||
|
||||
To run example, from project root directory:
|
||||
|
||||
```sh
|
||||
cd examples/c++/risc0
|
||||
./compile.sh
|
||||
./run.sh
|
||||
```
|
||||
|
||||
## What's in the example
|
||||
|
||||
The example follows [STARK by Hand](https://dev.risczero.com/proof-system/stark-by-hand), structured in the following Lessons:
|
||||
|
||||
1. The Execution Trace
|
||||
2. Rule checks to validate a computation
|
||||
3. Padding the Trace
|
||||
4. Constructing Trace Polynomials
|
||||
5. ZK Commitments of the Trace Data
|
||||
6. Constraint Polynomials
|
||||
7. Mixing Constraint Polynomials
|
||||
8. The Core of the RISC Zero STARK
|
||||
9. The DEEP Technique
|
||||
10. Mixing (Batching) for FRI
|
||||
11. FRI Protocol (Commit Phase)
|
||||
12. FRI Protocol (Query Phase)
|
||||
15
examples/c++/risc0/compile.sh
Executable file
15
examples/c++/risc0/compile.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit immediately on error
|
||||
set -e
|
||||
|
||||
mkdir -p build/example
|
||||
mkdir -p build/icicle
|
||||
|
||||
# Configure and build Icicle
|
||||
cmake -S ../../../icicle/ -B build/icicle -DCMAKE_BUILD_TYPE=Release -DFIELD=babybear
|
||||
cmake --build build/icicle
|
||||
|
||||
# Configure and build the example application
|
||||
cmake -S . -B build/example
|
||||
cmake --build build/example
|
||||
275
examples/c++/risc0/example.cu
Normal file
275
examples/c++/risc0/example.cu
Normal file
@@ -0,0 +1,275 @@
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <list>
|
||||
|
||||
#include "polynomials/polynomials.h"
|
||||
#include "polynomials/cuda_backend/polynomial_cuda_backend.cuh"
|
||||
#include "ntt/ntt.cuh"
|
||||
|
||||
using namespace polynomials;
|
||||
|
||||
// define the polynomial type
|
||||
typedef Polynomial<scalar_t> Polynomial_t;
|
||||
|
||||
// RISC-V register type
|
||||
typedef int64_t rv_t;
|
||||
|
||||
// Convert RISC-V registers to Finite Fields
|
||||
void to_ff(rv_t* rv, scalar_t* s, size_t n) {
|
||||
for (int i = 0; i < n; ++i) {
|
||||
s[i] = scalar_t::from(rv[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void p_print(Polynomial_t * p, int logn, scalar_t shift, std::string header = "Print Vector") {
|
||||
std::cout << header << std::endl;
|
||||
auto n = 1 << logn;
|
||||
auto omega = scalar_t::omega(logn);
|
||||
auto x = shift;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
std::cout << i << ": " << (*p)(x) << std::endl;
|
||||
x = x*omega;
|
||||
}
|
||||
}
|
||||
|
||||
// value to polynomial
|
||||
Polynomial_t p_value(scalar_t value) {
|
||||
auto p_value = Polynomial_t::from_coefficients(&value , 1);
|
||||
return p_value;
|
||||
}
|
||||
|
||||
Polynomial_t p_rotate(Polynomial_t* p, int logn) {
|
||||
// rotate polynomial coefficients right by one position
|
||||
auto n = 1 << logn;
|
||||
auto evaluations_rou_domain = std::make_unique<scalar_t[]>(n);
|
||||
p->evaluate_on_rou_domain(logn, evaluations_rou_domain.get() );
|
||||
scalar_t tmp = evaluations_rou_domain[n-1];
|
||||
for (int i = n-1; i > 0; --i) {
|
||||
evaluations_rou_domain[i] = evaluations_rou_domain[i-1];
|
||||
}
|
||||
evaluations_rou_domain[0] = tmp;
|
||||
return Polynomial_t::from_rou_evaluations(evaluations_rou_domain.get(), n);
|
||||
}
|
||||
|
||||
// mix polynomials (c.f. mix polynomial evaluations)
|
||||
Polynomial_t p_mix(Polynomial_t* in[], size_t nmix, scalar_t mix_parameter) {
|
||||
scalar_t factor = mix_parameter;
|
||||
Polynomial_t out = in[0]->clone();
|
||||
for (int i = 1; i < nmix; ++i) {
|
||||
out += factor * (*in[i]);
|
||||
factor = factor * mix_parameter;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void solve_linear(scalar_t xa, scalar_t ya, scalar_t xb, scalar_t yb, scalar_t * coeffs) {
|
||||
coeffs[1] = (ya - yb) * scalar_t::inverse(xa - xb);
|
||||
coeffs[0] = ya - coeffs[1] * xa;
|
||||
}
|
||||
|
||||
std::unique_ptr<scalar_t[]> InterpolateOnLargerDomain(Polynomial_t * p, int n, scalar_t shift = scalar_t::one()) {
|
||||
const int deg = p->degree();
|
||||
auto input = std::make_unique<scalar_t[]>(n);
|
||||
// TBD: check if scalar_t constructor initializes to zero
|
||||
for (int i = 0; i < n; ++i) {
|
||||
input[i] = scalar_t::zero();
|
||||
}
|
||||
p->copy_coeffs(input.get(), 0/*start*/, deg);
|
||||
auto ntt_config = ntt::default_ntt_config<scalar_t>();
|
||||
ntt_config.coset_gen = shift;
|
||||
auto evals_h = std::make_unique<scalar_t[]>(n);
|
||||
auto err = ntt::ntt(input.get(), n, ntt::NTTDir::kForward, ntt_config, evals_h.get());
|
||||
return evals_h;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
std::cout << "This is an ICICLE C++ implementation of the STARK by Hand Explainer." << std::endl;
|
||||
std::cout << "https://dev.risczero.com/proof-system/stark-by-hand" << std::endl;
|
||||
|
||||
const int logn=3;
|
||||
const int n = 1 << logn;
|
||||
|
||||
std::cout << "Initializing NTT" << std::endl;
|
||||
static const int MAX_NTT_LOG_SIZE = 24;
|
||||
auto ntt_config = ntt::default_ntt_config<scalar_t>();
|
||||
const scalar_t basic_root = scalar_t::omega(MAX_NTT_LOG_SIZE);
|
||||
ntt::init_domain(basic_root, ntt_config.ctx);
|
||||
std::cout << "Initializing Polynomials" << std::endl;
|
||||
// Virtual factory design pattern: initializing polynomimals factory for CUDA backend
|
||||
Polynomial_t::initialize(std::make_unique<CUDAPolynomialFactory<>>());
|
||||
|
||||
std::cout << std::endl << "Lesson 1: The Execution Trace" << std::endl;
|
||||
// Trace: Data Columns
|
||||
rv_t rv_d1_trace[] = {24, 30, 54, 84, 78, 15, 29, 50};
|
||||
rv_t rv_d2_trace[] = {30, 54, 84, 138, 2, 77, 21, 36};
|
||||
rv_t rv_d3_trace[] = {54, 84, 138, 222, 71, 17, 92, 33};
|
||||
auto d1_trace = std::make_unique<scalar_t[]>(n);
|
||||
auto d2_trace = std::make_unique<scalar_t[]>(n);
|
||||
auto d3_trace = std::make_unique<scalar_t[]>(n);
|
||||
to_ff(rv_d1_trace, d1_trace.get(), n);
|
||||
to_ff(rv_d2_trace, d2_trace.get(), n);
|
||||
to_ff(rv_d3_trace, d3_trace.get(), n);
|
||||
// Trace: Control Columns
|
||||
// Init steps are flagged in c1_trace
|
||||
// Computation steps are flagged in c2_trace
|
||||
// Termination step is flagged in c3_trace
|
||||
// 0s at the end of each control column correspond to the padding of the trace
|
||||
rv_t rv_c1_trace[] = {1, 0, 0, 0, 0, 0, 0, 0};
|
||||
rv_t rv_c2_trace[] = {0, 1, 1, 1, 0, 0, 0, 0};
|
||||
rv_t rv_c3_trace[] = {0, 0, 0, 1, 0, 0, 0, 0};
|
||||
auto c1_trace = std::make_unique<scalar_t[]>(n);
|
||||
auto c2_trace = std::make_unique<scalar_t[]>(n);
|
||||
auto c3_trace = std::make_unique<scalar_t[]>(n);
|
||||
to_ff(rv_c1_trace, c1_trace.get(), n);
|
||||
to_ff(rv_c2_trace, c2_trace.get(), n);
|
||||
to_ff(rv_c3_trace, c3_trace.get(), n);
|
||||
|
||||
std::cout << "Lesson 2: Rule checks to validate a computation" << std::endl;
|
||||
std::cout << "We use rule-checking polynomials." << std::endl;
|
||||
|
||||
std::cout << "Lesson 3: Padding the Trace" << std::endl;
|
||||
// The trace is padded to a power of 2 size to allow for efficient NTT operations.
|
||||
// we already did this in the initialization of the trace data
|
||||
// We will construct a zero-knowledge proof that:
|
||||
// this trace represents a program that satisfies these 6 rules:
|
||||
// 1) Fibonacci words here
|
||||
// 2) d1_trace[0] == 24 (init 1 constraint)
|
||||
// 3) d2_trace[0] == 30 (init 2 constraint)
|
||||
// 4) d3_trace[3] == 28 (termination constraint)
|
||||
// 5) if c2_trace[i] == 1, then d2_trace[i] == d1_trace[i+1]
|
||||
// 6) if c2_trace[i] == 1, then d3_trace[i] == d2_trace[i+1}
|
||||
|
||||
std::cout << "Lesson 4: Constructing Trace Polynomials" << std::endl;
|
||||
auto p_d1 = Polynomial_t::from_rou_evaluations(d1_trace.get(), n);
|
||||
auto p_d2 = Polynomial_t::from_rou_evaluations(d2_trace.get(), n);
|
||||
auto p_d3 = Polynomial_t::from_rou_evaluations(d3_trace.get(), n);
|
||||
auto p_c1 = Polynomial_t::from_rou_evaluations(c1_trace.get(), n);
|
||||
auto p_c2 = Polynomial_t::from_rou_evaluations(c2_trace.get(), n);
|
||||
auto p_c3 = Polynomial_t::from_rou_evaluations(c3_trace.get(), n);
|
||||
|
||||
std::cout << "Lesson 5: ZK Commitments of the Trace Data" << std::endl;
|
||||
std::cout << "To maintain a zk protocol, the trace polynomials are evaluated over a zk commitment domain" << std::endl;
|
||||
std::cout << "zk commitment domain is a coset of Reed Solomon domain shifted by a basic root of unity" << std::endl;
|
||||
scalar_t xzk = basic_root;
|
||||
p_print(&p_d1, logn, xzk, "ZK commitment for d1 polynomial");
|
||||
std::cout << "Build Merkle Tree for ZK commitments (outside the scope of this example)" << std::endl;
|
||||
|
||||
std::cout << "Lesson 6: Constraint Polynomials" << std::endl;
|
||||
std::cout << "The constraints are used to check the correctness of the trace. In this example, we check 6 rules to establish the validity of the trace." << std::endl;
|
||||
auto p_fib_constraint = (p_d3 - p_d2 - p_d1) * (p_c1 + p_c2 + p_c3);
|
||||
auto fib_constraint_zkcommitment = InterpolateOnLargerDomain(&p_fib_constraint, 4*n, xzk);
|
||||
|
||||
auto p_init1_constraint = (p_d1 - p_value(scalar_t::from(24))) * p_c1;
|
||||
// sanity checks printing
|
||||
p_print(&p_init1_constraint, logn+2, scalar_t::one(), "Reed-Solomon constraint polynomial gives 0s in every 4th row");
|
||||
p_print(&p_init1_constraint, logn+2, xzk, "ZK Commitment constraint polynomial gives no 0s");
|
||||
auto p_init2_constraint = (p_d2 - p_value(scalar_t::from(30))) * p_c1;
|
||||
auto p_termination_constraint = (p_d3 - p_value(scalar_t::from(222))) * p_c3;
|
||||
auto p_recursion_constraint1 = (p_d1 - p_rotate(&p_d2, logn)) * p_c2;
|
||||
auto p_recursion_constraint2 = (p_d2 - p_rotate(&p_d3, logn)) * p_c2;
|
||||
|
||||
std::cout << std::endl << "Lesson 7: Mixing Constraint Polynomials" << std::endl;
|
||||
Polynomial_t * p_all_constraints[] = {&p_fib_constraint, &p_init1_constraint, &p_init2_constraint, &p_termination_constraint, &p_recursion_constraint1, &p_recursion_constraint2};
|
||||
const size_t nmix = sizeof(p_all_constraints) / sizeof(p_all_constraints[0]);
|
||||
auto p_mixed_constraints = p_mix(p_all_constraints, nmix, scalar_t::from(5));
|
||||
std::cout << "All constraint polynomials are low-degree:" << std::endl;
|
||||
for( int i = 0; i < nmix; ++i) {
|
||||
std::cout << i << ": " << p_all_constraints[i]->degree() << std::endl;
|
||||
}
|
||||
|
||||
std::cout << "Lesson 8: The Core of the RISC Zero STARK" << std::endl;
|
||||
std::cout << "Degree of the mixed constraints polynomial: " << p_mixed_constraints.degree() << std::endl;
|
||||
auto p_validity = p_mixed_constraints.divide_by_vanishing_polynomial(n);
|
||||
std::cout << "Degree of the validity polynomial: " << p_validity.degree() << std::endl;
|
||||
std::cout << "The Verifier should provide the Merke commitment for the above" << std::endl;
|
||||
|
||||
std::cout << "Lesson 9: The DEEP Technique" << std::endl;
|
||||
std::cout << "The DEEP technique improves the security of a single query by sampling outside of the commitment domain." << std::endl;
|
||||
// In the original STARK protocol, the Verifier tests validity polynomial at a number of test points;
|
||||
// the soundness of the protocol depends on the number of tests.
|
||||
// The DEEP-ALI technique allows us to achieve a high degree of soundness with a single test.
|
||||
// The details of DEEP are described in the following lesson.
|
||||
|
||||
auto DEEP_point = scalar_t::from(93);
|
||||
std::cout << "The prover convinces the verifier that V=C/Z at the DEEP_test_point, " << DEEP_point << std::endl;
|
||||
const scalar_t coeffs1[2] = {scalar_t::zero()-DEEP_point, scalar_t::one()};
|
||||
auto denom_DEEP1 = Polynomial_t::from_coefficients(coeffs1, 2);
|
||||
auto [p_d1_DEEP, r] = (p_d1 - p_value(DEEP_point)).divide(denom_DEEP1);
|
||||
std::cout << "The DEEP d1 degree is: " << p_d1_DEEP.degree() << std::endl;
|
||||
// d2, d3 use recursion constraints and need the point corresponding to the previous state (clock cycle)
|
||||
auto omega = scalar_t::omega(logn);
|
||||
auto DEEP_prev_point = DEEP_point*scalar_t::inverse(omega);
|
||||
auto coeffs2 = std::make_unique<scalar_t[]>(2);
|
||||
coeffs2[0] = scalar_t::zero() - DEEP_prev_point;
|
||||
coeffs2[1] = scalar_t::one();
|
||||
auto denom_DEEP2 = Polynomial_t::from_coefficients(coeffs2.get(), 2);
|
||||
|
||||
auto coeffs_d2bar = std::make_unique<scalar_t[]>(2);
|
||||
solve_linear(DEEP_point, p_d2(DEEP_point), DEEP_prev_point, p_d2(DEEP_prev_point), coeffs_d2bar.get());
|
||||
auto d2bar = Polynomial_t::from_coefficients(coeffs_d2bar.get(), 2);
|
||||
auto [p_d2_DEEP, r2] = (p_d2 - d2bar).divide(denom_DEEP1*denom_DEEP2);
|
||||
std::cout << "The DEEP d2 degree is: " << p_d2_DEEP.degree() << std::endl;
|
||||
|
||||
auto coeffs_d3bar = std::make_unique<scalar_t[]>(2);
|
||||
solve_linear(DEEP_point, p_d3(DEEP_point), DEEP_prev_point, p_d3(DEEP_prev_point), coeffs_d3bar.get());
|
||||
auto d3bar = Polynomial_t::from_coefficients(coeffs_d3bar.get(), 2);
|
||||
auto [p_d3_DEEP, r3] = (p_d3 - d3bar).divide(denom_DEEP1*denom_DEEP2);
|
||||
std::cout << "The DEEP d3 degree is: " << p_d3_DEEP.degree() << std::endl;
|
||||
|
||||
// DEEP c{1,2,3} polynomials
|
||||
const scalar_t coeffs_c1bar[1] = {p_c1(DEEP_point)};
|
||||
auto c1bar = Polynomial_t::from_coefficients(coeffs_c1bar, 1);
|
||||
auto [p_c1_DEEP, r_c1] = (p_c1 - c1bar).divide(denom_DEEP1);
|
||||
std::cout << "The DEEP c1 degree is: " << p_c1_DEEP.degree() << std::endl;
|
||||
const scalar_t coeffs_c2bar[1] = {p_c2(DEEP_point)};
|
||||
auto c2bar = Polynomial_t::from_coefficients(coeffs_c2bar, 1);
|
||||
auto [p_c2_DEEP, r_c2] = (p_c2 - c2bar).divide(denom_DEEP1);
|
||||
std::cout << "The DEEP c2 degree is: " << p_c2_DEEP.degree() << std::endl;
|
||||
const scalar_t coeffs_c3bar[1] = {p_c3(DEEP_point)};
|
||||
auto c3bar = Polynomial_t::from_coefficients(coeffs_c3bar, 1);
|
||||
auto [p_c3_DEEP, r_c3] = (p_c3 - c3bar).divide(denom_DEEP1);
|
||||
std::cout << "The DEEP c3 degree is: " << p_c3_DEEP.degree() << std::endl;
|
||||
// DEEP validity polynomial
|
||||
const scalar_t coeffs_vbar[1] = {p_validity(DEEP_point)};
|
||||
auto vbar = Polynomial_t::from_coefficients(coeffs_vbar, 1);
|
||||
auto [v_DEEP, r_v] = (p_validity - vbar).divide(denom_DEEP1);
|
||||
std::cout << "The DEEP validity polynomial degree is: " << v_DEEP.degree() << std::endl;
|
||||
std::cout << "The Prover sends DEEP polynomials to the Verifier" << std::endl;
|
||||
|
||||
std::cout << "Lesson 10: Mixing (Batching) for FRI" << std::endl;
|
||||
std::cout << "The initial FRI polynomial is the mix of the 7 DEEP polynomials." << std::endl;
|
||||
Polynomial_t* all_DEEP[] = {&p_d1_DEEP, &p_d2_DEEP, &p_d3_DEEP, &p_c1_DEEP, &p_c2_DEEP, &p_c3_DEEP, &v_DEEP};
|
||||
Polynomial_t fri_input = p_mix(all_DEEP, 7, scalar_t::from(99));
|
||||
std::cout << "The degree of the mixed DEEP polynomial is: " << fri_input.degree() << std::endl;
|
||||
|
||||
std::cout << "Lesson 11: FRI Protocol (Commit Phase)" << std::endl;
|
||||
std::cout << "The prover provides information to convince the verifier that the DEEP polynomials are low-degree." << std::endl;
|
||||
int nof_rounds = 3;
|
||||
Polynomial_t feven[nof_rounds], fodd[nof_rounds], fri[nof_rounds+1];
|
||||
scalar_t rfri[nof_rounds];
|
||||
fri[0] = fri_input.clone();
|
||||
for (int i = 0; i < nof_rounds; ++i) {
|
||||
feven[i] = fri[i].even();
|
||||
fodd[i] = fri[i].odd();
|
||||
rfri[i] = scalar_t::rand_host();
|
||||
fri[i+1] = feven[i] + rfri[i]*fodd[i];
|
||||
std::cout << "The degree of the Round " << i << " polynomial is: " << fri[i+1].degree() << std::endl;
|
||||
}
|
||||
|
||||
std::cout << "Lesson 12: FRI Protocol (Query Phase)" << std::endl;
|
||||
// We use Polynomial API to evaluate the FRI polynomials
|
||||
// In practice, verifier will use Merkle commitments
|
||||
auto xp = scalar_t::rand_host();
|
||||
auto xm = scalar_t::zero() - xp;
|
||||
scalar_t lhs[nof_rounds], rhs[nof_rounds];
|
||||
for (int i = 0; i < nof_rounds; ++i) {
|
||||
rhs[i] = (rfri[i]+xp)*fri[i](xp)*scalar_t::inverse(scalar_t::from(2)*xp) + (rfri[i]+xm)*fri[i](xm)*scalar_t::inverse(scalar_t::from(2)*xm);
|
||||
lhs[i] = fri[i+1](xp*xp);
|
||||
std::cout << "Round " << i << std::endl << "rhs: " << rhs[i] << std::endl << "lhs: " << lhs[i] << std::endl;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
2
examples/c++/risc0/run.sh
Executable file
2
examples/c++/risc0/run.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
./build/example/example
|
||||
34
examples/golang/msm/README.md
Normal file
34
examples/golang/msm/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# ICICLE example: MultiScalar Multiplication (MSM) in Golang
|
||||
|
||||
`ICICLE` provides Golang bindings to CUDA-accelerated C++ implementation of [Multi-Scalar Multiplication](https://github.com/ingonyama-zk/ingopedia/blob/master/src/msm.md).
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
err := Msm(
|
||||
/* Scalars input vector */ scalars,
|
||||
/* Points input vector */ points,
|
||||
/* MSMConfig reference */ &cfg,
|
||||
/* Projective point result */ results)
|
||||
```
|
||||
|
||||
In this example we use `BN254` and `BLS12377` curves. The function computes $result = \sum_{i=0}^{size-1} scalars[i] \cdot points[i]$, where input `points[]` uses affine coordinates, and `result` uses projective coordinates.
|
||||
|
||||
## What's in the example
|
||||
|
||||
1. Define the size of MSM.
|
||||
2. Generate random inputs on-device
|
||||
3. Configure MSM
|
||||
4. Execute MSM on-device
|
||||
5. Move the result on host
|
||||
|
||||
Running the example:
|
||||
```sh
|
||||
go run main.go
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The default sizes are 2^17 - 2^22. You can change this by passing the `-l <size> -u <size>` options. To change the size range to 2^21 - 2^24, run the example like this:
|
||||
> ```sh
|
||||
> go run main.go -l=21 -u=24
|
||||
> ```
|
||||
209
examples/golang/msm/main.go
Normal file
209
examples/golang/msm/main.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ingonyama-zk/icicle/v2/wrappers/golang/core"
|
||||
cr "github.com/ingonyama-zk/icicle/v2/wrappers/golang/cuda_runtime"
|
||||
|
||||
"github.com/ingonyama-zk/icicle/v2/wrappers/golang/curves/bls12377"
|
||||
|
||||
bls12377G2 "github.com/ingonyama-zk/icicle/v2/wrappers/golang/curves/bls12377/g2"
|
||||
bls12377Msm "github.com/ingonyama-zk/icicle/v2/wrappers/golang/curves/bls12377/msm"
|
||||
"github.com/ingonyama-zk/icicle/v2/wrappers/golang/curves/bn254"
|
||||
|
||||
bn254G2 "github.com/ingonyama-zk/icicle/v2/wrappers/golang/curves/bn254/g2"
|
||||
bn254Msm "github.com/ingonyama-zk/icicle/v2/wrappers/golang/curves/bn254/msm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var logSizeMin int
|
||||
var logSizeMax int
|
||||
|
||||
flag.IntVar(&logSizeMin, "l", 17, "Minimum log size")
|
||||
flag.IntVar(&logSizeMax, "u", 22, "Maximum log size")
|
||||
flag.Parse()
|
||||
|
||||
sizeMax := 1 << logSizeMax
|
||||
|
||||
print("Generating BN254 scalars ... ")
|
||||
startTime := time.Now()
|
||||
scalarsBn254Max := bn254.GenerateScalars(sizeMax)
|
||||
println(time.Since(startTime).String())
|
||||
|
||||
print("Generating BN254 points ... ")
|
||||
startTime = time.Now()
|
||||
pointsBn254Max := bn254.GenerateAffinePoints(sizeMax)
|
||||
println(time.Since(startTime).String())
|
||||
|
||||
print("Generating BN254 G2 points ... ")
|
||||
startTime = time.Now()
|
||||
pointsBn254G2Max := bn254G2.G2GenerateAffinePoints(sizeMax)
|
||||
println(time.Since(startTime).String())
|
||||
|
||||
print("Generating BLS12_377 scalars ... ")
|
||||
startTime = time.Now()
|
||||
scalarsBls12377Max := bls12377.GenerateScalars(sizeMax)
|
||||
println(time.Since(startTime).String())
|
||||
|
||||
print("Generating BLS12_377 points ... ")
|
||||
startTime = time.Now()
|
||||
pointsBls12377Max := bls12377.GenerateAffinePoints(sizeMax)
|
||||
println(time.Since(startTime).String())
|
||||
|
||||
print("Generating BLS12_377 G2 points ... ")
|
||||
startTime = time.Now()
|
||||
pointsBls12377G2Max := bls12377G2.G2GenerateAffinePoints(sizeMax)
|
||||
println(time.Since(startTime).String())
|
||||
|
||||
for logSize := logSizeMin; logSize <= logSizeMax; logSize++ {
|
||||
|
||||
// Define the size of the problem, here 2^18.
|
||||
size := 1 << logSize
|
||||
|
||||
fmt.Printf("---------------------- MSM size 2^%d=%d ------------------------\n", logSize, size)
|
||||
|
||||
// println(scalarsBls12377, pointsBls12377, pointsBn254G2)
|
||||
// println(scalarsBn254, pointsBn254, pointsBls12377G2)
|
||||
|
||||
print("Configuring bn254 MSM ... ")
|
||||
startTime = time.Now()
|
||||
|
||||
scalarsBn254 := scalarsBn254Max[:size]
|
||||
pointsBn254 := pointsBn254Max[:size]
|
||||
pointsBn254G2 := pointsBn254G2Max[:size]
|
||||
|
||||
cfgBn254 := core.GetDefaultMSMConfig()
|
||||
cfgBn254G2 := core.GetDefaultMSMConfig()
|
||||
cfgBn254.IsAsync = true
|
||||
cfgBn254G2.IsAsync = true
|
||||
|
||||
streamBn254, _ := cr.CreateStream()
|
||||
streamBn254G2, _ := cr.CreateStream()
|
||||
|
||||
cfgBn254.Ctx.Stream = &streamBn254
|
||||
cfgBn254G2.Ctx.Stream = &streamBn254G2
|
||||
|
||||
var projectiveBn254 bn254.Projective
|
||||
var projectiveBn254G2 bn254G2.G2Projective
|
||||
|
||||
var msmResultBn254 core.DeviceSlice
|
||||
var msmResultBn254G2 core.DeviceSlice
|
||||
|
||||
_, e := msmResultBn254.MallocAsync(projectiveBn254.Size(), projectiveBn254.Size(), streamBn254)
|
||||
if e != cr.CudaSuccess {
|
||||
errorString := fmt.Sprint(
|
||||
"Bn254 Malloc failed: ", e)
|
||||
panic(errorString)
|
||||
}
|
||||
_, e = msmResultBn254G2.MallocAsync(projectiveBn254G2.Size(), projectiveBn254G2.Size(), streamBn254G2)
|
||||
if e != cr.CudaSuccess {
|
||||
errorString := fmt.Sprint(
|
||||
"Bn254 Malloc G2 failed: ", e)
|
||||
panic(errorString)
|
||||
}
|
||||
|
||||
println(time.Since(startTime).String())
|
||||
|
||||
print("Configuring Bls12377 MSM ... ")
|
||||
startTime = time.Now()
|
||||
|
||||
scalarsBls12377 := scalarsBls12377Max[:size]
|
||||
pointsBls12377 := pointsBls12377Max[:size]
|
||||
pointsBls12377G2 := pointsBls12377G2Max[:size]
|
||||
|
||||
cfgBls12377 := core.GetDefaultMSMConfig()
|
||||
cfgBls12377G2 := core.GetDefaultMSMConfig()
|
||||
cfgBls12377.IsAsync = true
|
||||
cfgBls12377G2.IsAsync = true
|
||||
|
||||
streamBls12377, _ := cr.CreateStream()
|
||||
streamBls12377G2, _ := cr.CreateStream()
|
||||
|
||||
cfgBls12377.Ctx.Stream = &streamBls12377
|
||||
cfgBls12377G2.Ctx.Stream = &streamBls12377G2
|
||||
|
||||
var projectiveBls12377 bls12377.Projective
|
||||
var projectiveBls12377G2 bls12377G2.G2Projective
|
||||
|
||||
var msmResultBls12377 core.DeviceSlice
|
||||
var msmResultBls12377G2 core.DeviceSlice
|
||||
|
||||
_, e = msmResultBls12377.MallocAsync(projectiveBls12377.Size(), projectiveBls12377.Size(), streamBls12377)
|
||||
if e != cr.CudaSuccess {
|
||||
errorString := fmt.Sprint(
|
||||
"Bls12_377 Malloc failed: ", e)
|
||||
panic(errorString)
|
||||
}
|
||||
_, e = msmResultBls12377G2.MallocAsync(projectiveBls12377G2.Size(), projectiveBls12377G2.Size(), streamBls12377G2)
|
||||
if e != cr.CudaSuccess {
|
||||
errorString := fmt.Sprint(
|
||||
"Bls12_377 Malloc G2 failed: ", e)
|
||||
panic(errorString)
|
||||
}
|
||||
|
||||
println(time.Since(startTime).String())
|
||||
|
||||
print("Executing bn254 MSM on device ... ")
|
||||
startTime = time.Now()
|
||||
|
||||
e = bn254Msm.Msm(scalarsBn254, pointsBn254, &cfgBn254, msmResultBn254)
|
||||
if e != cr.CudaSuccess {
|
||||
errorString := fmt.Sprint(
|
||||
"bn254 Msm failed: ", e)
|
||||
panic(errorString)
|
||||
}
|
||||
e = bn254G2.G2Msm(scalarsBn254, pointsBn254G2, &cfgBn254G2, msmResultBn254G2)
|
||||
if e != cr.CudaSuccess {
|
||||
errorString := fmt.Sprint(
|
||||
"bn254 Msm G2 failed: ", e)
|
||||
panic(errorString)
|
||||
}
|
||||
|
||||
msmResultBn254Host := make(core.HostSlice[bn254.Projective], 1)
|
||||
msmResultBn254G2Host := make(core.HostSlice[bn254G2.G2Projective], 1)
|
||||
|
||||
msmResultBn254Host.CopyFromDeviceAsync(&msmResultBn254, streamBn254)
|
||||
msmResultBn254G2Host.CopyFromDeviceAsync(&msmResultBn254G2, streamBn254G2)
|
||||
|
||||
msmResultBn254.FreeAsync(streamBn254)
|
||||
msmResultBn254G2.FreeAsync(streamBn254G2)
|
||||
|
||||
cr.SynchronizeStream(&streamBn254)
|
||||
cr.SynchronizeStream(&streamBn254G2)
|
||||
|
||||
println(time.Since(startTime).String())
|
||||
|
||||
print("Executing Bls12377 MSM on device ... ")
|
||||
startTime = time.Now()
|
||||
|
||||
e = bls12377Msm.Msm(scalarsBls12377, pointsBls12377, &cfgBls12377, msmResultBls12377)
|
||||
if e != cr.CudaSuccess {
|
||||
errorString := fmt.Sprint(
|
||||
"bls12_377 Msm failed: ", e)
|
||||
panic(errorString)
|
||||
}
|
||||
e = bls12377G2.G2Msm(scalarsBls12377, pointsBls12377G2, &cfgBls12377G2, msmResultBls12377G2)
|
||||
if e != cr.CudaSuccess {
|
||||
errorString := fmt.Sprint(
|
||||
"bls12_377 Msm G2 failed: ", e)
|
||||
panic(errorString)
|
||||
}
|
||||
|
||||
msmResultBls12377Host := make(core.HostSlice[bls12377.Projective], 1)
|
||||
msmResultBls12377G2Host := make(core.HostSlice[bls12377G2.G2Projective], 1)
|
||||
|
||||
msmResultBls12377Host.CopyFromDeviceAsync(&msmResultBls12377, streamBls12377)
|
||||
msmResultBls12377G2Host.CopyFromDeviceAsync(&msmResultBls12377G2, streamBls12377G2)
|
||||
|
||||
msmResultBls12377.FreeAsync(streamBls12377)
|
||||
msmResultBls12377G2.FreeAsync(streamBls12377G2)
|
||||
|
||||
cr.SynchronizeStream(&streamBls12377)
|
||||
cr.SynchronizeStream(&streamBls12377G2)
|
||||
|
||||
println(time.Since(startTime).String())
|
||||
}
|
||||
}
|
||||
39
examples/golang/ntt/README.md
Normal file
39
examples/golang/ntt/README.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# ICICLE example: Number Theoretic Transform (NTT) in Golang
|
||||
|
||||
## Key-Takeaway
|
||||
|
||||
`ICICLE` provides Golang bindings to CUDA-accelerated C++ implementation of [Number Theoretic Transform](https://github.com/ingonyama-zk/ingopedia/blob/master/src/fft.md).
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
err := Ntt(
|
||||
/* input slice */ scalars,
|
||||
/* NTT Direction */ core.KForward,
|
||||
/* NTT Configuration */ &cfg,
|
||||
/* output slice */ result)
|
||||
```
|
||||
|
||||
In this example we use the `BN254` and `BLS12377` fields.
|
||||
|
||||
## What's in this example
|
||||
|
||||
1. Define the size of NTT.
|
||||
2. Generate random inputs
|
||||
3. Set up the domain.
|
||||
4. Configure NTT
|
||||
5. Execute NTT on-device
|
||||
6. Move the result on host
|
||||
|
||||
Running the example:
|
||||
|
||||
```sh
|
||||
go run main.go
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The default size is 2^20. You can change this by passing the `-s <size>` option. To change the size to 2^23, run the example like this:
|
||||
|
||||
```sh
|
||||
go run main.go -s=23
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user