Skip to main content

silicera_lab/
arms.rs

1//! Portable vs host-ISA vs Silicera selection harness.
2//!
3//! Identical inputs, sample counts, warmups, and improvement thresholds across
4//! three arms. The integer domain uses a real runtime AVX2 path when detected
5//! (`HostIsaReduce`); other domains still use algorithmic stand-ins.
6//! Separately compiled `-march=native` binaries remain a future comparison track.
7//!
8//! Domains: integer, memory (multi size-class), float, branch, concurrency.
9
10use 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/// Shared harness knobs (must be identical across arms).
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ArmsHarnessConfig {
22    /// Warmup iterations.
23    pub warmup: usize,
24    /// Timed iterations.
25    pub iterations: usize,
26    /// Minimum relative improvement to prefer native/Silicera over portable.
27    pub min_improvement: f64,
28    /// Working-set target for the single-target memory arm (`L1`/`L2`/`L3`/`DRAM`).
29    pub mem_target: String,
30    /// When true, memory arm runs all size classes (L1/L2/L3/DRAM).
31    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    /// Measurement config.
48    pub fn measurement(&self) -> MeasurementConfig {
49        MeasurementConfig {
50            warmup: self.warmup,
51            iterations: self.iterations,
52            ..Default::default()
53        }
54    }
55}
56
57/// One arm result.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct ArmResult {
60    /// Arm id: `portable`, `native`, `silicera`.
61    pub arm: String,
62    /// Median ns.
63    pub summary: MeasurementSummary,
64    /// Notes / methodology.
65    pub notes: String,
66}
67
68/// Full three-arm report for one workload family.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct ArmsReport {
71    /// Workload family name.
72    pub workload: String,
73    /// Host brand.
74    pub host_brand: String,
75    /// Fingerprint.
76    pub fingerprint: Option<String>,
77    /// Config used.
78    pub config: ArmsHarnessConfig,
79    /// Arms in order: portable, native, silicera.
80    pub arms: Vec<ArmResult>,
81    /// Which arm had the lowest median.
82    pub fastest_arm: String,
83    /// True when native beat silicera's selected median (Silicera loss / tie path).
84    pub native_beats_silicera: bool,
85    /// True when portable beat both (Silicera and native lose).
86    pub portable_wins: bool,
87    /// Honest conclusion string.
88    pub conclusion: String,
89}
90
91/// Aggregate harness run across domains.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct HarnessSuiteReport {
94    /// Host brand.
95    pub host_brand: String,
96    /// Fingerprint.
97    pub fingerprint: Option<String>,
98    /// Per-workload reports.
99    pub reports: Vec<ArmsReport>,
100    /// Count where Silicera selection matched the fastest measured arm.
101    pub silicera_selection_ok: usize,
102    /// Count where portable won (no specialization gain).
103    pub portable_win_count: usize,
104    /// Count where native beat Silicera dispatch.
105    pub native_beats_silicera_count: usize,
106    /// Size-class memory winner change summary (when multi-size run).
107    pub size_winner_changes: Vec<String>,
108}
109
110/// Run integer-domain three-arm harness.
111pub 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
133/// Run float-domain three-arm harness.
134pub 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
158/// Run branch-domain three-arm harness.
159pub 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
178/// Run concurrency-domain three-arm harness.
179pub 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
204/// Run memory-domain three-arm harness (scan vs prefetch-friendly).
205pub 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
394/// Summarize whether memory size-class winners differ across L1/L2/L3/DRAM.
395pub 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
454/// Run both integer and memory harnesses (legacy).
455pub 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
462/// Run harness for a domain: `all|integer|memory|float|branch|concurrency`.
463pub 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}