Skip to main content

qualia_core_db/inference/ambient_orchestration/
manager.rs

1//! Orchestration core: the ambient orchestration manager and the
2//! sub-threshold orchestrator that drives policy adaptation.
3
4use super::*;
5use std::collections::HashMap;
6use std::thread;
7use std::time::Duration;
8
9/// Ambient Orchestration Manager
10pub struct AmbientOrchestrationManager {
11    pub(super) devices: HashMap<String, AmbientDevice>,
12    orchestrator: SubThresholdOrchestrator,
13    power_manager: PowerManager,
14    performance_monitor: AmbientPerformanceMonitor,
15    task_scheduler: TaskScheduler,
16}
17
18/// Sub-threshold orchestrator
19pub struct SubThresholdOrchestrator {
20    orchestration_policy: OrchestrationPolicy,
21    workload_analyzer: WorkloadAnalyzer,
22    resource_allocator: ResourceAllocator,
23    adaptation_engine: AdaptationEngine,
24}
25
26impl AmbientOrchestrationManager {
27    /// Create new ambient orchestration manager
28    pub fn new() -> Self {
29        Self {
30            devices: HashMap::new(),
31            orchestrator: SubThresholdOrchestrator::new(),
32            power_manager: PowerManager::new(),
33            performance_monitor: AmbientPerformanceMonitor::new(),
34            task_scheduler: TaskScheduler::new(),
35        }
36    }
37
38    /// Register ambient device
39    pub fn register_device(&mut self, device: AmbientDevice) -> Result<(), AmbientError> {
40        // Validate device
41        self.validate_device(&device)?;
42
43        // Store device
44        self.devices.insert(device.device_id.clone(), device);
45
46        Ok(())
47    }
48
49    /// Discover ambient devices using sysinfo to enumerate real local hardware.
50    pub fn discover_devices(&mut self) -> Result<Vec<String>, AmbientError> {
51        let mut handles = [AmbientDeviceHandle {
52            device_id_hash: 0,
53            device_type: DeviceType::Embedded,
54            compute_units: 0,
55            memory_size: 0,
56            state: DeviceState::Offline,
57        }; 9];
58        let written = self.discover_devices_into(&mut handles)?;
59        let mut discovered = Vec::with_capacity(written);
60        for handle in handles.into_iter().take(written) {
61            if handle.device_id_hash == crate::q_hash("local_host") {
62                discovered.push("local_host".to_string());
63            } else {
64                discovered.push(format!("cpu_core_{}", discovered.len().saturating_sub(1)));
65            }
66        }
67        Ok(discovered)
68    }
69
70    /// Zero-heap device discovery snapshots for hot-path schedulers.
71    pub fn discover_devices_into(
72        &mut self,
73        out: &mut [AmbientDeviceHandle],
74    ) -> Result<usize, AmbientError> {
75        use sysinfo::System;
76
77        // sysinfo 0.39: explicitly refresh after construction before reading
78        // CPU/memory (matches the ingest.rs pattern). H0 hardware discovery
79        // depends on these values being populated.
80        let mut sys = System::new_all();
81        sys.refresh_all();
82        let mut discovered = 0usize;
83        self.devices.clear();
84
85        let cpus = sys.cpus();
86        let cpu_count = cpus.len().max(1);
87        let cpu_brand = cpus
88            .first()
89            .map(|c| c.brand().to_string())
90            .unwrap_or_else(|| "Unknown CPU".to_string());
91        let base_freq_mhz = cpus.first().map(|c| c.frequency()).unwrap_or(1000);
92        let total_mem = sys.total_memory(); // bytes
93
94        // Register the local machine as one aggregate compute device
95        let host_id = "local_host".to_string();
96        let host = AmbientDevice {
97            device_id: host_id.clone(),
98            device_type: DeviceType::Embedded,
99            capabilities: DeviceCapabilities {
100                neural_engines: vec![NeuralEngine::ONNXRuntime],
101                compute_units: cpu_count as u32,
102                memory_size: total_mem,
103                battery_capacity: 0,
104                thermal_limit: 95.0,
105                supported_frameworks: vec![Framework::ONNX, Framework::Custom(cpu_brand.clone())],
106            },
107            current_state: DeviceState::Active,
108            performance_profile: PerformanceProfile {
109                peak_performance: base_freq_mhz as f64 * cpu_count as f64 / 1000.0,
110                sustainable_performance: base_freq_mhz as f64 * cpu_count as f64 * 0.8 / 1000.0,
111                thermal_performance: base_freq_mhz as f64 * cpu_count as f64 * 0.6 / 1000.0,
112                battery_performance: 1.0,
113                efficiency_factor: 0.90,
114            },
115            power_profile: PowerProfile {
116                baseline_power: 5.0 * cpu_count as f64,
117                active_power: 15.0 * cpu_count as f64,
118                peak_power: 35.0 * cpu_count as f64,
119                idle_power: 1.0,
120                sleep_power: 0.5,
121            },
122        };
123        if self.register_device(host).is_ok() {
124            if discovered >= out.len() {
125                return Err(AmbientError::InsufficientResources(
126                    "device output buffer full".to_string(),
127                ));
128            }
129            out[discovered] = self.snapshot_device_handle("local_host").unwrap();
130            discovered += 1;
131        }
132
133        // Register up to 8 individual logical CPU cores for fine-grained scheduling
134        for i in 0..cpu_count.min(8) {
135            let core_freq = cpus.get(i).map(|c| c.frequency()).unwrap_or(base_freq_mhz);
136            let core_id = format!("cpu_core_{}", i);
137            let core = AmbientDevice {
138                device_id: core_id.clone(),
139                device_type: DeviceType::Embedded,
140                capabilities: DeviceCapabilities {
141                    neural_engines: vec![NeuralEngine::ONNXRuntime],
142                    compute_units: 1,
143                    memory_size: total_mem / cpu_count as u64,
144                    battery_capacity: 0,
145                    thermal_limit: 95.0,
146                    supported_frameworks: vec![Framework::ONNX],
147                },
148                current_state: DeviceState::Active,
149                performance_profile: PerformanceProfile {
150                    peak_performance: core_freq as f64 / 1000.0,
151                    sustainable_performance: core_freq as f64 * 0.8 / 1000.0,
152                    thermal_performance: core_freq as f64 * 0.6 / 1000.0,
153                    battery_performance: 1.0,
154                    efficiency_factor: 0.85,
155                },
156                power_profile: PowerProfile {
157                    baseline_power: 5.0,
158                    active_power: 15.0,
159                    peak_power: 35.0,
160                    idle_power: 1.0,
161                    sleep_power: 0.5,
162                },
163            };
164            if self.register_device(core).is_ok() {
165                if discovered >= out.len() {
166                    return Err(AmbientError::InsufficientResources(
167                        "device output buffer full".to_string(),
168                    ));
169                }
170                out[discovered] = self.snapshot_device_handle(&core_id).unwrap();
171                discovered += 1;
172            }
173        }
174
175        Ok(discovered)
176    }
177
178    /// Submit task for execution
179    pub fn submit_task(&mut self, task: Task) -> Result<String, AmbientError> {
180        // Validate task
181        self.validate_task(&task)?;
182
183        // Add to task queue
184        self.task_scheduler.submit_task(task.clone())?;
185
186        Ok(task.task_id.clone())
187    }
188
189    /// Execute neural inference task
190    pub fn execute_neural_inference(
191        &mut self,
192        device_id: &str,
193        model_data: &[u8],
194        input_data: &[u8],
195    ) -> Result<Vec<u8>, AmbientError> {
196        let mut out = vec![0u8; 1024];
197        let written =
198            self.execute_neural_inference_into(device_id, model_data, input_data, &mut out)?;
199        out.truncate(written);
200        Ok(out)
201    }
202
203    /// Zero-heap neural inference API using caller-owned output storage.
204    pub fn execute_neural_inference_into(
205        &mut self,
206        device_id: &str,
207        model_data: &[u8],
208        input_data: &[u8],
209        out: &mut [u8],
210    ) -> Result<usize, AmbientError> {
211        // Clone device to release the borrow on self.devices before calling the helper
212        let device = self
213            .devices
214            .get(device_id)
215            .ok_or_else(|| AmbientError::DeviceNotFound(device_id.to_string()))?
216            .clone();
217        self.execute_inference_on_device(&device, model_data, input_data, out)
218    }
219
220    /// Execute sub-threshold computation
221    pub fn execute_sub_threshold_computation(
222        &mut self,
223        device_id: &str,
224        computation: SubThresholdComputation,
225    ) -> Result<ComputationResult, AmbientError> {
226        // Clone device to release the borrow on self.devices before calling the helper
227        let device = self
228            .devices
229            .get(device_id)
230            .ok_or_else(|| AmbientError::DeviceNotFound(device_id.to_string()))?
231            .clone();
232        self.execute_computation_on_device(&device, &computation)
233    }
234
235    /// Get device status
236    pub fn get_device_status(&self, device_id: &str) -> Option<DeviceStatus> {
237        self.devices.get(device_id).map(|device| DeviceStatus {
238            device_id: device.device_id.clone(),
239            device_type: device.device_type.clone(),
240            state: device.current_state.clone(),
241            battery_level: self.power_manager.get_battery_level(device_id),
242            thermal_state: self.power_manager.get_thermal_state(device_id),
243            performance: device.performance_profile.clone(),
244            power_consumption: self.power_manager.get_power_consumption(device_id),
245        })
246    }
247
248    /// Get performance statistics
249    pub fn get_performance_stats(&self) -> AmbientGlobalMetrics {
250        self.performance_monitor.get_global_stats()
251    }
252
253    /// Set the ambient orchestration state used for power estimation.
254    ///
255    /// Callers (e.g. the `TaskOrchestrator` state machine in `orchestrator.rs`)
256    /// should push `ModelLifecycle` transitions here so the power manager can
257    /// track real power draw.
258    pub fn set_orchestration_state(&mut self, state: AmbientOrchestrationState) {
259        self.power_manager.set_orchestration_state(state);
260    }
261
262    /// Set the number of models currently active on the managed device.
263    pub fn set_active_model_count(&mut self, count: usize) {
264        self.power_manager.set_active_model_count(count);
265    }
266
267    /// Get the aggregated power/thermal/battery snapshot.
268    pub fn get_power_metrics(&self) -> PowerMetrics {
269        self.power_manager.get_power_metrics()
270    }
271
272    /// Estimate battery life remaining (hours) for the given charge and
273    /// battery capacity.
274    pub fn estimate_battery_life_remaining(
275        &self,
276        current_battery_pct: f64,
277        battery_capacity_wh: f64,
278    ) -> f64 {
279        self.power_manager
280            .estimate_battery_life_remaining(current_battery_pct, battery_capacity_wh)
281    }
282
283    /// Whether inference should be throttled due to thermal or battery pressure.
284    pub fn should_throttle_inference(&self) -> bool {
285        self.power_manager.should_throttle_inference()
286    }
287
288    /// List all devices
289    pub fn list_devices(&self) -> Vec<String> {
290        self.devices.keys().cloned().collect()
291    }
292
293    pub fn list_devices_into(
294        &self,
295        out: &mut [AmbientDeviceHandle],
296    ) -> Result<usize, AmbientError> {
297        if out.len() < self.devices.len() {
298            return Err(AmbientError::InsufficientResources(
299                "device output buffer full".to_string(),
300            ));
301        }
302
303        let mut written = 0usize;
304        for device_id in self.devices.keys() {
305            out[written] = self.snapshot_device_handle(device_id).unwrap();
306            written += 1;
307        }
308        Ok(written)
309    }
310
311    /// Get pending tasks
312    pub fn get_pending_tasks(&self) -> Vec<Task> {
313        self.task_scheduler.get_pending_tasks()
314    }
315
316    pub fn get_pending_tasks_into(&self, out: &mut [TaskHandle]) -> Result<usize, AmbientError> {
317        self.task_scheduler.get_pending_tasks_into(out)
318    }
319
320    /// Optimize orchestration policy
321    pub fn optimize_orchestration(&mut self) -> Result<(), AmbientError> {
322        // Feed execution history into workload analyzer
323        let history = self.task_scheduler.recent_history(10);
324        for record in history {
325            let sample = WorkloadSample {
326                timestamp: record.end_time,
327                cpu_usage: record.resource_usage.compute_units_used as f64 / 100.0,
328                memory_usage: record.resource_usage.memory_used as f64 / (1024.0 * 1024.0),
329                neural_engine_usage: record.resource_usage.neural_engines_used as f64,
330                power_consumption: record.resource_usage.power_consumed,
331                thermal_state: record.resource_usage.thermal_impact,
332                battery_level: self.power_manager.get_battery_level(&record.device_id),
333            };
334            self.orchestrator.workload_analyzer.record_sample(sample);
335        }
336
337        // Analyze current workload
338        let workload_analysis = self.orchestrator.workload_analyzer.analyze_workload();
339
340        // Adapt orchestration policy (mutable: records adaptation history)
341        let new_policy = self
342            .orchestrator
343            .adaptation_engine
344            .adapt_policy(workload_analysis);
345
346        // Adjust based on active power policy
347        let adjusted_policy = match self.power_manager.power_policy() {
348            PowerPolicy::PowerSaving | PowerPolicy::UltraPowerSaving => {
349                OrchestrationPolicy::BatteryAware
350            }
351            _ => new_policy,
352        };
353
354        // Update orchestration policy
355        self.orchestrator.orchestration_policy = adjusted_policy;
356
357        Ok(())
358    }
359
360    // Internal methods
361
362    /// Validate device
363    fn validate_device(&self, device: &AmbientDevice) -> Result<(), AmbientError> {
364        if device.device_id.is_empty() {
365            return Err(AmbientError::InvalidDevice(
366                "Device ID cannot be empty".to_string(),
367            ));
368        }
369
370        if device.capabilities.neural_engines.is_empty() {
371            return Err(AmbientError::InvalidDevice(
372                "Device must have at least one neural engine".to_string(),
373            ));
374        }
375
376        Ok(())
377    }
378
379    /// Validate task
380    fn validate_task(&self, task: &Task) -> Result<(), AmbientError> {
381        if task.task_id.is_empty() {
382            return Err(AmbientError::InvalidTask(
383                "Task ID cannot be empty".to_string(),
384            ));
385        }
386
387        if task.resource_requirements.compute_units == 0 {
388            return Err(AmbientError::InvalidTask(
389                "Task must require at least one compute unit".to_string(),
390            ));
391        }
392
393        Ok(())
394    }
395
396    /// Execute inference on device
397    fn execute_inference_on_device(
398        &self,
399        device: &AmbientDevice,
400        model_data: &[u8],
401        input_data: &[u8],
402        out: &mut [u8],
403    ) -> Result<usize, AmbientError> {
404        // In real implementation, would use NNAPI/CoreML for inference
405        // For now, simulate inference
406        let _ = (device, model_data, input_data);
407        thread::sleep(Duration::from_millis(100)); // Simulate 100ms inference
408
409        if out.len() < 1024 {
410            return Err(AmbientError::InsufficientResources(
411                "inference output buffer too small".to_string(),
412            ));
413        }
414        out[..1024].fill(0);
415        Ok(1024)
416    }
417
418    /// Execute computation on device
419    fn execute_computation_on_device(
420        &self,
421        _device: &AmbientDevice,
422        _computation: &SubThresholdComputation,
423    ) -> Result<ComputationResult, AmbientError> {
424        // In real implementation, would execute sub-threshold computation
425        // For now, simulate computation
426        thread::sleep(Duration::from_millis(50)); // Simulate 50ms computation
427
428        Ok(ComputationResult {
429            result_data: vec![0u8; 512],
430            execution_time: Duration::from_millis(50),
431            power_consumed: 0.1,
432            thermal_impact: 0.5,
433        })
434    }
435
436    fn snapshot_device_handle(&self, device_id: &str) -> Option<AmbientDeviceHandle> {
437        self.devices
438            .get(device_id)
439            .map(|device| AmbientDeviceHandle {
440                device_id_hash: crate::q_hash(&device.device_id),
441                device_type: device.device_type.clone(),
442                compute_units: device.capabilities.compute_units,
443                memory_size: device.capabilities.memory_size,
444                state: device.current_state.clone(),
445            })
446    }
447}
448
449impl SubThresholdOrchestrator {
450    /// Create new sub-threshold orchestrator
451    pub fn new() -> Self {
452        Self {
453            orchestration_policy: OrchestrationPolicy::Adaptive,
454            workload_analyzer: WorkloadAnalyzer::new(),
455            resource_allocator: ResourceAllocator::new(),
456            adaptation_engine: AdaptationEngine::new(),
457        }
458    }
459
460    /// Optimize computation for sub-threshold operation.
461    ///
462    /// Uses the resource allocator to check available compute units and
463    /// scales the computation's resource requirements accordingly. The
464    /// orchestration policy influences the reduction factor:
465    /// - `PowerEfficiency`: 50% compute, 40% power, 50% thermal
466    /// - `ThermalAware`: 60% compute, 50% power, 60% thermal
467    /// - `PerformanceFirst`: 90% compute, 80% power, 80% thermal
468    /// - `Adaptive`/other: 70% compute, 50% power, 60% thermal
469    pub fn optimize_for_sub_threshold(
470        &mut self,
471        computation: SubThresholdComputation,
472    ) -> SubThresholdComputation {
473        let mut optimized = computation;
474
475        let (compute_factor, power_factor, thermal_factor) = match self.orchestration_policy {
476            OrchestrationPolicy::PowerEfficiency => (0.50, 0.40, 0.50),
477            OrchestrationPolicy::ThermalAware => (0.60, 0.50, 0.60),
478            OrchestrationPolicy::BatteryAware => (0.40, 0.30, 0.50),
479            OrchestrationPolicy::PerformanceFirst => (0.90, 0.80, 0.80),
480            OrchestrationPolicy::Adaptive => (0.70, 0.50, 0.60),
481        };
482
483        // Check available resources before scaling.
484        let available = self.resource_allocator.available_compute_units();
485        let requested = optimized.resource_requirements.compute_units;
486        let scaled = (requested as f64 * compute_factor) as u32;
487        // Don't request more than what's available.
488        optimized.resource_requirements.compute_units = scaled.min(available);
489        optimized.resource_requirements.power_budget *= power_factor;
490        optimized.resource_requirements.thermal_budget *= thermal_factor;
491
492        optimized
493    }
494
495    /// Get a mutable reference to the workload analyzer for recording samples.
496    pub fn workload_analyzer_mut(&mut self) -> &mut WorkloadAnalyzer {
497        &mut self.workload_analyzer
498    }
499
500    /// Get a mutable reference to the resource allocator.
501    pub fn resource_allocator_mut(&mut self) -> &mut ResourceAllocator {
502        &mut self.resource_allocator
503    }
504}