Skip to main content

cherenkov/control/
state.rs

1//! Small, bounded snapshots shared with the control thread. No prompts or logits.
2
3use crate::config::{Config, Source};
4use anyhow::{Result, ensure};
5use serde::Serialize;
6use serde_json::{Value, json};
7use std::sync::Mutex;
8use std::time::Instant;
9
10pub struct State {
11    source: Source,
12    config: Mutex<Versioned>,
13    stats: Mutex<Stats>,
14    started: Instant,
15}
16
17#[derive(Clone, Serialize)]
18pub struct Versioned {
19    pub generation: u64,
20    pub config: Config,
21}
22
23#[derive(Default, Serialize)]
24pub struct Stats {
25    pub ready: bool,
26    pub queued_requests: usize,
27    pub active_requests: usize,
28    pub completed_requests: u64,
29    pub failed_requests: u64,
30    pub rejected_requests: u64,
31    pub generated_tokens: u64,
32    pub prompt_tokens: u64,
33    pub cached_tokens: u64,
34    pub prefill_seconds: f64,
35    pub decode_seconds: f64,
36    pub current: Option<Current>,
37    pub cache: CacheStats,
38    pub memory: MemoryStats,
39    pub http_address: Option<String>,
40    pub cancelled_requests: u64,
41    pub active_state_reserved_bytes: usize,
42    /// Contended prefill chunk size the worker is currently pacing toward.
43    pub prefill_chunk_tokens: usize,
44    pub sessions: SessionStats,
45    pub active: Vec<ActiveRequest>,
46    #[serde(skip)]
47    pub(crate) activity: crate::qwen4_exp::gpu::ExpertActivity,
48}
49
50#[derive(Default, Serialize)]
51pub struct SessionStats {
52    pub entries: usize,
53    pub bytes: usize,
54    pub evictions: u64,
55}
56
57#[derive(Serialize)]
58pub struct ActiveRequest {
59    pub id: String,
60    pub session_id: Option<String>,
61    pub phase: &'static str,
62    #[serde(flatten)]
63    pub usage: crate::server::UsageStats,
64    pub reserved_state_bytes: usize,
65}
66
67#[derive(Serialize)]
68pub struct Current {
69    pub request_id: String,
70    pub config_generation: u64,
71    pub phase: &'static str,
72    pub generated_tokens: u64,
73}
74
75#[derive(Default, Serialize)]
76pub struct CacheStats {
77    pub entries: usize,
78    pub bytes: usize,
79    pub evictions: u64,
80}
81
82#[derive(Default, Serialize)]
83pub struct MemoryStats {
84    #[serde(flatten)]
85    pub resources: crate::qwen4_exp::gpu::MemoryStats,
86    pub observed_at_uptime_seconds: f64,
87}
88
89impl State {
90    pub(super) fn summary(&self) -> Result<Value> {
91        self.activity_page(|activity| Ok(super::activity::summary(activity)))
92    }
93    pub(super) fn layers(&self, offset: usize, limit: usize) -> Result<Value> {
94        self.activity_page(|activity| super::activity::layers(activity, offset, limit))
95    }
96
97    pub(super) fn experts(&self, layer: usize, offset: usize, limit: usize) -> Result<Value> {
98        self.activity_page(|activity| super::activity::experts(activity, layer, offset, limit))
99    }
100
101    fn activity_page<T: Serialize>(
102        &self,
103        query: impl FnOnce(&crate::qwen4_exp::gpu::ExpertActivity) -> Result<T>,
104    ) -> Result<Value> {
105        let stats = self.stats.lock().unwrap();
106
107        ensure!(stats.ready, "model not ready: still loading");
108        stats.activity.validate_dimensions()?;
109
110        let snapshot = super::stats::Snapshot {
111            observation: super::stats::Observation {
112                observed_at_uptime_seconds: stats.memory.observed_at_uptime_seconds,
113                elapsed_seconds: stats.activity.elapsed_seconds,
114                gpu_timestamps_available: stats.activity.gpu_timestamps_available,
115                gpu_timing: stats.activity.gpu_timing.clone(),
116            },
117            data: query(&stats.activity)?,
118        };
119
120        drop(stats);
121
122        Ok(serde_json::to_value(snapshot)?)
123    }
124
125    pub fn new(source: Source, config: Config) -> Self {
126        Self {
127            source,
128            config: Mutex::new(Versioned {
129                generation: 1,
130                config,
131            }),
132            stats: Mutex::new(Stats::default()),
133            started: Instant::now(),
134        }
135    }
136
137    pub fn config(&self) -> Versioned {
138        self.config.lock().unwrap().clone()
139    }
140
141    pub fn show_config(&self) -> Value {
142        json!({"effective": self.config(), "source": self.source.path,
143            "reloadable_sections": ["defaults"], "restart_required_sections": ["server", "limits", "experts"]})
144    }
145
146    pub fn reload(&self) -> Result<Value> {
147        ensure!(
148            self.source.path.is_some(),
149            "server was started without --config"
150        );
151
152        let next = self.source.resolve()?;
153        let mut current = self.config.lock().unwrap();
154        let changes = current.config.restart_changes(&next);
155
156        ensure!(
157            changes.is_empty(),
158            "restart required for changed sections: {}",
159            changes.join(", ")
160        );
161
162        if current.config != next {
163            current.config = next;
164            current.generation += 1;
165        }
166
167        Ok(json!({"generation": current.generation, "config": current.config}))
168    }
169
170    pub fn update(&self, f: impl FnOnce(&mut Stats)) {
171        f(&mut self.stats.lock().unwrap());
172    }
173
174    pub fn status(&self) -> Value {
175        let config = self.config();
176
177        json!({"uptime_seconds": self.started.elapsed().as_secs_f64(),
178            "stats": *self.stats.lock().unwrap(),
179            "capabilities": {"active_sequences": config.config.limits.active_requests,
180                "sampling": true, "retained_sessions": config.config.limits.max_sessions > 0, "cancellation": true}})
181    }
182
183    pub fn begin(&self) {
184        self.update(|s| {
185            s.queued_requests -= 1;
186            s.active_requests += 1;
187        });
188    }
189
190    pub fn current(&self, request_id: &str, generation: u64, phase: &'static str, tokens: usize) {
191        self.update(|s| {
192            s.current = Some(Current {
193                request_id: request_id.to_owned(),
194                config_generation: generation,
195                phase,
196                generated_tokens: tokens as u64,
197            });
198        });
199    }
200
201    pub fn token(&self) {
202        self.update(|s| {
203            s.generated_tokens += 1;
204
205            if let Some(current) = &mut s.current {
206                current.generated_tokens += 1;
207            }
208        });
209    }
210
211    pub(crate) fn observe(
212        &self,
213        gpu: &crate::qwen4_exp::gpu::Gpu<'_>,
214        cache: &crate::prefix_cache::PrefixCache,
215        spare: &mut crate::qwen4_exp::gpu::ExpertActivity,
216    ) {
217        let (entries, bytes, evictions) = cache.stats();
218
219        gpu.copy_expert_activity(spare);
220
221        let memory = MemoryStats {
222            resources: gpu.memory_stats(),
223            observed_at_uptime_seconds: self.started.elapsed().as_secs_f64(),
224        };
225
226        self.publish_observation(
227            CacheStats {
228                entries,
229                bytes,
230                evictions,
231            },
232            memory,
233            spare,
234        );
235    }
236
237    /// Swap complete snapshots while holding the lock; retain the old allocation
238    /// as the worker's spare so the next observation can reuse it.
239    pub(crate) fn publish_observation(
240        &self,
241        cache: CacheStats,
242        memory: MemoryStats,
243        spare: &mut crate::qwen4_exp::gpu::ExpertActivity,
244    ) {
245        self.update(|s| {
246            s.cache = cache;
247            s.memory = memory;
248
249            std::mem::swap(&mut s.activity, spare);
250        });
251    }
252}