1use serde::{Deserialize, Serialize};
2
3use super::{
4 AdapterConstraints, ComparisonReport, ForgeError, GeneratedShader, Schedule, TuningResult,
5 ValidationReport, CUDARC_API_VERSION, FORGE_SCHEMA_VERSION, NAGA_API_VERSION, WGPU_API_VERSION,
6};
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct HardwareProfile {
13 pub adapter: AdapterIdentity,
14 pub constraints: AdapterConstraints,
15 pub memory_class: String,
17 pub supports_timestamp_query: bool,
18 pub max_compute_workgroup_storage_size: u32,
19 pub max_storage_buffer_binding_size: u64,
20 pub min_storage_buffer_offset_alignment: u32,
21 pub min_uniform_buffer_offset_alignment: u32,
22}
23
24impl HardwareProfile {
25 pub fn topology_hash(&self) -> Result<String, ForgeError> {
28 let bytes = serde_json::to_vec(&(
29 FORGE_SCHEMA_VERSION,
30 &self.adapter,
31 &self.constraints,
32 &self.memory_class,
33 self.max_compute_workgroup_storage_size,
34 self.min_storage_buffer_offset_alignment,
35 self.min_uniform_buffer_offset_alignment,
36 WGPU_API_VERSION,
37 ))?;
38 Ok(blake3::hash(&bytes).to_hex().to_string())
39 }
40
41 pub fn to_pretty_json(&self) -> Result<String, ForgeError> {
42 Ok(serde_json::to_string_pretty(self)?)
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum ValidationLevel {
49 Generated,
50 NagaValidated,
51 PipelineCreated,
52 OracleVerified,
53 Profiled,
54 Certified,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "snake_case")]
59pub enum TimingSource {
60 GpuTimestamp,
61 CompletionClock,
62 Synthetic,
63}
64
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66pub struct TimingSummary {
67 pub source: TimingSource,
68 pub sample_count: usize,
69 pub minimum_ns: u64,
70 pub median_ns: u64,
71 pub p95_ns: u64,
72}
73
74impl TimingSummary {
75 pub fn from_samples(source: TimingSource, samples: &[u64]) -> Option<Self> {
76 if samples.is_empty() {
77 return None;
78 }
79 let mut sorted = samples.to_vec();
80 sorted.sort_unstable();
81 let median_ns = sorted[(sorted.len() - 1) / 2];
82 let p95_index = ((sorted.len() - 1) * 95).div_ceil(100);
83 Some(Self {
84 source,
85 sample_count: sorted.len(),
86 minimum_ns: sorted[0],
87 median_ns,
88 p95_ns: sorted[p95_index],
89 })
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct AdapterIdentity {
95 pub name: String,
96 pub vendor: u32,
97 pub device: u32,
98 pub device_type: String,
99 pub backend: String,
100 pub driver: String,
101 pub driver_info: String,
102}
103
104impl AdapterIdentity {
105 pub fn cache_key(
115 &self,
116 semantic_hash: &str,
117 source_hash: &str,
118 schedule: Schedule,
119 tolerance: (f32, f32),
120 ) -> Result<String, ForgeError> {
121 let bytes = serde_json::to_vec(&(
122 FORGE_SCHEMA_VERSION,
123 self,
124 semantic_hash,
125 source_hash,
126 schedule,
127 env!("CARGO_PKG_VERSION"),
128 WGPU_API_VERSION,
129 NAGA_API_VERSION,
130 CUDARC_API_VERSION,
131 tolerance.0.to_bits(),
132 tolerance.1.to_bits(),
133 ))?;
134 Ok(blake3::hash(&bytes).to_hex().to_string())
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
139pub struct CertificationManifest {
140 pub forge_schema_version: u32,
141 pub crate_version: String,
142 pub wgpu_api_version: String,
143 pub naga_api_version: String,
144 pub kernel_id: String,
145 pub semantic_hash: String,
146 pub source_hash: String,
147 pub schedule: Schedule,
148 pub validation_level: ValidationLevel,
149 pub validation: ValidationReport,
150 pub adapter: Option<AdapterIdentity>,
151 pub oracle: Option<ComparisonReport>,
152 pub timing: Option<TimingSummary>,
153 pub cache_key: Option<String>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub vector_seed: Option<u64>,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub vector_hash: Option<String>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub certified_at_unix: Option<u64>,
169}
170
171impl CertificationManifest {
172 pub fn naga_only(generated: &GeneratedShader, validation: ValidationReport) -> Self {
173 Self {
174 forge_schema_version: FORGE_SCHEMA_VERSION,
175 crate_version: env!("CARGO_PKG_VERSION").to_string(),
176 wgpu_api_version: WGPU_API_VERSION.to_string(),
177 naga_api_version: NAGA_API_VERSION.to_string(),
178 kernel_id: generated.kernel_id.clone(),
179 semantic_hash: generated.semantic_hash.clone(),
180 source_hash: generated.source_hash.clone(),
181 schedule: generated.schedule,
182 validation_level: ValidationLevel::NagaValidated,
183 validation,
184 adapter: None,
185 oracle: None,
186 timing: None,
187 cache_key: None,
188 vector_seed: None,
189 vector_hash: None,
190 certified_at_unix: None,
191 }
192 }
193
194 pub fn to_pretty_json(&self) -> Result<String, ForgeError> {
195 Ok(serde_json::to_string_pretty(self)?)
196 }
197}
198
199#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
200pub struct TuningManifest {
201 pub forge_schema_version: u32,
202 pub crate_version: String,
203 pub wgpu_api_version: String,
204 pub naga_api_version: String,
205 pub kernel_id: String,
206 pub semantic_hash: String,
207 pub winning_source_hash: String,
208 pub adapter: AdapterIdentity,
209 pub cache_key: String,
210 pub result: TuningResult,
211}
212
213impl TuningManifest {
214 pub fn new(
215 generated_winner: &GeneratedShader,
216 adapter: AdapterIdentity,
217 result: TuningResult,
218 ) -> Result<Self, ForgeError> {
219 let cache_key = adapter.cache_key(
223 &generated_winner.semantic_hash,
224 &generated_winner.source_hash,
225 generated_winner.schedule,
226 (1.0e-6, 1.0e-5),
227 )?;
228 Ok(Self {
229 forge_schema_version: FORGE_SCHEMA_VERSION,
230 crate_version: env!("CARGO_PKG_VERSION").to_string(),
231 wgpu_api_version: WGPU_API_VERSION.to_string(),
232 naga_api_version: NAGA_API_VERSION.to_string(),
233 kernel_id: generated_winner.kernel_id.clone(),
234 semantic_hash: generated_winner.semantic_hash.clone(),
235 winning_source_hash: generated_winner.source_hash.clone(),
236 adapter,
237 cache_key,
238 result,
239 })
240 }
241
242 pub fn to_pretty_json(&self) -> Result<String, ForgeError> {
243 Ok(serde_json::to_string_pretty(self)?)
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 fn sample_profile() -> HardwareProfile {
252 HardwareProfile {
253 adapter: AdapterIdentity {
254 name: "Test GPU".to_string(),
255 vendor: 4318,
256 device: 1,
257 device_type: "DiscreteGpu".to_string(),
258 backend: "Vulkan".to_string(),
259 driver: "test".to_string(),
260 driver_info: "1.0".to_string(),
261 },
262 constraints: AdapterConstraints::portable(),
263 memory_class: "discrete".to_string(),
264 supports_timestamp_query: true,
265 max_compute_workgroup_storage_size: 32768,
266 max_storage_buffer_binding_size: 1 << 30,
267 min_storage_buffer_offset_alignment: 256,
268 min_uniform_buffer_offset_alignment: 256,
269 }
270 }
271
272 #[test]
273 fn topology_hash_is_stable_and_sensitive() {
274 let profile = sample_profile();
275 assert_eq!(
276 profile.topology_hash().unwrap(),
277 profile.topology_hash().unwrap()
278 );
279 let mut other = sample_profile();
280 other.memory_class = "unified".to_string();
281 assert_ne!(
282 profile.topology_hash().unwrap(),
283 other.topology_hash().unwrap()
284 );
285 }
286
287 #[test]
288 fn cache_key_changes_when_tolerance_changes() {
289 let adapter = sample_profile().adapter;
295 let strict = adapter
296 .cache_key("sem", "src", Schedule::default(), (1.0e-6, 1.0e-5))
297 .unwrap();
298 let strict_again = adapter
299 .cache_key("sem", "src", Schedule::default(), (1.0e-6, 1.0e-5))
300 .unwrap();
301 let loose = adapter
302 .cache_key("sem", "src", Schedule::default(), (1.0e-2, 1.0e-2))
303 .unwrap();
304 assert_eq!(strict, strict_again, "same inputs must yield the same key");
305 assert_ne!(strict, loose, "a coarser tolerance must change the key");
306 }
307
308 #[test]
309 fn timing_summary_is_robust_and_deterministic() {
310 let timing =
311 TimingSummary::from_samples(TimingSource::Synthetic, &[100, 50, 10_000, 75, 80])
312 .unwrap();
313 assert_eq!(timing.minimum_ns, 50);
314 assert_eq!(timing.median_ns, 80);
315 assert_eq!(timing.p95_ns, 10_000);
316 }
317}