1use cortiq_core::CmfModel;
4use cortiq_core::mask::{MaskCatalog, MaskDiff, TaskMask};
5#[cfg(not(target_os = "macos"))]
6use cortiq_core::types::SimdType;
7use cortiq_core::types::{ExecutionMode, LayerStats, PerformanceMetrics};
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 ffn_neurons = |i: usize| -> usize {
77 model
78 .tensor(&format!("model.layers.{i}.mlp.gate_proj.weight"))
79 .and_then(|t| t.shape.first().copied())
80 .unwrap_or(arch.intermediate_size)
81 };
82 let layer_stats: Vec<LayerStats> = (0..n_layers)
83 .map(|i| LayerStats {
84 layer_idx: i,
85 active_neurons: ffn_neurons(i),
86 total_neurons: ffn_neurons(i),
87 active_heads: arch.num_attention_heads,
88 total_heads: arch.num_attention_heads,
89 is_alive: true,
90 placement: "gpu".to_string(),
91 avg_forward_ms: 0.0,
92 })
93 .collect();
94
95 let state = RuntimeState {
96 active_task: model.masks.default_task.clone(),
97 active_mask: model.masks.fallback().cloned(),
98 execution_mode,
99 metrics: PerformanceMetrics::default(),
100 layer_stats,
101 };
102
103 Self {
104 model,
105 state: Arc::new(RwLock::new(state)),
106 started_at: Instant::now(),
107 }
108 }
109
110 fn detect_execution_mode() -> ExecutionMode {
118 #[cfg(target_os = "macos")]
119 {
120 ExecutionMode::AppleUnified {
121 metal_layers: vec![],
122 }
123 }
124 #[cfg(not(target_os = "macos"))]
125 {
126 #[cfg(target_arch = "aarch64")]
127 let simd_type = SimdType::Neon;
128 #[cfg(target_arch = "x86_64")]
129 let simd_type = SimdType::Avx2;
130 #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
131 let simd_type = SimdType::None;
132 ExecutionMode::CpuOnly {
133 simd_type,
134 threads: crate::pool::Pool::effective_threads(),
135 }
136 }
137 }
138
139 pub async fn switch_task(&self, task_name: &str) -> Result<SwitchResult, anyhow::Error> {
141 let new_mask = self
142 .model
143 .masks
144 .get(task_name)
145 .ok_or_else(|| anyhow::anyhow!("Task mask '{}' not found", task_name))?
146 .clone();
147
148 let mut state = self.state.write().await;
149 let previous_task = state.active_task.clone();
150
151 if new_mask.quality.is_none() {
153 tracing::warn!(
154 "task '{task_name}': mask has no measured quality — \
155 treat outputs as unvalidated"
156 );
157 }
158
159 let switch_start = Instant::now();
160
161 let diff = if let Some(ref current) = state.active_mask {
163 current.diff(&new_mask)
164 } else {
165 MaskDiff {
166 changed_layers: (0..self.model.arch().num_layers).collect(),
167 neurons_added: 0,
168 neurons_removed: 0,
169 ffn_delta: vec![],
170 }
171 };
172
173 for ls in &mut state.layer_stats {
175 ls.active_neurons = new_mask.ffn_active_count(ls.layer_idx);
176 ls.active_heads = new_mask.active_head_count(ls.layer_idx);
177 ls.is_alive = new_mask.layer_alive(ls.layer_idx);
178 }
179
180 let switch_latency = switch_start.elapsed().as_secs_f64() * 1000.0;
181
182 state.active_task = task_name.to_string();
183 let sparsity = new_mask.sparsity;
184 state.active_mask = Some(new_mask);
185 state.metrics.last_switch_latency_ms = switch_latency;
186 state.metrics.total_switches += 1;
187
188 let active_params = self.estimate_active_params(&state);
189
190 Ok(SwitchResult {
191 previous_task,
192 new_task: task_name.to_string(),
193 switch_mode: "warm".to_string(),
194 switch_latency_ms: switch_latency,
195 new_sparsity: sparsity,
196 new_active_params: active_params,
197 diff,
198 })
199 }
200
201 pub async fn status(&self) -> RuntimeStatus {
203 let state = self.state.read().await;
204 let mut metrics = state.metrics.clone();
205 metrics.uptime_seconds = self.started_at.elapsed().as_secs();
206
207 let active_sparsity = state
208 .active_mask
209 .as_ref()
210 .map(|m| m.sparsity)
211 .unwrap_or(0.0);
212
213 let active_layers = state.layer_stats.iter().filter(|l| l.is_alive).count();
214
215 RuntimeStatus {
216 model_name: self.model.arch().arch_name.clone(),
217 model_path: self.model.path.display().to_string(),
218 format: format!("CMF v{}", self.model.header.version),
219 quantization: format!("{:?}", self.model.header.quant_type),
220 execution_mode: state.execution_mode.clone(),
221 active_task: state.active_task.clone(),
222 active_sparsity,
223 active_params: self.estimate_active_params(&state),
224 active_layers,
225 total_layers: self.model.arch().num_layers * self.model.arch().num_loops,
226 performance: metrics,
227 layer_stats: state.layer_stats.clone(),
228 }
229 }
230
231 pub fn masks(&self) -> &MaskCatalog {
233 &self.model.masks
234 }
235
236 pub fn model(&self) -> &CmfModel {
238 &self.model
239 }
240
241 pub async fn active_mask(&self) -> Option<TaskMask> {
243 self.state.read().await.active_mask.clone()
244 }
245
246 pub async fn active_selection(&self) -> (String, Option<TaskMask>) {
249 let state = self.state.read().await;
250 (state.active_task.clone(), state.active_mask.clone())
251 }
252
253 pub async fn record_generation(&self, gen_tokens: usize, elapsed_ms: f64, ttft_ms: f64) {
255 let mut state = self.state.write().await;
256 let m = &mut state.metrics;
257 let prev_total = m.tokens_generated as f64;
258 m.tokens_generated += gen_tokens as u64;
259 if elapsed_ms > 0.0 {
260 let tps = gen_tokens as f64 / (elapsed_ms / 1000.0);
261 let total = prev_total + gen_tokens as f64;
263 m.avg_tokens_per_sec = if total > 0.0 {
264 (m.avg_tokens_per_sec * prev_total + tps * gen_tokens as f64) / total
265 } else {
266 tps
267 };
268 }
269 if m.avg_time_to_first_token_ms == 0.0 {
270 m.avg_time_to_first_token_ms = ttft_ms;
271 } else {
272 m.avg_time_to_first_token_ms = m.avg_time_to_first_token_ms * 0.9 + ttft_ms * 0.1;
273 }
274 }
275
276 fn estimate_active_params(&self, state: &RuntimeState) -> String {
278 let total_params = self.model.total_param_count() as f64 / 1e9;
279 let sparsity = state
280 .active_mask
281 .as_ref()
282 .map(|m| m.sparsity as f64)
283 .unwrap_or(0.0);
284 let active = total_params * (1.0 - sparsity);
285 format!("{:.2}B / {:.2}B", active, total_params)
286 }
287}
288
289#[cfg(not(target_os = "macos"))]
290fn num_cpus() -> usize {
291 std::thread::available_parallelism()
292 .map(|n| n.get())
293 .unwrap_or(4)
294}