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)]
218 pub tensors: CoverageLevel,
219 #[serde(default)]
220 pub gradients: CoverageLevel,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub gradient_contract: Option<GradientContract>,
223 pub logical_memory: CoverageLevel,
224 pub physical_memory: CoverageLevel,
225 pub device_timing: CoverageLevel,
226 #[serde(default)]
227 pub required_semantic_labels: Vec<String>,
228 #[serde(default, skip_serializing_if = "Vec::is_empty")]
231 pub gpu_expected_semantic_labels: Vec<String>,
232 #[serde(default, skip_serializing_if = "Vec::is_empty")]
234 pub cpu_only_semantic_labels: Vec<String>,
235}
236
237impl CaptureContract {
238 pub fn resolved_gpu_expected_semantic_labels(&self) -> Vec<String> {
239 if self.gpu_expected_semantic_labels.is_empty() && self.cpu_only_semantic_labels.is_empty()
240 {
241 self.required_semantic_labels.clone()
242 } else {
243 self.gpu_expected_semantic_labels.clone()
244 }
245 }
246
247 pub fn resolved_cpu_only_semantic_labels(&self) -> Vec<String> {
248 self.cpu_only_semantic_labels.clone()
249 }
250
251 pub fn validate(&self) -> Result<()> {
253 let mut semantic_labels = BTreeSet::new();
254 for label in &self.required_semantic_labels {
255 ensure!(
256 !label.trim().is_empty(),
257 "required semantic labels must not be empty"
258 );
259 ensure!(
260 semantic_labels.insert(label.as_str()),
261 "required semantic label `{label}` is declared more than once"
262 );
263 }
264 let explicitly_classified = !self.gpu_expected_semantic_labels.is_empty()
265 || !self.cpu_only_semantic_labels.is_empty();
266 if explicitly_classified {
267 let mut classified_labels = BTreeSet::new();
268 for (class, labels) in [
269 ("GPU-expected", &self.gpu_expected_semantic_labels),
270 ("CPU-only", &self.cpu_only_semantic_labels),
271 ] {
272 let mut class_labels = BTreeSet::new();
273 for label in labels {
274 ensure!(
275 !label.trim().is_empty(),
276 "{class} semantic labels must not be empty"
277 );
278 ensure!(
279 class_labels.insert(label.as_str()),
280 "{class} semantic label `{label}` is declared more than once"
281 );
282 ensure!(
283 semantic_labels.contains(label.as_str()),
284 "{class} semantic label `{label}` is not a required application label"
285 );
286 ensure!(
287 classified_labels.insert(label.as_str()),
288 "semantic label `{label}` is classified as both GPU-expected and CPU-only"
289 );
290 }
291 }
292 ensure!(
293 classified_labels == semantic_labels,
294 "GPU-expected and CPU-only semantic labels must partition all required application labels"
295 );
296 }
297 match (self.gradients, self.gradient_contract.as_ref()) {
298 (CoverageLevel::Complete, Some(contract)) => contract.validate(),
299 (CoverageLevel::Complete, None) => {
300 anyhow::bail!("complete gradient coverage requires an exact gradient contract")
301 }
302 (_, Some(_)) => anyhow::bail!(
303 "an exact gradient contract requires complete declared gradient coverage"
304 ),
305 (_, None) => Ok(()),
306 }
307 }
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
311#[serde(rename_all = "snake_case")]
312pub enum CapabilityLevel {
313 Unavailable,
314 Partial,
315 Complete,
316 Invalid,
317}
318
319#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
320pub struct CapabilityState {
321 pub level: CapabilityLevel,
322 pub source: String,
323 pub reason: String,
324}
325
326impl Default for CapabilityState {
327 fn default() -> Self {
328 Self::unavailable("capability was not recorded by this evidence schema")
329 }
330}
331
332impl CapabilityState {
333 pub fn invalid(source: impl Into<String>, reason: impl Into<String>) -> Self {
334 Self {
335 level: CapabilityLevel::Invalid,
336 source: source.into(),
337 reason: reason.into(),
338 }
339 }
340
341 pub fn unavailable(reason: impl Into<String>) -> Self {
342 Self {
343 level: CapabilityLevel::Unavailable,
344 source: "none".into(),
345 reason: reason.into(),
346 }
347 }
348
349 pub fn from_coverage(
350 coverage: CoverageLevel,
351 source: impl Into<String>,
352 reason: impl Into<String>,
353 ) -> Self {
354 Self {
355 level: match coverage {
356 CoverageLevel::None => CapabilityLevel::Unavailable,
357 CoverageLevel::Partial => CapabilityLevel::Partial,
358 CoverageLevel::Complete => CapabilityLevel::Complete,
359 },
360 source: source.into(),
361 reason: reason.into(),
362 }
363 }
364
365 pub fn is_available(&self) -> bool {
366 matches!(
367 self.level,
368 CapabilityLevel::Partial | CapabilityLevel::Complete
369 )
370 }
371
372 pub fn is_complete(&self) -> bool {
373 self.level == CapabilityLevel::Complete
374 }
375}
376
377#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
378pub struct EvidenceCapabilities {
379 pub structural_trace: CapabilityState,
380 pub outer_wall_time: CapabilityState,
381 pub nested_host_time: CapabilityState,
382 pub nested_device_time: CapabilityState,
383 pub operation_coverage: CapabilityState,
384 #[serde(default)]
385 pub tensor_coverage: CapabilityState,
386 #[serde(default)]
387 pub gradient_coverage: CapabilityState,
388 pub logical_memory_coverage: CapabilityState,
389 pub physical_memory_coverage: CapabilityState,
390 pub gpu_correlation: CapabilityState,
391 pub provenance_binding: CapabilityState,
392}
393
394#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
395#[serde(rename_all = "snake_case")]
396pub enum CapabilityKind {
397 StructuralTrace,
398 OuterWallTime,
399 NestedHostTime,
400 NestedDeviceTime,
401 Operations,
402 Tensors,
403 Gradients,
404 LogicalMemory,
405 PhysicalMemory,
406 GpuCorrelation,
407 ProvenanceBinding,
408}
409
410impl EvidenceCapabilities {
411 pub fn get(&self, kind: CapabilityKind) -> &CapabilityState {
412 match kind {
413 CapabilityKind::StructuralTrace => &self.structural_trace,
414 CapabilityKind::OuterWallTime => &self.outer_wall_time,
415 CapabilityKind::NestedHostTime => &self.nested_host_time,
416 CapabilityKind::NestedDeviceTime => &self.nested_device_time,
417 CapabilityKind::Operations => &self.operation_coverage,
418 CapabilityKind::Tensors => &self.tensor_coverage,
419 CapabilityKind::Gradients => &self.gradient_coverage,
420 CapabilityKind::LogicalMemory => &self.logical_memory_coverage,
421 CapabilityKind::PhysicalMemory => &self.physical_memory_coverage,
422 CapabilityKind::GpuCorrelation => &self.gpu_correlation,
423 CapabilityKind::ProvenanceBinding => &self.provenance_binding,
424 }
425 }
426}