From 2be0b1882a3c29a27c7dbefa81d71abb7322c7af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 4 Oct 2026 15:41:34 +0900 Subject: [PATCH 1/2] fix(mlx): reject non-finite and incomplete native parity output --- CHANGELOG.d/mlx-finite-parity-output.md | 6 + DOCUMENTATION.md | 1 + crates/mlx_native_receipt/src/main.rs | 284 ++++++++++++++++++++-- docs/research/mlx-finite-parity-output.md | 19 ++ 4 files changed, 293 insertions(+), 17 deletions(-) create mode 100644 CHANGELOG.d/mlx-finite-parity-output.md create mode 100644 docs/research/mlx-finite-parity-output.md diff --git a/CHANGELOG.d/mlx-finite-parity-output.md b/CHANGELOG.d/mlx-finite-parity-output.md new file mode 100644 index 000000000..6a5434043 --- /dev/null +++ b/CHANGELOG.d/mlx-finite-parity-output.md @@ -0,0 +1,6 @@ +# Native MLX CPU parity receipt finite-output guard + +- Refuse NaN, either infinity, and incomplete or oversized backend output before parity reduction and output digest construction. A single NaN must not collapse through `f64::max` into an exact-zero difference receipt. +- Retain the fixed native MLX CPU matrix objective and byte-identical finite exact-result receipt. Add permanent finite/mismatch/shape/non-finite controls and continue to execute the real native backend probe. +- Exercise the private native-call boundary with device/matmul/evaluation/null-data failure controls and actual native-handle frees; production dispatch stays bound to the pinned native functions. These are injected status controls, not observed native hardware faults. +- This is a local candidate correction of ADR0025's existing probe, not a new estimator/GPU parity claim or protected-main release. Research provenance is in `docs/research/mlx-finite-parity-output.md`; current full coverage and independent review remain separately required. diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 6fa4b9683..b87a4d615 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -13,6 +13,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin | naruon modular consumer contract | [`docs/connectors/naruon-artifact-consumer.md`](docs/connectors/naruon-artifact-consumer.md) | | contextual-orchestrator interpretation port | [`docs/connectors/contextual-orchestrator-interpretation-port.md`](docs/connectors/contextual-orchestrator-interpretation-port.md) | | Orchestrator live HTTP doctoring | [`docs/research/orchestrator-live-http.md`](docs/research/orchestrator-live-http.md) | +| Native MLX finite-output parity guard | [`docs/research/mlx-finite-parity-output.md`](docs/research/mlx-finite-parity-output.md) | | UML/runtime/scientific flows | [`docs/UML.md`](docs/UML.md) | | Logical/physical ERD | [`docs/ERD.md`](docs/ERD.md) | | Security policy | [`SECURITY.md`](SECURITY.md) | diff --git a/crates/mlx_native_receipt/src/main.rs b/crates/mlx_native_receipt/src/main.rs index 01d6b7e60..7f8d47bf7 100644 --- a/crates/mlx_native_receipt/src/main.rs +++ b/crates/mlx_native_receipt/src/main.rs @@ -16,8 +16,21 @@ use mlx_native_receipt::{RECEIPT_SCHEMA_VERSION, digest}; fn run() -> Result> { let lhs = [1.0_f32, 2.0]; let rhs = [3.0_f32, 4.0]; + let output = mlx_cpu_matmul(lhs, rhs)?; + receipt_from_output(lhs, rhs, &output) +} + +/// Bind an evaluated MLX output to the exact CPU reference objective. +#[cfg(target_os = "macos")] +fn receipt_from_output( + lhs: [f32; 2], + rhs: [f32; 2], + output: &[f32], +) -> Result> { let rust = [lhs[0] * rhs[0] + lhs[1] * rhs[1]]; - let output = mlx_cpu_matmul(&lhs, &rhs)?; + if output.len() != rust.len() || output.iter().any(|value| !value.is_finite()) { + return Err("MLX CPU parity failed".into()); + } let observed = output .iter() .zip(&rust) @@ -27,23 +40,72 @@ fn run() -> Result> { return Err("MLX CPU parity failed".into()); } let objective = lhs.iter().chain(&rhs).copied().collect::>(); - let result = output; Ok(ProbeReceipt { schema_version: RECEIPT_SCHEMA_VERSION, backend_code: "mlx_cpu_macos_native", execution_environment_code: "macos_native", objective_sha256: digest(&objective), - output_sha256: digest(&result), + output_sha256: digest(output), observed_maximum_difference: observed, }) } +/// Exact native calls whose failure results control probe settlement. +#[cfg(target_os = "macos")] +#[derive(Clone, Copy)] +struct MlxProbeCalls { + /// Select the native CPU device before allocating array handles. + select_device: unsafe extern "C" fn(mlx_sys::mlx_device) -> std::ffi::c_int, + /// Submit the fixed matrix product to the native stream. + matmul: unsafe extern "C" fn( + *mut mlx_sys::mlx_array, + mlx_sys::mlx_array, + mlx_sys::mlx_array, + mlx_sys::mlx_stream, + ) -> std::ffi::c_int, + /// Materialize the result only after successful submission. + evaluate: unsafe extern "C" fn(mlx_sys::mlx_array) -> std::ffi::c_int, + /// Borrow scalar storage only after successful materialization. + data: unsafe extern "C" fn(mlx_sys::mlx_array) -> *const f32, + /// Settle one owned input or result array. + free_array: unsafe extern "C" fn(mlx_sys::mlx_array) -> std::ffi::c_int, + /// Settle the owned stream after its arrays. + free_stream: unsafe extern "C" fn(mlx_sys::mlx_stream) -> std::ffi::c_int, + /// Settle the owned device on success and refusal. + free_device: unsafe extern "C" fn(mlx_sys::mlx_device) -> std::ffi::c_int, +} + +#[cfg(target_os = "macos")] +impl MlxProbeCalls { + /// Retain the pinned MLX C ABI at the production entry point. + fn native() -> Self { + Self { + select_device: mlx_sys::mlx_set_default_device, + matmul: mlx_sys::mlx_matmul, + evaluate: mlx_sys::mlx_array_eval, + data: mlx_sys::mlx_array_data_float32, + free_array: mlx_sys::mlx_array_free, + free_stream: mlx_sys::mlx_stream_free, + free_device: mlx_sys::mlx_device_free, + } + } +} + +#[cfg(target_os = "macos")] +fn mlx_cpu_matmul(lhs: [f32; 2], rhs: [f32; 2]) -> Result, String> { + mlx_cpu_matmul_with_calls(&lhs, &rhs, MlxProbeCalls::native()) +} + +/// Evaluate and settle the same owned native handles on every status path. #[cfg(target_os = "macos")] -fn mlx_cpu_matmul(lhs: &[f32; 2], rhs: &[f32; 2]) -> Result, String> { +fn mlx_cpu_matmul_with_calls( + lhs: &[f32; 2], + rhs: &[f32; 2], + calls: MlxProbeCalls, +) -> 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, + mlx_array_new, mlx_array_new_data, mlx_device_new_type, mlx_device_type__MLX_CPU, + mlx_dtype__MLX_FLOAT32, mlx_stream_new_device, }; use std::ffi::c_void; @@ -52,8 +114,8 @@ fn mlx_cpu_matmul(lhs: &[f32; 2], rhs: &[f32; 2]) -> Result, String> { // 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); + if (calls.select_device)(device) != 0 { + let _ = (calls.free_device)(device); return Err("failed to select the MLX CPU device".into()); } let stream = mlx_stream_new_device(device); @@ -70,14 +132,14 @@ fn mlx_cpu_matmul(lhs: &[f32; 2], rhs: &[f32; 2]) -> Result, String> { mlx_dtype__MLX_FLOAT32, ); let mut result = mlx_array_new(); - let operation_status = mlx_matmul(&raw mut result, left, right, stream); + let operation_status = (calls.matmul)(&raw mut result, left, right, stream); let evaluation_status = if operation_status == 0 { - mlx_array_eval(result) + (calls.evaluate)(result) } else { operation_status }; let output = if evaluation_status == 0 { - let pointer = mlx_array_data_float32(result); + let pointer = (calls.data)(result); if pointer.is_null() { None } else { @@ -86,11 +148,11 @@ fn mlx_cpu_matmul(lhs: &[f32; 2], rhs: &[f32; 2]) -> Result, String> { } 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); + let _ = (calls.free_array)(result); + let _ = (calls.free_array)(right); + let _ = (calls.free_array)(left); + let _ = (calls.free_stream)(stream); + let _ = (calls.free_device)(device); output.ok_or_else(|| "MLX CPU matrix product failed".into()) } } @@ -130,8 +192,196 @@ fn main() -> Result<(), Box> { mod tests { use super::run; + /// The pinned native scheduler mutates shared stream vectors without a lock. + static NATIVE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + + /// Serialize native test journeys while retaining normal parallel Rust tests. + fn native_test_guard() -> std::sync::MutexGuard<'static, ()> { + NATIVE_TEST_LOCK + .lock() + .expect("native test mutex must remain healthy") + } + + #[test] + fn refuses_non_finite_backend_output_before_parity_reduction() { + for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] { + let error = super::receipt_from_output([1.0, 2.0], [3.0, 4.0], &[value]) + .expect_err("non-finite backend output must not become a parity receipt"); + assert_eq!(error.to_string(), "MLX CPU parity failed"); + } + } + + #[test] + fn refuses_incomplete_backend_output() { + for output in [&[][..], &[11.0, 11.0][..]] { + assert!(super::receipt_from_output([1.0, 2.0], [3.0, 4.0], output).is_err()); + } + } + + #[test] + fn finite_output_preserves_exact_parity_and_digest() { + let receipt = super::receipt_from_output([1.0, 2.0], [3.0, 4.0], &[11.0]) + .expect("exact finite scalar must pass"); + assert_eq!(receipt.output_sha256, mlx_native_receipt::digest(&[11.0])); + assert_eq!( + receipt.observed_maximum_difference.to_bits(), + 0.0_f64.to_bits() + ); + assert!(super::receipt_from_output([1.0, 2.0], [3.0, 4.0], &[12.0]).is_err()); + } + + #[test] + fn rejects_non_finite_receipt_before_json_publication() { + for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] { + let receipt = mlx_native_receipt::ProbeReceipt { + schema_version: mlx_native_receipt::RECEIPT_SCHEMA_VERSION, + backend_code: "mlx_cpu_macos_native", + execution_environment_code: "macos_native", + objective_sha256: "a".repeat(64), + output_sha256: "b".repeat(64), + observed_maximum_difference: value, + }; + assert_eq!( + super::emit_receipt(&receipt) + .expect_err("non-finite receipt must not publish JSON") + .to_string(), + "observed_maximum_difference must be finite" + ); + } + } + + #[test] + fn device_selection_failure_refuses_a_backend_result() { + unsafe extern "C" fn refused_device(_device: mlx_sys::mlx_device) -> std::ffi::c_int { + 1 + } + let _guard = native_test_guard(); + let mut calls = super::MlxProbeCalls::native(); + calls.select_device = refused_device; + assert_eq!( + super::mlx_cpu_matmul_with_calls(&[1.0, 2.0], &[3.0, 4.0], calls), + Err("failed to select the MLX CPU device".to_owned()) + ); + } + + thread_local! { + static COUNTS: std::cell::Cell<[usize; 5]> = const { + std::cell::Cell::new([0; 5]) + }; + } + + /// Record one native call within the current test thread. + fn count(index: usize) { + COUNTS.with(|counts| { + let mut value = counts.get(); + value[index] += 1; + counts.set(value); + }); + } + + #[test] + fn native_handles_are_settled_on_every_backend_status_path() { + unsafe extern "C" fn free_array(array: mlx_sys::mlx_array) -> std::ffi::c_int { + count(0); + // SAFETY: the runner owns this native handle and settles it once. + unsafe { mlx_sys::mlx_array_free(array) } + } + unsafe extern "C" fn free_stream(stream: mlx_sys::mlx_stream) -> std::ffi::c_int { + count(1); + // SAFETY: the runner owns this native handle and settles it once. + unsafe { mlx_sys::mlx_stream_free(stream) } + } + unsafe extern "C" fn free_device(device: mlx_sys::mlx_device) -> std::ffi::c_int { + count(2); + // SAFETY: the runner owns this native handle and settles it once. + unsafe { mlx_sys::mlx_device_free(device) } + } + unsafe extern "C" fn evaluate(array: mlx_sys::mlx_array) -> std::ffi::c_int { + count(3); + // SAFETY: this is the live native result supplied by the runner. + unsafe { mlx_sys::mlx_array_eval(array) } + } + unsafe extern "C" fn data(array: mlx_sys::mlx_array) -> *const f32 { + count(4); + // SAFETY: the runner evaluated this live array before reading it. + unsafe { mlx_sys::mlx_array_data_float32(array) } + } + unsafe extern "C" fn refused_device(_device: mlx_sys::mlx_device) -> std::ffi::c_int { + 1 + } + unsafe extern "C" fn refused_matmul( + _result: *mut mlx_sys::mlx_array, + _left: mlx_sys::mlx_array, + _right: mlx_sys::mlx_array, + _stream: mlx_sys::mlx_stream, + ) -> std::ffi::c_int { + 1 + } + unsafe extern "C" fn refused_evaluation(_array: mlx_sys::mlx_array) -> std::ffi::c_int { + count(3); + 1 + } + unsafe extern "C" fn null_data(_array: mlx_sys::mlx_array) -> *const f32 { + count(4); + std::ptr::null() + } + let _guard = native_test_guard(); + let native = super::MlxProbeCalls { + evaluate, + data, + free_array, + free_stream, + free_device, + ..super::MlxProbeCalls::native() + }; + let cases = [ + (native, Ok(vec![11.0]), [3, 1, 1, 1, 1]), + ( + super::MlxProbeCalls { + select_device: refused_device, + ..native + }, + Err("failed to select the MLX CPU device".to_owned()), + [0, 0, 1, 0, 0], + ), + ( + super::MlxProbeCalls { + matmul: refused_matmul, + ..native + }, + Err("MLX CPU matrix product failed".to_owned()), + [3, 1, 1, 0, 0], + ), + ( + super::MlxProbeCalls { + evaluate: refused_evaluation, + ..native + }, + Err("MLX CPU matrix product failed".to_owned()), + [3, 1, 1, 1, 0], + ), + ( + super::MlxProbeCalls { + data: null_data, + ..native + }, + Err("MLX CPU matrix product failed".to_owned()), + [3, 1, 1, 1, 1], + ), + ]; + for (calls, expected, counts) in cases { + COUNTS.with(|counter| counter.set([0; 5])); + assert_eq!( + super::mlx_cpu_matmul_with_calls(&[1.0, 2.0], &[3.0, 4.0], calls), + expected + ); + assert_eq!(COUNTS.with(std::cell::Cell::get), counts); + } + } + #[test] fn emits_only_an_exact_macos_native_mlx_cpu_receipt() { + let _guard = native_test_guard(); 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"); diff --git a/docs/research/mlx-finite-parity-output.md b/docs/research/mlx-finite-parity-output.md new file mode 100644 index 000000000..7b6966295 --- /dev/null +++ b/docs/research/mlx-finite-parity-output.md @@ -0,0 +1,19 @@ +# macOS MLX probe finite parity output + +## Contract and finding + +PRD v0.4 §13 requires measured compute parity and §17 treats numerical divergence as scientific-integrity failure. This correction enforces the existing native MLX CPU matrix-probe contract; it does not change an estimator target, introduce a new GPU backend, or establish scientific GPU parity. ADR 0025 remains authoritative. + +The existing probe reduced absolute differences with `fold(0.0_f64, f64::max)`. Rust documents `f64::max` as ignoring a single NaN operand (Rust Project Developers, n.d.). Consequently, a synthetic backend output `[NaN]` produced `observed_maximum_difference = 0.0` and an accepted receipt. The exact expression was executed with pinned Rust1.98.0, and the extracted production receipt path then failed the permanent rejection assertion. `zip` also truncates unequal lengths: empty/extra output could pass the parity reduction. A separate permanent shape rejection test failed before the guard was added. + +## Repair and verification boundaries + +The real MLX runner now delegates receipt construction to one private `receipt_from_output` boundary. It requires the complete expected scalar output and finite backend values before computing parity or digests. The unchanged finite exact result11 retains its objective/output digests and zero difference; finite mismatch12 fails. Tests cover NaN, positive/negative infinity, empty/extra output, exact finite success, mismatch, and actual native MLX execution. These injected outputs are deterministic boundary fixtures, not observations of the real MLX backend emitting NaN. + +The FFI status/control flow and matching native constructors/frees are preserved through a private call table; production fills it only with the pinned native MLX functions. Test-only call substitutions exercise device-selection, multiplication, evaluation and null-data failure paths against real allocated native handles. Recording wrappers delegate actual frees and assert the expected array/stream/device settlement counts and that evaluation/data calls do not occur after an earlier failure. These synthetic return-code controls cover runner behavior, not an observed native OOM or hardware failure. The ordinary native probe and packaged binary still execute unmodified native function targets. A full workspace run of the initial call-table candidate terminated with SIGTRAP while two native test threads were concurrently creating streams. The retained macOS crash frames identify simultaneous `Scheduler::new_stream` calls; the pinned scheduler implementation mutates its stream/thread vectors without a lock. Four unchanged-source serial/parallel replay processes later passed, so those retries are not credited as a causal repair. A test-only mutex now serializes every native test journey while leaving ordinary Rust tests parallel and production dispatch unchanged. The call-table refactor, cleanup controls and test serialization require a new whole-candidate review rather than reuse of the earlier finite-output-only verdict. The probe is not an Event Lineage estimator, full CPUf64 estimator parity study, Metal/GPU execution receipt, or release evidence. Earlier full-workspace passes precede this source change and must be reacquired. Protected-main adoption and independent exact-head review remain pending. + +## References (APA 7) + +Rust Project Developers. (n.d.). *f64: max*. Rust standard library documentation. Retrieved October 3, 2026, from https://doc.rust-lang.org/std/primitive.f64.html#method.max + +Actual primary-source retrieval: parent `web.run` opened the official documentation and located the `max` section. Its documented NaN behavior is also independently reproduced with the repository's pinned Rust1.98.0 toolchain. Published documentation currently renders standard library1.99.0; it is supporting semantics, not proof of installed compiler identity. From ba0c931871986996261b24879408e262e809d9cd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 9 Nov 2026 17:53:00 +0900 Subject: [PATCH 2/2] Refuse oversized native MLX probe results before reading storage Read the evaluated element count through the private call table and refuse any result other than the single expected scalar before borrowing its storage. A two-element result whose first value equals the exact product was previously copied as Ok([11.0]) and passed parity. Add an oversized-result control that assigns a real two-element native array and asserts every handle is settled without a data read. --- CHANGELOG.d/mlx-finite-parity-output.md | 2 +- crates/mlx_native_receipt/src/main.rs | 36 ++++++++++++++++++++++- docs/research/mlx-finite-parity-output.md | 2 +- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.d/mlx-finite-parity-output.md b/CHANGELOG.d/mlx-finite-parity-output.md index 6a5434043..cbcabd38c 100644 --- a/CHANGELOG.d/mlx-finite-parity-output.md +++ b/CHANGELOG.d/mlx-finite-parity-output.md @@ -2,5 +2,5 @@ - Refuse NaN, either infinity, and incomplete or oversized backend output before parity reduction and output digest construction. A single NaN must not collapse through `f64::max` into an exact-zero difference receipt. - Retain the fixed native MLX CPU matrix objective and byte-identical finite exact-result receipt. Add permanent finite/mismatch/shape/non-finite controls and continue to execute the real native backend probe. -- Exercise the private native-call boundary with device/matmul/evaluation/null-data failure controls and actual native-handle frees; production dispatch stays bound to the pinned native functions. These are injected status controls, not observed native hardware faults. +- Exercise the private native-call boundary with device/matmul/evaluation/oversized-result/null-data failure controls and actual native-handle frees; the runner checks the native element count before reading result storage. Production dispatch stays bound to the pinned native functions. These are injected status controls, not observed native hardware faults. - This is a local candidate correction of ADR0025's existing probe, not a new estimator/GPU parity claim or protected-main release. Research provenance is in `docs/research/mlx-finite-parity-output.md`; current full coverage and independent review remain separately required. diff --git a/crates/mlx_native_receipt/src/main.rs b/crates/mlx_native_receipt/src/main.rs index 7f8d47bf7..4a1ca21f2 100644 --- a/crates/mlx_native_receipt/src/main.rs +++ b/crates/mlx_native_receipt/src/main.rs @@ -65,6 +65,8 @@ struct MlxProbeCalls { ) -> std::ffi::c_int, /// Materialize the result only after successful submission. evaluate: unsafe extern "C" fn(mlx_sys::mlx_array) -> std::ffi::c_int, + /// Read the evaluated element count before borrowing scalar storage. + size: unsafe extern "C" fn(mlx_sys::mlx_array) -> usize, /// Borrow scalar storage only after successful materialization. data: unsafe extern "C" fn(mlx_sys::mlx_array) -> *const f32, /// Settle one owned input or result array. @@ -83,6 +85,7 @@ impl MlxProbeCalls { select_device: mlx_sys::mlx_set_default_device, matmul: mlx_sys::mlx_matmul, evaluate: mlx_sys::mlx_array_eval, + size: mlx_sys::mlx_array_size, data: mlx_sys::mlx_array_data_float32, free_array: mlx_sys::mlx_array_free, free_stream: mlx_sys::mlx_stream_free, @@ -138,7 +141,7 @@ fn mlx_cpu_matmul_with_calls( } else { operation_status }; - let output = if evaluation_status == 0 { + let output = if evaluation_status == 0 && (calls.size)(result) == 1 { let pointer = (calls.data)(result); if pointer.is_null() { None @@ -279,6 +282,29 @@ mod tests { }); } + /// Assign a two-element result whose first value equals the exact product. + unsafe extern "C" fn oversized_matmul( + result: *mut mlx_sys::mlx_array, + _left: mlx_sys::mlx_array, + _right: mlx_sys::mlx_array, + _stream: mlx_sys::mlx_stream, + ) -> std::ffi::c_int { + let values = [11.0_f32, 11.0]; + // SAFETY: the temporary handle is created from Rust-owned data, + // assigned into the runner-owned result and settled once here. + unsafe { + let oversized = mlx_sys::mlx_array_new_data( + values.as_ptr().cast::(), + [1_i32, 2].as_ptr(), + 2, + mlx_sys::mlx_dtype__MLX_FLOAT32, + ); + let status = mlx_sys::mlx_array_set(result, oversized); + let _ = mlx_sys::mlx_array_free(oversized); + status + } + } + #[test] fn native_handles_are_settled_on_every_backend_status_path() { unsafe extern "C" fn free_array(array: mlx_sys::mlx_array) -> std::ffi::c_int { @@ -368,6 +394,14 @@ mod tests { Err("MLX CPU matrix product failed".to_owned()), [3, 1, 1, 1, 1], ), + ( + super::MlxProbeCalls { + matmul: oversized_matmul, + ..native + }, + Err("MLX CPU matrix product failed".to_owned()), + [3, 1, 1, 1, 0], + ), ]; for (calls, expected, counts) in cases { COUNTS.with(|counter| counter.set([0; 5])); diff --git a/docs/research/mlx-finite-parity-output.md b/docs/research/mlx-finite-parity-output.md index 7b6966295..715eff472 100644 --- a/docs/research/mlx-finite-parity-output.md +++ b/docs/research/mlx-finite-parity-output.md @@ -10,7 +10,7 @@ The existing probe reduced absolute differences with `fold(0.0_f64, f64::max)`. The real MLX runner now delegates receipt construction to one private `receipt_from_output` boundary. It requires the complete expected scalar output and finite backend values before computing parity or digests. The unchanged finite exact result11 retains its objective/output digests and zero difference; finite mismatch12 fails. Tests cover NaN, positive/negative infinity, empty/extra output, exact finite success, mismatch, and actual native MLX execution. These injected outputs are deterministic boundary fixtures, not observations of the real MLX backend emitting NaN. -The FFI status/control flow and matching native constructors/frees are preserved through a private call table; production fills it only with the pinned native MLX functions. Test-only call substitutions exercise device-selection, multiplication, evaluation and null-data failure paths against real allocated native handles. Recording wrappers delegate actual frees and assert the expected array/stream/device settlement counts and that evaluation/data calls do not occur after an earlier failure. These synthetic return-code controls cover runner behavior, not an observed native OOM or hardware failure. The ordinary native probe and packaged binary still execute unmodified native function targets. A full workspace run of the initial call-table candidate terminated with SIGTRAP while two native test threads were concurrently creating streams. The retained macOS crash frames identify simultaneous `Scheduler::new_stream` calls; the pinned scheduler implementation mutates its stream/thread vectors without a lock. Four unchanged-source serial/parallel replay processes later passed, so those retries are not credited as a causal repair. A test-only mutex now serializes every native test journey while leaving ordinary Rust tests parallel and production dispatch unchanged. The call-table refactor, cleanup controls and test serialization require a new whole-candidate review rather than reuse of the earlier finite-output-only verdict. The probe is not an Event Lineage estimator, full CPUf64 estimator parity study, Metal/GPU execution receipt, or release evidence. Earlier full-workspace passes precede this source change and must be reacquired. Protected-main adoption and independent exact-head review remain pending. +The FFI status/control flow and matching native constructors/frees are preserved through a private call table; production fills it only with the pinned native MLX functions. After successful evaluation the runner reads the native element count and refuses any result other than the single expected scalar before borrowing its storage. A test-only multiplication target that assigns a two-element result whose first value equals the exact product11 was accepted as `Ok([11.0])` before this check and is now refused with every handle settled. Test-only call substitutions exercise device-selection, multiplication, evaluation, oversized-result and null-data failure paths against real allocated native handles. Recording wrappers delegate actual frees and assert the expected array/stream/device settlement counts and that evaluation/data calls do not occur after an earlier failure. These synthetic return-code controls cover runner behavior, not an observed native OOM or hardware failure. The ordinary native probe and packaged binary still execute unmodified native function targets. A full workspace run of the initial call-table candidate terminated with SIGTRAP while two native test threads were concurrently creating streams. The retained macOS crash frames identify simultaneous `Scheduler::new_stream` calls; the pinned scheduler implementation mutates its stream/thread vectors without a lock. Four unchanged-source serial/parallel replay processes later passed, so those retries are not credited as a causal repair. A test-only mutex now serializes every native test journey while leaving ordinary Rust tests parallel and production dispatch unchanged. The call-table refactor, cleanup controls and test serialization require a new whole-candidate review rather than reuse of the earlier finite-output-only verdict. The probe is not an Event Lineage estimator, full CPUf64 estimator parity study, Metal/GPU execution receipt, or release evidence. Earlier full-workspace passes precede this source change and must be reacquired. Protected-main adoption and independent exact-head review remain pending. ## References (APA 7)