Skip to main content

qualia_core_db/inference/runtime/receipt/
manifest.rs

1use 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    /// Number of tokens produced by the declared prompt before decode begins.
23    #[serde(default)]
24    pub prompt_tokens: u32,
25    /// Prepared runtime context capacity used for this run.
26    #[serde(default)]
27    pub context_window: u32,
28    /// Complete sampling/termination contract. Comparator runs must declare an identical policy.
29    #[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}