qualia_core_db/inference/runtime/receipt/
manifest.rs1use serde::{Deserialize, Serialize};
2use sha2::{Digest, Sha256};
3use std::io::Read;
4use std::path::Path;
5
6use super::ExecutionReceipt;
7
8pub const MANIFEST_SCHEMA_VERSION: u16 = 3;
9pub const RAW_GREEDY_DECODE_POLICY: &str =
10 "greedy-argmax;temperature=0;top_k=1;top_p=1;repeat_penalty=1;repeat_last_n=0;eos=ignored";
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub struct BenchmarkManifest {
14 pub schema_version: u16,
15 pub benchmark_kind: String,
16 pub executable_commit: String,
17 pub dirty_diff_hash: String,
18 pub executable_sha256: String,
19 pub model_path: String,
20 pub model_sha256: String,
21 pub prompt_token_sha256: String,
22 #[serde(default)]
24 pub prompt_tokens: u32,
25 #[serde(default)]
27 pub context_window: u32,
28 #[serde(default)]
30 pub decode_policy: String,
31 pub quantization: String,
32 pub decode_steps_requested: u32,
33 pub decode_steps_executed: u32,
34 pub warmup_runs: u16,
35 pub measured_runs: u16,
36 pub median_tok_s: f64,
37 pub p95_ms_per_token: f64,
38 pub receipt: ExecutionReceipt,
39}
40
41impl BenchmarkManifest {
42 pub fn validate(&self) -> Result<(), &'static str> {
43 if self.schema_version != MANIFEST_SCHEMA_VERSION {
44 return Err("unsupported benchmark manifest schema");
45 }
46 if self.benchmark_kind.is_empty() {
47 return Err("benchmark kind is required");
48 }
49 if self.model_sha256.len() != 64
50 || self.prompt_token_sha256.len() != 64
51 || self.executable_sha256.len() != 64
52 {
53 return Err("SHA-256 fields must contain 64 hexadecimal characters");
54 }
55 if self.measured_runs == 0 {
56 return Err("at least one measured run is required");
57 }
58 if self.decode_steps_executed == 0 {
59 return Err("at least one decode step is required");
60 }
61 if self.prompt_tokens == 0 || self.context_window == 0 {
62 return Err("prompt token count and context window are required");
63 }
64 if self.decode_policy.is_empty() {
65 return Err("decode policy is required");
66 }
67 if self
68 .prompt_tokens
69 .saturating_add(self.decode_steps_executed)
70 > self.context_window
71 {
72 return Err("prompt plus decode budget exceeds the prepared context window");
73 }
74 if !self.median_tok_s.is_finite() || !self.p95_ms_per_token.is_finite() {
75 return Err("benchmark rates must be finite");
76 }
77 Ok(())
78 }
79}
80
81pub fn sha256_file(path: &Path) -> Result<String, std::io::Error> {
82 let mut file = std::fs::File::open(path)?;
83 let mut hasher = Sha256::new();
84 let mut buffer = [0u8; 1024 * 1024];
85 loop {
86 let read = file.read(&mut buffer)?;
87 if read == 0 {
88 break;
89 }
90 hasher.update(&buffer[..read]);
91 }
92 Ok(hex::encode(hasher.finalize()))
93}
94
95pub fn sha256_token_ids(token_ids: &[u32]) -> String {
96 let mut hasher = Sha256::new();
97 for token_id in token_ids {
98 hasher.update(token_id.to_le_bytes());
99 }
100 hex::encode(hasher.finalize())
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use crate::inference::runtime::receipt::BackendKind;
107
108 #[test]
109 fn manifest_round_trip_and_validation() {
110 let manifest = BenchmarkManifest {
111 schema_version: MANIFEST_SCHEMA_VERSION,
112 benchmark_kind: "raw-decode".into(),
113 executable_commit: "abc".into(),
114 dirty_diff_hash: "def".into(),
115 executable_sha256: "c".repeat(64),
116 model_path: "model.gguf".into(),
117 model_sha256: "a".repeat(64),
118 prompt_token_sha256: "b".repeat(64),
119 prompt_tokens: 32,
120 context_window: 1024,
121 decode_policy: RAW_GREEDY_DECODE_POLICY.into(),
122 quantization: "Q8_0".into(),
123 decode_steps_requested: 256,
124 decode_steps_executed: 256,
125 warmup_runs: 1,
126 measured_runs: 5,
127 median_tok_s: 100.0,
128 p95_ms_per_token: 11.0,
129 receipt: ExecutionReceipt::new(BackendKind::Cuda, BackendKind::Cuda, "model", "plan"),
130 };
131 manifest.validate().unwrap();
132 let json = serde_json::to_string(&manifest).unwrap();
133 let decoded: BenchmarkManifest = serde_json::from_str(&json).unwrap();
134 assert_eq!(decoded, manifest);
135 }
136
137 #[test]
138 fn manifest_rejects_undeclared_decode_policy() {
139 let mut manifest = BenchmarkManifest {
140 schema_version: MANIFEST_SCHEMA_VERSION,
141 benchmark_kind: "raw-decode".into(),
142 executable_commit: "abc".into(),
143 dirty_diff_hash: "def".into(),
144 executable_sha256: "c".repeat(64),
145 model_path: "model.gguf".into(),
146 model_sha256: "a".repeat(64),
147 prompt_token_sha256: "b".repeat(64),
148 prompt_tokens: 5,
149 context_window: 1024,
150 decode_policy: RAW_GREEDY_DECODE_POLICY.into(),
151 quantization: "Q8_0".into(),
152 decode_steps_requested: 256,
153 decode_steps_executed: 256,
154 warmup_runs: 1,
155 measured_runs: 5,
156 median_tok_s: 100.0,
157 p95_ms_per_token: 11.0,
158 receipt: ExecutionReceipt::new(BackendKind::Cuda, BackendKind::Cuda, "model", "plan"),
159 };
160 manifest.decode_policy.clear();
161 assert_eq!(manifest.validate(), Err("decode policy is required"));
162 }
163}