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​
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()
Setting and Getting the Default Device​
You can set the default device for all threads:
device := runtime.CreateDevice("CUDA", 0) // or other
defaultDevice := runtime.SetDefaultDevice(device);
Setting a default device should be done once from the main thread of the application. If another device or backend is needed for a specific thread runtime.SetDevice should be used instead.
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
}