1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17pub enum ExperimentId {
18 SiliconSplit,
20 PortableVsNativeVsSilicera,
22 ColdMachine,
24 WrongMachine,
26}
27
28impl ExperimentId {
29 pub fn all() -> &'static [ExperimentId] {
31 &[
32 ExperimentId::SiliconSplit,
33 ExperimentId::PortableVsNativeVsSilicera,
34 ExperimentId::ColdMachine,
35 ExperimentId::WrongMachine,
36 ]
37 }
38
39 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct ExperimentArm {
63 pub name: String,
65 pub summary: Option<MeasurementSummary>,
67 pub notes: String,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct ExperimentReport {
74 pub id: ExperimentId,
76 pub title: String,
78 pub host_brand: String,
80 pub fingerprint: Option<String>,
82 pub arms: Vec<ExperimentArm>,
84 pub conclusion: String,
86}
87
88pub 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 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
253pub 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