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: if std::env::var("CMF_TASK").as_deref() == Ok("off") {
103 String::new()
104 } else {
105 model.masks.default_task.clone()
106 },
107 active_mask: if std::env::var("CMF_TASK").as_deref() == Ok("off") {
112 None
113 } else {
114 model.masks.fallback().cloned()
115 },
116 execution_mode,
117 metrics: PerformanceMetrics::default(),
118 layer_stats,
119 };
120
121 Self {
122 model,
123 state: Arc::new(RwLock::new(state)),
124 started_at: Instant::now(),
125 }
126 }
127
128 fn detect_execution_mode() -> ExecutionMode {
136 #[cfg(target_os = "macos")]
137 {
138 ExecutionMode::AppleUnified {
139 metal_layers: vec![],
140 }
141 }
142 #[cfg(not(target_os = "macos"))]
143 {
144 #[cfg(target_arch = "aarch64")]
145 let simd_type = SimdType::Neon;
146 #[cfg(target_arch = "x86_64")]
147 let simd_type = SimdType::Avx2;
148 #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
149 let simd_type = SimdType::None;
150 ExecutionMode::CpuOnly {
151 simd_type,
152 threads: crate::pool::Pool::effective_threads(),
153 }
154 }
155 }
156
157 pub async fn switch_task(&self, task_name: &str) -> Result<SwitchResult, anyhow::Error> {
159 let new_mask = self
160 .model
161 .masks
162 .get(task_name)
163 .ok_or_else(|| anyhow::anyhow!("Task mask '{}' not found", task_name))?
164 .clone();
165
166 let mut state = self.state.write().await;
167 let previous_task = state.active_task.clone();
168
169 if new_mask.quality.is_none() {
171 tracing::warn!(
172 "task '{task_name}': mask has no measured quality — \
173 treat outputs as unvalidated"
174 );
175 }
176
177 let switch_start = Instant::now();
178
179 let diff = if let Some(ref current) = state.active_mask {
181 current.diff(&new_mask)
182 } else {
183 MaskDiff {
184 changed_layers: (0..self.model.arch().num_layers).collect(),
185 neurons_added: 0,
186 neurons_removed: 0,
187 ffn_delta: vec![],
188 }
189 };
190
191 for ls in &mut state.layer_stats {
193 ls.active_neurons = new_mask.ffn_active_count(ls.layer_idx);
194 ls.active_heads = new_mask.active_head_count(ls.layer_idx);
195 ls.is_alive = new_mask.layer_alive(ls.layer_idx);
196 }
197
198 let switch_latency = switch_start.elapsed().as_secs_f64() * 1000.0;
199
200 state.active_task = task_name.to_string();
201 let sparsity = new_mask.sparsity;
202 state.active_mask = Some(new_mask);
203 state.metrics.last_switch_latency_ms = switch_latency;
204 state.metrics.total_switches += 1;
205
206 let active_params = self.estimate_active_params(&state);
207
208 Ok(SwitchResult {
209 previous_task,
210 new_task: task_name.to_string(),
211 switch_mode: "warm".to_string(),
212 switch_latency_ms: switch_latency,
213 new_sparsity: sparsity,
214 new_active_params: active_params,
215 diff,
216 })
217 }
218
219 pub async fn status(&self) -> RuntimeStatus {
221 let state = self.state.read().await;
222 let mut metrics = state.metrics.clone();
223 metrics.uptime_seconds = self.started_at.elapsed().as_secs();
224
225 let active_sparsity = state
226 .active_mask
227 .as_ref()
228 .map(|m| m.sparsity)
229 .unwrap_or(0.0);
230
231 let active_layers = state.layer_stats.iter().filter(|l| l.is_alive).count();
232
233 RuntimeStatus {
234 model_name: self.model.arch().arch_name.clone(),
235 model_path: self.model.path.display().to_string(),
236 format: format!("CMF v{}", self.model.header.version),
237 quantization: format!("{:?}", self.model.header.quant_type),
238 execution_mode: state.execution_mode.clone(),
239 active_task: state.active_task.clone(),
240 active_sparsity,
241 active_params: self.estimate_active_params(&state),
242 active_layers,
243 total_layers: self.model.arch().num_layers * self.model.arch().num_loops,
244 performance: metrics,
245 layer_stats: state.layer_stats.clone(),
246 }
247 }
248
249 pub fn masks(&self) -> &MaskCatalog {
251 &self.model.masks
252 }
253
254 pub fn model(&self) -> &CmfModel {
256 &self.model
257 }
258
259 pub async fn active_mask(&self) -> Option<TaskMask> {
261 self.state.read().await.active_mask.clone()
262 }
263
264 pub async fn active_selection(&self) -> (String, Option<TaskMask>) {
267 let state = self.state.read().await;
268 (state.active_task.clone(), state.active_mask.clone())
269 }
270
271 pub async fn record_generation(&self, gen_tokens: usize, elapsed_ms: f64, ttft_ms: f64) {
273 let mut state = self.state.write().await;
274 let m = &mut state.metrics;
275 let prev_total = m.tokens_generated as f64;
276 m.tokens_generated += gen_tokens as u64;
277 if elapsed_ms > 0.0 {
278 let tps = gen_tokens as f64 / (elapsed_ms / 1000.0);
279 let total = prev_total + gen_tokens as f64;
281 m.avg_tokens_per_sec = if total > 0.0 {
282 (m.avg_tokens_per_sec * prev_total + tps * gen_tokens as f64) / total
283 } else {
284 tps
285 };
286 }
287 if m.avg_time_to_first_token_ms == 0.0 {
288 m.avg_time_to_first_token_ms = ttft_ms;
289 } else {
290 m.avg_time_to_first_token_ms = m.avg_time_to_first_token_ms * 0.9 + ttft_ms * 0.1;
291 }
292 }
293
294 fn estimate_active_params(&self, state: &RuntimeState) -> String {
296 let total_params = self.model.total_param_count() as f64 / 1e9;
297 let sparsity = state
298 .active_mask
299 .as_ref()
300 .map(|m| m.sparsity as f64)
301 .unwrap_or(0.0);
302 let active = total_params * (1.0 - sparsity);
303 format!("{:.2}B / {:.2}B", active, total_params)
304 }
305}
306
307#[cfg(not(target_os = "macos"))]
308fn num_cpus() -> usize {
309 std::thread::available_parallelism()
310 .map(|n| n.get())
311 .unwrap_or(4)
312}