Skip to main content

silicera_lab/
native_artifacts.rs

1//! Dual-artifact comparison: portable codegen vs `-C target-cpu=native`.
2//!
3//! Builds the standalone `benchmarks/arm_kernels` package twice (generic vs
4//! native), runs each binary, and compares measured medians. This is the
5//! dual-artifact track beyond in-process AVX2 stand-ins.
6//!
7//! Requires a working `cargo` on PATH. Never invents timings.
8
9use std::path::{Path, PathBuf};
10use std::process::Command;
11use std::time::Instant;
12
13use serde::{Deserialize, Serialize};
14use silicera::hardware::HardwareInfo;
15use silicera::Result;
16
17/// One compiled artifact run.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ArtifactArm {
20    /// `portable` or `native`.
21    pub arm: String,
22    /// RUSTFLAGS used.
23    pub rustflags: String,
24    /// Path to binary (best-effort).
25    pub binary: String,
26    /// Kernel name requested.
27    pub kernel: String,
28    /// Iterations inside the binary.
29    pub iterations: usize,
30    /// Median nanoseconds reported by the binary (or wall-time fallback).
31    pub median_ns: f64,
32    /// Raw stdout (truncated).
33    pub stdout_excerpt: String,
34    /// Notes / methodology.
35    pub notes: String,
36}
37
38/// Full dual-artifact report.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct NativeArtifactReport {
41    /// Host brand.
42    pub host_brand: String,
43    /// Fingerprint.
44    pub fingerprint: Option<String>,
45    /// Kernel.
46    pub kernel: String,
47    /// Portable arm.
48    pub portable: ArtifactArm,
49    /// Native arm.
50    pub native: ArtifactArm,
51    /// Silicera selection under min_improvement.
52    pub silicera_choice: String,
53    /// True when native beat portable by threshold.
54    pub native_wins: bool,
55    /// True when portable is best.
56    pub portable_wins: bool,
57    /// Conclusion.
58    pub conclusion: String,
59}
60
61/// Locate workspace root (directory containing `benchmarks/arm_kernels`).
62pub fn find_workspace_root(start: &Path) -> Option<PathBuf> {
63    let mut cur = start.to_path_buf();
64    for _ in 0..8 {
65        if cur.join("benchmarks").join("arm_kernels").join("Cargo.toml").is_file() {
66            return Some(cur);
67        }
68        if !cur.pop() {
69            break;
70        }
71    }
72    None
73}
74
75/// Run portable vs native compiled artifact comparison.
76pub fn run_native_artifact_compare(
77    info: &HardwareInfo,
78    kernel: &str,
79    iterations: usize,
80    min_improvement: f64,
81    workspace: &Path,
82) -> Result<NativeArtifactReport> {
83    let kernels_dir = workspace.join("benchmarks").join("arm_kernels");
84    if !kernels_dir.join("Cargo.toml").is_file() {
85        return Err(silicera::SiliceraError::Parse(format!(
86            "missing benchmarks/arm_kernels at {}",
87            kernels_dir.display()
88        )));
89    }
90
91    let target_root = workspace.join("target").join("silicera-arms");
92    std::fs::create_dir_all(&target_root)?;
93
94    let portable = build_and_run(
95        &kernels_dir,
96        &target_root.join("portable"),
97        "portable",
98        "-C target-cpu=x86-64-v2",
99        kernel,
100        iterations,
101    )?;
102    let native = build_and_run(
103        &kernels_dir,
104        &target_root.join("native"),
105        "native",
106        "-C target-cpu=native",
107        kernel,
108        iterations,
109    )?;
110
111    let native_wins =
112        native.median_ns < portable.median_ns * (1.0 - min_improvement);
113    let portable_wins = portable.median_ns <= native.median_ns;
114    let silicera_choice = if native_wins {
115        "silicera→native_artifact".into()
116    } else {
117        "silicera→portable_artifact".into()
118    };
119    let conclusion = if native_wins {
120        format!(
121            "Native artifact faster ({:.0} vs {:.0} ns, min_improvement={}). \
122             Silicera would prefer the native-compiled kernel for this host/kernel.",
123            native.median_ns, portable.median_ns, min_improvement
124        )
125    } else if portable_wins {
126        format!(
127            "Portable artifact ≤ native ({:.0} vs {:.0} ns). \
128             No specialization win from -C target-cpu=native under these knobs — report honestly.",
129            portable.median_ns, native.median_ns
130        )
131    } else {
132        format!(
133            "Difference below threshold. portable={:.0} native={:.0} ns.",
134            portable.median_ns, native.median_ns
135        )
136    };
137
138    Ok(NativeArtifactReport {
139        host_brand: info.brand.clone(),
140        fingerprint: info.fingerprint.as_ref().map(|f| f.value.clone()),
141        kernel: kernel.into(),
142        portable,
143        native,
144        silicera_choice,
145        native_wins,
146        portable_wins,
147        conclusion,
148    })
149}
150
151/// Default kernels for a single-machine artifact suite.
152pub fn default_artifact_kernels() -> &'static [&'static str] {
153    &["dot_f32", "saxpy_f32", "checksum_u8", "reduce_i32"]
154}
155
156/// Suite of native-artifact comparisons on one host.
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct NativeArtifactSuite {
159    /// Host brand.
160    pub host_brand: String,
161    /// Fingerprint.
162    pub fingerprint: Option<String>,
163    /// Per-kernel reports.
164    pub reports: Vec<NativeArtifactReport>,
165    /// Kernels where native won.
166    pub native_win_count: usize,
167    /// Kernels where portable won / tied.
168    pub portable_win_count: usize,
169    /// Summary line.
170    pub summary: String,
171}
172
173/// Run all default (or provided) kernels.
174pub fn run_native_artifact_suite(
175    info: &HardwareInfo,
176    kernels: &[&str],
177    iterations: usize,
178    min_improvement: f64,
179    workspace: &Path,
180) -> Result<NativeArtifactSuite> {
181    let mut reports = Vec::new();
182    for k in kernels {
183        reports.push(run_native_artifact_compare(
184            info,
185            k,
186            iterations,
187            min_improvement,
188            workspace,
189        )?);
190    }
191    let native_win_count = reports.iter().filter(|r| r.native_wins).count();
192    let portable_win_count = reports.iter().filter(|r| r.portable_wins).count();
193    let summary = format!(
194        "artifact suite: native_wins={native_win_count} portable_wins={portable_win_count} kernels={}",
195        reports.len()
196    );
197    Ok(NativeArtifactSuite {
198        host_brand: info.brand.clone(),
199        fingerprint: info.fingerprint.as_ref().map(|f| f.value.clone()),
200        reports,
201        native_win_count,
202        portable_win_count,
203        summary,
204    })
205}
206
207/// Convert a suite into HNEP workload entries (names `artifact-<kernel>`).
208pub fn suite_to_workload_entries(suite: &NativeArtifactSuite) -> Vec<silicera::WorkloadEntry> {
209    use silicera::{Confidence, WorkloadEntry};
210    suite
211        .reports
212        .iter()
213        .map(|r| {
214            let (winner, wmed, bmed, conf) = if r.native_wins {
215                (
216                    "native_artifact",
217                    Some(r.native.median_ns),
218                    Some(r.portable.median_ns),
219                    Confidence::High,
220                )
221            } else if r.portable_wins {
222                (
223                    "portable_artifact",
224                    Some(r.portable.median_ns),
225                    Some(r.native.median_ns),
226                    Confidence::Medium,
227                )
228            } else {
229                (
230                    "portable_artifact",
231                    Some(r.portable.median_ns),
232                    Some(r.native.median_ns),
233                    Confidence::Low,
234                )
235            };
236            WorkloadEntry {
237                name: format!("artifact-{}", r.kernel),
238                winner: winner.into(),
239                confidence: conf,
240                rationale: r.conclusion.clone(),
241                winner_median_ns: wmed,
242                baseline_median_ns: bmed,
243            }
244        })
245        .collect()
246}
247
248fn build_and_run(
249    kernels_dir: &Path,
250    target_dir: &Path,
251    arm: &str,
252    rustflags: &str,
253    kernel: &str,
254    iterations: usize,
255) -> Result<ArtifactArm> {
256    std::fs::create_dir_all(target_dir)?;
257    let status = Command::new("cargo")
258        .arg("build")
259        .arg("--release")
260        .arg("--manifest-path")
261        .arg(kernels_dir.join("Cargo.toml"))
262        .env("CARGO_TARGET_DIR", target_dir)
263        .env("RUSTFLAGS", rustflags)
264        .status()
265        .map_err(|e| silicera::SiliceraError::Parse(format!("cargo build failed to start: {e}")))?;
266    if !status.success() {
267        return Err(silicera::SiliceraError::Parse(format!(
268            "cargo build ({arm}) failed with {status}"
269        )));
270    }
271
272    let bin = target_dir
273        .join("release")
274        .join(if cfg!(windows) {
275            "silicera-arm-kernels.exe"
276        } else {
277            "silicera-arm-kernels"
278        });
279    if !bin.is_file() {
280        return Err(silicera::SiliceraError::Parse(format!(
281            "expected binary at {}",
282            bin.display()
283        )));
284    }
285
286    let t0 = Instant::now();
287    let output = Command::new(&bin)
288        .arg("--kernel")
289        .arg(kernel)
290        .arg("--iterations")
291        .arg(iterations.to_string())
292        .output()
293        .map_err(|e| silicera::SiliceraError::Parse(format!("run {arm}: {e}")))?;
294    let wall_ns = t0.elapsed().as_nanos() as f64;
295    let stdout = String::from_utf8_lossy(&output.stdout).to_string();
296    let stderr = String::from_utf8_lossy(&output.stderr).to_string();
297    if !output.status.success() {
298        return Err(silicera::SiliceraError::Parse(format!(
299            "{arm} binary failed: {stderr}"
300        )));
301    }
302    let median_ns = parse_median_ns(&stdout).unwrap_or(wall_ns);
303    let excerpt: String = stdout.chars().take(400).collect();
304    Ok(ArtifactArm {
305        arm: arm.into(),
306        rustflags: rustflags.into(),
307        binary: bin.display().to_string(),
308        kernel: kernel.into(),
309        iterations,
310        median_ns,
311        stdout_excerpt: excerpt,
312        notes: format!("built with RUSTFLAGS='{rustflags}'"),
313    })
314}
315
316fn parse_median_ns(stdout: &str) -> Option<f64> {
317    for line in stdout.lines() {
318        let line = line.trim();
319        if let Some(rest) = line.strip_prefix("median_ns=") {
320            return rest.trim().parse().ok();
321        }
322    }
323    None
324}