mirror of
https://github.com/luxfi/accel.git
synced 2026-10-30 17:03:45 +00:00
516 lines
13 KiB
Go
516 lines
13 KiB
Go
//go:build accel
|
|
|
|
// Package fhe provides GPU-accelerated Fully Homomorphic Encryption operations.
|
|
//
|
|
// Copyright (c) 2024-2025 Lux Industries Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
package fhe
|
|
|
|
/*
|
|
#cgo CFLAGS: -I${SRCDIR}/../../../internal/capi
|
|
#cgo CXXFLAGS: -I${SRCDIR}/../../../internal/capi -std=c++17 -O3
|
|
#cgo linux LDFLAGS: -lcudart -lfhe_multi_gpu
|
|
#cgo darwin LDFLAGS: -framework Metal -framework Foundation -lfhe_multi_gpu
|
|
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
#include <stdlib.h>
|
|
|
|
// Multi-GPU context handle
|
|
typedef struct MultiGPUContext MultiGPUContext;
|
|
|
|
// Multi-GPU C API declarations
|
|
int multi_gpu_init(MultiGPUContext** ctx);
|
|
void multi_gpu_cleanup(MultiGPUContext* ctx);
|
|
int multi_gpu_count(MultiGPUContext* ctx);
|
|
bool multi_gpu_has_nvlink(MultiGPUContext* ctx);
|
|
int multi_gpu_p2p_copy(MultiGPUContext* ctx, int src_dev, void* src, int dst_dev, void* dst, size_t size);
|
|
void multi_gpu_barrier(MultiGPUContext* ctx);
|
|
int multi_gpu_distribute_work(MultiGPUContext* ctx, uint32_t total, uint32_t* work_per_gpu, uint32_t* offsets);
|
|
int multi_gpu_ntt_forward(MultiGPUContext* ctx, uint64_t** data, uint64_t** twiddles, uint32_t* n_per_gpu, uint64_t q);
|
|
int multi_gpu_ntt_inverse(MultiGPUContext* ctx, uint64_t** data, uint64_t** twiddles, uint32_t* n_per_gpu, uint64_t q);
|
|
int multi_gpu_memory_info(MultiGPUContext* ctx, int device, size_t* free_mem, size_t* total_mem);
|
|
int multi_gpu_set_device(int device);
|
|
int multi_gpu_sync_device(MultiGPUContext* ctx, int device);
|
|
int multi_gpu_sync_all(MultiGPUContext* ctx);
|
|
void* multi_gpu_get_stream(MultiGPUContext* ctx, int device);
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"unsafe"
|
|
|
|
"github.com/luxfi/accel"
|
|
)
|
|
|
|
// MultiGPUContext manages coordination across multiple GPUs for FHE operations.
|
|
type MultiGPUContext struct {
|
|
ptr *C.MultiGPUContext
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewMultiGPUContext initializes a multi-GPU context.
|
|
// Returns error if no GPUs are available or initialization fails.
|
|
func NewMultiGPUContext() (*MultiGPUContext, error) {
|
|
var ptr *C.MultiGPUContext
|
|
ret := C.multi_gpu_init(&ptr)
|
|
if ret != 0 || ptr == nil {
|
|
return nil, fmt.Errorf("failed to initialize multi-GPU context: error code %d", ret)
|
|
}
|
|
|
|
return &MultiGPUContext{
|
|
ptr: ptr,
|
|
}, nil
|
|
}
|
|
|
|
// Close releases all GPU resources and cleans up the context.
|
|
func (ctx *MultiGPUContext) Close() {
|
|
ctx.mu.Lock()
|
|
defer ctx.mu.Unlock()
|
|
|
|
if ctx.ptr != nil {
|
|
C.multi_gpu_cleanup(ctx.ptr)
|
|
ctx.ptr = nil
|
|
}
|
|
}
|
|
|
|
// GPUCount returns the number of available GPUs.
|
|
func (ctx *MultiGPUContext) GPUCount() int {
|
|
ctx.mu.RLock()
|
|
defer ctx.mu.RUnlock()
|
|
|
|
if ctx.ptr == nil {
|
|
return 0
|
|
}
|
|
|
|
return int(C.multi_gpu_count(ctx.ptr))
|
|
}
|
|
|
|
// HasNVLink returns true if NVLink is available between any GPUs.
|
|
// NVLink enables high-speed peer-to-peer transfers.
|
|
func (ctx *MultiGPUContext) HasNVLink() bool {
|
|
ctx.mu.RLock()
|
|
defer ctx.mu.RUnlock()
|
|
|
|
if ctx.ptr == nil {
|
|
return false
|
|
}
|
|
|
|
return bool(C.multi_gpu_has_nvlink(ctx.ptr))
|
|
}
|
|
|
|
// P2PCopy performs peer-to-peer memory copy between GPUs.
|
|
// Uses NVLink when available, falls back to host staging otherwise.
|
|
// srcDevice and dstDevice are GPU indices [0, GPUCount).
|
|
func (ctx *MultiGPUContext) P2PCopy(srcDevice int, srcPtr unsafe.Pointer, dstDevice int, dstPtr unsafe.Pointer, size int) error {
|
|
ctx.mu.RLock()
|
|
defer ctx.mu.RUnlock()
|
|
|
|
if ctx.ptr == nil {
|
|
return fmt.Errorf("multi-GPU context is closed")
|
|
}
|
|
|
|
ret := C.multi_gpu_p2p_copy(
|
|
ctx.ptr,
|
|
C.int(srcDevice),
|
|
srcPtr,
|
|
C.int(dstDevice),
|
|
dstPtr,
|
|
C.size_t(size),
|
|
)
|
|
|
|
if ret != 0 {
|
|
return fmt.Errorf("P2P copy failed: error code %d", ret)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Barrier synchronizes all GPUs using events.
|
|
// Blocks until all GPU streams have completed pending work.
|
|
func (ctx *MultiGPUContext) Barrier() {
|
|
ctx.mu.RLock()
|
|
defer ctx.mu.RUnlock()
|
|
|
|
if ctx.ptr == nil {
|
|
return
|
|
}
|
|
|
|
C.multi_gpu_barrier(ctx.ptr)
|
|
}
|
|
|
|
// WorkDistribution describes how work is distributed across GPUs.
|
|
type WorkDistribution struct {
|
|
WorkPerGPU []uint32 // Number of elements assigned to each GPU
|
|
Offsets []uint32 // Starting offset for each GPU's work
|
|
}
|
|
|
|
// DistributeWork calculates optimal work distribution across GPUs.
|
|
// Uses load balancing based on available GPU memory.
|
|
func (ctx *MultiGPUContext) DistributeWork(totalElements uint32) (*WorkDistribution, error) {
|
|
ctx.mu.RLock()
|
|
defer ctx.mu.RUnlock()
|
|
|
|
if ctx.ptr == nil {
|
|
return nil, fmt.Errorf("multi-GPU context is closed")
|
|
}
|
|
|
|
numGPUs := int(C.multi_gpu_count(ctx.ptr))
|
|
workPerGPU := make([]uint32, numGPUs)
|
|
offsets := make([]uint32, numGPUs)
|
|
|
|
ret := C.multi_gpu_distribute_work(
|
|
ctx.ptr,
|
|
C.uint32_t(totalElements),
|
|
(*C.uint32_t)(unsafe.Pointer(&workPerGPU[0])),
|
|
(*C.uint32_t)(unsafe.Pointer(&offsets[0])),
|
|
)
|
|
|
|
if ret != 0 {
|
|
return nil, fmt.Errorf("work distribution failed: error code %d", ret)
|
|
}
|
|
|
|
return &WorkDistribution{
|
|
WorkPerGPU: workPerGPU,
|
|
Offsets: offsets,
|
|
}, nil
|
|
}
|
|
|
|
// DistributedNTTData holds data distributed across multiple GPUs for NTT operations.
|
|
type DistributedNTTData struct {
|
|
DataPerGPU []unsafe.Pointer // GPU memory pointers for data on each GPU
|
|
TwiddlesPerGPU []unsafe.Pointer // GPU memory pointers for twiddle factors on each GPU
|
|
NPerGPU []uint32 // Number of elements on each GPU
|
|
Q uint64 // Modulus
|
|
}
|
|
|
|
// NTTForward performs distributed forward NTT across multiple GPUs.
|
|
// Uses four-step algorithm to decompose work across GPUs.
|
|
func (ctx *MultiGPUContext) NTTForward(data *DistributedNTTData) error {
|
|
ctx.mu.RLock()
|
|
defer ctx.mu.RUnlock()
|
|
|
|
if ctx.ptr == nil {
|
|
return fmt.Errorf("multi-GPU context is closed")
|
|
}
|
|
|
|
if data == nil {
|
|
return fmt.Errorf("distributed NTT data is nil")
|
|
}
|
|
|
|
numGPUs := int(C.multi_gpu_count(ctx.ptr))
|
|
if len(data.DataPerGPU) != numGPUs || len(data.TwiddlesPerGPU) != numGPUs || len(data.NPerGPU) != numGPUs {
|
|
return fmt.Errorf("data arrays size mismatch: expected %d GPUs", numGPUs)
|
|
}
|
|
|
|
// Convert Go slices to C arrays
|
|
cDataPtrs := make([]*C.uint64_t, numGPUs)
|
|
cTwiddlePtrs := make([]*C.uint64_t, numGPUs)
|
|
for i := 0; i < numGPUs; i++ {
|
|
cDataPtrs[i] = (*C.uint64_t)(data.DataPerGPU[i])
|
|
cTwiddlePtrs[i] = (*C.uint64_t)(data.TwiddlesPerGPU[i])
|
|
}
|
|
|
|
ret := C.multi_gpu_ntt_forward(
|
|
ctx.ptr,
|
|
(**C.uint64_t)(unsafe.Pointer(&cDataPtrs[0])),
|
|
(**C.uint64_t)(unsafe.Pointer(&cTwiddlePtrs[0])),
|
|
(*C.uint32_t)(unsafe.Pointer(&data.NPerGPU[0])),
|
|
C.uint64_t(data.Q),
|
|
)
|
|
|
|
if ret != 0 {
|
|
return fmt.Errorf("distributed NTT forward failed: error code %d", ret)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NTTInverse performs distributed inverse NTT across multiple GPUs.
|
|
func (ctx *MultiGPUContext) NTTInverse(data *DistributedNTTData) error {
|
|
ctx.mu.RLock()
|
|
defer ctx.mu.RUnlock()
|
|
|
|
if ctx.ptr == nil {
|
|
return fmt.Errorf("multi-GPU context is closed")
|
|
}
|
|
|
|
if data == nil {
|
|
return fmt.Errorf("distributed NTT data is nil")
|
|
}
|
|
|
|
numGPUs := int(C.multi_gpu_count(ctx.ptr))
|
|
if len(data.DataPerGPU) != numGPUs || len(data.TwiddlesPerGPU) != numGPUs || len(data.NPerGPU) != numGPUs {
|
|
return fmt.Errorf("data arrays size mismatch: expected %d GPUs", numGPUs)
|
|
}
|
|
|
|
// Convert Go slices to C arrays
|
|
cDataPtrs := make([]*C.uint64_t, numGPUs)
|
|
cTwiddlePtrs := make([]*C.uint64_t, numGPUs)
|
|
for i := 0; i < numGPUs; i++ {
|
|
cDataPtrs[i] = (*C.uint64_t)(data.DataPerGPU[i])
|
|
cTwiddlePtrs[i] = (*C.uint64_t)(data.TwiddlesPerGPU[i])
|
|
}
|
|
|
|
ret := C.multi_gpu_ntt_inverse(
|
|
ctx.ptr,
|
|
(**C.uint64_t)(unsafe.Pointer(&cDataPtrs[0])),
|
|
(**C.uint64_t)(unsafe.Pointer(&cTwiddlePtrs[0])),
|
|
(*C.uint32_t)(unsafe.Pointer(&data.NPerGPU[0])),
|
|
C.uint64_t(data.Q),
|
|
)
|
|
|
|
if ret != 0 {
|
|
return fmt.Errorf("distributed NTT inverse failed: error code %d", ret)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GPUMemoryInfo holds memory information for a GPU.
|
|
type GPUMemoryInfo struct {
|
|
FreeMem int64 // Free memory in bytes
|
|
TotalMem int64 // Total memory in bytes
|
|
}
|
|
|
|
// GetMemoryInfo returns memory information for a specific GPU.
|
|
func (ctx *MultiGPUContext) GetMemoryInfo(device int) (*GPUMemoryInfo, error) {
|
|
ctx.mu.RLock()
|
|
defer ctx.mu.RUnlock()
|
|
|
|
if ctx.ptr == nil {
|
|
return nil, fmt.Errorf("multi-GPU context is closed")
|
|
}
|
|
|
|
var freeMem, totalMem C.size_t
|
|
ret := C.multi_gpu_memory_info(
|
|
ctx.ptr,
|
|
C.int(device),
|
|
&freeMem,
|
|
&totalMem,
|
|
)
|
|
|
|
if ret != 0 {
|
|
return nil, fmt.Errorf("failed to get memory info for GPU %d: error code %d", device, ret)
|
|
}
|
|
|
|
return &GPUMemoryInfo{
|
|
FreeMem: int64(freeMem),
|
|
TotalMem: int64(totalMem),
|
|
}, nil
|
|
}
|
|
|
|
// SetDevice sets the active GPU device.
|
|
func SetDevice(device int) error {
|
|
ret := C.multi_gpu_set_device(C.int(device))
|
|
if ret != 0 {
|
|
return fmt.Errorf("failed to set device %d: error code %d", device, ret)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SyncDevice synchronizes a specific GPU stream.
|
|
func (ctx *MultiGPUContext) SyncDevice(device int) error {
|
|
ctx.mu.RLock()
|
|
defer ctx.mu.RUnlock()
|
|
|
|
if ctx.ptr == nil {
|
|
return fmt.Errorf("multi-GPU context is closed")
|
|
}
|
|
|
|
ret := C.multi_gpu_sync_device(ctx.ptr, C.int(device))
|
|
if ret != 0 {
|
|
return fmt.Errorf("failed to sync device %d: error code %d", device, ret)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SyncAll synchronizes all GPU streams.
|
|
func (ctx *MultiGPUContext) SyncAll() error {
|
|
ctx.mu.RLock()
|
|
defer ctx.mu.RUnlock()
|
|
|
|
if ctx.ptr == nil {
|
|
return fmt.Errorf("multi-GPU context is closed")
|
|
}
|
|
|
|
ret := C.multi_gpu_sync_all(ctx.ptr)
|
|
if ret != 0 {
|
|
return fmt.Errorf("failed to sync all devices: error code %d", ret)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetStream returns the GPU stream for a specific device.
|
|
// Returns nil if device is invalid or context is closed.
|
|
func (ctx *MultiGPUContext) GetStream(device int) unsafe.Pointer {
|
|
ctx.mu.RLock()
|
|
defer ctx.mu.RUnlock()
|
|
|
|
if ctx.ptr == nil {
|
|
return nil
|
|
}
|
|
|
|
stream := C.multi_gpu_get_stream(ctx.ptr, C.int(device))
|
|
return unsafe.Pointer(stream)
|
|
}
|
|
|
|
// DistributedBatchEncrypt encrypts plaintexts distributed across multiple GPUs.
|
|
func DistributedBatchEncrypt(mgpu *MultiGPUContext, params Params, pk *PublicKey, pts []*Plaintext) ([]*Ciphertext, error) {
|
|
if mgpu == nil || pk == nil || len(pts) == 0 {
|
|
return nil, ErrInvalidInput
|
|
}
|
|
|
|
numGPUs := mgpu.GPUCount()
|
|
if numGPUs <= 1 {
|
|
// Fall back to single-GPU batch encrypt
|
|
sess, err := accel.NewSession()
|
|
if err != nil {
|
|
return batchEncryptCPU(params, pk, pts)
|
|
}
|
|
defer sess.Close()
|
|
return batchEncryptGPU(sess, params, pk, pts)
|
|
}
|
|
|
|
// Distribute work across GPUs
|
|
dist, err := mgpu.DistributeWork(uint32(len(pts)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
results := make([]*Ciphertext, len(pts))
|
|
var wg sync.WaitGroup
|
|
var encryptErr error
|
|
var errMu sync.Mutex
|
|
|
|
for gpuIdx := 0; gpuIdx < numGPUs; gpuIdx++ {
|
|
offset := dist.Offsets[gpuIdx]
|
|
count := dist.WorkPerGPU[gpuIdx]
|
|
if count == 0 {
|
|
continue
|
|
}
|
|
|
|
wg.Add(1)
|
|
go func(gpu int, off, cnt uint32) {
|
|
defer wg.Done()
|
|
|
|
if err := SetDevice(gpu); err != nil {
|
|
errMu.Lock()
|
|
if encryptErr == nil {
|
|
encryptErr = err
|
|
}
|
|
errMu.Unlock()
|
|
return
|
|
}
|
|
|
|
// Encrypt batch for this GPU
|
|
batch := pts[off : off+cnt]
|
|
cts, err := batchEncryptCPU(params, pk, batch) // Use CPU for now
|
|
if err != nil {
|
|
errMu.Lock()
|
|
if encryptErr == nil {
|
|
encryptErr = err
|
|
}
|
|
errMu.Unlock()
|
|
return
|
|
}
|
|
|
|
// Copy results
|
|
for i, ct := range cts {
|
|
results[int(off)+i] = ct
|
|
}
|
|
}(gpuIdx, offset, count)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
if encryptErr != nil {
|
|
return nil, encryptErr
|
|
}
|
|
|
|
mgpu.Barrier()
|
|
return results, nil
|
|
}
|
|
|
|
// DistributedBatchMultiply performs distributed ciphertext multiplication.
|
|
func DistributedBatchMultiply(mgpu *MultiGPUContext, params Params, ct1s, ct2s []*Ciphertext, rlk *RelinKey) ([]*Ciphertext, error) {
|
|
if mgpu == nil || rlk == nil || len(ct1s) == 0 || len(ct1s) != len(ct2s) {
|
|
return nil, ErrInvalidInput
|
|
}
|
|
|
|
numGPUs := mgpu.GPUCount()
|
|
if numGPUs <= 1 {
|
|
// Fall back to sequential
|
|
results := make([]*Ciphertext, len(ct1s))
|
|
for i := range ct1s {
|
|
ct, err := Multiply(params, ct1s[i], ct2s[i], rlk)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
results[i] = ct
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// Distribute work
|
|
dist, err := mgpu.DistributeWork(uint32(len(ct1s)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
results := make([]*Ciphertext, len(ct1s))
|
|
var wg sync.WaitGroup
|
|
var mulErr error
|
|
var errMu sync.Mutex
|
|
|
|
for gpuIdx := 0; gpuIdx < numGPUs; gpuIdx++ {
|
|
offset := dist.Offsets[gpuIdx]
|
|
count := dist.WorkPerGPU[gpuIdx]
|
|
if count == 0 {
|
|
continue
|
|
}
|
|
|
|
wg.Add(1)
|
|
go func(gpu int, off, cnt uint32) {
|
|
defer wg.Done()
|
|
|
|
if err := SetDevice(gpu); err != nil {
|
|
errMu.Lock()
|
|
if mulErr == nil {
|
|
mulErr = err
|
|
}
|
|
errMu.Unlock()
|
|
return
|
|
}
|
|
|
|
// Process batch for this GPU
|
|
for i := uint32(0); i < cnt; i++ {
|
|
idx := int(off + i)
|
|
ct, err := multiplyCPU(params, ct1s[idx], ct2s[idx], rlk)
|
|
if err != nil {
|
|
errMu.Lock()
|
|
if mulErr == nil {
|
|
mulErr = err
|
|
}
|
|
errMu.Unlock()
|
|
return
|
|
}
|
|
results[idx] = ct
|
|
}
|
|
}(gpuIdx, offset, count)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
if mulErr != nil {
|
|
return nil, mulErr
|
|
}
|
|
|
|
mgpu.Barrier()
|
|
return results, nil
|
|
}
|