Skip to main content

silicera_lab/
silicon_split.rs

1//! Silicon Split — multi-machine measured strategy comparison.
2//!
3//! Protocol: identical workload set + candidate set + measurement knobs on
4//! Machine A and Machine B. Artifacts are comparable HNEPs / sanitized exports.
5//! Until Machine B exists, emit a **placeholder schema** and verdict UNKNOWN.
6//! Never invent Machine B numbers.
7
8use std::path::Path;
9
10use serde::{Deserialize, Serialize};
11use silicera::compare::{
12    compare_profiles, compare_with_placeholder, CompareReport, MachineBPlaceholder,
13    MeasurementEcho, SanitizedSplitExport, StrategyVector, SILICON_SPLIT_PROTOCOL,
14    SILICON_SPLIT_PROTOCOL_VERSION,
15};
16use silicera::hardware::HardwareInfo;
17use silicera::hnep::HnepProfile;
18use silicera::measure::MeasurementConfig;
19use silicera::Result;
20
21use crate::profile_gen::{generate_profile, ProfileGenConfig};
22
23/// Fixed protocol knobs for Silicon Split (must match across machines).
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct SiliconSplitProtocol {
26    /// Protocol id.
27    pub protocol: String,
28    /// Protocol semver.
29    pub protocol_version: String,
30    /// Measurement echo.
31    pub measurement: MeasurementEcho,
32}
33
34impl Default for SiliconSplitProtocol {
35    fn default() -> Self {
36        Self {
37            protocol: SILICON_SPLIT_PROTOCOL.into(),
38            protocol_version: SILICON_SPLIT_PROTOCOL_VERSION.into(),
39            measurement: MeasurementEcho {
40                warmup: 3,
41                iterations: 20,
42                min_improvement: 0.03,
43                candidate_set: "baseline|scan_dense|copy|copy_loop|candidate".into(),
44                workload_set: "memory-l2|integer|float|branch|memscan-l1|memscan-l2|memscan-l3|memscan-dram"
45                    .into(),
46            },
47        }
48    }
49}
50
51impl SiliconSplitProtocol {
52    /// Build profile-gen config locked to this protocol.
53    pub fn profile_gen_config(&self, role: &str) -> ProfileGenConfig {
54        ProfileGenConfig {
55            measurement: MeasurementConfig {
56                warmup: self.measurement.warmup,
57                iterations: self.measurement.iterations,
58                ..Default::default()
59            },
60            label: format!("silicon-split-{role}"),
61            size_tree: true,
62            size_class_tournaments: true,
63            extended_size_targets: false,
64            min_improvement: self.measurement.min_improvement,
65            only: None,
66        }
67    }
68}
69
70/// Train Machine A or B under the Silicon Split protocol.
71pub fn train_split_profile(
72    info: &HardwareInfo,
73    role: &str,
74    out: &Path,
75    protocol: &SiliconSplitProtocol,
76) -> Result<(HnepProfile, SanitizedSplitExport)> {
77    let cfg = protocol.profile_gen_config(role);
78    let (profile, _results) = generate_profile(info, &cfg, out)?;
79    let export = sanitize_export(&profile, role, &protocol.measurement);
80    Ok((profile, export))
81}
82
83/// Wrap an existing HNEP as a sanitized Silicon Split export.
84pub fn sanitize_export(
85    profile: &HnepProfile,
86    role: &str,
87    measurement: &MeasurementEcho,
88) -> SanitizedSplitExport {
89    SanitizedSplitExport {
90        protocol: SILICON_SPLIT_PROTOCOL.into(),
91        protocol_version: SILICON_SPLIT_PROTOCOL_VERSION.into(),
92        machine_role: role.into(),
93        strategy_vector: StrategyVector::from_profile(role, profile),
94        measurement_echo: measurement.clone(),
95        profile: profile.clone(),
96    }
97}
98
99/// Write sanitized export JSON.
100pub fn write_export(export: &SanitizedSplitExport, path: &Path) -> Result<()> {
101    if let Some(parent) = path.parent() {
102        std::fs::create_dir_all(parent)?;
103    }
104    let text = serde_json::to_string_pretty(export)?;
105    std::fs::write(path, text)?;
106    Ok(())
107}
108
109/// Read sanitized export JSON.
110pub fn read_export(path: &Path) -> Result<SanitizedSplitExport> {
111    let text = std::fs::read_to_string(path)?;
112    let export: SanitizedSplitExport = serde_json::from_str(&text)?;
113    export.profile.verify_integrity()?;
114    if export.protocol != SILICON_SPLIT_PROTOCOL {
115        return Err(silicera::SiliceraError::Parse(format!(
116            "unexpected silicon-split protocol {}",
117            export.protocol
118        )));
119    }
120    Ok(export)
121}
122
123/// Write Machine B placeholder next to Machine A artifacts.
124pub fn write_machine_b_placeholder(a: &HnepProfile, path: &Path) -> Result<MachineBPlaceholder> {
125    let ph = MachineBPlaceholder::from_machine_a(a);
126    if let Some(parent) = path.parent() {
127        std::fs::create_dir_all(parent)?;
128    }
129    std::fs::write(path, serde_json::to_string_pretty(&ph)?)?;
130    Ok(ph)
131}
132
133/// Produce a Silicon Split report from two profiles, or A + placeholder.
134pub fn split_report_profiles(a: &HnepProfile, b: Option<&HnepProfile>) -> CompareReport {
135    match b {
136        Some(b) => compare_profiles(a, b),
137        None => compare_with_placeholder(a, "second-zen-box"),
138    }
139}
140
141/// Load exports and compare (B path optional → placeholder verdict).
142pub fn split_report_paths(a: &Path, b: Option<&Path>) -> Result<CompareReport> {
143    let pa = if a.extension().and_then(|e| e.to_str()) == Some("json") {
144        read_export(a)?.profile
145    } else {
146        HnepProfile::read_from(a)?
147    };
148    let pb = match b {
149        Some(path) => {
150            let p = if path.extension().and_then(|e| e.to_str()) == Some("json") {
151                read_export(path)?.profile
152            } else {
153                HnepProfile::read_from(path)?
154            };
155            Some(p)
156        }
157        None => None,
158    };
159    Ok(split_report_profiles(&pa, pb.as_ref()))
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use silicera::hardware::{HardwareBackend, MockHardware};
166    use silicera::knowledge::KnowledgePack;
167    use silicera::SplitVerdict;
168    use tempfile::tempdir;
169
170    #[test]
171    fn mock_a_placeholder_unknown() {
172        let kp = KnowledgePack::builtin();
173        let info = MockHardware::zen5_dual_ccd().discover(&kp).unwrap();
174        let dir = tempdir().unwrap();
175        let out = dir.path().join("a.hnep");
176        let protocol = SiliconSplitProtocol {
177            measurement: MeasurementEcho {
178                warmup: 1,
179                iterations: 3,
180                min_improvement: 0.03,
181                ..MeasurementEcho::default()
182            },
183            ..Default::default()
184        };
185        let (profile, export) = train_split_profile(&info, "A", &out, &protocol).unwrap();
186        assert!(!export.strategy_vector.workloads.is_empty());
187        let report = split_report_profiles(&profile, None);
188        assert_eq!(report.verdict, SplitVerdict::Unknown);
189        assert!(report.b_is_placeholder);
190    }
191
192    #[test]
193    fn mock_two_machines_compare_no_invented_physics() {
194        // Two mock SKUs: compare structure only — timings are mock-host measurements,
195        // not fabricated second-SKU marketing numbers.
196        let kp = KnowledgePack::builtin();
197        let a_info = MockHardware::zen5_dual_ccd().discover(&kp).unwrap();
198        let b_info = MockHardware::zen4_single_ccd().discover(&kp).unwrap();
199        let dir = tempdir().unwrap();
200        let protocol = SiliconSplitProtocol {
201            measurement: MeasurementEcho {
202                warmup: 1,
203                iterations: 3,
204                min_improvement: 0.03,
205                ..MeasurementEcho::default()
206            },
207            ..Default::default()
208        };
209        let (pa, _) = train_split_profile(&a_info, "A", &dir.path().join("a.hnep"), &protocol).unwrap();
210        let (pb, _) = train_split_profile(&b_info, "B", &dir.path().join("b.hnep"), &protocol).unwrap();
211        let report = compare_profiles(&pa, &pb);
212        assert!(!report.b_is_placeholder);
213        assert!(!report.fingerprint_a.is_empty());
214        assert!(!report.fingerprint_b.is_empty());
215        assert_ne!(report.fingerprint_a, report.fingerprint_b);
216        // Verdict is whatever measurement says — must be one of the real enums.
217        let _ = report.verdict.label();
218    }
219}
220