From bf22b3739e3f2819cefc6a346c9637ce23021662 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 19:28:19 +0000 Subject: [PATCH 1/3] foundry-parity-lab: arm C per-store cost via INT8 packed GEMM (WIP) --- crates/foundry-parity-lab/Cargo.toml | 3 + crates/foundry-parity-lab/src/arm_c.rs | 241 ++++++++++++++++++ crates/foundry-parity-lab/src/fixture.rs | 3 + crates/foundry-parity-lab/src/lib.rs | 2 + crates/foundry-parity-lab/src/oracle.rs | 15 ++ crates/foundry-parity-lab/src/source.rs | 17 ++ .../tests/integration/cost.rs | 193 ++++++++++++++ .../tests/integration/main.rs | 1 + 8 files changed, 475 insertions(+) create mode 100644 crates/foundry-parity-lab/src/arm_c.rs create mode 100644 crates/foundry-parity-lab/tests/integration/cost.rs diff --git a/crates/foundry-parity-lab/Cargo.toml b/crates/foundry-parity-lab/Cargo.toml index 15014f8dc..29b7a72c5 100644 --- a/crates/foundry-parity-lab/Cargo.toml +++ b/crates/foundry-parity-lab/Cargo.toml @@ -22,6 +22,9 @@ description = "D-FOUNDRY-PARITY-1 lab: can source-owned planes, a DT64 lane bind lance-graph-dt64 = { path = "../lance-graph-dt64" } lance-graph-mask-risc = { path = "../lance-graph-mask-risc" } lance-graph-contract = { path = "../lance-graph-contract" } +# Arm C: the INT8 packed GEMM (ndarray#418), from the same coordinate +# mask-risc uses, so the graph keeps one ndarray package. +ndarray = { git = "https://github.com/AdaWorldAPI/ndarray", branch = "master", default-features = false, features = ["std"] } [[test]] name = "integration" diff --git a/crates/foundry-parity-lab/src/arm_c.rs b/crates/foundry-parity-lab/src/arm_c.rs new file mode 100644 index 000000000..71b969574 --- /dev/null +++ b/crates/foundry-parity-lab/src/arm_c.rs @@ -0,0 +1,241 @@ +//! ARM C: per-store ingredient cost as one INT8 GEMM (gap G1, partial). +//! +//! `C[store][item] = Σ_k cost[store][k] · demand[k][item]`. A is the cost +//! matrix (stores × categories, `u8`), B the menu's demand matrix (categories +//! × items, `i8`), and `u8 × i8 → i32` is exact here: costs are 50..=249 and +//! demands 1..=3. The product runs through `ndarray::simd::int8_gemm_packed_strided` +//! (AMX `TDPBUSD` where the host has it, the scalar tier otherwise; both exact). +//! +//! What this arm costs, stated rather than hidden: +//! +//! * **A is staged.** The source owns one `i32` lane per category over stores, +//! which is A's transpose, and the kernel reads `u8` rows. So the bound cost +//! lanes are copied into a 64 × 64 `u8` buffer once per binding (4 096 bytes, +//! reported by [`CostQuery::staged_bytes`]). A cost that does not fit `u8` is +//! refused, never truncated. Categories no item demands are not bound; their +//! columns stay zero. +//! * **B is prepared once** per menu revision and reused under the menu's +//! identity and revision only, never a digest (the rule +//! `dr-isa-stream-lab`'s `ReuseKey` applies to its operands, #1992). +//! +//! What it still cannot answer: the limiting resource (an argmin across lanes) +//! is not a matrix product and stays BLOCKED (G1). + +use std::cell::RefCell; + +use lance_graph_contract::class_view::ClassView; +use lance_graph_dt64::dr::FrameStore; +use lance_graph_dt64::{BindStore, LaneBinding, LaneRefusal}; +use lance_graph_mask_risc::{ + execute, tile_words_for, ExecError, LaneRef, MaskOp, Operand, Planes, Pred, Program, Scratch, + Terminal, Value, +}; +use ndarray::simd::{int8_gemm_packed_strided, int8_tile_gemm_tier, PackedI8B}; + +use crate::bind::{COST0, STORE_ID}; +use crate::fixture::{ + self as fx, CATEGORIES, DENIED_STORE, ITEMS, MENU_REV, STORES, STORE_ID_BASE, +}; +use crate::source::{Network, Plane}; + +/// Output columns of the one B band: 8 items, padded to the kernel's 16. +const BAND: usize = 16; + +/// The categories any menu item demands, ascending. Only these are bound. +pub fn demanded_categories() -> Vec { + let mut ks: Vec = (0..ITEMS) + .flat_map(|m| fx::recipe(m).demand.into_iter().map(|(k, _)| k)) + .collect(); + ks.sort_unstable(); + ks.dedup(); + ks +} + +/// The fields arm C binds: the demanded categories' cost lanes and `storeId`. +pub fn cost_fields() -> Vec { + let mut f: Vec = demanded_categories() + .into_iter() + .map(|k| (COST0 + k) as u8) + .collect(); + f.push(STORE_ID as u8); + f +} + +/// Why arm C could not answer. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CostRefusal { + Lane(LaneRefusal), + Exec(ExecError), + /// A cost does not fit the kernel's unsigned 8-bit A operand. + NotU8 { + field: u8, + row: usize, + value: i32, + }, + /// A bound plane had another kind than its field requires. + PlaneKind { + field: u8, + }, + /// `storeId` was not bound. + NoIdentity, + /// The prepared demand band was made for another menu revision. + MenuRevision { + prepared: u32, + asked: u32, + }, + /// `q` outside `1..=128`. + Quantity { + q: i32, + }, +} + +impl From for CostRefusal { + fn from(r: LaneRefusal) -> Self { + CostRefusal::Lane(r) + } +} + +impl From for CostRefusal { + fn from(e: ExecError) -> Self { + CostRefusal::Exec(e) + } +} + +/// The menu's demand matrix, VNNI-packed once. +pub struct PreparedDemand { + menu: u32, + band: [PackedI8B; 1], +} + +impl PreparedDemand { + /// Pack the demand of menu revision `menu` (K = 64 categories × 16 cols). + pub fn prepare(menu: u32) -> PreparedDemand { + let mut b = vec![0i8; CATEGORIES * BAND]; + for m in 0..ITEMS { + for (k, d) in fx::recipe(m).demand { + b[k * BAND + m] = i8::try_from(d).expect("demand fits i8"); + } + } + PreparedDemand { + menu, + band: [PackedI8B::pack(&b, CATEGORIES)], + } + } + + /// Reuse rule: the menu revision must match. Nothing else is consulted. + pub fn admit(&self, menu: u32) -> Result<(), CostRefusal> { + if self.menu == menu { + Ok(()) + } else { + Err(CostRefusal::MenuRevision { + prepared: self.menu, + asked: menu, + }) + } + } +} + +/// Per-store cost over one binding: staged A, the identity lane, the denied mask. +pub struct CostQuery<'s> { + a: Box<[u8]>, + ids: &'s [u32], + denied: u64, + out: RefCell>, +} + +impl<'s> CostQuery<'s> { + /// Check the binding once, borrow every bound lane, stage A, and compute + /// the IAM stand-in's denied mask with one mask-RISC program. + pub fn open( + binding: &LaneBinding, + store: &BindStore, + view: &V, + frames: &FrameStore, + source: &'s Network, + ) -> Result, CostRefusal> { + let mut a = vec![0u8; STORES * CATEGORIES].into_boxed_slice(); + let mut ids = None; + for lane in binding.borrow_all(store, view, frames, source)? { + let (field, plane) = lane?; + match (usize::from(field), plane) { + (STORE_ID, Plane::U32(v)) => ids = Some(v), + (f, Plane::I32(v)) if (COST0..COST0 + CATEGORIES).contains(&f) => { + let k = f - COST0; + for (row, &value) in v.iter().enumerate() { + a[row * CATEGORIES + k] = u8::try_from(value) + .map_err(|_| CostRefusal::NotU8 { field, row, value })?; + } + } + _ => return Err(CostRefusal::PlaneKind { field }), + } + } + let ids = ids.ok_or(CostRefusal::NoIdentity)?; + let lanes = [LaneRef::U32(ids)]; + let planes = Planes { + n_rows: STORES, + masks: &[], + lanes: &lanes, + }; + let program = Program::new( + vec![MaskOp::Pred { + pred: Pred::NeU32 { + lane: 0, + v: DENIED_STORE, + }, + under: None, + dst: 0, + }], + Terminal::Keep { + mask: Operand::Scratch(0), + }, + ); + let mut scratch = Scratch::new(tile_words_for(STORES), 1); + let visible = match execute(&program, &planes, &mut scratch, None)? { + Value::Mask(_) => scratch.slot(0).map_or(0, |s| s[0]), + _ => unreachable!("Keep yields a mask"), + }; + Ok(CostQuery { + a, + ids, + denied: !visible, + out: RefCell::new(vec![0i32; STORES * BAND].into_boxed_slice()), + }) + } + + /// Bytes copied from the source to stage A. + pub fn staged_bytes(&self) -> usize { + self.a.len() + } + + /// Which INT8 tier runs (`"AMX TDPBUSD"` or the scalar reference). + pub fn tier() -> &'static str { + int8_tile_gemm_tier() + } + + /// Ingredient cost of making `q` of every item at every store, indexed + /// `[store row][item]` (store row = id − base). `None` for a store the + /// reader is denied. One GEMM per call. + pub fn per_store( + &self, + demand: &PreparedDemand, + q: i32, + ) -> Result<[[Option; ITEMS]; STORES], CostRefusal> { + if !(1..=crate::arm_b::Q_MAX).contains(&q) { + return Err(CostRefusal::Quantity { q }); + } + demand.admit(MENU_REV)?; + let mut c = self.out.borrow_mut(); + int8_gemm_packed_strided(&self.a, CATEGORIES, &demand.band, &mut c, STORES); + let mut out = [[None; ITEMS]; STORES]; + for r in 0..STORES { + if self.denied >> r & 1 == 1 { + continue; + } + let s = (self.ids[r] - STORE_ID_BASE) as usize; + for m in 0..ITEMS { + out[s][m] = Some(i64::from(q) * i64::from(c[r * BAND + m])); + } + } + Ok(out) + } +} diff --git a/crates/foundry-parity-lab/src/fixture.rs b/crates/foundry-parity-lab/src/fixture.rs index 1dda0131b..a0a01ca3e 100644 --- a/crates/foundry-parity-lab/src/fixture.rs +++ b/crates/foundry-parity-lab/src/fixture.rs @@ -18,6 +18,9 @@ pub const EQUIPMENT: usize = 4; pub const SKILLS: usize = 4; /// Menu items. pub const ITEMS: usize = 8; +/// Revision of the menu (recipes). A prepared demand operand is valid for +/// exactly one revision. +pub const MENU_REV: u32 = 1; /// Global identity of row 0. pub const STORE_ID_BASE: u32 = 1000; diff --git a/crates/foundry-parity-lab/src/lib.rs b/crates/foundry-parity-lab/src/lib.rs index ccd8ee005..d71b6a6f1 100644 --- a/crates/foundry-parity-lab/src/lib.rs +++ b/crates/foundry-parity-lab/src/lib.rs @@ -11,6 +11,7 @@ //! * [`source`] owns the planes and implements DT64's `LaneSource`. //! * [`bind`] is the class: its 211-field basis, route and demanded binding. //! * [`arm_b`] answers through `borrow_all` + mask-RISC programs. +//! * [`arm_c`] answers per-store cost as one INT8 GEMM over staged A. //! * [`arm_a`] is the materialized-object null model. //! * [`oracle`] answers from the fixture alone, and also evaluates the one //! scenario (Level S) the DR path cannot: no overlay contract exists. @@ -20,6 +21,7 @@ pub mod arm_a; pub mod arm_b; +pub mod arm_c; pub mod bind; pub mod fixture; pub mod oracle; diff --git a/crates/foundry-parity-lab/src/oracle.rs b/crates/foundry-parity-lab/src/oracle.rs index 9935502ea..9e68fa6a0 100644 --- a/crates/foundry-parity-lab/src/oracle.rs +++ b/crates/foundry-parity-lab/src/oracle.rs @@ -99,6 +99,21 @@ impl Oracle { .sum() } + /// Ingredient cost of making `q` of `m` at store row `s`, whether or not + /// it can make them; `None` when the reader may not see the store. + pub fn store_cost(&self, s: usize, m: usize, q: i32) -> Option { + if fx::store_id(s) == DENIED_STORE { + return None; + } + Some( + fx::recipe(m) + .demand + .iter() + .map(|&(k, d)| i64::from(d * q) * i64::from(fx::unit_cost(s, k))) + .sum(), + ) + } + /// Ingredient cost of making `q` of `m` at every store that can. pub fn network_cost(&self, m: usize, q: i32) -> i64 { let r = fx::recipe(m); diff --git a/crates/foundry-parity-lab/src/source.rs b/crates/foundry-parity-lab/src/source.rs index 83106431b..58e691a4f 100644 --- a/crates/foundry-parity-lab/src/source.rs +++ b/crates/foundry-parity-lab/src/source.rs @@ -148,6 +148,23 @@ impl Network { }); } + /// A source-side price change at store row `s`, category `k`. The plane + /// changes in place and the frame advances. + pub fn set_unit_cost(&mut self, s: usize, k: usize, value: i32) { + let r = (0..STORES) + .find(|&r| self.order.row(r) == s) + .expect("the store is in the population"); + if let Owned::I32(lane) = &mut self.planes[crate::bind::COST0 + k] { + lane[r] = value; + } + let next = self.frames.revisions().source + 1; + self.frames.advance(FrameRevisions { + source: next, + relation_map: 1, + coordinates: 1, + }); + } + pub fn order(&self) -> Order { self.order } diff --git a/crates/foundry-parity-lab/tests/integration/cost.rs b/crates/foundry-parity-lab/tests/integration/cost.rs new file mode 100644 index 000000000..0f66ed44f --- /dev/null +++ b/crates/foundry-parity-lab/tests/integration/cost.rs @@ -0,0 +1,193 @@ +//! Arm C: per-store ingredient cost as one INT8 GEMM, against the oracle. + +use foundry_parity_lab::arm_b::Guards; +use foundry_parity_lab::arm_c::{ + cost_fields, demanded_categories, CostQuery, CostRefusal, PreparedDemand, +}; +use foundry_parity_lab::bind::{World, READER}; +use foundry_parity_lab::fixture::{case, ITEMS, MENU_REV, STORES}; +use foundry_parity_lab::oracle::Oracle; +use foundry_parity_lab::source::{Network, Order}; +use lance_graph_dt64::LaneSource; + +use crate::common::open; + +fn cost_query<'s>(world: &World, net: &'s Network) -> Result, CostRefusal> { + let binding = world.bind(net, &cost_fields(), net.order().space(), READER)?; + CostQuery::open(&binding, &world.store, &world.view, net.frames(), net) +} + +#[test] +fn per_store_cost_matches_the_oracle_for_every_store_item_and_quantity() { + let world = World::new(); + let net = Network::ingest(Order::Identity); + let demand = PreparedDemand::prepare(MENU_REV); + let query = cost_query(&world, &net).expect("binds and stages"); + let o = Oracle::BASELINE; + for q in [1, 3, 128] { + let c = query.per_store(&demand, q).expect("answers"); + for s in 0..STORES { + for m in 0..ITEMS { + assert_eq!(c[s][m], o.store_cost(s, m, q), "store {s} item {m} q {q}"); + } + } + } + // Anti-vacuity: costs genuinely vary by store, so agreement is informative. + let c = query.per_store(&demand, 1).unwrap(); + let mut distinct: Vec = (0..STORES).filter_map(|s| c[s][0]).collect(); + distinct.sort_unstable(); + distinct.dedup(); + assert!(distinct.len() > 20, "{} distinct costs", distinct.len()); + assert_eq!( + c[case::HIDDEN], + [None; ITEMS], + "the denied store has no cost" + ); + assert_eq!( + (0..STORES).filter(|&s| c[s][0].is_some()).count(), + STORES - 1 + ); +} + +#[test] +fn per_store_cost_sums_to_arm_bs_network_cost_over_feasible_stores() { + // Two independent arms: arm B's MaskedSum over CanCook, arm C's GEMM + // restricted to the same feasibility mask. + let world = World::new(); + let net = Network::ingest(Order::Identity); + let demand = PreparedDemand::prepare(MENU_REV); + let costs = cost_query(&world, &net).unwrap(); + for m in 0..ITEMS { + let cook = open(&world, &net, m, true, false).unwrap(); + for q in [1, 3] { + let mask = cook.cook_mask(q, Guards::ALL).unwrap(); + let c = costs.per_store(&demand, q).unwrap(); + let via_gemm: i64 = (0..STORES) + .filter(|&r| mask >> r & 1 == 1) + .map(|r| c[(cook.store_at(r) - 1000) as usize][m].unwrap()) + .sum(); + assert_eq!(via_gemm, cook.network_cost(q).unwrap(), "item {m} q {q}"); + } + } +} + +#[test] +fn only_demanded_cost_lanes_are_bound_and_a_is_staged_once() { + let world = World::new(); + let net = Network::ingest(Order::Identity); + let fields = cost_fields(); + let query = cost_query(&world, &net).unwrap(); + assert_eq!(fields.len(), demanded_categories().len() + 1); + assert!( + fields.len() < 64 / 2, + "the menu demands a minority of the 64 categories" + ); + assert_eq!( + net.borrows.get(), + fields.len() as u64, + "one borrow per bound lane" + ); + assert_eq!(query.staged_bytes(), 64 * 64); + let demand = PreparedDemand::prepare(MENU_REV); + for q in 1..=5 { + query.per_store(&demand, q).unwrap(); + } + assert_eq!( + net.borrows.get(), + fields.len() as u64, + "answers reuse the staged A" + ); +} + +#[test] +fn a_cost_that_does_not_fit_u8_is_refused_not_truncated() { + let world = World::new(); + let k = demanded_categories()[0]; + let mut net = Network::ingest(Order::Identity); + net.set_unit_cost(10, k, 300); + match cost_query(&world, &net) { + Err(CostRefusal::NotU8 { row, value, .. }) => assert_eq!((row, value), (10, 300)), + other => panic!("expected NotU8, got {:?}", other.err()), + } + // Silent half: the largest u8 is admitted and answered exactly. + let mut net = Network::ingest(Order::Identity); + net.set_unit_cost(10, k, 255); + let c = cost_query(&world, &net) + .expect("255 fits u8") + .per_store(&PreparedDemand::prepare(MENU_REV), 1) + .unwrap(); + let expected: i64 = (0..ITEMS) + .map(|m| { + foundry_parity_lab::fixture::recipe(m) + .demand + .iter() + .map(|&(kk, d)| { + let price = if kk == k { + 255 + } else { + foundry_parity_lab::fixture::unit_cost(10, kk) + }; + i64::from(d) * i64::from(price) + }) + .sum::() + }) + .sum(); + assert_eq!((0..ITEMS).map(|m| c[10][m].unwrap()).sum::(), expected); +} + +#[test] +fn a_stale_binding_is_refused_after_a_price_change() { + let world = World::new(); + let mut net = Network::ingest(Order::Identity); + let binding = world + .bind(&net, &cost_fields(), net.order().space(), READER) + .unwrap(); + net.set_unit_cost(3, demanded_categories()[0], 99); + let r = CostQuery::open(&binding, &world.store, &world.view, net.frames(), &net); + assert!( + matches!( + r, + Err(CostRefusal::Lane(lance_graph_dt64::LaneRefusal::Frame(_))) + ), + "{:?}", + r.err() + ); +} + +#[test] +fn a_demand_band_prepared_for_another_menu_is_refused() { + let world = World::new(); + let net = Network::ingest(Order::Identity); + let query = cost_query(&world, &net).unwrap(); + let other = PreparedDemand::prepare(MENU_REV + 1); + assert_eq!( + query.per_store(&other, 1).err(), + Some(CostRefusal::MenuRevision { + prepared: MENU_REV + 1, + asked: MENU_REV + }) + ); + let demand = PreparedDemand::prepare(MENU_REV); + for q in [0, 129] { + assert_eq!( + query.per_store(&demand, q).err(), + Some(CostRefusal::Quantity { q }) + ); + } +} + +#[test] +fn another_row_order_costs_identically_by_identity() { + let world = World::new(); + let net = Network::ingest(Order::Reversed); + let c = cost_query(&world, &net) + .expect("binds in its own space") + .per_store(&PreparedDemand::prepare(MENU_REV), 2) + .unwrap(); + let o = Oracle::BASELINE; + for s in 0..STORES { + for m in 0..ITEMS { + assert_eq!(c[s][m], o.store_cost(s, m, 2), "store {s} item {m}"); + } + } +} diff --git a/crates/foundry-parity-lab/tests/integration/main.rs b/crates/foundry-parity-lab/tests/integration/main.rs index 6b09db93f..5c54dd5b6 100644 --- a/crates/foundry-parity-lab/tests/integration/main.rs +++ b/crates/foundry-parity-lab/tests/integration/main.rs @@ -3,5 +3,6 @@ #![allow(clippy::needless_range_loop)] pub mod common; pub mod controls; +pub mod cost; pub mod parity; pub mod refusals; From 611b25227a301a4784d100b45049a0697310d7e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 19:32:06 +0000 Subject: [PATCH 2/3] foundry-parity-lab: arm C per-store cost via INT8 packed GEMM (G1, partial) Per-store ingredient cost for every item is one u8 x i8 -> i32 GEMM, C[store][item] = sum_k cost[store][k] * demand[k][item], through ndarray::simd::int8_gemm_packed_strided (ndarray#418; AMX TDPBUSD on the lab host). Only the 23 demanded cost lanes are bound. A is staged once per binding (the source owns A's transpose as i32 lanes; 4096 B, counted); a cost that does not fit u8 is refused. The demand band is packed once and reused only under the same menu revision, the identity-and-revision rule #1992's ReuseKey applies. Tests: oracle and null-model parity over 64 x 8 x q in {1,3,128}; the sum over arm B's CanCook mask equals arm B's network_cost; u8 overflow refused, 255 admitted; stale binding, foreign menu revision and out-of-range q refused; reversed order answers by identity. Five disable runs turn the suite red. The limiting resource (argmin) stays blocked. Plan G1/row 14/H2/roadmap updated; the first arm A comparator allocated per call and is replaced by a fair one. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XAqKMPJjVvasjwqhjJ6c9p --- .claude/plans/foundry-parity-v1.md | 55 ++++++++++++++++--- crates/foundry-parity-lab/src/arm_a.rs | 35 ++++++++++++ crates/foundry-parity-lab/src/bin/bench.rs | 40 +++++++++++++- .../tests/integration/cost.rs | 9 +-- 4 files changed, 127 insertions(+), 12 deletions(-) diff --git a/.claude/plans/foundry-parity-v1.md b/.claude/plans/foundry-parity-v1.md index 0ebdcffa0..f9ba08321 100644 --- a/.claude/plans/foundry-parity-v1.md +++ b/.claude/plans/foundry-parity-v1.md @@ -30,7 +30,8 @@ instance; Foundry columns cite its public documentation only, as evidence of - [x] Named cases 01–09, refusals, negative controls (§5). - [x] Disable runs: five guards broken one at a time, suite red each time, tree restored. - [x] Bench in ns/µs (§6). -- [ ] P1: element-wise cross-lane arithmetic in an admitted operator (per-store max, limiting resource, per-store cost) — see §7 G1. +- [x] P1a (2026-11-10): per-store cost through the INT8 packed GEMM (ndarray#418), arm C — oracle-exact, staged A counted (§5a, §6). +- [ ] P1b: element-wise cross-lane arithmetic in an admitted operator (per-store max without the sweep, limiting resource) — see §7 G1. - [ ] P3: scenario overlay frame (§7 G3); governed action / writeback gate (§7 G4). - [ ] Production IAM in the read path instead of the lab stand-in (§7 G2). @@ -93,7 +94,7 @@ compared against Foundry. | 11 | History / replay | `FrameStore` revisions; Lance versions | lab stale-frame refusal + rebind | LAB (invalidation) / UNKNOWN (replay at a past version through DT64) | Bind at a historical frame | Answer at frame N after the source moved to N+1 | PARTIAL | | 12 | Derived state + invalidation | `LaneBinding::check` on every open | lab `a_stale_frame_is_refused_and_a_rebind_sees_the_restock` | LAB | Incremental recompute of only the changed subset (lab recomputes the whole question; it is 64 rows) | — | FUNCTIONALLY-RELATED | | 13 | What-if scenarios (beta in Foundry docs) | `contract::scenario::ScenarioBranch` (facade) | `lance-graph-contract/src/scenario.rs` | PARTIAL; DR path BLOCKED | Overlay frame witness for DT64 bindings (§7 G3) | lab `the_scenario_is_oracle_only_and_lowers_capacity` (oracle only) | BLOCKED | -| 14 | Aggregations / analytics | mask-RISC `Count`, `MaskedSumI32`, group terminals | lab `network_capacity`, `network_cost` | LAB | Per-row cross-lane arithmetic (§7 G1) | Per-store cost through an admitted terminal | PARTIAL | +| 14 | Aggregations / analytics | mask-RISC `Count`, `MaskedSumI32`, group terminals; ndarray INT8 packed GEMM | lab `network_capacity`, `network_cost`; arm C `per_store` (per-store cost) | LAB | Per-row min / argmin across lanes (§7 G1) | Limiting resource through an admitted operator | PARTIAL | | 15 | Operational applications | a2ui-rs / consumer CustomObject | not exercised here | UNKNOWN | — | — | NOT-COMPARABLE here | | 16 | Audit / decision provenance | Witnesses (route, frame, basis) per answer | `LaneBinding` fields | PARTIAL | Decision record: who asked, which witnesses, which answer | — | MISSING | | 17 | Data connections / egress | Source adapters (OGAR, Lance) | outside this lab | UNKNOWN | — | — | NOT-COMPARABLE here | @@ -126,6 +127,31 @@ lanes); quantity outside `1..=128`. Disable runs (each red, then restored): staff predicate threshold → 0; denied mask → empty; ingredient threshold off by one q; unknown mask → empty; fulfil deadline → `i32::MAX`. +### 5a. Arm C — per-store cost as one INT8 GEMM (P1a) + +`C[store][item] = Σ_k cost[store][k] · demand[k][item]` through +`ndarray::simd::int8_gemm_packed_strided` (ndarray#418, AMX `TDPBUSD` on the +lab host). Only the cost lanes of categories some item demands are bound (plus +`storeId`). **A is staged**: the source owns one `i32` lane per category (A's +transpose) and the kernel reads `u8` rows, so the bound lanes are copied once +per binding into a 64 × 64 `u8` buffer (4 096 B, counted). A cost that does not +fit `u8` is refused, never truncated. B (the menu's demand matrix) is packed +once and reused only under the same menu revision — the identity-and-revision +rule #1992's `ReuseKey` applies to its operands, never a digest. + +Tests (7): oracle parity for 64 stores × 8 items × q ∈ {1, 3, 128} and against +the materialized null model; the sum over arm B's `CanCook` mask equals arm B's +`network_cost` (two independent arms); demanded lanes only, one borrow each, A +staged once; `u8` overflow refused and 255 admitted exactly; a stale binding +refused after a price change; another menu revision and out-of-range q +refused; reversed row order answers by identity. Disable runs (each red, then +restored): truncate instead of refuse; ignore the denied mask; demand in the +wrong column; admit any menu; stage only the first cost lane. + +Not exercised: the scalar tier of the kernel (the host has AMX); ndarray#418 +tests both tiers against each other. The limiting resource (argmin) remains +BLOCKED — it is not a matrix product. + Not exercised: the `NotMonotone` and `Ceiling` guards of the layer-cake sweep are defensive; no admitted fixture can reach them (feasibility predicates are all `≥ threshold(q)` with thresholds non-decreasing in q). They are labelled, @@ -150,6 +176,11 @@ three runs, ranges across runs: | arm A: build 64 objects (51 968 B second copy) | 20 818–23 216 | | arm A: `CanCook(q=1)` over 64 objects | 1 792–1 844 | | arm A: `NetworkCapacity` | 1 811–1 922 | +| arm C cold: bind 23 cost lanes + `storeId` | 5 567–5 953 | +| arm C cold: open (`borrow_all` + stage A, 4 096 B) | 1 139–1 562 | +| arm C cold: prepare demand band (once per menu revision) | 452–629 | +| arm C warm: per-store cost, 64 stores × 8 items, AMX `TDPBUSD` | 654–1 170 | +| arm A: per-store cost, 64 × 8 (recipes read once) | 1 188–1 661 | Reading, not spin: a single WHERE-shaped question is ~4× cheaper in arm B and copies nothing; but the per-store maximum and the network capacity are ~8× @@ -159,11 +190,18 @@ A's build cost (21 µs) amortises after roughly two capacity questions. At 64 rows every mask is one word, so mask-RISC's fixed per-program cost (program construction + validation, not separated here) dominates; scaling is OPEN. +Arm C (separate run of the same bench, three runs): per-store cost for all 8 +items is ~1.5–1.8× cheaper than arm A's walk once arm A reads its recipes +once. A first comparator called `fx::recipe` per (store, item) and measured +7.7–15 µs; that was 512 allocations, not the objects, and is not the +comparison. At 64 × 64 × 16 the GEMM is tiny; the per-call fixed cost +(tile configuration, output mapping) likely dominates, and scaling is OPEN. + ## 7. Missing-contract register | Id | Missing operation | Why the current contract cannot express it | Lawful alternative today | Smallest test | Needs | |---|---|---|---|---|---| -| G1 | Element-wise cross-lane arithmetic: `min_k ⌊stock_k / d_k⌋` per row, argmin (limiting resource), per-row weighted sum (per-store cost) | mask-RISC terminals reduce over rows; predicates compare a lane to a constant | Layer-cake over monotone masks (max only, ~30× programs); no alternative for argmin or per-row cost | Per-store max and cost in one program, equal to oracle | Decision + mask-RISC/Quack PR (H2) | +| G1 | Element-wise cross-lane arithmetic: `min_k ⌊stock_k / d_k⌋` per row, argmin (limiting resource). (Per-row weighted sum — per-store cost — is now LAB via arm C, §5a.) | mask-RISC terminals reduce over rows; predicates compare a lane to a constant; min/argmin is not a matrix product | Layer-cake over monotone masks (max only, ~30× programs); none for argmin | Per-store max and limiting resource in one program, equal to oracle | Decision + mask-RISC/Quack PR (H2) | | G2 | Row-level authorization from IAM in the read path | DR `Scope` is per lane and consistency-only; RBAC grants are class/op, not wired to DT64 | Lab stand-in predicate on the identity lane | Same lab tests with the predicate supplied by `lance-graph-rbac` | Decision on where IAM evaluates (H3) | | G3 | Scenario overlay over a binding | No frame witness for a proposed, non-canonical plane | Oracle only | Store 12 closed: overlay answer = oracle, canonical frame unchanged | Overlay frame contract (H3) | | G4 | Governed action / writeback | No command identity, preconditions, idempotency, atomic commit or audit on this path; `MethodBinding::effects` is descriptive | none | Preview vs commit of "reassign order 12→17" with a stale-precondition refusal | Separate gate; not in a read-only lab | @@ -176,8 +214,9 @@ construction + validation, not separated here) dominates; scaling is OPEN. WHERE-shaped feasibility, fulfilment, capacity and network cost are exact through existing contracts with no copy. Falsified for anything needing a per-row value combining several lanes. -- **H2 (missing algebra):** *supported by G1, G5.* The limiting resource and - per-store cost cannot be expressed; the maximum only by a costly sweep. +- **H2 (missing algebra):** *supported by G1, G5, narrowed by arm C.* The + per-store cost IS expressible, as an INT8 GEMM over staged A (§5a). The + limiting resource still cannot be expressed; the maximum only by a sweep. - **H3 (infrastructure):** *supported by G2, G3, G4, G6* but untested in depth here: authorization, scenarios, actions and method execution all lack a contract on this path. @@ -187,8 +226,10 @@ None is chosen over the others; all three hold in part. ## 9. Roadmap - **P0 (this PR):** the lab and this matrix. -- **P1:** G1 — decide whether element-wise cross-lane arithmetic enters - mask-RISC/Quack, then re-run the per-store questions against the oracle. +- **P1a (done, 2026-11-10):** per-store cost via the INT8 packed GEMM (arm C). +- **P1b:** G1 remainder — decide whether element-wise cross-lane min/argmin + enters mask-RISC/Quack, then re-run per-store max and limiting resource + against the oracle. - **P2:** G6 + G5 — `CanCook` as a registered method lowered from its `MethodRef`; recipe demand as bound relation lanes. - **P3:** G3 scenario overlay frame; G4 action gate (preview vs commit) as a diff --git a/crates/foundry-parity-lab/src/arm_a.rs b/crates/foundry-parity-lab/src/arm_a.rs index be4cc925e..1ae28bca6 100644 --- a/crates/foundry-parity-lab/src/arm_a.rs +++ b/crates/foundry-parity-lab/src/arm_a.rs @@ -100,6 +100,41 @@ impl Objects { } } + /// Ingredient cost of `q` of `m` at one object; `None` if denied. + pub fn store_cost(&self, s: usize, m: usize, q: i32) -> Option { + let o = &self.stores[s]; + if o.id == DENIED_STORE { + return None; + } + Some( + fx::recipe(m) + .demand + .iter() + .map(|&(k, d)| i64::from(d * q) * i64::from(o.cost[k])) + .sum(), + ) + } + + /// Every store's cost for every item at `q`, recipes read once. + pub fn store_costs(&self, q: i32) -> [[Option; ITEMS]; STORES] { + let recipes: Vec<_> = (0..ITEMS).map(fx::recipe).collect(); + let mut out = [[None; ITEMS]; STORES]; + for (s, o) in self.stores.iter().enumerate() { + if o.id == DENIED_STORE { + continue; + } + for (m, r) in recipes.iter().enumerate() { + out[s][m] = Some( + r.demand + .iter() + .map(|&(k, d)| i64::from(d * q) * i64::from(o.cost[k])) + .sum(), + ); + } + } + out + } + /// `NetworkCapacity` on the objects. pub fn network_capacity(&self, m: usize) -> i64 { (0..STORES) diff --git a/crates/foundry-parity-lab/src/bin/bench.rs b/crates/foundry-parity-lab/src/bin/bench.rs index 71ddbecf7..bc9b06891 100644 --- a/crates/foundry-parity-lab/src/bin/bench.rs +++ b/crates/foundry-parity-lab/src/bin/bench.rs @@ -7,8 +7,9 @@ use std::time::Instant; use foundry_parity_lab::arm_a::Objects; use foundry_parity_lab::arm_b::{demanded_fields, Guards, ItemQuery}; +use foundry_parity_lab::arm_c::{cost_fields, CostQuery, PreparedDemand}; use foundry_parity_lab::bind::{World, READER, SPACE_A}; -use foundry_parity_lab::fixture::STORES; +use foundry_parity_lab::fixture::{MENU_REV, STORES}; use foundry_parity_lab::source::{Network, Order}; use lance_graph_dt64::LaneSource; @@ -105,6 +106,37 @@ fn main() { }), ); println!(); + println!("arm C tier: {}", CostQuery::tier()); + let cf = cost_fields(); + report( + "arm C cold: bind cost lanes", + median(|| { + black_box(world.bind(&net, &cf, SPACE_A, READER).unwrap()); + }), + ); + let cb = world.bind(&net, &cf, SPACE_A, READER).unwrap(); + report( + "arm C cold: open (borrow_all + stage A)", + median(|| { + black_box(CostQuery::open(&cb, &world.store, &world.view, net.frames(), &net).unwrap()); + }), + ); + report( + "arm C cold: prepare demand band", + median(|| { + black_box(PreparedDemand::prepare(MENU_REV)); + }), + ); + let cq = CostQuery::open(&cb, &world.store, &world.view, net.frames(), &net).unwrap(); + let demand = PreparedDemand::prepare(MENU_REV); + println!("arm C staged bytes {}", cq.staged_bytes()); + report( + "arm C warm: per-store cost, 64 x 8 items", + median(|| { + black_box(cq.per_store(&demand, black_box(3)).unwrap()); + }), + ); + println!(); report( "arm A: build 64 objects", median(|| { @@ -126,4 +158,10 @@ fn main() { black_box(objects.network_capacity(0)); }), ); + report( + "arm A: per-store cost, 64 x 8 items", + median(|| { + black_box(objects.store_costs(black_box(3))); + }), + ); } diff --git a/crates/foundry-parity-lab/tests/integration/cost.rs b/crates/foundry-parity-lab/tests/integration/cost.rs index 0f66ed44f..fe3fead89 100644 --- a/crates/foundry-parity-lab/tests/integration/cost.rs +++ b/crates/foundry-parity-lab/tests/integration/cost.rs @@ -47,6 +47,9 @@ fn per_store_cost_matches_the_oracle_for_every_store_item_and_quantity() { (0..STORES).filter(|&s| c[s][0].is_some()).count(), STORES - 1 ); + // The materialized null model agrees too. + let objects = foundry_parity_lab::arm_a::Objects::build(false); + assert_eq!(objects.store_costs(1), c); } #[test] @@ -78,10 +81,8 @@ fn only_demanded_cost_lanes_are_bound_and_a_is_staged_once() { let fields = cost_fields(); let query = cost_query(&world, &net).unwrap(); assert_eq!(fields.len(), demanded_categories().len() + 1); - assert!( - fields.len() < 64 / 2, - "the menu demands a minority of the 64 categories" - ); + // 23 of the 64 categories are demanded by some item, plus `storeId`. + assert_eq!(fields.len(), 24); assert_eq!( net.borrows.get(), fields.len() as u64, From 288f130227b280205ae296ade1bacb4713e7cee7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 19:44:47 +0000 Subject: [PATCH 3/3] foundry-parity-lab: arm C refuses a binding missing a demanded cost lane borrow_all validates only the fields a binding carries, so a binding that omitted a demanded cost lane opened and left that column of A zero, understating every cost (all zero for an identity-only binding). open now tracks which demanded categories were bound and refuses with NotBound, matching arm B. The test drops one lane and binds identity only (both refused) and keeps the full binding as the silent half. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XAqKMPJjVvasjwqhjJ6c9p --- crates/foundry-parity-lab/src/arm_c.rs | 13 +++++++++ .../tests/integration/cost.rs | 27 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/crates/foundry-parity-lab/src/arm_c.rs b/crates/foundry-parity-lab/src/arm_c.rs index 71b969574..808799a81 100644 --- a/crates/foundry-parity-lab/src/arm_c.rs +++ b/crates/foundry-parity-lab/src/arm_c.rs @@ -78,6 +78,10 @@ pub enum CostRefusal { }, /// `storeId` was not bound. NoIdentity, + /// A demanded cost lane was not in the binding; its column would read as zero. + NotBound { + field: u8, + }, /// The prepared demand band was made for another menu revision. MenuRevision { prepared: u32, @@ -155,12 +159,14 @@ impl<'s> CostQuery<'s> { ) -> Result, CostRefusal> { let mut a = vec![0u8; STORES * CATEGORIES].into_boxed_slice(); let mut ids = None; + let mut bound = [false; CATEGORIES]; for lane in binding.borrow_all(store, view, frames, source)? { let (field, plane) = lane?; match (usize::from(field), plane) { (STORE_ID, Plane::U32(v)) => ids = Some(v), (f, Plane::I32(v)) if (COST0..COST0 + CATEGORIES).contains(&f) => { let k = f - COST0; + bound[k] = true; for (row, &value) in v.iter().enumerate() { a[row * CATEGORIES + k] = u8::try_from(value) .map_err(|_| CostRefusal::NotU8 { field, row, value })?; @@ -170,6 +176,13 @@ impl<'s> CostQuery<'s> { } } let ids = ids.ok_or(CostRefusal::NoIdentity)?; + // `borrow_all` checks only the fields the binding carries; an omitted + // demanded lane would leave its column of A zero and understate cost. + if let Some(k) = demanded_categories().into_iter().find(|&k| !bound[k]) { + return Err(CostRefusal::NotBound { + field: (COST0 + k) as u8, + }); + } let lanes = [LaneRef::U32(ids)]; let planes = Planes { n_rows: STORES, diff --git a/crates/foundry-parity-lab/tests/integration/cost.rs b/crates/foundry-parity-lab/tests/integration/cost.rs index fe3fead89..26c691936 100644 --- a/crates/foundry-parity-lab/tests/integration/cost.rs +++ b/crates/foundry-parity-lab/tests/integration/cost.rs @@ -136,6 +136,33 @@ fn a_cost_that_does_not_fit_u8_is_refused_not_truncated() { assert_eq!((0..ITEMS).map(|m| c[10][m].unwrap()).sum::(), expected); } +#[test] +fn a_binding_missing_a_demanded_cost_lane_is_refused() { + let world = World::new(); + let net = Network::ingest(Order::Identity); + let all = cost_fields(); + let dropped = all[0]; + let partial: Vec = all.iter().copied().filter(|&f| f != dropped).collect(); + let binding = world + .bind(&net, &partial, net.order().space(), READER) + .expect("a partial binding is itself valid"); + match CostQuery::open(&binding, &world.store, &world.view, net.frames(), &net) { + Err(CostRefusal::NotBound { field }) => assert_eq!(field, dropped), + other => panic!("expected NotBound, got {:?}", other.err()), + } + // Identity-only binding: refused, never answered as all-zero costs. + let ids_only = [*all.last().unwrap()]; + let binding = world + .bind(&net, &ids_only, net.order().space(), READER) + .unwrap(); + assert!(matches!( + CostQuery::open(&binding, &world.store, &world.view, net.frames(), &net), + Err(CostRefusal::NotBound { .. }) + )); + // Silent half: the full binding still opens. + assert!(cost_query(&world, &net).is_ok()); +} + #[test] fn a_stale_binding_is_refused_after_a_price_change() { let world = World::new();