Download engine/crates/runtime/src/tests/engine.rs from respanai/span-02: direct link, hf CLI and curl.
- Browser
- Download file 21.2 kB
-
https://huggingface.co/respanai/span-02/resolve/main/engine/crates/runtime/src/tests/engine.rs
- Command line
-
hf download hf://respanai/span-02/engine/crates/runtime/src/tests/engine.rs
-
curl -L -o engine.rs https://huggingface.co/respanai/span-02/resolve/main/engine/crates/runtime/src/tests/engine.rs
21.2 kB
| use super::*; | |
| use dllm_core::policy::{GateMode, Policy}; | |
| use dllm_core::serving::{Item, ItemResult}; | |
| use std::sync::atomic::Ordering::Relaxed; | |
| fn cached_prefix_equals_cold_and_skips_rows() { | |
| let prefix = toks(1, 200); | |
| let items: Vec<Item> = (0..4).map(|q| text_item(&format!("q{q}"), &prefix, &toks(10 + q, 20), 3)).collect(); | |
| let (cold, _) = engine(32768, 2048, 512, |c| c.prefix_cache = false); | |
| let (warm, ctl) = engine(32768, 2048, 512, |_| {}); | |
| let want = cold.run(&items, &[], &think_pol()).unwrap(); | |
| let r1 = warm.run(&items, &[], &think_pol()).unwrap(); | |
| let rows1 = warm.stats().prefill_rows; | |
| let r2 = warm.run(&items, &[], &think_pol()).unwrap(); | |
| let rows2 = warm.stats().prefill_rows - rows1; | |
| for ((a, b), c) in want.iter().zip(&r1).zip(&r2) { | |
| assert_eq!(a.p0, b.p0); | |
| assert_eq!(a.p0, c.p0); | |
| assert_eq!(a.think, b.think); | |
| assert_eq!(a.think, c.think); | |
| assert_eq!((a.trace_tokens, a.out_tokens), (c.trace_tokens, c.out_tokens)); | |
| } | |
| // 4 identical-prefix questions: first run computes the shared rows once, second run only the partial tails (<= 16 + 2 rows per branch) | |
| assert!(rows1 < 4 * 230, "shared prefix must be computed once, rows {rows1}"); | |
| assert!(rows2 <= 4 * 34, "cached rerun computes only the tail, rows {rows2}"); | |
| assert!(warm.stats().hit_rate() > 0.4); | |
| let _ = ctl; | |
| } | |
| fn image_identity_does_not_alias_in_cache() { | |
| let mut imgs = vec![]; | |
| for k in 0..3u8 { | |
| imgs.push(vec![7u8, k, 1, 2, 3]); // same n_soft, different content | |
| } | |
| let prefix: Vec<u32> = { | |
| let mut p = toks(5, 20); | |
| p.insert(3, 258880); | |
| p | |
| }; | |
| let item = text_item("q", &prefix, &toks(77, 40), 2); | |
| let (shared, _) = engine(32768, 2048, 512, |_| {}); | |
| let mut got = vec![]; | |
| for im in &imgs { | |
| got.push(shared.run(&[item.clone()], &[im.clone()], &think_pol()).unwrap().remove(0)); | |
| } | |
| for (k, im) in imgs.iter().enumerate() { | |
| let (fresh, _) = engine(32768, 2048, 512, |c| c.prefix_cache = false); | |
| let want = fresh.run(&[item.clone()], &[im.clone()], &think_pol()).unwrap().remove(0); | |
| assert_eq!(want.p0, got[k].p0, "image {k} aliased in the prefix cache"); | |
| } | |
| assert_ne!(got[0].p0, got[1].p0); | |
| // same image again => cache hit and same result; the encoded soft tokens are reused | |
| let again = shared.run(&[item.clone()], &[imgs[1].clone()], &think_pol()).unwrap().remove(0); | |
| assert_eq!(again.p0, got[1].p0); | |
| assert!(shared.stats().soft_cache_hits + shared.stats().cache_hit_pages > 0); | |
| } | |
| fn image_block_straddling_chunk_and_shared_boundary() { | |
| // long image (n_soft = 40) + short state: shared boundary falls inside the image block for some lengths | |
| for pre in 0..20u32 { | |
| let mut prefix = toks(9, 3 + pre as usize); | |
| prefix.push(258880); | |
| prefix.extend(toks(4, 5)); | |
| let items: Vec<Item> = (0..3).map(|q| text_item(&format!("q{q}"), &prefix, &toks(30 + q, 25 + q as usize), 2)).collect(); | |
| let img = vec![39u8, 5, 6, 7]; | |
| let (a, _) = engine(32768, 2048, 512, |_| {}); | |
| let (b, _) = engine(32768, 2048, 48, |c| { c.prefix_cache = false; c.max_prefill_rows = 48 }); | |
| let ra = a.run(&items, &[img.clone()], &think_pol()).unwrap(); | |
| let rb = b.run(&items, &[img.clone()], &think_pol()).unwrap(); | |
| for (x, y) in ra.iter().zip(&rb) { | |
| assert_eq!((x.p0.clone(), x.think.clone()), (y.p0.clone(), y.think.clone()), "pre {pre}"); | |
| } | |
| } | |
| } | |
| fn bad_image_is_an_error() { | |
| let (e, _) = engine(32768, 256, 256, |_| {}); | |
| let item = text_item("q", &{ let mut p = toks(2, 10); p.push(258880); p }, &toks(3, 5), 2); | |
| let err = e.run(&[item.clone()], &[b"BAD image".to_vec()], &think_pol()).unwrap_err(); | |
| assert!(format!("{err:#}").contains("cannot decode image"), "{err:#}"); | |
| // engine still works | |
| assert!(e.run(&[item], &[vec![3u8, 1, 1]], &think_pol()).is_ok()); | |
| // placeholder / image count mismatch | |
| let plain = text_item("q", &toks(2, 10), &toks(3, 5), 2); | |
| assert!(e.run(&[plain], &[vec![3u8, 1, 1]], &think_pol()).is_err()); | |
| } | |
| fn kv_exhaustion_is_an_error_not_a_panic_and_recovers() { | |
| let (e, _) = engine(32768, 8, 512, |_| {}); | |
| let big = text_item("big", &toks(1, 400), &toks(2, 10), 2); | |
| let err = e.run(&[big], &[], &think_pol()).unwrap_err(); | |
| assert!(format!("{err:#}").contains("KV pool exhausted"), "{err:#}"); | |
| // larger than the whole pool: reported as a capacity limit (server: 422 "maximum context length"), not a transient shortage | |
| assert!(format!("{err:#}").contains("KV cache capacity exceeded"), "{err:#}"); | |
| let small = text_item("s", &toks(1, 20), &toks(2, 5), 2); | |
| assert!(e.run(&[small], &[], &Policy { tau: 0.0, budget_ms: 0, ..Policy::default() }).is_ok()); | |
| let st = e.stats(); | |
| assert_eq!(st.free_pages + st.tree_pages, st.num_pages); | |
| } | |
| fn decode_exhaustion_midway_fails_only_that_request() { | |
| // a never-ending thought (cap huge) outgrows a 40-page pool: Err, pages released, a later request succeeds | |
| let (e, _) = engine(1 << 20, 40, 512, |_| {}); | |
| // find an item whose thought never ends under tau=1.01 (lim = 100000) by trying toks seeds | |
| let mut found = false; | |
| for s in 0..40u32 { | |
| let it = text_item("n", &toks(s, 30), &toks(s + 100, 8), 2); | |
| let pol = Policy { tau: 1.01, cap: 4000, budget_ms: 0, ..Policy::default() }; | |
| match e.run(&[it], &[], &pol) { | |
| Err(err) => { | |
| assert!(format!("{err:#}").contains("KV pool exhausted"), "{err:#}"); | |
| // the prompt fits the pool; running out while thinking is not the admission capacity error | |
| assert!(!format!("{err:#}").contains("KV cache capacity exceeded"), "{err:#}"); | |
| found = true; | |
| break; | |
| } | |
| Ok(_) => {} | |
| } | |
| } | |
| assert!(found, "no endless thought found among seeds"); | |
| let st = e.stats(); | |
| assert_eq!(st.free_pages + st.tree_pages, st.num_pages, "pages leaked after failure"); | |
| assert!(e.run(&[text_item("s", &toks(1, 20), &toks(2, 5), 2)], &[], &Policy { tau: 0.0, budget_ms: 0, ..Policy::default() }).is_ok()); | |
| } | |
| fn eviction_under_tiny_pool_keeps_answers() { | |
| let (big, _) = engine(32768, 4096, 512, |_| {}); | |
| let (tiny, _) = engine(32768, 60, 512, |_| {}); | |
| for r in 0..60u32 { | |
| let prefix = toks(r % 25, 150); | |
| let items: Vec<Item> = (0..2).map(|q| text_item(&format!("q{q}"), &prefix, &toks(r * 3 + q, 12), 3)).collect(); | |
| let a = big.run(&items, &[], &think_pol()).unwrap(); | |
| let b = tiny.run(&items, &[], &think_pol()).unwrap(); | |
| for (x, y) in a.iter().zip(&b) { | |
| assert_eq!((&x.p0, &x.think, x.trace_tokens), (&y.p0, &y.think, y.trace_tokens)); | |
| } | |
| } | |
| let st = tiny.stats(); | |
| assert!(st.cache_evicted_pages > 0, "expected evictions"); | |
| assert_eq!(st.free_pages + st.tree_pages, st.num_pages); | |
| } | |
| fn gate_modes() { | |
| let (e, _) = engine(32768, 2048, 512, |_| {}); | |
| let items: Vec<Item> = (0..12u32).map(|q| text_item(&format!("q{q}"), &toks(1, 30), &toks(q, 9), 3)).collect(); | |
| let n = |g: GateMode, tau: f32| e.run(&items, &[], &Policy { tau, gate: g, budget_ms: 0, cap: 5, ..Policy::default() }).unwrap().iter().filter(|r| r.think.is_some()).count(); | |
| assert_eq!(n(GateMode::Never, 2.0), 0); | |
| assert_eq!(n(GateMode::Tau, 2.0), 12); | |
| assert_eq!(n(GateMode::Tau, 0.0), 0); | |
| } | |
| fn budget_forces_readout_of_endless_thoughts() { | |
| // 8 questions whose thoughts would run 100000 tokens: the 150 ms wall-clock budget cuts them and reads them out | |
| let (e, _) = engine(1 << 20, 1 << 16, 512, |_| {}); | |
| for s in 0..60u32 { | |
| let probe = text_item("n", &toks(s, 30), &toks(s + 100, 8), 2); | |
| if e.run(&[probe.clone()], &[], &Policy { tau: 1.01, cap: 200, budget_ms: 0, ..Policy::default() }).unwrap()[0].trace_tokens < 200 { | |
| continue; | |
| } | |
| let items: Vec<Item> = (0..8).map(|q| Item { key: format!("q{q}"), ..probe.clone() }).collect(); | |
| let pol = Policy { tau: 1.01, cap: 1 << 19, budget_ms: 150, ..Policy::default() }; | |
| let t = std::time::Instant::now(); | |
| let r = e.run(&items, &[], &pol).unwrap(); | |
| assert!(e.stats().forced_by_budget >= 8, "forced {}", e.stats().forced_by_budget); | |
| assert!(r.iter().all(|r| r.think.is_some() && r.trace_tokens > 0 && r.trace_tokens < 100000)); | |
| assert!(t.elapsed().as_millis() < 1000, "the budget must cut the thoughts"); | |
| return; | |
| } | |
| panic!("no endless thought among the seeds"); | |
| } | |
| fn concurrency_equals_serial_with_prefix_sharing() { | |
| let (e, ctl) = engine(32768, 3000, 512, |_| {}); | |
| let e = std::sync::Arc::new(e); | |
| let mut reqs = vec![]; | |
| for r in 0..24u32 { | |
| let prefix = toks(r % 6, 100 + 7 * (r % 6) as usize); | |
| reqs.push((0..3u32).map(|q| text_item(&format!("q{q}"), &prefix, &toks(r * 5 + q, 10 + q as usize), 3)).collect::<Vec<_>>()); | |
| } | |
| let serial: Vec<_> = reqs.iter().map(|r| e.run(r, &[], &think_pol()).unwrap()).collect(); | |
| let reqs = std::sync::Arc::new(reqs); | |
| let hs: Vec<_> = (0..32).map(|t| { | |
| let (e, reqs) = (e.clone(), reqs.clone()); | |
| std::thread::spawn(move || (0..reqs.len()).map(|i| (i, e.run(&reqs[(i + t) % reqs.len()], &[], &think_pol()).unwrap(), (i + t) % reqs.len())).collect::<Vec<_>>()) | |
| }).collect(); | |
| for h in hs { | |
| for (_, res, ri) in h.join().unwrap() { | |
| for (a, b) in res.iter().zip(&serial[ri]) { | |
| assert_eq!((&a.p0, &a.think, a.trace_tokens, a.out_tokens), (&b.p0, &b.think, b.trace_tokens, b.out_tokens)); | |
| } | |
| } | |
| } | |
| assert!(e.stats().hit_rate() > 0.5); | |
| let _ = ctl.rows.load(Relaxed); | |
| } | |
| fn think_override_and_trace_ids() { | |
| use dllm_core::policy::ThinkMode; | |
| let (e, _) = engine(32768, 2048, 512, |_| {}); | |
| let items: Vec<Item> = (0..12u32).map(|q| text_item(&format!("q{q}"), &toks(1, 30), &toks(q, 9), 3)).collect(); | |
| let base = Policy { tau: 0.0, budget_ms: 0, cap: 20, ..Policy::default() }; // server policy that never thinks | |
| let auto = e.run(&items, &[], &base.with_think(ThinkMode::Auto)).unwrap(); | |
| assert!(auto.iter().all(|r| r.think.is_none() && r.trace_ids.is_empty())); | |
| let on = e.run(&items, &[], &base.with_think(ThinkMode::On)).unwrap(); | |
| assert!(on.iter().all(|r| r.think.is_some() && r.trace_tokens > 0 && r.trace_ids.is_empty()), "trace ids only on request"); | |
| let on_tr = e.run(&items, &[], &Policy { return_trace: true, ..base.with_think(ThinkMode::On) }).unwrap(); | |
| for (a, b) in on.iter().zip(&on_tr) { | |
| assert_eq!((&a.p0, &a.think, a.trace_tokens, a.out_tokens), (&b.p0, &b.think, b.trace_tokens, b.out_tokens), "return_trace must not change results"); | |
| assert_eq!(b.trace_ids.len(), b.trace_tokens); | |
| assert!(b.trace_tokens <= 20); | |
| } | |
| let think_all = Policy { tau: 2.0, ..base }; | |
| let off = e.run(&items, &[], &Policy { return_trace: true, ..think_all.with_think(ThinkMode::Off) }).unwrap(); | |
| assert!(off.iter().all(|r| r.think.is_none() && r.trace_tokens == 0 && r.trace_ids.is_empty())); | |
| } | |
| /// --think-ms: the per-branch cap is a pure function of the request (prefill rows, images, questions, thinking questions) under a fixed cost | |
| /// model; the same request gives identical answers and trace lengths alone, repeated, chunked over several prefill forwards and under | |
| /// concurrent load (other requests interleaved in the same forwards) | |
| fn think_ms_cap_is_deterministic_and_follows_the_cost_model() { | |
| use dllm_core::policy::{ThinkCost, ThinkShape}; | |
| let cost = ThinkCost::from_json(r#"{"base_ms":20,"prefill_ms_per_tok":0.1,"image_ms":0,"question_ms":1,"readout_ms":5,"readout_ms_per_branch":1,"tok_ms":[[1,2.0],[4,4.0],[16,10.0]],"min_tokens":2}"#).unwrap(); | |
| let pol = Policy { tau: 1.01, cap: 4000, budget_ms: 0, think_ms: 150, think_cost: cost, ..Policy::default() }; | |
| let free = Policy { think_ms: 0, ..pol }; | |
| // requests of 1..4 questions sharing a prefix (all think: tau 1.01) | |
| let prefix = toks(11, 120); | |
| let reqs: Vec<Vec<Item>> = (1..=4).map(|n| (0..n).map(|q| text_item(&format!("q{q}"), &prefix, &toks(40 + 7 * q as u32 + n as u32, 30 + 5 * q), 3)).collect()).collect(); | |
| let expect_cap = |items: &[Item]| { | |
| let ids: Vec<Vec<i32>> = items.iter().map(|it| it.ids.iter().map(|&t| t as i32).collect()).collect(); | |
| let s = ThinkShape { prefill_tokens: crate::engine::prefill_rows(&ids), questions: items.len(), thinking: items.len(), ..Default::default() }; | |
| cost.cap(150.0, &s, 4000) | |
| }; | |
| let (e, _) = engine(32768, 4096, 512, |c| c.prefix_cache = false); | |
| let mut want = vec![]; | |
| for items in &reqs { | |
| let cap = expect_cap(items); | |
| assert!(cap > 0 && cap < 100, "test request must get a small positive cap, got {cap}"); | |
| let unl = e.run(items, &[], &free).unwrap(); | |
| let r = e.run(items, &[], &pol).unwrap(); | |
| for (a, b) in unl.iter().zip(&r) { | |
| assert_eq!(a.p0, b.p0); | |
| assert_eq!(b.trace_tokens, a.trace_tokens.min(cap), "trace = unrestricted thought cut at the cost-model cap {cap}"); | |
| } | |
| want.push(r); | |
| } | |
| // more thinking questions => smaller per-branch cap | |
| assert!(expect_cap(&reqs[3]) < expect_cap(&reqs[0])); | |
| let same = |a: &[ItemResult], b: &[ItemResult]| a.iter().zip(b).all(|(x, y)| x.p0 == y.p0 && x.think == y.think && x.trace_tokens == y.trace_tokens); | |
| // repeated | |
| for (items, w) in reqs.iter().zip(&want) { | |
| assert!(same(&e.run(items, &[], &pol).unwrap(), w)); | |
| } | |
| // prefill split over many small forwards (max_rows 64: gate decisions of one request arrive in different iterations) | |
| let (small, _) = engine(32768, 4096, 64, |c| c.prefix_cache = false); | |
| for (items, w) in reqs.iter().zip(&want) { | |
| assert!(same(&small.run(items, &[], &pol).unwrap(), w), "chunked prefill changed the answer"); | |
| } | |
| // concurrent load with the radix cache on: 8 threads x 3 rounds of all requests interleaved | |
| let (busy, _) = engine(32768, 4096, 512, |_| {}); | |
| let busy = std::sync::Arc::new(busy); | |
| let hs: Vec<_> = (0..8) | |
| .map(|t| { | |
| let (busy, reqs, want, pol) = (busy.clone(), reqs.clone(), want.clone(), pol); | |
| std::thread::spawn(move || { | |
| for k in 0..3 { | |
| let i = (t + k) % reqs.len(); | |
| assert!(same(&busy.run(&reqs[i], &[], &pol).unwrap(), &want[i]), "load changed the answer"); | |
| } | |
| }) | |
| }) | |
| .collect(); | |
| for h in hs { | |
| h.join().unwrap(); | |
| } | |
| } | |
| /// number questions: greedy answer after the end-of-thought token until end-of-turn (or ANSWER_MAX tokens); `think: on` thinks first | |
| fn number_questions_generate_the_greedy_answer() { | |
| use crate::engine::{ANSWER_MAX, EOS, MK, TURN_END}; | |
| use crate::fake::{logits, step, REC0}; | |
| use dllm_core::policy::ThinkMode; | |
| let greedy = |rec: &mut crate::fake::Rec, excl: &[i32]| -> i32 { | |
| let l = logits(rec); | |
| let mut b = -1i32; | |
| for v in 0..256i32 { | |
| if excl.contains(&v) { | |
| continue; | |
| } | |
| if b < 0 || l[v as usize] > l[b as usize] { | |
| b = v; | |
| } | |
| } | |
| b | |
| }; | |
| let stop = |t: i32| t == MK || t == EOS || t == TURN_END; | |
| let (e, _) = engine(32768, 2048, 512, |_| {}); | |
| let mut checked = 0; | |
| for s in 0..12u32 { | |
| let mut it = text_item("n", &toks(s, 30), &toks(s + 50, 7), 0); | |
| it.number = true; | |
| it.label_ids = vec![]; | |
| it.keys = vec![]; | |
| let base = Policy { tau: 0.8, cap: 40, budget_ms: 0, ..Policy::default() }; | |
| for think in [false, true] { | |
| let pol = if think { base.with_think(ThinkMode::On) } else { base }; | |
| let r = e.run(&[it.clone()], &[], &Policy { return_trace: true, ..pol }).unwrap().remove(0); | |
| // reference with the fake LM | |
| let mut rec = REC0; | |
| for &t in &it.ids { | |
| rec = step(&rec, t as i32, None, 0); | |
| } | |
| let mut thought = vec![]; | |
| if think { | |
| let mut x = rec; | |
| let mut t = greedy(&mut x, &[MK, EOS, TURN_END]); | |
| loop { | |
| thought.push(t); | |
| x = step(&x, t, None, 0); | |
| t = greedy(&mut x, &[]); | |
| if stop(t) || thought.len() >= 40 { | |
| break; | |
| } | |
| } | |
| for &t in &thought { | |
| rec = step(&rec, t, None, 0); | |
| } | |
| } | |
| rec = step(&rec, MK, None, 0); | |
| let mut ans = vec![]; | |
| let mut lp = 0.0f64; | |
| loop { | |
| let l = logits(&rec); | |
| let t = greedy(&mut rec, &[]); | |
| if stop(t) || ans.len() >= ANSWER_MAX { | |
| break; | |
| } | |
| let m = l[t as usize] as f64; | |
| lp += m - (m + l.iter().map(|&v| (v as f64 - m).exp()).sum::<f64>().ln()); | |
| ans.push(t as u32); | |
| rec = step(&rec, t, None, 0); | |
| } | |
| assert_eq!(r.answer_ids, ans, "seed {s} think {think}"); | |
| assert!((r.answer_logprob - lp).abs() < 1e-9); | |
| assert_eq!(r.think.is_some(), think); | |
| assert!(r.p0.is_empty()); | |
| if think { | |
| assert_eq!(r.trace_ids, thought.iter().map(|&t| t as u32).collect::<Vec<_>>(), "seed {s}"); | |
| } | |
| checked += !ans.is_empty() as usize; | |
| } | |
| } | |
| assert!(checked > 4, "fake LM produced too few non-empty answers ({checked})"); | |
| // mixed with choice questions in one request: the choice answers are unchanged | |
| let mut num = text_item("n", &toks(1, 30), &toks(51, 7), 0); | |
| num.number = true; | |
| num.label_ids = vec![]; | |
| num.keys = vec![]; | |
| let ch = text_item("c", &toks(1, 30), &toks(52, 7), 3); | |
| let alone = e.run(&[ch.clone()], &[], &think_pol()).unwrap(); | |
| let mixed = e.run(&[num, ch], &[], &think_pol()).unwrap(); | |
| assert_eq!((&alone[0].p0, &alone[0].think, alone[0].trace_tokens), (&mixed[1].p0, &mixed[1].think, mixed[1].trace_tokens)); | |
| } | |
| /// a decoder that answers several tokens per cycle (as a speculative decoder accepting all its drafts): `n` one-row greedy forwards per branch, | |
| /// KV written up to pos + n - 1 and surplus tokens after a stop / ANSWER_MAX returned like rejected-then-ignored ones | |
| struct Burst(usize); | |
| impl crate::ThinkDecoder for Burst { | |
| fn lookahead(&self) -> usize { | |
| self.0 - 1 | |
| } | |
| fn step(&mut self, m: &mut dyn ModelRunner, reqs: &[crate::ThinkReq]) -> anyhow::Result<Vec<Vec<i32>>> { | |
| crate::GreedyDecoder.step(m, reqs) | |
| } | |
| fn answer_step(&mut self, m: &mut dyn ModelRunner, reqs: &[crate::ThinkReq]) -> anyhow::Result<Vec<Vec<(i32, f64)>>> { | |
| let mut out = vec![]; | |
| for r in reqs { | |
| let (mut tok, mut acc) = (r.tok, vec![]); | |
| for j in 0..self.0 { | |
| m.forward(&crate::model::RowBatch::decode(&[tok], &[(r.pages, r.pos + j)]), None)?; | |
| let z = m.logits_rows(&[0])?.remove(0); | |
| acc.push(dllm_model::spec::greedy_logprob(&z)); | |
| tok = acc[j].0; | |
| } | |
| out.push(acc); | |
| } | |
| Ok(out) | |
| } | |
| } | |
| /// number answers decoded several tokens per cycle equal the one-token-per-step answers (ids and log-probability bit for bit), with and without a thought | |
| fn multi_token_answer_cycles_equal_one_token_steps() { | |
| use dllm_core::policy::ThinkMode; | |
| let run = |n: usize| { | |
| let m = FakeModel::full_kv(2048, 512); | |
| let cfg = EngineConfig { max_len: 32768, prep: Some(Arc::new(FakePrep)), ..Default::default() }; | |
| let e = Engine::start_with(cfg, Box::new(m), Some(Box::new(FakeVision { encodes: 0 })), tok(), Box::new(Burst(n))).unwrap(); | |
| let mut out = vec![]; | |
| let num = |key: &str, prefix: &[u32], tail: &[u32]| Item { number: true, label_ids: vec![], keys: vec![], ..text_item(key, prefix, tail, 0) }; | |
| for s in 0..12u32 { | |
| let base = Policy { tau: 0.8, cap: 40, budget_ms: 0, ..Policy::default() }; | |
| for think in [false, true] { | |
| let pol = if think { base.with_think(ThinkMode::On) } else { base }; | |
| // two questions in one request: answers of different lengths share answer cycles | |
| let r = e.run(&[num("n", &toks(s, 30), &toks(s + 50, 7)), num("m", &toks(s, 30), &toks(s + 90, 5))], &[], &pol).unwrap(); | |
| out.push(r.iter().map(|x| (x.answer_ids.clone(), x.answer_logprob.to_bits(), x.out_tokens)).collect::<Vec<_>>()); | |
| } | |
| } | |
| out | |
| }; | |
| let want = run(1); | |
| assert!(want.iter().flatten().filter(|x| x.0.len() >= 2).count() > 4, "fake LM produced too few multi-token answers"); | |
| for n in [2, 3, 7, 40] { | |
| assert_eq!(run(n), want, "{n} tokens per cycle"); | |
| } | |
| } | |