1use std::collections::hash_map::DefaultHasher;
8use std::fs;
9use std::hash::{Hash, Hasher};
10use std::path::PathBuf;
11use std::sync::Mutex;
12
13use crate::host::RuntimeProfile;
14use crate::system_profile::SystemProfile;
15
16static TUNE_CACHE: Mutex<Option<AutoTuner>> = Mutex::new(None);
19
20#[derive(Clone, Debug)]
26pub struct AutoTuner {
27 pub fingerprint: u64,
29 pub tuned_at: String,
31 pub health_config: super::health::HealthConfig,
33 pub load_balance_config: super::load_balance::LoadBalanceConfig,
35 pub planning_tuning: super::planning::PlanningTuning,
37 pub tcp_config: TcpAutoConfig,
39 pub worker_pool: WorkerPoolConfig,
41}
42
43#[derive(Clone, Debug)]
45pub struct TcpAutoConfig {
46 pub max_inflight_batches: usize,
47 pub reconnect_attempts: usize,
48 pub reconnect_backoff_ms: u64,
49}
50
51#[derive(Clone, Debug)]
53pub struct WorkerPoolConfig {
54 pub compute_workers: usize,
55 pub io_workers: usize,
56 pub total_workers: usize,
57}
58
59impl AutoTuner {
60 pub fn from_system_profile(profile: &SystemProfile) -> Self {
63 let fp = compute_fingerprint(profile);
64 {
65 let cache = TUNE_CACHE.lock().unwrap();
66 if let Some(cached) = cache.as_ref() {
67 if cached.fingerprint == fp {
68 return cached.clone();
69 }
70 }
71 }
72 let tuner = tune(profile);
73 let mut cache = TUNE_CACHE.lock().unwrap();
74 *cache = Some(tuner.clone());
75 tuner
76 }
77
78 pub fn load_cache() -> Option<Self> {
81 let current = SystemProfile::detect_fast();
88 let current_fp = compute_fingerprint(¤t);
89
90 {
92 let cache = TUNE_CACHE.lock().unwrap();
93 if let Some(cached) = cache.as_ref() {
94 if cached.fingerprint == current_fp {
95 return Some(cached.clone());
96 }
97 }
98 }
99
100 let path = cache_path()?;
102 let data = fs::read_to_string(&path).ok()?;
103 let cached: AutoTuner = serde_json::from_str(&data).ok()?;
104
105 if cached.fingerprint != current_fp {
106 return None;
107 }
108
109 let mut cache = TUNE_CACHE.lock().unwrap();
111 *cache = Some(cached.clone());
112 Some(cached)
113 }
114
115 pub fn save_cache(&self) {
117 let path = match cache_path() {
118 Some(p) => p,
119 None => return,
120 };
121 if let Some(parent) = path.parent() {
122 let _ = fs::create_dir_all(parent);
123 }
124 if let Ok(data) = serde_json::to_string_pretty(self) {
125 let _ = fs::write(&path, data);
126 }
127 }
128
129 pub fn benchmark_tcp(&mut self) {
133 }
136}
137
138fn tune(profile: &SystemProfile) -> AutoTuner {
143 let fingerprint = compute_fingerprint(profile);
144 let tuned_at = now_iso8601();
145 let rp: RuntimeProfile = profile.into();
146
147 AutoTuner {
148 fingerprint,
149 tuned_at,
150 health_config: super::health::HealthConfig::autotuned(&rp),
151 load_balance_config: super::load_balance::LoadBalanceConfig::autotuned(&rp),
152 planning_tuning: super::planning::PlanningTuning::from_runtime_profile(&rp, 40),
153 tcp_config: tune_tcp(profile, &rp),
154 worker_pool: tune_worker_pool(profile, &rp),
155 }
156}
157
158fn compute_fingerprint(profile: &SystemProfile) -> u64 {
159 let mut hasher = DefaultHasher::new();
160 profile.cpu.logical_cores.hash(&mut hasher);
161 profile.memory.total_gb.to_bits().hash(&mut hasher);
162 for gpu in &profile.gpus {
163 gpu.name.hash(&mut hasher);
164 gpu.vram_gb.to_bits().hash(&mut hasher);
165 }
166 profile.npus.len().hash(&mut hasher);
167 profile.acceleration_mode.hash(&mut hasher);
168 hasher.finish()
169}
170
171fn cache_path() -> Option<PathBuf> {
172 let base = dirs_next::cache_dir().or_else(|| {
173 std::env::var("HOME")
174 .or_else(|_| std::env::var("USERPROFILE"))
175 .ok()
176 .map(PathBuf::from)
177 })?;
178 Some(base.join("ghostlink").join("autotune.json"))
179}
180
181fn now_iso8601() -> String {
182 let dur = std::time::SystemTime::now()
183 .duration_since(std::time::UNIX_EPOCH)
184 .unwrap_or_default();
185 let secs = dur.as_secs();
186 let days = secs / 86400;
188 let remaining = secs % 86400;
189 let hours = remaining / 3600;
190 let minutes = (remaining % 3600) / 60;
191 let seconds = remaining % 60;
192
193 let (year, month, day) = civil_from_days(days as i64);
195
196 format!(
197 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
198 year, month, day, hours, minutes, seconds
199 )
200}
201
202fn civil_from_days(days: i64) -> (i64, u32, u32) {
204 let z = days + 719468;
206 let era = if z >= 0 { z } else { z - 146096 } / 146097;
207 let doe = z - era * 146097;
208 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
209 let y = yoe + era * 400;
210 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
211 let mp = (5 * doy + 2) / 153;
212 let d = doy - (153 * mp + 2) / 5 + 1;
213 let m = if mp < 10 { mp + 3 } else { mp - 9 };
214 let y = if m <= 2 { y + 1 } else { y };
215 (y, m as u32, d as u32)
216}
217
218fn tune_tcp(profile: &SystemProfile, _rp: &RuntimeProfile) -> TcpAutoConfig {
219 let (max_inflight, reconnect_attempts, reconnect_backoff_ms) = match profile.acceleration_mode {
220 crate::host::AccelerationMode::Gpu => (256, 5, 5),
221 crate::host::AccelerationMode::Avx512 => (128, 3, 10),
222 _ => (64, 3, 10),
223 };
224
225 let gpu_count = profile
227 .gpus
228 .iter()
229 .filter(|g| g.vram_gb > 0.0)
230 .count()
231 .max(1);
232 TcpAutoConfig {
233 max_inflight_batches: max_inflight * gpu_count,
234 reconnect_attempts,
235 reconnect_backoff_ms,
236 }
237}
238
239fn tune_worker_pool(profile: &SystemProfile, rp: &RuntimeProfile) -> WorkerPoolConfig {
240 let compute = rp.recommended_workers.max(1);
241 let io = match profile.acceleration_mode {
242 crate::host::AccelerationMode::Gpu => compute / 2,
243 _ => compute / 4,
244 }
245 .max(1);
246 WorkerPoolConfig {
247 compute_workers: compute,
248 io_workers: io,
249 total_workers: compute + io,
250 }
251}
252
253impl serde::Serialize for AutoTuner {
258 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
259 use serde::ser::SerializeStruct;
260 let mut st = s.serialize_struct("AutoTuner", 7)?;
261 st.serialize_field("fingerprint", &self.fingerprint)?;
262 st.serialize_field("tuned_at", &self.tuned_at)?;
263 st.serialize_field("healthy_latency_us", &self.health_config.healthy_latency_us)?;
264 st.serialize_field(
265 "degraded_latency_us",
266 &self.health_config.degraded_latency_us,
267 )?;
268 st.serialize_field("lock_timeout_us", &self.load_balance_config.lock_timeout_us)?;
269 st.serialize_field(
270 "max_inflight_batches",
271 &self.tcp_config.max_inflight_batches,
272 )?;
273 st.serialize_field("compute_workers", &self.worker_pool.compute_workers)?;
274 st.end()
275 }
276}
277
278impl<'de> serde::Deserialize<'de> for AutoTuner {
279 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
280 use serde::de::{MapAccess, Visitor};
281 struct TunerVisitor;
282 impl<'de> Visitor<'de> for TunerVisitor {
283 type Value = AutoTuner;
284 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
285 f.write_str("struct AutoTuner")
286 }
287 fn visit_map<V: MapAccess<'de>>(self, mut map: V) -> Result<AutoTuner, V::Error> {
288 let mut fingerprint = 0u64;
289 let mut tuned_at = String::new();
290 let mut healthy_latency_us = 10.0f32;
291 let mut degraded_latency_us = 20.0f32;
292 let mut lock_timeout_us = 1000u64;
293 let mut max_inflight_batches = 64usize;
294 let mut compute_workers = 4usize;
295
296 while let Some(key) = map.next_key::<String>()? {
297 match key.as_str() {
298 "fingerprint" => fingerprint = map.next_value()?,
299 "tuned_at" => tuned_at = map.next_value()?,
300 "healthy_latency_us" => healthy_latency_us = map.next_value()?,
301 "degraded_latency_us" => degraded_latency_us = map.next_value()?,
302 "lock_timeout_us" => lock_timeout_us = map.next_value()?,
303 "max_inflight_batches" => max_inflight_batches = map.next_value()?,
304 "compute_workers" => compute_workers = map.next_value()?,
305 _ => {
306 let _: serde_json::Value = map.next_value()?;
307 }
308 }
309 }
310
311 Ok(AutoTuner {
312 fingerprint,
313 tuned_at,
314 health_config: super::health::HealthConfig {
315 healthy_latency_us,
316 degraded_latency_us,
317 ..Default::default()
318 },
319 load_balance_config: super::load_balance::LoadBalanceConfig {
320 lock_timeout_us,
321 ..Default::default()
322 },
323 planning_tuning: super::planning::PlanningTuning {
324 max_layers_per_assignment: compute_workers.max(1),
325 },
326 tcp_config: TcpAutoConfig {
327 max_inflight_batches,
328 reconnect_attempts: 3,
329 reconnect_backoff_ms: 10,
330 },
331 worker_pool: WorkerPoolConfig {
332 compute_workers,
333 io_workers: compute_workers / 2,
334 total_workers: compute_workers + compute_workers / 2,
335 },
336 })
337 }
338 }
339 d.deserialize_map(TunerVisitor)
340 }
341}
342
343#[cfg(test)]
348mod tests {
349 use super::*;
350 use crate::system_profile::SystemProfile;
351
352 #[test]
353 fn autotuner_derives_from_system_profile() {
354 let sp = SystemProfile::detect_fast();
355 let tuner = AutoTuner::from_system_profile(&sp);
356 assert!(tuner.worker_pool.compute_workers >= 1);
357 assert!(tuner.tcp_config.max_inflight_batches >= 1);
358 }
359
360 #[test]
361 fn autotuner_fingerprint_is_stable() {
362 let sp = SystemProfile::detect_fast();
363 let fp1 = compute_fingerprint(&sp);
364 let fp2 = compute_fingerprint(&sp);
365 assert_eq!(fp1, fp2);
366 }
367
368 #[test]
369 fn autotuner_round_trips_through_json() {
370 let sp = SystemProfile::detect_fast();
371 let tuner = AutoTuner::from_system_profile(&sp);
372 let json = serde_json::to_string_pretty(&tuner).expect("serialize");
373 let restored: AutoTuner = serde_json::from_str(&json).expect("deserialize");
374 assert_eq!(tuner.fingerprint, restored.fingerprint);
375 assert_eq!(
376 tuner.tcp_config.max_inflight_batches,
377 restored.tcp_config.max_inflight_batches
378 );
379 }
380
381 #[test]
382 fn autotuner_detects_hardware_change() {
383 let sp = SystemProfile::detect_fast();
384 let mut tuner = AutoTuner::from_system_profile(&sp);
385
386 tuner.fingerprint = 0;
388
389 let json = serde_json::to_string(&tuner).unwrap();
390 std::env::set_var("HOME", std::env::temp_dir());
391 let restored: AutoTuner = serde_json::from_str(&json).unwrap();
393 assert_eq!(restored.fingerprint, 0);
394 }
395
396 #[test]
397 fn load_cache_rejects_stale_in_memory_fingerprint() {
398 {
404 let mut cache = TUNE_CACHE.lock().unwrap();
405 let mut stale = tune(&SystemProfile::detect_fast());
406 stale.fingerprint = 0; *cache = Some(stale);
408 }
409 if let Some(tuner) = AutoTuner::load_cache() {
410 assert_ne!(
411 tuner.fingerprint, 0,
412 "load_cache() returned the stale in-memory entry instead of rejecting it"
413 );
414 }
415 }
417
418 #[test]
419 fn tcp_config_scales_with_multi_gpu() {
420 use crate::host::{AccelerationMode, GpuBackend};
421 use crate::system_profile::{CpuInfo, GpuInfo, MemoryInfo};
422
423 let single = SystemProfile {
424 gpus: vec![GpuInfo {
425 name: "RTX 4090".into(),
426 vram_gb: 24.0,
427 backend: GpuBackend::Cuda,
428 compute_capability: "8.9".into(),
429 driver_version: "555".into(),
430 pci_address: None,
431 }],
432 cpu: CpuInfo {
433 logical_cores: 16,
434 ..Default::default()
435 },
436 memory: MemoryInfo {
437 total_gb: 64.0,
438 ..Default::default()
439 },
440 acceleration_mode: AccelerationMode::Gpu,
441 ..Default::default()
442 };
443
444 let dual = SystemProfile {
445 gpus: vec![
446 GpuInfo {
447 name: "RTX 4090".into(),
448 vram_gb: 24.0,
449 backend: GpuBackend::Cuda,
450 compute_capability: "8.9".into(),
451 driver_version: "555".into(),
452 pci_address: None,
453 },
454 GpuInfo {
455 name: "RTX 3090".into(),
456 vram_gb: 24.0,
457 backend: GpuBackend::Cuda,
458 compute_capability: "8.6".into(),
459 driver_version: "555".into(),
460 pci_address: None,
461 },
462 ],
463 cpu: CpuInfo {
464 logical_cores: 16,
465 ..Default::default()
466 },
467 memory: MemoryInfo {
468 total_gb: 64.0,
469 ..Default::default()
470 },
471 acceleration_mode: AccelerationMode::Gpu,
472 ..Default::default()
473 };
474
475 let s = AutoTuner::from_system_profile(&single);
476 let d = AutoTuner::from_system_profile(&dual);
477
478 assert!(d.tcp_config.max_inflight_batches > s.tcp_config.max_inflight_batches);
479 }
480}