Skip to main content
Version: 3.1.0

ICICLE Golang Usage Guide

Overview​

This guide covers the usage of ICICLE's Golang API, including device management, memory operations, data transfer, synchronization, and compute APIs.

Device Management​

note

See all ICICLE runtime APIs in runtime.go

Loading a Backend​

The backend can be loaded from a specific path or from an environment variable. This is essential for setting up the computing environment.

import "github.com/ingonyama-zk/icicle/v3/wrappers/golang/runtime"

result := runtime.LoadBackendFromEnvOrDefault()
// or load from custom install dir
result := runtime.LoadBackend("/path/to/backend/installdir", true)

Setting and Getting Active Device​

You can set the active device for the current thread and retrieve it when needed:

device = runtime.CreateDevice("CUDA", 0) // or other
result := runtime.SetDevice(device)
// or query current (thread) device
activeDevice := runtime.GetActiveDevice()

Querying Device Information​

Retrieve the number of available devices and check if a pointer is allocated on the host or on the active device:

numDevices := runtime.GetDeviceCount()

var ptr unsafe.Pointer
isHostMemory = runtime.IsHostMemory(ptr)
isDeviceMemory = runtime.IsActiveDeviceMemory(ptr)

Memory Management​

Allocating and Freeing Memory​

Memory can be allocated and freed on the active device:

ptr, err := runtime.Malloc(1024) // Allocate 1024 bytes
err := runtime.Free(ptr) // Free the allocated memory

Asynchronous Memory Operations​

You can perform memory allocation and deallocation asynchronously using streams:

stream, err := runtime.CreateStream()

ptr, err := runtime.MallocAsync(1024, stream)
err = runtime.FreeAsync(ptr, stream)

Querying Available Memory​

Retrieve the total and available memory on the active device:

size_t total_memory, available_memory;
availableMemory, err := runtime.GetAvailableMemory()
freeMemory := availableMemory.Free
totalMemory := availableMemory.Total

Setting Memory Values​

Set memory to a specific value on the active device, synchronously or asynchronously:

err := runtime.Memset(ptr, 0, 1024) // Set 1024 bytes to 0
err := runtime.MemsetAsync(ptr, 0, 1024, stream)

Data Transfer​

Explicit Data Transfers​

To avoid device-inference overhead, use explicit copy functions:

result := runtime.CopyToHost(host_dst, device_src, size)
result := runtime.CopyToHostAsync(host_dst, device_src, size, stream)
result := runtime.CopyToDevice(device_dst, host_src, size)
result := runtime.CopyToDeviceAsync(device_dst, host_src, size, stream)

Stream Management​

Creating and Destroying Streams​

Streams are used to manage asynchronous operations:

stream, err := runtime.CreateStream()
err = runtime.DestroyStream(stream)

Synchronization​

Synchronizing Streams and Devices​

Ensure all previous operations on a stream or device are completed before proceeding:

err := runtime.StreamSynchronize(stream)
err := runtime.DeviceSynchronize()

Device Properties​

Checking Device Availability​

Check if a device is available and retrieve a list of registered devices:

dev := runtime.CreateDevice("CPU", 0)
isCPUAvail := runtime.IsDeviceAvailable(dev)

Querying Device Properties​

Retrieve properties of the active device:

properties, err := runtime.GetDeviceProperties(properties);

/******************/
// where DeviceProperties is
type DeviceProperties struct {
UsingHostMemory bool // Indicates if the device uses host memory
NumMemoryRegions int32 // Number of memory regions available on the device
SupportsPinnedMemory bool // Indicates if the device supports pinned memory
}

Compute APIs​

Multi-Scalar Multiplication (MSM) Example​

Icicle provides high-performance compute APIs such as the Multi-Scalar Multiplication (MSM) for cryptographic operations. Here's a simple example of how to use the MSM API.

package main

import (
"fmt"

"github.com/ingonyama-zk/icicle/v3/wrappers/golang/core"
"github.com/ingonyama-zk/icicle/v3/wrappers/golang/runtime"

"github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254"
bn254Msm "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254/msm"
)

