qualia_core_db/inference/runtime/receipt/
execution.rs1use 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 #[serde(default)]
61 pub tuning_profile: String,
62 pub stop_reason: String,
63 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}