Skip to main content

cortiq_engine/
prism.rs

1//! Prism/Bonsai signed FWHT activation boundary.
2//!
3//! Prism matrices carry an explicit signed-Hadamard activation boundary: the
4//! source checkpoint folds `D·H` into every forward matrix and stores the
5//! embedding twin in the inverse basis. Keep the transform in one small
6//! module so CPU fallback, tests, and GPU paths share one typed header.
7
8use cortiq_core::CmfModel;
9use cortiq_core::hadamard::{signed_fwht_forward, signed_fwht_inverse};
10use std::collections::HashSet;
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::sync::{Arc, Mutex, OnceLock};
13
14/// A validated, immutable view of the Prism header used by the hot per-op
15/// path.  Header validation is intentionally done before this object enters
16/// the cache; the cache therefore removes repeated manifest scans without
17/// weakening the fail-closed contract of the loader/runtime.
18struct ValidatedDescriptor {
19    signs: Vec<f32>,
20    widths: Vec<usize>,
21    block_size: usize,
22    activation_f16: bool,
23    forward_names: HashSet<String>,
24    inverse_names: HashSet<String>,
25    affine_names: HashSet<String>,
26}
27
28impl ValidatedDescriptor {
29    fn from_model(model: &CmfModel) -> Self {
30        let cfg = model
31            .header
32            .arch
33            .prism_hadamard
34            .as_ref()
35            .expect("Prism tensor without prism_hadamard metadata");
36        let t0 = std::time::Instant::now();
37        cfg.validate()
38            .expect("invalid prism_hadamard metadata in CMF header");
39        PROFILE_VALIDATIONS.fetch_add(1, Ordering::Relaxed);
40        PROFILE_VALIDATE_NS.fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
41
42        // Keep both the manifest spelling and its canonical spelling.  This
43        // is exactly the old `manifest_name_matches` rule, represented once
44        // rather than rescanned for every projection and token.
45        let mut forward_names = HashSet::with_capacity(cfg.forward_weight_names.len() * 2);
46        for n in &cfg.forward_weight_names {
47            forward_names.insert(n.clone());
48            if let Some(canonical) = n.strip_prefix("language_model.") {
49                forward_names.insert(canonical.to_string());
50            }
51        }
52        let mut inverse_names = HashSet::with_capacity(cfg.inverse_weight_names.len() * 2);
53        for n in &cfg.inverse_weight_names {
54            inverse_names.insert(n.clone());
55            if let Some(canonical) = n.strip_prefix("language_model.") {
56                inverse_names.insert(canonical.to_string());
57            }
58        }
59        let affine_names = cfg
60            .affine
61            .as_ref()
62            .map(|affine| affine.target_names.iter().cloned().collect())
63            .unwrap_or_default();
64
65        Self {
66            signs: cfg.signs.clone(),
67            widths: cfg.widths.clone(),
68            block_size: cfg.block_size,
69            activation_f16: cfg.activation_f16,
70            forward_names,
71            inverse_names,
72            affine_names,
73        }
74    }
75
76    fn signs_for_width(&self, width: usize) -> Option<&[f32]> {
77        let mut off = 0usize;
78        for &w in &self.widths {
79            if w == width {
80                return self.signs.get(off..off + w);
81            }
82            off += w;
83        }
84        None
85    }
86}
87
88static DESCRIPTORS: OnceLock<Mutex<std::collections::HashMap<u64, Arc<ValidatedDescriptor>>>> =
89    OnceLock::new();
90
91// Most Prism calls occur on a small fixed worker pool.  Avoid taking the
92// process-wide mutex after the first lookup on each worker while retaining a
93// UID key (rather than an mmap address) for reload correctness.
94thread_local! {
95    static LOCAL_DESCRIPTOR: std::cell::RefCell<Option<(u64, Arc<ValidatedDescriptor>)>> =
96        const { std::cell::RefCell::new(None) };
97}
98
99static PROFILE_VALIDATIONS: AtomicU64 = AtomicU64::new(0);
100static PROFILE_VALIDATE_NS: AtomicU64 = AtomicU64::new(0);
101static PROFILE_LOOKUPS: AtomicU64 = AtomicU64::new(0);
102static PERF_FORWARD_CALLS: AtomicU64 = AtomicU64::new(0);
103static PERF_FORWARD_NS: AtomicU64 = AtomicU64::new(0);
104static PERF_INVERSE_CALLS: AtomicU64 = AtomicU64::new(0);
105static PERF_INVERSE_NS: AtomicU64 = AtomicU64::new(0);
106
107fn perf_enabled() -> bool {
108    static ON: OnceLock<bool> = OnceLock::new();
109    *ON.get_or_init(|| std::env::var("CMF_PERF_PROFILE").as_deref() == Ok("1"))
110}
111
112fn descriptors() -> &'static Mutex<std::collections::HashMap<u64, Arc<ValidatedDescriptor>>> {
113    DESCRIPTORS.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
114}
115
116fn descriptor_for(model: &CmfModel) -> Arc<ValidatedDescriptor> {
117    PROFILE_LOOKUPS.fetch_add(1, Ordering::Relaxed);
118    let uid = model.uid();
119    if let Some(hit) = LOCAL_DESCRIPTOR.with(|slot| {
120        slot.borrow()
121            .as_ref()
122            .filter(|(cached_uid, _)| *cached_uid == uid)
123            .map(|(_, desc)| Arc::clone(desc))
124    }) {
125        return hit;
126    }
127
128    // `CMF_PRISM_CACHE=0` is an audit switch: it preserves the strict
129    // validation and old behavior while making repeated validation measurable
130    // against the cached production path.
131    let use_cache = std::env::var("CMF_PRISM_CACHE").map_or(true, |v| v != "0");
132    let desc = if !use_cache {
133        Arc::new(ValidatedDescriptor::from_model(model))
134    } else {
135        let mut all = descriptors()
136            .lock()
137            .expect("Prism descriptor cache poisoned");
138        Arc::clone(
139            all.entry(uid)
140                .or_insert_with(|| Arc::new(ValidatedDescriptor::from_model(model))),
141        )
142    };
143    LOCAL_DESCRIPTOR.with(|slot| *slot.borrow_mut() = Some((uid, Arc::clone(&desc))));
144    desc
145}
146
147/// Print cache/validation counters for a bounded benchmark.  The report is
148/// opt-in so normal CLI output and production hot paths remain unchanged.
149pub fn profile_report() {
150    if std::env::var("CMF_PRISM_PROFILE").is_err() {
151        return;
152    }
153    eprintln!(
154        "[prism-profile] descriptor lookups={} validations={} validation_ms={:.3}",
155        PROFILE_LOOKUPS.load(Ordering::Relaxed),
156        PROFILE_VALIDATIONS.load(Ordering::Relaxed),
157        PROFILE_VALIDATE_NS.load(Ordering::Relaxed) as f64 / 1e6,
158    );
159}
160
161/// Aggregate the CPU-side signed-Hadamard work for one bounded benchmark.
162/// This is deliberately opt-in and does not sample or print in the hot path.
163pub fn perf_report() {
164    if !perf_enabled() {
165        return;
166    }
167    let fc = PERF_FORWARD_CALLS.load(Ordering::Relaxed);
168    let ic = PERF_INVERSE_CALLS.load(Ordering::Relaxed);
169    eprintln!(
170        "[perf-prism] forward_calls={} forward_ms={:.3} forward_ms_per_call={:.3} inverse_calls={} inverse_ms={:.3} inverse_ms_per_call={:.3}",
171        fc,
172        PERF_FORWARD_NS.load(Ordering::Relaxed) as f64 / 1e6,
173        PERF_FORWARD_NS.load(Ordering::Relaxed) as f64 / 1e6 / fc.max(1) as f64,
174        ic,
175        PERF_INVERSE_NS.load(Ordering::Relaxed) as f64 / 1e6,
176        PERF_INVERSE_NS.load(Ordering::Relaxed) as f64 / 1e6 / ic.max(1) as f64,
177    );
178}
179
180fn descriptor(model: &CmfModel, width: usize) -> (Arc<ValidatedDescriptor>, usize, bool) {
181    let desc = descriptor_for(model);
182    let _signs = desc
183        .signs_for_width(width)
184        .expect("Prism width has no explicit sign vector");
185    (desc.clone(), desc.block_size, desc.activation_f16)
186}
187
188#[inline]
189fn round_f16(x: f32) -> f32 {
190    cortiq_core::quant::f16_to_f32(cortiq_core::hadamard::prism_f32_to_f16_rne(x))
191}
192
193/// Transform an activation row before a forward Prism matrix: `x·D·H`.
194/// The source contract requests an f16 boundary after the FWHT; keeping that
195/// cast here makes the CPU implementation agree with the trained MLX path.
196pub fn forward(model: &CmfModel, x: &[f32]) -> Vec<f32> {
197    let (desc, block, activation_f16) = descriptor(model, x.len());
198    let signs = desc
199        .signs_for_width(x.len())
200        .expect("Prism width has no explicit sign vector");
201    let perf_t0 = perf_enabled().then(std::time::Instant::now);
202    let mut out = x.to_vec();
203    signed_fwht_forward(&mut out, signs, block).expect("validated Prism FWHT activation");
204    if activation_f16 {
205        for v in &mut out {
206            *v = round_f16(*v);
207        }
208    }
209    if let Some(t0) = perf_t0 {
210        PERF_FORWARD_CALLS.fetch_add(1, Ordering::Relaxed);
211        PERF_FORWARD_NS.fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
212    }
213    out
214}
215
216/// Whether this named matrix is one of the source's forward-basis weights.
217/// Vision tensors and auxiliary projections are intentionally not included.
218pub fn is_forward_weight(model: &CmfModel, name: &str) -> bool {
219    model.header.arch.prism_hadamard.is_some() && descriptor_for(model).forward_names.contains(name)
220}
221
222/// True when this tensor belongs to a Prism header, regardless of whether it
223/// is a forward or inverse matrix.  GPU graph/device paths use this as a
224/// conservative safety gate because they do not carry the transform
225/// descriptor yet.
226pub fn has_contract(model: &CmfModel) -> bool {
227    model.header.arch.prism_hadamard.is_some()
228}
229
230/// Whether this exact canonical matrix carries the production affine
231/// correction.  Ordinary Prism q2tp and non-Prism dtype16 remain the raw
232/// `(c - 1.5) * s` codec; no correction is inferred from the architecture
233/// name or from the tensor dtype alone.
234pub fn is_affine_target(model: &CmfModel, name: &str) -> bool {
235    model.header.arch.prism_hadamard.is_some() && descriptor_for(model).affine_names.contains(name)
236}
237
238/// Apply the forward basis only to a manifest-listed matrix.  This is the
239/// mixed-profile counterpart of [`forward`]: ordinary q2tp may retain q4tp
240/// for attention/down projections, but those matrices are still stored in
241/// the same signed-Hadamard basis as the production Prism profile.
242pub fn forward_weight(model: &CmfModel, name: &str, x: &[f32]) -> Vec<f32> {
243    if is_forward_weight(model, name) {
244        forward(model, x)
245    } else {
246        x.to_vec()
247    }
248}
249
250/// Transform a decoded embedding row back to the caller basis: `z·H·D`.
251pub fn inverse_embedding(model: &CmfModel, x: &mut [f32]) {
252    let (desc, block, activation_f16) = descriptor(model, x.len());
253    let signs = desc
254        .signs_for_width(x.len())
255        .expect("Prism width has no explicit sign vector");
256    let perf_t0 = perf_enabled().then(std::time::Instant::now);
257    // MLX dequantizes an embedding row and enters the inverse boundary as
258    // the module dtype (float16), then performs the normalized FWHT in
259    // float32 and casts back to that dtype.  `row_f32` exposes the decoded
260    // row as f32 for the native runtime, so reproduce both dtype boundaries
261    // explicitly instead of silently using a higher-precision embedding.
262    if activation_f16 {
263        for v in x.iter_mut() {
264            *v = round_f16(*v);
265        }
266    }
267    signed_fwht_inverse(x, signs, block).expect("validated Prism FWHT embedding");
268    if activation_f16 {
269        for v in x.iter_mut() {
270            *v = round_f16(*v);
271        }
272    }
273    if let Some(t0) = perf_t0 {
274        PERF_INVERSE_CALLS.fetch_add(1, Ordering::Relaxed);
275        PERF_INVERSE_NS.fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
276    }
277}
278
279/// The source manifest names are wrapper-qualified, while CMF stores the
280/// canonical runtime name.  The embedding is the only inverse matrix in the
281/// Prism checkpoint; all other projections use `forward` activations.
282pub fn is_inverse_embedding(model: &CmfModel, name: &str) -> bool {
283    // The header gate comes FIRST: without a Prism descriptor no tensor is
284    // an inverse matrix, the embedding included. Checking the name before
285    // the header sent every non-Prism model whose embedding row is decoded
286    // through `row_f32` into `inverse_embedding`, which panics on the
287    // missing descriptor (HunYuan q4tp was the first to trip it).
288    if model.header.arch.prism_hadamard.is_none() {
289        return false;
290    }
291    if name == "model.embed_tokens.weight" {
292        return true;
293    }
294    descriptor_for(model).inverse_names.contains(name)
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    use cortiq_core::types::PrismHadamardConfig;
301
302    #[test]
303    fn oracle_roundtrip() {
304        let mut x = (0..1024).map(|i| i as f32 / 1024.0).collect::<Vec<_>>();
305        let original = x.clone();
306        let signs = (0..1024)
307            .map(|i| if i & 1 == 0 { 1.0 } else { -1.0 })
308            .collect::<Vec<_>>();
309        signed_fwht_forward(&mut x, &signs, 1024).unwrap();
310        signed_fwht_inverse(&mut x, &signs, 1024).unwrap();
311        for (a, b) in x.iter().zip(original) {
312            assert!((a - b).abs() < 2e-5);
313        }
314        let cfg = PrismHadamardConfig {
315            version: 1,
316            block_size: 1024,
317            transform: "normalized-sylvester-walsh-hadamard".into(),
318            axis: "input-last-dimension".into(),
319            sign_mode: "explicit".into(),
320            widths: vec![1024],
321            signs,
322            forward_weight_names: vec![],
323            inverse_weight_names: vec![],
324            gdn_v_grouped: true,
325            activation_f16: true,
326            affine: None,
327        };
328        cfg.validate().unwrap();
329    }
330
331    #[test]
332    fn inverse_embedding_matches_mlx_f16_boundaries() {
333        let signs = (0..1024)
334            .map(|i| if i % 3 == 0 { -1.0 } else { 1.0 })
335            .collect::<Vec<_>>();
336        let decoded = (0..1024)
337            .map(|i| (i as f32 * 0.001_234_567).sin())
338            .collect::<Vec<_>>();
339        // This is the exact boundary sequence in runtime.py: decoded row
340        // → f16, inverse normalized FWHT in f32, then → f16.
341        let mut expected = decoded.iter().map(|&v| round_f16(v)).collect::<Vec<_>>();
342        signed_fwht_inverse(&mut expected, &signs, 1024).unwrap();
343        for v in &mut expected {
344            *v = round_f16(*v);
345        }
346        let mut actual = decoded;
347        for v in &mut actual {
348            *v = round_f16(*v);
349        }
350        signed_fwht_inverse(&mut actual, &signs, 1024).unwrap();
351        for v in &mut actual {
352            *v = round_f16(*v);
353        }
354        assert_eq!(actual, expected);
355    }
356}