Skip to main content

candle_graph/
capability.rs

1//! Typed coverage and capability states used across capture, analysis, and comparison.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use anyhow::{ensure, Result};
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8
9/// Domain separator for ordered gradient-manifest digests.
10pub 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/// One parameter expected in a complete gradient capture.
41#[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/// Expected runtime state of one caller-defined parameter family.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "snake_case")]
61pub enum GradientFamilyExpectation {
62    Active,
63    Inactive,
64    /// The caller's data determines whether this family is missing, attached
65    /// with an exact zero, or present. If any members are present, at least
66    /// `min_present` must be present; zero alone is not a structural failure.
67    DataConditional,
68}
69
70/// Family-level expectation applied after exact parameter-manifest validation.
71#[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/// Exact, digest-bound gradient parameter manifest and family expectations.
105#[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    /// Validate a constructed or deserialized contract before trusting it.
127    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 [&parameter.root, &parameter.key, &parameter.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    /// Coverage of activation-producing operations. `Complete` means every operation in the
218    /// measured region whose output is an activation is category-linked through tensor metadata
219    /// or logical-storage evidence.
220    #[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    /// Required application spans expected to project onto the GPU. When both semantic-class
234    /// lists are empty, all required semantic labels retain the legacy GPU-expected meaning.
235    #[serde(default, skip_serializing_if = "Vec::is_empty")]
236    pub gpu_expected_semantic_labels: Vec<String>,
237    /// Required application spans that must not appear in an Nsight GPU projection report.
238    #[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    /// The single source of the activation contract rule, shared by [`Self::validate`] and
253    /// evidence capability assessment so producer validation and packet qualification cannot
254    /// drift apart.
255    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    /// Validate relationships that must hold before a producer starts capture.
275    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}