//! Request runtime: continuous-batching scheduler + radix prefix cache + the answer policy (no-think readout, tau gate, thought, post-thought readout), //! on top of a `ModelRunner`. //! //! One engine thread owns the model. `Engine::run` (any thread) preprocesses images, expands the prompts, enqueues the request and blocks on the reply. //! Per iteration: (1) admit waiting requests (earliest deadline first, page / branch / row budgets) and run ONE packed prefill forward for the new work of //! all of them (shared prefix rows once per request + per-branch tails; cache hits skip rows except the last two of every branch), read the readout row //! of every branch from it; (2) one decode step for ALL thinking branches of ALL requests (`ThinkDecoder`); (3) one readout forward for the branches that //! finished their thought (end-of-thought row = post-thought letter distribution); (4) one decode cycle for every answering number question //! (`ThinkDecoder::answer_step`). //! Scheduling ideas follow SGLang `srt/managers/schedule_policy.py` (cache-aware ordering, in-batch prefix de-duplication by deferral) and //! `srt/managers/scheduler.py` (iteration-level batching); the prefix tree is `radix.rs`. use crate::model::*; use crate::radix::{page_keys, NodeId, PKey, RadixCache, ROOT}; use crate::think::{GreedyDecoder, ThinkDecoder, ThinkReq}; use anyhow::{anyhow, bail, Result}; use dllm_core::policy::{GateMode, Policy, ThinkShape}; use dllm_core::serving::{Item, ItemResult}; use dllm_core::tokenizer::{Special, Tok}; use dllm_model::gpu_model::{RowBatch, PAGE}; use dllm_model::spec::greedy_logprob; use std::collections::{HashMap, VecDeque}; use std::sync::mpsc::{channel, Receiver, Sender}; use std::sync::{Arc, Mutex}; use std::time::Instant; pub const MK: i32 = 101; pub const EOS: i32 = 1; pub const TURN_END: i32 = 106; /// longest generated answer of a number question (tokens, without the closing end-of-turn) pub const ANSWER_MAX: usize = 32; /// a request whose remaining wall-clock budget (minus the readout reserve) is below this does not start a thought (ms) const MIN_THINK_MS: f64 = 60.0; #[derive(Clone)] pub struct EngineConfig { /// model context limit (thoughts are clamped to the room left in it) pub max_len: usize, /// max concurrent branches (prefilled or thinking) pub max_branches: usize, /// soft limit of new rows per prefill iteration (the model's max_rows is the hard limit of one forward; larger work is chunked) pub max_prefill_rows: usize, pub prefix_cache: bool, /// image preprocessing (None => requests with images fail) pub prep: Option>, /// encoded images kept on the device (by content hash) pub soft_cache_images: usize, /// forward row cap while any branch is thinking (0 = model max_rows): keeps decode steps / readouts responsive; the model max_rows applies otherwise pub think_max_rows: usize, } impl Default for EngineConfig { fn default() -> Self { EngineConfig { max_len: 32768, max_branches: 256, max_prefill_rows: 2048, prefix_cache: true, prep: None, soft_cache_images: 8, think_max_rows: 1024 } } } /// counters of the scheduler (tests and diagnostics) #[derive(Debug, Clone, Default)] pub struct Stats { pub requests: u64, pub items: u64, pub think_items: u64, pub errors: u64, pub prefill_forwards: u64, pub prefill_rows: u64, pub decode_forwards: u64, pub probe_forwards: u64, pub cache_query_pages: u64, pub cache_hit_pages: u64, pub cache_evicted_pages: u64, pub deferred_in_batch: u64, pub vision_encodes: u64, pub soft_cache_hits: u64, pub peak_branches: u64, pub free_pages: usize, pub tree_pages: usize, pub num_pages: usize, /// sliding-window pool (0 pages = model without sliding layers): pool size, free pages, pages kept for cached prefixes pub sliding_pages: usize, pub sliding_free: usize, pub sliding_tree: usize, pub forced_by_budget: u64, /// thinking skipped because the budget left no useful time (or the deterministic cap was 0) pub skipped_think: u64, /// thinking cut because the sliding-window pool had no page for the next step pub swa_forced: u64, pub thought_tokens: u64, } impl Stats { pub fn hit_rate(&self) -> f64 { if self.cache_query_pages == 0 { 0.0 } else { self.cache_hit_pages as f64 / self.cache_query_pages as f64 } } } #[derive(Debug, Clone, Copy)] struct Blk { s: usize, e: usize, img: usize, } struct ReqIn { items: Vec, ids: Vec>, keys: Vec>, blocks: Vec>, images: Vec, pol: Policy, reply: Sender>>, /// request arrival (HTTP layer if it called `set_arrival`, else the call to `run`): the think budget counts from here arrive: Instant, /// request shape for the deterministic `--think-ms` cap (`thinking` is filled in after the gate) shape: ThinkShape, } enum Msg { Req(ReqIn), Shutdown, } pub struct Engine { tx: Mutex>, stats: Arc>, sp: Special, prep: Option>, has_vision: bool, join: Mutex>>, max_len: usize, } fn fnv_mix(a: u64, b: u64) -> u64 { let mut z = (a ^ b.wrapping_mul(0x9E3779B97F4A7C15)).wrapping_add(0x9E3779B97F4A7C15); z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9); z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB); z ^ (z >> 31) } impl Engine { pub fn start(cfg: EngineConfig, model: Box, vision: Option>, tok: Arc) -> Result { Self::start_with(cfg, model, vision, tok, Box::new(GreedyDecoder)) } pub fn start_with(cfg: EngineConfig, mut model: Box, vision: Option>, tok: Arc, think: Box) -> Result { if model.num_pages() < 4 { bail!("model has too few KV pages"); } let t = Instant::now(); match model.enable_graphs() { Ok(true) => eprintln!("decode CUDA graphs captured in {:.2}s", t.elapsed().as_secs_f32()), Ok(false) => {} Err(e) => eprintln!("decode CUDA graphs disabled: {e:#}"), } let (tx, rx) = channel(); let stats = Arc::new(Mutex::new(Stats::default())); let has_vision = vision.is_some(); let prep = cfg.prep.clone(); let max_len = cfg.max_len; let mut sch = Sched::new(cfg, model, vision, think, stats.clone()); let join = std::thread::Builder::new().name("dllm-engine".into()).spawn(move || sch.run_loop(rx))?; Ok(Engine { tx: Mutex::new(tx), stats, sp: tok.sp, prep, has_vision, join: Mutex::new(Some(join)), max_len }) } pub fn stats(&self) -> Stats { self.stats.lock().unwrap().clone() } /// Blocking; callable concurrently from many threads. All questions of one call share the state prefix and the images. pub fn run(&self, items: &[Item], images: &[Vec], pol: &Policy) -> Result> { if items.is_empty() { return Ok(vec![]); } let arrive = dllm_core::serving::take_arrival().unwrap_or_else(Instant::now); let mut prepared = vec![]; if !images.is_empty() { if !self.has_vision { bail!("images are not supported by this engine (no vision encoder)"); } let prep = self.prep.as_ref().ok_or_else(|| anyhow!("no image preprocessor configured"))?; // images of one request are prepared (decode + resize + tower) concurrently; the tower serialises internally let one = |i: usize, b: &Vec| -> Result { prep.prepare(b).map_err(|e| anyhow!("cannot decode image {i}: {e:#}")) }; if images.len() == 1 { prepared.push(one(0, &images[0])?); } else { let rs: Vec> = std::thread::scope(|sc| { let hs: Vec<_> = images.iter().enumerate().map(|(i, b)| sc.spawn(move || one(i, b))).collect(); hs.into_iter().map(|h| h.join().unwrap_or_else(|_| Err(anyhow!("image preparation panicked")))).collect() }); for r in rs { prepared.push(r?); } } } let soft: Vec = prepared.iter().map(|p| p.n_soft).collect(); let tok_sp = self.sp; let mut ids = vec![]; let mut keys = vec![]; let mut blocks = vec![]; for it in items { let n_ph = it.ids.iter().filter(|&&t| t == tok_sp.image).count(); if n_ph != images.len() { bail!("item {}: {} image placeholders for {} images", it.key, n_ph, images.len()); } if it.ids.is_empty() { bail!("item {}: empty prompt", it.key); } let mut ex: Vec = Vec::with_capacity(it.ids.len() + soft.iter().sum::() + 2 * soft.len()); let mut k = 0; for &t in &it.ids { if t == tok_sp.image { ex.push(tok_sp.boi as i32); ex.extend(std::iter::repeat(tok_sp.image as i32).take(soft[k])); ex.push(tok_sp.eoi as i32); k += 1; } else { ex.push(t as i32); } } // keys + blocks (image soft tokens carry the image content hash and their offset: traps (a)/(e)) let mut kv = Vec::with_capacity(ex.len()); let mut bl: Vec = vec![]; let mut i = 0; while i < ex.len() { if ex[i] == tok_sp.image as i32 { let img = bl.len(); let s = i; while i < ex.len() && ex[i] == tok_sp.image as i32 { let j = (i - s) as u64; kv.push((1u64 << 63) | (fnv_mix(prepared[img].hash ^ ((prepared[img].n_soft as u64) << 40), j + 1) >> 1)); i += 1; } bl.push(Blk { s, e: i, img }); } else { kv.push(ex[i] as u32 as u64); i += 1; } } ids.push(ex); keys.push(kv); blocks.push(bl); } let (rtx, rrx) = channel(); let shape = ThinkShape { prefill_tokens: prefill_rows(&ids), images: images.len(), image_bytes: images.iter().map(|b| b.len()).sum(), questions: items.len(), thinking: 0 }; let req = ReqIn { items: items.to_vec(), ids, keys, blocks, images: prepared, pol: *pol, reply: rtx, arrive, shape }; self.tx.lock().unwrap().send(Msg::Req(req)).map_err(|_| anyhow!("engine thread has stopped"))?; rrx.recv().map_err(|_| anyhow!("engine thread died"))? } pub fn max_len(&self) -> usize { self.max_len } } impl Drop for Engine { fn drop(&mut self) { let _ = self.tx.lock().unwrap().send(Msg::Shutdown); if let Some(j) = self.join.lock().unwrap().take() { let _ = j.join(); } } } // ================================================================================================================ scheduler #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum Phase { Prefill, /// `--think-ms`: the gate fired; waits for the rest of the request's gate decisions, then gets the request's deterministic cap and thinks Gated, Think, /// feed `feed` (starting at `probe_pos`) and read the letter logits of the last row (number questions: the first answer token) Probe, /// number question: greedy answer tokens after the end-of-thought token (`answer_step`) Answer, } struct Branch { req: u64, item: usize, /// number question (generated answer instead of a letter readout) number: bool, /// number answer: tokens so far, sum of their log-probabilities, position of `ans[0]` ans: Vec, ans_lp: f64, ans_pos: usize, l: usize, pol: Policy, pages: Vec, node: NodeId, phase: Phase, p0: Vec, gi: Vec, fed: usize, maxgen: usize, /// position of gi[0] base: usize, feed: Vec, probe_pos: usize, nt: usize, out_tok: usize, deadline: Option, /// sliding-window pool: sliding ref held on page index j (`sl[j]`), trim cursor, shared-prefix pages of the request sl: Vec, sl_cur: usize, sp_pages: usize, } struct Req { inp: ReqIn, results: Vec>, remaining: usize, failed: bool, arrive: Instant, /// sliding pages reserved for this request's prefill (recycled within the request), and the reserve's size cap sl_res: Vec, sl_cap: usize, } struct Seg { tokens: Vec, start: usize, pages: Vec, blocks: Vec, req: u64, branch: Option, } struct Pool { free: Vec, rc: Vec, } impl Pool { fn decref(&mut self, p: i32) { let r = &mut self.rc[p as usize]; assert!(*r > 0, "decref of free page {p}"); *r -= 1; if *r == 0 { self.free.push(p); } } fn incref(&mut self, p: i32) { assert!(self.rc[p as usize] > 0, "incref of free page {p}"); self.rc[p as usize] += 1; } } struct Sched { cfg: EngineConfig, model: Box, vision: Option>, think: Box, pool: Pool, tree: RadixCache, reqs: HashMap, waiting: VecDeque, branches: Vec>, free_slots: Vec, live_branches: usize, next_id: u64, soft_cache: Vec<(u64, Arc)>, st: Stats, shared: Arc>, job: Option, ema_probe_ms: f64, /// EMA of one prefill forward (ms), used to cut thinking branches BEFORE a forward that would overrun their deadline ema_pf_fwd_ms: f64, ema_dec_ms: f64, /// vision-tower encode time per image (the next forward's estimate includes the encodes it will trigger) ema_vis_ms: f64, ema_pf_ms_row: f64, /// decayed least-squares sums of (rows, ms) of prefill forwards: ms ~ a + b * rows pf_fit: [f64; 5], /// sliding-window KV pool bookkeeping (None = the model has no separate sliding-window pool) swa: Option, } struct PrefillJob { segs: Vec, chunks: Vec>, next: usize, rids: Vec, } fn is_stop(t: i32) -> bool { t == MK || t == EOS || t == TURN_END } fn softmax64(z: &[f32]) -> Vec { let m = z.iter().cloned().fold(f32::NEG_INFINITY, f32::max) as f64; let e: Vec = z.iter().map(|&v| (v as f64 - m).exp()).collect(); let s: f64 = e.iter().sum(); e.iter().map(|v| v / s).collect() } impl Sched { fn new(cfg: EngineConfig, model: Box, vision: Option>, think: Box, shared: Arc>) -> Self { let n = model.num_pages(); let swa = match (model.sliding_pages(), model.sliding_window()) { (s, w) if s > 0 && w > 0 => Some(Swa::new(n, s, w)), _ => None, }; let st = Stats { num_pages: n, sliding_pages: model.sliding_pages(), ..Default::default() }; Sched { cfg, model, vision, think, pool: Pool { free: (0..n as i32).rev().collect(), rc: vec![0; n] }, tree: RadixCache::new(), reqs: HashMap::new(), waiting: VecDeque::new(), branches: vec![], free_slots: vec![], live_branches: 0, next_id: 1, soft_cache: vec![], st, shared, job: None, ema_probe_ms: 3.0, ema_pf_fwd_ms: 10.0, ema_dec_ms: 3.0, ema_vis_ms: 10.0, ema_pf_ms_row: 0.02, pf_fit: [0.0; 5], swa, } } fn run_loop(&mut self, rx: Receiver) { let mut shutdown = false; loop { let idle = self.waiting.is_empty() && self.live_branches == 0; if idle { if cfg!(debug_assertions) { self.check_idle(); } if shutdown { return; } match rx.recv() { Ok(Msg::Req(r)) => self.enqueue(r), Ok(Msg::Shutdown) | Err(_) => return, } } while let Ok(m) = rx.try_recv() { match m { Msg::Req(r) => self.enqueue(r), Msg::Shutdown => shutdown = true, } } // load-aware budget accounting: if the next prefill forward would run past (deadline - reserve) of a thinking branch, cut and read that branch out FIRST let est = self.next_prefill_est_ms(); if est > 0.0 && self.cut_due(est) { let tp0 = Instant::now(); if self.probe_step() { self.ema_probe_ms = 0.9 * self.ema_probe_ms + 0.1 * tp0.elapsed().as_secs_f64() * 1e3; } } self.prefill_step(); self.decode_step(); let tp = Instant::now(); let probed = self.probe_step(); if probed { self.ema_probe_ms = 0.9 * self.ema_probe_ms + 0.1 * tp.elapsed().as_secs_f64() * 1e3; } self.answer_step(); self.publish(); if shutdown && self.waiting.is_empty() && self.live_branches == 0 { return; } } } /// measured time needed to finish a cut request: one readout (probe) pass + one decode step of slack /// prefill cost model ms = a + b*rows (fixed expert-streaming floor + per-row slope), least squares over recent forwards fn pf_model(&self) -> (f64, f64) { let [n, sx, sy, sxx, sxy] = self.pf_fit; let var = n * sxx - sx * sx; if n < 6.0 || var < 1e-6 * n * n { return (8.0, self.ema_pf_ms_row.min(0.1)); } let b = ((n * sxy - sx * sy) / var).clamp(0.005, 0.3); let a = ((sy - b * sx) / n).clamp(2.0, 40.0); (a, b) } /// expected duration of the prefill forward the scheduler is about to run (0 = none pending) fn next_prefill_est_ms(&self) -> f64 { let (a, b) = self.pf_model(); let cap = if self.thinking_live() { self.think_cap() } else { usize::MAX }; if let Some(j) = &self.job { if j.next < j.chunks.len() { let rows: usize = j.chunks[j.next].iter().map(|c| c.2 - c.1).sum(); return (a + b * rows.min(cap) as f64 + self.uncached_images(&j.segs, &j.chunks[j.next]) as f64 * self.ema_vis_ms).min(400.0); } } if !self.waiting.is_empty() { return self.ema_pf_fwd_ms.min(a + b * cap.min(self.model.max_rows()) as f64).min(400.0); } 0.0 } /// images of a chunk whose soft tokens are not in the device cache (the chunk will run the vision tower for them) fn uncached_images(&self, segs: &[Seg], ch: &[(usize, usize, usize)]) -> usize { let mut seen: Vec = vec![]; for &(si, lo, hi) in ch { let sg = &segs[si]; let Some(r) = self.reqs.get(&sg.req) else { continue }; for bl in sg.blocks.iter().filter(|bl| bl.s < sg.start + hi && bl.e > sg.start + lo) { let p = &r.inp.images[bl.img]; let h = p.hash ^ ((p.n_soft as u64) << 48); if !seen.contains(&h) && !self.soft_cache.iter().any(|(x, _)| *x == h) { seen.push(h); } } } seen.len() } /// forward row cap while thinking is live (`think_max_rows`, 0 = the model's max_rows) fn think_cap(&self) -> usize { if self.cfg.think_max_rows == 0 { self.model.max_rows() } else { self.cfg.think_max_rows } } /// force the readout of thinking branches whose deadline is within reserve + `extra_ms`; returns whether any was cut fn cut_due(&mut self, extra_ms: f64) -> bool { let now = Instant::now() + std::time::Duration::from_secs_f64((self.reserve_ms() + extra_ms) / 1e3); let late: Vec = (0..self.branches.len()).filter(|&i| self.branches[i].as_ref().map_or(false, |b| b.phase == Phase::Think && b.deadline.map_or(false, |d| now >= d))).collect(); for &bi in &late { self.st.forced_by_budget += 1; self.end_think(bi); } !late.is_empty() } fn reserve_ms(&self) -> f64 { (1.5 * self.ema_probe_ms + self.ema_dec_ms).max(2.0) } fn publish(&mut self) { self.st.free_pages = self.pool.free.len(); if let Some(sw) = &self.swa { self.st.sliding_free = sw.free.len() + self.reqs.values().map(|r| r.sl_res.len()).sum::(); self.st.sliding_tree = sw.tree_n; } self.st.tree_pages = self.tree.total_pages(); *self.shared.lock().unwrap() = self.st.clone(); } fn enqueue(&mut self, inp: ReqIn) { let id = self.next_id; self.next_id += 1; let n = inp.items.len(); self.st.requests += 1; self.st.items += n as u64; let arrive = inp.arrive; self.reqs.insert(id, Req { inp, results: vec![None; n], remaining: n, failed: false, arrive, sl_res: vec![], sl_cap: 0 }); self.waiting.push_back(id); } // -------------------------------------------------------------------------------------------- pages fn alloc(&mut self, n: usize) -> Result> { if self.pool.free.len() < n { let need = n - self.pool.free.len(); self.evict_tree(need); } if self.pool.free.len() < n { bail!("KV pool exhausted ({} pages total, {} needed, {} free)", self.pool.rc.len(), n, self.pool.free.len()); } let mut v = Vec::with_capacity(n); for _ in 0..n { let p = self.pool.free.pop().unwrap(); self.pool.rc[p as usize] = 1; v.push(p); } Ok(v) } fn release_branch(&mut self, bi: usize) { if self.swa_on() { let held: Vec = { let b = self.branches[bi].as_ref().unwrap(); b.sl.iter().enumerate().filter(|(_, &h)| h).map(|(j, _)| b.pages[j]).collect() }; for f in held { self.sl_unref(f); } self.branches[bi].as_mut().unwrap().sl.clear(); } if let Some(b) = self.branches[bi].take() { for &p in &b.pages { self.pool.decref(p); } if b.node != ROOT { self.tree.dec_lock(b.node); } self.free_slots.push(bi); self.live_branches -= 1; } } fn new_branch(&mut self, b: Branch) -> usize { self.live_branches += 1; self.st.peak_branches = self.st.peak_branches.max(self.live_branches as u64); if let Some(i) = self.free_slots.pop() { self.branches[i] = Some(b); i } else { self.branches.push(Some(b)); self.branches.len() - 1 } } // -------------------------------------------------------------------------------------------- completion fn finish_item(&mut self, bi: usize, res: ItemResult) { let (rid, item) = { let b = self.branches[bi].as_ref().unwrap(); (b.req, b.item) }; self.release_branch(bi); if let Some(r) = self.reqs.get_mut(&rid) { if r.failed { return; } r.results[item] = Some(res); r.remaining -= 1; if r.remaining == 0 { let mut r = self.reqs.remove(&rid).unwrap(); self.sl_drop_reserve(std::mem::take(&mut r.sl_res)); self.publish(); let out: Vec = r.results.into_iter().map(|x| x.unwrap()).collect(); let _ = r.inp.reply.send(Ok(out)); } } } fn fail_req(&mut self, rid: u64, msg: String) { self.waiting.retain(|&x| x != rid); let bis: Vec = (0..self.branches.len()).filter(|&i| self.branches[i].as_ref().map_or(false, |b| b.req == rid)).collect(); for bi in bis { self.release_branch(bi); } if let Some(mut r) = self.reqs.remove(&rid) { self.sl_drop_reserve(std::mem::take(&mut r.sl_res)); self.st.errors += 1; let _ = r.inp.reply.send(Err(anyhow!(msg))); } } fn fail_branches(&mut self, bis: &[usize], msg: &str) { let mut rids: Vec = bis.iter().filter_map(|&i| self.branches[i].as_ref().map(|b| b.req)).collect(); rids.sort(); rids.dedup(); for r in rids { self.fail_req(r, msg.to_string()); } } // -------------------------------------------------------------------------------------------- admission + prefill /// One scheduler iteration of prefill: admit (EDF) while the pending job is below `max_prefill_rows`, then run ONE forward of the job (every forward /// streams all experts, so big batches are cheap per row); the rest of the job continues next iteration, after a decode step of the thinking branches, /// so a long prefill never stalls them. fn prefill_step(&mut self) { if !self.waiting.is_empty() { let pending: usize = self.job.as_ref().map_or(0, |j| j.chunks[j.next..].iter().map(|c| c.iter().map(|p| p.2 - p.1).sum::()).sum()); if pending < self.cfg.max_prefill_rows { self.admit(pending); } } let Some(mut job) = self.job.take() else { return }; let mut spent = 0.0f64; let mut failed: Option = None; while job.next < job.chunks.len() { let ch: Vec<(usize, usize, usize)> = job.chunks[job.next].iter().copied().filter(|c| self.reqs.contains_key(&job.segs[c.0].req)).collect(); if ch.is_empty() { job.next += 1; continue; } let mut ch = ch; let mut rows: usize = ch.iter().map(|c| c.2 - c.1).sum(); // while branches think, forwards stay at `think_max_rows` (a big no-think pack would delay their decode steps / readouts) let tcap = self.think_cap(); if tcap > 0 && rows > tcap && self.thinking_live() { let parts = self.split_chunk(&job.segs, &ch, tcap); if parts.len() > 1 { ch = parts[0].clone(); rows = ch.iter().map(|c| c.2 - c.1).sum(); job.chunks.splice(job.next + 1..job.next + 1, parts[1..].to_vec()); job.chunks[job.next] = ch.clone(); } } // exactly one forward per iteration if spent > 0.0 { break; } let t0 = Instant::now(); if let Err(e) = self.run_chunk(&job.segs, &ch) { failed = Some(format!("{e:#}")); break; } let el = t0.elapsed().as_secs_f64() * 1e3; self.ema_pf_fwd_ms = 0.9 * self.ema_pf_fwd_ms + 0.1 * el; spent += el; job.next += 1; } if let Some(e) = failed { for r in job.rids.clone() { self.fail_req(r, format!("prefill failed: {e}")); } } else if job.next < job.chunks.len() { self.job = Some(job); } } /// chunk an ordered list of pieces (seg, lo, hi) into forwards of at most `cap` rows (same cut rules as `build_chunks`) fn chunk_pieces(&self, segs: &[Seg], ch: &[(usize, usize, usize)], cap: usize) -> Result>> { let tmp: Vec = ch .iter() .map(|&(si, lo, hi)| { let s = &segs[si]; Seg { tokens: s.tokens[lo..hi].to_vec(), start: s.start + lo, pages: vec![], blocks: s.blocks.clone(), req: s.req, branch: if hi == s.tokens.len() { s.branch } else { None } } }) .collect(); Ok(self.build_chunks(&tmp, cap)?.into_iter().map(|p| p.into_iter().map(|(ti, lo2, hi2)| (ch[ti].0, ch[ti].1 + lo2, ch[ti].1 + hi2)).collect()).collect()) } /// any branch thinking / waiting for its readout (prefill forwards are then kept at `think_max_rows` so decode steps are not starved) fn thinking_live(&self) -> bool { self.branches.iter().flatten().any(|b| matches!(b.phase, Phase::Think | Phase::Probe | Phase::Answer)) } /// re-chunk the pieces of one chunk with a smaller row cap (same cut rules as `build_chunks`) fn split_chunk(&self, segs: &[Seg], ch: &[(usize, usize, usize)], cap: usize) -> Vec> { let tmp: Vec = ch .iter() .map(|&(si, lo, hi)| { let s = &segs[si]; Seg { tokens: s.tokens[lo..hi].to_vec(), start: s.start + lo, pages: vec![], blocks: s.blocks.clone(), req: s.req, branch: if hi == s.tokens.len() { s.branch } else { None } } }) .collect(); match self.build_chunks(&tmp, cap) { Ok(parts) => parts.into_iter().map(|p| p.into_iter().map(|(ti, lo2, hi2)| (ch[ti].0, ch[ti].1 + lo2, ch[ti].1 + hi2)).collect()).collect(), Err(_) => vec![ch.to_vec()], } } fn admit(&mut self, pending: usize) { // earliest deadline first (arrival + budget); a cached page is worth 0.5 ms of urgency (cache hits go slightly earlier, deadlines dominate). // (A shortest-job-first variant lowered nothing measurable at c=16 and raised p99: not used.) let mut order: Vec<(f64, u64)> = vec![]; for &rid in &self.waiting { let r = &self.reqs[&rid]; let b = r.inp.pol.budget_ms as u64; let dl = (if b == 0 { 60_000 } else { b }) as f64 - r.arrive.elapsed().as_secs_f64() * 1e3; let k = &r.inp.keys[0]; let c = (k.len() - 1) / PAGE; let m = if self.cfg.prefix_cache { self.tree.match_len(&page_keys(&k[..c * PAGE])) } else { 0 }; order.push((dl - 0.5 * m as f64, rid)); } order.sort_by(|a, b| a.partial_cmp(b).unwrap()); let mut segs: Vec = vec![]; let mut inflight: Vec> = vec![]; let mut rows = 0usize; let mut admitted: Vec = vec![]; for (_, rid) in order { if pending + rows >= self.cfg.max_prefill_rows && !segs.is_empty() { break; } let n = self.reqs[&rid].inp.items.len(); if self.live_branches > 0 && self.live_branches + n > self.cfg.max_branches { continue; } match self.plan_request(rid, &mut segs, &mut inflight) { Ok(Some(r)) => { rows += r; admitted.push(rid); self.waiting.retain(|&x| x != rid); } Ok(None) => {} Err(e) => self.fail_req(rid, format!("{e:#}")), } } if segs.is_empty() { return; } let cap = self.model.max_rows(); // re-pack: the not-yet-run pieces of the pending job and the new segments are chunked TOGETHER (piece order kept, so a request's shared // prefix still precedes its branch tails), so every forward is as full as possible instead of the old job's tail chunk running alone if let Some(j) = self.job.take() { let mut pieces: Vec<(usize, usize, usize)> = j.chunks[j.next..].concat(); let mut all = j.segs; let base = all.len(); for (i, s) in segs.iter().enumerate() { pieces.push((base + i, 0, s.tokens.len())); } let n_new = segs.len(); all.extend(segs); let mut rids = j.rids; rids.extend(admitted.iter().copied()); match self.chunk_pieces(&all, &pieces, cap) { Ok(chunks) => self.job = Some(PrefillJob { segs: all, chunks, next: 0, rids }), Err(_) => { // fall back: old pieces as they were, new segments chunked on their own behind them let old: Vec<(usize, usize, usize)> = pieces[..pieces.len() - n_new].to_vec(); let new_segs: Vec = all.drain(base..).collect(); let mut chunks = if old.is_empty() { vec![] } else { self.chunk_pieces(&all, &old, cap).unwrap_or_else(|_| vec![old.clone()]) }; match self.build_chunks(&new_segs, cap) { Ok(c) => { all.extend(new_segs); for c in c { chunks.push(c.into_iter().map(|(si, lo, hi)| (si + base, lo, hi)).collect()); } self.job = Some(PrefillJob { segs: all, chunks, next: 0, rids }); } Err(e) => { if !chunks.is_empty() { self.job = Some(PrefillJob { segs: all, chunks, next: 0, rids }); } for r in admitted { self.fail_req(r, format!("prefill failed: {e:#}")); } } } } } return; } match self.build_chunks(&segs, cap) { Ok(chunks) => self.job = Some(PrefillJob { segs, chunks, next: 0, rids: admitted }), Err(e) => { for r in admitted { self.fail_req(r, format!("prefill failed: {e:#}")); } } } } /// Plan one request: cache lookup per branch, shared region, page allocation, segments. Ok(None) = does not fit now (deferred). fn plan_request(&mut self, rid: u64, segs: &mut Vec, inflight: &mut Vec>) -> Result> { let (n, lens): (usize, Vec) = { let r = &self.reqs[&rid]; (r.inp.items.len(), r.inp.ids.iter().map(|x| x.len()).collect()) }; // 1. match + lock let mut matched: Vec> = vec![]; let mut nodes: Vec = vec![]; let mut cmax: Vec = vec![]; let mut qpages = 0usize; for b in 0..n { let l = lens[b]; let c = (l - 1) / PAGE; // trap (b): the last two prompt positions are always recomputed cmax.push(c); qpages += c; let (mut pages, node) = if self.cfg.prefix_cache && c > 0 { let k = page_keys(&self.reqs[&rid].inp.keys[b][..c * PAGE]); self.tree.match_prefix(&k) } else { (vec![], ROOT) }; if let Some(sw) = &self.swa { // SWA partial miss: the hit is only usable up to the last point whose preceding window is still resident (the node lock stays deeper: harmless) pages.truncate(sw.valid_len(&pages)); } self.tree.inc_lock(node); for &p in &pages { self.pool.incref(p); } matched.push(pages); nodes.push(node); } let rollback = |s: &mut Sched, matched: &Vec>, nodes: &Vec| { for (m, &nd) in matched.iter().zip(nodes) { for &p in m { s.pool.decref(p); } s.tree.dec_lock(nd); } }; // in-batch prefix de-duplication (SGLang schedule_policy in-batch prefix caching): wait for the request admitted earlier in this iteration if self.cfg.prefix_cache && !inflight.is_empty() { let k0 = page_keys(&self.reqs[&rid].inp.keys[0][..cmax[0] * PAGE]); let best = inflight.iter().map(|o| o.iter().zip(&k0).take_while(|(a, b)| a == b).count()).max().unwrap_or(0); if best > matched[0].len() { rollback(self, &matched, &nodes); self.st.deferred_in_batch += 1; return Ok(None); } } // 2. shared region let r = &self.reqs[&rid]; let keys = &r.inp.keys; let mut sp_pages = 0usize; if n >= 2 { let mut lcp = keys[0].len(); for k in &keys[1..] { lcp = lcp.min(keys[0].iter().zip(k).take_while(|(a, b)| a == b).count()); } sp_pages = (lcp / PAGE).min(*cmax.iter().min().unwrap()); // trap (e): an image block must not straddle the shared/tail boundary loop { let bnd = sp_pages * PAGE; match r.inp.blocks[0].iter().find(|bl| bl.s < bnd && bnd < bl.e) { Some(bl) => sp_pages = bl.s / PAGE, None => break, } } } let mcl: Vec = matched.iter().map(|m| m.len()).collect(); let ms = (0..n).map(|b| mcl[b].min(sp_pages)).min().unwrap(); let need_shared = sp_pages - ms; let total_pages: Vec = lens.iter().map(|&l| (l + 1 + PAGE - 1) / PAGE).collect(); let start_pages: Vec = (0..n).map(|b| sp_pages.max(matched[b].len())).collect(); let need: usize = need_shared + (0..n).map(|b| total_pages[b] - start_pages[b]).sum::(); let _ = r; // a request that needs more pages than the WHOLE pool can never be admitted (not a transient shortage): fail it as a capacity limit if need > self.pool.rc.len() { rollback(self, &matched, &nodes); bail!("KV pool exhausted: KV cache capacity exceeded ({} pages needed, {} pages total)", need, self.pool.rc.len()); } // 3. allocate let alloc = match self.alloc(need) { Ok(a) => a, Err(e) => { rollback(self, &matched, &nodes); if self.live_branches == 0 && segs.is_empty() { return Err(e); } return Ok(None); } }; // sliding-window reserve: the most sliding pages this request's prefill can hold at once (exact for short prompts; window + one forward for long ones) if self.swa_on() { let sw = self.swa.as_ref().unwrap(); let bound = sw.wpages() + 2 * SWA_SLACK_PAGES + self.model.max_rows() / PAGE + 4; let p_req = need_shared.min(bound) + (0..n).map(|b| (total_pages[b] - start_pages[b]).min(bound)).sum::(); if p_req > sw.n { for &p in &alloc { self.pool.decref(p); } rollback(self, &matched, &nodes); let total = self.swa.as_ref().unwrap().n; bail!("sliding-window KV pool exhausted: KV cache capacity exceeded ({} pages needed, {} pages total)", p_req, total); } if !self.sl_make_free(p_req) { for &p in &alloc { self.pool.decref(p); } rollback(self, &matched, &nodes); if self.live_branches == 0 && segs.is_empty() { let sw = self.swa.as_ref().unwrap(); bail!("sliding-window KV pool exhausted ({} pages total, {} needed, {} free)", sw.n, p_req, sw.free.len()); } return Ok(None); } let sw = self.swa.as_mut().unwrap(); let at = sw.free.len() - p_req; let res: Vec = sw.free.drain(at..).collect(); let r = self.reqs.get_mut(&rid).unwrap(); r.sl_res = res; r.sl_cap = p_req; } let mut it = alloc.into_iter(); let new_shared: Vec = (0..need_shared).map(|_| it.next().unwrap()).collect(); let mut shared: Vec = matched[0][..ms].to_vec(); shared.extend_from_slice(&new_shared); for _ in 1..n { for &p in &new_shared { self.pool.incref(p); } } // 4. branches + segments let (pol, ids0_blocks) = { let r = &self.reqs[&rid]; (r.inp.pol, r.inp.blocks[0].clone()) }; let mut rows = 0; if need_shared > 0 { let r = &self.reqs[&rid]; segs.push(Seg { tokens: r.inp.ids[0][ms * PAGE..sp_pages * PAGE].to_vec(), start: ms * PAGE, pages: shared.clone(), blocks: ids0_blocks, req: rid, branch: None }); rows += need_shared * PAGE; } for b in 0..n { let mut pages = shared.clone(); if matched[b].len() > sp_pages { pages.extend_from_slice(&matched[b][sp_pages..]); } for _ in start_pages[b]..total_pages[b] { pages.push(it.next().unwrap()); } // matched pages beyond the usable cap are dropped (their refs were taken: release the surplus) let r = &self.reqs[&rid]; let l = lens[b]; let start = start_pages[b] * PAGE; let mut toks: Vec = r.inp.ids[b][start..l].to_vec(); toks.push(MK); rows += toks.len(); let blocks = r.inp.blocks[b].clone(); let item = &r.inp.items[b]; let n_unexp = item.ids.len(); let n_img = r.inp.images.len(); let room = self.cfg.max_len as i64 - n_unexp as i64 - ((dllm_core::tokenizer::img_soft_max() + 2) * n_img) as i64 - 4; let mt = (pol.cap as i64 + 3).min(room).max(4); let maxgen = (pol.cap as i64 + 1).min(mt) as usize; let br = Branch { req: rid, item: b, number: item.number, ans: vec![], ans_lp: 0.0, ans_pos: 0, l, pol, pages: pages.clone(), node: nodes[b], phase: Phase::Prefill, p0: vec![], gi: vec![], fed: 0, maxgen, base: l, feed: vec![], probe_pos: 0, nt: 0, out_tok: 1, deadline: if pol.budget_ms > 0 { Some(self.reqs[&rid].arrive + std::time::Duration::from_millis(pol.budget_ms as u64)) } else { None }, sl: vec![], sl_cur: 0, sp_pages, }; let bi = self.new_branch(br); if self.swa_on() { // refs on the resident (cached / shared) pages inside the window of this branch's first query (its tail, or the shared segment it waits for) let q0 = if need_shared > 0 { ms * PAGE } else { start }; let lo = self.swa.as_ref().unwrap().keep_lo(q0); let np = self.branches[bi].as_ref().unwrap().pages.len(); self.branches[bi].as_mut().unwrap().sl = vec![false; np]; self.branches[bi].as_mut().unwrap().sl_cur = lo; for j in lo..start_pages[b].max(sp_pages) { let f = self.branches[bi].as_ref().unwrap().pages[j]; if self.swa.as_ref().unwrap().smap[f as usize] != 0 { self.sl_ref(bi, j); } } } segs.push(Seg { tokens: toks, start, pages, blocks, req: rid, branch: Some(bi) }); } self.st.cache_query_pages += qpages as u64; self.st.cache_hit_pages += (0..n).map(|b| matched[b].len().min(cmax[b])).sum::() as u64; if self.cfg.prefix_cache { inflight.push(page_keys(&self.reqs[&rid].inp.keys[0][..cmax[0] * PAGE])); } Ok(Some(rows)) } fn soft_for(&mut self, rid: u64, img: usize) -> Result> { let (hash, gh, gw, patches) = { let p = &self.reqs[&rid].inp.images[img]; (p.hash ^ ((p.n_soft as u64) << 48), p.gh, p.gw, p.patches.clone()) }; if let Some(pos) = self.soft_cache.iter().position(|(h, _)| *h == hash) { let e = self.soft_cache.remove(pos); self.soft_cache.push(e.clone()); self.st.soft_cache_hits += 1; return Ok(e.1); } let v = self.vision.as_mut().ok_or_else(|| anyhow!("no vision encoder"))?; let s = Arc::new(v.encode(&patches, gh, gw)?); self.st.vision_encodes += 1; self.soft_cache.push((hash, s.clone())); while self.soft_cache.len() > self.cfg.soft_cache_images.max(1) { self.soft_cache.remove(0); } Ok(s) } fn build_chunks(&self, segs: &[Seg], max_rows: usize) -> Result>> { // chunking: pieces (seg, lo, hi) let mut chunks: Vec> = vec![vec![]]; let mut cur = 0usize; for (si, s) in segs.iter().enumerate() { let mut lo = 0; while lo < s.tokens.len() { let rem = s.tokens.len() - lo; let room = max_rows - cur; if rem <= room { chunks.last_mut().unwrap().push((si, lo, s.tokens.len())); cur += rem; break; } // split: cut <= room, not inside an image block, tail segs keep their last two rows together let mut cut = room.min(rem); if s.branch.is_some() { cut = cut.min(rem.saturating_sub(2)); } loop { let a = s.start + lo + cut; match s.blocks.iter().find(|b| b.s < a && a < b.e) { Some(b) => cut = if b.s > s.start + lo { b.s - (s.start + lo) } else { 0 }, None => break, } if cut == 0 { break; } } if cut == 0 { if chunks.last().unwrap().is_empty() { bail!("cannot chunk prefill: an unsplittable image block / row group exceeds max_rows {max_rows}"); } chunks.push(vec![]); cur = 0; continue; } chunks.last_mut().unwrap().push((si, lo, lo + cut)); lo += cut; chunks.push(vec![]); cur = 0; } } chunks.retain(|c| !c.is_empty()); Ok(chunks) } fn run_chunk(&mut self, segs: &[Seg], ch: &[(usize, usize, usize)]) -> Result<()> { let mut b = RowBatch::default(); let mut offs: HashMap = HashMap::new(); let mut done: Vec<(usize, usize, usize)> = vec![]; // (branch, gate row, readout row) let mut soft_rows: Vec<(u64, usize, i32, i32)> = vec![]; // (req, img, row, idx) for &(si, lo, hi) in ch { let s = &segs[si]; let off = *offs.entry(si).or_insert_with(|| { let o = b.block_table.len() as i32; b.block_table.extend_from_slice(&s.pages); o }); for i in lo..hi { let pos = s.start + i; let row = b.tokens.len() as i32; let blk = s.blocks.iter().find(|bl| bl.s <= pos && pos < bl.e); b.tokens.push(s.tokens[i]); b.pos.push(pos as i32); b.slot.push(s.pages[pos / PAGE] * PAGE as i32 + (pos % PAGE) as i32); b.bt_off.push(off); b.kv_hi.push(pos as i32); b.kv_hi_sliding.push(match blk { Some(bl) => (bl.e - 1) as i32, None => pos as i32, }); if let Some(bl) = blk { soft_rows.push((s.req, bl.img, row, (pos - bl.s) as i32)); } } if let (Some(bi), true) = (s.branch, hi == s.tokens.len()) { let r = b.tokens.len() - 1; done.push((bi, r - 1, r)); } } // soft tokens (tower encodes of uncached images run here, asynchronously, and finish inside the forward's sync below) let (tv, enc0) = (Instant::now(), self.st.vision_encodes); let mut holders: Vec> = vec![]; let mut map: HashMap<(u64, usize), usize> = HashMap::new(); let mut rows: Vec = vec![]; for (rid, img, row, idx) in soft_rows { let k = *match map.get(&(rid, img)) { Some(k) => k, None => { let t = self.soft_for(rid, img)?; holders.push(t); map.insert((rid, img), holders.len() - 1); map.get(&(rid, img)).unwrap() } }; rows.push(SoftRow { img: k, row, idx }); } if self.swa_on() { self.sl_chunk(segs, ch, false)?; self.swa_flush()?; } let t0 = Instant::now(); if rows.is_empty() { self.model.forward(&b, None)?; } else { let sel = SoftSel { imgs: holders.iter().map(|h| h.as_ref()).collect(), rows }; self.model.forward(&b, Some(&sel))?; } self.model.sync()?; let el = t0.elapsed().as_secs_f64() * 1e3; let n_enc = self.st.vision_encodes - enc0; if n_enc > 0 { // time beyond the forward model = the tower encodes; the chunk stays out of the forward fit let (a, bb) = self.pf_model(); let vis = tv.elapsed().as_secs_f64() * 1e3 - (a + bb * b.rows() as f64); self.ema_vis_ms = (0.8 * self.ema_vis_ms + 0.2 * (vis / n_enc as f64)).clamp(1.0, 100.0); } if b.rows() >= 32 && n_enc == 0 { self.ema_pf_ms_row = 0.8 * self.ema_pf_ms_row + 0.2 * (el / b.rows() as f64); let (x, y) = (b.rows() as f64, el); let f = &mut self.pf_fit; for v in f.iter_mut() { *v *= 0.98; } f[0] += 1.0; f[1] += x; f[2] += y; f[3] += x * x; f[4] += x * y; } self.st.prefill_forwards += 1; self.st.prefill_rows += b.rows() as u64; if self.swa_on() { self.sl_chunk(segs, ch, true)?; } if !done.is_empty() { self.finish_prefill(done)?; } Ok(()) } /// end of a branch's prefill: letters at the readout row, gate decision, first thought token, cache insertion fn finish_prefill(&mut self, done: Vec<(usize, usize, usize)>) -> Result<()> { // letter logits grouped by label set let mut p0: HashMap> = HashMap::new(); let mut groups: Vec<(Vec, Vec<(usize, usize)>)> = vec![]; for &(bi, _g, r) in &done { let b = self.branches[bi].as_ref().unwrap(); if b.number { continue; } let lab: Vec = self.reqs[&b.req].inp.items[b.item].label_ids.iter().map(|&x| x as i32).collect(); match groups.iter_mut().find(|g| g.0 == lab) { Some(g) => g.1.push((bi, r)), None => groups.push((lab, vec![(bi, r)])), } } for (lab, members) in &groups { let rows: Vec = members.iter().map(|m| m.1).collect(); let lg = self.model.letter_logits(&rows, lab)?; for ((bi, _), z) in members.iter().zip(lg) { p0.insert(*bi, z); } } // gate decisions let mut think: Vec<(usize, usize)> = vec![]; // (branch, gate row) let rsv_g = self.reserve_ms(); let mut skipped = 0u64; let mut answer_now: Vec<(usize, usize)> = vec![]; // number questions answered without a thought: (branch, end-of-thought row) for &(bi, g, r) in &done { let b = self.branches[bi].as_mut().unwrap(); let t = if b.number { // the trained gate reads option letters: number questions think only when the request asks for it b.pol.think_numbers && b.pol.gate != GateMode::Never } else { let z = p0.remove(&bi).unwrap(); let pm = softmax64(&z).into_iter().fold(0.0f64, f64::max); b.p0 = z; match b.pol.gate { GateMode::Tau => pm < b.pol.tau as f64, GateMode::Never => false, } }; // a request whose budget is (almost) gone cannot think usefully; skip thinking (answer from p0) instead of burning decode steps + a readout on it let hopeless = t && b.deadline.map_or(false, |d| d.saturating_duration_since(Instant::now()).as_secs_f64() * 1e3 - rsv_g < MIN_THINK_MS); if hopeless { skipped += 1; } else if t { think.push((bi, g)); } if b.number && !(t && !hopeless) { answer_now.push((bi, r)); } } self.st.skipped_think += skipped; // first thought token d = argmax excluding MK / EOS / if !think.is_empty() { let rows: Vec = think.iter().map(|x| x.1).collect(); let d = self.model.argmax_rows(&rows, &[MK, EOS, TURN_END])?; for ((bi, _), t) in think.iter().zip(d) { let b = self.branches[*bi].as_mut().unwrap(); b.gi = vec![t]; b.fed = 0; if b.pol.think_ms > 0 { b.phase = Phase::Gated; } else { b.phase = Phase::Think; self.st.think_items += 1; } } } // number questions without a thought: the end-of-thought row gives the first answer token if !answer_now.is_empty() { let rows: Vec = answer_now.iter().map(|x| x.1).collect(); let lg = self.model.logits_rows(&rows)?; for (&(bi, row), z) in answer_now.iter().zip(lg) { let pos = self.branches[bi].as_ref().unwrap().l + 1; self.branches[bi].as_mut().unwrap().phase = Phase::Answer; self.start_answer(bi, &z, pos, row); } } let mut rids: Vec = done.iter().filter_map(|d| self.branches[d.0].as_ref().map(|b| b.req)).collect(); rids.sort_unstable(); rids.dedup(); // sliding pool: a request whose prefill is complete returns the rest of its reserve (thinking growth draws from the free list) if self.swa_on() { for &rid in &rids { let pending = (0..self.branches.len()).any(|i| !done.iter().any(|d| d.0 == i) && self.branches[i].as_ref().map_or(false, |b| b.req == rid && b.phase == Phase::Prefill)); if !pending { if let Some(r) = self.reqs.get_mut(&rid) { let res = std::mem::take(&mut r.sl_res); r.sl_cap = 0; self.sl_drop_reserve(res); } } } } // cache insertion + completion of non-thinking branches let bis: Vec = done.iter().map(|d| d.0).filter(|&bi| self.branches[bi].is_some()).collect(); for bi in bis { if self.cfg.prefix_cache { self.insert_branch(bi); } let b = self.branches[bi].as_ref().unwrap(); if self.swa_on() && matches!(b.phase, Phase::Think | Phase::Answer) { let q = if b.phase == Phase::Answer { b.ans_pos + b.ans.len() - 1 } else { b.l }; self.sl_trim(bi, q); } let b = self.branches[bi].as_ref().unwrap(); if b.phase == Phase::Prefill { let res = ItemResult { p0: b.p0.clone(), think: None, trace_tokens: 0, out_tokens: 1, ..Default::default() }; self.finish_item(bi, res); } } // --think-ms: once every question of a request has its gate decision, its gated branches get the request's deterministic cap for rid in rids { self.release_gated(rid); } Ok(()) } /// `--think-ms`: when no question of request `rid` is still in prefill, give its gated branches the per-branch cap of the fixed cost model /// (a pure function of the request: prefill rows, images, questions and the number of thinking questions) and start them; a cap of 0 /// answers them from p0 without thinking fn release_gated(&mut self, rid: u64) { let mut gated = vec![]; for (i, b) in self.branches.iter().enumerate() { if let Some(b) = b.as_ref().filter(|b| b.req == rid) { match b.phase { Phase::Prefill => return, Phase::Gated => gated.push(i), _ => {} } } } if gated.is_empty() { return; } let shape = ThinkShape { thinking: gated.len(), ..self.reqs[&rid].inp.shape }; let pol = self.branches[gated[0]].as_ref().unwrap().pol; let cap = pol.think_cost.cap(pol.think_ms as f64, &shape, pol.cap); for bi in gated { if cap == 0 && self.branches[bi].as_ref().unwrap().number { // no thought: re-read the end-of-thought row (position l, already in the KV cache) for the first answer token self.st.skipped_think += 1; let b = self.branches[bi].as_mut().unwrap(); b.gi.clear(); b.feed = vec![MK]; b.probe_pos = b.l; b.phase = Phase::Probe; } else if cap == 0 { self.st.skipped_think += 1; let b = self.branches[bi].as_ref().unwrap(); let res = ItemResult { p0: b.p0.clone(), think: None, trace_tokens: 0, out_tokens: 1, ..Default::default() }; self.finish_item(bi, res); } else { let b = self.branches[bi].as_mut().unwrap(); b.pol.cap = cap; b.maxgen = b.maxgen.min(cap + 1); b.phase = Phase::Think; self.st.think_items += 1; } } } /// hand the full prompt pages of a prefilled branch to the tree (SGLang cache_unfinished_req) fn insert_branch(&mut self, bi: usize) { let (rid, item, l) = { let b = self.branches[bi].as_ref().unwrap(); (b.req, b.item, b.l) }; let n_full = l / PAGE; if n_full == 0 { return; } let key = page_keys(&self.reqs[&rid].inp.keys[item][..n_full * PAGE]); let pages: Vec = self.branches[bi].as_ref().unwrap().pages[..n_full].to_vec(); let (mm, _) = self.tree.insert(&key, &pages); for &p in &pages[mm..n_full] { self.pool.incref(p); // adopted by the tree } let (tp, node) = self.tree.match_prefix(&key); debug_assert_eq!(tp.len(), n_full); self.tree.inc_lock(node); let old = self.branches[bi].as_ref().unwrap().node; if old != ROOT { self.tree.dec_lock(old); } let b = self.branches[bi].as_mut().unwrap(); b.node = node; for i in 0..n_full { let b = self.branches[bi].as_mut().unwrap(); if b.pages[i] != tp[i] { // identical content already cached under another page: switch to it, drop ours // (sliding pool: only if the cached page still has the sliding KV this branch's window needs) let held = b.sl.get(i).copied().unwrap_or(false); if held && self.swa.as_ref().unwrap().smap[tp[i] as usize] == 0 { continue; } let ours = b.pages[i]; b.pages[i] = tp[i]; self.pool.incref(tp[i]); if held { self.branches[bi].as_mut().unwrap().sl[i] = false; self.sl_ref(bi, i); self.sl_unref(ours); } self.pool.decref(ours); } } // the tree keeps the sliding KV of the prompt's window tail (+1 window) and of the window before the shared-prefix boundary if self.swa_on() { let sp = self.branches[bi].as_ref().unwrap().sp_pages; for r in self.sl_claim_ranges(l, sp) { for j in r.start..r.end.min(n_full) { let f = tp[j] as usize; let sw = self.swa.as_mut().unwrap(); if sw.smap[f] != 0 && !sw.tree_sl[f] { sw.tree_sl[f] = true; sw.tree_n += 1; sw.src[f] += 1; } } } } } // -------------------------------------------------------------------------------------------- decode fn ensure_pages(&mut self, bi: usize, last_pos: usize) -> Result<()> { let have = self.branches[bi].as_ref().unwrap().pages.len(); let need = last_pos / PAGE + 1; if need > have { let np = self.alloc(need - have)?; self.branches[bi].as_mut().unwrap().pages.extend(np); } Ok(()) } fn decode_step(&mut self) { let la = self.think.lookahead(); let now = Instant::now() + std::time::Duration::from_secs_f64(self.reserve_ms() / 1e3); let late: Vec = (0..self.branches.len()).filter(|&i| self.branches[i].as_ref().map_or(false, |b| b.phase == Phase::Think && b.deadline.map_or(false, |d| now >= d))).collect(); for bi in late { self.st.forced_by_budget += 1; self.end_think(bi); } let mut act: Vec = (0..self.branches.len()).filter(|&i| self.branches[i].as_ref().map_or(false, |b| b.phase == Phase::Think)).collect(); if act.is_empty() { return; } // pages for the positions this step may write; a branch that cannot grow fails its request let mut failed: Vec<(usize, String)> = vec![]; let mut cut: Vec = vec![]; for &bi in &act { let pos = { let b = self.branches[bi].as_ref().unwrap(); b.base + b.gi.len() - 1 }; if let Err(e) = self.ensure_pages(bi, pos + la) { failed.push((bi, format!("{e:#}"))); } else if self.swa_on() { if self.sl_write(bi, pos, pos + la).is_err() { // sliding pool exhausted: the branch stops thinking and is read out (no new pages needed) cut.push(bi); } } } for &bi in &cut { self.st.swa_forced += 1; self.end_think(bi); } act.retain(|i| !cut.contains(i)); for (bi, m) in &failed { self.fail_branches(&[*bi], m); } act.retain(|&i| self.branches[i].is_some()); if act.is_empty() { return; } if let Err(e) = self.swa_flush() { self.fail_branches(&act, &format!("decode failed: {e:#}")); return; } let out = { let reqs: Vec = act .iter() .map(|&bi| { let b = self.branches[bi].as_ref().unwrap(); ThinkReq { pages: &b.pages, pos: b.base + b.gi.len() - 1, tok: *b.gi.last().unwrap(), uid: bi } }) .collect(); let t0 = Instant::now(); let r = self.think.step(self.model.as_mut(), &reqs); let el = t0.elapsed().as_secs_f64() * 1e3; self.ema_dec_ms = 0.9 * self.ema_dec_ms + 0.1 * el; r }; let out = match out { Ok(o) => o, Err(e) => { self.fail_branches(&act, &format!("decode failed: {e:#}")); return; } }; self.st.decode_forwards += 1; for (&bi, toks) in act.iter().zip(out) { let b = self.branches[bi].as_mut().unwrap(); b.fed = b.gi.len(); // the last token has been fed by this step let mut fin = false; for t in toks { b.gi.push(t); if is_stop(t) || b.gi.len() - 1 >= b.maxgen { fin = true; break; } } { // every returned token except the last was a row of the decoder's forward (KV written); only the last one still has to be fed // (equals the old `fed = len before the step` for one-token decoders; also when the step ended thinking: a speculative step may have accepted several tokens) let b = self.branches[bi].as_mut().unwrap(); b.fed = b.gi.len() - 1; } if fin { self.end_think(bi); } } if self.swa_on() { for &bi in &act { let q = match self.branches[bi].as_ref() { Some(b) if b.phase == Phase::Probe => b.probe_pos, Some(b) if b.phase == Phase::Think => b.base + b.gi.len() - 1, _ => continue, }; self.sl_trim(bi, q); } } } fn end_think(&mut self, bi: usize) { let b = self.branches[bi].as_mut().unwrap(); // thought = gi[:cap] cut at the first end-of-thought / stop token let cap = b.pol.cap; let mut th = b.gi[..b.gi.len().min(cap)].to_vec(); if let Some(j) = th.iter().position(|&t| is_stop(t)) { th.truncate(j); } b.nt = th.len(); b.out_tok = b.gi.len() + 1; let fed = b.fed.min(th.len()); b.probe_pos = b.l + fed; b.feed = th[fed..].to_vec(); b.feed.push(MK); b.phase = Phase::Probe; } fn probe_step(&mut self) -> bool { let mut act: Vec = (0..self.branches.len()).filter(|&i| self.branches[i].as_ref().map_or(false, |b| b.phase == Phase::Probe)).collect(); if act.is_empty() { return false; } let mut failed = vec![]; for &bi in &act { let (last, ) = { let b = self.branches[bi].as_ref().unwrap(); (b.probe_pos + b.feed.len() - 1,) }; if let Err(e) = self.ensure_pages(bi, last) { failed.push((bi, format!("{e:#}"))); } else if self.swa_on() { let p0 = self.branches[bi].as_ref().unwrap().probe_pos; if let Err(e) = self.sl_write(bi, p0, last) { failed.push((bi, format!("{e:#}"))); } } } for (bi, m) in &failed { self.fail_branches(&[*bi], m); } act.retain(|&i| self.branches[i].is_some()); let max_rows = self.model.max_rows(); // chunk by rows let mut start = 0; while start < act.len() { let mut end = start; let mut rows = 0; while end < act.len() { let n = self.branches[act[end]].as_ref().unwrap().feed.len(); if rows + n > max_rows && end > start { break; } rows += n; end += 1; } let chunk: Vec = act[start..end].to_vec(); start = end; if let Err(e) = self.probe_chunk(&chunk) { self.fail_branches(&chunk, &format!("probe failed: {e:#}")); } } true } fn probe_chunk(&mut self, chunk: &[usize]) -> Result<()> { let mut toks = vec![]; let mut seqs_meta: Vec<(usize, usize)> = vec![]; // (branch, pos) let mut last_rows = vec![]; for &bi in chunk { let b = self.branches[bi].as_ref().unwrap(); for (i, &t) in b.feed.iter().enumerate() { toks.push(t); seqs_meta.push((bi, b.probe_pos + i)); } last_rows.push(toks.len() - 1); } let batch = { let seqs: Vec<(&[i32], usize)> = seqs_meta.iter().map(|&(bi, pos)| (self.branches[bi].as_ref().unwrap().pages.as_slice(), pos)).collect(); RowBatch::decode(&toks, &seqs) }; self.swa_flush()?; self.model.forward(&batch, None)?; self.st.probe_forwards += 1; let mut groups: Vec<(Vec, Vec<(usize, usize)>)> = vec![]; let mut answers: Vec<(usize, usize)> = vec![]; for (k, &bi) in chunk.iter().enumerate() { let b = self.branches[bi].as_ref().unwrap(); if b.number { answers.push((bi, last_rows[k])); continue; } let lab: Vec = self.reqs[&b.req].inp.items[b.item].label_ids.iter().map(|&x| x as i32).collect(); match groups.iter_mut().find(|g| g.0 == lab) { Some(g) => g.1.push((bi, last_rows[k])), None => groups.push((lab, vec![(bi, last_rows[k])])), } } for (lab, members) in groups { let rows: Vec = members.iter().map(|m| m.1).collect(); let lg = self.model.letter_logits(&rows, &lab)?; for ((bi, _), z) in members.into_iter().zip(lg) { self.probe_done(bi, z); } } if !answers.is_empty() { let rows: Vec = answers.iter().map(|m| m.1).collect(); let lg = self.model.logits_rows(&rows)?; for ((bi, row), z) in answers.into_iter().zip(lg) { let b = self.branches[bi].as_mut().unwrap(); let pos = b.probe_pos + b.feed.len(); if b.nt > 0 || !b.gi.is_empty() { self.st.thought_tokens += b.nt as u64; } b.phase = Phase::Answer; self.start_answer(bi, &z, pos, row); } } Ok(()) } /// number question: first answer token from the logits of the end-of-thought row (`row` of the last forward); the answer continues in `answer_step` fn start_answer(&mut self, bi: usize, z: &[f32], pos: usize, row: usize) { let (t, lp) = greedy_logprob(z); let b = self.branches[bi].as_mut().unwrap(); b.ans_pos = pos; if is_stop(t) { self.finish_number(bi); } else { b.ans = vec![t]; b.ans_lp = lp; if let Err(e) = self.think.prime(self.model.as_mut(), bi, row, pos, t) { self.fail_branches(&[bi], &format!("answer step failed: {e:#}")); } } } /// one decode cycle for every answering number question (all of them in one step of the decoder: speculative with the MTP draft, or one greedy row) fn answer_step(&mut self) { let act: Vec = (0..self.branches.len()).filter(|&i| self.branches[i].as_ref().map_or(false, |b| b.phase == Phase::Answer)).collect(); if act.is_empty() { return; } let la = self.think.lookahead(); let mut ok = vec![]; for &bi in &act { let pos = { let b = self.branches[bi].as_ref().unwrap(); b.ans_pos + b.ans.len() - 1 }; let room = self.ensure_pages(bi, pos + la).is_ok() && (!self.swa_on() || self.sl_write(bi, pos, pos + la).is_ok()); if room { ok.push(bi); } else { self.finish_number(bi); // pool exhausted: answer with the tokens so far } } if ok.is_empty() { return; } if let Err(e) = self.swa_flush() { self.fail_branches(&ok, &format!("answer step failed: {e:#}")); return; } let out = { let reqs: Vec = ok .iter() .map(|&bi| { let b = self.branches[bi].as_ref().unwrap(); ThinkReq { pages: &b.pages, pos: b.ans_pos + b.ans.len() - 1, tok: *b.ans.last().unwrap(), uid: bi } }) .collect(); self.think.answer_step(self.model.as_mut(), &reqs) }; let out = match out { Ok(o) => o, Err(e) => { self.fail_branches(&ok, &format!("answer step failed: {e:#}")); return; } }; self.st.decode_forwards += 1; for (&bi, toks) in ok.iter().zip(out) { let b = self.branches[bi].as_mut().unwrap(); let mut fin = false; for (t, lp) in toks { if is_stop(t) || b.ans.len() >= ANSWER_MAX { fin = true; break; } b.ans.push(t); b.ans_lp += lp; } let q = b.ans_pos + b.ans.len() - 1; if fin { self.finish_number(bi); } else if self.swa_on() { self.sl_trim(bi, q); } } } fn finish_number(&mut self, bi: usize) { let b = self.branches[bi].as_ref().unwrap(); let thought = !b.gi.is_empty(); let res = ItemResult { p0: vec![], think: thought.then(Vec::new), trace_tokens: if thought { b.nt } else { 0 }, out_tokens: if thought { b.out_tok } else { 1 } + b.ans.len() + 1, trace_ids: if thought { trace_ids(&b.gi, b.nt, &b.pol) } else { vec![] }, answer_ids: b.ans.iter().map(|&t| t as u32).collect(), answer_logprob: b.ans_lp, }; self.finish_item(bi, res); } fn probe_done(&mut self, bi: usize, z: Vec) { let b = self.branches[bi].as_mut().unwrap(); self.st.thought_tokens += b.nt as u64; let res = ItemResult { p0: b.p0.clone(), think: Some(z), trace_tokens: b.nt, out_tokens: b.out_tok, trace_ids: trace_ids(&b.gi, b.nt, &b.pol), ..Default::default() }; self.finish_item(bi, res); } } /// the first `n` generated thought tokens (the trace the readout was conditioned on), only when the request asked for it /// prefill rows of a request: the longest prefix shared by all its (image-expanded) prompts once, plus every prompt's own tail pub fn prefill_rows(ids: &[Vec]) -> usize { let Some(first) = ids.first() else { return 0 }; let lcp = ids[1..].iter().fold(first.len(), |l, x| x.iter().zip(&first[..l]).take_while(|(a, b)| a == b).count()); lcp + ids.iter().map(|x| x.len() - lcp).sum::() } fn trace_ids(gi: &[i32], n: usize, pol: &Policy) -> Vec { if pol.return_trace { gi[..n.min(gi.len())].iter().map(|&t| t as u32).collect() } else { vec![] } } // ================================================================================================================ sliding-window KV pool // // The 25 sliding-window layers only ever read the last `window` (1024) positions, so their KV lives in a SEPARATE, smaller page pool (vLLM hybrid KV-cache // manager: per-group block tables with null blocks outside the window; SGLang SWAKVPool / SWARadixCache). Full-attention layers keep the full paged pool. // Identity of a page = its FULL page id (block tables, slots, radix tree, refcounts are unchanged); the model translates it for the sliding layers through a // device page map smap[full page] -> sliding page (0 = none, the never-read null page). // * attach on first write: before a forward every page a row writes gets a sliding page (from the owning request's reserve, else the global free list); // * refcounts: a branch holds a ref on page j while j >= lo(q) where q = the lowest position the branch may still query // (lo(q) = (q - window + 1) / 16 - SWA_SLACK_PAGES); the radix tree holds one ref per cached page whose sliding KV it keeps (`tree_sl`); // * a page whose refs drop to 0 is detached and its sliding page recycled (into the owner request's reserve while it lives, else the free list); // * prefix cache: a hit at page m is only usable if the pages covering the window before position 16m are resident (`sl_valid_len`); otherwise // the match is cut back (partial miss: only the cut part is recomputed). Inserting a branch lets the tree keep the window tail of the prompt // (plus one extra window and the window before the request's shared-prefix boundary, the usual divergence point of later requests); // * admission: a request reserves the sliding pages its prefill can hold at peak (all new pages for short prompts; window + one forward of rows for long ones), // so an admitted prefill never runs out mid-way (same guarantee as the up-front full-page allocation). pub(crate) const SWA_SLACK_PAGES: usize = 2; pub(crate) struct Swa { pub window: usize, pub smap: Vec, pub src: Vec, pub tree_sl: Vec, /// per sliding page: request that attached it pub owner: Vec, pub free: Vec, pub n: usize, /// pages with tree_sl set pub tree_n: usize, dirty: Option<(usize, usize)>, } impl Swa { fn new(full_pages: usize, n: usize, window: usize) -> Swa { Swa { window, smap: vec![0; full_pages], src: vec![0; full_pages], tree_sl: vec![false; full_pages], owner: vec![0; n + 1], free: (1..=n as i32).rev().collect(), n, tree_n: 0, dirty: None } } fn mark(&mut self, f: usize) { self.dirty = Some(match self.dirty { None => (f, f), Some((a, b)) => (a.min(f), b.max(f)), }); } /// first page index a query at position `q` may read pub fn need_lo(&self, q: usize) -> usize { q.saturating_sub(self.window - 1) / PAGE } /// first page index a branch whose lowest future query is `q` keeps (window + slack) pub fn keep_lo(&self, q: usize) -> usize { self.need_lo(q).saturating_sub(SWA_SLACK_PAGES) } /// pages of one window pub fn wpages(&self) -> usize { (self.window + PAGE - 1) / PAGE + 1 } /// longest usable prefix of a radix match `pages`: every page covering the window before the first recomputed position must be resident pub fn valid_len(&self, pages: &[i32]) -> usize { let mut m = pages.len(); loop { let lo = self.need_lo(m * PAGE); match (lo..m).rev().find(|&j| self.smap[pages[j] as usize] == 0) { None => return m, Some(t) => m = t, } } } } impl Sched { fn swa_on(&self) -> bool { self.swa.is_some() } /// evict LRU radix leaves (whole pages) until `need` pages were handed back; releases the tree's sliding refs of those pages fn evict_tree(&mut self, need: usize) -> usize { let mut ev: Vec = vec![]; let got = self.tree.evict(need, &mut |ps| ev.extend_from_slice(ps)); for p in ev { if let Some(sw) = self.swa.as_mut() { if sw.tree_sl[p as usize] { sw.tree_sl[p as usize] = false; sw.tree_n -= 1; self.sl_unref(p); } } self.pool.decref(p); } self.st.cache_evicted_pages += got as u64; got } /// make `n` sliding pages free in the global list (evicting cached prefixes); false if impossible fn sl_make_free(&mut self, n: usize) -> bool { loop { let free = self.swa.as_ref().unwrap().free.len(); if free >= n { return true; } if self.evict_tree((n - free).max(4)) == 0 { return false; } } } /// drop one sliding ref of full page `f`; at zero the sliding page is detached and recycled fn sl_unref(&mut self, f: i32) { let sw = self.swa.as_mut().unwrap(); let r = &mut sw.src[f as usize]; assert!(*r > 0, "sliding unref of page {f} without refs"); *r -= 1; if *r > 0 { return; } let s = sw.smap[f as usize]; sw.smap[f as usize] = 0; sw.mark(f as usize); let owner = sw.owner[s as usize]; sw.owner[s as usize] = 0; match self.reqs.get_mut(&owner) { Some(r) if r.sl_res.len() < r.sl_cap => r.sl_res.push(s), _ => self.swa.as_mut().unwrap().free.push(s), } } /// give full page `f` a sliding page (owner request `rid`) if it has none fn sl_attach(&mut self, f: i32, rid: u64) -> Result<()> { if self.swa.as_ref().unwrap().smap[f as usize] != 0 { return Ok(()); } let s = match self.reqs.get_mut(&rid).and_then(|r| r.sl_res.pop()) { Some(s) => s, None => { if !self.sl_make_free(1) { let sw = self.swa.as_ref().unwrap(); bail!("sliding-window KV pool exhausted ({} pages, {} free)", sw.n, sw.free.len()); } self.swa.as_mut().unwrap().free.pop().unwrap() } }; let sw = self.swa.as_mut().unwrap(); sw.smap[f as usize] = s; sw.owner[s as usize] = rid; sw.mark(f as usize); Ok(()) } /// branch `bi` takes its sliding ref on page index `j` (the page must be attached) fn sl_ref(&mut self, bi: usize, j: usize) { let b = self.branches[bi].as_mut().unwrap(); if b.sl.len() <= j { b.sl.resize(j + 1, false); } if b.sl[j] { return; } let f = b.pages[j] as usize; let sw = self.swa.as_mut().unwrap(); assert!(sw.smap[f] != 0, "sliding ref on unattached page {f}"); b.sl[j] = true; sw.src[f] += 1; } /// attach + ref the pages of branch `bi` that hold positions `p0..=p1` (rows about to be written) fn sl_write(&mut self, bi: usize, p0: usize, p1: usize) -> Result<()> { let rid = self.branches[bi].as_ref().unwrap().req; for j in p0 / PAGE..=p1 / PAGE { let f = self.branches[bi].as_ref().unwrap().pages[j]; self.sl_attach(f, rid)?; self.sl_ref(bi, j); } Ok(()) } /// the lowest position branch `bi` may still query is `q`: release its refs below the window (+ slack) fn sl_trim(&mut self, bi: usize, q: usize) { let lo = self.swa.as_ref().unwrap().keep_lo(q); let (from, to) = { let b = self.branches[bi].as_ref().unwrap(); (b.sl_cur, lo.min(b.sl.len())) }; for j in from..to { let b = self.branches[bi].as_mut().unwrap(); if b.sl[j] { b.sl[j] = false; let f = b.pages[j]; self.sl_unref(f); } } let b = self.branches[bi].as_mut().unwrap(); b.sl_cur = b.sl_cur.max(to); } /// push pending page-map changes to the model (before every forward) fn swa_flush(&mut self) -> Result<()> { let d = match self.swa.as_mut() { Some(sw) => sw.dirty.take(), None => return Ok(()), }; if let Some((a, b)) = d { self.model.set_sliding_map(a, &self.swa.as_ref().unwrap().smap[a..=b])?; } Ok(()) } /// request `rid` leaves: its unused sliding reserve goes back to the free list fn sl_drop_reserve(&mut self, res: Vec) { if let Some(sw) = self.swa.as_mut() { sw.free.extend(res); } } /// pages the tree keeps sliding KV for after inserting a branch whose prompt is `l` tokens and whose request shares `sp` prefix pages: /// the window tail of the prompt plus one more window, and the window before the shared-prefix boundary fn sl_claim_ranges(&self, l: usize, sp: usize) -> Vec> { let sw = self.swa.as_ref().unwrap(); let mut v = vec![sw.keep_lo(l).saturating_sub(sw.wpages())..l / PAGE]; if sp > 0 { v.push(sw.keep_lo(sp * PAGE)..sp); } v } } impl Sched { /// sliding pool around one prefill forward: before (`after` = false) attach + ref the pages its rows write; after it, release what no future query reads fn sl_chunk(&mut self, segs: &[Seg], ch: &[(usize, usize, usize)], after: bool) -> Result<()> { for &(si, lo, hi) in ch { let s = &segs[si]; if !self.reqs.contains_key(&s.req) || hi <= lo { continue; } let (p0, p1) = (s.start + lo, s.start + hi - 1); let owners: Vec = match s.branch { Some(bi) => vec![bi], None => (0..self.branches.len()).filter(|&i| self.branches[i].as_ref().map_or(false, |b| b.req == s.req && b.phase == Phase::Prefill)).collect(), }; if after { if s.branch.is_none() || hi < s.tokens.len() { for &bi in &owners { self.sl_trim(bi, p1 + 1); } } } else { for j in p0 / PAGE..=p1 / PAGE { self.sl_attach(s.pages[j], s.req)?; for &bi in &owners { debug_assert_eq!(self.branches[bi].as_ref().unwrap().pages[j], s.pages[j]); self.sl_ref(bi, j); } } } } Ok(()) } } impl Sched { /// invariants when nothing is live: every full page is free or in the tree; every sliding page is free, reserved, or kept by the tree fn check_idle(&self) { let n = self.pool.rc.len(); assert_eq!(self.pool.free.len() + self.tree.total_pages(), n, "idle: full pages leaked"); if let Some(sw) = &self.swa { let mut tree_sl = 0; for f in 0..n { let t = sw.tree_sl[f] as u32; tree_sl += t as usize; assert_eq!(sw.src[f], t, "idle: page {f} has {} sliding refs (tree {t})", sw.src[f]); assert_eq!(sw.smap[f] != 0, t == 1, "idle: page {f} sliding attachment without a tree ref"); } let res: usize = self.reqs.values().map(|r| r.sl_res.len()).sum(); assert_eq!(tree_sl, sw.tree_n); assert_eq!(sw.free.len() + res + tree_sl, sw.n, "idle: sliding pages leaked"); } } }