use std::path::Path;
use anyhow::{Context, Result, bail, ensure};
use metrale_model_engine::traits::{ForwardSelect, Model, SequenceState};
use serde::Serialize;
use super::{Comparison, Run, Timing, argmax_bf16, compare, logits, timing};
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct Step {
pub tokens: Vec<u32>,
pub accepted: usize,
}
enum Feed<'a> {
Reference { greedy: &'a [u32] },
Drafter,
Replay {
steps: &'a [Step],
bootstrap: u32,
mtp: bool,
},
}
impl Feed<'_> {
fn mtp(&self) -> bool {
matches!(self, Feed::Drafter | Feed::Replay { mtp: true, .. })
}
}
fn bytes_of(tokens: &[u32]) -> impl Iterator<Item = u8> + '_ {
tokens.iter().flat_map(|t| t.to_le_bytes())
}
fn propose(
model: &dyn Model,
seq: &mut SequenceState,
last: u32,
k: usize,
out: &mut Vec<u8>,
) -> Result<Vec<u32>> {
let drafts = model.run_mtp_propose_multi(last, seq.seq_len, k - 1, seq, 0, None)?;
ensure!(
drafts.len() == k - 1,
"the drafter proposed {} of {}",
drafts.len(),
k - 1
);
out.extend(logits(model, model.logits_buffer_ptr())?);
out.extend(bytes_of(&drafts));
Ok(drafts)
}
pub(crate) fn drafts(greedy: &[u32], p: usize, s: usize, k: usize, vocab: u32) -> Vec<u32> {
(1..k)
.map(|j| {
let t = greedy.get(p + j).copied().unwrap_or(0);
if s % 2 == 1 && j == 1 + s % (k - 1) {
(t + 1) % vocab
} else {
t
}
})
.collect()
}
pub(crate) fn accepted(tokens: &[u32], outs: &[u32]) -> usize {
(1..tokens.len())
.take_while(|&j| outs[j - 1] == tokens[j])
.count()
}
fn verify(model: &dyn Model, tokens: &[u32], seq: &mut SequenceState) -> Result<Vec<u32>> {
Ok(match tokens.len() {
2 => model
.decode_verify_graphed(&[tokens[0], tokens[1]], seq, 0)?
.to_vec(),
3 => model
.decode_verify_graphed_k3(&[tokens[0], tokens[1], tokens[2]], seq, 0)?
.to_vec(),
4 => model
.decode_verify_graphed_k4(&[tokens[0], tokens[1], tokens[2], tokens[3]], seq, 0)?
.to_vec(),
k => bail!("no single-sequence verify for K={k}"),
})
}
pub(crate) fn continuation(model: &dyn Model, prompt: &[u32], n: usize) -> Result<Vec<u32>> {
let mut seq = model.alloc_sequence()?;
let result = (|| {
let mut out = vec![argmax_bf16(&logits(
model,
model.prefill(prompt, &mut seq, 0)?,
)?)];
while out.len() < n {
let l = logits(model, model.decode(out[out.len() - 1], &mut seq, 0)?)?;
out.push(argmax_bf16(&l));
}
Ok(out)
})();
model.free_sequence(&mut seq)?;
result
}
fn run_verify(
model: &dyn Model,
prompt: &[u32],
k: usize,
steps: usize,
feed: Feed<'_>,
) -> Result<(Vec<Step>, Run, u32)> {
let vocab = model.vocab_size();
let mut seq = model.alloc_sequence()?;
let result = (|| {
let mut prefill = logits(model, model.prefill(prompt, &mut seq, 0)?)?;
let mut last = argmax_bf16(&prefill);
let mut next = Vec::new();
if feed.mtp() {
let l = logits(model, model.decode(last, &mut seq, 0)?)?;
last = match feed {
Feed::Replay { bootstrap, .. } => bootstrap,
_ => argmax_bf16(&l),
};
prefill.extend(l);
model.save_hidden_for_mtp(0, 0)?;
next = propose(model, &mut seq, last, k, &mut prefill)?;
}
let bootstrap = last;
let (mut p, mut log, mut out) = (0usize, Vec::new(), Vec::new());
let (mut enqueue_ms, mut step_ms) = (Vec::new(), Vec::new());
for s in 0..steps {
let tokens: Vec<u32> = match feed {
Feed::Reference { greedy } => std::iter::once(last)
.chain(drafts(greedy, p, s, k, vocab as u32))
.collect(),
Feed::Drafter => std::iter::once(last).chain(next.iter().copied()).collect(),
Feed::Replay { steps, .. } => steps[s].tokens.clone(),
};
model.sync_secondary()?;
let t0 = std::time::Instant::now();
let outs = verify(model, &tokens, &mut seq)?;
let t1 = std::time::Instant::now();
let mut l = vec![0u8; k * vocab * 2];
model.copy_logits_to_host(model.logits_buffer_ptr(), &mut l)?;
enqueue_ms.push((t1 - t0).as_secs_f64() * 1e3);
step_ms.push(t0.elapsed().as_secs_f64() * 1e3);
let na = match feed {
Feed::Replay { steps, .. } => steps[s].accepted,
_ => accepted(&tokens, &outs),
};
let rejected = k - 1 - na;
seq.seq_len -= rejected;
let keep = seq.tokens.len() - rejected;
seq.tokens.truncate(keep);
if feed.mtp() && rejected > 0 {
model.trim_proposer_state(&mut seq, na, 0)?;
}
model.commit_accepted_prefix(&mut seq, na + 1, k)?;
last = match feed {
Feed::Replay { steps, .. } => steps.get(s + 1).map_or(outs[na], |n| n.tokens[0]),
_ => outs[na],
};
p += na + 1;
if feed.mtp() {
model.save_hidden_for_mtp(na, 0)?;
if rejected == 0 {
model.trim_proposer_state(&mut seq, na, 0)?;
}
next = propose(model, &mut seq, last, k, &mut l)?;
}
log.push(Step {
tokens,
accepted: na,
});
out.push(l);
}
model.sync_secondary()?;
Ok((
log,
Run {
prefill,
steps: out,
tokens: Vec::new(),
enqueue_ms,
step_ms,
},
bootstrap,
))
})();
model.free_sequence(&mut seq)?;
result
}
#[derive(Debug, Serialize)]
pub(crate) struct VerifyReport {
pub k: usize,
pub accepted: Vec<usize>,
pub comparisons: Vec<Comparison>,
pub timings: Vec<Timing>,
pub detection_control: Comparison,
}
pub(crate) fn diff_verify(
model: &dyn Model,
prompt: &[u32],
ks: &[usize],
steps: usize,
mtp: bool,
forwards: &[(&'static str, ForwardSelect)],
) -> Result<Vec<VerifyReport>> {
ensure!(!forwards.is_empty(), "no forward to compare");
let mut out = Vec::with_capacity(ks.len());
for &k in ks {
model.set_forward(&ForwardSelect::Legacy)?;
let greedy;
let feed = if mtp {
Feed::Drafter
} else {
greedy = continuation(model, prompt, steps * k + 1)?;
Feed::Reference { greedy: &greedy }
};
if mtp && out.is_empty() {
run_verify(model, prompt, k, steps, Feed::Drafter)?;
}
let (log, reference, bootstrap) = run_verify(model, prompt, k, steps, feed)?;
let replay = |steps| Feed::Replay {
steps,
bootstrap,
mtp,
};
let (mut comparisons, mut timings) = (Vec::new(), vec![timing("legacy", &reference)]);
for (name, sel) in forwards {
model.set_forward(sel)?;
let (_, r, _) = run_verify(model, prompt, k, steps, replay(&log))?;
let c = compare(name, &reference, &r);
tracing::info!("circuit diff: verify K={k} {name}: {c:?}");
timings.push(timing(name, &r));
comparisons.push(c);
}
let mut changed = log.clone();
let at = steps / 2;
changed[at].tokens[k - 1] = (changed[at].tokens[k - 1] + 1) % model.vocab_size() as u32;
let (_, r, _) = run_verify(model, prompt, k, steps, replay(&changed))?;
out.push(VerifyReport {
k,
accepted: log.iter().map(|s| s.accepted).collect(),
comparisons,
timings,
detection_control: compare("last forward, one draft changed", &reference, &r),
});
}
model.set_forward(&ForwardSelect::Legacy)?;
Ok(out)
}
pub(crate) fn verify_failures(reports: &[VerifyReport]) -> Vec<String> {
let mut out = Vec::new();
for r in reports {
for c in &r.comparisons {
if !c.prefill_equal || c.mismatched_steps > 0 {
out.push(format!(
"verify K={}, {}: prefill equal {}, {} of {} steps differ (first at {:?})",
r.k, c.variant, c.prefill_equal, c.mismatched_steps, c.steps, c.first_mismatch
));
}
}
if r.detection_control.mismatched_steps == 0 {
out.push(format!(
"verify K={}: the detection control saw no difference after changing a draft",
r.k
));
}
let full = r.accepted.iter().filter(|&&a| a == r.k - 1).count();
if full == 0 || full == r.accepted.len() {
out.push(format!(
"verify K={}: the reference accepted {:?}; it needs both full and partial accepts",
r.k, r.accepted
));
}
}
out
}
#[derive(Debug, Serialize)]
struct VerifyDiffReport {
graphs: &'static str,
mtp: bool,
steps: usize,
variants: Vec<super::Variant>,
verify: Vec<VerifyReport>,
verdict: &'static str,
reasons: Vec<String>,
}
pub(crate) fn verify_report(
model: &dyn Model,
prompt: &[u32],
(ks, mtp): (&[usize], bool),
steps: usize,
forwards: &[(&'static str, ForwardSelect)],
out: &Path,
) -> Result<()> {
let variants = super::disclosed(model, forwards)?;
let verify = diff_verify(model, prompt, ks, steps, mtp, forwards)?;
let reasons = verify_failures(&verify);
let report = VerifyDiffReport {
graphs: if std::env::var("METRALE_DEBUG_NO_GRAPH").as_deref() == Ok("1") {
"eager"
} else {
"graphed"
},
mtp,
steps,
variants,
verify,
verdict: if reasons.is_empty() { "PASS" } else { "FAIL" },
reasons: reasons.clone(),
};
std::fs::write(out, serde_json::to_vec_pretty(&report)?)
.with_context(|| format!("writing {}", out.display()))?;
println!("{}", serde_json::to_string_pretty(&report)?);
if !reasons.is_empty() {
bail!("circuit diff FAILED:\n {}", reasons.join("\n "));
}
Ok(())
}