1use std::collections::{BTreeMap, BTreeSet};
4
5use anyhow::{ensure, Result};
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8
9pub const GRADIENT_MANIFEST_SCHEMA: &str = "candle-graph/gradient-manifest/1";
11
12#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum CoverageLevel {
15 #[default]
16 None,
17 Partial,
18 Complete,
19}
20
21impl CoverageLevel {
22 pub fn with_observations(self, count: usize) -> Self {
23 if count == 0 {
24 self
25 } else {
26 self.max(Self::Partial)
27 }
28 }
29}
30
31#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum MeasurementScope {
34 #[default]
35 Unknown,
36 ProfiledWork,
37 ProductionEquivalent,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct ExpectedGradient {
43 pub root: String,
44 pub key: String,
45 pub family: String,
46}
47
48impl ExpectedGradient {
49 pub fn new(root: impl Into<String>, key: impl Into<String>, family: impl Into<String>) -> Self {
50 Self {
51 root: root.into(),
52 key: key.into(),
53 family: family.into(),
54 }
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "snake_case")]
61pub enum GradientFamilyExpectation {
62 Active,
63 Inactive,
64 DataConditional,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72pub struct GradientFamilyContract {
73 pub family: String,
74 pub expectation: GradientFamilyExpectation,
75 pub min_present: usize,
76}
77
78impl GradientFamilyContract {
79 pub fn active(family: impl Into<String>, min_present: usize) -> Self {
80 Self {
81 family: family.into(),
82 expectation: GradientFamilyExpectation::Active,
83 min_present,
84 }
85 }
86
87 pub fn inactive(family: impl Into<String>) -> Self {
88 Self {
89 family: family.into(),
90 expectation: GradientFamilyExpectation::Inactive,
91 min_present: 0,
92 }
93 }
94
95 pub fn data_conditional(family: impl Into<String>, min_present: usize) -> Self {
96 Self {
97 family: family.into(),
98 expectation: GradientFamilyExpectation::DataConditional,
99 min_present,
100 }
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106pub struct GradientContract {
107 pub manifest_sha256: String,
108 pub expected: Vec<ExpectedGradient>,
109 pub families: Vec<GradientFamilyContract>,
110}
111
112impl GradientContract {
113 pub fn new(
114 expected: Vec<ExpectedGradient>,
115 families: Vec<GradientFamilyContract>,
116 ) -> Result<Self> {
117 let contract = Self {
118 manifest_sha256: gradient_manifest_sha256(&expected),
119 expected,
120 families,
121 };
122 contract.validate()?;
123 Ok(contract)
124 }
125
126 pub fn validate(&self) -> Result<()> {
128 ensure!(
129 !self.expected.is_empty(),
130 "gradient manifest must not be empty"
131 );
132 ensure!(
133 !self.families.is_empty(),
134 "gradient families must not be empty"
135 );
136 ensure!(
137 self.manifest_sha256 == gradient_manifest_sha256(&self.expected),
138 "gradient manifest SHA-256 does not match its ordered parameter manifest"
139 );
140
141 let mut keys = BTreeSet::new();
142 let mut members = BTreeMap::<&str, usize>::new();
143 for parameter in &self.expected {
144 ensure!(
145 !parameter.root.trim().is_empty()
146 && !parameter.key.trim().is_empty()
147 && !parameter.family.trim().is_empty(),
148 "gradient manifest root, key, and family must not be empty"
149 );
150 ensure!(
151 keys.insert((parameter.root.as_str(), parameter.key.as_str())),
152 "gradient manifest declares ({:?}, {:?}) more than once",
153 parameter.root,
154 parameter.key
155 );
156 *members.entry(parameter.family.as_str()).or_default() += 1;
157 }
158
159 let mut family_names = BTreeSet::new();
160 for family in &self.families {
161 ensure!(
162 !family.family.trim().is_empty(),
163 "gradient family name must not be empty"
164 );
165 ensure!(
166 family_names.insert(family.family.as_str()),
167 "gradient family {:?} is declared more than once",
168 family.family
169 );
170 let member_count = members.get(family.family.as_str()).copied().unwrap_or(0);
171 ensure!(
172 member_count > 0,
173 "gradient family {:?} has no manifest members",
174 family.family
175 );
176 match family.expectation {
177 GradientFamilyExpectation::Inactive => ensure!(
178 family.min_present == 0,
179 "inactive gradient family {:?} must have min_present=0",
180 family.family
181 ),
182 GradientFamilyExpectation::Active
183 | GradientFamilyExpectation::DataConditional => ensure!(
184 family.min_present > 0 && family.min_present <= member_count,
185 "active or data-conditional gradient family {:?} needs min_present in 1..={member_count}",
186 family.family
187 ),
188 }
189 }
190 for family in members.keys() {
191 ensure!(
192 family_names.contains(family),
193 "gradient manifest family {family:?} has no family contract"
194 );
195 }
196 Ok(())
197 }
198}
199
200fn gradient_manifest_sha256(expected: &[ExpectedGradient]) -> String {
201 let mut digest = Sha256::new();
202 digest.update(GRADIENT_MANIFEST_SCHEMA.as_bytes());
203 digest.update([0]);
204 for parameter in expected {
205 for value in [¶meter.root, ¶meter.key, ¶meter.family] {
206 digest.update((value.len() as u64).to_le_bytes());
207 digest.update(value.as_bytes());
208 }
209 }
210 format!("sha256:{:x}", digest.finalize())
211}
212
213#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
214pub struct CaptureContract {
215 pub measurement_scope: MeasurementScope,
216 pub operations: CoverageLevel,
217 #[serde(default)]
221 pub activations: CoverageLevel,
222 #[serde(default)]
223 pub tensors: CoverageLevel,
224 #[serde(default)]
225 pub gradients: CoverageLevel,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub gradient_contract: Option<GradientContract>,
228 pub logical_memory: CoverageLevel,
229 pub physical_memory: CoverageLevel,
230 pub device_timing: CoverageLevel,
231 #[serde(default)]
232 pub required_semantic_labels: Vec<String>,
233 #[serde(default, skip_serializing_if = "Vec::is_empty")]
236 pub gpu_expected_semantic_labels: Vec<String>,
237 #[serde(default, skip_serializing_if = "Vec::is_empty")]
239 pub cpu_only_semantic_labels: Vec<String>,
240}
241
242impl CaptureContract {
243 pub fn resolved_gpu_expected_semantic_labels(&self) -> Vec<String> {
244 if self.gpu_expected_semantic_labels.is_empty() && self.cpu_only_semantic_labels.is_empty()
245 {
246 self.required_semantic_labels.clone()
247 } else {
248 self.gpu_expected_semantic_labels.clone()
249 }
250 }
251
252 pub fn activation_contract_violation(&self) -> Option<&'static str> {
256 if self.activations != CoverageLevel::Complete {
257 return None;
258 }
259 if self.operations != CoverageLevel::Complete {
260 return Some("complete activation coverage requires complete operation coverage");
261 }
262 if self.tensors == CoverageLevel::None && self.logical_memory == CoverageLevel::None {
263 return Some(
264 "complete activation coverage requires tensor metadata or logical-memory evidence",
265 );
266 }
267 None
268 }
269
270 pub fn resolved_cpu_only_semantic_labels(&self) -> Vec<String> {
271 self.cpu_only_semantic_labels.clone()
272 }
273
274 pub fn validate(&self) -> Result<()> {
276 let mut semantic_labels = BTreeSet::new();
277 for label in &self.required_semantic_labels {
278 ensure!(
279 !label.trim().is_empty(),
280 "required semantic labels must not be empty"
281 );
282 ensure!(
283 semantic_labels.insert(label.as_str()),
284 "required semantic label `{label}` is declared more than once"
285 );
286 }
287 let explicitly_classified = !self.gpu_expected_semantic_labels.is_empty()
288 || !self.cpu_only_semantic_labels.is_empty();
289 if explicitly_classified {
290 let mut classified_labels = BTreeSet::new();
291 for (class, labels) in [
292 ("GPU-expected", &self.gpu_expected_semantic_labels),
293 ("CPU-only", &self.cpu_only_semantic_labels),
294 ] {
295 let mut class_labels = BTreeSet::new();
296 for label in labels {
297 ensure!(
298 !label.trim().is_empty(),
299 "{class} semantic labels must not be empty"
300 );
301 ensure!(
302 class_labels.insert(label.as_str()),
303 "{class} semantic label `{label}` is declared more than once"
304 );
305 ensure!(
306 semantic_labels.contains(label.as_str()),
307 "{class} semantic label `{label}` is not a required application label"
308 );
309 ensure!(
310 classified_labels.insert(label.as_str()),
311 "semantic label `{label}` is classified as both GPU-expected and CPU-only"
312 );
313 }
314 }
315 ensure!(
316 classified_labels == semantic_labels,
317 "GPU-expected and CPU-only semantic labels must partition all required application labels"
318 );
319 }
320 if let Some(reason) = self.activation_contract_violation() {
321 anyhow::bail!(reason);
322 }
323 match (self.gradients, self.gradient_contract.as_ref()) {
324 (CoverageLevel::Complete, Some(contract)) => contract.validate(),
325 (CoverageLevel::Complete, None) => {
326 anyhow::bail!("complete gradient coverage requires an exact gradient contract")
327 }
328 (_, Some(_)) => anyhow::bail!(
329 "an exact gradient contract requires complete declared gradient coverage"
330 ),
331 (_, None) => Ok(()),
332 }
333 }
334}
335
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
337#[serde(rename_all = "snake_case")]
338pub enum CapabilityLevel {
339 Unavailable,
340 Partial,
341 Complete,
342 Invalid,
343}
344
345#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
346pub struct CapabilityState {
347 pub level: CapabilityLevel,
348 pub source: String,
349 pub reason: String,
350}
351
352impl Default for CapabilityState {
353 fn default() -> Self {
354 Self::unavailable("capability was not recorded by this evidence schema")
355 }
356}
357
358impl CapabilityState {
359 pub fn invalid(source: impl Into<String>, reason: impl Into<String>) -> Self {
360 Self {
361 level: CapabilityLevel::Invalid,
362 source: source.into(),
363 reason: reason.into(),
364 }
365 }
366
367 pub fn unavailable(reason: impl Into<String>) -> Self {
368 Self {
369 level: CapabilityLevel::Unavailable,
370 source: "none".into(),
371 reason: reason.into(),
372 }
373 }
374
375 pub fn from_coverage(
376 coverage: CoverageLevel,
377 source: impl Into<String>,
378 reason: impl Into<String>,
379 ) -> Self {
380 Self {
381 level: match coverage {
382 CoverageLevel::None => CapabilityLevel::Unavailable,
383 CoverageLevel::Partial => CapabilityLevel::Partial,
384 CoverageLevel::Complete => CapabilityLevel::Complete,
385 },
386 source: source.into(),
387 reason: reason.into(),
388 }
389 }
390
391 pub fn is_available(&self) -> bool {
392 matches!(
393 self.level,
394 CapabilityLevel::Partial | CapabilityLevel::Complete
395 )
396 }
397
398 pub fn is_complete(&self) -> bool {
399 self.level == CapabilityLevel::Complete
400 }
401}
402
403#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
404pub struct EvidenceCapabilities {
405 pub structural_trace: CapabilityState,
406 pub outer_wall_time: CapabilityState,
407 pub nested_host_time: CapabilityState,
408 pub nested_device_time: CapabilityState,
409 pub operation_coverage: CapabilityState,
410 #[serde(default)]
411 pub activation_coverage: CapabilityState,
412 #[serde(default)]
413 pub tensor_coverage: CapabilityState,
414 #[serde(default)]
415 pub gradient_coverage: CapabilityState,
416 pub logical_memory_coverage: CapabilityState,
417 pub physical_memory_coverage: CapabilityState,
418 pub gpu_correlation: CapabilityState,
419 pub provenance_binding: CapabilityState,
420}
421
422#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
423#[serde(rename_all = "snake_case")]
424pub enum CapabilityKind {
425 StructuralTrace,
426 OuterWallTime,
427 NestedHostTime,
428 NestedDeviceTime,
429 Operations,
430 Activations,
431 Tensors,
432 Gradients,
433 LogicalMemory,
434 PhysicalMemory,
435 GpuCorrelation,
436 ProvenanceBinding,
437}
438
439impl EvidenceCapabilities {
440 pub fn get(&self, kind: CapabilityKind) -> &CapabilityState {
441 match kind {
442 CapabilityKind::StructuralTrace => &self.structural_trace,
443 CapabilityKind::OuterWallTime => &self.outer_wall_time,
444 CapabilityKind::NestedHostTime => &self.nested_host_time,
445 CapabilityKind::NestedDeviceTime => &self.nested_device_time,
446 CapabilityKind::Operations => &self.operation_coverage,
447 CapabilityKind::Activations => &self.activation_coverage,
448 CapabilityKind::Tensors => &self.tensor_coverage,
449 CapabilityKind::Gradients => &self.gradient_coverage,
450 CapabilityKind::LogicalMemory => &self.logical_memory_coverage,
451 CapabilityKind::PhysicalMemory => &self.physical_memory_coverage,
452 CapabilityKind::GpuCorrelation => &self.gpu_correlation,
453 CapabilityKind::ProvenanceBinding => &self.provenance_binding,
454 }
455 }
456}