mirror of
https://github.com/luxfi/aivm.git
synced 2026-10-30 17:02:34 +00:00
461 lines
16 KiB
C++
461 lines
16 KiB
C++
// Copyright (C) 2026, Lux Partners Limited. All rights reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// aivm_determinism_test.cpp — cross-backend determinism harness.
|
|
//
|
|
// Compares CPU reference vs GPU engine (Metal on Apple, CUDA on Linux+CUDA)
|
|
// across canonical workloads. WGSL byte-equivalence is verified at the
|
|
// kernel-source level (the encoding matches CPU/Metal/CUDA by construction;
|
|
// see aivm_kernels_common.wgsl). When LUX_HAS_WEBGPU is wired, the WGPU
|
|
// backend factory becomes available and this harness will exercise the
|
|
// fourth path automatically.
|
|
//
|
|
// Workloads (the brief):
|
|
// 1. 1000 attestations across 4 quote families (BLS / Corona /
|
|
// Groth16 / SLH-DSA) → byte-equivalent across backends
|
|
// 2. 50 model registrations + license-update path
|
|
// 3. 100 audit anchors with parent-root chain integrity
|
|
// 4. Attestation expiry properly excludes expired entries
|
|
// 5. Wrong attesting_key (zero) → rejected
|
|
// 6. Empty round → deterministic non-zero root
|
|
|
|
#include "lux/aivm/aivm_gpu_engine.hpp"
|
|
#include "lux/aivm/aivm_cpu_reference.hpp"
|
|
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <memory>
|
|
#include <vector>
|
|
|
|
using namespace aivm::gpu;
|
|
|
|
#if defined(LUX_AIVM_TEST_WGPU)
|
|
namespace aivm::gpu {
|
|
std::unique_ptr<AIVMGPUEngine> create_aivm_gpu_engine_wgpu();
|
|
}
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
int g_passed = 0;
|
|
int g_failed = 0;
|
|
|
|
#define EXPECT(name, cond) \
|
|
do { \
|
|
if (!(cond)) { \
|
|
std::printf(" FAIL[%s]: %s\n", (name), #cond); \
|
|
std::fflush(stdout); \
|
|
++g_failed; \
|
|
return; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define PASS(name) \
|
|
do { \
|
|
std::printf(" ok : %s\n", (name)); \
|
|
std::fflush(stdout); \
|
|
++g_passed; \
|
|
} while (0)
|
|
|
|
AIVMRoundDescriptor make_desc(uint64_t round, uint64_t epoch = 0)
|
|
{
|
|
AIVMRoundDescriptor d{};
|
|
d.chain_id = 1u;
|
|
d.round = round;
|
|
d.timestamp_ns = 1700000000000000000ULL;
|
|
d.epoch = epoch;
|
|
d.mode = static_cast<uint32_t>(AIVMTransitionMode::FullRound);
|
|
d.closing_flag = 1u;
|
|
return d;
|
|
}
|
|
|
|
AttestationOp make_att(uint64_t key_seed, uint8_t kind = 0u, uint64_t expiry = 0u)
|
|
{
|
|
AttestationOp op{};
|
|
for (uint32_t i = 0; i < 32; ++i)
|
|
op.tee_quote_digest[i] = uint8_t((key_seed * 31u + i) & 0xFFu);
|
|
for (uint32_t i = 0; i < 32; ++i)
|
|
op.measurement[i] = uint8_t((key_seed * 17u + i) & 0xFFu);
|
|
for (uint32_t i = 0; i < 48; ++i)
|
|
op.attesting_key[i] = uint8_t((key_seed * 41u + i + 1u) & 0xFFu);
|
|
op.expiry_ns = expiry;
|
|
op.kind = kind;
|
|
op.evidence_offset = 0;
|
|
op.evidence_len = 64u;
|
|
return op;
|
|
}
|
|
|
|
ModelOp make_model_register(uint64_t seed, uint64_t param_count = 1000u)
|
|
{
|
|
ModelOp op{};
|
|
for (uint32_t i = 0; i < 32; ++i) op.model_root[i] = uint8_t((seed * 11u + i + 1u) & 0xFFu);
|
|
for (uint32_t i = 0; i < 32; ++i) op.weight_hash[i] = uint8_t((seed * 23u + i + 7u) & 0xFFu);
|
|
for (uint32_t i = 0; i < 32; ++i) op.license_root[i] = uint8_t((seed * 7u + i + 13u) & 0xFFu);
|
|
for (uint32_t i = 0; i < 20; ++i) op.owner_addr[i] = uint8_t((seed * 3u + i + 5u) & 0xFFu);
|
|
op.parameter_count = param_count;
|
|
op.modality = uint32_t(seed % 4u);
|
|
op.kind = static_cast<uint32_t>(ModelOpKind::Register);
|
|
return op;
|
|
}
|
|
|
|
ModelOp make_license_update(const ModelOp& reg, uint8_t fill)
|
|
{
|
|
ModelOp upd = reg;
|
|
upd.kind = static_cast<uint32_t>(ModelOpKind::UpdateLicense);
|
|
for (uint32_t i = 0; i < 32; ++i) upd.license_root[i] = uint8_t(fill ^ i);
|
|
return upd;
|
|
}
|
|
|
|
AnchorOp make_anchor(uint64_t height, const uint8_t parent[32])
|
|
{
|
|
AnchorOp op{};
|
|
for (uint32_t i = 0; i < 32; ++i) op.commit_root[i] = uint8_t((height * 13u + i + 1u) & 0xFFu);
|
|
std::memcpy(op.parent_root, parent, 32);
|
|
for (uint32_t i = 0; i < 32; ++i) op.validator_set_root_at_commit[i]
|
|
= uint8_t((height * 19u + i + 3u) & 0xFFu);
|
|
op.height = height;
|
|
op.timestamp_ns = 1700000000000000000ULL + height * 1000000000ULL;
|
|
return op;
|
|
}
|
|
|
|
struct WorkloadResult {
|
|
uint8_t attestation_root[32];
|
|
uint8_t model_registry_root[32];
|
|
uint8_t audit_root[32];
|
|
uint8_t aivm_state_root[32];
|
|
uint32_t active;
|
|
uint32_t expired;
|
|
uint32_t models;
|
|
uint32_t anchors;
|
|
|
|
static WorkloadResult from(const AIVMTransitionResult& r) {
|
|
WorkloadResult w{};
|
|
std::memcpy(w.attestation_root, r.attestation_root, 32);
|
|
std::memcpy(w.model_registry_root, r.model_registry_root, 32);
|
|
std::memcpy(w.audit_root, r.audit_root, 32);
|
|
std::memcpy(w.aivm_state_root, r.aivm_state_root, 32);
|
|
w.active = r.active_attestations;
|
|
w.expired = r.expired_attestations;
|
|
w.models = r.model_count;
|
|
w.anchors = r.anchor_count;
|
|
return w;
|
|
}
|
|
|
|
bool equals(const WorkloadResult& o) const {
|
|
return std::memcmp(attestation_root, o.attestation_root, 32) == 0
|
|
&& std::memcmp(model_registry_root, o.model_registry_root, 32) == 0
|
|
&& std::memcmp(audit_root, o.audit_root, 32) == 0
|
|
&& std::memcmp(aivm_state_root, o.aivm_state_root, 32) == 0
|
|
&& active == o.active
|
|
&& expired == o.expired
|
|
&& models == o.models
|
|
&& anchors == o.anchors;
|
|
}
|
|
};
|
|
|
|
WorkloadResult run_cpu(const AIVMRoundDescriptor& desc,
|
|
const std::vector<AttestationOp>& a,
|
|
const std::vector<ModelOp>& m,
|
|
const std::vector<AnchorOp>& n)
|
|
{
|
|
auto state = ref::AIVMReferenceState::empty();
|
|
auto r = ref::run_reference(state, desc, a, m, n);
|
|
return WorkloadResult::from(r);
|
|
}
|
|
|
|
WorkloadResult run_gpu(AIVMGPUEngine* engine,
|
|
const AIVMRoundDescriptor& desc,
|
|
const std::vector<AttestationOp>& a,
|
|
const std::vector<ModelOp>& m,
|
|
const std::vector<AnchorOp>& n)
|
|
{
|
|
auto h = engine->begin_round(desc);
|
|
if (!a.empty()) engine->push_attestation_ops(h, a);
|
|
if (!m.empty()) engine->push_model_ops(h, m);
|
|
if (!n.empty()) engine->push_anchor_ops(h, n);
|
|
auto r = engine->run_until_done(h);
|
|
engine->end_round(h);
|
|
return WorkloadResult::from(r);
|
|
}
|
|
|
|
void test_brief_workload(AIVMGPUEngine* engine)
|
|
{
|
|
std::vector<AttestationOp> a_ops;
|
|
// 1000 attestations across 4 kinds (BLS=0/SGX, Corona=1/SEV,
|
|
// Groth16=2/TDX, SLH-DSA=3/NvTrust). The kernel does not branch on
|
|
// kind; the kind only enters the leaf hash so different kinds produce
|
|
// different roots. Mix them to exercise all four.
|
|
for (uint64_t i = 1; i <= 1000; ++i)
|
|
a_ops.push_back(make_att(i, uint8_t(i % 4u)));
|
|
|
|
std::vector<ModelOp> m_ops;
|
|
for (uint64_t i = 1; i <= 50; ++i) {
|
|
auto reg = make_model_register(i, 1000u + i*100u);
|
|
m_ops.push_back(reg);
|
|
if ((i % 5u) == 0u) m_ops.push_back(make_license_update(reg, uint8_t(0x99u + i)));
|
|
}
|
|
|
|
std::vector<AnchorOp> n_ops;
|
|
uint8_t parent[32] = {};
|
|
for (uint64_t h = 1; h <= 100; ++h) {
|
|
auto op = make_anchor(h, parent);
|
|
n_ops.push_back(op);
|
|
std::memcpy(parent, op.commit_root, 32);
|
|
}
|
|
|
|
auto desc = make_desc(1u);
|
|
auto cpu = run_cpu(desc, a_ops, m_ops, n_ops);
|
|
|
|
if (engine == nullptr) {
|
|
auto cpu2 = run_cpu(desc, a_ops, m_ops, n_ops);
|
|
EXPECT("brief.cpu.det", cpu.equals(cpu2));
|
|
std::printf(" brief: CPU-only path; 1000a/50m/100n; root match across runs\n");
|
|
PASS("brief workload (1000a / 50m / 100n) — CPU determinism");
|
|
return;
|
|
}
|
|
auto gpu = run_gpu(engine, desc, a_ops, m_ops, n_ops);
|
|
EXPECT("brief.match", cpu.equals(gpu));
|
|
std::printf(" brief: roots match CPU<->GPU; active=%u models=%u anchors=%u\n",
|
|
gpu.active, gpu.models, gpu.anchors);
|
|
PASS("brief workload (1000a / 50m / 100n) — CPU<->GPU byte match");
|
|
}
|
|
|
|
// v0.59 extension — sweep size families to catch any size-dependent kernel
|
|
// drift (XVM caught a small/medium-only race at large/xlarge in its parallel
|
|
// rewrite; AIVM's locate-then-writeback is byte-identical at all sizes by
|
|
// construction, but we exercise it explicitly).
|
|
void test_size_sweep(AIVMGPUEngine* engine)
|
|
{
|
|
if (engine == nullptr) {
|
|
PASS("size sweep (skipped — no GPU)");
|
|
return;
|
|
}
|
|
struct Size { const char* name; uint32_t a, m, n; };
|
|
const Size sizes[] = {
|
|
{"small", 100, 5, 10},
|
|
{"medium", 1000, 50, 100},
|
|
// Capped to arena sizes so we exercise saturation but stay
|
|
// determinism-test fast (xlarge takes minutes through Metal apply).
|
|
{"large", 1024, 512, 1024},
|
|
};
|
|
for (const auto& s : sizes) {
|
|
std::vector<AttestationOp> a_ops;
|
|
for (uint64_t i = 1; i <= s.a; ++i)
|
|
a_ops.push_back(make_att(i, uint8_t(i % 4u)));
|
|
std::vector<ModelOp> m_ops;
|
|
for (uint64_t i = 1; i <= s.m; ++i)
|
|
m_ops.push_back(make_model_register(i, 1000u + i*100u));
|
|
std::vector<AnchorOp> n_ops;
|
|
uint8_t parent[32] = {};
|
|
for (uint64_t h = 1; h <= s.n; ++h) {
|
|
auto op = make_anchor(h, parent);
|
|
n_ops.push_back(op);
|
|
std::memcpy(parent, op.commit_root, 32);
|
|
}
|
|
|
|
auto desc = make_desc(uint64_t(s.a + s.m + s.n));
|
|
auto cpu = run_cpu(desc, a_ops, m_ops, n_ops);
|
|
auto gpu = run_gpu(engine, desc, a_ops, m_ops, n_ops);
|
|
if (!cpu.equals(gpu)) {
|
|
std::printf(" FAIL[size.%s]: CPU != GPU\n", s.name);
|
|
std::fflush(stdout);
|
|
++g_failed;
|
|
return;
|
|
}
|
|
std::printf(" size %-7s: %u/%u/%u → active=%u models=%u anchors=%u\n",
|
|
s.name, s.a, s.m, s.n, gpu.active, gpu.models, gpu.anchors);
|
|
}
|
|
PASS("size sweep small/medium/large — CPU<->GPU byte match");
|
|
}
|
|
|
|
void test_empty_round(AIVMGPUEngine* engine)
|
|
{
|
|
auto desc = make_desc(1u);
|
|
auto cpu = run_cpu(desc, {}, {}, {});
|
|
bool nonzero = false;
|
|
for (auto b : cpu.aivm_state_root) if (b != 0) { nonzero = true; break; }
|
|
EXPECT("empty.cpu.nz", nonzero);
|
|
|
|
if (engine != nullptr) {
|
|
auto gpu = run_gpu(engine, desc, {}, {}, {});
|
|
EXPECT("empty.match", cpu.equals(gpu));
|
|
}
|
|
PASS("empty round deterministic non-zero");
|
|
}
|
|
|
|
void test_expiry_excludes(AIVMGPUEngine* engine)
|
|
{
|
|
std::vector<AttestationOp> a_ops;
|
|
a_ops.push_back(make_att(1u, 0u, 1u)); // expired
|
|
a_ops.push_back(make_att(2u, 0u, 0u)); // never expires
|
|
a_ops.push_back(make_att(3u, 1u, uint64_t(1) << 60)); // far future
|
|
|
|
auto desc = make_desc(1u);
|
|
desc.timestamp_ns = 100u;
|
|
|
|
auto cpu = run_cpu(desc, a_ops, {}, {});
|
|
EXPECT("expiry.cpu.expired", cpu.expired == 1u);
|
|
EXPECT("expiry.cpu.active", cpu.active == 2u);
|
|
|
|
if (engine != nullptr) {
|
|
auto gpu = run_gpu(engine, desc, a_ops, {}, {});
|
|
EXPECT("expiry.match", cpu.equals(gpu));
|
|
}
|
|
PASS("attestation expiry excludes expired");
|
|
}
|
|
|
|
void test_wrong_attkey_rejected(AIVMGPUEngine* engine)
|
|
{
|
|
std::vector<AttestationOp> a_ops;
|
|
auto bad = make_att(1u);
|
|
for (auto& b : bad.attesting_key) b = 0u;
|
|
a_ops.push_back(bad);
|
|
a_ops.push_back(make_att(2u));
|
|
|
|
auto desc = make_desc(1u);
|
|
auto cpu = run_cpu(desc, a_ops, {}, {});
|
|
EXPECT("attkey.cpu.applied", cpu.active == 1u);
|
|
|
|
if (engine != nullptr) {
|
|
auto gpu = run_gpu(engine, desc, a_ops, {}, {});
|
|
EXPECT("attkey.match", cpu.equals(gpu));
|
|
}
|
|
PASS("wrong attesting_key rejected");
|
|
}
|
|
|
|
void test_anchor_chain(AIVMGPUEngine* engine)
|
|
{
|
|
std::vector<AnchorOp> n_ops;
|
|
uint8_t parent[32] = {};
|
|
for (uint64_t h = 1; h <= 100; ++h) {
|
|
auto op = make_anchor(h, parent);
|
|
n_ops.push_back(op);
|
|
std::memcpy(parent, op.commit_root, 32);
|
|
}
|
|
|
|
auto desc = make_desc(1u);
|
|
auto cpu = run_cpu(desc, {}, {}, n_ops);
|
|
EXPECT("chain.cpu.count", cpu.anchors == 100u);
|
|
|
|
if (engine != nullptr) {
|
|
auto gpu = run_gpu(engine, desc, {}, {}, n_ops);
|
|
EXPECT("chain.match", cpu.equals(gpu));
|
|
}
|
|
PASS("100 audit anchors with parent-root chain");
|
|
}
|
|
|
|
void test_two_engines_match(AIVMGPUEngine* engine)
|
|
{
|
|
if (engine == nullptr) {
|
|
PASS("two-engines (skipped — no GPU)");
|
|
return;
|
|
}
|
|
auto a = AIVMGPUEngine::create();
|
|
auto b = AIVMGPUEngine::create();
|
|
EXPECT("twoeng.a", a != nullptr);
|
|
EXPECT("twoeng.b", b != nullptr);
|
|
|
|
std::vector<AttestationOp> a_ops;
|
|
for (uint64_t i = 1; i <= 32; ++i)
|
|
a_ops.push_back(make_att(i, uint8_t(i % 4u)));
|
|
|
|
std::vector<ModelOp> m_ops;
|
|
for (uint64_t i = 1; i <= 16; ++i) m_ops.push_back(make_model_register(i));
|
|
|
|
auto desc = make_desc(1u);
|
|
auto ra = run_gpu(a.get(), desc, a_ops, m_ops, {});
|
|
auto rb = run_gpu(b.get(), desc, a_ops, m_ops, {});
|
|
EXPECT("twoeng.match", ra.equals(rb));
|
|
PASS("two engines bytewise identical");
|
|
}
|
|
|
|
void test_engine_api_surface(AIVMGPUEngine* engine)
|
|
{
|
|
// Drives run_epoch / poll_round_result / round_active / handle hygiene
|
|
// on whatever GPU backend the harness is currently running.
|
|
if (engine == nullptr) {
|
|
PASS("api-surface (skipped — no GPU)");
|
|
return;
|
|
}
|
|
EXPECT("api.idle.before", engine->round_active() == false);
|
|
|
|
std::vector<AttestationOp> a_ops;
|
|
for (uint64_t i = 1; i <= 4; ++i) a_ops.push_back(make_att(i, 0u));
|
|
|
|
auto desc = make_desc(1u);
|
|
auto h = engine->begin_round(desc);
|
|
EXPECT("api.handle", h.valid());
|
|
EXPECT("api.idle.during", engine->round_active() == true);
|
|
|
|
// Double begin_round must reject without disturbing the active round.
|
|
auto h2 = engine->begin_round(desc);
|
|
EXPECT("api.double_begin", h2.valid() == false);
|
|
|
|
// Bogus handle ops are silent no-ops.
|
|
AIVMRoundHandle bogus{0xDEADBEEFu};
|
|
engine->push_attestation_ops(bogus, a_ops);
|
|
engine->push_model_ops(bogus, std::span<const ModelOp>{});
|
|
engine->push_anchor_ops(bogus, std::span<const AnchorOp>{});
|
|
|
|
engine->push_attestation_ops(h, a_ops);
|
|
auto re = engine->run_epoch(h);
|
|
EXPECT("api.run_epoch.applied", re.attestation_apply_count == 4u);
|
|
EXPECT("api.run_epoch.status", re.status == 1u);
|
|
|
|
auto rp = engine->poll_round_result(h);
|
|
EXPECT("api.poll.match", std::memcmp(rp.aivm_state_root,
|
|
re.aivm_state_root, 32) == 0);
|
|
|
|
auto rp_bogus = engine->poll_round_result(bogus);
|
|
bool all_zero = true;
|
|
for (auto b : rp_bogus.aivm_state_root) if (b != 0) { all_zero = false; break; }
|
|
EXPECT("api.poll.bogus_zero", all_zero);
|
|
|
|
engine->end_round(bogus);
|
|
EXPECT("api.idle.bogus_endround", engine->round_active() == true);
|
|
|
|
engine->end_round(h);
|
|
EXPECT("api.idle.after", engine->round_active() == false);
|
|
PASS("API surface: round_active / run_epoch / poll / handle hygiene");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void run_all_against(const char* label, AIVMGPUEngine* engine)
|
|
{
|
|
std::printf("[%s] %s\n", label,
|
|
engine ? engine->device_name() : "(CPU-only)");
|
|
test_brief_workload(engine);
|
|
test_empty_round(engine);
|
|
test_expiry_excludes(engine);
|
|
test_wrong_attkey_rejected(engine);
|
|
test_anchor_chain(engine);
|
|
test_two_engines_match(engine);
|
|
test_engine_api_surface(engine);
|
|
test_size_sweep(engine);
|
|
}
|
|
|
|
int main(int /*argc*/, char** /*argv*/)
|
|
{
|
|
setvbuf(stdout, nullptr, _IOLBF, 0);
|
|
std::printf("[aivm_determinism_test] starting\n");
|
|
|
|
// -- Default backend (Metal on Apple, CUDA on Linux+CUDA, else nullptr) --
|
|
auto engine = AIVMGPUEngine::create();
|
|
run_all_against("default", engine.get());
|
|
|
|
#if defined(LUX_AIVM_TEST_WGPU)
|
|
// -- WGSL/Dawn backend — runs the same workloads through the WebGPU
|
|
// runtime and compares to the CPU oracle for byte equivalence. --
|
|
auto wgpu_engine = create_aivm_gpu_engine_wgpu();
|
|
if (wgpu_engine == nullptr) {
|
|
std::printf("[wgpu] runtime unavailable — driver compiled but no Dawn/wgpu-native at link\n");
|
|
} else {
|
|
run_all_against("wgpu", wgpu_engine.get());
|
|
}
|
|
#endif
|
|
|
|
std::printf("[aivm_determinism_test] passed=%d failed=%d\n", g_passed, g_failed);
|
|
return g_failed == 0 ? 0 : 1;
|
|
}
|