open-standards/eoc/eoc-rs/crates/eoc-distributed/src/kv_cache_aware.rs
Transaction Science 6ea90afbed ingest wave A4: 3 crates · smart-byte-atproto + op-openbanking + eoc-distributed
Smart Byte (1 crate, 2127 LOC, 35 tests)
  smart-byte-atproto    ATProto (Bluesky federation) adapter.
                        PLC + did:web resolution; XRPC client; CARv1 encode/decode
                        with CIDv1 (dag-cbor + raw, SHA-256); sketch MST with
                        order-independent BLAKE3 root; Repo + UnsignedCommit /
                        SignedCommit (Ed25519 over CBOR); firehose transport
                        trait + in-memory impl; AT URI ↔ smart-byte AID bridge
                        with SAID commitment.

OpenPay (1 crate, 2397 LOC, 44 tests)
  op-openbanking        UK OBIE R/W v3.1.10/3.1.11, Berlin Group NextGenPSD2
                        XS2A v1.3.13, STET PSD2 v1.7, Australia CDR Banking v1.31,
                        FDX v5/v6, SGFinDex shapes. AISP / PISP / PIIS / VRP
                        traits. FAPI 1.0 Advanced (PS256/ES256, mTLS, RFC 8705
                        certificate binding, JWK registration). Operators wire
                        their own JWS signer + HTTP transport — keeps the crate
                        KMS-agnostic.

EOC (1 crate, 2179 LOC, 32 tests)
  eoc-distributed       Distributed-inference scheduling. Worker trait with
                        Capability + Accelerator (CPU/GPU/TPU/NPU/Mixed) +
                        Load.busyness()/projected_micro_joules()/projected_g_co2e().
                        RoundRobin / LeastBusy / LatencyWeighted / JouleWeighted
                        (default) / CarbonWeighted routers. Work-stealing
                        scheduler. vLLM-style continuous batching with KV-block
                        budget. Ray-Serve-style auto-scaler with cooldown.
                        KV-cache-aware sticky-session routing.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-24 12:22:09 -04:00

169 lines
5 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! KV-cache locality routing.
//!
//! When a worker already holds the prefix KV-cache for a session, sending
//! the *next* turn to the same worker avoids re-prefilling, which is
//! often 10-100× cheaper in joules than placing the work cold. This is
//! the trick behind vLLM's `PrefixCachingScheduler`, SGLang's RadixAttn,
//! and the "sticky session" mode in LiteLLM router.
//!
//! [`KvCacheAwareRouter`] is a thin index: `session_id -> worker_id`, with
//! a fallback to the joule-weighted router when the session is unknown
//! (cold start) or the previously-used worker is no longer alive.
use std::collections::HashMap;
use crate::error::{DistributedError, Result};
use crate::router::{Request, Router, Strategy};
use crate::worker::Worker;
/// Locality-aware request descriptor: like [`Request`] but with a
/// session id.
#[derive(Debug, Clone)]
pub struct LocalRequest<'a> {
/// Caller-side session id (matches the previous turn's session).
pub session_id: String,
/// Model.
pub model: &'a str,
/// Expected new tokens for the cold-start fallback's joule maths.
pub expected_tokens: u32,
}
/// KV-cache locality router.
#[derive(Debug)]
pub struct KvCacheAwareRouter {
sticky: HashMap<String, String>,
fallback: Router,
}
impl Default for KvCacheAwareRouter {
fn default() -> Self {
Self::new(Strategy::JouleWeighted)
}
}
impl KvCacheAwareRouter {
/// Construct with a fallback strategy for cold sessions.
pub fn new(fallback: Strategy) -> Self {
Self {
sticky: HashMap::new(),
fallback: Router::new(fallback),
}
}
/// Borrow the fallback router.
pub fn fallback(&self) -> &Router {
&self.fallback
}
/// Pick a worker for `req`. If the session has been seen and the
/// previously-used worker is still in `workers` and serves the
/// model, returns it (cache hit). Otherwise falls back to
/// joule-weighted placement and records the binding.
pub fn pick<'w>(
&mut self,
workers: &'w [&'w dyn Worker],
req: &LocalRequest<'_>,
) -> Result<&'w dyn Worker> {
if workers.is_empty() {
return Err(DistributedError::NoWorkers);
}
if let Some(prev) = self.sticky.get(&req.session_id) {
if let Some(w) = workers
.iter()
.copied()
.find(|w| w.id() == prev && w.capability().serves(req.model))
{
return Ok(w);
}
// Stale binding — drop it.
self.sticky.remove(&req.session_id);
}
let pick = self.fallback.pick(
workers,
&Request {
model: req.model,
expected_tokens: req.expected_tokens,
},
)?;
self.sticky
.insert(req.session_id.clone(), pick.id().to_string());
Ok(pick)
}
/// Forget the binding for `session_id`. Caller would invoke this on
/// session end, KV eviction, or worker failure.
pub fn forget(&mut self, session_id: &str) {
self.sticky.remove(session_id);
}
/// Number of remembered sessions.
pub fn len(&self) -> usize {
self.sticky.len()
}
/// Whether there are no remembered sessions.
pub fn is_empty(&self) -> bool {
self.sticky.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::worker::{Accelerator, Capability, InMemoryWorker, Load};
fn mk(id: &str, micro_j: u32) -> InMemoryWorker {
InMemoryWorker::new(
id,
Capability {
models: vec!["m".into()],
accelerator: Accelerator::Gpu,
max_concurrency: 8,
continuous_batching: true,
paged_kv: true,
zone: "EU-FR".into(),
},
Load {
micro_joules_per_token: micro_j,
..Load::idle()
},
)
}
#[test]
fn sticky_after_first_call() {
let a = mk("a", 200);
let b = mk("b", 50);
let pool: Vec<&dyn Worker> = vec![&a, &b];
let mut r = KvCacheAwareRouter::default();
let first = r
.pick(
&pool,
&LocalRequest {
session_id: "s1".into(),
model: "m",
expected_tokens: 10,
},
)
.expect("ok")
.id()
.to_string();
// Even if we flip the load so the OTHER worker would now be
// cheaper, the sticky binding must keep us on `first`.
let cheaper = mk("c", 1);
let pool2: Vec<&dyn Worker> = vec![&a, &b, &cheaper];
let second = r
.pick(
&pool2,
&LocalRequest {
session_id: "s1".into(),
model: "m",
expected_tokens: 10,
},
)
.expect("ok")
.id()
.to_string();
assert_eq!(first, second);
}
}