1use std::fs::{create_dir_all, File};
13use std::io::Write;
14use std::path::{Path, PathBuf};
15use std::time::{Duration, Instant};
16
17use crate::hardware_passport::measure_decode_proxy_tok_s;
18use crate::inference_modes::{set_inference_mode, InferenceMode};
19use crate::lab::audit_path::audit_hot_path;
20use crate::lab::device_roof::calibrate_device_roof;
21use crate::lab::experiment_log::{append_run_csv, ExperimentRun};
22use crate::lab::micro::run_q4k_soa_microbench;
23use crate::llm_bench::{
24 set_coop_gemv, set_ffn_fusion, set_kv_int8, set_resident_decode, set_resident_prefill,
25 set_resident_weights,
26};
27
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
30pub struct LabConfig {
31 pub label: String,
32 pub resident: bool,
33 pub coop: bool,
34 pub ffn_fusion: bool,
35 pub kv_int8: bool,
36 pub mode: InferenceMode,
37 pub backend: Option<&'static str>, }
39
40impl LabConfig {
41 pub fn toggles_json(&self) -> String {
42 format!(
43 "{{\"resident\":{},\"coop\":{},\"ffn_fusion\":{},\"kv_int8\":{},\"mode\":\"{}\",\"backend\":\"{}\"}}",
44 self.resident,
45 self.coop,
46 self.ffn_fusion,
47 self.kv_int8,
48 self.mode.as_str(),
49 self.backend.unwrap_or("auto")
50 )
51 }
52
53 pub fn apply(&self) {
54 set_resident_decode(self.resident);
55 set_resident_prefill(self.resident);
56 set_resident_weights(true);
57 set_coop_gemv(self.coop);
58 set_ffn_fusion(self.ffn_fusion);
59 set_kv_int8(self.kv_int8);
60 set_inference_mode(self.mode);
61 std::env::set_var("QUALIA_INFERENCE_MODE", self.mode.as_str());
62 match self.backend {
63 Some(b) => std::env::set_var("QUALIA_WGPU_BACKEND", b),
64 None => {
65 }
67 }
68 }
69}
70
71pub fn default_search_space() -> Vec<LabConfig> {
73 let mut v = Vec::new();
74 let modes = [
75 InferenceMode::Portable,
76 InferenceMode::FastVerify,
77 InferenceMode::CudaTc,
78 ];
79 let backends: [Option<&'static str>; 3] = [None, Some("vulkan"), Some("dx12")];
80 for &resident in &[true, false] {
81 for &coop in &[true, false] {
82 for &kv in &[true, false] {
83 for &mode in &modes {
84 if !resident && mode == InferenceMode::CudaTc {
86 continue;
87 }
88 for &backend in &backends {
89 if backend.is_some() && (!resident || mode == InferenceMode::CudaTc) {
91 continue;
92 }
93 let label = format!(
94 "r{}_c{}_k{}_{}_{}",
95 resident as u8,
96 coop as u8,
97 kv as u8,
98 mode.as_str(),
99 backend.unwrap_or("auto")
100 );
101 v.push(LabConfig {
102 label,
103 resident,
104 coop,
105 ffn_fusion: true, kv_int8: kv,
107 mode,
108 backend,
109 });
110 }
111 }
112 }
113 }
114 }
115 v
116}
117
118#[derive(Debug, Clone)]
119pub struct TrialResult {
120 pub config: LabConfig,
121 pub tok_s: Option<f64>,
122 pub wall_ms: f64,
123 pub generation: u32,
124}
125
126#[derive(Debug, Clone)]
127pub struct AutoImproveConfig {
128 pub model: PathBuf,
129 pub tokens: u32,
130 pub max_duration: Duration,
131 pub out_dir: PathBuf,
132 pub ollama_model: Option<String>,
133 pub ollama_url: String,
134 pub elite_resample: usize,
136 pub plateau_rel: f64,
138 pub plateau_gens: u32,
139 pub max_generations: u32,
140}
141
142impl Default for AutoImproveConfig {
143 fn default() -> Self {
144 Self {
145 model: PathBuf::from("C:\\LLM_Models\\P64\\smollm2-360m-instruct-q8_0.p64"),
146 tokens: 16,
147 max_duration: Duration::from_secs(2 * 3600),
148 out_dir: PathBuf::from("experiments/inference-lab/lockin"),
149 ollama_model: Some("qualia-smol-q8:latest".into()),
150 ollama_url: "http://127.0.0.1:11434".into(),
151 elite_resample: 3,
152 plateau_rel: 0.02,
153 plateau_gens: 2,
154 max_generations: 8,
155 }
156 }
157}
158
159#[derive(Debug, Clone)]
160pub struct LockInPackage {
161 pub best: Option<TrialResult>,
162 pub ollama_tok_s: Option<f64>,
163 pub a_gap: Option<f64>,
164 pub trials: usize,
165 pub generations: u32,
166 pub elapsed_secs: f64,
167 pub out_dir: PathBuf,
168 pub methodology: String,
169}
170
171pub fn try_ollama_decode_tok_s(url: &str, model: &str, tokens: u32) -> Option<f64> {
173 let body = format!(
177 r#"{{"model":"{model}","prompt":"Write a short paragraph about rivers.","stream":false,"options":{{"num_predict":{tokens},"temperature":0}}}}"#
178 );
179 let out = std::process::Command::new("curl")
180 .args([
181 "-sS",
182 "-X",
183 "POST",
184 &format!("{url}/api/generate"),
185 "-H",
186 "Content-Type: application/json",
187 "-d",
188 &body,
189 "--max-time",
190 "120",
191 ])
192 .output()
193 .ok()?;
194 if !out.status.success() {
195 return None;
196 }
197 let text = String::from_utf8_lossy(&out.stdout);
198 let eval_count = parse_json_u64(&text, "eval_count")?;
200 let eval_duration = parse_json_u64(&text, "eval_duration")?;
201 if eval_duration == 0 {
202 return None;
203 }
204 Some((eval_count as f64) / (eval_duration as f64 / 1e9))
205}
206
207fn parse_json_u64(s: &str, key: &str) -> Option<u64> {
208 let pat = format!("\"{key}\":");
209 let i = s.find(&pat)?;
210 let rest = s[i + pat.len()..].trim_start();
211 let num: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
212 num.parse().ok()
213}
214
215fn measure_config(model: &Path, tokens: u32, cfg: &LabConfig) -> TrialResult {
216 cfg.apply();
217 let t0 = Instant::now();
218 let tok_s = measure_decode_proxy_tok_s(model, tokens);
219 let wall_ms = t0.elapsed().as_secs_f64() * 1e3;
220 TrialResult {
221 config: cfg.clone(),
222 tok_s,
223 wall_ms,
224 generation: 0,
225 }
226}
227
228pub fn run_auto_improve(cfg: &AutoImproveConfig) -> Result<LockInPackage, String> {
230 if !cfg.model.is_file() {
231 return Err(format!("model not found: {}", cfg.model.display()));
232 }
233 create_dir_all(&cfg.out_dir).map_err(|e| e.to_string())?;
234 let csv_path = cfg.out_dir.join("runs.csv");
235 let log_path = cfg.out_dir.join("auto_improve.log");
236 let mut log = File::create(&log_path).map_err(|e| e.to_string())?;
237
238 macro_rules! logln {
239 ($($t:tt)*) => {{
240 let line = format!($($t)*);
241 let _ = writeln!(log, "{line}");
242 let _ = log.flush();
243 log::info!("lab_auto|{line}");
244 eprintln!("lab_auto|{line}");
245 }};
246 }
247
248 logln!(
249 "start model={} tokens={} budget_s={:.0}",
250 cfg.model.display(),
251 cfg.tokens,
252 cfg.max_duration.as_secs_f64()
253 );
254
255 let audit = audit_hot_path();
257 let _ = std::fs::write(cfg.out_dir.join("audit_path.txt"), audit.format_report());
258 let roof = calibrate_device_roof(512);
259 let _ = std::fs::write(cfg.out_dir.join("device_roof.txt"), roof.format_report());
260 let micro = run_q4k_soa_microbench(256, 32);
261 let _ = std::fs::write(cfg.out_dir.join("micro_q4k.txt"), micro.format_report());
262
263 let ollama_tok_s = cfg.ollama_model.as_ref().and_then(|m| {
264 logln!("ollama_probe model={m}");
265 try_ollama_decode_tok_s(&cfg.ollama_url, m, cfg.tokens.max(16))
266 });
267 if let Some(o) = ollama_tok_s {
268 logln!("ollama_decode_tok_s={o:.3}");
269 } else {
270 logln!("ollama_probe skipped or failed");
271 }
272
273 let space = default_search_space();
274 logln!("search_space_size={}", space.len());
275
276 let t_start = Instant::now();
277 let mut all_trials: Vec<TrialResult> = Vec::new();
278 let mut best: Option<TrialResult> = None;
279 let mut gens_without_improve = 0u32;
280 let mut generation = 0u32;
281
282 let model_id = cfg
283 .model
284 .file_name()
285 .and_then(|s| s.to_str())
286 .unwrap_or("model")
287 .to_string();
288 let passport_key =
289 crate::hardware_passport::read_passport(&crate::hardware_passport::default_cache_path())
290 .map(|p| p.key)
291 .unwrap_or_default();
292
293 let mut queue: Vec<LabConfig> = space;
295 while generation < cfg.max_generations && t_start.elapsed() < cfg.max_duration {
296 generation += 1;
297 logln!(
298 "generation={generation} queue={} elapsed_s={:.0}",
299 queue.len(),
300 t_start.elapsed().as_secs_f64()
301 );
302
303 let gen_best_before = best.as_ref().and_then(|b| b.tok_s).unwrap_or(0.0);
304
305 for cfg_point in queue.drain(..) {
306 if t_start.elapsed() >= cfg.max_duration {
307 logln!("budget_exhausted mid-generation");
308 break;
309 }
310 let mut trial = measure_config(&cfg.model, cfg.tokens, &cfg_point);
311 trial.generation = generation;
312 logln!(
313 "trial gen={generation} label={} tok_s={:?} wall_ms={:.0}",
314 trial.config.label,
315 trial.tok_s,
316 trial.wall_ms
317 );
318
319 let mut run = ExperimentRun {
321 run_id: ExperimentRun::new_id(),
322 utc: ExperimentRun::utc_now(),
323 git_sha: String::new(),
324 host_passport_key: passport_key.clone(),
325 adapter: roof.best_label.clone(),
326 backend: trial.config.backend.unwrap_or("auto").to_string(),
327 model_id: model_id.clone(),
328 model_hash: String::new(),
329 layout: if model_id.contains("soa") {
330 "soa".into()
331 } else if model_id.contains("f16") {
332 "f16".into()
333 } else {
334 "verbatim".into()
335 },
336 mode: trial.config.mode.as_str().into(),
337 profile: "lab-auto".into(),
338 toggles_json: trial.config.toggles_json(),
339 qualia_decode_tok_s: trial.tok_s,
340 ollama_decode_tok_s: ollama_tok_s,
341 a_gap: None,
342 prefill_tok_s: None,
343 phase_ns_json: "{}".into(),
344 n_ulp_max: if micro.cuda_ok {
345 Some(micro.max_ulp)
346 } else {
347 None
348 },
349 c_score: None,
350 notes: format!("gen={generation}"),
351 };
352 run.compute_a_gap();
353 let _ = append_run_csv(&csv_path, &run);
354
355 let improve = match (&best, trial.tok_s) {
356 (None, Some(t)) if t > 0.0 => true,
357 (Some(b), Some(t)) => t > b.tok_s.unwrap_or(0.0) * (1.0 + 1e-6),
358 _ => false,
359 };
360 if improve {
361 logln!(
362 "NEW_BEST tok_s={:?} label={}",
363 trial.tok_s,
364 trial.config.label
365 );
366 best = Some(trial.clone());
367 }
368 all_trials.push(trial);
369 }
370
371 let mut ranked: Vec<_> = all_trials
373 .iter()
374 .filter(|t| t.tok_s.unwrap_or(0.0) > 0.0)
375 .cloned()
376 .collect();
377 ranked.sort_by(|a, b| {
378 b.tok_s
379 .unwrap_or(0.0)
380 .partial_cmp(&a.tok_s.unwrap_or(0.0))
381 .unwrap_or(std::cmp::Ordering::Equal)
382 });
383
384 let gen_best = best.as_ref().and_then(|b| b.tok_s).unwrap_or(0.0);
385 if gen_best > gen_best_before * (1.0 + cfg.plateau_rel) {
386 gens_without_improve = 0;
387 } else {
388 gens_without_improve += 1;
389 }
390 logln!(
391 "generation_end best={:.4} plateau_gens={gens_without_improve}",
392 gen_best
393 );
394 if gens_without_improve >= cfg.plateau_gens && generation >= 2 {
395 logln!("plateau_stop");
396 break;
397 }
398
399 queue.clear();
401 for elite in ranked.iter().take(cfg.elite_resample) {
402 queue.push(elite.config.clone());
403 }
404 if let Some(b) = best.as_ref() {
405 for neighbor in neighbors(&b.config) {
406 if !queue.iter().any(|c| c == &neighbor) {
407 queue.push(neighbor);
408 }
409 }
410 }
411 if queue.is_empty() {
412 break;
413 }
414 }
415
416 set_resident_decode(true);
418 set_coop_gemv(true);
419 set_ffn_fusion(true);
420 set_kv_int8(true);
421 set_inference_mode(InferenceMode::Portable);
422
423 let a_gap = match (best.as_ref().and_then(|b| b.tok_s), ollama_tok_s) {
424 (Some(q), Some(o)) if q > 0.0 => Some(o / q),
425 _ => None,
426 };
427
428 let methodology =
429 build_methodology(&best, ollama_tok_s, a_gap, &audit, &all_trials, generation);
430 let _ = std::fs::write(cfg.out_dir.join("METHODOLOGY.md"), &methodology);
431
432 if let Some(ref b) = best {
433 let _ = std::fs::write(
434 cfg.out_dir.join("BEST_CONFIG.json"),
435 format!(
436 "{{\n \"label\": \"{}\",\n \"tok_s\": {},\n \"toggles\": {},\n \"mode\": \"{}\",\n \"backend\": \"{}\",\n \"resident\": {},\n \"coop\": {},\n \"kv_int8\": {}\n}}\n",
437 b.config.label,
438 b.tok_s.unwrap_or(0.0),
439 b.config.toggles_json(),
440 b.config.mode.as_str(),
441 b.config.backend.unwrap_or("auto"),
442 b.config.resident,
443 b.config.coop,
444 b.config.kv_int8
445 ),
446 );
447 let backend_line = b
448 .config
449 .backend
450 .map(|be| format!("$env:QUALIA_WGPU_BACKEND='{be}'\n"))
451 .unwrap_or_default();
452 let apply = format!(
453 "# Auto-generated by lab auto-improve — apply winning config\n\
454 $env:QUALIA_INFERENCE_MODE='{}'\n\
455 {backend_line}\
456 $env:QUALIA_LLM_RESIDENT_DECODE='{}'\n\
457 $env:QUALIA_LLM_COOP_GEMV='{}'\n\
458 $env:QUALIA_LLM_KV_INT8='{}'\n\
459 # tok_s measured ≈ {:?}\n",
460 b.config.mode.as_str(),
461 if b.config.resident { "1" } else { "0" },
462 if b.config.coop { "1" } else { "0" },
463 if b.config.kv_int8 { "1" } else { "0" },
464 b.tok_s
465 );
466 let _ = std::fs::write(cfg.out_dir.join("apply-best.ps1"), apply);
467 }
468
469 let pkg = LockInPackage {
470 best,
471 ollama_tok_s,
472 a_gap,
473 trials: all_trials.len(),
474 generations: generation,
475 elapsed_secs: t_start.elapsed().as_secs_f64(),
476 out_dir: cfg.out_dir.clone(),
477 methodology,
478 };
479 let summary = format_lockin_summary(&pkg);
480 let _ = std::fs::write(cfg.out_dir.join("LOCKIN_SUMMARY.txt"), &summary);
481 logln!("done\n{summary}");
482 Ok(pkg)
483}
484
485fn neighbors(c: &LabConfig) -> Vec<LabConfig> {
486 let mut out = Vec::new();
487 let mut flip = |mut x: LabConfig, label_sfx: &str| {
488 x.label = format!("{}_{label_sfx}", c.label);
489 out.push(x);
490 };
491 let mut a = c.clone();
492 a.coop = !a.coop;
493 flip(a, "flip_coop");
494 let mut b = c.clone();
495 b.kv_int8 = !b.kv_int8;
496 flip(b, "flip_kv");
497 let mut d = c.clone();
498 d.resident = !d.resident;
499 flip(d, "flip_res");
500 if c.mode == InferenceMode::Portable {
501 let mut e = c.clone();
502 e.mode = InferenceMode::FastVerify;
503 flip(e, "to_fast_verify");
504 }
505 out
506}
507
508fn build_methodology(
509 best: &Option<TrialResult>,
510 ollama: Option<f64>,
511 a_gap: Option<f64>,
512 audit: &crate::lab::audit_path::HotPathAudit,
513 trials: &[TrialResult],
514 gens: u32,
515) -> String {
516 let mut s = String::from("# Lab auto-improve methodology lock-in\n\n");
517 s.push_str(
518 "Generated by `qualia-cli llm lab auto` — **evidence-based**, not LLM kernel rewrite.\n\n",
519 );
520 s.push_str("## Best config\n\n");
521 if let Some(b) = best {
522 s.push_str(&format!(
523 "- label: `{}`\n- tok_s: {:?}\n- toggles: `{}`\n\n",
524 b.config.label,
525 b.tok_s,
526 b.config.toggles_json()
527 ));
528 } else {
529 s.push_str("- **none** (all trials failed)\n\n");
530 }
531 s.push_str("## Ollama yardstick\n\n");
532 s.push_str(&format!(
533 "- ollama_tok_s: {:?}\n- a_gap: {:?}\n\n",
534 ollama, a_gap
535 ));
536 s.push_str("## Audit notes (unfinished integration)\n\n");
537 for n in &audit.notes {
538 s.push_str(&format!("- {n}\n"));
539 }
540 s.push_str(&format!(
541 "\n## Search stats\n\n- trials: {}\n- generations: {gens}\n- ffn_fusion_in_resident: {}\n\n",
542 trials.len(),
543 audit.ffn_fusion_in_resident_decode
544 ));
545 s.push_str("## Next engineering (not search)\n\n");
546 s.push_str("1. **T-A1** (done wiring) — re-optimise fused_ffn body (coop/shared act) if A-gap still open.\n");
547 s.push_str("2. **T-A2** CUDA hidden-on-device full layer stack.\n");
548 s.push_str("3. Re-run `lab auto` after each; only lock-in when A-gap phase targets improve.\n");
549 s.push_str("\n## Apply\n\n```powershell\n. .\\experiments\\inference-lab\\lockin\\apply-best.ps1\n```\n");
550 s
551}
552
553pub fn format_lockin_summary(pkg: &LockInPackage) -> String {
554 format!(
555 "LOCK-IN SUMMARY\n out_dir: {}\n trials: {}\n generations: {}\n elapsed_s: {:.1}\n best_tok_s: {:?}\n best_label: {}\n ollama_tok_s:{:?}\n a_gap: {:?}\n",
556 pkg.out_dir.display(),
557 pkg.trials,
558 pkg.generations,
559 pkg.elapsed_secs,
560 pkg.best.as_ref().and_then(|b| b.tok_s),
561 pkg.best
562 .as_ref()
563 .map(|b| b.config.label.as_str())
564 .unwrap_or("none"),
565 pkg.ollama_tok_s,
566 pkg.a_gap
567 )
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573
574 #[test]
575 fn search_space_nonempty_and_dedup_labels() {
576 let space = default_search_space();
577 assert!(
578 space.len() >= 8,
579 "expected a real search grid, got {}",
580 space.len()
581 );
582 let mut labels: Vec<&str> = space.iter().map(|c| c.label.as_str()).collect();
583 labels.sort_unstable();
584 labels.dedup();
585 assert_eq!(
586 labels.len(),
587 space.len(),
588 "duplicate labels in search space"
589 );
590 }
591
592 #[test]
593 fn neighbors_flip_bits() {
594 let c = LabConfig {
595 label: "base".into(),
596 resident: true,
597 coop: true,
598 ffn_fusion: true,
599 kv_int8: true,
600 mode: InferenceMode::Portable,
601 backend: None,
602 };
603 let n = neighbors(&c);
604 assert!(n.len() >= 3);
605 assert!(n.iter().any(|x| x.coop != c.coop));
606 assert!(n.iter().any(|x| x.kv_int8 != c.kv_int8));
607 assert!(n.iter().any(|x| x.mode == InferenceMode::FastVerify));
608 }
609
610 #[test]
611 fn toggles_json_is_compact() {
612 let c = LabConfig {
613 label: "t".into(),
614 resident: true,
615 coop: false,
616 ffn_fusion: true,
617 kv_int8: true,
618 mode: InferenceMode::FastVerify,
619 backend: Some("vulkan"),
620 };
621 let j = c.toggles_json();
622 assert!(j.contains("\"resident\":true"));
623 assert!(j.contains("\"coop\":false"));
624 assert!(j.contains("fast-verify"));
625 assert!(j.contains("vulkan"));
626 }
627
628 #[test]
629 fn lockin_summary_handles_empty_best() {
630 let pkg = LockInPackage {
631 best: None,
632 ollama_tok_s: None,
633 a_gap: None,
634 trials: 0,
635 generations: 0,
636 elapsed_secs: 0.0,
637 out_dir: PathBuf::from("experiments/inference-lab/lockin"),
638 methodology: String::new(),
639 };
640 let s = format_lockin_summary(&pkg);
641 assert!(s.contains("none"));
642 assert!(s.contains("LOCK-IN SUMMARY"));
643 }
644}