Skip to main content

qualia_core_db/inference/runtime/receipt/
execution.rs

1use serde::{Deserialize, Serialize};
2
3pub const RECEIPT_SCHEMA_VERSION: u16 = 2;
4pub const COUNTER_DECODE_STEPS: u64 = 1 << 0;
5pub const COUNTER_GRAPH_LAUNCHES: u64 = 1 << 1;
6pub const COUNTER_COMPUTE_DISPATCHES: u64 = 1 << 2;
7pub const COUNTER_DEVICE_FENCES: u64 = 1 << 3;
8pub const COUNTER_HOST_TO_DEVICE_BYTES: u64 = 1 << 4;
9pub const COUNTER_DEVICE_TO_HOST_BYTES: u64 = 1 << 5;
10pub const COUNTER_FALLBACKS: u64 = 1 << 6;
11pub const COUNTER_HOT_ALLOCATIONS: u64 = 1 << 7;
12pub const COUNTER_COMPILE_CALLS: u64 = 1 << 8;
13pub const COUNTER_IMMUTABLE_UPLOAD_BYTES: u64 = 1 << 9;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "kebab-case")]
17pub enum BackendKind {
18    Cpu,
19    WgpuDx12,
20    WgpuVulkan,
21    WgpuMetal,
22    Cuda,
23    Metal,
24    Unknown,
25}
26
27#[repr(C)]
28#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
29pub struct ExecutionCounters {
30    pub decode_steps: u64,
31    pub graph_launches: u64,
32    pub compute_dispatches: u64,
33    pub device_fences: u64,
34    pub host_to_device_bytes: u64,
35    pub device_to_host_bytes: u64,
36    pub fallback_count: u64,
37    pub hot_path_allocations: u64,
38    pub compile_calls: u64,
39    pub immutable_upload_bytes: u64,
40}
41
42#[repr(C)]
43#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
44pub struct ArtifactCleanupCounters {
45    pub temp_created_bytes: u64,
46    pub temp_removed_bytes: u64,
47    pub temp_retained_bytes: u64,
48    pub temp_cleanup_failures: u64,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct ExecutionReceipt {
53    pub schema_version: u16,
54    pub requested_backend: BackendKind,
55    pub executed_backend: BackendKind,
56    pub model_instance_id: String,
57    pub prepared_plan_id: String,
58    pub graph_hash: String,
59    /// Cold backend tuning record. Empty for runtimes without an explicit tuning profile.
60    #[serde(default)]
61    pub tuning_profile: String,
62    pub stop_reason: String,
63    /// Bit set means the corresponding [`ExecutionCounters`] value was measured or proven.
64    /// An unset bit distinguishes "unknown" from a measured zero.
65    pub counter_coverage: u64,
66    pub counters: ExecutionCounters,
67    pub artifacts: ArtifactCleanupCounters,
68}
69
70impl ExecutionReceipt {
71    pub fn new(
72        requested_backend: BackendKind,
73        executed_backend: BackendKind,
74        model_instance_id: impl Into<String>,
75        prepared_plan_id: impl Into<String>,
76    ) -> Self {
77        Self {
78            schema_version: RECEIPT_SCHEMA_VERSION,
79            requested_backend,
80            executed_backend,
81            model_instance_id: model_instance_id.into(),
82            prepared_plan_id: prepared_plan_id.into(),
83            graph_hash: String::new(),
84            tuning_profile: String::new(),
85            stop_reason: String::new(),
86            counter_coverage: 0,
87            counters: ExecutionCounters::default(),
88            artifacts: ArtifactCleanupCounters::default(),
89        }
90    }
91
92    pub fn backend_matches_request(&self) -> bool {
93        self.requested_backend == self.executed_backend && self.counters.fallback_count == 0
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn receipt_round_trips_and_rejects_fallback_as_match() {
103        let mut receipt =
104            ExecutionReceipt::new(BackendKind::Cuda, BackendKind::Cuda, "model-1", "plan-1");
105        receipt.counters.decode_steps = 256;
106        let json = serde_json::to_string(&receipt).unwrap();
107        let decoded: ExecutionReceipt = serde_json::from_str(&json).unwrap();
108        assert_eq!(decoded, receipt);
109        assert!(decoded.backend_matches_request());
110
111        receipt.counters.fallback_count = 1;
112        assert!(!receipt.backend_matches_request());
113    }
114
115    #[test]
116    fn schema_one_receipt_without_tuning_profile_remains_readable() {
117        let receipt =
118            ExecutionReceipt::new(BackendKind::Cuda, BackendKind::Cuda, "model-1", "plan-1");
119        let mut value = serde_json::to_value(receipt).unwrap();
120        value["schema_version"] = serde_json::json!(1);
121        value
122            .as_object_mut()
123            .unwrap()
124            .remove("tuning_profile")
125            .unwrap();
126
127        let decoded: ExecutionReceipt = serde_json::from_value(value).unwrap();
128        assert_eq!(decoded.schema_version, 1);
129        assert!(decoded.tuning_profile.is_empty());
130    }
131}