use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::Arc;
use fuel_ir::DType;
use fuel_ir::dispatch::OpKind;
use fuel_ir::probe::BackendId;
use fuel_vulkan_backend::VulkanBackend;
use super::bit_stability::{KernelInvoker, VerifyError, VerifyOutcome, verify_bit_stability};
use super::invoker_cpu::CpuInvoker;
use super::invoker_vulkan::VulkanInvoker;
use super::ledger::{LedgerRecord, VerificationLedger};
use super::probe_recipes::{build_primitive_probe, probe_seed};
use crate::kernel::KernelBindingTable;
#[allow(
dead_code,
reason = "GAP-236 (unpublished verify API): fkc::verify's modules are private, so nothing outside this crate can reach it. Does NOT retire itself -- the expiry lives in GAP-236 and in Unpopped's handback precondition guard, which fires on their side when the API is named."
)]
const ITERS: usize = 16;
#[allow(
dead_code,
reason = "GAP-236 (unpublished verify API): fkc::verify's modules are private, so nothing outside this crate can reach it. Does NOT retire itself -- the expiry lives in GAP-236 and in Unpopped's handback precondition guard, which fires on their side when the API is named."
)]
const CLAIM: &str = "bit_stable_on_same_hardware";
#[allow(
dead_code,
reason = "GAP-236 (unpublished verify API): fkc::verify's modules are private, so nothing outside this crate can reach it. Does NOT retire itself -- the expiry lives in GAP-236 and in Unpopped's handback precondition guard, which fires on their side when the API is named."
)]
const BYTE_EXACT_OPS: &[OpKind] = &[
OpKind::Copy,
OpKind::Flip,
OpKind::Roll,
OpKind::Triu,
OpKind::Tril,
OpKind::Gather,
OpKind::IndexSelect,
OpKind::MaskedFill,
OpKind::Pad,
OpKind::PadBackward,
OpKind::WriteSlice,
OpKind::WriteSliceRotating,
OpKind::Concat,
OpKind::Cast,
OpKind::ArgMaxDim,
OpKind::ArgMinDim,
];
#[derive(Debug)]
#[allow(
dead_code,
reason = "GAP-236 (unpublished verify API): fkc::verify's modules are private, so nothing outside this crate can reach it. Does NOT retire itself -- the expiry lives in GAP-236 and in Unpopped's handback precondition guard, which fires on their side when the API is named."
)]
pub struct VulkanSeedAttempt {
pub op: String,
pub dtypes: Vec<DType>,
pub kernel_revision_hash: u64,
pub outcome: String,
}
#[allow(
dead_code,
reason = "GAP-236 (unpublished verify API): fkc::verify's modules are private, so nothing outside this crate can reach it. Does NOT retire itself -- the expiry lives in GAP-236 and in Unpopped's handback precondition guard, which fires on their side when the API is named."
)]
fn verified_at_string() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
format!("epoch:{secs}")
}
#[allow(
dead_code,
reason = "GAP-236 (unpublished verify API): fkc::verify's modules are private, so nothing outside this crate can reach it. Does NOT retire itself -- the expiry lives in GAP-236 and in Unpopped's handback precondition guard, which fires on their side when the API is named."
)]
pub fn run_vulkan_verification(
force: bool,
) -> std::result::Result<(VerificationLedger, Vec<VulkanSeedAttempt>), VerifyError> {
let (_, _, ledger, log) =
run_vulkan_verification_on(fuel_vulkan_backend::DeviceSelection::PreferDiscrete, force)?;
Ok((ledger, log))
}
#[allow(
dead_code,
reason = "GAP-236 (unpublished verify API): fkc::verify's modules are private, so nothing outside this crate can reach it. Does NOT retire itself -- the expiry lives in GAP-236 and in Unpopped's handback precondition guard, which fires on their side when the API is named."
)]
pub fn run_vulkan_verification_on(
selection: fuel_vulkan_backend::DeviceSelection,
force: bool,
) -> std::result::Result<(String, usize, VerificationLedger, Vec<VulkanSeedAttempt>), VerifyError> {
let mut table = KernelBindingTable::new();
crate::vulkan_dispatch::register_vulkan_kernels(&mut table);
let mut ledger =
VerificationLedger::from_records(VerificationLedger::embedded().records().to_vec());
let backend = VulkanBackend::with_selection(selection)
.map(Arc::new)
.map_err(|e| VerifyError::Backend(format!("no Vulkan device: {e}")))?;
let device_name = backend.device_name.clone();
let gpu_id = backend.gpu_id;
let mut log = Vec::new();
for (op, dtypes, backend_id, entry) in table.iter_entries() {
if backend_id != BackendId::Vulkan {
continue;
}
let dtypes_vec = dtypes.to_vec();
let rev = entry.kernel_revision_hash;
if !force && ledger.has_pass(BackendId::Vulkan, dtypes, rev, CLAIM) {
log.push(VulkanSeedAttempt {
op: format!("{op:?}"),
dtypes: dtypes_vec,
kernel_revision_hash: rev,
outcome: "skip: already has a pass".to_string(),
});
continue;
}
let probe = match build_primitive_probe(op, dtypes, probe_seed(op, dtypes)) {
Some(p) => p,
None => {
log.push(VulkanSeedAttempt {
op: format!("{op:?}"),
dtypes: dtypes_vec,
kernel_revision_hash: rev,
outcome: "skip: no probe recipe".to_string(),
});
continue;
}
};
if probe.out_seed.is_some() {
log.push(VulkanSeedAttempt {
op: format!("{op:?}"),
dtypes: dtypes_vec,
kernel_revision_hash: rev,
outcome: "skip: probe needs a seeded output, VulkanInvoker cannot seed".to_string(),
});
continue;
}
let inv = VulkanInvoker::new(backend.clone(), probe.out_dtype, probe.out_shape.clone())
.with_params(probe.params.clone());
let inputs = probe.inputs.clone();
let attempt = catch_unwind(AssertUnwindSafe(|| {
verify_bit_stability(&inv, entry, std::slice::from_ref(&inputs), ITERS)
}));
let (result, outcome) = match attempt {
Ok(Ok(VerifyOutcome::Pass)) => (Some("pass"), "pass".to_string()),
Ok(Ok(VerifyOutcome::Fail { detail })) => (Some("fail"), format!("fail: {detail}")),
Ok(Ok(VerifyOutcome::NoReference)) => (None, "skip: no reference".to_string()),
Ok(Err(e)) => (Some("fail"), format!("fail: invoke error {e:?}")),
Err(_) => (Some("fail"), "fail: kernel invocation panicked".to_string()),
};
if let Some(result) = result {
ledger.upsert(LedgerRecord {
kernel_ref: entry.kernel_source.to_string(),
backend: "Vulkan".to_string(),
dtypes: dtypes.iter().map(|d| format!("{d:?}")).collect(),
kernel_revision_hash: rev,
claim: CLAIM.to_string(),
result: result.to_string(),
verified_at: verified_at_string(),
protocol_version: 1,
evidence: serde_json::json!({ "repeat_calls": ITERS, "harness": "v-fkc-9/seed_vulkan_ledger" }),
});
}
log.push(VulkanSeedAttempt {
op: format!("{op:?}"),
dtypes: dtypes_vec,
kernel_revision_hash: rev,
outcome,
});
}
let mut cpu_table = KernelBindingTable::new();
crate::dispatch::register_cpu_kernels(&mut cpu_table);
for (op, dtypes, backend_id, vk_entry) in table.iter_entries() {
if backend_id != BackendId::Vulkan || !BYTE_EXACT_OPS.contains(&op) {
continue;
}
let dtypes_vec = dtypes.to_vec();
let rev = vk_entry.kernel_revision_hash;
if !force && ledger.has_pass(BackendId::Vulkan, dtypes, rev, "max_ulp") {
continue;
}
let Some(probe) = build_primitive_probe(op, dtypes, probe_seed(op, dtypes)) else {
continue;
};
let cpu_entry = cpu_table
.iter_entries()
.find(|(o, d, b, _)| *o == op && *d == dtypes && *b == BackendId::Cpu)
.map(|(_, _, _, e)| e);
let Some(cpu_entry) = cpu_entry else {
if ledger.has_pass(BackendId::Vulkan, dtypes, rev, CLAIM) {
for claim in ["max_ulp", "max_relative", "max_absolute"] {
ledger.upsert(LedgerRecord {
kernel_ref: vk_entry.kernel_source.to_string(),
backend: "Vulkan".to_string(),
dtypes: dtypes.iter().map(|d| format!("{d:?}")).collect(),
kernel_revision_hash: rev,
claim: claim.to_string(),
result: "pass".to_string(),
verified_at: verified_at_string(),
protocol_version: 1,
evidence: serde_json::json!({
"bound": 0,
"basis": "byte-mover-determinism",
"note": "no CPU reference for this integer dtype; byte-exact from bit_stable determinism + no-arithmetic byte-mover contract + shared byte-width kernel (float variant diffed vs CPU)"
}),
});
}
log.push(VulkanSeedAttempt {
op: format!("{op:?}"),
dtypes: dtypes_vec,
kernel_revision_hash: rev,
outcome: "max_ulp pass (byte-mover; no CPU ref, structural)".to_string(),
});
} else {
log.push(VulkanSeedAttempt {
op: format!("{op:?}"),
dtypes: dtypes_vec,
kernel_revision_hash: rev,
outcome: "max_ulp skip: no CPU reference and no bit_stable pass".to_string(),
});
}
continue;
};
if probe.out_seed.is_some() {
log.push(VulkanSeedAttempt {
op: format!("{op:?}"),
dtypes: dtypes_vec,
kernel_revision_hash: rev,
outcome: "max_ulp skip: probe needs a seeded output, VulkanInvoker cannot seed"
.to_string(),
});
continue;
}
let cand = VulkanInvoker::new(backend.clone(), probe.out_dtype, probe.out_shape.clone())
.with_params(probe.params.clone());
let refr = CpuInvoker::new(probe.out_dtype, probe.out_shape.clone())
.with_params(probe.params.clone());
let inputs = probe.inputs.clone();
let attempt = catch_unwind(AssertUnwindSafe(
|| -> std::result::Result<bool, VerifyError> {
let a = cand.invoke(vk_entry, &inputs)?;
let b = refr.invoke(cpu_entry, &inputs)?;
Ok(a.bytes == b.bytes)
},
));
let (result, outcome) = match attempt {
Ok(Ok(true)) => (Some("pass"), "max_ulp pass".to_string()),
Ok(Ok(false)) => (
Some("fail"),
"max_ulp fail: bytes differ from CPU reference".to_string(),
),
Ok(Err(e)) => (Some("fail"), format!("max_ulp invoke error {e:?}")),
Err(_) => (Some("fail"), "max_ulp panicked".to_string()),
};
if let Some(result) = result {
for claim in ["max_ulp", "max_relative", "max_absolute"] {
ledger.upsert(LedgerRecord {
kernel_ref: vk_entry.kernel_source.to_string(),
backend: "Vulkan".to_string(),
dtypes: dtypes.iter().map(|d| format!("{d:?}")).collect(),
kernel_revision_hash: rev,
claim: claim.to_string(),
result: result.to_string(),
verified_at: verified_at_string(),
protocol_version: 1,
evidence: serde_json::json!({ "bound": 0, "reference": "cpu", "harness": "v-fkc-9/seed_vulkan_ledger" }),
});
}
}
log.push(VulkanSeedAttempt {
op: format!("{op:?}"),
dtypes: dtypes_vec,
kernel_revision_hash: rev,
outcome,
});
}
Ok((device_name, gpu_id, ledger, log))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore = "cross-vendor comparison: needs a second Vulkan adapter + --features vulkan"]
fn masked_fill_cross_vendor_second_adapter_verify_only() {
use fuel_vulkan_backend::DeviceSelection;
let run = |sel: DeviceSelection, want_vendor: u32, label: &str| {
let (name, gpu_id, _ledger, log) = match run_vulkan_verification_on(sel, true) {
Ok(v) => v,
Err(e) => {
println!("[x-vendor] {label}: cannot run: {e:?}");
return None;
}
};
let descs =
fuel_vulkan_backend::probe::enumerate_devices().expect("vulkan probe enumerates");
let d = descs
.iter()
.find(|d| d.device_index as usize == gpu_id)
.unwrap_or_else(|| panic!("no descriptor for gpu_id {gpu_id}"))
.clone();
println!(
"[x-vendor] {label}: gpu_id={gpu_id} name={name:?} sku={:?} vendor=0x{:04x}",
d.hardware_sku, d.vendor_id,
);
assert_eq!(
d.vendor_id, want_vendor,
"{label}: wanted vendor {want_vendor:#06x}, got {:#06x} ({}). A silent \
fallback would make this a same-vendor run that still passes.",
d.vendor_id, d.hardware_sku,
);
let mf: Vec<(Vec<fuel_ir::DType>, String)> = log
.iter()
.filter(|a| a.op == "MaskedFill")
.map(|a| (a.dtypes.clone(), a.outcome.clone()))
.collect();
for (dt, out) in &mf {
println!("[x-vendor] {label}: MaskedFill {dt:?}: {out}");
}
Some((d, mf))
};
let Some((amd_dev, amd)) = run(DeviceSelection::ByName("AMD".to_string()), 0x1002, "AMD")
else {
println!("[x-vendor] second adapter unavailable — reporting and stopping.");
return;
};
let Some((nv_dev, nv)) = run(DeviceSelection::PreferDiscrete, 0x10de, "NVIDIA") else {
println!("[x-vendor] discrete adapter unavailable — cannot compare.");
return;
};
assert!(
!amd.is_empty() && !nv.is_empty(),
"a comparison over zero MaskedFill attempts is vacuous — `force` did not take effect"
);
let key = |v: &Vec<(Vec<fuel_ir::DType>, String)>| {
let mut k: Vec<String> = v.iter().map(|(d, _)| format!("{d:?}")).collect();
k.sort();
k
};
assert_eq!(
key(&amd),
key(&nv),
"⚠️ the two adapters attempted DIFFERENT dtype sets, so their outcomes are not \
comparable. AMD={:?} NVIDIA={:?}",
key(&amd),
key(&nv),
);
let fails = |v: &Vec<(Vec<fuel_ir::DType>, String)>| -> Vec<_> {
v.iter()
.filter(|(_, o)| o.contains("fail"))
.cloned()
.collect()
};
println!(
"[x-vendor] {} attempts each — {} ({} failing) vs {} ({} failing)",
amd.len(),
amd_dev.hardware_sku,
fails(&amd).len(),
nv_dev.hardware_sku,
fails(&nv).len(),
);
assert!(
fails(&amd).is_empty() && fails(&nv).is_empty(),
"⚠️ DIVERGENCE: masked_fill is contract-claimed 'bit-identical across any \
hardware'. AMD failures={:?}; NVIDIA failures={:?}. The CONTRACT'S CLAIM is \
then wrong, which outranks any ledger record — report, do not reconcile.",
fails(&amd),
fails(&nv),
);
}
#[test]
#[ignore = "re-seeding tool: writes the verified ledger; needs a live Vulkan device + --features vulkan"]
fn seed_vulkan_verified_ledger() {
let (ledger, log) = run_vulkan_verification(true).expect("vulkan seeding runs");
for a in &log {
println!(
"[v-fkc-9] {} {:?} (rev={}): {}",
a.op, a.dtypes, a.kernel_revision_hash, a.outcome
);
}
let passed = log
.iter()
.filter(|a| a.outcome == "pass" || a.outcome == "max_ulp pass")
.count();
let failed = log
.iter()
.filter(|a| a.outcome.starts_with("fail") || a.outcome.contains("fail"))
.count();
let skipped = log
.iter()
.filter(|a| a.outcome.starts_with("skip") || a.outcome.contains("skip"))
.count();
println!(
"[v-fkc-9] {passed} passed, {failed} failed, {skipped} skipped, {} attempts",
log.len()
);
assert!(
passed > 0,
"expected at least one Vulkan kernel to verify; got 0 — see log"
);
let vk_passes = ledger
.records()
.iter()
.filter(|r| r.backend == "Vulkan" && r.result == "pass")
.count();
println!("[v-fkc-9] ledger now holds {vk_passes} Vulkan pass records");
let summary = super::super::ledger::write_merged_ledger(ledger.records());
println!(
"[v-fkc-9] merged {} record(s) into {} existing -> {} total, written to {}",
summary.fresh,
summary.before,
summary.after,
summary.path.display(),
);
}
}