use std::sync::{Arc, Mutex};
use metrale_scheduler::{Fault, ScriptedDeviceIo};
use super::model::ModelCfg;
use super::runner::{EOS, ReqSpec, RunOptions, Scenario, run_scenario, run_scenario_with};
use super::scenarios;
use crate::scheduler::io::{SyncDeviceIo, TracingDeviceIo};
static SERIAL: Mutex<()> = Mutex::new(());
fn scripted(sc: &Scenario, setup: fn(&ScriptedDeviceIo<SyncDeviceIo>)) -> Vec<String> {
let _g = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let vocab = sc.cfg.vocab;
run_scenario_with(
sc,
Arc::new(move |model, sink| {
let dev = ScriptedDeviceIo::new(
SyncDeviceIo::new(model),
|seq: &metrale_model_engine::traits::SequenceState| {
if seq.session_hash != 0 {
seq.session_hash
} else {
u64::from(seq.tokens.first().copied().unwrap_or(0))
}
},
vocab,
1,
);
setup(&dev);
Box::new(TracingDeviceIo::new(dev, sink))
}),
)
}
fn traced(sc: &Scenario) -> Vec<String> {
let _g = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
run_scenario(sc)
}
fn output(lines: &[String], id: u64) -> String {
let key = format!("out s{id}: ");
lines
.iter()
.find_map(|l| l.strip_prefix(&key))
.unwrap_or_else(|| panic!("no output for s{id}"))
.to_string()
}
fn positions(lines: &[String], needle: &str) -> Vec<usize> {
lines
.iter()
.enumerate()
.filter(|(_, l)| l.contains(needle))
.map(|(i, _)| i)
.collect()
}
fn model_toks(n: usize, base: u32) -> Vec<u32> {
(0..n as u32).map(|i| base + i).collect()
}
fn three_streams() -> Scenario {
Scenario {
name: "scripted_three_streams",
cfg: ModelCfg::default(),
opts: RunOptions::default(),
reqs: vec![
ReqSpec::new(1, 5, model_toks(8, 10)),
ReqSpec::new(2, 5, model_toks(8, 20)),
ReqSpec::new(3, 7, model_toks(8, 30)),
],
}
}
#[test]
fn a_decode_greedy_emits_the_device_script_in_order() {
let lines = scripted(&three_streams(), |d| {
d.script(1, [70, 71, 72, EOS]);
d.script(2, [80, 81, EOS]);
d.script(3, [90, 91, 92, 93, EOS]);
});
assert_eq!(
output(&lines, 1),
"T10 T70 T71 T72 Done(stop, n=5, think=0, cached=0, acc=0, guard=None)"
);
assert_eq!(
output(&lines, 2),
"T20 T80 T81 Done(stop, n=4, think=0, cached=0, acc=0, guard=None)"
);
assert_eq!(
output(&lines, 3),
"T30 T90 T91 T92 T93 Done(stop, n=6, think=0, cached=0, acc=0, guard=None)"
);
for l in lines.iter().filter(|l| l.starts_with("out s")) {
assert!(
!l.contains(" T11 ") && !l.contains(" T21 ") && !l.contains(" T31 "),
"{l}"
);
}
}
#[test]
fn a_a_finished_sequence_is_released_before_the_survivors_are_compacted() {
let lines = scripted(&three_streams(), |d| {
d.script(1, [70, EOS]);
d.script(2, [80, 81, 82, EOS]);
d.script(3, [90, 91, 92, 93, 94, EOS]);
});
let releases = positions(&lines, "fx apply ReleaseSeq{cache=true, finish_sequence}");
assert_eq!(releases.len(), 3, "one release per finished sequence");
let compactions = positions(&lines, "fx apply CompactSlot");
let first_compaction = compactions
.first()
.copied()
.expect("a compaction after the first retire");
assert!(releases[0] < first_compaction);
let next_launch = positions(&lines, "fx launch Decode")
.into_iter()
.find(|&i| i > releases[0])
.expect("a decode step after the first retire");
assert!(first_compaction < next_launch);
assert!(lines.iter().any(|l| l == "fx apply EpShutdown"));
assert!(
releases
.iter()
.all(|&r| r < positions(&lines, "fx apply EpShutdown")[0])
);
}
#[test]
fn b_a_deadline_finishes_the_sequence_in_the_tick_it_expires() {
let mut expired = ReqSpec::new(1, 5, model_toks(8, 10));
expired.expired_deadline = true;
let sc = Scenario {
name: "scripted_deadline",
cfg: ModelCfg::default(),
opts: RunOptions::default(),
reqs: vec![expired, ReqSpec::new(2, 5, model_toks(4, 20))],
};
let lines = scripted(&sc, |d| {
d.script(1, [70, 71, 72, 73, 74, 75]);
d.script(2, [80, 81, EOS]);
});
let out1 = output(&lines, 1);
assert!(
out1.ends_with(
"Done(timeout, n=2, think=0, cached=0, acc=0, guard=Some(\"request_timeout\"))"
),
"{out1}"
);
let release = positions(&lines, "fx apply ReleaseSeq{cache=true, finish_sequence}")[0];
let launches = positions(&lines, "fx launch Decode");
let before = launches.iter().filter(|&&i| i < release).count();
assert_eq!(
before, 1,
"exactly one decode step ran before the timeout retire"
);
assert!(output(&lines, 2).ends_with("Done(stop, n=4, think=0, cached=0, acc=0, guard=None)"));
}
#[test]
fn b_a_lora_rotation_waits_for_quiescence() {
let sc = Scenario {
name: "scripted_lora",
cfg: ModelCfg::default(),
opts: RunOptions {
lora_rotation: Some("adapter-y".into()),
..RunOptions::default()
},
reqs: vec![ReqSpec::new(1, 5, model_toks(6, 10))],
};
let lines = scripted(&sc, |d| d.script(1, [70, 71, 72, EOS]));
let lora = positions(&lines, "fx lora Rotate(adapter-y)");
assert_eq!(lora.len(), 1);
let release = positions(&lines, "fx apply ReleaseSeq{cache=true, finish_sequence}");
assert_eq!(release.len(), 1);
assert!(
release[0] < lora[0],
"the rotation waits for the active sequence to retire"
);
assert!(lines.iter().any(|l| l == "rotation ack: Done"));
}
#[test]
fn b_the_watchdog_restores_the_boundary_snapshot_before_resteering() {
let sc = scenarios::all()
.into_iter()
.find(|s| s.name == "watchdog_rollback")
.expect("scenario");
let lines = traced(&sc);
let saves = positions(&lines, "fx apply SsmSnapshotSave{slot=0}");
let restores = positions(&lines, "fx apply SsmSnapshotRestore{slot=0}");
assert!(!restores.is_empty(), "the content-loop watchdog re-steered");
assert!(
saves[0] < restores[0],
"a boundary snapshot exists before it is restored"
);
let out = output(&lines, 1);
assert!(out.contains(", 12, "), "{out}");
}
#[test]
fn c_kv_exhaustion_preempts_one_victim_and_the_rest_continue() {
let lines = scripted(&three_streams(), |d| {
d.script(1, [70, 71, 72, EOS]);
d.script(2, [80, 81, EOS]);
d.script(3, [90, 91, 92, EOS]);
d.fail_next(Fault::KvExhausted);
});
let requeues = positions(&lines, "fx apply ReleaseSeq{cache=true, preempt_requeue}");
assert_eq!(requeues.len(), 1, "exactly one victim is requeued");
let launches = positions(&lines, "fx launch Decode{n=");
let retry = launches
.iter()
.find(|&&i| i > requeues[0])
.expect("a retry after the preemption");
assert!(
lines[*retry].starts_with("fx launch Decode{n=2"),
"{}",
lines[*retry]
);
for id in 1..=3 {
let out = output(&lines, id);
assert!(!out.contains("Error"), "{out}");
assert!(out.contains("Done(stop"), "{out}");
}
}
#[test]
fn c_a_fatal_device_error_fails_every_row_in_the_batch_and_only_them() {
let mut late = ReqSpec::new(3, 5, model_toks(6, 30));
late.arrive_at_tick = Some(1);
let sc = Scenario {
name: "scripted_fatal",
cfg: ModelCfg::default(),
opts: RunOptions::default(),
reqs: vec![
ReqSpec::new(1, 5, model_toks(6, 10)),
ReqSpec::new(2, 5, model_toks(6, 20)),
late,
],
};
let lines = scripted(&sc, |d| {
d.script(3, [90, 91, EOS]);
d.fail_next(Fault::Fatal);
});
assert_eq!(output(&lines, 1), "T10 Error(scripted device fault)");
assert_eq!(output(&lines, 2), "T20 Error(scripted device fault)");
assert_eq!(
positions(&lines, "fx apply ReleaseSeq{cache=false, send_error}").len(),
2
);
assert_eq!(
output(&lines, 3),
"T30 T90 T91 Done(stop, n=4, think=0, cached=0, acc=0, guard=None)"
);
}