// SPDX-FileCopyrightText: Copyright (c) 2026 The SGLang Authors // SPDX-License-Identifier: Apache-2.0 use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use sgl_kv_indexer::{PrefixIndex, PrefixIndexError, PrefixMatch, PrefixOutcome}; use sgl_router::buckets_reorg::{Bucket, BucketGroups, BucketResolver, EngineGroup}; use sgl_router::config::AffinityConfig; use sgl_router::discovery::{ModelId, WorkerId, WorkerSpec}; use sgl_router::policies::prefix_provider::RadixTreePrefixProvider; use sgl_router::policies_reorg::admission::{Decision, EngineAdmission, EngineMetrics}; use sgl_router::policies_reorg::cache_aware::{CacheAwarePolicy, CacheSource, PrefixMemo}; use sgl_router::policies_reorg::power_of_two::PowerOfTwoPolicy; use sgl_router::policies_reorg::{PickError, PickRequest, Policy, Stage}; use sgl_router::state::kv_events::{ compute_block_hashes, compute_block_hashes_bigram, BlockSizeOracle, HashTree, KvWorkerId, }; use sgl_router::state::load_monitor::engine_reported_load::{ EngineReportedLoadTable, LoadStat, NativeCacheRankLoad, }; use sgl_router::workers::Worker; const TOKENS: [u32; 8] = [1, 2, 3, 4, 5, 6, 7, 8]; fn engine(id: &str, active: usize) -> Arc { let engine = Arc::new(Worker::new(WorkerSpec { id: WorkerId(id.into()), url: format!("http://{id}"), mode: Stage::Plain, model_ids: vec![ModelId("m".into())], bootstrap_port: None, })); engine.active_requests.store(active, Ordering::Relaxed); engine } fn config() -> AffinityConfig { AffinityConfig { cache_affinity_min_matched_tokens: Some(1), cache_switch_margin_tokens: 0, ..Default::default() } } fn oracle() -> Arc { let oracle = BlockSizeOracle::new(); oracle.try_set(1).unwrap(); oracle } fn local(entries: &[(&Arc, usize)]) -> Arc { let tree = Arc::new(HashTree::new()); let hashes = compute_block_hashes(&TOKENS, 1); for (worker, depth) in entries { tree.insert( &KvWorkerId::new(worker.url.clone(), 0), None, &hashes[..*depth], ); } Arc::new(CacheSource::Local(RadixTreePrefixProvider::new( tree, oracle(), ))) } fn request(model: &ModelId) -> PickRequest<'_> { PickRequest { token_ids: Some(&TOKENS), ..PickRequest::new(model, Stage::Plain, 8) } } fn report( table: &EngineReportedLoadTable, engine: &Worker, waiting: u64, pending: u64, at: Instant, ) { table.set( &engine.url, 0, LoadStat { num_running_reqs: 1, num_waiting_reqs: waiting, num_tokens: 10, max_total_num_tokens: 100, native_cache: Some(NativeCacheRankLoad { num_waiting_uncached_tokens: pending, num_total_tokens: 10, max_running_requests: 100, total_prefill_uncached_tokens: 0, total_prefill_busy_us: 0, }), }, at, ); } #[derive(Debug)] struct Reject { id: &'static str, calls: Mutex)>>, } impl Reject { fn new(id: &'static str) -> Arc { Arc::new(Self { id, calls: Mutex::new(Vec::new()), }) } } impl EngineAdmission for Reject { fn check(&self, engine: &Worker, metrics: &EngineMetrics) -> Result { self.calls .lock() .unwrap() .push((engine.id.0.clone(), metrics.waiting_requests)); Ok(if engine.id.0 == self.id { Decision::Reject("full".into()) } else { Decision::Allow }) } } struct Index { result: Result, calls: AtomicUsize, hashes: Mutex>>, } #[tonic::async_trait] impl PrefixIndex for Index { async fn match_prefix(&self, hashes: Vec) -> Result { self.calls.fetch_add(1, Ordering::Relaxed); self.hashes.lock().unwrap().push(hashes); tokio::task::yield_now().await; self.result.clone() } } fn remote( result: Result, oracle: Arc, ) -> (Arc, Arc) { let index = Arc::new(Index { result, calls: AtomicUsize::new(0), hashes: Mutex::new(Vec::new()), }); ( Arc::new(CacheSource::Remote { index: index.clone(), block_size: oracle, }), index, ) } fn matches(entries: &[(&str, u32)]) -> PrefixOutcome { PrefixOutcome::Matched { matches: entries .iter() .map(|(url, depth)| PrefixMatch { address: (*url).into(), worker_id: "not-a-routing-identity".into(), matched_prefix_blocks: *depth, }) .collect(), best_prefix_blocks: entries.iter().map(|(_, depth)| *depth).max().unwrap_or(0), } } #[tokio::test] async fn local_prefix_wins_within_group_and_threshold_misses_use_load() { let engines = [ engine("deep", 9), engine("shallow", 0), engine("outside", 0), ]; let source = local(&[(&engines[0], 7), (&engines[1], 4), (&engines[2], 8)]); let model = ModelId("m".into()); for (minimum, ratio, expected) in [(Some(1), None, 0), (Some(8), None, 1), (None, Some(0.9), 1)] { let policy = CacheAwarePolicy::new( source.clone(), EngineReportedLoadTable::new(), AffinityConfig { cache_affinity_min_matched_tokens: minimum, cache_affinity_min_match_ratio: ratio, ..config() }, ) .unwrap(); for stage in [Stage::Plain, Stage::Prefill] { let request = PickRequest { stage, ..request(&model) }; let pick = policy.pick(&engines[..2], &request).await.unwrap(); assert!(Arc::ptr_eq(&pick.engine, &engines[expected])); } } } #[tokio::test] async fn remote_urls_are_exact_duplicate_depths_merge_and_block_counts_are_capped() { let engines = [engine("a", 9), engine("b", 0)]; let (source, _) = remote( Ok(matches(&[ ("http://a", 1), ("http://a", u32::MAX), ("http://b/", u32::MAX), ])), oracle(), ); let policy = CacheAwarePolicy::new( source, EngineReportedLoadTable::new(), AffinityConfig { cache_affinity_min_match_ratio: Some(1.0), ..config() }, ) .unwrap(); let model = ModelId("m".into()); assert_eq!( policy .pick(&engines, &request(&model)) .await .unwrap() .engine .id .0, "a" ); } #[tokio::test] async fn memo_reuses_io_but_reruns_admission_and_group_selection() { let engines = [engine("a", 0), engine("b", 0)]; let (source, index) = remote(Ok(matches(&[("http://a", 8), ("http://b", 7)])), oracle()); let mut policy = CacheAwarePolicy::new(source.clone(), EngineReportedLoadTable::new(), config()).unwrap(); let admission = Reject::new("a"); policy.admission = admission.clone(); let memo = PrefixMemo::default(); let model = ModelId("m".into()); let request = PickRequest { prefix: Some(&memo), ..request(&model) }; assert!(matches!( policy.pick(&engines[..1], &request).await, Err(PickError::NoAdmissibleEngine(_)) )); assert_eq!( policy .pick(&engines[1..], &request) .await .unwrap() .engine .id .0, "b" ); assert_eq!( policy.pick(&engines, &request).await.unwrap().engine.id.0, "b" ); assert_eq!(index.calls.load(Ordering::Relaxed), 1); assert_eq!(admission.calls.lock().unwrap().len(), 4); // A second namespace in the same request must query its own backend. let (other, other_index) = remote(Ok(matches(&[("http://a", 8)])), oracle()); let policy = CacheAwarePolicy::new(other, EngineReportedLoadTable::new(), config()).unwrap(); assert_eq!( policy.pick(&engines, &request).await.unwrap().engine.id.0, "a" ); assert_eq!(other_index.calls.load(Ordering::Relaxed), 1); } #[tokio::test] async fn concurrent_picks_share_one_query_and_new_requests_query_again() { let engines = [engine("a", 0)]; let (source, index) = remote(Ok(PrefixOutcome::Empty), oracle()); let policy = CacheAwarePolicy::new(source, EngineReportedLoadTable::new(), config()).unwrap(); let model = ModelId("m".into()); let memo = PrefixMemo::default(); let request = PickRequest { prefix: Some(&memo), ..request(&model) }; let (left, right) = tokio::join!( policy.pick(&engines, &request), policy.pick(&engines, &request) ); assert!(left.is_ok() && right.is_ok()); assert_eq!(index.calls.load(Ordering::Relaxed), 1); let next = PrefixMemo::default(); policy .pick( &engines, &PickRequest { prefix: Some(&next), ..request }, ) .await .unwrap(); assert_eq!(index.calls.load(Ordering::Relaxed), 2); } #[tokio::test] async fn unavailable_index_falls_back_but_rejected_queries_fail() { let engines = [engine("a", 9), engine("b", 0)]; let model = ModelId("m".into()); for error in [ PrefixIndexError::Timeout, PrefixIndexError::Unreachable, PrefixIndexError::Overloaded, PrefixIndexError::QueryTooLarge, PrefixIndexError::Rejected(sgl_kv_indexer::RpcCode::InvalidArgument), ] { let rejected = matches!(error, PrefixIndexError::Rejected(_)); let (source, index) = remote(Err(error), oracle()); let policy = CacheAwarePolicy::new(source, EngineReportedLoadTable::new(), config()).unwrap(); let memo = PrefixMemo::default(); let request = PickRequest { prefix: Some(&memo), ..request(&model) }; let result = policy.pick(&engines, &request).await; if rejected { assert!(matches!(result, Err(PickError::InvalidSignal(_)))); } else { assert_eq!(result.unwrap().engine.id.0, "b"); policy.pick(&engines, &request).await.unwrap(); } assert_eq!(index.calls.load(Ordering::Relaxed), 1); } } #[tokio::test] async fn missing_tokens_or_block_size_skip_io_and_bigram_hashes_match_workers() { let engines = [engine("a", 0)]; let model = ModelId("m".into()); let (source, index) = remote(Ok(PrefixOutcome::Empty), BlockSizeOracle::new()); let policy = CacheAwarePolicy::new(source, EngineReportedLoadTable::new(), config()).unwrap(); policy.pick(&engines, &request(&model)).await.unwrap(); assert_eq!(index.calls.load(Ordering::Relaxed), 0); let oracle = oracle(); oracle.set_bigram(true); let (source, index) = remote(Ok(PrefixOutcome::Empty), oracle); let policy = CacheAwarePolicy::new(source, EngineReportedLoadTable::new(), config()).unwrap(); policy .pick(&engines, &PickRequest::new(&model, Stage::Plain, 8)) .await .unwrap(); assert_eq!(index.calls.load(Ordering::Relaxed), 0); policy.pick(&engines, &request(&model)).await.unwrap(); assert_eq!( index.hashes.lock().unwrap()[0], compute_block_hashes_bigram(&TOKENS, 1) ); } #[tokio::test] async fn queue_diversion_and_saturation_use_only_this_group() { let engines = [engine("owner", 0), engine("cold", 9), engine("outside", 0)]; let model = ModelId("m".into()); for (cold_waiting, floor, expected, reason) in [ (0, None, "cold", "no_cache_candidate"), (5, None, "owner", "saturation_pin"), (3, Some(2), "owner", "saturation_pin"), ] { let table = EngineReportedLoadTable::new(); report(&table, &engines[0], 5, 100, Instant::now()); report(&table, &engines[1], cold_waiting, 1, Instant::now()); report(&table, &engines[2], 0, 0, Instant::now()); let policy = CacheAwarePolicy::new( local(&[(&engines[0], 8)]), table, AffinityConfig { worker_queue_limit: Some(4), saturation_queue_floor: floor, ..config() }, ) .unwrap(); let pick = policy.pick(&engines[..2], &request(&model)).await.unwrap(); assert_eq!(pick.engine.id.0, expected); assert_eq!(pick.reason, reason); } } #[tokio::test] async fn hard_rejection_never_becomes_cold_fallback_or_saturation_bypass() { let engines = [engine("owner", 0), engine("cold", 9)]; let model = ModelId("m".into()); for floor in [None, Some(2)] { let table = EngineReportedLoadTable::new(); for engine in &engines { report(&table, engine, 5, 100, Instant::now()); } let mut policy = CacheAwarePolicy::new( local(&[(&engines[0], 8)]), table, AffinityConfig { worker_queue_limit: Some(4), saturation_queue_floor: floor, ..config() }, ) .unwrap(); let admission = Reject::new("owner"); policy.admission = admission.clone(); assert!(matches!( policy.pick(&engines, &request(&model)).await, Err(PickError::NoAdmissibleEngine(_)) )); assert_eq!( *admission.calls.lock().unwrap(), vec![("owner".into(), Some(5))] ); } let mut policy = CacheAwarePolicy::new(local(&[]), EngineReportedLoadTable::new(), config()).unwrap(); policy.admission = Reject::new("owner"); assert!(matches!( policy.pick(&engines, &request(&model)).await, Err(PickError::AdmissionRejected(_)) )); } #[tokio::test] async fn guard_switches_near_ties_only_with_complete_fresh_telemetry() { let engines = [engine("deep", 0), engine("shallow", 9)]; let model = ModelId("m".into()); for (margin, stale, expected) in [(0, false, "deep"), (1, false, "shallow"), (1, true, "deep")] { let table = EngineReportedLoadTable::new(); report(&table, &engines[0], 5, 100, Instant::now()); report( &table, &engines[1], 1, 1, Instant::now() - if stale { Duration::from_secs(3600) } else { Duration::ZERO }, ); let policy = CacheAwarePolicy::new( local(&[(&engines[0], 8), (&engines[1], 7)]), table, AffinityConfig { cache_switch_margin_tokens: margin, pressure_abs_threshold_tokens: 10, ..config() }, ) .unwrap(); assert_eq!( policy .pick(&engines, &request(&model)) .await .unwrap() .engine .id .0, expected ); } } #[tokio::test] async fn candidate_cap_is_applied_before_admission() { let engines = [engine("deep", 9), engine("shallow", 0)]; let model = ModelId("m".into()); let mut policy = CacheAwarePolicy::new( local(&[(&engines[0], 8), (&engines[1], 7)]), EngineReportedLoadTable::new(), AffinityConfig { cache_candidate_min_workers: 1, cache_candidate_max_workers: 1, ..config() }, ) .unwrap(); let admission = Reject::new("deep"); policy.admission = admission.clone(); assert!(matches!( policy.pick(&engines, &request(&model)).await, Err(PickError::NoAdmissibleEngine(_)) )); assert_eq!(admission.calls.lock().unwrap().len(), 1); } #[tokio::test] async fn decode_group_and_invalid_configuration_are_rejected() { let source = local(&[]); for invalid in [ AffinityConfig { cache_candidate_min_workers: 0, ..config() }, AffinityConfig { cache_candidate_ratio: f64::NAN, ..config() }, AffinityConfig { saturation_queue_floor: Some(1), ..config() }, ] { assert!(matches!( CacheAwarePolicy::new(source.clone(), EngineReportedLoadTable::new(), invalid), Err(PickError::InvalidConfiguration(_)) )); } let policy = Arc::new(CacheAwarePolicy::new(source, EngineReportedLoadTable::new(), config()).unwrap()); let model = ModelId("m".into()); assert!(matches!( policy .pick( &[engine("a", 0)], &PickRequest::new(&model, Stage::Decode, 8) ) .await, Err(PickError::InvalidConfiguration(_)) )); let pd = |prefill: Arc, decode: Arc| { Bucket::new( "pd", BucketGroups::Pd { prefill: EngineGroup::new(prefill), decode: EngineGroup::new(decode), }, ) }; let load = Arc::new(PowerOfTwoPolicy::new(EngineReportedLoadTable::new())); assert!(matches!( BucketResolver::new(vec![pd(load.clone(), policy.clone())]), Err(PickError::InvalidConfiguration(_)) )); assert!(BucketResolver::new(vec![pd(policy, load)]).is_ok()); }