use std::sync::Arc;
use metrale_scheduler::ScriptedDeviceIo;
use super::model::ModelCfg;
use super::runner::{EOS, ReqSpec, RunOptions, Scenario, run_scenario, run_scenario_with};
use super::scenarios;
use super::tests::{SERIAL, golden_dir};
use crate::scheduler::PipelineFaults;
use crate::scheduler::io::{AsyncDeviceIo, TracingDeviceIo};
const RING_ROWS: usize = 16;
pub(super) fn async_run(sc: &Scenario) -> 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 = AsyncDeviceIo::new(model, RING_ROWS)
.expect("the recording model feeds tokens on the device");
let dev = ScriptedDeviceIo::new(
dev,
|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,
2,
);
Box::new(TracingDeviceIo::new(dev, sink))
}),
)
}
pub(super) fn sync_run(sc: &Scenario) -> Vec<String> {
let _g = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
run_scenario(sc)
}
pub(super) fn client_lines(lines: &[String]) -> Vec<String> {
lines
.iter()
.filter(|l| l.starts_with("out s") || l.starts_with("rotation ack"))
.cloned()
.collect()
}
pub(super) fn count(lines: &[String], needle: &str) -> usize {
lines.iter().filter(|l| l.contains(needle)).count()
}
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 early_stops(name: &'static str, cfg: ModelCfg) -> Scenario {
early_stops_with_prompts(name, cfg, [5, 5, 7])
}
fn early_stops_with_prompts(name: &'static str, cfg: ModelCfg, prompts: [usize; 3]) -> Scenario {
let mut reqs = vec![
ReqSpec::new(1, prompts[0], vec![10, 70, 71, 72, EOS]),
ReqSpec::new(2, prompts[1], vec![20, 80, 81, EOS]),
ReqSpec::new(3, prompts[2], vec![30, 90, 91, 92, 93, EOS]),
];
for r in &mut reqs {
r.max_tokens = 12;
}
Scenario {
name,
cfg,
opts: RunOptions::default(),
reqs,
}
}
#[test]
fn every_golden_scenario_emits_the_same_client_streams_under_the_async_router() {
let mut fed_by_scenario = Vec::new();
for sc in scenarios::all() {
let golden = std::fs::read_to_string(golden_dir().join(format!("{}.trace", sc.name)))
.unwrap_or_else(|e| panic!("golden for {}: {e}", sc.name));
let expected: Vec<String> = golden
.lines()
.filter(|l| l.starts_with("out s") || l.starts_with("rotation ack"))
.map(str::to_string)
.collect();
let live = async_run(&sc);
assert_eq!(
client_lines(&live),
expected,
"{}: the async router changed what a client saw",
sc.name
);
fed_by_scenario.push((sc.name, count(&live, "fx launch DecodeFed")));
}
eprintln!("ahead launches per scenario: {fed_by_scenario:?}");
let fed = |name: &str| {
fed_by_scenario
.iter()
.find(|(n, _)| *n == name)
.map(|(_, c)| *c)
.unwrap_or_else(|| panic!("no scenario {name}"))
};
assert!(fed("plain_decode") > 0, "plain decode never ran ahead");
assert!(fed("slai_policy") > 0);
for depth_one in [
"host_logits_paths",
"watchdog_rollback",
"verify_k2",
"verify_k3",
"verify_k4",
"batched_verify_k4",
"dflash",
"dflash_batched",
"ngram",
"self_spec",
"cancel_mid_stream",
] {
assert_eq!(fed(depth_one), 0, "{depth_one} ran a step ahead");
}
}
#[test]
fn the_suite_fails_when_the_over_run_discard_is_off() {
let sc = early_stops("pipeline_negative_control", ModelCfg::default());
let expected = client_lines(&sync_run(&sc));
let mut control = sc.clone();
control.opts.pipeline_faults = PipelineFaults { keep_overrun: true };
let live = async_run(&control);
assert_ne!(
client_lines(&live),
expected,
"with the discard off the over-run token must reach a client"
);
assert!(
output(&live, 2).contains("n=5") && expected[1].contains("n=4"),
"{}\n{}",
output(&live, 2),
expected[1]
);
assert!(count(&live, "fx launch DecodeFed") > 0);
}
#[test]
fn b_an_over_run_after_a_stop_is_never_emitted() {
let sc = early_stops("pipeline_overrun", ModelCfg::default());
let live = async_run(&sc);
assert_eq!(
output(&live, 1),
"T10 T70 T71 T72 Done(stop, n=5, think=0, cached=0, acc=0, guard=None)"
);
assert_eq!(
output(&live, 2),
"T20 T80 T81 Done(stop, n=4, think=0, cached=0, acc=0, guard=None)"
);
assert_eq!(
output(&live, 3),
"T30 T90 T91 T92 T93 Done(stop, n=6, think=0, cached=0, acc=0, guard=None)"
);
assert_eq!(client_lines(&live), client_lines(&sync_run(&sc)));
assert!(count(&live, "fx launch DecodeFed") >= 3);
assert_eq!(count(&live, "fx apply Rollback"), 2);
for id in 1..=3 {
assert_eq!(output(&live, id).matches("Done(").count(), 1);
}
}
#[test]
fn b_the_rollback_names_exactly_the_stopped_sequence() {
let sc = early_stops("pipeline_rollback_target", ModelCfg::default());
let live = async_run(&sc);
assert_eq!(count(&live, "release_decode_blocks(s2@"), 1);
assert_eq!(count(&live, "release_decode_blocks(s3@"), 1);
assert_eq!(count(&live, "release_decode_blocks(s1@"), 0);
let at = |id: u64| {
live.iter()
.position(|l| l.contains(&format!("release_decode_blocks(s{id}@")))
.expect("release")
};
assert!(at(2) < at(3));
}
#[test]
fn b_the_kv_block_table_reconciles_after_an_over_run() {
let cfg = ModelCfg {
block_size: 4,
..ModelCfg::default()
};
let sc = early_stops_with_prompts("pipeline_kv_reconcile", cfg, [5, 4, 7]);
let live = async_run(&sc);
let sync = sync_run(&sc);
assert_eq!(client_lines(&live), client_lines(&sync));
let released: Vec<&String> = live
.iter()
.filter(|l| l.contains("release_decode_blocks("))
.collect();
assert_eq!(released.len(), 2);
assert!(
released.iter().any(|l| l.ends_with("n=1)")),
"no over-run crossed a block boundary: {released:?}"
);
let frees = |lines: &[String]| -> Vec<String> {
let mut v: Vec<String> = lines
.iter()
.filter(|l| l.starts_with("free_sequence("))
.cloned()
.collect();
v.sort();
v
};
assert_eq!(frees(&live), frees(&sync));
}
#[test]
fn b_readiness_order_does_not_change_emissions() {
let reversed = early_stops(
"pipeline_reversed_completion",
ModelCfg {
reversed_completion: true,
..ModelCfg::default()
},
);
let live = async_run(&reversed);
assert_eq!(client_lines(&live), client_lines(&sync_run(&reversed)));
assert!(count(&live, "fx launch DecodeFed") > 0);
assert!(
count(&live, "event_synchronize(") > 0,
"the older step was never pending"
);
let lagging = early_stops(
"pipeline_lagging_completion",
ModelCfg {
event_lag: 3,
..ModelCfg::default()
},
);
let live = async_run(&lagging);
assert_eq!(client_lines(&live), client_lines(&sync_run(&lagging)));
assert_eq!(count(&live, "event_synchronize("), 0);
}
#[test]
fn c_a_device_fault_mid_pipeline_fails_only_the_rows_it_hit() {
let mut late = ReqSpec::new(3, 5, vec![30, 90, 91, EOS]);
late.arrive_at_tick = Some(1);
let mut reqs = vec![
ReqSpec::new(1, 5, vec![10, 70, 71, 72, 73, EOS]),
ReqSpec::new(2, 5, vec![20, 80, 81, 82, 83, EOS]),
late,
];
for r in &mut reqs {
r.max_tokens = 12;
}
let sc = Scenario {
name: "pipeline_device_fault",
cfg: ModelCfg {
event_query_fault_at: Some(1),
..ModelCfg::default()
},
opts: RunOptions::default(),
reqs,
};
let live = async_run(&sc);
assert_eq!(output(&live, 1), "T10 Error(scripted device fault)");
assert_eq!(output(&live, 2), "T20 Error(scripted device fault)");
assert_eq!(
output(&live, 3),
"T30 T90 T91 Done(stop, n=4, think=0, cached=0, acc=0, guard=None)"
);
assert_eq!(
count(&live, "fx apply ReleaseSeq{cache=false, send_error}"),
2
);
assert_eq!(
count(&live, "fx apply ReleaseSeq{cache=true, finish_sequence}"),
1
);
}
#[test]
fn a_model_without_a_feed_refuses_the_async_router() {
let model = super::model::RecordingModel::new(ModelCfg {
device_token_feed: false,
..ModelCfg::default()
});
let err = AsyncDeviceIo::new(Arc::new(model), RING_ROWS)
.err()
.expect("refused");
assert!(
format!("{err:#}").contains("no device token feed"),
"{err:#}"
);
}