1use cortiq_core::mask::{MaskCatalog, MaskDiff, TaskMask};
4#[cfg(not(target_os = "macos"))]
5use cortiq_core::types::SimdType;
6use cortiq_core::types::{ExecutionMode, LayerStats, PerformanceMetrics};
7use cortiq_core::CmfModel;
8use serde::{Deserialize, Serialize};
9use std::sync::Arc;
10use std::time::Instant;
11use tokio::sync::RwLock;
12
13pub struct CortiqRuntime {
15 model: Arc<CmfModel>,
17 state: Arc<RwLock<RuntimeState>>,
19 started_at: Instant,
21}
22
23#[derive(Debug)]
25struct RuntimeState {
26 active_task: String,
27 active_mask: Option<TaskMask>,
28 execution_mode: ExecutionMode,
29 metrics: PerformanceMetrics,
30 layer_stats: Vec<LayerStats>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct SwitchResult {
36 pub previous_task: String,
37 pub new_task: String,
38 pub switch_mode: String,
39 pub switch_latency_ms: f64,
40 pub new_sparsity: f32,
41 pub new_active_params: String,
42 pub diff: MaskDiff,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct RuntimeStatus {
48 pub model_name: String,
49 pub model_path: String,
50 pub format: String,
51 pub quantization: String,
52 pub execution_mode: ExecutionMode,
53 pub active_task: String,
54 pub active_sparsity: f32,
55 pub active_params: String,
56 pub active_layers: usize,
57 pub total_layers: usize,
58 pub performance: PerformanceMetrics,
59 pub layer_stats: Vec<LayerStats>,
60}
61
62impl CortiqRuntime {
63 pub fn new(model: Arc<CmfModel>) -> Self {
65 let arch = model.arch();
66 let n_layers = arch.num_layers;
67
68 let execution_mode = Self::detect_execution_mode();
70
71 let layer_stats: Vec<LayerStats> = (0..n_layers)
73 .map(|i| LayerStats {
74 layer_idx: i,
75 active_neurons: arch.intermediate_size,
76 total_neurons: arch.intermediate_size,
77 active_heads: arch.num_attention_heads,
78 total_heads: arch.num_attention_heads,
79 is_alive: true,
80 placement: "gpu".to_string(),
81 avg_forward_ms: 0.0,
82 })
83 .collect();
84
85 let state = RuntimeState {
86 active_task: model.masks.default_task.clone(),
87 active_mask: model.masks.fallback().cloned(),
88 execution_mode,
89 metrics: PerformanceMetrics::default(),
90 layer_stats,
91 };
92
93 Self {
94 model,
95 state: Arc::new(RwLock::new(state)),
96 started_at: Instant::now(),
97 }
98 }
99
100 fn detect_execution_mode() -> ExecutionMode {
102 #[cfg(target_os = "macos")]
104 {
105 ExecutionMode::AppleUnified {
106 metal_layers: vec![],
107 }
108 }
109 #[cfg(not(target_os = "macos"))]
110 {
111 ExecutionMode::CpuOnly {
112 simd_type: SimdType::Avx2,
113 threads: num_cpus(),
114 }
115 }
116 }
117
118 pub async fn switch_task(&self, task_name: &str) -> Result<SwitchResult, anyhow::Error> {
120 let new_mask = self
121 .model
122 .masks
123 .get(task_name)
124 .ok_or_else(|| anyhow::anyhow!("Task mask '{}' not found", task_name))?
125 .clone();
126
127 let mut state = self.state.write().await;
128 let previous_task = state.active_task.clone();
129
130 if new_mask.quality.is_none() {
132 tracing::warn!(
133 "task '{task_name}': mask has no measured quality — \
134 treat outputs as unvalidated"
135 );
136 }
137
138 let switch_start = Instant::now();
139
140 let diff = if let Some(ref current) = state.active_mask {
142 current.diff(&new_mask)
143 } else {
144 MaskDiff {
145 changed_layers: (0..self.model.arch().num_layers).collect(),
146 neurons_added: 0,
147 neurons_removed: 0,
148 ffn_delta: vec![],
149 }
150 };
151
152 for ls in &mut state.layer_stats {
154 ls.active_neurons = new_mask.ffn_active_count(ls.layer_idx);
155 ls.active_heads = new_mask.active_head_count(ls.layer_idx);
156 ls.is_alive = new_mask.layer_alive(ls.layer_idx);
157 }
158
159 let switch_latency = switch_start.elapsed().as_secs_f64() * 1000.0;
160
161 state.active_task = task_name.to_string();
162 let sparsity = new_mask.sparsity;
163 state.active_mask = Some(new_mask);
164 state.metrics.last_switch_latency_ms = switch_latency;
165 state.metrics.total_switches += 1;
166
167 let active_params = self.estimate_active_params(&state);
168
169 Ok(SwitchResult {
170 previous_task,
171 new_task: task_name.to_string(),
172 switch_mode: "warm".to_string(),
173 switch_latency_ms: switch_latency,
174 new_sparsity: sparsity,
175 new_active_params: active_params,
176 diff,
177 })
178 }
179
180 pub async fn status(&self) -> RuntimeStatus {
182 let state = self.state.read().await;
183 let mut metrics = state.metrics.clone();
184 metrics.uptime_seconds = self.started_at.elapsed().as_secs();
185
186 let active_sparsity = state
187 .active_mask
188 .as_ref()
189 .map(|m| m.sparsity)
190 .unwrap_or(0.0);
191
192 let active_layers = state.layer_stats.iter().filter(|l| l.is_alive).count();
193
194 RuntimeStatus {
195 model_name: self.model.arch().arch_name.clone(),
196 model_path: self.model.path.display().to_string(),
197 format: format!("CMF v{}", self.model.header.version),
198 quantization: format!("{:?}", self.model.header.quant_type),
199 execution_mode: state.execution_mode.clone(),
200 active_task: state.active_task.clone(),
201 active_sparsity,
202 active_params: self.estimate_active_params(&state),
203 active_layers,
204 total_layers: self.model.arch().num_layers,
205 performance: metrics,
206 layer_stats: state.layer_stats.clone(),
207 }
208 }
209
210 pub fn masks(&self) -> &MaskCatalog {
212 &self.model.masks
213 }
214
215 pub fn model(&self) -> &CmfModel {
217 &self.model
218 }
219
220 pub async fn active_mask(&self) -> Option<TaskMask> {
222 self.state.read().await.active_mask.clone()
223 }
224
225 pub async fn record_generation(&self, gen_tokens: usize, elapsed_ms: f64, ttft_ms: f64) {
227 let mut state = self.state.write().await;
228 let m = &mut state.metrics;
229 let prev_total = m.tokens_generated as f64;
230 m.tokens_generated += gen_tokens as u64;
231 if elapsed_ms > 0.0 {
232 let tps = gen_tokens as f64 / (elapsed_ms / 1000.0);
233 let total = prev_total + gen_tokens as f64;
235 m.avg_tokens_per_sec = if total > 0.0 {
236 (m.avg_tokens_per_sec * prev_total + tps * gen_tokens as f64) / total
237 } else {
238 tps
239 };
240 }
241 if m.avg_time_to_first_token_ms == 0.0 {
242 m.avg_time_to_first_token_ms = ttft_ms;
243 } else {
244 m.avg_time_to_first_token_ms = m.avg_time_to_first_token_ms * 0.9 + ttft_ms * 0.1;
245 }
246 }
247
248 fn estimate_active_params(&self, state: &RuntimeState) -> String {
250 let total_params = self.model.total_param_count() as f64 / 1e9;
251 let sparsity = state
252 .active_mask
253 .as_ref()
254 .map(|m| m.sparsity as f64)
255 .unwrap_or(0.0);
256 let active = total_params * (1.0 - sparsity);
257 format!("{:.2}B / {:.2}B", active, total_params)
258 }
259}
260
261#[cfg(not(target_os = "macos"))]
262fn num_cpus() -> usize {
263 std::thread::available_parallelism()
264 .map(|n| n.get())
265 .unwrap_or(4)
266}