1use serde::{Deserialize, Serialize};
11use silicera::hardware::HardwareInfo;
12use silicera::measure::{MeasurementConfig, MeasurementEngine, MeasurementSummary};
13use silicera::Result;
14
15use crate::bench::{
16 BranchBench, CacheTarget, ConcurrencyBench, HostIsaDot, HostIsaReduce, MemOpBench,
17};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ArmsHarnessConfig {
22 pub warmup: usize,
24 pub iterations: usize,
26 pub min_improvement: f64,
28 pub mem_target: String,
30 pub mem_all_sizes: bool,
32}
33
34impl Default for ArmsHarnessConfig {
35 fn default() -> Self {
36 Self {
37 warmup: 5,
38 iterations: 30,
39 min_improvement: 0.03,
40 mem_target: "L2".into(),
41 mem_all_sizes: false,
42 }
43 }
44}
45
46impl ArmsHarnessConfig {
47 pub fn measurement(&self) -> MeasurementConfig {
49 MeasurementConfig {
50 warmup: self.warmup,
51 iterations: self.iterations,
52 ..Default::default()
53 }
54 }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct ArmResult {
60 pub arm: String,
62 pub summary: MeasurementSummary,
64 pub notes: String,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct ArmsReport {
71 pub workload: String,
73 pub host_brand: String,
75 pub fingerprint: Option<String>,
77 pub config: ArmsHarnessConfig,
79 pub arms: Vec<ArmResult>,
81 pub fastest_arm: String,
83 pub native_beats_silicera: bool,
85 pub portable_wins: bool,
87 pub conclusion: String,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct HarnessSuiteReport {
94 pub host_brand: String,
96 pub fingerprint: Option<String>,
98 pub reports: Vec<ArmsReport>,
100 pub silicera_selection_ok: usize,
102 pub portable_win_count: usize,
104 pub native_beats_silicera_count: usize,
106 pub size_winner_changes: Vec<String>,
108}
109
110pub fn run_integer_arms(info: &HardwareInfo, cfg: &ArmsHarnessConfig) -> Result<ArmsReport> {
112 let eng = MeasurementEngine::new(cfg.measurement());
113 let bench = HostIsaReduce::new(65_536);
114 if !bench.verify_correctness() {
115 return Err(silicera::SiliceraError::CorrectnessFailure(
116 "HostIsaReduce portable vs host-ISA disagree".into(),
117 ));
118 }
119 let portable = eng.measure(|| {
120 let _ = bench.run_portable();
121 })?;
122 let native = eng.measure(|| {
123 let _ = bench.run_host_isa();
124 })?;
125 let isa_note = if HostIsaReduce::avx2_active() {
126 "portable = scalar i32 sum; host-isa = runtime AVX2 reduction (target_feature); not a separate -march=native binary"
127 } else {
128 "portable = scalar i32 sum; host-isa fell back to scalar (AVX2 not detected)"
129 };
130 finish_report("integer-host-isa", info, cfg, portable, native, isa_note)
131}
132
133pub fn run_float_arms(info: &HardwareInfo, cfg: &ArmsHarnessConfig) -> Result<ArmsReport> {
135 let eng = MeasurementEngine::new(cfg.measurement());
136 let bench = HostIsaDot::new(65_536);
137 if !bench.verify_correctness(1e-3) {
138 return Err(silicera::SiliceraError::CorrectnessFailure(
139 "HostIsaDot portable vs host-ISA disagree beyond tolerance".into(),
140 ));
141 }
142 let portable = eng.measure(|| {
143 let _ = bench.run_portable();
144 })?;
145 let native = eng.measure(|| {
146 let _ = bench.run_host_isa();
147 })?;
148 finish_report(
149 "float-host-isa-dot",
150 info,
151 cfg,
152 portable,
153 native,
154 "portable = scalar f32 dot; host-isa = runtime AVX when detected; correctness gated",
155 )
156}
157
158pub fn run_branch_arms(info: &HardwareInfo, cfg: &ArmsHarnessConfig) -> Result<ArmsReport> {
160 let eng = MeasurementEngine::new(cfg.measurement());
161 let bench = BranchBench::new(8192);
162 let portable = eng.measure(|| {
163 let _ = bench.run_baseline();
164 })?;
165 let native = eng.measure(|| {
166 let _ = bench.run_candidate();
167 })?;
168 finish_report(
169 "branch-mix",
170 info,
171 cfg,
172 portable,
173 native,
174 "portable = unpredictable branches; native = branchless-ish stand-in",
175 )
176}
177
178pub fn run_concurrency_arms(info: &HardwareInfo, cfg: &ArmsHarnessConfig) -> Result<ArmsReport> {
180 let eng = MeasurementEngine::new(cfg.measurement());
181 let threads = 4.min(info.topology.thread_count().max(1));
182 let bench = ConcurrencyBench {
183 iters: 20_000,
184 threads,
185 };
186 let portable = eng.measure(|| {
187 let _ = bench.run_baseline();
188 })?;
189 let native = eng.measure(|| {
190 let _ = bench.run_candidate();
191 })?;
192 finish_report(
193 "concurrency-atomics",
194 info,
195 cfg,
196 portable,
197 native,
198 &format!(
199 "portable = contended atomics; native = per-thread locals+reduce; threads={threads} (Silicera-owned; no OS affinity changes)"
200 ),
201 )
202}
203
204pub fn run_memory_arms(info: &HardwareInfo, cfg: &ArmsHarnessConfig) -> Result<ArmsReport> {
206 let target = parse_mem_target(&cfg.mem_target);
207 run_memory_arms_for_target(info, cfg, target)
208}
209
210fn parse_mem_target(s: &str) -> CacheTarget {
211 match s.to_uppercase().as_str() {
212 "L1" => CacheTarget::L1,
213 "L3" => CacheTarget::L3,
214 "DRAM" => CacheTarget::Dram,
215 _ => CacheTarget::L2,
216 }
217}
218
219fn run_memory_arms_for_target(
220 info: &HardwareInfo,
221 cfg: &ArmsHarnessConfig,
222 target: CacheTarget,
223) -> Result<ArmsReport> {
224 let eng = MeasurementEngine::new(cfg.measurement());
225 let mut op = MemOpBench::for_target(&info.topology, target);
226 let portable = eng.measure(|| {
227 let _ = op.run_scan_stride();
228 })?;
229 let native = eng.measure(|| {
230 let _ = op.run_scan_dense();
231 })?;
232 let copy = eng.measure(|| {
233 let _ = op.run_copy();
234 })?;
235 let (sil_name, sil_summary, notes_extra) = {
236 let mut best = ("portable", portable.clone());
237 if native.median_ns < best.1.median_ns {
238 best = ("native", native.clone());
239 }
240 if copy.median_ns < best.1.median_ns {
241 best = ("copy", copy.clone());
242 }
243 (
244 format!("silicera→{}", best.0),
245 best.1,
246 format!(
247 "selection pool medians: portable={:.0} native={:.0} copy={:.0}",
248 portable.median_ns, native.median_ns, copy.median_ns
249 ),
250 )
251 };
252
253 let native_beats_silicera = native.median_ns + f64::EPSILON < sil_summary.median_ns;
254 let portable_wins =
255 portable.median_ns <= native.median_ns && portable.median_ns <= sil_summary.median_ns;
256
257 let conclusion = build_conclusion(
258 native_beats_silicera,
259 portable_wins,
260 &sil_name,
261 portable.median_ns,
262 native.median_ns,
263 sil_summary.median_ns,
264 );
265
266 Ok(ArmsReport {
267 workload: format!("memop-{}", target.class_id()),
268 host_brand: info.brand.clone(),
269 fingerprint: info.fingerprint.as_ref().map(|f| f.value.clone()),
270 config: cfg.clone(),
271 arms: vec![
272 ArmResult {
273 arm: "portable".into(),
274 summary: portable,
275 notes: format!("scan_stride; {}", target.describe(&info.topology)),
276 },
277 ArmResult {
278 arm: "native".into(),
279 summary: native,
280 notes: "scan_dense stand-in for host-tuned kernel".into(),
281 },
282 ArmResult {
283 arm: sil_name.clone(),
284 summary: sil_summary,
285 notes: format!(
286 "Silicera selection after measurement. {notes_extra}. {}",
287 "Not a separately compiled -march=native binary in this release."
288 ),
289 },
290 ],
291 fastest_arm: if portable_wins {
292 "portable".into()
293 } else if native_beats_silicera {
294 "native".into()
295 } else {
296 "silicera".into()
297 },
298 native_beats_silicera,
299 portable_wins,
300 conclusion,
301 })
302}
303
304fn finish_report(
305 workload: &str,
306 info: &HardwareInfo,
307 cfg: &ArmsHarnessConfig,
308 portable: MeasurementSummary,
309 native: MeasurementSummary,
310 method_note: &str,
311) -> Result<ArmsReport> {
312 let improved = native.median_ns < portable.median_ns * (1.0 - cfg.min_improvement);
313 let (sil_name, sil_summary) = if improved || native.median_ns < portable.median_ns {
314 ("silicera→native", native.clone())
315 } else {
316 ("silicera→portable", portable.clone())
317 };
318 let native_beats_silicera = native.median_ns + f64::EPSILON < sil_summary.median_ns;
319 let portable_wins =
320 portable.median_ns <= native.median_ns && portable.median_ns <= sil_summary.median_ns;
321 let conclusion = build_conclusion(
322 native_beats_silicera,
323 portable_wins,
324 sil_name,
325 portable.median_ns,
326 native.median_ns,
327 sil_summary.median_ns,
328 );
329 Ok(ArmsReport {
330 workload: workload.into(),
331 host_brand: info.brand.clone(),
332 fingerprint: info.fingerprint.as_ref().map(|f| f.value.clone()),
333 config: cfg.clone(),
334 arms: vec![
335 ArmResult {
336 arm: "portable".into(),
337 summary: portable,
338 notes: method_note.into(),
339 },
340 ArmResult {
341 arm: "native".into(),
342 summary: native,
343 notes: method_note.into(),
344 },
345 ArmResult {
346 arm: sil_name.into(),
347 summary: sil_summary,
348 notes: format!(
349 "selected by median after measurement (min_improvement={})",
350 cfg.min_improvement
351 ),
352 },
353 ],
354 fastest_arm: if portable_wins {
355 "portable".into()
356 } else if native_beats_silicera {
357 "native".into()
358 } else {
359 "silicera".into()
360 },
361 native_beats_silicera,
362 portable_wins,
363 conclusion,
364 })
365}
366
367fn build_conclusion(
368 native_beats_silicera: bool,
369 portable_wins: bool,
370 sil_name: &str,
371 p: f64,
372 n: f64,
373 s: f64,
374) -> String {
375 if native_beats_silicera {
376 format!(
377 "LOSS/CAVEAT: native median ({n:.0} ns) beat Silicera dispatch ({s:.0} ns). \
378 Print losses clearly — do not hide them. Silicera arm was {sil_name}."
379 )
380 } else if portable_wins {
381 format!(
382 "Portable median ({p:.0} ns) ≤ native ({n:.0} ns) and Silicera ({s:.0} ns). \
383 No specialization win on this host/workload under these knobs."
384 )
385 } else {
386 format!(
387 "Silicera selected {sil_name} (median {s:.0} ns). Portable={p:.0} native={n:.0}. \
388 Selection is measured, not claimed universal. Host-ISA arm uses runtime AVX2 when present; \
389 separately compiled -march=native artifacts remain a future comparison track."
390 )
391 }
392}
393
394pub fn summarize_size_winner_changes(reports: &[ArmsReport]) -> Vec<String> {
396 let mem: Vec<(&str, &str)> = reports
397 .iter()
398 .filter(|r| r.workload.starts_with("memop-"))
399 .filter_map(|r| {
400 let class = r.workload.strip_prefix("memop-")?;
401 let sel = r
402 .arms
403 .iter()
404 .find(|a| a.arm.starts_with("silicera"))
405 .map(|a| a.arm.as_str())
406 .unwrap_or("?");
407 Some((class, sel))
408 })
409 .collect();
410 if mem.len() < 2 {
411 return Vec::new();
412 }
413 let first = mem[0].1;
414 let all_same = mem.iter().all(|(_, s)| *s == first);
415 if all_same {
416 vec![format!(
417 "memop size-class Silicera selection identical across {:?}: {first}",
418 mem.iter().map(|(c, _)| *c).collect::<Vec<_>>()
419 )]
420 } else {
421 mem.iter()
422 .map(|(c, s)| format!("{c} → {s}"))
423 .collect::<Vec<_>>()
424 .into_iter()
425 .chain(std::iter::once(
426 "Winners DO change by size class on this host/run".into(),
427 ))
428 .collect()
429 }
430}
431
432fn suite_from(info: &HardwareInfo, reports: Vec<ArmsReport>) -> HarnessSuiteReport {
433 let size_winner_changes = summarize_size_winner_changes(&reports);
434 let portable_win_count = reports.iter().filter(|r| r.portable_wins).count();
435 let native_beats_silicera_count = reports.iter().filter(|r| r.native_beats_silicera).count();
436 let silicera_selection_ok = reports
437 .iter()
438 .filter(|r| {
439 (!r.native_beats_silicera && r.fastest_arm.starts_with("silicera"))
440 || (r.portable_wins && r.fastest_arm == "portable")
441 })
442 .count();
443 HarnessSuiteReport {
444 host_brand: info.brand.clone(),
445 fingerprint: info.fingerprint.as_ref().map(|f| f.value.clone()),
446 reports,
447 silicera_selection_ok,
448 portable_win_count,
449 native_beats_silicera_count,
450 size_winner_changes,
451 }
452}
453
454pub fn run_full_harness(
456 info: &HardwareInfo,
457 cfg: &ArmsHarnessConfig,
458) -> Result<Vec<ArmsReport>> {
459 Ok(run_domain_harness(info, cfg, "all")?.reports)
460}
461
462pub fn run_domain_harness(
464 info: &HardwareInfo,
465 cfg: &ArmsHarnessConfig,
466 domain: &str,
467) -> Result<HarnessSuiteReport> {
468 let mut reports = Vec::new();
469 let d = domain.to_ascii_lowercase();
470 let all = d == "all";
471
472 if all || d == "integer" {
473 reports.push(run_integer_arms(info, cfg)?);
474 }
475 if all || d == "float" {
476 reports.push(run_float_arms(info, cfg)?);
477 }
478 if all || d == "branch" {
479 reports.push(run_branch_arms(info, cfg)?);
480 }
481 if all || d == "concurrency" {
482 reports.push(run_concurrency_arms(info, cfg)?);
483 }
484 if all || d == "memory" {
485 if cfg.mem_all_sizes || all {
486 for t in CacheTarget::all() {
487 reports.push(run_memory_arms_for_target(info, cfg, t)?);
488 }
489 } else {
490 reports.push(run_memory_arms(info, cfg)?);
491 }
492 }
493
494 if reports.is_empty() {
495 return Err(silicera::SiliceraError::Parse(format!(
496 "unknown harness domain '{domain}' (all|integer|memory|float|branch|concurrency)"
497 )));
498 }
499 Ok(suite_from(info, reports))
500}