Skip to main content

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
}