open-standards/eoc/eoc-rs/crates/eoc-distributed/src/worker.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

236 lines
7.3 KiB
Rust

//! Worker abstractions for the distributed inference pool.
//!
//! A [`Worker`] is anything that can advertise a [`Capability`] set, report
//! a [`Load`] snapshot, and (optionally) serve work. The trait is async so
//! a worker can wrap a local model, a remote HTTP backend, or a vLLM /
//! Triton endpoint indifferently. The router, scheduler, batch admission
//! and KV-cache locality modules all operate purely on [`Worker`] +
//! [`Load`] snapshots, so adding a new accelerator class is additive.
use serde::{Deserialize, Serialize};
/// Accelerator class advertised by a worker.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Accelerator {
/// CPU-only worker (llama.cpp, ggml, vendor BLAS).
Cpu,
/// Nvidia / AMD GPU (CUDA, ROCm).
Gpu,
/// Google TPU.
Tpu,
/// Apple Neural Engine / Qualcomm Hexagon / etc.
Npu,
/// Fabric of mixed accelerators (heterogeneous node).
Mixed,
}
impl Accelerator {
/// Stable short tag, useful in logs.
pub fn tag(&self) -> &'static str {
match self {
Accelerator::Cpu => "cpu",
Accelerator::Gpu => "gpu",
Accelerator::Tpu => "tpu",
Accelerator::Npu => "npu",
Accelerator::Mixed => "mixed",
}
}
}
/// Static capabilities advertised by a worker at registration time.
///
/// The fields here are deliberately a superset of vLLM's `ModelConfig`,
/// Triton's model repository metadata, and Ray Serve's deployment
/// signature, so any one of those can be projected into a `Capability`
/// without losing routing-relevant information.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Capability {
/// Models served by this worker (one worker may host several).
pub models: Vec<String>,
/// Accelerator class.
pub accelerator: Accelerator,
/// Max concurrent requests the worker is willing to accept.
pub max_concurrency: u32,
/// Whether the worker supports continuous batching (vLLM / TGI).
pub continuous_batching: bool,
/// Whether the worker exposes a paged-attention KV cache.
pub paged_kv: bool,
/// Carbon zone the worker physically lives in. Matches the catalog
/// used by `eoc-carbon`.
pub zone: String,
}
impl Capability {
/// True if this worker can serve `model`.
pub fn serves(&self, model: &str) -> bool {
self.models.iter().any(|m| m == model)
}
}
/// Live load snapshot reported by a worker.
///
/// All fields are absolute, never deltas. `joules_per_token` is the
/// rolling estimate published by the worker's own meter — the scheduler
/// uses it directly for joule-weighted placement.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Load {
/// In-flight requests right now.
pub in_flight: u32,
/// Tokens queued ahead of any new request.
pub queued_tokens: u32,
/// EMA of per-request latency in milliseconds.
pub p50_latency_ms: u32,
/// EMA of per-request latency in milliseconds (tail).
pub p99_latency_ms: u32,
/// Energy efficiency reported by the worker (micro-joules per
/// generated token). Lower is greener.
pub micro_joules_per_token: u32,
/// gCO2e/kWh observed in the worker's zone right now.
pub g_co2e_per_kwh: f32,
}
impl Load {
/// An idle, perfectly green load. Useful as a default in tests.
pub fn idle() -> Self {
Self {
in_flight: 0,
queued_tokens: 0,
p50_latency_ms: 0,
p99_latency_ms: 0,
micro_joules_per_token: 0,
g_co2e_per_kwh: 0.0,
}
}
/// "Busyness" used by the least-busy router. Lower = better.
pub fn busyness(&self) -> f64 {
// Weighted: each in-flight request costs more than queued tokens
// alone because requests already past admission also hold KV
// entries. Coefficients match LiteLLM's default least-busy.
(self.in_flight as f64) * 4.0 + (self.queued_tokens as f64) * 0.1
}
/// Joule cost projection for a hypothetical request of `tokens`
/// generated tokens. Used by the joule-weighted router.
pub fn projected_micro_joules(&self, tokens: u32) -> u64 {
(self.micro_joules_per_token as u64) * (tokens as u64)
}
/// Grams of CO2 equivalent for `tokens` generated tokens at the
/// worker's current zone intensity. Uses the worker's reported
/// micro-joules-per-token and zone intensity directly.
pub fn projected_g_co2e(&self, tokens: u32) -> f64 {
let micro_j = self.projected_micro_joules(tokens) as f64;
let joules = micro_j / 1_000_000.0;
let kwh = joules / 3_600_000.0;
kwh * (self.g_co2e_per_kwh as f64)
}
}
/// A worker is a thing that serves inference requests.
///
/// Implementations wrap local models, remote HTTP backends (vLLM, Triton,
/// TGI), or speculative-decoding gateways. The trait is intentionally
/// minimal: routing, batching, KV-cache locality and failure detection
/// all happen *above* the worker.
#[async_trait::async_trait]
pub trait Worker: Send + Sync {
/// Stable id for this worker. Must be unique inside a [`Cluster`].
fn id(&self) -> &str;
/// Static capabilities.
fn capability(&self) -> &Capability;
/// Current load snapshot.
fn load(&self) -> Load;
}
/// In-memory worker used by tests and as a reference implementation. A
/// realistic backend would proxy to an out-of-process server, but
/// every routing decision in this crate is driven by snapshots, so we
/// can exercise everything in unit tests with no I/O.
#[derive(Debug, Clone)]
pub struct InMemoryWorker {
id: String,
cap: Capability,
load: Load,
}
impl InMemoryWorker {
/// Construct.
pub fn new(id: impl Into<String>, cap: Capability, load: Load) -> Self {
Self {
id: id.into(),
cap,
load,
}
}
/// Mutate the reported load — used by tests to simulate pressure.
pub fn set_load(&mut self, load: Load) {
self.load = load;
}
}
#[async_trait::async_trait]
impl Worker for InMemoryWorker {
fn id(&self) -> &str {
&self.id
}
fn capability(&self) -> &Capability {
&self.cap
}
fn load(&self) -> Load {
self.load
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cap(model: &str, zone: &str) -> Capability {
Capability {
models: vec![model.to_string()],
accelerator: Accelerator::Gpu,
max_concurrency: 16,
continuous_batching: true,
paged_kv: true,
zone: zone.to_string(),
}
}
#[test]
fn busyness_is_monotone() {
let a = Load {
in_flight: 1,
queued_tokens: 0,
..Load::idle()
};
let b = Load {
in_flight: 4,
queued_tokens: 0,
..Load::idle()
};
assert!(a.busyness() < b.busyness());
}
#[test]
fn joule_projection_is_linear() {
let l = Load {
micro_joules_per_token: 100,
..Load::idle()
};
assert_eq!(l.projected_micro_joules(10), 1_000);
assert_eq!(l.projected_micro_joules(100), 10_000);
}
#[test]
fn serves_filters_models() {
let c = cap("llama-70b", "EU-FR");
assert!(c.serves("llama-70b"));
assert!(!c.serves("mistral-7b"));
}
}