diff --git a/Cargo.lock b/Cargo.lock index 1eb258ba6..f1ca92ac5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,15 @@ dependencies = [ "ghash", ] +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -119,6 +128,26 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.13.1", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.119", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -174,7 +203,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", - "shlex", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", ] [[package]] @@ -202,6 +240,26 @@ dependencies = [ name = "citation_edge" version = "0.2.0" +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "cmov" version = "0.5.4" @@ -597,6 +655,12 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + [[package]] name = "hashbrown" version = "0.15.5" @@ -820,6 +884,15 @@ dependencies = [ "libc", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -879,6 +952,16 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libredox" version = "0.1.20" @@ -950,6 +1033,12 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -970,6 +1059,27 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "mlx-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e3bc3880111918b2d5018f845d48fd995f9901f16efc81d1fcfd2f4210b8219" +dependencies = [ + "bindgen", + "cc", + "cmake", +] + +[[package]] +name = "mlx_native_receipt" +version = "0.2.0" +dependencies = [ + "mlx-sys", + "serde", + "serde_json", + "sha2", +] + [[package]] name = "modality_source" version = "0.2.0" @@ -990,6 +1100,16 @@ dependencies = [ name = "network_analysis" version = "0.2.0" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1143,6 +1263,16 @@ dependencies = [ "temporal_core", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -1231,6 +1361,35 @@ dependencies = [ "bitflags 2.13.1", ] +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + [[package]] name = "relation_absence" version = "0.2.0" @@ -1277,6 +1436,12 @@ version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustls" version = "0.23.43" @@ -1391,6 +1556,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "shlex" version = "2.0.1" diff --git a/Cargo.toml b/Cargo.toml index df9aae9d5..68a0a9405 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ members = [ "crates/topic_measurement", "crates/psychometric_core", "crates/orchestrator_live", + "crates/mlx_native_receipt", ] default-members = [ "crates/evidence_core", @@ -117,6 +118,7 @@ default-members = [ "crates/topic_measurement", "crates/psychometric_core", "crates/orchestrator_live", + "crates/mlx_native_receipt", ] [workspace.package] diff --git a/crates/mlx_native_receipt/Cargo.toml b/crates/mlx_native_receipt/Cargo.toml new file mode 100644 index 000000000..ae6f248a3 --- /dev/null +++ b/crates/mlx_native_receipt/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "mlx_native_receipt" +description = "macOS-native MLX execution receipt probe for TEPP." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true +publish = false + +[dependencies] +serde.workspace = true +serde_json.workspace = true +sha2.workspace = true + +[target.'cfg(target_os = "macos")'.dependencies] +# Use the C contract directly because mlx-rs 0.25.3 unconditionally enables +# mlx-sys's default Metal feature even when its own default features are off. +mlx-sys = { version = "=0.2.0", default-features = false, features = ["accelerate"] } + +[lints.rust] +# Unsafe is isolated to the audited opaque-handle C FFI function; callers and +# every mathematical operation remain safe Rust. +unsafe_code = "allow" +unsafe_op_in_unsafe_fn = "deny" + +[lints.clippy] +all = "deny" +pedantic = "deny" diff --git a/crates/mlx_native_receipt/src/main.rs b/crates/mlx_native_receipt/src/main.rs new file mode 100644 index 000000000..68bb3ea83 --- /dev/null +++ b/crates/mlx_native_receipt/src/main.rs @@ -0,0 +1,139 @@ +#![deny(missing_docs)] +//! macOS-native MLX backend receipt probe. +//! +//! This binary executes an identified matrix product on MLX, compares it with +//! the Rust CPU reference, and emits a receipt only for the device that +//! actually executed. It is not an Event Lineage estimator receipt. + +use serde::Serialize; +use sha2::{Digest, Sha256}; + +#[derive(Serialize)] +struct ProbeReceipt { + schema_version: &'static str, + backend_code: &'static str, + execution_environment_code: &'static str, + objective_sha256: String, + output_sha256: String, + observed_maximum_difference: f64, +} + +fn digest(values: &[f32]) -> String { + let mut hasher = Sha256::new(); + for value in values { + hasher.update(value.to_le_bytes()); + } + format!("{:x}", hasher.finalize()) +} + +#[cfg(target_os = "macos")] +fn run() -> Result> { + let lhs = [1.0_f32, 2.0]; + let rhs = [3.0_f32, 4.0]; + let rust = [lhs[0] * rhs[0] + lhs[1] * rhs[1]]; + let output = mlx_cpu_matmul(&lhs, &rhs)?; + let observed = output + .iter() + .zip(&rust) + .map(|(left, right)| f64::from((left - right).abs())) + .fold(0.0_f64, f64::max); + if observed.to_bits() != 0.0_f64.to_bits() { + return Err("MLX CPU parity failed".into()); + } + let objective = lhs.iter().chain(&rhs).copied().collect::>(); + let result = output; + Ok(ProbeReceipt { + schema_version: "tepp.mlx_native_probe_receipt.v1", + backend_code: "mlx_cpu_macos_native", + execution_environment_code: "macos_native", + objective_sha256: digest(&objective), + output_sha256: digest(&result), + observed_maximum_difference: observed, + }) +} + +#[cfg(target_os = "macos")] +fn mlx_cpu_matmul(lhs: &[f32; 2], rhs: &[f32; 2]) -> Result, String> { + use mlx_sys::{ + mlx_array_data_float32, mlx_array_eval, mlx_array_free, mlx_array_new, mlx_array_new_data, + mlx_device_free, mlx_device_new_type, mlx_device_type__MLX_CPU, mlx_dtype__MLX_FLOAT32, + mlx_matmul, mlx_set_default_device, mlx_stream_free, mlx_stream_new_device, + }; + use std::ffi::c_void; + + // SAFETY: every opaque MLX handle is created by the matching constructor, + // checked for a successful status before dereference, and freed exactly + // once after the evaluated scalar is copied into Rust-owned memory. + unsafe { + let device = mlx_device_new_type(mlx_device_type__MLX_CPU, 0); + if mlx_set_default_device(device) != 0 { + let _ = mlx_device_free(device); + return Err("failed to select the MLX CPU device".into()); + } + let stream = mlx_stream_new_device(device); + let left = mlx_array_new_data( + lhs.as_ptr().cast::(), + [1_i32, 2].as_ptr(), + 2, + mlx_dtype__MLX_FLOAT32, + ); + let right = mlx_array_new_data( + rhs.as_ptr().cast::(), + [2_i32, 1].as_ptr(), + 2, + mlx_dtype__MLX_FLOAT32, + ); + let mut result = mlx_array_new(); + let operation_status = mlx_matmul(&raw mut result, left, right, stream); + let evaluation_status = if operation_status == 0 { + mlx_array_eval(result) + } else { + operation_status + }; + let output = if evaluation_status == 0 { + let pointer = mlx_array_data_float32(result); + if pointer.is_null() { + None + } else { + Some(vec![*pointer]) + } + } else { + None + }; + let _ = mlx_array_free(result); + let _ = mlx_array_free(right); + let _ = mlx_array_free(left); + let _ = mlx_stream_free(stream); + let _ = mlx_device_free(device); + output.ok_or_else(|| "MLX CPU matrix product failed".into()) + } +} + +#[cfg(not(target_os = "macos"))] +fn run() -> Result> { + Err("macOS-native MLX receipt unavailable on this host".into()) +} + +fn main() -> Result<(), Box> { + let receipt = run()?; + println!("{}", serde_json::to_string(&receipt)?); + Ok(()) +} + +#[cfg(all(test, target_os = "macos"))] +mod tests { + use super::run; + + #[test] + fn emits_only_an_exact_macos_native_mlx_cpu_receipt() { + let receipt = run().expect("installed MLX CPU must execute the probe"); + assert_eq!(receipt.backend_code, "mlx_cpu_macos_native"); + assert_eq!(receipt.execution_environment_code, "macos_native"); + assert_eq!( + receipt.observed_maximum_difference.to_bits(), + 0.0_f64.to_bits() + ); + assert_eq!(receipt.objective_sha256.len(), 64); + assert_eq!(receipt.output_sha256.len(), 64); + } +} diff --git a/docs/adr/0025-macos-native-rust-mlx-metal-boundary.md b/docs/adr/0025-macos-native-rust-mlx-metal-boundary.md index 14c7ebac3..b6e045541 100644 --- a/docs/adr/0025-macos-native-rust-mlx-metal-boundary.md +++ b/docs/adr/0025-macos-native-rust-mlx-metal-boundary.md @@ -25,7 +25,9 @@ parameter/draw digests, execution identity, and method-derived numerical parity evidence against the Rust CPU f64 reference. `mlx_metal_macos_native` is valid only when native Metal execution occurred. Linux and container CI may record only `rust_cpu`, `mlx_cpu`, `mlx_cuda`, or `rust_opencl` when that backend -actually executes; `mlx_opencl` is not a backend. Missing authentication, backend receipt, or +actually executes. A macOS-native MLX CPU execution records +`mlx_cpu_macos_native`; it is never relabeled as Metal. `mlx_opencl` is not a +backend. Missing authentication, backend receipt, or parity evidence fails closed. ## Cross-repository contract @@ -68,6 +70,8 @@ fallback as Metal. ## Verification, testing and acceptance +- the native MLX CPU probe executes a known objective, compares its output with + the Rust CPU reference, and emits `mlx_cpu_macos_native` only on macOS; - macOS hardware E2E proves native MLX Metal execution and receipt binding; - container E2E proves authenticated host-gateway/Unix-socket access and that Linux cannot emit a Metal receipt; diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 8c58d47d2..828574708 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -20,6 +20,9 @@ Metal behind authenticated local transport, exact backend receipts, Linux `rust_cpu`/`mlx_cpu`/`mlx_cuda`/`rust_opencl` portability, and fail-closed parity. The native service and hardware E2E remain a release gap. +- `mlx_native_receipt` provides a macOS-only, Rust-owned MLX CPU execution + probe. Its receipt proves only the stated matrix objective and cannot be + reused as an Event Lineage estimator or Metal receipt. ## 2026-08-25 Event Lineage anchor contract slice