func main() {

// Load installed backends
runtime.LoadBackendFromEnvOrDefault()

// trying to choose CUDA if available, or fallback to CPU otherwise (default device)
deviceCuda := runtime.CreateDevice("CUDA", 0) // GPU-0
if runtime.IsDeviceAvailable(&deviceCuda) {
runtime.SetDevice(&deviceCuda)
} // else we stay on CPU backend

// Setup inputs
const size = 1 << 18

// Generate random inputs
scalars := bn254.GenerateScalars(size)
points := bn254.GenerateAffinePoints(size)

// (optional) copy scalars to device memory explicitly
var scalarsDevice core.DeviceSlice
scalars.CopyToDevice(&scalarsDevice, true)

// MSM configuration
cfgBn254 := core.GetDefaultMSMConfig()

// allocate memory for the result
result := make(core.HostSlice[bn254.Projective], 1)

// execute bn254 MSM on device
err := bn254Msm.Msm(scalarsDevice, points, &cfgBn254, result)

// Check for errors
if err != runtime.Success {
errorString := fmt.Sprint(
"bn254 Msm failed: ", err)
panic(errorString)
}

// free explicitly allocated device memory
scalarsDevice.Free()
}

Polynomial Operations Example​

Here's another example demonstrating polynomial operations using Icicle:

package main

import (
"fmt"

"github.com/ingonyama-zk/icicle/v3/wrappers/golang/core"
"github.com/ingonyama-zk/icicle/v3/wrappers/golang/runtime"

"github.com/ingonyama-zk/icicle/v3/wrappers/golang/fields/babybear"
"github.com/ingonyama-zk/icicle/v3/wrappers/golang/fields/babybear/ntt"
"github.com/ingonyama-zk/icicle/v3/wrappers/golang/fields/babybear/polynomial"
)

func initBabybearDomain() runtime.EIcicleError {
cfgInitDomain := core.GetDefaultNTTInitDomainConfig()
rouIcicle := babybear.ScalarField{}
rouIcicle.FromUint32(1461624142)
return ntt.InitDomain(rouIcicle, cfgInitDomain)
}

func init() {
// Load installed backends
runtime.LoadBackendFromEnvOrDefault()

// trying to choose CUDA if available, or fallback to CPU otherwise (default device)
deviceCuda := runtime.CreateDevice("CUDA", 0) // GPU-0
if runtime.IsDeviceAvailable(&deviceCuda) {
runtime.SetDevice(&deviceCuda)
} // else we stay on CPU backend

// build domain for ntt is required for some polynomial ops that rely on ntt
err := initBabybearDomain()
if err != runtime.Success {
errorString := fmt.Sprint(
"Babybear Domain initialization failed: ", err)
panic(errorString)
}
}

func main() {

// Setup inputs
const polySize = 1 << 10

// randomize two polynomials over babybear field
var fBabybear polynomial.DensePolynomial
defer fBabybear.Delete()
var gBabybear polynomial.DensePolynomial
defer gBabybear.Delete()
fBabybear.CreateFromCoeffecitients(babybear.GenerateScalars(polySize))
gBabybear.CreateFromCoeffecitients(babybear.GenerateScalars(polySize / 2))

// Perform polynomial multiplication
rBabybear := fBabybear.Multiply(&gBabybear) // Executes on the current device
defer rBabybear.Delete()
rDegree := rBabybear.Degree()

fmt.Println("f Degree: ", fBabybear.Degree())
fmt.Println("g Degree: ", gBabybear.Degree())
fmt.Println("r Degree: ", rDegree)
}

In this example, the polynomial multiplication is used to perform polynomial multiplication on CUDA or CPU, showcasing the flexibility and power of Icicle's compute APIs.

Error Handling​

Checking for Errors​

Icicle APIs return an EIcicleError enumeration value. Always check the returned value to ensure that operations were successful.

if result != runtime.SUCCESS {
// Handle error
}