fix: make accel dependency optional with build tags

The ai package imported github.com/luxfi/accel which requires CGO and
C headers not available in CI. This caused integration tests to fail
with [build failed] because the dependency chain couldn't compile.

Solution:
- Split accel.go into accel_cgo.go (requires cgo && accel tags) and
  accel_stub.go (fallback without CGO)
- Extract shared types to accel_types.go
- Update tests to skip when acceleration is not available
This commit is contained in:
2026-02-05 16:57:52 -08:00
parent 23e5e51d84
commit 663a2cfc6b
4 changed files with 195 additions and 23 deletions
+14 -15
View File
@@ -1,6 +1,8 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build cgo && accel
package ai
import (
@@ -11,19 +13,6 @@ import (
"github.com/luxfi/accel/ops/consensus"
)
// Vote represents a consensus vote for batch processing.
type Vote struct {
VoterID [32]byte
BlockID [32]byte
IsPreference bool
}
// ValidatorInfo contains validator information for quorum calculations.
type ValidatorInfo struct {
ValidatorID [32]byte
Weight uint64
}
// Backend provides GPU-accelerated consensus using luxfi/accel.
type Backend struct {
mu sync.RWMutex
@@ -81,7 +70,7 @@ func (b *Backend) ProcessVotesBatch(votes []Vote) (int, error) {
}
// ComputeQuorum checks if a quorum is reached for a set of votes.
func (b *Backend) ComputeQuorum(votes []Vote, validators []ValidatorInfo, threshold float64) (*consensus.QuorumResult, error) {
func (b *Backend) ComputeQuorum(votes []Vote, validators []ValidatorInfo, threshold float64) (*QuorumResult, error) {
if !b.initialized {
return nil, fmt.Errorf("backend not initialized")
}
@@ -103,7 +92,17 @@ func (b *Backend) ComputeQuorum(votes []Vote, validators []ValidatorInfo, thresh
}
}
return consensus.ComputeQuorum(voteData, validatorWeights, threshold)
result, err := consensus.ComputeQuorum(voteData, validatorWeights, threshold)
if err != nil {
return nil, err
}
return &QuorumResult{
HasQuorum: result.HasQuorum,
TotalWeight: result.TotalWeight,
VotedWeight: result.VotedWeight,
QuorumWeight: result.QuorumWeight,
}, nil
}
// GetThroughput returns the current throughput in votes/second.
+100
View File
@@ -0,0 +1,100 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build !cgo || !accel
package ai
import (
"fmt"
"sync"
)
// Backend provides a stub consensus backend when luxfi/accel is not available.
// This stub allows the package to compile without CGO dependencies.
type Backend struct {
mu sync.RWMutex
batchSize int
throughput float64
initialized bool
}
// NewBackend creates a stub consensus backend.
// Returns an error indicating acceleration is not available.
func NewBackend(batchSize int) (*Backend, error) {
return &Backend{
batchSize: batchSize,
initialized: false,
}, nil
}
// ProcessVotesBatch is a stub that returns an error when accel is not available.
func (b *Backend) ProcessVotesBatch(votes []Vote) (int, error) {
if len(votes) == 0 {
return 0, nil
}
// Stub implementation: just count votes without GPU acceleration
return len(votes), nil
}
// ComputeQuorum is a stub that computes quorum without GPU acceleration.
func (b *Backend) ComputeQuorum(votes []Vote, validators []ValidatorInfo, threshold float64) (*QuorumResult, error) {
if len(validators) == 0 {
return nil, fmt.Errorf("no validators provided")
}
// Create a map of validator weights
weights := make(map[[32]byte]uint64)
var totalWeight uint64
for _, v := range validators {
weights[v.ValidatorID] = v.Weight
totalWeight += v.Weight
}
// Count voted weight
var votedWeight uint64
votedValidators := make(map[[32]byte]bool)
for _, vote := range votes {
if vote.IsPreference && !votedValidators[vote.VoterID] {
if weight, ok := weights[vote.VoterID]; ok {
votedWeight += weight
votedValidators[vote.VoterID] = true
}
}
}
quorumWeight := uint64(float64(totalWeight) * threshold)
hasQuorum := votedWeight >= quorumWeight
return &QuorumResult{
HasQuorum: hasQuorum,
TotalWeight: totalWeight,
VotedWeight: votedWeight,
QuorumWeight: quorumWeight,
}, nil
}
// GetThroughput returns zero when acceleration is not available.
func (b *Backend) GetThroughput() float64 {
return 0
}
// IsEnabled returns false when acceleration is not available.
func (b *Backend) IsEnabled() bool {
return false
}
// GetDeviceInfo returns empty string when acceleration is not available.
func (b *Backend) GetDeviceInfo() string {
return ""
}
// NewMLXBackend creates a stub backend when MLX is not available.
func NewMLXBackend(batchSize int) (*Backend, error) {
return NewBackend(batchSize)
}
// NewAccelBackend creates a stub backend when acceleration is not available.
func NewAccelBackend(batchSize int) (*Backend, error) {
return NewBackend(batchSize)
}
+25
View File
@@ -0,0 +1,25 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package ai
// Vote represents a consensus vote for batch processing.
type Vote struct {
VoterID [32]byte
BlockID [32]byte
IsPreference bool
}
// ValidatorInfo contains validator information for quorum calculations.
type ValidatorInfo struct {
ValidatorID [32]byte
Weight uint64
}
// QuorumResult contains the result of a quorum check.
type QuorumResult struct {
HasQuorum bool
TotalWeight uint64
VotedWeight uint64
QuorumWeight uint64
}
+56 -8
View File
@@ -14,8 +14,10 @@ func TestMLXBackendInitialization(t *testing.T) {
t.Fatalf("Failed to initialize MLX backend: %v", err)
}
// When running without accel, the backend may not be enabled
// This is expected behavior - skip tests that require acceleration
if !backend.IsEnabled() {
t.Error("MLX backend should be enabled")
t.Skip("MLX backend not available (requires accel build tag)")
}
if backend.GetDeviceInfo() == "" {
@@ -54,10 +56,10 @@ func TestMLXBackendProcessVotes(t *testing.T) {
t.Errorf("Expected to process %d votes, got %d", len(votes), processed)
}
// Check throughput was updated
// Check throughput - only expect positive throughput when enabled
throughput := backend.GetThroughput()
if throughput <= 0 {
t.Error("Throughput should be positive after processing")
if backend.IsEnabled() && throughput <= 0 {
t.Error("Throughput should be positive after processing when enabled")
}
t.Logf("Processing throughput: %.2f votes/sec", throughput)
}
@@ -145,10 +147,10 @@ func TestMLXBackendConcurrency(t *testing.T) {
<-done
}
// Check throughput after concurrent processing
// Check throughput after concurrent processing - only expect positive when enabled
throughput := backend.GetThroughput()
if throughput <= 0 {
t.Error("Throughput should be positive after concurrent processing")
if backend.IsEnabled() && throughput <= 0 {
t.Error("Throughput should be positive after concurrent processing when enabled")
}
t.Logf("Concurrent processing throughput: %.2f votes/sec", throughput)
}
@@ -160,8 +162,10 @@ func TestAccelBackendInitialization(t *testing.T) {
t.Fatalf("Failed to initialize AccelBackend: %v", err)
}
// When running without accel, the backend may not be enabled
// This is expected behavior - skip tests that require acceleration
if !backend.IsEnabled() {
t.Error("AccelBackend should be enabled")
t.Skip("AccelBackend not available (requires accel build tag)")
}
if backend.GetDeviceInfo() == "" {
@@ -194,3 +198,47 @@ func TestAccelBackendProcessVotes(t *testing.T) {
t.Errorf("Expected to process %d votes, got %d", len(votes), processed)
}
}
// TestBackendStubFunctionality tests that the stub works correctly
func TestBackendStubFunctionality(t *testing.T) {
backend, err := NewBackend(100)
if err != nil {
t.Fatalf("Failed to create backend: %v", err)
}
// Test that ProcessVotesBatch works (with or without accel)
votes := make([]Vote, 5)
for i := range votes {
votes[i] = Vote{
VoterID: [32]byte{byte(i)},
BlockID: [32]byte{1},
IsPreference: true,
}
}
processed, err := backend.ProcessVotesBatch(votes)
if err != nil {
t.Fatalf("ProcessVotesBatch failed: %v", err)
}
if processed != len(votes) {
t.Errorf("Expected %d processed, got %d", len(votes), processed)
}
// Test ComputeQuorum works
validators := []ValidatorInfo{
{ValidatorID: [32]byte{0}, Weight: 100},
{ValidatorID: [32]byte{1}, Weight: 100},
{ValidatorID: [32]byte{2}, Weight: 100},
}
result, err := backend.ComputeQuorum(votes, validators, 0.5)
if err != nil {
t.Fatalf("ComputeQuorum failed: %v", err)
}
if result == nil {
t.Fatal("ComputeQuorum returned nil result")
}
t.Logf("Quorum result: HasQuorum=%v, VotedWeight=%d, TotalWeight=%d",
result.HasQuorum, result.VotedWeight, result.TotalWeight)
}