Files
dex/pkg/lx/orderbook_cuda_parity_test.go
zeekay c9c79f5c96 dex/pkg/lx: AMM cross-platform + CLOB CUDA + ABI mirror asserts + parity
AMM unification (one Go entry point, per-platform impls):
- amm_gpu.go          (cgo) — BatchEvalConstantProduct dispatcher with
                                LUX_GPU_DISABLE kill switch + fallback metrics
- amm_gpu_metal.go    (cgo && darwin) — Metal driver via lux-dex-amm-metal
- amm_gpu_cuda.go     (cgo && linux)  — CUDA driver via lux-dex-amm-cuda
- amm_gpu_stub.go     (cgo && other)  — errAMMGPUUnsupported sentinel
- amm_nogpu.go        (!cgo)          — pure CPU path

CLOB CUDA wire (the LP-108 missing piece — Backend enum can come back):
- orderbook_cuda_types.go     — Go mirrors of DEXOrder (64B), DEXTrade (80B),
                                 DEXPrice; assertDEXABI() in init() panics
                                 on any size/offset drift vs the C header
- orderbook_match_cpu.go      — MatchOrderCPU canonical Go oracle
- orderbook_cuda.go           (cgo && linux) — gpuMatchOrder via
                                 lux-dex-clob-cuda → libdex_clob_cuda.a →
                                 luxcpp/cuda/kernels/gpu/dex_swap.cu
- orderbook_cuda_stub.go      (other) — errCLOBGPUUnsupported sentinel
- orderbook_gpu.go            — MatchOrderGPU dispatcher with strict fallback
                                 policy + backend.RecordFallback observability
- orderbook_cuda_parity_test.go — 8 randomized parity subtests using
                                  DEXTrade.EqualFields / DEXOrder.EqualFields
                                  (NOT struct ==) so padding bytes can't
                                  cause false negatives

Padding normalization: _ blank pads renamed to _pad (addressable);
ClearPadding() methods on DEXOrder/DEXTrade; CPU oracle + CUDA host
driver both zero pad bytes for deterministic structural comparison.

Branding: all `Lux*`/`LUX_*` types in the C ABI mirror dropped (DEXOrder,
DEXTrade, DEXPrice, AMMReservePair, DEX_SIDE_BID, etc.).

Temporary replace github.com/luxfi/crypto => ../../lux/crypto pending
publication of crypto with backend.GPUDisabled / RecordFallback /
FallbackReason. Remove the replace once crypto is tagged.
2026-05-24 14:19:56 -07:00

92 lines
3.0 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package lx
import (
"math/rand"
"testing"
)
// TestMatchOrder_GPUMatchesCPU pins the field-equality contract between
// MatchOrderGPU and MatchOrderCPU. On hosts without CUDA the GPU path
// transparently falls back to CPU, so this test still passes (it proves
// CPU == CPU and exercises the dispatcher path). On Linux+NVIDIA hosts
// it proves the CUDA dispatch produces the same trade list and remaining
// quantity field-for-field.
//
// We compare via DEXTrade.EqualFields / DEXOrder.EqualFields, NOT `==`,
// so padding bytes are not allowed to influence the result. Padding is
// also explicitly cleared on both paths (host driver post-copy + Go
// MatchOrderCPU) so the production objects stay deterministic too.
func TestMatchOrder_GPUMatchesCPU(t *testing.T) {
for seed := int64(1); seed <= 8; seed++ {
seed := seed
t.Run("", func(t *testing.T) {
rng := rand.New(rand.NewSource(seed))
incoming := DEXOrder{
OrderID: uint64(seed*1000 + 1),
UserID: 1,
Price: DEXPrice{Integer: uint64(100 + rng.Intn(20)), Fraction: 0},
Quantity: uint64(50 + rng.Intn(450)),
Timestamp: uint64(seed),
Side: DEXSideBid,
Type: DEXOrderLimit,
Status: DEXStatusOpen,
}
incoming.Remaining = incoming.Quantity
bookSize := 16 + rng.Intn(64)
book := make([]DEXOrder, bookSize)
indices := make([]uint32, bookSize)
for i := range book {
book[i] = DEXOrder{
OrderID: uint64(seed*10000) + uint64(i),
UserID: 2 + uint64(i)%4,
Price: DEXPrice{Integer: uint64(90 + rng.Intn(30)), Fraction: 0},
Quantity: uint64(10 + rng.Intn(100)),
Timestamp: uint64(seed*100) + uint64(i),
Side: DEXSideAsk,
Type: DEXOrderLimit,
Status: DEXStatusOpen,
}
book[i].Remaining = book[i].Quantity
indices[i] = uint32(i)
}
// Deep-copy book so CPU and GPU runs start from identical state.
bookCPU := append([]DEXOrder(nil), book...)
bookGPU := append([]DEXOrder(nil), book...)
incomingCPU := incoming
tradesCPU, remCPU := MatchOrderCPU(&incomingCPU, bookCPU, indices, 1_000_000, 9_999)
incomingGPU := incoming
tradesGPU, remGPU, err := MatchOrderGPU(&incomingGPU, bookGPU, indices, 1_000_000, 9_999)
if err != nil {
t.Fatalf("MatchOrderGPU error: %v", err)
}
if remCPU != remGPU {
t.Fatalf("remaining mismatch: cpu=%d gpu=%d", remCPU, remGPU)
}
if len(tradesCPU) != len(tradesGPU) {
t.Fatalf("trade count mismatch: cpu=%d gpu=%d", len(tradesCPU), len(tradesGPU))
}
for i := range tradesCPU {
if !tradesCPU[i].EqualFields(tradesGPU[i]) {
t.Fatalf("trade[%d] field mismatch:\n cpu=%+v\n gpu=%+v",
i, tradesCPU[i], tradesGPU[i])
}
}
for i := range bookCPU {
if !bookCPU[i].EqualFields(bookGPU[i]) {
t.Fatalf("book[%d] field mismatch:\n cpu=%+v\n gpu=%+v",
i, bookCPU[i], bookGPU[i])
}
}
})
}
}