Skip to main content

silicera_lab/
experiments.rs

1//! Experiment domains for Silicera lab demos.
2//!
3//! These experiments are conceptually runnable demos. Reported timings always
4//! come from the measurement engine on the current host — never fabricated.
5
6use serde::{Deserialize, Serialize};
7use silicera::hardware::HardwareInfo;
8use silicera::measure::{MeasurementConfig, MeasurementEngine, MeasurementSummary};
9use silicera::topology::TopologyGraph;
10use silicera::Result;
11
12use crate::arms::{run_integer_arms, run_memory_arms, ArmsHarnessConfig};
13use crate::bench::{CacheTarget, ConcurrencyBench, IntegerBench, MemoryBench};
14
15/// Experiment identifier.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17pub enum ExperimentId {
18    /// Multi-machine strategy split (this host = Machine A; B may be placeholder).
19    SiliconSplit,
20    /// Portable vs `-march=native`-style vs Silicera-selected.
21    PortableVsNativeVsSilicera,
22    /// Fresh machine with no prior profile (cold start).
23    ColdMachine,
24    /// Profile from a different fingerprint (wrong machine).
25    WrongMachine,
26}
27
28impl ExperimentId {
29    /// All built-in experiments.
30    pub fn all() -> &'static [ExperimentId] {
31        &[
32            ExperimentId::SiliconSplit,
33            ExperimentId::PortableVsNativeVsSilicera,
34            ExperimentId::ColdMachine,
35            ExperimentId::WrongMachine,
36        ]
37    }
38
39    /// Stable slug.
40    pub fn slug(self) -> &'static str {
41        match self {
42            ExperimentId::SiliconSplit => "silicon-split",
43            ExperimentId::PortableVsNativeVsSilicera => "portable-vs-native-vs-silicera",
44            ExperimentId::ColdMachine => "cold-machine",
45            ExperimentId::WrongMachine => "wrong-machine",
46        }
47    }
48
49    /// Title.
50    pub fn title(self) -> &'static str {
51        match self {
52            ExperimentId::SiliconSplit => "Silicon Split (multi-machine protocol)",
53            ExperimentId::PortableVsNativeVsSilicera => "Portable vs Native vs Silicera",
54            ExperimentId::ColdMachine => "Cold Machine",
55            ExperimentId::WrongMachine => "Wrong Machine",
56        }
57    }
58}
59
60/// One measured arm of an experiment.
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct ExperimentArm {
63    /// Arm name.
64    pub name: String,
65    /// Summary when measured.
66    pub summary: Option<MeasurementSummary>,
67    /// Notes (methodology / caveats).
68    pub notes: String,
69}
70
71/// Experiment report — structure for real measurements.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct ExperimentReport {
74    /// Experiment id.
75    pub id: ExperimentId,
76    /// Title.
77    pub title: String,
78    /// Host brand.
79    pub host_brand: String,
80    /// Fingerprint if supported.
81    pub fingerprint: Option<String>,
82    /// Arms.
83    pub arms: Vec<ExperimentArm>,
84    /// Narrative conclusion (no invented %).
85    pub conclusion: String,
86}
87
88/// Run an experiment on the given hardware/topology.
89pub fn run_experiment(
90    id: ExperimentId,
91    info: &HardwareInfo,
92    cfg: MeasurementConfig,
93) -> Result<ExperimentReport> {
94    match id {
95        ExperimentId::SiliconSplit => silicon_split(info, cfg),
96        ExperimentId::PortableVsNativeVsSilicera => portable_native_silicera(info, cfg),
97        ExperimentId::ColdMachine => cold_machine(info, cfg),
98        ExperimentId::WrongMachine => wrong_machine(info, cfg),
99    }
100}
101
102fn silicon_split(info: &HardwareInfo, cfg: MeasurementConfig) -> Result<ExperimentReport> {
103    // Local cache-crossing arms remain useful context; the research question is
104    // multi-machine strategy divergence — answered via CLI silicon-split / compare.
105    let eng = MeasurementEngine::new(cfg);
106    let topo = &info.topology;
107    let l1 = MemoryBench::for_target(topo, CacheTarget::L1);
108    let dram = MemoryBench::for_target(topo, CacheTarget::Dram);
109    let s_l1 = eng.measure(|| {
110        let _ = l1.run();
111    })?;
112    let s_dram = eng.measure(|| {
113        let _ = dram.run();
114    })?;
115    Ok(ExperimentReport {
116        id: ExperimentId::SiliconSplit,
117        title: ExperimentId::SiliconSplit.title().into(),
118        host_brand: info.brand.clone(),
119        fingerprint: info.fingerprint.as_ref().map(|f| f.value.clone()),
120        arms: vec![
121            ExperimentArm {
122                name: "Machine A L1 touch".into(),
123                summary: Some(s_l1),
124                notes: CacheTarget::L1.describe(topo),
125            },
126            ExperimentArm {
127                name: "Machine A DRAM touch".into(),
128                summary: Some(s_dram),
129                notes: CacheTarget::Dram.describe(topo),
130            },
131            ExperimentArm {
132                name: "Machine B".into(),
133                summary: None,
134                notes: "PLACEHOLDER — not measured on this run. Train on a second Zen box with \
135                        `silicera silicon-split train --role B`. Never invent B numbers."
136                    .into(),
137            },
138        ],
139        conclusion: format!(
140            "Question: can two machines benefit from different measured strategies? \
141             Answer on this host alone: UNKNOWN (Machine B absent). \
142             Local L1 vs DRAM arms show cache-level sensitivity on {} ({} domains). \
143             Complete the protocol: train A → export → placeholder for B → compare when B exists. \
144             See docs/research/silicon-split.md.",
145            info.brand,
146            topo.domain_count()
147        ),
148    })
149}
150
151fn portable_native_silicera(
152    info: &HardwareInfo,
153    cfg: MeasurementConfig,
154) -> Result<ExperimentReport> {
155    let arms_cfg = ArmsHarnessConfig {
156        warmup: cfg.warmup,
157        iterations: cfg.iterations,
158        min_improvement: 0.03,
159        mem_target: "L2".into(),
160        mem_all_sizes: false,
161    };
162    let int_report = run_integer_arms(info, &arms_cfg)?;
163    let mem_report = run_memory_arms(info, &arms_cfg)?;
164    let mut arms = Vec::new();
165    for r in [&int_report, &mem_report] {
166        for a in &r.arms {
167            arms.push(ExperimentArm {
168                name: format!("{}:{}", r.workload, a.arm),
169                summary: Some(a.summary.clone()),
170                notes: a.notes.clone(),
171            });
172        }
173    }
174    let loss_note = if int_report.native_beats_silicera || mem_report.native_beats_silicera {
175        " At least one workload shows native median beating Silicera dispatch — report losses."
176    } else {
177        ""
178    };
179    Ok(ExperimentReport {
180        id: ExperimentId::PortableVsNativeVsSilicera,
181        title: ExperimentId::PortableVsNativeVsSilicera.title().into(),
182        host_brand: info.brand.clone(),
183        fingerprint: info.fingerprint.as_ref().map(|f| f.value.clone()),
184        arms,
185        conclusion: format!(
186            "Identical warmup/iterations/min_improvement across arms. \
187             integer: {}. memory: {}.{}",
188            int_report.conclusion, mem_report.conclusion, loss_note
189        ),
190    })
191}
192
193fn cold_machine(info: &HardwareInfo, cfg: MeasurementConfig) -> Result<ExperimentReport> {
194    let eng = MeasurementEngine::new(cfg.clone());
195    let bench = IntegerBench { n: 7 };
196    let cold = eng.measure(|| {
197        let _ = bench.run_baseline();
198    })?;
199    Ok(ExperimentReport {
200        id: ExperimentId::ColdMachine,
201        title: ExperimentId::ColdMachine.title().into(),
202        host_brand: info.brand.clone(),
203        fingerprint: info.fingerprint.as_ref().map(|f| f.value.clone()),
204        arms: vec![ExperimentArm {
205            name: "no-profile baseline".into(),
206            summary: Some(cold),
207            notes: format!(
208                "warmup={} iterations={} — simulates first-run without HNEP",
209                cfg.warmup, cfg.iterations
210            ),
211        }],
212        conclusion: "Cold machine uses baseline dispatch until `silicera train` produces \
213                     an HNEP. No speedup is claimed without a profile."
214            .into(),
215    })
216}
217
218fn wrong_machine(info: &HardwareInfo, cfg: MeasurementConfig) -> Result<ExperimentReport> {
219    let eng = MeasurementEngine::new(cfg);
220    let bench = ConcurrencyBench {
221        iters: 50_000,
222        threads: 4.min(info.topology.thread_count().max(1)),
223    };
224    let baseline = eng.measure(|| {
225        let _ = bench.run_baseline();
226    })?;
227    Ok(ExperimentReport {
228        id: ExperimentId::WrongMachine,
229        title: ExperimentId::WrongMachine.title().into(),
230        host_brand: info.brand.clone(),
231        fingerprint: info.fingerprint.as_ref().map(|f| f.value.clone()),
232        arms: vec![
233            ExperimentArm {
234                name: "host baseline".into(),
235                summary: Some(baseline),
236                notes: "measured on this host".into(),
237            },
238            ExperimentArm {
239                name: "foreign HNEP".into(),
240                summary: None,
241                notes: "not executed — runtime must report PROFILE MISMATCH and fall back \
242                        (see silicera-runtime). Run `silicera verify` with a mismatched \
243                        profile to observe the path."
244                    .into(),
245            },
246        ],
247        conclusion: "Wrong-machine safety is a correctness requirement: mismatch → baseline. \
248                     Never silently apply another SKU's winners."
249            .into(),
250    })
251}
252
253/// Describe cache targets for docs/CLI.
254pub fn describe_cache_targets(topo: &TopologyGraph) -> Vec<String> {
255    [
256        CacheTarget::L1,
257        CacheTarget::L2,
258        CacheTarget::L3,
259        CacheTarget::Dram,
260    ]
261    .iter()
262    .map(|t| t.describe(topo))
263    .collect()
264}
265